├── .distignore ├── .editorconfig ├── .git-blame-ignore-revs ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── translation.md ├── actions │ └── build-plugin │ │ └── action.yml ├── dependabot.yml └── workflows │ ├── build-rc.yml │ ├── compile.yml │ ├── e2e.yml │ ├── phpcbf-check.yml │ ├── phpstan.yml │ ├── phpunit.yml │ ├── release.yml │ ├── rerun.yml │ └── zip-pr.yml ├── .gitignore ├── .nvmrc ├── .phpcs.xml.dist ├── .wp-env.json ├── Gruntfile.js ├── LICENSE.txt ├── Readme.md ├── TECHNICAL.md ├── assets ├── admin │ ├── css │ │ ├── admin.css │ │ └── admin.css.map │ ├── js │ │ ├── admin.js │ │ ├── admin.min.js │ │ ├── index.php │ │ └── src │ │ │ ├── booking.js │ │ │ ├── bookingrules.js │ │ │ ├── export.js │ │ │ ├── holiday.js │ │ │ ├── location.js │ │ │ ├── map.js │ │ │ ├── massoperations.js │ │ │ ├── migration.js │ │ │ ├── restriction.js │ │ │ ├── templates.js │ │ │ ├── timeframe.js │ │ │ └── toolltip.js │ └── sass │ │ ├── admin.scss │ │ ├── lib │ │ └── _jquery-ui.scss │ │ └── partials │ │ ├── _admin-edit.scss │ │ ├── _dashboard.scss │ │ ├── _form-booking.scss │ │ ├── _form-export.scss │ │ ├── _form-map.scss │ │ ├── _form-timeframe.scss │ │ ├── _migration.scss │ │ └── _plugin_update.scss ├── global │ ├── cb-ci │ │ └── logo.png │ ├── css │ │ ├── vendor.css │ │ └── vendor.css.map │ ├── images │ │ ├── marker-icon-2x-black.png │ │ └── marker-shadow.png │ ├── js │ │ ├── dictionary.js │ │ ├── settings.js │ │ ├── vendor.js │ │ ├── vendor.js.map │ │ └── vendor.min.js │ └── sass │ │ ├── global.scss │ │ ├── lib │ │ ├── _animate.scss │ │ └── _reset.scss │ │ └── partials │ │ ├── _extends.scss │ │ ├── _mixins.scss │ │ ├── _utilities.scss │ │ └── _variables.scss ├── map │ ├── css │ │ ├── cb-map-admin.css │ │ └── cb-map-shortcode.css │ ├── images │ │ ├── marker-icon-2x-black.png │ │ └── marker-shadow.png │ ├── js │ │ ├── cb-map-filters.js │ │ ├── cb-map-locationview.js │ │ ├── cb-map-positioning.js │ │ ├── cb-map-replace-link.js │ │ └── cb-map-shortcode.js │ ├── leaflet-messagebox │ │ ├── LICENSE │ │ ├── README.md │ │ ├── leaflet-messagebox.css │ │ └── leaflet-messagebox.js │ └── overscroll │ │ ├── jquery.overscroll.js │ │ └── jquery.overscroll.min.js ├── packaged │ └── .gitignore └── public │ ├── css │ ├── partials │ │ ├── forms.css │ │ └── forms.css.map │ ├── public.css │ ├── public.css.map │ └── themes │ │ ├── daterangepicker │ │ └── daterangepicker.css │ │ ├── graphene.css │ │ ├── graphene.css.map │ │ ├── kasimir.css │ │ ├── kasimir.css.map │ │ ├── select2 │ │ └── select2.min.css │ │ ├── twentynineteen.css │ │ ├── twentynineteen.css.map │ │ ├── twentytwenty.css │ │ └── twentytwenty.css.map │ ├── js │ ├── index.php │ ├── public.js │ ├── public.min.js │ ├── src │ │ ├── .gitkeep │ │ ├── bookings.js │ │ ├── lib │ │ │ └── litepicker.js │ │ └── litepicker.js │ └── vendor │ │ ├── daterangepicker.min.js │ │ ├── moment.min.js │ │ └── select2.min.js │ └── sass │ ├── mixins │ └── _calendar.scss │ ├── partials │ ├── _bookings.scss │ ├── _calendar.scss │ ├── _forms.scss │ ├── _layouts.scss │ ├── _lists.scss │ ├── _map.scss │ ├── _mixins.scss │ ├── _notice.scss │ ├── _shortcodeItemsTable.scss │ ├── _single.scss │ ├── _table.scss │ └── _themeoptimization.scss │ ├── public.scss │ └── themes │ ├── graphene.scss │ ├── kasimir.scss │ ├── twentynineteen.scss │ └── twentytwenty.scss ├── bin ├── build-zip.sh ├── install-wp-tests.sh ├── setup-cypress-env.sh └── update-pot.sh ├── codecov.yml ├── commonsbooking.php ├── composer.json ├── composer.lock ├── includes ├── Admin.php ├── OptionsArray.php ├── Plugin.php ├── Public.php ├── Shortcodes.php ├── Template.php ├── TemplateParser.php ├── Users.php ├── commons-api-json-schema │ ├── commons-api.availability.schema.json │ ├── commons-api.items.schema.json │ ├── commons-api.locations.schema.json │ ├── commons-api.owners.schema.json │ ├── commons-api.projects.schema.json │ └── velogistics-api.items.schema.json └── gbfs-json-schema │ ├── gbfs.json │ ├── station_information.json │ ├── station_status.json │ └── system_information.json ├── index.php ├── languages ├── commonsbooking-de_DE.mo ├── commonsbooking-de_DE.po └── commonsbooking.pot ├── package-lock.json ├── package.json ├── phpstan-baseline.neon ├── phpstan.neon ├── phpunit.xml.dist ├── readme.txt ├── screenshots └── example_0.5.9_de.png ├── src ├── API │ ├── AvailabilityRoute.php │ ├── BaseRoute.php │ ├── GBFS │ │ ├── BaseRoute.php │ │ ├── Discovery.php │ │ ├── StationInformation.php │ │ ├── StationStatus.php │ │ └── SystemInformation.php │ ├── ItemsRoute.php │ ├── LocationsRoute.php │ ├── OwnersRoute.php │ ├── ProjectsRoute.php │ └── Share.php ├── CB │ ├── CB.php │ └── CB1UserFields.php ├── Exception │ ├── BookingCodeException.php │ ├── BookingDeniedException.php │ ├── BookingRuleException.php │ ├── ExportException.php │ ├── OverlappingException.php │ ├── PostException.php │ └── TimeframeInvalidException.php ├── Helper │ ├── API.php │ ├── GeoCodeService.php │ ├── GeoHelper.php │ ├── Helper.php │ ├── NominatimGeoCodeService.php │ └── Wordpress.php ├── Map │ ├── BaseShortcode.php │ ├── LocationMapAdmin.php │ ├── MapData.php │ ├── MapFilter.php │ ├── MapItemAvailable.php │ ├── MapShortcode.php │ └── SearchShortcode.php ├── Messages │ ├── AdminMessage.php │ ├── BookingCodesMessage.php │ ├── BookingMessage.php │ ├── BookingReminderMessage.php │ ├── LocationBookingReminderMessage.php │ ├── Message.php │ └── RestrictionMessage.php ├── Migration │ ├── Booking.php │ └── Migration.php ├── Model │ ├── BookablePost.php │ ├── Booking.php │ ├── BookingCode.php │ ├── Calendar.php │ ├── CustomPost.php │ ├── Day.php │ ├── Item.php │ ├── Location.php │ ├── Map.php │ ├── MessageRecipient.php │ ├── Restriction.php │ ├── Timeframe.php │ └── Week.php ├── Plugin.php ├── Repository │ ├── ApiShares.php │ ├── BookablePost.php │ ├── Booking.php │ ├── BookingCodes.php │ ├── CB1.php │ ├── Item.php │ ├── Location.php │ ├── PostRepository.php │ ├── Restriction.php │ ├── Timeframe.php │ └── UserRepository.php ├── Service │ ├── Booking.php │ ├── BookingCodes.php │ ├── BookingRule.php │ ├── BookingRuleApplied.php │ ├── Cache.php │ ├── Holiday.php │ ├── MassOperations.php │ ├── Scheduler.php │ ├── TimeframeExport.php │ ├── Upgrade.php │ └── iCalendar.php ├── Settings │ └── Settings.php ├── View │ ├── Admin │ │ └── Filter.php │ ├── Booking.php │ ├── BookingCodes.php │ ├── Calendar.php │ ├── Dashboard.php │ ├── Item.php │ ├── Location.php │ ├── Map.php │ ├── MassOperations.php │ ├── Migration.php │ ├── Restriction.php │ ├── TimeframeExport.php │ └── View.php └── Wordpress │ ├── CustomPostType │ ├── Booking.php │ ├── CustomPostType.php │ ├── Item.php │ ├── Location.php │ ├── Map.php │ ├── Restriction.php │ └── Timeframe.php │ ├── Options │ ├── AdminOptions.php │ └── OptionsTab.php │ ├── PostStatus │ └── PostStatus.php │ └── Widget │ └── UserWidget.php ├── templates ├── booking-single-form.php ├── booking-single-notallowed.php ├── booking-single.php ├── calendar-key.php ├── dashboard-index.php ├── item-calendar-header.php ├── item-single-meta.php ├── item-single.php ├── item-withlocation.php ├── location-calendar-header.php ├── location-single-meta.php ├── location-single.php ├── location-withitem.php ├── map-admin-page-template.php ├── massoperations-index.php ├── shortcode-bookings.php ├── shortcode-items.php ├── shortcode-items_table.php ├── shortcode-locations.php ├── timeframe-calendar-day.php ├── timeframe-calendar.php ├── timeframe-notallowed.php ├── timeframe-withitem.php └── timeframe-withlocation.php └── tests ├── cypress ├── cypress.config.js ├── e2e │ ├── admin-booking.cy.js │ ├── booking-process.cy.js │ ├── cpt-creation.cy.js │ ├── cpt-frontend-pages.cy.js │ ├── litepicker-overbooking.cy.js │ ├── load-shortcodes.cy.js │ └── load-wp.cy.js ├── fixtures │ ├── bookableItems.json │ └── bookableLocations.json ├── support │ ├── commands.js │ └── e2e.js └── wordpress-files │ ├── content-example.xml │ └── e2e-override.json └── php ├── API ├── AvailabilityRouteEmptyTest.php ├── AvailabilityRouteTest.php ├── CB_REST_Route_UnitTestCase.php ├── CB_REST_UnitTestCase.php ├── GBFS │ ├── DiscoveryRouteTest.php │ ├── StationInformationRouteTest.php │ ├── StationStatusRouteTest.php │ ├── StationStatusTest.php │ └── SystemInformationRouteTest.php ├── ItemsRouteTest.php ├── LocationsRouteTest.php └── ProjectsRouteTest.php ├── BaseTestCase.php ├── CB └── CBTest.php ├── Helper ├── GeoHelperTest.php ├── HelperTest.php └── WordpressTest.php ├── Messages ├── BookingMessageTest.php ├── BookingReminderMessageTest.php ├── Email_Test_Case.php ├── LocationBookingReminderMessageTest.php ├── MessageTest.php └── RestrictionMessageTest.php ├── Model ├── BookablePostTest.php ├── BookingTest.php ├── CalendarTest.php ├── DayTest.php ├── ItemTest.php ├── LocationTest.php ├── MapTest.php ├── MessageRecipientTest.php ├── RestrictionTest.php ├── TimeframeTest.php └── WeekTest.php ├── PluginTest.php ├── Repository ├── BookingCodesRandomnessTest.php ├── BookingCodesTest.php ├── BookingTest.php ├── CB1Test.php ├── ItemTest.php ├── LocationTest.php ├── RestrictionTest.php ├── TimeframeTest.php └── UserRepositoryTest.php ├── Service ├── BookingCodesTest.php ├── BookingRuleAppliedTest.php ├── BookingRuleTest.php ├── BookingTest.php ├── CacheTest.php ├── HolidayTest.php ├── MassOperationsTest.php ├── SchedulerTest.php ├── TimeframeExportTest.php ├── TimeframeExport_AJAX_Test.php ├── UpgradeTest.php ├── Upgrade_AJAX_Test.php └── iCalendarTest.php ├── Settings └── SettingsTest.php ├── View ├── BookingCodesTest.php ├── BookingTest.php ├── BookingTest_AJAX_Test.php ├── CalendarTest.php ├── MassOperationsTest.php └── ViewTest.php ├── Wordpress ├── CustomPostType │ ├── BookingTest.php │ ├── CustomPostTypeTest.php │ └── TimeframeTest.php └── CustomPostTypeTest.php └── bootstrap.php /.distignore: -------------------------------------------------------------------------------- 1 | .* 2 | .*/ 3 | *.lock 4 | *.md 5 | *.zip 6 | *.sh 7 | /bin/ 8 | /build/ 9 | /node_modules/ 10 | /tests/ 11 | composer.* 12 | Gruntfile.js 13 | codecov.yml 14 | none 15 | package-lock.json 16 | package.json 17 | phpcs.xml 18 | phpunit.xml 19 | phpunit.xml.dist 20 | phpunit 21 | project.json 22 | README.md 23 | tsconfig.* 24 | webpack.config.js 25 | phpstan.neon 26 | phpstan-baseline.neon 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | charset = utf-8 3 | end_of_line = lf 4 | indent_size = 4 5 | indent_style = space 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | max_line_length = 120 9 | tab_width = 4 10 | 11 | [Gruntfile.js] 12 | indent_style = tab 13 | 14 | [{*.ctp,*.hphp,*.inc,*.module,*.php,*.php4,*.php5,*.phtml}] 15 | indent_style = tab 16 | 17 | [*.scss] 18 | indent_size = 2 19 | 20 | [*.vue] 21 | indent_size = 2 22 | tab_width = 2 23 | 24 | [{*.bash,*.sh,*.zsh}] 25 | indent_size = 2 26 | tab_width = 2 27 | 28 | [{*.cjsx,*.coffee}] 29 | indent_size = 2 30 | tab_width = 2 31 | 32 | [{*.har,*.jsb2,*.jsb3,*.json,.babelrc,.eslintrc,.stylelintrc,bowerrc,composer.lock,jest.config}] 33 | indent_size = 2 34 | 35 | [{*.yaml,*.yml}] 36 | indent_size = 2 -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # .git-blame-ignore-revs 2 | # Apply phpcbf to entire codebase 3 | 59340f68fb9c6d6e680ac3ad40a3d095e98837a6 4 | 5 | # Apply phpcbf to php tests code 6 | 9f0bd0de52d5dca442317c14867be7702e1a7c73 7 | 8 | # Apply phpcbf to commonsbooking.php 9 | f8bdfd6d5d595f22d7c0345a2eb35571343ed134 10 | 11 | # Apply after wrong merge 12 | 2deacd97716f5a1128b8e5b7cf6bb9bdd161b4f8 13 | 14 | # Apply last time before automatic action 15 | 102673ed5e3e2969090ce754aa97dde88bc7377d -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug, triage 6 | assignees: '' 7 | 8 | --- 9 | Plugin Version (or master commit hash): 10 | 11 | PHP Version: 12 | 13 | **Describe the bug** 14 | 15 | A clear and concise description of what the bug is. 16 | 17 | **To Reproduce** 18 | 19 | Steps to reproduce the behavior: 20 | 21 | **Expected behavior** 22 | 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Screenshots** 26 | 27 | If applicable, add screenshots to help explain your problem. 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: About an Issue / Feature Request 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/translation.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Translation 3 | about: Translation improvement 4 | title: "[Translation]" 5 | labels: translation 6 | assignees: '' 7 | 8 | --- 9 | 10 | **original string** 11 | 12 | 13 | 14 | **improved string** 15 | -------------------------------------------------------------------------------- /.github/actions/build-plugin/action.yml: -------------------------------------------------------------------------------- 1 | name: 'Build Plugin' 2 | description: 'Builds plugin into ./build/commonsbooking' 3 | 4 | inputs: 5 | generate_zip: 6 | description: 'Set to true, if a zip file should be generated in the root of the plugin folder' 7 | required: false 8 | default: 'false' 9 | options: 10 | - true 11 | - false 12 | 13 | runs: 14 | using: "composite" 15 | steps: 16 | 17 | - uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: '7.4' 20 | 21 | - name: Cache Composer packages 22 | id: composer-cache 23 | uses: actions/cache@v4 24 | with: 25 | path: vendor 26 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 27 | restore-keys: | 28 | ${{ runner.os }}-php- 29 | 30 | - name: Install dependencies 31 | run: | 32 | composer install --no-dev --prefer-dist --no-progress 33 | shell: bash 34 | 35 | - uses: actions/setup-node@v4 36 | with: 37 | node-version-file: '.nvmrc' 38 | cache: 'npm' 39 | - run: npm ci --legacy-peer-deps --include=dev 40 | shell: bash 41 | 42 | - name: Run build script (skip zip generation) 43 | if: ${{ inputs.generate_zip == 'false' }} 44 | run: bin/build-zip.sh --skip-zip 45 | shell: bash 46 | 47 | - name: Run build script and generate zip file 48 | if: ${{ inputs.generate_zip == 'true' }} 49 | run: bin/build-zip.sh 50 | shell: bash 51 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | # Maintain dependencies for GitHub Actions 4 | - package-ecosystem: "github-actions" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | 9 | # Maintain dependencies for npm 10 | - package-ecosystem: "npm" 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | commit-message: 15 | prefix: "npm" 16 | include: "scope" 17 | 18 | # Maintain dependencies for Composer 19 | - package-ecosystem: "composer" 20 | directory: "/" 21 | schedule: 22 | interval: "weekly" 23 | commit-message: 24 | prefix: "composer" 25 | include: "scope" 26 | -------------------------------------------------------------------------------- /.github/workflows/build-rc.yml: -------------------------------------------------------------------------------- 1 | name: Build zip from release branch 2 | on: 3 | push: 4 | branches: 5 | - 'release/**' 6 | 7 | jobs: 8 | zip-rc: 9 | runs-on: ubuntu-24.04 10 | steps: 11 | 12 | - uses: actions/checkout@v4 13 | 14 | - uses: ./.github/actions/build-plugin 15 | 16 | - name: Generate zip 17 | uses: 10up/action-wordpress-plugin-build-zip@stable 18 | env: 19 | SLUG: commonsbooking 20 | BUILD_DIR: ./build/commonsbooking 21 | 22 | -------------------------------------------------------------------------------- /.github/workflows/compile.yml: -------------------------------------------------------------------------------- 1 | name: Compile changes 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - 'release/**' 7 | paths-ignore: 8 | - '**.md' 9 | - '**.txt' 10 | 11 | jobs: 12 | i18n-coverage: 13 | runs-on: ubuntu-24.04 14 | name: Check i18n coverage 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: '7.4' 21 | 22 | - name: Install WP-CLI 23 | run: | 24 | curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar 25 | chmod +x wp-cli.phar 26 | sudo mv wp-cli.phar /usr/local/bin/wp 27 | 28 | - name: Generate pot files 29 | run: bin/update-pot.sh 30 | 31 | - name: Commit changes 32 | uses: elstudio/actions-js-build/commit@v4 33 | with: 34 | commitMessage: Ran wp i18n make-pot 35 | 36 | - name: Check i18n coverage 37 | uses: alexkiro/i18n-coverage@v1.0.1 38 | with: 39 | token: ${{ secrets.GITHUB_TOKEN }} 40 | translations-path: 'languages/*.po' 41 | ignore-languages: 'en' 42 | 43 | grunt: 44 | runs-on: ubuntu-24.04 45 | name: Run grunt and commit changes 46 | needs: i18n-coverage 47 | steps: 48 | 49 | - uses: actions/checkout@v4 50 | with: 51 | ref: ${{ github.ref }} # checkout the latest commit so that we can push changes 52 | 53 | - uses: shivammathur/setup-php@v2 54 | with: 55 | php-version: '7.4' 56 | 57 | - uses: actions/setup-node@v4 58 | with: 59 | node-version-file: '.nvmrc' 60 | cache: 'npm' 61 | 62 | - run: npm ci --legacy-peer-deps --include=dev 63 | 64 | - name: Validate composer.json and composer.lock 65 | run: composer validate 66 | 67 | 68 | - name: Compile with Grunt 69 | uses: elstudio/actions-js-build/build@v4 70 | with: 71 | args: build 72 | 73 | - name: Commit changes 74 | uses: elstudio/actions-js-build/commit@v4 75 | with: 76 | commitMessage: Ran grunt 77 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: E2E Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | - 'release/**' 8 | paths-ignore: 9 | - '**.md' 10 | - '**.txt' 11 | pull_request: 12 | branches: 13 | - 'master' 14 | 15 | 16 | env: 17 | DB_DATABASE: wordpress_test 18 | DB_USER: root 19 | DB_PASSWORD: '' 20 | 21 | jobs: 22 | e2e: 23 | name: 'WP ${{ matrix.core.name }} on PHP ${{ matrix.php }}' 24 | runs-on: ubuntu-24.04 25 | 26 | strategy: 27 | matrix: 28 | php: 29 | - '7.4' 30 | - '8.3' 31 | core: 32 | - {name: 'tested', version: 'null'} 33 | - {name: 'minimum', version: 'WordPress/WordPress#5.9'} 34 | - {name: 'trunk', version: 'WordPress/WordPress#master'} 35 | 36 | steps: 37 | - uses: actions/checkout@v4 38 | 39 | - uses: ./.github/actions/build-plugin 40 | 41 | - name: Overwrite PHP versions & core for wp-env 42 | if: matrix.core.version != 'null' 43 | run: jq '. + {"phpVersion":"${{ matrix.php }}","core":"${{ matrix.core.version }}"}' tests/cypress/wordpress-files/e2e-override.json > .wp-env.override.json 44 | 45 | - name: setup wp env 46 | run: npm run env:start 47 | 48 | - name: install test data for e2e test 49 | run: npm run cypress:setup 50 | 51 | - name: Cypress run 52 | uses: cypress-io/github-action@v6 53 | with: 54 | install: false 55 | config-file: tests/cypress/cypress.config.js 56 | 57 | - name: Upload Cypress screenshots 58 | if: ${{ always() }} 59 | uses: actions/upload-artifact@v4 60 | with: 61 | name: cypress-screenshots-${{ matrix.php }}_${{ matrix.core.name }} 62 | path: ${{ github.workspace }}/tests/cypress/screenshots/ 63 | rerun-on-failure: 64 | needs: e2e 65 | if: failure() && fromJSON(github.run_attempt) < 3 66 | runs-on: ubuntu-latest 67 | steps: 68 | - env: 69 | GH_REPO: ${{ github.repository }} 70 | GH_TOKEN: ${{ github.token }} 71 | GH_DEBUG: api 72 | run: gh workflow run rerun.yml -r ${{ github.head_ref || github.ref_name }} -F run_id=${{ github.run_id }} 73 | -------------------------------------------------------------------------------- /.github/workflows/phpcbf-check.yml: -------------------------------------------------------------------------------- 1 | name: PHP Code Beautifier Check 2 | 3 | on: 4 | push: 5 | paths: 6 | - '**/*.php' 7 | pull_request: 8 | paths: 9 | - '**/*.php' 10 | workflow_dispatch: 11 | 12 | 13 | jobs: 14 | phpcbf-check: 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up PHP 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: '8.1' # Adjust this to your PHP version 25 | extensions: uopz 26 | 27 | 28 | - name: Install dependencies 29 | run: composer install --prefer-dist --no-progress --ignore-platform-reqs 30 | 31 | - name: Run phpcbf 32 | run: ./vendor/bin/phpcbf -q --parallel=1 src templates includes tests commonsbooking.php 33 | 34 | - name: Verify code style is unchanged 35 | run: | 36 | if [[ -n "$(git status --porcelain)" ]]; then 37 | echo "ERROR: phpcbf made changes. Please run phpcbf locally and commit the fixes." 38 | exit 1 39 | else 40 | echo "Code style is compliant." 41 | fi 42 | -------------------------------------------------------------------------------- /.github/workflows/phpstan.yml: -------------------------------------------------------------------------------- 1 | name: PHPStan 2 | 3 | on: 4 | # Run on all pushes and on all pull requests. 5 | # Prevent the build from running when there are only irrelevant changes. 6 | push: 7 | paths-ignore: 8 | - '**.md' 9 | - '**.txt' 10 | - '**.yml' 11 | - '**.xml' 12 | pull_request: 13 | # Allow manually triggering the workflow. 14 | workflow_dispatch: 15 | 16 | jobs: 17 | phpstan: 18 | name: 'WP latest on PHP 8.3' 19 | 20 | runs-on: ubuntu-24.04 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | 25 | # TODO Use the composer action below 26 | - uses: shivammathur/setup-php@v2 27 | with: 28 | php-version: '8.3' 29 | extensions: uopz 30 | 31 | - name: Validate composer.json and composer.lock 32 | run: composer validate 33 | 34 | - name: Install dependencies (other PHP versions) 35 | run: | 36 | composer install --prefer-dist --no-progress --ignore-platform-reqs 37 | 38 | - name: Run static suite 39 | run: | 40 | composer dump-autoload -o 41 | 42 | #- uses: actions/checkout@v4 43 | #- uses: php-actions/composer@v6 44 | 45 | - name: PHPStan Static Analysis 46 | uses: php-actions/phpstan@v3 47 | with: 48 | configuration: phpstan.neon 49 | -------------------------------------------------------------------------------- /.github/workflows/phpunit.yml: -------------------------------------------------------------------------------- 1 | name: PHP Unit Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | - 'release/**' 8 | paths-ignore: 9 | - '**.md' 10 | - '**.txt' 11 | pull_request: 12 | branches: 13 | - 'master' 14 | 15 | 16 | env: 17 | DB_DATABASE: wordpress_test 18 | DB_USER: root 19 | DB_PASSWORD: '' 20 | 21 | jobs: 22 | phpunit: 23 | name: 'WP ${{ matrix.core.name }} on PHP ${{ matrix.php }}' 24 | 25 | runs-on: ubuntu-24.04 26 | 27 | strategy: 28 | matrix: 29 | php: 30 | - '7.4' 31 | - '8.3' 32 | core: 33 | - { name: 'latest', version: 'latest' } 34 | - { name: 'minimum', version: '5.9' } 35 | - { name: 'trunk', version: 'trunk' } 36 | 37 | steps: 38 | - uses: actions/checkout@v4 39 | 40 | - name: Set up MySQL 41 | run: | 42 | sudo /etc/init.d/mysql start 43 | 44 | - uses: shivammathur/setup-php@v2 45 | with: 46 | php-version: ${{ matrix.php }} 47 | extensions: uopz 48 | 49 | # This is needed for the WordPress test suite installation ( bin/install-wp-tests.sh ) 50 | - name: Install SVN 51 | run: sudo apt install -y subversion 52 | 53 | 54 | - name: Validate composer.json and composer.lock 55 | run: composer validate 56 | 57 | - name: Cache Composer packages 58 | id: composer-cache 59 | uses: actions/cache@v4 60 | with: 61 | path: vendor 62 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 63 | restore-keys: | 64 | ${{ runner.os }}-php- 65 | 66 | - name: Install dependencies (PHP 7.4) 67 | if: matrix.php == '7.4' 68 | run: | 69 | bash bin/install-wp-tests.sh wordpress_test root root localhost ${{ matrix.core.version }} 70 | composer install --prefer-dist --no-progress 71 | 72 | - name: Install dependencies (other PHP versions) 73 | if: matrix.php != '7.4' 74 | run: | 75 | bash bin/install-wp-tests.sh wordpress_test root root localhost ${{ matrix.core.version }} 76 | composer install --prefer-dist --no-progress --ignore-platform-reqs 77 | 78 | - name: Run test suite 79 | run: | 80 | composer dump-autoload -o 81 | php vendor/bin/phpunit 82 | 83 | - name: Upload coverage reports to Codecov 84 | uses: codecov/codecov-action@v5 85 | with: 86 | token: ${{ secrets.CODECOV_TOKEN }} 87 | files: ./build/logs/clover.xml 88 | rerun-on-failure: 89 | needs: phpunit 90 | if: failure() && fromJSON(github.run_attempt) < 3 91 | runs-on: ubuntu-latest 92 | steps: 93 | - env: 94 | GH_REPO: ${{ github.repository }} 95 | GH_TOKEN: ${{ github.token }} 96 | GH_DEBUG: api 97 | run: gh workflow run rerun.yml -r ${{ github.head_ref || github.ref_name }} -F run_id=${{ github.run_id }} 98 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to WordPress.org 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | name: New Release 10 | runs-on: ubuntu-24.04 11 | steps: 12 | 13 | - uses: actions/checkout@v4 14 | 15 | - uses: ./.github/actions/build-plugin 16 | 17 | - name: Check i18n coverage before release 18 | uses: alexkiro/i18n-coverage@v1.0.1 19 | with: 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | translations-path: 'languages/*.po' 22 | ignore-languages: 'en' 23 | min-coverage: 100 24 | 25 | - name: WordPress Plugin Deploy 26 | uses: 10up/action-wordpress-plugin-deploy@stable 27 | with: 28 | dry-run: false 29 | generate-zip: true 30 | env: 31 | SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} 32 | SVN_USERNAME: ${{ secrets.SVN_USERNAME }} 33 | BUILD_DIR: build/commonsbooking 34 | SLUG: commonsbooking # optional, remove if GitHub repo name matches SVN slug, including capitalization 35 | 36 | - name: Upload release asset 37 | uses: actions/upload-release-asset@v1 38 | env: 39 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 40 | with: 41 | upload_url: ${{ github.event.release.upload_url }} 42 | asset_path: ${{ steps.deploy.outputs.zip-path }} 43 | asset_name: ${{ github.event.repository.name }}.zip 44 | asset_content_type: application/zip 45 | -------------------------------------------------------------------------------- /.github/workflows/rerun.yml: -------------------------------------------------------------------------------- 1 | name: Rerun workflow 2 | 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | run_id: 7 | required: true 8 | jobs: 9 | rerun: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: rerun ${{ inputs.run_id }} 13 | env: 14 | GH_REPO: ${{ github.repository }} 15 | GH_TOKEN: ${{ github.token }} 16 | GH_DEBUG: api 17 | run: | 18 | gh run watch ${{ inputs.run_id }} > /dev/null 2>&1 19 | gh run rerun ${{ inputs.run_id }} --failed 20 | -------------------------------------------------------------------------------- /.github/workflows/zip-pr.yml: -------------------------------------------------------------------------------- 1 | name: Build zip from PR 2 | on: 3 | pull_request: 4 | branches: 5 | - 'master' 6 | 7 | jobs: 8 | zip-pr: 9 | if: github.repository == 'wielebenwir/commonsbooking' 10 | runs-on: ubuntu-24.04 11 | steps: 12 | 13 | - uses: actions/checkout@v4 14 | with: 15 | fetch-depth: 0 # Important for getting full commit history 16 | 17 | - name: Update Version Comment 18 | run: | 19 | sed -i "s|define('COMMONSBOOKING_VERSION_COMMENT',.*|define('COMMONSBOOKING_VERSION_COMMENT', '${{ github.head_ref }} at ${{ github.event.pull_request.head.sha}}');|" commonsbooking.php 20 | - uses: ./.github/actions/build-plugin 21 | with: 22 | generate_zip: 'true' 23 | 24 | - name: Upload zip 25 | uses: SamKirkland/FTP-Deploy-Action@v4.3.5 26 | with: 27 | exclude: | 28 | ** 29 | ! commonsbooking.zip 30 | server: ${{ secrets.CBZIPUPLOAD_SERVER }} 31 | username: ${{ secrets.CBZIPUPLOAD_USER }} 32 | password: ${{ secrets.CBZIPUPLOAD_KEY }} 33 | server-dir: ${{ github.event.number }}/ 34 | dangerous-clean-slate: true 35 | protocol: ftps 36 | 37 | - name: Find Comment 38 | uses: peter-evans/find-comment@v3 39 | id: fc 40 | with: 41 | issue-number: ${{ github.event.pull_request.number }} 42 | comment-author: 'github-actions[bot]' 43 | body-includes: Download built plugin zip 44 | 45 | - name: Create or update comment 46 | uses: peter-evans/create-or-update-comment@v4 47 | with: 48 | comment-id: ${{ steps.fc.outputs.comment-id }} 49 | issue-number: ${{ github.event.pull_request.number }} 50 | body: | 51 | [Download built plugin zip](https://builds.commonsbooking.org/${{ github.event.number }}/commonsbooking.zip) 52 | [Test Plugin in Wordpress Playground](https://playground.wordpress.net/#{%22steps%22:[{%22step%22:%22installPlugin%22,%22pluginData%22:{%22resource%22:%22url%22,%22url%22:%22https://builds.commonsbooking.org/${{ github.event.number }}/commonsbooking.zip%22}},{%22step%22:%22importWxr%22,%22file%22:{%22resource%22:%22url%22,%22url%22:%22https://raw.githubusercontent.com/wielebenwir/commonsbooking/refs/heads/master/tests/cypress/wordpress-files/content-example.xml%22}}],%22preferredVersions%22:{%22php%22:%227.4%22,%22wp%22:%22latest%22},%22login%22:true}) 53 | edit-mode: replace 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .sass-cache 2 | .DS_Store 3 | .nova 4 | yarn.lock 5 | cache 6 | commonsbooking.zip 7 | node_modules 8 | .phpunit.* 9 | /phpunit 10 | assets/packaged 11 | src/Wordpress/.DS_Store 12 | tests/cypress/screenshots 13 | /vendor 14 | /build 15 | src/.DS_Store 16 | assets/.DS_Store 17 | #assets/admin/css 18 | #assets/global/css 19 | #assets/public/css 20 | #vendor 21 | views/.DS_Store 22 | /composer.phar 23 | copy-svn.sh 24 | .vscode/settings.json 25 | .idea 26 | cb-local-1.code-workspace 27 | commonsbookingdocker.code-workspace 28 | wp-core 29 | tests/cypress/screenshots 30 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.5.1 2 | -------------------------------------------------------------------------------- /.wp-env.json: -------------------------------------------------------------------------------- 1 | { 2 | "phpVersion" : "7.4", 3 | "core" : "Wordpress/Wordpress#6.8", 4 | "plugins" : [ 5 | ".", 6 | "https://downloads.wordpress.org/plugin/wp-crontrol.zip", 7 | "https://downloads.wordpress.org/plugin/wordpress-importer.zip", 8 | "https://downloads.wordpress.org/plugin/query-monitor.zip", 9 | "https://downloads.wordpress.org/plugin/wp-mail-logging.zip", 10 | "https://downloads.wordpress.org/plugin/jsm-show-post-meta.zip", 11 | "https://downloads.wordpress.org/plugin/disable-welcome-messages-and-tips.zip" 12 | ], 13 | "port" : 1000, 14 | "testsPort" : 1001, 15 | "config" : { 16 | "WP_DEBUG" : true, 17 | "WP_DEBUG_LOG" : true, 18 | "WP_DEBUG_DISPLAY" : false 19 | }, 20 | "themes" : [ 21 | "flegfleg/kasimir-theme" 22 | ], 23 | "env" : { 24 | "tests" : { 25 | "mappings" : { 26 | "wp-content/plugins/commonsbooking" : "." 27 | }, 28 | "config" : { 29 | "WP_DEBUG" : true, 30 | "WP_DEBUG_LOG" : true, 31 | "WP_DEBUG_DISPLAY" : false 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /TECHNICAL.md: -------------------------------------------------------------------------------- 1 | ### Formatter 2 | 3 | We adhere to [PHPCS](https://github.com/PHPCSStandards/PHP_CodeSniffer) rules defined in the [phpcs.xml](https://github.com/wielebenwir/commonsbooking/blob/master/.phpcs.xml.dist) rules file, as it is a mature tool and well established in the Wordpress-Plugin development scene. 4 | A program to apply auto-formattable rules of PHPCS is `phpcbf` and we encourage everyone 5 | to configure this tool in their IDE so that contribution commits consist of properly formatted code. 6 | Both are already in the dev dependencies of the repository code. 7 | 8 | We have an automatic check [as Github Action](https://github.com/wielebenwir/commonsbooking/tree/master/.github/workflows/phpcbf-check.yml) in our CI/CD-Pipeline, which prevents code contributions that not adhere to the rules. 9 | 10 | #### Ignore formatter revisions 11 | 12 | We use .git-blame-ignore-revs to track repo-wide cosmetic refactorings by auto format tools like prettier/phpcbf. 13 | See [Github Documentation](https://docs.github.com/de/repositories/working-with-files/using-files/viewing-and-understanding-files#ignore-commits-in-the-blame-view) 14 | 15 | You can also configure your local git so it always ignores the revs in that file: 16 | 17 | ```bash 18 | git config blame.ignoreRevsFile .git-blame-ignore-revs 19 | ``` 20 | -------------------------------------------------------------------------------- /assets/admin/js/index.php: -------------------------------------------------------------------------------- 1 | { 9 | const DATES_SEPERATOR = ","; 10 | //we need to add a leading zero if the day or month is less than 10 11 | var day = date.getDate(); 12 | var month = date.getMonth() + 1; 13 | var dd = day <= 9 ? "0" + day : day; 14 | //month is 0 based, so we need to add 1 15 | var mm = month <= 9 ? "0" + month : month; 16 | var yyyy = date.getFullYear(); 17 | var dateStr = yyyy + "-" + mm + "-" + dd; 18 | if (manualDateInput.val().length > 0) { 19 | if (manualDateInput.val().slice(-1) !== DATES_SEPERATOR) { 20 | manualDateInput.val(manualDateInput.val() + DATES_SEPERATOR + dateStr); 21 | } else { 22 | manualDateInput.val(manualDateInput.val() + dateStr); 23 | } 24 | } else { 25 | manualDateInput.val(dateStr + DATES_SEPERATOR); 26 | } 27 | } 28 | if (manualDatePicker.length) { 29 | manualDatePicker.datepicker({ 30 | // enable selecting multiple dates 31 | onSelect: function(dateText, inst) { 32 | var date = $(this).datepicker( "getDate" ); 33 | addHolidayToInput(date); 34 | } 35 | }); 36 | } 37 | if (holidayLoadButton.length) { 38 | var fillHolidays = (year, state) => { 39 | var holidays = feiertagejs.getHolidays(year, state); 40 | //add holidays to input field, comma separated in format (d.m.Y) and with trailing semi-colon 41 | holidays.forEach((holiday) => { 42 | var date = new Date(holiday.date); 43 | addHolidayToInput(date); 44 | }); 45 | }; 46 | 47 | holidayLoadButton.click(function () { 48 | fillHolidays( 49 | $('#_cmb2_holidayholiday_year').val(), 50 | $('#_cmb2_holidayholiday_state').val() 51 | ); 52 | }); 53 | } 54 | }); 55 | })(jQuery); 56 | -------------------------------------------------------------------------------- /assets/admin/js/src/location.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | 'use strict'; 3 | $(function () { 4 | 5 | /** 6 | * Hides set-elements. 7 | * @param set 8 | */ 9 | const hideFieldset = function (set) { 10 | $.each(set, function () { 11 | $(this).parents('.cmb-row').hide(); 12 | }); 13 | }; 14 | 15 | /** 16 | * Show set-elements. 17 | * @param set 18 | */ 19 | const showFieldset = function (set) { 20 | $.each(set, function () { 21 | $(this).parents('.cmb-row').show(); 22 | }); 23 | }; 24 | 25 | const useGlobalSettings = $('#_cb_use_global_settings'); 26 | const allowLockDaysCheckbox = $('#_cb_allow_lockdays_in_range'); 27 | const countLockedDaysCheckbox = $('#_cb_count_lockdays_in_range'); 28 | const countAmountLockedDays = $('#_cb_count_lockdays_maximum'); 29 | 30 | const handleCountLockedDays = function () { 31 | if (countLockedDaysCheckbox.prop('checked')) { 32 | showFieldset(countAmountLockedDays); 33 | } else { 34 | hideFieldset(countAmountLockedDays); 35 | } 36 | }; 37 | handleCountLockedDays(); 38 | countLockedDaysCheckbox.change(function () { 39 | handleCountLockedDays(); 40 | }); 41 | 42 | const handleAllowLockDays = function () { 43 | if (allowLockDaysCheckbox.prop('checked')) { 44 | showFieldset(countLockedDaysCheckbox); 45 | handleCountLockedDays(); 46 | } else { 47 | hideFieldset(countLockedDaysCheckbox); 48 | hideFieldset(countAmountLockedDays); 49 | } 50 | } 51 | handleAllowLockDays(); 52 | allowLockDaysCheckbox.change(function () { 53 | handleAllowLockDays(); 54 | } ); 55 | 56 | //hide settings if global settings are used 57 | const handleUseGlobalSettings = function () { 58 | if (useGlobalSettings.prop('checked')) { 59 | hideFieldset(allowLockDaysCheckbox); 60 | hideFieldset(countLockedDaysCheckbox); 61 | hideFieldset(countAmountLockedDays); 62 | } 63 | else { 64 | showFieldset(allowLockDaysCheckbox); 65 | showFieldset(countLockedDaysCheckbox); 66 | handleCountLockedDays(); 67 | } 68 | } 69 | handleUseGlobalSettings(); 70 | useGlobalSettings.change(function () { 71 | handleUseGlobalSettings(); 72 | }); 73 | 74 | }); 75 | })(jQuery); 76 | -------------------------------------------------------------------------------- /assets/admin/js/src/massoperations.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | 'use strict'; 3 | $(function () { 4 | 5 | $('#orphans-migration-start').on('click', function (event) { 6 | event.preventDefault(); 7 | 8 | $('#orphans-migration-in-progress').show(); 9 | 10 | let checkedBoxes = $('.post-checkboxes:checkbox:checked'); 11 | let ids = []; 12 | checkedBoxes.each(function () { 13 | ids.push($(this).val()); 14 | }); 15 | let data = ids; 16 | 17 | $.post( 18 | cb_ajax_orphaned_booking_migration.ajax_url, 19 | { 20 | _ajax_nonce: cb_ajax_orphaned_booking_migration.nonce, 21 | action: "cb_orphaned_booking_migration", 22 | data: data 23 | }).done(function (data) { 24 | if (data.success) { 25 | $('#orphans-migration-in-progress').hide(); 26 | $('#orphans-migration-done').show(); 27 | $('#orphans-migration-done span').text(data.message); 28 | //now remove all rows that have been migrated 29 | ids.forEach(function (id) { 30 | $('#row-booking-' + id).remove(); 31 | }); 32 | } else { 33 | $('#orphans-migration-in-progress').hide(); 34 | $('#orphans-migration-failed').show(); 35 | $('#orphans-migration-failed span').text(data.message); 36 | } 37 | }); 38 | }) 39 | }); 40 | })(jQuery); 41 | -------------------------------------------------------------------------------- /assets/admin/js/src/restriction.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | 'use strict'; 3 | $(function () { 4 | const form = $('input[name=post_type][value=cb_restriction]').parent('form').find('#cb_restriction-custom-fields'); 5 | 6 | // disable send button on change of form 7 | form.find('input, select, textarea').on('keyup change paste', function () { 8 | form.find('input[name=restriction-send]').prop("disabled", true); 9 | }); 10 | 11 | }); 12 | })(jQuery); 13 | -------------------------------------------------------------------------------- /assets/admin/js/src/templates.js: -------------------------------------------------------------------------------- 1 | (function ($) { 2 | 'use strict'; 3 | $(function () { 4 | const hideFieldset = function(set) { 5 | $.each(set, function() { 6 | $(this).parents(".cmb-row").hide(); 7 | }); 8 | }; 9 | const showFieldset = function(set) { 10 | $.each(set, function() { 11 | $(this).parents(".cmb-row").show(); 12 | }); 13 | }; 14 | const emailform = $("#templates"); 15 | if (emailform.length) { 16 | const eventCreateCheckbox = $('#emailtemplates_mail-booking_ics_attach'); 17 | const eventTitleInput = $('#emailtemplates_mail-booking_ics_event-title'); 18 | const eventDescInput = $('#emailtemplates_mail-booking_ics_event-description'); 19 | const eventFieldSet = [eventTitleInput,eventDescInput]; 20 | 21 | const handleiCalAttachmentSelection = function() { 22 | showFieldset(eventFieldSet); 23 | if (!eventCreateCheckbox.prop("checked")) { 24 | hideFieldset(eventFieldSet); 25 | eventCreateCheckbox.prop("checked", false); 26 | } 27 | }; 28 | 29 | handleiCalAttachmentSelection(); 30 | 31 | eventCreateCheckbox.click(function() { 32 | handleiCalAttachmentSelection(); 33 | }); 34 | } 35 | }); 36 | })(jQuery); 37 | -------------------------------------------------------------------------------- /assets/admin/js/src/toolltip.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Tooltips 3 | * 4 | * Enables Javascript tooltips via jQuery UI (loaded in the WP Backend by default) 5 | * 6 | * Usage: 7 | */ 8 | 9 | (function ($) { 10 | 'use strict'; 11 | $(function () { 12 | $( document ).tooltip(); 13 | }); 14 | })(jQuery); 15 | -------------------------------------------------------------------------------- /assets/admin/sass/admin.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Admin styles 3 | * 4 | * Loaded only in the WP Backend 5 | */ 6 | 7 | /* Global styles */ 8 | @use "../../global/sass/global"; 9 | 10 | /* Dashboard */ 11 | @use "./partials/_dashboard"; 12 | 13 | /* Timeframe Form */ 14 | @use "./partials/_form-timeframe"; 15 | 16 | /** Migration Settings */ 17 | @use "./partials/_migration"; 18 | 19 | /** Export Settings */ 20 | @use "./partials/_form-export"; 21 | 22 | /* Booking form */ 23 | @use "./partials/form-booking"; 24 | 25 | /* Map creation */ 26 | @use "./partials/form-map"; 27 | 28 | /* Plugin update page */ 29 | @use "./partials/plugin_update"; 30 | 31 | /* Edit Screens Backend */ 32 | @use "./partials/admin-edit"; 33 | 34 | /* Lib: jQuery UI */ 35 | @use "lib/_jquery-ui"; 36 | .cb-admin { 37 | @include jquery-ui.jquery-ui; 38 | } -------------------------------------------------------------------------------- /assets/admin/sass/partials/_admin-edit.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Backend Edit Screens and messages 3 | * 4 | */ 5 | 6 | .notice-error, div.error { 7 | background-color: #f0b6b6 !important; 8 | } -------------------------------------------------------------------------------- /assets/admin/sass/partials/_dashboard.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Dashboard admin page 3 | * 4 | */ 5 | 6 | @use "../../../global/sass/partials/extends"; 7 | 8 | .cb-admin-page { 9 | .cb-admin-cols { 10 | display: flex; 11 | flex-direction: row; 12 | div { 13 | margin-right: 15px; 14 | 15 | 16 | h2 { 17 | padding-top: 0; 18 | } 19 | 20 | ul { 21 | li { 22 | margin-top: 15px; 23 | @extend .cb-box; 24 | .cb-summmary-item { 25 | font-weight: bold; 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | 33 | 34 | .cb_welcome-panel { 35 | position: relative; 36 | overflow: auto; 37 | margin: 16px 0; 38 | padding: 23px 10px 0; 39 | border: 1px solid #c3c4c7; 40 | box-shadow: 0 1px 1px rgba(0,0,0,.04); 41 | background: #fff; 42 | font-size: 13px; 43 | line-height: 1.7; 44 | 45 | 46 | h3 { 47 | margin: 1.33em 0 0; 48 | font-size: 16px; 49 | } 50 | 51 | li { 52 | font-size: 14px; 53 | } 54 | 55 | .cb_welcome-panel-content { 56 | margin-left: 13px; 57 | max-width: 1500px; 58 | } 59 | 60 | .cb_welcome-panel-column-container { 61 | clear: both; 62 | position: relative; 63 | } 64 | 65 | .cb_welcome-panel-column { 66 | width: 32%; 67 | min-width: 200px; 68 | float: left; 69 | 70 | p { 71 | margin-top: 7px; 72 | color: #3c434a; 73 | } 74 | 75 | ul { 76 | margin: .8em 1em 1em 0; 77 | } 78 | 79 | li { 80 | line-height: 1.14285714; 81 | list-style-type: none; 82 | padding: 0 0 8px; 83 | 84 | a { 85 | text-decoration: none; 86 | } 87 | } 88 | 89 | } 90 | 91 | 92 | .cb_welcome-panel-column:first-child { 93 | width: 36%; 94 | } 95 | } -------------------------------------------------------------------------------- /assets/admin/sass/partials/_form-booking.scss: -------------------------------------------------------------------------------- 1 | .post-type-cb_booking { 2 | #publish { 3 | display: none; 4 | } 5 | } -------------------------------------------------------------------------------- /assets/admin/sass/partials/_form-export.scss: -------------------------------------------------------------------------------- 1 | #timeframe-export-done, 2 | #timeframe-export-failed, 3 | #timeframe-export-in-progress 4 | { 5 | margin-bottom: 1rem; 6 | display: none; 7 | } 8 | 9 | #timeframe-export-in-progress { 10 | @keyframes blinkText { 11 | from {opacity:0;} 12 | to {opacity: 1;} 13 | } 14 | .blinking { 15 | animation: blinkText 1s infinite alternate; 16 | } 17 | } -------------------------------------------------------------------------------- /assets/admin/sass/partials/_form-map.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * The CPT creation form map 3 | */ 4 | 5 | #cmb2-metabox-cb_map-custom-fields { 6 | .map-organizer { 7 | .cmb2-metabox-title { 8 | font-size: x-large; 9 | } 10 | .cmb2-metabox-title:before { 11 | /* arrow to right */ 12 | content: "\2192 "; 13 | } 14 | } 15 | 16 | .cmb-group-name { 17 | font-size: x-large; 18 | font-weight: bold; 19 | } 20 | 21 | .cmb-group-name:before { 22 | content: "\2192 "; 23 | } 24 | 25 | #shortcode-field { 26 | display: flex; 27 | align-items: center; 28 | flex-wrap: nowrap; 29 | gap: 10px; 30 | } 31 | 32 | #shortcode-field code { 33 | white-space: nowrap; 34 | margin-right: 10px; 35 | } 36 | 37 | #shortcode-field .button { 38 | padding: 5px 10px; 39 | line-height: 1.5; 40 | } 41 | } -------------------------------------------------------------------------------- /assets/admin/sass/partials/_form-timeframe.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Timeframe form 3 | * 4 | */ 5 | 6 | #cb_timeframe-custom-fields { 7 | .cmb2-id-start-time, 8 | .cmb2-id-start-date, 9 | .cmb2-id-end-date, 10 | .cmb2-id-end-time, 11 | .cmb2-id-grid, 12 | .cmb2-id-repetition, 13 | .cmb2-id-weekdays, 14 | .cmb2-id-title-timeframe-rep-config, 15 | .cmb2-id-repetition-start, 16 | .cmb2-id-repetition-end, 17 | .cmb2-id-full-day { 18 | display: none; 19 | } 20 | } 21 | 22 | select[readonly] { 23 | background: #eee !important; 24 | pointer-events: none !important; 25 | touch-action: none !important; 26 | } 27 | 28 | a.disabled { 29 | opacity: 0.5; 30 | pointer-events: none; 31 | cursor: default; 32 | } 33 | 34 | 35 | .cmb2-id-booking-codes-list { 36 | .cmb-td { 37 | > table.cmb2-codes-outer-table { 38 | width: 100%; 39 | > tbody > tr { 40 | display: flex; 41 | flex-wrap: wrap; 42 | justify-content: flex-start; 43 | gap: 1rem; 44 | > td { 45 | margin-bottom: unset; 46 | padding-bottom: unset; 47 | padding-top: unset; 48 | } 49 | } 50 | table.cmb2-codes-column { 51 | border: none; 52 | td { 53 | border: none; 54 | display: table-cell; 55 | padding-top: 0.5rem; 56 | padding-bottom: 0.5rem; 57 | } 58 | tr:nth-child(odd) { 59 | background-color: #f2f2f2; 60 | } 61 | } 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /assets/admin/sass/partials/_migration.scss: -------------------------------------------------------------------------------- 1 | #upgrade-in-progress, 2 | #upgrade-done, 3 | { 4 | margin-top: 1rem; 5 | display: none; 6 | } 7 | 8 | #migration-in-progress, 9 | #migration-state, 10 | #migration-done 11 | { 12 | margin-top: 1rem; 13 | display: none; 14 | } 15 | 16 | #booking-migration-in-progress, 17 | #booking-migration-done, 18 | #booking-migration-failed 19 | { 20 | margin-bottom: 1rem; 21 | display: none; 22 | } 23 | 24 | .blinking { 25 | animation: blinkText 1s infinite alternate; 26 | } 27 | 28 | #orphans-migration-in-progress, 29 | #orphans-migration-done, 30 | #orphans-migration-failed 31 | { 32 | margin-bottom: 1rem; 33 | display: none; 34 | } 35 | 36 | @keyframes blinkText { 37 | from {opacity:0;} 38 | to {opacity: 1;} 39 | } -------------------------------------------------------------------------------- /assets/admin/sass/partials/_plugin_update.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Plugin list update message 3 | * 4 | */ 5 | 6 | .cb-major-update-warning { 7 | margin-bottom: 5px; 8 | max-width: 1000px; 9 | display: -webkit-box; 10 | display: -ms-flexbox; 11 | display: flex; } 12 | .cb-major-update-warning__separator { 13 | margin: 15px -12px; } 14 | .cb-major-update-warning__icon { 15 | font-size: 17px; 16 | margin-left: 9px; 17 | margin-right: 5px; } 18 | .cb-major-update-warning__title { 19 | font-weight: 600; 20 | margin-bottom: 10px; } 21 | .cb-major-update-warning + p { 22 | display: none; } 23 | 24 | .notice-success .cb-major-update-warning__separator { 25 | border: 1px solid var(--commonsbooking-color-success); } 26 | 27 | .notice-success .cb-major-update-warning__icon { 28 | color: var(--commonsbooking-color-success); } 29 | 30 | .notice-warning .cb-major-update-warning__separator { 31 | border: 1px solid var(--commonsbooking-color-warning); } 32 | 33 | .notice-warning .cb-major-update-warning__icon { 34 | color: var(--commonsbooking-color-success); } 35 | 36 | -------------------------------------------------------------------------------- /assets/global/cb-ci/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wielebenwir/commonsbooking/2f394da297c7d6394d451068f92a12beb82b6fa3/assets/global/cb-ci/logo.png -------------------------------------------------------------------------------- /assets/global/css/vendor.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wielebenwir/commonsbooking/2f394da297c7d6394d451068f92a12beb82b6fa3/assets/global/css/vendor.css -------------------------------------------------------------------------------- /assets/global/images/marker-icon-2x-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wielebenwir/commonsbooking/2f394da297c7d6394d451068f92a12beb82b6fa3/assets/global/images/marker-icon-2x-black.png -------------------------------------------------------------------------------- /assets/global/images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wielebenwir/commonsbooking/2f394da297c7d6394d451068f92a12beb82b6fa3/assets/global/images/marker-shadow.png -------------------------------------------------------------------------------- /assets/global/js/dictionary.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Access the cb2 dictionary. 3 | * 4 | * Blank file provides access to the cb2_dictionary object. 5 | * See CB2_Dictionary::localize(); 6 | * 7 | * @author Florian Egermann 8 | * 9 | * @since 2.0.0 10 | * 11 | */ 12 | -------------------------------------------------------------------------------- /assets/global/js/settings.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Access the cb2 plugin settings. 3 | * 4 | * Blank file provides access to the cb2_settings object. 5 | * See CB2_Settings::localize(); 6 | * 7 | * @author Annesley Newholm 8 | * 9 | * @since 2.0.0 10 | * 11 | */ 12 | -------------------------------------------------------------------------------- /assets/global/sass/global.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * Global styles 3 | * 4 | * Shared in Front- and Backend 5 | */ 6 | 7 | /* Lib: reset.css */ 8 | @use "lib/_reset"; 9 | 10 | /* Lib: animate.css */ 11 | @use "lib/_animate"; 12 | 13 | /* Variables: colors, widths, padding */ 14 | @use "partials/_variables"; 15 | 16 | /* Mixins */ 17 | @use "partials/_mixins"; 18 | 19 | /* Extends */ 20 | @use "partials/_extends"; 21 | 22 | /* Utilites */ 23 | @use "partials/_utilities"; -------------------------------------------------------------------------------- /assets/global/sass/lib/_reset.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * CSS Reset 3 | * 4 | * Adapted from: http: //meyerweb.com/eric/tools/css/reset/ 5 | * 6 | * @package CommonsBooking2 7 | */ 8 | 9 | .cb-content { 10 | 11 | ol, 12 | ul, 13 | li, 14 | h2, 15 | h3, 16 | dd, 17 | dl, 18 | dt, 19 | .hentry { 20 | list-style: none; 21 | margin: 0; 22 | padding: 0; 23 | } 24 | 25 | 26 | blockquote, 27 | q { 28 | quotes: none; 29 | } 30 | 31 | blockquote:before, 32 | blockquote:after, 33 | q:before, 34 | q:after { 35 | content: ''; 36 | content: none; 37 | } 38 | 39 | table { 40 | border-collapse: collapse; 41 | border-spacing: 0; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /assets/global/sass/partials/_mixins.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Framework mixins 3 | * 4 | * Breakpoints 5 | * Animation 6 | */ 7 | 8 | /* -------------------- Breakpoints (for lists) -------------------- */ 9 | 10 | @mixin breakpoint($point) { 11 | @if $point == phablet { 12 | @media (min-width: 37.5em) { @content ; } 13 | } 14 | @else if $point == mobileonly { 15 | @media (max-width: 37.5em) { @content ; } 16 | } 17 | } 18 | 19 | 20 | /* -------------------- Animation -------------------- */ 21 | 22 | @mixin transition($args...) { 23 | -webkit-transition: $args; 24 | -moz-transition: $args; 25 | -ms-transition: $args; 26 | -o-transition: $args; 27 | transition: $args; 28 | } 29 | 30 | @mixin background-hover($baseColor, $targetcolor) { 31 | @include transition(all .3s ease); 32 | background-color: $baseColor; 33 | 34 | &:hover { 35 | background-color: $targetcolor; 36 | } 37 | } 38 | 39 | @mixin checkered-bg( $color ) { 40 | $color2: darken($color, 2%); 41 | background-image: linear-gradient(45deg, $color 25%, $color2 25%, $color2 50%, $color 50%, $color 75%, $color2 75%, $color2 100%); 42 | background-size: 20px 20px; 43 | box-shadow: none; 44 | } -------------------------------------------------------------------------------- /assets/global/sass/partials/_utilities.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Debug 3 | * 4 | */ 5 | 6 | .cb-hidden { display: none; } 7 | 8 | /* Template name in content */ 9 | .cb-debug { 10 | opacity: 0; 11 | position: absolute; 12 | right: 0; 13 | top: 0; 14 | font-size: 10px; 15 | background: transparent !important; 16 | } 17 | 18 | .cb-wrapper:hover .cb-debug { 19 | opacity: 1; 20 | } 21 | -------------------------------------------------------------------------------- /assets/map/css/cb-map-admin.css: -------------------------------------------------------------------------------- 1 | .cb-map-settings-cat-filter-list .children { 2 | margin-left: 1.5em; 3 | } 4 | 5 | th { 6 | width: 250px; 7 | } 8 | 9 | .category-wrapper { 10 | height: 150px; 11 | padding: 10px; 12 | background-color: #fff; 13 | overflow-y: scroll; 14 | } 15 | 16 | .option-group { 17 | display: none; 18 | } 19 | 20 | button>span.dashicons { 21 | display: inline-block; 22 | margin-top: 4px; 23 | } 24 | 25 | @keyframes spin { 100% { -webkit-transform: rotateZ(360deg); transform:rotateZ(360deg); } } 26 | 27 | .rotate { 28 | animation: spin 2s linear infinite; 29 | } 30 | 31 | .dashicons-editor-help { 32 | cursor: help; 33 | } 34 | 35 | table.text-left { 36 | text-align: left; 37 | } 38 | 39 | .display-none { 40 | display: none; 41 | } 42 | 43 | .filter-label-name { 44 | text-indent: 1em; 45 | } 46 | 47 | .option-group summary { 48 | font-size: 23px; 49 | color: rgb(35, 40, 45); 50 | padding-bottom: 4px; 51 | padding-top: 9px; 52 | line-height: 29px; 53 | outline: none; 54 | cursor: pointer 55 | } 56 | -------------------------------------------------------------------------------- /assets/map/css/cb-map-shortcode.css: -------------------------------------------------------------------------------- 1 | /* moved to /public/sass/partials/_map.scss */ -------------------------------------------------------------------------------- /assets/map/images/marker-icon-2x-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wielebenwir/commonsbooking/2f394da297c7d6394d451068f92a12beb82b6fa3/assets/map/images/marker-icon-2x-black.png -------------------------------------------------------------------------------- /assets/map/images/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wielebenwir/commonsbooking/2f394da297c7d6394d451068f92a12beb82b6fa3/assets/map/images/marker-shadow.png -------------------------------------------------------------------------------- /assets/map/js/cb-map-locationview.js: -------------------------------------------------------------------------------- 1 | var cb_map_locationview = { 2 | defaults: {}, 3 | marker: null, 4 | map: null, 5 | 6 | init_map: function (latitude, longitude, add_marker) { 7 | // set up the map 8 | map = new L.Map('cb_locationview_map'); 9 | 10 | // TODO generalize this part into method/class construct (see cb-map-positioning.js) 11 | // possible fix to avoid missing tiles, found on: https://stackoverflow.com/questions/38832273/leafletjs-not-loading-all-tiles-until-moving-map 12 | // also see https://github.com/wielebenwir/commonsbooking/issues/1060 13 | map.on("load", function() { setTimeout(() => { 14 | map.invalidateSize(); 15 | }, 500); }); 16 | 17 | // create the tile layer with correct attribution 18 | var osmUrl = 'https://{s}.tile.osm.org/{z}/{x}/{y}.png'; 19 | var osmAttrib = 'Map data © OpenStreetMap contributors'; 20 | var osm = new L.TileLayer(osmUrl, {minZoom: 10, maxZoom: 17, attribution: osmAttrib}); 21 | 22 | map.setView(new L.LatLng(latitude, longitude), 18); 23 | map.addLayer(osm); 24 | 25 | if (add_marker) { 26 | this.add_marker(latitude, longitude); 27 | } 28 | 29 | this.map = map; 30 | }, 31 | 32 | add_marker: function (latitude, longitude) { 33 | var that = this; 34 | 35 | this.marker = L.marker([latitude, longitude], { 36 | draggable: false, 37 | autoPan: false 38 | }).addTo(map); 39 | 40 | }, 41 | } 42 | 43 | jQuery(document).ready(function ($) { 44 | cb_map_locationview.init_map(parseFloat(cb_map_locationview.defaults.latitude), parseFloat(cb_map_locationview.defaults.longitude), true); 45 | }); 46 | -------------------------------------------------------------------------------- /assets/map/js/cb-map-replace-link.js: -------------------------------------------------------------------------------- 1 | jQuery(document).ready(function ($) { 2 | 3 | var $adresses = $('.cb-address'); 4 | 5 | $adresses.each(function() { 6 | var $map_button = $(this).find('a.cb-button'); 7 | if($map_button) { 8 | $timeframe = $map_button.closest('.cb-timeframe'); 9 | timeframe_id = $timeframe.attr('id'); 10 | 11 | var coords; 12 | 13 | if(cb_map_timeframes_geo[timeframe_id]) { 14 | coords = cb_map_timeframes_geo[timeframe_id]; 15 | } 16 | 17 | if(coords && coords.lat && coords.lon) { 18 | var href = 'https://www.openstreetmap.org/?mlat='+coords.lat+'&mlon='+coords.lon+'#map=19/'+coords.lat+'/'+coords.lon+'&layers=C'; 19 | $map_button.attr('href', href); 20 | } 21 | else { 22 | var href = $map_button.attr('href'); 23 | href = href.replace('http://maps.google.com/?q', 'https://www.openstreetmap.org/search?query'); 24 | $map_button.attr('href', href); 25 | } 26 | } 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /assets/map/leaflet-messagebox/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Martijn Grendelman 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of leaflet-messagebox nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | 29 | -------------------------------------------------------------------------------- /assets/map/leaflet-messagebox/README.md: -------------------------------------------------------------------------------- 1 | A Leaflet plugin to display a temporary message on a map 2 | ([Demo](https://www.grendelman.net/leaflet/)) 3 | 4 | # Leaflet.Messagebox 5 | 6 | Leaflet.Messagebox is a simple control to display a temporary message, like an 7 | error, on a [Leaflet](http://leafletjs.com/) map. The message is hidden after 8 | a configurable timeout. 9 | 10 | ## Using the Messagebox 11 | 12 | There a are two ways to add the messagebox to the map. First: 13 | 14 | var options = { timeout: 5000 } 15 | var box = L.control.messagebox(options).addTo(map); 16 | 17 | or, add it on map initialization: 18 | 19 | var map = L.map( 'mapdiv', {'messagebox': true, ...} ); 20 | map.messagebox.options.timeout = 5000; 21 | 22 | Then, show a message: 23 | 24 | box.show( 'This is the message' ); 25 | 26 | or, when implicitly used with the map: 27 | 28 | map.messagebox.show( 'This is the message' ); 29 | 30 | ## Available Options: 31 | 32 | There are only two options: 33 | 34 | `position:` (string) The standard Leaflet.Control position parameter. Optional, defaults to 'topright' 35 | 36 | `timeout:` (integer) The duration the messagebox is shown in milliseconds. Optional, defaults to 3000 (3 sec). 37 | 38 | ## Styling ## 39 | 40 | The messagebox can be styled with CSS, see [the css file]( leaflet-messagebox.css) for details. 41 | 42 | # License 43 | 44 | Leaflet.Messagebox is free software. Please see [LICENSE](LICENSE) for details. 45 | -------------------------------------------------------------------------------- /assets/map/leaflet-messagebox/leaflet-messagebox.css: -------------------------------------------------------------------------------- 1 | .leaflet-control-messagebox { 2 | display: none; /* Initially hidden */ 3 | border: 2px solid red; 4 | background-color: white; 5 | padding: 3px 10px; 6 | } 7 | -------------------------------------------------------------------------------- /assets/map/leaflet-messagebox/leaflet-messagebox.js: -------------------------------------------------------------------------------- 1 | L.Control.Messagebox = L.Control.extend({ 2 | options: { 3 | position: 'topright', 4 | timeout: 3000 5 | }, 6 | 7 | onAdd: function (map) { 8 | this._container = L.DomUtil.create('div', 'leaflet-control-messagebox'); 9 | //L.DomEvent.disableClickPropagation(this._container); 10 | return this._container; 11 | }, 12 | 13 | show: function (message, timeout) { 14 | var elem = this._container; 15 | elem.innerHTML = message; 16 | elem.style.display = 'block'; 17 | 18 | timeout = timeout || this.options.timeout; 19 | 20 | if (typeof this.timeoutID == 'number') { 21 | clearTimeout(this.timeoutID); 22 | } 23 | this.timeoutID = setTimeout(function () { 24 | elem.style.display = 'none'; 25 | }, timeout); 26 | } 27 | }); 28 | 29 | L.Map.mergeOptions({ 30 | messagebox: false 31 | }); 32 | 33 | L.Map.addInitHook(function () { 34 | if (this.options.messagebox) { 35 | this.messagebox = new L.Control.Messagebox(); 36 | this.addControl(this.messagebox); 37 | } 38 | }); 39 | 40 | L.control.messagebox = function (options) { 41 | return new L.Control.Messagebox(options); 42 | }; 43 | -------------------------------------------------------------------------------- /assets/packaged/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /assets/public/css/partials/forms.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | /*# sourceMappingURL=forms.css.map */ 4 | -------------------------------------------------------------------------------- /assets/public/css/partials/forms.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": "", 4 | "sources": [], 5 | "names": [], 6 | "file": "forms.css" 7 | } -------------------------------------------------------------------------------- /assets/public/css/themes/graphene.css: -------------------------------------------------------------------------------- 1 | #booking-form-container select { 2 | height: 40px; 3 | width: 30%; 4 | } 5 | #booking-form-container span.date { 6 | background-color: transparent; 7 | border: none; 8 | width: 20%; 9 | } -------------------------------------------------------------------------------- /assets/public/css/themes/graphene.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": ";AACI,8BAAO;EACH,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,GAAG;;;AAGd,iCAAU;EACN,gBAAgB,EAAE,WAAW;EAC7B,MAAM,EAAE,IAAI;EACZ,KAAK,EAAE,GAAG", 4 | "sources": ["../../sass/themes/graphene.scss"], 5 | "names": [], 6 | "file": "graphene.css" 7 | } -------------------------------------------------------------------------------- /assets/public/css/themes/twentynineteen.css: -------------------------------------------------------------------------------- 1 | .cb-wrapper { 2 | /* Twenty Nineteen Theme*/ 3 | } 4 | .cb-wrapper h2::before { 5 | content: ""; 6 | margin: 0; 7 | background: transparent; 8 | } -------------------------------------------------------------------------------- /assets/public/css/themes/twentynineteen.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": ";AAAA,WAAY;EACR,0BAA0B;;;AAC1B,sBAAW;EACP,OAAO,EAAE,EAAE;EACX,MAAM,EAAE,CAAC;EACT,UAAU,EAAE,WAAW", 4 | "sources": ["../../sass/themes/twentynineteen.scss"], 5 | "names": [], 6 | "file": "twentynineteen.css" 7 | } -------------------------------------------------------------------------------- /assets/public/css/themes/twentytwenty.css: -------------------------------------------------------------------------------- 1 | .cb-wrapper { 2 | /* Twenty Twenty Theme*/ 3 | } 4 | .cb-wrapper h1, .cb-wrapper h2, .cb-wrapper h3, .cb-wrapper h4 { 5 | margin: 0 !important; 6 | } 7 | 8 | .litepicker .container__days .day-item { 9 | font-size: 2.2rem; 10 | } -------------------------------------------------------------------------------- /assets/public/css/themes/twentytwenty.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "mappings": ";AAAA,WAAY;EACR,wBAAwB;;;AACxB,8DAAe;EACX,MAAM,EAAE,YAAY;;;;AAOhB,sCAAU;EACN,SAAS,EAAE,MAAM", 4 | "sources": ["../../sass/themes/twentytwenty.scss"], 5 | "names": [], 6 | "file": "twentytwenty.css" 7 | } -------------------------------------------------------------------------------- /assets/public/js/index.php: -------------------------------------------------------------------------------- 1 | 8 | * @license GPL-2.0+ 9 | * @since 2.0 10 | * @link http://www.wielebenwir.de 11 | * @copyright 2020 wielebenwir 12 | */ 13 | 14 | @use "../../../global/sass/partials/extends"; 15 | @use "layouts"; 16 | 17 | /* main wrapper */ 18 | .cb-wrapper { 19 | 20 | position: relative; 21 | clear: both; 22 | display: block; 23 | margin-top: var(--commonsbooking-spacer-big); 24 | color: var(--commonsbooking-textcolor-dark); 25 | @extend .cb-box ; 26 | 27 | .cb-list-content.cb-pickupinstructions div { 28 | white-space: pre-wrap; // enable line break in content from meta textfields 29 | } 30 | 31 | .cb-address.cb-location-pickupinstructions { 32 | white-space: pre-wrap; // enable line break in content from meta textfields 33 | margin-top: 0; 34 | border-left: green; 35 | border-width: 1px; 36 | } 37 | 38 | /* headline */ 39 | h1, h2, h3, h4 { 40 | margin: 0; 41 | padding-bottom: var(--commonsbooking-spacer); 42 | padding-top: 0; 43 | } 44 | 45 | .cb-title { 46 | color: var(--commonsbooking-color-primary); 47 | } 48 | 49 | .cb-location-title a{ 50 | color: var(--commonsbooking-color-primary); 51 | text-decoration: none; 52 | } 53 | 54 | .cb-item-title a{ 55 | color: var(--commonsbooking-color-primary); 56 | text-decoration: none; 57 | } 58 | 59 | /* sub lists wrapper */ 60 | > .cb-wrapper { 61 | overflow: hidden; 62 | @extend .cb-box-inner ; 63 | 64 | 65 | > div { /* sub list items */ 66 | padding: var(--commonsbooking-spacer-small) 0; 67 | &:last-of-type { 68 | border-bottom: none; 69 | } 70 | } 71 | } 72 | 73 | .template-timeframe-withitem, 74 | .template-timeframe-withlocation, 75 | .template-item-withlocation 76 | { 77 | @extend .cb-col-auto ; 78 | } 79 | .cb-list-header { 80 | @extend .cb-col-auto ; 81 | } 82 | .cb-list-header img{ 83 | 84 | } 85 | .cb-action { 86 | background: transparent; 87 | .cb-button { 88 | display: inline-block; 89 | } 90 | } 91 | 92 | } 93 | 94 | -------------------------------------------------------------------------------- /assets/public/sass/partials/_notice.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Notices 3 | * 4 | * Block-Level 5 | * inline 6 | * 7 | */ 8 | 9 | @use "../../../global/sass/partials/extends"; 10 | 11 | .cb-wrapper { 12 | .cb-notice { 13 | @extend .cb-box; 14 | margin-bottom: var(--commonsbooking-spacer-big); 15 | font-size: var(--commonsbooking-font-size-big); 16 | vertical-align: middle; 17 | font-weight: bold; 18 | position: relative; 19 | background: var(--commonsbooking-color-noticebg); 20 | 21 | /* Post status */ 22 | &.cb-status-unconfirmed { 23 | background: var(--commonsbooking-color-noticebg); 24 | } 25 | &.cb-status-confirmed { 26 | background: var(--commonsbooking-color-success); 27 | } 28 | &.cb-status-cancelled { 29 | background: var(--commonsbooking-color-success); 30 | } 31 | &.error { 32 | background: var(--commonsbooking-color-error); 33 | color: var(--commonsbooking-textcolor-light); 34 | } 35 | } 36 | .cb-notice-small { 37 | display: block; 38 | &.cb-status-not-available { 39 | color: var(--commonsbooking-color-error); 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /assets/public/sass/partials/_single.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Single item, single location, single booking view 3 | * 4 | */ 5 | 6 | @use "../../../global/sass/partials/extends"; 7 | 8 | .cb-wrapper { 9 | /* invert the color scheme from the lists: wrapper has no bg, sub-wrapper colored */ 10 | &.template-location-single, 11 | &.template-item-single, 12 | &.template-booking-single { 13 | background: transparent; 14 | padding: 0; 15 | > div:not(.cb-notice) { 16 | @extend .cb-box; 17 | > div { /* sub list items */ 18 | &:last-of-type { 19 | border-bottom: 0; 20 | } 21 | } 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /assets/public/sass/partials/_table.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wielebenwir/commonsbooking/2f394da297c7d6394d451068f92a12beb82b6fa3/assets/public/sass/partials/_table.scss -------------------------------------------------------------------------------- /assets/public/sass/partials/_themeoptimization.scss: -------------------------------------------------------------------------------- 1 | /** 2 | * Optimisation & Compatibility 3 | * 4 | * Tweaks to make CB play nice with popular Wordpress themes. 5 | * 6 | */ 7 | 8 | .cb-wrapper { 9 | /* Twenty Nineteen Theme*/ 10 | h2::before { 11 | content: ''; 12 | margin: 0; 13 | background: transparent; 14 | } 15 | /* Twenty Twenty Theme*/ 16 | h1, h2, h3, h4 { 17 | margin: 0 !important; 18 | } 19 | 20 | .litepicker { 21 | .container { 22 | &__days { 23 | .day-item { 24 | font-size: 1rem; 25 | } 26 | } 27 | } 28 | } 29 | } 30 | 31 | /* Enable table scrolling in themes (e.g. twentyfifteen) */ 32 | table.cb-items-table { 33 | table-layout: auto !important; 34 | } 35 | -------------------------------------------------------------------------------- /assets/public/sass/public.scss: -------------------------------------------------------------------------------- 1 | /* 2 | * This stylesheet is used to style the public-facing components of the plugin 3 | */ 4 | 5 | /* Mixins */ 6 | @use "mixins/_calendar" as calendarmixins; 7 | 8 | /* Global styles */ 9 | @use "../../global/sass/global"; 10 | 11 | /* Multi-column layouts */ 12 | @use "partials/_layouts"; 13 | 14 | /* List styles */ 15 | @use "partials/_lists"; 16 | 17 | /* List styles */ 18 | @use "partials/_single"; 19 | 20 | /* Form styles */ 21 | @use "partials/_forms"; 22 | 23 | /* Calendar styles */ 24 | @use "partials/_calendar"; 25 | 26 | /* Map styles */ 27 | @use "partials/_map"; 28 | 29 | /* Booking list styles */ 30 | @use "partials/_bookings"; 31 | 32 | /* Notices and restrictions styles */ 33 | @use "partials/_notice"; 34 | 35 | /* Styles for items table */ 36 | @use "partials/_shortcodeItemsTable"; 37 | 38 | /* Fixes for specific WP Themes */ 39 | @use "partials/_themeoptimization"; 40 | -------------------------------------------------------------------------------- /assets/public/sass/themes/graphene.scss: -------------------------------------------------------------------------------- 1 | #booking-form-container { 2 | select { 3 | height: 40px; 4 | width: 30%; 5 | } 6 | 7 | span.date { 8 | background-color: transparent; 9 | border: none; 10 | width: 20%; 11 | } 12 | } -------------------------------------------------------------------------------- /assets/public/sass/themes/twentynineteen.scss: -------------------------------------------------------------------------------- 1 | .cb-wrapper { 2 | /* Twenty Nineteen Theme*/ 3 | h2::before { 4 | content: ''; 5 | margin: 0; 6 | background: transparent; 7 | } 8 | } -------------------------------------------------------------------------------- /assets/public/sass/themes/twentytwenty.scss: -------------------------------------------------------------------------------- 1 | .cb-wrapper { 2 | /* Twenty Twenty Theme*/ 3 | h1, h2, h3, h4 { 4 | margin: 0 !important; 5 | } 6 | } 7 | 8 | .litepicker { 9 | .container { 10 | &__days { 11 | .day-item { 12 | font-size: 2.2rem; 13 | } 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /bin/build-zip.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Borrows from https://github.com/woocommerce/woocommerce/blob/trunk/plugins/woocommerce/bin/build-zip.sh 3 | 4 | # stop execution if any command exits with non-zero status 5 | set -e 6 | # treat use of unset variables as error (use "${var:-}" to default to empty string) 7 | set -u 8 | 9 | 10 | PLUGIN_SLUG="commonsbooking" 11 | PROJECT_PATH=$(pwd) 12 | BUILD_PATH="${PROJECT_PATH}/build" 13 | DEST_PATH="$BUILD_PATH/$PLUGIN_SLUG" 14 | SKIP_ZIP=0 15 | 16 | 17 | # Parse command line options 18 | while [ "$#" -gt 0 ]; do 19 | case "$1" in 20 | --skip-zip) 21 | SKIP_ZIP=1 22 | shift 23 | ;; 24 | *) 25 | echo "Invalid option: $1" 26 | exit 1 27 | ;; 28 | esac 29 | done 30 | 31 | echo "Generating build directory..." 32 | rm -rf "$BUILD_PATH" 33 | mkdir -p "$DEST_PATH" 34 | 35 | echo "Installing PHP and JS dependencies..." 36 | npm ci 37 | echo "Running JS Build..." 38 | npm run dist 39 | echo "Cleaning up PHP dependencies..." 40 | composer install --no-dev --ignore-platform-reqs 41 | echo "Syncing files..." 42 | rsync -rc --exclude-from="$PROJECT_PATH/.distignore" "$PROJECT_PATH/" "$DEST_PATH/" --delete --delete-excluded 43 | 44 | if [ "$SKIP_ZIP" -eq 1 ]; then 45 | echo "Build done! (Skipped zip file generation)" 46 | exit 0 47 | fi 48 | 49 | echo "Generating zip file..." 50 | cd "$BUILD_PATH" 51 | zip -q -r "${PLUGIN_SLUG}.zip" "$PLUGIN_SLUG/" 52 | 53 | cd "$PROJECT_PATH" 54 | mv "$BUILD_PATH/${PLUGIN_SLUG}.zip" "$PROJECT_PATH" 55 | echo "${PLUGIN_SLUG}.zip file generated!" 56 | 57 | echo "Build done!" 58 | -------------------------------------------------------------------------------- /bin/setup-cypress-env.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Install our example posts from a WP export file 4 | wp-env run tests-cli wp import /var/www/html/wp-content/plugins/commonsbooking/tests/cypress/wordpress-files/content-example.xml --authors=create 5 | # Switch to Kasimir theme 6 | wp-env run tests-cli wp theme activate kasimir-theme 7 | # Create subscriber with username "subscriber" and password "password" 8 | wp-env run tests-cli wp user create subscriber sub@sub.de --role=subscriber --user_pass=password -------------------------------------------------------------------------------- /bin/update-pot.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | #This script will generate a new .pot file and replace it with the old one when there are changes 3 | #We do this so that we don't spam our commit log when the only thing that has changed is the POT-Creation-Date 4 | #Please run this script directly from the plugin folder like so: bin/update-pot.sh 5 | wp i18n make-pot . languages/commonsbooking.pot.new 6 | #now, only move the .pot.new file over, when something else than the creation date has changed 7 | if diff -I '^"POT-Creation-Date:' languages/commonsbooking.pot languages/commonsbooking.pot.new >/dev/null 2>&1; then 8 | rm languages/commonsbooking.pot.new 9 | echo "no changes detected" 10 | else 11 | mv languages/commonsbooking.pot.new languages/commonsbooking.pot 12 | echo "updating po file from change .pot" 13 | wp i18n update-po languages/commonsbooking.pot languages/commonsbooking-de_DE.po 14 | echo "updated translation file" 15 | fi 16 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | informational: true 6 | target: 80% 7 | patch: 8 | default: 9 | informational: true 10 | target: 80% 11 | 12 | # As long as github checks deform the `commits` view (for individual commits) in pull_request, we disable this 13 | github_checks: false 14 | -------------------------------------------------------------------------------- /commonsbooking.php: -------------------------------------------------------------------------------- 1 | 'Version' ), false ) ); 35 | 36 | global $cb_db_version; 37 | $cb_db_version = '1.0'; 38 | 39 | require __DIR__ . '/includes/Admin.php'; 40 | require __DIR__ . '/includes/Public.php'; 41 | require __DIR__ . '/vendor/autoload.php'; 42 | require __DIR__ . '/vendor/cmb2/cmb2/init.php'; 43 | require __DIR__ . '/vendor/mustardBees/cmb-field-select2/cmb-field-select2.php'; 44 | require __DIR__ . '/vendor/ed-itsolutions/cmb2-field-ajax-search/cmb2-field-ajax-search.php'; 45 | require_once __DIR__ . '/includes/Plugin.php'; 46 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wielebenwir/commonsbooking", 3 | "description": "~", 4 | "version": "0.0.1", 5 | "config": { 6 | "platform-check": false, 7 | "allow-plugins": { 8 | "php-http/discovery": true, 9 | "dealerdirect/phpcodesniffer-composer-installer": true, 10 | "phpstan/extension-installer": true 11 | } 12 | }, 13 | "autoload": { 14 | "psr-4": { 15 | "CommonsBooking\\": "src/" 16 | } 17 | }, 18 | "autoload-dev": { 19 | "psr-4": { 20 | "CommonsBooking\\Tests\\": "tests/php/" 21 | } 22 | }, 23 | "repositories": [ 24 | { 25 | "type": "vcs", 26 | "url": "https://github.com/wielebenwir/cmb-field-select2" 27 | }, 28 | { 29 | "type": "vcs", 30 | "url": "https://github.com/wielebenwir/cmb2-field-ajax-search" 31 | } 32 | 33 | ], 34 | "require": { 35 | "php": ">=7.4", 36 | "cmb2/cmb2": "^2.10.1", 37 | "mustardbees/cmb-field-select2": "^3.0.4", 38 | "geocoder-php/nominatim-provider": "^5.7", 39 | "php-http/curl-client": "^2.3", 40 | "nyholm/psr7": "^1.8", 41 | "opis/json-schema": "^2.3.0", 42 | "scssphp/scssphp": "^1.11.1", 43 | "ext-json": "*", 44 | "ext-curl": "*", 45 | "symfony/cache": "^5.4.30", 46 | "symfony/service-contracts": "^v2.5.2", 47 | "eluceo/ical": "~2.14.0", 48 | "phpmailer/phpmailer": "^6.8.1", 49 | "ed-itsolutions/cmb2-field-ajax-search": "v2.0.1" 50 | }, 51 | "require-dev": { 52 | "mockery/mockery": "^1.6.6", 53 | "phpunit/phpunit": "^9.6.21", 54 | "yoast/phpunit-polyfills": "^4.0.0", 55 | "wp-coding-standards/wpcs": "^3.1", 56 | "phpcompatibility/phpcompatibility-wp": "^2.1.4", 57 | "slope-it/clock-mock": "^0.4.0", 58 | "phpunit/php-code-coverage": "^9.2.32", 59 | "myclabs/deep-copy": "^1.12.0", 60 | "phpstan/phpstan": "^2.1", 61 | "szepeviktor/phpstan-wordpress": "^2.0", 62 | "phpstan/extension-installer": "^1.4", 63 | "ext-libxml": "*", 64 | "ext-dom": "*" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /includes/Plugin.php: -------------------------------------------------------------------------------- 1 | init(); 22 | $cbPlugin->initRoutes(); 23 | $cbPlugin->initBookingcodes(); 24 | -------------------------------------------------------------------------------- /includes/Shortcodes.php: -------------------------------------------------------------------------------- 1 | '', 7 | ), 8 | $atts, 9 | 'cb' 10 | ); 11 | 12 | echo commonsbooking_sanitizeHTML( commonsbooking_parse_shortcode( $atts['tag'] ) ); 13 | } 14 | 15 | add_shortcode( 'cb', 'commonsbooking_tag' ); 16 | -------------------------------------------------------------------------------- /includes/commons-api-json-schema/commons-api.availability.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema#", 3 | "$id": "https://github.com/wielebenwir/commons-api/blob/master/commons-api.availability.schema.json", 4 | "title": "Commons Api Availability Schema", 5 | "description": "A schema for availability slots of the Commons API", 6 | "definitions": { 7 | "availabilitySlot": { 8 | "type": "object", 9 | "description": "A time slot at which an item can be booked at a certain location.", 10 | "properties": { 11 | "start": { 12 | "type": "string", 13 | "format": "date-time" 14 | }, 15 | "end": { 16 | "type": "string", 17 | "format": "date-time" 18 | }, 19 | "locationId": { 20 | "type": "string" 21 | }, 22 | "itemId": { 23 | "type": "string" 24 | } 25 | }, 26 | "required": ["start", "end", "locationId", "itemId"] 27 | } 28 | }, 29 | "type": "object", 30 | "properties": { 31 | "availability": { 32 | "type": "array", 33 | "descriptions": "An array with the availability slots for bookable items.", 34 | "items": { "$ref": "#/definitions/availabilitySlot" }, 35 | "uniqueItems": true 36 | } 37 | }, 38 | "required": ["availability"] 39 | } 40 | -------------------------------------------------------------------------------- /includes/commons-api-json-schema/commons-api.locations.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema#", 3 | "$id": "https://github.com/wielebenwir/commons-api/blob/master/commons-api.locations.schema.json", 4 | "title": "Commons Api Locations Schema", 5 | "description": "A schema for item locations of the Commons API", 6 | "definitions": { 7 | "location": { 8 | "type": "object", 9 | "description": "A GeoJson feature describing a location at which users can pick up items.", 10 | "properties": { 11 | "type": { "type": "string", "enum": ["Feature"] }, 12 | "properties": { 13 | "type": "object", 14 | "description": "The properties of the location", 15 | "properties": { 16 | "id": { "type": "string" }, 17 | "name": { "type": "string" }, 18 | "description": { "type": "string" }, 19 | "url": { 20 | "type": "string", 21 | "format": "uri" 22 | }, 23 | "address": { "type": "string" } 24 | }, 25 | "required": ["name", "id"] 26 | }, 27 | "geometry": { 28 | "type": "object", 29 | "description": "A GeoJson point, containing the coordinates of the location", 30 | "required": ["type", "coordinates"], 31 | "properties": { 32 | "type": { "type": "string", "enum": ["Point"] }, 33 | "coordinates": { 34 | "type": "array", 35 | "minItems": 2, 36 | "items": { 37 | "type": "number" 38 | } 39 | } 40 | } 41 | } 42 | }, 43 | "required": ["type", "properties", "geometry"] 44 | } 45 | }, 46 | "type": "object", 47 | "properties": { 48 | "locations": { 49 | "type": "object", 50 | "description": "A GeoJson feature collection of the locations at which users can pick up items.", 51 | "required": ["type", "features"], 52 | "properties": { 53 | "type": { "type": "string", "enum": ["FeatureCollection"] }, 54 | "features": { 55 | "type": "array", 56 | "items": { "$ref": "#/definitions/location" }, 57 | "uniqueItems": true 58 | } 59 | } 60 | } 61 | }, 62 | "required": ["locations"] 63 | } 64 | -------------------------------------------------------------------------------- /includes/commons-api-json-schema/commons-api.owners.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema#", 3 | "$id": "https://github.com/wielebenwir/commons-api/blob/master/commons-api.owners.schema.json", 4 | "title": "Commons Api Owners Schema", 5 | "description": "A schema for item owners of the Commons API", 6 | "definitions": { 7 | "owner": { 8 | "type": "object", 9 | "properties": { 10 | "id": { "type": "string" }, 11 | "name": { 12 | "type": "string" 13 | }, 14 | "url": { 15 | "type": "string", 16 | "format": "uri" 17 | } 18 | }, 19 | "required": ["name", "id"] 20 | } 21 | }, 22 | "type": "object", 23 | "properties": { 24 | "owners": { 25 | "type": "array", 26 | "descriptions": "An array with the item owners as values.", 27 | "items": { "$ref": "#/definitions/owner" }, 28 | "uniqueItems": true 29 | } 30 | }, 31 | "required": ["locations"] 32 | } 33 | -------------------------------------------------------------------------------- /includes/commons-api-json-schema/commons-api.projects.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema#", 3 | "$id": "https://github.com/wielebenwir/commons-api/blob/master/commons-api.projects.schema.json", 4 | "title": "Commons Api Projects Schema", 5 | "description": "A schema for projects of the Commons API", 6 | "definitions": { 7 | "project": { 8 | "type": "object", 9 | "description": "The project that organizes the lending of the items", 10 | "properties": { 11 | "id": { "type": "string" }, 12 | "name": { "type": "string" }, 13 | "url": { 14 | "type": "string", 15 | "format": "uri" 16 | }, 17 | "description": { "type": "string" }, 18 | "image": { "type": "string", "format": "uri" }, 19 | "language": { "type": "string" } 20 | }, 21 | "required": ["id", "name", "url"] 22 | } 23 | }, 24 | "type": "object", 25 | "properties": { 26 | "projects": { 27 | "type": "array", 28 | "descriptions": "An array with the item projects as values.", 29 | "items": { "$ref": "#/definitions/project" }, 30 | "uniqueItems": true 31 | } 32 | }, 33 | "required": ["projects"] 34 | } 35 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | ", 6 | "license": "SEE LICENSE IN LICENSE.txt", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/wielebenwir/cb" 10 | }, 11 | "devDependencies": { 12 | "@babel/preset-env": "^7.26.9", 13 | "@wordpress/env": "^10.22.0", 14 | "commons-api": "git+https://github.com/wielebenwir/commons-api.git", 15 | "cypress": "^14.3.1", 16 | "editorconfig": "^2.0.1", 17 | "grunt": "^1.6.1", 18 | "grunt-babel": "^8.0.0", 19 | "grunt-cli": "^1.5.0", 20 | "grunt-contrib-concat": "^2.1.0", 21 | "grunt-contrib-copy": "^1.0.0", 22 | "grunt-contrib-jshint": "^3.2.0", 23 | "grunt-contrib-nodeunit": "^5.0.0", 24 | "grunt-contrib-uglify": "^5.2.2", 25 | "grunt-contrib-watch": "^1.1.0", 26 | "grunt-dart-sass": "^2.0.1", 27 | "matchdep": "^2.0.0", 28 | "sass": "^1.85.1" 29 | }, 30 | "scripts": { 31 | "start": "composer install --ignore-platform-reqs && npm install && npm run dist", 32 | "env": "wp-env", 33 | "env:start": "wp-env start", 34 | "env:stop": "wp-env stop", 35 | "cypress:setup": "./bin/setup-cypress-env.sh", 36 | "cypress:open": "cypress open --config-file tests/cypress/cypress.config.js", 37 | "cypress:run": "cypress run --config-file tests/cypress/cypress.config.js", 38 | "dist": "grunt dist" 39 | }, 40 | "dependencies": { 41 | "@commonsbooking/frontend": "^0.1.0-beta.7", 42 | "feiertagejs": "^1.4.1", 43 | "leaflet": "^1.7.1", 44 | "leaflet-easybutton": "^2.4.0", 45 | "leaflet-spin": "^1.1.2", 46 | "leaflet.markercluster": "^1.5.0", 47 | "shufflejs": "^6.1.1", 48 | "spin.js": "^2.3.2", 49 | "vue": "^3.5.13" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /phpstan.neon: -------------------------------------------------------------------------------- 1 | includes: 2 | - phpstan-baseline.neon 3 | parameters: 4 | phpVersion: 70400 5 | scanFiles: 6 | - commonsbooking.php 7 | scanDirectories: 8 | - vendor/cmb2/cmb2/includes 9 | paths: 10 | - commonsbooking.php 11 | - src/ 12 | - includes/ 13 | level: 4 14 | ignoreErrors: 15 | - '#Constant (COMMONSBOOKING.*|WP_DEBUG_LOG) not found.#' 16 | # - '#Instantiated class (CommonsBooking.*CB_Data) not found.#' 17 | - '#Function cb_object_to_array not found.#' 18 | - 19 | identifier: requireOnce.fileNotFound # https://github.com/szepeviktor/phpstan-wordpress/issues/239 20 | # As long as symfony exceptions do not extend from throwable 21 | - '#Psr\\Cache\\InvalidArgumentException is not subtype of Throwable#' 22 | - '#Psr\\Cache\\CacheException is not subtype of Throwable#' 23 | # Maybe this will be useful in the next phpstan level (because we have a very unusual way of dealing with psr/cache exceptions, bc we throw them all the way up, but the seem to be unchecked exceptions) 24 | # exceptions: 25 | # uncheckedExceptionRegexes: 26 | # - '#Psr\Cache\#' 27 | # - '#CacheException#' 28 | # uncheckedExceptionClasses: 29 | # - 'CacheException' 30 | # - 'Psr\Cache\CacheException' 31 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | tests/php 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | ./src 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /screenshots/example_0.5.9_de.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wielebenwir/commonsbooking/2f394da297c7d6394d451068f92a12beb82b6fa3/screenshots/example_0.5.9_de.png -------------------------------------------------------------------------------- /src/API/GBFS/BaseRoute.php: -------------------------------------------------------------------------------- 1 | data = new stdClass(); 31 | $data->data->stations = $this->getItemData( $request ); 32 | $data->last_updated = time(); 33 | $data->ttl = 60; 34 | $data->version = '2.3'; 35 | 36 | if ( WP_DEBUG ) { 37 | $this->validateData( $data ); 38 | } 39 | 40 | return new WP_REST_Response( $data, 200 ); 41 | } 42 | 43 | /** 44 | * Returns item data array. 45 | * 46 | * @param $request 47 | * 48 | * @return array 49 | */ 50 | public function getItemData( $request ): array { 51 | $data = []; 52 | $locations = Location::get(); 53 | 54 | foreach ( $locations as $location ) { 55 | try { 56 | $itemdata = $this->prepare_item_for_response( $location, $request ); 57 | $data[] = $itemdata; 58 | } catch ( Exception $exception ) { 59 | if ( WP_DEBUG ) { 60 | error_log( $exception->getMessage() ); 61 | } 62 | } 63 | } 64 | 65 | return $data; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/API/GBFS/Discovery.php: -------------------------------------------------------------------------------- 1 | get_feed( 'system_information' ); 39 | $feeds[] = $this->get_feed( 'station_information' ); 40 | $feeds[] = $this->get_feed( 'station_status' ); 41 | 42 | $lang = get_bloginfo( 'language' ); 43 | $data = new stdClass(); 44 | $data->data = new stdClass(); 45 | $data->data->$lang = new stdClass(); 46 | $data->data->$lang->feeds = $feeds; 47 | $data->last_updated = current_time( 'timestamp' ); 48 | $data->ttl = 86400; 49 | $data->version = '2.3'; 50 | 51 | if ( WP_DEBUG ) { 52 | $this->validateData( $data ); 53 | } 54 | 55 | return new WP_REST_Response( $data, 200 ); 56 | } 57 | 58 | private function get_feed( $name ): stdClass { 59 | $feed = new stdClass(); 60 | $feed->name = $name; 61 | $feed->url = get_rest_url() . 'commonsbooking/v1/' . $name . '.json'; 62 | return $feed; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/API/GBFS/SystemInformation.php: -------------------------------------------------------------------------------- 1 | data = new stdClass(); 33 | $data->data->name = get_bloginfo( 'name' ); 34 | $data->data->system_id = sha1( site_url() ); 35 | $data->data->language = get_bloginfo( 'language' ); 36 | $data->data->timezone = $tz; 37 | $data->last_updated = current_time( 'timestamp' ); 38 | $data->ttl = 86400; 39 | $data->version = '2.3'; 40 | 41 | if ( WP_DEBUG ) { 42 | $this->validateData( $data ); 43 | } 44 | 45 | return new WP_REST_Response( $data, 200 ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/API/ProjectsRoute.php: -------------------------------------------------------------------------------- 1 | get_items( $request ); 36 | } 37 | 38 | /** 39 | * Get a collection of projects 40 | */ 41 | public function get_items( $request ): WP_REST_Response { 42 | $data = new stdClass(); 43 | $data->projects = $this->getItemData(); 44 | 45 | if ( WP_DEBUG ) { 46 | $this->validateData( $data ); 47 | } 48 | 49 | return new WP_REST_Response( $data, 200 ); 50 | } 51 | 52 | /** 53 | * Returns raw data collection. 54 | * 55 | * @return object[] 56 | */ 57 | public function getItemData(): array { 58 | return [ 59 | (object) [ 60 | 'id' => '1', 61 | 'name' => get_bloginfo( 'name' ), 62 | 'url' => get_bloginfo( 'url' ), 63 | 'description' => get_bloginfo( 'description' ), 64 | 'language' => get_bloginfo( 'language' ), 65 | ], 66 | ]; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/API/Share.php: -------------------------------------------------------------------------------- 1 | name = $name; 36 | $this->enabled = $enabled === 'on'; 37 | $this->pushUrl = $pushUrl; 38 | $this->key = $key; 39 | $this->owner = $owner; 40 | } 41 | 42 | /** 43 | * @return mixed 44 | */ 45 | public function getName() { 46 | return $this->name; 47 | } 48 | 49 | /** 50 | * @return mixed 51 | */ 52 | public function isEnabled() { 53 | return $this->enabled; 54 | } 55 | 56 | /** 57 | * @return mixed 58 | */ 59 | public function getPushUrl() { 60 | return $this->pushUrl; 61 | } 62 | 63 | /** 64 | * @return mixed 65 | */ 66 | public function getKey() { 67 | return $this->key; 68 | } 69 | 70 | /** 71 | * @return mixed 72 | */ 73 | public function getOwner() { 74 | return $this->owner; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Exception/BookingCodeException.php: -------------------------------------------------------------------------------- 1 | redirectUrl = $redirectUrl; 16 | } 17 | 18 | /** 19 | * Will get the URL to redirect the user to after a booking is denied. 20 | * It is the referrer by default 21 | * 22 | * @return string 23 | */ 24 | public function getRedirectUrl(): string { 25 | if ( $this->redirectUrl ) { 26 | return $this->redirectUrl; 27 | } else { 28 | return sanitize_url( wp_get_referer() ); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Exception/BookingRuleException.php: -------------------------------------------------------------------------------- 1 | getPushUrl() ) { 20 | self::triggerPushUrl( $apiShare ); 21 | } 22 | } 23 | } 24 | 25 | /** 26 | * Makes a post request with api-key and owner to the configured push url. 27 | * 28 | * @param Share $share 29 | */ 30 | public static function triggerPushUrl( Share $share ) { 31 | $requestData = [ 32 | 'API_KEY' => $share->getKey(), 33 | 'OWNER' => $share->getOwner(), 34 | ]; 35 | wp_remote_post( $share->getPushUrl(), $requestData ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Helper/GeoCodeService.php: -------------------------------------------------------------------------------- 1 | getAddressData( $addressString ); 28 | } 29 | 30 | /** 31 | * Configure the service implementation in use 32 | * 33 | * @param GeoCodeService $instance 34 | * 35 | * @return void 36 | */ 37 | public static function setGeoCodeServiceInstance( GeoCodeService $instance ): void { 38 | self::$geoCodeService = $instance; 39 | } 40 | 41 | public static function resetGeoCoder(): void { 42 | self::setGeoCodeServiceInstance( new NominatimGeoCodeService() ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Helper/NominatimGeoCodeService.php: -------------------------------------------------------------------------------- 1 | 0, 38 | CURLOPT_SSL_VERIFYPEER => 0, 39 | ] 40 | ); 41 | 42 | $provider = Nominatim::withOpenStreetMapServer( 43 | $client, 44 | array_key_exists( 'HTTP_USER_AGENT', $_SERVER ) ? $_SERVER['HTTP_USER_AGENT'] : $defaultUserAgent 45 | ); 46 | $geoCoder = new StatefulGeocoder( $provider, 'en' ); 47 | 48 | try { 49 | $addresses = $geoCoder->geocodeQuery( GeocodeQuery::create( $addressString ) ); 50 | if ( ! $addresses->isEmpty() ) { 51 | return $addresses->first(); 52 | } 53 | } catch ( Exception $exception ) { 54 | // Nothing to do in this case 55 | } 56 | 57 | return null; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Map/BaseShortcode.php: -------------------------------------------------------------------------------- 1 | parse_attributes( $atts ); 23 | $options = array_filter( $atts, 'is_int', ARRAY_FILTER_USE_KEY ); 24 | 25 | if ( ! (int) $attrs['id'] ) { 26 | return '
' . esc_html__( 'no valid map id provided', 'commonsbooking' ) . '
'; 27 | } 28 | 29 | $post = get_post( $attrs['id'] ); 30 | 31 | if ( ! ( $post && $post->post_type == 'cb_map' ) ) { 32 | return '
' . esc_html__( 'no valid map id provided', 'commonsbooking' ) . '
'; 33 | } 34 | 35 | if ( $post->post_status != 'publish' ) { 36 | return '
' . esc_html__( 'map is not published', 'commonsbooking' ) . '
'; 37 | } 38 | 39 | $cb_map_id = $post->ID; 40 | $instance->inject_script( $cb_map_id ); 41 | return $instance->create_container( $cb_map_id, $attrs, $options, $content ); 42 | } 43 | abstract protected function parse_attributes( $atts ); 44 | abstract protected function inject_script( $cb_map_id ); 45 | abstract protected function create_container( $cb_map_id, $attrs, $options, $content ); 46 | } 47 | -------------------------------------------------------------------------------- /src/Map/LocationMapAdmin.php: -------------------------------------------------------------------------------- 1 | term_id, $group ) ) { 13 | ++$valid_groups_count; 14 | break; 15 | } 16 | } 17 | } 18 | 19 | return $valid_groups_count == count( $category_groups ); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Map/SearchShortcode.php: -------------------------------------------------------------------------------- 1 | null, 15 | 'layouts' => 'Filter,MapWithAutoSidebar', 16 | ], 17 | $atts 18 | ); 19 | } 20 | 21 | protected function inject_script( $cb_map_id ) { 22 | wp_enqueue_style( 'cb-commons-search' ); 23 | wp_enqueue_script( 'cb-commons-search' ); 24 | } 25 | 26 | protected function create_container( $cb_map_id, $attrs, $options, $content ) { 27 | // Ensure that the api and config object are only created once per page and per map 28 | if ( ! in_array( $cb_map_id, $this->processed_map_ids ) ) { 29 | $settings = MapData::get_settings( $cb_map_id ); 30 | $admin_ajax_url = wp_json_encode( pop_key( $settings, 'data_url' ) ); 31 | $nonce = wp_json_encode( pop_key( $settings, 'nonce' ) ); 32 | $data_loader = trim( 33 | ' 34 | const config = CommonsSearch.parseLegacyConfig(' . wp_json_encode( $settings ) . "); 35 | const api = CommonsSearch.createAdminAjaxAPI({ 36 | url: $admin_ajax_url, 37 | nonce: $nonce, 38 | mapId: $cb_map_id, 39 | }, config); 40 | if (!window.__CB_SEARCH_DATA) window.__CB_SEARCH_DATA = {}; 41 | window.__CB_SEARCH_DATA[$cb_map_id] = { config, api }; 42 | " 43 | ); 44 | $this->processed_map_ids[] = $cb_map_id; 45 | } else { 46 | $data_loader = "const { config, api } = window.__CB_SEARCH_DATA[$cb_map_id];"; 47 | } 48 | 49 | $content = trim( strip_tags( $content ) ); 50 | $content_is_config = $content && is_object( json_decode( $content ) ) && json_last_error() == JSON_ERROR_NONE; 51 | if ( $content_is_config ) { 52 | $user_config = "const userConfig = $content;"; 53 | } else { 54 | $user_config = 'const userConfig = {};'; 55 | } 56 | 57 | $layout_config = wp_json_encode( 58 | array( 59 | 'types' => array_map( 'trim', explode( ',', $attrs['layouts'] ) ), 60 | 'options' => $options, 61 | ) 62 | ); 63 | 64 | $init_script = "(function (el) { 65 | document.addEventListener('DOMContentLoaded', function() { 66 | $data_loader 67 | $user_config 68 | CommonsSearch.init(el, api, CommonsSearch.mergeConfigs(config, { layout: $layout_config, ...userConfig })); 69 | }); 70 | })(document.currentScript.parentElement)"; 71 | 72 | return "
"; 73 | } 74 | } 75 | 76 | function pop_key( &$array, $key ) { 77 | $value = $array[ $key ]; 78 | unset( $array[ $key ] ); 79 | return $value; 80 | } 81 | -------------------------------------------------------------------------------- /src/Messages/AdminMessage.php: -------------------------------------------------------------------------------- 1 | message = $message; 26 | $this->notice_type = $notice_type; 27 | 28 | add_action( 'admin_notices', array( $this, 'render' ) ); 29 | } 30 | 31 | 32 | /** 33 | * renders an admin message 34 | * 35 | * @return void 36 | */ 37 | public function render() { 38 | 39 | echo '
'; 40 | echo '

'; 41 | echo commonsbooking_sanitizeHTML( $this->message ); 42 | echo '

'; 43 | echo '
'; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Messages/BookingReminderMessage.php: -------------------------------------------------------------------------------- 1 | getPostId() ); 30 | $booking_user = get_userdata( $this->getPost()->post_author ); 31 | 32 | // get templates from Admin Options 33 | $template_body = Settings::getOption( 34 | 'commonsbooking_options_reminder', 35 | $this->action . '-body' 36 | ); 37 | $template_subject = Settings::getOption( 38 | 'commonsbooking_options_reminder', 39 | $this->action . '-subject', 40 | 'sanitize_text_field' 41 | ); 42 | 43 | // Setup email: From 44 | $fromHeaders = sprintf( 45 | 'From: %s <%s>', 46 | Settings::getOption( 'commonsbooking_options_templates', 'emailheaders_from-name', 'sanitize_text_field' ), 47 | sanitize_email( Settings::getOption( 'commonsbooking_options_templates', 'emailheaders_from-email' ) ) 48 | ); 49 | 50 | $this->prepareMail( 51 | MessageRecipient::fromUser( $booking_user ), 52 | $template_body, 53 | $template_subject, 54 | $fromHeaders, 55 | null, 56 | [ 57 | 'booking' => $booking, 58 | 'item' => $booking->getItem(), 59 | 'location' => $booking->getLocation(), 60 | 'user' => $booking_user, 61 | ] 62 | ); 63 | $this->sendNotificationMail(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Migration/Booking.php: -------------------------------------------------------------------------------- 1 | ID ], 41 | $items, 42 | $this->getDate() ?: $date, 43 | $asModel, 44 | Helper::getLastFullHourTimestamp() 45 | ); 46 | } 47 | if ( get_called_class() == Item::class ) { 48 | $bookableTimeframes = Timeframe::getBookableForCurrentUser( 49 | $locations, 50 | [ $this->ID ], 51 | $this->getDate() ?: $date, 52 | $asModel, 53 | Helper::getLastFullHourTimestamp() 54 | ); 55 | } 56 | 57 | return $bookableTimeframes; 58 | } 59 | 60 | /** 61 | * Checks if a specified location or item is bookable. 62 | * Bookable means, that there is at least one associated timeframe that is bookable. 63 | * 64 | * @return bool 65 | * @throws Exception 66 | */ 67 | public function isBookable(): bool { 68 | return count( $this->getBookableTimeframes() ) > 0; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Model/BookingCode.php: -------------------------------------------------------------------------------- 1 | date = $date; 52 | $this->item = $item; 53 | $this->code = $code; 54 | } 55 | 56 | /** 57 | * @return string 58 | */ 59 | public function getDate(): string { 60 | return $this->date; 61 | } 62 | 63 | /** 64 | * @return int 65 | */ 66 | public function getItem(): int { 67 | return $this->item; 68 | } 69 | 70 | public function getItemName() { 71 | $post = get_post( $this->getItem() ); 72 | 73 | return $post->post_title; 74 | } 75 | 76 | /** 77 | * @return string 78 | */ 79 | public function getCode(): string { 80 | return $this->code; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/Model/Item.php: -------------------------------------------------------------------------------- 1 | ID ], 32 | null, 33 | $asModel, 34 | Helper::getLastFullHourTimestamp() 35 | ); 36 | } 37 | 38 | /** 39 | * Will get all the admins for this item. 40 | * The admins can be configured in the backend. 41 | * This will not get the admins of the location that this item belongs to. If you want that, use the function from the Model/Timeframe class. 42 | * 43 | * TODO: This currently includes the author of the item as an admin. 44 | * This does not make sense in all contexts and should be fixed. 45 | * 46 | * @return array|mixed|string[] 47 | */ 48 | public function getAdmins() { 49 | $itemId = $this->ID; 50 | $itemAdminIds = get_post_meta( $itemId, '_' . \CommonsBooking\Wordpress\CustomPostType\Item::$postType . '_admins', true ); 51 | if ( is_string( $itemAdminIds ) ) { 52 | if ( strlen( $itemAdminIds ) > 0 ) { 53 | $itemAdminIds = [ $itemAdminIds ]; 54 | } else { 55 | $itemAdminIds = []; 56 | } 57 | } 58 | $itemAdminIds[] = get_post_field( 'post_author', $this->ID ); 59 | 60 | return array_values( 61 | array_unique( 62 | array_map( 'intval', $itemAdminIds ) 63 | ) 64 | ); 65 | } 66 | 67 | /** 68 | * Returns all applicable restrictions for this item. 69 | * 70 | * This function is not used anywhere yet. 71 | * 72 | * @return array 73 | * @throws Exception 74 | */ 75 | public function getRestrictions(): array { 76 | return \CommonsBooking\Repository\Restriction::get( 77 | [], 78 | [ $this->ID ], 79 | null, 80 | true 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Model/MessageRecipient.php: -------------------------------------------------------------------------------- 1 | email = $email; 36 | $this->niceName = $niceName; 37 | } 38 | 39 | /** 40 | * Construct a new recipient from a WP_User object 41 | * 42 | * @param WP_User $user 43 | * 44 | * @return MessageRecipient 45 | */ 46 | public static function fromUser( WP_User $user ): MessageRecipient { 47 | return new self( $user->user_email, $user->user_nicename ); 48 | } 49 | 50 | public function getEmail(): string { 51 | return $this->email; 52 | } 53 | 54 | public function getNiceName(): string { 55 | return $this->niceName; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Repository/ApiShares.php: -------------------------------------------------------------------------------- 1 | getKey() == $key ) { 49 | return $apiShare; 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Repository/Item.php: -------------------------------------------------------------------------------- 1 | post_type == \CommonsBooking\Wordpress\CustomPostType\Timeframe::getPostType() ) { 30 | $type = get_post_meta( $post->ID, 'type', true ); 31 | switch ( $type ) { 32 | case \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKING_ID: // booking 33 | return new \CommonsBooking\Model\Booking( $post ); 34 | case \CommonsBooking\Wordpress\CustomPostType\Timeframe::BOOKING_CANCELED_ID: // booking canceled 35 | return new \CommonsBooking\Model\Booking( $post ); 36 | } 37 | } 38 | 39 | if ( $post->post_type == \CommonsBooking\Wordpress\CustomPostType\Item::getPostType() ) { 40 | return new \CommonsBooking\Model\Item( $post ); 41 | } 42 | 43 | if ( $post->post_type == \CommonsBooking\Wordpress\CustomPostType\Location::getPostType() ) { 44 | return new \CommonsBooking\Model\Location( $post ); 45 | } 46 | 47 | if ( $post->post_type == \CommonsBooking\Wordpress\CustomPostType\Restriction::getPostType() ) { 48 | return new \CommonsBooking\Model\Restriction( $post ); 49 | } 50 | 51 | if ( $post->post_type == \CommonsBooking\Wordpress\CustomPostType\Booking::getPostType() ) { 52 | return new \CommonsBooking\Model\Booking( $post ); 53 | } 54 | } 55 | Plugin::setCacheItem( $post, [ $postId ] ); 56 | 57 | return $post; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Service/MassOperations.php: -------------------------------------------------------------------------------- 1 | getMessage(); 23 | } 24 | 25 | if ( $success ) { 26 | $result = array( 27 | 'success' => true, 28 | 'message' => __( 'All selected orphaned bookings have been migrated.', 'commonsbooking' ), 29 | ); 30 | } else { 31 | $result = array( 32 | 'success' => false, 33 | 'message' => ! empty( $errorMessage ) ? $errorMessage : __( 'An error occurred while moving bookings.', 'commonsbooking' ), 34 | ); 35 | } 36 | 37 | wp_send_json( $result ); 38 | } 39 | 40 | /** 41 | * Will migrate an array of booking IDs to a new location 42 | * The new location is determined by the location of the previous timeframe the booking was in 43 | * If no timeframe is found, the booking will be skipped 44 | * 45 | * @param int[] $bookingIds 46 | * 47 | * @return true 48 | * @throws \Exception 49 | */ 50 | public static function migrateOrphaned( array $bookingIds ): bool { 51 | if ( empty( $bookingIds ) ) { 52 | throw new \Exception( __( 'No bookings to move selected.', 'commonsbooking' ) ); 53 | } 54 | 55 | $orphanedBookings = \CommonsBooking\Repository\Booking::getOrphaned(); 56 | // iterate over them and assign them new locations 57 | foreach ( $orphanedBookings as $booking ) { 58 | if ( ! in_array( $booking->ID, $bookingIds ) ) { 59 | continue; 60 | } 61 | try { 62 | $moveLocation = $booking->getMoveableLocation(); 63 | if ( $moveLocation === null ) { 64 | throw new \Exception( sprintf( __( 'New location not found for booking with ID %s', 'commonsbooking' ), $booking->ID ) ); 65 | } 66 | } catch ( \Exception $e ) { 67 | throw new \Exception( sprintf( __( 'New location not found for booking with ID %s', 'commonsbooking' ), $booking->ID ) ); 68 | } 69 | if ( \CommonsBooking\Repository\Booking::getExistingBookings( $booking->getItemID(), $moveLocation->ID, $booking->getStartDate(), $booking->getEndDate() ) ) { 70 | throw new \Exception( sprintf( __( 'There is already a booking on the new location during the timeframe of booking with ID %s.', 'commonsbooking' ), $booking->ID ) ); 71 | } 72 | update_post_meta( $booking->ID, 'location-id', $moveLocation->ID ); 73 | } 74 | 75 | return true; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/View/Map.php: -------------------------------------------------------------------------------- 1 | '; 21 | wp_enqueue_script( 'cb-map-positioning_js', COMMONSBOOKING_MAP_ASSETS_URL . 'js/cb-map-positioning.js' ); 22 | 23 | // map defaults 24 | $defaults = [ 25 | 'latitude' => \CommonsBooking\Wordpress\CustomPostType\Map::LATITUDE_DEFAULT, 26 | 'longitude' => \CommonsBooking\Wordpress\CustomPostType\Map::LONGITUDE_DEFAULT, 27 | ]; 28 | wp_add_inline_script( 'cb-map-positioning_js', 'cb_map_positioning.defaults =' . wp_json_encode( $defaults ) ); 29 | } 30 | 31 | public static function renderGeoRefreshButton() { 32 | echo '
33 |
34 | 35 |
36 |
37 | 40 |

' . 41 | commonsbooking_sanitizeHTML( __( 'Click this button to automatically set the GPS coordinates based on the given address and set the marker on the map.
Save or update this location after setting the gps data.', 'commonsbooking' ) ) . 42 | '

43 | 46 |
47 |
'; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/View/Restriction.php: -------------------------------------------------------------------------------- 1 | args( 'id' ); 16 | $label = $field->args( 'name' ); 17 | $desc = $field->args( 'desc' ); 18 | $postId = $field->object_id(); 19 | $sent = get_post_meta( $postId, \CommonsBooking\Model\Restriction::META_SENT, true ); 20 | 21 | if ( $sent ) { 22 | $dateFormat = esc_html( get_option( 'date_format' ) ); 23 | $timeFormat = esc_html( get_option( 'time_format' ) ); 24 | $sent = date( $dateFormat . ' | ' . $timeFormat, $sent ); 25 | } 26 | 27 | ?> 28 |
29 |
30 | 31 |
32 |
33 | 35 | 36 |

37 | 38 |

39 | 40 | 41 |

42 | 43 |

44 | 45 |
46 |
47 | 24 |
25 |
26 | 27 |
28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 |
36 |
37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 | 45 | 46 | 47 | 48 | 49 |
50 |
51 |
52 | 53 | 54 | 55 |
56 |
57 | $tab ) { 27 | $groups = $tab['field_groups']; 28 | $option_key = self::$option_key . '_' . $tab_id; 29 | 30 | foreach ( $groups as $group ) { 31 | $fields = $group['fields']; 32 | 33 | foreach ( $fields as $field ) { 34 | $field_id = $field['id']; 35 | 36 | // set to current value from wp_options 37 | $field_value = Settings::getOption( $option_key, $field_id ); 38 | 39 | // we check if there is a default value set in OptionsArray.php and if the field type is not checkbox (cause checkboxes have empty values if unchecked ) 40 | if ( array_key_exists( 'default', $field ) && $field['type'] != 'checkbox' ) { 41 | // if field-value is not set already we add the default value to the options array 42 | if ( empty( $field_value ) ) { 43 | Settings::updateOption( $option_key, $field_id, $field['default'] ); 44 | $restored_fields[] = $field['name']; 45 | } 46 | } 47 | } 48 | } 49 | } 50 | 51 | // maybe show admin notice if fields are restored to their default value 52 | if ( ! empty( $restored_fields ) ) { 53 | $message = commonsbooking_sanitizeHTML( __( 'Default values for following fields automatically set or restored, because they were empty:
', 'commonsbooking' ) ); 54 | $message .= implode( '
', $restored_fields ); 55 | new AdminMessage( $message ); 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Wordpress/PostStatus/PostStatus.php: -------------------------------------------------------------------------------- 1 | name = $name; 31 | $this->label = $label; 32 | $this->public = $public; 33 | 34 | $this->registerPostStatus(); 35 | $this->addActions(); 36 | } 37 | 38 | /** 39 | * Registers current post status. 40 | */ 41 | public function registerPostStatus() { 42 | register_post_status( 43 | $this->name, 44 | array( 45 | 'label' => $this->label, 46 | 'public' => $this->public, 47 | 'label_count' => _n_noop( 48 | $this->label . ' (%s)', 49 | $this->label . ' (%s)' 50 | ), 51 | ) 52 | ); 53 | } 54 | 55 | /** 56 | * Adds edit actions for post-status to backend. 57 | */ 58 | public function addActions() { 59 | add_action( 'admin_footer-edit.php', array( $this, 'addQuickedit' ) ); 60 | add_action( 'admin_footer', array( $this, 'addOption' ) ); 61 | } 62 | 63 | /** 64 | * Adds poststatus option to backend. 65 | */ 66 | public function addOption() { 67 | global $post; 68 | 69 | $active = ''; 70 | if ( $post ) { 71 | if ( $post->post_status == $this->name ) { 72 | $active = "jQuery( '#post-status-display' ).text( '" . $this->label . "' ); jQuery( 'select[name=\"post_status\"]' ).val('" . $this->name . "');"; 73 | } 74 | 75 | echo "'; 81 | } 82 | } 83 | 84 | /** 85 | * Adds poststatus quickedit to backend. 86 | */ 87 | public function addQuickedit() { 88 | echo ""; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /templates/booking-single-form.php: -------------------------------------------------------------------------------- 1 | canCancel() ) { 17 | $form_post_status = 'canceled'; 18 | $icalbutton_label = esc_html__( 'Add to Calendar', 'commonsbooking' ); 19 | $button_label = esc_html__( 'Cancel Booking', 'commonsbooking' ); 20 | } 21 | 22 | if ( isset( $form_post_status ) ) { 23 | ?> 24 | ID ) { 26 | ?> 27 |
28 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | 53 | 54 | 57 | -------------------------------------------------------------------------------- /templates/booking-single-notallowed.php: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | '; 11 | if ( ! is_user_logged_in() ) { 12 | printf( 13 | '%s', 14 | esc_url( wp_login_url() ), 15 | esc_html__( 'Login to your account', 'commonsbooking' ) 16 | ); 17 | } 18 | ?> 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /templates/calendar-key.php: -------------------------------------------------------------------------------- 1 | 10 |
11 | :
12 |
| 13 |
| 14 |
| 15 |

16 |
-------------------------------------------------------------------------------- /templates/item-calendar-header.php: -------------------------------------------------------------------------------- 1 | thumbnail( 'cb_listing_medium' ) ); // div.thumbnail is printed by function 8 | ?> 9 |
10 |

post_title ); ?>

11 |
12 | 13 | -------------------------------------------------------------------------------- /templates/item-single-meta.php: -------------------------------------------------------------------------------- 1 | property 6 | * item Model methods are available as $item->myMethod() 7 | */ 8 | 9 | global $templateData; 10 | $item = $templateData['item']; 11 | -------------------------------------------------------------------------------- /templates/item-single.php: -------------------------------------------------------------------------------- 1 | ' . esc_html__( $bookThisItemText, 'commonsbooking' ) . ''; 17 | commonsbooking_get_template_part( 'location', 'calendar-header' ); // file: item-calendar-header.php 18 | commonsbooking_get_template_part( 'timeframe', 'calendar' ); // file: timeframe-calendar.php 19 | } 20 | 21 | // Multi item view 22 | if ( array_key_exists( 'locations', $templateData ) && $templateData['locations'] ) { 23 | foreach ( $templateData['locations'] as $location ) { 24 | $templateData['location'] = $location; 25 | commonsbooking_get_template_part( 'item', 'withlocation' ); // file: location-withitem.php 26 | } // end foreach $timeframes 27 | } // $item_is_selected 28 | 29 | 30 | // item not available if no valid location reference found 31 | if ( ! array_key_exists( 'location', $templateData ) && empty( $templateData['locations'] ) ) { ?> 32 |
33 | 40 |
41 | login or register.', 'commonsbooking' ) ), 45 | esc_url( wp_login_url( $current_url ) ), 46 | esc_url( wp_registration_url() ) 47 | ); 48 | ?> 49 |
50 | property 6 | * Timeframe Model methods are available as $timeframe->myMethod() 7 | * 8 | * Model: Timeframe 9 | */ 10 | global $templateData; 11 | $button_label = \CommonsBooking\Settings\Settings::getOption( COMMONSBOOKING_PLUGIN_SLUG . '_options_templates', 'label-booking-button' ); 12 | 13 | /** @var \CommonsBooking\Model\Location $location */ 14 | $location = $templateData['location']; 15 | /** @var \CommonsBooking\Model\Item $item */ 16 | $item = $templateData['item']; 17 | 18 | $permalink = add_query_arg( 'cb-location', $location->ID, get_the_permalink( $item->ID ) ); // booking link set to item detail page with location ID 19 | 20 | $timeframes = $location->getBookableTimeframesByItem( $item->ID, true ); 21 | ?> 22 | 23 | thumbnail( 'cb_listing_small' ) ); // div.thumbnail is printed by function ?> 24 |
25 |

post_title ); ?>

