Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 54 additions & 8 deletions app/assets/javascripts/osem-switch.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,61 @@
function checkboxSwitch(selector){
$(selector).bootstrapSwitch();

$(selector).on('switchChange.bootstrapSwitch', function(event, state) {
var url = $(this).attr('url') + state;
var method = $(this).attr('method') || 'patch';
// Prevent duplicated event handlers when the page is re-rendered.
$(selector).off('switchChange.bootstrapSwitch');
$(selector).off('.osemSwitchGuard');

$.ajax({
url: url,
type: method,
dataType: 'script'
});
// Mark as user-initiated before bootstrapSwitch triggers switchChange.
// Important: bootstrapSwitch often binds clicks on its generated wrapper/label,
// so we must listen on those too (not only on the hidden checkbox input).
$(selector).each(function() {
var $input = $(this);
$input.data('osem-user-toggle', false);

var $wrapper = $input.closest('.bootstrap-switch');
if ($wrapper.length === 0) {
$wrapper = $input.parent();
}

$wrapper.off('click.osemSwitchGuard mouseup.osemSwitchGuard touchend.osemSwitchGuard pointerup.osemSwitchGuard');
$wrapper.on(
'click.osemSwitchGuard mouseup.osemSwitchGuard touchend.osemSwitchGuard pointerup.osemSwitchGuard',
function() {
$input.data('osem-user-toggle', true);
}
);
});

$(selector).on('switchChange.bootstrapSwitch', function(_event, state) {
var $el = $(this);
if (!$el.data('osem-user-toggle')) {
return;
}

// bootstrapSwitch can emit multiple switchChange events per user click.
// Delay the request slightly, then read the final checkbox state to send once.
var existingTimer = $el.data('osem-user-toggle-timer');
if (existingTimer) {
clearTimeout(existingTimer);
}

var method = $el.attr('method') || 'patch';
var urlBase = $el.attr('url');

var timer = setTimeout(function() {
$el.data('osem-user-toggle', false);

var checked = $el.is(':checked');
var url = urlBase + (checked ? 'true' : 'false');

$.ajax({
url: url,
type: method,
dataType: 'script'
});
}, 180);

$el.data('osem-user-toggle-timer', timer);
});
}

Expand Down
45 changes: 44 additions & 1 deletion app/controllers/admin/conferences_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,19 +67,62 @@ def index
end

def new
@conference = Conference.new
if params[:duplicate_from].present?
source = Conference.find_by(short_title: params[:duplicate_from])
if source && can?(:read, source)
@conference = Conference.new(
description: source.description,
timezone: source.timezone,
start_hour: source.start_hour,
end_hour: source.end_hour,
color: source.color,
custom_css: source.custom_css,
ticket_layout: source.ticket_layout,
registration_limit: source.registration_limit,
booth_limit: source.booth_limit,
organization_id: source.organization_id
)
@duplicate_from_source = source.short_title
else
@conference = Conference.new
end
else
@conference = Conference.new
end
end

def create
@conference = Conference.new(conference_params)

if params[:duplicate_from].present?
source = Conference.find_by(short_title: params[:duplicate_from])
if source && can?(:read, source)
@conference.assign_attributes(
description: source.description,
custom_css: source.custom_css,
ticket_layout: source.ticket_layout,
registration_limit: source.registration_limit,
booth_limit: source.booth_limit,
color: source.color,
start_hour: source.start_hour,
end_hour: source.end_hour
)
end
end

if @conference.save
# user that creates the conference becomes organizer of that conference
current_user.add_role :organizer, @conference

if params[:duplicate_from].present?
source = Conference.find_by(short_title: params[:duplicate_from])
@conference.copy_associations_from(source) if source && can?(:read, source)
end

redirect_to admin_conference_path(id: @conference.short_title),
notice: 'Conference was successfully created.'
else
@duplicate_from_source = params[:duplicate_from]
flash.now[:error] = 'Could not create conference. ' + @conference.errors.full_messages.to_sentence
render action: 'new'
end
Expand Down
32 changes: 30 additions & 2 deletions app/controllers/admin/roles_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ class RolesController < Admin::BaseController
before_action :set_selection
authorize_resource :role, except: :index
# Show flash message with ajax calls
after_action :prepare_unobtrusive_flash, only: :toggle_user
after_action :prepare_unobtrusive_flash, only: %i[toggle_user toggle_comment_notifications]

def index
@roles = Role.where(resource: @conference)
Expand All @@ -21,7 +21,11 @@ def show
else
toggle_user_admin_conference_role_path(@conference.short_title, @role.name)
end
@users = @role.users
@users_roles = UsersRole.where(role: @role).includes(:user)
@comment_notifications_url =
if @track.nil?
toggle_comment_notifications_admin_conference_role_path(@conference.short_title, @role.name)
end
end

def edit
Expand Down Expand Up @@ -103,6 +107,30 @@ def toggle_user
end
end

def toggle_comment_notifications
user = User.find_by(email: user_params[:email])
state = user_params[:state]

redirect_url = admin_conference_role_path(@conference.short_title, @role.name)
unless user
redirect_to redirect_url, error: 'Could not find user. Please provide a valid email!' and return
end

users_role = UsersRole.find_by(user: user, role: @role)
unless users_role
redirect_to redirect_url, error: 'Could not find organizer setting for this user.' and return
end

# Be tolerant to different representations coming from the client (e.g. "true", "1", true).
email_notifications = ActiveModel::Type::Boolean.new.cast(state)
users_role.update!(email_notifications: email_notifications)

respond_to do |format|
format.js
format.html { redirect_to redirect_url, notice: 'Successfully updated notification setting.' }
end
end

protected

def set_selection
Expand Down
2 changes: 1 addition & 1 deletion app/models/admin_ability.rb
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ def signed_in_with_organizer_role(user, conf_ids_for_organization_admin = [])
role.resource_type == 'Conference' || role.resource_type == 'Track'
end

can [:edit, :update, :toggle_user], Role do |role|
can [:edit, :update, :toggle_user, :toggle_comment_notifications], Role do |role|
(role.resource_type == 'Conference' && (conf_ids.include? role.resource_id)) ||
(role.resource_type == 'Track' && (track_ids.include? role.resource_id))
end
Expand Down
82 changes: 82 additions & 0 deletions app/models/conference.rb
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,88 @@ def ended?
end_date < Time.current
end

##
# Copies associations from another conference (for duplication).
# Includes: registration_period, email_settings, venue+rooms, tickets,
# event_types, tracks, difficulty_levels, sponsorship_levels, sponsors.
# Excludes: events, registrations, and other user/attendee data.
#
def copy_associations_from(source)
return unless source && source != self

# Registration period (clamp to new conference dates)
if source.registration_period.present?
rp = source.registration_period
start_d = [rp.start_date, start_date].max
end_d = [rp.end_date, end_date].min
start_d = end_d = start_date if start_d > end_d
create_registration_period!(start_date: start_d, end_date: end_d)
end

# Email settings (conference already has one from create_email_settings)
if source.email_settings.present? && email_settings.present?
attrs = source.email_settings.attributes.except('id', 'conference_id', 'created_at', 'updated_at')
email_settings.update!(attrs)
end

# Venue and rooms (map old room id -> new room for tracks later)
room_id_map = {}
if source.venue.present?
new_venue = create_venue!(
source.venue.attributes.slice('name', 'street', 'city', 'country', 'description', 'postalcode', 'website', 'latitude', 'longitude')
)
source.venue.rooms.order(:id).each_with_index do |old_room, _idx|
new_room = new_venue.rooms.create!(
old_room.attributes.slice('name', 'size', 'order').merge(guid: SecureRandom.urlsafe_base64)
)
room_id_map[old_room.id] = new_room.id
end
end

# Tickets (conference already has one free ticket from create_free_ticket; skip source's free to avoid duplicate)
source.tickets.each do |t|
next if t.title == 'Free Access' && t.price_cents.zero?

tickets.create!(
t.attributes.slice('title', 'description', 'price_cents', 'price_currency', 'registration_ticket', 'visible', 'email_subject', 'email_body')
)
end

# Event types and difficulty levels (program exists from after_create)
source.program&.event_types&.each do |et|
program.event_types.create!(
et.attributes.slice('title', 'length', 'color', 'description', 'minimum_abstract_length', 'maximum_abstract_length', 'submission_template', 'enable_public_submission')
)
end
source.program&.difficulty_levels&.each do |dl|
program.difficulty_levels.create!(
dl.attributes.slice('title', 'description', 'color')
)
end

# Tracks (assign new room by same index, or nil if no room)
source.program&.tracks&.each do |t|
old_room_id = t.room_id
new_room_id = old_room_id ? room_id_map[old_room_id] : nil
program.tracks.create!(
t.attributes.slice('name', 'short_name', 'description', 'color', 'state', 'relevance', 'start_date', 'end_date', 'cfp_active').merge(
guid: SecureRandom.urlsafe_base64,
room_id: new_room_id
)
)
end

# Sponsorship levels and sponsors
source.sponsorship_levels.each do |sl|
new_sl = sponsorship_levels.create!(sl.attributes.slice('title', 'position'))
sl.sponsors.each do |sp|
new_sl.sponsors.create!(
sp.attributes.slice('name', 'description', 'website_url')
)
end
end
end

private

# Returns a different html colour for every i and consecutive colors are
Expand Down
15 changes: 14 additions & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,20 @@ def for_registration(conference)

# add scope
scope :comment_notifiable, lambda { |conference|
joins(:roles).where('roles.name IN (?)', %i[organizer cfp]).where('roles.resource_type = ? AND roles.resource_id = ?', 'Conference', conference.id)
joins(users_roles: :role)
.where(roles: { resource_type: 'Conference',
resource_id: conference.id,
name: %i[organizer cfp] })
.where(
Role.arel_table[:name]
.eq('cfp')
.or(
Role.arel_table[:name]
.eq('organizer')
.and(UsersRole.arel_table[:email_notifications].eq(true))
)
)
.distinct
}

# scopes for user distributions
Expand Down
7 changes: 4 additions & 3 deletions app/models/users_role.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
#
# Table name: users_roles
#
# id :bigint not null, primary key
# role_id :integer
# user_id :integer
# id :bigint not null, primary key
# email_notifications :boolean default(TRUE), not null
# role_id :integer
# user_id :integer
#
# Indexes
#
Expand Down
6 changes: 6 additions & 0 deletions app/views/admin/conferences/new.html.haml
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
- if @duplicate_from_source
.row
.col-md-8
.alert.alert-info
Duplicating from an existing conference. Please set Title, Short title, and Dates for the new conference.
.row
.col-md-8
= form_for(@conference, url: admin_conferences_path) do |f|
= hidden_field_tag :duplicate_from, @duplicate_from_source if @duplicate_from_source
= render partial: 'form_fields', locals: { f: f }
1 change: 1 addition & 0 deletions app/views/admin/conferences/show.html.haml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
%h1
%span.fa-solid.fa-gauge-high
Dashboard for #{@conference.title}
= link_to 'Duplicate', new_admin_conference_path(duplicate_from: @conference.short_title), class: 'btn btn-primary btn-sm', style: 'margin-left: 12px;'
%hr
.row
.col-sm-3.col-xs-6
Expand Down
17 changes: 13 additions & 4 deletions app/views/admin/roles/_users.html.haml
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
.page-header
%h3 Users (#{users.length})
- if users.present?
%h3 Users (#{users_roles.length})
- if users_roles.present?
%table.datatable#users
%thead
- if ( can? :toggle_user, @role )
%th.col-md-1
%th ID
%th Name
%th Email
- if @role.name == 'organizer' && can?(:toggle_user, @role)
%th Email notifications
%tbody
- users.each do |user|
- users_roles.each do |users_role|
- user = users_role.user
%tr
- if ( can? :toggle_user, @role )
%td.text-right
= hidden_field_tag "role[user_ids][]", nil
= check_box_tag @conference.short_title, @role.id, (@role.user_ids.include? user.id), url: "#{@url}?user[email]=#{user.email}&user[state]=", method: :post, class: 'switch-checkbox'
= check_box_tag @conference.short_title, @role.id, true, url: "#{@url}?user[email]=#{user.email}&user[state]=", method: :post, class: 'switch-checkbox'
%td= user.id
%td= user.name
%td= user.email
- if @role.name == 'organizer' && can?(:toggle_user, @role)
%td
= check_box_tag 'email_notifications', users_role.id, users_role.email_notifications,
url: "#{@comment_notifications_url}?user[email]=#{user.email}&user[state]=",
method: :post,
class: 'switch-checkbox'
- else
%h5 No users found!
2 changes: 1 addition & 1 deletion app/views/admin/roles/show.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@
.row
.col-md-12
#users_area
= render partial: 'users', locals: { users: @users }
= render partial: 'users', locals: { users_roles: @users_roles }
1 change: 1 addition & 0 deletions app/views/admin/roles/toggle_comment_notifications.js.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
$(".alert").remove();
1 change: 1 addition & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@
resources :roles, except: %i[new create] do
member do
post :toggle_user
post :toggle_comment_notifications
end
end

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddEmailNotificationsToUsersRoles < ActiveRecord::Migration[7.0]
def change
add_column :users_roles, :email_notifications, :boolean, default: true, null: false
end
end
Loading
Loading