├── .gitignore ├── app ├── views │ ├── articles │ │ ├── edit.html │ │ ├── new.html │ │ ├── index.html │ │ ├── index.mobile.html │ │ ├── article.html │ │ ├── show.html │ │ ├── show.mobile.html │ │ └── form.html │ ├── datasets │ │ ├── edit.html │ │ ├── new.html │ │ ├── index.mobile.html │ │ ├── dataset.html │ │ ├── show.mobile.html │ │ ├── show.html │ │ ├── index.html │ │ └── form.html │ ├── includes │ │ ├── footer.html │ │ ├── messages.html │ │ ├── head.html │ │ ├── header.html │ │ └── foot.html │ ├── 404.html │ ├── users │ │ ├── show.html │ │ ├── auth.html │ │ ├── login.html │ │ └── signup.html │ ├── 500.html │ ├── comments │ │ ├── form.html │ │ └── comment.html │ └── layouts │ │ ├── default.html │ │ └── mobile.html ├── mailer │ ├── templates │ │ └── comment.html │ └── index.js ├── controllers │ ├── tags.js │ ├── users.js │ └── datasets.js └── models │ ├── user.js │ ├── article.js │ └── dataset.js ├── Procfile ├── nodemon.json ├── .DS_Store ├── public ├── img │ ├── facebook.png │ ├── github.png │ ├── google.png │ ├── linkedin.png │ ├── twitter.png │ ├── glyphicons-halflings.png │ └── glyphicons-halflings-white.png ├── js │ ├── app.js │ ├── jquery.tagsinput.min.js │ └── bootstrap.min.js └── css │ ├── jquery.tagsinput.css │ ├── app.css │ └── bootstrap-responsive.min.css ├── scripts ├── README.md └── insert.js ├── config ├── env │ ├── env.json │ ├── test.js │ ├── production.js │ └── development.js ├── imager.js ├── config.js ├── passport │ ├── local.js │ ├── twitter.js │ ├── github.js │ ├── google.js │ ├── facebook.js │ └── linkedin.js ├── passport.js ├── middlewares │ └── authorization.js ├── express.js └── routes.js ├── test ├── helper.js ├── test-users.js └── test-articles.js ├── LICENSE.txt ├── server.js ├── package.json ├── lib └── utils.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | -------------------------------------------------------------------------------- /app/views/articles/edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'form.html' %} 2 | -------------------------------------------------------------------------------- /app/views/articles/new.html: -------------------------------------------------------------------------------- 1 | {% extends 'form.html' %} 2 | -------------------------------------------------------------------------------- /app/views/datasets/edit.html: -------------------------------------------------------------------------------- 1 | {% extends 'form.html' %} 2 | -------------------------------------------------------------------------------- /app/views/datasets/new.html: -------------------------------------------------------------------------------- 1 | {% extends 'form.html' %} 2 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: ./node_modules/.bin/forever -m 5 server.js 2 | -------------------------------------------------------------------------------- /app/views/includes/footer.html: -------------------------------------------------------------------------------- 1 | {# copyright and footer links #} 2 | -------------------------------------------------------------------------------- /nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "execMap": { 3 | "js": "node --harmony" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriswhong/ReallySimpleOpenData/HEAD/.DS_Store -------------------------------------------------------------------------------- /public/img/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriswhong/ReallySimpleOpenData/HEAD/public/img/facebook.png -------------------------------------------------------------------------------- /public/img/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriswhong/ReallySimpleOpenData/HEAD/public/img/github.png -------------------------------------------------------------------------------- /public/img/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriswhong/ReallySimpleOpenData/HEAD/public/img/google.png -------------------------------------------------------------------------------- /public/img/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriswhong/ReallySimpleOpenData/HEAD/public/img/linkedin.png -------------------------------------------------------------------------------- /public/img/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriswhong/ReallySimpleOpenData/HEAD/public/img/twitter.png -------------------------------------------------------------------------------- /public/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriswhong/ReallySimpleOpenData/HEAD/public/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /app/views/404.html: -------------------------------------------------------------------------------- 1 | datdata{% extends 'layouts/default.html' %} 2 | 3 | {% block main %} 4 |

404 - Not found

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /public/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chriswhong/ReallySimpleOpenData/HEAD/public/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /public/js/app.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | 3 | $('#tags').tagsInput({ 4 | 'height':'60px', 5 | 'width':'280px' 6 | }); 7 | 8 | }); 9 | -------------------------------------------------------------------------------- /app/views/users/show.html: -------------------------------------------------------------------------------- 1 | {% extends '../layouts/default.html' %} 2 | 3 | {% block main %} 4 |

{{ user.name || user.username }}

5 | {% endblock %} 6 | -------------------------------------------------------------------------------- /app/mailer/templates/comment.html: -------------------------------------------------------------------------------- 1 |

Hello {{ to }}

2 | 3 |

{{ from }} has added a comment "{{ body }}" on your article {{ article }}

4 | 5 |

Cheers

6 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | ##insert.js 2 | Populates the db with data.json from data.baltimorecity.gov 3 | 4 | - Make sure you have a valid user in the db 5 | - define `userObjectId` as the ObjectId of the user 6 | - run with `NODE_PATH` environment variable like `NODE_PATH=../config:../app/controllers node insert.js` -------------------------------------------------------------------------------- /app/views/500.html: -------------------------------------------------------------------------------- 1 | {% extends 'layouts/default.html' %} 2 | 3 | {% block main %} 4 |

500 - Oops! Internal server error occured

