Browse Source

Pre-Registration

development
Godwin 9 years ago
parent
commit
9ae5da70fb
  1. 28
      Capfile
  2. 14
      Gemfile
  3. 1
      app/assets/images/edit.svg
  4. 1
      app/assets/images/img.svg
  5. 1
      app/assets/images/link.svg
  6. 1
      app/assets/images/ol.svg
  7. 1
      app/assets/images/quote.svg
  8. 1
      app/assets/images/ul.svg
  9. 79
      app/assets/javascripts/editor.js
  10. 37
      app/assets/javascripts/editor.js.coffee
  11. 16
      app/assets/javascripts/main.js
  12. 813
      app/assets/stylesheets/_application.scss
  13. 212
      app/assets/stylesheets/_editor.scss
  14. 3
      app/assets/stylesheets/_settings.scss
  15. 6
      app/assets/stylesheets/bumbleberry-settings.json
  16. 183
      app/assets/stylesheets/user-mailer.scss
  17. 32
      app/controllers/application_controller.rb
  18. 574
      app/controllers/conferences_controller.rb
  19. 39
      app/controllers/oauths_controller.rb
  20. 71
      app/helpers/application_helper.rb
  21. 97
      app/mailers/user_mailer.rb
  22. 19
      app/models/conference.rb
  23. 16
      app/models/conference_registration.rb
  24. 14
      app/models/user.rb
  25. 32
      app/models/workshop.rb
  26. 3
      app/views/application/404.html.haml
  27. 13
      app/views/application/_header.html.haml
  28. 2
      app/views/application/_login_confirmation_sent.html.haml
  29. 26
      app/views/application/home.html.haml
  30. 8
      app/views/application/permission_denied.html.haml
  31. 15
      app/views/conferences/_confirm_email.html.haml
  32. 26
      app/views/conferences/_contact_info.html.haml
  33. 2
      app/views/conferences/_email_confirm.html.haml
  34. 4
      app/views/conferences/_header.html.haml
  35. 19
      app/views/conferences/_policy.html.haml
  36. 58
      app/views/conferences/_workshops.html.haml
  37. 30
      app/views/conferences/edit.html.haml
  38. 18
      app/views/conferences/register.html.haml
  39. 26
      app/views/layouts/application.html.haml
  40. 70
      app/views/layouts/user_mailer.html.haml
  41. 3
      app/views/shared/_donate_button.html.haml
  42. 21
      app/views/shared/_navbar.html.haml
  43. 8
      app/views/user_mailer/registration_confirmation.html.haml
  44. 7
      app/views/user_mailer/registration_confirmation.text.haml
  45. 1
      app/views/user_mailer/test_email.text.haml
  46. 15
      app/views/user_mailer/workshop_original_content_changed.html.haml
  47. 25
      app/views/user_mailer/workshop_original_content_changed.text.haml
  48. 15
      app/views/user_mailer/workshop_translated.html.haml
  49. 25
      app/views/user_mailer/workshop_translated.text.haml
  50. 50
      app/views/workshops/_show.html.haml
  51. 18
      app/views/workshops/_workshop_previews.html.haml
  52. 6
      app/views/workshops/facilitate_request_sent.html.haml
  53. 11
      app/views/workshops/index.html.haml
  54. 94
      app/views/workshops/new.html.haml
  55. 18
      app/views/workshops/show.html.haml
  56. 49
      config/deploy.rb
  57. 61
      config/deploy/production.rb
  58. 61
      config/deploy/staging.rb
  59. 12
      config/environments/development.rb
  60. 1
      config/environments/preview.rb
  61. 1
      config/environments/production.rb
  62. 11
      config/initializers/assets.rb
  63. 26
      config/initializers/sorcery.rb
  64. 45
      config/locales/en.yml
  65. 7
      config/locales/es.yml
  66. 8
      config/routes.rb
  67. 9
      db/migrate/20160515210708_add_registration_status_to_conferences.rb
  68. 5
      db/migrate/20160521230653_add_highest_step_to_conference_registrations.rb
  69. 5
      db/migrate/20160522022034_add_steps_completed_to_conference_registrations.rb
  70. 5
      db/migrate/20160526044849_add_needs_facilitators_to_workshops.rb
  71. 5
      db/migrate/20160529225253_add_languages_to_users.rb
  72. 5
      db/migrate/20160530175805_change_date_format_in_dynamic_translation_records.rb
  73. 23
      db/schema.rb
  74. 78
      vendor/assets/javascripts/markdown.js
  75. 835
      vendor/assets/javascripts/pen.js
  76. 1675
      vendor/assets/javascripts/world-110m.json
  77. 1675
      vendor/assets/javascripts/world-50m.json
  78. 159
      vendor/assets/stylesheets/pen.css

28
Capfile

@ -0,0 +1,28 @@
# Load DSL and set up stages
require "capistrano/setup"
# Include default deployment tasks
require "capistrano/deploy"
# Include tasks from other gems included in your Gemfile
#
# For documentation on these, see for example:
#
# https://github.com/capistrano/rvm
# https://github.com/capistrano/rbenv
# https://github.com/capistrano/chruby
# https://github.com/capistrano/bundler
# https://github.com/capistrano/rails
# https://github.com/capistrano/passenger
#
# require 'capistrano/rvm'
# require 'capistrano/rbenv'
# require 'capistrano/chruby'
# require 'capistrano/bundler'
# require 'capistrano/rails/assets'
# require 'capistrano/rails/migrations'
# require 'capistrano/passenger'
require 'capistrano/faster_assets'
# Load custom tasks from `lib/capistrano/tasks` if you have any defined
Dir.glob("lib/capistrano/tasks/*.rake").each { |r| import r }

14
Gemfile

@ -6,9 +6,6 @@ gem 'pg'
gem 'rack-mini-profiler'
gem 'haml'
# gem 'jquery-rails'
# gem 'jquery-ui-rails'
# gem 'coffee-rails', '~> 4.0.0'
gem 'nokogiri', '~> 1.6.8.rc2'
if Dir.exists?('../lingua_franca')
@ -33,8 +30,6 @@ gem 'oauth2', '~> 0.8.0'
gem 'carrierwave'
gem 'carrierwave-imageoptimizer'
gem 'mini_magick'
# gem 'nested_form'
# gem 'acts_as_list'
gem 'geocoder'
gem 'paper_trail', '~> 3.0.5'
gem 'sitemap_generator'
@ -55,8 +50,13 @@ group :development do
gem 'better_errors'
gem 'binding_of_caller'
gem 'meta_request'
# gem 'haml-rails'
# gem 'awesome_print'
gem 'capistrano', '~> 3.1'
gem 'capistrano-rails', '~> 1.1'
gem 'capistrano-faster-assets', '~> 1.0'
gem 'eventmachine', :github => 'krzcho/eventmachine', :branch => 'master'
gem 'thin', :github => 'krzcho/thin', :branch => 'master'
end
group :test do

1
app/assets/images/edit.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path fill="#333" d="M78.995,0L13.204,64.781c8.2,5.467,16.085,13.267,21.552,21.468l65.706-64.802C97.014,11.333,89.235,3.427,78.995,0z M7.338,78.343L-0.463,100l21.552-7.885C17.557,87.763,11.69,81.897,7.338,78.343z"></path></svg>

After

Width:  |  Height:  |  Size: 421 B

1
app/assets/images/img.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path fill="#FAF5F0" d="M35.417,33.351l18.75,27.083L68.75,45.851l18.75,25h-75L35.417,33.351z M4.166,8.315C0,8.314,0,8.351,0,12.518v75 c0,4.167,0,4.167,4.167,4.167h91.667c4.167,0,4.167,0,4.167-4.167v-75c0-4.167,0-4.167-4.167-4.167L4.166,8.315z"></path></svg>

After

Width:  |  Height:  |  Size: 450 B

1
app/assets/images/link.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path fill="#FAF5F0" d="M92.071,7.974C81.305-2.793,64.024-2.431,53.529,8.065L41.043,20.55c7.329-2.352,13.028-0.724,18.005,1.357l3.619-3.619 c5.157-5.157,13.752-4.886,18.819,0.181c5.157,5.157,5.519,13.843,0.362,19L65.019,54.298c-1.9,1.9-4.071,2.986-6.514,3.619 c-6.514,0.905-12.757-2.352-15.109-8.052L32.357,60.54c7.6,11.49,23.343,15.381,35.828,8.324c2.081-1.086,4.071-2.714,5.881-4.524 l17.914-17.914C102.476,35.931,102.838,18.741,92.071,7.974z M32.538,30.503c-2.171,1.086-4.162,2.714-5.881,4.524L8.019,53.574 C-2.476,64.069-2.838,81.259,7.929,92.026s28.047,10.405,38.543-0.09L58.957,79.45c-7.329,2.352-13.028,0.724-18.005-1.357 l-3.619,3.619c-5.157,5.157-13.752,4.886-18.819-0.181c-5.157-5.157-5.519-13.843-0.362-19l17.462-17.462 c1.9-1.9,4.071-2.986,6.514-3.619c6.514-0.905,12.757,2.352,15.109,8.052l11.129-10.676C60.676,27.336,44.933,23.445,32.538,30.503z"></path></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

1
app/assets/images/ol.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path fill="#FAF5F0" d="M95.833,25h-50c-4.167,0-4.167,0-4.167,4.167v4.167c0,4.167,0,4.167,4.167,4.167h50c4.167,0,4.167,0,4.167-4.167v-4.167 C100,25,100,25,95.833,25z M95.833,62.5h-50c-4.167,0-4.167,0-4.167,4.167v4.167c0,4.167,0,4.167,4.167,4.167h50 C100,75,100,75,100,70.833v-4.167C100,62.5,100,62.5,95.833,62.5z M95.833,87.5h-50c-4.167,0-4.167,0-4.167,4.167v4.167 c0,4.167,0,4.167,4.167,4.167h50C100,100,100,100,100,95.833v-4.167C100,87.5,100,87.5,95.833,87.5z M95.833,0h-50 c-4.167,0-4.167,0-4.167,4.167v4.167c0,4.167,0,4.167,4.167,4.167h50c4.167,0,4.167,0,4.167-4.167V4.167C100,0,100,0,95.833,0z M22.917,0H6.25L4.167,10.417H12.5V37.5h10.417V0z M14.583,61.75C6.25,61.75,0,67.333,0,75.667h9.167 c0-2.917,1.667-5.583,5.417-5.583c3.375,0,5.167,2.146,5.125,4.771c-0.021,1.708-1.188,3.292-6,7.333L0,93.75V100h29.167v-9.5H18 l5.104-5.125c4.688-4.688,6.062-7.396,6.062-11.375C29.167,67.667,22.917,61.75,14.583,61.75z"></path></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

1
app/assets/images/quote.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="17 -18 100 100" enable-background="new 17 -18 100 100" xml:space="preserve"><path fill="#FAF5F0" d="M93.2-3.9C106-3.9,117,4.9,117,23.9c0,26.7-21.8,42.4-34.2,44l-2.6-7.4c7.2-4.7,13.1-12.8,12.5-17.5 c-8.1-0.6-21.2-4.9-21.2-22.5C71.4,7.2,78.9-3.9,93.2-3.9z M38.9-3.9c12.8,0,23.8,8.8,23.8,27.8c0,26.7-21.8,42.4-34.2,44l-2.6-7.4 C33,55.8,38.9,47.8,38.2,43C30.2,42.4,17,38.1,17,20.5C17,7.2,24.6-3.9,38.9-3.9z"></path></svg>

After

Width:  |  Height:  |  Size: 541 B

1
app/assets/images/ul.svg

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 100" enable-background="new 0 0 100 100" xml:space="preserve"><path fill="#FAF5F0" d="M12.5,87.5C19.404,87.5,25,81.904,25,75s-5.596-12.5-12.5-12.5C5.596,62.5,0,68.096,0,75S5.596,87.5,12.5,87.5z M12.5,25 C19.396,25,25,19.396,25,12.5S19.396,0,12.5,0S0,5.604,0,12.5S5.604,25,12.5,25z M95.833,25h-50c-4.167,0-4.167,0-4.167,4.167v4.167 c0,4.167,0,4.167,4.167,4.167h50c4.167,0,4.167,0,4.167-4.167v-4.167C100,25,100,25,95.833,25z M95.833,62.5h-50 c-4.167,0-4.167,0-4.167,4.167v4.167c0,4.167,0,4.167,4.167,4.167h50C100,75,100,75,100,70.833v-4.167 C100,62.5,100,62.5,95.833,62.5z M95.833,87.5h-50c-4.167,0-4.167,0-4.167,4.167v4.167c0,4.167,0,4.167,4.167,4.167h50 C100,100,100,100,100,95.833v-4.167C100,87.5,100,87.5,95.833,87.5z M95.833,0h-50c-4.167,0-4.167,0-4.167,4.167v4.167 c0,4.167,0,4.167,4.167,4.167h50c4.167,0,4.167,0,4.167-4.167V4.167C100,0,100,0,95.833,0z"></path></svg>

After

Width:  |  Height:  |  Size: 1007 B

79
app/assets/javascripts/editor.js

@ -0,0 +1,79 @@
(function() {
var pens = {};
Array.prototype.forEach.call(document.querySelectorAll('.textarea'), function(editor) {
startEditing(editor);
});
function startEditing(editor) {
var name = editor.dataset.name;
pens[name] = new Pen({
editor: editor,
class: 'pen',
textarea: '<textarea name="' + name + '"></textarea>',
list: ['p', 'h3', 'h4', 'blockquote', 'insertorderedlist', 'insertunorderedlist', 'bold', 'italic', 'underline', 'strikethrough', 'createlink', 'insertimage'],
title: {
'p': 'Paragraph',
'h3': 'Major Heading',
'h4': 'Minor Heading',
'blockquote': 'Quotation',
'insertorderedlist': 'Ordered List',
'insertunorderedlist': 'Unordered List',
'bold': 'Bold',
'italic': 'Italic',
'underline': 'Underline',
'strikethrough': 'Strikethrough',
'createlink': 'Link',
'insertimage': 'Image'
}
});
}
Array.prototype.forEach.call(document.querySelectorAll('form'), function(form) {
var shouldAllowAlert = false;
form.addEventListener('submit', function() {
if (shouldAllowAlert) {
return;
}
Array.prototype.forEach.call(document.querySelectorAll('.textarea'), function(editor) {
var name = editor.dataset.name;
var textarea = document.querySelector('textarea[name="' + name + '"]');
if (!textarea) {
textarea = document.createElement('textarea');
textarea.name = name;
textarea.style.display = 'none';
form.appendChild(textarea);
}
textarea.value = editor.innerHTML;
pens[name].destroy();
});
}, false);
Array.prototype.forEach.call(form.querySelectorAll('button'), function(button) {
form.addEventListener('click', function(event) {
shouldAllowAlert = (event.target.value === 'cancel');
});
});
});
Array.prototype.forEach.call(document.querySelectorAll('.check-box-field .other input'), function(input) {
var checkbox = document.getElementById(input.parentElement.parentElement.attributes.for.value);
input.addEventListener('keyup', function(event) {
if (event.target.value) {
checkbox.checked = true;
}
});
input.addEventListener('click', function(event) {
checkbox.checked = true;
});
var setRequired = function() {
if (checkbox.checked) {
input.setAttribute('required', 'required');
} else {
input.removeAttribute('required');
}
};
Array.prototype.forEach.call(document.querySelectorAll('.check-box-field input'), function(_input) {
_input.addEventListener('change', function(event) { setRequired(); });
});
});
})();

37
app/assets/javascripts/editor.js.coffee

@ -1,37 +0,0 @@
#= require froala_editor.min.js
$ ->
$('[data-editable]').editable({inlineMode: true, blockTags: ["n", "p", "h2", "blockquote", "pre"], buttons: ["formatBlock", "bold", "italic", "underline", "insertOrderedList", "insertUnorderedList", "sep", "createLink", "insertImage", "insertVideo", "html", "undo", "redo"]})
$('[data-editor]').editable({inlineMode: false, blockTags: ["n", "p", "h2", "blockquote", "pre"], buttons: ["formatBlock", "bold", "italic", "underline", "insertOrderedList", "insertUnorderedList", "sep", "createLink", "html", "undo", "redo"]})
$('.field.country-select-field select').change () ->
$country = $(this)
country = $country.val()
$territory = $('.field.subregion-select-field select')
if $territory.data().country == country
$territory.removeClass('can cant').addClass('can')
return
$.post '/location/territories', {country: country},
(json) ->
$territory.html('')
if json && Object.keys(json).length
$.each json, (code, name) ->
$territory.append($('<option>').text(name).attr('value', code))
return
$territory.removeClass('can cant').addClass('can')
$territory.data().country = country
else
$territory.removeClass('can cant').addClass('cant')
return
, 'json'
$('img + input[type="file"]').change () ->
readURL(this);
return
readURL = (input) ->
reader = null
if input.files && input.files[0]
reader = new FileReader()
reader.readAsDataURL input.files[0]
reader.onload = (e) ->
$(input).prev().attr('src', e.target.result)
return

16
app/assets/javascripts/main.js

@ -0,0 +1,16 @@
(function() {
Array.prototype.forEach.call(document.querySelectorAll('.number-field,.email-field,.text-field'), function(field) {
var input = field.querySelector('input');
var positionLabel = function(input) {
field.classList[input.value ? 'remove' : 'add']('empty');
}
positionLabel(input);
input.addEventListener('keyup', function(event) { positionLabel(event.target); });
input.addEventListener('blur', function(event) { field.classList.remove('focused'); });
input.addEventListener('focus', function(event) { field.classList.add('focused'); });
});
var errorField = document.querySelector('.input-field.has-error input, .input-field.has-error textarea');
if (errorField) {
errorField.focus();
}
})();

813
app/assets/stylesheets/_application.scss

File diff suppressed because it is too large

212
app/assets/stylesheets/_editor.scss

