From 26f9dfde896f7ed4ec6328b841e8083b070737c7 Mon Sep 17 00:00:00 2001 From: Godwin Date: Tue, 5 Jul 2016 22:40:53 -0700 Subject: [PATCH] Schedule maker, stats, and minor fixes --- Gemfile | 5 +- app/assets/javascripts/main.js | 164 +++++---- app/assets/stylesheets/_application.scss | 332 +++++++++++++++--- app/controllers/conferences_controller.rb | 301 ++++++++++++++-- app/helpers/application_helper.rb | 48 ++- app/models/user.rb | 14 + app/models/workshop.rb | 12 +- app/views/conferences/admin/_edit.html.haml | 12 + .../conferences/admin/_housing.html.haml | 2 +- .../conferences/admin/_schedule.html.haml | 116 +++++- app/views/conferences/admin/_stats.html.haml | 5 + .../admin/_workshop_times.html.haml | 18 + app/views/conferences/stats.xlsx.haml | 18 + app/views/layouts/application.html.haml | 37 +- config/environments/development.rb | 2 +- config/locales/en.yml | 1 + .../20160703044620_add_block_to_workshops.rb | 5 + db/schema.rb | 3 +- 18 files changed, 912 insertions(+), 183 deletions(-) create mode 100644 app/views/conferences/admin/_workshop_times.html.haml create mode 100644 app/views/conferences/stats.xlsx.haml create mode 100644 db/migrate/20160703044620_add_block_to_workshops.rb diff --git a/Gemfile b/Gemfile index fe2563b..b6fd6c0 100644 --- a/Gemfile +++ b/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' diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 9061b9c..a49b1b8 100644 --- a/app/assets/javascripts/main.js +++ b/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(); })(); diff --git a/app/assets/stylesheets/_application.scss b/app/assets/stylesheets/_application.scss index 341d401..806691b 100644 --- a/app/assets/stylesheets/_application.scss +++ b/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); } } } diff --git a/app/controllers/conferences_controller.rb b/app/controllers/conferences_controller.rb index 0d8dc3e..6557991 100644 --- a/app/controllers/conferences_controller.rb +++ b/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 diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 4feb77a..44c2e88 100644 --- a/app/helpers/application_helper.rb +++ b/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…').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(']', '') diff --git a/app/models/user.rb b/app/models/user.rb index 6663aff..bbe7cab 100644 --- a/app/models/user.rb +++ b/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 diff --git a/app/models/workshop.rb b/app/models/workshop.rb index 2bed20f..7318a71 100644 --- a/app/models/workshop.rb +++ b/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) diff --git a/app/views/conferences/admin/_edit.html.haml b/app/views/conferences/admin/_edit.html.haml index 95a46d0..f50f0f1 100644 --- a/app/views/conferences/admin/_edit.html.haml +++ b/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 diff --git a/app/views/conferences/admin/_housing.html.haml b/app/views/conferences/admin/_housing.html.haml index 6e74688..85ad805 100644 --- a/app/views/conferences/admin/_housing.html.haml +++ b/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'] diff --git a/app/views/conferences/admin/_schedule.html.haml b/app/views/conferences/admin/_schedule.html.haml index 63fcade..ca62c31 100644 --- a/app/views/conferences/admin/_schedule.html.haml +++ b/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' diff --git a/app/views/conferences/admin/_stats.html.haml b/app/views/conferences/admin/_stats.html.haml index e69de29..eb97fc8 100644 --- a/app/views/conferences/admin/_stats.html.haml +++ b/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] diff --git a/app/views/conferences/admin/_workshop_times.html.haml b/app/views/conferences/admin/_workshop_times.html.haml new file mode 100644 index 0000000..ea185ab --- /dev/null +++ b/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] diff --git a/app/views/conferences/stats.xlsx.haml b/app/views/conferences/stats.xlsx.haml new file mode 100644 index 0000000..39a1f16 --- /dev/null +++ b/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 diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 0c7fa8b..02f4d61 100644 --- a/app/views/layouts/application.html.haml +++ b/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 diff --git a/config/environments/development.rb b/config/environments/development.rb index 2e1bda6..0aeda92 100644 --- a/config/environments/development.rb +++ b/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 diff --git a/config/locales/en.yml b/config/locales/en.yml index 5c8b64d..9c718b6 100644 --- a/config/locales/en.yml +++ b/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 diff --git a/db/migrate/20160703044620_add_block_to_workshops.rb b/db/migrate/20160703044620_add_block_to_workshops.rb new file mode 100644 index 0000000..7cf4e53 --- /dev/null +++ b/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 diff --git a/db/schema.rb b/db/schema.rb index ca8b4e8..51c4109 100644 --- a/db/schema.rb +++ b/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