diff --git a/.travis.yml b/.travis.yml index 5b4d3a5..86f189f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,7 +49,7 @@ before_script: # Initiate the generator to build using CLI. We use the --no-insight flag to # prevent yo from waiting for user input about sending anonymous usgae # statistics. - - yo hedley --no-insight --project-name=skeleton --github-repo=https://github.com/Foo/skeleton --db=skeleton --db-user=root --db-pass= --drupal-url=http://127.0.0.1 + - yo hedley --no-insight --project-name=skeleton --github-repo=https://github.com/Foo/skeleton --db=skeleton --db-user=root --db-pass= --drupal-url=http://127.0.0.1 --pusher-key=0 --pusher-secret=0 --pusher-id=0 # Configure client. - cd client diff --git a/app/index.js b/app/index.js index 256a5c5..23daab4 100644 --- a/app/index.js +++ b/app/index.js @@ -57,6 +57,24 @@ module.exports = yeoman.generators.Base.extend({ type: String, required: 'false' }); + + this.option('pusher-key', { + desc: 'The pusher key', + type: String, + required: 'false' + }); + + this.option('pusher-secret', { + desc: 'The pusher secret', + type: String, + required: 'false' + }); + + this.option('pusher-id', { + desc: 'The pusher app ID', + type: String, + required: 'false' + }); }, askForProjectName: function () { @@ -204,6 +222,81 @@ module.exports = yeoman.generators.Base.extend({ }.bind(this)); }, + askForPusherKey: function () { + // Allow passing an empty password. + if (this.options['pusher-key'] !== undefined) { + // Get the value from the CLI. + this.pusherKey = this.options['pusher-key']; + this.log('Setting pusher key: ' + this.pusherKey); + return; + } + + var done = this.async(); + + var prompts = [{ + name: 'pusherKey', + message: 'What is the Pusher key?', + // Empty by default, as otherwise it might not be able to set to blank. + default: '' + }]; + + this.prompt(prompts, function (props) { + this.pusherKey = props.pusherKey; + + done(); + }.bind(this)); + }, + + askForPusherSecret: function () { + // Allow passing an empty password. + if (this.options['pusher-secret'] !== undefined) { + // Get the value from the CLI. + this.pusherSecret = this.options['pusher-secret']; + this.log('Setting pusher secret: ' + this.pusherSecret); + return; + } + + var done = this.async(); + + var prompts = [{ + name: 'pusherSecret', + message: 'What is the Pusher Secret?', + // Empty by default, as otherwise it might not be able to set to blank. + default: '' + }]; + + this.prompt(prompts, function (props) { + this.pusherSecret = props.pusherSecret; + + done(); + }.bind(this)); + }, + + askForPusherId: function () { + // Allow passing an empty password. + if (this.options['pusher-id'] !== undefined) { + // Get the value from the CLI. + this.pusherId = this.options['pusher-id']; + this.log('Setting pusher app ID: ' + this.pusherId); + return; + } + + var done = this.async(); + + var prompts = [{ + name: 'pusherId', + message: 'What is the Pusher App ID?', + // Empty by default, as otherwise it might not be able to set to blank. + default: '' + }]; + + this.prompt(prompts, function (props) { + this.pusherId = props.pusherId; + + done(); + }.bind(this)); + }, + writing: { app: function() { var self = this; @@ -249,7 +342,10 @@ module.exports = yeoman.generators.Base.extend({ if (fileName === 'config.sh') { newContents = newContents .replace(/MYSQL_USERNAME=".*"/g, 'MYSQL_USERNAME="' + self.dbUser + '"') - .replace(/MYSQL_PASSWORD=".*"/g, 'MYSQL_PASSWORD="' + self.dbPass + '"'); + .replace(/MYSQL_PASSWORD=".*"/g, 'MYSQL_PASSWORD="' + self.dbPass + '"') + .replace(//g, self.pusherKey) + .replace(//g, self.pusherSecret) + .replace(//g, self.pusherId); } self.fs.write(newFileName, newContents); diff --git a/app/templates/client/app/index.html b/app/templates/client/app/index.html index 0980f12..3441ba3 100644 --- a/app/templates/client/app/index.html +++ b/app/templates/client/app/index.html @@ -70,6 +70,8 @@

