├── .bowerrc ├── .gitignore ├── .rock.yml ├── Gruntfile.js ├── README.md ├── TODO ├── bower.json ├── config ├── default.json └── runtime.json ├── examples ├── basic.js └── files.js ├── index.js ├── lib ├── api.js ├── clone.js ├── collection.js ├── deferrals.js ├── escape.js ├── fields.js ├── file.js ├── gx-express-router.js ├── item-data.js ├── item.js ├── middleware.js ├── models.js ├── passport-strategy.js ├── permission.js ├── plugins.js ├── sort.js ├── status.js └── user.js ├── package.json ├── plugins ├── image │ ├── config.js │ ├── index.js │ ├── script.js │ ├── style.css │ └── template.html └── image_list │ ├── config.js │ ├── index.js │ ├── script.js │ ├── style.css │ └── template.html ├── public ├── css │ ├── api.css │ ├── collection.css │ ├── editor │ │ ├── editor.css │ │ └── preview.css │ ├── global.css │ ├── item.css │ ├── login.css │ ├── setup.css │ └── user.css ├── dist │ ├── css │ │ └── dist.css │ ├── epic │ │ ├── base │ │ │ └── epiceditor.css │ │ ├── editor │ │ │ ├── epic-dark.css │ │ │ └── epic-light.css │ │ ├── preview │ │ │ ├── bartik.css │ │ │ ├── github.css │ │ │ └── preview-dark.css │ │ └── themes │ │ │ ├── base │ │ │ └── epiceditor.css │ │ │ ├── editor │ │ │ ├── epic-dark.css │ │ │ └── epic-light.css │ │ │ └── preview │ │ │ ├── bartik.css │ │ │ ├── github.css │ │ │ └── preview-dark.css │ ├── font │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ └── fontawesome-webfont.woff │ └── js │ │ └── global.js ├── images │ ├── dataflight.png │ ├── dataflight_125.png │ └── showcase_logo.png ├── js │ └── lib │ │ ├── Showcase.Collection.js │ │ ├── Showcase.Form.Input.js │ │ └── Showcase.js └── vendor │ └── hljs │ ├── hljs.css │ └── hljs.min.js ├── routes ├── api.js ├── collection.js ├── files.js ├── item.js ├── login.js ├── setup.js ├── users.js └── workspaces.js ├── spec ├── fixtures.md ├── resources.md └── schema.md ├── tests ├── collection.js ├── escape.js ├── file.js ├── item.js ├── lib │ ├── config.js │ └── index.js ├── permission.js ├── selenium │ ├── .rock.yml │ ├── Gemfile │ ├── Gemfile.lock │ └── test.rb ├── sort.js └── status.js └── views ├── api.html ├── collection.html ├── collections.html ├── error.html ├── error_db.html ├── error_fixtures.html ├── error_schema.html ├── fields.html ├── fields ├── checkbox.html ├── input.html ├── markdown.html ├── select.html └── text.html ├── flash.html ├── footer.html ├── header.html ├── item.html ├── items.html ├── layout.html ├── login.html ├── pagination.html ├── permissions.html ├── revision.html ├── setup.html ├── user.html ├── users.html ├── workspace.html └── workspaces.html /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "components" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.cache 3 | components 4 | .bundle 5 | tests/Gemfile.lock 6 | tests/vendor/ 7 | config/default.json 8 | 9 | -------------------------------------------------------------------------------- /.rock.yml: -------------------------------------------------------------------------------- 1 | runtime: node42 2 | build: npm install && bower install && grunt 3 | run: node examples/basic run 4 | test: | 5 | if [[ -n "$ROCK_ARG1" ]];then 6 | node node_modules/.bin/nodeunit "tests/${ROCK_ARG1}" 7 | else 8 | node node_modules/.bin/nodeunit tests 9 | fi 10 | 11 | run_test_server: rm -f /var/tmp/showcase-test.sqlite ; node examples/basic schema-sync ; node examples/basic fixtures-sync ; PORT=9800 node examples/basic run 12 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | module.exports = function(grunt) { 2 | 3 | grunt.initConfig({ 4 | pkg: grunt.file.readJSON('package.json'), 5 | concat: { 6 | javascript: { 7 | options: { separator: ';' }, 8 | src: [ 9 | 'components/underscore/underscore.js', 10 | 'components/jquery/jquery.js', 11 | 'components/jquery-ui/ui/jquery.ui.core.js', 12 | 'components/jquery-ui/ui/jquery.ui.widget.js', 13 | 'components/jquery-ui/ui/jquery.ui.mouse.js', 14 | 'components/jquery-ui/ui/jquery.ui.sortable.js', 15 | 'components/bootstrap/js/bootstrap.js', 16 | 'components/EpicEditor/epiceditor/js/epiceditor.js', 17 | 'components/swig/index.js', 18 | 'components/dropzone/downloads/dropzone.js', 19 | 'public/js/lib/Showcase.js', 20 | 'public/js/lib/Showcase.Collection.js', 21 | 'public/js/lib/Showcase.Form.Input.js' 22 | ], 23 | dest: 'public/dist/js/global.js' 24 | }, 25 | css: { 26 | src: [ 27 | 'components/bootstrap/css/bootstrap.min.css', 28 | 'components/font-awesome/css/font-awesome.min.css', 29 | 'components/dropzone/downloads/css/basic.css', 30 | 'components/jquery-ui/themes/base/jquery-ui.css' 31 | ], 32 | dest: 'public/dist/css/dist.css' 33 | } 34 | }, 35 | copy: { 36 | main: { 37 | files: [ { expand: true, cwd: 'components/font-awesome/font/', src: ['**'], dest: 'public/dist/font/' } ] 38 | }, 39 | epic: { 40 | files: [ { expand: true, cwd: 'components/EpicEditor/epiceditor/themes/', src: ['**'], dest: 'public/dist/epic/themes/' } ] 41 | } 42 | } 43 | }); 44 | 45 | grunt.loadNpmTasks('grunt-contrib-concat'); 46 | grunt.loadNpmTasks('grunt-contrib-copy'); 47 | 48 | grunt.registerTask('default', ['concat', 'copy']); 49 | 50 | }; 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Showcase 2 | 3 | Lightweight pluggable CMS in Node.js with an admin interface and RESTful API 4 | 5 | #### Features: 6 | 7 | - Admin interface 8 | - Respectable read/write REST+JSON API 9 | - Pluggable file storage 10 | - User management w/ roles and permissions 11 | - Workspaces for multi-tentant support 12 | - Optional [Passport](http://passportjs.org) / OAuth integration 13 | - Custom fields via plugins 14 | - Events via [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) radio 15 | - Basic searching & sorting 16 | 17 | 18 | ## Getting Started 19 | 20 | Create `app.js` for your project: 21 | 22 | ```javascript 23 | var showcase = require('showcase'); 24 | 25 | showcase.initialize({ 26 | "database": { 27 | "dialect": "mysql", 28 | "host": "localhost", 29 | "database": "cms", 30 | "username": "cms", 31 | "password": "cms" 32 | }, 33 | "files": { 34 | "tmp_path": "/var/tmp", 35 | "storage_path": "/var/tmp" 36 | }, 37 | "port": 3000 38 | }); 39 | 40 | showcase.run(); 41 | ``` 42 | 43 | Initialize the database schema: 44 | 45 | ``` 46 | $ node --harmony-generators app schema-sync 47 | ``` 48 | 49 | Initialize the application fixtures data: 50 | 51 | ``` 52 | $ node --harmony-generators app fixtures-sync 53 | ``` 54 | 55 | Start your server: 56 | 57 | ``` 58 | $ PORT=4000 node --harmony-generators app run 59 | ``` 60 | 61 | Once your app is running, create an admin user and log in. Then create a workspace, then create some collections, and start adding items. 62 | 63 | > Node version 0.11.3 is recommended. Showcase requires support for ES6 generators, so node v0.11.2 or greater. You may also try your luck with [gnode](https://github.com/TooTallNate/gnode) and node v0.10.x. Also note that if you need sqlite support, node v0.11.3 is the latest supported version of node for that library. 64 | 65 | 66 | ## Workspaces, Collections, and Items 67 | 68 | Start by creating a new workspace. A workspace can have an administrator, and will contain a set of collections. You may often want to create a workspace per website, or per project. 69 | 70 | Within a workspace, create collections. A collection is a set of like items. Other CMSs might call a collection a "content type" or "custom post type". In a relational database, a collection would be analogous to a table. Define a collection and its fields, and then use the admin interface to add items. 71 | 72 | Access and modify data in collections through the built-in RESTful API. 73 | 74 | ## Showcase API 75 | 76 | ### showcase.initialize(options) 77 | 78 | Initialize the application, given a set of options: 79 | 80 | Sepcify authentication and authorization parameters under the `auth` key: 81 | 82 | - `auth.passport_strategy` specifies an instance of a [passport](http://passportjs.org) strategy; defaults to local authentication 83 | 84 | Specify database connection details under the `database` key: 85 | 86 | - `database.dialect` can be `mysql`, `postgres`, or `sqlite` 87 | - `database.storage` specifies the file on disk for the `sqlite` dialect 88 | - `database.host` specifies the database connection host 89 | - `database.port` specifies the database connection port 90 | - `database.database` specifies the database database name 91 | - `database.username` sepcifies the database username 92 | - `database.password` specifies the database password 93 | - `database.logging` is a boolean to specify whether to log each database query 94 | - `database.define` is a passthrough to Sequelize's default model definitions for any extra customization 95 | 96 | Under the hood these are sent through to the [Sequelize constructor](http://sequelizejs.com/documentation#usage-options). 97 | 98 | - `files.tmp_path` specifies where incoming uploaded files should be stored during transfer 99 | - `files.storage_path` specifies long term storage where uploaded files should reside 100 | - `files.store` specifies a function to store an uploaded file; defaults writing to `files.storage_path` 101 | - `files.retrieve` specifies a function to retrieve a stored file 102 | - `files.public_url` specifies a function to generate a public url for a stored file 103 | - `files.name` specifies a function to generate a filename for an uploaded file 104 | 105 | By default files will be stored directly to `files.storage_path`, but you may override this functionality by specifying override methods to `store` and `retrieve`, etc. These override methods take a callback as a parameter and are bound to `File` instances. See `examples/files.js` for an example. 106 | 107 | And other various top-level configuration options: 108 | 109 | - `port` specifies the TCP port to listen on 110 | - `secret` specifies the secret used to hash session data (anything random enough will do) 111 | 112 | ### showcase.registerField(field) 113 | 114 | Register a custom field. Supplied `field` should be an object specifying the following keys: 115 | 116 | ##### field.config 117 | 118 | An object containing configuration information for the field. Specify the following options: 119 | 120 | - `name` - name of the custom field type 121 | - `inflate` - function to populate item value from stored field data; accepts `field`, `data`, `models`, and `callback` parameters 122 | - `preview` - function to provide a lightweight preview of the data for rendering in HTML lists; accepts a `data` parameter containing the stored data 123 | - `validator` - function to validate input 124 | 125 | ##### field.style 126 | 127 | A string containing CSS to style field elements 128 | 129 | ##### field.script 130 | 131 | A string containing JavaScript library code to be executed on forms containing this field 132 | 133 | ##### field.template 134 | 135 | A Swig template for rendering the form field 136 | 137 | ### showcase.run() 138 | 139 | Start up the server. Specify the HTTP port either via a `PORT` environment variable, or through a `port` key in options sent to `showcase.initialize`. 140 | 141 | ## Events 142 | 143 | Subscribe to change events through `showcase.radio`, an event emitter. For example to log changes to items: 144 | 145 | ```javascript 146 | showcase.radio.on('itemUpdate', function(item) { 147 | console.log("item was updated ", item); 148 | }) 149 | ``` 150 | 151 | ##### itemUpdate 152 | 153 | Fires when an item is updated. Receives the updated item as a parameter. 154 | 155 | ##### itemCreate 156 | 157 | Fires when an item is created. Receives the nascent item as a parameter. 158 | 159 | ##### itemDestroy 160 | 161 | Fires when at item is destroyed. Receives the moribund item as a parameter. 162 | 163 | 164 | ## License 165 | 166 | Copyright (c) 2013-2014 David Chester 167 | 168 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 169 | 170 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 171 | 172 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 173 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | DONE 2 | 3 | - remove entities and items 4 | - edit entities and items 5 | - field descriptions 6 | - logo in 7 | - reorder fields 8 | - design / style 9 | - factor 10 | - JSON api 11 | - field validators 12 | - pagination 13 | - fix requiredness 14 | - make the API not hardcoded to port 3000 15 | - JSON API for writes 16 | - document how to run on a port 17 | - upload images 18 | - put handle before subscription for collections 19 | - cancel out of adding a field 20 | - require adding one field 21 | - fields: date, images 22 | - drafts / publishing 23 | - warn on removing items 24 | - support markdown 25 | - pills w/ counts 26 | - fix not propagating fields with errors 27 | - namespaces / workspaces 28 | - access control / roles 29 | - fix type validation 30 | - new fields out-of-order 31 | - write tests 32 | - custom fields: selects 33 | - customized preview 34 | 35 | 36 | NEXT 37 | 38 | - log all changes 39 | - use node domains for exception handling 40 | - deal w/ high number of fields in listing 41 | - settings: jsonp 42 | - storage engines 43 | - references 44 | - bulk import/export for data 45 | - bulk import/export for collections schemas 46 | - custom fields: display order w/ dnd 47 | - file browser 48 | - generic files upload 49 | - document adding permissions for a user? 50 | - explain what are fields 51 | - custom fields 52 | - internationalization 53 | - oauth / user sessions 54 | - csrf tokens 55 | - soft deletes 56 | - custom api routes per entity 57 | - disallow entirely numeric keys 58 | 59 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "showcase", 3 | "dependencies": { 4 | "underscore": "1.5.2", 5 | "swig": "https://raw.github.com/paularmstrong/swig/0056bc70e22dd85b94a9934deca59f714538b68f/js/swig.js", 6 | "bootstrap": "http://getbootstrap.com/2.3.2/assets/bootstrap.zip", 7 | "jquery": "2.0.3", 8 | "jquery-ui": "~1.10.3", 9 | "font-awesome": "3.2.1", 10 | "EpicEditor": "0.2.2", 11 | "dropzone": "3.7.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /config/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "database": { 3 | "dialect": "sqlite", 4 | "storage": "/var/tmp/showcase.sqlite", 5 | "database": "cms", 6 | "username": "cms", 7 | "password": "cms" 8 | }, 9 | "files": { 10 | "tmp_path": "/var/tmp", 11 | "storage_path": "/var/tmp" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /config/runtime.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /examples/basic.js: -------------------------------------------------------------------------------- 1 | var showcase = require('../index'); 2 | 3 | var config = { 4 | database: { 5 | dialect: "sqlite", 6 | storage: "/var/tmp/showcase-test.sqlite", 7 | }, 8 | files: { 9 | tmp_path: "/var/tmp", 10 | storage_path: "/var/tmp", 11 | } 12 | }; 13 | 14 | showcase.initialize(config); 15 | showcase.run(); 16 | 17 | -------------------------------------------------------------------------------- /examples/files.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var mv = require('mv'); 3 | var showcase = require('../index'); 4 | 5 | var config = { 6 | database: { 7 | dialect: "sqlite", 8 | storage: "/var/tmp/showcase.sqlite", 9 | }, 10 | files: { 11 | tmp_path: "/var/tmp", 12 | storage_path: "/var/tmp", 13 | store: function(callback) { 14 | var filename = this.name(); 15 | var target_path = path.join(this.storage_path, 'files', filename); 16 | mv(this.path, target_path, function(err) { 17 | if (err) throw new Error(err); 18 | this.path = target_path; 19 | this.url = '/files/' + filename; 20 | callback(); 21 | }.bind(this)); 22 | } 23 | } 24 | }; 25 | 26 | showcase.initialize(config); 27 | showcase.run(); 28 | 29 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var express = require('express'); 3 | var path = require('path'); 4 | var swig = require('swig'); 5 | var consolidate = require('consolidate'); 6 | var Dreamer = require('dreamer'); 7 | var async = require('async'); 8 | var flash = require('connect-flash'); 9 | var mkdirp = require('mkdirp'); 10 | var armrest = require('armrest'); 11 | var util = require('util'); 12 | var router = require('./lib/gx-express-router'); 13 | var gx = require('gx'); 14 | var passport = require('passport'); 15 | var strategy = require('./lib/passport-strategy'); 16 | var User = require('./lib/user'); 17 | 18 | var app = express(); 19 | app.showcase = {}; 20 | 21 | var views = __dirname + '/views'; 22 | swig.init({ root: views, allowErrors: true }); 23 | 24 | var externalMiddleware = []; 25 | 26 | exports.middleware = function(fn) { 27 | externalMiddleware.push(fn); 28 | }; 29 | 30 | exports.initialize = function(config) { 31 | 32 | config = config || {}; 33 | config.files = config.files || {}; 34 | config.auth = config.auth || {}; 35 | 36 | if (!config.files.tmp_path) { 37 | throw new Error("please specify files.tmp_path in config"); 38 | } 39 | 40 | if (!config.files.storage_path) { 41 | throw new Error("please specify files.storage_path in config"); 42 | } 43 | 44 | var secret = config.secret; 45 | 46 | if (!secret) { 47 | console.warn("falling back to default session secret; please send a secret to showcase.initialize"); 48 | secret = "arthur is fond of jimz"; 49 | } 50 | 51 | var dreamer = Dreamer.initialize({ 52 | app: app, 53 | schema: __dirname + "/spec/schema.md", 54 | resources: __dirname + "/spec/resources.md", 55 | fixtures: __dirname + "/spec/fixtures.md", 56 | database: config.database 57 | }); 58 | 59 | app.dreamer = dreamer; 60 | 61 | config.auth.passport_strategy = config.auth.passport_strategy || strategy.local; 62 | passport.use(config.auth.passport_strategy); 63 | 64 | var storagePath; 65 | if (!config.files.storage_path.match(/^\//)) { 66 | var relativeBase = path.dirname(require.main.filename); 67 | storagePath = path.join(relativeBase, config.files.storage_path); 68 | } else { 69 | storagePath = config.files.storage_path; 70 | } 71 | 72 | mkdirp.sync(config.files.storage_path + "/files"); 73 | 74 | var File = require('./lib/file'); 75 | File.methods(config.files); 76 | 77 | var middleware = require('./lib/middleware').initialize(app); 78 | app.showcase.middleware = middleware; 79 | 80 | app.showcase.config = config; 81 | 82 | app.configure(function(){ 83 | app.engine('.html', consolidate.swig); 84 | app.set('router', router); 85 | app.set('view engine', 'html'); 86 | app.set('views', views); 87 | app.set('port', process.env.PORT || config.port || 3000); 88 | app.use(middleware.errorHandler); 89 | app.use(express.static(path.join(__dirname, 'public')), { maxAge: 600 }); 90 | app.use(express.static(storagePath)); 91 | app.use(express.favicon()); 92 | app.use(express.logger('dev')); 93 | app.use(express.bodyParser()); 94 | app.use(express.methodOverride()); 95 | app.use(express.cookieParser('__SECRET__')); 96 | app.use('/files', express.cookieSession({ secret: secret })); 97 | app.use('/workspaces', express.cookieSession({ secret: secret })); 98 | app.use('/admin', express.cookieSession({ secret: secret })); 99 | app.use(flash()); 100 | app.use(middleware.flashLoader); 101 | app.use(passport.initialize()); 102 | app.use(middleware.sessionLocalizer); 103 | app.use(middleware.setupChecker); 104 | externalMiddleware.forEach(function(fn) { fn(app) }); 105 | app.use(middleware.fixturesLoader); 106 | app.use(app.router); 107 | }); 108 | 109 | app.get('/', function(req, res) { 110 | res.redirect("/workspaces"); 111 | }); 112 | 113 | require('./routes/setup.js').initialize(app); 114 | require('./routes/workspaces.js').initialize(app); 115 | require('./routes/collection.js').initialize(app); 116 | require('./routes/item.js').initialize(app); 117 | require('./routes/api.js').initialize(app); 118 | require('./routes/users.js').initialize(app); 119 | require('./routes/login.js').initialize(app); 120 | require('./routes/files.js').initialize(app); 121 | 122 | var Radio = function() {}; 123 | util.inherits(Radio, require('events').EventEmitter); 124 | 125 | app.radio = new Radio(); 126 | exports.radio = app.radio; 127 | } 128 | 129 | var plugins = require('./lib/plugins'); 130 | var registerPlugins = function() { 131 | 132 | var image_list = require('./plugins/image_list'); 133 | plugins.register('field', image_list); 134 | 135 | var image = require('./plugins/image'); 136 | plugins.register('field', image); 137 | } 138 | 139 | exports.plugins = plugins; 140 | exports.registerField = function(field) { 141 | plugins.register('field', field); 142 | }; 143 | 144 | exports.run = function() { 145 | registerPlugins(); 146 | plugins.route(app); 147 | var server = app.dreamer.dream(); 148 | return server; 149 | }; 150 | 151 | exports.app = app; 152 | 153 | exports.mergeUser = User.merge; 154 | 155 | -------------------------------------------------------------------------------- /lib/api.js: -------------------------------------------------------------------------------- 1 | var dreamer = require('dreamer'); 2 | var gx = require('gx'); 3 | 4 | var API = { 5 | 6 | user: null, 7 | 8 | setupUser: function*() { 9 | 10 | if (this._loaded) return; 11 | 12 | // try just once, whether we succeed or not 13 | this._loaded = true; 14 | 15 | var models = dreamer.instance.models; 16 | 17 | var properties = { 18 | username: "api", 19 | is_superuser: true 20 | }; 21 | 22 | try { 23 | yield models.users 24 | .create(properties) 25 | .complete(gx.resume); 26 | 27 | } catch(e) {} 28 | 29 | var user = yield models.users 30 | .find({ where: { username: "api" }}) 31 | .complete(gx.resume); 32 | 33 | this.user = user; 34 | 35 | if (!this.user) { 36 | console.warn("couldn't find api user"); 37 | } 38 | } 39 | }; 40 | 41 | gx.gentrify(API); 42 | 43 | module.exports = API; 44 | -------------------------------------------------------------------------------- /lib/clone.js: -------------------------------------------------------------------------------- 1 | module.exports = function(obj) { 2 | try { 3 | var clone = JSON.parse(JSON.stringify(obj)); 4 | return clone; 5 | } catch(e) { 6 | console.warn("couldn't clone object: " + e.message); 7 | return null; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /lib/collection.js: -------------------------------------------------------------------------------- 1 | var dreamer = require('dreamer'); 2 | var models = dreamer.instance.models; 3 | var controls = require('./fields').controls; 4 | var gx = require('gx'); 5 | 6 | var Collection = function() { 7 | this.initialize.apply(this, arguments); 8 | }; 9 | 10 | Collection.prototype = { 11 | 12 | attributes: ['title', 'description', 'name', 'workspace_handle'], 13 | 14 | initialize: function*(properties) { 15 | 16 | if (!properties) return; 17 | if (properties.id) this.id = properties.id; 18 | 19 | this.title = properties.title; 20 | this.description = properties.description; 21 | this.name = properties.name; 22 | 23 | this.workspace_handle = properties.workspace_handle; 24 | this._data = properties.save ? properties : models.collections.build(this); 25 | this.fields = yield this._fields(); 26 | 27 | return this; 28 | }, 29 | 30 | save: function*() { 31 | 32 | this.attributes.forEach(function(key) { 33 | this._data[key] = this[key]; 34 | }, this); 35 | 36 | yield this._data.save().complete(gx.resume); 37 | this.id = this._data.id; 38 | }, 39 | 40 | update: function*(args) { 41 | 42 | var changed_fields = args.fields; 43 | 44 | this.attributes.forEach(function(key) { 45 | if (key in args) this[key] = args[key]; 46 | }, this); 47 | 48 | yield this.save(); 49 | 50 | if (!changed_fields) return; 51 | 52 | var changes = this._resolveFieldModifications({ 53 | initial_fields: this.fields, 54 | changed_fields: changed_fields 55 | }); 56 | 57 | yield this._updateFields({ changes: changes }); 58 | }, 59 | 60 | destroy: function*() { 61 | yield this._data.destroy().complete(gx.resume); 62 | }, 63 | 64 | _fields: function*() { 65 | 66 | if (!this.id) return; 67 | 68 | var fields = yield models.collection_fields 69 | .findAll({ where: { collection_id: this.id } }) 70 | .complete(gx.resume); 71 | 72 | fields = JSON.parse(JSON.stringify(fields)); 73 | 74 | fields.forEach(function(field) { 75 | 76 | field.control = controls 77 | .filter(function(c) { return c.name == field.data_type; }) 78 | .shift(); 79 | 80 | if (!field.control) { 81 | console.warn("couldn't find control: " + field.data_type); 82 | } 83 | 84 | if (field.control.meta) field.control.meta.call(field); 85 | }); 86 | 87 | fields.sort(function(a, b) { return a.index - b.index }); 88 | 89 | return fields; 90 | }, 91 | 92 | _resolveFieldModifications: function(args) { 93 | 94 | var initial_fields = args.initial_fields; 95 | var changed_fields = args.changed_fields; 96 | 97 | var changes = { additions: [], deletions: [], modifications: [] }; 98 | 99 | changed_fields.forEach(function(field) { 100 | var match = initial_fields 101 | .filter(function(f) { return field.id && field.id == f.id; } ) 102 | .shift(); 103 | 104 | if (!match) changes.additions.push(field); 105 | }); 106 | 107 | initial_fields.forEach(function(field) { 108 | 109 | var match = changed_fields 110 | .filter(function(f) { return field.id == f.id; } ) 111 | .shift(); 112 | 113 | if (!match) { 114 | changes.deletions.push(field); 115 | } else { 116 | Collection.field_attribute_names.forEach(function(name) { 117 | if (field[name] != match[name]) { 118 | field[name] = match[name]; 119 | changes.modifications[field.id] = field; 120 | } 121 | }); 122 | } 123 | }); 124 | 125 | changes.modifications = Object.keys(changes.modifications) 126 | .map(function(k) { return changes.modifications[k] }); 127 | 128 | return changes; 129 | }, 130 | 131 | _createFields: function*(fields) { 132 | 133 | for (var i = 0; i < fields.length; i++) { 134 | 135 | var field_data = fields[i]; 136 | field_data.collection_id = this.id; 137 | delete field_data.id; 138 | 139 | yield models.collection_fields 140 | .create(field_data) 141 | .complete(gx.resume); 142 | } 143 | }, 144 | 145 | _updateFields: function*(args) { 146 | 147 | var changes = args.changes; 148 | 149 | for (var i = 0; i < changes.deletions.length; i++) { 150 | 151 | var deletion = changes.deletions[i]; 152 | 153 | var field = yield models.collection_fields 154 | .find({ where: { id: deletion.id } }) 155 | .complete(gx.resume); 156 | 157 | yield field.destroy().complete(gx.resume); 158 | } 159 | 160 | for (i = 0; i < changes.modifications.length; i++) { 161 | 162 | var modification = changes.modifications[i]; 163 | 164 | var field = yield models.collection_fields 165 | .find({ where: { id: modification.id } }) 166 | .complete(gx.resume); 167 | 168 | field.updateAttributes(modification) 169 | yield field.save().complete(gx.resume); 170 | } 171 | 172 | for (i = 0; i < changes.additions.length; i++) { 173 | 174 | var addition = changes.additions[i]; 175 | 176 | delete addition.id; 177 | var field = models.collection_fields.build(addition); 178 | yield field.save().complete(gx.resume); 179 | } 180 | } 181 | }; 182 | 183 | Collection.create = function*(args) { 184 | 185 | var fields = args.fields; 186 | var properties = {}; 187 | 188 | Collection.prototype.attributes.forEach(function(key) { 189 | properties[key] = args[key] 190 | }); 191 | 192 | var collection = yield new Collection(properties); 193 | 194 | yield collection.save(); 195 | yield collection._createFields(fields); 196 | 197 | return collection; 198 | }; 199 | 200 | Collection.load = function*(args) { 201 | 202 | var id = args.id; 203 | var name = args.name; 204 | var workspace_handle = args.workspace_handle; 205 | 206 | var criteria = {}; 207 | 208 | if (id) { criteria.id = id; } 209 | if (name) { criteria.name = name; } 210 | if (workspace_handle) { criteria.workspace_handle = workspace_handle; } 211 | 212 | var data = yield models.collections 213 | .find({ where: criteria }) 214 | .complete(gx.resume); 215 | 216 | var collection = yield new Collection(data); 217 | 218 | return collection; 219 | }; 220 | 221 | Collection.all = function*(args) { 222 | 223 | var workspace_handle = args.workspace_handle; 224 | 225 | models.collections 226 | .findAll({ where: { workspace_handle: workspace_handle } }) 227 | .complete(gx.resume); 228 | 229 | models.collection_fields.findAll().complete(gx.resume); 230 | 231 | var collections = yield null; 232 | var fields = yield null; 233 | 234 | collections = collections.map(function(c) { return c.values }); 235 | 236 | var map = {}; 237 | collections.forEach(function(c) { map[c.id] = c; }); 238 | 239 | fields.forEach(function(field) { 240 | 241 | var collection = map[field.collection_id]; 242 | if (!collection) return; 243 | 244 | collection.fields = collection.fields || []; 245 | collection.fields.push(field.values); 246 | }); 247 | 248 | return collections; 249 | }; 250 | 251 | Collection.itemCounts = function*(args) { 252 | 253 | var query = "select collection_id, count(*) as count from items group by 1"; 254 | var rows = yield dreamer.instance.db.query(query).complete(gx.resume); 255 | 256 | var counts = {}; 257 | rows.forEach(function(row) { 258 | counts[row.collection_id] = row.count; 259 | }); 260 | 261 | return counts; 262 | }; 263 | 264 | Collection.field_attribute_names = [ 'id', 'title', 'name', 'data_type', 'description', 'is_required', 'index', 'meta' ]; 265 | 266 | Collection = gx.class(Collection, { functions: false }); 267 | 268 | module.exports = Collection; 269 | 270 | -------------------------------------------------------------------------------- /lib/deferrals.js: -------------------------------------------------------------------------------- 1 | var Deferrals = function() { 2 | this.queue = []; 3 | for (var i = 0; i < arguments.length; i++) { 4 | this.queue.push(arguments[i]); 5 | } 6 | this.run = function() { 7 | var deferral = this.queue.shift(); 8 | deferral(); 9 | }; 10 | }; 11 | 12 | module.exports = Deferrals; 13 | 14 | -------------------------------------------------------------------------------- /lib/escape.js: -------------------------------------------------------------------------------- 1 | exports.html = function(input) { 2 | 3 | if (!input) return; 4 | 5 | var input = String(input); 6 | 7 | return input.replace(/&(?!amp;|lt;|gt;|quot;|#39;)/g, '&') 8 | .replace(//g, '>') 10 | .replace(/"/g, '"') 11 | .replace(/'/g, '''); 12 | 13 | }; 14 | -------------------------------------------------------------------------------- /lib/fields.js: -------------------------------------------------------------------------------- 1 | var controls = [ 2 | { name: 'string', template: 'fields/input.html', validator: 'notEmpty', placeholder: '' }, 3 | { name: 'text', template: 'fields/text.html', validator: 'notEmpty', placeholder: '' }, 4 | { name: 'markdown', template: 'fields/markdown.html', validator: 'notEmpty', placeholder: '' }, 5 | { name: 'email', template: 'fields/input.html', validator: 'isEmail', placeholder: '' }, 6 | { name: 'ip_address', template: 'fields/input.html', validator: 'isIP', placeholder: 'XXX.XXX.XXX.XXX' }, 7 | { name: 'url', template: 'fields/input.html', validator: 'isUrl', placeholder: '' }, 8 | { name: 'date', template: 'fields/input.html', validator: 'isDate', placeholder: 'YYYY-MM-DD' }, 9 | { 10 | name: 'number', 11 | template: 'fields/input.html', 12 | validator: 'isNumeric', 13 | placeholder: '', 14 | inflate: function(field, item, models, callback) { 15 | callback(Number(item.data[field.name])); 16 | } 17 | }, 18 | { 19 | name: "select", 20 | template: "fields/select.html", 21 | meta: function() { 22 | if (this.meta && this.meta.split) { 23 | this.options = []; 24 | var lines = this.meta.split(/\n/); 25 | lines.forEach(function(line) { 26 | var option = {}; 27 | line = line.trim(); 28 | var matches = line.match(/(.+?)( \[(\w+)\])?$/); 29 | if (matches) { 30 | var title = matches[1]; 31 | var handle = matches[3]; 32 | option.title = title; 33 | option.handle = handle === undefined ? title : handle; 34 | } 35 | this.options.push(option); 36 | }, this); 37 | } 38 | }, 39 | meta_description: "Enter options each on their own line with optional handles in sqaure brackets following titles. For example: 'Black and White [b_w]'", 40 | preview: function(value, field, item, collection) { 41 | var preview = field.options.filter(function(option) { return value == option.handle }).shift(); 42 | return preview ? preview.title : value; 43 | } 44 | }, 45 | { 46 | name: 'checkbox', 47 | template: 'fields/checkbox.html', 48 | normalize: function(value) { 49 | return Array.isArray(value) ? value[value.length - 1] : value; 50 | }, 51 | inflate: function(field, item, models, callback) { 52 | var value = item.data[field.name]; 53 | return callback(Number(value) ? true : false); 54 | }, 55 | preview: function(value) { 56 | return Number(value) ? "✔" : ""; 57 | }, 58 | } 59 | ]; 60 | 61 | exports.controls = controls; 62 | 63 | exports.register = function(control) { 64 | controls.push(control); 65 | }; 66 | -------------------------------------------------------------------------------- /lib/file.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var dreamer = require('dreamer'); 3 | var models = dreamer.instance.models; 4 | var gx = require('gx'); 5 | var mv = require('mv'); 6 | var path = require('path'); 7 | var sha1 = require('sha1'); 8 | 9 | var File = function() { 10 | this.initialize.apply(this, arguments); 11 | }; 12 | 13 | File.prototype = { 14 | 15 | initialize: function*(properties) { 16 | 17 | if (properties.id) this.id = properties.id; 18 | 19 | if (!properties.path) throw new Error("we need a file path"); 20 | if (!properties.size) throw new Error("we need a file size"); 21 | if (!properties.original_filename) throw new Error("we need an original_filename"); 22 | if (!properties.content_type) throw new Error("we need a content_type"); 23 | 24 | this.item_id = properties.item_id; 25 | this.path = properties.path; 26 | this.url = properties.url; 27 | this.size = properties.size; 28 | this.original_filename = properties.original_filename; 29 | this.content_type = properties.content_type; 30 | this.storage_path = properties.storage_path; 31 | this.meta_json = '{}'; 32 | this.description = ''; 33 | 34 | this._model_instance = properties.save ? properties : models.files.build(this); 35 | 36 | return this; 37 | }, 38 | 39 | store: function*() { 40 | 41 | var filename = this.name(); 42 | var target_path = path.join(this.storage_path, 'files', filename); 43 | yield mv(this.path, target_path, gx.resume); 44 | this.path = target_path; 45 | this.url = '/files/' + filename; 46 | }, 47 | 48 | retrieve: function(args) { 49 | 50 | var stream = fs.createReadStream(this.path); 51 | return stream; 52 | }, 53 | 54 | save: function*() { 55 | 56 | File.mutable_attributes.forEach(function(key) { 57 | this._model_instance[key] = this[key]; 58 | }, this); 59 | 60 | yield this._model_instance.save().complete(gx.resume); 61 | this.id = this._model_instance.id; 62 | }, 63 | 64 | public_url: function() { 65 | return '/files/' + this.path; 66 | }, 67 | 68 | name: function() { 69 | var filename = sha1(this.path + Math.random()) + '-' + this.original_filename; 70 | return filename; 71 | } 72 | }; 73 | 74 | File.load = function*(args) { 75 | 76 | var id = args.id; 77 | 78 | var data = yield models.files 79 | .find({ where: { id: id }}) 80 | .complete(gx.resume); 81 | 82 | var file = yield new File(data); 83 | return file; 84 | }; 85 | 86 | 87 | File.all = function*(args) { 88 | 89 | var ids = args.id; 90 | 91 | var file_data = yield models.files 92 | .findAll({ where: { id : ids } }) 93 | .complete(gx.resume); 94 | 95 | var files = []; 96 | file_data.forEach( function(data) { 97 | var file = new File(data); 98 | files.push(file); 99 | }); 100 | return files; 101 | }; 102 | 103 | 104 | File.create = function*(args) { 105 | 106 | var item_id = args.item_id; 107 | var original_filename = args.original_filename; 108 | var size = args.size; 109 | var path = args.source_path; 110 | var content_type = args.content_type; 111 | var storage_path = args.storage_path; 112 | 113 | if (!storage_path) throw new Error("we need a storage_path"); 114 | 115 | var file = yield new File({ 116 | item_id: item_id, 117 | size: size, 118 | original_filename: original_filename, 119 | path: path, 120 | content_type: content_type, 121 | storage_path: storage_path 122 | }); 123 | 124 | yield file.store(); 125 | yield file.save(); 126 | 127 | return file; 128 | }; 129 | 130 | File.mutable_attributes = ['description', 'meta_json', 'path', 'url']; 131 | 132 | File.methods = function(methods) { 133 | ['store', 'retrieve', 'name', 'public_url'].forEach(function(method_name) { 134 | if (methods[method_name]) { 135 | File.prototype[method_name] = gx.gentrify(methods[method_name]); 136 | } 137 | }); 138 | }; 139 | 140 | File = gx.class(File, { functions: false }); 141 | 142 | File.distill = function(file) { 143 | var distilled_file = JSON.parse(JSON.stringify(file)); 144 | delete distilled_file._model_instance; 145 | return distilled_file; 146 | } 147 | 148 | module.exports = File; 149 | 150 | -------------------------------------------------------------------------------- /lib/gx-express-router.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var gx = require('gx'); 3 | 4 | var _set = express.application.set; 5 | 6 | express.application.set = function(setting, val) { 7 | 8 | // punch this in since it's otherwise inaccessible 9 | if (setting == 'router' && val) { 10 | this._router = val; 11 | } 12 | return _set.apply(this, arguments); 13 | }; 14 | 15 | var router = new express.Router(); 16 | var _route = router.route; 17 | 18 | router.route = function(method, path, callbacks) { 19 | 20 | var functions = []; 21 | var callbacks = flatten([].slice.call(arguments, 2)); 22 | 23 | callbacks.forEach(function(callback) { 24 | 25 | if (callback.constructor.name == 'GeneratorFunction') { 26 | functions.push(function(req, res) { 27 | gx.fn(callback)(req, res) 28 | }); 29 | } else { 30 | functions.push(callback); 31 | } 32 | }); 33 | 34 | _route.call(this, method, path, functions); 35 | }; 36 | 37 | // from express utils 38 | function flatten(arr, ret) { 39 | 40 | var ret = ret || []; 41 | var len = arr.length; 42 | 43 | for (var i = 0; i < len; ++i) { 44 | if (Array.isArray(arr[i])) { 45 | flatten(arr[i], ret); 46 | } else { 47 | ret.push(arr[i]); 48 | } 49 | } 50 | return ret; 51 | }; 52 | 53 | module.exports = router; 54 | -------------------------------------------------------------------------------- /lib/item-data.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | var moment = require('moment'); 3 | var dreamer = require('dreamer').instance; 4 | var gx = require('gx'); 5 | var models = dreamer.models; 6 | 7 | var ItemData = function() { 8 | this.initialize.apply(this, arguments); 9 | }; 10 | 11 | ItemData.find = function*(args) { 12 | 13 | var item_id = args.item_id; 14 | 15 | var item_data = yield models.item_data 16 | .find({ where: { item_id: item_id } }) 17 | .complete(gx.resume); 18 | 19 | item_data.data = JSON.parse(item_data.data); 20 | return item_data; 21 | }; 22 | 23 | ItemData.update = function*(args) { 24 | 25 | var item = args.item; 26 | var data = args.data; 27 | var user_id = args.user_id; 28 | 29 | var storable_data = JSON.stringify(data); 30 | 31 | var item_data = yield models.item_data 32 | .find({ where: { item_id: item.id } }) 33 | .complete(gx.resume); 34 | 35 | var initial_data = JSON.parse(item_data.data); 36 | var initial_user_id = item_data.user_id; 37 | 38 | try { 39 | assert.deepEqual(initial_data, data); 40 | return [item_data, false]; 41 | 42 | } catch (e) { 43 | 44 | item_data.data = storable_data; 45 | yield item_data.save().complete(gx.resume); 46 | 47 | models.item_data_revisions.create({ 48 | user_id: initial_user_id, 49 | item_id: item.id, 50 | data: JSON.stringify(initial_data), 51 | content_type: 'application/json' 52 | }); 53 | 54 | return [item_data, true]; 55 | } 56 | }; 57 | 58 | ItemData.create = function*(args) { 59 | 60 | var item = args.item; 61 | var item_id = item.id; 62 | var data = args.data; 63 | var user_id = args.user_id; 64 | 65 | var data = models.item_data.build({ 66 | item_id: item_id, 67 | data: JSON.stringify(data), 68 | content_type: 'application/json', 69 | user_id: user_id 70 | }); 71 | 72 | yield data.save().complete(gx.resume); 73 | }; 74 | 75 | ItemData.findAll = function*(args) { 76 | 77 | var success = args.success; 78 | var error = args.error || console.warn; 79 | var item_ids = args.item_ids; 80 | 81 | var item_data = yield models.item_data 82 | .findAll({ where: { item_id: item_ids } }) 83 | .complete(gx.resume); 84 | 85 | var data = {}; 86 | 87 | item_data.forEach(function(item) { 88 | data[item.item_id] = JSON.parse(item.data); 89 | }); 90 | 91 | return data; 92 | }; 93 | 94 | ItemData = gx.gentrify(ItemData); 95 | 96 | module.exports = ItemData; 97 | 98 | -------------------------------------------------------------------------------- /lib/middleware.js: -------------------------------------------------------------------------------- 1 | var gx = require('gx'); 2 | var API = require('./api'); 3 | var Status = require('./status'); 4 | var Permissions = require('./permission'); 5 | 6 | exports.initialize = function(app) { 7 | 8 | var models = app.dreamer.models; 9 | 10 | var workspaceLoader = function(req, res, next) { 11 | 12 | var workspace_handle = req.params.workspace_handle; 13 | if (!workspace_handle) return next(); 14 | 15 | models.workspaces.find({ where: { handle: workspace_handle } }) 16 | .error(req.error) 17 | .success(function(workspace) { 18 | 19 | if (!workspace) return req.error("invalid workspace handle: " + workspace_handle); 20 | 21 | res.locals.workspace = workspace; 22 | req.showcase = req.showcase || {}; 23 | req.showcase.workspace = workspace; 24 | next(); 25 | }); 26 | }; 27 | 28 | var workspacePermission = function(required_permission) { 29 | 30 | var levels = { 'superuser': 1000, 'administrator': 300, 'editor': 200 }; 31 | var required_level = levels[required_permission]; 32 | 33 | if (!required_level) throw new Error("Couldn't find permission level: " + required_permission); 34 | 35 | return function(req, res, next) { 36 | 37 | var workspace_handle = req.params.workspace_handle; 38 | 39 | if (req.session && req.session.is_superuser) { 40 | req.showcase.workspace_user_permission = res.locals.workspace_user_permission = 'administrator'; 41 | next(); 42 | 43 | } else if (req.method == 'GET') { 44 | /* get workspace permission level to drive display of page controls */ 45 | if (req.session && req.session.username) { 46 | models.workspace_user_permissions 47 | .find({ where: { user_id: req.session.user_id, workspace_handle: workspace_handle }}) 48 | .error(req.error) 49 | .success(function(workspace_user_permission) { 50 | if (!workspace_user_permission) { return; } 51 | var permission = Permissions.name(workspace_user_permission.selectedValues.permission_id); 52 | req.showcase.workspace_user_permission = res.locals.workspace_user_permission = permission; 53 | }); 54 | } 55 | next(); 56 | 57 | } else if (req.session && req.session.username) { 58 | /* get workspace permission level to determine update permissions */ 59 | models.workspace_user_permissions 60 | .find({ where: { user_id: req.session.user_id, workspace_handle: workspace_handle }}) 61 | .error(req.error) 62 | .success(function(workspace_user_permission) { 63 | if (!workspace_user_permission) { return; } 64 | var permission = Permissions.name(workspace_user_permission.selectedValues.permission_id); 65 | var level = levels[permission] || 0; 66 | req.showcase.workspace_user_permission = res.locals.workspace_user_permission = permission; 67 | 68 | if (req.method !== 'GET' && level < required_level) { 69 | console.warn("bad permissions: ", req.method, permission, required_level, level); 70 | req.flash('danger', "You don't have permissions to do that!"); 71 | res.redirect('/'); 72 | } else { 73 | next(); 74 | } 75 | }); 76 | } else { 77 | req.flash('info', 'Please login...'); 78 | res.redirect("/admin/login"); 79 | } 80 | }; 81 | }; 82 | 83 | var requireSuperuser = function(req, res, next) { 84 | 85 | if (req.session && req.session.is_superuser) { 86 | next(); 87 | } else { 88 | req.flash('danger', 'Only a superuser admin can do that'); 89 | res.redirect('/'); 90 | } 91 | }; 92 | 93 | var errorHandler = function(req, res, next) { 94 | 95 | req.error = function(error) { 96 | 97 | if (arguments.length == 2) { 98 | var status = arguments[0]; 99 | var error = arguments[1]; 100 | } else { 101 | var status = 500; 102 | var error = arguments[0]; 103 | } 104 | 105 | try { 106 | var response = JSON.parse(JSON.stringify(error)); 107 | response.message = error.toString(); 108 | } catch(e) { 109 | var response = { error: error }; 110 | } 111 | 112 | res.json(status, response); 113 | }; 114 | 115 | next(); 116 | }; 117 | 118 | var flashLoader = function(req, res, next) { 119 | 120 | var _render = res.render; 121 | 122 | res.render = function() { 123 | res.locals.messages = req.flash(); 124 | _render.apply(res, arguments); 125 | }; 126 | 127 | next(); 128 | }; 129 | 130 | var sessionLocalizer = function(req, res, next) { 131 | res.locals.session = req.session; 132 | next(); 133 | }; 134 | 135 | var setupChecker = function(req, res, next) { 136 | 137 | if (app.showcase.setupComplete) return next(); 138 | if (req.path == '/admin/setup') return next(); 139 | if (req.path.match(/\/admin\/error/)) return next(); 140 | if (req.path.match(/\/js\//)) return next(); 141 | if (req.path.match(/\/css\//)) return next(); 142 | if (req.path == '/') return next(); 143 | 144 | var models = app.dreamer.models; 145 | var passport_strategy = app.showcase.config.auth.passport_strategy; 146 | 147 | models.users.count({ where: "username != 'api'" }) 148 | .error(function(err) { 149 | 150 | var message = 151 | err.toString ? err.toString() : 152 | typeof err == 'object' ? JSON.stringify(err) : 'unknown error'; 153 | 154 | if (message.match(/no.such.table/i)) { 155 | req.flash('danger', message); 156 | res.status(500); 157 | res.render("error_schema.html"); 158 | } else { 159 | req.flash('danger', message); 160 | res.status(500); 161 | res.render("error_db.html"); 162 | } 163 | }) 164 | .success(function(users_count) { 165 | 166 | gx(function*() { 167 | 168 | var status_count = yield models.statuses.count().complete(gx.resume); 169 | 170 | if (!status_count) { 171 | req.flash('danger', "Couldn't find 'statuses' fixtures data"); 172 | res.status(500); 173 | res.render("error_fixtures.html"); 174 | 175 | } else if (users_count) { 176 | app.showcase.setupComplete = true; 177 | next(); 178 | 179 | } else if (passport_strategy._callbackURL) { 180 | // all set if we have a third-party auth strategy 181 | app.showcase.setupComplete = true; 182 | next(); 183 | 184 | } else { 185 | res.redirect("/admin/setup"); 186 | } 187 | }); 188 | }); 189 | }; 190 | 191 | var fixturesLoader = function(req, res, next) { 192 | 193 | if (req.path == '/') return next(); 194 | if (!app.showcase.setupComplete) return next(); 195 | 196 | gx(function*() { 197 | yield Status.load(); 198 | yield Permissions.load(); 199 | yield API.setupUser(); 200 | next(); 201 | }); 202 | }; 203 | 204 | return { 205 | workspacePermission: workspacePermission, 206 | workspaceLoader: workspaceLoader, 207 | requireSuperuser: requireSuperuser, 208 | errorHandler: errorHandler, 209 | flashLoader: flashLoader, 210 | sessionLocalizer: sessionLocalizer, 211 | setupChecker: setupChecker, 212 | fixturesLoader: fixturesLoader 213 | }; 214 | }; 215 | -------------------------------------------------------------------------------- /lib/models.js: -------------------------------------------------------------------------------- 1 | var dreamer = require('dreamer').instance; 2 | module.exports = dreamer.models; 3 | 4 | -------------------------------------------------------------------------------- /lib/passport-strategy.js: -------------------------------------------------------------------------------- 1 | var PassportLocal = require('passport-local'); 2 | 3 | var local = new PassportLocal.Strategy(function(username, password, done) { 4 | var models = require('dreamer').instance.models; 5 | models.users 6 | .find({ where: { username: username } }) 7 | .complete(function(err, user) { 8 | if (err) return done(err); 9 | if (!user) return done(null, false, { message: 'No user by that username' }); 10 | done(null, user); 11 | }); 12 | }); 13 | 14 | exports.local = local; 15 | -------------------------------------------------------------------------------- /lib/permission.js: -------------------------------------------------------------------------------- 1 | var dreamer = require('dreamer'); 2 | var models = dreamer.instance.models; 3 | var gx = require('gx'); 4 | 5 | var permissions; 6 | var map = { id: {}, name: {} }; 7 | 8 | var Permission = { 9 | 10 | load: function*() { 11 | 12 | if (Permission._loaded) return; 13 | 14 | var _permissions = yield models.permissions.findAll() 15 | .complete(gx.resume); 16 | 17 | permissions = JSON.parse(JSON.stringify(_permissions)); 18 | 19 | permissions.forEach(function(permission) { 20 | map.name[permission.name] = permission.id; 21 | map.id[permission.id] = permission.name; 22 | }); 23 | 24 | Permission._loaded = true; 25 | }, 26 | 27 | id: function(name) { 28 | 29 | if (!Permission._loaded) throw new Error("permissions not yet loaded"); 30 | 31 | var permission_id = map.name[name]; 32 | if (!permission_id) { 33 | throw new Error("couldn't find permission id for permission name " + name); 34 | } 35 | return permission_id; 36 | }, 37 | 38 | name: function(id) { 39 | 40 | if (!Permission._loaded) throw new Error("permissions not yet loaded"); 41 | 42 | var permission_name = map.id[id]; 43 | if (!permission_name) { 44 | throw new Error("couldn't find permission name for permission id " + id); 45 | } 46 | return permission_name; 47 | } 48 | }; 49 | 50 | module.exports = Permission; 51 | gx.keys(Permission, { functions: false }); 52 | 53 | -------------------------------------------------------------------------------- /lib/plugins.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var swig = require('swig'); 4 | 5 | var fields = require('./fields'); 6 | 7 | var plugins = { field: [], auth: [], behavior: [], middleware: [] }; 8 | 9 | var style = ''; 10 | var script = ''; 11 | 12 | exports.slurp = function(dir) { 13 | var config = require(path.join(dir, './config')); 14 | 15 | var script = fs.readFileSync(path.join(dir, 'script.js'), 'utf-8'); 16 | var template = fs.readFileSync(path.join(dir, 'template.html'), 'utf-8'); 17 | var style = fs.readFileSync(path.join(dir, 'style.css'), 'utf-8'); 18 | 19 | return { 20 | config: config, 21 | script: script, 22 | template: template, 23 | style: style 24 | }; 25 | } 26 | 27 | exports.register = function(type, plugin) { 28 | 29 | if (!plugins[type]) throw new Error('bad plugin type: ' + type); 30 | 31 | plugins[type].push(plugin); 32 | 33 | if (type == 'field') { 34 | fields.register(plugin.config); 35 | } 36 | 37 | if (plugin.style) style += '\n' + plugin.style; 38 | if (plugin.script) script += ';' + plugin.script; 39 | 40 | if (plugin.template) { 41 | var template_path = path.join('plugins', type, plugin.config.name); 42 | swig.compile(plugin.template, { filename: template_path }); 43 | plugin.config.template = template_path; 44 | } 45 | }; 46 | 47 | exports.route = function(app) { 48 | 49 | app.get('/css/plugins.css', function(req, res) { 50 | res.set('content-type', 'text/css'); 51 | res.send(style); 52 | }); 53 | 54 | app.get('/js/plugins.js', function(req, res) { 55 | res.set('content-type', 'application/javascript'); 56 | res.send(script); 57 | }); 58 | }; 59 | -------------------------------------------------------------------------------- /lib/sort.js: -------------------------------------------------------------------------------- 1 | var sort = { 2 | deserialize: function(serialized_sort) { 3 | if (!serialized_sort) return null; 4 | return serialized_sort 5 | .split(',') 6 | .map(function(s) { 7 | var comps = s.split(':'); 8 | var field_name = comps[0]; 9 | var order = comps[1] == 'desc' ? 'desc' : 'asc'; 10 | return { 11 | field_name: field_name, 12 | order: order 13 | }; 14 | }); 15 | }, 16 | serialize: function(sort) { 17 | if (!sort) return ''; 18 | if (!sort.length) return ''; 19 | return sort 20 | .map(function(s) { 21 | var field_name = s.field_name; 22 | var order = s.order == 'desc' ? 'desc' : null; 23 | return [field_name, order].filter(function(f) { return f }).join(':'); 24 | }) 25 | .join(','); 26 | } 27 | }; 28 | 29 | module.exports = sort; 30 | 31 | -------------------------------------------------------------------------------- /lib/status.js: -------------------------------------------------------------------------------- 1 | var dreamer = require('dreamer'); 2 | var models = dreamer.instance.models; 3 | var gx = require('gx'); 4 | 5 | var statuses; 6 | var map = { id: {}, name: {} }; 7 | 8 | var Status = { 9 | 10 | load: function*() { 11 | 12 | if (Status._loaded) return; 13 | 14 | var _statuses = yield models.statuses.findAll() 15 | .complete(gx.resume); 16 | 17 | statuses = JSON.parse(JSON.stringify(_statuses)); 18 | 19 | statuses.forEach(function(status) { 20 | map.name[status.name] = status.id; 21 | map.id[status.id] = status.name; 22 | }); 23 | 24 | Status._loaded = true; 25 | }, 26 | 27 | id: function(name) { 28 | 29 | if (!Status._loaded) throw new Error("statuses not yet loaded"); 30 | 31 | var status_id = map.name[name]; 32 | if (!status_id) { 33 | throw new Error("couldn't find status id for status name " + name); 34 | } 35 | return status_id; 36 | }, 37 | 38 | name: function(id) { 39 | 40 | if (!Status._loaded) throw new Error("statuses not yet loaded"); 41 | 42 | var status_name = map.id[id]; 43 | if (!status_name) { 44 | throw new Error("couldn't find status name for status id " + id); 45 | } 46 | return status_name; 47 | } 48 | }; 49 | 50 | module.exports = Status; 51 | gx.keys(Status, { functions: false }); 52 | 53 | -------------------------------------------------------------------------------- /lib/user.js: -------------------------------------------------------------------------------- 1 | var User = { 2 | 3 | merge: function(user_data, done) { 4 | 5 | var models = require('dreamer').instance.models; 6 | var username = user_data.username; 7 | var is_superuser = user_data.is_superuser; 8 | 9 | models.users 10 | .findOrCreate({ username: username }, { is_superuser: is_superuser }) 11 | .complete(function(err, data) { 12 | if (err) return done(err); 13 | var showcase_user = JSON.parse(JSON.stringify(data)); 14 | done(null, showcase_user); 15 | }); 16 | } 17 | }; 18 | 19 | module.exports = User; 20 | 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "showcase", 3 | "version": "1.0.17", 4 | "devDependencies": { 5 | "nodeunit": "*", 6 | "bower": "*", 7 | "grunt": "*", 8 | "grunt-cli": "*", 9 | "grunt-contrib-concat": "*", 10 | "grunt-contrib-copy": "*" 11 | }, 12 | "dependencies": { 13 | "dreamer": "https://github.com/dchester/dreamer/archive/v1.1.1.tar.gz", 14 | "async": "0.2.9", 15 | "express": "3.4.7", 16 | "swig": "= v0.14.0", 17 | "consolidate": "0.10.0", 18 | "connect-flash": "0.1.1", 19 | "validator": "2.0.0", 20 | "pagination": "https://github.com/vanng822/pagination/archive/v0.3.3.tar.gz", 21 | "path": "0.4.9", 22 | "sha1": "1.1.0", 23 | "mv": "1.0.0", 24 | "mkdirp": "0.3.5", 25 | "armrest": "2.1.2", 26 | "mysql": "2.0.0-rc2", 27 | "connect-domain": "0.5.0", 28 | "moment": "2.5.0", 29 | "gx": "https://github.com/dchester/gx/archive/v0.1.0.tar.gz", 30 | "config": "~0.4.35", 31 | "passport": "^0.2.0", 32 | "passport-local": "^1.0.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /plugins/image/config.js: -------------------------------------------------------------------------------- 1 | var File = require('../../lib/file'); 2 | 3 | module.exports = { 4 | name: 'image', 5 | template: 'image', 6 | validator: null, 7 | placeholder: '', 8 | preview: function(value) { 9 | 10 | if (typeof value !== 'object') { 11 | try { JSON.parse(value) } 12 | catch(e) { return '' } 13 | } else if (value === null) { 14 | return ''; 15 | } 16 | return '

