Changeset 3443682
- Timestamp:
- 01/21/2026 01:08:42 AM (2 months ago)
- Location:
- petpress
- Files:
-
- 273 added
- 11 edited
-
tags/2.2.1 (added)
-
tags/2.2.1/.vscode (added)
-
tags/2.2.1/.vscode/launch.json (added)
-
tags/2.2.1/.vscode/settings.json (added)
-
tags/2.2.1/freemius (added)
-
tags/2.2.1/freemius/LICENSE.txt (added)
-
tags/2.2.1/freemius/assets (added)
-
tags/2.2.1/freemius/assets/css (added)
-
tags/2.2.1/freemius/assets/css/admin (added)
-
tags/2.2.1/freemius/assets/css/admin/account.css (added)
-
tags/2.2.1/freemius/assets/css/admin/add-ons.css (added)
-
tags/2.2.1/freemius/assets/css/admin/affiliation.css (added)
-
tags/2.2.1/freemius/assets/css/admin/checkout.css (added)
-
tags/2.2.1/freemius/assets/css/admin/clone-resolution.css (added)
-
tags/2.2.1/freemius/assets/css/admin/common.css (added)
-
tags/2.2.1/freemius/assets/css/admin/connect.css (added)
-
tags/2.2.1/freemius/assets/css/admin/debug.css (added)
-
tags/2.2.1/freemius/assets/css/admin/dialog-boxes.css (added)
-
tags/2.2.1/freemius/assets/css/admin/gdpr-optin-notice.css (added)
-
tags/2.2.1/freemius/assets/css/admin/index.php (added)
-
tags/2.2.1/freemius/assets/css/admin/optout.css (added)
-
tags/2.2.1/freemius/assets/css/admin/plugins.css (added)
-
tags/2.2.1/freemius/assets/css/customizer.css (added)
-
tags/2.2.1/freemius/assets/css/index.php (added)
-
tags/2.2.1/freemius/assets/img (added)
-
tags/2.2.1/freemius/assets/img/index.php (added)
-
tags/2.2.1/freemius/assets/img/petpress-data-manager.png (added)
-
tags/2.2.1/freemius/assets/img/petpress.png (added)
-
tags/2.2.1/freemius/assets/img/plugin-icon.png (added)
-
tags/2.2.1/freemius/assets/img/theme-icon.png (added)
-
tags/2.2.1/freemius/assets/index.php (added)
-
tags/2.2.1/freemius/assets/js (added)
-
tags/2.2.1/freemius/assets/js/index.php (added)
-
tags/2.2.1/freemius/assets/js/jquery.form.js (added)
-
tags/2.2.1/freemius/assets/js/nojquery.ba-postmessage.js (added)
-
tags/2.2.1/freemius/assets/js/postmessage.js (added)
-
tags/2.2.1/freemius/assets/js/pricing (added)
-
tags/2.2.1/freemius/assets/js/pricing/14fb1bd5b7c41648488b06147f50a0dc.svg (added)
-
tags/2.2.1/freemius/assets/js/pricing/178afa6030e76635dbe835e111d2c507.png (added)
-
tags/2.2.1/freemius/assets/js/pricing/27b5a722a5553d9de0170325267fccec.png (added)
-
tags/2.2.1/freemius/assets/js/pricing/4375c4a3ddc6f637c2ab9a2d7220f91e.png (added)
-
tags/2.2.1/freemius/assets/js/pricing/4529cac82a2d1f300d3c4702b7b5e8f3.svg (added)
-
tags/2.2.1/freemius/assets/js/pricing/5480ed23b199531a8cbc05924f26952b.png (added)
-
tags/2.2.1/freemius/assets/js/pricing/b4f3b958f4a019862d81b15f3f8eee3a.svg (added)
-
tags/2.2.1/freemius/assets/js/pricing/c03f665db27af43971565560adfba594.png (added)
-
tags/2.2.1/freemius/assets/js/pricing/cb5fc4f6ec7ada72e986f6e7dde365bf.png (added)
-
tags/2.2.1/freemius/assets/js/pricing/dd89563360f0272635c8f0ab7d7f1402.png (added)
-
tags/2.2.1/freemius/assets/js/pricing/e366d70661d8ad2493bd6afbd779f125.png (added)
-
tags/2.2.1/freemius/assets/js/pricing/f18006f6535a1a6e9c6bfbffafe6f18a.svg (added)
-
tags/2.2.1/freemius/assets/js/pricing/f3aac72a8e63997d6bb888f816457e9b.png (added)
-
tags/2.2.1/freemius/assets/js/pricing/f928f1be99776af83e8e6be4baf8ffe7.svg (added)
-
tags/2.2.1/freemius/assets/js/pricing/fde48e4609a6ddc11d639fc2421f2afd.png (added)
-
tags/2.2.1/freemius/assets/js/pricing/freemius-pricing.js (added)
-
tags/2.2.1/freemius/assets/js/pricing/freemius-pricing.js.LICENSE.txt (added)
-
tags/2.2.1/freemius/config.php (added)
-
tags/2.2.1/freemius/includes (added)
-
tags/2.2.1/freemius/includes/class-freemius-abstract.php (added)
-
tags/2.2.1/freemius/includes/class-freemius.php (added)
-
tags/2.2.1/freemius/includes/class-fs-admin-notices.php (added)
-
tags/2.2.1/freemius/includes/class-fs-api.php (added)
-
tags/2.2.1/freemius/includes/class-fs-garbage-collector.php (added)
-
tags/2.2.1/freemius/includes/class-fs-hook-snapshot.php (added)
-
tags/2.2.1/freemius/includes/class-fs-lock.php (added)
-
tags/2.2.1/freemius/includes/class-fs-logger.php (added)
-
tags/2.2.1/freemius/includes/class-fs-options.php (added)
-
tags/2.2.1/freemius/includes/class-fs-plugin-updater.php (added)
-
tags/2.2.1/freemius/includes/class-fs-security.php (added)
-
tags/2.2.1/freemius/includes/class-fs-storage.php (added)
-
tags/2.2.1/freemius/includes/class-fs-user-lock.php (added)
-
tags/2.2.1/freemius/includes/customizer (added)
-
tags/2.2.1/freemius/includes/customizer/class-fs-customizer-support-section.php (added)
-
tags/2.2.1/freemius/includes/customizer/class-fs-customizer-upsell-control.php (added)
-
tags/2.2.1/freemius/includes/customizer/index.php (added)
-
tags/2.2.1/freemius/includes/debug (added)
-
tags/2.2.1/freemius/includes/debug/class-fs-debug-bar-panel.php (added)
-
tags/2.2.1/freemius/includes/debug/debug-bar-start.php (added)
-
tags/2.2.1/freemius/includes/debug/index.php (added)
-
tags/2.2.1/freemius/includes/entities (added)
-
tags/2.2.1/freemius/includes/entities/class-fs-affiliate-terms.php (added)
-
tags/2.2.1/freemius/includes/entities/class-fs-affiliate.php (added)
-
tags/2.2.1/freemius/includes/entities/class-fs-billing.php (added)
-
tags/2.2.1/freemius/includes/entities/class-fs-entity.php (added)
-
tags/2.2.1/freemius/includes/entities/class-fs-payment.php (added)
-
tags/2.2.1/freemius/includes/entities/class-fs-plugin-info.php (added)
-
tags/2.2.1/freemius/includes/entities/class-fs-plugin-license.php (added)
-
tags/2.2.1/freemius/includes/entities/class-fs-plugin-plan.php (added)
-
tags/2.2.1/freemius/includes/entities/class-fs-plugin-tag.php (added)
-
tags/2.2.1/freemius/includes/entities/class-fs-plugin.php (added)
-
tags/2.2.1/freemius/includes/entities/class-fs-pricing.php (added)
-
tags/2.2.1/freemius/includes/entities/class-fs-scope-entity.php (added)
-
tags/2.2.1/freemius/includes/entities/class-fs-site.php (added)
-
tags/2.2.1/freemius/includes/entities/class-fs-subscription.php (added)
-
tags/2.2.1/freemius/includes/entities/class-fs-user.php (added)
-
tags/2.2.1/freemius/includes/entities/index.php (added)
-
tags/2.2.1/freemius/includes/fs-core-functions.php (added)
-
tags/2.2.1/freemius/includes/fs-essential-functions.php (added)
-
tags/2.2.1/freemius/includes/fs-html-escaping-functions.php (added)
-
tags/2.2.1/freemius/includes/fs-plugin-info-dialog.php (added)
-
tags/2.2.1/freemius/includes/index.php (added)
-
tags/2.2.1/freemius/includes/l10n.php (added)
-
tags/2.2.1/freemius/includes/managers (added)
-
tags/2.2.1/freemius/includes/managers/class-fs-admin-menu-manager.php (added)
-
tags/2.2.1/freemius/includes/managers/class-fs-admin-notice-manager.php (added)
-
tags/2.2.1/freemius/includes/managers/class-fs-cache-manager.php (added)
-
tags/2.2.1/freemius/includes/managers/class-fs-checkout-manager.php (added)
-
tags/2.2.1/freemius/includes/managers/class-fs-clone-manager.php (added)
-
tags/2.2.1/freemius/includes/managers/class-fs-contact-form-manager.php (added)
-
tags/2.2.1/freemius/includes/managers/class-fs-debug-manager.php (added)
-
tags/2.2.1/freemius/includes/managers/class-fs-gdpr-manager.php (added)
-
tags/2.2.1/freemius/includes/managers/class-fs-key-value-storage.php (added)
-
tags/2.2.1/freemius/includes/managers/class-fs-license-manager.php (added)
-
tags/2.2.1/freemius/includes/managers/class-fs-option-manager.php (added)
-
tags/2.2.1/freemius/includes/managers/class-fs-permission-manager.php (added)
-
tags/2.2.1/freemius/includes/managers/class-fs-plan-manager.php (added)
-
tags/2.2.1/freemius/includes/managers/class-fs-plugin-manager.php (added)
-
tags/2.2.1/freemius/includes/managers/index.php (added)
-
tags/2.2.1/freemius/includes/sdk (added)
-
tags/2.2.1/freemius/includes/sdk/Exceptions (added)
-
tags/2.2.1/freemius/includes/sdk/Exceptions/ArgumentNotExistException.php (added)
-
tags/2.2.1/freemius/includes/sdk/Exceptions/EmptyArgumentException.php (added)
-
tags/2.2.1/freemius/includes/sdk/Exceptions/Exception.php (added)
-
tags/2.2.1/freemius/includes/sdk/Exceptions/InvalidArgumentException.php (added)
-
tags/2.2.1/freemius/includes/sdk/Exceptions/OAuthException.php (added)
-
tags/2.2.1/freemius/includes/sdk/Exceptions/index.php (added)
-
tags/2.2.1/freemius/includes/sdk/FreemiusBase.php (added)
-
tags/2.2.1/freemius/includes/sdk/FreemiusWordPress.php (added)
-
tags/2.2.1/freemius/includes/sdk/LICENSE.txt (added)
-
tags/2.2.1/freemius/includes/sdk/index.php (added)
-
tags/2.2.1/freemius/includes/supplements (added)
-
tags/2.2.1/freemius/includes/supplements/fs-essential-functions-1.1.7.1.php (added)
-
tags/2.2.1/freemius/includes/supplements/fs-essential-functions-2.2.1.php (added)
-
tags/2.2.1/freemius/includes/supplements/fs-migration-2.5.1.php (added)
-
tags/2.2.1/freemius/includes/supplements/index.php (added)
-
tags/2.2.1/freemius/index.php (added)
-
tags/2.2.1/freemius/languages (added)
-
tags/2.2.1/freemius/languages/freemius-cs_CZ.mo (added)
-
tags/2.2.1/freemius/languages/freemius-da_DK.mo (added)
-
tags/2.2.1/freemius/languages/freemius-de_DE.mo (added)
-
tags/2.2.1/freemius/languages/freemius-es_ES.mo (added)
-
tags/2.2.1/freemius/languages/freemius-fr_FR.mo (added)
-
tags/2.2.1/freemius/languages/freemius-he_IL.mo (added)
-
tags/2.2.1/freemius/languages/freemius-hu_HU.mo (added)
-
tags/2.2.1/freemius/languages/freemius-it_IT.mo (added)
-
tags/2.2.1/freemius/languages/freemius-ja.mo (added)
-
tags/2.2.1/freemius/languages/freemius-nl_NL.mo (added)
-
tags/2.2.1/freemius/languages/freemius-ru_RU.mo (added)
-
tags/2.2.1/freemius/languages/freemius-ta.mo (added)
-
tags/2.2.1/freemius/languages/freemius-zh_CN.mo (added)
-
tags/2.2.1/freemius/languages/freemius.pot (added)
-
tags/2.2.1/freemius/languages/index.php (added)
-
tags/2.2.1/freemius/require.php (added)
-
tags/2.2.1/freemius/start.php (added)
-
tags/2.2.1/freemius/templates (added)
-
tags/2.2.1/freemius/templates/account (added)
-
tags/2.2.1/freemius/templates/account.php (added)
-
tags/2.2.1/freemius/templates/account/billing.php (added)
-
tags/2.2.1/freemius/templates/account/index.php (added)
-
tags/2.2.1/freemius/templates/account/partials (added)
-
tags/2.2.1/freemius/templates/account/partials/activate-license-button.php (added)
-
tags/2.2.1/freemius/templates/account/partials/addon.php (added)
-
tags/2.2.1/freemius/templates/account/partials/deactivate-license-button.php (added)
-
tags/2.2.1/freemius/templates/account/partials/disconnect-button.php (added)
-
tags/2.2.1/freemius/templates/account/partials/index.php (added)
-
tags/2.2.1/freemius/templates/account/partials/site.php (added)
-
tags/2.2.1/freemius/templates/account/payments.php (added)
-
tags/2.2.1/freemius/templates/add-ons.php (added)
-
tags/2.2.1/freemius/templates/add-trial-to-pricing.php (added)
-
tags/2.2.1/freemius/templates/admin-notice.php (added)
-
tags/2.2.1/freemius/templates/ajax-loader.php (added)
-
tags/2.2.1/freemius/templates/api-connectivity-message-js.php (added)
-
tags/2.2.1/freemius/templates/auto-installation.php (added)
-
tags/2.2.1/freemius/templates/checkout (added)
-
tags/2.2.1/freemius/templates/checkout.php (added)
-
tags/2.2.1/freemius/templates/checkout/frame.php (added)
-
tags/2.2.1/freemius/templates/checkout/process-redirect.php (added)
-
tags/2.2.1/freemius/templates/checkout/redirect.php (added)
-
tags/2.2.1/freemius/templates/clone-resolution-js.php (added)
-
tags/2.2.1/freemius/templates/connect (added)
-
tags/2.2.1/freemius/templates/connect.php (added)
-
tags/2.2.1/freemius/templates/connect/index.php (added)
-
tags/2.2.1/freemius/templates/connect/permission.php (added)
-
tags/2.2.1/freemius/templates/connect/permissions-group.php (added)
-
tags/2.2.1/freemius/templates/contact.php (added)
-
tags/2.2.1/freemius/templates/debug (added)
-
tags/2.2.1/freemius/templates/debug.php (added)
-
tags/2.2.1/freemius/templates/debug/api-calls.php (added)
-
tags/2.2.1/freemius/templates/debug/index.php (added)
-
tags/2.2.1/freemius/templates/debug/logger.php (added)
-
tags/2.2.1/freemius/templates/debug/plugins-themes-sync.php (added)
-
tags/2.2.1/freemius/templates/debug/scheduled-crons.php (added)
-
tags/2.2.1/freemius/templates/email.php (added)
-
tags/2.2.1/freemius/templates/forms (added)
-
tags/2.2.1/freemius/templates/forms/affiliation.php (added)
-
tags/2.2.1/freemius/templates/forms/data-debug-mode.php (added)
-
tags/2.2.1/freemius/templates/forms/deactivation (added)
-
tags/2.2.1/freemius/templates/forms/deactivation/contact.php (added)
-
tags/2.2.1/freemius/templates/forms/deactivation/form.php (added)
-
tags/2.2.1/freemius/templates/forms/deactivation/index.php (added)
-
tags/2.2.1/freemius/templates/forms/deactivation/retry-skip.php (added)
-
tags/2.2.1/freemius/templates/forms/email-address-update.php (added)
-
tags/2.2.1/freemius/templates/forms/index.php (added)
-
tags/2.2.1/freemius/templates/forms/license-activation.php (added)
-
tags/2.2.1/freemius/templates/forms/optout.php (added)
-
tags/2.2.1/freemius/templates/forms/premium-versions-upgrade-handler.php (added)
-
tags/2.2.1/freemius/templates/forms/premium-versions-upgrade-metadata.php (added)
-
tags/2.2.1/freemius/templates/forms/resend-key.php (added)
-
tags/2.2.1/freemius/templates/forms/subscription-cancellation.php (added)
-
tags/2.2.1/freemius/templates/forms/trial-start.php (added)
-
tags/2.2.1/freemius/templates/forms/user-change.php (added)
-
tags/2.2.1/freemius/templates/gdpr-optin-js.php (added)
-
tags/2.2.1/freemius/templates/index.php (added)
-
tags/2.2.1/freemius/templates/js (added)
-
tags/2.2.1/freemius/templates/js/index.php (added)
-
tags/2.2.1/freemius/templates/js/jquery.content-change.php (added)
-
tags/2.2.1/freemius/templates/js/open-license-activation.php (added)
-
tags/2.2.1/freemius/templates/js/permissions.php (added)
-
tags/2.2.1/freemius/templates/js/style-premium-theme.php (added)
-
tags/2.2.1/freemius/templates/partials (added)
-
tags/2.2.1/freemius/templates/partials/index.php (added)
-
tags/2.2.1/freemius/templates/partials/network-activation.php (added)
-
tags/2.2.1/freemius/templates/plugin-icon.php (added)
-
tags/2.2.1/freemius/templates/plugin-info (added)
-
tags/2.2.1/freemius/templates/plugin-info/description.php (added)
-
tags/2.2.1/freemius/templates/plugin-info/features.php (added)
-
tags/2.2.1/freemius/templates/plugin-info/index.php (added)
-
tags/2.2.1/freemius/templates/plugin-info/screenshots.php (added)
-
tags/2.2.1/freemius/templates/pricing.php (added)
-
tags/2.2.1/freemius/templates/secure-https-header.php (added)
-
tags/2.2.1/freemius/templates/sticky-admin-notice-js.php (added)
-
tags/2.2.1/freemius/templates/tabs-capture-js.php (added)
-
tags/2.2.1/freemius/templates/tabs.php (added)
-
tags/2.2.1/includes (added)
-
tags/2.2.1/includes/datamanager (added)
-
tags/2.2.1/includes/datamanager/ppdm-bird-breeds.txt (added)
-
tags/2.2.1/includes/datamanager/ppdm-cat-breeds.txt (added)
-
tags/2.2.1/includes/datamanager/ppdm-dog-breeds.txt (added)
-
tags/2.2.1/includes/datamanager/ppdm-rabbit-breeds.txt (added)
-
tags/2.2.1/includes/datamanager/ppdm-reptile-breeds.txt (added)
-
tags/2.2.1/includes/datamanager/ppdm-rodent-breeds.txt (added)
-
tags/2.2.1/includes/datamanager/ppdm-species-full.txt (added)
-
tags/2.2.1/includes/datamanager/ppdm-species.txt (added)
-
tags/2.2.1/includes/images (added)
-
tags/2.2.1/includes/images/adoption-pending.png (added)
-
tags/2.2.1/includes/images/airdriemedia_cat.jpg (added)
-
tags/2.2.1/includes/images/airdriemedia_dog.jpg (added)
-
tags/2.2.1/includes/images/airdriemedia_other.jpg (added)
-
tags/2.2.1/includes/images/foster.png (added)
-
tags/2.2.1/includes/images/sponsored-pet.png (added)
-
tags/2.2.1/includes/images/working-cat.png (added)
-
tags/2.2.1/includes/pp-admin-style.css (added)
-
tags/2.2.1/includes/pp-admin.js (added)
-
tags/2.2.1/includes/pp-dm-admin.js (added)
-
tags/2.2.1/includes/pp-style.css (added)
-
tags/2.2.1/includes/pp.js (added)
-
tags/2.2.1/includes/untitled folder (added)
-
tags/2.2.1/petpress.php (added)
-
tags/2.2.1/pp-Animal.php (added)
-
tags/2.2.1/pp-AnimalsFirst.php (added)
-
tags/2.2.1/pp-DB.php (added)
-
tags/2.2.1/pp-DataManager.php (added)
-
tags/2.2.1/pp-DataSource.php (added)
-
tags/2.2.1/pp-DetailPage.php (added)
-
tags/2.2.1/pp-Options.php (added)
-
tags/2.2.1/pp-PetFinder.php (added)
-
tags/2.2.1/pp-PetPoint.php (added)
-
tags/2.2.1/pp-PetPressDM.php (added)
-
tags/2.2.1/pp-RescueGroups.php (added)
-
tags/2.2.1/pp-Roster.php (added)
-
tags/2.2.1/pp-Stats.php (added)
-
tags/2.2.1/pp-Utilities.php (added)
-
tags/2.2.1/readme.txt (added)
-
tags/2.2.1/templates (added)
-
tags/2.2.1/templates/pp-detail-template.php (added)
-
trunk/includes/pp-style.css (modified) (2 diffs)
-
trunk/includes/pp.js (modified) (2 diffs)
-
trunk/petpress.php (modified) (6 diffs)
-
trunk/pp-DataSource.php (modified) (3 diffs)
-
trunk/pp-DetailPage.php (modified) (2 diffs)
-
trunk/pp-Options.php (modified) (8 diffs)
-
trunk/pp-PetPoint.php (modified) (3 diffs)
-
trunk/pp-RescueGroups.php (modified) (2 diffs)
-
trunk/pp-Roster.php (modified) (5 diffs)
-
trunk/pp-Utilities.php (modified) (6 diffs)
-
trunk/readme.txt (modified) (2 diffs)
Legend:
- Unmodified
- Added
- Removed
-
petpress/trunk/includes/pp-style.css
r3438868 r3443682 205 205 width: 100%; 206 206 height: 100%; 207 background: rgba(0, 0, 0, 0.7); 208 z-index: 3; 207 background: rgba(0, 0, 0, 0.9); 208 z-index: 9999; 209 justify-content: center; 210 align-items: center; 211 } 212 213 #pp_lightbox_content { 214 display: flex; 215 flex-direction: column; 216 align-items: center; 217 justify-content: center; 218 height: 100%; 219 width: 100%; 220 padding: 60px 80px; 221 box-sizing: border-box; 209 222 } 210 223 211 224 #pp_lightbox img { 212 max-width: 80%; 213 max-height: 80%; 214 display: block; 215 margin: auto; 216 margin-top: 20%; 217 z-index: 4; 218 } 219 220 #pp_lightbox_close_btn, #pp_lightbox_prev_btn, #pp_lightbox_next_btn { 225 width: 80vw; 226 max-height: calc(100vh - 100px); 227 object-fit: contain; 228 } 229 230 #pp_lightbox_counter { 231 color: white; 232 font-size: 14px; 233 margin-top: 10px; 234 text-align: center; 235 } 236 237 #pp_lightbox_close_btn, 238 #pp_lightbox_prev_btn, 239 #pp_lightbox_next_btn { 240 position: absolute; 241 color: white; 242 font-size: 36px; 243 cursor: pointer; 244 padding: 16px; 245 user-select: none; 246 transition: opacity 0.2s; 247 z-index: 10000; 248 } 249 250 #pp_lightbox_close_btn:hover, 251 #pp_lightbox_prev_btn:hover, 252 #pp_lightbox_next_btn:hover { 253 opacity: 0.7; 254 } 255 256 #pp_lightbox_close_btn { 257 top: 10px; 258 right: 20px; 259 font-size: 40px; 260 } 261 262 #pp_lightbox_prev_btn, 263 #pp_lightbox_next_btn { 264 top: 50%; 265 transform: translateY(-50%); 266 } 267 268 #pp_lightbox_prev_btn { 269 left: 20px; 270 } 271 272 #pp_lightbox_next_btn { 273 right: 20px; 274 } 275 276 @media (max-width: 768px) { 277 #pp_lightbox_content { 278 padding: 50px 10px; 279 } 280 281 #pp_lightbox_prev_btn, 282 #pp_lightbox_next_btn { 283 font-size: 24px; 284 padding: 12px; 285 } 286 287 #pp_lightbox_prev_btn { 288 left: 5px; 289 } 290 291 #pp_lightbox_next_btn { 292 right: 5px; 293 } 294 } 295 296 .pp_hidden_tile 297 { 298 display:none; 299 } 300 .pp_shown_tile{ 301 display:block; 302 } 303 304 #pp_sort_btn_container { 305 display: flex; 306 width: 100%; 307 gap: 8px; 308 } 309 310 .pp_sort_btn { 311 flex: 1; 312 white-space: nowrap; 313 padding: 8px 12px; 314 position: relative; 315 transition: all 0.15s ease; 316 text-align: center; 317 } 318 319 .pp_sort_btn.pp_sort_active { 320 /* background: linear-gradient(to bottom, #d4ebf7 5%, #b8dff5 100%); */ 321 /* color: #005a87; */ 322 border-color: #666; 323 font-weight: bold; 324 } 325 .pp_sort_btn.pp_sort_active::before, 326 .pp_sort_btn.pp_sort_active::after { 221 327 position: absolute; 222 328 top: 50%; 223 329 transform: translateY(-50%); 224 color: white; 225 font-size: 30px; 226 cursor: pointer; 227 padding:2em; 228 } 229 230 #pp_lightbox_close_btn { 231 top:10px; 232 right: 10px; 233 } 234 235 #pp_lightbox_prev_btn { 236 left: 10px; 237 } 238 239 #pp_lightbox_next_btn { 240 right: 10px; 241 } 242 243 .pp_hidden_tile 244 { 245 display:none; 246 } 247 .pp_shown_tile{ 248 display:block; 249 } 250 251 #pp_sort_btn_container 252 { 253 display:flex; 254 } 255 256 .pp_sort_btn { 257 display: inline; 258 margin-right: 10px; 259 flex:1; 260 330 font-size: 12px; 331 line-height: 1; 332 } 333 334 .pp_sort_btn.pp_sort_active::before { 335 left: 6px; 336 } 337 338 .pp_sort_btn.pp_sort_active::after { 339 right: 6px; 340 } 341 342 .pp_sort_btn.pp_sort_asc::before, 343 .pp_sort_btn.pp_sort_asc::after { 344 content: "▼"; 345 } 346 347 .pp_sort_btn.pp_sort_desc::before, 348 .pp_sort_btn.pp_sort_desc::after { 349 content: "▲"; 350 } 351 352 @media (max-width: 480px) { 353 .pp_sort_btn { 354 padding: 6px 4px; 355 font-size: 0.85em; 356 } 357 358 .pp_sort_btn.pp_sort_active::before, 359 .pp_sort_btn.pp_sort_active::after { 360 font-size: 10px; 361 } 362 363 .pp_sort_btn.pp_sort_active::before { 364 left: 4px; 365 } 366 367 .pp_sort_btn.pp_sort_active::after { 368 right: 4px; 369 } 261 370 } 262 371 … … 297 406 } 298 407 299 /* Remove margin from the last button */300 .pp_sort_btn:last-child {301 margin-right: 0;302 }303 408 304 409 .pp_stickie1_img { position: absolute; -
petpress/trunk/includes/pp.js
r3438868 r3443682 8 8 9 9 10 jQuery("#pp_moreinfo li").each(function() {11 var text = jQuery(this)12 .contents()13 .filter(function() { return this.nodeType === 3; })14 .first();15 16 if (text.length && jQuery.trim(text.text()).indexOf("Price:") === 0) {17 text[0].nodeValue = text[0].nodeValue.replace("Price:", "Adoption Fee:");18 }19 });20 21 22 23 24 10 pp_globals.intialNumTiles = jQuery('.pp_tile_container').attr('numtiles'); 25 11 … … 33 19 Lightbox effect for detail page 34 20 */ 35 if (typeof cPhoto1 !== 'undefined') 36 { 21 (function() { 22 var $lightbox = jQuery('#pp_lightbox'); 23 if (!$lightbox.length) return; 24 25 var photosData = $lightbox.attr('data-photos'); 26 if (!photosData) return; 27 37 28 var images = []; 38 39 if (cPhoto1.trim() !== '') { 40 images.push(cPhoto1); 41 } 42 if (cPhoto2.trim() !== '') { 43 images.push(cPhoto2); 44 } 45 if (cPhoto3.trim() !== '') { 46 images.push(cPhoto3); 47 } 29 try { 30 images = JSON.parse(photosData); 31 } catch (e) { 32 return; 33 } 34 35 if (!images.length) return; 48 36 49 37 var currentIndex = 0; 38 var $img = jQuery('#pp_lightboxImg'); 39 var $counter = jQuery('#pp_lightbox_counter'); 40 var $prevBtn = jQuery('#pp_lightbox_prev_btn'); 41 var $nextBtn = jQuery('#pp_lightbox_next_btn'); 42 43 function updateNavVisibility() { 44 if (images.length <= 1) { 45 $prevBtn.hide(); 46 $nextBtn.hide(); 47 $counter.hide(); 48 } else { 49 $prevBtn.show(); 50 $nextBtn.show(); 51 $counter.show(); 52 } 53 } 50 54 51 55 function showImage(index) { 52 jQuery('#pp_lightboxImg').attr('src', images[index]);53 56 currentIndex = index; 54 } 55 56 showImage(currentIndex); 57 58 jQuery('.pp_lightbox_trigger').click(function () { 59 var clickedIndex = jQuery(this).attr('data-index'); 60 showImage(clickedIndex); 61 jQuery('#pp_lightbox').fadeIn(); 62 }); 57 $img.attr('src', images[index]); 58 $counter.text((index + 1) + ' / ' + images.length); 59 updateNavVisibility(); 60 } 61 62 function openLightbox(index) { 63 showImage(index); 64 $lightbox.fadeIn(200); 65 jQuery('body').css('overflow', 'hidden'); 66 } 63 67 64 68 function closeLightbox() { 65 jQuery('#pp_lightbox').fadeOut(); 66 } 67 68 jQuery('#pp_lightbox, #pp_lightbox_close_btn').click(function (e) { 69 if (e.target.id === 'pp_lightbox' || e.target.id === 'pp_lightbox_close_btn') { 69 $lightbox.fadeOut(200); 70 jQuery('body').css('overflow', ''); 71 } 72 73 function nextImage() { 74 if (images.length > 1) { 75 showImage((currentIndex + 1) % images.length); 76 } 77 } 78 79 function prevImage() { 80 if (images.length > 1) { 81 showImage((currentIndex - 1 + images.length) % images.length); 82 } 83 } 84 85 // Open lightbox when clicking trigger images 86 jQuery('.pp_lightbox_trigger').on('click', function() { 87 var index = parseInt(jQuery(this).attr('data-index'), 10) || 0; 88 openLightbox(index); 89 }); 90 91 // Close on background click (darkened area, not image or buttons) 92 $lightbox.on('click', function(e) { 93 var targetId = e.target.id; 94 if (targetId === 'pp_lightbox' || targetId === 'pp_lightbox_content' || targetId === 'pp_lightbox_close_btn') { 70 95 closeLightbox(); 71 96 } 72 97 }); 73 98 74 jQuery('#pp_lightbox_prev_btn').click(function (e) { 99 // Navigation buttons 100 $prevBtn.on('click', function(e) { 75 101 e.stopPropagation(); 76 currentIndex = (currentIndex - 1 + images.length) % images.length; 77 showImage(currentIndex); 78 }); 79 80 jQuery('#pp_lightbox_next_btn').click(function (e) { 102 prevImage(); 103 }); 104 105 $nextBtn.on('click', function(e) { 81 106 e.stopPropagation(); 82 currentIndex = (currentIndex + 1) % images.length;83 showImage(currentIndex);84 }); 85 86 jQuery('#pp_lightboxImg').click(function(e) {107 nextImage(); 108 }); 109 110 // Prevent closing when clicking the image 111 $img.on('click', function(e) { 87 112 e.stopPropagation(); 88 113 }); 89 } 114 115 // Keyboard navigation 116 jQuery(document).on('keydown', function(e) { 117 if (!$lightbox.is(':visible')) return; 118 119 switch (e.key) { 120 case 'Escape': 121 closeLightbox(); 122 break; 123 case 'ArrowLeft': 124 prevImage(); 125 break; 126 case 'ArrowRight': 127 nextImage(); 128 break; 129 } 130 }); 131 132 // Touch/swipe support 133 var touchStartX = 0; 134 var touchEndX = 0; 135 136 $lightbox.on('touchstart', function(e) { 137 touchStartX = e.originalEvent.changedTouches[0].screenX; 138 }); 139 140 $lightbox.on('touchend', function(e) { 141 touchEndX = e.originalEvent.changedTouches[0].screenX; 142 var diff = touchStartX - touchEndX; 143 if (Math.abs(diff) > 50) { 144 if (diff > 0) { 145 nextImage(); 146 } else { 147 prevImage(); 148 } 149 } 150 }); 151 })(); 90 152 }); 91 153 92 154 function setSortIndicator(attributeIN){ 155 var buttonId; 156 var newSortOrder = 0; 157 93 158 switch (attributeIN){ 94 159 case "data-name": 95 theArrow = "#sortNameBTN > span"; 96 newSortOrder = 0; // a-z 160 buttonId = "#sortNameBTN"; 97 161 break; 98 162 case "data-age": 99 163 case "data-agegroupindex": 100 theArrow = "#sortAgeBTN > span"; 101 newSortOrder = 0; // youngest to oldest 164 buttonId = "#sortAgeBTN"; 102 165 break; 103 166 case "data-weight": 104 167 case "data-sizeindex": 105 theArrow = "#sortSizeBTN > span"; 106 newSortOrder = 0; // smallest to largest 168 buttonId = "#sortSizeBTN"; 107 169 break; 108 170 case "data-daysin": 109 theArrow = "#sortDaysInBTN > span"; 110 newSortOrder = 0; // long term to short term 171 buttonId = "#sortDaysInBTN"; 111 172 break; 112 173 } 113 174 114 sameButtonSelected = (jQuery(theArrow).hasClass("dashicons-arrow-up-alt") || jQuery(theArrow).hasClass("dashicons-arrow-down-alt")); 115 116 // sortUp = jQuery(theArrow).hasClass("dashicons-arrow-up-alt"); 117 // sortDown = jQuery(theArrow).hasClass("dashicons-arrow-down-alt"); 118 119 jQuery(".pp_sort_btn > span").removeClass("dashicons-arrow-up-alt"); 120 jQuery(".pp_sort_btn > span").removeClass("dashicons-arrow-down-alt"); 121 122 if (!sameButtonSelected) { pp_globals.sortOrder = newSortOrder;} 123 if (pp_globals.sortOrder == 0) 124 { 125 jQuery(theArrow).addClass("dashicons-arrow-down-alt"); 175 var $btn = jQuery(buttonId); 176 var sameButtonSelected = $btn.hasClass("pp_sort_active"); 177 178 // Clear all buttons 179 jQuery(".pp_sort_btn").removeClass("pp_sort_active pp_sort_asc pp_sort_desc"); 180 181 // Toggle sort order if same button clicked 182 if (!sameButtonSelected) { 183 pp_globals.sortOrder = newSortOrder; 184 } 185 186 // Apply active state and direction 187 $btn.addClass("pp_sort_active"); 188 if (pp_globals.sortOrder == 0) { 189 $btn.addClass("pp_sort_desc"); 126 190 } else { 127 jQuery(theArrow).addClass("dashicons-arrow-up-alt");191 $btn.addClass("pp_sort_asc"); 128 192 } 129 193 } -
petpress/trunk/petpress.php
r3438868 r3443682 4 4 * Plugin Name: PetPress 5 5 * Plugin URI: https://www.airdriemedia.com/petpress 6 * Version: 2.2 6 * Version: 2.2.1 7 7 * Description: Display adoptable animals on your shelter's website. Compatible with PetPoint and AnimalsFirst or without external data source. 8 8 * Author: Airdrie Media … … 13 13 */ 14 14 if ( !defined( 'PP_PLUGIN_VERSION' ) ) { 15 define( 'PP_PLUGIN_VERSION', '2.2 ' );15 define( 'PP_PLUGIN_VERSION', '2.2.1' ); 16 16 } 17 17 if ( !defined( 'PP_DATABASE_VERSION' ) ) { … … 93 93 require_once 'pp-Utilities.php'; 94 94 require_once 'pp-Animal.php'; 95 // Cron retry and self-healing hooks (after ppUtils is loaded) 96 add_action( 'petpress_cron_retry', 'ppUtils::retryCronSchedule' ); 97 add_action( 'admin_init', 'ppUtils::ensureCronScheduled' ); 95 98 // todo check to see if option is set to this website 96 99 $options = get_option( 'petpress_options' ); … … 107 110 add_action( 'wp_enqueue_scripts', [$this, 'pp_scripts'] ); 108 111 add_action( 'init', [$this, 'petpress_update_check'] ); 109 //add_filter( 'cron_schedules', [$this,'petpress_add_everyhalfhour_schedule']);110 //add_filter( 'cron_schedules', [$this,'petpress_add_everyquarterhour_schedule']);111 112 add_filter( 'cron_schedules', 'ppUtils::add_30_schedule' ); 112 113 add_filter( 'cron_schedules', 'ppUtils::add_15_schedule' ); … … 187 188 $db = new ppDB(); 188 189 $db->activate(); 189 //ppUtils::createCronJob("30m", "PetPressDB"); // JWB initial interval?190 190 } catch ( InvalidArgumentException $e ) { 191 // Handle the error192 191 logError( "Could not create database: " . $e->getMessage() ); 193 192 } … … 203 202 } 204 203 } catch ( InvalidArgumentException $e ) { 205 // Handle the error206 204 echo "Could not create database: " . $e->getMessage(); 207 205 } -
petpress/trunk/pp-DataSource.php
r3438868 r3443682 10 10 11 11 public function __construct(){ 12 // add_filter( 'cron_schedules', 'pp_add_everyhalfhour_schedule' ); // JWB unused (?) 12 13 13 } 14 14 … … 47 47 global $wpdb; 48 48 49 $theCurDate = ppUtils::getDBTime(0); 50 51 $table_name = $wpdb->prefix . "petpress_animals"; 52 53 $dateofbirth = $critterIN->get_dateofbirth(); 54 if (isset($dateofbirth)){ 55 $dateofbirth = date("Y-m-d", strtotime($dateofbirth)); 56 } 57 58 $lastintakedate = $critterIN->get_lastintakedate(); 59 if (isset($lastintakedate)){ 60 $lastintakedate = date("Y-m-d", strtotime($lastintakedate)); 61 } 62 $rc = $wpdb->replace( 63 $table_name, 64 array( 65 'time' => $theCurDate, 66 'id' => $critterIN->get_id(), 67 'adoptionapplicationurl' => $critterIN->get_adoptionapplicationurl(), 68 'agegroup' => $critterIN->get_agegroup(), 69 'altered' => $critterIN->get_altered(), 70 'arn' => $critterIN->get_arn(), 71 'behaviorresult' => $critterIN->get_behaviorresult(), 72 'behaviortestlist' => $critterIN->get_behaviortestlist(), 73 'breed' => ppUtils::unscrambleBreedName($critterIN->get_breed()) , 74 'chipnumber' => $critterIN->get_chipnumber(), 75 'coatlength' => $critterIN->get_coatlength() , 76 'companyid' => $critterIN->get_companyid() , 77 'colorpattern' => $critterIN->get_colorpattern() , 78 'dateofbirth' => $dateofbirth , 79 'declawed' => $critterIN->get_declawed(), 80 'featured' => $critterIN->get_featured(), 81 'housetrained' => $critterIN->get_housetrained(), 82 'lastintakedate' => $lastintakedate , 83 'livedwithanimals' => $critterIN->get_livedwithanimals() , 84 'livedwithanimaltypes' => $critterIN->get_livedwithanimaltypes() , 85 'livedwithchildren' => $critterIN->get_livedwithchildren() , 86 'location' => $critterIN->get_location(), 87 'memo' => $critterIN->get_memo(), 88 'name' => $critterIN->get_name(), 89 'nocats' => $critterIN->get_nocats(), 90 'nodogs' => $critterIN->get_nodogs(), 91 'nokids' => $critterIN->get_nokids(), 92 'onhold' => $critterIN->get_onhold(), 93 'price' => $critterIN->get_price(), 94 'primarycolor' => $critterIN->get_primarycolor(), 95 'photo1' => $critterIN->get_photo1(), 96 'photo2' => $critterIN->get_photo2(), 97 'photo3' => $critterIN->get_photo3(), 98 'reasonforsurrender' => $critterIN->get_reasonforsurrender(), 99 'secondarybreed' => ppUtils::unscrambleBreedName($critterIN->get_secondarybreed()), 100 'secondarycolor' => $critterIN->get_secondarycolor(), 101 'sex' => $critterIN->get_sex(), 102 'shotscurrent' => $critterIN->get_shotscurrent() , 103 'sitename' => $critterIN->get_sitename(), 104 'size' => $critterIN->get_size(), 105 'specialneeds' => $critterIN->get_specialneeds(), 106 'species' => $critterIN->get_species(), 107 'stage' => $critterIN->get_stage(), 108 'sublocation' => $critterIN->get_sublocation(), 109 'systemofrecord' => $critterIN->get_systemofrecord(), 110 'videoid' => $critterIN->get_videoid(), 111 'weight' => $critterIN->get_weight() 112 ) 113 ) ; 114 $testval = $rc; 49 50 $theCurDate = ppUtils::getDBTime(0); 51 52 $table_name = $wpdb->prefix . "petpress_animals"; 53 54 $dateofbirth = $critterIN->get_dateofbirth(); 55 if (isset($dateofbirth)){ 56 $dateofbirth = date("Y-m-d", strtotime($dateofbirth)); 57 } 58 59 $lastintakedate = $critterIN->get_lastintakedate(); 60 if (isset($lastintakedate)){ 61 $lastintakedate = date("Y-m-d", strtotime($lastintakedate)); 62 } 63 $rc = $wpdb->replace( 64 $table_name, 65 array( 66 'time' => $theCurDate, 67 'id' => $critterIN->get_id(), 68 'adoptionapplicationurl' => $critterIN->get_adoptionapplicationurl(), 69 'agegroup' => $critterIN->get_agegroup(), 70 'altered' => $critterIN->get_altered(), 71 'arn' => $critterIN->get_arn(), 72 'behaviorresult' => $critterIN->get_behaviorresult(), 73 'behaviortestlist' => $critterIN->get_behaviortestlist(), 74 'breed' => ppUtils::unscrambleBreedName($critterIN->get_breed()) , 75 'chipnumber' => $critterIN->get_chipnumber(), 76 'coatlength' => $critterIN->get_coatlength() , 77 'companyid' => $critterIN->get_companyid() , 78 'colorpattern' => $critterIN->get_colorpattern() , 79 'dateofbirth' => $dateofbirth , 80 'declawed' => $critterIN->get_declawed(), 81 'featured' => $critterIN->get_featured(), 82 'housetrained' => $critterIN->get_housetrained(), 83 'lastintakedate' => $lastintakedate , 84 'livedwithanimals' => $critterIN->get_livedwithanimals() , 85 'livedwithanimaltypes' => $critterIN->get_livedwithanimaltypes() , 86 'livedwithchildren' => $critterIN->get_livedwithchildren() , 87 'location' => $critterIN->get_location(), 88 'memo' => $critterIN->get_memo(), 89 'name' => $critterIN->get_name(), 90 'nocats' => $critterIN->get_nocats(), 91 'nodogs' => $critterIN->get_nodogs(), 92 'nokids' => $critterIN->get_nokids(), 93 'onhold' => $critterIN->get_onhold(), 94 'price' => $critterIN->get_price(), 95 'primarycolor' => $critterIN->get_primarycolor(), 96 'photo1' => $critterIN->get_photo1(), 97 'photo2' => $critterIN->get_photo2(), 98 'photo3' => $critterIN->get_photo3(), 99 'reasonforsurrender' => $critterIN->get_reasonforsurrender(), 100 'secondarybreed' => ppUtils::unscrambleBreedName($critterIN->get_secondarybreed()), 101 'secondarycolor' => $critterIN->get_secondarycolor(), 102 'sex' => $critterIN->get_sex(), 103 'shotscurrent' => $critterIN->get_shotscurrent() , 104 'sitename' => $critterIN->get_sitename(), 105 'size' => $critterIN->get_size(), 106 'specialneeds' => $critterIN->get_specialneeds(), 107 'species' => $critterIN->get_species(), 108 'stage' => $critterIN->get_stage(), 109 'sublocation' => $critterIN->get_sublocation(), 110 'systemofrecord' => $critterIN->get_systemofrecord(), 111 'videoid' => $critterIN->get_videoid(), 112 'weight' => $critterIN->get_weight() 113 ) 114 ) ; 115 $testval = $rc; 116 115 117 } 116 118 … … 133 135 ppUtils::petpress_log("In fetchAnimals, data purged. Writing new animal data START at " . date('Y-m-d H:i:s')); 134 136 foreach ($roster as $critter) { 135 $this->writeOneAnimal($critter); 137 if ( is_wp_error( $critter ) ) { 138 ppUtils::petpress_log(sprintf('writeOneAnimal will not be called as wp_error was returned within roster. (%s:%d)', __METHOD__, __LINE__ )); 139 140 } 141 else { 142 $this->writeOneAnimal($critter); 143 } 136 144 } 137 145 ppUtils::petpress_log("In fetchAnimals, animals written END. Called setFetchTime at " . date('Y-m-d H:i:s')); -
petpress/trunk/pp-DetailPage.php
r3438868 r3443682 371 371 if ($cBehaviorResult = $critter->get_behaviorresult()) 372 372 $h .= "<li>Behavior test result: <span data-behaviorresult='{$cBehaviorResult}'>{$cBehaviorResult}</span></li>\n"; 373 373 /* 374 374 if ($optPrice) 375 375 if ($cPrice = $critter->get_price()) 376 376 $h .= "<li>Price: $<span data-price='{$cPrice}'>{$cPrice}</span></li>\n"; 377 377 */ 378 if ( $optPrice ) { 379 if ($cPrice = $critter->get_price()){ 380 $label = ppUtils::optionValue('price_label'); 381 if ($label == 0) {$label = "Price";} 382 $h .= "<li>$label: $<span data-price='{$cPrice}'>{$cPrice}</span></li>\n"; 383 //echo '<div class="price">' . esc_html($animal->price) . '</div>'; 384 }} 385 386 378 387 if ($optShotsCurrent) 379 388 if ($cShotsCurrent = $critter->get_shotscurrent()){ … … 455 464 456 465 457 $lightboxscript = <<< lightboxcode 458 <div id="pp_lightbox"> 459 <span id="pp_lightbox_close_btn">×</span> 460 <span id="pp_lightbox_prev_btn"><</span> 461 <img id="pp_lightboxImg" src="" alt="Lightbox Image"> 462 <span id="pp_lightbox_next_btn">></span> 463 </div> 464 <script> 465 var cPhoto1 = "$cPhoto1"; 466 var cPhoto2 = "$cPhoto2"; 467 var cPhoto3 = "$cPhoto3"; 468 </script> 469 lightboxcode; 470 471 $h .= $lightboxscript; 466 $photos = array_filter([$cPhoto1, $cPhoto2, $cPhoto3], function($p) { 467 return !empty($p); 468 }); 469 $photosJson = esc_attr(wp_json_encode(array_values($photos))); 470 471 $h .= '<div id="pp_lightbox" data-photos="' . $photosJson . '">'; 472 $h .= '<span id="pp_lightbox_close_btn" role="button" aria-label="Close lightbox">×</span>'; 473 $h .= '<span id="pp_lightbox_prev_btn" role="button" aria-label="Previous image">❮</span>'; 474 $h .= '<div id="pp_lightbox_content">'; 475 $h .= '<img id="pp_lightboxImg" src="" alt="' . esc_attr($cName) . '">'; 476 $h .= '<span id="pp_lightbox_counter"></span>'; 477 $h .= '</div>'; 478 $h .= '<span id="pp_lightbox_next_btn" role="button" aria-label="Next image">❯</span>'; 479 $h .= '</div>'; 472 480 473 481 -
petpress/trunk/pp-Options.php
r3438868 r3443682 23 23 return $new_value; 24 24 } 25 /* 26 public function setFastRefreshOption($new_value, $old_value, $option) { 27 28 if (($new_value['fastrefresh'] ?? 0) == 1) { 29 ppUtils::createCronJob("qh"); 30 } 31 else 32 { 33 ppUtils::createCronJob("hh"); 34 } 35 return $new_value; 36 } 37 */ 25 38 26 39 27 public function setRefreshInterval($new_value, $old_value, $option){ … … 102 90 data manager to create / modify / delete animal data directly on your website without a separate back-end system. 103 91 <?php 104 $h = ppUtils::get_last_petpress_error(24 );92 $h = ppUtils::get_last_petpress_error(24, true); 105 93 echo $h; 106 94 ?> … … 210 198 211 199 add_settings_field( 212 'apikey', // Field ID 213 'Rescue Groups API Key', // Field title 214 array($this, 'rg_apikey_callback'), // Callback function 215 'petpress', // Page slug 216 'pet_press_pro_rescuegroups_section' // Section ID 217 ); 218 219 220 200 'rg_apikey', 201 'Rescue Groups API Key', 202 array($this, 'rg_apikey_callback'), 203 'petpress', 204 'pet_press_pro_rescuegroups_section' 205 ); 206 207 add_settings_field( 208 'rg_orgid', 209 'Rescue Groups Organization ID', 210 array($this, 'rg_orgid_callback'), 211 'petpress', 212 'pet_press_pro_rescuegroups_section' 213 ); 214 221 215 add_settings_section( 222 216 'pet_press_pro_petfinder_section', // Section ID … … 479 473 'pet_press_pro_main_section' 480 474 ); 475 /* 481 476 add_settings_field( 482 477 'price_Checkbox_Element', … … 486 481 'pet_press_pro_main_section' 487 482 ); 483 */ 484 add_settings_field( 485 'price_element', 486 __('', 'petpress'), 487 [$this, 'cb_price_element_callback'], 488 'petpress', 489 'pet_press_pro_main_section' 490 ); 488 491 489 492 add_settings_field( … … 601 604 } 602 605 603 public function rg_apikey_callback() {606 public function rg_apikey_callback() { 604 607 $options = get_option('petpress_options'); 605 608 ?> 606 609 <input type="text" name="petpress_options[rg_authkey]" style="width:60ch" value="<?php echo isset($options['rg_authkey']) ? esc_attr($options['rg_authkey']) : ''; ?>" /> 607 610 <br>This is your RescueGroups.org API key. 611 <?php 612 } 613 614 public function rg_orgid_callback() { 615 $options = get_option('petpress_options'); 616 ?> 617 <input type="text" name="petpress_options[rg_orgid]" style="width:20ch" value="<?php echo isset($options['rg_orgid']) ? esc_attr($options['rg_orgid']) : ''; ?>" /> 618 <br>Your RescueGroups.org Organization ID (numeric). You can find this in your RescueGroups account settings. 608 619 <?php 609 620 } … … 976 987 echo '<label for="onhold_checkbox"> Show "on hold" status information on detail page (note: will not appear if the option "do not show on hold animals" option above is selected.)'; 977 988 } 989 /* 978 990 public function cb_price_element_callback() { 979 991 $setVal = $this->getOption('price'); … … 983 995 echo '<label for="price_checkbox"> Show price information on detail page'; 984 996 } 997 */ 998 public function cb_price_element_callback() { 999 $priceVal = $this->getOption('price', 0); 1000 $labelVal = $this->getOption('price_label', 'Price'); // Default to 'Price' 1001 1002 $options = [ 1003 'Price' => 'Price', 1004 'Adoption Fee' => 'Adoption Fee' 1005 ]; 1006 1007 echo '<label style="display: flex; align-items: center; gap: 10px;">'; 1008 echo '<input type="checkbox" id="price_checkbox" name="petpress_options[price]" value="1" '; 1009 echo esc_attr(checked(1, $priceVal, false)); 1010 echo '/> '; 1011 echo esc_html__('Show price information on detail page, labeled as ', 'petpress'); 1012 1013 echo '<select id="price_label" name="petpress_options[price_label]">'; 1014 foreach ($options as $value => $label) { 1015 printf( 1016 '<option value="%s" %s>%s</option>', 1017 esc_attr($value), 1018 selected($labelVal, $value, false), 1019 esc_html($label) 1020 ); 1021 } 1022 echo '</select>'; 1023 echo '</label>'; 1024 } 985 1025 986 1026 public function cb_social_element_callback() { -
petpress/trunk/pp-PetPoint.php
r3438868 r3443682 54 54 if (is_wp_error($outputWS)) { 55 55 error_log('PetPress getRosterXML WP_Error: ' . $outputWS->get_error_message()); 56 return new WP_Error('PetPressError', sprintf(__('PetPress Error: %s(%d) Network error', 'petpress'), __METHOD__, __LINE__), ['status' => 400]); 56 return new WP_Error( 57 'PetPressError', 58 sprintf( 59 __('PetPress Error: %s(%d) Getting Roster: %s', 'petpress'), 60 __METHOD__, 61 __LINE__, 62 $outputWS->get_error_message() 63 ), 64 ['code' => $outputWS->get_error_code()] 65 ); 66 57 67 } 58 68 … … 240 250 241 251 if ( is_wp_error( $outputWS ) ) { 242 //throw new ErrorException("Error getting single animal from ". $urlWSComplete ." (LN#" . __LINE__ . "). This may be a temporary connection issue.");243 252 return new WP_Error('PetPressError', sprintf(__('PetPress Error: %s(%d) Error getting single animal from %s. This may be a temporary connection issue.','petpress'), __METHOD__, __LINE__, $urlWSComplete ), array( 'status' => 400 )); 244 253 … … 256 265 if (is_string($outputWS["body"])){ 257 266 if (strpos($outputWS["body"], "502 Bad Gateway") !== false) { 258 // throw new ErrorException("There is currently a problem with the PetPoint gateway (502 Bad Gateway) (LN#" . __LINE__ . "). This may be a temporary connection issue.");259 267 return new WP_Error('PetPressError', sprintf(__('PetPress Error: %s(%d) There is currently a problem with the PetPoint gateway (502 Bad Gateway)','petpress'), __METHOD__, __LINE__ ), array( 'status' => 502 )); 260 268 -
petpress/trunk/pp-RescueGroups.php
r3438868 r3443682 25 25 public function getRosterXML() 26 26 { 27 28 27 $options = get_option('petpress_options'); 29 28 30 if (is_array($options) && isset($options['rg_authkey'])) { 31 if (empty($options['rg_authkey'])){ 32 //throw new ErrorException("Animals First API key is not set in PetPress settings."); 33 // 34 return new WP_Error( 35 'PetPressError', sprintf(__( 36 'PetPress Error: %s(%d) Rescue Groups API key is not set in PetPress settings.' 37 ,'petpress'), __METHOD__, __LINE__ ), array( 'status' => 400 ) 38 ); 39 } 40 $this->apikey = $options['rg_authkey']; 41 // Use $authkey 42 } 43 44 //$apiURL = "https://api.rescuegroups.org/http/v2.json" 45 46 $apiUrl = "https://api.rescuegroups.org/v5"; 47 48 $apiUrl = "https://api.rescuegroups.org/v5/public/animals/search/available?orgID=54&include=pictures,species&fields[pictures]=large"; 49 50 /* 51 $tokenCh = curl_init($tokenUrl); 52 curl_setopt($tokenCh, CURLOPT_RETURNTRANSFER, true); 53 //curl_setopt($tokenCh, CURLOPT_POSTFIELDS, http_build_query($tokenData)); 54 55 $tokenResponse = curl_exec($tokenCh); 56 if (PHP_VERSION_ID < 80000) { curl_close($tokenCh); } 57 58 59 $tokenData = json_decode($tokenResponse, true); 60 //$accessToken = $tokenData['access_token']; 61 62 if (is_null($tokenData)) 63 { 64 return new WP_Error('PetPressError', sprintf(__('AnimalsFirst Error: %s(%d) Network error', 'petpress'), __METHOD__, __LINE__), ['data' => "Animals First token is null. Possible cause: bad Authentication Key"]); 65 } 66 */ 67 // Initialize an array to store all animals 68 $allAnimals = array(); 69 70 // Set the initial page number 71 $page = 1; 72 73 // Loop through pages until we've retrieved all animals 74 //do { 75 // Make API call to get dogs from the specific organization 76 $pageUrl = $apiUrl . "&page=" . $page; 77 $ch = curl_init($pageUrl); 78 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 79 /* 80 curl_setopt($ch, CURLOPT_HTTPHEADER, array( 81 'Authorization: ' . 'r5yhqoEU', // TODO replace 82 'Content-Type: application/json', 83 )); 84 */ 85 86 curl_setopt($ch, CURLOPT_HTTPHEADER, array( 87 'Authorization: ' . $this->apikey, 88 'Content-Type: application/json', 89 )); 90 91 92 93 94 95 //error_log( 'About to execute a call for AnimalsFirst data with URL: ' . $pageUrl ); 96 97 $response = curl_exec($ch); 98 if (PHP_VERSION_ID < 80000) { curl_close($ch); } 99 100 101 // Process the response 102 $data = json_decode($response, true); 103 104 if (is_null($data)) 105 { 106 return new WP_Error('PetPressError', sprintf(__('RescueGroups Error: %s(%d) Network error', 'petpress'), __METHOD__, __LINE__), ['status' => "Animal data not found", 'message' => $data['message']]); 107 } 108 109 if (isset($data['errors'])) { 110 if ($data['errors'][0]['status'] != 200){ 111 logError("RescueGroups returned an error [{$data['errors'][0]['status']}] | Title: {$data['errors'][0]['title']} | Detail: {$data['errors'][0]['detail']}"); 112 return new WP_Error('PetPressError', sprintf(__('RescueGroups Error: %s(%d) Network error', 'petpress'), __METHOD__, __LINE__), 113 ['status' => $data['errors'][0]['status'], 114 'title' => $data['errors'][0]['title'], 115 'detail' => $data['errors'][0]['detail']] 116 ); 117 return[]; 118 } 119 } 120 if (isset($data['data'])) { 121 $allAnimals = array_merge($allAnimals, $data['data']); 122 $page++; 123 } 124 125 // $allAnimals = $data; 126 /* 127 $totalPages = 1; 128 // Check if we've reached the last page 129 if (isset($data['pagination']['total_pages'])) { 130 $totalPages = $data['pagination']['total_pages']; 131 } 132 */ 133 // } while ($page <= $totalPages); 134 135 // Now $allAnimals contains all the animals from all pages 136 // You can process or display them as needed 137 138 // Sort the animals alphabetically 139 140 if ( !empty($allAnimals) && is_array($allAnimals) ) { 141 usort($allAnimals, function($a, $b) { 142 return strcmp($a['name'], $b['name']); 143 }); 144 } else { 145 return new WP_Error( 146 'PetPressError', 147 sprintf(__('PetPress Error: %s(%d) No valid animals found','petpress'), 148 __METHOD__, __LINE__ ), 149 array( 'status' => 400 ) 150 ); 151 } 152 153 154 //foreach ($allAnimals as $animal) { 155 // echo "Name: " . $animal['name'] . "\n"; 156 // echo "Breed: " . $animal['breeds']['primary'] . "\n"; 157 // echo "Status: " . $animal['status'] . "\n\n"; 158 //} 29 if (!is_array($options) || empty($options['rg_authkey'])) { 30 return new WP_Error( 31 'PetPressError', 32 sprintf(__('PetPress Error: %s(%d) Rescue Groups API key is not set in PetPress settings.', 'petpress'), __METHOD__, __LINE__), 33 ['status' => 400] 34 ); 35 } 36 $this->apikey = $options['rg_authkey']; 37 $orgId = $options['rg_orgid'] ?? ''; 38 39 if (empty($orgId)) { 40 return new WP_Error( 41 'PetPressError', 42 sprintf(__('PetPress Error: %s(%d) Rescue Groups Organization ID is not set in PetPress settings.', 'petpress'), __METHOD__, __LINE__), 43 ['status' => 400] 44 ); 45 } 46 47 // RescueGroups API v5 - fetch animals for specific organization 48 $apiUrl = 'https://api.rescuegroups.org/v5/public 49 /orgs/' . urlencode($orgId) . '/animals/search/available?' 50 . 'include=pictures,videos,species' 51 . '&fields[animals]=name,breedPrimary,breedSecondary,sex,ageGroup,birthDate,descriptionHtml,createdDate' 52 . '&fields[pictures]=small,large,original' 53 . '&fields[videos]=urlEmbed' 54 . '&limit=250'; 55 56 $ch = curl_init($apiUrl); 57 curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 58 curl_setopt($ch, CURLOPT_HTTPHEADER, [ 59 'Authorization: ' . $this->apikey, 60 'Content-Type: application/vnd.api+json', 61 ]); 62 63 $response = curl_exec($ch); 64 $curlError = curl_error($ch); 65 $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); 66 curl_close($ch); 67 68 if ($curlError) { 69 return new WP_Error( 70 'PetPressError', 71 sprintf(__('RescueGroups Error: %s(%d) Network error', 'petpress'), __METHOD__, __LINE__), 72 ['status' => 'cURL error', 'message' => $curlError] 73 ); 74 } 75 76 $data = json_decode($response, true); 77 78 if (is_null($data)) { 79 return new WP_Error( 80 'PetPressError', 81 sprintf(__('RescueGroups Error: %s(%d) Invalid JSON response', 'petpress'), __METHOD__, __LINE__), 82 ['status' => 'Parse error', 'httpCode' => $httpCode] 83 ); 84 } 85 86 // Handle API errors 87 if (isset($data['errors']) && !empty($data['errors'])) { 88 $error = $data['errors'][0]; 89 return new WP_Error( 90 'PetPressError', 91 sprintf(__('RescueGroups Error: %s(%d) API error', 'petpress'), __METHOD__, __LINE__), 92 [ 93 'status' => $error['status'] ?? $httpCode, 94 'title' => $error['title'] ?? 'Unknown error', 95 'detail' => $error['detail'] ?? '', 96 ] 97 ); 98 } 99 100 if (!isset($data['data']) || empty($data['data'])) { 101 return new WP_Error( 102 'PetPressError', 103 sprintf(__('PetPress Error: %s(%d) No animals found', 'petpress'), __METHOD__, __LINE__), 104 ['status' => 400] 105 ); 106 } 107 108 // Build lookup maps for included resources (pictures, videos, species) 109 $picturesMap = []; 110 $videosMap = []; 111 $speciesMap = []; 112 113 if (isset($data['included']) && is_array($data['included'])) { 114 foreach ($data['included'] as $resource) { 115 $type = $resource['type'] ?? null; 116 $id = $resource['id'] ?? null; 117 if (!$type || !$id) { 118 continue; 119 } 120 121 $attrs = $resource['attributes'] ?? []; 122 123 if ($type === 'pictures') { 124 $picturesMap[$id] = [ 125 'small' => $attrs['small']['url'] ?? null, 126 'large' => $attrs['large']['url'] ?? null, 127 'original' => $attrs['original']['url'] ?? null, 128 ]; 129 } elseif ($type === 'videos') { 130 $videosMap[$id] = [ 131 'urlEmbed' => $attrs['urlEmbed'] ?? null, 132 ]; 133 } elseif ($type === 'species') { 134 $speciesMap[$id] = $attrs['singular'] ?? $attrs['plural'] ?? 'Unknown'; 135 } 136 } 137 } 138 139 // Process animals and attach related data 140 $allAnimals = []; 141 foreach ($data['data'] as $animal) { 142 $attrs = $animal['attributes'] ?? []; 143 $relationships = $animal['relationships'] ?? []; 144 145 // Map pictures 146 $pictures = []; 147 if (isset($relationships['pictures']['data'])) { 148 foreach ($relationships['pictures']['data'] as $picRef) { 149 $picId = $picRef['id'] ?? null; 150 if ($picId && isset($picturesMap[$picId])) { 151 $pictures[] = $picturesMap[$picId]; 152 } 153 } 154 } 155 156 // Map videos 157 $videos = []; 158 if (isset($relationships['videos']['data'])) { 159 foreach ($relationships['videos']['data'] as $vidRef) { 160 $vidId = $vidRef['id'] ?? null; 161 if ($vidId && isset($videosMap[$vidId])) { 162 $videos[] = $videosMap[$vidId]; 163 } 164 } 165 } 166 167 // Map species 168 $species = 'Unknown'; 169 if (isset($relationships['species']['data'][0]['id'])) { 170 $speciesId = $relationships['species']['data'][0]['id']; 171 $species = $speciesMap[$speciesId] ?? 'Unknown'; 172 } 173 174 $allAnimals[] = [ 175 'id' => $animal['id'], 176 'attributes' => [ 177 'name' => $attrs['name'] ?? '', 178 'breedPrimary' => $attrs['breedPrimary'] ?? '', 179 'breedSecondary' => $attrs['breedSecondary'] ?? '', 180 'species' => $species, 181 'sex' => $attrs['sex'] ?? '', 182 'ageGroup' => $attrs['ageGroup'] ?? '', 183 'birthDate' => $attrs['birthDate'] ?? '', 184 'descriptionHTML' => $attrs['descriptionHtml'] ?? '', 185 'createdDate' => $attrs['createdDate'] ?? '', 186 ], 187 '_pictures' => $pictures, 188 '_videos' => $videos, 189 ]; 190 } 191 192 if (empty($allAnimals)) { 193 return new WP_Error( 194 'PetPressError', 195 sprintf(__('PetPress Error: %s(%d) No valid animals found', 'petpress'), __METHOD__, __LINE__), 196 ['status' => 400] 197 ); 198 } 199 200 // Sort alphabetically by name 201 usort($allAnimals, function ($a, $b) { 202 return strcmp($a['attributes']['name'] ?? '', $b['attributes']['name'] ?? ''); 203 }); 159 204 160 205 return $allAnimals; … … 230 275 } 231 276 } 232 233 234 public function fetchAnimalsFromSource() { // returns array of animals 235 277 278 public function fetchAnimalsFromSource() 279 { 236 280 $theRawData = $this->getRosterXML(); 237 281 238 if ( is_wp_error( $theRawData )) {282 if (is_wp_error($theRawData)) { 239 283 return $theRawData; 240 284 } 241 285 242 243 $roster = array(); 244 $rosterIDs = array(); 245 246 foreach ($theRawData as $anAnimal){ 286 $roster = []; 287 288 foreach ($theRawData as $anAnimal) { 247 289 $critter = new ppAnimal(); 248 $critter->set_id($anAnimal["id"]); 249 250 // $critter->set_adoptionapplicationurl($anAnimal["url"]); 251 252 $attrs = $anAnimal["attributes"]; 253 254 if (isset($attrs["birthDate"])) { $critter->set_age($critter->get_age_in_months($attrs["birthDate"])); } 255 256 //$critter->set_agegroup($anAnimal["age"]); 257 /* 258 if ($anAnimal["attributes"]["spayed_neutered"] == 1) 259 { 260 $critter->set_altered("Yes"); 261 } 262 elseif ($anAnimal["attributes"]["spayed_neutered"] == 0){ 263 $critter->set_altered("No"); 264 } 265 else{ 266 $critter->set_altered("Unknown or unspecified"); 267 }*/ 268 //$critter->set_altered($anAnimal["attributes"]["spayed_neutered"]); 269 270 271 if (isset($attrs["breedPrimary"])) { $critter->set_breed($attrs["breedPrimary"]); } 272 if (isset($attrs["breedSecondary"])) { $critter->set_secondarybreed($attrs["breedSecondary"]); } 273 274 //if (isset($anAnimal["shelter_id"])) { $critter->set_companyid($anAnimal["shelter_id"]); } 275 //if (isset($anAnimal["attributes"]["house_trained"])) { $critter->set_housetrained($anAnimal["attributes"]["house_trained"]); } 276 277 if (isset($attrs["createdDate"])) { $critter->set_lastintakedate($attrs["createdDate"]); } 278 279 if (isset($attrs["name"])) { $critter->set_name($attrs["name"]); } 280 281 if (isset($attrs["descriptionHTML"])) {$critter->set_memo($attrs["descriptionHTML"]); } 282 /* 283 if (isset($anAnimal["coat"])) { $critter->set_coatlength($anAnimal["coat"]); } 284 if (isset($anAnimal["declawed"])) {$critter->set_declawed($anAnimal["declawed"]); } 285 */ 286 // if (isset($anAnimal["secondary_breed"])) {$critter->set_secondarybreed($anAnimal["secondary_breed"]); } 287 288 //if (isset($anAnimal["secondary_color"])) {$critter->set_secondarycolor($anAnimal["secondary_color"]); } 289 if (isset($attrs["sex"])) {$critter->set_sex($attrs["sex"]);} 290 291 //if (isset($anAnimal["adoption_fee"])) {$critter->set_price($anAnimal["adoption_fee"]);} 292 293 //if (isset($anAnimal["size"])) {$critter->set_size($anAnimal["size"]);} 294 295 //if (isset($anAnimal["status"])) {$critter->set_stage($anAnimal["status"]);} 296 /* 297 if (isset($anAnimal["shots_current"])) {$critter->set_shotscurrent($anAnimal["shots_current"]);} 298 if (isset($anAnimal["attributes"]["special_needs"])) {$critter->set_specialneeds($anAnimal["attributes"]["special_needs"]);} 299 */ 300 //if (isset($anAnimal["species"])) {$critter->set_species("Dog");} // TODO 301 302 if (isset($anAnimal['relationships']['species']['data'][0]['id'])) { 303 switch ($anAnimal['relationships']['species']['data'][0]['id']) 304 { 305 case 3: 306 $theSpecies = "Cat"; 307 break; 308 case 8: 309 $theSpecies = "Dog"; 310 break; 311 case 23: 312 $theSpecies = "Horse"; 313 break; 314 case 32: 315 $theSpecies = "Rabbit"; 316 break; 317 default: 318 $theSpecies = $anAnimal['relationships']['species']['data'][0]['id']; 319 break; 320 321 } 322 $critter->set_species($theSpecies); 323 } 324 else 325 { 326 $critter->set_species("Unknown"); 327 } 328 329 //if (isset($anAnimal["af"])) {$critter->set_datasource("af");} 330 $critter->set_photo1($attrs["pictureThumbnailUrl"]); 331 //$critter->set_photo2($anAnimal["photo_url_2"]); 332 //$critter->set_photo3($anAnimal["photo_url_3"]); 333 334 //if (isset($anAnimal["primary_color"])) {$critter->set_primarycolor($anAnimal["primary_color"]);} 335 336 $critter->set_systemofrecord("RescueGroups"); 337 /* 338 if (isset($anAnimal["videos"][0]["embed"])) {$critter->set_videoid($anAnimal["videos"][0]["embed"]);} 339 */ 340 341 //if (isset($anAnimal["weight"])) {$critter->set_weight($anAnimal["weight"]);} 342 343 $roster[]=$critter; 344 } 345 346 return [ 'roster' => $roster ]; 347 290 $attrs = $anAnimal['attributes'] ?? []; 291 292 $critter->set_id($anAnimal['id']); 293 $critter->set_systemofrecord('RescueGroups'); 294 295 if (isset($attrs['name'])) { 296 $critter->set_name($attrs['name']); 297 } 298 if (!empty($attrs['birthDate'])) { 299 $critter->set_age($critter->get_age_in_months($attrs['birthDate'])); 300 } 301 if (!empty($attrs['ageGroup'])) { 302 $critter->set_agegroup($attrs['ageGroup']); 303 } 304 if (isset($attrs['breedPrimary'])) { 305 $critter->set_breed($attrs['breedPrimary']); 306 } 307 if (isset($attrs['breedSecondary'])) { 308 $critter->set_secondarybreed($attrs['breedSecondary']); 309 } 310 if (isset($attrs['sex'])) { 311 $critter->set_sex($attrs['sex']); 312 } 313 if (isset($attrs['descriptionHTML'])) { 314 $critter->set_memo($attrs['descriptionHTML']); 315 } 316 if (isset($attrs['createdDate'])) { 317 $critter->set_lastintakedate($attrs['createdDate']); 318 } 319 320 // Species is mapped from included resources in getRosterXML 321 $critter->set_species($attrs['species'] ?? 'Unknown'); 322 323 // Set photos (up to 3) 324 if (!empty($anAnimal['_pictures'])) { 325 $photoSetters = ['set_photo1', 'set_photo2', 'set_photo3']; 326 $photoIndex = 0; 327 foreach ($anAnimal['_pictures'] as $pic) { 328 $photoUrl = $pic['large'] ?? $pic['original'] ?? $pic['small'] ?? null; 329 if ($photoUrl && $photoIndex < 3) { 330 $critter->{$photoSetters[$photoIndex]}($photoUrl); 331 $photoIndex++; 332 } 333 } 334 } elseif (isset($attrs['pictureThumbnailUrl'])) { 335 $critter->set_photo1($attrs['pictureThumbnailUrl']); 336 } 337 338 // Set video 339 if (!empty($anAnimal['_videos'][0]['urlEmbed'])) { 340 $critter->set_videoid($anAnimal['_videos'][0]['urlEmbed']); 341 } 342 343 $roster[] = $critter; 344 } 345 346 return ['roster' => $roster]; 348 347 } 349 348 -
petpress/trunk/pp-Roster.php
r3438868 r3443682 88 88 89 89 $h .= '<div id="pp_sort_btn_container">'; // D1 90 $h .= '<button class="pp_sort_btn pp_button" id="sortNameBTN" onClick="sortTiles(\'data-name\')">Name <span class="dashicons dashicons-arrow-down-alt"></span></button>'; 90 //$h .= '<button class="pp_sort_btn pp_button" id="sortNameBTN" onClick="sortTiles(\'data-name\')">Name</button>'; 91 $h .= '<button class="pp_sort_btn pp_button pp_sort_active pp_sort_desc" id="sortNameBTN" onClick="sortTiles(\'data-name\')">Name</button>'; 91 92 92 93 $optSizeWeight = (ppUtils::optionChecked("sizeweight") && ($dataSource != "PetFinder")); … … 101 102 102 103 if (!ppUtils::optionChecked("sizeweight") || ($dataSource != "PetPoint")){ 103 $h .= '<button class="pp_sort_btn pp_button" id="sortAgeBTN" onClick="sortTiles(\'data-agegroupindex\')">Age <span class="dashicons"></span></button>';104 $h .= '<button class="pp_sort_btn pp_button" id="sortSizeBTN" onClick="sortTiles(\'data-sizeindex\')">Size <span class="dashicons"></span></button>';104 $h .= '<button class="pp_sort_btn pp_button" id="sortAgeBTN" onClick="sortTiles(\'data-agegroupindex\')">Age</button>'; 105 $h .= '<button class="pp_sort_btn pp_button" id="sortSizeBTN" onClick="sortTiles(\'data-sizeindex\')">Size</button>'; 105 106 } 106 107 else{ // specific value checkbox is checked and datasource is PetPoint 107 $h .= '<button class="pp_sort_btn pp_button" id="sortAgeBTN" onClick="sortTiles(\'data-age\')">Age <span class="dashicons"></span></button>';108 $h .= '<button class="pp_sort_btn pp_button" id="sortSizeBTN" onClick="sortTiles(\'data-weight\')">Size <span class="dashicons"></span></button>';108 $h .= '<button class="pp_sort_btn pp_button" id="sortAgeBTN" onClick="sortTiles(\'data-age\')">Age</button>'; 109 $h .= '<button class="pp_sort_btn pp_button" id="sortSizeBTN" onClick="sortTiles(\'data-weight\')">Size</button>'; 109 110 } 110 111 if (($optDaysIn) && ($dataSource != "PetFinder")): 111 $h .= '<button class="pp_sort_btn pp_button" id="sortDaysInBTN" onClick="sortTiles(\'data-daysin\')">Days In <span class="dashicons"></span></button>';112 $h .= '<button class="pp_sort_btn pp_button" id="sortDaysInBTN" onClick="sortTiles(\'data-daysin\')">Days In</button>'; 112 113 endif; 113 114 $h .= '</div>'; // pp_sort_btn_container d1 … … 248 249 } 249 250 250 $vitals = []; 251 252 if ($optSizeWeight) { // todo set the option checked outside of the loop 253 if (($critter->get_age()) != "" && ($critter->get_age() > 0)){ 254 $vitals[] = ppUtils::approximateAge($critter->get_age()); 255 } 256 elseif (($critter->get_age()) != "" && ($critter->get_age() == 0)){ 257 $vitals[] = $critter->get_agegroup(); 258 } 259 if ($critter->get_sex() != ""){ 260 $vitals[] = strtolower($critter->get_sex()); 261 } 262 if 263 (($cWeight) != "" && ($cWeight > 0)) { 264 if ($dataSource == "PetFinder") { 265 $vitals[] = $critter->get_size(); 251 $vitals = ['age' => '', 'sex' => '', 'sizeweight' => '']; 252 253 $sex = $critter->get_sex(); 254 $age = $critter->get_age(); 255 256 if ($optSizeWeight) { 257 if (!empty($age) && $age > 0) { 258 $vitals['age'] = ppUtils::approximateAge($age); 259 } elseif ($age !== null && $age == 0) { 260 $vitals['age'] = $critter->get_agegroup() ?? ''; 261 } 262 if (!empty($sex)) { 263 $vitals['sex'] = strtolower($sex); 264 } 265 if (!empty($cWeight) && $cWeight > 0) { 266 if ($dataSource == 'PetFinder') { 267 $vitals['sizeweight'] = $critter->get_size() ?? ''; 268 } else { 269 $vitals['sizeweight'] = ppUtils::shortenWeightUnit($critter->get_weight()); 266 270 } 267 else 268 { 269 $vitals[] = ppUtils::shortenWeightUnit($critter->get_weight()); 270 } 271 } 272 } 273 else { 274 if ($cAgegroup != "") { 275 $vitals[] = strtolower($cAgegroup); 276 } 277 if ($critter->get_sex() != "") { 278 $vitals[] = strtolower($critter->get_sex()); 279 } 280 if (($dataSource == "PetFinder") || ($dataSource == "AnimalsFirst")) { 281 $vitals[] = $critter->get_size(); 282 } 283 else 284 { 285 if ($weightclass != "") { 286 $vitals[] = $weightclass; 287 } 288 } 271 } 272 } else { 273 if (!empty($cAgegroup)) { 274 $vitals['age'] = strtolower($cAgegroup); 275 } 276 if (!empty($sex)) { 277 $vitals['sex'] = strtolower($sex); 278 } 279 if ($dataSource == 'PetFinder' || $dataSource == 'AnimalsFirst') { 280 $vitals['sizeweight'] = $critter->get_size() ?? ''; 281 } elseif (!empty($weightclass)) { 282 $vitals['sizeweight'] = $weightclass; 283 } 289 284 } 290 285 … … 305 300 306 301 307 $age = $vitals[0] ?? ''; 308 $h .= "<span class='ppListItemAge'><b>Age:</b> {$age}<br></span>"; 309 310 $sex = $vitals[1] ?? ''; 311 $h .= "<span class='ppListItemAge'><b>Sex:</b> {$sex}<br></span>"; 312 313 $h .= "<span class='ppListItemSizeWeight'>"; 314 315 $value = $vitals[2] ?? ''; 316 317 if (!empty($value)) { 302 if (!empty($vitals['age'])) { 303 $h .= "<span class='ppListItemAge'><b>Age:</b> {$vitals['age']}<br></span>"; 304 } 305 306 if (!empty($vitals['sex'])) { 307 $h .= "<span class='ppListItemAge'><b>Sex:</b> {$vitals['sex']}<br></span>"; 308 } 309 310 if (!empty($vitals['sizeweight'])) { 311 $h .= "<span class='ppListItemSizeWeight'>"; 318 312 if ($optSizeWeight) { 319 $h .= "<b>Weight:</b> {$v alue}<br>";313 $h .= "<b>Weight:</b> {$vitals['sizeweight']}<br>"; 320 314 } else { 321 $h .= "<b>Size:</b> {$value}<br>"; 322 } 323 } 324 325 $h .= "</span>"; 326 315 $h .= "<b>Size:</b> {$vitals['sizeweight']}<br>"; 316 } 317 $h .= "</span>"; 318 } 319 327 320 if ($optDaysIn) { 328 321 $daysIn = $critter->get_daysin(); … … 380 373 } 381 374 382 $h .= "<!-- LAST SERVER ERROR (1 WEEK)"; 383 $h .= ppUtils::get_last_petpress_error(168); 384 $h .= " LAST SERVER ERROR -->"; 385 386 if (!PP_PETPRESS_DEBUG) { $h .= "<!--"; } 387 $h .="This page generated at " . date('Y-m-d H:i:s') . " (server time zone: " . date_default_timezone_get() . "). If older than the current time, the page may be cached.\n"; 375 $h .= "\n<!-- PetPress Diagnostics BEGIN -->"; 376 $h .= "\n<!-- Last Server Error Data BEGIN (1 WEEK): "; 377 $errString = ppUtils::get_last_petpress_error(168, false); 378 if (empty($errString)) { 379 $errString = "[no errors in the last week]"; 380 } 381 $h .= $errString; 382 $h .= "\nLast Server Error Data END -->"; 383 384 if (!PP_PETPRESS_DEBUG) { $h .= "\n<!--"; } 385 $h .="This page generated at " . date('Y-m-d H:i:s') . " (server time zone: " . date_default_timezone_get() . "). If that is not the current time, the page may be cached.\n"; 388 386 $h .="PetPress-fetch: The last fetch from the data source was " . ppUtils::getFetchTime() .", though if the page is cached (see above) then this information may be out-of-date.\n"; 387 $h .= ppUtils::nextPetpressCronUTC() . "\n"; 388 $cron_error = get_option( 'petpress_cron_schedule_error' ); 389 if ( ! empty( $cron_error ) && is_array( $cron_error ) ) { 390 $h .= "PetPress-cron-error: " . esc_html( $cron_error['message'] ) . " (at " . esc_html( $cron_error['time'] ) . ")\n"; 391 } 389 392 if (!PP_PETPRESS_DEBUG) { $h .= "-->"; } 390 393 $h .= "<!-- PetPress Diagnostics END -->"; 391 394 392 395 -
petpress/trunk/pp-Utilities.php
r3438868 r3443682 25 25 } 26 26 27 public static function setFetchTime() { 28 $current_time = current_time('mysql'); 29 update_option('petpress_last_fetch', $current_time); 30 31 } 27 public static function setFetchTime() { 28 // Get current time as Unix timestamp in UTC 29 $timestamp_utc = current_time( 'timestamp', true ); 30 31 // Format as MySQL datetime in UTC 32 $current_time = gmdate( 'Y-m-d H:i:s', $timestamp_utc ); 33 34 update_option( 'petpress_last_fetch', $current_time . " UTC"); 35 } 36 32 37 33 38 public static function getFetchTime() { … … 45 50 return (""); 46 51 } 52 53 public static function nextPetpressCronUTC() { 54 $hook = 'petpress_cron'; 55 $next = wp_next_scheduled( $hook ); 56 if ( ! $next ) { 57 return 'No wp-cron job scheduled for "' . esc_html( $hook ) . '". This is normal if using the PetPress Data Manager.'; 58 } 59 $datetime_utc = gmdate( 'Y-m-d H:i:s', $next ) . ' UTC'; 60 return 'The next fetch of data is scheduled for ' . esc_html( $datetime_utc ); 61 } 47 62 48 63 public static function getDataSource() … … 362 377 return $theDBTime; 363 378 } 364 /* 365 static public function createCronJob($intervalIN, $theDataSource) { 366 $interval = in_array($intervalIN, ['15m', '10m', '05m']) ? $intervalIN : '30m'; 367 368 foreach (['petpress_cron','petpress_cron_dog_data_load', 'petpress_cron_cat_data_load', 'petpress_cron_other_data_load','petpress_cron_dog', 'petpress_cron_cat', 'petpress_cron_other'] as $legacy_hook) { 369 while ($event = wp_get_scheduled_event($legacy_hook)) { 370 wp_unschedule_event($event->timestamp, $legacy_hook); 371 } 372 } 373 374 if (isset($theDataSource) && $theDataSource !== 'PetPressDM') { 375 376 $result = wp_schedule_event( time(), $interval, 'petpress_cron' ); 377 378 if ( is_wp_error( $result ) ) { 379 error_log( 'PetPress cron scheduling failed: ' . $result->get_error_message() ); 380 } 381 382 } 383 384 } 385 */ 386 387 static public function createCronJob( $intervalIN, $theDataSource ) { 388 $interval = in_array( $intervalIN, [ '15m', '10m', '05m' ], true ) ? $intervalIN : '30m'; 389 390 foreach ( [ 'petpress_cron', 'petpress_cron_dog_data_load', 'petpress_cron_cat_data_load', 'petpress_cron_other_data_load', 'petpress_cron_dog', 'petpress_cron_cat', 'petpress_cron_other' ] as $legacy_hook ) { 391 while ( $event = wp_get_scheduled_event( $legacy_hook ) ) { 392 wp_unschedule_event( $event->timestamp, $legacy_hook ); 393 } 394 } 395 396 if ( isset( $theDataSource ) && $theDataSource !== 'PetPressDM' ) { 397 398 $max_attempts = 5; 399 $attempt = 0; 400 $hook = 'petpress_cron'; 401 $timestamp = time(); 402 403 // Do not create duplicates. 379 380 381 static public function createCronJob( $intervalIN, $theDataSource ) { 382 $interval = in_array( $intervalIN, [ '15m', '10m', '05m' ], true ) ? $intervalIN : '30m'; 383 $hook = 'petpress_cron'; 384 385 // Store desired settings for retry mechanism 386 update_option( 'petpress_cron_settings', [ 387 'interval' => $interval, 388 'datasource' => $theDataSource, 389 ], false ); 390 391 // Use a transient lock to prevent race conditions 392 $lock_key = 'petpress_cron_scheduling'; 393 if ( get_transient( $lock_key ) ) { 394 return; // Another process is scheduling 395 } 396 set_transient( $lock_key, true, 30 ); // 30 second lock 397 398 try { 399 // Clear all legacy hooks 400 foreach ( [ 'petpress_cron', 'petpress_cron_dog_data_load', 'petpress_cron_cat_data_load', 'petpress_cron_other_data_load', 'petpress_cron_dog', 'petpress_cron_cat', 'petpress_cron_other' ] as $legacy_hook ) { 401 while ( $event = wp_get_scheduled_event( $legacy_hook ) ) { 402 wp_unschedule_event( $event->timestamp, $legacy_hook ); 403 } 404 } 405 406 if ( ! isset( $theDataSource ) || $theDataSource === 'PetPressDM' ) { 407 delete_transient( $lock_key ); 408 delete_option( 'petpress_cron_settings' ); 409 return; 410 } 411 412 // Check again after clearing (another process may have scheduled) 413 if ( wp_next_scheduled( $hook ) ) { 414 delete_transient( $lock_key ); 415 delete_option( 'petpress_cron_schedule_error' ); 416 return; 417 } 418 419 // Schedule the event 420 $result = wp_schedule_event( time(), $interval, $hook, [], true ); 421 422 if ( is_wp_error( $result ) || ! $result ) { 423 $message = is_wp_error( $result ) ? $result->get_error_message() : 'wp_schedule_event returned false'; 424 error_log( 'PetPress cron scheduling failed: ' . $message ); 425 426 // Store error for admin notice 427 update_option( 'petpress_cron_schedule_error', [ 428 'message' => $message, 429 'time' => current_time( 'mysql' ), 430 ], false ); 431 432 // Schedule a retry attempt 433 self::scheduleRetry(); 434 } else { 435 // Success - clear any previous error 436 delete_option( 'petpress_cron_schedule_error' ); 437 438 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 439 error_log( 'PetPress cron scheduled successfully' ); 440 } 441 } 442 } finally { 443 delete_transient( $lock_key ); 444 } 445 } 446 447 /** 448 * Schedule a retry attempt for cron creation 449 */ 450 static private function scheduleRetry( $attempt = 1 ) { 451 $retry_hook = 'petpress_cron_retry'; 452 $max_retries = 5; 453 454 if ( $attempt > $max_retries ) { 455 error_log( "PetPress cron scheduling failed after {$max_retries} retries" ); 456 return; 457 } 458 459 // Don't schedule if already scheduled 460 if ( wp_next_scheduled( $retry_hook ) ) { 461 return; 462 } 463 464 // Store the attempt number 465 update_option( 'petpress_cron_retry_attempt', $attempt, false ); 466 467 // Try again in 2 minutes 468 wp_schedule_single_event( time() + 120, $retry_hook ); 469 } 470 471 /** 472 * Retry handler - called by the retry hook 473 */ 474 static public function retryCronSchedule() { 475 $settings = get_option( 'petpress_cron_settings' ); 476 if ( ! $settings ) { 477 return; 478 } 479 480 $hook = 'petpress_cron'; 481 $attempt = (int) get_option( 'petpress_cron_retry_attempt', 1 ); 482 483 // Already scheduled? We're done 484 if ( wp_next_scheduled( $hook ) ) { 485 delete_option( 'petpress_cron_schedule_error' ); 486 delete_option( 'petpress_cron_retry_attempt' ); 487 return; 488 } 489 490 $interval = $settings['interval'] ?? '30m'; 491 492 // Attempt to schedule 493 $result = wp_schedule_event( time(), $interval, $hook, [], true ); 494 495 if ( is_wp_error( $result ) || ! $result ) { 496 $message = is_wp_error( $result ) ? $result->get_error_message() : 'wp_schedule_event returned false'; 497 error_log( "PetPress cron retry {$attempt} failed: " . $message ); 498 499 update_option( 'petpress_cron_schedule_error', [ 500 'message' => $message . " (retry {$attempt}/5)", 501 'time' => current_time( 'mysql' ), 502 ], false ); 503 504 // Schedule another retry 505 self::scheduleRetry( $attempt + 1 ); 506 } else { 507 delete_option( 'petpress_cron_schedule_error' ); 508 delete_option( 'petpress_cron_retry_attempt' ); 509 error_log( "PetPress cron scheduled successfully on retry {$attempt}" ); 510 } 511 } 512 513 /** 514 * Check if cron should be scheduled but isn't, and fix it 515 * Call this on admin_init or init 516 */ 517 static public function ensureCronScheduled() { 518 $settings = get_option( 'petpress_cron_settings' ); 519 if ( ! $settings ) { 520 return; 521 } 522 523 $datasource = $settings['datasource'] ?? ''; 524 if ( empty( $datasource ) || $datasource === 'PetPressDM' ) { 525 return; 526 } 527 528 $hook = 'petpress_cron'; 529 530 // If already scheduled, nothing to do 404 531 if ( wp_next_scheduled( $hook ) ) { 405 532 return; 406 533 } 407 534 408 do { 409 $attempt++; 410 411 // Ask for WP_Error on failure (fifth param = true). 412 $result = wp_schedule_event( $timestamp, $interval, $hook, [], true ); 413 414 if ( ! is_wp_error( $result ) && $result ) { 415 // Success. 416 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 417 error_log( "PetPress cron scheduled successfully on attempt {$attempt}" ); 418 } 419 break; 420 } 421 422 // Failure: log error. 423 $message = is_wp_error( $result ) 424 ? $result->get_error_message() 425 : 'wp_schedule_event returned false'; 426 427 error_log( "PetPress cron scheduling failed (attempt {$attempt}): {$message}" ); 428 429 if ( $attempt < $max_attempts ) { 430 // Simple backoff: wait a bit and nudge timestamp forward. 431 sleep( 2 ); 432 $timestamp = time() + ( 10 * $attempt ); 433 } 434 435 } while ( $attempt < $max_attempts ); 436 } 437 } 535 // Not scheduled but should be - try to schedule 536 $interval = $settings['interval'] ?? '30m'; 537 $result = wp_schedule_event( time(), $interval, $hook, [], true ); 538 539 if ( ! is_wp_error( $result ) && $result ) { 540 delete_option( 'petpress_cron_schedule_error' ); 541 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { 542 error_log( 'PetPress cron scheduled via ensureCronScheduled' ); 543 } 544 } 545 } 438 546 439 547 … … 503 611 504 612 public static function petpress_log( $message ) { 505 // Toggle debugging here506 613 $debug_enabled = defined( 'PP_PETPRESS_DEBUG' ) && PP_PETPRESS_DEBUG; 507 614 … … 515 622 } 516 623 517 public static function get_last_petpress_error($hoursWindow )624 public static function get_last_petpress_error($hoursWindow, $formatForDisplay) 518 625 { 626 627 $numDays = round($hoursWindow / 24, 1); // 1 decimal place 628 // Usage: "{$hoursWindow} hours ({$numDays} days)" 629 630 519 631 $error = get_option( 'petpress_last_cron_error' ); 520 632 … … 528 640 529 641 if ( $diff <= $limit ) { 530 $h = "<div style='border: 1px solid; padding:1em 2em'>"; 531 $h .= "<h3>Most Recent PetPress Connection Error (within the last " . $hoursWindow ." hours)</h3>"; 532 $h .= "Time: " . date( 'F j, Y \a\t g:i:s A', strtotime( $timestamp )) . " (server time, which is ".date_default_timezone_get().")<br>"; 533 $h .= "Error Code: " . $error['code']. "<br>"; 642 $h = ""; 643 if ($formatForDisplay){ 644 $h = "<div style='border: 1px solid; padding:1em 2em'>"; 645 $h .= "<h3>"; 646 } 647 $h .= "Most Recent PetPress Connection Error within the last {$hoursWindow} hours ({$numDays} days) \n"; 648 if ($formatForDisplay){ 649 $h .= "</h3>"; 650 } 651 $h .= "Error Time: " . date( 'F j, Y \a\t g:i:s A', strtotime( $timestamp )) . " (server time, which is ".date_default_timezone_get().")\n"; 652 if ($formatForDisplay){ $h .= "<br>"; } 653 $h .= "Error Code: " . $error['code']. "\n"; 654 if ($formatForDisplay){ $h .= "<br>"; } 534 655 $h .= "Error Message: " . $error['msg']; 535 656 $data = $error['data']; 536 657 $errorData = esc_html( print_r( $data, true ) ); 537 658 $errorData = preg_replace('/^Array\s*\(|\)\s*$/m', '', trim($errorData)); 538 $h .= '<pre>' . $errorData . '</pre>'; 659 if ($formatForDisplay){ $h .= "<pre>"; } 660 $h .= $errorData ; 661 if ($formatForDisplay){ $h .= "</pre>"; } 539 662 540 $status = isset( $error['data']['status'] ) ? (int) $error['data']['status'] : 0; 541 switch ($status) 542 { 543 case 401: 544 $h .= "<p>Error '401' indicates that you are not authorized to get the information from the data source. There is likely something wrong with your authorization key / login credentials.</p>"; 545 break; 546 case 403: 547 $h .= "<p>Error '403' indicates that the data request was not allowed. It is likely there is something wrong with your authorization key / login credentials.</p>"; 548 break; 549 case 404: 550 $h .= "<p>Error '404' indicates that a resource was not found. This may indicate that the back-end service is not functioning correctly.</p>"; 551 break; 552 case 409: 553 $h .= "<p>Error '409' indicates that the data source is rejecting requests from PetPress because they are too frequent. You need to set the data refresh option below to a longer interval. If you have multiple websites pulling data from the datasource, be aware that the rate limit for the data source applies to all sites and applicaitons combined, not just this instance.</p>"; 554 break; 555 case 500: 556 $h .= "<p>Error '500' is a generic response indicating some kind of server-level error of the data source.</p>"; 557 break; 558 case 502: 559 $h .= "<p>Error '502' indicates a gateway problem -- some element of the network between this web server and the data source. This is likely a temporary condition.</p>"; 560 break; 561 case 503: 562 $h .= "<p>Error '503' indicates that the data source service is not available. This is likely a temporary condition.</p>"; 563 break; 564 case 504: 565 $h .= "<p>Error '504' indicates a gateway timeout -- some element of the network between this web server and the data source did not get a timely response.</p>"; 566 break; 567 568 } 569 570 $h .= "<p>NB: <i>This error will be shown here for " .$hoursWindow." hours after the event even if the underlying issue is resolved.</i></p>"; 571 572 $h .= "</div>"; 663 $status = isset($error['data']['status']) ? (int)$error['data']['status'] : 0; 664 665 $messages = [ 666 401 => "Error '401': Not authorized. Check your authorization key/login credentials.", 667 403 => "Error '403': Request not allowed. Verify your authorization key.", 668 404 => "Error '404': Resource not found. Data source may be down.", 669 409 => "Error '409': Rate limit exceeded. Increase data refresh interval. Rate limit applies across all sites/apps.", 670 500 => "Error '500': Data source server error.", 671 502 => "Error '502': Gateway issue. Temporary network problem.", 672 503 => "Error '503': Data source unavailable. Temporary condition.", 673 504 => "Error '504': Gateway timeout. Network delay issue." 674 ]; 675 676 if (isset($messages[$status])) { 677 $h .= $formatForDisplay ? "<p>" . $messages[$status] . "</p>" : $messages[$status]; 678 } else { 679 $h .= $formatForDisplay ? "<p>Unknown error '{$status}'</p>" : "Unknown error '{$status}'"; 680 } 681 682 // NB message (consistent formatting) 683 $h .= $formatForDisplay 684 ? "\n<p>NB: <i>This error will be reported for {$hoursWindow} hours ({$numDays} days) after the event even if the underlying issue is resolved.</i></p></div>\n" 685 : "\nNB: This error will be reported for {$hoursWindow} hours ({$numDays} days) after the event even if the underlying issue is resolved.</div>\n"; 573 686 return $h; 574 687 } -
petpress/trunk/readme.txt
r3438891 r3443682 5 5 Requires at least: 5.7 6 6 Tested up to: 6.9 7 Stable tag: 2.2 7 Stable tag: 2.2.1 8 8 Requires PHP: 7.4 9 9 License: GPL v2 or later … … 30 30 31 31 == Changelog == 32 Version 2.2.1 33 1) When Price is shown in the detail page, allow for selection of "Price" or "Adoption Fee" as the label. 34 2) Cron job scheduling more robust, with locking and re-try 35 3) Sorting buttons on roster page updated look (better for phones) 36 4) Cleaned up lightbox effect slightly, images appear larger (if screen size permits) and fewer issues with z-order 37 32 38 33 39 Version 2.2
Note: See TracChangeset
for help on using the changeset viewer.