├── .gitignore ├── .nodemonignore ├── .scss-lint.yml ├── LICENSE.txt ├── app.js ├── client ├── admin.js ├── app.js ├── form.js ├── graphs │ ├── bar.js │ ├── index.js │ ├── line.js │ ├── matrixplot.js │ ├── numberline.js │ ├── pie.js │ ├── radar.js │ └── stack.js ├── input.js ├── lib │ ├── cookie.js │ ├── jargonizer.js │ ├── modal.js │ ├── sharemodal.js │ ├── spinner.js │ └── stickyheader.js ├── nav.js ├── results │ ├── graphs │ │ ├── bar.js │ │ ├── index.js │ │ ├── line.js │ │ ├── matrixplot.js │ │ ├── number-incomplete.js │ │ ├── number-percentile.js │ │ ├── number-responses.js │ │ ├── number-score.js │ │ ├── numberline-agreement.js │ │ ├── numberline-disagreement.js │ │ ├── pie.js │ │ ├── responsiveness-table.js │ │ └── stack.js │ ├── index.js │ └── renderers │ │ ├── even-over.js │ │ └── original.js ├── shared.js └── vendor │ ├── ZeroClipboard.min.js │ ├── chosen.jquery.js │ ├── d3.js │ ├── findAndReplaceDOMText.js │ ├── jquery.ba-throttle-debounce.js │ ├── jquery.js │ ├── jquery.tooltipster.min.js │ ├── parsley.min.js │ ├── require.js │ ├── stupidtable.min.js │ └── velocity.js ├── config.js ├── config.rb ├── controllers ├── about.js ├── admin.js ├── auth.js ├── dashboard.js ├── export.js ├── home.js ├── overview.js ├── survey.js ├── survey_data.js └── survey_share.js ├── gulpfile.js ├── lib ├── calculations │ ├── even-over.js │ └── original.js ├── email_hash.js ├── random.js ├── route_class.js └── survey_template.js ├── migrations ├── 001-add-users.js ├── 002-expiring-mongo-sessions.js ├── 003-elevate-aggregate-score.js └── 004-survey-creators.js ├── models ├── forgot_key.js ├── survey.js ├── survey_response.js └── user.js ├── package.json ├── processes.json ├── public ├── data │ ├── jargon.json │ └── traits.json ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── fonts │ └── karla │ │ ├── Karla-Bold-demo.html │ │ ├── Karla-Bold-webfont.eot │ │ ├── Karla-Bold-webfont.svg │ │ ├── Karla-Bold-webfont.ttf │ │ ├── Karla-Bold-webfont.woff │ │ ├── Karla-Bold-webfont.woff2 │ │ ├── Karla-BoldItalic-demo.html │ │ ├── Karla-BoldItalic-webfont.eot │ │ ├── Karla-BoldItalic-webfont.svg │ │ ├── Karla-BoldItalic-webfont.ttf │ │ ├── Karla-BoldItalic-webfont.woff │ │ ├── Karla-BoldItalic-webfont.woff2 │ │ ├── Karla-Italic-demo.html │ │ ├── Karla-Italic-webfont.eot │ │ ├── Karla-Italic-webfont.svg │ │ ├── Karla-Italic-webfont.ttf │ │ ├── Karla-Italic-webfont.woff │ │ ├── Karla-Italic-webfont.woff2 │ │ ├── Karla-Regular-demo.html │ │ ├── Karla-Regular-webfont.eot │ │ ├── Karla-Regular-webfont.svg │ │ ├── Karla-Regular-webfont.ttf │ │ ├── Karla-Regular-webfont.woff │ │ └── Karla-Regular-webfont.woff2 ├── img │ ├── chosen │ │ ├── chosen-sprite.png │ │ └── chosen-sprite@2x.png │ ├── hero.jpg │ ├── landing_hero.png │ ├── logo.png │ ├── logo.svg │ ├── logo_gray.svg │ └── screenshots │ │ ├── dashboard.png │ │ ├── results.png │ │ └── survey.png └── vendor │ └── ZeroClipboard.swf ├── readme.md ├── sass ├── components │ ├── _buttons.scss │ ├── _cards.scss │ ├── _cookie-error.scss │ ├── _dropdowns.scss │ ├── _footer.scss │ ├── _forms.scss │ ├── _graphs.scss │ ├── _jargon.scss │ ├── _modals.scss │ ├── _nav.scss │ ├── _progress.scss │ ├── _sliders.scss │ └── _tables.scss ├── layouts │ ├── _dashboard.scss │ ├── _landing.scss │ ├── _login.scss │ ├── _overview.scss │ ├── _primary.scss │ └── _share.scss ├── main.scss ├── partials │ ├── _base.scss │ ├── _breakpoints.scss │ ├── _color.scss │ ├── _grid.scss │ ├── _type.scss │ └── _z.scss └── vendor │ ├── _chosen.scss │ ├── _includemedia.scss │ └── _tooltipster.scss ├── survey_fields.md ├── surveys ├── 20141020.json ├── 20141106.json ├── 20141209.json ├── 20150203.json └── 20150331.json ├── tasks ├── build.js ├── bundle.js ├── default.js ├── js-lint.js ├── scss-lint.js ├── serve.js ├── style.js └── watch.js └── views ├── 404.jade ├── admin ├── account.jade └── users.jade ├── email ├── activated │ └── activated.html.jade ├── buttons.jade ├── forgotpassword │ └── forgotpassword.html.jade ├── layout.jade ├── logo.html ├── registration │ └── registration.html.jade ├── style.css ├── surveysharedhasaccount │ └── surveysharedhasaccount.html.jade └── surveysharednoaccount │ └── surveysharednoaccount.html.jade ├── form-welcome.jade ├── form.jade ├── forms.jade ├── landing.jade ├── layouts ├── auth_base.jade └── base.jade ├── partials ├── cookie-error.jade ├── formcomponents.jade ├── formcomponents │ ├── checkbox.jade │ ├── email.jade │ ├── form.jade │ ├── gridselect.jade │ ├── password.jade │ ├── progress.jade │ ├── radio.jade │ ├── select.jade │ ├── single-checkbox.jade │ ├── slider.jade │ ├── teamselect.jade │ ├── text.jade │ └── textarea.jade ├── modal.jade ├── modals │ └── share.jade └── nav.jade ├── survey ├── about.jade ├── done.jade ├── even-over │ ├── detail.jade │ └── overview.jade ├── header.jade ├── nav.jade ├── new.jade └── original │ ├── nav.jade │ ├── overview.jade │ └── scores.jade ├── terms.jade └── user ├── account.jade ├── dashboard.jade ├── forgot.jade ├── login.jade ├── reset.jade └── signup.jade /.gitignore: -------------------------------------------------------------------------------- 1 | # cache files 2 | .sass-cache 3 | migrations/.migrate 4 | 5 | # installed packages 6 | node_modules 7 | 8 | # project/compiled files 9 | design 10 | public/js 11 | public/css/main.css 12 | 13 | # configuration 14 | config/local.js -------------------------------------------------------------------------------- /.nodemonignore: -------------------------------------------------------------------------------- 1 | client/ 2 | public/ 3 | views/ 4 | sass/ 5 | node_modules/ 6 | .sass-cache/ 7 | -------------------------------------------------------------------------------- /.scss-lint.yml: -------------------------------------------------------------------------------- 1 | # SCSS linting rules. Taken from the default configuration and modified a bit to suit the Planetary Manifest. 2 | exclude: 'sass/vendor/**' 3 | 4 | linters: 5 | BorderZero: 6 | enabled: true 7 | 8 | ColorKeyword: 9 | enabled: false 10 | 11 | Comment: 12 | enabled: false 13 | 14 | DebugStatement: 15 | enabled: true 16 | 17 | DeclarationOrder: 18 | enabled: true 19 | 20 | DuplicateProperty: 21 | enabled: true 22 | 23 | ElsePlacement: 24 | enabled: true 25 | style: same_line # or 'new_line' 26 | 27 | EmptyLineBetweenBlocks: 28 | enabled: true 29 | ignore_single_line_blocks: true 30 | 31 | EmptyRule: 32 | enabled: true 33 | 34 | FinalNewline: 35 | enabled: true 36 | present: true 37 | 38 | HexLength: 39 | enabled: true 40 | style: short # or 'long' 41 | 42 | HexNotation: 43 | enabled: true 44 | style: lowercase # or 'uppercase' 45 | 46 | HexValidation: 47 | enabled: true 48 | 49 | IdWithExtraneousSelector: 50 | enabled: true 51 | 52 | Indentation: 53 | enabled: true 54 | character: space # or 'tab' 55 | width: 4 56 | 57 | LeadingZero: 58 | enabled: true 59 | style: include_zero # or 'exclude_zero' 60 | 61 | MergeableSelector: 62 | enabled: true 63 | force_nesting: true 64 | 65 | NameFormat: 66 | enabled: true 67 | convention: hyphenated_lowercase # or 'BEM', or a regex pattern 68 | 69 | PlaceholderInExtend: 70 | enabled: true 71 | 72 | PropertySortOrder: 73 | enabled: false 74 | ignore_unspecified: false 75 | 76 | PropertySpelling: 77 | enabled: true 78 | extra_properties: [] 79 | 80 | SelectorDepth: 81 | enabled: true 82 | max_depth: 3 83 | 84 | SelectorFormat: 85 | enabled: true 86 | convention: hyphenated_lowercase # or 'snake_case', or 'camel_case', or a regex pattern 87 | 88 | Shorthand: 89 | enabled: true 90 | 91 | SingleLinePerProperty: 92 | enabled: true 93 | allow_single_line_rule_sets: true 94 | 95 | SingleLinePerSelector: 96 | enabled: true 97 | 98 | SpaceAfterComma: 99 | enabled: true 100 | 101 | SpaceAfterPropertyColon: 102 | enabled: true 103 | style: one_space # or 'no_space', or 'at_least_one_space', or 'aligned' 104 | 105 | SpaceAfterPropertyName: 106 | enabled: true 107 | 108 | SpaceBeforeBrace: 109 | enabled: true 110 | allow_single_line_padding: false 111 | 112 | SpaceBetweenParens: 113 | enabled: true 114 | spaces: 0 115 | 116 | StringQuotes: 117 | enabled: true 118 | style: double_quotes # or double_quotes 119 | 120 | TrailingSemicolon: 121 | enabled: true 122 | 123 | TrailingZero: 124 | enabled: false 125 | 126 | UnnecessaryMantissa: 127 | enabled: true 128 | 129 | UnnecessaryParentReference: 130 | enabled: true 131 | 132 | UrlFormat: 133 | enabled: true 134 | 135 | UrlQuotes: 136 | enabled: true 137 | 138 | ZeroUnit: 139 | enabled: true 140 | 141 | Compass::*: 142 | enabled: false -------------------------------------------------------------------------------- /client/admin.js: -------------------------------------------------------------------------------- 1 | if( !window.console ) { window.console = {}; } 2 | if( !console.log ) { console.log = function() {}; } 3 | 4 | var $ = require( 'jquery' ), 5 | ZeroClipboard = require( './vendor/ZeroClipboard.min' ); 6 | 7 | // These export onto the jQuery namespace 8 | require( './vendor/jquery.tooltipster.min' ); 9 | require( './vendor/stupidtable.min' ); 10 | 11 | $('.table--expand-button').click( function(e){ 12 | $(this).closest('tr').toggleClass('expanded'); 13 | e.preventDefault(); 14 | } ); 15 | 16 | $( '#select-all' ).change(function() { 17 | var elems = $('.table--checkbox .form--element-control'); 18 | if (this.checked) { 19 | elems.each(function() { 20 | this.checked = true; 21 | }); 22 | } else { 23 | elems.each(function() { 24 | this.checked = false; 25 | }); 26 | } 27 | }); 28 | 29 | 30 | (function($) { 31 | var re = /([^&=]+)=?([^&]*)/g; 32 | var decode = function(str) { 33 | return decodeURIComponent(str.replace(/\+/g, ' ')); 34 | }; 35 | $.parseParams = function(query) { 36 | var params = {}, e; 37 | if (query) { 38 | if (query.substr(0, 1) == '?') { 39 | query = query.substr(1); 40 | } 41 | 42 | while (e = re.exec(query)) { 43 | var k = decode(e[1]); 44 | var v = decode(e[2]); 45 | if (params[k] !== undefined) { 46 | if (!$.isArray(params[k])) { 47 | params[k] = [params[k]]; 48 | } 49 | params[k].push(v); 50 | } else { 51 | params[k] = v; 52 | } 53 | } 54 | } 55 | return params; 56 | }; 57 | })(jQuery); 58 | 59 | function setMultipleActive(val) { 60 | var elems = $('.table--checkbox .form--element-control:checked'); 61 | var keys = []; 62 | elems.each(function() { 63 | keys.push($(this).closest('tr').data('key')); 64 | }); 65 | window.location = '/dashboard/multiple-active?val=' + (val ? 1 : 0) + '&keys=' + JSON.stringify(keys) + '&all=0'; 66 | } 67 | 68 | $( '.button--mark-inactive' ).click(function() { 69 | setMultipleActive(false); 70 | }); 71 | 72 | $( '.button--mark-active' ).click(function() { 73 | setMultipleActive(true); 74 | }); 75 | 76 | var activeSorting = $('table.sortable th.sorting-no-icon'); 77 | 78 | $( 'table.sortable th' ).each( function(idx, el) { 79 | var key = $(el).data('sort-key'); 80 | if (!key) { 81 | return; 82 | } 83 | 84 | $(el).click(function() { 85 | var map = $.parseParams( document.location.search ); 86 | map.sort = key; 87 | map.page = 1; 88 | if (key == activeSorting.data('sort-key')) { 89 | map.dir = activeSorting.hasClass('sorting-asc') ? 'desc' : 'asc'; 90 | } 91 | window.location = '/dashboard?' + $.param(map); 92 | }); 93 | }); 94 | 95 | $( '.dashboard--copy' ).each( function( idx, el ) { 96 | $( el ).attr( 'data-clipboard-text', 97 | window.location.protocol.replace( ':', '' ) 98 | + '://' 99 | + window.location.host 100 | + $( el ).data( 'url' ) 101 | ); 102 | 103 | ZeroClipboard.config( { swfPath: "/vendor/ZeroClipboard.swf" } ); 104 | var client = new ZeroClipboard( el ); 105 | 106 | client.on( 'ready', function( readyEvent ) { 107 | client.on( 'beforecopy', function( event ) { 108 | $( event.target ).removeClass( 'dashboard--zc-copied' ); 109 | } ); 110 | 111 | client.on( 'aftercopy', function( event ) { 112 | $( event.target ).addClass( 'dashboard--zc-copied' ); 113 | } ); 114 | } ); 115 | } ); 116 | 117 | //click handlers for table sorting 118 | $( 'main' ).show(); 119 | -------------------------------------------------------------------------------- /client/app.js: -------------------------------------------------------------------------------- 1 | if( !window.console ) { window.console = {}; } 2 | if( !console.log ) { console.log = function() {}; } 3 | 4 | var $ = require( 'jquery' ), 5 | d3 = require( './vendor/d3' ), 6 | Color = require( 'color' ), 7 | 8 | Spinner = require( './lib/spinner' ), 9 | modal = require( './lib/modal' ), 10 | sharemodal = require( './lib/sharemodal' ), 11 | jargonizer = require( './lib/jargonizer' ), 12 | 13 | surveyResults = require( './results/index' ); 14 | 15 | // These export onto the jQuery namespace 16 | require( './vendor/jquery.tooltipster.min' ); 17 | require( './vendor/parsley.min' ); 18 | require( 'rangeslider.js' ); // this is just the name of the package 19 | require( './input' ); 20 | 21 | console.log( 'Running app.js' ); 22 | 23 | var spinner = new Spinner(); 24 | 25 | $( '.tooltip' ).each( function() { 26 | $( this ).tooltipster( { 27 | contentAsHTML: true, 28 | theme: 'tooltipster-default ' + $( this ).attr( 'class' ).replace( /tooltip\s?/, '' ) 29 | } ); 30 | } ); 31 | 32 | $( 'input[type="range"]' ).rangeslider( { 33 | polyfill: false 34 | } ).on( 'change', function() { 35 | var area = $(this).closest( '.form--element-control' ); 36 | var left = area.find( '.rangeslider__handle' ).css( 'left' ); 37 | var tip = area.find( '.range-value' ); 38 | tip.css( 'left', left ); 39 | tip.find( '.range-value-label' ).html( $( this ).val() ); 40 | } ); 41 | 42 | 43 | $( '.get_link' ).click( function( e ) { 44 | e.preventDefault(); 45 | $( '.link-popup' ).toggleClass( 'visible' ); 46 | $( '.link-popup input' ).focus().select(); 47 | } ); 48 | 49 | $( document ).on( 'click', function() { 50 | $( '.nav--toggle' ).removeClass( 'shown' ); 51 | } ); 52 | 53 | $( '.nav--toggle' ).on( 'click', function( e ) { 54 | e.stopPropagation(); 55 | } ); 56 | 57 | $( '.nav--toggle-button' ).on( 'click', function(e) { 58 | $( '.nav--toggle' ).toggleClass( 'shown' ); 59 | } ); 60 | 61 | // responsive tables 62 | $( '.table-wrapper' ).on( 'scroll', function() { 63 | $( '.row-header' ).css( 'left', $( this ).scrollLeft() ); 64 | } ); 65 | 66 | var chart = d3.selectAll( '.chart, .table, .filter' ), 67 | firstRender = true, 68 | charts = []; 69 | 70 | // bind click handlers to modal links 71 | $( '.modal--open' ).on( 'click', function() { 72 | modal( $( this ).attr( 'data-modalname' ) ).open(); 73 | } ); 74 | 75 | function getRandomArbitrary(min, max) { 76 | return Math.round(Math.random() * (max - min) + min); 77 | } 78 | 79 | function updateNumberPlaceholder(el, index) { 80 | if (!el.context.parentNode) { 81 | window.clearTimeout(dataPlaceholderIntervals[index]); 82 | return; 83 | } 84 | 85 | var rand = getRandomArbitrary(1, 100); 86 | el.html(rand); 87 | } 88 | 89 | var dataPlaceholderIntervals = {}; 90 | 91 | function showDataPlaceholders() { 92 | $('.data-placeholder--number').each(function(i) { 93 | var el = $(this); 94 | dataPlaceholderIntervals[i] = window.setInterval(function() { 95 | updateNumberPlaceholder(el, i); 96 | }, 50); 97 | }); 98 | } 99 | 100 | // Survey graphs 101 | $( function() { 102 | showDataPlaceholders(); 103 | 104 | if( typeof survey !== 'undefined' ) { 105 | spinner.loading( 'survey' ); 106 | surveyResults.init(); 107 | jargonizer(); 108 | } 109 | } ); 110 | -------------------------------------------------------------------------------- /client/form.js: -------------------------------------------------------------------------------- 1 | if( !window.console ) { window.console = {}; } 2 | if( !console.log ) { console.log = function() {}; } 3 | 4 | var $ = require( 'jquery' ); 5 | 6 | // These export onto the jQuery namespace 7 | require( 'rangeslider.js' ); // this is just the name of the package 8 | require( './vendor/jquery.tooltipster.min' ); 9 | require( './vendor/parsley.min' ); 10 | 11 | require( './input.js' ); 12 | require( './lib/jargonizer' )(); 13 | 14 | $( function() { 15 | $( '.tooltip' ).each( function() { 16 | $( this ).tooltipster( { 17 | contentAsHTML: true, 18 | theme: 'tooltipster-default ' + $( this ).attr( 'class' ).replace( /tooltip\s?/, '' ) 19 | } ); 20 | } ); 21 | 22 | $( 'input[type="range"]' ).rangeslider({ 23 | polyfill: false, 24 | onSlide: function(position, value) { 25 | var area = $(this.$element).closest('.form--element-control'); 26 | var left = area.find('.rangeslider__handle').css('left'); 27 | var tip = area.find('.range-value'); 28 | tip.css('left', left); 29 | tip.find('.range-value-label').html(value); 30 | } 31 | }); 32 | 33 | $('.get_link').click( function(e){ 34 | e.preventDefault(); 35 | $('.link-popup').toggleClass('visible'); 36 | $('.link-popup input').focus().select(); 37 | }); 38 | 39 | $(document).on('click', function(){ 40 | $('.nav--toggle').removeClass('shown'); 41 | }); 42 | 43 | $('.nav--toggle').on('click', function(e){ 44 | e.stopPropagation(); 45 | }) 46 | 47 | $('.nav--toggle-button').on( 'click', function(e) { 48 | $('.nav--toggle').toggleClass('shown'); 49 | }); 50 | 51 | //responsive tables 52 | 53 | $('.table-wrapper').scroll( function(){ 54 | $('.row-header').css('left', $(this).scrollLeft()); 55 | }) 56 | 57 | //teamselect click handler 58 | 59 | $('.teamselect--dropdown').change(function() { 60 | if ($(this).find("option:selected").attr("value") === "newteam") { 61 | $(this).removeClass('is-shown').attr("name", ""); 62 | $('.teamselect--text').addClass('is-shown').focus().attr("name", "team"); 63 | }; 64 | }); 65 | 66 | $('.teamselect--text').blur(function() { 67 | if (!$(this).val()) { 68 | $(this).removeClass('is-shown'); 69 | $('.teamselect--dropdown').addClass('is-shown').find('option').prop('selected', false); 70 | } 71 | }); 72 | } ); 73 | -------------------------------------------------------------------------------- /client/graphs/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bar: require( './bar' ), 3 | matrixplot: require( './matrixplot' ), 4 | numberline: require( './numberline' ), 5 | pie: require( './pie' ), 6 | radar: require( './radar' ), 7 | stack: require( './stack' ), 8 | line: require( './line' ) 9 | }; 10 | -------------------------------------------------------------------------------- /client/graphs/numberline.js: -------------------------------------------------------------------------------- 1 | var $ = require( 'jquery' ), 2 | d3 = require( '../vendor/d3' ); 3 | 4 | module.exports = function( width, data ) { 5 | function chart( div ) { 6 | div.each( function() { 7 | var scale = d3.scale.linear() 8 | .domain( [ 0, 1 ] ) 9 | .range( [ 10, width - 10 ] ), 10 | el = d3.select( this ), 11 | g = el.select( 'g' ); 12 | 13 | if( g.empty() ) { 14 | g = d3.select( this ).append( 'svg' ) 15 | .attr( 'width', width ) 16 | .attr( 'height', 120 ) 17 | .append( 'g' ); 18 | 19 | g.append( 'rect' ) 20 | .attr( 'class', 'line' ) 21 | .attr( 'width', width ) 22 | .attr( 'height', 1 ) 23 | .attr( 'x', 0 ) 24 | .attr( 'y', 50 ); 25 | 26 | var labels = [ [ 0, 'Strongly Disagree', 'start' ], [ 2, 'Neutral', 'middle' ], [ 4, 'Strongly Agree', 'end' ] ], 27 | labelItems = g.selectAll( 'text' ) 28 | .data( labels ) 29 | .enter() 30 | .append( 'text' ) 31 | .attr( 'y', 80 ) 32 | .attr( 'text-anchor', function( d ) { return d[ 2 ]; } ); 33 | 34 | labelItems.append( 'tspan' ) 35 | .text( function( d ) { return '(' + d[ 0 ] + ')'; } ) 36 | .attr( 'x', function( d, i ) { return ( ( i / ( labels.length - 1 ) ) * 100 ) + '%'; } ) 37 | .attr( 'dy', 0 ); 38 | 39 | labelItems.append( 'tspan' ) 40 | .text( function( d ) { return d[ 1 ]; } ) 41 | .attr( 'x', function( d, i ) { return ( ( i / ( labels.length - 1 ) ) * 100 ) + '%'; } ) 42 | .attr( 'dy', 20 ); 43 | 44 | g.selectAll( 'circle' ) 45 | .data( data ) 46 | .enter() 47 | .append( 'circle' ) 48 | .attr( 'class', function( d, i ) { return 'circle-' + ( data.length > 1 ? i : 'only' ); } ) 49 | .attr( 'cx', function( d ) { 50 | return scale( d ); 51 | }) 52 | .attr( 'cy', 50 ) 53 | .attr( 'r', 10 ); 54 | } else { 55 | g.selectAll( 'circle' ).each( function( d, i ) { 56 | d3.select( this ) 57 | .transition() 58 | .duration( 1000 ) 59 | .attr( 'cx', scale( data[ i ] ) ); 60 | } ); 61 | } 62 | } ); 63 | } 64 | 65 | return chart; 66 | }; 67 | -------------------------------------------------------------------------------- /client/lib/cookie.js: -------------------------------------------------------------------------------- 1 | // Cookie functions 2 | exports.createCookie = function(name,value,days) { 3 | var expires = ""; 4 | if (days) { 5 | var date = new Date(); 6 | date.setTime(date.getTime()+(days*24*60*60*1000)); 7 | expires = "; expires="+date.toGMTString(); 8 | } 9 | document.cookie = name+"="+value+expires+"; path=/"; 10 | }; 11 | 12 | exports.readCookie = function(name) { 13 | var nameEQ = name + "="; 14 | var ca = document.cookie.split(';'); 15 | for(var i=0;i < ca.length;i++) { 16 | var c = ca[i]; 17 | while (c.charAt(0)==' ') c = c.substring(1,c.length); 18 | if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); 19 | } 20 | return null; 21 | }; 22 | 23 | exports.eraseCookie = function(name) { 24 | createCookie(name,"",-1); 25 | }; 26 | 27 | exports.cookiesEnabled = function() { 28 | var cookieEnabled = (navigator.cookieEnabled) ? true : false; 29 | if( typeof navigator.cookieEnabled == "undefined" && !cookieEnabled ) { 30 | document.cookie = "testcookie"; 31 | cookieEnabled = ( document.cookie.indexOf( "testcookie" ) != -1 ) ? true : false; 32 | } 33 | return cookieEnabled; 34 | }; 35 | -------------------------------------------------------------------------------- /client/lib/jargonizer.js: -------------------------------------------------------------------------------- 1 | var $ = require( 'jquery' ), 2 | findAndReplaceDOMText = require( '../vendor/findAndReplaceDOMText' ); 3 | 4 | /** 5 | * Pulls in the jargon.json file (from public) and replaces any instances of jargon 6 | * in the page with a tooltip-wrapped version that includes a hover-definition. 7 | * @return {null} 8 | */ 9 | module.exports = function() { 10 | $.getJSON( '/data/jargon.json', function( jargon ) { 11 | var body = document.body; 12 | 13 | $.each( jargon, function( key, value ) { 14 | findAndReplaceDOMText( body, { 15 | find: new RegExp( '\\b' + key.replace( /([\[\]*.\\\/^$?!+\()])/g, '\$1' ), 'gi' ), 16 | replace: function( portion, match ) { 17 | var el = $( '' ); 18 | el.html( portion.text ); 19 | return el[ 0 ]; 20 | } 21 | } ); 22 | } ); 23 | 24 | $( body ).find( '.jargon--tooltip[data-tip]' ).tooltipster( { 25 | functionBefore: function( origin, continueTooltip ) { 26 | origin.tooltipster( 'content', origin.data( 'tip' ) ); 27 | continueTooltip(); 28 | }, 29 | theme: 'tooltipster-default' 30 | } ); 31 | } ); 32 | }; -------------------------------------------------------------------------------- /client/lib/modal.js: -------------------------------------------------------------------------------- 1 | var $ = require( 'jquery' ); 2 | 3 | var modalList = {}; 4 | 5 | var ModalObj = function(modalName) { 6 | 7 | this.name = modalName; 8 | this.object = $('.modal--' + this.name); 9 | 10 | this.open = function() { 11 | console.log('modal ' + this.name + ' opened'); 12 | this.object.addClass('is-shown'); 13 | 14 | $('.modal').on('click', $.proxy( function(e) { 15 | this.close(); 16 | }, this)); 17 | 18 | $('.modal--body').on('click', function(e) { 19 | e.stopPropagation(); 20 | }); 21 | 22 | $('.modal--close').on('click', $.proxy( function(e) { 23 | this.close(); 24 | }, this)); 25 | }, 26 | 27 | this.close = function() { 28 | console.log('modal ' + this.name + ' closed'); 29 | this.object.removeClass('is-shown'); 30 | } 31 | }; 32 | 33 | var modal = function(modalName) { 34 | 35 | if( !( modalName in modalList ) ) { 36 | modalList[ modalName ] = new ModalObj( modalName ); 37 | }; 38 | 39 | return modalList[ modalName ]; 40 | 41 | }; 42 | 43 | module.exports = modal; -------------------------------------------------------------------------------- /client/lib/spinner.js: -------------------------------------------------------------------------------- 1 | var $ = require( 'jquery' ); 2 | 3 | module.exports = function() { 4 | var $spinner = $( '.spinner' ), 5 | $gradient = $( '.nav--gradient' ), 6 | states = {}; 7 | 8 | function onChange() { 9 | var showSpinner = false; 10 | 11 | for( var key in states ) { 12 | if( states[ key ] ) 13 | showSpinner = true; 14 | } 15 | 16 | $spinner[ showSpinner ? 'removeClass' : 'addClass' ]('hidden'); 17 | $gradient[ showSpinner ? 'addClass' : 'removeClass' ]('loading'); 18 | } 19 | 20 | this.loading = function( key ) { 21 | states[ key ] = true; 22 | onChange(); 23 | }; 24 | 25 | this.done = function( key ) { 26 | delete states[ key ]; 27 | onChange(); 28 | }; 29 | }; 30 | -------------------------------------------------------------------------------- /client/nav.js: -------------------------------------------------------------------------------- 1 | var $ = require( 'jquery' ); 2 | 3 | $(document).ready( function() { 4 | 5 | var hoverTimer; 6 | 7 | // nav toggling 8 | $('.dropdown').on('click', function(e){ 9 | e.stopPropagation(); 10 | }); 11 | 12 | $('.dropdown').on( 'mouseenter', function(e) { 13 | clearTimeout(hoverTimer); 14 | $(this).addClass('is-active'); 15 | }); 16 | 17 | $('.dropdown').on( 'mouseleave', function(e) { 18 | hoverTimer = setTimeout( function(){ 19 | $('.dropdown').removeClass('is-active'); 20 | }, 200); 21 | }); 22 | 23 | }); 24 | -------------------------------------------------------------------------------- /client/results/graphs/bar.js: -------------------------------------------------------------------------------- 1 | var $ = require( 'jquery' ), 2 | d3 = require( '../../vendor/d3' ), 3 | bar = require( '../../graphs/bar' ); 4 | 5 | module.exports = function( charts, data, responses, colors ) { 6 | $( '.chart.bar[data-col]' ).each( function() { 7 | var width = $( this ).width(), 8 | height = $( this ).data( 'height' ) || 200, 9 | col = $( this ).data( 'col' ), 10 | margins = ( $( this ).data( 'margins' ) || '' ).split( ',' ), 11 | barData = responses.dimension( function( d ) { 12 | return d[ col ]; 13 | } ), 14 | barData2 = responses.dimension( function( d ) { 15 | return d[ col ]; 16 | } ); 17 | 18 | var chart = bar( width, height ) 19 | .dimension( barData ) 20 | .altDimension( barData2 ) 21 | .color( colors[ 'graph-a' ], colors[ 'graph-a' ] ); 22 | chart.margins.apply( chart, margins ); 23 | 24 | charts.push( [ chart, this ] ); 25 | $('.data-placeholder--box', this).remove(); 26 | } ); 27 | }; 28 | -------------------------------------------------------------------------------- /client/results/graphs/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bar: require( './bar' ), 3 | matrixplot: require( './matrixplot' ), 4 | pie: require( './pie' ), 5 | stack: require( './stack' ), 6 | line: require( './line' ), 7 | numberResponses: require( './number-responses' ), 8 | numberIncomplete: require( './number-incomplete' ), 9 | numberPercentile: require( './number-percentile' ), 10 | numberScore: require( './number-score' ), 11 | numberlineAgreement: require( './numberline-agreement' ), 12 | numberlineDisagreement: require( './numberline-disagreement' ), 13 | responsivenessTable: require( './responsiveness-table' ) 14 | }; 15 | -------------------------------------------------------------------------------- /client/results/graphs/line.js: -------------------------------------------------------------------------------- 1 | var $ = require( 'jquery' ), 2 | d3 = require( '../../vendor/d3' ), 3 | line = require( '../../graphs/line' ); 4 | 5 | module.exports = function( charts, data, responses, colors ) { 6 | $( '.chart.line[data-cols]' ).each( function() { 7 | var width = $( this ).width(), 8 | height = 200, 9 | col = $( this ).data( 'col' ), 10 | lineData = responses.dimension( function( d ) { 11 | return d[ col ]; 12 | } ), 13 | lineData2 = responses.dimension( function( d ) { 14 | return d[ col ]; 15 | } ); 16 | 17 | var chart = graphs.line( width, height ) 18 | .dimension( lineData ) 19 | .altDimension( lineData2 ) 20 | .color( colors[ 'graph-a' ], colors[ 'graph-a' ] ); 21 | 22 | charts.push( [ chart, this ] ); 23 | } ); 24 | }; 25 | -------------------------------------------------------------------------------- /client/results/graphs/matrixplot.js: -------------------------------------------------------------------------------- 1 | var $ = require( 'jquery' ), 2 | d3 = require( '../../vendor/d3' ), 3 | matrixplot = require( '../../graphs/matrixplot' ); 4 | 5 | module.exports = function( charts, data, responses, colors ) { 6 | $( '.chart.matrixplot' ).each( function() { 7 | var width = $( this ).width(), 8 | height = $( this ).data( 'height' ) || width, 9 | colX = $( this ).data( 'col-x' ).toLowerCase(), 10 | colXValues = $( this ).data( 'x-values' ).split( ',' ), 11 | colY = $( this ).data( 'col-y' ).toLowerCase(), 12 | colYValues = $( this ).data( 'y-values' ).split( ',' ), 13 | plotData = responses.dimension( function( d ) { 14 | return [ ( d[ colX ] || '' ).toLowerCase(), ( d[ colY ] || '' ).toLowerCase() ]; 15 | } ), 16 | chart = matrixplot( width, height, { 17 | xLabels: $( this ).data( 'x-labels' ).split( ',' ), 18 | xValues: colXValues, 19 | yLabels: $( this ).data( 'y-labels' ).split( ',' ), 20 | yValues: colYValues 21 | } ) 22 | .color( colors[ 'graph-a' ], colors[ 'graph-a' ] ) 23 | .dimension( plotData ); 24 | 25 | charts.push( [ chart, this ] ); 26 | $('.data-placeholder--box', this).remove(); 27 | } ); 28 | }; 29 | -------------------------------------------------------------------------------- /client/results/graphs/number-incomplete.js: -------------------------------------------------------------------------------- 1 | var $ = require( 'jquery' ), 2 | d3 = require( '../../vendor/d3' ); 3 | 4 | module.exports = function( charts, data, responses, colors ) { 5 | $( '.number.incomplete' ).each( function() { 6 | var $number = $( this ); 7 | 8 | var chart = function() { 9 | $number.html( data.incompleteCount ); 10 | }; 11 | 12 | charts.push( [ chart, this ] ); 13 | $('.data-placeholder--box', this).remove(); 14 | } ); 15 | }; -------------------------------------------------------------------------------- /client/results/graphs/number-percentile.js: -------------------------------------------------------------------------------- 1 | var $ = require( 'jquery' ), 2 | d3 = require( '../../vendor/d3' ); 3 | 4 | module.exports = function( charts, data, responses, colors ) { 5 | $( '.number.percentile' ).each( function() { 6 | var $number = $( this ); 7 | 8 | var postfix = [ 'th', 'st', 'nd', 'rd' ]; 9 | 10 | var chart = function( _, filters ) { 11 | // filter by distance from CEO (d['management_layers']) 12 | var filterData = responses.dimension( function( d ) { return d[ 'management_layers' ]; } ), 13 | filterNum = filterData.group().all().reduce( function( pV, cV ) { return pV + cV.value; }, 0 ); 14 | 15 | if( filterNum < data.responses.length || filters.length ) { 16 | $number.addClass( 'is-hidden' ); 17 | } else { 18 | $number.removeClass( 'is-hidden' ); 19 | } 20 | 21 | filterData.dispose(); 22 | }; 23 | 24 | $number.addClass( 'is-hidden' ); 25 | $.getJSON( '/view/' + data.key + '/percentile.json', function( pct ) { 26 | var percentile = Math.round( pct.percentile ), 27 | lastDigit = parseInt( percentile.toString().slice( -1 ) ); 28 | 29 | $number.removeClass( 'is-hidden' ).html( percentile + ( postfix[ lastDigit ] || postfix[ 0 ] ) + ' percentile globally' ); 30 | } ); 31 | 32 | charts.push( [ chart, this ] ); 33 | } ); 34 | }; -------------------------------------------------------------------------------- /client/results/graphs/number-responses.js: -------------------------------------------------------------------------------- 1 | var $ = require( 'jquery' ), 2 | d3 = require( '../../vendor/d3' ); 3 | 4 | module.exports = function( charts, data, responses, colors ) { 5 | $( '.number.responses' ).each( function() { 6 | var $number = $( this ); 7 | 8 | var chart = function() { 9 | // filter by distance from CEO (d['management_layers']) 10 | var filterData = responses.dimension( function( d ) { return d[ 'management_layers' ]; } ); 11 | var num = filterData.group().all().reduce( function( pV, cV ) { return pV + cV.value; }, 0 ); 12 | 13 | console.log('Responses', num); 14 | $number.html( num ); 15 | filterData.dispose(); 16 | }; 17 | 18 | var filterData = responses.dimension( function( d ) { return d[ 'management_layers' ]; } ); 19 | var num = filterData.group().all().reduce( function( pV, cV ) { return pV + cV.value; }, 0 ); 20 | 21 | console.log('Filter data:', filterData); 22 | console.log("UPDATING"); 23 | $('.ovw-scoring--responses-total').html('of ' + num); 24 | 25 | charts.push( [ chart, this ] ); 26 | $('.data-placeholder--box', this).remove(); 27 | } ); 28 | }; -------------------------------------------------------------------------------- /client/results/graphs/number-score.js: -------------------------------------------------------------------------------- 1 | var $ = require( 'jquery' ), 2 | d3 = require( '../../vendor/d3' ); 3 | 4 | module.exports = function( charts, data, responses, colors, calculate ) { 5 | $( '.number.score' ).each( function() { 6 | var $score = $( this ); 7 | 8 | var chart = function( _, filters ) { 9 | var sum = 0; 10 | 11 | data.survey.overalls.forEach( function( row, rdx ) { 12 | row.forEach( function( column, cdx ) { 13 | sum += calculate.columnScore( responses, column ) * 100; 14 | } ); 15 | } ); 16 | 17 | var average = Math.round( sum / ( data.survey.overalls.length * data.survey.columns.length ) ); 18 | var oldAverage = $score.data('average'); 19 | var diff = Math.round((average - oldAverage) / oldAverage * 100); 20 | 21 | var activeFilterNum = filters && filters.length; 22 | 23 | if( activeFilterNum ) { 24 | $('.ovw-scoring--responses-total').addClass('is-shown'); 25 | $('.ovw-scoring--reset').addClass('is-shown'); 26 | 27 | if (diff !== 0) { 28 | $('.ovw-scoring--score-diff').addClass('is-shown'); 29 | $('.score-diff-num').addClass('is-shown').html(diff + '%'); 30 | } else { 31 | $('.ovw-scoring--score-diff').removeClass('is-shown'); 32 | } 33 | 34 | if (diff > 0) { 35 | $('.score-diff-up').addClass('is-shown'); 36 | $('.score-diff-down').removeClass('is-shown'); 37 | $('.score-diff-num').attr('class', 'score-diff-num score-diff-up is-shown'); 38 | } else if (diff < 0) { 39 | $('.score-diff-up').removeClass('is-shown'); 40 | $('.score-diff-down').addClass('is-shown'); 41 | $('.score-diff-num').attr('class', 'score-diff-num score-diff-down is-shown'); 42 | } else { 43 | $('.score-diff-up').removeClass('is-shown'); 44 | $('.score-diff-down').removeClass('is-shown'); 45 | $('.score-diff-num').attr('class', 'score-diff-num'); 46 | } 47 | } else { 48 | $('.ovw-scoring--score-diff').removeClass('is-shown'); 49 | $('.ovw-scoring--responses-total').removeClass('is-shown'); 50 | 51 | $('.score-diff-num').removeClass('is-shown'); 52 | $('.ovw-scoring--reset').removeClass('is-shown'); 53 | $('.score-diff-up').removeClass('is-shown'); 54 | $('.score-diff-down').removeClass('is-shown'); 55 | } 56 | 57 | console.log('Diff', diff, activeFilterNum); 58 | 59 | $score.html( average ); 60 | $score.data('average', average); 61 | }; 62 | 63 | charts.push( [ chart, this ] ); 64 | $('.data-placeholder--box', this).remove(); 65 | } ); 66 | }; 67 | -------------------------------------------------------------------------------- /client/results/graphs/numberline-agreement.js: -------------------------------------------------------------------------------- 1 | var $ = require( 'jquery' ), 2 | d3 = require( '../../vendor/d3' ), 3 | numberline = require( '../../graphs/numberline' ); 4 | 5 | module.exports = function( charts, data, responses, colors, calculate ) { 6 | $( '.chart.numberline.agreement' ).each( function() { 7 | var $this = $( this ), 8 | domain = $( this ).data( 'domain' ), 9 | domainIdx, 10 | closest = { 11 | q: null, 12 | leadership: 0, 13 | team: 4 14 | }; 15 | 16 | $.each( data.survey.columns, function( idx, col ) { 17 | if( col.title === domain ) { 18 | domainIdx = idx; 19 | } 20 | } ); 21 | 22 | data.survey.overalls.forEach( function( row, rdx ) { 23 | // filter by distance from leadership (d['management_layers']) 24 | var leadershipDim = responses 25 | .dimension( function( d ) { return d[ 'management_layers' ]; } ) 26 | .filter( function( d ) { return parseInt( d ) <= 2; } ), 27 | leadership = calculate.columnScore( responses, row[ domainIdx ] ); 28 | leadershipDim.dispose(); 29 | 30 | var teamDim = responses 31 | .dimension( function( d ) { return d[ 'management_layers' ]; } ) 32 | .filter( function( d ) { return parseInt( d ) > 2; } ), 33 | team = calculate.columnScore( responses, row[ domainIdx ] ); 34 | teamDim.dispose(); 35 | 36 | if( Math.abs( leadership - team ) <= Math.abs( closest.leadership - closest.team ) ) { 37 | closest.leadership = leadership; 38 | closest.team = team; 39 | closest.q = row[ domainIdx ]; 40 | } 41 | } ); 42 | 43 | var closestQuestion; 44 | data.survey.pages.filter( function( page, pdx ) { 45 | return page.fields.filter( function( field, fdx ) { 46 | if( field.type === 'gridselect' ) { 47 | for( var key in field.options ) { 48 | if( key === closest.q ) { 49 | closestQuestion = field.options[ key ]; 50 | return true; 51 | } 52 | } 53 | 54 | return false; 55 | } 56 | } ); 57 | } ); 58 | 59 | var $chartbox = $( '
' ); 60 | $this.append( $chartbox ); 61 | $this.append( $( '

