diff --git a/.travis.yml b/.travis.yml index 28001067..479561f8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ env: global: - AEGIR_HOSTING_VERSION=7.x-3.x - AEGIR_TESTS_VERSION=master - - AEGIR_PROVISION_VERSION=7.x-3.x + - AEGIR_PROVISION_VERSION=feature/quick-review #env: # - test: Ubuntu 14.04 Apache diff --git a/alias/hosting_alias.module b/alias/hosting_alias.module index ce3a1f1b..867df99d 100644 --- a/alias/hosting_alias.module +++ b/alias/hosting_alias.module @@ -72,7 +72,7 @@ function hosting_alias_form_site_node_form_alter(&$form, &$form_state) { function hosting_alias_site_form_validate($form, &$form_state) { $aliases = $form_state['values']['aliases'] = array_filter($form_state['values']['aliases']); foreach ($aliases as $key => $alias) { - hosting_alias_validate_alias($form_state['node'], $alias, $key); + hosting_alias_validate_alias($form_state['values'], $alias, $key); } } @@ -365,8 +365,7 @@ function hosting_alias_node_revision_delete($node) { function hosting_alias_validate_alias($site, $alias, $key) { if ($alias = strtolower(trim($alias))) { $alias = strtolower(trim($alias)); - $params = isset($site->nid) ? array('nid' => $site->nid) : array(); - if (!hosting_domain_allowed($alias, $params) || $alias == $site->title) { + if (!hosting_domain_allowed($alias, (array) $site) || $alias == $site->title) { form_set_error("aliases][$key", t('The domain name @alias is already in use', array('@alias' => $alias))); } if (!_hosting_valid_fqdn_wildcard($alias)) { diff --git a/client/hosting_client.module b/client/hosting_client.module index c8339eaf..762a075f 100644 --- a/client/hosting_client.module +++ b/client/hosting_client.module @@ -88,6 +88,10 @@ function hosting_client_permission() { 'edit client uname' => array( 'title' => t('edit client uname'), ), + 'bypass domain owner check' => array( + 'title' => t('bypass domain owner check'), + 'description' => t('Allows the client to register a subdomain, even when the above domain is owned by another client.'), + ), ); } @@ -634,7 +638,7 @@ function hosting_client_view($node, $view_mode, $langcode = NULL) { $header = array(t('Allowed users')); $node->content['users_view'] = array( '#type' => 'item', - '#value' => theme('table', array('header' => $header, 'rows' => $rows)), + '#markup' => theme('table', array('header' => $header, 'rows' => $rows)), '#class' => 'client', '#prefix' => '
', '#suffix' => '
', @@ -991,6 +995,12 @@ function hosting_client_configure($form, &$form_state) { '#description' => t('If this setting is on, any new client nodes will automatically have a system user account generated for them, and associated with the new client node. Users going through the signup form module have a user created regardless of this setting.'), '#default_value' => variable_get('hosting_client_register_user', FALSE), ); + $form['hosting_client_subdomain_owner_check'] = array( + '#type' => 'checkbox', + '#title' => t('Check owner upon creating subdomains.'), + '#description' => t("As a policy a hosting_client user is not allowed to create a site on a domain or subdomain thereof that is already in use by another client. (Unles the client has the 'bypass domain owner check' permission)"), + '#default_value' => variable_get('hosting_client_subdomain_owner_check', FALSE), + ); // User e-mail settings. $form['email'] = array( @@ -1175,3 +1185,81 @@ function hosting_client_views_api() { 'path' => drupal_get_path('module', 'hosting_client') . '/includes/views', ); } + +/** + * Implements hook_allow_domain(). + * + * Disallow domains already used as any site's title/url, unless the site has + * been deleted. + * + * @see hosting_domain_allowed() + */ +function hosting_client_allow_domain($url, $params = array()) { + // If we do not have to be here, leave ASAP. + if (!variable_get('hosting_client_subdomain_owner_check', FALSE) || user_access('bypass domain owner check')) { + return TRUE; + } + + // Get the client node from the client name. + $client = hosting_get_client($params['client']); + + // Prepare array. + $subdomains = array(); + + // Break up url. + $url_array = explode('.', $url); + while ($url_array) { + $subdomains[] = implode('.', $url_array); + array_shift($url_array); + } + + // Start query + $query = db_select('node', 'n') + ->fields('n', array('nid')) + ->condition('n.type', 'site'); + $query->leftJoin('hosting_site', 'h', 'h.nid = n.nid'); + $query->condition('h.status', HOSTING_SITE_DELETED, '<>'); + + // Check for either ... + $or = db_or(); + + $sites = db_or(); + $aliases = db_or(); + + // Iterate over all subdomains and populate sites and aliases checks. + foreach ($subdomains as $domain) { + // Regular site names + $sites->condition('n.title', $domain); + + // And aliases + $aliases->condition('a.alias', $domain); + } + + // Any of the above domains that do not belong to this client. + $or->condition( + db_and() + ->condition($sites) + ->condition('h.client', $client->nid, '<>') + ); + + if (module_exists('hosting_alias')) { + $query->leftJoin('hosting_site_alias', 'a', 'n.vid = a.vid'); + + // Any of the above domain aliases that do not belong to this client. + $or->condition( + db_and() + ->condition($aliases) + ->condition('h.client', $client->nid, '<>') + ); + } + + // Add conditions to the query. + $query->condition($or); + + // For existing sites, don't match the site's current url. + if (isset($params['nid'])) { + $query->condition('n.nid', $params['nid'], '<>'); + } + + return !$query->countQuery()->execute()->fetchField(); +} diff --git a/hosting.css b/hosting.css index fe185b83..b25c2311 100644 --- a/hosting.css +++ b/hosting.css @@ -79,6 +79,18 @@ td.hosting-actions .hosting-button-disabled { text-transform: none; } +.view-hosting-task-list .hosting-status time { + font-size:0.8em; + color: #555; + text-transform: uppercase; + padding: 0 1em 0 0; + white-space: nowrap; +} + +.view-hosting-task-list .hosting-status time em.placeholder { + font-style: normal; +} + /** * Status icons, colors. */ diff --git a/migrate/hosting_migrate.module b/migrate/hosting_migrate.module index 65cc62f8..9342b896 100644 --- a/migrate/hosting_migrate.module +++ b/migrate/hosting_migrate.module @@ -49,13 +49,13 @@ function hosting_migrate_hosting_tasks() { $tasks = array(); $tasks['site']['migrate'] = array( - 'title' => t('Migrate'), + 'title' => t('Migrate Site'), 'description' => t('Move the site to a new platform.'), 'dialog' => TRUE, ); $tasks['platform']['migrate'] = array( - 'title' => t('Migrate'), + 'title' => t('Migrate Sites'), 'description' => t('Migrate sites to a new platform.'), 'page arguments' => array('hosting_migrate_platform', 1), 'dialog' => TRUE, diff --git a/platform/hosting_platform.module b/platform/hosting_platform.module index ca949878..fa5484a6 100644 --- a/platform/hosting_platform.module +++ b/platform/hosting_platform.module @@ -74,13 +74,13 @@ function hosting_platform_node_info() { function hosting_platform_hosting_tasks() { $tasks = array(); $tasks['platform']['verify'] = array( - 'title' => t('Verify'), + 'title' => t('Verify Platform'), 'description' => t('Verify that the platform is correctly installed and working.'), 'weight' => 10, 'provision_save' => TRUE, ); $tasks['platform']['delete'] = array( - 'title' => t('Delete'), + 'title' => t('Delete Platform'), 'description' => t('Deleting this platform will completely remove it from the hosting system. This process can not be undone. It can not be performed if you have sites currently running on this platform. @@ -332,8 +332,8 @@ function hosting_platform_form(&$node) { '#maxlength' => 255, ); - // Allow edition if the node is in creation, or if wasn't verified correctly - // *and* we're not using a makefile. The reason while we don't allow editing + // Allow editing if the node is in creation, or if wasn't verified correctly + // *and* we're not using a makefile. The reason why we don't allow editing // the path if the makefile was specified is that there's the possibility // that the platform path was actually created when the node was saved the // first time and we have cruft lying around to cleanup. @@ -378,7 +378,7 @@ function hosting_platform_form(&$node) { // Send it on form submission. $form['publish_path'] = array( '#type' => 'hidden', - '#value' => $node->publish_path, + '#value' => hosting_path_normalize($node->publish_path), ); } @@ -490,7 +490,7 @@ function hosting_platform_insert($node) { ->fields(array( 'vid' => $node->vid, 'nid' => $node->nid, - 'publish_path' => $node->publish_path, + 'publish_path' => hosting_path_normalize($node->publish_path), 'makefile' => isset($node->makefile) ? $node->makefile : (isset($node->frommakefile['makefile']) ? $node->frommakefile['makefile'] : ''), 'verified' => isset($node->verified) ? $node->verified : 0, 'web_server' => $node->web_server, @@ -520,7 +520,7 @@ function hosting_platform_update($node) { } db_update('hosting_platform') ->fields(array( - 'publish_path' => $node->publish_path, + 'publish_path' => hosting_path_normalize($node->publish_path), 'makefile' => isset($node->makefile) ? $node->makefile : (isset($node->frommakefile['makefile']) ? $node->frommakefile['makefile'] : ''), 'web_server' => $node->web_server, 'verified' => $node->verified, diff --git a/queued/hosting_queued.admin.inc b/queued/hosting_queued.admin.inc index edb255fd..8592403e 100644 --- a/queued/hosting_queued.admin.inc +++ b/queued/hosting_queued.admin.inc @@ -53,6 +53,13 @@ function hosting_queued_settings_form($form, &$form_state) { 60 * 60, ), 'format_interval'), ); + + $form['hosting_queued_paused'] = array( + '#type' => 'checkbox', + '#title' => t('Pause the Hosting Queue'), + '#description' => t('Temporarily pause the hosting queue from running tasks.'), + '#default_value' => variable_get('hosting_queued_paused', 0), + ); return system_settings_form($form); // @ignore security_fapi_markup diff --git a/queued/hosting_queued.drush.inc b/queued/hosting_queued.drush.inc index 08246da8..0e895844 100644 --- a/queued/hosting_queued.drush.inc +++ b/queued/hosting_queued.drush.inc @@ -61,9 +61,47 @@ function drush_hosting_queued() { // frontend. variable_set('hosting_queued_process_started', REQUEST_TIME); - watchdog('hosting_queued', 'Started Hosting queue daemon, waiting for new tasks'); - drush_log('Started hosting queue daemon. Waiting for new tasks.', 'ok'); + // Check if hosting queue is paused, show appropriate message. + $is_paused = variable_get('hosting_queued_paused', 0); + if ($is_paused) { + watchdog('hosting_queued', 'Started Hosting queue daemon, hosting queue is paused.'); + drush_log('Started hosting queue daemon, hosting queue is paused.', 'ok'); + } + else { + watchdog('hosting_queued', 'Started Hosting queue daemon, waiting for new tasks'); + drush_log('Started hosting queue daemon. Waiting for new tasks.', 'ok'); + } + + global $conf; + while (TRUE) { + + // Detect if the hosting queue is paused or not. + // Reload variables. Since this is still a single request, any variable changes are not available. + $conf = variable_initialize(); + if (variable_get('hosting_queued_paused', 0)) { + + // If was not paused on last cycle, announce that it has been paused. + if (!$is_paused) { + drush_log('Hosting Queued has been paused.', 'ok'); + } + sleep(1); + + // Save current state for next cycle. + $is_paused = variable_get('hosting_queued_paused', 0); + + continue; + } + else { + // If was paused on last cycle, announce that it has been unpaused. + if ($is_paused) { + drush_log('Hosting Queued has been unpaused.', 'ok'); + } + + // Save current state for next cycle. + $is_paused = variable_get('hosting_queued_paused', 0); + } + try { // Should we terminate. if (time() > $end_time) { diff --git a/server/hosting_server.module b/server/hosting_server.module index 73147487..57fd7e0c 100644 --- a/server/hosting_server.module +++ b/server/hosting_server.module @@ -130,14 +130,14 @@ function hosting_server_menu() { function hosting_server_hosting_tasks() { $tasks = array(); $tasks['server']['verify'] = array( - 'title' => t('Verify'), + 'title' => t('Verify Server'), 'description' => t('Verify that the server is correctly installed and working.'), 'weight' => 10, 'provision_save' => TRUE, ); $tasks['server']['delete'] = array( - 'title' => t('Delete'), + 'title' => t('Delete Server'), 'description' => t('Delete the server.'), ); diff --git a/site/hosting_site.drush.inc b/site/hosting_site.drush.inc index 88dc8917..ca1398c8 100644 --- a/site/hosting_site.drush.inc +++ b/site/hosting_site.drush.inc @@ -24,6 +24,7 @@ function hosting_hosting_site_context_options(&$task) { $task->options['client_email'] = $user->mail; } $task->context_options['client_name'] = $client->uname; + $task->options['site_install_method'] = $task->ref->install_method; } /** diff --git a/site/hosting_site.form.inc b/site/hosting_site.form.inc index c2ef047e..e9bcb8af 100644 --- a/site/hosting_site.form.inc +++ b/site/hosting_site.form.inc @@ -403,8 +403,9 @@ function hosting_site_validate($node, &$form) { } // TODO: maybe we should allow creation of sites that conflict with HOSTING_SITE_DISABLED (which would then need to be renamed before being re-enabled) + // TODO: This error is also triggered when the user attempts to register a subdomain of a used site. Perhaps we should read the error code and split the errors? if (!hosting_domain_allowed($url, (array) $node)) { - form_set_error('title', t("The domain name you have specified is already in use.")); + form_set_error('title', t("The domain name you have specified is already in use, or does not belong to you.")); } // If the quota module is loaded and this is a new node, check diff --git a/site/hosting_site.module b/site/hosting_site.module index 38a91752..35c9a577 100644 --- a/site/hosting_site.module +++ b/site/hosting_site.module @@ -294,7 +294,7 @@ function hosting_site_hosting_tasks() { ); $tasks['site']['verify'] = array( - 'title' => t('Verify'), + 'title' => t('Verify Site'), 'description' => t('Confirm that the site has been correctly installed and regenerate all configuration files to match the hosting front end.'), 'provision_save' => TRUE, ); @@ -311,7 +311,7 @@ function hosting_site_hosting_tasks() { It may be disabled again if needed.'), ); $tasks['site']['delete'] = array( - 'title' => t('Delete'), + 'title' => t('Delete Site'), 'description' => t('Deleting this site will completely remove it from the hosting system, but will keep the last backup available. This process can not be undone. Are you really sure you want to delete this site?'), @@ -661,18 +661,20 @@ function hosting_site_status_codes($type = NULL) { * @see hosting_domain_allowed() */ function hosting_site_allow_domain($url, $params = array()) { - $query = "SELECT COUNT(n.nid) FROM {node} n - JOIN {hosting_site} h ON n.nid = h.nid - WHERE type = 'site' AND n.title = :title AND h.status <> :status"; - $args[':title'] = $url; - $args[':status'] = HOSTING_SITE_DELETED; + $query = db_select('node', 'n') + ->fields('n', array('nid')) + ->condition('n.type', 'site') + ->condition('n.title', $url); + + $query->leftJoin('hosting_site', 'h', 'h.nid = n.nid'); + $query->condition('h.status', HOSTING_SITE_DELETED, '<>'); + // For existing sites, don't match the site's current url. if (isset($params['nid'])) { - $query .= " AND n.nid <> :nid"; - $args[':nid'] = $params['nid']; + $query->condition('n.nid', $params['nid'], '<>'); } - $result = !db_query($query, $args)->fetchField(); - return $result; + + return !$query->countQuery()->execute()->fetchField(); } /** diff --git a/site/hosting_site.nodeapi.inc b/site/hosting_site.nodeapi.inc index f7dc81f3..1ee9d0a2 100644 --- a/site/hosting_site.nodeapi.inc +++ b/site/hosting_site.nodeapi.inc @@ -111,8 +111,6 @@ function hosting_site_view($node, $view_mode, $langcode = NULL) { 'changed' => $node->changed, ); drupal_add_js($settings, array('type' => 'setting', 'scope' => JS_DEFAULT)); - - drupal_add_js(drupal_get_path('module', 'hosting_task') . '/hosting_task.js'); } return $node; } @@ -249,6 +247,62 @@ function hosting_site_insert(&$node) { // Provide a dummy profile, e.g. for hosting-import. $node->profile = isset($node->profile) ? $node->profile : 0; + // If platform NID is not defined, but platform data is available, + // create the platform. + if (empty($node->platform) + && !empty($node->platform_node) + && !empty($node->platform_node->publish_path) + ) { + + // If a platform exists for the given path, use that. + $existing_platform_nid = db_select('hosting_platform', 'p') + ->condition('publish_path', $node->platform_node->publish_path) + ->fields('p', array('nid')) + ->execute() + ->fetchField(); + + // Use the existing platform NID. + if ($existing_platform_nid) { + $node->platform = $existing_platform_nid; + } + // Or prepare a new one. + else { + + // If platform_status hasn't been explicitly set, + // assume platform status matches site status: + // If developer creates a site node that's disabled, + // the platform should be too. + if (empty($node->platform_node->platform_status)) { + $node->platform_node->platform_status = $node->site_status; + } + + // If web_server hasn't been explicity set, use hostmaster's web server. + if (empty($node->platform_node->web_server)) { + $hostmaster = hosting_context_load('hostmaster'); + $hostmaster_platform = node_load($hostmaster->platform); + $node->platform_node->web_server = $hostmaster_platform->nid; + } + + // If platform title has not been set, generate one from the site title. + if (empty($node->platform_node->title)) { + $node->platform_node->title = 'platform_' . preg_replace("/[!\W]/", "", $node->title); + } + + // If platform UID has not been set, use site UID. + if (empty($node->platform_node->uid)) { + $node->platform_node->uid = $node->uid; + } + + $platform_nid = node_save($node->platform_node); + $node->platform = $node->platform_node->nid; + } + } + + // If there is no platform NID and no publish path, throw an exception. + if (empty($node->platform) && empty($node->platform_node->publish_path)) { + throw new Exception('Site nodes require either platform or platform_node->publish_path property'); + } + $id = db_insert('hosting_site') ->fields(array( 'vid' => $node->vid, diff --git a/task/hosting_task.info b/task/hosting_task.info index cd76d6a0..3405dade 100644 --- a/task/hosting_task.info +++ b/task/hosting_task.info @@ -2,9 +2,11 @@ name = Hosting tasks description = Allow Hostmaster to keep track of tasks that have been processed on the servers. package = Hosting dependencies[] = hosting +dependencies[] = timeago core = 7.x files[] = includes/views/handlers/hosting_task_handler_field_status.inc +files[] = includes/views/handlers/hosting_task_handler_field_type.inc files[] = includes/views/handlers/hosting_task_handler_filter_type.inc files[] = includes/views/handlers/hosting_task_handler_filter_status.inc files[] = includes/views/handlers/hosting_task_handler_sort_default.inc diff --git a/task/hosting_task.install b/task/hosting_task.install index fadd378c..0f986ed4 100644 --- a/task/hosting_task.install +++ b/task/hosting_task.install @@ -325,4 +325,12 @@ function hosting_task_update_6200() { */ function hosting_task_update_7000() { drupal_flush_all_caches(); +} + +/** + * Install timeago module and set the default format for the hosting_timeago date type. + */ +function hosting_task_update_7001() { + module_enable(array('timeago')); + variable_set('date_format_hosting_timeago', 'date_format_dynamic'); } \ No newline at end of file diff --git a/task/hosting_task.js b/task/hosting_task.js index ae8cfe88..74c4cb05 100644 --- a/task/hosting_task.js +++ b/task/hosting_task.js @@ -1,91 +1,182 @@ (function($) { -hostingTaskRefreshList = function() { - if (!Drupal.settings.hostingTaskRefresh.nid) { - return null; - } - - var hostingTaskListRefreshCallback = function(data, responseText) { - // If the node has been modified, reload the whole page. - if (Drupal.settings.hostingTaskRefresh.changed < data.changed) { - // only reload if there is no modal frame currently open - if ($(document).data('hostingOpenModalFrame') != true) { - // If a specific URL was specified, go there. - if (data.navigate_url) { - document.location = data.navigate_url; - } - // Fall back to just doing a reload of the current page. - else { - document.location.reload(); + Drupal.behaviors.hostingTasks = { + attach: function (context, settings) { + + // Attach to the global hosting tasks block. + if ($('#hostingTasks').length > 0) { + Drupal.settings.hostingTasks.vue = new Vue({ + el: '#hostingTasks', + data: { + tasks: Drupal.settings.hostingTasks.tasks, + }, + watch: { + + // Watch tasks for changes: update timeago if timestamp changes + tasks: function (tasks, oldTasks) { + for (var i = 0, len = tasks.length; i < len; i++) { + var task = tasks[i]; + if (task.timestamp != oldTasks[i].timestamp) { + + // Set a tiny timeout so timeago reset happens after DOM update. + setTimeout(function () { + $("time.timeago").timeago("updateFromDOM"); + }, 10); + return; + } + } + }, + } + }); + } + + // Attach to the available_tasks block, if there is one. + if ($('#hostingAvailableTasks').length > 0) { + Drupal.settings.hostingTasks.vueAvailable = new Vue({ + el: '#hostingAvailableTasks', + data: { + tasks: Drupal.settings.hostingAvailableTasks, + }, + }); + + } + + setTimeout("Drupal.behaviors.hostingTasks.checkTasks()", settings.hostingTasks.refreshTimeout); + }, + checkTasks: function () { + var url = Drupal.settings.hostingTasks.url; + $.getJSON(url, function (data) { + + // Replace vue data with new data. + Drupal.settings.hostingTasks.vue.tasks = data.tasks; + if (data.availableTasks && Drupal.settings.hostingTasks.vueAvailable) { + Drupal.settings.hostingTasks.vueAvailable.tasks = data.availableTasks; + } + + // Stop if needed. + if (Drupal.settings.hostingTasks.halt != true) { + setTimeout("Drupal.behaviors.hostingTasks.checkTasks()", Drupal.settings.hostingTasks.refreshTimeout); + } + }); + }, + }; + + Drupal.behaviors.hostingTimeAgo = { + attach: function (context, settings) { + $.timeago.settings.refreshMillis = 1000; + $.timeago.settings.strings = { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "ago", + suffixFromNow: "from now", + inPast: 'any moment now', + seconds: "%d sec", + minute: "1 min", + minutes: "%d min", + hour: "1 hr", + hours: "%d hrs", + day: "1 day", + days: "%d days", + month: "1 month", + months: "%d months", + year: "1 year", + years: "%d years", + wordSeparator: " ", + numbers: [] + } + $(".timeago", context).timeago(); } - } } - else { - $("#hosting-task-list").html(data.markup); - - hostingTaskBindButtons('#hosting-task-list'); - setTimeout("hostingTaskRefreshList()", Drupal.settings.hostingTaskRefresh.refreshTimeout); - } - } - - hostingTaskAddOverlay('#hosting-task-list'); - $.get(Drupal.settings.basePath + 'hosting/tasks/' + Drupal.settings.hostingTaskRefresh.nid + '/list', null, hostingTaskListRefreshCallback , 'json' ); -} - - -function hostingTaskAddOverlay(elem) { - $(elem).prepend('
'); -} - - -hostingTaskRefreshQueueBlock = function() { - if (Drupal.settings.hostingTaskRefresh.queueBlock != 1) { - return null; - } - - var hostingTaskQueueRefreshCallback = function(data, responseText) { - $("#block-views-hosting-task-list-block .content").html(data.markup); - - hostingTaskBindButtons('#block-views-hosting-task-list-block'); - setTimeout("hostingTaskRefreshQueueBlock()", Drupal.settings.hostingTaskRefresh.refreshTimeout); - } - - hostingTaskAddOverlay('#block-views-hosting-task-list-block .view-content'); - $.get(Drupal.settings.basePath + 'hosting/tasks/queue', null, hostingTaskQueueRefreshCallback , 'json'); -} - -$(document).ready(function() { - $(document).data('hostingOpenModalFrame', false); - setTimeout("hostingTaskRefreshList()", Drupal.settings.hostingTaskRefresh.refreshTimeout); - setTimeout("hostingTaskRefreshQueueBlock()", Drupal.settings.hostingTaskRefresh.refreshTimeout); - hostingTaskBindButtons($(this)); - $('#hosting-task-confirm-form-actions a').click(function() { - if (parent.Drupal.modalFrame.isOpen) { - setTimeout(function() { parent.Drupal.modalFrame.close({}, {}); }, 1); - return false; - } - }); - -}); - -hostingTaskBindButtons = function(elem) { - $('.hosting-button-dialog', elem).click(function() { - $(document).data('hostingOpenModalFrame', true) - var options = { - url : Drupal.settings.basePath + 'hosting/js' + $(this).attr('href'), - draggable : false, - width : 600, - height : 150, - onSubmit : function() { - $(document).data('hostingOpenModalFrame', false) - hostingTaskRefreshQueueBlock(); - hostingTaskRefreshList(); - } - } - Drupal.modalFrame.open(options); - return false; - }); -} - - -})(jQuery); +}(jQuery)); + +// (function($) { +// +// hostingTaskRefreshList = function() { +// if (!Drupal.settings.hostingTaskRefresh.nid) { +// return null; +// } +// +// var hostingTaskListRefreshCallback = function(data, responseText) { +// // If the node has been modified, reload the whole page. +// if (Drupal.settings.hostingTaskRefresh.changed < data.changed) { +// // only reload if there is no modal frame currently open +// if ($(document).data('hostingOpenModalFrame') != true) { +// // If a specific URL was specified, go there. +// if (data.navigate_url) { +// document.location = data.navigate_url; +// } +// // Fall back to just doing a reload of the current page. +// else { +// document.location.reload(); +// } +// } +// } +// else { +// $("#hosting-task-list").html(data.markup); +// +// hostingTaskBindButtons('#hosting-task-list'); +// setTimeout("hostingTaskRefreshList()", Drupal.settings.hostingTaskRefresh.refreshTimeout); +// } +// } +// +// hostingTaskAddOverlay('#hosting-task-list'); +// $.get(Drupal.settings.basePath + 'hosting/tasks/' + Drupal.settings.hostingTaskRefresh.nid + '/list', null, hostingTaskListRefreshCallback , 'json' ); +// } +// +// +// function hostingTaskAddOverlay(elem) { +// $(elem).prepend('
'); +// } +// +// +// hostingTaskRefreshQueueBlock = function() { +// if (Drupal.settings.hostingTaskRefresh.queueBlock != 1) { +// return null; +// } +// +// var hostingTaskQueueRefreshCallback = function(data, responseText) { +// // $("#block-views-hosting-task-list-block .content").html(data.markup); +// // +// // hostingTaskBindButtons('#block-views-hosting-task-list-block'); +// setTimeout("hostingTaskRefreshQueueBlock()", Drupal.settings.hostingTaskRefresh.refreshTimeout); +// } +// +// // hostingTaskAddOverlay('#block-views-hosting-task-list-block .view-content'); +// $.get(Drupal.settings.basePath + 'hosting/tasks/queue', null, hostingTaskQueueRefreshCallback , 'json'); +// } +// +// $(document).ready(function() { +// $(document).data('hostingOpenModalFrame', false); +// setTimeout("hostingTaskRefreshList()", Drupal.settings.hostingTaskRefresh.refreshTimeout); +// setTimeout("hostingTaskRefreshQueueBlock()", Drupal.settings.hostingTaskRefresh.refreshTimeout); +// hostingTaskBindButtons($(this)); +// $('#hosting-task-confirm-form-actions a').click(function() { +// if (parent.Drupal.modalFrame.isOpen) { +// setTimeout(function() { parent.Drupal.modalFrame.close({}, {}); }, 1); +// return false; +// } +// }); +// +// }); +// +// hostingTaskBindButtons = function(elem) { +// $('.hosting-button-dialog', elem).click(function() { +// $(document).data('hostingOpenModalFrame', true) +// var options = { +// url : Drupal.settings.basePath + 'hosting/js' + $(this).attr('href'), +// draggable : false, +// width : 600, +// height : 150, +// onSubmit : function() { +// $(document).data('hostingOpenModalFrame', false) +// hostingTaskRefreshQueueBlock(); +// hostingTaskRefreshList(); +// } +// } +// Drupal.modalFrame.open(options); +// return false; +// }); +// } +// +// +// })(jQuery); diff --git a/task/hosting_task.module b/task/hosting_task.module index 9a2fc560..434a2d33 100644 --- a/task/hosting_task.module +++ b/task/hosting_task.module @@ -10,10 +10,10 @@ * Adds refreshTimeout javascript variable. */ function hosting_task_init() { - $settings['hostingTaskRefresh'] = array( - 'refreshTimeout' => variable_get('hosting_task_refresh_timeout', 30000), - ); - drupal_add_js($settings, 'setting'); +// $settings['hostingTaskRefresh'] = array( +// 'refreshTimeout' => variable_get('hosting_task_refresh_timeout', 30000), +// ); +// drupal_add_js($settings, 'setting'); } @@ -48,13 +48,12 @@ function hosting_task_menu() { } } - $items['hosting/tasks/%node/list'] = array( + $items['hosting/json/tasks'] = array( 'title' => 'Task list', 'description' => 'AJAX callback for refreshing task list', 'page callback' => 'hosting_task_ajax_list', - 'page arguments' => array(2), - 'access callback' => 'node_access', - 'access arguments' => array('view', 2), + 'page arguments' => array(3), + 'access arguments' => array('access task logs'), 'type' => MENU_CALLBACK, ); @@ -68,14 +67,6 @@ function hosting_task_menu() { 'type' => MENU_CALLBACK, ); - $items['hosting/tasks/queue'] = array( - 'title' => 'Task list', - 'description' => 'AJAX callback for refreshing task queue', - 'page callback' => 'hosting_task_ajax_queue', - 'access arguments' => array('access task logs'), - 'type' => MENU_CALLBACK, - ); - // Custom path to task node views for overlay. // See hosting_task_overlay_paths(). $items['hosting/task/%node'] = array( @@ -192,23 +183,20 @@ function hosting_task_ajax_command_hosting_table_check($selector, $url, $setting /** * Page callback to provide JSON output for a task. */ -function hosting_task_ajax_list($node) { - $return['markup'] = hosting_task_table($node); - $return['changed'] = $node->changed; - $return['navigate_url'] = url('node/' . $node->nid); - drupal_json_output($return); - exit(); -} +function hosting_task_ajax_list($nid = NULL) { -/** - * AJAX callback for refreshing task list. - */ -function hosting_task_ajax_queue() { - $view = views_get_view('hosting_task_list'); - $view->set_display('block'); - $view->pre_execute(); - $return['markup'] = $view->render('block'); + // Load available tasks if a node is specified and accessible. + $node = node_load($nid); + if ($node && node_access('view', $node)) { + $return['availableTasks'] = array_values(hosting_task_fetch_tasks($node->nid)); + $return['changed'] = $node->changed; + $return['navigate_url'] = url('node/' . $node->nid); + } + + // Load global tasks for block. + $return['tasks'] = views_get_view_result('hosting_task_list', 'block'); + // Output JSON drupal_json_output($return); exit(); } @@ -584,7 +572,6 @@ function hosting_add_task($nid, $type, $args = NULL, $status = HOSTING_TASK_QUEU * Implements hook_form(). */ function hosting_task_confirm_form($form, $form_state, $node, $task) { - drupal_add_js(drupal_get_path('module', 'hosting_task') . '/hosting_task.js'); $tasks = hosting_available_tasks($node->type); if (!isset($tasks[$task]['dialog']) || !$tasks[$task]['dialog']) { @@ -848,7 +835,12 @@ function hosting_available_tasks($type = NULL, $reset = FALSE) { drupal_alter('hosting_tasks', $cache); } if (isset($type)) { - return $cache[$type]; + if (isset($cache[$type])) { + return $cache[$type]; + } + else { + return array(); + } } else { return $cache; @@ -1520,56 +1512,12 @@ function hosting_task_list($filter_by = NULL, $filter_value = NULL) { * simple interface. */ function hosting_task_table($node) { - $output = ''; - - $headers[] = t('Task'); - $headers[] = array( - 'data' => t('Actions'), - 'class' => array('hosting-actions'), - ); - - $tasklist = hosting_task_fetch_tasks($node->nid); - $rows = array(); - - foreach ($tasklist as $task => $info) { - $row = array(); - - if (!isset($info['nid']) && !$info['task_permitted']) { - // Just don't show those tasks, since we'll not be able to run them. - continue; - } - - if (empty($info['title'])) { - // Skip tasks from types that have since been removed. - continue; - } - - $row['type'] = array( - 'data' => $info['title'], - 'class' => array('hosting-status'), - ); - $actions = array(); - - if (isset($info['task_status']) && ($info['task_status'] == 0)) { - $actions['cancel'] = _hosting_task_button(t('Cancel'), sprintf("hosting/tasks/%d/cancel", $info['nid']), t("Cancel the task and remove it from the queue"), 'hosting-button-stop', !$info['task_permitted']); - } - else { - $actions['run'] = _hosting_task_button(t('Run'), sprintf("hosting_confirm/%d/%s_%s", $node->nid, $node->type, $task), $info['description'], 'hosting-button-run', $info['task_permitted'], $info['dialog']); - } - - $actions['log'] = _hosting_task_button(t('View'), isset($info['nid']) ? 'hosting/task/' . $info['nid'] : '', t("Display the task log"), 'hosting-button-log', isset($info['nid']) && user_access('access task logs'), TRUE, FALSE); - $row['actions'] = array( - 'data' => implode('', $actions), - 'class' => array('hosting-actions'), - ); - - $rows[] = array( - 'data' => $row, - 'class' => array($info['class']), - ); - } - $output .= theme('table', array('header' => $headers, 'rows' => $rows, 'attributes' => array('class' => array('hosting-table')))); - return $output; + $tasks = hosting_task_fetch_tasks($node->nid); + $settings['hostingTasks']['availableTasks'] = $tasks; + drupal_add_js($settings, 'setting'); + drupal_add_js(drupal_get_path('module', 'hosting_task') . '/hosting_task.js'); + drupal_add_js('https://npmcdn.com/vue/dist/vue.js', 'external'); + return theme('hosting_task_table', array('tasks' => $tasks)); } /** @@ -1670,10 +1618,20 @@ function _hosting_task_list($filter_by, $filter_value, $count = 5, $element = 0, } /** - * @todo Please document this function. - * @see http://drupal.org/node/1354 + * Retrieve the available tasks for a given Aegir Object. This function is used + * on the site/platform/server node page to provide the list of actions a user + * can take. + * + * @param $rid + * The node ID of the desired object. + * + * @return array + * A list of available tasks and metadata about the last task status. + * + * The keys of the array are the type of task, such as "verify" and "install". */ function hosting_task_fetch_tasks($rid) { + $return = array(); $node = node_load($rid); $result = db_query("SELECT n.nid, t.task_type, t.task_status FROM {node} n INNER JOIN {hosting_task} t ON n.vid = t.vid @@ -1691,6 +1649,8 @@ function hosting_task_fetch_tasks($rid) { $tasks = hosting_available_tasks($node->type); ksort($tasks); + global $user; + foreach ($tasks as $type => $hook_task) { if (!isset($return[$type])) { @@ -1702,6 +1662,8 @@ function hosting_task_fetch_tasks($rid) { $task = array(); $task = array_merge($return[$type], $hook_task); + $task['task_type'] = $type; + $allowed = (isset($task['exists']) && !in_array($task['task_status'], array(HOSTING_TASK_QUEUED, HOSTING_TASK_PROCESSING))) || !isset($task['exists']); if ($allowed && empty($task['hidden']) && $access_callback($node, $type)) { $task['task_permitted'] = TRUE; @@ -1718,12 +1680,61 @@ function hosting_task_fetch_tasks($rid) { } $task['class'] = hosting_task_status_class($task['task_status']); + // Generate Links + $task['view_link'] = FALSE; + $task['view_link_text'] = t('View'); + $task['run_link'] = FALSE; + $task['cancel_link'] = FALSE; + + $nid = isset($task['nid'])? $task['nid']: NULL; + $ref_type = $node->type; + $task_type = $task['task_type']; + + // View Logs link + if (!empty($nid) && drupal_valid_path("hosting/task/{$nid}")) { + $task['view_link'] = array( + 'url' => url("hosting/task/{$nid}"), + 'title' => t('Display the task log.'), + 'text' => t('View'), + ); + } + + // Cancel Task link + if (isset($task['task_status']) && ($task['task_status'] === HOSTING_TASK_QUEUED)) { +// $actions['cancel'] = _hosting_task_button(t('Cancel'), sprintf("hosting/tasks/%d/cancel", $info['nid']), t("Cancel the task and remove it from the queue"), 'hosting-button-stop', !$info['task_permitted']); + + $task['cancel_link'] = array( + 'url' => url("hosting/tasks/{$nid}/cancel"), + 'title' => t('Cancel the task and remove it from the queue.'), + 'text' => t('Cancel'), + ); + } + else { +// $actions['run'] = _hosting_task_button(t('Run'), sprintf("hosting_confirm/%d/%s_%s", $node->nid, $node->type, $task), $info['description'], 'hosting-button-run', $info['task_permitted'], $info['dialog']); + $task['run_link'] = array( + 'url' => url("hosting_confirm/{$rid}/{$ref_type}_{$task_type}", array( + 'query'=> array( + 'token' => drupal_get_token($user->uid), + ) + )), + 'title' => $task['description'], + 'text' => t('Run'), + ); + } + + + if (empty($nid) && $task['task_permitted'] == FALSE) { + $task['hidden'] = TRUE; + } + $return[$type] = $task; } return $return; } + + /** * Traslate a task status code into a css class. */ @@ -1752,6 +1763,28 @@ function hosting_task_status_class($status = NULL) { return $class; } +/** + * Get a task's human type name from it's machine name. + */ +function hosting_task_type_name($task_type, $node_type = NULL) { + + if ($node_type) { + $tasks = hosting_available_tasks($node_type); + } + else { + // Tasks are grouped by node type, and might vary by that even thought type value is the same. + $all_tasks = hosting_available_tasks(); + $tasks = array_merge($all_tasks['server'], $all_tasks['platform'], $all_tasks['site']); + } + + if (isset($tasks[$task_type]['title'])) { + return $tasks[$task_type]['title']; + } + else { + return $task_type; + } +} + /** * Views integration. */ @@ -1759,6 +1792,7 @@ function hosting_task_views_api() { return array( 'api' => 3, 'path' => drupal_get_path('module', 'hosting_task') . '/includes/views', + 'template path' => drupal_get_path('module', 'hosting_task') . '/templates', ); } @@ -1777,6 +1811,15 @@ function hosting_task_overlay_paths() { return $paths; } +/** + * Implements hook_date_format_types(). + */ +function hosting_task_date_format_types() { + return array( + 'hosting_timeago' => t('Dynamic Time Ago'), + ); +} + /** * Implements hook_preprocess_views_view_table(). */ @@ -1785,16 +1828,67 @@ function hosting_task_preprocess_views_view_table(&$vars) { switch ($id) { case 'hosting_task_list-block': - drupal_add_js(drupal_get_path('module', 'hosting_task') . '/hosting_task.js'); - $settings['hostingTaskRefresh'] = array( 'queueBlock' => 1, ); - drupal_add_js($settings, 'setting'); + break; } } +/** + * Implements hook_preprocess_HOOK(). + */ +function hosting_task_preprocess_page(&$variables) { + $settings['hostingTasks'] = array( + 'url' => url("hosting/json/tasks"), + 'refreshTimeout' => 2000, + 'tasks' => views_get_view_result('hosting_task_list', 'block'), + ); + + // If on a hosting node page... + if (isset($variables['node']) && !empty($variables['node']->nid)) { + $availableTasks = hosting_task_fetch_tasks($variables['node']->nid); + + // Get rid of array keys so it becomes an array. + $settings['hostingAvailableTasks'] = array_values($availableTasks); + $settings['hostingTasks']['url'] = url("hosting/json/tasks/" . $variables['node']->nid); + } + + drupal_add_js($settings, 'setting'); + drupal_add_js(drupal_get_path('module', 'hosting_task') . '/hosting_task.js'); + drupal_add_js('https://npmcdn.com/vue/dist/vue.js', 'external'); +} + +/** + * Implements hook_views_post_execute() + * + * Alter the results of the hosting_task_list view. + * This view is used to generate the JSON list of tasks. + */ +function hosting_task_views_post_execute(&$view) { + + // Parse Results on hosting_task_list view. + if ($view->name == 'hosting_task_list') { + foreach ($view->result as $i => &$result) { + $result->status_class = hosting_task_status_class($result->hosting_task_task_status); + $result->ref_url = url("node/$result->node_hosting_task_nid"); + $result->task_url = url("hosting/task/$result->nid"); + $result->task_link_text = t('View'); + $result->timestamp = date('c', $result->node_created); + $result->timestamp_human = format_date($result->node_created); + + // Load the correct task type name. + if (isset($result->node_hosting_task_type)) { + $result->task_type_name = hosting_task_type_name($result->hosting_task_task_type, $result->node_hosting_task_type); + } + else { + $result->task_type_name = hosting_task_type_name($result->hosting_task_task_type); + } + } + } +} + /** * Set a task's status according to its log. * @@ -1953,3 +2047,18 @@ function hosting_task_entity_property_info_alter(&$info) { ); */ } + +/** + * Implements hook_theme(). + */ +function hosting_task_theme($existing, $type, $theme, $path) { + $theme = array(); + $theme['hosting_task_table'] = array( + 'template' => 'hosting-task-table', + 'path' => drupal_get_path('module', 'hosting_task') . '/templates', + 'variables' => array( + array('tasks' => array()), + ), + ); + return $theme; +} \ No newline at end of file diff --git a/task/includes/views/handlers/hosting_task_handler_field_type.inc b/task/includes/views/handlers/hosting_task_handler_field_type.inc new file mode 100644 index 00000000..ac0e620f --- /dev/null +++ b/task/includes/views/handlers/hosting_task_handler_field_type.inc @@ -0,0 +1,48 @@ + 'text'); + return $options; + } + + function options_form(&$form, &$form_state) { + $form['type_format'] = array( + '#type' => 'radios', + '#title' => t('Display mode'), + '#options' => array( + 'raw' => t('Raw value'), + 'text' => t('Text value'), + ), + '#default_value' => isset($this->options['type_format'])? $this->options['type_format']: 'title', + '#description' => t('Output the machine-name or human name of the task type. To ensure correct name, make sure Node Type field (assigned to reference) is added to this view.'), + ); + parent::options_form($form, $form_state); + } + + function render($values) { + $value = $this->get_value($values); + switch ($this->options['type_format']) { + case 'text': + + // Return the right type of task. + if (isset($values->node_hosting_task_type)) { + return hosting_task_type_name($value, $values->node_hosting_task_type); + } + else { + return hosting_task_type_name($value); + } + default: + return $value; + } + } +} + diff --git a/task/includes/views/hosting_task.views.inc b/task/includes/views/hosting_task.views.inc index ac324e2d..99ffc6d8 100644 --- a/task/includes/views/hosting_task.views.inc +++ b/task/includes/views/hosting_task.views.inc @@ -27,7 +27,7 @@ function hosting_task_views_data() { 'title' => t('Type'), 'help' => t('The type of task.'), 'field' => array( - 'handler' => 'views_handler_field', + 'handler' => 'hosting_task_handler_field_type', 'click sortable' => TRUE, ), 'filter' => array( diff --git a/task/includes/views/hosting_task.views_default.inc b/task/includes/views/hosting_task.views_default.inc index 58a7d7c4..7c7c2915 100644 --- a/task/includes/views/hosting_task.views_default.inc +++ b/task/includes/views/hosting_task.views_default.inc @@ -101,13 +101,38 @@ function hosting_task_views_default_views() { $handler->display->display_options['fields']['title']['field'] = 'title'; $handler->display->display_options['fields']['title']['relationship'] = 'rid'; $handler->display->display_options['fields']['title']['exclude'] = TRUE; - /* Field: Hosting Task: Type */ + /* Field: Content: Type */ + $handler->display->display_options['fields']['type']['id'] = 'type'; + $handler->display->display_options['fields']['type']['table'] = 'node'; + $handler->display->display_options['fields']['type']['field'] = 'type'; + $handler->display->display_options['fields']['type']['relationship'] = 'rid'; + $handler->display->display_options['fields']['type']['exclude'] = TRUE; + /* Field: Hosting Task: Executed */ + $handler->display->display_options['fields']['executed']['id'] = 'executed'; + $handler->display->display_options['fields']['executed']['table'] = 'hosting_task'; + $handler->display->display_options['fields']['executed']['field'] = 'executed'; + $handler->display->display_options['fields']['executed']['exclude'] = TRUE; + $handler->display->display_options['fields']['executed']['date_format'] = 'hosting_timeago'; + $handler->display->display_options['fields']['executed']['second_date_format'] = 'hosting_timeago'; + /* Field: Content: Post date */ + $handler->display->display_options['fields']['created']['id'] = 'created'; + $handler->display->display_options['fields']['created']['table'] = 'node'; + $handler->display->display_options['fields']['created']['field'] = 'created'; + $handler->display->display_options['fields']['created']['label'] = ''; + $handler->display->display_options['fields']['created']['exclude'] = TRUE; + $handler->display->display_options['fields']['created']['alter']['text'] = '[created]'; + $handler->display->display_options['fields']['created']['element_label_colon'] = FALSE; + $handler->display->display_options['fields']['created']['element_default_classes'] = FALSE; + $handler->display->display_options['fields']['created']['date_format'] = 'dynamic'; + $handler->display->display_options['fields']['created']['second_date_format'] = 'custom'; + /* Field: Output */ $handler->display->display_options['fields']['task_type']['id'] = 'task_type'; $handler->display->display_options['fields']['task_type']['table'] = 'hosting_task'; $handler->display->display_options['fields']['task_type']['field'] = 'task_type'; + $handler->display->display_options['fields']['task_type']['ui_name'] = 'Output'; $handler->display->display_options['fields']['task_type']['label'] = 'Task'; $handler->display->display_options['fields']['task_type']['alter']['alter_text'] = TRUE; - $handler->display->display_options['fields']['task_type']['alter']['text'] = '[task_type]: [title]'; + $handler->display->display_options['fields']['task_type']['alter']['text'] = '[task_type]: [title] '; $handler->display->display_options['fields']['task_type']['element_class'] = 'hosting-status'; /* Field: Content: Nid */ $handler->display->display_options['fields']['nid']['id'] = 'nid'; @@ -120,20 +145,6 @@ function hosting_task_views_default_views() { $handler->display->display_options['fields']['nid']['alter']['path'] = 'hosting/task/[nid]'; $handler->display->display_options['fields']['nid']['alter']['link_class'] = 'hosting-button-enabled hosting-button-log hosting-button-dialog'; $handler->display->display_options['fields']['nid']['element_class'] = 'hosting-actions'; - /* Field: Hosting Task: Executed */ - $handler->display->display_options['fields']['executed']['id'] = 'executed'; - $handler->display->display_options['fields']['executed']['table'] = 'hosting_task'; - $handler->display->display_options['fields']['executed']['field'] = 'executed'; - $handler->display->display_options['fields']['executed']['exclude'] = TRUE; - $handler->display->display_options['fields']['executed']['date_format'] = 'long'; - /* Field: Content: Post date */ - $handler->display->display_options['fields']['created']['id'] = 'created'; - $handler->display->display_options['fields']['created']['table'] = 'node'; - $handler->display->display_options['fields']['created']['field'] = 'created'; - $handler->display->display_options['fields']['created']['label'] = ''; - $handler->display->display_options['fields']['created']['exclude'] = TRUE; - $handler->display->display_options['fields']['created']['element_label_colon'] = FALSE; - $handler->display->display_options['fields']['created']['date_format'] = 'long'; /* Sort criterion: Content: Updated date */ $handler->display->display_options['sorts']['changed']['id'] = 'changed'; $handler->display->display_options['sorts']['changed']['table'] = 'node'; @@ -154,12 +165,14 @@ function hosting_task_views_default_views() { $handler->display->display_options['filters']['type']['value'] = array( 'task' => 'task', ); + $handler->display->display_options['filters']['type']['group'] = 1; $handler->display->display_options['filters']['type']['expose']['operator'] = FALSE; /* Filter criterion: Content: Published */ $handler->display->display_options['filters']['status']['id'] = 'status'; $handler->display->display_options['filters']['status']['table'] = 'node'; $handler->display->display_options['filters']['status']['field'] = 'status'; $handler->display->display_options['filters']['status']['value'] = '1'; + $handler->display->display_options['filters']['status']['group'] = 1; $handler->display->display_options['filters']['status']['expose']['operator'] = FALSE; /* Display: Block */ @@ -271,12 +284,14 @@ function hosting_task_views_default_views() { $handler->display->display_options['fields']['created']['table'] = 'node'; $handler->display->display_options['fields']['created']['field'] = 'created'; $handler->display->display_options['fields']['created']['label'] = 'Created'; - $handler->display->display_options['fields']['created']['date_format'] = 'time ago'; + $handler->display->display_options['fields']['created']['date_format'] = 'hosting_timeago'; + $handler->display->display_options['fields']['created']['second_date_format'] = 'long'; /* Field: Hosting Task: Executed */ $handler->display->display_options['fields']['executed']['id'] = 'executed'; $handler->display->display_options['fields']['executed']['table'] = 'hosting_task'; $handler->display->display_options['fields']['executed']['field'] = 'executed'; - $handler->display->display_options['fields']['executed']['date_format'] = 'time ago'; + $handler->display->display_options['fields']['executed']['date_format'] = 'hosting_timeago'; + $handler->display->display_options['fields']['executed']['second_date_format'] = 'long'; /* Field: Hosting Task: Execution time */ $handler->display->display_options['fields']['delta']['id'] = 'delta'; $handler->display->display_options['fields']['delta']['table'] = 'hosting_task'; @@ -293,6 +308,62 @@ function hosting_task_views_default_views() { $handler->display->display_options['fields']['nid']['alter']['path'] = 'hosting/task/[nid]'; $handler->display->display_options['fields']['nid']['alter']['link_class'] = 'hosting-button-enabled hosting-button-log hosting-button-dialog'; $handler->display->display_options['fields']['nid']['element_class'] = 'hosting-actions'; + $handler->display->display_options['defaults']['filter_groups'] = FALSE; + $handler->display->display_options['defaults']['filters'] = FALSE; + /* Filter criterion: Content: Type */ + $handler->display->display_options['filters']['type']['id'] = 'type'; + $handler->display->display_options['filters']['type']['table'] = 'node'; + $handler->display->display_options['filters']['type']['field'] = 'type'; + $handler->display->display_options['filters']['type']['value'] = array( + 'task' => 'task', + ); + $handler->display->display_options['filters']['type']['expose']['operator'] = FALSE; + /* Filter criterion: Content: Published */ + $handler->display->display_options['filters']['status']['id'] = 'status'; + $handler->display->display_options['filters']['status']['table'] = 'node'; + $handler->display->display_options['filters']['status']['field'] = 'status'; + $handler->display->display_options['filters']['status']['value'] = '1'; + $handler->display->display_options['filters']['status']['expose']['operator'] = FALSE; + /* Filter criterion: Hosting Task: Status */ + $handler->display->display_options['filters']['task_status']['id'] = 'task_status'; + $handler->display->display_options['filters']['task_status']['table'] = 'hosting_task'; + $handler->display->display_options['filters']['task_status']['field'] = 'task_status'; + $handler->display->display_options['filters']['task_status']['exposed'] = TRUE; + $handler->display->display_options['filters']['task_status']['expose']['operator_id'] = 'task_status_op'; + $handler->display->display_options['filters']['task_status']['expose']['label'] = 'Status'; + $handler->display->display_options['filters']['task_status']['expose']['operator'] = 'task_status_op'; + $handler->display->display_options['filters']['task_status']['expose']['identifier'] = 'task_status'; + $handler->display->display_options['filters']['task_status']['expose']['remember_roles'] = array( + 2 => '2', + 1 => 0, + 3 => 0, + 8 => 0, + 4 => 0, + 6 => 0, + 7 => 0, + 5 => 0, + 9 => 0, + ); + /* Filter criterion: Hosting Task: Type */ + $handler->display->display_options['filters']['task_type']['id'] = 'task_type'; + $handler->display->display_options['filters']['task_type']['table'] = 'hosting_task'; + $handler->display->display_options['filters']['task_type']['field'] = 'task_type'; + $handler->display->display_options['filters']['task_type']['exposed'] = TRUE; + $handler->display->display_options['filters']['task_type']['expose']['operator_id'] = 'task_type_op'; + $handler->display->display_options['filters']['task_type']['expose']['label'] = 'Type'; + $handler->display->display_options['filters']['task_type']['expose']['operator'] = 'task_type_op'; + $handler->display->display_options['filters']['task_type']['expose']['identifier'] = 'task_type'; + $handler->display->display_options['filters']['task_type']['expose']['remember_roles'] = array( + 2 => '2', + 1 => 0, + 3 => 0, + 8 => 0, + 4 => 0, + 6 => 0, + 7 => 0, + 5 => 0, + 9 => 0, + ); $handler->display->display_options['path'] = 'hosting/tasks'; $handler->display->display_options['menu']['type'] = 'normal'; $handler->display->display_options['menu']['title'] = 'Tasks'; @@ -412,7 +483,8 @@ function hosting_task_views_default_views() { $handler->display->display_options['fields']['executed']['id'] = 'executed'; $handler->display->display_options['fields']['executed']['table'] = 'hosting_task'; $handler->display->display_options['fields']['executed']['field'] = 'executed'; - $handler->display->display_options['fields']['executed']['date_format'] = 'time ago'; + $handler->display->display_options['fields']['executed']['date_format'] = 'dynamic'; + $handler->display->display_options['fields']['executed']['second_date_format'] = 'long'; /* Field: Hosting Task: Execution time */ $handler->display->display_options['fields']['delta']['id'] = 'delta'; $handler->display->display_options['fields']['delta']['table'] = 'hosting_task'; diff --git a/task/js/jquery.timeago.js b/task/js/jquery.timeago.js new file mode 100644 index 00000000..c85ebcc5 --- /dev/null +++ b/task/js/jquery.timeago.js @@ -0,0 +1,229 @@ +/** + * Timeago is a jQuery plugin that makes it easy to support automatically + * updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago"). + * + * @name timeago + * @version 1.5.3 + * @requires jQuery v1.2.3+ + * @author Ryan McGeary + * @license MIT License - http://www.opensource.org/licenses/mit-license.php + * + * For usage and examples, visit: + * http://timeago.yarp.com/ + * + * Copyright (c) 2008-2015, Ryan McGeary (ryan -[at]- mcgeary [*dot*] org) + */ + +(function (factory) { + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define(['jquery'], factory); + } else if (typeof module === 'object' && typeof module.exports === 'object') { + factory(require('jquery')); + } else { + // Browser globals + factory(jQuery); + } +}(function ($) { + $.timeago = function(timestamp) { + if (timestamp instanceof Date) { + return inWords(timestamp); + } else if (typeof timestamp === "string") { + return inWords($.timeago.parse(timestamp)); + } else if (typeof timestamp === "number") { + return inWords(new Date(timestamp)); + } else { + return inWords($.timeago.datetime(timestamp)); + } + }; + var $t = $.timeago; + + $.extend($.timeago, { + settings: { + refreshMillis: 60000, + allowPast: true, + allowFuture: false, + localeTitle: false, + cutoff: 0, + autoDispose: true, + strings: { + prefixAgo: null, + prefixFromNow: null, + suffixAgo: "ago", + suffixFromNow: "from now", + inPast: 'any moment now', + seconds: "less than a minute", + minute: "about a minute", + minutes: "%d minutes", + hour: "about an hour", + hours: "about %d hours", + day: "a day", + days: "%d days", + month: "about a month", + months: "%d months", + year: "about a year", + years: "%d years", + wordSeparator: " ", + numbers: [] + } + }, + + inWords: function(distanceMillis) { + if (!this.settings.allowPast && ! this.settings.allowFuture) { + throw 'timeago allowPast and allowFuture settings can not both be set to false.'; + } + + var $l = this.settings.strings; + var prefix = $l.prefixAgo; + var suffix = $l.suffixAgo; + if (this.settings.allowFuture) { + if (distanceMillis < 0) { + prefix = $l.prefixFromNow; + suffix = $l.suffixFromNow; + } + } + + if (!this.settings.allowPast && distanceMillis >= 0) { + return this.settings.strings.inPast; + } + + var seconds = Math.abs(distanceMillis) / 1000; + var minutes = seconds / 60; + var hours = minutes / 60; + var days = hours / 24; + var years = days / 365; + + function substitute(stringOrFunction, number) { + var string = $.isFunction(stringOrFunction) ? stringOrFunction(number, distanceMillis) : stringOrFunction; + var value = ($l.numbers && $l.numbers[number]) || number; + return string.replace(/%d/i, value); + } + + var words = seconds < 45 && substitute($l.seconds, Math.round(seconds)) || + seconds < 90 && substitute($l.minute, 1) || + minutes < 45 && substitute($l.minutes, Math.round(minutes)) || + minutes < 90 && substitute($l.hour, 1) || + hours < 24 && substitute($l.hours, Math.round(hours)) || + hours < 42 && substitute($l.day, 1) || + days < 30 && substitute($l.days, Math.round(days)) || + days < 45 && substitute($l.month, 1) || + days < 365 && substitute($l.months, Math.round(days / 30)) || + years < 1.5 && substitute($l.year, 1) || + substitute($l.years, Math.round(years)); + + var separator = $l.wordSeparator || ""; + if ($l.wordSeparator === undefined) { separator = " "; } + return $.trim([prefix, words, suffix].join(separator)); + }, + + parse: function(iso8601) { + var s = $.trim(iso8601); + s = s.replace(/\.\d+/,""); // remove milliseconds + s = s.replace(/-/,"/").replace(/-/,"/"); + s = s.replace(/T/," ").replace(/Z/," UTC"); + s = s.replace(/([\+\-]\d\d)\:?(\d\d)/," $1$2"); // -04:00 -> -0400 + s = s.replace(/([\+\-]\d\d)$/," $100"); // +09 -> +0900 + return new Date(s); + }, + datetime: function(elem) { + var iso8601 = $t.isTime(elem) ? $(elem).attr("datetime") : $(elem).attr("title"); + return $t.parse(iso8601); + }, + isTime: function(elem) { + // jQuery's `is()` doesn't play well with HTML5 in IE + return $(elem).get(0).tagName.toLowerCase() === "time"; // $(elem).is("time"); + } + }); + + // functions that can be called via $(el).timeago('action') + // init is default when no action is given + // functions are called with context of a single element + var functions = { + init: function() { + var refresh_el = $.proxy(refresh, this); + refresh_el(); + var $s = $t.settings; + if ($s.refreshMillis > 0) { + this._timeagoInterval = setInterval(refresh_el, $s.refreshMillis); + } + }, + update: function(timestamp) { + var date = (timestamp instanceof Date) ? timestamp : $t.parse(timestamp); + $(this).data('timeago', { datetime: date }); + if ($t.settings.localeTitle) $(this).attr("title", date.toLocaleString()); + refresh.apply(this); + }, + updateFromDOM: function() { + $(this).data('timeago', { datetime: $t.parse( $t.isTime(this) ? $(this).attr("datetime") : $(this).attr("title") ) }); + refresh.apply(this); + }, + dispose: function () { + if (this._timeagoInterval) { + window.clearInterval(this._timeagoInterval); + this._timeagoInterval = null; + } + } + }; + + $.fn.timeago = function(action, options) { + var fn = action ? functions[action] : functions.init; + if (!fn) { + throw new Error("Unknown function name '"+ action +"' for timeago"); + } + // each over objects here and call the requested function + this.each(function() { + fn.call(this, options); + }); + return this; + }; + + function refresh() { + var $s = $t.settings; + + //check if it's still visible + if ($s.autoDispose && !$.contains(document.documentElement,this)) { + //stop if it has been removed + $(this).timeago("dispose"); + return this; + } + + var data = prepareData(this); + + if (!isNaN(data.datetime)) { + if ( $s.cutoff == 0 || Math.abs(distance(data.datetime)) < $s.cutoff) { + $(this).text(inWords(data.datetime)); + } else { + if ($(this).attr('title').length > 0) { + $(this).text($(this).attr('title')); + } + } + } + return this; + } + + function prepareData(element) { + element = $(element); + if (!element.data("timeago")) { + element.data("timeago", { datetime: $t.datetime(element) }); + var text = $.trim(element.text()); + if ($t.settings.localeTitle) { + element.attr("title", element.data('timeago').datetime.toLocaleString()); + } else if (text.length > 0 && !($t.isTime(element) && element.attr("title"))) { + element.attr("title", text); + } + } + return element.data("timeago"); + } + + function inWords(date) { + return $t.inWords(distance(date)); + } + + function distance(date) { + return (new Date().getTime() - date.getTime()); + } + + // fix for IE6 suckage + document.createElement("abbr"); + document.createElement("time"); +})); diff --git a/task/templates/hosting-task-table.tpl.php b/task/templates/hosting-task-table.tpl.php new file mode 100644 index 00000000..220e60ba --- /dev/null +++ b/task/templates/hosting-task-table.tpl.php @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + +
+ + + +
+ title ?> + + + view_link): + $task = (object) $task; + ?> + + + view_link_text; ?> + + + + view_link_text; ?> + + + + run_link): + $task = (object) $task; + ?> + + + run_link['text']; ?> + + + + run_link['text']; ?> + + +
\ No newline at end of file diff --git a/task/templates/views-view-table--hosting-task-list--block.tpl.php b/task/templates/views-view-table--hosting-task-list--block.tpl.php new file mode 100644 index 00000000..805435ad --- /dev/null +++ b/task/templates/views-view-table--hosting-task-list--block.tpl.php @@ -0,0 +1,62 @@ + + + id="hostingTasks"> + + + + + + + $label): ?> + + + + + + + + > + $content): ?> + + + + + + +
scope="col"> + +
> + +