diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index c019d5c..db7bb13 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -40,7 +40,9 @@ dlg.removeAttribute('aria-hidden'); dlg.setAttribute('role', 'alertdialog'); dlg.setAttribute('tabindex', '0'); - dlg.focus(); + if (!dlg.getAttribute('data-nofocus')) { + dlg.focus(); + } setTimeout(function() { dlg.classList.add('open'); }, 100); } window.closeOverlay = function(dlg, primaryContent, body) { @@ -114,6 +116,14 @@ return false; }); }); + var helpDlg = document.getElementById('help-dlg'); + forEachElement('[data-help-text]', function(link) { + link.addEventListener('click', function(event) { + event.preventDefault(); + openDlg(helpDlg, link); + return false; + }); + }); var loginDlg = document.getElementById('login-dlg'); forEachElement('[data-sign-in]', function(link) { link.addEventListener('click', function(event) { diff --git a/app/assets/javascripts/registrations.js b/app/assets/javascripts/registrations.js index f467202..db55a28 100644 --- a/app/assets/javascripts/registrations.js +++ b/app/assets/javascripts/registrations.js @@ -1,168 +1,200 @@ (function() { - var searchControl = document.getElementById('search'); + var searchControl = document.getElementById('search'); - function filterTable() { - forEach(document.getElementById('search-table').getElementsByTagName('TBODY')[0].getElementsByTagName('TR'), function(tr) { - if (tr.classList.contains('editable')) { - tr.classList.remove('hidden'); + function filterTable() { + forEach(document.getElementById('search-table').getElementsByTagName('TBODY')[0].getElementsByTagName('TR'), function(tr) { + if (tr.classList.contains('editable')) { + tr.classList.remove('hidden'); - var value = searchControl.value; - if (value) { - var words = value.split(/\s+/); - for (var i = 0; i < words.length; i++) { - var word = new RegExp(words[i].replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), "i"); - if (tr.innerHTML.search(word) == -1) { - tr.classList.add('hidden'); - } - } - } - } - }); - } + var value = searchControl.value; + if (value) { + var words = value.split(/\s+/); + for (var i = 0; i < words.length; i++) { + var word = new RegExp(words[i].replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), "i"); + if (tr.innerHTML.search(word) == -1) { + tr.classList.add('hidden'); + } + } + } + } + }); + } - // ref = https://davidwalsh.name/element-matches-selector - function selectorMatches(el, selector) { - var p = Element.prototype; - var f = p.matches || p.webkitMatchesSelector || p.mozMatchesSelector || p.msMatchesSelector || function(s) { - return [].indexOf.call(document.querySelectorAll(s), this) !== -1; - }; - return f.call(el, selector); - } + // ref = https://davidwalsh.name/element-matches-selector + function selectorMatches(el, selector) { + if (el instanceof Element) { + var p = Element.prototype; + var f = p.matches || p.webkitMatchesSelector || p.mozMatchesSelector || p.msMatchesSelector || function(s) { + return [].indexOf.call(document.querySelectorAll(s), this) !== -1; + }; + return f.call(el, selector); + } + return false; + } - function saveRow(row) { - if (row) { - row.classList.remove('editing'); - var table = row.parentElement.parentElement; - var editRow = row.nextSibling; - var url = table.getAttribute('data-update-url'); - var data = new FormData(); - var request = new XMLHttpRequest(); - request.onreadystatechange = function() { - if (request.readyState == 4) { - row.classList.remove('requesting'); - if (request.status == 200 && request.responseText) { - var tempTable = document.createElement('table'); - tempTable.innerHTML = request.responseText; - var rows = tempTable.getElementsByTagName('tr'); - row.innerHTML = rows[0].innerHTML; - editRow.innerHTML = rows[1].innerHTML; - } - } - } - request.open('POST', url, true); - cells = editRow.getElementsByClassName('cell-editor'); - data.append('key', row.getAttribute('data-key')); - data.append('button', 'update'); - var changed = false; - for (var i = 0; i < cells.length; i++) { - if (cells[i].value !== cells[i].getAttribute('data-value')) { - data.append(cells[i].getAttribute('name'), cells[i].value); - changed = true; - } - } - if (changed) { - row.classList.add('requesting'); - request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); - request.send(data); - } - } - } + function saveRow(row) { + if (row) { + row.classList.remove('editing'); + var table = row.parentElement.parentElement; + var editRow = row.nextSibling; + var url = table.getAttribute('data-update-url'); + var data = new FormData(); + var request = new XMLHttpRequest(); + request.onreadystatechange = function() { + if (request.readyState == 4) { + row.classList.remove('requesting'); + if (request.status == 200 && request.responseText) { + var tempTable = document.createElement('table'); + tempTable.innerHTML = request.responseText; + var rows = tempTable.getElementsByTagName('tr'); + row.innerHTML = rows[0].innerHTML; + editRow.innerHTML = rows[1].innerHTML; + } + } + } + request.open('POST', url, true); + cells = editRow.getElementsByClassName('cell-editor'); + data.append('key', row.getAttribute('data-key')); + data.append('button', 'update'); + var changed = false; + for (var i = 0; i < cells.length; i++) { + if (cells[i].value !== cells[i].getAttribute('data-value')) { + data.append(cells[i].getAttribute('name'), cells[i].value); + changed = true; + } + } + if (changed) { + row.classList.add('requesting'); + request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + request.send(data); + } + } + } - function editTableCell(cell) { - if (cell && selectorMatches(cell, 'tr[data-key].editable td')) { - editTableRow(cell.parentElement, cell); - } else if (!cell || !selectorMatches(cell, 'tr[data-key].editable + tr, tr[data-key].editable + tr *')) { - var currentRow = document.querySelector('tr[data-key].editable.editing'); - if (currentRow) { - saveRow(currentRow); - } - } - } - function editTableRow(row, cell) { - if (selectorMatches(row, 'tr[data-key].editable')) { - var key = row.getAttribute('data-key'); - var currentRow = document.querySelector('tr[data-key].editable.editing'); - if (currentRow && currentRow.getAttribute('data-key') !== key) { - saveRow(currentRow); - } - var editor = row.nextSibling; - if (!row.classList.contains('editing')) { - row.classList.add('editing'); - var focusElement = null; - if (cell) { - focusElement = editor.querySelector('td[data-column-id="' + cell.getAttribute('data-column-id') + '"] .cell-editor'); - } - focusElement = focusElement || editor.getElementsByClassName('cell-editor')[0]; - focusElement.focus(); - if (focusElement.tagName === 'TEXTAREA' || (focusElement.tagName === 'INPUT' && focusElement.type != 'number' && focusElement.type != 'email')) { - focusElement.setSelectionRange(0, focusElement.value.length); - } - } - } - } - document.addEventListener('click', function (event) { editTableCell(event.target); }); - document.addEventListener('keyup', function (event) { - if (event.code === "Enter") { - var currentRow = document.querySelector('tr[data-key].editable.editing'); - if (currentRow) { - event.stopPropagation(); - event.preventDefault(); - var next = event.shiftKey ? 'previousSibling' : 'nextSibling'; - var cell = document.activeElement.parentElement.getAttribute('data-column-id'); - var row = currentRow[next] ? currentRow[next][next] : null; - if (!row) { - rows = currentRow.parentElement.children; - row = event.shiftKey ? rows[rows.length - 2] : rows[0]; - } - editTableRow(row, row.querySelector('[data-column-id="' + cell + '"]')); - } - } else if (event.code === "Escape") { - var currentRow = document.querySelector('tr[data-key].editable.editing'); - if (currentRow) { - event.stopPropagation(); - event.preventDefault(); - saveRow(currentRow); - } - } - }); - if (document.observe) { - document.observe("focusin", function (event) { editTableCell(event.target); }); - } else { - document.addEventListener("focus", function (event) { editTableCell(event.target); }, true); - } + function sortBy(table, column, dir) { + if (!dir) { + dir = 'down'; + } else if (dir == 'down') { + dir = 'up'; + } else { + dir = 'down'; + } - window.onbeforeunload = function() { - editTableCell(); - }; + var url = table.getAttribute('data-sort-url') + '?sort_column=' + column + '&sort_dir=' + dir; + var tableContainer = table.parentElement; - searchControl.addEventListener('keyup', filterTable); - searchControl.addEventListener('search', filterTable); + tableContainer.classList.add('requesting'); + var request = new XMLHttpRequest(); + request.onreadystatechange = function() { + if (request.readyState == 4) { + tableContainer.classList.remove('requesting'); + if (request.status == 200 && request.responseText) { + tableContainer.innerHTML = request.responseText; + } + } + } + request.open('GET', url, true); + request.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); + request.send(); + } - forEachElement('[data-expands]', function(button) { - button.addEventListener('click', function(event) { - var element = document.getElementById(event.target.getAttribute('data-expands')); - document.body.classList.add('expanded-element'); - element.classList.add('expanded'); - }); - }); - forEachElement('[data-contracts]', function(button) { - button.addEventListener('click', function(event) { - var element = document.getElementById(event.target.getAttribute('data-contracts')); - document.body.classList.remove('expanded-element'); - element.classList.remove('expanded'); - }); - }); - forEachElement('[data-opens-modal]', function(button) { - button.addEventListener('click', function(event) { - var element = document.getElementById(event.target.getAttribute('data-opens-modal')); - document.body.classList.add('modal-open'); - element.classList.add('open'); - }); - }); - forEachElement('[data-closes-modal]', function(element) { - element.addEventListener('click', function(event) { - document.getElementById(event.target.getAttribute('data-closes-modal')).classList.remove('open'); - document.body.classList.remove('modal-open'); - }); - }); + function editTableCell(cell) { + if (cell && selectorMatches(cell, 'tr[data-key].editable td')) { + editTableRow(cell.parentElement, cell); + } else if (cell && selectorMatches(cell, 'th[data-colname]')) { + sortBy(cell.parentElement.parentElement.parentElement, cell.getAttribute('data-colname'), cell.getAttribute('data-dir')); + } else if (!cell || !selectorMatches(cell, 'tr[data-key].editable + tr, tr[data-key].editable + tr *')) { + var currentRow = document.querySelector('tr[data-key].editable.editing'); + if (currentRow) { + saveRow(currentRow); + } + } + } + function editTableRow(row, cell) { + if (selectorMatches(row, 'tr[data-key].editable')) { + var key = row.getAttribute('data-key'); + var currentRow = document.querySelector('tr[data-key].editable.editing'); + if (currentRow && currentRow.getAttribute('data-key') !== key) { + saveRow(currentRow); + } + var editor = row.nextSibling; + if (!row.classList.contains('editing')) { + row.classList.add('editing'); + var focusElement = null; + if (cell) { + focusElement = editor.querySelector('td[data-column-id="' + cell.getAttribute('data-column-id') + '"] .cell-editor'); + } + focusElement = focusElement || editor.getElementsByClassName('cell-editor')[0]; + focusElement.focus(); + if (focusElement.tagName === 'TEXTAREA' || (focusElement.tagName === 'INPUT' && focusElement.type != 'number' && focusElement.type != 'email')) { + focusElement.setSelectionRange(0, focusElement.value.length); + } + } + } + } + document.addEventListener('click', function (event) { editTableCell(event.target); }); + document.addEventListener('keyup', function (event) { + if (event.code === "Enter") { + var currentRow = document.querySelector('tr[data-key].editable.editing'); + if (currentRow) { + event.stopPropagation(); + event.preventDefault(); + var next = event.shiftKey ? 'previousSibling' : 'nextSibling'; + var cell = document.activeElement.parentElement.getAttribute('data-column-id'); + var row = currentRow[next] ? currentRow[next][next] : null; + if (!row) { + rows = currentRow.parentElement.children; + row = event.shiftKey ? rows[rows.length - 2] : rows[0]; + } + editTableRow(row, row.querySelector('[data-column-id="' + cell + '"]')); + } + } else if (event.code === "Escape") { + var currentRow = document.querySelector('tr[data-key].editable.editing'); + if (currentRow) { + event.stopPropagation(); + event.preventDefault(); + saveRow(currentRow); + } + } + }); + if (document.observe) { + document.observe("focusin", function (event) { editTableCell(event.target); }); + } else { + document.addEventListener("focus", function (event) { editTableCell(event.target); }, true); + } + + window.onbeforeunload = function() { + editTableCell(); + }; + + searchControl.addEventListener('keyup', filterTable); + searchControl.addEventListener('search', filterTable); + + forEachElement('[data-expands]', function(button) { + button.addEventListener('click', function(event) { + var element = document.getElementById(event.target.getAttribute('data-expands')); + document.body.classList.add('expanded-element'); + element.classList.add('expanded'); + }); + }); + forEachElement('[data-contracts]', function(button) { + button.addEventListener('click', function(event) { + var element = document.getElementById(event.target.getAttribute('data-contracts')); + document.body.classList.remove('expanded-element'); + element.classList.remove('expanded'); + }); + }); + forEachElement('[data-opens-modal]', function(button) { + button.addEventListener('click', function(event) { + var element = document.getElementById(event.target.getAttribute('data-opens-modal')); + document.body.classList.add('modal-open'); + element.classList.add('open'); + }); + }); + forEachElement('[data-closes-modal]', function(element) { + element.addEventListener('click', function(event) { + document.getElementById(event.target.getAttribute('data-closes-modal')).classList.remove('open'); + document.body.classList.remove('modal-open'); + }); + }); })(); diff --git a/app/assets/stylesheets/_admin.scss b/app/assets/stylesheets/_admin.scss index 5da4120..b9980e0 100644 --- a/app/assets/stylesheets/_admin.scss +++ b/app/assets/stylesheets/_admin.scss @@ -29,6 +29,61 @@ nav.sub-nav { } table, .table { + tr.spacer td { + border: 0; + height: 0.5em; + } + + &[data-sort-url] { + [data-colname] { + position: relative; + cursor: pointer; + white-space: nowrap; + + @include after { + content: 'ꜜ'; + position: absolute; + bottom: -1em; + left: 50%; + font-size: 1.5em; + opacity: 0; + z-index: 2; + margin-left: -0.25em; + pointer-events: none; + @include _(transition, opacity 150ms ease-in-out); + } + + &:hover { + @include after { + opacity: 1; + } + } + } + + [data-dir] { + @include after { + opacity: 0.5; + } + + &:hover { + @include after { + content: 'ꜛ'; + } + } + } + [data-dir="up"] { + @include after { + content: 'ꜛ'; + } + + &:hover { + @include after { + content: 'ꜜ'; + } + } + } + } + th, td, .table-th, .table-td { &.center { text-align: center; @@ -51,45 +106,50 @@ table, .table { width: 1.75em; &.happy { - background-image: url("data:image/svg+xml;utf8,"); + @include after { + content: '\1F601'; + opacity: 0.5; + } } &.unhappy { - background-image: url('data:image/svg+xml;utf8,'); + @include after { + content: '\1F621'; + } } } } td, .table-td { &.inner-table { - padding: 0; + padding: 0.5em; table { margin: 0; width: 100%; } - tr:first-child { - td, th { - border-top: 0; - } - } + // tr:first-child { + // td, th { + // border-top: 0; + // } + // } - tr:last-child { - td, th { - border-bottom: 0; - } - } + // tr:last-child { + // td, th { + // border-bottom: 0; + // } + // } - td, th { - &:first-child { - border-left: 0 - } + // td, th { + // &:first-child { + // border-left: 0 + // } - &:last-child { - border-right: 0 - } - } + // &:last-child { + // border-right: 0 + // } + // } } &.bold { @@ -359,6 +419,7 @@ body.expanded-element { z-index: 1002; background-color: #F8F8F8; flex: 1; + padding-bottom: 1em; } .actions { @@ -636,6 +697,12 @@ nav.sub-menu { } } +#registrations-table { + .button { + margin-top: 0; + } +} + #main article #registration-admin-menu { margin: 1em 0 0; padding: 0; @@ -745,6 +812,15 @@ nav.sub-menu { } } +@include keyframes(unhappy) { + from { + @include _(transform, rotate(15deg)); + } + to { + @include _(transform, rotate(-15deg)); + } +} + #admin-housing, #admin-schedule { .guests-housed { margin-bottom: 1em; @@ -759,12 +835,33 @@ nav.sub-menu { .data { display: inline-block; font-size: 1.125em; + + @include after { + margin-left: 0.5em; + font-size: 1.5em; + } + + &.happy { + @include after { + content: '\1F60D'; + } + } + + &.unhappy { + @include after { + content: '\1F61E'; + } + } } } #housing-table { @include _(transition, opacity 1s ease-in-out); + table { + margin-left: 0; + } + &.loading { @include _(opacity, 0.5); pointer-events: none; @@ -780,6 +877,10 @@ nav.sub-menu { text-align: right; @include font-family(primary); } + + .name { + min-width: 10em; + } } } @@ -788,28 +889,17 @@ nav.sub-menu { td { background-color: lighten($colour-1, 40%); - &:hover { - background-color: $colour-1; - } - &.full { background-color: $gray; - &:hover { - background-color: #CCC; - } + .button { + background-color: #888; + } } } - a { - display: block; - color: $white; - text-align: center; - @include font-family(secondary); - - @include after { - display: none; - } + .button { + display: inline-block; } } @@ -819,15 +909,31 @@ nav.sub-menu { } td { - vertical-align: top; + vertical-align: middle; } .state { position: relative; font-family: inherit; + padding: 0; + position: relative; + width: 2em; + height: 2em; + + @include after { + position: absolute; + bottom: 0; + left: 0; + font-size: 1.5em; + } &.unhappy { cursor: pointer; + + @include after { + @include _(transform-origin, bottom); + @include _(animation, unhappy ease-in-out 1s infinite alternate both); + } } ul { @@ -837,7 +943,7 @@ nav.sub-menu { top: 0; background-color: $white; border: 0.1em solid #CCC; - padding: 0.25em 0.75em 0.25em 1.5em; + padding: 0.25em 0.75em; margin: 0; list-style-type: square; @include default-box-shadow(top, 2); @@ -846,7 +952,12 @@ nav.sub-menu { li { white-space: nowrap; - margin: 0; + margin: 0 0 0 1em; + + &:first-child:last-child { + list-style: none; + margin: 0; + } } &:hover { @@ -893,25 +1004,71 @@ nav.sub-menu { background-color: $white; width: 80%; margin: auto; - padding: 1em; height: 80%; cursor: default; @include default-box-shadow(top, 2); h3 { + text-align: center; margin: 0 0 1em; + padding: 0.5em 0.6667em; + color: $white; + background-color: $green; + @include _(text-stroke, 1px rgba(0, 0, 0, 0.25)); } } } } +.table-scroller.no-edit { + box-shadow: none; + background-color: transparent; + + table { + margin-bottom: 0; + } +} + +#table, #help-dlg { + .legend ul { + @include _-(display, flex); + list-style: none; + padding: 0; + + li { + @include _(flex, 1); + text-align: center; + margin-bottom: 0.5em; + padding: 0.125em 0.5em; + margin: 0.1em; + border: 0.1em solid $light-gray; + background-color: #F8F8F8; + @include font-family(secondary); + + &.other-host, &.other-space, &.bad-match { + opacity: 0.5; + } + + &.selected-space, &.other-space { + background-color: $colour-5; + } + + &.other-host { + background-color: $colour-1; + } + } + } +} #table { + display: flex; + flex-direction: column; position: relative; overflow: auto; height: 80%; - height: calc(100% - 4em); + height: calc(100% - 6.5em); background-color: $white; + margin: 1em; @include _(transition, background-color 250ms ease-in-out); &.loading { @@ -924,7 +1081,7 @@ nav.sub-menu { } table { - margin: 0 0 2em; + margin: 0 0 1em; } h4 { @@ -992,45 +1149,27 @@ nav.sub-menu { } } - .legend ul { - @include _-(display, flex); - list-style: none; - padding: 0; - - li { - @include _(flex, 1); - text-align: center; - margin-bottom: 0.5em; - padding: 0.125em 0.5em; - margin: 0.1em; - border: 0.1em solid $light-gray; - background-color: #F8F8F8; - @include font-family(secondary); + .p { + max-height: 4em; + overflow: auto; + } - &.other-host, &.other-space, &.bad-match { - opacity: 0.5; - } + .guest-table { + flex: 1; + display: flex; + } - &.selected-space, &.other-space { - background-color: $colour-5; - } + td, th { + white-space: nowrap; - &.other-host { - background-color: $colour-1; - } + &.break-ok { + white-space: normal; + min-width: 10em; } } - - .p { - max-height: 4em; - overflow: auto; - } } #admin-housing { - #table table { - min-width: 100em; - } #hosts { background-color: $white; diff --git a/app/assets/stylesheets/_application.scss b/app/assets/stylesheets/_application.scss index 4fca4e6..a958a9a 100644 --- a/app/assets/stylesheets/_application.scss +++ b/app/assets/stylesheets/_application.scss @@ -141,21 +141,6 @@ table, .table { background-color: transparent; border: 0; } - - &.state { - background-size: 1.333em; - background-repeat: no-repeat; - background-position: center; - width: 1.75em; - - &.happy { - background-image: url("data:image/svg+xml;utf8,"); - } - - &.unhappy { - background-image: url('data:image/svg+xml;utf8,'); - } - } } th, .table-th { @@ -167,42 +152,42 @@ table, .table { } } - td, .table-td { - &.inner-table { - padding: 0; + // td, .table-td { + // &.inner-table { + // padding: 0; - table { - margin: 0; - width: 100%; - } + // table { + // margin: 0; + // width: 100%; + // } - tr:first-child { - td, th { - border-top: 0; - } - } + // tr:first-child { + // td, th { + // border-top: 0; + // } + // } - tr:last-child { - td, th { - border-bottom: 0; - } - } + // tr:last-child { + // td, th { + // border-bottom: 0; + // } + // } - td, th { - &:first-child { - border-left: 0 - } - - &:last-child { - border-right: 0 - } - } - } - - &.bold { - @include font-family(secondary); - } - } + // td, th { + // &:first-child { + // border-left: 0 + // } + + // &:last-child { + // border-right: 0 + // } + // } + // } + + // &.bold { + // @include font-family(secondary); + // } + // } tbody th { width: 0.1rem; @@ -2449,13 +2434,14 @@ a.logo { .register-link { font-size: 1.25em; margin: 0.5em; - - .button { - @include _(animation, radiate 2s linear infinite alternate); - } } } +.help-link { + float: right; + background-color: $red; +} + .conference-details { .links { text-align: center; @@ -2906,6 +2892,29 @@ body { text-align: left; } + #help-dlg { + .dlg-content { + @include _-(display, flex); + @include _(flex-direction, column); + max-width: 60rem; + } + + .dlg-inner { + overflow: auto; + } + + .title { + background-color: $red; + font-size: 2em; + text-align: left; + } + + .message { + text-align: left; + font-size: 1.125em; + } + } + #info-dlg .message { text-align: left; @@ -2956,10 +2965,6 @@ body { to { background-position: 60px 30px; } } -@include keyframes(radiate) { - to { background-color: $green; } -} - html :focus { outline: 0; } diff --git a/app/assets/stylesheets/bumbleberry-settings.json b/app/assets/stylesheets/bumbleberry-settings.json index 8a08ee5..fe4fa1a 100644 --- a/app/assets/stylesheets/bumbleberry-settings.json +++ b/app/assets/stylesheets/bumbleberry-settings.json @@ -10,7 +10,7 @@ "and_chr": ["59"], "chrome": ["59"], "edge": ["13"], - "firefox": ["50"], + "firefox": ["52"], "ie": ["11"], "ios_saf": ["8", "9"] } diff --git a/app/controllers/conference_administration_controller.rb b/app/controllers/conference_administration_controller.rb index f0cd794..83853ff 100644 --- a/app/controllers/conference_administration_controller.rb +++ b/app/controllers/conference_administration_controller.rb @@ -1,7 +1,10 @@ require 'geocoder/calculations' require 'rest_client' +require 'registration_controller_helper' class ConferenceAdministrationController < ApplicationController + include RegistrationControllerHelper + def administration set_conference return do_403 unless @this_conference.host? current_user @@ -224,6 +227,47 @@ class ConferenceAdministrationController < ApplicationController return respond_to do |format| format.xlsx { render xlsx: '../conferences/stats', filename: "stats-#{DateTime.now.strftime('%Y-%m-%d')}" } end + else + if params[:sort_column] + col = params[:sort_column].to_sym + @excel_data[:data].sort_by! do |row| + value = row[col] + + if row[:raw_values].key?(col) + value = if row[:raw_values][col].is_a?(TrueClass) + 't' + elsif row[:raw_values][col].is_a?(FalseClass) + '' + else + row[:raw_values][col] + end + elsif value.is_a?(City) + value = value.sortable_string + end + + if value.nil? + case @excel_data[:column_types][col] + when :datetime, [:date, :day] + value = Date.new + when :money + value = 0 + else + value = '' + end + end + + value + end + + if params[:sort_dir] == 'up' + @sort_dir = :up + @excel_data[:data].reverse! + end + + @sort_column = col + else + @sort_column = :name + end end @registration_count = @registrations.size @@ -249,6 +293,10 @@ class ConferenceAdministrationController < ApplicationController end end end + + if request.xhr? + render html: view_context.html_table(@excel_data, view_context.registrations_table_options) + end end def administrate_stats @@ -372,6 +420,7 @@ class ConferenceAdministrationController < ApplicationController return respond_to do |format| format.xlsx { render xlsx: '../conferences/stats', filename: "housing" } end + else end end @@ -428,16 +477,21 @@ class ConferenceAdministrationController < ApplicationController columns: [ :name, :email, + :date, :status, :is_attending, :is_subscribed, :registration_fees_paid, - :date, + :payment_currency, + :payment_method, :city, :preferred_language ] + User.AVAILABLE_LANGUAGES.map { |l| "language_#{l}".to_sym } + - [ + [ + :group_ride, + :organization, + :org_non_member_interest, :arrival, :departure, :housing, @@ -451,8 +505,7 @@ class ConferenceAdministrationController < ApplicationController :last_day, :address, :phone - ] + ConferenceRegistration.all_spaces + - ConferenceRegistration.all_considerations + [ + ] + ConferenceRegistration.all_spaces + [ :notes ], column_types: { @@ -460,6 +513,7 @@ class ConferenceAdministrationController < ApplicationController date: :datetime, email: :email, companion_email: :email, + org_non_member_interest: :text, arrival: [:date, :day], departure: [:date, :day], registration_fees_paid: :money, @@ -476,6 +530,9 @@ class ConferenceAdministrationController < ApplicationController is_subscribed: 'articles.user_settings.headings.email_subscribe', city: 'forms.labels.generic.event_location', date: 'articles.conference_registration.terms.Date', + group_ride: 'articles.conference_registration.step_names.group_ride', + organization: 'articles.conference_registration.step_names.org_select', + org_non_member_interest: 'articles.conference_registration.step_names.org_non_member_interest', preferred_language: 'articles.conference_registration.terms.Preferred_Languages', arrival: 'forms.labels.generic.arrival', departure: 'forms.labels.generic.departure', @@ -485,13 +542,15 @@ class ConferenceAdministrationController < ApplicationController companion: 'articles.conference_registration.terms.companion', companion_email: 'articles.conference_registration.terms.companion_email', registration_fees_paid: 'articles.conference_registration.headings.fees_paid', + payment_currency: 'forms.labels.generic.payment_currency', + payment_method: 'forms.labels.generic.payment_method', other: 'forms.labels.generic.other_notes', - can_provide_housing: 'articles.conference_registration.can_provide_housing', + can_provide_housing: 'articles.conference_registration.housing_provider', first_day: 'forms.labels.generic.first_day', last_day: 'forms.labels.generic.last_day', notes: 'forms.labels.generic.notes', phone: 'forms.labels.generic.phone', - address: 'forms.labels.generic.address', + address: 'forms.labels.generic.address_short', contact_info: 'articles.conference_registration.headings.contact_info', questions: 'articles.conference_registration.headings.questions', hosting: 'articles.conference_registration.headings.hosting' @@ -504,7 +563,7 @@ class ConferenceAdministrationController < ApplicationController end User.AVAILABLE_LANGUAGES.each do |l| - @excel_data[:keys]["language_#{l}".to_sym] = "languages.#{l.to_s}" + @excel_data[:keys]["language_#{l}".to_sym] = "languages.#{l.to_s}" end ConferenceRegistration.all_spaces.each do |s| @excel_data[:column_types][s] = :number @@ -517,41 +576,51 @@ class ConferenceAdministrationController < ApplicationController user = r.user_id ? User.where(id: r.user_id).first : nil if user.present? companion = view_context.companion(r) - companion = companion.is_a?(User) ? companion.name : (view_context._"articles.conference_registration.terms.registration_status.#{companion}") if companion.present? + companion = companion.is_a?(User) ? companion.name : I18n.t("articles.conference_registration.terms.registration_status.#{companion}") if companion.present? steps = r.steps_completed || [] if id.nil? || id == r.id + registration_data = r.data || {} housing_data = r.housing_data || {} availability = housing_data['availability'] || [] availability[0] = Date.parse(availability[0]) if availability[0].present? availability[1] = Date.parse(availability[1]) if availability[1].present? + org = r.user.organizations.first data = { id: r.id, name: user.firstname || '', email: user.email || '', - status: (view_context._"articles.conference_registration.terms.registration_status.#{view_context.registration_status(r)}"), - is_attending: (view_context._"articles.conference_registration.questions.bike.#{r.is_attending == 'n' ? 'no' : 'yes'}"), - is_subscribed: user.is_subscribed == false ? (view_context._'articles.conference_registration.questions.bike.no') : '', + status: I18n.t("articles.conference_registration.terms.registration_status.#{view_context.registration_status(r)}"), + is_attending: I18n.t("articles.conference_registration.questions.bike.#{r.is_attending == 'n' ? 'no' : 'yes'}"), + is_subscribed: user.is_subscribed == false ? I18n.t('articles.conference_registration.questions.bike.no') : '', date: r.created_at ? r.created_at.strftime("%F %T") : '', city: r.city || '', preferred_language: user.locale.present? ? (view_context.language_name user.locale) : '', arrival: r.arrival ? r.arrival.strftime("%F %T") : '', departure: r.departure ? r.departure.strftime("%F %T") : '', - housing: r.housing.present? ? (view_context._"articles.conference_registration.questions.housing.#{r.housing}") : '', - bike: r.bike.present? ? (view_context._"articles.conference_registration.questions.bike.#{r.bike}") : '', - food: r.food.present? ? (view_context._"articles.conference_registration.questions.food.#{r.food}") : '', + group_ride: registration_data['group_ride'].present? ? I18n.t("forms.actions.generic.#{registration_data['group_ride']}") : '', + organization: org.present? ? org.name : '', + org_non_member_interest: registration_data['non_member_interest'], + housing: r.housing.present? ? I18n.t("articles.conference_registration.questions.housing_short.#{r.housing}") : '', + bike: r.bike.present? ? I18n.t("articles.conference_registration.questions.bike.#{r.bike}") : '', + food: r.food.present? ? I18n.t("articles.conference_registration.questions.food.#{r.food}") : '', companion: companion, companion_email: (housing_data['companion'] || { 'email' => ''})['email'], - registration_fees_paid: r.registration_fees_paid, - other: r.allergies.present? ? "#{r.allergies}\n\n#{r.other}" : r.other, - can_provide_housing: r.can_provide_housing ? (view_context._'articles.conference_registration.questions.bike.yes') : '', + registration_fees_paid: registration_data['payment_amount'], + payment_currency: registration_data['payment_currency'], + payment_method: registration_data['payment_method'].present? ? I18n.t("forms.labels.generic.payment_type.#{registration_data['payment_method']}") : '', + other: [r.allergies, r.other, housing_data['other']].compact.join("\n\n"), + can_provide_housing: r.can_provide_housing.nil? ? '' : I18n.t("articles.conference_registration.questions.bike.#{r.can_provide_housing ? 'yes' : 'no'}"), first_day: availability[0].present? ? availability[0].strftime("%F %T") : '', last_day: availability[1].present? ? availability[1].strftime("%F %T") : '', notes: housing_data['notes'], address: housing_data['address'], phone: housing_data['phone'], raw_values: { + group_ride: registration_data['group_ride'], + registration_fees_paid: registration_data['payment_amount'].to_f, + payment_method: registration_data['payment_method'], housing: r.housing, bike: r.bike, food: r.food, @@ -560,12 +629,13 @@ class ConferenceAdministrationController < ApplicationController preferred_language: user.locale, is_attending: r.is_attending != 'n', is_subscribed: user.is_subscribed, - can_provide_housing: r.can_provide_housing, + can_provide_housing: r.can_provide_housing.to_s, first_day: availability[0].present? ? availability[0].to_date : nil, last_day: availability[1].present? ? availability[1].to_date : nil }, html_values: { date: r.created_at.present? ? r.created_at.strftime("%F %T") : '', + registration_fees_paid: registration_data['payment_amount'].present? ? view_context.number_to_currency(registration_data['payment_amount'].to_f, unit: '$') : '', arrival: r.arrival.present? ? view_context.date(r.arrival.to_date, :span_same_year_date_1) : '', departure: r.departure.present? ? view_context.date(r.departure.to_date, :span_same_year_date_1) : '', first_day: availability[0].present? ? view_context.date(availability[0].to_date, :span_same_year_date_1) : '', @@ -574,17 +644,13 @@ class ConferenceAdministrationController < ApplicationController } User.AVAILABLE_LANGUAGES.each do |l| can_speak = ((user.languages || []).include? l.to_s) - data["language_#{l}".to_sym] = (can_speak ? (view_context._'articles.conference_registration.questions.bike.yes') : '') + data["language_#{l}".to_sym] = (can_speak ? I18n.t('articles.conference_registration.questions.bike.yes') : '') data[:raw_values]["language_#{l}".to_sym] = can_speak end ConferenceRegistration.all_spaces.each do |s| space = (housing_data['space'] || {})[s.to_s] data[s] = space.present? ? space.to_i : nil - end - ConferenceRegistration.all_considerations.each do |c| - consideration = (housing_data['considerations'] || []).include?(c.to_s) - data[c] = (consideration ? (view_context._'articles.conference_registration.questions.bike.yes') : '') - data[:raw_values][c] = consideration + data[:raw_values][s] = space.present? ? space.to_i : 0 end @excel_data[:data] << data end @@ -593,18 +659,18 @@ class ConferenceAdministrationController < ApplicationController if html_format yes_no = [ - [(view_context._"articles.conference_registration.questions.bike.yes"), true], - [(view_context._"articles.conference_registration.questions.bike.no"), false] + [I18n.t('forms.actions.generic.yes'), true], + [I18n.t('forms.actions.generic.no'), false] ] @column_options = { housing: ConferenceRegistration.all_housing_options.map { |h| [ - (view_context._"articles.conference_registration.questions.housing.#{h}"), + I18n.t("articles.conference_registration.questions.housing_short.#{h}"), h] }, bike: ConferenceRegistration.all_bike_options.map { |b| [ - (view_context._"articles.conference_registration.questions.bike.#{b}"), + I18n.t("articles.conference_registration.questions.bike.#{b}"), b] }, food: ConferenceRegistration.all_food_options.map { |f| [ - (view_context._"articles.conference_registration.questions.food.#{f}"), + I18n.t("articles.conference_registration.questions.food.#{f}"), f] }, arrival: view_context.conference_days_options_list(:before_plus_one), departure: view_context.conference_days_options_list(:after_minus_one), @@ -615,16 +681,19 @@ class ConferenceAdministrationController < ApplicationController is_subscribed: [yes_no.last], can_provide_housing: yes_no, first_day: view_context.conference_days_options_list(:before), - last_day: view_context.conference_days_options_list(:after) + last_day: view_context.conference_days_options_list(:after), + group_ride: [:yes, :no, :maybe].map { |o| [I18n.t("forms.actions.generic.#{o}"), o] }, + payment_currency: Conference.default_currencies.map { |c| [c, c] }, + payment_method: ConferenceRegistration.all_payment_methods.map { |c| [I18n.t("forms.labels.generic.payment_type.#{c}"), c] } } User.AVAILABLE_LANGUAGES.each do |l| @column_options["language_#{l}".to_sym] = [ - [(view_context._"articles.conference_registration.questions.bike.yes"), true] + [I18n.t("articles.conference_registration.questions.bike.yes"), true] ] end ConferenceRegistration.all_considerations.each do |c| @column_options[c.to_sym] = [ - [(view_context._"articles.conference_registration.questions.bike.yes"), true] + [I18n.t("articles.conference_registration.questions.bike.yes"), true] ] end end @@ -633,7 +702,7 @@ class ConferenceAdministrationController < ApplicationController def get_housing_data @hosts = {} @guests = {} - ConferenceRegistration.where(:conference_id => @this_conference.id).each do |registration| + ConferenceRegistration.where(conference_id: @this_conference.id).each do |registration| if registration.can_provide_housing @hosts[registration.id] = registration elsif registration.housing.present? && registration.housing != 'none' @@ -657,6 +726,7 @@ class ConferenceAdministrationController < ApplicationController @housing_data[id][:space][s.to_sym] = size end end + @unhappy_people = Set.new @guests_housed = 0 @@ -691,7 +761,7 @@ class ConferenceAdministrationController < ApplicationController if (guest.housing == 'house' && space == :tent) || (guest.housing == 'tent' && (space == :bed_space || space == :floor_space)) - @housing_data[host_id][:guest_data][guest_id][:warnings][:space] = { actual: (view_context._"forms.labels.generic.#{space.to_s}"), expected: (view_context._"articles.conference_registration.questions.housing.#{guest.housing}")} + @housing_data[host_id][:guest_data][guest_id][:warnings][:space] = { actual: (view_context._"forms.labels.generic.#{space.to_s}"), expected: (view_context._"articles.conference_registration.questions.housing_short.#{guest.housing}")} end if data['companion'].present? @@ -717,6 +787,7 @@ class ConferenceAdministrationController < ApplicationController end end end + @unhappy_people << guest_id if @housing_data[host_id][:guest_data][guest_id][:errors].present? || @housing_data[host_id][:guest_data][guest_id][:warnings].present? else # make sure the housing data is empty if the host wasn't found, just in case something happened to the host @guests[guest_id].housing_data ||= {} @@ -739,6 +810,7 @@ class ConferenceAdministrationController < ApplicationController if @housing_data[id][:guests][space].size > space_available @housing_data[id][:warnings][:space][space] << :overbooked + @unhappy_people << id end end end @@ -1021,12 +1093,22 @@ class ConferenceAdministrationController < ApplicationController registration.city_id = city.id end end - when :housing, :bike, :food, :allergies, :other - registration.send("#{key.to_s}=", value) + when :housing, :bike, :food + registration.send("#{key}=", value) + when :other + registration.housing_data ||= {} + registration.housing_data[key] = value + # delete deprecated values + registration.allergies = nil + registration.other = nil when :registration_fees_paid - registration.registration_fees_paid = value.to_i + registration.data ||= {} + registration.data['payment_amount'] = value.to_f + when :group_ride, :payment_currency, :payment_method + registration.data ||= {} + registration.data[key.to_s] = value.present? ? value.to_sym : nil when :can_provide_housing - registration.send("#{key.to_s}=", value.present?) + registration.send("#{key.to_s}=", value == 'true' ? true : (value == 'false' ? false : nil)) when :arrival, :departure registration.send("#{key.to_s}=", value.present? ? Date.parse(value) : nil) when :companion_email diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb index 57d59b8..2cb13b2 100644 --- a/app/helpers/admin_helper.rb +++ b/app/helpers/admin_helper.rb @@ -160,21 +160,6 @@ module AdminHelper end end - # housing_data = guest.housing_data || [] - - # if housing_data['host'].present? - # if housing_data['host'] == host.id - # return space == housing_data['space'] ? :selected_space : :other_space - # end - - # return :other_host - # end - - # if space_matches?(space, guest.housing) && available_dates_match?(host, guest) - # return :good_match - # end - - # return :bad_match return :good_match end @@ -190,6 +175,7 @@ module AdminHelper def available_dates_match?(host, guest) return false unless host.housing_data['availability'].present? && host.housing_data['availability'][1].present? + return false unless guest.arrival.present? && guest.departure.present? if host.housing_data['availability'][0] <= guest.arrival && host.housing_data['availability'][1] >= guest.departure return true @@ -208,4 +194,10 @@ module AdminHelper end).html_safe end).html_safe end + + def admin_help_pages + return { + housing: :housing + } + end end diff --git a/app/helpers/geocoder_helper.rb b/app/helpers/geocoder_helper.rb index 6f5d29a..b607da3 100644 --- a/app/helpers/geocoder_helper.rb +++ b/app/helpers/geocoder_helper.rb @@ -74,7 +74,7 @@ module GeocoderHelper return nil unless city.present? hash = Hash.new - region_translation = region.present? && country.present? ? _("geography.subregions.#{country}.#{region}", locale: locale) : '' + region_translation = region.present? && country.present? ? I18n.t("geography.subregions.#{country}.#{region}", locale: locale, resolve: false) : '' country_translation = country.present? ? _("geography.countries.#{country}", locale: locale) : '' hash[:city] = _!(city) if city.present? hash[:region] = region_translation if region_translation.present? diff --git a/app/helpers/registration_helper.rb b/app/helpers/registration_helper.rb index 0a71205..7b6c9c4 100644 --- a/app/helpers/registration_helper.rb +++ b/app/helpers/registration_helper.rb @@ -8,6 +8,7 @@ module RegistrationHelper def registration_status(registration) return :unregistered if registration.nil? + return :cancelled if registration.is_attending == 'n' return registration.status end @@ -47,7 +48,7 @@ module RegistrationHelper completed_steps = registration.steps_completed || [] last_step = nil steps = current_registration_steps(registration) || [] - steps.each do | step | + steps.each do |step| # return the last enabled step if this one is disabled return last_step unless step[:enabled] diff --git a/app/helpers/table_helper.rb b/app/helpers/table_helper.rb index 4b20d3c..c355465 100644 --- a/app/helpers/table_helper.rb +++ b/app/helpers/table_helper.rb @@ -1,7 +1,12 @@ module TableHelper def html_edit_table(excel_data, options = {}) attributes = { class: options[:class], id: options[:id] } - attributes[:data] = { 'update-url' => options[:editable] } if options[:editable].present? + if options[:editable].present? || options[:sortable].present? + attributes[:data] = { + 'update-url' => options[:editable], + 'sort-url' => options[:sortable] + } + end if options[:column_names].is_a? Hash return content_tag(:table, attributes) do @@ -9,11 +14,11 @@ module TableHelper column_names = {} (content_tag(:thead) do headers = '' - options[:column_names].each do | header_name, columns | + options[:column_names].each do |header_name, columns| column_names[header_name] ||= [] headers += content_tag(:th, excel_data[:keys][header_name].present? ? _(excel_data[:keys][header_name]) : '', colspan: 2) row_count = columns.size - columns.each do | column | + columns.each do |column| column_names[header_name] << column if (options[:row_spans] || {})[column].present? row_count += (options[:row_spans][column] - 1) @@ -30,15 +35,18 @@ module TableHelper for i in 0...max_columns columns_html = '' - column_names.each do | header_name, columns | + column_names.each do |header_name, columns| column = columns[i] if column.present? attributes = { class: [excel_data[:column_types][column]], data: { 'column-id' => column } } if (options[:row_spans] || {})[column].present? attributes[:rowspan] = options[:row_spans][column] end - columns_html += content_tag(:th, excel_data[:keys][column].present? ? _(excel_data[:keys][column]) : '', rowspan: attributes[:rowspan]) + - edit_column(nil, column, nil, attributes, excel_data, options) + + column_text = excel_data[:keys][column].present? ? _(excel_data[:keys][column]) : '' + + columns_html += content_tag(:th, column_text.html_safe, rowspan: attributes[:rowspan]) + + edit_column(nil, column, nil, attributes, excel_data, options) elsif column != false columns_html += content_tag(:td, ' ', colspan: 2, class: :empty) end @@ -56,8 +64,9 @@ module TableHelper if (excel_data[:column_types] || {})[column] != :table && ((options[:column_names] || []).include? column) rows += content_tag(:tr, { class: 'always-edit', data: { key: '' } }) do attributes = { class: [excel_data[:column_types][column]], data: { 'column-id' => column } } - columns = content_tag(:th, excel_data[:keys][column].present? ? _(excel_data[:keys][column]) : '') + - edit_column(nil, column, nil, attributes, excel_data, options) + column_text = excel_data[:keys][column].present? ? _(excel_data[:keys][column]) : '' + + columns = content_tag(:th, column_text.html_safe) + edit_column(nil, column, nil, attributes, excel_data, options) end end end @@ -70,7 +79,16 @@ module TableHelper def html_table(excel_data, options = {}) options[:html] = true attributes = { class: options[:class], id: options[:id] } - attributes[:data] = { 'update-url' => options[:editable] } if options[:editable].present? + + if options[:editable].present? + attributes[:data] ||= {} + attributes[:data]['update-url'] = options[:editable] + end + if options[:sortable].present? + attributes[:data] ||= {} + attributes[:data]['sort-url'] = options[:sortable] + end + content_tag(:table, attributes) do (content_tag(:thead) do content_tag(:tr, excel_header_columns(excel_data)) @@ -106,7 +124,17 @@ module TableHelper data[:columns].each do |column| unless data[:column_types].present? && data[:column_types][column] == :table - columns += content_tag(:th, data[:keys][column].present? ? _(data[:keys][column]) : '', class: class_name) + column_text = data[:keys][column].present? ? _(data[:keys][column]) : '' + attrs = { class: class_name } + + unless @sort_column.nil? + attrs[:data] = { colname: column } + + if @sort_column == column + attrs[:data][:dir] = @sort_dir || :down + end + end + columns += content_tag(:th, column_text.html_safe, attrs) end end @@ -214,7 +242,7 @@ module TableHelper def edit_column(row, column, value, attributes, data, options) attributes[:class] << 'has-editor' - raw_value = row.present? ? (row[:raw_values][column] || value) : nil + raw_value = row.present? ? ((row[:raw_values] || {})[column] || value) : nil if row.present? && options[:html] && row[:html_values].present? && row[:html_values][column].present? value = row[:html_values][column] @@ -299,14 +327,16 @@ module TableHelper ] + User.AVAILABLE_LANGUAGES.map { |l| "language_#{l}".to_sym }, questions: [ :registration_fees_paid, + :payment_currency, + :payment_method, :is_attending, :arrival, :departure, :housing, :bike, :food, + :group_ride, :companion_email, - :allergies, :other ], hosting: [ @@ -315,14 +345,15 @@ module TableHelper :phone, :first_day, :last_day - ] + ConferenceRegistration.all_spaces + - ConferenceRegistration.all_considerations + [ + ] + ConferenceRegistration.all_spaces + [ :notes ] }, row_spans: { - allergies: 3, - other: 2 + other: 2, + address: 2, + city: 2, + notes: 4 }, required_columns: [:name, :email], editable: administration_update_path(@this_conference.slug, @admin_step), @@ -337,12 +368,15 @@ module TableHelper primary_key: :id, column_names: [ :registration_fees_paid, + :payment_currency, + :payment_method, :is_attending, :is_subscribed, :city, :preferred_language, :arrival, :departure, + :group_ride, :housing, :bike, :food, @@ -360,6 +394,7 @@ module TableHelper ConferenceRegistration.all_spaces + ConferenceRegistration.all_considerations, editable: administration_update_path(@this_conference.slug, @admin_step), + sortable: administration_step_path(@this_conference.slug, @admin_step), column_options: @column_options } end diff --git a/app/helpers/widgets_helper.rb b/app/helpers/widgets_helper.rb index c18fc5a..a9f4b23 100644 --- a/app/helpers/widgets_helper.rb +++ b/app/helpers/widgets_helper.rb @@ -99,75 +99,86 @@ module WidgetsHelper def host_guests_table(registration) id = registration.id html = '' + first_row = true @housing_data[id][:guests].each do |area, guests| guest_rows = '' - guests.each do |guest_id, guest| - status_html = '' + space_size = (@housing_data[id][:space][area] || 0) - @housing_data[id][:guest_data][guest_id][:errors].each do |error, value| - if value.is_a?(Array) - value.each do |v| - status_html += content_tag(:li, _("errors.messages.housing.space.#{error.to_s}", vars: v)) + if space_size > 0 || guests.size > 0 + guests.each do |guest_id, guest| + status_html = '' + + @housing_data[id][:guest_data][guest_id][:errors].each do |error, value| + if value.is_a?(Array) + value.each do |v| + status_html += content_tag(:li, _("errors.messages.housing.space.#{error.to_s}", vars: v)) + end + else + status_html += content_tag(:li, _("errors.messages.housing.space.#{error.to_s}", vars: value)) end - else - status_html += content_tag(:li, _("errors.messages.housing.space.#{error.to_s}", vars: value)) end - end - @housing_data[id][:guest_data][guest_id][:warnings].each do |error, value| - if value.is_a?(Array) - value.each do |v| - status_html += content_tag(:li, _("warnings.messages.housing.space.#{error.to_s}", v)) + @housing_data[id][:guest_data][guest_id][:warnings].each do |error, value| + if value.is_a?(Array) + value.each do |v| + status_html += content_tag(:li, _("warnings.messages.housing.space.#{error.to_s}", v)) + end + else + status_html += content_tag(:li, _("warnings.messages.housing.space.#{error.to_s}", vars: value)) end - else - status_html += content_tag(:li, _("warnings.messages.housing.space.#{error.to_s}", vars: value)) + end + + if status_html.present? + status_html = content_tag(:ul, status_html.html_safe) + end + + guest_rows += content_tag :tr, id: "hosted-guest-#{guest_id}" do + (content_tag :td, guest[:guest].user.name) + + (content_tag :td do + (guest[:guest].from + + (content_tag :a, (_'actions.workshops.Remove'), href: '#', class: 'remove-guest', data: { guest: guest_id })).html_safe + end) + + (content_tag :td, status_html.html_safe, class: [:state, status_html.present? ? :unhappy : :happy]) + end + end + + # add empty rows to represent empty guest spots + for i in guests.size...space_size + guest_rows += content_tag :tr, class: 'empty-space' do + (content_tag :td, ' '.html_safe, colspan: 2) + + (content_tag :td) end end + status_html = '' + if @housing_data[id][:warnings].present? && @housing_data[id][:warnings][:space].present? && @housing_data[id][:warnings][:space][area].present? + @housing_data[id][:warnings][:space][area].each do |w| + status_html += content_tag(:li, _("warnings.messages.housing.space.#{w.to_s}")) + end + end if status_html.present? status_html = content_tag(:ul, status_html.html_safe) end - guest_rows += content_tag :tr, id: "hosted-guest-#{guest_id}" do - (content_tag :td, guest[:guest].user.name) + - (content_tag :td do - (guest[:guest].from + - (content_tag :a, (_'actions.workshops.Remove'), href: '#', class: 'remove-guest', data: { guest: guest_id })).html_safe - end) + - (content_tag :td, status_html.html_safe, class: [:state, status_html.present? ? :unhappy : :happy]) + unless first_row + html += content_tag :tr, class: :spacer do + content_tag :td, '', colspan: 3 + end end - end - space_size = (@housing_data[id][:space][area] || 0) - - # add empty rows to represent empty guest spots - for i in guests.size...space_size - guest_rows += content_tag :tr, class: 'empty-space' do - (content_tag :td, ' '.html_safe, colspan: 2) + - (content_tag :td) + html += content_tag :tr do + (content_tag :th, (_"forms.labels.generic.#{area}"), colspan: 2) + + (content_tag :th, status_html.html_safe, class: [:state, status_html.present? ? :unhappy : :happy]) end - end - - status_html = '' - if @housing_data[id][:warnings].present? && @housing_data[id][:warnings][:space].present? && @housing_data[id][:warnings][:space][area].present? - @housing_data[id][:warnings][:space][area].each do |w| - status_html += content_tag(:li, _("warnings.messages.housing.space.#{w.to_s}")) + html += guest_rows + html += content_tag :tr, class: 'place-guest' do + content_tag :td, class: guests.size >= space_size ? 'full' : nil, colspan: 3 do + content_tag :a, (_"forms.actions.generic.place_guest_in.#{area}"), class: 'select-guest button small', href: '#', data: { host: id, space: area } + end end - end - if status_html.present? - status_html = content_tag(:ul, status_html.html_safe) - end - html += content_tag :tr do - (content_tag :th, (_"forms.labels.generic.#{area}"), colspan: 2) + - (content_tag :th, status_html.html_safe, class: [:state, status_html.present? ? :unhappy : :happy]) - end - html += guest_rows - html += content_tag :tr, class: 'place-guest' do - content_tag :td, class: guests.size >= space_size ? 'full' : nil, colspan: 3 do - content_tag :a, (_'forms.actions.generic.place_guest'), class: 'select-guest', href: '#', data: { host: id, space: area } - end + first_row = false end end @@ -181,21 +192,31 @@ module WidgetsHelper id = registration.id @housing_data[id][:guests].each do |area, guests| max_space = @housing_data[id][:space][area] || 0 - area_name = (_"forms.labels.generic.#{area}") - status_html = '' - if @housing_data[id][:warnings].present? && @housing_data[id][:warnings][:space].present? && @housing_data[id][:warnings][:space][area].present? - @housing_data[id][:warnings][:space][area].each do |w| - status_html += content_tag(:div, _("warnings.housing.space.#{w.to_s}"), class: 'warning') + + # don't include the area if the host doesn't want anyone there + if max_space > 0 || guests.size > 0 + area_name = (_"forms.labels.generic.#{area}") + status_html = '' + if @housing_data[id][:warnings].present? && @housing_data[id][:warnings][:space].present? && @housing_data[id][:warnings][:space][area].present? + @housing_data[id][:warnings][:space][area].each do |w| + status_html += content_tag(:div, _("warnings.housing.space.#{w.to_s}"), class: 'warning') + end end + space_html = content_tag(:h5, area_name + _!(" (#{guests.size.to_s}/#{max_space.to_s})") + status_html.html_safe) + guest_items = '' + guests.each do |guest_id, guest| + guest_items += content_tag(:li, guest[:guest].user.name, id: "hosted-guest-#{guest_id}") + end + space_html += content_tag(:ul, guest_items.html_safe) + + # see if the space is overbooked + booked_state = guests.size >= max_space ? (guests.size > max_space ? :overbooked : :booked) : nil + + # let space be overbooked, even bed space can be overbooked if a couple is staying in the bed + space_html += button :place_guest, type: :button, value: "#{area}:#{id}", class: [:small, 'place-guest', 'on-top-only', booked_state, max_space > 0 ? nil : :unwanted] + + html += content_tag(:div, space_html, class: [:space, area, max_space > 0 || guests.size > 0 ? nil : 'on-top-only']) end - space_html = content_tag(:h5, area_name + _!(" (#{guests.size.to_s}/#{max_space.to_s})") + status_html.html_safe) - guest_items = '' - guests.each do |guest_id, guest| - guest_items += content_tag(:li, guest[:guest].user.name, id: "hosted-guest-#{guest_id}") - end - space_html += content_tag(:ul, guest_items.html_safe) - space_html += button :place_guest, type: :button, value: "#{area}:#{id}", class: [:small, 'place-guest', 'on-top-only', guests.size >= max_space ? (guests.size > max_space ? :overbooked : :booked) : nil, max_space > 0 ? nil : :unwanted] - html += content_tag(:div, space_html, class: [:space, area, max_space > 0 || guests.size > 0 ? nil : 'on-top-only']) end classes << 'status-warning' if @housing_data[id][:warnings].present? @@ -228,6 +249,16 @@ module WidgetsHelper end end + def link_help_dlg(topic, args = {}) + @help_dlg ||= true + args[:data] ||= {} + args[:data]['info-title'] = I18n.t("help.headings.#{topic}") + args[:data]['help-text'] = true + content_tag(:a, args) do + (I18n.t('help.link_text') + content_tag(:template, (render "/help/#{topic}"), class: 'message')).html_safe + end + end + def button_with_confirmation(button_name, confirmation_text = nil, args = {}) if confirmation_text.is_a? Hash args = confirmation_text diff --git a/app/views/application/user_settings.html.haml b/app/views/application/user_settings.html.haml index 11db2c4..e81c3e8 100644 --- a/app/views/application/user_settings.html.haml +++ b/app/views/application/user_settings.html.haml @@ -7,10 +7,10 @@ - if @conference.present? && (@conference.registration_status == :pre || @conference.registration_status == :open) %p=_'articles.user_settings.paragraphs.conference_registration', :t = link_to (_'actions.conference.edit_registration'), register_path(@conference.slug), class: :button - - if @conferences.present? + - if @my_conferences.present? %h3=_'articles.user_settings.headings.Your_Conferences' .link-dump - - @conferences.each do | conference | + - @my_conferences.each do |conference| = link_to (_!conference.title), administrate_conference_path(conference.slug), class: :button = form_tag update_settings_path do diff --git a/app/views/conference_administration/_hosts_table.html.haml b/app/views/conference_administration/_hosts_table.html.haml index 804754c..d20f1a5 100644 --- a/app/views/conference_administration/_hosts_table.html.haml +++ b/app/views/conference_administration/_hosts_table.html.haml @@ -1,11 +1,20 @@ .guests-housed %h5 Guests Housed: - .data="#{@guests_housed} / #{@guests.size}" + .data{class: @guests_housed < @guests.size ? :unhappy : :happy }="#{@guests_housed} / #{@guests.size}" +- if @guests_housed > 0 + .guests-housed + %h5 Unhappy hosts and guests: + .data{class: @unhappy_people.size > 0 ? :unhappy : :happy }="#{@unhappy_people.size}" +- first_row = true %table.hosts.admin-edit - - @hosts.each do | id, registration | + - @hosts.each do |id, registration| + - unless first_row + %tr.spacer + %td %tr.host %th .name=registration.user.name .address=registration.housing_data['address'] %td.inner-table{colspan: 2}=host_guests_table(registration) + - first_row = false diff --git a/app/views/conference_administration/_housing.html.haml b/app/views/conference_administration/_housing.html.haml index 42030e2..f5564b8 100644 --- a/app/views/conference_administration/_housing.html.haml +++ b/app/views/conference_administration/_housing.html.haml @@ -6,5 +6,5 @@ = admin_update_form class: 'guest-dlg', id: 'guest-list-table' do %h3 Select a Guest #table - .actions + .actions.center = link_to (_'links.download.Excel'), administration_step_path(@this_conference.slug, @admin_step, :format => :xlsx), class: [:button, :download] diff --git a/app/views/conference_administration/_registrations.html.haml b/app/views/conference_administration/_registrations.html.haml index 3d97266..9e00615 100644 --- a/app/views/conference_administration/_registrations.html.haml +++ b/app/views/conference_administration/_registrations.html.haml @@ -6,12 +6,12 @@ %a.button{data: { expands: 'registrations-table' }}='expand' %a.button.delete{data: { contracts: 'registrations-table' }}='close' %a.button.modify{data: { 'opens-modal': 'new-registration' }}='+' - .table-scroller - = html_table @excel_data, registrations_table_options + .table-scroller#registrations + = html_table(@excel_data, registrations_table_options) = admin_update_form id: 'new-registration', class: 'modal-edit' do .modal-edit-overlay{data: { 'closes-modal': 'new-registration' }} .modal-edit-content = html_edit_table @excel_data, registrations_edit_table_options - .actions.right - %a.button.subdued{data: { 'closes-modal': 'new-registration' }}='Cancel' + .actions.center + %a.button.subdued{data: { 'closes-modal': 'new-registration' }} Cancel = button :save, value: :save, class: :modify diff --git a/app/views/conference_administration/_select_guest_table.html.haml b/app/views/conference_administration/_select_guest_table.html.haml index b0075e4..5346117 100644 --- a/app/views/conference_administration/_select_guest_table.html.haml +++ b/app/views/conference_administration/_select_guest_table.html.haml @@ -1,50 +1,54 @@ -= hidden_field_tag :host, host.id -.host-field - %h4.inline=_'forms.labels.generic.name' - %span.plain-value= host.user.name -.host-field - %h4.inline=_'articles.conference_registration.headings.host.availability' - %span.plain-value= host.housing_data['availability'].present? && host.housing_data['availability'][1].present? ? date_span(host.housing_data['availability'][0].to_date, host.housing_data['availability'][1].to_date) : '' -- if host.housing_data['considerations'].present? - .host-field - %h4.inline=_'articles.conference_registration.headings.host.considerations' - %span.plain-value= (host.housing_data['considerations'].map { | consideration | _"articles.conference_registration.host.considerations.#{consideration}" }).join(', ') -- if sanitize(host.housing_data['notes'], tags: []).present? - .host-field - %h4=_'articles.conference_registration.headings.host.notes' - %blockquote= host.housing_data['notes'].html_safe -%table.guests.admin-edit - %tr - %th.corner - %th=_'forms.labels.generic.organization' - %th=_'forms.labels.generic.city' - %th=_'forms.labels.generic.housing' - %th=_'articles.admin.housing.headings.arrival_departure' - %th=_'articles.conference_registration.headings.companion' - %th=_'forms.labels.generic.food' - %th=_'forms.labels.generic.other' - - @guests.each do | id, registration | - %tr.selectable{class: get_housing_match(host, registration, space).to_s.gsub('_', '-'), data: {host: host.id, guest: id, space: space}} - %th=registration.user.name - %td - - if registration.user.organizations.present? - = registration.user.organizations.first.name - - else - %em None - %td=registration.city - %td=registration.housing.present? ? (_"articles.conference_registration.questions.housing.#{registration.housing}") : '' - %td=date_span(registration.arrival.to_date, registration.departure.to_date) - - companion = companion(registration) - %td=companion.present? ? (companion.is_a?(User) ? companion.named_email : (_"articles.conference_registration.terms.registration_status.#{companion}")) : '' - %td=registration.food.present? ? (_"articles.conference_registration.questions.food.#{registration.food}") : '' - %td - .p=registration.other +.host + = hidden_field_tag :host, host.id + %h4 Host + .table-scroller.no-edit + %table.guests.admin-edit + %tr + %th.corner + %th=_'forms.labels.generic.hosting_space' + %th=_'forms.labels.generic.first_day' + %th=_'forms.labels.generic.last_day' + %th=_'articles.conference_registration.headings.host.notes' + %tr + - availability = host.housing_data['availability'] + %th=host.user.name + %td=_"forms.labels.generic.#{space}" + %td=(availability || [])[0].present? ? availability[0].present? ? date(availability[0].to_date, :span_same_year_date_1) : '' : '' + %td=(availability || [])[1].present? ? date(availability[1].to_date, :span_same_year_date_1) : '' + %td=((host.housing_data || {})['notes'] || '').html_safe -.legend - %h4 Legend - %ul - %li.good-match Good Match - %li.bad-match Poor Match - %li.selected-space Also in this space - %li.other-space Also with this host - %li.other-host Already hosted +%h4 Guests +.guest-table + .table-scroller.no-edit + %table.guests.admin-edit + %tr + %th.corner + %th=_'forms.labels.generic.housing' + %th=_'forms.labels.generic.arrival' + %th=_'forms.labels.generic.departure' + %th=_'forms.labels.generic.other_notes' + %th=_'forms.labels.generic.city' + %th=_'forms.labels.generic.organization' + %th=_'articles.conference_registration.headings.companion' + %th=_'forms.labels.generic.food' + - @guests.each do |id, registration| + %tr.selectable{class: get_housing_match(host, registration, space).to_s.gsub('_', '-'), data: {host: host.id, guest: id, space: space}} + %th.break-ok=registration.user.name + %td=registration.housing.present? ? (_"articles.conference_registration.questions.housing.#{registration.housing}") : '' + %td + - if registration.arrival.present? + =date(registration.arrival.to_date, :span_same_year_date_1) + %td + - if registration.departure.present? + =date(registration.departure.to_date, :span_same_year_date_1) + %td.break-ok + .p=[registration.allergies, registration.other, (registration.housing_data || {})['other']].compact.join("\n\n") + %td=registration.city + %td.break-ok + - if registration.user.organizations.present? + = registration.user.organizations.first.name + - else + %em None + - companion = companion(registration) + %td=companion.present? ? (companion.is_a?(User) ? companion.named_email : (_"articles.conference_registration.terms.registration_status.#{companion}")) : '' + %td=registration.food.present? ? (_"articles.conference_registration.questions.food.#{registration.food}") : '' diff --git a/app/views/conference_administration/administration.html.haml b/app/views/conference_administration/administration.html.haml index 4bd44fb..7a443a9 100644 --- a/app/views/conference_administration/administration.html.haml +++ b/app/views/conference_administration/administration.html.haml @@ -1,7 +1,7 @@ - body_class 'banner-bottom' unless @this_conference.poster.present? - add_stylesheet :admin - content_for :banner do - = render :partial => 'application/header', :locals => { page_group: :administration, page_key: 'Administration', image_file: @this_conference.poster_url || 'admin.jpg'} + = render partial: 'application/header', locals: { page_group: :administration, page_key: 'Administration', image_file: @this_conference.poster_url || 'admin.jpg'} %article = row do @@ -10,7 +10,7 @@ %h2=@this_conference.title %p=_'articles.admin.paragraphs.administration', :p %ul.break - - administration_steps.each do | step, actions | + - administration_steps.each do |step, actions| %li .info %h3=_"articles.admin.#{step}.heading", :t @@ -18,7 +18,7 @@ %p=_"articles.admin.#{step}.description", :p .actions.figures - - actions.each do | action | + - actions.each do |action| - action_text = (_"articles.admin.#{step}.headings.#{action}", :t) .figure = link_to administration_step_path(@this_conference.slug, action.to_s) do diff --git a/app/views/conference_administration/administration_step.html.haml b/app/views/conference_administration/administration_step.html.haml index dda310c..7b6396a 100644 --- a/app/views/conference_administration/administration_step.html.haml +++ b/app/views/conference_administration/administration_step.html.haml @@ -1,11 +1,13 @@ - body_class 'banner-bottom' unless @this_conference.poster.present? - add_stylesheet :admin - content_for :banner do - = render :partial => 'application/header', :locals => { page_group: :administration, page_key: 'Administration', image_file: @this_conference.poster_url || 'admin.jpg'} + = render partial: 'application/header', locals: { page_group: :administration, page_key: 'Administration', image_file: @this_conference.poster_url || 'admin.jpg'} %article{id: "admin-#{@admin_step}"} = row do = columns(medium: 12) do + - if admin_help_pages[@admin_step.to_sym] + = link_help_dlg("admin_#{admin_help_pages[@admin_step.to_sym]}", class: ['button', 'help-link']) %h2.floating=(_"articles.admin.#{@admin_group}.headings.#{@admin_step}", :t) = row do = columns(medium: 12) do diff --git a/app/views/conferences/index.html.haml b/app/views/conferences/index.html.haml deleted file mode 100644 index 589f737..0000000 --- a/app/views/conferences/index.html.haml +++ /dev/null @@ -1,14 +0,0 @@ -- page_name = 'All '+(@conference_type ? @conference_type.title+' ' : '')+' Conferences' -- title page_name -- banner_image '/assets/conference.jpg' -- page_style :list -- content_for :banner do - .row - .columns - %h1=_'page.Conferences' - -%h2=page_name - -%ul.small-block-grid-1.medium-block-grid-2.large-block-grid-3.conference-list.preview-list - - @conferences.each do |conference| - %li=render 'preview', :conference => conference diff --git a/app/views/help/_admin_housing.html.haml b/app/views/help/_admin_housing.html.haml new file mode 100644 index 0000000..1839f7e --- /dev/null +++ b/app/views/help/_admin_housing.html.haml @@ -0,0 +1,31 @@ +%p On this page you will see a list of hosts (housing providers) each with a list of their open spaces, beds, floor space, and tent space. + +%h3 How to match a guest with host + +%p Find a host and select a space to place a guest in, then press the "Place a guest in..." button to add a guest to that area. Once you press that button a popup will appear showing you extended details on the host and a list of guests with details. Take a look at the notes section for both the host and the guest to determine if the two will make a good match but also pay attention to the colour of the row: + +.legend + %h4 Legend + %ul + %li.good-match Good Match + %li.bad-match Poor Match + %li.selected-space Also in this space + %li.other-space Also with this host + %li.other-host Already hosted + +%h3 Dealing with poor matches + +%p A poor match can happen for several reasons: +%ul + %li A host space may not be available for the entire time that a guest wishes to stay + %li A guest may be placed in a space (bed, floor, or tent) which is not their preferred space + %li A guest may be placed in a space without their companion + %li A host may have more guests than they indicated as their maximum + +%p When a match is likely good, you will see a 😁 beside their placement, when a match is poor you will see a 😡. If you see the unhappy face, you can hover over the face to show more details on why the guest or host is unhappy. + +%p You may wish to remove the guest and fin a better placement but keep in mind that this will not always be possible. For example, often guests will ask to be placed with many other friends but arranging this can be difficult. Speak with guests and hosts if possible to arrange a compromise, it is generally encouraged that guests stay with new people as long as they feel safe. + +%h3 Notifying hosts and guests of their placements + +%p At the moment there is no automated tool to do this for you. You will need to download the excel speadsheet at the bottom of the page to get all placements and their contact info. diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index f9f7556..8ac3abf 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -61,6 +61,13 @@ .dlg-inner %p.message='' %button.close=_'modals.done_button' + - if @help_dlg.present? + .dlg#help-dlg{data: { nofocus: 1 }} + .dlg-content + %h2.title=_'modals.help' + .dlg-inner + .message='' + %button.close=_'modals.done_button' - if @login_dlg.present? .dlg#login-dlg .dlg-content diff --git a/config/assets_cdn.yml b/config/assets_cdn.yml deleted file mode 100644 index ef092c9..0000000 --- a/config/assets_cdn.yml +++ /dev/null @@ -1,15 +0,0 @@ -development: - enabled: false - host: bikebike.org - -preview: - enabled: false - host: preview-cdn.bikebike.org - protocol: https - fallback_protocol: http - -production: - enabled: true - host: cdn.bikebike.org - protocol: https - fallback_protocol: http diff --git a/config/environments/development.rb b/config/environments/development.rb index 92c9c17..a6d1348 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -44,7 +44,7 @@ BikeBike::Application.configure do # enable_starttls_auto: true, # openssl_verify_mode: 'none', # user_name: 'info@bikebike.org', - # password: config.app_config['email_password'] + # password: 'Toronto@)!)' # } config.action_mailer.raise_delivery_errors = true config.action_mailer.perform_deliveries = true diff --git a/config/locales/en.yml b/config/locales/en.yml index b7a4ac3..900bfcc 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1331,6 +1331,10 @@ en: conference: actions: Register: Register + help: + link_text: Help + headings: + admin_housing: Help for Housing modals: confirm: Please Confirm yes_button: 'Yes' @@ -1461,10 +1465,9 @@ en: instead of a guest form. If you want to consider surrounding cities as well, enter the distance from %{city} that you wish to be included as providers. Cities are measured from center to center. - housing: Pair each housing provider with a list of guests. Try to match - guests with hosts and other guests who are good matches and respect their - requests, but also keep in mind that we cannot always respect all requests - and part of the Bike!Bike! experience is getting to know new people. + housing: Match guests with hosts, and fulfill their housing + requests to the best of your ability. Use guest notes and the face icons to determine if a guest is a good match for the host. + Hover over unhappy faces for details on how to find the guest a better match. Keep in mind that you may not be able to accommodate every request. locations: heading: Locations description: Locations are used to schedule workshops, events, and meals. @@ -2079,6 +2082,7 @@ en: quiet: Quiet household pets: House has dogs or cats can_provide_housing: I can provide housing. + housing_provider: Host? not_attending: I will not be attending the conference questions: bike: @@ -2096,6 +2100,10 @@ en: house: Indoor Location none: I'll take care of it tent: Tent Space + housing_short: + house: House + none: None + tent: Tent bikes: medium: Medium none: "(none)" @@ -2116,9 +2124,10 @@ en: unregistered: Unregistered preregistered: Preregistered registered: Registered + cancelled: Cancelled companion: Companion companion_email: Companion Email - Preferred_Languages: Preferred Language + Preferred_Languages: Language is_attending: Attending? about_bikebike: paragraphs: @@ -2282,6 +2291,8 @@ en: paypal: Online on_arrival: In person none: None + payment_method: Payment Method + payment_currency: Payment Currency space: Available Space hosting_dates: When will you be in town? type: Type @@ -2295,8 +2306,8 @@ en: other: Disabilities, housing preferences, etc. email: Email address allergies: Allergies - arrival: Arrival date - departure: Departure date + arrival: Arrival + departure: Departure location: City, State/Province, Country name: Name subject: Subject @@ -2306,10 +2317,13 @@ en: notes: Notes message: 'Your Message:' address: Street Address + address_short: Address phone: Phone number + phone_short: Phone bed_space: Bed/Couch Space floor_space: Floor Space tent_space: Tent Space + hosting_space: Space first_day: From last_day: To body: Body @@ -2329,7 +2343,7 @@ en: closed: Closed open: Open pre: Pre-Registration - registration_status: Registration Status + registration_status: Status companion: Email address block_number: Block days: Days @@ -2395,6 +2409,10 @@ en: create: Create delete: Delete place_guest: Place Guest + place_guest_in: + bed_space: Place a guest in a bed or couch + floor_space: Place a guest on the floor + tent_space: Place a guest in a tent space set_host: Set Host add_comment: Add Comment reply: Reply