+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 5249ea96..b13e3a64 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,11 @@ If you're a developer looking to leverage this for a church, you're welcome to b
[Small Groups Example.](https://www.tenth.org/smallgroups)
[Classes Example.](https://www.tenth.org/abs)
+### Event Calendar
+Automatically sync meetings from TouchPoint to your WordPress site, with details and images imported from their
+involvements.
+[Example.](https://www.tenth.org/events)
+
### Crazy-Simple RSVP interface
Let folks RSVP for an event for each member in their family in just a few clicks.
No login required, just an email address and zip code.
@@ -30,18 +35,23 @@ No login required, just an email address and zip code.
Show your Staff members, Elders, or other collections of people, automatically kept in sync with TouchPoint.
[Example.](https://www.tenth.org/about/staff) (This example and others like is are 100% updated from TouchPoint, including the titles and social links.)
+### Embedded Reports
+Any SQL or Python report generated in TouchPoint can be embedded into your website and automatically updated. For example,
+we have a financial update chart that is automatically updated to reflect giving.
+[Example (the bar graph on this page).](https://www.tenth.org/give)
+
### Outreach Partners
Automatically import partner bios and info from TouchPoint for display on your public website, with
appropriate care for their security.
[Example.](https://www.tenth.org/outreach/partners)
-### Events
-Improve display of events in the TouchPoint Custom Mobile App by providing content from [The Events Calendar Plugin by
-ModernTribe](https://theeventscalendar.com/). This is compatible with both the free and "Pro" versions.
-
### Authentication (Beta)
Authenticate TouchPoint users to WordPress, so you can know your website users.
+### Old App Calendar (Deprecated)
+Improve display of events in the TouchPoint Custom Mobile App by providing content from [The Events Calendar Plugin by
+Modern Tribe](https://theeventscalendar.com/). This is compatible with both the free and "Pro" versions.
+
## Costs & Considerations
**This plugin is FREE!** We developed this plugin for us, but want to share it with any other churches that would
@@ -61,12 +71,16 @@ If you're not sure whether WordPress is the right tool for you, feel free to get
relationships with several firms who could help with the setup and technical maintenance if you're interested. But,
it's probably not the right tool for every church.
+We do collect some basic usage data when you use this plugin, including the admin email address configured in WordPress,
+the site address, and the name of the site. We use this data to understand how the plugin is being used and to improve
+it. You can choose in the plugin settings whether to allow us to list your site publicly as a reference, including some
+basic anonymous statistics such as the number of involvements you have synced or the number of people who have RSVPed to
+meetings through the plugin.
+
## Future Features
- Authenticate
- Track viewership of webpages and web resources non-anonymously. (Know who attended your virtual worship service.)
- Sync WordPress Permissions with TouchPoint involvements or roles.
-- Events
- - Sync TouchPoint Meetings with events on your public web calendar.
- Small Groups
- Suggest demographically-targeted small groups.
- Integrated Directory
@@ -79,21 +93,20 @@ it's probably not the right tool for every church.
Making this work requires notable configuration of your TouchPoint database. We've scripted what we can, and the
remainder is in [the Installation Instructions](https://github.com/TenthPres/TouchPoint-WP/wiki/Installation).
-Some features require other plugins, which may have fees attached.
-
You will need a TouchPoint user account with API-level access. New TouchPoint databases do not have one by default.
If your church doesn't have one, open a support ticket with TouchPoint to create one, referencing this plugin.
If you're using the Authentication component, your WordPress site **MUST** use HTTPS with a valid certificate.
We don't promise support for old versions of WordPress or PHP. You will need to keep both up to date.
+[See currently-supported PHP versions here.](https://www.php.net/supported-versions.php)
## Multisite Support
At the moment, this plugin won't perform very well in a multisite environment. We're working on that, though, as we
-plan on moving our own infrastructure toward multisite soon. As currently planned, ALL sites in a multisite network
-will share ONE TouchPoint connection, and many (though not all) of the settings would be shared across the network. If
-you're interested in using this plugin in a multisite environment, [please get in touch](mailto:jkurtz@tenth.org).
+plan on moving our own infrastructure toward multisite eventually. As currently planned, ALL sites in a multisite
+network will share ONE TouchPoint connection, and many (though not all) of the settings would be shared across the
+network. If you're interested in using this plugin in a multisite environment, [please get in touch](mailto:jkurtz@tenth.org).
## Credit & Hat-Tips
diff --git a/TESTING.md b/TESTING.md
new file mode 100644
index 00000000..9c267e8c
--- /dev/null
+++ b/TESTING.md
@@ -0,0 +1,248 @@
+# Testing TouchPoint-WP
+
+This document describes how to run and write tests for the TouchPoint-WP plugin.
+
+## Test Infrastructure
+
+This project uses:
+- **PHPUnit 9.6** for testing framework
+- **Yoast PHPUnit Polyfills** for PHP 8.0+ compatibility
+- **WordPress filter/action system** implemented directly for integration testing
+
+The testing approach implements WordPress's filter and action system directly in the bootstrap file, allowing tests to verify real WordPress filter behavior without requiring a full WordPress installation.
+
+## Running Tests
+
+### Prerequisites
+
+- PHP 8.0 or higher
+- Composer
+
+### Installation
+
+First, install the development dependencies:
+
+```bash
+composer install
+```
+
+### Running the Test Suite
+
+To run all tests (unit and integration):
+
+```bash
+composer test
+```
+
+Or directly with PHPUnit:
+
+```bash
+./vendor/bin/phpunit
+```
+
+### Running Specific Tests
+
+To run only unit tests:
+
+```bash
+./vendor/bin/phpunit tests/Unit
+```
+
+To run only integration tests:
+
+```bash
+./vendor/bin/phpunit tests/Integration
+```
+
+To run tests in a specific file:
+
+```bash
+./vendor/bin/phpunit tests/Unit/GeoTest.php
+```
+
+To run a specific test method:
+
+```bash
+./vendor/bin/phpunit --filter test_distance_calculation
+```
+
+### Code Coverage
+
+To generate a code coverage report:
+
+```bash
+composer test-coverage
+```
+
+This will create an HTML coverage report in the `coverage/` directory. Open `coverage/index.html` in your browser to view the report.
+
+## Writing Tests
+
+### Test Structure
+
+Tests are organized in the `tests/` directory:
+
+- `tests/Unit/` - Unit tests for individual classes and methods
+- `tests/Integration/` - Integration tests that test WordPress filter/action integration
+- `tests/TestCase.php` - Base test case class
+- `tests/bootstrap.php` - Bootstrap file with WordPress filter/action implementations
+
+### Test Types
+
+#### Unit Tests
+
+Unit tests focus on testing individual methods and classes in isolation.
+
+Example unit test:
+
+```php
+myMethod();
+
+ $this->assertSame('expected', $result);
+ }
+}
+```
+
+#### Integration Tests
+
+Integration tests verify WordPress filter and action integration. The test environment provides real implementations of `add_filter`, `apply_filters`, and `remove_all_filters`.
+
+Example integration test:
+
+```php
+assertSame('#FF0000', $color);
+
+ // Clean up
+ remove_all_filters('tp_custom_color_function');
+ }
+}
+```
+
+### Testing WordPress Filters
+
+The bootstrap file implements WordPress's filter system:
+
+- `add_filter($hook, $callback, $priority, $accepted_args)` - Add a filter
+- `apply_filters($hook, $value, ...$args)` - Apply filters to a value
+- `remove_all_filters($hook, $priority)` - Remove all filters from a hook
+
+These functions work like WordPress's actual filter system, including:
+- Priority-based ordering
+- Multiple filters on the same hook
+- Passing multiple arguments to filters
+- Filter chaining (each filter receives the output of the previous)
+```
+
+### Creating a New Test
+
+1. Create a new test file in the appropriate directory:
+ - `tests/Unit/` for unit tests
+ - `tests/Integration/` for integration tests
+2. Extend the `tp\TouchPointWP\Tests\TestCase` class
+3. Add the `@covers` annotation to specify which class(es) you're testing
+4. Write test methods (must start with `test_` or use the `@test` annotation)
+5. Use Brain Monkey to mock WordPress functions as needed
+
+### Test Naming Conventions
+
+- Test files should be named `{ClassName}_Test.php`
+- Test methods should be named `test_{methodName}_{scenario}` (e.g., `test_distance_calculationSamePoint`)
+- Use descriptive names that explain what is being tested
+
+### Assertions
+
+PHPUnit provides many assertion methods. Common ones include:
+
+- `assertSame($expected, $actual)` - Strict equality check
+- `assertEquals($expected, $actual)` - Loose equality check
+- `assertTrue($condition)` - Check if condition is true
+- `assertFalse($condition)` - Check if condition is false
+- `assertNull($value)` - Check if value is null
+- `assertInstanceOf($class, $object)` - Check object type
+- `assertEqualsWithDelta($expected, $actual, $delta)` - Check numeric equality with tolerance
+
+See the [PHPUnit documentation](https://phpunit.readthedocs.io/) for more assertion methods.
+
+## Continuous Integration
+
+Tests are automatically run on every push and pull request via GitHub Actions. The test workflow:
+
+- Runs on PHP versions 8.0, 8.1, 8.2, and 8.3
+- Installs dependencies
+- Runs the complete test suite
+- Generates code coverage report (PHP 8.3 only)
+
+You can view the test results in the "Actions" tab of the GitHub repository.
+
+## Best Practices
+
+1. **Write isolated tests** - Each test should be independent and not rely on other tests
+2. **Test one thing per test** - Each test method should verify one specific behavior
+3. **Use descriptive test names** - Test names should clearly explain what is being tested
+4. **Mock external dependencies** - Use mocks or stubs for external services and WordPress functions
+5. **Keep tests fast** - Unit tests should run quickly; avoid unnecessary setup or external calls
+6. **Test edge cases** - Include tests for boundary conditions, error cases, and unusual inputs
+7. **Maintain tests** - Update tests when you change code; failing tests should be fixed, not removed
+
+## Troubleshooting
+
+### Tests fail with "undefined constant" errors
+
+Make sure all required constants are defined in `tests/bootstrap.php`.
+
+### Tests fail with "undefined function" errors
+
+WordPress functions may need to be mocked in `tests/bootstrap.php` or in individual test files.
+
+### Can't run tests
+
+Ensure you have installed dev dependencies:
+
+```bash
+composer install
+```
+
+If you've only installed production dependencies, add the dev dependencies:
+
+```bash
+composer install --dev
+```
diff --git a/assets/branding/icon-curcolor.svg b/assets/branding/icon-curcolor.svg
new file mode 100644
index 00000000..92713b4b
--- /dev/null
+++ b/assets/branding/icon-curcolor.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/assets/js/base-defer.js b/assets/js/base-defer.js
index f33625f2..68ab0f84 100644
--- a/assets/js/base-defer.js
+++ b/assets/js/base-defer.js
@@ -1,1534 +1,1533 @@
"use strict";
+(function() {
+
// noinspection JSUnresolvedVariable
-const { __, _x, _n, _nx, sprintf } = wp.i18n;
-
-function utilInit() {
- tpvm._utils.stringArrayToListString = function(strings) {
- let concat = strings.join(''),
- comma = ', ',
- and = ' & ',
- useOxford = false,
- last, str;
- if (concat.indexOf(', ') !== -1) {
- comma = '; ';
- useOxford = true;
- }
- if (concat.indexOf(' & ') !== -1) {
- and = ' ' + __('and', 'TouchPoint-WP') + ' ';
- useOxford = true;
- }
-
- last = strings.pop();
- str = strings.join(comma);
- if (strings.length > 0) {
- if (useOxford)
- str += comma.trim();
- str += and;
- }
- str += last;
- return str;
- }
+ const {__, sprintf} = wp.i18n;
+
+ function utilInit() {
+ tpvm._utils.stringArrayToListString = function (strings) {
+ let concat = strings.join(''),
+ comma = ', ',
+ and = ' & ',
+ useOxford = false,
+ last, str;
+ if (concat.indexOf(', ') !== -1) {
+ comma = '; ';
+ useOxford = true;
+ }
+ if (concat.indexOf(' & ') !== -1) {
+ and = ' ' + __('and', 'TouchPoint-WP') + ' ';
+ useOxford = true;
+ }
- /**
- *
- * @param {string} action The name of the action function, minus the word "action"
- * @param {object} object The object to which the action belongs.
- */
- tpvm._utils.registerAction = function(action, object) {
- if (typeof object[action + "Action"] === "function") {
- let sc = object.shortClass;
- if (typeof sc !== "string") {
- console.warn(`Action '${action}' cannot be registered because the short class name is missing.`)
- return;
+ last = strings.pop();
+ str = strings.join(comma);
+ if (strings.length > 0) {
+ if (useOxford)
+ str += comma.trim();
+ str += and;
}
- let actionLC = action.toLowerCase();
- if (!tpvm._actions.hasOwnProperty(actionLC)) {
- tpvm._actions[actionLC] = [];
+ str += last;
+ return str;
+ }
+
+ /**
+ *
+ * @param {string} action The name of the action function, minus the word "action"
+ * @param {object} object The object to which the action belongs.
+ */
+ tpvm._utils.registerAction = function (action, object) {
+ if (typeof object[action + "Action"] === "function") {
+ let sc = object.shortClass;
+ if (typeof sc !== "string") {
+ console.warn(`Action '${action}' cannot be registered because the short class name is missing.`)
+ return;
+ }
+ let actionLC = action.toLowerCase();
+ if (!tpvm._actions.hasOwnProperty(actionLC)) {
+ tpvm._actions[actionLC] = [];
+ }
+ tpvm._actions[actionLC].push({
+ action: () => object[action + "Action"](),
+ uid: sc + object.id
+ });
}
- tpvm._actions[actionLC].push({
- action: () => object[action + "Action"](),
- uid: sc + object.id
- });
}
- }
- tpvm._utils.applyHashForAction = function(action, object) {
- // Make sure a function exists
- if (typeof object[action + "Action"] !== "function") {
- return;
- }
+ tpvm._utils.applyHashForAction = function (action, object) {
+ // Make sure a function exists
+ if (typeof object[action + "Action"] !== "function") {
+ return;
+ }
- // Figure out the needed hash
- action = action.toLowerCase()
- if (tpvm._actions[action].length === 1) {
- window.location.hash = "tp-" + action;
- } else if (tpvm._actions[action].length > 1) {
- window.location.hash = "tp-" + action + "-" + object.shortClass + object.id;
+ // Figure out the needed hash
+ action = action.toLowerCase()
+ if (tpvm._actions[action].length === 1) {
+ window.location.hash = "tp-" + action;
+ } else if (tpvm._actions[action].length > 1) {
+ window.location.hash = "tp-" + action + "-" + object.shortClass + object.id;
+ }
}
- }
-
- tpvm._utils.clearHash = function() {
- window.location.hash = "";
- }
- /**
- *
- * @param {?string} limitToAction
- */
- tpvm._utils.handleHash = function(limitToAction = null) {
- if (window.location.hash.substring(1, 4) !== "tp-") {
- return;
+ tpvm._utils.clearHash = function () {
+ if (!!window.history) {
+ window.history.pushState("", "", `${window.location.pathname}${window.location.search}`)
+ } else {
+ window.location.hash = "";
+ }
}
- let [action, identifier] = window.location.hash.toLowerCase().substring(4).split('-', 2);
+ /**
+ *
+ * @param {?string} limitToAction
+ */
+ tpvm._utils.handleHash = function (limitToAction = null) {
+ if (window.location.hash.substring(1, 4) !== "tp-") {
+ return;
+ }
- if (tpvm._actions[action] === undefined || (limitToAction !== null && action !== limitToAction.toLowerCase())) {
- return;
- }
- if (tpvm._actions[action].length === 1 && identifier === undefined) {
- tpvm._actions[action][0].action();
- return;
- }
+ let [action, identifier] = window.location.hash.toLowerCase().substring(4).split('-', 2);
- let obj = tpvm._actions[action].find((t) => t.uid === identifier);
- if (obj !== undefined && typeof obj.action === "function") {
- obj.action();
- }
- }
- tpvm.addEventListener("load", tpvm._utils.handleHash);
+ if (tpvm._actions[action] === undefined || (limitToAction !== null && action !== limitToAction.toLowerCase())) {
+ return;
+ }
+ if (tpvm._actions[action].length === 1 && identifier === undefined) {
+ tpvm._actions[action][0].action();
+ return;
+ }
- tpvm._utils.defaultSwalClasses = function() {
- return {
- container: 'tp-swal-container'
+ let obj = tpvm._actions[action].find((t) => t.uid === identifier);
+ if (obj !== undefined && typeof obj.action === "function") {
+ obj.action();
+ }
}
- }
+ tpvm.addEventListener("load", tpvm._utils.handleHash);
- tpvm._utils.arrayAdd = function (a, b) {
- if (a.length !== b.length) {
- console.error("Array lengths do not match.");
- return;
- }
- for (const ai in a) {
- a[ai] += b[ai];
+ tpvm._utils.defaultSwalClasses = function () {
+ return {
+ container: 'tp-swal-container'
+ }
}
- return a;
- }
- tpvm._utils.averageColor = function (arr) {
- let components = [0, 0, 0, 0],
- useAlpha = false,
- denominator = 0;
- for (const ai in arr) {
- arr[ai] = arr[ai].replace(';', '').trim();
- if (typeof arr[ai] === "string" && arr[ai][0] === '#' && arr[ai].length === 4) { // #abc
- components = tpvm._utils.arrayAdd(components, [
- parseInt(arr[ai][1] + arr[ai][1], 16),
- parseInt(arr[ai][2] + arr[ai][2], 16),
- parseInt(arr[ai][3] + arr[ai][3], 16),
- 255]);
- denominator++;
- } else if (typeof arr[ai] === "string" && arr[ai][0] === '#' && arr[ai].length === 5) { // #abcd
- components = tpvm._utils.arrayAdd(components, [
- parseInt(arr[ai][1] + arr[ai][1], 16),
- parseInt(arr[ai][2] + arr[ai][2], 16),
- parseInt(arr[ai][3] + arr[ai][3], 16),
- parseInt(arr[ai][4] + arr[ai][4], 16)]);
- useAlpha = true;
- denominator++;
- } else if (typeof arr[ai] === "string" && arr[ai][0] === '#' && arr[ai].length === 7) { // #aabbcc
- components = tpvm._utils.arrayAdd(components, [
- parseInt(arr[ai][1] + arr[ai][2], 16),
- parseInt(arr[ai][3] + arr[ai][4], 16),
- parseInt(arr[ai][5] + arr[ai][6], 16),
- 255]);
- denominator++;
- } else if (typeof arr[ai] === "string" && arr[ai][0] === '#' && arr[ai].length === 9) { // #aabbccdd
- components = tpvm._utils.arrayAdd(components, [
- parseInt(arr[ai][1] + arr[ai][2], 16),
- parseInt(arr[ai][3] + arr[ai][4], 16),
- parseInt(arr[ai][5] + arr[ai][6], 16),
- parseInt(arr[ai][7] + arr[ai][8], 16)]);
- useAlpha = true;
- denominator++;
- } else {
- console.error("Can't calculate the color for " + arr[ai]);
+ tpvm._utils.arrayAdd = function (a, b) {
+ if (a.length !== b.length) {
+ console.error("Array lengths do not match.");
+ return;
}
+ for (const ai in a) {
+ a[ai] += b[ai];
+ }
+ return a;
+ }
+
+ tpvm._utils.averageColor = function (arr) {
+ let components = [0, 0, 0, 0],
+ useAlpha = false,
+ denominator = 0;
+ for (const ai in arr) {
+ arr[ai] = arr[ai].replace(';', '').trim();
+ if (typeof arr[ai] === "string" && arr[ai][0] === '#' && arr[ai].length === 4) { // #abc
+ components = tpvm._utils.arrayAdd(components, [
+ parseInt(arr[ai][1] + arr[ai][1], 16),
+ parseInt(arr[ai][2] + arr[ai][2], 16),
+ parseInt(arr[ai][3] + arr[ai][3], 16),
+ 255]);
+ denominator++;
+ } else if (typeof arr[ai] === "string" && arr[ai][0] === '#' && arr[ai].length === 5) { // #abcd
+ components = tpvm._utils.arrayAdd(components, [
+ parseInt(arr[ai][1] + arr[ai][1], 16),
+ parseInt(arr[ai][2] + arr[ai][2], 16),
+ parseInt(arr[ai][3] + arr[ai][3], 16),
+ parseInt(arr[ai][4] + arr[ai][4], 16)]);
+ useAlpha = true;
+ denominator++;
+ } else if (typeof arr[ai] === "string" && arr[ai][0] === '#' && arr[ai].length === 7) { // #aabbcc
+ components = tpvm._utils.arrayAdd(components, [
+ parseInt(arr[ai][1] + arr[ai][2], 16),
+ parseInt(arr[ai][3] + arr[ai][4], 16),
+ parseInt(arr[ai][5] + arr[ai][6], 16),
+ 255]);
+ denominator++;
+ } else if (typeof arr[ai] === "string" && arr[ai][0] === '#' && arr[ai].length === 9) { // #aabbccdd
+ components = tpvm._utils.arrayAdd(components, [
+ parseInt(arr[ai][1] + arr[ai][2], 16),
+ parseInt(arr[ai][3] + arr[ai][4], 16),
+ parseInt(arr[ai][5] + arr[ai][6], 16),
+ parseInt(arr[ai][7] + arr[ai][8], 16)]);
+ useAlpha = true;
+ denominator++;
+ } else {
+ console.error("Can't calculate the color for " + arr[ai]);
+ }
+ }
+ if (!useAlpha) {
+ components.pop();
+ }
+ for (const ci in components) {
+ components[ci] = Math.round(components[ci] / denominator).toString(16); // convert to hex
+ components[ci] = ("00" + components[ci]).slice(-2); // pad, just in case there's only one digit.
+ }
+ return "#" + components.join('');
}
- if (!useAlpha) {
- components.pop();
- }
- for (const ci in components) {
- components[ci] = Math.round(components[ci] / denominator).toString(16); // convert to hex
- components[ci] = ("00" + components[ci]).slice(-2); // pad, just in case there's only one digit.
+
+ tpvm._utils.ga = function (command, hitType, category, action, label = null, value = null) {
+ if (typeof ga === "function") {
+ ga(command, hitType, category, action, label, value);
+ }
+ if (typeof gtag === "function") {
+ gtag(hitType, action, {
+ 'event_category': category,
+ 'event_label': label,
+ 'value': value
+ });
+ }
}
- return "#" + components.join('');
}
- tpvm._utils.ga = function(command, hitType, category, action, label = null, value = null) {
- if (typeof ga === "function") {
- ga(command, hitType, category, action, label, value);
+ utilInit();
+
+ class TP_DataGeo {
+ static loc = {
+ "lat": null,
+ "lng": null,
+ "type": null,
+ "human": __('Loading...', 'TouchPoint-WP')
+ };
+
+ get shortClass() {
+ return "geo";
}
- if (typeof gtag === "function") {
- gtag(hitType, action, {
- 'event_category': category,
- 'event_label': label,
- 'value': value
- });
+
+ static init() {
+ tpvm.trigger('dataGeo_class_loaded');
}
- }
-}
-utilInit();
-
-class TP_DataGeo {
- static loc = {
- "lat": null,
- "lng": null,
- "type": null,
- "human": __('Loading...', 'TouchPoint-WP')
- };
-
- get shortClass() {
- return "geo";
- }
- static init() {
- tpvm.trigger('dataGeo_class_loaded');
- }
+ static geoByNavigator(then = null, error = null) {
+ navigator.geolocation.getCurrentPosition(geo, err);
- static geoByNavigator(then = null, error = null) {
- navigator.geolocation.getCurrentPosition(geo, err);
+ function geo(pos) {
+ TP_DataGeo.loc = {
+ "lat": pos.coords.latitude,
+ "lng": pos.coords.longitude,
+ "type": "nav",
+ "permission": null,
+ "human": __('Your Location', 'TouchPoint-WP')
+ }
- function geo(pos) {
- TP_DataGeo.loc = {
- "lat": pos.coords.latitude,
- "lng": pos.coords.longitude,
- "type": "nav",
- "permission": null,
- "human": __('Your Location', 'TouchPoint-WP')
- }
+ if (then !== null) {
+ then(TP_DataGeo.loc)
+ }
- if (then !== null) {
- then(TP_DataGeo.loc)
+ tpvm.trigger("dataGeo_located", TP_DataGeo.loc)
}
- tpvm.trigger("dataGeo_located", TP_DataGeo.loc)
- }
+ function err(e) {
+ let userFacingMessage;
- function err(e) {
- let userFacingMessage;
+ if (error !== null) {
+ error(e)
+ }
- if (error !== null) {
- error(e)
- }
+ console.error(e);
- console.error(e);
+ switch (e.code) {
+ case e.PERMISSION_DENIED:
+ userFacingMessage = __("User denied the request for Geolocation.", 'TouchPoint-WP');
+ break;
- switch(e.code) {
- case e.PERMISSION_DENIED:
- userFacingMessage = __("User denied the request for Geolocation.", 'TouchPoint-WP');
- break;
+ case e.POSITION_UNAVAILABLE:
+ userFacingMessage = __("Location information is unavailable.", 'TouchPoint-WP');
+ break;
- case e.POSITION_UNAVAILABLE:
- userFacingMessage = __("Location information is unavailable.", 'TouchPoint-WP');
- break;
+ case e.TIMEOUT:
+ userFacingMessage = __("The request to get user location timed out.", 'TouchPoint-WP');
+ break;
- case e.TIMEOUT:
- userFacingMessage = __("The request to get user location timed out.", 'TouchPoint-WP');
- break;
+ default:
+ userFacingMessage = __("An unknown error occurred.", 'TouchPoint-WP');
+ break;
+ }
- default:
- userFacingMessage = __("An unknown error occurred.", 'TouchPoint-WP');
- break;
+ tpvm.trigger("dataGeo_error", userFacingMessage)
}
-
- tpvm.trigger("dataGeo_error", userFacingMessage)
}
- }
- /**
- * Get the user's location.
- *
- * @param then function Callback for when the location is available.
- * @param error function Callback for an error. (Error data structure may vary.)
- * @param type string Type of fetching to use. "nav", "ip" or "both"
- */
- static getLocation(then, error, type = "both") {
- if (type === "both") {
- type = ["nav", "ip"];
- } else {
- type = [type];
- }
-
- tpvm.addEventListener("dataGeo_located", then);
-
- // navigator is preferred if available and allowed.
- if (navigator.geolocation && navigator.permissions && type.indexOf("nav") > -1) {
- navigator.permissions.query({name: 'geolocation'}).then(
- function(PermissionStatus) {
- TP_DataGeo.loc.permission = PermissionStatus.state;
- if (PermissionStatus.state === 'granted') {
- return TP_DataGeo.geoByNavigator(null, error);
- } else {
- // Fallback to Server
- if (type.indexOf("ip") > -1) {
- return TP_DataGeo.geoByServer(null, error);
+ /**
+ * Get the user's location.
+ *
+ * @param then function Callback for when the location is available.
+ * @param error function Callback for an error. (Error data structure may vary.)
+ * @param type string Type of fetching to use. "nav", "ip" or "both"
+ */
+ static getLocation(then, error, type = "both") {
+ if (type === "both") {
+ type = ["nav", "ip"];
+ } else {
+ type = [type];
+ }
+
+ tpvm.addEventListener("dataGeo_located", then);
+
+ // navigator is preferred if available and allowed.
+ if (navigator.geolocation && navigator.permissions && type.indexOf("nav") > -1) {
+ navigator.permissions.query({name: 'geolocation'}).then(
+ function (PermissionStatus) {
+ TP_DataGeo.loc.permission = PermissionStatus.state;
+ if (PermissionStatus.state === 'granted') {
+ return TP_DataGeo.geoByNavigator(null, error);
} else {
- error({error: true, message: __("No geolocation option available.", 'TouchPoint-WP')});
+ // Fallback to Server
+ if (type.indexOf("ip") > -1) {
+ return TP_DataGeo.geoByServer(null, error);
+ } else {
+ error({error: true, message: __("No geolocation option available.", 'TouchPoint-WP')});
+ }
}
}
- }
- )
- } else {
- // Fallback to Server
- if (type.indexOf("ip") > -1) {
- return TP_DataGeo.geoByServer(null, error);
+ )
} else {
- error({error: true, message: __("No geolocation option available.", 'TouchPoint-WP')});
+ // Fallback to Server
+ if (type.indexOf("ip") > -1) {
+ return TP_DataGeo.geoByServer(null, error);
+ } else {
+ error({error: true, message: __("No geolocation option available.", 'TouchPoint-WP')});
+ }
}
}
- }
- static geoByServer(then, error) {
- tpvm.getData('geolocate').then(function (responseData) {
- if (responseData.hasOwnProperty("error")) {
- error(responseData.error)
- tpvm.trigger("dataGeo_error", responseData.error)
- } else {
- for (const di in responseData) {
- if (responseData.hasOwnProperty(di))
- TP_DataGeo.loc[di] = responseData[di];
- }
+ static geoByServer(then, error) {
+ tpvm.getData('geolocate').then(function (responseData) {
+ if (responseData.hasOwnProperty("error")) {
+ error(responseData.error)
+ tpvm.trigger("dataGeo_error", responseData.error)
+ } else {
+ for (const di in responseData) {
+ if (responseData.hasOwnProperty(di))
+ TP_DataGeo.loc[di] = responseData[di];
+ }
- if (then !== null) {
- then(TP_DataGeo.loc)
- }
+ if (then !== null) {
+ then(TP_DataGeo.loc)
+ }
- tpvm.trigger("dataGeo_located", TP_DataGeo.loc)
- }
- }, error);
- }
-}
-TP_DataGeo.init();
-
-class TP_MapMarker
-{
- /**
- *
- * @type {TP_Mappable[]}
- */
- items = [];
-
- color = "#000";
-
- geoStr = "";
-
- /**
- * @type {google.maps.Marker}
- */
- gMkr = null;
-
- constructor(options) {
- if (!options.hasOwnProperty('icon')) {
- options.icon = {
- path: "M172.268 501.67C26.97 291.031 0 269.413 0 192 0 85.961 85.961 0 192 0s192 85.961 192 192c0 77.413-26.97 99.031-172.268 309.67-9.535 13.774-29.93 13.773-39.464 0z", // from FontAwesome
- fillColor: options.color ?? "#000",
- fillOpacity: .85,
- anchor: new google.maps.Point(172.268, 501.67),
- strokeWeight: 1,
- scale: 0.04,
- labelOrigin: new google.maps.Point(190, 198)
- }
- }
- this.gMkr = new google.maps.Marker(options);
- let that = this;
- this.gMkr.addListener("click", () => that.handleClick());
+ tpvm.trigger("dataGeo_located", TP_DataGeo.loc)
+ }
+ }, error);
+ }
}
+ tpvm.TP_DataGeo = TP_DataGeo;
+ TP_DataGeo.init();
- get visibleItems() {
- return this.items.filter((i) => i._visible);
- }
+ class TP_MapMarker {
+ /**
+ *
+ * @type {TP_Mappable[]}
+ */
+ items = [];
- get visible() {
- return this.visibleItems.length > 0
- }
+ color = "#000";
- get inBounds() {
- return this.gMkr.getMap().getBounds().contains(this.gMkr.getPosition());
- }
+ geoStr = "";
- get useIcon() {
- let icon = this.visibleItems.find((i) => i.useIcon !== false)
- if (icon === undefined) {
- return false;
+ /**
+ * @type {google.maps.marker.AdvancedMarkerElement}
+ */
+ gMkr = null;
+ map = null;
+
+ constructor(options) {
+ this.color = options.color ?? "#000";
+ this.map = options.map;
+
+ if (options.hasOwnProperty('color')) {
+ delete options.color; // can't be passed to AdvancedMarkerElement
+ }
+
+ if (!options.hasOwnProperty('content')) {
+ const label = document.createElement('div');
+ label.classList.add("map-marker-label");
+ const pin = new google.maps.marker.PinElement({
+ glyph: label,
+ glyphColor: "#000",
+ background: this.color,
+ borderColor: "#000",
+ scale: .65,
+ });
+ options.content = pin.element;
+ }
+
+ this.gMkr = new google.maps.marker.AdvancedMarkerElement(options);
+ let that = this;
+ this.gMkr.addListener("gmp-click", () => that.handleClick());
+ this.gMkr.content.classList.add("tp-map-marker");
+ }
+
+ get visibleItems() {
+ return this.items.filter((i) => i._visible);
}
- return icon.useIcon;
- }
- updateLabel(highlighted = false) {
- if (this.gMkr === null) {
- return;
+ get visible() {
+ return this.visibleItems.length > 0
}
- let icon = this.gMkr.getIcon();
+ get inBounds() {
+ let map = this.gMkr.map;
+ if (!map) { // if map failed to render for some reason, this prevents entries from being hidden.
+ return true;
+ }
+ return map.getBounds().contains(this.gMkr.position);
+ }
- // Update icon color
- this.color = tpvm._utils.averageColor(this.visibleItems.map((i) => i.color))
- if (icon !== undefined && icon.hasOwnProperty("fillColor")) {
- icon.fillColor = this.color;
- this.gMkr.setIcon(icon);
+ get useIcon() {
+ let icon = this.visibleItems.find((i) => i.useIcon !== false)
+ if (icon === undefined) {
+ return false;
+ }
+ return icon.useIcon;
}
- // Update visibility
- this.gMkr.setVisible(this.visibleItems.length > 0);
+ updateLabel(highlighted = false) {
+ if (this.gMkr === null) {
+ return;
+ }
- // Update title
- this.gMkr.setTitle(tpvm._utils.stringArrayToListString(this.visibleItems.map((i) => i.name)))
+ let icon = this.gMkr.content;
+
+ // Update icon color
+ this.color = tpvm._utils.averageColor(this.visibleItems.map((i) => i.color));
+ if (icon !== undefined && icon.hasOwnProperty("fillColor")) {
+ icon.fillColor = this.color;
+ this.gMkr.content.style.backgroundColor = this.color; // Update AdvancedMarkerElement content style
+ }
- // Update label proper
- if (highlighted) {
- this.gMkr.setLabel(null); // Remove label if highlighted, because labels don't animate.
- } else {
- this.gMkr.setLabel(this.getLabelContent());
+ // Update visibility
+ this.gMkr.map = (this.visibleItems.length > 0 ? this.map : null);
+
+ // Update title
+ this.gMkr.title = tpvm._utils.stringArrayToListString(this.visibleItems.map((i) => i.name));
+
+ this.gMkr.content.getElementsByTagName('div')[0].innerHTML = this.getLabelContent() || ""; // Set label content
}
- }
- getLabelContent() {
- let label = null;
- if (this.visibleItems.length > 1) {
- label = {
- text: this.visibleItems.length.toString(),
- color: "#000000",
- fontSize: "100%"
+ getLabelContent() {
+ if (this.visibleItems.length > 1) {
+ return this.visibleItems.length.toString();
+ } else if (this.useIcon !== false) { // icon for secure partners
+ return this.useIcon;
}
- } else if (this.useIcon !== false) { // icon for secure partners
- label = this.useIcon;
+ return null;
}
- return label;
- }
- // noinspection JSUnusedGlobalSymbols Used dynamically from markers.
- handleClick() {
- if (this.gMkr === null) {
- return;
- }
+ // noinspection JSUnusedGlobalSymbols Used dynamically from markers.
+ handleClick() {
+ if (this.gMkr === null) {
+ return;
+ }
- tpvm._utils.clearHash();
+ tpvm._utils.clearHash();
- const mp = this.gMkr.getMap();
- TP_MapMarker.smoothZoom(mp, this.gMkr.getPosition()).then(() => 1)
+ const mp = this.gMkr.map;
+ TP_MapMarker.smoothZoom(mp, this.gMkr.position).then(() => 1)
- tpvm._utils.ga('send', 'event', this.items[0].itemTypeName, 'mapMarker click', this.gMkr.getTitle());
- }
+ tpvm._utils.ga('send', 'event', this.items[0].itemTypeName, 'mapMarker click', this.gMkr.getTitle());
+ }
- /**
- * Smoothly zoom in (or out) on the given map. By default, zooms in to the max level allowed.
- *
- * @param {google.maps.Map} map The Google Maps map
- * @param {google.maps.LatLng} position The position to move to center
- * @param {number, undefined} zoomTo Google Maps zoom level, or undefined for maxZoom.
- */
- static async smoothZoom(map, position = null, zoomTo = undefined) {
- if (zoomTo === undefined || zoomTo > map.maxZoom) {
- zoomTo = map.maxZoom;
- }
-
- if (map.getZoom() !== zoomTo) {
- let z = google.maps.event.addListener(map, 'zoom_changed', () => {
- google.maps.event.removeListener(z);
- setTimeout(() => this.smoothZoom(map, position, zoomTo), 150);
- });
- if (map.getZoom() < zoomTo) { // zoom in
- map.setZoom(map.getZoom() + 1);
- } else { // zoom out
- map.setZoom(map.getZoom() - 1);
+ /**
+ * Smoothly zoom in (or out) on the given map. By default, zooms in to the max level allowed.
+ *
+ * @param {google.maps.Map} map The Google Maps map
+ * @param {google.maps.LatLng} position The position to move to center
+ * @param {number, undefined} zoomTo Google Maps zoom level, or undefined for maxZoom.
+ */
+ static async smoothZoom(map, position = null, zoomTo = undefined) {
+ if (zoomTo === undefined || zoomTo > map.maxZoom) {
+ zoomTo = map.maxZoom;
}
- if (position !== null) {
- let oldPos = map.getCenter(),
- newPos = new google.maps.LatLng((oldPos.lat() + position.lat() * 2) / 3, (oldPos.lng() + position.lng() * 2) / 3);
- map.panTo(newPos);
+
+ if (map.getZoom() !== zoomTo) {
+ let z = google.maps.event.addListener(map, 'zoom_changed', () => {
+ google.maps.event.removeListener(z);
+ setTimeout(() => this.smoothZoom(map, position, zoomTo), 150);
+ });
+ if (map.getZoom() < zoomTo) { // zoom in
+ map.setZoom(map.getZoom() + 1);
+ } else if (map.getZoom() > zoomTo) { // zoom out
+ map.setZoom(map.getZoom() - 1);
+ }
+ if (position !== null) {
+ let oldPos = map.getCenter(),
+ newPos = new google.maps.LatLng((oldPos.lat() + position.lat * 2) / 3, (oldPos.lng() + position.lng * 2) / 3);
+ map.panTo(newPos);
+ }
+ } else {
+ map.panTo(position);
}
- } else {
- map.panTo(position);
}
}
-}
+ tpvm.TP_MapMarker = TP_MapMarker;
-class TP_Mappable {
- name = "";
- post_id = 0;
- _id = null; // For situations where the ID needs to happen early in the instantiation chain.
+ class TP_Mappable {
+ name = "";
+ post_id = 0;
+ _id = null; // For situations where the ID needs to happen early in the instantiation chain.
- geo = {};
+ geo = {};
- color = "#000";
+ color = "#000";
- /**
- * @type {TP_Mappable[]}
- */
- static items = [];
- static itemsWithoutMarkers = [];
+ /**
+ * @type {TP_Mappable[]}
+ */
+ static items = [];
+ static itemsWithoutMarkers = [];
- _visible = true;
+ _visible = true;
- /**
- * All markers on all maps.
- *
- * @type {TP_MapMarker[]}
- */
- static markers = [];
+ /**
+ * All markers on all maps.
+ *
+ * @type {TP_MapMarker[]}
+ */
+ static markers = [];
- /**
- * Markers for this specific object.
- *
- * @type {TP_MapMarker[]}
- */
- markers = [];
+ /**
+ * Markers for this specific object.
+ *
+ * @type {TP_MapMarker[]}
+ */
+ markers = [];
- constructor(obj, id = null) {
- this._id = id;
+ constructor(obj, id = null) {
+ this._id = id;
- if (obj.geo !== undefined && obj.geo !== null && obj.geo.lat !== null && obj.geo.lng !== null) {
- obj.geo.lat = Math.round(obj.geo.lat * 1000) / 1000;
- obj.geo.lng = Math.round(obj.geo.lng * 1000) / 1000;
- }
+ if (obj.geo !== undefined && obj.geo !== null && obj.geo.lat !== null && obj.geo.lng !== null) {
+ obj.geo.lat = Math.round(obj.geo.lat * 1000) / 1000;
+ obj.geo.lng = Math.round(obj.geo.lng * 1000) / 1000;
+ }
- this.geo = [obj.geo] ?? [];
+ this.geo = [obj.geo] ?? [];
- this.name = obj.name.replace("&", "&");
- this.post_id = obj.post_id;
+ this.name = obj.name.replace("&", "&");
+ this.post_id = obj.post_id;
- if (obj.post_id === undefined) {
- this.post_id = 0;
- }
+ if (obj.post_id === undefined) {
+ this.post_id = 0;
+ }
- if (obj.hasOwnProperty('color')) {
- this.color = obj.color;
- }
+ if (obj.hasOwnProperty('color')) {
+ this.color = obj.color;
+ }
+
+ for (const ei in this.connectedElements) {
+ if (!this.connectedElements.hasOwnProperty(ei)) continue;
- for (const ei in this.connectedElements) {
- if (!this.connectedElements.hasOwnProperty(ei)) continue;
+ let mappable = this;
+ this.connectedElements[ei].addEventListener('mouseenter', function (e) {
+ e.stopPropagation();
+ mappable.toggleHighlighted(true);
+ });
+ this.connectedElements[ei].addEventListener('mouseleave', function (e) {
+ e.stopPropagation();
+ mappable.toggleHighlighted(false);
+ });
- let mappable = this;
- this.connectedElements[ei].addEventListener('mouseenter', function(e){e.stopPropagation(); mappable.toggleHighlighted(true);});
- this.connectedElements[ei].addEventListener('mouseleave', function(e){e.stopPropagation(); mappable.toggleHighlighted(false);});
+ let ce = this.connectedElements[ei],
+ actionBtns = Array.from(ce.querySelectorAll('[data-tp-action]'));
+ if (ce.hasAttribute('data-tp-action')) {
+ // if there's a sole button, it should be added to the list so it works, too.
+ actionBtns.push(ce);
+ }
- let actionBtns = this.connectedElements[ei].querySelectorAll('[data-tp-action]')
- for (const ai in actionBtns) {
- if (!actionBtns.hasOwnProperty(ai)) continue;
- const action = actionBtns[ai].getAttribute('data-tp-action');
- if (typeof mappable[action + "Action"] === "function") {
- tpvm._utils.registerAction(action, mappable)
- actionBtns[ai].addEventListener('click', function (e) {
- e.stopPropagation();
- mappable[action + "Action"]();
- });
+ for (const ai in actionBtns) {
+ if (!actionBtns.hasOwnProperty(ai)) continue;
+ const action = actionBtns[ai].getAttribute('data-tp-action');
+ if (typeof mappable[action + "Action"] === "function") {
+ tpvm._utils.registerAction(action, mappable)
+ actionBtns[ai].addEventListener('click', function (e) {
+ e.stopPropagation();
+ mappable[action + "Action"]();
+ });
+ }
}
}
+
+ TP_Mappable.items.push(this);
}
- TP_Mappable.items.push(this);
- }
+ /**
+ * Returns the ID used for instances in tpvm. Must be implemented by extenders if not the post_id.
+ *
+ * @return {int}
+ */
+ get id() {
+ return this.post_id;
+ }
- /**
- * Returns the ID used for instances in tpvm. Must be implemented by extenders if not the post_id.
- *
- * @return {int}
- */
- get id() {
- return this.post_id;
- }
+ get shortClass() {
+ return "mpbl";
+ }
- get shortClass() {
- return "mpbl";
- }
+ static initMap(containerElt, mapOptions, list) {
+ google.maps.visualRefresh = true;
+ const map = new google.maps.Map(containerElt, mapOptions),
+ bounds = new google.maps.LatLngBounds();
- static initMap(containerElt, mapOptions, list) {
- google.maps.visualRefresh = true;
- const map = new google.maps.Map(containerElt, mapOptions),
- bounds = new google.maps.LatLngBounds();
+ for (const ii in list) {
+ if (!list.hasOwnProperty(ii)) continue;
- for (const ii in list) {
- if (!list.hasOwnProperty(ii)) continue;
+ // skip items that aren't locatable.
+ let hasMarkers = false;
+ for (const gi in list[ii].geo) {
+ if (list[ii].geo[gi] === null || list[ii].geo[gi].lat === null || list[ii].geo[gi].lng === null)
+ continue;
- // skip items that aren't locatable.
- let hasMarkers = false;
- for (const gi in list[ii].geo) {
- if (list[ii].geo[gi] === null || list[ii].geo[gi].lat === null || list[ii].geo[gi].lng === null)
- continue;
+ const item = list[ii],
+ geoStr = "" + item.geo[gi].lat + "," + item.geo[gi].lng;
+ let mkr = this.markers.find((m) => m.gMkr.map === map && m.geoStr === geoStr);
- const item = list[ii],
- geoStr = "" + item.geo[gi].lat + "," + item.geo[gi].lng;
- let mkr = this.markers.find((m) => m.gMkr.getMap() === map && m.geoStr === geoStr);
-
- // If there isn't already a marker for the item on the right map, create one.
- if (mkr === undefined) {
- mkr = new TP_MapMarker({
- position: item.geo[gi],
- color: item.color,
- map: map,
- animation: google.maps.Animation.DROP,
- });
- mkr.geoStr = geoStr;
+ // If there isn't already a marker for the item on the right map, create one.
+ if (mkr === undefined) {
+ mkr = new TP_MapMarker({
+ position: item.geo[gi],
+ color: item.color,
+ map: map,
+ });
+ mkr.geoStr = geoStr;
- // Add to collection of all markers
- this.markers.push(mkr);
- }
+ // Add to collection of all markers
+ this.markers.push(mkr);
+ }
- bounds.extend(mkr.gMkr.getPosition());
+ bounds.extend(mkr.gMkr.position);
- // If the marker doesn't already have a reference to this item, add one.
- if (!mkr.items.includes(item)) {
- mkr.items.push(item);
- }
+ // If the marker doesn't already have a reference to this item, add one.
+ if (!mkr.items.includes(item)) {
+ mkr.items.push(item);
+ }
- // If the item doesn't already have a reference to this marker, add one.
- if (!item.markers.includes(mkr)) {
- item.markers.push(mkr);
- }
+ // If the item doesn't already have a reference to this marker, add one.
+ if (!item.markers.includes(mkr)) {
+ item.markers.push(mkr);
+ }
- hasMarkers = true;
+ hasMarkers = true;
- mkr.updateLabel();
- }
- if (!hasMarkers) {
- this.itemsWithoutMarkers.push(this);
+ mkr.updateLabel();
+ }
+ if (!hasMarkers) {
+ this.itemsWithoutMarkers.push(this);
+ }
}
- }
- map.fitBounds(bounds);
+ map.fitBounds(bounds);
- map.addListener('bounds_changed', this.handleZoom);
+ map.addListener('bounds_changed', this.handleZoom);
- // Add Map Reset links
- let elts = document.getElementsByClassName("TouchPointWP-map-resetLink");
- for (const ei in elts) {
- if (! elts.hasOwnProperty(ei)) continue;
- elts[ei].addEventListener("click", (e) => {
- tpvm._utils.clearHash();
- e.preventDefault();
- map.fitBounds(bounds);
- });
+ // Add Map Reset links
+ let elts = document.getElementsByClassName("TouchPointWP-map-resetLink");
+ for (const ei in elts) {
+ if (!elts.hasOwnProperty(ei)) continue;
+ elts[ei].addEventListener("click", (e) => {
+ tpvm._utils.clearHash();
+ e.preventDefault();
+ map.fitBounds(bounds);
+ });
+ }
}
- }
-
- // noinspection JSUnusedGlobalSymbols Used dynamically from warning text.
- /**
- *
- * @param {google.maps.Map} map
- */
- static resetMap(map) {
- console.log("reset " + map.getMapTypeId())
- }
- /**
- * Currently, this will apply visibility to ALL mappable items, even if they're on a different map.
- */
- static handleZoom() {
- if (TP_Mappable.items.length > 1) { // Don't hide details on Single pages
- for (const ii in TP_Mappable.items) {
- TP_Mappable.items[ii].applyVisibilityToConnectedElements();
+ /**
+ * Currently, this will apply visibility to ALL mappable items, even if they're on a different map.
+ */
+ static handleZoom() {
+ if (TP_Mappable.items.length > 1) { // Don't hide details on Single pages
+ for (const ii in TP_Mappable.items) {
+ TP_Mappable.items[ii].applyVisibilityToConnectedElements();
+ }
+ TP_Mappable.updateFilterWarnings();
}
- TP_Mappable.updateFilterWarnings();
}
- }
- static updateFilterWarnings() {
- let elts = document.getElementsByClassName("TouchPointWP-map-warning-visibleOnly"),
- includesBoth = TP_Mappable.mapIncludesVisibleItemsWhichAreBothInAndOutOfBounds;
- for (const ei in elts) {
- if (!elts.hasOwnProperty(ei))
- continue;
- elts[ei].style.display = (TP_Mappable.mapExcludesSomeVisibleMarkers && !includesBoth) ? "" : "none";
- }
+ static updateFilterWarnings() {
+ let elts = document.getElementsByClassName("TouchPointWP-map-warning-visibleOnly"),
+ includesBoth = TP_Mappable.mapIncludesVisibleItemsWhichAreBothInAndOutOfBounds;
+ elts[ei].style = undefined;
+ for (const ei in elts) {
+ if (!elts.hasOwnProperty(ei))
+ continue;
+ elts[ei].style.display = (TP_Mappable.mapExcludesSomeVisibleMarkers && !includesBoth) ? "" : "none";
+ }
- elts = document.getElementsByClassName("TouchPointWP-map-warning-visibleAndInvisible");
- for (const ei in elts) {
- if (!elts.hasOwnProperty(ei))
- continue;
- elts[ei].style.display = includesBoth ? "" : "none";
- }
+ elts = document.getElementsByClassName("TouchPointWP-map-warning-visibleAndInvisible");
+ for (const ei in elts) {
+ if (!elts.hasOwnProperty(ei))
+ continue;
+ elts[ei].style.display = includesBoth ? "" : "none";
+ }
- elts = document.getElementsByClassName("TouchPointWP-map-warning-zoomOrReset");
- for (const ei in elts) {
- if (!elts.hasOwnProperty(ei))
- continue;
- elts[ei].style.display = TP_Mappable.mapExcludesSomeVisibleMarkers ? "" : "none";
+ elts = document.getElementsByClassName("TouchPointWP-map-warning-zoomOrReset");
+ for (const ei in elts) {
+ if (!elts.hasOwnProperty(ei))
+ continue;
+ elts[ei].style.display = TP_Mappable.mapExcludesSomeVisibleMarkers ? "" : "none";
+ }
}
- }
- updateMarkerLabels() {
- for (const mi in this.markers) {
- this.markers[mi].updateLabel();
+ updateMarkerLabels() {
+ for (const mi in this.markers) {
+ this.markers[mi].updateLabel();
+ }
}
- }
- // noinspection JSUnusedGlobalSymbols Used dynamically from btns.
- showOnMapAction() {
- tpvm._utils.ga('send', 'event', this.itemTypeName, 'showOnMap btn click', this.name);
+ // noinspection JSUnusedGlobalSymbols Used dynamically from btns.
+ showOnMapAction() {
+ tpvm._utils.ga('send', 'event', this.itemTypeName, 'showOnMap btn click', this.name);
+
+ tpvm._utils.applyHashForAction("showOnMap", this);
+
+ // One marker (probably typical)
+ if (this.markers.length === 1) {
+ let mp = this.markers[0].gMkr.map,
+ el = mp.getDiv(),
+ rect = el.getBoundingClientRect(),
+ viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight),
+ mpWithinView = !(rect.bottom < 0 || rect.top - viewHeight >= 0);
+ TP_MapMarker.smoothZoom(mp, this.markers[0].gMkr.position).then(() => 1);
+ if (!mpWithinView) {
+ window.scroll({
+ top: rect.top,
+ left: rect.left,
+ behavior: 'smooth'
+ })
+ }
+ return;
+ }
- tpvm._utils.applyHashForAction("showOnMap", this);
+ // No Markers
+ if (this.markers.length === 0) {
+ console.warn("\"Show on Map\" was called on a Mappable item that doesn't have markers.")
+ return;
+ }
- // One marker (probably typical)
- if (this.markers.length === 1) {
- let mp = this.markers[0].gMkr.getMap(),
- el = mp.getDiv(),
- rect = el.getBoundingClientRect(),
- viewHeight = Math.max(document.documentElement.clientHeight, window.innerHeight),
- mpWithinView = !(rect.bottom < 0 || rect.top - viewHeight >= 0);
- TP_MapMarker.smoothZoom(mp, this.markers[0].gMkr.getPosition()).then(() => 1);
- if (!mpWithinView) {
- window.scroll({
- top: rect.top,
- left: rect.left,
- behavior: 'smooth'
- })
+ // More than one marker
+ console.warn("\"Show on Map\" for Mappable items with multiple markers is not fully supported.")
+ // Hide all non-matching markers. There isn't really a way to get them back, but that's why this isn't fully supported.
+ for (const mi in TP_Mappable.markers) {
+ for (const ii in TP_Mappable.markers[mi].items) {
+ TP_Mappable.markers[mi].items[ii].toggleVisibility(TP_Mappable.markers[mi].items[ii] === this);
+ }
}
- return;
}
- // No Markers
- if (this.markers.length === 0) {
- console.warn("\"Show on Map\" was called on a Mappable item that doesn't have markers.")
- return;
+ get itemTypeName() {
+ return this.constructor.name;
}
- // More than one marker
- console.warn("\"Show on Map\" for Mappable items with multiple markers is not fully supported.")
- // Hide all non-matching markers. There isn't really a way to get them back, but that's why this isn't fully supported.
- for (const mi in TP_Mappable.markers) {
- for (const ii in TP_Mappable.markers[mi].items) {
- TP_Mappable.markers[mi].items[ii].toggleVisibility(TP_Mappable.markers[mi].items[ii] === this);
- }
+ get visible() {
+ return this._visible && (this.markers.some((m) => m.visible) || this.markers.length === 0);
}
- }
-
- get itemTypeName() {
- return this.constructor.name;
- }
-
- get visible() {
- return this._visible && (this.markers.some((m) => m.visible) || this.markers.length === 0);
- }
-
- get inBounds() {
- return this.markers.some((m) => m.inBounds);
- }
- static get mapExcludesSomeVisibleMarkers() {
- return this.markers.some((m) => m.visible && !m.inBounds);
- }
+ get inBounds() {
+ return this.markers.some((m) => m.inBounds);
+ }
- static get mapIncludesVisibleItemsWhichAreBothInAndOutOfBounds() {
- return this.items.some((i) => i.visible && i.markers.some((mk) => mk.inBounds) && i.markers.some((mk) => !mk.inBounds))
- }
+ static get mapExcludesSomeVisibleMarkers() {
+ return this.markers.some((m) => m.visible && !m.inBounds);
+ }
- get useIcon() {
- return false;
- }
+ static get mapIncludesVisibleItemsWhichAreBothInAndOutOfBounds() {
+ return this.items.some((i) => i.visible && i.markers.some((mk) => mk.inBounds) && i.markers.some((mk) => !mk.inBounds))
+ }
- get highlightable() {
- return true;
- }
+ get useIcon() {
+ return false;
+ }
- toggleVisibility(vis = null) {
- if (vis === null) {
- this._visible = !this._visible
- } else {
- this._visible = !!vis;
+ get highlightable() {
+ return true;
}
- this._visible = vis;
- this.updateMarkerLabels();
+ toggleVisibility(vis = null) {
+ if (vis === null) {
+ this._visible = !this._visible
+ } else {
+ this._visible = !!vis;
+ }
- this.applyVisibilityToConnectedElements();
+ this._visible = vis;
+ this.updateMarkerLabels();
- return this._visible;
- }
+ this.applyVisibilityToConnectedElements();
- get connectedElements() {
- const clsName = this.constructor.name.toLowerCase().replace("_", "-");
- const sPath = '[data-' + clsName + '="' + this.post_id + '"]'
- return document.querySelectorAll(sPath);
- }
+ return this._visible;
+ }
- applyVisibilityToConnectedElements() {
- let elts = this.connectedElements;
- for (const ei in elts) {
- if (!elts.hasOwnProperty(ei))
- continue;
- elts[ei].style.display = (this.visible && (this.inBounds || !TP_Mappable.mapExcludesSomeVisibleMarkers)) ? "" : "none";
+ get connectedElements() {
+ const clsName = this.constructor.name.toLowerCase().replace("_", "-");
+ const sPath = '[data-' + clsName + '="' + this.post_id + '"]'
+ return document.querySelectorAll(sPath);
}
- }
- toggleHighlighted(hl) {
- this.highlighted = !!hl;
+ applyVisibilityToConnectedElements() {
+ let elts = this.connectedElements;
+ for (const ei in elts) {
+ if (!elts.hasOwnProperty(ei))
+ continue;
+ elts[ei].style.display = (this.visible && (this.inBounds || !TP_Mappable.mapExcludesSomeVisibleMarkers)) ? "" : "none";
+ }
+ }
- if (!this.highlightable)
- this.highlighted = false;
+ toggleHighlighted(hl) {
+ this.highlighted = !!hl;
- if (this.highlighted) {
- let item = this;
- for (let mi in this.markers) {
- const mk = item.markers[mi];
- if (TP_Mappable.items.length > 1) {
- if (mk.gMkr.getAnimation() !== google.maps.Animation.BOUNCE) {
- mk.gMkr.setAnimation(google.maps.Animation.BOUNCE);
+ if (!this.highlightable)
+ this.highlighted = false;
+
+ if (this.highlighted) {
+ let item = this;
+ for (let mi in this.markers) {
+ const mk = item.markers[mi];
+ if (TP_Mappable.items.length > 1) {
+ if (!mk.gMkr.content.classList.contains("highlighted-marker")) {
+ mk.gMkr.content.classList.add("highlighted-marker");
+ }
}
+ mk.updateLabel(this.highlighted)
}
- mk.updateLabel(this.highlighted)
- }
- } else {
- for (const mi in this.markers) {
- let mk = this.markers[mi];
- if (mk.gMkr.getAnimation() !== null) {
- mk.gMkr.setAnimation(null)
+ } else {
+ for (const mi in this.markers) {
+ let mk = this.markers[mi];
+ if (mk.gMkr.content.classList.contains("highlighted-marker")) {
+ mk.gMkr.content.classList.remove("highlighted-marker");
+ }
+ mk.updateLabel(this.highlighted)
}
- mk.updateLabel(this.highlighted)
}
}
}
-}
+ tpvm.TP_Mappable = TP_Mappable;
-class TP_Involvement extends TP_Mappable {
- invId = "";
- invType = "involvement"; // overwritten by constructor
+ class TP_Involvement extends tpvm.TP_Mappable {
+ invId = "";
+ invType = "involvement"; // overwritten by constructor
- attributes = {};
+ attributes = {};
- static currentFilters = {};
+ static currentFilters = {};
- static actions = ['join', 'contact'];
+ static actions = ['join', 'contact'];
- constructor(obj) {
- super(obj, obj.invId);
+ constructor(obj) {
+ super(obj, obj.invId);
- this.invId = obj.invId;
- this.invType = obj.invType;
+ this.invId = obj.invId;
+ this.invType = obj.invType;
- this.attributes = obj.attributes ?? null;
+ this.attributes = obj.attributes ?? null;
- tpvm.involvements[this.invId] = this;
- }
+ tpvm.involvements[this.invId] = this;
+ }
- get id() {
- return parseInt(this._id);
- }
+ get id() {
+ return parseInt(this._id);
+ }
- get shortClass() {
- return "i";
- }
+ get shortClass() {
+ return "i";
+ }
- // noinspection JSUnusedGlobalSymbols Used via dynamic instantiation.
- static fromObjArray(invArr) {
- let ret = [];
- for (const i in invArr) {
- if (!invArr.hasOwnProperty(i)) continue;
+ // noinspection JSUnusedGlobalSymbols Used via dynamic instantiation.
+ static fromObjArray(invArr) {
+ let ret = [];
+ for (const i in invArr) {
+ if (!invArr.hasOwnProperty(i)) continue;
- if (typeof invArr[i].invId === "undefined") {
- continue;
- }
+ if (typeof invArr[i].invId === "undefined") {
+ continue;
+ }
- if (typeof tpvm.involvements[invArr[i].invId] === "undefined") {
- ret.push(new this(invArr[i]));
- } else {
- ret.push(tpvm.involvements[invArr[i].invId]);
+ if (typeof tpvm.involvements[invArr[i].invId] === "undefined") {
+ ret.push(new this(invArr[i]));
+ } else {
+ ret.push(tpvm.involvements[invArr[i].invId]);
+ }
}
- }
- tpvm.trigger("Involvement_fromObjArray");
- return ret;
- };
+ tpvm.trigger("Involvement_fromObjArray");
+ return ret;
+ };
- // noinspection JSUnusedGlobalSymbols Called by inline.
- static initFilters() {
- const filtOptions = document.querySelectorAll("[data-involvement-filter]");
- for (const ei in filtOptions) {
- if (!filtOptions.hasOwnProperty(ei)) continue;
- filtOptions[ei].addEventListener('change', this.applyFilters.bind(this, "Involvement"))
+ // noinspection JSUnusedGlobalSymbols Called by inline.
+ static initFilters() {
+ const filtOptions = document.querySelectorAll("[data-involvement-filter]");
+ for (const ei in filtOptions) {
+ if (!filtOptions.hasOwnProperty(ei)) continue;
+ filtOptions[ei].addEventListener('change', this.applyFilters.bind(this, "Involvement"))
+ }
}
- }
- static applyFilters(invType, ev = null) {
- if (ev !== null) {
- let attr = ev.target.getAttribute("data-involvement-filter"),
- val = ev.target.value;
- if (attr !== null) {
- if (val === "") {
- delete this.currentFilters[attr];
- } else {
- this.currentFilters[attr] = val;
+ static applyFilters(invType, ev = null) {
+ if (ev !== null) {
+ let attr = ev.target.getAttribute("data-involvement-filter"),
+ val = ev.target.value;
+ if (attr !== null) {
+ if (val === "") {
+ delete this.currentFilters[attr];
+ } else {
+ this.currentFilters[attr] = val;
+ }
}
}
- }
- groupLoop:
- for (const ii in tpvm.involvements) {
- if (!tpvm.involvements.hasOwnProperty(ii)) continue;
- const group = tpvm.involvements[ii];
- for (const ai in this.currentFilters) {
- if (!this.currentFilters.hasOwnProperty(ai)) continue;
-
- if (!group.attributes.hasOwnProperty(ai) ||
- group.attributes[ai] === null ||
- ( !Array.isArray(group.attributes[ai]) &&
- group.attributes[ai].slug !== this.currentFilters[ai] &&
- group.attributes[ai] !== this.currentFilters[ai]
- ) || (
- Array.isArray(group.attributes[ai]) &&
- group.attributes[ai].find(a => a.slug === this.currentFilters[ai]) === undefined
- )
- ) {
- group.toggleVisibility(false)
- continue groupLoop;
+ groupLoop:
+ for (const ii in tpvm.involvements) {
+ if (!tpvm.involvements.hasOwnProperty(ii)) continue;
+ const group = tpvm.involvements[ii];
+ for (const ai in this.currentFilters) {
+ if (!this.currentFilters.hasOwnProperty(ai)) continue;
+
+ if (!group.attributes.hasOwnProperty(ai) ||
+ group.attributes[ai] === null ||
+ (!Array.isArray(group.attributes[ai]) &&
+ group.attributes[ai].slug !== this.currentFilters[ai] &&
+ group.attributes[ai] !== this.currentFilters[ai]
+ ) || (
+ Array.isArray(group.attributes[ai]) &&
+ group.attributes[ai].find(a => a.slug === this.currentFilters[ai]) === undefined
+ )
+ ) {
+ group.toggleVisibility(false)
+ continue groupLoop;
+ }
}
+ group.toggleVisibility(true)
}
- group.toggleVisibility(true)
- }
- TP_Mappable.updateFilterWarnings();
- }
+ TP_Mappable.updateFilterWarnings();
+ }
- static init() {
- tpvm.trigger('Involvement_class_loaded');
- }
+ static init() {
+ tpvm.trigger('Involvement_class_loaded');
+ }
- async doJoin(people, showConfirm = true) {
- let inv = this;
- showConfirm = !!showConfirm;
+ async doJoin(people, showConfirm = true) {
+ let inv = this;
+ showConfirm = !!showConfirm;
- tpvm._utils.ga('send', 'event', inv.invType, 'join complete', inv.name);
+ tpvm._utils.ga('send', 'event', inv.invType, 'join complete', inv.name);
- let res = await tpvm.postData('inv/join', {invId: inv.invId, people: people, invType: inv.invType});
- if (res.success.length > 0) {
- if (showConfirm) {
- Swal.fire({
- icon: 'success',
- // translators: %s is the name of an involvement, like a particular small group
- title: sprintf(__('Added to %s', 'TouchPoint-WP'), inv.name),
- timer: 3000,
- customClass: tpvm._utils.defaultSwalClasses(),
- confirmButtonText: __('OK', 'TouchPoint-WP')
- });
- }
- } else {
- console.error(res);
- if (showConfirm) {
- Swal.fire({
- icon: 'error',
- title: __('Something strange happened.', 'TouchPoint-WP'),
- timer: 3000,
- customClass: tpvm._utils.defaultSwalClasses(),
- confirmButtonText: __('OK', 'TouchPoint-WP')
- });
+ let res = await tpvm.postData('inv/join', {invId: inv.invId, people: people, invType: inv.invType});
+ if (res.success.length > 0) {
+ if (showConfirm) {
+ Swal.fire({
+ icon: 'success',
+ // translators: %s is the name of an involvement, like a particular small group
+ title: sprintf(__('Added to %s', 'TouchPoint-WP'), inv.name),
+ timer: 3000,
+ customClass: tpvm._utils.defaultSwalClasses(),
+ confirmButtonText: __('OK', 'TouchPoint-WP')
+ });
+ }
+ } else {
+ console.error(res);
+ if (showConfirm) {
+ Swal.fire({
+ icon: 'error',
+ title: __('Something strange happened.', 'TouchPoint-WP'),
+ timer: 3000,
+ customClass: tpvm._utils.defaultSwalClasses(),
+ confirmButtonText: __('OK', 'TouchPoint-WP')
+ });
+ }
}
}
- }
- async doInvContact(fromPerson, message, showConfirm = true) {
- let inv = this;
- showConfirm = !!showConfirm;
-
- tpvm._utils.ga('send', 'event', inv.invType, 'contact complete', inv.name);
-
- let res = await tpvm.postData('inv/contact', {
- invId: inv.invId,
- fromPerson: fromPerson,
- message: message,
- invType: inv.invType,
- fromEmail: tpvm.userEmail
- });
- if (res.success.length > 0) {
- if (showConfirm) {
- Swal.fire({
- icon: 'success',
- title: __('Your message has been sent.', 'TouchPoint-WP'),
- timer: 3000,
- customClass: tpvm._utils.defaultSwalClasses(),
- confirmButtonText: __('OK', 'TouchPoint-WP')
- });
+ async doInvContact(fromPerson, message, showConfirm = true) {
+ let inv = this;
+ showConfirm = !!showConfirm;
+
+ tpvm._utils.ga('send', 'event', inv.invType, 'contact complete', inv.name);
+
+ let res = await tpvm.postData('inv/contact', {
+ invId: inv.invId,
+ fromPerson: fromPerson,
+ message: message,
+ invType: inv.invType,
+ fromEmail: tpvm.userEmail
+ });
+ if (res.success.length > 0) {
+ if (showConfirm) {
+ Swal.fire({
+ icon: 'success',
+ title: __('Your message has been sent.', 'TouchPoint-WP'),
+ timer: 3000,
+ customClass: tpvm._utils.defaultSwalClasses(),
+ confirmButtonText: __('OK', 'TouchPoint-WP')
+ });
+ }
+ } else {
+ console.error(res);
+ if (showConfirm) {
+ Swal.fire({
+ icon: 'error',
+ title: __('Something strange happened.', 'TouchPoint-WP'),
+ timer: 3000,
+ customClass: tpvm._utils.defaultSwalClasses(),
+ confirmButtonText: __('OK', 'TouchPoint-WP')
+ });
+ }
}
- } else {
- console.error(res);
- if (showConfirm) {
- Swal.fire({
- icon: 'error',
- title: __('Something strange happened.', 'TouchPoint-WP'),
- timer: 3000,
+ }
+
+ // noinspection JSUnusedGlobalSymbols Used dynamically from btns.
+ joinAction() {
+ let inv = this,
+ // translators: %s is the name of an Involvement
+ title = sprintf(__('Join %s', 'TouchPoint-WP'), inv.name);
+
+ tpvm._utils.ga('send', 'event', inv.invType, 'join btn click', inv.name);
+
+ tpvm._utils.applyHashForAction("join", this);
+
+ TP_Person.DoInformalAuth(title).then(
+ (res) => joinUi(inv, res).then(tpvm._utils.clearHash),
+ () => tpvm._utils.clearHash()
+ )
+
+ function joinUi(inv, people) {
+ tpvm._utils.ga('send', 'event', inv.invType, 'join userIdentified', inv.name);
+
+ return Swal.fire({
+ title: title,
+ html: `
${__('Who is joining the group?', 'TouchPoint-WP')}
` + TP_Person.peopleArrayToCheckboxes(people),
customClass: tpvm._utils.defaultSwalClasses(),
- confirmButtonText: __('OK', 'TouchPoint-WP')
+ showConfirmButton: true,
+ showCancelButton: true,
+ confirmButtonText: __('Join', 'TouchPoint-WP'),
+ cancelButtonText: __('Cancel', 'TouchPoint-WP'),
+ focusConfirm: false,
+ preConfirm: () => {
+ let form = document.getElementById('tp_people_list_checkboxes'),
+ inputs = form.querySelectorAll("input"),
+ data = [];
+ for (const ii in inputs) {
+ if (!inputs.hasOwnProperty(ii) || !inputs[ii].checked) continue;
+ data.push(tpvm.people[inputs[ii].value]);
+ }
+
+ if (data.length < 1) {
+ let prompt = document.getElementById('swal-tp-text');
+ prompt.innerText = __("Select who should be added to the group.", 'TouchPoint-WP');
+ prompt.classList.add('error')
+ return false;
+ }
+
+ Swal.showLoading();
+
+ return inv.doJoin(data, true);
+ }
});
}
}
- }
- // noinspection JSUnusedGlobalSymbols Used dynamically from btns.
- joinAction() {
- let inv = this,
- // translators: %s is the name of an Involvement
- title = sprintf(__('Join %s', 'TouchPoint-WP'), inv.name);
-
- tpvm._utils.ga('send', 'event', inv.invType, 'join btn click', inv.name);
-
- tpvm._utils.applyHashForAction("join", this);
-
- TP_Person.DoInformalAuth(title).then(
- (res) => joinUi(inv, res).then(tpvm._utils.clearHash),
- () => tpvm._utils.clearHash()
- )
-
- function joinUi(inv, people) {
- tpvm._utils.ga('send', 'event', inv.invType, 'join userIdentified', inv.name);
-
- return Swal.fire({
- title: title,
- html: `
${__('Who is joining the group?', 'TouchPoint-WP')}
` + TP_Person.peopleArrayToCheckboxes(people),
- customClass: tpvm._utils.defaultSwalClasses(),
- showConfirmButton: true,
- showCancelButton: true,
- confirmButtonText: __('Join', 'TouchPoint-WP'),
- cancelButtonText: __('Cancel', 'TouchPoint-WP'),
- focusConfirm: false,
- preConfirm: () => {
- let form = document.getElementById('tp_people_list_checkboxes'),
- inputs = form.querySelectorAll("input"),
- data = [];
- for (const ii in inputs) {
- if (!inputs.hasOwnProperty(ii) || !inputs[ii].checked) continue;
- data.push(tpvm.people[inputs[ii].value]);
- }
+ // noinspection JSUnusedGlobalSymbols Used dynamically from btns.
+ contactAction() {
+ let inv = this,
+ // translators: %s is the name of an involvement. This is a heading for a modal.
+ title = sprintf(__("Contact the Leaders of %s", 'TouchPoint-WP'), inv.name);
- if (data.length < 1) {
- let prompt = document.getElementById('swal-tp-text');
- prompt.innerText = __("Select who should be added to the group.", 'TouchPoint-WP');
- prompt.classList.add('error')
- return false;
- }
+ tpvm._utils.ga('send', 'event', inv.invType, 'contact btn click', inv.name);
- Swal.showLoading();
+ tpvm._utils.applyHashForAction("contact", this);
- return inv.doJoin(data, true);
- }
- });
- }
- }
+ TP_Person.DoInformalAuth(title).then(
+ (res) => contactUi(inv, res).then(tpvm._utils.clearHash),
+ () => tpvm._utils.clearHash()
+ )
- get itemTypeName() {
- return this.invType;
- }
+ function contactUi(inv, people) {
+ tpvm._utils.ga('send', 'event', inv.invType, 'contact userIdentified', inv.name);
- // noinspection JSUnusedGlobalSymbols Used dynamically from btns.
- contactAction() {
- let inv = this,
- // translators: %s is the name of an involvement. This is a heading for a modal.
- title = sprintf(__("Contact the Leaders of %s", 'TouchPoint-WP'), inv.name);
-
- tpvm._utils.ga('send', 'event', inv.invType, 'contact btn click', inv.name);
-
- tpvm._utils.applyHashForAction("contact", this);
-
- TP_Person.DoInformalAuth(title).then(
- (res) => contactUi(inv, res).then(tpvm._utils.clearHash),
- () => tpvm._utils.clearHash()
- )
-
- function contactUi(inv, people) {
- tpvm._utils.ga('send', 'event', inv.invType, 'contact userIdentified', inv.name);
-
- return Swal.fire({
- title: title,
- html: '',
- customClass: tpvm._utils.defaultSwalClasses(),
- showConfirmButton: true,
- showCancelButton: true,
- confirmButtonText: __('Send', 'TouchPoint-WP'),
- cancelButtonText: __('Cancel', 'TouchPoint-WP'),
- focusConfirm: false,
- preConfirm: () => {
- let form = document.getElementById('tp_inv_contact_form'),
- fromPerson = tpvm.people[parseInt(form.getElementsByTagName('select')[0].value)],
- message = form.getElementsByTagName('textarea')[0].value;
-
- if (message.length < 5) {
- let prompt = document.getElementById('swal-tp-text');
- prompt.innerText = __("Please provide a message.", 'TouchPoint-WP');
- prompt.classList.add('error')
- return false;
- }
+ return Swal.fire({
+ title: title,
+ html: '',
+ customClass: tpvm._utils.defaultSwalClasses(),
+ showConfirmButton: true,
+ showCancelButton: true,
+ confirmButtonText: __('Send', 'TouchPoint-WP'),
+ cancelButtonText: __('Cancel', 'TouchPoint-WP'),
+ focusConfirm: false,
+ preConfirm: () => {
+ let form = document.getElementById('tp_inv_contact_form'),
+ fromPerson = tpvm.people[parseInt(form.getElementsByTagName('select')[0].value)],
+ message = form.getElementsByTagName('textarea')[0].value;
+
+ if (message.length < 5) {
+ let prompt = document.getElementById('swal-tp-text');
+ prompt.innerText = __("Please provide a message.", 'TouchPoint-WP');
+ prompt.classList.add('error')
+ return false;
+ }
- Swal.showLoading();
+ Swal.showLoading();
- return inv.doInvContact(fromPerson, message, true);
- }
- });
+ return inv.doInvContact(fromPerson, message, true);
+ }
+ });
+ }
}
- }
- static initMap(mapDivId) {
- let mapOptions = {
- mapTypeId: google.maps.MapTypeId.ROADMAP,
- linksControl: false,
- maxZoom: 15,
- minZoom: 2,
- panControl: false,
- addressControl: false,
- enableCloseButton: false,
- mapTypeControl: false,
- zoomControl: false,
- gestureHandling: 'greedy',
- styles: [
- {
- featureType: "poi", //points of interest
- stylers: [
- {visibility: 'off'}
- ]
- },
- {
- featureType: "road",
- stylers: [
- {visibility: 'on'}
- ]
- },
- {
- featureType: "transit",
- stylers: [
- {visibility: 'on'}
- ]
- }
- ],
- zoom: 15,
- center: {lat: 0, lng: 0}, // gets overwritten by bounds later.
- streetViewControl: false,
- fullscreenControl: false,
- disableDefaultUI: true
- };
+ static initMap(mapDivId) {
+ let mapOptions = {
+ mapTypeId: google.maps.MapTypeId.ROADMAP,
+ mapId: "f0fb8ca5f6beff5288b80a8d",
+ linksControl: false,
+ maxZoom: 16,
+ minZoom: 2,
+ panControl: false,
+ addressControl: false,
+ enableCloseButton: false,
+ mapTypeControl: false,
+ zoomControl: false,
+ gestureHandling: 'greedy',
+ zoom: 15,
+ center: {lat: 0, lng: 0}, // gets overwritten by bounds later.
+ streetViewControl: false,
+ fullscreenControl: false,
+ disableDefaultUI: true
+ };
+
+ super.initMap(document.getElementById(mapDivId), mapOptions, tpvm.involvements)
+ }
+
+ static initNearby(targetId, type, count) {
+ if (window.location.pathname.substring(0, 10) === "/wp-admin/")
+ return;
- super.initMap(document.getElementById(mapDivId), mapOptions, tpvm.involvements)
- }
+ let target = document.getElementById(targetId);
+ if (!target) // make sure element actually exists (it may not if shortcode was within a tease)
+ return;
- static initNearby(targetId, type, count) {
- if (window.location.pathname.substring(0, 10) === "/wp-admin/")
- return;
-
- let target = document.getElementById(targetId);
- if (!target) // make sure element actually exists (it may not if shortcode was within a tease)
- return;
-
- tpvm._invNear.nearby = ko.observableArray([]);
- tpvm._invNear.labelStr = ko.observable(__("Loading...", 'TouchPoint-WP'));
- ko.applyBindings(tpvm._invNear, target);
-
- // continue to next action for either success or failure.
- TP_DataGeo.getLocation(getNearbyInvolvements, getNearbyInvolvements, 'nav');
-
- function getNearbyInvolvements() {
- tpvm.getData('inv/nearby', {
- lat: TP_DataGeo.loc.lat,
- lng: TP_DataGeo.loc.lng,
- type: type,
- limit: count,
- locale: tpvm.locale
- }).then(handleNearbyLoaded);
- }
-
- function handleNearbyLoaded(response) {
- tpvm._invNear.nearby(response.invList);
- if (response.error !== undefined) {
- if (response.geo === false) {
- if (navigator.geolocation && location.protocol === 'https:') {
- tpvm._invNear.labelStr(__("We don't know where you are.", 'TouchPoint-WP') + " " + __("Click here to use your actual location.", 'TouchPoint-WP') + "");
+ tpvm._invNear.nearby = ko.observableArray([]);
+ tpvm._invNear.labelStr = ko.observable(__("Loading...", 'TouchPoint-WP'));
+ ko.applyBindings(tpvm._invNear, target);
+
+ // continue to next action for either success or failure.
+ TP_DataGeo.getLocation(getNearbyInvolvements, getNearbyInvolvements, 'nav');
+
+ function getNearbyInvolvements() {
+ tpvm.getData('inv/nearby', {
+ lat: TP_DataGeo.loc.lat,
+ lng: TP_DataGeo.loc.lng,
+ type: type,
+ limit: count,
+ locale: tpvm.locale
+ }).then(handleNearbyLoaded);
+ }
+
+ function handleNearbyLoaded(response) {
+ tpvm._invNear.nearby(response.invList);
+ if (response.error !== undefined) {
+ if (response.geo === false) {
+ if (navigator.geolocation && location.protocol === 'https:') {
+ tpvm._invNear.labelStr(__("We don't know where you are.", 'TouchPoint-WP') + " " + __("Click here to use your actual location.", 'TouchPoint-WP') + "");
+ } else {
+ tpvm._invNear.labelStr(__("We don't know where you are.", 'TouchPoint-WP'));
+ }
} else {
- tpvm._invNear.labelStr(__("We don't know where you are.", 'TouchPoint-WP'));
+ tpvm._invNear.labelStr(response.error);
}
+ } else if (response.geo?.human !== undefined) {
+ let label = response.geo.human;
+ if (response.geo.type === "ip" && navigator.geolocation && location.protocol === 'https:') {
+ label += " " + __("Click here to use your actual location.", 'TouchPoint-WP') + "";
+ }
+ tpvm._invNear.labelStr(label);
} else {
- tpvm._invNear.labelStr(response.error);
- }
- } else if (response.geo?.human !== undefined) {
- let label = response.geo.human;
- if (response.geo.type === "ip" && navigator.geolocation && location.protocol === 'https:') {
- label += " " + __("Click here to use your actual location.", 'TouchPoint-WP') + "";
+ tpvm._invNear.labelStr(__("Your Location", 'TouchPoint-WP'));
}
- tpvm._invNear.labelStr(label);
- } else {
- tpvm._invNear.labelStr(__("Your Location", 'TouchPoint-WP'));
+ setTimeout(getNearbyInvolvements, 600000); // 10 minutes
}
- setTimeout(getNearbyInvolvements, 600000); // 10 minutes
}
}
-}
-TP_Involvement.init();
+ tpvm.TP_Involvement = TP_Involvement;
+ TP_Involvement.init();
-class TP_Person {
- peopleId;
- familyId;
- displayName;
+ class TP_Person {
+ peopleId;
+ familyId;
+ displayName;
- static actions = ['join', 'contact'];
+ static actions = ['join', 'contact'];
- constructor(peopleId) {
- peopleId = Number(peopleId);
- this.peopleId = peopleId;
+ constructor(peopleId) {
+ peopleId = Number(peopleId);
+ this.peopleId = peopleId;
- for (const ei in this.connectedElements) {
- if (!this.connectedElements.hasOwnProperty(ei)) continue;
+ for (const ei in this.connectedElements) {
+ if (!this.connectedElements.hasOwnProperty(ei)) continue;
- let psn = this;
+ let psn = this;
- let actionBtns = this.connectedElements[ei].querySelectorAll('[data-tp-action]')
- for (const ai in actionBtns) {
- if (!actionBtns.hasOwnProperty(ai)) continue;
- const action = actionBtns[ai].getAttribute('data-tp-action');
- if (TP_Person.actions.includes(action)) {
- tpvm._utils.registerAction(action, psn)
- actionBtns[ai].addEventListener('click', function (e) {
- e.stopPropagation();
- psn[action + "Action"]();
- });
+ let actionBtns = this.connectedElements[ei].querySelectorAll('[data-tp-action]')
+ for (const ai in actionBtns) {
+ if (!actionBtns.hasOwnProperty(ai)) continue;
+ const action = actionBtns[ai].getAttribute('data-tp-action');
+ if (TP_Person.actions.includes(action)) {
+ tpvm._utils.registerAction(action, psn)
+ actionBtns[ai].addEventListener('click', function (e) {
+ e.stopPropagation();
+ psn[action + "Action"]();
+ });
+ }
}
}
+
+ tpvm.people[peopleId] = this;
}
- tpvm.people[peopleId] = this;
- }
+ /**
+ * Returns the ID used for instances in tpvm. Must be implemented by extenders if not the post_id.
+ *
+ * @return {int}
+ */
+ get id() {
+ return this.peopleId;
+ }
- /**
- * Returns the ID used for instances in tpvm. Must be implemented by extenders if not the post_id.
- *
- * @return {int}
- */
- get id() {
- return this.peopleId;
- }
+ get shortClass() {
+ return "p";
+ }
- get shortClass() {
- return "p";
- }
+ static fromObj(obj) {
+ let person;
+ if (tpvm.people[obj.peopleId] !== undefined) {
+ person = tpvm.people[obj.peopleId]
+ } else {
+ person = new TP_Person(obj.peopleId);
+ }
+ for (const a in obj) {
+ if (!obj.hasOwnProperty(a) || a === 'peopleId') continue;
- static fromObj(obj) {
- let person;
- if (tpvm.people[obj.peopleId] !== undefined) {
- person = tpvm.people[obj.peopleId]
- } else {
- person = new TP_Person(obj.peopleId);
+ person[a] = obj[a];
+ }
+ return person;
}
- for (const a in obj) {
- if (!obj.hasOwnProperty(a) || a === 'peopleId') continue;
- person[a] = obj[a];
- }
- return person;
- }
+ static fromObjArray(peopleArray) {
+ let ret = [];
- static fromObjArray(peopleArray) {
- let ret = [];
+ for (const pi in peopleArray) {
+ if (!peopleArray.hasOwnProperty(pi)) continue;
+ ret.push(tpvm.TP_Person.fromObj(peopleArray[pi]));
+ }
+ tpvm.trigger("Person_fromObjArray");
- for (const pi in peopleArray) {
- if (!peopleArray.hasOwnProperty(pi)) continue;
- ret.push(TP_Person.fromObj(peopleArray[pi]));
+ return ret;
}
- tpvm.trigger("Person_fromObjArray");
- return ret;
- }
+ // noinspection JSUnusedGlobalSymbols Called in Person.php
+ static identByFamily(primaryFamIds = [], secondaryFamIds = []) {
+ tpvm._plausibleUsers = Object.entries(tpvm.people).filter(([, p]) => primaryFamIds.indexOf(p.familyId) > -1).map(([, p]) => p);
+ tpvm._secondaryUsers = Object.entries(tpvm.people).filter(([, p]) => secondaryFamIds.indexOf(p.familyId) > -1).map(([, p]) => p);
+ }
- // noinspection JSUnusedGlobalSymbols Called in Person.php
- static identByFamily(primaryFamIds = [], secondaryFamIds = []) {
- tpvm._plausibleUsers = Object.entries(tpvm.people).filter(([,p]) => primaryFamIds.indexOf(p.familyId) > -1).map(([,p]) => p);
- tpvm._secondaryUsers = Object.entries(tpvm.people).filter(([,p]) => secondaryFamIds.indexOf(p.familyId) > -1).map(([,p]) => p);
- }
+ static init() {
+ tpvm.trigger('Person_class_loaded');
+ }
- static init() {
- tpvm.trigger('Person_class_loaded');
- }
+ get connectedElements() {
+ const sPath = '[data-tp-person="' + this.peopleId + '"]'
+ return document.querySelectorAll(sPath);
+ }
- get connectedElements() {
- const sPath = '[data-tp-person="' + this.peopleId + '"]'
- return document.querySelectorAll(sPath);
- }
+ static mergePeopleArrays(a, b) {
+ return [...new Set([...a, ...b])]
+ }
- static mergePeopleArrays(a, b) {
- return [...new Set([...a, ...b])]
- }
+ /**
+ * Take an array of TP_Person objects and make a list of checkboxes out of them.
+ *
+ * @param array TP_Person[]
+ */
+ static peopleArrayToCheckboxes(array) {
+ let out = ""
- }
+ /**
+ * Take an array of TP_Person objects and make a list of radio buttons out of them.
+ *
+ * @param options object - an object that is a key-value map of data-value: translated-label
+ * @param array TP_Person[]
+ * @param secondaryArray TP_Person[]
+ */
+ static peopleArrayToRadio(options, array, secondaryArray = null) {
+ let out = "
"
- /**
- * Take an array of TP_Person objects and make a list of radio buttons out of them.
- *
- * @param options object - an object that is a key-value map of data-value: translated-label
- * @param array TP_Person[]
- * @param secondaryArray TP_Person[]
- */
- static peopleArrayToRadio(options, array, secondaryArray = null) {
- let out = "
"
-
- // headers
- out += "
";
- for (const oi in options) {
- if (!options.hasOwnProperty(oi)) continue;
- out += `
${options[oi]}
`
- }
- out += `
`;
-
- // people -- primary array
- for (const pi in array) {
- if (!array.hasOwnProperty(pi)) continue;
- let p = array[pi];
-
- out += '
'
+ // headers
+ out += "
";
for (const oi in options) {
if (!options.hasOwnProperty(oi)) continue;
- out += `
`;
- out += ``;
- for (const pi in secondaryArray) {
- if (!secondaryArray.hasOwnProperty(pi)) continue;
- let p = secondaryArray[pi];
+ // people -- primary array
+ for (const pi in array) {
+ if (!array.hasOwnProperty(pi)) continue;
+ let p = array[pi];
out += '
'
for (const oi in options) {
if (!options.hasOwnProperty(oi)) continue;
out += `
";
+
+ // Increment days
+ $mo1 = $d->format('n');
+ $oldDd = $d->format('d');
+ $d->add($aDay);
+ $d->setTimezone($tz);
+
+ // handle fall DST transitions.
+ while ($d->format('d') == $oldDd) {
+ $d->add($aDay);
+ $d->setTimezone($tz);
+ }
+
+ try {
+ $d = new DateTime($d->format('Y-m-d 00:00:00'), $tz);
+ $d2359 = new DateTime($d->format('Y-m-d 23:59:59'), $tz);
+ } catch (Exception) {
+ // unlikely to ever run, since the format is provided.
+ $d2359->add($aDay);
+ $d2359->setTimezone($tz);
+ }
+ $mo2 = $d->format('n');
+
+ if ($mo1 !== $mo2) {
+ if ($isMonthBefore) {
+ $isMonthBefore = false;
+ } else {
+ $isMonthAfter = true;
+ $lastDayOfMonth = $d;
+ }
+ }
+ } while (!$isMonthAfter || $d->format('w') !== '0');
+ $r .= '
';
+
+ if ($this->eventCount > 0) {
+ $this->html = $r;
+ } else {
+ // Translators: %s is the plural name of the of the Meetings, such as "Events".
+ $message = wp_sprintf(__("There are no %s published for this month.", "TouchPoint-WP"), TouchPointWP::instance()->settings->mc_name_plural);
+ $this->html = "
$message
";
+ }
+
+ $this->next = DateTimeImmutable::createFromMutable($lastDayOfMonth);
+ try {
+ $this->prev = $firstDayOfMonth->sub($aDay)->setTimezone($tz);
+ } catch (Exception) {
+ $this->prev = null;
+ }
+ }
+
+ /**
+ * Render the grid as HTML.
+ *
+ * @return string
+ */
+ public function __toString(): string
+ {
+ self::enqueueCalendarStyle();
+ return $this->html;
+ }
+
+ /**
+ * This function enqueues the stylesheet for the calendar grid.
+ */
+ public static function enqueueCalendarStyle(): void
+ {
+ wp_enqueue_style(
+ TouchPointWP::SHORTCODE_PREFIX . 'calendar-grid-style',
+ TouchPointWP::instance()->assets_url . 'template/calendar-grid-style.css',
+ [],
+ TouchPointWP::VERSION
+ );
+ }
+
+ /**
+ * This method returns a navigation bar for the calendar grid with simply next/prev month links.
+ *
+ * @param bool $withMonthName
+ * @param string $class
+ *
+ * @return string
+ */
+ public function navBar(bool $withMonthName = false, string $class=""): string
+ {
+ $r = "
";
+ $r .= "
";
+ $r .= $this->getPrevLink();
+ $r .= "
";
+
+ if ($withMonthName) {
+ $r .= "
";
+ $r .= "
$this->monthName
";
+ $r .= "
";
+ }
+
+ $r .= "
";
+ $r .= $this->getNextLink();
+ $r .= "
";
+ $r .= "
";
+
+ return $r;
+ }
+
+ /**
+ * Adjust a WP_Query object to filter only to events that overlap with the given day.
+ *
+ * @param WP_Query $q The original query object.
+ * @param DateTimeInterface $d1 The first day of the range.
+ * @param DateTimeInterface $d2 The last day of the range.
+ * @param DateTimeZone $tz The timezone to use.
+ *
+ * @return WP_Query
+ * @throws Exception
+ */
+ private static function adjustQueryForRange(WP_Query $q, DateTimeInterface $d1, DateTimeInterface $d2, DateTimeZone $tz): WP_Query
+ {
+ $q = clone $q;
+
+ $dStart = new DateTime($d1->format('Y-m-d 00:00:00'), $tz);
+ $dEnd = new DateTime($d2->format('Y-m-d 23:59:59'), $tz);
+
+ $existingMq = $q->get('meta_query');
+
+ $mq = [
+ [
+ 'key' => Meeting::MEETING_START_META_KEY,
+ 'value' => $dEnd->format('U'),
+ 'compare' => "<="
+ ],
+ [
+ [
+ 'key' => Meeting::MEETING_END_META_KEY,
+ 'value' => $dStart->format('U'),
+ 'compare' => ">="
+ ],
+ [ // This condition is to allow for the possibility of events without end times.
+ [
+ 'key' => Meeting::MEETING_END_META_KEY,
+ 'compare' => '=',
+ 'value' => 0
+ ],
+ [
+ 'key' => Meeting::MEETING_START_META_KEY,
+ 'value' => $dStart->format('U'),
+ 'compare' => ">"
+ ],
+ 'relation' => 'AND'
+ ],
+ 'relation' => 'OR'
+ ],
+ [
+ 'key' => Meeting::MEETING_META_KEY,
+ 'value' => 0,
+ 'compare' => "!="
+ ],
+ [
+ 'key' => Meeting::MEETING_IS_GROUP_MEMBER,
+ 'value' => 0,
+ 'compare' => "="
+ ],
+ 'relation' => 'AND'
+ ];
+
+ if (!empty($existingMq)) {
+ $mq = [
+ 'relation' => 'AND',
+ $existingMq,
+ $mq,
+ ];
+ }
+
+ $q->set('meta_query', $mq);
+
+ $q->set('meta_key', Meeting::MEETING_START_META_KEY);
+ $q->set('orderby', 'meta_value');
+ $q->set('order', 'ASC');
+
+ $q->set('posts_per_page', 100000);
+ $q->set('posts_per_archive_page', 100000);
+
+ $q->set('post_type', Involvement_PostTypeSettings::getPostTypes());
+
+ return $q;
+ }
+
+
+ /**
+ * Get HTML for a link to the next month.
+ *
+ * @return string
+ */
+ public function getNextLink(): string
+ {
+ return self::getLinkForDate($this->next);
+ }
+
+ /**
+ * Get HTML for a link to the previous month.
+ *
+ * @return string
+ */
+ public function getPrevLink(): string
+ {
+ return self::getLinkForDate($this->prev);
+ }
+
+ /**
+ * Get an HTML link for the period that includes the given date. This ONLY provides the URL Parameter portion, and
+ * is only meant to facilitate the next/prev links.
+ *
+ * @param DateTimeInterface $date
+ *
+ * @return string
+ */
+ protected static function getLinkForDate(DateTimeInterface $date): string
+ {
+ $link = "?page=" . $date->format('m-Y');
+ $label = self::getMonthNameForDate($date);
+ return "$label";
+ }
+
+ /**
+ * Get the name of the month for a given date, with the year if different from current year.
+ *
+ * @param DateTimeInterface $date
+ *
+ * @return string
+ */
+ protected static function getMonthNameForDate(DateTimeInterface $date): string
+ {
+ $ts = DateFormats::timestampWithoutOffset($date);
+ if ($date->format('Y') === Utilities::dateTimeNow()->format('Y')) {
+ $label = wp_date('F', $ts);
+ } else {
+ $label = wp_date('F Y', $ts);
+ }
+ return $label;
+ }
+
+
+ protected static ?CalendarGrid $defaultItem = null;
+
+ /**
+ * Get a standard calendar grid, as would be used for most applications on a standard archive page.
+ *
+ * @return CalendarGrid
+ */
+ public final static function getDefaultGrid(): CalendarGrid
+ {
+ global $wp_query;
+ if (self::$defaultItem === null) {
+ if (!isset($_GET['page']) || !preg_match('/^(?P[0-9]{2})-(?P[0-9]{4})$/', $_GET['page'], $matches)) {
+ $matches = [
+ 'mo' => null,
+ 'yr' => null
+ ];
+ }
+
+ self::$defaultItem = new CalendarGrid($wp_query, $matches['mo'], $matches['yr']);
+ }
+ return self::$defaultItem;
+ }
+
+ /**
+ * @return void
+ */
+ public static function shortcode(): void
+ {
+ if (have_posts()) {
+ global $wp_query;
+
+ $grid = self::getDefaultGrid();
+ echo $grid->navBar(true);
+ echo $grid;
+ if ($grid->eventCount > 0) {
+ echo $grid->navBar(false, 'bottom');
+ }
+
+ wp_reset_query();
+ $taxQuery = [[]];
+ $wp_query->tax_query->queries = $taxQuery;
+ $wp_query->query_vars['tax_query'] = $taxQuery;
+ $wp_query->is_tax = false; // prevents templates from thinking this is a taxonomy archive
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/TouchPoint-WP/EventsCalendar.php b/src/TouchPoint-WP/EventsCalendar.php
index 037a1302..e23d6cce 100644
--- a/src/TouchPoint-WP/EventsCalendar.php
+++ b/src/TouchPoint-WP/EventsCalendar.php
@@ -9,20 +9,41 @@
exit(1);
}
+use tp\TouchPointWP\Interfaces\api;
+use tp\TouchPointWP\Interfaces\module;
use WP_Post;
-
-if ( ! TOUCHPOINT_COMPOSER_ENABLED) {
- require_once 'api.php';
-}
+use WP_Query;
/**
- * Provides an interface to bridge the gap between The Events Calendar plugin (by ModernTribe) and the TouchPoint
+ * Provides an interface to bridge the gap between The Events Calendar plugin (by Modern Tribe) and the TouchPoint
* mobile app.
+ *
+ * This class and its features are deprecated since it will no longer be needed when mobile v2 is retired.
+ *
+ * @since 0.0.90 Deprecated. Will be removed once v2.0 apps are no longer in use, as this won't be necessary for 3.0+.
+ * @deprecated since 0.0.90 Will not be necessary once mobile 3.0 is dependable.
*/
abstract class EventsCalendar implements api, module
{
+ /**
+ * @param array $params
+ *
+ * @return array
+ *
+ * @since 0.0.2 Added
+ * @since 0.0.90 Deprecated. Will be removed once v2.0 apps are no longer in use, as this won't be necessary for 3.0+.
+ * @deprecated since 0.0.90 Will not be necessary once mobile 3.0 exists.
+ */
protected static function generateEventsList(array $params = []): array
{
+ if (TouchPointWP::instance()->settings->ec_app_cal_provider === 'meetings') {
+ return self::generateEventsListFromMeetings($params);
+ }
+
+ if (!function_exists('tribe_get_events')) {
+ return [];
+ }
+
$eventsList = [];
$params = array_merge(
@@ -39,9 +60,6 @@ protected static function generateEventsList(array $params = []): array
$usePro = TouchPointWP::useTribeCalendarPro();
- $tpDomain = TouchPointWP::instance()->settings->host;
- $dlDomain = TouchPointWP::instance()->settings->host_deeplink;
-
foreach ($eventsQ as $eQ) {
/** @var WP_Post $eQ */
global $post;
@@ -64,66 +82,249 @@ protected static function generateEventsList(array $params = []): array
$locationContent = implode(" • ", $locationContent);
$content = trim(get_the_content(null, true, $eQ->ID));
- $content = apply_filters('the_content', $content);
- $content = apply_filters(TouchPointWP::HOOK_PREFIX . 'app_events_content', $content);
-
- $content = html_entity_decode($content);
-
- // Add Header and footer Scripts, etc.
- if ($content !== '') {
- ob_start();
- do_action('wp_print_styles');
- do_action('wp_print_head_scripts');
- $content = ob_get_clean() . $content;
-
- ob_start();
- do_action('wp_print_footer_scripts');
- do_action('wp_print_scripts');
- $content .= ob_get_clean();
- }
+ $content = self::formatContent($content);
+ $content_ios = self::deeplinkReplacements($content);
- // Add domain to relative links
+ // Not needed for apps, but helpful for diagnostics
+ $eO['ID'] = $eQ->ID;
+
+ // Android (apparently not used on iOS?)
+ $eO['all_day'] = tribe_event_is_all_day($eQ->ID);
+
+ // Android
+ $eO['image'] = get_the_post_thumbnail_url($eQ->ID, 'large');
+ // iOS
+ $eO['RelatedImageFileKey'] = $eO['image'];
+
+ // iOS
+ $eO['Description'] = str_replace("{{MOBILE_OS}}", "iOS", $content_ios);
+ // Android
+ $eO['content'] = str_replace("{{MOBILE_OS}}", "android", $content);
+
+ // iOS
+ $eO['Subject'] = $eQ->post_title;
+ // Android
+ $eO['title'] = $eQ->post_title;
+
+ // iOS
+ $eO['StartDateTime'] = tribe_get_start_date($eQ->ID, true, 'c');
+ // Android
+ $eO['start_date'] = $eO['StartDateTime'];
+
+ // iOS
+ $eO['Location'] = $locationContent;
+ // Android
+ $eO['room'] = $locationContent;
+
+ $eventsList[] = $eO;
+ }
+
+ return $eventsList;
+ }
+
+ private static function deeplinkReplacements(string $content): string
+ {
+ $tpDomain = TouchPointWP::instance()->settings->host;
+ $dlDomain = TouchPointWP::instance()->settings->host_deeplink;
+
+ // Replace TouchPoint links with deeplinks where applicable
+ // Registration Links
+ if ($tpDomain !== '' && $dlDomain !== '') {
$content = preg_replace(
- "/['\"]\/([^\/\"']*)[\"']/i",
- '"' . get_home_url() . '/$1"',
+ "/:\/\/$tpDomain\/OnlineReg\/([\d]+)/i",
+ "://" . $dlDomain . '/registrations/register/${1}?from={{MOBILE_OS}}',
$content
);
+ }
+
+ return $content;
+ }
+
+ private static function formatContent(?string $content): string
+ {
+ $content = apply_filters('the_content', $content);
+
+ /**
+ * Allows for manipulation of the html returned to the calendar feature of 2.0 Mobile apps.
+ *
+ * @since 0.0.2 Added
+ * @since 0.0.90 Deprecated
+ * @deprecated 0.0.90 Will be going away with Mobile App version 2.0
+ *
+ * @param string $content The html thus far.
+ */
+ $content = apply_filters('tp_app_events_content', $content);
+ $content = html_entity_decode($content);
+
+ // Add Header and footer Scripts, etc.
+ if ($content !== '') {
+ ob_start();
+ do_action('wp_print_styles');
+ do_action('wp_print_head_scripts');
+ $content = ob_get_clean() . $content;
+
+ ob_start();
+ do_action('wp_print_footer_scripts');
+ do_action('wp_print_scripts');
+ $content .= ob_get_clean();
+ }
+
+ // Add domain to relative links
+ $content = preg_replace(
+ "/['\"]\/([^\/\"']*)[\"']/i",
+ '"' . get_home_url() . '/$1"',
+ $content
+ );
- // Replace TouchPoint links with deeplinks where applicable
- // Registration Links
- if ($tpDomain !== '' && $dlDomain !== '') {
- $content = preg_replace(
- "/:\/\/$tpDomain\/OnlineReg\/([\d]+)/i",
- "://" . $dlDomain . '/registrations/register/${1}?from={{MOBILE_OS}}',
- $content
- );
+ // Deeplink replacements should really happen here if they were dependable on Android.
+
+ if ($content !== '') {
+ $cssUrl = null;
+ if (TouchPointWP::instance()->settings->ec_use_standardizing_style === 'on') {
+ $cssUrl = TouchPointWP::instance(
+ )->assets_url . 'template/ec-standardizing-style.css?v=' . TouchPointWP::VERSION;
}
- if ($content !== '') {
- $cssUrl = null;
- if (TouchPointWP::instance()->settings->ec_use_standardizing_style === 'on') {
- $cssUrl = TouchPointWP::instance(
- )->assets_url . 'template/ec-standardizing-style.css?v=' . TouchPointWP::VERSION;
- }
- $cssUrl = apply_filters(TouchPointWP::HOOK_PREFIX . 'app_events_css_url', $cssUrl);
- if (is_string($cssUrl)) {
- $content = "" . $content;
- }
+ /**
+ * Insert a CSS file into all event content for mobile 2.0 app.
+ *
+ * @since 0.0.3 Added
+ * @since 0.0.90 Deprecated
+ * @deprecated 0.0.90 Will be going away with Mobile App version 2.0
+ *
+ * @param string $cssUrl The url for a CSS file. By default, one provided with the plugin is used.
+ */
+ $cssUrl = apply_filters('tp_app_events_css_url', $cssUrl);
+ if (is_string($cssUrl)) {
+ $content = "" . $content;
}
+ }
+
+ return $content;
+ }
+
+ /**
+ * @param array $params
+ *
+ * @return array
+ *
+ * @since 0.0.90 Added and Deprecated. Will be removed once v2.0 apps are no longer in use, as this won't be necessary for 3.0+.
+ * @deprecated
+ */
+ protected static function generateEventsListFromMeetings(array $params = []): array
+ {
+ $eventsList = [];
+
+ $q = new WP_Query();
+
+ $existingMq = $q->get('meta_query');
+
+ $now = Utilities::dateTimeNow();
+
+ $mq = [
+ [
+ [
+ 'key' => Meeting::MEETING_END_META_KEY,
+ 'value' => $now->format('U'),
+ 'compare' => ">="
+ ],
+ [ // This condition is to allow for the possibility of events without end times.
+ [
+ 'key' => Meeting::MEETING_END_META_KEY,
+ 'compare' => '=',
+ 'value' => 0
+ ],
+ [
+ 'key' => Meeting::MEETING_START_META_KEY,
+ 'value' => $now->format('U'),
+ 'compare' => ">"
+ ],
+ 'relation' => 'AND'
+ ],
+ 'relation' => 'OR'
+ ],
+ [
+ 'key' => Meeting::MEETING_META_KEY,
+ 'value' => 0,
+ 'compare' => ">"
+ ],
+ 'relation' => 'AND'
+ ];
+
+ if (!empty($existingMq)) {
+ $mq = [
+ 'relation' => 'AND',
+ $existingMq,
+ $mq,
+ ];
+ }
+
+ $q->set('meta_query', $mq);
+
+ $q->set('meta_key', Meeting::MEETING_START_META_KEY);
+ $q->set('orderby', 'meta_value');
+ $q->set('order', 'ASC');
+ $q->set('posts_per_page', 200);
+
+ $q->set('post_type', Involvement_PostTypeSettings::getPostTypes());
+
+ $iids = [];
+ $count = 0;
+
+ foreach ($q->get_posts() as $eQ) {
+ /** @var WP_Post $eQ */
+ global $post;
+ $post = $eQ;
+
+ try {
+ $e = Meeting::fromPost($eQ);
+ } catch (TouchPointWP_Exception) {
+ continue;
+ }
+
+ $iid = $e->involvementId();
+ if (in_array($iid, $iids)) {
+ continue;
+ }
+ $iids[] = $iid;
+
+ $eO = [];
+
+ $locationContent = [];
+ $separator = " • ";
+
+ $location = $e->locationName();
+ if ($location !== '' && $location !== null) {
+ $locationContent[] = $location;
+ }
+// $sstring = $e->scheduleString($separator);
+// if ($sstring) {
+// $locationContent[] = html_entity_decode($sstring);
+// }
+ if ($e->isMultiDay()) {
+ $locationContent[] = __("Multi-Day", "TouchPoint-WP");
+ }
+ $locationContent = implode($separator, $locationContent);
+
+ $content = trim(get_the_content(null, true, $eQ->ID));
+ $content .= "
";
+ $content = self::formatContent($content);
+ $content_ios = self::deeplinkReplacements($content);
// Not needed for apps, but helpful for diagnostics
$eO['ID'] = $eQ->ID;
+ $eO['IID'] = $iid;
// Android (apparently not used on iOS?)
- $eO['all_day'] = tribe_event_is_all_day($eQ->ID);
+ $eO['all_day'] = $e->isAllDay();
// Android
- $eO['image'] = get_the_post_thumbnail_url($eQ->ID, 'large');
+ $eO['image'] = get_the_post_thumbnail_url($eQ, 'large');
// iOS
$eO['RelatedImageFileKey'] = $eO['image'];
// iOS
- $eO['Description'] = str_replace("{{MOBILE_OS}}", "iOS", $content);
+ $eO['Description'] = str_replace("{{MOBILE_OS}}", "iOS", $content_ios);
// Android
$eO['content'] = str_replace("{{MOBILE_OS}}", "android", $content);
@@ -133,7 +334,7 @@ protected static function generateEventsList(array $params = []): array
$eO['title'] = $eQ->post_title;
// iOS
- $eO['StartDateTime'] = tribe_get_start_date($eQ->ID, true, 'c');
+ $eO['StartDateTime'] = $e->startDt->format('c');
// Android
$eO['start_date'] = $eO['StartDateTime'];
@@ -152,6 +353,10 @@ protected static function generateEventsList(array $params = []): array
* Print json for Events Calendar for Mobile app.
*
* @param array $params Parameters from the request to use for filtering or such.
+ *
+ * @since 0.0.2 Added
+ * @since 0.0.90 Deprecated. Will be removed once v2.0 apps are no longer in use, as this won't be necessary for 3.0+.
+ * @deprecated since 0.0.90 Will not be necessary once mobile 3.0 exists.
*/
protected static function echoAppList(array $params = []): void
{
@@ -166,6 +371,11 @@ protected static function echoAppList(array $params = []): void
* Generate previews of the HTML generated for the App Events Calendar
*
* This is wildly inefficient since each iframe will calculate the full list.
+ *
+ * @param array $params Parameters from the request to use for filtering or such.
+ * @since 0.0.90 Added
+ * @since 0.0.90 Deprecated. Will be removed once v2.0 apps are no longer in use, as this won't be necessary for 3.0+.
+ * @deprecated since 0.0.90 Will not be necessary once mobile 3.0 exists.
*/
protected static function previewAppList(array $params = []): void
{
@@ -174,12 +384,24 @@ protected static function previewAppList(array $params = []): void
foreach ($eventsList as $i => $eo) {
echo "
{$eo['title']}
";
$url = get_site_url() . "/" .
- TouchPointWP::API_ENDPOINT . "/" .
- TouchPointWP::API_ENDPOINT_APP_EVENTS . "/" . $i;
+ TouchPointWP::API_ENDPOINT . "/" .
+ TouchPointWP::API_ENDPOINT_APP_EVENTS . "/" . $i;
echo "";
}
}
+ /**
+ * Generate previews of the HTML generated for one item on the 2.0 app events calendar.
+ *
+ * This is wildly inefficient since each iframe will calculate the full list.
+ *
+ * @param array $params Parameters from the request to use for filtering or such.
+ * @param int $item The item to preview
+ *
+ * @since 0.0.90 Added
+ * @since 0.0.90 Deprecated. Will be removed once v2.0 apps are no longer in use, as this won't be necessary for 3.0+.
+ * @deprecated since 0.0.90 Will not be necessary once mobile 3.0 exists.
+ */
protected static function previewAppListItem(array $params = [], int $item = 0): void
{
$eventsList = self::generateEventsList($params);
@@ -187,6 +409,17 @@ protected static function previewAppListItem(array $params = [], int $item = 0):
echo $eventsList[$item]['content'];
}
+ /**
+ * Handle API requests
+ *
+ * @param array $uri The request URI already parsed by parse_url()
+ *
+ * @return bool False if endpoint is not found. Should print the result.
+ *
+ * @since 0.0.2 Added
+ * @since 0.0.90 Deprecated. Will be removed once v2.0 apps are no longer in use, as this won't be necessary for 3.0+.
+ * @deprecated since 0.0.90 Will not be necessary once mobile 3.0 exists.
+ */
public static function api(array $uri): bool
{
if (count($uri['path']) === 2) {
@@ -197,8 +430,8 @@ public static function api(array $uri): bool
// Preview list
if (count($uri['path']) === 3 &&
- strtolower($uri['path'][2]) === 'preview' &&
- TouchPointWP::currentUserIsAdmin()
+ strtolower($uri['path'][2]) === 'preview' &&
+ TouchPointWP::currentUserIsAdmin()
) {
EventsCalendar::previewAppList($uri['query']);
exit;
@@ -206,8 +439,8 @@ public static function api(array $uri): bool
// Preview items
if (count($uri['path']) === 3 &&
- is_numeric($uri['path'][2]) &&
- TouchPointWP::currentUserIsAdmin()
+ is_numeric($uri['path'][2]) &&
+ TouchPointWP::currentUserIsAdmin()
) {
EventsCalendar::previewAppListItem($uri['query'], intval($uri['path'][2]));
exit;
diff --git a/src/TouchPoint-WP/ExtraValueHandler.php b/src/TouchPoint-WP/ExtraValueHandler.php
index b2bd8741..9718c4af 100644
--- a/src/TouchPoint-WP/ExtraValueHandler.php
+++ b/src/TouchPoint-WP/ExtraValueHandler.php
@@ -8,11 +8,13 @@
use DateTime;
/**
- * Manages the handling of Extra Values. Items that support Extra Values MUST use the ExtraValues Trait.
+ * Classes that have extra values should implement the ExtraValues trait, which provides a ExtraValues() method which
+ * returns an instance of this class and allows getting the values via its getter. That trait has the abstract methods
+ * that need to be implemented for Extra Values to be available.
*/
class ExtraValueHandler
{
- /** @var object|extraValues $owner */
+ /** @var object|extraValues $owner The object that has extra values */
protected object $owner; // Must have the extraValues trait.
/**
diff --git a/src/TouchPoint-WP/Utilities/Geo.php b/src/TouchPoint-WP/Geo.php
similarity index 52%
rename from src/TouchPoint-WP/Utilities/Geo.php
rename to src/TouchPoint-WP/Geo.php
index 618b210c..d28d54dc 100644
--- a/src/TouchPoint-WP/Utilities/Geo.php
+++ b/src/TouchPoint-WP/Geo.php
@@ -3,14 +3,28 @@
* @package TouchPointWP
*/
-namespace tp\TouchPointWP\Utilities;
+namespace tp\TouchPointWP;
+
+use stdClass;
+
/**
- * Utility class for geographical attributes and calculations. Not to be confused with the geo interface.
- * @see \tp\TouchPointWP\geo
+ * A standardized set of fields for geographical information.
*/
-abstract class Geo
+class Geo extends stdClass
{
+ public ?float $lat = null;
+ public ?float $lng = null;
+ public ?string $human = null;
+ public ?string $type = null;
+
+ public function __construct(?float $lat = null, ?float $lng = null, ?string $human = null, ?string $type = null) {
+ $this->lat = $lat;
+ $this->lng = $lng;
+ $this->human = $human;
+ $this->type = $type;
+ }
+
/**
* Get distance between two geographic points by lat/lng pairs. Returns a number in miles.
*
@@ -28,8 +42,11 @@ public static function distance(float $latA, float $lngA, float $latB, float $ln
$latB_r = deg2rad($latB);
$lngB_r = deg2rad($lngB);
- return round(3959 * acos(
+ return round(
+ 3959 * acos(
cos($latA_r) * cos($latB_r) * cos($lngB_r - $lngA_r) + sin($latA_r) * sin($latB_r)
- ), 1);
+ ),
+ 1
+ );
}
}
\ No newline at end of file
diff --git a/src/TouchPoint-WP/Interfaces/actionButtons.php b/src/TouchPoint-WP/Interfaces/actionButtons.php
new file mode 100644
index 00000000..40fb2c85
--- /dev/null
+++ b/src/TouchPoint-WP/Interfaces/actionButtons.php
@@ -0,0 +1,24 @@
+post = $object;
$this->name = $object->post_title;
- $this->invId = intval($object->{self::INVOLVEMENT_META_KEY});
+ $this->invId = intval($object->{TouchPointWP::INVOLVEMENT_META_KEY});
$this->post_id = $object->ID;
$this->invType = get_post_type($this->post_id);
if ($this->invId === 0) {
- throw new TouchPointWP_Exception("No Involvement ID provided in the post.");
+ throw new TouchPointWP_Exception("No Involvement ID provided in the post.", 171002);
}
} elseif (gettype($object) === "object") {
// Sql Object, probably.
@@ -122,7 +137,7 @@ protected function __construct(object $object)
_doing_it_wrong(
__FUNCTION__,
esc_html(
- __('Creating an Involvement object from an object without a post_id is not yet supported.')
+ __('Creating an Involvement object from an object without a post_id is not yet supported.', 'TouchPoint-WP')
),
esc_attr(TouchPointWP::VERSION)
);
@@ -143,21 +158,21 @@ protected function __construct(object $object)
}
// clean up involvement type to not have hook prefix, if it does.
- if (strpos($this->invType, TouchPointWP::HOOK_PREFIX) === 0) {
+ if (str_starts_with($this->invType, TouchPointWP::HOOK_PREFIX)) {
$this->invType = substr($this->invType, strlen(TouchPointWP::HOOK_PREFIX));
}
$postTerms = [
- TouchPointWP::TAX_RESCODE,
- TouchPointWP::TAX_AGEGROUP,
- TouchPointWP::TAX_WEEKDAY,
- TouchPointWP::TAX_TENSE,
- TouchPointWP::TAX_DAYTIME,
- TouchPointWP::TAX_INV_MARITAL,
- TouchPointWP::TAX_DIV
+ Taxonomies::TAX_RESCODE,
+ Taxonomies::TAX_AGEGROUP,
+ Taxonomies::TAX_WEEKDAY,
+ Taxonomies::TAX_TENSE,
+ Taxonomies::TAX_DAYTIME,
+ Taxonomies::TAX_INV_MARITAL,
+ Taxonomies::TAX_DIV
];
if (TouchPointWP::instance()->settings->enable_campuses === "on") {
- $postTerms[] = TouchPointWP::TAX_CAMPUS;
+ $postTerms[] = Taxonomies::TAX_CAMPUS;
}
$terms = wp_get_post_terms(
@@ -174,7 +189,7 @@ protected function __construct(object $object)
'slug' => $t->slug
];
$ta = $t->taxonomy;
- if (strpos($ta, TouchPointWP::HOOK_PREFIX) === 0) {
+ if (str_starts_with($ta, TouchPointWP::HOOK_PREFIX)) {
$ta = substr_replace($ta, "", 0, $hookLength);
}
if ( ! isset($this->attributes->$ta)) {
@@ -200,7 +215,11 @@ protected function __construct(object $object)
continue;
}
if (property_exists(self::class, $k)) { // properties
- $this->$k = maybe_unserialize($v[0]);
+ try {
+ $this->$k = maybe_unserialize($v[0]);
+ } catch (TypeError $e) {
+ new TouchPointWP_Exception($e);
+ }
}
}
@@ -232,7 +251,7 @@ protected function __construct(object $object)
}
// Color!
- $this->color = Utilities::getColorFor("default", "involvement");
+ $this->color = Utilities\Colors::getColorFor("default", "involvement");
}
$this->registerConstruction();
@@ -255,8 +274,6 @@ final protected static function &allTypeSettings(): array
public static function init(): void
{
foreach (self::allTypeSettings() as $type) {
- /** @var $type Involvement_PostTypeSettings */
-
register_post_type(
$type->postType,
[
@@ -265,7 +282,8 @@ public static function init(): void
'singular_name' => $type->nameSingular
],
'public' => true,
- 'hierarchical' => $type->hierarchical,
+ 'hierarchical' => $type->hierarchical || $type->importMeetings ||
+ TouchPointWP::instance()->settings->enable_meeting_cal === 'on',
'show_ui' => false,
'show_in_nav_menus' => true,
'show_in_rest' => true,
@@ -283,21 +301,31 @@ public static function init(): void
],
'query_var' => $type->slug,
'can_export' => false,
- 'delete_with_user' => false
+ 'delete_with_user' => false,
+ 'capability_type' => 'post',
+ 'capabilities' => [
+ 'create_posts' => 'do_not_allow', // Disable creating new posts
+ 'edit_posts' => 'do_not_allow', // Disable editing posts
+ 'edit_others_posts' => 'do_not_allow', // Disable editing others' posts
+ 'delete_posts' => 'do_not_allow', // Disable deleting posts
+ 'delete_others_posts' => 'do_not_allow', // Disable deleting others' posts
+ 'publish_posts' => 'do_not_allow', // Disable publishing posts
+ ],
+ 'map_meta_cap' => true, // Ensure users can still view posts
]
);
}
// Register default templates for Involvements
- add_filter('template_include', [self::class, 'templateFilter']);
+ add_filter('template_include', [self::class, 'templateFilter'], 10, 1);
// Register function to return schedule instead of publishing date
add_filter('get_the_date', [self::class, 'filterPublishDate'], 10, 3);
add_filter('get_the_time', [self::class, 'filterPublishDate'], 10, 3);
// Register function to return leaders instead of authors
- add_filter('the_author', [self::class, 'filterAuthor'], 10, 3);
- add_filter('get_the_author_display_name', [self::class, 'filterAuthor'], 10, 3);
+ add_filter('the_author', [self::class, 'filterAuthor'], 10, 1);
+ add_filter('get_the_author_display_name', [self::class, 'filterAuthor'], 10, 1);
}
public static function checkUpdates(): void
@@ -333,20 +361,37 @@ public static function checkUpdates(): void
*
* @param bool $verbose Whether to print debugging info.
*
- * @return false|int False on failure, or the number of groups that were updated or deleted.
+ * @return int False on failure, or the number of groups that were updated or deleted.
*/
- public static function updateFromTouchPoint(bool $verbose = false)
+ public final static function updateFromTouchPoint(bool $verbose = false, bool $applyChanges = true): int
{
$count = 0;
$success = true;
+ $startTime = microtime(true);
+
// Prevent other threads from attempting for an hour.
- TouchPointWP::instance()->settings->set('inv_cron_last_run', time() - self::CRON_OFFSET + 3600);
+ if ($applyChanges) {
+ TouchPointWP::instance()->settings->set('inv_cron_last_run', time() + 3600);
+ }
+
+ // Any user can preview changes, only admins can apply them.
+ $verbose &= TouchPointWP::currentUserIsAdmin() | (TouchPointWP::currentUserPerson() !== null && !$applyChanges);
+
+ ini_set('max_execution_time', 300);
+ ini_set('memory_limit', '512M');
+ if (!defined('WP_MAX_MEMORY_LIMIT')) {
+ define('WP_MAX_MEMORY_LIMIT', '512M');
+ }
- $verbose &= TouchPointWP::currentUserIsAdmin();
foreach (self::allTypeSettings() as $type) {
- if (count($type->importDivs) < 1) {
+
+ if ($verbose) {
+ echo "
$type->namePlural
";
+ }
+
+ if (count($type->importDivs) < 1 && $type->postType !== Meeting::POST_TYPE) {
// Don't update if there aren't any divisions selected yet.
if ($verbose) {
print "Skipping $type->namePlural because no divisions are selected.";
@@ -355,14 +400,17 @@ public static function updateFromTouchPoint(bool $verbose = false)
}
// Divisions
- $divs = Utilities::idArrayToIntArray($type->importDivs, false);
+ $update = false;
try {
- $update = self::updateInvolvementPostsForType($type, $divs, $verbose);
+ TouchPointWP::instance()->setTpWpUserAsCurrent();
+ $update = self::updateInvolvementPostsForType($type, $verbose, $applyChanges);
} catch (Exception $e) {
if ($verbose) {
echo "An exception occurred while syncing $type->namePlural: " . $e->getMessage();
}
continue;
+ } finally {
+ TouchPointWP::instance()->unsetTpWpUserAsCurrent();
}
if ($update === false) {
@@ -370,13 +418,20 @@ public static function updateFromTouchPoint(bool $verbose = false)
} else {
$count += $update;
}
+
+ if ($verbose) {
+ $time = microtime(true) - $startTime;
+ echo "
$time seconds have elapsed.
";
+ }
}
unset($type);
- if ($success && $count !== 0) {
- TouchPointWP::instance()->settings->set('inv_cron_last_run', time());
- } else {
- TouchPointWP::instance()->settings->set('inv_cron_last_run', 0);
+ if ($applyChanges) {
+ if ($success && $count !== 0) {
+ TouchPointWP::instance()->settings->set('inv_cron_last_run', time());
+ } else {
+ TouchPointWP::instance()->settings->set('inv_cron_last_run', 0);
+ }
}
if ($verbose) {
@@ -400,28 +455,48 @@ public static function updateFromTouchPoint(bool $verbose = false)
*/
public static function templateFilter(string $template): string
{
- if (apply_filters(TouchPointWP::HOOK_PREFIX . 'use_default_templates', true, self::class)) {
+ $className = self::class;
+ $useTemplates = true;
+
+ /**
+ * Determines whether the plugin's default templates should be used. Theme developers can return false in this
+ * filter to prevent the default templates from applying, especially if they conflict with the theme.
+ *
+ * Default is true.
+ *
+ * @since 0.0.6 Added
+ *
+ * @param bool $value The value to return. True will allow the default templates to be applied.
+ * @param string $className The name of the class calling for the template.
+ */
+ if (!!apply_filters('tp_use_default_templates', $useTemplates, $className)) {
$postTypesToFilter = Involvement_PostTypeSettings::getPostTypes();
- $templateFilesToOverwrite = TouchPointWP::TEMPLATES_TO_OVERWRITE;
+ $templateFilesToOverwrite = self::TEMPLATES_TO_OVERWRITE;
if (count($postTypesToFilter) == 0) {
return $template;
}
- if ( ! in_array(ltrim(strrchr($template, '/'), '/'), $templateFilesToOverwrite)) {
+ if (!in_array(ltrim(strrchr($template, '/'), '/'), $templateFilesToOverwrite)) {
return $template;
}
+ if (is_post_type_archive(Meeting::POST_TYPE) && file_exists(
+ TouchPointWP::$dir . '/src/templates/meeting-archive.php'
+ )) {
+ return TouchPointWP::$dir . '/src/templates/meeting-archive.php';
+ }
+
if (is_post_type_archive($postTypesToFilter) && file_exists(
TouchPointWP::$dir . '/src/templates/involvement-archive.php'
)) {
- $template = TouchPointWP::$dir . '/src/templates/involvement-archive.php';
+ return TouchPointWP::$dir . '/src/templates/involvement-archive.php';
}
if (is_singular($postTypesToFilter) && file_exists(
TouchPointWP::$dir . '/src/templates/involvement-single.php'
)) {
- $template = TouchPointWP::$dir . '/src/templates/involvement-single.php';
+ return TouchPointWP::$dir . '/src/templates/involvement-single.php';
}
}
@@ -435,50 +510,163 @@ public static function templateFilter(string $template): string
* @return bool|string True if involvement can be joined. False if no registration exists. Or, a string with why
* it can't be joined otherwise.
*/
- public function acceptingNewMembers()
+ public function acceptingNewMembers(): bool|string
{
- if (get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "groupFull", true) === '1') {
- return __("Currently Full", 'TouchPoint-WP');
- }
+ if (!isset($this->_acceptingNewMembers)) {
+ if (get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "groupFull", true) === '1') {
+ $this->_acceptingNewMembers = __("Currently Full", 'TouchPoint-WP');
+ return $this->_acceptingNewMembers;
+ }
- if (get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "groupClosed", true) === '1') {
- return __("Currently Closed", 'TouchPoint-WP');
- }
+ if (get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "groupClosed", true) === '1') {
+ $this->_acceptingNewMembers = __("Currently Closed", 'TouchPoint-WP');
+ return $this->_acceptingNewMembers;
+ }
- $now = current_datetime();
- $regStart = get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "regStart", true);
- if ($regStart !== false && $regStart !== '' && $regStart > $now) {
- return __("Registration Not Open Yet", 'TouchPoint-WP');
- }
+ $now = Utilities::dateTimeNow();
+ $regStart = get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "regStart", true);
+ if ($regStart !== false && $regStart !== '' && $regStart > $now) {
+ $this->_acceptingNewMembers = __("Registration Not Open Yet", 'TouchPoint-WP');
+ return $this->_acceptingNewMembers;
+ }
+
+ $regEnd = get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "regEnd", true);
+ if ($regEnd !== false && $regEnd !== '' && $regEnd < $now) {
+ $this->_acceptingNewMembers = __("Registration Closed", 'TouchPoint-WP');
+ return $this->_acceptingNewMembers;
+ }
+
+ if (intval(get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "regTypeId", true)) === 0) {
+ $this->_acceptingNewMembers = false; // no online registration available
+ return $this->_acceptingNewMembers;
+ }
- $regEnd = get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "regEnd", true);
- if ($regEnd !== false && $regEnd !== '' && $regEnd < $now) {
- return __("Registration Closed", 'TouchPoint-WP');
+ $this->_acceptingNewMembers = true;
}
- if (intval(get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "regTypeId", true)) === 0) {
- return false; // no online registration available
+ return $this->_acceptingNewMembers;
+ }
+ private mixed $_acceptingNewMembers;
+
+
+ /**
+ * Gets a URL for registration. A registration url will be provided unless there is no viable registration url to
+ * send users to, in which case null will be returned.
+ *
+ * @return ?string
+ */
+ public function getRegistrationUrl(): ?string
+ {
+ if ($this->_registrationUrl === "") {
+ $regUrl = get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "regUrl", true);
+ $regType = intval(get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "regTypeId", true));
+ $hasRegQuestions = intval(get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "hasRegQuestions", true));
+ if ($hasRegQuestions && ($regUrl === "" || $regUrl === null)) {
+ $this->_registrationUrl = TouchPointWP::instance()->host() . "/OnlineReg/" . $this->invId;
+ } elseif ($regType === 0 || $regUrl === "" || $regUrl === null) {
+ $this->_registrationUrl = null;
+ } elseif (stristr($regUrl, "http") === false) {
+ $this->_registrationUrl = TouchPointWP::instance()->host() . $regUrl;
+ } else {
+ $this->_registrationUrl = $regUrl;
+ }
}
+ return $this->_registrationUrl;
+ }
+ private ?string $_registrationUrl = "";
- return true;
+
+ /**
+ * Get the registration type for the involvement.
+ *
+ * @return int enum of RegistrationType
+ */
+ public function getRegistrationType(): int
+ {
+ if (!isset($this->_registrationType)) {
+ if ($this->acceptingNewMembers() !== true) {
+ $this->_registrationType = RegistrationType::CLOSED;
+ return $this->_registrationType;
+ }
+
+ // Determine intended indication based on site setting.
+ $siteRegType = intval(get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "siteRegTypeId", true));
+ $regUrl = $this->getRegistrationUrl();
+ switch ($siteRegType) {
+ case 1:
+ $this->_registrationType = RegistrationType::FORM;
+ return $this->_registrationType;
+
+ case 3:
+ $this->_registrationType = RegistrationType::RSVP;
+ return $this->_registrationType;
+
+ case 5:
+ if ($regUrl !== null) {
+ $this->_registrationType = RegistrationType::EXTERNAL;
+ return $this->_registrationType;
+ }
+ break; // Registration isn't possible at a link, therefore, assume there was a mistake and continue.
+
+
+ case 7:
+ $this->_registrationType = RegistrationType::CLOSED;
+ return $this->_registrationType;
+ }
+
+ // If the involvement has a redirection link, assume it's an external form
+ if ($regUrl !== null) {
+ $this->_registrationType = RegistrationType::EXTERNAL;
+ return $this->_registrationType;
+ }
+
+
+ // If the involvement has registration questions, assume it's a form.
+ if (
+ // Has registration questions
+ intval(get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "hasRegQuestions", true)) === 1 ||
+
+ // Has registration type not equal to Join
+ intval(get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "regTypeId", true)) !== 1
+ ) {
+ $this->_registrationType = RegistrationType::FORM;
+ return $this->_registrationType;
+ }
+
+ $this->_registrationType = RegistrationType::JOIN;
+ }
+ return $this->_registrationType;
}
+ private int $_registrationType;
+
/**
* Whether the involvement should link to a registration form, rather than directly joining the org.
*
+ * @since 0.0.90 Deprecated
+ * @deprecated 0.0.90 Does not take into account all the possible registration types; will be removed in a future
+ * version.
+ *
+ * @noinspection PHPUnused
* @return bool
*/
public function useRegistrationForm(): bool
{
if (!isset($this->_useRegistrationForm)) {
$this->_useRegistrationForm = (get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "hasRegQuestions", true) === '1' ||
- intval(get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "regTypeId", true)) !== 1);
+ intval(get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "regTypeId", true)) !== 1);
}
return $this->_useRegistrationForm;
}
private bool $_useRegistrationForm;
+
/**
+ * Get an array of objects that correspond to key details of meetings. Does NOT return the actual Meeting objects.
+ * Since this is used for involvements regardless of whether their meetings are imported, this pulls from the object
+ * array that's imported directly from the API. It does not take into account Meeting objects, or meetings that
+ * belong to child involvements.
+ *
* @return stdClass[]
*/
protected function meetings(): array
@@ -488,6 +676,14 @@ protected function meetings(): array
if ($m === "") {
$m = [];
}
+
+ // Make sure items are unique. #204
+ $m = array_unique($m, SORT_REGULAR);
+
+ usort($m, function ($a, $b) {
+ return $a->mtgStartDt <=> $b->mtgStartDt;
+ });
+
$this->_meetings = $m;
}
@@ -511,49 +707,93 @@ protected function schedules(): array
}
/**
- * Get a description of the meeting schedule in a human-friendly phrase, e.g. Sundays at 11:00am, starting January
- * 14.
+ * Get the parent of this object **which may be an object of a different class**.
+ *
+ * Returns null if there is no parent.
*
- * This is separated out to a static method to prevent involvement from being instantiated (with those database hits)
- * when the content is cached. (10x faster or more)
+ * @return Involvement|null
+ */
+ public function getParent(): ?Involvement
+ {
+ if ($this->parentPostId === null) {
+ $this->parentPostId = $this->post->post_parent;
+ if ($this->parentPostId > 0) {
+ $parent = get_post($this->parentPostId);
+ if ($parent !== null) {
+ try {
+ $this->parentObject = self::fromPost($parent);
+ } catch (TouchPointWP_Exception) {
+ $this->parentObject = null;
+ }
+ }
+ }
+ }
+ return $this->parentObject;
+ }
+ protected ?int $parentPostId = null;
+ protected ?self $parentObject = null;
+
+ /**
+ * Get the several different strings that can be used to describe the start/end/etc of this involvement.
*
* @param int $invId
* @param ?Involvement $inv
*
- * @return string
+ * @return ?string[]
*/
- public static function scheduleString(int $invId, $inv = null): ?string
+ protected static function scheduleStrings(int $invId, $inv = null): ?array
{
- $cacheKey = $invId . "_" . get_locale();
+ if (isset($inv->_scheduleStrings)) {
+ return $inv->_scheduleStrings;
+ }
+
+ $cacheKey = $invId . "_" . get_locale() . "_v2";
$schStr = wp_cache_get($cacheKey, self::SCHEDULE_STRING_CACHE_GROUP);
- if (!! $schStr) {
+ if (!! $schStr && self::ENABLE_SCHEDULE_STRING_CACHE) {
return $schStr;
}
if (! $inv) {
try {
$inv = self::fromInvId($invId);
- } catch (TouchPointWP_Exception $e) {
+ } catch (TouchPointWP_Exception) {
return null;
}
}
- if (! isset($inv->_scheduleString)) {
- $inv->_scheduleString = $inv->scheduleString_calc();
- wp_cache_set(
- $cacheKey,
- $inv->_scheduleString,
- self::SCHEDULE_STRING_CACHE_GROUP,
- self::SCHEDULE_STRING_CACHE_EXPIRATION
- );
- }
- return $inv->_scheduleString;
+ $inv->_scheduleStrings = $inv->scheduleStrings_calc();
+ wp_cache_set(
+ $cacheKey,
+ $inv->_scheduleStrings,
+ self::SCHEDULE_STRING_CACHE_GROUP,
+ self::SCHEDULE_STRING_CACHE_EXPIRATION
+ );
+ return $inv->_scheduleStrings;
+ }
+
+
+ /**
+ * Get a description of the meeting schedule in a human-friendly phrase, e.g. Sundays at 11:00am, starting January
+ * 14.
+ *
+ * This is separated out to a static method to prevent involvement from being instantiated (with those database
+ * hits) when the content is cached. (10x faster or more)
+ *
+ * @param int $objId Involvement Id.
+ * @param ?Involvement $obj
+ *
+ * @return ?string
+ */
+ public static function scheduleString(int $objId, $obj = null): ?string
+ {
+ $s = self::scheduleStrings($objId, $obj);
+ return $s['combined'];
}
/**
* Get the next meeting date/time from either the meetings or schedules.
*
- * @return DateTimeImmutable|null
+ * @return DateTimeExtended|null
*/
- public function nextMeeting(): ?DateTimeImmutable
+ public function nextMeeting(): ?DateTimeExtended
{
$now = new DateTimeImmutable();
$this->_nextMeeting = null;
@@ -561,33 +801,27 @@ public function nextMeeting(): ?DateTimeImmutable
if ($this->_nextMeeting === null) {
// meetings
foreach ($this->meetings() as $m) {
- $mdt = $m->dt;
+ $mdt = $m->mtgStartDt;
if ($mdt > $now) {
+ // meetings are sorted, so first one past now is the next meeting
if ($this->_nextMeeting === null || $mdt < $this->_nextMeeting) {
$this->_nextMeeting = $mdt;
+ break;
}
}
}
// schedules
foreach ($this->schedules() as $s) {
- $mdt = $s->next;
- if ($mdt > $now) {
- if ($this->_nextMeeting === null || $mdt < $this->_nextMeeting) {
- $this->_nextMeeting = $mdt;
- }
+ $mdt = $s->nextStartDt;
+ if ($mdt === null) {
+ continue;
}
- }
- }
-
- // schedules + 1 week (assumes schedules are recurring weekly)
- if ($this->_nextMeeting === null) { // really only needed if we don't have a date yet.
- foreach ($this->schedules() as $s) {
- $mdt = $s->next->modify("+1 week");
- if ($mdt > $now) {
- if ($this->_nextMeeting === null || $mdt < $this->_nextMeeting) {
- $this->_nextMeeting = $mdt;
- }
+ if ($mdt <= $now) { // If "next meeting" is past, add a week and re-check.
+ $mdt = $mdt->modify("+1 week");
+ }
+ if ($this->_nextMeeting === null || $mdt < $this->_nextMeeting) {
+ $this->_nextMeeting = $mdt;
}
}
}
@@ -596,17 +830,45 @@ public function nextMeeting(): ?DateTimeImmutable
}
/**
+ * @param \DateTimeInterface $apiMeeting
+ *
+ * @return bool
+ *
+ * TODO update with #184
+ */
+ protected static function apiMeetingIsAllDay($dateTime): bool
+ {
+ return $dateTime->format("His") === "000000";
+ }
+
+
+ /**
+ * @param $apiSchedule
+ *
+ * @return bool
+ *
+ * TODO remove or change with #184
+ */
+ protected static function apiScheduleIsAllDay($apiSchedule): bool
+ {
+ return $apiSchedule->nextStartDt->format("His") === "000000";
+ }
+
+ /**
+ * Group meetings and schedules together such that typical recurrences can be stated. Only returns patterns that
+ * have at least N occurrences.
+ *
* @param array $meetings
* @param array $schedules
+ * @param int $minNumber (N) The minimum required number of recurrences.
*
* @return ?array[]
*/
- private static function computeCommonOccurrences(array $meetings = [], array $schedules = []): ?array
+ protected static function computeCommonOccurrences(array $meetings = [], array $schedules = [], int $minNumber = 3): ?array
{
try {
- $siteTz = wp_timezone();
- $now = new DateTimeImmutable("now", $siteTz);
- } catch (Exception $e) {
+ $now = Utilities::dateTimeNow();
+ } catch (Exception) {
return null;
}
@@ -618,15 +880,23 @@ private static function computeCommonOccurrences(array $meetings = [], array $sc
continue;
}
- $dt = $s->next;
-
- $coInx = $dt->format('w-Hi');
+ /** @var DateTimeExtended $start */
+ $start = $s->nextStartDt;
+ if ($start === null) {
+ continue;
+ }
+ if ($start->isAllDay) {
+ $coInx = $start->format('w-9999');
+ } else {
+ $coInx = $start->format('w-Hi');
+ }
$commonOccurrences[$coInx] = [
- 'count' => 20,
- 'example' => $dt
+ 'count' => 20,
+ 'example' => $start,
+ 'exampleEnd' => null
];
}
- unset($dt, $coInx, $s);
+ unset($start, $coInx, $s);
// If there isn't a schedule, but there are common meeting dates/times, use those.
foreach ($meetings as $m) {
@@ -634,84 +904,130 @@ private static function computeCommonOccurrences(array $meetings = [], array $sc
continue;
}
- $dt = $m->dt;
+ /** @var DateTimeExtended $start */
+ $start = $m->mtgStartDt;
+
+ /** @var ?DateTimeExtended $end */
+ $end = $m->mtgEndDt;
- if ($dt < $now) {
+ if ($start < $now) {
continue;
}
- $coInx = $dt->format('w-Hi');
+ if ($start->isAllDay) {
+ $coInx = $start->format('w-9999');
+ } else {
+ $coInx = $start->format('w-Hi');
+ }
if (isset($commonOccurrences[$coInx])) {
$commonOccurrences[$coInx]['count']++;
} else {
$commonOccurrences[$coInx] = [
- 'count' => 1,
- 'example' => $dt
+ 'count' => 1,
+ 'example' => $start,
+ 'exampleEnd' => $end
];
}
}
- unset($dt, $coInx, $m);
+ unset($start, $coInx, $m);
- return array_filter($commonOccurrences, fn($co) => $co['count'] > 2);
+ return array_filter($commonOccurrences, fn($co) => $co['count'] >= $minNumber);
}
/**
- * Calculate the schedule string.
+ * Calculate the schedule strings.
*
- * @return string
+ * @return string[]
*/
- protected function scheduleString_calc(): ?string
+ protected function scheduleStrings_calc(): array
{
$commonOccurrences = self::computeCommonOccurrences($this->meetings(), $this->schedules());
- $dayStr = null;
- $timeFormat = get_option('time_format');
$dateFormat = get_option('date_format');
- if (count($commonOccurrences) > 0) {
- $uniqueTimeStrings = [];
- $days = [];
- if (count($commonOccurrences) > 1) { // this is only needed if there's multiple schedules
- foreach ($commonOccurrences as $k => $co) {
- $timeStr = substr($k, 2);
- if ( ! in_array($timeStr, $uniqueTimeStrings, true)) {
- $uniqueTimeStrings[] = $timeStr;
-
- $weekday = "d" . $k[0];
- if ( ! isset($days[$weekday])) {
- $days[$weekday] = [];
- }
- $days[$weekday][] = $co['example'];
- }
+ $r = [
+ 'datetime' => null,
+ 'date' => null,
+ 'time' => null,
+ 'firstLast' => null,
+ 'combined' => null
+ ];
+
+ $uniqueTimeStrings = [];
+ $days = [];
+ $common = true;
+ if (count($commonOccurrences) > 1) { // this is only needed if there's multiple schedules
+ foreach ($commonOccurrences as $k => $co) {
+ $timeStr = substr($k, 2);
+ if ( ! in_array($timeStr, $uniqueTimeStrings, true)) {
+ $uniqueTimeStrings[] = $timeStr;
+ }
+
+ $weekday = "d" . $k[0];
+ if ( ! isset($days[$weekday])) {
+ $days[$weekday] = [];
+ }
+
+ if ( ! in_array($co['example'], $days[$weekday], true)) {
+ $days[$weekday][] = $co['example'];
+ }
+ }
+ unset($timeStr, $k, $co, $weekday);
+ } elseif (count($commonOccurrences) > 0) {
+ $cok = array_key_first($commonOccurrences);
+ $days["d" . $cok[0]][] = $commonOccurrences[$cok]['example'];
+ } else {
+ $common = false;
+ $commonOccurrences = self::computeCommonOccurrences($this->meetings(), $this->schedules(), 0);
+
+ foreach ($commonOccurrences as $k => $co) {
+ $timeStr = substr($k, 2);
+ if (!in_array($timeStr, $uniqueTimeStrings, true)) {
+ $uniqueTimeStrings[] = $timeStr;
+ }
+
+ $weekday = "d" . $k[0];
+ if ( ! isset($days[$weekday])) {
+ $days[$weekday] = [];
+ }
+
+ if ( ! in_array($co['example'], $days[$weekday], true)) {
+ $days[$weekday][] = $co['example'];
}
- unset($timeStr, $k, $co, $weekday);
- } else {
- $cok = array_key_first($commonOccurrences);
- $days["d" . $cok[0]][] = $commonOccurrences[$cok]['example'];
}
+ unset($timeStr, $k, $co, $weekday);
+ }
+ if ($common) {
if (count($uniqueTimeStrings) > 1) { // Multiple different times. Sun at 9am & 11am, and Sat at 6pm
// multiple different times of day
$dayStr = [];
foreach ($days as $dk => $dta) {
$timeStr = [];
foreach ($dta as $dt) {
- /** @var $dt DateTimeImmutable */
- $ts = $dt->format($timeFormat);
- $ts = apply_filters(TouchPointWP::HOOK_PREFIX . 'adjust_time_string', $ts, $dt);
- $timeStr[] = $ts;
+ /** @var $dt DateTimeExtended */
+ if ($dt->isAllDay) {
+ continue; // skip all-days
+ }
+ $timeStr[] = DateFormats::TimeStringFormatted($dt);
}
- $timeStr = Utilities::stringArrayToListString($timeStr);
if (count($days) > 1) { // Mon at 7pm & Tue at 8pm
$day = Utilities::getDayOfWeekShortForNumber(intval($dk[1]));
} else {
$day = Utilities::getPluralDayOfWeekNameForNumber(intval($dk[1]));
}
- // translators: "Mon at 7pm" or "Sundays at 9am & 11am"
- $dayStr[] = wp_sprintf(__('%1$s at %2$s', 'TouchPoint-WP'), $day, $timeStr);
+ if (count($timeStr) > 0) {
+ $timeStr = Utilities::stringArrayToListString($timeStr);
+ // translators: %1$s is the date(s), %2$s is the time(s).
+ $dayStr[] = wp_sprintf(__('%1$s at %2$s', 'TouchPoint-WP'), $day, $timeStr);
+ } else {
+ // translators: "Mon All Day" or "Sundays All Day"
+ $dayStr[] = wp_sprintf(__('%1$s All Day', 'TouchPoint-WP'), $day);
+ }
}
$dayStr = Utilities::stringArrayToListString($dayStr);
+ $r['date'] = $dayStr;
} else { // one time of day. Tue & Thu at 7pm
if (count($days) > 1) {
// more than one day per week
@@ -725,73 +1041,151 @@ protected function scheduleString_calc(): ?string
$k = array_key_first($days);
$dayStr = Utilities::getPluralDayOfWeekNameForNumber(intval($k[1]));
}
+ $r['date'] = $dayStr;
$dt = array_values($days)[0][0];
- /** @var $dt DateTimeImmutable */
- $timeStr = $dt->format($timeFormat);
- $timeStr = apply_filters(TouchPointWP::HOOK_PREFIX . 'adjust_time_string', $timeStr, $dt);
- $dayStr = wp_sprintf(__('%1$s at %2$s', 'TouchPoint-WP'), $dayStr, $timeStr);
+ /** @var $dt DateTimeExtended */
+ if ($dt->isAllDay) {
+ // translators: "Mon All Day" or "Sundays All Day"
+ $dayStr = wp_sprintf(__('%1$s All Day', 'TouchPoint-WP'), $dayStr);
+ $r['time'] = __('All Day', 'TouchPoint-WP');
+ } else {
+ $timeStr = DateFormats::TimeStringFormatted($dt);
+
+ // translators: %1$s is the date(s), %2$s is the time(s).
+ $dayStr = wp_sprintf(__('%1$s at %2$s', 'TouchPoint-WP'), $dayStr, $timeStr);
+ $r['time'] = $timeStr;
+ }
}
- }
- // Convert start and end to string.
- if ($this->firstMeeting !== null && $this->lastMeeting !== null) {
- if ($dayStr === null) {
- $dayStr = wp_sprintf(
+ // Convert start and end to string,
+ if ($this->firstMeeting !== null && $this->lastMeeting !== null) {
+ $r['firstLast'] = wp_sprintf(
// translators: {start date} through {end date} e.g. February 14 through August 12
__('%1$s through %2$s', 'TouchPoint-WP'),
$this->firstMeeting->format($dateFormat),
$this->lastMeeting->format($dateFormat)
);
- } else {
- $dayStr = wp_sprintf(
- // translators: {schedule}, {start date} through {end date} e.g. Sundays at 11am, February 14 through August 12
- __('%1$s, %2$s through %3$s', 'TouchPoint-WP'),
- $dayStr,
- $this->firstMeeting->format($dateFormat),
- $this->lastMeeting->format($dateFormat)
- );
- }
- } elseif ($this->firstMeeting !== null) {
- if ($dayStr === null) {
- $dayStr = wp_sprintf(
+ if ($dayStr === null) {
+ $dayStr = $r['firstLast'];
+ } else {
+ $dayStr = wp_sprintf(
+ // translators: {schedule}, {start date} through {end date} e.g. Sundays at 11am, February 14 through August 12
+ __('%1$s, %2$s through %3$s', 'TouchPoint-WP'),
+ $dayStr,
+ $this->firstMeeting->format($dateFormat),
+ $this->lastMeeting->format($dateFormat)
+ );
+ }
+ } elseif ($this->firstMeeting !== null) {
+ $r['firstLast'] = wp_sprintf(
// translators: Starts {start date} e.g. Starts September 15
__('Starts %1$s', 'TouchPoint-WP'),
$this->firstMeeting->format($dateFormat)
);
- } else {
- $dayStr = wp_sprintf(
- // translators: {schedule}, starting {start date} e.g. Sundays at 11am, starting February 14
- __('%1$s, starting %2$s', 'TouchPoint-WP'),
- $dayStr,
- $this->firstMeeting->format($dateFormat)
- );
- }
- } elseif ($this->lastMeeting !== null) {
- if ($dayStr === null) {
- $dayStr = wp_sprintf(
+ if ($dayStr === null) {
+ $dayStr = $r['firstLast'];
+ } else {
+ $dayStr = wp_sprintf(
+ // translators: {schedule}, starting {start date} e.g. Sundays at 11am, starting February 14
+ __('%1$s, starting %2$s', 'TouchPoint-WP'),
+ $dayStr,
+ $this->firstMeeting->format($dateFormat)
+ );
+ }
+ } elseif ($this->lastMeeting !== null) {
+ $r['firstLast'] = wp_sprintf(
// translators: Through {end date} e.g. Through September 15
__('Through %1$s', 'TouchPoint-WP'),
$this->lastMeeting->format($dateFormat)
);
+ if ($dayStr === null) {
+ $dayStr = $r['firstLast'];
+ } else {
+ $dayStr = wp_sprintf(
+ // translators: {schedule}, through {end date} e.g. Sundays at 11am, through February 14
+ __('%1$s, through %2$s', 'TouchPoint-WP'),
+ $dayStr,
+ $this->lastMeeting->format($dateFormat)
+ );
+ }
+ }
+
+ $r['combined'] = $dayStr;
+ } else { // Uncommon schedules
+
+ if (count($commonOccurrences) === 0) {
+ return $r;
+ }
+
+ $forceDateTime = false;
+ $dateTimeArr = new StringableArray();
+ $dateArr = new StringableArray();
+ $timeArr = [];
+ $now = Utilities::dateTimeNow();
+
+ // filter meetings to only those not past
+ $meetings = $this->meetings();
+ $meetings = array_filter($meetings, fn($m) => $m->status == 1);
+ $uncancelledMeetings = $meetings; // includes historical
+// $originalMeetingCount = count($meetings);
+ $meetings = array_filter($meetings, fn($m) => $m->mtgEndDt > $now);
+ if (count($meetings) === 0) { // if no future, revert to historical.
+ $meetings = $uncancelledMeetings;
+ }
+// $andOthers = (count($meetings) !== $originalMeetingCount); This, when fed to the ->toListString methods
+// below can be used to add "and others" to the list of dates/times to indicate that there are historical
+// meetings that are not being shown. However, this currently seems more confusing than helpful.
+
+ foreach ($meetings as $m) {
+ $a = DateFormats::DurationToStringArray($m->mtgStartDt, $m->mtgEndDt, null, $m->mtgStartDt->isAllDay);
+
+ if (isset($a['datetime'])) {
+ $forceDateTime = true;
+ $dateTimeArr[] = $a['datetime'];
+ } else {
+ $dateTimeArr[] = wp_sprintf(
+ // translators: %1$s is the date(s), %2$s is the time(s).
+ __('%1$s at %2$s', 'TouchPoint-WP'), $a['date'], $a['time']
+ );
+ if ( !$dateArr->contains(['date'])) {
+ $dateArr[] = $a['date'];
+ }
+ if (!in_array($a['time'], $timeArr)) {
+ $timeArr[] = $a['time'];
+ }
+ }
+ }
+ if (count($timeArr) > 1) {
+ $forceDateTime = true;
+ }
+
+ if ($forceDateTime) {
+ $r['datetime'] = $dateTimeArr->toListString(2);
+ $r['combined'] = $r['datetime'];
} else {
- $dayStr = wp_sprintf(
- // translators: {schedule}, through {end date} e.g. Sundays at 11am, through February 14
- __('%1$s, through %2$s', 'TouchPoint-WP'),
- $dayStr,
- $this->lastMeeting->format($dateFormat)
+ $dateStr = $dateArr->toListString(2);
+
+ $r['date'] = $dateStr;
+ $r['time'] = $timeArr[0];
+ $r['combined'] = wp_sprintf(
+ // translators: %1$s is the date(s), %2$s is the time(s).
+ __('%1$s at %2$s', 'TouchPoint-WP'),
+ $dateStr,
+ $timeArr[0]
);
}
}
- return $dayStr;
+ return $r;
}
+
/**
- * Returns an array of the Involvement's Divisions, excluding those that cause it to be included.
+ * Gets the division terms for the involvement.
*
- * @return string[]
+ * @return WP_Term[]
*/
- public function getDivisionsStrings(): array
+ protected function getDivisions(): array
{
$exclude = $this->settings()->importDivs;
@@ -810,17 +1204,46 @@ public function getDivisionsStrings(): array
];
}
- $this->divisions = wp_get_post_terms($this->post_id, TouchPointWP::TAX_DIV, ['meta_query' => $mq]);
+ $this->divisions = wp_get_post_terms($this->post_id, Taxonomies::TAX_DIV, ['meta_query' => $mq]);
}
+ return $this->divisions;
+ }
+
+ /**
+ * Returns an array of the Involvement's Divisions, excluding those that cause it to be included.
+ *
+ * @return string[]
+ * @noinspection PhpUnused
+ */
+ public function getDivisionsStrings(): array
+ {
$out = [];
- foreach ($this->divisions as $d) {
+ foreach ($this->getDivisions() as $d) {
$out[] = $d->name;
}
return $out;
}
+
+ /**
+ * Returns an array of links to the Involvement's Divisions, excluding those that cause it to be included.
+ *
+ * @return string[]
+ */
+ public function getDivisionsLinks(): array
+ {
+ $out = [];
+ foreach ($this->getDivisions() as $d) {
+ $name = $d->name;
+ $link = get_term_link($d);
+ $out[] = "$name";
+ }
+
+ return $out;
+ }
+
/**
* Get the setting object for a specific post type or involvement type
*
@@ -852,6 +1275,26 @@ public static function getPostTypes(): array
return $r;
}
+
+ /**
+ * Get an array of Involvement Post Types, with some basic info
+ *
+ * @return array[]
+ */
+ public static function getPostTypesSummary(): array
+ {
+ $r = [];
+ foreach (self::allTypeSettings() as $pt) {
+ $r[] = [
+ /** @var Involvement_PostTypeSettings $pt */
+ 'postType' => $pt->postTypeWithoutPrefix(),
+ 'nameSingular' => $pt->nameSingular,
+ 'namePlural' => $pt->namePlural
+ ];
+ }
+ return $r;
+ }
+
/**
* Display action buttons for an involvement. Takes an id parameter for the Involvement ID. If not provided,
* the current post will be used.
@@ -895,7 +1338,7 @@ public static function actionsShortcode($params = [], string $content = ""): str
try {
$inv = self::fromPost($post);
$iid = $inv->invId;
- } catch (TouchPointWP_Exception $e) {
+ } catch (TouchPointWP_Exception) {
$iid = null;
}
}
@@ -982,7 +1425,7 @@ public static function filterShortcode($params = []): string
// language=javascript
"
tpvm.addEventListener('Involvement_fromObjArray', function() {
- TP_Involvement.initFilters();
+ tpvm.TP_Involvement.initFilters();
});"
);
self::$filterJsAdded = true;
@@ -999,7 +1442,7 @@ public static function doInvolvementList(WP_Query $q, $params = []): void
$q->set('orderby', 'title'); // will mostly be overwritten by geographic sort, if available.
$q->set('order', 'ASC');
- if ($q->is_post_type_archive()) {
+ if ( ! isset($q->query['post_parent'])) {
$q->set('post_parent', 0);
}
@@ -1060,7 +1503,7 @@ public static function doInvolvementList(WP_Query $q, $params = []): void
}
if (count($divs) > 0) {
$taxQuery[] = [
- 'taxonomy' => TouchPointWP::TAX_DIV,
+ 'taxonomy' => Taxonomies::TAX_DIV,
'field' => 'ID',
'terms' => $divs
];
@@ -1077,7 +1520,10 @@ public static function doInvolvementList(WP_Query $q, $params = []): void
}
}
- $containerClass = $params['class'] ?? self::$containerClass;
+ $containerClass = $params['class'] ?? [];
+ if (!str_contains(" " . $containerClass . " ", " " . self::$containerClass . " ")) {
+ $containerClass .= " " . self::$containerClass;
+ }
// Groupings
foreach ($terms as $termId => $name) {
@@ -1109,7 +1555,10 @@ public static function doInvolvementList(WP_Query $q, $params = []): void
usort($posts, [Involvement::class, 'sortPosts']);
- foreach ($posts as $post) {
+ foreach ($posts as $postI) {
+ global $post;
+ $post = $postI;
+
$loadedPart = get_template_part('list-item', 'involvement-list-item');
if ($loadedPart === false) {
require TouchPointWP::$dir . "/src/templates/parts/involvement-list-item.php";
@@ -1132,28 +1581,29 @@ public static function doInvolvementList(WP_Query $q, $params = []): void
* Get a WP_Post by the Involvement ID if it exists. Return null if it does not.
*
* @param string|string[] $postType
- * @param $involvementId
+ * @param mixed $involvementId
*
* @return int|WP_Post|null
*/
- private static function getWpPostByInvolvementId($postType, $involvementId)
+ private static function getWpPostByInvolvementId($postType, $involvementId): WP_Post|null
{
$involvementId = (string)$involvementId;
$q = new WP_Query([
'post_type' => $postType,
- 'meta_key' => self::INVOLVEMENT_META_KEY,
+ 'meta_key' => TouchPointWP::INVOLVEMENT_META_KEY,
'meta_value' => $involvementId,
'numberposts' => 2
// only need one, but if there's two, there should be an error condition.
]);
+ /** @var $posts WP_Post[] */
$posts = $q->get_posts();
$counts = count($posts);
if ($counts > 1) { // multiple posts match, which isn't great.
new TouchPointWP_Exception("Multiple Posts Exist", 170006);
}
if ($counts > 0) { // post exists already.
- return $posts[0];
+ return reset($posts);
} else {
return null;
}
@@ -1163,11 +1613,11 @@ private static function getWpPostByInvolvementId($postType, $involvementId)
* Print a list of involvements that match the given criteria.
*
* @param array|string $params
- * @param string $content
+ * @param string $content A string that is shown if no content is available.
*
* @return string
*
- * @noinspection PhpUnusedParameterInspection
+ * @noinspection PhpMissingParamTypeInspection
*/
public static function listShortcode($params = [], string $content = ""): string
{
@@ -1185,7 +1635,17 @@ public static function listShortcode($params = [], string $content = ""): string
'type' => null,
'div' => null,
'class' => self::$containerClass,
- 'includecss' => apply_filters(TouchPointWP::HOOK_PREFIX . 'use_css', true, self::class),
+
+ /**
+ * Determines whether or not to automatically include the plugin-default CSS. Return false to use your
+ * own CSS instead.
+ *
+ * @since 0.0.15 Added
+ *
+ * @param bool $useCss Whether or not to include the default CSS. True = include
+ * @param string $className The name of the current calling class.
+ */
+ 'includecss' => apply_filters('tp_use_css', true, self::class),
'itemclass' => self::$itemClass,
'usequery' => false
],
@@ -1203,12 +1663,31 @@ public static function listShortcode($params = [], string $content = ""): string
$render = ob_get_clean();
if (trim($render) == "") {
+ if ($content !== "") {
+ return apply_shortcodes($content);
+ }
return "";
}
return apply_shortcodes($render);
}
+
+ /**
+ * Get the list of involvements through API endpoint. This is used by the Involvement List Shortcode.
+ *
+ * @return void
+ */
+ protected static function ajaxListShortcode(): void
+ {
+ // This is an AJAX call, so we need to set the headers.
+ if ( ! headers_sent()) {
+ TouchPointWP::doCacheHeaders(TouchPointWP::CACHE_PRIVATE);
+ }
+
+ echo self::listShortcode($_GET, __('None right now.', 'TouchPoint-WP'));
+ }
+
/**
* @param array|string $params
* @param string $content
@@ -1216,7 +1695,7 @@ public static function listShortcode($params = [], string $content = ""): string
* @return string
* @noinspection PhpUnusedParameterInspection
*/
- public static function nearbyShortcode($params = [], string $content = ""): string
+ public static function nearbyShortcode(array|string $params = [], string $content = ""): string
{
TouchPointWP::requireScript("knockout-defer");
TouchPointWP::requireScript("base-defer");
@@ -1322,9 +1801,7 @@ protected static final function filterDropdownHtml(array $params, Involvement_Po
// Division
if (in_array('div', $filters)) {
$exclude = $settings->importDivs;
- if (count(
- $exclude
- ) == 1) { // Exclude the imported div if there's only one, as all invs would have that div.
+ if (count($exclude) == 1) { // Exclude the imported div if there's only one as all would have it.
$mq = ['relation' => "AND"];
foreach ($exclude as $e) {
$mq[] = [
@@ -1346,7 +1823,7 @@ protected static final function filterDropdownHtml(array $params, Involvement_Po
}
$dvName = TouchPointWP::instance()->settings->dv_name_singular;
$dvList = get_terms([
- 'taxonomy' => TouchPointWP::TAX_DIV,
+ 'taxonomy' => Taxonomies::TAX_DIV,
'hide_empty' => true,
'meta_query' => $mq,
TouchPointWP::HOOK_PREFIX . 'post_type' => $postType
@@ -1394,7 +1871,7 @@ protected static final function filterDropdownHtml(array $params, Involvement_Po
$rcName = TouchPointWP::instance()->settings->rc_name_singular;
$rcList = get_terms(
[
- 'taxonomy' => TouchPointWP::TAX_RESCODE,
+ 'taxonomy' => Taxonomies::TAX_RESCODE,
'hide_empty' => true,
TouchPointWP::HOOK_PREFIX . 'post_type' => $postType
]
@@ -1416,12 +1893,12 @@ protected static final function filterDropdownHtml(array $params, Involvement_Po
// Campuses
if (in_array('campus', $filters) && TouchPointWP::instance()->settings->enable_campuses === "on") {
$cName = TouchPointWP::instance()->settings->camp_name_singular;
- if (strtolower($cName) == "language") {
+ if (Translation::useCampusAsLanguage()) {
$cName = __("Language", 'TouchPoint-WP');
}
$cList = get_terms(
[
- 'taxonomy' => TouchPointWP::TAX_CAMPUS,
+ 'taxonomy' => Taxonomies::TAX_CAMPUS,
'hide_empty' => true,
TouchPointWP::HOOK_PREFIX . 'post_type' => $postType
]
@@ -1445,7 +1922,7 @@ protected static final function filterDropdownHtml(array $params, Involvement_Po
$wdName = __("Weekday", 'TouchPoint-WP');
$wdList = get_terms(
[
- 'taxonomy' => TouchPointWP::TAX_WEEKDAY,
+ 'taxonomy' => Taxonomies::TAX_WEEKDAY,
'hide_empty' => true,
'orderby' => 'id',
TouchPointWP::HOOK_PREFIX . 'post_type' => $postType
@@ -1471,7 +1948,7 @@ protected static final function filterDropdownHtml(array $params, Involvement_Po
$todName = __("Time of Day", 'TouchPoint-WP');
$todList = get_terms(
[
- 'taxonomy' => TouchPointWP::TAX_DAYTIME,
+ 'taxonomy' => Taxonomies::TAX_DAYTIME,
'hide_empty' => true,
'orderby' => 'id',
TouchPointWP::HOOK_PREFIX . 'post_type' => $postType
@@ -1505,7 +1982,7 @@ protected static final function filterDropdownHtml(array $params, Involvement_Po
if (in_array('agegroup', $filters)) {
$agName = __("Age", 'TouchPoint-WP');
$agList = get_terms([
- 'taxonomy' => TouchPointWP::TAX_AGEGROUP,
+ 'taxonomy' => Taxonomies::TAX_AGEGROUP,
'hide_empty' => true,
'orderby' => 't.id',
TouchPointWP::HOOK_PREFIX . 'post_type' => $postType
@@ -1522,19 +1999,19 @@ protected static final function filterDropdownHtml(array $params, Involvement_Po
if ($params['includeMapWarnings']) {
$content .= "
";
- $content .= sprintf(
+ $content .= wp_sprintf(
"%s ",
- sprintf(
+ wp_sprintf(
__("The %s listed are only those shown on the map.", 'TouchPoint-WP'),
$settings->namePlural
)
);
- $content .= sprintf(
+ $content .= wp_sprintf(
"%s ",
- sprintf(
+ wp_sprintf(
// translators: %s is the link to "reset the map"
__("Zoom out or %s to see more.", 'TouchPoint-WP'),
- sprintf(
+ wp_sprintf(
"%s",
_x("reset the map", "Zoom out or reset the map to see more.", 'TouchPoint-WP')
)
@@ -1553,13 +2030,25 @@ protected static final function filterDropdownHtml(array $params, Involvement_Po
*
* @param WP_Post $post
*
- * @return Involvement
+ * @return ?Involvement
*
* @throws TouchPointWP_Exception If the involvement can't be created from the post, an exception is thrown.
*/
- public static function fromPost(WP_Post $post): Involvement
+ public static function fromPost(WP_Post $post): ?Involvement
{
- $iid = intval($post->{self::INVOLVEMENT_META_KEY});
+ $iid = intval($post->{TouchPointWP::INVOLVEMENT_META_KEY});
+
+ if ($iid === 0) {
+ $iid = intval(get_post_meta($post->ID, TouchPointWP::INVOLVEMENT_META_KEY, true));
+ }
+
+ if (Meeting::postIsType($post) && !Involvement::postIsType($post)) {
+ throw new TouchPointWP_Exception("The post is a Meeting, not an Involvement.", 171004);
+ }
+
+ if ($iid === 0) {
+ throw new TouchPointWP_Exception("Invalid Involvement ID provided.", 171002);
+ }
if ( ! isset(self::$_instances[$iid])) {
self::$_instances[$iid] = new Involvement($post);
@@ -1569,6 +2058,30 @@ public static function fromPost(WP_Post $post): Involvement
}
+ /**
+ * Create an Involvement object from an object from its involvement ID.
+ *
+ * @param string $postType
+ * @param int $involvementId
+ *
+ * @return ?Involvement
+ *
+ * @throws TouchPointWP_Exception If the involvement can't be created, an exception is thrown.
+ */
+ public static function fromInvolvementId(string $postType, int $involvementId): ?Involvement
+ {
+ if ( ! isset(self::$_instances[$involvementId])) {
+ $post = self::getWpPostByInvolvementId($postType, $involvementId);
+ if ($post === null) {
+ return null;
+ }
+ self::$_instances[$involvementId] = Involvement::fromPost($post);
+ }
+
+ return self::$_instances[$involvementId];
+ }
+
+
/**
* Handle API requests
*
@@ -1596,10 +2109,26 @@ public static function api(array $uri): bool
self::ajaxNearby();
exit;
+ case "list":
+ self::ajaxListShortcode();
+ exit;
+
+ /** @noinspection SpellCheckingInspection */
+ case "posttypes":
+ // Return the post types that are available for involvements.
+ header('Content-Type: application/json');
+ echo json_encode(Involvement::getPostTypesSummary());
+ exit;
+
case "force-sync":
TouchPointWP::doCacheHeaders(TouchPointWP::CACHE_NONE);
echo self::updateFromTouchPoint(true);
exit;
+
+ case "preview-sync":
+ TouchPointWP::doCacheHeaders(TouchPointWP::CACHE_NONE);
+ echo self::updateFromTouchPoint(true, false);
+ exit;
}
return false;
@@ -1616,7 +2145,7 @@ public static function ajaxNearby(): void
$type = $_GET['type'] ?? "";
$lat = $_GET['lat'] ?? null;
$lng = $_GET['lng'] ?? null;
- $limit = $_GET['limit'] ?? null;
+ $limit = $_GET['limit'] ?? 10;
$settings = self::getSettingsForPostType($type);
@@ -1660,6 +2189,10 @@ public static function ajaxNearby(): void
exit;
}
+ if ($geoObj->type == "loc") {
+ $geoObj->type = "ip";
+ }
+
$lat = $geoObj->lat;
$lng = $geoObj->lng;
@@ -1668,6 +2201,7 @@ public static function ajaxNearby(): void
$geoObj = TouchPointWP::instance()->reverseGeocode($lat, $lng);
if ($geoObj !== false) {
+ $geoObj->type = "nav";
$r['geo'] = $geoObj;
}
}
@@ -1678,8 +2212,8 @@ public static function ajaxNearby(): void
http_response_code(Http::NOT_FOUND);
echo json_encode([
"invList" => [],
- "error" => sprintf("No %s Found.", $settings->namePlural),
- "error_i18n" => sprintf(__("No %s Found.", "TouchPoint-WP"), $settings->namePlural)
+ "error" => wp_sprintf("No %s Found.", $settings->namePlural),
+ "error_i18n" => wp_sprintf(__("No %s Found.", "TouchPoint-WP"), $settings->namePlural)
]);
exit;
}
@@ -1714,7 +2248,7 @@ public static function ajaxNearby(): void
* @param int $limit Number of results to return. 0-100 inclusive.
*
* @return object[]|null An array of database query result objects, or null if the location isn't provided or
- * valid.
+ * valid.
*/
private static function getInvsNear(float $lat, float $lng, string $postType, int $limit = 3): ?array
{
@@ -1728,6 +2262,7 @@ private static function getInvsNear(float $lat, float $lng, string $postType, in
global $wpdb;
$settingsPrefix = TouchPointWP::SETTINGS_PREFIX;
+ $metaInvId = TouchPointWP::INVOLVEMENT_META_KEY;
/** @noinspection SqlResolve */
$q = $wpdb->prepare(
"
@@ -1755,7 +2290,7 @@ private static function getInvsNear(float $lat, float $lng, string $postType, in
$wpdb->postmeta as pmClosed ON p.ID = pmClosed.post_id AND pmClosed.meta_key = '{$settingsPrefix}groupClosed'
WHERE p.post_type = %s AND pmClosed.meta_value != 1 AND pmFull.meta_value != 1
) as l
- JOIN $wpdb->postmeta as pmInv ON l.ID = pmInv.post_id AND pmInv.meta_key = '{$settingsPrefix}invId'
+ JOIN $wpdb->postmeta as pmInv ON l.ID = pmInv.post_id AND pmInv.meta_key = '$metaInvId'
ORDER BY distance LIMIT %d
",
$lat,
@@ -1769,25 +2304,6 @@ private static function getInvsNear(float $lat, float $lng, string $postType, in
}
- /**
- * Create a Involvement object from an object from a database query.
- *
- * @param object $obj A database object from which an Involvement object should be created.
- *
- * @return Involvement
- * @throws TouchPointWP_Exception
- */
- private static function fromObj(object $obj): Involvement
- {
- $iid = intval($obj->invId);
-
- if ( ! isset(self::$_instances[$iid])) {
- self::$_instances[$iid] = new Involvement($obj);
- }
-
- return self::$_instances[$iid];
- }
-
/**
* Create an Involvement object from an Involvement ID. Only Involvements that are already imported as Posts are
* currently available.
@@ -1851,9 +2367,6 @@ public static function load(): bool
/// Syncing ///
///////////////
- // Do an update if needed.
- add_action(TouchPointWP::INIT_ACTION_HOOK, [self::class, 'checkUpdates']);
-
// Setup cron for updating Involvements daily.
add_action(self::CRON_HOOK, [self::class, 'updateCron']);
if ( ! wp_next_scheduled(self::CRON_HOOK)) {
@@ -1865,6 +2378,9 @@ public static function load(): bool
);
}
+ // Do an update if needed.
+ add_action(TouchPointWP::INIT_ACTION_HOOK, [self::class, 'checkUpdates']);
+
return true;
}
@@ -1878,8 +2394,24 @@ public static function updateCron(): void
{
try {
self::updateFromTouchPoint();
- } catch (Exception $ex) {
+ } catch (Exception) {
+ }
+ }
+
+ protected static DateTimeImmutable $_updateExpiry;
+
+ protected static function updateExpiry(): DateTimeImmutable
+ {
+ if (!isset(self::$_updateExpiry)) {
+ $diff = TouchPointWP::instance()->settings->mc_archive_days;
+ try {
+ $interval = new DateInterval("P{$diff}D");
+ self::$_updateExpiry = Utilities::dateTimeNow()->sub($interval);
+ } catch (Exception) {
+ self::$_updateExpiry = Utilities::dateTimeNow();
+ }
}
+ return self::$_updateExpiry;
}
/**
@@ -1900,7 +2432,7 @@ public function getDistance(bool $useHiForFalse = false)
return $useHiForFalse ? 25000 : false;
}
- return Utilities\Geo::distance(
+ return Geo::distance(
$this->geo->lat,
$this->geo->lng,
self::$compareGeo->lat,
@@ -1949,21 +2481,46 @@ public static function sort(Involvement $a, Involvement $b): int
/**
* Put Post objects that represent Small Groups in order of increasing distance.
*
- * @param WP_Post $a
- * @param WP_Post $b
+ * @param WP_Post|Involvement $a
+ * @param WP_Post|Involvement $b
*
* @return int
*/
- public static function sortPosts(WP_Post $a, WP_Post $b): int
+ public static function sortPosts(WP_Post|Involvement $a, WP_Post|Involvement $b): int
{
- try {
- $a = self::fromPost($a);
- $b = self::fromPost($b);
+ $comparable = true;
+ if ($a instanceof WP_Post) {
+ try {
+ if (Meeting::postIsType($a)) {
+ $a = Meeting::fromPost($a)->involvement();
+ } elseif (Involvement::postIsType($a)) {
+ $a = Involvement::fromPost($a);
+ } else {
+ $comparable = false;
+ }
+ } catch (TouchPointWP_Exception) {
+ $comparable = false;
+ }
+ }
+ if ($b instanceof WP_Post) {
+ try {
+ if (Meeting::postIsType($b)) {
+ $b = Meeting::fromPost($b)->involvement();
+ } elseif (Involvement::postIsType($b)) {
+ $b = Involvement::fromPost($b);
+ } else {
+ $comparable = false;
+ }
+ } catch (TouchPointWP_Exception) {
+ $comparable = false;
+ }
+ }
+ if ($comparable) {
return self::sort($a, $b);
- } catch (TouchPointWP_Exception $ex) {
- return $a <=> $b;
}
+
+ return $a <=> $b;
}
@@ -2017,6 +2574,22 @@ public static function mapShortcode($params = [], string $content = ""): string
if ($params['all']) {
self::requireAllObjectsInJs();
self::$_hasArchiveMap = true;
+ } else {
+ // enqueue this object for js instantiation
+ $post = get_post();
+ if ($post) {
+ $inv = null;
+ try {
+ if (Meeting::postIsType($post)) {
+ $inv = Meeting::fromPost($post)?->involvement();
+ } elseif (Involvement::postIsType($post)) {
+ $inv = Involvement::fromPost($post);
+ }
+ } catch (TouchPointWP_Exception) {
+ // If the post is not an involvement, do nothing.
+ }
+ $inv?->enqueueForJsInstantiation();
+ }
}
$script = file_get_contents(TouchPointWP::$dir . "/src/js-partials/involvement-map-inline.js");
@@ -2028,7 +2601,6 @@ public static function mapShortcode($params = [], string $content = ""): string
$script
);
- // TODO move the style to a css file... or something.
$content = "
";
} else {
$content = "";
@@ -2037,7 +2609,6 @@ public static function mapShortcode($params = [], string $content = ""): string
return $content;
}
-
/**
* Indicates whether a map of a single Involvement can be displayed.
*
@@ -2049,41 +2620,40 @@ public function hasGeo(): bool
return false;
}
- return $this->geo !== null && $this->geo->lat !== null && $this->geo->lng !== null;
+ return $this->geo !== null && $this->geo->lat !== null && $this->geo->lng !== null &&
+ is_numeric($this->geo->lat) && is_numeric($this->geo->lng);
}
- public function asGeoIFace(string $type = "unknown"): ?object
+ public function asGeoIFace(string $type = "unknown"): ?Geo
{
if ($this->hasGeo()) {
- return (object)[
- 'lat' => $this->geo->lat,
- 'lng' => $this->geo->lng,
- 'human' => $this->name,
- 'type' => $type
- ];
+ return new Geo(
+ $this->geo->lat,
+ $this->geo->lng,
+ $this->name,
+ $type
+ );
}
return null;
}
-
/**
* Update posts that are based on an involvement.
*
* @param Involvement_PostTypeSettings $typeSets
- * @param string|int $divs
* @param bool $verbose
+ * @param bool $applyChanges
*
* @return false|int False on failure. Otherwise, the number of updates.
*/
- final protected static function updateInvolvementPostsForType(
- Involvement_PostTypeSettings $typeSets,
- $divs,
- bool $verbose
- ) {
+ final protected static function updateInvolvementPostsForType(Involvement_PostTypeSettings $typeSets, bool $verbose, bool $applyChanges = true): bool|int
+ {
$siteTz = wp_timezone();
- set_time_limit(180);
+ if (!set_time_limit(180) && $verbose) {
+ echo "
Time limit could not be extended. May not be able to complete all updates.
";
+ }
+ }
+ }
+
+ /**
+ * @param object $inv Involvement info from API
+ * @param DateTimeZone $siteTz Timezone object
+ * @param bool $verbose Print details
+ *
+ * @return void
+ */
+ protected static function standardizeApiData(object $inv, DateTimeZone $siteTz, bool $verbose): void
+ {
+ if ($verbose) {
+ Utilities::var_dump_expandable($inv);
+ }
+
+ // TODO convert firstMeeting, lastMeeting and schedules to RRules
+
+
+ // Start and end dates
+ if ($inv->firstMeeting !== null) {
+ try {
+ $inv->firstMeeting = new DateTimeExtended($inv->firstMeeting, $siteTz);
+ $inv->firstMeeting->isAllDay = true;
+ } catch (Exception) {
+ $inv->firstMeeting = null;
+ }
+ }
+ if ($inv->lastMeeting !== null) {
+ try {
+ $inv->lastMeeting = new DateTimeExtended($inv->lastMeeting, $siteTz);
+ $inv->lastMeeting->isAllDay = true;
+ } catch (Exception) {
+ $inv->lastMeeting = null;
+ }
+ }
+
+ // Meeting and Schedule date/time strings as DateTimeExtended
+ foreach ($inv->schedules as $i => $s) {
+ try {
+ if ($s->nextStartDt === $s->nextEndDt || $s->nextEndDt == null) {
+ $s->nextEndDt = null;
+ } else {
+ $s->nextEndDt = new DateTimeExtended($s->nextEndDt, $siteTz);
+ }
+ $s->nextStartDt = new DateTimeExtended($s->nextStartDt, $siteTz);
+ $s->nextStartDt->isAllDay = self::apiScheduleIsAllDay($s);
+ } catch (Exception) {
+ unset($inv->schedules[$i]);
+ }
+ }
+
+ // TODO: extract end of meeting series from RRule and set involvement last meeting appropriately.
+ // TODO: extract start of meeting series from series (not rrule necessarily) and set involvement first meeting appropriately.
+ // TODO: figure out how to make meetings and series related, perhaps?
+ // TODO: calculate recurrence better.
+
+// $rset = new RSet();
+// foreach ($inv->meetingSeries as $i => $ms) {
+// try {
+// if ($ms->seriesStartDt === $ms->seriesEndDt || $ms->seriesEndDt == null) {
+// $ms->seriesEndDt = null;
+// } else {
+// $ms->seriesEndDt = new DateTimeExtended($ms->seriesEndDt, $siteTz);
+// }
+// $ms->seriesStartDt = new DateTimeExtended($ms->seriesStartDt, $siteTz);
+// $ms->seriesStartDt->isAllDay = self::apiMeetingIsAllDay($ms->seriesStartDt);
+//
+// $ms->rro = RRule::createFromRfcString($ms->rRuleString ?? "");
+// $rset->addRRule($ms->rro);
+//
+// var_dump($ms->rro);
+// var_dump($ms->rro->humanReadable());
+// echo "";
+//
+// } catch (Exception) {
+// unset($inv->meetingSeries[$i]);
+// }
+//
+// if ($ms->name == null || trim($ms->name) === "") {
+// $ms->name = null;
+// }
+// }
+
+ foreach ($inv->meetings as $i => $m) {
+ try {
+ if ($m->mtgStartDt === $m->mtgEndDt || $m->mtgEndDt == null) {
+ $m->mtgEndDt = null;
+ } else {
+ $m->mtgEndDt = new DateTimeExtended($m->mtgEndDt, $siteTz);
+ }
+ $m->mtgStartDt = new DateTimeExtended($m->mtgStartDt, $siteTz);
+ $m->mtgStartDt->isAllDay = self::apiMeetingIsAllDay($m->mtgStartDt);
+
+ // if meetings exist beyond lastMeeting, nullify lastMeeting
+ if ($inv->lastMeeting !== null && $m->mtgStartDt > $inv->lastMeeting) {
+ $inv->lastMeeting = null;
+ }
+ } catch (Exception) {
+ unset($inv->meetings[$i]);
+ }
+
+ if ($m->name == null || trim($m->name) === "") {
+ $m->name = null;
+ }
+
+ $m->involvementId = $inv->involvementId;
+ }
+
+ // Registration start
+ if ($inv->regStart !== null) {
+ try {
+ $inv->regStart = new DateTimeImmutable($inv->regStart, $siteTz);
+ } catch (Exception) {
+ $inv->regStart = null;
+ }
+ }
+
+ // Registration end
+ if ($inv->regEnd !== null) {
+ try {
+ $inv->regEnd = new DateTimeImmutable($inv->regEnd, $siteTz);
+ } catch (Exception) {
+ $inv->regEnd = null;
+ }
+ }
+ }
+
+
/**
* Replace the date with the schedule summary
*
@@ -2580,11 +3743,11 @@ public static function filterPublishDate($theDate, $format, $post = null): strin
$invTypes = Involvement_PostTypeSettings::getPostTypes();
if (in_array(get_post_type($post), $invTypes)) {
- if (is_numeric($post)) {
- $post = get_post($post);
+ if (self::postIsType($post)) {
+ $theDate = self::scheduleString(intval($post->{TouchPointWP::INVOLVEMENT_META_KEY})) ?? "";
+ } elseif (Meeting::postIsType($post)) {
+ $theDate = Meeting::scheduleString(intval($post->{Meeting::MEETING_META_KEY})) ?? "";
}
-
- $theDate = self::scheduleString(intval($post->{self::INVOLVEMENT_META_KEY})) ?? "";
}
return $theDate;
@@ -2607,11 +3770,17 @@ public static function filterAuthor($author): string
if (in_array(get_post_type($postId), $invTypes)) {
$post = get_post($postId);
- try {
- $i = Involvement::fromPost($post);
- $author = $i->leaders()->__toString();
- } catch (TouchPointWP_Exception $e) {
+ $author = null;
+ try {
+ if (Involvement::postIsType($post)) {
+ $inv = Involvement::fromPost($post);
+ $author = $inv->leaders()->__toString();
+ } elseif (Meeting::postIsType($post)) {
+ $mtg = Meeting::fromPost($post);
+ $author = $mtg->involvement()->leaders()->__toString();
+ }
+ } catch (TouchPointWP_Exception) {
}
}
@@ -2628,14 +3797,24 @@ public function leaders(): PersonArray
if ( ! isset($this->_leaders)) {
$s = $this->settings();
- $q = new PersonQuery(
- [
- 'meta_key' => Person::META_INV_MEMBER_PREFIX . $this->invId,
- 'meta_value' => $s->leaderTypes,
- 'meta_compare' => 'IN'
- ]
- );
-
+ // If there aren't leader types (as is the case for all Event types), default to attend leader type.
+ if (count($s->leaderTypes) == 0) {
+ $q = new PersonQuery(
+ [
+ 'meta_key' => Person::META_INV_ATTEND_PREFIX . $this->invId,
+ 'meta_value' => ['at10'], // Leader Attend type.
+ 'meta_compare' => 'IN'
+ ]
+ );
+ } else {
+ $q = new PersonQuery(
+ [
+ 'meta_key' => Person::META_INV_MEMBER_PREFIX . $this->invId,
+ 'meta_value' => $s->leaderTypes,
+ 'meta_compare' => 'IN'
+ ]
+ );
+ }
$this->_leaders = $q->get_results();
}
@@ -2656,15 +3835,20 @@ public function hosts(): ?PersonArray
return null;
}
- $q = new PersonQuery(
- [
- 'meta_key' => Person::META_INV_MEMBER_PREFIX . $this->invId,
- 'meta_value' => $s->hostTypes,
- 'meta_compare' => 'IN'
- ]
- );
+ // If there aren't host types, there are no hosts.
+ if (count($s->hostTypes) == 0) {
+ $this->_hosts = new PersonArray();
+ } else {
+ $q = new PersonQuery(
+ [
+ 'meta_key' => Person::META_INV_MEMBER_PREFIX . $this->invId,
+ 'meta_value' => $s->hostTypes,
+ 'meta_compare' => 'IN'
+ ]
+ );
- $this->_hosts = $q->get_results();
+ $this->_hosts = $q->get_results();
+ }
}
return $this->_hosts;
@@ -2674,7 +3858,7 @@ public function hosts(): ?PersonArray
* Get the members of the involvement. Note that not all members are necessarily synced to WordPress from
* TouchPoint.
*
- * @return PersonArray
+ * @return ?PersonArray
*/
public function members(): ?PersonArray
{
@@ -2695,7 +3879,7 @@ public function members(): ?PersonArray
/**
* Get the settings object that corresponds to the Involvement's Post Type
*
- * @return Involvement_PostTypeSettings|null
+ * @return ?Involvement_PostTypeSettings
*/
protected function settings(): ?Involvement_PostTypeSettings
{
@@ -2743,63 +3927,71 @@ public function toJsonLD(): ?array
/**
* Get notable attributes, such as gender restrictions, as strings.
*
- * @param array $exclude Attributes listed here will be excluded. (e.g. if shown for a parent inv, not needed
- * here.)
+ * @param array|StringableArray $exclude Attributes listed here will be excluded. (e.g. if shown for a parent inv,
+ * not needed here.)
*
- * @return string[]
+ * @return NotableAttributes
+ *
+ * @since 0.0.11
+ * @since 0.0.96 Changed to use NotableAttributes class, which is a StringableArray.
*/
- public function notableAttributes(array $exclude = []): array
+ public function notableAttributes(array|StringableArray $exclude = []): NotableAttributes
{
- $r = [];
+ if (!is_array($exclude)) {
+ $exclude = $exclude->getArrayCopy();
+ }
- $schStr = self::scheduleString($this->invId, $this);
- if ($schStr) {
- $r[] = $schStr;
+ $asMeeting = $this->asAMeeting();
+ if ($asMeeting !== null) {
+ $attrs = $asMeeting->notableAttributes(['involvement']);
+ } else {
+ $attrs = self::scheduleStrings($this->invId, $this);
+ unset($attrs['combined']);
+ $attrs = array_filter($attrs);
+ $attrs = new NotableAttributes($attrs);
}
unset($schStr);
- if ($this->locationName) {
- $r[] = $this->locationName;
+ $l = $this->locationName();
+ if ($l) {
+ $attrs['location'] = $l;
}
+ unset($l);
- foreach ($this->getDivisionsStrings() as $a) {
- $r[] = $a;
+ if (!in_array('divisions', $exclude)) {
+ foreach ($this->getDivisionsLinks() as $k => $a) {
+ $attrs["divisions_$k"] = $a;
+ }
}
if ($this->leaders()->count() > 0) {
- $r[] = $this->leaders()->__toString();
+ $attrs['leaders'] = $this->leaders()->toLinks();
}
if ($this->genderId != 0) {
switch ($this->genderId) {
case 1:
- $r[] = __('Men Only', 'TouchPoint-WP');
+ $attrs['gender'] = __('Men Only', 'TouchPoint-WP');
break;
case 2:
- $r[] = __('Women Only', 'TouchPoint-WP');
+ $attrs['gender'] = __('Women Only', 'TouchPoint-WP');
break;
}
}
$canJoin = $this->acceptingNewMembers();
if (is_string($canJoin)) {
- $r[] = $canJoin;
+ $attrs['closed'] = $canJoin;
}
- $r = array_filter($r, fn($i) => ! in_array($i, $exclude));
-
- if ($this->hasGeo() &&
- (
- $exclude === [] ||
- (
- $this->locationName !== null &&
- ! in_array($this->locationName, $exclude)
- )
- )
+ if ($this->hasGeo() && (
+ $this->locationName !== null &&
+ ! in_array($this->locationName, $exclude)
+ )
) {
$dist = $this->getDistance();
if ($dist !== false) {
- $r[] = wp_sprintf(
+ $attrs['distance'] = wp_sprintf(
_x(
"%2.1fmi",
"miles. Unit is appended to a number. %2.1f is the number, so %2.1fmi looks like '12.3mi'",
@@ -2810,45 +4002,130 @@ public function notableAttributes(array $exclude = []): array
}
}
- return apply_filters(TouchPointWP::HOOK_PREFIX . "involvement_attributes", $r, $this);
+ $attrs = $this->processAttributeExclusions($attrs, $exclude);
+
+ $inv = $this;
+
+ /**
+ * Allows for manipulation of the notable attributes strings for an Involvement. An array of strings.
+ * Typically, these are the standardized strings that appear on the Involvement to give information about it,
+ * such as the schedule, leaders, and location.
+ *
+ * @see Involvement::notableAttributes()
+ * @see PostTypeCapable::notableAttributes()
+ *
+ * @since 0.0.11 Added
+ *
+ * @param NotableAttributes $attrs The list of notable attributes.
+ * @param Involvement $inv The Involvement object.
+ */
+ return apply_filters("tp_involvement_attributes", $attrs, $inv);
}
/**
* Returns the html with buttons for actions the user can perform. This must be called *within* an element with
- * the
- * `data-tp-involvement` attribute with the post_id (NOT the Inv ID) as the value.
+ * the `data-tp-involvement` attribute with the post_id (NOT the Inv ID) as the value.
*
- * @param ?string $context A reference to where the action buttons are meant to be used.
- * @param string $btnClass A string for classes to add to the buttons. Note that buttons can be a or button
- * elements.
+ * @param string|null $context A string that gives filters some context for where the request is coming from
+ * @param string $btnClass HTML class names to put into the buttons/links
+ * @param bool $withTouchPointLink Whether to include a link to the item within TouchPoint.
+ * @param bool $absoluteLinks Set true to make the links absolute, so they work from apps or emails.
+ * @param bool $includeRegister Set false to exclude the register button.
*
- * @return string
+ * @return StringableArray
*/
- public function getActionButtons(string $context = null, string $btnClass = ""): string
+ public function getActionButtons(?string $context = null, string $btnClass = "", bool $withTouchPointLink = true, bool $absoluteLinks = false, bool $includeRegister = true): StringableArray
{
- TouchPointWP::requireScript('swal2-defer');
- TouchPointWP::requireScript('base-defer');
- $this->enqueueForJsInstantiation();
- $this->enqueueForJsonLdInstantiation();
- Person::enqueueUsersForJsInstantiation();
+ if (!$absoluteLinks) {
+ TouchPointWP::requireScript('swal2-defer');
+ TouchPointWP::requireScript('base-defer');
+ $this->enqueueForJsInstantiation();
+ $this->enqueueForJsonLdInstantiation();
+ Person::enqueueUsersForJsInstantiation();
+ }
+ $classesOnly = $btnClass;
if ($btnClass !== "") {
$btnClass = " class=\"$btnClass\"";
}
- $ret = "";
- $count = 0;
- if (self::allowContact($this->invType)) {
- $text = __("Contact Leaders", 'TouchPoint-WP');
- $ret = " ";
- TouchPointWP::enqueueActionsStyle('inv-contact');
- $count++;
+ $baseLink = get_permalink($this->post_id);
+
+ $ret = new StringableArray();
+ if (self::allowContact($this->invType) && $this->leaders()->count() > 0) {
+ $text = __("Contact Leaders", 'TouchPoint-WP');
+ if (!$absoluteLinks) {
+ $ret['contact_leader'] = " ";
+ TouchPointWP::enqueueActionsStyle('inv-contact');
+ } else {
+ $iid = $this->invId;
+ $ret['contact_leader'] = "$text ";
+ }
+ }
+
+ // Register Button
+ if ($includeRegister === true) {
+ $ret['register'] = $this->getRegisterButton($classesOnly);
+ }
+
+ // Show on map button. (Only works if map is called before this is.)
+ if (self::$_hasArchiveMap && $this->geo !== null && !$absoluteLinks) {
+ $text = __("Show on Map", 'TouchPoint-WP');
+ if ($ret->count() > 1) {
+ TouchPointWP::requireScript("fontAwesome");
+ $ret->prepend("", "map");
+ } else {
+ $ret->prepend("", "map");
+ }
}
- if ($this->acceptingNewMembers() === true) {
- if ($this->useRegistrationForm()) {
+ if ($withTouchPointLink && TouchPointWP::currentUserIsAdmin()) {
+ $tpHost = TouchPointWP::instance()->host();
+ // Translators: %s is the system name, "TouchPoint" by default.
+ $title = wp_sprintf(__("Involvement in %s", "TouchPoint-WP"), TouchPointWP::instance()->settings->system_name);
+ $logo = TouchPointWP::TouchPointIcon();
+ $ret['inv_tp'] = "invId\" title=\"$title\" class=\"tp-TouchPoint-logo $classesOnly\">$logo";
+ }
+
+ /**
+ * Allows for manipulation of the action buttons for an Involvement. This is the list of buttons that appear
+ * on the Involvement to allow the user to interact with it.
+ *
+ * @since 0.0.7 Added
+ *
+ * @see Involvement::getActionButtons()
+ * @see PostTypeCapable::getActionButtons()
+ *
+ * @param StringableArray $ret The list of action buttons.
+ * @param Involvement $this The Involvement object.
+ * @param ?string $context A reference to where the action buttons are meant to be used.
+ * @param string $btnClass A string for classes to add to the buttons. Note that buttons can be 'a' or 'button'
+ * elements.
+ */
+ return apply_filters("tp_involvement_actions", $ret, $this, $context, $btnClass);
+ }
+
+ /**
+ * Get the HTML for the register button. Labels depend on several settings within TouchPoint.
+ *
+ * @param string $btnClass Class names
+ * @param bool $absoluteLinks Whether only absolute links should be provided that can be used in emails, apps,
+ * etc.
+ * @param ?Meeting $forMeeting If these buttons are for a meeting, pass the meeting
+ *
+ * @return ?string HTML for the registration button, whatever that should be. Null if nothing to return.
+ */
+ public function getRegisterButton(string $btnClass, bool $absoluteLinks = false, ?Meeting $forMeeting = null): ?string
+ {
+ if ($btnClass !== "") {
+ $btnClass = " class=\"$btnClass\"";
+ }
+
+ switch ($this->getRegistrationType()) {
+ case RegistrationType::FORM:
$text = __('Register', 'TouchPoint-WP');
- switch (get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "regTypeId", true)) {
+ $regTypeId = get_post_meta($this->post_id, TouchPointWP::SETTINGS_PREFIX . "regTypeId", true);
+ switch ($regTypeId) {
case 1: // Join Involvement (skip other options because this option is common)
break;
case 5: // Create Account
@@ -2873,31 +4150,96 @@ public function getActionButtons(string $context = null, string $btnClass = ""):
$text = __('Get Tickets', 'TouchPoint-WP');
break;
}
- $link = TouchPointWP::instance()->host() . "/OnlineReg/" . $this->invId;
- $ret .= "$text ";
- TouchPointWP::enqueueActionsStyle('inv-register');
- } else {
+
+ // If this is a meeting with record attendance type, don't show unless the meeting is within an hour.
+ if ($forMeeting !== null && $regTypeId == 18) {
+ if ($forMeeting->startDt > Utilities::dateTimeNow()->modify('+1 hour') ||
+ ($forMeeting->endDt ?? $forMeeting->startDt) < Utilities::dateTimeNow()->modify('-1 hour')) {
+ return null;
+ }
+ }
+
+ // If this is a meeting with tickets, don't show unless the meeting is in the present or future.
+ if ($forMeeting !== null && $regTypeId == 21) {
+ if (($forMeeting->endDt ?? $forMeeting->startDt->modify('+1 hour')) < Utilities::dateTimeNow()) {
+ return null;
+ }
+ }
+
+ $link = TouchPointWP::instance()->host() . "/OnlineReg/" . $this->invId;
+ if (!$absoluteLinks) {
+ TouchPointWP::enqueueActionsStyle('inv-register');
+ }
+ return "$text ";
+
+ case RegistrationType::JOIN:
$text = __('Join', 'TouchPoint-WP');
- $ret .= " ";
- TouchPointWP::enqueueActionsStyle('inv-join');
- }
- $count++;
+ if (!$absoluteLinks) {
+ TouchPointWP::enqueueActionsStyle('inv-join');
+ return " ";
+ }
+ $link = get_permalink($this->post_id()) . "#tp-join-i" . $this->invId;
+ return "$text ";
+
+ case RegistrationType::EXTERNAL:
+ $text = __('Register', 'TouchPoint-WP');
+ $link = $this->getRegistrationUrl();
+ if (!$absoluteLinks) {
+ TouchPointWP::enqueueActionsStyle('inv-register');
+ }
+ return "$text ";
+
+ case RegistrationType::RSVP:
+ $asAMeeting = $this->asAMeeting();
+ if ($asAMeeting !== null) {
+ if ($absoluteLinks) {
+ return $asAMeeting->getRsvpLink($btnClass);
+ }
+ return $asAMeeting->getRsvpButton($btnClass);
+ }
}
+ return null;
+ }
- // Show on map button. (Only works if map is called before this is.)
- if (self::$_hasArchiveMap && $this->geo !== null) {
- $text = __("Show on Map", 'TouchPoint-WP');
- if ($count > 1) {
- TouchPointWP::requireScript("fontAwesome");
- $ret = " " . $ret;
- } else {
- $ret = " " . $ret;
+ /**
+ * If the post for this involvement is also a single Meeting post, return that object. Otherwise, null.
+ *
+ * @return ?Meeting
+ */
+ protected function asAMeeting(): ?Meeting
+ {
+ if (!$this->post) {
+ $this->post = get_post($this->post_id);
+ }
+ if (Meeting::postIsType($this->post)) {
+ try {
+ return Meeting::fromPost($this->post);
+ } catch (TouchPointWP_Exception) {
+ return null;
}
}
+ return null;
+ }
- return apply_filters(TouchPointWP::HOOK_PREFIX . "involvement_actions", $ret, $this, $context, $btnClass);
+ /**
+ * Indicates if the meeting is in the past.
+ *
+ * @return bool
+ */
+ public function isPast(): bool
+ {
+ $m = $this->asAMeeting();
+ if ($m) {
+ return $m->isPast();
+ }
+ return false;
}
+ /**
+ * Get the JS for instantiation.
+ *
+ * @return string
+ */
public static function getJsInstantiationString(): string
{
$queue = static::getQueueForJsInstantiation();
@@ -2909,7 +4251,7 @@ public static function getJsInstantiationString(): string
$listStr = json_encode($queue);
return "\ttpvm.addEventListener('Involvement_class_loaded', function() {
- TP_Involvement.fromObjArray($listStr);\n\t});\n";
+ tpvm.TP_Involvement.fromObjArray($listStr);\n\t});\n";
}
public function getTouchPointId(): int
@@ -2917,6 +4259,30 @@ public function getTouchPointId(): int
return $this->invId;
}
+ /**
+ * Indicates if the given post can be instantiated as an Involvement.
+ *
+ * @param WP_Post $post
+ *
+ * @return bool
+ */
+ public static function postIsType(WP_Post $post): bool
+ {
+ return intval(get_post_meta($post->ID, TouchPointWP::INVOLVEMENT_META_KEY, true)) > 0;
+ }
+
+ /**
+ * Indicates if the given post type name is the post type for this class.
+ *
+ * @param string $postType
+ *
+ * @return bool
+ */
+ public static function postTypeMatches(string $postType): bool
+ {
+ return str_starts_with($postType, "tp_inv_") || $postType == "tp_smallgroup" || $postType == "tp_course";
+ }
+
/**
* Handles the API call to join an involvement through a 'join' button.
*/
@@ -2944,13 +4310,19 @@ private static function ajaxInvJoin(): void
}
try {
- $data = TouchPointWP::instance()->apiPost('inv_join', $inputData);
+ $data = TouchPointWP::instance()->api->pyPost('inv_join', $inputData);
} catch (TouchPointWP_Exception $ex) {
http_response_code(Http::SERVER_ERROR);
echo json_encode(['error' => $ex->getMessage()]);
exit;
}
+ try {
+ $stats = Stats::instance();
+ $stats->involvementJoins += count($data->success);
+ $stats->updateDb();
+ } catch (Exception) {}
+
echo json_encode(['success' => $data->success]);
exit;
}
@@ -2965,8 +4337,30 @@ private static function ajaxInvJoin(): void
*/
protected static function allowContact(string $invType): bool
{
- $allowed = !!apply_filters(TouchPointWP::HOOK_PREFIX . 'allow_contact', true);
- return !!apply_filters(TouchPointWP::HOOK_PREFIX . 'inv_allow_contact', $allowed, $invType);
+ $allowed = true;
+
+ /**
+ * Determines whether contact of any kind is allowed. This is meant to prevent abuse in contact forms by
+ * removing the ability to contact people and thereby hiding the forms.
+ *
+ * @since 0.0.35 Added
+ *
+ * @param bool $allowed True if contact is allowed.
+ */
+ $allowed = !!apply_filters('tp_allow_contact', $allowed);
+
+ /**
+ * Determines whether contact is allowed for any Involvements. This is called *after* tp_allow_contact, and
+ * that will set the default.
+ *
+ * @since 0.0.35 Added
+ *
+ * @see tp_allow_contact
+ *
+ * @param bool $allowed Previous response from tp_allow_contact. True if contact is allowed.
+ * @param string $invType The name of the Involvement Type.
+ */
+ return !!apply_filters('tp_inv_allow_contact', $allowed, $invType);
}
/**
@@ -3022,14 +4416,30 @@ private static function ajaxContact(): void
// Submit the contact
try {
- $data = TouchPointWP::instance()->apiPost('inv_contact', $inputData);
+ $data = TouchPointWP::instance()->api->pyPost('inv_contact', $inputData);
} catch (TouchPointWP_Exception $ex) {
http_response_code(Http::SERVER_ERROR);
echo json_encode(['error' => $ex->getMessage()]);
exit;
}
+ try {
+ $stats = Stats::instance();
+ $stats->involvementContacts += count($data->success);
+ $stats->updateDb();
+ } catch (Exception) {}
+
echo json_encode(['success' => $data->success]);
exit;
}
+
+ /**
+ * Get the name of the location.
+ *
+ * @return ?string
+ */
+ public function locationName(): ?string
+ {
+ return $this->locationName;
+ }
}
diff --git a/src/TouchPoint-WP/Involvement_PostTypeSettings.php b/src/TouchPoint-WP/Involvement_PostTypeSettings.php
index 3dee09d2..aaa53f29 100644
--- a/src/TouchPoint-WP/Involvement_PostTypeSettings.php
+++ b/src/TouchPoint-WP/Involvement_PostTypeSettings.php
@@ -19,9 +19,12 @@
* @property-read string $namePlural
* @property-read string $slug
* @property-read string[] $importDivs
+ * @property-read string[] $importCampuses
* @property-read bool $useImages
* @property-read bool $useGeo
* @property-read bool $hierarchical
+ * @property-read bool $importMeetings
+ * @property-read string $meetingGroupingMethod
* @property-read string $groupBy
* @property-read string[] $excludeIf
* @property-read string[] $leaderTypes
@@ -42,9 +45,12 @@ class Involvement_PostTypeSettings
protected string $namePlural;
protected string $slug;
protected array $importDivs = [];
+ protected array $importCampuses = [];
protected bool $useImages = false;
protected bool $useGeo = false;
protected bool $hierarchical = false;
+ protected bool $importMeetings = false;
+ protected string $meetingGroupingMethod = Meeting::GROUP_NONE;
protected string $groupBy = "";
protected array $excludeIf = [];
protected array $leaderTypes = [];
@@ -62,7 +68,7 @@ class Involvement_PostTypeSettings
*/
final public static function &instance(): array
{
- if ( ! isset(self::$settings)) {
+ if (!isset(self::$settings)) {
$json = json_decode(TouchPointWP::instance()->settings->inv_json);
$settingsArr = [];
@@ -70,7 +76,24 @@ final public static function &instance(): array
$settingsArr[] = new Involvement_PostTypeSettings($o);
}
- self::$settings = $settingsArr;
+ if (TouchPointWP::instance()->settings->enable_meeting_cal === 'on') {
+ $settingsArr[] = Meeting::getTypeSettings();
+ }
+
+ /**
+ * Adjust Involvement Post Type Settings. These settings define virtually all attributes of how a set of
+ * Involvements is synced to WordPress.
+ *
+ * If you're using this filter, you will need to add your function VERY early (init with a low sequence
+ * number or earlier) because post types are registered early.
+ *
+ * @see Involvement_PostTypeSettings
+ *
+ * @since 0.0.90 Added
+ *
+ * @param Involvement_PostTypeSettings[] $settingsArr An array of the Post Type Settings objects.
+ */
+ self::$settings = apply_filters("tp_get_involvement_type_settings", $settingsArr);
}
return self::$settings;
@@ -92,6 +115,25 @@ public function __construct(object $o)
}
}
+ /**
+ * Get a list of all division IDs that are being imported by all types.
+ *
+ * @return int[]
+ */
+ public static function getAllDivs(): array
+ {
+ $r = [];
+ foreach (self::instance() as $s) {
+ $r = [...$r, ...$s->importDivs];
+ }
+ return array_unique($r);
+ }
+
+ /**
+ * Get the Post Type for use with WordPress functions
+ *
+ * @return string
+ */
public function postTypeWithPrefix(): string
{
self::instance();
@@ -99,6 +141,11 @@ public function postTypeWithPrefix(): string
return TouchPointWP::HOOK_PREFIX . $this->postType;
}
+ /**
+ * Get the Post Type without the hook prefix.
+ *
+ * @return string
+ */
public function postTypeWithoutPrefix(): string
{
self::instance();
@@ -118,7 +165,7 @@ public function __get($what)
return $this->$what;
}
- return TouchPointWP_Settings::UNDEFINED_PLACEHOLDER;
+ return Settings::UNDEFINED_PLACEHOLDER;
}
/**
@@ -167,11 +214,11 @@ public static function getForInvType(string $postType): ?Involvement_PostTypeSet
$prefixLength = strlen(self::POST_TYPE_PREFIX);
foreach (self::instance() as $type) {
if ($type->postType === $postType ||
- $type->__get('postType') === $postType ||
- (
- substr($type->postType, 0, $prefixLength) === self::POST_TYPE_PREFIX &&
- substr($type->postType, $prefixLength) === $postType
- )
+ $type->__get('postType') === $postType ||
+ (
+ substr($type->postType, 0, $prefixLength) === self::POST_TYPE_PREFIX &&
+ substr($type->postType, $prefixLength) === $postType
+ )
) {
return $type;
}
@@ -214,7 +261,7 @@ public static function validateNewSettings(string $new): string
$name = preg_replace('/\W+/', '-', strtolower($type->namePlural));
try {
$type->slug = $name . ($first ? "" : "-" . bin2hex(random_bytes(1)));
- } catch (Exception $e) {
+ } catch (Exception) {
$type->slug = $name . ($first ? "" : "-" . bin2hex($count++));
}
$first = false;
@@ -249,7 +296,7 @@ public static function validateNewSettings(string $new): string
$slug = preg_replace('/\W+/', '', strtolower($type->slug));
try {
$type->postType = self::POST_TYPE_PREFIX . $slug . ($first ? "" : "_" . bin2hex(random_bytes(1)));
- } catch (Exception $e) {
+ } catch (Exception) {
$type->postType = self::POST_TYPE_PREFIX . $slug . ($first ? "" : "_" . bin2hex($count++));
}
$first = false;
@@ -283,19 +330,6 @@ public static function validateNewSettings(string $new): string
return json_encode($new);
}
- /**
- * @param string|string[]|int[] $memberTypes
- *
- * @return int[]
- */
- protected static function memberTypesToInts($memberTypes): array
- {
- $memberTypes = str_replace('mt', '', $memberTypes);
-
- /** @noinspection SpellCheckingInspection */
- return array_map('intval', $memberTypes);
- }
-
/**
* Get the leader member types, as an array of ints
*
@@ -303,7 +337,7 @@ protected static function memberTypesToInts($memberTypes): array
*/
public function leaderTypeInts(): array
{
- return self::memberTypesToInts($this->leaderTypes);
+ return Utilities::idArrayToIntArray($this->leaderTypes);
}
/**
@@ -317,6 +351,6 @@ public function hostTypeInts(): ?array
return null;
}
- return self::memberTypesToInts($this->hostTypes);
+ return Utilities::idArrayToIntArray($this->hostTypes);
}
}
\ No newline at end of file
diff --git a/src/TouchPoint-WP/Location.php b/src/TouchPoint-WP/Location.php
index eb548e6a..a9f6a9e6 100644
--- a/src/TouchPoint-WP/Location.php
+++ b/src/TouchPoint-WP/Location.php
@@ -5,18 +5,20 @@
namespace tp\TouchPointWP;
+use tp\TouchPointWP\Interfaces\hasGeo;
+
/**
* A Location is generally a physical place, with an internet connection. These likely correspond to campuses, but
* don't necessarily need to.
*/
-class Location implements geo
+class Location implements hasGeo
{
protected static ?array $_locations = null;
public string $name;
public ?float $lat;
public ?float $lng;
- public float $radius;
+ public float $radius; // miles
public array $ipAddresses;
protected function __construct($data)
@@ -50,7 +52,7 @@ public static function getLocations(): array
return self::$_locations;
}
- public static function getLocationForIP(string $ipAddress = null): ?Location
+ public static function getLocationForIP(?string $ipAddress = null): ?Location
{
$ipAddress = $ipAddress ?? Utilities::getClientIp();
@@ -79,25 +81,33 @@ public function hasGeo(): bool
return $this->lat !== null && $this->lng !== null;
}
- public function asGeoIFace(string $type = "unknown"): ?object
+ public function asGeoIFace(string $type = "unknown"): ?Geo
{
if ($this->hasGeo()) {
- return (object)[
- 'lat' => $this->lat,
- 'lng' => $this->lng,
- 'human' => $this->name,
- 'type' => $type
- ];
+ return new Geo(
+ $this->lat,
+ $this->lng,
+ $this->name,
+ $type
+ );
}
return null;
}
+ /**
+ * Get a location that corresponds to a given lat/lng, or null if none match.
+ *
+ * @param float $lat
+ * @param float $lng
+ *
+ * @return Location|null
+ */
public static function getLocationForLatLng(float $lat, float $lng): ?Location
{
$locs = self::getLocations();
foreach ($locs as $l) {
- $d = Utilities\Geo::distance($lat, $lng, $l->lat, $l->lng);
+ $d = Geo::distance($lat, $lng, $l->lat, $l->lng);
if ($d <= $l->radius) {
return $l;
}
@@ -106,6 +116,13 @@ public static function getLocationForLatLng(float $lat, float $lng): ?Location
return null;
}
+ /**
+ * Validates and corrects the Locations settings value.
+ *
+ * @param string $settings
+ *
+ * @return false|string
+ */
public static function validateSetting(string $settings)
{
$d = json_decode($settings);
@@ -117,7 +134,7 @@ public static function validateSetting(string $settings)
}
$l->lat = Utilities::toFloatOrNull($l->lat);
$l->lng = Utilities::toFloatOrNull($l->lng);
- $l->radius = Utilities::toFloatOrNull($l->radius, 1);
+ $l->radius = Utilities::toFloatOrNull($l->radius, 2);
$l->ipAddresses = array_values(
array_filter($l->ipAddresses, fn($ip) => filter_var($ip, FILTER_VALIDATE_IP))
@@ -126,4 +143,14 @@ public static function validateSetting(string $settings)
return json_encode($d);
}
+
+ /**
+ * Get the name of the location.
+ *
+ * @return ?string
+ */
+ public function locationName(): ?string
+ {
+ return $this->name;
+ }
}
\ No newline at end of file
diff --git a/src/TouchPoint-WP/Meeting.php b/src/TouchPoint-WP/Meeting.php
index 83d8516c..b6403453 100644
--- a/src/TouchPoint-WP/Meeting.php
+++ b/src/TouchPoint-WP/Meeting.php
@@ -10,17 +10,252 @@
}
if ( ! TOUCHPOINT_COMPOSER_ENABLED) {
- require_once 'api.php';
+ require_once "Interfaces/api.php";
+ require_once "Interfaces/involvementMeetingCommon.php";
}
+use DateTime;
+use DateTimeImmutable;
use Exception;
+use tp\TouchPointWP\Interfaces\api;
+use tp\TouchPointWP\Interfaces\involvementMeetingCommon;
+use tp\TouchPointWP\Interfaces\module;
+use tp\TouchPointWP\Utilities\DateFormats;
use tp\TouchPointWP\Utilities\Http;
+use tp\TouchPointWP\Utilities\NotableAttributes;
+use tp\TouchPointWP\Utilities\StringableArray;
+use WP_Post;
+use WP_Query;
+use WP_Term;
/**
* Handle meeting content, particularly RSVPs.
*/
-abstract class Meeting implements api, module
+class Meeting extends PostTypeCapable implements api, module, involvementMeetingCommon
{
+ use jsInstantiation;
+// use jsonLd; TODO
+
+ public const POST_TYPE_WO_PRE = "meeting";
+ public const POST_TYPE = TouchPointWP::HOOK_PREFIX . self::POST_TYPE_WO_PRE;
+
+ public const MEETING_META_KEY = TouchPointWP::SETTINGS_PREFIX . "mtgId";
+ public const MEETING_START_META_KEY = TouchPointWP::SETTINGS_PREFIX . "mtgStartDt";
+ public const MEETING_END_META_KEY = TouchPointWP::SETTINGS_PREFIX . "mtgEndDt";
+ public const MEETING_FEAT_META_KEY = TouchPointWP::SETTINGS_PREFIX . "mtgFeatured";
+ public const MEETING_STATUS_META_KEY = TouchPointWP::SETTINGS_PREFIX . "status";
+ public const MEETING_INV_ID_META_KEY = TouchPointWP::SETTINGS_PREFIX . "mtgInvId";
+ public const MEETING_IS_GROUP_MEMBER = TouchPointWP::SETTINGS_PREFIX . "isGroupMember";
+
+ public const GROUP_NONE = "none";
+ public const GROUP_UNSCHEDULED = "unscheduled";
+ public const GROUP_ALL = "all";
+
+ public const SHORTCODE_GRID = TouchPointWP::SHORTCODE_PREFIX . "Calendar";
+
+ public const STATUS_CANCELLED = "cancelled";
+ public const STATUS_SCHEDULED = "scheduled";
+ public const STATUS_UNKNOWN = "unknown";
+
+ // This is the same as the meta key for involvement locations.
+ public const MEETING_LOCATION_META_KEY = TouchPointWP::SETTINGS_PREFIX . "locationName";
+
+ private static bool $_isLoaded = false;
+ private static array $_instances = [];
+ private static ?Involvement_PostTypeSettings $_typeSet = null;
+
+ public ?DateTimeImmutable $startDt = null;
+ public ?DateTimeImmutable $endDt = null;
+
+ protected object $attributes;
+ protected string $name;
+ protected int $mtgId;
+
+
+ /**
+ * Meeting constructor.
+ *
+ * @param $object WP_Post|object an object representing the meeting's post.
+ * Must have post_id AND mtg id attributes.
+ *
+ * @throws TouchPointWP_Exception
+ */
+ protected function __construct(object $object)
+ {
+ $this->attributes = (object)[];
+
+ if (gettype($object) === "object" && get_class($object) == WP_Post::class) {
+ // WP_Post Object
+ $this->post = $object;
+ $this->name = $object->post_title;
+ $this->mtgId = intval($object->{Meeting::MEETING_META_KEY});
+ $this->post_id = $object->ID;
+
+ if ($this->mtgId === 0) {
+ throw new TouchPointWP_Exception("No Meeting ID provided in the post.", 171003);
+ }
+ } elseif (gettype($object) === "object") {
+ // Sql Object, probably.
+
+ if ( ! property_exists($object, 'post_id')) {
+ _doing_it_wrong(
+ __FUNCTION__,
+ esc_html(
+ __('Creating a Meeting object from an object without a post_id is not yet supported.', 'TouchPoint-WP')
+ ),
+ esc_attr(TouchPointWP::VERSION)
+ );
+ }
+
+ $this->post = get_post($object, "OBJECT");
+ $this->post_id = $this->post->ID;
+
+ foreach ($object as $property => $value) {
+ if (property_exists(self::class, $property)) {
+ $this->$property = $value;
+ }
+ // TODO add an else for nonstandard/optional metadata fields
+ }
+ } else {
+ throw new TouchPointWP_Exception("Could not construct a Meeting with the information provided.");
+ }
+
+ $postTerms = [
+ Taxonomies::TAX_RESCODE,
+ Taxonomies::TAX_AGEGROUP,
+ Taxonomies::TAX_WEEKDAY,
+ Taxonomies::TAX_TENSE,
+ Taxonomies::TAX_DAYTIME,
+ Taxonomies::TAX_INV_MARITAL,
+ Taxonomies::TAX_DIV
+ ];
+ if (TouchPointWP::instance()->settings->enable_campuses === "on") {
+ $postTerms[] = Taxonomies::TAX_CAMPUS;
+ }
+
+ $terms = wp_get_post_terms(
+ $this->post_id,
+ $postTerms
+ );
+
+ if (is_array($terms) && count($terms) > 0) {
+ $hookLength = strlen(TouchPointWP::HOOK_PREFIX);
+ foreach ($terms as $t) {
+ /** @var WP_Term $t */
+ $to = (object)[
+ 'name' => $t->name,
+ 'slug' => $t->slug
+ ];
+ $ta = $t->taxonomy;
+ if (str_starts_with($ta, TouchPointWP::HOOK_PREFIX)) {
+ $ta = substr_replace($ta, "", 0, $hookLength);
+ }
+ if ( ! isset($this->attributes->$ta)) {
+ $this->attributes->$ta = $to;
+ } elseif ( ! is_array($this->attributes->$ta)) {
+ $this->attributes->$ta = [$this->attributes->$ta, $to];
+ } else {
+ $this->attributes->$ta[] = $to;
+ }
+ }
+ }
+
+ $meta = get_post_meta($this->post_id);
+ $prefixLength = strlen(TouchPointWP::SETTINGS_PREFIX);
+
+ foreach ($meta as $k_tp => $v) {
+ if (substr($k_tp, 0, $prefixLength) !== TouchPointWP::SETTINGS_PREFIX) {
+ continue; // not ours.
+ }
+
+ $k = substr($k_tp, $prefixLength);
+ if ($k === "mtgId") {
+ continue;
+ }
+ if (property_exists(self::class, $k)) { // properties
+ $this->$k = maybe_unserialize($v[0]);
+ }
+ }
+
+ // JS attributes, for filtering mostly.
+// $this->attributes->genderId = (string)$this->genderId; TODO restore if needed (may be able to just inherit from parent).
+
+ // Start and End
+ $start = intval(get_post_meta($this->post_id, self::MEETING_START_META_KEY, true));
+ $end = intval(get_post_meta($this->post_id, self::MEETING_END_META_KEY, true));
+ $tz = wp_timezone();
+ $tz0 = Utilities::utcTimeZone();
+ $startDt = $start === 0 ? null : DateTime::createFromFormat("U", $start, $tz0)->setTimezone($tz);
+ $endDt = $end === 0 ? null : DateTime::createFromFormat("U", $end, $tz0)->setTimezone($tz);
+ $this->startDt = ($startDt === null ? null : DateTimeImmutable::createFromMutable($startDt));
+ $this->endDt = ($endDt === null ? null : DateTimeImmutable::createFromMutable($endDt));
+
+ $this->registerConstruction();
+ }
+
+ /**
+ * Get the Meeting ID.
+ *
+ * @return int
+ */
+ public function mtgId(): int
+ {
+ return $this->mtgId;
+ }
+
+
+ /**
+ * Create a Meeting object from a Meeting ID. Only Meetings that are already imported as Posts are currently
+ * available.
+ *
+ * @param int $mid A database object from which a Meeting object should be created.
+ *
+ * @return ?Meeting Null if the involvement is not imported/available.
+ * @throws TouchPointWP_Exception
+ */
+ private static function fromMtgId(int $mid): ?Meeting
+ {
+ if ( ! isset(self::$_instances[$mid])) {
+ $post = self::getWpPostByMeetingId(Involvement::getPostTypes(), $mid);
+ self::$_instances[$mid] = new Meeting($post);
+ }
+
+ return self::$_instances[$mid];
+ }
+
+ /**
+ * Get a WP_Post by the Meeting ID if it exists. Return null if it does not.
+ *
+ * @param string|string[] $postType
+ * @param mixed $meetingId
+ *
+ * @return WP_Post|null
+ */
+ private static function getWpPostByMeetingId($postType, $meetingId): WP_Post|null
+ {
+ $meetingId = (string)$meetingId;
+
+ $q = new WP_Query([
+ 'post_type' => $postType,
+ 'meta_key' => self::MEETING_META_KEY,
+ 'meta_value' => $meetingId,
+ 'numberposts' => 2
+ // only need one, but if there's two, there should be an error condition.
+ ]);
+ /** @var $posts WP_Post[] */
+ $posts = $q->get_posts();
+ $counts = count($posts);
+ if ($counts > 1) { // multiple posts match, which isn't great.
+ new TouchPointWP_Exception("Multiple Posts Exist", 170006);
+ }
+ if ($counts > 0) { // post exists already.
+ return reset($posts);
+ } else {
+ return null;
+ }
+ }
+
+
/**
* Register scripts and styles to be used on display pages.
*/
@@ -41,6 +276,379 @@ public static function registerScriptsAndStyles(): void
);
}
+
+ /**
+ * Get the PostTypeSettings object for Meeting Involvements.
+ *
+ * @return Involvement_PostTypeSettings
+ */
+ public static function getTypeSettings(): Involvement_PostTypeSettings
+ {
+ if (self::$_typeSet == null) {
+ self::$_typeSet = new Involvement_PostTypeSettings((object)[
+ 'namePlural' => _x("Events", "What Meetings should be called, plural.", 'TouchPoint-WP'),
+ 'nameSingular' => _x("Event", "What Meetings should be called, singular.", 'TouchPoint-WP'),
+ 'slug' => TouchPointWP::instance()->settings->mc_slug,
+ 'importMeetings' => true,
+ 'useImages' => true,
+ 'useGeo' => false,
+ 'hierarchical' => true,
+ 'postType' => self::POST_TYPE_WO_PRE,
+ 'meetingGroupingMethod' => TouchPointWP::instance()->settings->mc_grouping_method
+ ]);
+ }
+ return self::$_typeSet;
+ }
+
+
+ /**
+ * Create a Meeting object from an object from a WP_Post object.
+ *
+ * @param WP_Post $post
+ *
+ * @return Meeting
+ *
+ * @throws TouchPointWP_Exception If the meeting can't be created from the post, an exception is thrown.
+ */
+ public static function fromPost(WP_Post $post): Meeting
+ {
+ $mid = intval($post->{Meeting::MEETING_META_KEY});
+
+ if ($mid === 0) {
+ throw new TouchPointWP_Exception("Invalid Meeting ID provided.", 171003);
+ }
+
+ if ( ! isset(self::$_instances[$mid])) {
+ self::$_instances[$mid] = new Meeting($post);
+ }
+
+ return self::$_instances[$mid];
+ }
+
+ /**
+ * Get the involvement ID (without necessarily instantiating the Involvement)
+ *
+ * @since 0.0.90 Added
+ *
+ * @return int
+ */
+ public function involvementId(): int
+ {
+ return intval(get_post_meta($this->post_id, self::MEETING_INV_ID_META_KEY, true));
+ }
+
+ /**
+ * Get the Involvement object associated with this Meeting.
+ *
+ * @return Involvement
+ * @throws TouchPointWP_Exception
+ */
+ public function involvement(): Involvement
+ {
+ if (Involvement::postIsType($this->post)) {
+ return Involvement::fromPost($this->post);
+ }
+ $inv = Involvement::fromInvolvementId($this->post->post_type, $this->involvementId());
+ if ($inv) {
+ return $inv;
+ }
+ throw new TouchPointWP_Exception("Meeting is not associated with an Involvement.", 171002);
+ }
+
+
+ /**
+ * Get the parent of this object **which is a different class**.
+ *
+ * Returns null if there is no parent.
+ *
+ * In cases where a meeting post is also an involvement post, it will return the involvement, which has the same post_id.
+ *
+ * @return ?Involvement|Meeting
+ */
+ public function getParent(): Involvement|Meeting|null
+ {
+ if (!$this->isMeetingGroup() && $this->isMeetingGroupMember()) {
+ $parent = get_post($this->post->post_parent);
+ if ($parent) {
+ try {
+ return Meeting::fromPost($parent);
+ } catch (TouchPointWP_Exception) {
+ }
+ }
+ }
+ try {
+ return $this->involvement();
+ } catch (TouchPointWP_Exception) {
+ return null;
+ }
+ }
+
+
+ /**
+ * Get the meeting date/time in human-readable form.
+ *
+ * @param int $objId
+ * @param ?Meeting $obj
+ *
+ * @return ?string
+ */
+ public static function scheduleString(int $objId, $obj = null): ?string
+ {
+ if (!$obj) {
+ try {
+ $obj = self::fromMtgId($objId);
+ } catch (TouchPointWP_Exception) {
+ return null;
+ }
+ }
+
+ $s = $obj?->scheduleStringArray();
+
+ return $s?->join();
+ }
+
+ /**
+ * Get the human-readable schedule for the meeting as a string or set of strings in an array.
+ *
+ * @return StringableArray
+ *
+ * @since 0.0.90 Added
+ */
+ public function scheduleStringArray(): StringableArray
+ {
+ return DateFormats::DurationToStringArray($this->startDt, $this->endDt, $this->isMultiDay(), $this->isAllDay());
+ }
+
+
+ /**
+ * Get notable attributes, such as gender restrictions, as strings.
+ *
+ * @param array|StringableArray $exclude Attributes listed here will be excluded. (e.g. if shown for a parent, not
+ * needed here.)
+ *
+ * @return NotableAttributes
+ */
+ public function notableAttributes(array|StringableArray $exclude = []): NotableAttributes
+ {
+ if (!is_array($exclude)) {
+ $exclude = $exclude->getArrayCopy();
+ }
+
+ if (in_array('involvement', $exclude)) {
+ $attrs = null;
+ } else {
+ try {
+ $attrs = $this->involvement()->notableAttributes(['date', 'datetime', 'time', 'firstLast']);
+ } catch (TouchPointWP_Exception) {
+ $attrs = null;
+ }
+ }
+
+ $d = $this->scheduleStringArray();
+ if ($attrs !== null) {
+ foreach ($attrs as $k => $v) {
+ if (is_string($v) && $v !== "") {
+ $d[$k] = $v;
+ }
+ }
+ }
+ $attrs = $d;
+ unset($d);
+
+ $status = $this->status_i18n(true);
+ if ($status) {
+ $attrs['status'] = $status;
+ } else {
+ // Add an "in the past" label if the thing is already past. (end may be null)
+ if ($this->isPast()) {
+ $attrs['past'] = __("In the Past", "TouchPoint-WP");
+ }
+ }
+
+ $loc = $this->locationName();
+ if ($loc) {
+ $attrs['location'] = $loc;
+ }
+
+ $attrs = $this->processAttributeExclusions($attrs, $exclude);
+ $mtg = $this;
+
+ /**
+ * Allows for manipulation of the notable attributes strings for a Meeting. An array of strings.
+ * Typically, these are the standardized strings that appear on the Involvement to give information about it,
+ * such as the schedule, leaders, and location.
+ *
+ * @see Meeting::notableAttributes()
+ * @see PostTypeCapable::notableAttributes()
+ *
+ * @since 0.0.90 Added
+ * @since 0.0.96 Changed to use NotableAttributes instead of array.
+ *
+ * @param NotableAttributes $attrs The list of notable attributes.
+ * @param Meeting $mtg The Meeting object.
+ */
+ return apply_filters("tp_meeting_attributes", $attrs, $mtg);
+ }
+
+ /**
+ * @param string|null $context A string that gives filters some context for where the request is coming from
+ * @param string $btnClass HTML class names to put into the buttons/links
+ * @param bool $withTouchPointLink Whether to include a link to the item within TouchPoint.
+ * @param bool $absoluteLinks Set true to make the links absolute, so they work from apps or emails.
+ *
+ * @return StringableArray
+ */
+ public function getActionButtons(?string $context = null, string $btnClass = "", bool $withTouchPointLink = true, bool $absoluteLinks = false): StringableArray
+ {
+ if (!$absoluteLinks) {
+ TouchPointWP::requireScript('swal2-defer');
+ TouchPointWP::requireScript('base-defer');
+ $this->enqueueForJsInstantiation();
+// $this->enqueueForJsonLdInstantiation();
+ Person::enqueueUsersForJsInstantiation();
+ }
+
+ try {
+ $inv = $this->involvement();
+ } catch (TouchPointWP_Exception) {
+ return new StringableArray();
+ }
+
+ $ret = $inv->getActionButtons($context . "_meeting", $btnClass,
+ $withTouchPointLink && $this->isMeetingGroup(),
+ $absoluteLinks, false);
+
+ if ($this->status() !== self::STATUS_CANCELLED) {
+ if ($inv->getRegistrationType() === RegistrationType::RSVP) {
+ if ($absoluteLinks) {
+ $ret['register'] = $this->getRsvpLink($btnClass);
+ } else {
+ $ret['register'] = $this->getRsvpButton($btnClass);
+ }
+ } else {
+ if (($this->endDt ?? $this->startDt) > Utilities::dateTimeNow()) {
+ $ret['register'] = $inv->getRegisterButton($btnClass, $absoluteLinks, $this);
+ }
+ }
+ }
+
+ if ($withTouchPointLink && TouchPointWP::currentUserIsAdmin() && !$this->isMeetingGroup()) {
+ $tpHost = TouchPointWP::instance()->host();
+ // Translators: %s is the system name, "TouchPoint" by default.
+ $title = wp_sprintf(__("Meeting in %s", "TouchPoint-WP"), TouchPointWP::instance()->settings->system_name);
+ $logo = TouchPointWP::TouchPointIcon();
+ $ret['mtg_tp'] = "mtgId\" title=\"$title\" class=\"tp-TouchPoint-logo $btnClass\">$logo";
+ }
+
+ /**
+ * Allows for manipulation of the action buttons for a Meeting. This is the list of buttons that appear
+ * on the Meeting to allow the user to interact with it.
+ *
+ * @since 0.0.90 Added
+ *
+ * @see Meeting::getActionButtons()
+ * @see PostTypeCapable::getActionButtons()
+ *
+ * @param StringableArray $ret The list of action buttons.
+ * @param Meeting $this The Meeting object.
+ * @param ?string $context A reference to where the action buttons are meant to be used.
+ * @param string $btnClass A string for classes to add to the buttons. Note that buttons can be 'a' or 'button'
+ * elements.
+ */
+ return apply_filters("tp_meeting_actions", $ret, $this, $context, $btnClass);
+ }
+
+ public function isMeetingGroup(): bool
+ {
+ return $this->mtgId < 0;
+ }
+
+ public function isMeetingGroupMember(): bool
+ {
+ return !!get_post_meta($this->post_id, Meeting::MEETING_IS_GROUP_MEMBER, true);
+ }
+
+ public function isFeatured(): bool
+ {
+ return !!get_post_meta($this->post_id, Meeting::MEETING_FEAT_META_KEY, true);
+ }
+
+ /**
+ * Get the status of the meeting, in a code-oriented name (for css, etc.)
+ *
+ * @return string
+ */
+ public function status(): string
+ {
+ $status = intval(get_post_meta($this->post_id, self::MEETING_STATUS_META_KEY, true));
+
+ return match ($status) {
+ 0 => self::STATUS_CANCELLED,
+ 1 => self::STATUS_SCHEDULED,
+ default => self::STATUS_UNKNOWN,
+ };
+ }
+
+ /**
+ * @param bool $excludeScheduled "Scheduled" is the default (and correct) status for most events. Set this to true
+ * to return null instead of "Scheduled".
+ *
+ * @return string|null
+ */
+ public function status_i18n(bool $excludeScheduled = false): ?string
+ {
+ $status = intval(get_post_meta($this->post_id, self::MEETING_STATUS_META_KEY, true));
+
+ return match ($status) {
+ 0 => __("Cancelled", "TouchPoint-WP"),
+ 1 => $excludeScheduled ? null : __("Scheduled", "TouchPoint-WP"),
+ default => _x("Unknown", "Event Status is not a recognized value.", "TouchPoint-WP"),
+ };
+ }
+
+ /**
+ * Filters the post thumbnail ID. Allows meetings to have the image of their parent without having an image themselves.
+ *
+ * @param int|false $thumbnail_id Post thumbnail ID or false if the post does not exist.
+ * @param int|WP_Post|null $post Post ID or WP_Post object. Default is global `$post`.
+ */
+ public static function filterThumbnailId(int|false $thumbnail_id, int|WP_Post|null $post): bool|int
+ {
+ if ($thumbnail_id > 0) { // If already set, we have nothing to do.
+ return $thumbnail_id;
+ }
+
+ if (is_numeric($post)) {
+ $post = get_post($post);
+ }
+
+ if (!$post instanceof WP_Post) { // Something went wrong because we don't have a post.
+ return $thumbnail_id;
+ }
+
+ if (!self::postIsType($post) || Involvement::postIsType($post)) {
+ // Second condition is necessary to prevent loops when meeting post == involvement post
+ return $thumbnail_id;
+ }
+
+ try {
+ $meeting = Meeting::fromPost($post);
+ $involvementPostId = $meeting->involvement()?->post_id();
+ if (!$involvementPostId) {
+ return $thumbnail_id;
+ }
+
+ if (get_the_content(post: $involvementPostId) !== get_the_content(post: $meeting->post_id())) {
+ return $thumbnail_id;
+ }
+
+ return get_post_thumbnail_id($involvementPostId);
+ } catch (TouchPointWP_Exception) {
+ }
+
+ return $thumbnail_id;
+ }
+
/**
* Handle API requests
*
@@ -64,17 +672,44 @@ public static function api(array $uri): bool
return false;
}
+ /**
+ * @inheritDoc
+ */
+ public static function getJsInstantiationString(): string
+ {
+ $queue = static::getQueueForJsInstantiation();
+
+ if (count($queue) < 1) {
+ return "\t// No Meetings to instantiate.\n";
+ }
+
+ $listStr = json_encode($queue);
+
+ return ""; // TODO someday, probably.
+
+// return "\ttpvm.addEventListener('Involvement_class_loaded', function() {
+// tpvm.TP_Involvement.fromObjArray($listStr);\n\t});\n";
+ }
+
+ /**
+ * Gets a TouchPoint item ID number, regardless of what type of object this is.
+ *
+ * @return int
+ */
+ public function getTouchPointId(): int
+ {
+ return $this->mtgId;
+ }
+
/**
* @param $opts
*
* @return object
* @throws TouchPointWP_Exception
*/
- private static function getMeetingInfo($opts): object
+ private static function getMeetingInfoForRsvp($opts): object
{
- // TODO caching
-
- return TouchPointWP::instance()->apiPost('mtg', $opts);
+ return TouchPointWP::instance()->api->pyPost('mtg', $opts);
}
/**
@@ -94,7 +729,7 @@ private static function ajaxGetMeetingInfo(): void
}
try {
- $data = self::getMeetingInfo($_GET);
+ $data = self::getMeetingInfoForRsvp($_GET);
} catch (TouchPointWP_Exception $ex) {
http_response_code(Http::SERVER_ERROR);
echo json_encode(['error' => $ex->getMessage()]);
@@ -132,14 +767,260 @@ private static function ajaxSubmitRsvps(): void
}
try {
- $data = TouchPointWP::instance()->apiPost('mtg_rsvp', json_decode($inputData));
+ $data = TouchPointWP::instance()->api->pyPost('mtg_rsvp', json_decode($inputData));
} catch (Exception $ex) {
http_response_code(Http::SERVER_ERROR);
echo json_encode(['error' => $ex->getMessage()]);
exit;
}
+ try {
+ $stats = Stats::instance();
+ $stats->rsvps += count($data->success);
+ $stats->updateDb();
+ } catch (Exception) {}
+
echo json_encode(['success' => $data->success]);
exit;
}
+
+ /**
+ * @param string $btnClass
+ *
+ * @return string
+ */
+ public function getRsvpButton(string $btnClass = ""): string
+ {
+ TouchPointWP::requireScript('swal2-defer');
+ TouchPointWP::requireScript('meeting-defer');
+ TouchPointWP::enqueueActionsStyle('rsvp');
+ Person::enqueueUsersForJsInstantiation();
+
+ $link = __("RSVP", "TouchPoint-WP");
+ $preloadMsg = __("Loading...", "TouchPoint-WP");
+
+ $btnClass = trim($btnClass);
+ if ($btnClass !== '' && !str_starts_with($btnClass, "class=")) {
+ $btnClass = "class=\"$btnClass\"";
+ }
+
+ return "mtgId\">$link$preloadMsg";
+ }
+
+ /**
+ * Get a link to RSVP for the meeting that can be used in emails, apps, or other contexts. This is a link to the
+ * RSVP function in WordPress, not an RSVP magic link used in TouchPoint emails.
+ *
+ * @param string $btnClass
+ *
+ * @return string
+ */
+ public function getRsvpLink(string $btnClass = ""): string
+ {
+ $link = __("RSVP", "TouchPoint-WP");
+
+ $baseUrl = get_permalink($this->post_id);
+
+ $btnClass = trim($btnClass);
+ if ($btnClass !== '' && !str_starts_with($btnClass, "class=")) {
+ $btnClass = "class=\"$btnClass\"";
+ }
+
+ $mid = $this->mtgId;
+ return "$link";
+
+ }
+
+ /**
+ * Indicates if the given post can be instantiated as a Meeting.
+ *
+ * @param \WP_Post $post
+ *
+ * @return bool
+ */
+ public static function postIsType(WP_Post $post): bool
+ {
+ return intval(get_post_meta($post->ID, Meeting::MEETING_META_KEY, true)) != 0;
+ }
+
+ /**
+ * Indicates if the given post type name is the post type for this class.
+ *
+ * @param string $postType
+ *
+ * @return bool
+ */
+ public static function postTypeMatches(string $postType): bool
+ {
+ return $postType === self::POST_TYPE;
+ }
+
+ public static function load(): bool
+ {
+ if (self::$_isLoaded) {
+ return true;
+ }
+
+ self::$_isLoaded = true;
+
+ add_action(TouchPointWP::INIT_ACTION_HOOK, [self::class, 'init']);
+
+ //////////////////
+ /// Shortcodes ///
+ //////////////////
+
+ if ( ! shortcode_exists(self::SHORTCODE_GRID)) {
+ add_shortcode(self::SHORTCODE_GRID, [CalendarGrid::class, "shortcode"]);
+ }
+
+ return true;
+ }
+
+ public static function init(): void
+ {
+ add_filter('post_thumbnail_id', [self::class, 'filterThumbnailId'], 10, 3);
+ }
+
+ /**
+ * Indicate the tense of the meeting.
+ *
+ * @return string
+ */
+ public function tense(): string
+ {
+ if ($this->endDt < Utilities::dateTimeNow()) {
+ return Taxonomies::TAX_TENSE_PAST;
+ }
+ if ($this->startDt > Utilities::dateTimeNow()) {
+ return Taxonomies::TAX_TENSE_FUTURE;
+ }
+ return Taxonomies::TAX_TENSE_PRESENT;
+ }
+
+ /**
+ * Indicates if the meeting is in the past.
+ *
+ * @return bool
+ */
+ public function isPast(): bool
+ {
+ return ($this->endDt ?? $this->startDt) < Utilities::dateTimeNow();
+ }
+
+ /**
+ * Indicates if the meeting is multi-day.
+ *
+ * @return bool
+ */
+ public function isMultiDay(): bool
+ {
+ if ($this->endDt === null) {
+ return false;
+ }
+ return $this->startDt->format("Ymd") !== $this->endDt->format("Ymd");
+ }
+
+ /**
+ * Indicates if the meeting is labeled as all-day.
+ *
+ * Currently, TouchPoint doesn't have the capacity for this.
+ *
+ * TODO When TouchPoint supports an all-day marker, add it here. #184
+ *
+ * @return bool
+ */
+ public function isAllDay(): bool
+ {
+ return $this->startDt->format("His") === "000000";
+ }
+
+ /**
+ * Get the date portion for the meeting start, formatted.
+ *
+ * @return string
+ */
+ public function dateString(): string
+ {
+ if (!$this->isMultiDay() || $this->endDt === null) {
+ return DateFormats::DateStringFormatted($this->startDt);
+ }
+ return DateFormats::DateStringFormatted($this->startDt) . " &endash; " . DateFormats::DateStringFormatted($this->endDt);
+ }
+
+ /**
+ * Get the time portion for the meeting start, formatted.
+ *
+ * @return ?string
+ */
+ public function startTimeString(): ?string
+ {
+ if ($this->isAllDay())
+ return null;
+ return DateFormats::TimeStringFormatted($this->startDt);
+ }
+
+ /**
+ * Get the time portion for the meeting end, formatted. Null if no end is defined or end is same as start.
+ *
+ * @return ?string
+ */
+ public function endTimeString(): ?string
+ {
+ if ($this->endDt === null) {
+ return null;
+ }
+ return DateFormats::TimeStringFormatted($this->endDt);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function hasGeo(): bool
+ {
+ try {
+ $i = $this->involvement();
+ if ($i->locationName() !== $this->locationName()) {
+ // Meetings can't have geographical references yet. TODO Add Meeting Geographical ref when possible. #187
+ return false;
+ }
+
+ return $this->involvement()->hasGeo();
+ } catch (TouchPointWP_Exception) {
+ return false;
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function asGeoIFace(string $type = "unknown"): ?Geo
+ {
+ // In case location on meeting doesn't match location on
+ if (!$this->hasGeo()) {
+ return null;
+ }
+
+ try {
+ return $this->involvement()->asGeoIFace($type);
+ } catch (TouchPointWP_Exception) {
+ return null;
+ }
+ }
+
+ /**
+ * @inheritDoc
+ */
+ public function locationName(): ?string
+ {
+ $loc = get_post_meta($this->post_id, Meeting::MEETING_LOCATION_META_KEY, true);
+ if ($loc) {
+ return $loc;
+ }
+ try {
+ if ($this->post_id() !== $this->involvement()->post_id()) {
+ return $this->involvement()->locationName();
+ }
+ } catch (TouchPointWP_Exception) {}
+ return null;
+ }
}
\ No newline at end of file
diff --git a/src/TouchPoint-WP/MeetingArray.php b/src/TouchPoint-WP/MeetingArray.php
new file mode 100644
index 00000000..f136d112
--- /dev/null
+++ b/src/TouchPoint-WP/MeetingArray.php
@@ -0,0 +1,181 @@
+_meetings = $meetingArray;
+ $this->_involvement = $involvement;
+ }
+
+ public function __get(string $what)
+ {
+ if ($this->count() < 1) {
+ return null;
+ }
+
+ return match ($what) {
+ 'name' => $this->_involvement?->name ?? "",
+ 'mtgId' => -1 * $this[0]->mtgId, // Meeting groups have negative meetingIds, which are the negative of the first meeting in the group
+ 'mtgStartDt' => $this->mtgStartDt(),
+ 'mtgEndDt' => $this->mtgEndDt(),
+ 'location' => $this->_involvement?->location ?? null,
+ 'status' => $this->status(),
+ 'involvementId' => $this->_involvement?->involvementId ?? null,
+ default => null,
+ };
+ }
+
+ /**
+ * Get the first start time of the meetings in the array.
+ *
+ * @return DateTimeInterface
+ */
+ public function mtgStartDt(): DateTimeInterface
+ {
+ $minDate = null;
+ foreach ($this as $meeting) {
+ if ($minDate === null || $meeting->mtgStartDt < $minDate) {
+ $minDate = $meeting->mtgStartDt;
+ }
+ }
+ return $minDate;
+ }
+
+ /**
+ * Get the last end time (or start time) of the meetings in the array.
+ *
+ * @return DateTimeInterface
+ */
+ public function mtgEndDt(): DateTimeInterface
+ {
+ $maxDate = null;
+ foreach ($this as $meeting) {
+ if ($maxDate === null || ($meeting->mtgEndDt ?? $meeting->mtgStartDt) > $maxDate) {
+ $maxDate = ($meeting->mtgEndDt ?? $meeting->mtgStartDt);
+ }
+ }
+ return $maxDate;
+ }
+
+ /**
+ * Get the status of the meetings in the array.
+ *
+ * @return int
+ */
+ public function status(): int
+ {
+ $status = 0;
+ foreach ($this as $meeting) {
+ if ($meeting->status > $status) {
+ $status = $meeting->status;
+ }
+ }
+ return $status;
+ }
+
+ /**
+ * Retrieve an external iterator
+ *
+ * @link https://php.net/manual/en/iteratoraggregate.getiterator.php
+ */
+ public function getIterator(): ArrayIterator
+ {
+ return new ArrayIterator($this->_meetings);
+ }
+
+ /**
+ * Whether a offset exists
+ *
+ * @link https://php.net/manual/en/arrayaccess.offsetexists.php
+ */
+ public function offsetExists(mixed $offset): bool
+ {
+ return isset($this->_meetings[$offset]);
+ }
+
+ /**
+ * Offset to retrieve
+ *
+ * @link https://php.net/manual/en/arrayaccess.offsetget.php
+ */
+ public function offsetGet(mixed $offset): mixed
+ {
+ if (isset($this->_meetings[$offset])) {
+ return $this->_meetings[$offset];
+ }
+
+ return null;
+ }
+
+ /**
+ * Offset to set
+ *
+ * @link https://php.net/manual/en/arrayaccess.offsetset.php
+ */
+ public function offsetSet(mixed $offset, mixed $value): void
+ {
+ if ($offset === null) {
+ $this->_meetings[] = $value;
+ } else {
+ $this->_meetings[$offset] = $value;
+ }
+ }
+
+ /**
+ * Offset to unset
+ *
+ * @link https://php.net/manual/en/arrayaccess.offsetunset.php
+ */
+ public function offsetUnset(mixed $offset): void
+ {
+ if (isset($this->_meetings[$offset])) {
+ unset($this->_meetings[$offset]);
+ }
+ }
+
+ /**
+ * Count meetings in the array
+ *
+ * @link https://php.net/manual/en/countable.count.php
+ */
+ public function count(): int
+ {
+ return count($this->_meetings);
+ }
+}
\ No newline at end of file
diff --git a/src/TouchPoint-WP/Partner.php b/src/TouchPoint-WP/Partner.php
index 2b0e7d77..f4b21310 100644
--- a/src/TouchPoint-WP/Partner.php
+++ b/src/TouchPoint-WP/Partner.php
@@ -10,14 +10,21 @@
}
if ( ! TOUCHPOINT_COMPOSER_ENABLED) {
- require_once "api.php";
+ require_once "Interfaces/api.php";
require_once "jsInstantiation.php";
- require_once "updatesViaCron.php";
+ require_once "Interfaces/updatesViaCron.php";
require_once "Utilities.php";
}
use Exception;
use JsonSerializable;
+use stdClass;
+use tp\TouchPointWP\Interfaces\api;
+use tp\TouchPointWP\Interfaces\hasGeo;
+use tp\TouchPointWP\Interfaces\module;
+use tp\TouchPointWP\Interfaces\updatesViaCron;
+use tp\TouchPointWP\Utilities\NotableAttributes;
+use tp\TouchPointWP\Utilities\StringableArray;
use WP_Error;
use WP_Post;
use WP_Query;
@@ -26,10 +33,11 @@
/**
* An Outreach partner, corresponding to a family in TouchPoint.
*/
-class Partner implements api, JsonSerializable, updatesViaCron, geo, module
+class Partner extends PostTypeCapable implements api, JsonSerializable, updatesViaCron, hasGeo, module
{
use jsInstantiation {
jsInstantiation::enqueueForJsInstantiation as protected enqueueForJsInstantiationTrait;
+ jsonSerialize as public jsonSerializeTrait;
}
use extraValues;
@@ -59,10 +67,6 @@ class Partner implements api, JsonSerializable, updatesViaCron, geo, module
public string $name;
protected int $familyId;
- public int $post_id;
- public string $post_excerpt;
- protected WP_Post $post;
-
public const FAMILY_META_KEY = TouchPointWP::SETTINGS_PREFIX . "famId";
public const POST_TYPE = TouchPointWP::HOOK_PREFIX . "partner";
@@ -81,7 +85,7 @@ class Partner implements api, JsonSerializable, updatesViaCron, geo, module
*/
protected function __construct(object $object)
{
- $this->attributes = (object)[];
+ $this->attributes = new stdClass();
if (gettype($object) === "object" && get_class($object) == WP_Post::class) {
// WP_Post Object
@@ -118,12 +122,15 @@ protected function __construct(object $object)
throw new TouchPointWP_Exception("Could not construct a Partner with the information provided.");
}
- $terms = wp_get_post_terms(
- $this->post_id,
- [
- TouchPointWP::TAX_GP_CATEGORY
- ]
- );
+ $terms = [];
+ if (TouchPointWP::instance()->settings->global_primary_tax !== "") {
+ $terms = wp_get_post_terms(
+ $this->post_id,
+ [
+ Taxonomies::TAX_GP_CATEGORY
+ ]
+ );
+ }
if (is_array($terms) && count($terms) > 0) {
$hookLength = strlen(TouchPointWP::HOOK_PREFIX);
@@ -148,7 +155,7 @@ protected function __construct(object $object)
// Primary category
if (TouchPointWP::instance()->settings->global_primary_tax !== "") {
- $this->category = array_filter($terms, fn($t) => $t->taxonomy === TouchPointWP::TAX_GP_CATEGORY);
+ $this->category = array_filter($terms, fn($t) => $t->taxonomy === Taxonomies::TAX_GP_CATEGORY);
}
}
@@ -193,7 +200,7 @@ protected function __construct(object $object)
// Color!
if (count($this->category) > 0) {
$c = $this->category[0];
- $this->color = Utilities::getColorFor($c->slug, $c->taxonomy);
+ $this->color = Utilities\Colors::getColorFor($c->slug, $c->taxonomy);
}
$this->registerConstruction();
@@ -216,7 +223,7 @@ public static function init(): void
'hierarchical' => false,
'show_ui' => false,
'show_in_nav_menus' => true,
- 'show_in_rest' => false, // For the benefit of secure partners
+ 'show_in_rest' => true, // For the benefit of secure partners
'supports' => [
'title',
'custom-fields',
@@ -231,7 +238,17 @@ public static function init(): void
],
'query_var' => TouchPointWP::instance()->settings->global_slug,
'can_export' => false,
- 'delete_with_user' => false
+ 'delete_with_user' => false,
+ 'capability_type' => 'post',
+ 'capabilities' => [
+ 'create_posts' => 'do_not_allow', // Disable creating new posts
+ 'edit_posts' => 'do_not_allow', // Disable editing posts
+ 'edit_others_posts' => 'do_not_allow', // Disable editing others' posts
+ 'delete_posts' => 'do_not_allow', // Disable deleting posts
+ 'delete_others_posts' => 'do_not_allow', // Disable deleting others' posts
+ 'publish_posts' => 'do_not_allow', // Disable publishing posts
+ ],
+ 'map_meta_cap' => true, // Ensure users can still view posts
]
);
@@ -245,8 +262,6 @@ public static function init(): void
// Register function to return nulls instead of authors
add_filter('the_author', [self::class, 'filterAuthor'], 10, 3);
add_filter('get_the_author_display_name', [self::class, 'filterAuthor'], 10, 3);
-
- self::checkUpdates();
}
public static function checkUpdates(): void
@@ -290,6 +305,15 @@ public static function updateFromTouchPoint(bool $verbose = false)
$verbose &= TouchPointWP::currentUserIsAdmin();
+ TouchPointWP::instance()->setTpWpUserAsCurrent();
+
+ if (TouchPointWP::instance()->settings->enable_global !== 'on') {
+ if ($verbose) {
+ echo "Global is not enabled.";
+ }
+ return 0;
+ }
+
$customFev = TouchPointWP::instance()->settings->global_fev_custom;
$fevFields = $customFev;
@@ -327,11 +351,14 @@ public static function updateFromTouchPoint(bool $verbose = false)
$q['groupBy'] = 'FamilyId';
$q['context'] = 'partner';
+ $timeout = max(20, TouchPointWP::instance()->doPersonCount($q, 10) * 3);
+
// Submit to API
- $familyData = TouchPointWP::instance()->doPersonQuery($q, $verbose, 50);
+ $familyData = TouchPointWP::instance()->doPersonQuery($q, $verbose, $timeout);
$postsToKeep = [];
$count = 0;
+ $termsToKeep = [];
foreach ($familyData->people as $f) {
/** @var object $f */
@@ -372,13 +399,21 @@ public static function updateFromTouchPoint(bool $verbose = false)
if ($post instanceof WP_Error) {
new TouchPointWP_WPError($post);
+
+ if ($verbose) {
+ var_dump($post);
+ echo "";
+ }
+
continue;
}
/** @var $post WP_Post */
// Apply Types
- $f->familyEV = ExtraValueHandler::jsonToDataTyped($f->familyEV);
+ if ($f->familyEV !== null) {
+ $f->familyEV = ExtraValueHandler::jsonToDataTyped($f->familyEV);
+ }
// Post Content
$post->post_content = self::getFamEvAsContent($descriptionEv, $f, '');
@@ -386,14 +421,15 @@ public static function updateFromTouchPoint(bool $verbose = false)
// Excerpt / Summary
$post->post_excerpt = self::getFamEvAsContent($summaryEv, $f, null);
- // Partner Category
+ // Partner Category This can't be moved to Taxonomy class because values aren't known.
if ($categoryEv !== '') {
$category = $f->familyEV->$categoryEv->value ?? null;
// Insert Term if new
- if ($category !== null && ! Utilities::termExists($category, TouchPointWP::TAX_GP_CATEGORY)) {
- Utilities::insertTerm(
+ $term = Taxonomies::termExists($category, Taxonomies::TAX_GP_CATEGORY);
+ if ($category !== null && !$term) {
+ $term = Taxonomies::insertTerm(
$category,
- TouchPointWP::TAX_GP_CATEGORY,
+ Taxonomies::TAX_GP_CATEGORY,
[
'description' => $category,
'slug' => sanitize_title($category)
@@ -401,8 +437,16 @@ public static function updateFromTouchPoint(bool $verbose = false)
);
TouchPointWP::queueFlushRewriteRules();
}
- // Apply term to post
- wp_set_post_terms($post->ID, $category, TouchPointWP::TAX_GP_CATEGORY, false);
+ if (is_wp_error($term)) {
+ new TouchPointWP_WPError($term);
+ } else {
+ if ( !!$term && !!$term['term_id']) {
+ $term['term_id'] = intval($term['term_id']);
+ $termsToKeep[] = $term['term_id'];
+ // Apply term to post
+ wp_set_post_terms($post->ID, [$term['term_id']], Taxonomies::TAX_GP_CATEGORY, false);
+ }
+ }
}
// Title & Slug -- slugs should only be updated if there's a reason, like a title change. Otherwise, they increment.
@@ -453,7 +497,9 @@ public static function updateFromTouchPoint(bool $verbose = false)
// Positioning.
if ($latEv !== "" && $lngEv !== "" && // Has EV Lat/Lng
- property_exists($f->familyEV, $latEv) && property_exists($f->familyEV, $lngEv) &&
+ is_object($f->familyEV) &&
+ property_exists($f->familyEV, $latEv) &&
+ property_exists($f->familyEV, $lngEv) &&
$f->familyEV->$latEv !== null && $f->familyEV->$latEv->value !== null &&
$f->familyEV->$lngEv !== null && $f->familyEV->$lngEv->value !== null) {
update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "geo_lat", Utilities::toFloatOrNull($f->familyEV->$latEv->value));
@@ -491,13 +537,24 @@ public static function updateFromTouchPoint(bool $verbose = false)
'post__not_in' => $postsToKeep
]
);
-
foreach ($q->get_posts() as $post) {
set_time_limit(10);
wp_delete_post($post->ID, true);
$count++;
}
+ // Delete terms that are no longer used
+ if (TouchPointWP::instance()->settings->global_primary_tax !== "") {
+ $terms = get_terms(
+ ['taxonomy' => Taxonomies::TAX_GP_CATEGORY, 'hide_empty' => false, 'exclude' => $termsToKeep]
+ );
+ if (!is_wp_error($terms)) {
+ foreach ($terms as $term) {
+ wp_delete_term($term->term_id, Taxonomies::TAX_GP_CATEGORY);
+ }
+ }
+ }
+
if ($count !== 0) {
TouchPointWP::instance()->settings->set('global_cron_last_run', time());
}
@@ -505,6 +562,8 @@ public static function updateFromTouchPoint(bool $verbose = false)
if ($count > 0) {
TouchPointWP::instance()->flushRewriteRules();
}
+
+ TouchPointWP::instance()->unsetTpWpUserAsCurrent();
return $count;
}
@@ -519,9 +578,26 @@ public static function updateFromTouchPoint(bool $verbose = false)
*/
public static function templateFilter(string $template): string
{
- if (apply_filters(TouchPointWP::HOOK_PREFIX . 'use_default_templates', true, self::class)) {
+ $className = self::class;
+ $useTemplates = true;
+
+ /**
+ * Determines whether the plugin's default templates should be used. Theme developers can return false in this
+ * filter to prevent the default templates from applying, especially if they conflict with the theme.
+ *
+ * Default is true.
+ *
+ * TODO merge with the same filter in Involvement
+ *
+ * @param bool $value The value to return. True will allow the default templates to be applied.
+ * @param string $className The name of the class calling for the template.
+ *
+ *@since 0.0.6 Added
+ *
+ */
+ if (!!apply_filters('tp_use_default_templates', $useTemplates, $className)) {
$postTypesToFilter = self::POST_TYPE;
- $templateFilesToOverwrite = TouchPointWP::TEMPLATES_TO_OVERWRITE;
+ $templateFilesToOverwrite = self::TEMPLATES_TO_OVERWRITE;
if ( ! in_array(ltrim(strrchr($template, '/'), '/'), $templateFilesToOverwrite)) {
return $template;
@@ -630,12 +706,27 @@ public static function listShortcode($params = [], string $content = ""): string
}
$params = array_change_key_case($params, CASE_LOWER);
+ $useCss = true;
+ $className = self::class;
+
// set some defaults
/** @noinspection SpellCheckingInspection */
$params = shortcode_atts(
[
'class' => self::$containerClass,
- 'includecss' => apply_filters(TouchPointWP::HOOK_PREFIX . 'use_css', true, self::class),
+
+ /**
+ * Determines whether or not to automatically include the plugin-default CSS. Return false to use your
+ * own CSS instead.
+ *
+ * @since 0.0.15 Added
+ *
+ * TODO merge with the same filter in Involvement
+ *
+ * @param bool $useCss Whether or not to include the default CSS. True = include
+ * @param string $className The name of the current calling class.
+ */
+ 'includecss' => apply_filters('tp_use_css', $useCss, $className),
'itemclass' => self::$itemClass,
],
$params,
@@ -703,7 +794,7 @@ public static function filterShortcode($params = []): string
// language=javascript
"
tpvm.addEventListener('Partner_fromObjArray', function() {
- TP_Partner.initFilters();
+ tpvm.TP_Partner.initFilters();
});"
);
self::$filterJsAdded = true;
@@ -731,7 +822,7 @@ protected static final function filterDropdownHtml($params = []): string
$params = shortcode_atts(
[
'class' => "TouchPoint-Partner filterBar",
- 'filters' => strtolower(implode(",", ["partner_category"])),
+ 'filters' => "partner_category",
'includeMapWarnings' => self::$_hasArchiveMap
],
$params,
@@ -752,15 +843,17 @@ protected static final function filterDropdownHtml($params = []): string
if (in_array('partner_category', $filters)
&& TouchPointWP::instance()->settings->global_primary_tax !== "") {
- $tax = get_taxonomy(TouchPointWP::TAX_GP_CATEGORY);
- $name = substr($tax->name, strlen(TouchPointWP::SETTINGS_PREFIX));
- $content .= "";
}
if ($params['includeMapWarnings']) {
@@ -1074,7 +1167,6 @@ public static function mapShortcode($params = [], string $content = ""): string
$script
);
- // TODO move the style to a css file... or something.
$content = "";
} else {
$content = "";
@@ -1105,8 +1197,7 @@ public static function filterPublishDate($theDate, $format, $post = null): strin
if ($format == '') {
try {
$gp = self::fromPost($post);
- $theDate = $gp->notableAttributes();
- $theDate = implode(TouchPointWP::$joiner, $theDate);
+ $theDate = $gp->notableAttributes()->join();
} catch (TouchPointWP_Exception $e) {
}
} else {
@@ -1151,7 +1242,12 @@ public static function filterAuthor($author): string
public static function getFamEvAsContent(string $ev, object $famObj, ?string $default): ?string
{
$newContent = $default;
- if ($ev !== "" && property_exists($famObj->familyEV, $ev) && $famObj->familyEV->$ev !== null && $famObj->familyEV->$ev->value !== null) {
+ if ($ev !== "" &&
+ isset($famObj->familyEV) &&
+ property_exists($famObj->familyEV, $ev) &&
+ $famObj->familyEV->$ev !== null &&
+ $famObj->familyEV->$ev->value !== null) {
+
$newContent = $famObj->familyEV->$ev->value;
$newContent = Utilities::standardizeHtml($newContent, "partner-import");
}
@@ -1163,29 +1259,56 @@ public static function getFamEvAsContent(string $ev, object $famObj, ?string $de
/**
* Get notable attributes as strings.
*
- * @return string[]
+ * @param array|StringableArray $exclude Attributes listed here will be excluded. (e.g. if shown for a parent, not needed here.)
+ *
+ * @return NotableAttributes
+ * @since 0.0.6 Added
+ * @since 0.0.96 Changed to use NotableAttributes instead of array.
+ *
*/
- public function notableAttributes(): array
+ public function notableAttributes(array|StringableArray $exclude = []): NotableAttributes
{
- $r = [];
+ $attrs = new NotableAttributes();
+ if (!is_array($exclude)) {
+ $exclude = $exclude->getArrayCopy();
+ }
+ $l = $this->locationName();
if ($this->decoupleLocation) {
- $r[] = TouchPointWP::instance()->settings->global_name_singular_decoupled;
- } elseif ($this->location !== "" && $this->location !== null) {
- $r[] = $this->location;
+ $attrs['secure'] = $l;
+ } elseif ($l) {
+ $attrs['location'] = $l;
}
+ unset($l);
foreach ($this->category as $c) {
- $r[] = $c->name;
+ $attrs['category'] = $c->name;
}
// Not shown on map (only if there is a map, and the partner isn't on it because they lack geo.)
if (self::$_hasArchiveMap && $this->geo === null && ! $this->decoupleLocation) {
- $r[] = __("Not Shown on Map", "TouchPoint-WP");
+ $attrs['hidden'] = __("Not Shown on Map", "TouchPoint-WP");
TouchPointWP::requireScript("fontAwesome"); // For map icons
}
- return apply_filters(TouchPointWP::HOOK_PREFIX . "partner_attributes", $r, $this);
+ $attrs = $this->processAttributeExclusions($attrs, $exclude);
+ $partner = $this;
+
+ /**
+ * Allows for manipulation of the notable attributes strings for an Partner. An array of strings.
+ * Typically, these are the standardized strings that appear on the Partner to give information about it,
+ * such as the type and location.
+ *
+ * @see Partner::notableAttributes()
+ * @see PostTypeCapable::notableAttributes()
+ *
+ * @since 0.0.6 Added
+ * @since 0.0.96 Changed to use NotableAttributes instead of array.
+ *
+ * @param NotableAttributes $attrs The list of notable attributes.
+ * @param Partner $partner The Partner object.
+ */
+ return apply_filters("tp_partner_attributes", $attrs, $partner);
}
/**
@@ -1207,28 +1330,47 @@ protected function enqueueForJsInstantiation(): bool
* Returns the html with buttons for actions the user can perform. This must be called *within* an element with
* the `data-tp-partner` attribute with the post_id as the value or 0 for secure partners.
*
- * @param ?string $context A reference to where the action buttons are meant to be used.
- * @param string $btnClass A string for classes to add to the buttons. Note that buttons can be a or button
- * elements.
+ * @param string|null $context A string that gives filters some context for where the request is coming from
+ * @param string $btnClass HTML class names to put into the buttons/links
+ * @param bool $withTouchPointLink Whether to include a link to the item within TouchPoint. (not used)
+ * @param bool $absoluteLinks Set true to make the links absolute, so they work from apps or emails.
*
- * @return string
+ * @return StringableArray
*/
- public function getActionButtons(string $context = null, string $btnClass = ""): string
+ public function getActionButtons(?string $context = null, string $btnClass = "", bool $withTouchPointLink = true, bool $absoluteLinks = false): StringableArray
{
$this->enqueueForJsInstantiation();
- $ret = "";
+ $ret = new StringableArray();
if ($btnClass !== "") {
$btnClass = " class=\"$btnClass\"";
}
// Show on map button. (Only works if map is called before this is.)
- if (self::$_hasArchiveMap && ! $this->decoupleLocation && $this->geo !== null) {
+ if (self::$_hasArchiveMap && ! $this->decoupleLocation && $this->geo !== null && !$absoluteLinks) {
$text = __("Show on Map", "TouchPoint-WP");
- $ret .= " ";
- }
-
- return apply_filters(TouchPointWP::HOOK_PREFIX . "partner_actions", $ret, $this, $context, $btnClass);
+ $ret['map'] = " ";
+ }
+
+ // TouchPoint link is excluded for privacy, and because we don't really have People IDs readily available.
+
+ /**
+ * Allows for manipulation of the action buttons for a Partner. This is the list of buttons that appear
+ * on the Partner to allow the user to interact with it.
+ *
+ * @since 0.0.7 Added
+ *
+ * @see Partner::getActionButtons()
+ * @see PostTypeCapable::getActionButtons()
+ *
+ * @param StringableArray $ret The list of action buttons.
+ * @param Partner $this The Partner object.
+ * @param ?string $context A reference to where the action buttons are meant to be used.
+ * @param string $btnClass A string for classes to add to the buttons. Note that buttons can be 'a' or 'button'
+ * elements.
+ * @param bool $absoluteLinks Set true to make the links absolute, so they work from apps or emails.
+ */
+ return apply_filters("tp_partner_actions", $ret, $this, $context, $btnClass, $absoluteLinks);
}
public static function getJsInstantiationString(): string
@@ -1244,7 +1386,7 @@ public static function getJsInstantiationString(): string
$listStr = json_encode($queue);
return "\ttpvm.addEventListener('Partner_class_loaded', function() {
- TP_Partner.fromObjArray($listStr);\n\t});\n";
+ tpvm.TP_Partner.fromObjArray($listStr);\n\t});\n";
}
/**
@@ -1324,6 +1466,30 @@ protected function removeExtraValueWP(string $name): bool
return delete_post_meta($this->post_id, self::META_FEV_PREFIX . $name);
}
+ /**
+ * Indicates if the given post can be instantiated as a Partner.
+ *
+ * @param WP_Post $post
+ *
+ * @return bool
+ */
+ public static function postIsType(WP_Post $post): bool
+ {
+ return intval(get_post_meta($post->ID, self::FAMILY_META_KEY, true)) > 0;
+ }
+
+ /**
+ * Indicates if the given post type name is the post type for this class.
+ *
+ * @param string $postType
+ *
+ * @return bool
+ */
+ public static function postTypeMatches(string $postType): bool
+ {
+ return $postType === self::POST_TYPE;
+ }
+
/**
* Serialize. Mostly, manage the security requirements.
*
@@ -1349,7 +1515,7 @@ public function jsonSerialize(): object
}
}
- return $this;
+ return $this->jsonSerializeTrait();
}
/**
@@ -1366,17 +1532,32 @@ public function hasGeo(): bool
return $this->geo !== null;
}
- public function asGeoIFace(string $type = "unknown"): ?object
+ public function asGeoIFace(string $type = "unknown"): ?Geo
{
if ($this->hasGeo()) {
- return (object)[
- 'lat' => $this->geo->lat,
- 'lng' => $this->geo->lng,
- 'human' => $this->name,
- 'type' => $type
- ];
+ return new Geo(
+ $this->geo->lat,
+ $this->geo->lng,
+ $this->name,
+ $type
+ );
}
return null;
}
+
+ /**
+ * Get the name of the location.
+ *
+ * @return ?string
+ */
+ public function locationName(): ?string
+ {
+ if ($this->decoupleLocation) {
+ return TouchPointWP::instance()->settings->global_name_singular_decoupled;
+ } elseif ($this->location !== "" && $this->location !== null) {
+ return $this->location;
+ }
+ return null;
+ }
}
diff --git a/src/TouchPoint-WP/Person.php b/src/TouchPoint-WP/Person.php
index f63e2c0e..06d21aad 100644
--- a/src/TouchPoint-WP/Person.php
+++ b/src/TouchPoint-WP/Person.php
@@ -10,23 +10,27 @@
}
if ( ! TOUCHPOINT_COMPOSER_ENABLED) {
- require_once "api.php";
+ require_once "Interfaces/api.php";
require_once "extraValues.php";
require_once "jsInstantiation.php";
- require_once "updatesViaCron.php";
+ require_once "Interfaces/actionButtons.php";
+ require_once "Interfaces/updatesViaCron.php";
require_once "InvolvementMembership.php";
require_once "Utilities.php";
require_once "Utilities/PersonQuery.php";
- require_once "Utilities/Session.php";
}
use Exception;
use JsonSerializable;
use stdClass;
+use tp\TouchPointWP\Interfaces\actionButtons;
+use tp\TouchPointWP\Interfaces\api;
+use tp\TouchPointWP\Interfaces\module;
+use tp\TouchPointWP\Interfaces\updatesViaCron;
use tp\TouchPointWP\Utilities\Http;
use tp\TouchPointWP\Utilities\PersonArray;
use tp\TouchPointWP\Utilities\PersonQuery;
-use tp\TouchPointWP\Utilities\Session;
+use tp\TouchPointWP\Utilities\StringableArray;
use WP_Term;
use WP_User;
@@ -40,11 +44,8 @@
* @property ?int campus_term_id The Campus term ID
* @property-read ?WP_Term resCode The ResCode taxonomy, if present
* @property ?int rescode_term_id The ResCode term ID
- * @property ?string $loginSessionToken A token that is saved on the Session variable and used to ensure links
- * aren't used between sessions.
- * @property ?string $loginToken A token used to validate the user.
*/
-class Person extends WP_User implements api, JsonSerializable, module, updatesViaCron
+class Person extends WP_User implements api, JsonSerializable, module, updatesViaCron, actionButtons
{
use jsInstantiation;
use extraValues;
@@ -105,15 +106,13 @@ class Person extends WP_User implements api, JsonSerializable, module, updatesVi
'user_activation_key',
'spam',
'show_admin_bar_front',
-// 'role', // Excluding prevents this from being set through __set
+// 'role', // Excluding prevents this from being set through __set
'locale'
];
private const FIELDS_FOR_META = [
'picture',
'familyId',
- 'loginToken',
- 'loginSessionToken',
'campus_term_id',
'rescode_term_id'
];
@@ -139,7 +138,7 @@ protected function __construct($id = 0, $name = '', $site_id = '')
*
* @return Person|TouchPointWP_Exception If a WP User ID is not provided, this exception is returned.
*/
- public static function fromQueryResult($queryResult): Person
+ public static function fromQueryResult($queryResult): Person|TouchPointWP_Exception
{
if (is_numeric($queryResult)) {
return new Person($queryResult);
@@ -395,11 +394,11 @@ public static function peopleListShortcode($params = [], string $content = ""):
/** @noinspection SpellCheckingInspection */
$params = shortcode_atts(
[
- 'class' => 'TouchPoint-person people-list',
+ 'class' => 'people-list',
'invid' => null,
- 'id' => wp_unique_id('tp-actions-'),
'withsubgroups' => false,
- 'btnclass' => 'btn button'
+ 'btnclass' => 'btn button',
+ 'context' => ''
],
$params,
self::SHORTCODE_PEOPLE_LIST
@@ -435,7 +434,7 @@ public static function peopleListShortcode($params = [], string $content = ""):
self::$_indexingQueries['inv'][$iid] = [
'invId' => $iid,
'memTypes' => null,
-// 'subGroups' => null,
+// 'subGroups' => null,
'with_subGroups' => false // populated below
];
}
@@ -476,6 +475,11 @@ public static function peopleListShortcode($params = [], string $content = ""):
$people = $q->get_results();
$btnClass = $params['btnclass'];
+ $listClass = $params['class'];
+
+ if ($content === "") {
+ $content = "";
+ }
$loadedPart = get_template_part('person-list', 'person-list');
if ($loadedPart === false) {
@@ -485,8 +489,10 @@ public static function peopleListShortcode($params = [], string $content = ""):
$out .= ob_get_clean();
}
// TODO DIR make sure this actually works with external partials.
- // TODO DIR provide an alternate if there are no people available.
-
+
+ if (trim($out) === "") {
+ return $content;
+ }
return $out;
}
@@ -654,7 +660,7 @@ public static function updateCron(): void
{
try {
self::updateFromTouchPoint();
- } catch (Exception $ex) {
+ } catch (Exception) {
}
}
@@ -663,10 +669,10 @@ public static function updateCron(): void
*
* @param bool $verbose Whether to print debugging info.
*
- * @return false|int False on failure, or the number of partner posts that were updated or deleted.
+ * @return false|int False on failure, or the number of people that were updated or deleted.
* @throws TouchPointWP_Exception
*/
- protected static function updateFromTouchPoint(bool $verbose = false)
+ protected static function updateFromTouchPoint(bool $verbose = false): bool|int
{
global $wpdb;
@@ -688,16 +694,27 @@ protected static function updateFromTouchPoint(bool $verbose = false)
// Find People Lists in post content and add their involvements to the query.
if (TouchPointWP::instance()->settings->enable_people_lists) {
- $posts = Utilities::getPostContentWithShortcode(self::SHORTCODE_PEOPLE_LIST);
+ $posts = Utilities\Database::getPostContentWithShortcode(self::SHORTCODE_PEOPLE_LIST);
+
+ global $post;
+ $originalPost = $post;
self::$_indexingMode = true;
foreach ($posts as $postI) {
- global $post;
+ if (!is_object($postI)) {
+ continue;
+ }
+ if (!property_exists($postI, 'post_content')) {
+ continue;
+ }
+
$post = $postI;
set_time_limit(10);
apply_shortcodes($postI->post_content);
}
self::$_indexingMode = false;
+
+ $post = $originalPost;
}
// Add Involvement Leaders to the query.
@@ -707,7 +724,7 @@ protected static function updateFromTouchPoint(bool $verbose = false)
// Get the InvIds for the Involvement Type's Posts
$postType = $type->postTypeWithPrefix();
- $key = Involvement::INVOLVEMENT_META_KEY;
+ $key = TouchPointWP::INVOLVEMENT_META_KEY;
global $wpdb;
/** @noinspection SqlResolve */
$sql = "SELECT pm.meta_value AS iid
@@ -723,11 +740,11 @@ protected static function updateFromTouchPoint(bool $verbose = false)
self::$_indexingQueries['inv'][$iid] = [
'invId' => $iid,
'memTypes' => $type->leaderTypeInts(),
-// 'subGroups' => null,
+// 'subGroups' => null,
'with_subGroups' => false
];
} elseif (is_array(self::$_indexingQueries['inv'][$iid]['memTypes'])) {
- $r = array_merge(
+ $r = array_merge(
self::$_indexingQueries['inv'][$iid]['memTypes'],
$type->leaderTypeInts()
);
@@ -765,8 +782,12 @@ protected static function updateFromTouchPoint(bool $verbose = false)
self::$_indexingQueries['meta']['pev'] = TouchPointWP::instance()->getPersonEvFields($pevFieldIds);
self::$_indexingQueries['context'] = 'peopleLists';
+ $timeout = max((count(self::$_indexingQueries['pid']) / 2) + (count(self::$_indexingQueries['inv']) * 10) + 10, 50);
+
// Submit to API
- $people = TouchPointWP::instance()->doPersonQuery(self::$_indexingQueries, $verbose, 50);
+ $people = TouchPointWP::instance()->doPersonQuery(self::$_indexingQueries, $verbose, $timeout);
+
+ TouchPointWP::instance()->setTpWpUserAsCurrent();
set_time_limit(count($people->people) * 5 + 10); // a very generous time limit.
@@ -781,6 +802,8 @@ protected static function updateFromTouchPoint(bool $verbose = false)
}
}
+ TouchPointWP::instance()->unsetTpWpUserAsCurrent();
+
return $count;
}
@@ -948,11 +971,11 @@ public static function updatePersonFromApiData($pData, bool $allowCreation, bool
$person->picture = $pData->Picture;
// resCodes and Campuses
- $person->rescode_term_id = TouchPointWP::getTaxTermId(TouchPointWP::TAX_RESCODE, $pData->ResCode);
+ $person->rescode_term_id = Taxonomies::getTaxTermId(Taxonomies::TAX_RESCODE, $pData->ResCode);
if (TouchPointWP::instance()->settings->enable_campuses !== "on") {
$person->campus_term_id = null;
} else {
- $person->campus_term_id = TouchPointWP::getTaxTermId(TouchPointWP::TAX_CAMPUS, $pData->CampusId);
+ $person->campus_term_id = Taxonomies::getTaxTermId(Taxonomies::TAX_CAMPUS, $pData->CampusId);
}
// Deliberately do not update usernames or passwords, as those could be set by any number of places for any number of reasons.
@@ -1086,21 +1109,6 @@ protected function submitUpdate(): void
$this->_userMetaToUpdate = [];
}
- /**
- * Used for setting or clearing a user's login tokens
- *
- * @param string|null $session
- * @param string|null $login
- *
- * @return void
- */
- public function setLoginTokens(?string $session, ?string $login): void
- {
- $this->loginToken = $login;
- $this->loginSessionToken = $session;
- $this->submitUpdate();
- }
-
/**
* Send WordPress User IDs to WordPress for storage in an extra value
*
@@ -1112,12 +1120,12 @@ protected static function updatePeopleWordPressIDs()
return;
}
try {
- TouchPointWP::instance()->apiPost('person_wpIds', [
+ TouchPointWP::instance()->api->pyPost('person_wpIds', [
'people' => self::$_peopleWhoNeedWpIdUpdatedInTouchPoint,
'evName' => TouchPointWP::instance()->settings->people_ev_wpId
]);
self::$_peopleWhoNeedWpIdUpdatedInTouchPoint = [];
- } catch (Exception $ex) { // If it fails this time, it'll probably get fixed next time
+ } catch (Exception) { // If it fails this time, it'll probably get fixed next time
}
}
@@ -1135,7 +1143,7 @@ protected function campus(): ?WP_Term
if ($this->campus_term_id === null) {
$this->_campus = null;
} else {
- $this->_campus = WP_Term::get_instance($this->campus_term_id, TouchPointWP::TAX_CAMPUS);
+ $this->_campus = WP_Term::get_instance($this->campus_term_id, Taxonomies::TAX_CAMPUS);
}
$this->_campusLoaded = true;
}
@@ -1154,7 +1162,7 @@ protected function resCode(): ?WP_Term
if ($this->rescode_term_id === null) {
$this->_resCode = null;
} else {
- $this->_resCode = WP_Term::get_instance($this->rescode_term_id, TouchPointWP::TAX_RESCODE);
+ $this->_resCode = WP_Term::get_instance($this->rescode_term_id, Taxonomies::TAX_RESCODE);
}
$this->_resCodeLoaded = true;
}
@@ -1164,33 +1172,77 @@ protected function resCode(): ?WP_Term
/**
* Returns the html with buttons for actions the user can perform. This must be called *within* an element with
- * the `data-tp-person` attribute with the invId as the value.
+ * the `data-tp-person` attribute with the peopleId as the value.
*
* @param ?string $context A reference to where the action buttons are meant to be used.
* @param string $btnClass A string for classes to add to the buttons. Note that buttons can be a or button
* elements.
+ * @param bool $withTouchPointLink
+ * @param bool $absoluteLinks
*
- * @return string
+ * @return StringableArray
*/
- public function getActionButtons(string $context = null, string $btnClass = ""): string
+ public function getActionButtons(?string $context = null, string $btnClass = "", bool $withTouchPointLink = true, bool $absoluteLinks = false): StringableArray
{
- TouchPointWP::requireScript('swal2-defer');
- TouchPointWP::requireScript('base-defer');
- $this->enqueueForJsInstantiation();
+ if (!$absoluteLinks) {
+ TouchPointWP::requireScript('swal2-defer');
+ TouchPointWP::requireScript('base-defer');
+ $this->enqueueForJsInstantiation();
+ Person::enqueueUsersForJsInstantiation();
+ }
+ $classesOnly = $btnClass;
if ($btnClass !== "") {
$btnClass = " class=\"$btnClass\"";
}
+ global $wp;
+ $baseLink = add_query_arg($wp->query_vars, home_url($wp->request));
- $ret = "";
+ $ret = new StringableArray();
if (self::allowContact()) {
$text = __("Contact", "TouchPoint-WP");
- TouchPointWP::enqueueActionsStyle('person-contact');
- self::enqueueUsersForJsInstantiation();
- $ret = " ";
+ $pid = $this->peopleId;
+ if (!$absoluteLinks) {
+ $ret['contact'] = " ";
+ TouchPointWP::enqueueActionsStyle('person-contact');
+ self::enqueueUsersForJsInstantiation();
+ } else {
+ $ret['contact'] = "$text ";
+ }
}
- return apply_filters(TouchPointWP::HOOK_PREFIX . "person_actions", $ret, $this, $context, $btnClass);
+ if ($withTouchPointLink && TouchPointWP::currentUserIsAdmin()) {
+ // Translators: %s is the system name, "TouchPoint" by default.
+ $title = wp_sprintf(__("Person in %s", "TouchPoint-WP"), TouchPointWP::instance()->settings->system_name);
+ $logo = TouchPointWP::TouchPointIcon();
+ $ret['inv_tp'] = "getProfileUrl()}\" title=\"$title\" class=\"tp-TouchPoint-logo $classesOnly\">$logo";
+ }
+
+ /**
+ * Allows for manipulation of the action buttons for a Person. This is the list of buttons that appear
+ * on the Person to allow the user to interact with them.
+ *
+ * @since 0.0.90 Added
+ * @since 0.0.96 Adjusted parameters and return value to have type StringableArray rather than string. If the
+ * return value is not a StringableArray, it will be forced into one.
+ *
+ * @see Person::getActionButtons()
+ *
+ * @param StringableArray $ret The list of action buttons.
+ * @param Person $this The Person object.
+ * @param ?string $context A reference to where the action buttons are meant to be used.
+ * @param string $btnClass A string for classes to add to the buttons. Note that buttons can be 'a' or 'button'
+ * elements.
+ */
+ $ret = apply_filters("tp_person_actions", $ret, $this, $context, $btnClass);
+
+ if ($ret instanceof StringableArray) {
+ return $ret;
+ }
+
+ $r = new StringableArray();
+ $r->append($ret);
+ return $r;
}
/**
@@ -1203,11 +1255,8 @@ public function getActionButtons(string $context = null, string $btnClass = ""):
public static function enqueueForJS_byPeopleId(int $pid): ?bool
{
$p = self::fromPeopleId($pid);
- if ($p === null) {
- return null;
- }
- return $p->enqueueForJsInstantiation();
+ return $p?->enqueueForJsInstantiation();
}
/**
@@ -1221,6 +1270,29 @@ public static function enqueueUsersForJsInstantiation(): void
self::$_enqueueUsersForJsInstantiation = true;
}
+
+ /**
+ * TODO there has to be a better way to do this.
+ *
+ * @return Person[]
+ */
+ protected static function getPeopleFromTransient(): array
+ {
+ $loggedInUser = TouchPointWP::currentUserPerson();
+
+ if ($loggedInUser === null) {
+ return [];
+ }
+
+ $peopleTransient = get_transient("tp_person_ident_" . $loggedInUser->peopleId);
+ if (!$peopleTransient || !isset($peopleTransient->people)) {
+ return [];
+ }
+
+ return $peopleTransient->people;
+ }
+
+
/**
* Return the instances to be used for instantiation.
*
@@ -1235,8 +1307,7 @@ protected static function getQueueForJsInstantiation(): array
}
if (self::$_enqueueUsersForJsInstantiation) {
- $s = Session::instance();
- $list = array_merge($list, $s->people ?? []);
+ $list = array_merge($list, self::getPeopleFromTransient());
}
// Remove duplicates. (array_unique won't handle objects cleanly)
@@ -1278,14 +1349,15 @@ public static function getJsInstantiationString(): string
$listStr = json_encode($queue);
$out = "\ttpvm.addOrTriggerEventListener('Person_class_loaded', function() {\n";
- $out .= "\t\tTP_Person.fromObjArray($listStr);\n";
+ $out .= "\t\ttpvm.TP_Person.fromObjArray($listStr);\n";
- if (self::$_enqueueUsersForJsInstantiation) {
- $s = Session::instance();
- $pFids = json_encode($s->primaryFam ?? []);
- $sFids = json_encode($s->secondaryFam ?? []);
- $out .= "\t\tTP_Person.identByFamily($pFids, $sFids);\n";
- }
+// TODO restore, better.
+// if (self::$_enqueueUsersForJsInstantiation) {
+// $s = Session::instance();
+// $pFids = json_encode($s->primaryFam ?? []);
+// $sFids = json_encode($s->secondaryFam ?? []);
+// $out .= "\t\tTP_Person.identByFamily($pFids, $sFids);\n";
+// }
$out .= "\t});\n";
@@ -1330,6 +1402,10 @@ protected static function generateUserName(object $pData): string
{
// Best. Matches TouchPoint username. However, it is possible users won't have usernames.
foreach ($pData->Usernames as $u) {
+ if (stripos($u, 'admin') !== false) {
+ continue;
+ }
+
if ( ! username_exists($u)) {
return $u;
}
@@ -1352,28 +1428,52 @@ protected static function generateUserName(object $pData): string
return self::BACKUP_USER_PREFIX . $pData->PeopleId;
}
- public function hasProfilePage(): bool
+ /**
+ * Returns true if the person has posts and therefore has a user page.
+ *
+ * @return bool
+ */
+ public function hasUserPage(): bool
{
return count_user_posts($this->ID) > 0;
}
- public function getProfileUrl(): ?string
+
+ /**
+ * Get the link to the person's author post page.
+ *
+ * @return ?string
+ */
+ public function getUserUrl(): ?string
{
- if ( ! ! apply_filters(TouchPointWP::HOOK_PREFIX . 'use_person_link', $this->hasProfilePage(), $this)) {
+ if ($this->hasUserPage()) {
return get_author_posts_url($this->ID);
} else {
return null;
}
}
+ /**
+ * Returns the person's TouchPoint profile URL.
+ *
+ * @return ?string
+ */
+ public function getProfileUrl(): ?string
+ {
+ $tpHost = TouchPointWP::instance()->host();
+ return "$tpHost/Person/$this->peopleId";
+ }
+
/**
* Take an array of Person-ish objects and return a nicely human-readable list of names.
*
* @param Person[]|PersonArray $people TODO make api compliant with Person object--remove coalesces. (#120)
*
+ * TODO merge with Utilities::stringArrayToListString()
+ *
* @return ?string Returns a human-readable list of names, nicely formatted with commas and such.
*/
- public static function arrangeNamesForPeople($people, int $familyLimit = 3): ?string
+ public static function arrangeNamesForPeople($people, bool $asLink = false, int $familyLimit = 3): ?string
{
$people = self::groupByFamily($people);
if (count($people) === 0) {
@@ -1389,17 +1489,17 @@ public static function arrangeNamesForPeople($people, int $familyLimit = 3): ?st
$and = ' & ';
$useOxford = false;
foreach ($people as $family) {
- $fn = self::formatNamesForFamily($family);
- if (strpos($fn, ', ') !== false) {
+ $fn = self::formatNamesForFamily($family, $asLink);
+ if (str_contains($fn, ', ')) {
$comma = '; ';
- $useOxford = true;
}
- if (strpos($fn, ' & ') !== false) {
+ if (str_contains($fn, ' & ')) {
$and = ' ' . __('and', 'TouchPoint-WP') . ' ';
$useOxford = true;
}
$familyNames[] = $fn;
}
+
if ($andOthers) {
$last = _x("others", "list of people, and *others*", "TouchPoint-WP");
} else {
@@ -1427,7 +1527,7 @@ public static function arrangeNamesForPeople($people, int $familyLimit = 3): ?st
*
* @return ?string Returns a human-readable list of names, nicely formatted with commas and such.
*/
- protected static function formatNamesForFamily(array $family): ?string // Standardize API (#120)
+ protected static function formatNamesForFamily(array $family, bool $asLink = false): ?string // Standardize API (#120)
{
if (count($family) < 1) {
return null;
@@ -1440,24 +1540,41 @@ protected static function formatNamesForFamily(array $family): ?string // Stand
usort($family, fn($a, $b) => ($a->GenderId ?? 0) <=> ($b->GenderId ?? 0));
$isFirst = true;
+ $hasLink = false;
foreach ($family as $p) {
$last = $p->LastName ?? $p->last_name;
if ($standingLastName != $last) {
$string .= " " . $standingLastName;
+ if ($hasLink) {
+ $string .= "";
+ }
+
$standingLastName = $last;
}
- if ( ! $isFirst && count($family) > 1) {
+ if (!$isFirst && count($family) > 1) {
$string .= " & ";
}
+ $hasLink = false;
+ if ($asLink) {
+ $link = $p->getUserUrl();
+ if ($link !== null) {
+ $string .= "";
+ $hasLink = true;
+ }
+ }
$string .= $p->GoesBy ?? $p->first_name;
$isFirst = false;
}
$string .= " " . $standingLastName;
+ if ($hasLink) {
+ $string .= "";
+ }
+
$lastAmpPos = strrpos($string, " & ");
return str_replace(" & ", ", ", substr($string, 0, $lastAmpPos)) . substr($string, $lastAmpPos);
@@ -1516,6 +1633,19 @@ private static function ajaxIdent(): void
unset($inputData->pid);
}
+ // user validation.
+ $comment = "";
+ $valid = Utilities::validateRegistrantEmailAddress("", $inputData->email, $comment);
+ if (!$valid) {
+ http_response_code(Http::BAD_REQUEST);
+ echo json_encode([
+ 'error' => $comment,
+ 'error_i18n' => __("Registration Blocked for Spam.", 'TouchPoint-WP')
+ ]);
+ exit;
+ }
+ unset($valid, $comment);
+
$r = self::ident($inputData);
echo json_encode($r);
@@ -1523,7 +1653,9 @@ private static function ajaxIdent(): void
}
/**
- * Make the API call to get family members, store the results to the Session, and return them.
+ * Make the API call to get family members and return them.
+ *
+ * This is used for both formal and informal auth. Email addresses should be checked for spam likelihood before this point.
*
* @param $inputData
*
@@ -1531,22 +1663,9 @@ private static function ajaxIdent(): void
*/
public static function ident($inputData): array
{
- // user validation.
- $comment = "";
- $valid = Utilities::validateRegistrantEmailAddress("", $inputData->email, $comment);
- if (!$valid) {
- http_response_code(Http::BAD_REQUEST);
- echo json_encode([
- 'error' => $comment,
- 'error_i18n' => __("Registration Blocked for Spam.", 'TouchPoint-WP')
- ]);
- exit;
- }
- unset($valid, $comment);
-
try {
$inputData->context = "ident";
- $data = TouchPointWP::instance()->apiPost('ident', $inputData, 30);
+ $data = TouchPointWP::instance()->api->pyPost('ident', $inputData, 30);
} catch (Exception $ex) {
http_response_code(Http::SERVER_ERROR);
echo json_encode(['error' => $ex->getMessage()]);
@@ -1557,7 +1676,11 @@ public static function ident($inputData): array
$data->primaryFam = $data->primaryFam ?? [];
- $s = Session::instance();
+ try {
+ $stats = Stats::instance();
+ $stats->softAuths += count($people);
+ $stats->updateDb();
+ } catch (Exception) {}
$ret = [];
$primaryFam = $s->primaryFam ?? [];
@@ -1581,10 +1704,17 @@ public static function ident($inputData): array
$sPeople = array_merge($s->people ?? [], $ret);
$ids = array_map(fn($p) => $p->peopleId, $sPeople);
$uniqIds = array_unique($ids);
- $s->people = array_values(array_intersect_key($sPeople, $uniqIds));
- $s->primaryFam = $primaryFam;
- $s->secondaryFam = $secondaryFam;
+ // TODO determine if this is ever actually used or, more importantly, useful. There has to be a better way to do this.
+ $primaryPerson = $sPeople[0] ?? null;
+ if ($primaryPerson) {
+ $personTransient = (object)[
+ 'people' => array_values(array_intersect_key($sPeople, $uniqIds)),
+ 'primaryFam' => $primaryFam,
+ 'secondaryFam' => $secondaryFam
+ ];
+ set_transient("tp_person_ident_" . $primaryPerson->peopleId, $personTransient, 60 * 60 * 12);
+ }
return [
'people' => $ret,
@@ -1594,6 +1724,8 @@ public static function ident($inputData): array
/**
* Return JSON for a people search, validating that the person has access to those people.
+ *
+ * @deprecated Needs to be rewritten with proper API.
*
* @return void
*/
@@ -1605,9 +1737,9 @@ private static function ajaxSrc(): void
if ($onBehalfOf === null) {
http_response_code(Http::UNAUTHORIZED);
echo json_encode([
- "error" => "Not Authorized.",
- "error_i18n" => __("You may need to sign in.", 'TouchPoint-WP')
- ]);
+ "error" => "Not Authorized.",
+ "error_i18n" => __("You may need to sign in.", 'TouchPoint-WP')
+ ]);
exit;
}
@@ -1617,7 +1749,7 @@ private static function ajaxSrc(): void
if ($q['q'] !== '') {
try {
- $data = TouchPointWP::instance()->apiGet('src', $q, 30);
+ $data = TouchPointWP::instance()->api->pyGet('src', $q, 30);
$data = $data->people ?? [];
} catch (Exception $ex) {
http_response_code(Http::SERVER_ERROR);
@@ -1636,10 +1768,25 @@ private static function ajaxSrc(): void
];
$out['results'] = [];
+
+ $hasDupNames = false;
+ $names = [];
foreach ($data as $p) {
+ $name = "$p->goesBy $p->lastName";
+ if (in_array($name, $names)) {
+ $hasDupNames = true;
+ break;
+ } else {
+ $names[] = $name;
+ }
+ }
+
+ foreach ($data as $p) {
+ $showPid = $hasDupNames || str_contains($q['q'], $p->peopleId);
+
$out['results'][] = [
'id' => $p->peopleId,
- 'text' => $p->goesBy . " " . $p->lastName
+ 'text' => trim("$p->goesBy $p->lastName" . ($showPid ? " ($p->peopleId)" : ""))
];
}
} else {
@@ -1678,6 +1825,10 @@ public static function api(array $uri): bool
self::ajaxContact();
exit;
+ case "list":
+ self::ajaxPeopleListShortcode();
+ exit;
+
case "force-sync":
TouchPointWP::doCacheHeaders(TouchPointWP::CACHE_NONE);
try {
@@ -1699,8 +1850,27 @@ public static function api(array $uri): bool
*/
protected static function allowContact(): bool
{
- $allowed = !!apply_filters(TouchPointWP::HOOK_PREFIX . 'allow_contact', true);
- return !!apply_filters(TouchPointWP::HOOK_PREFIX . 'person_allow_contact', $allowed);
+ /**
+ * Determines whether contact of any kind is allowed. This is meant to prevent abuse in contact forms by
+ * removing the ability to contact people and thereby hiding the forms.
+ *
+ * @since 0.0.35 Added
+ *
+ * @param bool $allowed True if contact is allowed.
+ */
+ $allowed = !!apply_filters('tp_allow_contact', true);
+
+ /**
+ * Determines whether contact is allowed for any People. This is called *after* tp_allow_contact, and
+ * that will set the default.
+ *
+ * @since 0.0.35 Added
+ *
+ * @see tp_allow_contact
+ *
+ * @param bool $allowed Previous response from tp_allow_contact. True if contact is allowed.
+ */
+ return !!apply_filters('tp_person_allow_contact', $allowed);
}
/**
@@ -1733,9 +1903,9 @@ private static function ajaxContact(): void
if (!$validate) {
http_response_code(Http::BAD_REQUEST);
echo json_encode([
- 'error' => $result,
- 'error_i18n' => __("Contact Blocked for Spam.", 'TouchPoint-WP')
- ]);
+ 'error' => $result,
+ 'error_i18n' => __("Contact Blocked for Spam.", 'TouchPoint-WP')
+ ]);
exit;
}
@@ -1744,7 +1914,7 @@ private static function ajaxContact(): void
// Submit the contact
try {
- $data = TouchPointWP::instance()->apiPost('person_contact', $inputData);
+ $data = TouchPointWP::instance()->api->pyPost('person_contact', $inputData);
} catch (Exception $ex) {
http_response_code(Http::SERVER_ERROR);
echo json_encode(['error' => $ex->getMessage()]);
@@ -1755,6 +1925,21 @@ private static function ajaxContact(): void
exit;
}
+ /**
+ * Handles the AJAX call to return a list of people.
+ *
+ * @return void
+ */
+ protected static function ajaxPeopleListShortcode(): void
+ {
+ // This is an AJAX call, so we need to set the headers.
+ if ( ! headers_sent()) {
+ TouchPointWP::doCacheHeaders(TouchPointWP::CACHE_PRIVATE);
+ }
+
+ echo self::peopleListShortcode($_GET, __('None right now.', 'TouchPoint-WP'));
+ }
+
/**
* Loads the module and initializes the other actions.
*
@@ -1804,7 +1989,7 @@ public static function checkUpdates(): void
if (TouchPointWP::instance()->settings->person_cron_last_run * 1 < time() - 86400 - 3600) {
try {
self::updateFromTouchPoint();
- } catch (Exception $ex) {
+ } catch (Exception) {
}
}
}
diff --git a/src/TouchPoint-WP/PostTypeCapable.php b/src/TouchPoint-WP/PostTypeCapable.php
new file mode 100644
index 00000000..b66296c9
--- /dev/null
+++ b/src/TouchPoint-WP/PostTypeCapable.php
@@ -0,0 +1,138 @@
+post_id;
+ }
+
+ public function getPost(bool $create = false): ?WP_Post
+ {
+ if ($this->post === null) {
+ $this->post = get_post($this->post_id);
+ }
+ return $this->post;
+ }
+
+ /**
+ * Create relevant objects from a given post
+ *
+ * @param WP_Post $post
+ *
+ * @return ?PostTypeCapable
+ * @throws TouchPointWP_Exception
+ */
+ public static function fromPost(WP_Post $post): ?self
+ {
+ if (Involvement::postIsType($post)) {
+ return Involvement::fromPost($post);
+ }
+ if (Meeting::postIsType($post)) {
+ return Meeting::fromPost($post);
+ }
+ if (Partner::postIsType($post)) {
+ return Partner::fromPost($post);
+ }
+ return null;
+ }
+
+ /**
+ * Get the link for the post.
+ *
+ * @return string
+ */
+ public function permalink(): string
+ {
+ return get_permalink($this->post);
+ }
+
+ /**
+ * Get notable attributes.
+ *
+ * @param array|StringableArray $exclude Attributes listed here will be excluded. (e.g. if shown for a parent, not needed here.)
+ *
+ * @return NotableAttributes
+ */
+ public abstract function notableAttributes(array|StringableArray $exclude = []): NotableAttributes;
+
+ /**
+ * Handle exclusions for the notableAttributes $exclusion variable.
+ *
+ * Removes all array items that have a value or key contained in the $exclude array's values.
+ *
+ * @param StringableArray $subject
+ * @param array $exclude
+ *
+ * @return NotableAttributes
+ */
+ protected function processAttributeExclusions(StringableArray $subject, array $exclude): NotableAttributes
+ {
+ $subject = array_diff($subject->getArrayCopy(), $exclude);
+ foreach ($exclude as $e) {
+ if (isset($subject[$e])) {
+ unset($subject[$e]);
+ }
+ }
+ return new NotableAttributes($subject);
+ }
+
+ /**
+ * Indicates if the given post can be instantiated as the given post type.
+ *
+ * @param WP_Post $post
+ *
+ * @return bool
+ */
+ public static abstract function postIsType(WP_Post $post): bool;
+
+
+ /**
+ * Indicates if the given post type name is the post type for this class.
+ *
+ * @param string $postType
+ *
+ * @return bool
+ */
+ public static abstract function postTypeMatches(string $postType): bool;
+
+
+ /**
+ * Gets a TouchPoint item ID number, regardless of what type of object this is.
+ *
+ * @return int
+ */
+ public abstract function getTouchPointId(): int;
+
+}
\ No newline at end of file
diff --git a/src/TouchPoint-WP/RegistrationType.php b/src/TouchPoint-WP/RegistrationType.php
new file mode 100644
index 00000000..447ebbf6
--- /dev/null
+++ b/src/TouchPoint-WP/RegistrationType.php
@@ -0,0 +1,26 @@
+name = $params['name'];
$this->type = $params['type'];
- $this->interval = max(floor(floatval($params['interval']) * 4) / 4, 0.25);
+ $interval = $params['interval'] ?? 24;
+ $this->interval = max(floor(floatval($interval) * 4) / 4, 0.25);
$this->p1 = $params['p1'] ?? "";
}
@@ -74,7 +75,7 @@ protected function __construct($params)
*
* @return void
*/
- protected function mergeParams($params)
+ protected function mergeParams($params): void
{
$this->interval = min($this->interval, $params['interval'] ?? $this->interval);
}
@@ -96,10 +97,12 @@ public static function fromParams($params): self
}
$params['type'] = strtolower($params['type']);
- if ($params['type'] !== 'sql') {
+ if ($params['type'] !== 'sql' && $params['type'] !== 'python') {
throw new TouchPointWP_Exception("Invalid Report type.", 173002);
}
+ $params['interval'] = floatval($params['interval'] ?? 24);
+
$key = self::cacheKey($params);
if (isset(self::$_instances[$key])) {
self::$_instances[$key]->mergeParams($params);
@@ -138,7 +141,7 @@ public static function load(): bool
/// Cron ///
////////////
- // Setup cron for updating People daily.
+ // Setup cron for updating Reports daily.
add_action(self::CRON_HOOK, [self::class, 'updateCron']);
if ( ! wp_next_scheduled(self::CRON_HOOK)) {
// Runs every 15 minutes, starting now.
@@ -179,7 +182,17 @@ public static function init(): void
'has_archive' => false,
'rewrite' => false,
'can_export' => false,
- 'delete_with_user' => false
+ 'delete_with_user' => false,
+ 'capability_type' => 'post',
+ 'capabilities' => [
+ 'create_posts' => 'do_not_allow', // Disable creating new posts
+ 'edit_posts' => 'do_not_allow', // Disable editing posts
+ 'edit_others_posts' => 'do_not_allow', // Disable editing others' posts
+ 'delete_posts' => 'do_not_allow', // Disable deleting posts
+ 'delete_others_posts' => 'do_not_allow', // Disable deleting others' posts
+ 'publish_posts' => 'do_not_allow', // Disable publishing posts
+ ],
+ 'map_meta_cap' => true, // Ensure users can still view posts
]
);
}
@@ -216,6 +229,107 @@ public static function api(array $uri): bool
}
exit;
}
+ } else if (count($uri['path']) === 4 || count($uri['path']) === 5) {
+ $parts = explode(".", $uri['path'][3], 2);
+ if (count($parts) === 2) {
+ [$filename, $ext] = $parts;
+ } else {
+ $filename = $parts[0];
+ $ext = null;
+ if (isset($uri['path'][4])) {
+ $ext = str_replace("_", '.', $uri['path'][4]) ?? null;
+ }
+ }
+
+ switch ($uri['path'][2]) {
+ case "py":
+ TouchPointWP::doCacheHeaders(TouchPointWP::CACHE_NONE);
+
+ try {
+ $r = Report::fromParams([
+ 'type' => 'python',
+ 'name' => $filename,
+ 'p1' => $_GET['p1'] ?? ''
+ ]);
+ $content = $r->content();
+ } catch (TouchPointWP_Exception) {
+ http_response_code(Http::SERVER_ERROR);
+ exit;
+ }
+
+ if ($content === self::DEFAULT_CONTENT) {
+ http_response_code(Http::NOT_FOUND);
+ exit;
+ }
+
+ switch ($ext) {
+ case "svg":
+ header("Content-Type: image/svg+xml");
+ break;
+
+ case "svg.png":
+ $bgColor = $_GET['bg'] ?? null;
+
+ if ($bgColor !== null) {
+ $bgColor = strtolower($bgColor);
+ if (preg_match('/^[0-9a-f]{6}$/', $bgColor)) {
+ $bgColor = "#" . $bgColor;
+ }
+ }
+
+ $bgColorStr = ($bgColor === null) ? "" : "_$bgColor";
+
+ $cached = get_post_meta($r->getPost()->ID, self::META_PREFIX . "svg_png" . $bgColorStr, true);
+ if ($cached !== '') {
+ $content = base64_decode($cached);
+ } else {
+ try {
+ $content = ImageConversions::svgToPng($content, $bgColor);
+ update_post_meta($r->getPost()->ID, self::META_PREFIX . "svg_png" . $bgColorStr, base64_encode($content));
+ } catch (TouchPointWP_Exception $e) {
+ http_response_code(Http::SERVICE_UNAVAILABLE);
+ echo $e->getMessage();
+ exit;
+ } catch (Exception $e) {
+ http_response_code(Http::SERVER_ERROR);
+ echo $e->getMessage();
+ exit;
+ }
+ }
+ header("Content-Type: image/png");
+ break;
+
+ default:
+ header("Content-Type: text/plain");
+ break;
+ }
+
+
+ echo $content;
+ exit;
+
+ case "sql":
+ TouchPointWP::doCacheHeaders(TouchPointWP::CACHE_NONE);
+ header("Cache-Control: max-age=3600, must-revalidate, public");
+ try {
+ $r = Report::fromParams([
+ 'type' => 'sql',
+ 'name' => $filename,
+ 'p1' => $_GET['p1'] ?? ''
+ ]);
+ } catch (TouchPointWP_Exception) {
+ http_response_code(Http::SERVER_ERROR);
+ exit;
+ }
+ $content = $r->content();
+ if ($content === self::DEFAULT_CONTENT) {
+ http_response_code(Http::NOT_FOUND);
+ exit;
+ }
+
+ echo $content;
+ exit;
+ }
}
return false;
@@ -255,8 +369,9 @@ protected function title(): string
*
* @return string
*/
- public static function reportShortcode($params = [], string $content = ""): string
+ public static function reportShortcode(mixed $params = [], string $content = ""): string
{
+ /** @noinspection PhpRedundantOptionalArgumentInspection */
$params = array_change_key_case($params, CASE_LOWER);
$params = shortcode_atts(
@@ -265,13 +380,17 @@ public static function reportShortcode($params = [], string $content = ""): stri
'name' => '',
'interval' => 24,
'p1' => '',
- 'showupdated' => 'true'
+ 'showupdated' => 'true',
+ 'inline' => 'false'
],
$params,
self::SHORTCODE_REPORT
);
$params['showupdated'] = (strtolower($params['showupdated']) === 'true' || $params['showupdated'] === 1);
+ $params['inline'] = (strtolower($params['inline']) === 'true' || $params['inline'] === 1);
+
+ $params['showupdated'] = $params['showupdated'] && !$params['inline'];
try {
$report = self::fromParams($params);
@@ -281,7 +400,7 @@ public static function reportShortcode($params = [], string $content = ""): stri
if (self::$_indexingMode) {
// It has been added to the index already, so our work here is done.
- return "";
+ return $content;
}
$rc = $report->content();
@@ -292,7 +411,24 @@ public static function reportShortcode($params = [], string $content = ""): stri
// Add Figure elt with a unique ID
$idAttr = "id=\"" . wp_unique_id('tp-report-') . "\"";
- $rc = "\n\t" . str_replace("\n", "\n\t", $rc);
+
+ $class = self::$classDefault;
+
+ /**
+ * Filter the class name to be used for the displaying the report.
+ *
+ * @param string $class The class name to be used.
+ * @param Report $report The report being displayed.
+ */
+ $class = apply_filters("tp_rpt_figure_class", $class, $report);
+
+ $permalink = esc_attr(get_post_permalink($report->getPost()));
+
+ $elt = $params['inline'] ? "span" : "figure";
+ $nt = $params['inline'] ? "" : "\n\t";
+ $n = $params['inline'] ? "" : "\n";
+
+ $rc = "<$elt $idAttr class=\"$class\" data-tp-report=\"$permalink\">$nt" . str_replace("\n", $nt, $rc);
// If desired, add a caption that indicates when the table was last updated.
if ($params['showupdated']) {
@@ -303,10 +439,10 @@ public static function reportShortcode($params = [], string $content = ""): stri
get_the_modified_time('', $report->getPost())
);
- $rc .= "\n\t$updatedS";
+ $rc .= "$nt$updatedS";
}
- $rc .= "\n";
+ $rc .= "$n$elt>";
return $rc;
}
@@ -323,25 +459,24 @@ public function getPost(bool $create = false): ?WP_Post
{
if ( ! $this->_postLoaded || ($this->post === null && $create)) {
$q = new WP_Query([
- 'post_type' => self::POST_TYPE,
- 'meta_query' => [
- 'relation' => 'AND',
- [
- 'key' => self::TYPE_META_KEY,
- 'value' => $this->type
- ],
- [
- 'key' => self::NAME_META_KEY,
- 'value' => $this->name
- ],
- [
- 'key' => self::P1_META_KEY,
- 'value' => $this->p1
- ]
- ],
- 'numberposts' => 2
-// only need one, but if there's two, there should be an error condition.
- ]);
+ 'post_type' => self::POST_TYPE,
+ 'meta_query' => [
+ 'relation' => 'AND',
+ [
+ 'key' => self::TYPE_META_KEY,
+ 'value' => $this->type
+ ],
+ [
+ 'key' => self::NAME_META_KEY,
+ 'value' => $this->name
+ ],
+ [
+ 'key' => self::P1_META_KEY,
+ 'value' => $this->p1
+ ]
+ ],
+ 'numberposts' => 2 // only need one, but if there's two, there should be an error condition.
+ ]);
$reportPosts = $q->get_posts();
$counts = count($reportPosts);
@@ -349,18 +484,18 @@ public function getPost(bool $create = false): ?WP_Post
new TouchPointWP_Exception("Multiple Posts Exist", 170006);
}
if ($counts > 0) { // post exists already.
- $this->post = $reportPosts[0];
+ $this->post = reset($reportPosts);
} elseif ($create) {
$postId = wp_insert_post([
- 'post_type' => self::POST_TYPE,
- 'post_status' => 'publish',
- 'post_name' => $this->title(),
- 'meta_input' => [
- self::NAME_META_KEY => $this->name,
- self::TYPE_META_KEY => $this->type,
- self::P1_META_KEY => $this->p1
- ]
- ]);
+ 'post_type' => self::POST_TYPE,
+ 'post_status' => 'publish',
+ 'post_name' => $this->title() . " " . $this->type,
+ 'meta_input' => [
+ self::NAME_META_KEY => $this->name,
+ self::TYPE_META_KEY => $this->type,
+ self::P1_META_KEY => $this->p1
+ ]
+ ]);
if (is_wp_error($postId)) {
$this->post = null;
new TouchPointWP_WPError($postId);
@@ -388,12 +523,15 @@ public function getPost(bool $create = false): ?WP_Post
*
* @return int|WP_Error|null
*/
- protected function submitUpdate()
+ protected function submitUpdate(): int|null|WP_Error
{
if ( ! $this->getPost()) {
return null;
}
+ // Clear the cached PNGs if they exist.
+ Database::deletePostMetaByPrefix($this->post->ID, self::META_PREFIX . "svg_png");
+
return wp_update_post($this->post);
}
@@ -426,8 +564,10 @@ public function content(string $contentIfError = self::DEFAULT_CONTENT): string
*/
public static function updateFromTouchPoint(bool $forceEvenIfNotDue = false): int
{
+ TouchPointWP::instance()->setTpWpUserAsCurrent();
+
// Find Report Shortcodes in post content and add their involvements to the query.
- $referencingPosts = Utilities::getPostContentWithShortcode(self::SHORTCODE_REPORT);
+ $referencingPosts = Database::getPostContentWithShortcode(self::SHORTCODE_REPORT);
$postIdsToNotDelete = [];
@@ -436,14 +576,19 @@ public static function updateFromTouchPoint(bool $forceEvenIfNotDue = false): in
//////////////////
self::$_indexingMode = true;
+
+ global $post;
+ $originalPost = $post;
+
foreach ($referencingPosts as $postI) {
- global $post;
$post = $postI;
set_time_limit(10);
apply_shortcodes($postI->post_content);
}
self::$_indexingMode = false;
+ $post = $originalPost;
+
$needsUpdate = [];
foreach (self::$_instances as $report) {
if ($report->getPost()) {
@@ -461,7 +606,7 @@ public static function updateFromTouchPoint(bool $forceEvenIfNotDue = false): in
$updates = [];
if (count($needsUpdate) > 0) {
- $data = TouchPointWP::instance()->apiPost('report_run', ['reports' => $needsUpdate], 60);
+ $data = TouchPointWP::instance()->api->pyPost('report_run', ['reports' => $needsUpdate], 60);
$updates = $data->report_results ?? [];
}
@@ -474,12 +619,18 @@ public static function updateFromTouchPoint(bool $forceEvenIfNotDue = false): in
foreach ($updates as $u) {
try {
$report = self::fromParams($u);
- } catch (TouchPointWP_Exception $e) {
+ } catch (TouchPointWP_Exception) {
continue;
}
+ $content = $u->result;
+
+ if ($u->type === 'sql') {
+ $content = self::cleanupSqlContent($content);
+ }
+
$post = $report->getPost(true);
- $post->post_content = self::cleanupContent($u->result);
+ $post->post_content = $content;
$submit = $report->submitUpdate();
if ( ! in_array($post->ID, $postIdsToNotDelete)) {
@@ -511,6 +662,8 @@ public static function updateFromTouchPoint(bool $forceEvenIfNotDue = false): in
TouchPointWP::instance()->flushRewriteRules();
}
+ TouchPointWP::instance()->unsetTpWpUserAsCurrent();
+
return $updateCount;
}
@@ -522,7 +675,7 @@ public static function updateFromTouchPoint(bool $forceEvenIfNotDue = false): in
*
* @return string
*/
- private static function cleanupContent(string $content): string
+ private static function cleanupSqlContent(string $content): string
{
$closes = substr($content, strrpos($content, '') + 5);
$content = substr($content, 0, strrpos($content, '
interval * 60) % 60;
- $h = $this->interval - ($m / 60);
+ $i = $this->interval - ($diff / 60); // subtract to avoid updates shifting later in the day.
+ $m = ($i * 60) % 60;
+ $h = $i - ($m / 60);
return new DateInterval("PT{$h}H{$m}M");
}
@@ -592,18 +748,18 @@ public static function updateCron(): void
if ( ! $forked) {
self::updateFromTouchPoint();
}
- } catch (Exception $ex) {
+ } catch (Exception) {
}
}
/**
* Handle which data should be converted to JSON. Used for posting to the API.
*
- * @return object data which can be serialized by json_encode
+ * @return array data which can be serialized by json_encode
*/
- public function jsonSerialize(): object
+ public function jsonSerialize(): array
{
- return (object)[
+ return [
'name' => $this->name,
'type' => $this->type,
'p1' => $this->p1
@@ -615,7 +771,7 @@ public function jsonSerialize(): object
*
* @return void
*/
- public static function checkUpdates()
+ public static function checkUpdates(): void
{
// This method does nothing because the overhead is relatively great, and should not be hooked to every page load.
}
diff --git a/src/TouchPoint-WP/Rsvp.php b/src/TouchPoint-WP/Rsvp.php
index 0ff7a04e..a78f680a 100644
--- a/src/TouchPoint-WP/Rsvp.php
+++ b/src/TouchPoint-WP/Rsvp.php
@@ -5,18 +5,18 @@
namespace tp\TouchPointWP;
-// TODO sort out what goes here, and what goes in Meetings.
+// TODO sort out what goes here, and what goes in Meetings. Answer: all of this should go to Meetings.
+
+use tp\TouchPointWP\Interfaces\module;
if ( ! defined('ABSPATH')) {
exit(1);
}
-if ( ! TOUCHPOINT_COMPOSER_ENABLED) {
- require_once 'Meeting.php';
-}
-
/**
* This class provides the RSVP functionality for Meetings.
+ *
+ * @deprecated 0.0.90 TODO is this true?
*/
abstract class Rsvp implements module
{
@@ -117,6 +117,8 @@ public static function shortcode(array $params, string $content): string
TouchPointWP::enqueueActionsStyle('rsvp');
Person::enqueueUsersForJsInstantiation();
+ // TODO merge with Meeting::rsvpButton()
+
return "$content$preloadMsg";
}
}
diff --git a/src/TouchPoint-WP/TouchPointWP_Settings.php b/src/TouchPoint-WP/Settings.php
similarity index 61%
rename from src/TouchPoint-WP/TouchPointWP_Settings.php
rename to src/TouchPoint-WP/Settings.php
index 03b22d80..1aa9ec83 100644
--- a/src/TouchPoint-WP/TouchPointWP_Settings.php
+++ b/src/TouchPoint-WP/Settings.php
@@ -17,6 +17,7 @@
*
* @property-read string enable_authentication Whether the Authentication module is included.
* @property-read string enable_involvements Whether the Involvement module is included.
+ * @property-read string enable_meeting_cal Whether the Meeting Calendar module is included.
* @property-read string enable_people_lists Whether to allow public People Lists.
* @property-read string enable_rsvp Whether the RSVP module is included.
* @property-read string enable_global Whether to import Global partners.
@@ -27,9 +28,15 @@
* @property-read string system_name What the church calls TouchPoint
* @property-read string api_user Username of a user account with API access
* @property-read string api_pass Password for a user account with API access
+ * @property-read string api_pat The Personal Access Token used for newer API calls
+ * @property-read string api_pat_expires The date/time when the PAT is expected to expire.
* @property-read string api_script_name The name of the script loaded into TouchPoint for API Interfacing
* @property-read string google_maps_api_key Google Maps API Key for embedded maps
* @property-read string google_geo_api_key Google Maps API Key for geocoding
+ * @property-read string ipapi_key The API key for ipapi.co for geolocation.
+ * @property-read ?int ipapi_ratelimit_exp The time at which the rate limit for ipapi.co will expire.
+ *
+ * @property-read string enable_public_listing Whether to allow the site to be listed as using TouchPoint-WP
*
* @property-read array people_contact_keywords Keywords to use for the generic Contact person button.
* @property-read string people_ev_bio Extra Value field that should be imported as a User bio.
@@ -65,11 +72,21 @@
*
* @property-read string locations_json JSON string describing fixed locations.
*
+ * @property-read string ec_app_cal_provider The provider of the calendar data for the mobile app. Either "meetings" or "tribe".
* @property-read string ec_use_standardizing_style Whether to insert the standardizing stylesheet into mobile app requests.
*
+ * @property-read string mc_name_plural What Meetings should be called, plural (e.g. "Events" or "Meetings")
+ * @property-read string mc_name_singular What a Meeting code should be called, singular (e.g. "Event" or "Meeting")
+ * @property-read string mc_slug Slug for meetings in the meeting calendar (e.g. "events" for church.org/events)
+ * @property-read int mc_future_days Number of days into the future to import.
+ * @property-read int mc_archive_days Number of days to wait to move something to history.
+ * @property-read int|string mc_hist_days Number of days of history to keep. (Can be '' if module isn't enabled.)
+ * @property-read string mc_grouping_method Whether and how to collect meetings into groups.
+ *
* @property-read string rc_name_plural What resident codes should be called, plural (e.g. "Resident Codes" or "Zones")
* @property-read string rc_name_singular What a resident code should be called, singular (e.g. "Resident Code" or "Zone")
* @property-read string rc_slug Slug for resident code taxonomy (e.g. "zones" for church.org/zones)
+ * @property-read array rc_additional_post_types Which post types should have the division taxonomy.
*
* @property-read string camp_name_plural What campuses should be called, plural (e.g. "Campuses" or "Languages")
* @property-read string camp_name_singular What a campus should be called, singular (e.g. "Campus" or "Language")
@@ -79,14 +96,15 @@
* @property-read string dv_name_singular What a division should be called, singular (e.g. "Division" or "Ministry")
* @property-read string dv_slug Slug for division taxonomy (e.g. "ministries" for church.org/ministries)
* @property-read array dv_divisions Which divisions should be imported
+ * @property-read array dv_additional_post_types Which post types should have the division taxonomy.
*/
-class TouchPointWP_Settings
+class Settings
{
/**
* The singleton of TouchPointWP_Settings.
*/
- private static ?TouchPointWP_Settings $_instance = null;
+ private static ?Settings $_instance = null;
/**
* The main plugin object.
@@ -98,6 +116,13 @@ class TouchPointWP_Settings
*/
protected array $settings = [];
+ /**
+ * Indicates whether strings are included in the settings parameters.
+ *
+ * @var bool
+ */
+ protected bool $hasStrings = false;
+
public const UNDEFINED_PLACEHOLDER = INF;
/**
@@ -109,23 +134,16 @@ public function __construct(TouchPointWP $parent)
{
$this->parent = $parent;
- // Initialise settings.
- add_action('init', [$this, 'initSettings'], 11);
+ $this->initSettings();
// Register plugin settings.
add_action('admin_init', [$this, 'registerSettings']);
// Add settings page to menu.
- add_action('admin_menu', [$this, 'add_menu_item']);
+ add_action('admin_menu', [$this, 'addMenuItems']);
// Add settings link to plugins page.
- add_filter(
- 'plugin_action_links_' . plugin_basename($this->parent->file),
- [
- $this,
- 'add_settings_link',
- ]
- );
+ add_filter('plugin_action_links_' . plugin_basename($this->parent->file), [$this, 'addSettingsLink']);
// Configure placement of plugin settings page. See readme for implementation.
add_filter(TouchPointWP::SETTINGS_PREFIX . 'menu_settings', [$this, 'configureSettings']);
@@ -138,12 +156,12 @@ public function __construct(TouchPointWP $parent)
*
* @param ?TouchPointWP $parent Object instance.
*
- * @return TouchPointWP_Settings instance
- * @since 1.0.0
+ * @return Settings instance
+ * @since 0.0.90 Added
* @static
* @see TouchPointWP()
*/
- public static function instance(?TouchPointWP $parent = null): TouchPointWP_Settings
+ public static function instance(?TouchPointWP $parent = null): Settings
{
if (is_null($parent)) {
$parent = TouchPointWP::instance();
@@ -157,13 +175,16 @@ public static function instance(?TouchPointWP $parent = null): TouchPointWP_Sett
}
/**
- * Initialise settings
+ * Initialize settings
+ *
+ * @param bool $withStrings Indicates that labels need to be included
*
* @return void
*/
- public function initSettings(): void
+ public function initSettings(bool $withStrings = false): void
{
- $this->settings = $this->settingsFields();
+
+ $this->settings = $this->settingsFields($withStrings);
}
/**
@@ -175,98 +196,186 @@ public function hasValidApiSettings(): bool
{
$host = $this->getWithoutDefault('host');
- return ! ($this->getWithoutDefault('api_script_name') === TouchPointWP_Settings::UNDEFINED_PLACEHOLDER ||
- $host === TouchPointWP_Settings::UNDEFINED_PLACEHOLDER || $host === '' ||
- $this->getWithoutDefault('api_user') === TouchPointWP_Settings::UNDEFINED_PLACEHOLDER ||
- $this->getWithoutDefault('api_pass') === TouchPointWP_Settings::UNDEFINED_PLACEHOLDER);
+ return ! ($this->getWithoutDefault('api_script_name') === Settings::UNDEFINED_PLACEHOLDER ||
+ $host === Settings::UNDEFINED_PLACEHOLDER || $host === '' ||
+ $this->getWithoutDefault('api_user') === Settings::UNDEFINED_PLACEHOLDER ||
+ $this->getWithoutDefault('api_pass') === Settings::UNDEFINED_PLACEHOLDER);
+ }
+
+ /**
+ * Used internally to determine if a particular setting should be auto-loaded.
+ *
+ * @param string $settingName
+ *
+ * @return bool
+ */
+ private function settingShouldBeAutoLoaded(string $settingName): bool
+ {
+ if (str_contains($settingName, '_cron_last_run')
+ || $settingName === "DEBUG"
+ || $settingName === "meta_familyEvFields" // because it's used when registering the taxonomies on every init.
+ ) {
+ return true;
+ }
+ foreach ($this->settings as $page) {
+ foreach ($page['fields'] as $f) {
+ if ($f['id'] === $settingName) {
+ if (isset($f['autoload'])) {
+ return !!$f['autoload'];
+ }
+ return false;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * @param $fieldId
+ *
+ * @return ?array
+ */
+ private function getFieldMeta($fieldId): ?array
+ {
+ foreach ($this->settings as $category) {
+ foreach ($category['fields'] as $field) {
+ if ($field['id'] === $fieldId) {
+ return $field;
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * A wrapper for the usual WordPress translation function, in order to make sure translations aren't called too early.
+ *
+ * @param string $text
+ * @param string $domain
+ *
+ * @see __()
+ *
+ * @return string
+ */
+ protected function __(string $text, string $domain): string
+ {
+ if (! $this->hasStrings) {
+ // If the init function hasn't been called yet, we can't use the translation function.
+ return "";
+ }
+
+ return __($text, $domain);
}
/**
* Build settings fields
*
* @param bool|string $includeDetail Set to true to get options from TouchPoint, likely including the API calls. Set
- * to the key of a specific page to only load options for that page.
+ * to the key of a specific page to only load options for that page.
*
- * @return array Fields to be displayed on settings page
+ * @return array[] Fields to be displayed on settings page
*/
- private function settingsFields($includeDetail = false): array
+ private function settingsFields(bool $withStrings, bool|string $includeDetail = false): array
{
// Don't call API if we don't have API credentials
if ( ! $this->hasValidApiSettings()) {
$includeDetail = false;
}
- if (count($this->settings) > 0 && $includeDetail === false) {
- // Settings are already loaded, and they have adequate detail for the task at hand.
+ if (count($this->settings) > 0 && ($this->hasStrings >= $withStrings) && $includeDetail === false) {
+ // Settings are already loaded.
return $this->settings;
}
- $this->settings['basic'] = [
- 'title' => __('Basic Settings', 'TouchPoint-WP'),
- 'description' => __('Connect to TouchPoint and choose which features you wish to use.', 'TouchPoint-WP'),
+ $settings = [];
+
+ if ($withStrings) {
+ $this->hasStrings = true;
+ }
+
+ $settings['basic'] = [
+ 'title' => $this->__('Basic Settings', 'TouchPoint-WP'),
+ 'description' => $this->__('Connect to TouchPoint and choose which features you wish to use.', 'TouchPoint-WP'),
'fields' => [
[
'id' => 'enable_authentication',
- 'label' => __('Enable Authentication', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Enable Authentication', 'TouchPoint-WP'),
+ 'description' => $this->__(
'Allow TouchPoint users to sign into this website with TouchPoint.',
'TouchPoint-WP'
),
'type' => 'checkbox',
'default' => '',
+ 'autoload' => true,
'callback' => fn($new) => $this->validation_updateScriptsIfChanged($new, 'enable_authentication'),
],
[
'id' => 'enable_rsvp',
- 'label' => __('Enable RSVP Tool', 'TouchPoint-WP'),
- 'description' => __('Add a crazy-simple RSVP button to WordPress event pages.', 'TouchPoint-WP'),
+ 'label' => $this->__('Enable RSVP Tool', 'TouchPoint-WP'),
+ 'description' => $this->__('Add a crazy-simple RSVP button to WordPress event pages.', 'TouchPoint-WP'),
'type' => 'checkbox',
'default' => '',
+ 'autoload' => true,
],
[
'id' => 'enable_involvements',
- 'label' => __('Enable Involvements', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Enable Involvements', 'TouchPoint-WP'),
+ 'description' => $this->__(
'Load Involvements from TouchPoint for involvement listings and entries native in your website.',
'TouchPoint-WP'
),
'type' => 'checkbox',
'default' => '',
+ 'autoload' => true,
+ ],
+ [
+ 'id' => 'enable_meeting_cal',
+ 'label' => $this->__('Enable Meeting Calendar', 'TouchPoint-WP'),
+ 'description' => $this->__(
+ 'Load Meetings from TouchPoint for a calendar, native in your website.',
+ 'TouchPoint-WP'
+ ),
+ 'type' => 'checkbox',
+ 'default' => '',
+ 'autoload' => true,
],
[
'id' => 'enable_people_lists',
- 'label' => __('Enable Public People Lists', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Enable Public People Lists', 'TouchPoint-WP'),
+ 'description' => $this->__(
'Import public people listings from TouchPoint (e.g. staff or elders)',
'TouchPoint-WP'
),
'type' => 'checkbox',
'default' => '',
+ 'autoload' => true,
],
[
'id' => 'enable_global',
- 'label' => __('Enable Global Partner Listings', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Enable Global Partner Listings', 'TouchPoint-WP'),
+ 'description' => $this->__(
'Import ministry partners from TouchPoint to list publicly.',
'TouchPoint-WP'
),
'type' => 'checkbox',
'default' => '',
+ 'autoload' => true,
],
[
'id' => 'enable_campuses',
- 'label' => __('Enable Campuses', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Enable Campuses', 'TouchPoint-WP'),
+ 'description' => $this->__(
"Import campuses as a taxonomy. (You probably want to do this if you're multi-campus.)",
'TouchPoint-WP'
),
'type' => 'checkbox',
'default' => '',
+ 'autoload' => true,
],
[
'id' => 'system_name',
- 'label' => __('Display Name', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Display Name', 'TouchPoint-WP'),
+ 'description' => $this->__(
'What your church calls your TouchPoint database.',
'TouchPoint-WP'
),
@@ -276,20 +385,24 @@ private function settingsFields($includeDetail = false): array
],
[
'id' => 'host',
- 'label' => __('TouchPoint Host Name', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('TouchPoint Host Name', 'TouchPoint-WP'),
+ 'description' => $this->__(
'The domain for your TouchPoint database, without the https or any slashes.',
'TouchPoint-WP'
),
'type' => 'text',
'default' => '',
+ 'autoload' => true,
'placeholder' => 'mychurch.tpsdb.com',
- 'callback' => [$this, 'validation_lowercase']
+ 'callback' => function ($new) {
+ $new = $this->validation_lowercase($new);
+ return $this->validation_invalidatePATIfChanged($new, "host");
+ }
],
[
'id' => 'host_deeplink',
- 'label' => __('Custom Mobile App Deeplink Host Name', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Custom Mobile App Deeplink Host Name', 'TouchPoint-WP'),
+ 'description' => $this->__(
"The domain for your mobile app deeplinks, without the https or any slashes. If you aren't using the custom mobile app, leave this blank.",
'TouchPoint-WP'
),
@@ -300,53 +413,61 @@ private function settingsFields($includeDetail = false): array
],
[
'id' => 'api_user',
- 'label' => __('TouchPoint API Username', 'TouchPoint-WP'),
- 'description' => __(
- 'The username of a user account in TouchPoint with API permissions.',
+ 'label' => $this->__('TouchPoint API Username', 'TouchPoint-WP'),
+ 'description' => $this->__(
+ 'The username of a user account in TouchPoint with API permissions. It is strongly recommended that you create a separate person/user for this purpose, rather than using a staff member\'s account.',
'TouchPoint-WP'
),
'type' => 'text',
'default' => '',
+ 'autoload' => true,
'placeholder' => '',
+ 'callback' => fn($new) => $this->validation_invalidatePATIfChanged($new, 'api_user')
],
[
'id' => 'api_pass',
- 'label' => __('TouchPoint API User Password', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('TouchPoint API User Password', 'TouchPoint-WP'),
+ 'description' => $this->__(
'The password of a user account in TouchPoint with API permissions.',
'TouchPoint-WP'
),
'type' => 'text_secret',
'default' => '',
+ 'autoload' => true,
'placeholder' => $this->passwordPlaceholder('api_pass'),
- 'callback' => fn($new) => $this->validation_secret($new, 'api_pass')
+ 'callback' => function($new) {
+ $new = $this->validation_secret($new, 'api_pass');
+ return $this->validation_invalidatePATIfChanged($new, 'api_pass');
+ }
],
[
'id' => 'api_script_name',
- 'label' => __('TouchPoint API Script Name', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('TouchPoint API Script Name', 'TouchPoint-WP'),
+ 'description' => $this->__(
'The name of the Python script loaded into TouchPoint. Don\'t change this unless you know what you\'re doing.',
'TouchPoint-WP'
),
'type' => 'text',
'default' => 'WebApi',
+ 'autoload' => true,
'placeholder' => '',
],
[
'id' => 'google_maps_api_key',
- 'label' => __('Google Maps Javascript API Key', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Google Maps Javascript API Key', 'TouchPoint-WP'),
+ 'description' => $this->__(
'Required for embedding maps.',
'TouchPoint-WP'
),
'type' => 'text',
'default' => '',
+ 'autoload' => true,
'placeholder' => '',
],
[
'id' => 'google_geo_api_key',
- 'label' => __('Google Maps Geocoding API Key', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Google Maps Geocoding API Key', 'TouchPoint-WP'),
+ 'description' => $this->__(
'Optional. Allows for reverse geocoding of user locations.',
'TouchPoint-WP'
),
@@ -354,21 +475,43 @@ private function settingsFields($includeDetail = false): array
'default' => '',
'placeholder' => '',
],
+ [
+ 'id' => 'ipapi_key',
+ 'label' => $this->__('ipapi.co API Key', 'TouchPoint-WP'),
+ 'description' => $this->__(
+ 'Optional. Allows for geolocation of user IP addresses. This generally will work without a key, but may be rate limited.',
+ 'TouchPoint-WP'
+ ),
+ 'type' => 'text',
+ 'default' => '',
+ 'placeholder' => '',
+ ],
+ [
+ 'id' => 'enable_public_listing',
+ 'label' => $this->__('List Site in Directory', 'TouchPoint-WP'),
+ 'description' => $this->__(
+ "Allow the TouchPoint-WP developers to publicly list your site/church as using TouchPoint-WP. Helps other prospective churches see what can be done by combining WordPress with the best ChMS on the planet. Only applies if this site is accessible on the public internet.",
+ 'TouchPoint-WP'
+ ),
+ 'type' => 'checkbox',
+ 'default' => 'on',
+ ],
],
];
// Add Script generation section if necessary settings are established.
- if ($this->getWithoutDefault('system_name') !== self::UNDEFINED_PLACEHOLDER
- && $this->hasValidApiSettings()) {
+ if ($includeDetail
+ && $this->getWithoutDefault('system_name') !== self::UNDEFINED_PLACEHOLDER
+ && $this->hasValidApiSettings()) {
/** @noinspection HtmlUnknownTarget */
- $this->settings['basic']['fields'][] = [
+ $settings['basic']['fields'][] = [
'id' => 'generate-scripts',
- 'label' => __('Generate Scripts', 'TouchPoint-WP'),
+ 'label' => $this->__('Generate Scripts', 'TouchPoint-WP'),
'type' => 'instructions',
'description' => strtr(
- '
',
[
'{apiUrl}' => "/" . TouchPointWP::API_ENDPOINT . "/" . TouchPointWP::API_ENDPOINT_ADMIN_SCRIPTZIP,
@@ -383,14 +526,14 @@ private function settingsFields($includeDetail = false): array
$includeThis = $includeDetail === true || $includeDetail === 'people';
$urlParts = wp_parse_url(home_url());
$defaultUserPev = $urlParts['host'] . " User ID";
- $this->settings['people'] = [
- 'title' => __('People', 'TouchPoint-WP'),
- 'description' => __('Manage how people are synchronized between TouchPoint and WordPress.', 'TouchPoint-WP'),
+ $settings['people'] = [
+ 'title' => $this->__('People', 'TouchPoint-WP'),
+ 'description' => $this->__('Manage how people are synchronized between TouchPoint and WordPress.', 'TouchPoint-WP'),
'fields' => [
[
'id' => 'people_contact_keywords',
- 'label' => __('Contact Keywords', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Contact Keywords', 'TouchPoint-WP'),
+ 'description' => $this->__(
'These keywords will be used when someone clicks the "Contact" button on a Person\'s listing or profile.',
'TouchPoint-WP'
),
@@ -401,8 +544,8 @@ private function settingsFields($includeDetail = false): array
],
[
'id' => 'people_ev_wpId',
- 'label' => __('Extra Value for WordPress User ID', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Extra Value for WordPress User ID', 'TouchPoint-WP'),
+ 'description' => $this->__(
'The name of the extra value to use for the WordPress User ID. If you are using multiple WordPress instances with one TouchPoint database, you will need these values to be unique between WordPress instances. In most cases, the default is fine.',
'TouchPoint-WP'
),
@@ -412,8 +555,8 @@ private function settingsFields($includeDetail = false): array
],
[
'id' => 'people_ev_bio',
- 'label' => __('Extra Value: Biography', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Extra Value: Biography', 'TouchPoint-WP'),
+ 'description' => $this->__(
'Import a Bio from a Person Extra Value field. Can be an HTML or Text Extra Value. This will overwrite any values set by WordPress. Leave blank to not import.',
'TouchPoint-WP'
),
@@ -423,8 +566,8 @@ private function settingsFields($includeDetail = false): array
],
[
'id' => 'people_ev_custom',
- 'label' => __('Extra Values to Import', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Extra Values to Import', 'TouchPoint-WP'),
+ 'description' => $this->__(
'Import People Extra Value fields as User Meta data.',
'TouchPoint-WP'
),
@@ -438,15 +581,15 @@ private function settingsFields($includeDetail = false): array
}
if (get_option(TouchPointWP::SETTINGS_PREFIX . 'enable_authentication') === "on") { // TODO MULTI
-// $includeThis = $includeDetail === true || $includeDetail === 'authentication';
- $this->settings['authentication'] = [
- 'title' => __('Authentication', 'TouchPoint-WP'),
- 'description' => __('Allow users to log into WordPress using TouchPoint.', 'TouchPoint-WP'),
+// $includeThis = $includeDetail === true || $includeDetail === 'authentication';
+ $settings['authentication'] = [
+ 'title' => $this->__('Authentication', 'TouchPoint-WP'),
+ 'description' => $this->__('Allow users to log into WordPress using TouchPoint.', 'TouchPoint-WP'),
'fields' => [
[
'id' => 'auth_default',
- 'label' => __('Make TouchPoint the default authentication method.', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Make TouchPoint the default authentication method.', 'TouchPoint-WP'),
+ 'description' => $this->__(
'By checking this box, the TouchPoint login page will become the default. To prevent the redirect and reach the standard WordPress login page, add \'tp_no_redirect\' as a URL parameter.',
'TouchPoint-WP'
),
@@ -455,8 +598,8 @@ private function settingsFields($includeDetail = false): array
],
[
'id' => 'auth_auto_provision',
- 'label' => __('Enable Auto-Provisioning', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Enable Auto-Provisioning', 'TouchPoint-WP'),
+ 'description' => $this->__(
'Automatically create WordPress users, if needed, to match authenticated TouchPoint users.',
'TouchPoint-WP'
),
@@ -465,8 +608,8 @@ private function settingsFields($includeDetail = false): array
],
[
'id' => 'auth_change_profile_urls',
- 'label' => __('Change \'Edit Profile\' links', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Change Profile Links', 'TouchPoint-WP'),
+ 'description' => $this->__(
'"Edit Profile" links will take the user to their TouchPoint profile, instead of their WordPress profile.',
'TouchPoint-WP'
),
@@ -475,15 +618,15 @@ private function settingsFields($includeDetail = false): array
],
[
'id' => 'auth_full_logout',
- 'label' => __('Enable full logout', 'TouchPoint-WP'),
- 'description' => __('Logout of TouchPoint when logging out of WordPress.', 'TouchPoint-WP'),
+ 'label' => $this->__('Enable full logout', 'TouchPoint-WP'),
+ 'description' => $this->__('Logout of TouchPoint when logging out of WordPress.', 'TouchPoint-WP'),
'type' => 'checkbox',
'default' => 'on',
],
[
'id' => 'auth_prevent_admin_bar',
- 'label' => __('Prevent Subscriber Admin Bar', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Prevent Subscriber Admin Bar', 'TouchPoint-WP'),
+ 'description' => $this->__(
'By enabling this option, users who can\'t edit anything won\'t see the Admin bar.',
'TouchPoint-WP'
),
@@ -496,17 +639,18 @@ private function settingsFields($includeDetail = false): array
if (get_option(TouchPointWP::SETTINGS_PREFIX . 'enable_involvements') === "on") { // TODO MULTI
$includeThis = $includeDetail === true || $includeDetail === 'involvements';
- $this->settings['involvements'] = [
- 'title' => __('Involvements', 'TouchPoint-WP'),
- 'description' => __('Import Involvements from TouchPoint to list them on your website, for Small Groups, Classes, and more. Select the division(s) that immediately correspond to the type of Involvement you want to list. For example, if you want a Small Group list and have a Small Group Division, only select the Small Group Division. If you want Involvements to be filterable by additional Divisions, select those Divisions on the Divisions tab, not here.', 'TouchPoint-WP'),
+ $settings['involvements'] = [
+ 'title' => $this->__('Involvements', 'TouchPoint-WP'),
+ 'description' => $this->__('Import Involvements from TouchPoint to list them on your website, for Small Groups, Classes, and more. Select the division(s) that immediately correspond to the type of Involvement you want to list. For example, if you want a Small Group list and have a Small Group Division, only select the Small Group Division. If you want Involvements to be filterable by additional Divisions, select those Divisions on the Divisions tab, not here.', 'TouchPoint-WP'),
'fields' => [
[
'id' => 'inv_json', // involvement settings json (stored as a json string)
'type' => 'textarea',
- 'label' => __('Involvement Post Types', 'TouchPoint-WP'),
+ 'label' => $this->__('Involvement Post Types', 'TouchPoint-WP'),
'default' => '[]',
+ 'autoload' => true,
'hidden' => true,
- 'description' => ! $includeThis ? "" : function () {
+ 'description' => !$includeThis ? "" : function () {
TouchPointWP::requireScript("base");
TouchPointWP::requireScript("knockout-defer");
TouchPointWP::requireScript("select2-defer");
@@ -531,14 +675,14 @@ private function settingsFields($includeDetail = false): array
if (get_option(TouchPointWP::SETTINGS_PREFIX . 'enable_global') === "on") { // TODO MULTI
$includeThis = $includeDetail === true || $includeDetail === 'global';
- $this->settings['global'] = [
- 'title' => __('Global Partners', 'TouchPoint-WP'),
- 'description' => __('Manage how global partners are imported from TouchPoint for listing on WordPress. Partners are grouped by family, and content is provided through Family Extra Values. This works for both People and Business records.', 'TouchPoint-WP'),
+ $settings['global'] = [
+ 'title' => $this->__('Global Partners', 'TouchPoint-WP'),
+ 'description' => $this->__('Manage how global partners are imported from TouchPoint for listing on WordPress. Partners are grouped by family, and content is provided through Family Extra Values. This works for both People and Business records.', 'TouchPoint-WP'),
'fields' => [
[
'id' => 'global_name_plural',
- 'label' => __('Global Partner Name (Plural)', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Global Partner Name (Plural)', 'TouchPoint-WP'),
+ 'description' => $this->__(
'What you call Global Partners at your church',
'TouchPoint-WP'
),
@@ -548,8 +692,8 @@ private function settingsFields($includeDetail = false): array
],
[
'id' => 'global_name_singular',
- 'label' => __('Global Partner Name (Singular)', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Global Partner Name (Singular)', 'TouchPoint-WP'),
+ 'description' => $this->__(
'What you call a Global Partner at your church',
'TouchPoint-WP'
),
@@ -559,8 +703,8 @@ private function settingsFields($includeDetail = false): array
],
[
'id' => 'global_name_plural_decoupled',
- 'label' => __('Global Partner Name for Secure Places (Plural)', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Global Partner Name for Secure Places (Plural)', 'TouchPoint-WP'),
+ 'description' => $this->__(
'What you call Secure Global Partners at your church',
'TouchPoint-WP'
),
@@ -570,8 +714,8 @@ private function settingsFields($includeDetail = false): array
],
[
'id' => 'global_name_singular_decoupled',
- 'label' => __('Global Partner Name for Secure Places (Singular)', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Global Partner Name for Secure Places (Singular)', 'TouchPoint-WP'),
+ 'description' => $this->__(
'What you call a Secure Global Partner at your church',
'TouchPoint-WP'
),
@@ -581,20 +725,21 @@ private function settingsFields($includeDetail = false): array
],
[
'id' => 'global_slug',
- 'label' => __('Global Partner Slug', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Global Partner Slug', 'TouchPoint-WP'),
+ 'description' => $this->__(
'The root path for Global Partner posts',
'TouchPoint-WP'
),
'type' => 'text',
'default' => 'partners',
+ 'autoload' => true,
'placeholder' => 'partners',
'callback' => fn($new) => $this->validation_slug($new, 'global_slug')
],
[
'id' => 'global_search',
- 'label' => __('Saved Search', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Saved Search', 'TouchPoint-WP'),
+ 'description' => $this->__(
'Anyone who is included in this saved search will be included in the listing.',
'TouchPoint-WP'
),
@@ -604,8 +749,8 @@ private function settingsFields($includeDetail = false): array
],
[
'id' => 'global_description',
- 'label' => __('Extra Value: Description', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Extra Value: Description', 'TouchPoint-WP'),
+ 'description' => $this->__(
'Import a description from a Family Extra Value field. Can be an HTML or Text Extra Value. This becomes the body of the Global Partner post.',
'TouchPoint-WP'
),
@@ -615,8 +760,8 @@ private function settingsFields($includeDetail = false): array
],
[
'id' => 'global_summary',
- 'label' => __('Extra Value: Summary', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Extra Value: Summary', 'TouchPoint-WP'),
+ 'description' => $this->__(
'Optional. Import a short description from a Family Extra Value field. Can be an HTML or Text Extra Value. If not provided, the full bio will be truncated.',
'TouchPoint-WP'
),
@@ -626,8 +771,8 @@ private function settingsFields($includeDetail = false): array
],
[
'id' => 'global_geo_lat',
- 'label' => __('Latitude Override', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Latitude Override', 'TouchPoint-WP'),
+ 'description' => $this->__(
'Designate a text Family Extra Value that will contain a latitude that overrides any locations on the partner\'s profile for the partner map. Both latitude and longitude must be provided for an override to take place.',
'TouchPoint-WP'
),
@@ -637,8 +782,8 @@ private function settingsFields($includeDetail = false): array
],
[
'id' => 'global_geo_lng',
- 'label' => __('Longitude Override', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Longitude Override', 'TouchPoint-WP'),
+ 'description' => $this->__(
'Designate a text Family Extra Value that will contain a longitude that overrides any locations on the partner\'s profile for the partner map. Both latitude and longitude must be provided for an override to take place.',
'TouchPoint-WP'
),
@@ -648,8 +793,8 @@ private function settingsFields($includeDetail = false): array
],
[
'id' => 'global_location',
- 'label' => __('Public Location', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Public Location', 'TouchPoint-WP'),
+ 'description' => $this->__(
'Designate a text Family Extra Value that will contain the partner\'s location, as you want listed publicly. For partners who have DecoupleLocation enabled, this field will be associated with the map point, not the list entry.',
'TouchPoint-WP'
),
@@ -659,8 +804,8 @@ private function settingsFields($includeDetail = false): array
],
[
'id' => 'global_fev_custom',
- 'label' => __('Extra Values to Import', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Extra Values to Import', 'TouchPoint-WP'),
+ 'description' => $this->__(
'Import Family Extra Value fields as Meta data on the partner\'s post',
'TouchPoint-WP'
),
@@ -671,49 +816,72 @@ private function settingsFields($includeDetail = false): array
],
[
'id' => 'global_primary_tax',
- 'label' => __('Primary Taxonomy', 'TouchPoint-WP'),
- 'description' => __(
+ 'label' => $this->__('Primary Taxonomy', 'TouchPoint-WP'),
+ 'description' => $this->__(
'Import a Family Extra Value as the primary means by which partners are organized.',
'TouchPoint-WP'
),
'type' => 'select',
'options' => $includeThis ? $this->parent->getFamilyEvFieldsAsKVArray('code', true) : [],
'default' => "",
+ 'autoload' => true,
],
],
];
}
- if (TouchPointWP::useTribeCalendar()) {
+ if (TouchPointWP::useTribeOrMeetingCalendars()) {
+ $options = [];
+ $default = '';
+ if (TouchPointWP::useTribeCalendar()) {
+ $options['tribe'] = $this->__('Events Calendar plugin by Modern Tribe', 'TouchPoint-WP');
+ $default = 'tribe';
+ }
+ if (get_option(TouchPointWP::SETTINGS_PREFIX . 'enable_meeting_cal') === "on") {
+ $options['meetings'] = $this->__('TouchPoint Meetings', 'TouchPoint-WP');
+ $default = 'meetings';
+ }
+
/** @noinspection HtmlUnknownTarget */
- $this->settings['events_calendar'] = [
- 'title' => __('Events Calendar', 'TouchPoint-WP'),
- 'description' => __('Integrate with The Events Calendar from ModernTribe.', 'TouchPoint-WP'),
+ $settings['events_calendar'] = [
+ 'title' => $this->__('App 2.0 Calendar', 'TouchPoint-WP'),
+ 'description' => $this->__('Integrate Custom Mobile app version 2.0 with The Events Calendar from Modern Tribe.', 'TouchPoint-WP'),
'fields' => [
+ [
+ 'id' => 'ec_app_cal_provider',
+ 'label' => $this->__('Events Provider', 'TouchPoint-WP'),
+ 'description' => $this->__(
+ 'The source of events for version 2.0 of the Custom Mobile App.',
+ 'TouchPoint-WP'
+ ),
+ 'type' => 'select',
+ 'options' => $options,
+ 'default' => $default,
+ ],
[
'id' => 'ec_app_cal_url',
- 'label' => __('Events for Custom Mobile App', 'TouchPoint-WP'),
+ 'label' => $this->__('Events for Custom Mobile App', 'TouchPoint-WP'),
'type' => 'instructions',
'description' => strtr(
- '
' . __('To use your Events Calendar events in the Custom mobile app, set the Provider to Wordpress Plugin - Modern Tribe and use this url:', 'TouchPoint-WP') . '
' .
+ '
' . $this->__("To use your Events Calendar events in the Custom mobile app, set the Provider to Wordpress Plugin - Modern Tribe (regardless of which provider you're using above) and use this url:", 'TouchPoint-WP') . '
";
+ }
+ foreach ($children as $child) {
+
+ if (!Involvement::postIsType($child)) {
+ // This is just a precaution; it shouldn't happen because the query above only includes involvements.
+ continue;
+ }
+
+ /** @var WP_Post $child */
+ echo "
";
+ }
+ foreach ($children as $child) {
+
+ if (!Meeting::postIsType($child)) {
+ // This is just a precaution; it shouldn't happen because the query above only includes involvements.
+ continue;
+ }
+
+ /** @var WP_Post $child */
+ echo "