Browse Source

Schedule maker, stats, and minor fixes

development
Godwin 8 years ago
parent
commit
26f9dfde89
  1. 5
      Gemfile
  2. 164
      app/assets/javascripts/main.js
  3. 332
      app/assets/stylesheets/_application.scss
  4. 301
      app/controllers/conferences_controller.rb
  5. 48
      app/helpers/application_helper.rb
  6. 14
      app/models/user.rb
  7. 12
      app/models/workshop.rb
  8. 12
      app/views/conferences/admin/_edit.html.haml
  9. 2
      app/views/conferences/admin/_housing.html.haml
  10. 116
      app/views/conferences/admin/_schedule.html.haml
  11. 5
      app/views/conferences/admin/_stats.html.haml
  12. 18
      app/views/conferences/admin/_workshop_times.html.haml
  13. 18
      app/views/conferences/stats.xlsx.haml
  14. 37
      app/views/layouts/application.html.haml
  15. 2
      config/environments/development.rb
  16. 1
      config/locales/en.yml
  17. 5
      db/migrate/20160703044620_add_block_to_workshops.rb
  18. 3
      db/schema.rb

5
Gemfile

@ -35,13 +35,16 @@ gem 'geocoder'
gem 'paper_trail', '~> 3.0.5'
gem 'sitemap_generator'
gem 'activerecord-session_store'
gem 'paypal-express', '0.7.1'
gem 'paypal-express'#, '0.7.1'
gem 'sass-json-vars'
gem 'premailer-rails'
gem 'redcarpet'
gem 'sidekiq'
gem 'letter_opener'
gem 'launchy'
# gem 'axlsx'
# gem 'excelinator'
gem 'to_spreadsheet'#, :git => 'git://github.com/glebm/to_spreadsheet.git'
group :test do
gem 'rspec'

164
app/assets/javascripts/main.js

@ -15,24 +15,6 @@
window.forEach = function(a, f) { Array.prototype.forEach.call(a, f) };
window.forEachElement = function(s, f, p) { forEach((p || document).querySelectorAll(s), f) };
forEachElement('.number-field,.email-field,.text-field', function(field) {
var input = field.querySelector('input');
var positionLabel = function(input) {
field.classList[input.value ? 'remove' : 'add']('empty');
}
positionLabel(input);
input.addEventListener('keyup', function(event) {
positionLabel(event.target);
});
input.addEventListener('blur', function(event) {
positionLabel(event.target);
field.classList.remove('focused');
});
input.addEventListener('focus', function(event) {
field.classList.add('focused');
});
});
var overlay = document.getElementById('content-overlay');
if (overlay) {
var body = document.querySelector('body');
@ -50,29 +32,35 @@
}, true);
function openDlg(dlg, link) {
body.setAttribute('style', 'width: ' + body.clientWidth + 'px');
dlg.querySelector('.message').innerHTML = decodeURI(link.dataset.confirmation);
dlg.querySelector('.confirm').addEventListener('click', function(event) {
event.preventDefault();
if (link.tagName == 'BUTTON') {
var form = link.parentElement
while (form && form.tagName != 'FORM') {
var form = form.parentElement
}
if (form) {
var input = document.createElement('input');
input.type = 'hidden';
input.name = 'button';
input.value = link.value;
form.appendChild(input);
form.submit();
dlg.querySelector('.message').innerHTML = link.querySelector('.message').innerHTML
if (link.dataset.infoTitle) {
dlg.querySelector('.title').innerHTML = decodeURI(link.dataset.infoTitle);
}
confirmBtn = dlg.querySelector('.confirm');
if (confirmBtn) {
confirmBtn.addEventListener('click', function(event) {
event.preventDefault();
if (link.tagName == 'BUTTON') {
var form = link.parentElement
while (form && form.tagName != 'FORM') {
var form = form.parentElement
}
if (form) {
var input = document.createElement('input');
input.type = 'hidden';
input.name = 'button';
input.value = link.value;
form.appendChild(input);
form.submit();
}
} else {
window.location.href = link.getAttribute('href');
}
} else {
window.location.href = link.getAttribute('href');
}
});
});
}
primaryContent.setAttribute('aria-hidden', 'true');
document.getElementById('overlay').onclick =
dlg.querySelector('.delete').onclick = function() { closeDlg(dlg); };
dlg.querySelector('.close').onclick = function() { closeDlg(dlg); };
body.classList.add('has-overlay');
dlg.removeAttribute('aria-hidden');
dlg.setAttribute('role', 'alertdialog');
@ -99,11 +87,14 @@
return false;
});
});
}
var errorField = document.querySelector('.input-field.has-error input, .input-field.has-error textarea');
if (errorField) {
errorField.focus();
var infoDlg = document.getElementById('info-dlg');
forEachElement('[data-info-text]', function(link) {
link.addEventListener('click', function(event) {
event.preventDefault();
openDlg(infoDlg, link);
return false;
});
});
}
var htmlNode = document.documentElement;
@ -120,28 +111,73 @@
htmlNode.setAttribute('data-input', 'mouse');
}
});
forEachElement('form.js-xhr', function(form) {
if (form.addEventListener) {
form.addEventListener('submit', function(event) {
event.preventDefault();
form.classList.add('requesting');
var data = new FormData(form);
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (request.readyState == 4) {
form.classList.remove('requesting');
if (request.status == 200) {
var response = JSON.parse(request.responseText);
for (var i = 0; i < response.length; i++) {
form.querySelector(response[i].selector).innerHTML = response[i].html;
var errorField = document.querySelector('.input-field.has-error input, .input-field.has-error textarea');
if (errorField) {
errorField.focus();
}
window.initNodeFunctions = [ function(node) {
forEachElement('.number-field,.email-field,.text-field', function(field) {
var input = field.querySelector('input');
var positionLabel = function(input) {
field.classList[input.value ? 'remove' : 'add']('empty');
}
positionLabel(input);
input.addEventListener('keyup', function(event) {
positionLabel(event.target);
});
input.addEventListener('blur', function(event) {
positionLabel(event.target);
field.classList.remove('focused');
});
input.addEventListener('focus', function(event) {
field.classList.add('focused');
});
}, node || document);
forEachElement('form.js-xhr', function(form) {
if (form.addEventListener) {
form.addEventListener('submit', function(event) {
event.preventDefault();
form.classList.add('requesting');
var data = new FormData(form);
var request = new XMLHttpRequest();
request.onreadystatechange = function() {
if (request.readyState == 4) {
form.classList.remove('requesting');
if (request.status == 200) {
var response = JSON.parse(request.responseText);
for (var i = 0; i < response.length; i++) {
var element;
if (response[i].selector) {
element = form.querySelector(response[i].selector);
}
if (response[i].globalSelector) {
element = document.querySelector(response[i].globalSelector);
}
if (response[i].html) {
element.innerHTML = response[i].html;
window.initNode(element);
}
if (response[i].className) {
element.className = response[i].className;
}
}
}
}
}
}
request.open('POST', form.getAttribute('action'), true);
request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
request.send(data);
}, false);
}
});
request.open('POST', form.getAttribute('action'), true);
request.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
request.send(data);
}, false);
}
}, node || document);
} ];
window.initNode = function(node) {
forEach(initNodeFunctions, function(fn) {
fn(node);
});
};
initNode();
})();

