Skip to content
Closed
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
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ gem 'cloudinary'

# for internationalizing
gem 'rails-i18n'
# Windows: timezone data (required on Windows for tzinfo)
gem 'tzinfo-data', platforms: %i[ windows jruby ]

# as authentification framework
gem 'devise'
Expand Down
21 changes: 8 additions & 13 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -254,8 +254,7 @@ GEM
faraday (~> 2.0)
fastimage (2.3.0)
feature (1.4.0)
ffi (1.17.0-arm64-darwin)
ffi (1.17.0-x86_64-darwin)
ffi (1.17.0-x64-mingw-ucrt)
ffi (1.17.0-x86_64-linux-gnu)
font-awesome-sass (6.5.1)
sassc (~> 2.0)
Expand Down Expand Up @@ -384,9 +383,7 @@ GEM
next_rails (1.3.0)
colorize (>= 0.8.1)
nio4r (2.7.0)
nokogiri (1.16.6-arm64-darwin)
racc (~> 1.4)
nokogiri (1.16.6-x86_64-darwin)
nokogiri (1.16.6-x64-mingw-ucrt)
racc (~> 1.4)
nokogiri (1.16.6-x86_64-linux)
racc (~> 1.4)
Expand Down Expand Up @@ -644,8 +641,7 @@ GEM
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
sqlite3 (1.7.2-arm64-darwin)
sqlite3 (1.7.2-x86_64-darwin)
sqlite3 (1.7.2-x64-mingw-ucrt)
sqlite3 (1.7.2-x86_64-linux)
ssrf_filter (1.1.2)
stripe (5.55.0)
Expand All @@ -672,6 +668,8 @@ GEM
turbolinks-source (5.2.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
tzinfo-data (1.2025.3)
tzinfo (>= 1.0.0)
uglifier (4.2.0)
execjs (>= 0.3.0, < 3)
unicode-display_width (2.5.0)
Expand Down Expand Up @@ -707,11 +705,7 @@ GEM
zeitwerk (2.6.13)

PLATFORMS
arm64-darwin-20
arm64-darwin-23
arm64-darwin-24
x86_64-darwin-21
x86_64-darwin-23
x64-mingw-ucrt
x86_64-linux

DEPENDENCIES
Expand Down Expand Up @@ -824,6 +818,7 @@ DEPENDENCIES
timecop
transitions
turbolinks
tzinfo-data
uglifier (>= 1.3.0)
unobtrusive_flash (>= 3)
web-console
Expand All @@ -832,7 +827,7 @@ DEPENDENCIES
whenever

RUBY VERSION
ruby 3.3.8p144
ruby 3.3.10p183

BUNDLED WITH
2.5.6
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# [Snap!Con](https://snapcon.org) Base Repository
## [Snap!Con](https://snapcon.org) Production Repository
[![Specs](https://github.com/snap-cloud/snapcon/actions/workflows/spec.yml/badge.svg)](https://github.com/snap-cloud/snapcon/actions/workflows/spec.yml)
[![Maintainability](https://qlty.sh/gh/snap-cloud/projects/snapcon/maintainability.svg)](https://qlty.sh/gh/snap-cloud/projects/snapcon)
[![Code Coverage](https://qlty.sh/gh/snap-cloud/projects/snapcon/coverage.svg)](https://qlty.sh/gh/snap-cloud/projects/snapcon)
Expand Down
2 changes: 1 addition & 1 deletion app/assets/stylesheets/application.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,5 @@
*= require selectize.bootstrap3
*= require conferences

*= require fullcalendar-scheduler/main.css
*= require fullcalendar-scheduler/main.css
*/
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
1 change: 1 addition & 0 deletions app/controllers/admin/physical_tickets_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ def index
@physical_tickets = @conference.physical_tickets
@tickets_sold_distribution = @conference.tickets_sold_distribution
@tickets_turnover_distribution = @conference.tickets_turnover_distribution
@ticket_sales_by_currency_distribution = @conference.ticket_sales_by_currency_distribution
end
end
end
118 changes: 118 additions & 0 deletions app/models/conference.rb
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,42 @@ def tickets_turnover_distribution
result
end

##
# Gross ticket sales per enabled currency (no refunds/fees).
# Returns a hash suitable for donut chart: { "USD" => { value:, color: }, ... }
# Only includes currencies that are enabled for the conference (base + conversions).
def ticket_sales_by_currency_distribution
result = {}
enabled_currencies = enabled_currencies_list
return result if enabled_currencies.blank?

# Gross sales: sum(amount_paid_cents * quantity) per currency, paid only
sums = ticket_purchases.paid.group(:currency).sum('amount_paid_cents * quantity')
enabled_currencies.each do |currency|
total_cents = sums[currency].to_i
next if total_cents.zero?

amount = Money.new(total_cents, currency)
label = "#{currency} (#{ApplicationController.helpers.humanized_money(amount)})"
# Use amount in major units (e.g. 50 for $50) so chart tooltip shows readable numbers, not cents
result[label] = {
'value' => (total_cents / 100.0).round(2),
'color' => "\##{Digest::MD5.hexdigest(currency)[0..5]}"
}
end
result
end

##
# List of currencies enabled for this conference (base + conversion targets).
def enabled_currencies_list
base = tickets.first&.price_currency
return [] if base.blank?

targets = currency_conversions.pluck(:to_currency).uniq
[base] | targets
end

##
# Calculates the overall program minutes
#
Expand Down Expand Up @@ -805,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
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
3 changes: 3 additions & 0 deletions app/views/admin/physical_tickets/index.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
.col-md-4
= render 'donut_chart', title: 'Tickets turnover',
combined_data: @tickets_turnover_distribution
.col-md-4
= render 'donut_chart', title: 'Gross ticket sales by currency',
combined_data: @ticket_sales_by_currency_distribution
%br
- if @physical_tickets.any?
.row
Expand Down
7 changes: 4 additions & 3 deletions config/puma.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,16 @@
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
workers ENV.fetch('WEB_CONCURRENCY') { 2 }
worker_count = ENV.fetch('WEB_CONCURRENCY') { Gem.win_platform? ? 0 : 2 }.to_i
workers worker_count
# Set a 10 minute timeout in development for debugging.
worker_timeout 60 * 60 * 10 if ENV.fetch('RAILS_ENV') == 'development'
worker_timeout 60 * 60 * 10 if ENV.fetch('RAILS_ENV') == 'development' && worker_count > 0

# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
# process behavior so workers use less memory.
preload_app!
preload_app! if worker_count > 0

lowlevel_error_handler do |ex, env|
Sentry.capture_exception(
Expand Down
27 changes: 27 additions & 0 deletions info.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
project:
name: 'snapcon' # Your project name, e.g. Cue-to-cue
owner: 'CS169L-26' # Do not change
teamId: '04' # Your team number, e.g. 02
identities:
heroku: 'https://sp26-04-snapcon.herokuapp.com' # Your Heroku app URL
members:
member1: # Add all project members
name: 'Ethan' # Member 1 name
surname: 'Stone' # Member 1 last name
githubUsername: 'Ethan-Stone1' # Member 1 GitHub username
herokuEmail: 'ethanstone@berkeley.edu' # Member 1 Heroku username
member2:
name: 'Benjamin'
surname: 'Sikes'
githubUsername: 'sikesbc'
herokuEmail: 'bcsikes@berkeley.edu'
member3:
name: 'Yijun'
surname: 'Zhou'
githubUsername: 'zhouyijun111'
herokuEmail: 'zhouyijun@berkeley.edu'
member4:
name: 'Xinwei'
surname: 'Li'
githubUsername: 'li-xinwei'
herokuEmail: 'xinweili@berkeley.edu'
Loading
Loading