Compare commits

...

22 Commits

Author SHA1 Message Date
Daniel Waxweiler 979ecbc91f upgrade gulp to version 5 2024-05-16 08:57:35 +01:00
Daniel Waxweiler 08e80615c6 add note about API to description 2024-05-15 08:34:06 +01:00
Daniel Waxweiler 526d57d1b2 add missing changelog entry 2024-05-15 08:32:32 +01:00
Daniel Waxweiler 718d66506b update some dev dependencies 2024-05-15 08:31:06 +01:00
Daniel Waxweiler cccd1a78b5 add some spacing between event items 2024-05-14 10:30:42 +02:00
Daniel Waxweiler fa99821ffc fix undefined variable for both error views 2024-05-14 10:28:39 +02:00
Daniel Waxweiler 7bc35a3923 mention use of GraphQL API 2024-05-13 14:23:37 +02:00
Daniel Waxweiler 82800a9db3 clarify two steps in release procedure 2024-04-06 08:27:24 +02:00
Daniel Waxweiler 46170377af prepare next release 2024-04-06 08:25:16 +02:00
Daniel Waxweiler 5c1a186456 release version 1.0.0 2024-04-06 08:22:26 +02:00
Daniel Waxweiler 7a0075e5ed clarify features 2024-04-06 07:50:47 +02:00
Daniel Waxweiler 392d552dab npm audit fix 2024-04-06 07:42:42 +02:00
Daniel Waxweiler 797a08cfb6 update dev dependencies 2024-04-06 07:41:15 +02:00
Daniel Waxweiler c5279a37f3 remove unused code and dependencies 2024-04-05 23:44:53 +02:00
Daniel Waxweiler 99e58d0382 update dependencies 2024-04-05 23:39:30 +02:00
Daniel Waxweiler dc1949ba4c
move requests to backend (#18) 2024-04-05 23:26:11 +02:00
Daniel Waxweiler 3269ccca1a add missing readme changelog entry 2024-04-02 22:13:20 +02:00
Daniel Waxweiler 2d91350cc9 prepare next release 2024-04-02 22:09:58 +02:00
Daniel Waxweiler e7fd47a346 release version 0.11.5 2024-04-02 22:09:20 +02:00
Daniel Waxweiler 8d4e81878e confirm compatibility with WordPress 6.5 2024-04-02 22:02:55 +02:00
Daniel Waxweiler 418dc829d0 document after-release step 2024-04-02 22:01:53 +02:00
Daniel Waxweiler a75d3a3915 prepare next release 2023-11-11 20:43:50 +01:00
45 changed files with 7189 additions and 7878 deletions

3
.gitignore vendored
View File

@ -3,3 +3,6 @@ build/
coverage/ coverage/
node_modules/ node_modules/
vendor/ vendor/
.phpunit.cache
.phpunit.result.cache

View File

@ -1,5 +1,2 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npm test npm test
npx lint-staged npx lint-staged

View File

@ -6,6 +6,8 @@ More details can be found in the [WordPress Plugin Directory](https://wordpress.
The current changelog can be found under [source/changelog.txt](source/changelog.txt). The current changelog can be found under [source/changelog.txt](source/changelog.txt).
This plugin uses [Mobilizon's GraphQL API](https://docs.joinmobilizon.org/contribute/graphql_api/).
## Development ## Development
### Setup ### Setup
@ -22,18 +24,23 @@ The current changelog can be found under [source/changelog.txt](source/changelog
### Release procedure ### Release procedure
1. Make sure `changelog.txt` is up-to-date. 1. Make sure `changelog.txt` is up-to-date.
2. Use a new version number and copy over the new section into `readme.txt`. 2. Create a new section with a new version number.
3. Update `package.json` with the same version number. 3. Copy over the new section into `readme.txt`.
4. Update the `package-lock.json`: `npm i --package-lock-only` 4. Update `package.json` with the same version number.
5. Build: `npm run build-prod` 5. Update the `package-lock.json`: `npm i --package-lock-only`
6. Make sure screenshots are up-to-date. 6. Build: `npm run build-prod`
7. Copy the built plugin into `/trunk` of SVN. 7. Make sure screenshots are up-to-date.
8. Create a new tag of the new version: `svn cp trunk tags/<version>` 8. Copy the built plugin into `/trunk` of SVN.
9. Check the version number occurrences in both folders. 9. Create a new tag of the new version: `svn cp trunk tags/<version>`
10. Commit everything together to the release SVN: `svn ci -m "release version <version>"` Make sure to add new files beforehand. 10. Check the version number occurrences in both folders.
11. Commit the new version in git with the same message. 11. Make sure to handle exclamation and question marks in `svn status`.
12. Tag the new version: `git tag v<version>` 12. Commit everything together to the release SVN: `svn ci -m "release version <version>"`
13. Push the new tag to the repository: `git push --tags` 13. Commit the new version in git with the same message.
14. Tag the new version: `git tag v<version>`
15. Push the new tag to the repository: `git push --tags`
16. Append `-next` to the version number in `package.json`.
17. Update the `package-lock.json`: `npm i --package-lock-only`
18. Commit: `git commit -am "prepare next release"`
### Other commands ### Other commands
@ -44,3 +51,4 @@ The current changelog can be found under [source/changelog.txt](source/changelog
- Update PHP dependencies: `composer update` - Update PHP dependencies: `composer update`
- Check for direct PHP dependency updates: `composer outdated --direct` - Check for direct PHP dependency updates: `composer outdated --direct`
- Format code with prettier: `npm run format` - Format code with prettier: `npm run format`
- Generate `vendor/autoload.php` file after creating new class: `composer dump-autoload`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

@ -1 +1,10 @@
{} {
"autoload": {
"psr-4": {
"MobilizonConnector\\": "source/includes/"
}
},
"require-dev": {
"phpunit/phpunit": "^9.6"
}
}

1738
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -11,9 +11,8 @@ function injectMetadata() {
return src( return src(
[ [
FOLDER_BUILD + '/front/block-events-loader.js', FOLDER_BUILD + '/front/block-events-loader.js',
FOLDER_BUILD + '/front/events-loader.js',
FOLDER_BUILD + '/' + PACKAGE.name + '.php', FOLDER_BUILD + '/' + PACKAGE.name + '.php',
FOLDER_BUILD + '/includes/constants.php', FOLDER_BUILD + '/includes/Constants.php',
FOLDER_BUILD + '/readme.txt', FOLDER_BUILD + '/readme.txt',
], ],
{ base: './' }, { base: './' },

12277
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "connector-mobilizon", "name": "connector-mobilizon",
"version": "0.11.4", "version": "1.0.0-next",
"description": "Display Mobilizon events in WordPress.", "description": "Display Mobilizon events in WordPress.",
"private": true, "private": true,
"type": "module", "type": "module",
@ -11,8 +11,8 @@
"coverage": "c8 --all --reporter=html --reporter=text --include=source/**/*.js ava", "coverage": "c8 --all --reporter=html --reporter=text --include=source/**/*.js ava",
"eslint": "npx eslint source/**/*.js", "eslint": "npx eslint source/**/*.js",
"format": "npx prettier --write .", "format": "npx prettier --write .",
"prepare": "husky install", "prepare": "husky",
"test": "ava" "test": "ava && ./vendor/bin/phpunit"
}, },
"author": { "author": {
"name": "Daniel Waxweiler", "name": "Daniel Waxweiler",
@ -25,33 +25,31 @@
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"graphql": "16.8.1", "graphql": "16.8.1",
"graphql-request": "6.1.0", "luxon": "3.4.4"
"luxon": "3.4.3",
"object-hash": "3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.23.3", "@babel/core": "7.24.5",
"@babel/eslint-parser": "7.23.3", "@babel/eslint-parser": "7.24.5",
"@babel/preset-env": "7.23.3", "@babel/preset-env": "7.24.5",
"@babel/preset-react": "7.23.3", "@babel/preset-react": "7.24.1",
"@wordpress/eslint-plugin": "17.2.0", "@wordpress/eslint-plugin": "18.0.0",
"ava": "5.3.1", "ava": "6.1.3",
"babel-loader": "9.1.3", "babel-loader": "9.1.3",
"browser-env": "3.3.0", "browser-env": "3.3.0",
"c8": "8.0.1", "c8": "9.1.0",
"copy-webpack-plugin": "11.0.0", "copy-webpack-plugin": "12.0.2",
"eslint": "8.53.0", "eslint": "8.57.0",
"eslint-plugin-ava": "14.0.0", "eslint-plugin-ava": "14.0.0",
"eslint-plugin-jsx": "0.1.0", "eslint-plugin-jsx": "0.1.0",
"eslint-plugin-react": "7.33.2", "eslint-plugin-react": "7.34.1",
"esm": "3.2.25", "esm": "3.2.25",
"gulp": "4.0.2", "gulp": "5.0.0",
"gulp-replace": "1.1.4", "gulp-replace": "1.1.4",
"husky": "8.0.3", "husky": "9.0.11",
"lint-staged": "15.1.0", "lint-staged": "15.2.2",
"prettier": "3.0.3", "prettier": "3.2.5",
"rimraf": "5.0.5", "rimraf": "5.0.7",
"webpack": "5.89.0", "webpack": "5.91.0",
"webpack-cli": "5.1.4" "webpack-cli": "5.1.4"
}, },
"ava": { "ava": {
@ -63,7 +61,7 @@
"niceName": "Connector for Mobilizon", "niceName": "Connector for Mobilizon",
"phpMinimumVersion": 7.4, "phpMinimumVersion": 7.4,
"wordpressMinimumVersion": 5.6, "wordpressMinimumVersion": 5.6,
"wordpressTestedUpToVersion": "6.3" "wordpressTestedUpToVersion": "6.5"
}, },
"lint-staged": { "lint-staged": {
"source/**/*.js": "eslint", "source/**/*.js": "eslint",

27
phpunit.xml Normal file
View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.6/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheResultFile=".phpunit.cache/test-results"
executionOrder="depends,defects"
forceCoversAnnotation="false"
beStrictAboutCoversAnnotation="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutTodoAnnotatedTests="true"
convertDeprecationsToExceptions="true"
failOnRisky="true"
failOnWarning="true"
verbose="true">
<testsuites>
<testsuite name="default">
<directory>tests</directory>
</testsuite>
</testsuites>
<coverage cacheDirectory=".phpunit.cache/code-coverage"
processUncoveredFiles="true">
<include>
<directory suffix=".php">source/includes</directory>
</include>
</coverage>
</phpunit>

View File

@ -1,11 +1,27 @@
### [Unreleased] ### [Unreleased]
#### Added #### Added
#### Changed #### Changed
- Add some spacing between event items
- Update dependencies
#### Deprecated #### Deprecated
#### Removed #### Removed
#### Fixed #### Fixed
- Fix undefined variable $classNamePrefix for both error views
#### Security #### Security
### [1.0.0]
#### Added
- Display name of group when it cannot be found
#### Changed
- Let backend do requests to API of Mobilizon instance for increased privacy
- Update dependencies
#### Fixed
- Fix displaying more than one block in the editor
### [0.11.5]
#### Changed
- Confirm compatibility with WordPress 6.5
### [0.11.4] ### [0.11.4]
#### Changed #### Changed
- Confirm compatibility with WordPress 6.4 - Confirm compatibility with WordPress 6.4

View File

@ -10,11 +10,18 @@
* License: <wordpress-license> * License: <wordpress-license>
*/ */
require_once __DIR__ . '/includes/constants.php'; require_once __DIR__ . '/includes/exceptions/GeneralException.php';
require_once __DIR__ . '/includes/settings.php'; require_once __DIR__ . '/includes/exceptions/GroupNotFoundException.php';
require_once __DIR__ . '/includes/events-list-block.php'; require_once __DIR__ . '/includes/Constants.php';
require_once __DIR__ . '/includes/events-list-shortcut.php'; require_once __DIR__ . '/includes/Api.php';
require_once __DIR__ . '/includes/events-list-widget.php'; require_once __DIR__ . '/includes/EventsCache.php';
require_once __DIR__ . '/includes/Settings.php';
require_once __DIR__ . '/includes/DateTimeWrapper.php';
require_once __DIR__ . '/includes/Formatter.php';
require_once __DIR__ . '/includes/GraphQlClient.php';
require_once __DIR__ . '/includes/EventsListBlock.php';
require_once __DIR__ . '/includes/EventsListShortcut.php';
require_once __DIR__ . '/includes/EventsListWidget.php';
// Exit if this file is called directly. // Exit if this file is called directly.
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
@ -24,11 +31,11 @@ if (!defined('ABSPATH')) {
final class Mobilizon_Connector { final class Mobilizon_Connector {
private function __construct() { private function __construct() {
add_action('init', [$this, 'register_api']);
add_action('init', [$this, 'register_blocks']); add_action('init', [$this, 'register_blocks']);
add_action('init', [$this, 'register_settings'], 1); // required for register_blocks add_action('init', [$this, 'register_settings'], 1); // required for register_blocks
add_action('init', [$this, 'register_shortcut']); add_action('init', [$this, 'register_shortcut']);
add_action('widgets_init', [$this, 'register_widget']); add_action('widgets_init', [$this, 'register_widget']);
add_action('wp_enqueue_scripts', [$this, 'register_scripts']);
register_activation_hook(__FILE__, [$this, 'enable_activation']); register_activation_hook(__FILE__, [$this, 'enable_activation']);
} }
@ -49,12 +56,15 @@ final class Mobilizon_Connector {
$settings = array( $settings = array(
'isShortOffsetNameShown' => MobilizonConnector\Settings::isShortOffsetNameShown(), 'isShortOffsetNameShown' => MobilizonConnector\Settings::isShortOffsetNameShown(),
'locale' => str_replace('_', '-', get_locale()), 'locale' => str_replace('_', '-', get_locale()),
'timeZone' => wp_timezone_string(), 'timeZone' => wp_timezone_string()
'url' => MobilizonConnector\Settings::getUrl()
); );
wp_add_inline_script($scriptName, 'var MOBILIZON_CONNECTOR = ' . json_encode($settings), 'before'); wp_add_inline_script($scriptName, 'var MOBILIZON_CONNECTOR = ' . json_encode($settings), 'before');
} }
public function register_api() {
MobilizonConnector\Api::init();
}
public function register_blocks() { public function register_blocks() {
$scriptName = MobilizonConnector\EventsListBlock::initAndReturnScriptName(); $scriptName = MobilizonConnector\EventsListBlock::initAndReturnScriptName();
$this->load_settings_globally_before_script($scriptName); $this->load_settings_globally_before_script($scriptName);
@ -64,12 +74,6 @@ final class Mobilizon_Connector {
MobilizonConnector\Settings::init(); MobilizonConnector\Settings::init();
} }
public function register_scripts() {
$name = MobilizonConnector\NAME . '-js';
wp_enqueue_script($name, plugins_url('front/events-loader.js', __FILE__ ));
$this->load_settings_globally_before_script($name);
}
public function register_shortcut() { public function register_shortcut() {
MobilizonConnector\EventsListShortcut::init(); MobilizonConnector\EventsListShortcut::init();
} }

View File

@ -1,75 +1,101 @@
/* eslint-disable @wordpress/i18n-ellipsis */ /* eslint-disable @wordpress/i18n-ellipsis */
import { loadEventList } from '../../events-loader.js' import {
clearEventsList,
displayErrorMessage,
displayEvents,
hideErrorMessages,
showLoadingIndicator,
} from '../../events-displayer.js'
const { InspectorControls, useBlockProps } = wp.blockEditor const { InspectorControls, useBlockProps } = wp.blockEditor
const { PanelBody } = wp.components const { Panel, PanelBody } = wp.components
const { useEffect } = wp.element const { useEffect } = wp.element
const { __ } = wp.i18n const { __ } = wp.i18n
const NAME = '<wordpress-name>' const NAME = '<wordpress-name>'
let timer
export default ({ attributes, setAttributes }) => { export default ({ attributes, setAttributes }) => {
let timer
const blockProps = useBlockProps({ const blockProps = useBlockProps({
className: NAME + '_events-list', className: NAME + '_events-list',
'data-maximum': attributes.eventsCount,
'data-group-name': attributes.groupName,
}) })
function reloadEventList() { function reloadEventList(eventsCount, groupName) {
if (timer) { if (timer) {
clearTimeout(timer) clearTimeout(timer)
} }
timer = setTimeout(() => { timer = setTimeout(async () => {
const container = document.getElementById(blockProps.id) const container = document.getElementById(blockProps.id)
if (container) { if (container) {
loadEventList(container) hideErrorMessages(container)
clearEventsList(container)
showLoadingIndicator(container)
let url = `/wp-json/connector-mobilizon/v1/events?eventsCount=${eventsCount}`
if (groupName) {
url += `&groupName=${groupName}`
}
await fetch(url)
.then((response) => response.text())
.then((data) => {
const events = JSON.parse(data)
displayEvents({
events,
document,
container,
maxEventsCount: eventsCount,
})
})
.catch((data) => {
displayErrorMessage({ data, container })
})
} }
}, 500) }, 500)
} }
useEffect(() => { useEffect(() => {
reloadEventList() reloadEventList(attributes.eventsCount, attributes.groupName)
}, []) }, [])
function updateEventsCount(event) { function updateEventsCount(event) {
let newValue = Number(event.target.value) let newValue = Number(event.target.value)
if (newValue < 1) newValue = 1 if (newValue < 1) newValue = 1
setAttributes({ eventsCount: newValue }) setAttributes({ eventsCount: newValue })
reloadEventList() reloadEventList(newValue, attributes.groupName)
} }
function updateGroupName(event) { function updateGroupName(event) {
setAttributes({ groupName: event.target.value }) const newValue = event.target.value
reloadEventList() setAttributes({ groupName: newValue })
reloadEventList(attributes.eventsCount, newValue)
} }
return [ return [
<InspectorControls> <InspectorControls>
<PanelBody title={__('Events List Settings', '<wordpress-name>')}> <Panel>
<label <PanelBody title={__('Events List Settings', '<wordpress-name>')}>
className="components-base-control__label" <label
htmlFor={NAME + '_events-count'} className="components-base-control__label"
> htmlFor={NAME + '_events-count'}
{__('Number of events to show', '<wordpress-name>')} >
</label> {__('Number of events to show', '<wordpress-name>')}
<input </label>
className="components-text-control__input" <input
type="number" className="components-text-control__input"
value={attributes.eventsCount} type="number"
onChange={updateEventsCount} value={attributes.eventsCount}
id={NAME + '_events-count'} onChange={updateEventsCount}
/> id={NAME + '_events-count'}
<label />
className="components-base-control__label" <label
htmlFor={NAME + '_group-name'} className="components-base-control__label"
> htmlFor={NAME + '_group-name'}
{__('Group name (optional)', '<wordpress-name>')} >
</label> {__('Group name (optional)', '<wordpress-name>')}
<input </label>
className="components-text-control__input" <input
type="text" className="components-text-control__input"
value={attributes.groupName} type="text"
onChange={updateGroupName} value={attributes.groupName}
id={NAME + '_group-name'} onChange={updateGroupName}
/> id={NAME + '_group-name'}
</PanelBody> />
</PanelBody>
</Panel>
</InspectorControls>, </InspectorControls>,
<div {...blockProps}> <div {...blockProps}>
<div className="general-error" style={{ display: 'none' }}> <div className="general-error" style={{ display: 'none' }}>

View File

@ -18,7 +18,6 @@ test.before(() => {
test.beforeEach((t) => { test.beforeEach((t) => {
t.context.container = document.createElement('div') t.context.container = document.createElement('div')
t.context.container.setAttribute('data-maximum', '2')
const errorMessageGeneral = document.createElement('div') const errorMessageGeneral = document.createElement('div')
errorMessageGeneral.setAttribute('class', 'general-error') errorMessageGeneral.setAttribute('class', 'general-error')
@ -40,24 +39,20 @@ test.beforeEach((t) => {
}) })
test('#displayEvents one event', (t) => { test('#displayEvents one event', (t) => {
const data = { const events = [
events: { {
elements: [ title: 'a',
{ url: 'b',
title: 'a', beginsOn: '2021-04-15T10:30:00Z',
url: 'b', endsOn: '2021-04-15T15:30:00Z',
beginsOn: '2021-04-15T10:30:00Z', physicalAddress: {
endsOn: '2021-04-15T15:30:00Z', description: 'c',
physicalAddress: { locality: 'd',
description: 'c', },
locality: 'd',
},
},
],
}, },
} ]
const container = t.context.container const container = t.context.container
displayEvents({ data, document, container }) displayEvents({ events, document, container, maxEventsCount: 2 })
const list = container.querySelector('ul') const list = container.querySelector('ul')
t.is(list.children[0].childNodes[0].tagName, 'A') t.is(list.children[0].childNodes[0].tagName, 'A')
t.is(list.children[0].childNodes[0].getAttribute('href'), 'b') t.is(list.children[0].childNodes[0].getAttribute('href'), 'b')

View File

@ -6,18 +6,14 @@ export function clearEventsList(container) {
list.replaceChildren() list.replaceChildren()
} }
export function displayEvents({ data, document, container }) { export function displayEvents({ events, document, container, maxEventsCount }) {
hideLoadingIndicator(container) hideLoadingIndicator(container)
const isShortOffsetNameShown = const isShortOffsetNameShown =
window.MOBILIZON_CONNECTOR.isShortOffsetNameShown window.MOBILIZON_CONNECTOR.isShortOffsetNameShown
const locale = window.MOBILIZON_CONNECTOR.locale const locale = window.MOBILIZON_CONNECTOR.locale
const maxEventsCount = container.getAttribute('data-maximum')
const timeZone = window.MOBILIZON_CONNECTOR.timeZone const timeZone = window.MOBILIZON_CONNECTOR.timeZone
const events = data.events
? data.events.elements
: data.group.organizedEvents.elements
const eventsCount = Math.min(maxEventsCount, events.length) const eventsCount = Math.min(maxEventsCount, events.length)
const list = container.querySelector('ul') const list = container.querySelector('ul')
for (let i = 0; i < eventsCount; i++) { for (let i = 0; i < eventsCount; i++) {

View File

@ -1,38 +0,0 @@
import {
clearEventsList,
displayEvents,
displayErrorMessage,
hideErrorMessages,
showLoadingIndicator,
} from './events-displayer.js'
import * as GraphqlWrapper from './graphql-wrapper.js'
const NAME = '<wordpress-name>'
const URL_SUFFIX = '/api'
document.addEventListener('DOMContentLoaded', loadEventLists)
function loadEventLists() {
const eventLists = document.getElementsByClassName(NAME + '_events-list')
for (const list of eventLists) {
loadEventList(list)
}
}
export function loadEventList(container) {
const url = MOBILIZON_CONNECTOR.url + URL_SUFFIX
const limit = parseInt(container.getAttribute('data-maximum'))
const groupName = container.getAttribute('data-group-name')
hideErrorMessages(container)
clearEventsList(container)
showLoadingIndicator(container)
if (groupName) {
GraphqlWrapper.getUpcomingEventsByGroupName({ url, limit, groupName })
.then((data) => displayEvents({ data, document, container }))
.catch((data) => displayErrorMessage({ data, container }))
} else {
GraphqlWrapper.getUpcomingEvents({ url, limit })
.then((data) => displayEvents({ data, document, container }))
.catch((data) => displayErrorMessage({ data, container }))
}
}

View File

@ -1,78 +0,0 @@
import SessionCache from './session-cache.js'
import { request } from 'graphql-request'
import DateTimeWrapper from './date-time-wrapper.js'
export function getUpcomingEvents({ url, limit }) {
const query = `
query ($limit: Int) {
events(limit: $limit) {
elements {
id,
title,
url,
beginsOn,
endsOn,
physicalAddress {
description,
locality
}
},
total
}
}
`
const dataInCache = SessionCache.get(sessionStorage, {
url,
query,
variables: { limit },
})
if (dataInCache !== null) {
return Promise.resolve(dataInCache)
}
return request(url, query, { limit }).then((data) => {
SessionCache.add(sessionStorage, { url, query, variables: { limit } }, data)
return Promise.resolve(data)
})
}
export function getUpcomingEventsByGroupName({ url, limit, groupName }) {
const query = `
query ($afterDatetime: DateTime, $groupName: String!, $limit: Int) {
group(preferredUsername: $groupName) {
organizedEvents(afterDatetime: $afterDatetime, limit: $limit) {
elements {
id,
title,
url,
beginsOn,
endsOn,
physicalAddress {
description,
locality
}
},
total
}
}
}
`
const afterDatetime = DateTimeWrapper.getCurrentDatetimeAsString()
const dataInCache = SessionCache.get(sessionStorage, {
url,
query,
variables: { afterDatetime, groupName, limit },
})
if (dataInCache !== null) {
return Promise.resolve(dataInCache)
}
return request(url, query, { afterDatetime, groupName, limit }).then(
(data) => {
SessionCache.add(
sessionStorage,
{ url, query, variables: { afterDatetime, groupName, limit } },
data,
)
return Promise.resolve(data)
},
)
}

View File

@ -1,6 +0,0 @@
import test from 'ava'
import hash from './object-hash-wrapper.js'
test('#hash object', (t) => {
t.is(hash({ foo: 'bar' }), 'a75c05bdca7d704bdfcd761913e5a4e4636e956b')
})

View File

@ -1,5 +0,0 @@
import objectHash from 'object-hash'
export default function hash(object) {
return objectHash(object)
}

View File

@ -1,33 +0,0 @@
import test from 'ava'
import SessionCache from './session-cache.js'
const fakeStorage = {
elements: {},
clear() {
this.elements = {}
},
getItem(key) {
const value = this.elements[key]
if (value === undefined) return null
return value
},
setItem(key, value) {
this.elements[key] = value
},
}
test.afterEach(() => {
fakeStorage.clear()
})
test('#add & #get', (t) => {
SessionCache.add(fakeStorage, { a: 'b' }, { c: 'd' })
t.deepEqual(SessionCache.get(fakeStorage, { a: 'b' }), { c: 'd' })
})
test('#get no entry', (t) => {
t.is(SessionCache.get(fakeStorage, { a: 'bb' }), null)
})

View File

@ -1,27 +0,0 @@
import hash from './object-hash-wrapper.js'
const MAX_AGE_IN_MS = 120000
export default class SessionCache {
static add(storage, parameters, data) {
const key = hash(parameters)
const timestamp = Date.now()
const value = {
data,
timestamp,
}
storage.setItem(key, JSON.stringify(value))
}
static get(storage, parameters) {
const key = hash(parameters)
const value = JSON.parse(storage.getItem(key))
if (
value &&
value.timestamp &&
value.timestamp > Date.now() - MAX_AGE_IN_MS
)
return value.data
return null
}
}

53
source/includes/Api.php Normal file
View File

@ -0,0 +1,53 @@
<?php
namespace MobilizonConnector;
class Api {
public static function init() {
add_action('rest_api_init', 'MobilizonConnector\Api::init_api');
}
public static function init_api() {
register_rest_route(
NAME . '/v1',
'/events',
[
'methods' => 'GET',
'callback' => 'MobilizonConnector\Api::get_events',
'args' => [
'eventsCount' => [
'required' => true,
'validate_callback' => function($param, $request, $key) {
return is_numeric($param) && $param > 0;
}
],
'groupName' => [
'validate_callback' => function($param, $request, $key) {
return !is_numeric($param);
}
]
],
'permission_callback' => '__return_true',
]
);
}
public static function get_events($request) {
$eventsCount = $request['eventsCount'];
$groupName = isset($request['groupName']) ? $request['groupName'] : '';
$url = Settings::getUrl();
try {
if ($groupName) {
$events = GraphQlClient::get_upcoming_events_by_group_name($url, (int) $eventsCount, $groupName);
} else {
$events = GraphQlClient::get_upcoming_events($url, (int) $eventsCount);
}
return $events;
} catch (GeneralException $e) {
return new \WP_Error('events_not_loading', 'The events could not be loaded!', array('status' => 500));
} catch (GroupNotFoundException $e) {
return new \WP_Error('group_not_found', sprintf('The group "%s" could not be found!', $groupName), array('status' => 404));
}
}
}

View File

@ -1,11 +1,6 @@
<?php <?php
namespace MobilizonConnector; namespace MobilizonConnector;
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
exit;
}
const DEFAULT_EVENTS_COUNT = 5; const DEFAULT_EVENTS_COUNT = 5;
const NAME = '<wordpress-name>'; const NAME = '<wordpress-name>';
const NICE_NAME = '<wordpress-nice-name>'; const NICE_NAME = '<wordpress-nice-name>';

View File

@ -0,0 +1,34 @@
<?php
namespace MobilizonConnector;
final class DateTimeWrapper {
private $dateTime;
private $locale;
private $timeZone;
public function __construct(string $text, string $locale = 'en-GB', string $timeZone = 'utc') {
if (!$locale) {
$locale = 'en-GB';
}
if (!$timeZone) {
$timeZone = 'utc';
}
$this->dateTime = new \DateTime($text);
$this->locale = $locale;
$this->timeZone = new \DateTimeZone($timeZone);
}
public function get24Time(): string {
$formatter = \IntlDateFormatter::create($this->locale, \IntlDateFormatter::NONE, \IntlDateFormatter::SHORT, $this->timeZone);
return $formatter->format($this->dateTime);
}
public function getShortDate(): string {
$formatter = \IntlDateFormatter::create($this->locale, \IntlDateFormatter::SHORT, \IntlDateFormatter::NONE, $this->timeZone);
return $formatter->format($this->dateTime);
}
public function getTimeZoneName(): string {
return $this->timeZone->getName();
}
}

View File

@ -0,0 +1,19 @@
<?php
namespace MobilizonConnector;
final class EventsCache {
private static $MAX_AGE_IN_S = 120;
public static function set(array $parameters, mixed $data): void {
// md5 is used as key must be 172 characters or fewer in length.
$key = md5(json_encode($parameters));
set_transient($key, $data, self::$MAX_AGE_IN_S);
}
public static function get(array $parameters): mixed {
$key = md5(json_encode($parameters));
$data = get_transient($key);
return $data;
}
}

View File

@ -1,11 +1,6 @@
<?php <?php
namespace MobilizonConnector; namespace MobilizonConnector;
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
exit;
}
class EventsListBlock { class EventsListBlock {
public static function initAndReturnScriptName(): string { public static function initAndReturnScriptName(): string {
@ -41,12 +36,29 @@ class EventsListBlock {
} }
public static function render($block_attributes, $content) { public static function render($block_attributes, $content) {
$classNamePrefix = NAME; $url = Settings::getUrl();
$eventsCount = $block_attributes['eventsCount']; $eventsCount = $block_attributes['eventsCount'];
$groupName = isset($block_attributes['groupName']) ? $block_attributes['groupName'] : ''; $groupName = isset($block_attributes['groupName']) ? $block_attributes['groupName'] : '';
$classNamePrefix = NAME;
ob_start(); ob_start();
require dirname(__DIR__) . '/view/events-list.php'; try {
if ($groupName) {
$events = GraphQlClient::get_upcoming_events_by_group_name($url, (int) $eventsCount, $groupName);
} else {
$events = GraphQlClient::get_upcoming_events($url, (int) $eventsCount);
}
$locale = get_locale();
$isShortOffsetNameShown = Settings::isShortOffsetNameShown();
$timeZone = wp_timezone_string();
require dirname(__DIR__) . '/view/events-list.php';
} catch (GeneralException $e) {
require dirname(__DIR__) . '/view/events-list-not-loaded.php';
} catch (GroupNotFoundException $e) {
require dirname(__DIR__) . '/view/events-list-group-not-found.php';
}
$output = ob_get_clean(); $output = ob_get_clean();
return $output; return $output;
} }

View File

@ -1,11 +1,6 @@
<?php <?php
namespace MobilizonConnector; namespace MobilizonConnector;
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
exit;
}
class EventsListShortcut { class EventsListShortcut {
public static function init() { public static function init() {
@ -24,12 +19,29 @@ class EventsListShortcut {
), $atts ), $atts
); );
$classNamePrefix = NAME; $url = Settings::getUrl();
$eventsCount = $atts_with_overriden_defaults['events-count']; $eventsCount = $atts_with_overriden_defaults['events-count'];
$groupName = $atts_with_overriden_defaults['group-name']; $groupName = $atts_with_overriden_defaults['group-name'];
$classNamePrefix = NAME;
ob_start(); ob_start();
require dirname(__DIR__) . '/view/events-list.php'; try {
if ($groupName) {
$events = GraphQlClient::get_upcoming_events_by_group_name($url, (int) $eventsCount, $groupName);
} else {
$events = GraphQlClient::get_upcoming_events($url, (int) $eventsCount);
}
$locale = get_locale();
$isShortOffsetNameShown = Settings::isShortOffsetNameShown();
$timeZone = wp_timezone_string();
require dirname(__DIR__) . '/view/events-list.php';
} catch (GeneralException $e) {
require dirname(__DIR__) . '/view/events-list-not-loaded.php';
} catch (GroupNotFoundException $e) {
require dirname(__DIR__) . '/view/events-list-group-not-found.php';
}
$output = ob_get_clean(); $output = ob_get_clean();
return $output; return $output;
} }

View File

@ -1,11 +1,6 @@
<?php <?php
namespace MobilizonConnector; namespace MobilizonConnector;
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
exit;
}
class EventsListWidget extends \WP_Widget { class EventsListWidget extends \WP_Widget {
public function __construct() { public function __construct() {
@ -25,11 +20,28 @@ class EventsListWidget extends \WP_Widget {
echo $args['before_title'].apply_filters('widget_title', $options['title']).$args['after_title']; echo $args['before_title'].apply_filters('widget_title', $options['title']).$args['after_title'];
} }
$classNamePrefix = NAME; $url = Settings::getUrl();
$eventsCount = $options['eventsCount']; $eventsCount = $options['eventsCount'];
$groupName = isset($options['groupName']) ? $options['groupName'] : ''; $groupName = isset($options['groupName']) ? $options['groupName'] : '';
$classNamePrefix = NAME;
require dirname(__DIR__) . '/view/events-list.php'; try {
if ($groupName) {
$events = GraphQlClient::get_upcoming_events_by_group_name($url, (int) $eventsCount, $groupName);
} else {
$events = GraphQlClient::get_upcoming_events($url, (int) $eventsCount);
}
$locale = get_locale();
$isShortOffsetNameShown = Settings::isShortOffsetNameShown();
$timeZone = wp_timezone_string();
require dirname(__DIR__) . '/view/events-list.php';
} catch (GeneralException $e) {
require dirname(__DIR__) . '/view/events-list-not-loaded.php';
} catch (GroupNotFoundException $e) {
require dirname(__DIR__) . '/view/events-list-group-not-found.php';
}
echo $args['after_widget']; echo $args['after_widget'];
} }

View File

@ -0,0 +1,42 @@
<?php
namespace MobilizonConnector;
final class Formatter
{
public static function format_date(string $locale, string $timeZone, string $start, ?string $end, bool $isShortOffsetNameShown): string {
$startDateTime = new DateTimeWrapper($start, $locale, $timeZone);
$dateText = $startDateTime->getShortDate();
$dateText .= ' ' . $startDateTime->get24Time();
if (!$end && $isShortOffsetNameShown) {
$dateText .= ' (' . $startDateTime->getTimeZoneName() . ')';
}
if ($end) {
$endDateTime = new DateTimeWrapper($end, $locale, $timeZone);
if ($startDateTime->getShortDate() != $endDateTime->getShortDate()) {
$dateText .= ' - ';
$dateText .= $endDateTime->getShortDate() . ' ';
} else {
$dateText .= ' - ';
}
$dateText .= $endDateTime->get24Time();
if ($isShortOffsetNameShown) {
$dateText .= ' (' . $endDateTime->getTimeZoneName() . ')';
}
}
return $dateText;
}
public static function format_location(string $description, string $locality): string {
$location = '';
if ($description && trim($description)) {
$location .= trim($description);
}
if ($location && $locality) {
$location .= ', ';
}
if ($locality) {
$location .= $locality;
}
return $location;
}
}

View File

@ -0,0 +1,113 @@
<?php
namespace MobilizonConnector;
final class GraphQlClient {
public static function query(string $endpoint, string $query, array $variables = [], ?string $token = null): array
{
$headers = ['Content-Type: application/json'];
if ($token !== null) {
$headers[] = "Authorization: bearer $token";
}
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => $headers,
'content' => json_encode(['query' => $query, 'variables' => $variables]),
]
]);
$data = @file_get_contents($endpoint, false, $context);
if ($data === false) {
$error = error_get_last();
throw new \ErrorException($error['message'], $error['type']);
}
return json_decode($data, true);
}
public static function get_upcoming_events(string $url, int $limit): array {
$query = <<<'GRAPHQL'
query ($limit: Int) {
events(limit: $limit) {
elements {
id,
title,
url,
beginsOn,
endsOn,
physicalAddress {
description,
locality
}
},
total
}
}
GRAPHQL;
$cachedEvents = EventsCache::get(['url' => $url, 'query' => $query, 'limit' => $limit]);
if ($cachedEvents !== false) {
return $cachedEvents;
}
$endpoint = $url . '/api';
$data = self::query($endpoint, $query, ['limit' => $limit]);
self::checkData($data);
$events = $data['data']['events']['elements'];
EventsCache::set(['url' => $url, 'query' => $query, 'limit' => $limit], $events);
return $events;
}
public static function get_upcoming_events_by_group_name(string $url, int $limit, string $groupName): array {
$query = <<<'GRAPHQL'
query ($afterDatetime: DateTime, $groupName: String!, $limit: Int) {
group(preferredUsername: $groupName) {
organizedEvents(afterDatetime: $afterDatetime, limit: $limit) {
elements {
id,
title,
url,
beginsOn,
endsOn,
physicalAddress {
description,
locality
}
},
total
}
}
}
GRAPHQL;
$afterDatetime = date(\DateTime::ISO8601);
$cachedEvents = EventsCache::get(['url' => $url, 'query' => $query, 'afterDatetime' => $afterDatetime, 'groupName' => $groupName, 'limit' => $limit]);
if ($cachedEvents !== false) {
return $cachedEvents;
}
$endpoint = $url . '/api';
$data = self::query($endpoint, $query, ['afterDatetime' => $afterDatetime, 'groupName' => $groupName, 'limit' => $limit]);
self::checkData($data);
$events = $data['data']['group']['organizedEvents']['elements'];
EventsCache::set(['url' => $url, 'query' => $query, 'afterDatetime' => $afterDatetime, 'groupName' => $groupName, 'limit' => $limit], $events);
return $events;
}
private static function checkData($data) {
if (isset($data['errors'])) {
if (count($data['errors']) > 0 &&
isset($data['errors'][0]['code']) &&
$data['errors'][0]['code'] === 'group_not_found') {
throw new GroupNotFoundException(serialize($data['errors'][0]));
} else {
throw new GeneralException(serialize($data['errors']));
}
}
}
}

View File

@ -1,11 +1,6 @@
<?php <?php
namespace MobilizonConnector; namespace MobilizonConnector;
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
exit;
}
class Settings { class Settings {
private static $DEFAULT_OPTION_URL = 'https://mobilizon.fr'; private static $DEFAULT_OPTION_URL = 'https://mobilizon.fr';

View File

@ -0,0 +1,12 @@
<?php
namespace MobilizonConnector;
class GeneralException extends \Exception {
public function __construct($message, $code = 0, Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
public function __toString() {
return __CLASS__ . ": [{$this->code}]: {$this->message}\n";
}
}

View File

@ -0,0 +1,12 @@
<?php
namespace MobilizonConnector;
class GroupNotFoundException extends \Exception {
public function __construct($message, $code = 0, Throwable $previous = null) {
parent::__construct($message, $code, $previous);
}
public function __toString() {
return __CLASS__ . ": [{$this->code}]: {$this->message}\n";
}
}

View File

@ -16,13 +16,15 @@ License: <wordpress-license>
Features Features
- Display events as Gutenberg block, as widget and as shortcut - Display events as Gutenberg block, as widget and as shortcut
- Display events' title, date, and location, if available - Display events' title with link, date, and location, if available
- Cache requests' responses for 2 minutes in the browser's `sessionStorage` - Cache requests' responses for 2 minutes in the database
- Configure number of events to show per block, per widget and per shortcut - Configure number of events to show per block, per widget and per shortcut
- Optionally filter events by a specific group per block, per widget and per shortcut - Optionally filter events by a specific public group per block, per widget and per shortcut
- Set the URL of the Mobilizon instance in the settings - Set the URL of the Mobilizon instance in the settings
- Toggle adding named offset in brackets after the time in the settings - Toggle adding named offset in brackets after the time in the settings
This plugin requests the events via Mobilizon's GraphQL API.
The source code is available on [Github](https://github.com/dwaxweiler/connector-mobilizon). The source code is available on [Github](https://github.com/dwaxweiler/connector-mobilizon).
## Installation ## Installation
@ -41,6 +43,19 @@ You have to use their username, e.g. `@nosliensvivants`, and append the name of
## Changelog ## Changelog
### [1.0.0]
#### Added
- Display name of group when it cannot be found
#### Changed
- Let backend do requests to API of Mobilizon instance for increased privacy
- Update dependencies
#### Fixed
- Fix displaying more than one block in the editor
### [0.11.5]
#### Changed
- Confirm compatibility with WordPress 6.5
### [0.11.4] ### [0.11.4]
#### Changed #### Changed
- Confirm compatibility with WordPress 6.4 - Confirm compatibility with WordPress 6.4

View File

@ -1,6 +1,6 @@
<?php <?php
require_once __DIR__ . '/includes/constants.php'; require_once __DIR__ . '/includes/Constants.php';
require_once __DIR__ . '/includes/settings.php'; require_once __DIR__ . '/includes/Settings.php';
// If uninstall.php is not called by WordPress, exit. // If uninstall.php is not called by WordPress, exit.
if (!defined('WP_UNINSTALL_PLUGIN')) { if (!defined('WP_UNINSTALL_PLUGIN')) {

View File

@ -0,0 +1,11 @@
<?php
namespace MobilizonConnector;
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
exit;
}
?>
<div class="<?php echo esc_attr($classNamePrefix); ?>_events-list">
<?php echo esc_html(sprintf(__('The group "%s" could not be found!', 'connector-mobilizon'), $groupName)); ?>
</div>

View File

@ -0,0 +1,11 @@
<?php
namespace MobilizonConnector;
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
exit;
}
?>
<div class="<?php echo esc_attr($classNamePrefix); ?>_events-list">
<?php esc_html_e('The events could not be loaded!', 'connector-mobilizon'); ?>
</div>

View File

@ -1,4 +1,6 @@
<?php <?php
namespace MobilizonConnector;
// Exit if this file is called directly. // Exit if this file is called directly.
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;

View File

@ -1,14 +1,23 @@
<?php <?php
namespace MobilizonConnector;
// Exit if this file is called directly. // Exit if this file is called directly.
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;
} }
?> ?>
<div class="<?php echo esc_attr($classNamePrefix); ?>_events-list" <div class="<?php echo esc_attr($classNamePrefix); ?>_events-list">
data-maximum="<?php echo esc_attr($eventsCount); ?>" <ul style="list-style-type: none; padding-left: 0;">
data-group-name="<?php echo esc_attr($groupName); ?>"> <?php foreach($events as $event) { ?>
<div class="general-error" style="display: none;"><?php esc_html_e('The events could not be loaded!', 'connector-mobilizon'); ?></div> <li style="margin-top: 10px;">
<div class="group-not-found" style="display: none;"><?php esc_html_e('The group could not be found!', 'connector-mobilizon'); ?></div> <a href="<?php echo esc_attr($event['url']); ?>"><?php echo esc_html_e($event['title']); ?></a>
<div class="loading-indicator" style="display: none;"><?php esc_html_e('Loading...', 'connector-mobilizon'); ?></div> <br>
<ul style="list-style-type: none; padding-left: 0;"></ul> <?php echo esc_html_e(Formatter::format_date($locale, $timeZone, $event['beginsOn'], $event['endsOn'], $isShortOffsetNameShown)); ?>
<?php if (isset($event['physicalAddress'])) { ?>
<br>
<?php echo esc_html_e(Formatter::format_location($event['physicalAddress']['description'], $event['physicalAddress']['locality'])) ?>
<?php } ?>
</li>
<?php } ?>
</ul>
</div> </div>

View File

@ -1,4 +1,6 @@
<?php <?php
namespace MobilizonConnector;
// Exit if this file is called directly. // Exit if this file is called directly.
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;

View File

@ -1,4 +1,6 @@
<?php <?php
namespace MobilizonConnector;
// Exit if this file is called directly. // Exit if this file is called directly.
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;

View File

@ -1,4 +1,6 @@
<?php <?php
namespace MobilizonConnector;
// Exit if this file is called directly. // Exit if this file is called directly.
if (!defined('ABSPATH')) { if (!defined('ABSPATH')) {
exit; exit;

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
use MobilizonConnector\DateTimeWrapper;
use PHPUnit\Framework\TestCase;
final class DateTimeWrapperTest extends TestCase {
public function testCanGet24TimeForUsualTime(): void {
$d = new DateTimeWrapper('2020-12-24T16:45:00Z');
$this->assertSame('16:45', $d->get24Time());
}
public function testCanGetShortDateForUsualDate(): void {
$d = new DateTimeWrapper('2020-12-24T16:45:00Z');
$this->assertSame('24/12/2020', $d->getShortDate());
}
public function testCanGetShortDateForUsualDateWithLocaleWithUnderscore(): void {
$d = new DateTimeWrapper('2020-12-24T16:45:00Z');
$this->assertSame('24/12/2020', $d->getShortDate(), 'en_GB');
}
public function testCanGetShortDateForUsualDateWithTimezoneString(): void {
$d = new DateTimeWrapper('2020-12-24T16:45:00Z', 'en-GB', 'Europe/Rome');
$this->assertSame('24/12/2020', $d->getShortDate());
}
public function testCanGetShortDateForUsualDateWithNamedOffset(): void {
$d = new DateTimeWrapper('2020-12-24T16:45:00Z', 'en-GB', 'UTC');
$this->assertSame('24/12/2020', $d->getShortDate());
}
public function testCanGetShortDateForUsualDateWithOffset(): void {
$d = new DateTimeWrapper('2020-12-24T16:45:00Z', 'en-GB', '+02:00');
$this->assertSame('24/12/2020', $d->getShortDate());
}
public function testCanGetShortDateForUsualDateWithEmptyTimezone(): void {
$d = new DateTimeWrapper('2020-12-24T16:45:00Z', 'en-GB', '');
$this->assertSame('24/12/2020', $d->getShortDate());
}
public function testCanGetShortOffsetNameForUsualTime(): void {
$d = new DateTimeWrapper('2020-12-24T16:45:00Z');
$this->assertSame('UTC', $d->getTimeZoneName());
}
}

52
tests/FormatterTest.php Normal file
View File

@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
use MobilizonConnector\Formatter;
use PHPUnit\Framework\TestCase;
final class FormatterTest extends PHPUnit\Framework\TestCase
{
public function testCanDateFormatOneDate(): void {
$this->assertSame('15/04/2021 10:30 - 15:30', Formatter::format_date('en-GB', 'UTC', '2021-04-15T10:30:00Z', '2021-04-15T15:30:00Z', false));
}
public function testCanDateFormatOneDateWithOffset(): void {
$this->assertSame('15/04/2021 10:30 - 15:30 (UTC)', Formatter::format_date('en-GB', 'UTC', '2021-04-15T10:30:00Z', '2021-04-15T15:30:00Z', true));
}
public function testCanDateFormatOneDateWithTimeZoneOffset(): void {
$this->assertSame('15/04/2021 11:30 - 16:30', Formatter::format_date('en-GB', '+01:00', '2021-04-15T10:30:00Z', '2021-04-15T15:30:00Z', false));
}
public function testCanDateFormatTwoDates(): void {
$this->assertSame('15/04/2021 10:30 - 16/04/2021 15:30', Formatter::format_date('en-GB', 'UTC', '2021-04-15T10:30:00Z', '2021-04-16T15:30:00Z', false));
}
public function testCanDateFormatTwoDatesWithOffset(): void {
$this->assertSame('15/04/2021 10:30 - 16/04/2021 15:30 (UTC)', Formatter::format_date('en-GB', 'UTC', '2021-04-15T10:30:00Z', '2021-04-16T15:30:00Z', true));
}
public function testCanDateFormatWhenSecondDateIsNull(): void {
$this->assertSame('15/04/2021 10:30', Formatter::format_date('en-GB', 'UTC', '2021-04-15T10:30:00Z', null, false));
}
public function testCanDateFormatWhenSecondDateIsNullWithOffset(): void {
$this->assertSame('15/04/2021 10:30 (UTC)', Formatter::format_date('en-GB', 'UTC', '2021-04-15T10:30:00Z', null, true));
}
public function testCanLocationFormatBothParameters(): void {
$this->assertSame('a, b', Formatter::format_location('a', 'b'));
}
public function testLocationFormatDescriptionOnly(): void {
$this->assertSame('a', Formatter::format_location('a', ''));
}
public function testLocationFormatDescriptionWithSpaceOnly(): void {
$this->assertSame('', Formatter::format_location(' ', ''));
}
public function testLocationFormatLocalityOnly(): void {
$this->assertSame('a', Formatter::format_location('', 'a'));
}
}

View File

@ -8,7 +8,6 @@ const FOLDER_SOURCE = './source'
module.exports = { module.exports = {
entry: { entry: {
'block-events-loader': FOLDER_SOURCE + '/front/block-events-loader.js', 'block-events-loader': FOLDER_SOURCE + '/front/block-events-loader.js',
'events-loader': FOLDER_SOURCE + '/front/events-loader.js',
}, },
output: { output: {
filename: '[name].js', filename: '[name].js',