Skip to content

Instantly share code, notes, and snippets.

@mitio
Last active August 29, 2015 13:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mitio/9529403 to your computer and use it in GitHub Desktop.
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
.DS_Store
*.csv
#!/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