'; 17 | }, 18 | deflate: function(field, item, models, callback) { 19 | if (!item.data[field.name]) return callback(''); 20 | var json_string; 21 | if (typeof item.data[field.name] === 'object') { 22 | json_string = JSON.stringify(item.data[field.name]); 23 | } 24 | callback(json_string || item.data[field.name]); 25 | }, 26 | inflate: function(field, item, models, callback) { 27 | 28 | if (!item.data[field.name]) return callback(null); 29 | 30 | try { var parsed_data = JSON.parse(item.data[field.name]); } 31 | catch (e) { 32 | console.log("couldn't parse image list data"); 33 | return callback([]); 34 | } 35 | 36 | if (!parsed_data) return callback(null); 37 | var file_id = parsed_data.file_id; 38 | 39 | if (!file_id || file_id == 'undefined'){ 40 | console.log("found undefined file_id in field: " + field.name); 41 | return callback(null); 42 | } 43 | 44 | File.load({ id: file_id }, function(err, file) { 45 | 46 | var inflated_file = { 47 | url: file.url, 48 | size: file.size, 49 | original_filename: file.original_filename, 50 | content_type: file.content_type, 51 | file_id: file.id 52 | } 53 | 54 | callback(inflated_file); 55 | }); 56 | } 57 | }; 58 | 59 | -------------------------------------------------------------------------------- /plugins/image/index.js: -------------------------------------------------------------------------------- 1 | var showcase = require("../../index"); 2 | module.exports = showcase.plugins.slurp(__dirname); 3 | -------------------------------------------------------------------------------- /plugins/image/script.js: -------------------------------------------------------------------------------- 1 | Showcase.namespace("Showcase.Plugins.Image"); 2 | 3 | Showcase.Plugins.Image = Showcase.Class.create({ 4 | 5 | initialize: function(args) { 6 | 7 | this.container = args.container; 8 | this.input = args.input; 9 | this.file = args.file; 10 | 11 | var dropzone = new Dropzone(this.container, { 12 | url: "/files", 13 | clickable: true, 14 | addRemoveLinks: true, 15 | maxFiles: 1 16 | }); 17 | 18 | this.dropzone = dropzone; 19 | 20 | this.populate(); 21 | 22 | dropzone.on("complete", function(file) { 23 | console.log('added file!'); 24 | console.log(file); 25 | var response = JSON.parse(file.xhr.responseText); 26 | file.previewElement.setAttribute("data-file-id", response[0].id); 27 | this.sync(); 28 | 29 | }.bind(this)); 30 | 31 | dropzone.on("removedfile", function() { 32 | console.log('removed file!'); 33 | this.sync(); 34 | 35 | }.bind(this)); 36 | }, 37 | 38 | sync: function() { 39 | 40 | var preview = this.container.querySelector(".dz-preview"); 41 | 42 | if (preview) { 43 | var file_id = preview.getAttribute("data-file-id"); 44 | this.input.value = JSON.stringify({ file_id: file_id }); 45 | this.container.classList.add("populated"); 46 | } else { 47 | this.input.value = ""; 48 | this.container.classList.remove("populated"); 49 | } 50 | }, 51 | 52 | populate: function() { 53 | 54 | var file = this.file; 55 | 56 | if (!file) return; 57 | 58 | var mockFile = { 59 | name: file.original_filename, 60 | id: file.file_id, 61 | url: file.url, 62 | size: file.size 63 | }; 64 | 65 | this.dropzone.emit("addedfile", mockFile); 66 | this.dropzone.emit("thumbnail", mockFile, mockFile.url); 67 | 68 | var preview = this.container.querySelector('.dz-preview'); 69 | 70 | var file_id = this.file.file_id; 71 | preview.setAttribute("data-file-id", file_id); 72 | 73 | this.sync(); 74 | } 75 | }); 76 | 77 | -------------------------------------------------------------------------------- /plugins/image/style.css: -------------------------------------------------------------------------------- 1 | .sc-image .dz { 2 | width: 90%; 3 | padding-bottom: 58px; 4 | position: relative; 5 | margin-bottom: 10px; 6 | } 7 | .sc-image .dz.populated { 8 | padding-bottom: 0; 9 | } 10 | .sc-image .dz.dz-drag-hover .target { 11 | background: #ffc; 12 | } 13 | .sc-image .dz .dz-preview.ui-state-highlight { 14 | background: #ffc; 15 | } 16 | .sc-image .dz .ui-sortable-helper { 17 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.4); 18 | } 19 | .sc-image .dz .dz-preview { 20 | height: 50px; 21 | overflow: hidden; 22 | margin: 3px; 23 | border: 1px solid #e6ecee; 24 | background: #f3f9fb; 25 | } 26 | .sc-image .dz-preview { 27 | position: relative; 28 | } 29 | .sc-image .dz-details { 30 | position: relative; 31 | padding-left: 15%; 32 | } 33 | .sc-image .dz-details > * { 34 | vertical-align: top; 35 | line-height: 3.5; 36 | } 37 | .sc-image .dz-details img { 38 | width: 10%; 39 | position: absolute; 40 | left: 2%; 41 | top: 5px; 42 | } 43 | .sc-image .dz-details .dz-filename { 44 | padding-right: 3px; 45 | display: inline-block; 46 | text-overflow: ellipsis; 47 | overflow: hidden; 48 | white-space: nowrap; 49 | } 50 | .sc-image .dz-details .dz-size:before { 51 | content: " ("; 52 | } 53 | .sc-image .dz-details .dz-size:after { 54 | content: ")"; 55 | } 56 | .sc-image .dz-details .dz-size { 57 | opacity: 0.4; 58 | display: inline-block; 59 | text-align: right; 60 | } 61 | .sc-image .dz-details .dz-size * { 62 | font-weight: normal; 63 | } 64 | .sc-image .dz-error-mark { 65 | display: none; 66 | } 67 | .sc-image .dz-success-mark { 68 | display: none; 69 | } 70 | .sc-image .dz-error-message { 71 | position: absolute; 72 | background: #f8784c; 73 | top: 0; 74 | color: white; 75 | } 76 | .sc-image .dz-remove { 77 | position: absolute; 78 | top: 14px; 79 | right: 4%; 80 | width: 10px; 81 | font-size: 0; 82 | } 83 | .sc-image .dz-remove:after { 84 | position: absolute; 85 | font-size: 14px; 86 | right: 0; 87 | content: "\f00d"; 88 | font-family: FontAwesome; 89 | color: steelblue; 90 | color: rgba(0, 0, 0, 0.2); 91 | } 92 | .sc-image .dz-progress { 93 | opacity: 0.3; 94 | position: relative; 95 | top: -30px; 96 | height: 13px; 97 | background: steelblue; 98 | left: 68%; 99 | -webkit-transform: scaleX(0.2); 100 | -webkit-transform-origin-x: 0; 101 | -moz-transform: scaleX(0.2); 102 | -moz-transform-origin-x: 0; 103 | transform: scaleX(0.2); 104 | transform-origin-x: 0; 105 | } 106 | .sc-image .dz-preview[data-file-id] .dz-filename { 107 | font-weight: bold; 108 | } 109 | .sc-image .dz-preview[data-file-id] .dz-progress { 110 | display: none; 111 | } 112 | .sc-image .dz.populated .target { 113 | display: none; 114 | } 115 | .sc-image .dz .target { 116 | border-radius: 4px; 117 | border: 3px dashed rgba(0, 0, 0, 0.1); 118 | position: absolute; 119 | bottom: 0; 120 | width: 100%; 121 | text-align: center; 122 | font-size: 20px; 123 | padding: 10px; 124 | color: rgba(0,0,0,0.4); 125 | pointer-events: none; 126 | } 127 | -------------------------------------------------------------------------------- /plugins/image/template.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
Drag file here or click to upload...
6 |
7 |
8 | 9 | 29 | -------------------------------------------------------------------------------- /plugins/image_list/config.js: -------------------------------------------------------------------------------- 1 | var File = require('../../lib/file'); 2 | 3 | module.exports = { 4 | name: 'image_list', 5 | template: 'image_list', 6 | validator: null, 7 | placeholder: '', 8 | preview: function(value) { 9 | 10 | try { var parsed_data = JSON.parse(value) } 11 | catch (e) { return "" } 12 | 13 | return '' + parsed_data.file_ids.length + ''; 14 | 15 | }, 16 | inflate: function(field, item, models, callback) { 17 | 18 | if (!item.data[field.name]) return callback(null); 19 | 20 | try { var parsed_data = JSON.parse(item.data[field.name]); } 21 | catch (e) { 22 | console.log("couldn't parse image list data"); 23 | return callback([]); 24 | } 25 | 26 | var file_ids = parsed_data.file_ids; 27 | var inflated_files = []; 28 | var tasks = []; 29 | File.all({ id: file_ids }, function(err, files) { 30 | var inflated_files = []; 31 | var sorted_files = []; 32 | files.forEach( function(file) { 33 | var inflated_file = { 34 | url: file.url, 35 | size: file.size, 36 | original_filename: file.original_filename, 37 | content_type: file.content_type, 38 | file_id: file.id 39 | } 40 | 41 | inflated_files.push(inflated_file); 42 | }); 43 | file_ids.forEach(function(file_id) { 44 | var file = inflated_files.filter(function(f) { return f.file_id == file_id })[0]; 45 | sorted_files.push(file); 46 | }); 47 | return callback(sorted_files); 48 | }); 49 | } 50 | }; 51 | 52 | -------------------------------------------------------------------------------- /plugins/image_list/index.js: -------------------------------------------------------------------------------- 1 | var showcase = require("../../index"); 2 | module.exports = showcase.plugins.slurp(__dirname); 3 | -------------------------------------------------------------------------------- /plugins/image_list/script.js: -------------------------------------------------------------------------------- 1 | Showcase.namespace("Showcase.Plugins.ImageList"); 2 | 3 | Showcase.Plugins.ImageList = Showcase.Class.create({ 4 | 5 | initialize: function(args) { 6 | 7 | this.container = args.container; 8 | this.input = args.input; 9 | this.files = args.files || []; 10 | 11 | var dropzone = new Dropzone(this.container, { 12 | url: "/files", 13 | clickable: true, 14 | addRemoveLinks: true 15 | }); 16 | 17 | this.dropzone = dropzone; 18 | 19 | this.populate(); 20 | 21 | dropzone.on("complete", function(file) { 22 | var response = JSON.parse(file.xhr.responseText); 23 | file.previewElement.setAttribute("data-file-id", response[0].id); 24 | this.sync(); 25 | 26 | }.bind(this)); 27 | 28 | dropzone.on("removedfile", function() { 29 | this.sync(); 30 | 31 | }.bind(this)); 32 | 33 | $(this.container).sortable({ 34 | placeholder: "dz-preview ui-state-highlight", 35 | items: ".dz-preview", 36 | update: function() { 37 | this.sync(); 38 | }.bind(this) 39 | }); 40 | 41 | $(this.container).disableSelection(); 42 | }, 43 | 44 | sync: function() { 45 | 46 | var file_ids = []; 47 | var previews = this.container.querySelectorAll(".dz-preview"); 48 | [].forEach.call(previews, function(el) { 49 | var id = el.getAttribute("data-file-id"); 50 | if (!id) return; 51 | file_ids.push(id); 52 | }); 53 | 54 | this.input.value = JSON.stringify({ file_ids: file_ids }); 55 | }, 56 | 57 | populate: function() { 58 | 59 | var mockFiles = []; 60 | 61 | this.files.forEach(function(file) { 62 | mockFiles.push({ 63 | name: file.original_filename, 64 | id: file.file_id, 65 | url: file.url, 66 | size: file.size 67 | }); 68 | }); 69 | 70 | mockFiles.forEach(function(file) { 71 | this.dropzone.emit("addedfile", file); 72 | this.dropzone.emit("thumbnail", file, file.url); 73 | 74 | }.bind(this)); 75 | 76 | var previews = this.container.querySelectorAll('.dz-preview'); 77 | 78 | [].forEach.call(previews, function(el, index) { 79 | var file_id = this.files[index].file_id; 80 | el.setAttribute("data-file-id", file_id); 81 | }.bind(this)); 82 | 83 | this.sync(); 84 | } 85 | }); 86 | 87 | -------------------------------------------------------------------------------- /plugins/image_list/style.css: -------------------------------------------------------------------------------- 1 | .sc-image-list .dz { 2 | width: 90%; 3 | padding-bottom: 58px; 4 | position: relative; 5 | margin-bottom: 10px; 6 | } 7 | .sc-image-list .dz.populated { 8 | padding-bottom: 0; 9 | } 10 | .sc-image-list .dz.dz-drag-hover .target { 11 | background: #ffc; 12 | } 13 | .sc-image-list .dz .dz-preview.ui-state-highlight { 14 | background: #ffc; 15 | } 16 | .sc-image-list .dz .ui-sortable-helper { 17 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.4); 18 | } 19 | .sc-image-list .dz .dz-preview { 20 | height: 50px; 21 | overflow: hidden; 22 | margin: 3px; 23 | border: 1px solid #e6ecee; 24 | background: #f3f9fb; 25 | cursor: -moz-grab; 26 | cursor: -webkit-grab; 27 | cursor: grab; 28 | } 29 | .sc-image-list .dz-preview { 30 | position: relative; 31 | } 32 | .sc-image-list .dz-details { 33 | position: relative; 34 | padding-left: 15%; 35 | } 36 | .sc-image-list .dz-details > * { 37 | vertical-align: top; 38 | line-height: 3.5; 39 | } 40 | .sc-image-list .dz-details img { 41 | width: 10%; 42 | position: absolute; 43 | left: 2%; 44 | top: 5px; 45 | } 46 | .sc-image-list .dz-details .dz-filename { 47 | padding-right: 3px; 48 | display: inline-block; 49 | text-overflow: ellipsis; 50 | overflow: hidden; 51 | white-space: nowrap; 52 | } 53 | .sc-image-list .dz-details .dz-size:before { 54 | content: " ("; 55 | } 56 | .sc-image-list .dz-details .dz-size:after { 57 | content: ")"; 58 | } 59 | .sc-image-list .dz-details .dz-size { 60 | opacity: 0.4; 61 | display: inline-block; 62 | text-align: right; 63 | } 64 | .sc-image-list .dz-details .dz-size * { 65 | font-weight: normal; 66 | } 67 | .sc-image-list .dz-error-mark { 68 | display: none; 69 | } 70 | .sc-image-list .dz-success-mark { 71 | display: none; 72 | } 73 | .sc-image-list .dz-error-message { 74 | position: absolute; 75 | background: #f8784c; 76 | top: 0; 77 | color: white; 78 | } 79 | .sc-image-list .dz-remove { 80 | position: absolute; 81 | top: 14px; 82 | right: 4%; 83 | width: 10px; 84 | font-size: 0; 85 | } 86 | .sc-image-list .dz-remove:after { 87 | position: absolute; 88 | font-size: 14px; 89 | right: 0; 90 | content: "\f00d"; 91 | font-family: FontAwesome; 92 | color: steelblue; 93 | color: rgba(0, 0, 0, 0.2); 94 | } 95 | .sc-image-list .dz-progress { 96 | opacity: 0.3; 97 | position: relative; 98 | top: -30px; 99 | height: 13px; 100 | background: steelblue; 101 | left: 68%; 102 | -webkit-transform: scaleX(0.2); 103 | -webkit-transform-origin-x: 0; 104 | -moz-transform: scaleX(0.2); 105 | -moz-transform-origin-x: 0; 106 | transform: scaleX(0.2); 107 | transform-origin-x: 0; 108 | } 109 | .sc-image-list .dz-preview[data-file-id] .dz-filename { 110 | font-weight: bold; 111 | } 112 | .sc-image-list .dz-preview[data-file-id] .dz-progress { 113 | display: none; 114 | } 115 | .sc-image-list .dz.populated .target { 116 | display: none; 117 | } 118 | .sc-image-list .dz .target { 119 | border-radius: 4px; 120 | border: 3px dashed rgba(0, 0, 0, 0.1); 121 | position: absolute; 122 | bottom: 0; 123 | width: 100%; 124 | text-align: center; 125 | font-size: 20px; 126 | padding: 10px; 127 | color: rgba(0,0,0,0.4); 128 | pointer-events: none; 129 | } 130 | -------------------------------------------------------------------------------- /plugins/image_list/template.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
5 |
Drag files here or click to upload...
6 |
7 |
8 | 9 | 28 | -------------------------------------------------------------------------------- /public/css/api.css: -------------------------------------------------------------------------------- 1 | h2:first-of-type { 2 | margin-top: 0.3em; 3 | 4 | } 5 | h3 { 6 | font-size: 20px; 7 | } 8 | h5 { 9 | color: #666; 10 | } 11 | .resource-description { 12 | margin: 12px 0 24px; 13 | display: block; 14 | } 15 | .parameters { 16 | width: auto; 17 | min-width: auto; 18 | border: 1px solid #ddd; 19 | } 20 | .parameters td, 21 | .parameters th { 22 | border-right: 1px solid #ddd; 23 | } 24 | .row { 25 | margin-left: 0px; 26 | } 27 | .nav.nav-tabs.nav-stacked { 28 | margin-top: 0.6em; 29 | } 30 | .resources-listing { 31 | position: relative; 32 | } 33 | .resources-listing-nav { 34 | margin-right: 30px; 35 | } 36 | .resources-listing-nav.affix { 37 | top: 32px; 38 | width: 270px; 39 | } 40 | .sidebar { 41 | margin: 0; 42 | } 43 | .table { 44 | min-width: inherit; 45 | } 46 | .preview-link { 47 | position: absolute; 48 | right: 14px; 49 | margin-top: -41px; 50 | color: #888; 51 | background: rgba(0,0,0,0.05); 52 | padding: 1px 8px; 53 | font-size: 12px; 54 | border-radius: 2px; 55 | } 56 | -------------------------------------------------------------------------------- /public/css/collection.css: -------------------------------------------------------------------------------- 1 | #entity_form { 2 | width: 990px; 3 | } 4 | #new_field { 5 | margin-top: 0.6em; 6 | margin-bottom: 1.2em; 7 | } 8 | .handle i { 9 | opacity: 0.7; 10 | cursor: move; 11 | } 12 | #fields { 13 | margin: 20px 0 0 0; 14 | width: 90%; 15 | } 16 | #fields li { 17 | list-style: none; 18 | } 19 | .sortable-placeholder { 20 | bordder 1px dotted rgba(#000, 0.5); 21 | } 22 | #fields .row { 23 | margin: 0; 24 | position: relative; 25 | border-bottom: 1px solid rgba(0, 0, 0, 0.06); 26 | padding-bottom: 10px; 27 | margin-bottom: 10px; 28 | } 29 | #fields input { 30 | width: 90%; 31 | } 32 | .labels > div { 33 | display: inline-block; 34 | width: 15%; 35 | font-size: 14px; 36 | color: rgba(0, 0, 0, 0.4); 37 | } 38 | .labels > div.title { 39 | width: 50%; 40 | font-size: 16px; 41 | padding-left: 0.0em; 42 | color: rgba(0, 0, 0, 0.7); 43 | cursor: pointer; 44 | } 45 | .labels > div.tools { 46 | display: inline-block; 47 | width: auto; 48 | } 49 | .labels .tools > div { 50 | display: inline-block; 51 | padding: 5px; 52 | opacity: 0.4; 53 | } 54 | .labels .tools > div:hover { 55 | opacity: 0.9; 56 | cursor: pointer; 57 | } 58 | .inverse { 59 | background: #555; 60 | color: white; 61 | } 62 | .inverse h2 { 63 | color: white; 64 | } 65 | .overlay { 66 | display: none; 67 | position: fixed; 68 | height: 100%; 69 | width: 100%; 70 | background: rgba(0, 0, 0, 0.5); 71 | } 72 | .inputs { 73 | -webkit-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); 74 | -moz-box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); 75 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.3); 76 | padding: 20px; 77 | background: white; 78 | margin: auto; 79 | border-radius: 4px; 80 | max-height: 90%; 81 | overflow-y: auto; 82 | width: 750px; 83 | } 84 | .inputs h2 { 85 | margin: 0 0 10px; 86 | font-weight: normal; 87 | color: #333; 88 | font-size: 24px; 89 | } 90 | .overlay.visible { 91 | display: -webkit-box; 92 | display: -moz-box; 93 | display: box; 94 | z-index: 10; 95 | } 96 | .overlay.visible inputs { 97 | max-height: 90%; 98 | overflow: auto; 99 | } 100 | .no-fields { 101 | display: none; 102 | } 103 | .error-no-fields .no-fields { 104 | display: block; 105 | } 106 | .data_type_meta { 107 | display: none; 108 | } 109 | .data_type_meta.active { 110 | display: block; 111 | } 112 | -------------------------------------------------------------------------------- /public/css/editor/editor.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #555; 3 | font-size: 15px; 4 | background: white; 5 | font-family: Arial, Helvetica, sans-serif; 6 | line-height: 1.35em; 7 | padding: 10px; 8 | margin: 0; 9 | } 10 | -------------------------------------------------------------------------------- /public/css/editor/preview.css: -------------------------------------------------------------------------------- 1 | html { padding:0 10px; } 2 | 3 | body { 4 | margin:0; 5 | padding:0; 6 | background:#fff; 7 | } 8 | 9 | #epiceditor-wrapper{ 10 | background:white; 11 | } 12 | 13 | #epiceditor-preview{ 14 | padding-top:10px; 15 | padding-bottom:10px; 16 | font-family: Helvetica,arial,freesans,clean,sans-serif; 17 | font-size:15px; 18 | line-height:1.35; 19 | color: #333; 20 | text-rendering: optimizespeed; 21 | } 22 | 23 | #epiceditor-preview>*:first-child{ 24 | margin-top:0!important; 25 | } 26 | 27 | #epiceditor-preview>*:last-child{ 28 | margin-bottom:0!important; 29 | } 30 | 31 | #epiceditor-preview a{ 32 | color:#4183C4; 33 | text-decoration:none; 34 | } 35 | 36 | #epiceditor-preview a:hover{ 37 | text-decoration:underline; 38 | } 39 | 40 | #epiceditor-preview h1, 41 | #epiceditor-preview h2, 42 | #epiceditor-preview h3, 43 | #epiceditor-preview h4, 44 | #epiceditor-preview h5, 45 | #epiceditor-preview h6{ 46 | margin:20px 0 10px; 47 | padding:0; 48 | font-weight:bold; 49 | -webkit-font-smoothing:antialiased; 50 | } 51 | 52 | #epiceditor-preview h1 tt, 53 | #epiceditor-preview h1 code, 54 | #epiceditor-preview h2 tt, 55 | #epiceditor-preview h2 code, 56 | #epiceditor-preview h3 tt, 57 | #epiceditor-preview h3 code, 58 | #epiceditor-preview h4 tt, 59 | #epiceditor-preview h4 code, 60 | #epiceditor-preview h5 tt, 61 | #epiceditor-preview h5 code, 62 | #epiceditor-preview h6 tt, 63 | #epiceditor-preview h6 code{ 64 | font-size:inherit; 65 | } 66 | 67 | #epiceditor-preview h1{ 68 | font-size:28px; 69 | color:#000; 70 | } 71 | 72 | #epiceditor-preview h2{ 73 | font-size:24px; 74 | border-bottom:1px solid #ccc; 75 | color:#000; 76 | } 77 | 78 | #epiceditor-preview h3{ 79 | font-size:18px; 80 | } 81 | 82 | #epiceditor-preview h4{ 83 | font-size:16px; 84 | } 85 | 86 | #epiceditor-preview h5{ 87 | font-size:14px; 88 | } 89 | 90 | #epiceditor-preview h6{ 91 | color:#777; 92 | font-size:14px; 93 | } 94 | 95 | #epiceditor-preview p, 96 | #epiceditor-preview blockquote, 97 | #epiceditor-preview ul, 98 | #epiceditor-preview ol, 99 | #epiceditor-preview dl, 100 | #epiceditor-preview li, 101 | #epiceditor-preview table, 102 | #epiceditor-preview pre{ 103 | margin:15px 0; 104 | } 105 | 106 | #epiceditor-preview hr{ 107 | background:transparent url('../../images/modules/pulls/dirty-shade.png') repeat-x 0 0; 108 | border:0 none; 109 | color:#ccc; 110 | height:4px; 111 | padding:0; 112 | } 113 | 114 | #epiceditor-preview>h2:first-child, 115 | #epiceditor-preview>h1:first-child, 116 | #epiceditor-preview>h1:first-child+h2, 117 | #epiceditor-preview>h3:first-child, 118 | #epiceditor-preview>h4:first-child, 119 | #epiceditor-preview>h5:first-child, 120 | #epiceditor-preview>h6:first-child{ 121 | margin-top:0; 122 | padding-top:0; 123 | } 124 | 125 | #epiceditor-preview h1+p, 126 | #epiceditor-preview h2+p, 127 | #epiceditor-preview h3+p, 128 | #epiceditor-preview h4+p, 129 | #epiceditor-preview h5+p, 130 | #epiceditor-preview h6+p{ 131 | margin-top:0; 132 | } 133 | 134 | #epiceditor-preview li p.first{ 135 | display:inline-block; 136 | } 137 | 138 | #epiceditor-preview ul, 139 | #epiceditor-preview ol{ 140 | padding-left:30px; 141 | } 142 | 143 | #epiceditor-preview ul li>:first-child, 144 | #epiceditor-preview ol li>:first-child{ 145 | margin-top:0; 146 | } 147 | 148 | #epiceditor-preview ul li>:last-child, 149 | #epiceditor-preview ol li>:last-child{ 150 | margin-bottom:0; 151 | } 152 | 153 | #epiceditor-preview dl{ 154 | padding:0; 155 | } 156 | 157 | #epiceditor-preview dl dt{ 158 | font-size:14px; 159 | font-weight:bold; 160 | font-style:italic; 161 | padding:0; 162 | margin:15px 0 5px; 163 | } 164 | 165 | #epiceditor-preview dl dt:first-child{ 166 | padding:0; 167 | } 168 | 169 | #epiceditor-preview dl dt>:first-child{ 170 | margin-top:0; 171 | } 172 | 173 | #epiceditor-preview dl dt>:last-child{ 174 | margin-bottom:0; 175 | } 176 | 177 | #epiceditor-preview dl dd{ 178 | margin:0 0 15px; 179 | padding:0 15px; 180 | } 181 | 182 | #epiceditor-preview dl dd>:first-child{ 183 | margin-top:0; 184 | } 185 | 186 | #epiceditor-preview dl dd>:last-child{ 187 | margin-bottom:0; 188 | } 189 | 190 | #epiceditor-preview blockquote{ 191 | border-left:4px solid #DDD; 192 | padding:0 15px; 193 | color:#777; 194 | } 195 | 196 | #epiceditor-preview blockquote>:first-child{ 197 | margin-top:0; 198 | } 199 | 200 | #epiceditor-preview blockquote>:last-child{ 201 | margin-bottom:0; 202 | } 203 | 204 | #epiceditor-preview table{ 205 | padding:0; 206 | border-collapse: collapse; 207 | border-spacing: 0; 208 | font-size: 100%; 209 | font: inherit; 210 | } 211 | 212 | #epiceditor-preview table tr{ 213 | border-top:1px solid #ccc; 214 | background-color:#fff; 215 | margin:0; 216 | padding:0; 217 | } 218 | 219 | #epiceditor-preview table tr:nth-child(2n){ 220 | background-color:#f8f8f8; 221 | } 222 | 223 | #epiceditor-preview table tr th{ 224 | font-weight:bold; 225 | } 226 | 227 | #epiceditor-preview table tr th, 228 | #epiceditor-preview table tr td{ 229 | border:1px solid #ccc; 230 | text-align:left; 231 | margin:0; 232 | padding:6px 13px; 233 | } 234 | 235 | #epiceditor-preview table tr th>:first-child, 236 | #epiceditor-preview table tr td>:first-child{ 237 | margin-top:0; 238 | } 239 | 240 | #epiceditor-preview table tr th>:last-child, 241 | #epiceditor-preview table tr td>:last-child{ 242 | margin-bottom:0; 243 | } 244 | 245 | #epiceditor-preview img{ 246 | max-width:100%; 247 | } 248 | 249 | #epiceditor-preview span.frame{ 250 | display:block; 251 | overflow:hidden; 252 | } 253 | 254 | #epiceditor-preview span.frame>span{ 255 | border:1px solid #ddd; 256 | display:block; 257 | float:left; 258 | overflow:hidden; 259 | margin:13px 0 0; 260 | padding:7px; 261 | width:auto; 262 | } 263 | 264 | #epiceditor-preview span.frame span img{ 265 | display:block; 266 | float:left; 267 | } 268 | 269 | #epiceditor-preview span.frame span span{ 270 | clear:both; 271 | color:#333; 272 | display:block; 273 | padding:5px 0 0; 274 | } 275 | 276 | #epiceditor-preview span.align-center{ 277 | display:block; 278 | overflow:hidden; 279 | clear:both; 280 | } 281 | 282 | #epiceditor-preview span.align-center>span{ 283 | display:block; 284 | overflow:hidden; 285 | margin:13px auto 0; 286 | text-align:center; 287 | } 288 | 289 | #epiceditor-preview span.align-center span img{ 290 | margin:0 auto; 291 | text-align:center; 292 | } 293 | 294 | #epiceditor-preview span.align-right{ 295 | display:block; 296 | overflow:hidden; 297 | clear:both; 298 | } 299 | 300 | #epiceditor-preview span.align-right>span{ 301 | display:block; 302 | overflow:hidden; 303 | margin:13px 0 0; 304 | text-align:right; 305 | } 306 | 307 | #epiceditor-preview span.align-right span img{ 308 | margin:0; 309 | text-align:right; 310 | } 311 | 312 | #epiceditor-preview span.float-left{ 313 | display:block; 314 | margin-right:13px; 315 | overflow:hidden; 316 | float:left; 317 | } 318 | 319 | #epiceditor-preview span.float-left span{ 320 | margin:13px 0 0; 321 | } 322 | 323 | #epiceditor-preview span.float-right{ 324 | display:block; 325 | margin-left:13px; 326 | overflow:hidden; 327 | float:right; 328 | } 329 | 330 | #epiceditor-preview span.float-right>span{ 331 | display:block; 332 | overflow:hidden; 333 | margin:13px auto 0; 334 | text-align:right; 335 | } 336 | 337 | #epiceditor-preview code, 338 | #epiceditor-preview tt{ 339 | margin:0 2px; 340 | padding:0 5px; 341 | white-space:nowrap; 342 | border:1px solid #eaeaea; 343 | background-color:#f8f8f8; 344 | border-radius:3px; 345 | } 346 | 347 | #epiceditor-preview pre>code{ 348 | margin:0; 349 | padding:0; 350 | white-space:pre; 351 | border:none; 352 | background:transparent; 353 | } 354 | 355 | #epiceditor-preview .highlight pre, 356 | #epiceditor-preview pre{ 357 | background-color:#f8f8f8; 358 | border:1px solid #ccc; 359 | font-size:13px; 360 | line-height:19px; 361 | overflow:auto; 362 | padding:6px 10px; 363 | border-radius:3px; 364 | } 365 | 366 | #epiceditor-preview pre code, 367 | #epiceditor-preview pre tt{ 368 | background-color:transparent; 369 | border:none; 370 | } 371 | -------------------------------------------------------------------------------- /public/css/item.css: -------------------------------------------------------------------------------- 1 | form.delete { 2 | display: inline; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /public/css/login.css: -------------------------------------------------------------------------------- 1 | .login-form input[type=text] { 2 | width: 400px; 3 | } 4 | -------------------------------------------------------------------------------- /public/css/setup.css: -------------------------------------------------------------------------------- 1 | .setup-form input[type=text] { 2 | width: 400px; 3 | } 4 | 5 | -------------------------------------------------------------------------------- /public/css/user.css: -------------------------------------------------------------------------------- 1 | .table-secondary { 2 | width: 100%; 3 | margin-bottom: 20px; 4 | } 5 | .table-secondary tr td { 6 | border-bottom: 1px solid #dde7ec; 7 | } 8 | .table-secondary tr td { 9 | padding-left: 0; 10 | } 11 | .table-secondary tr td select { 12 | font-size: 14px; 13 | color: rgba(0, 0, 0, 0.4); 14 | margin: 5px 0; 15 | } 16 | select option { 17 | display: block; 18 | padding: 20px; 19 | } 20 | .workspaces-row.superuser { 21 | display: none; 22 | } 23 | -------------------------------------------------------------------------------- /public/dist/epic/editor/epic-dark.css: -------------------------------------------------------------------------------- 1 | html { padding:10px; } 2 | 3 | body { 4 | border:0; 5 | background:rgb(41,41,41); 6 | font-family:monospace; 7 | font-size:14px; 8 | padding:10px; 9 | color:#ddd; 10 | line-height:1.35em; 11 | margin:0; 12 | padding:0; 13 | } 14 | -------------------------------------------------------------------------------- /public/dist/epic/editor/epic-light.css: -------------------------------------------------------------------------------- 1 | html { padding:10px; } 2 | 3 | body { 4 | border:0; 5 | background:#fcfcfc; 6 | font-family:monospace; 7 | font-size:14px; 8 | padding:10px; 9 | line-height:1.35em; 10 | margin:0; 11 | padding:0; 12 | } 13 | -------------------------------------------------------------------------------- /public/dist/epic/preview/bartik.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Georgia, "Times New Roman", Times, serif; 3 | line-height: 1.5; 4 | font-size: 87.5%; 5 | word-wrap: break-word; 6 | margin: 2em; 7 | padding: 0; 8 | border: 0; 9 | outline: 0; 10 | background: #fff; 11 | } 12 | 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6 { 19 | margin: 1.0em 0 0.5em; 20 | font-weight: inherit; 21 | } 22 | 23 | h1 { 24 | font-size: 1.357em; 25 | color: #000; 26 | } 27 | 28 | h2 { 29 | font-size: 1.143em; 30 | } 31 | 32 | p { 33 | margin: 0 0 1.2em; 34 | } 35 | 36 | del { 37 | text-decoration: line-through; 38 | } 39 | 40 | tr:nth-child(odd) { 41 | background-color: #dddddd; 42 | } 43 | 44 | img { 45 | outline: 0; 46 | } 47 | 48 | code { 49 | background-color: #f2f2f2; 50 | background-color: rgba(40, 40, 0, 0.06); 51 | } 52 | 53 | pre { 54 | background-color: #f2f2f2; 55 | background-color: rgba(40, 40, 0, 0.06); 56 | margin: 10px 0; 57 | overflow: hidden; 58 | padding: 15px; 59 | white-space: pre-wrap; 60 | } 61 | 62 | pre code { 63 | font-size: 100%; 64 | background-color: transparent; 65 | } 66 | 67 | blockquote { 68 | background: #f7f7f7; 69 | border-left: 1px solid #bbb; 70 | font-style: italic; 71 | margin: 1.5em 10px; 72 | padding: 0.5em 10px; 73 | } 74 | 75 | blockquote:before { 76 | color: #bbb; 77 | content: "\201C"; 78 | font-size: 3em; 79 | line-height: 0.1em; 80 | margin-right: 0.2em; 81 | vertical-align: -.4em; 82 | } 83 | 84 | blockquote:after { 85 | color: #bbb; 86 | content: "\201D"; 87 | font-size: 3em; 88 | line-height: 0.1em; 89 | vertical-align: -.45em; 90 | } 91 | 92 | blockquote > p:first-child { 93 | display: inline; 94 | } 95 | 96 | table { 97 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 98 | border: 0; 99 | border-spacing: 0; 100 | font-size: 0.857em; 101 | margin: 10px 0; 102 | width: 100%; 103 | } 104 | 105 | table table { 106 | font-size: 1em; 107 | } 108 | 109 | table tr th { 110 | background: #757575; 111 | background: rgba(0, 0, 0, 0.51); 112 | border-bottom-style: none; 113 | } 114 | 115 | table tr th, 116 | table tr th a, 117 | table tr th a:hover { 118 | color: #FFF; 119 | font-weight: bold; 120 | } 121 | 122 | table tbody tr th { 123 | vertical-align: top; 124 | } 125 | 126 | tr td, 127 | tr th { 128 | padding: 4px 9px; 129 | border: 1px solid #fff; 130 | text-align: left; /* LTR */ 131 | } 132 | 133 | tr:nth-child(odd) { 134 | background: #e4e4e4; 135 | background: rgba(0, 0, 0, 0.105); 136 | } 137 | 138 | tr, 139 | tr:nth-child(even) { 140 | background: #efefef; 141 | background: rgba(0, 0, 0, 0.063); 142 | } 143 | 144 | a { 145 | color: #0071B3; 146 | } 147 | 148 | a:hover, 149 | a:focus { 150 | color: #018fe2; 151 | } 152 | 153 | a:active { 154 | color: #23aeff; 155 | } 156 | 157 | a:link, 158 | a:visited { 159 | text-decoration: none; 160 | } 161 | 162 | a:hover, 163 | a:active, 164 | a:focus { 165 | text-decoration: underline; 166 | } 167 | 168 | -------------------------------------------------------------------------------- /public/dist/epic/preview/github.css: -------------------------------------------------------------------------------- 1 | html { padding:0 10px; } 2 | 3 | body { 4 | margin:0; 5 | padding:0; 6 | background:#fff; 7 | } 8 | 9 | #epiceditor-wrapper{ 10 | background:white; 11 | } 12 | 13 | #epiceditor-preview{ 14 | padding-top:10px; 15 | padding-bottom:10px; 16 | font-family: Helvetica,arial,freesans,clean,sans-serif; 17 | font-size:13px; 18 | line-height:1.6; 19 | } 20 | 21 | #epiceditor-preview>*:first-child{ 22 | margin-top:0!important; 23 | } 24 | 25 | #epiceditor-preview>*:last-child{ 26 | margin-bottom:0!important; 27 | } 28 | 29 | #epiceditor-preview a{ 30 | color:#4183C4; 31 | text-decoration:none; 32 | } 33 | 34 | #epiceditor-preview a:hover{ 35 | text-decoration:underline; 36 | } 37 | 38 | #epiceditor-preview h1, 39 | #epiceditor-preview h2, 40 | #epiceditor-preview h3, 41 | #epiceditor-preview h4, 42 | #epiceditor-preview h5, 43 | #epiceditor-preview h6{ 44 | margin:20px 0 10px; 45 | padding:0; 46 | font-weight:bold; 47 | -webkit-font-smoothing:antialiased; 48 | } 49 | 50 | #epiceditor-preview h1 tt, 51 | #epiceditor-preview h1 code, 52 | #epiceditor-preview h2 tt, 53 | #epiceditor-preview h2 code, 54 | #epiceditor-preview h3 tt, 55 | #epiceditor-preview h3 code, 56 | #epiceditor-preview h4 tt, 57 | #epiceditor-preview h4 code, 58 | #epiceditor-preview h5 tt, 59 | #epiceditor-preview h5 code, 60 | #epiceditor-preview h6 tt, 61 | #epiceditor-preview h6 code{ 62 | font-size:inherit; 63 | } 64 | 65 | #epiceditor-preview h1{ 66 | font-size:28px; 67 | color:#000; 68 | } 69 | 70 | #epiceditor-preview h2{ 71 | font-size:24px; 72 | border-bottom:1px solid #ccc; 73 | color:#000; 74 | } 75 | 76 | #epiceditor-preview h3{ 77 | font-size:18px; 78 | } 79 | 80 | #epiceditor-preview h4{ 81 | font-size:16px; 82 | } 83 | 84 | #epiceditor-preview h5{ 85 | font-size:14px; 86 | } 87 | 88 | #epiceditor-preview h6{ 89 | color:#777; 90 | font-size:14px; 91 | } 92 | 93 | #epiceditor-preview p, 94 | #epiceditor-preview blockquote, 95 | #epiceditor-preview ul, 96 | #epiceditor-preview ol, 97 | #epiceditor-preview dl, 98 | #epiceditor-preview li, 99 | #epiceditor-preview table, 100 | #epiceditor-preview pre{ 101 | margin:15px 0; 102 | } 103 | 104 | #epiceditor-preview hr{ 105 | background:transparent url('../../images/modules/pulls/dirty-shade.png') repeat-x 0 0; 106 | border:0 none; 107 | color:#ccc; 108 | height:4px; 109 | padding:0; 110 | } 111 | 112 | #epiceditor-preview>h2:first-child, 113 | #epiceditor-preview>h1:first-child, 114 | #epiceditor-preview>h1:first-child+h2, 115 | #epiceditor-preview>h3:first-child, 116 | #epiceditor-preview>h4:first-child, 117 | #epiceditor-preview>h5:first-child, 118 | #epiceditor-preview>h6:first-child{ 119 | margin-top:0; 120 | padding-top:0; 121 | } 122 | 123 | #epiceditor-preview h1+p, 124 | #epiceditor-preview h2+p, 125 | #epiceditor-preview h3+p, 126 | #epiceditor-preview h4+p, 127 | #epiceditor-preview h5+p, 128 | #epiceditor-preview h6+p{ 129 | margin-top:0; 130 | } 131 | 132 | #epiceditor-preview li p.first{ 133 | display:inline-block; 134 | } 135 | 136 | #epiceditor-preview ul, 137 | #epiceditor-preview ol{ 138 | padding-left:30px; 139 | } 140 | 141 | #epiceditor-preview ul li>:first-child, 142 | #epiceditor-preview ol li>:first-child{ 143 | margin-top:0; 144 | } 145 | 146 | #epiceditor-preview ul li>:last-child, 147 | #epiceditor-preview ol li>:last-child{ 148 | margin-bottom:0; 149 | } 150 | 151 | #epiceditor-preview dl{ 152 | padding:0; 153 | } 154 | 155 | #epiceditor-preview dl dt{ 156 | font-size:14px; 157 | font-weight:bold; 158 | font-style:italic; 159 | padding:0; 160 | margin:15px 0 5px; 161 | } 162 | 163 | #epiceditor-preview dl dt:first-child{ 164 | padding:0; 165 | } 166 | 167 | #epiceditor-preview dl dt>:first-child{ 168 | margin-top:0; 169 | } 170 | 171 | #epiceditor-preview dl dt>:last-child{ 172 | margin-bottom:0; 173 | } 174 | 175 | #epiceditor-preview dl dd{ 176 | margin:0 0 15px; 177 | padding:0 15px; 178 | } 179 | 180 | #epiceditor-preview dl dd>:first-child{ 181 | margin-top:0; 182 | } 183 | 184 | #epiceditor-preview dl dd>:last-child{ 185 | margin-bottom:0; 186 | } 187 | 188 | #epiceditor-preview blockquote{ 189 | border-left:4px solid #DDD; 190 | padding:0 15px; 191 | color:#777; 192 | } 193 | 194 | #epiceditor-preview blockquote>:first-child{ 195 | margin-top:0; 196 | } 197 | 198 | #epiceditor-preview blockquote>:last-child{ 199 | margin-bottom:0; 200 | } 201 | 202 | #epiceditor-preview table{ 203 | padding:0; 204 | border-collapse: collapse; 205 | border-spacing: 0; 206 | font-size: 100%; 207 | font: inherit; 208 | } 209 | 210 | #epiceditor-preview table tr{ 211 | border-top:1px solid #ccc; 212 | background-color:#fff; 213 | margin:0; 214 | padding:0; 215 | } 216 | 217 | #epiceditor-preview table tr:nth-child(2n){ 218 | background-color:#f8f8f8; 219 | } 220 | 221 | #epiceditor-preview table tr th{ 222 | font-weight:bold; 223 | } 224 | 225 | #epiceditor-preview table tr th, 226 | #epiceditor-preview table tr td{ 227 | border:1px solid #ccc; 228 | text-align:left; 229 | margin:0; 230 | padding:6px 13px; 231 | } 232 | 233 | #epiceditor-preview table tr th>:first-child, 234 | #epiceditor-preview table tr td>:first-child{ 235 | margin-top:0; 236 | } 237 | 238 | #epiceditor-preview table tr th>:last-child, 239 | #epiceditor-preview table tr td>:last-child{ 240 | margin-bottom:0; 241 | } 242 | 243 | #epiceditor-preview img{ 244 | max-width:100%; 245 | } 246 | 247 | #epiceditor-preview span.frame{ 248 | display:block; 249 | overflow:hidden; 250 | } 251 | 252 | #epiceditor-preview span.frame>span{ 253 | border:1px solid #ddd; 254 | display:block; 255 | float:left; 256 | overflow:hidden; 257 | margin:13px 0 0; 258 | padding:7px; 259 | width:auto; 260 | } 261 | 262 | #epiceditor-preview span.frame span img{ 263 | display:block; 264 | float:left; 265 | } 266 | 267 | #epiceditor-preview span.frame span span{ 268 | clear:both; 269 | color:#333; 270 | display:block; 271 | padding:5px 0 0; 272 | } 273 | 274 | #epiceditor-preview span.align-center{ 275 | display:block; 276 | overflow:hidden; 277 | clear:both; 278 | } 279 | 280 | #epiceditor-preview span.align-center>span{ 281 | display:block; 282 | overflow:hidden; 283 | margin:13px auto 0; 284 | text-align:center; 285 | } 286 | 287 | #epiceditor-preview span.align-center span img{ 288 | margin:0 auto; 289 | text-align:center; 290 | } 291 | 292 | #epiceditor-preview span.align-right{ 293 | display:block; 294 | overflow:hidden; 295 | clear:both; 296 | } 297 | 298 | #epiceditor-preview span.align-right>span{ 299 | display:block; 300 | overflow:hidden; 301 | margin:13px 0 0; 302 | text-align:right; 303 | } 304 | 305 | #epiceditor-preview span.align-right span img{ 306 | margin:0; 307 | text-align:right; 308 | } 309 | 310 | #epiceditor-preview span.float-left{ 311 | display:block; 312 | margin-right:13px; 313 | overflow:hidden; 314 | float:left; 315 | } 316 | 317 | #epiceditor-preview span.float-left span{ 318 | margin:13px 0 0; 319 | } 320 | 321 | #epiceditor-preview span.float-right{ 322 | display:block; 323 | margin-left:13px; 324 | overflow:hidden; 325 | float:right; 326 | } 327 | 328 | #epiceditor-preview span.float-right>span{ 329 | display:block; 330 | overflow:hidden; 331 | margin:13px auto 0; 332 | text-align:right; 333 | } 334 | 335 | #epiceditor-preview code, 336 | #epiceditor-preview tt{ 337 | margin:0 2px; 338 | padding:0 5px; 339 | white-space:nowrap; 340 | border:1px solid #eaeaea; 341 | background-color:#f8f8f8; 342 | border-radius:3px; 343 | } 344 | 345 | #epiceditor-preview pre>code{ 346 | margin:0; 347 | padding:0; 348 | white-space:pre; 349 | border:none; 350 | background:transparent; 351 | } 352 | 353 | #epiceditor-preview .highlight pre, 354 | #epiceditor-preview pre{ 355 | background-color:#f8f8f8; 356 | border:1px solid #ccc; 357 | font-size:13px; 358 | line-height:19px; 359 | overflow:auto; 360 | padding:6px 10px; 361 | border-radius:3px; 362 | } 363 | 364 | #epiceditor-preview pre code, 365 | #epiceditor-preview pre tt{ 366 | background-color:transparent; 367 | border:none; 368 | } 369 | -------------------------------------------------------------------------------- /public/dist/epic/preview/preview-dark.css: -------------------------------------------------------------------------------- 1 | html { padding:0 10px; } 2 | 3 | body { 4 | margin:0; 5 | padding:10px 0; 6 | background:#000; 7 | } 8 | 9 | #epiceditor-preview h1, 10 | #epiceditor-preview h2, 11 | #epiceditor-preview h3, 12 | #epiceditor-preview h4, 13 | #epiceditor-preview h5, 14 | #epiceditor-preview h6, 15 | #epiceditor-preview p, 16 | #epiceditor-preview blockquote { 17 | margin: 0; 18 | padding: 0; 19 | } 20 | #epiceditor-preview { 21 | background:#000; 22 | font-family: "Helvetica Neue", Helvetica, "Hiragino Sans GB", Arial, sans-serif; 23 | font-size: 13px; 24 | line-height: 18px; 25 | color: #ccc; 26 | } 27 | #epiceditor-preview a { 28 | color: #fff; 29 | } 30 | #epiceditor-preview a:hover { 31 | color: #00ff00; 32 | text-decoration: none; 33 | } 34 | #epiceditor-preview a img { 35 | border: none; 36 | } 37 | #epiceditor-preview p { 38 | margin-bottom: 9px; 39 | } 40 | #epiceditor-preview h1, 41 | #epiceditor-preview h2, 42 | #epiceditor-preview h3, 43 | #epiceditor-preview h4, 44 | #epiceditor-preview h5, 45 | #epiceditor-preview h6 { 46 | color: #cdcdcd; 47 | line-height: 36px; 48 | } 49 | #epiceditor-preview h1 { 50 | margin-bottom: 18px; 51 | font-size: 30px; 52 | } 53 | #epiceditor-preview h2 { 54 | font-size: 24px; 55 | } 56 | #epiceditor-preview h3 { 57 | font-size: 18px; 58 | } 59 | #epiceditor-preview h4 { 60 | font-size: 16px; 61 | } 62 | #epiceditor-preview h5 { 63 | font-size: 14px; 64 | } 65 | #epiceditor-preview h6 { 66 | font-size: 13px; 67 | } 68 | #epiceditor-preview hr { 69 | margin: 0 0 19px; 70 | border: 0; 71 | border-bottom: 1px solid #ccc; 72 | } 73 | #epiceditor-preview blockquote { 74 | padding: 13px 13px 21px 15px; 75 | margin-bottom: 18px; 76 | font-family:georgia,serif; 77 | font-style: italic; 78 | } 79 | #epiceditor-preview blockquote:before { 80 | content:"\201C"; 81 | font-size:40px; 82 | margin-left:-10px; 83 | font-family:georgia,serif; 84 | color:#eee; 85 | } 86 | #epiceditor-preview blockquote p { 87 | font-size: 14px; 88 | font-weight: 300; 89 | line-height: 18px; 90 | margin-bottom: 0; 91 | font-style: italic; 92 | } 93 | #epiceditor-preview code, #epiceditor-preview pre { 94 | font-family: Monaco, Andale Mono, Courier New, monospace; 95 | } 96 | #epiceditor-preview code { 97 | background-color: #000; 98 | color: #f92672; 99 | padding: 1px 3px; 100 | font-size: 12px; 101 | -webkit-border-radius: 3px; 102 | -moz-border-radius: 3px; 103 | border-radius: 3px; 104 | } 105 | #epiceditor-preview pre { 106 | display: block; 107 | padding: 14px; 108 | color:#66d9ef; 109 | margin: 0 0 18px; 110 | line-height: 16px; 111 | font-size: 11px; 112 | border: 1px solid #d9d9d9; 113 | white-space: pre-wrap; 114 | word-wrap: break-word; 115 | } 116 | #epiceditor-preview pre code { 117 | background-color: #000; 118 | color:#ccc; 119 | font-size: 11px; 120 | padding: 0; 121 | } 122 | -------------------------------------------------------------------------------- /public/dist/epic/themes/editor/epic-dark.css: -------------------------------------------------------------------------------- 1 | html { padding:10px; } 2 | 3 | body { 4 | border:0; 5 | background:rgb(41,41,41); 6 | font-family:monospace; 7 | font-size:14px; 8 | padding:10px; 9 | color:#ddd; 10 | line-height:1.35em; 11 | margin:0; 12 | padding:0; 13 | } 14 | -------------------------------------------------------------------------------- /public/dist/epic/themes/editor/epic-light.css: -------------------------------------------------------------------------------- 1 | html { padding:10px; } 2 | 3 | body { 4 | border:0; 5 | background:#fcfcfc; 6 | font-family:monospace; 7 | font-size:14px; 8 | padding:10px; 9 | line-height:1.35em; 10 | margin:0; 11 | padding:0; 12 | } 13 | -------------------------------------------------------------------------------- /public/dist/epic/themes/preview/bartik.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Georgia, "Times New Roman", Times, serif; 3 | line-height: 1.5; 4 | font-size: 87.5%; 5 | word-wrap: break-word; 6 | margin: 2em; 7 | padding: 0; 8 | border: 0; 9 | outline: 0; 10 | background: #fff; 11 | } 12 | 13 | h1, 14 | h2, 15 | h3, 16 | h4, 17 | h5, 18 | h6 { 19 | margin: 1.0em 0 0.5em; 20 | font-weight: inherit; 21 | } 22 | 23 | h1 { 24 | font-size: 1.357em; 25 | color: #000; 26 | } 27 | 28 | h2 { 29 | font-size: 1.143em; 30 | } 31 | 32 | p { 33 | margin: 0 0 1.2em; 34 | } 35 | 36 | del { 37 | text-decoration: line-through; 38 | } 39 | 40 | tr:nth-child(odd) { 41 | background-color: #dddddd; 42 | } 43 | 44 | img { 45 | outline: 0; 46 | } 47 | 48 | code { 49 | background-color: #f2f2f2; 50 | background-color: rgba(40, 40, 0, 0.06); 51 | } 52 | 53 | pre { 54 | background-color: #f2f2f2; 55 | background-color: rgba(40, 40, 0, 0.06); 56 | margin: 10px 0; 57 | overflow: hidden; 58 | padding: 15px; 59 | white-space: pre-wrap; 60 | } 61 | 62 | pre code { 63 | font-size: 100%; 64 | background-color: transparent; 65 | } 66 | 67 | blockquote { 68 | background: #f7f7f7; 69 | border-left: 1px solid #bbb; 70 | font-style: italic; 71 | margin: 1.5em 10px; 72 | padding: 0.5em 10px; 73 | } 74 | 75 | blockquote:before { 76 | color: #bbb; 77 | content: "\201C"; 78 | font-size: 3em; 79 | line-height: 0.1em; 80 | margin-right: 0.2em; 81 | vertical-align: -.4em; 82 | } 83 | 84 | blockquote:after { 85 | color: #bbb; 86 | content: "\201D"; 87 | font-size: 3em; 88 | line-height: 0.1em; 89 | vertical-align: -.45em; 90 | } 91 | 92 | blockquote > p:first-child { 93 | display: inline; 94 | } 95 | 96 | table { 97 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 98 | border: 0; 99 | border-spacing: 0; 100 | font-size: 0.857em; 101 | margin: 10px 0; 102 | width: 100%; 103 | } 104 | 105 | table table { 106 | font-size: 1em; 107 | } 108 | 109 | table tr th { 110 | background: #757575; 111 | background: rgba(0, 0, 0, 0.51); 112 | border-bottom-style: none; 113 | } 114 | 115 | table tr th, 116 | table tr th a, 117 | table tr th a:hover { 118 | color: #FFF; 119 | font-weight: bold; 120 | } 121 | 122 | table tbody tr th { 123 | vertical-align: top; 124 | } 125 | 126 | tr td, 127 | tr th { 128 | padding: 4px 9px; 129 | border: 1px solid #fff; 130 | text-align: left; /* LTR */ 131 | } 132 | 133 | tr:nth-child(odd) { 134 | background: #e4e4e4; 135 | background: rgba(0, 0, 0, 0.105); 136 | } 137 | 138 | tr, 139 | tr:nth-child(even) { 140 | background: #efefef; 141 | background: rgba(0, 0, 0, 0.063); 142 | } 143 | 144 | a { 145 | color: #0071B3; 146 | } 147 | 148 | a:hover, 149 | a:focus { 150 | color: #018fe2; 151 | } 152 | 153 | a:active { 154 | color: #23aeff; 155 | } 156 | 157 | a:link, 158 | a:visited { 159 | text-decoration: none; 160 | } 161 | 162 | a:hover, 163 | a:active, 164 | a:focus { 165 | text-decoration: underline; 166 | } 167 | 168 | -------------------------------------------------------------------------------- /public/dist/epic/themes/preview/github.css: -------------------------------------------------------------------------------- 1 | html { padding:0 10px; } 2 | 3 | body { 4 | margin:0; 5 | padding:0; 6 | background:#fff; 7 | } 8 | 9 | #epiceditor-wrapper{ 10 | background:white; 11 | } 12 | 13 | #epiceditor-preview{ 14 | padding-top:10px; 15 | padding-bottom:10px; 16 | font-family: Helvetica,arial,freesans,clean,sans-serif; 17 | font-size:13px; 18 | line-height:1.6; 19 | } 20 | 21 | #epiceditor-preview>*:first-child{ 22 | margin-top:0!important; 23 | } 24 | 25 | #epiceditor-preview>*:last-child{ 26 | margin-bottom:0!important; 27 | } 28 | 29 | #epiceditor-preview a{ 30 | color:#4183C4; 31 | text-decoration:none; 32 | } 33 | 34 | #epiceditor-preview a:hover{ 35 | text-decoration:underline; 36 | } 37 | 38 | #epiceditor-preview h1, 39 | #epiceditor-preview h2, 40 | #epiceditor-preview h3, 41 | #epiceditor-preview h4, 42 | #epiceditor-preview h5, 43 | #epiceditor-preview h6{ 44 | margin:20px 0 10px; 45 | padding:0; 46 | font-weight:bold; 47 | -webkit-font-smoothing:antialiased; 48 | } 49 | 50 | #epiceditor-preview h1 tt, 51 | #epiceditor-preview h1 code, 52 | #epiceditor-preview h2 tt, 53 | #epiceditor-preview h2 code, 54 | #epiceditor-preview h3 tt, 55 | #epiceditor-preview h3 code, 56 | #epiceditor-preview h4 tt, 57 | #epiceditor-preview h4 code, 58 | #epiceditor-preview h5 tt, 59 | #epiceditor-preview h5 code, 60 | #epiceditor-preview h6 tt, 61 | #epiceditor-preview h6 code{ 62 | font-size:inherit; 63 | } 64 | 65 | #epiceditor-preview h1{ 66 | font-size:28px; 67 | color:#000; 68 | } 69 | 70 | #epiceditor-preview h2{ 71 | font-size:24px; 72 | border-bottom:1px solid #ccc; 73 | color:#000; 74 | } 75 | 76 | #epiceditor-preview h3{ 77 | font-size:18px; 78 | } 79 | 80 | #epiceditor-preview h4{ 81 | font-size:16px; 82 | } 83 | 84 | #epiceditor-preview h5{ 85 | font-size:14px; 86 | } 87 | 88 | #epiceditor-preview h6{ 89 | color:#777; 90 | font-size:14px; 91 | } 92 | 93 | #epiceditor-preview p, 94 | #epiceditor-preview blockquote, 95 | #epiceditor-preview ul, 96 | #epiceditor-preview ol, 97 | #epiceditor-preview dl, 98 | #epiceditor-preview li, 99 | #epiceditor-preview table, 100 | #epiceditor-preview pre{ 101 | margin:15px 0; 102 | } 103 | 104 | #epiceditor-preview hr{ 105 | background:transparent url('../../images/modules/pulls/dirty-shade.png') repeat-x 0 0; 106 | border:0 none; 107 | color:#ccc; 108 | height:4px; 109 | padding:0; 110 | } 111 | 112 | #epiceditor-preview>h2:first-child, 113 | #epiceditor-preview>h1:first-child, 114 | #epiceditor-preview>h1:first-child+h2, 115 | #epiceditor-preview>h3:first-child, 116 | #epiceditor-preview>h4:first-child, 117 | #epiceditor-preview>h5:first-child, 118 | #epiceditor-preview>h6:first-child{ 119 | margin-top:0; 120 | padding-top:0; 121 | } 122 | 123 | #epiceditor-preview h1+p, 124 | #epiceditor-preview h2+p, 125 | #epiceditor-preview h3+p, 126 | #epiceditor-preview h4+p, 127 | #epiceditor-preview h5+p, 128 | #epiceditor-preview h6+p{ 129 | margin-top:0; 130 | } 131 | 132 | #epiceditor-preview li p.first{ 133 | display:inline-block; 134 | } 135 | 136 | #epiceditor-preview ul, 137 | #epiceditor-preview ol{ 138 | padding-left:30px; 139 | } 140 | 141 | #epiceditor-preview ul li>:first-child, 142 | #epiceditor-preview ol li>:first-child{ 143 | margin-top:0; 144 | } 145 | 146 | #epiceditor-preview ul li>:last-child, 147 | #epiceditor-preview ol li>:last-child{ 148 | margin-bottom:0; 149 | } 150 | 151 | #epiceditor-preview dl{ 152 | padding:0; 153 | } 154 | 155 | #epiceditor-preview dl dt{ 156 | font-size:14px; 157 | font-weight:bold; 158 | font-style:italic; 159 | padding:0; 160 | margin:15px 0 5px; 161 | } 162 | 163 | #epiceditor-preview dl dt:first-child{ 164 | padding:0; 165 | } 166 | 167 | #epiceditor-preview dl dt>:first-child{ 168 | margin-top:0; 169 | } 170 | 171 | #epiceditor-preview dl dt>:last-child{ 172 | margin-bottom:0; 173 | } 174 | 175 | #epiceditor-preview dl dd{ 176 | margin:0 0 15px; 177 | padding:0 15px; 178 | } 179 | 180 | #epiceditor-preview dl dd>:first-child{ 181 | margin-top:0; 182 | } 183 | 184 | #epiceditor-preview dl dd>:last-child{ 185 | margin-bottom:0; 186 | } 187 | 188 | #epiceditor-preview blockquote{ 189 | border-left:4px solid #DDD; 190 | padding:0 15px; 191 | color:#777; 192 | } 193 | 194 | #epiceditor-preview blockquote>:first-child{ 195 | margin-top:0; 196 | } 197 | 198 | #epiceditor-preview blockquote>:last-child{ 199 | margin-bottom:0; 200 | } 201 | 202 | #epiceditor-preview table{ 203 | padding:0; 204 | border-collapse: collapse; 205 | border-spacing: 0; 206 | font-size: 100%; 207 | font: inherit; 208 | } 209 | 210 | #epiceditor-preview table tr{ 211 | border-top:1px solid #ccc; 212 | background-color:#fff; 213 | margin:0; 214 | padding:0; 215 | } 216 | 217 | #epiceditor-preview table tr:nth-child(2n){ 218 | background-color:#f8f8f8; 219 | } 220 | 221 | #epiceditor-preview table tr th{ 222 | font-weight:bold; 223 | } 224 | 225 | #epiceditor-preview table tr th, 226 | #epiceditor-preview table tr td{ 227 | border:1px solid #ccc; 228 | text-align:left; 229 | margin:0; 230 | padding:6px 13px; 231 | } 232 | 233 | #epiceditor-preview table tr th>:first-child, 234 | #epiceditor-preview table tr td>:first-child{ 235 | margin-top:0; 236 | } 237 | 238 | #epiceditor-preview table tr th>:last-child, 239 | #epiceditor-preview table tr td>:last-child{ 240 | margin-bottom:0; 241 | } 242 | 243 | #epiceditor-preview img{ 244 | max-width:100%; 245 | } 246 | 247 | #epiceditor-preview span.frame{ 248 | display:block; 249 | overflow:hidden; 250 | } 251 | 252 | #epiceditor-preview span.frame>span{ 253 | border:1px solid #ddd; 254 | display:block; 255 | float:left; 256 | overflow:hidden; 257 | margin:13px 0 0; 258 | padding:7px; 259 | width:auto; 260 | } 261 | 262 | #epiceditor-preview span.frame span img{ 263 | display:block; 264 | float:left; 265 | } 266 | 267 | #epiceditor-preview span.frame span span{ 268 | clear:both; 269 | color:#333; 270 | display:block; 271 | padding:5px 0 0; 272 | } 273 | 274 | #epiceditor-preview span.align-center{ 275 | display:block; 276 | overflow:hidden; 277 | clear:both; 278 | } 279 | 280 | #epiceditor-preview span.align-center>span{ 281 | display:block; 282 | overflow:hidden; 283 | margin:13px auto 0; 284 | text-align:center; 285 | } 286 | 287 | #epiceditor-preview span.align-center span img{ 288 | margin:0 auto; 289 | text-align:center; 290 | } 291 | 292 | #epiceditor-preview span.align-right{ 293 | display:block; 294 | overflow:hidden; 295 | clear:both; 296 | } 297 | 298 | #epiceditor-preview span.align-right>span{ 299 | display:block; 300 | overflow:hidden; 301 | margin:13px 0 0; 302 | text-align:right; 303 | } 304 | 305 | #epiceditor-preview span.align-right span img{ 306 | margin:0; 307 | text-align:right; 308 | } 309 | 310 | #epiceditor-preview span.float-left{ 311 | display:block; 312 | margin-right:13px; 313 | overflow:hidden; 314 | float:left; 315 | } 316 | 317 | #epiceditor-preview span.float-left span{ 318 | margin:13px 0 0; 319 | } 320 | 321 | #epiceditor-preview span.float-right{ 322 | display:block; 323 | margin-left:13px; 324 | overflow:hidden; 325 | float:right; 326 | } 327 | 328 | #epiceditor-preview span.float-right>span{ 329 | display:block; 330 | overflow:hidden; 331 | margin:13px auto 0; 332 | text-align:right; 333 | } 334 | 335 | #epiceditor-preview code, 336 | #epiceditor-preview tt{ 337 | margin:0 2px; 338 | padding:0 5px; 339 | white-space:nowrap; 340 | border:1px solid #eaeaea; 341 | background-color:#f8f8f8; 342 | border-radius:3px; 343 | } 344 | 345 | #epiceditor-preview pre>code{ 346 | margin:0; 347 | padding:0; 348 | white-space:pre; 349 | border:none; 350 | background:transparent; 351 | } 352 | 353 | #epiceditor-preview .highlight pre, 354 | #epiceditor-preview pre{ 355 | background-color:#f8f8f8; 356 | border:1px solid #ccc; 357 | font-size:13px; 358 | line-height:19px; 359 | overflow:auto; 360 | padding:6px 10px; 361 | border-radius:3px; 362 | } 363 | 364 | #epiceditor-preview pre code, 365 | #epiceditor-preview pre tt{ 366 | background-color:transparent; 367 | border:none; 368 | } 369 | -------------------------------------------------------------------------------- /public/dist/epic/themes/preview/preview-dark.css: -------------------------------------------------------------------------------- 1 | html { padding:0 10px; } 2 | 3 | body { 4 | margin:0; 5 | padding:10px 0; 6 | background:#000; 7 | } 8 | 9 | #epiceditor-preview h1, 10 | #epiceditor-preview h2, 11 | #epiceditor-preview h3, 12 | #epiceditor-preview h4, 13 | #epiceditor-preview h5, 14 | #epiceditor-preview h6, 15 | #epiceditor-preview p, 16 | #epiceditor-preview blockquote { 17 | margin: 0; 18 | padding: 0; 19 | } 20 | #epiceditor-preview { 21 | background:#000; 22 | font-family: "Helvetica Neue", Helvetica, "Hiragino Sans GB", Arial, sans-serif; 23 | font-size: 13px; 24 | line-height: 18px; 25 | color: #ccc; 26 | } 27 | #epiceditor-preview a { 28 | color: #fff; 29 | } 30 | #epiceditor-preview a:hover { 31 | color: #00ff00; 32 | text-decoration: none; 33 | } 34 | #epiceditor-preview a img { 35 | border: none; 36 | } 37 | #epiceditor-preview p { 38 | margin-bottom: 9px; 39 | } 40 | #epiceditor-preview h1, 41 | #epiceditor-preview h2, 42 | #epiceditor-preview h3, 43 | #epiceditor-preview h4, 44 | #epiceditor-preview h5, 45 | #epiceditor-preview h6 { 46 | color: #cdcdcd; 47 | line-height: 36px; 48 | } 49 | #epiceditor-preview h1 { 50 | margin-bottom: 18px; 51 | font-size: 30px; 52 | } 53 | #epiceditor-preview h2 { 54 | font-size: 24px; 55 | } 56 | #epiceditor-preview h3 { 57 | font-size: 18px; 58 | } 59 | #epiceditor-preview h4 { 60 | font-size: 16px; 61 | } 62 | #epiceditor-preview h5 { 63 | font-size: 14px; 64 | } 65 | #epiceditor-preview h6 { 66 | font-size: 13px; 67 | } 68 | #epiceditor-preview hr { 69 | margin: 0 0 19px; 70 | border: 0; 71 | border-bottom: 1px solid #ccc; 72 | } 73 | #epiceditor-preview blockquote { 74 | padding: 13px 13px 21px 15px; 75 | margin-bottom: 18px; 76 | font-family:georgia,serif; 77 | font-style: italic; 78 | } 79 | #epiceditor-preview blockquote:before { 80 | content:"\201C"; 81 | font-size:40px; 82 | margin-left:-10px; 83 | font-family:georgia,serif; 84 | color:#eee; 85 | } 86 | #epiceditor-preview blockquote p { 87 | font-size: 14px; 88 | font-weight: 300; 89 | line-height: 18px; 90 | margin-bottom: 0; 91 | font-style: italic; 92 | } 93 | #epiceditor-preview code, #epiceditor-preview pre { 94 | font-family: Monaco, Andale Mono, Courier New, monospace; 95 | } 96 | #epiceditor-preview code { 97 | background-color: #000; 98 | color: #f92672; 99 | padding: 1px 3px; 100 | font-size: 12px; 101 | -webkit-border-radius: 3px; 102 | -moz-border-radius: 3px; 103 | border-radius: 3px; 104 | } 105 | #epiceditor-preview pre { 106 | display: block; 107 | padding: 14px; 108 | color:#66d9ef; 109 | margin: 0 0 18px; 110 | line-height: 16px; 111 | font-size: 11px; 112 | border: 1px solid #d9d9d9; 113 | white-space: pre-wrap; 114 | word-wrap: break-word; 115 | } 116 | #epiceditor-preview pre code { 117 | background-color: #000; 118 | color:#ccc; 119 | font-size: 11px; 120 | padding: 0; 121 | } 122 | -------------------------------------------------------------------------------- /public/dist/font/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchester/showcase/e6819fe96a2a9e3cd5d7fd5e0f016994000cb153/public/dist/font/FontAwesome.otf -------------------------------------------------------------------------------- /public/dist/font/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchester/showcase/e6819fe96a2a9e3cd5d7fd5e0f016994000cb153/public/dist/font/fontawesome-webfont.eot -------------------------------------------------------------------------------- /public/dist/font/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchester/showcase/e6819fe96a2a9e3cd5d7fd5e0f016994000cb153/public/dist/font/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /public/dist/font/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchester/showcase/e6819fe96a2a9e3cd5d7fd5e0f016994000cb153/public/dist/font/fontawesome-webfont.woff -------------------------------------------------------------------------------- /public/images/dataflight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchester/showcase/e6819fe96a2a9e3cd5d7fd5e0f016994000cb153/public/images/dataflight.png -------------------------------------------------------------------------------- /public/images/dataflight_125.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchester/showcase/e6819fe96a2a9e3cd5d7fd5e0f016994000cb153/public/images/dataflight_125.png -------------------------------------------------------------------------------- /public/images/showcase_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchester/showcase/e6819fe96a2a9e3cd5d7fd5e0f016994000cb153/public/images/showcase_logo.png -------------------------------------------------------------------------------- /public/js/lib/Showcase.Collection.js: -------------------------------------------------------------------------------- 1 | Showcase.Collection = function(args) { 2 | 3 | this.initialize = function(args) { 4 | 5 | this.element = args.element; 6 | this.element.querySelector('#new_field') 7 | .addEventListener('click', function() { 8 | this.addFieldInputs({ show: true }); 9 | }.bind(this)); 10 | 11 | var source = this.element.querySelector("#row_template").innerHTML; 12 | this.template = swig.compile(source); 13 | 14 | this.element.addEventListener('submit', function(e) { 15 | if (!this.element.querySelector("input[name=field_title]")) { 16 | e.preventDefault(); 17 | this.element.classList.add('error-no-fields'); 18 | } 19 | }.bind(this)); 20 | }; 21 | 22 | this.addFieldInputs = function(args) { 23 | 24 | args = args || {}; 25 | 26 | var field = args.field; 27 | 28 | this.element.classList.remove('error-no-fields'); 29 | 30 | var fields = this.element.querySelector('#fields'); 31 | var row = document.createElement('li'); 32 | row.className = 'row'; 33 | 34 | row.innerHTML = this.template({ field: field }); 35 | fields.appendChild(row); 36 | 37 | var deleter = row.querySelector(".delete"); 38 | deleter.addEventListener('click', function() { 39 | row.parentNode.removeChild(row); 40 | }); 41 | 42 | new Showcase.Form.Input.Pair({ 43 | source: row.querySelector('input[name=field_title]'), 44 | target: row.querySelector('input[name=field_name]') 45 | }); 46 | 47 | new Showcase.Form.Input.Reflector({ 48 | source: row.querySelector('.inputs .title'), 49 | target: row.querySelector('.labels .title') 50 | }); 51 | 52 | new Showcase.Form.Input.Reflector({ 53 | source: row.querySelector('.inputs .data_type'), 54 | target: row.querySelector('.labels .data_type') 55 | }); 56 | 57 | new Showcase.Form.Input.Reflector({ 58 | source: row.querySelector('.inputs .required'), 59 | target: row.querySelector('.labels .required'), 60 | transform: function(is_required) { return Number(is_required) ? 'required' : 'optional' } 61 | }); 62 | 63 | var modal = row.querySelector('.overlay'); 64 | 65 | row.querySelector('.title').addEventListener('click', function(e) { 66 | modal.classList.add('visible'); 67 | }); 68 | 69 | row.querySelector('.settings').addEventListener('click', function(e) { 70 | modal.classList.add('visible'); 71 | }); 72 | 73 | modal.querySelector('button.save_fields').addEventListener('click', function(e) { 74 | modal.classList.remove('visible'); 75 | }); 76 | 77 | modal.querySelector('button.cancel_fields').addEventListener('click', function(e) { 78 | var field_id_input = row.querySelector('input[name=field_id]'); 79 | if (!(field_id_input && field_id_input.value)) { 80 | row.parentNode.removeChild(row); 81 | } 82 | modal.classList.remove('visible'); 83 | }); 84 | 85 | if (args.show) { 86 | modal.classList.add('visible'); 87 | } 88 | 89 | }; 90 | 91 | this.initialize(args); 92 | } 93 | 94 | -------------------------------------------------------------------------------- /public/js/lib/Showcase.Form.Input.js: -------------------------------------------------------------------------------- 1 | Showcase.namespace("Showcase.Form.Input"); 2 | 3 | Showcase.Form.Input.Pair = function(args) { 4 | 5 | this.source = args.source; 6 | this.target = args.target; 7 | 8 | this.transform = args.transform || function(str) { 9 | return str 10 | .toLowerCase() 11 | .replace(/(^\s+|\s+$)/g, '') 12 | .replace(/[^a-z]+/g, '_'); 13 | }; 14 | 15 | if (!parseInt(this.target.getAttribute('data-autoupdate'))) return; 16 | 17 | this.source.addEventListener('keyup', function(e) { 18 | 19 | var transformedValue = this.transform(this.source.value); 20 | 21 | if (!this.target.getAttribute('data-dirty')) { 22 | this.target.value = transformedValue; 23 | } 24 | 25 | }.bind(this)); 26 | 27 | this.target.addEventListener('keyup', function() { 28 | this.target.setAttribute('data-dirty', true); 29 | 30 | }.bind(this)); 31 | }; 32 | 33 | Showcase.Form.Input.Reflector = function(args) { 34 | 35 | this.source = args.source; 36 | this.target = args.target; 37 | 38 | this.transform = args.transform || function(value) { return value }; 39 | 40 | this.reflect = function(e) { this.target.textContent = this.transform(e.target.value); }; 41 | 42 | this.target.innerText = this.transform(this.source.value); 43 | 44 | this.source.addEventListener('change', this.reflect.bind(this)); 45 | this.source.addEventListener('keyup', this.reflect.bind(this)); 46 | }; 47 | 48 | 49 | -------------------------------------------------------------------------------- /public/js/lib/Showcase.js: -------------------------------------------------------------------------------- 1 | window.Showcase = { 2 | 3 | namespace: function(namespace, obj) { 4 | 5 | var parts = namespace.split('.'); 6 | 7 | var parent = Showcase; 8 | 9 | for(var i = 1, length = parts.length; i < length; i++) { 10 | var currentPart = parts[i]; 11 | parent[currentPart] = parent[currentPart] || {}; 12 | parent = parent[currentPart]; 13 | } 14 | 15 | return parent; 16 | } 17 | }; 18 | 19 | /* Adapted from https://github.com/Jakobo/PTClass */ 20 | 21 | /* 22 | Copyright (c) 2005-2010 Sam Stephenson 23 | 24 | Permission is hereby granted, free of charge, to any person obtaining a copy 25 | of this software and associated documentation files (the "Software"), to deal 26 | in the Software without restriction, including without limitation the rights 27 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 28 | copies of the Software, and to permit persons to whom the Software is 29 | furnished to do so, subject to the following conditions: 30 | 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 32 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 33 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 34 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 35 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 36 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 37 | SOFTWARE. 38 | */ 39 | /* Based on Alex Arnell's inheritance implementation. */ 40 | /** section: Language 41 | * class Class 42 | * 43 | * Manages Prototype's class-based OOP system. 44 | * 45 | * Refer to Prototype's web site for a [tutorial on classes and 46 | * inheritance](http://prototypejs.org/learn/class-inheritance). 47 | **/ 48 | (function(globalContext) { 49 | /* ------------------------------------ */ 50 | /* Import from object.js */ 51 | /* ------------------------------------ */ 52 | var _toString = Object.prototype.toString, 53 | NULL_TYPE = 'Null', 54 | UNDEFINED_TYPE = 'Undefined', 55 | BOOLEAN_TYPE = 'Boolean', 56 | NUMBER_TYPE = 'Number', 57 | STRING_TYPE = 'String', 58 | OBJECT_TYPE = 'Object', 59 | FUNCTION_CLASS = '[object Function]'; 60 | function isFunction(object) { 61 | return _toString.call(object) === FUNCTION_CLASS; 62 | } 63 | function extend(destination, source) { 64 | for (var property in source) if (source.hasOwnProperty(property)) // modify protect primitive slaughter 65 | destination[property] = source[property]; 66 | return destination; 67 | } 68 | function keys(object) { 69 | if (Type(object) !== OBJECT_TYPE) { throw new TypeError(); } 70 | var results = []; 71 | for (var property in object) { 72 | if (object.hasOwnProperty(property)) { 73 | results.push(property); 74 | } 75 | } 76 | return results; 77 | } 78 | function Type(o) { 79 | switch(o) { 80 | case null: return NULL_TYPE; 81 | case (void 0): return UNDEFINED_TYPE; 82 | } 83 | var type = typeof o; 84 | switch(type) { 85 | case 'boolean': return BOOLEAN_TYPE; 86 | case 'number': return NUMBER_TYPE; 87 | case 'string': return STRING_TYPE; 88 | } 89 | return OBJECT_TYPE; 90 | } 91 | function isUndefined(object) { 92 | return typeof object === "undefined"; 93 | } 94 | /* ------------------------------------ */ 95 | /* Import from Function.js */ 96 | /* ------------------------------------ */ 97 | var slice = Array.prototype.slice; 98 | function argumentNames(fn) { 99 | var names = fn.toString().match(/^[\s\(]*function[^(]*\(([^)]*)\)/)[1] 100 | .replace(/\/\/.*?[\r\n]|\/\*(?:.|[\r\n])*?\*\//g, '') 101 | .replace(/\s+/g, '').split(','); 102 | return names.length == 1 && !names[0] ? [] : names; 103 | } 104 | function wrap(fn, wrapper) { 105 | var __method = fn; 106 | return function() { 107 | var a = update([bind(__method, this)], arguments); 108 | return wrapper.apply(this, a); 109 | } 110 | } 111 | function update(array, args) { 112 | var arrayLength = array.length, length = args.length; 113 | while (length--) array[arrayLength + length] = args[length]; 114 | return array; 115 | } 116 | function merge(array, args) { 117 | array = slice.call(array, 0); 118 | return update(array, args); 119 | } 120 | function bind(fn, context) { 121 | if (arguments.length < 2 && isUndefined(arguments[0])) return this; 122 | var __method = fn, args = slice.call(arguments, 2); 123 | return function() { 124 | var a = merge(args, arguments); 125 | return __method.apply(context, a); 126 | } 127 | } 128 | 129 | /* ------------------------------------ */ 130 | /* Import from Prototype.js */ 131 | /* ------------------------------------ */ 132 | var emptyFunction = function(){}; 133 | 134 | var Class = (function() { 135 | 136 | // Some versions of JScript fail to enumerate over properties, names of which 137 | // correspond to non-enumerable properties in the prototype chain 138 | var IS_DONTENUM_BUGGY = (function(){ 139 | for (var p in { toString: 1 }) { 140 | // check actual property name, so that it works with augmented Object.prototype 141 | if (p === 'toString') return false; 142 | } 143 | return true; 144 | })(); 145 | 146 | function subclass() {}; 147 | function create() { 148 | var parent = null, properties = [].slice.apply(arguments); 149 | if (isFunction(properties[0])) 150 | parent = properties.shift(); 151 | 152 | function klass() { 153 | this.initialize.apply(this, arguments); 154 | } 155 | 156 | extend(klass, Class.Methods); 157 | klass.superclass = parent; 158 | klass.subclasses = []; 159 | 160 | if (parent) { 161 | subclass.prototype = parent.prototype; 162 | klass.prototype = new subclass; 163 | try { parent.subclasses.push(klass) } catch(e) {} 164 | } 165 | 166 | for (var i = 0, length = properties.length; i < length; i++) 167 | klass.addMethods(properties[i]); 168 | 169 | if (!klass.prototype.initialize) 170 | klass.prototype.initialize = emptyFunction; 171 | 172 | klass.prototype.constructor = klass; 173 | return klass; 174 | } 175 | 176 | function addMethods(source) { 177 | var ancestor = this.superclass && this.superclass.prototype, 178 | properties = keys(source); 179 | 180 | // IE6 doesn't enumerate `toString` and `valueOf` (among other built-in `Object.prototype`) properties, 181 | // Force copy if they're not Object.prototype ones. 182 | // Do not copy other Object.prototype.* for performance reasons 183 | if (IS_DONTENUM_BUGGY) { 184 | if (source.toString != Object.prototype.toString) 185 | properties.push("toString"); 186 | if (source.valueOf != Object.prototype.valueOf) 187 | properties.push("valueOf"); 188 | } 189 | 190 | for (var i = 0, length = properties.length; i < length; i++) { 191 | var property = properties[i], value = source[property]; 192 | if (ancestor && isFunction(value) && 193 | argumentNames(value)[0] == "$super") { 194 | var method = value; 195 | value = wrap((function(m) { 196 | return function() { return ancestor[m].apply(this, arguments); }; 197 | })(property), method); 198 | 199 | value.valueOf = bind(method.valueOf, method); 200 | value.toString = bind(method.toString, method); 201 | } 202 | this.prototype[property] = value; 203 | } 204 | 205 | return this; 206 | } 207 | 208 | return { 209 | create: create, 210 | Methods: { 211 | addMethods: addMethods 212 | } 213 | }; 214 | })(); 215 | 216 | if (globalContext.exports) { 217 | globalContext.exports.Class = Class; 218 | } 219 | else { 220 | globalContext.Class = Class; 221 | } 222 | })(Showcase); 223 | -------------------------------------------------------------------------------- /public/vendor/hljs/hljs.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | github.com style (c) Vasily Polovnyov 4 | 5 | */ 6 | 7 | pre code { 8 | display: block; padding: 0.5em; 9 | color: #333; 10 | background: #f8f8f8; 11 | } 12 | 13 | pre .comment, 14 | pre .template_comment, 15 | pre .diff .header, 16 | pre .javadoc { 17 | color: #998; 18 | font-style: italic 19 | } 20 | 21 | pre .keyword, 22 | pre .css .rule .keyword, 23 | pre .winutils, 24 | pre .javascript .title, 25 | pre .lisp .title, 26 | pre .nginx .title, 27 | pre .subst, 28 | pre .request, 29 | pre .status { 30 | color: #000; 31 | font-weight: bold 32 | } 33 | 34 | pre .number, 35 | pre .hexcolor { 36 | color: #099; 37 | } 38 | 39 | pre .string, 40 | pre .tag .value, 41 | pre .phpdoc, 42 | pre .tex .formula { 43 | color: #d14 44 | } 45 | 46 | pre .title, 47 | pre .id { 48 | color: #900; 49 | font-weight: bold 50 | } 51 | 52 | pre .javascript .title, 53 | pre .lisp .title, 54 | pre .subst { 55 | font-weight: normal 56 | } 57 | 58 | pre .class .title, 59 | pre .haskell .type, 60 | pre .vhdl .literal, 61 | pre .tex .command { 62 | color: #458; 63 | font-weight: bold 64 | } 65 | 66 | pre .tag, 67 | pre .tag .title, 68 | pre .rules .property, 69 | pre .django .tag .keyword { 70 | color: #000080; 71 | font-weight: normal 72 | } 73 | 74 | pre .attribute { 75 | color: navy; 76 | } 77 | 78 | pre .variable, 79 | pre .instancevar, 80 | pre .lisp .body { 81 | color: #099; 82 | } 83 | 84 | pre .regexp { 85 | color: #009926 86 | } 87 | 88 | pre .class { 89 | color: #458; 90 | font-weight: bold 91 | } 92 | 93 | pre .symbol, 94 | pre .ruby .symbol .string, 95 | pre .ruby .symbol .keyword, 96 | pre .ruby .symbol .keymethods, 97 | pre .lisp .keyword, 98 | pre .tex .special, 99 | pre .input_number { 100 | color: #990073 101 | } 102 | 103 | pre .builtin, 104 | pre .built_in, 105 | pre .lisp .title { 106 | color: #0086b3 107 | } 108 | 109 | pre .preprocessor, 110 | pre .pi, 111 | pre .doctype, 112 | pre .shebang, 113 | pre .cdata { 114 | color: #999; 115 | font-weight: bold 116 | } 117 | 118 | pre .deletion { 119 | background: #fdd 120 | } 121 | 122 | pre .addition { 123 | background: #dfd 124 | } 125 | 126 | pre .diff .change { 127 | background: #0086b3 128 | } 129 | 130 | pre .chunk { 131 | color: #aaa 132 | } 133 | 134 | pre .tex .formula { 135 | opacity: 0.5; 136 | } 137 | -------------------------------------------------------------------------------- /routes/api.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | var armrest = require('armrest'); 3 | var Deferrals = require('../lib/deferrals'); 4 | var Item = require('../lib/item'); 5 | var api = require('../lib/api.js'); 6 | var Sort = require('../lib/sort'); 7 | var clone = require('../lib/clone'); 8 | 9 | var Collection = require('../lib/collection.js'); 10 | var EXAMPLE_LENGTH = 1500; 11 | 12 | exports.initialize = function(app) { 13 | 14 | var models = app.dreamer.models; 15 | var workspaceLoader = app.showcase.middleware.workspaceLoader; 16 | 17 | app.post('/api/:workspace_handle/:collection_handle', workspaceLoader, function*(req, res) { 18 | 19 | var workspace = req.showcase.workspace; 20 | var status = req.body._status; 21 | var collection_name = req.params.collection_handle; 22 | var data = req.body; 23 | var user_id = api.user.id; 24 | 25 | var collection = yield Collection.load({ name: collection_name, workspace_handle: workspace.handle }); 26 | 27 | if (!collection) return res.json(404, { 28 | message: "couldn't find collection", 29 | code: "no_collection_found" 30 | }); 31 | 32 | var item = yield Item.build({ 33 | collection_id: collection.id, 34 | status: status, 35 | data: data, 36 | user_id: user_id 37 | }); 38 | 39 | if (!item) return res.json(404, { 40 | message: "couldn't find item", 41 | code: "no_item_found" 42 | }); 43 | 44 | var errors = item.validate(); 45 | 46 | if (errors) return res.json(400, { 47 | message: "validation failed", 48 | code: "validation_failed", 49 | errors: errors 50 | }); 51 | 52 | yield item.save({ user_id: user_id }); 53 | 54 | res.json(201, Item.distill(item)); 55 | }); 56 | 57 | app.get('/api/:workspace_handle/:collection_handle/:item_id', workspaceLoader, function*(req, res) { 58 | 59 | var workspace = req.showcase.workspace; 60 | var item_id = req.params.item_id; 61 | 62 | var criteria = resolveCriteria(item_id); 63 | var item = yield Item.load(criteria); 64 | 65 | if (!item) { 66 | return res.json(404, { 67 | message: "couldn't find item", 68 | code: "no_item_found" 69 | }); 70 | } 71 | 72 | var distilled_item = Item.distill(item); 73 | 74 | res.json(200, distilled_item); 75 | }); 76 | 77 | app.delete('/api/:workspace_handle/:collection_handle/:item_id', workspaceLoader, function*(req, res) { 78 | 79 | var workspace = req.showcase.workspace; 80 | var item_id = req.params.item_id; 81 | 82 | var criteria = resolveCriteria(item_id); 83 | var item = yield Item.load(criteria); 84 | 85 | if (!item) return res.json(404, { 86 | message: "couldn't find item", 87 | code: "no_item_found" 88 | }); 89 | 90 | yield item.destroy(); 91 | res.send(204); 92 | }); 93 | 94 | var patchItem = function*(req, res) { 95 | 96 | var workspace = req.showcase.workspace; 97 | var item_id = req.params.item_id; 98 | var status = req.body._status; 99 | var data = req.body; 100 | var user_id = api.user.id; 101 | 102 | var criteria = resolveCriteria(item_id); 103 | var item = yield Item.load(criteria); 104 | 105 | if (!item) return res.json(404, { 106 | message: "couldn't find item", 107 | code: "no_item_found" 108 | }); 109 | 110 | item.update({ 111 | status: status, 112 | data: req.body, 113 | user_id: user_id, 114 | }); 115 | 116 | var errors = item.validate(); 117 | 118 | if (errors) return res.json(400, { 119 | message: "validation failed", 120 | code: "validation_failed", 121 | errors: errors 122 | }); 123 | 124 | yield item.save({ user_id: user_id }); 125 | 126 | res.json(Item.distill(item)); 127 | }; 128 | 129 | app.patch('/api/:workspace_handle/:collection_handle/:item_id', workspaceLoader, patchItem); 130 | app.post('/api/:workspace_handle/:collection_handle/:item_id', workspaceLoader, patchItem); 131 | 132 | app.get('/api/:workspace_handle/:collection_handle', workspaceLoader, function*(req, res) { 133 | 134 | var name = req.params.collection_handle; 135 | var per_page = req.query.per_page || 40; 136 | var page = req.query.page || 0; 137 | var collection, items; 138 | var workspace = req.showcase.workspace; 139 | var sort = Sort.deserialize(req.query.sort); 140 | var search = req.query.q; 141 | 142 | var collection = yield Collection.load({ name: name, workspace_handle: workspace.handle }); 143 | 144 | if (!collection) return res.json(404, { 145 | message: "couldn't find collection", 146 | code: "no_collection_found" 147 | }); 148 | 149 | var criteria = {}; 150 | 151 | collection.fields.forEach(function(field) { 152 | if (field.name in req.query) { 153 | criteria[field.name] = req.query[field.name]; 154 | } 155 | }); 156 | var standard_fields = ['status', 'id']; 157 | standard_fields.forEach(function(field) { 158 | if (field in req.query) { 159 | criteria[field] = req.query[field]; 160 | } 161 | }); 162 | 163 | var items = yield Item.all({ 164 | collection_id: collection.id, 165 | criteria: criteria, 166 | sort: sort, 167 | page: page, 168 | per_page: per_page, 169 | search: search 170 | }); 171 | 172 | items.forEach(function(item) { 173 | item.collection = collection; 174 | }); 175 | 176 | var distilled_items = []; 177 | 178 | items.forEach(function(item) { 179 | distilled_items.push(Item.distill(item)); 180 | }); 181 | 182 | var total_count = items.totalCount; 183 | var range_start = (items.page - 1) * items.per_page; 184 | var range_end = range_start + distilled_items.length - 1; 185 | 186 | var content_range = "items " + range_start + "-" + range_end + "/" + total_count; 187 | 188 | res.header('Content-Range', content_range); 189 | res.json(distilled_items); 190 | }); 191 | 192 | app.get('/workspaces/:workspace_handle/api', workspaceLoader, function* (req, res) { 193 | 194 | var workspace = req.showcase.workspace; 195 | var api = armrest.client("localhost:" + app.get('port')); 196 | 197 | var collections = yield Collection.all({ workspace_handle: workspace.handle }); 198 | 199 | var collection_resources = []; 200 | 201 | async.forEach(collections, function(collection, cb) { 202 | 203 | var route = '/api/' + workspace.handle + '/' + collection.name; 204 | 205 | api.get({ 206 | url: route, 207 | params: { per_page: 1 }, 208 | success: function(items, response) { 209 | 210 | var resource = { collection: collection }; 211 | 212 | var example_response = response.body; 213 | if (example_response && example_response.length > EXAMPLE_LENGTH) { 214 | example_response = example_response.substring(0, EXAMPLE_LENGTH) + '...'; 215 | } 216 | 217 | if (response.body !== '[]') { 218 | resource.example_listing_response = example_response; 219 | } 220 | 221 | collection_resources.push(resource); 222 | cb(); 223 | } 224 | }); 225 | 226 | }, function() { 227 | 228 | collection_resources = collection_resources 229 | .sort(function(a, b) { return a.collection.title.localeCompare(b.collection.title); }); 230 | 231 | res.render("api.html", { collection_resources: collection_resources }); 232 | }); 233 | }); 234 | 235 | app.get('/api/:workspace_handle', workspaceLoader, function*(req, res) { 236 | 237 | var workspace = clone(req.showcase.workspace); 238 | delete workspace.id; 239 | 240 | var collections = yield Collection.all({ workspace_handle: workspace.handle }); 241 | 242 | collections.forEach(function(collection) { 243 | delete collection.id; 244 | delete collection.workspace_handle; 245 | collection.fields.forEach(function(field) { 246 | delete field.id; 247 | delete field.collection_id; 248 | delete field.index; 249 | delete field.meta; 250 | }); 251 | }); 252 | 253 | workspace.collections = collections; 254 | res.json(workspace); 255 | }); 256 | }; 257 | 258 | function resolveCriteria(identifier) { 259 | 260 | var criteria = {}; 261 | var field = identifier.match(/[a-z]/) ? 'key' : 'id'; 262 | criteria[field] = identifier; 263 | 264 | return criteria; 265 | }; 266 | -------------------------------------------------------------------------------- /routes/collection.js: -------------------------------------------------------------------------------- 1 | var Collection = require('../lib/collection.js'); 2 | var controls = require('../lib/fields').controls; 3 | 4 | var coalesceArray = function(data) { 5 | return Array.isArray(data) ? data : [ data ]; 6 | }; 7 | 8 | exports.initialize = function(app) { 9 | 10 | var models = app.dreamer.models; 11 | var workspaceLoader = app.showcase.middleware.workspaceLoader; 12 | var workspaceAdmin = app.showcase.middleware.workspacePermission('administrator'); 13 | 14 | app.get("/workspaces/:workspace_handle/collections", workspaceLoader, workspaceAdmin, function*(req, res) { 15 | 16 | var collections, collection_counts; 17 | var workspace_handle = req.params.workspace_handle; 18 | 19 | var collections = yield Collection.all({ workspace_handle: workspace_handle }); 20 | var collection_counts = yield Collection.itemCounts(); 21 | 22 | res.render("collections.html", { 23 | collections: collections, 24 | collection_counts: collection_counts 25 | }); 26 | }); 27 | 28 | app.get("/workspaces/:workspace_handle/collections/new", workspaceLoader, workspaceAdmin, function(req, res) { 29 | 30 | var workspace = req.showcase.workspace; 31 | var workspace_handle = req.params.workspace_handle; 32 | 33 | var subtitle = ' › New'; 34 | res.render("collection.html", { 35 | subtitle: subtitle, 36 | controls: controls 37 | }); 38 | }); 39 | 40 | app.post("/workspaces/:workspace_handle/collections/new", workspaceLoader, workspaceAdmin, function*(req, res) { 41 | 42 | var title = req.body.title; 43 | var description = req.body.description; 44 | var name = req.body.name; 45 | var workspace = req.showcase.workspace; 46 | 47 | var fields = []; 48 | var field_attributes = {}; 49 | 50 | Collection.field_attribute_names.forEach(function(name) { 51 | var data = req.body["field_" + name]; 52 | field_attributes[name] = Array.isArray(data) ? data : [ data ]; 53 | }); 54 | 55 | field_attributes.name.forEach(function(field_name, index) { 56 | 57 | var field_data = { name: field_name }; 58 | 59 | Collection.field_attribute_names.forEach(function(name) { 60 | if (name == 'name') return; 61 | field_data[name] = field_attributes[name][index]; 62 | }); 63 | 64 | field_data.is_required = Number(field_data.is_required) ? true : false; 65 | fields.push(field_data); 66 | }); 67 | 68 | var collection = yield Collection.create({ 69 | title: title, 70 | description: description, 71 | name: name, 72 | workspace_handle: workspace.handle, 73 | fields: fields, 74 | }); 75 | 76 | req.flash('info', 'Created new collection'); 77 | res.redirect('/workspaces/' + workspace.handle + '/collections'); 78 | }); 79 | 80 | app.post("/workspaces/:workspace_handle/collections/:id/edit", workspaceLoader, workspaceAdmin, function*(req, res) { 81 | 82 | var collection_id = req.params.id; 83 | 84 | var title = req.body.title; 85 | var description = req.body.description; 86 | var name = req.body.name; 87 | var workspace = req.showcase.workspace; 88 | 89 | var fields = []; 90 | var field_attributes = {}; 91 | 92 | Collection.field_attribute_names.forEach(function(name) { 93 | var data = req.body["field_" + name]; 94 | field_attributes[name] = Array.isArray(data) ? data : [ data ]; 95 | }); 96 | 97 | field_attributes.name.forEach(function(field_name, index) { 98 | 99 | var field_data = { name: field_name, collection_id: collection_id }; 100 | 101 | Collection.field_attribute_names.forEach(function(name) { 102 | if (name == 'name') return; 103 | field_data[name] = field_attributes[name][index]; 104 | }); 105 | 106 | field_data.is_required = Number(field_data.is_required) ? true : false; 107 | fields.push(field_data); 108 | }); 109 | 110 | var collection = yield Collection.load({ id: collection_id }); 111 | 112 | yield collection.update({ 113 | title: title, 114 | description: description, 115 | name: name, 116 | fields: fields, 117 | }); 118 | 119 | req.flash('info', 'Saved ' + collection.title + ' Collection'); 120 | res.redirect('/workspaces/' + workspace.handle + '/collections'); 121 | }); 122 | 123 | app.get("/workspaces/:workspace_handle/collections/:id/edit", workspaceLoader, workspaceAdmin, function*(req, res) { 124 | 125 | var collection_id = req.params.id; 126 | var collection = yield Collection.load({ id: collection_id }); 127 | 128 | res.render("collection.html", { 129 | controls: controls, 130 | collection: collection, 131 | fields: collection.fields, 132 | subtitle: " › " + collection.title 133 | }); 134 | }); 135 | 136 | app.del("/workspaces/:workspace_handle/collections/:id", workspaceLoader, workspaceAdmin, function*(req, res) { 137 | 138 | var workspace = req.showcase.workspace; 139 | var collection_id = req.params.id; 140 | var collection = yield Collection.load({ id: collection_id }); 141 | 142 | yield collection.destroy(); 143 | 144 | res.redirect("/workspaces/" + workspace.handle + "/collections"); 145 | }); 146 | 147 | }; 148 | 149 | -------------------------------------------------------------------------------- /routes/files.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | var path = require('path'); 3 | var File = require('../lib/file'); 4 | 5 | exports.initialize = function(app) { 6 | 7 | var models = app.dreamer.models; 8 | var storage_path = app.showcase.config.files.storage_path; 9 | 10 | app.post('/files', function(req, res) { 11 | 12 | var item_id = req.params.item_id; 13 | var file_keys = Object.keys(req.files); 14 | 15 | var files = []; 16 | 17 | async.forEach(file_keys, function(key, callback) { 18 | 19 | var upload = req.files[key]; 20 | 21 | if (upload.size == 0) return callback(); 22 | var content_type = upload.type || upload.headers['content-type'] || 'application/octet-stream'; 23 | 24 | File.create({ 25 | original_filename: upload.name, 26 | source_path: upload.path, 27 | size: upload.size, 28 | item_id: item_id, 29 | content_type: content_type, 30 | storage_path: storage_path 31 | 32 | }, function(err, file) { 33 | files.push(File.distill(file)); 34 | callback(); 35 | }); 36 | 37 | }, function(err) { 38 | if (err) return req.error(err); 39 | res.json(files); 40 | }); 41 | }); 42 | }; 43 | 44 | -------------------------------------------------------------------------------- /routes/item.js: -------------------------------------------------------------------------------- 1 | var querystring = require('querystring'); 2 | var async = require('async'); 3 | var Pagination = require('pagination').ItemPaginator; 4 | 5 | var Deferrals = require('../lib/deferrals'); 6 | var Collection = require('../lib/collection'); 7 | var Item = require('../lib/item'); 8 | var Sort = require('../lib/sort'); 9 | var clone = require('../lib/clone'); 10 | 11 | exports.initialize = function(app) { 12 | 13 | var models = app.dreamer.models; 14 | var workspaceLoader = app.showcase.middleware.workspaceLoader; 15 | var workspaceEditor = app.showcase.middleware.workspacePermission('editor'); 16 | 17 | app.del("/workspaces/:workspace_handle/collections/:collection_id/items/:item_id", workspaceLoader, workspaceEditor, function*(req, res) { 18 | 19 | var collection_id = req.params.collection_id; 20 | var item_id = req.params.item_id; 21 | var workspace = req.showcase.workspace; 22 | 23 | var item = yield Item.load({ id: item_id }); 24 | 25 | yield item.destroy(); 26 | 27 | res.redirect("/workspaces/" + workspace.handle + "/collections/" + collection_id + "/items"); 28 | }); 29 | 30 | app.get("/workspaces/:workspace_handle/collections/:collection_id/items", workspaceLoader, workspaceEditor, function*(req, res) { 31 | 32 | var collection_id = req.params.collection_id; 33 | var page = Number(req.query.page) || 1; 34 | var per_page = app.showcase.config.items_per_page || 100; 35 | var fields_count = app.showcase.config.item_summary_display_fields_count || 8; 36 | var sort = Sort.deserialize(req.query.sort); 37 | var search = req.query.q; 38 | 39 | var items = yield Item.all({ 40 | collection_id: collection_id, 41 | per_page: per_page, 42 | page: page, 43 | sort: sort, 44 | search: search 45 | }); 46 | 47 | var pagination = new Pagination({ 48 | rowsPerPage: per_page, 49 | totalResult: items.totalCount, 50 | current: page, 51 | prelink: req.url 52 | }); 53 | 54 | pagination.data = pagination.getPaginationData(); 55 | pagination.data.prelink = pagination.preparePreLink(pagination.data.prelink); 56 | 57 | items.forEach(function(item) { 58 | item = Item._preview(item, items.collection); 59 | Object.keys(item.data).forEach(function(key) { 60 | if (typeof item.data[key] == 'string') { 61 | if (item.data[key].length > 255) { 62 | item.data[key] = item.data[key].substring(0, 127) + '...'; 63 | } 64 | } 65 | }); 66 | }); 67 | 68 | var fields = items.collection.fields.slice(0, fields_count - 1); 69 | var sort_attribute = sort && sort.length ? sort[0] : {}; 70 | 71 | var column_fields = [].concat( 72 | [ { name: 'id', title: 'ID' } ], 73 | fields, 74 | [ { name: 'status', title: 'Status' } ] 75 | ); 76 | 77 | column_fields.forEach(function(f) { 78 | 79 | var query = clone(req.query); 80 | delete query.page; 81 | 82 | if (sort_attribute.field_name == f.name) { 83 | query.sort = f.name + (sort_attribute.order == 'desc' ? ':asc' : ':desc'); 84 | f.sort_url = '?' + querystring.stringify(query); 85 | f.sort_indicator = sort_attribute.order == 'desc' ? '▾' : '▴'; 86 | } else { 87 | query.sort = f.name; 88 | f.sort_url = '?' + querystring.stringify(query); 89 | } 90 | }); 91 | 92 | res.render("items.html", { 93 | items: items, 94 | collection: items.collection, 95 | fields: fields, 96 | pagination: pagination.data, 97 | column_fields: column_fields, 98 | q: search 99 | }); 100 | }); 101 | 102 | app.get("/workspaces/:workspace_handle/collections/:collection_id/items/:item_id/edit", workspaceLoader, workspaceEditor, function*(req, res) { 103 | 104 | var item_id = req.params.item_id; 105 | 106 | var collection, item, fields, item_data; 107 | 108 | var item = yield Item.load({ id: item_id }); 109 | var revisions = yield item.revisions(); 110 | 111 | var action = 'Edit'; 112 | var fields = item.collection.fields; 113 | 114 | res.render("item.html", { 115 | item: item, 116 | collection: item.collection, 117 | fields: item.collection.fields, 118 | action: action, 119 | revisions: revisions 120 | }); 121 | }); 122 | 123 | app.get("/workspaces/:workspace_handle/collections/:collection_id/items/:item_id/revisions/:revision_id", workspaceLoader, workspaceEditor, function*(req, res) { 124 | 125 | var item_id = req.params.item_id; 126 | var revision_id = req.params.revision_id; 127 | 128 | var item = yield Item.load({ id: item_id }); 129 | var revision = yield item.revision({ revision_id: revision_id }); 130 | 131 | res.render("revision.html", { revision: revision }); 132 | }); 133 | 134 | app.post("/workspaces/:workspace_handle/collections/:collection_id/items/:item_id/restore", workspaceLoader, workspaceEditor, function*(req, res) { 135 | 136 | var revision_id = req.body.revision_id; 137 | var workspace = req.showcase.workspace; 138 | var item_id = req.params.item_id; 139 | var user_id = req.session.user_id; 140 | 141 | yield Item.restore({ 142 | id: item_id, 143 | user_id: user_id, 144 | revision_id: revision_id, 145 | }); 146 | 147 | req.flash('info', "Restored prior version for item #" + item_id); 148 | res.redirect('/workspaces/' + workspace.handle + '/collections/' + req.params.collection_id + '/items/' + item_id + '/edit'); 149 | }); 150 | 151 | app.post("/workspaces/:workspace_handle/collections/:collection_id/items/:item_id/edit", workspaceLoader, workspaceEditor, function*(req, res) { 152 | 153 | var collection_id = req.params.collection_id; 154 | var item_id = req.params.item_id; 155 | var status = req.body._status; 156 | var workspace = req.showcase.workspace; 157 | var user_id = req.session.user_id; 158 | 159 | var item = yield Item.load({ id: item_id }); 160 | 161 | item.update({ 162 | status: status, 163 | data: req.body, 164 | user_id: user_id, 165 | }); 166 | 167 | var errors = item.validate(); 168 | 169 | if (errors) { 170 | 171 | var action = "Edit"; 172 | 173 | return res.render("item.html", { 174 | item: item, 175 | errors: errors, 176 | collection: item.collection, 177 | fields: item.collection.fields, 178 | action: action 179 | }); 180 | } 181 | 182 | yield item.save({ user_id: user_id }); 183 | 184 | req.flash('info', 'Saved item #' + item_id); 185 | res.redirect("/workspaces/" + workspace.handle + "/collections/" + collection_id + "/items"); 186 | }); 187 | 188 | app.get("/workspaces/:workspace_handle/collections/:collection_id/items/new", workspaceLoader, workspaceEditor, function*(req, res) { 189 | 190 | var collection_id = req.params.collection_id; 191 | 192 | var collection = yield Collection.load({ id: collection_id }); 193 | 194 | res.render("item.html", { 195 | collection: collection, 196 | fields: collection.fields, 197 | action: 'New' 198 | }); 199 | }); 200 | 201 | app.post("/workspaces/:workspace_handle/collections/:collection_id/items/new", workspaceLoader, workspaceEditor, function*(req, res) { 202 | 203 | var collection_id = req.params.collection_id; 204 | var status = req.body._status; 205 | var workspace = req.showcase.workspace; 206 | var user_id = req.session.user_id; 207 | 208 | var item = yield Item.build({ 209 | collection_id: collection_id, 210 | status: status, 211 | data: req.body, 212 | user_id: user_id, 213 | }); 214 | 215 | var errors = item.validate(); 216 | 217 | if (errors) { 218 | 219 | return res.render("item.html", { 220 | item: item, 221 | errors: errors, 222 | collection: item.collection, 223 | fields: item.collection.fields, 224 | action: "New" 225 | }); 226 | } 227 | 228 | yield item.save({ user_id: user_id }); 229 | 230 | req.flash('info', "Created item"); 231 | res.redirect("/workspaces/" + workspace.handle + "/collections/" + collection_id + "/items"); 232 | }); 233 | }; 234 | -------------------------------------------------------------------------------- /routes/login.js: -------------------------------------------------------------------------------- 1 | var passport = require('passport'); 2 | var gx = require('gx'); 3 | 4 | exports.initialize = function(app) { 5 | 6 | var config = app.showcase.config; 7 | var strategy = config.auth.passport_strategy; 8 | 9 | var config_superusers = {}; 10 | (config.auth.superusers || []).forEach(function(username) { 11 | config_superusers[username] = true; 12 | }); 13 | 14 | app.get('/admin/login', function(req, res, next) { 15 | if (strategy._callbackURL) { 16 | // kick off the OAuth dance if there's a callback URL 17 | passport.authenticate(strategy.name).call(null, req, res, next); 18 | } else { 19 | // otherwise present our login page 20 | res.render("login.html"); 21 | } 22 | }); 23 | 24 | app.post('/admin/login', function*(req, res, next) { 25 | 26 | // passport requires a truthy password 27 | req.body.password = 'password' in req.body ? req.body.password : 'password'; 28 | 29 | var handler = function(err, user, info) { 30 | 31 | if (err) return req.error(err); 32 | if (!user) { 33 | req.flash('danger', info.message); 34 | return res.redirect('/admin/login'); 35 | } 36 | req.session.passport.user = user; 37 | res.redirect('/admin/login/callback'); 38 | }; 39 | 40 | passport.authenticate(strategy.name, handler).call(null, req, res, next); 41 | }); 42 | 43 | app.get('/admin/login/callback', function(req, res, next) { 44 | 45 | var handler = function(err, user, info) { 46 | 47 | if (!user) { 48 | req.flash('danger', "Failed logging in authenticated user: " + (info && info.message ? info.message : '')); 49 | return res.redirect('/admin/login'); 50 | } 51 | 52 | req.logIn(user, function() { 53 | 54 | req.session.username = user.username; 55 | req.session.user_id = user.id; 56 | req.session.is_superuser = config_superusers[user.username] || user.is_superuser; 57 | 58 | res.redirect('/workspaces'); 59 | }); 60 | }; 61 | 62 | if (req.session.passport.user) { 63 | // if we're already authenticated call the handler directly 64 | handler(null, req.session.passport.user); 65 | } else { 66 | // otherwise authenticate via passort 67 | passport.authenticate(strategy.name, handler).call(null, req, res, next); 68 | } 69 | }); 70 | 71 | app.get('/admin/logout', function(req, res) { 72 | req.logout(); 73 | req.session = null; 74 | res.redirect('/'); 75 | }); 76 | }; 77 | 78 | -------------------------------------------------------------------------------- /routes/setup.js: -------------------------------------------------------------------------------- 1 | var gx = require('gx'); 2 | 3 | exports.initialize = function(app) { 4 | 5 | var models = app.dreamer.models; 6 | 7 | app.get('/admin/setup', function(req, res) { 8 | res.render("setup.html"); 9 | }); 10 | 11 | app.post('/admin/setup', function*(req, res) { 12 | 13 | var count = yield models.users 14 | .count({ where: "username != 'api'" }) 15 | .complete(gx.resume); 16 | 17 | if (count) { 18 | req.flash('danger', 'Already set up'); 19 | return res.redirect('/admin/users'); 20 | } 21 | 22 | yield models.users 23 | .create({ username: req.body.username, is_superuser: 1 }) 24 | .complete(gx.resume); 25 | 26 | req.flash('info', 'Created superuser. Time to log in now...'); 27 | res.redirect('/admin/login'); 28 | }); 29 | 30 | app.get('/admin/error/schema', function(req, res) { 31 | res.render("error_schema.html"); 32 | }); 33 | 34 | app.get('/admin/error/db', function(req, res) { 35 | res.render("error_db.html"); 36 | }); 37 | }; 38 | 39 | -------------------------------------------------------------------------------- /routes/users.js: -------------------------------------------------------------------------------- 1 | var async = require('async'); 2 | var gx = require('gx'); 3 | var Permission = require('../lib/permission'); 4 | var config = require('config'); 5 | 6 | exports.initialize = function(app) { 7 | 8 | var models = app.dreamer.models; 9 | var requireSuperuser = app.showcase.middleware.requireSuperuser; 10 | 11 | app.get("/admin/users", requireSuperuser, function*(req, res) { 12 | 13 | var users = yield models.users 14 | .findAll({}) 15 | .complete(gx.resume); 16 | 17 | res.render("users.html", { users: users }); 18 | }); 19 | 20 | app.get("/admin/users/new", requireSuperuser, function(req, res) { 21 | 22 | res.render("user.html", { action: 'New' }); 23 | }); 24 | 25 | app.get("/admin/users/:user_id/edit", requireSuperuser, function*(req, res) { 26 | 27 | var user_id = req.params.user_id; 28 | var user, workspaces, permissions; 29 | 30 | models.users 31 | .find({ where: { id: user_id } }) 32 | .complete(gx.resume); 33 | 34 | models.workspaces 35 | .findAll({}) 36 | .complete(gx.resume); 37 | 38 | models.workspace_user_permissions 39 | .findAll({ where: { user_id: user_id } }) 40 | .complete(gx.resume); 41 | 42 | var user = yield null; 43 | var workspaces = yield null; 44 | var permissions = yield null; 45 | 46 | var is_config_superuser = (config.auth.superusers || []) 47 | .filter(function(username) { return username === user.username }) 48 | .length; 49 | 50 | workspaces.forEach(function(workspace) { 51 | 52 | var workspace_permission = permissions 53 | .filter(function(p) { return p.workspace_handle == workspace.handle; }) 54 | .shift(); 55 | 56 | if (workspace_permission) { 57 | workspace.permission = Permission.name(workspace_permission.permission_id); 58 | } 59 | }); 60 | 61 | res.render("user.html", { 62 | action: 'Edit', 63 | user: user, 64 | workspaces: workspaces, 65 | is_config_superuser: is_config_superuser 66 | }); 67 | }); 68 | 69 | app.post("/admin/users/:user_id/edit", requireSuperuser, function*(req, res) { 70 | 71 | var user_id = req.params.user_id; 72 | var username = req.body.username; 73 | var is_superuser = Number(req.body.is_superuser) || 0; 74 | 75 | var user = yield models.users 76 | .find({ where: { id: user_id } }) 77 | .complete(gx.resume); 78 | 79 | user.username = username; 80 | user.is_superuser = is_superuser; 81 | 82 | var errors = user.validate(); 83 | 84 | if (errors) { 85 | req.flash('danger', 'There was an error: ' + JSON.stringify(errors)); 86 | return res.redirect("/admin/users"); 87 | } 88 | 89 | var workspace_permissions = []; 90 | 91 | var workspace_handles = arrayify(req.body.workspace_handle); 92 | var permissions = arrayify(req.body.permission); 93 | 94 | workspace_handles.forEach(function(handle, index) { 95 | permission = { 96 | user_id: user.id, 97 | permission_id: Permission.id(permissions[index]), 98 | workspace_handle: handle 99 | }; 100 | workspace_permissions.push(permission); 101 | }); 102 | 103 | yield user.save().complete(gx.resume); 104 | req.flash('info', 'Saved user'); 105 | 106 | var query = 'delete from workspace_user_permissions where user_id = ?'; 107 | 108 | yield app.dreamer.db 109 | .query(query, null, {raw: true}, [user_id]) 110 | .complete(gx.resume); 111 | 112 | async.forEach(workspace_permissions, function(permission, callback) { 113 | 114 | models.workspace_user_permissions 115 | .create(permission) 116 | .error(req.error) 117 | .success(callback); 118 | 119 | }, function() { 120 | res.redirect("/admin/users"); 121 | }); 122 | }); 123 | 124 | app.post("/admin/users/new", requireSuperuser, function*(req, res) { 125 | 126 | var is_superuser = Number(req.body.is_superuser) || 0; 127 | var username = req.body.username; 128 | 129 | var user = models.users.build({ 130 | username: username, 131 | is_superuser: is_superuser 132 | }); 133 | 134 | var errors = user.validate(); 135 | 136 | if (!errors) { 137 | yield user.save().complete(gx.resume); 138 | req.flash('info', 'Created new user'); 139 | res.redirect("/admin/users"); 140 | } else { 141 | req.flash('error', 'There was an error'); 142 | res.redirect("/admin/users/new"); 143 | } 144 | }); 145 | }; 146 | 147 | function arrayify(data) { 148 | if (Array.isArray(data)) return data; 149 | else if (data === undefined) return []; 150 | else return [ data ]; 151 | }; 152 | -------------------------------------------------------------------------------- /routes/workspaces.js: -------------------------------------------------------------------------------- 1 | var gx = require('gx'); 2 | 3 | exports.initialize = function(app) { 4 | 5 | var models = app.dreamer.models; 6 | var workspaceAdmin = app.showcase.middleware.workspacePermission('administrator'); 7 | var workspaceLoader = app.showcase.middleware.workspaceLoader; 8 | var requireSuperuser = app.showcase.middleware.requireSuperuser; 9 | 10 | var fields = ['title', 'handle', 'description']; 11 | 12 | app.get("/workspaces", function*(req, res) { 13 | 14 | var workspaces = yield models.workspaces 15 | .findAll({}) 16 | .complete(gx.resume); 17 | 18 | res.render("workspaces.html", { workspaces: workspaces }); 19 | }); 20 | 21 | app.get("/workspaces/new", requireSuperuser, function(req, res) { 22 | 23 | var breadcrumbs = [ { text: 'New' } ]; 24 | 25 | res.render("workspace.html", { 26 | breadcrumbs: breadcrumbs 27 | }); 28 | }); 29 | 30 | app.get("/workspaces/:workspace_handle/edit", workspaceLoader, workspaceAdmin, function(req, res) { 31 | 32 | var workspace = req.showcase.workspace; 33 | 34 | var breadcrumbs = [ 35 | { href: '/workspaces/' + workspace.handle + '/collections', text: workspace.title } 36 | ]; 37 | 38 | res.render("workspace.html", { 39 | breadcrumbs: breadcrumbs, 40 | workspace: workspace 41 | }); 42 | }); 43 | 44 | app.post("/workspaces/new", requireSuperuser, function*(req, res) { 45 | 46 | var workspace = models.workspaces.build({ 47 | title: req.body.title, 48 | handle: req.body.handle, 49 | description: req.body.description 50 | }); 51 | 52 | var errors = workspace.validate(); 53 | 54 | fields.forEach(function(field) { 55 | if (workspace[field]) return; 56 | errors = errors || {}; 57 | errors[field] = [field + ' is required']; 58 | }); 59 | 60 | if (!errors) { 61 | yield workspace.save().complete(gx.resume); 62 | req.flash('info', 'Saved new workspace'); 63 | res.redirect("/workspaces"); 64 | } else { 65 | req.flash('danger', 'There was an error: ' + JSON.stringify(errors)); 66 | res.redirect("/workspaces/new"); 67 | } 68 | }); 69 | 70 | app.delete("/workspaces/:workspace_handle", workspaceLoader, workspaceAdmin, function*(req, res) { 71 | 72 | var workspace = req.showcase.workspace; 73 | yield workspace.destroy().complete(gx.resume); 74 | req.flash('info', 'Deleted workspace'); 75 | res.redirect('/workspaces'); 76 | }); 77 | 78 | app.post("/workspaces/:workspace_handle/edit", workspaceLoader, workspaceAdmin, function*(req, res) { 79 | 80 | var workspace = req.showcase.workspace; 81 | 82 | fields.forEach(function(field) { 83 | workspace[field] = req.body[field]; 84 | }); 85 | 86 | var errors = workspace.validate(); 87 | 88 | fields.forEach(function(field) { 89 | if (workspace[field]) return; 90 | errors = errors || {}; 91 | errors[field] = [field + ' is required']; 92 | }); 93 | 94 | if (!errors) { 95 | yield workspace.save().complete(gx.resume); 96 | req.flash('info', 'Saved workspace'); 97 | res.redirect("/workspaces"); 98 | } else { 99 | req.flash('danger', 'There was an error: ' + JSON.stringify(errors)); 100 | res.redirect("/workspaces/" + workspace.handle); 101 | } 102 | 103 | }); 104 | }; 105 | -------------------------------------------------------------------------------- /spec/fixtures.md: -------------------------------------------------------------------------------- 1 | ### Permissions 2 | 3 | | name | 4 | |---------------| 5 | | administrator | 6 | | editor | 7 | | viewer | 8 | 9 | ### Statuses 10 | 11 | | name | 12 | |-----------| 13 | | draft | 14 | | published | 15 | | deleted | 16 | 17 | ### Users 18 | 19 | | username | is_superuser | 20 | |----------|--------------| 21 | | api | 1 | 22 | 23 | -------------------------------------------------------------------------------- /spec/resources.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dchester/showcase/e6819fe96a2a9e3cd5d7fd5e0f016994000cb153/spec/resources.md -------------------------------------------------------------------------------- /spec/schema.md: -------------------------------------------------------------------------------- 1 | ### Collections 2 | 3 | Different types of entities or models 4 | 5 | ``` 6 | - title 7 | - description 8 | - name 9 | - workspace_handle fk=workspaces.handle 10 | ``` 11 | 12 | ### Collection Fields 13 | 14 | Possible fields or attributes an entity might have 15 | 16 | ``` 17 | - collection_id 18 | - title 19 | - name 20 | - data_type 21 | - description 22 | - is_required boolean 23 | - index integer 24 | - meta text 25 | ``` 26 | 27 | ### Items 28 | 29 | Instances of entities; records 30 | 31 | ``` 32 | - collection_id 33 | - status_id 34 | - key nullable 35 | - create_time 36 | - update_time 37 | ``` 38 | 39 | ### Statuses 40 | 41 | ``` 42 | - name 43 | ``` 44 | 45 | ### Item Data 46 | 47 | Data comprising the item record 48 | 49 | ``` 50 | - item_id 51 | - data text 52 | - content_type 53 | - user_id 54 | ``` 55 | 56 | ### Item Data Revisions 57 | 58 | Data comprising the item record 59 | 60 | ``` 61 | - item_id 62 | - data text 63 | - content_type 64 | - create_time default=now 65 | - user_id 66 | ``` 67 | 68 | ### Files 69 | 70 | Uploaded files associated with items 71 | 72 | ``` 73 | - item_id nullable 74 | - path 75 | - url 76 | - original_filename nullable 77 | - description nullable 78 | - content_type 79 | - meta_json text,nullable 80 | - size integer 81 | ``` 82 | 83 | ### Users 84 | 85 | ``` 86 | - username unique 87 | - is_superuser boolean 88 | - external_user_id nullable 89 | ``` 90 | ### Permissions 91 | 92 | ``` 93 | - name 94 | ``` 95 | 96 | ### Workspace User Permissions 97 | 98 | ``` 99 | - user_id 100 | - workspace_handle 101 | - permission_id 102 | ``` 103 | 104 | ### Workspaces 105 | 106 | ``` 107 | - title 108 | - handle 109 | - description 110 | ``` 111 | 112 | 113 | -------------------------------------------------------------------------------- /tests/collection.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var gx = require('gx'); 3 | 4 | var suite = require('./lib'); 5 | var showcase = require('../index'); 6 | var config = require('./lib/config'); 7 | 8 | showcase.initialize(config.showcase); 9 | 10 | var dream = require('dreamer').instance; 11 | var error = function(e) { console.warn(e) }; 12 | var Collection = require('../lib/collection.js'); 13 | 14 | exports.setUp = suite.setUp; 15 | exports.tearDown = suite.tearDown; 16 | 17 | exports.create = function(test) { 18 | 19 | gx(function*() { 20 | 21 | var collection = yield Collection.create({ 22 | title: 'Books', 23 | description: 'Books for reading', 24 | name: 'books', 25 | workspace_handle: 'test', 26 | fields: config.fixtures.book_fields, 27 | }); 28 | 29 | test.equal(collection.title, 'Books'); 30 | test.equal(collection.description, 'Books for reading'); 31 | test.equal(collection.workspace_handle, 'test'); 32 | test.done(); 33 | 34 | }); 35 | }; 36 | 37 | exports.load = function(test) { 38 | 39 | gx(function*() { 40 | 41 | var collection = yield Collection.create({ 42 | title: 'Books', 43 | description: 'Books for reading', 44 | name: 'books', 45 | workspace_handle: 'test', 46 | fields: config.fixtures.book_fields, 47 | }); 48 | 49 | collection = yield Collection.load({ id: collection.id }); 50 | 51 | test.equal(collection.title, 'Books'); 52 | test.equal(collection.description, 'Books for reading'); 53 | test.equal(collection.workspace_handle, 'test'); 54 | test.done(); 55 | }); 56 | }; 57 | 58 | exports.update = function(test) { 59 | 60 | gx(function*() { 61 | 62 | var collection = yield Collection.create({ 63 | title: 'Books', 64 | description: 'Books for reading', 65 | name: 'books', 66 | workspace_handle: 'test', 67 | fields: config.fixtures.book_fields 68 | }); 69 | 70 | collection = yield Collection.load({ id: collection.id }); 71 | 72 | yield collection.update({ title: "Wonderful Books" }); 73 | collection = yield Collection.load({ id: collection.id }); 74 | 75 | test.equal(collection.title, "Wonderful Books"); 76 | test.done(); 77 | }); 78 | }; 79 | 80 | exports.all = function(test) { 81 | 82 | gx(function*() { 83 | 84 | var collection = yield Collection.create({ 85 | title: 'Books', 86 | description: 'Books for reading', 87 | name: 'books', 88 | workspace_handle: 'test', 89 | fields: config.fixtures.book_fields, 90 | }); 91 | 92 | var collections = yield Collection.all({ workspace_handle: 'test' }); 93 | 94 | test.equal(collections.length, 1); 95 | test.done(); 96 | }); 97 | }; 98 | 99 | exports.destroy = function(test) { 100 | 101 | gx(function*() { 102 | 103 | var collection = yield Collection.create({ 104 | title: 'Books', 105 | description: 'Books for reading', 106 | name: 'books', 107 | workspace_handle: 'test', 108 | fields: config.fixtures.book_fields, 109 | }); 110 | 111 | var collection_id = collection.id; 112 | 113 | yield collection.destroy(); 114 | collection = yield Collection.load({ id: collection_id }); 115 | 116 | test.equal(collection, undefined); 117 | test.done(); 118 | }); 119 | }; 120 | 121 | -------------------------------------------------------------------------------- /tests/escape.js: -------------------------------------------------------------------------------- 1 | var escape = require('../lib/escape'); 2 | 3 | exports.html = function(test) { 4 | var output = escape.html('home'); 5 | test.equal(output, '<a href="/">home</a>'); 6 | test.done(); 7 | }; 8 | 9 | exports.plain = function(test) { 10 | var output = escape.html('plain text'); 11 | test.equal(output, 'plain text'); 12 | test.done(); 13 | }; 14 | 15 | -------------------------------------------------------------------------------- /tests/file.js: -------------------------------------------------------------------------------- 1 | var gx = require('gx'); 2 | var mkdirp = require('mkdirp'); 3 | var suite = require('./lib'); 4 | var showcase = require('../index'); 5 | var config = require('./lib/config'); 6 | var fs = require('fs'); 7 | 8 | showcase.initialize(config.showcase); 9 | 10 | var dream = require('dreamer').instance; 11 | var error = function(e) { console.warn(e) }; 12 | 13 | var File = require('../lib/file.js'); 14 | 15 | exports.tearDown = suite.tearDown; 16 | 17 | exports.setUp = function(callback) { 18 | 19 | suite.setUp(function() { 20 | fs.writeFileSync('/tmp/showcase-file1', 'contents1'); 21 | fs.writeFileSync('/tmp/showcase-file2', 'contents2'); 22 | mkdirp.sync('/var/tmp/showcase-test/files'); 23 | callback(); 24 | }); 25 | }; 26 | 27 | exports.create = function(test) { 28 | 29 | gx(function*() { 30 | 31 | var file = yield File.create({ 32 | source_path: '/tmp/showcase-file1', 33 | size: 100, 34 | item_id: 3, 35 | content_type: 'text/plain', 36 | original_filename: 'file.txt', 37 | storage_path: '/var/tmp/showcase-test' 38 | }); 39 | 40 | test.equal(file.item_id, 3); 41 | test.equal(file.original_filename, 'file.txt'); 42 | test.equal(file.content_type, 'text/plain'); 43 | test.equal(file.size, 100); 44 | 45 | test.done(); 46 | }); 47 | }; 48 | 49 | exports.load = function(test) { 50 | 51 | gx(function*() { 52 | 53 | var file = yield File.create({ 54 | source_path: '/tmp/showcase-file1', 55 | size: 100, 56 | item_id: 7, 57 | content_type: 'text/plain', 58 | original_filename: 'file.txt', 59 | storage_path: config.showcase.files.storage_path 60 | }); 61 | 62 | var loaded_file = yield File.load({ id: file.id }); 63 | 64 | test.equal(loaded_file.id, 1); 65 | test.equal(loaded_file.item_id, 7); 66 | test.equal(loaded_file.original_filename, 'file.txt'); 67 | test.equal(loaded_file.content_type, 'text/plain'); 68 | test.equal(loaded_file.size, 100); 69 | 70 | test.done(); 71 | }); 72 | }; 73 | 74 | exports.retrieve = function(test) { 75 | 76 | gx(function*() { 77 | 78 | var file = yield File.create({ 79 | source_path: '/tmp/showcase-file1', 80 | size: 100, 81 | item_id: 7, 82 | content_type: 'text/plain', 83 | original_filename: 'file.txt', 84 | storage_path: config.showcase.files.storage_path 85 | }); 86 | 87 | var stream = file.retrieve(); 88 | var buffer = ''; 89 | 90 | stream.on('data', function(chunk) { 91 | buffer += chunk; 92 | }); 93 | 94 | stream.on('end', function() { 95 | test.equal(buffer, 'contents1'); 96 | test.done(); 97 | }); 98 | }); 99 | }; 100 | 101 | 102 | -------------------------------------------------------------------------------- /tests/lib/config.js: -------------------------------------------------------------------------------- 1 | exports.fixtures = { 2 | book_fields: [ 3 | { 4 | title: 'Title', 5 | name: 'title', 6 | description: 'title', 7 | data_type: 'string', 8 | is_required: 1, 9 | index: 0, 10 | meta: '' 11 | }, { 12 | title: 'Author', 13 | name: 'author', 14 | description: 'author', 15 | data_type: 'string', 16 | is_required: 1, 17 | index: 1, 18 | meta: '' 19 | }, { 20 | title: 'ISBN', 21 | name: 'isbn', 22 | description: 'isbn', 23 | data_type: 'number', 24 | is_required: 1, 25 | index: 2, 26 | meta: '' 27 | }, { 28 | title: 'Public Domain', 29 | name: 'is_public_domain', 30 | description: 'Is this book in the public domain?', 31 | data_type: 'checkbox', 32 | is_required: 0, 33 | index: 3, 34 | meta: '' 35 | }, { 36 | title: 'Book Image', 37 | name: 'book_image', 38 | description: 'A cover image for the book', 39 | data_type: 'image', 40 | is_required: 0, 41 | index: 4, 42 | meta: '' 43 | } 44 | ] 45 | }; 46 | 47 | exports.showcase = { 48 | "database": { 49 | "dialect": "sqlite", 50 | "storage": '/var/tmp/showcase-tester.sqlite', 51 | "database": "cms", 52 | "logging": false 53 | }, 54 | "files": { 55 | "tmp_path": "/var/tmp", 56 | "storage_path": "/var/tmp" 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /tests/lib/index.js: -------------------------------------------------------------------------------- 1 | var showcase = require('../../index'); 2 | var config = require('../lib/config'); 3 | var gx = require('gx'); 4 | 5 | showcase.initialize(config.showcase); 6 | 7 | var dreamer = require('dreamer'); 8 | var dream = dreamer.instance; 9 | var error = function(e) { console.warn(e) }; 10 | var models = dream.models; 11 | var Status = require('../../lib/status'); 12 | var Permission = require('../../lib/permission'); 13 | 14 | exports.setUp = function(callback) { 15 | 16 | gx(function*() { 17 | yield dream.db.drop().complete(gx.resume); 18 | yield dream.db.sync().complete(gx.resume); 19 | yield dreamer.Fixtures.sync(dream, dream.fixtures, gx.resume); 20 | yield Status.load(); 21 | yield Permission.load(); 22 | var plugins = require('../../lib/plugins'); 23 | var image = require('../../plugins/image'); 24 | plugins.register('field', image); 25 | callback(); 26 | }); 27 | }; 28 | 29 | exports.tearDown = function(callback) { 30 | dream.db.drop().success(function() { 31 | callback(); 32 | }); 33 | }; 34 | 35 | process.on('uncaughtException', function(err) { 36 | console.warn("ERROR: " + err); 37 | throw new Error(err); 38 | }); 39 | 40 | -------------------------------------------------------------------------------- /tests/permission.js: -------------------------------------------------------------------------------- 1 | var gx = require('gx'); 2 | var suite = require('./lib'); 3 | var Permission = require('../lib/permission'); 4 | 5 | exports.setUp = suite.setUp; 6 | exports.tearDown = suite.tearDown; 7 | 8 | exports.id = function(test) { 9 | 10 | gx(function*() { 11 | test.equal(Permission.id('administrator'), 1); 12 | test.equal(Permission.id('editor'), 2); 13 | test.equal(Permission.id('viewer'), 3); 14 | test.done(); 15 | }); 16 | }; 17 | 18 | exports.name = function(test) { 19 | 20 | gx(function*() { 21 | test.equal(Permission.name(1), 'administrator'); 22 | test.equal(Permission.name(2), 'editor'); 23 | test.equal(Permission.name(3), 'viewer'); 24 | test.done(); 25 | }); 26 | }; 27 | 28 | -------------------------------------------------------------------------------- /tests/selenium/.rock.yml: -------------------------------------------------------------------------------- 1 | runtime: ruby20 2 | run: rspec test.rb 3 | -------------------------------------------------------------------------------- /tests/selenium/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'selenium-webdriver' 4 | gem 'rspec' 5 | gem 'page-object' 6 | -------------------------------------------------------------------------------- /tests/selenium/Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | childprocess (0.3.9) 5 | ffi (~> 1.0, >= 1.0.11) 6 | data_magic (0.16.1) 7 | faker (>= 1.1.2) 8 | yml_reader (>= 0.2) 9 | diff-lcs (1.2.5) 10 | faker (1.2.0) 11 | i18n (~> 0.5) 12 | ffi (1.9.3) 13 | i18n (0.6.5) 14 | multi_json (1.8.2) 15 | page-object (0.9.3) 16 | page_navigation (>= 0.8) 17 | selenium-webdriver (>= 2.37.0) 18 | watir-webdriver (>= 0.6.4) 19 | page_navigation (0.9) 20 | data_magic (>= 0.14) 21 | rspec (2.14.1) 22 | rspec-core (~> 2.14.0) 23 | rspec-expectations (~> 2.14.0) 24 | rspec-mocks (~> 2.14.0) 25 | rspec-core (2.14.7) 26 | rspec-expectations (2.14.4) 27 | diff-lcs (>= 1.1.3, < 2.0) 28 | rspec-mocks (2.14.4) 29 | rubyzip (1.0.0) 30 | selenium-webdriver (2.37.0) 31 | childprocess (>= 0.2.5) 32 | multi_json (~> 1.0) 33 | rubyzip (~> 1.0.0) 34 | websocket (~> 1.0.4) 35 | watir-webdriver (0.6.4) 36 | selenium-webdriver (>= 2.18.0) 37 | websocket (1.0.7) 38 | yml_reader (0.2) 39 | 40 | PLATFORMS 41 | ruby 42 | 43 | DEPENDENCIES 44 | page-object 45 | rspec 46 | selenium-webdriver 47 | -------------------------------------------------------------------------------- /tests/selenium/test.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'selenium-webdriver' 3 | require 'page-object' 4 | 5 | class SignUpPage 6 | 7 | include PageObject 8 | 9 | text_field(:username, :name => 'username') 10 | button(:sign_up, :class => 'btn-primary') 11 | 12 | def sign_up_with (username) 13 | self.username = username 14 | self.sign_up 15 | end 16 | end 17 | 18 | class LoginPage 19 | 20 | include PageObject 21 | 22 | text_field(:username, :name => 'username') 23 | button(:log_in, :class => 'btn-primary') 24 | 25 | def log_in_with (username) 26 | self.username = username 27 | self.log_in 28 | end 29 | end 30 | 31 | class WorkspacesPage 32 | 33 | include PageObject 34 | 35 | end 36 | 37 | class WorkspacePage 38 | 39 | include PageObject 40 | 41 | text_field(:title, :name => 'title') 42 | text_field(:handle, :name => 'handle') 43 | text_field(:description, :name => 'description') 44 | 45 | button(:create, :class => 'btn-primary') 46 | 47 | def initialize_page 48 | self.navigate_to("http://localhost:3000/workspaces") 49 | end 50 | 51 | def create_with (title, handle, description) 52 | 53 | self.title = title 54 | self.handle = handle 55 | self.description = description 56 | self.create 57 | end 58 | end 59 | 60 | describe "test1" do 61 | 62 | driver = Selenium::WebDriver.for :firefox 63 | p = WorkspacePage.new(driver) 64 | 65 | =begin 66 | driver.get "http://localhost:3000/admin/login" 67 | 68 | wait = Selenium::WebDriver::Wait.new(:timeout => 10) 69 | wait.until { driver.title.downcase.start_with? "setup" } 70 | 71 | # sign up 72 | driver.find_element(:name => 'username').send_keys('admin') 73 | driver.find_element(:css => '.setup-form button').click 74 | 75 | # sign in 76 | driver.find_element(:name => 'username').send_keys('admin') 77 | driver.find_element(:css => '.login-form button').click 78 | 79 | # create a workspace 80 | driver.find_element(:link_text => 'New Workspace').click 81 | wait.until { driver.find_element(:name => 'title') } 82 | 83 | workspace_page = WorkspacePage.new(driver) 84 | workspace_page.create_with('test_workspace', 'test_workspace', 'test_workspace'); 85 | 86 | # make a collection 87 | wait.until { driver.find_element(:link_text => 'test_workspace') } 88 | driver.find_element(:link_text => 'test_workspace').click 89 | 90 | wait.until { driver.find_element(:link_text => 'New Collection') } 91 | driver.find_element(:link_text => 'New Collection').click 92 | 93 | driver.find_element(:name => 'title').send_keys("books of the month") 94 | driver.find_element(:name => 'description').send_keys("books of the month") 95 | 96 | driver.find_element(:id => 'new_field').click 97 | wait.until { driver.find_element(:name => 'field_title') } 98 | 99 | driver.find_element(:name => 'field_title').send_keys('book title') 100 | driver.find_element(:name => 'field_description').send_keys('title of the book') 101 | 102 | driver.find_element(:class => 'save_fields').click 103 | driver.find_element(:id => 'save_collection').click 104 | =end 105 | 106 | end 107 | 108 | =begin 109 | 110 | class Collection 111 | 112 | attr_reader :title 113 | 114 | def initialize (driver, title, description, fields) 115 | 116 | number = Random.rand(10000) 117 | title += " " + number.to_s 118 | @title = title; 119 | 120 | element = driver.find_element :partial_link_text => 'New Collection' 121 | element.click 122 | 123 | wait = Selenium::WebDriver::Wait.new(:timeout => 10) 124 | wait.until { driver.title.downcase.start_with? "collection" } 125 | 126 | driver.find_element(:name => 'title').send_keys(title) 127 | driver.find_element(:name => 'description').send_keys(description) 128 | 129 | driver.find_element(:id => 'new_field').click 130 | wait.until { driver.find_element(:name => 'field_title') } 131 | 132 | driver.find_element(:name => 'field_title').send_keys(fields[:title]) 133 | driver.find_element(:name => 'field_description').send_keys(fields[:description]) 134 | 135 | driver.find_element(:class => 'save_fields').click 136 | driver.find_element(:id => 'save_collection').click 137 | 138 | end 139 | end 140 | 141 | describe "collections" do 142 | 143 | it "should create a new collection" do 144 | 145 | driver = Selenium::WebDriver.for :firefox 146 | driver.get "http://localhost:3000/workspaces" 147 | 148 | collection = Collection.new(driver, 'Books', 'Featured books of the month', { :title => 'title' }) 149 | 150 | alert = driver.find_element(:class => 'alert-success') 151 | alert.text.should == "Created new collection"; 152 | 153 | content = driver.find_element(:class => 'content') 154 | content.text.should match /#{collection.title}/ 155 | 156 | driver.find_element(:class => 'delete').submit 157 | driver.quit 158 | end 159 | 160 | it "should update a collection" do 161 | 162 | driver = Selenium::WebDriver.for :firefox 163 | driver.get "http://localhost:3000/admin/entities" 164 | 165 | collection = Collection.new(driver, 'Books', 'Featured books of the month', { :title => 'title' }) 166 | 167 | driver.find_element(:partial_link_text => 'edit').click 168 | 169 | wait = Selenium::WebDriver::Wait.new(:timeout => 10) 170 | wait.until { driver.title == "Collection" } 171 | 172 | number = Random.rand(10000) 173 | title = collection.title + " " + number.to_s 174 | 175 | input = driver.find_element(:name => 'title') 176 | input.clear() 177 | input.send_keys(title) 178 | input.submit() 179 | 180 | driver.get "http://localhost:3000/admin/entities" 181 | 182 | content = driver.find_element(:class => 'content') 183 | content.text.should match /#{collection.title}/ 184 | 185 | driver.find_element(:class => 'delete').submit 186 | driver.quit 187 | end 188 | end 189 | 190 | describe "items" do 191 | 192 | it "should create new items" do 193 | 194 | driver = Selenium::WebDriver.for :firefox 195 | driver.get "http://localhost:3000/admin/entities" 196 | 197 | collection = Collection.new(driver, 'Books', 'Featured books of the month', { :title => 'title' }) 198 | 199 | driver.find_element(:partial_link_text => collection.title).click 200 | 201 | wait = Selenium::WebDriver::Wait.new(:timeout => 10) 202 | wait.until { driver.title == "Items" } 203 | 204 | driver.find_element(:partial_link_text => 'Create New').click 205 | wait.until { driver.title == "Item" } 206 | 207 | driver.find_element(:name => 'title').send_keys('A Beautiful Title') 208 | driver.find_element(:class => 'btn-primary').submit 209 | 210 | wait.until { driver.title == "Items" } 211 | 212 | content = driver.find_element(:class => 'content') 213 | content.text.should match /A Beautiful Title/ 214 | 215 | driver.find_element(:class => 'delete').submit 216 | 217 | driver.get "http://localhost:3000/admin/entities" 218 | driver.find_element(:class => 'delete').submit 219 | driver.quit 220 | 221 | end 222 | end 223 | 224 | =end 225 | 226 | -------------------------------------------------------------------------------- /tests/sort.js: -------------------------------------------------------------------------------- 1 | var Sort = require('../lib/sort.js'); 2 | 3 | exports.serialize = function(test) { 4 | 5 | var serialized_sort = Sort.serialize([ { field_name: 'title' } ]) 6 | test.equal(serialized_sort, "title"); 7 | 8 | serialized_sort = Sort.serialize([ { field_name: 'title', order: 'desc' } ]); 9 | test.equal(serialized_sort, "title:desc"); 10 | 11 | serialized_sort = Sort.serialize([ { field_name: 'title', order: 'esc' } ]); 12 | test.equal(serialized_sort, "title"); 13 | 14 | serialized_sort = Sort.serialize([ { field_name: 'title', order: 'asc' } ]); 15 | test.equal(serialized_sort, "title"); 16 | 17 | serialized_sort = Sort.serialize([ { field_name: 'title', order: 'asc' }, { field_name: 'count', order: 'desc' } ]); 18 | test.equal(serialized_sort, "title,count:desc"); 19 | 20 | serialized_sort = Sort.serialize([]); 21 | test.equal(serialized_sort, ""); 22 | 23 | serialized_sort = Sort.serialize(null); 24 | test.equal(serialized_sort, ""); 25 | 26 | test.done(); 27 | }; 28 | 29 | exports.deserialize = function(test) { 30 | 31 | var sort = Sort.deserialize("title:desc"); 32 | test.deepEqual(sort, [ { field_name: 'title', order: 'desc' } ]) 33 | 34 | sort = Sort.deserialize("title:asc"); 35 | test.deepEqual(sort, [ { field_name: 'title', order: 'asc' } ]) 36 | 37 | sort = Sort.deserialize("title"); 38 | test.deepEqual(sort, [ { field_name: 'title', order: 'asc' } ]) 39 | 40 | sort = Sort.deserialize("title,count:desc"); 41 | test.deepEqual(sort,[ { field_name: 'title', order: 'asc' }, { field_name: 'count', order: 'desc' } ]); 42 | 43 | sort = Sort.deserialize(""); 44 | test.equal(sort, null); 45 | 46 | test.done(); 47 | }; 48 | -------------------------------------------------------------------------------- /tests/status.js: -------------------------------------------------------------------------------- 1 | var gx = require('gx'); 2 | var suite = require('./lib'); 3 | var Status = require('../lib/status'); 4 | 5 | exports.setUp = suite.setUp; 6 | exports.tearDown = suite.tearDown; 7 | 8 | exports.id = function(test) { 9 | 10 | gx(function*() { 11 | test.equal(Status.id('draft'), 1); 12 | test.equal(Status.id('published'), 2); 13 | test.done(); 14 | }); 15 | }; 16 | 17 | exports.name = function(test) { 18 | 19 | gx(function*() { 20 | test.equal(Status.name(1), 'draft'); 21 | test.equal(Status.name(2), 'published'); 22 | test.done(); 23 | }); 24 | }; 25 | 26 | -------------------------------------------------------------------------------- /views/api.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block css %} 4 | 5 | 6 | {% endblock %} 7 | 8 | {% block nav %} 9 | 14 | {% endblock %} 15 | 16 | {% block pagetitle %} 17 |
18 |

{{ workspace.title }} REST API

19 |
20 |
21 | {% endblock %} 22 | 23 | {% block content %} 24 | 25 | 26 |
27 | 28 | 42 | 43 |
44 | 45 | {% for resource in collection_resources %} 46 | 47 | 48 | 49 |

{{ resource.collection.title }}

50 | 51 |

List {{ resource.collection.title }}

52 | 53 |
GET /api/{{ workspace.handle }}/{{ resource.collection.name }}
54 | Preview → 55 | 56 |
57 | Get a listing of {{ resource.collection.title|lower }}. 58 |
59 | 60 |
Request Parameters
61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | {% for field in resource.collection.fields %} 69 | 70 | 71 | 72 | 73 | 74 | {% endfor %} 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 |
NameRequired?Description
{{ field.name }}OptionalFilter to items with {{ field.name }} equal to the supplied value
statusOptionalLimit results to the specified status of published or draft
pageOptionalPage number of the result set starting with page 1
per_pageOptionalNumber of items to show per page of results
qOptionalSearch query matching against all fields
96 | 97 | {% if resource.example_listing_response %} 98 |
Example Response
99 |
{{ resource.example_listing_response }}
100 | {% endif %} 101 | 102 |
103 | 104 |

Get a {{ resource.collection.title }} Item

105 | 106 |
GET /api/{{ workspace.handle }}/{{resource.collection.name}}/:identifier
107 | 108 |
109 | Get a details on a single one of the {{ resource.collection.title|lower }}. The identifier may be either the id for the item, or alternatively its key in the case that it has a field with that name. 110 |
111 | 112 |
113 | 114 |

Create {{ resource.collection.title }}

115 | 116 |
POST /api/{{ workspace.handle }}/{{resource.collection.name}}
117 | 118 |
119 | Create an item in the {{ resource.collection.title }} collection. 120 |
121 | 122 |
Request Parameters
123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | {% for field in resource.collection.fields %} 131 | 132 | 133 | 134 | 135 | 136 | {% endfor %} 137 |
NameRequired?Description
{{ field.name }}{% if field.is_required %}Required{% else %}Optional{% endif %}{{ field.description }}
138 | 139 |
140 | 141 |

Update {{ resource.collection.title }}

142 | 143 |
PATCH /api/{{ workspace.handle }}/{{resource.collection.name}}/:identifier
144 | 145 |
146 | Update an item in the {{ resource.collection.title }} collection. 147 |
148 | 149 |
Request Parameters
150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | {% for field in resource.collection.fields %} 158 | 159 | 160 | 161 | 162 | 163 | {% endfor %} 164 | 165 | 166 | 167 | 168 | 169 | 170 |
NameRequired?Description
{{ field.name }}Optional{{ field.description }}
statusOptionalSpecify status of published or draft
171 | 172 |
173 | 174 | {% endfor %} 175 |
176 |
177 | 178 | {% endblock %} 179 | 180 | {% block footer_js %} 181 | 182 | 183 | {% endblock %} 184 | -------------------------------------------------------------------------------- /views/collections.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% set title = 'Collections' %} 4 | 5 | {% block nav %} 6 | 10 | {% endblock %} 11 | 12 | {% block pagetitle %} 13 |
14 |

{{ workspace.title }}

15 | 16 |
17 | 18 | 23 |
24 | 25 |
26 |

{{ workspace.description }}

27 |
28 | {% endblock %} 29 | 30 | {% block content %} 31 |
32 | {% if collections.length %} 33 | 34 | {% for collection in collections %} 35 | 36 | 40 | 41 | 42 | 55 | 56 | {% endfor %} 57 |
37 | {{ collection.title }} 38 | {{ collection_counts[collection.id]|default(0) }} 39 | {{ collection.description }} 43 | 44 | 45 | Configure 46 | 47 |
48 | 49 | 53 |
54 |
58 | {% else %} 59 | New Collection 60 | {% endif %} 61 |
62 | 63 | {% endblock %} 64 | 65 | {% block footer_js %} 66 | 67 | 77 | 78 | {% endblock %} 79 | -------------------------------------------------------------------------------- /views/error.html: -------------------------------------------------------------------------------- 1 | Something went wrong. 2 | 3 | {{ error }} 4 | 5 | -------------------------------------------------------------------------------- /views/error_db.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% set title = 'Database Error' %} 4 | 5 | {% block page_header %} 6 |

Database Error

7 | {% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |

Database Error...

13 |
14 |
15 | {% endblock %} 16 | 17 | -------------------------------------------------------------------------------- /views/error_fixtures.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% set title = 'Fixtures Error' %} 4 | 5 | {% block page_header %} 6 |

Fixtures Error

7 | {% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |

Couldn't find fixtures data!

13 | Try node app fixtures-sync to install fixtures data. 14 |
15 |
16 | {% endblock %} 17 | 18 | -------------------------------------------------------------------------------- /views/error_schema.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% set title = 'Schema Error' %} 4 | 5 | {% block page_header %} 6 |

Schema Error

7 | {% endblock %} 8 | 9 | {% block content %} 10 |
11 |
12 |

Couldn't find valid database schema!

13 | Try node app schema-sync to initialize the database. 14 |
15 |
16 | {% endblock %} 17 | 18 | -------------------------------------------------------------------------------- /views/fields.html: -------------------------------------------------------------------------------- 1 | {% extends "layout.html" %} 2 | 3 | {% block content %} 4 |

5 | Fields 6 |

7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | {% for field in fields %} 15 | 16 | 17 | 18 | 19 | 20 | {% endfor %} 21 |
NameValidatorTemplate
{{ field.name }}{{ field.validator }}{{ field.template }}
22 |
23 | {% endblock %} 24 | -------------------------------------------------------------------------------- /views/fields/checkbox.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /views/fields/input.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /views/fields/markdown.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 23 | 24 | -------------------------------------------------------------------------------- /views/fields/select.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /views/fields/text.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /views/flash.html: -------------------------------------------------------------------------------- 1 | {% for message in messages.success %} 2 | 3 |
4 | {{ message }} 5 |
6 | 7 | {% endfor %} 8 | 9 | {% for message in messages.info %} 10 | 11 |
12 | {{ message }} 13 |
14 | 15 | {% endfor %} 16 | 17 | {% for message in messages.warning %} 18 | 19 |
20 | {{ message }} 21 |
22 | 23 | {% endfor %} 24 | 25 | {% for message in messages.danger %} 26 | 27 |
28 | {{ message }} 29 |
30 | 31 | {% endfor %} 32 | 33 | -------------------------------------------------------------------------------- /views/footer.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /views/header.html: -------------------------------------------------------------------------------- 1 | 21 | 22 | -------------------------------------------------------------------------------- /views/item.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% set title = 'Item' %} 4 | 5 | {% block nav %} 6 | 16 | {% endblock %} 17 | 18 | {% block pagetitle %} 19 |
20 | 28 |
29 |
30 | {% endblock %} 31 | 32 | {% block css %} 33 | 34 | {% endblock %} 35 | 36 | {% block content %} 37 |
38 | 39 |
40 | 41 | {% for field in fields %} 42 | 43 | 49 | 58 | 59 | {% endfor %} 60 |
44 | {{ field.title }} 45 |
46 | {% if field.is_required %}Required{% else %}Optional{% endif %} 47 |
48 |
50 | {% include field.control.template %} 51 | 52 | {% if errors[field.name] %} 53 |
{{ errors[field.name] }}
54 | {% endif %} 55 | 56 |
{{ field.description }}
57 |
61 |
62 | 63 | {% if item.status == "published" %} 64 | 68 | 72 | {% else %} 73 | 77 | 81 | {% endif %} 82 | 83 | {% if item.id %} 84 |
85 | 86 | 90 |
91 | Last updated by {{ item.user.username }} {{ item.updated_relative_time }} 92 | {% endif %} 93 | 94 | {% if revisions.length %} 95 |