Login with demo / 1234

+ + @@ -87,6 +89,7 @@

Login with demo / 1234

+ diff --git a/app/templates/client/app/scripts/app.js b/app/templates/client/app/scripts/app.js index 4ff7176..3beff7e 100644 --- a/app/templates/client/app/scripts/app.js +++ b/app/templates/client/app/scripts/app.js @@ -16,6 +16,7 @@ angular 'config', 'leaflet-directive', 'LocalStorageModule', + 'pusher-angular', 'ui.router', 'angular-loading-bar' ]) diff --git a/app/templates/client/app/scripts/controllers/events.js b/app/templates/client/app/scripts/controllers/events.js index ca5df47..02e561a 100644 --- a/app/templates/client/app/scripts/controllers/events.js +++ b/app/templates/client/app/scripts/controllers/events.js @@ -8,7 +8,7 @@ * Controller of the clientApp */ angular.module('clientApp') - .controller('EventsCtrl', function ($scope, events, authors, mapConfig, $state, $stateParams, $log) { + .controller('EventsCtrl', function ($scope, events, authors, mapConfig, Events, $state, $stateParams, channelManager, $log) { // Initialize values. $scope.events = events; @@ -44,6 +44,20 @@ angular.module('clientApp') selectedAuthorId($stateParams.userId); } + // Get list of chanels. + var channels = channelManager.getChannels(); + angular.forEach(channels, function(channel, companyId) { + // Listen to event. + channel.bind('new-event', function(data) { + Events.get(companyId, data.userId).then(function(value) { + // Update events list of the company. + angular.extend($scope.events, value); + // Update user's events count. + $scope.authors[data.userId].count = Object.keys(value).length; + }); + }); + }); + // Select marker in the Map. $scope.$on('leafletDirectiveMarker.click', function(event, args) { var stateName = $stateParams.userId ? 'dashboard.byCompany.byUser.events.event' : 'dashboard.byCompany.events.event'; diff --git a/app/templates/client/app/scripts/services/account.js b/app/templates/client/app/scripts/services/account.js index 5e957d1..654a469 100644 --- a/app/templates/client/app/scripts/services/account.js +++ b/app/templates/client/app/scripts/services/account.js @@ -8,7 +8,7 @@ * Service in the clientApp. */ angular.module('clientApp') - .service('Account', function ($q, $http, $timeout, Config, $rootScope, $log) { + .service('Account', function ($q, $http, $timeout, Config, $rootScope, channelManager, $log) { // A private cache key. var cache = {}; @@ -37,6 +37,7 @@ angular.module('clientApp') transformResponse: prepareResponse }).success(function(response) { setCache(response[0]); + setChannels(response[0].companies); deferred.resolve(response[0]); }); @@ -97,4 +98,24 @@ angular.module('clientApp') cache = {}; }); + /** + * Subscribe user to companies channeles. + * + * @param array companies + * The user's companies list. + */ + var setChannels = function(companies) { + if (!companies) { + // User doesn't have repositories yet. + return; + } + + companies.forEach(function (company) { + if (!company.id) { + // repoId is null. + return; + } + channelManager.addChannel(company.id); + }); + }; }); diff --git a/app/templates/client/app/scripts/services/auth.js b/app/templates/client/app/scripts/services/auth.js index 1defe8a..99e7d85 100644 --- a/app/templates/client/app/scripts/services/auth.js +++ b/app/templates/client/app/scripts/services/auth.js @@ -51,6 +51,13 @@ angular.module('clientApp') return !!localStorageService.get('access_token'); }; + /** + * Returns access token. + */ + this.getAccessToken = function() { + return localStorageService.get('access_token'); + }; + /** * Authentication failed, set state to login. */ diff --git a/app/templates/client/app/scripts/services/pusher.js b/app/templates/client/app/scripts/services/pusher.js new file mode 100644 index 0000000..38183ec --- /dev/null +++ b/app/templates/client/app/scripts/services/pusher.js @@ -0,0 +1,88 @@ +'use strict'; + +/** + * @ngdoc service + * @name clientApp.account + * @description + * # account + * Service in the clientApp. + */ +angular.module('clientApp') + .service('channelManager', function ($q, $http, $timeout, $pusher, $log, Config, Auth) { + + var channels = {}; + + this.pusher = null; + + /** + * Get all pusher channels. + * + * @returns {{}} + * Return list of existing channels. + */ + this.getChannels = function () { + return channels; + }; + + /** + * Get Company channel. + * + * @param companyId + * Company ID. + * + * @returns {*} + * Return company's channel. + */ + this.getChannel = function (companyId) { + return (companyId in channels) ? channels[companyId] : null; + }; + + /** + * Add new company's chanel. + * + * @param companyId + * Company ID. + * + * @returns {*} + * Return new company's channel. + */ + this.addChannel = function (companyId) { + if (!!channels[companyId]) { + // Already subscribed to channel. + return; + } + var pusher = $pusher(this.getClient()); + channels[companyId] = pusher.subscribe('private-company-' + companyId); + return channels[companyId]; + }; + + /** + * Get the Pusher object. + * + * @returns {*} + * Returns existing object or creates a new one. + */ + this.getClient = function() { + return this.pusher ? this.pusher : this.createNewPusher(); + }; + + /** + * Create a new Pusher object. + * + * @returns {*} + * Return the Pusher object. + */ + this.createNewPusher = function() { + var pusherConf = { + authEndpoint: Config.backend + '/api/v1.0/pusher_auth', + auth: { + headers: { + "access-token": Auth.getAccessToken() + } + } + }; + this.pusher = new Pusher(Config.pusherKey, pusherConf); + return this.pusher; + }; + + }); diff --git a/app/templates/client/bower.json b/app/templates/client/bower.json index 1ce01bd..36a5861 100644 --- a/app/templates/client/bower.json +++ b/app/templates/client/bower.json @@ -12,7 +12,9 @@ "angular-leaflet-directive": "~0.7.9", "angular-ui-router": "~0.2.13", "angular-local-storage": "~0.1.5", - "angular-loading-bar": "~0.6.0" + "angular-loading-bar": "~0.6.0", + "pusher-angular": "~0.1.5", + "pusher": "~2.2.4" }, "devDependencies": { "angular-mocks": "~1.3.0", diff --git a/app/templates/config.sh b/app/templates/config.sh index 2c78b1e..74288a6 100755 --- a/app/templates/config.sh +++ b/app/templates/config.sh @@ -67,10 +67,21 @@ MYSQL_DB_NAME="skeleton" ## # Post install script. -# function post_install {} +function post_install { + chmod 777 www/sites/default/settings.php + + # Pusher integration. + echo "\$conf['skeleton_pusher_app_key'] = '';" >> www/sites/default/settings.php + echo "\$conf['skeleton_pusher_app_secret'] = '';" >> www/sites/default/settings.php + echo "\$conf['skeleton_pusher_app_id'] = '';" >> www/sites/default/settings.php +} # Post upgrade script. -# function post_upgrade {} +function post_upgrade { + post_install +} # Post reset script. -# function post_reset {} +function post_reset { + post_install +} diff --git a/app/templates/default.config.sh b/app/templates/default.config.sh index cf13197..e80b1ca 100755 --- a/app/templates/default.config.sh +++ b/app/templates/default.config.sh @@ -68,10 +68,21 @@ MYSQL_DB_NAME="skeleton" ## # Post install script. -# function post_install {} +function post_install { + chmod 777 www/sites/default/settings.php + + # Pusher integration. + echo "\$conf['skeleton_pusher_app_key'] = '';" >> www/sites/default/settings.php + echo "\$conf['skeleton_pusher_app_secret'] = '';" >> www/sites/default/settings.php + echo "\$conf['skeleton_pusher_app_id'] = '';" >> www/sites/default/settings.php +} # Post upgrade script. -# function post_upgrade {} +function post_upgrade { + post_install +} # Post reset script. -# function post_reset {} +function post_reset { + post_install +} diff --git a/app/templates/install b/app/templates/install index d405849..4872fde 100644 --- a/app/templates/install +++ b/app/templates/install @@ -137,6 +137,9 @@ symlink_externals # Install the Drupal profile as defined in the config.sh file. install_drupal_profile +# Composer install +composer_install + # Setup the files directory. create_sites_default_files_directory diff --git a/app/templates/scripts/helper-functions.sh b/app/templates/scripts/helper-functions.sh index 5697e69..0e24945 100644 --- a/app/templates/scripts/helper-functions.sh +++ b/app/templates/scripts/helper-functions.sh @@ -187,6 +187,18 @@ function install_drupal_profile { cd $ROOT } +## +# Composer install. +## +function composer_install { + echo -e "${LBLUE}> Composer install${RESTORE}" + + cd $ROOT/www/sites/default/files/composer + composer install + echo + + cd $ROOT +} ## # Create (if not exists) and set the proper file permissions diff --git a/app/templates/scripts/reset b/app/templates/scripts/reset index 03b315e..8ce89c8 100755 --- a/app/templates/scripts/reset +++ b/app/templates/scripts/reset @@ -142,6 +142,9 @@ delete_sites_default_content # Install the installation profile. install_drupal_profile +# Composer install +composer_install + # Setup the files directory. create_sites_default_files_directory diff --git a/app/templates/skeleton/drupal-org.make b/app/templates/skeleton/drupal-org.make index f44e0e4..c53efed 100644 --- a/app/templates/skeleton/drupal-org.make +++ b/app/templates/skeleton/drupal-org.make @@ -8,6 +8,9 @@ projects[admin_menu][version] = "3.0-rc5" projects[admin_views][subdir] = "contrib" projects[admin_views][version] = "1.4" +projects[composer_manager][subdir] = "contrib" +projects[composer_manager][version] = "1.7" + projects[ctools][subdir] = "contrib" projects[ctools][version] = "1.7" @@ -105,4 +108,4 @@ projects[migrate][subdir] = "development" projects[migrate][version] = "2.7" projects[migrate_extras][subdir] = "development" -projects[migrate_extras][version] = 2.5 \ No newline at end of file +projects[migrate_extras][version] = 2.5 diff --git a/app/templates/skeleton/modules/custom/skeleton_event/skeleton_event.module b/app/templates/skeleton/modules/custom/skeleton_event/skeleton_event.module index f1cf336..c327e17 100644 --- a/app/templates/skeleton/modules/custom/skeleton_event/skeleton_event.module +++ b/app/templates/skeleton/modules/custom/skeleton_event/skeleton_event.module @@ -5,3 +5,20 @@ */ include_once 'skeleton_event.features.inc'; + +/** + * Implement hook_node_insert(). + */ +function skeleton_event_node_insert($node) { + if ($node->type != 'event') { + return; + } + $wrapper = entity_metadata_wrapper('node', $node); + $company_id = $wrapper->{OG_AUDIENCE_FIELD}->value(array('identifier' => TRUE)); + + $data = array( + 'userId' => $node->uid, + ); + + skeleton_pusher_trigger_event($company_id, 'new-event', $data); +} diff --git a/app/templates/skeleton/modules/custom/skeleton_pusher/composer.json b/app/templates/skeleton/modules/custom/skeleton_pusher/composer.json new file mode 100644 index 0000000..7301cf4 --- /dev/null +++ b/app/templates/skeleton/modules/custom/skeleton_pusher/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "pusher/pusher-php-server": "2.2" + } +} \ No newline at end of file diff --git a/app/templates/skeleton/modules/custom/skeleton_pusher/skeleton_pusher.info b/app/templates/skeleton/modules/custom/skeleton_pusher/skeleton_pusher.info new file mode 100644 index 0000000..c9767d0 --- /dev/null +++ b/app/templates/skeleton/modules/custom/skeleton_pusher/skeleton_pusher.info @@ -0,0 +1,4 @@ +name = Skeleton Pusher +core = 7.x +package = Skeleton +dependencies[] = composer_manager diff --git a/app/templates/skeleton/modules/custom/skeleton_pusher/skeleton_pusher.module b/app/templates/skeleton/modules/custom/skeleton_pusher/skeleton_pusher.module new file mode 100644 index 0000000..5152360 --- /dev/null +++ b/app/templates/skeleton/modules/custom/skeleton_pusher/skeleton_pusher.module @@ -0,0 +1,51 @@ +trigger('private-company-' . $company_id, $event_name, $data); +} + +/** + * Get a Pusher instance. + * + * @return \Pusher + * The Pusher object. + * + * @throws \RestfulServerConfigurationException + */ +function skeleton_pusher_get_pusher() { + $pusher = &drupal_static(__FUNCTION__); + if ($pusher) { + return $pusher; + } + + $app_key = variable_get('skeleton_pusher_app_key'); + $app_secret = variable_get('skeleton_pusher_app_secret'); + $app_id = variable_get('skeleton_pusher_app_id'); + + if (empty($app_key) || empty($app_secret) || empty($app_id)) { + throw new \RestfulServerConfigurationException('Pusher app is not configured properly.'); + } + + return new Pusher($app_key, $app_secret, $app_id); +} diff --git a/app/templates/skeleton/modules/custom/skeleton_restful/plugins/formatter/simple/SkeletonRestfulFormatterSimple.class.php b/app/templates/skeleton/modules/custom/skeleton_restful/plugins/formatter/simple/SkeletonRestfulFormatterSimple.class.php new file mode 100644 index 0000000..12e30d4 --- /dev/null +++ b/app/templates/skeleton/modules/custom/skeleton_restful/plugins/formatter/simple/SkeletonRestfulFormatterSimple.class.php @@ -0,0 +1,38 @@ +contentType = 'application/problem+json; charset=utf-8'; + return $data; + } + + return $data; + } + + /** + * {@inheritdoc} + */ + public function render(array $structured_data) { + return drupal_json_encode($structured_data); + } +} + diff --git a/app/templates/skeleton/modules/custom/skeleton_restful/plugins/formatter/simple/simple.inc b/app/templates/skeleton/modules/custom/skeleton_restful/plugins/formatter/simple/simple.inc new file mode 100644 index 0000000..8c22f18 --- /dev/null +++ b/app/templates/skeleton/modules/custom/skeleton_restful/plugins/formatter/simple/simple.inc @@ -0,0 +1,8 @@ + t('Simple'), + 'description' => t('Encode using the JSON format without any wrappers.'), + 'name' => 'simple', + 'class' => 'SkeletonRestfulFormatterSimple', +); diff --git a/app/templates/skeleton/modules/custom/skeleton_restful/plugins/restful/user/pusher_auth/1.0/SkeletonPusherAuthResource.class.php b/app/templates/skeleton/modules/custom/skeleton_restful/plugins/restful/user/pusher_auth/1.0/SkeletonPusherAuthResource.class.php new file mode 100644 index 0000000..d998d0f --- /dev/null +++ b/app/templates/skeleton/modules/custom/skeleton_restful/plugins/restful/user/pusher_auth/1.0/SkeletonPusherAuthResource.class.php @@ -0,0 +1,64 @@ + array( + \RestfulInterface::POST => 'viewEntity', + ), + ); + + /** + * Overrides \RestfulEntityBaseUser::publicFieldsInfo(). + */ + public function publicFieldsInfo() { + $public_fields = array(); + + $public_fields['auth'] = array( + 'callback' => array($this, 'getPusherAuth'), + ); + + return $public_fields; + } + + /** + * Overrides \RestfulEntityBase::viewEntity(). + * + * Always return the current user. + */ + public function viewEntity($entity_id) { + $request = $this->getRequest(); + + if (empty($request['channel_name'])) { + throw new \RestfulBadRequestException('"channel_name" property is missing'); + } + + if (empty($request['socket_id'])) { + throw new \RestfulBadRequestException('"socket_id" property is missing'); + } + + $account = $this->getAccount(); + return parent::viewEntity($account->uid); + } + + /** + * Get the pusher auth. + */ + protected function getPusherAuth() { + $request = $this->getRequest(); + + $pusher = skeleton_pusher_get_pusher(); + $result = $pusher->socket_auth($request['channel_name'], $request['socket_id']); + $data = drupal_json_decode($result); + + return $data['auth']; + } +} diff --git a/app/templates/skeleton/modules/custom/skeleton_restful/plugins/restful/user/pusher_auth/1.0/pusher_auth__1_0.inc b/app/templates/skeleton/modules/custom/skeleton_restful/plugins/restful/user/pusher_auth/1.0/pusher_auth__1_0.inc new file mode 100644 index 0000000..aeaaa63 --- /dev/null +++ b/app/templates/skeleton/modules/custom/skeleton_restful/plugins/restful/user/pusher_auth/1.0/pusher_auth__1_0.inc @@ -0,0 +1,13 @@ + t('Pusher auth'), + 'description' => t('Authenticate a user by the token for integration with Pusher.'), + 'resource' => 'pusher_auth', + 'class' => 'SkeletonPusherAuthResource', + 'entity_type' => 'user', + 'bundle' => 'user', + 'authentication_types' => TRUE, + // Add a passthrough formatter. + 'formatter' => 'simple', +); diff --git a/app/templates/skeleton/skeleton.info b/app/templates/skeleton/skeleton.info index c12d5f7..eb8a00c 100644 --- a/app/templates/skeleton/skeleton.info +++ b/app/templates/skeleton/skeleton.info @@ -19,6 +19,7 @@ dependencies[] = file ; Contrib modules dependencies[] = admin_menu_toolbar dependencies[] = admin_views +dependencies[] = composer_manager dependencies[] = ctools dependencies[] = date dependencies[] = entity @@ -49,4 +50,5 @@ dependencies[] = views_bulk_operations dependencies[] = skeleton_company dependencies[] = skeleton_event dependencies[] = skeleton_file +dependencies[] = skeleton_pusher dependencies[] = skeleton_restful diff --git a/app/templates/travis.config.sh b/app/templates/travis.config.sh index cc12874..6c2a8b7 100755 --- a/app/templates/travis.config.sh +++ b/app/templates/travis.config.sh @@ -68,10 +68,21 @@ MYSQL_DB_NAME="drupal" ## # Post install script. -# function post_install {} +function post_install { + chmod 777 www/sites/default/settings.php + + # Pusher integration. + echo "\$conf['skeleton_pusher_app_key'] = '';" >> www/sites/default/settings.php + echo "\$conf['skeleton_pusher_app_secret'] = '';" >> www/sites/default/settings.php + echo "\$conf['skeleton_pusher_app_id'] = '';" >> www/sites/default/settings.php +} # Post upgrade script. -# function post_upgrade {} +function post_upgrade { + post_install +} # Post reset script. -# function post_reset {} +function post_reset { + post_install +}