332
app/assets/stylesheets/_application.scss

@ -13,7 +13,7 @@ body {
padding-bottom: 20vw;
}
h1, h2, h3, h4, h5, label, button, .button, dt, th, .table-th, nav.sub-menu {
h1, h2, h3, h4, h5, h6, label, button, .button, dt, th, .table-th, nav.sub-menu {
@include font-family(secondary);
font-weight: normal;
}
@ -31,6 +31,18 @@ h3.subtitle {
font-size: 1.5em;
}
h4 {
font-size: 1.25em;
}
h5 {
font-size: 1.125em;
}
h6 {
font-size: 1em;
}
p {
font-size: 4vw;
}
@ -41,6 +53,7 @@ a {
border-bottom: 0 solid;
outline: 0;
position: relative;
cursor: pointer;
@include after {
content: '';
@ -101,10 +114,25 @@ table, .table {
&:last-child {
padding-right: 1em;
}*/
&.status {
width: 0.1rem;
background-color: transparent;
border: 0;
}
}
th, .table-th {
background-color: #F8F8F8;
&.corner {
background-color: transparent;
border: 0;
}
}
tbody th {
width: 0.1rem;
}
}
@ -382,18 +410,25 @@ input {
}
}
.hidden {
display: none !important;
}
.field-error {
display: block;
background-color: rgba($colour-2, 0.333);
@include font-family(secondary);
padding: 0.5em 1em;
margin: 0 0.2em;
text-align: center;
@include default-box-shadow(top, 2);
}
.input-field {
.field-error {
display: block;
float: right;
background-color: rgba($colour-2, 0.333);
@include font-family(secondary);
padding: 0.5em 1em;
margin: 0 0.2em;
text-align: center;
@include _(transform, skewX(-15deg));
@include _(transform-origin, 0 100%);
@include default-box-shadow(top, 2);
@include _(animation, bend ease-in-out 500ms infinite alternate both);
}
@ -463,10 +498,9 @@ input {
}
.input-field-help {
margin-bottom: 1em;
margin-left: 1em;
margin: 0.5em 1em 0;
line-height: 1.3333em;
font-size: 1.25em;
font-size: 1.125em;
}
.check-box-field,
@ -1136,13 +1170,10 @@ ul.warnings li,
margin: 1em 0 0;
padding: 0;
list-style: none;
// border: 0.1em solid #EEE;
// background-color: #F8F8F8;
a {
display: block;
padding: 0.5em 0.75em;
//@include font-family(secondary);
border: 0.1rem solid #EEE;
border-top: 0;
border-right: 0;
@ -1158,10 +1189,6 @@ ul.warnings li,
li {
margin: 0;
&:last-child a {
border: 0;
}
&.current {
a {
color: $white;
@ -1171,6 +1198,44 @@ ul.warnings li,
}
}
.data-set {
display: table-row;
}
.data-set-key, .data-set-value {
display: table-cell;
padding: 0.25em 0.5em;
vertical-align: top;
border-bottom: 0.1rem solid #EEE;
}
.data-set-key {
font-size: 1em;
width: 1rem;
white-space: nowrap;
}
.data-set:last-child {
.data-set-key, .data-set-value {
border: 0;
}
}
.details {
display: table;
width: 100%;
&.inline {
width: auto;
}
}
.space, .address {
.data-set-key, .data-set-value {
white-space: nowrap;
}
}
.admin-blocks {
@include _-(display, inline-flex);
@include _(flex-wrap, wrap);
@ -1199,34 +1264,6 @@ ul.warnings li,
border-bottom: 0.1rem solid #EEE;
}
.details {
display: table;
width: 100%;
}
.data-set {
display: table-row;
}
.data-set-key, .data-set-value {
display: table-cell;
padding: 0.25em 0.5em;
vertical-align: top;
border-bottom: 0.1rem solid #EEE;
}
.data-set:last-child {
.data-set-key, .data-set-value {
border: 0;
}
}
.space, .address {
.data-set-key, .data-set-value {
white-space: nowrap;
}
}
.amenities {
list-style: none;
padding: 0;
@ -1342,7 +1379,7 @@ ul.warnings li,
#guests {
.guests {
@include _-(display, flex);
@include _(align-items, flex-start);
//@include _(align-items, flex-start);
@include _(flex-wrap, wrap);
list-style: none;
padding: 0;
@ -1366,7 +1403,7 @@ ul.warnings li,
}
}
#admin-schedule {
#admin-workshop_times {
.workshop-blocks {
td, th {
vertical-align: top;
@ -1386,11 +1423,177 @@ ul.warnings li,
}
}
.details.org-members {
padding: 1em;
border: 0.1rem solid #EEE;
border-bottom: 0;
}
table.schedule {
width: 100%;
margin: 0 0 1em;
td {
text-align: center;
&.empty {
border-top: 0;
border-bottom: 0;
background-color: #F8F8F8;
}
&.workshop {
background-color: lighten($colour-1, 40%);
&.filled {
background-color: lighten($colour-1, 25%);
}
}
&.event {
background-color: lighten($colour-2, 25%);
}
&.meal {
background-color: lighten($colour-3, 25%);
}
.title {
@include font-family(secondary);
}
.status {
display: inline-block;
text-align: left;
float: right;
font-size: 0.9em;
margin-top: 0.5em;
}
.conflict-score {
text-align: right;
.title {
@include font-family(secondary);
}
}
.errors {
position: relative;
border-top: 0.1rem solid #666;
margin-top: 0.5em;
padding-top: 0.25em;
@include before {
content: '!';
display: inline-block;
position: absolute;
z-index: 1;
top: 0;
left: -1.2em;
width: 1em;
color: $white;
@include font-family(secondary);
}
@include after {
content: '';
position: absolute;
top: 0.1em;
left: -1.333em;
width: 0;
height: 0;
font-size: 1.25em;
border: 0.5em solid;
border-left-color: transparent;
border-right-color: transparent;
border-width: 0 0.5em 0.75em;
color: darken($colour-2, 25%);
}
}
#main .columns & form {
margin-top: 0;
button {
margin-top: 0.5em;
float: left;
}
}
}
}
#admin-schedule {
.workshop-list {
@include _-(display, flex);
@include _(flex-wrap, wrap);
padding: 0;
li {
@include _(flex, 1);
@include _(flex-basis, 48%);
margin: 1%;
@include _(transition, background-color 250ms ease-in-out);
.title {
@include _(transition, background-color 250ms ease-in-out);
}
&.booked {
background-color: lighten($colour-1, 40%);
.not-booked-only {
display: none;
}
.data-set-key, .data-set-value {
border-bottom-color: #888;
}
}
&.not-booked {
.booked-only {
display: none;
}
}
.field-error {
background-color: lighten($colour-2, 22.5%);
margin: 0;
}
.already-booked {
overflow: hidden;
max-height: 0;
&.is-true {
max-height: 3em;
@include _(transition, max-height 150ms ease-in-out);
}
}
.workshop-description {
max-height: none;
}
.details {
margin: 0 5% 1em;
width: 90%;
}
}
#main .columns & form {
margin: 0;
}
}
}
#main {
position: relative;
background-color: $white;
padding-bottom: rems(2);
flex: 1;
@include _(flex, 1);
article {
padding: rems(2.5) 0;
@ -1816,6 +2019,8 @@ body {
bottom: 0;
left: 0;
max-width: 50rem;
max-height: 100%;
overflow-y: auto;
margin: auto;
z-index: 1001;
background-color: $white;
@ -1841,9 +2046,34 @@ body {
}
.message {
font-size: 1.5em;
margin-bottom: 2em;
}
#info-dlg .title {
background-color: $colour-1;
font-size: 1.5em;
text-align: left;
}
#info-dlg .message {
text-align: left;
p, h4 {
font-size: 0.8em;
}
h3 {
font-size: 1em;
}
h5 {
font-size: 0.667em;
}
h6 {
font-size: 0.8em;
}
}
}
@include keyframes(fade-out) {
@ -3150,7 +3380,7 @@ html[data-ontop] {
}
&:last-child {
@include _(border-radius, 0 0.15em 0.15em 0);
@include _(border-radius, 0 0 0.15em 0.15em);
}
}
}

