Commit 6f362a65 authored by Tuan Ngo's avatar Tuan Ngo

add SEO modules

parent 8af7d265
No preview for this file type
---
language: php
os: linux
version: ~> 1.0
php: "7.2"
addons:
chrome: stable
cache:
directories:
- "$HOME/.composer/cache"
- "$HOME/.drush/cache"
- "$HOME/.rvm"
- "${TMPDIR:-/tmp}/phpstan/cache"
branches:
only:
- 8.x-1.x
- 8.x-2.x
- wip
env:
global:
- ORCA_SUT_NAME=drupal/checklistapi
- ORCA_SUT_BRANCH=8.x-1.x
- ORCA_VERSION=dev-master
- ORCA_PACKAGES_CONFIG=../checklistapi/tests/packages.yml
jobs:
fast_finish: true
include:
- { name: "Static code analysis", env: ORCA_JOB=STATIC_CODE_ANALYSIS }
- { name: "Deprecated code scan", env: ORCA_JOB=DEPRECATED_CODE_SCAN }
- { name: "Isolated test w/ recommended package versions", env: ORCA_JOB=ISOLATED_RECOMMENDED }
# - { name: "Integrated test w/ recommended package versions", env: ORCA_JOB=INTEGRATED_RECOMMENDED }
- { name: "Isolated test w/ recommended package versions & previous minor release of Drupal core", env: ORCA_JOB=CORE_PREVIOUS }
- { name: "Isolated test w/ dev package versions", env: ORCA_JOB=ISOLATED_DEV }
# - { name: "Integrated test w/ dev package versions", env: ORCA_JOB=INTEGRATED_DEV }
- { name: "Isolated test w/ dev package versions & next minor dev version of Drupal core", env: ORCA_JOB=CORE_NEXT }
allow_failures:
- env: ORCA_JOB=ISOLATED_DEV
# - env: ORCA_JOB=INTEGRATED_DEV
- env: ORCA_JOB=CORE_NEXT
before_install:
- composer create-project --no-dev acquia/orca ../orca "$ORCA_VERSION"
- ../orca/bin/travis/before_install.sh
install: ../orca/bin/travis/install.sh
before_script: ../orca/bin/travis/before_script.sh
script: ../orca/bin/travis/script.sh
before_cache: ../orca/bin/travis/before_cache.sh
after_success: ../orca/bin/travis/after_success.sh
after_failure: ../orca/bin/travis/after_failure.sh
after_script: ../orca/bin/travis/after_script.sh
This diff is collapsed.
# Checklist API
[![Build Status](https://travis-ci.org/TravisCarden/checklistapi.svg?branch=8.x-1.x)](https://travis-ci.org/TravisCarden/checklistapi)
## Contents of This File
- [Introduction](#introduction)
- [Installation](#installation)
- [Implementation](#implementation)
- [Drush](#drush)
## Introduction
Current Maintainer: [TravisCarden](https://www.drupal.org/u/traviscarden)
Checklist API provides a simple interface for modules to create fillable,
persistent checklists that track progress with completion times and users. See
checklistapi_example.module for an example implementation.
## Installation
Checklist API is installed in the usual way. See [Installing contributed
modules](https://www.drupal.org/documentation/install/modules-themes/modules-8).
## Implementation
Checklists are declared as multidimensional arrays using
`hook_checklistapi_checklist_info()`. They can be altered using
`hook_checklistapi_checklist_info_alter()`. Checklist API handles creation of
menu items and permissions. Progress details are saved in one config file per
checklist.
See checklistapi.api.php for more details.
## Drush
Checklist API has Drush integration. To see the list of available commands, run
`drush --filter=checklistapi`. For more about Drush, including installation
instructions, see the [Drush docs](http://drush.org/).
<?php
/**
* @file
* Hooks provided by the Checklist API module.
*/
/**
* @addtogroup hooks
* @{
*/
/**
* Define all checklists provided by the module.
*
* Any number of checklists can be defined in an implementation of this hook.
* Checklist API will register menu items and create permissions for each one.
*
* For a working example, see checklistapiexample.module.
*
* @return array
* An array of checklist definitions. Each definition is keyed by an arbitrary
* unique identifier. The corresponding multidimensional array describing the
* checklist may contain the following key-value pairs:
* - #title: The title of the checklist.
* - #path: The Drupal path where the checklist will be accessed.
* - #callback: A callback to provide the checklist items. See
* callback_checklistapi_checklist_items(). (This value is technically
* optional in order to provide backward compatibility for modules using the
* deprecated method of defining checklist items right in this array.)
* - #callback_arguments: (optional) An array of values to pass as arguments
* to the callback.
* - #description: (optional) A brief description of the checklist for its
* corresponding menu item.
* - #help: (optional) User help to be displayed in the "System help" block
* via hook_help().
* - #menu_name: (optional) The machine name of a menu to place the checklist
* into (e.g., "main-menu" or "navigation"). If this is omitted, Drupal will
* try to infer the correct menu placement from the specified path.
* - #storage: (optional) The storage backend for saving checklist progress.
* Allowed values are "config" and "state" for the Configuration and State
* systems respectively. Defaults to "config".
* - #weight: (optional) A floating point number used to sort the list of
* checklists before being output. Lower numbers appear before higher
* numbers.
* - (deprecated) Any number of arrays representing groups of items, to be
* presented as vertical tabs. Use #callback instead.
*
* @see callback_checklistapi_checklist_items()
* @see hook_checklistapi_checklist_info_alter()
* @see checklistapiexample_checklistapi_checklist_info()
*/
function hook_checklistapi_checklist_info() {
$definitions = [];
$definitions['example_checklist'] = [
'#title' => t('Example checklist'),
'#path' => 'example-checklist',
'#callback' => 'callback_checklistapi_checklist_items',
'#callback_arguments' => ['Example value'],
'#description' => t('An example checklist.'),
'#help' => t('<p>This is an example checklist.</p>'),
];
return $definitions;
}
/**
* Define the checklist items for a given checklist.
*
* Declared in hook_checklistapi_checklist_info().
*
* @param mixed $argument
* Any number of arguments may be passed from a checklist definition via its
* #callback_arguments array.
*
* @return array
* An array of arrays representing groups of items, to be presented as
* vertical tabs. Each group is keyed by an arbitrary identifier, unique in
* the scope of the checklist. The corresponding multimensional array
* describing the group may contain the following key-value pairs:
* - #title: The title of the group, used as the vertical tab label.
* - #description: (optional) A description of the group.
* - #weight: (optional) A floating point number used to sort the list of
* groups before being output. Lower numbers appear before higher numbers.
* - Any number of arrays representing checklist items. Each item is keyed by
* an arbitrary identifier, unique in the scope of the checklist. The
* corresponding multimensional array describing the item may contain the
* following key-value pairs:
* - #title: The title of the item.
* - #description: (optional) A description of the item, for display beneath
* the title.
* - #default_value: (optional) The default checked state of the item--TRUE
* for checked or FALSE for unchecked. Defaults to FALSE.
* This is useful for automatically checking items that can be
* programmatically tested (e.g., a module is installed or a configuration
* setting has a certain value).
* - #weight: (optional) A floating point number used to sort the list of
* items before being output. Lower numbers appear before higher
* numbers.
* - Any number of arrays representing links. Each link is keyed by an
* arbitrary unique identifier. The corresponding multimensional array
* describing the link may contain the following key-value pairs:
* - #text: The link text.
* - #url: The link url as a \Drupal\Core\Url object.
* - #weight: (optional) A floating point number used to sort the list of
* items before being output. Lower numbers appear before higher
* numbers.
*
* @see hook_checklistapi_checklist_info()
* @see checklistapiexample_checklistapi_checklist_items()
*/
function callback_checklistapi_checklist_items($argument) {
return [
'example_group' => [
'#title' => t('Example group'),
'#description' => t('<p>Here are some example items.</p>'),
'example_item_1' => [
'#title' => t('Example item 1'),
'example_link' => [
'#text' => t('Example.com'),
'#url' => \Drupal\Core\Url::fromUri('http://www.example.com/'),
],
],
'example_item_2' => [
'#title' => t('Example item 2'),
],
],
];
}
/**
* Alter checklist definitions.
*
* This hook is invoked by checklistapi_get_checklist_info(). The checklist
* definitions are passed in by reference. Additional checklists may be added,
* or existing checklists may be altered or removed.
*
* Note: Checklist paths (#path) cannot be altered. See
* https://www.drupal.org/docs/8/api/routing-system/altering-existing-routes-and-adding-new-routes-based-on-dynamic-ones
* instead.
*
* @param array $definitions
* The multidimensional array of checklist definitions returned by
* hook_checklistapi_checklist_info().
*
* @see hook_checklistapi_checklist_info()
* @see checklistapiexample_checklistapi_checklist_info_alter()
*/
function hook_checklistapi_checklist_info_alter(array &$definitions) {
// Add an item.
$definitions['example_checklist']['example_group']['new_item'] = [
'title' => t('New item'),
];
// Add a group.
$definitions['example_checklist']['new_group'] = [
'#title' => t('New group'),
];
// Move an item.
$definitions['example_checklist']['new_group']['example_item_1'] = $definitions['example_checklist']['example_group']['example_item_1'];
unset($definitions['example_checklist']['example_group']['example_item_1']);
// Remove an item.
unset($definitions['example_checklist']['example_group']['example_item_2']);
}
/**
* @} End of "addtogroup hooks".
*/
#checklistapi-checklist-form div.description p {
margin: .5em 0;
}
#checklistapi-checklist-form span.completion-details {
font-style: italic;
opacity: 0.66;
}
#checklistapi-checklist-form.compact-mode div.description p {
display: none;
}
/**
* Progress bar.
*
* @see system.theme.css
*/
#checklistapi-checklist-form .progress {
font-weight: normal;
margin-bottom: 0.5em;
}
#checklistapi-checklist-form .progress__bar {
animation-name: none;
background-image: none;
}
#checklistapi-checklist-form .compact-link {
clear: both;
padding-top: 1em;
}
name: Checklist API
type: module
description: Provides an API for creating fillable, persistent checklists.
package: Other
core: 8.x
core_version_requirement: '^8 || ^9'
configure: checklistapi.report
# Information added by Drupal.org packaging script on 2020-04-10
version: '8.x-1.11'
project: 'checklistapi'
datestamp: 1586536893
<?php
/**
* @file
* Install, update, and uninstall functions for the Checklist API module.
*/
use Drupal\checklistapi\ChecklistapiChecklist;
use Drupal\Core\Render\Element;
/**
* Update saved progress configuration to new schema.
*/
function checklistapi_update_8001() {
$definitions = checklistapi_get_checklist_info();
foreach ($definitions as $id => $definition) {
$checklist = checklistapi_checklist_load($id);
/**
* Avoid trying to operate on null due to failure to load a checklist.
* @see https://www.drupal.org/project/checklistapi/issues/3122093
*/
if (!$checklist->config) {
continue;
}
$config = $checklist->config->get(ChecklistapiChecklist::PROGRESS_CONFIG_KEY);
$config['#items'] = [];
$groups = $checklist->items;
foreach (Element::children($groups) as $group_key) {
$group = $groups[$group_key];
// Loop through items.
foreach (Element::children($group) as $item_key) {
if ($config[$item_key]) {
$config['#items'][$item_key] = $config[$item_key];
}
unset($config[$item_key]);
}
}
$checklist->config->set(ChecklistapiChecklist::PROGRESS_CONFIG_KEY, $config)
->save();
}
}
(function ($) {
'use strict';
/**
* Updates the progress bar as checkboxes are changed.
*/
Drupal.behaviors.checklistapiUpdateProgressBar = {
attach: function (context) {
const total_items = $(':checkbox.checklistapi-item', context).length;
let progress_bar = $('#checklistapi-checklist-form .progress__bar', context);
let progress_percentage = $('#checklistapi-checklist-form .progress__percentage', context);
$(':checkbox.checklistapi-item', context).change(function () {
const num_items_checked = $(':checkbox.checklistapi-item:checked', context).length;
const percent_complete = Math.round(num_items_checked / total_items * 100);
let args = {};
progress_bar.css('width', percent_complete + '%');
args['@complete'] = num_items_checked;
args['@total'] = total_items;
args['@percent'] = percent_complete;
progress_percentage.html(Drupal.t('@complete of @total (@percent%)', args));
});
}
};
/**
* Provides the summary information for the checklist form vertical tabs.
*/
Drupal.behaviors.checklistapiFieldsetSummaries = {
attach: function (context) {
$('#checklistapi-checklist-form .vertical-tabs__panes > details', context).drupalSetSummary(function (context) {
const total = $(':checkbox.checklistapi-item', context).length;
let args = {};
if (total) {
args['@complete'] = $(':checkbox.checklistapi-item:checked', context).length;
args['@total'] = total;
args['@percent'] = Math.round(args['@complete'] / args['@total'] * 100);
return Drupal.t('@complete of @total (@percent%)', args);
}
});
}
};
/**
* Adds dynamic item descriptions toggling.
*/
Drupal.behaviors.checklistapiCompactModeLink = {
attach: function (context) {
let is_compact_mode = $('#checklistapi-checklist-form', context).hasClass('compact-mode');
const text = is_compact_mode ? Drupal.t('Show item descriptions') : Drupal.t('Hide item descriptions');
$('#checklistapi-checklist-form .compact-link', context).html('<a href="#">' + text + '</a>');
$('#checklistapi-checklist-form .compact-link a', context).click(function () {
$(this).closest('#checklistapi-checklist-form').toggleClass('compact-mode');
let is_compact_mode = $(this).closest('#checklistapi-checklist-form').hasClass('compact-mode');
$(this)
.text(is_compact_mode ? Drupal.t('Show item descriptions') : Drupal.t('Hide item descriptions'))
.attr('title', is_compact_mode ? Drupal.t('Expand layout to include item descriptions.') : Drupal.t('Compress layout by hiding item descriptions.'));
document.cookie = 'Drupal.visitor.checklistapi_compact_mode=' + (is_compact_mode ? 1 : 0);
return false;
});
}
};
/**
* Prompts the user if they try to leave the page with unsaved changes.
*
* Note: Auto-checked items are not considered unsaved changes for the purpose
* of this feature.
*/
Drupal.behaviors.checklistapiPromptBeforeLeaving = {
getFormState: function () {
return $('#checklistapi-checklist-form :checkbox.checklistapi-item').serializeArray().toString();
},
attach: function () {
const beginningState = this.getFormState();
$(window).bind('beforeunload', function () {
var endingState = Drupal.behaviors.checklistapiPromptBeforeLeaving.getFormState();
if (beginningState !== endingState) {
return Drupal.t('Your changes will be lost if you leave the page without saving.');
}
});
$('#checklistapi-checklist-form').submit(function () {
$(window).unbind('beforeunload');
});
$('#checklistapi-checklist-form .clear-saved-progress').click(function () {
$(window).unbind('beforeunload');
});
}
};
})(jQuery);
checklistapi:
version: 1.x
css:
theme:
checklistapi.css: {}
js:
checklistapi.js: {}
checklistapi.report:
title: Checklists
description: Get an overview of your installed checklists with progress details.
route_name: checklistapi.report
parent: system.admin_reports
<?php
/**
* @file
* An API for creating fillable, persistent checklists.
*
* Provides an interface for creating checklists that track progress with
* completion times and users.
*/
use Drupal\checklistapi\ChecklistapiChecklist;
use Drupal\Component\Utility\Unicode;
use Drupal\Core\Render\Element;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* Access callback: Checks the current user's access to a given checklist.
*
* @param string $id
* The checklist ID.
* @param string $operation
* (optional) The operation to test access for. Accepted values are "view",
* "edit", and "any". Defaults to "any".
*
* @return bool
* Returns TRUE if the current user has access to perform a given operation on
* the specified checklist, or FALSE if not.
*
* @throws InvalidArgumentException
* Throws an exception if an unsupported operation is supplied.
*/
function checklistapi_checklist_access($id, $operation = 'any') {
$all_operations = ['view', 'edit', 'any'];
if (!in_array($operation, $all_operations)) {
throw new \InvalidArgumentException(sprintf('No such operation "%s"', $operation));
}
$current_user = \Drupal::currentUser();
$access['view'] = $current_user->hasPermission('view any checklistapi checklist') || $current_user->hasPermission("view {$id} checklistapi checklist");
$access['edit'] = $current_user->hasPermission('edit any checklistapi checklist') || $current_user->hasPermission("edit {$id} checklistapi checklist");
$access['any'] = $access['view'] || $access['edit'];
return $access[$operation];
}
/**
* Loads a checklist object.
*
* @param string $id
* The checklist ID.
*
* @return Drupal\checklistapi\ChecklistapiChecklist|false
* A fully-loaded checklist object, or FALSE if the checklist is not found.
*/
function checklistapi_checklist_load($id) {
$definition = checklistapi_get_checklist_info($id);
return ($definition) ? new ChecklistapiChecklist($definition) : FALSE;
}
/**
* Adds the checklist items to a given definition.
*
* @param array $definition
* A checklist definition as returned from checklistapi_get_checklist_info().
*
* @return array
* The checklist definition with checklist items added.
*/
function checklistapi_add_checklist_items(array $definition) {
if (!empty($definition['#callback']) && is_callable($definition['#callback'])) {
// Remove any checklist items from the original definition.
foreach (Element::children($definition) as $child) {
unset($definition[$child]);
}
// Invoke the callback function.
$definition += call_user_func_array($definition['#callback'], $definition['#callback_arguments'] ?? []);
}
return $definition;
}
/**
* Determines whether the current user is in compact mode.
*
* Compact mode shows checklist forms with less description text.
*
* Whether the user is in compact mode is determined by a cookie. If the user
* does not have the cookie, the setting defaults to off.
*
* @return bool
* TRUE when in compact mode, or FALSE when in expanded mode.
*/
function checklistapi_compact_mode_is_on() {
// PHP converts dots into underscores in cookie names.
return (bool) \Drupal::request()->cookies->get('Drupal_visitor_checklistapi_compact_mode', FALSE);
}
/**
* Gets checklist definitions.
*
* @param string $id
* (optional) A checklist ID. Defaults to NULL.
*
* @return array|false
* The definition of the specified checklist, or FALSE if no such checklist
* exists, or an array of all checklist definitions if none is specified.
*/
function checklistapi_get_checklist_info($id = NULL) {
$definitions = &drupal_static(__FUNCTION__);
if (!is_array($definitions)) {
// Get definitions.
$definitions = \Drupal::moduleHandler()->invokeAll('checklistapi_checklist_info');
foreach ($definitions as $key => $value) {
$definitions[$key] = checklistapi_add_checklist_items($value);
}
$definitions = checklistapi_sort_array($definitions);
// Let other modules alter them.
\Drupal::moduleHandler()->alter('checklistapi_checklist_info', $definitions);
$definitions = checklistapi_sort_array($definitions);
// Inject checklist IDs.
foreach ($definitions as $key => $value) {
$definitions[$key] = ['#id' => $key] + $definitions[$key];
}
}
if (!empty($id)) {
return (!empty($definitions[$id])) ? $definitions[$id] : FALSE;
}
return $definitions;
}
/**
* Implements hook_help().
*/
function checklistapi_help($route_name, RouteMatchInterface $route_match) {
foreach (checklistapi_get_checklist_info() as $id => $definition) {
$checklist = new ChecklistapiChecklist($definition);
if ($checklist->getRouteName() == $route_name) {
// The checklist has help and the current user has access to view it.
if (!empty($definition['#help']) && checklistapi_checklist_access($id)) {
return $definition['#help'];
}
// Otherwise the loop can break early since only one checklist can occupy
// a given route.
else {
break;
}
}
}
}
/**
* Implements hook_module_preinstall().
*/
function checklistapi_module_preinstall($module) {
drupal_static_reset('checklistapi_get_checklist_info');
}
/**
* Recursively sorts array elements by #weight.
*
* @param array $array
* A nested array of elements and properties, such as the checklist
* definitions returned by hook_checklistapi_checklist_info().
*
* @return array
* The input array sorted recursively by #weight.
*
* @see checklistapi_get_checklist_info()
*/
function checklistapi_sort_array(array $array) {
$child_keys = Element::children($array);
if (!count($child_keys)) {
// No children to sort.
return $array;
}
$incrementer = 0;
$children = [];
foreach ($child_keys as $key) {
// Move child to a temporary array for sorting.
$children[$key] = $array[$key];
unset($array[$key]);
// Supply a default weight if missing or invalid.
if (empty($children[$key]['#weight']) || !is_numeric($children[$key]['#weight'])) {
$children[$key]['#weight'] = 0;
}
// Increase each weight incrementally to preserve the original order when
// not overridden. This accounts for undefined behavior in PHP's uasort()
// function when its comparison callback finds two values equal.
$children[$key]['#weight'] += ($incrementer++ / 1000);
// Descend into child.
$children[$key] = checklistapi_sort_array($children[$key]);
}
// Sort by weight.
uasort($children, ['Drupal\Component\Utility\SortArray', 'sortByWeightProperty']);
// Remove incremental weight hack.
foreach ($children as $key => $child) {
$children[$key]['#weight'] = floor($children[$key]['#weight']);
}
// Put children back in the main array.
$array += $children;
return $array;
}
/**
* Converts a string to lowerCamel case, suitably for a class property name.
*
* @param string $string
* The input string.
*
* @return string
* The input string converted to camelCase.
*/
function checklistapi_strtolowercamel($string) {
$string = str_replace('_', ' ', $string);
$string = ucwords($string);
$string = str_replace(' ', '', $string);
$string = Unicode::lcfirst($string);
return $string;
}
/**
* Implements hook_theme().
*/
function checklistapi_theme() {
return [
'checklistapi_progress_bar' => [
'path' => drupal_get_path('module', 'checklistapi') . '/templates',
'template' => 'checklistapi-progress-bar',
'variables' => [
'message' => '',
'number_complete' => 0,
'number_of_items' => 0,
'percent_complete' => 0,
],
],
];
}
permission_callbacks:
- \Drupal\checklistapi\ChecklistapiPermissions::universalPermissions
- \Drupal\checklistapi\ChecklistapiPermissions::perChecklistPermissions
checklistapi.report:
path: /admin/reports/checklistapi
defaults:
_title: Checklists
_controller: \Drupal\checklistapi\Controller\ChecklistapiController::report
requirements:
_permission: view checklistapi checklists report
route_callbacks:
- \Drupal\checklistapi\Routing\ChecklistapiRoutes::routes
services:
access_check.checklistapi:
class: Drupal\checklistapi\Access\ChecklistapiAccessCheck
tags:
- { name: access_check, applies_to: _checklistapi_access }
checklistapi_storage.config:
class: Drupal\checklistapi\Storage\ConfigStorage
arguments: ['@config.factory']
checklistapi_storage.state:
class: Drupal\checklistapi\Storage\StateStorage
arguments: ['@state']
name: Checklist API Example
type: module
description: Provides an example implementation of the Checklist API.
package: Example modules
core: 8.x
core_version_requirement: '^8 || ^9'
dependencies:
- drupal:checklistapi
configure: checklistapi.checklists.example_checklist
# Information added by Drupal.org packaging script on 2020-04-10
version: '8.x-1.11'
project: 'checklistapi'
datestamp: 1586536893
<?php
/**
* @file
* Install, update and uninstall functions for the checklistapiexample module.
*/
/**
* Implements hook_uninstall().
*/
function checklistapiexample_uninstall() {
/** @var \Drupal\checklistapi\Storage\StateStorage $storage */
$storage = \Drupal::service('checklistapi_storage.state');
$storage->setChecklistId('example_checklist')->deleteSavedProgress();
}
/**
* Move saved progress from old config storage to new state storage.
*/
function checklistapiexample_update_8101() {
$checklist_id = 'example_checklist';
// Get saved progress from old config storage.
/** @var \Drupal\checklistapi\Storage\ConfigStorage $old_config_storage */
$old_config_storage = \Drupal::service('checklistapi_storage.config');
$saved_progress = $old_config_storage->setChecklistId($checklist_id)
->getSavedProgress();
if (!$saved_progress) {
return;
}
// Copy saved progress to new state storage.
/** @var \Drupal\checklistapi\Storage\StateStorage $new_state_storage */
$new_state_storage = \Drupal::service('checklistapi_storage.state');
$new_state_storage->setChecklistId($checklist_id)
->setSavedProgress($saved_progress);
// Delete old config storage.
$old_config_storage->deleteSavedProgress();
}
checklistapiexample.checklist:
title: Checklist API example
description: An example implementation of the Checklist API.
route_name: checklistapi.checklists.example_checklist
parent: system.admin_config_development
{
"name": "drupal/checklistapiexample",
"type": "drupal-module",
"description": "Provides an example implementation of the Checklist API.",
"license": "GPL-2.0-or-later",
"require": {},
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
}
},
"minimum-stability": "dev"
}
{
"name": "drupal/checklistapi",
"type": "drupal-module",
"description": "Provides an API for creating fillable, persistent checklists.",
"homepage": "http://drupal.org/project/checklistapi",
"license": "GPL-2.0-or-later",
"authors": [
{
"name": "Travis Carden",
"email": "travis.carden@gmail.com"
}
],
"require": {
"php": ">=5.6.0"
},
"extra": {
"branch-alias": {
"dev-master": "1.x-dev"
},
"drush": {
"services": {
"drush.services.yml": "^10"
}
}
},
"minimum-stability": "dev",
"support": {
"issues": "https://www.drupal.org/project/issues/checklistapi",
"source": "https://git.drupalcode.org/project/checklistapi"
}
}
# Schema for the configuration files of the Checklist API module.
checklistapi.progress.*:
type: config_object
label: 'Checklist saved progress'
mapping:
progress:
type: mapping
label: 'The saved progress data.'
mapping:
'#changed':
type: integer
label: 'Last changed timestamp'
'#changed_by':
type: string
label: 'Last change user ID'
'#completed_items':
type: integer
label: 'Number of completed items'
'#items':
type: sequence
label: 'Checklist items'
sequence:
type: mapping
mapping:
'#completed':
type: integer
label: 'Completed timestamp'
'#uid':
type: string
label: 'Completed user ID'
build:
assessment:
validate_codebase:
phplint:
container_composer:
csslint:
eslint:
phpcs:
phpstan:
halt-on-fail: false
testing:
run_tests.standard:
types: 'Simpletest,PHPUnit-Unit,PHPUnit-Kernel,PHPUnit-Functional'
suppress-deprecations: false
run_tests.js:
concurrency: 1
types: 'PHPUnit-FunctionalJavascript'
suppress-deprecations: false
nightwatchjs:
services:
checklistapi.commands:
class: \Drupal\checklistapi\Commands\ChecklistapiCommands
arguments: ['@logger.factory']
tags:
- { name: drush.command }
<?php
namespace Drupal\checklistapi\Access;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
use Drupal\Core\Routing\RouteMatchInterface;
/**
* An access check service for checklist routes.
*/
class ChecklistapiAccessCheck implements AccessInterface {
/**
* Checks routing access for the checklist.
*
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*
* @return \Drupal\Core\Access\AccessResult
* Returns an access result.
*/
public function access(RouteMatchInterface $route_match) {
$op = $route_match->getParameter('op') ?: 'any';
$id = $route_match->getParameter('checklist_id');
if (!$id) {
return AccessResult::neutral();
}
return AccessResult::allowedIf(checklistapi_checklist_access($id, $op));
}
}
<?php
namespace Drupal\checklistapi;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
use Drupal\user\Entity\User;
/**
* Defines the checklist class.
*/
class ChecklistapiChecklist {
/**
* The configuration key for saved progress.
*/
const PROGRESS_CONFIG_KEY = 'progress';
/**
* The checklist ID.
*
* @var string
*/
public $id;
/**
* The checklist title.
*
* @var string
*/
public $title;
/**
* The menu item description.
*
* @var string
*/
public $description;
/**
* The checklist path.
*
* @var string
*/
public $path;
/**
* The checklist help.
*
* @var string
*/
public $help;
/**
* The name of the menu to put the menu item in.
*
* @var string
*/
public $menuName;
/**
* The storage backend for saved progress.
*
* @var \Drupal\checklistapi\Storage\StorageInterface
*/
private $storage;
/**
* The checklist weight.
*
* @var float
*/
public $weight;
/**
* The number of list items in the checklist.
*
* @var int
*/
public $numberOfItems = 0;
/**
* The checklist groups and items.
*
* @var array
*/
public $items = [];
/**
* The saved progress data.
*
* @var array
*/
public $savedProgress;
/**
* The configuration object for saving progress.
*
* @var \Drupal\Core\Config\Config
*/
public $config;
/**
* Constructs a ChecklistapiChecklist object.
*
* @param array $definition
* A checklist definition, as returned by checklistapi_get_checklist_info().
*/
public function __construct(array $definition) {
foreach (Element::children($definition) as $group_key) {
$this->numberOfItems += count(Element::children($definition[$group_key]));
$this->items[$group_key] = $definition[$group_key];
unset($definition[$group_key]);
}
foreach ($definition as $property_key => $value) {
if ($property_key === '#storage') {
continue;
}
$property_name = checklistapi_strtolowercamel(mb_substr($property_key, 1));
$this->$property_name = $value;
}
$storage = 'config';
$allowed_storage_values = ['config', 'state'];
if (isset($definition['#storage']) && in_array($definition['#storage'], $allowed_storage_values)) {
$storage = $definition['#storage'];
}
$this->storage = \Drupal::service("checklistapi_storage.{$storage}")
->setChecklistId($this->id);
$this->savedProgress = $this->storage->getSavedProgress();
}
/**
* Clears the saved progress for the checklist.
*
* Deletes the storage object containing the checklist's saved progress.
*/
public function clearSavedProgress() {
$this->storage->deleteSavedProgress();
\Drupal::messenger()->addMessage(t('%title saved progress has been cleared.', [
'%title' => $this->title,
]));
}
/**
* Gets the total number of completed items.
*
* @return int
* The number of completed items.
*/
public function getNumberCompleted() {
return (!empty($this->savedProgress['#completed_items'])) ? $this->savedProgress['#completed_items'] : 0;
}
/**
* Gets the total number of items.
*
* @return int
* The number of items.
*/
public function getNumberOfItems() {
return $this->numberOfItems;
}
/**
* Gets the name of the last user to update the checklist.
*
* @return string
* The themed name of the last user to update the checklist, 'n/a' if there
* is no saved progress, or '[missing user] if the user no longer exists.
*/
public function getLastUpdatedUser() {
if (!isset($this->savedProgress['#changed_by'])) {
return t('n/a');
}
$user = User::load($this->savedProgress['#changed_by']);
return ($user) ? $user->getAccountName() : t('[missing user]');
}
/**
* Gets the last updated date.
*
* @return string
* The last updated date formatted with format_date(), or 'n/a' if there is
* no saved progress.
*/
public function getLastUpdatedDate() {
return (!empty($this->savedProgress['#changed'])) ? \Drupal::service('date.formatter')->format($this->savedProgress['#changed']) : t('n/a');
}
/**
* Gets the percentage of items complete.
*
* @return float
* The percent complete.
*/
public function getPercentComplete() {
if ($this->getNumberOfItems() == 0) {
return 100;
}
return ($this->getNumberCompleted() / $this->getNumberOfItems()) * 100;
}
/**
* Gets the route name.
*
* @return string
* The route name.
*/
public function getRouteName() {
return "checklistapi.checklists.{$this->id}";
}
/**
* Gets the checklist form URL.
*
* @return \Drupal\Core\Url
* The URL to the checklist form.
*/
public function getUrl() {
return new Url($this->getRouteName());
}
/**
* Determines whether the checklist has saved progress.
*
* @return bool
* TRUE if the checklist has saved progress, or FALSE if it doesn't.
*/
public function hasSavedProgress() {
return (bool) $this->storage->getSavedProgress();
}
/**
* Saves checklist progress.
*
* @param array $values
* A multidimensional array of form state checklist values.
*
* @see checklistapi_checklist_form_submit()
*/
public function saveProgress(array $values) {
$user = \Drupal::currentUser();
$time = time();
$num_changed_items = 0;
$progress = [
'#changed' => $time,
'#changed_by' => $user->id(),
'#completed_items' => 0,
'#items' => [],
];
// Loop through groups.
foreach ($values as $group_key => $group) {
if (!is_array($group)) {
continue;
}
// Loop through items.
foreach ($group as $item_key => $item) {
$definition = checklistapi_get_checklist_info($this->id);
if (!in_array($item_key, array_keys($definition[$group_key]))) {
// This item wasn't in the checklist definition. Don't include it with
// saved progress.
continue;
}
$old_item = (!empty($this->savedProgress['#items'][$item_key])) ? $this->savedProgress['#items'][$item_key] : 0;
if ($item == 1) {
// Item is checked.
$progress['#completed_items']++;
if ($old_item) {
// Item was previously checked. Use saved value.
$new_item = $old_item;
}
else {
// Item is newly checked. Set new value.
$new_item = [
'#completed' => $time,
'#uid' => $user->id(),
];
$num_changed_items++;
}
$progress['#items'][$item_key] = $new_item;
}
else {
// Item is unchecked.
if ($old_item) {
// Item was previously checked.
$num_changed_items++;
}
}
}
}
// Sort array elements alphabetically so changes to the order of items in
// checklist definitions over time don't affect the order of elements in the
// saved progress details. This reduces non-substantive changes to
// configuration files.
ksort($progress);
$this->storage->setSavedProgress($progress);
\Drupal::messenger()->addMessage(\Drupal::translation()->formatPlural(
$num_changed_items,
'%title progress has been saved. 1 item changed.',
'%title progress has been saved. @count items changed.',
['%title' => $this->title]
));
}
/**
* Determines whether the current user has access to the checklist.
*
* @param string $operation
* (optional) The operation to test access for. Possible values are "view",
* "edit", and "any". Defaults to "any".
*
* @return bool
* Returns TRUE if the user has access, or FALSE if not.
*/
public function userHasAccess($operation = 'any') {
return checklistapi_checklist_access($this->id, $operation);
}
}
<?php
namespace Drupal\checklistapi;
/**
* Defines a class containing permission callbacks.
*/
class ChecklistapiPermissions {
/**
* Constructs a ChecklistapiPermissions object.
*/
public function __construct() {
$this->editPermissionDescription = t('Check and uncheck list items and save changes, or clear saved progress.');
$this->viewPermissionDescription = t('Read-only access: View list items and saved progress.');
}
/**
* Returns an array of universal permissions.
*
* @return array
* An array of permission details.
*/
public function universalPermissions() {
$perms['view checklistapi checklists report'] = [
'title' => t('View the Checklists report'),
];
$perms['view any checklistapi checklist'] = [
'title' => t('View any checklist'),
'description' => $this->viewPermissionDescription,
];
$perms['edit any checklistapi checklist'] = [
'title' => t('Edit any checklist'),
'description' => $this->editPermissionDescription,
];
return $perms;
}
/**
* Returns an array of per checklist permissions.
*
* @return array
* An array of permission details.
*/
public function perChecklistPermissions() {
$perms = [];
// Per checklist permissions.
foreach (checklistapi_get_checklist_info() as $id => $definition) {
$checklist = checklistapi_checklist_load($id);
if (!$checklist) {
continue;
}
$title = $checklist->title;
$perms["view {$id} checklistapi checklist"] = [
'title' => t('View the @name checklist', ['@name' => $title]),
'description' => $this->viewPermissionDescription,
];
$perms["edit {$id} checklistapi checklist"] = [
'title' => t('Edit the @name checklist', ['@name' => $title]),
'description' => $this->editPermissionDescription,
];
}
return $perms;
}
}
<?php
namespace Drupal\checklistapi\Commands;
use Consolidation\OutputFormatters\FormatterManager;
use Consolidation\OutputFormatters\Options\FormatterOptions;
use Consolidation\OutputFormatters\StructuredData\RowsOfFields;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Render\Element;
use Drupal\user\Entity\User;
use Drush\Commands\DrushCommands;
use Drush\Commands\help\ListCommands;
use Psr\Log\LoggerAwareInterface;
/**
* Checklist API Drush command fileA Drush commandfile.
*/
class ChecklistapiCommands extends DrushCommands implements LoggerAwareInterface {
/**
* The logger channel.
*
* @var \Drupal\Core\Logger\LoggerChannelInterface
*/
protected $logger;
/**
* Constructs an instance.
*
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_channel
* The logger channel factory.
*/
public function __construct(LoggerChannelFactoryInterface $logger_channel) {
$this->logger = $logger_channel->get('drush');
}
/**
* Get an overview of your installed checklists with progress details.
*
* @command checklistapi:list
* @aliases capi-list,capil,checklistapi-list
*/
public function listCommand() {
$definitions = checklistapi_get_checklist_info();
if (empty($definitions)) {
return $this->logger->alert(dt('No checklists available.'));
}
// Build table rows.
$rows = [];
// The first row is the table header.
$rows[] = [
dt('Checklist'),
dt('Progress'),
dt('Last updated'),
dt('Last updated by'),
];
foreach ($definitions as $id => $definition) {
$checklist = checklistapi_checklist_load($id);
$row = [];
$row[] = dt('!title (@id)', [
'!title' => strip_tags($checklist->title),
'@id' => $id,
]);
$row[] = dt('@completed of @total (@percent%)', [
'@completed' => $checklist->getNumberCompleted(),
'@total' => $checklist->getNumberOfItems(),
'@percent' => round($checklist->getPercentComplete()),
]);
$row[] = $checklist->getLastUpdatedDate();
$row[] = $checklist->getLastUpdatedUser();
$rows[] = $row;
}
$formatter_manager = new FormatterManager();
$opts = [
FormatterOptions::INCLUDE_FIELD_LABELS => FALSE,
FormatterOptions::TABLE_STYLE => 'compact',
FormatterOptions::TERMINAL_WIDTH => ListCommands::getTerminalWidth(),
];
$formatter_options = new FormatterOptions([], $opts);
$formatter_manager->write($this->output(), 'table', new RowsOfFields($rows), $formatter_options);
}
/**
* Show detailed info for a given checklist.
*
* @param string $checklist_id
* The checklist machine name, e.g., "example_checklist".
*
* @return string|void
* The command output.
*
* @command checklistapi:info
* @aliases capi-info,capii,checklistapi-info
*/
public function infoCommand($checklist_id) {
$checklist = checklistapi_checklist_load($checklist_id);
// Make sure the given checklist exists.
if (!$checklist) {
return $this->logger->error(dt('No such checklist "@id".', [
'@id' => $checklist_id,
]));
}
$output = [];
// Print the help.
if (!empty($checklist->help)) {
$output[] = strip_tags($checklist->help);
}
// Print last updated and progress details.
if ($checklist->hasSavedProgress()) {
$output[] = '';
$output[] = dt('Last updated @date by @user', [
'@date' => $checklist->getLastUpdatedDate(),
'@user' => $checklist->getLastUpdatedUser(),
]);
$output[] = dt('@completed of @total (@percent%) complete', [
'@completed' => $checklist->getNumberCompleted(),
'@total' => $checklist->getNumberOfItems(),
'@percent' => round($checklist->getPercentComplete()),
]);
}
// Loop through groups.
$groups = $checklist->items;
foreach (Element::children($groups) as $group_key) {
$group = &$groups[$group_key];
// Print group title.
$output[] = '';
$output[] = strip_tags($group['#title']) . ':';
// Loop through items.
foreach (Element::children($group) as $item_key) {
$item = &$group[$item_key];
$saved_item = !empty($checklist->savedProgress['#items'][$item_key]) ? $checklist->savedProgress['#items'][$item_key] : 0;
// Build title.
$title = strip_tags($item['#title']);
if ($saved_item) {
// Append completion details.
$user = User::load($saved_item['#uid']);
$title .= ' - ' . dt('Completed @time by @user', [
'@time' => \Drupal::service('date.formatter')->format($saved_item['#completed'], 'short'),
'@user' => $user->getAccountName(),
]);
}
// Print the list item.
$output[] = dt(' [@x] !title', [
'@x' => ($saved_item) ? 'x' : ' ',
'!title' => $title,
]);
}
}
$output[] = '';
return implode(PHP_EOL, $output);
}
}
<?php
namespace Drupal\checklistapi\Controller;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Link;
use Drupal\Core\Url;
/**
* Controller for Checklist API.
*/
class ChecklistapiController extends ControllerBase {
/**
* Returns the Checklists report.
*
* @return array
* Returns a render array.
*/
public function report() {
// Define table header.
$header = [
['data' => $this->t('Checklist')],
[
'data' => $this->t('Progress'),
'class' => [RESPONSIVE_PRIORITY_MEDIUM],
],
[
'data' => $this->t('Last updated'),
'class' => [RESPONSIVE_PRIORITY_MEDIUM],
],
[
'data' => $this->t('Last updated by'),
'class' => [RESPONSIVE_PRIORITY_LOW],
],
['data' => $this->t('Operations')],
];
// Build table rows.
$rows = [];
$definitions = checklistapi_get_checklist_info();
foreach ($definitions as $id => $definition) {
$checklist = checklistapi_checklist_load($id);
$row = [];
$row[] = [
'data' => ($checklist->userHasAccess()) ? Link::fromTextAndUrl($checklist->title, $checklist->getUrl()) : $checklist->title,
'title' => (!empty($checklist->description)) ? $checklist->description : '',
];
$row[] = $this->t('@completed of @total (@percent%)', [
'@completed' => $checklist->getNumberCompleted(),
'@total' => $checklist->getNumberOfItems(),
'@percent' => round($checklist->getPercentComplete()),
]);
$row[] = $checklist->getLastUpdatedDate();
$row[] = $checklist->getLastUpdatedUser();
if ($checklist->userHasAccess('edit') && $checklist->hasSavedProgress()) {
$row[] = [
'data' => [
'#type' => 'operations',
'#links' => [
'clear' => [
'title' => $this->t('Clear'),
'url' => Url::fromRoute($checklist->getRouteName() . '.clear', [], [
'query' => $this->getDestinationArray(),
]),
],
],
],
];
}
else {
$row[] = '';
}
$rows[] = $row;
}
// Compile output.
$output['table'] = [
'#theme' => 'table',
'#header' => $header,
'#rows' => $rows,
'#empty' => $this->t('No checklists available.'),
];
return $output;
}
}
<?php
namespace Drupal\checklistapi\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a form to clear saved progress for a given checklist.
*/
class ChecklistapiChecklistClearForm extends ConfirmFormBase {
/**
* The checklist object.
*
* @var \Drupal\checklistapi\ChecklistapiChecklist
*/
public $checklist;
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'checklistapi_checklist_clear_confirm';
}
/**
* {@inheritdoc}
*/
public function getQuestion() {
return t('Are you sure you want to clear saved progress?');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl() {
return $this->checklist->getUrl();
}
/**
* {@inheritdoc}
*/
public function getDescription() {
return t('All progress details for %checklist will be erased. This action cannot be undone.', [
'%checklist' => $this->checklist->title,
]);
}
/**
* {@inheritdoc}
*/
public function getConfirmText() {
return t('Clear');
}
/**
* {@inheritdoc}
*/
public function getCancelText() {
return t('Cancel');
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $checklist_id = NULL) {
$this->checklist = checklistapi_checklist_load($checklist_id);
$form['#checklist'] = $this->checklist;
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
// Clear saved progress.
$form['#checklist']->clearSavedProgress();
// Redirect back to checklist.
$form_state->setRedirect($form['#checklist']->getRouteName());
}
}
<?php
namespace Drupal\checklistapi\Form;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Form\FormInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Link;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Render\Element;
use Drupal\user\Entity\User;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provides a checklist form.
*/
class ChecklistapiChecklistForm implements FormInterface, ContainerInjectionInterface {
/**
* The date formatter service.
*
* @var \Drupal\Core\Datetime\DateFormatterInterface
*/
protected $dateFormatter;
/**
* The messenger service.
*
* @var \Drupal\Core\Messenger\MessengerInterface
*/
protected $messenger;
/**
* Constructs an instance.
*
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
* The date formatter service.
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
* The messenger service.
*/
public function __construct(DateFormatterInterface $date_formatter, MessengerInterface $messenger) {
$this->dateFormatter = $date_formatter;
$this->messenger = $messenger;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
/** @var \Drupal\Core\Datetime\DateFormatterInterface $date_formatter */
$date_formatter = $container->get('date.formatter');
/** @var \Drupal\Core\Messenger\MessengerInterface $messenger */
$messenger = $container->get('messenger');
return new static($date_formatter, $messenger);
}
/**
* {@inheritdoc}
*/
public function getFormId() {
return 'checklistapi_checklist_form';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, $checklist_id = NULL) {
$form['#checklist'] = $checklist = checklistapi_checklist_load($checklist_id);
$user_has_edit_access = $checklist->userHasAccess('edit');
// Progress bar.
$form['progress_bar'] = [
'#theme' => 'checklistapi_progress_bar',
'#message' => ($checklist->hasSavedProgress()) ? t('Last updated @date by @user', [
'@date' => $checklist->getLastUpdatedDate(),
'@user' => $checklist->getLastUpdatedUser(),
]) : '',
'#number_complete' => $checklist->getNumberCompleted(),
'#number_of_items' => $checklist->getNumberOfItems(),
'#percent_complete' => (int) round($checklist->getPercentComplete()),
'#attached' => [
'library' => [
'classy/progress',
],
],
];
// Compact mode.
if (checklistapi_compact_mode_is_on()) {
$form['#attributes']['class'] = ['compact-mode'];
}
$form['compact_mode_link'] = [
'#markup' => '<div class="compact-link"></div>',
];
// General properties.
$form['checklistapi'] = [
'#attached' => [
'library' => ['checklistapi/checklistapi'],
],
'#tree' => TRUE,
'#type' => 'vertical_tabs',
];
// Loop through groups.
$num_autochecked_items = 0;
$groups = $checklist->items;
foreach (Element::children($groups) as $group_key) {
$group = &$groups[$group_key];
$form[$group_key] = [
'#title' => Xss::filter($group['#title']),
'#type' => 'details',
'#group' => 'checklistapi',
];
if (!empty($group['#description'])) {
$form[$group_key]['#description'] = Xss::filterAdmin($group['#description']);
}
// Loop through items.
foreach (Element::children($group) as $item_key) {
$item = &$group[$item_key];
$saved_item = !empty($checklist->savedProgress['#items'][$item_key]) ? $checklist->savedProgress['#items'][$item_key] : 0;
// Build title.
$title = Xss::filter($item['#title']);
if ($saved_item) {
// Append completion details.
$user = User::load($saved_item['#uid']);
$title .= '<span class="completion-details"> - ' . t('Completed @time by @user', [
'@time' => $this->dateFormatter->format($saved_item['#completed'], 'short'),
'@user' => ($user) ? $user->getAccountName() : t('[missing user]'),
]) . '</span>';
}
// Set default value.
$default_value = FALSE;
if ($saved_item) {
$default_value = TRUE;
}
elseif (!empty($item['#default_value'])) {
if ($default_value = $item['#default_value']) {
$num_autochecked_items++;
}
}
// Get description.
$description = (isset($item['#description'])) ? '<p>' . Xss::filterAdmin($item['#description']) . '</p>' : '';
// Append links.
$links = [];
foreach (Element::children($item) as $link_key) {
$link = &$item[$link_key];
$links[] = Link::fromTextAndUrl($link['#text'], $link['#url'])->toString();
}
if (count($links)) {
$description .= '<div class="links">' . implode(' | ', $links) . '</div>';
}
// Compile the list item.
$form[$group_key][$item_key] = [
'#attributes' => ['class' => ['checklistapi-item']],
'#default_value' => $default_value,
'#description' => Xss::filterAdmin($description),
'#disabled' => !($user_has_edit_access),
'#title' => Xss::filterAdmin($title),
'#type' => 'checkbox',
'#group' => $group_key,
'#parents' => ['checklistapi', $group_key, $item_key],
];
}
}
// Actions.
$form['actions'] = [
'#access' => $user_has_edit_access,
'#type' => 'actions',
'#weight' => 100,
'save' => [
'#button_type' => 'primary',
'#type' => 'submit',
'#value' => t('Save'),
],
'clear' => [
'#access' => $checklist->hasSavedProgress(),
'#button_type' => 'danger',
'#attributes' => ['class' => ['clear-saved-progress']],
'#submit' => [[$this, 'clear']],
'#type' => 'submit',
'#value' => t('Clear saved progress'),
],
];
// Alert the user of autochecked items. Only set the message on GET requests
// to prevent it from reappearing after saving the form. (Testing the
// request method may not be the "correct" way to accomplish this.)
if ($num_autochecked_items && $_SERVER['REQUEST_METHOD'] == 'GET') {
$args = [
'%checklist' => $checklist->title,
'@num' => $num_autochecked_items,
];
$message = \Drupal::translation()->formatPlural(
$num_autochecked_items,
t('%checklist found 1 unchecked item that was already completed and checked it for you. Save the form to record the change.', $args),
t('%checklist found @num unchecked items that were already completed and checked them for you. Save the form to record the changes.', $args)
);
$this->messenger->addStatus($message);
}
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state) {}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state) {
/** @var \Drupal\checklistapi\ChecklistapiChecklist $checklist */
$checklist = $form['#checklist'];
// Save progress.
$values = $form_state->getValue('checklistapi');
$checklist->saveProgress($values);
// Preserve the active tab after submission.
$form_state->setRedirect($checklist->getRouteName(), [], [
'fragment' => $values['checklistapi__active_tab'],
]);
}
/**
* Form submission handler for the 'clear' action.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function clear(array &$form, FormStateInterface $form_state) {
$form_state->setRedirect($form['#checklist']->getRouteName() . '.clear');
}
}
<?php
namespace Drupal\checklistapi\Routing;
use Symfony\Component\Routing\Route;
/**
* Provides routes for checklists.
*/
class ChecklistapiRoutes {
/**
* Provides dynamic routes.
*
* @return \Symfony\Component\Routing\Route[]
* An array of route objects.
*/
public function routes() {
$routes = [];
$definitions = \Drupal::moduleHandler()->invokeAll('checklistapi_checklist_info');
foreach ($definitions as $id => $definition) {
// Ignore incomplete definitions.
if (empty($definition['#path']) || empty($definition['#title'])) {
continue;
}
$requirements = ['_checklistapi_access' => 'TRUE'];
// View/edit checklist.
$routes["checklistapi.checklists.{$id}"] = new Route($definition['#path'], [
'_title' => (string) $definition['#title'],
'_form' => '\Drupal\checklistapi\Form\ChecklistapiChecklistForm',
'checklist_id' => $id,
'op' => 'any',
], $requirements);
// Clear saved progress.
$routes["checklistapi.checklists.{$id}.clear"] = new Route("{$definition['#path']}/clear", [
'_title' => 'Clear',
'_form' => '\Drupal\checklistapi\Form\ChecklistapiChecklistClearForm',
'checklist_id' => $id,
'op' => 'edit',
], $requirements);
}
return $routes;
}
}
<?php
namespace Drupal\checklistapi\Storage;
use Drupal\Core\Config\ConfigFactoryInterface;
/**
* Provides config-based checklist progress storage.
*/
class ConfigStorage extends StorageBase {
/**
* The configuration key for saved progress.
*/
const CONFIG_KEY = 'progress';
/**
* The config object.
*
* @var \Drupal\Core\Config\Config|null
*/
private $config;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
private $configFactory;
/**
* Constructs a class instance.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
*/
public function __construct(ConfigFactoryInterface $config_factory) {
$this->configFactory = $config_factory;
}
/**
* {@inheritdoc}
*/
public function getSavedProgress() {
return $this->getConfig()->get(self::CONFIG_KEY);
}
/**
* {@inheritdoc}
*/
public function setSavedProgress(array $progress) {
$this->getConfig()->set(self::CONFIG_KEY, $progress)->save();
}
/**
* {@inheritdoc}
*/
public function deleteSavedProgress() {
$this->getConfig()->delete();
}
/**
* Gets the config object.
*
* @return \Drupal\Core\Config\Config
* Returns the config object.
*/
private function getConfig() {
if (empty($this->config)) {
$this->config = $this->configFactory
->getEditable("checklistapi.progress.{$this->getChecklistId()}");
}
return $this->config;
}
}
<?php
namespace Drupal\checklistapi\Storage;
use Drupal\Core\State\StateInterface;
/**
* Provides state-based checklist progress storage.
*/
class StateStorage extends StorageBase {
/**
* The state service.
*
* @var \Drupal\Core\State\StateInterface
*/
private $state;
/**
* Constructs a class instance.
*
* @param \Drupal\Core\State\StateInterface $state
* The state service.
*/
public function __construct(StateInterface $state) {
$this->state = $state;
}
/**
* {@inheritdoc}
*/
public function getSavedProgress() {
return $this->state->get($this->stateKey());
}
/**
* {@inheritdoc}
*/
public function setSavedProgress(array $progress) {
$this->state->set($this->stateKey(), $progress);
}
/**
* {@inheritdoc}
*/
public function deleteSavedProgress() {
$this->state->delete($this->stateKey());
}
/**
* Returns the state key.
*
* @return string
* The state key.
*/
private function stateKey() {
return 'checklistapi.progress.' . $this->getChecklistId();
}
}
<?php
namespace Drupal\checklistapi\Storage;
/**
* Provides a base storage implementation for others to extend.
*/
abstract class StorageBase implements StorageInterface {
/**
* The checklist ID.
*
* @var string
*/
private $checklistId;
/**
* Sets the checklist ID.
*
* @param string $id
* The checklist ID.
*
* @return self
* The storage object.
*/
public function setChecklistId($id) {
if (!is_string($id)) {
throw new \InvalidArgumentException('A checklist ID must be a string.');
}
$this->checklistId = $id;
return $this;
}
/**
* Gets the checklist ID.
*
* @return string
* Returns the checklist ID.
*/
protected function getChecklistId() {
if (empty($this->checklistId)) {
throw new \LogicException('You must set the checklist ID before accessing saved progress.');
}
return $this->checklistId;
}
}
<?php
namespace Drupal\checklistapi\Storage;
/**
* Provides an interface for checklist storage.
*/
interface StorageInterface {
/**
* Sets the checklist ID.
*
* @param string $id
* The checklist ID.
*/
public function setChecklistId($id);
/**
* Gets the saved checklist progress.
*
* @return mixed
* The stored value, or NULL if no value exists.
*/
public function getSavedProgress();
/**
* Sets the saved checklist progress.
*
* @param array $progress
* An array of checklist progress data as built by ChecklistapiChecklist.
*/
public function setSavedProgress(array $progress);
/**
* Deletes the saved checklist progress.
*/
public function deleteSavedProgress();
}
{#
/**
* @file
* Default theme implementation of a checklist progress bar.
*
* Available variables:
* - message: A string containing information to be displayed.
* - number_complete: The number of items completed.
* - number_of_items: The total number of items in the checklist.
* - percent_complete: The percentage of the progress.
*
* @ingroup themeable
*/
#}
<div class="progress" data-drupal-progress>
<div class="progress__track"><div class="progress__bar" style="width: {{ percent_complete }}%;"></div></div>
<div class="progress__percentage">{{ number_complete }} of {{ number_of_items }} ({{ percent_complete }}%)</div>
<div class="progress__description">{{ message }}</div>
</div>
<?php
namespace Drupal\Tests\checklistapi\Functional;
use Drupal\Tests\BrowserTestBase;
/**
* Functionally tests Checklist API.
*
* @group checklistapi
*
* @todo Add tests for vertical tabs progress indicators.
* @todo Add tests for saving and retrieving checklist progress.
* @todo Add tests for clearing saved progress.
*/
class ChecklistapiTest extends BrowserTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = [
'checklistapi',
'checklistapiexample',
'help',
'block',
];
/**
* {@inheritdoc}
*/
protected $defaultTheme = 'stark';
/**
* A user object with permission to edit any checklist.
*
* @var \Drupal\user\Entity\User
*/
protected $privilegedUser;
/**
* {@inheritdoc}
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
public function setUp() {
parent::setUp();
// Create a privileged user.
$permissions = ['edit any checklistapi checklist'];
$this->privilegedUser = $this->drupalCreateUser($permissions);
$this->drupalLogin($this->privilegedUser);
// Place help block.
$this->drupalPlaceBlock('help_block', ['region' => 'help']);
}
/**
* Tests checklist access.
*
* @throws \Drupal\Core\Entity\EntityStorageException
*/
public function testChecklistAccess() {
// Assert that access is granted to a user with "edit any checklistapi
// checklist" permission.
$this->drupalGet('admin/config/development/checklistapi-example');
$this->assertResponse(200);
// Assert that access is granted to a user with checklist-specific
// permission.
$permissions = ['edit example_checklist checklistapi checklist'];
$semi_privileged_user = $this->drupalCreateUser($permissions);
$this->drupalLogin($semi_privileged_user);
$this->drupalGet('admin/config/development/checklistapi-example');
$this->assertResponse(200);
// Assert that access is denied to a non-privileged user.
$this->drupalLogout();
$this->drupalGet('admin/config/development/checklistapi-example');
$this->assertResponse(403);
}
/**
* Tests checklist composition.
*/
public function testChecklistComposition() {
// Assert that a per-checklist help block is created.
$this->drupalGet('admin/config/development/checklistapi-example');
$this->assertRaw('This checklist based on');
}
/**
* Tests permissions.
*/
public function testPermissions() {
self::assertTrue($this->checkPermissions([
'view checklistapi checklists report',
'view any checklistapi checklist',
'edit any checklistapi checklist',
]), 'Created universal permissions.');
self::assertTrue($this->checkPermissions([
'view example_checklist checklistapi checklist',
'edit example_checklist checklistapi checklist',
]), 'Created per-checklist permissions.');
}
}
<?php
namespace Drupal\Tests\checklistapi\Unit;
use Drupal\Core\Render\Element;
use Drupal\Core\Url;
use Drupal\Tests\UnitTestCase;
require_once __DIR__ . '/../../../checklistapi.module';
/**
* Tests the functions in checklistapi.module.
*
* @group checklistapi
*/
class ChecklistapiModuleTest extends UnitTestCase {
/**
* Tests checklistapi_sort_array().
*/
public function testChecklistapiSortArray() {
$input = [
'#title' => 'Checklist API test',
'#path' => 'admin/config/development/checklistapi-test',
// @codingStandardsIgnoreLine
'#description' => 'A test checklist.',
'#help' => '<p>This is a test checklist.</p>',
'group_two' => [
'#title' => 'Group two',
],
'group_one' => [
'#title' => 'Group one',
// @codingStandardsIgnoreLine
'#description' => '<p>Group one description.</p>',
'#weight' => -1,
'item_three' => [
'#title' => 'Item three',
'#weight' => 1,
],
'item_one' => [
'#title' => 'Item one',
// @codingStandardsIgnoreLine
'#description' => 'Item one description',
'#weight' => -1,
'link_three' => [
'#text' => 'Link three',
'#url' => Url::fromUri('http://example.com/three'),
'#weight' => 3,
],
'link_two' => [
'#text' => 'Link two',
'#url' => Url::fromUri('http://example.com/two'),
'#weight' => 2,
],
'link_one' => [
'#text' => 'Link one',
'#url' => Url::fromUri('http://example.com/one'),
'#weight' => 1,
],
],
'item_two' => [
'#title' => 'Item two',
],
],
'group_four' => [
'#title' => 'Group four',
'#weight' => 1,
],
'group_three' => [
'#title' => 'Group three',
],
];
$output = checklistapi_sort_array($input);
$this->assertEquals(0, $output['group_two']['#weight'], 'Failed to supply a default for omitted element weight.');
$this->assertEquals(0, $output['group_three']['#weight'], 'Failed to supply a default in place of invalid element weight.');
$this->assertEquals(-1, $output['group_one']['#weight'], 'Failed to retain a valid element weight.');
$this->assertEquals(
['group_one', 'group_two', 'group_three', 'group_four'],
Element::children($output),
'Failed to sort elements by weight.'
);
$this->assertEquals(
['link_one', 'link_two', 'link_three'],
Element::children($output['group_one']['item_one']),
'Failed to recurse through element descendants.'
);
}
/**
* Tests checklistapi_strtolowercamel().
*/
public function testChecklistapiStrtolowercamel() {
$this->assertEquals('abcDefGhi', checklistapi_strtolowercamel('Abc def_ghi'), 'Failed to convert string to lowerCamel case.');
}
/**
* Tests that checklistapi_checklist_access() rejects an invalid mode.
*/
public function testChecklistapiChecklistAccessInvalidMode() {
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionMessage('No such operation "invalid operation');
checklistapi_checklist_access(NULL, 'invalid operation');
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment