├── .travis.yml ├── README.md ├── package.json ├── test.js ├── tutorials.json └── tutorials ├── admin-template-override.md ├── how-to-contribute.md ├── logging-system.md ├── microscope.md ├── security.md └── upgrading-to-v1-0.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | notifications: 5 | email: false 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Orion Tutorials 2 | [Orion](https://github.com/orionjs/core) is a CMS built on Meteor. If you haven't already please check out the current [Documentation](http://orionjs.org/docs/introduction) as that will be updated as Orion evolves. 3 | 4 | You can see the tutorials at [orionjs.org/tutorials](http://orionjs.org/tutorials). 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "orion-tutorials", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "test": "mocha" 6 | }, 7 | "devDependencies": { 8 | "mocha": "^1.17.1" 9 | } 10 | } -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | 3 | var file = fs.readFileSync('./tutorials.json', 'utf8'); 4 | 5 | describe('tutorials.json', function(){ 6 | it('should have valid json structure', function(){ 7 | JSON.parse(file); 8 | }) 9 | it('all files exists', function() { 10 | var json = JSON.parse(file); 11 | 12 | if (!Array.isArray(json)) { 13 | throw Error('tutorials.json is not an array'); 14 | } 15 | 16 | for (var i = 0; i < json.length; i++) { 17 | var page = json[i]; 18 | var fileContents = fs.readFileSync('./tutorials/' + page.file, 'utf8'); 19 | if (!fileContents) { 20 | throw Error('file "' + page.file + '" is empty'); 21 | } 22 | } 23 | }) 24 | it('should not generate errors with highlightjs', function() { 25 | var json = JSON.parse(file); 26 | 27 | if (!Array.isArray(json)) { 28 | throw Error('tutorials.json is not an array'); 29 | } 30 | 31 | for (var i = 0; i < json.length; i++) { 32 | var page = json[i]; 33 | var fileContents = fs.readFileSync('./tutorials/' + page.file, 'utf8'); 34 | if (!fileContents) { 35 | throw Error('file "' + page.file + '" is empty'); 36 | } 37 | 38 | var re = /\n```.*```/g; 39 | if (re.test(fileContents)) { 40 | throw Error('code without language should not start in a new line'); 41 | } 42 | 43 | var re = /```console\n/g; 44 | if (re.test(fileContents)) { 45 | throw Error('console language does not exist'); 46 | } 47 | } 48 | }); 49 | }) 50 | -------------------------------------------------------------------------------- /tutorials.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "slug": "microscope", 4 | "title": "OrionJS with Microscope Tutorial", 5 | "description": "A comprehensive tutorial that that goes through the end-to-end setup for Orion", 6 | "author": "fuzzybabybunny", 7 | "authorGithub": "fuzzybabybunny", 8 | "file": "microscope.md", 9 | "smallImage": "https://farm9.staticflickr.com/8447/7767672620_597a2a1a11_z.jpg", 10 | "bigImage": "https://farm9.staticflickr.com/8447/7767672620_b5e1cbd0f6_h.jpg" 11 | }, 12 | { 13 | "slug": "upgrading-to-v1-0", 14 | "title": "Upgrade your Orion app to v1.0", 15 | "description": "A complete tutorial on how to upgrade your app to be compatible with Orion v1.0", 16 | "author": "Nicolás López", 17 | "authorGithub": "nicolaslopezj", 18 | "file": "upgrading-to-v1-0.md", 19 | "smallImage": "http://i.imgur.com/U7QGtra.jpg", 20 | "bigImage": "http://i.imgur.com/l43lRWL.jpg" 21 | }, 22 | { 23 | "slug": "securing-orion", 24 | "title": "Security and Browser policies", 25 | "description": "For protecting your server against script injection and your clients against XSS attacks", 26 | "author": "Pierre-Eric Marchandet", 27 | "authorGithub": "PEM--", 28 | "file": "security.md", 29 | "smallImage": "http://i.imgur.com/ZKtmTLq.jpg", 30 | "bigImage": "http://i.imgur.com/bXKOYu0.jpg" 31 | }, 32 | { 33 | "slug": "how-to-contribute", 34 | "title": "How to Contribute", 35 | "description": "Set up your project so you contribute code", 36 | "author": "Alen Balja", 37 | "authorGithub": "mraak", 38 | "file": "how-to-contribute.md", 39 | "smallImage": "http://currentlabel.co.uk/hammock-thumb.jpg", 40 | "bigImage": "http://currentlabel.co.uk/hammock.jpg" 41 | }, 42 | { 43 | "slug": "admin-template-override", 44 | "title": "Admin template override", 45 | "description": "Add specific behavior to your admin interface", 46 | "author": "Pierre-Eric Marchandet", 47 | "authorGithub": "PEM--", 48 | "file": "admin-template-override.md", 49 | "smallImage": "http://apod.nasa.gov/apod/image/1308/arp271_gemini_2048.jpg", 50 | "bigImage": "http://apod.nasa.gov/apod/image/1308/arp271_gemini_2048.jpg" 51 | }, 52 | { 53 | "slug": "logging-system", 54 | "title": "Logging system", 55 | "description": "Create your own logger or re-use Orion's one", 56 | "author": "Pierre-Eric Marchandet", 57 | "authorGithub": "PEM--", 58 | "file": "logging-system.md", 59 | "smallImage": "https://s-media-cache-ak0.pinimg.com/originals/c5/68/46/c56846d983440c26fe430727d25f76b8.jpg", 60 | "bigImage": "https://s-media-cache-ak0.pinimg.com/originals/c5/68/46/c56846d983440c26fe430727d25f76b8.jpg" 61 | } 62 | ] 63 | -------------------------------------------------------------------------------- /tutorials/admin-template-override.md: -------------------------------------------------------------------------------- 1 | ## Admin template override 2 | ### Introduction 3 | Orion comes with default templates for all your collections. These templates are defined within the admin interface that you've chosen to import, namely **[orionjs:bootstrap](https://github.com/orionjs/orion/tree/master/packages/bootstrap)** or **[orionjs:materialize](https://github.com/orionjs/orion/tree/master/packages/materialize)** for the official ones. 4 | 5 | When you need to add some special behavior for a given collection, you can override the default template with your own one. This is made possible thanks to **[nicolaslopezj:reactive-templates](https://atmospherejs.com/nicolaslopezj/reactive-templates)**. 6 | 7 | ### Template inheritance 8 | In this tutorial, we are going to add a button for exporting a collection in a CSV file. 9 | 10 | > For conciseness, all code samples are provided either in CoffeeScript or in Jade. 11 | 12 | Our collection is named `subscribers` and we are going to extend its index. 13 | We start by creating a template file that displays the usual title, a tabular table 14 | and an additional button used for the CSV export. 15 | 16 | ```html 17 | 18 | 19 | 38 | ``` 39 | 40 | Now we set the logic in: 41 | 42 | ```js 43 | ReactiveTemplates.set('collections.subscribers.index', 'subscribersIndex'); 44 | 45 | if (Meteor.isClient) { 46 | Template.subscribersIndex.onCreated(function() { 47 | appLog.info('subscribersIndex created'); 48 | this.subscribersIndex_showTable = new ReactiveVar; 49 | return this.subscribersIndex_showTable.set(false); 50 | }); 51 | Template.subscribersIndex.onRendered(function() { 52 | return this.autorun((function(_this) { 53 | return function() { 54 | Template.currentData(); 55 | _this.subscribersIndex_showTable.set(false); 56 | return Meteor.defer(function() { 57 | return _this.subscribersIndex_showTable.set(true); 58 | }); 59 | }; 60 | })(this)); 61 | }); 62 | Template.subscribersIndex.helpers({ 63 | showTable: function() { 64 | return Template.instance().subscribersIndex_showTable.get(); 65 | } 66 | }); 67 | Template.subscribersIndex.events({ 68 | 'click tr': function(e, t) { 69 | var collection, dataTable, path, rowData; 70 | if (!$(event.target).is('td')) { 71 | return; 72 | } 73 | collection = Template.currentData().collection; 74 | dataTable = $(e.target).closest('table').DataTable(); 75 | rowData = dataTable.row(e.currentTarget).data(); 76 | if (rowData != null ? rowData.canShowUpdate() : void 0) { 77 | path = collection.updatePath(rowData); 78 | return RouterLayer.go(path); 79 | } 80 | }, 81 | 'click button.import-csv': function(e, t) { 82 | var csvButton, subscription; 83 | csvButton = t.$('button.import-csv'); 84 | csvButton.addClass('disabled'); 85 | return subscription = Meteor.subscribe('allSubscribers', { 86 | onReady: function() { 87 | var csv, data, i, len, sub; 88 | data = Subscribers.find({}, { 89 | name: 1, 90 | forname: 1, 91 | _id: 0 92 | }).fetch(); 93 | csv = (_.keys(data[0])).join(';'); 94 | for (i = 0, len = data.length; i < len; i++) { 95 | sub = data[i]; 96 | csv += '\n' + (_.values(sub)).join(';'); 97 | } 98 | blobDownload(csv, 'subscribers.csv', 'text/csv'); 99 | return csvButton.removeClass('disabled'); 100 | }, 101 | onError: function(err) { 102 | sAlert.warning('CSV subscription failed'); 103 | appLog.warn('CSV subscription failed', err); 104 | return csvButton.removeClass('disabled'); 105 | } 106 | }); 107 | } 108 | }); 109 | } 110 | 111 | if (Meteor.isServer) { 112 | Meteor.publish('allSubscribers', function() { 113 | return Subscribers.find(); 114 | }); 115 | } 116 | ``` 117 | 118 | ### Tip 119 | When you're creating your overridden templates, you should start from copying 120 | the ones from the official provided templates in **[orionjs:bootstrap](https://github.com/orionjs/orion/tree/master/packages/bootstrap)** or **[orionjs:materialize](https://github.com/orionjs/orion/tree/master/packages/materialize)**. They provide a nice starting point for adding your extended behavior. 121 | -------------------------------------------------------------------------------- /tutorials/how-to-contribute.md: -------------------------------------------------------------------------------- 1 | ## How to Contribute 2 | 3 | This is a simple 3 step guide how to contribute to core orion packages. 4 | 5 | ###1. Fork the orion repo 6 | 7 | Fork the repo https://github.com/orionjs/orion 8 | 9 | Create a folder _orionjs_ somewhere on your hard drive and cd into it, then clone your fork: 10 | 11 | `git clone https://github.com//orion` 12 | 13 | That's it, you have a fork that you can edit! 14 | 15 | Optionally, set _upstream_, so you can fetch from original repo into your fork whenever there are some changes there. Here is more info on how to do that: 16 | https://help.github.com/articles/syncing-a-fork/ 17 | 18 | 19 | ###2. Set up a working project 20 | 21 | To see the changes you make in your fork, you need a working project based on the packages from your fork. I simply use the blog example from orion-examples. 22 | 23 | Inside your _orionjs_ folder clone the examples repo. 24 | 25 | `git clone https://github.com/orionjs/examples.git` 26 | 27 | Your folder structure should now look like this: 28 | 29 | 30 | -orionjs 31 | --orion 32 | --examples 33 | ---blog 34 | 35 | 36 | Let your blog project use your fork's packages and not the original orion packages and run the project. 37 | 38 | `cd examples/blog` 39 | `export PACKAGE_DIRS=//orionjs/orion/packages` 40 | `meteor` 41 | 42 | (You may also want to set the env var in your .bash_profile to have it always ready.) 43 | 44 | That's it! Make some changes somewhere in the packages (e.g. packages/bootstrap), and you will see them in your running blog project. Continue working, commit back to your fork, others can work on your fork too, and when you're ready to merge your work into the main project, go to step 3. 45 | 46 | 47 | ###3. Send a Pull Request when ready 48 | 49 | Here's a very good guide, no more words are necessary. Just make sure NOT to send PR into main branch, ask around what is the current working branch. 50 | 51 | https://help.github.com/articles/using-pull-requests/ 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /tutorials/logging-system.md: -------------------------------------------------------------------------------- 1 | ## Logging system 2 | ### Introduction 3 | Orion provides its own logging system based on [Bunyan](https://github.com/trentm/node-bunyan). 4 | Isomorphic, it allows you to use the same API wether you want to log information 5 | on the client side or on the server side. 6 | 7 | ### Simple use 8 | Though we recommend creating your own logger to discriminate your logs from the 9 | one of Orion, you can easily use the default logger just like the [Console API](https://developer.chrome.com/devtools/docs/console-api): 10 | 11 | * **trace**: Use it for tracking intermediary state information. 12 | ```js 13 | orion.log.trace('Simple trace level message', {value: true}, false); 14 | ``` 15 | * **debug**: Use it for debugging. 16 | ```js 17 | orion.log.debug('Simple debug level message', {value: true}, false); 18 | ``` 19 | * **info**: Use it for tracking relevant state or status information. 20 | ```js 21 | orion.log.info('Simple info level message', {value: true}, false); 22 | ``` 23 | * **warn**: Use it for warning when something goes wrong but is still properly managed by your application. 24 | ```js 25 | orion.log.warn('Simple warning level message', {value: true}, false); 26 | ``` 27 | * **error**: Use it for errors when something goes wrong and you've no certainty that you can keep a proper application behavior. 28 | ```js 29 | orion.log.error('Simple error level message', {value: true}, false); 30 | ``` 31 | * **fatal**: Use it for fatal errors when the situation cannot be recovered with a full reboot. 32 | ```js 33 | orion.log.fatal('Simple fatal level message', {value: true}, false); 34 | ``` 35 | 36 | ### Setting the appropriate log level 37 | You can remove set the log level using [Bunyan](https://github.com/trentm/node-bunyan)'s API. Here are some useful commands: 38 | #### Remove all logs on Orion's default logger 39 | ```js 40 | orion.log.level('none'); 41 | ``` 42 | #### Set level for the Orion's default logger 43 | ```js 44 | // For debug level and above 45 | orion.log.level('debug'); 46 | ... 47 | // For info level and above 48 | orion.log.level('info'); 49 | ... 50 | // For fatal only level 51 | orion.log.level('fatal'); 52 | ``` 53 | 54 | ### Create your own logger 55 | We recommend that you create your own logging system to discriminate your logs 56 | from the ones of Orion so that it gets easier for you to check wether the issue 57 | is within your application or caused by a misuse or an issue of Orion. 58 | 59 | Within you application, just create a Javascript file that it sufficiently 60 | prioritized so that you logger is available as soon as possible (see [Meteor's File Load Order](http://docs.meteor.com/#/full/structuringyourapp)). 61 | A file named `lib/utils/priority/_logger.js` should be sufficiently prioritized. 62 | 63 | As well as a default logger, Orion provides its default log formatter `orion.logFormatter` that you can 64 | rely on for a simple logging strategy. Here's how to create your logger with the 65 | default formatter and a default log level set on **info**: 66 | ```js 67 | // Client and server side 68 | myAppLog = new bunyan.createLogger({ 69 | name: 'myApp', 70 | stream: orion.logFormatter, 71 | level: 'info' 72 | }); 73 | ``` 74 | 75 | Now wether you are client side or server side, you can use your logger like so: 76 | ```js 77 | myAppLog.info('Simple info level message', myVar, 'any text', anyOtherObjectOrVar); 78 | myAppLog.warn('Simple warning level message', myVar, 'any text', anyOtherObjectOrVar); 79 | myAppLog.error('Simple error level message', myVar, 'any text', anyOtherObjectOrVar); 80 | ... 81 | ``` 82 | 83 | ### Create you own formatter 84 | Log formatter are the real power of [Bunyan](https://github.com/trentm/node-bunyan). 85 | This is where you can implement all your logging strategies like: 86 | 87 | * Sending your server's logs to a [logstash](https://www.elastic.co/products/logstash) server. 88 | * Send an email when a specific log level has been executed. 89 | * Sending your client's logs and your server's logs to a SaaS 90 | (ex. [Loggly](https://www.loggly.com/), [LogEntries](https://logentries.com), ...). 91 | * Enabling logging strategy for a specific connected customer so that you can 92 | see what's this user has done within your application. 93 | * ... 94 | 95 | While being completely isomorphic in its API, the log formatter has to be written 96 | differently depending on its execution context, client or server side as we depict 97 | it afterwards. Both implementation must leverage the power of [Node's Stream](https://nodejs.org/api/stream.html). 98 | 99 | > Thanks to [Browserify](http://browserify.org/) and [Cosmo's meteor package for browserify](https://github.com/elidoran/cosmos-browserify), 100 | the [Node's Stream](https://nodejs.org/api/stream.html) is available on client side making it isomorphic as well. 101 | 102 | #### Client side log formatter 103 | For this part, we are simply using the basic [Console API](https://developer.chrome.com/devtools/docs/console-api) 104 | along with some colored styles thanks to [Log with style](https://www.npmjs.com/package/log-with-style) 105 | which is also exposed by Orion. 106 | 107 | On client side, Orion's log system exposes the following API: 108 | * `bunyan`: [Bunyan](https://github.com/trentm/node-bunyan). 109 | * `process`: [Browserified Node's process](https://www.npmjs.com/package/process). 110 | * `WritableStream`: [Browserified Node's WritableStream](https://nodejs.org/api/stream.html#stream_class_stream_writable). 111 | * `inherits`: [Browserified Node utils's inherits](https://nodejs.org/docs/latest/api/util.html#util_util_inherits_constructor_superconstructor). 112 | * `logStyle`: [Log with style](https://www.npmjs.com/package/log-with-style) a simple set of colors and styles for [Console API](https://developer.chrome.com/devtools/docs/console-api). 113 | 114 | One of the major advantage of using [Bunyan](https://github.com/trentm/node-bunyan) 115 | is that all log informations are treated as streams of JSON objects. In this 116 | formatter example we are using this capabilities to pick the appropriate value 117 | that we want to display in your DevTools console. 118 | 119 | ```js 120 | if (Meteor.isClient) { 121 | inherits(BrowserStdout, WritableStream); 122 | 123 | function BrowserStdout() { 124 | if (!(this instanceof BrowserStdout)) { 125 | return new BrowserStdout(); 126 | } 127 | WritableStream.call(this); 128 | } 129 | 130 | BrowserStdout.prototype._write = function(chunks, encoding, cb) { 131 | var output = JSON.parse(chunks.toString ? chunks.toString() : chunks); 132 | var color = '[c="color: green"]'; 133 | var level = 'INFO'; 134 | if (output.level > 40) { 135 | color = '[c="color: red"]'; 136 | if (output.level === 60) { 137 | level = 'FATAL'; 138 | } else { 139 | level = 'ERROR'; 140 | } 141 | } else if (output.level === 40) { 142 | color = '[c="color: orange"]'; 143 | level = 'WARNING'; 144 | } else if (output.level === 20) { 145 | level = 'DEBUG'; 146 | } else if (output.level === 10) { 147 | level = 'TRACE'; 148 | } 149 | logStyle(color + level + '[c] ' + '[c="color: blue"]' + output.name + '[c] ' + output.msg); 150 | process.nextTick(cb); 151 | }; 152 | 153 | myLogFormatter = BrowserStdout(); 154 | } 155 | ``` 156 | Now we have our custom log formatter client side named `myLogFormatter`. 157 | 158 | #### Server side log formatter 159 | For this part, we are using an already made formatter available in the NPM registry: [Bunyan Format](https://www.npmjs.com/package/bunyan-format). 160 | 161 | On server side, Orion's log system exposes the following API: 162 | * `bunyan`: [Bunyan](https://github.com/trentm/node-bunyan). 163 | * `bunyanFormat`: [Bunyan Format](https://www.npmjs.com/package/bunyan-format). 164 | 165 | ```js 166 | if (Meteor.isServer) { 167 | myLogFormatter = bunyanFormat({outputMode: 'short', color: true}); 168 | } 169 | ``` 170 | 171 | Super easy. Of course, you can go pretty far using [Node's Stream](https://nodejs.org/api/stream.html) 172 | and implement every use cases that would suit your logging strategies. 173 | 174 | #### Using our custom formatter 175 | Now that we have implemented our custom logger with the same name on the client 176 | side as well as the server side, we can go back to isomorphic Javascript and 177 | customize our logger with our shared `myLogFormatter`: 178 | 179 | ```js 180 | // Client and server side 181 | myAppLog = new bunyan.createLogger({ 182 | name: 'myApp', 183 | stream: myLogFormatter 184 | }); 185 | ``` 186 | 187 | ### Links 188 | * Inspired from [Ongoworks's Bunyan](https://github.com/ongoworks/meteor-bunyan) 189 | * [Bunyan](https://github.com/trentm/node-bunyan) 190 | * [Bunyan Format](https://www.npmjs.com/package/bunyan-format) 191 | * [Comparison between Winston and Bunyan](https://strongloop.com/strongblog/compare-node-js-logging-winston-bunyan/) 192 | * [Log with style](https://www.npmjs.com/package/log-with-style) 193 | * [Node's Stream](https://nodejs.org/api/stream.html) 194 | * [Handbook for Node's Stream](https://github.com/substack/stream-handbook) 195 | -------------------------------------------------------------------------------- /tutorials/microscope.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | **Table of Contents** 4 | 5 | - [Meteor OrionJS with Microscope Tutorial](#meteor-orionjs-with-microscope-tutorial) 6 | - [Purpose](#purpose) 7 | - [Cloning Microscope](#cloning-microscope) 8 | - [Download OrionJS](#download-orionjs) 9 | - [Initial Impressions](#initial-impressions) 10 | - [Creating Users](#creating-users) 11 | - [Adding and Removing Roles from Users](#adding-and-removing-roles-from-users) 12 | - [Getting Roles](#getting-roles) 13 | - [Setting Roles](#setting-roles) 14 | - [Adding Collections to OrionJS](#adding-collections-to-orionjs) 15 | - [Updating Collection Documents](#updating-collection-documents) 16 | - [Schemas](#schemas) 17 | - [Adding Comments Collection](#adding-comments-collection) 18 | - [Custom Input Types (Widgets)](#custom-input-types-widgets) 19 | - [Adding Summernote](#adding-summernote) 20 | - [Orion Attributes](#orion-attributes) 21 | - [Adding Images to Amazon S3 (updated 07/28/2015)](#adding-images-to-amazon-s3-updated-07282015) 22 | - [Setting up S3](#setting-up-s3) 23 | - [Configuring OrionJS](#configuring-orionjs) 24 | - [Changing Tabular Templates (updated 7/29/2015)](#changing-tabular-templates-updated-7292015) 25 | - [orion.attributeColumn()](#orionattributecolumn) 26 | - [Custom Tabular Templates](#custom-tabular-templates) 27 | - [Template-Level Subscriptions](#template-level-subscriptions) 28 | - [Meteor Tabular Render](#meteor-tabular-render) 29 | - [Meteor Tabular with Actual Templates](#meteor-tabular-with-actual-templates) 30 | - [Dictionary (updated 7/28/2015)](#dictionary-updated-7282015) 31 | - [Relationships](#relationships) 32 | - [hasOne](#hasone) 33 | - [Chicken and the Egg](#chicken-and-the-egg) 34 | - [Correcting File Load Order](#correcting-file-load-order) 35 | - [hasMany](#hasmany) 36 | - [Multiple Relationships (updated 7/31/2015)](#multiple-relationships-updated-7312015) 37 | - [Limitations of Defining Relationships](#limitations-of-defining-relationships) 38 | - [Setting Roles and Permissions (updated 8/3/2015)](#setting-roles-and-permissions-updated-832015) 39 | 40 | 41 | 42 | # Meteor OrionJS with Microscope Tutorial # 43 | 44 | For questions, pull requests, or error submissions for this tutorial, go [here.](https://github.com/fuzzybabybunny/microscope-orionjs) 45 | 46 | ## Purpose ## 47 | 48 | I haven't been able to find a good tutorial that goes through the end-to-end setup for OrionJS, so I decided to create this tutorial both as a learning resource for others but also as a way for me to keep track of my own progress as I poke around OrionJS and figure out how to do stuff with it. 49 | 50 | There will be cursing in this tutorial. 51 | 52 | ## Cloning Microscope ## 53 | 54 | Most Meteor developers should be familiar with Microscope, the social news app which the user codes along with when following the [Discover Meteor](https://www.discovermeteor.com/) tutorial book. If you haven't read and coded along with this book, do it. Now. 55 | 56 | As of this writing, Microscope does not have a backend admin system in place to manage its data, so I thought that it would be the ideal candidate for creating a tutorial on how to get OrionJS working. 57 | 58 | First, go to https://github.com/DiscoverMeteor/Microscope and clone the repo. 59 | 60 | ``` 61 | git clone git@github.com:DiscoverMeteor/Microscope.git 62 | cd Microscope 63 | meteor update 64 | ``` 65 | 66 | ## Download OrionJS ## 67 | 68 | OrionJS is designed to work nicely with Bootstrap, which is perfect because Microscope does as well. 69 | 70 | ``` 71 | meteor add orionjs:core fortawesome:fontawesome orionjs:bootstrap 72 | ``` 73 | 74 | When you do `meteor list` you should see (versions may be different): 75 | 76 | ``` 77 | 78 | accounts-password 1.1.1 Password support for accounts 79 | audit-argument-checks 1.0.3 Try to detect inadequate input sanitization 80 | fortawesome:fontawesome 4.3.0 Font Awesome (official): 500+ scalable vector icons, customizable via CSS, Retina friendly 81 | ian:accounts-ui-bootstrap-3 1.2.76 Bootstrap-styled accounts-ui with multi-language support. 82 | iron:router 1.0.9 Routing specifically designed for Meteor 83 | orionjs:bootstrap 1.2.1 A simple theme for orion 84 | orionjs:core 1.2.0 Orion 85 | sacha:spin 2.3.1 Simple spinner package for Meteor 86 | standard-app-packages 1.0.5 Moved to meteor-platform 87 | twbs:bootstrap 3.3.5 The most popular front-end framework for developing responsive, mobile first projects on the web. 88 | underscore 1.0.3 Collection of small helpers: _.map, _.each, ... 89 | 90 | ``` 91 | ## Initial Impressions ## 92 | 93 | Great! Now that we've added all these packages, let's start up Microscope and see how screwed up we made everything. 94 | ``` 95 | meteor 96 | ``` 97 | Then point your browser to `http://localhost:3000/` 98 | 99 | Everything looks like it should: 100 | 101 | ![enter image description here](https://lh3.googleusercontent.com/ADsFjG6v1AlTMaULUSVMG9qEBKrCQ902sUXuQI5N1dU=s0 "Screenshot from 2015-07-23 22:54:16.png") 102 | 103 | Now let's go to `http://localhost:3000/admin` 104 | 105 | ![enter image description here](https://lh3.googleusercontent.com/yjUHPNxc1wclW3M-pPFIk1J_F8IglciV8NU85nIwNE0=s0 "Screenshot from 2015-07-23 22:56:17.png") 106 | 107 | Looks like crap. 108 | 109 | The reason this is here is because there's a hidden red alert box that is floating right. It's a style that was defined in Microscope so let's go remove it. Comment out that line of code. 110 | 111 | ```css 112 | /client/stylesheets/style.css 113 | 114 | .alert { 115 | animation: fadeOut 2700ms ease-in 0s 1 forwards; 116 | -webkit-animation: fadeOut 2700ms ease-in 0s 1 forwards; 117 | -moz-animation: fadeOut 2700ms ease-in 0s 1 forwards; 118 | width: 250px; 119 | /* float: right;*/ 120 | clear: both; 121 | margin-bottom: 5px; 122 | pointer-events: auto; 123 | } 124 | ``` 125 | This is going to make Microscope look crappier but I'm crap at CSS so screwit. 126 | 127 | ![enter image description here](https://lh3.googleusercontent.com/UwazTdhYJp0Rx_aYIbwJp52UOQbhLPfv5VvqrZJH8jo=s0 "Screenshot from 2015-07-23 23:10:09.png") 128 | 129 | Better. The invisible alert box is still taking up space but I can't be screwed to do anything about it. 130 | 131 | ## Creating Users ## 132 | 133 | Looks like we need to create users and log in first before we can see the OrionJS backend. 134 | 135 | Go here and replace the original `// create two users` code with this: 136 | 137 | (Don't just copy this code and replace everything in `fixtures.js` with this since it's just partial code.) 138 | 139 | ```javascript 140 | /server/fixtures.js 141 | 142 | // Fixture data 143 | if (Posts.find().count() === 0) { 144 | var now = new Date().getTime(); 145 | 146 | // create two users 147 | 148 | var sachaId = Accounts.createUser({ 149 | profile: { 150 | name: 'Sacha Greif' 151 | }, 152 | username: "sacha", 153 | email: "sacha@example.com", 154 | password: "123456", 155 | }); 156 | 157 | var tomId = Accounts.createUser({ 158 | profile: { 159 | name: 'Tom Coleman' 160 | }, 161 | username: "tom", 162 | email: "tom@example.com", 163 | password: "123456", 164 | }); 165 | 166 | var sacha = Meteor.users.findOne(sachaId); 167 | var tom = Meteor.users.findOne(tomId); 168 | 169 | var telescopeId = Posts.insert({ 170 | title: 'Introducing Telescope', 171 | userId: sacha._id, 172 | author: sacha.profile.name, 173 | url: 'http://sachagreif.com/introducing-telescope/', 174 | submitted: new Date(now - 7 * 3600 * 1000), 175 | commentsCount: 2, 176 | upvoters: [], votes: 0 177 | }); 178 | 179 | Comments.insert({ 180 | postId: telescopeId, 181 | userId: tom._id, 182 | author: tom.profile.name, 183 | submitted: new Date(now - 5 * 3600 * 1000), 184 | body: 'Interesting project Sacha, can I get involved?' 185 | }); 186 | 187 | ``` 188 | 189 | Shut down Meteor with `Ctrl+C` and do a `meteor reset` and start it back up again with `meteor`. 190 | 191 | Now let's log in **as Sacha** and see what happens. Once logged in, click on the `Accounts` link on the left. 192 | 193 | ![enter image description here](https://lh3.googleusercontent.com/J5K9jjcJU_0KKQVfBcqfXiUNrqcto6slnUyOZ6O1Lks=s0 "Screenshot from 2015-07-23 23:41:12.png") 194 | 195 | - Nice. Absolutely nothing shows up except the `Accounts` collection. Microscope has a `Posts` collection as well as a `Comments` collection. 196 | 197 | - ... and WTF. Tom is an `admin` but Sacha is not. We didn't even tell Meteor to make Tom an `admin` in my fixtures code, so what the hell happened? 198 | 199 | It turns out that by default, OrionJS will create a user `Role` called `admin` and if there is no `admin`, will assign the **first** user created with `Accounts.createUser` as an `admin`. Remember this so you're not creating accidental admin users in your fixtures code. 200 | 201 | I can't spell Sasha's name for crap so let's remove him as admin and have Tom as admin. Because ignoring your spelling weaknesses make them go away. 202 | 203 | ## Adding and Removing Roles from Users ## 204 | 205 | Unlike other user roles packages, the `role` of the user in `OrionJS` isn't stored on the `user` object itself. You won't find something like: 206 | 207 | ```javascript 208 | { 209 | username: 'cletus', 210 | email: 'ileik@mysister.com', 211 | roles: [ 212 | 'admin', 213 | 'parent', 214 | 'varmit_hunter' 215 | ] 216 | } 217 | ``` 218 | Instead, each separate `role` is stored in a `Roles` collection and the `userId` of the user is referenced along with an array containing their `roles`. 219 | 220 | ### Getting Roles### 221 | Let's screw around in the Chrome console before we do anything. While in the `Accounts` admin page, do: 222 | 223 | ``` 224 | var id = Meteor.users.findOne({username: "sacha"})._id; 225 | Roles.userHasRole(id, "admin") 226 | 227 | true 228 | ``` 229 | It should return `true`. We got Sesha's userId and then used that ID to check if he has the role of "admin." 230 | 231 | Likewise, each user has a `roles()` and `hasRole()` method on its object: 232 | 233 | ``` 234 | // gets the currently logged in user, which should be Sassha 235 | var user = Meteor.user(); 236 | user.roles(); 237 | 238 | ["admin"] 239 | 240 | user.hasRole("admin") 241 | 242 | true 243 | ``` 244 | So that's how you check if a user has a role you want. 245 | 246 | Currently there doesn't seem to be a method to check *which* users have a certain role. 247 | 248 | ###Setting Roles### 249 | 250 | We need to give Tom the role of `admin`. 251 | 252 | In Chrome console: 253 | ``` 254 | var user = Meteor.users.findOne({username: "tom"}); 255 | Roles.addUserToRoles( user._id , ["admin"] ); 256 | 257 | VM11445:2 Uncaught TypeError: Roles.addUserToRoles is not a function(anonymous function) 258 | ``` 259 | DUH. You can't define roles on the client. Because that's dumb. 260 | 261 | Let's make a new file called `/server/admin.js` 262 | ```javascript 263 | /server/admin.js 264 | 265 | var tom = Meteor.users.findOne({username: 'tom'}); 266 | Roles.addUserToRoles( tom._id , ["admin"] ); 267 | ``` 268 | 269 | If we go back to the OrionJS admin console we should see that now Tom and Sache are both admins. 270 | 271 | Now we want to remove Sachet as an admin. 272 | 273 | ```javascript 274 | /server/admin.js 275 | 276 | var tom = Meteor.users.findOne({username: 'tom'}); 277 | Roles.addUserToRoles( tom._id , ["admin"] ); 278 | 279 | var nameIcantSpel = Meteor.users.findOne({username: 'sacha'}); 280 | Roles.removeUserFromRoles( nameIcantSpel._id, ["admin"] ); 281 | ``` 282 | You'll notice that you can log into `OrionJS` as Sacsh, but you won't see `Accounts` on the sidebar since he's no longer an `admin`. So log in as Tom instead. 283 | 284 | And now Tom is the only admin! 285 | 286 | ![enter image description here](https://lh3.googleusercontent.com/2cXRDE9AILHEWVhLumU2Fei-Tzf67Uwl8rOCZGfcsOk=s0 "Screenshot from 2015-07-24 00:26:13.png") 287 | 288 | Spelling weakness successfully ignored! 289 | 290 | ##Adding Collections to OrionJS## 291 | 292 | Here's where this tutorial gets less horrible. 293 | 294 | Microscope has a `Posts` and `Comments` collection, but both aren't visible yet in the admin thingy. That's because they're not `Orion` collections yet. 295 | 296 | Let's make them appear. 297 | 298 | ```javascript 299 | // This is what it used to be: 300 | // Posts = new Mongo.Collection('posts'); 301 | 302 | // Instead let's do: 303 | Posts = new orion.collection('posts', { 304 | singularName: 'post', // The name of one of these items 305 | pluralName: 'posts', // The name of more than one of these items 306 | link: { 307 | // * 308 | // * The text that you want to show in the sidebar. 309 | // * The default value is the name of the collection, so 310 | // * in this case it is not necessary. 311 | 312 | title: 'Posts' 313 | }, 314 | /** 315 | * Tabular settings for this collection 316 | */ 317 | tabular: { 318 | // here we set which data columns we want to appear on the data table 319 | // in the CMS panel 320 | columns: [ 321 | { 322 | data: "title", 323 | title: "Title" 324 | },{ 325 | data: "author", 326 | title: "Author" 327 | },{ 328 | data: "submitted", 329 | title: "Submitted" 330 | }, 331 | ] 332 | } 333 | }); 334 | 335 | Posts.allow({ 336 | update: function(userId, post) { return ownsDocument(userId, post); }, 337 | remove: function(userId, post) { return ownsDocument(userId, post); }, 338 | }); 339 | 340 | Posts.deny({ 341 | update: function(userId, post, fieldNames) { 342 | // may only edit the following two fields: 343 | ``` 344 | If you go back to the Microscope home page you'll see that everything appears to have remained normal. Orion collections are just extended Mongo collections. 345 | 346 | Now it looks like we got `Posts` appearing in `OrionJS`. 347 | 348 | ![enter image description here](https://lh3.googleusercontent.com/6T99yNvDLm2qxDp-ZLF8W0V9L64Op8W8-UAvuOQlEqo=s0 "Screenshot from 2015-07-24 00:37:51.png") 349 | 350 | Reactive search works. Sorting columns is working. Pagination is working. 351 | 352 | But when we click on a table item we get: 353 | 354 | ![enter image description here](https://lh3.googleusercontent.com/UPlVwdKLDXf2UBsX3JqLdTpoG989DelOau61HHB84MA=s0 "Screenshot from 2015-07-24 00:39:06.png") 355 | 356 | Errors and crap. 357 | 358 | Luckily the error is pretty descriptive. This form needs either a schema or a collection. 359 | 360 | ##Updating Collection Documents## 361 | 362 | When we click on one of the table items we expect to go to an update form for that particular item. `OrionJS` uses the vastly powerful `aldeed:autoform` package to generate its forms. `aldeed:autoform` in turn uses the `aldeed:simple-schema` package to know *how* to generate its forms. 363 | 364 | ###Schemas### 365 | 366 | Schemas are these little (tee-hee) things that define how the data in your database should be. If you've got a `User` document with a `first_name` property, you'd expect the value to be a `type: String`. If having a first name is critical, you'd want it to be `optional: false`. 367 | 368 | We use schemas to keep our data consistent. MongoDB is inherently a schema-less database. It would happily allow you to screw yourself over by storing an array of booleans inside the `first_name` property of your `user` document, for instance. And then you go to access it and your wife leaves you (probably not your husband because he's clueless). 369 | 370 | So this is why people decided to make a schema package for Meteor. They love you and want happy families. 371 | 372 | Let's start by defining a schema for our `Posts` collection all the way at the very bottom of `/lib/collections/posts.js`. I'm too lazy to type so read the comments. 373 | 374 | 375 | ```javascript 376 | /lib/collections/posts.js 377 | 378 | // Rest of the code above 379 | 380 | $addToSet: {upvoters: this.userId}, 381 | $inc: {votes: 1} 382 | }); 383 | 384 | if (! affected) 385 | throw new Meteor.Error('invalid', "You weren't able to upvote that post"); 386 | } 387 | }); 388 | 389 | /** 390 | * Now we will define and attach the schema for this collection. 391 | * Orion will automatically create the corresponding form. 392 | */ 393 | Posts.attachSchema(new SimpleSchema({ 394 | // We use `label` to put a custom label for this form field 395 | // Otherwise it would default to `Title` 396 | // 'optional: false' means that this field is required 397 | // If it's blank, the form won't submit and you'll get a red error message 398 | // 'type' is where you can set the expected data type for the 'title' key's value 399 | title: { 400 | type: String, 401 | optional: false, 402 | label: 'Post Title' 403 | }, 404 | // regEx will validate this form field according to a RegEx for a URL 405 | url: { 406 | type: String, 407 | optional: false, 408 | label: 'URL', 409 | regEx: SimpleSchema.RegEx.Url 410 | }, 411 | // autoform determines other aspects for how the form is generated 412 | // In this case we're making this field hidden from view 413 | userId: { 414 | type: String, 415 | optional: false, 416 | autoform: { 417 | type: "hidden", 418 | label: false 419 | }, 420 | }, 421 | author: { 422 | type: String, 423 | optional: false, 424 | }, 425 | // 'type: Date' means that this field is expecting a data as an entry 426 | submitted: { 427 | type: Date, 428 | optional: false, 429 | }, 430 | commentsCount: { 431 | type: Number, 432 | optional: false 433 | }, 434 | // 'type: [String]' means this key's value is an array of strings' 435 | upvoters: { 436 | type: [String], 437 | optional: true, 438 | autoform: { 439 | disabled: true, 440 | label: false 441 | }, 442 | }, 443 | votes: { 444 | type: Number, 445 | optional: true 446 | }, 447 | })); 448 | ``` 449 | 450 | I told you schemas are little, right? 451 | 452 | Save and try clicking on a post item again. 453 | 454 | ![enter image description here](https://lh3.googleusercontent.com/xUKRDnSsT3THV3fi-HocLdl3uAwkTyt_YRC_nswc8eA=s0 "Screenshot from 2015-07-24 03:57:39.png") 455 | 456 | Ohhhhh... crap! It's almost like I... planned... things. 457 | 458 | Play around with this form and look at how the `schema` we defined directly correlates to how this form was generated. 459 | 460 | - make the `Post Title` blank and save the form 461 | - remove `http` in `URL` and save the form 462 | - click on the `Submitted` field to see how `type: Date` works 463 | 464 | ##Adding Comments Collection## 465 | 466 | How about we do the same thing to the `Comments` collection as we did to the `Posts` collection? 467 | 468 | I'll race you. Ready? Go. 469 | Done I WIN. 470 | 471 | ```javascript 472 | /lib/collections/comments.js 473 | 474 | Comments = new orion.collection('comments', { 475 | singularName: 'comment', // The name of one of these items 476 | pluralName: 'comments', // The name of more than one of these items 477 | link: { 478 | // * 479 | // * The text that you want to show in the sidebar. 480 | // * The default value is the name of the collection, so 481 | // * in this case it is not necessary. 482 | 483 | title: 'Comments' 484 | }, 485 | /** 486 | * Tabular settings for this collection 487 | */ 488 | tabular: { 489 | // here we set which data columns we want to appear on the data table 490 | // in the CMS panel 491 | columns: [ 492 | { 493 | data: "author", 494 | title: "Author" 495 | },{ 496 | data: "postId", 497 | title: "Post ID" 498 | },{ 499 | data: "submitted", 500 | title: "Submitted" 501 | }, 502 | ] 503 | } 504 | }); 505 | 506 | Meteor.methods({ 507 | commentInsert: function(commentAttributes) { 508 | check(this.userId, String); 509 | check(commentAttributes, { 510 | postId: String, 511 | body: String 512 | }); 513 | 514 | var user = Meteor.user(); 515 | var post = Posts.findOne(commentAttributes.postId); 516 | 517 | if (!post) 518 | throw new Meteor.Error('invalid-comment', 'You must comment on a post'); 519 | 520 | comment = _.extend(commentAttributes, { 521 | userId: user._id, 522 | author: user.username, 523 | submitted: new Date() 524 | }); 525 | 526 | // update the post with the number of comments 527 | Posts.update(comment.postId, {$inc: {commentsCount: 1}}); 528 | 529 | // create the comment, save the id 530 | comment._id = Comments.insert(comment); 531 | 532 | // now create a notification, informing the user that there's been a comment 533 | createCommentNotification(comment); 534 | 535 | return comment._id; 536 | } 537 | }); 538 | 539 | /** 540 | * Now we will define and attach the schema for that collection. 541 | * Orion will automatically create the corresponding form. 542 | */ 543 | Comments.attachSchema(new SimpleSchema({ 544 | postId: { 545 | type: String, 546 | optional: false, 547 | label: 'Post ID' 548 | }, 549 | userId: { 550 | type: String, 551 | optional: false, 552 | label: 'User ID', 553 | }, 554 | author: { 555 | type: String, 556 | optional: false, 557 | }, 558 | submitted: { 559 | type: Date, 560 | optional: false, 561 | }, 562 | body: { 563 | type: String, 564 | optional: false, 565 | } 566 | })); 567 | ``` 568 | 569 | ![enter image description here](https://lh3.googleusercontent.com/LROiQg6mMP5rpG844QNv-njdxyB5ltffUASPpbx606M=s0 "Screenshot from 2015-07-24 04:33:38.png") 570 | 571 | ![enter image description here](https://lh3.googleusercontent.com/89A5cA83MF5WZn-69hoq2yqPvh5hvllTXyTpuQMuJJU=s0 "Screenshot from 2015-07-24 04:34:43.png") 572 | 573 | How about we do something even more CRAZY and add in a text editor for the `Body` field? 574 | 575 | ##Custom Input Types (Widgets)## 576 | 577 | First, some background. 578 | 579 | Forms have standard input types. Checkboxes. Radio buttons. Text. These are all supported out of the box by `aldeed:autoform`. But things like text editors (with buttons for selecting font type, size, color, etc) are custom, and `autoform` gives us a way to define our own widgets in the `schema` so that autoform can generate that form for us. 580 | 581 | ###Adding Summernote### 582 | 583 | First do `meteor add orionjs:summernote` 584 | 585 | Now do this to the `body` property: 586 | 587 | ```javascript 588 | /lib/collections/comments.js 589 | 590 | // now create a notification, informing the user that there's been a comment 591 | createCommentNotification(comment); 592 | 593 | return comment._id; 594 | } 595 | }); 596 | 597 | /** 598 | * Now we will attach the schema for that collection. 599 | * Orion will automatically create the corresponding form. 600 | */ 601 | Comments.attachSchema(new SimpleSchema({ 602 | postId: { 603 | type: String, 604 | optional: false, 605 | label: 'Post ID' 606 | }, 607 | userId: { 608 | type: String, 609 | optional: false, 610 | label: 'User ID', 611 | }, 612 | author: { 613 | type: String, 614 | optional: false, 615 | }, 616 | submitted: { 617 | type: Date, 618 | optional: false, 619 | }, 620 | body: orion.attribute('summernote', { 621 | label: 'Body' 622 | }), 623 | })); 624 | ``` 625 | Click on the comment by Sashi in the OrionJS admin panel to see what's up. 626 | 627 | ![enter image description here](https://lh3.googleusercontent.com/unDs4gjPq22hcncc11LY_Cp9eTD5RBh9G6IChwiVMYo=s0 "Screenshot from 2015-07-24 18:30:25.png") 628 | 629 | Niiiice. 630 | 631 | ###Orion Attributes### 632 | 633 | So what the hell is the `orion.attribute` we adding as the value for the `body` key in our schema? How about we just use our Chrome Console? 634 | ``` 635 | orion.attribute('summernote', { 636 | label: 'Body' 637 | }) 638 | 639 | Object {label: "Body", type: function, orionAttribute: "summernote", ...} 640 | autoform: Object 641 | label: "Body" 642 | orionAttribute: "summernote" 643 | type: function String() { [native code] } 644 | __proto__: Object 645 | ``` 646 | Oh. looks like by adding the `orionjs:summernote` package we got access to this method that returns a pre-made object for us that we can conveniently use in our schema. Remember that `aldeed:autoform` uses the schema to figure out *how* to generate form items, so this attribute did all the defining-the-input-widget stuff for us. 647 | 648 | `orion.attribute('nameOfAnAttribute', optionalObjectToExtendThisAttributeWith)` 649 | 650 | I'm going to make this comment fabulous. ROYGBIV and Comic Sans the crap out of that comment, Sechie. 651 | 652 | ![enter image description here](https://lh3.googleusercontent.com/eVMz0FVebRmy05ZPPU9p3b_91o56QjbK7GfgK0mIvaA=s0 "Screenshot from 2015-07-24 18:37:15.png") 653 | 654 | Save it. 655 | 656 | Remember that this comment is now in HTML, so we have to add triple spacebars `{{{ TRIPLEX #vindiesel }}}` in our `comment_item.html` to make sure Spacebars will render the HTML properly. 657 | 658 | ```html 659 | /client/templates/comments/comment_item.html 660 | 661 | 670 | ``` 671 | Let's survey the improvements by going to the main page and clicking on the comments for the `Introducing Telescope` post. 672 | 673 | ![enter image description here](https://lh3.googleusercontent.com/8ja1upLimMWalVzUCAMWNmtULEb-xpvPzOiih5l99wg=s0 "Screenshot from 2015-07-24 18:46:03.png") 674 | 675 | Beauty. 676 | 677 | ###Adding Images to Amazon S3 (updated 07/28/2015)### 678 | 679 | No comment will be complete without image spamming. 680 | 681 | Some notes: 682 | 683 | - currently, uploading images directly from Summernote doesn't work 684 | - images will be hosted through Amazon S3. I don't go over how to do it with `GridFS` or other filesystem packages. 685 | 686 | `meteor add orionjs:image-attribute orionjs:s3` 687 | 688 | ####Setting up S3#### 689 | 690 | You're going to want to follow this tutorial FIRST to set up your Amazon S3: 691 | 692 | https://github.com/Lepozepo/S3/#amazon-s3-uploader 693 | 694 | Add the AWS credentials in `http://localhost:3000/admin/config` and you are ready. 695 | 696 | Now it's schema time again since we want to add a file upload section to our Comment Update form! 697 | 698 | ```javascript 699 | /lib/collections/comments.js 700 | 701 | author: { 702 | type: String, 703 | optional: false, 704 | }, 705 | submitted: { 706 | type: Date, 707 | optional: false, 708 | }, 709 | body: orion.attribute('summernote', { 710 | label: 'Body' 711 | }), 712 | image: orion.attribute('image', { 713 | optional: true, 714 | label: 'Comment Image' 715 | }), 716 | })); 717 | ``` 718 | 719 | Go into a comment in the admin panel and upload something! 720 | 721 | ![enter image description here](https://lh3.googleusercontent.com/ZY2zJWFCdv25VDgbAE3yjPxD-ByUIngSW9-SEuzloaI=s0 "Screenshot from 2015-07-29 00:04:39.png") 722 | 723 | Make sure to click on the `Save` button after you're done. Also note that the moment you select an image the Amazon uploader will start. You'll see a progress bar. 724 | 725 | Finally, we should go see what it looks like on the main page, but remember to modify the `comment_item` template with the new `image` key that a `comment` document now has. 726 | 727 | ``` 728 | /client/templates/comments/comment_item.html 729 | 730 | 742 | ``` 743 | 744 | Voila! 745 | 746 | ![enter image description here](https://lh3.googleusercontent.com/bKct-GlqtHXJYrfQkoiaZrq1XH0D1Q08UyH_PUgPZYE=s0 "Screenshot from 2015-07-29 00:05:07.png") 747 | 748 | Sassha and Tom are going to KILL me. 749 | 750 | ##Changing Tabular Templates (updated 7/29/2015)## 751 | 752 | If we go back to our admin panel and look at comments, we see that the table is pretty dumb: 753 | 754 | `![enter image description here](https://lh3.googleusercontent.com/LROiQg6mMP5rpG844QNv-njdxyB5ltffUASPpbx606M=s0 "Screenshot from 2015-07-24 04:33:38.png") 755 | 756 | 1. The `Submitted` column contains WAY too much information. Something like Month-Day-Year-Time would look nicer. I'm going to completely ignore you people who do it the more logical way of Time-Day-Month-Year because, uh, freedom. 757 | 758 | 2. We also have the issue of the `Post ID` column being essentially stupid. I'd prefer if that column contained the title of the Post instead. 759 | 760 | 3. I also want a column that shows a short blurb of the comment's `body`, something like `Interesting project Sacha, can I...` 761 | 762 | Let's tackle #1 first. If you guessed that we need to go back into our `Schema` to change this, you just WON the JACKPOT of zero money. 763 | 764 | ###orion.attributeColumn()### 765 | 766 | We are interested in: 767 | 768 | orion.attributeColumn('createdAt', 'submitted', 'FREEDOM!!!'), 769 | 770 | ```javascript 771 | /lib/collections/comments.js 772 | 773 | Comments = new orion.collection('comments', { 774 | singularName: 'comment', // The name of one of these items 775 | pluralName: 'comments', // The name of more than one of these items 776 | link: { 777 | // * 778 | // * The text that you want to show in the sidebar. 779 | // * The default value is the name of the collection, so 780 | // * in this case it is not necessary. 781 | 782 | title: 'Comments' 783 | }, 784 | /** 785 | * Tabular settings for this collection 786 | */ 787 | tabular: { 788 | // here we set which data columns we want to appear on the data table 789 | // in the CMS panel 790 | columns: [ 791 | { 792 | data: "author", 793 | title: "Author" 794 | },{ 795 | data: "postId", 796 | title: "Post ID" 797 | }, 798 | orion.attributeColumn('createdAt', 'submitted', 'FREEDOM!!!'), 799 | ] 800 | } 801 | }); 802 | ``` 803 | 804 | Let's check it out! 805 | 806 | ![enter image description here](https://lh3.googleusercontent.com/YVicjxMpRLn3A5J1xNI-PRRQGWjCdtt_u6E77QHEp9A=s0 "Screenshot from 2015-07-24 20:10:05.png") 807 | 808 | This handy-dandy method goes like this: 809 | 810 | `orion.attributeColumn('nameOfTemplate', 'keyNameOnYourObject', 'columnLabel')` 811 | 812 | Think about it. Meteor uses templates to display stuff. We had a crazy long date and we wanted to change the *look* of it, so using a template makes sense. 813 | 814 | Luckily, `OrionJS` comes with some pre-made templates. One of them happens to be called `createdAt`. 815 | 816 | `createdAt` wants a `Date` object, which happens to reside in the `submitted` key of each of your documents in the `Comments` collection. Lastly, we tell it what we want our column label to be. 817 | 818 | Now, some freedom-hating people probably want a custom template for Time-Day-Month-Year. Let's get to that next. 819 | 820 | ###Custom Tabular Templates### 821 | 822 | Now onto issue #2 - the `Post ID` column should be the title of the Post instead. 823 | 824 | Open up Chrome Console and type in `Comments.findOne()`. It found a comment, right? 825 | 826 | Now do `Posts.findOne()`. Hmmm... no post found. That's because this route isn't subscribed to anything in the `Posts` collection. If you've done Meteor before, chances are you've been using `Iron Router` to manage your data subscriptions for your routes. Unfortunately, since OrionJS is a package, it's difficult to tap into and modify the generated routes that OrionJS has already made for us. So what do we do? 827 | 828 | ####Template-Level Subscriptions#### 829 | 830 | Meteor can subscribe to data in normal template callbacks (`onRendered, onCreated`). And as it turns out, OrionJS has a very standardized template naming scheme*. 831 | 832 | `collections.myCollection.index` - the main page that lists all the items in the collection 833 | `collections.myCollection.create` - the form for creating a new item in the collection 834 | `collections.myCollection.update` - the form for updating an existing item in the collection 835 | `collections.myCollection.delete` - the form/page for deleting an existing item in the collection 836 | 837 | These, by the way, are your standard pages for CRUD actions. 838 | 839 | *These aren't necessarily the actual names of the templates, but the `identifier` that OrionJS uses to find the actual template. 840 | 841 | BTW, if you want to see a list of all the route names that are registered with Iron Router, open up Chrome Console and do: 842 | 843 | ```javascript 844 | _.each(Router.routes, function(route){ 845 | console.log(route.getName()); 846 | }); 847 | ``` 848 | 849 | Here are some more identifiers: http://orionjs.org/docs/customization#overridetemplates 850 | 851 | So let's use this to subscribe to the `Posts` collection on the `comments.index` template. 852 | 853 | ```javascript 854 | /client/templates/orion/comments_index.js 855 | 856 | ReactiveTemplates.onCreated('collections.comments.index', function() { 857 | 858 | this.subscribe('posts', {sort: {submitted: -1, _id: -1}, limit: 0}); 859 | 860 | }); 861 | ``` 862 | 863 | What this is saying is that when the template with an identifier of 'collections.comments.index' (the `Comments` index page) is created, subscribe the template to the data you specify. 864 | 865 | Now go back to the `Comments` index page and do `Posts.find().count()` to see that you've got `Posts` now! 866 | 867 | ####Meteor Tabular Render#### 868 | 869 | So now that this page has the data we need, how do we actually change the contents of the cell itself? 870 | 871 | `OrionJS` uses `aldeed:meteor-tabular` to show its datatables, and it just so happens that this latter package provides a way to change the cell value: https://github.com/aldeed/meteor-tabular#example 872 | 873 | ```javascript 874 | /lib/collections/comments.js 875 | 876 | Comments = new orion.collection('comments', { 877 | singularName: 'comment', // The name of one of these items 878 | pluralName: 'comments', // The name of more than one of these items 879 | link: { 880 | title: 'Comments' 881 | }, 882 | /** 883 | * Tabular settings for this collection 884 | */ 885 | tabular: { 886 | // here we set which data columns we want to appear on the data table 887 | // in the CMS panel 888 | columns: [ 889 | { 890 | data: "author", 891 | title: "Author" 892 | },{ 893 | data: "postId", 894 | title: "Post Title", 895 | render: function (val, type, doc) { 896 | var postId = val; 897 | var postTitle = Posts.findOne(postId).title; 898 | return postTitle; 899 | } 900 | }, 901 | orion.attributeColumn('createdAt', 'submitted', 'FREEDOM!!!'), 902 | ] 903 | }, 904 | }); 905 | ``` 906 | Go play around inside this function. `console.log` `val`, `type`, and `doc` to see what they are: 907 | 908 | ```javascript 909 | { 910 | data: "postId", 911 | title: "Post Title", 912 | render: function (val, type, doc) { 913 | var postId = val; 914 | var postTitle = Posts.findOne(postId).title; 915 | return postTitle; 916 | } 917 | } 918 | ``` 919 | `data: "postId"` is critical here because that's how `val` gets its value. 920 | 921 | After you're through go back and look at the `Comments` index page: 922 | 923 | ![enter image description here](https://lh3.googleusercontent.com/xuT9mpGBby25enedfg64fTEyuVWIXP_jFPXokNKpkx0=s0 "Screenshot from 2015-07-29 17:06:16.png") 924 | 925 | ####Meteor Tabular with Actual Templates#### 926 | 927 | Finally, onto #3. We want a column that shows a short blurb of the comment `body`, something like `Interesting project Sacha, can I...`. This is called `truncating` a string. 928 | 929 | I want to create an actual template for this: 930 | 931 | ```html 932 | /client/templates/orion/comments_index_blurb_cell.html 933 | 934 | 937 | ``` 938 | 939 | Hold on there Skippy! Truncating a straight string that's, say, 100 characters long into one that's 15 characters long with a `...` at the end is fairly straightforward. But remember that we added Summernote and we have a *fabulous* comment? 940 | 941 | ![enter image description here](https://lh3.googleusercontent.com/8ja1upLimMWalVzUCAMWNmtULEb-xpvPzOiih5l99wg=s0 "Screenshot from 2015-07-24 18:46:03.png") 942 | 943 | The HTML for this comment actually looks like: 944 | 945 | ```html 946 |

You sure can Tom!!!

947 | ``` 948 | 949 | Sooo... we can't just do a simple truncate of this down to 15 characters. I mean, we can... 950 | 951 | ...IF WE'RE NUBZ! 952 | 953 | But `pathable` is not a nub: 954 | 955 | https://github.com/pathable/truncate 956 | 957 | Go to `/client/javascript` and literally just chuck this script's `jquery.truncate.js` file in there. Meteor will take care of minifying and loading this script automatically onto your page, as it does with *all* javascript that's not in the `/public` folder. 958 | 959 | And now we can go ahead and create a helper for our template: 960 | 961 | ```javascript 962 | /client/templates/orion/comments_index_blurb_cell.js 963 | 964 | Template.commentsIndexBlurbCell.helpers({ 965 | 966 | blurb: function(){ 967 | var blurb = jQuery.truncate(this.body, { 968 | length: 15 969 | }); 970 | return blurb 971 | } 972 | 973 | }); 974 | 975 | ``` 976 | 977 | Finally, we go back to modify our `tabular` object: 978 | 979 | ```javascript 980 | /lib/comments.js 981 | 982 | Comments = new orion.collection('comments', { 983 | singularName: 'comment', // The name of one of these items 984 | pluralName: 'comments', // The name of more than one of these items 985 | link: { 986 | title: 'Comments' 987 | }, 988 | /** 989 | * Tabular settings for this collection 990 | */ 991 | tabular: { 992 | // here we set which data columns we want to appear on the data table 993 | // in the CMS panel 994 | columns: [ 995 | { 996 | data: "author", 997 | title: "Author" 998 | },{ 999 | data: "postId", 1000 | title: "Post Title", 1001 | render: function (val, type, doc) { 1002 | var postId = val; 1003 | var postTitle = Posts.findOne(postId).title; 1004 | return postTitle; 1005 | } 1006 | },{ 1007 | data: "body", 1008 | title: "Comment", 1009 | tmpl: Meteor.isClient && Template.commentsIndexBlurbCell 1010 | }, 1011 | orion.attributeColumn('createdAt', 'submitted', 'FREEDOM!!!'), 1012 | ] 1013 | }, 1014 | }); 1015 | ``` 1016 | `data: "body"` subscribes us to the values for the `body` key so that it's available for our template. 1017 | 1018 | `tmpl: Meteor.isClient && Template.commentsIndexBlurbCell` looks kind of weird but remember that this code is in `/lib`, which runs on both the client and server, and `Template` isn't defined on the server. So that's why `aldeed:meteor-tabular` requires you to do this `Meteor.isClient` thing. 1019 | 1020 | WABAM! 1021 | 1022 | ![enter image description here](https://lh3.googleusercontent.com/_DiQmOxmyTqnDpR0lpfP3WC-D1TadvsDG08J_ApZvmg=s0 "Screenshot from 2015-07-29 17:53:03.png") 1023 | 1024 | And... done. 1025 | 1026 | ##Dictionary (updated 7/28/2015)## 1027 | 1028 | Let's go back to the Microscope main page. Say that you wanted to add a little description blurb after the word `Microscope` at the top left. 1029 | 1030 | - You not only want to add a description, but you want to be able to periodically change it as well AND you want text formatting on it AND you don't want to touch any code to change it - all you want is to change it from the OrionJS admin panel from inside of an update form. 1031 | 1032 | - And since I'm rolling right now, let's be able to change the `Microscope` word as well. 1033 | 1034 | - AND since people like to sue, let's add a terms and conditions blurb at the bottom of every `Post Submit` page that our lawyers can periodically update with worse and worse conditions for the consumer. 1035 | 1036 | Sounds like we need a collection, a schema, and a dictionary of some sort that keeps track of lolidontknowhowtoexplainjustkeepreading. 1037 | 1038 | Create a new file: 1039 | ```javascript 1040 | /lib/orion_dictionary.js 1041 | 1042 | orion.dictionary.addDefinition('title', 'mainPage', { 1043 | type: String, 1044 | label: 'Site Title', 1045 | optional: false, 1046 | min: 1, 1047 | max: 40 1048 | }); 1049 | 1050 | orion.dictionary.addDefinition('description', 'mainPage', 1051 | orion.attribute('summernote', { 1052 | label: 'Site Description', 1053 | optional: true 1054 | }) 1055 | ); 1056 | 1057 | orion.dictionary.addDefinition('termsAndConditions', 'submitPostPage', 1058 | orion.attribute('summernote', { 1059 | label: 'Terms and Conditions', 1060 | optional: true 1061 | }) 1062 | ); 1063 | ``` 1064 | 1065 | `orion.dictionary.addDefinition()` takes three arguments: 1066 | ``` 1067 | orion.dictionary.addDefinition( 1068 | nameOfYourDictionaryItem, 1069 | categoryOfYourDictionaryItem, 1070 | schemaForYourDictionaryItem 1071 | ); 1072 | ``` 1073 | You'll see how this pans out in a little bit. 1074 | ``` 1075 | /client/templates/includes/header.html 1076 | 1077 | 1120 | ``` 1121 | Let's look at the damage! 1122 | 1123 | Looks like a new entry called `Dictionary` was created. And `nameOfYourDictionaryItem, categoryOfYourDictionaryItem,` and `schemaForYourDictionaryItem` are being used from `orion.dictionary.addDefinition()`. Go ahead and mess around with the forms. 1124 | 1125 | ![enter image description here](https://lh3.googleusercontent.com/Fda7VeIxXRViSV6JjEZY3dR3A2cP2pnZ__5GjtAWdJE=s0 "Screenshot from 2015-07-28 00:39:13.png") 1126 | 1127 | ![enter image description here](https://lh3.googleusercontent.com/T-PkZDKttbIDYYxBldnpI-XYJJSVk6MqxaHFCpoTxNI=s0 "Screenshot from 2015-07-28 00:39:35.png") 1128 | 1129 | Now for the front-end! 1130 | 1131 | ![enter image description here](https://lh3.googleusercontent.com/nMwCXcpq0ntrzcfVB-EB_9w1JX02jIoq3D-wE6BAfrk=s0 "Screenshot from 2015-07-28 00:40:10.png") 1132 | 1133 | ![enter image description here](https://lh3.googleusercontent.com/cSMgyE6l0hrFCJ8Mzuoot8teEcptPP3AFb9btg5Vq3M=s0 "Screenshot from 2015-07-28 00:39:57.png") 1134 | 1135 | So PRO! The clipping-off of the T&C gives legitimacy and trustworthiness to the site. 1136 | 1137 | ##Relationships## 1138 | 1139 | [Additional Documentation](https://github.com/orionjs/documentation/blob/master/docs/attributes/relationships.md) 1140 | 1141 | OrionJS has the ability to define two types of relationships between collection objects, `hasOne` and `hasMany`. You can use these relationships to easily do CRUD between collections inside of the admin backend. 1142 | 1143 | To add the ability to define these two relationships, do: 1144 | 1145 | `meteor add orionjs:relationships` 1146 | 1147 | In the case of Microscope: 1148 | 1149 | `Posts` has many `Comments` 1150 | `Comments` has one `Post` 1151 | 1152 | ###hasOne### 1153 | 1154 | Let's do `hasOne` first. Just like with a traditional SQL database, the relationships are defined in the schema as well. 1155 | 1156 | ```javascript 1157 | /lib/collections/comments.js 1158 | 1159 | Comments.attachSchema(new SimpleSchema({ 1160 | // here is where we define `a comment has one post` 1161 | // Each document in Comment has a postId 1162 | postId: orion.attribute('hasOne', { 1163 | // the label is the text that will show up on the Update form's label 1164 | label: 'Post' 1165 | }, { 1166 | // specify the collection you're making the relationship with 1167 | collection: Posts, 1168 | // the key whose value you want to show for each Post document on the Update form 1169 | titleField: 'title', 1170 | // dunno 1171 | publicationName: 'someRandomString', 1172 | }), 1173 | userId: { 1174 | type: String, 1175 | optional: false, 1176 | label: 'User ID', 1177 | }, 1178 | author: { 1179 | type: String, 1180 | optional: false, 1181 | }, 1182 | submitted: { 1183 | type: Date, 1184 | optional: false, 1185 | }, 1186 | body: orion.attribute('summernote', { 1187 | label: 'Body' 1188 | }), 1189 | })); 1190 | ``` 1191 | 1192 | ####Chicken and the Egg#### 1193 | 1194 | By now I hope that things have blown up in the server: 1195 | 1196 | `W20150731-06:35:14.565(-7)? (STDERR) ReferenceError: Posts is not defined` 1197 | 1198 | Let's do a quick summary of our two files, `comments.js` and `posts.js`: 1199 | 1200 | ```javascript 1201 | /lib/collections/comments.js 1202 | 1203 | Comments = new orion.collection('comments', { 1204 | 1205 | // creates the collection and defines how the collection is represented as a table in OrionJS 1206 | 1207 | }); 1208 | 1209 | Comments.attachSchema(new SimpleSchema({ 1210 | 1211 | // defines the expected data types for each value inside a Comment document 1212 | // defines the relationship of a Comment document to a Post document 1213 | 1214 | }); 1215 | ``` 1216 | ```javascript 1217 | /lib/collections/posts.js 1218 | 1219 | Posts = new orion.collection('posts', { 1220 | 1221 | // creates the collection and defines how the collection is represented as a table in OrionJS 1222 | 1223 | }); 1224 | 1225 | Posts.attachSchema(new SimpleSchema({ 1226 | 1227 | // defines the expected data types for each value inside a Post document 1228 | // defines the relationship of a Post document to a Comment document 1229 | 1230 | }); 1231 | ``` 1232 | 1233 | See the problem? 1234 | 1235 | Meteor will load `comments.js` first because, for files residing in the same folder, Meteor will load files first in numerical order and then in alphabetical order. The problem arises because the `Comments` schema defines the relationship of the `Comments` collection to the `Posts` collection. The code in `comments.js` is referencing `Posts`, which doesn't exist yet because `Post = new orion.collection('posts', {...})` in `Posts.js` hasn't run yet. Basically, Meteor's trying to do this: 1236 | 1237 | 1. create the `Comments` collection. 1238 | 2. define the `Comments` schema, which requires the `Posts` collection to exist. 1239 | 3. create the `Posts` collection. 1240 | 4. define the `Posts` schema, which will require the `Comments` collection to exist. 1241 | 1242 | So it errors out at `#2`. Well.... this is awkward. 1243 | 1244 | Ideally, we would like to do things in this order: 1245 | 1246 | 1. create the `Posts` and `Comments` collections. 1247 | 2. define the `Posts` and `Comments` schemas, which depend on the above collections existing beforehand. 1248 | 1249 | ####Correcting File Load Order#### 1250 | 1251 | Maybe we should change up our folder structure. I propose: 1252 | 1253 | `/lib/collections/declarations` 1254 | 1255 | -> `posts.js` and `comments.js` containing code to create both collections 1256 | 1257 | `/lib/collections/schemas` 1258 | 1259 | -> `posts.js` and `comments.js` containing code defining the schemas. The code in the `declarations` folder will run after the code in the `schemas` folder because teh alphabets. 1260 | 1261 | ```javascript 1262 | /lib/collections/declarations/comments.js 1263 | 1264 | Comments = new orion.collection('comments', {...}); 1265 | 1266 | Meteor.methods({...}); 1267 | ``` 1268 | 1269 | ```javascript 1270 | /lib/collections/declarations/posts.js 1271 | 1272 | Posts = new orion.collection('posts', {...}); 1273 | 1274 | Posts.allow({...}); 1275 | 1276 | Posts.deny({...}); 1277 | 1278 | validatePost = function (post) {...}; 1279 | 1280 | Meteor.methods({...}); 1281 | 1282 | ``` 1283 | 1284 | ```javascript 1285 | /lib/collections/schemas/comments.js 1286 | 1287 | Comments.attachSchema(new SimpleSchema({...})); 1288 | ``` 1289 | 1290 | ```javascript 1291 | /lib/collections/schemas/posts.js 1292 | 1293 | Posts.attachSchema(new SimpleSchema({...})); 1294 | ``` 1295 | 1296 | Ok, re-arrange your code according to the structure above and afterwards go to the admin backend and click on a comment: 1297 | 1298 | ![enter image description here](https://lh3.googleusercontent.com/4t50qpBophwbQ3q-gYqMaI2NDO6mJDYTlBNTgsc4Qok=s0 "Screenshot from 2015-07-28 03:24:36.png") 1299 | 1300 | Will you look at that... OrionJS was able to determine the specific `post` that this comment is related to and create a dropdown menu for us to change the post if we so desire. And it works - if you change the post and click Save you'll see the comment pop up in there. 1301 | 1302 | ###hasMany### 1303 | 1304 | One `Post` has many `Comments`. Let's go to the schema for `Posts` 1305 | 1306 | ```javascript 1307 | /lib/collections/schemas/posts.js 1308 | 1309 | Posts.attachSchema(new SimpleSchema({ 1310 | 1311 | comments: orion.attribute('hasMany', { 1312 | // the value inside the `comments` key will be an array of comment IDs 1313 | type: [String], 1314 | label: 'Comments for this Post', 1315 | // optional is true because you can have a post without comments 1316 | optional: true 1317 | }, { 1318 | collection: Comments, 1319 | titleField: 'body', 1320 | publicationName: 'someOtherRandomString' 1321 | }), 1322 | 1323 | // We use `label` to put a custom label for this form field 1324 | // Otherwise it would default to `Title` 1325 | // 'optional: false' means that this field is required 1326 | // If it's blank, the form won't submit and you'll get a red error message 1327 | // 'type' is where you can set the expected data type for the 'title' key's value 1328 | title: { 1329 | type: String, 1330 | optional: false, 1331 | label: 'Post Title' 1332 | }, 1333 | // regEx will validate this form field according to a RegEx for a URL 1334 | url: { 1335 | type: String, 1336 | optional: false, 1337 | label: 'URL', 1338 | regEx: SimpleSchema.RegEx.Url 1339 | }, 1340 | // autoform determines other aspects for how the form is generated 1341 | // In this case we're making this field hidden from view 1342 | userId: { 1343 | type: String, 1344 | optional: false, 1345 | autoform: { 1346 | type: "hidden", 1347 | label: false 1348 | }, 1349 | }, 1350 | author: { 1351 | type: String, 1352 | optional: false, 1353 | }, 1354 | // 'type: Date' means that this field is expecting a data as an entry 1355 | submitted: { 1356 | type: Date, 1357 | optional: false, 1358 | }, 1359 | commentsCount: { 1360 | type: Number, 1361 | optional: false 1362 | }, 1363 | // 'type: [String]' means this key's value is an array of strings' 1364 | upvoters: { 1365 | type: [String], 1366 | optional: true, 1367 | autoform: { 1368 | disabled: true, 1369 | label: false 1370 | }, 1371 | }, 1372 | votes: { 1373 | type: Number, 1374 | optional: true 1375 | }, 1376 | })); 1377 | ``` 1378 | Inside the same file disable this `deny` rule: 1379 | ```javascript 1380 | // Posts.deny({ 1381 | // update: function(userId, post, fieldNames) { 1382 | // // may only edit the following two fields: 1383 | // return (_.without(fieldNames, 'url', 'title').length > 0); 1384 | // } 1385 | // }); 1386 | ``` 1387 | 1388 | Let's see it! 1389 | 1390 | ![enter image description here](https://lh3.googleusercontent.com/uM36XR54Q94ZpFu5dwqukgRRb318eoMtvQ72Kfn4PTg=s0 "Screenshot from 2015-07-28 04:36:28.png") 1391 | 1392 | Niiiice. 1393 | 1394 | ###Multiple Relationships (updated 7/31/2015)### 1395 | 1396 | In the case of the `Comments` collection, a single `Comment` has one `Post`, which we defined above, but it also has one `User` (the comment author). So let's take the comment's `userId` field and create a `hasOne` relationship with a `comment`: 1397 | 1398 | ```javascript 1399 | /lib/collections/schemas/comments.js 1400 | 1401 | Comments.attachSchema(new SimpleSchema({ 1402 | // here is where we define `a comment has one post` 1403 | // Each document in Comment has a postId 1404 | postId: orion.attribute('hasOne', { 1405 | type: String, 1406 | // the label is the text that will show up on the Update form's label 1407 | label: 'Post', 1408 | // optional is false because you shouldn't have a comment without a post 1409 | // associated with it 1410 | optional: false 1411 | }, { 1412 | // specify the collection you're making the relationship with 1413 | collection: Posts, 1414 | // the key whose value you want to show for each Post document on the Update form 1415 | titleField: 'title', 1416 | // dunno 1417 | publicationName: 'someRandomString', 1418 | }), 1419 | // here is where we define `a comment has one user (author)` 1420 | // Each document in Comment has a userId 1421 | userId: orion.attribute('hasOne', { 1422 | type: String, 1423 | label: 'Author', 1424 | optional: false 1425 | }, { 1426 | collection: Meteor.users, 1427 | // the key whose value you want to show on the Update form 1428 | titleField: 'profile.name', 1429 | publicationName: 'anotherRandomString', 1430 | }), 1431 | author: { 1432 | type: String, 1433 | optional: false, 1434 | autoform: { 1435 | type: 'hidden', 1436 | label: false 1437 | } 1438 | }, 1439 | submitted: { 1440 | type: Date, 1441 | optional: false, 1442 | }, 1443 | body: orion.attribute('summernote', { 1444 | label: 'Body' 1445 | }), 1446 | image: orion.attribute('image', { 1447 | optional: true, 1448 | label: 'Comment Image' 1449 | }), 1450 | })); 1451 | ``` 1452 | 1453 | ![enter image description here](https://lh3.googleusercontent.com/3OSrvBsw4ScDzxyRmt5_hE7Nk5-EwDN7O3Iwz-azBxY=s0 "Screenshot from 2015-07-31 18:39:07.png") 1454 | 1455 | Ah, nice. So it looks like we got a drop-down menu that has already been pre-filled with the name of the author. 1456 | 1457 | Change the `Author` to Tom Coleman and press the `Save` button. 1458 | 1459 | When we change the author with this drop-down menu, the `userId` property in this comment document will be updated to the `userId` of the new author. 1460 | 1461 | But there's a problem because this comment document also has a field called `author`. 1462 | 1463 | ```javascript 1464 | { 1465 | _id: "DQEwf83xnAusEw2Nt", 1466 | postId: "CaagbmWYHH9Kp6w2P", 1467 | userId: "5uhuWaSFdMMWc6G2i", // this ID has been updated and connects to Tom Coleman 1468 | author: "Sacha Greif", // but this hasn't been changed to "Tom Coleman" 1469 | submitted: ISODate("2015-08-01T00:00:00Z"), 1470 | body: "

" 1471 | } 1472 | ``` 1473 | 1474 | The `author` property was originally set in the Meteor Method called `commentInsert`, and it was called when this comment was originally created in `/client/templates/comments/comment_submit.js`. 1475 | 1476 | Unfortunately we haven't made any functionality that updates the `author` string when the `userId` value gets changed, which leads us to... 1477 | 1478 | ####Limitations of Defining Relationships#### 1479 | 1480 | MongoDB is inherently non-relational and implementing hard-relations like in an SQL DB requires extra code (which isn't currently available in the `orionjs:relationships` package). Be very careful setting "relationships." You can easily get some marvelous data inconsistencies that will LITERALLY lead to the extinction of all cats, or at the very least the problem we have above. 1481 | 1482 | For a good read on modeling data, check this out: http://docs.mongodb.org/manual/core/data-model-design/ 1483 | 1484 | In addition to what we just mentioned: 1485 | 1486 | - If you remove a comment on the Update Post page, it will NOT automatically remove that post from the associated Update Comment page or on the main website. 1487 | 1488 | - Likewise, if you change the post for a particular comment in the Update Comment page, it will also not automatically reflect in the associated Post page or on the main website. 1489 | 1490 | To change the `author` value when the `userId` changes on a `Comments` document, use this code (kind of hacky): 1491 | 1492 | ```javascript 1493 | /server/collections/comments.js 1494 | 1495 | // When there is a change to userId, author gets updated 1496 | var query = Comments.find(); 1497 | var handle = query.observeChanges({ 1498 | changed: function(commentId, changedField){ 1499 | if(changedField.userId){ 1500 | var username = Meteor.users.findOne(changedField.userId).profile.name; 1501 | Comments.update({_id: commentId}, {$set: {author: username}}); 1502 | }; 1503 | } 1504 | }); 1505 | ``` 1506 | 1507 | ##Setting Roles and Permissions (updated 8/3/2015)## 1508 | 1509 | As a user with an `admin` role, you are able to do full CRUD on every single collection in Microscope, which includes user accounts, the dictionary, and other configuration variables like the AWS secret key. 1510 | 1511 | But sometimes you want to give an undervalued and underpaid employee different permissions so that they are still able to log into OrionJS and manage things for you without being able to see or update *everything* in your system. As an evil asshole you want to lock people out of certain things, ya know? 1512 | 1513 | So we're going to want to define another role in addition to the `admin` role. 1514 | 1515 | By default, any new role you create will be locked out of everything, which is why you want to create a role and then set some `allow` rules. Check out the comments for explanations and [here](https://github.com/orionjs/documentation/blob/39cd73a29b112fe8f8b9ac0e56492fd00018b252/docs/accounts/roles.md) for further reading. 1516 | 1517 | ```javascript 1518 | /lib/roles/underpaid_worker.js 1519 | 1520 | /* 1521 | * First you must define the role 1522 | */ 1523 | UnderpaidWorker = new Roles.Role('underpaidWorker'); 1524 | 1525 | /** 1526 | * Allow the actions of the collection 1527 | */ 1528 | UnderpaidWorker.allow('collections.posts.index', true); // Allows the role to see the link in the sidebar 1529 | UnderpaidWorker.allow('collections.posts.insert', false); // Allows the role to insert documents 1530 | UnderpaidWorker.allow('collections.posts.update', true); // Allows the role to update documents 1531 | UnderpaidWorker.allow('collections.posts.remove', true); // Allows the role to remove documents 1532 | UnderpaidWorker.allow('collections.posts.showCreate', false); // Makes the "create" button visible 1533 | UnderpaidWorker.allow('collections.posts.showUpdate', true); // Allows the user to go to the update view 1534 | UnderpaidWorker.allow('collections.posts.showRemove', true); // Shows the delete button on the update view 1535 | 1536 | /** 1537 | * Set the index filter. 1538 | * This part is very important and sometimes is forgotten. 1539 | * Here you must specify which documents the role will be able to see in the index route 1540 | */ 1541 | UnderpaidWorker.helper('collections.posts.indexFilter', {}); // Allows the role to see all documents 1542 | 1543 | /** 1544 | * Allow the actions of the collection 1545 | */ 1546 | UnderpaidWorker.allow('collections.comments.index', true); // Allows the role to see the link in the sidebar 1547 | UnderpaidWorker.allow('collections.comments.insert', false); // Allows the role to insert documents 1548 | UnderpaidWorker.allow('collections.comments.update', true); // Allows the role to update documents 1549 | UnderpaidWorker.allow('collections.comments.remove', true); // Allows the role to remove documents 1550 | UnderpaidWorker.allow('collections.comments.showCreate', false); // Makes the "create" button visible 1551 | UnderpaidWorker.allow('collections.comments.showUpdate', true); // Allows the user to go to the update view 1552 | UnderpaidWorker.allow('collections.comments.showRemove', true); // Shows the delete button on the update view 1553 | 1554 | /** 1555 | * Set the index filter. 1556 | * This part is very important and sometimes is forgotten. 1557 | * Here you must specify which documents the role will be able to see in the index route 1558 | */ 1559 | UnderpaidWorker.helper('collections.comments.indexFilter', {}); // Allows the role to see all documents 1560 | ``` 1561 | 1562 | We are not allowing this underpaid worker to insert new posts or comments because we want to censor free speech as much as possible. 1563 | 1564 | Great! Now we need to assign a user to this role. I'm way, *way* too lazy to create a new user so let's just use an existing one: 1565 | 1566 | ```javascript 1567 | /server/roles.js 1568 | 1569 | var tom = Meteor.users.findOne({username: 'tom'}); 1570 | Roles.addUserToRoles( tom._id , ["admin"] ); 1571 | 1572 | var nameIcantSpel = Meteor.users.findOne({username: 'sacha'}); 1573 | Roles.removeUserFromRoles( nameIcantSpel._id, ["admin"] ); 1574 | Roles.addUserToRoles( nameIcantSpel._id , ["underpaidWorker"] ); 1575 | ``` 1576 | 1577 | Log in as Sexy and poke around (harr-harr): 1578 | 1579 | ![enter image description here](https://lh3.googleusercontent.com/6NhTuVXhi9bDOudFXw29Hpl_ZNNQgT184WAR2FqKJ-8=s0 "Screenshot from 2015-08-03 17:12:31.png") 1580 | 1581 | -------------------------------------------------------------------------------- /tutorials/security.md: -------------------------------------------------------------------------------- 1 | For protecting your server against script injection and your clients against XSS attacks, 2 | we recommend adding the following packages: 3 | 4 | ```sh 5 | meteor remove insecure autopublish 6 | meteor add audit-argument-checks browser-policy matteodem:easy-security 7 | ``` 8 | 9 | * [audit-argument-checks](https://atmospherejs.com/meteor/audit-argument-checks) checks the correctness of your development ensuring you that you've carefully checked user's inputs. 10 | * [browser-policy](https://atmospherejs.com/meteor/browser-policy) constraints modern users in not using anything else except what is precisely specified. 11 | * [matteodem:easy-security](https://atmospherejs.com/matteodem/easy-security) rate limits method calls, to avoid attacks DDOS-like attacks. 12 | 13 | The following browser policy rules have to be set server-side: 14 | **server/browserPolicy.coffee** 15 | ```coffee 16 | # Article sources: 17 | # * https://dweldon.silvrback.com/browser-policy 18 | # * http://paris.meteor.com/presentations/uByDe8qDLrNGJLzMC 19 | # Black list everything 20 | BrowserPolicy.framing.disallow() 21 | BrowserPolicy.content.disallowEval() 22 | # BrowserPolicy.content.disallowInlineScripts() 23 | BrowserPolicy.content.disallowConnect() 24 | # Only allow necessary protocols 25 | rootUrl = __meteor_runtime_config__.ROOT_URL 26 | BrowserPolicy.content.allowConnectOrigin rootUrl 27 | BrowserPolicy.content.allowConnectOrigin (rootUrl.replace 'http', 'ws') 28 | # Allow origin for Meteor hosting 29 | for protocol in ['http', 'https', 'ws', 'wss'] 30 | BrowserPolicy.content.allowConnectOrigin "#{protocol}://*.meteor.com" 31 | # Allow external CSS 32 | for origin in ['fonts.googleapis'] 33 | for protocol in ['http', 'https'] 34 | BrowserPolicy.content.allowStyleOrigin "#{protocol}://#{origin}" 35 | # Allow external fonts 36 | for origin in ['fonts.gstatic.com'] 37 | for protocol in ['http', 'https'] 38 | BrowserPolicy.content.allowFontOrigin "#{protocol}://#{origin}" 39 | # Allow Fonts and CSS 40 | for protocol in ['http', 'https'] 41 | BrowserPolicy.content.allowStyleOrigin "#{protocol}://fonts.googleapis.com" 42 | BrowserPolicy.content.allowFontOrigin "#{protocol}://fonts.gstatic.com" 43 | # Trusted sites 44 | for origin in ['*.google-analytics.com', 'browser-update.org'] 45 | for protocol in ['http', 'https'] 46 | BrowserPolicy.content.allowOriginForAll "#{protocol}://#{origin}" 47 | ``` 48 | -------------------------------------------------------------------------------- /tutorials/upgrading-to-v1-0.md: -------------------------------------------------------------------------------- 1 | Orion had a complete rewrite of its code base and some things are not 2 | compatible with old code, learn here what do you need to change. 3 | 4 | ### Updating Dictionary 5 | 6 | The dictionary has some underground changes. 7 | The only change in the api is that now to access a definition 8 | you must ask for ```category.name``` 9 | 10 | For Example: 11 | 12 | ```js 13 | orion.dictionary.addDefinition('description', 'site', { 14 | type: String, 15 | label: "Description" 16 | }); 17 | ``` 18 | 19 | Before: 20 | 21 | ```html 22 | 25 | ``` 26 | 27 | After: 28 | 29 | ```html 30 | 33 | ``` 34 | 35 | ### Updating Entities 36 | 37 | Entities changed completely and the they are named Orion Collections. 38 | Orion Collection look a lot like normal Meteor Collections, the api is 39 | the same. 40 | 41 | Also, publications are not made automatically, so you must make it yoursef. 42 | 43 | > You don't need to delete the database to upgrade, but you need to do a lot of changes in the code. 44 | 45 | 46 | Before: 47 | 48 | ```js 49 | orion.addEntity('posts', { 50 | title: { 51 | type: String, 52 | label: "Title", 53 | }, 54 | image: orion.attribute('file', { 55 | label: 'Image', 56 | optional: true 57 | }), 58 | body: orion.attribute('froala', { 59 | label: 'Body', 60 | optional: true 61 | }), 62 | }, { 63 | icon: 'bookmark', 64 | sidebarName: 'Posts', 65 | pluralName: 'Posts', 66 | singularName: 'Post', 67 | tableColumns: [ 68 | { data:'title', title: 'Title' }, 69 | orion.attributeColumn('file', 'image', 'Image'), 70 | orion.attributeColumn('froala', 'body', 'Preview') 71 | ] 72 | }); 73 | ``` 74 | 75 | After: 76 | 77 | ```js 78 | /** 79 | * We declare the collection just like meteor default way 80 | * but changing Meteor.Collection to orion.collection. 81 | * 82 | * We can set options to that new collection, like which fields 83 | * we will show in the index of the collection in the admin 84 | */ 85 | Posts = new orion.collection('posts', { 86 | singularName: 'post', // The name of one of this items 87 | pluralName: 'posts', // The name of more than one of this items 88 | link: { 89 | /** 90 | * The text that you want to show in the sidebar. 91 | * The default value is the name of the collection, so 92 | * in this case is not necesary, but I will put it here 93 | * for educational purposes. 94 | */ 95 | title: 'Posts' 96 | }, 97 | /** 98 | * Tabular settings for this collection 99 | */ 100 | tabular: { 101 | columns: [ 102 | { data: "title", title: "Title" }, 103 | orion.attributeColumn('file', 'image', 'Image'), 104 | orion.attributeColumn('summernote', 'body', 'Content'), 105 | orion.attributeColumn('createdBy', 'createdBy', 'Created By') 106 | ] 107 | } 108 | }); 109 | 110 | /** 111 | * Now we will attach the schema for that collection. 112 | * Orion will automatically create the corresponding form. 113 | */ 114 | Posts.attachSchema(new SimpleSchema({ 115 | title: { 116 | type: String 117 | }, 118 | image: orion.attribute('file', { 119 | label: 'Image', 120 | optional: true 121 | }), 122 | body: orion.attribute('summernote', { 123 | label: 'Body' 124 | }), 125 | /** 126 | * This attribute sets the user id of the user that created 127 | * this post automatically. 128 | */ 129 | createdBy: orion.attribute('createdBy'), 130 | /** 131 | * This attribute sets the created at date automatically to 132 | * this post. 133 | */ 134 | createdAt: orion.attribute('createdAt') 135 | })); 136 | ``` 137 | 138 | #### Extras 139 | 140 | Other things like config, pages, etc. where re-writed but have the same api. 141 | 142 | You can look at the [example blog](https://github.com/orionjs/example-blog) which 143 | was also upgraded to version ```1.0```. 144 | --------------------------------------------------------------------------------