301
app/controllers/conferences_controller.rb

@ -798,6 +798,39 @@ class ConferencesController < ApplicationController
@page_title = 'articles.conference_registration.headings.Administration'
case @admin_step.to_sym
when :stats
@registrations = ConferenceRegistration.where(:conference_id => @this_conference.id)
if request.format.xlsx?
logger.info "Generating stats.xls"
@excel_data = {
columns: [:name, :email, :city, :date, :languages],
column_types: {date: :date},
keys: {
name: 'forms.labels.generic.name',
email: 'forms.labels.generic.email',
city: 'forms.labels.generic.location',
date: 'articles.conference_registration.terms.Date',
languages: 'articles.conference_registration.terms.Languages'
},
data: [],
}
@registrations.each do | r |
user = r.user_id ? User.where(id: r.user_id).first : nil
if user.present?
@excel_data[:data] << {
name: user.firstname || '',
email: user.email || '',
date: r.created_at ? r.created_at.strftime("%F %T") : '',
city: r.city || '',
languages: ((r.languages || []).map { |x| view_context.language x }).join(', ').to_s
}
end
end
return respond_to do | format |
# format.html
format.xlsx { render xlsx: :stats, filename: "stats-#{DateTime.now.strftime('%Y-%m-%d')}" }
end
end
when :housing
# do a full analysis
analyze_housing
@ -811,6 +844,13 @@ class ConferencesController < ApplicationController
@length = 1.5
when :meals
@meals = Hash[@this_conference.meals.map{ |k, v| [k.to_i, v] }].sort.to_h
when :workshop_times
get_block_data
@workshop_blocks << {
'time' => nil,
'length' => 1.0,
'days' => []
}
when :schedule
get_scheule_data
end
@ -820,27 +860,175 @@ class ConferencesController < ApplicationController
end
def get_block_data
@workshop_blocks = @this_conference.workshop_blocks || []
@block_days = []
day = @this_conference.start_date
while day <= @this_conference.end_date
@block_days << day.wday
day += 1.day
end
end
def get_scheule_data
@meals = Hash[@this_conference.meals.map{ |k, v| [k.to_i, v] }].sort.to_h
@events = Event.where(:conference_id => @this_conference.id).order(start_time: :asc)
@workshops = Workshop.where(:conference_id => @this_conference.id).order(start_time: :asc)
@locations = {}
@workshop_blocks = @this_conference.workshop_blocks || []
@workshop_blocks << {
'time' => nil,
'length' => 1.0,
'days' => []
}
@workshops.each do |workshop|
if workshop.location_id
@locations[workshop.location_id] ||= workshop.location
get_block_data
@schedule = {}
day_1 = @this_conference.start_date.wday
@workshop_blocks.each_with_index do | info, block |
info['days'].each do | block_day |
day_diff = block_day.to_i - day_1
day_diff += 7 if day_diff < 0
day = (@this_conference.start_date + day_diff.days).to_date
time = info['time'].to_f
@schedule[day] ||= { times: {}, locations: {} }
@schedule[day][:times][time] ||= {}
@schedule[day][:times][time][:type] = :workshop
@schedule[day][:times][time][:length] = info['length'].to_f
@schedule[day][:times][time][:item] = { block: block, workshops: {} }
end
end
@block_days = []
day = @this_conference.start_date
while day <= @this_conference.end_date
@block_days << day.wday
day += 1.day
@workshops.each do | workshop |
if workshop.block.present?
block = @workshop_blocks[workshop.block['block'].to_i]
day_diff = workshop.block['day'].to_i - day_1
day_diff += 7 if day_diff < 0
day = (@this_conference.start_date + day_diff.days).to_date
if @schedule[day].present? && @schedule[day][:times].present? && @schedule[day][:times][block['time'].to_f].present?
@schedule[day][:times][block['time'].to_f][:item][:workshops][workshop.event_location_id] = { workshop: workshop, status: { errors: [], warnings: [], conflict_score: nil } }
@schedule[day][:locations][workshop.event_location_id] ||= workshop.event_location
end
end
end
@meals.each do | time, meal |
day = meal['day'].to_date
time = meal['time'].to_f
@schedule[day] ||= {}
@schedule[day][:times] ||= {}
@schedule[day][:times][time] ||= {}
@schedule[day][:times][time][:type] = :meal
@schedule[day][:times][time][:length] = (meal['length'] || 1.0).to_f
@schedule[day][:times][time][:item] = meal
end
@events.each do | event |
day = event.start_time.midnight.to_date
time = event.start_time.hour.to_f + (event.start_time.min / 60.0)
@schedule[day] ||= {}
@schedule[day][:times] ||= {}
@schedule[day][:times][time] ||= {}
@schedule[day][:times][time][:type] = :event
@schedule[day][:times][time][:length] = (event.end_time - event.start_time) / 3600.0
@schedule[day][:times][time][:item] = event
end
@schedule = @schedule.sort.to_h
@schedule.each do | day, data |
@schedule[day][:times] = data[:times].sort.to_h
end
@schedule.each do | day, data |
last_event = nil
data[:times].each do | time, time_data |
if last_event.present?
@schedule[day][:times][last_event][:next_event] = time
end
last_event = time
end
end
@schedule.deep_dup.each do | day, data |
data[:times].each do | time, time_data |
if time_data[:next_event].present? || time_data[:length] > 0.5
span = 0.5
length = time_data[:next_event].present? ? time_data[:next_event] - time : time_data[:length]
while span < length
@schedule[day][:times][time + span] ||= {
type: (span >= time_data[:length] ? :empty : :nil),
length: 0.5
}
span += 0.5
end
end
end
end
@schedule = @schedule.sort.to_h
@schedule.each do | day, data |
@schedule[day][:times] = data[:times].sort.to_h
data[:times].each do | time, time_data |
if time_data[:type] == :workshop && time_data[:item].present? && time_data[:item][:workshops].present?
ids = time_data[:item][:workshops].keys
(0..ids.length).each do | i |
if time_data[:item][:workshops][ids[i]].present?
workshop_i = time_data[:item][:workshops][ids[i]][:workshop]
conflicts = {}
(i+1..ids.length).each do | j |
workshop_j = time_data[:item][:workshops][ids[j]].present? ? time_data[:item][:workshops][ids[j]][:workshop] : nil
if workshop_i.present? && workshop_j.present?
workshop_i.active_facilitators.each do | facilitator_i |
workshop_j.active_facilitators.each do | facilitator_j |
if facilitator_i.id == facilitator_j.id
@schedule[day][:times][time][:status] ||= {}
@schedule[day][:times][time][:item][:workshops][ids[j]][:status][:errors] << {
name: :common_facilitator,
facilitator: facilitator_i,
workshop: workshop_i,
i18nVars: {
facilitator_name: facilitator_i.name,
workshop_title: workshop_i.title
}
}
end
end
end
end
end
(0..ids.length).each do | j |
workshop_j = time_data[:item][:workshops][ids[j]].present? ? time_data[:item][:workshops][ids[j]][:workshop] : nil
if workshop_i.present? && workshop_j.present? && workshop_i.id != workshop_j.id
workshop_i.interested.each do | interested_i |
workshop_j.interested.each do | interested_j |
conflicts[interested_i.id] ||= true
end
end
end
end
needs = JSON.parse(workshop_i.needs || '[]').map &:to_sym
amenities = JSON.parse(workshop_i.event_location.amenities || '[]').map &:to_sym
needs.each do | need |
@schedule[day][:times][time][:item][:workshops][ids[i]][:status][:errors] << {
name: :need_not_available,
need: need,
location: workshop_i.event_location,
workshop: workshop_i,
i18nVars: {
need: need.to_s,
location: workshop_i.event_location.title,
workshop_title: workshop_i.title
}
} unless amenities.include? need
end
@schedule[day][:times][time][:item][:workshops][ids[i]][:status][:conflict_score] = workshop_i.interested.present? ? (conflicts.length / workshop_i.interested.size) : 0
end
end
end
end
end
end
@ -948,13 +1136,24 @@ class ConferencesController < ApplicationController
case params[:admin_step]
when 'edit'
@this_conference.info = LinguaFranca::ActiveRecord::UntranslatedValue.new(params[:info]) unless @this_conference.info! == params[:info]
case params[:button]
when 'save'
@this_conference.info = LinguaFranca::ActiveRecord::UntranslatedValue.new(params[:info]) unless @this_conference.info! == params[:info]
params[:info_translations].each do | locale, value |
@this_conference.set_column_for_locale(:info, locale, value, current_user.id) unless value = @this_conference._info(locale)
params[:info_translations].each do | locale, value |
@this_conference.set_column_for_locale(:info, locale, value, current_user.id) unless value = @this_conference._info(locale)
end
@this_conference.save
return redirect_to register_step_path(@this_conference.slug, :administration)
when 'add_member'
org = nil
@this_conference.organizations.each do | organization |
org = organization if organization.id == params[:org_id].to_i
end
org.users << (User.get params[:email])
org.save
return redirect_to administration_step_path(@this_conference.slug, :edit)
end
@this_conference.save
return redirect_to register_step_path(@this_conference.slug, :administration)
when 'housing'
space = params[:button].split(':')[0]
host_id = params[:button].split(':')[1].to_i
@ -1091,7 +1290,7 @@ class ConferencesController < ApplicationController
return redirect_to administration_step_path(@this_conference.slug, :events)
end
when 'schedule'
when 'workshop_times'
case params[:button]
when 'save_block'
@this_conference.workshop_blocks ||= []
@ -1101,7 +1300,67 @@ class ConferencesController < ApplicationController
'days' => params[:days].keys
}
@this_conference.save
return redirect_to administration_step_path(@this_conference.slug, :schedule)
return redirect_to administration_step_path(@this_conference.slug, :workshop_times)
end
when 'schedule'
success = false
case params[:button]
when 'schedule_workshop'
workshop = Workshop.find_by!(conference_id: @this_conference.id, id: params[:id])
booked = false
workshop.event_location_id = params[:event_location]
block_data = params[:workshop_block].split(':')
workshop.block = {
day: block_data[0].to_i,
block: block_data[1].to_i
}
# make sure this spot isn't already taken
Workshop.where(:conference_id => @this_conference.id).each do | w |
if request.xhr?
if w.block.present? &&
w.id != workshop.id &&
w.block['day'] == workshop.block['day'] &&
w.block['block'] == workshop.block['block'] &&
w.event_location_id == workshop.event_location_id
return render json: [ {
selector: '.already-booked',
className: 'already-booked is-true'
} ]
end
else
return redirect_to administration_step_path(@this_conference.slug, :schedule)
end
end
workshop.save!
success = true
when 'deschedule_workshop'
workshop = Workshop.find_by!(conference_id: @this_conference.id, id: params[:id])
workshop.event_location_id = nil
workshop.block = nil
workshop.save!
success = true
end
if success
if request.xhr?
get_scheule_data
schedule = render_to_string partial: 'conferences/admin/schedule'
return render json: [ {
globalSelector: '#schedule-preview',
html: schedule
}, {
globalSelector: "#workshop-#{workshop.id}",
className: workshop.block.present? ? 'booked' : 'not-booked'
}, {
globalSelector: "#workshop-#{workshop.id} .already-booked",
className: 'already-booked'
} ]
else
return redirect_to administration_step_path(@this_conference.slug, :schedule)
end
end
end
do_404