96 |
97 |

Prior Revisions

98 | 99 | 100 | {% for revision in revisions %} 101 | 102 | 103 | 104 | 105 | 112 | 113 | {% endfor %} 114 |
{{ revision.username }}{{ revision.relative_time }}{{ revision.data }} 106 | View 107 |
108 | 109 |
115 |
116 | {% endif %} 117 |
118 | 119 | {% endblock %} 120 | 121 | {% block footer_js %} 122 | 123 | 140 | 141 | {% endblock %} 142 | -------------------------------------------------------------------------------- /views/items.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% set title = 'Items' %} 4 | 5 | {% block nav %} 6 | 11 | {% endblock %} 12 | 13 | {% block pagetitle %} 14 |
15 |

{{ collection.title }}

16 |
17 | 18 | 22 |
23 | 24 | 25 | Create New 26 | 27 |
28 | {{ pagination.fromResult }}-{{ pagination.toResult }} of {{ pagination.totalResult }} 29 | {% set icon_color = "icon-black" %} 30 | {% include "pagination.html" %} 31 |
32 |
33 | 34 | 35 |
36 |
37 |

{{ collection.description }}

38 |
39 | {% endblock %} 40 | 41 | {% block content %} 42 |
43 | 44 | 45 | {% if items.length %} 46 | 47 | 48 | 49 | {% for col in column_fields %} 50 | 56 | {% endfor %} 57 | 58 | 59 | 60 | {% for item in items %} 61 | 62 | 63 | 66 | 67 | {% for field in fields %} 68 | 69 | {% endfor %} 70 | 71 | 74 | 75 | 81 | 82 | 83 | 84 | {% endfor %} 85 |
51 | 52 | {{ col.title }} 53 | {{ col.sort_indicator }} 54 | 55 |
64 | #{{ item.id }} 65 |

