├── README ├── app ├── views │ ├── includes │ │ ├── footer.jade │ │ ├── foot.jade │ │ ├── header.jade │ │ └── head.jade │ ├── teams │ │ ├── edit.jade │ │ ├── new.jade │ │ ├── show.jade │ │ ├── index.jade │ │ └── form.jade │ ├── articles │ │ ├── edit.jade │ │ ├── new.jade │ │ ├── index.jade │ │ ├── show.jade │ │ └── form.jade │ ├── users │ │ ├── show.jade │ │ ├── login.jade │ │ ├── auth.jade │ │ └── signup.jade │ ├── 500.jade │ ├── 404.jade │ ├── comments │ │ ├── comment.jade │ │ └── form.jade │ └── layouts │ │ └── default.jade ├── mailer │ ├── templates │ │ └── comment.jade │ └── notify.js ├── controllers │ ├── comments.js │ ├── tags.js │ ├── teams.js │ ├── users.js │ └── articles.js └── models │ ├── organization.js │ ├── season.js │ ├── team.js │ ├── league.js │ ├── user.js │ └── article.js ├── Procfile ├── README.md ├── public ├── img │ ├── github.png │ ├── google.png │ ├── facebook.png │ ├── twitter.png │ ├── glyphicons-halflings.png │ └── glyphicons-halflings-white.png ├── js │ ├── app.js │ ├── jquery.tagsinput.min.js │ ├── bootbox.js │ ├── bootstrap.min.js │ └── jquery.min.js └── css │ ├── app.css │ ├── jquery.tagsinput.css │ └── bootstrap-responsive.min.css ├── .travis.yml ├── .gitignore ├── test ├── helper.js ├── test-users.js └── test-articles.js ├── config ├── imager.example.js ├── middlewares │ └── authorization.js ├── config.example.js ├── express.js ├── routes.js └── passport.js ├── package.json ├── server.js └── .jshintrc /README: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/views/includes/footer.jade: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: ./node_modules/.bin/forever -m 5 server.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | league 2 | ====== 3 | 4 | League management with node.js 5 | -------------------------------------------------------------------------------- /app/views/teams/edit.jade: -------------------------------------------------------------------------------- 1 | extends form 2 | 3 | block main 4 | h1 Edit Article 5 | hr 6 | -------------------------------------------------------------------------------- /app/views/teams/new.jade: -------------------------------------------------------------------------------- 1 | extends form 2 | 3 | block main 4 | h1 New Article 5 | hr 6 | -------------------------------------------------------------------------------- /public/img/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/league/master/public/img/github.png -------------------------------------------------------------------------------- /public/img/google.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/league/master/public/img/google.png -------------------------------------------------------------------------------- /app/views/articles/edit.jade: -------------------------------------------------------------------------------- 1 | extends form 2 | 3 | block main 4 | h1 Edit Article 5 | hr 6 | -------------------------------------------------------------------------------- /app/views/articles/new.jade: -------------------------------------------------------------------------------- 1 | extends form 2 | 3 | block main 4 | h1 New Article 5 | hr 6 | -------------------------------------------------------------------------------- /public/img/facebook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/league/master/public/img/facebook.png -------------------------------------------------------------------------------- /public/img/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/league/master/public/img/twitter.png -------------------------------------------------------------------------------- /app/views/users/show.jade: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block main 4 | h1= user.name 5 | 6 | -------------------------------------------------------------------------------- /app/views/teams/show.jade: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block main 4 | h1= team.name 5 | 6 | block content 7 | -------------------------------------------------------------------------------- /public/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/league/master/public/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /public/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tkh44/league/master/public/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /app/mailer/templates/comment.jade: -------------------------------------------------------------------------------- 1 | p Hello #{to} 2 | 3 | p #{from} has added a comment "#{body}" on your article #{article} 4 | 5 | p Cheers 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_script: 2 | - cp config/config.example.js config/config.js 3 | - cp config/imager.example.js config/imager.js 4 | services: 5 | - mongodb 6 | language: node_js 7 | node_js: 8 | - "0.10" 9 | - "0.8" 10 | -------------------------------------------------------------------------------- /app/views/500.jade: -------------------------------------------------------------------------------- 1 | extends layouts/default 2 | 3 | block main 4 | h1 Oops something went wrong 5 | br 6 | span 500 7 | 8 | block content 9 | #error-message-box 10 | #error-stack-trace 11 | pre 12 | code!= error 13 | -------------------------------------------------------------------------------- /app/views/404.jade: -------------------------------------------------------------------------------- 1 | extends layouts/default 2 | 3 | block main 4 | h1 Oops something went wrong 5 | br 6 | span 404 7 | 8 | block content 9 | #error-message-box 10 | #error-stack-trace 11 | pre 12 | code!= error 13 | 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | 14 | npm-debug.log 15 | *.sublime-* 16 | 17 | node_modules/ 18 | .monitor 19 | config/config.js 20 | config/imager.js 21 | Makefile -------------------------------------------------------------------------------- /app/views/comments/comment.jade: -------------------------------------------------------------------------------- 1 | 2 | if (comment && comment.user) 3 | .comment 4 | a(href="/users/"+comment.user._id) #{comment.user.name} 5 | | :  6 | != comment.body 7 | br 8 | span.date #{formatDate(comment.createdAt, "%b %d, %Y at %I:%M %p")} 9 | -------------------------------------------------------------------------------- /app/views/comments/form.jade: -------------------------------------------------------------------------------- 1 | form.form-vertical(method="post", action="/articles/#{article._id}/comments") 2 | .control-group 3 | .controls 4 | textarea#comment.input-xxlarge(type='text', rows="6", name="body", placeholder='Add your comment') 5 | button.btn.btn-primary(type='submit') Add comment 6 | -------------------------------------------------------------------------------- /app/views/teams/index.jade: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block main 4 | h1 Teams 5 | 6 | block content 7 | .row 8 | .span12 9 | a.btn.btn-primary(href='teams/new') Add Team 10 | .row 11 | .span9 12 | each team in teams 13 | .row 14 | .span12 15 | h1=team.name 16 | -------------------------------------------------------------------------------- /app/views/layouts/default.jade: -------------------------------------------------------------------------------- 1 | !!! 5 2 | html(lang='en', xmlns='http://www.w3.org/1999/xhtml', xmlns:fb='https://www.facebook.com/2008/fbml', itemscope='itemscope', itemtype='http://schema.org/Product') 3 | include ../includes/head 4 | body 5 | .wrapper 6 | include ../includes/header 7 | .container 8 | .main-content 9 | .main-head 10 | block main 11 | block content 12 | .push 13 | include ../includes/footer 14 | include ../includes/foot 15 | -------------------------------------------------------------------------------- /app/controllers/comments.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose') 7 | 8 | /** 9 | * Create comment 10 | */ 11 | 12 | exports.create = function (req, res) { 13 | var article = req.article 14 | var user = req.user 15 | 16 | if (!req.body.body) return res.redirect('/articles/'+ article.id) 17 | 18 | article.addComment(user, req.body, function (err) { 19 | if (err) return res.render('500') 20 | res.redirect('/articles/'+ article.id) 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /public/js/app.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | 3 | // confirmations 4 | $('.confirm').submit(function (e) { 5 | e.preventDefault(); 6 | var self = this; 7 | var msg = 'Are you sure?'; 8 | bootbox.confirm(msg, 'cancel', 'Yes! I am sure', function (action) { 9 | if (action) { 10 | $(self).unbind('submit'); 11 | $(self).trigger('submit'); 12 | } 13 | }); 14 | }); 15 | 16 | $('#tags').tagsInput({ 17 | 'height':'60px', 18 | 'width':'280px' 19 | }); 20 | 21 | }); 22 | -------------------------------------------------------------------------------- /app/views/users/login.jade: -------------------------------------------------------------------------------- 1 | extends auth 2 | 3 | block auth 4 | form.login.form-horizontal(action="/users/session", method="post") 5 | p.error= message 6 | .control-group 7 | label.control-label(for='email') Email 8 | .controls 9 | input#email(type='text', name="email", placeholder='Email') 10 | 11 | .control-group 12 | label.control-label(for='password') Password 13 | .controls 14 | input#password(type='password', name="password", placeholder='Password') 15 | 16 | .form-actions 17 | button.btn.btn-primary(type='submit') Login 18 |   19 | | or  20 | a.show-signup(href="/signup") sign up 21 | -------------------------------------------------------------------------------- /public/css/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 60px; 3 | } 4 | 5 | .date { 6 | font-size: 11px; 7 | color: #aaa; 8 | } 9 | 10 | .author { 11 | color: #aaa; 12 | } 13 | 14 | .title { 15 | padding: 5px 0 15px 0; 16 | display: block; 17 | font-size: 24px; 18 | } 19 | 20 | .article, .comment { 21 | padding: 15px 0; 22 | border-top: 1px solid #e8e8e8; 23 | } 24 | 25 | .main-head { 26 | text-align: center; 27 | padding: 20px; 28 | } 29 | 30 | .btn { 31 | outline: none !important; 32 | } 33 | 34 | .center { 35 | text-align: center; 36 | } 37 | 38 | .tags { 39 | margin-top: 10px; 40 | } 41 | 42 | a.tag { 43 | margin: 0 10px 0 0; 44 | } 45 | 46 | .error { 47 | color: #FF2602; 48 | } 49 | -------------------------------------------------------------------------------- /app/models/organization.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var mongoose = require('mongoose') 6 | , env = process.env.NODE_ENV || 'development' 7 | , config = require('../../config/config')[env] 8 | , Schema = mongoose.Schema 9 | , TeamSchema = require('./team.js') // Probably don't need this 10 | , LeagueSchema = require('./league.js') 11 | 12 | 13 | /** 14 | * Organization Schema 15 | */ 16 | 17 | var OrganizationSchema = new Schema({ 18 | name: {type : String, 'default' : '', trim : true}, 19 | owner: {type : Schema.ObjectId, ref : 'User'}, 20 | leagues: [LeagueSchema], 21 | createdAt : {type : Date, 'default' : Date.now} 22 | }) 23 | 24 | mongoose.model('Organization', OrganizationSchema) -------------------------------------------------------------------------------- /app/views/users/auth.jade: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block main 4 | h1= title 5 | 6 | block content 7 | .row 8 | .offset1.span5 9 | a(href="/auth/facebook") 10 | img(src="/img/facebook.png") 11 | a(href="/auth/github") 12 | img(src="/img/github.png") 13 | a(href="/auth/twitter") 14 | img(src="/img/twitter.png") 15 | a(href="/auth/google") 16 | img(src="/img/google.png") 17 | .span6 18 | if (typeof errors !== 'undefined') 19 | .fade.in.alert.alert-block.alert-error 20 | a.close(data-dismiss="alert", href="javascript:void(0)") x 21 | ul 22 | each error in errors 23 | li= error.type 24 | 25 | block auth 26 | -------------------------------------------------------------------------------- /app/models/season.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var mongoose = require('mongoose') 6 | , env = process.env.NODE_ENV || 'development' 7 | , config = require('../../config/config')[env] 8 | , Schema = mongoose.Schema 9 | , TeamSchema = require('./team.js') // Probably don't need this 10 | , LeagueSchema = require('./league.js') 11 | 12 | 13 | /** 14 | * Season Schema 15 | */ 16 | 17 | var SeasonSchema = new Schema({ 18 | name: {type: String, default: '', trim : true}, 19 | league: {type: Schema.ObjectId, ref: 'League'}, 20 | teams: [TeamSchema], 21 | startDate: {type: Date}, 22 | endDate: {type: Date}, 23 | createdAt : {type: Date, default: Date.now} 24 | }) 25 | 26 | mongoose.model('Season', SeasonSchema) -------------------------------------------------------------------------------- /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 | var callback = function (item, fn) { item.remove(fn) } 20 | 21 | async.parallel([ 22 | function (cb) { 23 | User.find().exec(function (err, users) { 24 | async.forEach(users, callback, cb) 25 | }) 26 | }, 27 | function (cb) { 28 | Article.find().exec(function (err, apps) { 29 | async.forEach(apps, callback, cb) 30 | }) 31 | } 32 | ], done) 33 | } 34 | -------------------------------------------------------------------------------- /app/views/includes/foot.jade: -------------------------------------------------------------------------------- 1 | script(type='text/javascript', src='/js/jquery.min.js') 2 | script(type='text/javascript', src='/js/bootstrap.min.js') 3 | script(type='text/javascript', src='/js/bootbox.js') 4 | script(type='text/javascript', src='/js/jquery.tagsinput.min.js') 5 | script(type='text/javascript', src='/js/app.js') 6 | script(type="text/javascript") 7 | var _gaq = _gaq || []; 8 | _gaq.push(['_setAccount', 'UA-25468002-1']); 9 | _gaq.push(['_trackPageview']); 10 | 11 | (function() { 12 | var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true; 13 | ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js'; 14 | var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s); 15 | })(); 16 | -------------------------------------------------------------------------------- /config/imager.example.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | variants: { 3 | article: { 4 | resize: { 5 | detail: "x440" 6 | }, 7 | crop: { 8 | thumb: "16000@" 9 | }, 10 | resizeAndCrop: { 11 | mini: {resize: "63504@", crop: "252x210"} 12 | } 13 | }, 14 | 15 | gallery: { 16 | crop: { 17 | thumb: "100x100" 18 | } 19 | } 20 | }, 21 | 22 | storage: { 23 | Rackspace: { 24 | auth: { 25 | username: "USERNAME", 26 | apiKey: "API_KEY", 27 | host: "lon.auth.api.rackspacecloud.com" 28 | }, 29 | container: "CONTAINER_NAME" 30 | }, 31 | S3: { 32 | key: 'API_KEY', 33 | secret: 'SECRET', 34 | bucket: 'BUCKET_NAME', 35 | region: 'REGION' 36 | } 37 | }, 38 | 39 | debug: true 40 | } 41 | -------------------------------------------------------------------------------- /app/views/articles/index.jade: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block main 4 | h1 Articles 5 | 6 | block content 7 | each article in articles 8 | .article 9 | a.title(href='/articles/'+article._id, title=article.title) #{article.title} 10 | p=article.body 11 | .author 12 | span= formatDate(article.createdAt, "%b %d, %Y at %I:%M %p") 13 | span  | Author :  14 | a(href="/users/"+article.user._id)=article.user.name 15 | |  |  16 | if (article.tags) 17 | span.tags 18 | span Tags :  19 | each tag in article.tags.split(',') 20 | a.tag(href="/tags/"+tag) 21 | i.icon-tags 22 | | #{tag} 23 | 24 | if (pages > 1) 25 | .pagination 26 | ul 27 | != createPagination(pages, page) 28 | -------------------------------------------------------------------------------- /app/controllers/tags.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose') 7 | , Article = mongoose.model('Article') 8 | 9 | /** 10 | * List items tagged with a tag 11 | */ 12 | 13 | exports.index = function (req, res) { 14 | var criteria = { tags: req.param('tag') } 15 | var perPage = 5 16 | var page = req.param('page') > 0 ? req.param('page') : 0 17 | var options = { 18 | perPage: perPage, 19 | page: page, 20 | criteria: criteria 21 | } 22 | 23 | Article.list(options, function(err, articles) { 24 | if (err) return res.render('500') 25 | Article.count(criteria).exec(function (err, count) { 26 | res.render('articles/index', { 27 | title: 'List of Articles', 28 | articles: articles, 29 | page: page, 30 | pages: count / perPage 31 | }) 32 | }) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /app/views/includes/header.jade: -------------------------------------------------------------------------------- 1 | 2 | header#header 3 | .navbar.navbar-inverse.navbar-fixed-top(role='navigation') 4 | .navbar-inner 5 | .container 6 | .nav-collapse.collapse 7 | ul.nav 8 | li 9 | a(href='/', title="Home") Home 10 | li 11 | a(href='/teams', title="team listing") Teams 12 | li 13 | a(href="/teams/new", title="new team") New 14 | li 15 | a(href='/leagues', title="league listing") Leagues 16 | ul.nav.pull-right 17 | if (req.isAuthenticated()) 18 | li 19 | a(href="/users/"+req.user.id, title="Profile") Profile 20 | li 21 | a(href="/logout", title="logout") Logout 22 | else 23 | li 24 | a(href="/login", title="Login") Login 25 | -------------------------------------------------------------------------------- /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()) { 8 | return res.redirect('/login') 9 | } 10 | next() 11 | }; 12 | 13 | 14 | /* 15 | * User authorizations routing middleware 16 | */ 17 | 18 | exports.user = { 19 | hasAuthorization : function (req, res, next) { 20 | if (req.profile.id != req.user.id) { 21 | return res.redirect('/users/'+req.profile.id) 22 | } 23 | next() 24 | } 25 | } 26 | 27 | 28 | /* 29 | * Article authorizations routing middleware 30 | */ 31 | 32 | exports.article = { 33 | hasAuthorization : function (req, res, next) { 34 | if (req.article.user.id != req.user.id) { 35 | return res.redirect('/articles/'+req.article.id) 36 | } 37 | next() 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/views/teams/form.jade: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block content 4 | - var action = '/teams' 5 | if (!team.isNew) 6 | - action += '/'+team.id 7 | 8 | if (typeof errors !== 'undefined') 9 | .fade.in.alert.alert-block.alert-error 10 | a.close(data-dismiss="alert", href="javascript:void(0)") x 11 | ul 12 | each error in errors 13 | li= error.type 14 | 15 | .row 16 | .span7 17 | form.form-horizontal(method="post", action=action, enctype="multipart/form-data") 18 | if (!team.isNew) 19 | input(type="hidden", name="_method", value="PUT") 20 | 21 | .control-group 22 | label.control-label(for='name') Name 23 | .controls 24 | input#title.input-xlarge(type='text', name="name", value=team.name, placeholder='Enter the name') 25 | 26 | 27 | .form-actions 28 | button.btn.btn-primary(type='submit') Save changes 29 |   30 | a.btn(href='/articles', title="cancel") Cancel 31 | -------------------------------------------------------------------------------- /app/views/users/signup.jade: -------------------------------------------------------------------------------- 1 | extends auth 2 | 3 | block auth 4 | form.signup.form-horizontal(action="/users", method="post") 5 | .control-group 6 | label.control-label(for='name') Full name 7 | .controls 8 | input#name(type='text', name="name", placeholder='Full name', value=user.name) 9 | 10 | .control-group 11 | label.control-label(for='email') Email 12 | .controls 13 | input#email(type='text', name="email", placeholder='Email', value=user.email) 14 | 15 | .control-group 16 | label.control-label(for='username') Username 17 | .controls 18 | input#username(type='text', name="username", placeholder='Username', value=user.username) 19 | 20 | .control-group 21 | label.control-label(for='password') Password 22 | .controls 23 | input#password(type='password', name="password", placeholder='Password') 24 | 25 | .form-actions 26 | button.btn.btn-primary(type='submit') Sign up 27 |   28 | | or  29 | a.show-login(href="/login") login 30 | -------------------------------------------------------------------------------- /app/models/team.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var mongoose = require('mongoose') 6 | , Schema = mongoose.Schema 7 | , LeagueSchema = require('./league.js') 8 | 9 | 10 | /** 11 | * Team Schema 12 | */ 13 | 14 | var TeamSchema = new Schema({ 15 | name: {type : String, default : 'Team'}, 16 | leagues: [LeagueSchema] 17 | }) 18 | 19 | TeamSchema.statics = { 20 | 21 | /** 22 | * Find team by id 23 | * 24 | * @param {ObjectId} id 25 | * @param {Function} cb 26 | * @api private 27 | */ 28 | 29 | load: function (id, cb) { 30 | this.findOne({ _id : id }) 31 | .populate('leagues') 32 | .exec(cb) 33 | }, 34 | 35 | /** 36 | * List teams 37 | * 38 | * @param {Object} options 39 | * @param {Function} cb 40 | * @api private 41 | */ 42 | 43 | list: function (options, cb) { 44 | var criteria = options.criteria || {} 45 | 46 | this.find(criteria) 47 | .populate('leagues') 48 | .sort({'createdAt': -1}) // sort by date 49 | .exec(cb) 50 | } 51 | 52 | } 53 | 54 | mongoose.model('Team', TeamSchema) -------------------------------------------------------------------------------- /app/views/articles/show.jade: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block main 4 | h1= article.title 5 | 6 | block content 7 | .row 8 | .span9 9 | p=article.body 10 | p.author 11 | span Author :  12 | a(href="/users/"+article.user._id)=article.user.name 13 | .date= formatDate(article.createdAt, "%b %d, %Y at %I:%M %p") 14 | if (article.tags) 15 | .tags 16 | | Tags :  17 | each tag in article.tags.split(',') 18 | a.tag(href="/tags/"+tag) 19 | i.icon-tags 20 | | #{tag} 21 | .span3 22 | if (!article.isNew && article.image && article.image.files && article.image.files.length) 23 | img(src=article.image.cdnUri + '/mini_' + article.image.files[0]) 24 | 25 | p 26 | br 27 | form.center.form-inline.confirm(action="/articles/"+article.id, method="post") 28 | a.btn(href='/articles/'+article._id+'/edit', title="edit") Edit 29 |    30 | input(type="hidden", name="_method", value="DELETE") 31 | button.btn.btn-danger(type="submit") delete 32 | 33 | p 34 | br 35 | h2 Comments 36 | each comment in article.comments 37 | include ../comments/comment 38 | include ../comments/form 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "noobjs" 3 | , "description": "A demo app in nodejs illustrating the use of express, jade and mongoose" 4 | , "version": "3.0.0" 5 | , "private": false 6 | , "author": "Madhusudhan Srinivasa (http://madhums.github.com)" 7 | , "engines": { 8 | "node": "0.10.x" 9 | , "npm": "1.2.x" 10 | } 11 | , "scripts": { 12 | "start": "NODE_ENV=development ./node_modules/.bin/nodemon server.js", 13 | "test": "NODE_ENV=test ./node_modules/.bin/mocha --reporter spec test/test-*.js" 14 | } 15 | , "dependencies": { 16 | "express": "latest" 17 | , "jade": "latest" 18 | , "mongoose": "latest" 19 | , "connect-mongo": "latest" 20 | , "connect-flash": "latest" 21 | , "passport": "latest" 22 | , "passport-local": "latest" 23 | , "passport-facebook": "latest" 24 | , "passport-twitter": "latest" 25 | , "passport-github": "latest" 26 | , "passport-google-oauth": "latest" 27 | , "imager": "latest" 28 | , "notifier": "latest" 29 | , "underscore": "latest" 30 | , "gzippo": "latest" 31 | , "async": "latest" 32 | , "view-helpers": "latest" 33 | , "forever": "latest" 34 | } 35 | , "devDependencies": { 36 | "supertest": "latest" 37 | , "should": "latest" 38 | , "mocha": "latest" 39 | , "nodemon": "latest" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/mailer/notify.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose') 7 | , Notifier = require('notifier') 8 | , env = process.env.NODE_ENV || 'development' 9 | , config = require('../../config/config')[env] 10 | 11 | /** 12 | * Notification methods 13 | */ 14 | 15 | var Notify = { 16 | 17 | /** 18 | * Comment notification 19 | * 20 | * @param {Object} options 21 | * @param {Function} cb 22 | * @api public 23 | */ 24 | 25 | comment: function (options, cb) { 26 | var article = options.article 27 | var author = article.user 28 | var user = options.currentUser 29 | var notifier = new Notifier(config.notifier) 30 | 31 | var obj = { 32 | to: author.email, 33 | from: 'your@product.com', 34 | subject: user.name + ' added a comment on your article ' + article.title, 35 | alert: user.name + ' says: "' + options.comment, 36 | locals: { 37 | to: author.name, 38 | from: user.name, 39 | body: options.comment, 40 | article: article.name 41 | } 42 | } 43 | 44 | // for apple push notifications 45 | /*notifier.use({ 46 | APN: true 47 | parseChannels: ['USER_' + author._id.toString()] 48 | })*/ 49 | 50 | notifier.send('comment', obj, cb) 51 | } 52 | } 53 | 54 | /** 55 | * Expose 56 | */ 57 | 58 | module.exports = Notify 59 | -------------------------------------------------------------------------------- /app/models/league.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var mongoose = require('mongoose') 6 | , env = process.env.NODE_ENV || 'development' 7 | , config = require('../../config/config')[env] 8 | , Schema = mongoose.Schema 9 | , TeamSchema = require('./team.js') 10 | 11 | 12 | /** 13 | * League Schema 14 | */ 15 | 16 | var LeagueSchema = new Schema({ 17 | name: {type : String, default : '', trim : true}, 18 | organization: {type: Schema.ObjectId, ref: 'Organization'}, 19 | teams: [TeamSchema], 20 | createdAt : {type : Date, default : Date.now} 21 | }) 22 | 23 | LeagueSchema.statics = { 24 | 25 | /** 26 | * Find team by id 27 | * 28 | * @param {ObjectId} id 29 | * @param {Function} cb 30 | * @api private 31 | */ 32 | 33 | load: function (id, cb) { 34 | this.findOne({ _id : id }) 35 | .populate('organization') 36 | .populate('teams') 37 | .exec(cb) 38 | }, 39 | 40 | /** 41 | * List teams 42 | * 43 | * @param {Object} options 44 | * @param {Function} cb 45 | * @api private 46 | */ 47 | 48 | list: function (options, cb) { 49 | var criteria = options.criteria || {} 50 | 51 | this.find(criteria) 52 | .populate('organization') 53 | .populate('teams') 54 | .sort({'createdAt': -1}) // sort by date 55 | .exec(cb) 56 | } 57 | 58 | } 59 | 60 | mongoose.model('League', LeagueSchema) -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 2 | /*! 3 | * nodejs-express-mongoose-demo 4 | * Copyright(c) 2013 Madhusudhan Srinivasa 5 | * MIT Licensed 6 | */ 7 | 8 | /** 9 | * Module dependencies. 10 | */ 11 | 12 | var express = require('express') 13 | , fs = require('fs') 14 | , passport = require('passport') 15 | 16 | /** 17 | * Main application entry file. 18 | * Please note that the order of loading is important. 19 | */ 20 | 21 | // Load configurations 22 | // if test env, load example file 23 | var env = process.env.NODE_ENV || 'development' 24 | , config = require('./config/config')[env] 25 | , auth = require('./config/middlewares/authorization') 26 | , mongoose = require('mongoose') 27 | 28 | // Bootstrap db connection 29 | mongoose.connect(config.db) 30 | 31 | // Bootstrap models 32 | var models_path = __dirname + '/app/models' 33 | fs.readdirSync(models_path).forEach(function (file) { 34 | require(models_path+'/'+file) 35 | }) 36 | 37 | // bootstrap passport config 38 | require('./config/passport')(passport, config) 39 | 40 | var app = express() 41 | // express settings 42 | require('./config/express')(app, config, passport) 43 | 44 | // Bootstrap routes 45 | require('./config/routes')(app, passport, auth) 46 | 47 | // Start the app by listening on 48 | var port = process.env.PORT || 3000 49 | app.listen(port) 50 | console.log('Express app started on port '+port) 51 | 52 | // expose app 53 | exports = module.exports = app 54 | -------------------------------------------------------------------------------- /app/controllers/teams.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | var mongoose = require('mongoose') 5 | , async = require('async') 6 | , Team = mongoose.model('Team') 7 | , _ = require('underscore') 8 | 9 | /** 10 | * Find team by id 11 | */ 12 | exports.team = function(req, res, next, id){ 13 | Team.load(id, function (err, team) { 14 | if (err) return next(err) 15 | if (!team) return next(new Error('Failed to load team ' + id)) 16 | req.team = team 17 | next() 18 | }) 19 | } 20 | 21 | /** 22 | * View an team 23 | */ 24 | exports.show = function(req, res){ 25 | res.render('teams/show', { 26 | title: req.team.title, 27 | team: req.team 28 | }) 29 | } 30 | 31 | /** 32 | * New team 33 | */ 34 | exports.new = function(req, res){ 35 | res.render('teams/new', { 36 | title: 'New Team', 37 | team: new Team({}) 38 | }) 39 | } 40 | 41 | /** 42 | * Create a team 43 | */ 44 | exports.create = function (req, res) { 45 | var team = new Team(req.body) 46 | team.user = req.user 47 | 48 | team.save( function(err) { 49 | if (err) { 50 | res.render('teams/new', { 51 | title: 'New Team', 52 | team: team, 53 | errors: err.errors 54 | }) 55 | } 56 | else { 57 | res.redirect('teams') 58 | } 59 | }); 60 | } 61 | 62 | /** 63 | * List of Teams 64 | */ 65 | exports.index = function(req, res){ 66 | var options = {} 67 | 68 | Team.list(options, function(err, teams) { 69 | if (err) return res.render('500') 70 | Team.count().exec(function (err, count) { 71 | res.render('teams/index', { 72 | title: 'List of Teams', 73 | teams: teams 74 | }) 75 | }) 76 | }) 77 | } 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /app/views/articles/form.jade: -------------------------------------------------------------------------------- 1 | extends ../layouts/default 2 | 3 | block content 4 | - var action = '/articles' 5 | if (!article.isNew) 6 | - action += '/'+article.id 7 | 8 | if (typeof errors !== 'undefined') 9 | .fade.in.alert.alert-block.alert-error 10 | a.close(data-dismiss="alert", href="javascript:void(0)") x 11 | ul 12 | each error in errors 13 | li= error.type 14 | 15 | .row 16 | .span7 17 | form.form-horizontal(method="post", action=action, enctype="multipart/form-data") 18 | if (!article.isNew) 19 | input(type="hidden", name="_method", value="PUT") 20 | 21 | .control-group 22 | label.control-label(for='title') Title 23 | .controls 24 | input#title.input-xlarge(type='text', name="title", value=article.title, placeholder='Enter the title') 25 | 26 | .control-group 27 | label.control-label(for='title') Image 28 | .controls 29 | input(type='file', name="image[]") 30 | 31 | .control-group 32 | label.control-label(for='desc') Body 33 | .controls 34 | textarea#desc.input-xlarge(type='text', rows="5", name="body", placeholder='Enter the article description')=article.body 35 | 36 | .control-group 37 | label.control-label(for='desc') Tags 38 | .controls 39 | input#tags(type='text', name="tags", value=article.tags, placeholder='Enter the tags') 40 | 41 | .form-actions 42 | button.btn.btn-primary(type='submit') Save changes 43 |   44 | a.btn(href='/articles', title="cancel") Cancel 45 | .span5 46 | if (!article.isNew && article.image && article.image.files && article.image.files.length) 47 | img(src=article.image.cdnUri + '/mini_' + article.image.files[0]) 48 | -------------------------------------------------------------------------------- /app/views/includes/head.jade: -------------------------------------------------------------------------------- 1 | head(prefix='og: http://ogp.me/ns# nodejsexpressdemo: http://ogp.me/ns/apps/nodejsexpressdemo#') 2 | meta(charset='utf-8') 3 | meta(http-equiv='X-UA-Compatible', content='IE=edge,chrome=1') 4 | meta(name='viewport', content='width=device-width,initial-scale=1') 5 | 6 | title= appName+' - '+title 7 | meta(http-equiv='Content-type', content='text/html;charset=UTF-8') 8 | meta(name="keywords", content="node.js, express, mongoose, mongodb, stylus demo") 9 | meta(name="description", content="A demo app illustrating the usage of express framework, mongoose orm, mongodb and stylus in node.js. The also illustrates how to properly organize your application") 10 | 11 | link(href='/favicon.ico', rel='shortcut icon', type='image/x-icon') 12 | // 13 | Opengraph tags 14 | meta(property='fb:app_id', content='293989217296609') 15 | meta(property='og:title', content='#{appName} - #{title}') 16 | meta(property='og:description', content='A demo app illustrating the usage of express framework, mongoose orm, mongodb and stylus in node.js. The also illustrates how to properly organize your application') 17 | meta(property='og:type', content='website') 18 | meta(property='og:url', content='http://nodejs-express-demo.herokuapp.com') 19 | meta(property='og:image', content='http://blog.getbootstrap.com/wp-content/themes/bootstrap-blog/img/bootstrap-blog-logo.png') 20 | meta(property='og:site_name', content='Node.js Express Mongoose Demo') 21 | meta(property='fb:admins', content='1037213945') 22 | 23 | // 24 | Application styles 25 | link(rel='stylesheet', href='/css/bootstrap.min.css') 26 | link(rel='stylesheet', href='/css/flat.css') 27 | link(rel="stylesheet", href="/css/jquery.tagsinput.css") 28 | // 29 | link(rel='stylesheet', href='/css/bootstrap-responsive.min.css') 30 | link(rel='stylesheet', href='/css/app.css') 31 | // 32 | 33 | // 34 | Le HTML5 shim, for IE6-8 support of HTML5 elements 35 | //if lt IE 9 36 | script(src='http://html5shim.googlecode.com/svn/trunk/html5.js') 37 | -------------------------------------------------------------------------------- /app/controllers/users.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose') 7 | , User = mongoose.model('User') 8 | 9 | exports.signin = function (req, res) {} 10 | 11 | /** 12 | * Auth callback 13 | */ 14 | 15 | exports.authCallback = function (req, res, next) { 16 | res.redirect('/') 17 | } 18 | 19 | /** 20 | * Show login form 21 | */ 22 | 23 | exports.login = function (req, res) { 24 | res.render('users/login', { 25 | title: 'Login', 26 | message: req.flash('error') 27 | }) 28 | } 29 | 30 | /** 31 | * Show sign up form 32 | */ 33 | 34 | exports.signup = function (req, res) { 35 | res.render('users/signup', { 36 | title: 'Sign up', 37 | user: new User() 38 | }) 39 | } 40 | 41 | /** 42 | * Logout 43 | */ 44 | 45 | exports.logout = function (req, res) { 46 | req.logout() 47 | res.redirect('/login') 48 | } 49 | 50 | /** 51 | * Session 52 | */ 53 | 54 | exports.session = function (req, res) { 55 | res.redirect('/') 56 | } 57 | 58 | /** 59 | * Create user 60 | */ 61 | 62 | exports.create = function (req, res) { 63 | var user = new User(req.body) 64 | user.provider = 'local' 65 | user.save(function (err) { 66 | if (err) { 67 | return res.render('users/signup', { errors: err.errors, user: user }) 68 | } 69 | req.logIn(user, function(err) { 70 | if (err) return next(err) 71 | return res.redirect('/') 72 | }) 73 | }) 74 | } 75 | 76 | /** 77 | * Show profile 78 | */ 79 | 80 | exports.show = function (req, res) { 81 | var user = req.profile 82 | res.render('users/show', { 83 | title: user.name, 84 | user: user 85 | }) 86 | } 87 | 88 | /** 89 | * Find user by id 90 | */ 91 | 92 | exports.user = function (req, res, next, id) { 93 | User 94 | .findOne({ _id : id }) 95 | .exec(function (err, user) { 96 | if (err) return next(err) 97 | if (!user) return next(new Error('Failed to load User ' + id)) 98 | req.profile = user 99 | next() 100 | }) 101 | } 102 | -------------------------------------------------------------------------------- /config/config.example.js: -------------------------------------------------------------------------------- 1 | 2 | var path = require('path') 3 | , rootPath = path.normalize(__dirname + '/..') 4 | , templatePath = path.normalize(__dirname + '/../app/mailer/templates') 5 | , notifier = { 6 | APN: false, 7 | email: false, // true 8 | actions: ['comment'], 9 | tplPath: templatePath, 10 | postmarkKey: 'POSTMARK_KEY', 11 | parseAppId: 'PARSE_APP_ID', 12 | parseApiKey: 'PARSE_MASTER_KEY' 13 | } 14 | 15 | module.exports = { 16 | development: { 17 | db: 'mongodb://localhost/noobjs_dev', 18 | root: rootPath, 19 | notifier: notifier, 20 | app: { 21 | name: 'Nodejs Express Mongoose Demo' 22 | }, 23 | facebook: { 24 | clientID: "APP_ID", 25 | clientSecret: "APP_SECRET", 26 | callbackURL: "http://localhost:3000/auth/facebook/callback" 27 | }, 28 | twitter: { 29 | clientID: "CONSUMER_KEY", 30 | clientSecret: "CONSUMER_SECRET", 31 | callbackURL: "http://localhost:3000/auth/twitter/callback" 32 | }, 33 | github: { 34 | clientID: 'APP_ID', 35 | clientSecret: 'APP_SECRET', 36 | callbackURL: 'http://localhost:3000/auth/github/callback' 37 | }, 38 | google: { 39 | clientID: "APP_ID", 40 | clientSecret: "APP_SECRET", 41 | callbackURL: "http://localhost:3000/auth/google/callback" 42 | }, 43 | }, 44 | test: { 45 | db: 'mongodb://localhost/noobjs_test', 46 | root: rootPath, 47 | notifier: notifier, 48 | app: { 49 | name: 'Nodejs Express Mongoose Demo' 50 | }, 51 | facebook: { 52 | clientID: "APP_ID", 53 | clientSecret: "APP_SECRET", 54 | callbackURL: "http://localhost:3000/auth/facebook/callback" 55 | }, 56 | twitter: { 57 | clientID: "CONSUMER_KEY", 58 | clientSecret: "CONSUMER_SECRET", 59 | callbackURL: "http://localhost:3000/auth/twitter/callback" 60 | }, 61 | github: { 62 | clientID: 'APP_ID', 63 | clientSecret: 'APP_SECRET', 64 | callbackURL: 'http://localhost:3000/auth/github/callback' 65 | }, 66 | google: { 67 | clientID: "APP_ID", 68 | clientSecret: "APP_SECRET", 69 | callbackURL: "http://localhost:3000/auth/google/callback" 70 | } 71 | }, 72 | production: {} 73 | } 74 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | //-------------------------------------------------------------------- 3 | // Enforcing Options: 4 | //-------------------------------------------------------------------- 5 | // These options are set to enforce key rules. If you need to make 6 | // exceptions to these rules in code, you must provide a reason in 7 | // the code for why you think the exception is necessary. 8 | //-------------------------------------------------------------------- 9 | "curly": true, 10 | "eqeqeq": true, 11 | "forin": true, 12 | "immed": true, 13 | "latedef": true, 14 | "newcap": true, 15 | "noempty": true, 16 | "nonew": true, 17 | "undef": true, 18 | "unused": true, 19 | "trailing": true, 20 | //REBEL!!!! 21 | "asi": true, 22 | "laxbreak": true, 23 | "laxcomma": true, 24 | "node": true, 25 | "es5": true, 26 | 27 | //-------------------------------------------------------------------- 28 | // Relaxing Options: 29 | //-------------------------------------------------------------------- 30 | // These option suppress specific rules. The currently suppresses rules 31 | // are very limited. If you want to use one of these suppressions in 32 | // your specific code, you must include an explanation of the reason 33 | // why you want to suppress a rule. 34 | //-------------------------------------------------------------------- 35 | "regexdash": true, // ???? 36 | "sub": true, // To support localization string mapping primarily 37 | 38 | //-------------------------------------------------------------------- 39 | // Globals: 40 | //-------------------------------------------------------------------- 41 | // These options define global variables that are exposed by various 42 | // JavaScript libraries and hosting environments. 43 | //-------------------------------------------------------------------- 44 | "browser": true, 45 | 46 | //-------------------------------------------------------------------- 47 | // Formatting rules 48 | //-------------------------------------------------------------------- 49 | // Provides uniform formatting rules. 50 | //-------------------------------------------------------------------- 51 | "indent": 2, 52 | "quotmark": "single" 53 | } -------------------------------------------------------------------------------- /config/express.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var express = require('express') 6 | , mongoStore = require('connect-mongo')(express) 7 | , flash = require('connect-flash') 8 | , helpers = require('view-helpers') 9 | 10 | module.exports = function (app, config, passport) { 11 | 12 | app.set('showStackError', true) 13 | // should be placed before express.static 14 | app.use(express.compress({ 15 | filter: function (req, res) { 16 | return /json|text|javascript|css/.test(res.getHeader('Content-Type')); 17 | }, 18 | level: 9 19 | })) 20 | app.use(express.favicon()) 21 | app.use(express.static(config.root + '/public')) 22 | 23 | // don't use logger for test env 24 | if (process.env.NODE_ENV !== 'test') { 25 | app.use(express.logger('dev')) 26 | } 27 | 28 | // set views path, template engine and default layout 29 | app.set('views', config.root + '/app/views') 30 | app.set('view engine', 'jade') 31 | 32 | app.configure(function () { 33 | // dynamic helpers 34 | app.use(helpers(config.app.name)) 35 | 36 | // cookieParser should be above session 37 | app.use(express.cookieParser()) 38 | 39 | // bodyParser should be above methodOverride 40 | app.use(express.bodyParser()) 41 | app.use(express.methodOverride()) 42 | 43 | // express/mongo session storage 44 | app.use(express.session({ 45 | secret: 'noobjs', 46 | store: new mongoStore({ 47 | url: config.db, 48 | collection : 'sessions' 49 | }) 50 | })) 51 | 52 | // connect flash for flash messages 53 | app.use(flash()) 54 | 55 | // use passport session 56 | app.use(passport.initialize()) 57 | app.use(passport.session()) 58 | 59 | // routes should be at the last 60 | app.use(app.router) 61 | 62 | // assume "not found" in the error msgs 63 | // is a 404. this is somewhat silly, but 64 | // valid, you can do whatever you like, set 65 | // properties, use instanceof etc. 66 | app.use(function(err, req, res, next){ 67 | // treat as 404 68 | if (~err.message.indexOf('not found')) return next() 69 | 70 | // log it 71 | console.error(err.stack) 72 | 73 | // error page 74 | res.status(500).render('500', { error: err.stack }) 75 | }) 76 | 77 | // assume 404 since no middleware responded 78 | app.use(function(req, res, next){ 79 | res.status(404).render('404', { url: req.originalUrl, error: 'Not found' }) 80 | }) 81 | 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /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('should not save the user to the database', function (done) { 43 | User.count(function (err, cnt) { 44 | count.should.equal(cnt) 45 | done() 46 | }) 47 | }) 48 | }) 49 | 50 | describe('Valid parameters', function () { 51 | before(function (done) { 52 | User.count(function (err, cnt) { 53 | count = cnt 54 | done() 55 | }) 56 | }) 57 | 58 | it('should redirect to /articles', function (done) { 59 | request(app) 60 | .post('/users') 61 | .field('name', 'Foo bar') 62 | .field('username', 'foobar') 63 | .field('email', 'foobar@example.com') 64 | .field('password', 'foobar') 65 | .expect('Content-Type', /plain/) 66 | .expect('Location', /\//) 67 | .expect(302) 68 | .expect(/Moved Temporarily/) 69 | .end(done) 70 | }) 71 | 72 | it('should insert a record to the database', function (done) { 73 | User.count(function (err, cnt) { 74 | cnt.should.equal(count + 1) 75 | done() 76 | }) 77 | }) 78 | 79 | it('should save the user to the database', function (done) { 80 | User.findOne({ username: 'foobar' }).exec(function (err, user) { 81 | should.not.exist(err) 82 | user.should.be.an.instanceOf(User) 83 | user.email.should.equal('foobar@example.com') 84 | done() 85 | }) 86 | }) 87 | }) 88 | }) 89 | 90 | after(function (done) { 91 | require('./helper').clearDb(done) 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /app/controllers/articles.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose') 7 | , Imager = require('imager') 8 | , async = require('async') 9 | , Article = mongoose.model('Article') 10 | , _ = require('underscore') 11 | 12 | /** 13 | * Find article by id 14 | */ 15 | 16 | exports.article = function(req, res, next, id){ 17 | var User = mongoose.model('User') 18 | 19 | Article.load(id, function (err, article) { 20 | if (err) return next(err) 21 | if (!article) return next(new Error('Failed to load article ' + id)) 22 | req.article = article 23 | next() 24 | }) 25 | } 26 | 27 | /** 28 | * New article 29 | */ 30 | 31 | exports.new = function(req, res){ 32 | res.render('articles/new', { 33 | title: 'New Article', 34 | article: new Article({}) 35 | }) 36 | } 37 | 38 | /** 39 | * Create an article 40 | */ 41 | 42 | exports.create = function (req, res) { 43 | var article = new Article(req.body) 44 | console.log(req.body); 45 | article.user = req.user 46 | 47 | article.uploadAndSave(req.files.image, function (err) { 48 | if (err) { 49 | res.render('articles/new', { 50 | title: 'New Article', 51 | article: article, 52 | errors: err.errors 53 | }) 54 | } 55 | else { 56 | res.redirect('/articles/'+article._id) 57 | } 58 | }) 59 | } 60 | 61 | /** 62 | * Edit an article 63 | */ 64 | 65 | exports.edit = function (req, res) { 66 | res.render('articles/edit', { 67 | title: 'Edit '+req.article.title, 68 | article: req.article 69 | }) 70 | } 71 | 72 | /** 73 | * Update article 74 | */ 75 | 76 | exports.update = function(req, res){ 77 | var article = req.article 78 | article = _.extend(article, req.body) 79 | 80 | article.uploadAndSave(req.files.image, function(err) { 81 | if (err) { 82 | res.render('articles/edit', { 83 | title: 'Edit Article', 84 | article: article, 85 | errors: err.errors 86 | }) 87 | } 88 | else { 89 | res.redirect('/articles/' + article._id) 90 | } 91 | }) 92 | } 93 | 94 | /** 95 | * View an article 96 | */ 97 | 98 | exports.show = function(req, res){ 99 | res.render('articles/show', { 100 | title: req.article.title, 101 | article: req.article 102 | }) 103 | } 104 | 105 | /** 106 | * Delete an article 107 | */ 108 | 109 | exports.destroy = function(req, res){ 110 | var article = req.article 111 | article.remove(function(err){ 112 | // req.flash('notice', 'Deleted successfully') 113 | res.redirect('/articles') 114 | }) 115 | } 116 | 117 | /** 118 | * List of Articles 119 | */ 120 | 121 | exports.index = function(req, res){ 122 | var page = req.param('page') > 0 ? req.param('page') : 0 123 | var perPage = 15 124 | var options = { 125 | perPage: perPage, 126 | page: page 127 | } 128 | 129 | Article.list(options, function(err, articles) { 130 | if (err) return res.render('500') 131 | Article.count().exec(function (err, count) { 132 | res.render('articles/index', { 133 | title: 'List of Articles', 134 | articles: articles, 135 | page: page, 136 | pages: count / perPage 137 | }) 138 | }) 139 | }) 140 | } 141 | -------------------------------------------------------------------------------- /config/routes.js: -------------------------------------------------------------------------------- 1 | 2 | var async = require('async') 3 | 4 | module.exports = function (app, passport, auth) { 5 | 6 | // user routes 7 | var users = require('../app/controllers/users') 8 | app.get('/login', users.login) 9 | app.get('/signup', users.signup) 10 | app.get('/logout', users.logout) 11 | app.post('/users', users.create) 12 | app.post('/users/session', passport.authenticate('local', {failureRedirect: '/login', failureFlash: 'Invalid email or password.'}), users.session) 13 | app.get('/users/:userId', users.show) 14 | app.get('/auth/facebook', passport.authenticate('facebook', { scope: [ 'email', 'user_about_me'], failureRedirect: '/login' }), users.signin) 15 | app.get('/auth/facebook/callback', passport.authenticate('facebook', { failureRedirect: '/login' }), users.authCallback) 16 | app.get('/auth/github', passport.authenticate('github', { failureRedirect: '/login' }), users.signin) 17 | app.get('/auth/github/callback', passport.authenticate('github', { failureRedirect: '/login' }), users.authCallback) 18 | app.get('/auth/twitter', passport.authenticate('twitter', { failureRedirect: '/login' }), users.signin) 19 | app.get('/auth/twitter/callback', passport.authenticate('twitter', { failureRedirect: '/login' }), users.authCallback) 20 | app.get('/auth/google', passport.authenticate('google', { failureRedirect: '/login', scope: 'https://www.google.com/m8/feeds' }), users.signin) 21 | app.get('/auth/google/callback', passport.authenticate('google', { failureRedirect: '/login', scope: 'https://www.google.com/m8/feeds' }), users.authCallback) 22 | 23 | app.param('userId', users.user) 24 | 25 | // article routes 26 | var articles = require('../app/controllers/articles') 27 | app.get('/articles', articles.index) 28 | app.get('/articles/new', auth.requiresLogin, articles.new) 29 | app.post('/articles', auth.requiresLogin, articles.create) 30 | app.get('/articles/:id', articles.show) 31 | app.get('/articles/:id/edit', auth.requiresLogin, auth.article.hasAuthorization, articles.edit) 32 | app.put('/articles/:id', auth.requiresLogin, auth.article.hasAuthorization, articles.update) 33 | app.del('/articles/:id', auth.requiresLogin, auth.article.hasAuthorization, articles.destroy) 34 | 35 | app.param('id', articles.article) 36 | 37 | // team routes 38 | var teams = require('../app/controllers/teams') 39 | app.get('/teams', teams.index) 40 | app.get('/teams/new', teams.new) 41 | app.post('/teams', teams.create) 42 | app.get('/teams/:teamId', teams.show) 43 | // app.get('/teams/:teamId/edit', auth.requiresLogin, auth.team.hasAuthorization, teams.edit) 44 | // app.put('/teams/:teamId', auth.requiresLogin, auth.team.hasAuthorization, teams.update) 45 | // app.del('/teams/:teamId', auth.requiresLogin, auth.team.hasAuthorization, teams.destroy) 46 | 47 | app.param('teamId', teams.team) 48 | 49 | // league routes 50 | var leagues = require('../app/controllers/leagues') 51 | app.get('/leagues', leagues.index) 52 | 53 | app.param('leagueId', leagues.league) 54 | 55 | // home route 56 | app.get('/', teams.index) 57 | 58 | // comment routes 59 | var comments = require('../app/controllers/comments') 60 | app.post('/articles/:id/comments', auth.requiresLogin, comments.create) 61 | 62 | // tag routes 63 | var tags = require('../app/controllers/tags') 64 | app.get('/tags/:tag', tags.index) 65 | 66 | } 67 | -------------------------------------------------------------------------------- /app/models/user.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var mongoose = require('mongoose') 7 | , Schema = mongoose.Schema 8 | , crypto = require('crypto') 9 | , _ = require('underscore') 10 | , authTypes = ['github', 'twitter', 'facebook', 'google'] 11 | 12 | /** 13 | * User Schema 14 | */ 15 | 16 | var UserSchema = new Schema({ 17 | name: String, 18 | email: String, 19 | username: String, 20 | provider: String, 21 | hashed_password: String, 22 | salt: String, 23 | facebook: {}, 24 | twitter: {}, 25 | github: {}, 26 | google: {} 27 | }) 28 | 29 | /** 30 | * Virtuals 31 | */ 32 | 33 | UserSchema 34 | .virtual('password') 35 | .set(function(password) { 36 | this._password = password 37 | this.salt = this.makeSalt() 38 | this.hashed_password = this.encryptPassword(password) 39 | }) 40 | .get(function() { return this._password }) 41 | 42 | /** 43 | * Validations 44 | */ 45 | 46 | var validatePresenceOf = function (value) { 47 | return value && value.length 48 | } 49 | 50 | // the below 4 validations only apply if you are signing up traditionally 51 | 52 | UserSchema.path('name').validate(function (name) { 53 | // if you are authenticating by any of the oauth strategies, don't validate 54 | if (authTypes.indexOf(this.provider) !== -1) return true 55 | return name.length 56 | }, 'Name cannot be blank') 57 | 58 | UserSchema.path('email').validate(function (email) { 59 | // if you are authenticating by any of the oauth strategies, don't validate 60 | if (authTypes.indexOf(this.provider) !== -1) return true 61 | return email.length 62 | }, 'Email cannot be blank') 63 | 64 | UserSchema.path('username').validate(function (username) { 65 | // if you are authenticating by any of the oauth strategies, don't validate 66 | if (authTypes.indexOf(this.provider) !== -1) return true 67 | return username.length 68 | }, 'Username cannot be blank') 69 | 70 | UserSchema.path('hashed_password').validate(function (hashed_password) { 71 | // if you are authenticating by any of the oauth strategies, don't validate 72 | if (authTypes.indexOf(this.provider) !== -1) return true 73 | return hashed_password.length 74 | }, 'Password cannot be blank') 75 | 76 | 77 | /** 78 | * Pre-save hook 79 | */ 80 | 81 | UserSchema.pre('save', function(next) { 82 | if (!this.isNew) return next() 83 | 84 | if (!validatePresenceOf(this.password) 85 | && authTypes.indexOf(this.provider) === -1) 86 | next(new Error('Invalid password')) 87 | else 88 | next() 89 | }) 90 | 91 | /** 92 | * Methods 93 | */ 94 | 95 | UserSchema.methods = { 96 | 97 | /** 98 | * Authenticate - check if the passwords are the same 99 | * 100 | * @param {String} plainText 101 | * @return {Boolean} 102 | * @api public 103 | */ 104 | 105 | authenticate: function(plainText) { 106 | return this.encryptPassword(plainText) === this.hashed_password 107 | }, 108 | 109 | /** 110 | * Make salt 111 | * 112 | * @return {String} 113 | * @api public 114 | */ 115 | 116 | makeSalt: function() { 117 | return Math.round((new Date().valueOf() * Math.random())) + '' 118 | }, 119 | 120 | /** 121 | * Encrypt password 122 | * 123 | * @param {String} password 124 | * @return {String} 125 | * @api public 126 | */ 127 | 128 | encryptPassword: function(password) { 129 | if (!password) return '' 130 | return crypto.createHmac('sha1', this.salt).update(password).digest('hex') 131 | } 132 | } 133 | 134 | mongoose.model('User', UserSchema) 135 | -------------------------------------------------------------------------------- /app/models/article.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var mongoose = require('mongoose') 6 | , Imager = require('imager') 7 | , env = process.env.NODE_ENV || 'development' 8 | , config = require('../../config/config')[env] 9 | , imagerConfig = require(config.root + '/config/imager.js') 10 | , Schema = mongoose.Schema 11 | 12 | /** 13 | * Getters 14 | */ 15 | 16 | var getTags = function (tags) { 17 | return tags.join(',') 18 | } 19 | 20 | /** 21 | * Setters 22 | */ 23 | 24 | var setTags = function (tags) { 25 | return tags.split(',') 26 | } 27 | 28 | /** 29 | * Article Schema 30 | */ 31 | 32 | var ArticleSchema = new Schema({ 33 | title: {type : String, default : '', trim : true}, 34 | body: {type : String, default : '', trim : true}, 35 | user: {type : Schema.ObjectId, ref : 'User'}, 36 | comments: [{ 37 | body: { type : String, default : '' }, 38 | user: { type : Schema.ObjectId, ref : 'User' }, 39 | createdAt: { type : Date, default : Date.now } 40 | }], 41 | tags: {type: [], get: getTags, set: setTags}, 42 | image: { 43 | cdnUri: String, 44 | files: [] 45 | }, 46 | createdAt : {type : Date, default : Date.now} 47 | }) 48 | 49 | /** 50 | * Validations 51 | */ 52 | 53 | ArticleSchema.path('title').validate(function (title) { 54 | return title.length > 0 55 | }, 'Article title cannot be blank') 56 | 57 | ArticleSchema.path('body').validate(function (body) { 58 | return body.length > 0 59 | }, 'Article body cannot be blank') 60 | 61 | /** 62 | * Pre-remove hook 63 | */ 64 | 65 | ArticleSchema.pre('remove', function (next) { 66 | var imager = new Imager(imagerConfig, 'S3') 67 | var files = this.image.files 68 | 69 | // if there are files associated with the item, remove from the cloud too 70 | imager.remove(files, function (err) { 71 | if (err) return next(err) 72 | }, 'article') 73 | 74 | next() 75 | }) 76 | 77 | /** 78 | * Methods 79 | */ 80 | 81 | ArticleSchema.methods = { 82 | 83 | /** 84 | * Save article and upload image 85 | * 86 | * @param {Object} images 87 | * @param {Function} cb 88 | * @api private 89 | */ 90 | 91 | uploadAndSave: function (images, cb) { 92 | if (!images || !images.length) return this.save(cb) 93 | 94 | var imager = new Imager(imagerConfig, 'S3') 95 | var self = this 96 | 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 | * Add comment 108 | * 109 | * @param {User} user 110 | * @param {Object} comment 111 | * @param {Function} cb 112 | * @api private 113 | */ 114 | 115 | addComment: function (user, comment, cb) { 116 | var notify = require('../mailer/notify') 117 | 118 | this.comments.push({ 119 | body: comment.body, 120 | user: user._id 121 | }) 122 | 123 | notify.comment({ 124 | article: this, 125 | currentUser: user, 126 | comment: comment.body 127 | }) 128 | 129 | this.save(cb) 130 | } 131 | 132 | } 133 | 134 | /** 135 | * Statics 136 | */ 137 | 138 | ArticleSchema.statics = { 139 | 140 | /** 141 | * Find article by id 142 | * 143 | * @param {ObjectId} id 144 | * @param {Function} cb 145 | * @api private 146 | */ 147 | 148 | load: function (id, cb) { 149 | this.findOne({ _id : id }) 150 | .populate('user', 'name email') 151 | .populate('comments.user') 152 | .exec(cb) 153 | }, 154 | 155 | /** 156 | * List articles 157 | * 158 | * @param {Object} options 159 | * @param {Function} cb 160 | * @api private 161 | */ 162 | 163 | list: function (options, cb) { 164 | var criteria = options.criteria || {} 165 | 166 | this.find(criteria) 167 | .populate('user', 'name') 168 | .sort({'createdAt': -1}) // sort by date 169 | .limit(options.perPage) 170 | .skip(options.perPage * options.page) 171 | .exec(cb) 172 | } 173 | 174 | } 175 | 176 | mongoose.model('Article', ArticleSchema) 177 | -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | 2 | var mongoose = require('mongoose') 3 | , LocalStrategy = require('passport-local').Strategy 4 | , TwitterStrategy = require('passport-twitter').Strategy 5 | , FacebookStrategy = require('passport-facebook').Strategy 6 | , GitHubStrategy = require('passport-github').Strategy 7 | , GoogleStrategy = require('passport-google-oauth').Strategy 8 | , User = mongoose.model('User') 9 | 10 | 11 | module.exports = function (passport, config) { 12 | // require('./initializer') 13 | 14 | // serialize sessions 15 | passport.serializeUser(function(user, done) { 16 | done(null, user.id) 17 | }) 18 | 19 | passport.deserializeUser(function(id, done) { 20 | User.findOne({ _id: id }, function (err, user) { 21 | done(err, user) 22 | }) 23 | }) 24 | 25 | // use local strategy 26 | passport.use(new LocalStrategy({ 27 | usernameField: 'email', 28 | passwordField: 'password' 29 | }, 30 | function(email, password, done) { 31 | User.findOne({ email: email }, function (err, user) { 32 | if (err) { return done(err) } 33 | if (!user) { 34 | return done(null, false, { message: 'Unknown user' }) 35 | } 36 | if (!user.authenticate(password)) { 37 | return done(null, false, { message: 'Invalid password' }) 38 | } 39 | return done(null, user) 40 | }) 41 | } 42 | )) 43 | 44 | // use twitter strategy 45 | passport.use(new TwitterStrategy({ 46 | consumerKey: config.twitter.clientID 47 | , consumerSecret: config.twitter.clientSecret 48 | , callbackURL: config.twitter.callbackURL 49 | }, 50 | function(token, tokenSecret, profile, done) { 51 | User.findOne({ 'twitter.id': profile.id }, function (err, user) { 52 | if (err) { return done(err) } 53 | if (!user) { 54 | user = new User({ 55 | name: profile.displayName 56 | , username: profile.username 57 | , provider: 'twitter' 58 | , twitter: profile._json 59 | }) 60 | user.save(function (err) { 61 | if (err) console.log(err) 62 | return done(err, user) 63 | }) 64 | } 65 | else { 66 | return done(err, user) 67 | } 68 | }) 69 | } 70 | )) 71 | 72 | // use facebook strategy 73 | passport.use(new FacebookStrategy({ 74 | clientID: config.facebook.clientID 75 | , clientSecret: config.facebook.clientSecret 76 | , callbackURL: config.facebook.callbackURL 77 | }, 78 | function(accessToken, refreshToken, profile, done) { 79 | User.findOne({ 'facebook.id': profile.id }, function (err, user) { 80 | if (err) { return done(err) } 81 | if (!user) { 82 | user = new User({ 83 | name: profile.displayName 84 | , email: profile.emails[0].value 85 | , username: profile.username 86 | , provider: 'facebook' 87 | , facebook: profile._json 88 | }) 89 | user.save(function (err) { 90 | if (err) console.log(err) 91 | return done(err, user) 92 | }) 93 | } 94 | else { 95 | return done(err, user) 96 | } 97 | }) 98 | } 99 | )) 100 | 101 | // use github strategy 102 | passport.use(new GitHubStrategy({ 103 | clientID: config.github.clientID, 104 | clientSecret: config.github.clientSecret, 105 | callbackURL: config.github.callbackURL 106 | }, 107 | function(accessToken, refreshToken, profile, done) { 108 | User.findOne({ 'github.id': profile.id }, function (err, user) { 109 | if (!user) { 110 | user = new User({ 111 | name: profile.displayName 112 | , email: profile.emails[0].value 113 | , username: profile.username 114 | , provider: 'github' 115 | , github: profile._json 116 | }) 117 | user.save(function (err) { 118 | if (err) console.log(err) 119 | return done(err, user) 120 | }) 121 | } else { 122 | return done(err, user) 123 | } 124 | }) 125 | } 126 | )) 127 | 128 | // use google strategy 129 | passport.use(new GoogleStrategy({ 130 | consumerKey: config.google.clientID, 131 | consumerSecret: config.google.clientSecret, 132 | callbackURL: config.google.callbackURL 133 | }, 134 | function(accessToken, refreshToken, profile, done) { 135 | User.findOne({ 'google.id': profile.id }, function (err, user) { 136 | if (!user) { 137 | user = new User({ 138 | name: profile.displayName 139 | , email: profile.emails[0].value 140 | , username: profile.username 141 | , provider: 'google' 142 | , google: profile._json 143 | }) 144 | user.save(function (err) { 145 | if (err) console.log(err) 146 | return done(err, user) 147 | }) 148 | } else { 149 | return done(err, user) 150 | } 151 | }) 152 | } 153 | )); 154 | } 155 | -------------------------------------------------------------------------------- /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 | 14 | var count, cookies 15 | 16 | /** 17 | * Articles tests 18 | */ 19 | 20 | describe('Articles', function () { 21 | before(function (done) { 22 | // create a user 23 | var user = new User({ 24 | email: 'foobar@example.com', 25 | name: 'Foo bar', 26 | username: 'foobar', 27 | password: 'foobar' 28 | }) 29 | user.save(done) 30 | }) 31 | 32 | describe('GET /articles', function () { 33 | it('should respond with Content-Type text/html', function (done) { 34 | request(app) 35 | .get('/articles') 36 | .expect('Content-Type', /html/) 37 | .expect(200) 38 | .expect(/List of Articles/) 39 | .end(done) 40 | }) 41 | }) 42 | 43 | describe('GET /articles/new', function () { 44 | context('When not logged in', function () { 45 | it('should redirect to /login', function (done) { 46 | request(app) 47 | .get('/articles/new') 48 | .expect('Content-Type', /plain/) 49 | .expect(302) 50 | .expect('Location', '/login') 51 | .expect(/Moved Temporarily/) 52 | .end(done) 53 | }) 54 | }) 55 | 56 | context('When logged in', function () { 57 | before(function (done) { 58 | // login the user 59 | request(app) 60 | .post('/users/session') 61 | .field('email', 'foobar@example.com') 62 | .field('password', 'foobar') 63 | .end(function (err, res) { 64 | // store the cookie 65 | cookies = res.headers['set-cookie'].pop().split(';')[0]; 66 | done() 67 | }) 68 | }) 69 | 70 | it('should respond with Content-Type text/html', function (done) { 71 | var req = request(app).get('/articles/new') 72 | req.cookies = cookies 73 | req 74 | .expect('Content-Type', /html/) 75 | .expect(200) 76 | .expect(/New Article/) 77 | .end(done) 78 | }) 79 | }) 80 | }) 81 | 82 | describe('POST /articles', function () { 83 | context('When not logged in', function () { 84 | it('should redirect to /login', function (done) { 85 | request(app) 86 | .get('/articles/new') 87 | .expect('Content-Type', /plain/) 88 | .expect(302) 89 | .expect('Location', '/login') 90 | .expect(/Moved Temporarily/) 91 | .end(done) 92 | }) 93 | }) 94 | 95 | context('When logged in', function () { 96 | before(function (done) { 97 | // login the user 98 | request(app) 99 | .post('/users/session') 100 | .field('email', 'foobar@example.com') 101 | .field('password', 'foobar') 102 | .end(function (err, res) { 103 | // store the cookie 104 | cookies = res.headers['set-cookie'].pop().split(';')[0]; 105 | done() 106 | }) 107 | }) 108 | 109 | describe('Invalid parameters', function () { 110 | before(function (done) { 111 | Article.count(function (err, cnt) { 112 | count = cnt 113 | done() 114 | }) 115 | }) 116 | 117 | it('should respond with error', function (done) { 118 | var req = request(app).post('/articles') 119 | req.cookies = cookies 120 | req 121 | .field('title', '') 122 | .field('body', 'foo') 123 | .expect('Content-Type', /html/) 124 | .expect(200) 125 | .expect(/Article title cannot be blank/) 126 | .end(done) 127 | }) 128 | 129 | it('should not save to the database', function (done) { 130 | Article.count(function (err, cnt) { 131 | count.should.equal(cnt) 132 | done() 133 | }) 134 | }) 135 | }) 136 | 137 | describe('Valid parameters', function () { 138 | before(function (done) { 139 | Article.count(function (err, cnt) { 140 | count = cnt 141 | done() 142 | }) 143 | }) 144 | 145 | it('should redirect to the new article page', function (done) { 146 | var req = request(app).post('/articles') 147 | req.cookies = cookies 148 | req 149 | .field('title', 'foo') 150 | .field('body', 'bar') 151 | .expect('Content-Type', /plain/) 152 | .expect('Location', /\/articles\//) 153 | .expect(302) 154 | .expect(/Moved Temporarily/) 155 | .end(done) 156 | }) 157 | 158 | it('should insert a record to the database', function (done) { 159 | Article.count(function (err, cnt) { 160 | cnt.should.equal(count + 1) 161 | done() 162 | }) 163 | }) 164 | 165 | it('should save the article to the database', function (done) { 166 | Article 167 | .findOne({ title: 'foo'}) 168 | .populate('user') 169 | .exec(function (err, article) { 170 | should.not.exist(err) 171 | article.should.be.an.instanceOf(Article) 172 | article.title.should.equal('foo') 173 | article.body.should.equal('bar') 174 | article.user.email.should.equal('foobar@example.com') 175 | article.user.name.should.equal('Foo bar') 176 | done() 177 | }) 178 | }) 179 | }) 180 | }) 181 | }) 182 | 183 | after(function (done) { 184 | require('./helper').clearDb(done) 185 | }) 186 | }) 187 | -------------------------------------------------------------------------------- /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;ilabel{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}.modal{position:absolute;top:10px;right:10px;left:10px;width:auto;margin:0}.modal.fade.in{top:auto}.modal-header .close{padding:10px;margin:-10px}.carousel-caption{position:static}}@media(max-width:767px){body{padding-right:20px;padding-left:20px}.navbar-fixed-top,.navbar-fixed-bottom{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}[class*="span"],.row-fluid [class*="span"]{display:block;float:none;width:auto;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:28px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-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}}@media(min-width:768px) and (max-width:979px){.row{margin-left:-20px;*zoom:1}.row:before,.row:after{display:table;content:""}.row:after{clear:both}[class*="span"]{float:left;margin-left:20px}.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;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:28px;margin-left:2.762430939%;*margin-left:2.709239449638298%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .span12{width:99.999999993%;*width:99.9468085036383%}.row-fluid .span11{width:91.436464082%;*width:91.38327259263829%}.row-fluid .span10{width:82.87292817100001%;*width:82.8197366816383%}.row-fluid .span9{width:74.30939226%;*width:74.25620077063829%}.row-fluid .span8{width:65.74585634900001%;*width:65.6926648596383%}.row-fluid .span7{width:57.182320438000005%;*width:57.129128948638304%}.row-fluid .span6{width:48.618784527%;*width:48.5655930376383%}.row-fluid .span5{width:40.055248616%;*width:40.0020571266383%}.row-fluid .span4{width:31.491712705%;*width:31.4385212156383%}.row-fluid .span3{width:22.928176794%;*width:22.874985304638297%}.row-fluid .span2{width:14.364640883%;*width:14.311449393638298%}.row-fluid .span1{width:5.801104972%;*width:5.747913482638298%}input,textarea,.uneditable-input{margin-left:0}input.span12,textarea.span12,.uneditable-input.span12{width:714px}input.span11,textarea.span11,.uneditable-input.span11{width:652px}input.span10,textarea.span10,.uneditable-input.span10{width:590px}input.span9,textarea.span9,.uneditable-input.span9{width:528px}input.span8,textarea.span8,.uneditable-input.span8{width:466px}input.span7,textarea.span7,.uneditable-input.span7{width:404px}input.span6,textarea.span6,.uneditable-input.span6{width:342px}input.span5,textarea.span5,.uneditable-input.span5{width:280px}input.span4,textarea.span4,.uneditable-input.span4{width:218px}input.span3,textarea.span3,.uneditable-input.span3{width:156px}input.span2,textarea.span2,.uneditable-input.span2{width:94px}input.span1,textarea.span1,.uneditable-input.span1{width:32px}}@media(min-width:1200px){.row{margin-left:-30px;*zoom:1}.row:before,.row:after{display:table;content:""}.row:after{clear:both}[class*="span"]{float:left;margin-left:30px}.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;content:""}.row-fluid:after{clear:both}.row-fluid [class*="span"]{display:block;float:left;width:100%;min-height:28px;margin-left:2.564102564%;*margin-left:2.510911074638298%;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;-ms-box-sizing:border-box;box-sizing:border-box}.row-fluid [class*="span"]:first-child{margin-left:0}.row-fluid .span12{width:100%;*width:99.94680851063829%}.row-fluid .span11{width:91.45299145300001%;*width:91.3997999636383%}.row-fluid .span10{width:82.905982906%;*width:82.8527914166383%}.row-fluid .span9{width:74.358974359%;*width:74.30578286963829%}.row-fluid .span8{width:65.81196581200001%;*width:65.7587743226383%}.row-fluid .span7{width:57.264957265%;*width:57.2117657756383%}.row-fluid .span6{width:48.717948718%;*width:48.6647572286383%}.row-fluid .span5{width:40.170940171000005%;*width:40.117748681638304%}.row-fluid .span4{width:31.623931624%;*width:31.5707401346383%}.row-fluid .span3{width:23.076923077%;*width:23.0237315876383%}.row-fluid .span2{width:14.529914530000001%;*width:14.4767230406383%}.row-fluid .span1{width:5.982905983%;*width:5.929714493638298%}input,textarea,.uneditable-input{margin-left:0}input.span12,textarea.span12,.uneditable-input.span12{width:1160px}input.span11,textarea.span11,.uneditable-input.span11{width:1060px}input.span10,textarea.span10,.uneditable-input.span10{width:960px}input.span9,textarea.span9,.uneditable-input.span9{width:860px}input.span8,textarea.span8,.uneditable-input.span8{width:760px}input.span7,textarea.span7,.uneditable-input.span7{width:660px}input.span6,textarea.span6,.uneditable-input.span6{width:560px}input.span5,textarea.span5,.uneditable-input.span5{width:460px}input.span4,textarea.span4,.uneditable-input.span4{width:360px}input.span3,textarea.span3,.uneditable-input.span3{width:260px}input.span2,textarea.span2,.uneditable-input.span2{width:160px}input.span1,textarea.span1,.uneditable-input.span1{width:60px}.thumbnails{margin-left:-30px}.thumbnails>li{margin-left:30px}.row-fluid .thumbnails{margin-left:0}}@media(max-width:979px){body{padding-top:0}.navbar-fixed-top,.navbar-fixed-bottom{position:static}.navbar-fixed-top{margin-bottom:18px}.navbar-fixed-bottom{margin-top:18px}.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 9px}.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:#999;text-shadow:none}.nav-collapse .nav>li>a,.nav-collapse .dropdown-menu a{padding:6px 15px;font-weight:bold;color:#999;-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 .dropdown-menu a:hover{background-color:#222}.nav-collapse.in .btn-group{padding:0;margin-top:5px}.nav-collapse .dropdown-menu{position:static;top:auto;left:auto;display:block;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 .dropdown-menu:before,.nav-collapse .dropdown-menu:after{display:none}.nav-collapse .dropdown-menu .divider{display:none}.nav-collapse .navbar-form,.nav-collapse .navbar-search{float:none;padding:9px 15px;margin:9px 0;border-top:1px solid #222;border-bottom:1px solid #222;-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 .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/bootbox.js: -------------------------------------------------------------------------------- 1 | /** 2 | * bootbox.js v2.3.1 3 | * 4 | * The MIT License 5 | * 6 | * Copyright (C) 2011-2012 by Nick Payne 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE 25 | */ 26 | var bootbox = window.bootbox || (function($) { 27 | 28 | var _locale = 'en', 29 | _defaultLocale = 'en', 30 | _animate = true, 31 | _icons = {}, 32 | /* last var should always be the public object we'll return */ 33 | that = {}; 34 | 35 | /** 36 | * standard locales. Please add more according to ISO 639-1 standard. Multiple language variants are 37 | * unlikely to be required. If this gets too large it can be split out into separate JS files. 38 | */ 39 | var _locales = { 40 | 'en' : { 41 | OK : 'OK', 42 | CANCEL : 'Cancel', 43 | CONFIRM : 'OK' 44 | }, 45 | 'fr' : { 46 | OK : 'OK', 47 | CANCEL : 'Annuler', 48 | CONFIRM : 'D\'accord' 49 | }, 50 | 'de' : { 51 | OK : 'OK', 52 | CANCEL : 'Abbrechen', 53 | CONFIRM : 'Akzeptieren' 54 | }, 55 | 'es' : { 56 | OK : 'OK', 57 | CANCEL : 'Cancelar', 58 | CONFIRM : 'Aceptar' 59 | }, 60 | 'br' : { 61 | OK : 'OK', 62 | CANCEL : 'Cancelar', 63 | CONFIRM : 'Sim' 64 | }, 65 | 'nl' : { 66 | OK : 'OK', 67 | CANCEL : 'Annuleren', 68 | CONFIRM : 'Accepteren' 69 | }, 70 | 'ru' : { 71 | OK : 'OK', 72 | CANCEL : 'Отмена', 73 | CONFIRM : 'Применить' 74 | }, 75 | 'it' : { 76 | OK : 'OK', 77 | CANCEL : 'Annulla', 78 | CONFIRM : 'Conferma' 79 | } 80 | }; 81 | 82 | function _translate(str, locale) { 83 | // we assume if no target locale is probided then we should take it from current setting 84 | if (locale == null) { 85 | locale = _locale; 86 | } 87 | if (typeof _locales[locale][str] == 'string') { 88 | return _locales[locale][str]; 89 | } 90 | 91 | // if we couldn't find a lookup then try and fallback to a default translation 92 | 93 | if (locale != _defaultLocale) { 94 | return _translate(str, _defaultLocale); 95 | } 96 | 97 | // if we can't do anything then bail out with whatever string was passed in - last resort 98 | return str; 99 | } 100 | 101 | that.setLocale = function(locale) { 102 | for (var i in _locales) { 103 | if (i == locale) { 104 | _locale = locale; 105 | return; 106 | } 107 | } 108 | throw new Error('Invalid locale: '+locale); 109 | } 110 | 111 | that.addLocale = function(locale, translations) { 112 | if (typeof _locales[locale] == 'undefined') { 113 | _locales[locale] = {}; 114 | } 115 | for (var str in translations) { 116 | _locales[locale][str] = translations[str]; 117 | } 118 | } 119 | 120 | that.setIcons = function(icons) { 121 | _icons = icons; 122 | if (typeof _icons !== 'object' || _icons == null) { 123 | _icons = {}; 124 | } 125 | } 126 | 127 | that.alert = function(/*str, label, cb*/) { 128 | var str = "", 129 | label = _translate('OK'), 130 | cb = null; 131 | 132 | switch (arguments.length) { 133 | case 1: 134 | // no callback, default button label 135 | str = arguments[0]; 136 | break; 137 | case 2: 138 | // callback *or* custom button label dependent on type 139 | str = arguments[0]; 140 | if (typeof arguments[1] == 'function') { 141 | cb = arguments[1]; 142 | } else { 143 | label = arguments[1]; 144 | } 145 | break; 146 | case 3: 147 | // callback and custom button label 148 | str = arguments[0]; 149 | label = arguments[1]; 150 | cb = arguments[2]; 151 | break; 152 | default: 153 | throw new Error("Incorrect number of arguments: expected 1-3"); 154 | break; 155 | } 156 | 157 | return that.dialog(str, { 158 | "label": label, 159 | "icon" : _icons.OK, 160 | "callback": cb 161 | }, { 162 | "onEscape": cb 163 | }); 164 | } 165 | 166 | that.confirm = function(/*str, labelCancel, labelOk, cb*/) { 167 | var str = "", 168 | labelCancel = _translate('CANCEL'), 169 | labelOk = _translate('CONFIRM'), 170 | cb = null; 171 | 172 | switch (arguments.length) { 173 | case 1: 174 | str = arguments[0]; 175 | break; 176 | case 2: 177 | str = arguments[0]; 178 | if (typeof arguments[1] == 'function') { 179 | cb = arguments[1]; 180 | } else { 181 | labelCancel = arguments[1]; 182 | } 183 | break; 184 | case 3: 185 | str = arguments[0]; 186 | labelCancel = arguments[1]; 187 | if (typeof arguments[2] == 'function') { 188 | cb = arguments[2]; 189 | } else { 190 | labelOk = arguments[2]; 191 | } 192 | break; 193 | case 4: 194 | str = arguments[0]; 195 | labelCancel = arguments[1]; 196 | labelOk = arguments[2]; 197 | cb = arguments[3]; 198 | break; 199 | default: 200 | throw new Error("Incorrect number of arguments: expected 1-4"); 201 | break; 202 | } 203 | 204 | return that.dialog(str, [{ 205 | "label": labelCancel, 206 | "icon" : _icons.CANCEL, 207 | "callback": function() { 208 | if (typeof cb == 'function') { 209 | cb(false); 210 | } 211 | } 212 | }, { 213 | "label": labelOk, 214 | "icon" : _icons.CONFIRM, 215 | "callback": function() { 216 | if (typeof cb == 'function') { 217 | cb(true); 218 | } 219 | } 220 | }]); 221 | } 222 | 223 | that.prompt = function(/*str, labelCancel, labelOk, cb*/) { 224 | var str = "", 225 | labelCancel = _translate('CANCEL'), 226 | labelOk = _translate('CONFIRM'), 227 | cb = null; 228 | 229 | switch (arguments.length) { 230 | case 1: 231 | str = arguments[0]; 232 | break; 233 | case 2: 234 | str = arguments[0]; 235 | if (typeof arguments[1] == 'function') { 236 | cb = arguments[1]; 237 | } else { 238 | labelCancel = arguments[1]; 239 | } 240 | break; 241 | case 3: 242 | str = arguments[0]; 243 | labelCancel = arguments[1]; 244 | if (typeof arguments[2] == 'function') { 245 | cb = arguments[2]; 246 | } else { 247 | labelOk = arguments[2]; 248 | } 249 | break; 250 | case 4: 251 | str = arguments[0]; 252 | labelCancel = arguments[1]; 253 | labelOk = arguments[2]; 254 | cb = arguments[3]; 255 | break; 256 | default: 257 | throw new Error("Incorrect number of arguments: expected 1-4"); 258 | break; 259 | } 260 | 261 | var header = str; 262 | 263 | // let's keep a reference to the form object for later 264 | var form = $("
"); 265 | form.append(""); 266 | 267 | var div = that.dialog(form, [{ 268 | "label": labelCancel, 269 | "icon" : _icons.CANCEL, 270 | "callback": function() { 271 | if (typeof cb == 'function') { 272 | cb(null); 273 | } 274 | } 275 | }, { 276 | "label": labelOk, 277 | "icon" : _icons.CONFIRM, 278 | "callback": function() { 279 | if (typeof cb == 'function') { 280 | cb( 281 | form.find("input[type=text]").val() 282 | ); 283 | } 284 | } 285 | }], { 286 | "header": header 287 | }); 288 | 289 | div.on("shown", function() { 290 | form.find("input[type=text]").focus(); 291 | 292 | // ensure that submitting the form (e.g. with the enter key) 293 | // replicates the behaviour of a normal prompt() 294 | form.on("submit", function(e) { 295 | e.preventDefault(); 296 | div.find(".btn-primary").click(); 297 | }); 298 | }); 299 | 300 | return div; 301 | } 302 | 303 | that.modal = function(/*str, label, options*/) { 304 | var str; 305 | var label; 306 | var options; 307 | 308 | var defaultOptions = { 309 | "onEscape": null, 310 | "keyboard": true, 311 | "backdrop": true 312 | }; 313 | 314 | switch (arguments.length) { 315 | case 1: 316 | str = arguments[0]; 317 | break; 318 | case 2: 319 | str = arguments[0]; 320 | if (typeof arguments[1] == 'object') { 321 | options = arguments[1]; 322 | } else { 323 | label = arguments[1]; 324 | } 325 | break; 326 | case 3: 327 | str = arguments[0]; 328 | label = arguments[1]; 329 | options = arguments[2]; 330 | break; 331 | default: 332 | throw new Error("Incorrect number of arguments: expected 1-3"); 333 | break; 334 | } 335 | 336 | defaultOptions['header'] = label; 337 | 338 | if (typeof options == 'object') { 339 | options = $.extend(defaultOptions, options); 340 | } else { 341 | options = defaultOptions; 342 | } 343 | 344 | return that.dialog(str, [], options); 345 | } 346 | 347 | that.dialog = function(str, handlers, options) { 348 | var hideSource = null, 349 | buttons = "", 350 | callbacks = [], 351 | options = options || {}; 352 | 353 | // check for single object and convert to array if necessary 354 | if (handlers == null) { 355 | handlers = []; 356 | } else if (typeof handlers.length == 'undefined') { 357 | handlers = [handlers]; 358 | } 359 | 360 | var i = handlers.length; 361 | while (i--) { 362 | var label = null, 363 | _class = null, 364 | icon = '', 365 | callback = null; 366 | 367 | if (typeof handlers[i]['label'] == 'undefined' && 368 | typeof handlers[i]['class'] == 'undefined' && 369 | typeof handlers[i]['callback'] == 'undefined') { 370 | // if we've got nothing we expect, check for condensed format 371 | 372 | var propCount = 0, // condensed will only match if this == 1 373 | property = null; // save the last property we found 374 | 375 | // be nicer to count the properties without this, but don't think it's possible... 376 | for (var j in handlers[i]) { 377 | property = j; 378 | if (++propCount > 1) { 379 | // forget it, too many properties 380 | break; 381 | } 382 | } 383 | 384 | if (propCount == 1 && typeof handlers[i][j] == 'function') { 385 | // matches condensed format of label -> function 386 | handlers[i]['label'] = property; 387 | handlers[i]['callback'] = handlers[i][j]; 388 | } 389 | } 390 | 391 | if (typeof handlers[i]['callback']== 'function') { 392 | callback = handlers[i]['callback']; 393 | } 394 | 395 | if (handlers[i]['class']) { 396 | _class = handlers[i]['class']; 397 | } else if (i == handlers.length -1 && handlers.length <= 2) { 398 | // always add a primary to the main option in a two-button dialog 399 | _class = 'btn-primary'; 400 | } 401 | 402 | if (handlers[i]['label']) { 403 | label = handlers[i]['label']; 404 | } else { 405 | label = "Option "+(i+1); 406 | } 407 | 408 | if (handlers[i]['icon']) { 409 | icon = " "; 410 | } 411 | 412 | buttons += "
"+icon+""+label+""; 413 | 414 | callbacks[i] = callback; 415 | } 416 | 417 | var parts = [""); 436 | 437 | var div = $(parts.join("\n")); 438 | 439 | // check whether we should fade in/out 440 | var shouldFade = (typeof options.animate === 'undefined') ? _animate : options.animate; 441 | 442 | if (shouldFade) { 443 | div.addClass("fade"); 444 | } 445 | 446 | // now we've built up the div properly we can inject the content whether it was a string or a jQuery object 447 | $(".modal-body", div).html(str); 448 | 449 | div.bind('hidden', function() { 450 | div.remove(); 451 | }); 452 | 453 | div.bind('hide', function() { 454 | if (hideSource == 'escape' && 455 | typeof options.onEscape == 'function') { 456 | options.onEscape(); 457 | } 458 | }); 459 | 460 | // hook into the modal's keyup trigger to check for the escape key 461 | $(document).bind('keyup.modal', function ( e ) { 462 | if (e.which == 27) { 463 | hideSource = 'escape'; 464 | } 465 | }); 466 | 467 | // well, *if* we have a primary - give the last dom element (first displayed) focus 468 | div.bind('shown', function() { 469 | $("a.btn-primary:last", div).focus(); 470 | }); 471 | 472 | // wire up button handlers 473 | div.on('click', '.modal-footer a, a.close', function(e) { 474 | var handler = $(this).data("handler"), 475 | cb = callbacks[handler], 476 | hideModal = null; 477 | 478 | if (typeof cb == 'function') { 479 | hideModal = cb(); 480 | } 481 | if (hideModal !== false){ 482 | e.preventDefault(); 483 | hideSource = 'button'; 484 | div.modal("hide"); 485 | } 486 | }); 487 | 488 | if (options.keyboard == null) { 489 | options.keyboard = (typeof options.onEscape == 'function'); 490 | } 491 | 492 | $("body").append(div); 493 | 494 | div.modal({ 495 | "backdrop" : options.backdrop || true, 496 | "keyboard" : options.keyboard 497 | }); 498 | 499 | return div; 500 | } 501 | 502 | that.hideAll = function() { 503 | $(".bootbox").modal("hide"); 504 | } 505 | 506 | that.animate = function(animate) { 507 | _animate = animate; 508 | } 509 | 510 | return that; 511 | })( window.jQuery ); 512 | -------------------------------------------------------------------------------- /public/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap.js by @fat & @mdo 3 | * Copyright 2012 Twitter, Inc. 4 | * http://www.apache.org/licenses/LICENSE-2.0.txt 5 | */ 6 | !function(a){a(function(){"use strict",a.support.transition=function(){var a=function(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd",msTransition:"MSTransitionEnd",transition:"transitionend"},c;for(c in b)if(a.style[c]!==undefined)return b[c]}();return a&&{end:a}}()})}(window.jQuery),!function(a){"use strict";var b='[data-dismiss="alert"]',c=function(c){a(c).on("click",b,this.close)};c.prototype.close=function(b){function f(){e.trigger("closed").remove()}var c=a(this),d=c.attr("data-target"),e;d||(d=c.attr("href"),d=d&&d.replace(/.*(?=#[^\s]*$)/,"")),e=a(d),b&&b.preventDefault(),e.length||(e=c.hasClass("alert")?c:c.parent()),e.trigger(b=a.Event("close"));if(b.isDefaultPrevented())return;e.removeClass("in"),a.support.transition&&e.hasClass("fade")?e.on(a.support.transition.end,f):f()},a.fn.alert=function(b){return this.each(function(){var d=a(this),e=d.data("alert");e||d.data("alert",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.alert.Constructor=c,a(function(){a("body").on("click.alert.data-api",b,c.prototype.close)})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.button.defaults,c)};b.prototype.setState=function(a){var b="disabled",c=this.$element,d=c.data(),e=c.is("input")?"val":"html";a+="Text",d.resetText||c.data("resetText",c[e]()),c[e](d[a]||this.options[a]),setTimeout(function(){a=="loadingText"?c.addClass(b).attr(b,b):c.removeClass(b).removeAttr(b)},0)},b.prototype.toggle=function(){var a=this.$element.parent('[data-toggle="buttons-radio"]');a&&a.find(".active").removeClass("active"),this.$element.toggleClass("active")},a.fn.button=function(c){return this.each(function(){var d=a(this),e=d.data("button"),f=typeof c=="object"&&c;e||d.data("button",e=new b(this,f)),c=="toggle"?e.toggle():c&&e.setState(c)})},a.fn.button.defaults={loadingText:"loading..."},a.fn.button.Constructor=b,a(function(){a("body").on("click.button.data-api","[data-toggle^=button]",function(b){var c=a(b.target);c.hasClass("btn")||(c=c.closest(".btn")),c.button("toggle")})})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=c,this.options.slide&&this.slide(this.options.slide),this.options.pause=="hover"&&this.$element.on("mouseenter",a.proxy(this.pause,this)).on("mouseleave",a.proxy(this.cycle,this))};b.prototype={cycle:function(b){return b||(this.paused=!1),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},to:function(b){var c=this.$element.find(".active"),d=c.parent().children(),e=d.index(c),f=this;if(b>d.length-1||b<0)return;return this.sliding?this.$element.one("slid",function(){f.to(b)}):e==b?this.pause().cycle():this.slide(b>e?"next":"prev",a(d[b]))},pause:function(a){return a||(this.paused=!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(b,c){var d=this.$element.find(".active"),e=c||d[b](),f=this.interval,g=b=="next"?"left":"right",h=b=="next"?"first":"last",i=this,j=a.Event("slide");this.sliding=!0,f&&this.pause(),e=e.length?e:this.$element.find(".item")[h]();if(e.hasClass("active"))return;if(a.support.transition&&this.$element.hasClass("slide")){this.$element.trigger(j);if(j.isDefaultPrevented())return;e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),this.$element.one(a.support.transition.end,function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger("slid")},0)})}else{this.$element.trigger(j);if(j.isDefaultPrevented())return;d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger("slid")}return f&&this.cycle(),this}},a.fn.carousel=function(c){return this.each(function(){var d=a(this),e=d.data("carousel"),f=a.extend({},a.fn.carousel.defaults,typeof c=="object"&&c);e||d.data("carousel",e=new b(this,f)),typeof c=="number"?e.to(c):typeof c=="string"||(c=f.slide)?e[c]():f.interval&&e.cycle()})},a.fn.carousel.defaults={interval:5e3,pause:"hover"},a.fn.carousel.Constructor=b,a(function(){a("body").on("click.carousel.data-api","[data-slide]",function(b){var c=a(this),d,e=a(c.attr("data-target")||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,"")),f=!e.data("modal")&&a.extend({},e.data(),c.data());e.carousel(f),b.preventDefault()})})}(window.jQuery),!function(a){"use strict";var b=function(b,c){this.$element=a(b),this.options=a.extend({},a.fn.collapse.defaults,c),this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};b.prototype={constructor:b,dimension:function(){var a=this.$element.hasClass("width");return a?"width":"height"},show:function(){var b,c,d,e;if(this.transitioning)return;b=this.dimension(),c=a.camelCase(["scroll",b].join("-")),d=this.$parent&&this.$parent.find("> .accordion-group > .in");if(d&&d.length){e=d.data("collapse");if(e&&e.transitioning)return;d.collapse("hide"),e||d.data("collapse",null)}this.$element[b](0),this.transition("addClass",a.Event("show"),"shown"),this.$element[b](this.$element[0][c])},hide:function(){var b;if(this.transitioning)return;b=this.dimension(),this.reset(this.$element[b]()),this.transition("removeClass",a.Event("hide"),"hidden"),this.$element[b](0)},reset:function(a){var b=this.dimension();return this.$element.removeClass("collapse")[b](a||"auto")[0].offsetWidth,this.$element[a!==null?"addClass":"removeClass"]("collapse"),this},transition:function(b,c,d){var e=this,f=function(){c.type=="show"&&e.reset(),e.transitioning=0,e.$element.trigger(d)};this.$element.trigger(c);if(c.isDefaultPrevented())return;this.transitioning=1,this.$element[b]("in"),a.support.transition&&this.$element.hasClass("collapse")?this.$element.one(a.support.transition.end,f):f()},toggle:function(){this[this.$element.hasClass("in")?"hide":"show"]()}},a.fn.collapse=function(c){return this.each(function(){var d=a(this),e=d.data("collapse"),f=typeof c=="object"&&c;e||d.data("collapse",e=new b(this,f)),typeof c=="string"&&e[c]()})},a.fn.collapse.defaults={toggle:!0},a.fn.collapse.Constructor=b,a(function(){a("body").on("click.collapse.data-api","[data-toggle=collapse]",function(b){var c=a(this),d,e=c.attr("data-target")||b.preventDefault()||(d=c.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),f=a(e).data("collapse")?"toggle":c.data();a(e).collapse(f)})})}(window.jQuery),!function(a){function d(){a(b).parent().removeClass("open")}"use strict";var b='[data-toggle="dropdown"]',c=function(b){var c=a(b).on("click.dropdown.data-api",this.toggle);a("html").on("click.dropdown.data-api",function(){c.parent().removeClass("open")})};c.prototype={constructor:c,toggle:function(b){var c=a(this),e,f,g;if(c.is(".disabled, :disabled"))return;return f=c.attr("data-target"),f||(f=c.attr("href"),f=f&&f.replace(/.*(?=#[^\s]*$)/,"")),e=a(f),e.length||(e=c.parent()),g=e.hasClass("open"),d(),g||e.toggleClass("open"),!1}},a.fn.dropdown=function(b){return this.each(function(){var d=a(this),e=d.data("dropdown");e||d.data("dropdown",e=new c(this)),typeof b=="string"&&e[b].call(d)})},a.fn.dropdown.Constructor=c,a(function(){a("html").on("click.dropdown.data-api",d),a("body").on("click.dropdown",".dropdown form",function(a){a.stopPropagation()}).on("click.dropdown.data-api",b,c.prototype.toggle)})}(window.jQuery),!function(a){function c(){var b=this,c=setTimeout(function(){b.$element.off(a.support.transition.end),d.call(b)},500);this.$element.one(a.support.transition.end,function(){clearTimeout(c),d.call(b)})}function d(a){this.$element.hide().trigger("hidden"),e.call(this)}function e(b){var c=this,d=this.$element.hasClass("fade")?"fade":"";if(this.isShown&&this.options.backdrop){var e=a.support.transition&&d;this.$backdrop=a('