diff options
| author | julie lala <jules@okfoc.us> | 2015-04-11 11:42:27 -0400 |
|---|---|---|
| committer | julie lala <jules@okfoc.us> | 2015-04-11 11:42:27 -0400 |
| commit | 65c1c6541f98eb863f9a19533f4bdb4bd9e38514 (patch) | |
| tree | 84cbe0902cc355b9e1556c346f217d1a4cafd552 | |
| parent | 0b6afc1d5aa8a2f33b1c21e04a10c3eaa43870c1 (diff) | |
| parent | 2b95bcf414f02551a384ef2020958e90431814dd (diff) | |
merge
21 files changed, 597 insertions, 231 deletions
diff --git a/app/index.js b/app/index.js index b312eb1..e462b48 100644 --- a/app/index.js +++ b/app/index.js @@ -77,8 +77,11 @@ function OKCMS(options) { var adminTemplateProvider = this._adminTemplateProvider = new OKTemplate({root: adminTemplateRoot}); - var db = new OKDB(options.db || 'fs'); var schemas = this._schemas = this._createSchemas(schemaConfig); + var db = new OKDB({ + db: options.db || 'fs', + schemas: schemas + }); var resourceCache = this._resourceCache = this._createResources(resourceConfig, db, schemas); @@ -119,6 +122,8 @@ OKCMS.prototype._createSchemas = function(schemaConfig) { schemaConfig = schemaConfig || {}; return Object.keys(schemaConfig).reduce(function(cache, key) { var spec = schemaConfig[key]; + // All resources have an autoincrementing index so we can order them suckas + spec.__index = {type: 'meta', autoincrement: true}; cache[key] = OKSchema(spec); return cache; }, {}); @@ -248,7 +253,7 @@ function ResourceCache(resources) { throw new Error('Undefined resource given to ResourceCache'); if (resource.bound) { cache[resource.type] = resource.parent; - cache[resource.type + ':' + resource.id] = resource; + cache[resource.type + ':' + resource.getID()] = resource; } else { cache[resource.type] = resource; } diff --git a/app/node_modules/okadminview/index.js b/app/node_modules/okadminview/index.js index a376df5..897c583 100644 --- a/app/node_modules/okadminview/index.js +++ b/app/node_modules/okadminview/index.js @@ -1,6 +1,9 @@ -var assign = require('object-assign') +var assign = require('object-assign'); +var cloneDeep = require('lodash.clonedeep'); var bodyParser = require('body-parser'); var methodOverride = require('method-override'); +var session = require('express-session'); +var flash = require('connect-flash'); var Q = require('q'); var pluralize = require('pluralize'); var OKQuery = require('okquery'); @@ -22,12 +25,14 @@ function OKAdminView(options) { throw new Error('No templateProvider provided to OKAdminView'); if (!options.meta) throw new Error('No meta query provided to OKAdminView'); + var app = options.app; var express = options.express; var meta = options.meta; var resourceCache = this._resourceCache = options.resourceCache; - var resourceConfig = this._resourceConfig = options.resourceConfig; + var resourceConfig = this._resourceConfig = cloneDeep(options.resourceConfig); var provider = options.templateProvider; + // Load templates var templates = this._templates = ['index', 'resource', 'resource_new'].reduce(function(cache, name) { @@ -37,6 +42,7 @@ function OKAdminView(options) { cache[name] = template; return cache; }, {}); + // OKAdmin middleware is a router, so mounts on 'use' Object.defineProperty(this, 'mount', { value: 'use', @@ -51,13 +57,14 @@ function OKAdminView(options) { var config = resourceConfig[key]; var type = config.type; var staticData = config.static || {}; + // Get parent level resource var resource = resourceCache.get(config.type); if (!resource) throw new Error('Something weird is going on'); - var id = staticData[resource.idField]; + var id = resource.getID(staticData); + // Check to see if there's a more specific instance resource = resourceCache.get(type, id) || resource; if (resource.bound) { - // Resource instances implement the query API return OKQuery({resource: resource});; } else { return OKQuery({resource: resource, query: config.query}) @@ -71,6 +78,14 @@ function OKAdminView(options) { strict: app.get('strict routing') }); + // Enable basic sessions for flash messages + router.use(session({ + secret: 'okadmin', + resave: false, + saveUninitialized: false + })); + // Enable flash messaging + router.use(flash()); // Parse form data router.use(bodyParser.urlencoded({extended: true})); // HTML forms only support POST and GET methods @@ -87,7 +102,10 @@ function OKAdminView(options) { router.get('/', function readIndex(req, res, next) { fetchIndexTemplateData(meta, indexQueries).then(function(data) { - view.renderIndex(req, res, data); + view.renderIndex(req, res, assign(data, { + success: req.flash('success'), + errors: req.flash('errors') + })); }).fail(errorHandler(req, res)); }); @@ -98,13 +116,11 @@ function OKAdminView(options) { errorHandler(req, res)(new Error('No such resource ' + type)); } else { meta.get().then(function(metadata) { - view.renderResourceNew(req, res, { - meta: metadata, - resource: { - type: resource.type, - spec: resource.spec - } - }); + var templateData = getResourceTemplateData(metadata, resource, {}); + view.renderResourceNew(req, res, assign(templateData, { + success: req.flash('success'), + errors: req.flash('errors'), + })); }).fail(errorHandler(req, res)); } }); @@ -120,13 +136,17 @@ function OKAdminView(options) { resource: resource, query: id }); - fetchResourceTemplateData(meta, query, getResourceTemplateData).then(function(data) { - if (!data) { - resourceMissingHandler(req, res)() - } else { - view.renderResource(req, res, data); - } - }).fail(errorHandler(req, res)); + fetchResourceTemplateData(meta, query, getResourceTemplateData) + .then(function(data) { + if (!data) { + resourceMissingHandler(req, res)() + } else { + view.renderResource(req, res, assign(data, { + success: req.flash('success'), + errors: req.flash('errors') + })); + } + }).fail(errorHandler(req, res)); } }); @@ -142,7 +162,8 @@ function OKAdminView(options) { try { resource.assertValid(data); resource.create(data).then(function(created) { - res.redirect(303, data[resource.idField]); + req.flash('success', {action: 'create'}); + res.redirect(303, resource.getID(data)); }).fail(errorHandler(req, res)); } catch (errors) { var templateData = getResourceTemplateData(metadata, resource, data); @@ -152,6 +173,40 @@ function OKAdminView(options) { } }); + router.put('/:type/__batch/', function putBatch(req, res, next) { + var type = req.params.type; + var body = req.body || {}; + var resourcesJSON = body[type]; + var resource = resourceCache.get(type); + if (!resourcesJSON || !resourcesJSON.length) { + res.status(400); + errorHandler(req, res)(new Error('Bad request')); + } else if (!resource) { + res.status(404); + errorHandler(req, res)(new Error('No such resource')); + } else { + try { + var ids = []; + var resourcesParsed = resourcesJSON.map(function(resourceJSON) { + var data = JSON.parse(resourceJSON); + ids.push(resource.getID(data)); + return data; + }); + } catch (e) { + errorHandler(req, res)(new Error('Resource batch contains invalid JSON')); + return; + } + Q.all([ + meta.get(), + resource.updateBatch(ids, resourcesParsed), + ]).then(function(results) { + var metadata = results.shift(); + req.flash('success', {action: 'batch_update'}); + res.redirect(303, '../..'); + }).fail(errorHandler(req, res)); + } + }); + router.put('/:type/:id/', function updateResource(req, res, next) { var type = req.params.type; var id = req.params.id; @@ -165,7 +220,8 @@ function OKAdminView(options) { try { resource.assertValid(data); resource.update(id, data).then(function(updated) { - res.redirect(303, '../' + updated[resource.idField]); + req.flash('success', {action: 'update'}); + res.redirect(303, '../' + resource.getID(updated)); }).fail(errorHandler(req, res)); } catch (errors) { var templateData = getResourceTemplateData(metadata, resource, data); @@ -184,6 +240,7 @@ function OKAdminView(options) { } else { meta.get().then(function(metadata) { resource.destroy(id).then(function() { + req.flash('success', {action: 'delete'}); res.redirect(303, '../..'); }).fail(errorHandler(req, res)); }).fail(errorHandler(req, res)); @@ -203,13 +260,19 @@ function getResourceTemplateData(meta, resource, data) { data = data || {}; var spec = Object.keys(resource.spec).reduce(function(cache, prop) { var value = data[prop]; - cache[prop].value = value; + var propSpec = cache[prop]; + // Decorate spec with actual resource values + propSpec.value = value; + // Some fields should not be shown to the user + if (propSpec.type === 'meta' || propSpec.static) { + propSpec.hidden = true; + } return cache; }, resource.spec); return { meta: meta, resource: { - id: data[resource.idField], + id: resource.getID(data), type: resource.type, spec: spec } @@ -245,7 +308,7 @@ OKAdminView.prototype.renderResourceNew = function(req, res, data) { * Annotate template data with schema info */ function fetchIndexTemplateData(meta, queries) { - return Q.Promise(function(resolve, reject) { + return Q.promise(function(resolve, reject) { Q.all([meta.get()].concat(queries.map(function(query) { return query.get(); }))).then(function(results) { @@ -266,14 +329,14 @@ function fetchIndexTemplateData(meta, queries) { } if (result.length) { - result.forEach(addToCache) + result.forEach(addData) } else { - addToCache(result); + addData(result); } - function addToCache(data) { + function addData(data) { // Report id to template under standard name - data.id = data[resource.idField]; + data.id = resource.getID(data); cache[key].data.push(data); } @@ -293,7 +356,7 @@ function fetchIndexTemplateData(meta, queries) { */ function fetchResourceTemplateData(meta, query, fn) { fn = fn || function(m, r, d) { return {meta: m, resource: d}; }; - return Q.Promise(function(resolve, reject) { + return Q.promise(function(resolve, reject) { meta.get().then(function(metadata) { query.get().then(function(data) { var resource = query.resource; diff --git a/app/node_modules/okadminview/package.json b/app/node_modules/okadminview/package.json index 4832db1..4c6d11c 100644 --- a/app/node_modules/okadminview/package.json +++ b/app/node_modules/okadminview/package.json @@ -10,6 +10,9 @@ "license": "None", "dependencies": { "body-parser": "^1.12.2", + "connect-flash": "^0.1.1", + "express-session": "^1.11.1", + "lodash.clonedeep": "^3.0.0", "method-override": "^2.3.2", "object-assign": "^2.0.0", "pluralize": "^1.1.2", diff --git a/app/node_modules/okdb/index.js b/app/node_modules/okdb/index.js index 4be646d..2cf6d8b 100644 --- a/app/node_modules/okdb/index.js +++ b/app/node_modules/okdb/index.js @@ -1,6 +1,10 @@ -var Q = require('q'); +var assign = require('object-assign') +var cloneDeep = require('lodash.clonedeep'); +var isobject = require('lodash.isobject'); var format = require('util').format; var low = require('lowdb'); +var Q = require('q'); + low.mixin(low.mixin(require('underscore-db'))); /** @@ -10,14 +14,14 @@ low.mixin(low.mixin(require('underscore-db'))); function OKDB(options) { if (!(this instanceof OKDB)) return new OKDB(options); options = options || {}; - var type; + var db; if (typeof options === 'string') - type = options; + db = options; else - type = options.type; - if (!type) - throw new Error('No DB type provided'); - switch (type) { + db = options.db; + if (!db) + throw new Error('No DB db provided to OKDB'); + switch (db) { case 'fs': return FSDB(options); default: @@ -27,88 +31,182 @@ function OKDB(options) { /** * DB implementation backed by a JSON file. - * TODO Incomplete */ function FSDB(options) { if (!(this instanceof FSDB)) return new FSDB(options); options = options || {}; + if (!options.schemas) + throw new Error('No schemas provided to FSDB') + this._schemas = options.schemas; var name = options.name || 'db'; var filename = name + '.json'; this._db = low(filename); } -FSDB.prototype._resolve = function(data, error) { - return Q.Promise(function resolvePromise(resolve, reject) { - if (error) { - reject(error); - } else { - resolve(data); - } - }); +FSDB.prototype.get = function(collection, id) { + var schema = this._schemas[collection]; + if (!schema) + return resolve(null, new Error('No such collection type')); + + var query = getQuery(schema, id); + if (!query) + return resolve(null, new Error('Bad query')); + + var result = this._db(collection).find(query); + return resolve(result ? cloneDeep(result) : result); }; -FSDB.prototype.get = function(collection, query) { - if (!query) { - return this._resolve(null, new Error('No query given')); +/** + * Add a new document to the DB + */ +FSDB.prototype.insert = function(collection, data) { + var schema = this._schemas[collection]; + var wrapped = this._db(collection); + if (!schema) + return resolve(null, new Error('No such collection type')); + // Get detached, clone deep, data sleep, beep beep + data = cloneDeep(data); + // Auto-increment fields + data = autoincrement(wrapped, schema, data); + var result = wrapped.chain().push(data).last().value(); + + if (result) { + return resolve(cloneDeep(result)); } else { - var data = this._db(collection).find(query); - return this._resolve(data); + return resolve(null, new Error('Problem inserting document')); } }; -FSDB.prototype.put = function(collection, query, data) { - data = data || {}; - if (!query) { - return this._resolve(null, new Error('No query given')); - } else if (this._db(collection).find(query)) { - var updated = this._db(collection) - .chain() - .find(query) - .assign(data) - .value(); - return this._resolve(updated); +/** + * Update an existing document in the DB + */ +FSDB.prototype.update = function(collection, id, data) { + var schema = this._schemas[collection]; + if (!schema) + return resolve(null, new Error('No such collection type')); + + var query = getQuery(schema, id); + var wrapped = this._db(collection); + var chain = wrapped.chain().find(query); + + if (!chain.value()) { + return resolve(null, new Error('Cannot update nonexistent entry')); + } + + var result = chain.assign(cloneDeep(data)).value(); + if (result ) { + return resolve(cloneDeep(result)); } else { - return this._resolve(null, new Error('Cannot update nonexistent entry')); + return resolve(null, new Error('Problem updating document')); } }; -FSDB.prototype.create = function(collection, data) { - var created = this._db(collection) - .chain() - .push(data) - .last() - .value(); - return this._resolve(created); +/** + * TODO Should be atomic ¯\_(ツ)_/¯ + */ +FSDB.prototype.updateBatch = function(collection, ids, datas) { + var schema = this._schemas[collection]; + if (!schema) + return resolve(null, new Error('No such collection type')); + if (!ids || !ids.length || !datas || !datas.length || + ids.length !== datas.length) { + return resolve(null, new Error('Bad input')); + } + + var doc = this._db(collection); + var results = ids.map(function(id, i) { + return doc.chain().find(getQuery(schema, id)).assign(datas[i]).value(); + }); + + return resolve(results); }; -FSDB.prototype.remove = function(collection, query) { - if (!collection || !query) { - return this._resolve(null, new Error('Bad input')); +FSDB.prototype.remove = function(collection, id) { + var schema = this._schemas[collection]; + if (!schema) + return resolve(null, new Error('No such collection type')); + + var query = getQuery(schema, id); + var result = this._db(collection).removeWhere(query); + + if (result) { + // Don't need to clone this ref, since it's removed anyway + return resolve(result); } else { - var data = this._db(collection).removeWhere(query); - if (!data) - var error = new Error('Cannot remove nonexistent entry'); - return this._resolve(data, error); + return resolve(null, new Error('Cannot remove nonexistent entry')); } }; -FSDB.prototype.find = function(collection, query) { - if (!collection || !query) { - return this._resolve(null, new Error('Bad input')); - } else { - var data = this._db(collection).find(query); - return this._resolve(data); - } +FSDB.prototype.sortBy = function(collection, prop, descend) { + var schema = this._schemas[collection]; + if (!schema) + return resolve(null, new Error('No such collection type')); + if (!prop) + return resolve(null, new Error('Bad input')); + + var result = this._db(collection).sortByOrder([prop], [!descend]); + return resolve(result); +}; + +FSDB.prototype.find = function(collection, id) { + var schema = this._schemas[collection]; + if (!schema) + return resolve(null, new Error('No such collection type')); + + var query = getQuery(schema, id); + var result = this._db(collection).find(query); + + return resolve(cloneDeep(result)); +}; + +FSDB.prototype.all = function(collection) { + var schema = this._schemas[collection]; + if (!schema) + return resolve(null, new Error('No such collection type')); + + var data = this._db(collection).value(); + return resolve(cloneDeep(data || [])); }; FSDB.prototype.getMeta = function() { var data = this._db('meta').first(); - return this._resolve(data || {}); + return resolve(data || {}); }; -FSDB.prototype.getAll = function(collection) { - var data = this._db(collection).toArray(); - return this._resolve(data || []); +/** + * Function implementing DB auto increment support + * Naive implementation, assumes DB is relatively small. + */ +function autoincrement(wrapper, schema, data) { + return schema.autoIncrementFields.reduce(function(data, field) { + var last = wrapper.chain().sort(field).first().value(); + var index = last ? last[field] : -1; + var incremented = {}; + incremented[field] = (parseInt(index) + 1); + return assign(data, incremented); + }, data); +} + +function getQuery(schema, id) { + if (schema && id) { + var query = {}; + query[schema.idField] = id; + return query; + } +} + +/** + * Helper function to create promises for DB data + */ +function resolve(data, error) { + return Q.promise(function resolvePromise(resolve, reject) { + if (error) { + reject(error); + } else { + resolve(data); + } + }); }; + module.exports = OKDB; diff --git a/app/node_modules/okdb/package.json b/app/node_modules/okdb/package.json index 659422e..5184cb6 100644 --- a/app/node_modules/okdb/package.json +++ b/app/node_modules/okdb/package.json @@ -9,7 +9,10 @@ "author": "OKFocus", "license": "None", "dependencies": { + "lodash.clonedeep": "^3.0.0", + "lodash.isobject": "^3.0.1", "lowdb": "^0.7.2", + "object-assign": "^2.0.0", "q": "^1.2.0", "underscore-db": "^0.8.1" } diff --git a/app/node_modules/okquery/index.js b/app/node_modules/okquery/index.js index 2d93e2a..9cc8b78 100644 --- a/app/node_modules/okquery/index.js +++ b/app/node_modules/okquery/index.js @@ -1,3 +1,4 @@ +var cloneDeep = require('lodash.clonedeep'); var assign = require('object-assign'); var isobject = require('lodash.isobject'); var Q = require('q'); @@ -16,6 +17,9 @@ function OKQuery(options) { var resource = options.resource; var type = resource.type; var query = options.query || '*'; + // Ensure immutability + if (isobject(query)) + query = cloneDeep(query); Object.defineProperty(this, 'resource', { value: resource, @@ -55,6 +59,9 @@ function createQuery(resource, query, options) { function queryComplex(resource, query) { var dynamicProp; + // Query is an object specifying key value pairs against which + // to match DB entries. Iterate through and check if any of the values + // is unbound e.g. :id var notDynamic = Object.keys(query).every(function(prop) { var matcher = query[prop]; if (isDynamic(matcher)) { @@ -71,6 +78,8 @@ function queryComplex(resource, query) { } } else { return function(id) { + // Bind the dynamic property to its value + // and add the pair to the query var dynamicQuery = {}; dynamicQuery[dynamicProp] = id; var query = assign({}, query, dynamicQuery); @@ -87,7 +96,8 @@ function queryDynamic(resource) { function queryAll(resource) { return function() { - return resource.all(); + // Always return sorted results + return resource.sortBy('__index'); }; } diff --git a/app/node_modules/okquery/package.json b/app/node_modules/okquery/package.json index 606d45b..5ba9dd5 100644 --- a/app/node_modules/okquery/package.json +++ b/app/node_modules/okquery/package.json @@ -9,6 +9,7 @@ "author": "OKFocus", "license": "None", "dependencies": { + "lodash.clonedeep": "^3.0.0", "lodash.isobject": "^3.0.1", "object-assign": "^2.0.0", "q": "^1.2.0" diff --git a/app/node_modules/okresource/index.js b/app/node_modules/okresource/index.js index 0e8498f..c3f9adb 100644 --- a/app/node_modules/okresource/index.js +++ b/app/node_modules/okresource/index.js @@ -20,17 +20,6 @@ function OKResource(options) { var schema = options.schema; var spec = schema.spec; - // Iterate through spec to find field which will act as the - // resource id in da db. - var idField = Object.keys(spec).reduce(function(idField, prop) { - var propSpec = spec[prop]; - if (propSpec.id) - idField = prop; - return idField; - // If schema has a prop called 'id', default to that one - }, spec.id && 'id'); - if (!idField) - throw new Error('Bad schema: no ID field'); var type = options.type; this._db = options.db; @@ -51,12 +40,6 @@ function OKResource(options) { enumerable: true }); - Object.defineProperty(this, 'idField', { - value: idField, - writable: false, - enumerable: true - }); - // Whether this resource represents a specific data point // or a whole class of data Object.defineProperty(this, 'bound', { @@ -74,19 +57,22 @@ OKResource.prototype.assertValid = function(data) { }; OKResource.prototype.all = function() { - return this._db.getAll(this.type); + return this._db.all(this.type); }; -OKResource.prototype.create = function(data) { +OKResource.prototype.getID = function(data) { data = data || {}; + return data[this._schema.idField]; +}; + +OKResource.prototype.create = function(data) { var type = this.type; var db = this._db; - var id = data[this.idField]; return Q.promise(function(resolve, reject) { - if (!id) { - reject(new Error('Data does not contain ID property')); + if (!data) { + reject(new Error('No data provided')); } else { - db.create(type, data).then(resolve).fail(reject); + db.insert(type, data).then(resolve).fail(reject); } }); }; @@ -94,13 +80,11 @@ OKResource.prototype.create = function(data) { OKResource.prototype.destroy = function(id) { var db = this._db; var type = this.type; - var query = {}; - query[this.idField] = id; return Q.promise(function(resolve, reject) { if (!id) { - reject(new Error('No ID given')); + reject(new Error('No ID provided')); } else { - db.remove(type, query).then(resolve).fail(reject); + db.remove(type, id).then(resolve).fail(reject); } }); }; @@ -111,7 +95,7 @@ OKResource.prototype.find = function(query) { var type = this.type; return Q.promise(function(resolve, reject) { if (!query) { - throw new Error('No query given'); + throw new Error('No query provided'); } else { db.find(type, query).then(resolve).fail(reject); } @@ -121,53 +105,65 @@ OKResource.prototype.find = function(query) { OKResource.prototype.get = function(id) { var db = this._db; var type = this.type; - var idField = this.idField; return Q.promise(function(resolve, reject) { if (!id) { - throw new Error('No ID given'); + throw new Error('No ID provided'); } else { - // We have the id, but we still need - // to resolve which field is the id field - // to match - var query = {}; - query[idField] = id; - db.get(type, query).then(resolve).fail(reject); + db.get(type, id).then(resolve).fail(reject); } }); }; OKResource.prototype.update = function(id, data) { - data = data || {}; var db = this._db; var type = this.type; - var idField = this.idField; return Q.promise(function(resolve, reject) { if (!id) { reject(new Error('No resource ID provided')); + } else if (!data) { + reject(new Error('No data provided')); } else { - var query = {}; - query[idField] = id; - db.put(type, query, data).then(resolve).fail(reject);; + db.update(type, id, data).then(resolve).fail(reject);; } }); }; +OKResource.prototype.updateBatch = function(ids, datas) { + var self = this; + var db = this._db; + var type = this.type; + return Q.promise(function(resolve, reject) { + if (!ids || !ids.length || !datas || !datas.length || + ids.length !== datas.length) { + reject(new Error('Bad input')); + } else { + db.updateBatch(type, ids, datas).then(resolve).fail(reject); + } + }); +} + + +/** + * Get all documents in collection sorted by property, + * optionally in descending order + */ +OKResource.prototype.sortBy = function(prop, descend) { + return this._db.sortBy(this.type, prop, descend); +}; + OKResource.prototype.updateOrCreate = function(id, data) { data = data || {}; var type = this.type; var db = this._db; - var idField = this.idField; - var query = {}; - query[idField] = id; return Q.promise(function(resolve, reject) { if (!id) { - reject(new Error('No resource ID provided')); + reject(new Error('No ID provided')); } else { - db.get(type, query).then(function(persisted) { + db.get(type, id).then(function(persisted) { if (persisted) { - db.put(type, query, data).then(resolve).fail(reject); + db.update(type, id, data).then(resolve).fail(reject); } else { - db.create(type, data).then(resolve).fail(reject); + db.insert(type, data).then(resolve).fail(reject); } }).fail(reject); } @@ -196,11 +192,15 @@ function OKResourceInstance(resource, options) { // configuration, but may not actually be present // in the database and need custom logic to handle this. var staticData = cloneDeep(options.static); - var id = staticData[resource.idField]; + var id = resource.getID(staticData); if (!id) throw new Error( 'Cannot create static OKResourceInstance without an ID field'); + this.getID = function() { + return id; + }; + /** * Ensure that static data is provided on get */ @@ -209,9 +209,9 @@ function OKResourceInstance(resource, options) { resource.get(id).then(function(data) { // Note the assign call. Don't expose private references! if (data) { - resolve(assign({}, data, cloneDeep(staticData))); + resolve(assign(data, cloneDeep(staticData))); } else { - resolve(assign({}, cloneDeep(staticData))); + resolve(cloneDeep(staticData)); } }).fail(reject); }); @@ -286,14 +286,9 @@ function OKResourceInstance(resource, options) { }); Object.defineProperty(this, 'spec', { - value: resource.spec, - writable: false, - enumerable: true - }); - - Object.defineProperty(this, 'id', { - value: id, - writable: false, + get: function() { + return resource.spec + }, enumerable: true }); @@ -303,23 +298,11 @@ function OKResourceInstance(resource, options) { enumerable: true }); - Object.defineProperty(this, 'idField', { - value: resource.idField, - writable: false, - enumerable: true - }); - Object.defineProperty(this, 'bound', { value: true, writable: false, enumerable: true }); - - Object.defineProperty(this, 'class', { - value: resource, - writable: false, - enumerable: true - }); } module.exports = OKResource; diff --git a/app/node_modules/okschema/index.js b/app/node_modules/okschema/index.js index 4b215d1..d53ed7b 100644 --- a/app/node_modules/okschema/index.js +++ b/app/node_modules/okschema/index.js @@ -56,6 +56,11 @@ var types = { }]; } } + }, + // Special type for resource meta information + 'meta': { + parent: 'string', + assertValid: function(spec, value) {} } } @@ -69,8 +74,9 @@ function OKSchema(spec) { if (!spec) throw new Error('No spec provided to OKSchema'); spec = cloneDeep(spec); + var specKeys = Object.keys(spec); // Cache the mschema version of our spec - this._mschemaSpec = Object.keys(spec).reduce(function(cache, prop) { + this._mschemaSpec = specKeys.reduce(function(cache, prop) { // If custom type, return its parent spec var type = spec[prop].type; if (types[type]) { @@ -82,12 +88,46 @@ function OKSchema(spec) { return cache; }, {}); + // Find ID field + var idField; + specKeys.every(function(prop) { + if (prop === 'id' || spec[prop].id) { + idField = prop; + return false; + } else { + return true; + } + }); + + // Register autoincrement fields + // NOTE Does not work for nested fields + var autoIncrementFields = specKeys.reduce(function(arr, prop) { + var specProp = spec[prop]; + if (specProp.autoincrement) { + arr.push(prop); + } + return arr; + }, []); + Object.defineProperty(this, 'spec', { get: function() { return cloneDeep(spec); }, enumerable: true }); + + Object.defineProperty(this, 'idField', { + value: idField, + writable: true, + enumerable: true + }); + + Object.defineProperty(this, 'autoIncrementFields',{ + get: function() { + return cloneDeep(autoIncrementFields); + }, + enumerable: true + }); } OKSchema.prototype.assertValid = function(data) { diff --git a/app/node_modules/okserver/index.js b/app/node_modules/okserver/index.js index 1645eaa..cf06b3c 100644 --- a/app/node_modules/okserver/index.js +++ b/app/node_modules/okserver/index.js @@ -67,16 +67,6 @@ function OKServer(options) { // Make sure this lady is last. Checks whether the desired // route has a trailing-slash counterpart and redirects there app.use(slash()); - - /** - * Create a handler which redirect all requests to - * the same route with a trailing slash appended - */ - function redirect(routeNoSlash) { - return function(req, res) { - res.redirect(301, routeNoSlash + '/'); - } - } } OKServer.prototype.listen = function listen(port) { diff --git a/app/node_modules/oktemplate/index.js b/app/node_modules/oktemplate/index.js index a37f78e..f2c2e07 100644 --- a/app/node_modules/oktemplate/index.js +++ b/app/node_modules/oktemplate/index.js @@ -14,13 +14,24 @@ var filters = { * Return a string formatted version of a JSON object. * Useful for quick debugging of template data. */ - stringify: function(obj) { + prettify: function(obj) { try { return '<pre>' + stringify(obj) + '</pre>'; } catch (e) { + return 'Error prettifying'; + } + }, + + /** + * Serialize Javascript objects into a JSON string + */ + stringify: function(obj) { + try { + return JSON.stringify(obj); + } catch (e) { return 'Error stringifying'; } - } + }, }; diff --git a/app/node_modules/okview/index.js b/app/node_modules/okview/index.js index 6eebe6e..2d1c0a0 100644 --- a/app/node_modules/okview/index.js +++ b/app/node_modules/okview/index.js @@ -159,10 +159,10 @@ function fetchTemplateData(meta, queries, id) { // Inform template of ID in generic field if (manyResult) { result = result.map(function(data) { - return assign({}, data, {id: data[resource.idField]}) + return assign({}, data, {id: resource.getID(data)}) }); } else { - result = assign({}, result, {id: result[resource.idField]}); + result = assign({}, result, {id: resource.getID(result)}); } // If we have a lot of results for a certain type, // we pluralize the key and yield an array of results diff --git a/examples/db.json b/examples/db.json index 148391a..20d4510 100644 --- a/examples/db.json +++ b/examples/db.json @@ -3,8 +3,8 @@ "bread": [ { "type": "pretzel", - "description": "really a very tasty bread! yup yes", - "color": "green", + "description": "really a very tasty bread!", + "color": "red", "id": "pretzel", "title": "Pretzel Chips", "images": [ @@ -23,7 +23,8 @@ "token": "", "title": "", "thumb": "" - } + }, + "__index": 0 }, { "type": "bagel", @@ -38,31 +39,73 @@ "token": "112498725", "title": "FW14-2H-VIDEO-V4 2", "thumb": "http://i.vimeocdn.com/video/497493142_640.jpg" - } + }, + "__index": 1 }, { "type": "pumpernickel", - "description": "grandma's recipe", + "description": "yup", "id": "pumpernickel", - "title": "Pumpernickel", + "title": "ok", "images": [ { "uri": "cool", "caption": "cool" } - ] + ], + "color": "red", + "video": { + "url": "", + "type": "", + "token": "", + "title": "", + "thumb": "" + }, + "__index": 2 + }, + { + "type": "cracker", + "title": "", + "description": "once upon a time this noble creature etc", + "color": "red", + "video": { + "url": "", + "type": "", + "token": "", + "title": "", + "thumb": "" + }, + "__index": 4, + "id": "cracker" + }, + { + "type": "croissant", + "title": "", + "description": "wow just wow", + "color": "red", + "video": { + "url": "", + "type": "", + "token": "", + "title": "", + "thumb": "" + }, + "__index": 3, + "id": "croissant" } ], "page": [ { "title": "About Us", "body": "Just a small bakery", - "id": "about" + "id": "about", + "__index": 1 }, { - "title": "contact", + "title": "ok...", "body": "2406 Old Rd, San Juan Bautista", - "id": "contact" + "id": "contact", + "__index": 0 } ] }
\ No newline at end of file diff --git a/site/db.json b/site/db.json index 8ee246c..c07e321 100644 --- a/site/db.json +++ b/site/db.json @@ -27,24 +27,24 @@ "uri": "http://twohustlers.com/bigimages/ss15_3.jpg", "caption": "CURABITUR BLANDIT TEMPUS PORTTITOR 4" } - ] + ], + "__index": 1 } ], - "advertising": [], - "experiential": [], - "content": [], "page": [ { "id": "about", "title": "WHO WE ARE", "body": "WE ARE A COLLECTIVE OF DIGITAL NATIVES, INDUSTRY INFLUENCERS, TRENDSETTERS, MILLENNIAL COMMUNICATORS & UNORTHODOX STRATEGISTS – A NEW MEDIA CREATIVE LAB. TOGETHER WE CREATE LIVING, BREATHING, CONTENT DRIVEN EXPERIENCES THAT ENGAGE TODAY’S CONSUMER AUDIENCE. WE ARE WILD, PUNK, PROVOCATIVE, RADICAL, FEARLESS, SUBVERSIVE, BOISTEROUS, ANARCHIC, UNEXPECTED & DISRUPTIVE. MOST OF ALL WE ARE CURIOUS. WELCOME TO THE WORLD OF TWO HUSTLERS.", - "image": "http://www.lansdowneresort.com/meetings/assets/images/masthead/meetings-team-building.jpg" + "image": "http://www.lansdowneresort.com/meetings/assets/images/masthead/meetings-team-building.jpg", + "__index": 1 }, { "id": "contact", "title": "CONTACT", "body": "TWOHUSTLERS\r\n50 WHITE STREET\r\nNEW YORK, NY 10013\r\n<a href=\"mailto:info@twohustlers.com\">INFO@TWOHUSTLERS.COM</a>\r\n+1 646 370-1180\r\nTWOHUSTLERS ©2014\r\nWEBSITE BY <a href=\"http://okfoc.us/\">OKFOCUS</a>", - "image": "http://checkingintocollege.com/wp/wp-content/uploads/2014/08/angryphone.jpg" + "image": "http://checkingintocollege.com/wp/wp-content/uploads/2014/08/angryphone.jpg", + "__index": 0 } ] -}
\ No newline at end of file +} diff --git a/themes/okadmin/public/css/main.css b/themes/okadmin/public/css/main.css index ad940e8..3762fd4 100644 --- a/themes/okadmin/public/css/main.css +++ b/themes/okadmin/public/css/main.css @@ -14,11 +14,12 @@ html, body { background-attachment: scroll; } -ul { +ul, ol { padding: 0; list-style: none; } +.main.index .resource-category button, a { color: #A200FF; text-decoration: none; @@ -26,6 +27,7 @@ a { text-transform: uppercase; } +.main.index .resource-category button:hover, a:hover { border-bottom: 1px solid #A200FF; } @@ -65,7 +67,12 @@ a:visited { border: 2px solid #ddd; } -nav { +.main.index .resource-category.active li:before { + content: "፧"; + margin-right: 1em; +} + +.resource-nav { background: white; width: 10%; margin: 2.5em 1em; @@ -89,21 +96,63 @@ h2 { transform: rotate(-1deg); } -.main.index .resource-category a.add-new { - border-bottom: 3px solid rgba(0, 0, 0, 0); +.main.index .resource-category nav { float: right; - font-size: 1.5em; +} + +.main.index .resource-category.active ol { + cursor: -webkit-grab; + cursor: grab; +} + +.main.index .resource-category.active li a { + pointer-events: none; +} + + /* Makes the button look like a link */ +.main.index .resource-category button { + background: none !important; + height: 1.5em; + border: none; + padding: 0 !important; + font: inherit; + cursor: pointer; +} + +.main.index .resource-category .btn { + border-bottom: 3px solid rgba(0, 0, 0, 0); color: rgba(0, 0, 0, 0.25); + line-height: 20px; } -.main.index .resource-category li { - margin: 1em 0; +.main.index .resource-category .btn { + display: none; +} + +.main.index .resource-category .btn.active { + display: inline; } -.main.index .resource-category a.add-new:hover { +.main.index .resource-category .btn:hover { border-bottom: 3px solid rgba(0, 0, 0, 0.25); } +.main.index .resource-category .btn { + margin-right: 1em; +} + +.main.index .resource-category .btn:last-child { + margin-right: 0; +} +.main.index .resource-category .add-btn { + font-size: 20px; +} + +.main.index .resource-category li { + margin: 1em 0; +} + + .main.resource { float: left; margin-top: 2em; @@ -278,3 +327,7 @@ li.image-element .remove-image:hover { .clear { clear: both; } + +.hidden { + display: none; +} diff --git a/themes/okadmin/public/js/app.js b/themes/okadmin/public/js/app.js index 441172f..1ab9956 100644 --- a/themes/okadmin/public/js/app.js +++ b/themes/okadmin/public/js/app.js @@ -50,10 +50,10 @@ var OKAdmin = function(){ })) // fix post indexing in list-driven inputs - $("form").submit(function(){ + $(".main.resource form").submit(function(){ $(".image-element").each(function(index){ $(this).find("input,textarea").each(function(){ - var field = $(this).attr("name").replace(/\[\]/, "[" + index + "]") + var field = $(this).attr("name").replace(/\[[0-9]*\]/, "[" + index + "]") $(this).attr("name", field) }) }) @@ -67,6 +67,47 @@ var OKAdmin = function(){ e.preventDefault() } }) + + $(".resource-category").on("click", ".edit-btn", function(e) { + e.preventDefault(); + var $parent = $(e.delegateTarget); + var $editBtn = $parent.find(".edit-btn"); + var $cancelBtn = $parent.find(".cancel-btn"); + var $saveBtn = $parent.find(".save-btn"); + var $ol = $parent.find("ol"); + var toggles = [$parent, $cancelBtn, $saveBtn, $editBtn]; + + $ol.sortable(); + $ol.disableSelection(); + toggle(); + + $cancelBtn.one("click", function(e) { + $ol.sortable("cancel"); + $ol.enableSelection(); + toggle(); + }); + + $saveBtn.one("click", function(e) { + $ol.sortable(); + toggle(); + }); + + function toggle() { + toggles.forEach(function($el) { + $el.toggleClass('active'); + }) + } + }); + + $(".resource-category").on("submit", "form", function(e) { + var $parent = $(e.delegateTarget); + $parent.find(".resource-input").each(function(index) { + var $input = $(this); + var parsed = JSON.parse($input.val()); + parsed.__index = index; + $input.val(JSON.stringify(parsed)); + }) + }); function pressEnter(fn){ return function(e){ diff --git a/themes/okadmin/templates/index.liquid b/themes/okadmin/templates/index.liquid index 95c64dd..0672613 100644 --- a/themes/okadmin/templates/index.liquid +++ b/themes/okadmin/templates/index.liquid @@ -1,23 +1,36 @@ {% include 'partials/head' %} +{% include 'partials/flash' %} + <section class="index main"> {% for pair in resources %} {% assign name = pair[0] %} {% assign resource = pair[1] %} - {% assign spec = resource.spec %} <section class="resource-category {{name}}"> - <header> - <h2>{{name | capitalize}}</h2> - </header> - <ul class="resource-list"> - {% for data in resource.data %} - <li><a href="{{resource.type}}/{{data.id}}/">{{data.id}}</a></li> - {% endfor %} - </ul> - <footer> - <a class="add-new" href="{{resource.type}}/new/">+</a> - </footer> + <form action="{{resource.type}}/__batch/" method="POST"> + <header> + <h2>{{name | capitalize}}</h2> + </header> + <input type="hidden" name="_method" value="PUT"> + <ol class="resource-list"> + {% for data in resource.data %} + <li> + <a href="{{resource.type}}/{{data.id}}/">{{data.id}}</a> + <input class="resource-input" type="hidden" name="{{resource.type}}[{{forloop.index0}}]" value='{{data | stringify}}'> + </li> + {% endfor %} + </ol> + <footer> + <nav> + <a class="btn cancel-btn" href="#">cancel</a> + <button type="submit" + class="btn save-btn" href="#">save</button> + <a class="btn edit-btn active" href="#">edit</a> + <a class="btn add-btn active" href="{{resource.type}}/new/">+</a> + </nav> + </footer> + </form> </section> {% endfor %} diff --git a/themes/okadmin/templates/partials/errors.liquid b/themes/okadmin/templates/partials/flash.liquid index cdb0b25..1980ab5 100644 --- a/themes/okadmin/templates/partials/errors.liquid +++ b/themes/okadmin/templates/partials/flash.liquid @@ -1,10 +1,13 @@ +<div class="success"> + {% for info in success %} + <div class="message">{{info.action}}</div> + {% endfor %} +</div> <div class="errors"> {% for error in errors %} <div class="error"> <div class="message">{{error.message}}</div> - <div class="assertion"> - Expected {{error.expected}} but got {{error.actual}} - </div> </div> {% endfor %} </div> + diff --git a/themes/okadmin/templates/partials/inputs.liquid b/themes/okadmin/templates/partials/inputs.liquid index 7d23c9e..99258f3 100644 --- a/themes/okadmin/templates/partials/inputs.liquid +++ b/themes/okadmin/templates/partials/inputs.liquid @@ -4,24 +4,28 @@ {% assign type = spec.type %} <div class="property {{type}}"> - <label for="{{name}}">{{name | capitalize}}</label> + <label + {% if spec.hidden %} + class="hidden" + {% endif %} + for="{{name}}">{{name | capitalize}}</label> {% if type == 'string' %} <input - {% if spec.disabled %} - disabled="true" + {% if spec.hidden %} + class="hidden" {% endif %} name="{{name}}" type="text" value="{{spec.value}}"> {% elsif type == 'text' %} <textarea - {% if spec.disabled %} - disabled="true" + {% if spec.hidden %} + class="hidden" {% endif %} name="{{name}}">{{spec.value}}</textarea> {% elsif type == 'enum' %} <select - {% if spec.disabled %} - disabled="true" + {% if spec.hidden %} + class="hidden" {% endif %} name="{{name}}"> {% for option in spec.options %} @@ -43,8 +47,8 @@ <ol> {% for image in spec.value %} <li class="image-element"> - <input type="hidden" name="{{name}}[][uri]" value="{{image.uri}}"> - <textarea class="caption" name="{{name}}[][caption]">{{image.caption}}</textarea> + <input type="hidden" name="{{name}}[{{forloop.index0}}][uri]" value="{{image.uri}}"> + <textarea class="caption" name="{{name}}[{{forloop.index0}}][caption]">{{image.caption}}</textarea> <img src="{{image.uri}}" alt="{{image.caption}}"> <button class="remove-image">♲</button> </li> @@ -64,6 +68,8 @@ </li> </script> </div> + {% elsif type == 'meta' %} + <input class="hidden" type="hidden" name="{{name}}" value="{{spec.value}}"> {% else %} <p><pre style="color: red">Admin template doesn't support '{{type}}' properties!</pre></p> {% endif %} diff --git a/themes/okadmin/templates/resource.liquid b/themes/okadmin/templates/resource.liquid index c321e8a..8078778 100644 --- a/themes/okadmin/templates/resource.liquid +++ b/themes/okadmin/templates/resource.liquid @@ -1,8 +1,8 @@ {% include 'partials/head' %} -{% include 'partials/errors' %} +{% include 'partials/flash' %} -<nav> +<nav class="resource-nav"> <a href="../..">Back</a> </nav> diff --git a/themes/okadmin/templates/resource_new.liquid b/themes/okadmin/templates/resource_new.liquid index c57dd83..d059445 100644 --- a/themes/okadmin/templates/resource_new.liquid +++ b/themes/okadmin/templates/resource_new.liquid @@ -1,8 +1,8 @@ {% include 'partials/head' %} -{% include 'partials/errors' %} +{% include 'partials/flash' %} -<nav> +<nav class="resource-nav"> <a href="../..">Back</a> </nav> |