26 | 30 |
31 | formattedBookableDate() ); ?> 32 |
33 | 36 | 37 |
38 |
39 | 40 |
41 | -------------------------------------------------------------------------------- /templates/location-calendar-header.php: -------------------------------------------------------------------------------- 1 | thumbnail( 'cb_listing_small' ) ); // div.thumbnail is printed by function 8 | ?> 9 |
10 |

11 | 12 | post_title ); ?> 13 | 14 |

15 | formattedAddressOneLine(); 17 | if ( ! empty( $locationAddress ) ) { 18 | ?> 19 |
20 | 23 | hasMap() ) { 25 | \CommonsBooking\View\Location::renderLocationMap( $location ); 26 | } 27 | ?> 28 |
29 | formattedPickupInstructionsOneLine() ) { 32 | ?> 33 | 34 | formattedPickupInstructionsOneLine() ); ?> 35 | 38 |
39 |
40 | 41 | 46 | -------------------------------------------------------------------------------- /templates/location-single-meta.php: -------------------------------------------------------------------------------- 1 | property 6 | * location Model methods are available as $location->myMethod() 7 | */ 8 | 9 | global $templateData; 10 | $location = $templateData['location']; 11 | 12 | $location_address = $location->formattedAddressOneLine(); 13 | $location_contact = $location->formattedContactInfoOneLine(); 14 | $pickup_instructions = $location->formattedPickupInstructions(); 15 | $show_contactinfo_unconfirmed = \CommonsBooking\Settings\Settings::getOption( 'commonsbooking_options_templates', 'show_contactinfo_unconfirmed' ); 16 | $text_hidden_contactinfo = \CommonsBooking\Settings\Settings::getOption( 'commonsbooking_options_templates', 'text_hidden-contactinfo' ); 17 | 18 | 19 | ?> 20 | 21 |
22 |
23 |
formattedAddressOneLine() ); ?>
24 | hasMap() ) { 26 | \CommonsBooking\View\Location::renderLocationMap( $location ); 27 | } 28 | ?> 29 |
30 | 31 | 32 |
33 |
34 | 38 |
formattedContactInfoOneLine() ); ?>
39 | 43 |
44 | 48 |
49 | 50 | 51 | 52 |
53 |
54 |
formattedPickupInstructionsOneLine() ); ?>
55 |
56 | 57 | -------------------------------------------------------------------------------- /templates/location-single.php: -------------------------------------------------------------------------------- 1 | ' . esc_html__( $bookThisItemText, 'commonsbooking' ) . ''; 19 | commonsbooking_get_template_part( 'item', 'calendar-header' ); // file: item-calendar-header.php 20 | commonsbooking_get_template_part( 'timeframe', 'calendar' ); // file: timeframe-calendar.php 21 | } 22 | 23 | // Multi item view 24 | if ( array_key_exists( 'items', $templateData ) && $templateData['items'] ) { 25 | foreach ( $templateData['items'] as $item ) { 26 | $templateData['item'] = $item; 27 | commonsbooking_get_template_part( 'location', 'withitem' ); // file: location-withitem.php 28 | } // end foreach $timeframes 29 | } // $item_is_selected 30 | 31 | if ( ! array_key_exists( 'item', $templateData ) && ! array_key_exists( 'items', $templateData ) ) { ?> 32 |
33 | 39 |
40 | login or register.', 'commonsbooking' ) ), 44 | esc_url( wp_login_url( $current_url ) ), 45 | esc_url( wp_registration_url() ) 46 | ); 47 | ?> 48 |
49 | property 6 | * Timeframe Model methods are available as $timeframe->myMethod() 7 | * 8 | * Model: Timeframe 9 | */ 10 | 11 | use CommonsBooking\Settings\Settings; 12 | global $templateData; 13 | 14 | 15 | /** @var \CommonsBooking\Model\Location $location */ 16 | $location = $templateData['location']; 17 | /** @var \CommonsBooking\Model\Item $item */ 18 | $item = $templateData['item']; 19 | $button_label = Settings::getOption( 'commonsbooking_options_templates', 'label-booking-button' ); 20 | $permalink = add_query_arg( 'cb-location', $location->ID, get_the_permalink( $item->ID ) ); // booking link set to item detail page with location ID 21 | 22 | 23 | $timeframes = $item->getBookableTimeframesByLocation( $location->ID, true ); 24 | ?> 25 | 26 | thumbnail( 'cb_listing_medium' ) ); // div.thumbnail is printed by function ?> 27 | 28 |
29 |

