├── composer └── autoload.php ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report_form.yml.license │ ├── feature_request_form.yml.license │ ├── config.yml │ ├── feature_request_form.yml │ └── bug_report_form.yml ├── CODEOWNERS └── workflows │ ├── reuse.yml │ ├── fixup.yml │ ├── lint-info-xml.yml │ ├── lint-php-cs.yml │ ├── lint-php.yml │ ├── pr-feedback.yml │ ├── setup.yml │ ├── phpunit-sqlite.yml │ └── appstore-build-publish.yml ├── vendor-bin └── phpunit │ └── composer.json ├── krankerl.toml ├── .git-blame-ignore-revs ├── tests ├── phpunit.xml ├── bootstrap.php └── Unit │ └── Db │ ├── BuildingMapperTest.php │ ├── StoryMapperTest.php │ ├── RestrictionMapperTest.php │ ├── RoomMapperTest.php │ ├── ResourceMapperTest.php │ └── VehicleMapperTest.php ├── .nextcloudignore ├── .php-cs-fixer.dist.php ├── AUTHORS.md ├── lib ├── Db │ ├── StoryModel.php │ ├── RestrictionModel.php │ ├── BuildingModel.php │ ├── VehicleModel.php │ ├── ResourceModel.php │ ├── VehicleMapper.php │ ├── ResourceMapper.php │ ├── RoomModel.php │ ├── BuildingMapper.php │ ├── StoryMapper.php │ ├── RestrictionMapper.php │ ├── RoomMapper.php │ └── AMapper.php ├── Service │ ├── RoomService.php │ ├── VehicleService.php │ ├── ResourceService.php │ ├── BuildingService.php │ └── UidValidationService.php ├── Listener │ ├── GroupDeletedListener.php │ └── UserDeletedListener.php ├── AppInfo │ └── Application.php ├── Connector │ ├── Resource │ │ ├── Vehicle.php │ │ ├── ResourceObject.php │ │ └── Backend.php │ └── Room │ │ ├── Backend.php │ │ └── Room.php ├── Command │ ├── CreateStory.php │ ├── CreateRestriction.php │ ├── CreateBuilding.php │ ├── CreateResource.php │ ├── DeleteResource.php │ ├── CreateVehicle.php │ ├── CreateRoom.php │ └── ListResources.php └── Migration │ └── Version1000Date20200805220319.php ├── composer.json ├── LICENSES ├── MIT.txt └── CC0-1.0.txt ├── REUSE.toml ├── CHANGELOG.md ├── appinfo └── info.xml ├── .gitignore ├── renovate.json └── README.md /composer/autoload.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | ./Unit 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.nextcloudignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | /build 4 | /composer.* 5 | /CONTRIBUTING.md 6 | .git 7 | .gitattributes 8 | .github 9 | .gitignore 10 | /krankerl.toml 11 | /l10n/no-php 12 | /.tx 13 | /.nextcloudignore 14 | /.php_cs.dist 15 | /phpunit*xml 16 | /psalm.xml 17 | /tests 18 | /vendor/bin 19 | /vendor-bin 20 | /.git-blame-ignore-revs 21 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | getFinder() 17 | ->ignoreVCSIgnored(true) 18 | ->notPath('build') 19 | ->notPath('l10n') 20 | ->notPath('src') 21 | ->notPath('vendor') 22 | ->in(__DIR__); 23 | return $config; 24 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | loadApp('calendar_resource_management'); 21 | -------------------------------------------------------------------------------- /.github/workflows/reuse.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | 6 | # SPDX-FileCopyrightText: 2022 Free Software Foundation Europe e.V. 7 | # 8 | # SPDX-License-Identifier: CC0-1.0 9 | 10 | name: REUSE Compliance Check 11 | 12 | on: [pull_request] 13 | 14 | jobs: 15 | reuse-compliance-check: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 20 | with: 21 | persist-credentials: false 22 | 23 | - name: REUSE Compliance Check 24 | uses: fsfe/reuse-action@676e2d560c9a403aa252096d99fcab3e1132b0f5 # v6.0.0 25 | -------------------------------------------------------------------------------- /AUTHORS.md: -------------------------------------------------------------------------------- 1 | 5 | # Authors 6 | 7 | - Andy Scherzinger 8 | - Anna Larch 9 | - Christoph Wurst 10 | - Clemens Sonnleitner 11 | - Daniel Kesselberg 12 | - Ferdinand Thiessen 13 | - Georg Ehrke 14 | - greta 15 | - John Molakvoæ 16 | - mokkin 17 | - N4IR0 18 | - q-wertz 19 | - Richard Steinmetz 20 | - skjnldsv 21 | - Zweihorn <4863737+Zweihorn@users.noreply.github.com> 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | contact_links: 4 | - name: 🚨 Report a security or privacy issue 5 | url: https://hackerone.com/nextcloud 6 | about: Report security and privacy related issues privately to the Nextcloud team, so we can coordinate the fix and release without potentially exposing all Nextcloud servers and users in the meantime. 7 | - name: ❓ Community Support and Help 8 | url: https://help.nextcloud.com/ 9 | about: Configuration, webserver/proxy or performance issues and other questions 10 | - name: 💼 Nextcloud Enterprise 11 | url: https://portal.nextcloud.com/ 12 | about: If you are a Nextcloud Enterprise customer, or need Professional support, so it can be resolved directly by our dedicated engineers more quickly 13 | -------------------------------------------------------------------------------- /lib/Db/StoryModel.php: -------------------------------------------------------------------------------- 1 | addType('buildingId', 'integer'); 34 | $this->addType('displayName', 'string'); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/Service/RoomService.php: -------------------------------------------------------------------------------- 1 | roomMapper = $roomMapper; 34 | $this->restrictionMapper = $restrictionMapper; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "platform": { 4 | "php": "8.0" 5 | }, 6 | "sort-packages": true, 7 | "optimize-autoloader": true, 8 | "classmap-authoritative": true, 9 | "autoloader-suffix": "CalendarResourceManagement", 10 | "allow-plugins": { 11 | "bamarni/composer-bin-plugin": true 12 | } 13 | }, 14 | "autoload": { 15 | "psr-4": { 16 | "OCA\\CalendarResourceManagement\\": "lib/" 17 | } 18 | }, 19 | "require": { 20 | "php": ">=8.0 <=8.3", 21 | "bamarni/composer-bin-plugin": "^1.8.3" 22 | }, 23 | "require-dev": { 24 | "nextcloud/coding-standard": "^1.4.0", 25 | "psalm/phar": "^5.26.1", 26 | "roave/security-advisories": "dev-master" 27 | }, 28 | "scripts": { 29 | "post-install-cmd": [ 30 | "@composer bin phpunit install --ansi" 31 | ], 32 | "cs:check": "php-cs-fixer fix --dry-run --diff", 33 | "cs:fix": "php-cs-fixer fix", 34 | "lint": "find . -name \\*.php -not -path './vendor/*' -exec php -l \"{}\" \\;", 35 | "test:unit": "phpunit -c tests/phpunit.xml" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/Service/VehicleService.php: -------------------------------------------------------------------------------- 1 | vehicleMapper = $vehicleMapper; 34 | $this->restrictionMapper = $restrictionMapper; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/Service/ResourceService.php: -------------------------------------------------------------------------------- 1 | resourceMapper = $resourceMapper; 34 | $this->restrictionMapper = $restrictionMapper; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/Service/BuildingService.php: -------------------------------------------------------------------------------- 1 | buildingMapper = $buildingMapper; 36 | $this->storyMapper = $storyMapper; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/Service/UidValidationService.php: -------------------------------------------------------------------------------- 1 | validateUid($uid)) { 30 | throw new InvalidArgumentException( 31 | 'Only the following characters are allowed in a uid: "a-z", "A-Z", "0-9", spaces and "_.@-\'"' 32 | ); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /.github/workflows/fixup.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Block fixup and squash commits 10 | 11 | on: 12 | pull_request: 13 | types: [opened, ready_for_review, reopened, synchronize] 14 | 15 | permissions: 16 | contents: read 17 | 18 | concurrency: 19 | group: fixup-${{ github.head_ref || github.run_id }} 20 | cancel-in-progress: true 21 | 22 | jobs: 23 | commit-message-check: 24 | if: github.event.pull_request.draft == false 25 | 26 | permissions: 27 | pull-requests: write 28 | name: Block fixup and squash commits 29 | 30 | runs-on: ubuntu-latest-low 31 | 32 | steps: 33 | - name: Run check 34 | uses: skjnldsv/block-fixup-merge-action@c138ea99e45e186567b64cf065ce90f7158c236a # v2 35 | with: 36 | repo-token: ${{ secrets.GITHUB_TOKEN }} 37 | -------------------------------------------------------------------------------- /lib/Db/RestrictionModel.php: -------------------------------------------------------------------------------- 1 | addType('entityType', 'string'); 39 | $this->addType('entityId', 'integer'); 40 | $this->addType('groupId', 'string'); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request_form.yml: -------------------------------------------------------------------------------- 1 | name: "🚀 Feature request" 2 | description: "Suggest an idea for this app" 3 | labels: ["enhancement", "0. to triage"] 4 | body: 5 | - type: textarea 6 | id: description-problem 7 | attributes: 8 | label: Is your feature request related to a problem? Please describe. 9 | description: | 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | - type: textarea 12 | id: description-solution 13 | attributes: 14 | label: Describe the solution you'd like 15 | description: | 16 | A clear and concise description of what you want to happen. 17 | - type: textarea 18 | id: description-alternatives 19 | attributes: 20 | label: Describe alternatives you've considered 21 | description: | 22 | A clear and concise description of any alternative solutions or features you've considered. 23 | - type: textarea 24 | id: additional-context 25 | attributes: 26 | label: Additional context 27 | description: | 28 | Add any other context or screenshots about the feature request here. 29 | -------------------------------------------------------------------------------- /lib/Listener/GroupDeletedListener.php: -------------------------------------------------------------------------------- 1 | mapper = $mapper; 31 | } 32 | 33 | /** 34 | * @inheritDoc 35 | */ 36 | public function handle(Event $event): void { 37 | if (!($event instanceof GroupDeletedEvent)) { 38 | return; 39 | } 40 | 41 | $this->mapper->deleteAllRestrictionsByGroupId($event->getGroup()->getGID()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | version = 1 4 | SPDX-PackageName = "calendar_resource_management" 5 | SPDX-PackageSupplier = "Nextcloud " 6 | SPDX-PackageDownloadLocation = "https://github.com/nextcloud/calendar_resource_management" 7 | 8 | [[annotations]] 9 | path = ["composer.json", "composer.lock"] 10 | precedence = "aggregate" 11 | SPDX-FileCopyrightText = "2021 Nextcloud GmbH and Nextcloud contributors" 12 | SPDX-License-Identifier = "AGPL-3.0-or-later" 13 | 14 | [[annotations]] 15 | path = ["renovate.json", "vendor-bin/phpunit/composer.json", "vendor-bin/phpunit/composer.lock"] 16 | precedence = "aggregate" 17 | SPDX-FileCopyrightText = "2024 Nextcloud GmbH and Nextcloud contributors" 18 | SPDX-License-Identifier = "AGPL-3.0-or-later" 19 | 20 | [[annotations]] 21 | path = "img/app.svg" 22 | precedence = "aggregate" 23 | SPDX-FileCopyrightText = "2018-2024 Google LLC" 24 | SPDX-License-Identifier = "Apache-2.0" 25 | 26 | [[annotations]] 27 | path = "composer/autoload.php" 28 | precedence = "aggregate" 29 | SPDX-FileCopyrightText = "Nils Adermann, Jordi Boggiano" 30 | SPDX-License-Identifier = "MIT" 31 | -------------------------------------------------------------------------------- /.github/workflows/lint-info-xml.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Lint info.xml 10 | 11 | on: pull_request 12 | 13 | permissions: 14 | contents: read 15 | 16 | concurrency: 17 | group: lint-info-xml-${{ github.head_ref || github.run_id }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | xml-linters: 22 | runs-on: ubuntu-latest-low 23 | 24 | name: info.xml lint 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 28 | 29 | - name: Download schema 30 | run: wget https://raw.githubusercontent.com/nextcloud/appstore/master/nextcloudappstore/api/v1/release/info.xsd 31 | 32 | - name: Lint info.xml 33 | uses: ChristophWurst/xmllint-action@36f2a302f84f8c83fceea0b9c59e1eb4a616d3c1 # v1.2 34 | with: 35 | xml-file: ./appinfo/info.xml 36 | xml-schema-file: ./info.xsd 37 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_form.yml: -------------------------------------------------------------------------------- 1 | name: "🐞 Bug report" 2 | description: "Help us to improve by reporting a bug" 3 | labels: ["bug", "0. to triage"] 4 | body: 5 | - type: textarea 6 | id: reproduce 7 | attributes: 8 | label: Steps to reproduce 9 | description: | 10 | Describe the steps to reproduce the bug. 11 | The better your description is _(go 'here', click 'there'...)_ the fastest you'll get an _(accurate)_ answer. 12 | value: | 13 | 1. 14 | 2. 15 | 3. 16 | validations: 17 | required: true 18 | - type: textarea 19 | id: Expected-behavior 20 | attributes: 21 | label: Expected behavior 22 | description: | 23 | Tell us what should happen 24 | validations: 25 | required: true 26 | - type: textarea 27 | id: actual-behavior 28 | attributes: 29 | label: Actual behavior 30 | description: Tell us what happens instead 31 | validations: 32 | required: true 33 | - type: input 34 | id: app-version 35 | attributes: 36 | label: App version 37 | description: | 38 | See apps admin page, e.g. 0.5.3 39 | - type: textarea 40 | id: additional-info 41 | attributes: 42 | label: Additional info 43 | description: Any additional information related to the issue (ex. browser console errors, software versions). 44 | -------------------------------------------------------------------------------- /lib/Db/BuildingModel.php: -------------------------------------------------------------------------------- 1 | addType('displayName', 'string'); 44 | $this->addType('description', 'string'); 45 | $this->addType('address', 'string'); 46 | $this->addType('isWheelchairAccessible', 'boolean'); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 5 | # Changelog 6 | All notable changes to this project will be documented in this file. 7 | 8 | ## 0.10.0 - 2025-09-01 9 | ### Added 10 | - Support for Nextcloud 32 11 | 12 | ## 0.9.0 - 2025-02-17 13 | ### Added 14 | - Support for Nextcloud 31 15 | - Support for PHP 8.4 16 | ### Fixed 17 | - Validate uids of rooms, resources and vehicles 18 | 19 | ## 0.8.0 - 2024-07-29 20 | ### Added 21 | - Update rooms and resources instantly (only on Nextcloud 30) 22 | - Support for Nextcloud 30 23 | ### Removed 24 | - Support for Nextcloud 26 25 | - Support for Nextcloud 27 26 | 27 | ## 0.7.0 - 2024-03-26 28 | ### Added 29 | - Support for Nextcloud 29 30 | - Support for PHP 8.3 31 | ### Removed 32 | - Support for Nextcloud 25 33 | - Support for PHP 7.4 34 | 35 | ## 0.6.0 - 2023-12-12 36 | ### Added 37 | - Support for Nextcloud 28 38 | 39 | ## 0.5.0 - 2023-05-17 40 | ### Added 41 | - Support for Nextcloud 27 42 | - Support for PHP 7.4 43 | ### Removed 44 | - Support for Nextcloud 24 45 | ### Changed 46 | - Command to create resources now uses optional parameters 47 | 48 | ## 0.4.0 - 2023-02-01 49 | ### Added 50 | - Support for Nextcloud 26 51 | - Support for PHP 8.2 52 | ### Removed 53 | - Support for Nextcloud 23 (EOL) 54 | - Support for PHP 7.4 (EOL) 55 | ### Changed 56 | - Migrate to new backend registrations 57 | - Use composers authoritative classmap 58 | 59 | ## 0.3.1 - 2022-09-09 60 | ### Added 61 | - Add krankerl.toml 62 | 63 | ## 0.3.0 - 2022-09-09 64 | ### Removed 65 | - Support for Nextcloud 22 66 | -------------------------------------------------------------------------------- /lib/AppInfo/Application.php: -------------------------------------------------------------------------------- 1 | registerCalendarResourceBackend(Connector\Resource\Backend::class); 40 | $context->registerCalendarRoomBackend(Connector\Room\Backend::class); 41 | $context->registerEventListener(GroupDeletedEvent::class, GroupDeletedListener::class); 42 | $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class); 43 | } 44 | 45 | /** 46 | * @inheritDoc 47 | */ 48 | public function boot(IBootContext $context): void { 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/Db/VehicleModel.php: -------------------------------------------------------------------------------- 1 | addType('vehicleType', 'string'); 53 | $this->addType('vehicleMake', 'string'); 54 | $this->addType('vehicleModel', 'string'); 55 | $this->addType('isElectric', 'boolean'); 56 | $this->addType('range', 'integer'); 57 | $this->addType('seatingCapacity', 'integer'); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/Listener/UserDeletedListener.php: -------------------------------------------------------------------------------- 1 | resourceMapper = $resourceMapper; 41 | $this->roomMapper = $roomMapper; 42 | $this->vehicleMapper = $vehicleMapper; 43 | } 44 | 45 | /** 46 | * @inheritDoc 47 | */ 48 | public function handle(Event $event): void { 49 | if (!($event instanceof UserDeletedEvent)) { 50 | return; 51 | } 52 | 53 | $this->resourceMapper->removeContactUserId($event->getUser()->getUID()); 54 | $this->roomMapper->removeContactUserId($event->getUser()->getUID()); 55 | $this->vehicleMapper->removeContactUserId($event->getUser()->getUID()); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/Db/ResourceModel.php: -------------------------------------------------------------------------------- 1 | addType('uid', 'string'); 56 | $this->addType('buildingId', 'integer'); 57 | $this->addType('displayName', 'string'); 58 | $this->addType('email', 'string'); 59 | $this->addType('resourceType', 'string'); 60 | $this->addType('contactPersonUserId', 'string'); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /appinfo/info.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 8 | calendar_resource_management 9 | Calendar Resource Management 10 | Management for calendar resources and rooms 11 | 12 | 0.11.0-dev.1 13 | agpl 14 | Hamza Mahjoubi 15 | Nextcloud Groupware Team 16 | CalendarResourceManagement 17 | 18 | 19 | 20 | office 21 | organization 22 | https://github.com/nextcloud/calendar_resource_management 23 | 24 | 25 | 26 | 27 | 28 | OCA\CalendarResourceManagement\Command\CreateBuilding 29 | OCA\CalendarResourceManagement\Command\CreateResource 30 | OCA\CalendarResourceManagement\Command\CreateRestriction 31 | OCA\CalendarResourceManagement\Command\CreateRoom 32 | OCA\CalendarResourceManagement\Command\CreateStory 33 | OCA\CalendarResourceManagement\Command\CreateVehicle 34 | OCA\CalendarResourceManagement\Command\ListResources 35 | OCA\CalendarResourceManagement\Command\DeleteResource 36 | 37 | 38 | -------------------------------------------------------------------------------- /.github/workflows/lint-php-cs.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Lint php-cs 10 | 11 | on: pull_request 12 | 13 | permissions: 14 | contents: read 15 | 16 | concurrency: 17 | group: lint-php-cs-${{ github.head_ref || github.run_id }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | lint: 22 | runs-on: ubuntu-latest 23 | 24 | name: php-cs 25 | 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 29 | with: 30 | persist-credentials: false 31 | 32 | - name: Get php version 33 | id: versions 34 | uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1 35 | 36 | - name: Set up php${{ steps.versions.outputs.php-min }} 37 | uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5 38 | with: 39 | php-version: ${{ steps.versions.outputs.php-min }} 40 | extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite 41 | coverage: none 42 | ini-file: development 43 | env: 44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | 46 | - name: Install dependencies 47 | run: | 48 | composer remove nextcloud/ocp --dev --no-scripts 49 | composer i 50 | 51 | - name: Lint 52 | run: composer run cs:check || ( echo 'Please run `composer run cs:fix` to format your code' && exit 1 ) 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: AGPL-3.0-or-later 3 | ### Intellij ### 4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 5 | 6 | ## Directory-based project format 7 | .idea/ 8 | /*.iml 9 | # if you remove the above rule, at least ignore user-specific stuff: 10 | # .idea/workspace.xml 11 | # .idea/tasks.xml 12 | # .idea/dictionaries 13 | # and these sensitive or high-churn files: 14 | # .idea/dataSources.ids 15 | # .idea/dataSources.xml 16 | # .idea/sqlDataSources.xml 17 | # .idea/dynamic.xml 18 | # and, if using gradle:: 19 | # .idea/gradle.xml 20 | # .idea/libraries 21 | 22 | ## File-based project format 23 | *.ipr 24 | *.iws 25 | 26 | ## Additional for IntelliJ 27 | out/ 28 | 29 | # generated by mpeltonen/sbt-idea plugin 30 | .idea_modules/ 31 | 32 | # generated by JIRA plugin 33 | atlassian-ide-plugin.xml 34 | 35 | # generated by Crashlytics plugin (for Android Studio and Intellij) 36 | com_crashlytics_export_strings.xml 37 | 38 | 39 | ### OSX ### 40 | .DS_Store 41 | .AppleDouble 42 | .LSOverride 43 | 44 | # Icon must end with two \r 45 | Icon 46 | 47 | 48 | # Thumbnails 49 | ._* 50 | 51 | # Files that might appear on external disk 52 | .Spotlight-V100 53 | .Trashes 54 | 55 | # Directories potentially created on remote AFP share 56 | .AppleDB 57 | .AppleDesktop 58 | Network Trash Folder 59 | Temporary Items 60 | .apdisk 61 | 62 | ### Sass ### 63 | build/.sass-cache/ 64 | 65 | ### Composer ### 66 | composer.phar 67 | vendor/ 68 | 69 | # vim ex mode 70 | .vimrc 71 | 72 | # kdevelop 73 | .kdev 74 | *.kdev4 75 | 76 | build/ 77 | js/ 78 | node_modules/ 79 | src/fonts 80 | *.clover 81 | 82 | # just sane ignores 83 | .*.sw[po] 84 | *.bak 85 | *.BAK 86 | *~ 87 | *.orig 88 | *.class 89 | .cvsignore 90 | Thumbs.db 91 | *.py[co] 92 | _darcs/* 93 | CVS/* 94 | .svn/* 95 | RCS/* 96 | 97 | /.project 98 | .php_cs.cache 99 | .php-cs-fixer.cache 100 | 101 | coverage/ 102 | 103 | js/public 104 | css/public 105 | -------------------------------------------------------------------------------- /lib/Connector/Resource/Vehicle.php: -------------------------------------------------------------------------------- 1 | entity->getVehicleType()) { 26 | $keys[] = IResourceMetadata::VEHICLE_TYPE; 27 | } 28 | if ($this->entity->getVehicleMake()) { 29 | $keys[] = IResourceMetadata::VEHICLE_MAKE; 30 | } 31 | if ($this->entity->getVehicleModel()) { 32 | $keys[] = IResourceMetadata::VEHICLE_MODEL; 33 | } 34 | $keys[] = IResourceMetadata::VEHICLE_IS_ELECTRIC; 35 | if ($this->entity->getRange() !== null) { 36 | $keys[] = IResourceMetadata::VEHICLE_RANGE; 37 | } 38 | if ($this->entity->getSeatingCapacity() !== null) { 39 | $keys[] = IResourceMetadata::VEHICLE_SEATING_CAPACITY; 40 | } 41 | 42 | return $keys; 43 | } 44 | 45 | /** 46 | * @param string $key 47 | * @return string|null 48 | */ 49 | public function getMetadataForKey(string $key): ?string { 50 | switch ($key) { 51 | case IResourceMetadata::VEHICLE_TYPE: 52 | return $this->entity->getVehicleType(); 53 | 54 | case IResourceMetadata::VEHICLE_MAKE: 55 | return $this->entity->getVehicleMake(); 56 | 57 | case IResourceMetadata::VEHICLE_MODEL: 58 | return $this->entity->getVehicleModel(); 59 | 60 | case IResourceMetadata::VEHICLE_IS_ELECTRIC: 61 | return $this->entity->getIsElectric() 62 | ? '1' 63 | : '0'; 64 | 65 | case IResourceMetadata::VEHICLE_RANGE: 66 | return (string)$this->entity->getRange(); 67 | 68 | case IResourceMetadata::VEHICLE_SEATING_CAPACITY: 69 | return (string)$this->entity->getSeatingCapacity(); 70 | 71 | default: 72 | return parent::getMetadataForKey($key); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /.github/workflows/lint-php.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Lint php 10 | 11 | on: pull_request 12 | 13 | permissions: 14 | contents: read 15 | 16 | concurrency: 17 | group: lint-php-${{ github.head_ref || github.run_id }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | matrix: 22 | runs-on: ubuntu-latest-low 23 | outputs: 24 | php-versions: ${{ steps.versions.outputs.php-versions }} 25 | steps: 26 | - name: Checkout app 27 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 28 | - name: Get version matrix 29 | id: versions 30 | uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1 31 | 32 | php-lint: 33 | runs-on: ubuntu-latest 34 | needs: matrix 35 | strategy: 36 | matrix: 37 | php-versions: ${{fromJson(needs.matrix.outputs.php-versions)}} 38 | 39 | name: php-lint 40 | 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 44 | 45 | - name: Set up php ${{ matrix.php-versions }} 46 | uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5 47 | with: 48 | php-version: ${{ matrix.php-versions }} 49 | extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite 50 | coverage: none 51 | ini-file: development 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 54 | 55 | - name: Lint 56 | run: composer run lint 57 | 58 | summary: 59 | permissions: 60 | contents: none 61 | runs-on: ubuntu-latest-low 62 | needs: php-lint 63 | 64 | if: always() 65 | 66 | name: php-lint-summary 67 | 68 | steps: 69 | - name: Summary status 70 | run: if ${{ needs.php-lint.result != 'success' && needs.php-lint.result != 'skipped' }}; then exit 1; fi 71 | -------------------------------------------------------------------------------- /lib/Command/CreateStory.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 34 | $this->storyMapper = $storyMapper; 35 | } 36 | 37 | /** 38 | * @return void 39 | */ 40 | protected function configure() { 41 | $this->setName('calendar-resource:story:create'); 42 | $this->setDescription('Create a story resource'); 43 | $this->addArgument( 44 | self::BUILDING_ID, 45 | InputArgument::REQUIRED, 46 | 'ID of the building, e.g. 17' 47 | ); 48 | $this->addArgument( 49 | self::DISPLAY_NAME, 50 | InputArgument::REQUIRED, 51 | 'Name of the floor, e.g. "2"' 52 | ); 53 | } 54 | 55 | /** 56 | * @return int 57 | */ 58 | protected function execute(InputInterface $input, OutputInterface $output): int { 59 | $displayName = (string)$input->getArgument(self::DISPLAY_NAME); 60 | $building = (int)$input->getArgument(self::BUILDING_ID); 61 | 62 | $storyModel = new StoryModel(); 63 | $storyModel->setDisplayName($displayName); 64 | $storyModel->setBuildingId($building); 65 | 66 | try { 67 | $inserted = $this->storyMapper->insert($storyModel); 68 | $output->writeln('Created new Story with ID:'); 69 | $output->writeln('' . $inserted->getId() . ''); 70 | } catch (Exception $e) { 71 | $this->logger->error($e->getMessage(), ['exception' => $e]); 72 | $output->writeln('Could not create entry: ' . $e->getMessage() . ''); 73 | return 1; 74 | } 75 | 76 | return 0; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /lib/Db/VehicleMapper.php: -------------------------------------------------------------------------------- 1 | findAllByFilter([ 42 | ['building_id', $buildingId, IQueryBuilder::PARAM_INT], 43 | ], $orderBy, $ascending, $limit, $offset); 44 | } 45 | 46 | /** 47 | * @param string $vehicleType 48 | * @param string $orderBy 49 | * @param bool $ascending 50 | * @param int|null $limit 51 | * @param int|null $offset 52 | * @return array 53 | */ 54 | public function findAllByVehicleType(string $vehicleType, 55 | string $orderBy = 'display_name', 56 | bool $ascending = true, 57 | ?int $limit = null, 58 | ?int $offset = null):array { 59 | return $this->findAllByFilter([ 60 | ['vehicle_type', $vehicleType, IQueryBuilder::PARAM_STR], 61 | ], $orderBy, $ascending, $limit, $offset); 62 | } 63 | 64 | /** 65 | * @param int $buildingId 66 | * @param string $vehicleType 67 | * @param string $orderBy 68 | * @param bool $ascending 69 | * @param int|null $limit 70 | * @param int|null $offset 71 | * @return array 72 | */ 73 | public function findAllByBuildingAndVehicleType(int $buildingId, 74 | string $vehicleType, 75 | string $orderBy = 'display_name', 76 | bool $ascending = true, 77 | ?int $limit = null, 78 | ?int $offset = null): array { 79 | return $this->findAllByFilter([ 80 | ['building_id', $buildingId, IQueryBuilder::PARAM_INT], 81 | ['vehicle_type', $vehicleType, IQueryBuilder::PARAM_STR], 82 | ], $orderBy, $ascending, $limit, $offset); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/Db/ResourceMapper.php: -------------------------------------------------------------------------------- 1 | findAllByFilter([ 42 | ['building_id', $buildingId, IQueryBuilder::PARAM_INT], 43 | ], $orderBy, $ascending, $limit, $offset); 44 | } 45 | 46 | /** 47 | * @param string $resourceType 48 | * @param string $orderBy 49 | * @param bool $ascending 50 | * @param int|null $limit 51 | * @param int|null $offset 52 | * @return array 53 | */ 54 | public function findAllByResourceType(string $resourceType, 55 | string $orderBy = 'display_name', 56 | bool $ascending = true, 57 | ?int $limit = null, 58 | ?int $offset = null):array { 59 | return $this->findAllByFilter([ 60 | ['resource_type', $resourceType, IQueryBuilder::PARAM_STR], 61 | ], $orderBy, $ascending, $limit, $offset); 62 | } 63 | 64 | /** 65 | * @param int $buildingId 66 | * @param string $resourceType 67 | * @param string $orderBy 68 | * @param bool $ascending 69 | * @param int|null $limit 70 | * @param int|null $offset 71 | * @return array 72 | */ 73 | public function findAllByBuildingAndResourceType(int $buildingId, 74 | string $resourceType, 75 | string $orderBy = 'display_name', 76 | bool $ascending = true, 77 | ?int $limit = null, 78 | ?int $offset = null): array { 79 | return $this->findAllByFilter([ 80 | ['building_id', $buildingId, IQueryBuilder::PARAM_INT], 81 | ['resource_type', $resourceType, IQueryBuilder::PARAM_STR], 82 | ], $orderBy, $ascending, $limit, $offset); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/Connector/Room/Backend.php: -------------------------------------------------------------------------------- 1 | mapper->findByUID($id); 59 | } catch (DoesNotExistException $ex) { 60 | return null; 61 | } catch (\Exception $ex) { 62 | $this->logger->error('Could not fetch room with id ' . $id, ['exception' => $ex]); 63 | throw new BackendTemporarilyUnavailableException($ex->getMessage()); 64 | } 65 | 66 | $restrictions = $this->restrictionMapper->findAllByEntityTypeAndId('room', $room->getId()); 67 | 68 | try { 69 | $story = $this->storyMapper->find($room->getStoryId()); 70 | $building = $this->buildingMapper->find($story->getBuildingId()); 71 | } catch (\Exception $ex) { 72 | $this->logger->error($ex->getMessage(), ['exception' => $ex]); 73 | throw new BackendTemporarilyUnavailableException($ex->getMessage()); 74 | } 75 | 76 | return new Room($room, $story, $building, $restrictions, $this); 77 | } 78 | 79 | /** 80 | * @return String[] 81 | */ 82 | public function listAllRooms(): array { 83 | return $this->mapper->findAllUIDs(); 84 | } 85 | 86 | /** 87 | * @return string 88 | */ 89 | public function getBackendIdentifier(): string { 90 | return $this->appName; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /.github/workflows/pr-feedback.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | 6 | # SPDX-FileCopyrightText: 2023-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-FileCopyrightText: 2023 Marcel Klehr 8 | # SPDX-FileCopyrightText: 2023 Joas Schilling <213943+nickvergessen@users.noreply.github.com> 9 | # SPDX-FileCopyrightText: 2023 Daniel Kesselberg 10 | # SPDX-FileCopyrightText: 2023 Florian Steffens 11 | # SPDX-License-Identifier: MIT 12 | 13 | name: 'Ask for feedback on PRs' 14 | on: 15 | schedule: 16 | - cron: '30 1 * * *' 17 | 18 | jobs: 19 | pr-feedback: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: The get-github-handles-from-website action 23 | uses: marcelklehr/get-github-handles-from-website-action@06b2239db0a48fe1484ba0bfd966a3ab81a08308 # v1.0.1 24 | id: scrape 25 | with: 26 | website: 'https://nextcloud.com/team/' 27 | 28 | - name: Get blocklist 29 | id: blocklist 30 | run: | 31 | blocklist=$(curl https://raw.githubusercontent.com/nextcloud/.github/master/non-community-usernames.txt | paste -s -d, -) 32 | echo "blocklist=$blocklist" >> "$GITHUB_OUTPUT" 33 | 34 | - uses: marcelklehr/pr-feedback-action@e397f3c7e655092b746e3610d121545530c6a90e 35 | with: 36 | feedback-message: | 37 | Hello there, 38 | Thank you so much for taking the time and effort to create a pull request to our Nextcloud project. 39 | 40 | We hope that the review process is going smooth and is helpful for you. We want to ensure your pull request is reviewed to your satisfaction. If you have a moment, our community management team would very much appreciate your feedback on your experience with this PR review process. 41 | 42 | Your feedback is valuable to us as we continuously strive to improve our community developer experience. Please take a moment to complete our short survey by clicking on the following link: https://cloud.nextcloud.com/apps/forms/s/i9Ago4EQRZ7TWxjfmeEpPkf6 43 | 44 | Thank you for contributing to Nextcloud and we hope to hear from you soon! 45 | 46 | (If you believe you should not receive this message, you can add yourself to the [blocklist](https://github.com/nextcloud/.github/blob/master/non-community-usernames.txt).) 47 | days-before-feedback: 14 48 | start-date: '2024-04-30' 49 | exempt-authors: '${{ steps.blocklist.outputs.blocklist }},${{ steps.scrape.outputs.users }}' 50 | exempt-bots: true 51 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:recommended", 5 | "helpers:pinGitHubActionDigests", 6 | ":dependencyDashboard", 7 | ":semanticCommits", 8 | ":gitSignOff" 9 | ], 10 | "timezone": "Europe/Berlin", 11 | "schedule": [ 12 | "before 5am on monday" 13 | ], 14 | "labels": [ 15 | "dependencies", 16 | "3. to review" 17 | ], 18 | "commitMessageAction": "Bump", 19 | "commitMessageTopic": "{{depName}}", 20 | "commitMessageExtra": "from {{currentVersion}} to {{#if isPinDigest}}{{{newDigestShort}}}{{else}}{{#if isMajor}}{{prettyNewMajor}}{{else}}{{#if isSingleVersion}}{{prettyNewVersion}}{{else}}{{#if newValue}}{{{newValue}}}{{else}}{{{newDigestShort}}}{{/if}}{{/if}}{{/if}}{{/if}}", 21 | "rangeStrategy": "bump", 22 | "rebaseWhen": "conflicted", 23 | "ignoreUnstable": false, 24 | "baseBranchPatterns": [ 25 | "main", 26 | "stable0.7" 27 | ], 28 | "enabledManagers": [ 29 | "composer", 30 | "github-actions" 31 | ], 32 | "ignoreDeps": [ 33 | "php" 34 | ], 35 | "packageRules": [ 36 | { 37 | "description": "Request PHP reviews", 38 | "matchManagers": [ 39 | "composer" 40 | ], 41 | "reviewers": [ 42 | "st3iny", 43 | "SebastianKrupinski" 44 | ] 45 | }, 46 | { 47 | "description": "Bump Github actions monthly and request reviews", 48 | "matchManagers": [ 49 | "github-actions" 50 | ], 51 | "extends": [ 52 | "schedule:monthly" 53 | ], 54 | "reviewers": [ 55 | "st3iny", 56 | "SebastianKrupinski" 57 | ] 58 | }, 59 | { 60 | "matchUpdateTypes": [ 61 | "minor", 62 | "patch" 63 | ], 64 | "matchCurrentVersion": "!/^0/", 65 | "automerge": true, 66 | "automergeType": "pr", 67 | "platformAutomerge": true, 68 | "labels": [ 69 | "dependencies", 70 | "4. to release" 71 | ], 72 | "reviewers": [] 73 | }, 74 | { 75 | "description": "Only automerge packages that follow semver", 76 | "matchPackageNames": [ 77 | "friendsofphp/php-cs-fixer" 78 | ], 79 | "automerge": false, 80 | "labels": [ 81 | "dependencies", 82 | "3. to review" 83 | ], 84 | "reviewers": [ 85 | "st3iny", 86 | "miaulalala" 87 | ] 88 | }, 89 | { 90 | "enabled": false, 91 | "matchBaseBranches": "/^stable(.)+/" 92 | }, 93 | { 94 | "matchBaseBranches": [ 95 | "main" 96 | ], 97 | "matchDepTypes": [ 98 | "devDependencies" 99 | ], 100 | "extends": [ 101 | "schedule:monthly" 102 | ] 103 | } 104 | ], 105 | "vulnerabilityAlerts": { 106 | "enabled": true, 107 | "semanticCommitType": "fix", 108 | "schedule": "before 7am every weekday", 109 | "dependencyDashboardApproval": false, 110 | "commitMessageSuffix": "" 111 | }, 112 | "osvVulnerabilityAlerts": true 113 | } 114 | -------------------------------------------------------------------------------- /lib/Command/CreateRestriction.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 42 | $this->restrictionMapper = $restrictionMapper; 43 | } 44 | 45 | /** 46 | * @return void 47 | */ 48 | protected function configure() { 49 | $this->setName('calendar-resource:restriction:create'); 50 | $this->setDescription('Create a restriction on a resource'); 51 | $this->addArgument(self::ENTITY_TYPE, InputArgument::REQUIRED); 52 | $this->addArgument(self::ENTITY_ID, InputArgument::REQUIRED); 53 | $this->addArgument(self::GROUP_ID, InputArgument::REQUIRED); 54 | } 55 | 56 | /** 57 | * @return int 58 | */ 59 | protected function execute(InputInterface $input, OutputInterface $output): int { 60 | $entityType = (string)$input->getArgument(self::ENTITY_TYPE); 61 | $entityId = (int)$input->getArgument(self::ENTITY_ID); 62 | $groupId = (string)$input->getArgument(self::GROUP_ID); 63 | 64 | $restrictionModel = new RestrictionModel(); 65 | $restrictionModel->setEntityType($entityType); 66 | $restrictionModel->setEntityId($entityId); 67 | $restrictionModel->setGroupId($groupId); 68 | 69 | try { 70 | $inserted = $this->restrictionMapper->insert($restrictionModel); 71 | $output->writeln('Created new Restriction with ID:'); 72 | $output->writeln('' . $inserted->getId() . ''); 73 | } catch (Exception $e) { 74 | $this->logger->error($e->getMessage(), ['exception' => $e]); 75 | $output->writeln('Could not create entry: ' . $e->getMessage() . ''); 76 | return 1; 77 | } 78 | 79 | switch ($entityType) { 80 | case 'vehicle': 81 | case 'resource': 82 | if (method_exists($this->resourceManager, 'update')) { 83 | $this->resourceManager->update(); 84 | } 85 | break; 86 | case 'room': 87 | if (method_exists($this->roomManager, 'update')) { 88 | $this->roomManager->update(); 89 | } 90 | break; 91 | } 92 | 93 | return 0; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /lib/Connector/Resource/ResourceObject.php: -------------------------------------------------------------------------------- 1 | entity = $entity; 46 | $this->restrictions = $restrictions; 47 | $this->backend = $backend; 48 | } 49 | 50 | /** 51 | * @return IBackend 52 | */ 53 | public function getBackend(): IBackend { 54 | return $this->backend; 55 | } 56 | 57 | /** 58 | * @return string 59 | */ 60 | public function getDisplayName(): string { 61 | return $this->entity->getDisplayName(); 62 | } 63 | 64 | /** 65 | * @return string 66 | */ 67 | public function getEMail(): string { 68 | return $this->entity->getEmail(); 69 | } 70 | 71 | /** 72 | * @return array 73 | */ 74 | public function getGroupRestrictions(): array { 75 | return $this->restrictions; 76 | } 77 | 78 | /** 79 | * @return string 80 | */ 81 | public function getId(): string { 82 | return $this->entity->getUid(); 83 | } 84 | 85 | /** 86 | * @return array 87 | */ 88 | public function getAllAvailableMetadataKeys(): array { 89 | $keys = []; 90 | 91 | if ($this->entity->getResourceType()) { 92 | $keys[] = IResourceMetadata::RESOURCE_TYPE; 93 | } 94 | if ($this->entity->getContactPersonUserId()) { 95 | $keys[] = IResourceMetadata::CONTACT_PERSON; 96 | } 97 | 98 | return $keys; 99 | } 100 | 101 | /** 102 | * @param string $key 103 | * @return string|null 104 | */ 105 | public function getMetadataForKey(string $key): ?string { 106 | switch ($key) { 107 | case IResourceMetadata::RESOURCE_TYPE: 108 | return $this->entity->getResourceType(); 109 | 110 | case IResourceMetadata::CONTACT_PERSON: 111 | return $this->entity->getContactPersonUserId(); 112 | 113 | default: 114 | return null; 115 | } 116 | } 117 | 118 | /** 119 | * @param string $key 120 | * @return bool 121 | */ 122 | public function hasMetadataForKey(string $key): bool { 123 | return \in_array($key, $this->getAllAvailableMetadataKeys(), true); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 5 | # Calendar Resource Management 6 | 7 | [![REUSE status](https://api.reuse.software/badge/github.com/nextcloud/calendar_resource_management)](https://api.reuse.software/info/github.com/nextcloud/calendar_resource_management) 8 | 9 | This app enables the 🗓️ [Calendar](https://github.com/nextcloud/calendar) App to work with resources and rooms 10 | 11 | ## Installation 12 | 13 | ### Obtain the latest pre-release build 14 | 15 | Builds are available at https://github.com/nextcloud-releases/calendar_resource_management/releases. 16 | 17 | Download and extract `calendar_resource_management.tar.gz` into `nextcloud/apps/`. 18 | 19 | ### Activate it within the apps menu 20 | 21 | ## Configuration 22 | 23 | All boolean fields default to false if not specified 24 | 25 | | Command | Description | Arguments (required) | Options | Associated Table | Notes | 26 | |---|---|---|---|---|---| 27 | | calendar-resource:building:create | Create a building resource | `display_name` | `--address` `--description` `--wheelchair-accessible` | `calresources_building` | | 28 | | calendar-resource:story:create | Create a story resource | `building_id` `display_name` | | `calresources_stories` | Needs an associated building id | 29 | | calendar-resource:room:create | Create a room resource | `story_id` `uid` `display_name` `email` `room_type` | `--contact-person-user-id` `--capacity` `--room-number` `--has-phone` `--has-video-conferencing` `--has-tv` `--has-projector` `--has-whiteboard` `--wheelchair-accessible` | `calresources_rooms` | Needs an associated story id | 30 | | calendar-resource:restriction:create | Create a restriction on a resource | `entity_type` `entity_id` `group_id` | | `calresources_restricts` | This restricts a resource to a group | 31 | | calendar-resource:resource:create | Create a general resource | `uid` `building_id` `display_name` `email` `resource_type` | `--contact-person-user-id` | `calresources_resources` | Needs an associated building id | 32 | | calendar-resource:vehicle:create | Create a vehicle resource | `uid` `building_id` `display_name` `email` `vehicle_type` `vehicle_make` `vehicle_model` | `--contact-person-user-id` `--is-electric` `--range` `--seating-capacity` | `calresources_vehicles` | Needs an associated building id | 33 | | calendar-resource:resources:list | List all resources | | | | | 34 | | calendar-resource:resource:delete | Delete a resource and anything that belongs to them | `resource_type` `id` | | | | 35 | 36 | ### Example for creating a room 37 | 38 | ``` 39 | php occ calendar-resource:building:create --address="Testweg 23, 12345 Berlin, Germany" "SpaceZ office Berlin" 40 | php occ calendar-resource:story:create 1 "2nd floor" 41 | php occ calendar-resource:room:create --wheelchair-accessible=1 --capacity=25 --room-number=201 1 "demouser" "berlin_main_office" "room.berlin.main@spacexyz.com" "Shared office" 42 | ``` 43 | 44 | CAVEAT: Each room needs a unique email address. A common workaround is to use fake email addresses like "room0001@none". 45 | Ref https://github.com/nextcloud/calendar_resource_management/issues/119#issuecomment-2114275319 46 | 47 | The resources will be added to the calendar app via cron. 48 | 49 | Any create command will return the ID of the created resource as the last line. 50 | -------------------------------------------------------------------------------- /lib/Db/RoomModel.php: -------------------------------------------------------------------------------- 1 | addType('storyId', 'integer'); 94 | $this->addType('uid', 'string'); 95 | $this->addType('displayName', 'string'); 96 | $this->addType('email', 'string'); 97 | $this->addType('roomType', 'string'); 98 | $this->addType('contactPersonUserId', 'string'); 99 | $this->addType('capacity', 'integer'); 100 | $this->addType('roomNumber', 'string'); 101 | $this->addType('hasPhone', 'boolean'); 102 | $this->addType('hasVideoConferencing', 'boolean'); 103 | $this->addType('hasTv', 'boolean'); 104 | $this->addType('hasProjector', 'boolean'); 105 | $this->addType('hasWhiteboard', 'boolean'); 106 | $this->addType('isWheelchairAccessible', 'boolean'); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /lib/Command/CreateBuilding.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 37 | $this->buildingMapper = $buildingMapper; 38 | } 39 | 40 | /** 41 | * @return void 42 | */ 43 | protected function configure() { 44 | $this->setName('calendar-resource:building:create'); 45 | $this->setDescription('Create a building resource'); 46 | $this->addArgument( 47 | self::DISPLAY_NAME, 48 | InputArgument::REQUIRED, 49 | 'The name of this building, e.g. Berlin HQ' 50 | ); 51 | $this->addOption( 52 | self::ADDRESS, 53 | null, 54 | InputOption::VALUE_REQUIRED, 55 | 'The address of the building, e.g. "Gerichtstraße 23, 13347 Berlin, Germany"' 56 | ); 57 | $this->addOption( 58 | self::DESCRIPTION, 59 | null, 60 | InputOption::VALUE_REQUIRED, 61 | 'An optional description of the building' 62 | ); 63 | $this->addOption( 64 | self::WHEELCHAIR, 65 | null, 66 | InputOption::VALUE_REQUIRED, 67 | 'Is this building wheelchair accessible? 0 (no) or 1 (yes)', 68 | '0' // Defaults to 0 to not wrongly advertise a building with barriers with default arguments 69 | ); 70 | } 71 | 72 | /** 73 | * @return int 74 | */ 75 | protected function execute(InputInterface $input, OutputInterface $output): int { 76 | $displayName = (string)$input->getArgument(self::DISPLAY_NAME); 77 | $description = (string)$input->getOption(self::DESCRIPTION); 78 | $address = (string)$input->getOption(self::ADDRESS); 79 | $wheelchair = (bool)$input->getOption(self::WHEELCHAIR); 80 | 81 | $buildingModel = new BuildingModel(); 82 | $buildingModel->setDisplayName($displayName); 83 | $buildingModel->setAddress($address); 84 | $buildingModel->setDescription($description); 85 | $buildingModel->setIsWheelchairAccessible($wheelchair); 86 | 87 | try { 88 | $inserted = $this->buildingMapper->insert($buildingModel); 89 | $output->writeln('Created new Building with ID:'); 90 | $output->writeln('' . $inserted->getId() . ''); 91 | } catch (Exception $e) { 92 | $this->logger->error($e->getMessage(), ['exception' => $e]); 93 | $output->writeln('Could not create entry: ' . $e->getMessage() . ''); 94 | return 1; 95 | } 96 | 97 | return 0; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /lib/Db/BuildingMapper.php: -------------------------------------------------------------------------------- 1 | db->getQueryBuilder(); 39 | 40 | $qb->select('*') 41 | ->from($this->tableName) 42 | ->where( 43 | $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) 44 | ); 45 | 46 | return $this->findEntity($qb); 47 | } 48 | 49 | /** 50 | * @param string $orderBy 51 | * @param bool $ascending 52 | * @param int|null $limit 53 | * @param int|null $offset 54 | * @return BuildingModel[] 55 | */ 56 | public function findAll(string $orderBy = 'display_name', 57 | bool $ascending = true, 58 | ?int $limit = null, 59 | ?int $offset = null): array { 60 | $qb = $this->db->getQueryBuilder(); 61 | 62 | $qb->select('*') 63 | ->from($this->tableName) 64 | ->orderBy($orderBy, $ascending ? 'ASC' : 'DESC'); 65 | 66 | if ($limit !== null) { 67 | $qb->setMaxResults($limit); 68 | } 69 | if ($offset !== null) { 70 | $qb->setFirstResult($offset); 71 | } 72 | 73 | return $this->findEntities($qb); 74 | } 75 | 76 | /** 77 | * @param string $search 78 | * @param string $orderBy 79 | * @param bool $ascending 80 | * @param int|null $limit 81 | * @param int|null $offset 82 | * @return BuildingModel[] 83 | */ 84 | public function search(string $search, 85 | string $orderBy = 'display_name', 86 | bool $ascending = true, 87 | ?int $limit = null, 88 | ?int $offset = null): array { 89 | $qb = $this->db->getQueryBuilder(); 90 | 91 | $qb->select('*') 92 | ->from($this->tableName) 93 | ->where($qb->expr()->iLike( 94 | 'display_name', 95 | $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($search) . '%', IQueryBuilder::PARAM_STR), 96 | IQueryBuilder::PARAM_STR 97 | )) 98 | ->orWhere($qb->expr()->iLike( 99 | 'description', 100 | $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($search) . '%', IQueryBuilder::PARAM_STR), 101 | IQueryBuilder::PARAM_STR 102 | )) 103 | ->orWhere($qb->expr()->iLike( 104 | 'address', 105 | $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($search) . '%', IQueryBuilder::PARAM_STR), 106 | IQueryBuilder::PARAM_STR 107 | )) 108 | ->orderBy($orderBy, $ascending ? 'ASC' : 'DESC'); 109 | 110 | if ($limit !== null) { 111 | $qb->setMaxResults($limit); 112 | } 113 | if ($offset !== null) { 114 | $qb->setFirstResult($offset); 115 | } 116 | 117 | return $this->findEntities($qb); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /lib/Connector/Resource/Backend.php: -------------------------------------------------------------------------------- 1 | getResourceEntity($id); 56 | if ($resource) { 57 | $restrictions = $this->restrictionMapper->findAllByEntityTypeAndId('resource', $resource->getId()); 58 | return new ResourceObject($resource, $restrictions, $this); 59 | } 60 | 61 | $vehicle = $this->getVehicleEntity($id); 62 | if ($vehicle) { 63 | $restrictions = $this->restrictionMapper->findAllByEntityTypeAndId('vehicle', $vehicle->getId()); 64 | return new Vehicle($vehicle, $restrictions, $this); 65 | } 66 | 67 | return null; 68 | } 69 | 70 | /** 71 | * @return String[] 72 | */ 73 | public function listAllResources(): array { 74 | return array_merge( 75 | $this->resourceMapper->findAllUIDs(), 76 | $this->vehicleMapper->findAllUIDs() 77 | ); 78 | } 79 | 80 | /** 81 | * @return string 82 | */ 83 | public function getBackendIdentifier(): string { 84 | return $this->appName; 85 | } 86 | 87 | /** 88 | * @param $uid 89 | * @return Db\ResourceModel|null 90 | * @throws BackendTemporarilyUnavailableException 91 | */ 92 | private function getResourceEntity($uid):?Db\ResourceModel { 93 | try { 94 | return $this->resourceMapper->findByUID($uid); 95 | } catch (DoesNotExistException $ex) { 96 | return null; 97 | } catch (\Exception $ex) { 98 | $this->logger->error('Could not fetch resource entity', ['exception' => $ex]); 99 | throw new BackendTemporarilyUnavailableException($ex->getMessage()); 100 | } 101 | } 102 | 103 | /** 104 | * @param $uid 105 | * @return Db\VehicleModel|null 106 | * @throws BackendTemporarilyUnavailableException 107 | */ 108 | private function getVehicleEntity($uid):?Db\VehicleModel { 109 | try { 110 | return $this->vehicleMapper->findByUID($uid); 111 | } catch (DoesNotExistException $ex) { 112 | return null; 113 | } catch (\Exception $ex) { 114 | $this->logger->error('Could not fetch vehicle entity', ['exception' => $ex]); 115 | throw new BackendTemporarilyUnavailableException($ex->getMessage()); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/Command/CreateResource.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 46 | $this->resourceMapper = $resourceMapper; 47 | } 48 | 49 | /** 50 | * @return void 51 | */ 52 | protected function configure() { 53 | $this->setName('calendar-resource:resource:create'); 54 | $this->setDescription('Create a general resource'); 55 | $this->addArgument(self::UID, InputArgument::REQUIRED); 56 | $this->addArgument(self::BUILDING_ID, InputArgument::REQUIRED); 57 | $this->addArgument(self::DISPLAY_NAME, InputArgument::REQUIRED); 58 | $this->addArgument(self::EMAIL, InputArgument::REQUIRED); 59 | $this->addArgument(self::TYPE, InputArgument::REQUIRED); 60 | $this->addOption(self::CONTACT, null, InputOption::VALUE_REQUIRED); 61 | } 62 | 63 | /** 64 | * @return int 65 | */ 66 | protected function execute(InputInterface $input, OutputInterface $output): int { 67 | $uid = (string)$input->getArgument(self::UID); 68 | $buildingId = (int)$input->getArgument(self::BUILDING_ID); 69 | $displayName = (string)$input->getArgument(self::DISPLAY_NAME); 70 | $email = (string)$input->getArgument(self::EMAIL); 71 | $type = (string)$input->getArgument(self::TYPE); 72 | $contact = (string)$input->getOption(self::CONTACT); 73 | 74 | $this->uidValidationService->validateUidAndThrow($uid); 75 | 76 | $resourceModel = new ResourceModel(); 77 | $resourceModel->setUid($uid); 78 | $resourceModel->setDisplayName($displayName); 79 | $resourceModel->setBuildingId($buildingId); 80 | $resourceModel->setEmail($email); 81 | $resourceModel->setResourceType($type); 82 | $resourceModel->setContactPersonUserId($contact); 83 | 84 | try { 85 | $inserted = $this->resourceMapper->insert($resourceModel); 86 | $output->writeln('Created new Resource with ID:'); 87 | $output->writeln('' . $inserted->getId() . ''); 88 | } catch (Exception $e) { 89 | $this->logger->error($e->getMessage(), ['exception' => $e]); 90 | $output->writeln('Could not create entry: ' . $e->getMessage() . ''); 91 | return 1; 92 | } 93 | 94 | if (method_exists($this->resourceManager, 'update')) { 95 | $this->resourceManager->update(); 96 | } 97 | 98 | return 0; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /lib/Db/StoryMapper.php: -------------------------------------------------------------------------------- 1 | db->getQueryBuilder(); 39 | 40 | $qb->select('*') 41 | ->from($this->tableName) 42 | ->where( 43 | $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) 44 | ); 45 | 46 | return $this->findEntity($qb); 47 | } 48 | 49 | /** 50 | * @param string $orderBy 51 | * @param bool $ascending 52 | * @param int|null $limit 53 | * @param int|null $offset 54 | * @return BuildingModel[] 55 | */ 56 | public function findAll(string $orderBy = 'display_name', 57 | bool $ascending = true, 58 | ?int $limit = null, 59 | ?int $offset = null): array { 60 | $qb = $this->db->getQueryBuilder(); 61 | 62 | $qb->select('*') 63 | ->from($this->tableName) 64 | ->orderBy($orderBy, $ascending ? 'ASC' : 'DESC'); 65 | 66 | if ($limit !== null) { 67 | $qb->setMaxResults($limit); 68 | } 69 | if ($offset !== null) { 70 | $qb->setFirstResult($offset); 71 | } 72 | 73 | return $this->findEntities($qb); 74 | } 75 | 76 | /** 77 | * @param int $buildingId 78 | * @param int|null $limit 79 | * @param int|null $offset 80 | * @return StoryModel[] 81 | */ 82 | public function findAllByBuilding(int $buildingId, 83 | ?int $limit = null, 84 | ?int $offset = null): array { 85 | $qb = $this->db->getQueryBuilder(); 86 | 87 | $qb->select('*') 88 | ->from($this->tableName) 89 | ->where( 90 | $qb->expr()->eq('building_id', $qb->createNamedParameter($buildingId, IQueryBuilder::PARAM_INT)) 91 | ) 92 | ->orderBy('display_name', 'ASC') 93 | ->setMaxResults($limit) 94 | ->setFirstResult($offset); 95 | 96 | return $this->findEntities($qb); 97 | } 98 | 99 | /** 100 | * @param int[] $buildingIds 101 | * @param int|null $limit 102 | * @param int|null $offset 103 | * @return StoryModel[] 104 | */ 105 | public function findAllByBuildings(array $buildingIds, 106 | ?int $limit = null, 107 | ?int $offset = null): array { 108 | $qb = $this->db->getQueryBuilder(); 109 | 110 | $qb->select('*') 111 | ->from($this->tableName) 112 | ->where( 113 | $qb->expr()->in('building_id', $qb->createNamedParameter($buildingIds, IQueryBuilder::PARAM_INT_ARRAY)) 114 | ) 115 | ->orderBy('display_name', 'ASC') 116 | ->setMaxResults($limit) 117 | ->setFirstResult($offset); 118 | 119 | return $this->findEntities($qb); 120 | } 121 | 122 | /** 123 | * @param int $buildingId 124 | */ 125 | public function deleteAllByBuildingId(int $buildingId):void { 126 | $qb = $this->db->getQueryBuilder(); 127 | 128 | $qb->delete($this->tableName) 129 | ->where( 130 | $qb->expr()->eq('building_id', $qb->createNamedParameter($buildingId, IQueryBuilder::PARAM_INT)) 131 | ) 132 | ->executeStatement(); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /lib/Command/DeleteResource.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 43 | $this->connection = $connection; 44 | } 45 | 46 | /** 47 | * @return void 48 | */ 49 | protected function configure() { 50 | $this->setName('calendar-resource:resource:delete'); 51 | $this->setDescription('Delete a resource and anything that belongs to them'); 52 | $this->addArgument( 53 | self::TYPE, 54 | InputArgument::REQUIRED, 55 | 'Type of resource (building, story, room, vehicle, resource, restriction)' 56 | ); 57 | $this->addArgument( 58 | self::ID, 59 | InputArgument::REQUIRED, 60 | 'ID of the resource to delete, e.g. 42' 61 | ); 62 | } 63 | 64 | /** 65 | * @return int 66 | */ 67 | protected function execute(InputInterface $input, OutputInterface $output): int { 68 | $type = (string)$input->getArgument(self::TYPE); 69 | $id = (int)$input->getArgument(self::ID); 70 | 71 | $mapper = AMapper::getMapper($type, $this->connection); 72 | 73 | if ($mapper === null) { 74 | $output->writeln('No such resource type found!'); 75 | return 3; 76 | } 77 | 78 | try { 79 | $entity = $mapper->find($id); 80 | } catch (DoesNotExistException|MultipleObjectsReturnedException $e) { 81 | $output->writeln('Could not find resource type ' . $type . ' with ID ' . $id . ''); 82 | return 2; 83 | } 84 | 85 | // delete cascading with FKs 86 | try { 87 | $mapper->delete($entity); 88 | $output->writeln('Deleted resource type ' . $type . ' with ID ' . $id . ' and all associated entries.'); 89 | } catch (Exception $e) { 90 | $this->logger->error($e->getMessage(), ['exception' => $e]); 91 | $output->writeln('Could not delete resource type ' . $type . ' with ID ' . $id . ': ' . $e->getMessage() . ''); 92 | return 1; 93 | } 94 | 95 | switch ($type) { 96 | case 'building': 97 | case 'story': 98 | case 'room': 99 | $this->updateRooms(); 100 | break; 101 | case 'vehicle': 102 | case 'resource': 103 | $this->updateResources(); 104 | break; 105 | default: 106 | $this->updateResources(); 107 | $this->updateRooms(); 108 | break; 109 | } 110 | 111 | return 0; 112 | } 113 | 114 | private function updateResources(): void { 115 | if (method_exists($this->resourceManager, 'update')) { 116 | $this->resourceManager->update(); 117 | } 118 | } 119 | 120 | private function updateRooms(): void { 121 | if (method_exists($this->roomManager, 'update')) { 122 | $this->roomManager->update(); 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /lib/Db/RestrictionMapper.php: -------------------------------------------------------------------------------- 1 | db->getQueryBuilder(); 46 | 47 | $qb->select('*') 48 | ->from($this->tableName) 49 | ->where( 50 | $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) 51 | ); 52 | 53 | return $this->findEntity($qb); 54 | } 55 | 56 | /** 57 | * @param string $orderBy 58 | * @param bool $ascending 59 | * @param int|null $limit 60 | * @param int|null $offset 61 | * @return BuildingModel[] 62 | */ 63 | public function findAll(string $orderBy = 'id', 64 | bool $ascending = true, 65 | ?int $limit = null, 66 | ?int $offset = null): array { 67 | $qb = $this->db->getQueryBuilder(); 68 | 69 | $qb->select('*') 70 | ->from($this->tableName) 71 | ->orderBy($orderBy, $ascending ? 'ASC' : 'DESC'); 72 | 73 | if ($limit !== null) { 74 | $qb->setMaxResults($limit); 75 | } 76 | if ($offset !== null) { 77 | $qb->setFirstResult($offset); 78 | } 79 | 80 | return $this->findEntities($qb); 81 | } 82 | 83 | /** 84 | * @param string $entityType 85 | * @param int $entityId 86 | * @param int|null $limit 87 | * @param int|null $offset 88 | * @return RestrictionModel[] 89 | */ 90 | public function findAllByEntityTypeAndId(string $entityType, 91 | int $entityId, 92 | ?int $limit = null, 93 | ?int $offset = null): array { 94 | $qb = $this->db->getQueryBuilder(); 95 | 96 | $qb->select('*') 97 | ->from($this->tableName) 98 | ->where( 99 | $qb->expr()->eq('entity_type', $qb->createNamedParameter($entityType, IQueryBuilder::PARAM_STR)) 100 | ) 101 | ->andWhere( 102 | $qb->expr()->eq('entity_id', $qb->createNamedParameter($entityId, IQueryBuilder::PARAM_INT)) 103 | ) 104 | ->orderBy('group_id', 'ASC') 105 | ->setMaxResults($limit) 106 | ->setFirstResult($offset); 107 | 108 | return $this->findEntities($qb); 109 | } 110 | 111 | /** 112 | * @param string $entityType 113 | * @param int $entityId 114 | */ 115 | public function deleteAllByEntityTypeAndId(string $entityType, 116 | int $entityId): void { 117 | $qb = $this->db->getQueryBuilder(); 118 | 119 | $qb->delete($this->tableName) 120 | ->where( 121 | $qb->expr()->eq('entity_type', $qb->createNamedParameter($entityType, IQueryBuilder::PARAM_STR)) 122 | ) 123 | ->andWhere( 124 | $qb->expr()->eq('entity_id', $qb->createNamedParameter($entityId, IQueryBuilder::PARAM_INT)) 125 | ) 126 | ->executeStatement(); 127 | } 128 | 129 | /** 130 | * @param string $groupId 131 | */ 132 | public function deleteAllRestrictionsByGroupId(string $groupId):void { 133 | $qb = $this->db->getQueryBuilder(); 134 | 135 | $qb->delete($this->tableName) 136 | ->where( 137 | $qb->expr()->eq('group_id', $qb->createNamedParameter($groupId, IQueryBuilder::PARAM_STR)) 138 | ) 139 | ->executeStatement(); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /tests/Unit/Db/BuildingMapperTest.php: -------------------------------------------------------------------------------- 1 | getQueryBuilder(); 24 | $qb->delete('calresources_buildings')->executeStatement(); 25 | 26 | $this->mapper = new BuildingMapper(self::$realDatabase); 27 | 28 | $buildings = $this->getSampleBuildings(); 29 | array_map(function ($building): void { 30 | $this->mapper->insert($building); 31 | }, $buildings); 32 | } 33 | 34 | public function testFind(): void { 35 | // Should sort alphabetically 36 | $allBuildings = $this->mapper->findAll('display_name', true, 1, 1); 37 | $this->assertEquals('Building 1', $allBuildings[0]->getDisplayName()); 38 | 39 | $building = $this->mapper->find($allBuildings[0]->getId()); 40 | $this->assertEquals('Building 1', $building->getDisplayName()); 41 | 42 | $this->expectException(DoesNotExistException::class); 43 | $this->mapper->find(-1); 44 | } 45 | 46 | public function testFindAll(): void { 47 | // Should sort alphabetically 48 | $allBuildings = $this->mapper->findAll(); 49 | $this->assertCount(5, $allBuildings); 50 | $this->assertEquals('Another Building 4', $allBuildings[0]->getDisplayName()); 51 | $this->assertEquals('Building 1', $allBuildings[1]->getDisplayName()); 52 | $this->assertEquals('Building 2', $allBuildings[2]->getDisplayName()); 53 | $this->assertEquals('Building 3', $allBuildings[3]->getDisplayName()); 54 | $this->assertEquals('Building 5', $allBuildings[4]->getDisplayName()); 55 | 56 | $allBuildings = $this->mapper->findAll('display_name', false); 57 | $this->assertCount(5, $allBuildings); 58 | $this->assertEquals('Building 5', $allBuildings[0]->getDisplayName()); 59 | $this->assertEquals('Building 3', $allBuildings[1]->getDisplayName()); 60 | $this->assertEquals('Building 2', $allBuildings[2]->getDisplayName()); 61 | $this->assertEquals('Building 1', $allBuildings[3]->getDisplayName()); 62 | $this->assertEquals('Another Building 4', $allBuildings[4]->getDisplayName()); 63 | 64 | $allBuildings = $this->mapper->findAll('display_name', true, 2, 1); 65 | $this->assertCount(2, $allBuildings); 66 | $this->assertEquals('Building 1', $allBuildings[0]->getDisplayName()); 67 | $this->assertEquals('Building 2', $allBuildings[1]->getDisplayName()); 68 | } 69 | 70 | public function testSearch(): void { 71 | $searchResults = $this->mapper->search('Another'); 72 | $this->assertCount(1, $searchResults); 73 | $this->assertEquals('Another Building 4', $searchResults[0]->getDisplayName()); 74 | 75 | $searchResults = $this->mapper->search('Foo'); 76 | $this->assertCount(1, $searchResults); 77 | $this->assertEquals('Building 2', $searchResults[0]->getDisplayName()); 78 | 79 | $searchResults = $this->mapper->search('Headquarters'); 80 | $this->assertCount(1, $searchResults); 81 | $this->assertEquals('Building 5', $searchResults[0]->getDisplayName()); 82 | 83 | $searchResults = $this->mapper->search('City'); 84 | $this->assertCount(1, $searchResults); 85 | $this->assertEquals('Building 5', $searchResults[0]->getDisplayName()); 86 | } 87 | 88 | protected function getSampleBuildings(): array { 89 | return [ 90 | BuildingModel::fromParams([ 91 | 'displayName' => 'Building 1', 92 | 'description' => 'Small offices', 93 | ]), 94 | BuildingModel::fromParams([ 95 | 'displayName' => 'Building 2', 96 | 'description' => 'Foo', 97 | ]), 98 | BuildingModel::fromParams([ 99 | 'displayName' => 'Building 3', 100 | ]), 101 | BuildingModel::fromParams([ 102 | 'displayName' => 'Another Building 4', 103 | ]), 104 | BuildingModel::fromParams([ 105 | 'displayName' => 'Building 5', 106 | 'description' => 'Headquarters', 107 | 'address' => 'Example Street 123' . PHP_EOL . '12345 Random City', 108 | 'isWheelchairAccessible' => true, 109 | ]), 110 | ]; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /tests/Unit/Db/StoryMapperTest.php: -------------------------------------------------------------------------------- 1 | getQueryBuilder(); 24 | $qb->delete('calresources_stories')->executeStatement(); 25 | 26 | $this->mapper = new StoryMapper(self::$realDatabase); 27 | 28 | $stories = $this->getSampleStories(); 29 | array_map(function ($story): void { 30 | $this->mapper->insert($story); 31 | }, $stories); 32 | } 33 | 34 | public function testFind(): void { 35 | $allStories = $this->mapper->findAllByBuilding(0); 36 | 37 | $story0 = $this->mapper->find($allStories[0]->getId()); 38 | $this->assertEquals($allStories[0]->getDisplayName(), $story0->getDisplayName()); 39 | 40 | $story1 = $this->mapper->find($allStories[1]->getId()); 41 | $this->assertEquals($allStories[1]->getDisplayName(), $story1->getDisplayName()); 42 | 43 | $this->expectException(DoesNotExistException::class); 44 | $this->mapper->find(-1); 45 | } 46 | 47 | public function testFindAllByBuilding(): void { 48 | $allStories = $this->mapper->findAllByBuilding(0); 49 | 50 | $this->assertCount(5, $allStories); 51 | 52 | $this->assertEquals('Floor 1', $allStories[0]->getDisplayName()); 53 | $this->assertEquals(0, $allStories[0]->getBuildingId()); 54 | $this->assertEquals('Floor 2', $allStories[1]->getDisplayName()); 55 | $this->assertEquals(0, $allStories[1]->getBuildingId()); 56 | $this->assertEquals('Floor 3', $allStories[2]->getDisplayName()); 57 | $this->assertEquals(0, $allStories[2]->getBuildingId()); 58 | $this->assertEquals('Floor 4', $allStories[3]->getDisplayName()); 59 | $this->assertEquals(0, $allStories[3]->getBuildingId()); 60 | $this->assertEquals('Ground-floor', $allStories[4]->getDisplayName()); 61 | $this->assertEquals(0, $allStories[4]->getBuildingId()); 62 | } 63 | 64 | public function testFindAllByBuildings(): void { 65 | $allStories = $this->mapper->findAllByBuildings([0, 2]); 66 | 67 | $this->assertCount(6, $allStories); 68 | 69 | $this->assertEquals('Floor 1', $allStories[0]->getDisplayName()); 70 | $this->assertEquals(0, $allStories[0]->getBuildingId()); 71 | $this->assertEquals('Floor 2', $allStories[1]->getDisplayName()); 72 | $this->assertEquals(0, $allStories[1]->getBuildingId()); 73 | $this->assertEquals('Floor 3', $allStories[2]->getDisplayName()); 74 | $this->assertEquals(0, $allStories[2]->getBuildingId()); 75 | $this->assertEquals('Floor 4', $allStories[3]->getDisplayName()); 76 | $this->assertEquals(0, $allStories[3]->getBuildingId()); 77 | $this->assertEquals('Ground-floor', $allStories[4]->getDisplayName()); 78 | $this->assertEquals(0, $allStories[4]->getBuildingId()); 79 | $this->assertEquals('Ground-floor', $allStories[5]->getDisplayName()); 80 | $this->assertEquals(2, $allStories[5]->getBuildingId()); 81 | } 82 | 83 | public function deleteAllByBuildingId(): void { 84 | $allStories = $this->mapper->findAllByBuildings([0, 2]); 85 | $this->assertCount(6, $allStories); 86 | 87 | $this->mapper->deleteAllByBuildingId(0); 88 | 89 | $allStories = $this->mapper->findAllByBuildings([0, 2]); 90 | $this->assertCount(1, $allStories); 91 | } 92 | 93 | protected function getSampleStories(): array { 94 | return [ 95 | StoryModel::fromParams([ 96 | 'buildingId' => 0, 97 | 'displayName' => 'Ground-floor', 98 | ]), 99 | StoryModel::fromParams([ 100 | 'buildingId' => 0, 101 | 'displayName' => 'Floor 1', 102 | ]), 103 | StoryModel::fromParams([ 104 | 'buildingId' => 0, 105 | 'displayName' => 'Floor 2', 106 | ]), 107 | StoryModel::fromParams([ 108 | 'buildingId' => 0, 109 | 'displayName' => 'Floor 3', 110 | ]), 111 | StoryModel::fromParams([ 112 | 'buildingId' => 0, 113 | 'displayName' => 'Floor 4', 114 | ]), 115 | StoryModel::fromParams([ 116 | 'buildingId' => 1, 117 | 'displayName' => 'Ground-floor', 118 | ]), 119 | StoryModel::fromParams([ 120 | 'buildingId' => 1, 121 | 'displayName' => 'Floor 1', 122 | ]), 123 | StoryModel::fromParams([ 124 | 'buildingId' => 2, 125 | 'displayName' => 'Ground-floor', 126 | ]), 127 | ]; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /.github/workflows/setup.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 2 | # SPDX-License-Identifier: MIT 3 | name: Setup 4 | on: pull_request 5 | 6 | jobs: 7 | setup: 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | php-versions: ['8.2', '8.3', '8.4'] 12 | nextcloud-versions: ['master'] 13 | db: ['mysql', 'sqlite', 'pgsql'] 14 | include: 15 | - php-versions: '8.3' 16 | db: 'sqlite' 17 | nextcloud-versions: stable30 18 | - php-versions: '8.4' 19 | db: 'sqlite' 20 | nextcloud-versions: stable31 21 | - php-versions: '8.4' 22 | db: 'sqlite' 23 | nextcloud-versions: stable32 24 | name: Nextcloud ${{ matrix.nextcloud-versions }} php${{ matrix.php-versions }} ${{ matrix.db }} setup testing 25 | services: 26 | mail-service: 27 | image: ghcr.io/christophwurst/docker-imap-devel:latest 28 | env: 29 | MAILNAME: mail.domain.tld 30 | MAIL_ADDRESS: user@domain.tld 31 | MAIL_PASS: mypassword 32 | ports: 33 | - 25:25 34 | - 143:143 35 | - 993:993 36 | - 4190:4190 37 | mysql-service: 38 | image: ghcr.io/nextcloud/continuous-integration-mariadb-10.11:latest 39 | env: 40 | MYSQL_ROOT_PASSWORD: my-secret-pw 41 | MYSQL_DATABASE: nextcloud 42 | MYSQL_USER: nextcloud 43 | MYSQL_PASSWORD: nextcloud 44 | ports: 45 | - 3306:3306 46 | options: >- 47 | --health-cmd="mysqladmin ping" 48 | --health-interval=10s 49 | --health-timeout=5s 50 | --health-retries=3 51 | postgres-service: 52 | image: ghcr.io/nextcloud/continuous-integration-postgres-14:latest 53 | env: 54 | POSTGRES_USER: nextcloud 55 | POSTGRES_DB: nextcloud 56 | POSTGRES_PASSWORD: nextcloud 57 | ports: 58 | - 5432:5432 59 | options: >- 60 | --health-cmd pg_isready 61 | --health-interval 10s 62 | --health-timeout 5s 63 | --health-retries 5 64 | steps: 65 | - name: Set up php${{ matrix.php-versions }} 66 | uses: shivammathur/setup-php@master 67 | with: 68 | php-version: ${{ matrix.php-versions }} 69 | extensions: ctype,curl,dom,gd,iconv,intl,json,mbstring,openssl,posix,sqlite,xml,zip,gmp 70 | coverage: xdebug 71 | - name: Checkout Nextcloud 72 | run: git clone https://github.com/nextcloud/server.git --recursive --depth 1 -b ${{ matrix.nextcloud-versions }} nextcloud 73 | - name: Patch version check for nightly PHP 74 | if: ${{ matrix.php-versions == '8.2' }} 75 | run: echo " nextcloud/lib/versioncheck.php 76 | - name: Install Nextcloud 77 | run: php -f nextcloud/occ maintenance:install --database-host 127.0.0.1 --database-name nextcloud --database-user nextcloud --database-pass nextcloud --admin-user admin --admin-pass admin --database ${{ matrix.db }} 78 | - name: Checkout Calendar Resource Management 79 | uses: actions/checkout@master 80 | with: 81 | path: nextcloud/apps/calendar_resource_management 82 | - name: Install dependencies 83 | working-directory: nextcloud/apps/calendar_resource_management 84 | run: composer i 85 | - name: Enable the app 86 | run: php -f nextcloud/occ app:enable calendar_resource_management 87 | - name: Create Resources 88 | run: | 89 | php -f nextcloud/occ calendar-resource:building:create --address='Amadeus Way, Gotham NG11 0AS' --description='Elizabeth Arkham Asylum for the Criminally Insane' --wheelchair-accessible=false 'Arkham Asylum' 90 | php -f nextcloud/occ calendar-resource:story:create 1 '1st floor' 91 | php -f nextcloud/occ calendar-resource:story:create 1 '2nd floor' 92 | php -f nextcloud/occ calendar-resource:room:create --contact-person-user-id='amadeus' --capacity=10 --room-number=404 --has-phone=true --has-video-conferencing=true --has-tv=false --has-projector=false --has-whiteboard=false --wheelchair-accessible=false 1 arkham_meeting_1 'The Joker' joker@arkham-asylum.com 'meeting-room' 93 | php -f nextcloud/occ calendar-resource:room:create 2 arkham_meeting_2 'Bane' bane@arkham-asylum.com 'other' 94 | summary: 95 | runs-on: ubuntu-latest 96 | needs: 97 | - setup 98 | if: always() 99 | name: Setup summary 100 | steps: 101 | - name: Setup test status 102 | run: if ${{ needs.setup.result != 'success' && needs.setup.result != 'skipped' }}; then exit 1; fi 103 | -------------------------------------------------------------------------------- /lib/Db/RoomMapper.php: -------------------------------------------------------------------------------- 1 | findAllByFilter([ 42 | ['room_type', $roomType, IQueryBuilder::PARAM_STR], 43 | ], $orderBy, $ascending, $limit, $offset); 44 | } 45 | 46 | /** 47 | * @param int $storyId 48 | * @param string $orderBy 49 | * @param bool $ascending 50 | * @param int|null $limit 51 | * @param int|null $offset 52 | * @return array 53 | */ 54 | public function findAllByStoryId(int $storyId, 55 | string $orderBy = 'display_name', 56 | bool $ascending = true, 57 | ?int $limit = null, 58 | ?int $offset = null): array { 59 | return $this->findAllByFilter([ 60 | ['story_id', $storyId, IQueryBuilder::PARAM_INT] 61 | ], $orderBy, $ascending, $limit, $offset); 62 | } 63 | 64 | /** 65 | * @param int $buildingId 66 | * @param string $orderBy 67 | * @param bool $ascending 68 | * @param int|null $limit 69 | * @param int|null $offset 70 | * @return array 71 | */ 72 | public function findAllByBuildingId(int $buildingId, 73 | string $orderBy = 'display_name', 74 | bool $ascending = true, 75 | ?int $limit = null, 76 | ?int $offset = null): array { 77 | $qb = $this->db->getQueryBuilder(); 78 | 79 | $qb->select('r.*') 80 | ->from('calresources_rooms', 'r') 81 | ->join('r', 'calresources_stories', 's', $qb->expr()->eq('r.story_id', 's.id', IQueryBuilder::PARAM_INT)) 82 | ->where( 83 | $qb->expr()->eq('s.building_id', $qb->createNamedParameter($buildingId, IQueryBuilder::PARAM_INT)) 84 | ) 85 | ->orderBy('r.' . $orderBy, $ascending ? 'ASC' : 'DESC'); 86 | 87 | 88 | if ($limit !== null) { 89 | $qb->setMaxResults($limit); 90 | } 91 | if ($offset !== null) { 92 | $qb->setFirstResult($offset); 93 | } 94 | 95 | return $this->findEntities($qb); 96 | } 97 | 98 | /** 99 | * @param string $roomType 100 | * @param int $storyId 101 | * @param string $orderBy 102 | * @param bool $ascending 103 | * @param int|null $limit 104 | * @param int|null $offset 105 | * @return array 106 | */ 107 | public function findAllByRoomTypeAndStoryId(string $roomType, 108 | int $storyId, 109 | string $orderBy = 'display_name', 110 | bool $ascending = true, 111 | ?int $limit = null, 112 | ?int $offset = null): array { 113 | return $this->findAllByFilter([ 114 | ['room_type', $roomType, IQueryBuilder::PARAM_STR], 115 | ['story_id', $storyId, IQueryBuilder::PARAM_INT], 116 | ], $orderBy, $ascending, $limit, $offset); 117 | } 118 | 119 | /** 120 | * @param string $roomType 121 | * @param int $buildingId 122 | * @param string $orderBy 123 | * @param bool $ascending 124 | * @param int|null $limit 125 | * @param int|null $offset 126 | * @return array 127 | */ 128 | public function findAllByRoomTypeAndBuildingId(string $roomType, 129 | int $buildingId, 130 | string $orderBy = 'display_name', 131 | bool $ascending = true, 132 | ?int $limit = null, 133 | ?int $offset = null): array { 134 | $qb = $this->db->getQueryBuilder(); 135 | 136 | $qb->select('r.*') 137 | ->from('calresources_rooms', 'r') 138 | ->join('calresources_stories', 's', $qb->expr()->eq('r.storyId', 's.id')) 139 | ->where( 140 | $qb->expr()->eq('s.building_id', $qb->createNamedParameter($buildingId, IQueryBuilder::PARAM_INT)) 141 | ) 142 | ->andWhere( 143 | $qb->expr()->eq('r.room_type', $qb->createNamedParameter($roomType, IQueryBuilder::PARAM_STR)) 144 | ) 145 | ->orderBy('r.' . $orderBy, $ascending ? 'ASC' : 'DESC'); 146 | 147 | 148 | if ($limit !== null) { 149 | $qb->setMaxResults($limit); 150 | } 151 | if ($offset !== null) { 152 | $qb->setFirstResult($offset); 153 | } 154 | 155 | return $this->findEntities($qb); 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /lib/Connector/Room/Room.php: -------------------------------------------------------------------------------- 1 | entity = $entity; 52 | $this->storyEntity = $storyEntity; 53 | $this->buildingEntity = $buildingEntity; 54 | $this->restrictions = $restrictions; 55 | $this->backend = $backend; 56 | } 57 | 58 | /** 59 | * @return IBackend 60 | */ 61 | public function getBackend(): IBackend { 62 | return $this->backend; 63 | } 64 | 65 | /** 66 | * @return string 67 | */ 68 | public function getDisplayName(): string { 69 | return $this->entity->getDisplayName(); 70 | } 71 | 72 | /** 73 | * @return string 74 | */ 75 | public function getEMail(): string { 76 | return $this->entity->getEmail(); 77 | } 78 | 79 | /** 80 | * @return array 81 | */ 82 | public function getGroupRestrictions(): array { 83 | return $this->restrictions; 84 | } 85 | 86 | /** 87 | * @return string 88 | */ 89 | public function getId(): string { 90 | return $this->entity->getUid(); 91 | } 92 | 93 | /** 94 | * @return array 95 | */ 96 | public function getAllAvailableMetadataKeys(): array { 97 | $keys = []; 98 | 99 | if ($this->entity->getRoomType()) { 100 | $keys[] = IRoomMetadata::ROOM_TYPE; 101 | } 102 | if ($this->entity->getCapacity()) { 103 | $keys[] = IRoomMetadata::CAPACITY; 104 | } 105 | if ($this->entity->getRoomNumber()) { 106 | $keys[] = IRoomMetadata::BUILDING_ROOM_NUMBER; 107 | } 108 | $keys[] = IRoomMetadata::BUILDING_ADDRESS; 109 | $keys[] = IRoomMetadata::BUILDING_STORY; 110 | if ($this->getFeatures() !== '') { 111 | $keys[] = IRoomMetadata::FEATURES; 112 | } 113 | 114 | return $keys; 115 | } 116 | 117 | /** 118 | * @param string $key 119 | * @return string|null 120 | */ 121 | public function getMetadataForKey(string $key): ?string { 122 | switch ($key) { 123 | case IRoomMetadata::ROOM_TYPE: 124 | return $this->entity->getRoomType(); 125 | 126 | case IRoomMetadata::CAPACITY: 127 | return (string)$this->entity->getCapacity(); 128 | 129 | case IRoomMetadata::BUILDING_ROOM_NUMBER: 130 | return $this->entity->getRoomNumber(); 131 | 132 | case IRoomMetadata::BUILDING_ADDRESS: 133 | return $this->buildingEntity->getAddress(); 134 | 135 | case IRoomMetadata::BUILDING_STORY: 136 | return $this->storyEntity->getDisplayName(); 137 | 138 | case IRoomMetadata::FEATURES: 139 | return $this->getFeatures(); 140 | 141 | default: 142 | return null; 143 | } 144 | } 145 | 146 | /** 147 | * @param string $key 148 | * @return bool 149 | */ 150 | public function hasMetadataForKey(string $key): bool { 151 | return \in_array($key, $this->getAllAvailableMetadataKeys(), true); 152 | } 153 | 154 | /** 155 | * @return string 156 | */ 157 | private function getFeatures():string { 158 | $features = []; 159 | 160 | if ($this->entity->getHasPhone()) { 161 | $features[] = 'PHONE'; 162 | } 163 | if ($this->entity->getHasVideoConferencing()) { 164 | $features[] = 'VIDEO-CONFERENCING'; 165 | } 166 | if ($this->entity->getHasTv()) { 167 | $features[] = 'TV'; 168 | } 169 | if ($this->entity->getHasProjector()) { 170 | $features[] = 'PROJECTOR'; 171 | } 172 | if ($this->entity->getHasWhiteboard()) { 173 | $features[] = 'WHITEBOARD'; 174 | } 175 | if ($this->entity->getIsWheelchairAccessible()) { 176 | $features[] = 'WHEELCHAIR-ACCESSIBLE'; 177 | } 178 | 179 | return implode(',', $features); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /lib/Command/CreateVehicle.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 50 | $this->vehicleMapper = $vehicleMapper; 51 | } 52 | 53 | /** 54 | * @return void 55 | */ 56 | protected function configure() { 57 | $this->setName('calendar-resource:vehicle:create'); 58 | $this->setDescription('Create a vehicle resource'); 59 | $this->addArgument(self::UID, InputArgument::REQUIRED); 60 | $this->addArgument(self::BUILDING_ID, InputArgument::REQUIRED); 61 | $this->addArgument(self::DISPLAY_NAME, InputArgument::REQUIRED); 62 | $this->addArgument(self::EMAIL, InputArgument::REQUIRED); 63 | $this->addArgument(self::VEHICLE_TYPE, InputArgument::REQUIRED); 64 | $this->addArgument(self::VEHICLE_MAKE, InputArgument::REQUIRED); 65 | $this->addArgument(self::VEHICLE_MODEL, InputArgument::REQUIRED); 66 | $this->addOption(self::CONTACT, null, InputOption::VALUE_REQUIRED); 67 | $this->addOption(self::IS_ELECTRIC, null, InputOption::VALUE_REQUIRED); 68 | $this->addOption(self::RANGE, null, InputOption::VALUE_REQUIRED); 69 | $this->addOption(self::SEATING_CAPACITY, null, InputOption::VALUE_REQUIRED); 70 | } 71 | 72 | /** 73 | * @return int 74 | */ 75 | protected function execute(InputInterface $input, OutputInterface $output): int { 76 | $uid = (string)$input->getArgument(self::UID); 77 | $buildingId = (int)$input->getArgument(self::BUILDING_ID); 78 | $displayName = (string)$input->getArgument(self::DISPLAY_NAME); 79 | $email = (string)$input->getArgument(self::EMAIL); 80 | $contact = (string)$input->getOption(self::CONTACT); 81 | $vehicleType = (string)$input->getArgument(self::VEHICLE_TYPE); 82 | $vehicleMake = (string)$input->getArgument(self::VEHICLE_MAKE); 83 | $model = (string)$input->getArgument(self::VEHICLE_MODEL); 84 | $isElectric = (bool)$input->getOption(self::IS_ELECTRIC); 85 | $range = (int)$input->getOption(self::RANGE); 86 | $seating = (int)$input->getOption(self::SEATING_CAPACITY); 87 | 88 | $this->uidValidationService->validateUidAndThrow($uid); 89 | 90 | $vehicleModel = new VehicleModel(); 91 | $vehicleModel->setBuildingId($buildingId); 92 | $vehicleModel->setUid($uid); 93 | $vehicleModel->setDisplayName($displayName); 94 | $vehicleModel->setEmail($email); 95 | $vehicleModel->setContactPersonUserId($contact); 96 | $vehicleModel->setVehicleType($vehicleType); 97 | $vehicleModel->setVehicleMake($vehicleMake); 98 | $vehicleModel->setVehicleModel($model); 99 | $vehicleModel->setIsElectric($isElectric); 100 | $vehicleModel->setRange($range); 101 | $vehicleModel->setSeatingCapacity($seating); 102 | 103 | try { 104 | $inserted = $this->vehicleMapper->insert($vehicleModel); 105 | $output->writeln('Created new Vehicle with ID:'); 106 | $output->writeln('' . $inserted->getId() . ''); 107 | } catch (\Exception $e) { 108 | $this->logger->error($e->getMessage(), ['exception' => $e]); 109 | $output->writeln('Could not create entry: ' . $e->getMessage() . ''); 110 | return 1; 111 | } 112 | 113 | if (method_exists($this->resourceManager, 'update')) { 114 | $this->resourceManager->update(); 115 | } 116 | 117 | return 0; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tests/Unit/Db/RestrictionMapperTest.php: -------------------------------------------------------------------------------- 1 | getQueryBuilder(); 24 | $qb->delete('calresources_restricts')->executeStatement(); 25 | 26 | $this->mapper = new RestrictionMapper(self::$realDatabase); 27 | 28 | $restrictions = $this->getSampleRestrictions(); 29 | array_map(function ($restriction): void { 30 | $this->mapper->insert($restriction); 31 | }, $restrictions); 32 | } 33 | 34 | public function testFind(): void { 35 | $allRestrictions = $this->mapper->findAllByEntityTypeAndId('type_1', 99); 36 | 37 | $story0 = $this->mapper->find($allRestrictions[0]->getId()); 38 | $this->assertEquals($allRestrictions[0]->getEntityType(), $story0->getEntityType()); 39 | $this->assertEquals($allRestrictions[0]->getEntityId(), $story0->getEntityId()); 40 | $this->assertEquals($allRestrictions[0]->getGroupId(), $story0->getGroupId()); 41 | 42 | $story1 = $this->mapper->find($allRestrictions[1]->getId()); 43 | $this->assertEquals($allRestrictions[1]->getEntityType(), $story1->getEntityType()); 44 | $this->assertEquals($allRestrictions[1]->getEntityId(), $story1->getEntityId()); 45 | $this->assertEquals($allRestrictions[1]->getGroupId(), $story1->getGroupId()); 46 | 47 | $this->expectException(DoesNotExistException::class); 48 | $this->mapper->find(-1); 49 | } 50 | 51 | public function testFindAllByEntityTypeAndId(): void { 52 | $allRestrictions = $this->mapper->findAllByEntityTypeAndId('type_1', 99); 53 | 54 | $this->assertCount(3, $allRestrictions); 55 | 56 | $this->assertEquals('type_1', $allRestrictions[0]->getEntityType()); 57 | $this->assertEquals(99, $allRestrictions[0]->getEntityId()); 58 | $this->assertEquals('group_1', $allRestrictions[0]->getGroupId()); 59 | $this->assertEquals('type_1', $allRestrictions[1]->getEntityType()); 60 | $this->assertEquals(99, $allRestrictions[1]->getEntityId()); 61 | $this->assertEquals('group_2', $allRestrictions[1]->getGroupId()); 62 | $this->assertEquals('type_1', $allRestrictions[2]->getEntityType()); 63 | $this->assertEquals(99, $allRestrictions[2]->getEntityId()); 64 | $this->assertEquals('group_99', $allRestrictions[2]->getGroupId()); 65 | } 66 | 67 | public function testDeleteAllByEntityTypeAndId(): void { 68 | $allRestrictions = $this->mapper->findAllByEntityTypeAndId('type_1', 99); 69 | 70 | $this->assertCount(3, $allRestrictions); 71 | 72 | $this->assertEquals('type_1', $allRestrictions[0]->getEntityType()); 73 | $this->assertEquals(99, $allRestrictions[0]->getEntityId()); 74 | $this->assertEquals('group_1', $allRestrictions[0]->getGroupId()); 75 | $this->assertEquals('type_1', $allRestrictions[1]->getEntityType()); 76 | $this->assertEquals(99, $allRestrictions[1]->getEntityId()); 77 | $this->assertEquals('group_2', $allRestrictions[1]->getGroupId()); 78 | $this->assertEquals('type_1', $allRestrictions[2]->getEntityType()); 79 | $this->assertEquals(99, $allRestrictions[2]->getEntityId()); 80 | $this->assertEquals('group_99', $allRestrictions[2]->getGroupId()); 81 | 82 | $this->mapper->deleteAllByEntityTypeAndId('type_1', 99); 83 | 84 | $allRestrictions = $this->mapper->findAllByEntityTypeAndId('type_1', 99); 85 | 86 | $this->assertCount(0, $allRestrictions); 87 | } 88 | 89 | public function testDeleteAllRestrictionsByGroupId(): void { 90 | $allRestrictions = $this->mapper->findAllByEntityTypeAndId('type_1', 99); 91 | 92 | $this->assertCount(3, $allRestrictions); 93 | $this->assertEquals('type_1', $allRestrictions[0]->getEntityType()); 94 | $this->assertEquals(99, $allRestrictions[0]->getEntityId()); 95 | $this->assertEquals('group_1', $allRestrictions[0]->getGroupId()); 96 | $this->assertEquals('type_1', $allRestrictions[1]->getEntityType()); 97 | $this->assertEquals(99, $allRestrictions[1]->getEntityId()); 98 | $this->assertEquals('group_2', $allRestrictions[1]->getGroupId()); 99 | $this->assertEquals('type_1', $allRestrictions[2]->getEntityType()); 100 | $this->assertEquals(99, $allRestrictions[2]->getEntityId()); 101 | $this->assertEquals('group_99', $allRestrictions[2]->getGroupId()); 102 | 103 | $this->mapper->deleteAllRestrictionsByGroupId('group_99'); 104 | 105 | $allRestrictions = $this->mapper->findAllByEntityTypeAndId('type_1', 99); 106 | 107 | $this->assertCount(2, $allRestrictions); 108 | $this->assertEquals('type_1', $allRestrictions[0]->getEntityType()); 109 | $this->assertEquals(99, $allRestrictions[0]->getEntityId()); 110 | $this->assertEquals('group_1', $allRestrictions[0]->getGroupId()); 111 | $this->assertEquals('type_1', $allRestrictions[1]->getEntityType()); 112 | $this->assertEquals(99, $allRestrictions[1]->getEntityId()); 113 | $this->assertEquals('group_2', $allRestrictions[1]->getGroupId()); 114 | } 115 | 116 | protected function getSampleRestrictions(): array { 117 | return [ 118 | RestrictionModel::fromParams([ 119 | 'entityType' => 'type_1', 120 | 'entityId' => 99, 121 | 'groupId' => 'group_1', 122 | ]), 123 | RestrictionModel::fromParams([ 124 | 'entityType' => 'type_1', 125 | 'entityId' => 99, 126 | 'groupId' => 'group_2', 127 | ]), 128 | RestrictionModel::fromParams([ 129 | 'entityType' => 'type_2', 130 | 'entityId' => 123, 131 | 'groupId' => 'group_1', 132 | ]), 133 | RestrictionModel::fromParams([ 134 | 'entityType' => 'type_3', 135 | 'entityId' => 456, 136 | 'groupId' => 'group_1', 137 | ]), 138 | RestrictionModel::fromParams([ 139 | 'entityType' => 'type_1', 140 | 'entityId' => 99, 141 | 'groupId' => 'group_99', 142 | ]), 143 | ]; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /tests/Unit/Db/RoomMapperTest.php: -------------------------------------------------------------------------------- 1 | getQueryBuilder(); 24 | $qb->delete('calresources_rooms')->executeStatement(); 25 | 26 | $this->mapper = new RoomMapper(self::$realDatabase); 27 | 28 | $rooms = $this->getSampleRooms(); 29 | array_map(function ($room): void { 30 | $this->mapper->insert($room); 31 | }, $rooms); 32 | } 33 | 34 | public function testFind(): void { 35 | $allRooms = $this->mapper->findAll(); 36 | 37 | $room0 = $this->mapper->find($allRooms[0]->getId()); 38 | $this->assertEquals($allRooms[0]->getDisplayName(), $room0->getDisplayName()); 39 | 40 | $room1 = $this->mapper->find($allRooms[1]->getId()); 41 | $this->assertEquals($allRooms[1]->getDisplayName(), $room1->getDisplayName()); 42 | 43 | $this->expectException(DoesNotExistException::class); 44 | $this->mapper->find(-1); 45 | } 46 | 47 | public function testFindByUID(): void { 48 | $room = $this->mapper->findByUID('uid0'); 49 | $this->assertEquals('Room 0', $room->getDisplayName()); 50 | 51 | $this->expectException(DoesNotExistException::class); 52 | $this->mapper->findByUID('uid-non-exist'); 53 | } 54 | 55 | public function testFindAll(): void { 56 | $roomSet0 = $this->mapper->findAll('display_name', true, 2, 0); 57 | 58 | $this->assertCount(2, $roomSet0); 59 | 60 | $this->assertEquals('Room 0', $roomSet0[0]->getDisplayName()); 61 | $this->assertEquals('Room 1', $roomSet0[1]->getDisplayName()); 62 | 63 | $roomSet1 = $this->mapper->findAll('display_name', true, 3, 5); 64 | 65 | $this->assertCount(3, $roomSet1); 66 | 67 | $this->assertEquals('Room 5', $roomSet1[0]->getDisplayName()); 68 | $this->assertEquals('Room 6', $roomSet1[1]->getDisplayName()); 69 | $this->assertEquals('Room 7', $roomSet1[2]->getDisplayName()); 70 | } 71 | 72 | public function testFindAllUIDs(): void { 73 | $uids = $this->mapper->findAllUIDs(); 74 | $this->assertEquals([ 75 | 'uid0', 76 | 'uid1', 77 | 'uid2', 78 | 'uid3', 79 | 'uid4', 80 | 'uid5', 81 | 'uid6', 82 | 'uid7', 83 | 'uid8', 84 | 'uid9', 85 | ], $uids); 86 | 87 | $uids = $this->mapper->findAllUIDs('display_name', true, 3, 5); 88 | $this->assertEquals([ 89 | 'uid5', 90 | 'uid6', 91 | 'uid7', 92 | ], $uids); 93 | } 94 | 95 | public function testFindAllByRoomType(): void { 96 | $rooms = $this->mapper->findAllByRoomType('room_type_1'); 97 | 98 | $this->assertCount(2, $rooms); 99 | 100 | $this->assertEquals('Room 0', $rooms[0]->getDisplayName()); 101 | $this->assertEquals('Room 1', $rooms[1]->getDisplayName()); 102 | } 103 | 104 | public function testFindAllByStoryId(): void { 105 | $rooms = $this->mapper->findAllByStoryId(99); 106 | 107 | $this->assertCount(2, $rooms); 108 | 109 | $this->assertEquals('Room 3', $rooms[0]->getDisplayName()); 110 | $this->assertEquals('Room 4', $rooms[1]->getDisplayName()); 111 | } 112 | 113 | public function testFindAllByRoomTypeAndStoryId(): void { 114 | $rooms = $this->mapper->findAllByRoomTypeAndStoryId('room_type_2', 99); 115 | 116 | $this->assertCount(1, $rooms); 117 | 118 | $this->assertEquals('Room 3', $rooms[0]->getDisplayName()); 119 | } 120 | 121 | protected function getSampleRooms(): array { 122 | return [ 123 | RoomModel::fromParams([ 124 | 'uid' => 'uid0', 125 | 'storyId' => 3, 126 | 'displayName' => 'Room 0', 127 | 'email' => 'room0@example.com', 128 | 'roomType' => 'room_type_1', 129 | 'contactPersonUserId' => 'user_1', 130 | ]), 131 | RoomModel::fromParams([ 132 | 'uid' => 'uid1', 133 | 'storyId' => 3, 134 | 'displayName' => 'Room 1', 135 | 'email' => 'room1@example.com', 136 | 'roomType' => 'room_type_1', 137 | 'contactPersonUserId' => 'user_1', 138 | ]), 139 | RoomModel::fromParams([ 140 | 'uid' => 'uid2', 141 | 'storyId' => 3, 142 | 'displayName' => 'Room 2', 143 | 'email' => 'room2@example.com', 144 | 'roomType' => 'room_type_2', 145 | 'contactPersonUserId' => 'user_1', 146 | ]), 147 | RoomModel::fromParams([ 148 | 'uid' => 'uid3', 149 | 'storyId' => 99, 150 | 'displayName' => 'Room 3', 151 | 'email' => 'room3@example.com', 152 | 'roomType' => 'room_type_2', 153 | 'contactPersonUserId' => 'user_2', 154 | ]), 155 | RoomModel::fromParams([ 156 | 'uid' => 'uid4', 157 | 'storyId' => 99, 158 | 'displayName' => 'Room 4', 159 | 'email' => 'room4@example.com', 160 | 'roomType' => 'room_type_3', 161 | 'contactPersonUserId' => 'user_2', 162 | ]), 163 | RoomModel::fromParams([ 164 | 'uid' => 'uid5', 165 | 'storyId' => 1, 166 | 'displayName' => 'Room 5', 167 | 'email' => 'room5@example.com', 168 | 'roomType' => 'room_type_3', 169 | ]), 170 | RoomModel::fromParams([ 171 | 'uid' => 'uid6', 172 | 'storyId' => 1, 173 | 'displayName' => 'Room 6', 174 | 'email' => 'room6@example.com', 175 | 'roomType' => 'room_type_4', 176 | 'isWheelchairAccessible' => true, 177 | ]), 178 | RoomModel::fromParams([ 179 | 'uid' => 'uid7', 180 | 'storyId' => 4, 181 | 'displayName' => 'Room 7', 182 | 'email' => 'room7@example.com', 183 | 'roomType' => 'room_type_4', 184 | 'capacity' => 50, 185 | 'roomNumber' => '204.1a', 186 | 'hasPhone' => true, 187 | 'hasVideoConferencing' => false, 188 | 'hasTv' => true, 189 | 'hasProjector' => true, 190 | 'hasWhiteboard' => false, 191 | 'isWheelchairAccessible' => true, 192 | ]), 193 | RoomModel::fromParams([ 194 | 'uid' => 'uid8', 195 | 'storyId' => 4, 196 | 'displayName' => 'Room 8', 197 | 'email' => 'room8@example.com', 198 | 'roomType' => 'room_type_5', 199 | 'isWheelchairAccessible' => false, 200 | ]), 201 | RoomModel::fromParams([ 202 | 'uid' => 'uid9', 203 | 'storyId' => 4, 204 | 'displayName' => 'Room 9', 205 | 'email' => 'room9@example.com', 206 | 'roomType' => 'room_type_5', 207 | ]), 208 | ]; 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /lib/Db/AMapper.php: -------------------------------------------------------------------------------- 1 | db->getQueryBuilder(); 26 | 27 | $qb->select('*') 28 | ->from($this->tableName) 29 | ->where( 30 | $qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)) 31 | ); 32 | 33 | return $this->findEntity($qb); 34 | } 35 | 36 | /** 37 | * @param string $uid 38 | * @return Entity 39 | * @throws DoesNotExistException 40 | * @throws MultipleObjectsReturnedException 41 | */ 42 | public function findByUID(string $uid):Entity { 43 | $qb = $this->db->getQueryBuilder(); 44 | 45 | $qb->select('*') 46 | ->from($this->tableName) 47 | ->where( 48 | $qb->expr()->eq('uid', $qb->createNamedParameter($uid, IQueryBuilder::PARAM_STR)) 49 | ); 50 | 51 | return $this->findEntity($qb); 52 | } 53 | 54 | /** 55 | * @param string $orderBy 56 | * @param bool $ascending 57 | * @param int|null $limit 58 | * @param int|null $offset 59 | * @return Entity[] 60 | */ 61 | public function findAll(string $orderBy = 'display_name', 62 | bool $ascending = true, 63 | ?int $limit = null, 64 | ?int $offset = null): array { 65 | $qb = $this->db->getQueryBuilder(); 66 | 67 | $qb->select('*') 68 | ->from($this->tableName) 69 | ->orderBy($orderBy, $ascending ? 'ASC' : 'DESC'); 70 | 71 | if ($limit !== null) { 72 | $qb->setMaxResults($limit); 73 | } 74 | if ($offset !== null) { 75 | $qb->setFirstResult($offset); 76 | } 77 | 78 | return $this->findEntities($qb); 79 | } 80 | 81 | /** 82 | * @param array $filter 83 | * @param string $orderBy 84 | * @param bool $ascending 85 | * @param int|null $limit 86 | * @param int|null $offset 87 | * @return Entity[] 88 | */ 89 | protected function findAllByFilter(array $filter, 90 | string $orderBy = 'display_name', 91 | bool $ascending = true, 92 | ?int $limit = null, 93 | ?int $offset = null):array { 94 | if (empty($filter)) { 95 | return $this->findAll($orderBy, $ascending, $limit, $offset); 96 | } 97 | 98 | $qb = $this->db->getQueryBuilder(); 99 | 100 | $qb->select('*') 101 | ->from($this->tableName) 102 | ->orderBy($orderBy, $ascending ? 'ASC' : 'DESC'); 103 | 104 | foreach ($filter as [$column, $value, $type]) { 105 | if ($value === null) { 106 | $qb->andWhere( 107 | $qb->expr()->isNull($column) 108 | ); 109 | } else { 110 | $qb->andWhere( 111 | $qb->expr()->eq($column, $qb->createNamedParameter($value, $type)) 112 | ); 113 | } 114 | } 115 | 116 | if ($limit !== null) { 117 | $qb->setMaxResults($limit); 118 | } 119 | if ($offset !== null) { 120 | $qb->setFirstResult($offset); 121 | } 122 | 123 | return $this->findEntities($qb); 124 | } 125 | 126 | /** 127 | * @param string $orderBy 128 | * @param bool $ascending 129 | * @param int|null $limit 130 | * @param int|null $offset 131 | * @return string[] 132 | * @throws Exception 133 | */ 134 | public function findAllUIDs(string $orderBy = 'display_name', 135 | bool $ascending = true, 136 | ?int $limit = null, 137 | ?int $offset = null): array { 138 | $qb = $this->db->getQueryBuilder(); 139 | 140 | $qb->select('uid') 141 | ->from($this->tableName) 142 | ->orderBy($orderBy, $ascending ? 'ASC' : 'DESC'); 143 | 144 | if ($limit !== null) { 145 | $qb->setMaxResults($limit); 146 | } 147 | if ($offset !== null) { 148 | $qb->setFirstResult($offset); 149 | } 150 | $stmt = $qb->executeQuery(); 151 | 152 | $uids = []; 153 | while ($row = $stmt->fetch()) { 154 | $uids[] = $row['uid'] ; 155 | } 156 | 157 | return $uids; 158 | } 159 | 160 | /** 161 | * @param string $search 162 | * @param string $searchBy 163 | * @param string $orderBy 164 | * @param bool $ascending 165 | * @param int|null $limit 166 | * @param int|null $offset 167 | * @return array 168 | */ 169 | public function search(string $search, 170 | string $searchBy = 'display_name', 171 | string $orderBy = 'display_name', 172 | bool $ascending = true, 173 | ?int $limit = null, 174 | ?int $offset = null): array { 175 | $qb = $this->db->getQueryBuilder(); 176 | 177 | $qb->select('*') 178 | ->from($this->tableName) 179 | ->where($qb->expr()->iLike( 180 | $searchBy, 181 | $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($search) . '%', IQueryBuilder::PARAM_STR), 182 | IQueryBuilder::PARAM_STR 183 | )) 184 | ->orderBy($orderBy, $ascending ? 'ASC' : 'DESC'); 185 | 186 | if ($limit !== null) { 187 | $qb->setMaxResults($limit); 188 | } 189 | if ($offset !== null) { 190 | $qb->setFirstResult($offset); 191 | } 192 | 193 | return $this->findEntities($qb); 194 | } 195 | 196 | /** 197 | * @param string $userId 198 | * @throws Exception 199 | */ 200 | public function removeContactUserId(string $userId): void { 201 | $qb = $this->db->getQueryBuilder(); 202 | 203 | $qb->update($this->tableName) 204 | ->set('contact_person_user_id', $qb->createNamedParameter(null)) 205 | ->where($qb->expr()->eq('contact_person_user_id', $qb->createNamedParameter($userId))); 206 | 207 | $qb->executeStatement(); 208 | } 209 | 210 | /** 211 | * @param string $type 212 | * @param \OCP\IDBConnection $db 213 | * @return BuildingMapper|ResourceMapper|RestrictionMapper|RoomMapper|StoryMapper|VehicleMapper|null 214 | */ 215 | public static function getMapper(string $type, \OCP\IDBConnection $db) { 216 | $type = strtolower($type); 217 | $mapper = null; 218 | switch ($type) { 219 | case 'building': 220 | $mapper = new BuildingMapper($db); 221 | break; 222 | case 'resource': 223 | $mapper = new ResourceMapper($db); 224 | break; 225 | case 'restriction': 226 | $mapper = new RestrictionMapper($db); 227 | break; 228 | case 'room': 229 | $mapper = new RoomMapper($db); 230 | break; 231 | case 'story': 232 | $mapper = new StoryMapper($db); 233 | break; 234 | case 'vehicle': 235 | $mapper = new VehicleMapper($db); 236 | break; 237 | default: 238 | break; 239 | } 240 | return $mapper; 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /.github/workflows/phpunit-sqlite.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2022-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: PHPUnit SQLite 10 | 11 | on: pull_request 12 | 13 | permissions: 14 | contents: read 15 | 16 | concurrency: 17 | group: phpunit-sqlite-${{ github.head_ref || github.run_id }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | matrix: 22 | runs-on: ubuntu-latest-low 23 | outputs: 24 | php-version: ${{ steps.versions.outputs.php-available-list }} 25 | server-max: ${{ steps.versions.outputs.branches-max-list }} 26 | steps: 27 | - name: Checkout app 28 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 29 | 30 | - name: Get version matrix 31 | id: versions 32 | uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1 33 | 34 | changes: 35 | runs-on: ubuntu-latest-low 36 | permissions: 37 | contents: read 38 | pull-requests: read 39 | 40 | outputs: 41 | src: ${{ steps.changes.outputs.src}} 42 | 43 | steps: 44 | - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2 45 | id: changes 46 | continue-on-error: true 47 | with: 48 | filters: | 49 | src: 50 | - '.github/workflows/**' 51 | - 'appinfo/**' 52 | - 'lib/**' 53 | - 'templates/**' 54 | - 'tests/**' 55 | - 'vendor/**' 56 | - 'vendor-bin/**' 57 | - '.php-cs-fixer.dist.php' 58 | - 'composer.json' 59 | - 'composer.lock' 60 | 61 | phpunit-sqlite: 62 | runs-on: ubuntu-latest 63 | 64 | needs: [changes, matrix] 65 | if: needs.changes.outputs.src != 'false' 66 | 67 | strategy: 68 | matrix: 69 | php-versions: ${{ fromJson(needs.matrix.outputs.php-version) }} 70 | server-versions: ${{ fromJson(needs.matrix.outputs.server-max) }} 71 | 72 | name: SQLite PHP ${{ matrix.php-versions }} Nextcloud ${{ matrix.server-versions }} 73 | 74 | steps: 75 | - name: Set app env 76 | run: | 77 | # Split and keep last 78 | echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV 79 | 80 | - name: Checkout server 81 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 82 | with: 83 | submodules: true 84 | repository: nextcloud/server 85 | ref: ${{ matrix.server-versions }} 86 | 87 | - name: Checkout app 88 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 89 | with: 90 | path: apps/${{ env.APP_NAME }} 91 | 92 | - name: Set up php ${{ matrix.php-versions }} 93 | uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5 94 | with: 95 | php-version: ${{ matrix.php-versions }} 96 | # https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html#prerequisites-for-manual-installation 97 | extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite 98 | coverage: none 99 | ini-file: development 100 | env: 101 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 102 | 103 | - name: Check composer file existence 104 | id: check_composer 105 | uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0 106 | with: 107 | files: apps/${{ env.APP_NAME }}/composer.json 108 | 109 | - name: Set up dependencies 110 | # Only run if phpunit config file exists 111 | if: steps.check_composer.outputs.files_exists == 'true' 112 | working-directory: apps/${{ env.APP_NAME }} 113 | run: composer i 114 | 115 | - name: Set up Nextcloud 116 | env: 117 | DB_PORT: 4444 118 | run: | 119 | mkdir data 120 | ./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin 121 | ./occ app:enable --force ${{ env.APP_NAME }} 122 | 123 | - name: Check PHPUnit script is defined 124 | id: check_phpunit 125 | continue-on-error: true 126 | working-directory: apps/${{ env.APP_NAME }} 127 | run: | 128 | composer run --list | grep '^ test:unit ' | wc -l | grep 1 129 | 130 | - name: PHPUnit 131 | # Only run if phpunit config file exists 132 | if: steps.check_phpunit.outcome == 'success' 133 | working-directory: apps/${{ env.APP_NAME }} 134 | run: composer run test:unit 135 | 136 | - name: Check PHPUnit integration script is defined 137 | id: check_integration 138 | continue-on-error: true 139 | working-directory: apps/${{ env.APP_NAME }} 140 | run: | 141 | composer run --list | grep '^ test:integration ' | wc -l | grep 1 142 | 143 | - name: Run Nextcloud 144 | # Only run if phpunit integration config file exists 145 | if: steps.check_integration.outcome == 'success' 146 | run: php -S localhost:8080 & 147 | 148 | - name: PHPUnit integration 149 | # Only run if phpunit integration config file exists 150 | if: steps.check_integration.outcome == 'success' 151 | working-directory: apps/${{ env.APP_NAME }} 152 | run: composer run test:integration 153 | 154 | - name: Print logs 155 | if: always() 156 | run: | 157 | cat data/nextcloud.log 158 | 159 | - name: Skipped 160 | # Fail the action when neither unit nor integration tests ran 161 | if: steps.check_phpunit.outcome == 'failure' && steps.check_integration.outcome == 'failure' 162 | run: | 163 | echo 'Neither PHPUnit nor PHPUnit integration tests are specified in composer.json scripts' 164 | exit 1 165 | 166 | summary: 167 | permissions: 168 | contents: none 169 | runs-on: ubuntu-latest-low 170 | needs: [changes, phpunit-sqlite] 171 | 172 | if: always() 173 | 174 | name: phpunit-sqlite-summary 175 | 176 | steps: 177 | - name: Summary status 178 | run: if ${{ needs.changes.outputs.src != 'false' && needs.phpunit-sqlite.result != 'success' }}; then exit 1; fi 179 | -------------------------------------------------------------------------------- /tests/Unit/Db/ResourceMapperTest.php: -------------------------------------------------------------------------------- 1 | getQueryBuilder(); 24 | $qb->delete('calresources_resources')->executeStatement(); 25 | 26 | $this->mapper = new ResourceMapper(self::$realDatabase); 27 | 28 | $resources = $this->getSampleResources(); 29 | array_map(function ($resource): void { 30 | $this->mapper->insert($resource); 31 | }, $resources); 32 | } 33 | 34 | public function testFind(): void { 35 | $allResources = $this->mapper->findAll(); 36 | 37 | $resource0 = $this->mapper->find($allResources[0]->getId()); 38 | $this->assertEquals($allResources[0]->getDisplayName(), $resource0->getDisplayName()); 39 | 40 | $resource1 = $this->mapper->find($allResources[1]->getId()); 41 | $this->assertEquals($allResources[1]->getDisplayName(), $resource1->getDisplayName()); 42 | 43 | $this->expectException(DoesNotExistException::class); 44 | $this->mapper->find(-1); 45 | } 46 | 47 | public function testFindByUID(): void { 48 | $resource = $this->mapper->findByUID('uid0'); 49 | $this->assertEquals('Resource 0', $resource->getDisplayName()); 50 | 51 | $this->expectException(DoesNotExistException::class); 52 | $this->mapper->findByUID('uid-non-exist'); 53 | } 54 | 55 | public function testFindAll(): void { 56 | $resourceSet0 = $this->mapper->findAll('display_name', true, 2, 0); 57 | 58 | $this->assertCount(2, $resourceSet0); 59 | 60 | $this->assertEquals('Resource 0', $resourceSet0[0]->getDisplayName()); 61 | $this->assertEquals('Resource 1', $resourceSet0[1]->getDisplayName()); 62 | 63 | $resourceSet1 = $this->mapper->findAll('display_name', true, 3, 5); 64 | 65 | $this->assertCount(3, $resourceSet1); 66 | 67 | $this->assertEquals('Resource 5', $resourceSet1[0]->getDisplayName()); 68 | $this->assertEquals('Resource 6', $resourceSet1[1]->getDisplayName()); 69 | $this->assertEquals('Resource 7', $resourceSet1[2]->getDisplayName()); 70 | } 71 | 72 | public function testFindAllUIDs(): void { 73 | $uids = $this->mapper->findAllUIDs(); 74 | $this->assertEquals([ 75 | 'uid0', 76 | 'uid1', 77 | 'uid2', 78 | 'uid3', 79 | 'uid4', 80 | 'uid5', 81 | 'uid6', 82 | 'uid7', 83 | 'uid8', 84 | 'uid9', 85 | ], $uids); 86 | 87 | $uids = $this->mapper->findAllUIDs('display_name', true, 3, 5); 88 | $this->assertEquals([ 89 | 'uid5', 90 | 'uid6', 91 | 'uid7', 92 | ], $uids); 93 | } 94 | 95 | public function testFindAllByBuilding(): void { 96 | $resourceSet0 = $this->mapper->findAllByBuilding(3, 'display_name', true); 97 | 98 | $this->assertCount(3, $resourceSet0); 99 | $this->assertEquals('Resource 0', $resourceSet0[0]->getDisplayName()); 100 | $this->assertEquals('Resource 1', $resourceSet0[1]->getDisplayName()); 101 | $this->assertEquals('Resource 2', $resourceSet0[2]->getDisplayName()); 102 | } 103 | 104 | public function testFindAllByResourceType(): void { 105 | $resourceSet0 = $this->mapper->findAllByResourceType('resource_type_5', 'display_name', true); 106 | 107 | $this->assertCount(2, $resourceSet0); 108 | $this->assertEquals('Resource 8', $resourceSet0[0]->getDisplayName()); 109 | $this->assertEquals('Resource 9', $resourceSet0[1]->getDisplayName()); 110 | } 111 | 112 | public function testFindAllByBuildingIdAndResourceType(): void { 113 | $resourceSet0 = $this->mapper->findAllByBuildingAndResourceType(3, 'resource_type_1', 'display_name', true); 114 | 115 | $this->assertCount(2, $resourceSet0); 116 | $this->assertEquals('Resource 0', $resourceSet0[0]->getDisplayName()); 117 | $this->assertEquals('Resource 1', $resourceSet0[1]->getDisplayName()); 118 | } 119 | 120 | protected function getSampleResources(): array { 121 | return [ 122 | ResourceModel::fromParams([ 123 | 'uid' => 'uid0', 124 | 'buildingId' => 3, 125 | 'displayName' => 'Resource 0', 126 | 'email' => 'resource0@example.com', 127 | 'resourceType' => 'resource_type_1', 128 | 'contactPersonUserId' => 'user_1', 129 | ]), 130 | ResourceModel::fromParams([ 131 | 'uid' => 'uid1', 132 | 'buildingId' => 3, 133 | 'displayName' => 'Resource 1', 134 | 'email' => 'resource1@example.com', 135 | 'resourceType' => 'resource_type_1', 136 | 'contactPersonUserId' => 'user_1', 137 | ]), 138 | ResourceModel::fromParams([ 139 | 'uid' => 'uid2', 140 | 'buildingId' => 3, 141 | 'displayName' => 'Resource 2', 142 | 'email' => 'resource2@example.com', 143 | 'resourceType' => 'resource_type_2', 144 | 'contactPersonUserId' => 'user_1', 145 | ]), 146 | ResourceModel::fromParams([ 147 | 'uid' => 'uid3', 148 | 'buildingId' => 99, 149 | 'displayName' => 'Resource 3', 150 | 'email' => 'resource3@example.com', 151 | 'resourceType' => 'resource_type_2', 152 | 'contactPersonUserId' => 'user_2', 153 | ]), 154 | ResourceModel::fromParams([ 155 | 'uid' => 'uid4', 156 | 'buildingId' => 99, 157 | 'displayName' => 'Resource 4', 158 | 'email' => 'resource4@example.com', 159 | 'resourceType' => 'resource_type_3', 160 | 'contactPersonUserId' => 'user_2', 161 | ]), 162 | ResourceModel::fromParams([ 163 | 'uid' => 'uid5', 164 | 'buildingId' => 1, 165 | 'displayName' => 'Resource 5', 166 | 'email' => 'resource5@example.com', 167 | 'resourceType' => 'resource_type_3', 168 | ]), 169 | ResourceModel::fromParams([ 170 | 'uid' => 'uid6', 171 | 'buildingId' => 1, 172 | 'displayName' => 'Resource 6', 173 | 'email' => 'resource6@example.com', 174 | 'resourceType' => 'resource_type_4', 175 | ]), 176 | ResourceModel::fromParams([ 177 | 'uid' => 'uid7', 178 | 'buildingId' => 4, 179 | 'displayName' => 'Resource 7', 180 | 'email' => 'resource7@example.com', 181 | 'resourceType' => 'resource_type_4', 182 | ]), 183 | ResourceModel::fromParams([ 184 | 'uid' => 'uid8', 185 | 'buildingId' => 4, 186 | 'displayName' => 'Resource 8', 187 | 'email' => 'resource8@example.com', 188 | 'resourceType' => 'resource_type_5', 189 | ]), 190 | ResourceModel::fromParams([ 191 | 'uid' => 'uid9', 192 | 'buildingId' => 4, 193 | 'displayName' => 'Resource 9', 194 | 'email' => 'resource9@example.com', 195 | 'resourceType' => 'resource_type_5', 196 | ]), 197 | ]; 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /lib/Command/CreateRoom.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 54 | $this->roomMapper = $roomMapper; 55 | } 56 | 57 | /** 58 | * @return void 59 | */ 60 | protected function configure() { 61 | $this->setName('calendar-resource:room:create'); 62 | $this->setDescription('Create a room resource'); 63 | $this->addArgument( 64 | self::STORY_ID, 65 | InputArgument::REQUIRED, 66 | 'ID of the story this room is located on, e.g. 17' 67 | ); 68 | $this->addArgument( 69 | self::UID, 70 | InputArgument::REQUIRED, 71 | 'Unique ID of this resource, e.g. "Berlin-office-meeting-1"' 72 | ); 73 | $this->addArgument( 74 | self::DISPLAY_NAME, 75 | InputArgument::REQUIRED, 76 | 'Short room description, e.g. "Big meeting room"' 77 | ); 78 | $this->addArgument( 79 | self::EMAIL, 80 | InputArgument::REQUIRED, 81 | '' // TODO: is this the email of the person responsible? 82 | ); 83 | $this->addArgument( 84 | self::TYPE, 85 | InputArgument::REQUIRED, 86 | 'Type of room, e.g. "Meeting room" or "Phone booth"', 87 | ); 88 | $this->addOption( 89 | self::CONTACT, 90 | null, 91 | InputOption::VALUE_REQUIRED, 92 | 'Optional information about the person who manages the room. This could be an email address or a phone number.' 93 | ); 94 | $this->addOption( 95 | self::CAPACITY, 96 | null, 97 | InputOption::VALUE_REQUIRED, 98 | 'Optional maximal number of people for this room, e.g. 8' 99 | ); 100 | $this->addOption( 101 | self::ROOM_NR, 102 | null, 103 | InputOption::VALUE_REQUIRED, 104 | 'Optional room number, e.g. 102A' 105 | ); 106 | $this->addOption( 107 | self::HAS_PHONE, 108 | null, 109 | InputOption::VALUE_REQUIRED, 110 | 'Does this room have a phone? 0 (no) or 1 (yes)', 111 | false 112 | ); 113 | $this->addOption( 114 | self::HAS_VIDEO, 115 | null, 116 | InputOption::VALUE_REQUIRED, 117 | 'Does this room have video conferencing equipment? 0 (no) or 1 (yes)', 118 | false 119 | ); 120 | $this->addOption( 121 | self::HAS_TV, 122 | null, 123 | InputOption::VALUE_REQUIRED, 124 | 'Does this room have a TV? 0 (no) or 1 (yes)', 125 | false 126 | ); 127 | $this->addOption( 128 | self::HAS_PROJECTOR, 129 | null, 130 | InputOption::VALUE_REQUIRED, 131 | 'Does this room a projector? 0 (no) or 1 (yes)', 132 | false 133 | ); 134 | $this->addOption( 135 | self::HAS_WHITEBOARD, 136 | null, 137 | InputOption::VALUE_REQUIRED, 138 | 'Does this room have a whiteboard? 0 (no) or 1 (yes)', 139 | false 140 | ); 141 | $this->addOption( 142 | self::IS_WHEELCHAIR_ACCESSIBLE, 143 | null, 144 | InputOption::VALUE_REQUIRED, 145 | 'Is this room wheelchair accessible? 0 (no) or 1 (yes)', 146 | false 147 | ); 148 | } 149 | 150 | /** 151 | * @return int 152 | */ 153 | protected function execute(InputInterface $input, OutputInterface $output): int { 154 | $storyId = (int)$input->getArgument(self::STORY_ID); 155 | $uid = (string)$input->getArgument(self::UID); 156 | $displayName = (string)$input->getArgument(self::DISPLAY_NAME); 157 | $email = (string)$input->getArgument(self::EMAIL); 158 | $type = (string)$input->getArgument(self::TYPE); 159 | $contact = (string)$input->getOption(self::CONTACT); 160 | $capacity = (int)$input->getOption(self::CAPACITY); 161 | $roomNr = (string)$input->getOption(self::ROOM_NR); 162 | $phone = (bool)$input->getOption(self::HAS_PHONE); 163 | $video = (bool)$input->getOption(self::HAS_VIDEO); 164 | $tv = (bool)$input->getOption(self::HAS_TV); 165 | $projector = (bool)$input->getOption(self::HAS_PROJECTOR); 166 | $whiteboard = (bool)$input->getOption(self::HAS_WHITEBOARD); 167 | $wheelchair = (bool)$input->getOption(self::IS_WHEELCHAIR_ACCESSIBLE); 168 | 169 | $this->uidValidationService->validateUidAndThrow($uid); 170 | 171 | $roomModel = new RoomModel(); 172 | $roomModel->setStoryId($storyId); 173 | $roomModel->setUid($uid); 174 | $roomModel->setDisplayName($displayName); 175 | $roomModel->setEmail($email); 176 | $roomModel->setRoomType($type); 177 | $roomModel->setContactPersonUserId($contact); 178 | $roomModel->setCapacity($capacity); 179 | $roomModel->setRoomNumber($roomNr); 180 | $roomModel->setHasPhone($phone); 181 | $roomModel->setHasVideoConferencing($video); 182 | $roomModel->setHasTv($tv); 183 | $roomModel->setHasProjector($projector); 184 | $roomModel->setHasWhiteboard($whiteboard); 185 | $roomModel->setIsWheelchairAccessible($wheelchair); 186 | 187 | try { 188 | $inserted = $this->roomMapper->insert($roomModel); 189 | $output->writeln('Created new Room with ID:'); 190 | $output->writeln('' . $inserted->getId() . ''); 191 | } catch (Exception $e) { 192 | $this->logger->error($e->getMessage(), ['exception' => $e]); 193 | $output->writeln('Could not create entry: ' . $e->getMessage() . ''); 194 | return 1; 195 | } 196 | 197 | if (method_exists($this->roomManager, 'update')) { 198 | $this->roomManager->update(); 199 | } 200 | 201 | return 0; 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /LICENSES/CC0-1.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /.github/workflows/appstore-build-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Build and publish app release 10 | 11 | on: 12 | release: 13 | types: [published] 14 | 15 | jobs: 16 | build_and_publish: 17 | runs-on: ubuntu-latest 18 | 19 | # Only allowed to be run on nextcloud-releases repositories 20 | if: ${{ github.repository_owner == 'nextcloud-releases' }} 21 | 22 | steps: 23 | - name: Check actor permission 24 | uses: skjnldsv/check-actor-permission@69e92a3c4711150929bca9fcf34448c5bf5526e7 # v3.0 25 | with: 26 | require: write 27 | 28 | - name: Set app env 29 | run: | 30 | # Split and keep last 31 | echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV 32 | echo "APP_VERSION=${GITHUB_REF##*/}" >> $GITHUB_ENV 33 | 34 | - name: Checkout 35 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 36 | with: 37 | path: ${{ env.APP_NAME }} 38 | 39 | - name: Get appinfo data 40 | id: appinfo 41 | uses: skjnldsv/xpath-action@d813024a13948950fd8d23b580254feeb4883d3c # master 42 | with: 43 | filename: ${{ env.APP_NAME }}/appinfo/info.xml 44 | expression: "//info//dependencies//nextcloud/@min-version" 45 | 46 | - name: Read package.json node and npm engines version 47 | uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3 48 | id: versions 49 | # Continue if no package.json 50 | continue-on-error: true 51 | with: 52 | path: ${{ env.APP_NAME }} 53 | fallbackNode: '^20' 54 | fallbackNpm: '^10' 55 | 56 | - name: Set up node ${{ steps.versions.outputs.nodeVersion }} 57 | # Skip if no package.json 58 | if: ${{ steps.versions.outputs.nodeVersion }} 59 | uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6.1.0 60 | with: 61 | node-version: ${{ steps.versions.outputs.nodeVersion }} 62 | 63 | - name: Set up npm ${{ steps.versions.outputs.npmVersion }} 64 | # Skip if no package.json 65 | if: ${{ steps.versions.outputs.npmVersion }} 66 | run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}' 67 | 68 | - name: Get php version 69 | id: php-versions 70 | uses: icewind1991/nextcloud-version-matrix@58becf3b4bb6dc6cef677b15e2fd8e7d48c0908f # v1.3.1 71 | with: 72 | filename: ${{ env.APP_NAME }}/appinfo/info.xml 73 | 74 | - name: Set up php ${{ steps.php-versions.outputs.php-min }} 75 | uses: shivammathur/setup-php@bf6b4fbd49ca58e4608c9c89fba0b8d90bd2a39f # 2.35.5 76 | with: 77 | php-version: ${{ steps.php-versions.outputs.php-min }} 78 | coverage: none 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 81 | 82 | - name: Check composer.json 83 | id: check_composer 84 | uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0 85 | with: 86 | files: "${{ env.APP_NAME }}/composer.json" 87 | 88 | - name: Install composer dependencies 89 | if: steps.check_composer.outputs.files_exists == 'true' 90 | run: | 91 | cd ${{ env.APP_NAME }} 92 | composer install --no-dev 93 | 94 | - name: Build ${{ env.APP_NAME }} 95 | # Skip if no package.json 96 | if: ${{ steps.versions.outputs.nodeVersion }} 97 | env: 98 | CYPRESS_INSTALL_BINARY: 0 99 | run: | 100 | cd ${{ env.APP_NAME }} 101 | npm ci 102 | npm run build --if-present 103 | 104 | - name: Check Krankerl config 105 | id: krankerl 106 | uses: andstor/file-existence-action@076e0072799f4942c8bc574a82233e1e4d13e9d6 # v3.0.0 107 | with: 108 | files: ${{ env.APP_NAME }}/krankerl.toml 109 | 110 | - name: Install Krankerl 111 | if: steps.krankerl.outputs.files_exists == 'true' 112 | run: | 113 | wget https://github.com/ChristophWurst/krankerl/releases/download/v0.14.0/krankerl_0.14.0_amd64.deb 114 | sudo dpkg -i krankerl_0.14.0_amd64.deb 115 | 116 | - name: Package ${{ env.APP_NAME }} ${{ env.APP_VERSION }} with krankerl 117 | if: steps.krankerl.outputs.files_exists == 'true' 118 | run: | 119 | cd ${{ env.APP_NAME }} 120 | krankerl package 121 | 122 | - name: Package ${{ env.APP_NAME }} ${{ env.APP_VERSION }} with makefile 123 | if: steps.krankerl.outputs.files_exists != 'true' 124 | run: | 125 | cd ${{ env.APP_NAME }} 126 | make appstore 127 | 128 | - name: Checkout server ${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }} 129 | continue-on-error: true 130 | id: server-checkout 131 | run: | 132 | NCVERSION='${{ fromJSON(steps.appinfo.outputs.result).nextcloud.min-version }}' 133 | wget --quiet https://download.nextcloud.com/server/releases/latest-$NCVERSION.zip 134 | unzip latest-$NCVERSION.zip 135 | 136 | - name: Checkout server master fallback 137 | uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1 138 | if: ${{ steps.server-checkout.outcome != 'success' }} 139 | with: 140 | submodules: true 141 | repository: nextcloud/server 142 | path: nextcloud 143 | 144 | - name: Sign app 145 | run: | 146 | # Extracting release 147 | cd ${{ env.APP_NAME }}/build/artifacts 148 | tar -xvf ${{ env.APP_NAME }}.tar.gz 149 | cd ../../../ 150 | # Setting up keys 151 | echo '${{ secrets.APP_PRIVATE_KEY }}' > ${{ env.APP_NAME }}.key 152 | wget --quiet "https://github.com/nextcloud/app-certificate-requests/raw/master/${{ env.APP_NAME }}/${{ env.APP_NAME }}.crt" 153 | # Signing 154 | php nextcloud/occ integrity:sign-app --privateKey=../${{ env.APP_NAME }}.key --certificate=../${{ env.APP_NAME }}.crt --path=../${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }} 155 | # Rebuilding archive 156 | cd ${{ env.APP_NAME }}/build/artifacts 157 | tar -zcvf ${{ env.APP_NAME }}.tar.gz ${{ env.APP_NAME }} 158 | 159 | - name: Attach tarball to github release 160 | uses: svenstaro/upload-release-action@6b7fa9f267e90b50a19fef07b3596790bb941741 # v2 161 | id: attach_to_release 162 | with: 163 | repo_token: ${{ secrets.GITHUB_TOKEN }} 164 | file: ${{ env.APP_NAME }}/build/artifacts/${{ env.APP_NAME }}.tar.gz 165 | asset_name: ${{ env.APP_NAME }}-${{ env.APP_VERSION }}.tar.gz 166 | tag: ${{ github.ref }} 167 | overwrite: true 168 | 169 | - name: Upload app to Nextcloud appstore 170 | uses: nextcloud-releases/nextcloud-appstore-push-action@a011fe619bcf6e77ddebc96f9908e1af4071b9c1 # v1 171 | with: 172 | app_name: ${{ env.APP_NAME }} 173 | appstore_token: ${{ secrets.APPSTORE_TOKEN }} 174 | download_url: ${{ steps.attach_to_release.outputs.browser_download_url }} 175 | app_private_key: ${{ secrets.APP_PRIVATE_KEY }} 176 | -------------------------------------------------------------------------------- /tests/Unit/Db/VehicleMapperTest.php: -------------------------------------------------------------------------------- 1 | getQueryBuilder(); 24 | $qb->delete('calresources_vehicles')->executeStatement(); 25 | 26 | $this->mapper = new VehicleMapper(self::$realDatabase); 27 | 28 | $vehicles = $this->getSampleVehicles(); 29 | array_map(function ($vehicle): void { 30 | $this->mapper->insert($vehicle); 31 | }, $vehicles); 32 | } 33 | 34 | public function testFind(): void { 35 | $allVehicles = $this->mapper->findAll(); 36 | 37 | $room0 = $this->mapper->find($allVehicles[0]->getId()); 38 | $this->assertEquals($allVehicles[0]->getDisplayName(), $room0->getDisplayName()); 39 | 40 | $room1 = $this->mapper->find($allVehicles[1]->getId()); 41 | $this->assertEquals($allVehicles[1]->getDisplayName(), $room1->getDisplayName()); 42 | 43 | $this->expectException(DoesNotExistException::class); 44 | $this->mapper->find(-1); 45 | } 46 | 47 | public function testFindByUID(): void { 48 | $vehicle = $this->mapper->findByUID('uid0'); 49 | $this->assertEquals('Vehicle 0', $vehicle->getDisplayName()); 50 | 51 | $this->expectException(DoesNotExistException::class); 52 | $this->mapper->findByUID('uid-non-exist'); 53 | } 54 | 55 | public function testFindAll(): void { 56 | $vehicleSet0 = $this->mapper->findAll('display_name', true, 2, 0); 57 | 58 | $this->assertCount(2, $vehicleSet0); 59 | 60 | $this->assertEquals('Vehicle 0', $vehicleSet0[0]->getDisplayName()); 61 | $this->assertEquals('Vehicle 1', $vehicleSet0[1]->getDisplayName()); 62 | 63 | $vehicleSet1 = $this->mapper->findAll('display_name', true, 3, 5); 64 | 65 | $this->assertCount(3, $vehicleSet1); 66 | 67 | $this->assertEquals('Vehicle 5', $vehicleSet1[0]->getDisplayName()); 68 | $this->assertEquals('Vehicle 6', $vehicleSet1[1]->getDisplayName()); 69 | $this->assertEquals('Vehicle 7', $vehicleSet1[2]->getDisplayName()); 70 | } 71 | 72 | public function testFindAllUIDs(): void { 73 | $uids = $this->mapper->findAllUIDs(); 74 | $this->assertEquals([ 75 | 'uid0', 76 | 'uid1', 77 | 'uid2', 78 | 'uid3', 79 | 'uid4', 80 | 'uid5', 81 | 'uid6', 82 | 'uid7', 83 | 'uid8', 84 | 'uid9', 85 | ], $uids); 86 | 87 | $uids = $this->mapper->findAllUIDs('display_name', true, 3, 5); 88 | $this->assertEquals([ 89 | 'uid5', 90 | 'uid6', 91 | 'uid7', 92 | ], $uids); 93 | } 94 | 95 | public function testFindAllByBuilding(): void { 96 | $vehicles = $this->mapper->findAllByBuilding(4); 97 | 98 | $this->assertCount(3, $vehicles); 99 | 100 | $this->assertEquals('Vehicle 7', $vehicles[0]->getDisplayName()); 101 | $this->assertEquals('Vehicle 8', $vehicles[1]->getDisplayName()); 102 | $this->assertEquals('Vehicle 9', $vehicles[2]->getDisplayName()); 103 | } 104 | 105 | public function testFindAllByVehicleType(): void { 106 | $vehicles = $this->mapper->findAllByVehicleType('vehicle_type_1'); 107 | 108 | $this->assertCount(2, $vehicles); 109 | 110 | $this->assertEquals('Vehicle 0', $vehicles[0]->getDisplayName()); 111 | $this->assertEquals('Vehicle 1', $vehicles[1]->getDisplayName()); 112 | } 113 | 114 | public function testFindAllByBuildingAndVehicleType(): void { 115 | $vehicles = $this->mapper->findAllByBuildingAndVehicleType(99, 'vehicle_type_2'); 116 | 117 | $this->assertCount(1, $vehicles); 118 | 119 | $this->assertEquals('Vehicle 3', $vehicles[0]->getDisplayName()); 120 | } 121 | 122 | protected function getSampleVehicles(): array { 123 | return [ 124 | VehicleModel::fromParams([ 125 | 'uid' => 'uid0', 126 | 'buildingId' => 3, 127 | 'displayName' => 'Vehicle 0', 128 | 'email' => 'vehicle0@example.com', 129 | 'vehicleType' => 'vehicle_type_1', 130 | 'vehicleMake' => 'vehicle_make_1', 131 | 'vehicleModel' => 'vehicle_model_1', 132 | 'contactPersonUserId' => 'user_1', 133 | ]), 134 | VehicleModel::fromParams([ 135 | 'uid' => 'uid1', 136 | 'buildingId' => 3, 137 | 'displayName' => 'Vehicle 1', 138 | 'email' => 'vehicle1@example.com', 139 | 'vehicleType' => 'vehicle_type_1', 140 | 'vehicleMake' => 'vehicle_make_1', 141 | 'vehicleModel' => 'vehicle_model_1', 142 | 'contactPersonUserId' => 'user_1', 143 | ]), 144 | VehicleModel::fromParams([ 145 | 'uid' => 'uid2', 146 | 'buildingId' => 3, 147 | 'displayName' => 'Vehicle 2', 148 | 'email' => 'vehicle2@example.com', 149 | 'vehicleType' => 'vehicle_type_2', 150 | 'vehicleMake' => 'vehicle_make_1', 151 | 'vehicleModel' => 'vehicle_model_1', 152 | 'contactPersonUserId' => 'user_1', 153 | ]), 154 | VehicleModel::fromParams([ 155 | 'uid' => 'uid3', 156 | 'buildingId' => 99, 157 | 'displayName' => 'Vehicle 3', 158 | 'email' => 'vehicle3@example.com', 159 | 'vehicleType' => 'vehicle_type_2', 160 | 'vehicleMake' => 'vehicle_make_1', 161 | 'vehicleModel' => 'vehicle_model_1', 162 | 'contactPersonUserId' => 'user_2', 163 | ]), 164 | VehicleModel::fromParams([ 165 | 'uid' => 'uid4', 166 | 'buildingId' => 99, 167 | 'displayName' => 'Vehicle 4', 168 | 'email' => 'vehicle4@example.com', 169 | 'vehicleType' => 'vehicle_type_3', 170 | 'vehicleMake' => 'vehicle_make_1', 171 | 'vehicleModel' => 'vehicle_model_1', 172 | 'contactPersonUserId' => 'user_2', 173 | ]), 174 | VehicleModel::fromParams([ 175 | 'uid' => 'uid5', 176 | 'buildingId' => 1, 177 | 'displayName' => 'Vehicle 5', 178 | 'email' => 'vehicle5@example.com', 179 | 'vehicleType' => 'vehicle_type_3', 180 | 'vehicleMake' => 'vehicle_make_1', 181 | 'vehicleModel' => 'vehicle_model_1', 182 | ]), 183 | VehicleModel::fromParams([ 184 | 'uid' => 'uid6', 185 | 'buildingId' => 1, 186 | 'displayName' => 'Vehicle 6', 187 | 'email' => 'vehicle6@example.com', 188 | 'vehicleType' => 'vehicle_type_4', 189 | 'vehicleMake' => 'vehicle_make_1', 190 | 'vehicleModel' => 'vehicle_model_1', 191 | ]), 192 | VehicleModel::fromParams([ 193 | 'uid' => 'uid7', 194 | 'buildingId' => 4, 195 | 'displayName' => 'Vehicle 7', 196 | 'email' => 'vehicle7@example.com', 197 | 'vehicleType' => 'vehicle_type_4', 198 | 'vehicleMake' => 'vehicle_make_1', 199 | 'vehicleModel' => 'vehicle_model_1', 200 | ]), 201 | VehicleModel::fromParams([ 202 | 'uid' => 'uid8', 203 | 'buildingId' => 4, 204 | 'displayName' => 'Vehicle 8', 205 | 'email' => 'vehicle8@example.com', 206 | 'vehicleType' => 'vehicle_type_5', 207 | 'vehicleMake' => 'vehicle_make_1', 208 | 'vehicleModel' => 'vehicle_model_1', 209 | ]), 210 | VehicleModel::fromParams([ 211 | 'uid' => 'uid9', 212 | 'buildingId' => 4, 213 | 'displayName' => 'Vehicle 9', 214 | 'email' => 'vehicle9@example.com', 215 | 'vehicleType' => 'vehicle_type_5', 216 | 'vehicleMake' => 'vehicle_make_1', 217 | 'vehicleModel' => 'vehicle_model_1', 218 | 'isElectric' => true, 219 | 'range' => 800, 220 | 'seatingCapacity' => 5, 221 | ]), 222 | ]; 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /lib/Command/ListResources.php: -------------------------------------------------------------------------------- 1 | buildingMapper = $buildingMapper; 57 | $this->resourceMapper = $resourceMapper; 58 | $this->restrictionMapper = $restrictionMapper; 59 | $this->roomMapper = $roomMapper; 60 | $this->storyMapper = $storyMapper; 61 | $this->vehicleMapper = $vehicleMapper; 62 | } 63 | 64 | /** 65 | * @return void 66 | */ 67 | protected function configure() { 68 | $this->setName('calendar-resource:resources:list'); 69 | $this->setDescription('List all resources'); 70 | } 71 | 72 | /** @return int */ 73 | protected function execute(InputInterface $input, OutputInterface $output): int { 74 | // Buildings 75 | $table = new Table($output); 76 | $output->writeln('Buildings:'); 77 | $table->setHeaders( 78 | [ 79 | 'ID', 80 | 'Name', 81 | 'Address', 82 | 'Description', 83 | 'Wheelchair Accessible' 84 | ] 85 | ); 86 | $buildings = $this->buildingMapper->findAll(); 87 | $row = 1; 88 | foreach ($buildings as $building) { 89 | $table->setRow($row, 90 | [ 91 | $building->getId(), 92 | $building->getDisplayName(), 93 | $building->getAddress(), 94 | $building->getDescription(), 95 | ($building->getIsWheelchairAccessible() ? 'yes' : 'no') 96 | ] 97 | ); 98 | $row++; 99 | } 100 | $table->render(); 101 | 102 | // Stories 103 | $table = new Table($output); 104 | $output->writeln('Stories:'); 105 | $table->setHeaders( 106 | [ 107 | 'ID', 108 | 'Located in', 109 | 'Display Name' 110 | ] 111 | ); 112 | foreach ($buildings as $building) { 113 | $stories = $this->storyMapper->findAllByBuilding($building->getId()); 114 | /** @var StoryModel $story */ 115 | foreach ($stories as $story) { 116 | $table->setRow($row, 117 | [ 118 | $story->getId(), 119 | $building->getDisplayName(), 120 | $story->getDisplayName(), 121 | ] 122 | ); 123 | $row++; 124 | } 125 | } 126 | $table->render(); 127 | 128 | // Rooms 129 | $table = new Table($output); 130 | $output->writeln('Rooms:'); 131 | foreach ($buildings as $building) { 132 | $stories = $this->storyMapper->findAllByBuilding($building->getId()); 133 | $table->setHeaders( 134 | [ 135 | 'ID', 136 | 'UID', 137 | 'Name', 138 | 'Located in', 139 | 'Email', 140 | 'Room Type', 141 | 'Contact Person', 142 | 'Capacity', 143 | 'Room Number', 144 | 'Phone', 145 | 'Video Conferencing', 146 | 'TV', 147 | 'Projector', 148 | 'Whiteboard', 149 | 'Wheelchair Accessible' 150 | ] 151 | ); 152 | foreach ($stories as $story) { 153 | $rooms = $this->roomMapper->findAllByStoryId($story->getId()); 154 | /** @var RoomModel $room */ 155 | foreach ($rooms as $room) { 156 | $table->setRow($row, 157 | [ 158 | $room->getId(), 159 | $room->getUid(), 160 | $room->getDisplayName(), 161 | $building->getDisplayName() . ', ' . $story->getDisplayName(), 162 | $room->getEmail(), 163 | $room->getRoomType(), 164 | $room->getContactPersonUserId(), 165 | $room->getCapacity(), 166 | $room->getRoomNumber(), 167 | ($room->getHasPhone() ? 'yes' : 'no'), 168 | ($room->getHasVideoConferencing() ? 'yes' : 'no'), 169 | ($room->getHasTv() ? 'yes' : 'no'), 170 | ($room->getHasProjector() ? 'yes' : 'no'), 171 | ($room->getHasWhiteboard() ? 'yes' : 'no'), 172 | ($room->getIsWheelchairAccessible() ? 'yes' : 'no'), 173 | ] 174 | ); 175 | $row++; 176 | } 177 | $row++; 178 | } 179 | } 180 | $table->render(); 181 | 182 | // Resources 183 | $table = new Table($output); 184 | $output->writeln('Resources:'); 185 | $table->setHeaders( 186 | [ 187 | 'ID', 188 | 'UID', 189 | 'Name', 190 | 'Located in', 191 | 'Contact Person', 192 | 'Type' 193 | ] 194 | ); 195 | foreach ($buildings as $building) { 196 | $resources = $this->resourceMapper->findAllByBuilding($building->getId()); 197 | /** @var ResourceModel $resource */ 198 | foreach ($resources as $resource) { 199 | $table->setRow($row, 200 | [ 201 | $resource->getId(), 202 | $resource->getUid(), 203 | $resource->getDisplayName(), 204 | $building->getId() . ' ' . $building->getDisplayName(), 205 | $resource->getContactPersonUserId(), 206 | $resource->getResourceType() 207 | ] 208 | ); 209 | $row++; 210 | } 211 | } 212 | $table->render(); 213 | 214 | // Vehicles 215 | $table = new Table($output); 216 | $output->writeln('Vehicles:'); 217 | $table->setHeaders( 218 | [ 219 | 'ID', 220 | 'UID', 221 | 'Name', 222 | 'Located in', 223 | 'Email', 224 | 'Contact Person', 225 | 'Type', 226 | 'Make', 227 | 'Model', 228 | 'Electric', 229 | 'Range', 230 | 'Capacity' 231 | ] 232 | ); 233 | foreach ($buildings as $building) { 234 | $vehicles = $this->vehicleMapper->findAllByBuilding($building->getId()); 235 | /** @var VehicleModel $vehicle */ 236 | foreach ($vehicles as $vehicle) { 237 | $table->setRow($row, 238 | [ 239 | $vehicle->getId(), 240 | $vehicle->getUid(), 241 | $vehicle->getDisplayName(), 242 | $building->getId() . ' ' . $building->getDisplayName(), 243 | $vehicle->getEmail(), 244 | $vehicle->getContactPersonUserId(), 245 | $vehicle->getVehicleType(), 246 | $vehicle->getVehicleMake(), 247 | $vehicle->getVehicleModel(), 248 | ($vehicle->getIsElectric() ? 'yes' : 'no'), 249 | $vehicle->getRange(), 250 | $vehicle->getSeatingCapacity() 251 | ] 252 | ); 253 | $row++; 254 | } 255 | } 256 | $table->render(); 257 | 258 | // Restrictions 259 | $table = new Table($output); 260 | $output->writeln('Restrictions:'); 261 | $table->setHeaders( 262 | [ 263 | 'ID', 264 | 'Entity', 265 | 'Entity ID', 266 | 'Restricted To' 267 | ] 268 | ); 269 | 270 | $restrictions = $this->restrictionMapper->findAll(); 271 | /** @var RestrictionModel $restriction */ 272 | foreach ($restrictions as $restriction) { 273 | $table->setRow($row, 274 | [ 275 | $restriction->getId(), 276 | $restriction->getEntityType(), 277 | $restriction->getEntityId(), 278 | $restriction->getGroupId(), 279 | ] 280 | ); 281 | $row++; 282 | } 283 | $table->render(); 284 | 285 | return 0; 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /lib/Migration/Version1000Date20200805220319.php: -------------------------------------------------------------------------------- 1 | hasTable('calresources_buildings')) { 36 | $table = $schema->createTable('calresources_buildings'); 37 | $table->addColumn('id', Types::BIGINT, [ 38 | 'autoincrement' => true, 39 | 'notnull' => true, 40 | 'length' => 11, 41 | 'unsigned' => true, 42 | ]); 43 | $table->addColumn('display_name', Types::STRING, [ 44 | 'notnull' => true, 45 | 'length' => 255, 46 | ]); 47 | $table->addColumn('description', Types::STRING, [ 48 | 'notnull' => false, 49 | 'length' => 4000, 50 | ]); 51 | $table->addColumn('address', Types::STRING, [ 52 | 'notnull' => false, 53 | 'length' => 4000, 54 | ]); 55 | $table->addColumn('is_wheelchair_accessible', Types::BOOLEAN, [ 56 | 'notnull' => false, 57 | 'default' => false 58 | ]); 59 | $table->setPrimaryKey(['id']); 60 | } 61 | 62 | /** 63 | * @see \OCA\CalendarResourceManagement\Db\StoryModel 64 | */ 65 | if (!$schema->hasTable('calresources_stories')) { 66 | $table = $schema->createTable('calresources_stories'); 67 | $table->addColumn('id', Types::BIGINT, [ 68 | 'autoincrement' => true, 69 | 'notnull' => true, 70 | 'length' => 11, 71 | 'unsigned' => true, 72 | ]); 73 | $table->addColumn('building_id', Types::BIGINT, [ 74 | 'notnull' => true, 75 | 'length' => 11, 76 | 'unsigned' => true, 77 | ]); 78 | $table->addColumn('display_name', Types::STRING, [ 79 | 'notnull' => true, 80 | 'length' => 255, 81 | ]); 82 | $table->setPrimaryKey(['id']); 83 | $table->addIndex(['building_id'], 'calresources_stories_bid'); 84 | } 85 | 86 | /** 87 | * @see \OCA\CalendarResourceManagement\Db\ResourceModel 88 | */ 89 | if (!$schema->hasTable('calresources_resources')) { 90 | $table = $schema->createTable('calresources_resources'); 91 | $table->addColumn('id', Types::BIGINT, [ 92 | 'autoincrement' => true, 93 | 'notnull' => true, 94 | 'length' => 11, 95 | 'unsigned' => true, 96 | ]); 97 | $table->addColumn('uid', Types::STRING, [ 98 | 'notnull' => true, 99 | 'length' => 255, 100 | ]); 101 | $table->addColumn('building_id', Types::BIGINT, [ 102 | 'notnull' => true, 103 | 'length' => 11, 104 | 'unsigned' => true, 105 | ]); 106 | $table->addColumn('display_name', Types::STRING, [ 107 | 'notnull' => true, 108 | 'length' => 255, 109 | ]); 110 | $table->addColumn('email', Types::STRING, [ 111 | 'notnull' => true, 112 | 'length' => 255, 113 | ]); 114 | $table->addColumn('resource_type', Types::STRING, [ 115 | 'notnull' => true, 116 | 'length' => 255, 117 | ]); 118 | $table->addColumn('contact_person_user_id', Types::STRING, [ 119 | 'notnull' => false, 120 | 'length' => 255, 121 | ]); 122 | 123 | $table->setPrimaryKey(['id']); 124 | $table->addIndex(['building_id'], 'calresources_resources_bid'); 125 | $table->addUniqueIndex(['uid'], 'calresources_resources_uid'); 126 | $table->addUniqueIndex(['email'], 'calresources_resources_eml'); 127 | } 128 | 129 | /** 130 | * @see \OCA\CalendarResourceManagement\Db\RestrictionModel 131 | */ 132 | if (!$schema->hasTable('calresources_restricts')) { 133 | $table = $schema->createTable('calresources_restricts'); 134 | $table->addColumn('id', Types::BIGINT, [ 135 | 'autoincrement' => true, 136 | 'notnull' => true, 137 | 'length' => 11, 138 | 'unsigned' => true, 139 | ]); 140 | $table->addColumn('entity_type', Types::STRING, [ 141 | 'notnull' => true, 142 | 'length' => 255, 143 | ]); 144 | $table->addColumn('entity_id', Types::BIGINT, [ 145 | 'notnull' => true, 146 | 'length' => 11, 147 | 'unsigned' => true, 148 | ]); 149 | $table->addColumn('group_id', Types::STRING, [ 150 | 'notnull' => true, 151 | 'length' => 255, 152 | ]); 153 | 154 | $table->setPrimaryKey(['id']); 155 | $table->addIndex(['entity_type', 'entity_id'], 'calresources_restricts_ent'); 156 | $table->addUniqueIndex(['entity_type', 'entity_id', 'group_id'], 'calresources_restricts_eeg'); 157 | } 158 | 159 | /** 160 | * @see \OCA\CalendarResourceManagement\Db\RoomModel 161 | */ 162 | if (!$schema->hasTable('calresources_rooms')) { 163 | $table = $schema->createTable('calresources_rooms'); 164 | $table->addColumn('id', Types::BIGINT, [ 165 | 'autoincrement' => true, 166 | 'notnull' => true, 167 | 'length' => 11, 168 | 'unsigned' => true, 169 | ]); 170 | $table->addColumn('story_id', Types::BIGINT, [ 171 | 'notnull' => true, 172 | 'length' => 11, 173 | 'unsigned' => true, 174 | ]); 175 | $table->addColumn('uid', Types::STRING, [ 176 | 'notnull' => true, 177 | 'length' => 255, 178 | ]); 179 | $table->addColumn('display_name', Types::STRING, [ 180 | 'notnull' => true, 181 | 'length' => 255, 182 | ]); 183 | $table->addColumn('email', Types::STRING, [ 184 | 'notnull' => true, 185 | 'length' => 255, 186 | ]); 187 | $table->addColumn('room_type', Types::STRING, [ 188 | 'notnull' => true, 189 | 'length' => 255, 190 | ]); 191 | $table->addColumn('contact_person_user_id', Types::STRING, [ 192 | 'notnull' => false, 193 | 'length' => 255, 194 | ]); 195 | $table->addColumn('capacity', Types::INTEGER, [ 196 | 'notnull' => false, 197 | ]); 198 | $table->addColumn('room_number', Types::STRING, [ 199 | 'notnull' => false, 200 | 'length' => 255, 201 | ]); 202 | $table->addColumn('has_phone', Types::BOOLEAN, [ 203 | 'notnull' => false, 204 | 'default' => false 205 | ]); 206 | $table->addColumn('has_video_conferencing', Types::BOOLEAN, [ 207 | 'notnull' => false, 208 | 'default' => false 209 | ]); 210 | $table->addColumn('has_tv', Types::BOOLEAN, [ 211 | 'notnull' => false, 212 | 'default' => false 213 | ]); 214 | $table->addColumn('has_projector', Types::BOOLEAN, [ 215 | 'notnull' => false, 216 | 'default' => false 217 | ]); 218 | $table->addColumn('has_whiteboard', Types::BOOLEAN, [ 219 | 'notnull' => false, 220 | 'default' => false 221 | ]); 222 | $table->addColumn('is_wheelchair_accessible', Types::BOOLEAN, [ 223 | 'notnull' => false, 224 | 'default' => false 225 | ]); 226 | 227 | $table->setPrimaryKey(['id']); 228 | $table->addIndex(['story_id'], 'calresources_rooms_sid'); 229 | $table->addUniqueIndex(['uid'], 'calresources_rooms_uid'); 230 | $table->addUniqueIndex(['email'], 'calresources_rooms_eml'); 231 | } 232 | 233 | /** 234 | * @see \OCA\CalendarResourceManagement\Db\VehicleModel 235 | */ 236 | if (!$schema->hasTable('calresources_vehicles')) { 237 | $table = $schema->createTable('calresources_vehicles'); 238 | $table->addColumn('id', Types::BIGINT, [ 239 | 'autoincrement' => true, 240 | 'notnull' => true, 241 | 'length' => 11, 242 | 'unsigned' => true, 243 | ]); 244 | $table->addColumn('uid', Types::STRING, [ 245 | 'notnull' => true, 246 | 'length' => 255, 247 | ]); 248 | $table->addColumn('building_id', Types::BIGINT, [ 249 | 'notnull' => true, 250 | 'length' => 11, 251 | 'unsigned' => true, 252 | ]); 253 | $table->addColumn('display_name', Types::STRING, [ 254 | 'notnull' => true, 255 | 'length' => 255, 256 | ]); 257 | $table->addColumn('email', Types::STRING, [ 258 | 'notnull' => true, 259 | 'length' => 255, 260 | ]); 261 | $table->addColumn('resource_type', Types::STRING, [ 262 | 'notnull' => true, 263 | 'length' => 255, 264 | 'default' => 'vehicle', 265 | ]); 266 | $table->addColumn('contact_person_user_id', Types::STRING, [ 267 | 'notnull' => false, 268 | 'length' => 255, 269 | ]); 270 | $table->addColumn('vehicle_type', Types::STRING, [ 271 | 'notnull' => true, 272 | 'length' => 255, 273 | ]); 274 | $table->addColumn('vehicle_make', Types::STRING, [ 275 | 'notnull' => true, 276 | 'length' => 255, 277 | ]); 278 | $table->addColumn('vehicle_model', Types::STRING, [ 279 | 'notnull' => true, 280 | 'length' => 255, 281 | ]); 282 | $table->addColumn('is_electric', Types::BOOLEAN, [ 283 | 'notnull' => false, 284 | 'default' => false 285 | ]); 286 | $table->addColumn('range', Types::INTEGER, [ 287 | 'notnull' => false, 288 | ]); 289 | $table->addColumn('seating_capacity', Types::INTEGER, [ 290 | 'notnull' => false, 291 | ]); 292 | 293 | $table->setPrimaryKey(['id']); 294 | $table->addIndex(['building_id'], 'calresources_vehicles_bid'); 295 | $table->addUniqueIndex(['uid'], 'calresources_vehicles_uid'); 296 | $table->addUniqueIndex(['email'], 'calresources_vehicles_eml'); 297 | } 298 | 299 | $buildings = $schema->getTable('calresources_buildings'); 300 | 301 | // add building FK to resources 302 | $resources = $schema->getTable('calresources_resources'); 303 | $resources->addForeignKeyConstraint($buildings, ['building_id'], ['id'], ['onDelete' => 'CASCADE']); 304 | 305 | // add building FK to vehicles 306 | $vehicles = $schema->getTable('calresources_vehicles'); 307 | $vehicles->addForeignKeyConstraint($buildings, ['building_id'], ['id'], ['onDelete' => 'CASCADE']); 308 | 309 | // add building FK to stories 310 | $stories = $schema->getTable('calresources_stories'); 311 | $stories->addForeignKeyConstraint($buildings, ['building_id'], ['id'], ['onDelete' => 'CASCADE']); 312 | 313 | // add stories FK to rooms 314 | $rooms = $schema->getTable('calresources_rooms'); 315 | $rooms->addForeignKeyConstraint($stories, ['story_id'], ['id'], ['onDelete' => 'CASCADE']); 316 | 317 | return $schema; 318 | } 319 | } 320 | --------------------------------------------------------------------------------