Browse Source

Admin improvements

development
Godwin 8 years ago
parent
commit
9e55d27d33
  1. 12
      app/assets/javascripts/main.js
  2. 348
      app/assets/javascripts/registrations.js
  3. 287
      app/assets/stylesheets/_admin.scss
  4. 115
      app/assets/stylesheets/_application.scss
  5. 2
      app/assets/stylesheets/bumbleberry-settings.json
  6. 158
      app/controllers/conference_administration_controller.rb
  7. 22
      app/helpers/admin_helper.rb
  8. 2
      app/helpers/geocoder_helper.rb
  9. 3
      app/helpers/registration_helper.rb
  10. 67
      app/helpers/table_helper.rb
  11. 155
      app/helpers/widgets_helper.rb
  12. 4
      app/views/application/user_settings.html.haml
  13. 13
      app/views/conference_administration/_hosts_table.html.haml
  14. 2
      app/views/conference_administration/_housing.html.haml
  15. 8
      app/views/conference_administration/_registrations.html.haml
  16. 102
      app/views/conference_administration/_select_guest_table.html.haml
  17. 6
      app/views/conference_administration/administration.html.haml
  18. 4
      app/views/conference_administration/administration_step.html.haml
  19. 14
      app/views/conferences/index.html.haml
  20. 31
      app/views/help/_admin_housing.html.haml
  21. 7
      app/views/layouts/application.html.haml
  22. 15
      config/assets_cdn.yml
  23. 2
      config/environments/development.rb
  24. 34
      config/locales/en.yml

12
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) {

348
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');
});
});
})();

287
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,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 87.8 73.2'><polygon fill='#{$colour-5}' points='34.3 73.2 0 32.6 18.7 16.8 35.4 36.5 70.1 0 87.8 16.8 '/></svg>");
@include after {
content: '\1F601';
opacity: 0.5;
}
}
&.unhappy {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path xmlns="http://www.w3.org/2000/svg" d="M50 5C25.1 5 5 25.1 5 50c0 24.9 20.1 45 45 45s45-20.1 45-45C95 25.1 74.9 5 50 5zM72.5 64.3l-8.2 8.2L50 58.2 35.7 72.5l-8.2-8.2L41.8 50 27.5 35.7l8.2-8.2L50 41.8l14.3-14.3 8.2 8.2L58.2 50 72.5 64.3z" fill="#{$colour-4}"/></svg>');
@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;

115
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,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 87.8 73.2'><polygon fill='#{$colour-5}' points='34.3 73.2 0 32.6 18.7 16.8 35.4 36.5 70.1 0 87.8 16.8 '/></svg>");
}
&.unhappy {
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path xmlns="http://www.w3.org/2000/svg" d="M50 5C25.1 5 5 25.1 5 50c0 24.9 20.1 45 45 45s45-20.1 45-45C95 25.1 74.9 5 50 5zM72.5 64.3l-8.2 8.2L50 58.2 35.7 72.5l-8.2-8.2L41.8 50 27.5 35.7l8.2-8.2L50 41.8l14.3-14.3 8.2 8.2L58.2 50 72.5 64.3z" fill="#{$colour-4}"/></svg>');
}
}
}
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;
}

2
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"]
}

158
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

22
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

2
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?

3
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]

67
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

155
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, '&nbsp'.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, '&nbsp'.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

4
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

13
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

2
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]

8
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

102
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}") : ''

6
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

4
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

14
app/views/conferences/index.html.haml

@ -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

31
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 &#x1F601; beside their placement, when a match is poor you will see a &#x1F621;. 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.

7
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

15
config/assets_cdn.yml

@ -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

2
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

34
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

Loading…
Cancel
Save