48
app/helpers/application_helper.rb

@ -708,12 +708,16 @@ module ApplicationHelper
end
def data_set(header_type, header_key, attributes = {}, &block)
raw_data_set(header_type, _(header_key), attributes, &block)
end
def raw_data_set(header_type, header, attributes = {}, &block)
attributes[:class] = attributes[:class].split(' ') if attributes[:class].is_a?(String)
attributes[:class] = [attributes[:class].to_s] if attributes[:class].is_a?(Symbol)
attributes[:class] ||= []
attributes[:class] << 'data-set'
content_tag(:div, attributes) do
content_tag(header_type, _(header_key), class: 'data-set-key') +
content_tag(header_type, header, class: 'data-set-key') +
content_tag(:div, class: 'data-set-value', &block)
end
end
@ -774,6 +778,16 @@ module ApplicationHelper
selectfield :time_span, value, lengths, args
end
def block_select(value = nil, args = {})
blocks = {}
@workshop_blocks.each_with_index do | info, block |
info['days'].each do | day |
blocks[(day.to_i * 10) + block] = [ "#{(I18n.t 'date.day_names')[day.to_i]} Block #{block + 1}", "#{day}:#{block}" ]
end
end
selectfield :workshop_block, value, blocks.sort.to_h.values, args
end
def location_select(value = nil, args = {})
locations = []
if @this_conference.event_locations.present?
@ -844,7 +858,7 @@ module ApplicationHelper
end
def admin_steps
[:edit, :stats, :broadcast, :housing, :locations, :meals, :events, :schedule]
[:edit, :stats, :broadcast, :housing, :locations, :meals, :events, :workshop_times, :schedule]
end
def valid_admin_steps
@ -903,15 +917,29 @@ module ApplicationHelper
def link_with_confirmation(link_text, confirmation_text, path, args = {})
@confirmation_dlg ||= true
args[:data] ||= {}
args[:data][:confirmation] = CGI::escapeHTML(confirmation_text)
link_to link_text, path, args
args[:data][:confirmation] = true
link_to path, args do
(link_text.to_s + content_tag(:template, confirmation_text, class: 'message')).html_safe
end
end
def link_info_dlg(link_text, info_text, info_title, args = {})
@info_dlg ||= true
args[:data] ||= {}
args[:data]['info-title'] = info_title
args[:data]['info-text'] = true
content_tag(:a, args) do
(link_text.to_s + content_tag(:template, info_text, class: 'message')).html_safe
end
end
def button_with_confirmation(button_name, confirmation_text, args = {})
@confirmation_dlg ||= true
args[:data] ||= {}
args[:data][:confirmation] = CGI::escapeHTML(confirmation_text)
button_tag button_name, args
args[:data][:confirmation] = true
button_tag args do
(button_name.to_s + content_tag(:template, confirmation_text, class: 'message')).html_safe
end
end
def richtext(text, reduce_headings = 2)
@ -924,6 +952,10 @@ module ApplicationHelper
html_safe
end
def truncate(text)
strip_tags(text.gsub('>', '> ')).gsub(/^(.{40,60})\s.*$/m, '\1&hellip;').html_safe
end
def textarea(name, value, options = {})
id = name.to_s.gsub('[', '_').gsub(']', '')
label_id = "#{id}-label"
@ -1003,6 +1035,10 @@ module ApplicationHelper
textfield(name, value, options.merge({type: :number}))
end
def emailfield(name, value, options = {})
textfield(name, value, options.merge({type: :email}))
end
def textfield(name, value, options = {})
html = ''
id = name.to_s.gsub('[', '_').gsub(']', '')