post_title ); ?>

30 | 34 |
35 | formattedBookableDate() ); ?> 36 |
37 | 40 | 41 |
42 |
43 | 44 |
45 | -------------------------------------------------------------------------------- /templates/map-admin-page-template.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wielebenwir/commonsbooking/2f394da297c7d6394d451068f92a12beb82b6fa3/templates/map-admin-page-template.php -------------------------------------------------------------------------------- /templates/massoperations-index.php: -------------------------------------------------------------------------------- 1 | 2 |

Mass Operations

3 |
4 |
5 |
6 |

7 | 10 | .

11 |
12 | 13 |
14 |
15 | 16 |
17 |
18 | 19 |
20 |
21 | 22 |
23 |
-------------------------------------------------------------------------------- /templates/shortcode-items.php: -------------------------------------------------------------------------------- 1 | property 10 | * location Model methods are available as $item->myMethod() 11 | */ 12 | 13 | 14 | global $templateData; 15 | $item = new \CommonsBooking\Model\Item( $templateData['item'] ); 16 | $hasTimeFrames = ( array_key_exists( 'data', $templateData ) && count( $templateData['data'] ) ); 17 | 18 | // the item-not-available message (if item ist currently not available) can be defined via plugin options -> message templates 19 | $noResultText = \CommonsBooking\Settings\Settings::getOption( COMMONSBOOKING_PLUGIN_SLUG . '_options_templates', 'item-not-available' ); 20 | 21 | 22 | ?> 23 |
24 | thumbnail( 'cb_listing_medium' ) ); ?> 25 |
26 |