' ).html( closestQuestion ) ); 62 | 63 | if( closest.q !== null ) { 64 | var agreementData = [ closest.leadership, closest.team ]; 65 | if( Math.abs( agreementData[ 0 ] - agreementData[ 1 ] ) <= 0.01 ) { 66 | agreementData = [ ( agreementData[ 0 ] + agreementData[ 1 ] ) / 2 ]; 67 | $( this ).prev( '.legend' ).find( '.agreement' ).addClass( 'show' ); 68 | } 69 | 70 | var chart = numberline( $( this ).width(), agreementData ); 71 | charts.push( [ chart, $chartbox[ 0 ] ] ); 72 | } else { 73 | $('.data-placeholder--err', this).addClass( 'visible' ); 74 | } 75 | 76 | 77 | $('.data-placeholder--box', this).remove(); 78 | } ); 79 | }; 80 | -------------------------------------------------------------------------------- /client/results/graphs/numberline-disagreement.js: -------------------------------------------------------------------------------- 1 | var $ = require( 'jquery' ), 2 | d3 = require( '../../vendor/d3' ), 3 | numberline = require( '../../graphs/numberline' ); 4 | 5 | module.exports = function( charts, data, responses, colors, calculate ) { 6 | $( '.chart.numberline.disagreement' ).each( function() { 7 | var $this = $( this ), 8 | domain = $( this ).data( 'domain' ), 9 | domainIdx, 10 | furthest = { 11 | q: null, 12 | leadership: null, 13 | team: null 14 | }; 15 | 16 | $.each( data.survey.columns, function( idx, col ) { 17 | if( col.title === domain ) { 18 | domainIdx = idx; 19 | } 20 | } ); 21 | 22 | data.survey.overalls.forEach( function( row, rdx ) { 23 | // filter by distance from CEO (d['management_layers']) 24 | var leadershipDim = responses 25 | .dimension( function( d ) { return d[ 'management_layers' ]; } ) 26 | .filter( function( d ) { return parseInt( d ) <= 2; } ), 27 | leadership = calculate.columnScore( responses, row[ domainIdx ] ); 28 | leadershipDim.dispose(); 29 | 30 | var teamDim = responses 31 | .dimension( function( d ) { return d[ 'management_layers' ]; } ) 32 | .filter( function( d ) { return parseInt( d ) > 2; } ), 33 | team = calculate.columnScore( responses, row[ domainIdx ] ); 34 | teamDim.dispose(); 35 | 36 | if( Math.abs( leadership - team ) >= Math.abs( furthest.leadership - furthest.team ) ) { 37 | furthest.leadership = leadership; 38 | furthest.team = team; 39 | furthest.q = row[ domainIdx ]; 40 | } 41 | } ); 42 | 43 | var furthestQuestion; 44 | data.survey.pages.filter( function( page, pdx ) { 45 | return page.fields.filter( function( field, fdx ) { 46 | if( field.type === 'gridselect' ) { 47 | for( var key in field.options ) { 48 | if( key === furthest.q ) { 49 | furthestQuestion = field.options[ key ]; 50 | return true; 51 | } 52 | } 53 | 54 | return false; 55 | } 56 | } ); 57 | } ); 58 | 59 | var $chartbox = $( '

