├── app
├── helpers
│ ├── .gitkeep
│ ├── json.js
│ ├── notest.js
│ ├── markdown.js
│ └── asset_pipeline.js
├── models
│ ├── .gitkeep
│ └── data.js
├── controllers
│ ├── .gitkeep
│ ├── home.js
│ ├── websocket_api.js
│ ├── http_api.js
│ ├── coap_api.js
│ └── mqtt_api.js
├── views
│ ├── network_button.hbs
│ ├── topic.hbs
│ ├── layout.hbs
│ └── home.hbs
└── assets
│ ├── css
│ ├── glyphicons-halflings.png
│ ├── lib
│ │ ├── layouts.less
│ │ ├── component-animations.less
│ │ ├── utilities.less
│ │ ├── breadcrumbs.less
│ │ ├── grid.less
│ │ ├── hero-unit.less
│ │ ├── responsive-768px-979px.less
│ │ ├── wells.less
│ │ ├── responsive-1200px-min.less
│ │ ├── close.less
│ │ ├── accordion.less
│ │ ├── pager.less
│ │ ├── scaffolding.less
│ │ ├── responsive.less
│ │ ├── responsive-utilities.less
│ │ ├── thumbnails.less
│ │ ├── alerts.less
│ │ ├── code.less
│ │ ├── pagination.less
│ │ ├── bootstrap.less
│ │ ├── tooltip.less
│ │ ├── labels-badges.less
│ │ ├── modals.less
│ │ ├── carousel.less
│ │ ├── progress-bars.less
│ │ ├── reset.less
│ │ ├── popovers.less
│ │ ├── responsive-767px-max.less
│ │ ├── responsive-navbar.less
│ │ ├── type.less
│ │ ├── dropdowns.less
│ │ ├── buttons.less
│ │ ├── button-groups.less
│ │ ├── tables.less
│ │ ├── navs.less
│ │ └── variables.less
│ └── main.css.less
│ └── js
│ ├── home.js
│ ├── bootstrap.js
│ ├── bootstrap
│ ├── bootstrap-transition.js
│ ├── bootstrap-alert.js
│ ├── bootstrap-button.js
│ ├── bootstrap-popover.js
│ ├── bootstrap-affix.js
│ ├── bootstrap-tab.js
│ ├── bootstrap-dropdown.js
│ ├── bootstrap-scrollspy.js
│ ├── bootstrap-collapse.js
│ ├── bootstrap-carousel.js
│ ├── bootstrap-modal.js
│ ├── bootstrap-typeahead.js
│ └── bootstrap-tooltip.js
│ ├── html5-shiv.js
│ └── topics.js
├── public
├── stylesheets
│ └── .gitkeep
└── images
│ ├── grey.png
│ └── diaonan.png
├── .gitignore
├── .gitmodules
├── test
├── mocha.opts
├── spec_helper.js
└── models
│ └── data_spec.js
├── .travis.yml
├── deploy
└── start.sh
├── start.sh
├── LICENSE
├── package.json
├── README.md
└── diaonan.js
/app/helpers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/models/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/controllers/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/app/views/network_button.hbs:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/stylesheets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/images/grey.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/diaonan/master/public/images/grey.png
--------------------------------------------------------------------------------
/public/images/diaonan.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/diaonan/master/public/images/diaonan.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | node_modules/*
3 | *~
4 | *.rdb
5 | public/stylesheets/*.css
6 | npm-debug.log
7 | log
8 | coverage
9 |
--------------------------------------------------------------------------------
/app/assets/css/glyphicons-halflings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/phodal/diaonan/master/app/assets/css/glyphicons-halflings.png
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "node_modules/jasmine-node"]
2 | path = node_modules/jasmine-node
3 | url = git@github.com:mcollina/jasmine-node.git
4 |
--------------------------------------------------------------------------------
/test/mocha.opts:
--------------------------------------------------------------------------------
1 | --require chai
2 | --require sinon
3 | --reporter dot
4 | --ui bdd
5 | --growl
6 | --colors
7 | --globals s
8 | --require test/spec_helper
9 |
--------------------------------------------------------------------------------
/app/helpers/json.js:
--------------------------------------------------------------------------------
1 | var hbs;
2 |
3 | hbs = require('hbs');
4 |
5 | module.exports = function (app) {
6 | return hbs.registerHelper('json', function (context) {
7 | return new hbs.SafeString(JSON.stringify(context));
8 | });
9 | };
10 |
--------------------------------------------------------------------------------
/app/assets/js/home.js:
--------------------------------------------------------------------------------
1 |
2 | $(document).ready(function() {
3 | $("#go").submit(function() {
4 | var val = $("#topic").val()
5 | if(val.length > 0) {
6 | window.location = "/topics/" + val;
7 | }
8 | return false;
9 | });
10 | });
11 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 0.10.32
4 | notifications:
5 | email: false
6 | services:
7 | - redis-server
8 | after_success: CODECLIMATE_REPO_TOKEN=779fe2fc997cafe9f035a6d2b41c6269d23f5449232c5386f9618966b311c917 codeclimate < coverage/lcov.info
9 |
--------------------------------------------------------------------------------
/app/controllers/home.js:
--------------------------------------------------------------------------------
1 | module.exports = function (app) {
2 | return app.get('/', function (req, res) {
3 | var _base;
4 | (_base = req.session).topics || (_base.topics = []);
5 | return res.render('home.hbs', {
6 | topics: req.session.topics
7 | });
8 | });
9 | };
10 |
--------------------------------------------------------------------------------
/deploy/start.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | export NVM_DIR="/root/.nvm"
3 | [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" # This loads nvm
4 | nvm use 0.10.35
5 | NODE_ENV=development
6 | forever stopall &
7 | forever start -l /home/www/diaonan/log/forever.log -o /home/www/diaonan/log/out.log -e /home/www/diaonan/log/err.log -a qest.js
8 |
--------------------------------------------------------------------------------
/start.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | export NVM_DIR="/root/.nvm"
3 | [ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh" # This loads nvm
4 | nvm use 0.10.35
5 | NODE_ENV=development
6 | forever stopall &
7 | forever start -l /home/www/diaonan/log/forever.log -o /home/www/diaonan/log/out.log -e /home/www/diaonan/log/err.log -a diaonan.js
8 |
--------------------------------------------------------------------------------
/app/helpers/notest.js:
--------------------------------------------------------------------------------
1 | var hbs;
2 |
3 | hbs = require('hbs');
4 |
5 | module.exports = function (app) {
6 | return hbs.registerHelper('notest', function (options) {
7 | var input;
8 | if (process.env.NODE_ENV !== "test") {
9 | input = options.fn(this);
10 | return input;
11 | } else {
12 | return "";
13 | }
14 | });
15 | };
16 |
--------------------------------------------------------------------------------
/app/helpers/markdown.js:
--------------------------------------------------------------------------------
1 | var hbs;
2 |
3 | hbs = require('hbs');
4 |
5 | module.exports = function(app) {
6 | return hbs.registerHelper('markdown', function(options) {
7 | var input, result;
8 | input = options.fn(this);
9 | result = require("markdown").markdown.toHTML(input);
10 | return result;
11 | });
12 | };
13 |
--------------------------------------------------------------------------------
/app/assets/css/lib/layouts.less:
--------------------------------------------------------------------------------
1 | //
2 | // Layouts
3 | // --------------------------------------------------
4 |
5 |
6 | // Container (centered, fixed-width layouts)
7 | .container {
8 | .container-fixed();
9 | }
10 |
11 | // Fluid layouts (left aligned, with sidebar, min- & max-width content)
12 | .container-fluid {
13 | padding-right: @gridGutterWidth;
14 | padding-left: @gridGutterWidth;
15 | .clearfix();
16 | }
--------------------------------------------------------------------------------
/app/assets/css/lib/component-animations.less:
--------------------------------------------------------------------------------
1 | //
2 | // Component animations
3 | // --------------------------------------------------
4 |
5 |
6 | .fade {
7 | opacity: 0;
8 | .transition(opacity .15s linear);
9 | &.in {
10 | opacity: 1;
11 | }
12 | }
13 |
14 | .collapse {
15 | position: relative;
16 | height: 0;
17 | overflow: hidden;
18 | .transition(height .35s ease);
19 | &.in {
20 | height: auto;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/assets/css/lib/utilities.less:
--------------------------------------------------------------------------------
1 | //
2 | // Utility classes
3 | // --------------------------------------------------
4 |
5 |
6 | // Quick floats
7 | .pull-right {
8 | float: right;
9 | }
10 | .pull-left {
11 | float: left;
12 | }
13 |
14 | // Toggling content
15 | .hide {
16 | display: none;
17 | }
18 | .show {
19 | display: block;
20 | }
21 |
22 | // Visibility
23 | .invisible {
24 | visibility: hidden;
25 | }
26 |
27 | // For Affix plugin
28 | .affix {
29 | position: fixed;
30 | }
31 |
--------------------------------------------------------------------------------
/app/assets/css/lib/breadcrumbs.less:
--------------------------------------------------------------------------------
1 | //
2 | // Breadcrumbs
3 | // --------------------------------------------------
4 |
5 |
6 | .breadcrumb {
7 | padding: 8px 15px;
8 | margin: 0 0 @baseLineHeight;
9 | list-style: none;
10 | background-color: #f5f5f5;
11 | .border-radius(4px);
12 | li {
13 | display: inline-block;
14 | .ie7-inline-block();
15 | text-shadow: 0 1px 0 @white;
16 | }
17 | .divider {
18 | padding: 0 5px;
19 | color: #ccc;
20 | }
21 | .active {
22 | color: @grayLight;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/assets/css/lib/grid.less:
--------------------------------------------------------------------------------
1 | //
2 | // Grid system
3 | // --------------------------------------------------
4 |
5 |
6 | // Fixed (940px)
7 | #grid > .core(@gridColumnWidth, @gridGutterWidth);
8 |
9 | // Fluid (940px)
10 | #grid > .fluid(@fluidGridColumnWidth, @fluidGridGutterWidth);
11 |
12 | // Reset utility classes due to specificity
13 | [class*="span"].hide,
14 | .row-fluid [class*="span"].hide {
15 | display: none;
16 | }
17 |
18 | [class*="span"].pull-right,
19 | .row-fluid [class*="span"].pull-right {
20 | float: right;
21 | }
22 |
--------------------------------------------------------------------------------
/app/assets/css/lib/hero-unit.less:
--------------------------------------------------------------------------------
1 | //
2 | // Hero unit
3 | // --------------------------------------------------
4 |
5 |
6 | .hero-unit {
7 | padding: 60px;
8 | margin-bottom: 30px;
9 | background-color: @heroUnitBackground;
10 | .border-radius(6px);
11 | h1 {
12 | margin-bottom: 0;
13 | font-size: 60px;
14 | line-height: 1;
15 | color: @heroUnitHeadingColor;
16 | letter-spacing: -1px;
17 | }
18 | p {
19 | font-size: 18px;
20 | font-weight: 200;
21 | line-height: @baseLineHeight * 1.5;
22 | color: @heroUnitLeadColor;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/app/assets/css/lib/responsive-768px-979px.less:
--------------------------------------------------------------------------------
1 | //
2 | // Responsive: Tablet to desktop
3 | // --------------------------------------------------
4 |
5 |
6 | @media (min-width: 768px) and (max-width: 979px) {
7 |
8 | // Fixed grid
9 | #grid > .core(@gridColumnWidth768, @gridGutterWidth768);
10 |
11 | // Fluid grid
12 | #grid > .fluid(@fluidGridColumnWidth768, @fluidGridGutterWidth768);
13 |
14 | // Input grid
15 | #grid > .input(@gridColumnWidth768, @gridGutterWidth768);
16 |
17 | // No need to reset .thumbnails here since it's the same @gridGutterWidth
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/app/assets/js/bootstrap.js:
--------------------------------------------------------------------------------
1 | //= require ./jquery-1.8.2.js
2 | //= require ./bootstrap/bootstrap-transition.js
3 | //= require ./bootstrap/bootstrap-alert.js
4 | //= require ./bootstrap/bootstrap-modal.js
5 | //= require ./bootstrap/bootstrap-dropdown.js
6 | //= require ./bootstrap/bootstrap-scrollspy.js
7 | //= require ./bootstrap/bootstrap-tab.js
8 | //= require ./bootstrap/bootstrap-tooltip.js
9 | //= require ./bootstrap/bootstrap-popover.js
10 | //= require ./bootstrap/bootstrap-button.js
11 | //= require ./bootstrap/bootstrap-collapse.js
12 | //= require ./bootstrap/bootstrap-carousel.js
13 | //= require ./bootstrap/bootstrap-typeahead.js
14 |
--------------------------------------------------------------------------------
/app/assets/css/lib/wells.less:
--------------------------------------------------------------------------------
1 | //
2 | // Wells
3 | // --------------------------------------------------
4 |
5 |
6 | // Base class
7 | .well {
8 | min-height: 20px;
9 | padding: 19px;
10 | margin-bottom: 20px;
11 | background-color: @wellBackground;
12 | border: 1px solid darken(@wellBackground, 7%);
13 | .border-radius(4px);
14 | .box-shadow(inset 0 1px 1px rgba(0,0,0,.05));
15 | blockquote {
16 | border-color: #ddd;
17 | border-color: rgba(0,0,0,.15);
18 | }
19 | }
20 |
21 | // Sizes
22 | .well-large {
23 | padding: 24px;
24 | .border-radius(6px);
25 | }
26 | .well-small {
27 | padding: 9px;
28 | .border-radius(3px);
29 | }
30 |
--------------------------------------------------------------------------------
/app/assets/css/lib/responsive-1200px-min.less:
--------------------------------------------------------------------------------
1 | //
2 | // Responsive: Large desktop and up
3 | // --------------------------------------------------
4 |
5 |
6 | @media (min-width: 1200px) {
7 |
8 | // Fixed grid
9 | #grid > .core(@gridColumnWidth1200, @gridGutterWidth1200);
10 |
11 | // Fluid grid
12 | #grid > .fluid(@fluidGridColumnWidth1200, @fluidGridGutterWidth1200);
13 |
14 | // Input grid
15 | #grid > .input(@gridColumnWidth1200, @gridGutterWidth1200);
16 |
17 | // Thumbnails
18 | .thumbnails {
19 | margin-left: -@gridGutterWidth1200;
20 | }
21 | .thumbnails > li {
22 | margin-left: @gridGutterWidth1200;
23 | }
24 | .row-fluid .thumbnails {
25 | margin-left: 0;
26 | }
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/app/assets/css/lib/close.less:
--------------------------------------------------------------------------------
1 | //
2 | // Close icons
3 | // --------------------------------------------------
4 |
5 |
6 | .close {
7 | float: right;
8 | font-size: 20px;
9 | font-weight: bold;
10 | line-height: @baseLineHeight;
11 | color: @black;
12 | text-shadow: 0 1px 0 rgba(255,255,255,1);
13 | .opacity(20);
14 | &:hover {
15 | color: @black;
16 | text-decoration: none;
17 | cursor: pointer;
18 | .opacity(40);
19 | }
20 | }
21 |
22 | // Additional properties for button version
23 | // iOS requires the button element instead of an anchor tag.
24 | // If you want the anchor version, it requires `href="#"`.
25 | button.close {
26 | padding: 0;
27 | cursor: pointer;
28 | background: transparent;
29 | border: 0;
30 | -webkit-appearance: none;
31 | }
--------------------------------------------------------------------------------
/app/assets/css/lib/accordion.less:
--------------------------------------------------------------------------------
1 | //
2 | // Accordion
3 | // --------------------------------------------------
4 |
5 |
6 | // Parent container
7 | .accordion {
8 | margin-bottom: @baseLineHeight;
9 | }
10 |
11 | // Group == heading + body
12 | .accordion-group {
13 | margin-bottom: 2px;
14 | border: 1px solid #e5e5e5;
15 | .border-radius(4px);
16 | }
17 | .accordion-heading {
18 | border-bottom: 0;
19 | }
20 | .accordion-heading .accordion-toggle {
21 | display: block;
22 | padding: 8px 15px;
23 | }
24 |
25 | // General toggle styles
26 | .accordion-toggle {
27 | cursor: pointer;
28 | }
29 |
30 | // Inner needs the styles because you can't animate properly with any styles on the element
31 | .accordion-inner {
32 | padding: 9px 15px;
33 | border-top: 1px solid #e5e5e5;
34 | }
35 |
--------------------------------------------------------------------------------
/app/assets/css/lib/pager.less:
--------------------------------------------------------------------------------
1 | //
2 | // Pager pagination
3 | // --------------------------------------------------
4 |
5 |
6 | .pager {
7 | margin: @baseLineHeight 0;
8 | list-style: none;
9 | text-align: center;
10 | .clearfix();
11 | }
12 | .pager li {
13 | display: inline;
14 | }
15 | .pager a,
16 | .pager span {
17 | display: inline-block;
18 | padding: 5px 14px;
19 | background-color: #fff;
20 | border: 1px solid #ddd;
21 | .border-radius(15px);
22 | }
23 | .pager a:hover {
24 | text-decoration: none;
25 | background-color: #f5f5f5;
26 | }
27 | .pager .next a,
28 | .pager .next span {
29 | float: right;
30 | }
31 | .pager .previous a {
32 | float: left;
33 | }
34 | .pager .disabled a,
35 | .pager .disabled a:hover,
36 | .pager .disabled span {
37 | color: @grayLight;
38 | background-color: #fff;
39 | cursor: default;
40 | }
--------------------------------------------------------------------------------
/app/assets/css/main.css.less:
--------------------------------------------------------------------------------
1 |
2 | @import "./lib/bootstrap.less";
3 |
4 | body {
5 | padding-top: 60px;
6 | padding-bottom: 40px;
7 | background-image: url("/images/grey.png");
8 | background-repeat: repeat;
9 | }
10 |
11 | @import "./lib/responsive.less";
12 |
13 | .logo {
14 | width: 400px;
15 | height:300px;
16 | background-image: url("/images/diaonan.png");
17 | background-repeat: repeat;
18 | }
19 |
20 | .jumbo {
21 |
22 | background-color: rgba(0, 0, 0, 0.1);
23 | .border-radius(5px);
24 | margin-top: -60px;
25 |
26 | .container();
27 | padding-top: 60px;
28 | padding-bottom: 40px;
29 | margin-bottom: 30px;
30 |
31 | .border-radius(6px);
32 | h1 {
33 | margin-bottom: 0;
34 | font-size: 60px;
35 | line-height: 1;
36 | color: @heroUnitHeadingColor;
37 | letter-spacing: -1px;
38 | }
39 |
40 | }
41 |
42 | .logo h1, .navbar-inverse .brand {
43 | color: #5bb75b;
44 | }
45 |
46 | .logo h3 {
47 | color: #ed561b;
48 | }
49 |
--------------------------------------------------------------------------------
/app/controllers/websocket_api.js:
--------------------------------------------------------------------------------
1 | module.exports = function (app) {
2 | var Data;
3 | Data = app.models.Data;
4 | return app.io.sockets.on('connection', function (socket) {
5 | var subscriptions;
6 | subscriptions = {};
7 | socket.on('subscribe', function (topic) {
8 | var subscription;
9 | subscription = function (currentData) {
10 | return socket.emit("/topics/" + topic, currentData.value);
11 | };
12 | subscriptions[topic] = subscription;
13 | Data.subscribe(topic, subscription);
14 | return Data.find(topic, function (err, data) {
15 | if ((data != null ? data.value : void 0) != null) {
16 | return subscription(data);
17 | }
18 | });
19 | });
20 | return socket.on('disconnect', function () {
21 | var listener, topic, _results;
22 | _results = [];
23 | for (topic in subscriptions) {
24 | listener = subscriptions[topic];
25 | _results.push(Data.unsubscribe(topic, listener));
26 | }
27 | return _results;
28 | });
29 | });
30 | };
31 |
--------------------------------------------------------------------------------
/test/spec_helper.js:
--------------------------------------------------------------------------------
1 | var async, config, env;
2 |
3 | env = require("../diaonan.js");
4 |
5 | async = require("async");
6 |
7 | config = {
8 | host: "127.0.0.1",
9 | port: 6379,
10 | db: 16
11 | };
12 |
13 | module.exports.globalSetup = function() {
14 | if (this.app != null) {
15 | return;
16 | }
17 | this.app = env.app;
18 | env.setup(config);
19 | return env.configure();
20 | };
21 |
22 | module.exports.globalTearDown = function() {
23 | return this.app.redis.client.end();
24 | };
25 |
26 | module.exports.setup = function(done) {
27 | env.setupAscoltatore(config);
28 | return async.parallel([
29 | (function(_this) {
30 | return function(cb) {
31 | return _this.app.ascoltatore.once("ready", cb);
32 | };
33 | })(this), (function(_this) {
34 | return function(cb) {
35 | return _this.app.redis.client.flushdb(cb);
36 | };
37 | })(this)
38 | ], done);
39 | };
40 |
41 | module.exports.tearDown = function(done) {
42 | return this.app.ascoltatore.close(done);
43 | };
44 |
--------------------------------------------------------------------------------
/app/assets/css/lib/scaffolding.less:
--------------------------------------------------------------------------------
1 | //
2 | // Scaffolding
3 | // --------------------------------------------------
4 |
5 |
6 | // Body reset
7 | // -------------------------
8 |
9 | body {
10 | margin: 0;
11 | font-family: @baseFontFamily;
12 | font-size: @baseFontSize;
13 | line-height: @baseLineHeight;
14 | color: @textColor;
15 | background-color: @bodyBackground;
16 | }
17 |
18 |
19 | // Links
20 | // -------------------------
21 |
22 | a {
23 | color: @linkColor;
24 | text-decoration: none;
25 | }
26 | a:hover {
27 | color: @linkColorHover;
28 | text-decoration: underline;
29 | }
30 |
31 |
32 | // Images
33 | // -------------------------
34 |
35 | // Rounded corners
36 | .img-rounded {
37 | .border-radius(6px);
38 | }
39 |
40 | // Add polaroid-esque trim
41 | .img-polaroid {
42 | padding: 4px;
43 | background-color: #fff;
44 | border: 1px solid #ccc;
45 | border: 1px solid rgba(0,0,0,.2);
46 | .box-shadow(0 1px 3px rgba(0,0,0,.1));
47 | }
48 |
49 | // Perfect circle
50 | .img-circle {
51 | .border-radius(500px); // crank the border-radius so it works with most reasonably sized images
52 | }
53 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2012 Matteo Collina
2 |
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "diaonan",
3 | "version": "0.0.1",
4 | "description": "A REST bridge to MQTT, CoAP",
5 | "keywords": [
6 | "CoAP",
7 | "MQTT",
8 | "WebSockets",
9 | "REST"
10 | ],
11 | "author": [
12 | "hello@matteocollina.com",
13 | "h@phodal.com"
14 | ],
15 | "licenses": [
16 | "MIT"
17 | ],
18 | "dependencies": {
19 | "coap": "0.9.1",
20 | "optimist": "= 0.3.4",
21 | "express": "~3.0.1",
22 | "hbs": "= 2.8.0",
23 | "socket.io": "= 0.9.17",
24 | "mqtt": "= 1.1.2",
25 | "redis": "~0.8.1",
26 | "hiredis": "= 0.1.17",
27 | "connect-redis": "1.4.0",
28 | "markdown": "0.3.1",
29 | "mincer": "1.1.0",
30 | "less": "= 1.3.3",
31 | "ascoltatori": "0.18.0",
32 | "underscore": "1.7.0",
33 | "async": "~0.9.0"
34 | },
35 | "devDependencies": {
36 | "zombie": "= 1.4.1",
37 | "mocha": "= 2.1.0",
38 | "sinon": "= 1.3.4",
39 | "chai": "= 1.1.0",
40 | "benchmark": "1.0.0",
41 | "request": "= 2.10.0",
42 | "istanbul": "0.3.2",
43 | "codeclimate-test-reporter": "0.0.4"
44 | },
45 | "scripts": {
46 | "test": "istanbul cover node_modules/mocha/bin/_mocha -R test/models"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/assets/css/lib/responsive.less:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Responsive v2.1.1
3 | *
4 | * Copyright 2012 Twitter, Inc
5 | * Licensed under the Apache License v2.0
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | *
8 | * Designed and built with all the love in the world @twitter by @mdo and @fat.
9 | */
10 |
11 |
12 | // Responsive.less
13 | // For phone and tablet devices
14 | // -------------------------------------------------------------
15 |
16 |
17 | // REPEAT VARIABLES & MIXINS
18 | // -------------------------
19 | // Required since we compile the responsive stuff separately
20 |
21 | @import "variables.less"; // Modify this for custom colors, font-sizes, etc
22 | @import "mixins.less";
23 |
24 |
25 | // RESPONSIVE CLASSES
26 | // ------------------
27 |
28 | @import "responsive-utilities.less";
29 |
30 |
31 | // MEDIA QUERIES
32 | // ------------------
33 |
34 | // Large desktops
35 | @import "responsive-1200px-min.less";
36 |
37 | // Tablets to regular desktops
38 | @import "responsive-768px-979px.less";
39 |
40 | // Phones to portrait tablets and narrow desktops
41 | @import "responsive-767px-max.less";
42 |
43 |
44 | // RESPONSIVE NAVBAR
45 | // ------------------
46 |
47 | // From 979px and below, show a button to toggle navbar contents
48 | @import "responsive-navbar.less";
49 |
--------------------------------------------------------------------------------
/app/assets/css/lib/responsive-utilities.less:
--------------------------------------------------------------------------------
1 | //
2 | // Responsive: Utility classes
3 | // --------------------------------------------------
4 |
5 |
6 | // Hide from screenreaders and browsers
7 | // Credit: HTML5 Boilerplate
8 | .hidden {
9 | display: none;
10 | visibility: hidden;
11 | }
12 |
13 | // Visibility utilities
14 |
15 | // For desktops
16 | .visible-phone { display: none !important; }
17 | .visible-tablet { display: none !important; }
18 | .hidden-phone { }
19 | .hidden-tablet { }
20 | .hidden-desktop { display: none !important; }
21 | .visible-desktop { display: inherit !important; }
22 |
23 | // Tablets & small desktops only
24 | @media (min-width: 768px) and (max-width: 979px) {
25 | // Hide everything else
26 | .hidden-desktop { display: inherit !important; }
27 | .visible-desktop { display: none !important ; }
28 | // Show
29 | .visible-tablet { display: inherit !important; }
30 | // Hide
31 | .hidden-tablet { display: none !important; }
32 | }
33 |
34 | // Phones only
35 | @media (max-width: 767px) {
36 | // Hide everything else
37 | .hidden-desktop { display: inherit !important; }
38 | .visible-desktop { display: none !important; }
39 | // Show
40 | .visible-phone { display: inherit !important; } // Use inherit to restore previous behavior
41 | // Hide
42 | .hidden-phone { display: none !important; }
43 | }
44 |
--------------------------------------------------------------------------------
/app/assets/css/lib/thumbnails.less:
--------------------------------------------------------------------------------
1 | //
2 | // Thumbnails
3 | // --------------------------------------------------
4 |
5 |
6 | // Note: `.thumbnails` and `.thumbnails > li` are overriden in responsive files
7 |
8 | // Make wrapper ul behave like the grid
9 | .thumbnails {
10 | margin-left: -@gridGutterWidth;
11 | list-style: none;
12 | .clearfix();
13 | }
14 | // Fluid rows have no left margin
15 | .row-fluid .thumbnails {
16 | margin-left: 0;
17 | }
18 |
19 | // Float li to make thumbnails appear in a row
20 | .thumbnails > li {
21 | float: left; // Explicity set the float since we don't require .span* classes
22 | margin-bottom: @baseLineHeight;
23 | margin-left: @gridGutterWidth;
24 | }
25 |
26 | // The actual thumbnail (can be `a` or `div`)
27 | .thumbnail {
28 | display: block;
29 | padding: 4px;
30 | line-height: @baseLineHeight;
31 | border: 1px solid #ddd;
32 | .border-radius(4px);
33 | .box-shadow(0 1px 3px rgba(0,0,0,.055));
34 | .transition(all .2s ease-in-out);
35 | }
36 | // Add a hover state for linked versions only
37 | a.thumbnail:hover {
38 | border-color: @linkColor;
39 | .box-shadow(0 1px 4px rgba(0,105,214,.25));
40 | }
41 |
42 | // Images and captions
43 | .thumbnail > img {
44 | display: block;
45 | max-width: 100%;
46 | margin-left: auto;
47 | margin-right: auto;
48 | }
49 | .thumbnail .caption {
50 | padding: 9px;
51 | color: @gray;
52 | }
53 |
--------------------------------------------------------------------------------
/app/assets/css/lib/alerts.less:
--------------------------------------------------------------------------------
1 | //
2 | // Alerts
3 | // --------------------------------------------------
4 |
5 |
6 | // Base styles
7 | // -------------------------
8 |
9 | .alert {
10 | padding: 8px 35px 8px 14px;
11 | margin-bottom: @baseLineHeight;
12 | text-shadow: 0 1px 0 rgba(255,255,255,.5);
13 | background-color: @warningBackground;
14 | border: 1px solid @warningBorder;
15 | .border-radius(4px);
16 | color: @warningText;
17 | }
18 | .alert h4 {
19 | margin: 0;
20 | }
21 |
22 | // Adjust close link position
23 | .alert .close {
24 | position: relative;
25 | top: -2px;
26 | right: -21px;
27 | line-height: @baseLineHeight;
28 | }
29 |
30 |
31 | // Alternate styles
32 | // -------------------------
33 |
34 | .alert-success {
35 | background-color: @successBackground;
36 | border-color: @successBorder;
37 | color: @successText;
38 | }
39 | .alert-danger,
40 | .alert-error {
41 | background-color: @errorBackground;
42 | border-color: @errorBorder;
43 | color: @errorText;
44 | }
45 | .alert-info {
46 | background-color: @infoBackground;
47 | border-color: @infoBorder;
48 | color: @infoText;
49 | }
50 |
51 |
52 | // Block alerts
53 | // -------------------------
54 |
55 | .alert-block {
56 | padding-top: 14px;
57 | padding-bottom: 14px;
58 | }
59 | .alert-block > p,
60 | .alert-block > ul {
61 | margin-bottom: 0;
62 | }
63 | .alert-block p + p {
64 | margin-top: 5px;
65 | }
66 |
--------------------------------------------------------------------------------
/app/assets/css/lib/code.less:
--------------------------------------------------------------------------------
1 | //
2 | // Code (inline and blocK)
3 | // --------------------------------------------------
4 |
5 |
6 | // Inline and block code styles
7 | code,
8 | pre {
9 | padding: 0 3px 2px;
10 | #font > #family > .monospace;
11 | font-size: @baseFontSize - 2;
12 | color: @grayDark;
13 | .border-radius(3px);
14 | }
15 |
16 | // Inline code
17 | code {
18 | padding: 2px 4px;
19 | color: #d14;
20 | background-color: #f7f7f9;
21 | border: 1px solid #e1e1e8;
22 | }
23 |
24 | // Blocks of code
25 | pre {
26 | display: block;
27 | padding: (@baseLineHeight - 1) / 2;
28 | margin: 0 0 @baseLineHeight / 2;
29 | font-size: @baseFontSize - 1; // 14px to 13px
30 | line-height: @baseLineHeight;
31 | word-break: break-all;
32 | word-wrap: break-word;
33 | white-space: pre;
34 | white-space: pre-wrap;
35 | background-color: #f5f5f5;
36 | border: 1px solid #ccc; // fallback for IE7-8
37 | border: 1px solid rgba(0,0,0,.15);
38 | .border-radius(4px);
39 |
40 | // Make prettyprint styles more spaced out for readability
41 | &.prettyprint {
42 | margin-bottom: @baseLineHeight;
43 | }
44 |
45 | // Account for some code outputs that place code tags in pre tags
46 | code {
47 | padding: 0;
48 | color: inherit;
49 | background-color: transparent;
50 | border: 0;
51 | }
52 | }
53 |
54 | // Enable scrollable blocks of code
55 | .pre-scrollable {
56 | max-height: 340px;
57 | overflow-y: scroll;
58 | }
--------------------------------------------------------------------------------
/app/controllers/http_api.js:
--------------------------------------------------------------------------------
1 | module.exports = function (app) {
2 | var Data, io;
3 | io = app.io;
4 | Data = app.models.Data;
5 | app.get(/^\/topics\/(.+)$/, function (req, res) {
6 | var index, topic, topics;
7 | topic = req.params[0];
8 | topics = req.session.topics || [];
9 | index = topics.indexOf(topic);
10 | if (index >= 0) {
11 | topics = [].concat(topics.splice(0, index), topics.splice(index + 1, req.session.topics.length));
12 | }
13 | topics.push(topic);
14 | if (topics.length > 5) {
15 | topics.pull();
16 | }
17 | req.session.topics = topics;
18 | return Data.find(topic, function (err, data) {
19 | var e, type;
20 | type = req.accepts(['txt', 'json', 'html']);
21 | if (type === "html") {
22 | return res.render('topic.hbs', {
23 | topic: topic
24 | });
25 | } else if (err != null) {
26 | return res.send(404);
27 | } else if (type === 'json') {
28 | res.contentType('json');
29 | try {
30 | return res.json(data.value);
31 | } catch (_error) {
32 | e = _error;
33 | return res.json("" + data.value);
34 | }
35 | } else if (type === 'txt') {
36 | return res.send(data.value);
37 | } else {
38 | return res.send(406);
39 | }
40 | });
41 | });
42 | return app.put(/^\/topics\/(.+)$/, function (req, res) {
43 | var payload, topic;
44 | topic = req.params[0];
45 | if (req.is("json")) {
46 | payload = req.body;
47 | } else {
48 | payload = req.body.payload;
49 | }
50 | Data.findOrCreate(topic, payload);
51 | return res.send(204);
52 | });
53 | };
54 |
--------------------------------------------------------------------------------
/app/views/topic.hbs:
--------------------------------------------------------------------------------
1 |
4 |
32 |
33 |
34 | {{ js "topics.js" }}
35 |
38 |
--------------------------------------------------------------------------------
/app/assets/css/lib/pagination.less:
--------------------------------------------------------------------------------
1 | //
2 | // Pagination (multiple pages)
3 | // --------------------------------------------------
4 |
5 |
6 | .pagination {
7 | height: @baseLineHeight * 2;
8 | margin: @baseLineHeight 0;
9 | }
10 | .pagination ul {
11 | display: inline-block;
12 | .ie7-inline-block();
13 | margin-left: 0;
14 | margin-bottom: 0;
15 | .border-radius(3px);
16 | .box-shadow(0 1px 2px rgba(0,0,0,.05));
17 | }
18 | .pagination ul > li {
19 | display: inline;
20 | }
21 | .pagination ul > li > a,
22 | .pagination ul > li > span {
23 | float: left;
24 | padding: 0 14px;
25 | line-height: (@baseLineHeight * 2) - 2;
26 | text-decoration: none;
27 | background-color: @paginationBackground;
28 | border: 1px solid @paginationBorder;
29 | border-left-width: 0;
30 | }
31 | .pagination ul > li > a:hover,
32 | .pagination ul > .active > a,
33 | .pagination ul > .active > span {
34 | background-color: #f5f5f5;
35 | }
36 | .pagination ul > .active > a,
37 | .pagination ul > .active > span {
38 | color: @grayLight;
39 | cursor: default;
40 | }
41 | .pagination ul > .disabled > span,
42 | .pagination ul > .disabled > a,
43 | .pagination ul > .disabled > a:hover {
44 | color: @grayLight;
45 | background-color: transparent;
46 | cursor: default;
47 | }
48 | .pagination ul > li:first-child > a,
49 | .pagination ul > li:first-child > span {
50 | border-left-width: 1px;
51 | .border-radius(3px 0 0 3px);
52 | }
53 | .pagination ul > li:last-child > a,
54 | .pagination ul > li:last-child > span {
55 | .border-radius(0 3px 3px 0);
56 | }
57 |
58 | // Centered
59 | .pagination-centered {
60 | text-align: center;
61 | }
62 | .pagination-right {
63 | text-align: right;
64 | }
65 |
--------------------------------------------------------------------------------
/app/assets/css/lib/bootstrap.less:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap v2.1.1
3 | *
4 | * Copyright 2012 Twitter, Inc
5 | * Licensed under the Apache License v2.0
6 | * http://www.apache.org/licenses/LICENSE-2.0
7 | *
8 | * Designed and built with all the love in the world @twitter by @mdo and @fat.
9 | */
10 |
11 | // CSS Reset
12 | @import "reset.less";
13 |
14 | // Core variables and mixins
15 | @import "variables.less"; // Modify this for custom colors, font-sizes, etc
16 | @import "mixins.less";
17 |
18 | // Grid system and page structure
19 | @import "scaffolding.less";
20 | @import "grid.less";
21 | @import "layouts.less";
22 |
23 | // Base CSS
24 | @import "type.less";
25 | @import "code.less";
26 | @import "forms.less";
27 | @import "tables.less";
28 |
29 | // Components: common
30 | @import "sprites.less";
31 | @import "dropdowns.less";
32 | @import "wells.less";
33 | @import "component-animations.less";
34 | @import "close.less";
35 |
36 | // Components: Buttons & Alerts
37 | @import "buttons.less";
38 | @import "button-groups.less";
39 | @import "alerts.less"; // Note: alerts share common CSS with buttons and thus have styles in buttons.less
40 |
41 | // Components: Nav
42 | @import "navs.less";
43 | @import "navbar.less";
44 | @import "breadcrumbs.less";
45 | @import "pagination.less";
46 | @import "pager.less";
47 |
48 | // Components: Popovers
49 | @import "modals.less";
50 | @import "tooltip.less";
51 | @import "popovers.less";
52 |
53 | // Components: Misc
54 | @import "thumbnails.less";
55 | @import "labels-badges.less";
56 | @import "progress-bars.less";
57 | @import "accordion.less";
58 | @import "carousel.less";
59 | @import "hero-unit.less";
60 |
61 | // Utility classes
62 | @import "utilities.less"; // Has to be last to override when necessary
63 |
--------------------------------------------------------------------------------
/app/assets/css/lib/tooltip.less:
--------------------------------------------------------------------------------
1 | //
2 | // Tooltips
3 | // --------------------------------------------------
4 |
5 |
6 | // Base class
7 | .tooltip {
8 | position: absolute;
9 | z-index: @zindexTooltip;
10 | display: block;
11 | visibility: visible;
12 | padding: 5px;
13 | font-size: 11px;
14 | .opacity(0);
15 | &.in { .opacity(80); }
16 | &.top { margin-top: -3px; }
17 | &.right { margin-left: 3px; }
18 | &.bottom { margin-top: 3px; }
19 | &.left { margin-left: -3px; }
20 | }
21 |
22 | // Wrapper for the tooltip content
23 | .tooltip-inner {
24 | max-width: 200px;
25 | padding: 3px 8px;
26 | color: @tooltipColor;
27 | text-align: center;
28 | text-decoration: none;
29 | background-color: @tooltipBackground;
30 | .border-radius(4px);
31 | }
32 |
33 | // Arrows
34 | .tooltip-arrow {
35 | position: absolute;
36 | width: 0;
37 | height: 0;
38 | border-color: transparent;
39 | border-style: solid;
40 | }
41 | .tooltip {
42 | &.top .tooltip-arrow {
43 | bottom: 0;
44 | left: 50%;
45 | margin-left: -@tooltipArrowWidth;
46 | border-width: @tooltipArrowWidth @tooltipArrowWidth 0;
47 | border-top-color: @tooltipArrowColor;
48 | }
49 | &.right .tooltip-arrow {
50 | top: 50%;
51 | left: 0;
52 | margin-top: -@tooltipArrowWidth;
53 | border-width: @tooltipArrowWidth @tooltipArrowWidth @tooltipArrowWidth 0;
54 | border-right-color: @tooltipArrowColor;
55 | }
56 | &.left .tooltip-arrow {
57 | top: 50%;
58 | right: 0;
59 | margin-top: -@tooltipArrowWidth;
60 | border-width: @tooltipArrowWidth 0 @tooltipArrowWidth @tooltipArrowWidth;
61 | border-left-color: @tooltipArrowColor;
62 | }
63 | &.bottom .tooltip-arrow {
64 | top: 0;
65 | left: 50%;
66 | margin-left: -@tooltipArrowWidth;
67 | border-width: 0 @tooltipArrowWidth @tooltipArrowWidth;
68 | border-bottom-color: @tooltipArrowColor;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-transition.js:
--------------------------------------------------------------------------------
1 | /* ===================================================
2 | * bootstrap-transition.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#transitions
4 | * ===================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ========================================================== */
19 |
20 |
21 | !function ($) {
22 |
23 | $(function () {
24 |
25 | "use strict"; // jshint ;_;
26 |
27 |
28 | /* CSS TRANSITION SUPPORT (http://www.modernizr.com/)
29 | * ======================================================= */
30 |
31 | $.support.transition = (function () {
32 |
33 | var transitionEnd = (function () {
34 |
35 | var el = document.createElement('bootstrap')
36 | , transEndEventNames = {
37 | 'WebkitTransition' : 'webkitTransitionEnd'
38 | , 'MozTransition' : 'transitionend'
39 | , 'OTransition' : 'oTransitionEnd otransitionend'
40 | , 'transition' : 'transitionend'
41 | }
42 | , name
43 |
44 | for (name in transEndEventNames){
45 | if (el.style[name] !== undefined) {
46 | return transEndEventNames[name]
47 | }
48 | }
49 |
50 | }())
51 |
52 | return transitionEnd && {
53 | end: transitionEnd
54 | }
55 |
56 | })()
57 |
58 | })
59 |
60 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/assets/css/lib/labels-badges.less:
--------------------------------------------------------------------------------
1 | //
2 | // Labels and badges
3 | // --------------------------------------------------
4 |
5 |
6 | // Base classes
7 | .label,
8 | .badge {
9 | font-size: @baseFontSize * .846;
10 | font-weight: bold;
11 | line-height: 14px; // ensure proper line-height if floated
12 | color: @white;
13 | vertical-align: baseline;
14 | white-space: nowrap;
15 | text-shadow: 0 -1px 0 rgba(0,0,0,.25);
16 | background-color: @grayLight;
17 | }
18 | // Set unique padding and border-radii
19 | .label {
20 | padding: 1px 4px 2px;
21 | .border-radius(3px);
22 | }
23 | .badge {
24 | padding: 1px 9px 2px;
25 | .border-radius(9px);
26 | }
27 |
28 | // Hover state, but only for links
29 | a {
30 | &.label:hover,
31 | &.badge:hover {
32 | color: @white;
33 | text-decoration: none;
34 | cursor: pointer;
35 | }
36 | }
37 |
38 | // Colors
39 | // Only give background-color difference to links (and to simplify, we don't qualifty with `a` but [href] attribute)
40 | .label,
41 | .badge {
42 | // Important (red)
43 | &-important { background-color: @errorText; }
44 | &-important[href] { background-color: darken(@errorText, 10%); }
45 | // Warnings (orange)
46 | &-warning { background-color: @orange; }
47 | &-warning[href] { background-color: darken(@orange, 10%); }
48 | // Success (green)
49 | &-success { background-color: @successText; }
50 | &-success[href] { background-color: darken(@successText, 10%); }
51 | // Info (turquoise)
52 | &-info { background-color: @infoText; }
53 | &-info[href] { background-color: darken(@infoText, 10%); }
54 | // Inverse (black)
55 | &-inverse { background-color: @grayDark; }
56 | &-inverse[href] { background-color: darken(@grayDark, 10%); }
57 | }
58 |
59 | // Quick fix for labels/badges in buttons
60 | .btn {
61 | .label,
62 | .badge {
63 | position: relative;
64 | top: -1px;
65 | }
66 | }
67 | .btn-mini {
68 | .label,
69 | .badge {
70 | top: 0;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/app/helpers/asset_pipeline.js:
--------------------------------------------------------------------------------
1 | var Mincer, hbs, path;
2 |
3 | hbs = require('hbs');
4 |
5 | path = require('path');
6 |
7 | Mincer = require('mincer');
8 |
9 | module.exports = function (app) {
10 | var environment, find_asset_paths, rewrite_extension;
11 | environment = new Mincer.Environment();
12 | environment.appendPath('app/assets/js');
13 | environment.appendPath('app/assets/css');
14 | app.use("/assets", Mincer.createServer(environment));
15 | rewrite_extension = function (source, ext) {
16 | var source_ext;
17 | source_ext = path.extname(source);
18 | if (source_ext === ext) {
19 | return source;
20 | } else {
21 | return source + ext;
22 | }
23 | };
24 | find_asset_paths = function (logicalPath, ext) {
25 | var asset, paths;
26 | asset = environment.findAsset(logicalPath);
27 | paths = [];
28 | if (!asset) {
29 | return null;
30 | }
31 | if ('production' !== process.env.NODE_ENV && asset.isCompiled) {
32 | asset.toArray().forEach(function (dep) {
33 | return paths.push('/assets/' + rewrite_extension(dep.logicalPath, ext) + '?body=1');
34 | });
35 | } else {
36 | paths.push('/assets/' + rewrite_extension(asset.digestPath, ext));
37 | }
38 | return paths;
39 | };
40 | hbs.registerHelper('js', function (logicalPath) {
41 | var paths, result;
42 | paths = find_asset_paths(logicalPath, ".js");
43 | if (!paths) {
44 | return new hbs.SafeString('');
45 | }
46 | result = paths.map(function (path) {
47 | return '';
48 | });
49 | return new hbs.SafeString(result.join("\n"));
50 | });
51 | return hbs.registerHelper('css', function (logicalPath) {
52 | var paths, result;
53 | paths = find_asset_paths(logicalPath, ".css");
54 | if (!paths) {
55 | return new hbs.SafeString('');
56 | }
57 | result = paths.map(function (path) {
58 | return '';
59 | });
60 | return new hbs.SafeString(result.join("\n"));
61 | });
62 | };
63 |
--------------------------------------------------------------------------------
/app/assets/js/html5-shiv.js:
--------------------------------------------------------------------------------
1 | /*! HTML5 Shiv vpre3.6 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
2 | Uncompressed source: https://github.com/aFarkas/html5shiv */
3 | (function(a,b){function h(a,b){var c=a.createElement("p"),d=a.getElementsByTagName("head")[0]||a.documentElement;return c.innerHTML="x",d.insertBefore(c.lastChild,d.firstChild)}function i(){var a=l.elements;return typeof a=="string"?a.split(" "):a}function j(a){var b={},c=a.createElement,f=a.createDocumentFragment,g=f();a.createElement=function(a){if(!l.shivMethods)return c(a);var f;return b[a]?f=b[a].cloneNode():e.test(a)?f=(b[a]=c(a)).cloneNode():f=c(a),f.canHaveChildren&&!d.test(a)?g.appendChild(f):f},a.createDocumentFragment=Function("h,f","return function(){var n=f.cloneNode(),c=n.createElement;h.shivMethods&&("+i().join().replace(/\w+/g,function(a){return c(a),g.createElement(a),'c("'+a+'")'})+");return n}")(l,g)}function k(a){var b;return a.documentShived?a:(l.shivCSS&&!f&&(b=!!h(a,"article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block}audio{display:none}canvas,video{display:inline-block;*display:inline;*zoom:1}[hidden]{display:none}audio[controls]{display:inline-block;*display:inline;*zoom:1}mark{background:#FF0;color:#000}")),g||(b=!j(a)),b&&(a.documentShived=b),a)}var c=a.html5||{},d=/^<|^(?:button|form|map|select|textarea|object|iframe|option|optgroup)$/i,e=/^<|^(?:a|b|button|code|div|fieldset|form|h1|h2|h3|h4|h5|h6|i|iframe|img|input|label|li|link|ol|option|p|param|q|script|select|span|strong|style|table|tbody|td|textarea|tfoot|th|thead|tr|ul)$/i,f,g;(function(){var c=b.createElement("a");c.innerHTML="",f="hidden"in c,f&&typeof injectElementWithStyles=="function"&&injectElementWithStyles("#modernizr{}",function(b){b.hidden=!0,f=(a.getComputedStyle?getComputedStyle(b,null):b.currentStyle).display=="none"}),g=c.childNodes.length==1||function(){try{b.createElement("a")}catch(a){return!0}var c=b.createDocumentFragment();return typeof c.cloneNode=="undefined"||typeof c.createDocumentFragment=="undefined"||typeof c.createElement=="undefined"}()})();var l={elements:c.elements||"abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video",shivCSS:c.shivCSS!==!1,shivMethods:c.shivMethods!==!1,type:"default",shivDocument:k};a.html5=l,k(b)})(this,document)
--------------------------------------------------------------------------------
/app/controllers/coap_api.js:
--------------------------------------------------------------------------------
1 | module.exports = function (app) {
2 | var Data;
3 | var _ = require("underscore");
4 |
5 | Data = app.models.Data;
6 | return function (req, res) {
7 | console.log(req.method, req.options);
8 | var handlerGet = function () {
9 | if (/^\/topics\/(.+)$/.exec(req.url) === null){
10 | res.code = '4.04';
11 | return res.end(JSON.stringify({error: 4.04, message: "not found"}));
12 | }
13 |
14 | var topic = /^\/topics\/(.+)$/.exec(req.url)[1];
15 | return Data.find(topic, function (err, data) {
16 | var e;
17 | console.log(data.value);
18 | if (err !== null) {
19 | res.code = '4.04';
20 | return res.end(JSON.stringify({error: 4.04}));
21 | } else {
22 | try {
23 | res.code = '2.05';
24 | res.end(JSON.stringify(data.value));
25 | return;
26 | } catch (_error) {
27 | e = _error;
28 | console.log(e);
29 | res.code = '2.06';
30 | res.end(JSON.stringify({error: 4.04, message: e}));
31 | return;
32 | }
33 | }
34 | });
35 | };
36 |
37 | var handPost = function () {
38 | function parse_buffer(req) {
39 | 'use strict';
40 | var results, block =[];
41 | var payload = req.payload.toString();
42 | try {
43 | payload = JSON.parse(payload);
44 | } catch (e) {
45 | console.log(payload);
46 | }
47 | results = {payload: payload};
48 | _.each(req.options, function (option) {
49 | if (/^Block([a-z0-9]{1,})$/.test(option.name)) {
50 | block.push(_.values(option).toString().split(',')[1]);
51 | }
52 | });
53 |
54 | results = _.extend(results, {block: block});
55 | return results;
56 | }
57 |
58 | if (/^\/topics\/(.+)$/.exec(req.url) === null){
59 | res.code = '4.04';
60 | res.end(JSON.stringify({error: 4.04, message: "no permisssion"}));
61 | return;
62 | }
63 | var topic = /^\/topics\/(.+)$/.exec(req.url)[1];
64 | Data.findOrCreate(topic, parse_buffer(req));
65 | res.code = '2.06';
66 | res.end(JSON.stringify({message: parse_buffer(req)}));
67 | };
68 |
69 | var other = function () {
70 | res.code = '4.04';
71 | res.end(JSON.stringify({error: "not support"}));
72 | };
73 |
74 | switch (req.method) {
75 | case "GET":
76 | handlerGet();
77 | break;
78 | case "PUT":
79 | case "POST":
80 | handPost();
81 | break;
82 | default:
83 | other();
84 | break;
85 | }
86 | };
87 | };
88 |
--------------------------------------------------------------------------------
/app/views/layout.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | MQTT协议,CoAP协议,WebSocket,REST,物联网协议在线测试
5 | {{css "main.css"}}
6 |
9 | {{js "bootstrap.js"}}
10 |
11 |
12 |
13 |
14 |
15 |
16 |
31 |
32 |
33 |
34 |
35 | {{{body}}}
36 |
40 |
41 |
42 |
43 |
44 | {{#notest}}
45 |
46 |
62 |
63 | {{/notest}}
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/app/assets/css/lib/modals.less:
--------------------------------------------------------------------------------
1 | //
2 | // Modals
3 | // --------------------------------------------------
4 |
5 |
6 | // Recalculate z-index where appropriate,
7 | // but only apply to elements within modal
8 | .modal-open .modal {
9 | .dropdown-menu { z-index: @zindexDropdown + @zindexModal; }
10 | .dropdown.open { *z-index: @zindexDropdown + @zindexModal; }
11 | .popover { z-index: @zindexPopover + @zindexModal; }
12 | .tooltip { z-index: @zindexTooltip + @zindexModal; }
13 | }
14 |
15 | // Background
16 | .modal-backdrop {
17 | position: fixed;
18 | top: 0;
19 | right: 0;
20 | bottom: 0;
21 | left: 0;
22 | z-index: @zindexModalBackdrop;
23 | background-color: @black;
24 | // Fade for backdrop
25 | &.fade { opacity: 0; }
26 | }
27 |
28 | .modal-backdrop,
29 | .modal-backdrop.fade.in {
30 | .opacity(80);
31 | }
32 |
33 | // Base modal
34 | .modal {
35 | position: fixed;
36 | top: 50%;
37 | left: 50%;
38 | z-index: @zindexModal;
39 | overflow: auto;
40 | width: 560px;
41 | margin: -250px 0 0 -280px;
42 | background-color: @white;
43 | border: 1px solid #999;
44 | border: 1px solid rgba(0,0,0,.3);
45 | *border: 1px solid #999; /* IE6-7 */
46 | .border-radius(6px);
47 | .box-shadow(0 3px 7px rgba(0,0,0,0.3));
48 | .background-clip(padding-box);
49 | &.fade {
50 | .transition(e('opacity .3s linear, top .3s ease-out'));
51 | top: -25%;
52 | }
53 | &.fade.in { top: 50%; }
54 | }
55 | .modal-header {
56 | padding: 9px 15px;
57 | border-bottom: 1px solid #eee;
58 | // Close icon
59 | .close { margin-top: 2px; }
60 | // Heading
61 | h3 {
62 | margin: 0;
63 | line-height: 30px;
64 | }
65 | }
66 |
67 | // Body (where all modal content resides)
68 | .modal-body {
69 | overflow-y: auto;
70 | max-height: 400px;
71 | padding: 15px;
72 | }
73 | // Remove bottom margin if need be
74 | .modal-form {
75 | margin-bottom: 0;
76 | }
77 |
78 | // Footer (for actions)
79 | .modal-footer {
80 | padding: 14px 15px 15px;
81 | margin-bottom: 0;
82 | text-align: right; // right align buttons
83 | background-color: #f5f5f5;
84 | border-top: 1px solid #ddd;
85 | .border-radius(0 0 6px 6px);
86 | .box-shadow(inset 0 1px 0 @white);
87 | .clearfix(); // clear it in case folks use .pull-* classes on buttons
88 |
89 | // Properly space out buttons
90 | .btn + .btn {
91 | margin-left: 5px;
92 | margin-bottom: 0; // account for input[type="submit"] which gets the bottom margin like all other inputs
93 | }
94 | // but override that for button groups
95 | .btn-group .btn + .btn {
96 | margin-left: -1px;
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/app/assets/css/lib/carousel.less:
--------------------------------------------------------------------------------
1 | //
2 | // Carousel
3 | // --------------------------------------------------
4 |
5 |
6 | .carousel {
7 | position: relative;
8 | margin-bottom: @baseLineHeight;
9 | line-height: 1;
10 | }
11 |
12 | .carousel-inner {
13 | overflow: hidden;
14 | width: 100%;
15 | position: relative;
16 | }
17 |
18 | .carousel {
19 |
20 | .item {
21 | display: none;
22 | position: relative;
23 | .transition(.6s ease-in-out left);
24 | }
25 |
26 | // Account for jankitude on images
27 | .item > img {
28 | display: block;
29 | line-height: 1;
30 | }
31 |
32 | .active,
33 | .next,
34 | .prev { display: block; }
35 |
36 | .active {
37 | left: 0;
38 | }
39 |
40 | .next,
41 | .prev {
42 | position: absolute;
43 | top: 0;
44 | width: 100%;
45 | }
46 |
47 | .next {
48 | left: 100%;
49 | }
50 | .prev {
51 | left: -100%;
52 | }
53 | .next.left,
54 | .prev.right {
55 | left: 0;
56 | }
57 |
58 | .active.left {
59 | left: -100%;
60 | }
61 | .active.right {
62 | left: 100%;
63 | }
64 |
65 | }
66 |
67 | // Left/right controls for nav
68 | // ---------------------------
69 |
70 | .carousel-control {
71 | position: absolute;
72 | top: 40%;
73 | left: 15px;
74 | width: 40px;
75 | height: 40px;
76 | margin-top: -20px;
77 | font-size: 60px;
78 | font-weight: 100;
79 | line-height: 30px;
80 | color: @white;
81 | text-align: center;
82 | background: @grayDarker;
83 | border: 3px solid @white;
84 | .border-radius(23px);
85 | .opacity(50);
86 |
87 | // we can't have this transition here
88 | // because webkit cancels the carousel
89 | // animation if you trip this while
90 | // in the middle of another animation
91 | // ;_;
92 | // .transition(opacity .2s linear);
93 |
94 | // Reposition the right one
95 | &.right {
96 | left: auto;
97 | right: 15px;
98 | }
99 |
100 | // Hover state
101 | &:hover {
102 | color: @white;
103 | text-decoration: none;
104 | .opacity(90);
105 | }
106 | }
107 |
108 |
109 | // Caption for text below images
110 | // -----------------------------
111 |
112 | .carousel-caption {
113 | position: absolute;
114 | left: 0;
115 | right: 0;
116 | bottom: 0;
117 | padding: 15px;
118 | background: @grayDark;
119 | background: rgba(0,0,0,.75);
120 | }
121 | .carousel-caption h4,
122 | .carousel-caption p {
123 | color: @white;
124 | line-height: @baseLineHeight;
125 | }
126 | .carousel-caption h4 {
127 | margin: 0 0 5px;
128 | }
129 | .carousel-caption p {
130 | margin-bottom: 0;
131 | }
132 |
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-alert.js:
--------------------------------------------------------------------------------
1 | /* ==========================================================
2 | * bootstrap-alert.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#alerts
4 | * ==========================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ========================================================== */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* ALERT CLASS DEFINITION
27 | * ====================== */
28 |
29 | var dismiss = '[data-dismiss="alert"]'
30 | , Alert = function (el) {
31 | $(el).on('click', dismiss, this.close)
32 | }
33 |
34 | Alert.prototype.close = function (e) {
35 | var $this = $(this)
36 | , selector = $this.attr('data-target')
37 | , $parent
38 |
39 | if (!selector) {
40 | selector = $this.attr('href')
41 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
42 | }
43 |
44 | $parent = $(selector)
45 |
46 | e && e.preventDefault()
47 |
48 | $parent.length || ($parent = $this.hasClass('alert') ? $this : $this.parent())
49 |
50 | $parent.trigger(e = $.Event('close'))
51 |
52 | if (e.isDefaultPrevented()) return
53 |
54 | $parent.removeClass('in')
55 |
56 | function removeElement() {
57 | $parent
58 | .trigger('closed')
59 | .remove()
60 | }
61 |
62 | $.support.transition && $parent.hasClass('fade') ?
63 | $parent.on($.support.transition.end, removeElement) :
64 | removeElement()
65 | }
66 |
67 |
68 | /* ALERT PLUGIN DEFINITION
69 | * ======================= */
70 |
71 | $.fn.alert = function (option) {
72 | return this.each(function () {
73 | var $this = $(this)
74 | , data = $this.data('alert')
75 | if (!data) $this.data('alert', (data = new Alert(this)))
76 | if (typeof option == 'string') data[option].call($this)
77 | })
78 | }
79 |
80 | $.fn.alert.Constructor = Alert
81 |
82 |
83 | /* ALERT DATA-API
84 | * ============== */
85 |
86 | $(function () {
87 | $('body').on('click.alert.data-api', dismiss, Alert.prototype.close)
88 | })
89 |
90 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/controllers/mqtt_api.js:
--------------------------------------------------------------------------------
1 | module.exports = function (app) {
2 | var Data;
3 | Data = app.models.Data;
4 | return function (client) {
5 | var listeners, unsubscribeAll;
6 | listeners = {};
7 | unsubscribeAll = function () {
8 | var listener, topic, _results;
9 | _results = [];
10 | for (topic in listeners) {
11 | listener = listeners[topic];
12 | _results.push(Data.unsubscribe(topic, listener));
13 | }
14 | return _results;
15 | };
16 | client.on('connect', function (packet) {
17 | client.id = packet.client;
18 | return client.connack({
19 | returnCode: 0
20 | });
21 | });
22 | client.on('subscribe', function (packet) {
23 | var granted, subscription, subscriptions, _i, _j, _len, _len1, _ref, _results;
24 | granted = [];
25 | subscriptions = [];
26 | _ref = packet.subscriptions;
27 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
28 | subscription = _ref[_i];
29 | subscriptions.push(subscription.topic.replace("#", "*"));
30 | granted.push(0);
31 | }
32 | client.suback({
33 | messageId: packet.messageId,
34 | granted: granted
35 | });
36 | _results = [];
37 | for (_j = 0, _len1 = subscriptions.length; _j < _len1; _j++) {
38 | subscription = subscriptions[_j];
39 | _results.push((function () {
40 | var listener;
41 | listener = function (data) {
42 | var error, value;
43 | try {
44 | if (typeof data.value === "string") {
45 | value = data.value;
46 | } else {
47 | value = data.jsonValue;
48 | }
49 | return client.publish({
50 | topic: data.key,
51 | payload: value
52 | });
53 | } catch (_error) {
54 | error = _error;
55 | console.log(error);
56 | return client.close();
57 | }
58 | };
59 | listeners[subscription] = listener;
60 | Data.subscribe(subscription, listener);
61 | return Data.find(new RegExp(subscription), function (err, data) {
62 | if (err != null) {
63 | throw err;
64 | }
65 | return listener(data);
66 | });
67 | })());
68 | }
69 | return _results;
70 | });
71 | client.on('publish', function (packet) {
72 | var error, payload;
73 | payload = packet.payload.toString();
74 | try {
75 | payload = JSON.parse(payload);
76 | } catch (_error) {
77 | error = _error;
78 | }
79 | return Data.findOrCreate(packet.topic, payload);
80 | });
81 | client.on('pingreq', function (packet) {
82 | return client.pingresp();
83 | });
84 | client.on('disconnect', function () {
85 | return client.stream.end();
86 | });
87 | client.on('error', function (error) {
88 | console.log(error);
89 | return client.stream.end();
90 | });
91 | client.on('close', function (err) {
92 | return unsubscribeAll();
93 | });
94 | return client.on('unsubscribe', function (packet) {
95 | return client.unsuback({
96 | messageId: packet.messageId
97 | });
98 | });
99 | };
100 | };
101 |
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-button.js:
--------------------------------------------------------------------------------
1 | /* ============================================================
2 | * bootstrap-button.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#buttons
4 | * ============================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ============================================================ */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* BUTTON PUBLIC CLASS DEFINITION
27 | * ============================== */
28 |
29 | var Button = function (element, options) {
30 | this.$element = $(element)
31 | this.options = $.extend({}, $.fn.button.defaults, options)
32 | }
33 |
34 | Button.prototype.setState = function (state) {
35 | var d = 'disabled'
36 | , $el = this.$element
37 | , data = $el.data()
38 | , val = $el.is('input') ? 'val' : 'html'
39 |
40 | state = state + 'Text'
41 | data.resetText || $el.data('resetText', $el[val]())
42 |
43 | $el[val](data[state] || this.options[state])
44 |
45 | // push to event loop to allow forms to submit
46 | setTimeout(function () {
47 | state == 'loadingText' ?
48 | $el.addClass(d).attr(d, d) :
49 | $el.removeClass(d).removeAttr(d)
50 | }, 0)
51 | }
52 |
53 | Button.prototype.toggle = function () {
54 | var $parent = this.$element.closest('[data-toggle="buttons-radio"]')
55 |
56 | $parent && $parent
57 | .find('.active')
58 | .removeClass('active')
59 |
60 | this.$element.toggleClass('active')
61 | }
62 |
63 |
64 | /* BUTTON PLUGIN DEFINITION
65 | * ======================== */
66 |
67 | $.fn.button = function (option) {
68 | return this.each(function () {
69 | var $this = $(this)
70 | , data = $this.data('button')
71 | , options = typeof option == 'object' && option
72 | if (!data) $this.data('button', (data = new Button(this, options)))
73 | if (option == 'toggle') data.toggle()
74 | else if (option) data.setState(option)
75 | })
76 | }
77 |
78 | $.fn.button.defaults = {
79 | loadingText: 'loading...'
80 | }
81 |
82 | $.fn.button.Constructor = Button
83 |
84 |
85 | /* BUTTON DATA-API
86 | * =============== */
87 |
88 | $(function () {
89 | $('body').on('click.button.data-api', '[data-toggle^=button]', function ( e ) {
90 | var $btn = $(e.target)
91 | if (!$btn.hasClass('btn')) $btn = $btn.closest('.btn')
92 | $btn.button('toggle')
93 | })
94 | })
95 |
96 | }(window.jQuery);
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 吊兰
2 |
3 | [](https://travis-ci.org/phodal/diaonan)
4 | [](https://codeclimate.com/github/phodal/diaonan)
5 | [](https://codeclimate.com/github/phodal/diaonan)
6 |
7 | ``吊兰``是一个开源的物联网**平台**。
8 |
9 | 在线Demo: [http://mqtt.phodal.com](http://mqtt.phodal.com)
10 |
11 | 此项目不再``维护``: 请使用[Lan](https://github.com/phodal/lan)(Github: [https://github.com/phodal/lan](https://github.com/phodal/lan)) 用于产品环境。
12 |
13 | Lan构架
14 |
15 | 
16 |
17 |
18 | ###APP: 教你设计物联网
19 |
20 |
21 |
23 |
24 |
25 | ###协议支持###
26 |
27 | - MQTT
28 | - HTTP GET/POST
29 | - WebSocket
30 | - CoAP
31 |
32 |
33 | ###支持设备
34 |
35 | - Arduino
36 | - 8051/51 Family
37 | - Raspberry Pi
38 | - PCduino
39 | - STM32
40 | - ARM
41 | - Android Devices
42 | - iOS Devices
43 | - Windows Phone Devices
44 | - ...
45 |
46 | ##示例
47 |
48 | REST示例如下所示:
49 |
50 | $ curl -X PUT -d '{ "dream": 1 }' \
51 | -H "Content-Type: application/json" \
52 | http://mqtt.phodal.com/topics/lettuce
53 |
54 | $ curl http://mqtt.phodal.com/topics/lettuce
55 | { "dream": 1 }
56 |
57 | Mosquitto示例
58 |
59 | mosquitto_pub -h mqtt.phodal.com -d -t lettuce -m "Hello, MQTT. This is my first message."
60 |
61 | Python MQTT示例:
62 |
63 | import mosquitto
64 | mqttc = mosquitto.Mosquitto("python_pub")
65 | mqttc.connect("mqtt.phodal.com", 1883, 60, True)
66 |
67 | mqttc.publish("lettuce", "Hello, World!")
68 |
69 | CoAP GET示例:
70 |
71 | coap-client -m get coap://mqtt.phodal.com:5683/topics/zero
72 |
73 | CoAP POST示例
74 |
75 | echo -n 'hello world' | coap post coap://mqtt.phodal.com/topics/zero
76 | echo -n '{"lettuce": 123}' | coap post coap://mqtt.phodal.com/topics/zero
77 |
78 | ##安装
79 |
80 | 1.安装redis
81 |
82 | sudo apt-get install redis-server
83 |
84 | or
85 |
86 | sudo yum install redis-server
87 |
88 | 2.安装nodejs依赖
89 |
90 | sudo npm install
91 |
92 | 3.Server
93 |
94 | node diaonan.js
95 |
96 | ##其他相关
97 |
98 | **在线查看**:[一步步搭建物联网系统](http://designiot.phodal.com/)
99 |
100 | [IOT CoAP](https://github.com/phodal/iot-coap)
101 |
102 | [物联网资料合集](https://github.com/phodal/collection-iot)
103 |
104 | [最小物联网系统](https://github.com/phodal/iot)
105 |
106 | ###交流
107 |
108 | QQ群:348100589
109 |
110 | 网站解答: [http://qa.phodal.com](http://qa.phodal.com)
111 |
112 | ## License
113 |
114 | Copyright (c) 2015 Phodal Fengda, [http://www.phodal.com](http://www.phodal.com)
115 |
116 | Copyright (c) 2012 Matteo Collina, [http://matteocollina.com](http://matteocollina.com)
117 |
118 | [待我代码编成,娶你为妻可好](http://www.xuntayizhan.com/person/ji-ke-ai-qing-zhi-er-shi-dai-wo-dai-ma-bian-cheng-qu-ni-wei-qi-ke-hao-wan/)
119 |
--------------------------------------------------------------------------------
/app/assets/js/topics.js:
--------------------------------------------------------------------------------
1 | //= require ./jsonlint.js
2 |
3 | function setupTopic(topic) {
4 | var url = window.location.protocol + '//' + window.location.hostname;
5 | if (window.location.port) {
6 | url += ':' + window.location.port;
7 | }
8 | var socket = io.connect(url)
9 |
10 | var textarea = $("#payload");
11 | var textarea_error = $("#payload-error");
12 | var textarea_field = $("#payload-field");
13 | var update = $("#update");
14 | var edit = $("#edit");
15 | var cancel = $("#cancel");
16 | var validate = $("#validate");
17 |
18 | update.hide();
19 | cancel.hide();
20 |
21 | var last_data = null;
22 |
23 | update_content = function(data) {
24 | data = { payload: data }
25 | last_data = data;
26 | if(textarea.attr("readonly")) {
27 | if(typeof data.payload === "string") {
28 | textarea.val(data.payload);
29 | validate.removeAttr("checked");
30 | } else {
31 | textarea.val(JSON.stringify(data.payload, null, 4));
32 | validate.attr("checked", true);
33 | last_data.json = true;
34 | }
35 | }
36 | };
37 |
38 | socket.on("connect", function(data) {
39 | socket.emit('subscribe', topic);
40 | });
41 |
42 | socket.on("/topics/" + topic, function(data) {
43 | update_content(data);
44 | });
45 |
46 | update.click(function() {
47 | var val = textarea.val();
48 | var settings = {
49 | url: "/topics/"+ topic,
50 | type: "PUT"
51 | }
52 | if(validate.attr("checked")) {
53 | try {
54 | val = JSON.parse(val);
55 | if(typeof val == 'string') {
56 | throw "invalid json";
57 | }
58 | settings.data = JSON.stringify(val);
59 | settings.contentType = "application/json";
60 | } catch(e) {
61 | textarea.addClass("error");
62 | textarea_field.addClass("error");
63 | textarea_error.show();
64 | return false;
65 | }
66 | } else {
67 | settings.data = { payload: val };
68 | }
69 |
70 | $.ajax(settings);
71 | textarea.removeClass("error");
72 | textarea_field.removeClass("error");
73 | textarea_error.hide();
74 |
75 | textarea.attr("readonly", true);
76 | validate.attr("disabled", true);
77 | edit.toggle();
78 | update.toggle();
79 | cancel.toggle();
80 | return false;
81 | });
82 |
83 | edit.click(function() {
84 | textarea.removeAttr("readonly");
85 | validate.removeAttr("disabled");
86 | update.toggle();
87 | cancel.toggle();
88 | edit.toggle();
89 | return false;
90 | });
91 |
92 | cancel.click(function() {
93 | if(last_data.json) {
94 | textarea.val(JSON.stringify(last_data.payload));
95 | } else {
96 | textarea.val(last_data.payload);
97 | }
98 | validate.attr("checked", last_data.json);
99 | textarea.attr("readonly", true);
100 | validate.attr("disabled", true);
101 | update.toggle();
102 | cancel.toggle();
103 | edit.toggle();
104 |
105 | textarea.removeClass("error");
106 | textarea_field.removeClass("error");
107 | textarea_error.hide();
108 | return false;
109 | });
110 | }
111 |
--------------------------------------------------------------------------------
/app/assets/css/lib/progress-bars.less:
--------------------------------------------------------------------------------
1 | //
2 | // Progress bars
3 | // --------------------------------------------------
4 |
5 |
6 | // ANIMATIONS
7 | // ----------
8 |
9 | // Webkit
10 | @-webkit-keyframes progress-bar-stripes {
11 | from { background-position: 40px 0; }
12 | to { background-position: 0 0; }
13 | }
14 |
15 | // Firefox
16 | @-moz-keyframes progress-bar-stripes {
17 | from { background-position: 40px 0; }
18 | to { background-position: 0 0; }
19 | }
20 |
21 | // IE9
22 | @-ms-keyframes progress-bar-stripes {
23 | from { background-position: 40px 0; }
24 | to { background-position: 0 0; }
25 | }
26 |
27 | // Opera
28 | @-o-keyframes progress-bar-stripes {
29 | from { background-position: 0 0; }
30 | to { background-position: 40px 0; }
31 | }
32 |
33 | // Spec
34 | @keyframes progress-bar-stripes {
35 | from { background-position: 40px 0; }
36 | to { background-position: 0 0; }
37 | }
38 |
39 |
40 |
41 | // THE BARS
42 | // --------
43 |
44 | // Outer container
45 | .progress {
46 | overflow: hidden;
47 | height: @baseLineHeight;
48 | margin-bottom: @baseLineHeight;
49 | #gradient > .vertical(#f5f5f5, #f9f9f9);
50 | .box-shadow(inset 0 1px 2px rgba(0,0,0,.1));
51 | .border-radius(4px);
52 | }
53 |
54 | // Bar of progress
55 | .progress .bar {
56 | width: 0%;
57 | height: 100%;
58 | color: @white;
59 | float: left;
60 | font-size: 12px;
61 | text-align: center;
62 | text-shadow: 0 -1px 0 rgba(0,0,0,.25);
63 | #gradient > .vertical(#149bdf, #0480be);
64 | .box-shadow(inset 0 -1px 0 rgba(0,0,0,.15));
65 | .box-sizing(border-box);
66 | .transition(width .6s ease);
67 | }
68 | .progress .bar + .bar {
69 | .box-shadow(inset 1px 0 0 rgba(0,0,0,.15), inset 0 -1px 0 rgba(0,0,0,.15));
70 | }
71 |
72 | // Striped bars
73 | .progress-striped .bar {
74 | #gradient > .striped(#149bdf);
75 | .background-size(40px 40px);
76 | }
77 |
78 | // Call animation for the active one
79 | .progress.active .bar {
80 | -webkit-animation: progress-bar-stripes 2s linear infinite;
81 | -moz-animation: progress-bar-stripes 2s linear infinite;
82 | -ms-animation: progress-bar-stripes 2s linear infinite;
83 | -o-animation: progress-bar-stripes 2s linear infinite;
84 | animation: progress-bar-stripes 2s linear infinite;
85 | }
86 |
87 |
88 |
89 | // COLORS
90 | // ------
91 |
92 | // Danger (red)
93 | .progress-danger .bar, .progress .bar-danger {
94 | #gradient > .vertical(#ee5f5b, #c43c35);
95 | }
96 | .progress-danger.progress-striped .bar, .progress-striped .bar-danger {
97 | #gradient > .striped(#ee5f5b);
98 | }
99 |
100 | // Success (green)
101 | .progress-success .bar, .progress .bar-success {
102 | #gradient > .vertical(#62c462, #57a957);
103 | }
104 | .progress-success.progress-striped .bar, .progress-striped .bar-success {
105 | #gradient > .striped(#62c462);
106 | }
107 |
108 | // Info (teal)
109 | .progress-info .bar, .progress .bar-info {
110 | #gradient > .vertical(#5bc0de, #339bb9);
111 | }
112 | .progress-info.progress-striped .bar, .progress-striped .bar-info {
113 | #gradient > .striped(#5bc0de);
114 | }
115 |
116 | // Warning (orange)
117 | .progress-warning .bar, .progress .bar-warning {
118 | #gradient > .vertical(lighten(@orange, 15%), @orange);
119 | }
120 | .progress-warning.progress-striped .bar, .progress-striped .bar-warning {
121 | #gradient > .striped(lighten(@orange, 15%));
122 | }
123 |
--------------------------------------------------------------------------------
/app/assets/css/lib/reset.less:
--------------------------------------------------------------------------------
1 | //
2 | // Modals
3 | // Adapted from http://github.com/necolas/normalize.css
4 | // --------------------------------------------------
5 |
6 |
7 | // Display in IE6-9 and FF3
8 | // -------------------------
9 |
10 | article,
11 | aside,
12 | details,
13 | figcaption,
14 | figure,
15 | footer,
16 | header,
17 | hgroup,
18 | nav,
19 | section {
20 | display: block;
21 | }
22 |
23 | // Display block in IE6-9 and FF3
24 | // -------------------------
25 |
26 | audio,
27 | canvas,
28 | video {
29 | display: inline-block;
30 | *display: inline;
31 | *zoom: 1;
32 | }
33 |
34 | // Prevents modern browsers from displaying 'audio' without controls
35 | // -------------------------
36 |
37 | audio:not([controls]) {
38 | display: none;
39 | }
40 |
41 | // Base settings
42 | // -------------------------
43 |
44 | html {
45 | font-size: 100%;
46 | -webkit-text-size-adjust: 100%;
47 | -ms-text-size-adjust: 100%;
48 | }
49 | // Focus states
50 | a:focus {
51 | .tab-focus();
52 | }
53 | // Hover & Active
54 | a:hover,
55 | a:active {
56 | outline: 0;
57 | }
58 |
59 | // Prevents sub and sup affecting line-height in all browsers
60 | // -------------------------
61 |
62 | sub,
63 | sup {
64 | position: relative;
65 | font-size: 75%;
66 | line-height: 0;
67 | vertical-align: baseline;
68 | }
69 | sup {
70 | top: -0.5em;
71 | }
72 | sub {
73 | bottom: -0.25em;
74 | }
75 |
76 | // Img border in a's and image quality
77 | // -------------------------
78 |
79 | img {
80 | /* Responsive images (ensure images don't scale beyond their parents) */
81 | max-width: 100%; /* Part 1: Set a maxium relative to the parent */
82 | width: auto\9; /* IE7-8 need help adjusting responsive images */
83 | height: auto; /* Part 2: Scale the height according to the width, otherwise you get stretching */
84 |
85 | vertical-align: middle;
86 | border: 0;
87 | -ms-interpolation-mode: bicubic;
88 | }
89 |
90 | // Prevent max-width from affecting Google Maps
91 | #map_canvas img {
92 | max-width: none;
93 | }
94 |
95 | // Forms
96 | // -------------------------
97 |
98 | // Font size in all browsers, margin changes, misc consistency
99 | button,
100 | input,
101 | select,
102 | textarea {
103 | margin: 0;
104 | font-size: 100%;
105 | vertical-align: middle;
106 | }
107 | button,
108 | input {
109 | *overflow: visible; // Inner spacing ie IE6/7
110 | line-height: normal; // FF3/4 have !important on line-height in UA stylesheet
111 | }
112 | button::-moz-focus-inner,
113 | input::-moz-focus-inner { // Inner padding and border oddities in FF3/4
114 | padding: 0;
115 | border: 0;
116 | }
117 | button,
118 | input[type="button"],
119 | input[type="reset"],
120 | input[type="submit"] {
121 | cursor: pointer; // Cursors on all buttons applied consistently
122 | -webkit-appearance: button; // Style clickable inputs in iOS
123 | }
124 | input[type="search"] { // Appearance in Safari/Chrome
125 | -webkit-box-sizing: content-box;
126 | -moz-box-sizing: content-box;
127 | box-sizing: content-box;
128 | -webkit-appearance: textfield;
129 | }
130 | input[type="search"]::-webkit-search-decoration,
131 | input[type="search"]::-webkit-search-cancel-button {
132 | -webkit-appearance: none; // Inner-padding issues in Chrome OSX, Safari 5
133 | }
134 | textarea {
135 | overflow: auto; // Remove vertical scrollbar in IE6-9
136 | vertical-align: top; // Readability and alignment cross-browser
137 | }
138 |
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-popover.js:
--------------------------------------------------------------------------------
1 | /* ===========================================================
2 | * bootstrap-popover.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#popovers
4 | * ===========================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * =========================================================== */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* POPOVER PUBLIC CLASS DEFINITION
27 | * =============================== */
28 |
29 | var Popover = function (element, options) {
30 | this.init('popover', element, options)
31 | }
32 |
33 |
34 | /* NOTE: POPOVER EXTENDS BOOTSTRAP-TOOLTIP.js
35 | ========================================== */
36 |
37 | Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype, {
38 |
39 | constructor: Popover
40 |
41 | , setContent: function () {
42 | var $tip = this.tip()
43 | , title = this.getTitle()
44 | , content = this.getContent()
45 |
46 | $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title)
47 | $tip.find('.popover-content > *')[this.options.html ? 'html' : 'text'](content)
48 |
49 | $tip.removeClass('fade top bottom left right in')
50 | }
51 |
52 | , hasContent: function () {
53 | return this.getTitle() || this.getContent()
54 | }
55 |
56 | , getContent: function () {
57 | var content
58 | , $e = this.$element
59 | , o = this.options
60 |
61 | content = $e.attr('data-content')
62 | || (typeof o.content == 'function' ? o.content.call($e[0]) : o.content)
63 |
64 | return content
65 | }
66 |
67 | , tip: function () {
68 | if (!this.$tip) {
69 | this.$tip = $(this.options.template)
70 | }
71 | return this.$tip
72 | }
73 |
74 | , destroy: function () {
75 | this.hide().$element.off('.' + this.type).removeData(this.type)
76 | }
77 |
78 | })
79 |
80 |
81 | /* POPOVER PLUGIN DEFINITION
82 | * ======================= */
83 |
84 | $.fn.popover = function (option) {
85 | return this.each(function () {
86 | var $this = $(this)
87 | , data = $this.data('popover')
88 | , options = typeof option == 'object' && option
89 | if (!data) $this.data('popover', (data = new Popover(this, options)))
90 | if (typeof option == 'string') data[option]()
91 | })
92 | }
93 |
94 | $.fn.popover.Constructor = Popover
95 |
96 | $.fn.popover.defaults = $.extend({} , $.fn.tooltip.defaults, {
97 | placement: 'right'
98 | , trigger: 'click'
99 | , content: ''
100 | , template: '
'
101 | })
102 |
103 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/assets/css/lib/popovers.less:
--------------------------------------------------------------------------------
1 | //
2 | // Popovers
3 | // --------------------------------------------------
4 |
5 |
6 | .popover {
7 | position: absolute;
8 | top: 0;
9 | left: 0;
10 | z-index: @zindexPopover;
11 | display: none;
12 | width: 236px;
13 | padding: 1px;
14 | background-color: @popoverBackground;
15 | -webkit-background-clip: padding-box;
16 | -moz-background-clip: padding;
17 | background-clip: padding-box;
18 | border: 1px solid #ccc;
19 | border: 1px solid rgba(0,0,0,.2);
20 | .border-radius(6px);
21 | .box-shadow(0 5px 10px rgba(0,0,0,.2));
22 |
23 | // Offset the popover to account for the popover arrow
24 | &.top { margin-bottom: 10px; }
25 | &.right { margin-left: 10px; }
26 | &.bottom { margin-top: 10px; }
27 | &.left { margin-right: 10px; }
28 |
29 | }
30 |
31 | .popover-title {
32 | margin: 0; // reset heading margin
33 | padding: 8px 14px;
34 | font-size: 14px;
35 | font-weight: normal;
36 | line-height: 18px;
37 | background-color: @popoverTitleBackground;
38 | border-bottom: 1px solid darken(@popoverTitleBackground, 5%);
39 | .border-radius(5px 5px 0 0);
40 | }
41 |
42 | .popover-content {
43 | padding: 9px 14px;
44 | p, ul, ol {
45 | margin-bottom: 0;
46 | }
47 | }
48 |
49 | // Arrows
50 | .popover .arrow,
51 | .popover .arrow:after {
52 | position: absolute;
53 | display: inline-block;
54 | width: 0;
55 | height: 0;
56 | border-color: transparent;
57 | border-style: solid;
58 | }
59 | .popover .arrow:after {
60 | content: "";
61 | z-index: -1;
62 | }
63 |
64 | .popover {
65 | &.top .arrow {
66 | bottom: -@popoverArrowWidth;
67 | left: 50%;
68 | margin-left: -@popoverArrowWidth;
69 | border-width: @popoverArrowWidth @popoverArrowWidth 0;
70 | border-top-color: @popoverArrowColor;
71 | &:after {
72 | border-width: @popoverArrowOuterWidth @popoverArrowOuterWidth 0;
73 | border-top-color: @popoverArrowOuterColor;
74 | bottom: -1px;
75 | left: -@popoverArrowOuterWidth;
76 | }
77 | }
78 | &.right .arrow {
79 | top: 50%;
80 | left: -@popoverArrowWidth;
81 | margin-top: -@popoverArrowWidth;
82 | border-width: @popoverArrowWidth @popoverArrowWidth @popoverArrowWidth 0;
83 | border-right-color: @popoverArrowColor;
84 | &:after {
85 | border-width: @popoverArrowOuterWidth @popoverArrowOuterWidth @popoverArrowOuterWidth 0;
86 | border-right-color: @popoverArrowOuterColor;
87 | bottom: -@popoverArrowOuterWidth;
88 | left: -1px;
89 | }
90 | }
91 | &.bottom .arrow {
92 | top: -@popoverArrowWidth;
93 | left: 50%;
94 | margin-left: -@popoverArrowWidth;
95 | border-width: 0 @popoverArrowWidth @popoverArrowWidth;
96 | border-bottom-color: @popoverArrowColor;
97 | &:after {
98 | border-width: 0 @popoverArrowOuterWidth @popoverArrowOuterWidth;
99 | border-bottom-color: @popoverArrowOuterColor;
100 | top: -1px;
101 | left: -@popoverArrowOuterWidth;
102 | }
103 | }
104 | &.left .arrow {
105 | top: 50%;
106 | right: -@popoverArrowWidth;
107 | margin-top: -@popoverArrowWidth;
108 | border-width: @popoverArrowWidth 0 @popoverArrowWidth @popoverArrowWidth;
109 | border-left-color: @popoverArrowColor;
110 | &:after {
111 | border-width: @popoverArrowOuterWidth 0 @popoverArrowOuterWidth @popoverArrowOuterWidth;
112 | border-left-color: @popoverArrowOuterColor;
113 | bottom: -@popoverArrowOuterWidth;
114 | right: -1px;
115 | }
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-affix.js:
--------------------------------------------------------------------------------
1 | /* ==========================================================
2 | * bootstrap-affix.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#affix
4 | * ==========================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ========================================================== */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* AFFIX CLASS DEFINITION
27 | * ====================== */
28 |
29 | var Affix = function (element, options) {
30 | this.options = $.extend({}, $.fn.affix.defaults, options)
31 | this.$window = $(window).on('scroll.affix.data-api', $.proxy(this.checkPosition, this))
32 | this.$element = $(element)
33 | this.checkPosition()
34 | }
35 |
36 | Affix.prototype.checkPosition = function () {
37 | if (!this.$element.is(':visible')) return
38 |
39 | var scrollHeight = $(document).height()
40 | , scrollTop = this.$window.scrollTop()
41 | , position = this.$element.offset()
42 | , offset = this.options.offset
43 | , offsetBottom = offset.bottom
44 | , offsetTop = offset.top
45 | , reset = 'affix affix-top affix-bottom'
46 | , affix
47 |
48 | if (typeof offset != 'object') offsetBottom = offsetTop = offset
49 | if (typeof offsetTop == 'function') offsetTop = offset.top()
50 | if (typeof offsetBottom == 'function') offsetBottom = offset.bottom()
51 |
52 | affix = this.unpin != null && (scrollTop + this.unpin <= position.top) ?
53 | false : offsetBottom != null && (position.top + this.$element.height() >= scrollHeight - offsetBottom) ?
54 | 'bottom' : offsetTop != null && scrollTop <= offsetTop ?
55 | 'top' : false
56 |
57 | if (this.affixed === affix) return
58 |
59 | this.affixed = affix
60 | this.unpin = affix == 'bottom' ? position.top - scrollTop : null
61 |
62 | this.$element.removeClass(reset).addClass('affix' + (affix ? '-' + affix : ''))
63 | }
64 |
65 |
66 | /* AFFIX PLUGIN DEFINITION
67 | * ======================= */
68 |
69 | $.fn.affix = function (option) {
70 | return this.each(function () {
71 | var $this = $(this)
72 | , data = $this.data('affix')
73 | , options = typeof option == 'object' && option
74 | if (!data) $this.data('affix', (data = new Affix(this, options)))
75 | if (typeof option == 'string') data[option]()
76 | })
77 | }
78 |
79 | $.fn.affix.Constructor = Affix
80 |
81 | $.fn.affix.defaults = {
82 | offset: 0
83 | }
84 |
85 |
86 | /* AFFIX DATA-API
87 | * ============== */
88 |
89 | $(window).on('load', function () {
90 | $('[data-spy="affix"]').each(function () {
91 | var $spy = $(this)
92 | , data = $spy.data()
93 |
94 | data.offset = data.offset || {}
95 |
96 | data.offsetBottom && (data.offset.bottom = data.offsetBottom)
97 | data.offsetTop && (data.offset.top = data.offsetTop)
98 |
99 | $spy.affix(data)
100 | })
101 | })
102 |
103 |
104 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/models/data.js:
--------------------------------------------------------------------------------
1 | var EventEmitter, KEYS_SET_NAME, events, globalEventEmitter;
2 |
3 | EventEmitter = require('events').EventEmitter;
4 |
5 | globalEventEmitter = new EventEmitter();
6 |
7 | globalEventEmitter.setMaxListeners(0);
8 |
9 | events = {};
10 |
11 | KEYS_SET_NAME = 'topics';
12 |
13 | module.exports = function (app) {
14 | var Data, buildKey;
15 | buildKey = function (key) {
16 | return "topic:" + key;
17 | };
18 | Data = (function () {
19 | function Data(key, value) {
20 | this.key = key;
21 | this.value = value;
22 | this.value || (this.value = null);
23 | }
24 |
25 | Object.defineProperty(Data.prototype, 'key', {
26 | enumerable: true,
27 | configurable: false,
28 | get: function () {
29 | return this._key;
30 | },
31 | set: function (key) {
32 | this.redisKey = buildKey(key);
33 | return this._key = key;
34 | }
35 | });
36 |
37 | Object.defineProperty(Data.prototype, 'jsonValue', {
38 | configurable: false,
39 | enumerable: true,
40 | get: function () {
41 | return JSON.stringify(this.value);
42 | },
43 | set: function (val) {
44 | return this.value = JSON.parse(val);
45 | }
46 | });
47 |
48 | Data.prototype.save = function (callback) {
49 | app.redis.client.set(this.redisKey, this.jsonValue, (function (_this) {
50 | return function (err) {
51 | return app.ascoltatore.publish(_this.key, _this.value, function () {
52 | if (callback != null) {
53 | return callback(err, _this);
54 | }
55 | });
56 | };
57 | })(this));
58 | return app.redis.client.sadd(KEYS_SET_NAME, this.key);
59 | };
60 |
61 | return Data;
62 |
63 | })();
64 | Data.find = function (pattern, callback) {
65 | var foundRecord;
66 | foundRecord = function (key) {
67 | return app.redis.client.get(buildKey(key), function (err, value) {
68 | if (err) {
69 | if (callback != null) {
70 | callback(err);
71 | }
72 | return;
73 | }
74 | if (value == null) {
75 | if (callback != null) {
76 | callback("Record not found");
77 | }
78 | return;
79 | }
80 | if (callback != null) {
81 | return callback(null, Data.fromRedis(key, value));
82 | }
83 | });
84 | };
85 | if (pattern.constructor !== RegExp) {
86 | foundRecord(pattern);
87 | } else {
88 | app.redis.client.smembers(KEYS_SET_NAME, function (err, topics) {
89 | var topic, _i, _len, _results;
90 | _results = [];
91 | for (_i = 0, _len = topics.length; _i < _len; _i++) {
92 | topic = topics[_i];
93 | if (pattern.test(topic)) {
94 | _results.push(foundRecord(topic));
95 | } else {
96 | _results.push(void 0);
97 | }
98 | }
99 | return _results;
100 | });
101 | }
102 | return Data;
103 | };
104 | Data.findOrCreate = function () {
105 | var arg, args, callback, key, value;
106 | args = Array.prototype.slice.call(arguments);
107 | key = args.shift();
108 | arg = args.shift();
109 | if (typeof arg === 'function') {
110 | callback = arg;
111 | } else {
112 | value = arg;
113 | callback = args.shift();
114 | }
115 | app.redis.client.get(buildKey(key), function (err, oldValue) {
116 | var data;
117 | data = Data.fromRedis(key, oldValue);
118 | if (value != null) {
119 | data.value = value;
120 | }
121 | return data.save(callback);
122 | });
123 | return Data;
124 | };
125 | Data.fromRedis = function (topic, value) {
126 | var data;
127 | data = new Data(topic);
128 | data.jsonValue = value;
129 | return data;
130 | };
131 | Data.subscribe = function (topic, callback) {
132 | callback._subscriber = function (actualTopic, value) {
133 | return callback(new Data(actualTopic, value));
134 | };
135 | app.ascoltatore.subscribe(topic, callback._subscriber);
136 | return this;
137 | };
138 | Data.unsubscribe = function (topic, callback) {
139 | app.ascoltatore.unsubscribe(topic, callback._subscriber);
140 | return this;
141 | };
142 | return Data;
143 | };
144 |
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-tab.js:
--------------------------------------------------------------------------------
1 | /* ========================================================
2 | * bootstrap-tab.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#tabs
4 | * ========================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ======================================================== */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* TAB CLASS DEFINITION
27 | * ==================== */
28 |
29 | var Tab = function (element) {
30 | this.element = $(element)
31 | }
32 |
33 | Tab.prototype = {
34 |
35 | constructor: Tab
36 |
37 | , show: function () {
38 | var $this = this.element
39 | , $ul = $this.closest('ul:not(.dropdown-menu)')
40 | , selector = $this.attr('data-target')
41 | , previous
42 | , $target
43 | , e
44 |
45 | if (!selector) {
46 | selector = $this.attr('href')
47 | selector = selector && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
48 | }
49 |
50 | if ( $this.parent('li').hasClass('active') ) return
51 |
52 | previous = $ul.find('.active a').last()[0]
53 |
54 | e = $.Event('show', {
55 | relatedTarget: previous
56 | })
57 |
58 | $this.trigger(e)
59 |
60 | if (e.isDefaultPrevented()) return
61 |
62 | $target = $(selector)
63 |
64 | this.activate($this.parent('li'), $ul)
65 | this.activate($target, $target.parent(), function () {
66 | $this.trigger({
67 | type: 'shown'
68 | , relatedTarget: previous
69 | })
70 | })
71 | }
72 |
73 | , activate: function ( element, container, callback) {
74 | var $active = container.find('> .active')
75 | , transition = callback
76 | && $.support.transition
77 | && $active.hasClass('fade')
78 |
79 | function next() {
80 | $active
81 | .removeClass('active')
82 | .find('> .dropdown-menu > .active')
83 | .removeClass('active')
84 |
85 | element.addClass('active')
86 |
87 | if (transition) {
88 | element[0].offsetWidth // reflow for transition
89 | element.addClass('in')
90 | } else {
91 | element.removeClass('fade')
92 | }
93 |
94 | if ( element.parent('.dropdown-menu') ) {
95 | element.closest('li.dropdown').addClass('active')
96 | }
97 |
98 | callback && callback()
99 | }
100 |
101 | transition ?
102 | $active.one($.support.transition.end, next) :
103 | next()
104 |
105 | $active.removeClass('in')
106 | }
107 | }
108 |
109 |
110 | /* TAB PLUGIN DEFINITION
111 | * ===================== */
112 |
113 | $.fn.tab = function ( option ) {
114 | return this.each(function () {
115 | var $this = $(this)
116 | , data = $this.data('tab')
117 | if (!data) $this.data('tab', (data = new Tab(this)))
118 | if (typeof option == 'string') data[option]()
119 | })
120 | }
121 |
122 | $.fn.tab.Constructor = Tab
123 |
124 |
125 | /* TAB DATA-API
126 | * ============ */
127 |
128 | $(function () {
129 | $('body').on('click.tab.data-api', '[data-toggle="tab"], [data-toggle="pill"]', function (e) {
130 | e.preventDefault()
131 | $(this).tab('show')
132 | })
133 | })
134 |
135 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/views/home.hbs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
让我们创造、连接一切的节点。
8 |
9 |
10 |
17 |
最近的节点
18 | {{#if topics.length }}
19 |
20 | {{#each topics}}
21 | - {{this}}
22 | {{/each}}
23 |
24 | {{else}}
25 |
26 | 没有节点,创建新的节点!
27 |
28 | {{/if}}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
创建
38 |
39 |
吊兰是一个在线的物联网测试平台,用他来测试你的物联网设备。
40 | 你可以在上面打开一个节点,也可以直接用MQTT,REST,CoAP,WebSocket等相应的来测试你的代码、硬件等等。
41 |
42 | {{#markdown}}
43 |
44 | REST示例如下所示:
45 |
46 | $ curl -X PUT -d '{ "dream": 1 }' -H "Content-Type: application/json" http://mqtt.phodal.com/topics/lettuce
47 |
48 | $ curl http://mqtt.phodal.com/topics/lettuce
49 | { "dream": 1 }
50 |
51 | Mosquitto示例
52 |
53 | mosquitto_pub -h mqtt.phodal.com -d -t lettuce -m "Hello, MQTT. This is my first message."
54 |
55 | Python MQTT示例:
56 |
57 | import mosquitto
58 | mqttc = mosquitto.Mosquitto("python_pub")
59 | mqttc.connect("mqtt.phodal.com", 1883, 60, True)
60 |
61 | mqttc.publish("lettuce", "Hello, World!")
62 |
63 | CoAP GET示例:
64 |
65 | coap-client -m get coap://mqtt.phodal.com:5683/topics/zero
66 |
67 | CoAP POST示例
68 |
69 | echo -n 'hello world' | coap post coap://mqtt.phodal.com/topics/zero
70 | echo -n '{"lettuce": 123}' | coap post coap://mqtt.phodal.com/topics/zero
71 |
72 | Arduino MQTT示例
73 |
74 | #include
75 | #include
76 | #include
77 |
78 | byte mac[] = { 0xDE, 0xED, 0xBA, 0xFE, 0xFE, 0xED };
79 | byte server[] = { 192, 168, 168, 78 }; // MQTT服务地址
80 | byte ip[] = { 192, 168, 168, 250 }; // 设备IP
81 |
82 | void callback(char* topic, byte* payload, unsigned int length) {
83 | // handle message arrived
84 | }
85 |
86 | EthernetClient ethClient;
87 | PubSubClient client(server, 1883, callback, ethClient);
88 |
89 | void setup()
90 | {
91 | Ethernet.begin(mac, ip);
92 | if (client.connect("arduinoClient")) {
93 | client.publish("outTopic","hello world");
94 | client.subscribe("inTopic");
95 | }
96 | }
97 |
98 | void loop()
99 | {
100 | client.loop();
101 | }
102 |
103 | 让我们用物联网做一些有趣的事!
104 |
105 | {{/markdown}}
106 |
107 |
108 |
109 |
More
110 |
111 | {{#markdown}}
112 |
113 | ###协议支持###
114 |
115 | - MQTT
116 | - RESTful(HTTP GET/POST)
117 | - WebSocket
118 | - CoAP
119 | {{/markdown}}
120 |
121 |
122 |
支持设备
123 | {{#markdown}}
124 |
125 | - Arduino
126 | - 8051/51 Family
127 | - Raspberry Pi
128 | - PCduino
129 | - STM32
130 | - ARM
131 | - Android Devices
132 | - iOS Devices
133 | - Windows Phone Devices
134 | - ...
135 |
136 | {{/markdown}}
137 |
138 |
139 | {{#markdown}}
140 |
141 | ###APP
142 |
143 | [教你设计物联网](https://play.google.com/store/apps/details?id=com.phodal.designiot)
144 |
145 | ###开源物联网项目
146 |
147 | [最小物联网系统HTTP版(PHP)](https://github.com/phodal/iot)
148 |
149 | [最小物联网系统CoAP版(NODE.JS)](https://github.com/phodal/iot-coap)
150 |
151 | [《物联网相关资料收集》](https://github.com/phodal/collection-iot)
152 |
153 | [《一步步搭建物联网系统》](https://github.com/phodal/designiot)
154 |
155 | ###交流
156 |
157 | QQ群:348100589
158 |
159 | ###联系我
160 |
161 | [h@phodal.com](mailto:h@phodal.com)
162 |
163 | {{/markdown}}
164 |
165 |
166 |
167 | {{ js "home.js" }}
168 |
--------------------------------------------------------------------------------
/app/assets/css/lib/responsive-767px-max.less:
--------------------------------------------------------------------------------
1 | //
2 | // Responsive: Landscape phone to desktop/tablet
3 | // --------------------------------------------------
4 |
5 |
6 | @media (max-width: 767px) {
7 |
8 | // Padding to set content in a bit
9 | body {
10 | padding-left: 20px;
11 | padding-right: 20px;
12 | }
13 | // Negative indent the now static "fixed" navbar
14 | .navbar-fixed-top,
15 | .navbar-fixed-bottom,
16 | .navbar-static-top {
17 | margin-left: -20px;
18 | margin-right: -20px;
19 | }
20 | // Remove padding on container given explicit padding set on body
21 | .container-fluid {
22 | padding: 0;
23 | }
24 |
25 | // TYPOGRAPHY
26 | // ----------
27 | // Reset horizontal dl
28 | .dl-horizontal {
29 | dt {
30 | float: none;
31 | clear: none;
32 | width: auto;
33 | text-align: left;
34 | }
35 | dd {
36 | margin-left: 0;
37 | }
38 | }
39 |
40 | // GRID & CONTAINERS
41 | // -----------------
42 | // Remove width from containers
43 | .container {
44 | width: auto;
45 | }
46 | // Fluid rows
47 | .row-fluid {
48 | width: 100%;
49 | }
50 | // Undo negative margin on rows and thumbnails
51 | .row,
52 | .thumbnails {
53 | margin-left: 0;
54 | }
55 | .thumbnails > li {
56 | float: none;
57 | margin-left: 0; // Reset the default margin for all li elements when no .span* classes are present
58 | }
59 | // Make all grid-sized elements block level again
60 | [class*="span"],
61 | .row-fluid [class*="span"] {
62 | float: none;
63 | display: block;
64 | width: 100%;
65 | margin-left: 0;
66 | .box-sizing(border-box);
67 | }
68 | .span12,
69 | .row-fluid .span12 {
70 | width: 100%;
71 | .box-sizing(border-box);
72 | }
73 |
74 | // FORM FIELDS
75 | // -----------
76 | // Make span* classes full width
77 | .input-large,
78 | .input-xlarge,
79 | .input-xxlarge,
80 | input[class*="span"],
81 | select[class*="span"],
82 | textarea[class*="span"],
83 | .uneditable-input {
84 | .input-block-level();
85 | }
86 | // But don't let it screw up prepend/append inputs
87 | .input-prepend input,
88 | .input-append input,
89 | .input-prepend input[class*="span"],
90 | .input-append input[class*="span"] {
91 | display: inline-block; // redeclare so they don't wrap to new lines
92 | width: auto;
93 | }
94 | .controls-row [class*="span"] + [class*="span"] {
95 | margin-left: 0;
96 | }
97 |
98 | // Modals
99 | .modal {
100 | position: fixed;
101 | top: 20px;
102 | left: 20px;
103 | right: 20px;
104 | width: auto;
105 | margin: 0;
106 | &.fade.in { top: auto; }
107 | }
108 |
109 | }
110 |
111 |
112 |
113 | // UP TO LANDSCAPE PHONE
114 | // ---------------------
115 |
116 | @media (max-width: 480px) {
117 |
118 | // Smooth out the collapsing/expanding nav
119 | .nav-collapse {
120 | -webkit-transform: translate3d(0, 0, 0); // activate the GPU
121 | }
122 |
123 | // Block level the page header small tag for readability
124 | .page-header h1 small {
125 | display: block;
126 | line-height: @baseLineHeight;
127 | }
128 |
129 | // Update checkboxes for iOS
130 | input[type="checkbox"],
131 | input[type="radio"] {
132 | border: 1px solid #ccc;
133 | }
134 |
135 | // Remove the horizontal form styles
136 | .form-horizontal {
137 | .control-label {
138 | float: none;
139 | width: auto;
140 | padding-top: 0;
141 | text-align: left;
142 | }
143 | // Move over all input controls and content
144 | .controls {
145 | margin-left: 0;
146 | }
147 | // Move the options list down to align with labels
148 | .control-list {
149 | padding-top: 0; // has to be padding because margin collaspes
150 | }
151 | // Move over buttons in .form-actions to align with .controls
152 | .form-actions {
153 | padding-left: 10px;
154 | padding-right: 10px;
155 | }
156 | }
157 |
158 | // Modals
159 | .modal {
160 | top: 10px;
161 | left: 10px;
162 | right: 10px;
163 | }
164 | .modal-header .close {
165 | padding: 10px;
166 | margin: -10px;
167 | }
168 |
169 | // Carousel
170 | .carousel-caption {
171 | position: static;
172 | }
173 |
174 | }
175 |
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-dropdown.js:
--------------------------------------------------------------------------------
1 | /* ============================================================
2 | * bootstrap-dropdown.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#dropdowns
4 | * ============================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ============================================================ */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* DROPDOWN CLASS DEFINITION
27 | * ========================= */
28 |
29 | var toggle = '[data-toggle=dropdown]'
30 | , Dropdown = function (element) {
31 | var $el = $(element).on('click.dropdown.data-api', this.toggle)
32 | $('html').on('click.dropdown.data-api', function () {
33 | $el.parent().removeClass('open')
34 | })
35 | }
36 |
37 | Dropdown.prototype = {
38 |
39 | constructor: Dropdown
40 |
41 | , toggle: function (e) {
42 | var $this = $(this)
43 | , $parent
44 | , isActive
45 |
46 | if ($this.is('.disabled, :disabled')) return
47 |
48 | $parent = getParent($this)
49 |
50 | isActive = $parent.hasClass('open')
51 |
52 | clearMenus()
53 |
54 | if (!isActive) {
55 | $parent.toggleClass('open')
56 | $this.focus()
57 | }
58 |
59 | return false
60 | }
61 |
62 | , keydown: function (e) {
63 | var $this
64 | , $items
65 | , $active
66 | , $parent
67 | , isActive
68 | , index
69 |
70 | if (!/(38|40|27)/.test(e.keyCode)) return
71 |
72 | $this = $(this)
73 |
74 | e.preventDefault()
75 | e.stopPropagation()
76 |
77 | if ($this.is('.disabled, :disabled')) return
78 |
79 | $parent = getParent($this)
80 |
81 | isActive = $parent.hasClass('open')
82 |
83 | if (!isActive || (isActive && e.keyCode == 27)) return $this.click()
84 |
85 | $items = $('[role=menu] li:not(.divider) a', $parent)
86 |
87 | if (!$items.length) return
88 |
89 | index = $items.index($items.filter(':focus'))
90 |
91 | if (e.keyCode == 38 && index > 0) index-- // up
92 | if (e.keyCode == 40 && index < $items.length - 1) index++ // down
93 | if (!~index) index = 0
94 |
95 | $items
96 | .eq(index)
97 | .focus()
98 | }
99 |
100 | }
101 |
102 | function clearMenus() {
103 | getParent($(toggle))
104 | .removeClass('open')
105 | }
106 |
107 | function getParent($this) {
108 | var selector = $this.attr('data-target')
109 | , $parent
110 |
111 | if (!selector) {
112 | selector = $this.attr('href')
113 | selector = selector && /#/.test(selector) && selector.replace(/.*(?=#[^\s]*$)/, '') //strip for ie7
114 | }
115 |
116 | $parent = $(selector)
117 | $parent.length || ($parent = $this.parent())
118 |
119 | return $parent
120 | }
121 |
122 |
123 | /* DROPDOWN PLUGIN DEFINITION
124 | * ========================== */
125 |
126 | $.fn.dropdown = function (option) {
127 | return this.each(function () {
128 | var $this = $(this)
129 | , data = $this.data('dropdown')
130 | if (!data) $this.data('dropdown', (data = new Dropdown(this)))
131 | if (typeof option == 'string') data[option].call($this)
132 | })
133 | }
134 |
135 | $.fn.dropdown.Constructor = Dropdown
136 |
137 |
138 | /* APPLY TO STANDARD DROPDOWN ELEMENTS
139 | * =================================== */
140 |
141 | $(function () {
142 | $('html')
143 | .on('click.dropdown.data-api touchstart.dropdown.data-api', clearMenus)
144 | $('body')
145 | .on('click.dropdown touchstart.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() })
146 | .on('click.dropdown.data-api touchstart.dropdown.data-api' , toggle, Dropdown.prototype.toggle)
147 | .on('keydown.dropdown.data-api touchstart.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown)
148 | })
149 |
150 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/assets/css/lib/responsive-navbar.less:
--------------------------------------------------------------------------------
1 | //
2 | // Responsive: Navbar
3 | // --------------------------------------------------
4 |
5 |
6 | // TABLETS AND BELOW
7 | // -----------------
8 | @media (max-width: @navbarCollapseWidth) {
9 |
10 | // UNFIX THE TOPBAR
11 | // ----------------
12 | // Remove any padding from the body
13 | body {
14 | padding-top: 0;
15 | }
16 | // Unfix the navbars
17 | .navbar-fixed-top,
18 | .navbar-fixed-bottom {
19 | position: static;
20 | }
21 | .navbar-fixed-top {
22 | margin-bottom: @baseLineHeight;
23 | }
24 | .navbar-fixed-bottom {
25 | margin-top: @baseLineHeight;
26 | }
27 | .navbar-fixed-top .navbar-inner,
28 | .navbar-fixed-bottom .navbar-inner {
29 | padding: 5px;
30 | }
31 | .navbar .container {
32 | width: auto;
33 | padding: 0;
34 | }
35 | // Account for brand name
36 | .navbar .brand {
37 | padding-left: 10px;
38 | padding-right: 10px;
39 | margin: 0 0 0 -5px;
40 | }
41 |
42 | // COLLAPSIBLE NAVBAR
43 | // ------------------
44 | // Nav collapse clears brand
45 | .nav-collapse {
46 | clear: both;
47 | }
48 | // Block-level the nav
49 | .nav-collapse .nav {
50 | float: none;
51 | margin: 0 0 (@baseLineHeight / 2);
52 | }
53 | .nav-collapse .nav > li {
54 | float: none;
55 | }
56 | .nav-collapse .nav > li > a {
57 | margin-bottom: 2px;
58 | }
59 | .nav-collapse .nav > .divider-vertical {
60 | display: none;
61 | }
62 | .nav-collapse .nav .nav-header {
63 | color: @navbarText;
64 | text-shadow: none;
65 | }
66 | // Nav and dropdown links in navbar
67 | .nav-collapse .nav > li > a,
68 | .nav-collapse .dropdown-menu a {
69 | padding: 9px 15px;
70 | font-weight: bold;
71 | color: @navbarLinkColor;
72 | .border-radius(3px);
73 | }
74 | // Buttons
75 | .nav-collapse .btn {
76 | padding: 4px 10px 4px;
77 | font-weight: normal;
78 | .border-radius(4px);
79 | }
80 | .nav-collapse .dropdown-menu li + li a {
81 | margin-bottom: 2px;
82 | }
83 | .nav-collapse .nav > li > a:hover,
84 | .nav-collapse .dropdown-menu a:hover {
85 | background-color: @navbarBackground;
86 | }
87 | .navbar-inverse .nav-collapse .nav > li > a:hover,
88 | .navbar-inverse .nav-collapse .dropdown-menu a:hover {
89 | background-color: @navbarInverseBackground;
90 | }
91 | // Buttons in the navbar
92 | .nav-collapse.in .btn-group {
93 | margin-top: 5px;
94 | padding: 0;
95 | }
96 | // Dropdowns in the navbar
97 | .nav-collapse .dropdown-menu {
98 | position: static;
99 | top: auto;
100 | left: auto;
101 | float: none;
102 | display: block;
103 | max-width: none;
104 | margin: 0 15px;
105 | padding: 0;
106 | background-color: transparent;
107 | border: none;
108 | .border-radius(0);
109 | .box-shadow(none);
110 | }
111 | .nav-collapse .dropdown-menu:before,
112 | .nav-collapse .dropdown-menu:after {
113 | display: none;
114 | }
115 | .nav-collapse .dropdown-menu .divider {
116 | display: none;
117 | }
118 | .nav-collapse .nav > li > .dropdown-menu {
119 | &:before,
120 | &:after {
121 | display: none;
122 | }
123 | }
124 | // Forms in navbar
125 | .nav-collapse .navbar-form,
126 | .nav-collapse .navbar-search {
127 | float: none;
128 | padding: (@baseLineHeight / 2) 15px;
129 | margin: (@baseLineHeight / 2) 0;
130 | border-top: 1px solid @navbarBackground;
131 | border-bottom: 1px solid @navbarBackground;
132 | .box-shadow(inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1));
133 | }
134 | .navbar-inverse .nav-collapse .navbar-form,
135 | .navbar-inverse .nav-collapse .navbar-search {
136 | border-top-color: @navbarInverseBackground;
137 | border-bottom-color: @navbarInverseBackground;
138 | }
139 | // Pull right (secondary) nav content
140 | .navbar .nav-collapse .nav.pull-right {
141 | float: none;
142 | margin-left: 0;
143 | }
144 | // Hide everything in the navbar save .brand and toggle button */
145 | .nav-collapse,
146 | .nav-collapse.collapse {
147 | overflow: hidden;
148 | height: 0;
149 | }
150 | // Navbar button
151 | .navbar .btn-navbar {
152 | display: block;
153 | }
154 |
155 | // STATIC NAVBAR
156 | // -------------
157 | .navbar-static .navbar-inner {
158 | padding-left: 10px;
159 | padding-right: 10px;
160 | }
161 |
162 |
163 | }
164 |
165 |
166 | // DEFAULT DESKTOP
167 | // ---------------
168 |
169 | @media (min-width: 980px) {
170 |
171 | // Required to make the collapsing navbar work on regular desktops
172 | .nav-collapse.collapse {
173 | height: auto !important;
174 | overflow: visible !important;
175 | }
176 |
177 | }
178 |
--------------------------------------------------------------------------------
/app/assets/css/lib/type.less:
--------------------------------------------------------------------------------
1 | //
2 | // Typography
3 | // --------------------------------------------------
4 |
5 |
6 | // Body text
7 | // -------------------------
8 |
9 | p {
10 | margin: 0 0 @baseLineHeight / 2;
11 | }
12 | .lead {
13 | margin-bottom: @baseLineHeight;
14 | font-size: @baseFontSize * 1.5;
15 | font-weight: 200;
16 | line-height: @baseLineHeight * 1.5;
17 | }
18 |
19 |
20 | // Emphasis & misc
21 | // -------------------------
22 |
23 | small {
24 | font-size: 85%; // Ex: 14px base font * 85% = about 12px
25 | }
26 | strong {
27 | font-weight: bold;
28 | }
29 | em {
30 | font-style: italic;
31 | }
32 | cite {
33 | font-style: normal;
34 | }
35 |
36 | // Utility classes
37 | .muted {
38 | color: @grayLight;
39 | }
40 | .text-warning {
41 | color: @warningText;
42 | }
43 | .text-error {
44 | color: @errorText;
45 | }
46 | .text-info {
47 | color: @infoText;
48 | }
49 | .text-success {
50 | color: @successText;
51 | }
52 |
53 |
54 | // Headings
55 | // -------------------------
56 |
57 | h1, h2, h3, h4, h5, h6 {
58 | margin: (@baseLineHeight / 2) 0;
59 | font-family: @headingsFontFamily;
60 | font-weight: @headingsFontWeight;
61 | line-height: 1;
62 | color: @headingsColor;
63 | text-rendering: optimizelegibility; // Fix the character spacing for headings
64 | small {
65 | font-weight: normal;
66 | line-height: 1;
67 | color: @grayLight;
68 | }
69 | }
70 | h1 { font-size: 36px; line-height: 40px; }
71 | h2 { font-size: 30px; line-height: 40px; }
72 | h3 { font-size: 24px; line-height: 40px; }
73 | h4 { font-size: 18px; line-height: 20px; }
74 | h5 { font-size: 14px; line-height: 20px; }
75 | h6 { font-size: 12px; line-height: 20px; }
76 |
77 | h1 small { font-size: 24px; }
78 | h2 small { font-size: 18px; }
79 | h3 small { font-size: 14px; }
80 | h4 small { font-size: 14px; }
81 |
82 |
83 | // Page header
84 | // -------------------------
85 |
86 | .page-header {
87 | padding-bottom: (@baseLineHeight / 2) - 1;
88 | margin: @baseLineHeight 0 (@baseLineHeight * 1.5);
89 | border-bottom: 1px solid @grayLighter;
90 | }
91 |
92 |
93 |
94 | // Lists
95 | // --------------------------------------------------
96 |
97 | // Unordered and Ordered lists
98 | ul, ol {
99 | padding: 0;
100 | margin: 0 0 @baseLineHeight / 2 25px;
101 | }
102 | ul ul,
103 | ul ol,
104 | ol ol,
105 | ol ul {
106 | margin-bottom: 0;
107 | }
108 | li {
109 | line-height: @baseLineHeight;
110 | }
111 | ul.unstyled,
112 | ol.unstyled {
113 | margin-left: 0;
114 | list-style: none;
115 | }
116 |
117 | // Description Lists
118 | dl {
119 | margin-bottom: @baseLineHeight;
120 | }
121 | dt,
122 | dd {
123 | line-height: @baseLineHeight;
124 | }
125 | dt {
126 | font-weight: bold;
127 | }
128 | dd {
129 | margin-left: @baseLineHeight / 2;
130 | }
131 | // Horizontal layout (like forms)
132 | .dl-horizontal {
133 | .clearfix(); // Ensure dl clears floats if empty dd elements present
134 | dt {
135 | float: left;
136 | width: @horizontalComponentOffset - 20;
137 | clear: left;
138 | text-align: right;
139 | .text-overflow();
140 | }
141 | dd {
142 | margin-left: @horizontalComponentOffset;
143 | }
144 | }
145 |
146 | // MISC
147 | // ----
148 |
149 | // Horizontal rules
150 | hr {
151 | margin: @baseLineHeight 0;
152 | border: 0;
153 | border-top: 1px solid @hrBorder;
154 | border-bottom: 1px solid @white;
155 | }
156 |
157 | // Abbreviations and acronyms
158 | abbr[title] {
159 | cursor: help;
160 | border-bottom: 1px dotted @grayLight;
161 | }
162 | abbr.initialism {
163 | font-size: 90%;
164 | text-transform: uppercase;
165 | }
166 |
167 | // Blockquotes
168 | blockquote {
169 | padding: 0 0 0 15px;
170 | margin: 0 0 @baseLineHeight;
171 | border-left: 5px solid @grayLighter;
172 | p {
173 | margin-bottom: 0;
174 | #font > .shorthand(16px,300,@baseLineHeight * 1.25);
175 | }
176 | small {
177 | display: block;
178 | line-height: @baseLineHeight;
179 | color: @grayLight;
180 | &:before {
181 | content: '\2014 \00A0';
182 | }
183 | }
184 |
185 | // Float right with text-align: right
186 | &.pull-right {
187 | float: right;
188 | padding-right: 15px;
189 | padding-left: 0;
190 | border-right: 5px solid @grayLighter;
191 | border-left: 0;
192 | p,
193 | small {
194 | text-align: right;
195 | }
196 | small {
197 | &:before {
198 | content: '';
199 | }
200 | &:after {
201 | content: '\00A0 \2014';
202 | }
203 | }
204 | }
205 | }
206 |
207 | // Quotes
208 | q:before,
209 | q:after,
210 | blockquote:before,
211 | blockquote:after {
212 | content: "";
213 | }
214 |
215 | // Addresses
216 | address {
217 | display: block;
218 | margin-bottom: @baseLineHeight;
219 | font-style: normal;
220 | line-height: @baseLineHeight;
221 | }
222 |
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-scrollspy.js:
--------------------------------------------------------------------------------
1 | /* =============================================================
2 | * bootstrap-scrollspy.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#scrollspy
4 | * =============================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ============================================================== */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* SCROLLSPY CLASS DEFINITION
27 | * ========================== */
28 |
29 | function ScrollSpy(element, options) {
30 | var process = $.proxy(this.process, this)
31 | , $element = $(element).is('body') ? $(window) : $(element)
32 | , href
33 | this.options = $.extend({}, $.fn.scrollspy.defaults, options)
34 | this.$scrollElement = $element.on('scroll.scroll-spy.data-api', process)
35 | this.selector = (this.options.target
36 | || ((href = $(element).attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
37 | || '') + ' .nav li > a'
38 | this.$body = $('body')
39 | this.refresh()
40 | this.process()
41 | }
42 |
43 | ScrollSpy.prototype = {
44 |
45 | constructor: ScrollSpy
46 |
47 | , refresh: function () {
48 | var self = this
49 | , $targets
50 |
51 | this.offsets = $([])
52 | this.targets = $([])
53 |
54 | $targets = this.$body
55 | .find(this.selector)
56 | .map(function () {
57 | var $el = $(this)
58 | , href = $el.data('target') || $el.attr('href')
59 | , $href = /^#\w/.test(href) && $(href)
60 | return ( $href
61 | && $href.length
62 | && [[ $href.position().top, href ]] ) || null
63 | })
64 | .sort(function (a, b) { return a[0] - b[0] })
65 | .each(function () {
66 | self.offsets.push(this[0])
67 | self.targets.push(this[1])
68 | })
69 | }
70 |
71 | , process: function () {
72 | var scrollTop = this.$scrollElement.scrollTop() + this.options.offset
73 | , scrollHeight = this.$scrollElement[0].scrollHeight || this.$body[0].scrollHeight
74 | , maxScroll = scrollHeight - this.$scrollElement.height()
75 | , offsets = this.offsets
76 | , targets = this.targets
77 | , activeTarget = this.activeTarget
78 | , i
79 |
80 | if (scrollTop >= maxScroll) {
81 | return activeTarget != (i = targets.last()[0])
82 | && this.activate ( i )
83 | }
84 |
85 | for (i = offsets.length; i--;) {
86 | activeTarget != targets[i]
87 | && scrollTop >= offsets[i]
88 | && (!offsets[i + 1] || scrollTop <= offsets[i + 1])
89 | && this.activate( targets[i] )
90 | }
91 | }
92 |
93 | , activate: function (target) {
94 | var active
95 | , selector
96 |
97 | this.activeTarget = target
98 |
99 | $(this.selector)
100 | .parent('.active')
101 | .removeClass('active')
102 |
103 | selector = this.selector
104 | + '[data-target="' + target + '"],'
105 | + this.selector + '[href="' + target + '"]'
106 |
107 | active = $(selector)
108 | .parent('li')
109 | .addClass('active')
110 |
111 | if (active.parent('.dropdown-menu').length) {
112 | active = active.closest('li.dropdown').addClass('active')
113 | }
114 |
115 | active.trigger('activate')
116 | }
117 |
118 | }
119 |
120 |
121 | /* SCROLLSPY PLUGIN DEFINITION
122 | * =========================== */
123 |
124 | $.fn.scrollspy = function (option) {
125 | return this.each(function () {
126 | var $this = $(this)
127 | , data = $this.data('scrollspy')
128 | , options = typeof option == 'object' && option
129 | if (!data) $this.data('scrollspy', (data = new ScrollSpy(this, options)))
130 | if (typeof option == 'string') data[option]()
131 | })
132 | }
133 |
134 | $.fn.scrollspy.Constructor = ScrollSpy
135 |
136 | $.fn.scrollspy.defaults = {
137 | offset: 10
138 | }
139 |
140 |
141 | /* SCROLLSPY DATA-API
142 | * ================== */
143 |
144 | $(window).on('load', function () {
145 | $('[data-spy="scroll"]').each(function () {
146 | var $spy = $(this)
147 | $spy.scrollspy($spy.data())
148 | })
149 | })
150 |
151 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-collapse.js:
--------------------------------------------------------------------------------
1 | /* =============================================================
2 | * bootstrap-collapse.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#collapse
4 | * =============================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ============================================================ */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* COLLAPSE PUBLIC CLASS DEFINITION
27 | * ================================ */
28 |
29 | var Collapse = function (element, options) {
30 | this.$element = $(element)
31 | this.options = $.extend({}, $.fn.collapse.defaults, options)
32 |
33 | if (this.options.parent) {
34 | this.$parent = $(this.options.parent)
35 | }
36 |
37 | this.options.toggle && this.toggle()
38 | }
39 |
40 | Collapse.prototype = {
41 |
42 | constructor: Collapse
43 |
44 | , dimension: function () {
45 | var hasWidth = this.$element.hasClass('width')
46 | return hasWidth ? 'width' : 'height'
47 | }
48 |
49 | , show: function () {
50 | var dimension
51 | , scroll
52 | , actives
53 | , hasData
54 |
55 | if (this.transitioning) return
56 |
57 | dimension = this.dimension()
58 | scroll = $.camelCase(['scroll', dimension].join('-'))
59 | actives = this.$parent && this.$parent.find('> .accordion-group > .in')
60 |
61 | if (actives && actives.length) {
62 | hasData = actives.data('collapse')
63 | if (hasData && hasData.transitioning) return
64 | actives.collapse('hide')
65 | hasData || actives.data('collapse', null)
66 | }
67 |
68 | this.$element[dimension](0)
69 | this.transition('addClass', $.Event('show'), 'shown')
70 | $.support.transition && this.$element[dimension](this.$element[0][scroll])
71 | }
72 |
73 | , hide: function () {
74 | var dimension
75 | if (this.transitioning) return
76 | dimension = this.dimension()
77 | this.reset(this.$element[dimension]())
78 | this.transition('removeClass', $.Event('hide'), 'hidden')
79 | this.$element[dimension](0)
80 | }
81 |
82 | , reset: function (size) {
83 | var dimension = this.dimension()
84 |
85 | this.$element
86 | .removeClass('collapse')
87 | [dimension](size || 'auto')
88 | [0].offsetWidth
89 |
90 | this.$element[size !== null ? 'addClass' : 'removeClass']('collapse')
91 |
92 | return this
93 | }
94 |
95 | , transition: function (method, startEvent, completeEvent) {
96 | var that = this
97 | , complete = function () {
98 | if (startEvent.type == 'show') that.reset()
99 | that.transitioning = 0
100 | that.$element.trigger(completeEvent)
101 | }
102 |
103 | this.$element.trigger(startEvent)
104 |
105 | if (startEvent.isDefaultPrevented()) return
106 |
107 | this.transitioning = 1
108 |
109 | this.$element[method]('in')
110 |
111 | $.support.transition && this.$element.hasClass('collapse') ?
112 | this.$element.one($.support.transition.end, complete) :
113 | complete()
114 | }
115 |
116 | , toggle: function () {
117 | this[this.$element.hasClass('in') ? 'hide' : 'show']()
118 | }
119 |
120 | }
121 |
122 |
123 | /* COLLAPSIBLE PLUGIN DEFINITION
124 | * ============================== */
125 |
126 | $.fn.collapse = function (option) {
127 | return this.each(function () {
128 | var $this = $(this)
129 | , data = $this.data('collapse')
130 | , options = typeof option == 'object' && option
131 | if (!data) $this.data('collapse', (data = new Collapse(this, options)))
132 | if (typeof option == 'string') data[option]()
133 | })
134 | }
135 |
136 | $.fn.collapse.defaults = {
137 | toggle: true
138 | }
139 |
140 | $.fn.collapse.Constructor = Collapse
141 |
142 |
143 | /* COLLAPSIBLE DATA-API
144 | * ==================== */
145 |
146 | $(function () {
147 | $('body').on('click.collapse.data-api', '[data-toggle=collapse]', function (e) {
148 | var $this = $(this), href
149 | , target = $this.attr('data-target')
150 | || e.preventDefault()
151 | || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '') //strip for ie7
152 | , option = $(target).data('collapse') ? 'toggle' : $this.data()
153 | $this[$(target).hasClass('in') ? 'addClass' : 'removeClass']('collapsed')
154 | $(target).collapse(option)
155 | })
156 | })
157 |
158 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/assets/css/lib/dropdowns.less:
--------------------------------------------------------------------------------
1 | //
2 | // Dropdown menus
3 | // --------------------------------------------------
4 |
5 |
6 | // Use the .menu class on any element within the topbar or ul.tabs and you'll get some superfancy dropdowns
7 | .dropup,
8 | .dropdown {
9 | position: relative;
10 | }
11 | .dropdown-toggle {
12 | // The caret makes the toggle a bit too tall in IE7
13 | *margin-bottom: -3px;
14 | }
15 | .dropdown-toggle:active,
16 | .open .dropdown-toggle {
17 | outline: 0;
18 | }
19 |
20 | // Dropdown arrow/caret
21 | // --------------------
22 | .caret {
23 | display: inline-block;
24 | width: 0;
25 | height: 0;
26 | vertical-align: top;
27 | border-top: 4px solid @black;
28 | border-right: 4px solid transparent;
29 | border-left: 4px solid transparent;
30 | content: "";
31 | }
32 |
33 | // Place the caret
34 | .dropdown .caret {
35 | margin-top: 8px;
36 | margin-left: 2px;
37 | }
38 |
39 | // The dropdown menu (ul)
40 | // ----------------------
41 | .dropdown-menu {
42 | position: absolute;
43 | top: 100%;
44 | left: 0;
45 | z-index: @zindexDropdown;
46 | display: none; // none by default, but block on "open" of the menu
47 | float: left;
48 | min-width: 160px;
49 | padding: 5px 0;
50 | margin: 2px 0 0; // override default ul
51 | list-style: none;
52 | background-color: @dropdownBackground;
53 | border: 1px solid #ccc; // Fallback for IE7-8
54 | border: 1px solid @dropdownBorder;
55 | *border-right-width: 2px;
56 | *border-bottom-width: 2px;
57 | .border-radius(6px);
58 | .box-shadow(0 5px 10px rgba(0,0,0,.2));
59 | -webkit-background-clip: padding-box;
60 | -moz-background-clip: padding;
61 | background-clip: padding-box;
62 |
63 | // Aligns the dropdown menu to right
64 | &.pull-right {
65 | right: 0;
66 | left: auto;
67 | }
68 |
69 | // Dividers (basically an hr) within the dropdown
70 | .divider {
71 | .nav-divider(@dropdownDividerTop, @dropdownDividerBottom);
72 | }
73 |
74 | // Links within the dropdown menu
75 | a {
76 | display: block;
77 | padding: 3px 20px;
78 | clear: both;
79 | font-weight: normal;
80 | line-height: @baseLineHeight;
81 | color: @dropdownLinkColor;
82 | white-space: nowrap;
83 | }
84 | }
85 |
86 | // Hover state
87 | // -----------
88 | .dropdown-menu li > a:hover,
89 | .dropdown-menu li > a:focus,
90 | .dropdown-submenu:hover > a {
91 | text-decoration: none;
92 | color: @dropdownLinkColorHover;
93 | background-color: @dropdownLinkBackgroundHover;
94 | #gradient > .vertical(@dropdownLinkBackgroundHover, darken(@dropdownLinkBackgroundHover, 5%));
95 | }
96 |
97 | // Active state
98 | // ------------
99 | .dropdown-menu .active > a,
100 | .dropdown-menu .active > a:hover {
101 | color: @dropdownLinkColorHover;
102 | text-decoration: none;
103 | outline: 0;
104 | background-color: @dropdownLinkBackgroundActive;
105 | #gradient > .vertical(@dropdownLinkBackgroundActive, darken(@dropdownLinkBackgroundActive, 5%));
106 | }
107 |
108 | // Disabled state
109 | // --------------
110 | // Gray out text and ensure the hover state remains gray
111 | .dropdown-menu .disabled > a,
112 | .dropdown-menu .disabled > a:hover {
113 | color: @grayLight;
114 | }
115 | // Nuke hover effects
116 | .dropdown-menu .disabled > a:hover {
117 | text-decoration: none;
118 | background-color: transparent;
119 | cursor: default;
120 | }
121 |
122 | // Open state for the dropdown
123 | // ---------------------------
124 | .open {
125 | // IE7's z-index only goes to the nearest positioned ancestor, which would
126 | // make the menu appear below buttons that appeared later on the page
127 | *z-index: @zindexDropdown;
128 |
129 | & > .dropdown-menu {
130 | display: block;
131 | }
132 | }
133 |
134 | // Right aligned dropdowns
135 | // ---------------------------
136 | .pull-right > .dropdown-menu {
137 | right: 0;
138 | left: auto;
139 | }
140 |
141 | // Allow for dropdowns to go bottom up (aka, dropup-menu)
142 | // ------------------------------------------------------
143 | // Just add .dropup after the standard .dropdown class and you're set, bro.
144 | // TODO: abstract this so that the navbar fixed styles are not placed here?
145 | .dropup,
146 | .navbar-fixed-bottom .dropdown {
147 | // Reverse the caret
148 | .caret {
149 | border-top: 0;
150 | border-bottom: 4px solid @black;
151 | content: "";
152 | }
153 | // Different positioning for bottom up menu
154 | .dropdown-menu {
155 | top: auto;
156 | bottom: 100%;
157 | margin-bottom: 1px;
158 | }
159 | }
160 |
161 | // Sub menus
162 | // ---------------------------
163 | .dropdown-submenu {
164 | position: relative;
165 | }
166 | .dropdown-submenu > .dropdown-menu {
167 | top: 0;
168 | left: 100%;
169 | margin-top: -6px;
170 | margin-left: -1px;
171 | -webkit-border-radius: 0 6px 6px 6px;
172 | -moz-border-radius: 0 6px 6px 6px;
173 | border-radius: 0 6px 6px 6px;
174 | }
175 | .dropdown-submenu:hover > .dropdown-menu {
176 | display: block;
177 | }
178 |
179 | .dropdown-submenu > a:after {
180 | display: block;
181 | content: " ";
182 | float: right;
183 | width: 0;
184 | height: 0;
185 | border-color: transparent;
186 | border-style: solid;
187 | border-width: 5px 0 5px 5px;
188 | border-left-color: darken(@dropdownBackground, 20%);
189 | margin-top: 5px;
190 | margin-right: -10px;
191 | }
192 | .dropdown-submenu:hover > a:after {
193 | border-left-color: @dropdownLinkColorHover;
194 | }
195 |
196 |
197 | // Tweak nav headers
198 | // -----------------
199 | // Increase padding from 15px to 20px on sides
200 | .dropdown .dropdown-menu .nav-header {
201 | padding-left: 20px;
202 | padding-right: 20px;
203 | }
204 |
205 | // Typeahead
206 | // ---------
207 | .typeahead {
208 | margin-top: 2px; // give it some space to breathe
209 | .border-radius(4px);
210 | }
211 |
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-carousel.js:
--------------------------------------------------------------------------------
1 | /* ==========================================================
2 | * bootstrap-carousel.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#carousel
4 | * ==========================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ========================================================== */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* CAROUSEL CLASS DEFINITION
27 | * ========================= */
28 |
29 | var Carousel = function (element, options) {
30 | this.$element = $(element)
31 | this.options = options
32 | this.options.slide && this.slide(this.options.slide)
33 | this.options.pause == 'hover' && this.$element
34 | .on('mouseenter', $.proxy(this.pause, this))
35 | .on('mouseleave', $.proxy(this.cycle, this))
36 | }
37 |
38 | Carousel.prototype = {
39 |
40 | cycle: function (e) {
41 | if (!e) this.paused = false
42 | this.options.interval
43 | && !this.paused
44 | && (this.interval = setInterval($.proxy(this.next, this), this.options.interval))
45 | return this
46 | }
47 |
48 | , to: function (pos) {
49 | var $active = this.$element.find('.item.active')
50 | , children = $active.parent().children()
51 | , activePos = children.index($active)
52 | , that = this
53 |
54 | if (pos > (children.length - 1) || pos < 0) return
55 |
56 | if (this.sliding) {
57 | return this.$element.one('slid', function () {
58 | that.to(pos)
59 | })
60 | }
61 |
62 | if (activePos == pos) {
63 | return this.pause().cycle()
64 | }
65 |
66 | return this.slide(pos > activePos ? 'next' : 'prev', $(children[pos]))
67 | }
68 |
69 | , pause: function (e) {
70 | if (!e) this.paused = true
71 | if (this.$element.find('.next, .prev').length && $.support.transition.end) {
72 | this.$element.trigger($.support.transition.end)
73 | this.cycle()
74 | }
75 | clearInterval(this.interval)
76 | this.interval = null
77 | return this
78 | }
79 |
80 | , next: function () {
81 | if (this.sliding) return
82 | return this.slide('next')
83 | }
84 |
85 | , prev: function () {
86 | if (this.sliding) return
87 | return this.slide('prev')
88 | }
89 |
90 | , slide: function (type, next) {
91 | var $active = this.$element.find('.item.active')
92 | , $next = next || $active[type]()
93 | , isCycling = this.interval
94 | , direction = type == 'next' ? 'left' : 'right'
95 | , fallback = type == 'next' ? 'first' : 'last'
96 | , that = this
97 | , e = $.Event('slide', {
98 | relatedTarget: $next[0]
99 | })
100 |
101 | this.sliding = true
102 |
103 | isCycling && this.pause()
104 |
105 | $next = $next.length ? $next : this.$element.find('.item')[fallback]()
106 |
107 | if ($next.hasClass('active')) return
108 |
109 | if ($.support.transition && this.$element.hasClass('slide')) {
110 | this.$element.trigger(e)
111 | if (e.isDefaultPrevented()) return
112 | $next.addClass(type)
113 | $next[0].offsetWidth // force reflow
114 | $active.addClass(direction)
115 | $next.addClass(direction)
116 | this.$element.one($.support.transition.end, function () {
117 | $next.removeClass([type, direction].join(' ')).addClass('active')
118 | $active.removeClass(['active', direction].join(' '))
119 | that.sliding = false
120 | setTimeout(function () { that.$element.trigger('slid') }, 0)
121 | })
122 | } else {
123 | this.$element.trigger(e)
124 | if (e.isDefaultPrevented()) return
125 | $active.removeClass('active')
126 | $next.addClass('active')
127 | this.sliding = false
128 | this.$element.trigger('slid')
129 | }
130 |
131 | isCycling && this.cycle()
132 |
133 | return this
134 | }
135 |
136 | }
137 |
138 |
139 | /* CAROUSEL PLUGIN DEFINITION
140 | * ========================== */
141 |
142 | $.fn.carousel = function (option) {
143 | return this.each(function () {
144 | var $this = $(this)
145 | , data = $this.data('carousel')
146 | , options = $.extend({}, $.fn.carousel.defaults, typeof option == 'object' && option)
147 | , action = typeof option == 'string' ? option : options.slide
148 | if (!data) $this.data('carousel', (data = new Carousel(this, options)))
149 | if (typeof option == 'number') data.to(option)
150 | else if (action) data[action]()
151 | else if (options.interval) data.cycle()
152 | })
153 | }
154 |
155 | $.fn.carousel.defaults = {
156 | interval: 5000
157 | , pause: 'hover'
158 | }
159 |
160 | $.fn.carousel.Constructor = Carousel
161 |
162 |
163 | /* CAROUSEL DATA-API
164 | * ================= */
165 |
166 | $(function () {
167 | $('body').on('click.carousel.data-api', '[data-slide]', function ( e ) {
168 | var $this = $(this), href
169 | , $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) //strip for ie7
170 | , options = !$target.data('modal') && $.extend({}, $target.data(), $this.data())
171 | $target.carousel(options)
172 | e.preventDefault()
173 | })
174 | })
175 |
176 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/assets/css/lib/buttons.less:
--------------------------------------------------------------------------------
1 | //
2 | // Buttons
3 | // --------------------------------------------------
4 |
5 |
6 | // Base styles
7 | // --------------------------------------------------
8 |
9 | // Core
10 | .btn {
11 | display: inline-block;
12 | .ie7-inline-block();
13 | padding: 4px 14px;
14 | margin-bottom: 0; // For input.btn
15 | font-size: @baseFontSize;
16 | line-height: @baseLineHeight;
17 | *line-height: @baseLineHeight;
18 | text-align: center;
19 | vertical-align: middle;
20 | cursor: pointer;
21 | .buttonBackground(@btnBackground, @btnBackgroundHighlight, @grayDark, 0 1px 1px rgba(255,255,255,.75));
22 | border: 1px solid @btnBorder;
23 | *border: 0; // Remove the border to prevent IE7's black border on input:focus
24 | border-bottom-color: darken(@btnBorder, 10%);
25 | .border-radius(4px);
26 | .ie7-restore-left-whitespace(); // Give IE7 some love
27 | .box-shadow(inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05));
28 |
29 | // Hover state
30 | &:hover {
31 | color: @grayDark;
32 | text-decoration: none;
33 | background-color: darken(@white, 10%);
34 | *background-color: darken(@white, 15%); /* Buttons in IE7 don't get borders, so darken on hover */
35 | background-position: 0 -15px;
36 |
37 | // transition is only when going to hover, otherwise the background
38 | // behind the gradient (there for IE<=9 fallback) gets mismatched
39 | .transition(background-position .1s linear);
40 | }
41 |
42 | // Focus state for keyboard and accessibility
43 | &:focus {
44 | .tab-focus();
45 | }
46 |
47 | // Active state
48 | &.active,
49 | &:active {
50 | background-color: darken(@white, 10%);
51 | background-color: darken(@white, 15%) e("\9");
52 | background-image: none;
53 | outline: 0;
54 | .box-shadow(inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05));
55 | }
56 |
57 | // Disabled state
58 | &.disabled,
59 | &[disabled] {
60 | cursor: default;
61 | background-color: darken(@white, 10%);
62 | background-image: none;
63 | .opacity(65);
64 | .box-shadow(none);
65 | }
66 |
67 | }
68 |
69 |
70 |
71 | // Button Sizes
72 | // --------------------------------------------------
73 |
74 | // Large
75 | .btn-large {
76 | padding: 9px 14px;
77 | font-size: @baseFontSize + 2px;
78 | line-height: normal;
79 | .border-radius(5px);
80 | }
81 | .btn-large [class^="icon-"] {
82 | margin-top: 2px;
83 | }
84 |
85 | // Small
86 | .btn-small {
87 | padding: 3px 9px;
88 | font-size: @baseFontSize - 2px;
89 | line-height: @baseLineHeight - 2px;
90 | }
91 | .btn-small [class^="icon-"] {
92 | margin-top: 0;
93 | }
94 |
95 | // Mini
96 | .btn-mini {
97 | padding: 2px 6px;
98 | font-size: @baseFontSize - 3px;
99 | line-height: @baseLineHeight - 3px;
100 | }
101 |
102 |
103 | // Block button
104 | // -------------------------
105 |
106 | .btn-block {
107 | display: block;
108 | width: 100%;
109 | padding-left: 0;
110 | padding-right: 0;
111 | .box-sizing(border-box);
112 | }
113 |
114 | // Vertically space out multiple block buttons
115 | .btn-block + .btn-block {
116 | margin-top: 5px;
117 | }
118 |
119 | // Specificity overrides
120 | input[type="submit"],
121 | input[type="reset"],
122 | input[type="button"] {
123 | &.btn-block {
124 | width: 100%;
125 | }
126 | }
127 |
128 |
129 |
130 | // Alternate buttons
131 | // --------------------------------------------------
132 |
133 | // Provide *some* extra contrast for those who can get it
134 | .btn-primary.active,
135 | .btn-warning.active,
136 | .btn-danger.active,
137 | .btn-success.active,
138 | .btn-info.active,
139 | .btn-inverse.active {
140 | color: rgba(255,255,255,.75);
141 | }
142 |
143 | // Set the backgrounds
144 | // -------------------------
145 | .btn {
146 | // reset here as of 2.0.3 due to Recess property order
147 | border-color: #c5c5c5;
148 | border-color: rgba(0,0,0,.15) rgba(0,0,0,.15) rgba(0,0,0,.25);
149 | }
150 | .btn-primary {
151 | .buttonBackground(@btnPrimaryBackground, @btnPrimaryBackgroundHighlight);
152 | }
153 | // Warning appears are orange
154 | .btn-warning {
155 | .buttonBackground(@btnWarningBackground, @btnWarningBackgroundHighlight);
156 | }
157 | // Danger and error appear as red
158 | .btn-danger {
159 | .buttonBackground(@btnDangerBackground, @btnDangerBackgroundHighlight);
160 | }
161 | // Success appears as green
162 | .btn-success {
163 | .buttonBackground(@btnSuccessBackground, @btnSuccessBackgroundHighlight);
164 | }
165 | // Info appears as a neutral blue
166 | .btn-info {
167 | .buttonBackground(@btnInfoBackground, @btnInfoBackgroundHighlight);
168 | }
169 | // Inverse appears as dark gray
170 | .btn-inverse {
171 | .buttonBackground(@btnInverseBackground, @btnInverseBackgroundHighlight);
172 | }
173 |
174 |
175 | // Cross-browser Jank
176 | // --------------------------------------------------
177 |
178 | button.btn,
179 | input[type="submit"].btn {
180 |
181 | // Firefox 3.6 only I believe
182 | &::-moz-focus-inner {
183 | padding: 0;
184 | border: 0;
185 | }
186 |
187 | // IE7 has some default padding on button controls
188 | *padding-top: 3px;
189 | *padding-bottom: 3px;
190 |
191 | &.btn-large {
192 | *padding-top: 7px;
193 | *padding-bottom: 7px;
194 | }
195 | &.btn-small {
196 | *padding-top: 3px;
197 | *padding-bottom: 3px;
198 | }
199 | &.btn-mini {
200 | *padding-top: 1px;
201 | *padding-bottom: 1px;
202 | }
203 | }
204 |
205 |
206 | // Link buttons
207 | // --------------------------------------------------
208 |
209 | // Make a button look and behave like a link
210 | .btn-link,
211 | .btn-link:active,
212 | .btn-link[disabled] {
213 | background-color: transparent;
214 | background-image: none;
215 | .box-shadow(none);
216 | }
217 | .btn-link {
218 | border-color: transparent;
219 | cursor: pointer;
220 | color: @linkColor;
221 | .border-radius(0);
222 | }
223 | .btn-link:hover {
224 | color: @linkColorHover;
225 | text-decoration: underline;
226 | background-color: transparent;
227 | }
228 | .btn-link[disabled]:hover {
229 | color: @grayDark;
230 | text-decoration: none;
231 | }
232 |
--------------------------------------------------------------------------------
/diaonan.js:
--------------------------------------------------------------------------------
1 | var EventEmitter, RedisStore, app, argv, ascoltatori, configure, express, fs, hbs, http, load, mqtt, optimist,
2 | optionParser, path, redis, setup, setupAscoltatore, start, coap;
3 |
4 | optimist = require('optimist');
5 |
6 | express = require('express');
7 |
8 | path = require('path');
9 |
10 | fs = require('fs');
11 |
12 | coap = require('coap');
13 |
14 | hbs = require('hbs');
15 |
16 | redis = require('redis');
17 |
18 | mqtt = require("mqtt");
19 |
20 | EventEmitter = require('events').EventEmitter;
21 |
22 | RedisStore = require('connect-redis')(express);
23 |
24 | ascoltatori = require('ascoltatori');
25 |
26 | module.exports.app = app = express();
27 |
28 | http = require('http').createServer(app);
29 |
30 | app.redis = {};
31 |
32 | module.exports.configure = configure = function () {
33 | var io;
34 |
35 | app.configure('development', function () {
36 | return app.use(express.errorHandler({
37 | dumpExceptions: true,
38 | showStack: true
39 | }));
40 | });
41 |
42 | app.configure('production', function () {
43 | return app.use(express.errorHandler());
44 | });
45 |
46 | app.configure(function () {
47 | app.set('views', __dirname + '/app/views');
48 | app.set('view engine', 'hbs');
49 | app.use(express.bodyParser());
50 | app.use(express.methodOverride());
51 | app.use(express.cookieParser());
52 | app.use(express.session({
53 | secret: "wyRLuS5A79wLn3ItlGVF61Gt"
54 | }, {
55 | store: new RedisStore({
56 | client: app.redis.client
57 | }),
58 | maxAge: 1000 * 60 * 60 * 24 * 14
59 | }));
60 | app.use(app.router);
61 | return app.use(express["static"](__dirname + '/public'));
62 | });
63 |
64 | io = app.io = require('socket.io').listen(http);
65 | io.configure('production', function () {
66 | io.enable('browser client minification');
67 | io.enable('browser client etag');
68 | io.enable('browser client gzip');
69 | return io.set('log level', 0);
70 | });
71 | io.configure('test', function () {
72 | return io.set('log level', 0);
73 | });
74 |
75 | load("models");
76 | load("controllers");
77 | return load("helpers");
78 | };
79 |
80 | load = function (key) {
81 | var component, loadPath, loadedModule, _i, _len, _ref, _results;
82 | app[key] = {};
83 | loadPath = __dirname + ("/app/" + key + "/");
84 | _ref = fs.readdirSync(loadPath);
85 | _results = [];
86 | for (_i = 0, _len = _ref.length; _i < _len; _i++) {
87 | component = _ref[_i];
88 | if (component.match(/(js|coffee)$/)) {
89 | component = path.basename(component, path.extname(component));
90 | loadedModule = require(loadPath + component)(app);
91 | if (((loadedModule != null ? loadedModule.name : void 0) != null) && loadedModule.name !== "") {
92 | component = loadedModule.name;
93 | }
94 | _results.push(app[key][component] = loadedModule);
95 | } else {
96 | _results.push(void 0);
97 | }
98 | }
99 | return _results;
100 | };
101 |
102 | optionParser = optimist
103 | ["default"]('port', 3000)
104 | ["default"]('mqtt', 1883)
105 | ["default"]('coap', 5683)
106 | ["default"]('redis-port', 6379)
107 | ["default"]('redis-host', '127.0.0.1')
108 | ["default"]('redis-db', 0)
109 | .usage("Usage: $0 [-p WEB-PORT] [-m MQTT-PORT][-c COAP-PORT] [-rp REDIS-PORT] [-rh REDIS-HOST]")
110 | .alias('port', 'p')
111 | .alias('mqtt', 'm')
112 | .alias('coap', 'c')
113 | .alias('redis-port', 'rp')
114 | .alias('redis-host', 'rh')
115 | .alias('redis-db', 'rd')
116 | .describe('port', 'The port the web server will listen to')
117 | .describe('mqtt', 'The port the mqtt server will listen to')
118 | .describe('coap', 'The port the coap server will listen to')
119 | .describe('redis-port', 'The port of the redis server')
120 | .describe('redis-host', 'The host of the redis server')
121 | .boolean("help")
122 | .describe("help", "This help");
123 |
124 | argv = optionParser.argv;
125 |
126 | module.exports.setupAscoltatore = setupAscoltatore = function (opts) {
127 | if (opts == null) {
128 | opts = {};
129 | }
130 | return app.ascoltatore = new ascoltatori.RedisAscoltatore({
131 | redis: redis,
132 | port: opts.port,
133 | host: opts.host,
134 | db: opts.db
135 | });
136 | };
137 |
138 | module.exports.setup = setup = function (opts) {
139 | var args;
140 | if (opts == null) {
141 | opts = {};
142 | }
143 | args = [opts.port, opts.host];
144 | app.redis.client = redis.createClient.apply(redis, args);
145 | app.redis.client.select(opts.db || 0);
146 | return setupAscoltatore(opts);
147 | };
148 |
149 | start = module.exports.start = function (opts, callback) {
150 | var countDone, done;
151 | if (opts === null || opts === undefined) {
152 | opts = {};
153 | }
154 | if (callback === null || callback === undefined) {
155 | callback = function () {
156 | };
157 | }
158 |
159 | opts.port || (opts.port = argv.port);
160 | opts.mqtt || (opts.mqtt = argv.mqtt);
161 | opts.coap || (opts.coap = argv.coap);
162 | opts.redisPort || (opts.redisPort = argv['redis-port']);
163 | opts.redisHost || (opts.redisHost = argv['redis-host']);
164 | opts.redisDB || (opts.redisDB = argv['redis-db']);
165 | if (argv.help) {
166 | optionParser.showHelp();
167 | return 1;
168 | }
169 |
170 | setup({
171 | port: opts.redisPort,
172 | host: opts.redisHost,
173 | db: opts.redisDB
174 | });
175 |
176 | configure();
177 | countDone = 0;
178 | done = function () {
179 | if (countDone++ === 2) {
180 | return callback();
181 | }
182 | };
183 |
184 | http.listen(opts.port, function () {
185 | console.log("oap-mqtt-rest web server listening on port %d in %s mode", opts.port, app.settings.env);
186 | return done();
187 | });
188 |
189 | coap.createServer(app.controllers.coap_api).listen(opts.coap, function () {
190 | console.log("coap-mqtt-rest coap server listening on port %d in %s mode", opts.coap, app.settings.env);
191 | return done();
192 | });
193 |
194 | mqtt.createServer(app.controllers.mqtt_api).listen(opts.mqtt, function () {
195 | console.log("oap-mqtt-rest mqtt server listening on port %d in %s mode", opts.mqtt, app.settings.env);
196 | return done();
197 | });
198 | return app;
199 | };
200 |
201 | if (require.main.filename === __filename) {
202 | start();
203 | }
204 |
--------------------------------------------------------------------------------
/app/assets/css/lib/button-groups.less:
--------------------------------------------------------------------------------
1 | //
2 | // Button groups
3 | // --------------------------------------------------
4 |
5 |
6 | // Make the div behave like a button
7 | .btn-group {
8 | position: relative;
9 | font-size: 0; // remove as part 1 of font-size inline-block hack
10 | vertical-align: middle; // match .btn alignment given font-size hack above
11 | white-space: nowrap; // prevent buttons from wrapping when in tight spaces (e.g., the table on the tests page)
12 | .ie7-restore-left-whitespace();
13 | }
14 |
15 | // Space out series of button groups
16 | .btn-group + .btn-group {
17 | margin-left: 5px;
18 | }
19 |
20 | // Optional: Group multiple button groups together for a toolbar
21 | .btn-toolbar {
22 | font-size: 0; // Hack to remove whitespace that results from using inline-block
23 | margin-top: @baseLineHeight / 2;
24 | margin-bottom: @baseLineHeight / 2;
25 | .btn-group {
26 | display: inline-block;
27 | .ie7-inline-block();
28 | }
29 | .btn + .btn,
30 | .btn-group + .btn,
31 | .btn + .btn-group {
32 | margin-left: 5px;
33 | }
34 | }
35 |
36 | // Float them, remove border radius, then re-add to first and last elements
37 | .btn-group > .btn {
38 | position: relative;
39 | .border-radius(0);
40 | }
41 | .btn-group > .btn + .btn {
42 | margin-left: -1px;
43 | }
44 | .btn-group > .btn,
45 | .btn-group > .dropdown-menu {
46 | font-size: @baseFontSize; // redeclare as part 2 of font-size inline-block hack
47 | }
48 |
49 | // Reset fonts for other sizes
50 | .btn-group > .btn-mini {
51 | font-size: 11px;
52 | }
53 | .btn-group > .btn-small {
54 | font-size: 12px;
55 | }
56 | .btn-group > .btn-large {
57 | font-size: 16px;
58 | }
59 |
60 | // Set corners individual because sometimes a single button can be in a .btn-group and we need :first-child and :last-child to both match
61 | .btn-group > .btn:first-child {
62 | margin-left: 0;
63 | -webkit-border-top-left-radius: 4px;
64 | -moz-border-radius-topleft: 4px;
65 | border-top-left-radius: 4px;
66 | -webkit-border-bottom-left-radius: 4px;
67 | -moz-border-radius-bottomleft: 4px;
68 | border-bottom-left-radius: 4px;
69 | }
70 | // Need .dropdown-toggle since :last-child doesn't apply given a .dropdown-menu immediately after it
71 | .btn-group > .btn:last-child,
72 | .btn-group > .dropdown-toggle {
73 | -webkit-border-top-right-radius: 4px;
74 | -moz-border-radius-topright: 4px;
75 | border-top-right-radius: 4px;
76 | -webkit-border-bottom-right-radius: 4px;
77 | -moz-border-radius-bottomright: 4px;
78 | border-bottom-right-radius: 4px;
79 | }
80 | // Reset corners for large buttons
81 | .btn-group > .btn.large:first-child {
82 | margin-left: 0;
83 | -webkit-border-top-left-radius: 6px;
84 | -moz-border-radius-topleft: 6px;
85 | border-top-left-radius: 6px;
86 | -webkit-border-bottom-left-radius: 6px;
87 | -moz-border-radius-bottomleft: 6px;
88 | border-bottom-left-radius: 6px;
89 | }
90 | .btn-group > .btn.large:last-child,
91 | .btn-group > .large.dropdown-toggle {
92 | -webkit-border-top-right-radius: 6px;
93 | -moz-border-radius-topright: 6px;
94 | border-top-right-radius: 6px;
95 | -webkit-border-bottom-right-radius: 6px;
96 | -moz-border-radius-bottomright: 6px;
97 | border-bottom-right-radius: 6px;
98 | }
99 |
100 | // On hover/focus/active, bring the proper btn to front
101 | .btn-group > .btn:hover,
102 | .btn-group > .btn:focus,
103 | .btn-group > .btn:active,
104 | .btn-group > .btn.active {
105 | z-index: 2;
106 | }
107 |
108 | // On active and open, don't show outline
109 | .btn-group .dropdown-toggle:active,
110 | .btn-group.open .dropdown-toggle {
111 | outline: 0;
112 | }
113 |
114 |
115 |
116 | // Split button dropdowns
117 | // ----------------------
118 |
119 | // Give the line between buttons some depth
120 | .btn-group > .btn + .dropdown-toggle {
121 | padding-left: 8px;
122 | padding-right: 8px;
123 | .box-shadow(inset 1px 0 0 rgba(255,255,255,.125), inset 0 1px 0 rgba(255,255,255,.2), 0 1px 2px rgba(0,0,0,.05));
124 | *padding-top: 5px;
125 | *padding-bottom: 5px;
126 | }
127 | .btn-group > .btn-mini + .dropdown-toggle {
128 | padding-left: 5px;
129 | padding-right: 5px;
130 | *padding-top: 2px;
131 | *padding-bottom: 2px;
132 | }
133 | .btn-group > .btn-small + .dropdown-toggle {
134 | *padding-top: 5px;
135 | *padding-bottom: 4px;
136 | }
137 | .btn-group > .btn-large + .dropdown-toggle {
138 | padding-left: 12px;
139 | padding-right: 12px;
140 | *padding-top: 7px;
141 | *padding-bottom: 7px;
142 | }
143 |
144 | .btn-group.open {
145 |
146 | // The clickable button for toggling the menu
147 | // Remove the gradient and set the same inset shadow as the :active state
148 | .dropdown-toggle {
149 | background-image: none;
150 | .box-shadow(inset 0 2px 4px rgba(0,0,0,.15), 0 1px 2px rgba(0,0,0,.05));
151 | }
152 |
153 | // Keep the hover's background when dropdown is open
154 | .btn.dropdown-toggle {
155 | background-color: @btnBackgroundHighlight;
156 | }
157 | .btn-primary.dropdown-toggle {
158 | background-color: @btnPrimaryBackgroundHighlight;
159 | }
160 | .btn-warning.dropdown-toggle {
161 | background-color: @btnWarningBackgroundHighlight;
162 | }
163 | .btn-danger.dropdown-toggle {
164 | background-color: @btnDangerBackgroundHighlight;
165 | }
166 | .btn-success.dropdown-toggle {
167 | background-color: @btnSuccessBackgroundHighlight;
168 | }
169 | .btn-info.dropdown-toggle {
170 | background-color: @btnInfoBackgroundHighlight;
171 | }
172 | .btn-inverse.dropdown-toggle {
173 | background-color: @btnInverseBackgroundHighlight;
174 | }
175 | }
176 |
177 |
178 | // Reposition the caret
179 | .btn .caret {
180 | margin-top: 8px;
181 | margin-left: 0;
182 | }
183 | // Carets in other button sizes
184 | .btn-mini .caret,
185 | .btn-small .caret,
186 | .btn-large .caret {
187 | margin-top: 6px;
188 | }
189 | .btn-large .caret {
190 | border-left-width: 5px;
191 | border-right-width: 5px;
192 | border-top-width: 5px;
193 | }
194 | // Upside down carets for .dropup
195 | .dropup .btn-large .caret {
196 | border-bottom: 5px solid @black;
197 | border-top: 0;
198 | }
199 |
200 |
201 |
202 | // Account for other colors
203 | .btn-primary,
204 | .btn-warning,
205 | .btn-danger,
206 | .btn-info,
207 | .btn-success,
208 | .btn-inverse {
209 | .caret {
210 | border-top-color: @white;
211 | border-bottom-color: @white;
212 | }
213 | }
214 |
215 |
216 |
217 | // Vertical button groups
218 | // ----------------------
219 |
220 | .btn-group-vertical {
221 | display: inline-block; // makes buttons only take up the width they need
222 | .ie7-inline-block();
223 | }
224 | .btn-group-vertical .btn {
225 | display: block;
226 | float: none;
227 | width: 100%;
228 | .border-radius(0);
229 | }
230 | .btn-group-vertical .btn + .btn {
231 | margin-left: 0;
232 | margin-top: -1px;
233 | }
234 | .btn-group-vertical .btn:first-child {
235 | .border-radius(4px 4px 0 0);
236 | }
237 | .btn-group-vertical .btn:last-child {
238 | .border-radius(0 0 4px 4px);
239 | }
240 | .btn-group-vertical .btn-large:first-child {
241 | .border-radius(6px 6px 0 0);
242 | }
243 | .btn-group-vertical .btn-large:last-child {
244 | .border-radius(0 0 6px 6px);
245 | }
246 |
--------------------------------------------------------------------------------
/app/assets/css/lib/tables.less:
--------------------------------------------------------------------------------
1 | //
2 | // Tables
3 | // --------------------------------------------------
4 |
5 |
6 | // BASE TABLES
7 | // -----------------
8 |
9 | table {
10 | max-width: 100%;
11 | background-color: @tableBackground;
12 | border-collapse: collapse;
13 | border-spacing: 0;
14 | }
15 |
16 | // BASELINE STYLES
17 | // ---------------
18 |
19 | .table {
20 | width: 100%;
21 | margin-bottom: @baseLineHeight;
22 | // Cells
23 | th,
24 | td {
25 | padding: 8px;
26 | line-height: @baseLineHeight;
27 | text-align: left;
28 | vertical-align: top;
29 | border-top: 1px solid @tableBorder;
30 | }
31 | th {
32 | font-weight: bold;
33 | }
34 | // Bottom align for column headings
35 | thead th {
36 | vertical-align: bottom;
37 | }
38 | // Remove top border from thead by default
39 | caption + thead tr:first-child th,
40 | caption + thead tr:first-child td,
41 | colgroup + thead tr:first-child th,
42 | colgroup + thead tr:first-child td,
43 | thead:first-child tr:first-child th,
44 | thead:first-child tr:first-child td {
45 | border-top: 0;
46 | }
47 | // Account for multiple tbody instances
48 | tbody + tbody {
49 | border-top: 2px solid @tableBorder;
50 | }
51 | }
52 |
53 |
54 |
55 | // CONDENSED TABLE W/ HALF PADDING
56 | // -------------------------------
57 |
58 | .table-condensed {
59 | th,
60 | td {
61 | padding: 4px 5px;
62 | }
63 | }
64 |
65 |
66 | // BORDERED VERSION
67 | // ----------------
68 |
69 | .table-bordered {
70 | border: 1px solid @tableBorder;
71 | border-collapse: separate; // Done so we can round those corners!
72 | *border-collapse: collapse; // IE7 can't round corners anyway
73 | border-left: 0;
74 | .border-radius(4px);
75 | th,
76 | td {
77 | border-left: 1px solid @tableBorder;
78 | }
79 | // Prevent a double border
80 | caption + thead tr:first-child th,
81 | caption + tbody tr:first-child th,
82 | caption + tbody tr:first-child td,
83 | colgroup + thead tr:first-child th,
84 | colgroup + tbody tr:first-child th,
85 | colgroup + tbody tr:first-child td,
86 | thead:first-child tr:first-child th,
87 | tbody:first-child tr:first-child th,
88 | tbody:first-child tr:first-child td {
89 | border-top: 0;
90 | }
91 | // For first th or td in the first row in the first thead or tbody
92 | thead:first-child tr:first-child th:first-child,
93 | tbody:first-child tr:first-child td:first-child {
94 | -webkit-border-top-left-radius: 4px;
95 | border-top-left-radius: 4px;
96 | -moz-border-radius-topleft: 4px;
97 | }
98 | thead:first-child tr:first-child th:last-child,
99 | tbody:first-child tr:first-child td:last-child {
100 | -webkit-border-top-right-radius: 4px;
101 | border-top-right-radius: 4px;
102 | -moz-border-radius-topright: 4px;
103 | }
104 | // For first th or td in the first row in the first thead or tbody
105 | thead:last-child tr:last-child th:first-child,
106 | tbody:last-child tr:last-child td:first-child,
107 | tfoot:last-child tr:last-child td:first-child {
108 | .border-radius(0 0 0 4px);
109 | -webkit-border-bottom-left-radius: 4px;
110 | border-bottom-left-radius: 4px;
111 | -moz-border-radius-bottomleft: 4px;
112 | }
113 | thead:last-child tr:last-child th:last-child,
114 | tbody:last-child tr:last-child td:last-child,
115 | tfoot:last-child tr:last-child td:last-child {
116 | -webkit-border-bottom-right-radius: 4px;
117 | border-bottom-right-radius: 4px;
118 | -moz-border-radius-bottomright: 4px;
119 | }
120 |
121 | // Special fixes to round the left border on the first td/th
122 | caption + thead tr:first-child th:first-child,
123 | caption + tbody tr:first-child td:first-child,
124 | colgroup + thead tr:first-child th:first-child,
125 | colgroup + tbody tr:first-child td:first-child {
126 | -webkit-border-top-left-radius: 4px;
127 | border-top-left-radius: 4px;
128 | -moz-border-radius-topleft: 4px;
129 | }
130 | caption + thead tr:first-child th:last-child,
131 | caption + tbody tr:first-child td:last-child,
132 | colgroup + thead tr:first-child th:last-child,
133 | colgroup + tbody tr:first-child td:last-child {
134 | -webkit-border-top-right-radius: 4px;
135 | border-top-right-radius: 4px;
136 | -moz-border-radius-topleft: 4px;
137 | }
138 |
139 | }
140 |
141 |
142 |
143 |
144 | // ZEBRA-STRIPING
145 | // --------------
146 |
147 | // Default zebra-stripe styles (alternating gray and transparent backgrounds)
148 | .table-striped {
149 | tbody {
150 | tr:nth-child(odd) td,
151 | tr:nth-child(odd) th {
152 | background-color: @tableBackgroundAccent;
153 | }
154 | }
155 | }
156 |
157 |
158 | // HOVER EFFECT
159 | // ------------
160 | // Placed here since it has to come after the potential zebra striping
161 | .table-hover {
162 | tbody {
163 | tr:hover td,
164 | tr:hover th {
165 | background-color: @tableBackgroundHover;
166 | }
167 | }
168 | }
169 |
170 |
171 | // TABLE CELL SIZING
172 | // -----------------
173 |
174 | // Reset default grid behavior
175 | table [class*=span],
176 | .row-fluid table [class*=span] {
177 | display: table-cell;
178 | float: none; // undo default grid column styles
179 | margin-left: 0; // undo default grid column styles
180 | }
181 |
182 | // Change the column widths to account for td/th padding
183 | .table {
184 | .span1 { .tableColumns(1); }
185 | .span2 { .tableColumns(2); }
186 | .span3 { .tableColumns(3); }
187 | .span4 { .tableColumns(4); }
188 | .span5 { .tableColumns(5); }
189 | .span6 { .tableColumns(6); }
190 | .span7 { .tableColumns(7); }
191 | .span8 { .tableColumns(8); }
192 | .span9 { .tableColumns(9); }
193 | .span10 { .tableColumns(10); }
194 | .span11 { .tableColumns(11); }
195 | .span12 { .tableColumns(12); }
196 | .span13 { .tableColumns(13); }
197 | .span14 { .tableColumns(14); }
198 | .span15 { .tableColumns(15); }
199 | .span16 { .tableColumns(16); }
200 | .span17 { .tableColumns(17); }
201 | .span18 { .tableColumns(18); }
202 | .span19 { .tableColumns(19); }
203 | .span20 { .tableColumns(20); }
204 | .span21 { .tableColumns(21); }
205 | .span22 { .tableColumns(22); }
206 | .span23 { .tableColumns(23); }
207 | .span24 { .tableColumns(24); }
208 | }
209 |
210 |
211 |
212 | // TABLE BACKGROUNDS
213 | // -----------------
214 | // Exact selectors below required to override .table-striped
215 |
216 | .table tbody tr {
217 | &.success td {
218 | background-color: @successBackground;
219 | }
220 | &.error td {
221 | background-color: @errorBackground;
222 | }
223 | &.warning td {
224 | background-color: @warningBackground;
225 | }
226 | &.info td {
227 | background-color: @infoBackground;
228 | }
229 | }
230 |
231 | // Hover states for .table-hover
232 | .table-hover tbody tr {
233 | &.success:hover td {
234 | background-color: darken(@successBackground, 5%);
235 | }
236 | &.error:hover td {
237 | background-color: darken(@errorBackground, 5%);
238 | }
239 | &.warning:hover td {
240 | background-color: darken(@warningBackground, 5%);
241 | }
242 | &.info:hover td {
243 | background-color: darken(@infoBackground, 5%);
244 | }
245 | }
246 |
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-modal.js:
--------------------------------------------------------------------------------
1 | /* =========================================================
2 | * bootstrap-modal.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#modals
4 | * =========================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ========================================================= */
19 |
20 |
21 | !function ($) {
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* MODAL CLASS DEFINITION
27 | * ====================== */
28 |
29 | var Modal = function (element, options) {
30 | this.options = options
31 | this.$element = $(element)
32 | .delegate('[data-dismiss="modal"]', 'click.dismiss.modal', $.proxy(this.hide, this))
33 | this.options.remote && this.$element.find('.modal-body').load(this.options.remote)
34 | }
35 |
36 | Modal.prototype = {
37 |
38 | constructor: Modal
39 |
40 | , toggle: function () {
41 | return this[!this.isShown ? 'show' : 'hide']()
42 | }
43 |
44 | , show: function () {
45 | var that = this
46 | , e = $.Event('show')
47 |
48 | this.$element.trigger(e)
49 |
50 | if (this.isShown || e.isDefaultPrevented()) return
51 |
52 | $('body').addClass('modal-open')
53 |
54 | this.isShown = true
55 |
56 | this.escape()
57 |
58 | this.backdrop(function () {
59 | var transition = $.support.transition && that.$element.hasClass('fade')
60 |
61 | if (!that.$element.parent().length) {
62 | that.$element.appendTo(document.body) //don't move modals dom position
63 | }
64 |
65 | that.$element
66 | .show()
67 |
68 | if (transition) {
69 | that.$element[0].offsetWidth // force reflow
70 | }
71 |
72 | that.$element
73 | .addClass('in')
74 | .attr('aria-hidden', false)
75 | .focus()
76 |
77 | that.enforceFocus()
78 |
79 | transition ?
80 | that.$element.one($.support.transition.end, function () { that.$element.trigger('shown') }) :
81 | that.$element.trigger('shown')
82 |
83 | })
84 | }
85 |
86 | , hide: function (e) {
87 | e && e.preventDefault()
88 |
89 | var that = this
90 |
91 | e = $.Event('hide')
92 |
93 | this.$element.trigger(e)
94 |
95 | if (!this.isShown || e.isDefaultPrevented()) return
96 |
97 | this.isShown = false
98 |
99 | $('body').removeClass('modal-open')
100 |
101 | this.escape()
102 |
103 | $(document).off('focusin.modal')
104 |
105 | this.$element
106 | .removeClass('in')
107 | .attr('aria-hidden', true)
108 |
109 | $.support.transition && this.$element.hasClass('fade') ?
110 | this.hideWithTransition() :
111 | this.hideModal()
112 | }
113 |
114 | , enforceFocus: function () {
115 | var that = this
116 | $(document).on('focusin.modal', function (e) {
117 | if (that.$element[0] !== e.target && !that.$element.has(e.target).length) {
118 | that.$element.focus()
119 | }
120 | })
121 | }
122 |
123 | , escape: function () {
124 | var that = this
125 | if (this.isShown && this.options.keyboard) {
126 | this.$element.on('keyup.dismiss.modal', function ( e ) {
127 | e.which == 27 && that.hide()
128 | })
129 | } else if (!this.isShown) {
130 | this.$element.off('keyup.dismiss.modal')
131 | }
132 | }
133 |
134 | , hideWithTransition: function () {
135 | var that = this
136 | , timeout = setTimeout(function () {
137 | that.$element.off($.support.transition.end)
138 | that.hideModal()
139 | }, 500)
140 |
141 | this.$element.one($.support.transition.end, function () {
142 | clearTimeout(timeout)
143 | that.hideModal()
144 | })
145 | }
146 |
147 | , hideModal: function (that) {
148 | this.$element
149 | .hide()
150 | .trigger('hidden')
151 |
152 | this.backdrop()
153 | }
154 |
155 | , removeBackdrop: function () {
156 | this.$backdrop.remove()
157 | this.$backdrop = null
158 | }
159 |
160 | , backdrop: function (callback) {
161 | var that = this
162 | , animate = this.$element.hasClass('fade') ? 'fade' : ''
163 |
164 | if (this.isShown && this.options.backdrop) {
165 | var doAnimate = $.support.transition && animate
166 |
167 | this.$backdrop = $('')
168 | .appendTo(document.body)
169 |
170 | if (this.options.backdrop != 'static') {
171 | this.$backdrop.click($.proxy(this.hide, this))
172 | }
173 |
174 | if (doAnimate) this.$backdrop[0].offsetWidth // force reflow
175 |
176 | this.$backdrop.addClass('in')
177 |
178 | doAnimate ?
179 | this.$backdrop.one($.support.transition.end, callback) :
180 | callback()
181 |
182 | } else if (!this.isShown && this.$backdrop) {
183 | this.$backdrop.removeClass('in')
184 |
185 | $.support.transition && this.$element.hasClass('fade')?
186 | this.$backdrop.one($.support.transition.end, $.proxy(this.removeBackdrop, this)) :
187 | this.removeBackdrop()
188 |
189 | } else if (callback) {
190 | callback()
191 | }
192 | }
193 | }
194 |
195 |
196 | /* MODAL PLUGIN DEFINITION
197 | * ======================= */
198 |
199 | $.fn.modal = function (option) {
200 | return this.each(function () {
201 | var $this = $(this)
202 | , data = $this.data('modal')
203 | , options = $.extend({}, $.fn.modal.defaults, $this.data(), typeof option == 'object' && option)
204 | if (!data) $this.data('modal', (data = new Modal(this, options)))
205 | if (typeof option == 'string') data[option]()
206 | else if (options.show) data.show()
207 | })
208 | }
209 |
210 | $.fn.modal.defaults = {
211 | backdrop: true
212 | , keyboard: true
213 | , show: true
214 | }
215 |
216 | $.fn.modal.Constructor = Modal
217 |
218 |
219 | /* MODAL DATA-API
220 | * ============== */
221 |
222 | $(function () {
223 | $('body').on('click.modal.data-api', '[data-toggle="modal"]', function ( e ) {
224 | var $this = $(this)
225 | , href = $this.attr('href')
226 | , $target = $($this.attr('data-target') || (href && href.replace(/.*(?=#[^\s]+$)/, ''))) //strip for ie7
227 | , option = $target.data('modal') ? 'toggle' : $.extend({ remote: !/#/.test(href) && href }, $target.data(), $this.data())
228 |
229 | e.preventDefault()
230 |
231 | $target
232 | .modal(option)
233 | .one('hide', function () {
234 | $this.focus()
235 | })
236 | })
237 | })
238 |
239 | }(window.jQuery);
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-typeahead.js:
--------------------------------------------------------------------------------
1 | /* =============================================================
2 | * bootstrap-typeahead.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#typeahead
4 | * =============================================================
5 | * Copyright 2012 Twitter, Inc.
6 | *
7 | * Licensed under the Apache License, Version 2.0 (the "License");
8 | * you may not use this file except in compliance with the License.
9 | * You may obtain a copy of the License at
10 | *
11 | * http://www.apache.org/licenses/LICENSE-2.0
12 | *
13 | * Unless required by applicable law or agreed to in writing, software
14 | * distributed under the License is distributed on an "AS IS" BASIS,
15 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 | * See the License for the specific language governing permissions and
17 | * limitations under the License.
18 | * ============================================================ */
19 |
20 |
21 | !function($){
22 |
23 | "use strict"; // jshint ;_;
24 |
25 |
26 | /* TYPEAHEAD PUBLIC CLASS DEFINITION
27 | * ================================= */
28 |
29 | var Typeahead = function (element, options) {
30 | this.$element = $(element)
31 | this.options = $.extend({}, $.fn.typeahead.defaults, options)
32 | this.matcher = this.options.matcher || this.matcher
33 | this.sorter = this.options.sorter || this.sorter
34 | this.highlighter = this.options.highlighter || this.highlighter
35 | this.updater = this.options.updater || this.updater
36 | this.$menu = $(this.options.menu).appendTo('body')
37 | this.source = this.options.source
38 | this.shown = false
39 | this.listen()
40 | }
41 |
42 | Typeahead.prototype = {
43 |
44 | constructor: Typeahead
45 |
46 | , select: function () {
47 | var val = this.$menu.find('.active').attr('data-value')
48 | this.$element
49 | .val(this.updater(val))
50 | .change()
51 | return this.hide()
52 | }
53 |
54 | , updater: function (item) {
55 | return item
56 | }
57 |
58 | , show: function () {
59 | var pos = $.extend({}, this.$element.offset(), {
60 | height: this.$element[0].offsetHeight
61 | })
62 |
63 | this.$menu.css({
64 | top: pos.top + pos.height
65 | , left: pos.left
66 | })
67 |
68 | this.$menu.show()
69 | this.shown = true
70 | return this
71 | }
72 |
73 | , hide: function () {
74 | this.$menu.hide()
75 | this.shown = false
76 | return this
77 | }
78 |
79 | , lookup: function (event) {
80 | var items
81 |
82 | this.query = this.$element.val()
83 |
84 | if (!this.query || this.query.length < this.options.minLength) {
85 | return this.shown ? this.hide() : this
86 | }
87 |
88 | items = $.isFunction(this.source) ? this.source(this.query, $.proxy(this.process, this)) : this.source
89 |
90 | return items ? this.process(items) : this
91 | }
92 |
93 | , process: function (items) {
94 | var that = this
95 |
96 | items = $.grep(items, function (item) {
97 | return that.matcher(item)
98 | })
99 |
100 | items = this.sorter(items)
101 |
102 | if (!items.length) {
103 | return this.shown ? this.hide() : this
104 | }
105 |
106 | return this.render(items.slice(0, this.options.items)).show()
107 | }
108 |
109 | , matcher: function (item) {
110 | return ~item.toLowerCase().indexOf(this.query.toLowerCase())
111 | }
112 |
113 | , sorter: function (items) {
114 | var beginswith = []
115 | , caseSensitive = []
116 | , caseInsensitive = []
117 | , item
118 |
119 | while (item = items.shift()) {
120 | if (!item.toLowerCase().indexOf(this.query.toLowerCase())) beginswith.push(item)
121 | else if (~item.indexOf(this.query)) caseSensitive.push(item)
122 | else caseInsensitive.push(item)
123 | }
124 |
125 | return beginswith.concat(caseSensitive, caseInsensitive)
126 | }
127 |
128 | , highlighter: function (item) {
129 | var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&')
130 | return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
131 | return '' + match + ''
132 | })
133 | }
134 |
135 | , render: function (items) {
136 | var that = this
137 |
138 | items = $(items).map(function (i, item) {
139 | i = $(that.options.item).attr('data-value', item)
140 | i.find('a').html(that.highlighter(item))
141 | return i[0]
142 | })
143 |
144 | items.first().addClass('active')
145 | this.$menu.html(items)
146 | return this
147 | }
148 |
149 | , next: function (event) {
150 | var active = this.$menu.find('.active').removeClass('active')
151 | , next = active.next()
152 |
153 | if (!next.length) {
154 | next = $(this.$menu.find('li')[0])
155 | }
156 |
157 | next.addClass('active')
158 | }
159 |
160 | , prev: function (event) {
161 | var active = this.$menu.find('.active').removeClass('active')
162 | , prev = active.prev()
163 |
164 | if (!prev.length) {
165 | prev = this.$menu.find('li').last()
166 | }
167 |
168 | prev.addClass('active')
169 | }
170 |
171 | , listen: function () {
172 | this.$element
173 | .on('blur', $.proxy(this.blur, this))
174 | .on('keypress', $.proxy(this.keypress, this))
175 | .on('keyup', $.proxy(this.keyup, this))
176 |
177 | if ($.browser.chrome || $.browser.webkit || $.browser.msie) {
178 | this.$element.on('keydown', $.proxy(this.keydown, this))
179 | }
180 |
181 | this.$menu
182 | .on('click', $.proxy(this.click, this))
183 | .on('mouseenter', 'li', $.proxy(this.mouseenter, this))
184 | }
185 |
186 | , move: function (e) {
187 | if (!this.shown) return
188 |
189 | switch(e.keyCode) {
190 | case 9: // tab
191 | case 13: // enter
192 | case 27: // escape
193 | e.preventDefault()
194 | break
195 |
196 | case 38: // up arrow
197 | e.preventDefault()
198 | this.prev()
199 | break
200 |
201 | case 40: // down arrow
202 | e.preventDefault()
203 | this.next()
204 | break
205 | }
206 |
207 | e.stopPropagation()
208 | }
209 |
210 | , keydown: function (e) {
211 | this.suppressKeyPressRepeat = !~$.inArray(e.keyCode, [40,38,9,13,27])
212 | this.move(e)
213 | }
214 |
215 | , keypress: function (e) {
216 | if (this.suppressKeyPressRepeat) return
217 | this.move(e)
218 | }
219 |
220 | , keyup: function (e) {
221 | switch(e.keyCode) {
222 | case 40: // down arrow
223 | case 38: // up arrow
224 | break
225 |
226 | case 9: // tab
227 | case 13: // enter
228 | if (!this.shown) return
229 | this.select()
230 | break
231 |
232 | case 27: // escape
233 | if (!this.shown) return
234 | this.hide()
235 | break
236 |
237 | default:
238 | this.lookup()
239 | }
240 |
241 | e.stopPropagation()
242 | e.preventDefault()
243 | }
244 |
245 | , blur: function (e) {
246 | var that = this
247 | setTimeout(function () { that.hide() }, 150)
248 | }
249 |
250 | , click: function (e) {
251 | e.stopPropagation()
252 | e.preventDefault()
253 | this.select()
254 | }
255 |
256 | , mouseenter: function (e) {
257 | this.$menu.find('.active').removeClass('active')
258 | $(e.currentTarget).addClass('active')
259 | }
260 |
261 | }
262 |
263 |
264 | /* TYPEAHEAD PLUGIN DEFINITION
265 | * =========================== */
266 |
267 | $.fn.typeahead = function (option) {
268 | return this.each(function () {
269 | var $this = $(this)
270 | , data = $this.data('typeahead')
271 | , options = typeof option == 'object' && option
272 | if (!data) $this.data('typeahead', (data = new Typeahead(this, options)))
273 | if (typeof option == 'string') data[option]()
274 | })
275 | }
276 |
277 | $.fn.typeahead.defaults = {
278 | source: []
279 | , items: 8
280 | , menu: ''
281 | , item: ''
282 | , minLength: 1
283 | }
284 |
285 | $.fn.typeahead.Constructor = Typeahead
286 |
287 |
288 | /* TYPEAHEAD DATA-API
289 | * ================== */
290 |
291 | $(function () {
292 | $('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
293 | var $this = $(this)
294 | if ($this.data('typeahead')) return
295 | e.preventDefault()
296 | $this.typeahead($this.data())
297 | })
298 | })
299 |
300 | }(window.jQuery);
301 |
--------------------------------------------------------------------------------
/app/assets/js/bootstrap/bootstrap-tooltip.js:
--------------------------------------------------------------------------------
1 | /* ===========================================================
2 | * bootstrap-tooltip.js v2.1.1
3 | * http://twitter.github.com/bootstrap/javascript.html#tooltips
4 | * Inspired by the original jQuery.tipsy by Jason Frame
5 | * ===========================================================
6 | * Copyright 2012 Twitter, Inc.
7 | *
8 | * Licensed under the Apache License, Version 2.0 (the "License");
9 | * you may not use this file except in compliance with the License.
10 | * You may obtain a copy of the License at
11 | *
12 | * http://www.apache.org/licenses/LICENSE-2.0
13 | *
14 | * Unless required by applicable law or agreed to in writing, software
15 | * distributed under the License is distributed on an "AS IS" BASIS,
16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 | * See the License for the specific language governing permissions and
18 | * limitations under the License.
19 | * ========================================================== */
20 |
21 |
22 | !function ($) {
23 |
24 | "use strict"; // jshint ;_;
25 |
26 |
27 | /* TOOLTIP PUBLIC CLASS DEFINITION
28 | * =============================== */
29 |
30 | var Tooltip = function (element, options) {
31 | this.init('tooltip', element, options)
32 | }
33 |
34 | Tooltip.prototype = {
35 |
36 | constructor: Tooltip
37 |
38 | , init: function (type, element, options) {
39 | var eventIn
40 | , eventOut
41 |
42 | this.type = type
43 | this.$element = $(element)
44 | this.options = this.getOptions(options)
45 | this.enabled = true
46 |
47 | if (this.options.trigger == 'click') {
48 | this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this))
49 | } else if (this.options.trigger != 'manual') {
50 | eventIn = this.options.trigger == 'hover' ? 'mouseenter' : 'focus'
51 | eventOut = this.options.trigger == 'hover' ? 'mouseleave' : 'blur'
52 | this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this))
53 | this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this))
54 | }
55 |
56 | this.options.selector ?
57 | (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) :
58 | this.fixTitle()
59 | }
60 |
61 | , getOptions: function (options) {
62 | options = $.extend({}, $.fn[this.type].defaults, options, this.$element.data())
63 |
64 | if (options.delay && typeof options.delay == 'number') {
65 | options.delay = {
66 | show: options.delay
67 | , hide: options.delay
68 | }
69 | }
70 |
71 | return options
72 | }
73 |
74 | , enter: function (e) {
75 | var self = $(e.currentTarget)[this.type](this._options).data(this.type)
76 |
77 | if (!self.options.delay || !self.options.delay.show) return self.show()
78 |
79 | clearTimeout(this.timeout)
80 | self.hoverState = 'in'
81 | this.timeout = setTimeout(function() {
82 | if (self.hoverState == 'in') self.show()
83 | }, self.options.delay.show)
84 | }
85 |
86 | , leave: function (e) {
87 | var self = $(e.currentTarget)[this.type](this._options).data(this.type)
88 |
89 | if (this.timeout) clearTimeout(this.timeout)
90 | if (!self.options.delay || !self.options.delay.hide) return self.hide()
91 |
92 | self.hoverState = 'out'
93 | this.timeout = setTimeout(function() {
94 | if (self.hoverState == 'out') self.hide()
95 | }, self.options.delay.hide)
96 | }
97 |
98 | , show: function () {
99 | var $tip
100 | , inside
101 | , pos
102 | , actualWidth
103 | , actualHeight
104 | , placement
105 | , tp
106 |
107 | if (this.hasContent() && this.enabled) {
108 | $tip = this.tip()
109 | this.setContent()
110 |
111 | if (this.options.animation) {
112 | $tip.addClass('fade')
113 | }
114 |
115 | placement = typeof this.options.placement == 'function' ?
116 | this.options.placement.call(this, $tip[0], this.$element[0]) :
117 | this.options.placement
118 |
119 | inside = /in/.test(placement)
120 |
121 | $tip
122 | .remove()
123 | .css({ top: 0, left: 0, display: 'block' })
124 | .appendTo(inside ? this.$element : document.body)
125 |
126 | pos = this.getPosition(inside)
127 |
128 | actualWidth = $tip[0].offsetWidth
129 | actualHeight = $tip[0].offsetHeight
130 |
131 | switch (inside ? placement.split(' ')[1] : placement) {
132 | case 'bottom':
133 | tp = {top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2}
134 | break
135 | case 'top':
136 | tp = {top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2}
137 | break
138 | case 'left':
139 | tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth}
140 | break
141 | case 'right':
142 | tp = {top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width}
143 | break
144 | }
145 |
146 | $tip
147 | .css(tp)
148 | .addClass(placement)
149 | .addClass('in')
150 | }
151 | }
152 |
153 | , setContent: function () {
154 | var $tip = this.tip()
155 | , title = this.getTitle()
156 |
157 | $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title)
158 | $tip.removeClass('fade in top bottom left right')
159 | }
160 |
161 | , hide: function () {
162 | var that = this
163 | , $tip = this.tip()
164 |
165 | $tip.removeClass('in')
166 |
167 | function removeWithAnimation() {
168 | var timeout = setTimeout(function () {
169 | $tip.off($.support.transition.end).remove()
170 | }, 500)
171 |
172 | $tip.one($.support.transition.end, function () {
173 | clearTimeout(timeout)
174 | $tip.remove()
175 | })
176 | }
177 |
178 | $.support.transition && this.$tip.hasClass('fade') ?
179 | removeWithAnimation() :
180 | $tip.remove()
181 |
182 | return this
183 | }
184 |
185 | , fixTitle: function () {
186 | var $e = this.$element
187 | if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') {
188 | $e.attr('data-original-title', $e.attr('title') || '').removeAttr('title')
189 | }
190 | }
191 |
192 | , hasContent: function () {
193 | return this.getTitle()
194 | }
195 |
196 | , getPosition: function (inside) {
197 | return $.extend({}, (inside ? {top: 0, left: 0} : this.$element.offset()), {
198 | width: this.$element[0].offsetWidth
199 | , height: this.$element[0].offsetHeight
200 | })
201 | }
202 |
203 | , getTitle: function () {
204 | var title
205 | , $e = this.$element
206 | , o = this.options
207 |
208 | title = $e.attr('data-original-title')
209 | || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title)
210 |
211 | return title
212 | }
213 |
214 | , tip: function () {
215 | return this.$tip = this.$tip || $(this.options.template)
216 | }
217 |
218 | , validate: function () {
219 | if (!this.$element[0].parentNode) {
220 | this.hide()
221 | this.$element = null
222 | this.options = null
223 | }
224 | }
225 |
226 | , enable: function () {
227 | this.enabled = true
228 | }
229 |
230 | , disable: function () {
231 | this.enabled = false
232 | }
233 |
234 | , toggleEnabled: function () {
235 | this.enabled = !this.enabled
236 | }
237 |
238 | , toggle: function () {
239 | this[this.tip().hasClass('in') ? 'hide' : 'show']()
240 | }
241 |
242 | , destroy: function () {
243 | this.hide().$element.off('.' + this.type).removeData(this.type)
244 | }
245 |
246 | }
247 |
248 |
249 | /* TOOLTIP PLUGIN DEFINITION
250 | * ========================= */
251 |
252 | $.fn.tooltip = function ( option ) {
253 | return this.each(function () {
254 | var $this = $(this)
255 | , data = $this.data('tooltip')
256 | , options = typeof option == 'object' && option
257 | if (!data) $this.data('tooltip', (data = new Tooltip(this, options)))
258 | if (typeof option == 'string') data[option]()
259 | })
260 | }
261 |
262 | $.fn.tooltip.Constructor = Tooltip
263 |
264 | $.fn.tooltip.defaults = {
265 | animation: true
266 | , placement: 'top'
267 | , selector: false
268 | , template: ''
269 | , trigger: 'hover'
270 | , title: ''
271 | , delay: 0
272 | , html: true
273 | }
274 |
275 | }(window.jQuery);
276 |
--------------------------------------------------------------------------------
/app/assets/css/lib/navs.less:
--------------------------------------------------------------------------------
1 | //
2 | // Navs
3 | // --------------------------------------------------
4 |
5 |
6 | // BASE CLASS
7 | // ----------
8 |
9 | .nav {
10 | margin-left: 0;
11 | margin-bottom: @baseLineHeight;
12 | list-style: none;
13 | }
14 |
15 | // Make links block level
16 | .nav > li > a {
17 | display: block;
18 | }
19 | .nav > li > a:hover {
20 | text-decoration: none;
21 | background-color: @grayLighter;
22 | }
23 |
24 | // Redeclare pull classes because of specifity
25 | .nav > .pull-right {
26 | float: right;
27 | }
28 |
29 | // Nav headers (for dropdowns and lists)
30 | .nav-header {
31 | display: block;
32 | padding: 3px 15px;
33 | font-size: 11px;
34 | font-weight: bold;
35 | line-height: @baseLineHeight;
36 | color: @grayLight;
37 | text-shadow: 0 1px 0 rgba(255,255,255,.5);
38 | text-transform: uppercase;
39 | }
40 | // Space them out when they follow another list item (link)
41 | .nav li + .nav-header {
42 | margin-top: 9px;
43 | }
44 |
45 |
46 |
47 | // NAV LIST
48 | // --------
49 |
50 | .nav-list {
51 | padding-left: 15px;
52 | padding-right: 15px;
53 | margin-bottom: 0;
54 | }
55 | .nav-list > li > a,
56 | .nav-list .nav-header {
57 | margin-left: -15px;
58 | margin-right: -15px;
59 | text-shadow: 0 1px 0 rgba(255,255,255,.5);
60 | }
61 | .nav-list > li > a {
62 | padding: 3px 15px;
63 | }
64 | .nav-list > .active > a,
65 | .nav-list > .active > a:hover {
66 | color: @white;
67 | text-shadow: 0 -1px 0 rgba(0,0,0,.2);
68 | background-color: @linkColor;
69 | }
70 | .nav-list [class^="icon-"] {
71 | margin-right: 2px;
72 | }
73 | // Dividers (basically an hr) within the dropdown
74 | .nav-list .divider {
75 | .nav-divider();
76 | }
77 |
78 |
79 |
80 | // TABS AND PILLS
81 | // -------------
82 |
83 | // Common styles
84 | .nav-tabs,
85 | .nav-pills {
86 | .clearfix();
87 | }
88 | .nav-tabs > li,
89 | .nav-pills > li {
90 | float: left;
91 | }
92 | .nav-tabs > li > a,
93 | .nav-pills > li > a {
94 | padding-right: 12px;
95 | padding-left: 12px;
96 | margin-right: 2px;
97 | line-height: 14px; // keeps the overall height an even number
98 | }
99 |
100 | // TABS
101 | // ----
102 |
103 | // Give the tabs something to sit on
104 | .nav-tabs {
105 | border-bottom: 1px solid #ddd;
106 | }
107 | // Make the list-items overlay the bottom border
108 | .nav-tabs > li {
109 | margin-bottom: -1px;
110 | }
111 | // Actual tabs (as links)
112 | .nav-tabs > li > a {
113 | padding-top: 8px;
114 | padding-bottom: 8px;
115 | line-height: @baseLineHeight;
116 | border: 1px solid transparent;
117 | .border-radius(4px 4px 0 0);
118 | &:hover {
119 | border-color: @grayLighter @grayLighter #ddd;
120 | }
121 | }
122 | // Active state, and it's :hover to override normal :hover
123 | .nav-tabs > .active > a,
124 | .nav-tabs > .active > a:hover {
125 | color: @gray;
126 | background-color: @white;
127 | border: 1px solid #ddd;
128 | border-bottom-color: transparent;
129 | cursor: default;
130 | }
131 |
132 |
133 | // PILLS
134 | // -----
135 |
136 | // Links rendered as pills
137 | .nav-pills > li > a {
138 | padding-top: 8px;
139 | padding-bottom: 8px;
140 | margin-top: 2px;
141 | margin-bottom: 2px;
142 | .border-radius(5px);
143 | }
144 |
145 | // Active state
146 | .nav-pills > .active > a,
147 | .nav-pills > .active > a:hover {
148 | color: @white;
149 | background-color: @linkColor;
150 | }
151 |
152 |
153 |
154 | // STACKED NAV
155 | // -----------
156 |
157 | // Stacked tabs and pills
158 | .nav-stacked > li {
159 | float: none;
160 | }
161 | .nav-stacked > li > a {
162 | margin-right: 0; // no need for the gap between nav items
163 | }
164 |
165 | // Tabs
166 | .nav-tabs.nav-stacked {
167 | border-bottom: 0;
168 | }
169 | .nav-tabs.nav-stacked > li > a {
170 | border: 1px solid #ddd;
171 | .border-radius(0);
172 | }
173 | .nav-tabs.nav-stacked > li:first-child > a {
174 | .border-top-radius(4px);
175 | }
176 | .nav-tabs.nav-stacked > li:last-child > a {
177 | .border-bottom-radius(4px);
178 | }
179 | .nav-tabs.nav-stacked > li > a:hover {
180 | border-color: #ddd;
181 | z-index: 2;
182 | }
183 |
184 | // Pills
185 | .nav-pills.nav-stacked > li > a {
186 | margin-bottom: 3px;
187 | }
188 | .nav-pills.nav-stacked > li:last-child > a {
189 | margin-bottom: 1px; // decrease margin to match sizing of stacked tabs
190 | }
191 |
192 |
193 |
194 | // DROPDOWNS
195 | // ---------
196 |
197 | .nav-tabs .dropdown-menu {
198 | .border-radius(0 0 6px 6px); // remove the top rounded corners here since there is a hard edge above the menu
199 | }
200 | .nav-pills .dropdown-menu {
201 | .border-radius(6px); // make rounded corners match the pills
202 | }
203 |
204 | // Default dropdown links
205 | // -------------------------
206 | // Make carets use linkColor to start
207 | .nav .dropdown-toggle .caret {
208 | border-top-color: @linkColor;
209 | border-bottom-color: @linkColor;
210 | margin-top: 6px;
211 | }
212 | .nav .dropdown-toggle:hover .caret {
213 | border-top-color: @linkColorHover;
214 | border-bottom-color: @linkColorHover;
215 | }
216 | /* move down carets for tabs */
217 | .nav-tabs .dropdown-toggle .caret {
218 | margin-top: 8px;
219 | }
220 |
221 | // Active dropdown links
222 | // -------------------------
223 | .nav .active .dropdown-toggle .caret {
224 | border-top-color: #fff;
225 | border-bottom-color: #fff;
226 | }
227 | .nav-tabs .active .dropdown-toggle .caret {
228 | border-top-color: @gray;
229 | border-bottom-color: @gray;
230 | }
231 |
232 | // Active:hover dropdown links
233 | // -------------------------
234 | .nav > .dropdown.active > a:hover {
235 | cursor: pointer;
236 | }
237 |
238 | // Open dropdowns
239 | // -------------------------
240 | .nav-tabs .open .dropdown-toggle,
241 | .nav-pills .open .dropdown-toggle,
242 | .nav > li.dropdown.open.active > a:hover {
243 | color: @white;
244 | background-color: @grayLight;
245 | border-color: @grayLight;
246 | }
247 | .nav li.dropdown.open .caret,
248 | .nav li.dropdown.open.active .caret,
249 | .nav li.dropdown.open a:hover .caret {
250 | border-top-color: @white;
251 | border-bottom-color: @white;
252 | .opacity(100);
253 | }
254 |
255 | // Dropdowns in stacked tabs
256 | .tabs-stacked .open > a:hover {
257 | border-color: @grayLight;
258 | }
259 |
260 |
261 |
262 | // TABBABLE
263 | // --------
264 |
265 |
266 | // COMMON STYLES
267 | // -------------
268 |
269 | // Clear any floats
270 | .tabbable {
271 | .clearfix();
272 | }
273 | .tab-content {
274 | overflow: auto; // prevent content from running below tabs
275 | }
276 |
277 | // Remove border on bottom, left, right
278 | .tabs-below > .nav-tabs,
279 | .tabs-right > .nav-tabs,
280 | .tabs-left > .nav-tabs {
281 | border-bottom: 0;
282 | }
283 |
284 | // Show/hide tabbable areas
285 | .tab-content > .tab-pane,
286 | .pill-content > .pill-pane {
287 | display: none;
288 | }
289 | .tab-content > .active,
290 | .pill-content > .active {
291 | display: block;
292 | }
293 |
294 |
295 | // BOTTOM
296 | // ------
297 |
298 | .tabs-below > .nav-tabs {
299 | border-top: 1px solid #ddd;
300 | }
301 | .tabs-below > .nav-tabs > li {
302 | margin-top: -1px;
303 | margin-bottom: 0;
304 | }
305 | .tabs-below > .nav-tabs > li > a {
306 | .border-radius(0 0 4px 4px);
307 | &:hover {
308 | border-bottom-color: transparent;
309 | border-top-color: #ddd;
310 | }
311 | }
312 | .tabs-below > .nav-tabs > .active > a,
313 | .tabs-below > .nav-tabs > .active > a:hover {
314 | border-color: transparent #ddd #ddd #ddd;
315 | }
316 |
317 | // LEFT & RIGHT
318 | // ------------
319 |
320 | // Common styles
321 | .tabs-left > .nav-tabs > li,
322 | .tabs-right > .nav-tabs > li {
323 | float: none;
324 | }
325 | .tabs-left > .nav-tabs > li > a,
326 | .tabs-right > .nav-tabs > li > a {
327 | min-width: 74px;
328 | margin-right: 0;
329 | margin-bottom: 3px;
330 | }
331 |
332 | // Tabs on the left
333 | .tabs-left > .nav-tabs {
334 | float: left;
335 | margin-right: 19px;
336 | border-right: 1px solid #ddd;
337 | }
338 | .tabs-left > .nav-tabs > li > a {
339 | margin-right: -1px;
340 | .border-radius(4px 0 0 4px);
341 | }
342 | .tabs-left > .nav-tabs > li > a:hover {
343 | border-color: @grayLighter #ddd @grayLighter @grayLighter;
344 | }
345 | .tabs-left > .nav-tabs .active > a,
346 | .tabs-left > .nav-tabs .active > a:hover {
347 | border-color: #ddd transparent #ddd #ddd;
348 | *border-right-color: @white;
349 | }
350 |
351 | // Tabs on the right
352 | .tabs-right > .nav-tabs {
353 | float: right;
354 | margin-left: 19px;
355 | border-left: 1px solid #ddd;
356 | }
357 | .tabs-right > .nav-tabs > li > a {
358 | margin-left: -1px;
359 | .border-radius(0 4px 4px 0);
360 | }
361 | .tabs-right > .nav-tabs > li > a:hover {
362 | border-color: @grayLighter @grayLighter @grayLighter #ddd;
363 | }
364 | .tabs-right > .nav-tabs .active > a,
365 | .tabs-right > .nav-tabs .active > a:hover {
366 | border-color: #ddd #ddd #ddd transparent;
367 | *border-left-color: @white;
368 | }
369 |
370 |
371 |
372 | // DISABLED STATES
373 | // ---------------
374 |
375 | // Gray out text
376 | .nav > .disabled > a {
377 | color: @grayLight;
378 | }
379 | // Nuke hover effects
380 | .nav > .disabled > a:hover {
381 | text-decoration: none;
382 | background-color: transparent;
383 | cursor: default;
384 | }
385 |
--------------------------------------------------------------------------------
/test/models/data_spec.js:
--------------------------------------------------------------------------------
1 | var async, expect, helper;
2 |
3 | helper = require("../spec_helper");
4 |
5 | expect = require('chai').expect;
6 |
7 | async = require("async");
8 |
9 | describe("Data", function () {
10 | var models;
11 | models = null;
12 | before(function () {
13 | helper.globalSetup();
14 | return models = helper.app.models;
15 | });
16 | after(function () {
17 | return helper.globalTearDown();
18 | });
19 | beforeEach(function (done) {
20 | return helper.setup(done);
21 | });
22 | afterEach(function (done) {
23 | return helper.tearDown(done);
24 | });
25 | it("should have a findOrCreate method", function () {
26 | return expect(models.Data.findOrCreate).to.exist;
27 | });
28 | it("should findOrCreate a new instance with a key", function (done) {
29 | return models.Data.findOrCreate("key", (function (_this) {
30 | return function (err, data) {
31 | expect(data).to.eql(new models.Data("key"));
32 | return done();
33 | };
34 | })(this));
35 | });
36 | it("should findOrCreate a new instance with a key and a value", function (done) {
37 | return models.Data.findOrCreate("aaa", "bbbb", (function (_this) {
38 | return function (err, data) {
39 | expect(data).to.eql(new models.Data("aaa", "bbbb"));
40 | return done();
41 | };
42 | })(this));
43 | });
44 | it("should findOrCreate an old instance overriding the value", function (done) {
45 | return models.Data.findOrCreate("aaa", "bbbb", (function (_this) {
46 | return function () {
47 | return models.Data.findOrCreate("aaa", "ccc", function () {
48 | return models.Data.find("aaa", function (err, data) {
49 | expect(data).to.eql(new models.Data("aaa", "ccc"));
50 | return done();
51 | });
52 | });
53 | };
54 | })(this));
55 | });
56 | it("should publish an update when calling findOrCreate", function (done) {
57 | models.Data.subscribe("aaa", (function (_this) {
58 | return function (data) {
59 | return done();
60 | };
61 | })(this));
62 | return models.Data.findOrCreate("aaa", "bbbb");
63 | });
64 | it("should allow subscribing in the create step", function (done) {
65 | return models.Data.findOrCreate("aaa", (function (_this) {
66 | return function (err, data) {
67 | models.Data.subscribe("aaa", function (curr) {
68 | if (curr.value === "ccc") {
69 | return done();
70 | }
71 | });
72 | data.value = "ccc";
73 | return data.save();
74 | };
75 | })(this));
76 | });
77 | it("should allow unsubscribing in the create step", function (done) {
78 | return models.Data.findOrCreate("aaa", (function (_this) {
79 | return function (err, data) {
80 | var func;
81 | func = function () {
82 | throw "This should never be called";
83 | };
84 | models.Data.subscribe("aaa", func);
85 | models.Data.unsubscribe("aaa", func);
86 | models.Data.subscribe("aaa", function (curr) {
87 | return done();
88 | });
89 | return data.save();
90 | };
91 | })(this));
92 | });
93 | it("should provide a find method that returns an error if there is no obj", function (done) {
94 | return models.Data.find("obj", (function (_this) {
95 | return function (err, data) {
96 | expect(err).to.eql("Record not found");
97 | return done();
98 | };
99 | })(this));
100 | });
101 | it("should provide a find method that uses a regexp for matching", function (done) {
102 | var results;
103 | results = [];
104 | return async.parallel([async.apply(models.Data.findOrCreate, "hello bob", "aaa"), async.apply(models.Data.findOrCreate, "hello mark", "aaa")], function () {
105 | return models.Data.find(/hello .*/, function (err, data) {
106 | if (err == null) {
107 | results.push(data.key);
108 | }
109 | if (results.length === 2) {
110 | expect(results).to.contain("hello bob");
111 | expect(results).to.contain("hello mark");
112 | return done();
113 | }
114 | });
115 | });
116 | });
117 | it("should provide a subscribe method that works for new topics", function (done) {
118 | var results;
119 | results = [];
120 | models.Data.subscribe("hello/*", function (data) {
121 | results.push(data.key);
122 | if (results.length === 2) {
123 | expect(results).to.contain("hello/bob");
124 | expect(results).to.contain("hello/mark");
125 | return done();
126 | }
127 | });
128 | return async.parallel([async.apply(models.Data.findOrCreate, "hello/bob", "aaa"), async.apply(models.Data.findOrCreate, "hello/mark", "aaa")]);
129 | });
130 | return describe("instance", function () {
131 | it("should get the key", function () {
132 | var subject;
133 | subject = new models.Data("key", "value");
134 | return expect(subject.key).to.eql("key");
135 | });
136 | it("should get the key (dis)", function () {
137 | var subject;
138 | subject = new models.Data("aaa");
139 | return expect(subject.key).to.eql("aaa");
140 | });
141 | it("should get the value", function () {
142 | var subject;
143 | subject = new models.Data("key", "value");
144 | return expect(subject.value).to.eql("value");
145 | });
146 | it("should get the value (dis)", function () {
147 | var subject;
148 | subject = new models.Data("key", "aaa");
149 | return expect(subject.value).to.eql("aaa");
150 | });
151 | it("should get the redisKey", function () {
152 | var subject;
153 | subject = new models.Data("key", "value");
154 | return expect(subject.redisKey).to.eql("topic:key");
155 | });
156 | it("should get the redisKey (dis)", function () {
157 | var subject;
158 | subject = new models.Data("aaa/42", "value");
159 | return expect(subject.redisKey).to.eql("topic:aaa/42");
160 | });
161 | it("should accept an object as value in the constructor", function () {
162 | var obj, subject;
163 | obj = {
164 | hello: 42
165 | };
166 | subject = new models.Data("key", obj);
167 | return expect(subject.value).to.eql(obj);
168 | });
169 | it("should export its value as JSON", function () {
170 | var obj, subject;
171 | obj = {
172 | hello: 42
173 | };
174 | subject = new models.Data("key", obj);
175 | return expect(subject.jsonValue).to.eql(JSON.stringify(obj));
176 | });
177 | it("should export its value as JSON when setting the value", function () {
178 | var obj, subject;
179 | obj = {
180 | hello: 42
181 | };
182 | subject = new models.Data("key");
183 | subject.value = obj;
184 | return expect(subject.jsonValue).to.eql(JSON.stringify(obj));
185 | });
186 | it("should set the value", function () {
187 | var subject;
188 | subject = new models.Data("key");
189 | subject.value = "bbb";
190 | return expect(subject.value).to.eql("bbb");
191 | });
192 | it("should set the value (dis)", function () {
193 | var subject;
194 | subject = new models.Data("key");
195 | subject.value = "ccc";
196 | return expect(subject.value).to.eql("ccc");
197 | });
198 | it("should set the json value", function () {
199 | var subject;
200 | subject = new models.Data("key");
201 | subject.jsonValue = JSON.stringify("ccc");
202 | return expect(subject.value).to.eql("ccc");
203 | });
204 | it("should have a save method", function () {
205 | var subject;
206 | subject = new models.Data("key");
207 | return expect(subject.save).to.exist;
208 | });
209 | it("should save an array", function (done) {
210 | var subject;
211 | subject = new models.Data("key");
212 | subject.value = [1, 2];
213 | return subject.save((function (_this) {
214 | return function () {
215 | return done();
216 | };
217 | })(this));
218 | });
219 | it("should support subscribing for change", function (done) {
220 | var subject;
221 | subject = new models.Data("key");
222 | return subject.save((function (_this) {
223 | return function () {
224 | models.Data.subscribe(subject.key, function (data) {
225 | expect(data.value).to.equal("aaaa");
226 | return done();
227 | });
228 | subject.value = "aaaa";
229 | return subject.save();
230 | };
231 | })(this));
232 | });
233 | it("should register for change before creation", function (done) {
234 | var subject;
235 | subject = new models.Data("key");
236 | models.Data.subscribe(subject.key, (function (_this) {
237 | return function (data) {
238 | expect(data.value).to.equal("aaaa");
239 | return done();
240 | };
241 | })(this));
242 | subject.value = "aaaa";
243 | return subject.save();
244 | });
245 | it("should save and findOrCreate", function (done) {
246 | var subject;
247 | subject = new models.Data("key");
248 | return subject.save((function (_this) {
249 | return function () {
250 | return models.Data.findOrCreate(subject.key, function (err, data) {
251 | expect(data).to.eql(subject);
252 | return done();
253 | });
254 | };
255 | })(this));
256 | });
257 | it("should save and find", function (done) {
258 | var subject;
259 | subject = new models.Data("key");
260 | return subject.save((function (_this) {
261 | return function () {
262 | return models.Data.find(subject.key, function (err, data) {
263 | expect(data).to.eql(subject);
264 | return done();
265 | });
266 | };
267 | })(this));
268 | });
269 | return it("should not persist the value before save", function (done) {
270 | var subject;
271 | subject = new models.Data("key");
272 | return subject.save((function (_this) {
273 | return function () {
274 | subject.value = "ccc";
275 | return models.Data.find(subject.key, function (err, data) {
276 | expect(data.value).to.not.eql("ccc");
277 | return done();
278 | });
279 | };
280 | })(this));
281 | });
282 | });
283 | });
284 |
--------------------------------------------------------------------------------
/app/assets/css/lib/variables.less:
--------------------------------------------------------------------------------
1 | //
2 | // Variables
3 | // --------------------------------------------------
4 |
5 |
6 | // Global values
7 | // --------------------------------------------------
8 |
9 |
10 | // Grays
11 | // -------------------------
12 | @black: #000;
13 | @grayDarker: #222;
14 | @grayDark: #333;
15 | @gray: #555;
16 | @grayLight: #999;
17 | @grayLighter: #eee;
18 | @white: #fff;
19 |
20 |
21 | // Accent colors
22 | // -------------------------
23 | @blue: #049cdb;
24 | @blueDark: #0064cd;
25 | @green: #46a546;
26 | @red: #9d261d;
27 | @yellow: #ffc40d;
28 | @orange: #f89406;
29 | @pink: #c3325f;
30 | @purple: #7a43b6;
31 |
32 |
33 | // Scaffolding
34 | // -------------------------
35 | @bodyBackground: @white;
36 | @textColor: @grayDark;
37 |
38 |
39 | // Links
40 | // -------------------------
41 | @linkColor: #08c;
42 | @linkColorHover: darken(@linkColor, 15%);
43 |
44 |
45 | // Typography
46 | // -------------------------
47 | @sansFontFamily: "Helvetica Neue", Helvetica, Arial, sans-serif;
48 | @serifFontFamily: Georgia, "Times New Roman", Times, serif;
49 | @monoFontFamily: Monaco, Menlo, Consolas, "Courier New", monospace;
50 |
51 | @baseFontSize: 14px;
52 | @baseFontFamily: @sansFontFamily;
53 | @baseLineHeight: 20px;
54 | @altFontFamily: @serifFontFamily;
55 |
56 | @headingsFontFamily: inherit; // empty to use BS default, @baseFontFamily
57 | @headingsFontWeight: bold; // instead of browser default, bold
58 | @headingsColor: inherit; // empty to use BS default, @textColor
59 |
60 | // Tables
61 | // -------------------------
62 | @tableBackground: transparent; // overall background-color
63 | @tableBackgroundAccent: #f9f9f9; // for striping
64 | @tableBackgroundHover: #f5f5f5; // for hover
65 | @tableBorder: #ddd; // table and cell border
66 |
67 | // Buttons
68 | // -------------------------
69 | @btnBackground: @white;
70 | @btnBackgroundHighlight: darken(@white, 10%);
71 | @btnBorder: #bbb;
72 |
73 | @btnPrimaryBackground: @linkColor;
74 | @btnPrimaryBackgroundHighlight: spin(@btnPrimaryBackground, 20%);
75 |
76 | @btnInfoBackground: #5bc0de;
77 | @btnInfoBackgroundHighlight: #2f96b4;
78 |
79 | @btnSuccessBackground: #62c462;
80 | @btnSuccessBackgroundHighlight: #51a351;
81 |
82 | @btnWarningBackground: lighten(@orange, 15%);
83 | @btnWarningBackgroundHighlight: @orange;
84 |
85 | @btnDangerBackground: #ee5f5b;
86 | @btnDangerBackgroundHighlight: #bd362f;
87 |
88 | @btnInverseBackground: #444;
89 | @btnInverseBackgroundHighlight: @grayDarker;
90 |
91 |
92 | // Forms
93 | // -------------------------
94 | @inputBackground: @white;
95 | @inputBorder: #ccc;
96 | @inputBorderRadius: 3px;
97 | @inputDisabledBackground: @grayLighter;
98 | @formActionsBackground: #f5f5f5;
99 |
100 | // Dropdowns
101 | // -------------------------
102 | @dropdownBackground: @white;
103 | @dropdownBorder: rgba(0,0,0,.2);
104 | @dropdownDividerTop: #e5e5e5;
105 | @dropdownDividerBottom: @white;
106 |
107 | @dropdownLinkColor: @grayDark;
108 | @dropdownLinkColorHover: @white;
109 | @dropdownLinkColorActive: @dropdownLinkColor;
110 |
111 | @dropdownLinkBackgroundActive: @linkColor;
112 | @dropdownLinkBackgroundHover: @dropdownLinkBackgroundActive;
113 |
114 |
115 |
116 | // COMPONENT VARIABLES
117 | // --------------------------------------------------
118 |
119 | // Z-index master list
120 | // -------------------------
121 | // Used for a bird's eye view of components dependent on the z-axis
122 | // Try to avoid customizing these :)
123 | @zindexDropdown: 1000;
124 | @zindexPopover: 1010;
125 | @zindexTooltip: 1030;
126 | @zindexFixedNavbar: 1030;
127 | @zindexModalBackdrop: 1040;
128 | @zindexModal: 1050;
129 |
130 |
131 | // Sprite icons path
132 | // -------------------------
133 | @iconSpritePath: "../img/glyphicons-halflings.png";
134 | @iconWhiteSpritePath: "../img/glyphicons-halflings-white.png";
135 |
136 |
137 | // Input placeholder text color
138 | // -------------------------
139 | @placeholderText: @grayLight;
140 |
141 |
142 | // Hr border color
143 | // -------------------------
144 | @hrBorder: @grayLighter;
145 |
146 |
147 | // Horizontal forms & lists
148 | // -------------------------
149 | @horizontalComponentOffset: 180px;
150 |
151 |
152 | // Wells
153 | // -------------------------
154 | @wellBackground: #f5f5f5;
155 |
156 |
157 | // Navbar
158 | // -------------------------
159 | @navbarCollapseWidth: 979px;
160 |
161 | @navbarHeight: 40px;
162 | @navbarBackgroundHighlight: #ffffff;
163 | @navbarBackground: darken(@navbarBackgroundHighlight, 5%);
164 | @navbarBorder: darken(@navbarBackground, 12%);
165 |
166 | @navbarText: #777;
167 | @navbarLinkColor: #777;
168 | @navbarLinkColorHover: @grayDark;
169 | @navbarLinkColorActive: @gray;
170 | @navbarLinkBackgroundHover: transparent;
171 | @navbarLinkBackgroundActive: darken(@navbarBackground, 5%);
172 |
173 | @navbarBrandColor: @navbarLinkColor;
174 |
175 | // Inverted navbar
176 | @navbarInverseBackground: #111111;
177 | @navbarInverseBackgroundHighlight: #222222;
178 | @navbarInverseBorder: #252525;
179 |
180 | @navbarInverseText: @grayLight;
181 | @navbarInverseLinkColor: @grayLight;
182 | @navbarInverseLinkColorHover: @white;
183 | @navbarInverseLinkColorActive: @navbarInverseLinkColorHover;
184 | @navbarInverseLinkBackgroundHover: transparent;
185 | @navbarInverseLinkBackgroundActive: @navbarInverseBackground;
186 |
187 | @navbarInverseSearchBackground: lighten(@navbarInverseBackground, 25%);
188 | @navbarInverseSearchBackgroundFocus: @white;
189 | @navbarInverseSearchBorder: @navbarInverseBackground;
190 | @navbarInverseSearchPlaceholderColor: #ccc;
191 |
192 | @navbarInverseBrandColor: @navbarInverseLinkColor;
193 |
194 |
195 | // Pagination
196 | // -------------------------
197 | @paginationBackground: #fff;
198 | @paginationBorder: #ddd;
199 | @paginationActiveBackground: #f5f5f5;
200 |
201 |
202 | // Hero unit
203 | // -------------------------
204 | @heroUnitBackground: @grayLighter;
205 | @heroUnitHeadingColor: inherit;
206 | @heroUnitLeadColor: inherit;
207 |
208 |
209 | // Form states and alerts
210 | // -------------------------
211 | @warningText: #c09853;
212 | @warningBackground: #fcf8e3;
213 | @warningBorder: darken(spin(@warningBackground, -10), 3%);
214 |
215 | @errorText: #b94a48;
216 | @errorBackground: #f2dede;
217 | @errorBorder: darken(spin(@errorBackground, -10), 3%);
218 |
219 | @successText: #468847;
220 | @successBackground: #dff0d8;
221 | @successBorder: darken(spin(@successBackground, -10), 5%);
222 |
223 | @infoText: #3a87ad;
224 | @infoBackground: #d9edf7;
225 | @infoBorder: darken(spin(@infoBackground, -10), 7%);
226 |
227 |
228 | // Tooltips and popovers
229 | // -------------------------
230 | @tooltipColor: #fff;
231 | @tooltipBackground: #000;
232 | @tooltipArrowWidth: 5px;
233 | @tooltipArrowColor: @tooltipBackground;
234 |
235 | @popoverBackground: #fff;
236 | @popoverArrowWidth: 10px;
237 | @popoverArrowColor: #fff;
238 | @popoverTitleBackground: darken(@popoverBackground, 3%);
239 |
240 | // Special enhancement for popovers
241 | @popoverArrowOuterWidth: @popoverArrowWidth + 1;
242 | @popoverArrowOuterColor: rgba(0,0,0,.25);
243 |
244 |
245 |
246 | // GRID
247 | // --------------------------------------------------
248 |
249 |
250 | // Default 940px grid
251 | // -------------------------
252 | @gridColumns: 12;
253 | @gridColumnWidth: 60px;
254 | @gridGutterWidth: 20px;
255 | @gridRowWidth: (@gridColumns * @gridColumnWidth) + (@gridGutterWidth * (@gridColumns - 1));
256 |
257 | // 1200px min
258 | @gridColumnWidth1200: 70px;
259 | @gridGutterWidth1200: 30px;
260 | @gridRowWidth1200: (@gridColumns * @gridColumnWidth1200) + (@gridGutterWidth1200 * (@gridColumns - 1));
261 |
262 | // 768px-979px
263 | @gridColumnWidth768: 42px;
264 | @gridGutterWidth768: 20px;
265 | @gridRowWidth768: (@gridColumns * @gridColumnWidth768) + (@gridGutterWidth768 * (@gridColumns - 1));
266 |
267 |
268 | // Fluid grid
269 | // -------------------------
270 | @fluidGridColumnWidth: percentage(@gridColumnWidth/@gridRowWidth);
271 | @fluidGridGutterWidth: percentage(@gridGutterWidth/@gridRowWidth);
272 |
273 | // 1200px min
274 | @fluidGridColumnWidth1200: percentage(@gridColumnWidth1200/@gridRowWidth1200);
275 | @fluidGridGutterWidth1200: percentage(@gridGutterWidth1200/@gridRowWidth1200);
276 |
277 | // 768px-979px
278 | @fluidGridColumnWidth768: percentage(@gridColumnWidth768/@gridRowWidth768);
279 | @fluidGridGutterWidth768: percentage(@gridGutterWidth768/@gridRowWidth768);
280 |
--------------------------------------------------------------------------------