titleLink() ); ?>

27 | excerpt() ); ?> 28 | 29 |
30 | 31 |
32 |
33 | 34 | $data ) { 41 | $location = new \CommonsBooking\Model\Location( $locationId ); 42 | set_query_var( 'item', $item ); 43 | set_query_var( 'location', $location ); 44 | set_query_var( 'data', $data ); 45 | commonsbooking_get_template_part( 'timeframe', 'withlocation' ); 46 | } 47 | } // end if ($timeframes) ?> -------------------------------------------------------------------------------- /templates/shortcode-items_table.php: -------------------------------------------------------------------------------- 1 | 15 | 16 |
17 | 20 |
21 | -------------------------------------------------------------------------------- /templates/shortcode-locations.php: -------------------------------------------------------------------------------- 1 | property 9 | * location Model methods are available as $location->myMethod() 10 | */ 11 | global $templateData; 12 | $location = new \CommonsBooking\Model\Location( $templateData['location'] ); 13 | 14 | // the location without items message is shown if there are currently no available items at this location. Can be defined via plugin options -> message templates 15 | $noResultText = \CommonsBooking\Settings\Settings::getOption( COMMONSBOOKING_PLUGIN_SLUG . '_options_templates', 'location-without-items' ); 16 | 17 | ?> 18 |
19 | thumbnail( 'cb_listing_medium' ) ); ?> 20 |
21 |