' ); 60 | $this.append( $chartbox ); 61 | $this.append( $( '

' ).html( furthestQuestion ) ); 62 | 63 | disagreementData = [ furthest.leadership, furthest.team ]; 64 | if( Math.abs( disagreementData[ 0 ] - disagreementData[ 1 ] ) <= 0.01 ) 65 | disagreementData = [ ( disagreementData[ 0 ] + disagreementData[ 1 ] ) / 2 ]; 66 | 67 | 68 | if( furthest.q !== null ) { 69 | var chart = numberline( $( this ).width(), disagreementData ); 70 | charts.push( [ chart, $chartbox[ 0 ] ] ); 71 | } else { 72 | $('.data-placeholder--err', this).addClass( 'visible' ); 73 | } 74 | 75 | $('.data-placeholder--box', this).remove(); 76 | } ); 77 | }; 78 | -------------------------------------------------------------------------------- /client/results/graphs/pie.js: -------------------------------------------------------------------------------- 1 | var $ = require( 'jquery' ), 2 | d3 = require( '../../vendor/d3' ), 3 | pie = require( '../../graphs/pie' ); 4 | 5 | module.exports = function( charts, data, responses, colors ) { 6 | $( '.chart.pie' ).each( function() { 7 | var width = $( this ).width(), 8 | height = 200, 9 | col = $( this ).data( 'col' ), 10 | subCol = $( this ).data( 'sub-col' ), 11 | pieData = responses.dimension( function( d ) { 12 | return (subCol ? d[ col ][ subCol ] : d[ col ]) 13 | } ), 14 | pieData2 = responses.dimension( function( d ) { 15 | return (subCol ? d[ col ][ subCol ] : d[ col ]) 16 | } ); 17 | 18 | var chart = pie( width, height ) 19 | .dimension( pieData ) 20 | .altDimension( pieData2 ) 21 | .color( graphColorScheme ); 22 | 23 | charts.push( [ chart, this ] ); 24 | $('.data-placeholder--box', this).remove(); 25 | } ); 26 | }; 27 | -------------------------------------------------------------------------------- /client/results/graphs/stack.js: -------------------------------------------------------------------------------- 1 | var $ = require( 'jquery' ), 2 | d3 = require( '../../vendor/d3' ), 3 | stack = require( '../../graphs/stack' ); 4 | 5 | module.exports = function( charts, data, responses, colors ) { 6 | $( '.chart.stacked' ).each( function() { 7 | var col = $( this ).data( 'col' ), 8 | stackedData = responses.dimension( function( d ) { 9 | return d[ col ]; 10 | } ), 11 | stackedData2 = responses.dimension( function( d ) { 12 | return d[ col ]; 13 | } ), 14 | stackedDataGroup = stackedData.group(); 15 | 16 | var width = $( this ).width(), 17 | height = $( this ).data( 'height' ) || 200; 18 | 19 | var y = d3.scale.ordinal() 20 | .rangeRoundBands([0, height], 0.1, 1) 21 | .domain($.unique(data.responses.map(function(d) { return d[ col ] })).sort(function(a, b) { return parseInt(a) > parseInt(b); })); 22 | 23 | var chart = stack( width, height, data.responses ) 24 | .dimension( stackedData ) 25 | .altDimension( stackedData2 ) 26 | .color( colors[ 'graph-a' ], colors[ 'graph-a' ] ) 27 | .y( y ); 28 | 29 | charts.push( [ chart, this ] ); 30 | $('.data-placeholder--box', this).remove(); 31 | } ); 32 | }; 33 | -------------------------------------------------------------------------------- /client/results/index.js: -------------------------------------------------------------------------------- 1 | var $ = require( 'jquery' ), 2 | Color = require( 'color' ), 3 | d3 = require( '../vendor/d3' ), 4 | crossfilter = require( 'crossfilter' ), 5 | 6 | Spinner = require( '../lib/spinner' ), 7 | stickyheader = require( '../lib/stickyheader' ), 8 | 9 | graphs = require( './graphs/index' ); 10 | 11 | require( '../vendor/jquery.tooltipster.min' ); 12 | 13 | var renderers = { 14 | 'original': require( './renderers/original' ), 15 | 'even-over': require( './renderers/even-over' ) 16 | }; 17 | 18 | var spinner = new Spinner(), 19 | firstRender = true, 20 | charts = [], 21 | activeFilters = []; 22 | 23 | // Renders the specified chart or list. 24 | function render( d ) { 25 | if( d ) 26 | d3.select( d[ 1 ] ).call( d[ 0 ], activeFilters ); 27 | } 28 | 29 | exports.init = function() { 30 | if ($(window).width() > 640) { 31 | $('.sticky-nav').stick_in_parent({recalc_every: 50}); 32 | $('.sticky-header').stick_in_parent({offset_top: 175}); 33 | } 34 | 35 | spinner.loading( 'survey' ); 36 | d3.json( '/view/' + survey + '/results.json', function( data ) { 37 | spinner.done( 'survey' ); 38 | 39 | if( data.responses.length === 0 ) return; 40 | 41 | var survey = data.survey, 42 | responses = crossfilter( data.responses ); 43 | 44 | var colors = {}; 45 | 46 | // Graphing colors 47 | [ 48 | 'green', 49 | 'red', 50 | 'graph-a', 51 | 'graph-b', 52 | 'blue' 53 | ].forEach( function( color ) { 54 | var obj = $( '

