diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml new file mode 100644 index 00000000..5af129c5 --- /dev/null +++ b/.github/workflows/push.yml @@ -0,0 +1,28 @@ +name: Build + +on: + push: + branches: + - "*" + +jobs: + build: + name: Create Build + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Permissions + run: chmod +x build.sh + shell: bash + + - name: Build + run: ./build.sh + shell: bash + + - name: Upload Artifacts + uses: actions/upload-artifact@v4 + with: + name: touchpoint-wp + path: build diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 5b1c26a6..67751958 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -1,23 +1,26 @@ +name: Create Release + on: push: tags: - 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10 -name: Create Release - jobs: - build: + release: name: Create Release runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v2 + - name: Permissions run: chmod +x build.sh shell: bash + - name: Build run: ./build.sh shell: bash + - name: Create Release id: create_release uses: actions/create-release@v1 @@ -28,6 +31,7 @@ jobs: release_name: Release ${{ github.ref }} draft: true prerelease: false + - name: Publish Built zip uses: actions/upload-release-asset@v1 env: @@ -36,4 +40,15 @@ jobs: upload_url: ${{ steps.create_release.outputs.upload_url }} asset_path: ./touchpoint-wp.zip asset_name: touchpoint-wp.zip - asset_content_type: application/zip \ No newline at end of file + asset_content_type: application/zip + + docs: + name: Update Documentation + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Generate Docs + run: php ./generateDocs.php + shell: bash \ No newline at end of file diff --git a/.gitignore b/.gitignore index b290ba17..12986275 100644 --- a/.gitignore +++ b/.gitignore @@ -12,14 +12,16 @@ composer.lock /.idea/deployment.xml /.idea/dataSources.xml /.idea/webServers.xml +/.idea/runConfigurations/Local.xml *.min.js node_modules /i18n/*.json /i18n/*.mo +/i18n/*.l10n.php package-lock.json /build/ -/touchpoint-wp.zip \ No newline at end of file +/touchpoint-wp.zip diff --git a/.idea/.gitignore b/.idea/.gitignore index 13566b81..a9d7db9c 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -6,3 +6,5 @@ # Datasource local storage ignored files /dataSources/ /dataSources.local.xml +# GitHub Copilot persisted chat sessions +/copilot/chatSessions diff --git a/.idea/misc.xml b/.idea/misc.xml index 691dd2ff..4c167176 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,9 @@ + + @noinspection - + \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml index 05d47f11..a20c6b90 100644 --- a/.idea/php.xml +++ b/.idea/php.xml @@ -13,9 +13,10 @@ + - + diff --git a/.idea/runConfigurations/Build__WSL_.xml b/.idea/runConfigurations/Build__WSL_.xml new file mode 100644 index 00000000..5805c68d --- /dev/null +++ b/.idea/runConfigurations/Build__WSL_.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/i18n__1_for_Translation.xml b/.idea/runConfigurations/i18n__1_for_Translation.xml new file mode 100644 index 00000000..8f161b58 --- /dev/null +++ b/.idea/runConfigurations/i18n__1_for_Translation.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/.idea/runConfigurations/i18n__2_for_Publish.xml b/.idea/runConfigurations/i18n__2_for_Publish.xml new file mode 100644 index 00000000..30b2968d --- /dev/null +++ b/.idea/runConfigurations/i18n__2_for_Publish.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml index 56782cab..7accee87 100644 --- a/.idea/sqldialects.xml +++ b/.idea/sqldialects.xml @@ -1,6 +1,7 @@ + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml index c297a493..45ef162f 100644 --- a/.idea/vcs.xml +++ b/.idea/vcs.xml @@ -1,5 +1,15 @@ + + + diff --git a/.idea/watcherTasks.xml b/.idea/watcherTasks.xml index f9519dfe..dc821a35 100644 --- a/.idea/watcherTasks.xml +++ b/.idea/watcherTasks.xml @@ -22,22 +22,22 @@ - - @@ -46,9 +46,9 @@ \ No newline at end of file diff --git a/README.md b/README.md index 5249ea96..b2427661 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 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..36b3d814 100644 --- a/assets/js/base-defer.js +++ b/assets/js/base-defer.js @@ -69,7 +69,11 @@ function utilInit() { } tpvm._utils.clearHash = function() { - window.location.hash = ""; + if (!!window.history) { + window.history.pushState("", "", `${window.location.pathname}${window.location.search}`) + } else { + window.location.hash = ""; + } } /** @@ -354,7 +358,11 @@ class TP_MapMarker } get inBounds() { - return this.gMkr.getMap().getBounds().contains(this.gMkr.getPosition()); + let map = this.gMkr.getMap(); + if (!map) { // if map failed to render for some reason, this prevents entries from being hidden. + return true; + } + return map.getBounds().contains(this.gMkr.getPosition()); } get useIcon() { @@ -513,7 +521,13 @@ class TP_Mappable { 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 actionBtns = this.connectedElements[ei].querySelectorAll('[data-tp-action]') + 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); + } + for (const ai in actionBtns) { if (!actionBtns.hasOwnProperty(ai)) continue; const action = actionBtns[ai].getAttribute('data-tp-action'); diff --git a/assets/js/meeting-defer.js b/assets/js/meeting-defer.js index 69b5ba6a..4e7246f3 100644 --- a/assets/js/meeting-defer.js +++ b/assets/js/meeting-defer.js @@ -54,7 +54,6 @@ class TP_Meeting { e.stopPropagation(); mtg[action + "Action"](); }); - tpvm._utils.handleHash(action); } } diff --git a/assets/template/actions-style.css b/assets/template/actions-style.css index 460e2d28..c01e516c 100644 --- a/assets/template/actions-style.css +++ b/assets/template/actions-style.css @@ -27,4 +27,9 @@ div.swal2-content table.tp-radio-list { button[data-tp-action]:not([disabled]) { cursor: pointer; +} + +.tp-TouchPoint-logo svg { + height: 1.2em; + margin-bottom: -0.2em; } \ No newline at end of file diff --git a/assets/template/calendar-grid-style.css b/assets/template/calendar-grid-style.css new file mode 100644 index 00000000..d84a14bc --- /dev/null +++ b/assets/template/calendar-grid-style.css @@ -0,0 +1,95 @@ + +div.calGrid { + display: grid; + grid-template-columns: repeat(7, 1fr); +} + +div.calGrid, +div.calGrid > div.calWeekdayHead, +div.calGrid > div.calDay { + border: 1px solid black; +} + +div.calGrid > div.calDay { + min-height: 5em; +} + +div.calGrid div.calDay > h3.calDayHead { + display: none; +} + +div.calGrid div.calDay > * { + display: block; +} + +div.calGrid div.calDay > a.event.feat { + font-weight: bold; + font-size: 1.2em; + padding-bottom: .5em; +} + +div.calGrid div.calDay > a.event.cancelled { + color: #f446; + text-decoration: line-through; +} + +div.calGrid div.calDay > a, +div.calGrid div.calWeekdayHead, +div.calGrid div.calDay > h3.calDayHead, +div.calGrid div.calDay > span.calDayNum { + padding: .2em 5px; +} + +div.calGrid div.calDay.before, +div.calGrid div.calDay.after { + opacity: .8; + background: #0001; +} + +div.calGridNav.bottom { + display: none; +} + +@media screen and (max-width: 1000px) { + div.calGrid { + display: grid; + grid-template-columns: unset; + } + + div.calGrid, + div.calGrid > div.calDay { + border: unset; + min-height: unset; + margin-bottom: 1em; + } + + div.calGrid div.calDay > a.event.notFirstDay, + div.calGrid div.calDay.noFirstDays { + display: none; + } + + div.calGrid div.calWeekdayHead, + div.calGrid div.calDay.before, + div.calGrid div.calDay.empty, + div.calGrid div.calDay.after { + display: none; + } + + div.calGrid div.calDay > span.calDayNum { + display: none; + } + + div.calGrid div.calDay > h3.calDayHead { + display:block; + } + + div.calGridNav.bottom { + display: flex; + } +} + +div.calGridNav { + display: flex; + justify-content: space-between; + align-items: center; +} \ No newline at end of file diff --git a/assets/template/partials-template-style.css b/assets/template/partials-template-style.css index 220c3d9e..b24cd20d 100644 --- a/assets/template/partials-template-style.css +++ b/assets/template/partials-template-style.css @@ -84,13 +84,20 @@ article.inv-list-item h2 a { text-align: right; } +.partner-actions button, +.partner-actions a.button, +.involvement-actions button, +.involvement-actions a.button { + display: inline-block; +} + .partner-list-item .partner-actions button, .partner-list-item .partner-actions a.button, .inv-list-item .involvement-actions button, .inv-list-item .involvement-actions a.button, .inv-list-item .child-involvements a { - position:relative; - z-index:10; + position: relative; + z-index: 10; } .partner-list-item div.post-meta-single, @@ -187,7 +194,49 @@ article .TouchPointWP-detail-cell.TouchPointWP-map-container { padding-bottom: 30%; } +main.TouchPointWP-main { + max-width: 1200px; + margin: auto; + padding: 0 1em; +} + article .TouchPointWP-detail-cell .TouchPointWP-detail-cell-section { text-align:center; margin-bottom: 2em; +} + +header div.header-image-container { + max-width: 1200px; + width: 100%; + margin-left: auto; + margin-right: auto; +} + +header div.header-image-container div.header-image { + width: 100%; + padding-bottom: 50%; + background-size: cover; + background-position: center; +} + +header div.header-image-container div.header-image.partner-header-image { + background-size: contain; + background-repeat: no-repeat; +} + +header img.tpwp-accessibility-header-image { + /* visible to screen readers only */ + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +header .tpwp-alert-block { + margin: 2em 20%; + background: #f003; + padding: 1em; } \ No newline at end of file diff --git a/composer.json b/composer.json index ceca4e2a..cc55b172 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,7 @@ "description": "A WordPress Plugin for integrating with TouchPoint Church Management Software.", "license": "AGPL-3.0-or-later", "type": "wordpress-plugin", - "version": "0.0.37", + "version": "0.0.95", "keywords": [ "wordpress", "wp", @@ -27,12 +27,15 @@ } }, "require": { - "php": ">=7.4.0", + "php": ">=8.0", "composer/installers": "~1.0", "ext-json": "*", - "ext-zip": "*" + "ext-zip": "*", + "ext-dom": "*" }, "require-dev": { + "pronamic/wp-documentor": "^1.3", + "skayo/phpdoc-md": "^0.2.0" }, "config": { "allow-plugins": { diff --git a/docs b/docs index 1543ab4b..01f959fa 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 1543ab4b99a528bf289e0759978f0db70054b70b +Subproject commit 01f959faa7b2ef2c2307d6aef36454a2be2466b1 diff --git a/generateDocs.php b/generateDocs.php new file mode 100644 index 00000000..2fe553a9 --- /dev/null +++ b/generateDocs.php @@ -0,0 +1,74 @@ + 86400)) { + echo "Downloading updated PHPDoc PHAR..."; + file_put_contents(PHPDOC_PHAR_FILENAME, fopen(PHPDOC_PHAR_URL, 'r')); + echo " Complete.\n\n"; +} + + +echo "Removing previous WP docs..."; +array_map('unlink', glob('docs/wp-*.md')); +echo " Complete.\n\n"; + + +echo "Indexing WordPress Hooks..."; +exec("php ./vendor/bin/wp-documentor parse --output=docs/wp-Api.md --prefix=tp_ --format=markdown ./src/TouchPoint-WP/"); +echo " Complete\n\n"; + + +echo "Correcting links in WordPress Hooks..."; +$doc = file_get_contents("docs/wp-Api.md"); +$pattern = '/\[(\.[\S-]+)]\(([\S]+)\), \[line (\d+)\]\(([\S-]+)/'; +$replace = "[src/TouchPoint-WP/$2](https://github.com/TenthPres/TouchPoint-WP/blob/master/src/TouchPoint-WP/$2), [line $3](https://github.com/TenthPres/TouchPoint-WP/blob/master/src/TouchPoint-WP/$4\n\n"; +$doc = preg_replace($pattern, $replace, $doc); +file_put_contents("docs/wp-Api.md", $doc); +echo " Complete\n\n"; + +echo "Removing previous documentation files..."; +array_map('unlink', glob('docs/tp-*.md')); +echo " Complete.\n\n"; + + +echo "Running PHPDoc Analysis..."; +exec("php " . PHPDOC_PHAR_FILENAME . " -d src -t docs --template=\"xml\""); +echo " Complete\n\n"; + +echo "Creating Markdown files..."; +$argv[1] = "docs/structure.xml"; +$argv[2] = "docs/"; +$argv[3] = "--lt"; +$argv[4] = "%c"; +$argv[5] = "--index"; +$argv[6] = "_Sidebar.md"; +include "vendor/skayo/phpdoc-md/bin/phpdocmd"; +echo " Complete.\n\n"; + + +echo "Removing xml files..."; +array_map('unlink', glob('docs/*.xml')); +echo " Complete.\n\n"; + + +echo "Merging sidebar files..."; +$sidebar = file_get_contents("docs/.Sidebar.md"); +$automaticSidebar = file_get_contents("docs/_Sidebar.md"); +$automaticSidebar = str_replace("API Index", "PHP API Index",$automaticSidebar); + +$sidebar .= $automaticSidebar; + +file_put_contents("docs/_Sidebar.md", $sidebar); +echo " Complete.\n\n"; + + +echo "Generating Footer..."; +$footer = "Documentation generated " . date("F j, Y g:ia."); +file_put_contents("docs/_Footer.md", $footer); +echo " Complete.\n\n"; + +echo "Committing and pushing to Repository..."; +echo exec("cd " . __DIR__ . "/docs && git add *.md && git commit -m \"Auto-Updated Documentation\" && git push"); +echo " Complete.\n\n"; \ No newline at end of file diff --git a/generateDocs.ps1 b/generateDocs.ps1 deleted file mode 100644 index fec7fa92..00000000 --- a/generateDocs.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -$pharFile = "phpDocumentor.phar" -$compareDt = (Get-Date).AddDays(-1) - -if (!(test-path $pharFile -newerThan $compareDt)) -{ - Write-Output "Downloading phpDocumentor as it was not found or is more than a day old..." - Invoke-WebRequest https://www.phpdoc.org/phpDocumentor.phar -OutFile $pharFile -} - -php phpDocumentor.phar \ No newline at end of file diff --git a/generateI18n_1forTranslation.ps1 b/generateI18n_1forTranslation.ps1 index ea187ba5..36a96c2f 100644 --- a/generateI18n_1forTranslation.ps1 +++ b/generateI18n_1forTranslation.ps1 @@ -7,5 +7,5 @@ if (!(test-path $pharFile -newerThan $compareDt)) Invoke-WebRequest https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar -OutFile $pharFile } -php .\wp-cli.phar i18n make-pot . --exclude="build" i18n/TouchPoint-WP.pot +php .\wp-cli.phar i18n make-pot . i18n/TouchPoint-WP.pot --exclude="build" php .\wp-cli.phar i18n update-po i18n/TouchPoint-WP.pot \ No newline at end of file diff --git a/generateI18n_2forPublish.ps1 b/generateI18n_2forPublish.ps1 index 8939a2e2..a466eb47 100644 --- a/generateI18n_2forPublish.ps1 +++ b/generateI18n_2forPublish.ps1 @@ -11,4 +11,5 @@ Remove-Item "i18n/*.json" Remove-Item "i18n/*.mo" php .\wp-cli.phar i18n make-json i18n --no-purge -php .\wp-cli.phar i18n make-mo i18n \ No newline at end of file +php .\wp-cli.phar i18n make-mo i18n +php .\wp-cli.phar i18n make-php i18n \ No newline at end of file diff --git a/i18n/TouchPoint-WP-es_ES.po b/i18n/TouchPoint-WP-es_ES.po index 05653e47..9a201c6e 100644 --- a/i18n/TouchPoint-WP-es_ES.po +++ b/i18n/TouchPoint-WP-es_ES.po @@ -12,162 +12,169 @@ msgstr "" "Language: \n" #. Plugin Name of the plugin +#: touchpoint-wp.php msgid "TouchPoint WP" msgstr "TouchPoint WP" #. Plugin URI of the plugin +#: touchpoint-wp.php msgid "https://github.com/tenthpres/touchpoint-wp" msgstr "https://github.com/tenthpres/touchpoint-wp" #. Description of the plugin +#: touchpoint-wp.php msgid "A WordPress Plugin for integrating with TouchPoint Church Management Software." msgstr "Un complemento de WordPress para integrarse con TouchPoint, el software de administración de iglesias." #. Author of the plugin +#: touchpoint-wp.php msgid "James K" msgstr "James K" #. Author URI of the plugin +#: touchpoint-wp.php msgid "https://github.com/jkrrv" msgstr "https://github.com/jkrrv" -#: src/templates/admin/invKoForm.php:17 +#: src/templates/admin/invKoForm.php:18 #: src/templates/admin/locationsKoForm.php:13 -#: src/templates/admin/locationsKoForm.php:50 +#: src/templates/admin/locationsKoForm.php:58 msgid "Delete" msgstr "Borrar" -#: src/templates/admin/invKoForm.php:23 +#: src/templates/admin/invKoForm.php:24 msgid "Singular Name" msgstr "Nombre singular" -#: src/templates/admin/invKoForm.php:31 +#: src/templates/admin/invKoForm.php:32 msgid "Plural Name" msgstr "Nombre Plural" -#: src/templates/admin/invKoForm.php:39 +#: src/templates/admin/invKoForm.php:40 msgid "Slug" msgstr "Slug" -#: src/templates/admin/invKoForm.php:47 -#: src/TouchPoint-WP/TouchPointWP_Settings.php:765 +#: src/templates/admin/invKoForm.php:48 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:977 msgid "Divisions to Import" msgstr "Divisiones a Importar" -#: src/templates/admin/invKoForm.php:60 +#: src/templates/admin/invKoForm.php:84 msgid "Import Hierarchically (Parent-Child Relationships)" msgstr "Importar jerárquicamente (relaciones padre-hijo)" -#: src/templates/admin/invKoForm.php:77 +#: src/templates/admin/invKoForm.php:110 msgid "Use Geographic Location" msgstr "Usar ubicación geográfica" -#: src/templates/admin/invKoForm.php:83 +#: src/templates/admin/invKoForm.php:116 msgid "Exclude Involvements if" msgstr "Excluir participaciones si" -#: src/templates/admin/invKoForm.php:87 +#: src/templates/admin/invKoForm.php:120 msgid "Involvement is Closed" msgstr "La participación está cerrada" -#: src/templates/admin/invKoForm.php:91 +#: src/templates/admin/invKoForm.php:124 msgid "Involvement is a Child Involvement" msgstr "La participación es una participación infantil" -#: src/templates/admin/invKoForm.php:113 +#: src/templates/admin/invKoForm.php:146 msgid "Leader Member Types" msgstr "Tipos de miembros de líder" -#: src/templates/admin/invKoForm.php:116 -#: src/templates/admin/invKoForm.php:132 -#: src/templates/admin/invKoForm.php:258 +#: src/templates/admin/invKoForm.php:63 +#: src/templates/admin/invKoForm.php:149 +#: src/templates/admin/invKoForm.php:165 +#: src/templates/admin/invKoForm.php:318 #: src/templates/parts/involvement-nearby-list.php:2 -#: src/TouchPoint-WP/Rsvp.php:77 -#: assets/js/base-defer.js:188 -#: assets/js/base-defer.js:1119 +#: src/TouchPoint-WP/Meeting.php:746 +#: src/TouchPoint-WP/Rsvp.php:75 +#: assets/js/base-defer.js:192 +#: assets/js/base-defer.js:1133 msgid "Loading..." msgstr "Cargando..." -#: src/templates/admin/invKoForm.php:128 +#: src/templates/admin/invKoForm.php:161 msgid "Host Member Types" msgstr "Tipos de miembros anfitriones" -#: src/templates/admin/invKoForm.php:144 +#: src/templates/admin/invKoForm.php:177 msgid "Default Grouping" msgstr "Agrupación Predeterminada" -#: src/templates/admin/invKoForm.php:148 +#: src/templates/admin/invKoForm.php:181 msgid "No Grouping" msgstr "Sin agrupar" -#: src/templates/admin/invKoForm.php:149 +#: src/templates/admin/invKoForm.php:182 msgid "Upcoming / Current" msgstr "Próximo / Actual" -#: src/templates/admin/invKoForm.php:150 +#: src/templates/admin/invKoForm.php:183 msgid "Current / Upcoming" msgstr "Actual / Próximo" -#: src/templates/admin/invKoForm.php:156 +#: src/templates/admin/invKoForm.php:191 msgid "Default Filters" msgstr "Filtros predeterminados" -#: src/templates/admin/invKoForm.php:165 +#: src/templates/admin/invKoForm.php:223 msgid "Gender" msgstr "Género" -#: src/templates/admin/invKoForm.php:179 -#: src/TouchPoint-WP/Involvement.php:1445 -#: src/TouchPoint-WP/TouchPointWP.php:1445 +#: src/templates/admin/invKoForm.php:237 +#: src/TouchPoint-WP/Involvement.php:1853 +#: src/TouchPoint-WP/Taxonomies.php:750 msgid "Weekday" msgstr "Día laborable" -#: src/templates/admin/invKoForm.php:183 -#: src/TouchPoint-WP/Involvement.php:1471 -#: src/TouchPoint-WP/TouchPointWP.php:1527 +#: src/templates/admin/invKoForm.php:241 +#: src/TouchPoint-WP/Involvement.php:1879 +#: src/TouchPoint-WP/Taxonomies.php:808 msgid "Time of Day" msgstr "Hora del día" -#: src/templates/admin/invKoForm.php:187 +#: src/templates/admin/invKoForm.php:245 msgid "Prevailing Marital Status" msgstr "Estado civil prevaleciente" -#: src/templates/admin/invKoForm.php:191 -#: src/TouchPoint-WP/TouchPointWP.php:1581 +#: src/templates/admin/invKoForm.php:249 +#: src/TouchPoint-WP/Taxonomies.php:837 msgid "Age Group" msgstr "Grupo de edad" -#: src/templates/admin/invKoForm.php:196 +#: src/templates/admin/invKoForm.php:254 msgid "Task Owner" msgstr "Propietario de la tarea" -#: src/templates/admin/invKoForm.php:203 +#: src/templates/admin/invKoForm.php:261 msgid "Contact Leader Task Keywords" msgstr "Palabras clave de la tarea del líder de contacto" -#: src/templates/admin/invKoForm.php:214 +#: src/templates/admin/invKoForm.php:272 msgid "Join Task Keywords" msgstr "Unirse a las palabras clave de la tarea" -#: src/templates/admin/invKoForm.php:230 +#: src/templates/admin/invKoForm.php:288 msgid "Add Involvement Post Type" msgstr "Agregar tipo de publicación de participación" -#: src/templates/admin/invKoForm.php:237 +#: src/templates/admin/invKoForm.php:295 msgid "Small Group" msgstr "Grupo Pequeño" -#: src/templates/admin/invKoForm.php:238 +#: src/templates/admin/invKoForm.php:296 msgid "Small Groups" msgstr "Grupos Pequeños" -#: src/templates/admin/invKoForm.php:368 +#: src/templates/admin/invKoForm.php:442 msgid "Select..." msgstr "Seleccione..." #. translators: %s will be the plural post type (e.g. Small Groups) #: src/templates/parts/involvement-list-none.php:16 -#: src/TouchPoint-WP/Involvement.php:1682 +#: src/TouchPoint-WP/Involvement.php:2099 msgid "No %s Found." msgstr "No se encontraron %s" @@ -177,759 +184,751 @@ msgid "%s will be imported overnight for the first time." msgstr "%s se importarán durante la noche por primera vez." #. translators: %s is "what you call TouchPoint at your church", which is a setting -#: src/TouchPoint-WP/Auth.php:140 +#: src/TouchPoint-WP/Auth.php:142 msgid "Sign in with your %s account" msgstr "Inicie sesión con su cuenta de %s" -#: src/TouchPoint-WP/Auth.php:402 +#: src/TouchPoint-WP/Auth.php:410 msgid "Your login token expired." msgstr "Su token de inicio de sesión caducó." -#: src/TouchPoint-WP/Auth.php:417 +#: src/TouchPoint-WP/Auth.php:425 msgid "Your login token is invalid." msgstr "Su token de inicio de sesión no es válido." -#: src/TouchPoint-WP/Auth.php:429 +#: src/TouchPoint-WP/Auth.php:437 msgid "Session could not be validated." msgstr "No se pudo validar la sesión." -#: src/TouchPoint-WP/EventsCalendar.php:59 +#: src/TouchPoint-WP/EventsCalendar.php:77 msgid "Recurring" msgstr "Periódico" -#: src/TouchPoint-WP/EventsCalendar.php:62 +#: src/TouchPoint-WP/EventsCalendar.php:80 +#: src/TouchPoint-WP/EventsCalendar.php:297 msgid "Multi-Day" msgstr "varios días" -#: src/TouchPoint-WP/Involvement.php:441 +#: src/TouchPoint-WP/Involvement.php:495 msgid "Currently Full" msgstr "Actualmente lleno" -#: src/TouchPoint-WP/Involvement.php:445 +#: src/TouchPoint-WP/Involvement.php:500 msgid "Currently Closed" msgstr "Actualmente cerrado" -#: src/TouchPoint-WP/Involvement.php:451 +#: src/TouchPoint-WP/Involvement.php:507 msgid "Registration Not Open Yet" msgstr "Registro aún no abierto" -#: src/TouchPoint-WP/Involvement.php:456 +#: src/TouchPoint-WP/Involvement.php:513 msgid "Registration Closed" msgstr "Registro cerrado" -#: src/TouchPoint-WP/Involvement.php:1318 -#: src/TouchPoint-WP/Partner.php:749 +#: src/TouchPoint-WP/Involvement.php:1728 +#: src/TouchPoint-WP/Partner.php:814 msgid "Any" msgstr "Cualquier" #. translators: %s is for the user-provided term for the items on the map (e.g. Small Group or Partner) -#: src/TouchPoint-WP/Involvement.php:1528 -#: src/TouchPoint-WP/Partner.php:771 +#: src/TouchPoint-WP/Involvement.php:1936 +#: src/TouchPoint-WP/Partner.php:838 msgid "The %s listed are only those shown on the map." msgstr "Los %s enumerados son solo los que se muestran en el mapa." -#: src/TouchPoint-WP/Involvement.php:2776 +#: src/TouchPoint-WP/Involvement.php:3556 msgid "Men Only" msgstr "Solo hombres" -#: src/TouchPoint-WP/Involvement.php:2779 +#: src/TouchPoint-WP/Involvement.php:3559 msgid "Women Only" msgstr "Solo mujeres" -#: src/TouchPoint-WP/Involvement.php:2842 +#: src/TouchPoint-WP/Involvement.php:3636 msgid "Contact Leaders" msgstr "Contacta con las líderes" -#: src/TouchPoint-WP/Involvement.php:2850 +#: src/TouchPoint-WP/Involvement.php:3706 +#: src/TouchPoint-WP/Involvement.php:3765 msgid "Register" msgstr "Regístrate ahora" -#: src/TouchPoint-WP/Involvement.php:2855 +#: src/TouchPoint-WP/Involvement.php:3712 msgid "Create Account" msgstr "Crear cuenta" -#: src/TouchPoint-WP/Involvement.php:2859 +#: src/TouchPoint-WP/Involvement.php:3716 msgid "Schedule" msgstr "Programe" -#: src/TouchPoint-WP/Involvement.php:2864 +#: src/TouchPoint-WP/Involvement.php:3721 msgid "Give" msgstr "Dar" -#: src/TouchPoint-WP/Involvement.php:2867 +#: src/TouchPoint-WP/Involvement.php:3724 msgid "Manage Subscriptions" msgstr "Administrar suscripciones" -#: src/TouchPoint-WP/Involvement.php:2870 +#: src/TouchPoint-WP/Involvement.php:3727 msgid "Record Attendance" msgstr "Registre su asistencia" -#: src/TouchPoint-WP/Involvement.php:2873 +#: src/TouchPoint-WP/Involvement.php:3730 msgid "Get Tickets" msgstr "Obtener boletos" -#: src/TouchPoint-WP/Involvement.php:2880 -#: assets/js/base-defer.js:987 +#: src/TouchPoint-WP/Involvement.php:3756 +#: assets/js/base-defer.js:1001 msgid "Join" msgstr "Únete" -#: src/TouchPoint-WP/Involvement.php:2889 -#: src/TouchPoint-WP/Partner.php:1227 +#: src/TouchPoint-WP/Involvement.php:3653 +#: src/TouchPoint-WP/Partner.php:1318 msgid "Show on Map" msgstr "Muestra en el mapa" #. translators: %s is for the user-provided "Global Partner" and "Secure Partner" terms. -#: src/TouchPoint-WP/Partner.php:778 +#: src/TouchPoint-WP/Partner.php:845 msgid "The %1$s listed are only those shown on the map, as well as all %2$s." msgstr "Los %1$s enumerados son solo los que se muestran en el mapa, así como todos los %2$s." -#: src/TouchPoint-WP/Partner.php:1184 +#: src/TouchPoint-WP/Partner.php:1259 msgid "Not Shown on Map" msgstr "No se muestra en el mapa" -#: src/TouchPoint-WP/Person.php:149 +#: src/TouchPoint-WP/Person.php:150 msgid "No WordPress User ID provided for initializing a person object." msgstr "No se proporcionó una identificación de usuario de WordPress para inicializar un objeto de persona." -#: src/TouchPoint-WP/Person.php:641 +#: src/TouchPoint-WP/Person.php:642 msgid "TouchPoint People ID" msgstr "ID de Personas de TouchPoint" -#: src/TouchPoint-WP/Person.php:1187 +#: src/TouchPoint-WP/Person.php:1192 msgid "Contact" msgstr "Contacta" -#: src/TouchPoint-WP/Rsvp.php:82 +#: src/TouchPoint-WP/Meeting.php:745 +#: src/TouchPoint-WP/Meeting.php:766 +#: src/TouchPoint-WP/Rsvp.php:80 msgid "RSVP" msgstr "RSVP" -#: src/TouchPoint-WP/TouchPointWP.php:2431 +#: src/TouchPoint-WP/TouchPointWP.php:2031 msgid "Unknown Type" msgstr "Tipo desconocido" -#: src/TouchPoint-WP/TouchPointWP.php:2488 +#: src/TouchPoint-WP/TouchPointWP.php:2088 msgid "Your Searches" msgstr "Tus búsquedas" -#: src/TouchPoint-WP/TouchPointWP.php:2491 +#: src/TouchPoint-WP/TouchPointWP.php:2091 msgid "Public Searches" msgstr "Búsquedas públicas" -#: src/TouchPoint-WP/TouchPointWP.php:2494 +#: src/TouchPoint-WP/TouchPointWP.php:2094 msgid "Status Flags" msgstr "Indicadores de Estado" -#: src/TouchPoint-WP/TouchPointWP.php:2499 -#: src/TouchPoint-WP/TouchPointWP.php:2500 +#: src/TouchPoint-WP/TouchPointWP.php:2099 +#: src/TouchPoint-WP/TouchPointWP.php:2100 msgid "Current Value" msgstr "Valor actual" -#: src/TouchPoint-WP/TouchPointWP.php:2628 -#: src/TouchPoint-WP/TouchPointWP.php:2668 +#: src/TouchPoint-WP/TouchPointWP.php:2217 +#: src/TouchPoint-WP/TouchPointWP.php:2253 msgid "Invalid or incomplete API Settings." msgstr "Configuración de API no válida o incompleta." -#: src/TouchPoint-WP/TouchPointWP.php:2636 -#: src/TouchPoint-WP/TouchPointWP.php:2675 +#: src/TouchPoint-WP/TouchPointWP.php:2267 +#: src/TouchPoint-WP/TouchPointWP.php:2311 msgid "Host appears to be missing from TouchPoint-WP configuration." msgstr "Parece que falta el host en la configuración de TouchPoint-WP." -#: src/TouchPoint-WP/TouchPointWP.php:2795 +#: src/TouchPoint-WP/TouchPointWP.php:2443 msgid "People Query Failed" msgstr "Consulta de registros de personas fallida" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:205 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:257 msgid "Basic Settings" msgstr "Ajustes básicos" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:206 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:258 msgid "Connect to TouchPoint and choose which features you wish to use." msgstr "Conéctese a TouchPoint y elija qué funciones desea usar." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:210 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:262 msgid "Enable Authentication" msgstr "Habilitar autenticación" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:211 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:263 msgid "Allow TouchPoint users to sign into this website with TouchPoint." msgstr "Permita que los usuarios de TouchPoint inicien sesión en este sitio web con TouchPoint." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:221 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:274 msgid "Enable RSVP Tool" msgstr "Habilitar la herramienta RSVP" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:222 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:275 msgid "Add a crazy-simple RSVP button to WordPress event pages." msgstr "Agregue un botón RSVP muy simple a las páginas de eventos de WordPress." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:228 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:282 msgid "Enable Involvements" msgstr "Habilitar Participaciones" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:229 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:283 msgid "Load Involvements from TouchPoint for involvement listings and entries native in your website." msgstr "Cargue participaciones desde TouchPoint para obtener listas de participación y entradas nativas en su sitio web." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:238 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:304 msgid "Enable Public People Lists" msgstr "Habilitar listas de personas públicas" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:239 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:305 msgid "Import public people listings from TouchPoint (e.g. staff or elders)" msgstr "Importe listados públicos de personas desde TouchPoint (por ejemplo, personal o ancianos)" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:248 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:315 msgid "Enable Global Partner Listings" msgstr "Habilitar listados de Socios Globales" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:249 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:316 msgid "Import ministry partners from TouchPoint to list publicly." msgstr "Importe socios ministeriales de TouchPoint para incluirlos en una lista pública." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:268 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:337 msgid "Display Name" msgstr "Nombre para mostrar" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:269 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:338 msgid "What your church calls your TouchPoint database." msgstr "Lo que su iglesia llama su base de datos TouchPoint." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:279 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:348 msgid "TouchPoint Host Name" msgstr "Nombre de host del TouchPoint" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:280 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:349 msgid "The domain for your TouchPoint database, without the https or any slashes." msgstr "El dominio de su base de datos TouchPoint, sin https ni barras." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:291 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:361 msgid "Custom Mobile App Deeplink Host Name" msgstr "Nombre de host de enlace profundo de aplicación móvil personalizada" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:303 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:373 msgid "TouchPoint API Username" msgstr "Nombre de usuario de la API de TouchPoint" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:314 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:385 msgid "TouchPoint API User Password" msgstr "Contraseña de usuario de la API de TouchPoint" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:315 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:386 msgid "The password of a user account in TouchPoint with API permissions." msgstr "La contraseña de una cuenta de usuario en TouchPoint con permisos de API." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:326 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:398 msgid "TouchPoint API Script Name" msgstr "Nombre de la secuencia de comandos de la API de TouchPoint" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:327 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:399 msgid "The name of the Python script loaded into TouchPoint. Don't change this unless you know what you're doing." msgstr "El nombre de la secuencia de comandos de Python cargada en TouchPoint. No cambies esto a menos que sepas lo que estás haciendo." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:337 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:410 msgid "Google Maps Javascript API Key" msgstr "Clave de la API de Javascript de Google Maps" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:338 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:411 msgid "Required for embedding maps." msgstr "Necesario para incrustar mapas." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:348 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:422 msgid "Google Maps Geocoding API Key" msgstr "Clave API de codificación geográfica de Google Maps" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:349 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:423 msgid "Optional. Allows for reverse geocoding of user locations." msgstr "Opcional. Permite la geocodificación inversa de las ubicaciones de los usuarios." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:366 -#: src/TouchPoint-WP/TouchPointWP_Settings.php:371 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:463 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:468 msgid "Generate Scripts" msgstr "Generar secuencias de comandos" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:370 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:467 msgid "Upload the package to {tpName} here" msgstr "Sube el paquete a {tpName} aquí" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:387 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:484 msgid "People" msgstr "Gente" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:388 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:485 msgid "Manage how people are synchronized between TouchPoint and WordPress." msgstr "Administre cómo se sincronizan las personas entre TouchPoint y WordPress." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:392 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:489 msgid "Contact Keywords" msgstr "Palabras clave de contacto" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:393 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:490 msgid "These keywords will be used when someone clicks the \"Contact\" button on a Person's listing or profile." msgstr "Estas palabras clave se utilizarán cuando alguien haga clic en el botón \"Contactar\" en la lista o el perfil de una Persona." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:404 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:501 msgid "Extra Value for WordPress User ID" msgstr "Valor Adicional para la ID de usuario de WordPress" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:405 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:502 msgid "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." msgstr "El nombre del valor adicional que se usará para el ID de usuario de WordPress. Si está utilizando varias instancias de WordPress con una base de datos de TouchPoint, necesitará que estos valores sean únicos entre las instancias de WordPress. En la mayoría de los casos, el valor predeterminado está bien." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:415 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:512 msgid "Extra Value: Biography" msgstr "Valor Adicional: Biografía" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:416 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:513 msgid "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." msgstr "Importe una biografía desde un campo de Valor Adicional de Persona. Puede ser un Valor Adicional HTML o de texto. Esto sobrescribirá cualquier valor establecido por WordPress. Dejar en blanco para no importar." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:426 -#: src/TouchPoint-WP/TouchPointWP_Settings.php:662 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:523 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:761 msgid "Extra Values to Import" msgstr "Valor Adicional para importar" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:427 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:524 msgid "Import People Extra Value fields as User Meta data." msgstr "Importe campos de valor extra de personas como metadatos de usuario." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:443 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:540 msgid "Authentication" msgstr "Autenticación" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:444 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:541 msgid "Allow users to log into WordPress using TouchPoint." msgstr "Permita que los usuarios inicien sesión en WordPress usando TouchPoint." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:448 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:545 msgid "Make TouchPoint the default authentication method." msgstr "Haga que TouchPoint sea el método de autenticación predeterminado." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:458 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:555 msgid "Enable Auto-Provisioning" msgstr "Habilitar el aprovisionamiento automático" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:459 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:556 msgid "Automatically create WordPress users, if needed, to match authenticated TouchPoint users." msgstr "Cree automáticamente usuarios de WordPress, si es necesario, para que coincidan con los usuarios autenticados de TouchPoint." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:468 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:565 msgid "Change 'Edit Profile' links" msgstr "Cambiar los enlaces 'Editar perfil'" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:469 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:566 msgid "\"Edit Profile\" links will take the user to their TouchPoint profile, instead of their WordPress profile." msgstr "Los enlaces \"Editar perfil\" llevarán al usuario a su perfil de TouchPoint, en lugar de a su perfil de WordPress." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:478 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:575 msgid "Enable full logout" msgstr "Habilitar cierre de sesión completo" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:479 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:576 msgid "Logout of TouchPoint when logging out of WordPress." msgstr "Cierre sesión en TouchPoint al cerrar sesión en WordPress." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:485 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:582 msgid "Prevent Subscriber Admin Bar" msgstr "Prevenir la barra de administración de suscriptores" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:486 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:583 msgid "By enabling this option, users who can't edit anything won't see the Admin bar." msgstr "Al habilitar esta opción, los usuarios que no pueden editar nada no verán la barra de administración." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:500 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:597 msgid "Involvements" msgstr "Involucramientos" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:501 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:598 msgid "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." msgstr "Importe participaciones desde TouchPoint para enumerarlas en su sitio web, para grupos pequeños, clases y más. Seleccione la(s) división(es) que corresponda(n) inmediatamente al tipo de participación que desea enumerar. Por ejemplo, si desea una lista de grupos pequeños y tiene una división de grupos pequeños, solo seleccione la división de grupos pequeños. Si desea que las participaciones se puedan filtrar por divisiones adicionales, seleccione esas divisiones en la pestaña Divisiones, no aquí." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:506 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:603 msgid "Involvement Post Types" msgstr "Tipos de publicaciones de Involucramientos" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:535 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:633 msgid "Global Partners" msgstr "Misioneros" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:536 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:634 msgid "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." msgstr "Administre cómo se importan los socios globales desde TouchPoint para incluirlos en WordPress. Los socios se agrupan por familia y el contenido se proporciona a través de Valor Extra Familiar. Esto funciona tanto para registros de personas como de empresas." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:540 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:638 msgid "Global Partner Name (Plural)" msgstr "Nombre de los misioneros (plural)" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:541 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:639 msgid "What you call Global Partners at your church" msgstr "Lo que llamas los Misioneros en tu iglesia" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:551 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:649 msgid "Global Partner Name (Singular)" msgstr "Nombre de un misionero (singular)" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:552 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:650 msgid "What you call a Global Partner at your church" msgstr "Lo que llamas un Misionero en tu iglesia" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:562 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:660 msgid "Global Partner Name for Secure Places (Plural)" msgstr "Nombre de los misioneros para lugares seguros (plural)" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:563 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:661 msgid "What you call Secure Global Partners at your church" msgstr "Lo que llamas un Misionero seguro en tu iglesia" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:573 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:671 msgid "Global Partner Name for Secure Places (Singular)" msgstr "Nombre de un misionero para lugares seguros (singular)" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:574 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:672 msgid "What you call a Secure Global Partner at your church" msgstr "Lo que llamas los Misioneros seguros en tu iglesia" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:584 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:682 msgid "Global Partner Slug" msgstr "Slug de Socio Global" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:585 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:683 msgid "The root path for Global Partner posts" msgstr "La ruta raíz para las publicaciones de Socios Globales" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:596 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:695 msgid "Saved Search" msgstr "Búsqueda Guardada" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:597 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:696 msgid "Anyone who is included in this saved search will be included in the listing." msgstr "Cualquiera que esté incluido en esta búsqueda guardada se incluirá en la lista." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:607 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:706 msgid "Extra Value: Description" msgstr "Valor Adicional: Descripción" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:608 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:707 msgid "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." msgstr "Importe una descripción de un campo de Valor Extra Familiar. Puede ser un valor adicional HTML o de texto. Esto se convierte en el cuerpo de la publicación del socio global." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:618 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:717 msgid "Extra Value: Summary" msgstr "Valor Adicional: Resumen" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:619 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:718 msgid "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." msgstr "Opcional. Importe una breve descripción de un campo de Valor Extra Familiar. Puede ser un Valor Adicional HTML o de texto. Si no se proporciona, la biografía completa se truncará." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:629 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:728 msgid "Latitude Override" msgstr "Anulación de latitud" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:630 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:729 msgid "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." msgstr "Designe un Valor Familiar Adicional de texto que contenga una latitud que anule cualquier ubicación en el perfil del socio para el mapa de socios. Tanto la latitud como la longitud deben proporcionarse para que se produzca una anulación." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:640 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:739 msgid "Longitude Override" msgstr "Anulación de longitud" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:641 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:740 msgid "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." msgstr "Designe un Valor Familiar Adicional de texto que contenga una longitud que anule cualquier ubicación en el perfil del socio para el mapa de socios. Tanto la latitud como la longitud deben proporcionarse para que se produzca una anulación." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:651 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:750 msgid "Public Location" msgstr "Ubicación Pública" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:652 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:751 msgid "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." msgstr "Designe un Valor Adicional Familiar de texto que contendrá la ubicación del socio, como desea que se enumere públicamente. Para los socios que tienen DecoupleLocation habilitado, este campo se asociará con el punto del mapa, no con la entrada de la lista." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:663 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:762 msgid "Import Family Extra Value fields as Meta data on the partner's post" msgstr "Importe campos de Valor Adicional Familiar como Metadatos en la publicación del socio" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:674 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:773 msgid "Primary Taxonomy" msgstr "Taxonomía Primaria" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:675 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:774 msgid "Import a Family Extra Value as the primary means by which partners are organized." msgstr "Importe un Valor Adicional Familiar como el medio principal por el cual se organizan los socios." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:690 -msgid "Events Calendar" -msgstr "Calendario de eventos" - -#: src/TouchPoint-WP/TouchPointWP_Settings.php:691 -msgid "Integrate with The Events Calendar from ModernTribe." -msgstr "Integre con el calendario de eventos de ModernTribe." - -#: src/TouchPoint-WP/TouchPointWP_Settings.php:695 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:817 msgid "Events for Custom Mobile App" msgstr "Eventos para la aplicación móvil personalizada" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:698 -msgid "To use your Events Calendar events in the Custom mobile app, set the Provider to Wordpress Plugin - Modern Tribe and use this url:" -msgstr "Para usar los eventos de su calendario de eventos en la aplicación móvil personalizada, configure el proveedor en Wordpress Plugin - Modern Tribe y use esta URL:" - -#: src/TouchPoint-WP/TouchPointWP_Settings.php:700 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:822 msgid "Preview" msgstr "Preestrena" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:715 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:837 msgid "Use Standardizing Stylesheet" msgstr "Usar hoja de estilo de estandarización" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:716 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:838 msgid "Inserts some basic CSS into the events feed to clean up display" msgstr "Inserta algo de CSS básico en el feed de eventos para limpiar la pantalla" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:726 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:935 msgid "Divisions" msgstr "Divisiones" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:727 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:936 msgid "Import Divisions from TouchPoint to your website as a taxonomy. These are used to classify users and involvements." msgstr "Importe Divisiones desde TouchPoint a su sitio web como una taxonomía. Estos se utilizan para clasificar a los usuarios y las participaciones." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:731 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:940 msgid "Division Name (Plural)" msgstr "Nombre de la División (plural)" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:732 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:941 msgid "What you call Divisions at your church" msgstr "Lo que llamas Divisiones en tu iglesia" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:742 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:952 msgid "Division Name (Singular)" msgstr "Nombre de la División (Singular)" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:743 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:953 msgid "What you call a Division at your church" msgstr "Lo que llamas una división en tu iglesia" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:753 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:964 msgid "Division Slug" msgstr "Slug de División" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:754 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:965 msgid "The root path for the Division Taxonomy" msgstr "La ruta raíz para la Taxonomía de División" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:766 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:978 msgid "These Divisions will be imported for the taxonomy" msgstr "Estas Divisiones se importarán para la taxonomía" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:804 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1029 msgid "Campuses" msgstr "Campus" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:805 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1030 msgid "Import Campuses from TouchPoint to your website as a taxonomy. These are used to classify users and involvements." msgstr "Importe Campus desde TouchPoint a su sitio web como una taxonomía. Estos se utilizan para clasificar a los usuarios y las participaciones." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:812 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1037 msgid "Campus Name (Plural)" msgstr "Nombre del Campus (Plural)" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:813 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1038 msgid "What you call Campuses at your church" msgstr "Lo que llamas Campus en tu iglesia" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:823 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1049 msgid "Campus Name (Singular)" msgstr "Nombre del Campus (Singular)" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:824 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1050 msgid "What you call a Campus at your church" msgstr "Lo que llamas un Campus en tu iglesia" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:834 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1061 msgid "Campus Slug" msgstr "Slug de Campus" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:835 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1062 msgid "The root path for the Campus Taxonomy" -msgstr "El camino raíz para la Taxonomía del Campus" +msgstr "La ruta raíz para la Taxonomía del Campus" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:849 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1078 msgid "Resident Codes" msgstr "Códigos de Residentes" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:850 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1079 msgid "Import Resident Codes from TouchPoint to your website as a taxonomy. These are used to classify users and involvements that have locations." msgstr "Importe Códigos de Residentes desde TouchPoint a su sitio web como una taxonomía. Estos se utilizan para clasificar los usuarios y las participaciones que tienen ubicaciones." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:854 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1083 msgid "Resident Code Name (Plural)" msgstr "Nombre de Código de Tesidente (Plural)" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:855 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1084 msgid "What you call Resident Codes at your church" msgstr "Lo que llamas Códigos de Residente en tu iglesia" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:865 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1095 msgid "Resident Code Name (Singular)" msgstr "Nombre de Código de Residente (singular)" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:866 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1096 msgid "What you call a Resident Code at your church" msgstr "Lo que llamas un Código de Residencia en tu iglesia" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:876 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1107 msgid "Resident Code Slug" msgstr "Slug de Código Residente" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:877 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1108 msgid "The root path for the Resident Code Taxonomy" msgstr "La ruta raíz para la Taxonomía del Código de Residente" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:1034 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1284 msgid "password saved" msgstr "contraseña guardada" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:1090 -#: src/TouchPoint-WP/TouchPointWP_Settings.php:1091 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1338 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1339 msgid "TouchPoint-WP" msgstr "TouchPoint-WP" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:1122 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1387 msgid "Settings" msgstr "Ajustes" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:1351 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1627 msgid "Script Update Failed" msgstr "Actualización de secuencia de comandos fallida" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:1470 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1749 msgid "TouchPoint-WP Settings" msgstr "Configuración de TouchPoint-WP" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:1521 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1800 msgid "Save Settings" msgstr "Guardar ajustes" -#: src/TouchPoint-WP/Person.php:1398 -#: src/TouchPoint-WP/Utilities.php:205 +#: src/TouchPoint-WP/Person.php:1451 +#: src/TouchPoint-WP/Utilities.php:286 #: assets/js/base-defer.js:18 msgid "and" msgstr "y" -#: assets/js/base-defer.js:208 -#: assets/js/base-defer.js:1154 +#: assets/js/base-defer.js:212 +#: assets/js/base-defer.js:1168 msgid "Your Location" msgstr "Tu ubicación" -#: assets/js/base-defer.js:229 +#: assets/js/base-defer.js:233 msgid "User denied the request for Geolocation." msgstr "El usuario denegó la solicitud de geolocalización." -#: assets/js/base-defer.js:233 +#: assets/js/base-defer.js:237 msgid "Location information is unavailable." msgstr "La información de ubicación no está disponible." -#: assets/js/base-defer.js:237 +#: assets/js/base-defer.js:241 msgid "The request to get user location timed out." msgstr "Se agotó el tiempo de espera de la solicitud para obtener la ubicación del usuario." -#: assets/js/base-defer.js:241 +#: assets/js/base-defer.js:245 msgid "An unknown error occurred." msgstr "Un error desconocido ocurrió." -#: assets/js/base-defer.js:277 -#: assets/js/base-defer.js:287 +#: assets/js/base-defer.js:281 +#: assets/js/base-defer.js:291 msgid "No geolocation option available." msgstr "No hay opción de geolocalización disponible." -#: assets/js/base-defer.js:917 -#: assets/js/base-defer.js:954 -#: assets/js/base-defer.js:1411 -#: assets/js/meeting-defer.js:183 +#: assets/js/base-defer.js:931 +#: assets/js/base-defer.js:968 +#: assets/js/base-defer.js:1425 +#: assets/js/meeting-defer.js:182 msgid "Something strange happened." msgstr "Algo extraño sucedió." -#: assets/js/base-defer.js:943 -#: assets/js/base-defer.js:1400 +#: assets/js/base-defer.js:957 +#: assets/js/base-defer.js:1414 msgid "Your message has been sent." msgstr "Tu mensaje ha sido enviado." -#: assets/js/base-defer.js:983 +#: assets/js/base-defer.js:997 msgid "Who is joining the group?" msgstr "¿Quién se une al grupo?" -#: assets/js/base-defer.js:988 -#: assets/js/base-defer.js:1046 -#: assets/js/base-defer.js:1362 -#: assets/js/base-defer.js:1455 -#: assets/js/meeting-defer.js:225 +#: assets/js/base-defer.js:1002 +#: assets/js/base-defer.js:1060 +#: assets/js/base-defer.js:1376 +#: assets/js/base-defer.js:1469 +#: assets/js/meeting-defer.js:224 msgid "Cancel" msgstr "Cancelar" -#: assets/js/base-defer.js:1001 +#: assets/js/base-defer.js:1015 msgid "Select who should be added to the group." msgstr "Seleccione quién debe agregarse al grupo." -#: assets/js/base-defer.js:1039 -#: assets/js/base-defer.js:1355 +#: assets/js/base-defer.js:1053 +#: assets/js/base-defer.js:1369 msgid "From" msgstr "De" -#: assets/js/base-defer.js:1040 -#: assets/js/base-defer.js:1356 +#: assets/js/base-defer.js:1054 +#: assets/js/base-defer.js:1370 msgid "Message" msgstr "Mensaje" -#: assets/js/base-defer.js:1055 -#: assets/js/base-defer.js:1371 +#: assets/js/base-defer.js:1069 +#: assets/js/base-defer.js:1385 msgid "Please provide a message." msgstr "Proporcione un mensaje." -#: assets/js/base-defer.js:1140 -#: assets/js/base-defer.js:1142 +#: assets/js/base-defer.js:1154 +#: assets/js/base-defer.js:1156 msgid "We don't know where you are." msgstr "No sabemos dónde estás." -#: assets/js/base-defer.js:1140 -#: assets/js/base-defer.js:1150 +#: assets/js/base-defer.js:1154 +#: assets/js/base-defer.js:1164 msgid "Click here to use your actual location." msgstr "Haga clic aquí para usar su ubicación real." -#: assets/js/base-defer.js:1301 -#: assets/js/base-defer.js:1318 +#: assets/js/base-defer.js:1315 +#: assets/js/base-defer.js:1332 msgid "clear" msgstr "borrar" -#: assets/js/base-defer.js:1307 +#: assets/js/base-defer.js:1321 msgid "Other Relatives..." msgstr "Otros familiares..." -#: assets/js/base-defer.js:1445 +#: assets/js/base-defer.js:1459 msgid "Tell us about yourself." msgstr "Dinos sobre ti." -#: assets/js/base-defer.js:1447 -#: assets/js/base-defer.js:1502 +#: assets/js/base-defer.js:1461 +#: assets/js/base-defer.js:1516 msgid "Email Address" msgstr "Correo electrónico" -#: assets/js/base-defer.js:1448 -#: assets/js/base-defer.js:1503 +#: assets/js/base-defer.js:1462 +#: assets/js/base-defer.js:1517 msgid "Zip Code" msgstr "Condigo postal" -#: assets/js/base-defer.js:1500 +#: assets/js/base-defer.js:1514 msgid "Our system doesn't recognize you,
so we need a little more info." msgstr "Nuestro sistema no te reconoce.
Necesitamos un poco más de información." -#: assets/js/base-defer.js:1504 +#: assets/js/base-defer.js:1518 msgid "First Name" msgstr "Primer nombre" -#: assets/js/base-defer.js:1505 +#: assets/js/base-defer.js:1519 msgid "Last Name" msgstr "Apellido" -#: assets/js/base-defer.js:1507 +#: assets/js/base-defer.js:1521 msgid "Phone" msgstr "Teléfono" @@ -937,189 +936,189 @@ msgstr "Teléfono" msgid "Event Past" msgstr "Evento pasado" -#: assets/js/meeting-defer.js:217 +#: assets/js/meeting-defer.js:216 msgid "Who is coming?" msgstr "¿Quien viene?" -#: assets/js/meeting-defer.js:217 +#: assets/js/meeting-defer.js:216 msgid "Indicate who is or is not coming. This will overwrite any existing RSVP." msgstr "Indica quien viene. Esto va a duplicar a una persona que ya está escrita." -#: assets/js/meeting-defer.js:217 +#: assets/js/meeting-defer.js:216 msgid "To avoid overwriting an existing RSVP, leave that person blank." msgstr "Para evadir duplicar una persona que ya está escrita, de ja el espacio de la persona vacío." -#: assets/js/meeting-defer.js:217 +#: assets/js/meeting-defer.js:216 msgid "To protect privacy, we won't show existing RSVPs here." msgstr "Para proteger tu privacidad, nosotros no mostraremos las personas que están registradas aquí." -#: assets/js/meeting-defer.js:217 +#: assets/js/meeting-defer.js:216 msgid "Yes" msgstr "Sí" -#: assets/js/meeting-defer.js:217 +#: assets/js/meeting-defer.js:216 msgid "No" msgstr "No" -#: assets/js/meeting-defer.js:223 +#: assets/js/meeting-defer.js:222 msgid "Add Someone Else" msgstr "Agregar a alguien más" -#: assets/js/meeting-defer.js:224 +#: assets/js/meeting-defer.js:223 msgid "Submit" msgstr "Enviar" -#: assets/js/meeting-defer.js:243 +#: assets/js/meeting-defer.js:242 msgid "Nothing to submit." msgstr "Nada que enviar." -#: src/TouchPoint-WP/TouchPointWP.php:2736 +#: src/TouchPoint-WP/TouchPointWP.php:2384 msgid "The scripts on TouchPoint that interact with this plugin are out-of-date, and an automatic update failed." msgstr "Los scripts en TouchPoint que interactúan con este complemento están desactualizados y falló una actualización automática." #. translators: "RSVP for {Event Name}" This is the heading on the RSVP modal. The event name isn't translated because it comes from TouchPoint. -#: assets/js/meeting-defer.js:205 +#: assets/js/meeting-defer.js:204 msgid "RSVP for %s" msgstr "RSVP para %s" -#: assets/js/meeting-defer.js:172 +#: assets/js/meeting-defer.js:171 msgid "Response Recorded" msgid_plural "Responses Recorded" msgstr[0] "Respuesta registrada" msgstr[1] "Respuestas registrada" #. translators: %s is the name of an involvement, like a particular small group -#: assets/js/base-defer.js:906 +#: assets/js/base-defer.js:920 msgid "Added to %s" msgstr "Añadido a %s" #. translators: %s is the name of an Involvement -#: assets/js/base-defer.js:967 +#: assets/js/base-defer.js:981 msgid "Join %s" msgstr "Únete %s" #. translators: %s is a person's name. This is a heading for a contact modal. -#: assets/js/base-defer.js:1338 +#: assets/js/base-defer.js:1352 msgid "Contact %s" msgstr "Contactar a %s" #. translators: %s is the name of an involvement. This is a heading for a modal. -#: assets/js/base-defer.js:1022 +#: assets/js/base-defer.js:1036 msgid "Contact the Leaders of %s" msgstr "Contacta a los líderes de %s" -#: assets/js/base-defer.js:1045 -#: assets/js/base-defer.js:1361 +#: assets/js/base-defer.js:1059 +#: assets/js/base-defer.js:1375 msgid "Send" msgstr "Envía" -#: assets/js/base-defer.js:909 -#: assets/js/base-defer.js:920 -#: assets/js/base-defer.js:946 -#: assets/js/base-defer.js:957 -#: assets/js/base-defer.js:1403 -#: assets/js/base-defer.js:1414 -#: assets/js/meeting-defer.js:175 -#: assets/js/meeting-defer.js:186 +#: assets/js/base-defer.js:923 +#: assets/js/base-defer.js:934 +#: assets/js/base-defer.js:960 +#: assets/js/base-defer.js:971 +#: assets/js/base-defer.js:1417 +#: assets/js/base-defer.js:1428 +#: assets/js/meeting-defer.js:174 +#: assets/js/meeting-defer.js:185 msgid "OK" msgstr "OK" -#: assets/js/base-defer.js:1454 +#: assets/js/base-defer.js:1468 msgid "Next" msgstr "Siguiente" -#: src/TouchPoint-WP/Involvement.php:1493 -#: src/TouchPoint-WP/TouchPointWP.php:1620 +#: src/TouchPoint-WP/Involvement.php:1901 +#: src/TouchPoint-WP/Taxonomies.php:869 msgid "Marital Status" msgstr "Estado civil" -#: src/TouchPoint-WP/Involvement.php:1506 +#: src/TouchPoint-WP/Involvement.php:1914 msgid "Age" msgstr "Años" -#: src/TouchPoint-WP/Involvement.php:1377 +#: src/TouchPoint-WP/Involvement.php:1785 msgid "Genders" msgstr "Géneros" -#: src/TouchPoint-WP/Utilities.php:69 +#: src/TouchPoint-WP/Utilities.php:136 msgctxt "e.g. event happens weekly on..." msgid "Sundays" msgstr "los domingos" -#: src/TouchPoint-WP/Utilities.php:70 +#: src/TouchPoint-WP/Utilities.php:137 msgctxt "e.g. event happens weekly on..." msgid "Mondays" msgstr "los lunes" -#: src/TouchPoint-WP/Utilities.php:71 +#: src/TouchPoint-WP/Utilities.php:138 msgctxt "e.g. event happens weekly on..." msgid "Tuesdays" msgstr "los martes" -#: src/TouchPoint-WP/Utilities.php:72 +#: src/TouchPoint-WP/Utilities.php:139 msgctxt "e.g. event happens weekly on..." msgid "Wednesdays" msgstr "los miércoles" -#: src/TouchPoint-WP/Utilities.php:73 +#: src/TouchPoint-WP/Utilities.php:140 msgctxt "e.g. event happens weekly on..." msgid "Thursdays" msgstr "los jueves" -#: src/TouchPoint-WP/Utilities.php:74 +#: src/TouchPoint-WP/Utilities.php:141 msgctxt "e.g. event happens weekly on..." msgid "Fridays" msgstr "los viernes" -#: src/TouchPoint-WP/Utilities.php:75 +#: src/TouchPoint-WP/Utilities.php:142 msgctxt "e.g. event happens weekly on..." msgid "Saturdays" msgstr "los sábados" -#: src/TouchPoint-WP/Utilities.php:111 -msgctxt "e.g. event happens weekly on..." +#: src/TouchPoint-WP/Utilities.php:188 +msgctxt "e.g. \"Event happens weekly on...\" or \"This ...\"" msgid "Sun" msgstr "Dom" -#: src/TouchPoint-WP/Utilities.php:112 -msgctxt "e.g. event happens weekly on..." +#: src/TouchPoint-WP/Utilities.php:189 +msgctxt "e.g. \"Event happens weekly on...\" or \"This ...\"" msgid "Mon" msgstr "Lun" -#: src/TouchPoint-WP/Utilities.php:113 -msgctxt "e.g. event happens weekly on..." +#: src/TouchPoint-WP/Utilities.php:190 +msgctxt "e.g. \"Event happens weekly on...\" or \"This ...\"" msgid "Tue" msgstr "Mar" -#: src/TouchPoint-WP/Utilities.php:114 -msgctxt "e.g. event happens weekly on..." +#: src/TouchPoint-WP/Utilities.php:191 +msgctxt "e.g. \"Event happens weekly on...\" or \"This ...\"" msgid "Wed" msgstr "Mié" -#: src/TouchPoint-WP/Utilities.php:115 -msgctxt "e.g. event happens weekly on..." +#: src/TouchPoint-WP/Utilities.php:192 +msgctxt "e.g. \"Event happens weekly on...\" or \"This ...\"" msgid "Thu" msgstr "Jue" -#: src/TouchPoint-WP/Utilities.php:116 -msgctxt "e.g. event happens weekly on..." +#: src/TouchPoint-WP/Utilities.php:193 +msgctxt "e.g. \"Event happens weekly on...\" or \"This ...\"" msgid "Fri" msgstr "Vie" -#: src/TouchPoint-WP/Utilities.php:117 -msgctxt "e.g. event happens weekly on..." +#: src/TouchPoint-WP/Utilities.php:194 +msgctxt "e.g. \"Event happens weekly on...\" or \"This ...\"" msgid "Sat" msgstr "Sáb" -#: src/templates/admin/invKoForm.php:95 +#: src/templates/admin/invKoForm.php:128 msgid "Based on Involvement setting in TouchPoint" msgstr "Basado en la configuración de participación en TouchPoint" -#: src/templates/admin/invKoForm.php:95 +#: src/templates/admin/invKoForm.php:128 msgid "Involvement does not meet weekly" msgstr "La participación no se reúne semanalmente" -#: src/templates/admin/invKoForm.php:99 +#: src/templates/admin/invKoForm.php:132 msgid "Involvement does not have a Schedule" msgstr "La participación no tiene horario" @@ -1135,342 +1134,740 @@ msgstr "Latitud" msgid "Longitude" msgstr "Longitud" -#: src/templates/admin/locationsKoForm.php:43 +#: src/templates/admin/locationsKoForm.php:51 msgid "Static IP Addresses" msgstr "Direcciones IP estáticas" -#: src/templates/admin/locationsKoForm.php:46 +#: src/templates/admin/locationsKoForm.php:54 msgid "If this Location has an internet connection with Static IP Addresses, you can put those addresses here so users are automatically identified with this location." msgstr "Si esta ubicación tiene una conexión a Internet con direcciones IP estáticas, puede colocar esas direcciones aquí para que los usuarios se identifiquen automáticamente con esta ubicación." -#: src/templates/admin/locationsKoForm.php:54 +#: src/templates/admin/locationsKoForm.php:62 msgid "Add IP Address" msgstr "Agregar dirección IP" -#: src/templates/admin/locationsKoForm.php:64 +#: src/templates/admin/locationsKoForm.php:72 msgid "Add Location" msgstr "Añade una ubicación" -#: src/templates/admin/locationsKoForm.php:71 +#: src/templates/admin/locationsKoForm.php:79 msgid "The Campus" msgstr "El campus" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:779 -#: src/TouchPoint-WP/TouchPointWP_Settings.php:785 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1004 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1010 msgid "Locations" msgstr "Ubicaciones" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:780 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1005 msgid "Locations are physical places, probably campuses. None are required, but they can help present geographic information clearly." msgstr "Las ubicaciones son lugares físicos, probablemente campus. No se requiere ninguno, pero pueden ayudar a presentar la información geográfica con claridad." -#: src/TouchPoint-WP/Utilities.php:161 +#: src/TouchPoint-WP/Utilities.php:236 msgctxt "Time of Day" msgid "Late Night" msgstr "Tarde en la noche" -#: src/TouchPoint-WP/Utilities.php:163 +#: src/TouchPoint-WP/Utilities.php:238 msgctxt "Time of Day" msgid "Early Morning" msgstr "Madrugada" -#: src/TouchPoint-WP/Utilities.php:165 +#: src/TouchPoint-WP/Utilities.php:240 msgctxt "Time of Day" msgid "Morning" msgstr "Mañana" -#: src/TouchPoint-WP/Utilities.php:167 +#: src/TouchPoint-WP/Utilities.php:242 msgctxt "Time of Day" msgid "Midday" msgstr "Mediodía" -#: src/TouchPoint-WP/Utilities.php:169 +#: src/TouchPoint-WP/Utilities.php:244 msgctxt "Time of Day" msgid "Afternoon" msgstr "Tarde" -#: src/TouchPoint-WP/Utilities.php:171 +#: src/TouchPoint-WP/Utilities.php:246 msgctxt "Time of Day" msgid "Evening" msgstr "Tardecita" -#: src/TouchPoint-WP/Utilities.php:173 +#: src/TouchPoint-WP/Utilities.php:248 msgctxt "Time of Day" msgid "Night" msgstr "Noche" -#: src/TouchPoint-WP/Involvement.php:1494 +#: src/TouchPoint-WP/Involvement.php:1902 msgctxt "Marital status for a group of people" msgid "Mostly Single" msgstr "Mayoría solteras" -#: src/TouchPoint-WP/Involvement.php:1495 +#: src/TouchPoint-WP/Involvement.php:1903 msgctxt "Marital status for a group of people" msgid "Mostly Married" msgstr "Mayoría casadas" #. translators: %s is the link to "reset the map" -#: src/TouchPoint-WP/Involvement.php:1536 -#: src/TouchPoint-WP/Partner.php:787 +#: src/TouchPoint-WP/Involvement.php:1944 +#: src/TouchPoint-WP/Partner.php:854 msgid "Zoom out or %s to see more." msgstr "Alejar o %s para ver más." -#: src/TouchPoint-WP/Involvement.php:1539 -#: src/TouchPoint-WP/Partner.php:790 +#: src/TouchPoint-WP/Involvement.php:1947 +#: src/TouchPoint-WP/Partner.php:857 msgctxt "Zoom out or reset the map to see more." msgid "reset the map" msgstr "restablecer el mapa" -#. translators: "Mon at 7pm" or "Sundays at 9am & 11am" -#: src/TouchPoint-WP/Involvement.php:712 -#: src/TouchPoint-WP/Involvement.php:732 +#. translators: %1$s is the date(s), %2$s is the time(s). +#: src/TouchPoint-WP/Involvement.php:995 +#: src/TouchPoint-WP/Involvement.php:1027 +#: src/TouchPoint-WP/Involvement.php:1120 +#: src/TouchPoint-WP/Involvement.php:1144 +#: src/TouchPoint-WP/TouchPointWP_Widget.php:71 +#: src/TouchPoint-WP/Utilities/DateFormats.php:288 +#: src/TouchPoint-WP/Utilities/DateFormats.php:352 msgid "%1$s at %2$s" msgstr "%1$s a las %2$s" #. translators: {start date} through {end date} e.g. February 14 through August 12 -#: src/TouchPoint-WP/Involvement.php:741 +#: src/TouchPoint-WP/Involvement.php:1036 msgid "%1$s through %2$s" msgstr "%1$s al %2$s" #. translators: {schedule}, {start date} through {end date} e.g. Sundays at 11am, February 14 through August 12 -#: src/TouchPoint-WP/Involvement.php:748 +#: src/TouchPoint-WP/Involvement.php:1045 msgid "%1$s, %2$s through %3$s" msgstr "%1$s, %2$s al %3$s" #. translators: Starts {start date} e.g. Starts September 15 -#: src/TouchPoint-WP/Involvement.php:758 +#: src/TouchPoint-WP/Involvement.php:1054 msgid "Starts %1$s" msgstr "Comienza el %1$s" #. translators: {schedule}, starting {start date} e.g. Sundays at 11am, starting February 14 -#: src/TouchPoint-WP/Involvement.php:764 +#: src/TouchPoint-WP/Involvement.php:1062 msgid "%1$s, starting %2$s" msgstr "%1$s, comienza el %2$s" #. translators: Through {end date} e.g. Through September 15 -#: src/TouchPoint-WP/Involvement.php:773 +#: src/TouchPoint-WP/Involvement.php:1070 msgid "Through %1$s" msgstr "Hasta el %1$s" #. translators: {schedule}, through {end date} e.g. Sundays at 11am, through February 14 -#: src/TouchPoint-WP/Involvement.php:779 +#: src/TouchPoint-WP/Involvement.php:1078 msgid "%1$s, through %2$s" msgstr "%1$s, hasta el %2$s" #. translators: number of miles #: src/templates/parts/involvement-nearby-list.php:10 -#: src/TouchPoint-WP/Involvement.php:2803 +#: src/TouchPoint-WP/Involvement.php:3577 msgctxt "miles. Unit is appended to a number. %2.1f is the number, so %2.1fmi looks like '12.3mi'" msgid "%2.1fmi" msgstr "%2.1fmi" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:304 -msgid "The username of a user account in TouchPoint with API permissions." -msgstr "El nombre de usuario de una cuenta de usuario en TouchPoint con permisos de API." +#: src/TouchPoint-WP/TouchPointWP_Settings.php:374 +msgid "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." +msgstr "El nombre de usuario de una cuenta de usuario en TouchPoint con permisos API. Se recomienda encarecidamente que cree una persona/usuario independiente para este fin, en lugar de utilizar la cuenta de un miembro del personal." -#: src/templates/admin/invKoForm.php:103 +#: src/templates/admin/invKoForm.php:136 msgid "Involvement has a registration type of \"No Online Registration\"" msgstr "La participación tiene un tipo de registro de \"No Online Registration\"" -#: src/templates/admin/invKoForm.php:107 +#: src/templates/admin/invKoForm.php:140 msgid "Involvement registration has ended (end date is past)" msgstr "El registro de participación ha finalizado (la fecha de finalización ya pasó)" -#: src/TouchPoint-WP/Involvement.php:1628 +#: src/TouchPoint-WP/Involvement.php:2040 msgid "This involvement type doesn't exist." msgstr "Este tipo de participación no existe." -#: src/TouchPoint-WP/Involvement.php:1638 +#: src/TouchPoint-WP/Involvement.php:2050 msgid "This involvement type doesn't have geographic locations enabled." msgstr "Este tipo de participación no tiene habilitadas las ubicaciones geográficas." -#: src/TouchPoint-WP/Involvement.php:1657 +#: src/TouchPoint-WP/Involvement.php:2069 msgid "Could not locate." msgstr "No se pudo localizar." -#: src/TouchPoint-WP/Meeting.php:91 -#: src/TouchPoint-WP/TouchPointWP.php:940 +#: src/TouchPoint-WP/Meeting.php:672 +#: src/TouchPoint-WP/TouchPointWP.php:1033 msgid "Only GET requests are allowed." msgstr "Solo se permiten solicitudes GET." -#: src/TouchPoint-WP/Meeting.php:119 -#: src/TouchPoint-WP/TouchPointWP.php:369 +#: src/TouchPoint-WP/Meeting.php:700 +#: src/TouchPoint-WP/TouchPointWP.php:362 msgid "Only POST requests are allowed." msgstr "Solo se permiten solicitudes POST." -#: src/TouchPoint-WP/Meeting.php:129 -#: src/TouchPoint-WP/TouchPointWP.php:378 +#: src/TouchPoint-WP/Meeting.php:710 +#: src/TouchPoint-WP/TouchPointWP.php:371 msgid "Invalid data provided." msgstr "Datos proporcionados no válidos." -#: src/TouchPoint-WP/Involvement.php:2941 -#: src/TouchPoint-WP/Involvement.php:3018 +#: src/TouchPoint-WP/Involvement.php:3861 +#: src/TouchPoint-WP/Involvement.php:3964 msgid "Invalid Post Type." msgstr "Tipo de publicación no válida." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:258 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:326 msgid "Enable Campuses" msgstr "Habilitar Campus" -#: src/TouchPoint-WP/TouchPointWP.php:1252 +#: src/TouchPoint-WP/Taxonomies.php:658 msgid "Classify posts by their general locations." msgstr "clasificar las publicaciones por sus ubicaciones generales." -#: src/TouchPoint-WP/TouchPointWP.php:1305 +#: src/TouchPoint-WP/Taxonomies.php:687 msgid "Classify posts by their church campus." msgstr "Clasifique las publicaciones por el campus." #. translators: %s: taxonomy name, singular -#: src/TouchPoint-WP/TouchPointWP.php:1357 +#: src/TouchPoint-WP/Taxonomies.php:718 msgid "Classify things by %s." msgstr "Clasifica las cosas por %s." -#: src/TouchPoint-WP/TouchPointWP.php:1444 +#: src/TouchPoint-WP/Taxonomies.php:749 msgid "Classify involvements by the day on which they meet." msgstr "Clasificar las participaciones por el día en que se reúnen." -#: src/TouchPoint-WP/TouchPointWP.php:1445 +#: src/TouchPoint-WP/Taxonomies.php:750 msgid "Weekdays" msgstr "Días de semana" -#: src/TouchPoint-WP/TouchPointWP.php:1482 +#: src/TouchPoint-WP/Taxonomies.php:776 msgid "Classify involvements by tense (present, future, past)" msgstr "Clasificar las implicaciones por tiempo (presente, futuro, pasado)" -#: src/TouchPoint-WP/TouchPointWP.php:1483 +#: src/TouchPoint-WP/Taxonomies.php:780 msgid "Tense" msgstr "Tiempo" -#: src/TouchPoint-WP/TouchPointWP.php:1483 +#: src/TouchPoint-WP/Taxonomies.php:780 msgid "Tenses" msgstr "Tiempos" -#: src/TouchPoint-WP/TouchPointWP.php:1526 +#: src/TouchPoint-WP/Taxonomies.php:803 msgid "Classify involvements by the portion of the day in which they meet." msgstr "Clasifique las participaciones por la parte del día en que se reúnen." -#: src/TouchPoint-WP/TouchPointWP.php:1527 +#: src/TouchPoint-WP/Taxonomies.php:809 msgid "Times of Day" msgstr "Tiempos del Día" -#: src/TouchPoint-WP/TouchPointWP.php:1580 +#: src/TouchPoint-WP/Taxonomies.php:835 msgid "Classify involvements and users by their age groups." msgstr "Clasifica las implicaciones y los usuarios por sus grupos de edad." -#: src/TouchPoint-WP/TouchPointWP.php:1581 +#: src/TouchPoint-WP/Taxonomies.php:838 msgid "Age Groups" msgstr "Grupos de Edad" -#: src/TouchPoint-WP/TouchPointWP.php:1619 +#: src/TouchPoint-WP/Taxonomies.php:864 msgid "Classify involvements by whether participants are mostly single or married." msgstr "Clasifique las participaciones según si los participantes son en su mayoría solteros o casados." -#: src/TouchPoint-WP/TouchPointWP.php:1620 +#: src/TouchPoint-WP/Taxonomies.php:870 msgid "Marital Statuses" msgstr "Estados Civiles" -#: src/TouchPoint-WP/TouchPointWP.php:1664 +#: src/TouchPoint-WP/Taxonomies.php:903 msgid "Classify Partners by category chosen in settings." msgstr "Clasifique a los ministeriales por categoría elegida en la configuración." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:259 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:327 msgid "Import campuses as a taxonomy. (You probably want to do this if you're multi-campus.)" msgstr "Importar campus como taxonomía. (Probablemente quieras hacer esto si tienes varios campus)." #. translators: %s: taxonomy name, plural -#: src/TouchPoint-WP/TouchPointWP.php:1225 +#: src/TouchPoint-WP/Taxonomies.php:51 msgid "Search %s" msgstr "Buscar %s" #. translators: %s: taxonomy name, plural -#: src/TouchPoint-WP/TouchPointWP.php:1227 +#: src/TouchPoint-WP/Taxonomies.php:53 msgid "All %s" msgstr "Todos los %s" #. translators: %s: taxonomy name, singular -#: src/TouchPoint-WP/TouchPointWP.php:1229 +#: src/TouchPoint-WP/Taxonomies.php:55 msgid "Edit %s" msgstr "Editar %s" #. translators: %s: taxonomy name, singular -#: src/TouchPoint-WP/TouchPointWP.php:1231 +#: src/TouchPoint-WP/Taxonomies.php:57 msgid "Update %s" msgstr "Actualizar %s" #. translators: %s: taxonomy name, singular -#: src/TouchPoint-WP/TouchPointWP.php:1233 +#: src/TouchPoint-WP/Taxonomies.php:59 msgid "Add New %s" msgstr "Agregar Nuevo %s" #. translators: %s: taxonomy name, singular -#: src/TouchPoint-WP/TouchPointWP.php:1235 +#: src/TouchPoint-WP/Taxonomies.php:61 msgid "New %s" msgstr "Nuevo %s" -#: src/TouchPoint-WP/Report.php:170 +#: src/TouchPoint-WP/Report.php:177 msgid "TouchPoint Reports" msgstr "Informes de TouchPoint" -#: src/TouchPoint-WP/Report.php:171 +#: src/TouchPoint-WP/Report.php:178 msgid "TouchPoint Report" msgstr "Informe de TouchPoint" #. translators: Last updated date/time for a report. %1$s is the date. %2$s is the time. -#: src/TouchPoint-WP/Report.php:301 +#: src/TouchPoint-WP/Report.php:417 msgid "Updated on %1$s at %2$s" msgstr "Actualizada %1$s %2$s" -#: src/TouchPoint-WP/TouchPointWP.php:274 +#: src/TouchPoint-WP/TouchPointWP.php:266 msgid "Every 15 minutes" msgstr "Cada 15 minutos" -#: src/TouchPoint-WP/Involvement.php:1420 +#: src/TouchPoint-WP/Involvement.php:1828 msgid "Language" msgstr "Idioma" -#: src/templates/admin/invKoForm.php:67 +#: src/templates/admin/invKoForm.php:91 msgid "Import Images from TouchPoint" msgstr "Importar imágenes desde TouchPoint" -#: src/templates/admin/invKoForm.php:71 +#: src/templates/admin/invKoForm.php:95 msgid "Importing images sometimes conflicts with other plugins. Disabling image imports can help." msgstr "La importación de imágenes a veces entra en conflicto con otros complementos. Deshabilitar las importaciones de imágenes puede ayudar." -#: src/TouchPoint-WP/Person.php:1404 +#: src/TouchPoint-WP/Person.php:1458 msgctxt "list of people, and *others*" msgid "others" msgstr "otros" -#: src/TouchPoint-WP/Involvement.php:3005 -#: src/TouchPoint-WP/Person.php:1720 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:850 +msgid "Meeting Calendars" +msgstr "Calendarios de Reuniones" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:851 +msgid "Import Meetings from TouchPoint to a calendar on your website." +msgstr "Importe reuniones desde TouchPoint a un calendario en su sitio web." + +#: src/templates/admin/invKoForm.php:102 +msgid "Import All Meetings to Calendar" +msgstr "Importe reuniones a los calendarios" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:879 +msgid "Meetings Slug" +msgstr "Slug de reuniones" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:880 +msgid "The root path for Meetings" +msgstr "La ruta raíz para las reuniones" + +#: src/TouchPoint-WP/Involvement.php:3951 +#: src/TouchPoint-WP/Person.php:1833 msgid "Contact Prohibited." msgstr "Contacto prohibido." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:449 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:546 msgid "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." msgstr "Al marcar esta casilla, la página de inicio de sesión de TouchPoint se convertirá en la predeterminada. Para evitar la redirección y llegar a la página de inicio de sesión estándar de WordPress, agregue 'tp_no_redirect' como parámetro de URL." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:292 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:362 msgid "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." msgstr "El dominio de los enlaces profundos de su aplicación móvil, sin https ni barras diagonales. Si no está utilizando la aplicación móvil personalizada, déjelo en blanco." -#: src/TouchPoint-WP/TouchPointWP_Settings.php:369 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:466 msgid "Once your settings on this page are set and saved, use this tool to generate the scripts needed for TouchPoint in a convenient installation package." msgstr "Una vez que haya configurado y guardado la configuración en esta página, utilice esta herramienta para generar los scripts necesarios para TouchPoint en un paquete de instalación conveniente." -#: assets/js/base-defer.js:1488 +#: assets/js/base-defer.js:1502 msgid "Something went wrong." msgstr "Algo salió mal." -#: src/TouchPoint-WP/Person.php:1609 +#: src/TouchPoint-WP/Person.php:1688 msgid "You may need to sign in." msgstr "Es posible que tengas que iniciar sesión." -#: src/TouchPoint-WP/Involvement.php:2995 -#: src/TouchPoint-WP/Person.php:1737 +#: src/TouchPoint-WP/Involvement.php:3941 +#: src/TouchPoint-WP/Person.php:1850 msgid "Contact Blocked for Spam." msgstr "Contacto bloqueado por spam." -#: src/TouchPoint-WP/Person.php:1541 +#: src/TouchPoint-WP/Person.php:1597 msgid "Registration Blocked for Spam." msgstr "Registro bloqueado por spam." + +#: src/templates/meeting-archive.php:27 +#: src/TouchPoint-WP/Meeting.php:269 +msgctxt "What Meetings should be called, plural." +msgid "Events" +msgstr "Eventos" + +#: src/TouchPoint-WP/Meeting.php:270 +msgctxt "What Meetings should be called, singular." +msgid "Event" +msgstr "Evento" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:293 +msgid "Enable Meeting Calendar" +msgstr "Habilitar calendario de reuniones" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:294 +msgid "Load Meetings from TouchPoint for a calendar, native in your website." +msgstr "Cargue reuniones desde TouchPoint para un calendario nativo en su sitio web." + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:892 +msgid "Days of Future" +msgstr "Días del futuro" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:893 +msgid "Meetings more than this many days in the future will not be imported." +msgstr "No se importarán reuniones que superen estos días en el futuro." + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:905 +msgid "Archive After Days" +msgstr "Archivo después de días" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:918 +msgid "Days of History" +msgstr "Días de la Historia" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:989 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1120 +msgid "Post Types" +msgstr "Tipos de publicaciones" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:990 +msgid "Select post types which should have Divisions available as a native taxonomy." +msgstr "Seleccione los tipos de publicaciones que deberían tener Divisiones disponibles como taxonomía nativa." + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1121 +msgid "Select post types which should have Resident Codes available as a native taxonomy." +msgstr "Seleccione los tipos de publicaciones que deberían tener códigos de residente disponibles como taxonomía nativa." + +#: src/TouchPoint-WP/Utilities/DateFormats.php:119 +#: src/TouchPoint-WP/Utilities/DateFormats.php:194 +msgid "Tonight" +msgstr "este noche" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:121 +#: src/TouchPoint-WP/Utilities/DateFormats.php:196 +msgid "Today" +msgstr "hoy" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:128 +#: src/TouchPoint-WP/Utilities/DateFormats.php:203 +msgid "Tomorrow" +msgstr "mañana" + +#: src/templates/admin/invKoForm.php:405 +msgid "(named person)" +msgstr "(persona nombrada)" + +#: src/TouchPoint-WP/Utilities.php:497 +msgid "Expand" +msgstr "Ampliar" + +#: src/TouchPoint-WP/Meeting.php:549 +msgid "Cancelled" +msgstr "Cancelado" + +#: src/TouchPoint-WP/Meeting.php:550 +msgid "Scheduled" +msgstr "Programado" + +#. translators: %1$s is "Monday". %2$s is "January 1". +#: src/TouchPoint-WP/Utilities/DateFormats.php:147 +msgctxt "Date format string" +msgid "Last %1$s, %2$s" +msgstr "el pasado %1$s %2$s" + +#. translators: %1$s is "Monday". %2$s is "January 1". +#: src/TouchPoint-WP/Utilities/DateFormats.php:153 +msgctxt "Date format string" +msgid "This %1$s, %2$s" +msgstr "este %1$s %2$s" + +#. translators: %1$s is "Monday". %2$s is "January 1". +#: src/TouchPoint-WP/Utilities/DateFormats.php:159 +msgctxt "Date format string" +msgid "Next %1$s, %2$s" +msgstr "el proximo %1$s %2$s" + +#. translators: %1$s is "Monday". %2$s is "January 1". +#: src/TouchPoint-WP/Utilities/DateFormats.php:164 +msgctxt "Date format string" +msgid "%1$s, %2$s" +msgstr "%1$s %2$s" + +#. Translators: %s is the singular name of the of a Meeting, such as "Event". +#: src/TouchPoint-WP/CalendarGrid.php:189 +msgid "%s is cancelled." +msgstr "%s esta cancelado." + +#. Translators: %s is the system name. "TouchPoint" by default. +#: src/TouchPoint-WP/Involvement.php:3665 +msgid "Involvement in %s" +msgstr "Participaciones en %s" + +#: src/TouchPoint-WP/Meeting.php:422 +msgid "In the Past" +msgstr "en el pasado" + +#. Translators: %s is the system name. "TouchPoint" by default. +#: src/TouchPoint-WP/Meeting.php:494 +msgid "Meeting in %s" +msgstr "Reunión en %s" + +#. Translators: %s is the system name. "TouchPoint" by default. +#: src/TouchPoint-WP/Person.php:1200 +msgid "Person in %s" +msgstr "Persona en %s" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:855 +msgid "Meeting Name (Plural)" +msgstr "Nombre de las reuniones (singular)" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:856 +msgid "What you call Meetings at your church" +msgstr "Lo que llamas Reuniones en tu iglesia" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:861 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:863 +msgid "Meetings" +msgstr "Reuniones" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:861 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:863 +msgid "Events" +msgstr "Eventos" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:867 +msgid "Meeting Name (Singular)" +msgstr "Nombre de la reunión (singular)" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:868 +msgid "What you call a Meeting at your church" +msgstr "Cómo se llama una Reunión en tu iglesia" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:873 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:875 +msgid "Meeting" +msgstr "Reunión" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:873 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:875 +msgid "Event" +msgstr "Evento" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:137 +msgctxt "Date string for day of the week, when the year is current." +msgid "l" +msgstr "l" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:138 +msgctxt "Date string when the year is current." +msgid "F j" +msgstr "j F" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:140 +msgctxt "Date string for day of the week, when the year is not current." +msgid "l" +msgstr "l" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:141 +msgctxt "Date string when the year is not current." +msgid "F j, Y" +msgstr "j F Y" + +#: src/TouchPoint-WP/Meeting.php:551 +msgctxt "Event Status is not a recognized value." +msgid "Unknown" +msgstr "desconocido" + +#: src/TouchPoint-WP/Involvement.php:136 +msgid "Creating an Involvement object from an object without a post_id is not yet supported." +msgstr "Aún no se admite la creación de un objeto de participación a partir de un objeto sin post_id." + +#. translators: "Mon All Day" or "Sundays All Day" +#: src/TouchPoint-WP/Involvement.php:998 +#: src/TouchPoint-WP/Involvement.php:1021 +msgid "%1$s All Day" +msgstr "todo el dia los %1$s" + +#: src/TouchPoint-WP/Meeting.php:94 +msgid "Creating a Meeting object from an object without a post_id is not yet supported." +msgstr "Aún no se admite la creación de un objeto de reunión a partir de un objeto sin post_id." + +#. translators: %1$s is the start date/time, %2$s is the end date/time. +#: src/TouchPoint-WP/Utilities/DateFormats.php:89 +#: src/TouchPoint-WP/Utilities/DateFormats.php:331 +msgid "%1$s – %2$s" +msgstr "%1$s – %2$s" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:211 +msgctxt "Short date string for day of the week, when the year is current." +msgid "D" +msgstr "D" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:212 +msgctxt "Short date string when the year is current." +msgid "M j" +msgstr "j M" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:214 +msgctxt "Short date string for day of the week, when the year is not current." +msgid "D" +msgstr "D" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:215 +msgctxt "Short date string when the year is not current." +msgid "M j, Y" +msgstr "j M Y" + +#. translators: %1$s is the start date, %2$s start time, %3$s is the end date, and %4$s end time. +#: src/TouchPoint-WP/Utilities/DateFormats.php:364 +msgid "%1$s at %2$s – %3$s at %4$s" +msgstr "%1$s a %2$s – %3$s a %4$s" + +#. translators: %1$s is "Mon". %2$s is "Jan 1". +#: src/TouchPoint-WP/Utilities/DateFormats.php:221 +msgctxt "Short date format string" +msgid "Last %1$s, %2$s" +msgstr "el pasado %1$s %2$s" + +#. translators: %1$s is "Mon". %2$s is "Jan 1". +#: src/TouchPoint-WP/Utilities/DateFormats.php:227 +msgctxt "Short date format string" +msgid "This %1$s, %2$s" +msgstr "este %1$s %2$s" + +#. translators: %1$s is "Mon". %2$s is "Jan 1". +#: src/TouchPoint-WP/Utilities/DateFormats.php:233 +msgctxt "Short date format string" +msgid "Next %1$s, %2$s" +msgstr "proximo %1$s %2$s" + +#. translators: %1$s is "Mon". %2$s is "Jan 1". +#: src/TouchPoint-WP/Utilities/DateFormats.php:238 +msgctxt "Short date format string" +msgid "%1$s, %2$s" +msgstr "%1$s %2$s" + +#. Translators: %s is the plural name of the of the Meetings, such as "Events". +#: src/TouchPoint-WP/CalendarGrid.php:269 +msgid "There are no %s published for this month." +msgstr "No hay %s publicados para este mes." + +#: src/templates/admin/locationsKoForm.php:43 +msgid "Radius (miles)" +msgstr "Radio (millas)" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:791 +msgid "Events Calendar plugin by Modern Tribe" +msgstr "Complemento de calendario de eventos de Modern Tribe" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:795 +msgid "TouchPoint Meetings" +msgstr "reuniones de TouchPoint" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:801 +msgid "App 2.0 Calendar" +msgstr "Calendario de la app 2.0" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:806 +msgid "Events Provider" +msgstr "Proveedor de eventos" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:807 +msgid "The source of events for version 2.0 of the Custom Mobile App." +msgstr "El origen de los eventos para la versión 2.0 de la aplicación móvil personalizada." + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:820 +msgid "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:" +msgstr "Para usar sus eventos del Calendario de eventos en la aplicación móvil personalizada, configure el Proveedor en Wordpress Plugin - Modern Tribe (independientemente del proveedor que esté utilizando anteriormente) y use esta URL:" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:802 +msgid "Integrate Custom Mobile app version 2.0 with The Events Calendar from Modern Tribe." +msgstr "Integre la versión 2.0 de la aplicación móvil personalizada con el calendario de eventos de Modern Tribe." + +#. Translators: %s is the singular name of the of a Meeting, such as "Event". +#: src/templates/involvement-single.php:49 +msgid "This %s has been Cancelled." +msgstr "Este %s ha sido Cancelado." + +#: src/templates/admin/invKoForm.php:197 +msgid "Division" +msgstr "Division" + +#: src/templates/admin/invKoForm.php:204 +msgid "Resident Code" +msgstr "Código de Residente" + +#: src/templates/admin/invKoForm.php:211 +msgid "Campus" +msgstr "Campus" + +#: src/TouchPoint-WP/Involvement.php:1022 +msgid "All Day" +msgstr "todo el dia" + +#: src/TouchPoint-WP/Utilities.php:291 +msgctxt "list of items, and *others*" +msgid "others" +msgstr "otros" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:433 +msgid "ipapi.co API Key" +msgstr "Clave API de ipapi.co" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:434 +msgid "Optional. Allows for geolocation of user IP addresses. This generally will work without a key, but may be rate limited." +msgstr "Opcional. Permite la geolocalización de las direcciones IP de los usuarios. Por lo general, esto funcionará sin una clave, pero puede tener una frecuencia limitada." + +#: src/templates/admin/invKoForm.php:60 +msgid "Import Campuses" +msgstr "Importar Campus" + +#: src/templates/admin/invKoForm.php:67 +msgid "All Campuses" +msgstr "Todos los campus" + +#: src/templates/admin/invKoForm.php:71 +msgid "(No Campus)" +msgstr "(Sin campus)" + +#: src/TouchPoint-WP/TouchPointWP_Widget.php:39 +msgid "TouchPoint-WP Status" +msgstr "Estado de TouchPoint-WP" + +#: src/TouchPoint-WP/TouchPointWP_Widget.php:96 +msgid "Imported" +msgstr "Importadas" + +#: src/TouchPoint-WP/TouchPointWP_Widget.php:97 +msgid "Last Updated" +msgstr "Actualizada" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:130 +#: src/TouchPoint-WP/Utilities/DateFormats.php:205 +msgid "Yesterday" +msgstr "Ayer" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:906 +msgid "Meetings more than this many days in the past will no longer update from TouchPoint, allowing you to keep some historical event information on the calendar for reference, even if you reuse and update the information in the Involvement." +msgstr "Las reuniones con más de esta cantidad de días en el pasado ya no se actualizarán desde TouchPoint, lo que le permitirá conservar información histórica de eventos en el calendario para referencia, incluso si reutiliza y actualiza la información en Participación." + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:919 +msgid "Meetings will be kept on the calendar until the event is this many days in the past. Once an event is older than this, it'll be deleted." +msgstr "Las reuniones se mantendrán en el calendario hasta que el evento tenga esta cantidad de días en el pasado. Una vez que un evento tenga más de esta cantidad de días, se eliminará." + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:444 +msgid "List Site in Directory" +msgstr "Listar sitio en directorio" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:445 +msgid "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." +msgstr "Permita que los desarrolladores de TouchPoint-WP incluyan públicamente su sitio o iglesia como sitios que usan TouchPoint-WP. Esto ayuda a otras iglesias potenciales a ver lo que se puede hacer al combinar WordPress con el mejor ChMS del planeta. Solo se aplica si este sitio es accesible en Internet público." diff --git a/i18n/TouchPoint-WP.pot b/i18n/TouchPoint-WP.pot index a748c21d..92ee09b7 100644 --- a/i18n/TouchPoint-WP.pot +++ b/i18n/TouchPoint-WP.pot @@ -2,197 +2,236 @@ # This file is distributed under the AGPLv3+. msgid "" msgstr "" -"Project-Id-Version: TouchPoint WP 0.0.37\n" +"Project-Id-Version: TouchPoint WP 0.0.95\n" "Report-Msgid-Bugs-To: https://wordpress.org/support/plugin/TouchPoint-WP\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -"POT-Creation-Date: 2024-02-07T14:00:10+00:00\n" +"POT-Creation-Date: 2024-11-28T19:12:52+00:00\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" -"X-Generator: WP-CLI 2.8.1\n" +"X-Generator: WP-CLI 2.11.0\n" "X-Domain: TouchPoint-WP\n" #. Plugin Name of the plugin +#: touchpoint-wp.php msgid "TouchPoint WP" msgstr "" #. Plugin URI of the plugin +#: touchpoint-wp.php msgid "https://github.com/tenthpres/touchpoint-wp" msgstr "" #. Description of the plugin +#: touchpoint-wp.php msgid "A WordPress Plugin for integrating with TouchPoint Church Management Software." msgstr "" #. Author of the plugin +#: touchpoint-wp.php msgid "James K" msgstr "" #. Author URI of the plugin +#: touchpoint-wp.php msgid "https://github.com/jkrrv" msgstr "" -#: src/templates/admin/invKoForm.php:17 +#: src/templates/admin/invKoForm.php:18 #: src/templates/admin/locationsKoForm.php:13 -#: src/templates/admin/locationsKoForm.php:50 +#: src/templates/admin/locationsKoForm.php:58 msgid "Delete" msgstr "" -#: src/templates/admin/invKoForm.php:23 +#: src/templates/admin/invKoForm.php:24 msgid "Singular Name" msgstr "" -#: src/templates/admin/invKoForm.php:31 +#: src/templates/admin/invKoForm.php:32 msgid "Plural Name" msgstr "" -#: src/templates/admin/invKoForm.php:39 +#: src/templates/admin/invKoForm.php:40 msgid "Slug" msgstr "" -#: src/templates/admin/invKoForm.php:47 -#: src/TouchPoint-WP/TouchPointWP_Settings.php:765 +#: src/templates/admin/invKoForm.php:48 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:977 msgid "Divisions to Import" msgstr "" #: src/templates/admin/invKoForm.php:60 -msgid "Import Hierarchically (Parent-Child Relationships)" +msgid "Import Campuses" +msgstr "" + +#: src/templates/admin/invKoForm.php:63 +#: src/templates/admin/invKoForm.php:149 +#: src/templates/admin/invKoForm.php:165 +#: src/templates/admin/invKoForm.php:318 +#: src/templates/parts/involvement-nearby-list.php:2 +#: src/TouchPoint-WP/Meeting.php:746 +#: src/TouchPoint-WP/Rsvp.php:75 +#: assets/js/base-defer.js:192 +#: assets/js/base-defer.js:1133 +msgid "Loading..." msgstr "" #: src/templates/admin/invKoForm.php:67 -msgid "Import Images from TouchPoint" +msgid "All Campuses" msgstr "" #: src/templates/admin/invKoForm.php:71 +msgid "(No Campus)" +msgstr "" + +#: src/templates/admin/invKoForm.php:84 +msgid "Import Hierarchically (Parent-Child Relationships)" +msgstr "" + +#: src/templates/admin/invKoForm.php:91 +msgid "Import Images from TouchPoint" +msgstr "" + +#: src/templates/admin/invKoForm.php:95 msgid "Importing images sometimes conflicts with other plugins. Disabling image imports can help." msgstr "" -#: src/templates/admin/invKoForm.php:77 +#: src/templates/admin/invKoForm.php:102 +msgid "Import All Meetings to Calendar" +msgstr "" + +#: src/templates/admin/invKoForm.php:110 msgid "Use Geographic Location" msgstr "" -#: src/templates/admin/invKoForm.php:83 +#: src/templates/admin/invKoForm.php:116 msgid "Exclude Involvements if" msgstr "" -#: src/templates/admin/invKoForm.php:87 +#: src/templates/admin/invKoForm.php:120 msgid "Involvement is Closed" msgstr "" -#: src/templates/admin/invKoForm.php:91 +#: src/templates/admin/invKoForm.php:124 msgid "Involvement is a Child Involvement" msgstr "" -#: src/templates/admin/invKoForm.php:95 +#: src/templates/admin/invKoForm.php:128 msgid "Based on Involvement setting in TouchPoint" msgstr "" -#: src/templates/admin/invKoForm.php:95 +#: src/templates/admin/invKoForm.php:128 msgid "Involvement does not meet weekly" msgstr "" -#: src/templates/admin/invKoForm.php:99 +#: src/templates/admin/invKoForm.php:132 msgid "Involvement does not have a Schedule" msgstr "" -#: src/templates/admin/invKoForm.php:103 +#: src/templates/admin/invKoForm.php:136 msgid "Involvement has a registration type of \"No Online Registration\"" msgstr "" -#: src/templates/admin/invKoForm.php:107 +#: src/templates/admin/invKoForm.php:140 msgid "Involvement registration has ended (end date is past)" msgstr "" -#: src/templates/admin/invKoForm.php:113 +#: src/templates/admin/invKoForm.php:146 msgid "Leader Member Types" msgstr "" -#: src/templates/admin/invKoForm.php:116 -#: src/templates/admin/invKoForm.php:132 -#: src/templates/admin/invKoForm.php:258 -#: src/templates/parts/involvement-nearby-list.php:2 -#: src/TouchPoint-WP/Rsvp.php:77 -#: assets/js/base-defer.js:188 -#: assets/js/base-defer.js:1119 -msgid "Loading..." -msgstr "" - -#: src/templates/admin/invKoForm.php:128 +#: src/templates/admin/invKoForm.php:161 msgid "Host Member Types" msgstr "" -#: src/templates/admin/invKoForm.php:144 +#: src/templates/admin/invKoForm.php:177 msgid "Default Grouping" msgstr "" -#: src/templates/admin/invKoForm.php:148 +#: src/templates/admin/invKoForm.php:181 msgid "No Grouping" msgstr "" -#: src/templates/admin/invKoForm.php:149 +#: src/templates/admin/invKoForm.php:182 msgid "Upcoming / Current" msgstr "" -#: src/templates/admin/invKoForm.php:150 +#: src/templates/admin/invKoForm.php:183 msgid "Current / Upcoming" msgstr "" -#: src/templates/admin/invKoForm.php:156 +#: src/templates/admin/invKoForm.php:191 msgid "Default Filters" msgstr "" -#: src/templates/admin/invKoForm.php:165 +#: src/templates/admin/invKoForm.php:197 +msgid "Division" +msgstr "" + +#: src/templates/admin/invKoForm.php:204 +msgid "Resident Code" +msgstr "" + +#: src/templates/admin/invKoForm.php:211 +msgid "Campus" +msgstr "" + +#: src/templates/admin/invKoForm.php:223 msgid "Gender" msgstr "" -#: src/templates/admin/invKoForm.php:179 -#: src/TouchPoint-WP/Involvement.php:1445 -#: src/TouchPoint-WP/TouchPointWP.php:1445 +#: src/templates/admin/invKoForm.php:237 +#: src/TouchPoint-WP/Involvement.php:1853 +#: src/TouchPoint-WP/Taxonomies.php:750 msgid "Weekday" msgstr "" -#: src/templates/admin/invKoForm.php:183 -#: src/TouchPoint-WP/Involvement.php:1471 -#: src/TouchPoint-WP/TouchPointWP.php:1527 +#: src/templates/admin/invKoForm.php:241 +#: src/TouchPoint-WP/Involvement.php:1879 +#: src/TouchPoint-WP/Taxonomies.php:808 msgid "Time of Day" msgstr "" -#: src/templates/admin/invKoForm.php:187 +#: src/templates/admin/invKoForm.php:245 msgid "Prevailing Marital Status" msgstr "" -#: src/templates/admin/invKoForm.php:191 -#: src/TouchPoint-WP/TouchPointWP.php:1581 +#: src/templates/admin/invKoForm.php:249 +#: src/TouchPoint-WP/Taxonomies.php:837 msgid "Age Group" msgstr "" -#: src/templates/admin/invKoForm.php:196 +#: src/templates/admin/invKoForm.php:254 msgid "Task Owner" msgstr "" -#: src/templates/admin/invKoForm.php:203 +#: src/templates/admin/invKoForm.php:261 msgid "Contact Leader Task Keywords" msgstr "" -#: src/templates/admin/invKoForm.php:214 +#: src/templates/admin/invKoForm.php:272 msgid "Join Task Keywords" msgstr "" -#: src/templates/admin/invKoForm.php:230 +#: src/templates/admin/invKoForm.php:288 msgid "Add Involvement Post Type" msgstr "" -#: src/templates/admin/invKoForm.php:237 +#: src/templates/admin/invKoForm.php:295 msgid "Small Group" msgstr "" -#: src/templates/admin/invKoForm.php:238 +#: src/templates/admin/invKoForm.php:296 msgid "Small Groups" msgstr "" -#: src/templates/admin/invKoForm.php:368 +#: src/templates/admin/invKoForm.php:405 +msgid "(named person)" +msgstr "" + +#: src/templates/admin/invKoForm.php:442 msgid "Select..." msgstr "" @@ -209,28 +248,43 @@ msgid "Longitude" msgstr "" #: src/templates/admin/locationsKoForm.php:43 +msgid "Radius (miles)" +msgstr "" + +#: src/templates/admin/locationsKoForm.php:51 msgid "Static IP Addresses" msgstr "" -#: src/templates/admin/locationsKoForm.php:46 +#: src/templates/admin/locationsKoForm.php:54 msgid "If this Location has an internet connection with Static IP Addresses, you can put those addresses here so users are automatically identified with this location." msgstr "" -#: src/templates/admin/locationsKoForm.php:54 +#: src/templates/admin/locationsKoForm.php:62 msgid "Add IP Address" msgstr "" -#: src/templates/admin/locationsKoForm.php:64 +#: src/templates/admin/locationsKoForm.php:72 msgid "Add Location" msgstr "" -#: src/templates/admin/locationsKoForm.php:71 +#: src/templates/admin/locationsKoForm.php:79 msgid "The Campus" msgstr "" +#. Translators: %s is the singular name of the of a Meeting, such as "Event". +#: src/templates/involvement-single.php:49 +msgid "This %s has been Cancelled." +msgstr "" + +#: src/templates/meeting-archive.php:27 +#: src/TouchPoint-WP/Meeting.php:269 +msgctxt "What Meetings should be called, plural." +msgid "Events" +msgstr "" + #. translators: %s will be the plural post type (e.g. Small Groups) #: src/templates/parts/involvement-list-none.php:16 -#: src/TouchPoint-WP/Involvement.php:1682 +#: src/TouchPoint-WP/Involvement.php:2099 msgid "No %s Found." msgstr "" @@ -241,1189 +295,1532 @@ msgstr "" #. translators: number of miles #: src/templates/parts/involvement-nearby-list.php:10 -#: src/TouchPoint-WP/Involvement.php:2803 +#: src/TouchPoint-WP/Involvement.php:3577 msgctxt "miles. Unit is appended to a number. %2.1f is the number, so %2.1fmi looks like '12.3mi'" msgid "%2.1fmi" msgstr "" #. translators: %s is "what you call TouchPoint at your church", which is a setting -#: src/TouchPoint-WP/Auth.php:140 +#: src/TouchPoint-WP/Auth.php:142 msgid "Sign in with your %s account" msgstr "" -#: src/TouchPoint-WP/Auth.php:402 +#: src/TouchPoint-WP/Auth.php:410 msgid "Your login token expired." msgstr "" -#: src/TouchPoint-WP/Auth.php:417 +#: src/TouchPoint-WP/Auth.php:425 msgid "Your login token is invalid." msgstr "" -#: src/TouchPoint-WP/Auth.php:429 +#: src/TouchPoint-WP/Auth.php:437 msgid "Session could not be validated." msgstr "" -#: src/TouchPoint-WP/EventsCalendar.php:59 +#. Translators: %s is the singular name of the of a Meeting, such as "Event". +#: src/TouchPoint-WP/CalendarGrid.php:189 +msgid "%s is cancelled." +msgstr "" + +#. Translators: %s is the plural name of the of the Meetings, such as "Events". +#: src/TouchPoint-WP/CalendarGrid.php:269 +msgid "There are no %s published for this month." +msgstr "" + +#: src/TouchPoint-WP/EventsCalendar.php:77 msgid "Recurring" msgstr "" -#: src/TouchPoint-WP/EventsCalendar.php:62 +#: src/TouchPoint-WP/EventsCalendar.php:80 +#: src/TouchPoint-WP/EventsCalendar.php:297 msgid "Multi-Day" msgstr "" -#: src/TouchPoint-WP/Involvement.php:441 +#: src/TouchPoint-WP/Involvement.php:136 +msgid "Creating an Involvement object from an object without a post_id is not yet supported." +msgstr "" + +#: src/TouchPoint-WP/Involvement.php:495 msgid "Currently Full" msgstr "" -#: src/TouchPoint-WP/Involvement.php:445 +#: src/TouchPoint-WP/Involvement.php:500 msgid "Currently Closed" msgstr "" -#: src/TouchPoint-WP/Involvement.php:451 +#: src/TouchPoint-WP/Involvement.php:507 msgid "Registration Not Open Yet" msgstr "" -#: src/TouchPoint-WP/Involvement.php:456 +#: src/TouchPoint-WP/Involvement.php:513 msgid "Registration Closed" msgstr "" -#. translators: "Mon at 7pm" or "Sundays at 9am & 11am" -#: src/TouchPoint-WP/Involvement.php:712 -#: src/TouchPoint-WP/Involvement.php:732 +#. translators: %1$s is the date(s), %2$s is the time(s). +#: src/TouchPoint-WP/Involvement.php:995 +#: src/TouchPoint-WP/Involvement.php:1027 +#: src/TouchPoint-WP/Involvement.php:1120 +#: src/TouchPoint-WP/Involvement.php:1144 +#: src/TouchPoint-WP/TouchPointWP_Widget.php:71 +#: src/TouchPoint-WP/Utilities/DateFormats.php:288 +#: src/TouchPoint-WP/Utilities/DateFormats.php:352 msgid "%1$s at %2$s" msgstr "" +#. translators: "Mon All Day" or "Sundays All Day" +#: src/TouchPoint-WP/Involvement.php:998 +#: src/TouchPoint-WP/Involvement.php:1021 +msgid "%1$s All Day" +msgstr "" + +#: src/TouchPoint-WP/Involvement.php:1022 +msgid "All Day" +msgstr "" + #. translators: {start date} through {end date} e.g. February 14 through August 12 -#: src/TouchPoint-WP/Involvement.php:741 +#: src/TouchPoint-WP/Involvement.php:1036 msgid "%1$s through %2$s" msgstr "" #. translators: {schedule}, {start date} through {end date} e.g. Sundays at 11am, February 14 through August 12 -#: src/TouchPoint-WP/Involvement.php:748 +#: src/TouchPoint-WP/Involvement.php:1045 msgid "%1$s, %2$s through %3$s" msgstr "" #. translators: Starts {start date} e.g. Starts September 15 -#: src/TouchPoint-WP/Involvement.php:758 +#: src/TouchPoint-WP/Involvement.php:1054 msgid "Starts %1$s" msgstr "" #. translators: {schedule}, starting {start date} e.g. Sundays at 11am, starting February 14 -#: src/TouchPoint-WP/Involvement.php:764 +#: src/TouchPoint-WP/Involvement.php:1062 msgid "%1$s, starting %2$s" msgstr "" #. translators: Through {end date} e.g. Through September 15 -#: src/TouchPoint-WP/Involvement.php:773 +#: src/TouchPoint-WP/Involvement.php:1070 msgid "Through %1$s" msgstr "" #. translators: {schedule}, through {end date} e.g. Sundays at 11am, through February 14 -#: src/TouchPoint-WP/Involvement.php:779 +#: src/TouchPoint-WP/Involvement.php:1078 msgid "%1$s, through %2$s" msgstr "" -#: src/TouchPoint-WP/Involvement.php:1318 -#: src/TouchPoint-WP/Partner.php:749 +#: src/TouchPoint-WP/Involvement.php:1728 +#: src/TouchPoint-WP/Partner.php:814 msgid "Any" msgstr "" -#: src/TouchPoint-WP/Involvement.php:1377 +#: src/TouchPoint-WP/Involvement.php:1785 msgid "Genders" msgstr "" -#: src/TouchPoint-WP/Involvement.php:1420 +#: src/TouchPoint-WP/Involvement.php:1828 msgid "Language" msgstr "" -#: src/TouchPoint-WP/Involvement.php:1493 -#: src/TouchPoint-WP/TouchPointWP.php:1620 +#: src/TouchPoint-WP/Involvement.php:1901 +#: src/TouchPoint-WP/Taxonomies.php:869 msgid "Marital Status" msgstr "" -#: src/TouchPoint-WP/Involvement.php:1494 +#: src/TouchPoint-WP/Involvement.php:1902 msgctxt "Marital status for a group of people" msgid "Mostly Single" msgstr "" -#: src/TouchPoint-WP/Involvement.php:1495 +#: src/TouchPoint-WP/Involvement.php:1903 msgctxt "Marital status for a group of people" msgid "Mostly Married" msgstr "" -#: src/TouchPoint-WP/Involvement.php:1506 +#: src/TouchPoint-WP/Involvement.php:1914 msgid "Age" msgstr "" #. translators: %s is for the user-provided term for the items on the map (e.g. Small Group or Partner) -#: src/TouchPoint-WP/Involvement.php:1528 -#: src/TouchPoint-WP/Partner.php:771 +#: src/TouchPoint-WP/Involvement.php:1936 +#: src/TouchPoint-WP/Partner.php:838 msgid "The %s listed are only those shown on the map." msgstr "" #. translators: %s is the link to "reset the map" -#: src/TouchPoint-WP/Involvement.php:1536 -#: src/TouchPoint-WP/Partner.php:787 +#: src/TouchPoint-WP/Involvement.php:1944 +#: src/TouchPoint-WP/Partner.php:854 msgid "Zoom out or %s to see more." msgstr "" -#: src/TouchPoint-WP/Involvement.php:1539 -#: src/TouchPoint-WP/Partner.php:790 +#: src/TouchPoint-WP/Involvement.php:1947 +#: src/TouchPoint-WP/Partner.php:857 msgctxt "Zoom out or reset the map to see more." msgid "reset the map" msgstr "" -#: src/TouchPoint-WP/Involvement.php:1628 +#: src/TouchPoint-WP/Involvement.php:2040 msgid "This involvement type doesn't exist." msgstr "" -#: src/TouchPoint-WP/Involvement.php:1638 +#: src/TouchPoint-WP/Involvement.php:2050 msgid "This involvement type doesn't have geographic locations enabled." msgstr "" -#: src/TouchPoint-WP/Involvement.php:1657 +#: src/TouchPoint-WP/Involvement.php:2069 msgid "Could not locate." msgstr "" -#: src/TouchPoint-WP/Involvement.php:2776 +#: src/TouchPoint-WP/Involvement.php:3556 msgid "Men Only" msgstr "" -#: src/TouchPoint-WP/Involvement.php:2779 +#: src/TouchPoint-WP/Involvement.php:3559 msgid "Women Only" msgstr "" -#: src/TouchPoint-WP/Involvement.php:2842 +#: src/TouchPoint-WP/Involvement.php:3636 msgid "Contact Leaders" msgstr "" -#: src/TouchPoint-WP/Involvement.php:2850 +#: src/TouchPoint-WP/Involvement.php:3653 +#: src/TouchPoint-WP/Partner.php:1318 +msgid "Show on Map" +msgstr "" + +#. Translators: %s is the system name. "TouchPoint" by default. +#: src/TouchPoint-WP/Involvement.php:3665 +msgid "Involvement in %s" +msgstr "" + +#: src/TouchPoint-WP/Involvement.php:3706 +#: src/TouchPoint-WP/Involvement.php:3765 msgid "Register" msgstr "" -#: src/TouchPoint-WP/Involvement.php:2855 +#: src/TouchPoint-WP/Involvement.php:3712 msgid "Create Account" msgstr "" -#: src/TouchPoint-WP/Involvement.php:2859 +#: src/TouchPoint-WP/Involvement.php:3716 msgid "Schedule" msgstr "" -#: src/TouchPoint-WP/Involvement.php:2864 +#: src/TouchPoint-WP/Involvement.php:3721 msgid "Give" msgstr "" -#: src/TouchPoint-WP/Involvement.php:2867 +#: src/TouchPoint-WP/Involvement.php:3724 msgid "Manage Subscriptions" msgstr "" -#: src/TouchPoint-WP/Involvement.php:2870 +#: src/TouchPoint-WP/Involvement.php:3727 msgid "Record Attendance" msgstr "" -#: src/TouchPoint-WP/Involvement.php:2873 +#: src/TouchPoint-WP/Involvement.php:3730 msgid "Get Tickets" msgstr "" -#: src/TouchPoint-WP/Involvement.php:2880 -#: assets/js/base-defer.js:987 +#: src/TouchPoint-WP/Involvement.php:3756 +#: assets/js/base-defer.js:1001 msgid "Join" msgstr "" -#: src/TouchPoint-WP/Involvement.php:2889 -#: src/TouchPoint-WP/Partner.php:1227 -msgid "Show on Map" -msgstr "" - -#: src/TouchPoint-WP/Involvement.php:2941 -#: src/TouchPoint-WP/Involvement.php:3018 +#: src/TouchPoint-WP/Involvement.php:3861 +#: src/TouchPoint-WP/Involvement.php:3964 msgid "Invalid Post Type." msgstr "" -#: src/TouchPoint-WP/Involvement.php:2995 -#: src/TouchPoint-WP/Person.php:1737 +#: src/TouchPoint-WP/Involvement.php:3941 +#: src/TouchPoint-WP/Person.php:1850 msgid "Contact Blocked for Spam." msgstr "" -#: src/TouchPoint-WP/Involvement.php:3005 -#: src/TouchPoint-WP/Person.php:1720 +#: src/TouchPoint-WP/Involvement.php:3951 +#: src/TouchPoint-WP/Person.php:1833 msgid "Contact Prohibited." msgstr "" -#: src/TouchPoint-WP/Meeting.php:91 -#: src/TouchPoint-WP/TouchPointWP.php:940 +#: src/TouchPoint-WP/Meeting.php:94 +msgid "Creating a Meeting object from an object without a post_id is not yet supported." +msgstr "" + +#: src/TouchPoint-WP/Meeting.php:270 +msgctxt "What Meetings should be called, singular." +msgid "Event" +msgstr "" + +#: src/TouchPoint-WP/Meeting.php:422 +msgid "In the Past" +msgstr "" + +#. Translators: %s is the system name. "TouchPoint" by default. +#: src/TouchPoint-WP/Meeting.php:494 +msgid "Meeting in %s" +msgstr "" + +#: src/TouchPoint-WP/Meeting.php:549 +msgid "Cancelled" +msgstr "" + +#: src/TouchPoint-WP/Meeting.php:550 +msgid "Scheduled" +msgstr "" + +#: src/TouchPoint-WP/Meeting.php:551 +msgctxt "Event Status is not a recognized value." +msgid "Unknown" +msgstr "" + +#: src/TouchPoint-WP/Meeting.php:672 +#: src/TouchPoint-WP/TouchPointWP.php:1033 msgid "Only GET requests are allowed." msgstr "" -#: src/TouchPoint-WP/Meeting.php:119 -#: src/TouchPoint-WP/TouchPointWP.php:369 +#: src/TouchPoint-WP/Meeting.php:700 +#: src/TouchPoint-WP/TouchPointWP.php:362 msgid "Only POST requests are allowed." msgstr "" -#: src/TouchPoint-WP/Meeting.php:129 -#: src/TouchPoint-WP/TouchPointWP.php:378 +#: src/TouchPoint-WP/Meeting.php:710 +#: src/TouchPoint-WP/TouchPointWP.php:371 msgid "Invalid data provided." msgstr "" +#: src/TouchPoint-WP/Meeting.php:745 +#: src/TouchPoint-WP/Meeting.php:766 +#: src/TouchPoint-WP/Rsvp.php:80 +msgid "RSVP" +msgstr "" + #. translators: %s is for the user-provided "Global Partner" and "Secure Partner" terms. -#: src/TouchPoint-WP/Partner.php:778 +#: src/TouchPoint-WP/Partner.php:845 msgid "The %1$s listed are only those shown on the map, as well as all %2$s." msgstr "" -#: src/TouchPoint-WP/Partner.php:1184 +#: src/TouchPoint-WP/Partner.php:1259 msgid "Not Shown on Map" msgstr "" -#: src/TouchPoint-WP/Person.php:149 +#: src/TouchPoint-WP/Person.php:150 msgid "No WordPress User ID provided for initializing a person object." msgstr "" -#: src/TouchPoint-WP/Person.php:641 +#: src/TouchPoint-WP/Person.php:642 msgid "TouchPoint People ID" msgstr "" -#: src/TouchPoint-WP/Person.php:1187 +#: src/TouchPoint-WP/Person.php:1192 msgid "Contact" msgstr "" -#: src/TouchPoint-WP/Person.php:1398 -#: src/TouchPoint-WP/Utilities.php:205 +#. Translators: %s is the system name. "TouchPoint" by default. +#: src/TouchPoint-WP/Person.php:1200 +msgid "Person in %s" +msgstr "" + +#: src/TouchPoint-WP/Person.php:1451 +#: src/TouchPoint-WP/Utilities.php:286 #: assets/js/base-defer.js:18 msgid "and" msgstr "" -#: src/TouchPoint-WP/Person.php:1404 +#: src/TouchPoint-WP/Person.php:1458 msgctxt "list of people, and *others*" msgid "others" msgstr "" -#: src/TouchPoint-WP/Person.php:1541 +#: src/TouchPoint-WP/Person.php:1597 msgid "Registration Blocked for Spam." msgstr "" -#: src/TouchPoint-WP/Person.php:1609 +#: src/TouchPoint-WP/Person.php:1688 msgid "You may need to sign in." msgstr "" -#: src/TouchPoint-WP/Report.php:170 +#: src/TouchPoint-WP/Report.php:177 msgid "TouchPoint Reports" msgstr "" -#: src/TouchPoint-WP/Report.php:171 +#: src/TouchPoint-WP/Report.php:178 msgid "TouchPoint Report" msgstr "" #. translators: Last updated date/time for a report. %1$s is the date. %2$s is the time. -#: src/TouchPoint-WP/Report.php:301 +#: src/TouchPoint-WP/Report.php:417 msgid "Updated on %1$s at %2$s" msgstr "" -#: src/TouchPoint-WP/Rsvp.php:82 -msgid "RSVP" -msgstr "" - -#: src/TouchPoint-WP/TouchPointWP.php:274 -msgid "Every 15 minutes" -msgstr "" - #. translators: %s: taxonomy name, plural -#: src/TouchPoint-WP/TouchPointWP.php:1225 +#: src/TouchPoint-WP/Taxonomies.php:51 msgid "Search %s" msgstr "" #. translators: %s: taxonomy name, plural -#: src/TouchPoint-WP/TouchPointWP.php:1227 +#: src/TouchPoint-WP/Taxonomies.php:53 msgid "All %s" msgstr "" #. translators: %s: taxonomy name, singular -#: src/TouchPoint-WP/TouchPointWP.php:1229 +#: src/TouchPoint-WP/Taxonomies.php:55 msgid "Edit %s" msgstr "" #. translators: %s: taxonomy name, singular -#: src/TouchPoint-WP/TouchPointWP.php:1231 +#: src/TouchPoint-WP/Taxonomies.php:57 msgid "Update %s" msgstr "" #. translators: %s: taxonomy name, singular -#: src/TouchPoint-WP/TouchPointWP.php:1233 +#: src/TouchPoint-WP/Taxonomies.php:59 msgid "Add New %s" msgstr "" #. translators: %s: taxonomy name, singular -#: src/TouchPoint-WP/TouchPointWP.php:1235 +#: src/TouchPoint-WP/Taxonomies.php:61 msgid "New %s" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:1252 +#: src/TouchPoint-WP/Taxonomies.php:658 msgid "Classify posts by their general locations." msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:1305 +#: src/TouchPoint-WP/Taxonomies.php:687 msgid "Classify posts by their church campus." msgstr "" #. translators: %s: taxonomy name, singular -#: src/TouchPoint-WP/TouchPointWP.php:1357 +#: src/TouchPoint-WP/Taxonomies.php:718 msgid "Classify things by %s." msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:1444 +#: src/TouchPoint-WP/Taxonomies.php:749 msgid "Classify involvements by the day on which they meet." msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:1445 +#: src/TouchPoint-WP/Taxonomies.php:750 msgid "Weekdays" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:1482 +#: src/TouchPoint-WP/Taxonomies.php:776 msgid "Classify involvements by tense (present, future, past)" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:1483 +#: src/TouchPoint-WP/Taxonomies.php:780 msgid "Tense" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:1483 +#: src/TouchPoint-WP/Taxonomies.php:780 msgid "Tenses" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:1526 +#: src/TouchPoint-WP/Taxonomies.php:803 msgid "Classify involvements by the portion of the day in which they meet." msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:1527 +#: src/TouchPoint-WP/Taxonomies.php:809 msgid "Times of Day" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:1580 +#: src/TouchPoint-WP/Taxonomies.php:835 msgid "Classify involvements and users by their age groups." msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:1581 +#: src/TouchPoint-WP/Taxonomies.php:838 msgid "Age Groups" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:1619 +#: src/TouchPoint-WP/Taxonomies.php:864 msgid "Classify involvements by whether participants are mostly single or married." msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:1620 +#: src/TouchPoint-WP/Taxonomies.php:870 msgid "Marital Statuses" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:1664 +#: src/TouchPoint-WP/Taxonomies.php:903 msgid "Classify Partners by category chosen in settings." msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:2431 +#: src/TouchPoint-WP/TouchPointWP.php:266 +msgid "Every 15 minutes" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP.php:2031 msgid "Unknown Type" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:2488 +#: src/TouchPoint-WP/TouchPointWP.php:2088 msgid "Your Searches" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:2491 +#: src/TouchPoint-WP/TouchPointWP.php:2091 msgid "Public Searches" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:2494 +#: src/TouchPoint-WP/TouchPointWP.php:2094 msgid "Status Flags" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:2499 -#: src/TouchPoint-WP/TouchPointWP.php:2500 +#: src/TouchPoint-WP/TouchPointWP.php:2099 +#: src/TouchPoint-WP/TouchPointWP.php:2100 msgid "Current Value" msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:2628 -#: src/TouchPoint-WP/TouchPointWP.php:2668 +#: src/TouchPoint-WP/TouchPointWP.php:2217 +#: src/TouchPoint-WP/TouchPointWP.php:2253 msgid "Invalid or incomplete API Settings." msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:2636 -#: src/TouchPoint-WP/TouchPointWP.php:2675 +#: src/TouchPoint-WP/TouchPointWP.php:2267 +#: src/TouchPoint-WP/TouchPointWP.php:2311 msgid "Host appears to be missing from TouchPoint-WP configuration." msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:2736 +#: src/TouchPoint-WP/TouchPointWP.php:2384 msgid "The scripts on TouchPoint that interact with this plugin are out-of-date, and an automatic update failed." msgstr "" -#: src/TouchPoint-WP/TouchPointWP.php:2795 +#: src/TouchPoint-WP/TouchPointWP.php:2443 msgid "People Query Failed" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:205 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:257 msgid "Basic Settings" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:206 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:258 msgid "Connect to TouchPoint and choose which features you wish to use." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:210 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:262 msgid "Enable Authentication" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:211 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:263 msgid "Allow TouchPoint users to sign into this website with TouchPoint." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:221 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:274 msgid "Enable RSVP Tool" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:222 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:275 msgid "Add a crazy-simple RSVP button to WordPress event pages." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:228 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:282 msgid "Enable Involvements" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:229 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:283 msgid "Load Involvements from TouchPoint for involvement listings and entries native in your website." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:238 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:293 +msgid "Enable Meeting Calendar" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:294 +msgid "Load Meetings from TouchPoint for a calendar, native in your website." +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:304 msgid "Enable Public People Lists" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:239 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:305 msgid "Import public people listings from TouchPoint (e.g. staff or elders)" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:248 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:315 msgid "Enable Global Partner Listings" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:249 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:316 msgid "Import ministry partners from TouchPoint to list publicly." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:258 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:326 msgid "Enable Campuses" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:259 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:327 msgid "Import campuses as a taxonomy. (You probably want to do this if you're multi-campus.)" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:268 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:337 msgid "Display Name" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:269 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:338 msgid "What your church calls your TouchPoint database." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:279 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:348 msgid "TouchPoint Host Name" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:280 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:349 msgid "The domain for your TouchPoint database, without the https or any slashes." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:291 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:361 msgid "Custom Mobile App Deeplink Host Name" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:292 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:362 msgid "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." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:303 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:373 msgid "TouchPoint API Username" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:304 -msgid "The username of a user account in TouchPoint with API permissions." +#: src/TouchPoint-WP/TouchPointWP_Settings.php:374 +msgid "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." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:314 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:385 msgid "TouchPoint API User Password" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:315 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:386 msgid "The password of a user account in TouchPoint with API permissions." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:326 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:398 msgid "TouchPoint API Script Name" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:327 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:399 msgid "The name of the Python script loaded into TouchPoint. Don't change this unless you know what you're doing." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:337 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:410 msgid "Google Maps Javascript API Key" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:338 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:411 msgid "Required for embedding maps." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:348 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:422 msgid "Google Maps Geocoding API Key" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:349 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:423 msgid "Optional. Allows for reverse geocoding of user locations." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:366 -#: src/TouchPoint-WP/TouchPointWP_Settings.php:371 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:433 +msgid "ipapi.co API Key" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:434 +msgid "Optional. Allows for geolocation of user IP addresses. This generally will work without a key, but may be rate limited." +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:444 +msgid "List Site in Directory" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:445 +msgid "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." +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:463 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:468 msgid "Generate Scripts" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:369 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:466 msgid "Once your settings on this page are set and saved, use this tool to generate the scripts needed for TouchPoint in a convenient installation package." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:370 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:467 msgid "Upload the package to {tpName} here" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:387 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:484 msgid "People" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:388 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:485 msgid "Manage how people are synchronized between TouchPoint and WordPress." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:392 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:489 msgid "Contact Keywords" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:393 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:490 msgid "These keywords will be used when someone clicks the \"Contact\" button on a Person's listing or profile." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:404 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:501 msgid "Extra Value for WordPress User ID" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:405 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:502 msgid "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." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:415 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:512 msgid "Extra Value: Biography" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:416 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:513 msgid "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." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:426 -#: src/TouchPoint-WP/TouchPointWP_Settings.php:662 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:523 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:761 msgid "Extra Values to Import" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:427 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:524 msgid "Import People Extra Value fields as User Meta data." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:443 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:540 msgid "Authentication" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:444 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:541 msgid "Allow users to log into WordPress using TouchPoint." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:448 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:545 msgid "Make TouchPoint the default authentication method." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:449 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:546 msgid "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." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:458 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:555 msgid "Enable Auto-Provisioning" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:459 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:556 msgid "Automatically create WordPress users, if needed, to match authenticated TouchPoint users." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:468 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:565 msgid "Change 'Edit Profile' links" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:469 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:566 msgid "\"Edit Profile\" links will take the user to their TouchPoint profile, instead of their WordPress profile." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:478 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:575 msgid "Enable full logout" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:479 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:576 msgid "Logout of TouchPoint when logging out of WordPress." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:485 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:582 msgid "Prevent Subscriber Admin Bar" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:486 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:583 msgid "By enabling this option, users who can't edit anything won't see the Admin bar." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:500 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:597 msgid "Involvements" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:501 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:598 msgid "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." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:506 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:603 msgid "Involvement Post Types" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:535 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:633 msgid "Global Partners" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:536 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:634 msgid "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." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:540 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:638 msgid "Global Partner Name (Plural)" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:541 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:639 msgid "What you call Global Partners at your church" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:551 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:649 msgid "Global Partner Name (Singular)" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:552 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:650 msgid "What you call a Global Partner at your church" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:562 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:660 msgid "Global Partner Name for Secure Places (Plural)" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:563 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:661 msgid "What you call Secure Global Partners at your church" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:573 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:671 msgid "Global Partner Name for Secure Places (Singular)" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:574 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:672 msgid "What you call a Secure Global Partner at your church" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:584 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:682 msgid "Global Partner Slug" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:585 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:683 msgid "The root path for Global Partner posts" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:596 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:695 msgid "Saved Search" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:597 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:696 msgid "Anyone who is included in this saved search will be included in the listing." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:607 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:706 msgid "Extra Value: Description" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:608 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:707 msgid "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." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:618 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:717 msgid "Extra Value: Summary" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:619 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:718 msgid "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." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:629 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:728 msgid "Latitude Override" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:630 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:729 msgid "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." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:640 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:739 msgid "Longitude Override" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:641 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:740 msgid "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." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:651 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:750 msgid "Public Location" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:652 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:751 msgid "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." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:663 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:762 msgid "Import Family Extra Value fields as Meta data on the partner's post" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:674 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:773 msgid "Primary Taxonomy" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:675 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:774 msgid "Import a Family Extra Value as the primary means by which partners are organized." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:690 -msgid "Events Calendar" +#: src/TouchPoint-WP/TouchPointWP_Settings.php:791 +msgid "Events Calendar plugin by Modern Tribe" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:691 -msgid "Integrate with The Events Calendar from ModernTribe." +#: src/TouchPoint-WP/TouchPointWP_Settings.php:795 +msgid "TouchPoint Meetings" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:695 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:801 +msgid "App 2.0 Calendar" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:802 +msgid "Integrate Custom Mobile app version 2.0 with The Events Calendar from Modern Tribe." +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:806 +msgid "Events Provider" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:807 +msgid "The source of events for version 2.0 of the Custom Mobile App." +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:817 msgid "Events for Custom Mobile App" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:698 -msgid "To use your Events Calendar events in the Custom mobile app, set the Provider to Wordpress Plugin - Modern Tribe and use this url:" +#: src/TouchPoint-WP/TouchPointWP_Settings.php:820 +msgid "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:" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:700 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:822 msgid "Preview" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:715 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:837 msgid "Use Standardizing Stylesheet" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:716 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:838 msgid "Inserts some basic CSS into the events feed to clean up display" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:726 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:850 +msgid "Meeting Calendars" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:851 +msgid "Import Meetings from TouchPoint to a calendar on your website." +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:855 +msgid "Meeting Name (Plural)" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:856 +msgid "What you call Meetings at your church" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:861 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:863 +msgid "Meetings" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:861 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:863 +msgid "Events" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:867 +msgid "Meeting Name (Singular)" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:868 +msgid "What you call a Meeting at your church" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:873 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:875 +msgid "Meeting" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:873 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:875 +msgid "Event" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:879 +msgid "Meetings Slug" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:880 +msgid "The root path for Meetings" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:892 +msgid "Days of Future" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:893 +msgid "Meetings more than this many days in the future will not be imported." +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:905 +msgid "Archive After Days" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:906 +msgid "Meetings more than this many days in the past will no longer update from TouchPoint, allowing you to keep some historical event information on the calendar for reference, even if you reuse and update the information in the Involvement." +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:918 +msgid "Days of History" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:919 +msgid "Meetings will be kept on the calendar until the event is this many days in the past. Once an event is older than this, it'll be deleted." +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:935 msgid "Divisions" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:727 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:936 msgid "Import Divisions from TouchPoint to your website as a taxonomy. These are used to classify users and involvements." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:731 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:940 msgid "Division Name (Plural)" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:732 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:941 msgid "What you call Divisions at your church" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:742 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:952 msgid "Division Name (Singular)" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:743 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:953 msgid "What you call a Division at your church" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:753 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:964 msgid "Division Slug" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:754 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:965 msgid "The root path for the Division Taxonomy" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:766 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:978 msgid "These Divisions will be imported for the taxonomy" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:779 -#: src/TouchPoint-WP/TouchPointWP_Settings.php:785 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:989 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1120 +msgid "Post Types" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:990 +msgid "Select post types which should have Divisions available as a native taxonomy." +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1004 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1010 msgid "Locations" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:780 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1005 msgid "Locations are physical places, probably campuses. None are required, but they can help present geographic information clearly." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:804 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1029 msgid "Campuses" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:805 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1030 msgid "Import Campuses from TouchPoint to your website as a taxonomy. These are used to classify users and involvements." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:812 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1037 msgid "Campus Name (Plural)" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:813 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1038 msgid "What you call Campuses at your church" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:823 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1049 msgid "Campus Name (Singular)" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:824 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1050 msgid "What you call a Campus at your church" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:834 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1061 msgid "Campus Slug" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:835 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1062 msgid "The root path for the Campus Taxonomy" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:849 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1078 msgid "Resident Codes" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:850 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1079 msgid "Import Resident Codes from TouchPoint to your website as a taxonomy. These are used to classify users and involvements that have locations." msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:854 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1083 msgid "Resident Code Name (Plural)" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:855 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1084 msgid "What you call Resident Codes at your church" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:865 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1095 msgid "Resident Code Name (Singular)" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:866 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1096 msgid "What you call a Resident Code at your church" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:876 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1107 msgid "Resident Code Slug" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:877 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1108 msgid "The root path for the Resident Code Taxonomy" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:1034 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1121 +msgid "Select post types which should have Resident Codes available as a native taxonomy." +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1284 msgid "password saved" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:1090 -#: src/TouchPoint-WP/TouchPointWP_Settings.php:1091 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1338 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1339 msgid "TouchPoint-WP" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:1122 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1387 msgid "Settings" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:1351 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1627 msgid "Script Update Failed" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:1470 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1749 msgid "TouchPoint-WP Settings" msgstr "" -#: src/TouchPoint-WP/TouchPointWP_Settings.php:1521 +#: src/TouchPoint-WP/TouchPointWP_Settings.php:1800 msgid "Save Settings" msgstr "" -#: src/TouchPoint-WP/Utilities.php:69 +#: src/TouchPoint-WP/TouchPointWP_Widget.php:39 +msgid "TouchPoint-WP Status" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Widget.php:96 +msgid "Imported" +msgstr "" + +#: src/TouchPoint-WP/TouchPointWP_Widget.php:97 +msgid "Last Updated" +msgstr "" + +#: src/TouchPoint-WP/Utilities.php:136 msgctxt "e.g. event happens weekly on..." msgid "Sundays" msgstr "" -#: src/TouchPoint-WP/Utilities.php:70 +#: src/TouchPoint-WP/Utilities.php:137 msgctxt "e.g. event happens weekly on..." msgid "Mondays" msgstr "" -#: src/TouchPoint-WP/Utilities.php:71 +#: src/TouchPoint-WP/Utilities.php:138 msgctxt "e.g. event happens weekly on..." msgid "Tuesdays" msgstr "" -#: src/TouchPoint-WP/Utilities.php:72 +#: src/TouchPoint-WP/Utilities.php:139 msgctxt "e.g. event happens weekly on..." msgid "Wednesdays" msgstr "" -#: src/TouchPoint-WP/Utilities.php:73 +#: src/TouchPoint-WP/Utilities.php:140 msgctxt "e.g. event happens weekly on..." msgid "Thursdays" msgstr "" -#: src/TouchPoint-WP/Utilities.php:74 +#: src/TouchPoint-WP/Utilities.php:141 msgctxt "e.g. event happens weekly on..." msgid "Fridays" msgstr "" -#: src/TouchPoint-WP/Utilities.php:75 +#: src/TouchPoint-WP/Utilities.php:142 msgctxt "e.g. event happens weekly on..." msgid "Saturdays" msgstr "" -#: src/TouchPoint-WP/Utilities.php:111 -msgctxt "e.g. event happens weekly on..." +#: src/TouchPoint-WP/Utilities.php:188 +msgctxt "e.g. \"Event happens weekly on...\" or \"This ...\"" msgid "Sun" msgstr "" -#: src/TouchPoint-WP/Utilities.php:112 -msgctxt "e.g. event happens weekly on..." +#: src/TouchPoint-WP/Utilities.php:189 +msgctxt "e.g. \"Event happens weekly on...\" or \"This ...\"" msgid "Mon" msgstr "" -#: src/TouchPoint-WP/Utilities.php:113 -msgctxt "e.g. event happens weekly on..." +#: src/TouchPoint-WP/Utilities.php:190 +msgctxt "e.g. \"Event happens weekly on...\" or \"This ...\"" msgid "Tue" msgstr "" -#: src/TouchPoint-WP/Utilities.php:114 -msgctxt "e.g. event happens weekly on..." +#: src/TouchPoint-WP/Utilities.php:191 +msgctxt "e.g. \"Event happens weekly on...\" or \"This ...\"" msgid "Wed" msgstr "" -#: src/TouchPoint-WP/Utilities.php:115 -msgctxt "e.g. event happens weekly on..." +#: src/TouchPoint-WP/Utilities.php:192 +msgctxt "e.g. \"Event happens weekly on...\" or \"This ...\"" msgid "Thu" msgstr "" -#: src/TouchPoint-WP/Utilities.php:116 -msgctxt "e.g. event happens weekly on..." +#: src/TouchPoint-WP/Utilities.php:193 +msgctxt "e.g. \"Event happens weekly on...\" or \"This ...\"" msgid "Fri" msgstr "" -#: src/TouchPoint-WP/Utilities.php:117 -msgctxt "e.g. event happens weekly on..." +#: src/TouchPoint-WP/Utilities.php:194 +msgctxt "e.g. \"Event happens weekly on...\" or \"This ...\"" msgid "Sat" msgstr "" -#: src/TouchPoint-WP/Utilities.php:161 +#: src/TouchPoint-WP/Utilities.php:236 msgctxt "Time of Day" msgid "Late Night" msgstr "" -#: src/TouchPoint-WP/Utilities.php:163 +#: src/TouchPoint-WP/Utilities.php:238 msgctxt "Time of Day" msgid "Early Morning" msgstr "" -#: src/TouchPoint-WP/Utilities.php:165 +#: src/TouchPoint-WP/Utilities.php:240 msgctxt "Time of Day" msgid "Morning" msgstr "" -#: src/TouchPoint-WP/Utilities.php:167 +#: src/TouchPoint-WP/Utilities.php:242 msgctxt "Time of Day" msgid "Midday" msgstr "" -#: src/TouchPoint-WP/Utilities.php:169 +#: src/TouchPoint-WP/Utilities.php:244 msgctxt "Time of Day" msgid "Afternoon" msgstr "" -#: src/TouchPoint-WP/Utilities.php:171 +#: src/TouchPoint-WP/Utilities.php:246 msgctxt "Time of Day" msgid "Evening" msgstr "" -#: src/TouchPoint-WP/Utilities.php:173 +#: src/TouchPoint-WP/Utilities.php:248 msgctxt "Time of Day" msgid "Night" msgstr "" -#: assets/js/base-defer.js:208 -#: assets/js/base-defer.js:1154 +#: src/TouchPoint-WP/Utilities.php:291 +msgctxt "list of items, and *others*" +msgid "others" +msgstr "" + +#: src/TouchPoint-WP/Utilities.php:497 +msgid "Expand" +msgstr "" + +#. translators: %1$s is the start date/time, %2$s is the end date/time. +#: src/TouchPoint-WP/Utilities/DateFormats.php:89 +#: src/TouchPoint-WP/Utilities/DateFormats.php:331 +msgid "%1$s – %2$s" +msgstr "" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:119 +#: src/TouchPoint-WP/Utilities/DateFormats.php:194 +msgid "Tonight" +msgstr "" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:121 +#: src/TouchPoint-WP/Utilities/DateFormats.php:196 +msgid "Today" +msgstr "" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:128 +#: src/TouchPoint-WP/Utilities/DateFormats.php:203 +msgid "Tomorrow" +msgstr "" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:130 +#: src/TouchPoint-WP/Utilities/DateFormats.php:205 +msgid "Yesterday" +msgstr "" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:137 +msgctxt "Date string for day of the week, when the year is current." +msgid "l" +msgstr "" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:138 +msgctxt "Date string when the year is current." +msgid "F j" +msgstr "" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:140 +msgctxt "Date string for day of the week, when the year is not current." +msgid "l" +msgstr "" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:141 +msgctxt "Date string when the year is not current." +msgid "F j, Y" +msgstr "" + +#. translators: %1$s is "Monday". %2$s is "January 1". +#: src/TouchPoint-WP/Utilities/DateFormats.php:147 +msgctxt "Date format string" +msgid "Last %1$s, %2$s" +msgstr "" + +#. translators: %1$s is "Monday". %2$s is "January 1". +#: src/TouchPoint-WP/Utilities/DateFormats.php:153 +msgctxt "Date format string" +msgid "This %1$s, %2$s" +msgstr "" + +#. translators: %1$s is "Monday". %2$s is "January 1". +#: src/TouchPoint-WP/Utilities/DateFormats.php:159 +msgctxt "Date format string" +msgid "Next %1$s, %2$s" +msgstr "" + +#. translators: %1$s is "Monday". %2$s is "January 1". +#: src/TouchPoint-WP/Utilities/DateFormats.php:164 +msgctxt "Date format string" +msgid "%1$s, %2$s" +msgstr "" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:211 +msgctxt "Short date string for day of the week, when the year is current." +msgid "D" +msgstr "" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:212 +msgctxt "Short date string when the year is current." +msgid "M j" +msgstr "" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:214 +msgctxt "Short date string for day of the week, when the year is not current." +msgid "D" +msgstr "" + +#: src/TouchPoint-WP/Utilities/DateFormats.php:215 +msgctxt "Short date string when the year is not current." +msgid "M j, Y" +msgstr "" + +#. translators: %1$s is "Mon". %2$s is "Jan 1". +#: src/TouchPoint-WP/Utilities/DateFormats.php:221 +msgctxt "Short date format string" +msgid "Last %1$s, %2$s" +msgstr "" + +#. translators: %1$s is "Mon". %2$s is "Jan 1". +#: src/TouchPoint-WP/Utilities/DateFormats.php:227 +msgctxt "Short date format string" +msgid "This %1$s, %2$s" +msgstr "" + +#. translators: %1$s is "Mon". %2$s is "Jan 1". +#: src/TouchPoint-WP/Utilities/DateFormats.php:233 +msgctxt "Short date format string" +msgid "Next %1$s, %2$s" +msgstr "" + +#. translators: %1$s is "Mon". %2$s is "Jan 1". +#: src/TouchPoint-WP/Utilities/DateFormats.php:238 +msgctxt "Short date format string" +msgid "%1$s, %2$s" +msgstr "" + +#. translators: %1$s is the start date, %2$s start time, %3$s is the end date, and %4$s end time. +#: src/TouchPoint-WP/Utilities/DateFormats.php:364 +msgid "%1$s at %2$s – %3$s at %4$s" +msgstr "" + +#: assets/js/base-defer.js:212 +#: assets/js/base-defer.js:1168 msgid "Your Location" msgstr "" -#: assets/js/base-defer.js:229 +#: assets/js/base-defer.js:233 msgid "User denied the request for Geolocation." msgstr "" -#: assets/js/base-defer.js:233 +#: assets/js/base-defer.js:237 msgid "Location information is unavailable." msgstr "" -#: assets/js/base-defer.js:237 +#: assets/js/base-defer.js:241 msgid "The request to get user location timed out." msgstr "" -#: assets/js/base-defer.js:241 +#: assets/js/base-defer.js:245 msgid "An unknown error occurred." msgstr "" -#: assets/js/base-defer.js:277 -#: assets/js/base-defer.js:287 +#: assets/js/base-defer.js:281 +#: assets/js/base-defer.js:291 msgid "No geolocation option available." msgstr "" #. translators: %s is the name of an involvement, like a particular small group -#: assets/js/base-defer.js:906 +#: assets/js/base-defer.js:920 msgid "Added to %s" msgstr "" -#: assets/js/base-defer.js:909 -#: assets/js/base-defer.js:920 -#: assets/js/base-defer.js:946 -#: assets/js/base-defer.js:957 -#: assets/js/base-defer.js:1403 -#: assets/js/base-defer.js:1414 -#: assets/js/meeting-defer.js:175 -#: assets/js/meeting-defer.js:186 +#: assets/js/base-defer.js:923 +#: assets/js/base-defer.js:934 +#: assets/js/base-defer.js:960 +#: assets/js/base-defer.js:971 +#: assets/js/base-defer.js:1417 +#: assets/js/base-defer.js:1428 +#: assets/js/meeting-defer.js:174 +#: assets/js/meeting-defer.js:185 msgid "OK" msgstr "" -#: assets/js/base-defer.js:917 -#: assets/js/base-defer.js:954 -#: assets/js/base-defer.js:1411 -#: assets/js/meeting-defer.js:183 +#: assets/js/base-defer.js:931 +#: assets/js/base-defer.js:968 +#: assets/js/base-defer.js:1425 +#: assets/js/meeting-defer.js:182 msgid "Something strange happened." msgstr "" -#: assets/js/base-defer.js:943 -#: assets/js/base-defer.js:1400 +#: assets/js/base-defer.js:957 +#: assets/js/base-defer.js:1414 msgid "Your message has been sent." msgstr "" #. translators: %s is the name of an Involvement -#: assets/js/base-defer.js:967 +#: assets/js/base-defer.js:981 msgid "Join %s" msgstr "" -#: assets/js/base-defer.js:983 +#: assets/js/base-defer.js:997 msgid "Who is joining the group?" msgstr "" -#: assets/js/base-defer.js:988 -#: assets/js/base-defer.js:1046 -#: assets/js/base-defer.js:1362 -#: assets/js/base-defer.js:1455 -#: assets/js/meeting-defer.js:225 +#: assets/js/base-defer.js:1002 +#: assets/js/base-defer.js:1060 +#: assets/js/base-defer.js:1376 +#: assets/js/base-defer.js:1469 +#: assets/js/meeting-defer.js:224 msgid "Cancel" msgstr "" -#: assets/js/base-defer.js:1001 +#: assets/js/base-defer.js:1015 msgid "Select who should be added to the group." msgstr "" #. translators: %s is the name of an involvement. This is a heading for a modal. -#: assets/js/base-defer.js:1022 +#: assets/js/base-defer.js:1036 msgid "Contact the Leaders of %s" msgstr "" -#: assets/js/base-defer.js:1039 -#: assets/js/base-defer.js:1355 +#: assets/js/base-defer.js:1053 +#: assets/js/base-defer.js:1369 msgid "From" msgstr "" -#: assets/js/base-defer.js:1040 -#: assets/js/base-defer.js:1356 +#: assets/js/base-defer.js:1054 +#: assets/js/base-defer.js:1370 msgid "Message" msgstr "" -#: assets/js/base-defer.js:1045 -#: assets/js/base-defer.js:1361 +#: assets/js/base-defer.js:1059 +#: assets/js/base-defer.js:1375 msgid "Send" msgstr "" -#: assets/js/base-defer.js:1055 -#: assets/js/base-defer.js:1371 +#: assets/js/base-defer.js:1069 +#: assets/js/base-defer.js:1385 msgid "Please provide a message." msgstr "" -#: assets/js/base-defer.js:1140 -#: assets/js/base-defer.js:1142 +#: assets/js/base-defer.js:1154 +#: assets/js/base-defer.js:1156 msgid "We don't know where you are." msgstr "" -#: assets/js/base-defer.js:1140 -#: assets/js/base-defer.js:1150 +#: assets/js/base-defer.js:1154 +#: assets/js/base-defer.js:1164 msgid "Click here to use your actual location." msgstr "" -#: assets/js/base-defer.js:1301 -#: assets/js/base-defer.js:1318 +#: assets/js/base-defer.js:1315 +#: assets/js/base-defer.js:1332 msgid "clear" msgstr "" -#: assets/js/base-defer.js:1307 +#: assets/js/base-defer.js:1321 msgid "Other Relatives..." msgstr "" #. translators: %s is a person's name. This is a heading for a contact modal. -#: assets/js/base-defer.js:1338 +#: assets/js/base-defer.js:1352 msgid "Contact %s" msgstr "" -#: assets/js/base-defer.js:1445 +#: assets/js/base-defer.js:1459 msgid "Tell us about yourself." msgstr "" -#: assets/js/base-defer.js:1447 -#: assets/js/base-defer.js:1502 +#: assets/js/base-defer.js:1461 +#: assets/js/base-defer.js:1516 msgid "Email Address" msgstr "" -#: assets/js/base-defer.js:1448 -#: assets/js/base-defer.js:1503 +#: assets/js/base-defer.js:1462 +#: assets/js/base-defer.js:1517 msgid "Zip Code" msgstr "" -#: assets/js/base-defer.js:1454 +#: assets/js/base-defer.js:1468 msgid "Next" msgstr "" -#: assets/js/base-defer.js:1488 +#: assets/js/base-defer.js:1502 msgid "Something went wrong." msgstr "" -#: assets/js/base-defer.js:1500 +#: assets/js/base-defer.js:1514 msgid "Our system doesn't recognize you,
so we need a little more info." msgstr "" -#: assets/js/base-defer.js:1504 +#: assets/js/base-defer.js:1518 msgid "First Name" msgstr "" -#: assets/js/base-defer.js:1505 +#: assets/js/base-defer.js:1519 msgid "Last Name" msgstr "" -#: assets/js/base-defer.js:1507 +#: assets/js/base-defer.js:1521 msgid "Phone" msgstr "" @@ -1431,49 +1828,49 @@ msgstr "" msgid "Event Past" msgstr "" -#: assets/js/meeting-defer.js:172 +#: assets/js/meeting-defer.js:171 msgid "Response Recorded" msgid_plural "Responses Recorded" msgstr[0] "" msgstr[1] "" #. translators: "RSVP for {Event Name}" This is the heading on the RSVP modal. The event name isn't translated because it comes from TouchPoint. -#: assets/js/meeting-defer.js:205 +#: assets/js/meeting-defer.js:204 msgid "RSVP for %s" msgstr "" -#: assets/js/meeting-defer.js:217 +#: assets/js/meeting-defer.js:216 msgid "Who is coming?" msgstr "" -#: assets/js/meeting-defer.js:217 +#: assets/js/meeting-defer.js:216 msgid "Indicate who is or is not coming. This will overwrite any existing RSVP." msgstr "" -#: assets/js/meeting-defer.js:217 +#: assets/js/meeting-defer.js:216 msgid "To avoid overwriting an existing RSVP, leave that person blank." msgstr "" -#: assets/js/meeting-defer.js:217 +#: assets/js/meeting-defer.js:216 msgid "To protect privacy, we won't show existing RSVPs here." msgstr "" -#: assets/js/meeting-defer.js:217 +#: assets/js/meeting-defer.js:216 msgid "Yes" msgstr "" -#: assets/js/meeting-defer.js:217 +#: assets/js/meeting-defer.js:216 msgid "No" msgstr "" -#: assets/js/meeting-defer.js:223 +#: assets/js/meeting-defer.js:222 msgid "Add Someone Else" msgstr "" -#: assets/js/meeting-defer.js:224 +#: assets/js/meeting-defer.js:223 msgid "Submit" msgstr "" -#: assets/js/meeting-defer.js:243 +#: assets/js/meeting-defer.js:242 msgid "Nothing to submit." msgstr "" diff --git a/package.json b/package.json index c4f224cc..fef681e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "touchpoint-wp", - "version": "0.0.37", + "version": "0.0.95", "description": "A WordPress Plugin for integrating with TouchPoint Church Management Software.", "directories": { "doc": "docs" diff --git a/phpdoc.xml b/phpdoc.dist.xml similarity index 70% rename from phpdoc.xml rename to phpdoc.dist.xml index 28923d91..97af9e7d 100644 --- a/phpdoc.xml +++ b/phpdoc.dist.xml @@ -1,14 +1,14 @@ + TouchPoint WP docs/phpApi .phpdoc/cache - + src/TouchPoint-WP @@ -17,10 +17,12 @@ php public - false + true TODO + + \ No newline at end of file diff --git a/src/TouchPoint-WP/Auth.php b/src/TouchPoint-WP/Auth.php index a3feca0c..5c9c11b5 100644 --- a/src/TouchPoint-WP/Auth.php +++ b/src/TouchPoint-WP/Auth.php @@ -5,6 +5,7 @@ namespace tp\TouchPointWP; +use Exception; use tp\TouchPointWP\Utilities\Http; use tp\TouchPointWP\Utilities\PersonQuery; use tp\TouchPointWP\Utilities\Session; @@ -16,7 +17,7 @@ } /** - * Allows users to login to WordPress with their TouchPoint credentials, and provides other user management + * Allows users to log in to WordPress with their TouchPoint credentials, and provides other user management * functionality. */ abstract class Auth implements api, module @@ -84,7 +85,7 @@ public static function load(): bool if (is_admin()) { try { self::createApiKeyIfNeeded(); - } catch (TouchPointWP_Exception $e) { + } catch (TouchPointWP_Exception) { } } @@ -132,18 +133,17 @@ public static function footer() */ public static function printLoginLink() { - $html = '

'; + $html = '

'; + $url = self::getLoginUrl(); /** @noinspection HtmlUnknownTarget */ - $html .= ''; + $html .= ""; $html .= sprintf( - // translators: %s is "what you call TouchPoint at your church", which is a setting + // translators: %s is "what you call TouchPoint at your church", which is a setting __('Sign in with your %s account', 'TouchPoint-WP'), htmlentities(TouchPointWP::instance()->settings->system_name) ); - printf( - $html, - self::getLoginUrl() - ); + $html .= '

'; + echo $html; } /** @@ -156,7 +156,7 @@ public static function getLoginUrl(): string { try { self::createApiKeyIfNeeded(); - } catch (TouchPointWP_Exception $e) { + } catch (TouchPointWP_Exception) { } $antiforgeryId = self::generateAntiForgeryId(); @@ -263,10 +263,13 @@ private static function createApiKeyIfNeeded(): void */ public static function redirectLoginFormMaybe() { - $redirect = apply_filters( - TouchPointWP::HOOK_PREFIX . 'auto_redirect_login', - (TouchPointWP::instance()->settings->auth_default === 'on') - ); + $redirect = TouchPointWP::instance()->settings->auth_default === 'on'; + /** + * Controls whether to redirect to the TouchPoint login automatically. + * + * @param bool $redirect Value preset from setting TouchPoint login as default. + */ + $redirect = apply_filters('tp_auto_redirect_login', $redirect); if (isset($_GET[TouchPointWP::HOOK_PREFIX . 'no_redirect'])) { $redirect = false; @@ -284,10 +287,15 @@ public static function redirectLoginFormMaybe() public static function removeAdminBarMaybe() { $removeBar = (TouchPointWP::instance()->settings->auth_prevent_admin_bar === 'on') - && ! is_admin() - && ! current_user_can('edit_posts'); + && ! is_admin() + && ! current_user_can('edit_posts'); - $removeBar = apply_filters(TouchPointWP::HOOK_PREFIX . 'prevent_admin_bar', $removeBar); + /** + * Allows for hiding the WordPress-provided Admin bar. + * + * @param bool $removeBar True if bar should be removed. + */ + $removeBar = apply_filters('tp_prevent_admin_bar', $removeBar); if ($removeBar) { show_admin_bar(false); @@ -303,7 +311,7 @@ public static function removeAdminBarMaybe() */ private static function wantsToLogin(): bool { - $wants_to_login = false; + $wantsToLogin = false; // redirect back from TouchPoint after a successful login if (isset($_GET['loginToken'])) { return false; @@ -315,10 +323,10 @@ private static function wantsToLogin(): bool // Exceptions $action = isset($_GET['loggedout']) ? 'loggedout' : $action; if ('login' == $action) { - $wants_to_login = true; + $wantsToLogin = true; } - return $wants_to_login; + return $wantsToLogin; } /** @@ -425,9 +433,9 @@ public static function authenticate($user, $username, $password) $s = Session::instance(); if ( ! $lst === $s->auth_sessionToken) { return new WP_Error([ - 177004, - __('Session could not be validated.', 'TouchPoint-WP') - ]); + 177004, + __('Session could not be validated.', 'TouchPoint-WP') + ]); } $p->setLoginTokens(null, null); @@ -435,6 +443,12 @@ public static function authenticate($user, $username, $password) $user = $p->toNewWpUser(); + try { + $stats = Stats::instance(); + $stats->userAuths += 1; + $stats->updateDb(); + } catch (Exception) {} + // Preload Ident people for potential use with InformalAuth. Skip if family is already loaded. if ( ! in_array($p->familyId, $s->primaryFam ?? [])) { Person::ident((object)[ diff --git a/src/TouchPoint-WP/CalendarGrid.php b/src/TouchPoint-WP/CalendarGrid.php new file mode 100644 index 00000000..e551f3df --- /dev/null +++ b/src/TouchPoint-WP/CalendarGrid.php @@ -0,0 +1,468 @@ + 12 || $year < 2020 || $year > 2100) { + $d = new DateTime('now', $tz); + $d = new DateTime($d->format('Y-m-01 00:00:00'), $tz); + } else { + $d = new DateTime(null, $tz); + $d->setDate($year, $month, 1); + $d->setTime(0, 0); + } + } catch (Exception $e) { + $this->html = ""; + return; + } + + $firstDayOfMonth = DateTimeImmutable::createFromMutable($d); + $lastDayOfMonth = DateTimeImmutable::createFromMutable($d); + + $this->monthName = self::getMonthNameForDate($d); + + // Get the day of the week for the first day of the month (0 = Sunday, 1 = Monday, ..., 6 = Saturday) + $offsetDays = intval($d->format('w')); // w: Numeric representation of the day of the week + + // Extra days at the end of the month + $daysInMonth = intval($d->format('t')); + $daysToShow = 7 * ceil(($daysInMonth + $offsetDays) / 7); + + // Set start of range to be before the offset + try { + $d->modify("-$offsetDays days"); + } catch (Exception) { // Exception is not feasible. + } + $d->setTimezone($tz); + $r = ""; + + // Create a table to display the calendar + $r .= '
'; + foreach (Utilities::getDaysOfWeekShort() as $dayStr) { + $r .= "
$dayStr
"; + } + + $isMonthBefore = ($offsetDays !== 0); + $isMonthAfter = false; + + // do a query for the whole range of days in the month + $d2 = DateTimeImmutable::createFromMutable($d)->add(new DateInterval("P{$daysToShow}D")); + try { + $newQ = self::adjustQueryForRange($q, $d, $d2, $tz); + } catch (Exception $e) { + $this->html = ""; + return; + } + unset($d2); + + $monthPosts = $newQ->get_posts(); + $monthEvents = []; + foreach ($monthPosts as $e) { + try { + $monthEvents[] = Meeting::fromPost($e); + } catch (Exception) { + // Ignore any exceptions + } + } + + try { + $aDay = new DateInterval("P1D"); + $d2359 = new DateTime($d->format('Y-m-d 23:59:59'), $tz); + } catch (Exception $e) { + $this->html = ""; + return; + } + + // Loop through the days of the month + do { + $ts = DateFormats::timestampWithoutOffset($d); + $day = wp_date("j", $ts); + $fullDay = DateFormats::DateStringFormatted($d); + $wd = wp_date("w", $ts); + + $cellClass = ["calDay"]; + if ($isMonthBefore) { + $cellClass[] = "before"; + $adder = 0; + } elseif ($isMonthAfter) { + $cellClass[] = "after"; + $adder = 0; + } else { + $adder = 1; + } + + $dayEvents = []; + foreach ($monthEvents as $m) { + if ($m->startDt > $d2359) { + break; + } + if ($m->startDt < $d2359 && ($m->endDt ?? $m->startDt) >= $d) { + $dayEvents[] = $m; + } + } + + if (count($dayEvents) === 0) { + $cellClass[] = "empty"; + } + + if ($d < Utilities::dateTimeTodayAtMidnight()) { + $cellClass[] = "past"; + } + + $cellClass[] = "weekday-$wd"; + + $dayHtml = ""; + $hasFirstDays = false; + + foreach ($dayEvents as $m) { + $link = $m->permalink(); + $notFirstDay = $m->startDt < $d; + + $attr = ""; + $status = $m->status(); + + if ($status === Meeting::STATUS_CANCELLED) { + // Translators: %s is the singular name of the of a Meeting, such as "Event". + $title = wp_sprintf(__("%s is cancelled.", "TouchPoint-WP"), TouchPointWP::instance()->settings->mc_name_singular); + $attr = "title=\"$title\""; + } + + $e = $m->getPost(); + + $classes = "event "; + $classes .= $status . " "; + $classes .= $m->tense(); + if ($m->isFeatured() && !$notFirstDay) { + $classes .= " feat"; + } + if ($notFirstDay) { + $classes .= " notFirstDay"; + $dayHtml .= "$e->post_title"; + } else { + $hasFirstDays = true; + $this->eventCount += $adder; + $ts = $m->startTimeString(); + if ($ts) { + $ts = "$ts "; + } else { + $ts = ""; + } + $dayHtml .= "$ts$e->post_title"; + } + } + + if (!$hasFirstDays) { + $cellClass[] = "noFirstDays"; + } + + $cellClass = implode(" ", $cellClass); + + // Print the cell + $r .= "
"; + $r .= "