{{ item.data[field.name]|raw }}

72 | {{ item.status|title }} 73 | 76 | 77 | 78 | Edit 79 | 80 |
86 | 87 | {% set icon_color = "icon-black" %} 88 | {% include "pagination.html" %} 89 | 90 |
91 | {{ pagination.fromResult }}-{{ pagination.toResult }} of {{ pagination.totalResult }} 92 |
93 | 94 | {% else %} 95 |
Nothing to see here yet.
96 | {% endif %} 97 |
98 | 99 | {% endblock %} 100 | -------------------------------------------------------------------------------- /views/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ title|default('Showcase') }} 8 | 9 | 10 | {% block css %}{% endblock %} 11 | {% block header_js %}{% endblock %} 12 | {% include 'permissions.html' %} 13 | 14 | 15 | 32 |
33 | {% include 'flash.html' %} 34 | 37 | {% block pagetitle %}{% endblock %} 38 | {% block content %}{% endblock %} 39 |
40 | {% include 'footer.html' %} 41 | {% block footer_js %}{% endblock %} 42 | 43 | 44 | -------------------------------------------------------------------------------- /views/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% set title = 'Sign In' %} 4 | 5 | {% block css %} 6 | 7 | {% endblock %} 8 | 9 | {% block page_header %} 10 |