14
app/models/user.rb

@ -36,4 +36,18 @@ class User < ActiveRecord::Base
return "#{name} <#{email}>"
end
def self.get(email)
user = where(email: email).first
unless user
# not really a good UX so we should fix this later
#do_404
#return
user = new(email: email)
user.save!
end
return user
end
end

12
app/models/workshop.rb

@ -2,6 +2,7 @@ class Workshop < ActiveRecord::Base
translates :info, :title
belongs_to :conference
belongs_to :event_location
has_many :workshop_facilitators, :dependent => :destroy
has_many :users, :through => :workshop_facilitators
@ -83,14 +84,19 @@ class Workshop < ActiveRecord::Base
end
def interested_count
return 0 unless id
interested.size
end
def interested
return [] unless id
return @interested if @interested.present?
collaborators = []
workshop_facilitators.each do |f|
collaborators << f.user_id unless f.role.to_sym == :requested || f.user_id.nil?
end
return 10 unless collaborators.present?
interested = WorkshopInterest.where("workshop_id=#{id} AND user_id NOT IN (#{collaborators.join ','})") || []
interested ? interested.size : 0
@interested = WorkshopInterest.where("workshop_id=#{id} AND user_id NOT IN (#{collaborators.join ','})") || []
end
def can_translate?(user, lang)