$fullDay

"; + $r .= "$day"; + + $r .= $dayHtml; + + $r .= "
"; + + // 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' => ">" + ], + '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; + } +} \ No newline at end of file diff --git a/src/TouchPoint-WP/EventsCalendar.php b/src/TouchPoint-WP/EventsCalendar.php index 037a1302..5f9e59a3 100644 --- a/src/TouchPoint-WP/EventsCalendar.php +++ b/src/TouchPoint-WP/EventsCalendar.php @@ -10,19 +10,40 @@ } use WP_Post; +use WP_Query; if ( ! TOUCHPOINT_COMPOSER_ENABLED) { require_once 'api.php'; } /** - * 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. + * + * @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. */ 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,61 +82,235 @@ 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); + + // Not needed for apps, but helpful for diagnostics + $eO['ID'] = $eQ->ID; - // Add domain to relative links + // 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); + // 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 formatContent(?string $content): string + { + $tpDomain = TouchPointWP::instance()->settings->host; + $dlDomain = TouchPointWP::instance()->settings->host_deeplink; + + $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( - "/['\"]\/([^\/\"']*)[\"']/i", - '"' . get_home_url() . '/$1"', + "/:\/\/$tpDomain\/OnlineReg\/([\d]+)/i", + "://" . $dlDomain . '/registrations/register/${1}?from={{MOBILE_OS}}', $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 - ); + 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+. + * @depreacted + */ + 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 .= "
" . $e->getActionButtons('mobile', 'btn', withTouchPointLink: false, absoluteLinks: true)->join(" ") . "
"; + $content = self::formatContent($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']; @@ -133,7 +325,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 +344,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 +362,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 +375,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 +400,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 +421,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 +430,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/Utilities/Geo.php b/src/TouchPoint-WP/Geo.php similarity index 50% rename from src/TouchPoint-WP/Utilities/Geo.php rename to src/TouchPoint-WP/Geo.php index 618b210c..b9c7b32f 100644 --- a/src/TouchPoint-WP/Utilities/Geo.php +++ b/src/TouchPoint-WP/Geo.php @@ -3,14 +3,32 @@ * @package TouchPointWP */ -namespace tp\TouchPointWP\Utilities; +namespace tp\TouchPointWP; + +use stdClass; + +if ( ! defined('ABSPATH')) { + exit(1); +} + /** - * 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 +46,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/Involvement.php b/src/TouchPoint-WP/Involvement.php index 6b1233a0..3ab60c86 100644 --- a/src/TouchPoint-WP/Involvement.php +++ b/src/TouchPoint-WP/Involvement.php @@ -13,19 +13,27 @@ require_once "api.php"; require_once "jsInstantiation.php"; require_once "jsonLd.php"; + require_once "hierarchical.php"; + require_once "scheduled.php"; require_once "updatesViaCron.php"; require_once "Utilities.php"; - require_once "Utilities/Geo.php"; require_once "Involvement_PostTypeSettings.php"; } use DateInterval; use DateTimeImmutable; +use DateTimeZone; use Exception; +use JsonSerializable; use stdClass; +use tp\TouchPointWP\Utilities\DateFormats; +use tp\TouchPointWP\Utilities\DateTimeExtended; use tp\TouchPointWP\Utilities\Http; use tp\TouchPointWP\Utilities\PersonArray; use tp\TouchPointWP\Utilities\PersonQuery; +use tp\TouchPointWP\Utilities\StringableArray; +use tp\TouchPointWP\Utilities\Translation; +use TypeError; use WP_Error; use WP_Post; use WP_Query; @@ -34,7 +42,7 @@ /** * Fundamental object meant to correspond to an Involvement in TouchPoint */ -class Involvement implements api, updatesViaCron, geo, module +class Involvement extends PostTypeCapable implements api, updatesViaCron, hasGeo, module, hierarchical, JsonSerializable, scheduled { use jsInstantiation; use jsonLd; @@ -44,12 +52,18 @@ class Involvement implements api, updatesViaCron, geo, module public const SHORTCODE_LIST = TouchPointWP::SHORTCODE_PREFIX . "Inv-List"; public const SHORTCODE_NEARBY = TouchPointWP::SHORTCODE_PREFIX . "Inv-Nearby"; public const SHORTCODE_ACTIONS = TouchPointWP::SHORTCODE_PREFIX . "Inv-Actions"; + + protected const SCHEDULE_STRING_CACHE_EXPIRATION = 3600 * 8; // 8 hours. Automatically deleted during sync. + protected const SCHEDULE_STRING_CACHE_GROUP = TouchPointWP::HOOK_PREFIX . "inv_schedule_string"; + protected const ENABLE_SCHEDULE_STRING_CACHE = true; + + protected const MEETING_STRATEGY_NONE = 0; + protected const MEETING_STRATEGY_SINGLE = 1; + protected const MEETING_STRATEGY_MULTIPLE = 2; + public const CRON_HOOK = TouchPointWP::HOOK_PREFIX . "inv_cron_hook"; public const CRON_OFFSET = 86400 + 3600; - public const SCHEDULE_STRING_CACHE_EXPIRATION = 3600 * 8; // 8 hours. Automatically deleted during sync. - public const SCHEDULE_STRING_CACHE_GROUP = TouchPointWP::HOOK_PREFIX . "inv_schedule_string"; - protected static bool $_hasUsedMap = false; protected static bool $_hasArchiveMap = false; private static array $_instances = []; @@ -59,14 +73,14 @@ class Involvement implements api, updatesViaCron, geo, module public static string $itemClass = 'inv-list-item'; private static bool $filterJsAdded = false; - public ?object $geo = null; + protected ?object $geo = null; static protected object $compareGeo; protected ?string $locationName = null; - protected ?DateTimeImmutable $_nextMeeting; - protected ?DateTimeImmutable $firstMeeting = null; - protected ?DateTimeImmutable $lastMeeting = null; - protected ?string $_scheduleString; + protected ?DateTimeExtended $_nextMeeting; + protected ?DateTimeExtended $firstMeeting = null; + protected ?DateTimeExtended $lastMeeting = null; + protected ?array $_scheduleStrings = null; protected ?array $_meetings = null; protected ?array $_schedules = null; protected PersonArray $_leaders; @@ -83,11 +97,8 @@ class Involvement implements api, updatesViaCron, geo, module */ public string $invType; - public int $post_id; public string $post_excerpt; - protected WP_Post $post; - - public const INVOLVEMENT_META_KEY = TouchPointWP::SETTINGS_PREFIX . "invId"; + protected ?WP_Post $post = null; public object $attributes; protected array $divisions; @@ -95,7 +106,7 @@ class Involvement implements api, updatesViaCron, geo, module /** * Involvement constructor. * - * @param $object WP_Post|object an object representing the involvement's post. + * @param object $object WP_Post|object an object representing the involvement's post. * Must have post_id AND inv id attributes. * * @throws TouchPointWP_Exception @@ -108,12 +119,12 @@ protected function __construct(object $object) // WP_Post Object $this->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 +133,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 +154,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 +185,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 +211,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); + } } } @@ -255,8 +270,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 +278,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, @@ -289,15 +303,15 @@ public static function init(): void } // 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 +347,34 @@ 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): 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); $verbose &= TouchPointWP::currentUserIsAdmin(); + ini_set('max_execution_time', 300); + ini_set('memory_limit', '512M'); + if (!defined('WP_MAX_MEMORY_LIMIT')) { + define('WP_MAX_MEMORY_LIMIT', '512M'); + } + + 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 +383,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); } catch (Exception $e) { if ($verbose) { echo "An exception occurred while syncing $type->namePlural: " . $e->getMessage(); } continue; + } finally { + TouchPointWP::instance()->unsetTpWpUserAsCurrent(); } if ($update === false) { @@ -370,6 +401,11 @@ public static function updateFromTouchPoint(bool $verbose = false) } else { $count += $update; } + + if ($verbose) { + $time = microtime(true) - $startTime; + echo "

