Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Initial Commit

  • Loading branch information...
commit 20495751263c5d125daba3a8f4192f100795d1de 0 parents
@mattt mattt authored
Showing with 24,709 additions and 0 deletions.
  1. +18 −0 .gitignore
  2. +3 −0  Gemfile
  3. +143 −0 Gemfile.lock
  4. +19 −0 LICENSE
  5. +308 −0 README.md
  6. +11 −0 Rakefile
  7. +20 −0 bin/helios
  8. +7 −0 example/Gemfile
  9. +163 −0 example/Gemfile.lock
  10. +1 −0  example/Procfile
  11. +8 −0 example/Sample.xcdatamodeld/.xccurrentversion
  12. +17 −0 example/Sample.xcdatamodeld/Sample.xcdatamodel/contents
  13. +9 −0 example/config.ru
  14. +43 −0 helios.gemspec
  15. +21 −0 lib/helios.rb
  16. +57 −0 lib/helios/backend.rb
  17. +37 −0 lib/helios/backend/data.rb
  18. +41 −0 lib/helios/backend/in-app-purchase.rb
  19. +35 −0 lib/helios/backend/passbook.rb
  20. +97 −0 lib/helios/backend/push-notification.rb
  21. +4 −0 lib/helios/commands.rb
  22. +29 −0 lib/helios/commands/console.rb
  23. +19 −0 lib/helios/commands/link.rb
  24. +77 −0 lib/helios/commands/new.rb
  25. +16 −0 lib/helios/commands/server.rb
  26. +74 −0 lib/helios/frontend.rb
  27. BIN  lib/helios/frontend/fonts/icons.eot
  28. BIN  lib/helios/frontend/fonts/icons.ttf
  29. BIN  lib/helios/frontend/fonts/icons.woff
  30. BIN  lib/helios/frontend/images/bg.jpg
  31. +36 −0 lib/helios/frontend/javascripts/helios.coffee
  32. +91 −0 lib/helios/frontend/javascripts/helios/collections.coffee
  33. +20 −0 lib/helios/frontend/javascripts/helios/models.coffee
  34. +38 −0 lib/helios/frontend/javascripts/helios/router.coffee
  35. +233 −0 lib/helios/frontend/javascripts/helios/views.coffee
  36. +662 −0 lib/helios/frontend/javascripts/vendor/backbone.datagrid.js
  37. +1,487 −0 lib/helios/frontend/javascripts/vendor/backbone.js
  38. +1,046 −0 lib/helios/frontend/javascripts/vendor/backbone.paginator.js
  39. +411 −0 lib/helios/frontend/javascripts/vendor/codemirror.javascript.js
  40. +3,047 −0 lib/helios/frontend/javascripts/vendor/codemirror.js
  41. +104 −0 lib/helios/frontend/javascripts/vendor/date.js
  42. +331 −0 lib/helios/frontend/javascripts/vendor/foundation.js
  43. +50 −0 lib/helios/frontend/javascripts/vendor/foundation/foundation.alerts.js
  44. +478 −0 lib/helios/frontend/javascripts/vendor/foundation/foundation.clearing.js
  45. +74 −0 lib/helios/frontend/javascripts/vendor/foundation/foundation.cookie.js
  46. +122 −0 lib/helios/frontend/javascripts/vendor/foundation/foundation.dropdown.js
  47. +403 −0 lib/helios/frontend/javascripts/vendor/foundation/foundation.forms.js
  48. +613 −0 lib/helios/frontend/javascripts/vendor/foundation/foundation.joyride.js
  49. +130 −0 lib/helios/frontend/javascripts/vendor/foundation/foundation.magellan.js
  50. +355 −0 lib/helios/frontend/javascripts/vendor/foundation/foundation.orbit.js
  51. +159 −0 lib/helios/frontend/javascripts/vendor/foundation/foundation.placeholder.js
  52. +272 −0 lib/helios/frontend/javascripts/vendor/foundation/foundation.reveal.js
  53. +183 −0 lib/helios/frontend/javascripts/vendor/foundation/foundation.section.js
  54. +195 −0 lib/helios/frontend/javascripts/vendor/foundation/foundation.tooltips.js
  55. +208 −0 lib/helios/frontend/javascripts/vendor/foundation/foundation.topbar.js
  56. +9,597 −0 lib/helios/frontend/javascripts/vendor/jquery.js
  57. +1,227 −0 lib/helios/frontend/javascripts/vendor/underscore.js
  58. +41 −0 lib/helios/frontend/stylesheets/_bariol.scss
  59. +219 −0 lib/helios/frontend/stylesheets/_codemirror.sass
  60. +80 −0 lib/helios/frontend/stylesheets/_fonts.sass
  61. +141 −0 lib/helios/frontend/stylesheets/_iphone.sass
  62. +989 −0 lib/helios/frontend/stylesheets/_settings.scss
  63. +191 −0 lib/helios/frontend/stylesheets/screen.sass
  64. +70 −0 lib/helios/frontend/templates/compose.jst.tpl
  65. +17 −0 lib/helios/frontend/templates/devices.jst.tpl
  66. +11 −0 lib/helios/frontend/templates/entities.jst.tpl
  67. +11 −0 lib/helios/frontend/templates/passes.jst.tpl
  68. +11 −0 lib/helios/frontend/templates/receipts.jst.tpl
  69. +46 −0 lib/helios/frontend/views/index.haml
  70. +1 −0  lib/helios/templates/.env.erb
  71. +3 −0  lib/helios/templates/.gitignore
  72. +10 −0 lib/helios/templates/Gemfile.erb
  73. +1 −0  lib/helios/templates/Procfile.erb
  74. +4 −0 lib/helios/templates/README.md.erb
  75. +11 −0 lib/helios/templates/config.ru.erb
  76. +3 −0  lib/helios/version.rb