12
app/views/conferences/admin/_edit.html.haml

@ -5,3 +5,15 @@
= textarea "info_translations[#{locale.to_s}]", @this_conference._info(locale), label: 'translate.pages.Locale_Translation', vars: { language: _("languages.#{locale}") }, lang: locale, edit_on: :focus
.actions.right
= button_tag :save, value: :save
%h4=_'articles.admin.edit.headings.host_organizations'
- @this_conference.organizations.each do | organization |
%p=organization.name
%h5=_'articles.admin.edit.headings.members'
.details.org-members.inline
- organization.users.each do | user |
= raw_data_set(:h6, user.name) do
= user.email
= form_tag administration_update_path(@this_conference.slug, :edit), class: 'mini-flex-form' do
= hidden_field_tag :org_id, organization.id
= emailfield :email, nil, required: true
= button_tag :add_member, value: :add_member, class: :small

2
app/views/conferences/admin/_housing.html.haml

@ -19,7 +19,7 @@
- if registration.user.present?
%li.guest{id: "guest-#{id}", data: { id: id, 'affected-hosts': @hosts_affected_by_guests[id].join(',') }}
%h4= registration.user.name
.city.on-top-only=registration.city
.city=registration.city
.email.on-top-only=registration.user.email
= button_tag :set_host, type: :button, class: [:small, 'set-host', 'not-on-top']

116
app/views/conferences/admin/_schedule.html.haml

