├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── admin.js ├── demo ├── models │ ├── client.js │ └── user.js ├── package.json ├── public │ └── admin │ │ └── css │ │ └── admin.css ├── server.js └── views │ └── admin │ ├── 404.jade │ ├── 500.jade │ ├── edit.jade │ ├── form.jade │ ├── includes │ ├── end.jade │ └── head.jade │ ├── index.jade │ ├── layouts │ └── default.jade │ └── list.jade └── package.json /.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 | node_modules 16 | 17 | .project 18 | .settings 19 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo 2 | node_modules 3 | .project 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 nodeminderjs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-django-admin 2 | ================= 3 | 4 | A Node.js admin site tool inspired by the Django framework admin site tool. 5 | 6 | It requires [express](http://expressjs.com/) and [mongoose](http://mongoosejs.com/). 7 | 8 | Theres is a demo app in the *demo* dir. There are example config files, models, views and stylesheets you can use in your own project. 9 | We are using [Bootstrap](http://getbootstrap.com/) in the demo app, but it's not required. 10 | 11 | ![list view](http://1.bp.blogspot.com/-y4VukFR63as/UpTzpdq1mQI/AAAAAAAAAGU/TTH2vpkz8dk/s1600/1.png) 12 | 13 | 14 | Disclaimer 15 | ---------- 16 | 17 | We are in an early development stage, so the project is not fully functional. 18 | 19 | There is absolutely no concern about backward compatibility until version 0.1.0 is reached. 20 | 21 | 22 | Install 23 | ------- 24 | 25 | $ npm install node-django-admin 26 | 27 | 28 | Models, views and stylesheets 29 | ----------------------------- 30 | 31 | You can copy the example files to your app and modify then: 32 | 33 | Copy the folder *demo/views/admin* to *your_app_views_dir/admin*. 34 | 35 | Copy the folder *demo/public/admin* to *your_public_dir/admin*. 36 | 37 | Example models are in the *demo/models* dir. 38 | 39 | The models must have the following requirements: 40 | 41 | * Define a virtual field for each ref field. 42 | * Define the static methods *load* and *list*. See the examples for more information. 43 | 44 | 45 | How to use 46 | ---------- 47 | 48 | Initialize the admin interface after initializing express: 49 | 50 | // Bootstrap admin site 51 | admin.config(app, mongoose, '/admin'); 52 | 53 | This must be called before configure express router: 54 | 55 | app.use(app.router); 56 | 57 | Register the mongoose models in the admin interface: 58 | 59 | Example: 60 | 61 | /** 62 | * Register the model in the admin interface 63 | */ 64 | 65 | admin.add({ 66 | path: 'users', 67 | model: 'User', 68 | list: [ 'name', 'email', 'client', 'role' ], 69 | edit: [ 'name', 'email', 'client', 'role' ], 70 | fields: { 71 | 'name': { 72 | header: 'Name' 73 | }, 74 | 'email': { 75 | header: 'Email', 76 | widget: 'email' 77 | }, 78 | 'client': { 79 | header: 'Client', 80 | widget: 'ref', 81 | model: 'Client', 82 | display: 'name', 83 | field: '_client' 84 | }, 85 | 'role': { 86 | header: 'Role', 87 | widget: 'sel', 88 | values: ['admin', 'client', 'staff'] 89 | } 90 | } 91 | }); 92 | 93 | ### Widgets 94 | 95 | * text (default) 96 | * email 97 | * ref 98 | * sel 99 | 100 | ![edit view](http://3.bp.blogspot.com/-iAvMiqBiOPw/UpTzpdJygFI/AAAAAAAAAGY/KaOT2TfdpVE/s1600/2.png) 101 | 102 | 103 | Links 104 | ----- 105 | 106 | * [Blog](http://nodeminderjs.blogspot.com.br/) 107 | * [npm registry](https://npmjs.org/package/node-django-admin) 108 | -------------------------------------------------------------------------------- /admin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * admin.js 3 | */ 4 | var path = require('path'); 5 | 6 | var paths = [], 7 | info = {}; 8 | 9 | var mongoose, 10 | base_url, 11 | path_url; 12 | 13 | exports.add = function(model_info) { 14 | paths.push(model_info.path); 15 | info[model_info.path] = model_info; 16 | }; 17 | 18 | exports.config = function(app, mongoose_app, base) { 19 | mongoose = mongoose_app; 20 | base_url = base.replace(/\/$/, ""); // remove trailing slash from base url 21 | 22 | // middleware to expose some helper functions and vars to templates 23 | app.use('/admin', function(req, res, next) { 24 | res.locals.capitalizeFirstLetter = capitalizeFirstLetter; 25 | res.locals.base = base_url; 26 | res.locals.menu = paths; 27 | next(); 28 | }); 29 | 30 | // routes 31 | app.get(path.join(base, '/'), index); 32 | 33 | app.get(path.join(base, '/:path/:id/edit'), edit); 34 | app.get(path.join(base, '/:path/new'), edit); 35 | app.get(path.join(base, '/:path'), list); 36 | 37 | app.post(path.join(base, '/:path/:id/delete'), del); 38 | app.post(path.join(base, '/:path/:id'), save); 39 | app.post(path.join(base, '/:path'), save); 40 | 41 | }; 42 | 43 | function index(req, res) { 44 | res.render('admin/index', { title: 'Admin' }); 45 | } 46 | 47 | function list(req, res) { 48 | var p = req.params.path, 49 | m = info[p].model, 50 | Model = mongoose.model(m); 51 | 52 | var page = (req.param('page') > 0 ? req.param('page') : 1) - 1; 53 | var perPage = 30; 54 | var options = { 55 | perPage: perPage, 56 | page: page 57 | }; 58 | 59 | Model.list(options, function(err, results) { 60 | if (err) return res.render('admin/500'); 61 | Model.count().exec(function(err, count) { 62 | res.render('admin/list', { 63 | title: capitalizeFirstLetter(p), 64 | list: info[p].list, 65 | fields: info[p].fields, 66 | data: results, 67 | path: p, 68 | page: page + 1, 69 | pages: Math.ceil(count / perPage) 70 | }); 71 | }); 72 | }); 73 | } 74 | 75 | function edit(req, res) { 76 | var p = req.params.path, 77 | id = req.params.id, 78 | meta = info[p]; 79 | Model = mongoose.model(meta.model); 80 | 81 | Model.load(id, function(err, doc) { 82 | if (err) return res.render('admin/500'); 83 | if (!doc) { 84 | doc = new Model(); 85 | } 86 | processEditFields(meta, doc, function() { 87 | res.render('admin/edit', { 88 | meta: meta, 89 | doc: doc, 90 | path: p, 91 | edit: meta.edit, 92 | field: meta.fields 93 | }); 94 | }); 95 | }); 96 | } 97 | 98 | function save(req, res) { 99 | var id = req.params.id, 100 | p = req.params.path, 101 | Model = mongoose.model(info[p].model); 102 | 103 | Model.findOne({_id: id}, function(err, doc) { 104 | if (err) console.log(err); 105 | processFormFields(info[p], req.body[p], function() { 106 | if (!id) { 107 | doc = new Model(req.body[p]); 108 | doc.password = '123change'; 109 | } 110 | else { 111 | updateFromObject(doc, req.body[p]); 112 | } 113 | doc.save(function(err) { 114 | if (err) console.log(err); 115 | return res.redirect(base_url + '/' + p); 116 | }); 117 | }); 118 | }); 119 | } 120 | 121 | function del(req, res) { 122 | var id = req.params.id, 123 | p = req.params.path, 124 | Model = mongoose.model(info[p].model); 125 | 126 | Model.remove({ _id: id }, function(err) { 127 | if (err) console.log(err); 128 | return res.redirect(base_url + '/' + p); 129 | }); 130 | } 131 | 132 | /** 133 | * Helper functions 134 | */ 135 | 136 | function capitalizeFirstLetter(string) 137 | { 138 | return string.charAt(0).toUpperCase() + string.slice(1); 139 | } 140 | 141 | function updateFromObject(doc, obj) { 142 | for (var field in obj) { 143 | doc[field] = obj[field]; 144 | } 145 | } 146 | 147 | function getType(obj) { 148 | return ({}).toString.call(obj).match(/\s([a-zA-Z]+)/)[1].toLowerCase(); 149 | } 150 | 151 | function processEditFields(meta, doc, cb) { 152 | var f, Model, field, 153 | fields = [], 154 | count = 0; // ToDo: change this to an array of fields to process 155 | 156 | for (f in meta.edit) 157 | if (meta.fields[meta.edit[f]].widget == 'ref') { 158 | count++; 159 | fields.push(meta.edit[f]); 160 | } 161 | 162 | if (!count) { 163 | return cb(); 164 | } 165 | 166 | for (f in fields) { 167 | field = meta.fields[fields[f]]; 168 | Model = mongoose.model(field.model); 169 | Model.find({}, field.display, {sort: field.display}, function(err, results) { 170 | if (err) console.log(err); 171 | field['values'] = results.map(function(e) { return e[field.display]; }); 172 | count--; 173 | if (count == 0) { 174 | return cb(); 175 | } 176 | }); 177 | } 178 | } 179 | 180 | function processFormFields(meta, body, cb) { 181 | var f, field, Model, 182 | query = {}, 183 | fields = [], 184 | count = 0; 185 | 186 | for (f in meta.edit) { 187 | if (meta.fields[meta.edit[f]].widget == 'ref') { 188 | fields.push(meta.edit[f]); 189 | count++; 190 | } 191 | } 192 | 193 | if (!count) { 194 | return cb(); 195 | } 196 | 197 | for (f in fields) { 198 | field = meta.fields[fields[f]]; 199 | Model = mongoose.model(field.model); 200 | query[field.display] = body[fields[f]]; 201 | Model.findOne(query, function(err, ref) { 202 | if (err) console.log(err); 203 | body[field.field] = ref['_id']; 204 | count--; 205 | if (count == 0) { 206 | return cb(); 207 | } 208 | }); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /demo/models/client.js: -------------------------------------------------------------------------------- 1 | /** 2 | * helpdesk 3 | * cliente.js 4 | */ 5 | 6 | var mongoose = require('mongoose'), 7 | Schema = mongoose.Schema, 8 | admin = require('node-django-admin'); 9 | 10 | /** 11 | * Schema 12 | */ 13 | 14 | var schema = new Schema({ 15 | name: { type: String, default: '' }, 16 | id: { type: String, default: '' }, 17 | contacts: [{ name: String, email: String, tel: String }] 18 | }, { collection: 'clients' }); 19 | 20 | /** 21 | * Validations 22 | */ 23 | 24 | var validatePresenceOf = function(value) { 25 | return value && value.length; 26 | } 27 | 28 | schema.path('name').validate(function(nome) { 29 | return nome.length; 30 | }, 'Name cannot be blank'); 31 | 32 | schema.path('id').validate(function(codigo) { 33 | return codigo.length; 34 | }, 'Id cannot be blank'); 35 | 36 | /** 37 | * Statics 38 | */ 39 | 40 | schema.statics = { 41 | 42 | load: function(id, cb) { 43 | this.findOne({ _id : id }) 44 | .exec(cb); 45 | }, 46 | 47 | list: function (options, cb) { 48 | var criteria = options.criteria || {}; 49 | var order = options.order || {'name': 1}; 50 | 51 | this.find(criteria) 52 | .sort(order) 53 | .limit(options.perPage) 54 | .skip(options.perPage * options.page) 55 | .exec(cb); 56 | } 57 | 58 | } 59 | 60 | /** 61 | * Register the model in mongoose 62 | */ 63 | 64 | mongoose.model('Client', schema); 65 | 66 | /** 67 | * Register the model in the admin interface 68 | */ 69 | 70 | admin.add({ 71 | path: 'clients', 72 | model: 'Client', 73 | list: [ 'name', 'id' ], 74 | edit: [ 'name', 'id' ], 75 | fields: { 76 | name: { 77 | header: 'Name' 78 | }, 79 | id: { 80 | header: 'Id' 81 | } 82 | } 83 | }); 84 | 85 | -------------------------------------------------------------------------------- /demo/models/user.js: -------------------------------------------------------------------------------- 1 | /** 2 | * node-django-admin 3 | * user.js 4 | */ 5 | 6 | var mongoose = require('mongoose'), 7 | Schema = mongoose.Schema, 8 | crypto = require('crypto'), 9 | admin = require('node-django-admin'); 10 | 11 | /** 12 | * User Schema 13 | */ 14 | 15 | var UserSchema = new Schema({ 16 | name: { type: String, default: '' }, 17 | email: { type: String, default: '', lowercase: true, unique: true }, 18 | hashed_password: { type: String, default: '' }, 19 | salt: { type: String, default: '' }, 20 | authToken: { type: String, default: '' }, 21 | _client: { type: Schema.Types.ObjectId, ref: 'Client' }, 22 | role: { type: String, default: 'client'} 23 | }); 24 | 25 | /** 26 | * Virtuals 27 | */ 28 | 29 | UserSchema 30 | .virtual('password') 31 | .set(function(password) { 32 | this._password = password; 33 | this.salt = this.makeSalt(); 34 | this.hashed_password = this.encryptPassword(password); 35 | }) 36 | .get(function() { return this._password; }); 37 | 38 | UserSchema 39 | .virtual('client') 40 | .get(function() { return ( (typeof(this._client) === 'object') ? this._client.name : this._client ); }); 41 | 42 | /** 43 | * Validations 44 | */ 45 | 46 | var validatePresenceOf = function(value) { 47 | return value && value.length; 48 | } 49 | 50 | UserSchema.path('name').validate(function(name) { 51 | return name.length; 52 | }, 'Name cannot be blank') 53 | 54 | UserSchema.path('email').validate(function (email) { 55 | return email.length; 56 | }, 'Email cannot be blank') 57 | 58 | UserSchema.path('email').validate(function(email, fn) { 59 | var User = mongoose.model('User'); 60 | 61 | // Check only when it is a new user or when email field is modified 62 | if (this.isNew || this.isModified('email')) { 63 | User.find({ email: email }, function(err, users) { 64 | fn(!err && users.length === 0); 65 | }); 66 | } else { 67 | fn(true); 68 | } 69 | }, 'Email already exists') 70 | 71 | UserSchema.path('hashed_password').validate(function(hashed_password) { 72 | return hashed_password.length; 73 | }, 'Password cannot be blank'); 74 | 75 | UserSchema.path('_client').validate(function(client) { 76 | var Client = mongoose.model('Client'); 77 | 78 | // Check only when it is a new user or when client field is modified 79 | if (this.isNew || this.isModified('_client')) { 80 | Client.findOne({ _id: client }, function(err, cli) { 81 | return err; 82 | }); 83 | } else { 84 | return true; 85 | } 86 | }, 'Invalid client'); 87 | 88 | /** 89 | * Pre-save hook 90 | */ 91 | 92 | UserSchema.pre('save', function(next) { 93 | if (!this.isNew) return next(); 94 | 95 | if (!validatePresenceOf(this.password)) { 96 | next(new Error('Invalid password')); 97 | } 98 | else { 99 | next(); 100 | } 101 | }); 102 | 103 | /** 104 | * Methods 105 | */ 106 | 107 | UserSchema.methods = { 108 | 109 | /** 110 | * Authenticate - check if the passwords are the same 111 | */ 112 | 113 | authenticate: function(plainText) { 114 | return this.encryptPassword(plainText) === this.hashed_password; 115 | }, 116 | 117 | /** 118 | * Make salt 119 | */ 120 | 121 | makeSalt: function () { 122 | return Math.round((new Date().valueOf() * Math.random())) + ''; 123 | }, 124 | 125 | /** 126 | * Encrypt password 127 | */ 128 | 129 | encryptPassword: function(password) { 130 | if (!password) return ''; 131 | var encrypred; 132 | try { 133 | encrypred = crypto.createHmac('sha1', this.salt).update(password).digest('hex'); 134 | return encrypred; 135 | } catch (err) { 136 | return ''; 137 | } 138 | } 139 | } 140 | 141 | /** 142 | * Statics 143 | */ 144 | 145 | UserSchema.statics = { 146 | 147 | load: function(id, cb) { 148 | this.findOne({ _id : id }) 149 | .populate('_client') 150 | .exec(cb); 151 | }, 152 | 153 | list: function(options, cb) { 154 | var criteria = options.criteria || {}; 155 | var order = options.order || {'name': 1}; 156 | 157 | this.find(criteria) 158 | .populate('_client', 'name id') 159 | .sort(order) 160 | .limit(options.perPage) 161 | .skip(options.perPage * options.page) 162 | .exec(cb); 163 | } 164 | 165 | } 166 | 167 | /** 168 | * Register the model in mongoose 169 | */ 170 | 171 | mongoose.model('User', UserSchema); 172 | 173 | /** 174 | * Register the model in the admin interface 175 | */ 176 | 177 | admin.add({ 178 | path: 'users', 179 | model: 'User', 180 | list: [ 'name', 'email', 'client', 'role' ], 181 | edit: [ 'name', 'email', 'client', 'role' ], 182 | fields: { 183 | 'name': { 184 | header: 'Name' 185 | }, 186 | 'email': { 187 | header: 'Email', 188 | widget: 'email' 189 | }, 190 | 'client': { 191 | header: 'Client', 192 | widget: 'ref', 193 | model: 'Client', 194 | display: 'name', 195 | field: '_client' 196 | }, 197 | 'role': { 198 | header: 'Role', 199 | widget: 'sel', 200 | values: ['admin', 'client', 'staff'] 201 | } 202 | } 203 | }); 204 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-django-admin-demo", 3 | "description": "Demo app using node-django-admin", 4 | "author": "nodeminderjs", 5 | "licence": "MIT", 6 | "keywords": [ 7 | "nodejs", 8 | "admin", 9 | "django", 10 | "express", 11 | "mongoose", 12 | "mongodb" 13 | ], 14 | "version": "0.0.1", 15 | "private": false, 16 | "engines": { 17 | "node": ">= 0.10.0", 18 | "npm": ">= 1.2.0" 19 | }, 20 | "repository" : { 21 | "type": "git", 22 | "url": "https://github.com/nodeminderjs/node-django-admin" 23 | }, 24 | "scripts": { 25 | "start": "NODE_ENV=development ./node_modules/.bin/nodemon server.js", 26 | "test": "NODE_ENV=test ./node_modules/.bin/mocha --reporter spec test/test-*.js" 27 | }, 28 | "dependencies": { 29 | "express": "latest", 30 | "jade": "latest", 31 | "mongoose": "latest" 32 | }, 33 | "devDependencies": { 34 | "supertest": "latest", 35 | "should": "latest", 36 | "mocha": "latest", 37 | "nodemon": "latest" 38 | } 39 | } 40 | 41 | -------------------------------------------------------------------------------- /demo/public/admin/css/admin.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 50px; 3 | margin: 0; 4 | color: rgb(34,34,34); 5 | } 6 | 7 | .jumbotron { 8 | padding: 24px 30px; 9 | margin-bottom: 10px; 10 | } 11 | 12 | .jumbotron h1 { 13 | margin-top: 10px; 14 | font-size: 48px; 15 | } 16 | 17 | a { font-weight: bold; text-decoration: none; } 18 | /* a:link { color: #688; } /* não visitados */ 19 | /* a:visited { color: #355; } /* já visitados */ 20 | /* a:hover { color: #57a; } /* mouse over */ 21 | /* a:focus { color: #a77; } /* click */ 22 | /* a:active { color: #a77; } /* links ativos - tab */ 23 | 24 | #list-top { 25 | position: relative; 26 | } 27 | 28 | #new { 29 | position: absolute; 30 | right: 0; 31 | bottom: 0; 32 | } 33 | 34 | #form-btn { 35 | position: relative; 36 | } 37 | 38 | #del-btn { 39 | position: absolute; 40 | right: 0; 41 | bottom: 0; 42 | } 43 | 44 | /* Override bootstrap css rules */ 45 | 46 | table { 47 | border-bottom: 4px solid rgb(34,34,34); 48 | } 49 | 50 | th { 51 | color: #fff; 52 | background-color: rgb(34,34,34); 53 | /* padding: 3px 6px; */ 54 | border-bottom: none !important; 55 | } 56 | 57 | a.list-group-item.active, a.list-group-item.active:hover, a.list-group-item.active:focus { 58 | color: #fff; 59 | background-color: rgb(34,34,34); 60 | border-color: rgb(34,34,34); 61 | background-image: none; 62 | } 63 | 64 | .form-horizontal .control-label { 65 | /* text-align: left; */ 66 | color: #fff; 67 | background-color: rgb(34,34,34); 68 | height: 34px; 69 | border-radius: 4px; 70 | border: 1px solid rgb(34,34,34); 71 | /* width: 14%; */ 72 | /* margin-left: 15px; */ 73 | } 74 | 75 | .form-horizontal .form-group { 76 | margin-left: 0; 77 | } -------------------------------------------------------------------------------- /demo/server.js: -------------------------------------------------------------------------------- 1 | /** 2 | * node-django-admin 3 | * server.js 4 | */ 5 | 6 | var express = require('express'), 7 | mongoose = require('mongoose'), 8 | fs = require('fs'), 9 | pkg = require('./package.json'), 10 | env = process.env.NODE_ENV || 'development'; 11 | 12 | var admin = require('node-django-admin'); 13 | 14 | // Bootstrap db connection 15 | mongoose.connect('mongodb://localhost/node-django-admin'); 16 | 17 | // Bootstrap models 18 | var models_path = __dirname + '/models'; 19 | fs.readdirSync(models_path).forEach(function(file) { 20 | if (~file.indexOf('.js')) 21 | require(models_path + '/' + file); 22 | }); 23 | 24 | // Express settings 25 | var app = express(); 26 | app.set('port', process.env.PORT || 3000); 27 | app.set('views', __dirname + '/views'); 28 | app.set('view engine', 'jade'); 29 | app.use(express.favicon()); 30 | app.use(express.static(__dirname + '/public')); 31 | if (env !== 'test') { 32 | app.use(express.logger('dev')); 33 | } 34 | app.use(function(req, res, next) { // expose package.json to views 35 | res.locals.pkg = pkg; 36 | next(); 37 | }); 38 | app.use(express.cookieParser()); 39 | app.use(express.bodyParser()); 40 | app.use(express.methodOverride()); 41 | app.use(express.session({ secret: 'SECRET' })); 42 | app.configure('development', function(){ 43 | app.use(express.errorHandler()); 44 | app.locals.pretty = true; 45 | }); 46 | 47 | // Bootstrap admin site 48 | admin.config(app, mongoose, '/admin'); 49 | 50 | //Bootstrap routes 51 | app.use(app.router); 52 | 53 | // Run server 54 | var server = require('http').createServer(app); 55 | 56 | server.listen(app.get('port'), function() { 57 | console.log('Express server listening on port ' + app.get('port')); 58 | }); 59 | 60 | -------------------------------------------------------------------------------- /demo/views/admin/404.jade: -------------------------------------------------------------------------------- 1 | extends layouts/default 2 | 3 | block content 4 | h1 Oops something went wrong 5 | br 6 | h2 404 7 | 8 | #error-message-box 9 | #error-stack-trace 10 | pre 11 | code!= error 12 | 13 | -------------------------------------------------------------------------------- /demo/views/admin/500.jade: -------------------------------------------------------------------------------- 1 | extends layouts/default 2 | 3 | block content 4 | h1 Oops something went wrong 5 | br 6 | h2 500 7 | 8 | pre= error 9 | -------------------------------------------------------------------------------- /demo/views/admin/edit.jade: -------------------------------------------------------------------------------- 1 | extends form 2 | 3 | //- block append content 4 | 5 | -------------------------------------------------------------------------------- /demo/views/admin/form.jade: -------------------------------------------------------------------------------- 1 | extends layouts/default 2 | 3 | block content 4 | - var action = base + '/' + path 5 | - action += doc.isNew ? '' : '/' + doc._id 6 | - var id = doc.isNew ? '' : doc._id 7 | 8 | h2= ( (id) ? "Edit " : "New " ) + ( (meta.name) ? meta.name : meta.model ) 9 | 10 | #form 11 | form.form-horizontal(method="post", action=action, enctype="multipart/form-data", role="form") 12 | input(type="hidden", name="_csrf", value="#{csrf_token}") 13 | input(type="hidden", name="_doc_id", value=id) 14 | 15 | each f in edit 16 | .form-group 17 | - var type = field[f].widget || 'text' 18 | - var header = field[f].header 19 | label.col-sm-2.control-label(for=f)= header + ':' 20 | .col-sm-10 21 | if type == 'sel' || type == 'ref' 22 | - var sel = ((doc.isNew) ? doc[f] : doc[f][field[f].display] || doc[f]) 23 | select.form-control(id=f, name=path+'['+f+']', placeholder='Enter '+header) 24 | each v in field[f].values 25 | if v == sel 26 | option(value=v, selected)= v 27 | else 28 | option(value=v)= v 29 | else 30 | input.form-control(id=f, type=type, name=path+'['+f+']', value=doc[f], placeholder='Enter '+header) 31 | 32 | .form-group 33 | .col-sm-2 34 | .col-sm-10 35 | #form-btn 36 | button.btn.btn-primary(type='submit') Save changes 37 |   38 | a.btn(href='#{base}/#{path}', title="cancel") Cancel 39 | 40 | if !doc.isNew 41 | #del-btn 42 | form(action=action+'/delete' method="post") 43 | button.btn.btn-danger(type='submit') Delete 44 | -------------------------------------------------------------------------------- /demo/views/admin/includes/end.jade: -------------------------------------------------------------------------------- 1 | // jQuery 2 | script(src="http://code.jquery.com/jquery-1.10.2.min.js") 3 | 4 | // Latest compiled and minified JavaScript 5 | script(src="//netdna.bootstrapcdn.com/bootstrap/3.0.2/js/bootstrap.min.js") 6 | -------------------------------------------------------------------------------- /demo/views/admin/includes/head.jade: -------------------------------------------------------------------------------- 1 | head 2 | title= title || 'Admin Site' 3 | meta(charset="utf-8") 4 | meta(http-equiv="X-UA-Compatible", content="IE=edge,chrome=1") 5 | 6 | meta(http-equiv="Content-type", content="text/html;charset=UTF-8") 7 | meta(name="keywords", content=pkg.keywords.join(",")) 8 | meta(name="description", content=pkg.description) 9 | 10 | link(href="/favicon.ico", rel="shortcut icon", type="image/x-icon") 11 | 12 | meta(name="viewport", content="width=device-width, initial-scale=1.0") 13 | 14 | // Latest compiled and minified CSS 15 | link(rel="stylesheet", href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap.min.css") 16 | 17 | // HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries 18 | // WARNING: Respond.js doesn't work if you view the page via file:// 19 | // if lt IE 9 20 | script(src="http://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js") 21 | script(src="http://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js") 22 | 23 | // Optional theme 24 | link(rel="stylesheet", href="//netdna.bootstrapcdn.com/bootstrap/3.0.2/css/bootstrap-theme.min.css") 25 | 26 | link(rel='stylesheet', href='/admin/css/admin.css') 27 | 28 | -------------------------------------------------------------------------------- /demo/views/admin/index.jade: -------------------------------------------------------------------------------- 1 | extends layouts/default 2 | 3 | block content 4 | h1 Admin 5 | 6 | -------------------------------------------------------------------------------- /demo/views/admin/layouts/default.jade: -------------------------------------------------------------------------------- 1 | doctype 5 2 | html 3 | include ../includes/head 4 | 5 | body 6 | div(class="navbar navbar-inverse navbar-fixed-top", role="navigation") 7 | div(class="container") 8 | div(class="navbar-header") 9 | button.navbar-toggle(type="button", data-toggle="collapse", data-target=".navbar-collapse") 10 | span.sr-only Toggle navigation 11 | span.icon-bar 12 | span.icon-bar 13 | span.icon-bar 14 | a.navbar-brand(href="#{base}") Admin Site 15 | div(class="collapse navbar-collapse") 16 | ul(class="nav navbar-nav") 17 | li.active 18 | a(href="#{base}") Home 19 | li.dropdown 20 | a.dropdown-toggle(href="#" data-toggle="dropdown")= "Models " 21 | b.caret 22 | ul.dropdown-menu 23 | each item in menu 24 | - var cls = item == path ? "active" : "" 25 | li(class=cls) 26 | a(role="menuitem", tabindex="-1", href="#{base}/#{item}")= capitalizeFirstLetter(item) 27 | 28 | // Main jumbotron for a primary marketing message or call to action 29 | .jumbotron 30 | .container 31 | h1 Welcome to the Admin Site demo! 32 | p This is a Node.js admin site tool inspired by the Django framework. 33 | p 34 | a.btn.btn-primary.btn-lg(role="button") Learn more » 35 | 36 | .container 37 | .row 38 | .col-md-2 39 | h2 Models 40 | 41 | //- .dropdown.theme-dropdown.open.clearfix 42 | //- ul.dropdown-menu(role="menu", style="position:static;") 43 | //- each item in menu 44 | //- - var cls = item == path ? "active" : "" 45 | //- li(class=cls) 46 | //- a(role="menuitem", tabindex="-1", href="#{base}/#{item}")= capitalizeFirstLetter(item) 47 | 48 | .list-group 49 | each item in menu 50 | - var cls = item == path ? "list-group-item active" : "list-group-item" 51 | a(class=cls, href="#{base}/#{item}")= capitalizeFirstLetter(item) 52 | 53 | //- p 54 | //- a.btn.btn-default(href="#", role="button") View details » 55 | 56 | .col-md-10 57 | block content 58 | 59 | include ../includes/end 60 | -------------------------------------------------------------------------------- /demo/views/admin/list.jade: -------------------------------------------------------------------------------- 1 | extends layouts/default 2 | 3 | block content 4 | #list 5 | #list-top 6 | h2= title 7 | #new 8 | a.btn.btn-xs.btn-default(href="#{base}/#{path}/new", role="button") New » 9 | 10 | //- .table-bordered 11 | table.table.table-striped.table-hover.table-condensed 12 | thead 13 | tr 14 | each f in list 15 | th= fields[f].header || capitalizeFirstLetter(f) 16 | th 17 | tbody 18 | each d in data 19 | tr 20 | each f in list 21 | td= d[f] 22 | td 23 | a.btn.btn-xs.btn-default(href="#{base}/#{path}/#{d._id}/edit") edit 24 | 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-django-admin", 3 | "description": "A Node.js admin site tool inspired by the Django framework admin site tool", 4 | "main": "admin.js", 5 | "author": "nodeminderjs", 6 | "licence": "MIT", 7 | "keywords": [ 8 | "nodejs", 9 | "admin", 10 | "django", 11 | "express", 12 | "mongoose", 13 | "mongodb", 14 | "bootstrap" 15 | ], 16 | "version": "0.0.9", 17 | "private": false, 18 | "engines": { 19 | "node": ">= 0.10.0", 20 | "npm": ">= 1.2.0" 21 | }, 22 | "repository" : { 23 | "type": "git", 24 | "url": "https://github.com/nodeminderjs/node-django-admin" 25 | }, 26 | "scripts": { 27 | "test": "NODE_ENV=test ./node_modules/.bin/mocha --reporter spec test/test-*.js" 28 | }, 29 | "dependencies": { 30 | } 31 | } 32 | 33 | --------------------------------------------------------------------------------