18 .gitignore
@@ -0,0 +1,18 @@
+*.gem
+*.rbc
+.bundle
+.config
+coverage
+InstalledFiles
+lib/bundler/man
+pkg
+rdoc
+spec/reports
+test/tmp
+test/version_tmp
+tmp
+
+# YARD artifacts
+.yardoc
+_yardoc
+doc/
3  Gemfile
@@ -0,0 +1,3 @@
+source "https://rubygems.org"
+
+gemspec
143 Gemfile.lock
@@ -0,0 +1,143 @@
+PATH
+ remote: .
+ specs:
+ helios (0.2.0)
+ coffee-script (~> 2.2)
+ commander (~> 4.1)
+ compass (~> 0.12)
+ foreman (~> 0.60)
+ haml (~> 3.1)
+ json (~> 1.7)
+ rack-contrib (~> 1.1)
+ rack-core-data (~> 0.3)
+ rack-in-app-purchase (~> 0.1)
+ rack-passbook (~> 0.1)
+ rack-push-notification (~> 0.4)
+ rails-database-url (~> 1.0)
+ sinatra (~> 1.3)
+ sinatra-assetpack (~> 0.1)
+ sinatra-backbone (~> 0.1.0.rc2)
+ sinatra-contrib (~> 1.3)
+ sinatra-param (~> 0.1)
+ sinatra-support (~> 1.2)
+ zurb-foundation (~> 4.0)
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ activesupport (3.2.13)
+ i18n (= 0.6.1)
+ multi_json (~> 1.0)
+ backports (3.1.1)
+ chunky_png (1.2.8)
+ coffee-script (2.2.0)
+ coffee-script-source
+ execjs
+ coffee-script-source (1.6.2)
+ commander (4.1.3)
+ highline (~> 1.6.11)
+ compass (0.12.2)
+ chunky_png (~> 1.2)
+ fssm (>= 0.2.7)
+ sass (~> 3.1)
+ diff-lcs (1.2.2)
+ eventmachine (1.0.3)
+ excon (0.17.0)
+ execjs (1.4.0)
+ multi_json (~> 1.0)
+ foreman (0.62.0)
+ thor (>= 0.13.6)
+ fssm (0.2.10)
+ haml (3.1.8)
+ highline (1.6.16)
+ houston (0.1.1)
+ commander (~> 4.1.2)
+ json (~> 1.7.3)
+ i18n (0.6.1)
+ jsmin (1.0.1)
+ json (1.7.7)
+ multi_json (1.7.2)
+ nokogiri (1.5.9)
+ rack (1.5.2)
+ rack-contrib (1.1.0)
+ rack (>= 0.9.1)
+ rack-core-data (0.3.1)
+ activesupport (>= 3.0)
+ nokogiri (~> 1.4)
+ rack (~> 1.4)
+ rack-contrib (~> 1.1)
+ sequel (~> 3.37)
+ sinatra (~> 1.3)
+ sinatra-param (~> 0.1)
+ rack-in-app-purchase (0.1.0)
+ rack (~> 1.4)
+ sequel (~> 3.37)
+ sinatra (~> 1.3)
+ venice
+ rack-passbook (0.1.1)
+ rack (~> 1.4)
+ sequel (~> 3.37)
+ sinatra (~> 1.3)
+ rack-protection (1.5.0)
+ rack
+ rack-push-notification (0.4.0)
+ houston (~> 0.1.1)
+ rack (~> 1.4)
+ rack-contrib (~> 1.1)
+ sequel (~> 3.37)
+ sinatra (~> 1.3)
+ sinatra-param (~> 0.1)
+ rack-test (0.6.2)
+ rack (>= 1.0)
+ rails-database-url (1.0.0)
+ rake (10.0.4)
+ rspec (2.13.0)
+ rspec-core (~> 2.13.0)
+ rspec-expectations (~> 2.13.0)
+ rspec-mocks (~> 2.13.0)
+ rspec-core (2.13.1)
+ rspec-expectations (2.13.0)
+ diff-lcs (>= 1.1.3, < 2.0)
+ rspec-mocks (2.13.0)
+ sass (3.2.7)
+ sequel (3.45.0)
+ sinatra (1.3.6)
+ rack (~> 1.4)
+ rack-protection (~> 1.3)
+ tilt (~> 1.3, >= 1.3.3)
+ sinatra-assetpack (0.1.7)
+ jsmin
+ rack-test
+ sinatra
+ tilt (>= 1.3.0)
+ sinatra-backbone (0.1.0.rc2)
+ sinatra
+ sinatra-contrib (1.3.2)
+ backports (>= 2.0)
+ eventmachine
+ rack-protection
+ rack-test
+ sinatra (~> 1.3.0)
+ tilt (~> 1.3)
+ sinatra-param (0.1.2)
+ sinatra (~> 1.3)
+ sinatra-support (1.2.2)
+ sinatra (>= 1.0)
+ terminal-table (1.4.5)
+ thor (0.18.1)
+ tilt (1.3.6)
+ venice (0.0.1)
+ commander (~> 4.1.2)
+ excon (~> 0.17.0)
+ json (~> 1.7.3)
+ terminal-table (~> 1.4.5)
+ zurb-foundation (4.0.9)
+ sass (>= 3.2.0)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ helios!
+ rake
+ rspec
19 LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2013 Heroku (http://heroku.com/)
+
+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:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+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.
308 README.md
@@ -0,0 +1,308 @@
+![Helios - an extensible open source mobile backend framework](https://raw.github.com/helios-framework/helios.io/assets/helios-banner.png)
+
+Helios is an open-source framework that provides essential backend services for iOS apps, from data synchronization and user accounts to push notifications, in-app purchases, and passbook integration. It allows developers to get a client-server app up-and-running in just a few minutes, and seamlessly incorporate functionality as necessary.
+
+Helios is designed for "mobile first" development. Build out great features on the device, and implement the server-side components as necessary. Pour all of your energy into crafting a great user experience, rather than getting mired down with the backend.
+
+One great example of this philosophy in Helios is Core Data Synchronization. This allows you to use your existing Core Data model definition to automatically generate a REST webservice, which can be used to shuttle data between the server and client. No iCloud, _no problem_.
+
+![Helios Web UI Screenshot](https://raw.github.com/helios-framework/helios.io/assets/helios-screenshot.png)
+
+Helios also comes with a Web UI. Browse and search through all of your database records, push notification registrations, in-app purchases, and passbook passes. You can even send targeted push notifications right from the browser.
+
+---
+
+## Requirements
+
+- Ruby 1.9
+- PostgreSQL 9.1 _([Postgres.app](http://postgresapp.com) is the easiest way to get a Postgres server running on your Mac)_
+
+## Getting Started
+
+1. Install Helios at the command prompt:
+
+ $ gem install helios
+
+2. Create a new Helios application:
+
+ $ helios new myapp
+
+3. Change directory to `myapp` and start the web server:
+
+ $ cd myapp; helios server
+
+4. Go to http://localhost:5000/admin and you’ll see your app's Web UI
+
+Read on for instructions on the following:
+
+- Linking a Core Data model
+- Integrating Helios into your mobile client
+
+## Usage
+
+Built on the Rack webserver interface, Helios can be easily added into any existing Rails or Sinatra application as middleware. Or, if you're starting with a Helios application, you can build a new Rails or Sinatra application on top of it.
+
+This means that you can develop your application using the tools and frameworks you love, and maintain flexibility with your architecture as your needs evolve.
+
+### Sinatra / Rack
+
+#### Gemfile
+
+```ruby
+gem 'helios'
+```
+
+#### config.ru
+
+```ruby
+require 'bundler'
+Bundler.require
+
+run Helios::Application.new do
+ service :data, model: 'path/to/DataModel.xcdatamodel'
+ service :push_notification
+ service :in_app_purchase
+ service :passbook
+ end
+```
+
+### Rails
+
+To create a Rails app that uses Postgres as its database, pass the `-d postgresql` argument to the `rails new` command:
+
+ $ rails new APP_PATH -d postgresql
+
+If you're adding Helios to an existing Rails project, be sure to specify a PostgreSQL database in `config/database.yml` and check that the `pg` gem is included in your `Gemfile`:
+
+#### Gemfile
+
+ gem 'helios'
+ gem 'pg'
+
+Helios can be run as Rails middleware by adding this to the configuration block in `config/application.rb`
+
+#### config/application.rb
+
+```ruby
+config.middleware.use Helios::Application do
+ service :data, model: 'path/to/DataModel.xcdatamodel'
+ service :push_notification
+ service :in_app_purchase
+ service :passbook
+end
+```
+
+## Available Services
+
+Each service in Helios can be enabled and configured separately:
+
+`data`: Generates a REST webservice from a schema definition. Currently supports Core Data (`.xcdatamodel`) files.
+
+**Parameters**
+
+- `model`: Path to the data model file
+
+**Associated Classes**
+
+Each entity in the specified data model will have a `Sequel::Model` subclass created for it under the `Rack::CoreData::Models` namespace.
+
+<table>
+ <caption>Endpoints</caption>
+ <tr>
+ <td><tt>GET /:resources</tt></td>
+ <td>Get list of all of the specified resources</td>
+ </tr>
+ <tr>
+ <td><tt>POST /:resources</tt></td>
+ <td>Create a new instance of the specified resource</td>
+ </tr>
+ <tr>
+ <td><tt>GET /:resources/:id</tt></td>
+ <td>Get the specified resource instance</td>
+ </tr>
+ <tr>
+ <td><tt>PUT /:resources/:id</tt></td>
+ <td>Update the specified resource instance</td>
+ </tr>
+ <tr>
+ <td><tt>DELETE /:resources/:id</tt></td>
+ <td>Delege the specified resource instance</td>
+ </tr>
+</table>
+
+---
+
+`push_notification`: Adds iOS push notification registration / unregistration endpoints.
+
+**Associated Classes**
+
+- `Rack::PushNotification::Device`
+
+<table>
+ <caption>Endpoints</caption>
+ <tr>
+ <td><tt>PUT /devices/:token</tt></td>
+ <td>Register or update existing device for push notifications</td>
+ </tr>
+ <tr>
+ <td><tt>DELETE /devices/:token</tt></td>
+ <td>Unregister a device from receiving push notifications</td>
+ </tr>
+</table>
+
+---
+
+`in_app_purchase`: Adds an endpoint for iOS in-app purchase receipt verification endpoints, as well one for returning product identifiers.
+
+**Associated Classes**
+
+- `Rack::InAppPurchase::Receipt`
+- `Rack::InAppPurchase::Product`
+
+<table>
+ <caption>Endpoints</caption>
+ <tr>
+ <td><tt>POST /receipts/verify</tt></td>
+ <td>Decode the associated Base64-encoded <tt>receipt-data</tt>, recording the receipt data and verifying the information with Apple</td>
+ </tr>
+ <tr>
+ <td><tt>GET /products/identifiers</tt></td>
+ <td>Return an array of valid product identifiers</td>
+ </tr>
+</table>
+
+---
+
+`passbook`: Adds endpoints for the [web service protocol](https://developer.apple.com/library/prerelease/ios/#documentation/PassKit/Reference/PassKit_WebService/WebService.html) for communicating with Passbook
+
+**Associated Classes**
+
+- `Rack::Passbook::Pass`
+- `Rack::Passbook::Registration`
+
+<table>
+ <caption>Endpoints</caption>
+ <tr>
+ <td><tt>GET /v1/passes/:passTypeIdentifier/:serialNumber</tt></td>
+ <td>Get the Latest Version of a Pass</td>
+ </tr>
+ <tr>
+ <td><tt>GET /v1/devices/:deviceLibraryIdentifier/registrations/:passTypeIdentifier[?passesUpdatedSince=tag]</tt></td>
+ <td>Get the Serial Numbers for Passes Associated with a Device</td>
+ </tr>
+ <tr>
+ <td><tt>POST /v1/devices/:deviceLibraryIdentifier/registrations/:passTypeIdentifier/:serialNumber</tt></td>
+ <td>Register a Device to Receive Push Notifications for a Pass</td>
+ </tr>
+ <tr>
+ <td><tt>DELETE /v1/devices/:deviceLibraryIdentifier/registrations/:passTypeIdentifier/:serialNumber</tt></td>
+ <td>Unregister a Device</td>
+ </tr>
+</table>
+
+## Command-Line Interface
+
+Helios comes with a CLI to help create and manage your application. After you `$ gem install helios`, you'll have the `helios` binary available.
+
+ $ helios --help
+ helios
+
+ A command-line interface for building mobile infrastructures
+
+ Commands:
+ console Open IRB session with Helios environment
+ help Display global or [command] help documentation.
+ link Links a Core Data model
+ new Creates a new Helios project
+ server Start running Helios locally
+
+### Creating an Application
+
+The first step to using Helios is to create a new application. This can be done with the `$ helios new` command, which should be familiar if you've ever used Rails.
+
+ $ helios new --help
+
+ Usage: helios new path/to/app
+
+ The `helios new` command creates a new Helios application with a default
+ directory structure and configuration at the path you specify.
+
+ Options:
+ --skip-gemfile Don't create a Gemfile
+ -B, --skip-bundle Don't run bundle install
+ -G, --skip-git Don't create a git repository
+ --edge Setup the application with Gemfile pointing to Helios repository
+ -f, --force Overwrite files that already exist
+ -p, --pretend Run but do not make any changes
+ -s, --skip Skip files that already exist
+
+### Linking a Core Data Model
+
+In order to keep your data model and REST webservices in sync, you can link it to your helios application:
+
+ $ helios link path/to/DataModel.xcdatamodel
+
+This creates a hard link between the data model file in your Xcode and Helios projects—any changes made to either file will affect both. The next time you start the server, Helios will automatically migrate the database to create tables and insert columns to accomodate any new entities or attributes.
+
+### Starting the Application Locally
+
+To run Helios in development mode on `localhost`, run the `server` command:
+
+ $ helios server
+
+### Running the Helios Console
+
+You can start an IRB session with the runtime environment of the Helios application with the `console` command:
+
+ $ helios console
+
+This command activates the services as configured by your Helios application, including any generated Core Data models. The `rack` module is automatically included on launch, allowing you to access everything more directly:
+
+ > Passbook::Passes.all # => [...]
+
+## Deploying to Heroku
+
+[Heroku](http://www.heroku.com) is the easiest way to get your app up and running. For full instructions on how to get started, check out ["Getting Started with Ruby on Heroku"](https://devcenter.heroku.com/articles/ruby).
+
+Once you've installed the [Heroku Toolbelt](https://toolbelt.heroku.com), and have a Heroku account, enter the following commands from the project directory:
+
+ $ heroku create
+ $ git push heroku master
+
+## Integrating with iOS Application
+
+### Core Data Synchronization
+
+With [AFIncrementalStore](https://github.com/AFNetworking/AFIncrementalStore), you can integrate your Helios app directly into the Core Data stack. Whether it’s a fetch or save changes request, or fulfilling an attribute or relation fault, AFIncrementalStore handles all of the networking needed to read and write to and from the server.
+
+See ["Building an iOS App with AFIncrementalStore and the Core Data Buildpack"](https://devcenter.heroku.com/articles/ios-core-data-buildpack-app) on the Heroku Dev Center for a comprehensive guide on how to use AFIncrementalStore with the Core Data buildpack. An article for Helios is forthcoming, but aside from deployment, the instructions are essentially unchanged.
+
+### Push Notification Registration
+
+```objective-c
+- (void)application:(UIApplication *)application
+didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken
+{
+ NSURL *serverURL = [NSURL URLWithString:@"http://raging-notification-3556.herokuapp.com/"];
+ Orbiter *orbiter = [[Orbiter alloc] initWithBaseURL:serverURL credential:nil];
+ [orbiter registerDeviceToken:deviceToken withAlias:nil success:^(id responseObject) {
+ NSLog(@"Registration Success: %@", responseObject);
+ } failure:^(NSError *error) {
+ NSLog(@"Registration Error: %@", error);
+ }];
+}
+```
+
+---
+
+## Contact
+
+Mattt Thompson
+
+- http://github.com/mattt
+- http://twitter.com/mattt
+- <mattt@heroku.com>
+
+## License
+
+Helios is released under the [MIT](http://opensource.org/licenses/MIT) license.
11 Rakefile
@@ -0,0 +1,11 @@
+require "bundler"
+Bundler.setup
+
+gemspec = eval(File.read("helios.gemspec"))
+
+task :build => "#{gemspec.full_name}.gem"
+
+file "#{gemspec.full_name}.gem" => gemspec.files + ["helios.gemspec"] do
+ system "gem build helios.gemspec"
+ system "gem install helios-#{Helios::VERSION}.gem"
+end
20 bin/helios
@@ -0,0 +1,20 @@
+#!/usr/bin/env ruby
+
+require 'commander/import'
+$:.push File.expand_path("../../lib", __FILE__)
+
+require 'helios/version'
+
+HighLine.track_eof = false # Fix for built-in Ruby
+Signal.trap("INT") {} # Suppress backtrace when exiting command
+
+program :version, Helios::VERSION
+program :description, 'A command-line interface for building mobile infrastructures'
+
+program :help, 'Author', 'Mattt Thompson <m@mattt.me>'
+program :help, 'Website', 'https://helios.io'
+program :help_formatter, :compact
+
+default_command :help
+
+require 'helios/commands'
7 example/Gemfile
@@ -0,0 +1,7 @@
+source "https://rubygems.org"
+
+gem 'helios', path: File.join(__FILE__, "../..")
+gem 'unicorn'
+gem 'pg'
+
+gem 'omniauth-github'
163 example/Gemfile.lock
@@ -0,0 +1,163 @@
+PATH
+ remote: /Users/mattt/Code/Ruby/helios
+ specs:
+ helios (0.2.0)
+ coffee-script (~> 2.2)
+ commander (~> 4.1)
+ compass (~> 0.12)
+ foreman (~> 0.60)
+ haml (~> 3.1)
+ json (~> 1.7)
+ rack-contrib (~> 1.1)
+ rack-core-data (~> 0.3)
+ rack-in-app-purchase (~> 0.1)
+ rack-passbook (~> 0.1)
+ rack-push-notification (~> 0.4)
+ rails-database-url (~> 1.0)
+ sinatra (~> 1.3)
+ sinatra-assetpack (~> 0.1)
+ sinatra-backbone (~> 0.1.0.rc2)
+ sinatra-contrib (~> 1.3)
+ sinatra-param (~> 0.1)
+ sinatra-support (~> 1.2)
+ zurb-foundation (~> 4.0)
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ activesupport (3.2.13)
+ i18n (= 0.6.1)
+ multi_json (~> 1.0)
+ backports (3.1.1)
+ chunky_png (1.2.8)
+ coffee-script (2.2.0)
+ coffee-script-source
+ execjs
+ coffee-script-source (1.6.2)
+ commander (4.1.3)
+ highline (~> 1.6.11)
+ compass (0.12.2)
+ chunky_png (~> 1.2)
+ fssm (>= 0.2.7)
+ sass (~> 3.1)
+ eventmachine (1.0.3)
+ excon (0.17.0)
+ execjs (1.4.0)
+ multi_json (~> 1.0)
+ faraday (0.8.6)
+ multipart-post (~> 1.1)
+ foreman (0.62.0)
+ thor (>= 0.13.6)
+ fssm (0.2.10)
+ haml (3.1.8)
+ hashie (1.2.0)
+ highline (1.6.16)
+ houston (0.1.1)
+ commander (~> 4.1.2)
+ json (~> 1.7.3)
+ httpauth (0.2.0)
+ i18n (0.6.1)
+ jsmin (1.0.1)
+ json (1.7.7)
+ jwt (0.1.8)
+ multi_json (>= 1.5)
+ kgio (2.8.0)
+ multi_json (1.7.1)
+ multipart-post (1.2.0)
+ nokogiri (1.5.9)
+ oauth2 (0.8.1)
+ faraday (~> 0.8)
+ httpauth (~> 0.1)
+ jwt (~> 0.1.4)
+ multi_json (~> 1.0)
+ rack (~> 1.2)
+ omniauth (1.1.3)
+ hashie (~> 1.2)
+ rack
+ omniauth-github (1.1.0)
+ omniauth (~> 1.0)
+ omniauth-oauth2 (~> 1.1)
+ omniauth-oauth2 (1.1.1)
+ oauth2 (~> 0.8.0)
+ omniauth (~> 1.0)
+ pg (0.14.1)
+ rack (1.5.2)
+ rack-contrib (1.1.0)
+ rack (>= 0.9.1)
+ rack-core-data (0.3.1)
+ activesupport (>= 3.0)
+ nokogiri (~> 1.4)
+ rack (~> 1.4)
+ rack-contrib (~> 1.1)
+ sequel (~> 3.37)
+ sinatra (~> 1.3)
+ sinatra-param (~> 0.1)
+ rack-in-app-purchase (0.1.0)
+ rack (~> 1.4)
+ sequel (~> 3.37)
+ sinatra (~> 1.3)
+ venice
+ rack-passbook (0.1.1)
+ rack (~> 1.4)
+ sequel (~> 3.37)
+ sinatra (~> 1.3)
+ rack-protection (1.5.0)
+ rack
+ rack-push-notification (0.4.0)
+ houston (~> 0.1.1)
+ rack (~> 1.4)
+ rack-contrib (~> 1.1)
+ sequel (~> 3.37)
+ sinatra (~> 1.3)
+ sinatra-param (~> 0.1)
+ rack-test (0.6.2)
+ rack (>= 1.0)
+ rails-database-url (1.0.0)
+ raindrops (0.10.0)
+ sass (3.2.7)
+ sequel (3.45.0)
+ sinatra (1.3.6)
+ rack (~> 1.4)
+ rack-protection (~> 1.3)
+ tilt (~> 1.3, >= 1.3.3)
+ sinatra-assetpack (0.1.7)
+ jsmin
+ rack-test
+ sinatra
+ tilt (>= 1.3.0)
+ sinatra-backbone (0.1.0.rc2)
+ sinatra
+ sinatra-contrib (1.3.2)
+ backports (>= 2.0)
+ eventmachine
+ rack-protection
+ rack-test
+ sinatra (~> 1.3.0)
+ tilt (~> 1.3)
+ sinatra-param (0.1.2)
+ sinatra (~> 1.3)
+ sinatra-support (1.2.2)
+ sinatra (>= 1.0)
+ terminal-table (1.4.5)
+ thor (0.18.1)
+ tilt (1.3.6)
+ unicorn (4.6.2)
+ kgio (~> 2.6)
+ rack
+ raindrops (~> 0.7)
+ venice (0.0.1)
+ commander (~> 4.1.2)
+ excon (~> 0.17.0)
+ json (~> 1.7.3)
+ terminal-table (~> 1.4.5)
+ zurb-foundation (4.0.9)
+ sass (>= 3.2.0)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ helios!
+ omniauth-github
+ pg
+ unicorn
1  example/Procfile
@@ -0,0 +1 @@
+web: bundle exec unicorn -p $PORT
8 example/Sample.xcdatamodeld/.xccurrentversion
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>_XCCurrentVersionName</key>
+ <string>Nil.xcdatamodel</string>
+</dict>
+</plist>
17 example/Sample.xcdatamodeld/Sample.xcdatamodel/contents
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<model name="" userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="2061" systemVersion="12D78" minimumToolsVersion="Automatic" macOSVersion="Automatic" iOSVersion="Automatic">
+ <entity name="Product" syncable="YES">
+ <attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
+ <attribute name="price" optional="YES" attributeType="Double" defaultValueString="0.0" syncable="YES"/>
+ <attribute name="productDescription" optional="YES" attributeType="String" syncable="YES"/>
+ <attribute name="quantity" optional="YES" attributeType="Integer 16" defaultValueString="0" syncable="YES"/>
+ </entity>
+ <entity name="User" syncable="YES">
+ <attribute name="email" optional="YES" attributeType="String" syncable="YES"/>
+ <attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
+ </entity>
+ <elements>
+ <element name="Product" positionX="160" positionY="192" width="128" height="105"/>
+ <element name="User" positionX="160" positionY="192" width="128" height="75"/>
+ </elements>
+</model>
9 example/config.ru
@@ -0,0 +1,9 @@
+require 'bundler'
+Bundler.require
+
+run Helios::Application.new do
+ service :data, model: Dir['*.xcdatamodel*'].first
+ service :push_notification
+ service :in_app_purchase
+ service :passbook
+ end
43 helios.gemspec
@@ -0,0 +1,43 @@
+# -*- encoding: utf-8 -*-
+$:.push File.expand_path("../lib", __FILE__)
+require "helios/version"
+
+Gem::Specification.new do |s|
+ s.name = "helios"
+ s.authors = ["Mattt Thompson"]
+ s.email = "mattt@heroku.com"
+ s.license = "MIT"
+ s.homepage = "http://helios.io"
+ s.version = Helios::VERSION
+ s.platform = Gem::Platform::RUBY
+ s.summary = "An extensible open-source mobile backend framework"
+ s.description = "Helios is an open-source framework that provides essential backend services for iOS apps, from data synchronization and user accounts to push notifications, in-app purchases, and passbook integration. It allows developers to get a client-server app up-and-running in just a few minutes, and seamlessly incorporate functionality as necessary."
+
+ s.add_dependency "commander", "~> 4.1"
+ s.add_dependency "foreman", "~> 0.60"
+ s.add_dependency "rack-contrib", "~> 1.1"
+ s.add_dependency "rack-core-data", "~> 0.3"
+ s.add_dependency "rack-push-notification", "~> 0.4"
+ s.add_dependency "rack-in-app-purchase", "~> 0.1"
+ s.add_dependency "rack-passbook", "~> 0.1"
+ s.add_dependency "json", "~> 1.7"
+ s.add_dependency "coffee-script", "~> 2.2"
+ s.add_dependency "sinatra", "~> 1.3"
+ s.add_dependency "sinatra-contrib", "~> 1.3"
+ s.add_dependency "sinatra-assetpack", "~> 0.1"
+ s.add_dependency "sinatra-backbone", "~> 0.1.0.rc2"
+ s.add_dependency "sinatra-param", "~> 0.1"
+ s.add_dependency "sinatra-support", "~> 1.2"
+ s.add_dependency "haml", "~> 3.1"
+ s.add_dependency "compass", "~> 0.12"
+ s.add_dependency "zurb-foundation", "~> 4.0"
+ s.add_dependency "rails-database-url", "~> 1.0"
+
+ s.add_development_dependency "rake", ">= 0"
+ s.add_development_dependency "rspec", ">= 0"
+
+ s.files = Dir["./**/*"].reject{|file| file =~ /\.\/(bin|example|log|pkg|script|spec|test|vendor)/} + Dir.glob("./lib/helios/templates", File::FNM_DOTMATCH)
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+ s.require_paths = ["lib"]
+end
21 lib/helios.rb
@@ -0,0 +1,21 @@
+require 'rack'
+
+module Helios
+ class Helios::Application
+ def initialize(app = nil, options = {}, &block)
+ map = {}
+ map['/'] = Helios::Backend.new(&block)
+ map['/admin'] = Helios::Frontend.new if options.fetch(:frontend, true)
+
+ @app = Rack::URLMap.new(map)
+ end
+
+ def call(env)
+ @app.call(env)
+ end
+ end
+end
+
+require 'helios/backend'
+require 'helios/frontend'
+require 'helios/version'
57 lib/helios/backend.rb
@@ -0,0 +1,57 @@
+require 'rack'
+
+module Helios
+ class Backend < Rack::Cascade
+ require 'rails-database-url' if const_defined?(:Rails)
+
+ def initialize(&block)
+ @services = []
+
+ block = lambda { |app|
+ service :data, model: Dir['**/*.xcdatamodeld'].first rescue false
+ service :push_notification
+ service :in_app_purchase
+ service :passbook
+ } unless block_given?
+
+ instance_eval(&block)
+
+ super(@services)
+ end
+
+ private
+
+ def service(identifier, options = {}, &block)
+ if identifier.is_a?(Class)
+ middleware = identifier
+ else
+ begin
+ middleware = Helios::Backend.const_get(constantize(identifier))
+ rescue NameError
+ raise LoadError, "Could not find matching service for #{identifier.inspect}. You may need to install an additional gem (such as helios-#{identifier})."
+ end
+ end
+
+ middleware.instance_eval{ include Helios::Administerable } if options.fetch(:frontend, true)
+
+ @services << middleware.new(self, options, &block) if middleware
+ end
+
+ def constantize(identifier)
+ identifier.to_s.split(/([[:alpha:]]*)/).select{|c| /[[:alpha:]]/ === c}.map(&:capitalize).join("")
+ end
+ end
+
+ module Administerable
+ attr_accessor :admin
+
+ def admin?
+ !!@admin
+ end
+ end
+end
+
+require 'helios/backend/data'
+require 'helios/backend/push-notification'
+require 'helios/backend/in-app-purchase'
+require 'helios/backend/passbook'
37 lib/helios/backend/data.rb
@@ -0,0 +1,37 @@
+require 'rack/core-data'
+require 'sinatra/param'
+
+class Helios::Backend::Data < Sinatra::Base
+ helpers Sinatra::Param
+
+ def initialize(app, options = {})
+ super(Rack::CoreData(options[:model]))
+
+ @model = Rack::CoreData::DataModel.new(options[:model])
+ end
+
+ before do
+ content_type :json
+ end
+
+ helpers Sinatra::Param
+
+ options '/' do
+ pass unless self.class < Helios::Administerable
+
+ links = []
+ @model.entities.each do |entity|
+ links << %{</#{entity.name.downcase.pluralize}>; rel="resource"}
+ end
+
+ response['Link'] = links.join("\n")
+
+ @model.entities.collect{ |entity|
+ {
+ name: entity.name,
+ url: "/#{entity.name.downcase.pluralize}",
+ attributes: Hash[entity.attributes.collect{|attribute| [attribute.name, attribute.type]}]
+ }
+ }.to_json
+ end
+end
41 lib/helios/backend/in-app-purchase.rb
@@ -0,0 +1,41 @@
+require 'rack/in-app-purchase'
+require 'sinatra/param'
+
+class Helios::Backend::InAppPurchase < Sinatra::Base
+ helpers Sinatra::Param
+
+ def initialize(app, options = {})
+ super(Rack::InAppPurchase.new)
+ end
+
+ before do
+ content_type :json
+ end
+
+ helpers Sinatra::Param
+
+ get '/receipts' do
+ param :q, String
+
+ receipts = Rack::InAppPurchase::Receipt.dataset
+ receipts = receipts.filter("tsv @@ to_tsquery('english', ?)", "#{params[:q]}:*") if params[:q] and not params[:q].empty?
+
+ if params[:page] or params[:per_page]
+ param :page, Integer, default: 1, min: 1
+ param :per_page, Integer, default: 100, in: (1..100)
+
+ {
+ receipts: receipts.limit(params[:per_page], (params[:page] - 1) * params[:per_page]).naked.all,
+ page: params[:page],
+ total: receipts.count
+ }.to_json
+ else
+ param :limit, Integer, default: 100, in: (1..100)
+ param :offset, Integer, default: 0, min: 0
+
+ {
+ receipts: receipts.limit(params[:limit], params[:offset]).naked.all
+ }.to_json
+ end
+ end
+end
35 lib/helios/backend/passbook.rb
@@ -0,0 +1,35 @@
+require 'rack/passbook'
+require 'sinatra/param'
+
+class Helios::Backend::Passbook < Sinatra::Base
+ helpers Sinatra::Param
+
+ def initialize(app, options = {})
+ super(Rack::Passbook.new)
+ end
+
+ get '/passes' do
+ param :q, String
+
+ passes = Rack::Passbook::Pass.dataset
+ passes = passes.filter("tsv @@ to_tsquery('english', ?)", "#{params[:q]}:*") if params[:q] and not params[:q].empty?
+
+ if params[:page] or params[:per_page]
+ param :page, Integer, default: 1, min: 1
+ param :per_page, Integer, default: 100, in: (1..100)
+
+ {
+ passes: passes.limit(params[:per_page], (params[:page] - 1) * params[:per_page]).naked.all,
+ page: params[:page],
+ total: passes.count
+ }.to_json
+ else
+ param :limit, Integer, default: 100, in: (1..100)
+ param :offset, Integer, default: 0, min: 0
+
+ {
+ passes: passes.limit(params[:limit], params[:offset]).naked.all
+ }.to_json
+ end
+ end
+end
97 lib/helios/backend/push-notification.rb
@@ -0,0 +1,97 @@
+require 'rack/push-notification'
+require 'sinatra/param'
+
+class Helios::Backend::PushNotification < Sinatra::Base
+ helpers Sinatra::Param
+
+ def initialize(app, options = {})
+ super(Rack::PushNotification.new)
+ end
+
+ get '/devices/?' do
+ param :q, String
+
+ devices = ::Rack::PushNotification::Device.dataset
+ devices = devices.filter("tsv @@ to_tsquery('english', ?)", "#{params[:q]}:*") if params[:q] and not params[:q].empty?
+
+ if params[:page] or params[:per_page]
+ param :page, Integer, default: 1, min: 1
+ param :per_page, Integer, default: 100, in: (1..100)
+
+ {
+ devices: devices.limit(params[:per_page], (params[:page] - 1) * params[:per_page]),
+ page: params[:page],
+ total: devices.count
+ }.to_json
+ else
+ param :limit, Integer, default: 100, in: (1..100)
+ param :offset, Integer, default: 0, min: 0
+
+ {
+ devices: devices.limit(params[:limit], params[:offset])
+ }.to_json
+ end
+ end
+
+ get '/devices/:token/?' do
+ record = ::Rack::PushNotification::Device.find(token: params[:token])
+
+ if record
+ {device: record}.to_json
+ else
+ status 404
+ end
+ end
+
+ head '/message' do
+ status 503 and return unless client
+
+ status 204
+ end
+
+ post '/message' do
+ status 503 and return unless client
+
+ param :payload, String, empty: false
+ param :tokens, Array, empty: false
+
+ tokens = params[:tokens] || ::Rack::PushNotification::Device.all.collect(&:token)
+
+ options = JSON.parse(params[:payload])
+ options[:alert] = options["aps"]["alert"]
+ options[:badge] = options["aps"]["badge"]
+ options[:sound] = options["aps"]["sound"]
+ options.delete("aps")
+
+ begin
+ notifications = tokens.collect{|token| Houston::Notification.new(options.update({device: token}))}
+ client.push(*notifications)
+
+ status 204
+ rescue => error
+ status 500
+
+ {error: error}.to_json
+ end
+ end
+
+ private
+
+ def client
+ begin
+ return nil unless settings.apn_certificate and ::File.exist?(settings.apn_certificate)
+
+ client = case settings.apn_environment.to_sym
+ when :development
+ Houston::Client.development
+ when :production
+ Houston::Client.production
+ end
+ client.certificate = ::File.read(settings.apn_certificate)
+
+ return client
+ rescue
+ return nil
+ end
+ end
+end
4 lib/helios/commands.rb
@@ -0,0 +1,4 @@
+require 'helios/commands/new'
+require 'helios/commands/link'
+require 'helios/commands/console'
+require 'helios/commands/server'
29 lib/helios/commands/console.rb
@@ -0,0 +1,29 @@
+command :console do |c|
+ c.syntax = 'helios console'
+ c.summary = 'Open IRB session with Helios environment'
+
+ c.action do |args, options|
+ require 'irb'
+ require 'foreman/env'
+ require 'sequel'
+
+ @env = {}
+ Foreman::Env.new(".env").entries do |name, value|
+ @env[name] = value
+ end
+
+ Sequel.connect(@env['DATABASE_URL'])
+
+ require 'rack/core-data'
+ require 'rack/push-notification'
+ require 'rack/in-app-purchase'
+ require 'rack/passbook'
+
+ include Rack
+
+ ARGV.clear
+ IRB.start
+ end
+end
+
+alias_command :c, :console
19 lib/helios/commands/link.rb
@@ -0,0 +1,19 @@
+require 'fileutils'
+
+command :link do |c|
+ c.syntax = 'helios link path/to/Model.xcdatamodel'
+ c.summary = 'Links a Core Data model'
+
+ c.action do |args, options|
+ say_error "Missing argument: path/to/Model.xcdatamodel" and abort if args.empty?
+ path = args.first
+
+ begin
+ File.link(path, File.basename(path))
+ say_ok "Xcode data model successfully linked"
+ say "Any changes made to the data model file will automatically be propagated to Helios the next time the server is started."
+ rescue => exception
+ say_error exception.message and abort
+ end
+ end
+end
77 lib/helios/commands/new.rb
@@ -0,0 +1,77 @@
+require 'fileutils'
+
+command :new do |c|
+ c.syntax = 'helios new path/to/app'
+ c.summary = 'Creates a new Helios project'
+ c.description = <<-EOF
+ The `helios new` command creates a new Helios application with a default
+ directory structure and configuration at the path you specify.
+ EOF
+ # c.example = <<-EOF
+ # helios new #{File.join(Dir.pwd, "app")}
+
+ # This generates a skeletal Helios installation in #{File.join(Dir.pwd, "app")}.
+ # See the README in the newly created application to get going.
+ # EOF
+
+ c.option '--skip-gemfile', "Don't create a Gemfile"
+ c.option '-B', '--skip-bundle', "Don't run bundle install"
+ c.option '-G', '--skip-git', "Don't create a git repository"
+
+ c.option '--edge', "Setup the application with Gemfile pointing to Helios repository"
+
+ c.option '-f', '--force', "Overwrite files that already exist"
+ c.option '-p', '--pretend', "Run but do not make any changes"
+ c.option '-s', '--skip', "Skip files that already exist"
+
+ c.action do |args, options|
+ say_error "Missing argument: path/to/app" and abort if args.empty?
+ path = args.first
+ app_name = path.split(/\//).last
+
+ begin
+ FileUtils.mkdir_p(path) and Dir.chdir(path)
+ log "create", ""
+
+ Dir.glob(File.join(File.dirname(__FILE__), "../templates/") + "*.erb", File::FNM_DOTMATCH).each do |template|
+ file = File.basename(template, ".erb")
+ erb = ERB.new(File.read(template))
+
+ next if file === "Gemfile" and options.skip_gemfile
+
+ if File.exist?(file)
+ if options.force and not options.skip
+ log "overwrite", file
+ else
+ log "exists", file
+ end
+ else
+ log "create", file
+ end
+
+ next if options.pretend
+
+ File.open(file, 'w') do |f|
+ f.puts erb.result binding
+ end
+ end
+
+ unless options.skip_bundle or not File.exist?("Gemfile")
+ log "run", "bundle install"
+ system 'bundle install'
+ end
+
+ unless options.skip_git
+ log "run", "git init"
+ system 'git init'
+ system 'git add .'
+ system 'git commit -m "Initial Commit"'
+ end
+ rescue => exception
+ say_error exception.message and abort
+ end
+ end
+end
+
+alias_command :create, :new
+alias_command :generate, :new
16 lib/helios/commands/server.rb
@@ -0,0 +1,16 @@
+command :server do |c|
+ c.syntax = 'helios server'
+ c.summary = 'Start running Helios locally'
+
+ c.action do |args, options|
+ begin
+ exec 'foreman start'
+ rescue => exception
+ say_error exception.message and abort
+ end
+ end
+end
+
+alias_command :s, :server
+alias_command :start, :server
+alias_command :launch, :server
74 lib/helios/frontend.rb
@@ -0,0 +1,74 @@
+require 'sinatra/base'
+require 'sinatra/assetpack'
+require 'sinatra/backbone'
+require 'sinatra/support'
+require 'rack/contrib'
+
+require 'compass'
+require 'zurb-foundation'
+
+module Helios
+ class Frontend < Sinatra::Base
+ set :root, File.join(File.dirname(__FILE__), "frontend")
+ set :sass, load_paths: ["#{self.root}/stylesheets"]
+
+ register Sinatra::CompassSupport
+ register Sinatra::AssetPack
+ register Sinatra::JstPages
+
+ use Rack::BounceFavicon
+
+ assets do
+ serve '/javascripts', from: '/javascripts'
+ serve '/stylesheets', from: '/stylesheets'
+ serve '/images', from: '/images'
+ serve '/fonts', from: '/fonts'
+
+ js :application, '/javascripts/application.js', [
+ 'javascripts/vendor/jquery.js',
+ 'javascripts/vendor/underscore.js',
+ 'javascripts/vendor/backbone.js',
+ 'javascripts/vendor/backbone.paginator.js',
+ 'javascripts/vendor/backbone.datagrid.js',
+ 'javascripts/vendor/codemirror.js',
+ 'javascripts/vendor/codemirror.javascript.js',
+ 'javascripts/vendor/foundation.js',
+ 'javascripts/vendor/foundation/foundation.dropdown.js',
+ 'javascripts/vendor/foundation/foundation.reveal.js',
+ 'javascripts/vendor/foundation/*',
+ 'javascripts/vendor/date.js',
+ 'javascripts/helios.js',
+ 'javascripts/helios/models.js',
+ 'javascripts/helios/collections.js',
+ 'javascripts/helios/templates.js',
+ 'javascripts/helios/views.js',
+ 'javascripts/helios/router.js',
+ ]
+
+ css :application, '/stylesheets/application.css', [
+ 'stylesheets/screen.css'
+ ]
+ end
+
+ set :views, settings.root + '/templates'
+ serve_jst '/javascripts/helios/templates.js'
+
+ get '' do
+ redirect request.fullpath + "/"
+ end
+
+ get '/' do
+ haml :'../views/index'
+ end
+ end
+end
+
+module Sinatra
+ module AssetPack
+ class Package
+ def production_path
+ add_cache_buster(@path, *files).gsub(/^\//, "")
+ end
+ end
+ end
+end
BIN  lib/helios/frontend/fonts/icons.eot
Binary file not shown
BIN  lib/helios/frontend/fonts/icons.ttf
Binary file not shown
BIN  lib/helios/frontend/fonts/icons.woff
Binary file not shown
BIN  lib/helios/frontend/images/bg.jpg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
36 lib/helios/frontend/javascripts/helios.coffee
@@ -0,0 +1,36 @@
+window.Helios = {
+ Version: "0.0.1"
+
+ Models: {}
+ Collections: {}
+ Views: {}
+ Routers: {}
+
+ initialize: ->
+ window.app = new Helios.Routers.Root
+ for entity in Helios.entities.models
+ do (entity) ->
+ name = entity.get('name').toLowerCase()
+ window.app[name] = ->
+ @views.entity = new Helios.Views.Entity({model: entity})
+ window.app.route entity.url(), name
+
+ window.app.views.entities = new Helios.Views.Entities({collection: Helios.entities})
+ window.app.views.entities.render()
+
+ Backbone.history.start({
+ root: window.location.pathname,
+ pushState: false,
+ hashChange: true
+ })
+}
+
+$ ->
+ $(document).foundation()
+ $('body').delegate 'a[href^=#]', 'click', (event) ->
+ event.preventDefault()
+ href = $(this).attr('href')
+ window.app.navigate(href, {trigger: true, replace: true})
+
+ Helios.entities = new Helios.Collections.Entities
+ Helios.entities.fetch(type: 'OPTIONS', success: Helios.initialize, error: Helios.initialize)
91 lib/helios/frontend/javascripts/helios/collections.coffee
@@ -0,0 +1,91 @@
+class Helios.Collection extends Backbone.Paginator.requestPager
+ paginator_ui:
+ firstPage: 1,
+ currentPage: 1,
+ perPage: 20
+
+ server_api:
+ 'q': ->
+ @query || ""
+ 'limit': ->
+ @perPage
+ 'offset': ->
+ (@currentPage - 1) * @perPage
+
+ parse: (response, options) ->
+ if _.isArray(response)
+ response
+ else
+ if response.page?
+ @total = response.total
+ @page = response.page
+ @totalPages = Math.ceil(@total / @perPage)
+
+ _.detect response, (value, key) ->
+ _.isArray(value)
+
+class Helios.Collections.Entities extends Backbone.Collection
+ model: Helios.Models.Entity
+ url: '/'
+
+ parse: (response, options) ->
+ if _.isArray(response)
+ response
+ else
+ if response.page?
+ @total = response.total
+ @page = response.page
+ @totalPages = Math.ceil(@total / @perPage)
+
+ _.detect response, (value, key) ->
+ _.isArray(value)
+
+class Helios.Collections.Resources extends Backbone.Collection
+ model: Helios.Models.Resource
+
+ parse: (response, options) ->
+ if _.isArray(response)
+ response
+ else
+ if response.page?
+ @total = response.total
+ @page = response.page
+ @totalPages = Math.ceil(@total / @perPage)
+
+ _.detect response, (value, key) ->
+ _.isArray(value)
+
+class Helios.Collections.Devices extends Helios.Collection
+ model: Helios.Models.Device
+ fields: ['token', 'alias', 'badge', 'locale', 'language', 'timezone', 'ip_address', 'lat', 'lng']
+
+ paginator_core:
+ type: 'GET'
+ dataType: 'json'
+ url: '/devices?'
+
+ comparator: (device) ->
+ device.get('token')
+
+
+class Helios.Collections.Receipts extends Helios.Collection
+ model: Helios.Models.Receipt
+ url: '/receipts'
+ fields: ['transaction_id', 'product_id', 'purchase_date', 'original_transaction_id', 'original_purchase_date', 'app_item_id', 'version_external_identifier', 'bid', 'bvrs', 'ip_address']
+
+ paginator_core:
+ type: 'GET'
+ dataType: 'json'
+ url: '/receipts?'
+
+
+class Helios.Collections.Passes extends Helios.Collection
+ model: Helios.Models.Pass
+ url: '/passes'
+ fields: ['pass_type_identifier', 'serial_number', 'authentication_token']
+
+ paginator_core:
+ type: 'GET'
+ dataType: 'json'
+ url: '/passes?'
+
20 lib/helios/frontend/javascripts/helios/models.coffee
@@ -0,0 +1,20 @@
+class Helios.Models.Entity extends Backbone.Model
+ idAttribute: "name"
+ url: ->
+ @get('resources').url.replace(/^\//, 'data/')
+
+ parse: (response) ->
+ response.resources = new Helios.Collections.Resources()
+ response.resources.url = response.url
+ response
+
+class Helios.Models.Resource extends Backbone.Model
+ idAttribute: "url"
+
+class Helios.Models.Device extends Backbone.Model
+ idAttribute: "token"
+
+class Helios.Models.Receipt extends Backbone.Model
+ idAttribute: "transaction_id"
+
+class Helios.Models.Pass extends Backbone.Model
38 lib/helios/frontend/javascripts/helios/router.coffee
@@ -0,0 +1,38 @@
+class Helios.Routers.Root extends Backbone.Router
+ el:
+ "div[role='main']"
+
+ initialize: (options) ->
+ @views = {}
+ super
+
+ routes:
+ '': 'index'
+ 'data': 'data'
+ 'push-notification': 'push_notification'
+ 'in-app-purchase': 'in_app_purchase'
+ 'passbook': 'passbook'
+
+ index: ->
+ Helios.entities.fetch(type: 'OPTIONS')
+
+ data: ->
+ Helios.entities.fetch(type: 'OPTIONS')
+ @views.entities.render()
+
+ push_notification: ->
+ @devices ?= new Helios.Collections.Devices
+ @views.devices ?= new Helios.Views.Devices(collection: @devices)
+ @views.devices.render()
+
+ in_app_purchase: ->
+ @receipts ?= new Helios.Collections.Receipts
+ @views.receipts ?= new Helios.Views.Receipts(collection: @receipts)
+ @views.receipts.render()
+
+ passbook: ->
+ @passes ?= new Helios.Collections.Passes
+ @views.passes ?= new Helios.Views.Passes(collection: @passes)
+ @views.passes.render()
+
+
233 lib/helios/frontend/javascripts/helios/views.coffee
@@ -0,0 +1,233 @@
+class Helios.Views.Entities extends Backbone.View
+ template: JST['entities']
+ el: "[role='main']"
+
+ events:
+ 'change #entities': ->
+ window.app.navigate(@$el.find("#entities").val(), {trigger: true})
+
+ initialize: ->
+ @collection.on 'reset', @render
+
+ render: =>
+ @$el.html(@template(entities: @collection))
+
+ @
+
+class Helios.Views.Entity extends Backbone.View
+ el: "[role='main']"
+
+ initialize: ->
+ @model.on 'reset', @render
+
+ @collection = @model.get('resources')
+ @collection.fetch({success: @render})
+
+ render: =>
+ if @collection
+ @datagrid ?= new Backbone.Datagrid({
+ collection: @collection,
+ columns: @collection.first().attributes.keys,
+ paginated: true,
+ perPage: 20
+ })
+ @$el.find("#datagrid").html(@datagrid.el)
+
+ @
+
+class Helios.Views.Devices extends Backbone.View
+ template: JST['devices']
+ el: "[role='main']"
+
+ events:
+ 'keyup form.filter input': 'filter'
+
+ initialize: ->
+ @datagrid = new Backbone.Datagrid({
+ collection: @collection,
+ columns: @collection.fields,
+ paginated: true,
+ perPage: 20
+ })
+
+ render: =>
+ @$el.html(@template())
+
+ @composeView ?= new Helios.Views.Compose()
+ @composeView.render()
+ @$el.find("#datagrid").html(@datagrid.el)
+
+ @
+
+ filter: (e) ->
+ e.preventDefault()
+ @collection.query = $(e.target).val()
+ @collection.fetch()
+
+class Helios.Views.Compose extends Backbone.View
+ template: JST['compose']
+ el: "#compose-modal"
+
+ events:
+ 'submit form': 'submit'
+ 'click button#send': 'submit'
+ 'keyup textarea': 'updatePreview'
+ 'focus textarea': ->
+ @$el.find("input[type=radio][value=selected]").prop('checked',true)
+
+ initialize: ->
+ window.setInterval(@updateTime, 10000)
+
+ render: ->
+ @$el.html(@template())
+
+ @editor = CodeMirror.fromTextArea(document.getElementById("payload"), {
+ mode: "application/json",
+ theme: "solarized-dark",
+ tabMode: "indent",
+ lineNumbers : true,
+ matchBrackets: true
+ })
+
+ @updatePreview()
+ @updateTime()
+
+ # $.ajax("/message"
+ # type: "HEAD"
+
+ # error: (data, status) =>
+ # @disable()
+ # )
+ @
+
+ submit: ->
+ $form = @$el.find("form#compose")
+ payload = @editor.getValue()
+
+ tokens = undefined
+ if $("input[name='recipients']:checked").val() == "specified"
+ tokens = [$form.find("#tokens").val()]
+
+ $.ajax("/message"
+ type: "POST"
+ dataType: "json"
+ data: {
+ tokens: tokens,
+ payload: payload
+ }
+ )
+
+ beforeSend: =>
+ @$el.find(".alert-error, .alert-success").remove()
+
+ success: (data, status) =>
+ alert = """
+ <div class="alert alert-block alert-success">
+ <button type="button" class="close" data-dismiss="alert">×</button>
+ <h4>Push Notification Succeeded</h4>
+ </div>
+ """
+ @$el.prepend(alert)
+
+ error: (data, status) =>
+ alert = """
+ <div class="alert alert-block alert-error">
+ <button type="button" class="close" data-dismiss="alert">×</button>
+ <h4>Push Notification Failed</h4>
+ <p>#{$.parseJSON(data.responseText).error}</p>
+ </div>
+ """
+ @$el.prepend(alert)
+
+
+ disable: ->
+ alert = """
+ <div class="alert alert-block">
+ <button type="button" class="close" data-dismiss="alert">×</button>
+ <h4>Push Notification Sending Unavailable</h4>
+ <p>Check that Rack::PushNotification initializes with a <tt>:certificate</tt> parameter, and that the certificate exists and is readable in the location specified.</p>
+ </div>
+ """
+
+ @$el.prepend(alert)
+
+ $(".iphone").css(opacity: 0.5)
+
+ $form = @$el.find("form#compose")
+ $form.css(opacity: 0.5)
+ $form.find("input").disable()
+
+ updatePreview: ->
+ try
+ json = $.parseJSON(@editor.getValue())
+ if alert = json.aps.alert
+ $(".preview p").text(alert)
+
+ catch error
+ $(".alert strong").text(error.name)
+ $(".alert span").text(error.message)
+ finally
+ if alert? and alert.length > 0
+ $(".notification").show()
+ $(".alert").hide()
+ else
+ $(".notification").hide()
+ $(".alert").show()
+
+ updateTime: ->
+ $time = $("time")
+ $time.attr("datetime", Date.now().toISOString())
+ $time.find(".time").text(Date.now().toString("HH:mm"))
+ $time.find(".date").text(Date.now().toString("dddd, MMMM d"))
+
+class Helios.Views.Receipts extends Backbone.View
+ template: JST['receipts']
+ el: "[role='main']"
+
+ events:
+ 'keyup form.filter input': 'filter'
+
+ initialize: ->
+ @datagrid = new Backbone.Datagrid({
+ collection: @collection,
+ columns: @collection.fields,
+ paginated: true,
+ perPage: 20
+ })
+
+ render: =>
+ @$el.html(@template())
+ @$el.find("#datagrid").html(@datagrid.el)
+
+ @
+
+ filter: (e) ->
+ e.preventDefault()
+ @collection.query = $(e.target).val()
+ @collection.fetch()
+
+class Helios.Views.Passes extends Backbone.View
+ template: JST['passes']
+ el: "[role='main']"
+
+ events:
+ 'keyup form.filter input': 'filter'
+
+ initialize: ->
+ @datagrid = new Backbone.Datagrid({
+ collection: @collection,
+ columns: @collection.fields,
+ paginated: true,
+ perPage: 20
+ })
+
+ render: =>
+ @$el.html(@template())
+ @$el.find("#datagrid").html(@datagrid.el)
+
+ @
+
+ filter: (e) ->
+ e.preventDefault()
+ @collection.query = $(e.target).val()
+ @collection.fetch()
662 lib/helios/frontend/javascripts/vendor/backbone.datagrid.js
@@ -0,0 +1,662 @@
+// backbone.datagrid v0.3.2
+//
+// Copyright (c) 2012 Loïc Frering <loic.frering@gmail.com>
+// Distributed under the MIT license
+
+(function() {
+
+var Datagrid = Backbone.View.extend({
+ initialize: function() {
+ this.columns = this.options.columns;
+ this.options = _.defaults(this.options, {
+ paginated: false,
+ page: 1,
+ perPage: 10,
+ tableClassName: 'table',
+ emptyMessage: '<p>No results found.</p>'
+ });
+
+ this.collection.on('reset', this.render, this);
+ this._prepare();
+ },
+
+ render: function() {
+ this.$el.empty();
+ this.renderTable();
+ if (this.options.paginated) {
+ this.renderPagination();
+ }
+
+ return this;
+ },
+
+ renderTable: function() {
+ var $table = $('<table></table>', {'class': this.options.tableClassName});
+ this.$el.append($table);
+
+ var header = new Header({columns: this.columns, sorter: this.sorter});
+ $table.append(header.render().el);
+
+ $table.append('<tbody></tbody>');
+
+ if (this.collection.isEmpty()) {
+ this.$el.append(this.options.emptyMessage);
+ } else {
+ this.collection.forEach(this.renderRow, this);
+ }
+ },
+
+ renderPagination: function() {
+ var pagination = new Pagination({pager: this.pager});
+ this.$el.append(pagination.render().el);
+ },
+
+ renderRow: function(model) {
+ var options = {
+ model: model,
+ columns: this.columns
+ };
+ var rowClassName = this.options.rowClassName;
+ if (_.isFunction(rowClassName)) {
+ rowClassName = rowClassName(model);
+ }
+ options.className = rowClassName;
+
+ var row = new Row(options);
+ this.$('tbody').append(row.render(this.columns).el);
+ },
+
+ refresh: function(options) {
+ if (this.options.paginated) {
+ this._page(options);
+ } else {
+ if (this.options.inMemory) {
+ this.collection.trigger('reset', this.collection);
+ if (options && options.success) {
+ options.success();
+ }
+ } else {
+ this._request(options);
+ }
+ }
+ },
+
+ sort: function(column, order) {
+ this.sorter.sort(column, order);
+ },
+
+ page: function(page) {
+ this.pager.page(page);
+ },
+
+ perPage: function(perPage) {
+ this.pager.set('perPage', perPage);
+ },
+
+ _sort: function() {
+ if (this.options.inMemory) {
+ this._sortInMemory();
+ } else {
+ this._sortRequest();
+ }
+ },
+
+ _sortInMemory: function() {
+ if (this.options.paginated) {
+ this._originalCollection.comparator = _.bind(this._comparator, this);
+ this._originalCollection.sort();
+ this.page(1);
+ } else {
+ this.collection.comparator = _.bind(this._comparator, this);
+ this.collection.sort();
+ }
+ },
+
+ _comparator: function(model1, model2) {
+ var columnComparator = this._comparatorForColumn(this.sorter.get('column'));
+ var order = columnComparator(model1, model2);
+ return this.sorter.sortedASC() ? order : -order;
+ },
+
+ _comparatorForColumn: function(column) {
+ var c = _.find(this.columns, function(c) {
+ return c.property === column || c.index === column;
+ });
+ return c ? c.comparator : undefined;
+ },
+
+ _sortRequest: function() {
+ this._request();
+ },
+
+ _page: function(options) {
+ if (this.options.inMemory) {
+ this._pageInMemory(options);
+ } else {
+ this._pageRequest(options);
+ }
+ },
+
+ _pageRequest: function(options) {
+ this._request(options);
+ },
+
+ _request: function(options) {
+ options = options || {};
+ var success = options.success;
+ var silent = options.silent;
+
+ options.data = this._getRequestData();
+ options.success = _.bind(function(collection) {
+ if (!this.columns || _.isEmpty(this.columns)) {
+ this._prepareColumns();
+ }
+ if (success) {
+ success();
+ }
+ if (this.options.paginated) {
+ this.pager.update(collection);
+ }
+ if (!silent) {
+ collection.trigger('reset', collection);
+ }
+ }, this);
+ options.silent = true;
+
+ this.collection.fetch(options);
+ },
+
+ _getRequestData: function() {
+ if (this.collection.data && _.isFunction(this.collection.data)) {
+ return this.collection.data(this.pager, this.sorter);
+ } else if (this.collection.data && typeof this.collection.data === 'object') {
+ var data = {};
+ _.each(this.collection.data, function(value, param) {
+ if (_.isFunction(value)) {
+ value = value(this.pager, this.sorter);
+ }
+ data[param] = value;
+ }, this);
+ return data;
+ } else if (this.options.paginated) {
+ return {
+ page: this.pager.get('currentPage'),
+ per_page: this.pager.get('perPage')
+ };
+ }
+
+ return {};
+ },
+
+ _pageInMemory: function(options) {
+ if (!this._originalCollection) {
+ this._originalCollection = this.collection.clone();
+ }
+
+ var page = this.pager.get('currentPage');
+ var perPage = this.pager.get('perPage');
+
+ var begin = (page - 1) * perPage;
+ var end = begin + perPage;
+
+ if (options && options.success) {
+ options.success();
+ }
+ this.pager.set('total', this._originalCollection.size());
+
+ this.collection.reset(this._originalCollection.slice(begin, end), options);
+ },
+
+ _prepare: function() {
+ this._prepareSorter();
+ this._preparePager();
+ this._prepareColumns();
+ this.refresh();
+ },
+
+ _prepareSorter: function() {
+ this.sorter = new Sorter();
+ this.sorter.on('change', function() {
+ this._sort(this.sorter.get('column'), this.sorter.get('order'));
+ }, this);
+ },
+
+ _preparePager: function() {
+ this.pager = new Pager({
+ currentPage: this.options.page,
+ perPage: this.options.perPage
+ });
+
+ this.pager.on('change:currentPage', function () {
+ this._page();
+ }, this);
+ this.pager.on('change:perPage', function() {
+ this.page(1);
+ }, this);
+ },
+
+ _prepareColumns: function() {
+ if (!this.columns || _.isEmpty(this.columns)) {
+ this._defaultColumns();
+ } else {
+ _.each(this.columns, function(column, i) {
+ this.columns[i] = this._prepareColumn(column, i);
+ }, this);
+ }
+ },
+
+ _prepareColumn: function(column, index) {
+ if (_.isString(column)) {
+ column = { property: column };
+ }
+ if (_.isObject(column)) {
+ column.index = index;
+ if (column.property) {
+ column.title = column.title || this._formatTitle(column.property);
+ } else if (!column.property && !column.view) {
+ throw new Error('Column \'' + column.title + '\' has no property and must accordingly define a custom cell view.');
+ }
+ if (this.options.inMemory && column.sortable) {
+ if (!column.comparator && !column.property && !column.sortedProperty) {
+ throw new Error('Invalid column definition: a sortable column must have a comparator, property or sortedProperty defined.');
+ }
+ column.comparator = column.comparator || this._defaultComparator(column.sortedProperty || column.property);
+ }
+ }
+ return column;
+ },
+
+ _formatTitle: function(title) {
+ return _.map(title.split(/\s|_/), function(word) {
+ return word.charAt(0).toUpperCase() + word.substr(1);
+ }).join(' ');
+ },
+
+ _defaultColumns: function() {
+ this.columns = [];
+ var model = this.collection.first(), i = 0;
+ if (model) {
+ for (var p in model.toJSON()) {
+ this.columns.push(this._prepareColumn(p, i++));
+ }
+ }
+ },
+
+ _defaultComparator: function(column) {
+ return function(model1, model2) {
+ var val1 = model1.has(column) ? model1.get(column) : '';
+ var val2 = model2.has(column) ? model2.get(column) : '';
+ return val1.localeCompare(val2);
+ };
+ }
+});
+
+var Header = Datagrid.Header = Backbone.View.extend({
+ tagName: 'thead',
+
+ initialize: function() {
+ this.columns = this.options.columns;
+ this.sorter = this.options.sorter;
+ },
+
+ render: function() {
+ var model = new Backbone.Model();
+ var headerColumn, columns = [];
+ _.each(this.columns, function(column, i) {
+ headerColumn = _.clone(column);
+ headerColumn.property = column.property || column.index;
+ headerColumn.view = column.headerView || {
+ type: HeaderCell,
+ sorter: this.sorter
+ };
+
+ model.set(headerColumn.property, column.title);
+ columns.push(headerColumn);
+ }, this);
+
+ var row = new Row({model: model, columns: columns, header: true});
+ this.$el.html(row.render().el);
+
+ return this;
+ }
+});
+
+var Row = Datagrid.Row = Backbone.View.extend({
+ tagName: 'tr',
+
+ initialize: function() {
+ this.columns = this.options.columns;
+ this.model.on('change', this.render, this);
+ },
+
+ render: function() {
+ this.$el.empty();
+ _.each(this.columns, this.renderCell, this);
+ return this;
+ },
+
+ renderCell: function(column) {
+ var cellView = this._resolveCellView(column);
+ this.$el.append(cellView.render().el);
+ },
+
+ _resolveCellView: function(column) {
+ var options = {
+ model: this.model,
+ column: column
+ };
+ if (this.options.header || column.header) {
+ options.tagName = 'th';
+ }
+ var cellClassName = column.cellClassName;
+ if (_.isFunction(cellClassName)) {
+ cellClassName = cellClassName(this.model);
+ }
+ options.className = cellClassName;
+
+
+ var view = column.view || Cell;
+
+ // Resolve view from string or function
+ if (typeof view !== 'object' && !(view.prototype && view.prototype.render)) {
+ if (_.isString(view)) {
+ options.callback = _.template(view);
+ view = CallbackCell;
+ } else if (_.isFunction(view) && !view.prototype.render) {
+ options.callback = view;
+ view = CallbackCell;
+ } else {
+ throw new TypeError('Invalid view passed to column "' + column.title + '".');
+ }
+ }
+
+ // Resolve view from options
+ else if (typeof view === 'object') {
+ _.extend(options, view);
+ view = view.type;
+ if (!view || !view.prototype || !view.prototype.render) {
+ throw new TypeError('Invalid view passed to column "' + column.title + '".');
+ }
+ }
+
+ return new view(options);
+ }
+});
+
+var Pagination = Datagrid.Pagination = Backbone.View.extend({
+ className: 'pagination pagination-centered',
+
+ events: {
+ 'click li:not(.disabled) a': 'page',
+ 'click li.disabled a': function(e) { e.preventDefault(); }
+ },
+
+ initialize: function() {
+ this.pager = this.options.pager;
+ },
+
+ render: function() {
+ var $ul = $('<ul></ul>'), $li;
+
+ $li = $('<li class="prev"><a href="#">«</a></li>');
+ if (!this.pager.hasPrev()) {
+ $li.addClass('disabled');
+ }
+ $ul.append($li);
+
+ if (this.pager.hasTotal()) {
+ for (var i = 1; i <= this.pager.get('totalPages'); i++) {
+ $li = $('<li></li>');
+ if (i === this.pager.get('currentPage')) {
+ $li.addClass('active');
+ }
+ $li.append('<a href="#">' + i + '</a>');
+ $ul.append($li);
+ }
+ }
+
+ $li = $('<li class="next"><a href="#">»</a></li>');
+ if (!this.pager.hasNext()) {
+ $li.addClass('disabled');
+ }
+ $ul.append($li);
+
+ this.$el.append($ul);
+ return this;
+ },
+
+ page: function(event) {
+ var $target = $(event.target), page;
+ if ($target.parent().hasClass('prev')) {
+ this.pager.prev();
+ } else if ($target.parent().hasClass('next')) {
+ this.pager.next();
+ }
+ else {
+ this.pager.page(parseInt($(event.target).html(), 10));
+ }
+ return false;
+ }
+});
+
+var Cell = Datagrid.Cell = Backbone.View.extend({
+ tagName: 'td',
+
+ initialize: function() {
+ this.column = this.options.column;
+ },
+
+ render: function() {
+ this._prepareValue();
+ this.$el.html(this.value);
+ return this;
+ },
+
+ _prepareValue: function() {
+ this.value = this.model.get(this.column.property);
+
+ if (this.value && this.value.length > 32) {
+ this.value = "<div>" + this.value + "</div>";
+ }
+ }
+});
+
+var CallbackCell = Datagrid.CallbackCell = Cell.extend({
+ initialize: function() {
+ CallbackCell.__super__.initialize.call(this);
+ this.callback = this.options.callback;
+ },
+
+ _prepareValue: function() {
+ this.value = this.callback(this.model.toJSON());
+ }
+});
+
+var ActionCell = Datagrid.ActionCell = Cell.extend({
+ initialize: function() {
+ ActionCell.__super__.initialize.call(this);
+ },
+
+ action: function() {
+ return this.options.action(this.model);
+ },
+
+ _prepareValue: function() {
+ var a = $('<a></a>');
+
+ a.html(this.options.label);
+ a.attr('href', this.options.href || '#');
+ if (this.options.actionClassName) {
+ a.addClass(this.options.actionClassName);
+ }
+ if (this.options.action) {