@ -1,18 +1,98 @@
.table.workshop-blocks
.table-tr.header
.table-th=_'forms.labels.generic.block_number'
.table-th=_'forms.labels.generic.time'
.table-th=_'forms.labels.generic.length'
.table-th=_'forms.labels.generic.days'
.table-th.form
- @workshop_blocks.each_with_index do | info, block |
- is_new = info['time'].blank?
= form_tag administration_update_path(@this_conference.slug, :schedule), class: ['table-tr', is_new ? 'new' : 'saved'] do
.table-th.center.big= is_new ? '' : (block + 1)
.table-td=time_select info['time'], small: true, label: false
.table-td=length_select info['length'], {small: true, label: false}, 0.5, 2
.table-td=checkboxes :days, @block_days, info['days'].map(&:to_i), 'date.day_names', vertical: true, small: true
.table-td.form
= hidden_field_tag :workshop_block, block
= button_tag :delete_block, value: :delete_block, class: [:small, :delete] if block == @workshop_blocks.length - 2
= button_tag (is_new ? :add_block : :update_block), value: :save_block, class: [:small, :add]
#schedule-preview
- @schedule.each do | day, data |
%h4=date(day, :weekday)
%table.schedule{class: data[:locations].present? ? 'has-locations' : 'no-locations'}
- if data[:locations].present?
%thead
%tr
%th.corner
- data[:locations].each do | id, location |
%th=location.title
%th.status
%tbody
- data[:times].each do | time, time_data |
%tr
- rowspan = (time_data[:length] * 2).to_i
%th=time(time)
- if time_data[:type] == :workshop
- if time_data[:item][:workshops].present?
- data[:locations].each do | id, location |
- if time_data[:item][:workshops][id].present?
- workshop = time_data[:item][:workshops][id][:workshop]
- status = time_data[:item][:workshops][id][:status]
- else
- workshop = status = nil
%td{class: [time_data[:type], workshop.present? ? :filled : nil], rowspan: rowspan}
- if workshop.present?
= form_tag administration_update_path(@this_conference.slug, :schedule), class: 'js-xhr' do
.title=workshop.title
.status
.conflict-score
%span.title Conflict Score:
%span.value="#{status[:conflict_score] * 100.0}%"
- if status[:errors].present?
.errors
- status[:errors].each do | error |
.error=_"errors.messages.schedule.#{error[:name].to_s}", vars: error[:i18nVars]
= hidden_field_tag :id, workshop.id
= button_tag :deschedule, value: :deschedule_workshop, class: [:delete, :small]
- else
.title="Block #{time_data[:item][:block] + 1}"
- else
%td{class: time_data[:type], rowspan: rowspan, colspan: data[:locations].present? ? data[:locations].size : 1}
.title="Block #{time_data[:item][:block] + 1}"
%td.status{rowspan: rowspan}
- if time_data[:status].present? && time_data[:status][:errors].present?
%ul.errors
- time_data[:status][:errors].each do | error |
%li=error.to_json.to_s
- elsif time_data[:type] != :nil
%td{class: time_data[:type], rowspan: rowspan, colspan: data[:locations].present? ? data[:locations].size : 1}
- case time_data[:type]
- when :meal
.title= time_data[:item]['title']
.location= EventLocation.find(time_data[:item]['location'].to_i).title
- when :event
.title= time_data[:item].title
.location= time_data[:item].event_location.title
%td.status{rowspan: rowspan}
- unless request.xhr?
%ul.workshop-list
- @workshops.each do | workshop |
%li{id: "workshop-#{workshop.id}", class: workshop.block.present? ? 'booked' : 'not-booked'}
%h4.title= workshop.title
= form_tag administration_update_path(@this_conference.slug, :schedule), class: 'js-xhr' do
.already-booked
.field-error='This block is already booked'
.workshop-description
.details
= data_set(:h5, 'articles.workshops.headings.interested_count') do
= workshop.interested_count
= data_set(:h5, 'articles.workshops.headings.facilitators') do
- facilitators = []
- workshop.active_facilitators.each do | facilitator |
- facilitators << facilitator.name
= facilitators.join ', '
= data_set(:h5, 'articles.workshops.headings.needs') do
- needs = []
- JSON.parse(workshop.needs || '[]').each do | need |
- needs << (_"workshop.options.needs.#{need}")
= _!(needs.join ', ')
= data_set(:h5, 'articles.workshops.headings.theme') do
= workshop.theme.present? ? (_"workshop.options.theme.#{workshop.theme}") : ''
= data_set(:h5, 'articles.workshops.headings.space') do
= workshop.space.present? ? (_"workshop.options.space.#{workshop.space}") : ''
= data_set(:h5, 'forms.labels.generic.info') do
= link_info_dlg truncate(workshop.info), (richtext workshop.info.html_safe), workshop.title
- if workshop.notes.present? && strip_tags(workshop.notes).length > 0
= data_set(:h5, 'forms.labels.generic.notes') do
= link_info_dlg truncate(workshop.notes), (richtext workshop.notes.html_safe), workshop.title
= hidden_field_tag :id, workshop.id
.flex-inputs
= location_select workshop.event_location_id, small: true, stretch: true
= block_select workshop.block.present? ? "#{workshop.block['day']}:#{workshop.block['block']}" : nil, small: true
.actions.next-prev
= button_tag :deschedule, value: :deschedule_workshop, class: [:delete, 'booked-only']
= button_tag :reschedule, value: :schedule_workshop, class: [:secondary, 'booked-only']
= button_tag :schedule_workshop, value: :schedule_workshop, class: 'not-booked-only'

5
app/views/conferences/admin/_stats.html.haml

@ -0,0 +1,5 @@
.details
= data_set(:h4, 'articles.admin.stats.headings.registrations') do
= @registrations.size
.actions
= link_to (_'links.download.Excel'), administration_step_path(@this_conference.slug, :stats, :format => :xlsx), class: [:button, :download]

18
app/views/conferences/admin/_workshop_times.html.haml