5 | {% endblock %} 6 | 7 | {% block content %} 8 | {% if (env == 'development') %} 9 |
10 |       {{ error }}
11 |     
12 | {% endif %} 13 | {% endblock %} 14 | -------------------------------------------------------------------------------- /config/env/env.json: -------------------------------------------------------------------------------- 1 | { 2 | "FACEBOOK_CLIENTID": "ID", 3 | "FACEBOOK_SECRET": "SECRET", 4 | "TWITTER_CLIENTID": "ID", 5 | "TWITTER_SECRET": "SECRET", 6 | "GITHUB_CLIENTID": "ID", 7 | "GITHUB_SECRET": "SECRET", 8 | "LINKEDIN_CLIENTID": "ID", 9 | "LINKEDIN_SECRET": "SECRET", 10 | "GOOGLE_CLIENTID": "ID", 11 | "GOOGLE_SECRET": "SECRET" 12 | } 13 | -------------------------------------------------------------------------------- /app/views/comments/form.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 | 7 |
8 | -------------------------------------------------------------------------------- /test/helper.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose') 7 | , async = require('async') 8 | , Article = mongoose.model('Article') 9 | , User = mongoose.model('User') 10 | 11 | /** 12 | * Clear database 13 | * 14 | * @param {Function} done 15 | * @api public 16 | */ 17 | 18 | exports.clearDb = function (done) { 19 | async.parallel([ 20 | function (cb) { 21 | User.collection.remove(cb) 22 | }, 23 | function (cb) { 24 | Article.collection.remove(cb) 25 | } 26 | ], done) 27 | } 28 | -------------------------------------------------------------------------------- /app/views/layouts/default.html: -------------------------------------------------------------------------------- 1 | {% include '../includes/head.html' %} 2 | 3 | 4 | {% include '../includes/header.html' %} 5 |
6 | 9 |
10 | {% include '../includes/messages.html' %} 11 |
12 |
13 | {% block content %}{% endblock %} 14 |
15 |
16 | {% include '../includes/footer.html' %} 17 | {% include '../includes/foot.html' %} 18 | 19 | 20 | -------------------------------------------------------------------------------- /config/imager.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Expose 4 | */ 5 | 6 | module.exports = { 7 | variants: { 8 | article: { 9 | resize: { 10 | detail: 'x440' 11 | }, 12 | crop: { 13 | 14 | }, 15 | resizeAndCrop: { 16 | mini: { resize: '63504@', crop: '252x210' } 17 | } 18 | }, 19 | 20 | gallery: { 21 | crop: { 22 | thumb: '100x100' 23 | } 24 | } 25 | }, 26 | 27 | storage: { 28 | S3: { 29 | key: process.env.IMAGER_S3_KEY, 30 | secret: process.env.IMAGER_S3_SECRET, 31 | bucket: process.env.IMAGER_S3_BUCKET 32 | } 33 | }, 34 | 35 | debug: true 36 | } 37 | -------------------------------------------------------------------------------- /app/views/layouts/mobile.html: -------------------------------------------------------------------------------- 1 | {% include '../includes/head.html' %} 2 | 3 | 4 | {% include '../includes/header.html' %} 5 |
6 | {# Customize this layout for mobile device resolutions #} 7 | 10 |
11 | {% include '../includes/messages.html' %} 12 |
13 |
14 | {% block content %}{% endblock %} 15 |
16 |
17 | {% include '../includes/footer.html' %} 18 | {% include '../includes/foot.html' %} 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/views/articles/index.html: -------------------------------------------------------------------------------- 1 | {% extends '../layouts/default.html' %} 2 | 3 | {% block main %} 4 |

{{ title }}

5 | {% endblock %} 6 | 7 | {% block content %} 8 | {% if (articles.length) %} 9 | {% for article in articles %} 10 | {% include 'article.html' %} 11 | {% endfor %} 12 | 13 | {% if (pages > 1) %} 14 | 19 | {% endif %} 20 | {% else %} 21 |

22 | No articles.  23 | create one 24 |

25 | {% endif %} 26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /app/views/articles/index.mobile.html: -------------------------------------------------------------------------------- 1 | {% extends '../layouts/mobile.html' %} 2 | {# More mobile customizations if needed #} 3 | 4 | {% block main %} 5 |

{{ title }}

6 | {% endblock %} 7 | 8 | {% block content %} 9 | {% if (articles.length) %} 10 | {% for article in articles %} 11 | {% include 'article.html' %} 12 | {% endfor %} 13 | 14 | {% if (pages > 1) %} 15 | 20 | {% endif %} 21 | {% else %} 22 |

23 | No articles.  24 | create one 25 |

26 | {% endif %} 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /app/views/datasets/index.mobile.html: -------------------------------------------------------------------------------- 1 | {% extends '../layouts/mobile.html' %} 2 | {# More mobile customizations if needed #} 3 | 4 | {% block main %} 5 |

{{ title }}

6 | {% endblock %} 7 | 8 | {% block content %} 9 | {% if (articles.length) %} 10 | {% for article in articles %} 11 | {% include 'article.html' %} 12 | {% endfor %} 13 | 14 | {% if (pages > 1) %} 15 | 20 | {% endif %} 21 | {% else %} 22 |

23 | No articles.  24 | create one 25 |

26 | {% endif %} 27 | {% endblock %} 28 | -------------------------------------------------------------------------------- /app/views/users/auth.html: -------------------------------------------------------------------------------- 1 | {% extends '../layouts/default.html' %} 2 | 3 | {% block main %} 4 |

{{ title }}

5 | {% endblock %} 6 | 7 | {% block content %} 8 | 9 | 10 | facebook auth 11 | 12 | 13 | github auth 14 | 15 | 16 | twitter auth 17 | 18 | 19 | google auth 20 | 21 | 22 | linkedin auth 23 | 24 | 25 |
26 |

OR

27 |
28 | 29 |
30 | {% block auth %}{% endblock %} 31 |
32 | {% endblock %} 33 | -------------------------------------------------------------------------------- /config/config.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var path = require('path'); 7 | var extend = require('util')._extend; 8 | 9 | var development = require('./env/development'); 10 | var test = require('./env/test'); 11 | var production = require('./env/production'); 12 | 13 | var notifier = { 14 | service: 'postmark', 15 | APN: false, 16 | email: true, // true 17 | actions: ['comment'], 18 | tplPath: path.join(__dirname, '..', 'app/mailer/templates'), 19 | key: 'POSTMARK_KEY' 20 | }; 21 | 22 | var defaults = { 23 | root: path.join(__dirname, '..'), 24 | notifier: notifier 25 | }; 26 | 27 | /** 28 | * Expose 29 | */ 30 | 31 | module.exports = { 32 | development: extend(development, defaults), 33 | test: extend(test, defaults), 34 | production: extend(production, defaults) 35 | }[process.env.NODE_ENV || 'development']; 36 | -------------------------------------------------------------------------------- /app/views/articles/article.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | {{ article.title }} 5 | 6 |

7 |

{{ article.body }}

8 | 9 | {{ article.createdAt.toISOString()|date('M d, Y h:i a') }} 10 | 11 | {% if (article.user) %} 12 |   -   13 | Author: 14 | 15 | {{ article.user.name || article.user.username }} 16 | 17 | {% endif %} 18 | 19 | {% if (article.tags) %} 20 |   -   21 | Tags: 22 | {% for tag in article.tags.split(',') %} 23 |   24 | {{ tag }} 25 |    26 | {% endfor %} 27 | {% endif %} 28 |
29 | -------------------------------------------------------------------------------- /app/controllers/tags.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var mongoose = require('mongoose'); 6 | var Article = mongoose.model('Article'); 7 | 8 | /** 9 | * List items tagged with a tag 10 | */ 11 | 12 | exports.index = function (req, res) { 13 | var criteria = { tags: req.params.tag }; 14 | var perPage = 5; 15 | var page = (req.params.page > 0 ? req.params.page : 1) - 1; 16 | var options = { 17 | perPage: perPage, 18 | page: page, 19 | criteria: criteria 20 | }; 21 | 22 | Article.list(options, function(err, articles) { 23 | if (err) return res.render('500'); 24 | Article.count(criteria).exec(function (err, count) { 25 | res.render('articles/index', { 26 | title: 'Articles tagged ' + req.params.tag, 27 | articles: articles, 28 | page: page + 1, 29 | pages: Math.ceil(count / perPage) 30 | }); 31 | }); 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /app/views/datasets/dataset.html: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 | {{ dataset.title }} 5 | 6 |

7 |

{{ dataset.description }}

8 | 9 | 10 | 11 | 12 | 13 | 14 | 21 | 22 | {% if (dataset.keyword) %} 23 | {% for tag in dataset.keyword.split(',') %} 24 |   25 | {{ tag }} 26 |    27 | {% endfor %} 28 | {% endif %} 29 |
30 | -------------------------------------------------------------------------------- /public/css/jquery.tagsinput.css: -------------------------------------------------------------------------------- 1 | div.tagsinput { border:1px solid #CCC; background: #FFF; padding:5px; width:300px; height:100px; overflow-y: auto;} 2 | div.tagsinput span.tag { border: 1px solid #a5d24a; -moz-border-radius:2px; -webkit-border-radius:2px; display: block; float: left; padding: 5px; text-decoration:none; background: #cde69c; color: #638421; margin-right: 5px; margin-bottom:5px;font-family: helvetica; font-size:13px;} 3 | div.tagsinput span.tag a { font-weight: bold; color: #82ad2b; text-decoration:none; font-size: 11px; } 4 | div.tagsinput input { width:80px; margin:0px; font-family: helvetica; font-size: 13px; border:1px solid transparent; padding:5px; background: transparent; color: #000; outline:0px; margin-right:5px; margin-bottom:5px; } 5 | div.tagsinput div { display:block; float: left; } 6 | .tags_clear { clear: both; width: 100%; height: 0px; } 7 | .not_valid {background: #FBD8DB !important; color: #90111A !important;} 8 | -------------------------------------------------------------------------------- /config/passport/local.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose'); 7 | var LocalStrategy = require('passport-local').Strategy; 8 | var config = require('config'); 9 | var User = mongoose.model('User'); 10 | 11 | /** 12 | * Expose 13 | */ 14 | 15 | module.exports = new LocalStrategy({ 16 | usernameField: 'email', 17 | passwordField: 'password' 18 | }, 19 | function(email, password, done) { 20 | var options = { 21 | criteria: { email: email }, 22 | select: 'name username email hashed_password salt' 23 | }; 24 | User.load(options, function (err, user) { 25 | if (err) return done(err) 26 | if (!user) { 27 | return done(null, false, { message: 'Unknown user' }); 28 | } 29 | if (!user.authenticate(password)) { 30 | return done(null, false, { message: 'Invalid password' }); 31 | } 32 | return done(null, user); 33 | }); 34 | } 35 | ); 36 | -------------------------------------------------------------------------------- /app/views/comments/comment.html: -------------------------------------------------------------------------------- 1 | 2 | {% if (comment && comment.user) %} 3 |
4 | {% if (comment.user.name) %} 5 | {% set name = comment.user.name %} 6 | {% else %} 7 | {% set name = comment.user.username %} 8 | {% endif %} 9 | 10 |

11 | {{ name }} 12 | :  13 | {{ comment.body }} 14 |

15 | 16 | 17 |
18 | 19 | {{ comment.createdAt.toISOString()|date('M d, Y h:i a') }} 20 | 21 | 22 |
23 | 24 |
25 | {% endif %} 26 | -------------------------------------------------------------------------------- /config/env/test.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Expose 4 | */ 5 | 6 | module.exports = { 7 | db: 'mongodb://localhost/noobjs_test', 8 | facebook: { 9 | clientID: process.env.FACEBOOK_CLIENTID, 10 | clientSecret: process.env.FACEBOOK_SECRET, 11 | callbackURL: "http://localhost:3000/auth/facebook/callback" 12 | }, 13 | twitter: { 14 | clientID: process.env.TWITTER_CLIENTID, 15 | clientSecret: process.env.TWITTER_SECRET, 16 | callbackURL: "http://localhost:3000/auth/twitter/callback" 17 | }, 18 | github: { 19 | clientID: process.env.GITHUB_CLIENTID, 20 | clientSecret: process.env.GITHUB_SECRET, 21 | callbackURL: 'http://localhost:3000/auth/github/callback' 22 | }, 23 | linkedin: { 24 | clientID: process.env.LINKEDIN_CLIENTID, 25 | clientSecret: process.env.LINKEDIN_SECRET, 26 | callbackURL: 'http://localhost:3000/auth/linkedin/callback' 27 | }, 28 | google: { 29 | clientID: process.env.GOOGLE_CLIENTID, 30 | clientSecret: process.env.GOOGLE_SECRET, 31 | callbackURL: "http://localhost:3000/auth/google/callback" 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose'); 7 | var LocalStrategy = require('passport-local').Strategy; 8 | var User = mongoose.model('User'); 9 | 10 | var local = require('./passport/local'); 11 | var google = require('./passport/google'); 12 | var facebook = require('./passport/facebook'); 13 | var twitter = require('./passport/twitter'); 14 | var linkedin = require('./passport/linkedin'); 15 | var github = require('./passport/github'); 16 | 17 | /** 18 | * Expose 19 | */ 20 | 21 | module.exports = function (passport) { 22 | // serialize sessions 23 | passport.serializeUser(function(user, done) { 24 | done(null, user.id) 25 | }) 26 | 27 | passport.deserializeUser(function(id, done) { 28 | User.load({ criteria: { _id: id } }, function (err, user) { 29 | done(err, user) 30 | }) 31 | }) 32 | 33 | // use these strategies 34 | passport.use(local); 35 | passport.use(google); 36 | passport.use(facebook); 37 | passport.use(twitter); 38 | passport.use(linkedin); 39 | passport.use(github); 40 | }; 41 | -------------------------------------------------------------------------------- /config/env/production.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Expose 4 | */ 5 | 6 | module.exports = { 7 | db: process.env.MONGOHQ_URL, 8 | facebook: { 9 | clientID: process.env.FACEBOOK_CLIENTID, 10 | clientSecret: process.env.FACEBOOK_SECRET, 11 | callbackURL: "http://nodejs-express-demo.herokuapp.com/auth/facebook/callback" 12 | }, 13 | twitter: { 14 | clientID: process.env.TWITTER_CLIENTID, 15 | clientSecret: process.env.TWITTER_SECRET, 16 | callbackURL: "http://nodejs-express-demo.herokuapp.com/auth/twitter/callback" 17 | }, 18 | github: { 19 | clientID: process.env.GITHUB_CLIENTID, 20 | clientSecret: process.env.GITHUB_SECRET, 21 | callbackURL: 'http://nodejs-express-demo.herokuapp.com/auth/github/callback' 22 | }, 23 | linkedin: { 24 | clientID: process.env.LINKEDIN_CLIENTID, 25 | clientSecret: process.env.LINKEDIN_SECRET, 26 | callbackURL: 'http://nodejs-express-demo.herokuapp.com/auth/linkedin/callback' 27 | }, 28 | google: { 29 | clientID: process.env.GOOGLE_CLIENTID, 30 | clientSecret: process.env.GOOGLE_SECRET, 31 | callbackURL: "http://nodejs-express-demo.herokuapp.com/auth/google/callback" 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Madhusudhan Srinivasa 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/passport/twitter.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose'); 7 | var TwitterStrategy = require('passport-twitter').Strategy; 8 | var config = require('config'); 9 | var User = mongoose.model('User'); 10 | 11 | /** 12 | * Expose 13 | */ 14 | 15 | module.exports = new TwitterStrategy({ 16 | consumerKey: config.twitter.clientID, 17 | consumerSecret: config.twitter.clientSecret, 18 | callbackURL: config.twitter.callbackURL 19 | }, 20 | function(accessToken, refreshToken, profile, done) { 21 | var options = { 22 | criteria: { 'twitter.id': profile.id } 23 | }; 24 | User.load(options, function (err, user) { 25 | if (err) return done(err); 26 | if (!user) { 27 | user = new User({ 28 | name: profile.displayName, 29 | username: profile.username, 30 | provider: 'twitter', 31 | twitter: profile._json 32 | }); 33 | user.save(function (err) { 34 | if (err) console.log(err); 35 | return done(err, user); 36 | }); 37 | } else { 38 | return done(err, user); 39 | } 40 | }); 41 | } 42 | ); 43 | -------------------------------------------------------------------------------- /app/views/users/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'auth.html' %} 2 | 3 | {% block auth %} 4 |
5 | 6 | 7 |

8 | {{ message }} 9 |

10 | 11 |
12 | 13 |
14 | 15 |
16 |
17 | 18 |
19 | 20 |
21 | 22 |
23 |
24 | 25 |
26 |
27 | 28 |   or   29 | 30 |
31 |
32 |
33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /config/passport/github.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose'); 7 | var GithubStrategy = require('passport-github').Strategy; 8 | var config = require('config'); 9 | var User = mongoose.model('User'); 10 | 11 | /** 12 | * Expose 13 | */ 14 | 15 | module.exports = new GithubStrategy({ 16 | clientID: config.github.clientID, 17 | clientSecret: config.github.clientSecret, 18 | callbackURL: config.github.callbackURL 19 | }, 20 | function(accessToken, refreshToken, profile, done) { 21 | var options = { 22 | criteria: { 'github.id': profile.id } 23 | }; 24 | User.load(options, function (err, user) { 25 | if (err) return done(err); 26 | if (!user) { 27 | user = new User({ 28 | name: profile.displayName, 29 | email: profile.emails[0].value, 30 | username: profile.username, 31 | provider: 'github', 32 | github: profile._json 33 | }); 34 | user.save(function (err) { 35 | if (err) console.log(err); 36 | return done(err, user); 37 | }); 38 | } else { 39 | return done(err, user); 40 | } 41 | }); 42 | } 43 | ); 44 | -------------------------------------------------------------------------------- /config/passport/google.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose'); 7 | var GoogleStrategy = require('passport-google-oauth').OAuth2Strategy; 8 | var config = require('config'); 9 | var User = mongoose.model('User'); 10 | 11 | /** 12 | * Expose 13 | */ 14 | 15 | module.exports = new GoogleStrategy({ 16 | clientID: config.google.clientID, 17 | clientSecret: config.google.clientSecret, 18 | callbackURL: config.google.callbackURL 19 | }, 20 | function(accessToken, refreshToken, profile, done) { 21 | var options = { 22 | criteria: { 'google.id': profile.id } 23 | }; 24 | User.load(options, function (err, user) { 25 | if (err) return done(err); 26 | if (!user) { 27 | user = new User({ 28 | name: profile.displayName, 29 | email: profile.emails[0].value, 30 | username: profile.username, 31 | provider: 'google', 32 | google: profile._json 33 | }); 34 | user.save(function (err) { 35 | if (err) console.log(err); 36 | return done(err, user); 37 | }); 38 | } else { 39 | return done(err, user); 40 | } 41 | }); 42 | } 43 | ); 44 | -------------------------------------------------------------------------------- /config/passport/facebook.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose'); 7 | var FacebookStrategy = require('passport-facebook').Strategy; 8 | var config = require('config'); 9 | var User = mongoose.model('User'); 10 | 11 | /** 12 | * Expose 13 | */ 14 | 15 | module.exports = new FacebookStrategy({ 16 | clientID: config.facebook.clientID, 17 | clientSecret: config.facebook.clientSecret, 18 | callbackURL: config.facebook.callbackURL 19 | }, 20 | function(accessToken, refreshToken, profile, done) { 21 | var options = { 22 | criteria: { 'facebook.id': profile.id } 23 | }; 24 | User.load(options, function (err, user) { 25 | if (err) return done(err); 26 | if (!user) { 27 | user = new User({ 28 | name: profile.displayName, 29 | email: profile.emails[0].value, 30 | username: profile.username, 31 | provider: 'facebook', 32 | facebook: profile._json 33 | }); 34 | user.save(function (err) { 35 | if (err) console.log(err); 36 | return done(err, user); 37 | }); 38 | } else { 39 | return done(err, user); 40 | } 41 | }); 42 | } 43 | ); 44 | -------------------------------------------------------------------------------- /scripts/insert.js: -------------------------------------------------------------------------------- 1 | //insert data.json from Baltimore city into the database 2 | //run with NODE_PATH environment variable: 3 | //NODE_PATH=../config:../app/controllers node insert.js 4 | 5 | //replace with a userID, this will assign a user to each dataset as it is written to the database 6 | var userObjectId = '5660f876389c8c2e4bc8d651'; 7 | 8 | var mongoose = require('mongoose'); 9 | var config = require('config'); 10 | require('../app/models/dataset.js') 11 | var Dataset = mongoose.model('Dataset'); 12 | var data = require('./baltimore.json') 13 | 14 | // Connect to mongodb 15 | var connect = function () { 16 | var options = { server: { socketOptions: { keepAlive: 1 } } }; 17 | mongoose.connect(config.db, options); 18 | }; 19 | 20 | connect(); 21 | 22 | mongoose.connection.on('error', console.log); 23 | 24 | 25 | 26 | 27 | 28 | data.forEach(function(datasetjson) { 29 | 30 | console.log('Writing dataset ' + datasetjson.title); 31 | datasetjson.user = mongoose.Types.ObjectId(userObjectId); 32 | var dataset = new Dataset(datasetjson); 33 | dataset.uploadAndSave(); 34 | 35 | }) 36 | 37 | 38 | mongoose.disconnect(function(err) { 39 | if (err) throw err; 40 | console.log('Disconnected from mongodb'); 41 | }); 42 | 43 | -------------------------------------------------------------------------------- /app/views/includes/messages.html: -------------------------------------------------------------------------------- 1 | {% if (info && info.length) %} 2 |
3 | 4 | 9 |
10 | {% endif %} 11 | 12 | {% if (errors && errors.length) %} 13 |
14 | 15 | 20 |
21 | {% endif %} 22 | 23 | {% if (success && success.length) %} 24 |
25 | 26 | 31 |
32 | {% endif %} 33 | 34 | {% if (warning && warning.length) %} 35 |
36 | 37 | 42 |
43 | {% endif %} 44 | -------------------------------------------------------------------------------- /config/passport/linkedin.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose'); 7 | var LinkedinStrategy = require('passport-linkedin').Strategy; 8 | var config = require('config'); 9 | var User = mongoose.model('User'); 10 | 11 | /** 12 | * Expose 13 | */ 14 | 15 | module.exports = new LinkedinStrategy({ 16 | consumerKey: config.linkedin.clientID, 17 | consumerSecret: config.linkedin.clientSecret, 18 | callbackURL: config.linkedin.callbackURL, 19 | profileFields: ['id', 'first-name', 'last-name', 'email-address'] 20 | }, 21 | function(accessToken, refreshToken, profile, done) { 22 | var options = { 23 | criteria: { 'linkedin.id': profile.id } 24 | }; 25 | User.load(options, function (err, user) { 26 | if (err) return done(err); 27 | if (!user) { 28 | user = new User({ 29 | name: profile.displayName, 30 | email: profile.emails[0].value, 31 | username: profile.emails[0].value, 32 | provider: 'linkedin', 33 | linkedin: profile._json 34 | }); 35 | user.save(function (err) { 36 | if (err) console.log(err); 37 | return done(err, user); 38 | }); 39 | } else { 40 | return done(err, user); 41 | } 42 | }); 43 | } 44 | ); 45 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * nodejs-express-mongoose-demo 4 | * Copyright(c) 2013 Madhusudhan Srinivasa 5 | * MIT Licensed 6 | */ 7 | /** 8 | * Module dependencies 9 | */ 10 | 11 | var fs = require('fs'); 12 | var join = require('path').join; 13 | var express = require('express'); 14 | var mongoose = require('mongoose'); 15 | var passport = require('passport'); 16 | var config = require('config'); 17 | 18 | var app = express(); 19 | var port = process.env.PORT || 3000; 20 | 21 | // Connect to mongodb 22 | var connect = function () { 23 | var options = { server: { socketOptions: { keepAlive: 1 } } }; 24 | mongoose.connect(config.db, options); 25 | }; 26 | connect(); 27 | 28 | mongoose.connection.on('error', console.log); 29 | mongoose.connection.on('disconnected', connect); 30 | 31 | // Bootstrap models 32 | fs.readdirSync(join(__dirname, 'app/models')).forEach(function (file) { 33 | if (~file.indexOf('.js')) require(join(__dirname, 'app/models', file)); 34 | }); 35 | 36 | // Bootstrap passport config 37 | require('./config/passport')(passport); 38 | 39 | // Bootstrap application settings 40 | require('./config/express')(app, passport); 41 | 42 | // Bootstrap routes 43 | require('./config/routes')(app, passport); 44 | 45 | app.listen(port); 46 | console.log('Express app started on port ' + port); 47 | 48 | /** 49 | * Expose 50 | */ 51 | 52 | module.exports = app; 53 | -------------------------------------------------------------------------------- /config/env/development.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Module dependencies. 4 | */ 5 | 6 | var fs = require('fs'); 7 | var env = {}; 8 | var envFile = require('path').join(__dirname, 'env.json'); 9 | 10 | // Read env.json file, if it exists, load the id's and secrets from that 11 | // Note that this is only in the development env 12 | // it is not safe to store id's in files 13 | 14 | if (fs.existsSync(envFile)) { 15 | env = fs.readFileSync(envFile, 'utf-8'); 16 | env = JSON.parse(env); 17 | Object.keys(env).forEach(function (key) { 18 | process.env[key] = env[key]; 19 | }); 20 | } 21 | 22 | /** 23 | * Expose 24 | */ 25 | 26 | module.exports = { 27 | db: 'mongodb://localhost/rsod_dev', 28 | facebook: { 29 | clientID: process.env.FACEBOOK_CLIENTID, 30 | clientSecret: process.env.FACEBOOK_SECRET, 31 | callbackURL: "http://localhost:3000/auth/facebook/callback" 32 | }, 33 | twitter: { 34 | clientID: process.env.TWITTER_CLIENTID, 35 | clientSecret: process.env.TWITTER_SECRET, 36 | callbackURL: "http://localhost:3000/auth/twitter/callback" 37 | }, 38 | github: { 39 | clientID: process.env.GITHUB_CLIENTID, 40 | clientSecret: process.env.GITHUB_SECRET, 41 | callbackURL: 'http://localhost:3000/auth/github/callback' 42 | }, 43 | linkedin: { 44 | clientID: process.env.LINKEDIN_CLIENTID, 45 | clientSecret: process.env.LINKEDIN_SECRET, 46 | callbackURL: 'http://localhost:3000/auth/linkedin/callback' 47 | }, 48 | google: { 49 | clientID: process.env.GOOGLE_CLIENTID, 50 | clientSecret: process.env.GOOGLE_SECRET, 51 | callbackURL: "http://localhost:3000/auth/google/callback" 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /app/mailer/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose'); 7 | var Notifier = require('notifier'); 8 | var config = require('config'); 9 | 10 | /** 11 | * Process the templates using swig - refer to notifier#processTemplate method 12 | * 13 | * @param {String} tplPath 14 | * @param {Object} locals 15 | * @return {String} 16 | * @api public 17 | */ 18 | 19 | Notifier.prototype.processTemplate = function (tplPath, locals) { 20 | var swig = require('swig'); 21 | locals.filename = tplPath; 22 | return swig.renderFile(tplPath, locals); 23 | }; 24 | 25 | /** 26 | * Expose 27 | */ 28 | 29 | module.exports = { 30 | 31 | /** 32 | * Comment notification 33 | * 34 | * @param {Object} options 35 | * @param {Function} cb 36 | * @api public 37 | */ 38 | 39 | comment: function (options, cb) { 40 | var article = options.article; 41 | var author = article.user; 42 | var user = options.currentUser; 43 | var notifier = new Notifier(config.notifier); 44 | 45 | var obj = { 46 | to: author.email, 47 | from: 'your@product.com', 48 | subject: user.name + ' added a comment on your article ' + article.title, 49 | alert: user.name + ' says: "' + options.comment, 50 | locals: { 51 | to: author.name, 52 | from: user.name, 53 | body: options.comment, 54 | article: article.name 55 | } 56 | }; 57 | 58 | // for apple push notifications 59 | /*notifier.use({ 60 | APN: true 61 | parseChannels: ['USER_' + author._id.toString()] 62 | })*/ 63 | 64 | try { 65 | notifier.send('comment', obj, cb); 66 | } catch (err) { 67 | console.log(err); 68 | } 69 | } 70 | }; 71 | -------------------------------------------------------------------------------- /app/views/users/signup.html: -------------------------------------------------------------------------------- 1 | {% extends 'auth.html' %} 2 | 3 | {% block auth %} 4 |
5 | 6 | 7 |
8 | 9 |
10 | 11 |
12 |
13 | 14 |
15 | 16 |
17 | 18 |
19 |
20 | 21 |
22 | 23 |
24 | 25 |
26 |
27 | 28 |
29 | 30 |
31 | 32 |
33 |
34 | 35 |
36 |
37 | 38 |   or   39 | 40 |
41 |
42 |
43 | {% endblock %} 44 | -------------------------------------------------------------------------------- /app/views/includes/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ title }} | {{ pkg.name }} 9 | 10 | 11 | 12 | {# Opengraph tags #} 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% block head %} 21 | {# Styles #} 22 | 23 | 24 | 25 | 26 | 27 | {% endblock %} 28 | 29 | 30 | {# HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries #} 31 | 35 | -------------------------------------------------------------------------------- /config/middlewares/authorization.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * Generic require login routing middleware 4 | */ 5 | 6 | exports.requiresLogin = function (req, res, next) { 7 | if (req.isAuthenticated()) return next() 8 | if (req.method == 'GET') req.session.returnTo = req.originalUrl 9 | res.redirect('/login') 10 | } 11 | 12 | /* 13 | * User authorization routing middleware 14 | */ 15 | 16 | exports.user = { 17 | hasAuthorization: function (req, res, next) { 18 | if (req.profile.id != req.user.id) { 19 | req.flash('info', 'You are not authorized') 20 | return res.redirect('/users/' + req.profile.id) 21 | } 22 | next() 23 | } 24 | } 25 | 26 | // /* 27 | // * Article authorization routing middleware 28 | // */ 29 | 30 | // exports.article = { 31 | // hasAuthorization: function (req, res, next) { 32 | // if (req.article.user.id != req.user.id) { 33 | // req.flash('info', 'You are not authorized') 34 | // return res.redirect('/articles/' + req.article.id) 35 | // } 36 | // next() 37 | // } 38 | // } 39 | 40 | /* 41 | * Dataset authorization routing middleware 42 | */ 43 | 44 | exports.dataset = { 45 | hasAuthorization: function (req, res, next) { 46 | console.log(req.user,req.dataset) 47 | if (req.dataset.user.id != req.user.id) { 48 | req.flash('info', 'You are not authorized') 49 | return res.redirect('/datasets/' + req.article.id) 50 | } 51 | next() 52 | } 53 | } 54 | 55 | /** 56 | * Comment authorization routing middleware 57 | */ 58 | 59 | exports.comment = { 60 | hasAuthorization: function (req, res, next) { 61 | // if the current user is comment owner or article owner 62 | // give them authority to delete 63 | if (req.user.id === req.comment.user.id || req.user.id === req.article.user.id) { 64 | next() 65 | } else { 66 | req.flash('info', 'You are not authorized') 67 | res.redirect('/articles/' + req.article.id) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-express-mongoose-demo", 3 | "description": "A demo app illustrating the usage of express, mongoose, passportjs, swig and other modules in nodejs", 4 | "keywords": [ 5 | "express", 6 | "mongoose", 7 | "mongodb", 8 | "passport", 9 | "demo" 10 | ], 11 | "version": "4.0.0", 12 | "private": false, 13 | "author": "Madhusudhan Srinivasa (http://madhums.github.com)", 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/madhums/node-express-mongoose-demo.git" 17 | }, 18 | "engines": { 19 | "node": "0.12.6" 20 | }, 21 | "scripts": { 22 | "start": "NODE_PATH=./config:./app/controllers NODE_ENV=development ./node_modules/.bin/nodemon server.js", 23 | "test": "NODE_PATH=./config:./app/controllers NODE_ENV=test ./node_modules/.bin/mocha --reporter spec --timeout 10000 test/test-*.js" 24 | }, 25 | "dependencies": { 26 | "async": "1.3.0", 27 | "body-parser": "1.13.2", 28 | "compression": "1.5.1", 29 | "connect-flash": "0.1.1", 30 | "connect-mongo": "0.8.1", 31 | "cookie-parser": "1.3.5", 32 | "cookie-session": "1.2.0", 33 | "csurf": "1.8.3", 34 | "express": "4.13.1", 35 | "express-session": "1.11.3", 36 | "forever": "0.14.2", 37 | "imager": "0.2.7", 38 | "method-override": "2.3.3", 39 | "mongoose": "4.0.6", 40 | "morgan": "1.6.1", 41 | "multer": "0.1.8", 42 | "notifier": "0.1.7", 43 | "passport": "0.2.2", 44 | "passport-facebook": "2.0.0", 45 | "passport-github": "0.1.5", 46 | "passport-google-oauth": "0.2.0", 47 | "passport-linkedin": "0.1.3", 48 | "passport-local": "1.0.0", 49 | "passport-twitter": "1.0.3", 50 | "swig": "1.4.2", 51 | "view-helpers": "0.1.5", 52 | "winston": "1.0.1" 53 | }, 54 | "devDependencies": { 55 | "supertest": "1.0.1", 56 | "should": "7.0.1", 57 | "mocha": "2.2.5", 58 | "nodemon": "1.3.7" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/views/articles/show.html: -------------------------------------------------------------------------------- 1 | {% extends '../layouts/default.html' %} 2 | 3 | {% block main %} 4 |

{{ article.title }}

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |

{{ article.body }}

11 |
12 | {% if (article.user) %} 13 | Author:   14 | 15 | {{ article.user.name || article.user.username }} 16 | 17 | {% endif %} 18 | {% if (article.tags) %} 19 |

20 | Tags:   21 | {% for tag in article.tags.split(',') %} 22 |   23 | {{ tag }} 24 |    25 | {% endfor %} 26 |

27 | {% endif %} 28 | {{ article.createdAt.toISOString()|date('M d, Y h:i a') }} 29 |
30 |
31 |
32 | {% if (!article.isNew && article.image && article.image.files && article.image.files.length) %} 33 | 34 | {% endif %} 35 |
36 |
37 | 38 |
39 |
40 | 41 | 42 | Edit 43 | 44 |   45 | 46 | 47 |
48 | 49 |
50 |

Comments

51 | {% for comment in article.comments %} 52 | {% include '../comments/comment.html' %} 53 | {% endfor %} 54 | {% include '../comments/form.html' %} 55 | {% endblock %} 56 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Formats mongoose errors into proper array 4 | * 5 | * @param {Array} errors 6 | * @return {Array} 7 | * @api public 8 | */ 9 | 10 | exports.errors = function (errors) { 11 | var keys = Object.keys(errors) 12 | var errs = [] 13 | 14 | // if there is no validation error, just display a generic error 15 | if (!keys) { 16 | return ['Oops! There was an error'] 17 | } 18 | 19 | keys.forEach(function (key) { 20 | if (errors[key]) errs.push(errors[key].message) 21 | }) 22 | 23 | return errs 24 | } 25 | 26 | /** 27 | * Index of object within an array 28 | * 29 | * @param {Array} arr 30 | * @param {Object} obj 31 | * @return {Number} 32 | * @api public 33 | */ 34 | 35 | exports.indexof = function (arr, obj) { 36 | var index = -1; // not found initially 37 | var keys = Object.keys(obj); 38 | // filter the collection with the given criterias 39 | var result = arr.filter(function (doc, idx) { 40 | // keep a counter of matched key/value pairs 41 | var matched = 0; 42 | 43 | // loop over criteria 44 | for (var i = keys.length - 1; i >= 0; i--) { 45 | if (doc[keys[i]] === obj[keys[i]]) { 46 | matched++; 47 | 48 | // check if all the criterias are matched 49 | if (matched === keys.length) { 50 | index = idx; 51 | return idx; 52 | } 53 | } 54 | }; 55 | }); 56 | return index; 57 | } 58 | 59 | /** 60 | * Find object in an array of objects that matches a condition 61 | * 62 | * @param {Array} arr 63 | * @param {Object} obj 64 | * @param {Function} cb - optional 65 | * @return {Object} 66 | * @api public 67 | */ 68 | 69 | exports.findByParam = function (arr, obj, cb) { 70 | var index = exports.indexof(arr, obj) 71 | if (~index && typeof cb === 'function') { 72 | return cb(undefined, arr[index]) 73 | } else if (~index && !cb) { 74 | return arr[index] 75 | } else if (!~index && typeof cb === 'function') { 76 | return cb('not found') 77 | } 78 | // else undefined is returned 79 | } 80 | -------------------------------------------------------------------------------- /app/views/articles/show.mobile.html: -------------------------------------------------------------------------------- 1 | {% extends '../layouts/mobile.html' %} 2 | 3 | {% block main %} 4 |

{{ article.title }}

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |

{{ article.body }}

11 |
12 | {% if (article.user) %} 13 | {% if (article.user.name) %} 14 | {% set name = article.user.name %} 15 | {% else %} 16 | {% set name = article.user.username %} 17 | {% endif %} 18 | 19 | {{ name }} 20 | {% endif %} 21 | {% if (article.tags) %} 22 |

Tags  

23 | {% for tag in article.tags.split(',') %} 24 |   25 | {{ tag }} 26 |    27 | {% endfor %} 28 | {% endif %} 29 | 30 | {{ article.createdAt.toISOString()|date('M d, Y h:i a') }} 31 |
32 |
33 |
34 | {% if (!article.isNew && article.image && article.image.files && article.image.files.length) %} 35 | 36 | {% endif %} 37 |
38 |
39 | 40 |
41 | 42 | 43 | Edit 44 | 45 |   46 | 47 | 48 |
49 | 50 |
51 |

Comments

52 | {% for comment in article.comments %} 53 | {% include '../comments/comment.html' %} 54 | {% endfor %} 55 | {% include '../comments/form.html' %} 56 | {% endblock %} 57 | -------------------------------------------------------------------------------- /app/views/datasets/show.mobile.html: -------------------------------------------------------------------------------- 1 | {% extends '../layouts/mobile.html' %} 2 | 3 | {% block main %} 4 |

{{ article.title }}

5 | {% endblock %} 6 | 7 | {% block content %} 8 |
9 |
10 |

{{ article.body }}

11 |
12 | {% if (article.user) %} 13 | {% if (article.user.name) %} 14 | {% set name = article.user.name %} 15 | {% else %} 16 | {% set name = article.user.username %} 17 | {% endif %} 18 | 19 | {{ name }} 20 | {% endif %} 21 | {% if (article.tags) %} 22 |

Tags  

23 | {% for tag in article.tags.split(',') %} 24 |   25 | {{ tag }} 26 |    27 | {% endfor %} 28 | {% endif %} 29 | 30 | {{ article.createdAt.toISOString()|date('M d, Y h:i a') }} 31 |
32 |
33 |
34 | {% if (!article.isNew && article.image && article.image.files && article.image.files.length) %} 35 | 36 | {% endif %} 37 |
38 |
39 | 40 |
41 | 42 | 43 | Edit 44 | 45 |   46 | 47 | 48 |
49 | 50 |
51 |

Comments

52 | {% for comment in article.comments %} 53 | {% include '../comments/comment.html' %} 54 | {% endfor %} 55 | {% include '../comments/form.html' %} 56 | {% endblock %} 57 | -------------------------------------------------------------------------------- /app/controllers/users.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose'); 7 | var User = mongoose.model('User'); 8 | var utils = require('../../lib/utils'); 9 | 10 | /** 11 | * Load 12 | */ 13 | 14 | exports.load = function (req, res, next, id) { 15 | var options = { 16 | criteria: { _id : id } 17 | }; 18 | User.load(options, function (err, user) { 19 | if (err) return next(err); 20 | if (!user) return next(new Error('Failed to load User ' + id)); 21 | req.profile = user; 22 | next(); 23 | }); 24 | }; 25 | 26 | /** 27 | * Create user 28 | */ 29 | 30 | exports.create = function (req, res) { 31 | var user = new User(req.body); 32 | user.provider = 'local'; 33 | user.save(function (err) { 34 | if (err) { 35 | return res.render('users/signup', { 36 | errors: utils.errors(err.errors), 37 | user: user, 38 | title: 'Sign up' 39 | }); 40 | } 41 | 42 | // manually login the user once successfully signed up 43 | req.logIn(user, function(err) { 44 | if (err) req.flash('info', 'Sorry! We are not able to log you in!'); 45 | return res.redirect('/'); 46 | }); 47 | }); 48 | }; 49 | 50 | /** 51 | * Show profile 52 | */ 53 | 54 | exports.show = function (req, res) { 55 | var user = req.profile; 56 | res.render('users/show', { 57 | title: user.name, 58 | user: user 59 | }); 60 | }; 61 | 62 | exports.signin = function (req, res) {}; 63 | 64 | /** 65 | * Auth callback 66 | */ 67 | 68 | exports.authCallback = login; 69 | 70 | /** 71 | * Show login form 72 | */ 73 | 74 | exports.login = function (req, res) { 75 | res.render('users/login', { 76 | title: 'Login' 77 | }); 78 | }; 79 | 80 | /** 81 | * Show sign up form 82 | */ 83 | 84 | exports.signup = function (req, res) { 85 | res.render('users/signup', { 86 | title: 'Sign up', 87 | user: new User() 88 | }); 89 | }; 90 | 91 | /** 92 | * Logout 93 | */ 94 | 95 | exports.logout = function (req, res) { 96 | req.logout(); 97 | res.redirect('/login'); 98 | }; 99 | 100 | /** 101 | * Session 102 | */ 103 | 104 | exports.session = login; 105 | 106 | /** 107 | * Login 108 | */ 109 | 110 | function login (req, res) { 111 | var redirectTo = req.session.returnTo ? req.session.returnTo : '/'; 112 | delete req.session.returnTo; 113 | res.redirect(redirectTo); 114 | }; 115 | -------------------------------------------------------------------------------- /app/views/includes/header.html: -------------------------------------------------------------------------------- 1 | 46 | -------------------------------------------------------------------------------- /app/views/articles/form.html: -------------------------------------------------------------------------------- 1 | {% extends '../layouts/default.html' %} 2 | 3 | 4 | {% block content %} 5 | {% if article.isNew %} 6 | {% set action = '/articles' %} 7 | {% else %} 8 | {% set action = '/articles/' + article._id %} 9 | {% endif %} 10 | 11 |
12 |
13 |
14 | 15 | 16 | 17 | {% if not article.isNew %} 18 | 19 | {% endif %} 20 | 21 |
22 | 23 |
24 | 25 |
26 |
27 | 28 |
29 | 30 |
31 | 32 |
33 |
34 | 35 |
36 | 37 |
38 | 39 |
40 |
41 | 42 |
43 | 44 |
45 | 46 |
47 |
48 | 49 |
50 |
51 | 52 |   53 | Cancel 54 |
55 |
56 |
57 |
58 |
59 | {% if (!article.isNew && article.image && article.image.files && article.image.files.length) %} 60 | 61 | {% endif %} 62 |
63 |
64 | {% endblock %} 65 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ReallySimpleOpenData (RSOD) 2 | A no-frills open data portal built with node, express,and mongodb. 3 | 4 | ## Key Tenets 5 | - You don't need expensive or complicated software to have an Open Data Portal 6 | - "Bells and Whistles" on Open Data Portals tend to get in the way of raw data access 7 | - Discoverability first, download second, utility *somewhere else* 8 | - Link to the data "where it lies". No need to host it all in the same place... host individual datasets where they are most useful and easiest to access 9 | - RSOD is just a searchable metadata catalog. That's it. No mapping, charting, data APIs, databases, user accounts, etc... 10 | - Designed around the [data.json catalog standard](https://project-open-data.cio.gov/catalog/#machine-readable-format). Think of this app as a data.json server. 11 | - Developed by Civic Hackers and Open Data Enthusiasts who want to give data publishers another open source option for publishing an Open Data catalog 12 | 13 | ## Vision 14 | People who want to create a searchable, standards-compliant open data catalog should be able to get started in just a few minutes for free. 15 | 16 | ##Data 17 | - All data are stored *elsewhere*. The user can paste in a link and choose the correct resource type. 18 | - One idea is to automate the use of third party services. (put in your Amazon S3 key and RSOD will retain it and give you a UI for uploading files when you create a dataset. Same for CartoDB, Github, generic FTP server, etc. RSOD can act as broker for the upload, but never hosts the data itself.) 19 | 20 | ## Project Needs 21 | - Node developers 22 | - UI/UX Help 23 | - Design Help 24 | 25 | ## Try it out - No guarantees at this point, it's very much in-progress 26 | - Clone this repo `git clone https://github.com/chriswhong/ReallySimpleOpenData.git` 27 | - Start MongoDB (`mongod` on a Mac) 28 | - Install dependencies `npm install` 29 | - Run reallysimpleopendata `npm start` 30 | - Open `http://localhost:3000` in your browser 31 | - Create a new user account and start adding datasets 32 | 33 | ###To populate with dummy data 34 | 35 | - read `scripts/README.md` for configuration details on how to execute insert.js to populate the DB with datasets from Baltimore 36 | 37 | 38 | ## License 39 | - This work is licensed under a [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/). 40 | 41 | ##Attribution 42 | Latest incarnation is largely based on [https://github.com/madhums/node-express-mongoose-demo](https://github.com/madhums/node-express-mongoose-demo) 43 | 44 | Catalog UI based on CKAN 45 | -------------------------------------------------------------------------------- /app/views/includes/foot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 93 | 94 | {% block foot %}{% endblock %} 95 | -------------------------------------------------------------------------------- /app/views/datasets/show.html: -------------------------------------------------------------------------------- 1 | {% extends '../layouts/default.html' %} 2 | 3 | 4 | {% block content %} 5 | 8 |
9 |
10 |
11 |
12 |
13 | {% if (req.isAuthenticated()) %} 14 | 15 | Manage 16 | 17 | {% endif %} 18 |

{{ dataset.title }}

19 |

{{ dataset.description }}

20 |
21 |
22 |

Data and Resources

23 | {% for resource in dataset.distribution %} 24 |
25 |
26 | File ({{ resource.mediaType }}) 27 |
28 |
29 | 30 |
31 |
32 | {% endfor %} 33 |
34 |
35 |
36 |
37 |
    38 | {% for tag in dataset.keyword %} 39 |
  • {{tag}}
  • 40 | {% endfor %} 41 |
42 |
43 | 44 |
45 | 46 | 47 |
48 |
49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 |
Created{{dataset.issued|date('F jS, Y')}}
Modified{{dataset.modified|date('F jS, Y')}}
Publisher{{dataset.publisher.name}}
Point of Contact{{dataset.contactPoint.fn}}: {{dataset.contactPoint.hasEmail}}
70 |
71 | 72 |
73 |
74 |
75 | 76 | 77 | 87 | 94 | {% endblock %} 95 | -------------------------------------------------------------------------------- /test/test-users.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose') 7 | , should = require('should') 8 | , request = require('supertest') 9 | , app = require('../server') 10 | , context = describe 11 | , User = mongoose.model('User') 12 | 13 | var cookies, count 14 | 15 | /** 16 | * Users tests 17 | */ 18 | 19 | describe('Users', function () { 20 | describe('POST /users', function () { 21 | describe('Invalid parameters', function () { 22 | before(function (done) { 23 | User.count(function (err, cnt) { 24 | count = cnt 25 | done() 26 | }) 27 | }) 28 | 29 | it('no email - should respond with errors', function (done) { 30 | request(app) 31 | .post('/users') 32 | .field('name', 'Foo bar') 33 | .field('username', 'foobar') 34 | .field('email', '') 35 | .field('password', 'foobar') 36 | .expect('Content-Type', /html/) 37 | .expect(200) 38 | // .expect(/Email cannot be blank/) 39 | .end(done) 40 | }) 41 | 42 | it('no name - should respond with errors', function (done) { 43 | request(app) 44 | .post('/users') 45 | .field('name', '') 46 | .field('username', 'foobar') 47 | .field('email', 'foobar@example.com') 48 | .field('password', 'foobar') 49 | .expect('Content-Type', /html/) 50 | .expect(200) 51 | // .expect(/Name cannot be blank/) 52 | .end(done) 53 | }) 54 | 55 | it('should not save the user to the database', function (done) { 56 | User.count(function (err, cnt) { 57 | count.should.equal(cnt) 58 | done() 59 | }) 60 | }) 61 | }) 62 | 63 | describe('Valid parameters', function () { 64 | before(function (done) { 65 | User.count(function (err, cnt) { 66 | count = cnt 67 | done() 68 | }) 69 | }) 70 | 71 | it('should redirect to /articles', function (done) { 72 | request(app) 73 | .post('/users') 74 | .field('name', 'Foo bar') 75 | .field('username', 'foobar') 76 | .field('email', 'foobar@example.com') 77 | .field('password', 'foobar') 78 | .expect('Content-Type', /plain/) 79 | .expect('Location', /\//) 80 | .expect(302) 81 | .expect(/Moved Temporarily/) 82 | .end(done) 83 | }) 84 | 85 | it('should insert a record to the database', function (done) { 86 | User.count(function (err, cnt) { 87 | cnt.should.equal(count + 1) 88 | done() 89 | }) 90 | }) 91 | 92 | it('should save the user to the database', function (done) { 93 | User.findOne({ username: 'foobar' }).exec(function (err, user) { 94 | should.not.exist(err) 95 | user.should.be.an.instanceOf(User) 96 | user.email.should.equal('foobar@example.com') 97 | done() 98 | }) 99 | }) 100 | }) 101 | }) 102 | 103 | after(function (done) { 104 | require('./helper').clearDb(done) 105 | }) 106 | }) 107 | -------------------------------------------------------------------------------- /config/express.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var express = require('express'); 7 | var session = require('express-session'); 8 | var compression = require('compression'); 9 | var morgan = require('morgan'); 10 | var cookieParser = require('cookie-parser'); 11 | var cookieSession = require('cookie-session'); 12 | var bodyParser = require('body-parser'); 13 | var methodOverride = require('method-override'); 14 | var csrf = require('csurf'); 15 | var multer = require('multer'); 16 | var swig = require('swig'); 17 | 18 | var mongoStore = require('connect-mongo')(session); 19 | var flash = require('connect-flash'); 20 | var winston = require('winston'); 21 | var helpers = require('view-helpers'); 22 | var config = require('config'); 23 | var pkg = require('../package.json'); 24 | 25 | var env = process.env.NODE_ENV || 'development'; 26 | 27 | /** 28 | * Expose 29 | */ 30 | 31 | module.exports = function (app, passport) { 32 | 33 | // Compression middleware (should be placed before express.static) 34 | app.use(compression({ 35 | threshold: 512 36 | })); 37 | 38 | // Static files middleware 39 | app.use(express.static(config.root + '/public')); 40 | 41 | // Use winston on production 42 | var log; 43 | if (env !== 'development') { 44 | log = { 45 | stream: { 46 | write: function (message, encoding) { 47 | winston.info(message); 48 | } 49 | } 50 | }; 51 | } else { 52 | log = 'dev'; 53 | } 54 | 55 | // Don't log during tests 56 | // Logging middleware 57 | if (env !== 'test') app.use(morgan(log)); 58 | 59 | // Swig templating engine settings 60 | if (env === 'development' || env === 'test') { 61 | swig.setDefaults({ 62 | cache: false 63 | }); 64 | } 65 | 66 | // set views path, template engine and default layout 67 | app.engine('html', swig.renderFile); 68 | app.set('views', config.root + '/app/views'); 69 | app.set('view engine', 'html'); 70 | 71 | // expose package.json to views 72 | app.use(function (req, res, next) { 73 | res.locals.pkg = pkg; 74 | res.locals.env = env; 75 | next(); 76 | }); 77 | 78 | // bodyParser should be above methodOverride 79 | app.use(bodyParser.json()); 80 | app.use(bodyParser.urlencoded({ extended: true })); 81 | app.use(multer()); 82 | app.use(methodOverride(function (req, res) { 83 | if (req.body && typeof req.body === 'object' && '_method' in req.body) { 84 | // look in urlencoded POST bodies and delete it 85 | var method = req.body._method; 86 | delete req.body._method; 87 | return method; 88 | } 89 | })); 90 | 91 | // CookieParser should be above session 92 | app.use(cookieParser()); 93 | app.use(cookieSession({ secret: 'secret' })); 94 | app.use(session({ 95 | resave: true, 96 | saveUninitialized: true, 97 | secret: pkg.name, 98 | store: new mongoStore({ 99 | url: config.db, 100 | collection : 'sessions' 101 | }) 102 | })); 103 | 104 | // use passport session 105 | app.use(passport.initialize()); 106 | app.use(passport.session()); 107 | 108 | // connect flash for flash messages - should be declared after sessions 109 | app.use(flash()); 110 | 111 | // should be declared after session and flash 112 | app.use(helpers(pkg.name)); 113 | 114 | // adds CSRF support 115 | if (process.env.NODE_ENV !== 'test') { 116 | app.use(csrf()); 117 | 118 | // This could be moved to view-helpers :-) 119 | app.use(function (req, res, next) { 120 | res.locals.csrf_token = req.csrfToken(); 121 | next(); 122 | }); 123 | } 124 | }; 125 | -------------------------------------------------------------------------------- /app/views/datasets/index.html: -------------------------------------------------------------------------------- 1 | {% extends '../layouts/default.html' %} 2 | 3 | 6 | 7 | {% block content %} 8 | 11 |
12 | 13 |
14 |
15 | 16 |
    17 |
  • 18 | Tags 19 |
  • 20 | {% for tagCount in tagCounts %} 21 |
  • 22 | {{tagCount.count}} 23 | {{tagCount._id}} 24 |
  • 25 | {% endfor %} 26 |
27 | 28 |
29 |
30 | 31 | {% if (req.isAuthenticated()) %} 32 |
33 | 34 |
35 |
 
36 | {% endif %} 37 | 38 |
39 |
40 |
41 |
42 | 43 | 44 |
45 |
46 |
47 |
48 |
 
49 | 50 |

{{count}} datasets found {% if (q.length) %}for "{{q}}"{% endif %}

51 |
52 |
53 | 54 |
55 | 56 | 57 | 63 | 64 | 65 |
66 | 67 |
68 |
69 |
70 |
71 |
72 |
73 | {% if (tags) %} 74 | {% for tag in tags %} 75 |
{{tag}} X
76 | {% endfor %} 77 | {% endif %} 78 |
79 |
80 | 81 |
82 |
 
83 | {% if (datasets.length) %} 84 | {% for dataset in datasets %} 85 | {% include 'dataset.html' %} 86 | {% endfor %} 87 | 88 | {% if (pages > 1) %} 89 |
    90 | {% autoescape false %} 91 | {{ createPagination(pages, page) }} 92 | {% endautoescape %} 93 |
94 | {% endif %} 95 | {% else %} 96 |

97 | No datasets.  98 | create one 99 |

100 | {% endif %} 101 | 102 | 103 |
104 |
105 |
106 | 107 | {% endblock %} 108 | -------------------------------------------------------------------------------- /app/controllers/datasets.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose') 7 | var Dataset = mongoose.model('Dataset') 8 | var utils = require('../../lib/utils') 9 | var extend = require('util')._extend 10 | 11 | /** 12 | * Load 13 | */ 14 | 15 | exports.load = function (req, res, next, id){ 16 | var User = mongoose.model('User'); 17 | 18 | Dataset.load(id, function (err, dataset) { 19 | if (err) return next(err); 20 | if (!dataset) return next(new Error('not found')); 21 | req.dataset = dataset; 22 | next(); 23 | }); 24 | }; 25 | 26 | /** 27 | * List 28 | */ 29 | 30 | exports.index = function (req, res){ 31 | console.log('tags',req.query.tags); 32 | var page = (req.query.page > 0 ? req.query.page : 1) - 1; 33 | var perPage = 30; 34 | var q = req.query.q; 35 | var sort = req.query.sort; 36 | var tags = req.query.tags; 37 | if (typeof tags == 'string') { 38 | tags = [tags]; 39 | } 40 | var options = { 41 | perPage: perPage, 42 | page: page, 43 | criteria: q, 44 | sort: sort, 45 | tags: tags 46 | }; 47 | console.log(options); 48 | 49 | //set up group by 50 | var agg = [ 51 | {$unwind: "$keyword" }, 52 | {$group: { 53 | _id: "$keyword", 54 | count: {$sum: 1} 55 | }}, 56 | {$sort: { count: -1 } }, 57 | {$limit: 15} 58 | ]; 59 | 60 | 61 | 62 | Dataset.list(options, function (err, datasets) { 63 | // console.log('List found ',datasets.length) 64 | if (err) return res.render('500'); 65 | Dataset.count1(options, function (err, count) { 66 | 67 | 68 | Dataset.aggregate(agg, function(err, tagCounts){ 69 | console.log(tags); 70 | res.render('datasets/index', { 71 | datasets: datasets, 72 | page: page + 1, 73 | pages: Math.ceil(count / perPage), 74 | count: count, 75 | q: q, 76 | tags: tags, 77 | tagCounts: tagCounts, 78 | sort: sort 79 | }); 80 | }); 81 | 82 | }); 83 | }); 84 | }; 85 | 86 | /** 87 | * New dataset 88 | */ 89 | 90 | exports.new = function (req, res){ 91 | res.render('datasets/new', { 92 | title: 'New Dataset', 93 | dataset: new Dataset({}) 94 | }); 95 | }; 96 | 97 | /** 98 | * Create a dataset 99 | */ 100 | 101 | exports.create = function (req, res) { 102 | var dataset = new Dataset(req.body); 103 | 104 | dataset.user = req.user; 105 | dataset.uploadAndSave(function (err) { 106 | if (!err) { 107 | req.flash('success', 'Successfully created dataset!'); 108 | return res.redirect('/datasets/'+dataset._id); 109 | } 110 | res.render('datasets/new', { 111 | title: 'New Dataset', 112 | dataset: dataset, 113 | errors: utils.errors(err.errors || err) 114 | }); 115 | }); 116 | }; 117 | 118 | /** 119 | * Edit a dataset 120 | */ 121 | 122 | exports.edit = function (req, res) { 123 | res.render('datasets/edit', { 124 | dataset: req.dataset 125 | }); 126 | }; 127 | 128 | /** 129 | * Update dataset 130 | */ 131 | 132 | exports.update = function (req, res){ 133 | var dataset = req.dataset; 134 | 135 | console.log(req.body); 136 | 137 | // make sure no one changes the user 138 | delete req.body.user; 139 | dataset = extend(dataset, req.body); 140 | 141 | dataset.uploadAndSave(function (err) { 142 | if (!err) { 143 | return res.redirect('/datasets/' + dataset._id); 144 | } 145 | 146 | res.render('datasets/edit', { 147 | title: 'Edit Dataset', 148 | dataset: dataset, 149 | errors: utils.errors(err.errors || err) 150 | }); 151 | }); 152 | }; 153 | 154 | /** 155 | * Show 156 | */ 157 | 158 | exports.show = function (req, res){ 159 | res.render('datasets/show', { 160 | title: req.dataset.title, 161 | dataset: req.dataset 162 | }); 163 | }; 164 | 165 | /** 166 | * Delete a dataset 167 | */ 168 | 169 | exports.destroy = function (req, res){ 170 | var dataset = req.dataset; 171 | dataset.remove(function (err){ 172 | req.flash('info', 'Deleted successfully'); 173 | res.redirect('/datasets'); 174 | }); 175 | }; 176 | -------------------------------------------------------------------------------- /app/models/user.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose'); 7 | var crypto = require('crypto'); 8 | 9 | var Schema = mongoose.Schema; 10 | var oAuthTypes = [ 11 | 'github', 12 | 'twitter', 13 | 'facebook', 14 | 'google', 15 | 'linkedin' 16 | ]; 17 | 18 | /** 19 | * User Schema 20 | */ 21 | 22 | var UserSchema = new Schema({ 23 | name: { type: String, default: '' }, 24 | email: { type: String, default: '' }, 25 | username: { type: String, default: '' }, 26 | provider: { type: String, default: '' }, 27 | hashed_password: { type: String, default: '' }, 28 | salt: { type: String, default: '' }, 29 | authToken: { type: String, default: '' }, 30 | facebook: {}, 31 | twitter: {}, 32 | github: {}, 33 | google: {}, 34 | linkedin: {} 35 | }); 36 | 37 | /** 38 | * Virtuals 39 | */ 40 | 41 | UserSchema 42 | .virtual('password') 43 | .set(function(password) { 44 | this._password = password; 45 | this.salt = this.makeSalt(); 46 | this.hashed_password = this.encryptPassword(password); 47 | }) 48 | .get(function() { return this._password }); 49 | 50 | /** 51 | * Validations 52 | */ 53 | 54 | var validatePresenceOf = function (value) { 55 | return value && value.length; 56 | }; 57 | 58 | // the below 5 validations only apply if you are signing up traditionally 59 | 60 | UserSchema.path('name').validate(function (name) { 61 | if (this.skipValidation()) return true; 62 | return name.length; 63 | }, 'Name cannot be blank'); 64 | 65 | UserSchema.path('email').validate(function (email) { 66 | if (this.skipValidation()) return true; 67 | return email.length; 68 | }, 'Email cannot be blank'); 69 | 70 | UserSchema.path('email').validate(function (email, fn) { 71 | var User = mongoose.model('User'); 72 | if (this.skipValidation()) fn(true); 73 | 74 | // Check only when it is a new user or when email field is modified 75 | if (this.isNew || this.isModified('email')) { 76 | User.find({ email: email }).exec(function (err, users) { 77 | fn(!err && users.length === 0); 78 | }); 79 | } else fn(true); 80 | }, 'Email already exists'); 81 | 82 | UserSchema.path('username').validate(function (username) { 83 | if (this.skipValidation()) return true; 84 | return username.length; 85 | }, 'Username cannot be blank'); 86 | 87 | UserSchema.path('hashed_password').validate(function (hashed_password) { 88 | if (this.skipValidation()) return true; 89 | return hashed_password.length && this._password.length; 90 | }, 'Password cannot be blank'); 91 | 92 | 93 | /** 94 | * Pre-save hook 95 | */ 96 | 97 | UserSchema.pre('save', function(next) { 98 | if (!this.isNew) return next(); 99 | 100 | if (!validatePresenceOf(this.password) && !this.skipValidation()) { 101 | next(new Error('Invalid password')); 102 | } else { 103 | next(); 104 | } 105 | }) 106 | 107 | /** 108 | * Methods 109 | */ 110 | 111 | UserSchema.methods = { 112 | 113 | /** 114 | * Authenticate - check if the passwords are the same 115 | * 116 | * @param {String} plainText 117 | * @return {Boolean} 118 | * @api public 119 | */ 120 | 121 | authenticate: function (plainText) { 122 | return this.encryptPassword(plainText) === this.hashed_password; 123 | }, 124 | 125 | /** 126 | * Make salt 127 | * 128 | * @return {String} 129 | * @api public 130 | */ 131 | 132 | makeSalt: function () { 133 | return Math.round((new Date().valueOf() * Math.random())) + ''; 134 | }, 135 | 136 | /** 137 | * Encrypt password 138 | * 139 | * @param {String} password 140 | * @return {String} 141 | * @api public 142 | */ 143 | 144 | encryptPassword: function (password) { 145 | if (!password) return ''; 146 | try { 147 | return crypto 148 | .createHmac('sha1', this.salt) 149 | .update(password) 150 | .digest('hex'); 151 | } catch (err) { 152 | return ''; 153 | } 154 | }, 155 | 156 | /** 157 | * Validation is not required if using OAuth 158 | */ 159 | 160 | skipValidation: function() { 161 | return ~oAuthTypes.indexOf(this.provider); 162 | } 163 | }; 164 | 165 | /** 166 | * Statics 167 | */ 168 | 169 | UserSchema.statics = { 170 | 171 | /** 172 | * Load 173 | * 174 | * @param {Object} options 175 | * @param {Function} cb 176 | * @api private 177 | */ 178 | 179 | load: function (options, cb) { 180 | options.select = options.select || 'name username'; 181 | this.findOne(options.criteria) 182 | .select(options.select) 183 | .exec(cb); 184 | } 185 | } 186 | 187 | mongoose.model('User', UserSchema); 188 | -------------------------------------------------------------------------------- /app/models/article.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose'); 7 | var Imager = require('imager'); 8 | var config = require('config'); 9 | 10 | var imagerConfig = require(config.root + '/config/imager.js'); 11 | var utils = require('../../lib/utils'); 12 | 13 | var Schema = mongoose.Schema; 14 | 15 | /** 16 | * Getters 17 | */ 18 | 19 | var getTags = function (tags) { 20 | return tags.join(','); 21 | }; 22 | 23 | /** 24 | * Setters 25 | */ 26 | 27 | var setTags = function (tags) { 28 | return tags.split(','); 29 | }; 30 | 31 | /** 32 | * Article Schema 33 | */ 34 | 35 | var ArticleSchema = new Schema({ 36 | title: {type : String, default : '', trim : true}, 37 | body: {type : String, default : '', trim : true}, 38 | user: {type : Schema.ObjectId, ref : 'User'}, 39 | comments: [{ 40 | body: { type : String, default : '' }, 41 | user: { type : Schema.ObjectId, ref : 'User' }, 42 | createdAt: { type : Date, default : Date.now } 43 | }], 44 | tags: {type: [], get: getTags, set: setTags}, 45 | image: { 46 | cdnUri: String, 47 | files: [] 48 | }, 49 | createdAt : {type : Date, default : Date.now} 50 | }); 51 | 52 | /** 53 | * Validations 54 | */ 55 | 56 | ArticleSchema.path('title').required(true, 'Article title cannot be blank'); 57 | ArticleSchema.path('body').required(true, 'Article body cannot be blank'); 58 | 59 | /** 60 | * Pre-remove hook 61 | */ 62 | 63 | ArticleSchema.pre('remove', function (next) { 64 | var imager = new Imager(imagerConfig, 'S3'); 65 | var files = this.image.files; 66 | 67 | // if there are files associated with the item, remove from the cloud too 68 | imager.remove(files, function (err) { 69 | if (err) return next(err); 70 | }, 'article'); 71 | 72 | next(); 73 | }); 74 | 75 | /** 76 | * Methods 77 | */ 78 | 79 | ArticleSchema.methods = { 80 | 81 | /** 82 | * Save article and upload image 83 | * 84 | * @param {Object} images 85 | * @param {Function} cb 86 | * @api private 87 | */ 88 | 89 | uploadAndSave: function (images, cb) { 90 | if (!images || !images.length) return this.save(cb) 91 | 92 | var imager = new Imager(imagerConfig, 'S3'); 93 | var self = this; 94 | 95 | this.validate(function (err) { 96 | if (err) return cb(err); 97 | imager.upload(images, function (err, cdnUri, files) { 98 | if (err) return cb(err); 99 | if (files.length) { 100 | self.image = { cdnUri : cdnUri, files : files }; 101 | } 102 | self.save(cb); 103 | }, 'article'); 104 | }); 105 | }, 106 | 107 | /** 108 | * Add comment 109 | * 110 | * @param {User} user 111 | * @param {Object} comment 112 | * @param {Function} cb 113 | * @api private 114 | */ 115 | 116 | addComment: function (user, comment, cb) { 117 | var notify = require('../mailer'); 118 | 119 | this.comments.push({ 120 | body: comment.body, 121 | user: user._id 122 | }); 123 | 124 | if (!this.user.email) this.user.email = 'email@product.com'; 125 | notify.comment({ 126 | article: this, 127 | currentUser: user, 128 | comment: comment.body 129 | }); 130 | 131 | this.save(cb); 132 | }, 133 | 134 | /** 135 | * Remove comment 136 | * 137 | * @param {commentId} String 138 | * @param {Function} cb 139 | * @api private 140 | */ 141 | 142 | removeComment: function (commentId, cb) { 143 | var index = utils.indexof(this.comments, { id: commentId }); 144 | if (~index) this.comments.splice(index, 1); 145 | else return cb('not found'); 146 | this.save(cb); 147 | } 148 | } 149 | 150 | /** 151 | * Statics 152 | */ 153 | 154 | ArticleSchema.statics = { 155 | 156 | /** 157 | * Find article by id 158 | * 159 | * @param {ObjectId} id 160 | * @param {Function} cb 161 | * @api private 162 | */ 163 | 164 | load: function (id, cb) { 165 | this.findOne({ _id : id }) 166 | .populate('user', 'name email username') 167 | .populate('comments.user') 168 | .exec(cb); 169 | }, 170 | 171 | /** 172 | * List articles 173 | * 174 | * @param {Object} options 175 | * @param {Function} cb 176 | * @api private 177 | */ 178 | 179 | list: function (options, cb) { 180 | var criteria = options.criteria || {} 181 | 182 | this.find(criteria) 183 | .populate('user', 'name username') 184 | .sort({'createdAt': -1}) // sort by date 185 | .limit(options.perPage) 186 | .skip(options.perPage * options.page) 187 | .exec(cb); 188 | } 189 | } 190 | 191 | mongoose.model('Article', ArticleSchema); 192 | -------------------------------------------------------------------------------- /app/models/dataset.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose'); 7 | var Imager = require('imager'); 8 | var config = require('config'); 9 | 10 | var imagerConfig = require(config.root + '/config/imager.js'); 11 | var utils = require('../../lib/utils'); 12 | 13 | var Schema = mongoose.Schema; 14 | 15 | /** 16 | * Getters 17 | */ 18 | 19 | var getTags = function (tags) { 20 | return tags.join(','); 21 | }; 22 | 23 | /** 24 | * Setters 25 | */ 26 | 27 | var setTags = function (tags) { 28 | return tags.split(','); 29 | }; 30 | 31 | /** 32 | * Dataset Schema 33 | */ 34 | 35 | var DatasetSchema = new Schema({ 36 | user: {type : Schema.ObjectId, ref : 'User'}, 37 | title: {type: String, default : '', trim : true, index: true}, 38 | description: {type: String, default : '', trim : true, index: true}, 39 | //keyword: {type: [], set: setTags}, 40 | keyword: [{type: String, default : '', trim : true}], 41 | modified: {type : Date, default : Date.now}, 42 | publisher: { 43 | name: {type: String, default : '', trim : true} 44 | }, 45 | contactPoint: { 46 | fn: {type: String, default : '', trim : true}, 47 | hasEmail: {type: String, default : '', trim : true} 48 | }, 49 | identifier: {type: String, default : '', trim : true}, 50 | accessLevel: {type: String, default : 'public', trim : true}, 51 | landingPage: {type: String, default : '', trim : true}, 52 | issued: {type : Date, default : Date.now}, 53 | distribution: [{ 54 | name: {type: String, default : '', trim : true}, 55 | description: {type: String, default : '', trim : true}, 56 | downloadURL: {type: String, default : '', trim : true}, 57 | accessURL: {type: String, default : '', trim : true}, 58 | mediaType: {type: String, default : '', trim : true} 59 | }], 60 | theme: [{type: String, default : '', trim : true}] 61 | }); 62 | 63 | /** 64 | * Validations 65 | */ 66 | 67 | DatasetSchema.path('title').required(true, 'Title cannot be blank'); 68 | DatasetSchema.path('description').required(true, 'Description cannot be blank'); 69 | 70 | 71 | /** 72 | * Methods 73 | */ 74 | 75 | DatasetSchema.methods = { 76 | 77 | /** 78 | * Save article and upload image 79 | * 80 | * @param {Object} images 81 | * @param {Function} cb 82 | * @api private 83 | */ 84 | 85 | uploadAndSave: function (cb) { 86 | return this.save(cb) 87 | } 88 | 89 | } 90 | 91 | /** 92 | * Statics 93 | */ 94 | 95 | DatasetSchema.statics = { 96 | 97 | /** 98 | * Find dataset by id 99 | * 100 | * @param {ObjectId} id 101 | * @param {Function} cb 102 | * @api private 103 | */ 104 | 105 | load: function (id, cb) { 106 | this.findOne({ _id : id }) 107 | .populate('user', 'name email username') 108 | //.populate('comments.user') 109 | .exec(cb); 110 | }, 111 | 112 | /** 113 | * List datasets 114 | * 115 | * @param {Object} options 116 | * @param {Function} cb 117 | * @api private 118 | */ 119 | 120 | list: function (options, cb) { 121 | var criteria = options.criteria || {}; 122 | var tags = options.tags; 123 | 124 | console.log(criteria); 125 | var regex = new RegExp(criteria, 'i'); 126 | var regex1 = new RegExp(tags,'i'); 127 | console.log(regex); 128 | var query = this.find(); 129 | 130 | if(tags) { 131 | query.where('keyword').in(tags) 132 | } 133 | 134 | query 135 | .or([{ 'title': { $regex: regex }}, { 'description': { $regex: regex }},{ 'keyword': { $regex: regex }}]) 136 | .populate('user', 'name username') 137 | .sort(parseSort(options.sort)) // sort by date 138 | .limit(options.perPage) 139 | .skip(options.perPage * options.page) 140 | .exec(cb); 141 | 142 | function parseSort(sort) { 143 | 144 | switch(sort) { 145 | case 'titleAsc': 146 | return {'title':1} 147 | break; 148 | case 'titleDesc': 149 | return {'title':-1} 150 | break; 151 | case 'lastModified': 152 | return {'modified':-1} 153 | break; 154 | 155 | default: 156 | return {'title':1} 157 | } 158 | 159 | return sort; 160 | } 161 | }, 162 | 163 | count1: function (options, cb) { 164 | var criteria = options.criteria || {}; 165 | var tags = options.tags; 166 | 167 | console.log(criteria); 168 | var regex = new RegExp(criteria, 'i'); 169 | var regex1 = new RegExp(tags,'i'); 170 | console.log(regex); 171 | var query = this.count(); 172 | 173 | if(tags) { 174 | query.where('keyword').in(tags) 175 | } 176 | query.or([{ 'title': { $regex: regex }}, { 'description': { $regex: regex }},{ 'keyword': { $regex: regex }}]) 177 | .exec(cb); 178 | } 179 | } 180 | 181 | mongoose.model('Dataset', DatasetSchema); 182 | -------------------------------------------------------------------------------- /config/routes.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * Module dependencies. 4 | */ 5 | 6 | // Note: We can require users, articles and other cotrollers because we have 7 | // set the NODE_PATH to be ./app/controllers (package.json # scripts # start) 8 | 9 | var users = require('users'); 10 | var datasets = require('datasets'); 11 | var tags = require('tags'); 12 | var auth = require('./middlewares/authorization'); 13 | 14 | /** 15 | * Route middlewares 16 | */ 17 | 18 | var datasetAuth = [auth.requiresLogin, auth.dataset.hasAuthorization]; 19 | //var articleAuth = [auth.requiresLogin, auth.article.hasAuthorization]; 20 | var commentAuth = [auth.requiresLogin, auth.comment.hasAuthorization]; 21 | 22 | /** 23 | * Expose routes 24 | */ 25 | 26 | module.exports = function (app, passport) { 27 | 28 | // user routes 29 | app.get('/login', users.login); 30 | app.get('/signup', users.signup); 31 | app.get('/logout', users.logout); 32 | app.post('/users', users.create); 33 | app.post('/users/session', 34 | passport.authenticate('local', { 35 | failureRedirect: '/login', 36 | failureFlash: 'Invalid email or password.' 37 | }), users.session); 38 | app.get('/users/:userId', users.show); 39 | app.get('/auth/facebook', 40 | passport.authenticate('facebook', { 41 | scope: [ 'email', 'user_about_me'], 42 | failureRedirect: '/login' 43 | }), users.signin); 44 | app.get('/auth/facebook/callback', 45 | passport.authenticate('facebook', { 46 | failureRedirect: '/login' 47 | }), users.authCallback); 48 | app.get('/auth/github', 49 | passport.authenticate('github', { 50 | failureRedirect: '/login' 51 | }), users.signin); 52 | app.get('/auth/github/callback', 53 | passport.authenticate('github', { 54 | failureRedirect: '/login' 55 | }), users.authCallback); 56 | app.get('/auth/twitter', 57 | passport.authenticate('twitter', { 58 | failureRedirect: '/login' 59 | }), users.signin); 60 | app.get('/auth/twitter/callback', 61 | passport.authenticate('twitter', { 62 | failureRedirect: '/login' 63 | }), users.authCallback); 64 | app.get('/auth/google', 65 | passport.authenticate('google', { 66 | failureRedirect: '/login', 67 | scope: [ 68 | 'https://www.googleapis.com/auth/userinfo.profile', 69 | 'https://www.googleapis.com/auth/userinfo.email' 70 | ] 71 | }), users.signin); 72 | app.get('/auth/google/callback', 73 | passport.authenticate('google', { 74 | failureRedirect: '/login' 75 | }), users.authCallback); 76 | app.get('/auth/linkedin', 77 | passport.authenticate('linkedin', { 78 | failureRedirect: '/login', 79 | scope: [ 80 | 'r_emailaddress' 81 | ] 82 | }), users.signin); 83 | app.get('/auth/linkedin/callback', 84 | passport.authenticate('linkedin', { 85 | failureRedirect: '/login' 86 | }), users.authCallback); 87 | 88 | app.param('userId', users.load); 89 | 90 | // article routes 91 | // app.param('id', articles.load); 92 | // app.get('/articles', articles.index); 93 | // app.get('/articles/new', auth.requiresLogin, articles.new); 94 | // app.post('/articles', auth.requiresLogin, articles.create); 95 | // app.get('/articles/:id', articles.show); 96 | // app.get('/articles/:id/edit', articleAuth, articles.edit); 97 | // app.put('/articles/:id', articleAuth, articles.update); 98 | // app.delete('/articles/:id', articleAuth, articles.destroy); 99 | 100 | // dataset routes 101 | app.param('id', datasets.load); 102 | app.get('/datasets', datasets.index); 103 | app.get('/datasets/new', auth.requiresLogin, datasets.new); 104 | app.post('/datasets', auth.requiresLogin, datasets.create); 105 | app.get('/datasets/:id', datasets.show); 106 | app.get('/datasets/:id/edit', datasetAuth, datasets.edit); 107 | app.put('/datasets/:id', datasetAuth, datasets.update); 108 | app.delete('/datasets/:id', datasetAuth, datasets.destroy); 109 | 110 | // home route 111 | app.get('/', datasets.index); 112 | 113 | // // comment routes 114 | // app.param('commentId', comments.load); 115 | // app.post('/articles/:id/comments', auth.requiresLogin, comments.create); 116 | // app.get('/articles/:id/comments', auth.requiresLogin, comments.create); 117 | // app.delete('/articles/:id/comments/:commentId', commentAuth, comments.destroy); 118 | 119 | // tag routes 120 | app.get('/tags/:tag', tags.index); 121 | 122 | 123 | /** 124 | * Error handling 125 | */ 126 | 127 | app.use(function (err, req, res, next) { 128 | // treat as 404 129 | if (err.message 130 | && (~err.message.indexOf('not found') 131 | || (~err.message.indexOf('Cast to ObjectId failed')))) { 132 | return next(); 133 | } 134 | console.error(err.stack); 135 | // error page 136 | res.status(500).render('500', { error: err.stack }); 137 | }); 138 | 139 | // assume 404 since no middleware responded 140 | app.use(function (req, res, next) { 141 | res.status(404).render('404', { 142 | url: req.originalUrl, 143 | error: 'Not found' 144 | }); 145 | }); 146 | } 147 | -------------------------------------------------------------------------------- /test/test-articles.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose') 7 | , should = require('should') 8 | , request = require('supertest') 9 | , app = require('../server') 10 | , context = describe 11 | , User = mongoose.model('User') 12 | , Article = mongoose.model('Article') 13 | , agent = request.agent(app) 14 | 15 | var count 16 | 17 | /** 18 | * Articles tests 19 | */ 20 | 21 | describe('Articles', function () { 22 | before(function (done) { 23 | // create a user 24 | var user = new User({ 25 | email: 'foobar@example.com', 26 | name: 'Foo bar', 27 | username: 'foobar', 28 | password: 'foobar' 29 | }) 30 | user.save(done) 31 | }) 32 | 33 | describe('GET /articles', function () { 34 | it('should respond with Content-Type text/html', function (done) { 35 | agent 36 | .get('/articles') 37 | .expect('Content-Type', /html/) 38 | .expect(200) 39 | .expect(/Articles/) 40 | .end(done) 41 | }) 42 | }) 43 | 44 | describe('GET /articles/new', function () { 45 | context('When not logged in', function () { 46 | it('should redirect to /login', function (done) { 47 | agent 48 | .get('/articles/new') 49 | .expect('Content-Type', /plain/) 50 | .expect(302) 51 | .expect('Location', '/login') 52 | .expect(/Moved Temporarily/) 53 | .end(done) 54 | }) 55 | }) 56 | 57 | context('When logged in', function () { 58 | before(function (done) { 59 | // login the user 60 | agent 61 | .post('/users/session') 62 | .field('email', 'foobar@example.com') 63 | .field('password', 'foobar') 64 | .end(function (err) { 65 | done() 66 | }) 67 | }) 68 | 69 | it('should respond with Content-Type text/html', function (done) { 70 | agent 71 | .get('/articles/new') 72 | .expect('Content-Type', /html/) 73 | .expect(200) 74 | .expect(/New Article/) 75 | .end(done) 76 | }) 77 | }) 78 | }) 79 | 80 | describe('POST /articles', function () { 81 | context('When not logged in', function () { 82 | it('should redirect to /login', function (done) { 83 | request(app) 84 | .get('/articles/new') 85 | .expect('Content-Type', /plain/) 86 | .expect(302) 87 | .expect('Location', '/login') 88 | .expect(/Moved Temporarily/) 89 | .end(done) 90 | }) 91 | }) 92 | 93 | context('When logged in', function () { 94 | before(function (done) { 95 | // login the user 96 | agent 97 | .post('/users/session') 98 | .field('email', 'foobar@example.com') 99 | .field('password', 'foobar') 100 | .end(function () { 101 | done(); 102 | }) 103 | }) 104 | 105 | describe('Invalid parameters', function () { 106 | before(function (done) { 107 | Article.count(function (err, cnt) { 108 | count = cnt 109 | done() 110 | }) 111 | }) 112 | 113 | it('should respond with error', function (done) { 114 | agent 115 | .post('/articles') 116 | .field('title', '') 117 | .field('body', 'foo') 118 | .expect('Content-Type', /html/) 119 | .expect(200) 120 | .expect(/Article title cannot be blank/) 121 | .end(done) 122 | }) 123 | 124 | it('should not save to the database', function (done) { 125 | Article.count(function (err, cnt) { 126 | count.should.equal(cnt) 127 | done() 128 | }) 129 | }) 130 | }) 131 | 132 | describe('Valid parameters', function () { 133 | before(function (done) { 134 | Article.count(function (err, cnt) { 135 | count = cnt 136 | done() 137 | }) 138 | }) 139 | 140 | it('should redirect to the new article page', function (done) { 141 | agent 142 | .post('/articles') 143 | .field('title', 'foo') 144 | .field('body', 'bar') 145 | .expect('Content-Type', /plain/) 146 | .expect('Location', /\/articles\//) 147 | .expect(302) 148 | .expect(/Moved Temporarily/) 149 | .end(done) 150 | }) 151 | 152 | it('should insert a record to the database', function (done) { 153 | Article.count(function (err, cnt) { 154 | cnt.should.equal(count + 1) 155 | done() 156 | }) 157 | }) 158 | 159 | it('should save the article to the database', function (done) { 160 | Article 161 | .findOne({ title: 'foo'}) 162 | .populate('user') 163 | .exec(function (err, article) { 164 | should.not.exist(err) 165 | article.should.be.an.instanceOf(Article) 166 | article.title.should.equal('foo') 167 | article.body.should.equal('bar') 168 | article.user.email.should.equal('foobar@example.com') 169 | article.user.name.should.equal('Foo bar') 170 | done() 171 | }) 172 | }) 173 | }) 174 | }) 175 | }) 176 | 177 | after(function (done) { 178 | require('./helper').clearDb(done) 179 | }) 180 | }) 181 | -------------------------------------------------------------------------------- /public/js/jquery.tagsinput.min.js: -------------------------------------------------------------------------------- 1 | (function(a){var b=new Array;var c=new Array;a.fn.doAutosize=function(b){var c=a(this).data("minwidth"),d=a(this).data("maxwidth"),e="",f=a(this),g=a("#"+a(this).data("tester_id"));if(e===(e=f.val())){return}var h=e.replace(/&/g,"&").replace(/\s/g," ").replace(//g,">");g.html(h);var i=g.width(),j=i+b.comfortZone>=c?i+b.comfortZone:c,k=f.width(),l=j=c||j>c&&j").css({position:"absolute",top:-9999,left:-9999,width:"auto",fontSize:f.css("fontSize"),fontFamily:f.css("fontFamily"),fontWeight:f.css("fontWeight"),letterSpacing:f.css("letterSpacing"),whiteSpace:"nowrap"}),h=a(this).attr("id")+"_autosize_tester";if(!a("#"+h).length>0){g.attr("id",h);g.appendTo("body")}f.data("minwidth",c);f.data("maxwidth",d);f.data("tester_id",h);f.css("width",c)};a.fn.addTag=function(d,e){e=jQuery.extend({focus:false,callback:true},e);this.each(function(){var f=a(this).attr("id");var g=a(this).val().split(b[f]);if(g[0]==""){g=new Array}d=jQuery.trim(d);if(e.unique){var h=a(g).tagExist(d);if(h==true){a("#"+f+"_tag").addClass("not_valid")}}else{var h=false}if(d!=""&&h!=true){a("").addClass("tag").append(a("").text(d).append(" "),a("",{href:"#",title:"Removing tag",text:"x"}).click(function(){return a("#"+f).removeTag(escape(d))})).insertBefore("#"+f+"_addTag");g.push(d);a("#"+f+"_tag").val("");if(e.focus){a("#"+f+"_tag").focus()}else{a("#"+f+"_tag").blur()}a.fn.tagsInput.updateTagsField(this,g);if(e.callback&&c[f]&&c[f]["onAddTag"]){var i=c[f]["onAddTag"];i.call(this,d)}if(c[f]&&c[f]["onChange"]){var j=g.length;var i=c[f]["onChange"];i.call(this,a(this),g[j-1])}}});return false};a.fn.removeTag=function(d){d=unescape(d);this.each(function(){var e=a(this).attr("id");var f=a(this).val().split(b[e]);a("#"+e+"_tagsinput .tag").remove();str="";for(i=0;i=0};a.fn.importTags=function(b){id=a(this).attr("id");a("#"+id+"_tagsinput .tag").remove();a.fn.tagsInput.importTags(this,b)};a.fn.tagsInput=function(d){var e=jQuery.extend({interactive:true,defaultText:"add a tag",minChars:0,width:"300px",height:"100px",autocomplete:{selectFirst:false},hide:true,delimiter:",",unique:true,removeWithBackspace:true,placeholderColor:"#666666",autosize:true,comfortZone:20,inputPadding:6*2},d);this.each(function(){if(e.hide){a(this).hide()}var d=a(this).attr("id");if(!d||b[a(this).attr("id")]){d=a(this).attr("id","tags"+(new Date).getTime()).attr("id")}var f=jQuery.extend({pid:d,real_input:"#"+d,holder:"#"+d+"_tagsinput",input_wrapper:"#"+d+"_addTag",fake_input:"#"+d+"_tag"},e);b[d]=f.delimiter;if(e.onAddTag||e.onRemoveTag||e.onChange){c[d]=new Array;c[d]["onAddTag"]=e.onAddTag;c[d]["onRemoveTag"]=e.onRemoveTag;c[d]["onChange"]=e.onChange}var g='
';if(e.interactive){g=g+''}g=g+'
';a(g).insertAfter(this);a(f.holder).css("width",e.width);a(f.holder).css("height",e.height);if(a(f.real_input).val()!=""){a.fn.tagsInput.importTags(a(f.real_input),a(f.real_input).val())}if(e.interactive){a(f.fake_input).val(a(f.fake_input).attr("data-default"));a(f.fake_input).css("color",e.placeholderColor);a(f.fake_input).resetAutosize(e);a(f.holder).bind("click",f,function(b){a(b.data.fake_input).focus()});a(f.fake_input).bind("focus",f,function(b){if(a(b.data.fake_input).val()==a(b.data.fake_input).attr("data-default")){a(b.data.fake_input).val("")}a(b.data.fake_input).css("color","#000000")});if(e.autocomplete_url!=undefined){autocomplete_options={source:e.autocomplete_url};for(attrname in e.autocomplete){autocomplete_options[attrname]=e.autocomplete[attrname]}if(jQuery.Autocompleter!==undefined){a(f.fake_input).autocomplete(e.autocomplete_url,e.autocomplete);a(f.fake_input).bind("result",f,function(b,c,f){if(c){a("#"+d).addTag(c[0]+"",{focus:true,unique:e.unique})}})}else if(jQuery.ui.autocomplete!==undefined){a(f.fake_input).autocomplete(autocomplete_options);a(f.fake_input).bind("autocompleteselect",f,function(b,c){a(b.data.real_input).addTag(c.item.value,{focus:true,unique:e.unique});return false})}}else{a(f.fake_input).bind("blur",f,function(b){var c=a(this).attr("data-default");if(a(b.data.fake_input).val()!=""&&a(b.data.fake_input).val()!=c){if(b.data.minChars<=a(b.data.fake_input).val().length&&(!b.data.maxChars||b.data.maxChars>=a(b.data.fake_input).val().length))a(b.data.real_input).addTag(a(b.data.fake_input).val(),{focus:true,unique:e.unique})}else{a(b.data.fake_input).val(a(b.data.fake_input).attr("data-default"));a(b.data.fake_input).css("color",e.placeholderColor)}return false})}a(f.fake_input).bind("keypress",f,function(b){if(b.which==b.data.delimiter.charCodeAt(0)||b.which==13){b.preventDefault();if(b.data.minChars<=a(b.data.fake_input).val().length&&(!b.data.maxChars||b.data.maxChars>=a(b.data.fake_input).val().length))a(b.data.real_input).addTag(a(b.data.fake_input).val(),{focus:true,unique:e.unique});a(b.data.fake_input).resetAutosize(e);return false}else if(b.data.autosize){a(b.data.fake_input).doAutosize(e)}});f.removeWithBackspace&&a(f.fake_input).bind("keydown",function(b){if(b.keyCode==8&&a(this).val()==""){b.preventDefault();var c=a(this).closest(".tagsinput").find(".tag:last").text();var d=a(this).attr("id").replace(/_tag$/,"");c=c.replace(/[\s]+x$/,"");a("#"+d).removeTag(escape(c));a(this).trigger("focus")}});a(f.fake_input).blur();if(f.unique){a(f.fake_input).keydown(function(b){if(b.keyCode==8||String.fromCharCode(b.which).match(/\w+|[áéíóúÁÉÍÓÚñÑ,/]+/)){a(this).removeClass("not_valid")}})}}});return this};a.fn.tagsInput.updateTagsField=function(c,d){var e=a(c).attr("id");a(c).val(d.join(b[e]))};a.fn.tagsInput.importTags=function(d,e){a(d).val("");var f=a(d).attr("id");var g=e.split(b[f]);for(i=0;i 12 | Datasets / {{dataset.title}} / edit 13 | 14 |
15 |
16 |

Sidebar

17 |
18 |
19 |
20 |
21 | 27 |
28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 | {% if not dataset.isNew %} 36 | 37 | {% endif %} 38 | 39 | 40 | 41 |
42 | 43 |
44 | 45 |
46 |
47 | 48 |
49 | 50 |
51 | 52 |
53 |
54 | 55 |
56 | 57 |
58 | 59 |
60 |
61 | 62 |
63 | 64 |
65 | 72 |
73 |
74 | 75 |
76 | 77 |
78 | 79 |
80 |
81 | 82 |
83 | 84 |
85 | 86 |
87 |
88 | 89 |
90 | 91 |
92 | 93 |
94 |
95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
104 | 105 | 106 |
107 |
108 | 109 | {% for resource in dataset.distribution %} 110 |
111 |
112 | 113 |
114 | 115 |
116 |
117 | 118 |
119 | 120 |
121 | 122 |
123 |
124 | 125 |
126 | 127 |
128 | 129 |
130 |
131 | 132 |
133 |
134 | 135 |
136 | 137 |
138 | 142 |
143 |
144 |
145 | {% endfor %} 146 |
147 |
148 |
149 | 150 |
151 |
152 |
153 | 154 | 155 |
156 |
157 |
158 | 159 |
160 |
161 | 162 |   163 | 164 |
165 |
166 | 167 |
168 | {% if !dataset.isNew %} 169 |
170 | 171 |   172 | 173 | 174 |
175 | {% endif %} 176 | 177 | 200 | 201 | {% endblock %} 202 | -------------------------------------------------------------------------------- /public/css/app.css: -------------------------------------------------------------------------------- 1 | 2 | /* Sticky footer styles 3 | -------------------------------------------------- */ 4 | 5 | html, 6 | body { 7 | height: 100%; 8 | /* The html and body elements cannot have any padding or margin. */ 9 | } 10 | 11 | @media (min-width: 1200px) { 12 | .container { 13 | width: 940px; 14 | } 15 | } 16 | 17 | /* Wrapper for page content to push down footer */ 18 | #wrap { 19 | min-height: 100%; 20 | height: auto; 21 | /* Negative indent footer by its height */ 22 | margin: 0 auto -60px; 23 | /* Pad bottom by footer height */ 24 | padding: 0 0 60px; 25 | } 26 | 27 | /* Set the fixed height of the footer here */ 28 | #footer { 29 | height: 60px; 30 | background-color: #f5f5f5; 31 | } 32 | 33 | /*Custom Navbar*/ 34 | 35 | .navbar-custom { 36 | background-color: #2155ba; 37 | border-color: #1c489e; 38 | } 39 | .navbar-custom .navbar-brand { 40 | color: #ffffff; 41 | } 42 | .navbar-custom .navbar-brand:hover, 43 | .navbar-custom .navbar-brand:focus { 44 | color: #e6e6e6; 45 | background-color: transparent; 46 | } 47 | .navbar-custom .navbar-text { 48 | color: #ffffff; 49 | } 50 | .navbar-custom .navbar-nav > li > a { 51 | color: #ffffff; 52 | } 53 | .navbar-custom .navbar-nav > li > a:hover, 54 | .navbar-custom .navbar-nav > li > a:focus { 55 | color: #c0c0c0; 56 | background-color: transparent; 57 | } 58 | .navbar-custom .navbar-nav > .active > a, 59 | .navbar-custom .navbar-nav > .active > a:hover, 60 | .navbar-custom .navbar-nav > .active > a:focus { 61 | color: #c0c0c0; 62 | background-color: #1c489e; 63 | } 64 | .navbar-custom .navbar-nav > .disabled > a, 65 | .navbar-custom .navbar-nav > .disabled > a:hover, 66 | .navbar-custom .navbar-nav > .disabled > a:focus { 67 | color: #cccccc; 68 | background-color: transparent; 69 | } 70 | .navbar-custom .navbar-toggle { 71 | border-color: #dddddd; 72 | } 73 | .navbar-custom .navbar-toggle:hover, 74 | .navbar-custom .navbar-toggle:focus { 75 | background-color: #dddddd; 76 | } 77 | .navbar-custom .navbar-toggle .icon-bar { 78 | background-color: #cccccc; 79 | } 80 | .navbar-custom .navbar-collapse, 81 | .navbar-custom .navbar-form { 82 | border-color: #1c479c; 83 | } 84 | .navbar-custom .navbar-nav > .dropdown > a:hover .caret, 85 | .navbar-custom .navbar-nav > .dropdown > a:focus .caret { 86 | border-top-color: #c0c0c0; 87 | border-bottom-color: #c0c0c0; 88 | } 89 | .navbar-custom .navbar-nav > .open > a, 90 | .navbar-custom .navbar-nav > .open > a:hover, 91 | .navbar-custom .navbar-nav > .open > a:focus { 92 | background-color: #1c489e; 93 | color: #c0c0c0; 94 | } 95 | .navbar-custom .navbar-nav > .open > a .caret, 96 | .navbar-custom .navbar-nav > .open > a:hover .caret, 97 | .navbar-custom .navbar-nav > .open > a:focus .caret { 98 | border-top-color: #c0c0c0; 99 | border-bottom-color: #c0c0c0; 100 | } 101 | .navbar-custom .navbar-nav > .dropdown > a .caret { 102 | border-top-color: #ffffff; 103 | border-bottom-color: #ffffff; 104 | } 105 | @media (max-width: 767) { 106 | .navbar-custom .navbar-nav .open .dropdown-menu > li > a { 107 | color: #ffffff; 108 | } 109 | .navbar-custom .navbar-nav .open .dropdown-menu > li > a:hover, 110 | .navbar-custom .navbar-nav .open .dropdown-menu > li > a:focus { 111 | color: #c0c0c0; 112 | background-color: transparent; 113 | } 114 | .navbar-custom .navbar-nav .open .dropdown-menu > .active > a, 115 | .navbar-custom .navbar-nav .open .dropdown-menu > .active > a:hover, 116 | .navbar-custom .navbar-nav .open .dropdown-menu > .active > a:focus { 117 | color: #c0c0c0; 118 | background-color: #1c489e; 119 | } 120 | .navbar-custom .navbar-nav .open .dropdown-menu > .disabled > a, 121 | .navbar-custom .navbar-nav .open .dropdown-menu > .disabled > a:hover, 122 | .navbar-custom .navbar-nav .open .dropdown-menu > .disabled > a:focus { 123 | color: #cccccc; 124 | background-color: transparent; 125 | } 126 | } 127 | .navbar-custom .navbar-link { 128 | color: #ffffff; 129 | } 130 | .navbar-custom .navbar-link:hover { 131 | color: #c0c0c0; 132 | } 133 | 134 | 135 | 136 | 137 | 138 | /* Custom page CSS 139 | -------------------------------------------------- */ 140 | /* Not required for template or sticky footer method. */ 141 | 142 | 143 | .container .text-muted { 144 | margin: 20px 0; 145 | } 146 | 147 | #footer > .container { 148 | padding-left: 15px; 149 | padding-right: 15px; 150 | } 151 | 152 | /* ----------------------------------------------- 153 | App styles 154 | ------------------------------------------------ */ 155 | 156 | 157 | .article, .comment { 158 | padding: 15px 0; 159 | border-bottom: 1px solid #e8e8e8; 160 | } 161 | 162 | .error, 163 | a.error, 164 | a.error:hover, 165 | a.error:focus, 166 | button.error, 167 | button.error:hover, 168 | button.error:focus { 169 | color: #D9534F; 170 | } 171 | 172 | .form-actions { 173 | background: #fff; 174 | border: 0; 175 | } 176 | 177 | #spacer { 178 | display: inline-block; 179 | } 180 | 181 | .muted { 182 | color: #aaa; 183 | font-size: 12px; 184 | } 185 | 186 | .alert { 187 | margin-top: 10px; 188 | } 189 | 190 | .navbar-brand { 191 | font-family: 'Six Caps', sans-serif; 192 | color:#fff; 193 | font-size:40px; 194 | text-shadow: 3px 3px 7px #333; 195 | } 196 | 197 | .navbar-nav > li > a.travis{ 198 | padding-top: 13px; 199 | 200 | } 201 | 202 | .navbar-right > li > a { 203 | padding-left: 5px; 204 | padding-right: 5px; 205 | } 206 | 207 | /*Dataset List View 208 | */ 209 | .dataset { 210 | border-bottom: 1px dotted #dddddd; 211 | padding: 8px 0 18px 0; 212 | } 213 | 214 | .dataset a { 215 | color: #000; 216 | } 217 | 218 | .dataset-count { 219 | margin-top: 0; 220 | } 221 | 222 | /*Sidebar*/ 223 | 224 | .list-group-item.active, .list-group-item.active:hover, .list-group-item.active:focus { 225 | 226 | background-color: #2155BA; 227 | } 228 | 229 | li.list-group-item.tag-count-item:hover { 230 | background-color: #F5F5F5; 231 | cursor: pointer; 232 | } 233 | 234 | .tagFilter .remove:hover { 235 | cursor: pointer; 236 | } 237 | 238 | .pager { 239 | margin: 0; 240 | } 241 | 242 | /*http://bootsnipp.com/snippets/featured/panels-with-nav-tabs*/ 243 | .panel.with-nav-tabs .panel-heading{ 244 | padding: 5px 5px 0 5px; 245 | } 246 | .panel.with-nav-tabs .nav-tabs{ 247 | border-bottom: none; 248 | } 249 | .panel.with-nav-tabs .nav-justified{ 250 | margin-bottom: -1px; 251 | } 252 | 253 | /********************************************************************/ 254 | /*** PANEL PRIMARY ***/ 255 | .with-nav-tabs.panel-primary .nav-tabs > li > a, 256 | .with-nav-tabs.panel-primary .nav-tabs > li > a:hover, 257 | .with-nav-tabs.panel-primary .nav-tabs > li > a:focus { 258 | color: #fff; 259 | } 260 | .with-nav-tabs.panel-primary .nav-tabs > .open > a, 261 | .with-nav-tabs.panel-primary .nav-tabs > .open > a:hover, 262 | .with-nav-tabs.panel-primary .nav-tabs > .open > a:focus, 263 | .with-nav-tabs.panel-primary .nav-tabs > li > a:hover, 264 | .with-nav-tabs.panel-primary .nav-tabs > li > a:focus { 265 | color: #fff; 266 | background-color: #3071a9; 267 | border-color: transparent; 268 | } 269 | .with-nav-tabs.panel-primary .nav-tabs > li.active > a, 270 | .with-nav-tabs.panel-primary .nav-tabs > li.active > a:hover, 271 | .with-nav-tabs.panel-primary .nav-tabs > li.active > a:focus { 272 | color: #428bca; 273 | background-color: #fff; 274 | border-color: #428bca; 275 | border-bottom-color: transparent; 276 | } 277 | .with-nav-tabs.panel-primary .nav-tabs > li.dropdown .dropdown-menu { 278 | background-color: #428bca; 279 | border-color: #3071a9; 280 | } 281 | .with-nav-tabs.panel-primary .nav-tabs > li.dropdown .dropdown-menu > li > a { 282 | color: #fff; 283 | } 284 | .with-nav-tabs.panel-primary .nav-tabs > li.dropdown .dropdown-menu > li > a:hover, 285 | .with-nav-tabs.panel-primary .nav-tabs > li.dropdown .dropdown-menu > li > a:focus { 286 | background-color: #3071a9; 287 | } 288 | .with-nav-tabs.panel-primary .nav-tabs > li.dropdown .dropdown-menu > .active > a, 289 | .with-nav-tabs.panel-primary .nav-tabs > li.dropdown .dropdown-menu > .active > a:hover, 290 | .with-nav-tabs.panel-primary .nav-tabs > li.dropdown .dropdown-menu > .active > a:focus { 291 | background-color: #4a9fe9; 292 | } 293 | /********************************************************************/ 294 | /*** PANEL SUCCESS ***/ 295 | .with-nav-tabs.panel-success .nav-tabs > li > a, 296 | .with-nav-tabs.panel-success .nav-tabs > li > a:hover, 297 | .with-nav-tabs.panel-success .nav-tabs > li > a:focus { 298 | color: #3c763d; 299 | } 300 | .with-nav-tabs.panel-success .nav-tabs > .open > a, 301 | .with-nav-tabs.panel-success .nav-tabs > .open > a:hover, 302 | .with-nav-tabs.panel-success .nav-tabs > .open > a:focus, 303 | .with-nav-tabs.panel-success .nav-tabs > li > a:hover, 304 | .with-nav-tabs.panel-success .nav-tabs > li > a:focus { 305 | color: #3c763d; 306 | background-color: #d6e9c6; 307 | border-color: transparent; 308 | } 309 | .with-nav-tabs.panel-success .nav-tabs > li.active > a, 310 | .with-nav-tabs.panel-success .nav-tabs > li.active > a:hover, 311 | .with-nav-tabs.panel-success .nav-tabs > li.active > a:focus { 312 | color: #3c763d; 313 | background-color: #fff; 314 | border-color: #d6e9c6; 315 | border-bottom-color: transparent; 316 | } 317 | .with-nav-tabs.panel-success .nav-tabs > li.dropdown .dropdown-menu { 318 | background-color: #dff0d8; 319 | border-color: #d6e9c6; 320 | } 321 | .with-nav-tabs.panel-success .nav-tabs > li.dropdown .dropdown-menu > li > a { 322 | color: #3c763d; 323 | } 324 | .with-nav-tabs.panel-success .nav-tabs > li.dropdown .dropdown-menu > li > a:hover, 325 | .with-nav-tabs.panel-success .nav-tabs > li.dropdown .dropdown-menu > li > a:focus { 326 | background-color: #d6e9c6; 327 | } 328 | .with-nav-tabs.panel-success .nav-tabs > li.dropdown .dropdown-menu > .active > a, 329 | .with-nav-tabs.panel-success .nav-tabs > li.dropdown .dropdown-menu > .active > a:hover, 330 | .with-nav-tabs.panel-success .nav-tabs > li.dropdown .dropdown-menu > .active > a:focus { 331 | color: #fff; 332 | background-color: #3c763d; 333 | } 334 | -------------------------------------------------------------------------------- /public/css/bootstrap-responsive.min.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.3.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 | */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;line-height:0;content:""}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}@-ms-viewport{width:device-width}.hidden{display:none;visibility:hidden}.visible-phone{display:none!important}.visible-tablet{display:none!important}.hidden-desktop{display:none!important}.visible-desktop{display:inherit!important}@media(min-width:768px) and (max-width:979px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-tablet{display:inherit!important}.hidden-tablet{display:none!important}}@media(max-width:767px){.hidden-desktop{display:inherit!important}.visible-desktop{display:none!important}.visible-phone{display:inherit!important}.hidden-phone{display:none!important}}.visible-print{display:none!important}@media print{.visible-print{display:inherit!important}.hidden-print{display:none!important}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:30px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:1170px}.span12{width:1170px}.span11{width:1070px}.span10{width:970px}.span9{width:870px}.span8{width:770px}.span7{width:670px}.span6{width:570px}.span5{width:470px}.span4{width:370px}.span3{width:270px}.span2{width:170px}.span1{width:70px}.offset12{margin-left:1230px}.offset11{margin-left:1130px}.offset10{margin-left:1030px}.offset9{margin-left:930px}.offset8{margin-left:830px}.offset7{margin-left:730px}.offset6{margin-left:630px}.offset5{margin-left:530px}.offset4{margin-left:430px}.offset3{margin-left:330px}.offset2{margin-left:230px}.offset1{margin-left:130px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.564102564102564%;*margin-left:2.5109110747408616%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.564102564102564%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145299145%;*width:91.39979996362975%}.row-fluid .span10{width:82.90598290598291%;*width:82.8527914166212%}.row-fluid .span9{width:74.35897435897436%;*width:74.30578286961266%}.row-fluid .span8{width:65.81196581196582%;*width:65.75877432260411%}.row-fluid .span7{width:57.26495726495726%;*width:57.21176577559556%}.row-fluid .span6{width:48.717948717948715%;*width:48.664757228587014%}.row-fluid .span5{width:40.17094017094017%;*width:40.11774868157847%}.row-fluid .span4{width:31.623931623931625%;*width:31.570740134569924%}.row-fluid .span3{width:23.076923076923077%;*width:23.023731587561375%}.row-fluid .span2{width:14.52991452991453%;*width:14.476723040552828%}.row-fluid .span1{width:5.982905982905983%;*width:5.929714493544281%}.row-fluid .offset12{margin-left:105.12820512820512%;*margin-left:105.02182214948171%}.row-fluid .offset12:first-child{margin-left:102.56410256410257%;*margin-left:102.45771958537915%}.row-fluid .offset11{margin-left:96.58119658119658%;*margin-left:96.47481360247316%}.row-fluid .offset11:first-child{margin-left:94.01709401709402%;*margin-left:93.91071103837061%}.row-fluid .offset10{margin-left:88.03418803418803%;*margin-left:87.92780505546462%}.row-fluid .offset10:first-child{margin-left:85.47008547008548%;*margin-left:85.36370249136206%}.row-fluid .offset9{margin-left:79.48717948717949%;*margin-left:79.38079650845607%}.row-fluid .offset9:first-child{margin-left:76.92307692307693%;*margin-left:76.81669394435352%}.row-fluid .offset8{margin-left:70.94017094017094%;*margin-left:70.83378796144753%}.row-fluid .offset8:first-child{margin-left:68.37606837606839%;*margin-left:68.26968539734497%}.row-fluid .offset7{margin-left:62.393162393162385%;*margin-left:62.28677941443899%}.row-fluid .offset7:first-child{margin-left:59.82905982905982%;*margin-left:59.72267685033642%}.row-fluid .offset6{margin-left:53.84615384615384%;*margin-left:53.739770867430444%}.row-fluid .offset6:first-child{margin-left:51.28205128205128%;*margin-left:51.175668303327875%}.row-fluid .offset5{margin-left:45.299145299145295%;*margin-left:45.1927623204219%}.row-fluid .offset5:first-child{margin-left:42.73504273504273%;*margin-left:42.62865975631933%}.row-fluid .offset4{margin-left:36.75213675213675%;*margin-left:36.645753773413354%}.row-fluid .offset4:first-child{margin-left:34.18803418803419%;*margin-left:34.081651209310785%}.row-fluid .offset3{margin-left:28.205128205128204%;*margin-left:28.0987452264048%}.row-fluid .offset3:first-child{margin-left:25.641025641025642%;*margin-left:25.53464266230224%}.row-fluid .offset2{margin-left:19.65811965811966%;*margin-left:19.551736679396257%}.row-fluid .offset2:first-child{margin-left:17.094017094017094%;*margin-left:16.98763411529369%}.row-fluid .offset1{margin-left:11.11111111111111%;*margin-left:11.004728132387708%}.row-fluid .offset1:first-child{margin-left:8.547008547008547%;*margin-left:8.440625568285142%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:30px}input.span12,textarea.span12,.uneditable-input.span12{width:1156px}input.span11,textarea.span11,.uneditable-input.span11{width:1056px}input.span10,textarea.span10,.uneditable-input.span10{width:956px}input.span9,textarea.span9,.uneditable-input.span9{width:856px}input.span8,textarea.span8,.uneditable-input.span8{width:756px}input.span7,textarea.span7,.uneditable-input.span7{width:656px}input.span6,textarea.span6,.uneditable-input.span6{width:556px}input.span5,textarea.span5,.uneditable-input.span5{width:456px}input.span4,textarea.span4,.uneditable-input.span4{width:356px}input.span3,textarea.span3,.uneditable-input.span3{width:256px}input.span2,textarea.span2,.uneditable-input.span2{width:156px}input.span1,textarea.span1,.uneditable-input.span1{width:56px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;line-height:0;content:""}.row:after{clear:both}[class*="span"]{float:left;min-height:1px;margin-left:20px}.container,.navbar-static-top .container,.navbar-fixed-top .container,.navbar-fixed-bottom .container{width:724px}.span12{width:724px}.span11{width:662px}.span10{width:600px}.span9{width:538px}.span8{width:476px}.span7{width:414px}.span6{width:352px}.span5{width:290px}.span4{width:228px}.span3{width:166px}.span2{width:104px}.span1{width:42px}.offset12{margin-left:764px}.offset11{margin-left:702px}.offset10{margin-left:640px}.offset9{margin-left:578px}.offset8{margin-left:516px}.offset7{margin-left:454px}.offset6{margin-left:392px}.offset5{margin-left:330px}.offset4{margin-left:268px}.offset3{margin-left:206px}.offset2{margin-left:144px}.offset1{margin-left:82px}.row-fluid{width:100%;*zoom:1}.row-fluid:before,.row-fluid:after{display:table;line-height:0;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:30px;margin-left:2.7624309392265194%;*margin-left:2.709239449864817%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .controls-row [class*="span"]+[class*="span"]{margin-left:2.7624309392265194%}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.43646408839778%;*width:91.38327259903608%}.row-fluid .span10{width:82.87292817679558%;*width:82.81973668743387%}.row-fluid .span9{width:74.30939226519337%;*width:74.25620077583166%}.row-fluid .span8{width:65.74585635359117%;*width:65.69266486422946%}.row-fluid .span7{width:57.18232044198895%;*width:57.12912895262725%}.row-fluid .span6{width:48.61878453038674%;*width:48.56559304102504%}.row-fluid .span5{width:40.05524861878453%;*width:40.00205712942283%}.row-fluid .span4{width:31.491712707182323%;*width:31.43852121782062%}.row-fluid .span3{width:22.92817679558011%;*width:22.87498530621841%}.row-fluid .span2{width:14.3646408839779%;*width:14.311449394616199%}.row-fluid .span1{width:5.801104972375691%;*width:5.747913483013988%}.row-fluid .offset12{margin-left:105.52486187845304%;*margin-left:105.41847889972962%}.row-fluid .offset12:first-child{margin-left:102.76243093922652%;*margin-left:102.6560479605031%}.row-fluid .offset11{margin-left:96.96132596685082%;*margin-left:96.8549429881274%}.row-fluid .offset11:first-child{margin-left:94.1988950276243%;*margin-left:94.09251204890089%}.row-fluid .offset10{margin-left:88.39779005524862%;*margin-left:88.2914070765252%}.row-fluid .offset10:first-child{margin-left:85.6353591160221%;*margin-left:85.52897613729868%}.row-fluid .offset9{margin-left:79.8342541436464%;*margin-left:79.72787116492299%}.row-fluid .offset9:first-child{margin-left:77.07182320441989%;*margin-left:76.96544022569647%}.row-fluid .offset8{margin-left:71.2707182320442%;*margin-left:71.16433525332079%}.row-fluid .offset8:first-child{margin-left:68.50828729281768%;*margin-left:68.40190431409427%}.row-fluid .offset7{margin-left:62.70718232044199%;*margin-left:62.600799341718584%}.row-fluid .offset7:first-child{margin-left:59.94475138121547%;*margin-left:59.838368402492065%}.row-fluid .offset6{margin-left:54.14364640883978%;*margin-left:54.037263430116376%}.row-fluid .offset6:first-child{margin-left:51.38121546961326%;*margin-left:51.27483249088986%}.row-fluid .offset5{margin-left:45.58011049723757%;*margin-left:45.47372751851417%}.row-fluid .offset5:first-child{margin-left:42.81767955801105%;*margin-left:42.71129657928765%}.row-fluid .offset4{margin-left:37.01657458563536%;*margin-left:36.91019160691196%}.row-fluid .offset4:first-child{margin-left:34.25414364640884%;*margin-left:34.14776066768544%}.row-fluid .offset3{margin-left:28.45303867403315%;*margin-left:28.346655695309746%}.row-fluid .offset3:first-child{margin-left:25.69060773480663%;*margin-left:25.584224756083227%}.row-fluid .offset2{margin-left:19.88950276243094%;*margin-left:19.783119783707537%}.row-fluid .offset2:first-child{margin-left:17.12707182320442%;*margin-left:17.02068884448102%}.row-fluid .offset1{margin-left:11.32596685082873%;*margin-left:11.219583872105325%}.row-fluid .offset1:first-child{margin-left:8.56353591160221%;*margin-left:8.457152932878806%}input,textarea,.uneditable-input{margin-left:0}.controls-row [class*="span"]+[class*="span"]{margin-left:20px}input.span12,textarea.span12,.uneditable-input.span12{width:710px}input.span11,textarea.span11,.uneditable-input.span11{width:648px}input.span10,textarea.span10,.uneditable-input.span10{width:586px}input.span9,textarea.span9,.uneditable-input.span9{width:524px}input.span8,textarea.span8,.uneditable-input.span8{width:462px}input.span7,textarea.span7,.uneditable-input.span7{width:400px}input.span6,textarea.span6,.uneditable-input.span6{width:338px}input.span5,textarea.span5,.uneditable-input.span5{width:276px}input.span4,textarea.span4,.uneditable-input.span4{width:214px}input.span3,textarea.span3,.uneditable-input.span3{width:152px}input.span2,textarea.span2,.uneditable-input.span2{width:90px}input.span1,textarea.span1,.uneditable-input.span1{width:28px}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom,.navbar-static-top{margin-right:-20px;margin-left:-20px}.container-fluid{padding:0}.dl-horizontal dt{float:none;width:auto;clear:none;text-align:left}.dl-horizontal dd{margin-left:0}.container{width:auto}.row-fluid{width:100%}.row,.thumbnails{margin-left:0}.thumbnails>li{float:none;margin-left:0}[class*="span"],.uneditable-input[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:100%;margin-left:0;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.span12,.row-fluid .span12{width:100%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="offset"]:first-child{margin-left:0}.input-large,.input-xlarge,.input-xxlarge,input[class*="span"],select[class*="span"],textarea[class*="span"],.uneditable-input{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.input-prepend input,.input-append input,.input-prepend input[class*="span"],.input-append input[class*="span"]{display:inline-block;width:auto}.controls-row [class*="span"]+[class*="span"]{margin-left:0}.modal{position:fixed;top:20px;right:20px;left:20px;width:auto;margin:0}.modal.fade{top:-100px}.modal.fade.in{top:20px}}@media(max-width:480px){.nav-collapse{-webkit-transform:translate3d(0,0,0)}.page-header h1 small{display:block;line-height:20px}input[type="checkbox"],input[type="radio"]{border:1px solid #ccc}.form-horizontal .control-label{float:none;width:auto;padding-top:0;text-align:left}.form-horizontal .controls{margin-left:0}.form-horizontal .control-list{padding-top:0}.form-horizontal .form-actions{padding-right:10px;padding-left:10px}.media .pull-left,.media .pull-right{display:block;float:none;margin-bottom:10px}.media-object{margin-right:0;margin-left:0}.modal{top:10px;right:10px;left:10px}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:20px}.navbar-fixed-bottom{margin-top:20px}.navbar-fixed-top .navbar-inner,.navbar-fixed-bottom .navbar-inner{padding:5px}.navbar .container{width:auto;padding:0}.navbar .brand{padding-right:10px;padding-left:10px;margin:0 0 0 -5px}.nav-collapse{clear:both}.nav-collapse .nav{float:none;margin:0 0 10px}.nav-collapse .nav>li{float:none}.nav-collapse .nav>li>a{margin-bottom:2px}.nav-collapse .nav>.divider-vertical{display:none}.nav-collapse .nav .nav-header{color:#777;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:9px 15px;font-weight:bold;color:#777;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px}.nav-collapse .btn{padding:4px 10px 4px;font-weight:normal;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.nav-collapse .dropdown-menu li+li a{margin-bottom:2px}.nav-collapse .nav>li>a:hover,.nav-collapse .nav>li>a:focus,.nav-collapse .dropdown-menu a:hover,.nav-collapse .dropdown-menu a:focus{background-color:#f2f2f2}.navbar-inverse .nav-collapse .nav>li>a,.navbar-inverse .nav-collapse .dropdown-menu a{color:#999}.navbar-inverse .nav-collapse .nav>li>a:hover,.navbar-inverse .nav-collapse .nav>li>a:focus,.navbar-inverse .nav-collapse .dropdown-menu a:hover,.navbar-inverse .nav-collapse .dropdown-menu a:focus{background-color:#111}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:none;float:none;max-width:none;padding:0;margin:0 15px;background-color:transparent;border:0;-webkit-border-radius:0;-moz-border-radius:0;border-radius:0;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none}.nav-collapse .open>.dropdown-menu{display:block}.nav-collapse .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .nav>li>.dropdown-menu:before,.nav-collapse .nav>li>.dropdown-menu:after{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:10px 15px;margin:10px 0;border-top:1px solid #f2f2f2;border-bottom:1px solid #f2f2f2;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);-moz-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1)}.navbar-inverse .nav-collapse .navbar-form,.navbar-inverse .nav-collapse .navbar-search{border-top-color:#111;border-bottom-color:#111}.navbar .nav-collapse .nav.pull-right{float:none;margin-left:0}.nav-collapse,.nav-collapse.collapse{height:0;overflow:hidden}.navbar .btn-navbar{display:block}.navbar-static .navbar-inner{padding-right:10px;padding-left:10px}}@media(min-width:980px){.nav-collapse.collapse{height:auto!important;overflow:visible!important}} 10 | -------------------------------------------------------------------------------- /public/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bootstrap.js v2.3.1 by @fat & @mdo 3 | * Copyright 2012 Twitter, Inc. 4 | * http://www.apache.org/licenses/LICENSE-2.0.txt 5 | */ 6 | !function(e){"use strict";e(function(){e.support.transition=function(){var e=function(){var e=document.createElement("bootstrap"),t={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"},n;for(n in t)if(e.style[n]!==undefined)return t[n]}();return e&&{end:e}}()})}(window.jQuery),!function(e){"use strict";var t='[data-dismiss="alert"]',n=function(n){e(n).on("click",t,this.close)};n.prototype.close=function(t){function s(){i.trigger("closed").remove()}var n=e(this),r=n.attr("data-target"),i;r||(r=n.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,"")),i=e(r),t&&t.preventDefault(),i.length||(i=n.hasClass("alert")?n:n.parent()),i.trigger(t=e.Event("close"));if(t.isDefaultPrevented())return;i.removeClass("in"),e.support.transition&&i.hasClass("fade")?i.on(e.support.transition.end,s):s()};var r=e.fn.alert;e.fn.alert=function(t){return this.each(function(){var r=e(this),i=r.data("alert");i||r.data("alert",i=new n(this)),typeof t=="string"&&i[t].call(r)})},e.fn.alert.Constructor=n,e.fn.alert.noConflict=function(){return e.fn.alert=r,this},e(document).on("click.alert.data-api",t,n.prototype.close)}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.button.defaults,n)};t.prototype.setState=function(e){var t="disabled",n=this.$element,r=n.data(),i=n.is("input")?"val":"html";e+="Text",r.resetText||n.data("resetText",n[i]()),n[i](r[e]||this.options[e]),setTimeout(function(){e=="loadingText"?n.addClass(t).attr(t,t):n.removeClass(t).removeAttr(t)},0)},t.prototype.toggle=function(){var e=this.$element.closest('[data-toggle="buttons-radio"]');e&&e.find(".active").removeClass("active"),this.$element.toggleClass("active")};var n=e.fn.button;e.fn.button=function(n){return this.each(function(){var r=e(this),i=r.data("button"),s=typeof n=="object"&&n;i||r.data("button",i=new t(this,s)),n=="toggle"?i.toggle():n&&i.setState(n)})},e.fn.button.defaults={loadingText:"loading..."},e.fn.button.Constructor=t,e.fn.button.noConflict=function(){return e.fn.button=n,this},e(document).on("click.button.data-api","[data-toggle^=button]",function(t){var n=e(t.target);n.hasClass("btn")||(n=n.closest(".btn")),n.button("toggle")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.$indicators=this.$element.find(".carousel-indicators"),this.options=n,this.options.pause=="hover"&&this.$element.on("mouseenter",e.proxy(this.pause,this)).on("mouseleave",e.proxy(this.cycle,this))};t.prototype={cycle:function(t){return t||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(e.proxy(this.next,this),this.options.interval)),this},getActiveIndex:function(){return this.$active=this.$element.find(".item.active"),this.$items=this.$active.parent().children(),this.$items.index(this.$active)},to:function(t){var n=this.getActiveIndex(),r=this;if(t>this.$items.length-1||t<0)return;return this.sliding?this.$element.one("slid",function(){r.to(t)}):n==t?this.pause().cycle():this.slide(t>n?"next":"prev",e(this.$items[t]))},pause:function(t){return t||(this.paused=!0),this.$element.find(".next, .prev").length&&e.support.transition.end&&(this.$element.trigger(e.support.transition.end),this.cycle(!0)),clearInterval(this.interval),this.interval=null,this},next:function(){if(this.sliding)return;return this.slide("next")},prev:function(){if(this.sliding)return;return this.slide("prev")},slide:function(t,n){var r=this.$element.find(".item.active"),i=n||r[t](),s=this.interval,o=t=="next"?"left":"right",u=t=="next"?"first":"last",a=this,f;this.sliding=!0,s&&this.pause(),i=i.length?i:this.$element.find(".item")[u](),f=e.Event("slide",{relatedTarget:i[0],direction:o});if(i.hasClass("active"))return;this.$indicators.length&&(this.$indicators.find(".active").removeClass("active"),this.$element.one("slid",function(){var t=e(a.$indicators.children()[a.getActiveIndex()]);t&&t.addClass("active")}));if(e.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(f);if(f.isDefaultPrevented())return;i.addClass(t),i[0].offsetWidth,r.addClass(o),i.addClass(o),this.$element.one(e.support.transition.end,function(){i.removeClass([t,o].join(" ")).addClass("active"),r.removeClass(["active",o].join(" ")),a.sliding=!1,setTimeout(function(){a.$element.trigger("slid")},0)})}else{this.$element.trigger(f);if(f.isDefaultPrevented())return;r.removeClass("active"),i.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return s&&this.cycle(),this}};var n=e.fn.carousel;e.fn.carousel=function(n){return this.each(function(){var r=e(this),i=r.data("carousel"),s=e.extend({},e.fn.carousel.defaults,typeof n=="object"&&n),o=typeof n=="string"?n:s.slide;i||r.data("carousel",i=new t(this,s)),typeof n=="number"?i.to(n):o?i[o]():s.interval&&i.pause().cycle()})},e.fn.carousel.defaults={interval:5e3,pause:"hover"},e.fn.carousel.Constructor=t,e.fn.carousel.noConflict=function(){return e.fn.carousel=n,this},e(document).on("click.carousel.data-api","[data-slide], [data-slide-to]",function(t){var n=e(this),r,i=e(n.attr("data-target")||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,"")),s=e.extend({},i.data(),n.data()),o;i.carousel(s),(o=n.attr("data-slide-to"))&&i.data("carousel").pause().to(o).cycle(),t.preventDefault()})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.collapse.defaults,n),this.options.parent&&(this.$parent=e(this.options.parent)),this.options.toggle&&this.toggle()};t.prototype={constructor:t,dimension:function(){var e=this.$element.hasClass("width");return e?"width":"height"},show:function(){var t,n,r,i;if(this.transitioning||this.$element.hasClass("in"))return;t=this.dimension(),n=e.camelCase(["scroll",t].join("-")),r=this.$parent&&this.$parent.find("> .accordion-group > .in");if(r&&r.length){i=r.data("collapse");if(i&&i.transitioning)return;r.collapse("hide"),i||r.data("collapse",null)}this.$element[t](0),this.transition("addClass",e.Event("show"),"shown"),e.support.transition&&this.$element[t](this.$element[0][n])},hide:function(){var t;if(this.transitioning||!this.$element.hasClass("in"))return;t=this.dimension(),this.reset(this.$element[t]()),this.transition("removeClass",e.Event("hide"),"hidden"),this.$element[t](0)},reset:function(e){var t=this.dimension();return this.$element.removeClass("collapse")[t](e||"auto")[0].offsetWidth,this.$element[e!==null?"addClass":"removeClass"]("collapse"),this},transition:function(t,n,r){var i=this,s=function(){n.type=="show"&&i.reset(),i.transitioning=0,i.$element.trigger(r)};this.$element.trigger(n);if(n.isDefaultPrevented())return;this.transitioning=1,this.$element[t]("in"),e.support.transition&&this.$element.hasClass("collapse")?this.$element.one(e.support.transition.end,s):s()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}};var n=e.fn.collapse;e.fn.collapse=function(n){return this.each(function(){var r=e(this),i=r.data("collapse"),s=e.extend({},e.fn.collapse.defaults,r.data(),typeof n=="object"&&n);i||r.data("collapse",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.collapse.defaults={toggle:!0},e.fn.collapse.Constructor=t,e.fn.collapse.noConflict=function(){return e.fn.collapse=n,this},e(document).on("click.collapse.data-api","[data-toggle=collapse]",function(t){var n=e(this),r,i=n.attr("data-target")||t.preventDefault()||(r=n.attr("href"))&&r.replace(/.*(?=#[^\s]+$)/,""),s=e(i).data("collapse")?"toggle":n.data();n[e(i).hasClass("in")?"addClass":"removeClass"]("collapsed"),e(i).collapse(s)})}(window.jQuery),!function(e){"use strict";function r(){e(t).each(function(){i(e(this)).removeClass("open")})}function i(t){var n=t.attr("data-target"),r;n||(n=t.attr("href"),n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")),r=n&&e(n);if(!r||!r.length)r=t.parent();return r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;return s=i(n),o=s.hasClass("open"),r(),o||s.toggleClass("open"),n.focus(),!1},keydown:function(n){var r,s,o,u,a,f;if(!/(38|40|27)/.test(n.keyCode))return;r=e(this),n.preventDefault(),n.stopPropagation();if(r.is(".disabled, :disabled"))return;u=i(r),a=u.hasClass("open");if(!a||a&&n.keyCode==27)return n.which==27&&u.find(t).focus(),r.click();s=e("[role=menu] li:not(.divider):visible a",u);if(!s.length)return;f=s.index(s.filter(":focus")),n.keyCode==38&&f>0&&f--,n.keyCode==40&&f').appendTo(document.body),this.$backdrop.click(this.options.backdrop=="static"?e.proxy(this.$element[0].focus,this.$element[0]):e.proxy(this.hide,this)),i&&this.$backdrop[0].offsetWidth,this.$backdrop.addClass("in");if(!t)return;i?this.$backdrop.one(e.support.transition.end,t):t()}else!this.isShown&&this.$backdrop?(this.$backdrop.removeClass("in"),e.support.transition&&this.$element.hasClass("fade")?this.$backdrop.one(e.support.transition.end,t):t()):t&&t()}};var n=e.fn.modal;e.fn.modal=function(n){return this.each(function(){var r=e(this),i=r.data("modal"),s=e.extend({},e.fn.modal.defaults,r.data(),typeof n=="object"&&n);i||r.data("modal",i=new t(this,s)),typeof n=="string"?i[n]():s.show&&i.show()})},e.fn.modal.defaults={backdrop:!0,keyboard:!0,show:!0},e.fn.modal.Constructor=t,e.fn.modal.noConflict=function(){return e.fn.modal=n,this},e(document).on("click.modal.data-api",'[data-toggle="modal"]',function(t){var n=e(this),r=n.attr("href"),i=e(n.attr("data-target")||r&&r.replace(/.*(?=#[^\s]+$)/,"")),s=i.data("modal")?"toggle":e.extend({remote:!/#/.test(r)&&r},i.data(),n.data());t.preventDefault(),i.modal(s).one("hide",function(){n.focus()})})}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("tooltip",e,t)};t.prototype={constructor:t,init:function(t,n,r){var i,s,o,u,a;this.type=t,this.$element=e(n),this.options=this.getOptions(r),this.enabled=!0,o=this.options.trigger.split(" ");for(a=o.length;a--;)u=o[a],u=="click"?this.$element.on("click."+this.type,this.options.selector,e.proxy(this.toggle,this)):u!="manual"&&(i=u=="hover"?"mouseenter":"focus",s=u=="hover"?"mouseleave":"blur",this.$element.on(i+"."+this.type,this.options.selector,e.proxy(this.enter,this)),this.$element.on(s+"."+this.type,this.options.selector,e.proxy(this.leave,this)));this.options.selector?this._options=e.extend({},this.options,{trigger:"manual",selector:""}):this.fixTitle()},getOptions:function(t){return t=e.extend({},e.fn[this.type].defaults,this.$element.data(),t),t.delay&&typeof t.delay=="number"&&(t.delay={show:t.delay,hide:t.delay}),t},enter:function(t){var n=e.fn[this.type].defaults,r={},i;this._options&&e.each(this._options,function(e,t){n[e]!=t&&(r[e]=t)},this),i=e(t.currentTarget)[this.type](r).data(this.type);if(!i.options.delay||!i.options.delay.show)return i.show();clearTimeout(this.timeout),i.hoverState="in",this.timeout=setTimeout(function(){i.hoverState=="in"&&i.show()},i.options.delay.show)},leave:function(t){var n=e(t.currentTarget)[this.type](this._options).data(this.type);this.timeout&&clearTimeout(this.timeout);if(!n.options.delay||!n.options.delay.hide)return n.hide();n.hoverState="out",this.timeout=setTimeout(function(){n.hoverState=="out"&&n.hide()},n.options.delay.hide)},show:function(){var t,n,r,i,s,o,u=e.Event("show");if(this.hasContent()&&this.enabled){this.$element.trigger(u);if(u.isDefaultPrevented())return;t=this.tip(),this.setContent(),this.options.animation&&t.addClass("fade"),s=typeof this.options.placement=="function"?this.options.placement.call(this,t[0],this.$element[0]):this.options.placement,t.detach().css({top:0,left:0,display:"block"}),this.options.container?t.appendTo(this.options.container):t.insertAfter(this.$element),n=this.getPosition(),r=t[0].offsetWidth,i=t[0].offsetHeight;switch(s){case"bottom":o={top:n.top+n.height,left:n.left+n.width/2-r/2};break;case"top":o={top:n.top-i,left:n.left+n.width/2-r/2};break;case"left":o={top:n.top+n.height/2-i/2,left:n.left-r};break;case"right":o={top:n.top+n.height/2-i/2,left:n.left+n.width}}this.applyPlacement(o,s),this.$element.trigger("shown")}},applyPlacement:function(e,t){var n=this.tip(),r=n[0].offsetWidth,i=n[0].offsetHeight,s,o,u,a;n.offset(e).addClass(t).addClass("in"),s=n[0].offsetWidth,o=n[0].offsetHeight,t=="top"&&o!=i&&(e.top=e.top+i-o,a=!0),t=="bottom"||t=="top"?(u=0,e.left<0&&(u=e.left*-2,e.left=0,n.offset(e),s=n[0].offsetWidth,o=n[0].offsetHeight),this.replaceArrow(u-r+s,s,"left")):this.replaceArrow(o-i,o,"top"),a&&n.offset(e)},replaceArrow:function(e,t,n){this.arrow().css(n,e?50*(1-e/t)+"%":"")},setContent:function(){var e=this.tip(),t=this.getTitle();e.find(".tooltip-inner")[this.options.html?"html":"text"](t),e.removeClass("fade in top bottom left right")},hide:function(){function i(){var t=setTimeout(function(){n.off(e.support.transition.end).detach()},500);n.one(e.support.transition.end,function(){clearTimeout(t),n.detach()})}var t=this,n=this.tip(),r=e.Event("hide");this.$element.trigger(r);if(r.isDefaultPrevented())return;return n.removeClass("in"),e.support.transition&&this.$tip.hasClass("fade")?i():n.detach(),this.$element.trigger("hidden"),this},fixTitle:function(){var e=this.$element;(e.attr("title")||typeof e.attr("data-original-title")!="string")&&e.attr("data-original-title",e.attr("title")||"").attr("title","")},hasContent:function(){return this.getTitle()},getPosition:function(){var t=this.$element[0];return e.extend({},typeof t.getBoundingClientRect=="function"?t.getBoundingClientRect():{width:t.offsetWidth,height:t.offsetHeight},this.$element.offset())},getTitle:function(){var e,t=this.$element,n=this.options;return e=t.attr("data-original-title")||(typeof n.title=="function"?n.title.call(t[0]):n.title),e},tip:function(){return this.$tip=this.$tip||e(this.options.template)},arrow:function(){return this.$arrow=this.$arrow||this.tip().find(".tooltip-arrow")},validate:function(){this.$element[0].parentNode||(this.hide(),this.$element=null,this.options=null)},enable:function(){this.enabled=!0},disable:function(){this.enabled=!1},toggleEnabled:function(){this.enabled=!this.enabled},toggle:function(t){var n=t?e(t.currentTarget)[this.type](this._options).data(this.type):this;n.tip().hasClass("in")?n.hide():n.show()},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}};var n=e.fn.tooltip;e.fn.tooltip=function(n){return this.each(function(){var r=e(this),i=r.data("tooltip"),s=typeof n=="object"&&n;i||r.data("tooltip",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.tooltip.Constructor=t,e.fn.tooltip.defaults={animation:!0,placement:"top",selector:!1,template:'
',trigger:"hover focus",title:"",delay:0,html:!1,container:!1},e.fn.tooltip.noConflict=function(){return e.fn.tooltip=n,this}}(window.jQuery),!function(e){"use strict";var t=function(e,t){this.init("popover",e,t)};t.prototype=e.extend({},e.fn.tooltip.Constructor.prototype,{constructor:t,setContent:function(){var e=this.tip(),t=this.getTitle(),n=this.getContent();e.find(".popover-title")[this.options.html?"html":"text"](t),e.find(".popover-content")[this.options.html?"html":"text"](n),e.removeClass("fade top bottom left right in")},hasContent:function(){return this.getTitle()||this.getContent()},getContent:function(){var e,t=this.$element,n=this.options;return e=(typeof n.content=="function"?n.content.call(t[0]):n.content)||t.attr("data-content"),e},tip:function(){return this.$tip||(this.$tip=e(this.options.template)),this.$tip},destroy:function(){this.hide().$element.off("."+this.type).removeData(this.type)}});var n=e.fn.popover;e.fn.popover=function(n){return this.each(function(){var r=e(this),i=r.data("popover"),s=typeof n=="object"&&n;i||r.data("popover",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.popover.Constructor=t,e.fn.popover.defaults=e.extend({},e.fn.tooltip.defaults,{placement:"right",trigger:"click",content:"",template:'

'}),e.fn.popover.noConflict=function(){return e.fn.popover=n,this}}(window.jQuery),!function(e){"use strict";function t(t,n){var r=e.proxy(this.process,this),i=e(t).is("body")?e(window):e(t),s;this.options=e.extend({},e.fn.scrollspy.defaults,n),this.$scrollElement=i.on("scroll.scroll-spy.data-api",r),this.selector=(this.options.target||(s=e(t).attr("href"))&&s.replace(/.*(?=#[^\s]+$)/,"")||"")+" .nav li > a",this.$body=e("body"),this.refresh(),this.process()}t.prototype={constructor:t,refresh:function(){var t=this,n;this.offsets=e([]),this.targets=e([]),n=this.$body.find(this.selector).map(function(){var n=e(this),r=n.data("target")||n.attr("href"),i=/^#\w/.test(r)&&e(r);return i&&i.length&&[[i.position().top+(!e.isWindow(t.$scrollElement.get(0))&&t.$scrollElement.scrollTop()),r]]||null}).sort(function(e,t){return e[0]-t[0]}).each(function(){t.offsets.push(this[0]),t.targets.push(this[1])})},process:function(){var e=this.$scrollElement.scrollTop()+this.options.offset,t=this.$scrollElement[0].scrollHeight||this.$body[0].scrollHeight,n=t-this.$scrollElement.height(),r=this.offsets,i=this.targets,s=this.activeTarget,o;if(e>=n)return s!=(o=i.last()[0])&&this.activate(o);for(o=r.length;o--;)s!=i[o]&&e>=r[o]&&(!r[o+1]||e<=r[o+1])&&this.activate(i[o])},activate:function(t){var n,r;this.activeTarget=t,e(this.selector).parent(".active").removeClass("active"),r=this.selector+'[data-target="'+t+'"],'+this.selector+'[href="'+t+'"]',n=e(r).parent("li").addClass("active"),n.parent(".dropdown-menu").length&&(n=n.closest("li.dropdown").addClass("active")),n.trigger("activate")}};var n=e.fn.scrollspy;e.fn.scrollspy=function(n){return this.each(function(){var r=e(this),i=r.data("scrollspy"),s=typeof n=="object"&&n;i||r.data("scrollspy",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.scrollspy.Constructor=t,e.fn.scrollspy.defaults={offset:10},e.fn.scrollspy.noConflict=function(){return e.fn.scrollspy=n,this},e(window).on("load",function(){e('[data-spy="scroll"]').each(function(){var t=e(this);t.scrollspy(t.data())})})}(window.jQuery),!function(e){"use strict";var t=function(t){this.element=e(t)};t.prototype={constructor:t,show:function(){var t=this.element,n=t.closest("ul:not(.dropdown-menu)"),r=t.attr("data-target"),i,s,o;r||(r=t.attr("href"),r=r&&r.replace(/.*(?=#[^\s]*$)/,""));if(t.parent("li").hasClass("active"))return;i=n.find(".active:last a")[0],o=e.Event("show",{relatedTarget:i}),t.trigger(o);if(o.isDefaultPrevented())return;s=e(r),this.activate(t.parent("li"),n),this.activate(s,s.parent(),function(){t.trigger({type:"shown",relatedTarget:i})})},activate:function(t,n,r){function o(){i.removeClass("active").find("> .dropdown-menu > .active").removeClass("active"),t.addClass("active"),s?(t[0].offsetWidth,t.addClass("in")):t.removeClass("fade"),t.parent(".dropdown-menu")&&t.closest("li.dropdown").addClass("active"),r&&r()}var i=n.find("> .active"),s=r&&e.support.transition&&i.hasClass("fade");s?i.one(e.support.transition.end,o):o(),i.removeClass("in")}};var n=e.fn.tab;e.fn.tab=function(n){return this.each(function(){var r=e(this),i=r.data("tab");i||r.data("tab",i=new t(this)),typeof n=="string"&&i[n]()})},e.fn.tab.Constructor=t,e.fn.tab.noConflict=function(){return e.fn.tab=n,this},e(document).on("click.tab.data-api",'[data-toggle="tab"], [data-toggle="pill"]',function(t){t.preventDefault(),e(this).tab("show")})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.$element=e(t),this.options=e.extend({},e.fn.typeahead.defaults,n),this.matcher=this.options.matcher||this.matcher,this.sorter=this.options.sorter||this.sorter,this.highlighter=this.options.highlighter||this.highlighter,this.updater=this.options.updater||this.updater,this.source=this.options.source,this.$menu=e(this.options.menu),this.shown=!1,this.listen()};t.prototype={constructor:t,select:function(){var e=this.$menu.find(".active").attr("data-value");return this.$element.val(this.updater(e)).change(),this.hide()},updater:function(e){return e},show:function(){var t=e.extend({},this.$element.position(),{height:this.$element[0].offsetHeight});return this.$menu.insertAfter(this.$element).css({top:t.top+t.height,left:t.left}).show(),this.shown=!0,this},hide:function(){return this.$menu.hide(),this.shown=!1,this},lookup:function(t){var n;return this.query=this.$element.val(),!this.query||this.query.length"+t+""})},render:function(t){var n=this;return t=e(t).map(function(t,r){return t=e(n.options.item).attr("data-value",r),t.find("a").html(n.highlighter(r)),t[0]}),t.first().addClass("active"),this.$menu.html(t),this},next:function(t){var n=this.$menu.find(".active").removeClass("active"),r=n.next();r.length||(r=e(this.$menu.find("li")[0])),r.addClass("active")},prev:function(e){var t=this.$menu.find(".active").removeClass("active"),n=t.prev();n.length||(n=this.$menu.find("li").last()),n.addClass("active")},listen:function(){this.$element.on("focus",e.proxy(this.focus,this)).on("blur",e.proxy(this.blur,this)).on("keypress",e.proxy(this.keypress,this)).on("keyup",e.proxy(this.keyup,this)),this.eventSupported("keydown")&&this.$element.on("keydown",e.proxy(this.keydown,this)),this.$menu.on("click",e.proxy(this.click,this)).on("mouseenter","li",e.proxy(this.mouseenter,this)).on("mouseleave","li",e.proxy(this.mouseleave,this))},eventSupported:function(e){var t=e in this.$element;return t||(this.$element.setAttribute(e,"return;"),t=typeof this.$element[e]=="function"),t},move:function(e){if(!this.shown)return;switch(e.keyCode){case 9:case 13:case 27:e.preventDefault();break;case 38:e.preventDefault(),this.prev();break;case 40:e.preventDefault(),this.next()}e.stopPropagation()},keydown:function(t){this.suppressKeyPressRepeat=~e.inArray(t.keyCode,[40,38,9,13,27]),this.move(t)},keypress:function(e){if(this.suppressKeyPressRepeat)return;this.move(e)},keyup:function(e){switch(e.keyCode){case 40:case 38:case 16:case 17:case 18:break;case 9:case 13:if(!this.shown)return;this.select();break;case 27:if(!this.shown)return;this.hide();break;default:this.lookup()}e.stopPropagation(),e.preventDefault()},focus:function(e){this.focused=!0},blur:function(e){this.focused=!1,!this.mousedover&&this.shown&&this.hide()},click:function(e){e.stopPropagation(),e.preventDefault(),this.select(),this.$element.focus()},mouseenter:function(t){this.mousedover=!0,this.$menu.find(".active").removeClass("active"),e(t.currentTarget).addClass("active")},mouseleave:function(e){this.mousedover=!1,!this.focused&&this.shown&&this.hide()}};var n=e.fn.typeahead;e.fn.typeahead=function(n){return this.each(function(){var r=e(this),i=r.data("typeahead"),s=typeof n=="object"&&n;i||r.data("typeahead",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.typeahead.defaults={source:[],items:8,menu:'',item:'
  • ',minLength:1},e.fn.typeahead.Constructor=t,e.fn.typeahead.noConflict=function(){return e.fn.typeahead=n,this},e(document).on("focus.typeahead.data-api",'[data-provide="typeahead"]',function(t){var n=e(this);if(n.data("typeahead"))return;n.typeahead(n.data())})}(window.jQuery),!function(e){"use strict";var t=function(t,n){this.options=e.extend({},e.fn.affix.defaults,n),this.$window=e(window).on("scroll.affix.data-api",e.proxy(this.checkPosition,this)).on("click.affix.data-api",e.proxy(function(){setTimeout(e.proxy(this.checkPosition,this),1)},this)),this.$element=e(t),this.checkPosition()};t.prototype.checkPosition=function(){if(!this.$element.is(":visible"))return;var t=e(document).height(),n=this.$window.scrollTop(),r=this.$element.offset(),i=this.options.offset,s=i.bottom,o=i.top,u="affix affix-top affix-bottom",a;typeof i!="object"&&(s=o=i),typeof o=="function"&&(o=i.top()),typeof s=="function"&&(s=i.bottom()),a=this.unpin!=null&&n+this.unpin<=r.top?!1:s!=null&&r.top+this.$element.height()>=t-s?"bottom":o!=null&&n<=o?"top":!1;if(this.affixed===a)return;this.affixed=a,this.unpin=a=="bottom"?r.top-n:null,this.$element.removeClass(u).addClass("affix"+(a?"-"+a:""))};var n=e.fn.affix;e.fn.affix=function(n){return this.each(function(){var r=e(this),i=r.data("affix"),s=typeof n=="object"&&n;i||r.data("affix",i=new t(this,s)),typeof n=="string"&&i[n]()})},e.fn.affix.Constructor=t,e.fn.affix.defaults={offset:0},e.fn.affix.noConflict=function(){return e.fn.affix=n,this},e(window).on("load",function(){e('[data-spy="affix"]').each(function(){var t=e(this),n=t.data();n.offset=n.offset||{},n.offsetBottom&&(n.offset.bottom=n.offsetBottom),n.offsetTop&&(n.offset.top=n.offsetTop),t.affix(n)})})}(window.jQuery); 7 | --------------------------------------------------------------------------------