11 | Sign In 12 |

13 | {% endblock %} 14 | 15 | {% block content %} 16 |
17 |

18 |
19 |

Sign in to your account

20 | 24 |
25 |
26 | {% endblock %} 27 | -------------------------------------------------------------------------------- /views/pagination.html: -------------------------------------------------------------------------------- 1 | 16 | -------------------------------------------------------------------------------- /views/permissions.html: -------------------------------------------------------------------------------- 1 | 21 | 22 | -------------------------------------------------------------------------------- /views/revision.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block content %} 4 | 5 |
6 |

Revision Details

7 | 8 | By {{ revision.username }}, {{ revision.relative_time }} ({{ revision.create_time }}) 9 | 10 |

11 |

Raw Data

12 |
{{ revision.data }}
13 | 14 |
15 | 16 | {% endblock %} 17 | 18 | -------------------------------------------------------------------------------- /views/setup.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% set title = 'Setup' %} 4 | 5 | {% block css %} 6 | 7 | {% endblock %} 8 | 9 | {% block page_header %} 10 |

Setup

11 | {% endblock %} 12 | 13 | {% block content %} 14 |
15 |
16 |

Create an administrator account

17 |
18 |
19 | 22 |
23 |
24 |
25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /views/user.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% set title = 'User' %} 4 | 5 | {% block css %} 6 | 7 | {% endblock %} 8 | 9 | {% block pagetitle %} 10 | 11 |
12 | 19 |
20 |
21 | 22 | {% endblock %} 23 | 24 | {% block content %} 25 | 26 |
27 | 28 |
29 | 30 | 31 | 34 | 40 | 41 | 42 | 45 | 54 | 55 | {% if workspaces.length %} 56 | 57 | 60 | 77 | 78 | {% endif %} 79 |
32 | Username 33 | 35 | 36 |
37 | Username for the end user 38 |
39 |
43 | Showcase Superuser 44 | 46 | 50 |
51 | Is this user all-powerful? User must log out and back in for changes to take effect. 52 |
53 |
58 | Workspace
Permissions 59 |
61 | 62 | {% for workspace in workspaces %} 63 | 64 | 65 | 73 | 74 | {% endfor %} 75 |
{{ workspace.title }} 66 | 67 | 72 |
76 |
80 | {% if is_config_superuser %} 81 |
User is configured as a superuser in showcase configuration file, so cannot be edited via this interface.
82 | {% else %} 83 | 87 | {% endif %} 88 |
89 |
90 | 91 | {% endblock %} 92 | 93 | {% block footer_js %} 94 | 110 | {% endblock %} 111 | 112 | -------------------------------------------------------------------------------- /views/users.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% set title = 'Users' %} 4 | 5 | {% block pagetitle %} 6 |
7 |