@ -0,0 +1,18 @@
.table.workshop-blocks
.table-tr.header
.table-th=_'forms.labels.generic.block_number'
.table-th=_'forms.labels.generic.time'
.table-th=_'forms.labels.generic.length'
.table-th=_'forms.labels.generic.days'
.table-th.form
- @workshop_blocks.each_with_index do | info, block |
- is_new = info['time'].blank?
= form_tag administration_update_path(@this_conference.slug, :workshop_times), class: ['table-tr', is_new ? 'new' : 'saved'] do
.table-th.center.big= is_new ? '' : (block + 1)
.table-td=time_select info['time'], small: true, label: false
.table-td=length_select info['length'], {small: true, label: false}, 0.5, 2
.table-td=checkboxes :days, @block_days, info['days'].map(&:to_i), 'date.day_names', vertical: true, small: true
.table-td.form
= hidden_field_tag :workshop_block, block
= button_tag :delete_block, value: :delete_block, class: [:small, :delete] if block == @workshop_blocks.length - 2
= button_tag (is_new ? :add_block : :update_block), value: :save_block, class: [:small, :add]

18
app/views/conferences/stats.xlsx.haml

@ -0,0 +1,18 @@
- key = @excel_data[:key] || 'excel.columns'
%table
%thead
%tr
- @excel_data[:columns].each do |column|
%th=_(@excel_data[:keys][column].present? ? @excel_data[:keys][column] : "#{key}.#{column.to_s}")
%tbody
- @excel_data[:data].each do |row|
%tr
- @excel_data[:columns].each do |column|
%td{class: @excel_data[:column_types][column].present? ? @excel_data[:column_types][column].to_s : nil}=_!row[column]
- format_xls 'table' do
- workbook use_autowidth: true
- format bg_color: '333333'
- format 'td', font_name: 'Calibri', bg_color: 'ffffff', fg_color: '333333'
- format 'th', font_name: 'Calibri', b: true, bg_color: '333333', fg_color: 'ffffff'
- format 'td.date', num_fmt: 22, font_name: 'Courier New', sz: 10

37
app/views/layouts/application.html.haml

@ -2,8 +2,7 @@
%html{ lang: I18n.locale.to_s }
%head
%meta{ charset: 'utf-8' }
%meta{ name: 'viewport', content: 'width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0' }
-#%title= (yield :title) + (content_for?(:title) ? (_!' | ') : '') + (_!'Bike!Bike!')
%meta{ name: 'viewport', content: 'width=device-width, initial-scale=1.0' }
- title = yield :title
%title=_!('Bike!Bike!' + (content_for?(:title) ? " - #{title}" : ''))
%meta{ name: 'description', content: (yield_or_default :description, I18n.t('page_descriptions.home')) }
@ -14,13 +13,11 @@
- @alt_lang_urls.each do |locale, url|
%link{ rel: :alternate, hreflang: locale, href: url }
- if content_for?(:og_image)
- og_image = yield :og_image
- og_image = request.base_url + og_image
%meta{property: 'og:title', content: title}
%meta{property: 'og:type', content: "website"}
%meta{property: 'og:image', content: (yield :og_image)}
-#%link{ href: asset_path('apple-touch-icon.png'), rel: 'apple-touch-icon' }
-#%link{ href: asset_path('apple-touch-icon-72x72.png'), rel: 'apple-touch-icon', sizes: '72x72' }
-#%link{ href: asset_path('apple-touch-icon-114x114.png'), rel: 'apple-touch-icon', sizes: '114x114' }
-#%link{ href: asset_path('apple-touch-icon-144x144.png'), rel: 'apple-touch-icon', sizes: '144x144' }
%meta{property: 'og:type', content: 'website'}
%meta{property: 'og:image', content: og_image}
= yield :head
%body{ class: page_style }
@ -45,16 +42,24 @@
= yield
#footer
%footer= render 'shared/footer'
- if @confirmation_dlg.present?
- if @confirmation_dlg.present? || @info_dlg.present?
#content-overlay
#overlay
.dlg#confirmation-dlg
.dlg-content
%h2.title=_'modals.confirm'
.dlg-inner
%p.message=''
%a.button.confirm=_'modals.yes_button'
%button.delete=_'modals.no_button'
- if @confirmation_dlg.present?
.dlg#confirmation-dlg
.dlg-content
%h2.title=_'modals.confirm'
.dlg-inner
%p.message=''
%a.button.confirm=_'modals.yes_button'
%button.delete.close=_'modals.no_button'
- if @info_dlg.present?
.dlg#info-dlg
.dlg-content
%h2.title=_'modals.info'
.dlg-inner
%p.message=''
%button.close=_'modals.done_button'
= yield :footer_scripts if content_for?(:footer_scripts)
= inline_scripts

2
config/environments/development.rb

@ -58,5 +58,5 @@ BikeBike::Application.configure do
# to deliver to the browser instead of email
config.action_mailer.delivery_method = :letter_opener
Paypal.sandbox!
# Paypal.sandbox!
end

1
config/locales/en.yml

@ -5533,6 +5533,7 @@ en:
needs: Needs
space: Space
theme: Theme
interested_count: Interested
Delete_Workshop: Delete Workshop
notes: Notes
Workshops: Workshops

5
db/migrate/20160703044620_add_block_to_workshops.rb

@ -0,0 +1,5 @@
class AddBlockToWorkshops < ActiveRecord::Migration
def change
add_column :workshops, :block, :json
end
end

3
db/schema.rb

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160630233219) do
ActiveRecord::Schema.define(version: 20160703044620) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -415,6 +415,7 @@ ActiveRecord::Schema.define(version: 20160630233219) do
t.string "locale"
t.integer "event_location_id"
t.boolean "needs_facilitators"
t.json "block"
end
end

Loading…
Cancel
Save