$time seconds have elapsed.

"; + } } unset($type); @@ -400,28 +436,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 +491,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 +657,10 @@ protected function meetings(): array if ($m === "") { $m = []; } + + // Make sure items are unique. #204 + $m = array_unique($m, SORT_REGULAR); + $this->_meetings = $m; } @@ -511,49 +684,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,7 +778,7 @@ public function nextMeeting(): ?DateTimeImmutable if ($this->_nextMeeting === null) { // meetings foreach ($this->meetings() as $m) { - $mdt = $m->dt; + $mdt = $m->mtgStartDt; if ($mdt > $now) { if ($this->_nextMeeting === null || $mdt < $this->_nextMeeting) { $this->_nextMeeting = $mdt; @@ -571,23 +788,15 @@ public function nextMeeting(): ?DateTimeImmutable // 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 +805,45 @@ public function nextMeeting(): ?DateTimeImmutable } /** + * @param $apiMeeting + * + * @return bool + * + * TODO update with #184 + */ + protected static function apiMeetingIsAllDay($apiMeeting): bool + { + return $apiMeeting->mtgStartDt->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 +855,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 +879,130 @@ private static function computeCommonOccurrences(array $meetings = [], array $sc continue; } - $dt = $m->dt; + /** @var DateTimeExtended $start */ + $start = $m->mtgStartDt; - if ($dt < $now) { + /** @var ?DateTimeExtended $end */ + $end = $m->mtgEndDt; + + 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 +1016,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 +1179,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 * @@ -895,7 +1293,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; } } @@ -1060,7 +1458,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 ]; @@ -1109,7 +1507,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 +1533,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; } @@ -1168,6 +1570,7 @@ private static function getWpPostByInvolvementId($postType, $involvementId) * @return string * * @noinspection PhpUnusedParameterInspection + * @noinspection PhpMissingParamTypeInspection */ public static function listShortcode($params = [], string $content = ""): string { @@ -1185,7 +1588,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 ], @@ -1322,9 +1735,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 +1757,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 +1805,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 +1827,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 +1856,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 +1882,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 +1916,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 +1933,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 +1964,17 @@ 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) { + throw new TouchPointWP_Exception("Invalid Involvement ID provided.", 171002); + } if ( ! isset(self::$_instances[$iid])) { self::$_instances[$iid] = new Involvement($post); @@ -1594,7 +2009,7 @@ public static function api(array $uri): bool case "nearby": TouchPointWP::doCacheHeaders(TouchPointWP::CACHE_PRIVATE); self::ajaxNearby(); - exit; +// exit; ajaxNearby() is no-return. case "force-sync": TouchPointWP::doCacheHeaders(TouchPointWP::CACHE_NONE); @@ -1616,7 +2031,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 +2075,10 @@ public static function ajaxNearby(): void exit; } + if ($geoObj->type == "loc") { + $geoObj->type = "ip"; + } + $lat = $geoObj->lat; $lng = $geoObj->lng; @@ -1668,6 +2087,7 @@ public static function ajaxNearby(): void $geoObj = TouchPointWP::instance()->reverseGeocode($lat, $lng); if ($geoObj !== false) { + $geoObj->type = "nav"; $r['geo'] = $geoObj; } } @@ -1678,8 +2098,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 +2134,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 +2148,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 +2176,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 +2190,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. @@ -1878,7 +2280,7 @@ public static function updateCron(): void { try { self::updateFromTouchPoint(); - } catch (Exception $ex) { + } catch (Exception) { } } @@ -1900,7 +2302,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, @@ -1961,7 +2363,7 @@ public static function sortPosts(WP_Post $a, WP_Post $b): int $b = self::fromPost($b); return self::sort($a, $b); - } catch (TouchPointWP_Exception $ex) { + } catch (TouchPointWP_Exception) { return $a <=> $b; } } @@ -2017,6 +2419,12 @@ 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) { + self::fromPost($post)?->enqueueForJsInstantiation(); + } } $script = file_get_contents(TouchPointWP::$dir . "/src/js-partials/involvement-map-inline.js"); @@ -2028,7 +2436,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 +2444,6 @@ public static function mapShortcode($params = [], string $content = ""): string return $content; } - /** * Indicates whether a map of a single Involvement can be displayed. * @@ -2052,38 +2458,35 @@ public function hasGeo(): bool return $this->geo !== null && $this->geo->lat !== null && $this->geo->lng !== 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; } - /** * Update posts that are based on an involvement. * * @param Involvement_PostTypeSettings $typeSets - * @param string|int $divs * @param bool $verbose * * @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|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.

