1
0
mirror of https://github.com/dwaxweiler/connector-mobilizon synced 2025-06-05 21:59:25 +02:00

70 Commits

Author SHA1 Message Date
e21c86f2e4 release version 1.3.0 2024-11-10 09:52:09 +01:00
5bcc03f9d1 handle location being null 2024-11-10 09:46:26 +01:00
460ea7894d update dependencies 2024-11-10 09:38:57 +01:00
8caceeaf76 add missing changelog entries 2024-11-10 09:12:09 +01:00
0c40efd565 load block script only in footer 2024-11-09 22:32:53 +01:00
e91ca97e88 add version to wp_register_script() 2024-11-09 11:20:34 +01:00
df2feceaaa use escape-only function for event-related data 2024-11-09 10:01:50 +01:00
bcd1f5247d confirm compatibility with WordPress 6.7 2024-11-09 09:54:47 +01:00
e2d34032cd add comment for translators 2024-11-08 23:17:19 +01:00
9708203f3c prepare next release 2024-08-04 19:01:13 +02:00
370e0d9e46 release version 1.2.0 2024-08-04 19:00:14 +02:00
9412b9cb90 add missing changelog entry 2024-08-04 18:54:48 +02:00
72045a31b0 update dependencies 2024-08-04 18:54:13 +02:00
0edad986d3 display event picture (#23) 2024-08-04 18:31:29 +02:00
a543a25a8a prepare next release 2024-07-18 19:20:22 +02:00
f0e955aa47 release version 1.1.0 2024-07-18 19:19:17 +02:00
80abd9a461 npm audit fix 2024-07-18 19:04:55 +02:00
fb1db8e836 update dependencies 2024-07-18 19:03:41 +02:00
db080657db confirm compatibility with WordPress 6.6 2024-07-18 18:51:47 +02:00
979ecbc91f upgrade gulp to version 5 2024-05-16 08:57:35 +01:00
08e80615c6 add note about API to description 2024-05-15 08:34:06 +01:00
526d57d1b2 add missing changelog entry 2024-05-15 08:32:32 +01:00
718d66506b update some dev dependencies 2024-05-15 08:31:06 +01:00
cccd1a78b5 add some spacing between event items 2024-05-14 10:30:42 +02:00
fa99821ffc fix undefined variable for both error views 2024-05-14 10:28:39 +02:00
7bc35a3923 mention use of GraphQL API 2024-05-13 14:23:37 +02:00
82800a9db3 clarify two steps in release procedure 2024-04-06 08:27:24 +02:00
46170377af prepare next release 2024-04-06 08:25:16 +02:00
5c1a186456 release version 1.0.0 2024-04-06 08:22:26 +02:00
7a0075e5ed clarify features 2024-04-06 07:50:47 +02:00
392d552dab npm audit fix 2024-04-06 07:42:42 +02:00
797a08cfb6 update dev dependencies 2024-04-06 07:41:15 +02:00
c5279a37f3 remove unused code and dependencies 2024-04-05 23:44:53 +02:00
99e58d0382 update dependencies 2024-04-05 23:39:30 +02:00
dc1949ba4c move requests to backend (#18) 2024-04-05 23:26:11 +02:00
3269ccca1a add missing readme changelog entry 2024-04-02 22:13:20 +02:00
2d91350cc9 prepare next release 2024-04-02 22:09:58 +02:00
e7fd47a346 release version 0.11.5 2024-04-02 22:09:20 +02:00
8d4e81878e confirm compatibility with WordPress 6.5 2024-04-02 22:02:55 +02:00
418dc829d0 document after-release step 2024-04-02 22:01:53 +02:00
a75d3a3915 prepare next release 2023-11-11 20:43:50 +01:00
d2fb67b5bc release version 0.11.4 2023-11-11 20:39:06 +01:00
a9f5205d78 remove Qodana Github action 2023-11-11 20:31:50 +01:00
25c76e4998 increase Node version number in Github action 2023-11-11 20:30:22 +01:00
6157cf3988 update dependencies 2023-11-11 20:28:16 +01:00
8644a7103c confirm compatibility with WordPress 6.4 2023-11-11 20:24:59 +01:00
4fd516bcf8 release version 0.11.3 2023-07-25 22:25:55 +02:00
238fdcf261 release version 0.11.2 2023-07-25 22:14:24 +02:00
eaf1ffa0c8 confirm compatibility with WordPress 6.3 2023-07-25 20:17:28 +02:00
48477a158e add missing changelog entry 2023-07-25 20:16:49 +02:00
9b4778b494 npm audit fix 2023-07-25 20:04:04 +02:00
60363d8a5c update dependencies 2023-07-25 20:03:31 +02:00
51ae9b9616 update dependencies 2023-05-07 18:00:02 +02:00
637142d00d release version 0.11.1 2023-05-07 17:46:27 +02:00
163d0d27a6 revert minimum PHP version to 7.4 to allow some more time for upgrading PHP 2023-05-07 17:41:36 +02:00
d0ada74642 release version 0.11.0 2023-03-29 23:00:36 +02:00
b242c33733 remove bootstrap from qodana.yaml 2023-03-29 22:47:59 +02:00
cac233ec45 set minimum PHP version to 8.0 2023-03-29 22:32:29 +02:00
9e394ac837 confirm compatibility with WP 6.2 2023-03-29 22:30:45 +02:00
b2811ad169 update dependencies 2023-03-29 22:26:44 +02:00
40f351efbf prefer online 2023-02-16 08:10:10 +01:00
ad0a9f77f8 ignore scripts too 2023-02-16 08:04:32 +01:00
76a2217582 use npm ci in qodana instead 2023-02-16 08:00:01 +01:00
742b16808c add qodana.yaml 2023-02-16 07:57:10 +01:00
41419e6550 Merge pull request #14 from wordpress-connector-for-mobilizon/update-deps
Update dependencies
2023-02-12 18:13:49 +01:00
ab7e7274d0 fix versions 2023-02-12 18:11:24 +01:00
b7742d3803 npm audit fix 2023-02-12 18:05:30 +01:00
e5dc313f6c update luxon 2023-02-12 18:02:33 +01:00
3b4c53da81 update dev deps 2023-02-12 17:56:48 +01:00
87434fc1c1 add qodana 2023-02-12 17:20:20 +01:00
48 changed files with 12053 additions and 10955 deletions

View File

@ -15,7 +15,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node-version: [16.x] node-version: [18.x]
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/ # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
steps: steps:

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 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,12 +11,11 @@ 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: './' },
) )
.pipe(replace('<wordpress-author-name>', PACKAGE.author.name)) .pipe(replace('<wordpress-author-name>', PACKAGE.author.name))
.pipe(replace('<wordpress-author-url>', PACKAGE.author.url)) .pipe(replace('<wordpress-author-url>', PACKAGE.author.url))
@ -26,22 +25,22 @@ function injectMetadata() {
.pipe( .pipe(
replace( replace(
'<wordpress-minimum-version>', '<wordpress-minimum-version>',
PACKAGE.additionalDetails.wordpressMinimumVersion PACKAGE.additionalDetails.wordpressMinimumVersion,
) ),
) )
.pipe(replace('<wordpress-name>', PACKAGE.name)) .pipe(replace('<wordpress-name>', PACKAGE.name))
.pipe(replace('<wordpress-nice-name>', PACKAGE.additionalDetails.niceName)) .pipe(replace('<wordpress-nice-name>', PACKAGE.additionalDetails.niceName))
.pipe( .pipe(
replace( replace(
'<wordpress-php-minimum-version>', '<wordpress-php-minimum-version>',
PACKAGE.additionalDetails.phpMinimumVersion PACKAGE.additionalDetails.phpMinimumVersion,
) ),
) )
.pipe( .pipe(
replace( replace(
'<wordpress-tested-up-to-version>', '<wordpress-tested-up-to-version>',
PACKAGE.additionalDetails.wordpressTestedUpToVersion PACKAGE.additionalDetails.wordpressTestedUpToVersion,
) ),
) )
.pipe(replace('<wordpress-version>', PACKAGE.version)) .pipe(replace('<wordpress-version>', PACKAGE.version))
.pipe(dest('.')) .pipe(dest('.'))

19968
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,18 +1,18 @@
{ {
"name": "connector-mobilizon", "name": "connector-mobilizon",
"version": "0.10.1", "version": "1.3.0",
"description": "Display Mobilizon events in WordPress.", "description": "Display Mobilizon events in WordPress.",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"build-dev": "webpack --mode=development && gulp inject", "build-dev": "webpack --mode=development && gulp inject",
"build-prod": "ava && webpack --mode=production && gulp inject", "build-prod": "rimraf -- build && ava && webpack --mode=production && gulp inject",
"clean": "rimraf -- build", "clean": "rimraf -- build",
"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",
@ -24,35 +24,33 @@
}, },
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"graphql": "^16.6.0", "graphql": "16.9.0",
"graphql-request": "^5.0.0", "luxon": "3.5.0"
"luxon": "3.0.4",
"object-hash": "3.0.0"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.19.6", "@babel/core": "7.26.0",
"@babel/eslint-parser": "7.19.1", "@babel/eslint-parser": "7.25.9",
"@babel/preset-env": "7.19.4", "@babel/preset-env": "7.26.0",
"@babel/preset-react": "7.18.6", "@babel/preset-react": "7.25.9",
"@wordpress/eslint-plugin": "13.4.0", "@wordpress/eslint-plugin": "21.4.0",
"ava": "5.0.1", "ava": "6.2.0",
"babel-loader": "8.2.5", "babel-loader": "9.2.1",
"browser-env": "3.3.0", "browser-env": "3.3.0",
"c8": "7.12.0", "c8": "10.1.2",
"copy-webpack-plugin": "11.0.0", "copy-webpack-plugin": "12.0.2",
"eslint": "8.26.0", "eslint": "8.57.0",
"eslint-plugin-ava": "13.2.0", "eslint-plugin-ava": "14.0.0",
"eslint-plugin-jsx": "0.1.0", "eslint-plugin-jsx": "0.1.0",
"eslint-plugin-react": "7.31.10", "eslint-plugin-react": "7.37.2",
"esm": "3.2.25", "esm": "3.2.25",
"gulp": "4.0.2", "gulp": "5.0.0",
"gulp-replace": "1.1.3", "gulp-replace": "1.1.4",
"husky": "8.0.1", "husky": "9.1.6",
"lint-staged": "13.0.3", "lint-staged": "15.2.10",
"prettier": "2.7.1", "prettier": "3.3.3",
"rimraf": "3.0.2", "rimraf": "5.0.10",
"webpack": "5.74.0", "webpack": "5.96.1",
"webpack-cli": "4.10.0" "webpack-cli": "5.1.4"
}, },
"ava": { "ava": {
"files": [ "files": [
@ -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.1" "wordpressTestedUpToVersion": "6.7"
}, },
"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>

2
qodana.yaml Normal file
View File

@ -0,0 +1,2 @@
version: '1.0'
linter: jetbrains/qodana-js:2022.3-eap

View File

@ -6,6 +6,71 @@
#### Fixed #### Fixed
#### Security #### Security
### [1.3.0]
#### Added
- Comment for translators what placeholder will contain
#### Changed
- Confirm compatibility with WordPress 6.7
- Load block script only in footer to reduce waiting time
- Update dependencies
#### Fixed
- Mark event-related data as non-translatable within plugin
- Add version number to script registration to break browser caching
- Handle location being null
### [1.2.0]
#### Added
- Display event picture if available
#### Changed
- Update dependencies
### [1.1.0]
#### Added
- Add some spacing between event items
#### Changed
- Update dependencies
- Confirm compatibility with WordPress 6.6
#### Fixed
- Fix undefined variable $classNamePrefix for both error views
### [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]
#### Changed
- Confirm compatibility with WordPress 6.4
- Update dependencies
### [0.11.3]
#### Fixed
- Clean up distributed files
### [0.11.2]
#### Changed
- Update dependencies
- Confirm compatibility with WordPress 6.3
### [0.11.1]
#### Fixed
- Revert minimum PHP version to 7.4 to allow some more time for upgrading PHP
### [0.11.0]
#### Changed
- Update dependencies
- Confirm compatibility with WordPress 6.2
#### Security
- Set minimum PHP version to oldest stable 8.0
### [0.10.1] ### [0.10.1]
#### Changed #### Changed
- Confirm compatibility with WordPress 6.1 - Confirm compatibility with WordPress 6.1

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,47 +1,72 @@
/* 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>
<Panel>
<PanelBody title={__('Events List Settings', '<wordpress-name>')}> <PanelBody title={__('Events List Settings', '<wordpress-name>')}>
<label <label
className="components-base-control__label" className="components-base-control__label"
@ -70,6 +95,7 @@ export default ({ attributes, setAttributes }) => {
id={NAME + '_group-name'} 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,9 +39,7 @@ test.beforeEach((t) => {
}) })
test('#displayEvents one event', (t) => { test('#displayEvents one event', (t) => {
const data = { const events = [
events: {
elements: [
{ {
title: 'a', title: 'a',
url: 'b', url: 'b',
@ -53,11 +50,9 @@ test('#displayEvents one event', (t) => {
locality: 'd', 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

@ -1,27 +1,36 @@
import Formatter from './formatter.js' import Formatter from './formatter.js'
import { createAnchorElement } from './html-creator.js' import { createAnchorElement, createImageElement } from './html-creator.js'
export function clearEventsList(container) { export function clearEventsList(container) {
const list = container.querySelector('ul') const list = container.querySelector('ul')
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++) {
const li = document.createElement('li') const li = document.createElement('li')
li.style.lineHeight = '150%'
li.style.marginTop = '20px'
if (events[i].picture) {
const img = createImageElement({
document,
alt: events[i].picture.alt ? events[i].picture.alt : '',
src: events[i].picture.base64 ? events[i].picture.base64 : '',
})
img.style.display = 'block'
img.style.maxWidth = '100%'
li.appendChild(img)
}
const a = createAnchorElement({ const a = createAnchorElement({
document, document,

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

@ -5,3 +5,10 @@ export function createAnchorElement({ document, text, url }) {
a.innerHTML = text a.innerHTML = text
return a return a
} }
export function createImageElement({ document, alt, src }) {
const img = document.createElement('img')
img.setAttribute('alt', alt)
img.setAttribute('src', src)
return img
}

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 {
@ -15,7 +10,7 @@ class EventsListBlock {
'wp-blocks', 'wp-blocks',
'wp-components', 'wp-components',
'wp-i18n' 'wp-i18n'
]); ], '<wordpress-version>', array('in_footer' => true));
register_block_type(NAME . '/events-list', [ register_block_type(NAME . '/events-list', [
'api_version' => 2, 'api_version' => 2,
'title' => __('Events List', 'connector-mobilizon'), 'title' => __('Events List', 'connector-mobilizon'),
@ -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();
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'; 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();
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'; 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;
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'; 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,170 @@
<?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
},
picture {
alt,
contentType,
url
}
},
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'];
foreach ($events as &$event) {
if ($event['picture']) {
$picture_response = self::download_image($event['picture']['url']);
if ($picture_response !== false) {
$picture_encoded = 'data:' . $event['picture']['contentType'] . ';base64,' . base64_encode($picture_response);
$event['picture']['base64'] = $picture_encoded;
}
}
unset($event);
}
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
},
picture {
alt,
contentType,
url
}
},
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'];
foreach ($events as &$event) {
if ($event['picture']) {
$picture_response = self::download_image($event['picture']['url']);
if ($picture_response !== false) {
$picture_encoded = 'data:' . $event['picture']['contentType'] . ';base64,' . base64_encode($picture_response);
$event['picture']['base64'] = $picture_encoded;
}
}
unset($event);
}
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']));
}
}
}
private static function download_image($url) {
// Initialize curl handle
$ch = curl_init($url);
// Set curl options
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_TIMEOUT, 60); // Set timeout to 60 seconds (adjust as needed)
// Execute the request
$image_data = curl_exec($ch);
// Check for errors
if (curl_errno($ch)) {
print_r(curl_error($ch));
throw new \Error('Error: ' . curl_error($ch));
}
// Close curl handle
curl_close($ch);
return $image_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 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,71 @@ You have to use their username, e.g. `@nosliensvivants`, and append the name of
## Changelog ## Changelog
### [1.3.0]
#### Added
- Comment for translators what placeholder will contain
#### Changed
- Confirm compatibility with WordPress 6.7
- Load block script only in footer to reduce waiting time
- Update dependencies
#### Fixed
- Mark event-related data as non-translatable within plugin
- Add version number to script registration to break browser caching
- Handle location being null
### [1.2.0]
#### Added
- Display event picture if available
#### Changed
- Update dependencies
### [1.1.0]
#### Added
- Add some spacing between event items
#### Changed
- Update dependencies
- Confirm compatibility with WordPress 6.6
#### Fixed
- Fix undefined variable $classNamePrefix for both error views
### [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]
#### Changed
- Confirm compatibility with WordPress 6.4
- Update dependencies
### [0.11.3]
#### Fixed
- Clean up distributed files
### [0.11.2]
#### Changed
- Update dependencies
- Confirm compatibility with WordPress 6.3
### [0.11.1]
#### Fixed
- Revert minimum PHP version to 7.4 to allow some more time for upgrading PHP
### [0.11.0]
#### Changed
- Update dependencies
- Confirm compatibility with WordPress 6.2
#### Security
- Set minimum PHP version to oldest stable 8.0
### [0.10.1] ### [0.10.1]
#### Changed #### Changed
- Confirm compatibility with WordPress 6.1 - Confirm compatibility with WordPress 6.1

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,14 @@
<?php
namespace MobilizonConnector;
// Exit if this file is called directly.
if (!defined('ABSPATH')) {
exit;
}
?>
<div class="<?php echo esc_attr($classNamePrefix); ?>_events-list">
<?php
/* translators: %s is replaced with the name of the group. */
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,26 @@
<?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="line-height: 150%; margin-top: 20px;">
<div class="group-not-found" style="display: none;"><?php esc_html_e('The group could not be found!', 'connector-mobilizon'); ?></div> <?php if (isset($event['picture'])) { ?>
<div class="loading-indicator" style="display: none;"><?php esc_html_e('Loading...', 'connector-mobilizon'); ?></div> <img alt="<?php echo esc_attr($event['picture']['alt']); ?>" src="<?php echo esc_attr($event['picture']['base64']); ?>" style="display: block; max-width: 100%;">
<ul style="list-style-type: none; padding-left: 0;"></ul> <?php } ?>
<a href="<?php echo esc_attr($event['url']); ?>"><?php echo esc_html($event['title']); ?></a>
<br>
<?php echo esc_html(Formatter::format_date($locale, $timeZone, $event['beginsOn'], $event['endsOn'], $isShortOffsetNameShown)); ?>
<?php if (isset($event['physicalAddress'])) { ?>
<br>
<?php echo esc_html(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());
}
}

56
tests/FormatterTest.php Normal file
View File

@ -0,0 +1,56 @@
<?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 testLocationFormatDescriptionOnlyWithNull(): void {
$this->assertSame('a', Formatter::format_location('a', null));
}
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',