titleLink() ); ?>

22 | excerpt() ); ?> 23 |
24 |
25 | 26 | $data ) { 29 | $item = new \CommonsBooking\Model\Item( $itemId ); 30 | set_query_var( 'item', $item ); 31 | set_query_var( 'location', $location ); 32 | set_query_var( 'data', $data ); 33 | commonsbooking_get_template_part( 'timeframe', 'withitem' ); // file: timeframe-withlocation.php 34 | } 35 | } else { 36 | ?> 37 |
38 | 39 | -------------------------------------------------------------------------------- /templates/timeframe-calendar-day.php: -------------------------------------------------------------------------------- 1 |
  • 2 | 3 |
    4 | getFormattedDate( 'd' ) ); ?> 5 | getFormattedDate( 'M' ) ); ?>> 6 |
    7 | 8 | $slot ) { 10 | if ( array_key_exists( 'timeframe', $slot ) && $slot['timeframe'] ) { 11 | if ( $slot['timeframe']['type'] == '2' ) { 12 | ?> 13 |
    14 | 15 | {{ slot.timestart }} - {{ slot.timeend }} 16 | 17 | {% if backend != 'true' %} 18 |
    19 | {{ wp_nonce|raw }} 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
    29 | {% endif %} 30 |
    31 | 34 |
    35 | 36 | {{ slot.timestart }} - {{ slot.timeend }} 37 | 38 |
    39 | 44 |
  • 45 | -------------------------------------------------------------------------------- /templates/timeframe-notallowed.php: -------------------------------------------------------------------------------- 1 | 7 | 8 |
    9 | '; 11 | if ( ! is_user_logged_in() ) { 12 | printf( 13 | '%s', 14 | esc_url( wp_login_url() ), 15 | esc_html__( 'Login to your account', 'commonsbooking' ) 16 | ); 17 | } 18 | ?> 19 |
    20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /templates/timeframe-withitem.php: -------------------------------------------------------------------------------- 1 | ID, get_the_permalink( $item->ID ) ); // booking link set to item detail page with location ID 6 | ?> 7 | 8 | thumbnail( 'cb_listing_small' ) ); // div.thumbnail is printed by function ?> 9 |
    10 |

    11 | 12 | post_title ); ?> 13 | 14 |

    15 |
    16 | '; 24 | } 25 | } 26 | ?> 27 |
    28 |
    29 |
    30 | 31 |
    32 | -------------------------------------------------------------------------------- /templates/timeframe-withlocation.php: -------------------------------------------------------------------------------- 1 | ID, get_the_permalink( $item->ID ) ); // booking link set to item detail page with location ID 14 | ?> 15 | 16 | thumbnail( 'cb_listing_small' ) ); // div.thumbnail is printed by function ?> 17 | 18 | 19 | 20 |
    21 |

    22 | 23 | post_title ); ?> 24 | 25 |

    26 |
    27 | '; 38 | } 39 | } 40 | ?> 41 |
    42 |
    43 |
    44 | 45 |
    46 | -------------------------------------------------------------------------------- /tests/cypress/cypress.config.js: -------------------------------------------------------------------------------- 1 | const { defineConfig } = require("cypress"); 2 | 3 | module.exports = defineConfig({ 4 | fixturesFolder: 'tests/cypress/fixtures', 5 | screenshotsFolder: 'tests/cypress/screenshots', 6 | videosFolder: 'tests/cypress/videos', 7 | downloadsFolder: 'tests/cypress/downloads', 8 | env: { 9 | wpAdmin:'admin', 10 | wpSubscriber: 'subscriber', 11 | wpPassword:'password', 12 | }, 13 | e2e: { 14 | baseUrl: 'http://localhost:1001/', 15 | setupNodeEvents(on, config) { 16 | // implement node event listeners here 17 | }, 18 | video: false, 19 | specPattern: 'tests/cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', 20 | supportFile: 'tests/cypress/support/e2e.js', 21 | }, 22 | }); 23 | -------------------------------------------------------------------------------- /tests/cypress/e2e/admin-booking.cy.js: -------------------------------------------------------------------------------- 1 | describe('test backend booking', () => { 2 | beforeEach( function() { 3 | cy.visit( '/wp-login.php' ); 4 | cy.wait( 1000 ); 5 | cy.get( '#user_login' ).type( Cypress.env( "wpAdmin" ) ); 6 | cy.get( '#user_pass' ).type( Cypress.env( "wpPassword" ) ); 7 | cy.get( '#wp-submit' ).click(); 8 | } ); 9 | it('can create entirely new admin booking', () => { 10 | let today = new Date() 11 | let inTwoDays = new Date() 12 | inTwoDays.setDate(today.getDate() + 2) 13 | const expectedStartDate = today.toLocaleDateString() 14 | const expectedEndDate = inTwoDays.toLocaleDateString() 15 | cy.visit('/wp-admin/post-new.php?post_type=cb_booking') 16 | cy.get('#title').type('Test booking') 17 | //TODO: get this data from fixtures 18 | cy.get('#item-id').select('BasicTest - NoAdmin') 19 | //make sure, that the correct location is automatically selected, 32 is the id of the location 20 | cy.get('#location-id').should('have.value','32') 21 | //the checkbox should be checked because the timeframe is a full-day timeframe 22 | cy.get('#full-day').should('be.checked') 23 | cy.get('#repetition-start_date').clear().type(expectedStartDate).type('{esc}'); 24 | cy.get('#repetition-end_date').clear().type(expectedEndDate).type('{esc}'); 25 | //click post button 26 | cy.get('#cb-submit-booking').click() 27 | cy.get('#message > p').contains('Post updated.') 28 | cy.get('#post-status-display').contains('Confirmed') 29 | 30 | //let's go to the frontend booking calendar and check that our item exists there 31 | //set date to today (this is probably unnecessary, but just to be sure) 32 | cy.clock(today.getTime()) 33 | cy.visit('/?cb_item=basictest-noadmin&cb-location=32') 34 | cy.get('.is-today').should('have.class', 'is-booked') 35 | }) 36 | 37 | after( function () { 38 | cy.visit('/wp-admin/edit.php?post_type=cb_booking') 39 | cy.get('.submitdelete').click({multiple: true, force: true}) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /tests/cypress/e2e/cpt-frontend-pages.cy.js: -------------------------------------------------------------------------------- 1 | describe('check load of CPT frontend pages where available', () => { 2 | const testName = 'BasicTest' 3 | it('loads items', () => { 4 | cy.visit('/?cb_item=basictest-noadmin') 5 | //wait so that the tile layer for the location map can load before taking the screenshot 6 | cy.wait(2000) 7 | cy.screenshot('cb-item-template') 8 | cy.get('.entry-title').contains(testName); 9 | //Check that location is correctly assigned 10 | cy.get('.cb-title > a').contains(testName); 11 | cy.get('.cb-timeframe-calendar').should('be.visible'); 12 | cy.get('#cb_locationview_map').should('be.visible') 13 | cy.get('#cb_locationview_map').scrollIntoView().screenshot('cb-itemtemplate-locationview-map') 14 | }) 15 | it ('loads locations', () => { 16 | cy.visit('/?cb_location=basictest-koln-dom-locmap-noadmin') 17 | //wait so that the tile layer for the location map can load before taking the screenshot 18 | cy.wait(2000) 19 | cy.screenshot('cb-location-template') 20 | cy.get('.entry-title').contains(testName); 21 | //check for location map 22 | cy.get('#cb_locationview_map').should('be.visible') 23 | cy.get('#cb_locationview_map').scrollIntoView().screenshot('cb-locationtemplate-locationview-map') 24 | //check address 25 | cy.get('.cb-location-address > :nth-child(2)').contains("Domkloster 4, 50667 Köln") 26 | //check item 27 | cy.get('.cb-title').contains(testName); 28 | //timeframe calendar is visible because there is only one assigned location 29 | cy.get('.cb-timeframe-calendar').should('be.visible'); 30 | } ) 31 | }) 32 | -------------------------------------------------------------------------------- /tests/cypress/fixtures/bookableItems.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "post_id": 30, 5 | "post_title": "BasicTest – NoAdmin", 6 | "post_name": "basictest-noadmin", 7 | "test_name": "BasicTest" 8 | }, 9 | { 10 | "post_id": 38, 11 | "post_title": "WeeklyRepetition NoOverbooking – NoAdmin", 12 | "post_name": "weeklyrepetition-noadmin", 13 | "test_name": "WeeklyRepetition NoOverbooking" 14 | }, 15 | { 16 | "post_id": 45, 17 | "post_title": "WeeklyRepetition Overbooking CountAll- NoAdmin", 18 | "post_name": "weeklyrepetition-overbooking-noadmin", 19 | "test_name": "WeeklyRepetition Overbooking CountAll" 20 | }, 21 | { 22 | "post_id": 55, 23 | "post_title": "WeeklyRepetition Overbooking CountOne – NoAdmin", 24 | "post_name": "weeklyrepetition-overbooking-countone-noadmin", 25 | "test_name": "WeeklyRepetition Overbooking CountOne" 26 | }, 27 | { 28 | "post_id": 59, 29 | "post_title": "DailyRep Holiday NoOverbooking – NoAdmin", 30 | "post_name": "dailyrep-holiday-nooverbooking-noadmin", 31 | "test_name": "DailyRep Holiday NoOverbooking" 32 | }, 33 | { 34 | "post_id": 71, 35 | "post_title": "DailyRep Holiday Overbooking CountAll- NoAdmin", 36 | "post_name": "dailyrep-holiday-overbooking-countall-noadmin", 37 | "test_name": "DailyRep Holiday Overbooking CountAll" 38 | }, 39 | { 40 | "post_id": 79, 41 | "post_title": "DailyRep Holiday Overbooking CountOne- NoAdmin", 42 | "post_name": "dailyrep-holiday-overbooking-countone-noadmin", 43 | "test_name": "DailyRep Holiday Overbooking CountOne" 44 | }, 45 | { 46 | "post_id": 90, 47 | "post_title": "WeeklyRepetition Overbooking NoCount - NoAdmin", 48 | "post_name": "weeklyrepetition-overbooking-nocount-noadmin", 49 | "test_name": "WeeklyRepetition Overbooking NoCount" 50 | }, 51 | { 52 | "post_id": 118, 53 | "post_title": "DailyRep Holiday NoOverbooking 1Day – NoAdmin", 54 | "post_name": "dailyrep-holiday-nooverbooking-1day-noadmin", 55 | "test_name": "DailyRep Holiday NoOverbooking 1Day" 56 | }, 57 | { 58 | "post_id": 112, 59 | "post_title": "WeeklyRepetition NoOverbooking 1Day – NoAdmin", 60 | "post_name": "weeklyrepetition-nooverbooking-1day-noadmin", 61 | "test_name": "WeeklyRepetition NoOverbooking 1Day" 62 | } 63 | ] 64 | } 65 | -------------------------------------------------------------------------------- /tests/cypress/fixtures/bookableLocations.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "post_id": 32, 5 | "post_title": "BasicTest – Köln Dom LocMap NoAdmin", 6 | "post_name": "basictest-koln-dom-locmap-noadmin", 7 | "test_name": "BasicTest" 8 | }, 9 | { 10 | "post_id": 39, 11 | "post_title": "WeeklyRepetition NoOverbooking – NoAdmin; NoGeo", 12 | "post_name": "weeklyrepetition-nooverbooking-noadmin-nogeo", 13 | "test_name": "WeeklyRepetition NoOverbooking" 14 | }, 15 | { 16 | "post_id": 47, 17 | "post_title": "WeeklyRepetition Overbooking CountAll – NoAdmin; NoGeo", 18 | "post_name": "weeklyrepetition-overbooking-countall-noadmin-nogeo", 19 | "test_name": "WeeklyRepetition Overbooking CountAll" 20 | }, 21 | { 22 | "post_id": 53, 23 | "post_title": "WeeklyRepetition Overbooking CountOne – NoAdmin ; NoGeo", 24 | "post_name": "weeklyrepetition-overbooking-countone-noadmin-nogeo", 25 | "test_name": "WeeklyRepetition Overbooking CountOne" 26 | }, 27 | { 28 | "post_id": 61, 29 | "post_title": "DailyRep Holiday NoOverbooking – NoAdmin; NoGeo", 30 | "post_name": "dailyrep-holiday-nooverbooking-noadmin-nogeo", 31 | "test_name": "DailyRep Holiday NoOverbooking" 32 | }, 33 | { 34 | "post_id": 73, 35 | "post_title": "DailyRep Holiday Overbooking CountAll – NoAdmin; NoGeo", 36 | "test_name": "DailyRep Holiday Overbooking CountAll" 37 | }, 38 | { 39 | "post_id": 81, 40 | "post_title": "DailyRep Holiday Overbooking CountOne- NoAdmin; NoGeo", 41 | "post_name": "dailyrep-holiday-overbooking-countone-noadmin-nogeo", 42 | "test_name": "DailyRep Holiday Overbooking CountOne" 43 | }, 44 | { 45 | "post_id": 88, 46 | "post_title": "WeeklyRepetition Overbooking NoCount – NoAdmin; NoGeo", 47 | "post_name": "weeklyrepetition-overbooking-nocount-noadmin-nogeo", 48 | "test_name": "WeeklyRepetition Overbooking NoCount" 49 | }, 50 | { 51 | "post_id": 116, 52 | "post_title": "DailyRep Holiday NoOverbooking 1Day – NoAdmin; NoGeo", 53 | "post_name": "dailyrep-holiday-nooverbooking-1day-noadmin-nogeo", 54 | "test_name": "DailyRep Holiday NoOverbooking 1Day" 55 | }, 56 | { 57 | "post_id": 110, 58 | "post_title": "WeeklyRepetition NoOverbooking 1Day – NoAdmin; NoGeo", 59 | "post_name": "weeklyrepetition-nooverbooking-1day-noadmin-nogeo", 60 | "test_name": "WeeklyRepetition NoOverbooking 1Day" 61 | } 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /tests/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add('login', (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) -------------------------------------------------------------------------------- /tests/cypress/support/e2e.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/e2e.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | Cypress.on('uncaught:exception', (err, runnable) => { 22 | // WordPress is throwing this error in the latest trunk when trying to login with a changed date #61224 in Trac. This workaround is in place until the issue is resolved 23 | if (err.message.includes('Cannot read properties of undefined (reading \'serialize\')')) { 24 | return false 25 | } 26 | 27 | // #1632 28 | if (err.message.includes('Cannot destructure property \'documentElement\' of \'o\' as it is null.')) { 29 | return false 30 | } 31 | // we still want to ensure there are no other unexpected 32 | // errors, so we let them fail the test 33 | }) 34 | -------------------------------------------------------------------------------- /tests/cypress/wordpress-files/e2e-override.json: -------------------------------------------------------------------------------- 1 | { 2 | "phpVersion": "VERSION_PLACEHOLDER", 3 | "plugins": [ 4 | "./build/commonsbooking", 5 | "https://downloads.wordpress.org/plugin/wordpress-importer.zip", 6 | "https://downloads.wordpress.org/plugin/disable-welcome-messages-and-tips.zip" 7 | ], 8 | "themes": [ 9 | "flegfleg/kasimir-theme" 10 | ], 11 | "env": { 12 | "tests": { 13 | "mappings": { 14 | "wp-content/plugins/commonsbooking": "./build/commonsbooking", 15 | "wp-content/plugins/commonsbooking/tests/cypress/wordpress-files": "./tests/cypress/wordpress-files" 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /tests/php/API/AvailabilityRouteEmptyTest.php: -------------------------------------------------------------------------------- 1 | ENDPOINT ); 17 | 18 | $response = rest_do_request( $request ); 19 | 20 | $this->assertSame( 200, $response->get_status() ); 21 | $this->assertSame( 0, count( $response->get_data()->availability ) ); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/php/API/CB_REST_Route_UnitTestCase.php: -------------------------------------------------------------------------------- 1 | assertNotNull( $this->ENDPOINT ); 14 | $routes = $this->server->get_routes(); 15 | $this->assertArrayHasKey( $this->ENDPOINT, $routes ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/php/API/CB_REST_UnitTestCase.php: -------------------------------------------------------------------------------- 1 | server = $wp_rest_server = new \WP_REST_Server(); 26 | 27 | // Enables api 28 | Settings::updateOption( 'commonsbooking_options_api', 'api-activated', 'on' ); 29 | Settings::updateOption( 'commonsbooking_options_api', 'apikey_not_required', 'on' ); 30 | 31 | // Registers routes (via rest_api_init hook) 32 | ( new Plugin() )->initRoutes(); 33 | 34 | // Applies hook 35 | do_action( 'rest_api_init' ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/php/API/GBFS/DiscoveryRouteTest.php: -------------------------------------------------------------------------------- 1 | setStreetName( 'Karl-Marx-Straße' ) 25 | ->setStreetNumber( '1' ) 26 | ->setPostalCode( '12043' ) 27 | ->setLocality( 'Berlin' ) 28 | ->setCountry( 'Germany' ) 29 | ->setCoordinates( 52.4863573, 13.4247667 ); 30 | 31 | return $location->build(); 32 | } 33 | 34 | /** 35 | * This can be used to get mocked locations from nominatim 36 | * 37 | * @param TestCase $case 38 | * 39 | * @return void 40 | */ 41 | public static function setUpGeoHelperMock( TestCase $case ): void { 42 | 43 | $sut = $case->createStub( GeoCodeService::class ); 44 | $sut->method( 'getAddressData' ) 45 | ->willReturn( self::mockedLocation() ); 46 | GeoHelper::setGeoCodeServiceInstance( $sut ); 47 | } 48 | 49 | public function testThatGeoCoding_worksOffline() { 50 | $address = GeoHelper::getAddressData( 'Karl-Marx-Straße 1, 12043 Berlin' ); 51 | $this->assertThatKarlMarxLocationIsProperlyGeoCoded( $address ); 52 | } 53 | 54 | public function testThatGeoCoding_worksOnline() { 55 | GeoHelper::resetGeoCoder(); 56 | 57 | $address = GeoHelper::getAddressData( 'Karl-Marx-Straße 1, 12043 Berlin' ); 58 | $this->assertThatKarlMarxLocationIsProperlyGeoCoded( $address ); 59 | } 60 | private function assertThatKarlMarxLocationIsProperlyGeoCoded( Location $address ): void { 61 | $this->assertEquals( 'Karl-Marx-Straße', $address->getStreetName() ); 62 | $this->assertEquals( '1', $address->getStreetNumber() ); 63 | $this->assertEquals( '12043', $address->getPostalCode() ); 64 | $this->assertEquals( 'Berlin', $address->getLocality() ); 65 | $this->assertEquals( 'Germany', $address->getCountry() ); 66 | // This won't check exact coords on purpose, because sometimes there are different results 67 | $this->assertStringStartsWith( '52.4863', '' . $address->getCoordinates()->getLatitude() ); 68 | $this->assertStringStartsWith( '13.424', '' . $address->getCoordinates()->getLongitude() ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /tests/php/Messages/BookingReminderMessageTest.php: -------------------------------------------------------------------------------- 1 | bookingId, 'pre-booking-reminder' ); 21 | $bookingMessage->sendMessage(); 22 | $mailer = $this->getMockMailer(); 23 | $this->assertEmpty( $mailer->ErrorInfo ); 24 | $this->assertEquals( self::FROM_MAIL, $mailer->From ); 25 | $this->assertEquals( self::FROM_NAME, $mailer->FromName ); 26 | $this->assertEmpty( $mailer->getBccAddresses() ); 27 | $this->assertEquals( $expectedSubject, $mailer->Subject ); 28 | $this->assertEquals( $expectedBody, $mailer->Body ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/php/Model/BookablePostTest.php: -------------------------------------------------------------------------------- 1 | assertTrue( $this->locationModel->isBookable() ); 22 | 23 | // now the same for an item 24 | $this->assertTrue( $this->itemModel->isBookable() ); 25 | } 26 | 27 | public function testGetBookableTimeframes() { 28 | ClockMock::freeze( new \DateTime( self::CURRENT_DATE ) ); 29 | // test basic getting with just one bookable timeframe for a location 30 | $bookableTimeframes = $this->locationModel->getBookableTimeframes( true ); 31 | $this->assertEquals( 1, count( $bookableTimeframes ) ); 32 | $this->assertEquals( $this->timeframeModel, $bookableTimeframes[0] ); 33 | 34 | // now the same for an item 35 | $bookableTimeframes = $this->itemModel->getBookableTimeframes( true ); 36 | $this->assertEquals( 1, count( $bookableTimeframes ) ); 37 | $this->assertEquals( $this->timeframeModel, $bookableTimeframes[0] ); 38 | 39 | // now we create more than one item + timeframe for the first location and check if we get both 40 | $item2 = new Item( $this->createItem( 'Item2', 'publish' ) ); 41 | $timeframe2 = new Timeframe( $this->createBookableTimeFrameIncludingCurrentDay( $this->locationId, $item2->ID ) ); 42 | 43 | $bookableTimeframes = $this->locationModel->getBookableTimeframes( true ); 44 | $this->assertEquals( 2, count( $bookableTimeframes ) ); 45 | $this->assertEquals( $this->timeframeModel, $bookableTimeframes[0] ); 46 | $this->assertEquals( $timeframe2, $bookableTimeframes[1] ); 47 | 48 | // and now let's test if we can get the specific timeframe for just one item for the location 49 | $bookableTimeframes = $this->locationModel->getBookableTimeframes( true, [], [ $item2->ID ] ); 50 | $this->assertEquals( 1, count( $bookableTimeframes ) ); 51 | $this->assertEquals( $timeframe2, $bookableTimeframes[0] ); 52 | } 53 | 54 | protected function setUp(): void { 55 | parent::setUp(); 56 | 57 | $this->timeframeId = $this->createBookableTimeFrameIncludingCurrentDay(); 58 | $this->locationModel = new Location( $this->locationId ); 59 | $this->itemModel = new Item( $this->itemId ); 60 | $this->timeframeModel = new Timeframe( $this->timeframeId ); 61 | } 62 | 63 | protected function tearDown(): void { 64 | parent::tearDown(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/php/Model/ItemTest.php: -------------------------------------------------------------------------------- 1 | timeframeModel; 23 | $this->assertEquals($timeframeArray, $this->itemModel->getBookableTimeframesByItem($this->locationId)); //Not working 24 | } 25 | */ 26 | 27 | public function testGetAdmins() { 28 | // Case: No admins 29 | // $this->assertEquals([], $this->itemModel->getAdmins()); - Currently this function includes the post author 30 | $this->assertEquals( [ self::USER_ID ], $this->itemModel->getAdmins() ); 31 | 32 | // Case: CB Manager as admin 33 | $this->createCBManager(); 34 | $adminItemModel = new Item( 35 | $this->createItem( 'Testitem2', 'publish', [ $this->cbManagerUserID ] ) 36 | ); 37 | // $this->assertEquals([$this->cbManagerUserID], $adminItemModel->getAdmins()); - Currently this function includes the post author 38 | $this->assertEquals( [ $this->cbManagerUserID, self::USER_ID ], $adminItemModel->getAdmins() ); 39 | } 40 | 41 | 42 | /** 43 | * Can be used after PR #1179 is merged 44 | * @return void 45 | * @throws \Exception 46 | */ 47 | /* 48 | public function testGetRestrictions() { 49 | $this->restrictionIds = array_unique($this->restrictionIds); 50 | $restrictionArray = []; 51 | foreach ($this->restrictionIds as $restrictionId) { 52 | $restrictionArray[] = new Restriction($restrictionId); 53 | } 54 | $this->assertEquals($restrictionArray, $this->itemModel->getRestrictions()); 55 | } 56 | */ 57 | 58 | protected function setUp(): void { 59 | parent::setUp(); 60 | $this->restrictionIds[] = $this->createRestriction( 61 | Restriction::META_HINT, 62 | $this->locationId, 63 | $this->itemId, 64 | strtotime( self::CURRENT_DATE ), 65 | null 66 | ); 67 | $this->timeframeModel = new Timeframe( $this->createBookableTimeFrameIncludingCurrentDay() ); 68 | $this->itemModel = new Item( $this->itemId ); 69 | $this->createSubscriber(); 70 | } 71 | 72 | protected function tearDown(): void { 73 | parent::tearDown(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tests/php/Model/MessageRecipientTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 'testmail@example.com', $this->manualRecipient->getEmail() ); 15 | } 16 | 17 | public function testGetNiceName() { 18 | $this->assertEquals( 'Test User', $this->manualRecipient->getNiceName() ); 19 | } 20 | 21 | public function testFromUser() { 22 | $recipient = MessageRecipient::fromUser( $this->subscriber ); 23 | $this->assertEquals( 'a@a.de', $recipient->getEmail() ); 24 | $this->assertEquals( 'normaluser', $recipient->getNiceName() ); 25 | } 26 | 27 | protected function setUp(): void { 28 | parent::setUp(); 29 | $this->manualRecipient = new MessageRecipient( 'testmail@example.com', 'Test User' ); 30 | 31 | $this->createSubscriber(); 32 | $this->subscriber = get_userdata( $this->subscriberId ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/php/Model/WeekTest.php: -------------------------------------------------------------------------------- 1 | week = new Week( 2023, 120 ); 19 | $this->assertEquals( 7, count( $this->week->getDays() ) ); 20 | $this->assertEquals( 21 | array( 22 | new Day( '2023-05-01' ), 23 | new Day( '2023-05-02' ), 24 | new Day( '2023-05-03' ), 25 | new Day( '2023-05-04' ), 26 | new Day( '2023-05-05' ), 27 | new Day( '2023-05-06' ), 28 | new Day( '2023-05-07' ), 29 | ), 30 | $this->week->getDays() 31 | ); 32 | } 33 | 34 | public function testGetDays2() { 35 | $this->week = new Week( 2023, 121 ); 36 | $this->assertEquals( 6, count( $this->week->getDays() ) ); 37 | $this->assertEquals( 38 | array( 39 | new Day( '2023-05-02' ), 40 | new Day( '2023-05-03' ), 41 | new Day( '2023-05-04' ), 42 | new Day( '2023-05-05' ), 43 | new Day( '2023-05-06' ), 44 | new Day( '2023-05-07' ), 45 | ), 46 | $this->week->getDays() 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/php/PluginTest.php: -------------------------------------------------------------------------------- 1 | assertIsArray( Plugin::getCustomPostTypes() ); 16 | // make sure, that we also have a model for each custom post type 17 | foreach ( Plugin::getCustomPostTypes() as $customPostType ) { 18 | // first, create a post of this type 19 | $post = wp_insert_post( 20 | [ 21 | 'post_type' => $customPostType, 22 | 'post_title' => 'Test ' . $customPostType, 23 | 'post_status' => 'publish', 24 | ] 25 | ); 26 | $this->assertIsInt( $post ); 27 | $this->postIDs[] = $post; 28 | // then, try to get a model from the post. Every declared CPT should have a model 29 | $this->assertInstanceOf( CustomPost::class, CustomPostType::getModel( $post ) ); 30 | } 31 | } 32 | 33 | protected function setUp(): void { 34 | parent::setUp(); 35 | } 36 | 37 | protected function tearDown(): void { 38 | foreach ( $this->postIDs as $postID ) { 39 | wp_delete_post( $postID, true ); 40 | } 41 | parent::tearDown(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/php/Repository/CB1Test.php: -------------------------------------------------------------------------------- 1 | timeframeInstanceId = parent::createBookableTimeFrameIncludingCurrentDay(); 17 | 18 | // Setup CB 2 Timeframe with CB 1 ID == 1 19 | update_post_meta( $this->timeframeInstanceId, '_cb_cb1_post_post_ID', 1 ); 20 | 21 | // Setup CB 2 Location with CB 1 ID == 2 22 | update_post_meta( $this->locationId, '_cb_cb1_post_post_ID', 2 ); 23 | 24 | // Setup CB 2 Item with CB 1 ID == 3 25 | update_post_meta( $this->itemId, '_cb_cb1_post_post_ID', 3 ); 26 | } 27 | 28 | protected function tearDown(): void { 29 | parent::tearDown(); 30 | } 31 | 32 | public function testGetCB2TimeframeId() { 33 | $this->assertTrue( CB1::getCB2TimeframeId( 1 ) == $this->timeframeInstanceId ); 34 | } 35 | 36 | public function testGetCB2LocationId() { 37 | $this->assertTrue( CB1::getCB2LocationId( 2 ) == $this->locationId ); 38 | } 39 | 40 | public function testGetCB2PostIdByCB1Id() { 41 | $this->assertTrue( CB1::getCB2PostIdByCB1Id( 1 ) == $this->timeframeInstanceId ); 42 | $this->assertTrue( CB1::getCB2PostIdByCB1Id( 2 ) == $this->locationId ); 43 | $this->assertTrue( CB1::getCB2PostIdByCB1Id( 3 ) == $this->itemId ); 44 | } 45 | 46 | public function testGetCB2ItemId() { 47 | $this->assertTrue( CB1::getCB2ItemId( 3 ) == $this->itemId ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/php/Repository/ItemTest.php: -------------------------------------------------------------------------------- 1 | createTimeframe( 15 | $this->locationId, 16 | $this->itemId, 17 | strtotime( 'midnight' ), 18 | strtotime( '+90 days' ) 19 | ); 20 | } 21 | 22 | public function testGetByLocation(): void { 23 | $this->assertEquals( 24 | [ $this->itemId ], 25 | array_map( 26 | fn( $item ) => $item->ID, 27 | Item::getByLocation( $this->locationId, true ) 28 | ) 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/php/Repository/LocationTest.php: -------------------------------------------------------------------------------- 1 | createTimeframe( 15 | $this->locationId, 16 | $this->itemId, 17 | strtotime( 'midnight' ), 18 | strtotime( '+90 days' ) 19 | ); 20 | } 21 | 22 | public function testGetByItem() { 23 | $this->assertTrue( count( Location::getByItem( $this->itemId, true ) ) == 1 ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tests/php/Repository/RestrictionTest.php: -------------------------------------------------------------------------------- 1 | locationId ], [ $this->itemId ] ); 26 | $this->assertIsArray( $restrictions ); 27 | $this->assertEquals( 1, count( $restrictions ) ); 28 | $this->assertEquals( $this->restrictionId, $restrictions[0]->ID ); 29 | } 30 | 31 | 32 | 33 | protected function setUp(): void { 34 | parent::setUp(); 35 | $this->timeframeId = $this->createBookableTimeFrameIncludingCurrentDay(); 36 | $this->restrictionId = $this->createRestriction( 37 | 'hint', 38 | $this->locationId, 39 | $this->itemId, 40 | strtotime( self::CURRENT_DATE ), 41 | strtotime( '+1 day', strtotime( self::CURRENT_DATE ) ) 42 | ); 43 | } 44 | 45 | protected function tearDown(): void { 46 | parent::tearDown(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/php/Service/BookingTest.php: -------------------------------------------------------------------------------- 1 | createBooking( 11 | $this->locationId, 12 | $this->itemId, 13 | strtotime( 'midnight', strtotime( self::CURRENT_DATE ) ), 14 | strtotime( '+2 days', strtotime( self::CURRENT_DATE ) ), 15 | '8:00 AM', 16 | '12:00 PM', 17 | 'unconfirmed' 18 | ); 19 | // first, we check if the cleanup will delete our freshly created unconfirmed booking (it should not) 20 | Booking::cleanupBookings(); 21 | $this->assertNotNull( get_post( $bookingId ) ); 22 | 23 | // we make the post 11 minutes old, so that the cleanup function will delete it (the cleanup function only deletes bookings older than 10 minutes) 24 | wp_update_post( 25 | [ 26 | 'ID' => $bookingId, 27 | 'post_date' => date( 'Y-m-d H:i:s', strtotime( '-11 minutes' ) ), 28 | ] 29 | ); 30 | 31 | // now we run the cleanup function again 32 | Booking::cleanupBookings(); 33 | 34 | // and check if the post is still there 35 | $this->assertNull( get_post( $bookingId ) ); 36 | } 37 | 38 | protected function setUp(): void { 39 | parent::setUp(); 40 | $this->firstTimeframeId = $this->createBookableTimeFrameIncludingCurrentDay(); 41 | } 42 | 43 | protected function tearDown(): void { 44 | parent::tearDown(); 45 | \Mockery::close(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tests/php/Service/HolidayTest.php: -------------------------------------------------------------------------------- 1 | ', $selectOptions ); 16 | // filter out the empty string after the last 17 | $selectOptions = array_filter( $selectOptions, fn( $option ) => ! empty( $option ) ); 18 | // 2022 + 2 years 19 | $this->assertCount( 3, $selectOptions ); 20 | $this->assertStringContainsString( '2022', $selectOptions[0] ); 21 | $this->assertStringContainsString( '2023', $selectOptions[1] ); 22 | $this->assertStringContainsString( '2024', $selectOptions[2] ); 23 | } 24 | 25 | public function testGetStatesOption() { 26 | $selectOptions = Holiday::getStatesOption(); 27 | $selectOptions = explode( '', $selectOptions ); 28 | // filter out the empty string after the last 29 | $selectOptions = array_filter( $selectOptions, fn( $option ) => ! empty( $option ) ); 30 | // 16 states + bund 31 | $this->assertCount( 17, $selectOptions ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/php/Service/iCalendarTest.php: -------------------------------------------------------------------------------- 1 | calendar->addBookingEvent( 18 | $this->bookingModel, 19 | static::$eventTitle, 20 | static::$eventDescription 21 | ); 22 | $this->assertNotNull( $this->calendar->getCalendarData() ); 23 | $calendarData = $this->calendar->getCalendarData(); 24 | $this->checkCalendarStringValid( $calendarData ); 25 | } 26 | 27 | public function testAddEventOneDay() { 28 | // tests the generic event adding method 29 | // test just for one day: 30 | $event = $this->calendar->addEvent( 31 | DateTimeImmutable::createFromFormat( 'Y-m-d', '2020-01-01' ), 32 | static::$eventTitle, 33 | static::$eventDescription 34 | ); 35 | $this->assertInstanceOf( \Eluceo\iCal\Domain\Entity\Event::class, $event ); 36 | $calendarData = $this->calendar->getCalendarData(); 37 | $this->checkCalendarStringValid( $calendarData ); 38 | } 39 | 40 | public function testAddEventMultipleDays() { 41 | $event = $this->calendar->addEvent( 42 | [ new \DateTimeImmutable( '2020-01-01 00:00:00' ),new \DateTimeImmutable( '2020-01-02 01:00:00' ) ], 43 | static::$eventTitle, 44 | static::$eventDescription, 45 | true 46 | ); 47 | $this->assertInstanceOf( \Eluceo\iCal\Domain\Entity\Event::class, $event ); 48 | $calendarData = $this->calendar->getCalendarData(); 49 | $this->checkCalendarStringValid( $calendarData ); 50 | } 51 | 52 | private function checkCalendarStringValid( string $calendar ) { 53 | $iCalendarArray = explode( "\r\n", $calendar ); 54 | $iCalendarArray = array_filter( $iCalendarArray ); 55 | $this->assertIsArray( $iCalendarArray ); 56 | 57 | $this->assertEquals( 'BEGIN:VCALENDAR', $iCalendarArray[0] ); 58 | $this->assertEquals( 'END:VCALENDAR', end( $iCalendarArray ) ); 59 | } 60 | 61 | protected function setUp(): void { 62 | parent::setUp(); 63 | $this->calendar = new iCalendar(); 64 | $this->bookingModel = new Booking( $this->bookingId ); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /tests/php/Settings/SettingsTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( $emailHeaderExpected, $emailBodyActual ); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/php/View/BookingTest.php: -------------------------------------------------------------------------------- 1 | createBooking( 14 | $this->locationId, 15 | $this->itemId, 16 | time() - 86400, 17 | time() + 86400 18 | ); 19 | } 20 | 21 | protected function tearDown(): void { 22 | parent::tearDown(); 23 | } 24 | 25 | public function testGetBookingListData() { 26 | wp_set_current_user( self::USER_ID ); 27 | $bookings = Booking::getBookingListData(); 28 | $this->assertTrue( $bookings['total'] == 1 ); 29 | 30 | // check for #1802, delete location. Booking list should still generate 31 | wp_delete_post( $this->locationId, true ); 32 | $bookings = Booking::getBookingListData(); 33 | $this->assertTrue( $bookings['total'] == 1 ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/php/View/MassOperationsTest.php: -------------------------------------------------------------------------------- 1 | assertTrue( $doc->loadHTML( $html ) ); 19 | $this->assertEquals( 0, count( libxml_get_errors() ) ); 20 | } 21 | 22 | public function testRenderBookingViewTable() { 23 | // first with empty result 24 | ob_start(); 25 | MassOperations::renderBookingViewTable( [] ); 26 | $html = ob_get_clean(); 27 | $this->assertStringContainsString( '

    No bookings found.

    ', $html ); 28 | 29 | // then with a booking 30 | $booking = new Booking( $this->createConfirmedBookingEndingToday() ); 31 | ob_start(); 32 | MassOperations::renderBookingViewTable( [ $booking ] ); 33 | $html = ob_get_clean(); 34 | // naive way of testing html validity 35 | libxml_use_internal_errors( true ); 36 | $doc = new \DOMDocument(); 37 | $this->assertTrue( $doc->loadHTML( $html ) ); 38 | $this->assertEquals( 0, count( libxml_get_errors() ) ); 39 | } 40 | 41 | protected function setUp(): void { 42 | parent::setUp(); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/php/bootstrap.php: -------------------------------------------------------------------------------- 1 |