"; + } $qOpts = []; @@ -2096,12 +2499,28 @@ final protected static function updateInvolvementPostsForType( } try { - $response = TouchPointWP::instance()->apiGet( - "InvsForDivs", - array_merge($qOpts, ['divs' => $divs]), - 180 - ); - } catch (TouchPointWP_Exception $e) { + $qOpts['camps'] = Utilities::idArrayToIntArray($typeSets->importCampuses, false); + + if ($typeSets->postType === Meeting::POST_TYPE) { + $qOpts['featMtgs'] = 1; + $qOpts['exDivs'] = Utilities::idArrayToIntArray(Involvement_PostTypeSettings::getAllDivs(), false); + } else { + $qOpts['divs'] = Utilities::idArrayToIntArray($typeSets->importDivs, false); + } + + if (TouchPointWP::instance()->settings->enable_meeting_cal === 'on' || $typeSets->importMeetings) { + // Meetings to be imported + $qOpts['mtgHist'] = TouchPointWP::instance()->settings->mc_hist_days; + $qOpts['mtgFuture'] = TouchPointWP::instance()->settings->mc_future_days; + } else { + // Meetings for involvements that aren't imported. For schedule strings only. + $qOpts['mtgHist'] = 0; + $qOpts['mtgFuture'] = 365; + } + + $response = TouchPointWP::instance()->apiGet("Invs", $qOpts, 180, $verbose); + + } catch (TouchPointWP_Exception) { return false; } unset($qOpts); @@ -2116,75 +2535,27 @@ final protected static function updateInvolvementPostsForType( $postsToKeep = []; try { - $now = new DateTimeImmutable("now", $siteTz); - $aYear = new DateInterval('P1Y'); - $nowPlus1Y = $now->add($aYear); + $now = Utilities::dateTimeNow(); + $histDays = intval(TouchPointWP::instance()->settings->mc_hist_days); + $histVal = new DateInterval("P{$histDays}D"); + $nowMinusH = $now->sub($histVal); unset($aYear); } catch (Exception $e) { + if ($verbose) { + $m = $e->getMessage(); + echo "