@ -0,0 +1,212 @@
$bumbleberry-no-markup: true;
@import "bumbleberry";
@import "settings";
$white: #FFF !default;
$black: #333 !default;
.pen-icon {
font-size: 16px;
line-height: 40px;
min-width: 20px;
font-style: normal;
display: block;
float: left;
padding: 0 10px;
height: 2.2em;
overflow: hidden;
color: $white;
text-align: center;
cursor: pointer;
@include _(user-select, none);
}
.pen {
position: relative;
&:focus {
outline: none;
}
}
.pen-menu, .pen-input {
font-size: 14px;
line-height:1;
}
.pen-input {
display :none;
position: absolute;
width: 100%;
left: 0;
top: 0;
height: 2.6em;
line-height: 20px;
background-color: $black;
color: $white;
border: none;
text-align: center;
&:focus {
outline: none;
}
}
.pen-textarea {
display: block;
background: $white;
padding: 20px;
}
.pen textarea {
font-size: 14px;
border: none;
background: none;
width: 100%;
_height: 200px;
min-height: 200px;
resize: none;
}
.pen-menu-below {
@include after {
top: -11px;
display: block;
@include _(transform, rotate(180deg));
}
}
.pen-menu {
white-space: nowrap;
box-shadow:1px 2px 3px -2px #222;
background: #333;
background-image: linear-gradient(to bottom, #222, #333);
opacity: 0.9;
position: fixed;
height: 5.2em;
border: 1px solid #333;
border-radius: 0.5em;
display:none;
z-index: 1000;
@include after {
content: " ";
top: 100%;
border: solid transparent;
height: 0;
width: 0;
position: absolute;
pointer-events: none;
border-color: rgba(51, 51, 51, 0);
border-top-color: #333;
border-width: 6px;
left: 50%;
margin-left: -6px;
}
[class^="icon-"], [class*=" icon-"] {
@include before {
speak: none;
display: inline-block;
width: 1em;
margin-right: .2em;
text-align: center;
font-variant: normal;
text-transform: none;
line-height: 1em;
margin-left: .2em;
vertical-align: text-top;
}
}
.pen-icon {
&:hover {
background:#000;
}
&.active {
background: #000;
@include _(box-shadow, #{inset 2px 2px 4px #000, 0 0 0.25em #000});
}
}
.icon-h1,
.icon-h2,
.icon-h3,
.icon-h4,
.icon-h5,
.icon-h6 {
@include before { @include font-family(secondary); }
}
.icon-h1 {
@include before { content: 'H1'; }
}
.icon-h2 {
@include before { content: 'H2'; }
}
.icon-h3 {
@include before { content: 'H1'; }
}
.icon-h4 {
@include before { content: 'H2'; }
}
.icon-h5 {
@include before { content: 'H5'; }
}
.icon-h6 {
@include before { content: 'H6'; }
}
.icon-bold {
border-bottom-left-radius: 0.5em;
clear: left;
@include before { content: 'B'; font-weight: bold; }
}
.icon-italic {
@include before { content: 'I'; font-style: italic;; }
}
.icon-underline {
@include before { content: 'U'; text-decoration: underline; }
}
.icon-p {
@include font-family(secondary);
@include before { content: ''; }
}
.icon-blockquote {
@include before {
background-image: inline-svg-image('quote.svg');
}
}
.icon-insertorderedlist {
border-top-right-radius: 0.5em;
@include before {
background-image: inline-svg-image('ol.svg');
}
}
.icon-insertunorderedlist { @include before {
background-image: inline-svg-image('ul.svg');
} }
.icon-strikethrough { @include before { content: 'S'; text-decoration: line-through; } }
.icon-insertunorderedlist,
.icon-insertorderedlist,
.icon-blockquote,
.icon-insertimage,
.icon-createlink {
@include before {
content: '';
background-size: contain;
height: 1em;
}
}
.icon-createlink {
@include before {
background-image: inline-svg-image('link.svg');
}
}
.icon-insertimage {
@include before {
background-image: inline-svg-image('img.svg');
}
}
}

3
app/assets/stylesheets/_settings.scss

@ -13,6 +13,9 @@ $colour-4: #D89E59; // orange
$colour-5: #02CA9E; // green
$white: #FFFEFE;
$black: #333;
$link-colour: darken($colour-1, 13%);
@mixin default-box-shadow($direction: top, $distance: 1, $inset: false, $additional-shadow: false) {
@if capable_of(box-shadow) {

6
app/assets/stylesheets/bumbleberry-settings.json

@ -1,11 +1,15 @@
{
"stylesheets": ["application", "translations", "email-example"],
"stylesheets": ["application", "editor"],
"precompile": {
"test": {
"chrome": ["50"]
},
"development": {
"and_chr": ["50"],
"chrome": ["50"],
"edge": ["13"],
"firefox": ["44"],
"ie": ["11"],
"ios_saf": ["8", "9"]
}
},

183
app/assets/stylesheets/user-mailer.scss

@ -0,0 +1,183 @@
@import "settings";
body {
width: 100% !important;
-webkit-text-size-adjust: 100%;
-ms-text-size-adjust: 100%;
margin: 0;
padding: 0;
}
.ExternalClass {
width: 100%;
line-height: 100%;
p, span, font, td, div {
line-height: 100%;
}
}
#backgroundTable {
margin: 0;
padding: 0;
width: 100% !important;
line-height: 100% !important;
}
img {
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
a & {
border: none;
}
}
.image_fix {
display: block;
max-width: 100%;
}
a {
color: $colour-1;
#outlook & {
padding: 0;
}
&:link { color: $colour-1; }
&:visited { color: $colour-1; }
&:hover { color: $colour-5; }
@media only screen and (max-device-width: 480px) {
&[href^="tel"], &[href^="sms"] {
text-decoration: none;
color: $black;
pointer-events: none;
cursor: default;
}
.mobile_link &[href^="tel"], .mobile_link &[href^="sms"] {
text-decoration: default;
color: $colour-1 !important;
pointer-events: auto;
cursor: default;
}
}
}
p {
margin: 1em;
color: $black !important;
}
h1, h2, h3, h4, h5, h6 {
color: $black !important;
a {
color: $colour-1 !important;
&:active {
color: $colour-4 !important;
}
&:visited {
color: $colour-2 !important;
}
}
}
table {
border-collapse: collapse;
max-width: 512px;
min-width: 280px;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
td {
border-collapse: collapse;
}
}
table#bb_full_width,
table#ecxbb_full_width {
&, table {
max-width: none;
width: 100%;
}
}
.diff, .ecxdiff {
margin: 1em 0 5em 1em;
overflow: auto;
ul {
list-style: none;
padding: 0;
margin: 0;
position: relative;
}
li {
width: 40%;
display: inline-block;
padding: 0.5em;
margin: 0;
background-color: lighten($colour-3, 33%);
border: 2px solid #DDD;
white-space: nowrap;
&:nth-child(odd) {
margin-right: -3px;
float: left;
}
&:nth-child(even) {
background-color: lighten($colour-5 , 33%);
}
}
}
// Buttons
h3 {
b {
padding: 10px 20px;
line-height: 50px;
a, a:visited {
color: #FFF !important;
background-color: $colour-5;
text-decoration: none !important;
border-radius: 4px;
padding: 10px 15px;
margin-left: 20px;
border-bottom: 3px solid darken($colour-5, 10%);
-webkit-box-shadow: 0 0.5em 1.5em -0.75em #000;
box-shadow: 0 0.5em 1.5em -0.75em #000;
cursor: pointer !important;
}
a:hover {
background-color: darken($colour-5, 10%);
}
}
}
@media only screen and (min-device-width: 768px) and (max-device-width: 1024px) {
a[href^="tel"], a[href^="sms"] {
text-decoration: none;
color: $colour-1;
pointer-events: none;
cursor: default;
.mobile_link & {
text-decoration: default;
color: $colour-1 !important;
pointer-events: auto;
cursor: default;
}
}
}

32
app/controllers/application_controller.rb

@ -18,6 +18,7 @@ class ApplicationController < LinguaFrancaApplicationController
def capture_page_info
# set the translator to the current user if we're logged in
I18n.config.translator = current_user
I18n.config.callback = self
# get the current confernece and set it globally
@conference = Conference.order("start_date DESC").first
@ -78,7 +79,8 @@ class ApplicationController < LinguaFrancaApplicationController
end
def do_404
params[:action] = 'error-403'
params[:_original_action] = params[:action]
params[:action] = 'error-404'
render 'application/404', status: 404
end
@ -88,6 +90,7 @@ class ApplicationController < LinguaFrancaApplicationController
def do_403(template = nil)
@template = template
params[:_original_action] = params[:action]
params[:action] = 'error-403'
render 'application/permission_denied', status: 403
end
@ -143,13 +146,12 @@ class ApplicationController < LinguaFrancaApplicationController
end
user = User.find_by_email(params[:email])
if !user
unless user
# not really a good UX so we should fix this later
#do_404
#return
user = User.new(:email => params[:email])
user.save!
user = User.find_by_email(params[:email])
end
# genereate the confirmation, send the email and show the 403
@ -218,4 +220,28 @@ class ApplicationController < LinguaFrancaApplicationController
auto_login(u)
end
def on_translation_change(object, data, locale, translator_id)
translator = User.find(translator_id)
mailer = "#{object.class.table_name.singularize}_translated"
object.get_translators(data, locale).each do |id, user|
if user.id != current_user.id && user.id != translator_id
UserMailer.send_mail mailer do
{ :args => [object, data, locale, user, translator] }
end
end
end
end
def on_translatable_content_change(object, data)
mailer = "#{object.class.table_name.singularize}_original_content_changed"
object.get_translators(data).each do |id, user|
if user.id != current_user.id
UserMailer.send_mail mailer do
{ :args => [object, data, user, current_user] }
end
end
end
end
end

574
app/controllers/conferences_controller.rb

@ -514,150 +514,219 @@ class ConferencesController < ApplicationController
end
def register
is_post = request.post? || session[:registration_step]
# is_post = request.post? || session[:registration_step]
set_conference
#if !@this_conference.registration_open
# do_404
# return
#end
set_conference_registration
@register_template = nil
if logged_in?
unless @this_conference.registration_open || @registration
do_404
return
end
# if the user is logged in start them off on the policy
# page, unless they have already begun registration then
# start them off with questions
@register_template = @registration ? (@registration.registration_fees_paid ? :done : :payment) : :policy
set_or_create_conference_registration
@name = current_user.firstname
# we should phase out last names
@name += " #{current_user.lastname}" if current_user.lastname
@name ||= current_user.username
@is_host = @this_conference.host? current_user
steps = registration_steps
return do_404 unless steps.present?
else
@register_template = :confirm_email
end
# process data from the last view
case (params[:button] || '').to_sym
when :confirm_email
@register_template = :email_sent if is_post
when :policy
@register_template = :questions if is_post
when :save
if is_post
if (new_registration = (!@registration))
@registration = ConferenceRegistration.new
end
@errors = {}
@warnings = []
form_step = params[:button] ? params[:button].to_sym : nil
@registration.conference_id = @this_conference.id
@registration.user_id = current_user.id
@registration.is_attending = 'yes'
@registration.is_confirmed = true
@registration.city = params[:location]
@registration.arrival = params[:arrival]
@registration.languages = params[:languages].keys.to_json
@registration.departure = params[:departure]
@registration.housing = params[:housing]
@registration.bike = params[:bike]
@registration.food = params[:food]
@registration.allergies = params[:allergies]
@registration.other = params[:other]
@registration.save
current_user.firstname = params[:name].squish
current_user.lastname = nil
current_user.save
if new_registration
UserMailer.send_mail :registration_confirmation do
{
:args => @registration
}
if form_step
if form_step.to_s =~ /^prev_(.+)$/
@register_template = steps[steps.find_index($1.to_sym) - 1]
else
case form_step
when :confirm_email
return do_confirm
when :contact_info
if params[:name].present? && params[:name].gsub(/[\s\W]/, '').present?
current_user.firstname = params[:name].squish
current_user.lastname = nil
else
@errors[:name] = :empty
end
end
@register_template = @registration.registration_fees_paid ? :done : :payment
end
when :payment
if is_post && @registration
amount = params[:amount].to_f
if params[:location].present? && params[:location].gsub(/[\s\W]/, '').present? && (l = Geocoder.search(params[:location])).present?
corrected = view_context.location(l.first)
if amount > 0
@registration.payment_confirmation_token = ENV['RAILS_ENV'] == 'test' ? 'token' : Digest::SHA256.hexdigest(rand(Time.now.to_f * 1000000).to_i.to_s)
@registration.save
host = "#{request.protocol}#{request.host_with_port}"
response = PayPal!.setup(
PayPalRequest(amount),
register_paypal_confirm_url(@this_conference.slug, :paypal_confirm, @registration.payment_confirmation_token),
register_paypal_confirm_url(@this_conference.slug, :paypal_cancel, @registration.payment_confirmation_token)
)
if ENV['RAILS_ENV'] != 'test'
redirect_to response.redirect_uri
if corrected.present?
@registration.city = corrected
if params[:location].gsub(/[\s,]/, '').downcase != @registration.city.gsub(/[\s,]/, '').downcase
@warnings << view_context._('warnings.messages.location_corrected',"Your location was corrected from \"#{params[:location]}\" to \"#{corrected}\". If this doesn't reflect your intended location, you can change this again in the contact info step.", vars: {original: params[:location], corrected: corrected})
end
else
@errors[:location] = :unknown
end
else
@errors[:location] = :empty
end
return
end
@register_template = :done
end
when :paypal_confirm
if @registration && @registration.payment_confirmation_token == params[:confirmation_token]
if ENV['RAILS_ENV'] == 'test'
@amount = YAML.load(@registration.payment_info)[:amount]
else
@amount = PayPal!.details(params[:token]).amount.total
# testing this does't work in test but it works in devo and prod
@registration.payment_info = {:payer_id => params[:PayerID], :token => params[:token], :amount => @amount}.to_yaml
if params[:languages].present?
current_user.languages = params[:languages].keys
else
@errors[:languages] = :empty
end
current_user.save! unless @errors.present?
end
@amount = (@amount * 100).to_i.to_s.gsub(/^(.*)(\d\d)$/, '\1.\2')
if @errors.present?
@register_template = form_step
else
unless @registration.nil?
@register_template = steps[steps.find_index(form_step) + 1]
# have we reached a new level?
unless @registration.steps_completed.include? form_step.to_s
@registration.steps_completed ||= []
@registration.steps_completed << form_step
# workshops is the last step
if @register_template == :workshops
UserMailer.send_mail :registration_confirmation do
{
:args => @registration
}
end
end
end
@registration.save!
@register_template = :paypal_confirm
end
when :paypal_confirmed
info = YAML.load(@registration.payment_info)
@amount = nil
status = nil
if ENV['RAILS_ENV'] == 'test'
status = info[:status]
@amount = info[:amount]
else
paypal = PayPal!.checkout!(info[:token], info[:payer_id], PayPalRequest(info[:amount]))
status = paypal.payment_info.first.payment_status
@amount = paypal.payment_info.first.amount.total
end
if status == 'Completed'
@registration.registration_fees_paid = @amount
@registration.save!
@register_template = :done
else
@register_template = :payment
end
when :paypal_cancel
if @registration
@registration.payment_confirmation_token = nil
@registration.save
@register_template = :payment
@registration.save!
end
end
end
when :register
@register_template = :questions
end
if @register_template == :payment && !@this_conference.paypal_username
@register_template = :done
end
@register_template ||= (params[:step] || current_step).to_sym
# don't let the user edit registration if registration is closed
if !@conference.registration_open && @register_template == :questions
@register_template = :done
end
return redirect_to register_path(@this_conference.slug) if
logged_in? && @register_template != current_step &&
!@registration.steps_completed.include?(@register_template.to_s)
# process data from the last view
# case (params[:button] || '').to_sym
# when :confirm_email
# @register_template = :email_sent if is_post
# when :policy
# @register_template = :questions if is_post
# when :save
# if is_post
# new_registration = !@registration
# @registration = ConferenceRegistration.new if new_registration
# @registration.conference_id = @this_conference.id
# @registration.user_id = current_user.id
# @registration.is_attending = 'yes'
# @registration.is_confirmed = true
# @registration.city = params[:location]
# @registration.arrival = params[:arrival]
# @registration.languages = params[:languages].keys.to_json
# @registration.departure = params[:departure]
# @registration.housing = params[:housing]
# @registration.bike = params[:bike]
# @registration.food = params[:food]
# @registration.allergies = params[:allergies]
# @registration.other = params[:other]
# @registration.save
# current_user.firstname = params[:name].squish
# current_user.lastname = nil
# current_user.save
# if new_registration
# UserMailer.send_mail :registration_confirmation do
# {
# :args => @registration
# }
# end
# end
# @register_template = @registration.registration_fees_paid ? :done : :payment
# end
# when :payment
# if is_post && @registration
# amount = params[:amount].to_f
# if amount > 0
# @registration.payment_confirmation_token = ENV['RAILS_ENV'] == 'test' ? 'token' : Digest::SHA256.hexdigest(rand(Time.now.to_f * 1000000).to_i.to_s)
# @registration.save
# host = "#{request.protocol}#{request.host_with_port}"
# response = PayPal!.setup(
# PayPalRequest(amount),
# register_paypal_confirm_url(@this_conference.slug, :paypal_confirm, @registration.payment_confirmation_token),
# register_paypal_confirm_url(@this_conference.slug, :paypal_cancel, @registration.payment_confirmation_token)
# )
# if ENV['RAILS_ENV'] != 'test'
# redirect_to response.redirect_uri
# end
# return
# end
# @register_template = :done
# end
# when :paypal_confirm
# if @registration && @registration.payment_confirmation_token == params[:confirmation_token]
# if ENV['RAILS_ENV'] == 'test'
# @amount = YAML.load(@registration.payment_info)[:amount]
# else
# @amount = PayPal!.details(params[:token]).amount.total
# # testing this does't work in test but it works in devo and prod
# @registration.payment_info = {:payer_id => params[:PayerID], :token => params[:token], :amount => @amount}.to_yaml
# end
# @amount = (@amount * 100).to_i.to_s.gsub(/^(.*)(\d\d)$/, '\1.\2')
# @registration.save!
# @register_template = :paypal_confirm
# end
# when :paypal_confirmed
# info = YAML.load(@registration.payment_info)
# @amount = nil
# status = nil
# if ENV['RAILS_ENV'] == 'test'
# status = info[:status]
# @amount = info[:amount]
# else
# paypal = PayPal!.checkout!(info[:token], info[:payer_id], PayPalRequest(info[:amount]))
# status = paypal.payment_info.first.payment_status
# @amount = paypal.payment_info.first.amount.total
# end
# if status == 'Completed'
# @registration.registration_fees_paid = @amount
# @registration.save!
# @register_template = :done
# else
# @register_template = :payment
# end
# when :paypal_cancel
# if @registration
# @registration.payment_confirmation_token = nil
# @registration.save
# @register_template = :payment
# end
# when :register
# @register_template = :questions
# end
# if @register_template == :payment && !@this_conference.paypal_username
# @register_template = :done
# end
# # don't let the user edit registration if registration is closed
# if !@conference.registration_open && @register_template == :questions
# @register_template = :done
# end
# prepare data for the next view
case @register_template
@ -680,12 +749,17 @@ class ConferencesController < ApplicationController
@languages = JSON.parse(@registration.languages).map &:to_sym
end
when :workshops
@my_workshops = [1,2,3,4].map { |i|
{
:title => (Forgery::LoremIpsum.sentence({:random => true}).gsub(/\.$/, '').titlecase),
:info => (Forgery::LoremIpsum.sentences(rand(1...5), {:random => true}))
}
}
@page_title = 'articles.conference_registration.headings.Workshops'
@workshops = Workshop.where(conference_id: @this_conference.id)
@my_workshops = Workshop.joins(:workshop_facilitators).where(
workshop_facilitators: { user_id: current_user.id },
conference_id: @this_conference.id
)
@workshops_in_need = Workshop.where(conference_id: @this_conference.id, needs_facilitators: true)
when :contact_info
@page_title = 'articles.conference_registration.headings.Contact_Info'
when :policy
@page_title = 'articles.conference_registration.headings.Policy_Agreement'
when :done
@amount = ((@registration.registration_fees_paid || 0) * 100).to_i.to_s.gsub(/^(.*)(\d\d)$/, '\1.\2')
end
@ -709,7 +783,7 @@ class ConferencesController < ApplicationController
session[:registration_step] = 'confirm'
redirect_to action: 'register'
else
do_404
return do_404
end
end
@ -734,7 +808,7 @@ class ConferencesController < ApplicationController
redirect_to action: 'register'
end
else
do_404
return do_404
end
end
@ -754,7 +828,7 @@ class ConferencesController < ApplicationController
# session[:registration_step] = 'paypal-confirmed'
# redirect_to action: 'register'
# else
# do_404
# return do_404
# end
# end
@ -825,32 +899,67 @@ class ConferencesController < ApplicationController
set_conference
set_conference_registration
@workshop = Workshop.find_by_id_and_conference_id(params[:workshop_id], @this_conference.id)
do_404 unless @workshop
return do_404 unless @workshop
@translations_available_for_editing = []
I18n.backend.enabled_locales.each do |locale|
@translations_available_for_editing << locale if @workshop.can_translate?(current_user, locale)
end
@page_title = 'page_titles.conferences.View_Workshop'
render 'workshops/show'
end
def create_workshop
set_conference
set_conference_registration
@languages = []
@workshop = Workshop.new
@languages = [I18n.locale.to_sym]
@needs = []
@page_title = 'page_titles.conferences.Create_Workshop'
render 'workshops/new'
end
def translate_workshop
@is_translating = true
@translation = params[:locale]
@page_title = 'page_titles.conferences.Translate_Workshop'
@page_title_vars = { language: view_context.language_name(@translation) }
edit_workshop
end
def edit_workshop
set_conference
set_conference_registration
@workshop = Workshop.find_by_id_and_conference_id(params[:workshop_id], @this_conference.id)
do_404 unless @workshop
return do_404 unless @workshop.present?
@page_title ||= 'page_titles.conferences.Edit_Workshop'
@can_edit = @workshop.can_edit?(current_user)
do_403 unless @can_edit || @workshop.can_translate?(current_user, I18n.locale)
@title = @workshop.title
@info = @workshop.info
@is_translating ||= false
if @is_translating
return do_404 if @translation.to_s == @workshop.locale.to_s || !I18n.backend.enabled_locales.include?(@translation.to_s)
return do_403 unless @workshop.can_translate?(current_user, @translation)
@title = @workshop._title(@translation)
@info = @workshop._info(@translation)
else
return do_403 unless @can_edit
@title = @workshop.title
@info = @workshop.info
end
@needs = JSON.parse(@workshop.needs || '[]').map &:to_sym
@languages = JSON.parse(@workshop.languages || '[]').map &:to_sym
@space = @workshop.space.to_sym if @workshop.space
@theme = @workshop.theme.to_sym if @workshop.theme
@notes = @workshop.notes
render 'workshops/new'
end
@ -859,15 +968,8 @@ class ConferencesController < ApplicationController
set_conference_registration
@workshop = Workshop.find_by_id_and_conference_id(params[:workshop_id], @this_conference.id)
if !@workshop
do_404
return
end
if !@workshop.can_delete?(current_user)
do_403
return
end
return do_404 unless @workshop.present?
return do_403 unless @workshop.can_delete?(current_user)
if request.post?
if params[:button] == 'confirm'
@ -876,11 +978,9 @@ class ConferencesController < ApplicationController
@workshop.destroy
end
redirect_to workshops_url
return
return redirect_to workshops_url
end
redirect_to edit_workshop_url(@this_conference.slug, @workshop.id)
return
return redirect_to edit_workshop_url(@this_conference.slug, @workshop.id)
end
render 'workshops/delete'
@ -890,30 +990,57 @@ class ConferencesController < ApplicationController
set_conference
set_conference_registration
if params[:workshop_id]
if params[:button].to_sym != :save
if params[:workshop_id].present?
return redirect_to view_workshop_url(@this_conference.slug, params[:workshop_id])
end
return redirect_to register_step_path(@this_conference.slug, 'workshops')
end
if params[:workshop_id].present?
workshop = Workshop.find(params[:workshop_id])
do_404 unless workshop
return do_404 unless workshop.present?
can_edit = workshop.can_edit?(current_user)
else
workshop = Workshop.new(:conference_id => @this_conference.id)
workshop.workshop_facilitators = [WorkshopFacilitator.new(:user_id => current_user.id, :role => :creator)]
can_edit = true
end
can_edit = workshop.can_edit?(current_user)
do_403 unless can_edit || workshop.can_translate?(current_user, I18n.locale)
title = params[:title]
info = params[:info].gsub(/^\s*(.*?)\s*$/, '\1')
workshop.title = params[:title]
workshop.info = params[:info]
if params[:translation].present? && workshop.can_translate?(current_user, params[:translation])
old_title = workshop._title(params[:translation])
old_info = workshop._info(params[:translation])
if can_edit
# dont allow translators to edit these fields
workshop.languages = (params[:languages] || {}).keys.to_json
workshop.needs = (params[:needs] || {}).keys.to_json
workshop.theme = params[:theme] == 'other' ? params[:other_theme] : params[:theme]
workshop.space = params[:space]
workshop.notes = params[:notes]
do_save = false
unless title == old_title
workshop.set_column_for_locale(:title, params[:translation], title, current_user.id)
do_save = true
end
unless info == old_info
workshop.set_column_for_locale(:info, params[:translation], info, current_user.id)
do_save = true
end
# only save if the text has changed, if we want to make sure only to update the translator id if necessary
workshop.save_translations if do_save
elsif can_edit
workshop.title = title
workshop.info = info
workshop.languages = (params[:languages] || {}).keys.to_json
workshop.needs = (params[:needs] || {}).keys.to_json
workshop.theme = params[:theme] == 'other' ? params[:other_theme] : params[:theme]
workshop.space = params[:space]
workshop.notes = params[:notes]
workshop.needs_facilitators = params[:needs_facilitators].present?
workshop.save
else
return do_403
end
workshop.save
redirect_to view_workshop_url(@this_conference.slug, workshop.id)
end
@ -921,7 +1048,7 @@ class ConferencesController < ApplicationController
set_conference
set_conference_registration
workshop = Workshop.find_by_id_and_conference_id(params[:workshop_id], @this_conference.id)
do_404 unless workshop
return do_404 unless workshop
# save the current state
interested = workshop.interested? current_user
@ -939,8 +1066,8 @@ class ConferencesController < ApplicationController
set_conference
set_conference_registration
@workshop = Workshop.find_by_id_and_conference_id(params[:workshop_id], @this_conference.id)
do_404 unless @workshop
do_403 if @workshop.facilitator?(current_user) || !current_user
return do_404 unless @workshop
return do_403 if @workshop.facilitator?(current_user) || !current_user
render 'workshops/facilitate'
end
@ -949,8 +1076,8 @@ class ConferencesController < ApplicationController
set_conference
set_conference_registration
workshop = Workshop.find_by_id_and_conference_id(params[:workshop_id], @this_conference.id)
do_404 unless workshop
do_403 if workshop.facilitator?(current_user) || !current_user
return do_404 unless workshop
return do_403 if workshop.facilitator?(current_user) || !current_user
# create the request by making the user a facilitator but making their role 'requested'
WorkshopFacilitator.create(user_id: current_user.id, workshop_id: workshop.id, role: :requested)
@ -968,8 +1095,8 @@ class ConferencesController < ApplicationController
set_conference
set_conference_registration
@workshop = Workshop.find_by_id_and_conference_id(params[:workshop_id], @this_conference.id)
do_404 unless @workshop
do_403 unless @workshop.requested_collaborator?(current_user)
return do_404 unless @workshop
return do_403 unless @workshop.requested_collaborator?(current_user)
render 'workshops/facilitate_request_sent'
end
@ -978,7 +1105,7 @@ class ConferencesController < ApplicationController
set_conference
set_conference_registration
workshop = Workshop.find_by_id_and_conference_id(params[:workshop_id], @this_conference.id)
do_404 unless workshop && current_user
return do_404 unless workshop && current_user
user_id = params[:user_id].to_i
action = params[:approve_or_deny].to_sym
@ -1019,7 +1146,7 @@ class ConferencesController < ApplicationController
end
end
do_403
return do_403
end
def add_workshop_facilitator
@ -1029,7 +1156,7 @@ class ConferencesController < ApplicationController
set_conference_registration
workshop = Workshop.find_by_id_and_conference_id(params[:workshop_id], @this_conference.id)
do_404 unless workshop && current_user
return do_404 unless workshop && current_user
unless workshop.facilitator?(user)
WorkshopFacilitator.create(user_id: user.id, workshop_id: workshop.id, role: :collaborator)
@ -1046,7 +1173,7 @@ class ConferencesController < ApplicationController
def schedule
set_conference
do_404 unless @this_conference.workshop_schedule_published || @this_conference.host?(current_user)
return do_404 unless @this_conference.workshop_schedule_published || @this_conference.host?(current_user)
@events = Event.where(:conference_id => @this_conference.id)
@locations = EventLocation.where(:conference_id => @this_conference.id)
@ -1057,7 +1184,7 @@ class ConferencesController < ApplicationController
def edit_schedule
set_conference
set_conference_registration
do_404 unless @this_conference.host?(current_user)
return do_404 unless @this_conference.host?(current_user)
@workshops = Workshop.where(:conference_id => @this_conference.id)
@events = Event.where(:conference_id => @this_conference.id)
@ -1126,7 +1253,7 @@ class ConferencesController < ApplicationController
def save_schedule
set_conference
do_404 unless @this_conference.host?(current_user)
return do_404 unless @this_conference.host?(current_user)
@days = Array.new
start_day = @this_conference.start_date.strftime('%u').to_i
@ -1164,7 +1291,6 @@ class ConferencesController < ApplicationController
session[:workshops][id] = {
:start_time => @workshops[i].start_time,
:end_time => @workshops[i].end_time,
:end_time => @workshops[i].end_time,
:event_location_id => @workshops[i].event_location_id
}
end
@ -1189,7 +1315,6 @@ class ConferencesController < ApplicationController
session[:events][id] = {
:start_time => @events[i].start_time,
:end_time => @events[i].end_time,
:end_time => @events[i].end_time,
:event_location_id => @events[i].event_location_id
}
end
@ -1228,7 +1353,7 @@ class ConferencesController < ApplicationController
def add_event
set_conference
set_conference_registration
do_404 unless @this_conference.host?(current_user)
return do_404 unless @this_conference.host?(current_user)
render 'events/edit'
end
@ -1236,22 +1361,22 @@ class ConferencesController < ApplicationController
def edit_event
set_conference
set_conference_registration
do_404 unless @this_conference.host?(current_user)
return do_404 unless @this_conference.host?(current_user)
@event = Event.find(params[:id])
do_403 unless @event.conference_id == @this_conference.id
return do_403 unless @event.conference_id == @this_conference.id
render 'events/edit'
end
def save_event
set_conference
do_404 unless @this_conference.host?(current_user)
return do_404 unless @this_conference.host?(current_user)
if params[:event_id]
event = Event.find(params[:event_id])
do_403 unless event.conference_id == @this_conference.id
return do_403 unless event.conference_id == @this_conference.id
else
event = Event.new(:conference_id => @this_conference.id)
end
@ -1268,7 +1393,7 @@ class ConferencesController < ApplicationController
def add_location
set_conference
set_conference_registration
do_404 unless @this_conference.host?(current_user)
return do_404 unless @this_conference.host?(current_user)
render 'event_locations/edit'
end
@ -1276,10 +1401,10 @@ class ConferencesController < ApplicationController
def edit_location
set_conference
set_conference_registration
do_404 unless @this_conference.host?(current_user)
return do_404 unless @this_conference.host?(current_user)
@location = EventLocation.find(params[:id])
do_403 unless @location.conference_id == @this_conference.id
return do_403 unless @location.conference_id == @this_conference.id
@amenities = JSON.parse(@location.amenities || '[]').map &:to_sym
@ -1288,12 +1413,12 @@ class ConferencesController < ApplicationController
def save_location
set_conference
do_404 unless @this_conference.host?(current_user)
return do_404 unless @this_conference.host?(current_user)
if params[:location_id]
location = EventLocation.find(params[:location_id])
do_403 unless location.conference_id == @this_conference.id
return do_403 unless location.conference_id == @this_conference.id
else
location = EventLocation.new(:conference_id => @this_conference.id)
end
@ -1313,6 +1438,61 @@ class ConferencesController < ApplicationController
# redirect_to conferences_url, notice: 'Conference was successfully destroyed.'
#end
helper_method :registration_steps
helper_method :current_registration_steps
helper_method :registration_complete?
def registration_steps(conference = nil)
conference ||= @this_conference || @conference
{
pre: [:policy, :contact_info, :workshops],
open: [:policy, :contact_info, :questions, :payment, :workshops]
}[conference.registration_status]
end
def required_steps(conference = nil)
# return the intersection of current steps and reuired steps
registration_steps(conference || @this_conference || @conference) & # current steps
[:policy, :contact_info, :questions] # all required steps
end
def registration_complete?(registration = @registration)
completed_steps = registration.steps_completed || []
required_steps(registration.conference).each do | step |
return false unless completed_steps.include?(step.to_s)
end
return true
end
def current_registration_steps(registration = @registration)
return nil unless registration.present?
steps = registration_steps(registration.conference)
current_steps = []
disable_steps = false
completed_steps = registration.steps_completed || []
steps.each do | step |
# disable the step if we've already found an incomplete step
enabled = !disable_steps
# record whether or not we've found an incomplete step
disable_steps ||= !completed_steps.include?(step.to_s)
current_steps << {
name: step,
enabled: enabled
}
end
return current_steps
end
def current_step(registration = @registration)
completed_steps = registration.steps_completed || []
(registration_steps(registration.conference) || []).each do | step |
return step unless completed_steps.include?(step.to_s)
end
return registration_steps(registration.conference).last
end
private
# Use callbacks to share common setup or constraints between actions.
def set_conference
@ -1321,24 +1501,20 @@ class ConferencesController < ApplicationController
def set_conference_registration
@registration = logged_in? ? ConferenceRegistration.find_by(:user_id => current_user.id, :conference_id => @this_conference.id) : nil
@is_host = @this_conference.host?(current_user)
if @registration || @is_host
@submenu = {
register_path(@this_conference.slug) => 'registration.Registration',
workshops_path(@this_conference.slug) => 'registration.Workshops'
}
@submenu[schedule_path(@this_conference.slug)] = 'registration.Schedule' if @this_conference.workshop_schedule_published || @is_host
if @is_host
@submenu[edit_conference_path(@this_conference.slug)] = 'registration.Edit'
@submenu[stats_path(@this_conference.slug)] = 'registration.Stats'
@submenu[broadcast_path(@this_conference.slug)] = 'registration.Broadcast'
end
end
end
def set_or_create_conference_registration
set_conference_registration
@registration ||= ConferenceRegistration.new(
conference: @this_conference,
user_id: current_user.id,
steps_completed: []
)
end
# Only allow a trusted parameter "white list" through.
def conference_params
params.require(:conference).permit(:title, :slug, :start_date, :end_date, :info, :poster, :cover, :workshop_schedule_published, :registration_open, :meals_provided, :meal_info, :travel_info, :conference_type_id, conference_types: [:id])
params.require(:conference).permit(:title, :slug, :start_date, :end_date, :info, :poster, :cover, :workshop_schedule_published, :registration_status, :meals_provided, :meal_info, :travel_info, :conference_type_id, conference_types: [:id])
end
def update_field_position(field_id, position)

39
app/controllers/oauths_controller.rb

@ -4,24 +4,37 @@ class OauthsController < ApplicationController
# sends the user on a trip to the provider,
# and after authorizing there back to the callback url.
def oauth
session[:oauth_last_url] = request.referer
login_at(auth_params[:provider])
end
def callback
provider = auth_params[:provider]
if @user = login_from(provider)
redirect_to root_path, :notice => "Logged in with #{provider.titleize}!"
else
begin
@user = create_from(auth_params[:provider])
reset_session
auto_login(@user)
redirect_to root_path, :notice => "Signed up with #{provider.titleize}!"
rescue
redirect_to root_path, :alert => "Failed to login with #{provider.titleize}!"
end
user_info = (sorcery_fetch_user_hash auth_params[:provider] || {})[:user_info]
user = User.find_by_email(user_info['email'])
# create the user if the email is not recognized
unless user
user = User.new(email: user_info['email'], firstname: user_info['name'])
user.save!
end
# log in the user
auto_login(user) if user
redirect_to (session[:oauth_last_url] || home_path)
#, :notice => "Logged in with #{provider.titleize}!"
# if @user = login_from(provider)
# else
# begin
# @user = create_from(auth_params[:provider])
# reset_session
# auto_login(@user)
# redirect_to redirect_url, :notice => "Signed up with #{provider.titleize}!"
# rescue
# redirect_to redirect_url, :alert => "Failed to login with #{provider.titleize}!"
# end
# end
end
private

71
app/helpers/application_helper.rb

@ -98,6 +98,35 @@ module ApplicationHelper
content_for(:banner) { ('<div class="row"><h1>' + banner_title.to_s + '</h1></div>').html_safe }
end
def add_stylesheet(sheet)
@stylesheets ||= []
@stylesheets << sheet
end
def stylesheets
html = ''
Rack::MiniProfiler.step('inject_css') do
html += inject_css!
end
(@stylesheets || []).each do |css|
Rack::MiniProfiler.step("inject_css #{css}") do
html += inject_css! css.to_s
end
end
html += stylesheet_link_tag 'i18n-debug' if request.params['i18nDebug']
return html.html_safe
end
def add_inline_script(script)
@_inline_scripts ||= []
@_inline_scripts << Rails.application.assets.find_asset("#{script.to_s}.js").to_s
end
def inline_scripts
return '' unless @_inline_scripts.present?
"<script>#{@_inline_scripts.join("\n")}</script>".html_safe
end
def banner_attribution
if @@banner_image && @@banner_attribution_details
src = @@banner_attribution_details[:src]
@ -185,12 +214,17 @@ module ApplicationHelper
false
end
def off_screen(text)
"<span class=\"screen-reader-text\">#{text}</span>".html_safe
end
def url_for_locale(locale)
url_for(params
.merge({action: (params[:_original_action] || params[:action])}
.merge(url_params(locale)))
.delete(:_original_action)
)
new_params = params.merge({action: (params[:_original_action] || params[:action])})
new_params.delete(:_original_action)
return url_for(new_params.merge({lang: locale.to_s})) if Rails.env.development? || Rails.env.test?
return "https://preview-#{locale.to_s}.bikebike.org#{url_for(new_params)}" if Rails.env.preview?
"https://#{locale.to_s}.bikebike.org#{url_for(new_params)}"
end
def registration_steps(conference = @conference)
@ -550,6 +584,18 @@ module ApplicationHelper
country = location.country
region = location.territory
city = location.city
elsif location.data.present? && location.data['address_components'].present?
component_map = {
'locality' => :city,
'administrative_area_level_1' => :region,
'country' => :country
}
location.data['address_components'].each do | component |
types = component['types']
country = component['short_name'] if types.include? 'country'
region = component['short_name'] if types.include? 'administrative_area_level_1'
city = component['long_name'] if types.include? 'locality'
end
else
country = location.data['country_code']
region = location.data['region_code']
@ -565,6 +611,14 @@ module ApplicationHelper
return hash.length > 1 ? _("geography.formats.#{hash.keys.join('_')}", vars: hash) : hash.values.first
end
def show_errors(field)
return '' unless @errors && @errors[field].present?
error_txt = _"errors.messages.fields.#{field.to_s}.#{@errors[field]}", :s
"<div class=\"field-error\">#{error_txt}</div>".html_safe
end
def nav_link(link, title = nil, class_name = nil)
if title.nil? && link.is_a?(Symbol)
title = link
@ -572,12 +626,13 @@ module ApplicationHelper
end
if class_name.nil? && title.is_a?(Symbol)
class_name = title
title = _"page_titles.#{title.to_s.titlecase}"
end
title = _"page_titles.#{title.to_s.titlecase.gsub(/\s/, '_')}"
classes = []
classes << class_name if class_name.present?
classes << 'current' if current_page?(link.gsub(/(^.*)\/$/, '\1'))
link_to title.html_safe, link, :class => classes
classes << "strlen-#{strip_tags(title).length}"
classes << 'current' if request.fullpath.start_with?(link.gsub(/^(.*?)\/$/, '\1'))
link_to "<span class=\"title\">#{title}</span>".html_safe, link, :class => classes
end
def date(date, format = :long)

97
app/mailers/user_mailer.rb

@ -1,3 +1,5 @@
require 'diffy'
class UserMailer < ActionMailer::Base
add_template_helper(ApplicationHelper)
include LinguaFrancaHelper
@ -26,10 +28,6 @@ class UserMailer < ActionMailer::Base
mail to: "to@example.org"
end
def test_email
mail to: 'goodgodwin@hotmail.com', subject: 'This is a test', from: 'info@preview.bikebike.org'
end
def conference_registration_email(conference, data, conference_registration)
@data = data
@conference = conference
@ -49,8 +47,8 @@ class UserMailer < ActionMailer::Base
def email_confirmation(confirmation)
@confirmation = confirmation
@host = UserMailer.default_url_options[:host]
mail to: confirmation.user.email,
subject: (_'email.subject.confirm_email','Please confirm your email address')
@subject = _'email.subject.confirm_email','Please confirm your email address'
mail to: confirmation.user.email, subject: @subject
end
def registration_confirmation(registration)
@ -58,19 +56,27 @@ class UserMailer < ActionMailer::Base
@registration = registration
@conference = Conference.find(@registration.conference_id)
@user = User.find(@registration.user_id)
mail to: @user.email,
subject: _('email.subject.registration_confirmed',
"Thank you for registering for #{@conference.title}",
:vars => {:conference_title => @conference.title})
@subject = @conference.registration_status.to_sym == :pre ?
_(
'email.subject.pre_registration_confirmed',
"Thank you for pre-registering for #{@conference.title}",
:vars => {:conference_title => @conference.title}
) : _(
'email.subject.registration_confirmed',
"Thank you for registering for #{@conference.title}",
:vars => {:conference_title => @conference.title}
)
mail to: @user.email, subject: @subject
end
def broadcast(host, subject, content, user, conference)
@host = host
@content = content
@banner = nil#(@host || 'http://localhost/') + (conference ? (conference.poster.preview.url || '') : image_url('logo.png'))
@subject = "[#{conference ? conference.title : 'Bike!Bike!'}] #{subject}"
if user && user.email
email = user.email
mail to: email, subject: "[#{conference ? conference.title : 'Bike!Bike!'}] #{subject}"
mail to: email, subject: @subject
end
end
@ -84,11 +90,10 @@ class UserMailer < ActionMailer::Base
end
@message = message
@conference = Conference.find(@workshop.conference_id)
mail to: addresses,
from: @requester.email,
subject: _('email.subject.workshop_facilitator_request',
"Request to facilitate #{@workshop.title} from #{@requester.firstname}",
@subject = _('email.subject.workshop_facilitator_request',
"Request to facilitate #{@workshop.title} from #{@requester.name}",
:vars => {:workshop_title => @workshop.title, :requester_name => @requester.firstname})
mail to: addresses, from: @requester.email, subject: @subject
end
def workshop_facilitator_request_approved(workshop, user)
@ -96,10 +101,10 @@ class UserMailer < ActionMailer::Base
@workshop = workshop
@conference = Conference.find(@workshop.conference_id)
@user = user
mail to: user.email,
subject: (_'email.subject.workshop_request_approved',
"You have been added as a facilitator of #{@workshop.title}",
:vars => {:workshop_title => @workshop.title})
@subject = (_'email.subject.workshop_request_approved',
"You have been added as a facilitator of #{@workshop.title}",
:vars => {:workshop_title => @workshop.title})
mail to: user.email, subject: @subject
end
def workshop_facilitator_request_denied(workshop, user)
@ -107,10 +112,56 @@ class UserMailer < ActionMailer::Base
@workshop = workshop
@conference = Conference.find(@workshop.conference_id)
@user = user
mail to: user.email,
subject: (_'email.subject.workshop_request_denied',
"Your request to facilitate #{@workshop.title} has been denied",
:vars => {:workshop_title => @workshop.title})
@subject = (_'email.subject.workshop_request_denied',
"Your request to facilitate #{@workshop.title} has been denied",
:vars => {:workshop_title => @workshop.title})
mail to: user.email, subject: @subject
end
def workshop_translated(workshop, data, locale, user, translator)
@host = UserMailer.default_url_options[:host]
@workshop = workshop
@data = data
@locale = locale
@locale_name = language_name(locale)
@user = user
@translator = translator
@subject = (_'email.subject.workshop_translated',
"The #{@locale_name} translation for #{@workshop.title} has been modified",
vars: {language: @language_name, workshop_title: @workshop.title})
@data.each do |field, values|
diff = Diffy::Diff.new(values[:old], values[:new])
@data[field][:diff] = {
text: diff.to_s(:text),
html: diff.to_s(:html)
}
end
@wrapper_id = :full_width
mail to: user.email, subject: @subject
end
def workshop_original_content_changed(workshop, data, user, translator)
@host = UserMailer.default_url_options[:host]
@workshop = workshop
@data = data
@user = user
@translator = translator
@subject = (_'email.subject.workshop_original_content_changed',
"Original content for #{@workshop.title} has been modified",
vars: {workshop_title: @workshop.title})
@data.each do |field, values|
diff = Diffy::Diff.new(values[:old], values[:new])
@data[field][:diff] = {
text: diff.to_s(:text),
html: diff.to_s(:html)
}
end
@wrapper_id = :full_width
mail to: user.email, subject: @subject
end
end

19
app/models/conference.rb

@ -42,8 +42,23 @@ class Conference < ActiveRecord::Base
def registered?(user)
registration = ConferenceRegistration.find_by(:user_id => user.id, :conference_id => id)
return false unless registration
return registration.is_attending
return registration ? registration.is_attending : false
end
def registration_exists?(user)
ConferenceRegistration.find_by(:user_id => user.id, :conference_id => id).present?
end
def registration_open
registration_status == :open
end
def registration_status
read_attribute(:registration_status).to_sym
end
def registration_status=(new_registration_status)
write_attribute :registration_status, new_registration_status.to_s
end
end

16
app/models/conference_registration.rb

@ -1,5 +1,11 @@
class ConferenceRegistration < ActiveRecord::Base
has_many :conference_registration_responses
AttendingOptions = [:yes, :no]
end
class ConferenceRegistration < ActiveRecord::Base
belongs_to :conference
belongs_to :user
has_many :conference_registration_responses
AttendingOptions = [:yes, :no]
def languages
user.languages
end
end

14
app/models/user.rb

@ -21,8 +21,18 @@ class User < ActiveRecord::Base
has_many :authentications, :dependent => :destroy
accepts_nested_attributes_for :authentications
def can_translate?
is_translator
def can_translate?(to_locale = nil, from_locale = nil)
is_translator unless to_locale.present?
from_locale = I18n.locale unless from_locale.present?
return languages.present? &&
to_locale.to_s != from_locale.to_s &&
languages.include?(to_locale.to_s) &&
languages.include?(from_locale.to_s)
end
def name
firstname || username || email
end
end

32
app/models/workshop.rb

@ -18,7 +18,7 @@ class Workshop < ActiveRecord::Base
return nil unless user
workshop_facilitators.each do |u|
if u.user_id == user.id
return conference.registered?(user) ? u.role.to_sym : :unregistered
return conference.registration_exists?(user) ? u.role.to_sym : :unregistered
end
end
return nil
@ -31,7 +31,7 @@ class Workshop < ActiveRecord::Base
def active_facilitators
users = []
workshop_facilitators.each do |u|
users << User.find(u.user_id) if u.role.to_sym != :request
users << User.find(u.user_id) unless u.role.to_sym == :requested
end
return users
end
@ -75,16 +75,18 @@ class Workshop < ActiveRecord::Base
end
def interested_count
return 0 unless id
collaborators = []
workshop_facilitators.each do |f|
collaborators << f.user_id unless f.role.to_sym == :requested
end
interested = WorkshopInterest.where("workshop_id=#{id} AND user_id NOT IN (#{collaborators.join ','})")
return 10 unless collaborators.present?
interested = WorkshopInterest.where("workshop_id=#{id} AND user_id NOT IN (#{collaborators.join ','})") || []
interested ? interested.size : 0
end
def can_translate?(user, lang)
(user.can_translate? && lang.to_sym != locale.to_sym) || can_edit?(user)
user.can_translate?(lang, locale) || (can_edit?(user) && lang.to_s != locale.to_s)
end
def conference_day
@ -100,6 +102,28 @@ class Workshop < ActiveRecord::Base
((end_time - start_time) / 60).to_i
end
def self.all_themes
[:race_gender, :mechanics, :funding, :organization, :community]
end
def get_translators(data, loc = nil)
notify_list = {}
active_facilitators.each do |facilitator|
notify_list[facilitator.id] = facilitator
end
data.each do | column, value |
(
loc.present? ?
get_translators_for_column_and_locale(column, loc) :
get_translators_for_column(column)
).each do |id|
notify_list[id] = User.find(id)
end
end
return notify_list
end
private
def make_slug
if !self.slug

3
app/views/application/404.html.haml

@ -5,5 +5,4 @@
= columns(medium: 12) do
%h1
=_'error.404.title','This page does not exist!'
%p
=p(_'error.404.description', :p)
=paragraph(_'error.404.description', :p)

13
app/views/application/_header.html.haml

@ -5,13 +5,14 @@
- figure = nil
- if capable_of(:css_mixblendmode)
- cover = "<div class=\"cover\" style=\"background-image: url(#{image})\"></div>"
- elsif capable_of(:svg) && Rails.env != 'test'
- banner_image = 'application/banner_image.svg'
- else
- style = "background-image: url(#{image})"
#header-title.short{style: style}
= (render banner_image, {:image => image}) if banner_image
= (render banner_image, {:image => image}) if defined?(banner_image) == "local-variable"
= cover.html_safe if cover
= row do
= columns do
%h1=_(@page_title || "page_titles.#{page_group.to_s}.#{page_key.to_s}")
- if @page_title.present? || defined?(page_group)
- content_for :title do
=@page_title.present? ? I18n.t(@page_title, @page_title_vars) : I18n.t("page_titles.#{page_group.to_s}.#{page_key.to_s}")
= row do
= columns do
%h1=(_"page_titles.#{page_group.to_s}.#{page_key.to_s}")

2
app/views/application/_login_confirmation_sent.html.haml

@ -2,4 +2,4 @@
= columns(medium: 12) do
%h2=_'articles.permission_denied.headings.confirmation_sent','Confirmation Sent'
= columns(medium: 6) do
=m('articles.permission_denied.paragraphs.confirmation_sent', :p)
%p='articles.permission_denied.paragraphs.confirmation_sent', :p

26
app/views/application/home.html.haml

@ -9,16 +9,16 @@
- if @schedule
%h3=_'articles.workshops.headings.Schedule'
= render 'schedule/programme', :schedule => @schedule, :conference => @conference, :workshops => @workshops, :events => @events, :locations => @locations, :show_interest => true, :day_parts => @day_parts, :show_previews => true
- elsif @conference.registration_open
%h3=_'articles.workshops.headings.Proposed_Workshops'
%p=_'articles.workshops.paragraphs.Proposed_Workshops', "Would you like to facilitate your own workshop? Simply register and visit the workshops page. If you have already registered you can access the page by restarting the registration process."
%ul.workshop-list
- @conference.workshops.sort_by{ |w| w.title.downcase }.each do |w|
%li
%h4=w.title
.workshop-interest
- if w.interested?(current_user)
=_'articles.workshops.info.you_are_interested_count', "You and #{w.interested_count - 1} others are interested in this workshop", :vars => {:count => (w.interested_count - 1)}
- elsif w.interested_count > 0
=_'articles.workshops.info.interested_count', "#{w.interested_count} people are interested in this workshop", :vars => {:count => w.interested_count}
.workshop-description=markdown w.info
- if @conference.registration_status == :open
%h3=_'articles.workshops.headings.Proposed_Workshops'
%p=_'articles.workshops.paragraphs.Proposed_Workshops', "Would you like to facilitate your own workshop? Simply register and visit the workshops page. If you have already registered you can access the page by restarting the registration process."
%ul.workshop-list
- @conference.workshops.sort_by{ |w| w.title.downcase }.each do |w|
%li
%h4=w.title
.workshop-interest
- if w.interested?(current_user)
=_'articles.workshops.info.you_are_interested_count', "You and #{w.interested_count - 1} others are interested in this workshop", :vars => {:count => (w.interested_count - 1)}
- elsif w.interested_count > 0
=_'articles.workshops.info.interested_count', "#{w.interested_count} people are interested in this workshop", :vars => {:count => w.interested_count}
.workshop-description=markdown w.info

8
app/views/application/permission_denied.html.haml

@ -1,7 +1,9 @@
= render :partial => 'application/header', :locals => {:image_file => @banner_image || '403.jpg'}
%article
- if @template
- if @template.present?
=render @template
- else
%h2=_'articles.permission_denied.headings.main','Sorry, you currently don\'t have access to this page'
%p=_'articles.permission_denied.paragraphs.main', :p
= row do
= columns do
%h2=_'articles.permission_denied.headings.main','Sorry, you currently don\'t have access to this page'
%p=_'articles.permission_denied.paragraphs.main', :p

15
app/views/conferences/_confirm_email.html.haml

@ -1,6 +1,15 @@
%article
= columns(medium: 12) do
%h2=_'articles.conference_registration.headings.Confirm_You_Email_Address','Confirm Email Address'
= columns(medium: 6, large: 5) do
= form_tag register_path(@this_conference.slug) do
%h2=_'articles.conference_registration.headings.Pre_Registration_Details','Pre-Registration is now open!'
%p=_'articles.conference_registration.paragraphs.Pre_Registration_Details', 'By completing the pre-registration process you are letting us know that you are interested in coming, it allows us to contact you when we have news, and lets us better plan by knowing who might be coming. Once registration is fully open, we will need to ask a few more questions and you can confirm whether or not you will actually be coming.'
%h3=_'articles.conference_registration.headings.Verify_Account','Verify your account'
%p=_'articles.conference_registration.paragraphs.Verify_Account', 'In order to confirm that you are a real person and that we will be able to contact you later, please verify your email address. Before continuing with pre-registration, we will send you an email that will allow you to continue with the process.'
= columns(medium: 12) do
= form_tag register_path(@this_conference.slug), class: 'flex-form' do
.email-field.input-field.big
= email_field_tag :email, nil, required: true
= label_tag :email
= button_tag :continue, :value => :confirm_email
= columns(medium: 12, class: 'flex-column') do
%p.stretch-item=_'articles.conference_registration.paragraphs.facebook_sign_in','Alternatively you can sign in using your Facebook account and skip waiting for us to send you an email.'
= link_to (_'forms.actions.generic.facebook_sign_in','Facebook Sign In'), auth_at_provider_path(:provider => :facebook), class: [:button, :facebook]

26
app/views/conferences/_contact_info.html.haml

@ -0,0 +1,26 @@
= columns(medium: 12) do
%h2=_(@page_title)
%p=_'articles.conference_registration.paragraphs.Contact_Info', :s, 2
= columns(medium: 12) do
= form_tag register_path(@this_conference.slug) do
%h3=_'articles.conference_registration.headings.name','What is your name?'
.text-field.input-field{class: @errors[:name].present? ? 'has-error' : nil}
= show_errors :name
= label_tag :name
= text_field_tag :name, @name, required: true
%h3=_'articles.conference_registration.headings.location','Where are you coming from?'
.text-field.input-field{class: @errors[:location].present? ? 'has-error' : nil}
= show_errors :location
= label_tag :location
= text_field_tag :location, @registration.city || location(lookup_ip_location), required: true
%h3=_'articles.conference_registration.headings.languages','Which languages do you speak?'
- puts @errors.to_json.to_s
.check-box-field.input-field{class: @errors[:languages].present? ? 'has-error' : nil}
- [:en, :es, :fr].each do |language|
= check_box_tag "languages[#{language}]", 1, (@registration.languages || [I18n.locale.to_s]).include?(language.to_s)
= label_tag "languages_#{language}" do
= _"languages.#{language}"
= show_errors :languages
.actions.next-prev
= button_tag (params[:step] == :save ? :save : :next), value: :contact_info
= button_tag :previous, value: :prev_contact_info, class: :subdued, formnovalidate: true

2
app/views/conferences/_email_confirm.html.haml

@ -3,4 +3,4 @@
= columns(medium: 12) do
%h2=_'articles.conference_registration.headings.email_confirm','Please confirm your email address'
= columns(medium: 12) do
=m('articles.conference_registration.paragraphs.email_confirm', :p)
%p=_'articles.conference_registration.paragraphs.email_confirm', :p

4
app/views/conferences/_header.html.haml

@ -2,7 +2,7 @@
.title
%h1=_!@conference.title
.details
%h3.primary=location(@conference.organizations.first.locations.first)
%h2.primary=location(@conference.organizations.first.locations.first)
.secondary
= date_span(@conference.start_date.to_date, @conference.end_date.to_date)
%img{src: @conference.poster.full.url || image_path('default_poster.jpg')}
%img{src: @conference.poster.full.url || image_path('default_poster.jpg'), role: :presentation, alt: ''}

19
app/views/conferences/_policy.html.haml

@ -1,10 +1,9 @@
%article
= columns(medium: 12) do
%h2=_'articles.conference_registration.headings.Policy_Agreement'
%p=_'articles.conference_registration.paragraphs.Policy_Agreement', :s, 2
= columns(medium: 10, push: 1) do
= render 'application/policy'
= columns(medium: 12) do
%p=_'articles.conference_registration.paragraphs.Confirm_Agreement', :p
= form_tag register_path(@this_conference.slug) do
.actions= button_tag :agree, :value => :policy
= columns(medium: 12) do
%h2=_@page_title
%p=_'articles.conference_registration.paragraphs.Policy_Agreement', :s, 2
= columns(medium: 10, push: 1) do
= render 'application/policy'
= columns(medium: 12) do
%p=_'articles.conference_registration.paragraphs.Confirm_Agreement', :p
= form_tag register_path(@this_conference.slug) do
.actions= button_tag :agree, :value => :policy

58
app/views/conferences/_workshops.html.haml

@ -1,42 +1,20 @@
= columns(medium: 12) do
%h2=_'articles.conference_registration.headings.Workshops','Workshops'
= columns(medium: 6) do
%h3=_'articles.conference_registration.headings.Add_Workshop'
= form_tag register_path(@this_conference.slug) do
.text-field.input-field
= text_field_tag :title, nil, required: true
= label_tag :title
= text_area_tag :info, nil, required: true
= button_tag :add
= columns(medium: 6) do
%h3=_'articles.conference_registration.headings.Your_Workshops'
%ul.workshops.my-workshops
- @my_workshops.each do |workshop|
%li
%h4=workshop[:title]
.info
%h5{:contenteditable => true}=workshop[:title]
%p{:contenteditable => true}=workshop[:info]
= form_tag register_path(@this_conference.slug) do
= button_tag :save, :value => :save
= button_tag :delete, :value => :delete
= button_tag :cancel, :value => :cancel
%h2=_@page_title
= columns(medium: 12) do
= form_tag register_path(@this_conference.slug) do
= button_tag :previous, :value => :registration
= button_tag :next, :value => :workshops
:javascript
function makeWorkshopsClickable() {
var workshops = document.querySelectorAll('ul.workshops h4');
for (var i = 0; i < workshops.length; i++) {
workshops[i].addEventListener('click', function(e) {
var workshop = e.target.parentElement;
workshop.className = 'view';
workshop.querySelector('form').onsubmit = function() {
workshop.removeAttribute('class');
return false;
}
}, false);
}
}
makeWorkshopsClickable();
- if @my_workshops.present?
%h3=_'articles.conference_registration.headings.Your_Workshops'
%p=_'articles.conference_registration.paragraphs.Your_Workshops', :p
= render 'workshops/workshop_previews', :workshops => @my_workshops
- else
%p=_'articles.conference_registration.paragraphs.Create_Workshop', :p
= link_to (_'articles.conference_registration.headings.Add_Workshop'), create_workshop_path(@this_conference.slug), class: :button
- if @workshops_in_need.present?
= columns(medium: 12) do
%h3=_'articles.conference_registration.headings.Workshops_Looking_For_Facilitators'
%p=_'articles.conference_registration.paragraphs.Workshops_Looking_For_Facilitators', :p
= render 'workshops/workshop_previews', :workshops => @workshops_in_need
- if @workshops.present?
= columns(medium: 12) do
%h3=_'articles.conference_registration.headings.All_Workshops'
-#%p=_'articles.conference_registration.paragraphs.All_Workshops', :p
= render 'workshops/workshop_previews', :workshops => @workshops

30
app/views/conferences/edit.html.haml

@ -13,18 +13,18 @@
= button_tag :save, :value => :save
:javascript
window.jQuery || document.write('<script src="https://code.jquery.com/jquery-2.1.3.min.js"><\/script>')
= javascript_include_tag "froala_editor.min.js"
= stylesheet_link_tag "froala_editor.min.css"
= stylesheet_link_tag "font-awesome.min.css"
:javascript
$(function() {
$('.text-editor textarea').editable({
language: '<% I18n.locale.to_s %>',
inlineMode: false,
blockTags: ["n", "p", "h2", "blockquote", "pre"],
buttons: ["formatBlock", "bold", "italic", "underline", "insertOrderedList", "insertUnorderedList", "sep", "createLink", "html", "undo", "redo"],
colors: ['#00ADEF', '#DD57EF', '#E6C74B', 'REMOVE']
});
});
/ :javascript
/ window.jQuery || document.write('<script src="https://code.jquery.com/jquery-2.1.3.min.js"><\/script>')
-#= javascript_include_tag "froala_editor.min.js"
-#= stylesheet_link_tag "froala_editor.min.css"
-#= stylesheet_link_tag "font-awesome.min.css"
/ :javascript
/ $(function() {
/ $('.text-editor textarea').editable({
/ language: '<% I18n.locale.to_s %>',
/ inlineMode: false,
/ blockTags: ["n", "p", "h2", "blockquote", "pre"],
/ buttons: ["formatBlock", "bold", "italic", "underline", "insertOrderedList", "insertUnorderedList", "sep", "createLink", "html", "undo", "redo"],
/ colors: ['#00ADEF', '#DD57EF', '#E6C74B', 'REMOVE']
/ });
/ });

18
app/views/conferences/register.html.haml

@ -1,4 +1,20 @@
= render :partial => 'page_header', :locals => {:page_key => 'Conference_Registration'}
- if (steps = current_registration_steps)
= row id: 'registration-steps', class: 'flow-steps' do
= columns do
%ul
- current_registration_steps.each do | step |
- text = _"articles.conference_registration.headings.#{step[:name].to_s}"
%li{class: [step[:enabled] ? :enabled : nil, @register_template == step[:name] ? :current : nil]}
- if step[:enabled]
.step= link_to text, register_step_path(@this_conference.slug, step[:name])
- else
.step= text
- if @warnings.present?
= row class: 'warnings', tag: :ul do
- @warnings.each do | warning |
= columns tag: :li do
= warning
%article
= row do
= render (@register_template || :register).to_s
= render @register_template.to_s

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

@ -3,19 +3,17 @@
%head
%meta{ charset: 'utf-8' }
%meta{ name: 'viewport', content: 'width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0' }
%title= (yield :title) + (content_for?(:title) ? (_!' | ') : '') + (_!'Bike!Bike!')
-#%title= (yield :title) + (content_for?(:title) ? (_!' | ') : '') + (_!'Bike!Bike!')
%title=_!('Bike!Bike!' + (content_for?(:title) ? " - #{yield :title}" : ''))
%meta{ name: 'description', content: (yield_or_default :description, I18n.t('page_descriptions.home')) }
= csrf_meta_tags
= inject_css!
- (@stylesheets || []).each do |css|
= inject_css! css
= stylesheet_link_tag 'i18n-debug' if request.params['i18nDebug']
= stylesheets
%link{ href: asset_path('favicon.ico'), rel: 'shortcut icon', type: 'image/x-icon' }
%link{ href: asset_path('favicon.ico'), rel: 'icon', type: 'image/x-icon' }
%link{ href: asset_path('apple-touch-icon.png'), rel: 'apple-touch-icon' }
%link{ href: asset_path('apple-touch-icon-72x72.png'), rel: 'apple-touch-icon', sizes: '72x72' }
%link{ href: asset_path('apple-touch-icon-114x114.png'), rel: 'apple-touch-icon', sizes: '114x114' }
%link{ href: asset_path('apple-touch-icon-144x144.png'), rel: 'apple-touch-icon', sizes: '144x144' }
-#%link{ href: asset_path('apple-touch-icon.png'), rel: 'apple-touch-icon' }
-#%link{ href: asset_path('apple-touch-icon-72x72.png'), rel: 'apple-touch-icon', sizes: '72x72' }
-#%link{ href: asset_path('apple-touch-icon-114x114.png'), rel: 'apple-touch-icon', sizes: '114x114' }
-#%link{ href: asset_path('apple-touch-icon-144x144.png'), rel: 'apple-touch-icon', sizes: '144x144' }
= yield :head
%body{ class: page_style }
@ -24,8 +22,8 @@
- if content_for?(:side_bar)
%nav#side-bar
= yield :side_bar
- flash.each do |key, msg|
= content_tag :div, msg, :id => key
-# - flash.each do |key, msg|
-# = content_tag :div, msg, :id => key
%header#banner=yield :banner
- if @submenu
=row do
@ -39,5 +37,7 @@
= yield
#footer
%footer= render 'shared/footer'
- if content_for?(:footer_scripts)
= yield :footer_scripts
= yield :footer_scripts if content_for?(:footer_scripts)
- add_inline_script :main
= inline_scripts

70
app/views/layouts/user_mailer.html.haml

@ -3,74 +3,10 @@
%head
%meta{:content => "text/html; charset=utf-8", "http-equiv" => "Content-Type"}/
%meta{:content => "width=device-width, initial-scale=1.0", :name => "viewport"}/
%title Your Message Subject or Title
:css
#outlook a {padding:0;}
body{width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0;}
.ExternalClass {width:100%;}
.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
#backgroundTable {margin:0; padding:0; width:100% !important; line-height: 100% !important;}
img {outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;}
a img {border:none;}
.image_fix {display:block;max-width:100%}
p {margin: 1em;color:#333 !important;}
h1, h2, h3, h4, h5, h6 {color:#333 !important;}
h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {color: #00ADEF !important;}
h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active { color: #D89E59 !important; }
h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited { color: #DD57EF !important; }
table td {border-collapse: collapse;}
table { border-collapse:collapse; mso-table-lspace:0pt; mso-table-rspace:0pt; max-width:512px;min-width:280px;}
a {color: #00ADEF;}
a:link { color: #00ADEF; }
a:visited { color: #00ADEF; }
a:hover { color: #02CA9E; }
@media only screen and (max-device-width: 480px) {
a[href^="tel"], a[href^="sms"] {
text-decoration: none;
color: #333;
pointer-events: none;
cursor: default;
}
.mobile_link a[href^="tel"], .mobile_link a[href^="sms"] {
text-decoration: default;
color: #00ADEF !important;
pointer-events: auto;
cursor: default;
}
}
h3 b {
padding: 10px 20px;
line-height: 50px;
}
h3 b a,
h3 b a:visited {
color: #FFF !important;
background-color: #02CA9E;
text-decoration: none !important;
border-radius: 4px;
padding: 10px 15px;
margin-left: 20px;
border-bottom: 3px solid #00AF88;
}
h3 b a:hover {
background-color: #00AF88;
}
@media only screen and (min-device-width: 768px) and (max-device-width: 1024px) {
a[href^="tel"], a[href^="sms"] {
text-decoration: none;
color: #00ADEF;
pointer-events: none;
cursor: default;
}
.mobile_link a[href^="tel"], .mobile_link a[href^="sms"] {
text-decoration: default;
color: #00ADEF !important;
pointer-events: auto;
cursor: default;
}
}
%title=@subject
%style{type: 'text/css'}=Rails.application.assets.find_asset("user-mailer.css").to_s
%body
%table#backgroundTable{:border => "0", :cellpadding => "0", :cellspacing => "0"}
%table{:border => "0", :cellpadding => "0", :cellspacing => "0", id: @wrapper_id.present? ? "bb_#{@wrapper_id.to_s}" : 'backgroundTable'}
%tr
%td
%table{:align => "center", :border => "0", :cellpadding => "0", :cellspacing => "0"}

3
app/views/shared/_donate_button.html.haml

@ -5,4 +5,5 @@
%input{type: 'hidden', name: 'item_name', value: 'Bike!Bike!'}
%input{type: 'hidden', name: 'no_note', value: '0'}
%input{type: 'hidden', name: 'currency_code', value: 'USD'}
%button{type: 'submit', value: 'PP-DonationsBF:paypal_logo.png:NonHostedGuest'}=_'donate.button_label','Donate'
%button{type: 'submit', value: 'PP-DonationsBF:paypal_logo.png:NonHostedGuest'}
%span.title=_'donate.button_label','Donate'

21
app/views/shared/_navbar.html.haml

@ -1,14 +1,23 @@
%nav
#main-nav
= row(class: 'inner-nav') do
- if logged_in?
= columns(medium: 12, class: 'user-nav') do
.user-nav-bar
=link_to (_!current_user.name), register_step_path(@conference.slug, :contact_info), class: 'my-account'
=link_to (_'forms.actions.generic.Log_out'), logout_path
= columns(medium: 4, class: 'inner-nav') do
= link_to '/', :class => 'logo' do
= link_to home_path, :class => 'logo' do
= svg_sprite('icons', "bb-icon-logo", "Logo")
= svg_sprite('icons', "bb-icon-logo-text", "Logo Text")
= off_screen _!'Bike Bike'
= columns(medium: 8, class: 'nav') do
= nav_link about_path, (_'page_titles.About'), 'about'
= nav_link policy_path, (_'page_titles.Policy'), 'policy'
- if @conference && @conference.registration_open
= nav_link register_path(@conference.slug), (_'page_titles.Register')
= nav_link :about
= nav_link :policy
- case (@conference ? @conference.registration_status.to_sym : :closed)
- when :pre
= nav_link register_path(@conference.slug), :pre_register, :register
- when :open
= nav_link register_path(@conference.slug), :register
- else
= render 'shared/donate_button', :email_address => (@conference.paypal_email_address || @conference.email_address)
= render 'shared/donate_button', :email_address => (@conference.paypal_email_address || @conference.email_address || @conference.organizations.first.email_address)

8
app/views/user_mailer/registration_confirmation.html.haml

@ -1,4 +1,8 @@
%p=_'email.general.paragraph.thank_you', "Thank you #{@user.firstname},", :vars => {:name => @user.firstname}
%p=_'email.registration.paragraph.confirmed', "You have successfully registered for #{@conference.title}. You can modify your registration details, pay, or add workshops at any time by restarting the registration process. If you have yet to pay or add your workshops and plan to do so, we ask that you take care of it as soon as possible to help us prepare in advance of your arrival.", :vars => {:conference_title => @conference.title}
%p=_'email.general.paragraph.see_you', "See you in #{@conference.location.city}!", :vars => {:conference_location => @conference.location.city}
- if @conference.registration_status.to_sym == :pre
%p=_'email.general.paragraph.pre_registered', "You have successfully pre-registered for #{@conference.title}. We will let you know when registration is fully open and if there is any important news about the conference that you should know about. We encourage you to create or volunteer to facilitate workshops soon. You can do that, or change your registration details at any time by clicking on the pre-register link again.", :vars => {:conference_title => @conference.title}
- else
%p=_'email.registration.paragraph.confirmed', :vars => {:conference_title => @conference.title}
%p=_'email.general.paragraph.see_you', :vars => {:conference_location => @conference.location.city}

7
app/views/user_mailer/registration_confirmation.text.haml

@ -1,5 +1,8 @@
=_'email.general.paragraph.thank_you', "Thank you #{@user.firstname},", :vars => {:name => @user.firstname}
=_'email.registration.paragraph.confirmed', "You have successfully registered for #{@conference.title}. You can modify your registration details, pay, or add workshops at any time by restarting the registration process. If you have yet to pay or add your workshops and plan to do so, we ask that you take care of it as soon as possible to help us prepare in advance of your arrival.", :vars => {:conference_title => @conference.title}
- if @conference.registration_status.to_sym == :pre
=_'email.general.paragraph.pre_registered', "You have successfully pre-registered for #{@conference.title}. We will let you know when registration is fully open and if there is any important news about the conference that you should know about. We encourage you to create or volunteer to facilitate workshops soon. You can do that, or change your registration details at any time by clicking on the pre-register link again.", :vars => {:conference_title => @conference.title}
- else
=_'email.registration.paragraph.confirmed', :vars => {:conference_title => @conference.title}
=_'email.general.paragraph.see_you', "See you in #{@conference.location.city}!", :vars => {:conference_location => @conference.location.city}
=_'email.general.paragraph.see_you', :vars => {:conference_location => @conference.location.city}

1
app/views/user_mailer/test_email.text.haml

@ -1 +0,0 @@
Hi, this is a test. Did you get it?

15
app/views/user_mailer/workshop_original_content_changed.html.haml

@ -0,0 +1,15 @@
%p=_('email.translations.paragraph.workshop_original_content_changed', "#{@translator.name} has modified the original content for #{@workshop.title}. Translations may need to be updated.", vars: {user_name: @translator.firstname, workshop_title: @workshop.title})
- @data.each do |field, values|
%h2=_"forms.labels.generic.#{field.to_s}"
%b=_'email.translations.headings.new_value'
%blockquote=_!values[:new].html_safe
%b=_'email.translations.headings.old_value'
%blockquote=_!values[:old].html_safe
%b=_'email.translations.headings.diff'
=_!(values[:diff][:html].html_safe)
%p
=_'email.workshop.paragraph.view_workshop',"You can view the workshop here: "
- workshop_link = view_workshop_url(@workshop.conference.slug, @workshop.id)
%a{href: workshop_link}=workshop_link

25
app/views/user_mailer/workshop_original_content_changed.text.haml

@ -0,0 +1,25 @@
=_('email.translations.paragraph.workshop_translated', "#{@translator.name} has modified the #{@locale_name} translation for #{@workshop.title}.", vars: {user_name: @translator.firstname, language: @locale_name, workshop_title: @workshop.title})
- @data.each do |field, values|
=(_!'** ') + (_"forms.labels.generic.#{field.to_s}")
=(_!' - ') + (_'email.translations.headings.new_value')
=(_!' ------------------------------ ')
=_!values[:new]
=(_!' ------------------------------ ')
=(_!' - ') + (_'email.translations.headings.old_value')
=(_!' ------------------------------ ')
=_!values[:old]
=(_!' ------------------------------ ')
=(_!' - ') + (_'email.translations.headings.diff')
=(_!' ------------------------------ ')
=(_!values[:diff][:text])
=(_!' ------------------------------ ')
=_'email.workshop.paragraph.view_workshop',"You can view the workshop here: "
= view_workshop_url(@workshop.conference.slug, @workshop.id)

15
app/views/user_mailer/workshop_translated.html.haml

@ -0,0 +1,15 @@
%p=_('email.translations.paragraph.workshop_translated', "#{@translator.name} has modified the #{@locale_name} translation for #{@workshop.title}.", vars: {user_name: @translator.firstname, language: @locale_name, workshop_title: @workshop.title})
- @data.each do |field, values|
%h2=_"forms.labels.generic.#{field.to_s}"
%b=_'email.translations.headings.new_value'
%blockquote=_!values[:new].html_safe
%b=_'email.translations.headings.old_value'
%blockquote=_!values[:old].html_safe
%b=_'email.translations.headings.diff'
=_!(values[:diff][:html].html_safe)
%p
=_'email.workshop.paragraph.view_workshop',"You can view the workshop here: "
- workshop_link = view_workshop_url(@workshop.conference.slug, @workshop.id)
%a{href: workshop_link}=workshop_link

25
app/views/user_mailer/workshop_translated.text.haml

@ -0,0 +1,25 @@
=_('email.translations.paragraph.workshop_translated', "#{@translator.firstname} has modified the #{@locale_name} translation for #{@workshop.title}.", vars: {user_name: @translator.firstname, language: @locale_name, workshop_title: @workshop.title})
- @data.each do |field, values|
=(_!'** ') + (_"forms.labels.generic.#{field.to_s}")
=(_!' - ') + (_'email.translations.headings.new_value')
=(_!' ------------------------------ ')
=_!values[:new]
=(_!' ------------------------------ ')
=(_!' - ') + (_'email.translations.headings.old_value')
=(_!' ------------------------------ ')
=_!values[:old]
=(_!' ------------------------------ ')
=(_!' - ') + (_'email.translations.headings.diff')
=(_!' ------------------------------ ')
=(_!values[:diff][:text])
=(_!' ------------------------------ ')
=_'email.workshop.paragraph.view_workshop',"You can view the workshop here: "
= view_workshop_url(@workshop.conference.slug, @workshop.id)

50
app/views/workshops/_show.html.haml

@ -6,14 +6,14 @@
=_'articles.workshops.info.you_are_interested_count', "You and #{workshop.interested_count - 1} others are interested in this workshop", :vars => {:count => (workshop.interested_count - 1)}
- else
=_'articles.workshops.info.interested_count', "#{workshop.interested_count} people are interested in this workshop", :vars => {:count => workshop.interested_count}
- if !preview && workshop.can_show_interest?(current_user)
- if preview.blank? && workshop.can_show_interest?(current_user)
= form_tag toggle_workshop_interest_path(workshop.conference.slug, workshop.id) do
= button_tag (workshop.interested?(current_user) ? :remove_interest : :show_interest), :value => :toggle_interest, :class => (workshop.interested?(current_user) ? 'delete' : 'add')
=markdown _!(workshop.info) || ''
- if !preview && logged_in? && current_user.can_translate?
= button_tag (workshop.interested?(current_user) ? :remove_interest : :show_interest), :value => :toggle_interest, :class => (workshop.interested?(current_user) ? :delete : :add)
=_!(workshop.info).html_safe if workshop.info.present?
- if preview.blank? && translations_available_for_editing
.actions
- I18n.backend.enabled_locales.each do |locale|
= (link_to (_'actions.workshops.Translate', "Translate into #{language_name(locale)}", :vars => {:language => language_name(locale)}), edit_workshop_url(workshop.conference.slug, workshop.id, url_params(locale)), :class => 'button translate') if workshop.can_translate?(current_user, locale)
- translations_available_for_editing.each do |locale|
= link_to (_'actions.workshops.Translate', "Translate into #{language_name(locale)}", :vars => {:language => language_name(locale)}), translate_workshop_url(workshop.conference.slug, workshop.id, locale), :class => [:button, :translate]
= columns(medium: 6) do
%h3=_'articles.workshops.headings.facilitators'
.facilitators
@ -21,38 +21,40 @@
- u = User.find(f.user_id)
- if logged_in? && (workshop.public_facilitator?(u) || f.user_id == current_user.id || workshop.active_facilitator?(current_user))
.facilitator
.name=_!(u.firstname || u.username || u.email)
.name=_!u.name
.role
=_"roles.workshops.facilitator.#{workshop.role(u).to_s}"
- if !preview && f.role.to_sym == :requested && workshop.active_facilitator?(current_user)
=(link_to (_'actions.workshops.Approve'), approve_facilitate_workshop_request_path(workshop.conference.slug, workshop.id, f.user_id, 'approve'), :class => 'button modify')
=(link_to (_'actions.workshops.Deny'), approve_facilitate_workshop_request_path(workshop.conference.slug, workshop.id, f.user_id, 'deny'), :class => 'button delete')
- elsif !preview && (f.user_id == current_user.id && f.role.to_sym != :creator) || (!workshop.conference.registered?(u) && workshop.active_facilitator?(current_user))
=(link_to (_'actions.workshops.Remove'), approve_facilitate_workshop_request_path(workshop.conference.slug, workshop.id, f.user_id, 'remove'), :class => 'button delete')
- if !preview
- if preview.blank? && f.role.to_sym == :requested && workshop.active_facilitator?(current_user)
=(link_to (_'actions.workshops.Approve'), approve_facilitate_workshop_request_path(workshop.conference.slug, workshop.id, f.user_id, 'approve'), :class => [:button, :modify])
=(link_to (_'actions.workshops.Deny'), approve_facilitate_workshop_request_path(workshop.conference.slug, workshop.id, f.user_id, 'deny'), :class => [:button, :delete])
- elsif preview.blank? && (f.user_id == current_user.id && f.role.to_sym != :creator) || (!workshop.conference.registered?(u) && workshop.active_facilitator?(current_user))
=(link_to (_'actions.workshops.Remove'), approve_facilitate_workshop_request_path(workshop.conference.slug, workshop.id, f.user_id, 'remove'), :class => [:button, :delete])
- unless preview.present?
.actions
=(link_to (_'actions.workshops.Facilitate'), facilitate_workshop_path(workshop.conference.slug, workshop.id), :class => 'button modify') if !workshop.facilitator?(current_user)
=(link_to (_'actions.workshops.Facilitate'), facilitate_workshop_path(workshop.conference.slug, workshop.id), :class => [:button, workshop.needs_facilitators ? :accented : :subdued]) unless workshop.facilitator?(current_user)
- if workshop.active_facilitator?(current_user)
= form_tag workshop_add_facilitator_path(workshop.conference.slug, workshop.id), :class => 'add-facilitator' do
%h4='Add a facilitator'
%h4=_'articles.workshops.headings.add_facilitator','Add a facilitator'
= form_tag workshop_add_facilitator_path(workshop.conference.slug, workshop.id), :class => 'add-facilitator flex-form' do
.email-field.input-field
= email_field_tag :email, nil, required: true
= label_tag :email
= button_tag :add
- if workshop.languages
- languages = JSON.parse(workshop.languages || '[]')
- if languages.present?
= columns(medium: 6) do
%h3=_'articles.workshops.headings.languages','Workshop Language'
%p= _!((JSON.parse(workshop.languages || '[]').map { |x| _"languages.#{x}" }).join(', ').to_s.html_safe)
- if workshop.theme
%p= _!((languages.map { |x| _"languages.#{x}" }).join(', ').to_s.html_safe)
- if workshop.theme.present?
= columns(medium: 6) do
%h3=_'articles.workshops.headings.theme','Theme'
%p= [:race_gender, :mechanics, :funding, :organization, :community].include?((workshop.theme || '').to_sym) ? (_"workshop.options.theme.#{workshop.theme}") : workshop.theme
%p= Workshop.all_themes.include?((workshop.theme || '').to_sym) ? (_"workshop.options.theme.#{workshop.theme}") : workshop.theme
- if workshop.active_facilitator?(current_user) || workshop.conference.host?(current_user)
- if workshop.needs
- needs = JSON.parse(workshop.needs || '[]')
- if needs.present?
= columns(medium: 6) do
%h3=_'articles.workshops.headings.needs','What do you need?'
%p= _!((JSON.parse(workshop.needs || '[]').map { |x| _"workshop.options.needs.#{x}" }).join(', ').to_s.html_safe)
- if workshop.notes
%p= _!((needs.map { |x| _"workshop.options.needs.#{x}" }).join(', ').to_s.html_safe)
- if workshop.notes.present?
= columns(medium: 12) do
%h3=_'articles.workshops.headings.notes','Notes'
=markdown _!(workshop.notes)
=_!(workshop.notes).html_safe

18
app/views/workshops/_workshop_previews.html.haml

@ -1,8 +1,12 @@
- # encoding: utf-8
%ul.workshop-previews
%ul.workshop-list
- workshops.sort_by{ |w| w.title.downcase }.each do |w|
- if w.title
%li
=link_to view_workshop_path(@this_conference.slug, w.id) do
%h4=_!(w.title)
%p=_!(w.info || '').gsub(/\n\#+(.*?)\r/, "\n<strong>\\1</strong>\r").gsub(/[\t ]*\r\n[\t ]*/, '<br>').html_safe
- is_interested = w.interested?(current_user)
%li{class: [is_interested ? :interested : nil]}
= link_to view_workshop_path(w.conference.slug, w.id) do
%h4.title=w.title
.workshop-interest
- if is_interested
=_'articles.workshops.info.you_are_interested_count', "You and #{w.interested_count - 1} others are interested in this workshop", :vars => {:count => (w.interested_count - 1)}
- elsif w.interested_count > 0
=_'articles.workshops.info.interested_count', "#{w.interested_count} people are interested in this workshop", :vars => {:count => w.interested_count}
.workshop-description=w.info.html_safe

6
app/views/workshops/facilitate_request_sent.html.haml

@ -1,9 +1,11 @@
= render 'conferences/page_header', :page_key => 'Facilitate_Workshop'
= render 'conferences/page_header', :page_key => 'Conference_Registration'
%article
= row do
%h2=_'page_titles.conferences.Facilitate_Workshop'
= columns(medium: 12) do
=m('articles.workshops.paragraphs.facilitate_request_sent','Your request has been sent. You will receive an email once your request is approved or denied or if the current facilitators have any questions.')
= columns(medium: 12) do
.actions
= link_to (_'actions.workshops.View'), view_workshop_url(@this_conference.slug, @workshop.id), :class => 'button'
= link_to (_'actions.workshops.View_All'), workshops_url(@this_conference.slug), :class => 'button'
= link_to (_'actions.workshops.View_All'), register_step_path(@this_conference.slug, :wokshops), :class => 'button'

11
app/views/workshops/index.html.haml

@ -1,13 +1,18 @@
= render 'conferences/page_header', :page_key => 'Workshops'
%article
= row do
= columns(medium: 12) do
%h2=_'articles.workshops.headings.Workshops'
= columns(medium: 6) do
= row do
= columns(medium: 12) do
%p=_'articles.workshops.paragraphs.Workshops', :p
= columns(medium: 6) do
= row do
= columns(medium: 12) do
%h3=_'articles.workshops.headings.Your_Workshops'
= render 'workshops/workshop_previews', :workshops => @my_workshops
= link_to (_'actions.workshops.create','New Workshop'), create_workshop_path(@this_conference.slug), class: [:button, :modify] if @conference.registration_open
= row do
= render 'workshops/workshop_previews', :workshops => @workshops
= columns(medium: 12) do
%h3=_'articles.workshops.headings.All_Workshops'
= render 'workshops/workshop_previews', :workshops => @workshops

94
app/views/workshops/new.html.haml

@ -1,69 +1,91 @@
= render 'conferences/page_header', :page_key => (@workshop ? 'Edit_Workshop' : 'Create_Workshop')
- add_stylesheet :editor
- add_inline_script :pen
- add_inline_script :markdown
- add_inline_script :editor
-#= render 'conferences/page_header', :page_key => (@workshop.id.present? ? 'Edit_Workshop' : 'Create_Workshop')
= render 'conferences/page_header', :page_key => 'Conference_Registration'
%article
= row do
= form_tag save_workshop_path(@this_conference.slug), class: 'composition' do
= (hidden_field_tag :workshop_id, @workshop.id) if @workshop
= columns(medium: 12) do
=m('articles.workshops.paragraphs.new_workshop','Please accurately describe your workshop in detail. This will help hosts decide if they wish to add it to the schedule and when it should best be scheduled. Enter normal text but if you want to get fancy you can use [Markdown](http://daringfireball.net/projects/markdown/basics).')
- if @workshop.id.present?
= hidden_field_tag :workshop_id, @workshop.id
- if @is_translating
%h2=_(@page_title, vars: @page_title_vars)
= hidden_field_tag :translation, @translation
- else
%h2=_@page_title, :t
- else
%h2=_@page_title, :t
.text-field.input-field.big
= label_tag :title
= text_field_tag :title, @title, :required => true
- if @workshop && I18n.locale.to_s != @workshop.locale.to_s
= text_field_tag :title, @title, required: true, lang: @translation
- if @is_translating
.original-text
%h4=_'translate.content.Translation_of'
.value=@workshop.title!
.text-area-field.input-field
= label_tag :info
= text_area_tag :info, @info, :required => true
- if @workshop && I18n.locale.to_s != @workshop.locale.to_s
.input-field-help=_'articles.workshops.paragraphs.info', :s, 2
.textarea#info{lang: @translation, data: {name: :info}}=(@info || '').html_safe
- if @is_translating
.original-text
%h4=_'translate.content.Translation_of'
.value=markdown @workshop.info!
- if !@workshop || @can_edit
.value=(@workshop.info! || '').html_safe
- if !@is_translating && (@workshop.id.blank? || @can_edit)
= columns(medium: 6) do
%h3=_'articles.workshops.headings.languages','Workshop Language'
- [:en, :es, :fr].each do |language|
.single-check-box-field.input-field
.input-field-help=_'articles.workshops.paragraphs.languages', :s, 2
.check-box-field.vertical.input-field
- [:en, :es, :fr].each do |language|
= check_box_tag "languages[#{language}]", 1, @languages && @languages.include?(language)
= label_tag "languages_#{language}" do
= check_box_tag "languages[#{language}]", 1, @languages && @languages.include?(language)
= _"languages.#{language}"
%h3=_'articles.workshops.headings.theme','Theme'
.input-field-help=_'articles.workshops.paragraphs.theme', :p
.input-field-help=_'articles.workshops.paragraphs.theme', :s, 2
- theme_found = false
- [:race_gender, :mechanics, :funding, :organization, :community].each do |theme|
.single-radio-button-field.input-field
.check-box-field.vertical.input-field
- Workshop.all_themes.each do |theme|
- is_selected = (@workshop.theme.to_s == theme.to_s)
- theme_found ||= is_selected
= radio_button_tag :theme, theme, is_selected
= label_tag "theme_#{theme}" do
- is_selected = (@theme == theme)
- theme_found ||= is_selected
= radio_button_tag :theme, theme, is_selected
= _"workshop.options.theme.#{theme}"
.single-radio-button-field.other-field.input-field
- is_other = (@workshop.theme.present? && !theme_found)
= radio_button_tag :theme, :other, is_other
= label_tag "theme_other" do
= radio_button_tag :theme, :other, (@theme && !theme_found)
%div
= _"workshop.options.theme.other"
= text_field_tag :other_theme, (@theme && !theme_found ? @theme : nil)
.other
= text_field_tag :other_theme, (is_other ? @workshop.theme : nil), placeholder: (_"workshop.options.theme.other"), required: is_other
= columns(medium: 6) do
%h3=_'articles.workshops.headings.needs','What do you need?'
- [:sound, :projector, :tools].each do |need|
.single-check-box-field.input-field
.input-field-help=_'articles.workshops.paragraphs.needs', :s, 2
.check-box-field.vertical.input-field
- [:sound, :projector, :tools].each do |need|
= check_box_tag "needs[#{need.to_s}]", 1, JSON.parse(@workshop.needs || '[]').include?(need.to_s)
= label_tag "needs_#{need}" do
= check_box_tag "needs[#{need}]", 1, @needs.include?(need)
= _"workshop.options.needs.#{need}"
= _"workshop.options.needs.#{need.to_s}"
%h3=_'articles.workshops.headings.space','Type of space'
.input-field-help=_'articles.workshops.paragraphs.space', :p
- [:meeting_room, :workshop, :outdoor_meeting].each do |space|
.single-radio-button-field.input-field
.input-field-help=_'articles.workshops.paragraphs.space', :s, 2
.check-box-field.vertical.input-field
- [:meeting_room, :workshop, :outdoor_meeting].each do |space|
= radio_button_tag :space, space, @workshop.space == space
= label_tag "space_#{space}" do
= radio_button_tag :space, space, @space == space
= _"workshop.options.space.#{space}"
%h3=_'articles.workshops.headings.needs_facilitators','Looking for help?'
.input-field-help=_'articles.workshops.paragraphs.needs_facilitators', :s, 2
.check-box-field.vertical.input-field
= check_box_tag :needs_facilitators, 1, @workshop.needs_facilitators
= label_tag :needs_facilitators do
= _'workshop.options.needs_facilitators', 'Needs Additional Facilitators'
= columns(medium: 12) do
%h3=_'articles.workshops.headings.notes','Notes for Conference Organizers and Workshop Facilitators'
%p=_'articles.workshops.paragraphs.notes','Notes are only viewable by conference hosts and workshop facilitators'
.text-area-field.input-field
= label_tag :notes
= text_area_tag :notes, @notes
.warning-info=_'articles.workshops.paragraphs.notes','Notes are only viewable by conference hosts and workshop facilitators'
.textarea#notes{data: {name: :notes}}=(@workshop.notes || '').html_safe
= columns(medium: 12) do
.actions.right
= button_tag :save, :value => :save
.actions.next-prev
= button_tag :save, value: :save
= button_tag :cancel, value: :cancel, class: :subdued, formnovalidate: true

18
app/views/workshops/show.html.haml

@ -1,8 +1,20 @@
= render 'conferences/page_header', :page_key => 'View_Workshop'
= render 'conferences/page_header', :page_key => 'Conference_Registration'
= row id: 'registration-steps', class: 'flow-steps' do
= columns do
%ul
- current_registration_steps.each do | step |
- text = _"articles.conference_registration.headings.#{step[:name].to_s}"
%li{class: [step[:enabled] ? :enabled : nil, :workshops == step[:name] ? :current : nil]}
- if step[:enabled]
.step= link_to text, register_step_path(@this_conference.slug, step[:name])
- else
.step= text
%article
= render 'workshops/show', :workshop => @workshop, :preview => false
= render 'workshops/show', :workshop => @workshop, :translations_available_for_editing => @translations_available_for_editing, :preview => false
= row do
= columns(medium: 12) do
.actions.right
= (link_to (_'actions.workshops.Edit'), edit_workshop_path(@this_conference.slug, @workshop.id), :class => 'button modify') if @workshop.can_edit?(current_user)
= (link_to (_'actions.workshops.Edit'), edit_workshop_path(@this_conference.slug, @workshop.id), :class => [:button, :modify]) if @workshop.can_edit?(current_user)
= (link_to (_'actions.workshops.Delete'), delete_workshop_path(@this_conference.slug, @workshop.id), :class => 'button delete') if @workshop.can_delete?(current_user)

49
config/deploy.rb

@ -0,0 +1,49 @@
# config valid only for current version of Capistrano
lock '3.5.0'
set :application, 'my_app_name'
set :repo_url, 'git@example.com:me/my_repo.git'
# Default branch is :master
# ask :branch, `git rev-parse --abbrev-ref HEAD`.chomp
# Default deploy_to directory is /var/www/my_app_name
# set :deploy_to, '/var/www/my_app_name'
# Default value for :scm is :git
# set :scm, :git
# Default value for :format is :airbrussh.
# set :format, :airbrussh
# You can configure the Airbrussh format using :format_options.
# These are the defaults.
# set :format_options, command_output: true, log_file: 'log/capistrano.log', color: :auto, truncate: :auto
# Default value for :pty is false
# set :pty, true
# Default value for :linked_files is []
# set :linked_files, fetch(:linked_files, []).push('config/database.yml', 'config/secrets.yml')
# Default value for linked_dirs is []
# set :linked_dirs, fetch(:linked_dirs, []).push('log', 'tmp/pids', 'tmp/cache', 'tmp/sockets', 'public/system')
# Default value for default_env is {}
# set :default_env, { path: "/opt/ruby/bin:$PATH" }
# Default value for keep_releases is 5
# set :keep_releases, 5
namespace :deploy do
after :restart, :clear_cache do
on roles(:web), in: :groups, limit: 3, wait: 10 do
# Here we can do anything such as:
# within release_path do
# execute :rake, 'cache:clear'
# end
end
end
end

61
config/deploy/production.rb

@ -0,0 +1,61 @@
# server-based syntax
# ======================
# Defines a single server with a list of roles and multiple properties.
# You can define all roles on a single server, or split them:
# server 'example.com', user: 'deploy', roles: %w{app db web}, my_property: :my_value
# server 'example.com', user: 'deploy', roles: %w{app web}, other_property: :other_value
# server 'db.example.com', user: 'deploy', roles: %w{db}
# role-based syntax
# ==================
# Defines a role with one or multiple servers. The primary server in each
# group is considered to be the first unless any hosts have the primary
# property set. Specify the username and a domain or IP for the server.
# Don't use `:all`, it's a meta role.
# role :app, %w{deploy@example.com}, my_property: :my_value
# role :web, %w{user1@primary.com user2@additional.com}, other_property: :other_value
# role :db, %w{deploy@example.com}
# Configuration
# =============
# You can set any configuration variable like in config/deploy.rb
# These variables are then only loaded and set in this stage.
# For available Capistrano configuration variables see the documentation page.
# http://capistranorb.com/documentation/getting-started/configuration/
# Feel free to add new variables to customise your setup.
# Custom SSH Options
# ==================
# You may pass any option but keep in mind that net/ssh understands a
# limited set of options, consult the Net::SSH documentation.
# http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start
#
# Global options
# --------------
# set :ssh_options, {
# keys: %w(/home/rlisowski/.ssh/id_rsa),
# forward_agent: false,
# auth_methods: %w(password)
# }
#
# The server-based syntax can be used to override options:
# ------------------------------------
# server 'example.com',
# user: 'user_name',
# roles: %w{web app},
# ssh_options: {
# user: 'user_name', # overrides user setting above
# keys: %w(/home/user_name/.ssh/id_rsa),
# forward_agent: false,
# auth_methods: %w(publickey password)
# # password: 'please use keys'
# }

61
config/deploy/staging.rb

@ -0,0 +1,61 @@
# server-based syntax
# ======================
# Defines a single server with a list of roles and multiple properties.
# You can define all roles on a single server, or split them:
# server 'example.com', user: 'deploy', roles: %w{app db web}, my_property: :my_value
# server 'example.com', user: 'deploy', roles: %w{app web}, other_property: :other_value
# server 'db.example.com', user: 'deploy', roles: %w{db}
# role-based syntax
# ==================
# Defines a role with one or multiple servers. The primary server in each
# group is considered to be the first unless any hosts have the primary
# property set. Specify the username and a domain or IP for the server.
# Don't use `:all`, it's a meta role.
# role :app, %w{deploy@example.com}, my_property: :my_value
# role :web, %w{user1@primary.com user2@additional.com}, other_property: :other_value
# role :db, %w{deploy@example.com}
# Configuration
# =============
# You can set any configuration variable like in config/deploy.rb
# These variables are then only loaded and set in this stage.
# For available Capistrano configuration variables see the documentation page.
# http://capistranorb.com/documentation/getting-started/configuration/
# Feel free to add new variables to customise your setup.
# Custom SSH Options
# ==================
# You may pass any option but keep in mind that net/ssh understands a
# limited set of options, consult the Net::SSH documentation.
# http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start
#
# Global options
# --------------
# set :ssh_options, {
# keys: %w(/home/rlisowski/.ssh/id_rsa),
# forward_agent: false,
# auth_methods: %w(password)
# }
#
# The server-based syntax can be used to override options:
# ------------------------------------
# server 'example.com',
# user: 'user_name',
# roles: %w{web app},
# ssh_options: {
# user: 'user_name', # overrides user setting above
# keys: %w(/home/user_name/.ssh/id_rsa),
# forward_agent: false,
# auth_methods: %w(publickey password)
# # password: 'please use keys'
# }

12
config/environments/development.rb

@ -30,10 +30,11 @@ BikeBike::Application.configure do
# This option may cause significant delays in view rendering with a large
# number of complex assets.
config.assets.debug = false
config.assets.digest = false
config.assets.compile = true
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
:enable_starttls_auto => true,
:address => 'mail.bikebike.org',
:domain => 'preview.bikebike.org',
:port => 587,
@ -45,15 +46,10 @@ BikeBike::Application.configure do
}
config.action_mailer.raise_delivery_errors = true
config.action_mailer.perform_deliveries = true
#config.force_ssl = true
#config.action_mailer.default_charset = 'utf-8'
#Carmen.i18n_backend.locale_paths = ''
#puts "CARMEN\t" + Carmen.i18n_backend.locale_paths
#PerfTools::CpuProfiler.start('/tmp/dev_prof')
config.serve_static_files = true
#config.assets.precompile = false
# config.action_controller.perform_caching = true
Paypal.sandbox!
#Paypal.sandbox = false
end

1
config/environments/preview.rb

@ -87,7 +87,6 @@ BikeBike::Application.configure do
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
:enable_starttls_auto => true,
:address => 'mail.bikebike.org',
:domain => 'preview.bikebike.org',
:port => 587,

1
config/environments/production.rb

@ -88,7 +88,6 @@ BikeBike::Application.configure do
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
:enable_starttls_auto => true,
:address => 'mail.bikebike.org',
:domain => 'preview.bikebike.org',
:port => 587,

11
config/initializers/assets.rb

@ -0,0 +1,11 @@
# Be sure to restart your server when you modify this file.
# Version of your assets, change this if you want to expire all your assets.
Rails.application.config.assets.version = '1.0'
# Add additional assets to the asset load path
# Rails.application.config.assets.paths << Emoji.images_path
# Precompile additional assets.
# application.js, application.css, and all non-JS/CSS in app/assets folder are already added.
Rails.application.config.assets.precompile += %w( map.js pen.js editor.js markdown.js main.js favicon.ico )

26
config/initializers/sorcery.rb

@ -3,7 +3,19 @@
# Available submodules are: :user_activation, :http_basic_auth, :remember_me,
# :reset_password, :session_timeout, :brute_force_protection, :activity_logging, :external
# Rails.application.config.sorcery.submodules = [:remember_me, :reset_password, :user_activation, :brute_force_protection, :external]
Rails.application.config.sorcery.submodules = [:remember_me, :reset_password, :brute_force_protection, :external]
require 'sorcery'
require 'openssl'
OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE if Rails.env.development?
ENV['bb_host'] = {
test: 'localhost:3000',
development: 'development.bikebike.org:3000',
preview: 'preview.bikebike.org',
production: 'bikebike.org',
}[Rails.env]
Rails.application.config.sorcery.submodules = [:external]
# Here you can configure each submodule's features.
Rails.application.config.sorcery.configure do |config|
@ -112,11 +124,13 @@ Rails.application.config.sorcery.configure do |config|
config.facebook.key = "257350517701074"
config.facebook.secret = "2f6ab1fd7eeff9aee73140991fc68314"
config.facebook.callback_url = "http://dev.bikebike.org/oauth/callback?provider=facebook"
config.facebook.user_info_mapping = {:email => "email", :username => "username", :avatar => "picture/data/url"}
config.facebook.callback_url = "#{ENV['bb_host']}/oauth/callback?provider=facebook"
config.facebook.user_info_mapping = {:email => "email", :username => "username"}
config.facebook.scope = "email"
config.facebook.display = "popup"
config.facebook.api_version = "v2.5"
config.facebook.user_info_path = "me?fields=email,name"
# config.github.key = ""
# config.github.secret = ""
# config.github.callback_url = "http://0.0.0.0:3000/oauth/callback?provider=github"
@ -306,7 +320,7 @@ Rails.application.config.sorcery.configure do |config|
# mailer class. Needed.
# Default: `nil`
#
user.reset_password_mailer = UserMailer
# user.reset_password_mailer = UserMailer
# reset password email method on your mailer class.
@ -378,7 +392,7 @@ Rails.application.config.sorcery.configure do |config|
# Unlock token mailer class
# Default: `nil`
#
user.unlock_token_mailer = UserMailer
# user.unlock_token_mailer = UserMailer
# -- activity logging --
# Last login attribute name.

45
config/locales/en.yml

@ -5212,6 +5212,7 @@ en:
un_translated: Untranslated content
outdated_translation: Outdated content
not_translation: Not a translation
Translation_of: Translation of
volunteer:
become_a_volunteer: Become a volunteer translator
volunteer: Can you help us translate it?
@ -5294,6 +5295,7 @@ en:
conference_registration:
headings:
Policy_Agreement: Safer Space Agreement
policy: Policy
arrival_and_departure: For what days will you need housing? (If you don't
need housing, just tell us how long you plan to hang out with Bike!Bike!)
other: Is there anything else you'd like to tell us?
@ -5316,6 +5318,7 @@ en:
Allergies: Allergies
Stats: Stats
Workshops: Workshops
workshops: Workshops
Your_Workshops: Your Workshops
payment_confirm: Please confirm your payment
Preview: Preview
@ -5323,8 +5326,13 @@ en:
date: Date
email: Email
fees_paid: Fees Paid
Contact_Info: Contact Info
contact_info: Contact Info
Add_Workshop: Propose a Workshop
Workshops_Looking_For_Facilitators: Workshops Looking for Facilitators
All_Workshops: All Workshops
paragraphs:
Policy_Agreement: Safer Space Agreement
Policy_Agreement: Ensuring that all attendees feel welcome, safe, and respected at all times is especially important to us all. Please ensure that you have fully read and understand our safer spaces policy below, if you have any questions or concerns you can reach out to the organizers at any time.
Confirm_Agreement: By clicking the "I Agree" button, you are pledging to do
your best to uphold Bike!Bike!'s safer space agreement. Thank you!
Registration_Info: Please fill in this registration form to help us prepare
@ -5361,6 +5369,10 @@ en:
participants_emailed: Your email has been sent to all participants of %%{conference_title}.
workshops: You can now take a look at proposed workshops and even propose
one yourself if you like.
Contact_Info: Please let us know a little bit about you.
Create_Workshop: At Bike!Bike! anyone can lead a workshop or just propose and idea that someone else can volunteer to lead. If, where, and when the workshop will be scheduled will ultimately be decided by the conference organizers.
Workshops_Looking_For_Facilitators: Would you like to lend a hand facilitating a workshop proposed by someone else? Below is a list of workshops that are actively looking for volunteers, if you are interested in helping out you can make a facilitation request.
Your_Workshops: The following is a list of all the workshops that you have created or have requested to facilitate.
questions:
bike:
large: Large
@ -5462,13 +5474,13 @@ en:
Workshops: Workshops
Your_Workshops: Your Workshops
facilitate: Request to Facilitate ‘%{workshop_title}’
add_facilitator: Add a facilitator
needs_facilitators: Looking for help?
paragraphs:
Proposed_Workshops: Would you like to facilitate your own workshop? Simply
register and visit the workshops page. If you have already registered you
can access the page by restarting the registration process.
info: Please accurately describe your workshop in detail. This will help hosts
decide if they wish to add it to the schedule and when it should best be
scheduled. Enter normal text but if you want to get fancy you can use [Markdown](http://daringfireball.net/projects/markdown/basics)
info: Describe your workshop in detail; highlighting text will reveal formatting controls.
space: What kind of space do you need for your workshop?
theme: Which of the themes below best match your workshop? This will help
hosts to avoid scheduling conflicts. Select other if none of the options
@ -5495,6 +5507,9 @@ en:
facilitate_request_sent: Your request has been sent. You will receive an email
once your request is approved or denied or if the current facilitators have
any questions.
languages: What language will the workshop be presented in? Will there be translations or translators available?
needs: If you need any of the following the conference organizers will do their best to make sure you have them. If you have any other requests, you can include them in the notes section.
needs_facilitators: Are you actively looking for help running this workshop? Anyone who is registered for the conference can request to facilitate at any time but checking this box will improve your chances of finding collaborators.
info:
interested_count:
one: One person is interested in this workshop
@ -5522,7 +5537,7 @@ en:
name: Name
subject: Subject
title: Title
info: Info
info: Description
content: Content
notes: Notes
message: 'Your Message:'
@ -5546,6 +5561,8 @@ en:
remove_interest: "-1"
show_interest: "+1"
add: "+"
previous: Previous
next: Next
page_titles:
'403':
Please_Confirm_Email: Please confirm your email
@ -5562,8 +5579,9 @@ en:
Registration_Stats: Registration Stats
View_Workshop: View Workshop
Workshops: Workshops
Delete_Workshop: Create Workshop
Delete_Workshop: Delete Workshop
Edit_Workshop: Edit Workshop
Translate_Workshop: Edit %{language} Workshop Translation
Edit: Edit Conference
Facilitate_Workshop: Workshop Facilitation Request
policy:
@ -5572,6 +5590,8 @@ en:
Page_Not_Found: Page Not Found
Policy: 'Policy'
About: 'About'
Register: 'Register'
Pre_Register: 'Pre-Register'
links:
footer:
help_text:
@ -5640,6 +5660,7 @@ en:
organization: Organizational Concerns
other: Other
race_gender: Race, Gender, or Class Politics
needs_facilitators: Needs Additional Facilitators
email:
confirmation:
paragraph:
@ -5650,10 +5671,13 @@ en:
subject:
confirm_email: Confirmation Email
registration_confirmed: Thank you for registering for %{conference_title}
pre_registration_confirmed: Thank you for pre-registering for %{conference_title}
workshop_facilitator_request: Request to facilitate %{workshop_title} from %{requester_name}
workshop_request_approved: You have been added as a facilitator of %{workshop_title}
workshop_request_denied: Your request to facilitate %{workshop_title} has been
denied
workshop_translated: The %{language} translation for %{workshop_title} has been modified
workshop_original_content_changed: Original content for %{workshop_title} has been modified
general:
paragraph:
see_you: See you in %{conference_location}!
@ -5665,7 +5689,14 @@ en:
the registration process. If you have yet to pay or add your workshops and
plan to do so, we ask that you take care of it as soon as possible to help
us prepare in advance of your arrival.
workshop:
translations:
headings:
new_value: 'New Value: '
old_value: 'Old Value: '
diff: 'Difference: '
paragraph:
workshop_translated: '%{user_name} has modified the %{language} translation for %{workshop_title}.'
workshop_original_content_changed: '%{user_name} has modified the original content for %{workshop_title}. Translations may need to be updated.'
paragraph:
request_approved: You have been added as a facilitator of %{workshop_title}.
request_denied: Your request to become a facilitator of %{workshop_title}

7
config/locales/es.yml

@ -702,7 +702,7 @@ es:
actions:
Register: Registrate
donate:
button_label: donar
button_label: Donar
articles:
conference_registration:
headings:
@ -744,7 +744,6 @@ es:
en: bikebike@gdlenbici.org
Workshops: 'Título de la actividad: Que necesitas? Tipo de espacio? Tema?
Quién es responsable? Descripción? Tiempo?'
Policy_Agreement: Acuerdo de Espacios Más Seguros
Confirm_Agreement: Al hacer click en el botón "Acepto", estás comprometiéndote
a cumplir el Acuerdo de Espacios Más Seguros de Bike!Bike!. ¡Gracias!
Email_Participants: Esta página se usa para contactar a todxs lxs participantes.
@ -851,8 +850,6 @@ es:
Proposed_Workshops: ¿Te gustaría facilitar tu propio taller? Simplemente regístrate
y visita la página de Talleres. Si ya te registraste puedes acceder a la
página reiniciando el proceso de registro.
info: 'Título de la actividad: Que necesitas? Tipo de espacio? Tema? Quién
es responsable? Descripción? Tiempo?'
Workshops: ¿Tienes alguna habilidad emocionante que quieres compartir con
nosotros? ¿Quieres charlar sobre crear espacios comunitarios seguros? ¿Quieres
asegurarte de que hagamos un buen paseo el fin de semana? ¡Propón un taller!
@ -1012,6 +1009,8 @@ es:
Page_Not_Found: Crear un taller
Policy: 'Política'
About: 'Acerca de'
Register: 'Registrate'
Pre_Register: 'Pre-Registrate'
actions:
workshops:
create: Crear

8
config/routes.rb

@ -6,6 +6,7 @@ BikeBike::Application.routes.draw do
post '/conferences/:slug/save' => 'conferences#save', :as => :save_conference
match '/conferences/:slug/register' => 'conferences#register', :as => :register, via: [:get, :post]
get '/conferences/:slug/register/:step' => 'conferences#register', :as => :register_step
match '/conferences/:slug/broadcast' => 'conferences#broadcast', :as => :broadcast, via: [:get, :post]
get '/conferences/:slug/stats' => 'conferences#stats', :as => :stats
get '/conferences/:slug/register/:button/:confirmation_token' => 'conferences#register', :as => :register_paypal_confirm
@ -24,12 +25,13 @@ BikeBike::Application.routes.draw do
get '/conferences/:slug/schedule/event/:id' => 'conferences#view_event', :as => :view_event
get '/conferences/:slug/schedule/event/:id/edit' => 'conferences#edit_event', :as => :edit_event
get '/conferences/:slug/workshops' => 'conferences#workshops', :as => :workshops
# get '/conferences/:slug/workshops' => 'conferences#workshops', :as => :workshops
match '/conferences/:slug/workshops/create' => 'conferences#create_workshop', :as => :create_workshop, via: [:get, :post]
post '/conferences/:slug/workshops/save' => 'conferences#save_workshop', :as => :save_workshop
get '/conferences/:slug/workshops/:workshop_id' => 'conferences#view_workshop', :as => :view_workshop
post '/conferences/:slug/workshops/:workshop_id/toggle-interest' => 'conferences#toggle_workshop_interest', :as => :toggle_workshop_interest
match '/conferences/:slug/workshops/:workshop_id/edit' => 'conferences#edit_workshop', :as => :edit_workshop, via: [:get, :post]
match '/conferences/:slug/workshops/:workshop_id/translate/:locale' => 'conferences#translate_workshop', :as => :translate_workshop, via: [:get, :post]
match '/conferences/:slug/workshops/:workshop_id/delete' => 'conferences#delete_workshop', :as => :delete_workshop, via: [:get, :post]
get '/conferences/:slug/workshops/:workshop_id/facilitate' => 'conferences#facilitate_workshop', :as => :facilitate_workshop
post '/conferences/:slug/workshops/:workshop_id/facilitate_request' => 'conferences#facilitate_request', :as => :facilitate_workshop_request
@ -43,7 +45,9 @@ BikeBike::Application.routes.draw do
get '/confirm/:token' => 'application#confirm', :as => :confirm
match '/doconfirm' => 'application#do_confirm', :as => :do_confirm, via: [:get, :post]
#post '/doconfirm' => 'application#do_confirm', :as => :do_confirm
post '/logout' => 'application#user_logout', :as => :logout
match '/logout' => 'application#user_logout', :as => :logout, :via => [:get, :post]
match '/oauth/callback' => 'oauths#callback', :via => [:get, :post]
get '/oauth/:provider' => 'oauths#oauth', :as => :auth_at_provider
post '/translator-request' => 'application#translator_request', :as => :translator_request
get '/error_404' => 'application#error_404'

9
db/migrate/20160515210708_add_registration_status_to_conferences.rb

@ -0,0 +1,9 @@
class AddRegistrationStatusToConferences < ActiveRecord::Migration
def change
add_column :conferences, :registration_status, :string
Conference.find_each do |conference|
conference.registration_status = conference.registration_open ? :open : :closed
conference.save!
end
end
end

5
db/migrate/20160521230653_add_highest_step_to_conference_registrations.rb

@ -0,0 +1,5 @@
class AddHighestStepToConferenceRegistrations < ActiveRecord::Migration
def change
add_column :conference_registrations, :highest_step, :string
end
end

5
db/migrate/20160522022034_add_steps_completed_to_conference_registrations.rb

@ -0,0 +1,5 @@
class AddStepsCompletedToConferenceRegistrations < ActiveRecord::Migration
def change
add_column :conference_registrations, :steps_completed, :json
end
end

5
db/migrate/20160526044849_add_needs_facilitators_to_workshops.rb

@ -0,0 +1,5 @@
class AddNeedsFacilitatorsToWorkshops < ActiveRecord::Migration
def change
add_column :workshops, :needs_facilitators, :boolean
end
end

5
db/migrate/20160529225253_add_languages_to_users.rb

@ -0,0 +1,5 @@
class AddLanguagesToUsers < ActiveRecord::Migration
def change
add_column :users, :languages, :json
end
end

5
db/migrate/20160530175805_change_date_format_in_dynamic_translation_records.rb

@ -0,0 +1,5 @@
class ChangeDateFormatInDynamicTranslationRecords < ActiveRecord::Migration
def change
change_column :dynamic_translation_records, :created_at, :timestamp
end
end

23
db/schema.rb

@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20160304021005) do
ActiveRecord::Schema.define(version: 20160530175805) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -81,6 +81,8 @@ ActiveRecord::Schema.define(version: 20160304021005) do
t.string "allergies"
t.string "languages"
t.string "food"
t.string "highest_step"
t.json "steps_completed"
end
create_table "conference_types", force: :cascade do |t|
@ -121,6 +123,7 @@ ActiveRecord::Schema.define(version: 20160304021005) do
t.string "paypal_password"
t.string "paypal_signature"
t.string "day_parts"
t.string "registration_status"
end
create_table "delayed_jobs", force: :cascade do |t|
@ -140,13 +143,13 @@ ActiveRecord::Schema.define(version: 20160304021005) do
add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree
create_table "dynamic_translation_records", force: :cascade do |t|
t.string "locale"
t.integer "translator_id"
t.string "model_type"
t.integer "model_id"
t.string "column"
t.text "value"
t.date "created_at"
t.string "locale"
t.integer "translator_id"
t.string "model_type"
t.integer "model_id"
t.string "column"
t.text "value"
t.datetime "created_at"
end
create_table "email_confirmations", force: :cascade do |t|
@ -188,7 +191,7 @@ ActiveRecord::Schema.define(version: 20160304021005) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "event_location_id"
t.string "type"
t.string "event_type"
end
create_table "locations", force: :cascade do |t|
@ -317,6 +320,7 @@ ActiveRecord::Schema.define(version: 20160304021005) do
t.string "firstname"
t.string "lastname"
t.boolean "is_translator"
t.json "languages"
end
add_index "users", ["activation_token"], name: "index_users_on_activation_token", using: :btree
@ -403,6 +407,7 @@ ActiveRecord::Schema.define(version: 20160304021005) do
t.text "notes"
t.string "locale"
t.integer "event_location_id"
t.boolean "needs_facilitators"
end
end

78
vendor/assets/javascripts/markdown.js

@ -0,0 +1,78 @@
/*! Licensed under MIT, https://github.com/sofish/pen */
(function(root) {
// only works with Pen
if(!root.Pen) return;
// markdown covertor obj
var covertor = {
keymap: { '96': '`', '62': '>', '49': '1', '46': '.', '45': '-', '42': '*', '35': '#'},
stack : []
};
// return valid markdown syntax
covertor.valid = function(str) {
var len = str.length;
if(str.match(/[#]{1,6}/)) {
return ['h' + len, len];
} else if(str === '```') {
return ['pre', len];
} else if(str === '>') {
return ['blockquote', len];
} else if(str === '1.') {
return ['insertorderedlist', len];
} else if(str === '-' || str === '*') {
return ['insertunorderedlist', len];
} else if(str.match(/(?:\.|\*|\-){3,}/)) {
return ['inserthorizontalrule', len];
}
};
// parse command
covertor.parse = function(e) {
var code = e.keyCode || e.which;
// when `space` is pressed
if (code === 32) {
var markdownSyntax = this.stack.join('');
// reset stack
this.stack = [];
var cmd = this.valid(markdownSyntax);
if (cmd) {
// prevents leading space after executing command
e.preventDefault();
return cmd;
}
}
// make cmd
if(this.keymap[code]) this.stack.push(this.keymap[code]);
return false;
};
// exec command
covertor.action = function(pen, cmd) {
// only apply effect at line start
if(pen.selection.focusOffset > cmd[1]) return;
var node = pen.selection.focusNode;
node.textContent = node.textContent.slice(cmd[1]);
pen.execCommand(cmd[0]);
};
// init covertor
covertor.init = function(pen) {
pen.on('keypress', function(e) {
var cmd = covertor.parse(e);
if(cmd) return covertor.action(pen, cmd);
});
};
// append to Pen
root.Pen.prototype.markdown = covertor;
}(window));

835
vendor/assets/javascripts/pen.js

@ -0,0 +1,835 @@
/*! Licensed under MIT, https://github.com/sofish/pen */
(function(root, doc) {
var Pen, debugMode, selection, utils = {};
var toString = Object.prototype.toString;
var slice = Array.prototype.slice;
// allow command list
var commandsReg = {
block: /^(?:p|h[1-6]|blockquote|pre)$/,
inline: /^(?:bold|italic|underline|insertorderedlist|insertunorderedlist|indent|outdent|strikethrough)$/,
source: /^(?:createlink|unlink)$/,
insert: /^(?:inserthorizontalrule|insertimage|insert)$/,
wrap: /^(?:code)$/
};
var lineBreakReg = /^(?:blockquote|pre|div)$/i;
var effectNodeReg = /(?:[pubia]|h[1-6]|blockquote|[uo]l|li)/i;
var strReg = {
whiteSpace: /(^\s+)|(\s+$)/g,
mailTo: /^(?!mailto:|.+\/|.+#|.+\?)(.*@.*\..+)$/,
http: /^(?!\w+?:\/\/|mailto:|\/|\.\/|\?|#)(.*)$/
};
var autoLinkReg = {
url: /((https?|ftp):\/\/|www\.)[^\s<]{3,}/gi,
prefix: /^(?:https?|ftp):\/\//i,
notLink: /^(?:img|a|input|audio|video|source|code|pre|script|head|title|style)$/i,
maxLength: 100
};
// type detect
utils.is = function(obj, type) {
return toString.call(obj).slice(8, -1) === type;
};
utils.forEach = function(obj, iterator, arrayLike) {
if (!obj) return;
if (arrayLike == null) arrayLike = utils.is(obj, 'Array');
if (arrayLike) {
for (var i = 0, l = obj.length; i < l; i++) iterator(obj[i], i, obj);
} else {
for (var key in obj) {
if (obj.hasOwnProperty(key)) iterator(obj[key], key, obj);
}
}
};
// copy props from a obj
utils.copy = function(defaults, source) {
utils.forEach(source, function (value, key) {
defaults[key] = utils.is(value, 'Object') ? utils.copy({}, value) :
utils.is(value, 'Array') ? utils.copy([], value) : value;
});
return defaults;
};
// log
utils.log = function(message, force) {
if (debugMode || force)
console.log('%cPEN DEBUGGER: %c' + message, 'font-family:arial,sans-serif;color:#1abf89;line-height:2em;', 'font-family:cursor,monospace;color:#333;');
};
utils.delayExec = function (fn) {
var timer = null;
return function (delay) {
clearTimeout(timer);
timer = setTimeout(function() {
fn();
}, delay || 1);
};
};
// merge: make it easy to have a fallback
utils.merge = function(config) {
// default settings
var defaults = {
class: 'pen',
debug: false,
toolbar: null, // custom toolbar
stay: config.stay || !config.debug,
stayMsg: 'Are you going to leave here?',
textarea: '<textarea name="content"></textarea>',
list: [
'blockquote', 'h2', 'h3', 'p', 'code', 'insertorderedlist', 'insertunorderedlist', 'inserthorizontalrule',
'indent', 'outdent', 'bold', 'italic', 'underline', 'createlink', 'insertimage'
],
titles: {},
cleanAttrs: ['id', 'class', 'style', 'name'],
cleanTags: ['script']
};
// user-friendly config
if (config.nodeType === 1) {
defaults.editor = config;
} else if (config.match && config.match(/^#[\S]+$/)) {
defaults.editor = doc.getElementById(config.slice(1));
} else {
defaults = utils.copy(defaults, config);
}
return defaults;
};
function commandOverall(ctx, cmd, val) {
var message = ' to exec 「' + cmd + '」 command' + (val ? (' with value: ' + val) : '');
try {
doc.execCommand(cmd, false, val);
} catch(err) {
// TODO: there's an error when insert a image to document, bug not a bug
return utils.log('fail' + message, true);
}
utils.log('success' + message);
}
function commandInsert(ctx, name, val) {
var node = getNode(ctx);
if (!node) return;
ctx._range.selectNode(node);
ctx._range.collapse(false);
// hide menu when a image was inserted
if(name === 'insertimage' && ctx._menu) toggleNode(ctx._menu, true);
return commandOverall(ctx, name, val);
}
function commandBlock(ctx, name) {
var list = effectNode(ctx, getNode(ctx), true);
if (list.indexOf(name) !== -1) name = 'p';
return commandOverall(ctx, 'formatblock', name);
}
function commandWrap(ctx, tag, value) {
value = '<' + tag + '>' + (value||selection.toString()) + '</' + tag + '>';
return commandOverall(ctx, 'insertHTML', value);
}
function initToolbar(ctx) {
var icons = '', inputStr = '<input class="pen-input" placeholder="http://" />';
ctx._toolbar = ctx.config.toolbar;
if (!ctx._toolbar) {
var toolList = ctx.config.list;
utils.forEach(toolList, function (name) {
var klass = 'pen-icon icon-' + name;
var title = ctx.config.titles[name] || '';
icons += '<i class="' + klass + '" data-action="' + name + '" title="' + title + '"></i>';
}, true);
if (toolList.indexOf('createlink') >= 0 || toolList.indexOf('insertimage') >= 0)
icons += inputStr;
} else if (ctx._toolbar.querySelectorAll('[data-action=createlink]').length ||
ctx._toolbar.querySelectorAll('[data-action=insertimage]').length) {
icons += inputStr;
}
if (icons) {
ctx._menu = doc.createElement('div');
ctx._menu.setAttribute('class', ctx.config.class + '-menu pen-menu');
ctx._menu.innerHTML = icons;
ctx._inputBar = ctx._menu.querySelector('input');
toggleNode(ctx._menu, true);
doc.body.appendChild(ctx._menu);
}
if (ctx._toolbar && ctx._inputBar) toggleNode(ctx._inputBar);
}
function initEvents(ctx) {
var toolbar = ctx._toolbar || ctx._menu, editor = ctx.config.editor;
var toggleMenu = utils.delayExec(function() {
ctx.highlight().menu();
});
var outsideClick = function() {};
function updateStatus(delay) {
ctx._range = ctx.getRange();
toggleMenu(delay);
}
if (ctx._menu) {
var setpos = function() {
if (ctx._menu.style.display === 'block') ctx.menu();
};
// change menu offset when window resize / scroll
addListener(ctx, root, 'resize', setpos);
addListener(ctx, root, 'scroll', setpos);
// toggle toolbar on mouse select
var selecting = false;
addListener(ctx, editor, 'mousedown', function() {
selecting = true;
});
addListener(ctx, editor, 'mouseleave', function() {
if (selecting) updateStatus(800);
selecting = false;
});
addListener(ctx, editor, 'mouseup', function() {
if (selecting) updateStatus(100);
selecting = false;
});
// Hide menu when focusing outside of editor
outsideClick = function(e) {
if (ctx._menu && !containsNode(editor, e.target) && !containsNode(ctx._menu, e.target)) {
removeListener(ctx, doc, 'click', outsideClick);
toggleMenu(100);
}
};
} else {
addListener(ctx, editor, 'click', function() {
updateStatus(0);
});
}
addListener(ctx, editor, 'keyup', function(e) {
if (e.which === 8 && ctx.isEmpty()) return lineBreak(ctx, true);
// toggle toolbar on key select
if (e.which !== 13 || e.shiftKey) return updateStatus(400);
var node = getNode(ctx, true);
if (!node || !node.nextSibling || !lineBreakReg.test(node.nodeName)) return;
if (node.nodeName !== node.nextSibling.nodeName) return;
// hack for webkit, make 'enter' behavior like as firefox.
if (node.lastChild.nodeName !== 'BR') node.appendChild(doc.createElement('br'));
utils.forEach(node.nextSibling.childNodes, function(child) {
if (child) node.appendChild(child);
}, true);
node.parentNode.removeChild(node.nextSibling);
focusNode(ctx, node.lastChild, ctx.getRange());
});
// check line break
addListener(ctx, editor, 'keydown', function(e) {
editor.classList.remove('pen-placeholder');
if (e.which !== 13 || e.shiftKey) return;
var node = getNode(ctx, true);
if (!node || !lineBreakReg.test(node.nodeName)) return;
var lastChild = node.lastChild;
if (!lastChild || !lastChild.previousSibling) return;
if (lastChild.previousSibling.textContent || lastChild.textContent) return;
// quit block mode for 2 'enter'
e.preventDefault();
var p = doc.createElement('p');
p.innerHTML = '<br>';
node.removeChild(lastChild);
if (!node.nextSibling) node.parentNode.appendChild(p);
else node.parentNode.insertBefore(p, node.nextSibling);
focusNode(ctx, p, ctx.getRange());
});
var menuApply = function(action, value) {
ctx.execCommand(action, value);
ctx._range = ctx.getRange();
ctx.highlight().menu();
};
// toggle toolbar on key select
addListener(ctx, toolbar, 'click', function(e) {
var node = e.target, action;
while (node !== toolbar && !(action = node.getAttribute('data-action'))) {
node = node.parentNode;
}
if (!action) return;
if (!/(?:createlink)|(?:insertimage)/.test(action)) return menuApply(action);
if (!ctx._inputBar) return;
// create link
var input = ctx._inputBar;
if (toolbar === ctx._menu) toggleNode(input);
else {
ctx._inputActive = true;
ctx.menu();
}
if (ctx._menu.style.display === 'none') return;
setTimeout(function() { input.focus(); }, 400);
var createlink = function() {
var inputValue = input.value;
if (!inputValue) action = 'unlink';
else {
inputValue = input.value
.replace(strReg.whiteSpace, '')
.replace(strReg.mailTo, 'mailto:$1')
.replace(strReg.http, 'http://$1');
}
menuApply(action, inputValue);
if (toolbar === ctx._menu) toggleNode(input, false);
else toggleNode(ctx._menu, true);
};
input.onkeypress = function(e) {
if (e.which === 13) return createlink();
};
});
// listen for placeholder
addListener(ctx, editor, 'focus', function() {
if (ctx.isEmpty()) lineBreak(ctx, true);
addListener(ctx, doc, 'click', outsideClick);
});
addListener(ctx, editor, 'blur', function() {
checkPlaceholder(ctx);
ctx.checkContentChange();
});
// listen for paste and clear style
addListener(ctx, editor, 'paste', function() {
setTimeout(function() {
ctx.cleanContent();
});
});
}
function addListener(ctx, target, type, listener) {
if (ctx._events.hasOwnProperty(type)) {
ctx._events[type].push(listener);
} else {
ctx._eventTargets = ctx._eventTargets || [];
ctx._eventsCache = ctx._eventsCache || [];
var index = ctx._eventTargets.indexOf(target);
if (index < 0) index = ctx._eventTargets.push(target) - 1;
ctx._eventsCache[index] = ctx._eventsCache[index] || {};
ctx._eventsCache[index][type] = ctx._eventsCache[index][type] || [];
ctx._eventsCache[index][type].push(listener);
target.addEventListener(type, listener, false);
}
return ctx;
}
// trigger local events
function triggerListener(ctx, type) {
if (!ctx._events.hasOwnProperty(type)) return;
var args = slice.call(arguments, 2);
utils.forEach(ctx._events[type], function (listener) {
listener.apply(ctx, args);
});
}
function removeListener(ctx, target, type, listener) {
var events = ctx._events[type];
if (!events) {
var _index = ctx._eventTargets.indexOf(target);
if (_index >= 0) events = ctx._eventsCache[_index][type];
}
if (!events) return ctx;
var index = events.indexOf(listener);
if (index >= 0) events.splice(index, 1);
target.removeEventListener(type, listener, false);
return ctx;
}
function removeAllListeners(ctx) {
utils.forEach(this._events, function (events) {
events.length = 0;
}, false);
if (!ctx._eventsCache) return ctx;
utils.forEach(ctx._eventsCache, function (events, index) {
var target = ctx._eventTargets[index];
utils.forEach(events, function (listeners, type) {
utils.forEach(listeners, function (listener) {
target.removeEventListener(type, listener, false);
}, true);
}, false);
}, true);
ctx._eventTargets = [];
ctx._eventsCache = [];
return ctx;
}
function checkPlaceholder(ctx) {
ctx.config.editor.classList[ctx.isEmpty() ? 'add' : 'remove']('pen-placeholder');
}
function trim(str) {
return (str || '').replace(/^\s+|\s+$/g, '');
}
// node.contains is not implemented in IE10/IE11
function containsNode(parent, child) {
if (parent === child) return true;
child = child.parentNode;
while (child) {
if (child === parent) return true;
child = child.parentNode;
}
return false;
}
function getNode(ctx, byRoot) {
var node, root = ctx.config.editor;
ctx._range = ctx._range || ctx.getRange();
node = ctx._range.commonAncestorContainer;
if (!node || node === root) return null;
while (node && (node.nodeType !== 1) && (node.parentNode !== root)) node = node.parentNode;
while (node && byRoot && (node.parentNode !== root)) node = node.parentNode;
return containsNode(root, node) ? node : null;
}
// node effects
function effectNode(ctx, el, returnAsNodeName) {
var nodes = [];
el = el || ctx.config.editor;
while (el && el !== ctx.config.editor) {
if (el.nodeName.match(effectNodeReg)) {
nodes.push(returnAsNodeName ? el.nodeName.toLowerCase() : el);
}
el = el.parentNode;
}
return nodes;
}
// breakout from node
function lineBreak(ctx, empty) {
var range = ctx._range = ctx.getRange(), node = doc.createElement('p');
if (empty) ctx.config.editor.innerHTML = '';
node.innerHTML = '<br>';
range.insertNode(node);
focusNode(ctx, node.childNodes[0], range);
}
function focusNode(ctx, node, range) {
range.setStartAfter(node);
range.setEndBefore(node);
range.collapse(false);
ctx.setRange(range);
}
function autoLink(node) {
if (node.nodeType === 1) {
if (autoLinkReg.notLink.test(node.tagName)) return;
utils.forEach(node.childNodes, function (child) {
autoLink(child);
}, true);
} else if (node.nodeType === 3) {
var result = urlToLink(node.nodeValue || '');
if (!result.links) return;
var frag = doc.createDocumentFragment(),
div = doc.createElement('div');
div.innerHTML = result.text;
while (div.childNodes.length) frag.appendChild(div.childNodes[0]);
node.parentNode.replaceChild(frag, node);
}
}
function urlToLink(str) {
var count = 0;
str = str.replace(autoLinkReg.url, function(url) {
var realUrl = url, displayUrl = url;
count++;
if (url.length > autoLinkReg.maxLength) displayUrl = url.slice(0, autoLinkReg.maxLength) + '...';
// Add http prefix if necessary
if (!autoLinkReg.prefix.test(realUrl)) realUrl = 'http://' + realUrl;
return '<a href="' + realUrl + '">' + displayUrl + '</a>';
});
return {links: count, text: str};
}
function toggleNode(node, hide) {
node.style.display = hide ? 'none' : 'block';
}
Pen = function(config) {
if (!config) throw new Error('Can\'t find config');
debugMode = config.debug;
// merge user config
var defaults = utils.merge(config);
var editor = defaults.editor;
if (!editor || editor.nodeType !== 1) throw new Error('Can\'t find editor');
// set default class
editor.classList.add(defaults.class);
// set contenteditable
editor.setAttribute('contenteditable', 'true');
// assign config
this.config = defaults;
// set placeholder
if (defaults.placeholder) editor.setAttribute('data-placeholder', defaults.placeholder);
checkPlaceholder(this);
// save the selection obj
this.selection = selection;
// define local events
this._events = {change: []};
// enable toolbar
initToolbar(this);
// init events
initEvents(this);
// to check content change
this._prevContent = this.getContent();
// enable markdown covert
if (this.markdown) this.markdown.init(this);
// stay on the page
if (this.config.stay) this.stay(this.config);
};
Pen.prototype.on = function(type, listener) {
addListener(this, this.config.editor, type, listener);
return this;
};
Pen.prototype.isEmpty = function(node) {
node = node || this.config.editor;
return !(node.querySelector('img')) && !(node.querySelector('blockquote')) &&
!(node.querySelector('li')) && !trim(node.textContent);
};
Pen.prototype.getContent = function() {
return this.isEmpty() ? '' : trim(this.config.editor.innerHTML);
};
Pen.prototype.setContent = function(html) {
this.config.editor.innerHTML = html;
this.cleanContent();
return this;
};
Pen.prototype.checkContentChange = function () {
var prevContent = this._prevContent, currentContent = this.getContent();
if (prevContent === currentContent) return;
this._prevContent = currentContent;
triggerListener(this, 'change', currentContent, prevContent);
};
Pen.prototype.getRange = function() {
var editor = this.config.editor, range = selection.rangeCount && selection.getRangeAt(0);
if (!range) range = doc.createRange();
if (!containsNode(editor, range.commonAncestorContainer)) {
range.selectNodeContents(editor);
range.collapse(false);
}
return range;
};
Pen.prototype.setRange = function(range) {
range = range || this._range;
if (!range) {
range = this.getRange();
range.collapse(false); // set to end
}
try {
selection.removeAllRanges();
selection.addRange(range);
} catch (e) {/* IE throws error sometimes*/}
return this;
};
Pen.prototype.focus = function(focusStart) {
if (!focusStart) this.setRange();
this.config.editor.focus();
return this;
};
Pen.prototype.execCommand = function(name, value) {
name = name.toLowerCase();
this.setRange();
if (commandsReg.block.test(name)) {
commandBlock(this, name);
} else if (commandsReg.inline.test(name) || commandsReg.source.test(name)) {
commandOverall(this, name, value);
} else if (commandsReg.insert.test(name)) {
commandInsert(this, name, value);
} else if (commandsReg.wrap.test(name)) {
commandWrap(this, name, value);
} else {
utils.log('can not find command function for name: ' + name + (value ? (', value: ' + value) : ''), true);
}
if (name === 'indent') this.checkContentChange();
else this.cleanContent({cleanAttrs: ['style']});
};
// remove attrs and tags
// pen.cleanContent({cleanAttrs: ['style'], cleanTags: ['id']})
Pen.prototype.cleanContent = function(options) {
var editor = this.config.editor;
if (!options) options = this.config;
utils.forEach(options.cleanAttrs, function (attr) {
utils.forEach(editor.querySelectorAll('[' + attr + ']'), function(item) {
item.removeAttribute(attr);
}, true);
}, true);
utils.forEach(options.cleanTags, function (tag) {
utils.forEach(editor.querySelectorAll(tag), function(item) {
item.parentNode.removeChild(item);
}, true);
}, true);
checkPlaceholder(this);
this.checkContentChange();
return this;
};
// auto link content, return content
Pen.prototype.autoLink = function() {
autoLink(this.config.editor);
return this.getContent();
};
// highlight menu
Pen.prototype.highlight = function() {
var toolbar = this._toolbar || this._menu
, node = getNode(this);
// remove all highlights
utils.forEach(toolbar.querySelectorAll('.active'), function(el) {
el.classList.remove('active');
}, true);
if (!node) return this;
var effects = effectNode(this, node)
, inputBar = this._inputBar
, highlight;
if (inputBar && toolbar === this._menu) {
// display link input if createlink enabled
inputBar.style.display = 'none';
// reset link input value
inputBar.value = '';
}
highlight = function(str) {
if (!str) return;
var el = toolbar.querySelector('[data-action=' + str + ']');
return el && el.classList.add('active');
};
utils.forEach(effects, function(item) {
var tag = item.nodeName.toLowerCase();
switch(tag) {
case 'a':
if (inputBar) inputBar.value = item.getAttribute('href');
tag = 'createlink';
break;
case 'img':
if (inputBar) inputBar.value = item.getAttribute('src');
tag = 'insertimage';
break;
case 'i':
tag = 'italic';
break;
case 'u':
tag = 'underline';
break;
case 'strike':
tag = 'strikethrough';
break;
case 'b':
tag = 'bold';
break;
case 'pre':
case 'code':
tag = 'code';
break;
case 'ul':
tag = 'insertunorderedlist';
break;
case 'ol':
tag = 'insertorderedlist';
break;
case 'li':
tag = 'indent';
break;
}
highlight(tag);
}, true);
return this;
};
// show menu
Pen.prototype.menu = function() {
if (!this._menu) return this;
if (selection.isCollapsed) {
this._menu.style.display = 'none'; //hide menu
this._inputActive = false;
return this;
}
if (this._toolbar) {
if (!this._inputBar || !this._inputActive) return this;
}
var offset = this._range.getBoundingClientRect()
, menuPadding = 10
, top = offset.top - menuPadding
, left = offset.left + (offset.width / 2)
, menu = this._menu
, menuOffset = {x: 0, y: 0}
, stylesheet = this._stylesheet;
// fixes some browser double click visual discontinuity
// if the offset has no width or height it should not be used
if (offset.width === 0 && offset.height === 0) return this;
// store the stylesheet used for positioning the menu horizontally
if (this._stylesheet === undefined) {
var style = document.createElement("style");
document.head.appendChild(style);
this._stylesheet = stylesheet = style.sheet;
}
// display block to caculate its width & height
menu.style.display = 'block';
menuOffset.x = left - (menu.clientWidth / 2);
menuOffset.y = top - menu.clientHeight;
// check to see if menu has over-extended its bounding box. if it has,
// 1) apply a new class if overflowed on top;
// 2) apply a new rule if overflowed on the left
if (stylesheet.cssRules.length > 0) {
stylesheet.deleteRule(0);
}
if (menuOffset.x < 0) {
menuOffset.x = 0;
stylesheet.insertRule('.pen-menu:after {left: ' + left + 'px;}', 0);
} else {
stylesheet.insertRule('.pen-menu:after {left: 50%; }', 0);
}
if (menuOffset.y < 0) {
menu.classList.add('pen-menu-below');
menuOffset.y = offset.top + offset.height + menuPadding;
} else {
menu.classList.remove('pen-menu-below');
}
menu.style.top = menuOffset.y + 'px';
menu.style.left = menuOffset.x + 'px';
return this;
};
Pen.prototype.stay = function(config) {
var ctx = this;
if (!window.onbeforeunload) {
window.onbeforeunload = function() {
if (!ctx._isDestroyed) return config.stayMsg;
};
}
};
Pen.prototype.destroy = function(isAJoke) {
var destroy = isAJoke ? false : true
, attr = isAJoke ? 'setAttribute' : 'removeAttribute';
if (!isAJoke) {
removeAllListeners(this);
try {
selection.removeAllRanges();
if (this._menu) this._menu.parentNode.removeChild(this._menu);
} catch (e) {/* IE throws error sometimes*/}
} else {
initToolbar(this);
initEvents(this);
}
this._isDestroyed = destroy;
this.config.editor[attr]('contenteditable', '');
return this;
};
Pen.prototype.rebuild = function() {
return this.destroy('it\'s a joke');
};
// a fallback for old browers
root.Pen = function(config) {
if (!config) return utils.log('can\'t find config', true);
var defaults = utils.merge(config)
, klass = defaults.editor.getAttribute('class');
klass = klass ? klass.replace(/\bpen\b/g, '') + ' pen-textarea ' + defaults.class : 'pen pen-textarea';
defaults.editor.setAttribute('class', klass);
defaults.editor.innerHTML = defaults.textarea;
return defaults.editor;
};
// export content as markdown
var regs = {
a: [/<a\b[^>]*href=["']([^"]+|[^']+)\b[^>]*>(.*?)<\/a>/ig, '[$2]($1)'],
img: [/<img\b[^>]*src=["']([^\"+|[^']+)[^>]*>/ig, '![]($1)'],
b: [/<b\b[^>]*>(.*?)<\/b>/ig, '**$1**'],
i: [/<i\b[^>]*>(.*?)<\/i>/ig, '***$1***'],
h: [/<h([1-6])\b[^>]*>(.*?)<\/h\1>/ig, function(a, b, c) {
return '\n' + ('######'.slice(0, b)) + ' ' + c + '\n';
}],
li: [/<(li)\b[^>]*>(.*?)<\/\1>/ig, '* $2\n'],
blockquote: [/<(blockquote)\b[^>]*>(.*?)<\/\1>/ig, '\n> $2\n'],
pre: [/<pre\b[^>]*>(.*?)<\/pre>/ig, '\n```\n$1\n```\n'],
p: [/<p\b[^>]*>(.*?)<\/p>/ig, '\n$1\n'],
hr: [/<hr\b[^>]*>/ig, '\n---\n']
};
Pen.prototype.toMd = function() {
var html = this.getContent()
.replace(/\n+/g, '') // remove line break
.replace(/<([uo])l\b[^>]*>(.*?)<\/\1l>/ig, '$2'); // remove ul/ol
for(var p in regs) {
if (regs.hasOwnProperty(p))
html = html.replace.apply(html, regs[p]);
}
return html.replace(/\*{5}/g, '**');
};
// make it accessible
if (doc.getSelection) {
selection = doc.getSelection();
root.Pen = Pen;
}
}(window, document));

1675
vendor/assets/javascripts/world-110m.json

File diff suppressed because it is too large

1675
vendor/assets/javascripts/world-50m.json

File diff suppressed because it is too large

159
vendor/assets/stylesheets/pen.css

@ -0,0 +1,159 @@
/*! Licensed under MIT, https://github.com/sofish/pen */
/* basic reset */
.pen:focus{outline:none;}
/*.pen, .pen-menu, .pen-input, .pen textarea{font:400 1.16em/1.45 Palatino, Optima, Georgia, serif;color:#331;}
.pen fieldset, img {border: 0;}
.pen blockquote{padding-left:10px;margin-left:-14px;border-left:4px solid #1abf89;}
.pen a{color:#1abf89;}
.pen del{text-decoration:line-through;}
.pen sub, .pen sup {font-size:75%;position:relative;vertical-align:text-top;}
:root .pen sub, :root .pen sup{vertical-align:baseline; /* for ie9 and other mordern browsers *//*}
.pen sup {top:-0.5em;}
.pen sub {bottom:-0.25em;}
.pen hr{border:none;border-bottom:1px solid #cfcfcf;margin-bottom:25px;*color:pink;*filter:chroma(color=pink);height:10px;*margin:-7px 0 15px;}
.pen small{font-size:0.8em;color:#888;}
.pen em, .pen b, .pen strong{font-weight:700;}
.pen pre{white-space:pre-wrap;padding:0.85em;background:#f8f8f8;}
*/
/* block-level element margin */
/*.pen p, .pen pre, .pen ul, .pen ol, .pen dl, .pen form, .pen table, .pen blockquote{margin-bottom:16px;}
*/
/* headers */
/*.pen h1, .pen h2, .pen h3, .pen h4, .pen h5, .pen h6{margin-bottom:16px;font-weight:700;line-height:1.2;}
.pen h1{font-size:2em;}
.pen h2{font-size:1.8em;}
.pen h3{font-size:1.6em;}
.pen h4{font-size:1.4em;}
.pen h5, .pen h6{font-size:1.2em;}*/
/* list */
/*.pen ul, .pen ol{margin-left:1.2em;}
.pen ul, .pen-ul{list-style:disc;}
.pen ol, .pen-ol{list-style:decimal;}
.pen li ul, .pen li ol, .pen-ul ul, .pen-ul ol, .pen-ol ul, .pen-ol ol{margin:0 2em 0 1.2em;}
.pen li ul, .pen-ul ul, .pen-ol ul{list-style: circle;}*/
/* pen menu */
.pen-menu [class^="icon-"], .pen-menu [class*=" icon-"] { /* reset to avoid conflicts with Bootstrap */
background: transparent;
background-image: none;
}
.pen-menu { min-width: 320px; }
.pen-menu, .pen-input{font-size:14px;line-height:1;}
.pen-menu{white-space:nowrap;box-shadow:1px 2px 3px -2px #222;background:#333;background-image:linear-gradient(to bottom, #222, #333);opacity:0.9;position:fixed;height:36px;border:1px solid #333;border-radius:3px;display:none;z-index:1000;}
.pen-menu:after {top:100%;border:solid transparent;content:" ";height:0;width:0;position:absolute;pointer-events:none;}
.pen-menu:after {border-color:rgba(51, 51, 51, 0);border-top-color:#333;border-width:6px;left:50%;margin-left:-6px;}
.pen-menu-below:after {top: -11px; display:block; -moz-transform: rotate(180deg); -webkit-transform: rotate(180deg); -ms-transform: rotate(180deg); -o-transform: rotate(180deg); transform: rotate(180deg);}
.pen-icon{
font-size: 16px;
line-height: 40px;;min-width:20px;display:inline-block;padding:0 10px;height:36px;overflow:hidden;color:#fff;text-align:center;cursor:pointer;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none;user-select:none;}
.pen-icon:first-of-type{border-top-left-radius:3px;border-bottom-left-radius:3px;}
.pen-icon:last-of-type{border-top-right-radius:3px;border-bottom-right-radius:3px;}
.pen-icon:hover{background:#000;}
.pen-icon.active{color:#1abf89;background:#000;box-shadow:inset 2px 2px 4px #000;}
.pen-input{position:absolute;width:100%;left:0;top:0;height:36px;line-height:20px;background:#333;color:#fff;border:none;text-align:center;display:none;font-family:arial, sans-serif;}
.pen-input:focus{outline:none;}
.pen-textarea{display:block;background:#f8f8f8;padding:20px;}
.pen textarea{font-size:14px;border:none;background:none;width:100%;_height:200px;min-height:200px;resize:none;}
/*@font-face {
font-family: 'pen';
src: url('font/fontello.eot?370dad08');
src: url('font/fontello.eot?370dad08#iefix') format('embedded-opentype'),
url('font/fontello.woff?370dad08') format('woff'),
url('font/fontello.ttf?370dad08') format('truetype'),
url('font/fontello.svg?370dad08#fontello') format('svg');
font-weight: normal;
font-style: normal;
}*/
.pen-menu [class^="icon-"]:before, .pen-menu [class*=" icon-"]:before {
speak: none;
display: inline-block;
width: 1em;
margin-right: .2em;
text-align: center;
font-variant: normal;
text-transform: none;
line-height: 1em;
margin-left: .2em;
}
.pen-menu .icon-location:before { content: '\e815'; } /* '' */
.pen-menu .icon-fit:before { content: '\e80f'; } /* '' */
.pen-menu .icon-justifyleft:before { content: '\e80a'; } /* '' */
.pen-menu .icon-justifycenter:before { content: '\e80b'; } /* '' */
.pen-menu .icon-justifyright:before { content: '\e80c'; } /* '' */
.pen-menu .icon-justifyfull:before { content: '\e80d'; } /* '' */
.pen-menu .icon-outdent:before { content: '\e800'; } /* '' */
.pen-menu .icon-indent:before { content: '\e801'; } /* '' */
.pen-menu .icon-mode:before { content: '\e813'; } /* '' */
.pen-menu .icon-fullscreen:before { content: '\e80e'; } /* '' */
.pen-menu .icon-undo:before { content: '\e817'; } /* '' */
.pen-menu .icon-code:before { content: '\e816'; } /* '' */
.pen-menu .icon-pre:before { content: '\e816'; } /* '' */
.pen-menu .icon-unlink:before { content: '\e811'; } /* '' */
.pen-menu .icon-superscript:before { content: '\e808'; } /* '' */
.pen-menu .icon-subscript:before { content: '\e809'; } /* '' */
.pen-menu .icon-inserthorizontalrule:before { content: '\e818'; } /* '' */
.pen-menu .icon-pin:before { content: '\e812'; } /* '' */
.pen {
position: relative;
}
/*.pen.hinted h1:before,
.pen.hinted h2:before,
.pen.hinted h3:before,
.pen.hinted h4:before,
.pen.hinted h5:before,
.pen.hinted h6:before,
.pen.hinted blockquote:before,
.pen.hinted hr:before {
color: #eee;
position: absolute;
right: 100%;
white-space: nowrap;
padding-right: 10px;
}
.pen.hinted blockquote { border-left: 0; margin-left: 0; padding-left: 0; }
.pen.hinted blockquote:before {
color: #1abf89;
content: ">";
font-weight: bold;
vertical-align: center;
}
.pen.hinted h1:before { content: "#";}
.pen.hinted h2:before { content: "##";}
.pen.hinted h3:before { content: "###";}
.pen.hinted h4:before { content: "####";}
.pen.hinted h5:before { content: "#####";}
.pen.hinted h6:before { content: "######";}
.pen.hinted hr:before { content: "﹘﹘﹘"; line-height: 1.2; vertical-align: bottom; }
.pen.hinted pre:before, .pen.hinted pre:after {
content: "```";
display: block;
color: #ccc;
}
.pen.hinted ul { list-style: none; }
.pen.hinted ul li:before {
content: "*";
color: #999;
line-height: 1;
vertical-align: bottom;
margin-left: -1.2em;
display: inline-block;
width: 1.2em;
}
.pen.hinted b:before, .pen.hinted b:after { content: "**"; color: #eee; font-weight: normal; }
.pen.hinted i:before, .pen.hinted i:after { content: "*"; color: #eee; }
.pen.hinted a { text-decoration: none; }
.pen.hinted a:before {content: "["; color: #ddd; }
.pen.hinted a:after { content: "](" attr(href) ")"; color: #ddd; }
.pen-placeholder:after { position: absolute; top: 0; left: 0; content: attr(data-placeholder); color: #999; cursor: text; }
*/
Loading…
Cancel
Save