' ).addClass( 'color-' + color.toLowerCase() ).appendTo( 'body' ); 55 | colors[ color ] = Color( obj.css( 'color' ) ); 56 | obj.remove(); 57 | } ); 58 | 59 | // Graphing color(s) 60 | var graphColorScheme = [ 61 | colors[ 'graph-a' ], 62 | colors[ 'graph-b' ] 63 | ]; 64 | 65 | // Initialize the appropriate renderer 66 | renderers[ survey.render ].init( data, responses, spinner, colors ); 67 | 68 | function resetAll() { 69 | // Don't hate me. 70 | $('.reset').click(); 71 | $('.ovw-scoring--responses-total').removeClass('is-shown'); 72 | } 73 | 74 | $('.ovw-scoring--reset').on( 'click', function( evt ) { 75 | resetAll(); 76 | return false; 77 | } ); 78 | 79 | function renderAll() { 80 | charts.forEach( render ); 81 | renderers[ survey.render ].render( activeFilters ); 82 | 83 | $('body').trigger('sticky_kit:recalc'); 84 | console.log('Completed render all'); 85 | } 86 | 87 | $( 'body' ).on( 'filter', '.chart, .table', function( evt, data ) { 88 | console.log( 'Filter', data ); 89 | 90 | if( data.value ) { 91 | var found = false; 92 | for( var i = 0; i < activeFilters.length; i++ ) { 93 | if( activeFilters[ i ].title === data.title ) { 94 | activeFilters[ i ].value = data.value; 95 | found = true; 96 | break; 97 | } 98 | } 99 | 100 | if( !found ) { 101 | activeFilters.push( data ); 102 | } 103 | } else { 104 | var found = false; 105 | for( var i = 0; i < activeFilters.length; i++ ) { 106 | if( activeFilters[ i ].title === data.title ) { 107 | found = i; 108 | break; 109 | } 110 | } 111 | 112 | if( found !== false ) { 113 | activeFilters.splice( i, 1 ); 114 | } 115 | } 116 | 117 | renderAll(); 118 | } ); 119 | 120 | [ 121 | 'stack', 122 | 'pie', 123 | 'matrixplot', 124 | 'bar', 125 | 'line', 126 | 'numberResponses', 127 | 'numberIncomplete', 128 | 'numberPercentile' 129 | ].forEach( function( gr ) { 130 | graphs[ gr ]( charts, data, responses, colors ); 131 | } ); 132 | 133 | renderAll(); 134 | } ); 135 | }; 136 | -------------------------------------------------------------------------------- /client/shared.js: -------------------------------------------------------------------------------- 1 | // Fixes an issue with jquery.tooltipster not being AMD or CommonJS 2 | var $ = require( 'jquery' ); 3 | global.jQuery = $; 4 | 5 | var cookie = require( './lib/cookie' ); 6 | var nav = require( './nav' ); 7 | 8 | if( !cookie.cookiesEnabled() ) { 9 | document.getElementById( 'cookies' ).className = 'cookies--not-enabled'; 10 | } 11 | -------------------------------------------------------------------------------- /client/vendor/stupidtable.min.js: -------------------------------------------------------------------------------- 1 | (function(d){d.fn.stupidtable=function(b){return this.each(function(){var a=d(this);b=b||{};b=d.extend({},d.fn.stupidtable.default_sort_fns,b);a.on("click.stupidtable","thead th",function(){var c=d(this),f=0,g=d.fn.stupidtable.dir;c.parents("tr").find("th").slice(0,c.index()).each(function(){var a=d(this).attr("colspan")||1;f+=parseInt(a,10)});var e=c.data("sort-default")||g.ASC;c.data("sort-dir")&&(e=c.data("sort-dir")===g.ASC?g.DESC:g.ASC);var l=c.data("sort")||null;null!==l&&(a.trigger("beforetablesort", {column:f,direction:e}),a.css("display"),setTimeout(function(){var h=[],m=b[l],k=a.children("tbody").children("tr");k.each(function(a,b){var c=d(b).children().eq(f),e=c.data("sort-value"),c="undefined"!==typeof e?e:c.text();h.push([c,b])});h.sort(function(a,b){return m(a[0],b[0])});e!=g.ASC&&h.reverse();k=d.map(h,function(a){return a[1]});a.children("tbody").append(k);a.find("th").data("sort-dir",null).removeClass("sorting-desc sorting-asc");c.data("sort-dir",e).addClass("sorting-"+e);a.trigger("aftertablesort", {column:f,direction:e});a.css("display")},10))})})};d.fn.stupidtable.dir={ASC:"asc",DESC:"desc"};d.fn.stupidtable.default_sort_fns={"int":function(b,a){return parseInt(b,10)-parseInt(a,10)},"float":function(b,a){return parseFloat(b)-parseFloat(a)},string:function(b,a){return ba?1:0},"string-ins":function(b,a){b=b.toLowerCase();a=a.toLowerCase();return ba?1:0}}})(jQuery); 2 | -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | // Set the current environment to true in the env object 2 | var currentEnv = process.env.NODE_ENV || 'development'; 3 | 4 | // Import local environment variables from config/local.js 5 | var local = require('./config/local'); 6 | 7 | exports.appName = 'ucros'; 8 | 9 | exports.env = { 10 | production: false, 11 | staging: false, 12 | test: false, 13 | development: false 14 | }; 15 | 16 | exports.env[ currentEnv ] = true; 17 | 18 | exports.log = { 19 | path: __dirname + '/var/log/app_#{currentEnv}.log' 20 | }; 21 | 22 | exports.server = { 23 | port: process.env.PORT || 4600, 24 | // In staging and production, listen loopback. nginx listens on the network. 25 | ip: '127.0.0.1' 26 | }; 27 | 28 | if( currentEnv != 'production' && currentEnv != 'staging' ) { 29 | exports.enableTests = true; 30 | 31 | // Listen on all IPs in dev/test (for testing from other machines) 32 | exports.server.ip = '0.0.0.0'; 33 | } 34 | 35 | exports.db = { 36 | ip: '127.0.0.1', 37 | port: 27017, 38 | database: exports.appName + '_' + currentEnv 39 | }; 40 | exports.db.url = 'mongodb://' + exports.db.ip + ':' + exports.db.port + '/' + exports.db.database; 41 | 42 | exports.auth = local.auth; 43 | 44 | exports.session = local.session; 45 | 46 | if( currentEnv === 'production' ) { 47 | exports.baseUrl = local.url.production; 48 | } else if( currentEnv === 'staging' ) { 49 | exports.baseUrl = local.url.staging; 50 | } else { 51 | exports.baseUrl = 'http://' + exports.server.ip + ':' + exports.server.port; 52 | } 53 | 54 | exports.surveyVersion = "20150331"; 55 | 56 | exports.email = local.email; 57 | -------------------------------------------------------------------------------- /config.rb: -------------------------------------------------------------------------------- 1 | require 'compass/import-once/activate' 2 | # Require any additional compass plugins here. 3 | 4 | require 'breakpoint' 5 | require 'susy' 6 | 7 | # Set this to the root of your project when deployed: 8 | http_path = "/" 9 | css_dir = "public/css" 10 | sass_dir = "sass" 11 | images_dir = "public/img" 12 | javascripts_dir = "public/js" 13 | 14 | # You can select your preferred output style here (can be overridden via the command line): 15 | # output_style = :expanded or :nested or :compact or :compressed 16 | 17 | # To enable relative paths to assets via compass helper functions. Uncomment: 18 | # relative_assets = true 19 | 20 | # To disable debugging comments that display the original location of your selectors. Uncomment: 21 | # line_comments = false 22 | 23 | 24 | # If you prefer the indented syntax, you might want to regenerate this 25 | # project again passing --syntax sass, or you can uncomment this: 26 | # preferred_syntax = :sass 27 | # and then run: 28 | # sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass 29 | -------------------------------------------------------------------------------- /controllers/about.js: -------------------------------------------------------------------------------- 1 | var AboutController = function() { 2 | this.index = function( req, res ) { 3 | res.render( 'landing' ); 4 | }; 5 | 6 | this.terms = function( req, res ) { 7 | res.render( 'terms' ); 8 | }; 9 | }; 10 | 11 | exports.setup = function( app ) { 12 | var about = new AboutController(); 13 | 14 | app.get( '/about', about.index ); 15 | app.get( '/terms', about.terms ); 16 | }; 17 | -------------------------------------------------------------------------------- /controllers/admin.js: -------------------------------------------------------------------------------- 1 | var User = require( '../models/user' ), 2 | Survey = require( '../models/survey' ), 3 | SurveyResponse = require( '../models/survey_response' ), 4 | auth = require( './auth' ); 5 | 6 | var AdminController = function() { 7 | this.users = function( req, res ) { 8 | var o = {}; 9 | o.map = function() { emit( 'score', this.score ); }; 10 | o.reduce = function( k, scores ) { 11 | var sum = 0, count = 0; 12 | for( var i = 0; i < scores.length; i++ ) { 13 | if( scores[ i ] > 0 ) { 14 | sum += scores[ i ]; 15 | count++; 16 | } 17 | }; 18 | return sum / count; 19 | } 20 | 21 | Survey.mapReduce( o, function( err, results ) { 22 | User.find( {}, function( err, users ) { 23 | SurveyResponse.count( {}, function( err, count ) { 24 | var totals = { 25 | users: users.length, 26 | responses: count, 27 | score: results[ 0 ].value 28 | }; 29 | 30 | res.render( 'admin/users', { 31 | userList: users, 32 | totals: totals 33 | } ); 34 | } ); 35 | } ); 36 | } ); 37 | }; 38 | 39 | this.viewUser = function( req, res ) { 40 | User.findOne( { _id: req.param( 'id' ) }, function( err, usr ) { 41 | res.render( 'admin/account', { 42 | accountUser: usr, 43 | csrfToken: req.csrfToken() 44 | } ); 45 | } ); 46 | }; 47 | 48 | this.updateUser = function( req, res ) { 49 | User.findOne( { _id: req.param( 'id' ) }, function( err, usr ) { 50 | auth.updateUser( usr, req.body, function( status ) { 51 | usr.activated = parseInt( req.body.activated ) === 1 ? true : false; 52 | 53 | if( req.body.roles && !usr.hasRole( req.body.roles ) && usr.validRole( req.body.roles ) ) { 54 | usr.roles = [ req.body.roles ]; 55 | } 56 | 57 | usr.save(); 58 | 59 | res.send( status ); 60 | } ); 61 | } ); 62 | }; 63 | }; 64 | 65 | exports.setup = function( app ) { 66 | var admin = new AdminController(); 67 | 68 | app.get( '/admin/users', auth.requiresRole( 'admin' ), admin.users ); 69 | 70 | app.get( '/admin/user/:id', auth.requiresRole( 'admin' ), admin.viewUser ); 71 | app.post( '/admin/user/:id', auth.requiresRole( 'admin' ), admin.updateUser ); 72 | }; 73 | -------------------------------------------------------------------------------- /controllers/export.js: -------------------------------------------------------------------------------- 1 | var auth = require( './auth' ), 2 | surveyTemplate = require( '../lib/survey_template' ), 3 | SurveyResponse = require( '../models/survey_response' ); 4 | 5 | var ExportController = function() { 6 | this.csv = function( req, res ) { 7 | var now = new Date(), 8 | cleanTitle = req.survey.title.replace( /[^a-z0-9]/ig, '' ), 9 | key = req.param( 'key' ), 10 | today = now.getFullYear() + '_' + ( now.getMonth() + 1 ) + '_' + now.getDate(), 11 | flatSurvey = [], 12 | csvData = [], 13 | survey = surveyTemplate.getSurvey( req.survey.version ); 14 | 15 | survey.pages.forEach( function( page ) { 16 | page.fields.forEach( function( q ) { 17 | if( q.type === 'gridselect' ) { 18 | for( var key in q.options ) { 19 | flatSurvey.push( { name: key, label: q.options[ key ] } ); 20 | } 21 | } else { 22 | flatSurvey.push( { name: q.name, label: q.label } ); 23 | } 24 | } ); 25 | } ); 26 | 27 | csvData.push( flatSurvey.map( function( q ) { return q.label; } ) ); 28 | 29 | SurveyResponse.find( { surveyId: req.survey.id }, function( err, responses ) { 30 | if( responses && responses.length ) { 31 | responses.forEach( function( response ) { 32 | csvData.push( flatSurvey.map( function( q ) { 33 | try { 34 | return response.response[ q.name ]; 35 | } catch( e ) { 36 | return ''; 37 | } 38 | } ) ); 39 | } ); 40 | } 41 | 42 | res.header('Content-Disposition', 'attachment; filename=' + today + '_' + cleanTitle + '.csv'); 43 | res.csv( csvData ); 44 | } ); 45 | }; 46 | }; 47 | 48 | exports.setup = function( app ) { 49 | var exporter = new ExportController(); 50 | 51 | app.get( '/export/:key', auth.canAccessSurvey, exporter.csv ); 52 | }; 53 | -------------------------------------------------------------------------------- /controllers/home.js: -------------------------------------------------------------------------------- 1 | var HomeController = function() { 2 | this.index = function( req, res ) { 3 | if( req.isAuthenticated() ) 4 | return res.redirect( '/dashboard' ); 5 | 6 | res.render( 'landing' ); 7 | }; 8 | }; 9 | 10 | exports.setup = function( app ) { 11 | var home = new HomeController(); 12 | 13 | app.get( '/', home.index ); 14 | }; 15 | -------------------------------------------------------------------------------- /controllers/overview.js: -------------------------------------------------------------------------------- 1 | var fs = require( 'fs' ), 2 | auth = require( './auth' ), 3 | Survey = require( '../models/survey' ), 4 | surveyTemplate = require( '../lib/survey_template' ); 5 | 6 | var OverviewController = function() { 7 | this.index = function( req, res ) { 8 | if( !req.survey ) { 9 | return res.redirect( '/404' ); 10 | } 11 | 12 | res.locals.classes.push( 'overview' ); 13 | 14 | var template = surveyTemplate.getSurvey( req.survey.version ); 15 | 16 | res.render( 'survey/' + template.render + '/overview', { 17 | survey: req.survey, 18 | validPermissions: Survey.validPermissions(), 19 | csrfToken: req.csrfToken() 20 | } ); 21 | }; 22 | 23 | this.detail = function( req, res ) { 24 | var template = surveyTemplate.getSurvey( req.survey.version ); 25 | 26 | fs.exists( __dirname + '/../views/survey/' + template.render + '/detail.jade', function( exists ) { 27 | if( exists ) { 28 | res.render( 'survey/' + template.render + '/detail', { 29 | survey: req.survey, 30 | validPermissions: Survey.validPermissions(), 31 | csrfToken: req.csrfToken() 32 | } ); 33 | } else { 34 | // if no scores template exists, head back to the overview 35 | res.redirect( '/view/' + req.param( 'key' ) ); 36 | } 37 | } ); 38 | }; 39 | 40 | this.domain = function( req, res ) { 41 | var template = surveyTemplate.getSurvey( req.survey.version ), 42 | domain = req.param( 'domain' ); 43 | domain = domain[ 0 ].toUpperCase() + domain.slice( 1 ); 44 | 45 | fs.exists( __dirname + '/../views/survey/' + template.render + '/scores.jade', function( exists ) { 46 | if( exists ) { 47 | res.render( 'survey/' + template.render + '/scores', { 48 | survey: req.survey, 49 | domain: domain, 50 | validPermissions: Survey.validPermissions(), 51 | csrfToken: req.csrfToken() 52 | } ); 53 | } else { 54 | // if no scores template exists, head back to the overview 55 | res.redirect( '/view/' + req.param( 'key' ) ); 56 | } 57 | } ); 58 | }; 59 | }; 60 | 61 | exports.setup = function( app ) { 62 | var overview = new OverviewController(); 63 | 64 | app.get( '/view/:key', auth.canAccessSurvey, overview.index ); 65 | app.get( '/view/:key/detail', auth.canAccessSurvey, overview.detail ); 66 | app.get( '/view/:key/scores/:domain', auth.canAccessSurvey, overview.domain ); 67 | }; 68 | -------------------------------------------------------------------------------- /controllers/survey_data.js: -------------------------------------------------------------------------------- 1 | var extend = require( 'util' )._extend, 2 | request = require( 'request' ), 3 | auth = require( './auth' ), 4 | config = require( '../config.js' ), 5 | surveyTemplate = require( '../lib/survey_template' ), 6 | Survey = require( '../models/survey' ), 7 | SurveyResponse = require( '../models/survey_response' ); 8 | 9 | var SurveyDataController = function() { 10 | this.data = function( req, res ) { 11 | var key = req.params.key, 12 | dummy = false; 13 | 14 | Survey.findOne( { key: key } ).exec( function( err, svy ) { 15 | if( !svy ) { 16 | dummy = true; 17 | svy = new Survey( { 18 | key: key, 19 | responses: [], 20 | responseCount: 0 21 | } ); 22 | } 23 | 24 | var data = svy.toObject(), 25 | send = function() { 26 | // Remove list of shared users from object sent to user 27 | delete data.users; 28 | 29 | res.send( data ); 30 | }; 31 | 32 | // Add the survey template 33 | data.survey = surveyTemplate.getSurvey( svy.version ); 34 | 35 | // Incomplete survey count 36 | data.incompleteCount = 0; 37 | 38 | if( !dummy ) { 39 | SurveyResponse.find( { surveyId: svy.id }, function( err, responses ) { 40 | data.responses = responses.filter( function( resp ) { return resp.submitted; } ) 41 | .map( function( resp ) { return resp.response; } ); 42 | data.incompleteCount = responses.filter( function( resp ) { return !resp.submitted; } ).length; 43 | 44 | send(); 45 | } ); 46 | } else { 47 | send(); 48 | } 49 | } ); 50 | }; 51 | 52 | this.percentile = function( req, res ) { 53 | Survey.find( { responseCount: { $gt: 0 } }, { score: 1, key: 1 } ).exec( function( err, surveys ) { 54 | // Get the score for the current survey 55 | var keySurvey = surveys.filter( function( s ) { return s.key === req.params.key; } )[ 0 ], 56 | surveyCount = surveys.length; 57 | // Get a count of the number of surveys with scores less than this one 58 | var surveysBelow = surveys.filter( function( s ) { 59 | return s.score < keySurvey.score; 60 | } ); 61 | 62 | // Get a count of the number of surveys with scores equal to this one 63 | var surveysEqual = surveys.filter( function( s ) { 64 | return s.score === keySurvey.score; 65 | } ); 66 | 67 | /** 68 | * ( ( B + 0.5E ) / n ) * 100 = Percentile 69 | * Where: 70 | * B = number of scores below x 71 | * E = number of scores equal to x 72 | * n = number of scores 73 | * @type {Number} 74 | */ 75 | res.send( { percentile: ( ( surveysBelow.length + ( 0.5 * surveysEqual.length ) ) / surveyCount ) * 100 } ); 76 | } ); 77 | }; 78 | }; 79 | 80 | exports.setup = function( app ) { 81 | var survey = new SurveyDataController(); 82 | 83 | app.get( '/view/:key/results.json', auth.canAccessSurvey, survey.data ); 84 | app.get( '/view/:key/percentile.json', auth.canAccessSurvey, survey.percentile ); 85 | }; 86 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var path = require( 'path' ), 2 | gulp = require( 'gulp' ), 3 | loadPlugins = require( 'gulp-load-plugins' ), 4 | includeAll = require( 'include-all' ); 5 | 6 | var plugins = loadPlugins( { 7 | pattern: [ 'gulp-*', 'merge-*', 'run-*', 'main-*' ], // the glob to search for 8 | replaceString: /\bgulp[\-.]|run[\-.]|merge[\-.]|main[\-.]/, // remove from the name of the module when adding it to the context 9 | camelizePluginName: true, 10 | lazy: true // lazy-load plugins on demand 11 | } ); 12 | 13 | /** 14 | * Loads Gulp configuration modules from the specified 15 | * relative path. These modules should export a function 16 | * that, when run, should either load/configure or register 17 | * a Gulp task. 18 | */ 19 | var loadTasks = function( relPath ) { 20 | return includeAll( { 21 | dirname: path.resolve( __dirname, relPath ), 22 | filter: /(.+)\.js$/ 23 | } ) || {}; 24 | }; 25 | 26 | var setupTasks = function( tasks ) { 27 | for( var taskName in tasks ) { 28 | if( tasks.hasOwnProperty( taskName ) ) { 29 | tasks[ taskName ]( gulp, plugins, path ); 30 | } 31 | } 32 | }; 33 | 34 | var tasks = loadTasks( './tasks' ); 35 | setupTasks( tasks ); 36 | -------------------------------------------------------------------------------- /lib/calculations/even-over.js: -------------------------------------------------------------------------------- 1 | // Calculate an individual column score 2 | exports.columnScore = function( responses, column, filter ) { 3 | var values = responses.dimension( function( d ) { return d[ column ]; } ), 4 | numResponses = 0; 5 | 6 | if( filter ) 7 | values.filter( d ); 8 | 9 | var valueResults = values.group().all(), 10 | valueResultsByKey = {}; 11 | valueResults.forEach( function( vr ) { 12 | if( vr.key !== -1 ) { 13 | valueResultsByKey[ vr.key ] = vr.value; 14 | numResponses += vr.value; 15 | } 16 | } ); 17 | 18 | // Make room for more dimensions 19 | values.dispose(); 20 | 21 | return ( 22 | ( valueResultsByKey[ 1 ] || 0 ) + 23 | ( ( valueResultsByKey[ 2 ] || 0 ) * 2 ) + 24 | ( ( valueResultsByKey[ 3 ] || 0 ) * 3 ) + 25 | ( ( valueResultsByKey[ 4 ] || 0 ) * 4 ) 26 | ) / ( Math.max( numResponses, 1 ) * 4 ); 27 | }; 28 | 29 | // Calculate overall scores 30 | exports.scores = function( data, responses ) { 31 | var columnTotal = [], 32 | overallScores = []; 33 | 34 | data.survey.overalls.forEach( function( row, rdx ) { 35 | var rowTotal = 0, 36 | rowData = []; 37 | 38 | row.forEach( function( column, cdx ) { 39 | var total = exports.columnScore( responses, column ), 40 | percentage = Math.round( total * 100 ); 41 | 42 | // Tabulate row total 43 | rowTotal += total; 44 | rowData.push( total ); 45 | 46 | // Tabulate column total 47 | if( !columnTotal[ cdx ] ) 48 | columnTotal[ cdx ] = total; 49 | else 50 | columnTotal[ cdx ] += total; 51 | } ); 52 | 53 | // Add row average 54 | rowTotal /= row.length; 55 | rowData.push( rowTotal ); 56 | 57 | overallScores.push( rowData ); 58 | } ); 59 | 60 | columnTotal = columnTotal.map( function( a ) { return a / overallScores.length; } ); 61 | columnTotal.push( columnTotal.reduce( function( a, b ) { return a + b; } ) / data.survey.columns.length ); 62 | 63 | return columnTotal; 64 | }; 65 | 66 | // Calculate scores and only return the total average 67 | exports.totalAverage = function( data, responses ) { 68 | var scores = exports.scores( data, responses ); 69 | 70 | return scores[ scores.length - 1 ]; 71 | }; 72 | -------------------------------------------------------------------------------- /lib/calculations/original.js: -------------------------------------------------------------------------------- 1 | // Calculate an individual column score 2 | exports.columnScore = function( responses, column, filter ) { 3 | var values = responses.dimension( function( d ) { return d[ column ]; } ), 4 | numResponses = 0; 5 | 6 | if( filter ) 7 | values.filter( d ); 8 | 9 | var valueResults = values.group().all(), 10 | valueResultsByKey = {}; 11 | valueResults.forEach( function( vr ) { 12 | if( vr.key !== -1 ) { 13 | valueResultsByKey[ vr.key ] = vr.value; 14 | numResponses += vr.value; 15 | } 16 | } ); 17 | 18 | // Make room for more dimensions 19 | values.dispose(); 20 | 21 | return ( 22 | ( valueResultsByKey[ 1 ] || 0 ) + 23 | ( ( valueResultsByKey[ 2 ] || 0 ) * 2 ) + 24 | ( ( valueResultsByKey[ 3 ] || 0 ) * 3 ) + 25 | ( ( valueResultsByKey[ 4 ] || 0 ) * 4 ) 26 | ) / ( numResponses * 4 ); 27 | }; 28 | 29 | // Calculate overall scores 30 | exports.scores = function( data, responses ) { 31 | var columnTotal = [], 32 | overallScores = []; 33 | 34 | data.survey.overalls.forEach( function( row, rdx ) { 35 | var rowTotal = 0, 36 | rowData = []; 37 | 38 | row.forEach( function( column, cdx ) { 39 | var total = exports.columnScore( responses, column ), 40 | percentage = Math.round( total * 100 ); 41 | 42 | // Tabulate row total 43 | rowTotal += total; 44 | rowData.push( total ); 45 | 46 | // Tabulate column total 47 | if( !columnTotal[ cdx ] ) 48 | columnTotal[ cdx ] = total; 49 | else 50 | columnTotal[ cdx ] += total; 51 | } ); 52 | 53 | // Add row average 54 | rowTotal /= row.length; 55 | rowData.push( rowTotal ); 56 | 57 | overallScores.push( rowData ); 58 | } ); 59 | 60 | columnTotal = columnTotal.map( function( a ) { return a / data.survey.rows.length; } ); 61 | columnTotal.push( columnTotal.reduce( function( a, b ) { return a + b; } ) / data.survey.columns.length ); 62 | 63 | overallScores.push( columnTotal ); 64 | 65 | return overallScores; 66 | }; 67 | 68 | // Calculate scores and only return the total average 69 | exports.totalAverage = function( data, responses ) { 70 | var scores = exports.scores( data, responses ), 71 | averagesRow = scores[ scores.length - 1 ]; 72 | 73 | return averagesRow[ averagesRow.length - 1 ]; 74 | }; 75 | -------------------------------------------------------------------------------- /lib/email_hash.js: -------------------------------------------------------------------------------- 1 | var crypto = require( 'crypto' ); 2 | 3 | module.exports = function() { 4 | return function( req, res, next ) { 5 | // returns the current user's email address as an md5 hash 6 | res.locals.emailHash = function() { 7 | if( req.user ) { 8 | var md5 = crypto.createHash( 'md5' ); 9 | md5.update( req.user.email.toLowerCase().trim() ); 10 | 11 | return md5.digest( 'hex' ); 12 | } 13 | 14 | return ''; 15 | }; 16 | 17 | next(); 18 | }; 19 | }; 20 | -------------------------------------------------------------------------------- /lib/random.js: -------------------------------------------------------------------------------- 1 | exports.defaultChars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; 2 | exports.string = function( length, chars ) { 3 | var result = ''; 4 | for( var i = length; i > 0; --i ) { 5 | result += chars[Math.round(Math.random() * (chars.length - 1))]; 6 | } 7 | return result; 8 | }; 9 | 10 | exports.seededNumber = function( seed, max, min ) { 11 | max = max || 1; 12 | min = min || 0; 13 | 14 | seed = ( seed * 9301 + 49297 ) % 233280; 15 | var rnd = seed / 233280; 16 | 17 | return { 18 | nextSeed: seed, 19 | value: min + rnd * (max - min) 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /lib/route_class.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return function( req, res, next ) { 3 | var classes = req.path.toLowerCase() 4 | .replace( /^\/([^\/]+)\/?$/, '$1' ).trim() 5 | .replace( /[^A-Za-z0-9_\-\/]+/g, '-' ) 6 | .replace( /[-]{2,}/g, '-' ) 7 | .split( '/' ); 8 | 9 | res.locals.classes = classes 10 | .filter( function( cls ) { 11 | return cls.length > 1; 12 | } ) 13 | .map( function( cls ) { 14 | if( cls.match( /^\d/ ) ) 15 | return '_' + cls; 16 | 17 | return cls; 18 | } ); 19 | 20 | // usage: 21 | // is(['/view', 'active']) -> returns: 'active' 22 | // is(['/view', 'active'], [/toggle$/, 'thing']) -> if url contains /view and ends with toggle returns: 'active thing' 23 | res.locals.is = function() { 24 | var classes = []; 25 | Array.prototype.slice.call( arguments, 0 ).forEach( function( itm ) { 26 | if( typeof itm[ 0 ] === 'object' ) 27 | var rxitm = itm[ 0 ]; 28 | else 29 | var rxitm = new RegExp( itm[ 0 ].replace( /\//g, '\\/' ).replace( /([.?+*^$()|])/g, '[$1]' ), 'i' ); 30 | if( req.path.match( rxitm ) ) 31 | classes.push( itm[ 1 ] ); 32 | } ); 33 | 34 | return classes.join( ' ' ); 35 | }; 36 | 37 | next(); 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /lib/survey_template.js: -------------------------------------------------------------------------------- 1 | var fs = require( 'fs' ), 2 | config = require( '../config' ), 3 | surveys = {}; 4 | 5 | exports.requiredFields = [ 6 | 'org_years', 7 | 'management_layers', 8 | 'org_position', 9 | 'org_traction', 10 | 'team' 11 | ]; 12 | 13 | exports.getCurrentSurvey = function() { 14 | return exports.getSurvey( config.surveyVersion ); 15 | }; 16 | 17 | exports.getSurvey = function( id ) { 18 | if( !surveys[ id ] ) { 19 | try { 20 | surveys[ id ] = JSON.parse( 21 | fs.readFileSync( __dirname + '/../surveys/' + id + '.json', 'utf-8' ) 22 | ); 23 | } catch( err ) { 24 | console.warn( 'Unable to read survey template "' + id + '":', err ); 25 | return {}; 26 | } 27 | } 28 | 29 | return surveys[ id ]; 30 | }; 31 | 32 | exports.validateCurrentSurvey = function() { 33 | var fieldError = [], 34 | requiredFields = exports.requiredFields.slice(), 35 | survey = exports.getCurrentSurvey(); 36 | 37 | survey.pages.forEach( function( page ) { 38 | page.fields.forEach( function( field ) { 39 | if( !field[ 'type' ] ) { 40 | fieldError.push( 'Field "' + field[ 'name' ] + '" missing required "type" attribute' ); 41 | } else if( field.type !== 'gridselect' && !field[ 'name' ] ) { 42 | console.warn( '[Warning] Field in survey missing name in survey version', config.surveyVersion, '\n', field ); 43 | } else if( requiredFields.indexOf( field.name ) > -1 ) { 44 | requiredFields.splice( requiredFields.indexOf( field.name ), 1 ); 45 | } else if( field.type === "gridselect" ) { 46 | for( var key in field.options ) { 47 | if( requiredFields.indexOf( key ) > -1 ) { 48 | requiredFields.splice( requiredFields.indexOf( key ), 1 ); 49 | } 50 | } 51 | } 52 | } ); 53 | } ); 54 | 55 | if( fieldError.length ) { 56 | fieldError.forEach( function( err ) { 57 | console.error( '[Error]', err, 'in survey version', config.surveyVersion ); 58 | } ); 59 | } 60 | 61 | if( requiredFields.length ) { 62 | console.error( 63 | '[Error] Missing required survey field' + ( requiredFields.length > 1 ? 's' : '' ), 64 | '"' + requiredFields.join( '", "' ) + '"', 65 | 'in survey version', 66 | config.surveyVersion 67 | ); 68 | } 69 | 70 | return ( !fieldError.length && !requiredFields.length ) ? true : false; 71 | }; 72 | -------------------------------------------------------------------------------- /migrations/001-add-users.js: -------------------------------------------------------------------------------- 1 | var mongoose = require( 'mongoose' ), 2 | User = require( '../models/user' ), 3 | config = require( '../config' ); 4 | 5 | // Connect to the MongoDB database 6 | mongoose.connect( config.db.url ); 7 | 8 | exports.up = function(next){ 9 | // Seed a user 10 | var user = new User( { 11 | email: 'global@test.io', 12 | password: '12345678', 13 | roles: [ 'admin' ] 14 | } ); 15 | 16 | user.save( function(err) { 17 | if( err ) { 18 | console.log( err ); 19 | } else { 20 | console.log( 'user: ' + user.email + ' saved.' ); 21 | } 22 | 23 | next(); 24 | } ); 25 | }; 26 | 27 | exports.down = function(next){ 28 | User.findOneAndRemove( { email: 'global@test.io' }, function() { 29 | next() 30 | } ); 31 | }; 32 | -------------------------------------------------------------------------------- /migrations/002-expiring-mongo-sessions.js: -------------------------------------------------------------------------------- 1 | var mongoose = require( 'mongoose' ), 2 | config = require( '../config' ); 3 | 4 | // Connect to the MongoDB database 5 | var db = mongoose.createConnection( config.db.url ); 6 | 7 | exports.up = function(next){ 8 | // expire sessions after 2 years 9 | db.collection( 'sessions' ).ensureIndex( 10 | { "lastAccess": 1 }, 11 | { expireAfterSeconds: 1000 * 60 * 60 * 24 * 365 * 2 }, 12 | function() { next(); } 13 | ); 14 | }; 15 | 16 | exports.down = function(next){ 17 | db.collection( 'sessions' ).dropIndex( 18 | { "lastAccess": 1 }, 19 | {}, 20 | function() { next(); } 21 | ); 22 | }; 23 | -------------------------------------------------------------------------------- /migrations/003-elevate-aggregate-score.js: -------------------------------------------------------------------------------- 1 | var mongoose = require( 'mongoose' ), 2 | config = require( '../config' ), 3 | SurveyResponse = require( '../models/survey_response' ), 4 | Survey = require( '../models/survey' ); 5 | 6 | // Connect to the MongoDB database 7 | var db = mongoose.createConnection( config.db.url ); 8 | 9 | exports.up = function(next){ 10 | // recalculate aggregate score for each survey 11 | Survey.find( {}, function( err, surveys ) { 12 | function pop() { 13 | var svy = surveys.pop(); 14 | if (svy !== undefined && svy !== null) { 15 | if(!svy.orgLeaderRole || svy.orgLeaderRole === '') { 16 | svy.orgLeaderRole = 'Unknown'; 17 | svy.orgLeader = 'Unknown'; 18 | svy.industry = 'Unknown'; 19 | svy.orgAge = 0; 20 | svy.orgSize = 'Not known'; 21 | } 22 | svy.save( function( err ) { 23 | Survey.updateScore( svy._id, function() { 24 | if( surveys.length ) { 25 | process.nextTick( pop ); 26 | } else { 27 | next(); 28 | } 29 | } ); 30 | } ); 31 | } else { 32 | if( surveys.length ) { 33 | process.nextTick( pop ); 34 | } else { 35 | next(); 36 | } 37 | } 38 | }; 39 | 40 | pop(); 41 | } ); 42 | }; 43 | 44 | exports.down = function(next){ 45 | next(); 46 | }; 47 | -------------------------------------------------------------------------------- /migrations/004-survey-creators.js: -------------------------------------------------------------------------------- 1 | var mongoose = require( 'mongoose' ), 2 | config = require( '../config' ), 3 | SurveyResponse = require( '../models/survey_response' ), 4 | Survey = require( '../models/survey' ); 5 | 6 | // Connect to the MongoDB database 7 | var db = mongoose.createConnection( config.db.url ); 8 | 9 | exports.up = function(next){ 10 | // recalculate aggregate score for each survey 11 | Survey.find( {}, function( err, surveys ) { 12 | function pop() { 13 | var svy = surveys.pop(); 14 | if (svy !== undefined && svy !== null) { 15 | if( !svy.creator && svy.users.length ) { 16 | var creator = svy.users.filter( function( user ) { 17 | return user.permissions.indexOf( 'owner' ); 18 | } )[ 0 ]; 19 | 20 | if( creator ) { 21 | svy.creator = creator.user; 22 | } 23 | } 24 | svy.save( function( err ) { 25 | if( surveys.length ) { 26 | process.nextTick( pop ); 27 | } else { 28 | next(); 29 | } 30 | } ); 31 | } else { 32 | if( surveys.length ) { 33 | process.nextTick( pop ); 34 | } else { 35 | next(); 36 | } 37 | } 38 | }; 39 | 40 | pop(); 41 | } ); 42 | }; 43 | 44 | exports.down = function(next){ 45 | next(); 46 | }; 47 | -------------------------------------------------------------------------------- /models/forgot_key.js: -------------------------------------------------------------------------------- 1 | var mongoose = require( 'mongoose' ), 2 | random = require( '../lib/random' ), 3 | Schema = mongoose.Schema; 4 | 5 | var forgotKeySchema = new Schema( { 6 | user: Schema.Types.ObjectId, 7 | created: { 8 | type: Number, 9 | default: Date.now 10 | }, 11 | key: { 12 | type: String, 13 | default: function() { 14 | return random.string( 24, random.defaultChars ); 15 | } 16 | } 17 | } ); 18 | 19 | module.exports = mongoose.model( 'ForgotKey', forgotKeySchema ); 20 | -------------------------------------------------------------------------------- /models/survey_response.js: -------------------------------------------------------------------------------- 1 | var mongoose = require( 'mongoose' ), 2 | Schema = mongoose.Schema; 3 | 4 | var surveyResponseSchema = new Schema( { 5 | surveyId: Schema.Types.ObjectId, 6 | response: Schema.Types.Mixed, 7 | started: { type: Date, default: Date.now }, 8 | submitted: { type: Date, default: null } 9 | } ); 10 | 11 | surveyResponseSchema.post( 'save', function( resp ) { 12 | if( resp.submitted ) { 13 | mongoose.model( 'Survey' ).updateScore( resp.surveyId ); 14 | } 15 | } ); 16 | 17 | module.exports = mongoose.model( 'SurveyResponse', surveyResponseSchema ); 18 | -------------------------------------------------------------------------------- /models/user.js: -------------------------------------------------------------------------------- 1 | var mongoose = require( 'mongoose' ), 2 | MongooseError = require( 'mongoose/lib/error' ), 3 | bcrypt = require( 'bcrypt' ), 4 | uniqueValidator = require( 'mongoose-unique-validator' ), 5 | random = require( '../lib/random' ), 6 | Schema = mongoose.Schema, 7 | SALT_WORK_FACTOR = 10; 8 | 9 | var validateEmail = function( email ) { 10 | var re = /^[^@]*@.+(\.\w{2,})+$/; 11 | return re.test( email ); 12 | }; 13 | 14 | var validatePassword = function( password ) { 15 | return password.length >= 8; 16 | }; 17 | 18 | var userSchema = mongoose.Schema( { 19 | email: { 20 | type: String, 21 | required: 'Email address is required.', 22 | unique: true, 23 | validate: [ validateEmail, 'Please enter a valid email address.' ] 24 | }, 25 | password: { 26 | type: String, 27 | required: 'Please enter a password.', 28 | validate: [ validatePassword, 'Passwords must be at least 8 characters long.' ] 29 | }, 30 | roles: [ String ], 31 | profile: { 32 | type: Schema.Types.Mixed, 33 | default: {} 34 | }, 35 | activated: { 36 | type: Boolean, 37 | default: false 38 | }, 39 | activationKey: String 40 | } ); 41 | 42 | // Validate unique emails 43 | userSchema.plugin( uniqueValidator, { message: 'The email address you entered is already in use.' } ); 44 | 45 | // Bcrypt middleware 46 | userSchema.pre( 'save', function( next, done ) { 47 | var user = this; 48 | 49 | if( user.isModified( 'password' ) ) { 50 | bcrypt.genSalt( SALT_WORK_FACTOR, function( err, salt ) { 51 | if( err ) return next( err ); 52 | 53 | bcrypt.hash( user.password, salt, function( err, hash ) { 54 | if( err ) return next( err ); 55 | user.password = hash; 56 | next(); 57 | } ); 58 | } ); 59 | } else { 60 | next(); 61 | } 62 | } ); 63 | 64 | userSchema.pre( 'save', function( next, done ) { 65 | var user = this; 66 | 67 | if( user.isNew || user.isModified( 'activated' ) ) { 68 | if( !user.activated ) { 69 | user.activationKey = random.string( 24, random.defaultChars ); 70 | } else { 71 | user.activationKey = null; 72 | } 73 | } 74 | 75 | next(); 76 | } ); 77 | 78 | // Role validation 79 | userSchema.method( 'hasRole', function( role ) { 80 | return this.roles.indexOf( role ) > -1; 81 | } ); 82 | 83 | userSchema.method( 'validRole', function( role ) { 84 | return [ 'user', 'admin' ].indexOf( role ) > -1; 85 | } ); 86 | 87 | // Password verification 88 | userSchema.method( 'comparePassword', function( candidatePassword, cb ) { 89 | bcrypt.compare( candidatePassword, this.password, function( err, isMatch ) { 90 | if( err ) return cb( err ); 91 | cb( null, isMatch ); 92 | }); 93 | } ); 94 | 95 | module.exports = mongoose.model( 'User', userSchema ); 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "responsiveos", 3 | "version": "2.0.0", 4 | "description": "Responsive OS", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git://github.com/planetarycorp/UCROS.git" 12 | }, 13 | "author": "Planetary", 14 | "license": "N/A", 15 | "bugs": { 16 | "url": "https://github.com/planetarycorp/UCROS/issues" 17 | }, 18 | "homepage": "https://github.com/planetarycorp/UCROS", 19 | "dependencies": { 20 | "array.prototype.find": "^1.0.0", 21 | "array.prototype.findindex": "^1.0.0", 22 | "bcrypt": "^0.8.1", 23 | "body-parser": "^1.12.0", 24 | "brfs": "^1.4.0", 25 | "browserify": "^9.0.7", 26 | "bulkify": "^1.1.1", 27 | "color": "^0.8.0", 28 | "compression": "^1.4.2", 29 | "cookie-parser": "^1.3.4", 30 | "crossfilter": "^1.3.11", 31 | "csurf": "^1.7.0", 32 | "email-templates": "~1.2.0", 33 | "express": "^4.12.2", 34 | "express-csv": "^0.6.0", 35 | "express-flash": "0.0.2", 36 | "express-session": "^1.10.3", 37 | "express-session-mongo": "^0.1.0", 38 | "gulp-autoprefixer": "^2.1.0", 39 | "gulp-csso": "^1.0.0", 40 | "gulp-load-plugins": "^0.8.1", 41 | "gulp-notify": "^2.2.0", 42 | "gulp-sass": "^1.3.3", 43 | "gulp-sourcemaps": "^1.5.1", 44 | "hat": "~0.0.3", 45 | "include-all": "^0.1.6", 46 | "jade": "^1.9.2", 47 | "jquery": "^2.1.3", 48 | "juice2": "^1.3.1", 49 | "lodash": "^3.5.0", 50 | "marked": "^0.3.3", 51 | "md5": "^1.0.0", 52 | "moment": "~2.9.0", 53 | "mongoose": "^4.0.1", 54 | "mongoose-paginate": "~3.1.3", 55 | "mongoose-text-search": "0.0.2", 56 | "mongoose-unique-validator": "^0.4.1", 57 | "morgan": "^1.5.1", 58 | "node-cache": "^1.1.0", 59 | "node-sass": "~3.0.0-beta.4", 60 | "nodemailer": "^1.3.2", 61 | "nodemailer-html-to-text": "^1.0.0", 62 | "nodemailer-ses-transport": "^1.2.0", 63 | "on-headers": "^1.0.0", 64 | "parsleyjs": "~2.0.7", 65 | "passport": "^0.2.1", 66 | "passport-local": "^1.0.0", 67 | "rangeslider.js": "^1.1.0", 68 | "request": "^2.53.0", 69 | "vinyl-buffer": "^1.0.0", 70 | "vinyl-source-stream": "^1.1.0", 71 | "watchify": "^2.4.0" 72 | }, 73 | "devDependencies": { 74 | "gulp": "~3.8.11", 75 | "gulp-jshint": "~1.9.2", 76 | "gulp-livereload": "~3.8.0", 77 | "gulp-minify-css": "~1.0.0", 78 | "gulp-nodemon": "~1.0.5", 79 | "gulp-scss-lint": "~0.1.10", 80 | "gulp-util": "~3.0.4", 81 | "tiny-lr": "~0.1.5" 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /processes.json: -------------------------------------------------------------------------------- 1 | { 2 | "apps" : [{ 3 | "name": "pulse", 4 | "script": "app.js", 5 | "log_date_format": "YYYY-MM-DD HH:mm Z", 6 | "error_file": "logs/error.log", 7 | "out_file": "logs/server.log", 8 | "env_production": { 9 | "NODE_ENV": "production" 10 | }, 11 | "env_development": { 12 | "NODE_ENV": "development" 13 | }, 14 | "exec_mode": "cluster_mode", 15 | "instances": 0, 16 | "port": 4600 17 | }] 18 | } 19 | -------------------------------------------------------------------------------- /public/data/jargon.json: -------------------------------------------------------------------------------- 1 | { 2 | "Purpose": "A written statement that guides your actions in absense of having a written plan or strategy", 3 | "Customer": "The person that you are responsible for making happy - this could be an actual customer or an internal stakeholder", 4 | "Role": "This is the set of responsibilities that you play. One person can have one title and still hold several roles. ", 5 | "Product and Service": "Physical goods or work performed by you or your team on behalf of others", 6 | "Real-Time Collaborative Software": "Computer programs like Google Docs where you can make a change on your screen, and a coworker will immediately see the change appear on their screen.", 7 | "Product": "Physical goods or work performed by you or your team on behalf of others", 8 | "Real-Time Feedback": "Immediate feedback that helps you understand what is working (or not working) at this moment", 9 | "Customer-Facing Tests": "Experiments you try with real customers (as opposed to making decisions and assuming you know what customers want or need)", 10 | "Success Theater": "Minimizing failures and exaggerating certain aspects of pet projects to make them look like successes and avoid embarassment", 11 | "Failures": "Information learned by serving others; used to change course or improve goods or services", 12 | "Default to Open": "Lean towards sharing information with your colleagues; make it easy for your coworkers to see what you are working on", 13 | "Ship": "Sharing, distributing, selling, or making available the products, services, or work you've performed for others", 14 | "Offering": "Goods, services, or work performed by you on behalf of others", 15 | "Superior": "Boss, manager, department head" 16 | } -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/favicon.ico -------------------------------------------------------------------------------- /public/fonts/karla/Karla-Bold-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/fonts/karla/Karla-Bold-webfont.eot -------------------------------------------------------------------------------- /public/fonts/karla/Karla-Bold-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/fonts/karla/Karla-Bold-webfont.ttf -------------------------------------------------------------------------------- /public/fonts/karla/Karla-Bold-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/fonts/karla/Karla-Bold-webfont.woff -------------------------------------------------------------------------------- /public/fonts/karla/Karla-Bold-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/fonts/karla/Karla-Bold-webfont.woff2 -------------------------------------------------------------------------------- /public/fonts/karla/Karla-BoldItalic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/fonts/karla/Karla-BoldItalic-webfont.eot -------------------------------------------------------------------------------- /public/fonts/karla/Karla-BoldItalic-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/fonts/karla/Karla-BoldItalic-webfont.ttf -------------------------------------------------------------------------------- /public/fonts/karla/Karla-BoldItalic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/fonts/karla/Karla-BoldItalic-webfont.woff -------------------------------------------------------------------------------- /public/fonts/karla/Karla-BoldItalic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/fonts/karla/Karla-BoldItalic-webfont.woff2 -------------------------------------------------------------------------------- /public/fonts/karla/Karla-Italic-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/fonts/karla/Karla-Italic-webfont.eot -------------------------------------------------------------------------------- /public/fonts/karla/Karla-Italic-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/fonts/karla/Karla-Italic-webfont.ttf -------------------------------------------------------------------------------- /public/fonts/karla/Karla-Italic-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/fonts/karla/Karla-Italic-webfont.woff -------------------------------------------------------------------------------- /public/fonts/karla/Karla-Italic-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/fonts/karla/Karla-Italic-webfont.woff2 -------------------------------------------------------------------------------- /public/fonts/karla/Karla-Regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/fonts/karla/Karla-Regular-webfont.eot -------------------------------------------------------------------------------- /public/fonts/karla/Karla-Regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/fonts/karla/Karla-Regular-webfont.ttf -------------------------------------------------------------------------------- /public/fonts/karla/Karla-Regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/fonts/karla/Karla-Regular-webfont.woff -------------------------------------------------------------------------------- /public/fonts/karla/Karla-Regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/fonts/karla/Karla-Regular-webfont.woff2 -------------------------------------------------------------------------------- /public/img/chosen/chosen-sprite.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/img/chosen/chosen-sprite.png -------------------------------------------------------------------------------- /public/img/chosen/chosen-sprite@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/img/chosen/chosen-sprite@2x.png -------------------------------------------------------------------------------- /public/img/hero.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/img/hero.jpg -------------------------------------------------------------------------------- /public/img/landing_hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/img/landing_hero.png -------------------------------------------------------------------------------- /public/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/img/logo.png -------------------------------------------------------------------------------- /public/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /public/img/logo_gray.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 34 | 35 | 36 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /public/img/screenshots/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/img/screenshots/dashboard.png -------------------------------------------------------------------------------- /public/img/screenshots/results.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/img/screenshots/results.png -------------------------------------------------------------------------------- /public/img/screenshots/survey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/img/screenshots/survey.png -------------------------------------------------------------------------------- /public/vendor/ZeroClipboard.swf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UCSoftware/responsive-pulse/96a352b00e57419b0f819db2065d331ffa1104b9/public/vendor/ZeroClipboard.swf -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # ROS Pulse 2 | 3 | ### Made with Love by [Planetary](https://planetary.io/) 4 | 5 | ## About 6 | 7 | The Pulse quantifies an organization’s operating system across 35 unique attributes, helping internal and external change agents track their impact. 8 | 9 | ## Is your organization prepared for the 21st century? 10 | 11 | The organizations we have today are unfit for the next century. Rigid hierarchies, inflexible matrix reporting structures, meeting-rich schedules, and decision-rights for only the most tenured members – these “features” prevent any real work from getting done. 12 | 13 | We’ve been studying groups that work in a new way and as a result have a bigger impact on the world than their competitors. We’ve been using this tool for that research and for our paying clients. It’s now available for free. Find out how your organization measures up. 14 | 15 | ## Measuring Up 16 | 17 | Administering the Responsive OS Pulse is easy. Once you create an account you'll be given a link you can distribute to your teams. Results can are analyzed in real-time and along a variety of dimensions. 18 | 19 | Intoning the principles measured by Responsive Pulse affords an organization greater responsiveness to competition, culture, technology, and all other forms of disruption. We believe these principles – and their associated implications on ways of working – dramatically improve even the largest and most traditional institutions – for employees, for customers, and for shareholders. 20 | 21 | ## Installation 22 | 23 | ### First, add a config/local.js file that looks like: 24 | 25 | ```javascript 26 | module.exports = { 27 | auth: { 28 | secret: 'a secret', 29 | adminPassword: 'an admin password' 30 | }, 31 | session: { 32 | secret: 'another secret' 33 | }, 34 | email: { 35 | from: 'an email address', 36 | accessKeyId: 'an access key id', 37 | secretAccessKey: 'a secret access key', 38 | rateLimit: a_number, 39 | region: 'a region' 40 | }, 41 | url: { 42 | production: 'a url', 43 | staging: 'another url' 44 | } 45 | }; 46 | ``` 47 | 48 | ### Then run an install: 49 | 50 | 1. npm install -g gulp migrate 51 | 2. npm install 52 | 3. migrate 53 | 4. gulp 54 | 55 | ## License 56 | 57 | Copyright 2015 Undercurrent, LLC 58 | 59 | Licensed under the Apache License, Version 2.0 (the "License"); 60 | you may not use this file except in compliance with the License. 61 | You may obtain a copy of the License at 62 | 63 | http://www.apache.org/licenses/LICENSE-2.0 64 | 65 | Unless required by applicable law or agreed to in writing, software 66 | distributed under the License is distributed on an "AS IS" BASIS, 67 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 68 | See the License for the specific language governing permissions and 69 | limitations under the License. -------------------------------------------------------------------------------- /sass/components/_buttons.scss: -------------------------------------------------------------------------------- 1 | @mixin button-generator($color) { 2 | background: $color; 3 | color: color-contrast($color); 4 | border: 0; 5 | 6 | &:hover { 7 | background: lighten($color, 5%); 8 | 9 | color: contrast-color($color, $gray, $white); 10 | } 11 | } 12 | 13 | .button { 14 | @include button-generator($white); 15 | 16 | display: inline-block; 17 | padding: 0.75em 2em; 18 | text-align: center; 19 | text-decoration: none; 20 | text-shadow: none; 21 | font-family: $sans; 22 | font-size: inherit; 23 | cursor: pointer; 24 | line-height: 1rem; 25 | 26 | &:disabled, 27 | &.button--disabled { 28 | cursor: default; 29 | opacity: 0.5; 30 | } 31 | 32 | + .button { 33 | margin-left: 1em; 34 | } 35 | 36 | i { 37 | margin-right: 0.55em; 38 | margin-left: -0.55em; 39 | } 40 | 41 | &:focus { 42 | outline: none; 43 | } 44 | } 45 | 46 | .button--primary { 47 | @include button-generator($blue); 48 | text-shadow: none; 49 | color: $white; 50 | } 51 | 52 | .button--action { 53 | @include button-generator($green); 54 | text-shadow: none; 55 | color: $white; 56 | } 57 | 58 | .button--danger { 59 | @include button-generator($red); 60 | text-shadow: none; 61 | } 62 | 63 | .button--large { 64 | font-size: 1.2em; 65 | padding: 1em 2.2em; 66 | } 67 | 68 | .button--form-change { 69 | background: none; 70 | border: 0; 71 | display: inline; 72 | font: inherit; 73 | margin: 0; 74 | padding: 0; 75 | outline: none; 76 | outline-offset: 0; 77 | color: $gray-light; 78 | cursor: pointer; 79 | text-decoration: underline; 80 | 81 | &:hover { 82 | color: $gray; 83 | } 84 | } 85 | 86 | .form--invalid { 87 | .button--primary:disabled, 88 | .button--primary:disabled:hover { 89 | @include button-generator($gray-light); 90 | color: $gray-lighter; 91 | opacity: 1; 92 | } 93 | } 94 | 95 | .button--icon { 96 | padding: 0.75rem 1.5rem 0.75rem 1.25rem 97 | } 98 | -------------------------------------------------------------------------------- /sass/components/_cards.scss: -------------------------------------------------------------------------------- 1 | $card-headroom: 1em; 2 | $card-inset: 1.5em; 3 | 4 | .card { 5 | position: relative; 6 | 7 | .card-body { 8 | display: inline-block; 9 | width: 100%; 10 | background: $white; 11 | border: 1px solid $gray-lighter; 12 | padding: $card-headroom $card-inset; 13 | margin-bottom: $card-headroom; 14 | 15 | &.card-body--no-border { 16 | border: none; 17 | } 18 | 19 | .card-breakout { 20 | clear: both; 21 | margin-left: -$card-headroom; 22 | margin-right: -$card-headroom; 23 | 24 | img { 25 | max-width: 100%; 26 | width: auto; 27 | height: auto; 28 | } 29 | } 30 | 31 | .card-intro { 32 | margin-bottom: $card-headroom * 2; 33 | 34 | } 35 | 36 | .data-controls { 37 | margin-bottom: 1em; 38 | text-align: right; 39 | } 40 | 41 | hr { 42 | height: 0; 43 | border: 0; 44 | border-top: 1px solid $off-white; 45 | margin: $card-headroom (-$card-inset); 46 | } 47 | 48 | .card-row { 49 | border-bottom: 1px solid $gray-lighter; 50 | padding: 2em 0; 51 | clear: both; 52 | max-width: 38em; 53 | 54 | form { 55 | display: inline-block; 56 | width: 100%; 57 | clear: both; 58 | } 59 | 60 | &:last-child { 61 | border-bottom: 0; 62 | } 63 | } 64 | } 65 | 66 | &.card-table { 67 | .card-body { 68 | padding: 0; 69 | } 70 | } 71 | } 72 | 73 | .card-title { 74 | font-size: $scale-medium; 75 | font-weight: 300; 76 | color: $gray-light; 77 | margin-bottom: $card-headroom / 2; 78 | } 79 | 80 | .card-responses, 81 | .card-score, 82 | .card-divergence { 83 | text-align: center; 84 | } 85 | 86 | .card--spacious { 87 | .card-body { 88 | padding: ($card-headroom * 1.5) ($card-inset * 1.5); 89 | 90 | .card-breakout { 91 | margin-left: -($card-inset * 1.5); 92 | margin-right: -($card-inset * 1.5); 93 | margin-top: ($card-headroom * 2); 94 | margin-bottom: ($card-headroom * 2); 95 | } 96 | 97 | } 98 | 99 | @include media((" span { 99 | float: none; 100 | text-align: center; 101 | display: inline-block; 102 | width: 100%; 103 | padding-top: 0; 104 | padding-bottom: 1em; 105 | } 106 | } 107 | } 108 | 109 | .dashboard--table { 110 | background: $white; 111 | } 112 | 113 | .table--list .no-responses { 114 | border-bottom: 0; 115 | } 116 | 117 | .survey-title { 118 | font-size: $scale-huge; 119 | color: $gray-darker; 120 | font-weight: 700; 121 | margin: 0; 122 | line-height: 1.33; 123 | } 124 | 125 | .dashboard--share { 126 | a { 127 | text-decoration: none; 128 | } 129 | 130 | &:hover, 131 | &.zeroclipboard-is-hover { 132 | &, 133 | a { 134 | color: $black; 135 | } 136 | } 137 | } 138 | 139 | .dashboard--copy, 140 | .dashboard--copy:hover { 141 | cursor: pointer; 142 | } 143 | 144 | .dashboard--zc-copied { 145 | animation: zc-copied 4s linear 0s; 146 | } 147 | 148 | @keyframes zc-copied { 149 | 0% { 150 | color: $green; 151 | } 152 | 153 | 100% { 154 | color: $gray-darker; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /sass/layouts/_landing.scss: -------------------------------------------------------------------------------- 1 | .hero { 2 | height: 28rem; 3 | background: url("/img/hero.jpg"); 4 | background-size: cover; 5 | background-position: center middle; 6 | margin-bottom: 3rem; 7 | } 8 | 9 | .hero--inner { 10 | @include container; 11 | padding: 6em 0; 12 | } 13 | 14 | .hero--text { 15 | color: $white; 16 | background: $green; 17 | display: inline-block; 18 | padding: 1rem 2rem; 19 | } 20 | 21 | .hero--cta { 22 | display: inline-block; 23 | margin: 1rem 0; 24 | border: 2px solid $white; 25 | border-radius: 3px; 26 | } 27 | 28 | .action { 29 | margin-top: 2em; 30 | } 31 | 32 | .placeholder { 33 | height: 20em; 34 | background: #eee; 35 | } 36 | 37 | .landing-screenshot { 38 | @include span(4); 39 | 40 | line-height: 0; 41 | height: auto; 42 | margin-bottom: $card-headroom; 43 | 44 | @include media(" span { 29 | display: block; 30 | width: 100%; 31 | color: $gray; 32 | border-bottom: 1px solid $gray-lighter; 33 | } 34 | } 35 | 36 | .ovw-scoring { 37 | float: right; 38 | text-align: center; 39 | } 40 | 41 | .ovw-scoring--inner { 42 | border-radius: $default-border-radius; 43 | background: $off-white; 44 | } 45 | 46 | .ovw-scoring--reset { 47 | color: $gray-light; 48 | margin: 0.5rem 0; 49 | display: none; 50 | 51 | &.is-shown { 52 | display: block; 53 | } 54 | } 55 | 56 | .ovw-scoring--percentile { 57 | display: block; 58 | margin: 0.5rem 0; 59 | 60 | &.is-hidden { 61 | display: none; 62 | } 63 | } 64 | 65 | .ovw-scoring--responses, 66 | .ovw-scoring--score { 67 | display: inline-block; 68 | vertical-align: middle; 69 | padding: 0.75rem 1rem 0.25rem; 70 | text-align: center; 71 | 72 | .number { 73 | font-size: $scale-xlarge; 74 | padding: 0.25rem 0; 75 | } 76 | } 77 | 78 | .ovw-scoring--responses { 79 | border-right: 1px solid $gray-lighter; 80 | 81 | .number { 82 | display: inline; 83 | } 84 | } 85 | 86 | .ovw-scoring--responses-total { 87 | color: $gray-light; 88 | padding-left: 0.25em; 89 | display: none; 90 | 91 | &.is-shown { 92 | display: inline-block; 93 | } 94 | } 95 | 96 | .ovw-scoring--score-diff { 97 | vertical-align: middle; 98 | padding-right: 1rem; 99 | padding-bottom: 0.25em; 100 | color: $gray; 101 | display: none; 102 | 103 | &.is-shown { 104 | display: inline-block; 105 | } 106 | } 107 | 108 | .score-diff-up, 109 | .score-diff-down { 110 | font-size: $scale-small; 111 | line-height: 1em; 112 | 113 | &.score-diff-num { 114 | font-size: $scale-normal; 115 | line-height: 1em; 116 | } 117 | } 118 | 119 | .score-diff-up, 120 | .score-diff-down, 121 | .score-diff-num { 122 | opacity: 0; 123 | 124 | &.is-shown { 125 | opacity: 1; 126 | } 127 | } 128 | 129 | .score-diff-up { 130 | color: $green; 131 | } 132 | 133 | .score-diff-down { 134 | color: $red; 135 | } 136 | 137 | .ovw-scoring--label { 138 | color: $gray-light; 139 | } 140 | 141 | .ovw-header { 142 | background: $white; 143 | border-left: 1px solid $gray-lighter; 144 | border-right: 1px solid $gray-lighter; 145 | z-index: 2000; 146 | } 147 | 148 | .ovw-header-inner { 149 | padding: 2em 1.5em 0; 150 | display: inline-block; 151 | width: 100%; 152 | } 153 | 154 | .is_stuck { 155 | z-index: 1000; 156 | } 157 | 158 | .verbatims { 159 | .verbatims--show-more { 160 | display: block; 161 | } 162 | 163 | .verbatims--show-less, .verbatims--list { 164 | display: none; 165 | } 166 | 167 | &.verbatims--expanded { 168 | .verbatims--list { 169 | display: block; 170 | } 171 | 172 | .verbatims--show-more { 173 | display: none; 174 | } 175 | 176 | .verbatims--show-less { 177 | display: block; 178 | } 179 | } 180 | 181 | &.verbatims--no-collapse { 182 | .verbatims--show-more, .verbatims--show-less { 183 | display: none; 184 | } 185 | 186 | .verbatims--list { 187 | display: block; 188 | } 189 | } 190 | } 191 | 192 | .results--section { 193 | padding-bottom: 3rem; 194 | } 195 | 196 | .structure--filter-team { 197 | text-align: right; 198 | color: $gray-light; 199 | margin: 1.5em 0 0; 200 | } 201 | -------------------------------------------------------------------------------- /sass/layouts/_primary.scss: -------------------------------------------------------------------------------- 1 | body { 2 | background: $off-white; 3 | margin: 0; 4 | } 5 | 6 | main { 7 | padding: 0 1em; 8 | 9 | section { 10 | @include container; 11 | } 12 | 13 | } 14 | 15 | .overview, .detail { 16 | .no-responses { 17 | text-shadow: 0 1px 0 $white; 18 | 19 | text-align: center; 20 | font-size: $scale-xlarge; 21 | font-weight: 300; 22 | padding-top: 3em; 23 | color: $gray-light; 24 | margin-bottom: 2em; 25 | 26 | @include media((" abs($color-brightness - $dark-text-brightness), $light, $dark); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /sass/partials/_grid.scss: -------------------------------------------------------------------------------- 1 | /* AGS - Airframe grid system 2 | * v0.1 */ 3 | 4 | // Grid configuration 5 | $grid-width: 60rem; 6 | $columns: 12; 7 | $gutter: 2%; 8 | 9 | $row-spacing: 1rem; 10 | $corner-radius: 4px; 11 | $r: $corner-radius; 12 | 13 | // The magic happens here 14 | @function cw($span-num, $total-num) { 15 | $ratio: ($total-num / $span-num); 16 | @return ((100% / ($ratio)) - (($ratio) - 1) * ($gutter / ($ratio))); 17 | } 18 | 19 | @mixin container { 20 | margin: 0 auto; 21 | width: $grid-width; 22 | } 23 | 24 | @mixin row { 25 | margin-bottom: $row-spacing; 26 | 27 | &:after { 28 | content: ""; 29 | display: table; 30 | clear: both; 31 | } 32 | } 33 | 34 | @mixin span($span) { 35 | float: left; 36 | width: cw($span, $columns); 37 | margin-right: $gutter; 38 | } 39 | 40 | @mixin last { 41 | float: right; 42 | margin-right: 0; 43 | } 44 | 45 | @mixin visible-grid { 46 | background: transparentize($blue, 0.9); 47 | } 48 | 49 | // Helper classes 50 | 51 | .g--container { 52 | margin: 0 auto; 53 | width: $grid-width; 54 | } 55 | 56 | .g--row { 57 | @include row; 58 | } 59 | 60 | .g--visible-grid { 61 | @include visible-grid; 62 | } 63 | 64 | .g--full { 65 | @include span(12); 66 | } 67 | 68 | .g--half { 69 | @include span(6); 70 | } 71 | 72 | .g--third { 73 | @include span(4); 74 | } 75 | 76 | .g--quarter { 77 | @include span(3); 78 | } 79 | 80 | .g--last { 81 | @include last; 82 | } -------------------------------------------------------------------------------- /sass/partials/_z.scss: -------------------------------------------------------------------------------- 1 | $z-top: 9999; 2 | 3 | $z-highest: 100; 4 | $z-high: 50; 5 | $z-ground: 0; 6 | $z-low: -50; 7 | $z-lowest: -100; 8 | 9 | $z-bottom: -9999; -------------------------------------------------------------------------------- /survey_fields.md: -------------------------------------------------------------------------------- 1 | # Required Fields 2 | 3 | __`org_years`__ 4 | *Recommended type:* `slider` 5 | Number of years at the organization 6 | 7 | __`management_layers`__ 8 | *Recommended type:* `slider` 9 | Number of layers of management between [you] and [your] CEO 10 | 11 | __`org_position`__ 12 | *Recommended type:* `radio` 13 | Organization's position in the market 14 | 15 | __`team`__ 16 | *Recommended type:* `teamselect` 17 | Team and/or department do [you] work in 18 | 19 | __Verbatims__ 20 | *Recommended type:* `textarea` 21 | `purpose_verbatims` 22 | `people_verbatims` 23 | `process_verbatims` 24 | `product_verbatims` 25 | `platform_verbatims` 26 | 27 | __Purpose__ 28 | *Required type:* `gridselect` 29 | *Options:* 30 | `PuN`: *x* Serve Networks 31 | `PuA`: *x* Distribute Authority 32 | `PuS`: *x* Seek Simplicity 33 | `PuI`: *x* Process Information 34 | `PuD`: *x* Encourage Divergence 35 | `PuR`: *x* Enable Crossover 36 | `PuT`: *x* Embrace Uncertainty 37 | 38 | __People__ 39 | *Required type:* `gridselect` 40 | *Options:* 41 | `PeN`: *x* Serve Networks 42 | `PeA`: *x* Distribute Authority 43 | `PeS`: *x* Seek Simplicity 44 | `PeI`: *x* Process Information 45 | `PeD`: *x* Encourage Divergence 46 | `PeR`: *x* Enable Crossover 47 | `PeT`: *x* Embrace Uncertainty 48 | 49 | __Process__ 50 | *Required type:* `gridselect` 51 | *Options:* 52 | `PrN`: *x* Serve Networks 53 | `PrA`: *x* Distribute Authority 54 | `PrS`: *x* Seek Simplicity 55 | `PrI`: *x* Process Information 56 | `PrD`: *x* Encourage Divergence 57 | `PrR`: *x* Enable Crossover 58 | `PrT`: *x* Embrace Uncertainty 59 | 60 | __Product__ 61 | *Required type:* `gridselect` 62 | *Options:* 63 | `PdN`: *x* Serve Networks 64 | `PdA`: *x* Distribute Authority 65 | `PdS`: *x* Seek Simplicity 66 | `PdI`: *x* Process Information 67 | `PdD`: *x* Encourage Divergence 68 | `PdR`: *x* Enable Crossover 69 | `PdT`: *x* Embrace Uncertainty 70 | 71 | __Platform__ 72 | *Required type:* `gridselect` 73 | *Options:* 74 | `PlN`: *x* Serve Networks 75 | `PlA`: *x* Distribute Authority 76 | `PlS`: *x* Seek Simplicity 77 | `PlI`: *x* Process Information 78 | `PlD`: *x* Encourage Divergence 79 | `PlR`: *x* Enable Crossover 80 | `PlT`: *x* Embrace Uncertainty 81 | -------------------------------------------------------------------------------- /tasks/build.js: -------------------------------------------------------------------------------- 1 | module.exports = function( gulp, plugins ) { 2 | gulp.task( 'build', [ 3 | 'bundle:dev', 4 | 'styles' 5 | ] ); 6 | }; 7 | -------------------------------------------------------------------------------- /tasks/bundle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Compile JavaScript with Browserify. 3 | * 4 | * --------------------------------------------------------------- 5 | * 6 | * Compiles the js bundle from `src/js` into a single file and places 7 | * them into `build/js` directory. 8 | * 9 | */ 10 | var path = require( 'path' ); 11 | var source = require( 'vinyl-source-stream' ); 12 | var buffer = require( 'vinyl-buffer' ); 13 | var watchify = require( 'watchify' ); 14 | var browserify = require( 'browserify' ); 15 | 16 | module.exports = function( gulp, plugins, path ) { 17 | var errorHandler = function( err ) { 18 | plugins.notify.onError( { 19 | message: "<%= error.message %>" 20 | } ).apply( this, arguments ); 21 | 22 | this.emit( 'end' ); 23 | }; 24 | 25 | var bundles = [ 26 | { 27 | entryPoint: './app.js', 28 | require: [], 29 | output: 'bundle.js', 30 | external: [ 'jquery' ] 31 | }, 32 | { 33 | entryPoint: './admin.js', 34 | require: [], 35 | output: 'admin.js', 36 | external: [ 'jquery' ] 37 | }, 38 | { 39 | entryPoint: './form.js', 40 | require: [], 41 | output: 'form.js', 42 | external: [ 'jquery' ] 43 | }, 44 | { 45 | entryPoint: './shared.js', 46 | require: [ 'jquery' ], 47 | output: 'shared.js', 48 | external: [] 49 | } 50 | ]; 51 | 52 | var args = watchify.args; 53 | args.debug = true; 54 | args.basedir = path.resolve( __dirname, '../client/' ); 55 | args.fullPaths = false; 56 | args.insertGlobals = true; 57 | 58 | var bundlers = {}; 59 | 60 | bundles.forEach( function( b ) { 61 | var bundler = browserify( b.entryPoint, args ); 62 | // add any other browserify options or transforms here 63 | bundler.transform( 'brfs' ); 64 | bundler.transform( 'bulkify' ); 65 | 66 | if( b.require ) { 67 | b.require.forEach( function( r ) { 68 | bundler.require( r ); 69 | } ); 70 | } 71 | 72 | if( b.external ) { 73 | b.external.forEach( function( e ) { 74 | bundler.external( e ); 75 | } ); 76 | } 77 | 78 | bundlers[ b.output ] = bundler; 79 | } ); 80 | 81 | gulp.task( 'bundle:dev', function() { 82 | bundles.forEach( function( b ) { 83 | bundle( b )(); 84 | } ); 85 | } ); // so you can run `gulp bundle:dev` to build the file 86 | gulp.task( 'bundle:watch', function() { 87 | bundles.forEach( function( b ) { 88 | var watcher = watchify( bundlers[ b.output ] ); 89 | watcher.on( 'update', bundle( b ) ); // on any dep update, runs the bundler 90 | } ); 91 | } ); 92 | 93 | function bundle( b ) { 94 | return function() { 95 | bundlers[ b.output ].bundle() 96 | // log errors if they happen 97 | .on( 'error', errorHandler ) 98 | .pipe( source( b.output ) ) 99 | // optional, remove if you dont want sourcemaps 100 | .pipe( buffer() ) 101 | .pipe( plugins.sourcemaps.init( { loadMaps: true } ) ) // loads map from browserify file 102 | .pipe( plugins.sourcemaps.write( './' ) ) // writes .map file 103 | // 104 | .pipe( gulp.dest( './public/js' ) ) 105 | .pipe( plugins.notify( { message: b.output + ': browserify bundler task complete', onLast: true } ) ); 106 | }; 107 | } 108 | }; 109 | -------------------------------------------------------------------------------- /tasks/default.js: -------------------------------------------------------------------------------- 1 | module.exports = function( gulp, plugins ) { 2 | gulp.task( 'default', [ 3 | 'build', 4 | 'watch', 5 | 'serve:dev' 6 | ] ); 7 | }; 8 | -------------------------------------------------------------------------------- /tasks/js-lint.js: -------------------------------------------------------------------------------- 1 | module.exports = function( gulp, plugins ) { 2 | gulp.task( 'js-lint', function() { 3 | gulp.src( [ 4 | 'controllers/**/*.js', 5 | 'lib/**/*.js', 6 | 'models/**/*.js' 7 | ] ).pipe( plugins.jshint() ); 8 | } ); 9 | }; 10 | -------------------------------------------------------------------------------- /tasks/scss-lint.js: -------------------------------------------------------------------------------- 1 | module.exports = function( gulp, plugins ) { 2 | var colors = plugins.util.colors; 3 | 4 | var linterErrors = function( file ) { 5 | if( !file.scsslint.success ) { 6 | plugins.util.log(colors.red(file.scsslint.issues.length + ' issues found in ' + file.path)); 7 | 8 | file.scsslint.issues.forEach(function (issue) { 9 | var severity = issue.severity === 'warning' ? 'W' : 'E'; 10 | var logMsg = colors.cyan(file.path) + ':' + colors.magenta(issue.line) + ' [' + severity + '] ' + issue.reason; 11 | 12 | plugins.util.log(logMsg); 13 | }); 14 | } else { 15 | plugins.util.log(colors.green('Linting passed for ' + file.path)); 16 | } 17 | }; 18 | 19 | gulp.task( 'scss-lint', function() { 20 | gulp.src( [ './src/styles/**/*.scss', '!./src/styles/vendor/**/*' ] ) 21 | .pipe( plugins.scssLint( { 22 | 'config': '.scss-lint.yml', 23 | 'customReport': linterErrors 24 | } ) ); 25 | } ); 26 | }; -------------------------------------------------------------------------------- /tasks/serve.js: -------------------------------------------------------------------------------- 1 | var extend = require( 'util' )._extend; 2 | 3 | module.exports = function( gulp, plugins, path ) { 4 | var options = { 5 | script: 'app.js', 6 | ext: [ 'js' ], 7 | ignore: [ 8 | path.resolve( __dirname, '../client/**' ), 9 | path.resolve( __dirname, '../public/**' ), 10 | path.resolve( __dirname, '../tasks/**/*' ) 11 | ] 12 | }; 13 | 14 | gulp.task( 'serve:dev', function () { 15 | plugins.nodemon( extend( options, { env: { 'NODE_ENV': 'development' } } ) ); 16 | } ); 17 | 18 | gulp.task( 'serve:stage', function () { 19 | plugins.nodemon( extend( options, { env: { 'NODE_ENV': 'staging' } } ) ); 20 | } ); 21 | 22 | gulp.task( 'serve:prod', function () { 23 | plugins.nodemon( extend( options, { env: { 'NODE_ENV': 'production' } } ) ); 24 | } ); 25 | }; 26 | -------------------------------------------------------------------------------- /tasks/style.js: -------------------------------------------------------------------------------- 1 | //this will handle errors for us 2 | var handleError = function( err ) { 3 | console.log( err.toString() ); 4 | this.emit( 'end' ); 5 | }; 6 | 7 | module.exports = function( gulp, plugins ) { 8 | //concat, minify css 9 | gulp.task( 'styles', function() { 10 | gulp.src(['./sass/**/*.scss']) 11 | .pipe( plugins.sass( { 12 | sourcemap: true, 13 | sourcemapPath: 'src/styles' 14 | } ) ) 15 | .on( 'error', handleError ) 16 | .pipe( plugins.csso() ) 17 | .pipe( plugins.autoprefixer() ) 18 | .pipe( gulp.dest( './public/css' ) ) 19 | .pipe( plugins.notify( { message: 'scss compilation complete', onLast: true } ) ); 20 | } ); 21 | }; -------------------------------------------------------------------------------- /tasks/watch.js: -------------------------------------------------------------------------------- 1 | module.exports = function( gulp, plugins ) { 2 | gulp.task( 'watch:styles', function() { 3 | gulp.watch( 'sass/**/*.scss', [ 'scss-lint', 'styles' ] ); 4 | } ); 5 | 6 | gulp.task( 'watch:js-lint', function() { 7 | gulp.watch( [ 8 | 'controllers/**/*.js', 9 | 'lib/**/*.js', 10 | 'models/**/*.js' 11 | ], [ 'js-lint' ] ); 12 | } ); 13 | 14 | gulp.task( 'watch', [ 15 | 'watch:styles', 16 | 'watch:js-lint', 17 | 'bundle:watch' 18 | ] ); 19 | }; 20 | -------------------------------------------------------------------------------- /views/404.jade: -------------------------------------------------------------------------------- 1 | extends /layouts/base 2 | 3 | block content 4 | main 5 | section 6 | .row 7 | .card.card-full.card--spacious.type-center 8 | .card-body 9 | .card-title Page not found. 10 | p Sorry, we couldn't find what you were looking for. Go back home. 11 | -------------------------------------------------------------------------------- /views/admin/account.jade: -------------------------------------------------------------------------------- 1 | extends /layouts/auth_base 2 | 3 | include /partials/formcomponents 4 | 5 | block content 6 | main 7 | section 8 | .row 9 | .card.card-full.card--spacious 10 | .card-body 11 | .card-row 12 | +form-group(true)(action=("/admin/user/" + accountUser.id)) 13 | h3 Basic Information 14 | +input-text("name", null, accountUser.profile.name || "", "Name", true)(required="") 15 | +input-text("organization", null, accountUser.profile.organization || "", "Organization", true)(required="") 16 | +input-email("email", null, accountUser.email, "E-mail", true)(required="") 17 | +input-password("password", null, "", "Password", true) 18 | input(type="hidden", name="_csrf", value=csrfToken) 19 | +form-buttons 20 | .card-row 21 | +form-group(true)(action=("/admin/user/" + accountUser.id)) 22 | h3 Account State 23 | +input-select("activated", {"1": "Yes", "0": "No"}, "Activated", accountUser.activated ? "1" : "0") 24 | div   25 | +input-select("roles", {"admin": "Admin", "user": "User"}, "Role", accountUser.roles.indexOf( 'admin' ) > -1 ? "admin" : "user") 26 | input(type="hidden", name="_csrf", value=csrfToken) 27 | +form-buttons -------------------------------------------------------------------------------- /views/admin/users.jade: -------------------------------------------------------------------------------- 1 | extends /layouts/auth_base 2 | 3 | block content 4 | main 5 | section 6 | .row 7 | .card.card-full.card-table 8 | .card-body 9 | table.table--list 10 | thead 11 | tr 12 | th User Count 13 | th Response Count 14 | th Average Score 15 | tbody 16 | tr 17 | td= totals.users 18 | td= totals.responses 19 | td= (totals.score * 100 ).toFixed( 2 ) + '%' 20 | .row 21 | .card.card-full.card-table 22 | .card-body 23 | table.table--list.sortable 24 | thead 25 | tr 26 | th(class="sorting-asc" data-sort="string-ins", data-sort-default="asc", data-sort-dir="asc") Email 27 | th(data-sort="string-ins", data-sort-default="desc") Name 28 | th(data-sort="string-ins", data-sort-default="desc") Organization 29 | th(data-sort="string-ins", data-sort-default="desc") Activated 30 | tbody 31 | each user in userList 32 | tr 33 | td 34 | a(href='/admin/user/' + user.id, title=user.email)= user.email 35 | td 36 | span= user.profile.name || '' 37 | td 38 | span= user.profile.organization || '' 39 | td 40 | span= user.activated ? 'Yes' : 'No' -------------------------------------------------------------------------------- /views/email/activated/activated.html.jade: -------------------------------------------------------------------------------- 1 | include ../buttons 2 | extends ../layout 3 | 4 | block title 5 | h1 Your account has been activated. 6 | 7 | block content 8 | p Thank you for activating your account on Responsive Pulse. You can now login with your username #{to}. 9 | 10 | block actions 11 | +email-button('Sign in', baseUrl + '/signin') 12 | -------------------------------------------------------------------------------- /views/email/buttons.jade: -------------------------------------------------------------------------------- 1 | mixin email-button(text, url) 2 |
3 | 8 | a.button(href=url, style="-webkit-font-smoothing:antialiased;background-color:#5998E1;border-radius:4px;color:#ffffff;display:inline-block;font-family:sans-serif;font-size:13px;font-weight:bold;line-height:40px;text-align:center;text-decoration:none;width:auto;padding: 0 25px;-webkit-text-size-adjust:none;")= text 9 | 13 |
14 | -------------------------------------------------------------------------------- /views/email/forgotpassword/forgotpassword.html.jade: -------------------------------------------------------------------------------- 1 | include ../buttons 2 | extends ../layout 3 | 4 | block title 5 | h1 Password Reset 6 | 7 | block content 8 | if name 9 | p Hi #{name}, 10 | p You've recently asked us to reset the password for your account at #{to}. To complete this process, please click the button below. 11 | p If you didn't make this request, please feel free to ignore this email. 12 | 13 | block actions 14 | if activated 15 | +email-button("Reset My Password", baseUrl + '/reset_password/' + key) 16 | else 17 | +email-button("Set My Password", baseUrl + '/activate?email=' + encodeURIComponent(to) + '&key=' + key) -------------------------------------------------------------------------------- /views/email/layout.jade: -------------------------------------------------------------------------------- 1 | 2 | html 3 | head 4 | meta(http-equiv="Content-Type", content="text/html; charset=utf-8") 5 | meta(name="viewport", content="width=device-width") 6 | style(type="text/css") 7 | include style.css 8 | body 9 | table.body(cellpadding="0", cellspacing="0") 10 | tr 11 | td.header-gradient(bgcolor="#e6bbb6", style="background-color: #e6bbb6;height: 15px;background-image: url(http://i.imgur.com/rGNZrUx.png);background-size: 100%;background-image: -webkit-gradient(linear,0 50%,100% 50%,color-stop(0%,#cfd8ff),color-stop(100%,#ff9b65));background-image: -webkit-linear-gradient(left,#cfd8ff,#ff9b65);background-image: -webkit-linear-gradient(left, #cfd8ff, #ff9b65);background-image: linear-gradient(to right,#cfd8ff,#ff9b65);padding: 0;line-height: 0;margin: 0;") 12 | 17 |
18 |
19 | 23 | tr 24 | td.center 25 | table.container 26 | tr 27 | td 28 | a(href="http://pulse.responsive.org") 29 | img.logo(alt="Responsive Pulse", width="206", height="80" src="http://i.imgur.com/0lUWCu0.png") 30 | tr 31 | td.box-hold 32 | table 33 | tr 34 | td.box 35 | block title 36 | block content 37 | .actions 38 | block actions 39 | tr 40 | td.footer An Undercurrent Platform 41 | tr 42 | td.spacer -------------------------------------------------------------------------------- /views/email/logo.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /views/email/registration/registration.html.jade: -------------------------------------------------------------------------------- 1 | include ../buttons 2 | extends ../layout 3 | 4 | block title 5 | h1 Your account has been created. 6 | 7 | block content 8 | p Thank you for creating an account with Responsive Pulse. You'll need to activate your account before you can sign in. 9 | 10 | block actions 11 | +email-button('Activate', baseUrl + '/reg_activate?email=' + encodeURIComponent(to) + '&key=' + key) 12 | -------------------------------------------------------------------------------- /views/email/surveysharedhasaccount/surveysharedhasaccount.html.jade: -------------------------------------------------------------------------------- 1 | include ../buttons 2 | extends ../layout 3 | 4 | block title 5 | h1 Survey shared with you 6 | 7 | block content 8 | if invitee 9 | p Hi #{invitee}, 10 | p #{inviter} has added you as a#{role.match(/^[aeiou]/) ? 'n' : ''} #{role} on their #{survey.title} survey. Click the button below to view. 11 | 12 | block actions 13 | +email-button('View Survey', baseUrl + '/view/' + survey.key) 14 | -------------------------------------------------------------------------------- /views/email/surveysharednoaccount/surveysharednoaccount.html.jade: -------------------------------------------------------------------------------- 1 | include ../buttons 2 | extends ../layout 3 | 4 | block title 5 | h1 Survey shared with you 6 | 7 | block content 8 | p Hi there, 9 | p 10 | | #{inviter} 11 | if organization 12 | | at #{organization} 13 | | has added you as a#{role.match(/^[aeiou]/) ? 'n' : ''} #{role} on their #{survey.title} survey. Click the button below to create an account and start collaborating! 14 | 15 | block actions 16 | +email-button('Get Started', baseUrl + '/activate?email=' + encodeURIComponent(email) + '&key=' + inviteeKey) 17 | -------------------------------------------------------------------------------- /views/form-welcome.jade: -------------------------------------------------------------------------------- 1 | extends layouts/base 2 | include partials/formcomponents 3 | 4 | block content 5 | main 6 | section 7 | .row 8 | .card.card-full 9 | .card-body 10 | +progress(page, totalPages, null, 'Welcome') 11 | form.form--group.form(method="post") 12 | .form--group 13 | if welcomeMessage 14 | p!= welcomeMessage 15 | else 16 | p Today's largest and most important organizations face two critical challenges: First, the world around them is changing faster than ever, and second, their own scale and complexity are working against them. To be successful in the future, we, as other organizations, need to strive to become more responsive – responsive to our customers, culture, competition, technology, regulation, and all other forces of disruption. 17 | p 18 | | To help us think about the future in a careful, deliberate way as we map out our vision for the future, we’re administering a brief survey to determine how responsive we are as an organization today. We think of it as taking our company’s ‘pulse.’ 19 | strong The survey is fully anonymous 20 | | and will help us understand our strengths, biases, values, capabilities, and patterns of working. 21 | p 22 | | Our understanding requires everyone’s participation and honest input. So please participate, 23 | em it only takes five minutes to complete. 24 | .form--group 25 | input(type="hidden", name="_csrf", value=csrfToken) 26 | button.button.button--primary(type="submit") Begin 27 | +progress(page, totalPages, true) 28 | 29 | append scripts 30 | script(src="/js/form.js") -------------------------------------------------------------------------------- /views/form.jade: -------------------------------------------------------------------------------- 1 | extends layouts/base 2 | include partials/formcomponents 3 | 4 | block content 5 | main 6 | section 7 | .row 8 | .card.card-full 9 | .card-body 10 | +progress(page, totalPages) 11 | .card-intro!= template.header || '' 12 | form.form--group.form(method="post") 13 | for field in template.fields 14 | .form--group 15 | case field.type 16 | when "text" 17 | +input-text(field.name, field.placeholder, form[ field.name ] || field.default || '', fillIn(field.label, surveyValues), false, true)(required="") 18 | when "textarea" 19 | +input-textarea(field.name, field.placeholder, form[ field.name ] || field.default || '', fillIn(field.label, surveyValues)) 20 | when "email" 21 | +input-email(field.name, field.placeholder, form[ field.name ] || field.default || '', fillIn(field.label, surveyValues))(required="") 22 | when "radio" 23 | +input-radio(field.name, field.options, fillIn(field.label, surveyValues), form[ field.name ])(required="") 24 | when "select" 25 | +input-select(field.name, field.options, fillIn(field.label, surveyValues), form[ field.name ], true) 26 | when "teamselect" 27 | +input-teamselect(field.name, teams, fillIn(field.label, surveyValues), form[ field.name ]) 28 | when "checkbox" 29 | if field.options 30 | +input-checkbox(field.name, field.options, fillIn(field.label, surveyValues), form[ field.name ]) 31 | else 32 | +input-single-checkbox(field.name, field.value || '1', fillIn(field.label, surveyValues), form[ field.name ]) 33 | when "gridselect" 34 | +input-gridselect(field.name, field.options, form)(required="") 35 | when "slider" 36 | +input-slider(field.name, fillIn(field.label, surveyValues), field.min.value, field.max.value, field.min.label, field.max.label, form[ field.name ], true) 37 | default 38 | .form--element-label Not yet implemented. 39 | .form--group 40 | input(type="hidden", name="_csrf", value=csrfToken) 41 | if page > 1 42 | a(href="./" + (page - 1)).button Back 43 | button.button.button--primary(type="submit")= (page == totalPages ? 'Submit' : 'Next') 44 | +progress(page, totalPages, true) 45 | 46 | append scripts 47 | script(src="/js/form.js") 48 | -------------------------------------------------------------------------------- /views/forms.jade: -------------------------------------------------------------------------------- 1 | extends layouts/base 2 | 3 | include partials/formcomponents 4 | 5 | block content 6 | main 7 | section 8 | .row 9 | .card.card-full 10 | .card-body 11 | h2.card-title Form Elements 12 | .form 13 | .form--group 14 | label.form--element.form--element-text 15 | span.form--element-label Text Input 16 | input.form--element-control(type="text", placeholder="Placeholder") 17 | label.form--element.form--element-textarea 18 | span.form--element-label Text Area 19 | textarea.form--element-control 20 | .form--group 21 | .form--element-label Radio Buttons 22 | .form--element.form--element-radio 23 | input(type="radio", name="radio", value="r1").form--element-control#r1 24 | label(for="r1").span.form--element-value Radio Choice 1 25 | .form--element.form--element-radio 26 | input(type="radio", name="radio", value="r2").form--element-control#r2 27 | label(for="r2").span.form--element-value Radio Choice 2 28 | .form--group 29 | .form--element-label Checkboxes 30 | .form--element.form--element-checkbox 31 | input(type="checkbox", name="check", value="c1").form--element-control#c1 32 | label(for="c1").span.form--element-value Checkbox 1 33 | .form--element.form--element-checkbox 34 | input(type="checkbox", name="check", value="c2").form--element-control#c2 35 | label(for="c2").span.form--element-value Checkbox 2 36 | .form--group 37 | .form--element-label Sliders 38 | +input-slider("slider", 0, 50, "Low", "High") 39 | .form--group 40 | label.form--element.form--element-select 41 | span.form--element-label Select Box 42 | select.form--element-control 43 | option(selected) Option 1 44 | option Option 2 45 | option Option 3 46 | .form--group 47 | span.form--element-label Grid Select 48 | +input-gridselect("Grid Select", { 49 | "gs1": "Our purpose serves and mobilizes a community of users and partners.", 50 | "gs2": "Our purpose empowers my decision making." 51 | }) 52 | .form--group 53 | span.form--element-label Buttons 54 | a(href="#").button Default Button 55 | a(href="#").button.button--primary Primary Button 56 | a(href="#").button.button--danger Danger Button 57 | 58 | block require 59 | | require(['main']); 60 | -------------------------------------------------------------------------------- /views/landing.jade: -------------------------------------------------------------------------------- 1 | extends layouts/base 2 | 3 | block content 4 | main 5 | .hero 6 | .hero--inner 7 | .hero--text 8 | h1 Build a better organization. 9 | p Get your team ready for the 21st century.
10 | | Find the changes that will make the quickest impact.
11 | | Track changes towards responsiveness. 12 | .hero--cta 13 | a.button.button--action(href="/signup") Get started — it's free! 14 | section 15 | .card.card-full.card--spacious 16 | .card-body 17 | h1.title.title-xlarge Is your organization prepared for the 21st century? 18 | p.copy-large The organizations we have today are unfit for the next century. Rigid hierarchies, inflexible matrix reporting structures, meeting-rich schedules, and decision-rights for only the most tenured members – these “features” prevent any real work from getting done. 19 | p.copy-large We’ve been studying groups that work in a new way and as a result have a bigger impact on the world than their competitors. We’ve been using this tool for that research and for our paying clients. It’s now available for free. Find out how your organization measures up. 20 | section.landing-screenshots 21 | .row 22 | .landing-screenshot 23 | img(src="/img/screenshots/dashboard.png") 24 | .landing-screenshot 25 | img(src="/img/screenshots/survey.png") 26 | .landing-screenshot 27 | img(src="/img/screenshots/results.png") 28 | section 29 | .card.card-full.card--spacious 30 | .card-body 31 | h1.title.title-xlarge Measuring Up. 32 | p.copy-large Administering the Responsive OS Pulse is easy. Once you create an account you'll be given a link you can distribute to your teams. Results can are analyzed in real-time and along a variety of dimensions. 33 | p.copy-large Intoning the principles measured by Responsive Pulse affords an organization greater responsiveness to competition, culture, technology, and all other forms of disruption. We believe these principles – and their associated implications on ways of working – dramatically improve even the largest and most traditional institutions – for employees, for customers, and for shareholders. 34 | p.sub-title Your data will never be shared, sold, or made public. We promise to anonymize it if it's used to establish benchmarks or improve the tool's accuracy. 35 | -------------------------------------------------------------------------------- /views/layouts/auth_base.jade: -------------------------------------------------------------------------------- 1 | extends base 2 | 3 | prepend scripts 4 | if survey 5 | script| var survey = '#{survey.key}'; 6 | script(src="/js/bundle.js") -------------------------------------------------------------------------------- /views/layouts/base.jade: -------------------------------------------------------------------------------- 1 | - function numberWithCommas(x) { 2 | - return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); 3 | - } 4 | doctype html 5 | html 6 | head 7 | meta(charset='utf-8') 8 | meta(name='description', content='') 9 | meta(name='HandheldFriendly', content='True') 10 | meta(name='MobileOptimized', content='320') 11 | meta(name='viewport', content='width=device-width, initial-scale=1, minimal-ui') 12 | meta(http-equiv='cleartype', content='on') 13 | title Responsive OS Pulse — An Undercurrent Product 14 | //- Fonts 15 | link(href='http://fonts.googleapis.com/css?family=Merriweather:400,400italic,700,700italic&subset=latin,latin-ext', rel='stylesheet' type='text/css') 16 | //- FontAwesome 17 | link(href='//maxcdn.bootstrapcdn.com/font-awesome/4.3.0/css/font-awesome.min.css', rel='stylesheet') 18 | link(rel='stylesheet', href='/css/main.css') 19 | //- Pingdom Monitoring 20 | script. 21 | var _prum = [['id', '54aac1d6abe53dd673f771ae'], 22 | ['mark', 'firstbyte', (new Date()).getTime()]]; 23 | (function() { 24 | var s = document.getElementsByTagName('script')[0] 25 | , p = document.createElement('script'); 26 | p.async = 'async'; 27 | p.src = '//rum-static.pingdom.net/prum.min.js'; 28 | s.parentNode.insertBefore(p, s); 29 | })(); 30 | block head 31 | body(class=classes.join(' ')) 32 | include /partials/cookie-error 33 | include /partials/nav 34 | 35 | block content 36 | 37 | footer.footer 38 | .footer--credit 39 | | © Undercurrent LLC, 2015. 40 | .footer--legal 41 | a(href="/terms") Terms of Use 42 | 43 | script(src='/js/shared.js') 44 | block scripts 45 | 46 | script. 47 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 48 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 49 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 50 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 51 | 52 | ga('create', 'UA-59741696-1', 'auto'); 53 | ga('send', 'pageview'); 54 | -------------------------------------------------------------------------------- /views/partials/cookie-error.jade: -------------------------------------------------------------------------------- 1 | #cookies 2 | p Cookies don't seem to be enabled in your browser. To use the Pulse, you must enable cookies. 3 | p 4 | | To learn how to manage your cookies, please visit your web browser's help center: 5 | a(href="https://support.google.com/chrome/answer/95647?hl=en", target="_blank") Chrome 6 | | , 7 | a(href="http://windows.microsoft.com/en-us/internet-explorer/delete-manage-cookies#ie=ie-11", target="_blank") Internet Explorer 8 | | , 9 | a(href="https://support.mozilla.org/en-US/kb/cookies-information-websites-store-on-your-computer", target="_blank") Firefox 10 | | , 11 | a(href="https://support.apple.com/kb/PH19214?viewlocale=en_US&locale=en_US", target="_blank") Safari -------------------------------------------------------------------------------- /views/partials/formcomponents.jade: -------------------------------------------------------------------------------- 1 | include formcomponents/email 2 | include formcomponents/password 3 | include formcomponents/text 4 | include formcomponents/textarea 5 | include formcomponents/select 6 | include formcomponents/radio 7 | include formcomponents/checkbox 8 | include formcomponents/single-checkbox 9 | include formcomponents/form 10 | include formcomponents/gridselect 11 | include formcomponents/progress 12 | include formcomponents/slider 13 | include formcomponents/teamselect -------------------------------------------------------------------------------- /views/partials/formcomponents/checkbox.jade: -------------------------------------------------------------------------------- 1 | mixin input-checkbox(name, options, label, value) 2 | if label 3 | .form--element-label= label 4 | each val, key in options 5 | label.form--element.form--element-checkbox 6 | input.form--element-control(type="checkbox", name=name, value=key, checked=(value == key ? true : false)) 7 | .form--element-value=val -------------------------------------------------------------------------------- /views/partials/formcomponents/email.jade: -------------------------------------------------------------------------------- 1 | mixin input-email(name, placeholder, value, label, readonly) 2 | if messages.error 3 | - errors = messages.error[0]; 4 | if typeof errors[name] !== 'undefined' 5 | - error = errors[name] 6 | else 7 | - error = undefined 8 | 9 | .form--area 10 | label.form--element.form--element-text(class=(error ? "form--element-invalid" : "")) 11 | if label 12 | .form--element-label= label 13 | if readonly 14 | +form-change-button() 15 | input.form--element-control(type="email", name=name, data-parsley-trigger="change", value=value, placeholder=placeholder, readonly=(readonly == true ? "readonly" : undefined))&attributes(attributes) 16 | if error 17 | .form--area-errors 18 | ul.form--element-invalid-message.filled 19 | li.parsley-required= error 20 | else 21 | .form--area-errors 22 | -------------------------------------------------------------------------------- /views/partials/formcomponents/form.jade: -------------------------------------------------------------------------------- 1 | mixin form-group(readonly) 2 | form.form--group(class=(readonly ? "form--readonly" : undefined), method="POST")&attributes(attributes) 3 | block 4 | 5 | mixin form-buttons() 6 | .form-row 7 | .form-row-button 8 | button.button.button--clear-changes Clear Changes 9 | .form-row-button.last 10 | button.button.button--primary Save Changes 11 | .form-feedback 12 | .form-submitted-success-label 13 | i.fa.fa-check 14 | span Changes saved! 15 | .form-submitted-fail-label 16 | i.fa.fa-times 17 | span Could not save changes. Please try again. 18 | 19 | mixin form-change-button() 20 | .form--element-status 21 | span.form--element-edited-label edited 22 | button.button--form-change change -------------------------------------------------------------------------------- /views/partials/formcomponents/password.jade: -------------------------------------------------------------------------------- 1 | mixin input-password(name, placeholder, value, label, readonly) 2 | if readonly 3 | .form--element.form--password 4 | .form--password-old 5 | label.form--element.form--element-text 6 | if label 7 | .form--element-label= label 8 | +form-change-button() 9 | .form--element-dummy.form--element-control(readonly=(readonly == true ? "readonly" : undefined))&attributes(attributes) •••••••• 10 | .form--password-confirm 11 | label.form--element.form--element-text 12 | .form--element-label Old Password 13 | input.form--element-control(type="password", name=name + "_old")&attributes(attributes) 14 | .form--password-reveal 15 | i.fa.fa-eye 16 | label.form--element.form--element-text 17 | .form--element-label New Password 18 | input.form--element-control(type="password", name=name)&attributes(attributes) 19 | .form--password-reveal 20 | i.fa.fa-eye 21 | else 22 | label.form--element.form--element-text 23 | if label 24 | .form--element-label= label 25 | input.form--element-control(type="password", name=name, value=value, placeholder=placeholder, readonly=(readonly == true ? "readonly" : undefined))&attributes(attributes) -------------------------------------------------------------------------------- /views/partials/formcomponents/progress.jade: -------------------------------------------------------------------------------- 1 | mixin progress(page, totalPages, nav, title) 2 | .progress(class=nav ? " progress-nav" : "") 3 | .progress-bar 4 | .progress-bar-completed(style="width:" + (page / totalPages * 100) + "%") 5 | if nav 6 | .progress-pagination 7 | .progress-pagination-first 8 | span.pagination-link First 9 | .progress-pagination-last 10 | span.pagination-link Last 11 | .progress-pagination-center 12 | span="Page " + page + " of " + totalPages 13 | else 14 | .progress-label 15 | ="Page " + page + " of " + totalPages 16 | if template 17 | = template.title ? ": " + template.title : "" 18 | else if title 19 | = ": " + title 20 | -------------------------------------------------------------------------------- /views/partials/formcomponents/radio.jade: -------------------------------------------------------------------------------- 1 | mixin input-radio(name, options, label, value) 2 | if messages.error 3 | - errors = messages.error[0]; 4 | if typeof errors[name] !== 'undefined' 5 | - error = errors[name] 6 | else 7 | - error = undefined 8 | 9 | .form--area.form--area-stacked 10 | if label 11 | .form--element-label= label 12 | .form--element-radio-options 13 | each val, key in options 14 | label.form--element.form--element-radio 15 | input.form--element-control(type="radio", name=name, value=key, checked=((messages.field && messages.field[0][name] == key) || (value && value == key) ? true : false))&attributes(attributes) 16 | .form--element-value=val 17 | if error 18 | .form--area-errors 19 | ul.form--element-invalid-message.filled 20 | li.parsley-required= error 21 | else 22 | .form--area-errors 23 | -------------------------------------------------------------------------------- /views/partials/formcomponents/select.jade: -------------------------------------------------------------------------------- 1 | mixin input-select-option(name, key, val, value) 2 | option(value=key, selected=((messages.field && messages.field[0][name] == key) || (value && value == key) ? true : false))=val 3 | 4 | mixin input-select(name, options, label, value, stacked) 5 | .form--area(class=(stacked == true ? "form--area-stacked" : "")) 6 | label.form--element.form--element-select(class=(stacked == true ? "form--element-select-stacked" : "")) 7 | if label 8 | .form--element-label= label 9 | select.form--chosen-select.form--element-control(name=name)&attributes(attributes) 10 | if options instanceof Array 11 | each val in options 12 | +input-select-option(name, val, val, value) 13 | else 14 | each val, key in options 15 | +input-select-option(name, key, val, value) 16 | -------------------------------------------------------------------------------- /views/partials/formcomponents/single-checkbox.jade: -------------------------------------------------------------------------------- 1 | mixin input-single-checkbox(name, value, label, stateValue) 2 | .form--area 3 | if label 4 | label.form--element.form--element-checkbox 5 | input.form--element-control(type="checkbox", name=name, value=value, checked=(stateValue == value ? true : false)) 6 | .form--element-value=label 7 | -------------------------------------------------------------------------------- /views/partials/formcomponents/slider.jade: -------------------------------------------------------------------------------- 1 | mixin input-slider(name, label, min, max, minLabel, maxLabel, value, stacked) 2 | .form--area(class=(stacked == true ? "form--area-stacked" : "")) 3 | label.form--element.form--element-slider 4 | if label 5 | .form--element-label= label 6 | .form--element-control 7 | if minLabel || maxLabel 8 | .rangeslider-labels 9 | .rangeslider-label-min=minLabel 10 | .rangeslider-label-max=maxLabel 11 | .range-value 12 | .range-value-label= min 13 | .range-value-arrow 14 | input(type="range", name=name, min=min, max=max, value=(value ? value : min))&attributes(attributes) 15 | -------------------------------------------------------------------------------- /views/partials/formcomponents/teamselect.jade: -------------------------------------------------------------------------------- 1 | mixin input-teamselect(name, teams, label, value) 2 | .form--area.form--area-stacked 3 | if !teams || teams.length === 0 4 | label.form--element.form--element-text.form--element-text-stacked.form--element-teamselect 5 | if label 6 | .form--element-label= label 7 | input(type="text", name="team").form--element-control 8 | else 9 | label.form--element.form--element-teamselect 10 | if label 11 | .form--element-label= label 12 | select.form--element-control(name="team")&attributes(attributes).teamselect--dropdown.is-shown 13 | each val in teams 14 | option(value=val, selected=((messages.field && messages.field[0][name] == val) || (value && value == val) ? true : false))=val 15 | option(value="-" disabled) ------------- 16 | option(value="newteam") Add Team 17 | input(type="text").form--element-control.teamselect--text -------------------------------------------------------------------------------- /views/partials/formcomponents/text.jade: -------------------------------------------------------------------------------- 1 | mixin input-text(name, placeholder, value, label, readonly, stacked) 2 | if messages.fieldErrors 3 | - errors = messages.fieldErrors[0] 4 | if errors[name] 5 | - error = errors[name].type 6 | else 7 | - error = undefined 8 | 9 | .form--area(class=(stacked == true ? "form--area-stacked" : "")) 10 | label.form--element.form--element-text(class=(stacked == true ? "form--element-text-stacked" : ""), class=(error ? "form--element-invalid" : "")) 11 | if label 12 | .form--element-label= label 13 | if readonly 14 | +form-change-button() 15 | input.form--element-control(type="text", name=name, value=(messages.field && messages.field[0][name] ? messages.field[0][name] : value), placeholder=placeholder, readonly=(readonly == true ? "readonly" : undefined))&attributes(attributes) 16 | if error 17 | .form--area-errors 18 | ul.form--element-invalid-message.filled 19 | li.parsley-required= ({ 'required': 'This field is required.' })[error] 20 | else 21 | .form--area-errors 22 | -------------------------------------------------------------------------------- /views/partials/formcomponents/textarea.jade: -------------------------------------------------------------------------------- 1 | mixin input-textarea(name, placeholder, value, label, readonly, rows) 2 | .form--area 3 | label.form--element.form--element-textarea 4 | if label 5 | .form--element-label!= label 6 | if readonly 7 | +form-change-button() 8 | textarea.form--element-control(name=name, rows=rows || 5, placeholder=placeholder, readonly=(readonly == true ? "readonly" : undefined))&attributes(attributes) 9 | if block 10 | block 11 | else 12 | != messages.field && messages.field[0][name] ? messages.field[0][name] : value -------------------------------------------------------------------------------- /views/partials/modal.jade: -------------------------------------------------------------------------------- 1 | mixin modal(name) 2 | .modal(class=name) 3 | .modal--body 4 | block 5 | a.modal--close Close 6 | .modal--overlay -------------------------------------------------------------------------------- /views/partials/modals/share.jade: -------------------------------------------------------------------------------- 1 | .modal--section 2 | h3.modal--section-title Already shared with 3 | ul.share--list 4 | - var activeUser = survey.users.filter( function( u ) { try { return u.user.id == user.id; } catch( e ) { return false; } } ) 5 | if activeUser.length 6 | - activeUser = activeUser[ 0 ]; 7 | else 8 | - activeUser = { permissions: [] }; 9 | - var ownerCount = survey.users.reduce( function( prev, cur ) { return cur.permissions.indexOf( 'owner' ) > -1 ? prev + 1 : prev; }, 0 ); 10 | script. 11 | var activeSurveyUser = { perms: !{JSON.stringify(activeUser.permissions)}, roles: !{JSON.stringify(user.roles)} }; 12 | if survey.users.filter( function( u ) { return u & u.user; } ).length 13 | for u in survey.users 14 | if u && u.user 15 | li.share--list-item(data-email=u.user.email) 16 | if u.user.profile && u.user.profile.name 17 | .share--list-name 18 | = u.user.profile.name 19 | span 20 | | ( 21 | = u.user.email 22 | | ) 23 | else 24 | .share--list-name= u.user.email 25 | .share--list-permissions 26 | span(class=(user.hasRole('admin') || activeUser.permissions.indexOf('owner') > -1 ? 'share--list-permissions--hidden' : '')) 27 | for perm in validPermissions 28 | if u.permissions.indexOf(perm) > -1 29 | = perm.split( ' ' ).map( function( p ) { return p[0].toUpperCase() + p.slice(1).toLowerCase() } ).join( ' ' ) 30 | select(class=(!user.hasRole('admin') && activeUser.permissions.indexOf('owner') === -1 ? 'share--list-permissions--hidden' : '')) 31 | for perm in validPermissions 32 | option(value=perm, selected=(u.permissions.indexOf(perm) > -1)) 33 | = perm.split( ' ' ).map( function( p ) { return p[0].toUpperCase() + p.slice(1).toLowerCase() } ).join( ' ' ) 34 | - var surveyUserIsOwnerAndActiveUserIsNot = (activeUser.permissions.indexOf('owner') === -1 && u.permissions.indexOf('owner') > -1); 35 | - var activeUserIsOnlyOwner = (user.id === u.user.id && activeUser.permissions.indexOf('owner') > -1 && ownerCount === 1); 36 | if (surveyUserIsOwnerAndActiveUserIsNot || activeUserIsOnlyOwner) && !user.hasRole('admin') 37 | .share--list-remove.share--list-remove--hidden Remove 38 | else 39 | .share--list-remove Remove 40 | else 41 | li.share--list-nobody 42 | div: strong Nobody, yet! 43 | p Add someone using their email address below to share these results. 44 | li.share--list-item.share--list-item-template(data-email='') 45 | .share--list-name 46 | .share--list-permissions 47 | span(class=(user.hasRole('admin') || activeUser.permissions.indexOf('owner') > -1 ? 'share--list-permissions--hidden' : '')) 48 | select(class=(!user.hasRole('admin') && activeUser.permissions.indexOf('owner') === -1 ? 'share--list-permissions--hidden' : '')) 49 | for perm in validPermissions 50 | option(value=perm) 51 | = perm.split( ' ' ).map( function( p ) { return p[0].toUpperCase() + p.slice(1).toLowerCase() } ).join( ' ' ) 52 | .share--list-remove Remove 53 | .modal--section 54 | h3.modal--section-title Add Collaborator 55 | .share--new 56 | .share--new-input 57 | input(type="text", name="email", placeholder="Email Address") 58 | input(type="hidden", name="_csrf", value=csrfToken) 59 | .share--new-submit 60 | a.button.button--primary Share -------------------------------------------------------------------------------- /views/partials/nav.jade: -------------------------------------------------------------------------------- 1 | mixin nav 2 | if user 3 | .nav--menu 4 | .nav--menu-item.dropdown 5 | .dropdown--menu-parent 6 | = user.profile.name 7 | .dropdown--menu 8 | a.dropdown--menu-item(href="/dashboard", class=is(['/dashboard', 'is-active'])) My Surveys 9 | if user.hasRole( 'admin' ) 10 | a.dropdown--menu-item(href="/admin/users", class=is(['/admin/user', 'is-active'])) All Users 11 | a.dropdown--menu-item(href="/account", class=is(['/account', 'is-active'])) My Account 12 | a.dropdown--menu-item(href="/logout") Sign Out 13 | else 14 | .nav--menu 15 | a.nav--menu-item(href="/login" class=is(['/login', 'is-active'])) Log In 16 | a.nav--menu-item.nav--menu-cta(href="/signup" class=is(['/signup', 'is--active'])) Sign Up 17 | 18 | nav.nav 19 | .nav--container 20 | a(href="/") 21 | .nav--logo 22 | .nav--logo-img 23 | img(src="/img/logo.svg") 24 | .nav--logo-title Responsive Pulse 25 | +nav 26 | -------------------------------------------------------------------------------- /views/survey/about.jade: -------------------------------------------------------------------------------- 1 | extends /layouts/auth_base 2 | 3 | block content 4 | main 5 | section 6 | .row 7 | .card.card-full 8 | .card-body 9 | h2.card-title More About the Responsive OS Pulse 10 | .card-intro.voice-ros 11 | p 12 | object(type='application/x-shockwave-flash', style='width: 100%; height: 340px;', data='http://vimeo.com/moogaloop.swf?clip_id=65692182&server=vimeo.com&show_title=0&show_byline=0&show_portrait=0&color=ff9933&fullscreen=1', allowfullscreen='true', allowscriptaccess='always') 13 | param(name='movie', value='http://vimeo.com/moogaloop.swf?clip_id=65692182&server=vimeo.com&show_title=0&show_byline=0&show_portrait=0&color=ff9933&fullscreen=1', allowfullscreen='true', allowscriptaccess='always') 14 | p 15 | | Hello there, we're 16 | a(href='http://undercurrent.com') Undercurrent 17 | | , a management consultancy with offices in NY and LA. We help our clients thrive in the face of unprecedented uncertainty and rampant organizational complexity. We call it 'being responsive.' Our clients (which include GE, AMEX, Pepsi, Ford, and The Gates Foundation) call it speed, efficiency, or foresight. Regardless of how you label it, after our engagements our clients are faster, leaner, and yet more adaptive to changes in their markets. 18 | p 19 | strong We're obsessed with how organizations work and how to make them work harder. 20 | | We've spent the last 7 years studying the most ambitious organizations of our time — a group of over 80 firms, including Amazon, Tesla, Airbnb, Uber, Netflix, and Apple. Our work is also heavily influenced by the study of complex systems, an emerging scientific field that investigates the behavior of a large system of interacting agents (like a stock market, immune system, or hill of ants). 21 | p 22 | | Combining these two approaches, we've built, tested, and iterated our organizational model of everything — the Responsive Operating System — which guides and informs our work, whether we're helping to pick a new CEO, auditing a manufacturing system, devising a product strategy, or helping a firm re-write its mission. 23 | -------------------------------------------------------------------------------- /views/survey/done.jade: -------------------------------------------------------------------------------- 1 | extends /layouts/base 2 | 3 | block content 4 | main 5 | section 6 | .card.card-full.card--spacious.type-center.card--success 7 | .card-body 8 | .card-title You're finished! 9 | p Now it's our turn to tabulate the responses.
10 | | Interested in learning more about responsive organizations? Visit Undercurrent.com. -------------------------------------------------------------------------------- /views/survey/even-over/overview.jade: -------------------------------------------------------------------------------- 1 | extends /layouts/auth_base 2 | include /partials/modal 3 | include /survey/header 4 | 5 | block content 6 | +modal('modal--share') 7 | include /partials/modals/share 8 | 9 | main 10 | section 11 | +header 12 | .results--nav-menu 13 | if typeof survey !== 'undefined' && survey.responseCount 14 | a.results--nav-menu-item(href='/view/' + survey.key, class=is([new RegExp('/view/' + survey.key + '$'), 'is-active'])) 15 | i.fa.fa-bookmark 16 | | Overview 17 | a.results--nav-menu-item(href='/view/' + survey.key + "/detail", class=is(['/view/' + survey.key + '/detail', 'is-active'])) 18 | i.fa.fa-bar-chart 19 | | Detailed Results 20 | .ovw-container 21 | if typeof survey !== 'undefined' && survey.responseCount 22 | .g--row 23 | .ovw-card-rowHeader.sticky-header: span Responsiveness 24 | .chart.bar.responsiveness(data-margins=",,,40") 25 | .g--row 26 | .g--half.identifies-most 27 | script.template(type="text/html"). 28 |