Could not calculate date values. Original Exception: $m

"; + } return false; } foreach ($invData as $inv) { set_time_limit(15); - if ($verbose) { - var_dump($inv); - } - - //////////////////////// // Standardize Inputs // //////////////////////// - // Start and end dates - if ($inv->firstMeeting !== null) { - try { - $inv->firstMeeting = new DateTimeImmutable($inv->firstMeeting, $siteTz); - } catch (Exception $e) { - $inv->firstMeeting = null; - } - } - if ($inv->lastMeeting !== null) { - try { - $inv->lastMeeting = new DateTimeImmutable($inv->lastMeeting, $siteTz); - } catch (Exception $e) { - $inv->lastMeeting = null; - } - } - - // Meeting and Schedule date/time strings as DateTimeImmutables - foreach ($inv->schedules as $i => $s) { - try { - $s->next = new DateTimeImmutable($s->next, $siteTz); - } catch (Exception $e) { - unset($inv->schedules[$i]); - } - } - foreach ($inv->meetings as $i => $m) { - try { - $m->dt = new DateTimeImmutable($m->dt, $siteTz); - } catch (Exception $e) { - unset($inv->meetings[$i]); - } - } - - // Registration start - if ($inv->regStart !== null) { - try { - $inv->regStart = new DateTimeImmutable($inv->regStart, $siteTz); - } catch (Exception $e) { - $inv->regStart = null; - } - } - - // Registration end - if ($inv->regEnd !== null) { - try { - $inv->regEnd = new DateTimeImmutable($inv->regEnd, $siteTz); - } catch (Exception $e) { - $inv->regEnd = null; - } - } + self::standardizeApiData($inv, $siteTz, $verbose); //////////////// @@ -2193,8 +2564,21 @@ final protected static function updateInvolvementPostsForType( // 'continue' causes involvement to be deleted (or not created). + // Exclude Meeting type if there are no meetings. + if ($typeSets->postType === Meeting::POST_TYPE && !$inv->isParent) { + if (count($inv->meetings) < 1) { + if ($verbose) { + echo "

Stopping processing because no meetings were returned. Involvement will be deleted from WordPress.

"; + } + continue; + } + } + // Filter by end dates to stay relevant - if ($inv->lastMeeting !== null && $inv->lastMeeting < $now) { // last meeting already happened. + if ($inv->lastMeeting !== null && ( + (!$typeSets->importMeetings && $inv->lastMeeting < $now) || + ($typeSets->importMeetings && $inv->lastMeeting < $nowMinusH)) + ) { // last meeting was long enough ago to no longer be relevant. if ($verbose) { echo "

Stopping processing because all meetings are in the past. Involvement will be deleted from WordPress.

"; } @@ -2260,306 +2644,690 @@ final protected static function updateInvolvementPostsForType( $post = self::getWpPostByInvolvementId($typeSets->postType, $inv->involvementId); - $titleToUse = $inv->regTitle ?? $inv->name; - $titleToUse = trim($titleToUse); + $inv->titleToUse = $inv->regTitle ?? $inv->name; + $inv->titleToUse = trim($inv->titleToUse); if ($post === null) { $post = wp_insert_post( [ // create new 'post_type' => $typeSets->postType, - 'post_name' => $titleToUse, + 'post_title' => $inv->titleToUse, + 'post_name' => $inv->titleToUse, + 'post_status' => 'publish', 'meta_input' => [ - self::INVOLVEMENT_META_KEY => $inv->involvementId + TouchPointWP::INVOLVEMENT_META_KEY => $inv->involvementId ] ] ); $post = get_post($post); } - if ($post instanceof WP_Error) { - new TouchPointWP_WPError($post); - continue; - } + $postsToKeep = [...$postsToKeep, ...Involvement::doPostUpdate($post, $inv, $typeSets, $verbose)]; + $postsToKeep[] = $post->ID; + } + unset($inv); - if ($post === null) { - new TouchPointWP_Exception("Post could not be found or created.", 171001); - continue; - } + ////////////////// + //// Removals //// + ////////////////// - /** @var $post WP_Post */ - if ($inv->description == null || trim($inv->description) == "") { - $post->post_content = null; - } else { - $post->post_content = Utilities::standardizeHtml($inv->description, "involvement-import"); - } - // Title & Slug -- slugs should only be updated if there's a reason, like a title change. Otherwise, they increment. - if ($post->post_title != $titleToUse || str_contains($post->post_name, "__trashed")) { - $post->post_title = $titleToUse; - $post->post_name = ''; // Slug will regenerate; - } + if ($verbose) { + $tsn = $typeSets->namePlural; + echo "

Deletions for $tsn

"; + } - // Parent Post - if ($typeSets->hierarchical) { - $parent = 0; - if ($inv->parentInvId > 0) { - $parent = self::getWpPostByInvolvementId($typeSets->postType, $inv->parentInvId); - $parent = $parent->ID; - if ($verbose) { - echo "

Parent Post: $parent

"; - } - } + // Delete posts that are no longer current + $q = new WP_Query([ + 'post_type' => $typeSets->postType, + 'nopaging' => true, + 'post__not_in' => $postsToKeep + ]); + $removals = 0; + foreach ($q->get_posts() as $post) { + set_time_limit(10); + wp_delete_post($post->ID, true); + $removals++; + } - $post->post_parent = $parent; - } + if ($verbose) { + echo "

Deleted $removals posts."; + } - // Status & Submit - $post->post_status = 'publish'; - wp_update_post($post); + return $removals + count($invData); + } + + /** + * Does the heavy-lifting of updating a given post, with the given information. + * + * @param mixed $post TODO give a firm type + * @param object $inv + * @param Involvement_PostTypeSettings $typeSets + * @param bool $verbose + * + * @return int[] A list of Post IDs that should be kept. + */ + protected static function doPostUpdate($post, object $inv, Involvement_PostTypeSettings $typeSets, bool $verbose = false): array + { + if ($post instanceof WP_Error) { + new TouchPointWP_WPError($post); + return []; + } - update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "locationName", $inv->location); - update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "memberCount", $inv->memberCount); - update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "genderId", $inv->genderId); - update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "groupFull", ! ! $inv->groupFull); - update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "groupClosed", ! ! $inv->closed); - update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "hasRegQuestions", ! ! $inv->hasRegQuestions); - update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "regTypeId", intval($inv->regTypeId)); + if ($post === null) { + new TouchPointWP_Exception("Post could not be found or created.", 171001); + return []; + } + /** @var $post WP_Post */ + if ($inv->description == null || trim($inv->description) === "") { + $post->post_content = ""; + } else { + $post->post_content = Utilities::standardizeHtml($inv->description, "involvement-import"); + } - // Registration start - if ($inv->regStart === null) { - delete_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "regStart"); - } else { - update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "regStart", $inv->regStart); - } + // Title & Slug -- slugs should only be updated if there's a reason, like a title change. Otherwise, they increment. + if ($post->post_title != $inv->titleToUse || str_contains($post->post_name, "__trashed")) { + $post->post_title = $inv->titleToUse; + $post->post_name = ''; // Slug will regenerate; + } - // Registration end - if ($inv->regEnd === null) { - delete_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "regEnd"); - } else { - update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "regEnd", $inv->regEnd); - } + if ($verbose) { + $link = get_permalink($post); + echo "

