I designed and built continuity-assistant.co.uk from scratch. It will be sold as a monthly service to Health and Safety consultancies.
The customers for this service are Heath and Safety consultants. Each has a portfolio of clients carrying out a wide variety of business like catering and manufacturing. The role of the consultants is to stop their client businesses from stopping, by helping them comply with laws and regulations for their niche. To see the application working, watch these screen casts.
The problem
The challenge is that each client business has a different set of checks needed to stay compliant and safe. The consequences of forgetting a check can range from a fine, loss of income or physical injury. Something as simple as a missed vehicle service or a late hygiene inspection can stop a business from trading for days.
Business owners are understandably more interested in other things and have been known to let some of these checks slip. They need an easy way to see a summary of when essential checks are due, and alerts by SMS and email. Senior managers only need to know about exceptional circumstances while people in other roles must receive more routine alerts. The management structure of each business can’t be predicted, so there needs to be a way to configure who gets each kind of alert.
In the past the same problem has been partially solved using spreadsheets, calendar apps and desktop databases like MS Access. There have been expensive and niche products that help particular industries keep compliant, but we need something more general.
What’s needed is a secure on line service that can easily be tailored to represent the liabilities of any business and to provide reminders about any kind of check, over any timescale.
Most of all, it has to be dead easy to use, otherwise business owners will not get past the trial. (Don’t make me think!)
A practical solution
The app is hosted by Heroku. Previously I’ve hosted Ruby on Rails apps on bespoke servers and EC2 instances, but installing Ruby and all the gems can be time consuming, not to mention the work needed when either Ruby or Rails or any of the gems needs to be updated.
Data is held in a Postgres SQL server on Heroku but I use MySQL locally, mostly its got a better admin UI (www.sequelpro.com). This has lead to a few SQL syntax problems.
In this project, Rails is only used for authentication, authorisation and fetching and storing model data. I removed all of the View and Helper code.
I reckon the roles of the Rails Asset Pipeline and Turbolinks are better done by grunt and bower. My productivity has gone up a notch since I stopped wrestling with asset pipeline issues.
Data is sent as JSON between the server and browser. I love the simplicity of JSON, just old fashioned text in hashes and arrays that can be brought to life in Javascript as objects. Angular has a useful filter called json that presents data very clearly on a web page.
How files are arranged
[aesop_image img=”http://35.176.131.37/wp-content/uploads/2015/03/rails-angular-files.jpg” offset=”120px” align=”left” lightbox=”on” captionposition=”left”]
The app is a Rails project, with no views or helpers. Instead there is a client directory, first created by Yeoman and this is where the Angular code is kept.
Javascript controllers, tests and views would be better grouped into functional groups but for now I’ve got them in directories for directives, controllers, filters and views.
When developing locally I run one Rails in one Terminal and ‘grunt serve’ in the other. The app is available at localhost:9000. Once the code is working locally, I run the ‘grunt build –force’ command and wait until the JS, HTML and SASS are all minified and copied into the Rails public directory. Then I push the changes to the master branch of git and deploy to the Heroku staging server.
This needs a bit of configuration in Gruntfile.js;
// The actual grunt server settings grunt.initConfig({ ... connect: { options: { port: 9000, // Change this to '0.0.0.0' to access the server from outside. hostname: 'localhost', livereload: 35729 }, proxies: [ { context: '/api', host: 'localhost', port: 3000 } ],
Rails REST API
The Rails app has no views, it just deals with authentication and an api configured in routes.rb like this;
Reminders::Application.routes.draw do # Tell the router to use the user/sessions controller devise_for :users, :controllers => { sessions: "users/sessions" } devise_scope:user do post '/check/is_user' => 'users/users#is_user', as: 'is_user' post '/api/v1/current_user' => 'api/v1/sessions#get_current_user' end namespace :api do namespace :v1 do devise_for :users resources :people resources :acting_capacities resources :liability_types do resources :check_types resources :liability_type_fields end resources :liabilities ... end end end
These routes are served as json, not html, by Rails controllers in development from MySQL or Postgres on Heroku.
Authentication
This was the hardest part of setting up the REST API between Rails and Angular. Rather than reinvent the wheel, I used the Devise gem. Most of the controllers need the user to be authenticated.
before_action :authenticate_user!
If the user logs in successfully, the SessionController’s create method responds with JSON containing the name and a token. The Angular Javascript on the browser then stores this data and sends it with every request.
ApplicationController uses acts_as_token_authentication_handler_for .
class ApplicationController < ActionController::Base acts_as_token_authentication_handler_for User end
On the client side, if a 401 is received, the app catches it and broadcasts event:unauthorized down the chain. This is handled by changing $location to ‘/login’. One disadvantage is that the dataless UI for the requested page is momentarily displayed. There is no data in it, but it shows up for a blink of the eye.
// Intercept any 401 responses .config(['$httpProvider', function($httpProvider) { var interceptor = ['$rootScope', '$location', '$q', function($scope, $location, $q) { var success = function(resp) { return resp; }, err = function(resp) { if (resp.status === 401) { var d = $q.defer(); $scope.$broadcast('event:unauthorized'); return d.promise; } return $q.reject(resp); }; return function(promise) { return promise.then(success, err); }; }]; $httpProvider.responseInterceptors.push(interceptor); }]) .run(['$rootScope', '$http', '$location', 'tokenHandler', function($rootScope, $http, $location, tokenHandler) { $rootScope.$on('event:unauthorized', function() { tokenHandler.set({}); $location.path('/login'); });
Angular filters
The app makes extensive use of filters to format json.
// Takes a person object and outputs a formated version of their name .filter('politeNameForPerson', function() { return function(p){ var politeName = p.title + " " + p.first_name + " " + p.second_name; return(politeName); }; }) .filter('translateStatus', function() { return function(status_as_a_number){ var statuses = { '-1': 'Not set yet', 0: 'Open', 1: 'Due', 2: 'Imminent', 3: 'Overdue', 4: 'Completed', 5: 'Abandoned'}; return(statuses[status_as_a_number]); }; })