├── 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 | [](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 |
--------------------------------------------------------------------------------