Updating Post $post->ID based on Involvement $inv->involvementId ($inv->titleToUse).

"; + } + + // Parent Post + if ($typeSets->hierarchical) { + $parent = 0; + if ($inv->parentInvId > 0) { + $parent = self::getWpPostByInvolvementId($typeSets->postType, $inv->parentInvId); + $parent = $parent->ID; - // Update image, if appropriate. - $imageUrl = ""; - if (!!$typeSets->useImages) { - $imageUrl = $inv->imageUrl; + if ($verbose) { + echo "

Parent Post: $parent

"; + } } - Utilities::updatePostImageFromUrl($post->ID, $imageUrl, $post->post_title); + $post->post_parent = $parent; + } - //////////////////// - //// SCHEDULING //// - //////////////////// + // Status & Submit + $post->post_status = 'publish'; + wp_update_post($post); - // Establish a container - if ( ! is_array($inv->meetings)) { - $inv->meetings = []; - } + update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "locationName", $inv->location); + update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "memberCount", $inv->memberCount); + update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "genderId", $inv->genderId); + update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "groupFull", !!$inv->groupFull); + update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "groupClosed", !!$inv->closed); + update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "hasRegQuestions", !!$inv->hasRegQuestions); + update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "regUrl", $inv->redirectUrl); + update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "regTypeId", intval($inv->regTypeId)); + update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "siteRegTypeId", intval($inv->siteRegTypeId)); + update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "hasRegQuestions", !!$inv->hasRegQuestions); - // Determine schedule characteristics for terms - $upcomingDateTimes = self::computeCommonOccurrences($inv->meetings, $inv->schedules); - $uniqueTimeStrings = []; - $timeTerms = []; - $days = []; - foreach ($upcomingDateTimes as $dtString => $dt) { - $weekday = "d" . $dtString[0]; + // Registration start + if ($inv->regStart === null) { + delete_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "regStart"); + } else { + update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "regStart", $inv->regStart); + } - // days - if ( ! isset($days[$weekday])) { - $days[$weekday] = []; - } - $days[$weekday][] = $dt['example']; + // Registration end + if ($inv->regEnd === null) { + delete_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "regEnd"); + } else { + update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "regEnd", $inv->regEnd); + } - // times - $timeStr = substr($dtString, 2); - if ( ! in_array($timeStr, $uniqueTimeStrings)) { - $uniqueTimeStrings[] = $timeStr; - $timeTerm = Utilities::getTimeOfDayTermForTime_noI18n($dt['example']); - if ( ! in_array($timeTerm, $timeTerms)) { - $timeTerms[] = $timeTerm; - } + // Update image, if appropriate. + $imageUrl = ""; + if (!!$typeSets->useImages) { + $imageUrl = $inv->imageUrl; + } + + $imageId = Utilities::updatePostImageFromUrl($post->ID, $imageUrl, $post->post_title); + + //////////////////// + //// SCHEDULING //// + //////////////////// + + // Establish a container + if ( ! is_array($inv->meetings)) { + $inv->meetings = []; + } + + // Determine schedule characteristics for terms + $upcomingDateTimes = self::computeCommonOccurrences($inv->meetings, $inv->schedules); + $uniqueTimeStrings = []; + $timeTerms = []; + $days = []; + + foreach ($upcomingDateTimes as $dtString => $dt) { + $weekday = "d" . $dtString[0]; + + // days + if ( ! isset($days[$weekday])) { + $days[$weekday] = []; + } + $days[$weekday][] = $dt['example']; + + // times + $timeStr = substr($dtString, 2); + if ( ! in_array($timeStr, $uniqueTimeStrings)) { + $uniqueTimeStrings[] = $timeStr; + if ($timeStr === "9999") { + // All day; doesn't need to be categorized. TODO see #184 + continue; + } + $timeTerm = Utilities::getTimeOfDayTermForTime_noI18n($dt['example']); + if ( ! in_array($timeTerm, $timeTerms)) { + $timeTerms[] = $timeTerm; } - unset($timeStr, $weekday); } + unset($timeStr, $weekday); + } + + // first and last meeting dates + $tense = Taxonomies::TAX_TENSE_PRESENT; + if ($inv->firstMeeting !== null && $inv->firstMeeting < Utilities::dateTimeNow()) { // First meeting already happened. + $inv->firstMeeting = null; // We don't need to list info from the past. + } + if ($inv->firstMeeting === null) { + delete_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "firstMeeting"); + } else { + update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "firstMeeting", $inv->firstMeeting); + } - // Start and end dates - $tense = TouchPointWP::TAX_TENSE_PRESENT; - if ($inv->firstMeeting !== null && $inv->firstMeeting < $now) { // First meeting already happened. - $inv->firstMeeting = null; // We don't need to list info from the past. + // Determine if there are meetings beyond the end date, and if so, nullify the end date + if ($inv->lastMeeting !== null) { + foreach ($inv->meetings as $m) { + if ($m->mtgStartDt > $inv->lastMeeting) { + $inv->lastMeeting = null; + break; + } } - if ($inv->firstMeeting === null) { - delete_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "firstMeeting"); + } + if ($inv->lastMeeting !== null && $inv->lastMeeting > Utilities::dateTimeNowPlus1Y()) { // Last mtg is > 1yr away + $inv->lastMeeting = null; // For all practical purposes: it's not ending. + } + if ($inv->lastMeeting === null) { + delete_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "lastMeeting"); + } else { + update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "lastMeeting", $inv->lastMeeting); + } + + // Clear Cached Schedule String + $cacheKey = $inv->involvementId . "_" . get_locale(); + wp_cache_delete($cacheKey, self::SCHEDULE_STRING_CACHE_GROUP); + + // Tense + if ($inv->firstMeeting !== null) { + $tense = Taxonomies::TAX_TENSE_FUTURE; + } + /** @noinspection PhpRedundantOptionalArgumentInspection */ + wp_set_post_terms($post->ID, [$tense], Taxonomies::TAX_TENSE, false); + + // Update meetings and schedules + update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "meetings", $inv->meetings); + update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "schedules", $inv->schedules); + + // Day of week taxonomy + $dayTerms = []; + foreach ($days as $k => $d) { + $dayTerms[] = Utilities::getDayOfWeekShortForNumber_noI18n(intval($k[1])); + } + /** @noinspection PhpRedundantOptionalArgumentInspection */ + wp_set_post_terms($post->ID, $dayTerms, Taxonomies::TAX_WEEKDAY, false); + + // Time of day taxonomy + /** @noinspection PhpRedundantOptionalArgumentInspection */ + wp_set_post_terms($post->ID, $timeTerms, Taxonomies::TAX_DAYTIME, false); + + + //////////////// + //// People //// + //////////////// + + // Leaders & Members are now imported through the Person sync. + + //////////////////// + //// Geographic //// + //////////////////// + + // Handle locations for involvement types that are geo-enabled + if ($typeSets->useGeo) { + // Handle locations + if (property_exists($inv, "lat") && $inv->lat !== null && + property_exists($inv, "lng") && $inv->lng !== null) { + update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "geo_lat", $inv->lat); + update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "geo_lng", $inv->lng); } else { - update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "firstMeeting", $inv->firstMeeting); + delete_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "geo_lat"); + delete_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "geo_lng"); } - if ($inv->lastMeeting !== null && $inv->lastMeeting > $nowPlus1Y) { // Last mtg is > 1yr away - $inv->lastMeeting = null; // For all practical purposes: it's not ending. + // Handle Resident Code + if (property_exists($inv, "resCodeName") && $inv->resCodeName !== null) { + /** @noinspection PhpRedundantOptionalArgumentInspection */ + wp_set_post_terms($post->ID, [$inv->resCodeName], Taxonomies::TAX_RESCODE, false); + } else { + /** @noinspection PhpRedundantOptionalArgumentInspection */ + wp_set_post_terms($post->ID, [], Taxonomies::TAX_RESCODE, false); } - if ($inv->lastMeeting === null) { - delete_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "lastMeeting"); + } + + + //////////////// + //// Campus //// + //////////////// + + if (TouchPointWP::instance()->settings->enable_campuses === "on") { + if (property_exists($inv, "campusName") && $inv->campusName !== null) { + /** @noinspection PhpRedundantOptionalArgumentInspection */ + wp_set_post_terms($post->ID, [$inv->campusName], Taxonomies::TAX_CAMPUS, false); } else { - update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "lastMeeting", $inv->lastMeeting); + /** @noinspection PhpRedundantOptionalArgumentInspection */ + wp_set_post_terms($post->ID, [], Taxonomies::TAX_CAMPUS, false); } - // Clear Cached Schedule String - $cacheKey = $inv->involvementId . "_" . get_locale(); - wp_cache_delete($cacheKey, self::SCHEDULE_STRING_CACHE_GROUP); + if (Translation::useCampusAsLanguage() && $inv->campusName !== null) { - // Tense - if ($inv->firstMeeting !== null) { - $tense = TouchPointWP::TAX_TENSE_FUTURE; + // Set content's original language based on Campus. + $langCode = Translation::getWpmlLangCodeForString($inv->campusName); + if ($langCode !== null) { + $args = [ + 'element_id' => $post->ID, + 'element_type' => apply_filters('wpml_element_type', $typeSets->postTypeWithPrefix()), + 'language_code' => $langCode, + 'source_language_code' => $langCode, + 'trid' => $post->ID + ]; + do_action( 'wpml_set_element_language_details', $args); + if ($verbose) { + echo "

Language Set to: $langCode

"; + } + } } - /** @noinspection PhpRedundantOptionalArgumentInspection */ - wp_set_post_terms($post->ID, [$tense], TouchPointWP::TAX_TENSE, false); + } - // Update meetings and schedules - update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "meetings", $inv->meetings); - update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "schedules", $inv->schedules); - // Day of week taxonomy - $dayTerms = []; - foreach ($days as $k => $d) { - $dayTerms[] = Utilities::getDayOfWeekShortForNumber_noI18n(intval($k[1])); + ///////////////////// + //// Demographic //// + ///////////////////// + + // Handle Marital Status + $maritalTax = []; + if ($inv->marital_denom > 4) { // only include involvements with at least 4 people with known marital statuses. + $marriedProportion = (float)$inv->marital_married / $inv->marital_denom; + if ($marriedProportion > 0.7) { + $maritalTax[] = "mostly_married"; + } elseif ($marriedProportion < 0.3) { + $maritalTax[] = "mostly_single"; } - /** @noinspection PhpRedundantOptionalArgumentInspection */ - wp_set_post_terms($post->ID, $dayTerms, TouchPointWP::TAX_WEEKDAY, false); + } + /** @noinspection PhpRedundantOptionalArgumentInspection */ + wp_set_post_terms($post->ID, $maritalTax, Taxonomies::TAX_INV_MARITAL, false); - // Time of day taxonomy + // Handle Age Groups + if ($inv->age_groups === null) { /** @noinspection PhpRedundantOptionalArgumentInspection */ - wp_set_post_terms($post->ID, $timeTerms, TouchPointWP::TAX_DAYTIME, false); + wp_set_post_terms($post->ID, [], Taxonomies::TAX_AGEGROUP, false); + } else { + /** @noinspection PhpRedundantOptionalArgumentInspection */ + wp_set_post_terms($post->ID, $inv->age_groups, Taxonomies::TAX_AGEGROUP, false); + } - //////////////// - //// People //// - //////////////// + /////////////////// + //// Divisions //// + /////////////////// - // Leaders & Members are now imported through the Person sync. + // Handle divisions + $divs = []; + if ($inv->divs !== null) { + foreach ($inv->divs as $d) { + $tid = TouchPointWP::getDivisionTermIdByDivId($d); + if ( ! ! $tid) { + $divs[] = $tid; + } + } + } + /** @noinspection PhpRedundantOptionalArgumentInspection */ + wp_set_post_terms($post->ID, $divs, Taxonomies::TAX_DIV, false); - //////////////////// - //// Geographic //// - //////////////////// + if ($verbose) { + echo "

Division Terms:

"; + Utilities::var_dump_expandable($divs); + } - // Handle locations for involvement types that are geo-enabled - if ($typeSets->useGeo) { - // Handle locations - if (property_exists($inv, "lat") && $inv->lat !== null && - property_exists($inv, "lng") && $inv->lng !== null) { - update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "geo_lat", $inv->lat); - update_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "geo_lng", $inv->lng); - } else { - delete_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "geo_lat"); - delete_post_meta($post->ID, TouchPointWP::SETTINGS_PREFIX . "geo_lng"); - } - // Handle Resident Code - if (property_exists($inv, "resCodeName") && $inv->resCodeName !== null) { - /** @noinspection PhpRedundantOptionalArgumentInspection */ - wp_set_post_terms($post->ID, [$inv->resCodeName], TouchPointWP::TAX_RESCODE, false); - } else { - /** @noinspection PhpRedundantOptionalArgumentInspection */ - wp_set_post_terms($post->ID, [], TouchPointWP::TAX_RESCODE, false); + ////////////////// + //// Meetings //// + ////////////////// + + $postsToKeep = self::updateMeetingsForInvolvement($post, $inv, $typeSets, $imageId, $verbose); + + if ($verbose) { + echo "
"; + } + + unset($post); + + return $postsToKeep; + } + + /** + * @param \WP_Post|object $post The parent post, which could be a group or Meeting. + * @param object $inv The involvement object from the API. + * @param Involvement_PostTypeSettings $typeSets + * @param int $imagePostId + * @param bool $verbose + * + * @return int[] An array of Post IDs that have been updated, and which should be retained. + */ + protected static function updateMeetingsForInvolvement( + object $post, object $inv, Involvement_PostTypeSettings $typeSets, + int $imagePostId, bool $verbose = false): array + { + + // Return if meetings shouldn't be imported at all. + if (!$typeSets->importMeetings && !$inv->showInSites) { + self::doMeetingMetaUpdates($post, null, false, $verbose); + return [$post->ID]; + } + + //////////////////////// + // Determine Strategy // + //////////////////////// + + $strategy = match (count($inv->meetings)) { + 0 => self::MEETING_STRATEGY_NONE, + 1 => self::MEETING_STRATEGY_SINGLE, + default => self::MEETING_STRATEGY_MULTIPLE, + }; + + //////////////////// + // Title and Slug // + //////////////////// + + $slugStrategy = []; + $slugFormats = [ // Define possible slugs, in increasing specificity. + //'Y', Causes issues with Core. https://wordpress.stackexchange.com/a/367757/185189 + 'Y-m', + 'Y-m-d', + 'Y-m-d-g', + 'Y-m-d-ga', + 'Y-m-d-gia', + 'Y-m-d-His', + ]; + if ($strategy === self::MEETING_STRATEGY_SINGLE) { + $inv->titleToUse = $inv->meetings[0]->name ?? $inv->titleToUse; + } elseif ($strategy === self::MEETING_STRATEGY_MULTIPLE) { + foreach ($inv->meetings as $mtgO) { + foreach ($slugFormats as $f) { + $s = $mtgO->mtgStartDt->format($f); + if (!isset($slugStrategy[$s])) { + $slugStrategy[$s] = 1; + } else { + $slugStrategy[$s]++; + } } } + } + $postsToKeep = []; + + if ($strategy === self::MEETING_STRATEGY_MULTIPLE) { + + // If the main post was previously a single, it needs to have the meeting info removed. + self::doMeetingMetaUpdates($post, null, false, $verbose); + + foreach ($inv->meetings as $mtgO) { + + //////////////////// + // Title and Slug // + //////////////////// + + $title = $mtgO->name ?? $inv->titleToUse; + $slug = $mtgO->mtgId; // Default slug of the meeting ID -- collision-safe. + foreach ($slugFormats as $f) { + $s = $mtgO->mtgStartDt->format($f); + if ($slugStrategy[$s] == 1) { + $slug = $s; + break; + } + } - //////////////// - //// Campus //// - //////////////// - if (TouchPointWP::instance()->settings->enable_campuses === "on") { - if (property_exists($inv, "campusName") && $inv->campusName !== null) { - /** @noinspection PhpRedundantOptionalArgumentInspection */ - wp_set_post_terms($post->ID, [$inv->campusName], TouchPointWP::TAX_CAMPUS, false); + ///////////////////////// + // Find or Create Post // + ///////////////////////// + + $loops = 1; + do { + $mtgP = new WP_Query([ + 'post_type' => $typeSets->postTypeWithPrefix(), + 'post_name' => $slug, + 'post_parent' => $post->ID, + 'posts_per_page' => 10, + 'numberposts' => 10, + 'meta_key' => Meeting::MEETING_META_KEY, + 'meta_value' => $mtgO->mtgId, + 'meta_compare' => '=' + ]); + + $counts = $mtgP->post_count; + $mtgP = $mtgP->get_posts(); + + if ($counts > 1) { // multiple posts match, which isn't great. + new TouchPointWP_Exception("Multiple Posts Exist, Attempting to Remedy Automatically", 170007); + if ($verbose) { + echo "

Multiple Posts Exist. An attempt will be made to remove them.

"; + } + + for ($i = 1; $i <= $counts; $i++) { + wp_delete_post($mtgP[$i]->ID, true); + } + } + $loops++; + } while ($counts > 1 && $loops < 3); + + $eventIsPast = ($mtgO->mtgEndDt ?? $mtgO->mtgStartDt) < Utilities::dateTimeNow(); + + if ($counts > 0) { // post exists already. + $mtgP = reset($mtgP); + } elseif ($eventIsPast) { + if ($verbose) { + echo "

Post not found for Meeting $mtgO->mtgId. As it is in the past, it will not be created.

"; + } + continue; } else { - /** @noinspection PhpRedundantOptionalArgumentInspection */ - wp_set_post_terms($post->ID, [], TouchPointWP::TAX_CAMPUS, false); + if ($verbose) { + echo "

Post not found for Meeting $mtgO->mtgId. Creating.

"; + } + // create new + $mtgP = wp_insert_post([ + 'post_type' => $typeSets->postTypeWithPrefix(), + 'post_title' => $title, + 'post_name' => $slug, + 'post_parent' => $post->ID, + 'post_status' => 'publish', + 'meta_input' => [ + Meeting::MEETING_META_KEY => $mtgO->mtgId + ] + ]); + $mtgP = get_post($mtgP); } - } + $mtgP->post_title = $title; + if (!$eventIsPast) { + $mtgP->post_content = Utilities::standardizeHtml($inv->description, "meeting-import"); + } + $mtgP->post_parent = $post->ID; + + self::doMeetingMetaUpdates($mtgP, $mtgO, !!$inv->showInSites, $verbose); - ///////////////////// - //// Demographic //// - ///////////////////// + wp_update_post($mtgP); - // Handle Marital Status - $maritalTax = []; - if ($inv->marital_denom > 4) { // only include involvements with at least 4 people with known marital statuses. - $marriedProportion = (float)$inv->marital_married / $inv->marital_denom; - if ($marriedProportion > 0.7) { - $maritalTax[] = "mostly_married"; - } elseif ($marriedProportion < 0.3) { - $maritalTax[] = "mostly_single"; + if ($mtgP->post_name !== $slug) { + Utilities::forceSlugUpdate($mtgP->ID, $slug); } + + $postsToKeep[] = $mtgP->ID; } - /** @noinspection PhpRedundantOptionalArgumentInspection */ - wp_set_post_terms($post->ID, $maritalTax, TouchPointWP::TAX_INV_MARITAL, false); + } else { // Single and None + $post->post_title = $inv->titleToUse; - // Handle Age Groups - if ($inv->age_groups === null) { - /** @noinspection PhpRedundantOptionalArgumentInspection */ - wp_set_post_terms($post->ID, [], TouchPointWP::TAX_AGEGROUP, false); - } else { - /** @noinspection PhpRedundantOptionalArgumentInspection */ - wp_set_post_terms($post->ID, $inv->age_groups, TouchPointWP::TAX_AGEGROUP, false); + // TODO resolve $post declarations. + // TODO resolve Undefined array key 0 warning + // TODO make sure synced + if ($strategy === self::MEETING_STRATEGY_SINGLE) { + self::doMeetingMetaUpdates($post, $inv->meetings[0], !!$inv->showInSites, $verbose); + } else { // MEETING_STRATEGY_NONE + self::doMeetingMetaUpdates($post, null, false, $verbose); } + wp_update_post($post); + + $postsToKeep[] = $post->ID; + } - /////////////////// - //// Divisions //// - /////////////////// + return $postsToKeep; + } - // Handle divisions - $divs = []; - if ($inv->divs !== null) { - foreach ($inv->divs as $d) { - $tid = TouchPointWP::getDivisionTermIdByDivId($d); - if ( ! ! $tid) { - $divs[] = $tid; - } - } + /** + * Update meta fields specific to Meetings, such as start/end date/time. + * + * (This is included in this class and not in Meeting because it's a part of the Involvement import process and + * the protected access is appropriate.) + * + * @param WP_Post $mtgP + * @param ?object $mtgO + * @param bool $feature + * @param bool $verbose + * + * @return void + */ + protected static function doMeetingMetaUpdates(WP_Post $mtgP, ?object $mtgO, bool $feature, bool $verbose = false): void + { + // If the main post was previously a single, it needs to have the meeting info removed. + if ($mtgO === null) { + delete_post_meta($mtgP->ID, Meeting::MEETING_META_KEY); + delete_post_meta($mtgP->ID, Meeting::MEETING_START_META_KEY); + delete_post_meta($mtgP->ID, Meeting::MEETING_END_META_KEY); + delete_post_meta($mtgP->ID, Meeting::MEETING_FEAT_META_KEY); + delete_post_meta($mtgP->ID, Meeting::MEETING_INV_ID_META_KEY); + delete_post_meta($mtgP->ID, Meeting::MEETING_STATUS_META_KEY); + } else { + $eventIsPast = ($mtgO->mtgEndDt ?? $mtgO->mtgStartDt) < Utilities::dateTimeNow(); + + update_post_meta($mtgP->ID, Meeting::MEETING_META_KEY, $mtgO->mtgId); + update_post_meta($mtgP->ID, Meeting::MEETING_START_META_KEY, DateFormats::timestampWithoutOffset($mtgO->mtgStartDt)); + update_post_meta($mtgP->ID, Meeting::MEETING_END_META_KEY, DateFormats::timestampWithoutOffset($mtgO->mtgEndDt)); + update_post_meta($mtgP->ID, Meeting::MEETING_FEAT_META_KEY, !!$feature); + update_post_meta($mtgP->ID, Meeting::MEETING_INV_ID_META_KEY, $mtgO->involvementId); + update_post_meta($mtgP->ID, Meeting::MEETING_STATUS_META_KEY, intval($mtgO->status)); + + if ($mtgO->location !== null && !$eventIsPast) { + update_post_meta($mtgP->ID, Meeting::MEETING_LOCATION_META_KEY, $mtgO->location); } - /** @noinspection PhpRedundantOptionalArgumentInspection */ - wp_set_post_terms($post->ID, $divs, TouchPointWP::TAX_DIV, false); if ($verbose) { - echo "

Division Terms:

"; - var_dump($divs); + $link = get_permalink($mtgP); + echo "

Updating Post $mtgP->ID based on Meeting $mtgO->mtgId.