Users

8 | 9 | 10 | New User 11 | 12 |
13 | {% endblock %} 14 | 15 | {% block content %} 16 | 17 |
18 | {% if users.length %} 19 | 20 | {% for user in users %} 21 | 22 | 25 | 31 | 32 | {% endfor %} 33 |
23 | {{ user.username }} 24 | 26 | 27 | 28 | Edit 29 | 30 |
34 | {% endif %} 35 |
36 | 37 | {% endblock %} 38 | -------------------------------------------------------------------------------- /views/workspace.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% set title = 'Workspace' %} 4 | 5 | {% block nav %} 6 | 12 | {% endblock %} 13 | 14 | {% block pagetitle %} 15 |
16 | {% if workspace.handle %} 17 |

{{ workspace.title }}

18 | {% else %} 19 |

New Workspace

20 | {% endif %} 21 |
22 | {% endblock %} 23 | 24 | {% block content %} 25 | 26 |
27 |
28 | 29 | 30 | 34 | 40 | 41 | 42 | 46 | 52 | 53 | 54 | 58 | 64 | 65 | 66 | 67 |
31 | Title 32 |
Required
33 |
35 | 36 |
37 | Title of the workspace for humans 38 |
39 |
43 | Handle 44 |
Required
45 |
47 | 48 |
49 | Name of the workspace for computers (all lowercase, no spaces) 50 |
51 |
55 | Description 56 |
Required
57 |
59 | 60 |
61 | What material does this workspace cover? 62 |
63 |
68 | 69 | 73 |
74 |
75 | 76 | {% endblock %} 77 | 78 | {% block footer_js %} 79 | 85 | {% endblock %} 86 | -------------------------------------------------------------------------------- /views/workspaces.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% set title = 'Workspaces' %} 4 | 5 | {% block pagetitle %} 6 |
7 |

Workspaces

8 |
9 | 10 | 13 |
14 |
15 |

Workspaces are groups of collections, often one per web site

16 |
17 | {% endblock %} 18 | 19 | {% block content %} 20 | 21 |
22 | {% if workspaces.length %} 23 | 24 | {% for workspace in workspaces %} 25 | 26 | 27 | 28 | 41 | 42 | {% endfor %} 43 |
{{ workspace.title }}{{ workspace.description }} 29 | 30 | 31 | Configure 32 | 33 |
34 | 35 | 39 |
40 |
44 | {% else %} 45 | New Workspace 46 | {% endif %} 47 |
48 | 49 | {% endblock %} 50 | 51 | {% block footer_js %} 52 | 53 | 63 | 64 | {% endblock %} 65 | 66 | --------------------------------------------------------------------------------