Your organization identifies most with:

29 |

<%= trait.fullTitle %>

30 |
<%= marked(trait.blurb) %>
31 | .g--half.g--last.identifies-least 32 | script.template(type="text/html"). 33 |

Your organization identifies least with:

34 |

<%= trait.fullTitle %>

35 |
<%= marked(trait.blurb) %>
36 | else 37 | .g--row 38 | .g--full.no-responses 39 | h2 Waiting for responses… 40 | h3 41 | a(href='/about') Learn more 42 | | about Responsive OS. 43 | -------------------------------------------------------------------------------- /views/survey/header.jade: -------------------------------------------------------------------------------- 1 | include nav 2 | 3 | mixin header 4 | .sticky-nav 5 | .results--nav 6 | +surveyNav 7 | if block 8 | block 9 | .ovw-header 10 | .ovw-header-inner 11 | if typeof survey !== 'undefined' && survey.responseCount 12 | .ovw-scoring 13 | .ovw-scoring--inner 14 | .ovw-scoring--responses 15 | .chart.number.responses 16 | .data-placeholder--number 0 17 | .ovw-scoring--responses-total 18 | | of 0 19 | .ovw-scoring--label 20 | | Responses 21 | .ovw-scoring--responses 22 | .chart.number.incomplete 23 | .data-placeholder--number 0 24 | .ovw-scoring--label 25 | | Incomplete 26 | .ovw-scoring--score 27 | .chart.number.score 28 | .data-placeholder--number 0 29 | .ovw-scoring--label 30 | | Score 31 | .ovw-scoring--score-diff 32 | .score-diff-up ▲ 33 | .score-diff-num 34 | | 0% 35 | .score-diff-down ▼ 36 | .ovw-scoring--percentile.number.percentile 37 | a.ovw-scoring--reset Reset Filters 38 | h2.survey-title= survey.title 39 | .survey-info 40 | if survey.createdAt 41 | | Created on 42 | = moment(survey.createdAt).format('MMM D, YYYY') 43 | | · 44 | | Last updated 45 | span.date-underline(title = survey.lastUpdate ? moment(survey.lastUpdate).format('MMM D, YYYY') : 'Never')=moment(survey.lastUpdate).fromNow() 46 | .clearfix 47 | -------------------------------------------------------------------------------- /views/survey/nav.jade: -------------------------------------------------------------------------------- 1 | mixin surveyNav 2 | if block 3 | block 4 | .results--nav-actions 5 | a.results--nav-menu-item.modal--open(data-modalname='share') 6 | i.fa.fa-fw.fa-share 7 | | Share 8 | a.results--nav-menu-item(href='/export/' + survey.key) 9 | i.fa.fa-fw.fa-download 10 | | Export 11 | -------------------------------------------------------------------------------- /views/survey/new.jade: -------------------------------------------------------------------------------- 1 | extends /layouts/auth_base 2 | 3 | include /partials/formcomponents 4 | 5 | block content 6 | main 7 | section 8 | .row 9 | .card.card-half.card-center.card--create 10 | .card-body 11 | h1.card-title Create New Survey 12 | form.form(method="post") 13 | if messages.error 14 | ul.form--errors 15 | for error in messages.error 16 | li= error 17 | if messages.success 18 | ul.form--success 19 | for success in messages.success 20 | li= success 21 | .form--group 22 | label.form--element.form--element-text 23 | +input-text("title", "e.g., June Responsive Inquiry", form["title"] || "", "Survey Name", false, true)(autofocus="") 24 | .form--group 25 | label.form--element.form--element-select 26 | +input-select("orgSize", orgSizes, "How many people work within your organization?", "", true) 27 | label.form--element.form--element-text 28 | +input-text("orgAge", "in years", "", "How old is your organization?", false, true)(type="number") 29 | label.form--element.form--element-select 30 | +input-select("industry", industries, "What primary industry does your organization work within?", "", true) 31 | .form--group 32 | label.form--element.form--element-text 33 | +input-text("orgLeader", "e.g., John Smith", "", "What is the name of the person who leads this organization?", false, true) 34 | label.form--element.form--element-text 35 | +input-text("orgLeaderRole", "e.g., Director of Marketing", "", "What is their title?", false, true) 36 | +input-textarea("welcomeMessage", "Please take this survey...", "", "Provide a welcome message that users will see upon first visiting the survey (formatted using Markdown):", false, 16). 37 | Today's largest and most important organizations face two critical challenges: First, the world around them is changing faster than ever, and second, their own scale and complexity are working against them. To be successful in the future, we, as other organizations, need to strive to become more responsive – responsive to our customers, culture, competition, technology, regulation, and all other forces of disruption. 38 | 39 | To help us think about the future in a careful, deliberate way as we map out our vision for the future, we’re administering a brief survey to determine how responsive we are as an organization today. We think of it as taking our company’s ‘pulse.’ *The survey is fully anonymous* and will help us understand our strengths, biases, values, capabilities, and patterns of working. 40 | 41 | Our understanding requires everyone’s participation and honest input. So please participate, _it only takes five minutes to complete._ 42 | .form--group 43 | label.form--element.form--element-textarea 44 | input(type="hidden", name="_csrf", value=csrfToken) 45 | button(type="submit").button.button--primary Create -------------------------------------------------------------------------------- /views/survey/original/nav.jade: -------------------------------------------------------------------------------- 1 | .results--nav-menu 2 | a.results--nav-menu-item(href='/view/' + survey.key) Overview 3 | a.results--nav-menu-item(href='/view/' + survey.key + '/scores/purpose') Purpose 4 | a.results--nav-menu-item(href='/view/' + survey.key + '/scores/people') People 5 | a.results--nav-menu-item(href='/view/' + survey.key + '/scores/process') Process 6 | a.results--nav-menu-item(href='/view/' + survey.key + '/scores/product') Product 7 | a.results--nav-menu-item(href='/view/' + survey.key + '/scores/platform') Platform -------------------------------------------------------------------------------- /views/survey/original/overview.jade: -------------------------------------------------------------------------------- 1 | extends /layouts/auth_base 2 | include /partials/modal 3 | include /survey/header 4 | 5 | block content 6 | +modal('modal--share') 7 | include /partials/modals/share 8 | 9 | main 10 | section 11 | if typeof survey !== 'undefined' && survey.responseCount 12 | +header 13 | include nav 14 | .ovw-container 15 | if typeof survey !== 'undefined' && survey.responseCount 16 | .ovw-card-row 17 | .ovw-card-rowHeader.sticky-header: span Structure 18 | .ovw-cardGroup.g--row 19 | .ovw-card.g--third 20 | .card-body 21 | h2.card-title Hierarchy 22 | .card-data 23 | .chart.stacked(data-title='Hierarchy', 24 | data-col='management_layers', 25 | data-y-label='Layers beneath CEO') 26 | .data-placeholder--box 27 | .ovw-card.g--third 28 | .card-body 29 | h2.card-title Position vs. Traction 30 | .card-data 31 | .chart.matrixplot(data-title='Position vs. Traction' 32 | data-col-x='org_position', data-col-y='org_traction', 33 | data-x-values='following,leading', data-y-values='advancing,declining', 34 | data-x-labels='Following,Leading', data-y-labels='Advancing,Declining') 35 | .data-placeholder--box 36 | .ovw-card.g--third.g--last 37 | .card-body 38 | h2.card-title Tenure 39 | .card-data 40 | .chart.bar.tenure(data-title='Tenure', 41 | data-col='org_years', 42 | data-x-label='in years', 43 | filterable='') 44 | .data-placeholder--box 45 | .ovw-card-row.g--row 46 | .ovw-card-rowHeader.sticky-header: span Responsiveness 47 | .ovw-cardGroup 48 | .ovw-card.card-full 49 | .chart.line.responsiveness 50 | .ovw-card-row.g--row 51 | .ovw-card-rowHeader.sticky-header: span Score Breakdown 52 | .ovw-cardGroup 53 | .ovw-card.card-full 54 | .card-body 55 | .card-intro 56 | p.voice-system 57 | | Select a team from the drop-down menu to view their scores compared to the organization overall. 58 | .overall.card-data 59 | .data-controls 60 | .button-group 61 | span Team: 62 | select.filter(data-col='team', data-min-value='2') 63 | .table-wrapper 64 | table.table.horizontal.responsive 65 | thead 66 | tr 67 | td.row-header   68 | td.tooltip.right-offset-arrow(title='Why we come to work') Purpose 69 | td.tooltip.right-offset-arrow(title='The teams that do the work') People 70 | td.tooltip.right-offset-arrow(title='What we deliver') Process 71 | td.tooltip.right-offset-arrow(title='How the work gets done') Product 72 | td.tooltip.right-offset-arrow(title='What the community creates on our behalf') Platform 73 | td.row-summary.col-header Total 74 | tbody 75 | else 76 | .ovw-card-row 77 | .ovw-card.card-full 78 | .no-responses 79 | h1 Waiting for responses… 80 | h2 81 | a(href='/about') Learn more 82 | | about Responsive OS. 83 | -------------------------------------------------------------------------------- /views/survey/original/scores.jade: -------------------------------------------------------------------------------- 1 | extends /layouts/auth_base 2 | include /partials/modal 3 | include /survey/header 4 | 5 | block content 6 | +modal('modal--share') 7 | include /partials/modals/share 8 | 9 | main 10 | section 11 | if typeof survey !== 'undefined' && survey.responseCount 12 | +header 13 | include nav 14 | .ovw-container 15 | .ovw-card-row 16 | .card.card-half 17 | .card-body 18 | h2.card-title= domain 19 | .card-data 20 | table.domain-table(data-domain=domain) 21 | thead 22 | tr 23 | th.label   24 | th Score 25 | tbody 26 | .data-placeholder--box 27 | 28 | .card.card-half.last 29 | .card-body 30 | h2.card-title Highest Agreement 31 | .card-data 32 | .chart.numberline.agreement(data-domain=domain) 33 | .data-placeholder--box.data-placeholder--third 34 | .data-placeholder--err Not enough data. 35 | .legend 36 | .legend-item.leadership Leadership 37 | .legend-item.team Team 38 | .legend-item.agreement Leadership & Team Agree 39 | .card-body 40 | h2.card-title Highest Disagreement 41 | .card-data 42 | .chart.numberline.disagreement(data-domain=domain) 43 | .data-placeholder--box.data-placeholder--third 44 | .data-placeholder--err Not enough data. 45 | .legend 46 | .legend-item.leadership Leadership 47 | .legend-item.team Team 48 | .ovw-card-row 49 | .card.card-full 50 | .card-body 51 | h2.card-title Verbatims 52 | .card-data 53 | ol.verbatims--list.table(data-domain=domain) 54 | -------------------------------------------------------------------------------- /views/user/account.jade: -------------------------------------------------------------------------------- 1 | extends /layouts/auth_base 2 | 3 | include /partials/formcomponents 4 | 5 | block content 6 | main 7 | section 8 | .row 9 | .card.card-full.card--spacious 10 | .card-body 11 | .card-row 12 | +form-group(true)(action="/account") 13 | h3 Basic Information 14 | +input-text("name", null, user.profile.name || "", "Name", true)(required="") 15 | +input-text("organization", null, user.profile.organization || "", "Organization", true)(required="") 16 | +input-email("email", null, user.email, "E-mail", true)(required="") 17 | +input-password("password", null, "", "Password", true) 18 | input(type="hidden", name="_csrf", value=csrfToken) 19 | +form-buttons 20 | -------------------------------------------------------------------------------- /views/user/forgot.jade: -------------------------------------------------------------------------------- 1 | extends /layouts/base 2 | 3 | include /partials/formcomponents 4 | 5 | block content 6 | main 7 | section 8 | .auth--form 9 | h1.card-title Forgotten Password 10 | form.form(method="post") 11 | if messages.error 12 | ul.form--errors 13 | for error in messages.error 14 | li= error 15 | if messages.success 16 | .form--success= messages.success[0] 17 | .form--group 18 | label.form--element.form--element-text 19 | //- The 'email' field is named 'username' because that is what the 20 | //- passport library requires to validate on req.logIn. 21 | +input-email("email", "E-mail Address", messages.email) 22 | input(type="hidden", name="_csrf", value=csrfToken) 23 | button(type="submit").button.button--primary.login--submit Reset Password 24 | .login--signup Need an account? Sign up here 25 | -------------------------------------------------------------------------------- /views/user/login.jade: -------------------------------------------------------------------------------- 1 | extends /layouts/base 2 | 3 | include /partials/formcomponents 4 | 5 | block content 6 | main 7 | section 8 | .auth--form 9 | h1 Log In 10 | form.form(method="post") 11 | if messages.error 12 | ul.form--errors 13 | for error in messages.error 14 | li= error 15 | if messages.success 16 | ul.form--success 17 | for success in messages.success 18 | li= success 19 | .form--group 20 | label.form--element.form--element-text 21 | //- The 'email' field is named 'username' because that is what the 22 | //- passport library requires to validate on req.logIn. 23 | +input-email("username", "E-mail Address", messages.email) 24 | label.form--element.form--element-text 25 | +input-password("password", "Password") 26 | a(href="/forgot").login--forgot Forgot? 27 | input(type="hidden", name="_csrf", value=csrfToken) 28 | button(type="submit").button.button--primary.login--submit Submit 29 | .login--signup Need an account? Sign up here 30 | -------------------------------------------------------------------------------- /views/user/reset.jade: -------------------------------------------------------------------------------- 1 | extends /layouts/base 2 | 3 | include /partials/formcomponents 4 | 5 | block content 6 | main 7 | section 8 | .row 9 | .card.card-third.card-center.card--login 10 | .card-body 11 | h1.card-title Reset Password 12 | form.form(method="post") 13 | if messages.error 14 | ul.form--errors 15 | for error in messages.error 16 | li= error 17 | if messages.success 18 | .form--success= messages.success[0] 19 | .form--group 20 | label.form--element.form--element-bundled.form--element-text 21 | input(type="password", name="password", placeholder="New Password").form--element-control 22 | label.form--element.form--element-bundled.form--element-text 23 | input(type="password", name="validate_password" placeholder="Re-type New Password").form--element-control 24 | input(type="hidden", name="_csrf", value=csrfToken) 25 | button(type="submit").button.button--primary.login--submit Reset Password 26 | -------------------------------------------------------------------------------- /views/user/signup.jade: -------------------------------------------------------------------------------- 1 | extends /layouts/base 2 | 3 | block content 4 | main 5 | section 6 | .auth--form 7 | h1 Sign Up 8 | form.form(method="post", action=(typeof activationKey !== 'undefined' ? '/activate' : '/signup')) 9 | if messages.error 10 | ul.form--errors 11 | for error in messages.error 12 | li= error 13 | .form--group 14 | label.form--element.form--element-bundled.form--element-text 15 | input(type="text", name="name", placeholder="First and Last Name").form--element-control 16 | label.form--element.form--element-bundled.form--element-text 17 | input(type="text", name="company", placeholder="Company").form--element-control 18 | label.form--element.form--element-bundled.form--element-text 19 | //- The 'email' field is named 'username' because that is what the 20 | //- passport library requires to validate on req.logIn. 21 | input(type="email", name="username", 22 | placeholder="E-mail Address", 23 | value=(typeof email !== 'undefined' ? email : ''), 24 | readonly=(typeof activationKey !== 'undefined' ? true : false)).form--element-control 25 | .form--group 26 | label.form--element.form--element-bundled.form--element-text 27 | input(type="password", name="password", placeholder="Password").form--element-control 28 | label.form--element.form--element-bundled.form--element-text 29 | input(type="password", name="validate_password" placeholder="Re-type Password").form--element-control 30 | .form--group 31 | .form--element.form--element-checkbox 32 | input(type="checkbox", name="check", value="c1").form--element-control#c1 33 | label(for="c1").span.form--element-value I agree to the Terms & Conditions 34 | input(type="hidden", name="_csrf", value=csrfToken) 35 | if typeof activationKey !== 'undefined' 36 | input(type="hidden", name="activationKey", value=activationKey) 37 | button(type="submit").button.button--primary.login--submit Submit 38 | .login--signup Already have an account? Log in here 39 | --------------------------------------------------------------------------------