"; } + } + } - $postsToKeep[] = $post->ID; + /** + * @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); + } - if ($verbose) { - echo "
"; + // 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; + } + } - unset($post); + // 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]); + } } - unset($inv); + 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); + } catch (Exception) { + unset($inv->meetings[$i]); + } - ////////////////// - //// Removals //// - ////////////////// + if ($m->name == null || trim($m->name) === "") { + $m->name = null; + } - // Delete posts that are no longer current - $q = new WP_Query([ - 'post_type' => $typeSets->postType, - 'nopaging' => true, - 'post__not_in' => $postsToKeep - ]); - $removals = 0; - foreach ($q->get_posts() as $post) { - set_time_limit(10); - wp_delete_post($post->ID, true); - $removals++; + $m->involvementId = $inv->involvementId; } - return $removals + count($invData); + // 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 +3348,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; @@ -2611,7 +3379,7 @@ public static function filterAuthor($author): string $i = Involvement::fromPost($post); $author = $i->leaders()->__toString(); - } catch (TouchPointWP_Exception $e) { + } catch (TouchPointWP_Exception) { } } @@ -2628,14 +3396,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 +3434,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 +3457,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 +3478,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 { @@ -2744,62 +3527,62 @@ 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.) + * here.) * * @return string[] */ public function notableAttributes(array $exclude = []): array { - $r = []; - - $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); } 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 +3593,128 @@ public function notableAttributes(array $exclude = []): array } } - return apply_filters(TouchPointWP::HOOK_PREFIX . "involvement_attributes", $r, $this); + $attrs = $this->processAttributeExclusions($attrs, $exclude); + + /** + * 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 string[] $attrs The list of notable attributes. + * @param Involvement $this The Involvement object. + */ + return apply_filters("tp_involvement_attributes", $attrs, $this); } /** * 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 ($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\""; } - if ($this->acceptingNewMembers() === true) { - if ($this->useRegistrationForm()) { + 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 +3739,82 @@ 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 apply_filters(TouchPointWP::HOOK_PREFIX . "involvement_actions", $ret, $this, $context, $btnClass); + return null; } + /** + * Get the JS for instantiation. + * + * @return string + */ public static function getJsInstantiationString(): string { $queue = static::getQueueForJsInstantiation(); @@ -2909,7 +3826,7 @@ public static function getJsInstantiationString(): string $listStr = json_encode($queue); return "\ttpvm.addEventListener('Involvement_class_loaded', function() { - TP_Involvement.fromObjArray($listStr);\n\t});\n"; + TP_Involvement.fromObjArray($listStr);\n\t});\n"; } public function getTouchPointId(): int @@ -2917,6 +3834,18 @@ 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; + } + /** * Handles the API call to join an involvement through a 'join' button. */ @@ -2951,6 +3880,12 @@ private static function ajaxInvJoin(): void exit; } + try { + $stats = Stats::instance(); + $stats->involvementJoins += count($data->success); + $stats->updateDb(); + } catch (Exception) {} + echo json_encode(['success' => $data->success]); exit; } @@ -2965,8 +3900,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); } /** @@ -3029,7 +3986,23 @@ private static function ajaxContact(): void 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..1c3aa0cf 100644 --- a/src/TouchPoint-WP/Involvement_PostTypeSettings.php +++ b/src/TouchPoint-WP/Involvement_PostTypeSettings.php @@ -19,9 +19,11 @@ * @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 $groupBy * @property-read string[] $excludeIf * @property-read string[] $leaderTypes @@ -42,9 +44,11 @@ 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 $groupBy = ""; protected array $excludeIf = []; protected array $leaderTypes = []; @@ -62,7 +66,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 +74,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 +113,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 +139,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(); @@ -167,11 +212,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 +259,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 +294,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 +328,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 +335,7 @@ protected static function memberTypesToInts($memberTypes): array */ public function leaderTypeInts(): array { - return self::memberTypesToInts($this->leaderTypes); + return Utilities::idArrayToIntArray($this->leaderTypes); } /** @@ -317,6 +349,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..393c198c 100644 --- a/src/TouchPoint-WP/Location.php +++ b/src/TouchPoint-WP/Location.php @@ -9,14 +9,14 @@ * 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) @@ -79,25 +79,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 +114,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 +132,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 +141,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..9b9052b2 100644 --- a/src/TouchPoint-WP/Meeting.php +++ b/src/TouchPoint-WP/Meeting.php @@ -11,16 +11,241 @@ if ( ! TOUCHPOINT_COMPOSER_ENABLED) { require_once 'api.php'; + require_once 'hierarchical.php'; + require_once 'scheduled.php'; } +use DateTime; +use DateTimeImmutable; use Exception; +use tp\TouchPointWP\Utilities\DateFormats; +use tp\TouchPointWP\Utilities\StringableArray; +use WP_Post; use tp\TouchPointWP\Utilities\Http; +use WP_Query; +use WP_Term; /** * Handle meeting content, particularly RSVPs. */ -abstract class Meeting implements api, module +class Meeting extends PostTypeCapable implements api, module, hasGeo, hierarchical, scheduled { + 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 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 +266,345 @@ 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 + ]); + } + 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); + } + $parent = get_post_parent($this->post_id); + if (Involvement::postIsType($parent)) { + return Involvement::fromPost(get_post($parent)); + } + 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 + */ + public function getParent(): ?Involvement + { + 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 $exclude Attributes listed here will be excluded. (e.g. if shown for a parent, not needed here.) + * + * @return string[] + */ + public function notableAttributes(array $exclude = []): array + { + if (in_array('involvement', $exclude)) { + $attrs = []; + } else { + try { + $attrs = $this->involvement()->notableAttributes(['date', 'datetime', 'time', 'firstLast']); + } catch (TouchPointWP_Exception) { + $attrs = []; + } + } + + $d = $this->scheduleStringArray(); + + $attrs = [...$d, ...$attrs]; + + $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->endDt ?? $this->startDt) < Utilities::dateTimeNow()) { + $attrs['past'] = __("In the Past", "TouchPoint-WP"); + } + } + + $loc = $this->locationName(); + if ($loc) { + $attrs['location'] = $loc; + } + + + + $attrs = $this->processAttributeExclusions($attrs, $exclude); + + /** + * 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 + * + * @param string[] $attrs The list of notable attributes. + * @param Meeting $this The Meeting object. + */ + return apply_filters("tp_meeting_attributes", $attrs, $this); + } + + /** + * @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, false, $absoluteLinks, false); + + if ($this->status() !== self::STATUS_CANCELLED) { + if (($this->endDt ?? $this->startDt) > Utilities::dateTimeNow()) { + $ret['register'] = $inv->getRegisterButton($btnClass, $absoluteLinks, $this); + } + + if ($inv->getRegistrationType() === RegistrationType::RSVP) { + if ($absoluteLinks) { + $ret['register'] = $this->getRsvpLink($btnClass); + } else { + $ret['register'] = $this->getRsvpButton($btnClass); + } + } + } + + if ($withTouchPointLink && TouchPointWP::currentUserIsAdmin()) { + $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 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,16 +628,43 @@ 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() { +// 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); } @@ -94,7 +685,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()]); @@ -139,7 +730,223 @@ private static function ajaxSubmitRsvps(): void 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; + } + + public static function load(): bool + { + if (self::$_isLoaded) { + return true; + } + + self::$_isLoaded = true; + + add_action(TouchPointWP::INIT_ACTION_HOOK, [self::class, 'init']); + + 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 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/Partner.php b/src/TouchPoint-WP/Partner.php index 2b0e7d77..275c4851 100644 --- a/src/TouchPoint-WP/Partner.php +++ b/src/TouchPoint-WP/Partner.php @@ -18,6 +18,8 @@ use Exception; use JsonSerializable; +use stdClass; +use tp\TouchPointWP\Utilities\StringableArray; use WP_Error; use WP_Post; use WP_Query; @@ -26,10 +28,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 +62,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 +80,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 +117,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 +150,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); } } @@ -290,6 +292,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; @@ -332,6 +343,7 @@ public static function updateFromTouchPoint(bool $verbose = false) $postsToKeep = []; $count = 0; + $termsToKeep = []; foreach ($familyData->people as $f) { /** @var object $f */ @@ -372,13 +384,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 +406,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 +422,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 +482,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 +522,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 +547,8 @@ public static function updateFromTouchPoint(bool $verbose = false) if ($count > 0) { TouchPointWP::instance()->flushRewriteRules(); } + + TouchPointWP::instance()->unsetTpWpUserAsCurrent(); return $count; } @@ -519,9 +563,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 +691,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, @@ -731,7 +807,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 +828,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 .= ""; + $content .= ""; + $content .= ""; + foreach (get_terms(Taxonomies::TAX_GP_CATEGORY) as $t) { + $content .= ""; + } + $content .= ""; } - $content .= ""; } if ($params['includeMapWarnings']) { @@ -1074,7 +1152,6 @@ public static function mapShortcode($params = [], string $content = ""): string $script ); - // TODO move the style to a css file... or something. $content = "
"; } else { $content = ""; @@ -1151,7 +1228,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 +1245,48 @@ public static function getFamEvAsContent(string $ev, object $famObj, ?string $de /** * Get notable attributes as strings. * + * @param array $exclude Attributes listed here will be excluded. (e.g. if shown for a parent, not needed here.) + * * @return string[] */ - public function notableAttributes(): array + public function notableAttributes(array $exclude = []): array { $r = []; + $l = $this->locationName(); if ($this->decoupleLocation) { - $r[] = TouchPointWP::instance()->settings->global_name_singular_decoupled; - } elseif ($this->location !== "" && $this->location !== null) { - $r[] = $this->location; + $r['secure'] = $l; + } elseif ($l) { + $r['location'] = $l; } + unset($l); foreach ($this->category as $c) { - $r[] = $c->name; + $r['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"); + $r['hidden'] = __("Not Shown on Map", "TouchPoint-WP"); TouchPointWP::requireScript("fontAwesome"); // For map icons } - return apply_filters(TouchPointWP::HOOK_PREFIX . "partner_attributes", $r, $this); + $r = $this->processAttributeExclusions($r, $exclude); + + /** + * 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 + * + * @param string[] $attrs The list of notable attributes. + * @param Partner $this The Partner object. + */ + return apply_filters("tp_partner_attributes", $r, $this); } /** @@ -1207,28 +1308,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. + * @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 @@ -1324,6 +1444,18 @@ 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; + } + /** * Serialize. Mostly, manage the security requirements. * @@ -1349,7 +1481,7 @@ public function jsonSerialize(): object } } - return $this; + return $this->jsonSerializeTrait(); } /** @@ -1366,17 +1498,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..8182216b 100644 --- a/src/TouchPoint-WP/Person.php +++ b/src/TouchPoint-WP/Person.php @@ -27,6 +27,7 @@ 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; @@ -41,7 +42,7 @@ * @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. + * 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 @@ -105,7 +106,7 @@ 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' ]; @@ -435,7 +436,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 ]; } @@ -677,6 +678,8 @@ protected static function updateFromTouchPoint(bool $verbose = false) $queryNeeded = false; $verbose &= TouchPointWP::currentUserIsAdmin(); + + TouchPointWP::instance()->setTpWpUserAsCurrent(); // Existing Users /** @noinspection SqlResolve */ @@ -707,7 +710,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 +726,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() ); @@ -781,6 +784,8 @@ protected static function updateFromTouchPoint(bool $verbose = false) } } + TouchPointWP::instance()->unsetTpWpUserAsCurrent(); + return $count; } @@ -948,11 +953,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. @@ -1135,7 +1140,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 +1159,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; } @@ -1168,11 +1173,11 @@ protected function resCode(): ?WP_Term * * @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. + * elements. * * @return string */ - public function getActionButtons(string $context = null, string $btnClass = ""): string + public function getActionButtons(string $context = null, string $btnClass = "", bool $withTouchPointLink = true): string { TouchPointWP::requireScript('swal2-defer'); TouchPointWP::requireScript('base-defer'); @@ -1182,15 +1187,36 @@ public function getActionButtons(string $context = null, string $btnClass = ""): $btnClass = " class=\"$btnClass\""; } - $ret = ""; + $ret = new StringableArray(); if (self::allowContact()) { $text = __("Contact", "TouchPoint-WP"); TouchPointWP::enqueueActionsStyle('person-contact'); self::enqueueUsersForJsInstantiation(); - $ret = " "; - } - - return apply_filters(TouchPointWP::HOOK_PREFIX . "person_actions", $ret, $this, $context, $btnClass); + $ret[] = " "; + } + + if ($withTouchPointLink && TouchPointWP::currentUserIsAdmin()) { + // Translators: %s is the system name. "TouchPoint" by default. + $title = sprintf(__("Person in %s", "TouchPoint-WP"), TouchPointWP::instance()->settings->system_name); + $logo = TouchPointWP::TouchPointIcon(); + $ret[] = "getProfileUrl()}\" title=\"$title\" class=\"tp-TouchPoint-logo $btnClass\">$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 + * + * @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. + */ + return apply_filters("tp_person_actions", $ret, $this, $context, $btnClass); } /** @@ -1330,6 +1356,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 +1382,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 +1443,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 +1481,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 +1494,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 +1587,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); @@ -1525,25 +1609,14 @@ private static function ajaxIdent(): void /** * Make the API call to get family members, store the results to the Session, 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 * * @return array */ 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); @@ -1559,6 +1632,12 @@ public static function ident($inputData): array $s = Session::instance(); + try { + $stats = Stats::instance(); + $stats->softAuths += count($people); + $stats->updateDb(); + } catch (Exception) {} + $ret = []; $primaryFam = $s->primaryFam ?? []; $secondaryFam = $s->secondaryFam ?? []; @@ -1605,9 +1684,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; } @@ -1636,10 +1715,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 { @@ -1699,8 +1793,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 +1846,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; } diff --git a/src/TouchPoint-WP/PostTypeCapable.php b/src/TouchPoint-WP/PostTypeCapable.php new file mode 100644 index 00000000..8128e413 --- /dev/null +++ b/src/TouchPoint-WP/PostTypeCapable.php @@ -0,0 +1,132 @@ +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 $exclude Attributes listed here will be excluded. (e.g. if shown for a parent, not needed here.) + * + * @return string[] + */ + public abstract function notableAttributes(array $exclude = []): array; + + /** + * 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 array $subject + * @param array $exclude + * + * @return array + */ + protected function processAttributeExclusions(array $subject, array $exclude): array + { + $subject = array_diff($subject, $exclude); + foreach ($exclude as $e) { + if (isset($subject[$e])) { + unset($subject[$e]); + } + } + return $subject; + } + + /** + * @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 abstract function getActionButtons(string $context = null, string $btnClass = "", bool $withTouchPointLink = true, bool $absoluteLinks = false): StringableArray; + + /** + * 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; + + + /** + * 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 @@ +interval = min($this->interval, $params['interval'] ?? $this->interval); } @@ -96,7 +104,7 @@ 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); } @@ -138,7 +146,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. @@ -216,6 +224,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 +364,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 +375,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 +395,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 +406,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 +434,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"; return $rc; } @@ -323,25 +454,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 +479,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 +518,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,6 +559,8 @@ 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); $postIdsToNotDelete = []; @@ -474,12 +609,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 +652,8 @@ public static function updateFromTouchPoint(bool $forceEvenIfNotDue = false): in TouchPointWP::instance()->flushRewriteRules(); } + TouchPointWP::instance()->unsetTpWpUserAsCurrent(); + return $updateCount; } @@ -522,7 +665,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 +738,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 diff --git a/src/TouchPoint-WP/Rsvp.php b/src/TouchPoint-WP/Rsvp.php index 0ff7a04e..d3c2ca6e 100644 --- a/src/TouchPoint-WP/Rsvp.php +++ b/src/TouchPoint-WP/Rsvp.php @@ -5,18 +5,16 @@ 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. 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 +115,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/Stats.php b/src/TouchPoint-WP/Stats.php new file mode 100644 index 00000000..b00d5934 --- /dev/null +++ b/src/TouchPoint-WP/Stats.php @@ -0,0 +1,468 @@ + $value) { + if (property_exists($this, $key)) { + $this->$key = $value; + } + } + } + } + + if (empty($this->installId) || empty($this->privateKey)) { + $this->privateKey = Utilities::createGuid(); + $this->installId = Utilities::createGuid(); + $this->_dirty = true; + } + + $sid = get_site_option('tp_siteId', null); + if (empty($sid)) { + $sid = Utilities::createGuid(); + update_site_option('tp_siteId', $sid); + } + $this->siteId = $sid; + + $this->updateDb(); + } + + /** + * Make sure saves have happened before the object is destroyed. + * + * @throws Exception + */ + protected function __destruct() + { + if ($this->_dirty) { + throw new Exception("Stats object was not saved."); + } + } + + /** + * Loads the module and initializes the other actions. + * + * @return bool + */ + public static function load(): bool + { + ////////////////// + /// Shortcodes /// + ////////////////// + + + /////////////// + /// Syncing /// + /////////////// + + // Setup cron for calling home weekly. + add_action(self::CRON_HOOK, [self::class, 'updateCron']); + if ( ! wp_next_scheduled(self::CRON_HOOK)) { + // Runs at 4am EST (9am UTC) + wp_schedule_event( + date('U', strtotime('tomorrow') + 3600 * 9), + 'weekly', + self::CRON_HOOK + ); + } + + return true; + } + + /** + * Call home, triggered by migration. + * + * @return void + */ + public static function migrate(): void + { + self::instance()->submitStats(); + } + + /** + * Call home, triggered by cron. + * + * @return void + */ + public static function updateCron(): void + { + $i = self::instance(); + $i->replacePrivateKey(); + $i->submitStats(); + } + + /** + * Check to see if a cron run is needed, and run it if so. Connected to an init function. + * + * @return void + */ + public static function checkUpdates() + { + // This method does nothing because the overhead is relatively great, and should not be hooked to every page load. + } + + /** + * Save the stats to the database. + * + * @return bool true on success (or if an update wasn't needed), false on failure. + */ + public function updateDb(): bool + { + if ($this->_dirty) { + $d = $this->jsonSerialize(); + unset($d['siteId']); + $r = update_option('tp_wp_stats', json_encode($d)); + if ($r) { + $this->_dirty = false; + } + return $r; + } + return true; + } + + /** + * Setter. Allows particular statistics to be set. + * + * @param $name + * @param $value + * + * @throws InvalidArgumentException + * + * @return void + */ + public function __set($name, $value) + { + if (in_array($name, ['privateKey', 'siteId', 'installId'])) { + throw new InvalidArgumentException("Cannot set $name directly."); + } + + if (property_exists($this, $name)) { + if ($this->$name !== $value) { + $this->$name = $value; + $this->_dirty = true; + } + } + } + + /** + * @param string $name + * + * @return void + * + * @throws InvalidArgumentException + */ + public function __get(string $name) + { + if (property_exists($this, $name) && !str_starts_with($name, '_')) { + return $this->$name; + } + } + + /** + * Assemble the information that's submitted. + * + * @param bool $updateQueried + * + * @return array + */ + public function getStatsForSubmission(bool $updateQueried = false): array + { + if ($updateQueried) { + $this->updateQueriedStats(); + } + + $data = $this->jsonSerialize(); + + $sets = TouchPointWP::instance()->settings; + + $data['site'] = get_site_url(); + $data['plugin'] = 'TouchPointWP'; + $data['version'] = TouchPointWP::VERSION; + $data['php'] = phpversion(); + $data['wp'] = get_bloginfo('version'); + $data['wpLocale'] = get_locale(); + $data['wpTimezone'] = get_option('timezone_string'); + $data['adminEmail'] = get_option('admin_email'); + $data['siteName'] = get_bloginfo('name'); + $data['siteLogoUrl'] = get_theme_mod('custom_logo'); + if ($data['siteLogoUrl']) { + $data['siteLogoUrl'] = esc_url(wp_get_attachment_image_src($data['siteLogoUrl'], 'full')[0]); + } else { + $data['siteLogoUrl'] = ''; + } + $data['listPublicly'] = 1 * ($sets->enable_public_listing === 'on'); + $data['installId'] = $this->installId; + $data['privateKey'] = $this->privateKey; + + return $data; + } + + /** + * Submit stats to Tenth. + * + * @return void + */ + protected function submitStats(): void + { + $this->updateQueriedStats(); + + $data = $this->getStatsForSubmission(); + + $endpoint = self::SUBMISSION_ENDPOINT; + + /** + * This plugin is designed to be used by other churches, but to help troubleshoot and understand usage, some + * basic statistics are sent back to Tenth. This filter allows you to change the endpoint to which the data is + * sent, which may be necessary if you have a proxy system setup. It also allows you to disable the sending of + * all information back to Tenth by setting the value to an empty string. + * + * The URL must use https. + * + * @since 0.0.96 Added + * + * @param string $endpoint The endpoint value to use. + */ + $endpoint = (string)apply_filters('tp_stats_endpoint', $endpoint); + + if ( ! str_starts_with($endpoint, 'https://')) { + return; + } + + wp_remote_post($endpoint, [ + 'body' => ['data' => $data], + 'timeout' => 10, + 'blocking' => false, + ]); + echo "ok"; + } + + /** + * Assemble the object into a format that can be serialized to JSON. Only includes + * the parameters that are part of this class, not those that are loaded from the database separately, + * such as version numbers. + * + * @inheritDoc + */ + public function jsonSerialize(): array + { + $r = []; + + // all properties that don't start with an underscore + foreach (get_object_vars($this) as $key => $value) { + if ( ! str_starts_with($key, '_')) { + $r[$key] = $value; + } + } + + return $r; + } + + /** + * Replace the private key with a new one. Should be done periodically. + * + * @return void + */ + protected function replacePrivateKey(): void + { + $this->privateKey = Utilities::createGuid(); + $this->_dirty = true; + $this->updateDb(); + } + + /** + * Update the stats that are determined from queries. + * + * @return void + */ + protected function updateQueriedStats(): void + { + global $wpdb; + + $this->involvementPosts = $wpdb->get_var("SELECT COUNT(DISTINCT meta_value) as c FROM $wpdb->postmeta WHERE meta_key = 'tp_invId'") ?? -1; + $this->reportPosts = $wpdb->get_var("SELECT COUNT(*) as c FROM $wpdb->posts WHERE post_type = 'tp_report'") ?? -1; + $this->meetings = $wpdb->get_var("SELECT COUNT(DISTINCT meta_value) as c FROM $wpdb->postmeta WHERE meta_key = 'tp_mtgId'") ?? -1; + $this->people = $wpdb->get_var("SELECT COUNT(DISTINCT meta_value) as c FROM $wpdb->usermeta WHERE meta_key = 'tp_peopleId';") ?? -1; + $this->partnerPosts = $wpdb->get_var("SELECT COUNT(*) as c FROM $wpdb->posts WHERE post_type = 'tp_partner'") ?? -1; + + $this->_dirty = true; + } + + /** + * Get the singleton. + * + * @return Stats + */ + public static function instance(): Stats + { + if (self::$instance === null) { + self::$instance = new self(); + } + return self::$instance; + } + + /** + * 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. + */ + public static function api(array $uri): bool + { + if (count($uri['path']) !== 3) { + return false; + } + + $s = self::instance(); + + switch (strtolower($uri['path'][2])) { + case "get": + if (strtolower($_GET['key']) == strtolower($s->privateKey) || + current_user_can('manage_options')) { + header('Content-Type: application/json'); + $s->updateQueriedStats(); + echo json_encode($s->getStatsForSubmission()); + exit; + } + + case "submit": + if ($_SERVER['REQUEST_METHOD'] === "POST") { + self::handleSubmission(); + } else { + $s->submitStats(); + } + exit; + + } + + return false; + } + + /** + * Handle submissions received to this site (presumably tenth.org) from other users of the plugin. + * + * @return bool True on success + */ + public static function handleSubmission(): bool + { + + if ($_SERVER['REQUEST_METHOD'] !== "POST") { + http_response_code(Http::METHOD_NOT_ALLOWED); + echo "Only POST requests are allowed."; + return false; + } + + $data = $_POST['data'] ?? null; + + if (empty($data)) { + http_response_code(Http::BAD_REQUEST); + echo "No data was submitted."; + return false; + } + + // validate that privateKey, installId, and siteId are all included. + if ( ! isset($data['privateKey']) || ! isset($data['installId']) || ! isset($data['siteId'])) { + http_response_code(Http::BAD_REQUEST); + echo "Keys not provided."; + return false; + } + + // remove any fields that are not part of the stats object. + $s = self::instance(); + $data = array_intersect_key($data, $s->getStatsForSubmission()); + $data['updatedDT'] = date('Y-m-d H:i:s'); + + // upsert the data into the database into the stats table without destructive replace function + global $wpdb; + $r = $wpdb->update($wpdb->prefix . TouchPointWP::TABLE_STATS, $data, ['installId' => $data['installId']]); + if ($r < 1) { + $r = $wpdb->insert($wpdb->prefix . TouchPointWP::TABLE_STATS, $data); + } + + if ($r === false) { + http_response_code(Http::SERVER_ERROR); + echo "Server error."; + return false; + } + + echo $r; + return true; + } +} \ No newline at end of file diff --git a/src/TouchPoint-WP/Taxonomies.php b/src/TouchPoint-WP/Taxonomies.php new file mode 100644 index 00000000..a0569bc5 --- /dev/null +++ b/src/TouchPoint-WP/Taxonomies.php @@ -0,0 +1,921 @@ + $singular, + 'singular_name' => $plural, + /* translators: %s: taxonomy name, plural */ + 'search_items' => sprintf(__('Search %s', 'TouchPoint-WP'), $plural), + /* translators: %s: taxonomy name, plural */ + 'all_items' => sprintf(__('All %s', 'TouchPoint-WP'), $plural), + /* translators: %s: taxonomy name, singular */ + 'edit_item' => sprintf(__('Edit %s', 'TouchPoint-WP'), $singular), + /* translators: %s: taxonomy name, singular */ + 'update_item' => sprintf(__('Update %s', 'TouchPoint-WP'), $singular), + /* translators: %s: taxonomy name, singular */ + 'add_new_item' => sprintf(__('Add New %s', 'TouchPoint-WP'), $singular), + /* translators: %s: taxonomy name, singular */ + 'new_item_name' => sprintf(__('New %s', 'TouchPoint-WP'), $singular), + 'menu_name' => $plural + ]; + } + + /** + * For the taxonomies that are based on Lookups in the TouchPoint database, insert or update the terms. Also + * removes any items that are no longer present in the list. + * + * @param string[] $list where the slug is the key and the name is the value. + * @param string $taxonomy + * @param bool $forceIdUpdate + * + * @return void + */ + public static function insertTermsForArrayBasedTaxonomy(array $list, string $taxonomy, bool $forceIdUpdate) + { + $existingIds = []; + foreach ($list as $slug => $name) { + // In addition to making sure term exists, make sure it has the correct meta id, too. + $term = self::termExists($name, $taxonomy); + $idUpdate = $forceIdUpdate; + if ( ! $term) { + $term = self::insertTerm( + $name, + $taxonomy, + [ + 'description' => $name, + 'slug' => $slug + ] + ); + if (is_wp_error($term)) { + new TouchPointWP_WPError($term); + $term = null; + } + if ($idUpdate) { + TouchPointWP::queueFlushRewriteRules(); + } + } + if ( ! ! $term) { + $existingIds[] = $term['term_id']; + } + } + + // Delete any terms that are no longer current. + $terms = get_terms(['taxonomy' => $taxonomy, 'hide_empty' => false, 'exclude' => $existingIds]); + if (!is_wp_error($terms)) { + foreach ($terms as $term) { + wp_delete_term($term->term_id, $taxonomy); + } + } + } + + /** + * For the taxonomies that are based on Lookups in the TouchPoint database, insert or update the terms. Also + * removes any items that are no longer present in the list. + * + * @param object[] $list + * @param string $taxonomy + * @param bool $forceIdUpdate + * + * @return void + */ + public static function insertTermsForLookupBasedTaxonomy(array $list, string $taxonomy, bool $forceIdUpdate) + { + $existingIds = []; + foreach ($list as $i) { + if ($i->name === null) { + continue; + } + // In addition to making sure term exists, make sure it has the correct meta id, too. + $term = self::termExists($i->name, $taxonomy); + $idUpdate = $forceIdUpdate; + if ( ! $term) { + $term = self::insertTerm( + $i->name, + $taxonomy, + [ + 'description' => $i->name, + 'slug' => sanitize_title($i->name) + ] + ); + if (is_wp_error($term)) { + new TouchPointWP_WPError($term); + $term = null; + } + $idUpdate = true; + } + // Update the term meta if term is new, or if id update is forced. + if ($term !== null && isset($term['term_id']) && $idUpdate) { + update_term_meta($term['term_id'], self::TAXMETA_LOOKUP_ID, $i->id); + } + if ($idUpdate) { + TouchPointWP::queueFlushRewriteRules(); + } + if ( ! ! $term) { + $existingIds[] = $term['term_id']; + } + } + + // Delete any terms that are no longer current. + $terms = get_terms(['taxonomy' => $taxonomy, 'hide_empty' => false, 'exclude' => $existingIds]); + if (!is_wp_error($terms)) { + foreach ($terms as $term) { + wp_delete_term($term->term_id, $taxonomy); + } + } + } + + /** + * Get the term id for a given taxonomy and value. + * + * @param $taxonomy string Taxonomy name + * @param $value int|string The Lookup ID or name or slug of the term. + * + * @return ?int + * + * @since 0.0.32 Added + */ + public static function getTaxTermId(string $taxonomy, $value): ?int + { + $args = [ + 'taxonomy' => $taxonomy, + 'hide_empty' => false, + 'fields' => 'ids' + ]; + + if (is_numeric($value)) { + // by lookup id + $args['meta_key'] = self::TAXMETA_LOOKUP_ID; + $args['meta_value'] = $value; + } else { + // by name + $args['name'] = $value; + $t = get_terms($args); + if (count($t) > 0) { + return $t[0]; + } + + // by slug + unset($args['name']); + $args['slug'] = $value; + } + $t = get_terms($args); + if (count($t) > 0) { + return $t[0]; + } + + return null; + } + + /** + * Filter to add a tp_post_type option to get_terms that takes either a string of one post type or an array of post + * types. + * + * @param $clauses + * @param $taxonomy + * @param $args + * + * Hat tip https://dfactory.eu/wp-how-to-get-terms-post-type/ + * + * @return mixed + */ + public static function getTermsClauses($clauses, $taxonomy, $args): array + { + if (isset($args[TouchPointWP::HOOK_PREFIX . 'post_type']) && ! empty($args[TouchPointWP::HOOK_PREFIX . 'post_type']) && $args['fields'] !== 'count') { + global $wpdb; + + $post_types = []; + + if (is_array($args[TouchPointWP::HOOK_PREFIX . 'post_type'])) { + foreach ($args[TouchPointWP::HOOK_PREFIX . 'post_type'] as $cpt) { + $post_types[] = "'" . $cpt . "'"; + } + } else { + $post_types[] = "'" . $args[TouchPointWP::HOOK_PREFIX . 'post_type'] . "'"; + } + + if ( ! empty($post_types)) { + $clauses['fields'] = 'DISTINCT ' . str_replace( + 'tt.*', + 'tt.term_taxonomy_id, tt.taxonomy, tt.description, tt.parent', + $clauses['fields'] + ) . ', COUNT(p.post_type) AS count'; + $clauses['join'] .= ' LEFT JOIN ' . $wpdb->term_relationships . ' AS r ON r.term_taxonomy_id = tt.term_taxonomy_id LEFT JOIN ' . $wpdb->posts . ' AS p ON p.ID = r.object_id'; + $clauses['where'] .= ' AND (p.post_type IN (' . implode( + ',', + $post_types + ) . ') OR (tt.parent = 0 AND tt.count = 0))'; + $clauses['orderby'] = 'GROUP BY t.term_id ' . $clauses['orderby']; + } + } + + return $clauses; + } + + /** + * Insert the terms for the registered taxonomies. (This is supposed to happen a while after the taxonomies are + * loaded.) + * + * @param ?TouchPointWP|string $instance Default value for init call may be null or ''. + * + * @return void + */ + public static function insertTerms($instance = null): void + { + if (!isset($instance) || $instance === '') { + $instance = TouchPointWP::instance(); + } + + // Resident Codes + $types = self::getPostTypesForTaxonomy($instance, self::TAX_RESCODE); + if (count($types) > 0) { + self::insertTermsForLookupBasedTaxonomy( + $instance->getResCodes(), + self::TAX_RESCODE, + self::$forceTermLookupIdUpdate + ); + } + + // Campuses + $types = self::getPostTypesForTaxonomy($instance, self::TAX_CAMPUS); + if (count($types) > 0) { + if ($instance->settings->enable_campuses == "on") { + $campuses = $instance->getCampuses(); + } else { + $campuses = []; + } + + self::insertTermsForLookupBasedTaxonomy( + $campuses, + self::TAX_CAMPUS, + self::$forceTermLookupIdUpdate + ); + } + + // Age Groups + $types = self::getPostTypesForTaxonomy($instance, self::TAX_AGEGROUP); + if (count($types) > 0) { + $taxTerms = [ + "20" => "20s", + "30" => "30s", + "40" => "40s", + "50" => "50s", + "60" => "60s", + "70" => "70+" + ]; + self::insertTermsForArrayBasedTaxonomy( + $taxTerms, + self::TAX_AGEGROUP, + self::$forceTermLookupIdUpdate + ); + } + + // Divisions: Involvements and Events + $types = self::getPostTypesForTaxonomy($instance, self::TAX_DIV); + if (count($types) > 0) { + $existingIds = []; + $enabledDivisions = $instance->settings->dv_divisions; + foreach ($instance->getDivisions() as $d) { + if (!in_array('div' . $d->id, $enabledDivisions)) { + continue; + } + if (!$d->pName || !$d->dName) { + continue; + } + + // Program + $idUpdate = self::$forceTermLookupIdUpdate; + $pTermInfo = self::termExists($d->pName, self::TAX_DIV, 0); + if (! $pTermInfo) { + $pTermInfo = self::insertTerm( + $d->pName, + self::TAX_DIV, + [ + 'description' => $d->pName, + 'slug' => sanitize_title($d->pName) + ] + ); + $idUpdate = true; + if (is_wp_error($pTermInfo)) { + new TouchPointWP_WPError($pTermInfo); + $pTermInfo = null; + continue; + } + } + $proId = get_term_meta($pTermInfo['term_id'], TouchPointWP::SETTINGS_PREFIX . 'programId', true); + if ($idUpdate || $proId !== intval($d->proId)) { + update_term_meta( + $pTermInfo['term_id'], + TouchPointWP::SETTINGS_PREFIX . 'programId', + intval($d->proId) + ); + TouchPointWP::queueFlushRewriteRules(); + } + if ( !! $pTermInfo) { + $existingIds[] = $pTermInfo['term_id']; + } + + // Division + $idUpdate = self::$forceTermLookupIdUpdate; + $dTermInfo = self::termExists($d->dName, self::TAX_DIV, $pTermInfo['term_id']); + if (! $dTermInfo) { + $dTermInfo = self::insertTerm( + $d->dName, + self::TAX_DIV, + [ + 'description' => $d->dName, + 'slug' => sanitize_title($d->dName), + 'parent' => $pTermInfo['term_id'] + ] + ); + $idUpdate = true; + if (is_wp_error($dTermInfo)) { + new TouchPointWP_WPError($dTermInfo); + $dTermInfo = null; + continue; + } + } + $divId = get_term_meta($dTermInfo['term_id'], TouchPointWP::SETTINGS_PREFIX . 'divId', true); + if ($idUpdate || $divId !== intval($d->id)) { + update_term_meta( + $dTermInfo['term_id'], + TouchPointWP::SETTINGS_PREFIX . 'divId', + intval($d->id) + ); + TouchPointWP::queueFlushRewriteRules(); + } + if ( !! $dTermInfo) { + $existingIds[] = $dTermInfo['term_id']; + } + } + + // Delete any terms that are no longer current. + $terms = get_terms(['taxonomy' => self::TAX_DIV, 'hide_empty' => false, 'exclude' => $existingIds]); + if (!is_wp_error($terms)) { + foreach ($terms as $term) { + wp_delete_term($term->term_id, self::TAX_DIV); + } + } + } + + // Weekdays + $types = self::getPostTypesForTaxonomy($instance, self::TAX_WEEKDAY); + if (count($types) > 0) { + // Weekdays + $days = [ + 'sun' => 'Sundays', + 'mon' => 'Mondays', + 'tue' => 'Tuesdays', + 'wed' => 'Wednesdays', + 'thu' => 'Thursdays', + 'fri' => 'Fridays', + 'sat' => 'Saturdays', + ]; + self::insertTermsForArrayBasedTaxonomy( + $days, + self::TAX_WEEKDAY, + self::$forceTermLookupIdUpdate + ); + } + + // Tenses + $types = self::getPostTypesForTaxonomy($instance, self::TAX_TENSE); + if (count($types) > 0) { + $tenses = [ + self::TAX_TENSE_FUTURE => 'Upcoming', + self::TAX_TENSE_PRESENT => 'Current', + self::TAX_TENSE_PAST => 'Past', + ]; + self::insertTermsForArrayBasedTaxonomy( + $tenses, + self::TAX_TENSE, + self::$forceTermLookupIdUpdate + ); + } + + // Time of Day + $types = self::getPostTypesForTaxonomy($instance, self::TAX_DAYTIME); + if (count($types) > 0) { + // Time of Day + $timesOfDay = [ + 'latenight' => 'Late Night', + 'earlymorning' => 'Early Morning', + 'morning' => 'Morning', + 'midday' => 'Midday', + 'afternoon' => 'Afternoon', + 'evening' => 'Evening', + 'night' => 'Night' + ]; + self::insertTermsForArrayBasedTaxonomy( + $timesOfDay, + self::TAX_DAYTIME, + self::$forceTermLookupIdUpdate + ); + } + + // Marital Status + $types = self::getPostTypesForTaxonomy($instance, self::TAX_INV_MARITAL); + if (count($types) > 0) { + $maritalStatuses = [ + 'mostly_single' => 'Mostly Single', + 'mostly_married' => 'Mostly Married' + ]; + self::insertTermsForArrayBasedTaxonomy( + $maritalStatuses, + self::TAX_INV_MARITAL, + self::$forceTermLookupIdUpdate + ); + } + + // Partner categories can't be included here because the values are only known at sync. + } + + /** + * Wrapper for the WordPress term_exists function to reduce database calls + * + * @param int|string $term The term to check. Accepts term ID, slug, or name. + * @param string $taxonomy Optional. The taxonomy name to use. + * @param int|null $parent Optional. ID of parent term under which to confine the exists search. + * + * @return mixed Returns null if the term does not exist. + * Returns the term ID if no taxonomy is specified and the term ID exists. + * Returns an array of the term ID and the term taxonomy ID if the taxonomy is specified and the + * pairing exists. + * Returns 0 if term ID 0 is passed to the function. + * + * @see term_exists() + */ + public static function termExists($term, string $taxonomy = "", ?int $parent = null) + { + $key = $term . "|" . $taxonomy . "|" . $parent; + if ( ! array_key_exists($key, self::$termExistsCache)) { + self::$termExistsCache[$key] = term_exists($term, $taxonomy, $parent); + } + + return self::$termExistsCache[$key]; + } + + /** + * Wrapper for the WordPress wp_insert_term function to reduce database calls + * + * Add a new term to the database. + * + * A non-existent term is inserted in the following sequence: + * 1. The term is added to the term table, then related to the taxonomy. + * 2. If everything is correct, several actions are fired. + * 3. The 'term_id_filter' is evaluated. + * 4. The term cache is cleaned. + * 5. Several more actions are fired. + * 6. An array is returned containing the `term_id` and `term_taxonomy_id`. + * + * If the 'slug' argument is not empty, then it is checked to see if the term + * is invalid. If it is not a valid, existing term, it is added and the term_id + * is given. + * + * If the taxonomy is hierarchical, and the 'parent' argument is not empty, + * the term is inserted and the term_id will be given. + * + * Error handling: + * If `$taxonomy` does not exist or `$term` is empty, + * a WP_Error object will be returned. + * + * If the term already exists on the same hierarchical level, + * or the term slug and name are not unique, a WP_Error object will be returned. + * + * @param string $term The term name to add. + * @param string $taxonomy The taxonomy to which to add the term. + * @param array|string $args { + * Optional. Array or query string of arguments for inserting a term. + * + * @type string $alias_of Slug of the term to make this term an alias of. + * Default empty string. Accepts a term slug. + * @type string $description The term description. Default empty string. + * @type int $parent The id of the parent term. Default 0. + * @type string $slug The term slug to use. Default empty string. + * } + * @return array|WP_Error { + * An array of the new term data, WP_Error otherwise. + * + * @type int $term_id The new term ID. + * @type int|string $term_taxonomy_id The new term taxonomy ID. Can be a numeric string. + * } + * + * @see wp_insert_term() + */ + public static function insertTerm(string $term, string $taxonomy, $args = []) + { + $parent = $args['parent'] ?? null; + $key = $term . "|" . $taxonomy . "|" . $parent; + $r = wp_insert_term($term, $taxonomy, $args); + if ( ! is_wp_error($r)) { + self::$termExistsCache[$key] = $r; + } + + return $r; + } + + /** + * @param TouchPointWP $instance + * @param string $taxonomy + * + * @return array + */ + protected static function getPostTypesForTaxonomy(TouchPointWP $instance, string $taxonomy): array + { + $types = []; + + switch ($taxonomy) { + case self::TAX_RESCODE: + if ($instance->settings->enable_involvements === "on") { + $types = Involvement_PostTypeSettings::getPostTypesWithGeoEnabled(); + } + if ($instance->settings->rc_additional_post_types) { + $types = array_merge( + $types, + $instance->settings->rc_additional_post_types + ); + } + if ($instance->settings->enable_meeting_cal === "on") { + $types[] = Meeting::POST_TYPE; + } + $types[] = 'user'; + return $types; + + case self::TAX_CAMPUS: + if ($instance->settings->enable_campuses !== "on") { + return $types; + } + if ($instance->settings->enable_involvements === "on") { + $types = Involvement_PostTypeSettings::getPostTypes(); + } + if ($instance->settings->enable_meeting_cal === "on") { + $types[] = Meeting::POST_TYPE; + } + $types[] = 'user'; + return $types; + + case self::TAX_DIV: + if ($instance->settings->enable_involvements === "on") { + $types = Involvement_PostTypeSettings::getPostTypes(); + } + if ($instance->settings->enable_meeting_cal === "on") { + $types[] = Meeting::POST_TYPE; + } + if ($instance->settings->dv_additional_post_types) { + $types = array_merge( + $types, + $instance->settings->dv_additional_post_types + ); + } + return $types; + + case self::TAX_AGEGROUP: + if ($instance->settings->enable_involvements === "on") { + $types = Involvement_PostTypeSettings::getPostTypes(); + } + $types[] = 'user'; + return $types; + + case self::TAX_WEEKDAY: + case self::TAX_TENSE: + case self::TAX_INV_MARITAL: + if ($instance->settings->enable_involvements === "on") { + $types = Involvement_PostTypeSettings::getPostTypes(); + } + return $types; + + case self::TAX_GP_CATEGORY: + if ($instance->settings->enable_involvements === "on" + && class_exists('\tp\TouchPointWP\Partner', false)) { + return [\tp\TouchPointWP\Partner::POST_TYPE]; + } + + } + + return $types; + } + + /** + * Register the taxonomies. + * + * @param TouchPointWP $instance + * + * @return void + */ + public static function registerTaxonomies(TouchPointWP $instance): void + { + // Resident Codes + $types = self::getPostTypesForTaxonomy($instance, self::TAX_RESCODE); + if (count($types) > 0) { + register_taxonomy( + self::TAX_RESCODE, + $types, + [ + 'hierarchical' => false, + 'show_ui' => false, + 'description' => __('Classify posts by their general locations.', 'TouchPoint-WP'), + 'labels' => self::getLabels( + $instance->settings->rc_name_singular, + $instance->settings->rc_name_plural + ), + 'public' => true, + 'show_in_rest' => true, + 'show_admin_column' => true, + + // Control the slugs used for this taxonomy + 'rewrite' => [ + 'slug' => $instance->settings->rc_slug, + 'with_front' => false, + 'hierarchical' => false + ], + ] + ); + // Terms inserted via insertTerms method + } + + // Campuses + $types = self::getPostTypesForTaxonomy($instance, self::TAX_CAMPUS); + if (count($types) > 0) { + register_taxonomy( + self::TAX_CAMPUS, + $types, + [ + 'hierarchical' => false, + 'show_ui' => false, + 'description' => __('Classify posts by their church campus.', 'TouchPoint-WP'), + 'labels' => self::getLabels( + $instance->settings->camp_name_singular, + $instance->settings->camp_name_plural + ), + 'public' => true, + 'show_in_rest' => true, + 'show_admin_column' => true, + + // Control the slugs used for this taxonomy + 'rewrite' => [ + 'slug' => $instance->settings->camp_slug, + 'with_front' => false, + 'hierarchical' => false + ], + ] + ); + // Terms inserted via insertTerms method + } + + // Divisions & Programs + $types = self::getPostTypesForTaxonomy($instance, self::TAX_DIV); + if (count($types) > 0) { + register_taxonomy( + self::TAX_DIV, + $types, + [ + 'hierarchical' => true, + 'show_ui' => true, + 'description' => sprintf( + /* translators: %s: taxonomy name, singular */ + __('Classify things by %s.', 'TouchPoint-WP'), + $instance->settings->dv_name_singular + ), + 'labels' => self::getLabels( + $instance->settings->dv_name_singular, + $instance->settings->dv_name_plural + ), + 'public' => true, + 'show_in_rest' => true, + 'show_admin_column' => false, + + // Control the slugs used for this taxonomy + 'rewrite' => [ + 'slug' => $instance->settings->dv_slug, + 'with_front' => false, + 'hierarchical' => true + ], + ] + ); + // Terms inserted via insertTerms method + } + + // Weekdays + $types = self::getPostTypesForTaxonomy($instance, self::TAX_WEEKDAY); + if (count($types) > 0) { + register_taxonomy( + self::TAX_WEEKDAY, + $types, + [ + 'hierarchical' => false, + 'show_ui' => false, + 'description' => __('Classify involvements by the day on which they meet.', 'TouchPoint-WP'), + 'labels' => self::getLabels(__('Weekday', 'TouchPoint-WP'), __('Weekdays', 'TouchPoint-WP')), + 'public' => true, + 'show_in_rest' => true, + 'show_admin_column' => true, + + // Control the slugs used for this taxonomy + 'rewrite' => [ + 'slug' => 'weekday', + 'with_front' => false, + 'hierarchical' => false + ], + ] + ); + // Terms inserted via insertTerms method + + } + + // Tenses + $types = self::getPostTypesForTaxonomy($instance, self::TAX_TENSE); + if (count($types) > 0) { + register_taxonomy( + self::TAX_TENSE, + $types, + [ + 'hierarchical' => false, + 'show_ui' => false, + 'description' => __( + 'Classify involvements by tense (present, future, past)', + 'TouchPoint-WP' + ), + 'labels' => self::getLabels(__("Tense", 'TouchPoint-WP'), __("Tenses", 'TouchPoint-WP')), + 'public' => true, + 'show_in_rest' => false, + 'show_admin_column' => false, + + // Control the slugs used for this taxonomy + 'rewrite' => [ + 'slug' => 'tense', + 'with_front' => false, + 'hierarchical' => false + ], + ] + ); + // Terms inserted via insertTerms method + + // Time of Day + /** @noinspection SpellCheckingInspection */ + register_taxonomy( + self::TAX_DAYTIME, + Involvement_PostTypeSettings::getPostTypes(), + [ + 'hierarchical' => false, + 'show_ui' => false, + 'description' => __( + 'Classify involvements by the portion of the day in which they meet.', + 'TouchPoint-WP' + ), + 'labels' => self::getLabels( + __('Time of Day', 'TouchPoint-WP'), + __('Times of Day', 'TouchPoint-WP') + ), + 'public' => true, + 'show_in_rest' => true, + 'show_admin_column' => true, + + // Control the slugs used for this taxonomy + 'rewrite' => [ + 'slug' => 'timeofday', + 'with_front' => false, + 'hierarchical' => false + ], + ] + ); + // Terms inserted via insertTerms method + } + + // Age Groups + $types = self::getPostTypesForTaxonomy($instance, self::TAX_AGEGROUP); + if (count($types) > 0) { + register_taxonomy( + self::TAX_AGEGROUP, + $types, + [ + 'hierarchical' => false, + 'show_ui' => false, + 'description' => __('Classify involvements and users by their age groups.', 'TouchPoint-WP'), + 'labels' => self::getLabels( + __('Age Group', 'TouchPoint-WP'), + __('Age Groups', 'TouchPoint-WP') + ), + 'public' => true, + 'show_in_rest' => true, + 'show_admin_column' => true, + + // Control the slugs used for this taxonomy + 'rewrite' => [ + 'slug' => self::TAX_AGEGROUP, + 'with_front' => false, + 'hierarchical' => false + ], + ] + ); + // Terms inserted via insertTerms method + } + + // Involvement Marital Status + $types = self::getPostTypesForTaxonomy($instance, self::TAX_INV_MARITAL); + if (count($types) > 0) { + register_taxonomy( + self::TAX_INV_MARITAL, + $types, + [ + 'hierarchical' => false, + 'show_ui' => false, + 'description' => __( + 'Classify involvements by whether participants are mostly single or married.', + 'TouchPoint-WP' + ), + 'labels' => self::getLabels( + __('Marital Status', 'TouchPoint-WP'), + __('Marital Statuses', 'TouchPoint-WP') + ), + 'public' => true, + 'show_in_rest' => true, + 'show_admin_column' => true, + + // Control the slugs used for this taxonomy + 'rewrite' => [ + 'slug' => self::TAX_INV_MARITAL, + 'with_front' => false, + 'hierarchical' => false + ], + ] + ); + // Terms inserted via insertTerms method + } + + // Global Partner Category + $types = self::getPostTypesForTaxonomy($instance, self::TAX_GP_CATEGORY); + if ($types > 0) { + $tax = $instance->settings->global_primary_tax; + if ($tax !== "" && + is_object($tax) && + $instance->settings->enable_global === "on" && + count($instance->getFamilyEvFields([$tax])) > 0) { + $tax = $instance->getFamilyEvFields([$tax])[0]; + $plural = $tax->field . "s"; // TODO Sad, but works. i18n someday. + register_taxonomy( + self::TAX_GP_CATEGORY, + $types, + [ + 'hierarchical' => false, + 'show_ui' => false, + 'description' => __('Classify Partners by category chosen in settings.', 'TouchPoint-WP'), + 'labels' => self::getLabels($tax->field, $plural), + 'public' => true, + 'show_in_rest' => true, + 'show_admin_column' => true, + + // Control the slugs used for this taxonomy + 'rewrite' => [ + 'slug' => self::TAX_GP_CATEGORY, + 'with_front' => false, + 'hierarchical' => false + ], + ] + ); + // Terms are inserted on sync. + } + } + } +} \ No newline at end of file diff --git a/src/TouchPoint-WP/TouchPointWP.php b/src/TouchPoint-WP/TouchPointWP.php index e2ccd442..70eef512 100644 --- a/src/TouchPoint-WP/TouchPointWP.php +++ b/src/TouchPoint-WP/TouchPointWP.php @@ -5,15 +5,16 @@ namespace tp\TouchPointWP; +use JsonException; use stdClass; +use tp\TouchPointWP\Utilities\Cleanup; +use tp\TouchPointWP\Utilities\Http; +use tp\TouchPointWP\Utilities\Session; use WP; use WP_Error; use WP_Http; use WP_Term; - -use tp\TouchPointWP\Utilities\Cleanup; -use tp\TouchPointWP\Utilities\Session; -use tp\TouchPointWP\Utilities\Http; +use WP_User; if ( ! defined('ABSPATH')) { @@ -22,6 +23,7 @@ if ( ! TOUCHPOINT_COMPOSER_ENABLED) { require_once "Utilities.php"; + require_once "Stats.php"; } @@ -33,7 +35,7 @@ class TouchPointWP /** * Version number */ - public const VERSION = "0.0.37"; + public const VERSION = "0.0.95"; /** * The Token @@ -50,20 +52,13 @@ class TouchPointWP public const API_ENDPOINT_PERSON = "person"; public const API_ENDPOINT_MEETING = "mtg"; public const API_ENDPOINT_ADMIN = "admin"; + public const API_ENDPOINT_STATS = "stats"; public const API_ENDPOINT_AUTH = "auth"; public const API_ENDPOINT_REPORT = "report"; public const API_ENDPOINT_ADMIN_SCRIPTZIP = "admin/scriptzip"; public const API_ENDPOINT_CLEANUP = "cleanup"; public const API_ENDPOINT_GEOLOCATE = "geolocate"; - public const TEMPLATES_TO_OVERWRITE = [ - 'archive.php', - 'singular.php', - 'single.php', - 'index.php', - 'template-canvas.php' - ]; - /** * Prefix to use for all shortcodes. */ @@ -74,38 +69,27 @@ class TouchPointWP */ public const HOOK_PREFIX = "tp_"; - public const INIT_ACTION_HOOK = self::HOOK_PREFIX . "init"; + public const INIT_ACTION_HOOK = "tp_init"; // Note that this is also hard-coded where the action is declared. /** * Prefix to use for all settings. */ public const SETTINGS_PREFIX = "tp_"; - public const TAX_RESCODE = self::HOOK_PREFIX . "rescode"; - public const TAX_CAMPUS = self::HOOK_PREFIX . "campus"; - public const TAX_DIV = self::HOOK_PREFIX . "div"; - public const TAX_WEEKDAY = self::HOOK_PREFIX . "weekday"; - public const TAX_TENSE = self::HOOK_PREFIX . "tense"; - public const TAX_TENSE_PAST = "past"; - public const TAX_TENSE_PRESENT = "present"; - public const TAX_TENSE_FUTURE = "future"; - public const TAX_DAYTIME = self::HOOK_PREFIX . "timeOfDay"; - public const TAX_AGEGROUP = self::HOOK_PREFIX . "agegroup"; - public const TAX_INV_MARITAL = self::HOOK_PREFIX . "inv_marital"; - public const TAX_GP_CATEGORY = self::HOOK_PREFIX . "partner_category"; - public const TAXMETA_LOOKUP_ID = self::HOOK_PREFIX . "lookup_id"; - /** * Table Names */ public const TABLE_PREFIX = "tp_"; public const TABLE_IP_GEO = self::TABLE_PREFIX . "ipGeo"; + public const TABLE_STATS = self::TABLE_PREFIX . "stats"; /** * Typical amount of time in hours for metadata to last (e.g. genders and resCodes). */ public const CACHE_TTL = 8; + public const TTL_IP_GEO = 5; // years + /** * Caching */ @@ -114,6 +98,10 @@ class TouchPointWP public const CACHE_NONE = 20; private static int $cacheLevel = self::CACHE_PUBLIC; + + public const INVOLVEMENT_META_KEY = TouchPointWP::SETTINGS_PREFIX . "invId"; + + /** * @var string Used for imploding arrays together in human-friendly formats. */ @@ -184,6 +172,10 @@ class TouchPointWP */ protected ?bool $global = null; + /** + * @var ?bool True after the Meeting/Events Calendar feature is loaded. + */ + protected ?bool $meeting = null; /** * @var ?bool True after the People feature is loaded. @@ -243,16 +235,16 @@ protected function __construct(string $file = '') // add_action( 'admin_enqueue_scripts', [$this, 'admin_enqueue_styles'], 10, 1 ); // TODO restore? // Load API for generic admin functions. -// if (is_admin()) { -// $this->admin(); // SOMEDAY if we ever need to interact with other post types, this should be uncommented. -// } +// if (is_admin()) { +// $this->admin(); // SOMEDAY if we ever need to interact with other post types, this should be uncommented. +// } add_filter('do_parse_request', [$this, 'parseRequest'], 10, 3); // Adds async and defer attributes to script tags. add_filter('script_loader_tag', [$this, 'filterByTag'], 10, 2); - add_filter('terms_clauses', [$this, 'getTermsClauses'], 10, 3); + add_filter('terms_clauses', [Taxonomies::class, 'getTermsClauses'], 10, 3); add_filter('site_transient_update_plugins', [Utilities::class, 'checkForUpdate_transient']); @@ -269,10 +261,10 @@ protected function __construct(string $file = '') public static function cronAdd15Minutes($schedules) { // Adds once weekly to the existing schedules. - $schedules['tp_every_15_minutes'] = array( + $schedules['tp_every_15_minutes'] = [ 'interval' => 15 * 60, 'display' => __('Every 15 minutes', 'TouchPoint-WP') - ); + ]; return $schedules; } @@ -284,11 +276,10 @@ public static function cronAdd15Minutes($schedules) * @param string|mixed $text The text to be modified. (should be a string, but this is WordPress, so maybe not.) * * @return string The modified text. - * @noinspection SpellCheckingInspection - * @since 0.0.23 + * @since 0.0.23 Added * */ - public static function capitalPyScript($text): string + public static function capitalPyScript(mixed $text): string { if ( ! self::instance()->settings->hasValidApiSettings()) { return $text; @@ -307,8 +298,10 @@ public function admin(): TouchPointWP_AdminAPI if ($this->admin === null) { if ( ! TOUCHPOINT_COMPOSER_ENABLED) { require_once 'TouchPointWP_AdminAPI.php'; + require_once 'TouchPointWP_Widget.php'; } $this->admin = new TouchPointWP_AdminAPI(); + TouchPointWP_Widget::init(); } return $this->admin; @@ -365,24 +358,39 @@ public static function postHeadersAndFiltering(): string if ($_SERVER['REQUEST_METHOD'] !== 'POST') { echo json_encode([ - 'error' => 'Only POST requests are allowed.', - 'error_i18n' => __("Only POST requests are allowed.", 'TouchPoint-WP') - ]); + 'error' => 'Only POST requests are allowed.', + 'error_i18n' => __("Only POST requests are allowed.", 'TouchPoint-WP') + ]); exit; } $inputData = file_get_contents('php://input'); if ($inputData[0] !== '{') { echo json_encode([ - 'error' => 'Invalid data provided.', - 'error_i18n' => __("Invalid data provided.", 'TouchPoint-WP') - ]); + 'error' => 'Invalid data provided.', + 'error_i18n' => __("Invalid data provided.", 'TouchPoint-WP') + ]); exit; } return $inputData; } + + /** + * Get TouchPoint icon as an SVG that can printed inline. + * + * @return string + */ + public static function TouchPointIcon(): string + { + if (self::$_icon === null) { + self::$_icon = file_get_contents(TouchPointWP::$dir . "/assets/branding/icon-curcolor.svg"); + } + return self::$_icon; + } + protected static ?string $_icon = null; + /** * @param bool $continue Whether to parse the request * @param WP $wp Current WordPress environment instance @@ -400,10 +408,10 @@ public function parseRequest($continue, $wp, $extraVars): bool $reqUri['path'] = $reqUri['path'] ?? ""; // Remove trailing slash if it exists (and, it probably does) - if (substr($reqUri['path'], -1) === '/') { + if (str_ends_with($reqUri['path'], '/')) { $reqUri['path'] = substr($reqUri['path'], 0, -1); } - + if (isset($_GET['locale']) && strlen($_GET['locale']) > 1) { $l = $_GET['locale']; add_filter('locale', fn() => $l, 1); @@ -427,7 +435,7 @@ public function parseRequest($continue, $wp, $extraVars): bool // App Events Endpoint if ($reqUri['path'][1] === TouchPointWP::API_ENDPOINT_APP_EVENTS && - TouchPointWP::useTribeCalendar() + TouchPointWP::useTribeOrMeetingCalendars() ) { if ( ! EventsCalendar::api($reqUri)) { return $continue; @@ -436,7 +444,7 @@ public function parseRequest($continue, $wp, $extraVars): bool // Involvement endpoint if ($reqUri['path'][1] === TouchPointWP::API_ENDPOINT_INVOLVEMENT && - $this->settings->enable_involvements === "on" + $this->settings->enable_involvements === "on" ) { if ( ! Involvement::api($reqUri)) { return $continue; @@ -445,7 +453,7 @@ public function parseRequest($continue, $wp, $extraVars): bool // Global Partner endpoint if ($reqUri['path'][1] === TouchPointWP::API_ENDPOINT_GLOBAL && - $this->settings->enable_global === "on" + $this->settings->enable_global === "on" ) { if ( ! Partner::api($reqUri)) { return $continue; @@ -475,7 +483,7 @@ public function parseRequest($continue, $wp, $extraVars): bool // Auth endpoints if ($reqUri['path'][1] === TouchPointWP::API_ENDPOINT_AUTH && - $this->settings->enable_authentication === "on" + $this->settings->enable_authentication === "on" ) { if ( ! Auth::api($reqUri)) { return $continue; @@ -490,6 +498,13 @@ public function parseRequest($continue, $wp, $extraVars): bool } } + // Stats endpoints + if ($reqUri['path'][1] === TouchPointWP::API_ENDPOINT_STATS) { + if ( ! Stats::api($reqUri)) { + return $continue; + } + } + // Cleanup endpoints if ($reqUri['path'][1] === TouchPointWP::API_ENDPOINT_CLEANUP) { if ( ! Cleanup::api($reqUri)) { @@ -499,7 +514,7 @@ public function parseRequest($continue, $wp, $extraVars): bool // Geolocate via IP if ($reqUri['path'][1] === TouchPointWP::API_ENDPOINT_GEOLOCATE && - count($reqUri['path']) === 2) { + count($reqUri['path']) === 2) { $this->ajaxGeolocate(); } } @@ -520,46 +535,11 @@ public static function currentUserIsAdmin(): bool return false; } - return current_user_can('manage_options'); - } - - /** - * Filter to add a tp_post_type option to get_terms that takes either a string of one post type or an array of post - * types. - * - * @param $clauses - * @param $taxonomy - * @param $args - * - * Hat tip https://dfactory.eu/wp-how-to-get-terms-post-type/ - * - * @return mixed - * @noinspection PhpUnusedParameterInspection WordPress API - */ - public function getTermsClauses($clauses, $taxonomy, $args): array - { - if (isset($args[self::HOOK_PREFIX . 'post_type']) && ! empty($args[self::HOOK_PREFIX . 'post_type']) && $args['fields'] !== 'count') { - global $wpdb; - - $post_types = []; - - if (is_array($args[self::HOOK_PREFIX . 'post_type'])) { - foreach ($args[self::HOOK_PREFIX . 'post_type'] as $cpt) { - $post_types[] = "'" . $cpt . "'"; - } - } else { - $post_types[] = "'" . $args[self::HOOK_PREFIX . 'post_type'] . "'"; - } - - if ( ! empty($post_types)) { - $clauses['fields'] = 'DISTINCT ' . str_replace( 'tt.*', 'tt.term_taxonomy_id, tt.taxonomy, tt.description, tt.parent', $clauses['fields'] ) . ', COUNT(p.post_type) AS count'; - $clauses['join'] .= ' LEFT JOIN ' . $wpdb->term_relationships . ' AS r ON r.term_taxonomy_id = tt.term_taxonomy_id LEFT JOIN ' . $wpdb->posts . ' AS p ON p.ID = r.object_id'; - $clauses['where'] .= ' AND (p.post_type IN (' . implode( ',', $post_types ) . ') OR (tt.parent = 0 AND tt.count = 0))'; - $clauses['orderby'] = 'GROUP BY t.term_id ' . $clauses['orderby']; - } + if ( ! function_exists('wp_get_current_user')) { + return false; } - return $clauses; + return current_user_can('manage_options'); } /** @@ -638,7 +618,7 @@ public function getJsLocalizationDir(): string /** * Load plugin textdomain */ - public function loadLocalizations() + public function loadLocalizations(): void { $locale = apply_filters('plugin_locale', get_locale(), 'TouchPoint-WP'); @@ -652,14 +632,89 @@ public function loadLocalizations() load_plugin_textdomain('TouchPoint-WP', false, $dir . '/i18n/'); } + public static function isTenth() + { + return site_url() === "https://www.tenth.org"; + } + + /** + * Create or update database tables + */ + protected function createTables(): void + { + global $wpdb; + require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); + + // IP Geo Caching table + $tableName = $wpdb->base_prefix . TouchPointWP::TABLE_IP_GEO; + $sql = "CREATE TABLE $tableName ( + id int(10) unsigned NOT NULL auto_increment, + ip varbinary(16) NOT NULL UNIQUE, + updatedDT datetime DEFAULT NOW(), + data text NOT NULL, + PRIMARY KEY (id) + )"; + dbDelta($sql); + + // Table for receiving info from other sites that use this plugin + if (self::isTenth()) { + $tableName = $wpdb->base_prefix . TouchPointWP::TABLE_STATS; + $sql = "CREATE TABLE $tableName ( + installId varchar(36) NOT NULL, + privateKey varchar(36) NOT NULL, + siteId varchar(36) NOT NULL, + site varchar(255) NOT NULL, + plugin varchar(255) NOT NULL, + version varchar(10) NOT NULL, + php varchar(10) NOT NULL, + wp varchar(10) NOT NULL, + wpLocale varchar(10) NOT NULL, + wpTimezone varchar(50) NOT NULL, + adminEmail varchar(255) NOT NULL, + siteName varchar(255) NOT NULL, + siteLogo varchar(512) NOT NULL, + createdDT datetime DEFAULT NOW(), + updatedDT datetime DEFAULT NOW() ON UPDATE NOW(), + + listPublicly tinyint(1) DEFAULT 1, + lastQueryDT datetime DEFAULT NULL, + lastQueryStatus int(3) DEFAULT NULL, + + involvementJoins int(10) DEFAULT 0, + involvementContacts int(10) DEFAULT 0, + involvementPosts int(10) DEFAULT 0, + reportPosts int(10) DEFAULT 0, + meetings int(10) DEFAULT 0, + rsvps int(10) DEFAULT 0, + people int(10) DEFAULT 0, + partnerPosts int(10) DEFAULT 0, + userAuths int(10) DEFAULT 0, + softAuths int(10) DEFAULT 0, + + PRIMARY KEY (installId) + )"; + dbDelta($sql); + } + } + /** * Compare the version numbers to determine if a migration is needed. */ - public function checkMigrations(): void + public function migrate($force = false): void { - if ($this->settings->version !== self::VERSION) { - $this->settings->migrate(); + if ($this->settings->version === self::VERSION && !$force) { + return; + } + + $this->createTables(); + + if (self::$_hasBeenInited) { + Taxonomies::insertTerms($this); + } else { + add_action(self::INIT_ACTION_HOOK, [Taxonomies::class, 'insertTerms']); } + + $this->settings->migrate(); } /** @@ -676,10 +731,12 @@ public static function load($file): TouchPointWP if (is_null($instance->settings)) { $instance->settings = TouchPointWP_Settings::instance($instance); if (is_admin()) { - $instance->checkMigrations(); + $instance->migrate(); } } + Stats::load(); + // Load Auth tool if enabled. if ($instance->settings->enable_authentication === "on") { if ( ! TOUCHPOINT_COMPOSER_ENABLED) { @@ -697,10 +754,8 @@ public static function load($file): TouchPointWP } // Load Involvements tool if enabled. - if ($instance->settings->enable_involvements === "on") { - if ( ! TOUCHPOINT_COMPOSER_ENABLED) { - require_once 'Involvement.php'; - } + if ($instance->settings->enable_involvements === "on" || + $instance->settings->enable_meeting_cal === "on") { $instance->involvements = Involvement::load(); } @@ -714,15 +769,17 @@ public static function load($file): TouchPointWP // Load Person for People Indexes. if ($instance->settings->enable_people_lists === "on") { - if ( ! TOUCHPOINT_COMPOSER_ENABLED) { - require_once 'Person.php'; - } $instance->people = Person::load(); } - // Load Events if enabled (by presence of Events Calendar plugin) - if (self::useTribeCalendar() - && ! class_exists("tp\TouchPointWP\EventsCalendar")) { + // Load Meetings / Events Calendar + if ($instance->settings->enable_meeting_cal === "on") { + $instance->meeting = Meeting::load(); + } + + // Load Tribe module if enabled (by presence of Events Calendar plugin) + if (self::useTribeOrMeetingCalendars() + && !class_exists("tp\TouchPointWP\EventsCalendar")) { if ( ! TOUCHPOINT_COMPOSER_ENABLED) { require_once 'EventsCalendar.php'; } @@ -731,7 +788,7 @@ public static function load($file): TouchPointWP // Load Reports (feature is always enabled) $instance->reports = Report::load(); - add_action('init', [self::class, 'init']); + add_action('init', [self::class, 'init'], 10); add_filter('the_content', [self::class, 'capitalPyScript'], 11); add_filter('widget_text_content', [self::class, 'capitalPyScript'], 11); @@ -739,11 +796,24 @@ public static function load($file): TouchPointWP return $instance; } + /** + * @var bool True of the init process has run. False if not. Prevents things from happening twice, which can cause errors. + */ + private static bool $_hasBeenInited = false; + + /** + * Initialize the plugin. + * + * @return void + */ public static function init(): void { + if (self::$_hasBeenInited) + return; + self::instance()->loadLocalizations(); - self::instance()->registerTaxonomies(); + Taxonomies::registerTaxonomies(self::instance()); // If any slugs have changed, flush. Only executes if already enqueued. self::instance()->flushRewriteRules(false); @@ -751,11 +821,22 @@ public static function init(): void // If the scripts need to be updated, do that. self::instance()->updateDeployedScripts(); + self::$_hasBeenInited = true; + self::requireScript("base"); - do_action(self::INIT_ACTION_HOOK); + /** + * Fires after the plugin has been initialized. + */ + do_action("tp_init"); } + /** + * Prints the inline 'base' script. This is meant to be called in the wp_head and admin_head, and should only be + * called once on a page, but that is not automatically validated. + * + * @return void + */ public static function renderBaseInlineScript(): void { include self::instance()->assets_dir . '/js/base-inline.php'; @@ -781,8 +862,7 @@ public function registerScriptsAndStyles(): void ); wp_set_script_translations( self::SHORTCODE_PREFIX . 'base-defer', - 'TouchPoint-WP', - $this->getJsLocalizationDir() + 'TouchPoint-WP', $this->getJsLocalizationDir() ); wp_register_script( @@ -816,10 +896,11 @@ public function registerScriptsAndStyles(): void true ); + $lang = strtolower(get_locale()); wp_register_script( TouchPointWP::SHORTCODE_PREFIX . "googleMaps", sprintf( - "https://maps.googleapis.com/maps/api/js?key=%s&v=3&libraries=geometry", + "https://maps.googleapis.com/maps/api/js?key=%s&v=3&libraries=geometry&language=$lang", TouchPointWP::instance()->settings->google_maps_api_key ), [TouchPointWP::SHORTCODE_PREFIX . "base-defer"], @@ -843,9 +924,9 @@ public function registerScriptsAndStyles(): void Partner::registerScriptsAndStyles(); } -// if ( ! ! $this->auth) { -// Auth::registerScriptsAndStyles(); -// } +// if ( ! ! $this->auth) { +// Auth::registerScriptsAndStyles(); +// } if ( ! ! $this->rsvp) { Meeting::registerScriptsAndStyles(); @@ -862,7 +943,13 @@ public function registerScriptsAndStyles(): void */ public static function requireScript(string $name = null): void { - if ( ! apply_filters(TouchPointWP::HOOK_PREFIX . "include_script_" . strtolower($name), true)) { + $filename = strtolower($name); + /** + * Filter to determine if a given script (which comes with TouchPoint-WP) should be included. + * + * @params bool $include Whether to include the script. + */ + if ( !apply_filters("tp_include_script_$filename", true)) { return; } @@ -890,7 +977,16 @@ public static function requireScript(string $name = null): void */ public static function requireStyle(string $name = null): void { - if ( ! apply_filters(TouchPointWP::HOOK_PREFIX . "include_style_" . strtolower($name), true)) { + $filename = strtolower($name); + + $includeStyle = true; + + /** + * Filter to determine if a given stylesheet (which comes with TouchPoint-WP) should be included. + * + * @params bool $include Whether to include the stylesheet. + */ + if ( ! apply_filters("tp_include_style_$filename", $includeStyle)) { return; } @@ -915,12 +1011,12 @@ public static function requireStyle(string $name = null): void */ public function filterByTag(?string $tag, ?string $handle): string { - if (strpos($tag, 'async') !== false && - strpos($handle, '-async') > 0) { + if (str_contains($tag, 'async') && + strpos($handle, '-async') > 0) { $tag = str_replace(' src=', ' async="async" src=', $tag); } - if (strpos($tag, 'defer') !== false && - strpos($handle, '-defer') > 0 + if (str_contains($tag, 'defer') && + strpos($handle, '-defer') > 0 ) { $tag = str_replace('"; +echo ""; ?>
@@ -55,6 +56,29 @@ + + + + +

+ +

+ + +

+

+ + +

+ +

+ + +

+ + + + @@ -72,6 +96,15 @@ + enable_meeting_cal === "on") { ?> + + + + + + + + @@ -146,19 +179,44 @@ - + get('dv_name_singular'), + __("Division", "TouchPoint-WP") + ); + + $resCodeLabel = wp_sprintf( + // Translators: %1$s is the user-provided name for ResCode. %2$s is "Resident Code" or translated equivalent. + _x('%1$s (%2$s)', "TouchPoint-WP"), + $this->get('rc_name_singular'), + __("Resident Code", "TouchPoint-WP") + ); + + $campusLabel = wp_sprintf( + // Translators: %1$s is the user-provided name for Campus. %2$s is "Campus" or translated equivalent. + _x('%1$s (%2$s)', "TouchPoint-WP"), + $this->get('camp_name_singular'), + __("Campus", "TouchPoint-WP") + ); + + ?>

- +

@@ -166,12 +224,12 @@

- +

get('enable_campuses') === "on") { ?>

- +

@@ -238,10 +296,12 @@ function InvType(data) { this.namePlural = ko.observable(data.namePlural ?? ""); this.slug = ko.observable(data.slug ?? "smallgroup").extend({slug: 0}); this.importDivs = ko.observable(data.importDivs ?? []); + this.importCampuses = ko.observable(data.importCampuses ?? []); this.useGeo = ko.observable(data.useGeo ?? false); this.useImages = ko.observable(data.useImages ?? true); this.excludeIf = ko.observable(data.excludeIf ?? []); this.hierarchical = ko.observable(data.hierarchical ?? false); + this.importMeetings = ko.observable(data.importMeetings ?? false); this.groupBy = ko.observable(data.groupBy ?? ""); this.leaderTypes = ko.observableArray(data.leaderTypes ?? []); this.hostTypes = ko.observableArray(data.hostTypes ?? []); @@ -267,6 +327,19 @@ function InvType(data) { } }) + this._importCampusesAll = ko.pureComputed({ + read: function() { + return self.importCampuses().length === 0; + }, + write: function(value) { + if (value) { + self.importCampuses([]); + } else if (self.importCampuses().length === 0) { + self.importCampuses(['c0']); + } + } + }); + // operations this.toggleVisibility = function() { self._visible(! self._visible()) @@ -286,6 +359,7 @@ function InvTypeVM(invData) { self.invTypes = ko.observableArray(invInits); self.divisions = tpvm._vmContext.divs; self.keywords = tpvm._vmContext.kws; + self.campuses = tpvm._vmContext.campuses; // Operations self.addInvType = function() { @@ -328,7 +402,7 @@ function initInvVm() { let types = tpvm._vmContext.invTypesVM.invTypes(); for (let i in types) { - let name = tpvm.people[invData[i].taskOwner]?.displayName ?? "(named person)"; + let name = tpvm.people[invData[i].taskOwner]?.displayName ?? ""; applySelect2ForData('#it-' + types[i].slug() + '-taskOwner', name, invData[i].taskOwner); } diff --git a/src/templates/admin/locationsKoForm.php b/src/templates/admin/locationsKoForm.php index 9ce280d3..b6ddc5f1 100644 --- a/src/templates/admin/locationsKoForm.php +++ b/src/templates/admin/locationsKoForm.php @@ -38,6 +38,14 @@ + + + + + + + + @@ -71,6 +79,7 @@ function Location(data) { this.name = ko.observable(data.name ?? ""); this.lat = ko.observable(data.lat ?? ""); this.lng = ko.observable(data.lng ?? ""); + this.radius = ko.observable(data.radius ?? 0.1); this.ipAddresses = ko.observableArray(data.ipAddresses ?? []); this._visible = ko.observable(false); diff --git a/src/templates/involvement-archive.php b/src/templates/involvement-archive.php index ea769f96..553c00f3 100644 --- a/src/templates/involvement-archive.php +++ b/src/templates/involvement-archive.php @@ -46,6 +46,8 @@

+
+ +
+settings; +$obj = PostTypeCapable::fromPost($p); TouchPointWP::enqueuePartialsStyle(); ?> -
id="post-" data-tp-involvement="post_id ?>"> +
id="post-" data-tp-involvement="ID ?>">
-
+
notableAttributes() as $a) + foreach ($obj->notableAttributes() as $a) { $metaStrings[] = sprintf( '%s', $a); } @@ -44,38 +79,74 @@ ?>
- getActionButtons('single-template', "btn button") ?> + getActionButtons('single-template', "btn button") ?>
- useGeo && $inv->geo !== null) { ?> -
- -
+ useGeo && $obj->hasGeo()) { ?> +
+ + +
hierarchical) { - $single_children = get_children([ - 'post_parent' => $inv->post_id, - 'orderby' => 'title', - 'order' => 'ASC', - 'post_type' => $postType - ]); - if (count($single_children) > 0) { - echo "
"; - } - foreach ($single_children as $post) { - /** @var WP_Post $post */ - $loadedPart = get_template_part('list-item', 'involvement-list-item'); - if ($loadedPart === false) { - TouchPointWP::enqueuePartialsStyle(); - require TouchPointWP::$dir . "/src/templates/parts/involvement-list-item.php"; - } - } - if (count($single_children) > 0) { - echo "
"; - } + $single_children = get_children([ + 'post_parent' => $p->ID, + 'orderby' => 'title', + 'order' => 'ASC', + 'meta_key' => TouchPointWP::INVOLVEMENT_META_KEY, + 'meta_value' => 0, + 'meta_compare' => '>' + ]); + if (count($single_children) > 0) { + echo "
"; + } + foreach ($single_children as $post) { + /** @var WP_Post $post */ + $loadedPart = get_template_part('list-item', 'involvement-list-item'); + if ($loadedPart === false) { + TouchPointWP::enqueuePartialsStyle(); + require TouchPointWP::$dir . "/src/templates/parts/involvement-list-item.php"; + } + } + if (count($single_children) > 0) { + echo "
"; + } +} + +if ($settings->importMeetings && $tps->enable_meeting_cal === "on") { + $meetings = get_children([ + 'post_parent' => $p->ID, + 'order' => 'ASC', + 'orderby' => 'meta_value_num', + 'meta_key' => Meeting::MEETING_START_META_KEY, + 'meta_value' => time(), + 'meta_compare' => '>' + ]); + $count = count($meetings); + if ($count > 0) { + echo "
"; + $heading = sprintf( + // translators: %1$s is the singular name of the event type, %2$s is the plural name of the event type + _n('Upcoming %1$s', 'Upcoming %2$s', 'TouchPoint-WP'), + TouchPointWP::instance()->settings->mc_name_singular, + TouchPointWP::instance()->settings->mc_name_plural + ); + echo "

$heading

"; + } + foreach ($meetings as $post) { + /** @var WP_Post $post */ + $loadedPart = get_template_part('list-item', 'event-list-item'); + if ($loadedPart === false) { + TouchPointWP::enqueuePartialsStyle(); + require TouchPointWP::$dir . "/src/templates/parts/meeting-list-item.php"; + } + } + if (count($meetings) > 0) { + echo "
"; + } } ?> name : false; + +get_header($postType); + +$description = get_the_archive_description(); + +if (have_posts()) { + global $wp_query; + + TouchPointWP::enqueuePartialsStyle(); + ?> + + +
+ + [0-9]{2})-(?P[0-9]{4})$/', $_GET['page'], $matches)) { + $matches = [ + 'mo' => null, + 'yr' => null + ]; + } + + $grid = new CalendarGrid($wp_query, $matches['mo'], $matches['yr']); + + 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 +} + ?> +
+ + +
+
-
id="post-" data-tp-partner="post_id ?>"> +
id="post-" data-tp-partner="post_id() ?>">
invType); @@ -21,7 +22,7 @@ ?> -
data-tp-involvement="post_id ?>"> +
data-tp-involvement="ID ?>">
$child->post_title"; - $childInv = Involvement::fromPost($child); + $childInv = PostTypeCapable::fromPost($child); $metaStrings = []; foreach ($childInv->notableAttributes($notableAttributes) as $a) diff --git a/src/templates/parts/meeting-list-item.php b/src/templates/parts/meeting-list-item.php new file mode 100644 index 00000000..291787d8 --- /dev/null +++ b/src/templates/parts/meeting-list-item.php @@ -0,0 +1,85 @@ + + +
data-tp-involvement="post_id() ?>"> +
+
+ ', esc_url(get_permalink())), ''); + ?> +
+ +
+
+ +
+
+ getActionButtons('list-item', "btn button"); ?> +
+ hierarchical) { + $children = get_children([ + 'post_parent' => $post->ID, + 'orderby' => 'title', + 'order' => 'ASC', + 'post_type' => get_post_type($post) + ]); + if (count($children) > 0) { + echo "
"; + } + foreach ($children as $child) { + /** @var WP_Post $child */ + echo "
"; + $link = get_permalink($child); + echo "

$child->post_title

"; + + $childInv = Involvement::fromPost($child); + + $metaStrings = []; + foreach ($childInv->notableAttributes($notableAttributes) as $a) + { + $metaStrings[] = sprintf( '%s', $a); + } + $m = implode(tp\TouchPointWP\TouchPointWP::$joiner, $metaStrings); + if ($m !== "") { + echo "$m"; + } + + echo "
"; + } + if (count($children) > 0) { + echo "
"; + } + } ?> +
\ No newline at end of file diff --git a/src/templates/parts/person-list-item.php b/src/templates/parts/person-list-item.php index 704d8002..e15eb78a 100644 --- a/src/templates/parts/person-list-item.php +++ b/src/templates/parts/person-list-item.php @@ -18,7 +18,7 @@
getProfileUrl(); + $link = $person->getUserUrl(); $useLink = $link !== null; if ($useLink) { /** @noinspection HtmlUnknownTarget */ diff --git a/touchpoint-wp.php b/touchpoint-wp.php index c23ced2c..4c54688e 100644 --- a/touchpoint-wp.php +++ b/touchpoint-wp.php @@ -6,7 +6,7 @@ * @author James K * @license AGPLv3+ * @link https://github.com/TenthPres/TouchPoint-WP - * @package TouchPoint-WP + * @package TouchPointWP */ /* @@ -14,14 +14,14 @@ Plugin URI: https://github.com/tenthpres/touchpoint-wp Update URI: https://github.com/tenthpres/touchpoint-wp Description: A WordPress Plugin for integrating with TouchPoint Church Management Software. -Version: 0.0.37 +Version: 0.0.95 Author: James K Author URI: https://github.com/jkrrv License: AGPLv3+ Text Domain: TouchPoint-WP -Requires at least: 5.5 -Tested up to: 6.2 -Requires PHP: 7.4 +Requires at least: 6.0 +Tested up to: 6.7 +Requires PHP: 8.0 Release Asset: true */ @@ -29,35 +29,45 @@ // die if called directly. if ( ! defined('WPINC')) { - die; + die; } define("TOUCHPOINT_COMPOSER_ENABLED", file_exists(__DIR__ . '/vendor/autoload.php')); /*** Load everything **/ if (TOUCHPOINT_COMPOSER_ENABLED) { - /** @noinspection PhpIncludeInspection - * @noinspection RedundantSuppression - */ - require_once __DIR__ . '/vendor/autoload.php'; + /** @noinspection PhpIncludeInspection + * @noinspection RedundantSuppression + */ + require_once __DIR__ . '/vendor/autoload.php'; } else { - require_once __DIR__ . "/src/TouchPoint-WP/TouchPointWP_Exception.php"; - require_once __DIR__ . "/src/TouchPoint-WP/TouchPointWP_WPError.php"; - require_once __DIR__ . "/src/TouchPoint-WP/TouchPointWP.php"; - require_once __DIR__ . "/src/TouchPoint-WP/TouchPointWP_Settings.php"; + require_once __DIR__ . "/src/TouchPoint-WP/TouchPointWP_Exception.php"; + require_once __DIR__ . "/src/TouchPoint-WP/TouchPointWP_WPError.php"; + require_once __DIR__ . "/src/TouchPoint-WP/TouchPointWP.php"; + require_once __DIR__ . "/src/TouchPoint-WP/TouchPointWP_Settings.php"; - require_once __DIR__ . "/src/TouchPoint-WP/api.php"; - require_once __DIR__ . "/src/TouchPoint-WP/module.php"; - require_once __DIR__ . "/src/TouchPoint-WP/Utilities/Cleanup.php"; - require_once __DIR__ . "/src/TouchPoint-WP/Utilities/Geo.php"; - require_once __DIR__ . "/src/TouchPoint-WP/Utilities/PersonArray.php"; - require_once __DIR__ . "/src/TouchPoint-WP/Utilities/Http.php"; - require_once __DIR__ . "/src/TouchPoint-WP/geo.php"; + require_once __DIR__ . "/src/TouchPoint-WP/api.php"; + require_once __DIR__ . "/src/TouchPoint-WP/module.php"; + require_once __DIR__ . "/src/TouchPoint-WP/PostTypeCapable.php"; + require_once __DIR__ . "/src/TouchPoint-WP/RegistrationType.php"; + require_once __DIR__ . "/src/TouchPoint-WP/Geo.php"; + require_once __DIR__ . "/src/TouchPoint-WP/Utilities.php"; + require_once __DIR__ . "/src/TouchPoint-WP/Utilities/Cleanup.php"; + require_once __DIR__ . "/src/TouchPoint-WP/Utilities/Translation.php"; + require_once __DIR__ . "/src/TouchPoint-WP/Utilities/PersonArray.php"; + require_once __DIR__ . "/src/TouchPoint-WP/Utilities/StringableArray.php"; + require_once __DIR__ . "/src/TouchPoint-WP/Utilities/DateFormats.php"; + require_once __DIR__ . "/src/TouchPoint-WP/Utilities/DateTimeExtended.php"; + require_once __DIR__ . "/src/TouchPoint-WP/Utilities/Http.php"; + require_once __DIR__ . "/src/TouchPoint-WP/Taxonomies.php"; + require_once __DIR__ . "/src/TouchPoint-WP/hasGeo.php"; - require_once __DIR__ . "/src/TouchPoint-WP/Person.php"; - require_once __DIR__ . "/src/TouchPoint-WP/Involvement.php"; - require_once __DIR__ . "/src/TouchPoint-WP/Location.php"; - require_once __DIR__ . "/src/TouchPoint-WP/Report.php"; + require_once __DIR__ . "/src/TouchPoint-WP/Person.php"; + require_once __DIR__ . "/src/TouchPoint-WP/Meeting.php"; + require_once __DIR__ . "/src/TouchPoint-WP/CalendarGrid.php"; + require_once __DIR__ . "/src/TouchPoint-WP/Involvement.php"; + require_once __DIR__ . "/src/TouchPoint-WP/Location.php"; + require_once __DIR__ . "/src/TouchPoint-WP/Report.php"; } /*** Load (set action hooks, etc.) ***/ diff --git a/wpml-config.xml b/wpml-config.xml index d8035779..622386c7 100644 --- a/wpml-config.xml +++ b/wpml-config.xml @@ -22,6 +22,7 @@ tp_groupclosed tp_hasregquestions tp_regtypeid + tp_siteregtypeid tp_meetings tp_schedules tp_geo_lat