Last active
August 29, 2015 13:57
-
-
Save mitio/9529403 to your computer and use it in GitHub Desktop.
"Quick" script to generate an example schedule for the Rails Girls study groups
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.DS_Store | |
*.csv |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
require 'csv' | |
require 'time' | |
require 'digest/md5' | |
if ARGV.empty? | |
puts "Usage: #{__FILE__} path/to/csv-with-participants.csv" | |
exit 1 | |
end | |
# Headers: | |
# | |
# Timestamp | |
# Името ви | |
# Името ви | |
# Имейл за връзка | |
# Телефон за връзка | |
# Допълнителни коментари | |
# Кои дни ви е удобно да посещавате сбирките? | |
# Кое от следните неща ви е най-интересно? | |
# Колко пъти искате да посещавате сбирките? | |
# Искате ли да се помагате за организацията? | |
# Кога може да започнете? | |
# Ще присъствате ли на нулевата сбирка в петък? | |
participants = CSV.read(ARGV.first, headers: true) | |
frequencies_translations = { | |
once: 'Веднъж седмично, сравнително редовно', | |
occasionally: 'Веднъж седмично, от време на време', | |
twice: 'Два пъти седмично, ако има места', | |
trice: 'Три или повече пъти седмично, ако има места', | |
} | |
days = [ | |
'Понеделник вечер', | |
'Вторник вечер', | |
'Сряда вечер', | |
'Четвъртък вечер', | |
'Петък вечер', | |
] | |
INTERESTS = { | |
all: 'И двете са ми интересни.', | |
frontend: 'Front-end неща – HTML, CSS, ползваемост и др.', | |
backend: 'Back-end неща – Ruby и програмиране като цяло', | |
} | |
def interested_in?(what, participant) | |
participant['Кое от следните неща ви е най-интересно?'] == INTERESTS[what] | |
end | |
def dom_id_for(participant, *other_keys) | |
key_components = [participant['Имейл за връзка'].to_s] + other_keys.map(&:to_s) | |
"row_#{Digest::MD5.hexdigest key_components.join('-')}" | |
end | |
def cell(text, **args) | |
[text, **args] | |
end | |
def row(*cells) | |
cells | |
end | |
class TextTable | |
def initialize(rows) | |
@rows = rows | |
end | |
def render | |
rows = @rows.map.with_index { |cells, i| render_row(cells, index: i) } | |
width = (rows.first || '').size | |
separator = '-' * width | |
rows = rows + [:separator] unless rows.last == :separator | |
rows = [:separator] + rows unless rows.first == :separator | |
rows = rows.map { |row| row == :separator ? separator : row } | |
render_table rows | |
end | |
private | |
def render_row(cells, index: nil) | |
return cells if cells == :separator | |
row_separator = '|' | |
row_separator + cells.map { |text, args| render_cell(text, args.merge(row_index: index)) }.join(row_separator) + row_separator | |
end | |
def render_cell(text, size: nil, align: :center, type: :text, **args) | |
if type == :flag | |
text = text ? 'X' : '' | |
size ||= 1 | |
end | |
text = text.to_s.strip | |
size ||= text.size | |
text = case align | |
when :left then text.ljust(size) | |
when :right then text.rjust(size) | |
when :center then text.center(size) | |
end | |
" #{text} " | |
end | |
def render_table(rows) | |
rows.join("\n") | |
end | |
end | |
class HtmlTable < TextTable | |
def initialize(*args) | |
super | |
@template = DATA.read | |
end | |
def render | |
@template % super | |
end | |
private | |
def render_table(rows) | |
rows.map { |row| %(<div class="row">#{row}</div>) }.join | |
end | |
def render_cell(text, size: nil, align: :center, type: :text, class_name: nil, row_index: nil, name: nil) | |
if type == :flag | |
checked = text ? 'checked="checked"' : '' | |
name ||= "row#{row_index}" | |
flag = %(<input type="radio" value="1" name="#{name}" #{checked} />) | |
%(<span class="flag #{text.to_s} #{class_name}">#{super flag, size: size, align: align}</span>) | |
elsif class_name | |
%(<span class="#{class_name}">#{super}</span>) | |
else | |
super | |
end | |
end | |
end | |
participants_by_visit_frequency = participants | |
.group_by { |participant| participant['Колко пъти искате да посещавате сбирките?'] } | |
longest_name_length = participants.map { |p| p['Името ви'].to_s.size }.max + 4 | |
flags_cell_size = 3 | |
flags_header_size = flags_cell_size * INTERESTS.size + INTERESTS.size.pred * 3 | |
schedule_table = [] | |
# Headers | |
schedule_table += [ | |
row( | |
cell('Име', size: longest_name_length), | |
cell('Пон', size: flags_header_size), | |
cell('Вто', size: flags_header_size), | |
cell('Сря', size: flags_header_size), | |
cell('Чет', size: flags_header_size), | |
cell('Пет', size: flags_header_size), | |
), | |
:separator, | |
row(*[ | |
[ | |
cell('', size: longest_name_length), | |
], | |
[ | |
cell('A', size: flags_cell_size), | |
cell('F', size: flags_cell_size), | |
cell('B', size: flags_cell_size), | |
] * 5 | |
].flatten(1)), | |
:separator, | |
] | |
participants_count = 0 | |
counts_per_group = Hash.new(0) | |
[ | |
[:once, :twice, :trice], | |
[:occasionally], | |
[:twice, :trice], | |
[:trice] | |
].each do |frequencies| | |
participants = frequencies | |
.map { |frequency| participants_by_visit_frequency[frequencies_translations[frequency]] } | |
.compact | |
.flatten | |
.sort_by { |participant| [participant['Кои дни ви е удобно да посещавате сбирките?'].split(', ').size, participant['Името ви'].to_s] } | |
participants.each do |participant| | |
participants_count += 1 | |
participant_index = "#{participants_count}." | |
name_with_index = "#{participant_index.ljust 3} #{participant['Името ви']}" | |
participant_schedule = [ | |
cell(name_with_index, size: longest_name_length, align: :left), | |
] | |
convenient_days = participant['Кои дни ви е удобно да посещавате сбирките?'].split(', ') | |
dom_id = dom_id_for(participant, *frequencies) | |
days.each_with_index do |day, day_index| | |
day_class = "day-#{day_index}" | |
INTERESTS.keys.each do |interest| | |
if interested_in?(interest, participant) and convenient_days.include?(day) | |
participant_schedule << cell(true, size: flags_cell_size, type: :flag, class_name: day_class, name: dom_id) | |
counts_per_group[[day, interest]] += 1 | |
else | |
participant_schedule << cell(false, size: flags_cell_size, type: :flag, class_name: day_class, name: dom_id) | |
end | |
end | |
end | |
schedule_table << row(*participant_schedule) | |
end | |
stats_row = [ | |
cell("attending-#{frequencies.first}", size: longest_name_length, align: :left) | |
] | |
days.each do |day| | |
INTERESTS.keys.each do |interest| | |
stats_row << cell(counts_per_group[[day, interest]], size: flags_cell_size) | |
end | |
end | |
schedule_table << :separator << row(*stats_row) | |
schedule_table << :separator | |
end | |
puts HtmlTable.new(schedule_table).render | |
__END__ | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<title>Schedule</title> | |
<style type="text/css"> | |
input[type=radio] { width: 18px; } | |
.flag.true { background-color: #aaffaa; } | |
.row:hover, .row.hover { background-color: #ddd; } | |
.row.red-alert:hover, .row.red-alert.hover { background-color: #dd8082; } | |
.day-0, .day-2, .day-4 { background-color: #edd; } | |
#schedule { padding: 10px 20px; background-color: #eee; border-radius: 3px; } | |
.extra-visits { color: #888; } | |
</style> | |
</head> | |
<body> | |
<pre>%s</pre> | |
<pre id="schedule"></pre> | |
<script src="http://code.jquery.com/jquery-1.11.0.min.js"></script> | |
<script> | |
function nameFrom(row) { | |
var nameWithIndex = $(row).text().split('|')[1]; | |
if (nameWithIndex) { | |
var name = nameWithIndex.split('. ')[1]; | |
if (name) { | |
return name.trim(); | |
} | |
} | |
return null; | |
} | |
function dayIndexFrom(input) { | |
return parseInt(($(input).closest('span').prop('class') || '').replace(/\D/g, ''), 10) | |
} | |
function generateSchedule() { | |
var scheduleByDay = {}; | |
var extraVisitsByDay = {}; | |
var generatedSchedule = ''; | |
var days = [ | |
'Понеделник', | |
'Вторник', | |
'Сряда', | |
'Четвъртък', | |
'Петък', | |
]; | |
$('div.row').each(function () { | |
var row = $(this); | |
var name = nameFrom(row); | |
if (name) { | |
var day = dayIndexFrom(row.find('input:checked')); | |
var attendanceFrequency = ''; | |
row.nextAll().each(function () { | |
var matches = $(this).text().match(/\battending-(\w+)/); | |
if (matches) { | |
attendanceFrequency = matches[1]; | |
return false; | |
} | |
}); | |
if (attendanceFrequency && attendanceFrequency != 'once' && attendanceFrequency != 'occasionally') { | |
extraVisitsByDay[day] = extraVisitsByDay[day] || []; | |
extraVisitsByDay[day].push(name); | |
} else { | |
scheduleByDay[day] = scheduleByDay[day] || []; | |
scheduleByDay[day].push(name); | |
} | |
} | |
}); | |
generatedSchedule = Object.keys(scheduleByDay).map(function (day) { | |
var scheduledVisits = scheduleByDay[day]; | |
var extraVisits = extraVisitsByDay[day] || []; | |
var names = scheduledVisits.concat(extraVisits.map(function (name) { | |
return '<span class="extra-visits">' + name + '*</span>'; | |
})); | |
return '<h3>' + days[day] + ' – ' + names.length + ' (' + scheduledVisits.length + ' + ' + extraVisits.length + ')</h3>' + names.join("\n"); | |
}).join("\n\n"); | |
$('#schedule').html(generatedSchedule); | |
} | |
$(function () { | |
$('div.row').on('mouseover', function () { | |
var name = nameFrom(this); | |
var checksPerDay = {}; | |
var sameDaySelectedMoreThanOnce = false; | |
$('div.row').each(function () { | |
$(this).removeClass('red-alert'); | |
if (name && nameFrom(this) === name) { | |
var dayIndex = dayIndexFrom($(this).find('input:checked')); | |
if (checksPerDay[dayIndex]) { | |
sameDaySelectedMoreThanOnce = true; | |
} else { | |
checksPerDay[dayIndex] = true; | |
} | |
$(this).addClass('hover'); | |
} else { | |
$(this).removeClass('hover'); | |
} | |
}); | |
if (sameDaySelectedMoreThanOnce) { | |
$('div.row.hover').addClass('red-alert'); | |
} | |
}); | |
// Restore previous states | |
for (var key in window.localStorage) { | |
if (key && key.match && key.match(/^row_\w+/)) { | |
var dayIndex = window.localStorage[key]; | |
if (dayIndex !== undefined) { | |
$('span.true.day-' + dayIndex + ' input[name="' + key + '"]').prop('checked', true); | |
} | |
} | |
} | |
$('input[type=radio]').on('change', function () { | |
window.localStorage[this.name] = dayIndexFrom($(this).closest('.row').find('input:checked')); | |
generateSchedule(); | |
}); | |
generateSchedule(); | |
}); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment