2 Commits

Author SHA1 Message Date
3218c354bb fix seeds
All checks were successful
CI/CD Pipeline / test (push) Successful in 10m56s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2023-12-28 18:18:10 +01:00
df8f603afa in preparation to moving userdata into app, we switched to arbitrary groups 2023-12-28 18:18:08 +01:00
201 changed files with 5385 additions and 21838 deletions

View File

@@ -1,6 +0,0 @@
target/
db.sqlite
.history/
frontend/node_modules/*
/static/
/data-ergo/

View File

@@ -11,45 +11,59 @@ env:
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: git.hofer.link/philipp/ci-images:rust-latest container: rust:latest
steps: steps:
- uses: actions/checkout@v3 - name: Setup Environment
- name: Run Test DB Script run: |
run: ./test_db.sh apt-get update -qq && apt-get install -y -qq sshpass musl musl-tools sqlite3 curl gnupg && mkdir -p /etc/apt/keyrings | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install nodejs -y && apt-get install npm -y
- name: Cache Cargo dependencies - name: Checkout
uses: Swatinem/rust-cache@v2 uses: actions/checkout@v3
- name: Build - name: Run Test DB Script
run: | run: ./test_db.sh
cargo build
cd frontend && npm install && npm run build - name: Build
- name: Frontend tests run: |
run: cd frontend && npx playwright install && npx playwright test --workers 1 --reporter html,line cargo build
- uses: actions/upload-artifact@v3 cd frontend && npm install && npm run build
if: always()
with: - name: Run Tests
name: playwright-report run: cargo test --verbose
path: frontend/playwright-report/
retention-days: 30
- name: Backend tests
run: cargo test --verbose
deploy-staging: deploy-staging:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: git.hofer.link/philipp/ci-images:rust-latest container: rust:latest
needs: [test] needs: [test]
if: github.ref == 'refs/heads/staging' if: github.ref == 'refs/heads/staging'
steps: steps:
- name: Setup Environment
run: |
rustup target add $CARGO_TARGET
apt-get update -qq && apt-get install -y -qq sshpass musl musl-tools sqlite3 curl gnupg
# Handling NodeSource GPG key
curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key -o nodesource.gpg.key
if [ -f /etc/apt/keyrings/nodesource.gpg ]; then
rm /etc/apt/keyrings/nodesource.gpg
fi
gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg nodesource.gpg.key
# Adding NodeSource repository
echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
# Installing Node.js and npm
apt-get update
apt-get install nodejs -y
apt-get install npm -y
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Run Test DB Script - name: Run Test DB Script
run: ./test_db.sh run: ./test_db.sh
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Build - name: Build
run: | run: |
cargo build --release --target $CARGO_TARGET cargo build --release --target $CARGO_TARGET
@@ -58,21 +72,21 @@ jobs:
- name: Deploy to Staging - name: Deploy to Staging
run: | run: |
mkdir -p ~/.ssh mkdir ~/.ssh
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa
scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/root/rowing-staging/rot-updating scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/rot-updating
scp -C staging-diff.sql $SSH_USER@$SSH_HOST:/root/rowing-staging/ scp staging-diff.sql $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
scp -C -r static $SSH_USER@$SSH_HOST:/root/rowing-staging/ scp -r static $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
scp -C -r templates $SSH_USER@$SSH_HOST:/root/rowing-staging/ scp -r templates $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
scp -C -r svelte $SSH_USER@$SSH_HOST:/root/rowing-staging/ scp -r svelte $SSH_USER@$SSH_HOST:/home/k004373/rowing-staging/
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rowing-staging' ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rotstaging'
ssh $SSH_USER@$SSH_HOST 'rm -f /root/rowing-staging/db.sqlite && cp /root/rowing-prod/db.sqlite /root/rowing-staging/db.sqlite && mkdir -p /root/rowing-staging/svelte/build && mkdir -p /root/rowing-staging/data-ergo/thirty && mkdir -p /root/rowing-staging/data-ergo/dozen && sqlite3 /root/rowing-staging/db.sqlite < /root/rowing-staging/staging-diff.sql' ssh $SSH_USER@$SSH_HOST 'rm /home/k004373/rowing-staging/db.sqlite && cp /home/k004373/rowing/db.sqlite /home/k004373/rowing-staging/db.sqlite && mkdir -p /home/k004373/rowing-staging/svelte/build && mkdir -p /home/k004373/rowing-staging/data-ergo/thirty && mkdir -p /home/k004373/rowing-staging/data-ergo/dozen && sqlite3 /home/k004373/rowing-staging/db.sqlite < /home/k004373/rowing-staging/staging-diff.sql'
ssh $SSH_USER@$SSH_HOST 'mv /root/rowing-staging/rot-updating /root/rowing-staging/rot' ssh $SSH_USER@$SSH_HOST 'mv /home/k004373/rowing-staging/rot-updating /home/k004373/rowing-staging/rot'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rowing-staging' ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rotstaging'
env: env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ secrets.SSH_HOST }} SSH_HOST: ${{ secrets.SSH_HOST }}
@@ -80,18 +94,20 @@ jobs:
deploy-main: deploy-main:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: git.hofer.link/philipp/ci-images:rust-latest container: rust:latest
needs: [test] needs: [test]
if: github.ref == 'refs/heads/main' if: github.ref == 'refs/heads/main'
steps: steps:
- name: Setup Environment
run: |
rustup target add $CARGO_TARGET
apt-get update -qq && apt-get install -y -qq sshpass musl musl-tools sqlite3 curl gnupg && mkdir -p /etc/apt/keyrings | curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_16.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list && apt-get update && apt-get install nodejs -y && apt-get install npm -y
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Run Test DB Script - name: Run Test DB Script
run: ./test_db.sh run: ./test_db.sh
- name: Cache Cargo dependencies
uses: Swatinem/rust-cache@v2
- name: Build - name: Build
run: | run: |
@@ -99,21 +115,21 @@ jobs:
strip target/$CARGO_TARGET/release/rot strip target/$CARGO_TARGET/release/rot
cd frontend && npm install && npm run build cd frontend && npm install && npm run build
- name: Deploy to production - name: Deploy to Main
run: | run: |
mkdir -p ~/.ssh mkdir ~/.ssh
ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts ssh-keyscan -H $SSH_HOST >> ~/.ssh/known_hosts
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa
scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/root/rowing-prod/rot-updating scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/k004373/rowing/rot-updating
scp -C -r static $SSH_USER@$SSH_HOST:/root/rowing-prod/ scp -r static $SSH_USER@$SSH_HOST:/home/k004373/rowing/
scp -C -r templates $SSH_USER@$SSH_HOST:/root/rowing-prod/ scp -r templates $SSH_USER@$SSH_HOST:/home/k004373/rowing/
scp -C -r svelte $SSH_USER@$SSH_HOST:/root/rowing-prod/ scp -r svelte $SSH_USER@$SSH_HOST:/home/k004373/rowing/
ssh $SSH_USER@$SSH_HOST 'mkdir -p /root/rowing-prod/svelte/build && mkdir -p /root/rowing-prod/data-ergo/thirty && mkdir -p /root/rowing-prod/data-ergo/dozen' ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/k004373/rowing/svelte/build && mkdir -p /home/k004373/rowing/data-ergo/thirty && mkdir -p /home/k004373/rowing/data-ergo/dozen'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rowing-prod' ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rot'
ssh $SSH_USER@$SSH_HOST 'mv /root/rowing-prod/rot-updating /root/rowing-prod/rot' ssh $SSH_USER@$SSH_HOST 'mv /home/k004373/rowing/rot-updating /home/k004373/rowing/rot'
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rowing-prod' ssh $SSH_USER@$SSH_HOST 'sudo systemctl start rot'
env: env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
SSH_HOST: ${{ secrets.SSH_HOST }} SSH_HOST: ${{ secrets.SSH_HOST }}

View File

@@ -1,51 +0,0 @@
name: Update Cargo Dependencies
on:
schedule:
- cron: '0 2 * * 5' # Run weekly on Friday at 2am
workflow_dispatch: # Allow manual triggering
jobs:
update-dependencies:
runs-on: ubuntu-latest
container: git.hofer.link/philipp/ci-images:rust-latest
steps:
- uses: actions/checkout@v3
- name: Update dependencies
run: |
cargo upgrade
cargo update
- name: Create Pull Request Staging
uses: https://git.hofer.link/philipp/create-pull-request@18ef1fdad70eec569ab10292c1fa79c1b5296370
with:
token: ${{ secrets.GITEATOKEN }}
commit-message: Update Cargo dependencies
title: Update Cargo dependencies
body: |
This PR updates Cargo dependencies to their latest versions.
@philipp
- Run `cargo upgrade` to update version requirements in Cargo.toml
- Run `cargo update` to update Cargo.lock
branch: update-cargo-dependencies
delete-branch: false
- name: Create Pull Request Main
uses: https://git.hofer.link/philipp/create-pull-request@18ef1fdad70eec569ab10292c1fa79c1b5296370
with:
token: ${{ secrets.GITEATOKEN }}
commit-message: Update Cargo dependencies
title: Update Cargo dependencies
body: |
This PR updates Cargo dependencies to their latest versions.
@philipp
- Run `cargo upgrade` to update version requirements in Cargo.toml
- Run `cargo update` to update Cargo.lock
branch: update-cargo-dependencies
base: main
delete-branch: true

1
.gitignore vendored
View File

@@ -5,4 +5,3 @@ Rocket.toml
frontend/node_modules/* frontend/node_modules/*
/static/ /static/
/data-ergo/ /data-ergo/
usage.txt

2231
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "rot" name = "rot"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2021"
[features] [features]
default = ["rest", "rowing-tera" ] default = ["rest", "rowing-tera" ]
@@ -9,26 +9,16 @@ rowing-tera = ["rocket_dyn_templates", "tera"]
rest = [] rest = []
[dependencies] [dependencies]
rocket = { version = "0.5", features = ["secrets"]} rocket = { version = "0.5.0", features = ["secrets"]}
rocket_dyn_templates = {version = "0.2", features = [ "tera" ], optional = true } rocket_dyn_templates = {version = "0.1.0", features = [ "tera" ], optional = true }
log = "0.4" log = "0.4"
env_logger = "0.11" env_logger = "0.10"
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono"] } sqlx = { version = "0.7", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono", "time"] }
argon2 = "0.5" argon2 = "0.5"
serde = { version = "1.0", features = [ "derive" ]} serde = { version = "1.0", features = [ "derive" ]}
serde_json = "1.0" serde_json = "1.0"
chrono = { version = "0.4", features = ["serde"]} chrono = { version = "0.4", features = ["serde"]}
chrono-tz = "0.10" chrono-tz = "0.8"
tera = { version = "1.20", features = ["date-locale"], optional = true} tera = { version = "1.18", features = ["date-locale"], optional = true}
ics = "0.5" ics = "0.5"
futures = "0.3" futures = "0.3"
lettre = "0.11"
csv = "1.3"
itertools = "0.14"
job_scheduler_ng = "2.2"
ureq = { version = "3.0", features = ["json"] }
regex = "1.11"
urlencoding = "2.1"
[target.'cfg(not(windows))'.dependencies]
openssl = { version = "0.10", features = [ "vendored" ] }

287
LICENSE
View File

@@ -1,287 +0,0 @@
EUROPEAN UNION PUBLIC LICENCE v. 1.2
EUPL © the European Union 2007, 2016
This European Union Public Licence (the EUPL) applies to the Work (as defined
below) which is provided under the terms of this Licence. Any use of the Work,
other than as authorised under this Licence is prohibited (to the extent such
use is covered by a right of the copyright holder of the Work).
The Work is provided under the terms of this Licence when the Licensor (as
defined below) has placed the following notice immediately following the
copyright notice for the Work:
Licensed under the EUPL
or has expressed by any other means his willingness to license under the EUPL.
1. Definitions
In this Licence, the following terms have the following meaning:
- The Licence: this Licence.
- The Original Work: the work or software distributed or communicated by the
Licensor under this Licence, available as Source Code and also as Executable
Code as the case may be.
- Derivative Works: the works or software that could be created by the
Licensee, based upon the Original Work or modifications thereof. This Licence
does not define the extent of modification or dependence on the Original Work
required in order to classify a work as a Derivative Work; this extent is
determined by copyright law applicable in the country mentioned in Article 15.
- The Work: the Original Work or its Derivative Works.
- The Source Code: the human-readable form of the Work which is the most
convenient for people to study and modify.
- The Executable Code: any code which has generally been compiled and which is
meant to be interpreted by a computer as a program.
- The Licensor: the natural or legal person that distributes or communicates
the Work under the Licence.
- Contributor(s): any natural or legal person who modifies the Work under the
Licence, or otherwise contributes to the creation of a Derivative Work.
- The Licensee or You: any natural or legal person who makes any usage of
the Work under the terms of the Licence.
- Distribution or Communication: any act of selling, giving, lending,
renting, distributing, communicating, transmitting, or otherwise making
available, online or offline, copies of the Work or providing access to its
essential functionalities at the disposal of any other natural or legal
person.
2. Scope of the rights granted by the Licence
The Licensor hereby grants You a worldwide, royalty-free, non-exclusive,
sublicensable licence to do the following, for the duration of copyright vested
in the Original Work:
- use the Work in any circumstance and for all usage,
- reproduce the Work,
- modify the Work, and make Derivative Works based upon the Work,
- communicate to the public, including the right to make available or display
the Work or copies thereof to the public and perform publicly, as the case may
be, the Work,
- distribute the Work or copies thereof,
- lend and rent the Work or copies thereof,
- sublicense rights in the Work or copies thereof.
Those rights can be exercised on any media, supports and formats, whether now
known or later invented, as far as the applicable law permits so.
In the countries where moral rights apply, the Licensor waives his right to
exercise his moral right to the extent allowed by law in order to make effective
the licence of the economic rights here above listed.
The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to
any patents held by the Licensor, to the extent necessary to make use of the
rights granted on the Work under this Licence.
3. Communication of the Source Code
The Licensor may provide the Work either in its Source Code form, or as
Executable Code. If the Work is provided as Executable Code, the Licensor
provides in addition a machine-readable copy of the Source Code of the Work
along with each copy of the Work that the Licensor distributes or indicates, in
a notice following the copyright notice attached to the Work, a repository where
the Source Code is easily and freely accessible for as long as the Licensor
continues to distribute or communicate the Work.
4. Limitations on copyright
Nothing in this Licence is intended to deprive the Licensee of the benefits from
any exception or limitation to the exclusive rights of the rights owners in the
Work, of the exhaustion of those rights or of other applicable limitations
thereto.
5. Obligations of the Licensee
The grant of the rights mentioned above is subject to some restrictions and
obligations imposed on the Licensee. Those obligations are the following:
Attribution right: The Licensee shall keep intact all copyright, patent or
trademarks notices and all notices that refer to the Licence and to the
disclaimer of warranties. The Licensee must include a copy of such notices and a
copy of the Licence with every copy of the Work he/she distributes or
communicates. The Licensee must cause any Derivative Work to carry prominent
notices stating that the Work has been modified and the date of modification.
Copyleft clause: If the Licensee distributes or communicates copies of the
Original Works or Derivative Works, this Distribution or Communication will be
done under the terms of this Licence or of a later version of this Licence
unless the Original Work is expressly distributed only under this version of the
Licence — for example by communicating EUPL v. 1.2 only. The Licensee
(becoming Licensor) cannot offer or impose any additional terms or conditions on
the Work or Derivative Work that alter or restrict the terms of the Licence.
Compatibility clause: If the Licensee Distributes or Communicates Derivative
Works or copies thereof based upon both the Work and another work licensed under
a Compatible Licence, this Distribution or Communication can be done under the
terms of this Compatible Licence. For the sake of this clause, Compatible
Licence refers to the licences listed in the appendix attached to this Licence.
Should the Licensee's obligations under the Compatible Licence conflict with
his/her obligations under this Licence, the obligations of the Compatible
Licence shall prevail.
Provision of Source Code: When distributing or communicating copies of the Work,
the Licensee will provide a machine-readable copy of the Source Code or indicate
a repository where this Source will be easily and freely available for as long
as the Licensee continues to distribute or communicate the Work.
Legal Protection: This Licence does not grant permission to use the trade names,
trademarks, service marks, or names of the Licensor, except as required for
reasonable and customary use in describing the origin of the Work and
reproducing the content of the copyright notice.
6. Chain of Authorship
The original Licensor warrants that the copyright in the Original Work granted
hereunder is owned by him/her or licensed to him/her and that he/she has the
power and authority to grant the Licence.
Each Contributor warrants that the copyright in the modifications he/she brings
to the Work are owned by him/her or licensed to him/her and that he/she has the
power and authority to grant the Licence.
Each time You accept the Licence, the original Licensor and subsequent
Contributors grant You a licence to their contributions to the Work, under the
terms of this Licence.
7. Disclaimer of Warranty
The Work is a work in progress, which is continuously improved by numerous
Contributors. It is not a finished work and may therefore contain defects or
bugs inherent to this type of development.
For the above reason, the Work is provided under the Licence on an as is basis
and without warranties of any kind concerning the Work, including without
limitation merchantability, fitness for a particular purpose, absence of defects
or errors, accuracy, non-infringement of intellectual property rights other than
copyright as stated in Article 6 of this Licence.
This disclaimer of warranty is an essential part of the Licence and a condition
for the grant of any rights to the Work.
8. Disclaimer of Liability
Except in the cases of wilful misconduct or damages directly caused to natural
persons, the Licensor will in no event be liable for any direct or indirect,
material or moral, damages of any kind, arising out of the Licence or of the use
of the Work, including without limitation, damages for loss of goodwill, work
stoppage, computer failure or malfunction, loss of data or any commercial
damage, even if the Licensor has been advised of the possibility of such damage.
However, the Licensor will be liable under statutory product liability laws as
far such laws apply to the Work.
9. Additional agreements
While distributing the Work, You may choose to conclude an additional agreement,
defining obligations or services consistent with this Licence. However, if
accepting obligations, You may act only on your own behalf and on your sole
responsibility, not on behalf of the original Licensor or any other Contributor,
and only if You agree to indemnify, defend, and hold each Contributor harmless
for any liability incurred by, or claims asserted against such Contributor by
the fact You have accepted any warranty or additional liability.
10. Acceptance of the Licence
The provisions of this Licence can be accepted by clicking on an icon I agree
placed under the bottom of a window displaying the text of this Licence or by
affirming consent in any other similar way, in accordance with the rules of
applicable law. Clicking on that icon indicates your clear and irrevocable
acceptance of this Licence and all of its terms and conditions.
Similarly, you irrevocably accept this Licence and all of its terms and
conditions by exercising any rights granted to You by Article 2 of this Licence,
such as the use of the Work, the creation by You of a Derivative Work or the
Distribution or Communication by You of the Work or copies thereof.
11. Information to the public
In case of any Distribution or Communication of the Work by means of electronic
communication by You (for example, by offering to download the Work from a
remote location) the distribution channel or media (for example, a website) must
at least provide to the public the information requested by the applicable law
regarding the Licensor, the Licence and the way it may be accessible, concluded,
stored and reproduced by the Licensee.
12. Termination of the Licence
The Licence and the rights granted hereunder will terminate automatically upon
any breach by the Licensee of the terms of the Licence.
Such a termination will not terminate the licences of any person who has
received the Work from the Licensee under the Licence, provided such persons
remain in full compliance with the Licence.
13. Miscellaneous
Without prejudice of Article 9 above, the Licence represents the complete
agreement between the Parties as to the Work.
If any provision of the Licence is invalid or unenforceable under applicable
law, this will not affect the validity or enforceability of the Licence as a
whole. Such provision will be construed or reformed so as necessary to make it
valid and enforceable.
The European Commission may publish other linguistic versions or new versions of
this Licence or updated versions of the Appendix, so far this is required and
reasonable, without reducing the scope of the rights granted by the Licence. New
versions of the Licence will be published with a unique version number.
All linguistic versions of this Licence, approved by the European Commission,
have identical value. Parties can take advantage of the linguistic version of
their choice.
14. Jurisdiction
Without prejudice to specific agreement between parties,
- any litigation resulting from the interpretation of this License, arising
between the European Union institutions, bodies, offices or agencies, as a
Licensor, and any Licensee, will be subject to the jurisdiction of the Court
of Justice of the European Union, as laid down in article 272 of the Treaty on
the Functioning of the European Union,
- any litigation arising between other parties and resulting from the
interpretation of this License, will be subject to the exclusive jurisdiction
of the competent court where the Licensor resides or conducts its primary
business.
15. Applicable Law
Without prejudice to specific agreement between parties,
- this Licence shall be governed by the law of the European Union Member State
where the Licensor has his seat, resides or has his registered office,
- this licence shall be governed by Belgian law if the Licensor has no seat,
residence or registered office inside a European Union Member State.
Appendix
Compatible Licences according to Article 5 EUPL are:
- GNU General Public License (GPL) v. 2, v. 3
- GNU Affero General Public License (AGPL) v. 3
- Open Software License (OSL) v. 2.1, v. 3.0
- Eclipse Public License (EPL) v. 1.0
- CeCILL v. 2.0, v. 2.1
- Mozilla Public Licence (MPL) v. 2
- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3
- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for
works other than software
- European Union Public Licence (EUPL) v. 1.1, v. 1.2
- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong
Reciprocity (LiLiQ-R+).
The European Commission may update this Appendix to later versions of the above
licences without producing a new version of the EUPL, as long as they provide
the rights granted in Article 2 of this Licence and protect the covered Source
Code from exclusive appropriation.
All other changes or additions to this Appendix require the production of a new
EUPL version.

View File

@@ -1,48 +1,15 @@
![latest CI run on main](https://git.hofer.link/Ruderverein-Donau-Linz/rowt/actions/workflows/action.yml/badge.svg?branch=main) # Frontend Process
´cd frontend´
´npm install´
´npm run (watch/build)´
# Build # Notes / Bugfixes
## Frontend ## Frontend
1. `cd frontend` - [] support esc to close sidebar
2. `npm install` - [] reload page -> don't throw input away!
3. `npm run (watch/build)`
# Run
## Backend ## Backend
1. `cargo r`
# Test # Nice to have
## Frontend ## Frontend
- `npx playwright test --workers 1 --project firefox` - [] my trips for cox
- Nice UI: `--ui`
- Generate tests: `npx playwright codegen`
## Backend (Unit + Integration)
`cargo t`
# Lints
- Rust: `cargo check`
- Tera files: `djlint **.html.tera --profile=jinja --reformat`
- Typescript: `prettier -w *.ts`
# Dependencies
- `sqlite3`
- `rust`
# Nginx config
```
server {
server_name staging.rudernlinz.at;
location / {
proxy_pass http://localhost:7999/; # The / is important!
}
}
server {
server_name app.rudernlinz.at;
location / {
proxy_pass http://localhost:8001/; # The / is important!
}
}
```

View File

@@ -2,7 +2,3 @@
secret_key = "/NtVGizglEoyoxBLzsRDWTy4oAG1qDw4J4O+CWJSv+fypD7W9sam8hUY4j90EZsbZk8wEradS5zBoWtWKi3k8w==" secret_key = "/NtVGizglEoyoxBLzsRDWTy4oAG1qDw4J4O+CWJSv+fypD7W9sam8hUY4j90EZsbZk8wEradS5zBoWtWKi3k8w=="
rss_key = "rss-key-for-ci" rss_key = "rss-key-for-ci"
limits = { file = "10 MiB", data-form = "10 MiB"} limits = { file = "10 MiB", data-form = "10 MiB"}
smtp_pw = "my-smtp-password"
usage_log_path = "./usage.txt"
openweathermap_key = "openweather-key"
wordpress_key = "pw-to-allow-sending-notifications"

View File

@@ -1,6 +0,0 @@
#!/bin/bash
rm -f db.sqlite
touch db.sqlite
sqlite3 db.sqlite < migration.sql
sqlite3 db.sqlite < seeds_demo.sql

View File

@@ -1,59 +0,0 @@
# Database
Since the database stabilized quite well over the last months/years, hopefully it will not change that much in the future.
Thus, here is the current (October '24) model and the reasoning behind it:
## User
![](./user.svg)
- All user-relevant fields are stored in `User`.
- `Role` (and its associative table `UserRole`) map current roles the user has. This is used for e.g. permissions (`Vorstand`, `Admin`, `cox`, ... roles) and fee calculation (`Donau Linz`, `scheckbuch`, `Rennjugend`).
- `Family` specifies, well, a family. Currently only used for fee calculation.
- `cluster` in `Role` groups roles together. There is a db check to only allow for at most 1 role of the same cluster (e.g. either `cox` or `bootsfuehrer`, but not both).
## Planned rowing adventures :-)
![](./planned.svg)
There are 2 main types:
1. **Trips:** Trips can be created by every cox. They are "simple", every-day trips.
2. **Events:** Events can be created by everyone who has the `manage_events` role. They are used if multiple coxes are needed, e.g. for "Fetzenfahrt", "Anrudern", .... Additionally, events are shown in public calendar (e.g. on the website).
`TripDetails` extracts the common data for both Trips and Events.
Rower can register using the `UserTrip` table.
This table expects either...
- a `user_id`, if a person who has an account registers to the trip/event
- a `user_note`, if the cox of a trip, or a `manage_events` user of an event wants to add a guest which has no account
## Logbook
![](./logbook.svg)
If `arrival` is NULL, the boat is assumed to still be on the water.
There are a few `LogbookType`s:
- `Wanderfahrt`: Used to check if a user has accomplished their `Fahrtenabzeichen` in the current year.
- `Regatta`
If the number of users entered is less than the boat's maximum capacity, the remaining spaces will be automatically assigned to guests.
## Boat
![](./boat.svg)
## Trailer
![](./trailer.svg)
## Fetching
![](./fetching.svg)
This tables are used to automatically fetch data (every hour). Currently we have:
- `Waterlevel` which fetches the current waterlevel in Linz from hydro (with their explicit permission :-))
- `Weather` weather data from *Open Weather*
## Misc
![](./misc.svg)
- **Log:** Logs "interesting" activities, to be viewed in the web ui
- **Notification**
- **Distance:** Default distances of certain common targets

View File

@@ -1,69 +0,0 @@
classDiagram
class Boat {
+int id
+string name
+int amount_seats
+int location_id
+int owner
+int year_built
+string boatbuilder
+bool default_shipmaster_only_steering
+bool convert_handoperated_possible
+string default_destination
+bool skull
+bool external
+bool deleted
}
class Location {
+int id
+string name
}
class Boathouse {
+int id
+int boat_id
+string aisle
+string side
+int level
}
class BoatDamage {
+int id
+int boat_id
+string desc
+int user_id_created
+datetime created_at
+int user_id_fixed
+datetime fixed_at
+int user_id_verified
+datetime verified_at
+bool lock_boat
}
class BoatReservation {
+int id
+int boat_id
+date start_date
+date end_date
+string time_desc
+string usage
+int user_id_applicant
+int user_id_confirmation
+datetime created_at
}
class User {
...
}
Boat "*" -- "1" User : owner
Boat "*" -- "1" Location
Boathouse "*" -- "1" Boat
BoatDamage "*" -- "1" Boat
BoatDamage "*" -- "1" User : created_by
BoatDamage "*" -- "0..1" User : fixed_by
BoatDamage "*" -- "0..1" User : verified_by
BoatReservation "*" -- "1" Boat
BoatReservation "*" -- "1" User : applicant
BoatReservation "*" -- "0..1" User : confirmed_by

View File

@@ -1 +0,0 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 825.1484375 855" style="max-width: 825.148px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"><path style="fill:none" class="edge-pattern-solid relation" id="id_Boat_User_1" d="M139.711,678L139.135,683.667C138.559,689.333,137.406,700.667,200.161,719.538C262.915,738.409,389.577,764.818,452.908,778.023L516.238,791.227"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Boat_Location_2" d="M242.162,678L244.971,683.667C247.78,689.333,253.398,700.667,243.99,714.444C234.583,728.222,210.151,744.444,197.935,752.556L185.719,760.667"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Boathouse_Boat_3" d="M111.75,230L111.75,243.333C111.75,256.667,111.75,283.333,112.713,300.833C113.675,318.333,115.601,326.667,116.563,330.833L117.526,335"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatDamage_Boat_4" d="M336.91,204.012L311.453,221.677C285.996,239.342,235.082,274.671,209.052,296.502C183.022,318.333,181.876,326.667,181.303,330.833L180.73,335"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatDamage_User_5" d="M386.832,285L385.841,289.167C384.849,293.333,382.866,301.667,381.874,338.583C380.883,375.5,380.883,441,380.883,508C380.883,575,380.883,643.5,403.442,689.616C426.001,735.733,471.12,759.465,493.679,771.331L516.238,783.198"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatDamage_User_6" d="M462.548,285L463.834,289.167C465.121,293.333,467.693,301.667,468.979,338.583C470.266,375.5,470.266,441,470.266,508C470.266,575,470.266,643.5,477.928,686.835C485.59,730.171,500.914,748.342,508.576,757.427L516.238,766.513"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatDamage_User_7" d="M502.676,244.022L512.021,255.018C521.367,266.015,540.059,288.007,549.404,331.754C558.75,375.5,558.75,441,558.75,508C558.75,575,558.75,643.5,557.221,685.25C555.693,727,552.635,742,551.107,749.5L549.578,757"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatReservation_Boat_8" d="M622.159,274L619.15,280C616.141,286,610.123,298,557.48,325.821C504.836,353.643,405.566,397.286,355.932,419.107L306.297,440.928"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatReservation_User_9" d="M672.826,274L672.201,280C671.576,286,670.327,298,669.703,336.75C669.078,375.5,669.078,441,669.078,508C669.078,575,669.078,643.5,652.035,689.041C634.991,734.582,600.904,757.164,583.86,768.455L566.816,779.746"/><path style="fill:none" class="edge-pattern-solid relation" id="id_BoatReservation_User_10" d="M750.844,274L753.891,280C756.938,286,763.031,298,766.078,336.75C769.125,375.5,769.125,441,769.125,508C769.125,575,769.125,643.5,735.407,690.268C701.689,737.037,634.253,762.074,600.535,774.592L566.816,787.111"/></g><g class="edgeLabels"><g transform="translate(136.25390625, 712)" class="edgeLabel"><g transform="translate(-21.7890625, -9)" class="label"><foreignObject height="18" width="43.578125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">owner</span></span></div></foreignObject></g></g><g transform="translate(123.43097887498585, 693.9244385293466)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(497.16837675300997, 767.971031051437)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(236.4945714638542, 700.3413024031892)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(203.59489959445315, 758.4829443529314)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(96.75, 247.5)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(124.43018037783705, 310.3413665531297)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(313.9811061744754, 201.66525613262883)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(195.50573949028518, 319.9824574544449)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(380.8828125, 506.5)" class="edgeLabel"><g transform="translate(-39.5859375, -9)" class="label"><foreignObject height="18" width="79.171875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">created_by</span></span></div></foreignObject></g></g><g transform="translate(369.078258009932, 299.5874584688675)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(502.7332112607392, 756.7755266715053)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(470.265625, 506.5)" class="edgeLabel"><g transform="translate(-29.796875, -9)" class="label"><foreignObject height="18" width="59.59375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">fixed_by</span></span></div></foreignObject></g></g><g transform="translate(451.7824474859586, 305.25359206268877)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(511.4230142164039, 738.4648690133215)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 36px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">0..1</span></div></foreignObject></g><g transform="translate(558.75, 506.5)" class="edgeLabel"><g transform="translate(-38.6875, -9)" class="label"><foreignObject height="18" width="77.375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">verified_by</span></span></div></foreignObject></g></g><g transform="translate(502.5790655821325, 267.07045663806787)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(562.770958609737, 737.8482329279067)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 36px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">0..1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(600.9055693832728, 282.9191045372611)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(323.35394884645706, 442.6169068399622)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(669.078125, 506.5)" class="edgeLabel"><g transform="translate(-32.0234375, -9)" class="label"><foreignObject height="18" width="64.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">applicant</span></span></div></foreignObject></g></g><g transform="translate(656.0942860844532, 289.85291648785403)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(584.6896419950095, 777.5863880546077)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(769.125, 506.5)" class="edgeLabel"><g transform="translate(-48.0234375, -9)" class="label"><foreignObject height="18" width="96.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">confirmed_by</span></span></div></foreignObject></g></g><g transform="translate(745.3930018207337, 296.3950723175959)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(583.4430300371845, 790.0820960054875)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 36px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">0..1</span></div></foreignObject></g></g><g class="nodes"><g transform="translate(157.1484375, 506.5)" id="classId-Boat-0" class="node default"><rect height="343" width="298.296875" y="-171.5" x="-149.1484375" class="outer title-state" style=""/><line y2="-141.5" y1="-141.5" x2="149.1484375" x1="-149.1484375" class="divider"/><line y2="160.5" y1="160.5" x2="149.1484375" x1="-149.1484375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.78125, -164)" height="18" width="35.5625" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Boat</span></div></foreignObject><foreignObject transform="translate( -141.6484375, -130)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -141.6484375, -108)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject><foreignObject transform="translate( -141.6484375, -86)" height="18" width="131.203125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int amount_seats</span></div></foreignObject><foreignObject transform="translate( -141.6484375, -64)" height="18" width="107.1875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int location_id</span></div></foreignObject><foreignObject transform="translate( -141.6484375, -42)" height="18" width="74.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int owner</span></div></foreignObject><foreignObject transform="translate( -141.6484375, -20)" height="18" width="100.0625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int year_built</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 2)" height="18" width="132.09375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string boatbuilder</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 24)" height="18" width="283.296875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool default_shipmaster_only_steering</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 46)" height="18" width="271.765625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool convert_handoperated_possible</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 68)" height="18" width="187.25"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string default_destination</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 90)" height="18" width="76.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool skull</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 112)" height="18" width="100.96875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool external</span></div></foreignObject><foreignObject transform="translate( -141.6484375, 134)" height="18" width="96.53125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool deleted</span></div></foreignObject></g></g><g transform="translate(131.75, 796.5)" id="classId-Location-1" class="node default"><rect height="101" width="107.9375" y="-50.5" x="-53.96875" class="outer title-state" style=""/><line y2="-20.5" y1="-20.5" x2="53.96875" x1="-53.96875" class="divider"/><line y2="39.5" y1="39.5" x2="53.96875" x1="-53.96875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -33.3359375, -43)" height="18" width="66.671875" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Location</span></div></foreignObject><foreignObject transform="translate( -46.46875, -9)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -46.46875, 13)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject></g></g><g transform="translate(111.75, 146.5)" id="classId-Boathouse-2" class="node default"><rect height="167" width="100.828125" y="-83.5" x="-50.4140625" class="outer title-state" style=""/><line y2="-53.5" y1="-53.5" x2="50.4140625" x1="-50.4140625" class="divider"/><line y2="72.5" y1="72.5" x2="50.4140625" x1="-50.4140625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -41.3359375, -76)" height="18" width="82.671875" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Boathouse</span></div></foreignObject><foreignObject transform="translate( -42.9140625, -42)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -42.9140625, -20)" height="18" width="83.1875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int boat_id</span></div></foreignObject><foreignObject transform="translate( -42.9140625, 2)" height="18" width="85.828125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string aisle</span></div></foreignObject><foreignObject transform="translate( -42.9140625, 24)" height="18" width="82.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string side</span></div></foreignObject><foreignObject transform="translate( -42.9140625, 46)" height="18" width="63.59375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int level</span></div></foreignObject></g></g><g transform="translate(419.79296875, 146.5)" id="classId-BoatDamage-3" class="node default"><rect height="277" width="165.765625" y="-138.5" x="-82.8828125" class="outer title-state" style=""/><line y2="-108.5" y1="-108.5" x2="82.8828125" x1="-82.8828125" class="divider"/><line y2="127.5" y1="127.5" x2="82.8828125" x1="-82.8828125" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -48.90625, -131)" height="18" width="97.8125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">BoatDamage</span></div></foreignObject><foreignObject transform="translate( -75.3828125, -97)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -75.3828125, -75)" height="18" width="83.1875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int boat_id</span></div></foreignObject><foreignObject transform="translate( -75.3828125, -53)" height="18" width="86.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string desc</span></div></foreignObject><foreignObject transform="translate( -75.3828125, -31)" height="18" width="145.4375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_created</span></div></foreignObject><foreignObject transform="translate( -75.3828125, -9)" height="18" width="150.765625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime created_at</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 13)" height="18" width="125.859375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_fixed</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 35)" height="18" width="131.203125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime fixed_at</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 57)" height="18" width="143.640625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_verified</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 79)" height="18" width="148.984375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime verified_at</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 101)" height="18" width="112.53125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool lock_boat</span></div></foreignObject></g></g><g transform="translate(686.09765625, 146.5)" id="classId-BoatReservation-4" class="node default"><rect height="255" width="194.21875" y="-127.5" x="-97.109375" class="outer title-state" style=""/><line y2="-97.5" y1="-97.5" x2="97.109375" x1="-97.109375" class="divider"/><line y2="116.5" y1="116.5" x2="97.109375" x1="-97.109375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -63.578125, -120)" height="18" width="127.15625" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">BoatReservation</span></div></foreignObject><foreignObject transform="translate( -89.609375, -86)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -89.609375, -64)" height="18" width="83.1875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int boat_id</span></div></foreignObject><foreignObject transform="translate( -89.609375, -42)" height="18" width="116.09375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+date start_date</span></div></foreignObject><foreignObject transform="translate( -89.609375, -20)" height="18" width="111.671875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+date end_date</span></div></foreignObject><foreignObject transform="translate( -89.609375, 2)" height="18" width="125.84375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string time_desc</span></div></foreignObject><foreignObject transform="translate( -89.609375, 24)" height="18" width="96.515625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string usage</span></div></foreignObject><foreignObject transform="translate( -89.609375, 46)" height="18" width="156.109375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_applicant</span></div></foreignObject><foreignObject transform="translate( -89.609375, 68)" height="18" width="179.21875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_confirmation</span></div></foreignObject><foreignObject transform="translate( -89.609375, 90)" height="18" width="150.765625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime created_at</span></div></foreignObject></g></g><g transform="translate(541.52734375, 796.5)" id="classId-User-5" class="node default"><rect height="79" width="50.578125" y="-39.5" x="-25.2890625" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="25.2890625" x1="-25.2890625" class="divider"/><line y2="28.5" y1="28.5" x2="25.2890625" x1="-25.2890625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.7890625, -32)" height="18" width="35.578125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">User</span></div></foreignObject><foreignObject transform="translate( -17.7890625, 2)" height="18" width="13.34375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">...</span></div></foreignObject></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -1,22 +0,0 @@
classDiagram
class Waterlevel {
+int id
+date day
+string time
+int max
+int min
+int mittel
+int tumax
+int tumin
+int tumittel
}
class Weather {
+int id
+date day
+float max_temp
+float wind_gust
+float rain_mm
}

View File

@@ -1 +0,0 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 297.875 271" style="max-width: 297.875px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"/><g class="edgeLabels"/><g class="nodes"><g transform="translate(57.0703125, 135.5)" id="classId-Waterlevel-0" class="node default"><rect height="255" width="98.140625" y="-127.5" x="-49.0703125" class="outer title-state" style=""/><line y2="-97.5" y1="-97.5" x2="49.0703125" x1="-49.0703125" class="divider"/><line y2="116.5" y1="116.5" x2="49.0703125" x1="-49.0703125" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -39.7265625, -120)" height="18" width="79.453125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Waterlevel</span></div></foreignObject><foreignObject transform="translate( -41.5703125, -86)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -41.5703125, -64)" height="18" width="70.734375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+date day</span></div></foreignObject><foreignObject transform="translate( -41.5703125, -42)" height="18" width="83.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string time</span></div></foreignObject><foreignObject transform="translate( -41.5703125, -20)" height="18" width="60.921875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int max</span></div></foreignObject><foreignObject transform="translate( -41.5703125, 2)" height="18" width="56.46875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int min</span></div></foreignObject><foreignObject transform="translate( -41.5703125, 24)" height="18" width="68.921875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int mittel</span></div></foreignObject><foreignObject transform="translate( -41.5703125, 46)" height="18" width="74.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int tumax</span></div></foreignObject><foreignObject transform="translate( -41.5703125, 68)" height="18" width="69.8125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int tumin</span></div></foreignObject><foreignObject transform="translate( -41.5703125, 90)" height="18" width="82.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int tumittel</span></div></foreignObject></g></g><g transform="translate(223.0078125, 135.5)" id="classId-Weather-1" class="node default"><rect height="167" width="133.734375" y="-83.5" x="-66.8671875" class="outer title-state" style=""/><line y2="-53.5" y1="-53.5" x2="66.8671875" x1="-66.8671875" class="divider"/><line y2="72.5" y1="72.5" x2="66.8671875" x1="-66.8671875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -31.421875, -76)" height="18" width="62.84375" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Weather</span></div></foreignObject><foreignObject transform="translate( -59.3671875, -42)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -59.3671875, -20)" height="18" width="70.734375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+date day</span></div></foreignObject><foreignObject transform="translate( -59.3671875, 2)" height="18" width="118.734375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+float max_temp</span></div></foreignObject><foreignObject transform="translate( -59.3671875, 24)" height="18" width="116.078125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+float wind_gust</span></div></foreignObject><foreignObject transform="translate( -59.3671875, 46)" height="18" width="106.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+float rain_mm</span></div></foreignObject></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -1,38 +0,0 @@
classDiagram
class Logbook {
+int id
+int boat_id
+int shipmaster
+int steering_person
+bool shipmaster_only_steering
+datetime departure
+datetime arrival
+string destination
+int distance_in_km
+string comments
+int logtype
}
class LogbookType {
+int id
+string name
}
class Rower {
+int logbook_id
+int rower_id
}
class User {
...
}
class Boat {
...
}
Logbook "*" -- "1" Boat
Logbook "*" -- "1" User : shipmaster
Logbook "*" -- "1" LogbookType
Rower "*" -- "1" Logbook
Rower "*" -- "1" User

View File

@@ -1 +0,0 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 479.31640625 635" style="max-width: 479.316px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"><path style="fill:none" class="edge-pattern-solid relation" id="id_Logbook_Boat_1" d="M104.595,458L103.682,463.667C102.769,469.333,100.943,480.667,100.03,493.833C99.117,507,99.117,522,99.117,529.5L99.117,537"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Logbook_User_2" d="M169.648,458L171.201,463.667C172.753,469.333,175.859,480.667,186.17,495.841C196.482,511.016,213.999,530.031,222.757,539.539L231.516,549.047"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Logbook_LogbookType_3" d="M249.359,386.671L276.46,404.226C303.561,421.781,357.763,456.89,384.864,480.112C411.965,503.333,411.965,514.667,411.965,520.333L411.965,526"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Rower_Logbook_4" d="M195.258,94.768L184.161,101.306C173.065,107.845,150.872,120.923,139.776,131.628C128.68,142.333,128.68,150.667,128.68,154.833L128.68,159"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Rower_User_5" d="M295.145,109L298.308,113.167C301.472,117.333,307.798,125.667,310.962,158.917C314.125,192.167,314.125,250.333,314.125,310C314.125,369.667,314.125,430.833,308.786,469.287C303.448,507.74,292.771,523.48,287.432,531.35L282.094,539.22"/></g><g class="edgeLabels"><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(87.04085562560904, 472.93282017540025)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(109.11718874999995, 514.5000010714285)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(178.96484375, 492)" class="edgeLabel"><g transform="translate(-39.125, -9)" class="label"><foreignObject height="18" width="78.25"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">shipmaster</span></span></div></foreignObject></g></g><g transform="translate(159.80612717025386, 478.8421081151056)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(225.69137113402684, 521.013028651661)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(255.8922067982493, 408.77480399297514)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(421.5407662903117, 503.3638240725512)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(172.56555899206518, 90.72886543263846)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(142.40747355283347, 143.09219729055113)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(292.13521073682756, 131.59771748907048)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(299.3312305692518, 528.1578500971559)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g class="nodes"><g transform="translate(128.6796875, 308.5)" id="classId-Logbook-0" class="node default"><rect height="299" width="241.359375" y="-149.5" x="-120.6796875" class="outer title-state" style=""/><line y2="-119.5" y1="-119.5" x2="120.6796875" x1="-120.6796875" class="divider"/><line y2="138.5" y1="138.5" x2="120.6796875" x1="-120.6796875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -33.7734375, -142)" height="18" width="67.546875" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Logbook</span></div></foreignObject><foreignObject transform="translate( -113.1796875, -108)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -113.1796875, -86)" height="18" width="83.1875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int boat_id</span></div></foreignObject><foreignObject transform="translate( -113.1796875, -64)" height="18" width="108.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int shipmaster</span></div></foreignObject><foreignObject transform="translate( -113.1796875, -42)" height="18" width="145.4375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int steering_person</span></div></foreignObject><foreignObject transform="translate( -113.1796875, -20)" height="18" width="226.359375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool shipmaster_only_steering</span></div></foreignObject><foreignObject transform="translate( -113.1796875, 2)" height="18" width="143.65625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime departure</span></div></foreignObject><foreignObject transform="translate( -113.1796875, 24)" height="18" width="118.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime arrival</span></div></foreignObject><foreignObject transform="translate( -113.1796875, 46)" height="18" width="130.3125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string destination</span></div></foreignObject><foreignObject transform="translate( -113.1796875, 68)" height="18" width="141.859375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int distance_in_km</span></div></foreignObject><foreignObject transform="translate( -113.1796875, 90)" height="18" width="126.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string comments</span></div></foreignObject><foreignObject transform="translate( -113.1796875, 112)" height="18" width="82.28125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int logtype</span></div></foreignObject></g></g><g transform="translate(411.96484375, 576.5)" id="classId-LogbookType-1" class="node default"><rect height="101" width="118.703125" y="-50.5" x="-59.3515625" class="outer title-state" style=""/><line y2="-20.5" y1="-20.5" x2="59.3515625" x1="-59.3515625" class="divider"/><line y2="39.5" y1="39.5" x2="59.3515625" x1="-59.3515625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -51.8515625, -43)" height="18" width="103.703125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">LogbookType</span></div></foreignObject><foreignObject transform="translate( -51.8515625, -9)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -51.8515625, 13)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject></g></g><g transform="translate(256.8046875, 58.5)" id="classId-Rower-2" class="node default"><rect height="101" width="123.09375" y="-50.5" x="-61.546875" class="outer title-state" style=""/><line y2="-20.5" y1="-20.5" x2="61.546875" x1="-61.546875" class="divider"/><line y2="39.5" y1="39.5" x2="61.546875" x1="-61.546875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -24.453125, -43)" height="18" width="48.90625" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Rower</span></div></foreignObject><foreignObject transform="translate( -54.046875, -9)" height="18" width="108.09375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int logbook_id</span></div></foreignObject><foreignObject transform="translate( -54.046875, 13)" height="18" width="92.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int rower_id</span></div></foreignObject></g></g><g transform="translate(256.8046875, 576.5)" id="classId-User-3" class="node default"><rect height="79" width="50.578125" y="-39.5" x="-25.2890625" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="25.2890625" x1="-25.2890625" class="divider"/><line y2="28.5" y1="28.5" x2="25.2890625" x1="-25.2890625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.7890625, -32)" height="18" width="35.578125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">User</span></div></foreignObject><foreignObject transform="translate( -17.7890625, 2)" height="18" width="13.34375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">...</span></div></foreignObject></g></g><g transform="translate(99.1171875, 576.5)" id="classId-Boat-4" class="node default"><rect height="79" width="50.5625" y="-39.5" x="-25.28125" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="25.28125" x1="-25.28125" class="divider"/><line y2="28.5" y1="28.5" x2="25.28125" x1="-25.28125" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.78125, -32)" height="18" width="35.5625" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Boat</span></div></foreignObject><foreignObject transform="translate( -17.78125, 2)" height="18" width="13.34375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">...</span></div></foreignObject></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -1,30 +0,0 @@
classDiagram
class Log {
+int id
+string msg
+datetime created_at
}
class Notification {
+int id
+int user_id
+string message
+datetime read_at
+datetime created_at
+string category
+string action_after_reading
+string link
}
class Distance {
+int id
+string destination
+int distance_in_km
}
class User {
...
}
%% Relationships
Notification "*" -- "1" User

View File

@@ -1 +0,0 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 652.421875 378" style="max-width: 652.422px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"><path style="fill:none" class="edge-pattern-solid relation" id="id_Notification_User_1" d="M330.664,241L330.664,245.167C330.664,249.333,330.664,257.667,330.664,266C330.664,274.333,330.664,282.667,330.664,286.833L330.664,291"/></g><g class="edgeLabels"><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(315.6640612500001, 258.4999989285714)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(340.66406125, 268.4999989285714)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g class="nodes"><g transform="translate(90.8828125, 124.5)" id="classId-Log-0" class="node default"><rect height="123" width="165.765625" y="-61.5" x="-82.8828125" class="outer title-state" style=""/><line y2="-31.5" y1="-31.5" x2="82.8828125" x1="-82.8828125" class="divider"/><line y2="50.5" y1="50.5" x2="82.8828125" x1="-82.8828125" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -14.6640625, -54)" height="18" width="29.328125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Log</span></div></foreignObject><foreignObject transform="translate( -75.3828125, -20)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 2)" height="18" width="83.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string msg</span></div></foreignObject><foreignObject transform="translate( -75.3828125, 24)" height="18" width="150.765625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime created_at</span></div></foreignObject></g></g><g transform="translate(330.6640625, 124.5)" id="classId-Notification-1" class="node default"><rect height="233" width="213.796875" y="-116.5" x="-106.8984375" class="outer title-state" style=""/><line y2="-86.5" y1="-86.5" x2="106.8984375" x1="-106.8984375" class="divider"/><line y2="105.5" y1="105.5" x2="106.8984375" x1="-106.8984375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -44, -109)" height="18" width="88" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Notification</span></div></foreignObject><foreignObject transform="translate( -99.3984375, -75)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -99.3984375, -53)" height="18" width="83.171875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id</span></div></foreignObject><foreignObject transform="translate( -99.3984375, -31)" height="18" width="117.84375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string message</span></div></foreignObject><foreignObject transform="translate( -99.3984375, -9)" height="18" width="129.421875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime read_at</span></div></foreignObject><foreignObject transform="translate( -99.3984375, 13)" height="18" width="150.765625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime created_at</span></div></foreignObject><foreignObject transform="translate( -99.3984375, 35)" height="18" width="114.28125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string category</span></div></foreignObject><foreignObject transform="translate( -99.3984375, 57)" height="18" width="198.796875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string action_after_reading</span></div></foreignObject><foreignObject transform="translate( -99.3984375, 79)" height="18" width="76.921875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string link</span></div></foreignObject></g></g><g transform="translate(565.9921875, 124.5)" id="classId-Distance-2" class="node default"><rect height="123" width="156.859375" y="-61.5" x="-78.4296875" class="outer title-state" style=""/><line y2="-31.5" y1="-31.5" x2="78.4296875" x1="-78.4296875" class="divider"/><line y2="50.5" y1="50.5" x2="78.4296875" x1="-78.4296875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -33.3515625, -54)" height="18" width="66.703125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Distance</span></div></foreignObject><foreignObject transform="translate( -70.9296875, -20)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -70.9296875, 2)" height="18" width="130.3125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string destination</span></div></foreignObject><foreignObject transform="translate( -70.9296875, 24)" height="18" width="141.859375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int distance_in_km</span></div></foreignObject></g></g><g transform="translate(330.6640625, 330.5)" id="classId-User-3" class="node default"><rect height="79" width="50.578125" y="-39.5" x="-25.2890625" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="25.2890625" x1="-25.2890625" class="divider"/><line y2="28.5" y1="28.5" x2="25.2890625" x1="-25.2890625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.7890625, -32)" height="18" width="35.578125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">User</span></div></foreignObject><foreignObject transform="translate( -17.7890625, 2)" height="18" width="13.34375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">...</span></div></foreignObject></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -1,56 +0,0 @@
classDiagram
class TripType {
+int id
+string name
+string desc
+string question
+string icon
}
class TripDetails {
+int id
+string planned_starting_time
+int max_people
+string day
+bool allow_guests
+string notes
+bool always_show
+bool is_locked
+int trip_type_id
}
class PlannedEvent {
+int id
+string name
+int planned_amount_cox
+int trip_details_id
+string created_at
}
class Trip {
+int id
+int cox_id
+int trip_details_id
+int planned_event_id
+string created_at
}
class UserTrip {
+int user_id
+string user_note
+int trip_details_id
+string created_at
}
class User {
...
}
TripType "1" -- "*" TripDetails
TripDetails "1" -- "*" PlannedEvent
Trip "*" -- "1" TripDetails
Trip "*" -- "1" PlannedEvent
UserTrip "*" -- "1" TripDetails
Trip "*" -- "1" User : cox
UserTrip "*" -- "1" User

View File

@@ -1 +0,0 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 589.3359375 723" style="max-width: 589.336px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"><path style="fill:none" class="edge-pattern-solid relation" id="id_TripType_TripDetails_1" d="M100.68,175L100.68,180.667C100.68,186.333,100.68,197.667,101.381,209C102.083,220.333,103.487,231.667,104.188,237.333L104.89,243"/><path style="fill:none" class="edge-pattern-solid relation" id="id_TripDetails_PlannedEvent_2" d="M120.68,498L120.68,502.167C120.68,506.333,120.68,514.667,131.446,525.958C142.212,537.25,163.745,551.5,174.511,558.625L185.277,565.75"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Trip_TripDetails_3" d="M229.757,175L224.929,180.667C220.101,186.333,210.445,197.667,202.806,209C195.167,220.333,189.546,231.667,186.735,237.333L183.924,243"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Trip_PlannedEvent_4" d="M300.898,175L300.898,180.667C300.898,186.333,300.898,197.667,300.898,230.25C300.898,262.833,300.898,316.667,300.898,369C300.898,421.333,300.898,472.167,300.274,501.75C299.649,531.333,298.399,539.667,297.774,543.833L297.15,548"/><path style="fill:none" class="edge-pattern-solid relation" id="id_UserTrip_TripDetails_5" d="M464.909,164L460.335,171.5C455.761,179,446.613,194,408.021,218.842C369.43,243.685,301.395,278.37,267.377,295.712L233.359,313.055"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Trip_User_6" d="M374.885,175L379.906,180.667C384.927,186.333,394.97,197.667,413.107,223.704C431.243,249.741,457.475,290.482,470.591,310.853L483.707,331.223"/><path style="fill:none" class="edge-pattern-solid relation" id="id_UserTrip_User_7" d="M515.295,164L515.934,171.5C516.572,179,517.848,194,517.211,221.833C516.574,249.667,514.024,290.333,512.749,310.667L511.473,331"/></g><g class="edgeLabels"><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(85.74123126895586, 192.54889358487895)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g transform="translate(112.67598010276458, 218.82506657703127)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(110.37975770723475, 517.2131907237797)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g transform="translate(173.9619280316911, 538.5833348850657)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(206.98977340825164, 178.59287202327315)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(200.1380412921721, 228.98825858921583)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(285.89843875, 192.50000107142858)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(308.93739583120146, 527.2499290496063)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(442.9909762769256, 171.1304408290506)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(250.76306675409873, 313.4700698998099)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(405.01171875, 209)" class="edgeLabel"><g transform="translate(-12.453125, -9)" class="label"><foreignObject height="18" width="24.90625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">cox</span></span></div></foreignObject></g></g><g transform="translate(375.2642134920149, 198.04574431047698)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(481.8451560278755, 303.38887030430874)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(501.8332413904589, 182.70896381395303)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(522.5394365266823, 309.47323674342846)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g class="nodes"><g transform="translate(100.6796875, 91.5)" id="classId-TripType-0" class="node default"><rect height="167" width="128.40625" y="-83.5" x="-64.203125" class="outer title-state" style=""/><line y2="-53.5" y1="-53.5" x2="64.203125" x1="-64.203125" class="divider"/><line y2="72.5" y1="72.5" x2="64.203125" x1="-64.203125" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -32.75, -76)" height="18" width="65.5" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">TripType</span></div></foreignObject><foreignObject transform="translate( -56.703125, -42)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -56.703125, -20)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject><foreignObject transform="translate( -56.703125, 2)" height="18" width="86.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string desc</span></div></foreignObject><foreignObject transform="translate( -56.703125, 24)" height="18" width="113.40625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string question</span></div></foreignObject><foreignObject transform="translate( -56.703125, 46)" height="18" width="82.265625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string icon</span></div></foreignObject></g></g><g transform="translate(120.6796875, 370.5)" id="classId-TripDetails-1" class="node default"><rect height="255" width="225.359375" y="-127.5" x="-112.6796875" class="outer title-state" style=""/><line y2="-97.5" y1="-97.5" x2="112.6796875" x1="-112.6796875" class="divider"/><line y2="116.5" y1="116.5" x2="112.6796875" x1="-112.6796875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -40.90625, -120)" height="18" width="81.8125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">TripDetails</span></div></foreignObject><foreignObject transform="translate( -105.1796875, -86)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -105.1796875, -64)" height="18" width="210.359375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string planned_starting_time</span></div></foreignObject><foreignObject transform="translate( -105.1796875, -42)" height="18" width="117.859375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int max_people</span></div></foreignObject><foreignObject transform="translate( -105.1796875, -20)" height="18" width="78.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string day</span></div></foreignObject><foreignObject transform="translate( -105.1796875, 2)" height="18" width="136.546875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool allow_guests</span></div></foreignObject><foreignObject transform="translate( -105.1796875, 24)" height="18" width="92.0625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string notes</span></div></foreignObject><foreignObject transform="translate( -105.1796875, 46)" height="18" width="139.203125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool always_show</span></div></foreignObject><foreignObject transform="translate( -105.1796875, 68)" height="18" width="110.75"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool is_locked</span></div></foreignObject><foreignObject transform="translate( -105.1796875, 90)" height="18" width="113.40625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int trip_type_id</span></div></foreignObject></g></g><g transform="translate(284.62890625, 631.5)" id="classId-PlannedEvent-2" class="node default"><rect height="167" width="198.703125" y="-83.5" x="-99.3515625" class="outer title-state" style=""/><line y2="-53.5" y1="-53.5" x2="99.3515625" x1="-99.3515625" class="divider"/><line y2="72.5" y1="72.5" x2="99.3515625" x1="-99.3515625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -52.90625, -76)" height="18" width="105.8125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">PlannedEvent</span></div></foreignObject><foreignObject transform="translate( -91.8515625, -42)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -91.8515625, -20)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject><foreignObject transform="translate( -91.8515625, 2)" height="18" width="183.703125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int planned_amount_cox</span></div></foreignObject><foreignObject transform="translate( -91.8515625, 24)" height="18" width="129.421875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int trip_details_id</span></div></foreignObject><foreignObject transform="translate( -91.8515625, 46)" height="18" width="128.53125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string created_at</span></div></foreignObject></g></g><g transform="translate(300.8984375, 91.5)" id="classId-Trip-3" class="node default"><rect height="167" width="172.03125" y="-83.5" x="-86.015625" class="outer title-state" style=""/><line y2="-53.5" y1="-53.5" x2="86.015625" x1="-86.015625" class="divider"/><line y2="72.5" y1="72.5" x2="86.015625" x1="-86.015625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -14.671875, -76)" height="18" width="29.34375" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Trip</span></div></foreignObject><foreignObject transform="translate( -78.515625, -42)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -78.515625, -20)" height="18" width="76.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int cox_id</span></div></foreignObject><foreignObject transform="translate( -78.515625, 2)" height="18" width="129.421875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int trip_details_id</span></div></foreignObject><foreignObject transform="translate( -78.515625, 24)" height="18" width="157.03125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int planned_event_id</span></div></foreignObject><foreignObject transform="translate( -78.515625, 46)" height="18" width="128.53125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string created_at</span></div></foreignObject></g></g><g transform="translate(509.125, 91.5)" id="classId-UserTrip-4" class="node default"><rect height="145" width="144.421875" y="-72.5" x="-72.2109375" class="outer title-state" style=""/><line y2="-42.5" y1="-42.5" x2="72.2109375" x1="-72.2109375" class="divider"/><line y2="61.5" y1="61.5" x2="72.2109375" x1="-72.2109375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -32.4609375, -65)" height="18" width="64.921875" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">UserTrip</span></div></foreignObject><foreignObject transform="translate( -64.7109375, -31)" height="18" width="83.171875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id</span></div></foreignObject><foreignObject transform="translate( -64.7109375, -9)" height="18" width="124.078125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string user_note</span></div></foreignObject><foreignObject transform="translate( -64.7109375, 13)" height="18" width="129.421875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int trip_details_id</span></div></foreignObject><foreignObject transform="translate( -64.7109375, 35)" height="18" width="128.53125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string created_at</span></div></foreignObject></g></g><g transform="translate(508.99609375, 370.5)" id="classId-User-5" class="node default"><rect height="79" width="50.578125" y="-39.5" x="-25.2890625" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="25.2890625" x1="-25.2890625" class="divider"/><line y2="28.5" y1="28.5" x2="25.2890625" x1="-25.2890625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.7890625, -32)" height="18" width="35.578125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">User</span></div></foreignObject><foreignObject transform="translate( -17.7890625, 2)" height="18" width="13.34375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">...</span></div></foreignObject></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -1,3 +0,0 @@
#!/bin/bash
find . -name "*.mermaid" -type f -exec sh -c 'mmdc -i "$1" -o "${1%.mermaid}.svg" -b transparent -t dark' sh {} \;

View File

@@ -1,25 +0,0 @@
classDiagram
class Trailer {
+int id
+string name
}
class TrailerReservation {
+int id
+int trailer_id
+date start_date
+date end_date
+string time_desc
+string usage
+int user_id_applicant
+int user_id_confirmation
+datetime created_at
}
class User {
...
}
TrailerReservation "*" -- "1" Trailer
TrailerReservation "*" -- "1" User : applicant
TrailerReservation "*" -- "0..1" User : confirmed_by

View File

@@ -1 +0,0 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 308.6796875 440" style="max-width: 308.68px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"><path style="fill:none" class="edge-pattern-solid relation" id="id_TrailerReservation_Trailer_1" d="M81.051,263L77.871,268.667C74.69,274.333,68.329,285.667,65.149,297C61.969,308.333,61.969,319.667,61.969,325.333L61.969,331"/><path style="fill:none" class="edge-pattern-solid relation" id="id_TrailerReservation_User_2" d="M152.609,263L152.609,268.667C152.609,274.333,152.609,285.667,157.049,298.833C161.489,312,170.369,327,174.809,334.5L179.249,342"/><path style="fill:none" class="edge-pattern-solid relation" id="id_TrailerReservation_User_3" d="M231.594,263L235.104,268.667C238.615,274.333,245.635,285.667,244.706,298.833C243.776,312,234.896,327,230.456,334.5L226.017,342"/></g><g class="edgeLabels"><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(59.4053421736739, 270.919346054189)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(72.21198785349168, 308.77448466334016)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(152.609375, 297)" class="edgeLabel"><g transform="translate(-32.0234375, -9)" class="label"><foreignObject height="18" width="64.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">applicant</span></span></div></foreignObject></g></g><g transform="translate(137.86568884349458, 280.6494351366758)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(178.24197705723373, 314.29962991184635)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g><g transform="translate(252.65625, 297)" class="edgeLabel"><g transform="translate(-48.0234375, -9)" class="label"><foreignObject height="18" width="96.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"><span class="edgeLabel">confirmed_by</span></span></div></foreignObject></g></g><g transform="translate(228.05818717831355, 285.77608024983135)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g></g><g transform="translate(242.83917505280118, 329.5822485013881)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 36px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">0..1</span></div></foreignObject></g></g><g class="nodes"><g transform="translate(61.96875, 381.5)" id="classId-Trailer-0" class="node default"><rect height="101" width="107.9375" y="-50.5" x="-53.96875" class="outer title-state" style=""/><line y2="-20.5" y1="-20.5" x2="53.96875" x1="-53.96875" class="divider"/><line y2="39.5" y1="39.5" x2="53.96875" x1="-53.96875" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -24.015625, -43)" height="18" width="48.03125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Trailer</span></div></foreignObject><foreignObject transform="translate( -46.46875, -9)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -46.46875, 13)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject></g></g><g transform="translate(152.609375, 135.5)" id="classId-TrailerReservation-1" class="node default"><rect height="255" width="194.21875" y="-127.5" x="-97.109375" class="outer title-state" style=""/><line y2="-97.5" y1="-97.5" x2="97.109375" x1="-97.109375" class="divider"/><line y2="116.5" y1="116.5" x2="97.109375" x1="-97.109375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -69.8125, -120)" height="18" width="139.625" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">TrailerReservation</span></div></foreignObject><foreignObject transform="translate( -89.609375, -86)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -89.609375, -64)" height="18" width="92.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int trailer_id</span></div></foreignObject><foreignObject transform="translate( -89.609375, -42)" height="18" width="116.09375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+date start_date</span></div></foreignObject><foreignObject transform="translate( -89.609375, -20)" height="18" width="111.671875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+date end_date</span></div></foreignObject><foreignObject transform="translate( -89.609375, 2)" height="18" width="125.84375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string time_desc</span></div></foreignObject><foreignObject transform="translate( -89.609375, 24)" height="18" width="96.515625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string usage</span></div></foreignObject><foreignObject transform="translate( -89.609375, 46)" height="18" width="156.109375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_applicant</span></div></foreignObject><foreignObject transform="translate( -89.609375, 68)" height="18" width="179.21875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id_confirmation</span></div></foreignObject><foreignObject transform="translate( -89.609375, 90)" height="18" width="150.765625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime created_at</span></div></foreignObject></g></g><g transform="translate(202.6328125, 381.5)" id="classId-User-2" class="node default"><rect height="79" width="50.578125" y="-39.5" x="-25.2890625" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="25.2890625" x1="-25.2890625" class="divider"/><line y2="28.5" y1="28.5" x2="25.2890625" x1="-25.2890625" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.7890625, -32)" height="18" width="35.578125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">User</span></div></foreignObject><foreignObject transform="translate( -17.7890625, 2)" height="18" width="13.34375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">...</span></div></foreignObject></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1,42 +0,0 @@
classDiagram
class User {
+int id
+string name
+string pw
+bool deleted
+datetime last_access
+string dob
+string weight
+string sex
+string dirty_thirty
+string dirty_dozen
+string member_since_date
+string birthdate
+string mail
+string nickname
+string notes
+string phone
+string address
+int family_id
+blob membership_pdf
+string user_token
}
class Family {
+int id
}
class Role {
+int id
+string name
+string cluster
}
class UserRole {
+int user_id
+int role_id
}
User "1" -- "*" UserRole
Role "1" -- "*" UserRole
User "1" -- "0..1" Family

View File

@@ -1 +0,0 @@
<svg aria-roledescription="classDiagram" role="graphics-document document" viewBox="0 0 402.4140625 664" style="max-width: 402.414px; background-color: transparent;" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" width="100%" id="my-svg"><style>#my-svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#ccc;}#my-svg .error-icon{fill:#a44141;}#my-svg .error-text{fill:#ddd;stroke:#ddd;}#my-svg .edge-thickness-normal{stroke-width:1px;}#my-svg .edge-thickness-thick{stroke-width:3.5px;}#my-svg .edge-pattern-solid{stroke-dasharray:0;}#my-svg .edge-thickness-invisible{stroke-width:0;fill:none;}#my-svg .edge-pattern-dashed{stroke-dasharray:3;}#my-svg .edge-pattern-dotted{stroke-dasharray:2;}#my-svg .marker{fill:lightgrey;stroke:lightgrey;}#my-svg .marker.cross{stroke:lightgrey;}#my-svg svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#my-svg p{margin:0;}#my-svg g.classGroup text{fill:#ccc;stroke:none;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:10px;}#my-svg g.classGroup text .title{font-weight:bolder;}#my-svg .nodeLabel,#my-svg .edgeLabel{color:#e0dfdf;}#my-svg .edgeLabel .label rect{fill:#1f2020;}#my-svg .label text{fill:#e0dfdf;}#my-svg .edgeLabel .label span{background:#1f2020;}#my-svg .classTitle{font-weight:bolder;}#my-svg .node rect,#my-svg .node circle,#my-svg .node ellipse,#my-svg .node polygon,#my-svg .node path{fill:#1f2020;stroke:#ccc;stroke-width:1px;}#my-svg .divider{stroke:#ccc;stroke-width:1;}#my-svg g.clickable{cursor:pointer;}#my-svg g.classGroup rect{fill:#1f2020;stroke:#ccc;}#my-svg g.classGroup line{stroke:#ccc;stroke-width:1;}#my-svg .classLabel .box{stroke:none;stroke-width:0;fill:#1f2020;opacity:0.5;}#my-svg .classLabel .label{fill:#ccc;font-size:10px;}#my-svg .relation{stroke:lightgrey;stroke-width:1;fill:none;}#my-svg .dashed-line{stroke-dasharray:3;}#my-svg .dotted-line{stroke-dasharray:1 2;}#my-svg #compositionStart,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #compositionEnd,#my-svg .composition{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #dependencyStart,#my-svg .dependency{fill:lightgrey!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionStart,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #extensionEnd,#my-svg .extension{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationStart,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #aggregationEnd,#my-svg .aggregation{fill:transparent!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopStart,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg #lollipopEnd,#my-svg .lollipop{fill:#1f2020!important;stroke:lightgrey!important;stroke-width:1;}#my-svg .edgeTerminals{font-size:11px;line-height:initial;}#my-svg .classTitleText{text-anchor:middle;font-size:18px;fill:#ccc;}#my-svg :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}</style><g><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker aggregation classDiagram" id="my-svg_classDiagram-aggregationEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker extension classDiagram" id="my-svg_classDiagram-extensionStart"><path d="M 1,7 L18,13 V 1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker extension classDiagram" id="my-svg_classDiagram-extensionEnd"><path d="M 1,1 V 13 L18,7 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="18" class="marker composition classDiagram" id="my-svg_classDiagram-compositionStart"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="1" class="marker composition classDiagram" id="my-svg_classDiagram-compositionEnd"><path d="M 18,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="6" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyStart"><path d="M 5,7 L9,13 L1,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="28" markerWidth="20" refY="7" refX="13" class="marker dependency classDiagram" id="my-svg_classDiagram-dependencyEnd"><path d="M 18,7 L9,13 L14,7 L9,1 Z"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="13" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopStart"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><defs><marker orient="auto" markerHeight="240" markerWidth="190" refY="7" refX="1" class="marker lollipop classDiagram" id="my-svg_classDiagram-lollipopEnd"><circle r="6" cy="7" cx="7" fill="transparent" stroke="black"/></marker></defs><g class="root"><g class="clusters"/><g class="edgePaths"><path style="fill:none" class="edge-pattern-solid relation" id="id_User_UserRole_1" d="M63.596,505L62.743,509.167C61.891,513.333,60.186,521.667,62.422,530C64.658,538.333,70.835,546.667,73.923,550.833L77.012,555"/><path style="fill:none" class="edge-pattern-solid relation" id="id_Role_UserRole_2" d="M315.83,318L308.6,353.333C301.37,388.667,286.909,459.333,261.526,503.341C236.143,547.348,199.837,564.697,181.684,573.371L163.531,582.045"/><path style="fill:none" class="edge-pattern-solid relation" id="id_User_Family_3" d="M220.891,380.93L242.145,405.775C263.398,430.62,305.906,480.31,327.16,511.155C348.414,542,348.414,554,348.414,560L348.414,566"/></g><g class="edgeLabels"><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(48.863498808878106, 521.5528105004219)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g transform="translate(75.90424420716624, 527.7282047434567)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(297.62591269237316, 332.1376838974091)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g transform="translate(180.78836069993307, 583.034192966888)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">*</span></div></foreignObject></g><g class="edgeLabel"><g transform="translate(0, 0)" class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel"></span></div></foreignObject></g></g><g transform="translate(220.86826990826685, 403.9791151705619)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"><foreignObject style="width: 9px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">1</span></div></foreignObject></g></g><g transform="translate(358.41406125, 543.4999989285715)" class="edgeTerminals"><g transform="translate(0, 0)" class="inner"/><foreignObject style="width: 36px; height: 12px;"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="edgeLabel">0..1</span></div></foreignObject></g></g><g class="nodes"><g transform="translate(114.4453125, 256.5)" id="classId-User-0" class="node default"><rect height="497" width="212.890625" y="-248.5" x="-106.4453125" class="outer title-state" style=""/><line y2="-218.5" y1="-218.5" x2="106.4453125" x1="-106.4453125" class="divider"/><line y2="237.5" y1="237.5" x2="106.4453125" x1="-106.4453125" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.7890625, -241)" height="18" width="35.578125" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">User</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -207)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -185)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -163)" height="18" width="73.375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string pw</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -141)" height="18" width="96.53125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+bool deleted</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -119)" height="18" width="158.75"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+datetime last_access</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -97)" height="18" width="79.609375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string dob</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -75)" height="18" width="99.171875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string weight</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -53)" height="18" width="77.8125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string sex</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -31)" height="18" width="126.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string dirty_thirty</span></div></foreignObject><foreignObject transform="translate( -98.9453125, -9)" height="18" width="135.640625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string dirty_dozen</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 13)" height="18" width="197.890625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string member_since_date</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 35)" height="18" width="115.1875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string birthdate</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 57)" height="18" width="82.25"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string mail</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 79)" height="18" width="121.390625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string nickname</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 101)" height="18" width="92.0625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string notes</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 123)" height="18" width="97.40625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string phone</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 145)" height="18" width="109.84375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string address</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 167)" height="18" width="93.828125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int family_id</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 189)" height="18" width="163.21875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+blob membership_pdf</span></div></foreignObject><foreignObject transform="translate( -98.9453125, 211)" height="18" width="132.078125"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string user_token</span></div></foreignObject></g></g><g transform="translate(348.4140625, 605.5)" id="classId-Family-1" class="node default"><rect height="79" width="65.6875" y="-39.5" x="-32.84375" class="outer title-state" style=""/><line y2="-9.5" y1="-9.5" x2="32.84375" x1="-32.84375" class="divider"/><line y2="28.5" y1="28.5" x2="32.84375" x1="-32.84375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -25.34375, -32)" height="18" width="50.6875" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Family</span></div></foreignObject><foreignObject transform="translate( -25.34375, 2)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject></g></g><g transform="translate(328.4140625, 256.5)" id="classId-Role-2" class="node default"><rect height="123" width="115.046875" y="-61.5" x="-57.5234375" class="outer title-state" style=""/><line y2="-31.5" y1="-31.5" x2="57.5234375" x1="-57.5234375" class="divider"/><line y2="50.5" y1="50.5" x2="57.5234375" x1="-57.5234375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -17.3359375, -54)" height="18" width="34.671875" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">Role</span></div></foreignObject><foreignObject transform="translate( -50.0234375, -20)" height="18" width="43.140625"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int id</span></div></foreignObject><foreignObject transform="translate( -50.0234375, 2)" height="18" width="92.9375"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string name</span></div></foreignObject><foreignObject transform="translate( -50.0234375, 24)" height="18" width="100.046875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+string cluster</span></div></foreignObject></g></g><g transform="translate(114.4453125, 605.5)" id="classId-UserRole-3" class="node default"><rect height="101" width="98.171875" y="-50.5" x="-49.0859375" class="outer title-state" style=""/><line y2="-20.5" y1="-20.5" x2="49.0859375" x1="-49.0859375" class="divider"/><line y2="39.5" y1="39.5" x2="49.0859375" x1="-49.0859375" class="divider"/><g class="label"><foreignObject height="0" width="0"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel"></span></div></foreignObject><foreignObject transform="translate( -35.125, -43)" height="18" width="70.25" class="classTitle"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">UserRole</span></div></foreignObject><foreignObject transform="translate( -41.5859375, -9)" height="18" width="83.171875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int user_id</span></div></foreignObject><foreignObject transform="translate( -41.5859375, 13)" height="18" width="78.71875"><div style="display: inline-block; white-space: nowrap;" xmlns="http://www.w3.org/1999/xhtml"><span class="nodeLabel">+int role_id</span></div></foreignObject></g></g></g></g></g></svg>

Before

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -1,94 +0,0 @@
# Nextcloud integration
- Based on [this plugin](https://github.com/nextcloud/user_external)
- Install that plugin via web
- Connect to server, enter nextcloud-docker-image: `docker exec -it nextcloud-aio-nextcloud bash`
- Adapt `/var/www/html/custom_apps/user_external/lib/BasicAuth.php` to switch from BasicAuth to RowtAuth:
```php
<?php
/**
* Copyright (c) 2019 Lutz Freitag <lutz.freitag@gottliebtfreitag.de>
* This file is licensed under the Affero General Public License version 3 or
* later.
* See the COPYING-README file.
*/
namespace OCA\UserExternal;
class BasicAuth extends Base {
private $authUrl;
public function __construct($authUrl) {
parent::__construct($authUrl);
$this->authUrl = $authUrl;
}
/**
* Check if the password is correct without logging in the user
*
* @param string $uid The username
* @param string $password The password
*
* @return true/false
*/
public function checkPassword($uid, $password) {
// Prepare POST data with credentials
$postData = http_build_query([
'name' => $uid,
'password' => $password
]);
// Create context with POST method
$context = stream_context_create([
'http' => [
'method' => 'POST',
'header' => 'Content-Type: application/x-www-form-urlencoded',
'content' => $postData,
'follow_location' => 0
]
]);
// Get the content of the response
$content = @file_get_contents($this->authUrl, false, $context);
if ($content === false) {
\OC::$server->getLogger()->error(
'ERROR: Failed to get content from Auth Url: '.$this->authUrl,
['app' => 'user_external']
);
return false;
}
// Check if the content is "SUCC"
if (trim($content) === "SUCC") {
$this->storeUser($uid);
return $uid;
}
return false;
}
}
```
- In `/var/www/html/config/config.php` add this:
```
'user_backends' =>
array (
0 =>
array (
'class' => '\\OCA\\UserExternal\\BasicAuth',
'arguments' =>
array (
0 => 'https://app.rudernlinz.at/nxauth',
),
),
),
```
- In `/var/www/html/config/config.php` add this `'skeletondirectory' => '',` to disable default folders for new users
- To automatically add users to a group (e.g. `vorstand`), use the `Auto Groups` plugin
- Shared folders are not shared with new members due to [this bug](https://github.com/nextcloud/server/issues/25062#issuecomment-766445043)
- Find DB config: `docker exec nextcloud-aio-database env | grep POSTGRES`
- Workaround: Connect to docker-db: `docker exec -it nextcloud-aio-database bash`
- Connect to db: `psql -U nextcloud -d nextcloud_database`
- (with `\l` you see all dbs)
- Connect to nextcloud db: `\c nextcloud_database`
- Do query from issue: `UPDATE oc_share SET accepted = 1 WHERE share_type = 1;`

View File

@@ -1,115 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 583 276" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<g transform="matrix(1,0,0,1,-35.4077,-299.343)">
<g transform="matrix(-1,-0.000178685,0.000154251,-0.863253,717.569,685.115)">
<ellipse cx="349.686" cy="225.908" rx="151.555" ry="64.755" style="fill:rgb(255,44,29);stroke:black;stroke-width:1.07px;"/>
</g>
<g transform="matrix(1,0,0,1,28.4418,190.037)">
<path d="M286.601,229.218C289.276,248.622 296.342,264.086 310.327,279.874C300.812,262.599 294.125,245.355 297.093,228.218L286.601,229.218Z" style="fill:rgb(209,17,3);stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,28.4418,190.037)">
<g transform="matrix(0.00205003,0.676578,-0.676578,0.00205003,289.722,204.596)">
<circle cx="0" cy="0" r="36.391" style="fill:white;stroke:black;stroke-width:1.48px;"/>
</g>
<g transform="matrix(0.00362135,0.46002,-0.46002,0.00362135,293.586,211.476)">
<circle cx="0" cy="0" r="36.391" style="stroke:black;stroke-width:2.17px;"/>
</g>
<g transform="matrix(0.00181067,0.23001,-0.23001,0.00181067,288.722,204.596)">
<circle cx="0" cy="0" r="36.391" style="fill:white;stroke:black;stroke-width:4.35px;"/>
</g>
</g>
<g transform="matrix(-1,0,0,1,706.392,190.037)">
<path d="M286.601,229.218C289.276,248.622 296.342,264.086 310.327,279.874C300.812,262.599 294.125,245.355 297.093,228.218L286.601,229.218Z" style="fill:rgb(209,17,3);stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(1,0,0,1,28.4418,190.037)">
<g transform="matrix(0.00205003,0.676578,-0.676578,0.00205003,388.597,204.596)">
<circle cx="0" cy="0" r="36.391" style="fill:white;stroke:black;stroke-width:1.48px;"/>
</g>
<g transform="matrix(0.00362135,0.46002,-0.46002,0.00362135,392.533,211.476)">
<circle cx="0" cy="0" r="36.391" style="stroke:black;stroke-width:2.17px;"/>
</g>
<g transform="matrix(0.00181067,0.23001,-0.23001,0.00181067,387.567,204.596)">
<circle cx="0" cy="0" r="36.391" style="fill:white;stroke:black;stroke-width:4.35px;"/>
</g>
</g>
<g transform="matrix(0.729283,-2.96924e-17,3.31033e-17,-1.08337,127.964,834.503)">
<path d="M363.785,318.565C366.3,316.683 367.623,314.549 367.623,312.377C367.623,305.546 354.786,300 338.975,300C323.164,300 310.327,305.546 310.327,312.377C310.327,314.549 311.65,316.683 314.165,318.565C330.705,314.514 347.245,314.47 363.785,318.565Z" style="stroke:black;stroke-width:1.08px;"/>
</g>
<g transform="matrix(0.511811,0.00507451,0.00737211,-0.743545,200.689,730.834)">
<path d="M363.785,318.565C366.3,316.683 367.623,314.549 367.623,312.377C367.623,305.546 354.786,300 338.975,300C323.164,300 310.327,305.546 310.327,312.377C310.327,314.549 311.65,316.683 314.165,318.565C330.705,314.514 347.245,314.47 363.785,318.565Z" style="fill:white;stroke:black;stroke-width:1.57px;"/>
</g>
<g transform="matrix(1,0,0,1,29.4418,190.037)">
<g transform="matrix(1.52947,0.197824,-0.138277,1.06908,-155.205,-130.843)">
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(255,0,4);stroke:black;stroke-width:0.75px;"/>
</g>
<g transform="matrix(1.38346,0.17894,-0.132531,1.02465,-106.692,-99.4881)">
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(255,83,71);stroke:black;stroke-width:0.81px;"/>
</g>
<g transform="matrix(1.25063,0.161758,-0.11774,0.910302,-66.5748,-47.1693)">
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(254,109,99);stroke:black;stroke-width:0.91px;"/>
</g>
</g>
<g transform="matrix(1.11717,-0.0134911,0.0134911,1.11717,11.4404,152.276)">
<g transform="matrix(-1.52947,0.197824,0.138277,1.06908,857.149,-122.797)">
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(255,0,4);stroke:black;stroke-width:0.67px;"/>
</g>
<g transform="matrix(-1.38346,0.17894,0.132531,1.02465,808.636,-91.4427)">
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(255,83,71);stroke:black;stroke-width:0.73px;"/>
</g>
<g transform="matrix(-1.25063,0.161758,0.11774,0.910302,768.519,-39.1239)">
<path d="M445.555,333.026L445.555,339.772C458.57,331.997 475.648,331.692 491.032,333.026C502.63,341.464 510.891,354.59 518.987,367.992C515.35,350.048 513.864,334.687 495.179,321.419C480.02,320.962 460.714,324.456 445.555,333.026Z" style="fill:rgb(254,109,99);stroke:black;stroke-width:0.81px;"/>
</g>
</g>
<path d="M458.479,299.843L501.801,385.706L483.536,357.128L459.922,356.81C461.219,359.389 462.626,361.984 464.124,364.598C469.852,374.595 476.484,384.07 483.354,392.071L520.953,379.098C518.698,372.227 509.385,353.734 505.6,346.35C490.238,320.681 470.903,301.887 458.479,299.843Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
<path d="M476.397,345.957L449.131,303.294C444.35,310.247 446.55,326.436 454.694,345.665L476.397,345.957Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
<g transform="matrix(1.91904,0,0,1.91904,-538.727,-501.927)">
<g transform="matrix(1,0,0,1,-206.042,-24.7226)">
<path d="M742.158,502.447C739.772,509.848 732.922,516.471 722.777,522.521C734.015,523.734 744.479,519.473 751.504,508.94L742.158,502.447Z" style="fill:rgb(254,109,99);stroke:black;stroke-width:0.52px;"/>
</g>
<g transform="matrix(1,0,0,1,-206.042,-24.7226)">
<path d="M751.365,508.94C762.221,499.436 762.789,489.684 757.237,479.785C749.705,485.381 744.028,488.47 735.811,487.991C737.641,497.119 742.164,504.525 751.365,508.94Z" style="fill:rgb(255,83,71);stroke:black;stroke-width:0.52px;"/>
</g>
</g>
<g transform="matrix(0.105752,-0.994393,0.994393,0.105752,9.03576,516.491)">
<g transform="matrix(-0.387971,-0.0870248,-0.0870248,0.387971,1030.34,-21.0172)">
<path d="M1944.68,934.772C1921.09,896.523 1932.18,782.181 1974.56,655.531C1990.23,608.676 2009.04,563.787 2029.1,525.376L2156.88,568.132C2149.78,610.876 2137.79,658.046 2122.11,704.901C2079.26,832.966 2018.43,931.728 1976.52,946.409L2085.08,568.504L1944.68,934.772Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:2.52px;stroke-linecap:butt;stroke-miterlimit:2;"/>
</g>
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
<path d="M182.692,268.985L182.692,174.156L193.766,174.156L193.766,262.63C191.152,263.999 188.537,265.462 185.934,267.001L182.692,268.985Z" style="stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
<path d="M182.692,292.805L193.766,287.03L193.766,290.902L182.692,298.191L182.692,292.805Z" style="stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
<path d="M186.692,318.086C188.312,317.289 189.931,316.492 191.551,315.695L193.766,314.518L193.766,497.999L191.577,497.079C190.016,496.484 188.455,495.89 186.895,495.295L182.692,493.866L182.692,319.896L186.692,318.086Z" style="stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
<path d="M182.692,319.896L186.692,318.086C188.312,317.289 189.931,316.492 191.551,315.695L193.766,314.518L193.766,290.902L182.692,298.191L182.692,319.896Z" style="stroke:black;stroke-width:1px;"/>
</g>
<g transform="matrix(-0.995725,0.0923706,-0.0923706,-0.995725,428.457,726.747)">
<path d="M182.692,512.971L182.692,571.911L193.766,571.911L193.766,519.91L192.912,519.38L182.692,512.971Z" style="stroke:black;stroke-width:1px;"/>
</g>
</g>
<g transform="matrix(1,0,0,1,56,0)">
<path d="M198.549,354.049L220.458,354.343C218.026,359.991 215.073,365.879 211.645,371.859L202.175,386.73C199.034,391.211 195.753,395.446 192.416,399.332L173.717,392.881L198.549,354.049Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
</g>
<g transform="matrix(1,0,0,1,56,0)">
<path d="M205.636,343.069L226.639,310.556C230.599,316.315 229.769,328.411 224.739,343.326L205.636,343.069Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
</g>
<g transform="matrix(1,0,0,1,56,0)">
<path d="M205.636,343.069L224.739,343.326L223.254,347.509C222.638,349.062 222.023,350.614 221.407,352.167L220.458,354.343L198.549,354.049L199.09,353.202L205.636,343.069Z" style="stroke:rgb(6,2,2);stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
</g>
<g transform="matrix(1,0,0,1,56,0)">
<path d="M173.717,392.881L154.817,386.36C157.874,377.045 162.499,366.877 168.23,356.88C176.635,342.215 186.42,329.568 195.697,320.682L195.659,320.331C200.211,317.065 213.64,307.706 217.291,307.105L198.317,344.921L198.307,344.827L173.717,392.881Z" style="fill:rgb(255,0,0);stroke:black;stroke-width:1px;stroke-linecap:butt;stroke-miterlimit:2;"/>
</g>
<g transform="matrix(-2.14337,0.024885,0.024885,2.14337,1380.31,-613.484)">
<g transform="matrix(1,0,0,1,-206.042,-24.7226)">
<path d="M742.158,502.447C739.772,509.848 732.922,516.471 722.777,522.521C734.015,523.734 744.479,519.473 751.504,508.94L742.158,502.447Z" style="fill:rgb(254,109,99);stroke:black;stroke-width:0.47px;"/>
</g>
<g transform="matrix(1,0,0,1,-206.042,-24.7226)">
<path d="M751.365,508.94C762.221,499.436 762.789,489.684 757.237,479.785C749.705,485.381 744.028,488.47 735.811,487.991C737.641,497.119 742.164,504.525 751.365,508.94Z" style="fill:rgb(255,83,71);stroke:black;stroke-width:0.47px;"/>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,156 +0,0 @@
# Wordpress auth
Add the following code to `wp-content/themes/bravada/functions.php`:
```
function rot_auth( $user, $username, $password ){
// Make sure a username and password are present for us to work with
if($username == '' || $password == '') return;
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, 'https://app.rudernlinz.at/wikiauth');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, "name=$username&password=$password");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// Execute the cURL session and get the response
$response = curl_exec($ch);
// Check for cURL errors
if(curl_errno($ch)){
$user = new WP_Error( 'denied', __('Curl error: ' . curl_error($ch)) );
}
// Close the cURL session
curl_close($ch);
if (strpos($response, 'SUCC') !== false) {
$user = get_user_by('login', $username);
if (!$user) {
// User does not exist, create a new one
$userdata = array(
'user_email' => $username,
'user_login' => $username,
'first_name' => $username,
'last_name' => ''
);
$new_user_id = wp_insert_user($userdata);
if (!is_wp_error($new_user_id)) {
// Load the new user info
$user = new WP_User($new_user_id);
// Set role based on username
if ($username == 'Philipp Hofer' || $username == 'Marie Birner') {
$user->set_role('administrator');
} else {
$user->set_role('editor');
}
} else {
// Handle error in user creation
return $new_user_id;
}
} else {
}
} else {
$user = new WP_Error( 'denied', __("Falscher Benutzername/Passwort. Verwendest du deine Accountdaten vom Ruderassistenten?") );
}
return $user;
}
// Comment this line if you wish to fall back on WordPress authentication
// Useful for times when the external service is offline
remove_action('authenticate', 'wp_authenticate_username_password', 20);
add_filter( 'authenticate', 'rot_auth', 10, 3 );
```
# Wordpress notify rowt on newly published article
Add the following code to `wp-content/themes/bravada/functions.php`:
```
function send_article_url_on_publish($new_status, $old_status, $post) {
// Check if the post is transitioning to 'publish' status
if ($new_status == 'publish' && $old_status != 'publish' && $post->post_type == 'post') {
// Get the URL of the newly published article
$article_url = get_permalink($post->ID);
$article_title = get_the_title($post->ID);
// URL to send the POST request to
$api_url = 'https://app.rudernlinz.at/new-blogpost';
// Prepare the data for the POST request
$body = array(
'article_url' => $article_url,
'article_title' => $article_title,
'pw' => "wordpress_key"
);
// Prepare the arguments for wp_remote_post
$args = array(
'body' => $body,
'timeout' => '5',
'redirection' => '5',
'httpversion' => '1.0',
'blocking' => true,
'headers' => array(),
'cookies' => array()
);
// Send the POST request
$response = wp_remote_post($api_url, $args);
// Optional: Check if the request was successful
if (is_wp_error($response)) {
error_log('Failed to send POST request: ' . $response->get_error_message());
} else {
error_log('POST request sent successfully with article URL: ' . $article_url);
}
}
if ($new_status != 'publish' && $old_status == 'publish' && $post->post_type == 'post') {
$article_url = get_permalink($post->ID);
// URL to send the POST request to
$api_url = 'https://app.rudernlinz.at/blogpost-unpublished';
// Prepare the data for the POST request
$body = array(
'article_url' => $article_url,
'pw' => "wordpress_key"
);
// Prepare the arguments for wp_remote_post
$args = array(
'body' => $body,
'timeout' => '5',
'redirection' => '5',
'httpversion' => '1.0',
'blocking' => true,
'headers' => array(),
'cookies' => array()
);
// Send the POST request
$response = wp_remote_post($api_url, $args);
// Optional: Check if the request was successful
if (is_wp_error($response)) {
error_log('Failed to send POST request: ' . $response->get_error_message());
} else {
error_log('POST request sent successfully with article URL: ' . $article_url);
}
}
}
// Hook the function to the 'transition_post_status' action
add_action('transition_post_status', 'send_article_url_on_publish', 10, 3);
```

5
fd
View File

@@ -1,5 +0,0 @@
#!/bin/bash
scp root@app.rudernlinz.at:/root/rowing-prod/db.sqlite db.sqlite
#sqlite3 db.sqlite < seeds.sql

5
frontend/.gitignore vendored
View File

@@ -1,6 +1 @@
package-lock.json package-lock.json
node_modules/
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/

View File

@@ -1,76 +1,77 @@
import * as d3 from "d3"; import * as d3 from 'd3';
export interface Data { export interface Data {
date: Date; date: Date;
km: number; km: number;
} }
if (sessionStorage.getItem("userStats")) { if(sessionStorage.getItem('userStats')) {
const data = JSON.parse( const data = JSON.parse(sessionStorage.getItem('userStats') || '{}') as Data[];
sessionStorage.getItem("userStats") || "{}",
) as Data[];
if (data.length >= 2) { if(data.length >= 2) {
const margin = { top: 20, right: 20, bottom: 50, left: 50 }; const margin = { top: 20, right: 20, bottom: 50, left: 50 };
const width: number = 960 - margin.left - margin.right; const width: number = 960 - margin.left - margin.right;
const height: number = 500 - margin.top - margin.bottom; const height: number = 500 - margin.top - margin.bottom;
data.forEach((d: Data) => { data.forEach((d: Data) => {
d.date = <Date>new Date(d.date); d.date = <Date> new Date(d.date)
d.km = +d.km; d.km = +d.km;
}); });
const x = d3 const x = d3.scaleTime()
.scaleTime()
.domain(<[Date, Date]>d3.extent(data, (d: Data) => d.date)) .domain(<[Date, Date]>d3.extent(data, (d: Data) => d.date))
.range([0, width]); .range([0, width]);
const y = d3 const y = d3.scaleLinear()
.scaleLinear()
.domain([0, Number(d3.max(data, (d: Data) => d.km))]) .domain([0, Number(d3.max(data, (d: Data) => d.km))])
.range([height, 0]); .range([height, 0]);
const line = d3 const line = d3.line<Data>()
.line<Data>()
.x((d: Data) => x(d.date)) .x((d: Data) => x(d.date))
.y((d: Data) => y(d.km)); .y((d: Data) => y(d.km));
const svg = d3 const svg = d3.select('#container')
.select("#container") .append('svg')
.append("svg") .attr('width', width + margin.left + margin.right)
.attr("width", width + margin.left + margin.right) .attr('height', height + margin.top + margin.bottom)
.attr("height", height + margin.top + margin.bottom)
.call(responsivefy) .call(responsivefy)
.append("g") .append('g')
.attr("transform", `translate(${margin.left},${margin.top})`); .attr('transform', `translate(${margin.left},${margin.top})`);
svg.append("path").data([data]).attr("class", "line").attr("d", line); svg.append('path')
.data([data])
svg .attr('class', 'line')
.append("g") .attr('d', line);
.attr("transform", `translate(0,${height})`)
svg.append('g')
.attr('transform', `translate(0,${height})`)
.call(d3.axisBottom(x)); .call(d3.axisBottom(x));
svg.append("g").call(d3.axisLeft(y)); svg.append('g')
.call(d3.axisLeft(y));
} }
} }
function responsivefy(svg: any) { function responsivefy(svg: any) {
const container = d3.select(svg.node().parentNode); const container = d3.select(svg.node().parentNode);
const width = parseInt(svg.style("width"), 10); const width = parseInt(svg.style('width'), 10);
const height = parseInt(svg.style("height"), 10); const height = parseInt(svg.style('height'), 10);
const aspect = width / height; const aspect = width / height;
svg svg.attr('viewBox', `0 0 ${width} ${height}`)
.attr("viewBox", `0 0 ${width} ${height}`) .attr('preserveAspectRatio', 'xMinYMid')
.attr("preserveAspectRatio", "xMinYMid")
.call(resize); .call(resize);
d3.select(window).on("resize." + container.attr("id"), resize); d3.select(window).on(
'resize.' + container.attr('id'),
resize
);
function resize() { function resize() {
const w = parseInt(container.style("width")); const w = parseInt(container.style('width'));
svg.attr("width", w); svg.attr('width', w);
svg.attr("height", Math.round(w / aspect)); svg.attr('height', Math.round(w / aspect));
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -9,9 +9,7 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.40.1",
"@types/d3": "^7.4.1", "@types/d3": "^7.4.1",
"@types/node": "^20.11.4",
"autoprefixer": "^10.4.14", "autoprefixer": "^10.4.14",
"postcss": "^8.4.21", "postcss": "^8.4.21",
"sass": "^1.60.0", "sass": "^1.60.0",

View File

@@ -1,77 +0,0 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
baseURL: 'http://127.0.0.1:8000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
//{
// name: 'webkit',
// use: { ...devices['Desktop Safari'] },
//},
/* Test against mobile viewports. */
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
//{
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
//},
/* Test against branded browsers. */
//{
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
//},
//{
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
//},
],
/* Run your local dev server before starting the tests */
webServer: {
timeout: 15 * 60 * 1000,
command: 'cd .. && ./test_db.sh && cargo r',
url: 'http://127.0.0.1:8000'
},
});

View File

@@ -12,5 +12,3 @@
@import 'components/chart'; @import 'components/chart';
@import 'components/search'; @import 'components/search';
@import 'components/important'; @import 'components/important';
@import 'components/searchable-table';
@import 'components/notification';

View File

@@ -28,8 +28,4 @@
&[aria-pressed='true'] { &[aria-pressed='true'] {
@apply outline outline-2 outline-offset-2 outline-primary-600 bg-primary-100 text-primary-950; @apply outline outline-2 outline-offset-2 outline-primary-600 bg-primary-100 text-primary-950;
} }
&-hidden {
@apply hidden;
}
} }

View File

@@ -4,8 +4,4 @@
.h2 { .h2 {
@apply font-bold uppercase tracking-wide text-center rounded-t-md text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-lg px-3 py-3; @apply font-bold uppercase tracking-wide text-center rounded-t-md text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 text-lg px-3 py-3;
}
.h3 {
@apply text-center text-xl uppercase tracking-wide font-bold text-primary-900 dark:text-white;
} }

View File

@@ -2,12 +2,3 @@
border-top-left-radius: 0px !important; border-top-left-radius: 0px !important;
border-top-right-radius: 0px !important; border-top-right-radius: 0px !important;
} }
.rounded-l-none-important {
border-bottom-left-radius: 0px !important;
border-top-left-radius: 0px !important;
}
.rounded-none-important {
border-radius: 0px !important;
}

View File

@@ -2,26 +2,6 @@
@apply relative block w-full bg-white dark:bg-black border-0 py-1.5 px-2 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-black placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:text-sm sm:leading-6; @apply relative block w-full bg-white dark:bg-black border-0 py-1.5 px-2 text-gray-900 dark:text-white ring-1 ring-inset ring-gray-300 dark:ring-black placeholder:text-gray-400 focus:z-10 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:text-sm sm:leading-6;
} }
.input-group {
@apply flex;
input[readonly],
select[disabled] {
opacity: .7;
}
&.editable {
input[type="reset"],
input[type="submit"] {
@apply block;
}
button[type="button"] {
@apply hidden;
}
}
}
select { select {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"); background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
background-repeat: no-repeat; background-repeat: no-repeat;

View File

@@ -10,12 +10,4 @@
&-white { &-white {
@apply text-white hover:text-primary-100 underline; @apply text-white hover:text-primary-100 underline;
} }
&-black {
@apply text-black hover:text-primary-950 dark:text-white hover:dark:text-primary-300 underline;
}
&-no-underline {
@apply no-underline;
}
} }

View File

@@ -1,5 +0,0 @@
.notification {
right: -.2rem;
top: -.1rem;
font-size: .5rem;
}

View File

@@ -1,178 +0,0 @@
/*!
* JSTable v1.6.5
*/
.dt-container{
position:relative;
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
overflow-y: hidden;
.dt-message {
text-align: center;
}
.dt-loading{
position: absolute;
top: 50%;
left: 50%;
width: 100%;
margin-left: -50%;
margin-top: -20px;
height: 40px;
text-align: center;
background-color: white;
display: flex;
justify-content: center;
align-items: center;
background: linear-gradient(to right,rgba(255,255,255,0) 0,rgba(255,255,255,0.9) 25%,rgba(255,255,255,0.9) 75%,rgba(255,255,255,0) 100%);
}
}
.dt-top,
.dt-bottom {
padding: 8px 10px;
display:flex;
justify-content:space-between;
.dt-info {
margin: 7px 0;
}
}
/* PAGER */
.dt-pagination {
ul {
margin: 0;
padding-left: 0;
li {
list-style: none;
float: left;
}
}
a, span{
border: 1px solid transparent;
float: left;
margin-left: 2px;
padding: 6px 12px;
position: relative;
text-decoration: none;
color: inherit;
}
a:hover {
background-color: #d9d9d9;
}
.active a{
&, &:focus, &:hover{
background-color: #d9d9d9;
cursor: default;
}
}
.dt-ellipsis span{
cursor: not-allowed;
}
.disabled a{
&, &:focus, &:hover{
cursor: not-allowed;
opacity: 0.4;
}
}
.pager a {
font-weight: bold;
}
}
.dt-table {
max-width: 100%;
width: 100%;
border-spacing: 0;
& > tbody, > tfoot, > thead{
& > tr{
& > td, & > th{
vertical-align: top;
padding: 8px 10px;
white-space: nowrap;
}
}
}
& > thead > tr{
& > th, & > td{
vertical-align: bottom;
text-align: left;
border-bottom: 1px solid #d9d9d9;
}
}
& > tfoot > tr{
& > th, & > td{
vertical-align: bottom;
text-align: left;
border-top: 1px solid #d9d9d9;
}
}
th {
vertical-align: bottom;
text-align: left;
&.dt-sorter {
position: relative;
cursor: pointer;
padding-right:20px;
&::before,
&::after {
content: "";
height: 0;
width: 0;
position: absolute;
right: 7px;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
opacity: 0.2;
}
&::before {
border-top: 4px solid #000;
top: 18px;
}
&::after {
border-bottom: 4px solid #000;
border-top: 4px solid transparent;
bottom: 22px;
}
&.asc::after,
&.desc::before {
opacity: 0.6;
}
}
}
}
.dt-loading.hidden{
display:none!important;
opacity:0!important;
}
.dt-input {
@extend .input;
}
.dt-selector {
@extend .input;
}

View File

@@ -10,7 +10,6 @@
&.open { &.open {
display: block; display: block;
height: 100dvh;
height: 100vh; height: 100vh;
right: 0; right: 0;
top: 0; top: 0;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

View File

@@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 372 372" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g transform="matrix(17.8229,1.77636e-15,-1.77636e-15,17.8229,-6843.65,-3821.15)">
<path d="M397.575,225.019C397.575,225.386 397.493,225.701 397.329,225.965C397.165,226.229 396.93,226.444 396.626,226.612C396.883,226.808 397.079,227.039 397.211,227.307C397.344,227.574 397.411,227.903 397.411,228.294C397.411,228.646 397.339,228.957 397.197,229.229C397.054,229.5 396.852,229.729 396.59,229.914C396.329,230.1 396.013,230.24 395.644,230.336C395.275,230.432 394.862,230.48 394.405,230.48C394.01,230.48 393.623,230.439 393.242,230.359C392.861,230.279 392.521,230.145 392.222,229.955C391.923,229.766 391.683,229.514 391.502,229.199C391.32,228.885 391.229,228.493 391.229,228.024L392.858,228.019C392.858,228.249 392.903,228.438 392.993,228.584C393.083,228.73 393.201,228.847 393.347,228.933C393.494,229.019 393.66,229.078 393.845,229.111C394.031,229.145 394.217,229.161 394.405,229.161C394.854,229.161 395.197,229.083 395.433,228.927C395.669,228.771 395.788,228.562 395.788,228.3C395.788,228.171 395.76,228.056 395.706,227.954C395.651,227.853 395.556,227.758 395.421,227.67C395.287,227.582 395.107,227.496 394.882,227.412C394.658,227.328 394.376,227.237 394.036,227.14C393.598,227.022 393.208,226.895 392.864,226.756C392.52,226.617 392.228,226.452 391.988,226.261C391.748,226.069 391.564,225.845 391.437,225.587C391.31,225.329 391.247,225.022 391.247,224.667C391.247,224.308 391.33,223.994 391.496,223.727C391.662,223.459 391.897,223.239 392.202,223.067C391.944,222.868 391.748,222.635 391.613,222.367C391.478,222.1 391.411,221.771 391.411,221.38C391.411,221.044 391.482,220.738 391.625,220.463C391.767,220.188 391.97,219.953 392.234,219.76C392.498,219.566 392.816,219.417 393.189,219.312C393.562,219.206 393.977,219.153 394.434,219.153C394.903,219.153 395.325,219.208 395.7,219.317C396.075,219.427 396.392,219.588 396.652,219.801C396.912,220.014 397.112,220.276 397.252,220.589C397.393,220.901 397.463,221.259 397.463,221.661L395.829,221.661C395.829,221.493 395.799,221.337 395.741,221.192C395.682,221.048 395.594,220.922 395.477,220.814C395.36,220.707 395.214,220.622 395.041,220.56C394.867,220.497 394.665,220.466 394.434,220.466C394.192,220.466 393.983,220.49 393.807,220.539C393.631,220.588 393.487,220.653 393.374,220.735C393.26,220.817 393.177,220.913 393.125,221.022C393.072,221.132 393.045,221.247 393.045,221.368C393.045,221.517 393.07,221.644 393.119,221.749C393.168,221.855 393.256,221.951 393.385,222.039C393.514,222.127 393.691,222.21 393.916,222.288C394.14,222.366 394.428,222.452 394.78,222.546C395.225,222.663 395.622,222.791 395.969,222.93C396.317,223.068 396.61,223.233 396.848,223.425C397.086,223.616 397.267,223.841 397.39,224.099C397.513,224.356 397.575,224.663 397.575,225.019ZM394.206,223.917C393.901,223.835 393.62,223.747 393.362,223.653C393.19,223.735 393.065,223.852 392.987,224.002C392.909,224.152 392.87,224.323 392.87,224.515C392.87,224.671 392.894,224.805 392.943,224.916C392.992,225.027 393.081,225.13 393.21,225.224C393.338,225.317 393.515,225.406 393.74,225.49C393.964,225.574 394.252,225.669 394.604,225.774C394.756,225.817 394.903,225.859 395.044,225.9C395.184,225.941 395.321,225.985 395.454,226.032C395.622,225.942 395.751,225.823 395.84,225.675C395.93,225.526 395.975,225.358 395.975,225.171C395.975,225.03 395.947,224.905 395.89,224.796C395.834,224.687 395.737,224.583 395.6,224.485C395.463,224.388 395.282,224.294 395.055,224.204C394.829,224.114 394.545,224.019 394.206,223.917Z" style="fill:white;fill-rule:nonzero;"/>
</g>
<g transform="matrix(-0.695637,0.718394,-0.718394,-0.695637,185.736,185.735)">
<circle cx="0" cy="0" r="185.772" style="fill:url(#_Linear1);"/>
</g>
<g transform="matrix(0.291595,0,0,0.291595,185.736,185.735)">
<g transform="matrix(1,0,0,1,-291.5,-512)">
<clipPath id="_clip2">
<rect x="0" y="0" width="583" height="1024"/>
</clipPath>
<g clip-path="url(#_clip2)">
<g transform="matrix(1,0,0,1,-1574,-536.199)">
<g transform="matrix(1,0,0,1,0,10.8235)">
<g transform="matrix(0.948324,0.317305,0.307947,-0.920356,-304.665,1886.18)">
<rect x="1838.29" y="1006.52" width="17.353" height="644.204" style="fill:white;"/>
</g>
<path d="M1944.68,934.772C1921.09,896.523 1932.18,782.181 1974.56,655.531C1990.23,608.676 2009.04,563.787 2029.1,525.376L2156.88,568.132C2149.78,610.876 2137.79,658.046 2122.11,704.901C2079.26,832.966 2018.43,931.728 1976.52,946.409L2085.08,568.504L1944.68,934.772Z" style="fill:white;"/>
</g>
<g transform="matrix(-1,0,0,1,3730.88,10.8235)">
<g transform="matrix(0.948324,0.317305,0.307947,-0.920356,-304.665,1886.18)">
<rect x="1838.29" y="1006.52" width="17.353" height="644.204" style="fill:white;"/>
</g>
<path d="M1944.68,934.772C1921.09,896.523 1932.18,782.181 1974.56,655.531C1990.23,608.676 2009.04,563.787 2029.1,525.376L2156.88,568.132C2149.78,610.876 2137.79,658.046 2122.11,704.901C2079.26,832.966 2018.43,931.728 1976.52,946.409L2085.08,568.504L1944.68,934.772Z" style="fill:white;"/>
</g>
</g>
</g>
</g>
</g>
<defs>
<linearGradient id="_Linear1" x1="0" y1="0" x2="1" y2="0" gradientUnits="userSpaceOnUse" gradientTransform="matrix(371.543,-2.84217e-14,2.84217e-14,371.543,-185.772,1.13687e-13)"><stop offset="0" style="stop-color:rgb(131,0,0);stop-opacity:1"/><stop offset="1" style="stop-color:rgb(255,0,0);stop-opacity:1"/></linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

View File

@@ -1,19 +0,0 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@@ -1,4 +0,0 @@
/*!
* JSTable v1.6.5
*/
const JSTableDefaultConfig={perPage:5,perPageSelect:[5,10,15,20,25],sortable:!0,searchable:!0,nextPrev:!0,firstLast:!1,prevText:"&lsaquo;",nextText:"&rsaquo;",firstText:"&laquo;",lastText:"&raquo;",ellipsisText:"&hellip;",truncatePager:!0,pagerDelta:2,classes:{top:"dt-top",info:"dt-info",input:"dt-input",table:"dt-table",bottom:"dt-bottom",search:"dt-search",sorter:"dt-sorter",wrapper:"dt-wrapper",dropdown:"dt-dropdown",ellipsis:"dt-ellipsis",selector:"dt-selector",container:"dt-container",pagination:"dt-pagination",loading:"dt-loading",message:"dt-message"},labels:{placeholder:"Search...",perPage:"{select} entries per page",noRows:"No entries found",info:"Showing {start} to {end} of {rows} entries",loading:"Loading...",infoFiltered:"Showing {start} to {end} of {rows} entries (filtered from {rowsTotal} entries)"},layout:{top:"{select}{search}",bottom:"{info}{pager}"},serverSide:!1,deferLoading:null,ajax:null,ajaxParams:{},queryParams:{page:"page",search:"search",sortColumn:"sortColumn",sortDirection:"sortDirection",perPage:"perPage"},addQueryParams:!0,rowAttributesCreator:null,searchDelay:null,method:"GET"};class JSTable{constructor(e,t={}){let s=e;"string"==typeof e&&(s=document.querySelector(e)),null!==s&&(this.config=this._merge(JSTableDefaultConfig,t),this.table=new JSTableElement(s),this.currentPage=1,this.columnRenderers=[],this.columnsNotSearchable=[],this.searchQuery=null,this.sortColumn=null,this.sortDirection="asc",this.isSearching=!1,this.dataCount=null,this.filteredDataCount=null,this.searchTimeout=null,this.pager=new JSTablePager(this),this._build(),this._buildColumns(),this.update(null===this.config.deferLoading),this._bindEvents(),this._emit("init"),this._parseQueryParams())}_build(){let e=this.config;this.wrapper=document.createElement("div"),this.wrapper.className=e.classes.wrapper;var t=["<div class='",e.classes.top,"'>",e.layout.top,"</div>","<div class='",e.classes.container,"'>","<div class='",e.classes.loading," hidden'>",e.labels.loading,"</div>","</div>","<div class='",e.classes.bottom,"'>",e.layout.bottom,"</div>"].join("");if(t=t.replace("{info}","<div class='"+e.classes.info+"'></div>"),e.perPageSelect){var s=["<div class='",e.classes.dropdown,"'>","<label>",e.labels.perPage,"</label>","</div>"].join(""),a=document.createElement("select");a.className=e.classes.selector,e.perPageSelect.forEach((function(t){var s=t===e.perPage,r=new Option(t,t,s,s);a.add(r)})),s=s.replace("{select}",a.outerHTML),t=t.replace(/\{select\}/g,s)}else t=t.replace(/\{select\}/g,"");if(e.searchable){var r=["<div class='",e.classes.search,"'>","<input class='",e.classes.input,"' placeholder='",e.labels.placeholder,"' type='text'>","</div>"].join("");t=t.replace(/\{search\}/g,r)}else t=t.replace(/\{search\}/g,"");this.table.element.classList.add(e.classes.table),t=t.replace("{pager}","<div class='"+e.classes.pagination+"'></div>"),this.wrapper.innerHTML=t,this.table.element.parentNode.replaceChild(this.wrapper,this.table.element),this.wrapper.querySelector("."+e.classes.container).appendChild(this.table.element),this._updatePagination(),this._updateInfo()}async update(e=!0){var t=this;this.currentPage>this.pager.getPages()&&(this.currentPage=this.pager.getPages());let s=t.wrapper.querySelector(" ."+t.config.classes.loading);if(s.classList.remove("hidden"),this.table.header.getCells().forEach((function(e,s){let a=t.table.head.rows[0].cells[s];a.innerHTML=e.getInnerHTML(),e.classes.length>0&&(a.className=e.classes.join(" "));for(let t in e.attributes)a.setAttribute(t,e.attributes[t]);a.setAttribute("data-sortable",e.isSortable)})),e)return this.getPageData(this.currentPage).then((function(e){t.table.element.classList.remove("hidden"),t.table.body.innerHTML="",e.forEach((function(e){t.table.body.appendChild(e.getFormatted(t.columnRenderers,t.config.rowAttributesCreator))})),s.classList.add("hidden")})).then((function(){t.getDataCount()<=0&&(t.wrapper.classList.remove("search-results"),t.setMessage(t.config.labels.noRows)),t._emit("update")})).then((function(){t._updatePagination(),t._updateInfo()}));t.table.element.classList.remove("hidden"),t.table.body.innerHTML="",this.getDataCount()<=0&&(t.wrapper.classList.remove("search-results"),t.setMessage(t.config.labels.noRows)),this._getData().forEach((function(e){t.table.body.appendChild(e.getFormatted(t.columnRenderers,t.config.rowAttributesCreator))})),s.classList.add("hidden")}_updatePagination(){let e=this.wrapper.querySelector(" ."+this.config.classes.pagination);e.innerHTML="",e.appendChild(this.pager.render(this.currentPage))}_updateInfo(){let e=this.wrapper.querySelector(" ."+this.config.classes.info),t=this.isSearching?this.config.labels.infoFiltered:this.config.labels.info;if(e&&t.length){var s=t.replace("{start}",this.getDataCount()>0?this._getPageStartIndex()+1:0).replace("{end}",this._getPageEndIndex()+1).replace("{page}",this.currentPage).replace("{pages}",this.pager.getPages()).replace("{rows}",this.getDataCount()).replace("{rowsTotal}",this.getDataCountTotal());e.innerHTML=s}}_getPageStartIndex(){return(this.currentPage-1)*this.config.perPage}_getPageEndIndex(){let e=this.currentPage*this.config.perPage-1;return e>this.getDataCount()-1?this.getDataCount()-1:e}_getData(){return this._emit("getData",this.table.dataRows),this.table.dataRows.filter((function(e){return e.visible}))}_fetchData(){var e=this;let t={searchQuery:this.searchQuery,sortColumn:this.sortColumn,sortDirection:this.sortDirection,start:this._getPageStartIndex(),length:this.config.perPage,datatable:1};t=Object.assign({},this.config.ajaxParams,t);let s=this.config.ajax+"?"+this._queryParams(t);return fetch(s,{method:this.config.method,credentials:"same-origin",headers:{Accept:"application/json","Content-Type":"application/json"}}).then((function(e){return e.json()})).then((function(t){return e._emit("fetchData",t),e.dataCount=t.recordsTotal,e.filteredDataCount=t.recordsFiltered,t.data})).then((function(e){let t=[];return e.forEach((function(e){t.push(JSTableRow.createFromData(e))})),t})).catch((function(e){console.error(e)}))}_queryParams(e){return Object.keys(e).map((t=>encodeURIComponent(t)+"="+encodeURIComponent(e[t]))).join("&")}getDataCount(){return this.isSearching?this.getDataCountFiltered():this.getDataCountTotal()}getDataCountFiltered(){return this.config.serverSide?this.filteredDataCount:this._getData().length}getDataCountTotal(){return this.config.serverSide?null!==this.config.deferLoading?this.config.deferLoading:this.dataCount:this.table.dataRows.length}getPageData(){if(this.config.serverSide)return this._fetchData();let e=this._getPageStartIndex();var t=this._getPageEndIndex();return Promise.resolve(this._getData()).then((function(s){return s.filter((function(s,a){return a>=e&&a<=t}))}))}async search(e){var t=this;if(this.searchQuery===e.toLowerCase())return!1;if(this.searchQuery=e.toLowerCase(),this.config.searchDelay){if(this.searchTimeout)return!1;this.searchTimeout=setTimeout((function(){t.searchTimeout=null}),this.config.searchDelay)}return this.currentPage=1,this.isSearching=!0,this.searchQuery.length?(this.config.serverSide||this.table.dataRows.forEach((function(e){e.visible=!1,t.searchQuery.split(" ").reduce((function(s,a){var r;let i=e.getCells();return i=i.filter((function(e,s){if(t.columnsNotSearchable.indexOf(s)<0)return!0})),r=i.some((function(e,t){if(e.getTextContent().toLowerCase().indexOf(a)>=0)return!0})),s&&r}),!0)&&(e.visible=!0)})),this.wrapper.classList.add("search-results"),this.update().then((function(){t._emit("search",e)}))):(this.table.dataRows.forEach((function(e){e.visible=!0})),this.isSearching=!1,t.wrapper.classList.remove("search-results"),t.update(),!1)}sort(e,t,s=!1){var a=this;if(this.sortColumn=e||0,this.sortDirection=t,this.sortColumn<0||this.sortColumn>this.table.getColumnCount()-1)return!1;var r=this.table.header.getCell(this.sortColumn),i=this.table.dataRows;this.table.header.getCells().forEach((function(e){e.removeClass("asc"),e.removeClass("desc")})),r.addClass(this.sortDirection),this.config.serverSide||(i=i.sort((function(e,t){var s=e.getCellTextContent(a.sortColumn).toLowerCase(),r=t.getCellTextContent(a.sortColumn).toLowerCase();return s=s.replace(/(\$|\,|\s|%)/g,""),r=r.replace(/(\$|\,|\s|%)/g,""),s=isNaN(s)||""===s?s:parseFloat(s),r=isNaN(r)||""===r?r:parseFloat(r),""===s&&""!==r||!isNaN(s)&&isNaN(r)?"asc"===a.sortDirection?1:-1:""!==s&&""===r||isNaN(s)&&!isNaN(r)?"asc"===a.sortDirection?-1:1:"asc"===a.sortDirection?s===r?0:s>r?1:-1:s===r?0:s<r?1:-1})),this.table.dataRows=i),this.config.serverSide&&s||this.update(),this._emit("sort",this.sortColumn,this.sortDirection)}async paginate(e){var t=this;return this.currentPage=e,this.update().then((function(){t._emit("paginate",t.currentPage,e)}))}_setQueryParam(e,t){if(!this.config.addQueryParams)return;const s=new URL(window.location.href);s.searchParams.set(this.config.queryParams[e],t),window.history.replaceState(null,null,s)}_bindEvents(){var e=this;this.wrapper.addEventListener("click",(function(t){var s=t.target;if(s.hasAttribute("data-page")){t.preventDefault();let a=parseInt(s.getAttribute("data-page"),10);e.paginate(a),e._setQueryParam("page",a)}if("TH"===s.nodeName&&s.hasAttribute("data-sortable")){if("false"===s.getAttribute("data-sortable"))return!1;t.preventDefault();let a=s.classList.contains("asc")?"desc":"asc";e.sort(s.cellIndex,a),e._setQueryParam("sortColumn",s.cellIndex),e._setQueryParam("sortDirection",a)}})),this.config.perPageSelect&&this.wrapper.addEventListener("change",(function(t){var s=t.target;if("SELECT"===s.nodeName&&s.classList.contains(e.config.classes.selector)){t.preventDefault();let a=parseInt(s.value,10);e._emit("perPageChange",e.config.perPage,a),e.config.perPage=a,e.update(),e._setQueryParam("perPage",a)}})),this.config.searchable&&this.wrapper.addEventListener("keyup",(function(t){"INPUT"===t.target.nodeName&&t.target.classList.contains(e.config.classes.input)&&(t.preventDefault(),e.search(t.target.value),e._setQueryParam("search",t.target.value))}))}on(e,t){this.events=this.events||{},this.events[e]=this.events[e]||[],this.events[e].push(t)}off(e,t){this.events=this.events||{},e in this.events!=!1&&this.events[e].splice(this.events[e].indexOf(t),1)}_emit(e){if(this.events=this.events||{},e in this.events!=!1)for(var t=0;t<this.events[e].length;t++)this.events[e][t].apply(this,Array.prototype.slice.call(arguments,1))}setMessage(e){var t=this.table.getColumnCount(),s=document.createElement("tr");s.innerHTML='<td class="'+this.config.classes.message+'" colspan="'+t+'">'+e+"</td>",this.table.body.innerHTML="",this.table.body.appendChild(s)}_buildColumns(){var e=this;let t=null,s=null;this.config.columns&&this.config.columns.forEach((function(a){isNaN(a.select)||(a.select=[a.select]),a.select.forEach((function(r){var i=e.table.header.getCell(r);if(void 0!==i){if(a.hasOwnProperty("render")&&"function"==typeof a.render&&(e.columnRenderers[r]=a.render),a.hasOwnProperty("sortable")){let r=!1;i.hasSortable?r=i.isSortable:(r=a.sortable,i.setSortable(r)),r&&(i.addClass(e.config.classes.sorter),a.hasOwnProperty("sort")&&1===a.select.length&&(t=a.select[0],s=a.sort))}a.hasOwnProperty("searchable")&&(i.addAttribute("data-searchable",a.searchable),!1===a.searchable&&e.columnsNotSearchable.push(r))}}))})),this.table.header.getCells().forEach((function(a,r){null===a.isSortable&&a.setSortable(e.config.sortable),a.isSortable&&(a.addClass(e.config.classes.sorter),a.hasSort&&(t=r,s=a.sortDirection))})),null!==t&&e.sort(t,s,!0)}_merge(e,t){var s=this;return Object.keys(e).forEach((function(a){!t.hasOwnProperty(a)||"object"!=typeof t[a]||t[a]instanceof Array||null===t[a]?t.hasOwnProperty(a)||(t[a]=e[a]):s._merge(e[a],t[a])})),t}async _parseQueryParams(){const e=new URLSearchParams(window.location.search);let t=e.get(this.config.queryParams.perPage);if(t){t=parseInt(t),this.config.perPage=t,this.wrapper.querySelectorAll("."+this.config.classes.selector).forEach((function(e){e.querySelectorAll("option").forEach((e=>e.removeAttribute("selected"))),e.value=t,e.querySelector(`option[value='${t}']`).setAttribute("selected","")})),this.update()}let s=e.get(this.config.queryParams.search);if(s){this.wrapper.querySelectorAll("."+this.config.classes.input).forEach((function(e){e.value=s})),await this.search(s)}let a=e.get(this.config.queryParams.page);a&&await this.paginate(parseInt(a));let r=e.get(this.config.queryParams.sortColumn);if(r){r=parseInt(r);let t=e.get(this.config.queryParams.sortDirection);t=null==t?"asc":t,this.sort(r,t)}}}class JSTableElement{constructor(e){this.element=e,this.body=this.element.tBodies[0],this.head=this.element.tHead,this.rows=Array.from(this.element.rows).map((function(e,t){return new JSTableRow(e,e.parentNode.nodeName,t)})),this.dataRows=this._getBodyRows(),this.header=this._getHeaderRow()}_getBodyRows(){return this.rows.filter((function(e){return!e.isHeader&&!e.isFooter}))}_getHeaderRow(){return this.rows.find((function(e){return e.isHeader}))}getColumnCount(){return this.header.getColumnCount()}getFooterRow(){return this.rows.find((function(e){return e.isFooter}))}}class JSTableRow{constructor(e,t="",s=null){this.cells=Array.from(e.cells).map((function(e){return new JSTableCell(e)})),this.d=this.cells.length,this.isHeader="THEAD"===t,this.isFooter="TFOOT"===t,this.visible=!0,this.rowID=s;var a=this;this.attributes={},[...e.attributes].forEach((function(e){a.attributes[e.name]=e.value}))}getCells(){return Array.from(this.cells)}getColumnCount(){return this.cells.length}getCell(e){return this.cells[e]}getCellTextContent(e){return this.getCell(e).getTextContent()}static createFromData(e){let t=document.createElement("tr");if(e.hasOwnProperty("data")){if(e.hasOwnProperty("attributes"))for(const s in e.attributes)t.setAttribute(s,e.attributes[s]);e=e.data}return e.forEach((function(e){let s=document.createElement("td");if(s.innerHTML=e&&e.hasOwnProperty("data")?e.data:e,e&&e.hasOwnProperty("attributes"))for(const t in e.attributes)s.setAttribute(t,e.attributes[t]);t.appendChild(s)})),new JSTableRow(t)}getFormatted(e,t=null){let s=document.createElement("tr");var a=this;for(let e in this.attributes)s.setAttribute(e,this.attributes[e]);let r=t?t.call(this,this.getCells()):{};for(const e in r)s.setAttribute(e,r[e]);return this.getCells().forEach((function(t,r){var i=document.createElement("td");i.innerHTML=t.getInnerHTML(),e.hasOwnProperty(r)&&(i.innerHTML=e[r].call(a,t.getElement(),r)),t.classes.length>0&&(i.className=t.classes.join(" "));for(let e in t.attributes)i.setAttribute(e,t.attributes[e]);s.appendChild(i)})),s}setCellClass(e,t){this.cells[e].addClass(t)}}class JSTableCell{constructor(e){this.textContent=e.textContent,this.innerHTML=e.innerHTML,this.className="",this.element=e,this.hasSortable=e.hasAttribute("data-sortable"),this.isSortable=this.hasSortable?"true"===e.getAttribute("data-sortable"):null,this.hasSort=e.hasAttribute("data-sort"),this.sortDirection=e.getAttribute("data-sort"),this.classes=[];var t=this;this.attributes={},[...e.attributes].forEach((function(e){t.attributes[e.name]=e.value}))}getElement(){return this.element}getTextContent(){return this.textContent}getInnerHTML(){return this.innerHTML}setClass(e){this.className=e}setSortable(e){this.isSortable=e}addClass(e){this.classes.push(e)}removeClass(e){this.classes.indexOf(e)>=0&&this.classes.splice(this.classes.indexOf(e),1)}addAttribute(e,t){this.attributes[e]=t}}class JSTablePager{constructor(e){this.instance=e}getPages(){let e=Math.ceil(this.instance.getDataCount()/this.instance.config.perPage);return 0===e?1:e}render(){var e=this.instance.config;let t=this.getPages(),s=document.createElement("ul");if(t>1){let a=1===this.instance.currentPage?1:this.instance.currentPage-1,r=this.instance.currentPage===t?t:this.instance.currentPage+1;e.firstLast&&s.appendChild(this.createItem("pager",1,e.firstText)),e.nextPrev&&s.appendChild(this.createItem("pager",a,e.prevText)),this.truncate().forEach((function(e){s.appendChild(e)})),e.nextPrev&&s.appendChild(this.createItem("pager",r,e.nextText)),e.firstLast&&s.appendChild(this.createItem("pager",t,e.lastText))}return s}createItem(e,t,s,a){let r=document.createElement("li");return r.className=e,r.innerHTML=a?"<span>"+s+"</span>":'<a href="#" data-page="'+t+'">'+s+"</a>",r}isValidPage(e){return e>0&&e<=this.getPages()}truncate(){var e,t=this,s=t.instance.config,a=2*s.pagerDelta,r=t.instance.currentPage,i=r-s.pagerDelta,n=r+s.pagerDelta,o=this.getPages(),l=[],c=[];if(this.instance.config.truncatePager){r<4-s.pagerDelta+a?n=3+a:r>this.getPages()-(3-s.pagerDelta+a)&&(i=this.getPages()-(2+a));for(var h=1;h<=o;h++)(1===h||h===o||h>=i&&h<=n)&&l.push(h);l.forEach((function(a){e&&(a-e==2?c.push(t.createItem("",e+1,e+1)):a-e!=1&&c.push(t.createItem(s.classes.ellipsis,0,s.ellipsisText,!0))),c.push(t.createItem(a==r?"active":"",a,a)),e=a}))}else for(let e=1;e<=this.getPages();e++)c.push(this.createItem(e===r?"active":"",e,e));return c}}window.JSTable=JSTable;

View File

@@ -1,21 +0,0 @@
// @ts-ignore
new JSTable('#basic', {
perPage: 100,
perPageSelect: [10,100],
// Customise the display text
labels: {
placeholder: 'Suchen (z.B. "Linz")',
perPage: '{select} per Seite',
noRows: 'Keine Einträge gefunden',
info: 'Zeigt {start} bis {end} von {rows} Einträgen',
loading: 'Laden...',
infoFiltered: 'Zeigt {start} bis {end} von {rows} Einträgen (gefiltert aus {rowsTotal} Einträgen)'
},
// Customise the layout
layout: {
top: '{search}{select}',
bottom: '{info}{pager}'
},
});

View File

@@ -1,197 +0,0 @@
import { test, expect } from "@playwright/test";
test("cox can create and delete trip", async ({ page }) => {
await page.goto("/auth");
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("cox");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("cox");
await page.getByPlaceholder("Passwort").press("Enter");
await page.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click();
await page.locator('a[href="#"]:has-text("Ausfahrt")').first().click();
await page.locator("#sidebar #planned_starting_time").click();
await page.locator("#sidebar #planned_starting_time").fill("18:00");
await page.locator("#sidebar #planned_starting_time").press("Tab");
await page.locator("#sidebar #planned_starting_time").press("Tab");
await page.getByRole("spinbutton").fill("5");
await page.getByRole("button", { name: "Erstellen", exact: true }).click();
await expect(page.locator("body")).toContainText("18:00 Uhr (cox) Details");
await page.goto("/planned");
await page.getByRole('link', { name: 'Details' }).nth(1).click();
await page.getByRole("link", { name: "Termin löschen" }).click();
await expect(page.locator("body")).toContainText("Erfolgreich gelöscht!");
});
// TODO: group -> cox can create trips
// TODO: cox can help/register at trips/events
test.describe("cox can edit trips", () => {
let sharedPage: Page;
test.beforeAll(async ({ browser }) => {
const page = await browser.newPage();
await page.goto("/auth");
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("cox");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("cox");
await page.getByPlaceholder("Passwort").press("Enter");
await page.locator('li').filter({ hasText: 'Geplante Ausfahrten' }).getByRole('link').click();
await page.locator('a[href="#"]:has-text("Ausfahrt")').first().click();
await page.locator("#sidebar #planned_starting_time").click();
await page.locator("#sidebar #planned_starting_time").fill("18:00");
await page.locator("#sidebar #planned_starting_time").press("Tab");
await page.locator("#sidebar #planned_starting_time").press("Tab");
await page.getByRole("spinbutton").fill("5");
await page.getByRole("button", { name: "Erstellen", exact: true }).click();
sharedPage = page;
});
test("edit remarks", async () => {
await sharedPage.goto("/planned");
await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click();
await sharedPage.locator("#sidebar #notes").click();
await sharedPage.locator("#sidebar #notes").fill("Meine Anmerkung");
await sharedPage.getByRole("button", { name: "Speichern" }).click();
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText(
"Meine Anmerkung",
);
await sharedPage
.getByRole("button", { name: "Ausfahrt erstellen schließen" })
.click();
});
test("add and remove guest", async () => {
await sharedPage.goto("/planned");
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await sharedPage.locator("#sidebar #user_note").click();
await sharedPage.locator("#sidebar #user_note").fill("Mein Gast");
await sharedPage.getByRole("button", { name: "Gast hinzufügen" }).click();
await expect(sharedPage.locator("body")).toContainText(
"Erfolgreich angemeldet!",
);
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 4",
);
await expect(sharedPage.locator("#sidebar")).toContainText(
"Mein Gast (Gast) Abmelden",
);
await expect(
sharedPage.getByRole("link", { name: "Termin löschen" }),
).not.toBeVisible();
await sharedPage.getByRole("link", { name: "Abmelden" }).click();
await expect(sharedPage.locator("body")).toContainText(
"Erfolgreich abgemeldet!",
);
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 5",
);
await expect(sharedPage.locator("#sidebar")).toContainText(
"Keine Ruderer angemeldet",
);
await expect(
sharedPage.getByRole("link", { name: "Termin löschen" }),
).toBeVisible();
await sharedPage
.getByRole("button", { name: "Ausfahrt erstellen schließen" })
.click();
});
test("change amount rower", async () => {
await sharedPage.goto("/planned");
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await expect(sharedPage.locator("#sidebar")).toContainText(
"Freie Plätze: 5",
);
await sharedPage.getByRole("spinbutton").click();
await sharedPage.getByRole("spinbutton").fill("3");
await sharedPage.getByRole("button", { name: "Speichern" }).click();
await expect(sharedPage.locator("body")).toContainText(
"Ausfahrt erfolgreich aktualisiert.",
);
});
test("call off trip", async () => {
// Someone registers...
await sharedPage.goto("/auth/logout");
await sharedPage.goto("/auth");
await sharedPage.getByPlaceholder("Name").click();
await sharedPage.getByPlaceholder("Name").fill("rower");
await sharedPage.getByPlaceholder("Name").press("Tab");
await sharedPage.getByPlaceholder("Passwort").fill("rower");
await sharedPage.getByPlaceholder("Passwort").press("Enter");
await sharedPage.goto("/planned");
await sharedPage.getByRole('link', { name: 'Mitrudern' }).nth(1).click();
// Login as cox again
await sharedPage.goto("/auth/logout");
await sharedPage.goto("/auth");
await sharedPage.getByPlaceholder("Name").click();
await sharedPage.getByPlaceholder("Name").fill("cox");
await sharedPage.getByPlaceholder("Name").press("Tab");
await sharedPage.getByPlaceholder("Passwort").fill("cox");
await sharedPage.getByPlaceholder("Passwort").press("Enter");
await sharedPage.goto("/planned");
// ... now I can cancel trip
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await sharedPage.getByRole("button", { name: "Ausfahrt absagen" }).click();
await expect(sharedPage.locator("body")).toContainText(
"Ausfahrt erfolgreich aktualisiert.",
);
await expect(sharedPage.locator("body")).toContainText("(Absage cox)");
// Done with the test -> cancel the cancellation of the trip, otherwise the afterAll function below fails
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
await sharedPage.getByRole("spinbutton").click();
await sharedPage.getByRole("spinbutton").fill("3");
await sharedPage.getByRole("button", { name: "Speichern" }).click();
// deregistering
await sharedPage.goto("/auth/logout");
await sharedPage.goto("/auth");
await sharedPage.getByPlaceholder("Name").click();
await sharedPage.getByPlaceholder("Name").fill("rower");
await sharedPage.getByPlaceholder("Name").press("Tab");
await sharedPage.getByPlaceholder("Passwort").fill("rower");
await sharedPage.getByPlaceholder("Passwort").press("Enter");
await sharedPage.goto("/planned");
await sharedPage.getByRole('link', { name: 'Abmelden' }).click();
// now cox can delete trip again in afterAll
await sharedPage.goto("/auth/logout");
await sharedPage.goto("/auth");
await sharedPage.getByPlaceholder("Name").click();
await sharedPage.getByPlaceholder("Name").fill("cox");
await sharedPage.getByPlaceholder("Name").press("Tab");
await sharedPage.getByPlaceholder("Passwort").fill("cox");
await sharedPage.getByPlaceholder("Passwort").press("Enter");
});
test.afterAll(async () => {
await sharedPage.goto("/planned");
await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click();
await sharedPage.getByRole("link", { name: "Termin löschen" }).click();
await sharedPage.close();
});
// TODO: 'Immer anzeigen' (also verify the functionality), 'Gesperrt' + type
});

View File

@@ -1,381 +0,0 @@
import { test, expect } from "@playwright/test";
test("Cox can start and cancel trip", async ({ page }, testInfo) => {
await page.goto("/auth");
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("cox2");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("cox");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/");
await page.getByRole("link", { name: "Ausfahrt eintragen" }).click();
if (testInfo.project.name.includes("Mobile")) {
// No left boat selector on mobile views
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
await page.getByRole("option", { name: "Joe" }).click();
} else {
await page.getByText('2x', { exact: true }).click();
await page.getByText("Joe", { exact: true }).click();
}
await page.getByLabel('Remove item: \'6\'').click(); // remove pre-filled cox2
await page.getByPlaceholder("Ruderer auswählen").click();
await page.getByRole("option", { name: "rower2" }).click();
await page.getByRole("option", { name: "cox2" }).click();
await expect(page.getByRole("listbox")).toContainText(
"Nur 2 Ruderer können hinzugefügt werden",
);
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
"rower2 cox",
);
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
await expect(page.locator("body")).toContainText(
"Ausfahrt erfolgreich hinzugefügt",
);
await expect(page.locator("body")).toContainText("Joe");
await page.getByRole("link", { name: "Joe" }).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole("link", { name: "Löschen" }).click();
});
test("Cox can start and finish trip", async ({ page }, testInfo) => {
await page.goto("/auth");
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("cox2");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("cox");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/");
await page.getByRole("link", { name: "Ausfahrt eintragen" }).click();
if (testInfo.project.name.includes("Mobile")) {
// No left boat selector on mobile views
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
await page.getByRole("option", { name: "Joe" }).click();
} else {
await page.getByText('2x', { exact: true }).click();
await page.getByText("Joe", { exact: true }).click();
}
await page.getByLabel('Remove item: \'6\'').click(); // remove pre-filled cox2
await page.getByPlaceholder("Ruderer auswählen").click();
await page.getByRole("option", { name: "rower2" }).click();
await page.getByRole("option", { name: "cox2" }).click();
await expect(page.getByRole("listbox")).toContainText(
"Nur 2 Ruderer können hinzugefügt werden",
);
// Trip starts 2 hours ago
const datetimeSelector = '#departure';
const currentValue = await page.$eval(datetimeSelector, el => el.value);
const currentDate = new Date(currentValue);
currentDate.setMinutes(currentDate.getMinutes());
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
const newDatetime = currentDate.toISOString().slice(0, 16);
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
"rower2 cox",
);
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
await expect(page.locator("body")).toContainText(
"Ausfahrt erfolgreich hinzugefügt",
);
await expect(page.locator("body")).toContainText("Joe");
await page.goto("/log");
await page.locator("div:nth-child(2) > .border-0").click();
await page.getByRole("combobox", { name: "Destination" }).click();
await page.getByRole("combobox", { name: "Destination" }).fill("Ottensheim");
await page.getByRole("button", { name: "Ausfahrt beenden" }).click();
await expect(page.locator("body")).toContainText(
"Ausfahrt korrekt eingetragen",
);
await page.goto('/log/show');
await expect(page.locator('body')).toContainText('Joe');
await expect(page.locator('body')).toContainText('(cox2)');
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
// Login as admin
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show");
await page.getByRole('link', { name: 'Joe' }).nth(1).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole('link', { name: 'Löschen' }).click();
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
});
test("Kiosk can start and cancel trip", async ({ page }, testInfo) => {
await page.goto("/log/kiosk/ekrv2019/Linz");
if (testInfo.project.name.includes("Mobile")) {
// No left boat selector on mobile views
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
await page.getByRole("option", { name: "Joe" }).click();
} else {
await page.getByText('2x', { exact: true }).click();
await page.getByText("Joe", { exact: true }).click();
}
await page.getByPlaceholder("Ruderer auswählen").click();
await page.getByRole("option", { name: "rower2" }).click();
await page.getByRole("option", { name: "cox2" }).click();
await expect(page.getByRole("listbox")).toContainText(
"Nur 2 Ruderer können hinzugefügt werden",
);
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
"rower2 cox",
);
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
await expect(page.locator("body")).toContainText(
"Ausfahrt erfolgreich hinzugefügt",
);
await expect(page.locator("body")).toContainText("Joe");
await page.getByRole("link", { name: "Joe" }).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole("link", { name: "Löschen" }).click();
});
test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
await page.goto("/log/kiosk/ekrv2019/Linz");
if (testInfo.project.name.includes("Mobile")) {
// No left boat selector on mobile views
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
await page.getByRole("option", { name: "Joe" }).click();
} else {
await page.getByText('2x', { exact: true }).click();
await page.getByText("Joe", { exact: true }).click();
}
await page.getByPlaceholder("Ruderer auswählen").click();
await page.getByRole("option", { name: "rower2" }).click();
await page.getByRole("option", { name: "cox2" }).click();
await expect(page.getByRole("listbox")).toContainText(
"Nur 2 Ruderer können hinzugefügt werden",
);
// Trip starts 2 hours ago
const datetimeSelector = '#departure';
const currentValue = await page.$eval(datetimeSelector, el => el.value);
const currentDate = new Date(currentValue);
currentDate.setMinutes(currentDate.getMinutes());
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
const newDatetime = currentDate.toISOString().slice(0, 16);
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
"rower2 cox",
);
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
await expect(page.locator("body")).toContainText(
"Ausfahrt erfolgreich hinzugefügt",
);
await expect(page.locator("body")).toContainText("Joe");
await page.goto("/log");
await page.locator('div:nth-child(2) > .pt-2 > div > div > div:nth-child(2) > .border-0').click(); // 2 trips currently running, try to close second one
await page.getByRole("combobox", { name: "Destination" }).click();
await page.getByRole("combobox", { name: "Destination" }).fill("Ottensheim");
await page.getByRole("button", { name: "Ausfahrt beenden" }).click();
await expect(page.locator("body")).toContainText(
"Ausfahrt korrekt eingetragen",
);
await page.getByRole('link', { name: 'Logbuch' }).click();
await expect(page.locator('body')).toContainText('Joe');
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
//Ausloggen...
await page.context().clearCookies();
await page.goto("/auth");
// Login as admin
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show");
await page.getByRole('link', { name: 'Joe' }).nth(1).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole('link', { name: 'Löschen' }).click();
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
});
test("Cox can start and finish trip with cox steering only", async ({ page }, testInfo) => {
await page.goto("/auth");
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("cox2");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("cox");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/");
await page.getByRole("link", { name: "Ausfahrt eintragen" }).click();
if (testInfo.project.name.includes("Mobile")) {
// No left boat selector on mobile views
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
await page.getByRole("option", { name: "cox_only_steering_boat" }).click();
} else {
await page.getByText('2+', { exact: true }).click();
await page.getByText("cox_only_steering_boat", { exact: true }).click();
}
// Trip starts 2 hours ago
const datetimeSelector = '#departure';
const currentValue = await page.$eval(datetimeSelector, el => el.value);
const currentDate = new Date(currentValue);
currentDate.setMinutes(currentDate.getMinutes());
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
const newDatetime = currentDate.toISOString().slice(0, 16);
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
"cox",
);
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
await expect(page.locator("body")).toContainText(
"Ausfahrt erfolgreich hinzugefügt",
);
await expect(page.locator("body")).toContainText("cox_only_steering_boat");
await page.goto("/log");
await page.locator("div:nth-child(2) > .border-0").click();
await page.getByRole("combobox", { name: "Destination" }).click();
await page.getByRole("combobox", { name: "Destination" }).fill("Ottensheim");
await page.getByRole("button", { name: "Ausfahrt beenden" }).click();
await expect(page.locator("body")).toContainText(
"Ausfahrt korrekt eingetragen",
);
await page.goto('/log/show');
await expect(page.locator('body')).toContainText('cox_only_steering_boat');
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
// Login as admin
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show");
await page.getByRole("link", { name: "cox_only_steering_boat" }).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole('link', { name: 'Löschen' }).click();
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
});
test("Kiosk can start and finish trip in one stop", async ({ page }, testInfo) => {
await page.goto("/log/kiosk/ekrv2019/Linz");
if (testInfo.project.name.includes("Mobile")) {
// No left boat selector on mobile views
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
await page.getByRole("option", { name: "Joe" }).click();
} else {
await page.getByText('2x', { exact: true }).click();
await page.getByText("Joe", { exact: true }).click();
}
await page.getByPlaceholder("Ruderer auswählen").click();
await page.getByRole("option", { name: "rower2" }).click();
await page.getByRole("option", { name: "cox2" }).click();
await expect(page.getByRole("listbox")).toContainText(
"Nur 2 Ruderer können hinzugefügt werden",
);
// Trip starts 2 hours ago
const datetimeSelector = '#departure';
const currentValue = await page.$eval(datetimeSelector, el => el.value);
const currentDate = new Date(currentValue);
currentDate.setMinutes(currentDate.getMinutes());
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
const newDatetime = currentDate.toISOString().slice(0, 16);
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
await page.getByLabel('Ankunftszeit').click();
await page.locator('#destination').fill('a');
await page.getByLabel('Distanz').fill('1');
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
"rower2 cox",
);
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
await expect(page.locator("body")).toContainText(
"Ausfahrt erfolgreich hinzugefügt",
);
await page.getByRole('link', { name: 'Logbuch' }).click();
await expect(page.locator('body')).toContainText('Joe');
await expect(page.locator('body')).toContainText('(cox2)');
await expect(page.locator('body')).toContainText('a (1 km)');
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
//Ausloggen...
await page.context().clearCookies();
await page.goto("/auth");
// Login as admin
await page.getByPlaceholder("Name").click();
await page.getByPlaceholder("Name").fill("main");
await page.getByPlaceholder("Name").press("Tab");
await page.getByPlaceholder("Passwort").fill("admin");
await page.getByPlaceholder("Passwort").press("Enter");
await page.goto("/log/show");
await page.getByRole('link', { name: 'Joe' }).nth(1).click();
page.once("dialog", (dialog) => {
dialog.accept().catch(() => {});
});
await page.getByRole('link', { name: 'Löschen' }).click();
//Ausloggen...
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
await page.getByRole('link', { name: 'Ausloggen' }).click();
});

View File

@@ -15,6 +15,5 @@
"noUnusedParameters": true, "noUnusedParameters": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"skipLibCheck": true "skipLibCheck": true
}, }
"exclude": ["tests/"]
} }

View File

@@ -24,7 +24,6 @@ export default defineConfig({
input: { input: {
main: './main.ts', main: './main.ts',
logbook: './logbook.ts', logbook: './logbook.ts',
table: './table.ts',
// Example for more entry points // Example for more entry points
// test: './src/test.ts', // test: './src/test.ts',
}, },

View File

@@ -8,30 +8,12 @@ CREATE TABLE IF NOT EXISTS "user" (
"weight" text, "weight" text,
"sex" text, "sex" text,
"dirty_thirty" text, "dirty_thirty" text,
"dirty_dozen" text, "dirty_dozen" text
"member_since_date" text,
"birthdate" text,
"mail" text,
"nickname" text,
"notes" text,
"phone" text,
"address" text,
"family_id" INTEGER REFERENCES family(id),
"membership_pdf" BLOB,
"user_token" TEXT NOT NULL DEFAULT (lower(hex(randomblob(16))))
);
CREATE TABLE IF NOT EXISTS "family" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT
); );
CREATE TABLE IF NOT EXISTS "role" ( CREATE TABLE IF NOT EXISTS "role" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" text NOT NULL UNIQUE, "name" text NOT NULL UNIQUE
"formatted_name" text,
"desc" text,
"cluster" text,
"hide_in_lists" BOOLEAN NOT NULL DEFAULT false
); );
CREATE TABLE IF NOT EXISTS "user_role" ( CREATE TABLE IF NOT EXISTS "user_role" (
@@ -104,11 +86,9 @@ CREATE TABLE IF NOT EXISTS "boat" (
"year_built" INTEGER, "year_built" INTEGER,
"boatbuilder" TEXT, "boatbuilder" TEXT,
"default_shipmaster_only_steering" boolean default false not null, "default_shipmaster_only_steering" boolean default false not null,
"convert_handoperated_possible" boolean default false not null,
"default_destination" text, "default_destination" text,
"skull" boolean default true NOT NULL, -- false => riemen "skull" boolean default true NOT NULL, -- false => riemen
"external" boolean default false NOT NULL, -- false => owned by different club "external" boolean default false NOT NULL -- false => owned by different club
"deleted" boolean NOT NULL DEFAULT FALSE
); );
CREATE TABLE IF NOT EXISTS "logbook_type" ( CREATE TABLE IF NOT EXISTS "logbook_type" (
@@ -148,105 +128,3 @@ CREATE TABLE IF NOT EXISTS "boat_damage" (
"verified_at" datetime, "verified_at" datetime,
"lock_boat" boolean not null default false -- if true: noone can use the boat "lock_boat" boolean not null default false -- if true: noone can use the boat
); );
CREATE TABLE IF NOT EXISTS "boathouse" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT,
"boat_id" INTEGER NOT NULL REFERENCES boat(id),
"aisle" TEXT NOT NULL CHECK (aisle in ('water', 'middle', 'mountain')),
"side" TEXT NOT NULL CHECK(side IN ('mountain', 'water')),
"level" INTEGER NOT NULL CHECK(level BETWEEN 0 AND 11),
CONSTRAINT unq UNIQUE (aisle, side, level) -- only 1 boat allowed to rest at each space
);
CREATE TABLE IF NOT EXISTS "notification" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"user_id" INTEGER NOT NULL REFERENCES user(id),
"message" TEXT NOT NULL,
"read_at" DATETIME,
"created_at" DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
"category" TEXT NOT NULL,
"action_after_reading" TEXT,
"link" TEXT
);
CREATE TABLE IF NOT EXISTS "boat_reservation" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"boat_id" INTEGER NOT NULL REFERENCES boat(id),
"start_date" DATE NOT NULL,
"end_date" DATE NOT NULL,
"time_desc" TEXT NOT NULL,
"usage" TEXT NOT NULL,
"user_id_applicant" INTEGER NOT NULL REFERENCES user(id),
"user_id_confirmation" INTEGER REFERENCES user(id),
"created_at" datetime not null default CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS "waterlevel" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"day" DATE NOT NULL,
"time" TEXT NOT NULL,
"max" INTEGER NOT NULL,
"min" INTEGER NOT NULL,
"mittel" INTEGER NOT NULL,
"tumax" INTEGER NOT NULL,
"tumin" INTEGER NOT NULL,
"tumittel" INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS "weather" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"day" DATE NOT NULL,
"max_temp" FLOAT NOT NULL,
"wind_gust" FLOAT NOT NULL,
"rain_mm" FLOAT NOT NULL
);
CREATE TABLE IF NOT EXISTS "trailer" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" text NOT NULL UNIQUE
);
CREATE TABLE IF NOT EXISTS "trailer_reservation" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"trailer_id" INTEGER NOT NULL REFERENCES trailer(id),
"start_date" DATE NOT NULL,
"end_date" DATE NOT NULL,
"time_desc" TEXT NOT NULL,
"usage" TEXT NOT NULL,
"user_id_applicant" INTEGER NOT NULL REFERENCES user(id),
"user_id_confirmation" INTEGER REFERENCES user(id),
"created_at" datetime not null default CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS "distance" (
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
"destination" text NOT NULL,
"distance_in_km" integer NOT NULL
);
CREATE TABLE IF NOT EXISTS "activity" (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
text TEXT NOT NULL,
relevant_for TEXT NOT NULL, -- e.g. user_id=123;trip_id=456
keep_until DATETIME
);
CREATE TRIGGER IF NOT EXISTS prevent_multiple_roles_same_cluster
BEFORE INSERT ON user_role
BEGIN
SELECT CASE
WHEN EXISTS (
SELECT 1
FROM user_role ur
JOIN role r1 ON ur.role_id = r1.id
JOIN role r2 ON r1."cluster" = r2."cluster"
WHERE ur.user_id = NEW.user_id
AND r2.id = NEW.role_id
AND r1.id != NEW.role_id
)
THEN RAISE(ABORT, 'User already has a role in this cluster')
END;
END;

10
package-lock.json generated
View File

@@ -1,6 +1,10 @@
{ {
"name": "rowt", "name": "rot",
"lockfileVersion": 3, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": {} "packages": {
"": {
"name": "rot"
}
}
} }

View File

@@ -4,16 +4,12 @@ Description=Rot
[Service] [Service]
User=root User=root
Group=root Group=root
WorkingDirectory=/home/rowing WorkingDirectory=/home/k004373/rowing
Environment="ROCKET_ENV=prod" Environment="ROCKET_ENV=prod"
Environment="ROCKET_ADDRESS=127.0.0.1" Environment="ROCKET_ADDRESS=127.0.0.1"
Environment="ROCKET_PORT=8001" Environment="ROCKET_PORT=8001"
Environment="RUST_LOG=info" Environment="RUST_LOG=info"
Environment="DATABASE_URL=sqliteL///home/stationslauf/db.sqlite" ExecStart=/home/k004373/rowing/rot
ExecStart=/home/rowing/rot
Restart=always
RestartSec=10
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@@ -4,14 +4,12 @@ Description=Rot Staging
[Service] [Service]
User=root User=root
Group=root Group=root
WorkingDirectory=/home/rowing-staging WorkingDirectory=/home/k004373/rowing-staging
Environment="ROCKET_ENV=prod" Environment="ROCKET_ENV=prod"
Environment="ROCKET_ADDRESS=127.0.0.1" Environment="ROCKET_ADDRESS=127.0.0.1"
Environment="ROCKET_PORT=7999" Environment="ROCKET_PORT=7999"
Environment="ROCKET_LOG=info" Environment="ROCKET_LOG=info"
ExecStart=/home/rowing-staging/rot ExecStart=/home/k004373/rowing-staging/rot
Restart=always
RestartSec=10
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target

View File

@@ -2,58 +2,25 @@ INSERT INTO "role" (name) VALUES ('admin');
INSERT INTO "role" (name) VALUES ('cox'); INSERT INTO "role" (name) VALUES ('cox');
INSERT INTO "role" (name) VALUES ('scheckbuch'); INSERT INTO "role" (name) VALUES ('scheckbuch');
INSERT INTO "role" (name) VALUES ('tech'); INSERT INTO "role" (name) VALUES ('tech');
INSERT INTO "role" (name) VALUES ('Donau Linz');
INSERT INTO "role" (name) VALUES ('manage_events');
INSERT INTO "role" (name) VALUES ('Rennrudern');
INSERT INTO "role" (name) VALUES ('paid');
INSERT INTO "role" (name) VALUES ('Vorstand');
INSERT INTO "role" (name) VALUES ('Bootsführer');
INSERT INTO "role" (name) VALUES ('schnupperant');
INSERT INTO "role" (name) VALUES ('kassier');
INSERT INTO "role" (name) VALUES ('schriftfuehrer');
INSERT INTO "role" (name) VALUES ('no-einschreibgebuehr');
INSERT INTO "role" (name) VALUES ('schnupper-betreuer');
INSERT INTO "role" (name) VALUES ('allow_website_login');
INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM'); INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
INSERT INTO "user_role" (user_id, role_id) VALUES(1,1); INSERT INTO "user_role" (user_id, role_id) VALUES(1,1);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,2); INSERT INTO "user_role" (user_id, role_id) VALUES(1,2);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,6);
INSERT INTO "user" (name, pw) VALUES('rower', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY'); INSERT INTO "user" (name, pw) VALUES('rower', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
INSERT INTO "user_role" (user_id, role_id) VALUES(2,5);
INSERT INTO "user" (name, pw) VALUES('guest', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$GF6gizbI79Bh0zA9its8S0gram956v+YIV8w8VpwJnQ'); INSERT INTO "user" (name, pw) VALUES('guest', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$GF6gizbI79Bh0zA9its8S0gram956v+YIV8w8VpwJnQ');
INSERT INTO "user_role" (user_id, role_id) VALUES(3,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(3,3); INSERT INTO "user_role" (user_id, role_id) VALUES(3,3);
INSERT INTO "user" (name, pw) VALUES('cox', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs'); INSERT INTO "user" (name, pw) VALUES('cox', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs');
INSERT INTO "user_role" (user_id, role_id) VALUES(4,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(4,2); INSERT INTO "user_role" (user_id, role_id) VALUES(4,2);
INSERT INTO "user_role" (user_id, role_id) VALUES(4,8);
INSERT INTO "user" (name) VALUES('new'); INSERT INTO "user" (name) VALUES('new');
INSERT INTO "user_role" (user_id, role_id) VALUES(5,5);
INSERT INTO "user" (name, pw) VALUES('cox2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs'); INSERT INTO "user" (name, pw) VALUES('cox2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs');
INSERT INTO "user_role" (user_id, role_id) VALUES(6,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(6,2); INSERT INTO "user_role" (user_id, role_id) VALUES(6,2);
INSERT INTO "user" (name, pw) VALUES('rower2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY'); INSERT INTO "user" (name, pw) VALUES('rower2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
INSERT INTO "user_role" (user_id, role_id) VALUES(7,5);
INSERT INTO "user" (name, pw) VALUES('teen', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
INSERT INTO "user_role" (user_id, role_id) VALUES(8,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(8,7);
INSERT INTO "user" (name, pw) VALUES('Vorstandsmitglied', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
INSERT INTO "user_role" (user_id, role_id) VALUES(9,5);
INSERT INTO "user" (name, pw) VALUES('main', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
INSERT INTO "user_role" (user_id, role_id) VALUES(10,1);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,2);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,6);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,9);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, date('now'), 'trip_details for a planned event'); INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, '1970-01-01', 'trip_details for a planned event');
INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1); INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('11:00', 1, date('now', '+1 day'), 'trip_details for trip from cox'); INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('11:00', 1, '1970-01-02', 'trip_details for trip from cox');
INSERT INTO "trip" (cox_id, trip_details_id) VALUES(4, 2); INSERT INTO "trip" (cox_id, trip_details_id) VALUES(4, 2);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, date('now'), 'same trip_details as id=1');
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Regatta', 'Regatta!', 'Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?', '&#127941;'); INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Regatta', 'Regatta!', 'Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?', '&#127941;');
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Lange Ausfahrt', 'Lange Ausfahrt!', 'Das ist eine lange Ausfahrt! Willst du wirklich teilnehmen?', '&#128170;'); INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Lange Ausfahrt', 'Lange Ausfahrt!', 'Das ist eine lange Ausfahrt! Willst du wirklich teilnehmen?', '&#128170;');
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Wanderfahrt', 'Wanderfahrt!', 'Kein normales Event. Das ist eine Wanderfahrt! Bitte überprüfe ob du alle Anforderungen erfüllst. Willst du wirklich teilnehmen?', '&#9969;'); INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Wanderfahrt', 'Wanderfahrt!', 'Kein normales Event. Das ist eine Wanderfahrt! Bitte überprüfe ob du alle Anforderungen erfüllst. Willst du wirklich teilnehmen?', '&#9969;');
@@ -66,7 +33,6 @@ INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Kaputtes Boot :-('
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Sehr kaputtes Boot :-((', 7, 1); INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Sehr kaputtes Boot :-((', 7, 1);
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Ottensheim Boot', 7, 2); INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Ottensheim Boot', 7, 2);
INSERT INTO "boat" (name, amount_seats, location_id, owner) VALUES ('second_private_boat_from_rower', 1, 1, 2); INSERT INTO "boat" (name, amount_seats, location_id, owner) VALUES ('second_private_boat_from_rower', 1, 1, 2);
INSERT INTO "boat" (name, amount_seats, location_id, default_shipmaster_only_steering) VALUES ('cox_only_steering_boat', 3, 1, true);
INSERT INTO "logbook_type" (name) VALUES ('Wanderfahrt'); INSERT INTO "logbook_type" (name) VALUES ('Wanderfahrt');
INSERT INTO "logbook_type" (name) VALUES ('Regatta'); INSERT INTO "logbook_type" (name) VALUES ('Regatta');
INSERT INTO "logbook" (boat_id, shipmaster,steering_person, shipmaster_only_steering, departure) VALUES (2, 2, 2, false, strftime('%Y', 'now') || '-12-24 10:00'); INSERT INTO "logbook" (boat_id, shipmaster,steering_person, shipmaster_only_steering, departure) VALUES (2, 2, 2, false, strftime('%Y', 'now') || '-12-24 10:00');
@@ -75,8 +41,3 @@ INSERT INTO "logbook" (boat_id, shipmaster, steering_person, shipmaster_only_ste
INSERT INTO "rower" (logbook_id, rower_id) VALUES(3,3); INSERT INTO "rower" (logbook_id, rower_id) VALUES(3,3);
INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at) VALUES(4,'Dolle bei Position 2 fehlt', 5, '2142-12-24 15:02'); INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at) VALUES(4,'Dolle bei Position 2 fehlt', 5, '2142-12-24 15:02');
INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at, lock_boat) VALUES(5, 'TOHT', 5, '2142-12-24 15:02', 1); INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at, lock_boat) VALUES(5, 'TOHT', 5, '2142-12-24 15:02', 1);
INSERT INTO "notification" (user_id, message, category) VALUES (1, 'This is a test notification', 'test-cat');
INSERT INTO "trailer" (name) VALUES('Großer Hänger');
INSERT INTO "trailer" (name) VALUES('Kleiner Hänger');
insert into distance(destination, distance_in_km) values('Ottensheim', 25);

View File

@@ -1,104 +0,0 @@
INSERT INTO "role" (name) VALUES ('admin');
INSERT INTO "role" (name) VALUES ('cox');
INSERT INTO "role" (name) VALUES ('scheckbuch');
INSERT INTO "role" (name) VALUES ('tech');
INSERT INTO "role" (name) VALUES ('Donau Linz');
INSERT INTO "role" (name) VALUES ('manage_events');
INSERT INTO "role" (name) VALUES ('Rennrudern');
INSERT INTO "role" (name) VALUES ('paid');
INSERT INTO "role" (name) VALUES ('Vorstand');
INSERT INTO "role" (name) VALUES ('Bootsführer');
INSERT INTO "role" (name) VALUES ('schnupperant');
INSERT INTO "role" (name) VALUES ('kassier');
INSERT INTO "role" (name) VALUES ('schriftfuehrer');
INSERT INTO "role" (name) VALUES ('no-einschreibgebuehr');
INSERT INTO "role" (name) VALUES ('schnupper-betreuer');
INSERT INTO "role" (name) VALUES ('allow_website_login');
INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
INSERT INTO "user_role" (user_id, role_id) VALUES(1,1);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,2);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,6);
INSERT INTO "user" (name, pw) VALUES('rower', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
INSERT INTO "user_role" (user_id, role_id) VALUES(2,5);
INSERT INTO "user" (name, pw) VALUES('guest', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$GF6gizbI79Bh0zA9its8S0gram956v+YIV8w8VpwJnQ');
INSERT INTO "user_role" (user_id, role_id) VALUES(3,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(3,3);
INSERT INTO "user" (name, pw) VALUES('cox', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs');
INSERT INTO "user_role" (user_id, role_id) VALUES(4,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(4,2);
INSERT INTO "user_role" (user_id, role_id) VALUES(4,8);
INSERT INTO "user" (name) VALUES('new');
INSERT INTO "user_role" (user_id, role_id) VALUES(5,5);
INSERT INTO "user" (name, pw) VALUES('cox2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs');
INSERT INTO "user_role" (user_id, role_id) VALUES(6,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(6,2);
INSERT INTO "user" (name, pw) VALUES('rower2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
INSERT INTO "user_role" (user_id, role_id) VALUES(7,5);
INSERT INTO "user" (name, pw) VALUES('teen', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
INSERT INTO "user_role" (user_id, role_id) VALUES(8,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(8,7);
INSERT INTO "user" (name, pw) VALUES('Vorstandsmitglied', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
INSERT INTO "user_role" (user_id, role_id) VALUES(9,5);
INSERT INTO "user" (name, pw) VALUES('main', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
INSERT INTO "user_role" (user_id, role_id) VALUES(10,1);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,2);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,6);
INSERT INTO "user_role" (user_id, role_id) VALUES(10,9);
INSERT INTO "user" (name, pw) VALUES('Lukas Rudinger', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM'); --11
INSERT INTO "user_role" (user_id, role_id) VALUES(11,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(11,2);
INSERT INTO "user_role" (user_id, role_id) VALUES(11,8);
INSERT INTO "user" (name, pw) VALUES('Claudia Fröhlich', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM'); --12
INSERT INTO "user_role" (user_id, role_id) VALUES(12,6);
INSERT INTO "user_role" (user_id, role_id) VALUES(12,5);
INSERT INTO "user" (name, pw) VALUES('Adeline Krebs', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM'); --13
INSERT INTO "user_role" (user_id, role_id) VALUES(13,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(13,2);
INSERT INTO "user_role" (user_id, role_id) VALUES(13,8);
INSERT INTO "user" (name, pw) VALUES('Michael Schweiß', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM'); --13
INSERT INTO "user_role" (user_id, role_id) VALUES(14,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(14,8);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('06:00', 4, date('now'), '');
INSERT INTO "trip" (cox_id, trip_details_id) VALUES(13, 1);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('14:00', 8, date('now'), 'Lasst uns den Markt entern!!');
INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('Marktfahrt', 2, 2);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('17:00', 4, date('now'), 'Feierabend-Ausfahrt');
INSERT INTO "trip" (cox_id, trip_details_id) VALUES(11, 3);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('18:00', 8, date('now'), '');
INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('Anfängertraining Ergo', 1, 4);
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('14:00', 4, date('now', '+1 day'), 'Der frühe Wurm wird vom Vogel gefressen!');
INSERT INTO "trip" (cox_id, trip_details_id) VALUES(13, 5);
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Regatta', 'Regatta!', 'Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?', '&#127941;');
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Lange Ausfahrt', 'Lange Ausfahrt!', 'Das ist eine lange Ausfahrt! Willst du wirklich teilnehmen?', '&#128170;');
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Wanderfahrt', 'Wanderfahrt!', 'Kein normales Event. Das ist eine Wanderfahrt! Bitte überprüfe ob du alle Anforderungen erfüllst. Willst du wirklich teilnehmen?', '&#9969;');
INSERT INTO "location" (name) VALUES ('Linz');
INSERT INTO "location" (name) VALUES ('Ottensheim');
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Haichenbach', 1, 1);
INSERT INTO "boat" (name, amount_seats, location_id, owner) VALUES ('private_boat_from_rower', 1, 1, 2);
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Joe', 2, 1);
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Kaputtes Boot :-(', 7, 1);
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Sehr kaputtes Boot :-((', 7, 1);
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Ottensheim Boot', 7, 2);
INSERT INTO "boat" (name, amount_seats, location_id, owner) VALUES ('second_private_boat_from_rower', 1, 1, 2);
INSERT INTO "boat" (name, amount_seats, location_id, default_shipmaster_only_steering) VALUES ('cox_only_steering_boat', 3, 1, true);
INSERT INTO "logbook_type" (name) VALUES ('Wanderfahrt');
INSERT INTO "logbook_type" (name) VALUES ('Regatta');
INSERT INTO "logbook" (boat_id, shipmaster,steering_person, shipmaster_only_steering, departure) VALUES (2, 2, 2, false, strftime('%Y', 'now') || '-12-24 10:00');
INSERT INTO "logbook" (boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km) VALUES (1, 4, 4, false, strftime('%Y', 'now') || '-12-24 10:00', strftime('%Y', 'now') || '-12-24 15:00', 'Ottensheim', 25);
INSERT INTO "logbook" (boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km) VALUES (3, 4, 4, false, strftime('%Y', 'now') || '-12-24 10:00', strftime('%Y', 'now') || '-12-24 11:30', 'Ottensheim + Regattastrecke', 29);
INSERT INTO "rower" (logbook_id, rower_id) VALUES(3,3);
INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at) VALUES(4,'Dolle bei Position 2 fehlt', 5, '2142-12-24 15:02');
INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at, lock_boat) VALUES(5, 'TOHT', 5, '2142-12-24 15:02', 1);
INSERT INTO "notification" (user_id, message, category) VALUES (1, 'This is a test notification', 'test-cat');
INSERT INTO "trailer" (name) VALUES('Großer Hänger');
INSERT INTO "trailer" (name) VALUES('Kleiner Hänger');
insert into distance(destination, distance_in_km) values('Ottensheim', 25);

1
shame.txt Normal file
View File

@@ -0,0 +1 @@
2023-06-06: Phil Baillon um 19:10 für 18 Uhr Fahrt abgemeldet

View File

@@ -1,7 +1,3 @@
#![allow(clippy::blocks_in_conditions)]
use std::ops::Deref;
pub mod model; pub mod model;
#[cfg(feature = "rowing-tera")] #[cfg(feature = "rowing-tera")]
@@ -10,91 +6,6 @@ pub mod tera;
#[cfg(feature = "rest")] #[cfg(feature = "rest")]
pub mod rest; pub mod rest;
pub mod scheduled;
pub(crate) const AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD: i64 = 10;
pub(crate) const RENNRUDERBEITRAG: i64 = 11000;
pub(crate) const BOAT_STORAGE: i64 = 4500;
pub(crate) const FAMILY_TWO: i64 = 30000;
pub(crate) const FAMILY_THREE_OR_MORE: i64 = 35000;
pub(crate) const STUDENT_OR_PUPIL: i64 = 8000;
pub(crate) const REGULAR: i64 = 22000;
pub(crate) const UNTERSTUETZEND: i64 = 2500;
pub(crate) const FOERDERND: i64 = 8500;
pub(crate) const SCHECKBUCH: i64 = 3000;
pub(crate) const EINSCHREIBGEBUEHR: i64 = 3000;
pub(crate) const DUAL_MEMBERSHIP: i64 = 18000;
pub(crate) const TRIAL_ROWING: i64 = 12000;
pub(crate) const TRIAL_ROWING_REDUCED: i64 = 6000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NonEmptyString(String);
impl NonEmptyString {
pub fn new(s: String) -> Option<Self> {
if s.is_empty() {
None
} else {
Some(NonEmptyString(s))
}
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_string(self) -> String {
self.0
}
}
// Implement Deref to allow automatic dereferencing to &str
impl Deref for NonEmptyString {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
// This allows &NonEmptyString to be converted to &str
impl AsRef<str> for NonEmptyString {
fn as_ref(&self) -> &str {
&self.0
}
}
// This allows NonEmptyString to be converted to String with .into()
impl From<NonEmptyString> for String {
fn from(s: NonEmptyString) -> Self {
s.0
}
}
impl TryFrom<&str> for NonEmptyString {
type Error = &'static str;
fn try_from(s: &str) -> Result<Self, Self::Error> {
if s.is_empty() {
Err("String cannot be empty")
} else {
Ok(NonEmptyString(s.to_string()))
}
}
}
impl TryFrom<String> for NonEmptyString {
type Error = &'static str;
fn try_from(s: String) -> Result<Self, Self::Error> {
if s.is_empty() {
Err("String cannot be empty")
} else {
Ok(NonEmptyString(s))
}
}
}
#[cfg(test)] #[cfg(test)]
#[macro_export] #[macro_export]
macro_rules! testdb { macro_rules! testdb {

View File

@@ -1,14 +1,11 @@
#![allow(clippy::blocks_in_conditions)]
use std::str::FromStr; use std::str::FromStr;
#[cfg(feature = "rest")] #[cfg(feature = "rest")]
use rot::rest; use rot::rest;
#[cfg(feature = "rowing-tera")] #[cfg(feature = "rowing-tera")]
use rot::tera; use rot::tera;
use rot::{scheduled, tera::Config};
use sqlx::{ConnectOptions, pool::PoolOptions, sqlite::SqliteConnectOptions}; use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, ConnectOptions};
#[macro_use] #[macro_use]
extern crate rocket; extern crate rocket;
@@ -27,7 +24,7 @@ async fn rocket() -> _ {
.await .await
.unwrap(); .unwrap();
let rocket = rocket::build().manage(db.clone()); let rocket = rocket::build().manage(db);
#[cfg(feature = "rowing-tera")] #[cfg(feature = "rowing-tera")]
let rocket = tera::config(rocket); let rocket = tera::config(rocket);
@@ -35,11 +32,5 @@ async fn rocket() -> _ {
#[cfg(feature = "rest")] #[cfg(feature = "rest")]
let rocket = rest::config(rocket); let rocket = rest::config(rocket);
let config: Config = rocket
.figment()
.extract()
.expect("Config extraction failed");
scheduled::schedule(&db, &config);
rocket rocket
} }

View File

@@ -1,270 +0,0 @@
use std::ops::DerefMut;
use super::{
logbook::{Logbook, LogbookWithBoatAndRowers},
role::Role,
user::{ManageUserUser, User},
};
use chrono::{DateTime, Duration, Local, NaiveDateTime, TimeZone, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct Activity {
pub id: i64,
pub created_at: NaiveDateTime,
pub text: String,
pub relevant_for: String,
pub keep_until: Option<NaiveDateTime>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct ActivityWithDetails {
#[serde(flatten)]
pub(crate) activity: Activity,
keep_until_days: Option<i64>,
}
impl From<Activity> for ActivityWithDetails {
fn from(activity: Activity) -> Self {
let keep_until_days = activity.keep_until.map(|keep_until| {
let now = Utc::now().naive_utc();
let duration = keep_until.signed_duration_since(now);
duration.num_days()
});
Self {
keep_until_days,
activity,
}
}
}
// TODO: add `reason` as additional db field, to be able to query and show this to the users
pub enum Reason<'a> {
Auth(ReasonAuth<'a>),
Logbook(ReasonLogbook<'a>),
// `User` changed the data of `User`, explanation in `String`
UserDataChange(&'a ManageUserUser, &'a User, String),
// New Note for User
NewUserNote(&'a ManageUserUser, &'a User, String),
}
impl From<Reason<'_>> for ActivityBuilder {
fn from(value: Reason<'_>) -> Self {
match value {
Reason::Auth(auth) => auth.into(),
Reason::UserDataChange(changed_by, changed_user, explanation) => Self::new(&format!(
"{changed_by} hat die Daten von {changed_user} aktualisiert: {explanation}"
))
.user(changed_user),
Reason::NewUserNote(changed_by, user, explanation) => {
Self::new(&format!("({changed_by}) {explanation}")).user(user)
}
Reason::Logbook(logbook) => logbook.into(),
}
}
}
pub enum ReasonAuth<'a> {
// `User` tried to login with `String` as UserAgent
SuccLogin(&'a User, String),
// `User` tried to login which was already deleted
DeletedUserLogin(&'a User),
// `User` tried to login, supplied wrong PW
WrongPw(&'a User),
}
impl<'a> From<ReasonAuth<'a>> for Reason<'a> {
fn from(auth_reason: ReasonAuth<'a>) -> Self {
Reason::Auth(auth_reason)
}
}
impl From<ReasonAuth<'_>> for ActivityBuilder {
fn from(value: ReasonAuth<'_>) -> Self {
match value {
ReasonAuth::SuccLogin(user, agent) => {
Self::new(&format!("{user} hat sich eingeloggt (User-Agent: {agent})"))
.user(user)
.keep_until_days(7)
}
ReasonAuth::DeletedUserLogin(user) => Self::new(&format!(
"{user} wollte sich einloggen, klappte jedoch nicht weil der Account gelöscht wurde."
))
.user(user)
.keep_until_days(30),
ReasonAuth::WrongPw(user) => Self::new(&format!(
"User {user} wollte sich einloggen, hat jedoch das falsche Passwort angegeben."
))
.user(user)
.keep_until_days(7),
}
}
}
pub enum ReasonLogbook<'a> {
// `User` tried to login with `String` as UserAgent
BoardOrAdminDeleted(&'a User, &'a LogbookWithBoatAndRowers),
}
impl<'a> From<ReasonLogbook<'a>> for Reason<'a> {
fn from(logbook_reason: ReasonLogbook<'a>) -> Self {
Reason::Logbook(logbook_reason)
}
}
impl From<ReasonLogbook<'_>> for ActivityBuilder {
fn from(value: ReasonLogbook<'_>) -> Self {
match value {
ReasonLogbook::BoardOrAdminDeleted(user, logbook) => Self::new(&format!(
"{user} hat den Logbuch-Eintrag gelöscht: {logbook}"
))
.user(user)
.logbook(&logbook.logbook)
.keep_until_days(7),
}
}
}
pub struct ActivityBuilder {
text: String,
relevant_for: String,
keep_until: Option<NaiveDateTime>,
}
impl ActivityBuilder {
/// TODO: maybe make this private, and only allow specific acitivites defined in `Reason`
#[must_use]
pub fn new(text: &str) -> Self {
Self {
text: text.into(),
relevant_for: String::new(),
keep_until: None,
}
}
#[must_use]
pub fn user(self, user: &User) -> Self {
Self {
relevant_for: format!("{}user-{};", self.relevant_for, user.id),
..self
}
}
#[must_use]
pub fn role(self, role: &Role) -> Self {
Self {
relevant_for: format!("{}role-{};", self.relevant_for, role.id),
..self
}
}
#[must_use]
pub fn logbook(self, logbook: &Logbook) -> Self {
Self {
relevant_for: format!("{}logbook-{};", self.relevant_for, logbook.id),
..self
}
}
#[must_use]
pub fn keep_until_days(self, days: i64) -> Self {
let now = Utc::now().naive_utc();
Self {
keep_until: Some(now + Duration::days(days)),
..self
}
}
pub async fn save(self, db: &SqlitePool) {
Activity::create(db, &self.text, &self.relevant_for, self.keep_until).await;
}
pub async fn save_tx(self, db: &mut Transaction<'_, Sqlite>) {
Activity::create_with_tx(db, &self.text, &self.relevant_for, self.keep_until).await;
}
}
impl Activity {
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!(
Self,
"SELECT id, created_at, text, relevant_for, keep_until FROM activity WHERE id like ?",
id
)
.fetch_one(db)
.await
.ok()
}
pub(super) async fn create_with_tx(
db: &mut Transaction<'_, Sqlite>,
text: &str,
relevant_for: &str,
keep_until: Option<NaiveDateTime>,
) {
sqlx::query!(
"INSERT INTO activity(text, relevant_for, keep_until) VALUES (?, ?, ?)",
text,
relevant_for,
keep_until
)
.execute(db.deref_mut())
.await
.unwrap();
}
pub(super) async fn create(
db: &SqlitePool,
text: &str,
relevant_for: &str,
keep_until: Option<NaiveDateTime>,
) {
let mut tx = db.begin().await.unwrap();
Self::create_with_tx(&mut tx, text, relevant_for, keep_until).await;
tx.commit().await.unwrap();
}
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<Activity> {
let user_str = format!("user-{};", user.id);
sqlx::query_as!(
Self,
"
SELECT id, created_at, text, relevant_for, keep_until FROM activity
WHERE
relevant_for like CONCAT('%', ?, '%')
ORDER BY created_at DESC;
",
user_str
)
.fetch_all(db)
.await
.unwrap()
}
async fn last(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(
Self,
"
SELECT id, created_at, text, relevant_for, keep_until FROM activity
ORDER BY id DESC
LIMIT 1000
"
)
.fetch_all(db)
.await
.unwrap()
}
pub async fn show(db: &SqlitePool) -> String {
let mut ret = String::new();
for log in Self::last(db).await {
let utc_time: DateTime<Utc> = Utc::from_utc_datetime(&Utc, &log.created_at);
let local_time = utc_time.with_timezone(&Local);
ret.push_str(&format!("- {local_time}: {}\n", log.text));
}
ret
}
}

View File

@@ -1,17 +1,12 @@
use std::ops::DerefMut; use std::ops::DerefMut;
use chrono::NaiveDateTime;
use rocket::serde::{Deserialize, Serialize}; use rocket::serde::{Deserialize, Serialize};
use rocket::FromForm; use rocket::FromForm;
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use crate::model::boathouse::Boathouse;
use super::location::Location;
use super::user::User; use super::user::User;
use std::fmt::Display;
#[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)] #[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct Boat { pub struct Boat {
pub id: i64, pub id: i64,
pub name: String, pub name: String,
@@ -22,25 +17,11 @@ pub struct Boat {
pub boatbuilder: Option<String>, pub boatbuilder: Option<String>,
pub default_destination: Option<String>, pub default_destination: Option<String>,
#[serde(default = "bool::default")] #[serde(default = "bool::default")]
pub convert_handoperated_possible: bool, default_shipmaster_only_steering: bool,
#[serde(default = "bool::default")]
pub default_shipmaster_only_steering: bool,
#[serde(default = "bool::default")] #[serde(default = "bool::default")]
skull: bool, skull: bool,
#[serde(default = "bool::default")] #[serde(default = "bool::default")]
pub external: bool, external: bool,
pub deleted: bool,
}
impl Display for Boat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let private_or_club_boat = if self.owner.is_some() {
"privat"
} else {
"Vereinsboot"
};
write!(f, "{} ({}, {private_or_club_boat})", self.name, self.cat())
}
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@@ -54,11 +35,9 @@ pub enum BoatDamage {
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct BoatWithDetails { pub struct BoatWithDetails {
#[serde(flatten)] #[serde(flatten)]
pub(crate) boat: Boat, boat: Boat,
damage: BoatDamage, damage: BoatDamage,
on_water: bool, on_water: bool,
reserved_today: bool,
cat: String,
} }
#[derive(FromForm)] #[derive(FromForm)]
@@ -68,7 +47,6 @@ pub struct BoatToAdd<'r> {
pub year_built: Option<i64>, pub year_built: Option<i64>,
pub boatbuilder: Option<&'r str>, pub boatbuilder: Option<&'r str>,
pub default_shipmaster_only_steering: bool, pub default_shipmaster_only_steering: bool,
pub convert_handoperated_possible: bool,
pub default_destination: Option<&'r str>, pub default_destination: Option<&'r str>,
pub skull: bool, pub skull: bool,
pub external: bool, pub external: bool,
@@ -85,7 +63,6 @@ pub struct BoatToUpdate<'r> {
pub default_shipmaster_only_steering: bool, pub default_shipmaster_only_steering: bool,
pub default_destination: Option<&'r str>, pub default_destination: Option<&'r str>,
pub skull: bool, pub skull: bool,
pub convert_handoperated_possible: bool,
pub external: bool, pub external: bool,
pub location_id: i64, pub location_id: i64,
pub owner: Option<i64>, pub owner: Option<i64>,
@@ -93,30 +70,35 @@ pub struct BoatToUpdate<'r> {
impl Boat { impl Boat {
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> { pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE id like ?", id) sqlx::query_as!(Self, "SELECT * FROM boat WHERE id like ?", id)
.fetch_one(db) .fetch_one(db)
.await .await
.ok() .ok()
} }
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> { pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE id like ?", id) sqlx::query_as!(Self, "SELECT * FROM boat WHERE id like ?", id)
.fetch_one(db.deref_mut()) .fetch_one(db.deref_mut())
.await .await
.ok() .ok()
} }
pub async fn find_by_name(db: &SqlitePool, name: String) -> Option<Self> { pub async fn find_by_name(db: &SqlitePool, name: String) -> Option<Self> {
sqlx::query_as!(Self, "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, convert_handoperated_possible, default_destination, skull, external, deleted FROM boat WHERE name like ?", name) sqlx::query_as!(Self, "SELECT * FROM boat WHERE name like ?", name)
.fetch_one(db) .fetch_one(db)
.await .await
.ok() .ok()
} }
pub async fn shipmaster_allowed(&self, db: &SqlitePool, user: &User) -> bool { pub async fn shipmaster_allowed(&self, db: &SqlitePool, user: &User) -> bool {
let mut tx = db.begin().await.unwrap(); if let Some(owner_id) = self.owner {
let ret = self.shipmaster_allowed_tx(&mut tx, user).await; return owner_id == user.id;
tx.commit().await.unwrap(); }
ret
if self.amount_seats == 1 {
return true;
}
user.has_role(db, "cox").await
} }
pub async fn shipmaster_allowed_tx( pub async fn shipmaster_allowed_tx(
@@ -124,32 +106,15 @@ impl Boat {
db: &mut Transaction<'_, Sqlite>, db: &mut Transaction<'_, Sqlite>,
user: &User, user: &User,
) -> bool { ) -> bool {
if user.has_role_tx(db, "admin").await {
return true;
}
if let Some(owner_id) = self.owner { if let Some(owner_id) = self.owner {
return owner_id == user.id; return owner_id == user.id;
} }
if user.has_role_tx(db, "Rennrudern").await {
let ottensheim = Location::find_by_name_tx(db, "Ottensheim".into())
.await
.unwrap();
if self.location_id == ottensheim.id {
return true;
}
}
if self.name == "Externes Boot" {
return true;
}
if self.amount_seats == 1 { if self.amount_seats == 1 {
return true; return true;
} }
user.allowed_to_steer_tx(db).await user.has_role_tx(db, "cox").await
} }
pub async fn is_locked(&self, db: &SqlitePool) -> bool { pub async fn is_locked(&self, db: &SqlitePool) -> bool {
@@ -160,20 +125,6 @@ impl Boat {
sqlx::query!("SELECT * FROM boat_damage WHERE boat_id=? AND lock_boat=false AND user_id_verified is null", self.id).fetch_optional(db).await.unwrap().is_some() sqlx::query!("SELECT * FROM boat_damage WHERE boat_id=? AND lock_boat=false AND user_id_verified is null", self.id).fetch_optional(db).await.unwrap().is_some()
} }
pub async fn reserved_today(&self, db: &SqlitePool) -> bool {
sqlx::query!(
"SELECT *
FROM boat_reservation
WHERE boat_id =?
AND date('now') BETWEEN start_date AND end_date;",
self.id
)
.fetch_optional(db)
.await
.unwrap()
.is_some()
}
pub async fn on_water(&self, db: &SqlitePool) -> bool { pub async fn on_water(&self, db: &SqlitePool) -> bool {
sqlx::query!( sqlx::query!(
"SELECT * FROM logbook WHERE boat_id=? AND arrival is null", "SELECT * FROM logbook WHERE boat_id=? AND arrival is null",
@@ -185,18 +136,6 @@ AND date('now') BETWEEN start_date AND end_date;",
.is_some() .is_some()
} }
pub(crate) fn cat(&self) -> String {
if self.external {
"Vereinsfremde Boote".to_string()
} else if self.default_shipmaster_only_steering {
format!("{}+", self.amount_seats - 1)
} else if self.skull {
format!("{}x", self.amount_seats)
} else {
format!("{}-", self.amount_seats)
}
}
async fn boats_to_details(db: &SqlitePool, boats: Vec<Boat>) -> Vec<BoatWithDetails> { async fn boats_to_details(db: &SqlitePool, boats: Vec<Boat>) -> Vec<BoatWithDetails> {
let mut res = Vec::new(); let mut res = Vec::new();
for boat in boats { for boat in boats {
@@ -207,14 +146,10 @@ AND date('now') BETWEEN start_date AND end_date;",
if boat.is_locked(db).await { if boat.is_locked(db).await {
damage = BoatDamage::Locked; damage = BoatDamage::Locked;
} }
let cat = boat.cat();
res.push(BoatWithDetails { res.push(BoatWithDetails {
damage, damage,
on_water: boat.on_water(db).await, on_water: boat.on_water(db).await,
reserved_today: boat.reserved_today(db).await,
boat, boat,
cat,
}); });
} }
res res
@@ -224,9 +159,8 @@ AND date('now') BETWEEN start_date AND end_date;",
let boats = sqlx::query_as!( let boats = sqlx::query_as!(
Boat, Boat,
" "
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external
FROM boat FROM boat
WHERE deleted=false
ORDER BY amount_seats DESC ORDER BY amount_seats DESC
" "
) )
@@ -237,62 +171,51 @@ ORDER BY amount_seats DESC
Self::boats_to_details(db, boats).await Self::boats_to_details(db, boats).await
} }
pub async fn all_for_boatshouse(db: &SqlitePool) -> Vec<BoatWithDetails> { pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<BoatWithDetails> {
let boats = sqlx::query_as!( if user.has_role(db, "admin").await {
return Self::all(db).await;
}
let boats = if user.has_role(db, "cox").await {
sqlx::query_as!(
Boat, Boat,
" "
SELECT SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external
b.id, FROM boat
b.name, WHERE owner is null or owner = ?
b.amount_seats, ORDER BY amount_seats DESC
b.location_id, ",
b.owner, user.id
b.year_built,
b.boatbuilder,
b.default_shipmaster_only_steering,
b.default_destination,
b.skull,
b.external,
b.deleted,
b.convert_handoperated_possible
FROM
boat AS b
WHERE
b.external = false
AND b.location_id = (SELECT id FROM location WHERE name = 'Linz')
AND b.deleted = false
ORDER BY
b.name DESC;
"
) )
.fetch_all(db) .fetch_all(db)
.await .await
.unwrap(); //TODO: fixme .unwrap() //TODO: fixme
} else {
sqlx::query_as!(
Boat,
"
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external
FROM boat
WHERE owner = ? OR (owner is null and amount_seats = 1)
ORDER BY amount_seats DESC
",
user.id
)
.fetch_all(db)
.await
.unwrap() //TODO: fixme
};
Self::boats_to_details(db, boats).await Self::boats_to_details(db, boats).await
} }
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<BoatWithDetails> {
let all_boats = Self::all(db).await;
let mut filtered_boats = Vec::new();
for boat in all_boats {
if boat.boat.shipmaster_allowed(db, user).await {
filtered_boats.push(boat);
}
}
filtered_boats
}
pub async fn all_at_location(db: &SqlitePool, location: String) -> Vec<BoatWithDetails> { pub async fn all_at_location(db: &SqlitePool, location: String) -> Vec<BoatWithDetails> {
let boats = sqlx::query_as!( let boats = sqlx::query_as!(
Boat, Boat,
" "
SELECT boat.id, boat.name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible SELECT boat.id, boat.name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external
FROM boat FROM boat
INNER JOIN location ON boat.location_id = location.id INNER JOIN location ON boat.location_id = location.id
WHERE location.name=? AND deleted = 0 WHERE location.name=?
ORDER BY amount_seats DESC ORDER BY amount_seats DESC
", ",
location location
@@ -306,7 +229,7 @@ ORDER BY amount_seats DESC
pub async fn create(db: &SqlitePool, boat: BoatToAdd<'_>) -> Result<(), String> { pub async fn create(db: &SqlitePool, boat: BoatToAdd<'_>) -> Result<(), String> {
sqlx::query!( sqlx::query!(
"INSERT INTO boat(name, amount_seats, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, location_id, owner, convert_handoperated_possible) VALUES (?,?,?,?,?,?,?,?,?,?,?)", "INSERT INTO boat(name, amount_seats, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, location_id, owner) VALUES (?,?,?,?,?,?,?,?,?,?)",
boat.name, boat.name,
boat.amount_seats, boat.amount_seats,
boat.year_built, boat.year_built,
@@ -316,8 +239,7 @@ ORDER BY amount_seats DESC
boat.skull, boat.skull,
boat.external, boat.external,
boat.location_id, boat.location_id,
boat.owner, boat.owner
boat.convert_handoperated_possible
) )
.execute(db) .execute(db)
.await.map_err(|e| e.to_string())?; .await.map_err(|e| e.to_string())?;
@@ -326,7 +248,7 @@ ORDER BY amount_seats DESC
pub async fn update(&self, db: &SqlitePool, boat: BoatToUpdate<'_>) -> Result<(), String> { pub async fn update(&self, db: &SqlitePool, boat: BoatToUpdate<'_>) -> Result<(), String> {
sqlx::query!( sqlx::query!(
"UPDATE boat SET name=?, amount_seats=?, year_built=?, boatbuilder=?, default_shipmaster_only_steering=?, default_destination=?, skull=?, external=?, location_id=?, owner=?, convert_handoperated_possible=? WHERE id=?", "UPDATE boat SET name=?, amount_seats=?, year_built=?, boatbuilder=?, default_shipmaster_only_steering=?, default_destination=?, skull=?, external=?, location_id=?, owner=? WHERE id=?",
boat.name, boat.name,
boat.amount_seats, boat.amount_seats,
boat.year_built, boat.year_built,
@@ -337,7 +259,6 @@ ORDER BY amount_seats DESC
boat.external, boat.external,
boat.location_id, boat.location_id,
boat.owner, boat.owner,
boat.convert_handoperated_possible,
self.id self.id
) )
.execute(db) .execute(db)
@@ -345,64 +266,12 @@ ORDER BY amount_seats DESC
Ok(()) Ok(())
} }
pub async fn owner(&self, db: &SqlitePool) -> Option<User> {
if let Some(owner_id) = self.owner {
Some(User::find_by_id(db, owner_id as i32).await.unwrap())
} else {
None
}
}
pub async fn delete(&self, db: &SqlitePool) { pub async fn delete(&self, db: &SqlitePool) {
sqlx::query!("UPDATE boat SET deleted=1 WHERE id=?", self.id) sqlx::query!("DELETE FROM boat WHERE id=?", self.id)
.execute(db) .execute(db)
.await .await
.unwrap(); //Okay, because we can only create a Boat of a valid id .unwrap(); //Okay, because we can only create a Boat of a valid id
} }
pub async fn boathouse(&self, db: &SqlitePool) -> Option<Boathouse> {
sqlx::query_as!(
Boathouse,
"SELECT * FROM boathouse WHERE boat_id like ?",
self.id
)
.fetch_one(db)
.await
.ok()
}
pub async fn on_water_between(
&self,
db: &mut Transaction<'_, Sqlite>,
dep: NaiveDateTime,
arr: NaiveDateTime,
) -> bool {
let dep = dep.format("%Y-%m-%dT%H:%M").to_string();
let arr = arr.format("%Y-%m-%dT%H:%M").to_string();
sqlx::query!(
"SELECT COUNT(*) AS overlap_count
FROM logbook
WHERE boat_id = ?
AND (
(departure <= ? AND arrival >= ?) -- Existing entry covers the entire new period
OR (departure >= ? AND departure < ?) -- Existing entry starts during the new period
OR (arrival > ? AND arrival <= ?) -- Existing entry ends during the new period
);",
self.id,
arr,
arr,
dep,
dep,
dep,
arr
)
.fetch_one(db.deref_mut())
.await
.unwrap()
.overlap_count
> 0
}
} }
#[cfg(test)] #[cfg(test)]
@@ -450,7 +319,6 @@ mod test {
year_built: None, year_built: None,
boatbuilder: "Best Boatbuilder".into(), boatbuilder: "Best Boatbuilder".into(),
default_shipmaster_only_steering: true, default_shipmaster_only_steering: true,
convert_handoperated_possible: false,
skull: true, skull: true,
external: false, external: false,
location_id: Some(1), location_id: Some(1),
@@ -476,7 +344,6 @@ mod test {
year_built: None, year_built: None,
boatbuilder: "Best Boatbuilder".into(), boatbuilder: "Best Boatbuilder".into(),
default_shipmaster_only_steering: true, default_shipmaster_only_steering: true,
convert_handoperated_possible: false,
skull: true, skull: true,
external: false, external: false,
location_id: Some(1), location_id: Some(1),
@@ -579,7 +446,6 @@ mod test {
year_built: None, year_built: None,
boatbuilder: None, boatbuilder: None,
default_shipmaster_only_steering: false, default_shipmaster_only_steering: false,
convert_handoperated_possible: false,
skull: true, skull: true,
external: false, external: false,
location_id: 1, location_id: 1,
@@ -603,7 +469,6 @@ mod test {
year_built: None, year_built: None,
boatbuilder: None, boatbuilder: None,
default_shipmaster_only_steering: false, default_shipmaster_only_steering: false,
convert_handoperated_possible: false,
skull: true, skull: true,
external: false, external: false,
location_id: 999, location_id: 999,

View File

@@ -1,12 +1,10 @@
use crate::model::{boat::Boat, user::User}; use crate::model::{boat::Boat, user::User};
use chrono::NaiveDateTime; use chrono::NaiveDateTime;
use rocket::FromForm;
use rocket::serde::{Deserialize, Serialize}; use rocket::serde::{Deserialize, Serialize};
use rocket::FromForm;
use sqlx::{FromRow, SqlitePool}; use sqlx::{FromRow, SqlitePool};
use super::log::Log; use super::log::Log;
use super::notification::Notification;
use super::role::Role;
#[derive(FromRow, Debug, Serialize, Deserialize)] #[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct BoatDamage { pub struct BoatDamage {
@@ -73,10 +71,6 @@ impl BoatDamage {
" "
SELECT id, boat_id, desc, user_id_created, created_at, user_id_fixed, fixed_at, user_id_verified, verified_at, lock_boat SELECT id, boat_id, desc, user_id_created, created_at, user_id_fixed, fixed_at, user_id_verified, verified_at, lock_boat
FROM boat_damage FROM boat_damage
WHERE (
verified_at IS NULL
OR verified_at >= datetime('now', '-30 days')
)
ORDER BY created_at DESC ORDER BY created_at DESC
" "
) )
@@ -119,10 +113,6 @@ ORDER BY created_at DESC
pub async fn create(db: &SqlitePool, boatdamage: BoatDamageToAdd<'_>) -> Result<(), String> { pub async fn create(db: &SqlitePool, boatdamage: BoatDamageToAdd<'_>) -> Result<(), String> {
Log::create(db, format!("New boat damage: {boatdamage:?}")).await; Log::create(db, format!("New boat damage: {boatdamage:?}")).await;
let Some(boat) = Boat::find_by_id(db, boatdamage.boat_id as i32).await else {
return Err("Boot gibt's ned".into());
};
let was_unusable_before = boat.is_locked(db).await;
sqlx::query!( sqlx::query!(
"INSERT INTO boat_damage(boat_id, desc, user_id_created, lock_boat) VALUES (?,?,?, ?)", "INSERT INTO boat_damage(boat_id, desc, user_id_created, lock_boat) VALUES (?,?,?, ?)",
@@ -134,217 +124,63 @@ ORDER BY created_at DESC
.execute(db) .execute(db)
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
if !was_unusable_before && boat.is_locked(db).await {
Notification::create_for_steering_people(db, &format!("Liebe Steuerberechtigte, bitte beachten, dass {} bis auf weiteres aufgrund von Reparaturarbeiten gesperrt ist.", boat.name), "Boot gesperrt", None, None).await;
}
let technicals =
User::all_with_role(db, &Role::find_by_name(db, "tech").await.unwrap()).await;
for technical in technicals {
if technical.id as i32 != boatdamage.user_id_created {
Notification::create(
db,
&technical,
&format!(
"{} hat einen neuen Bootschaden für Boot '{}' angelegt: {}",
User::find_by_id(db, boatdamage.user_id_created)
.await
.unwrap()
.name,
boat.name,
boatdamage.desc
),
"Neuer Bootsschaden angelegt",
None,
None,
)
.await;
}
}
Notification::create(
db,
&User::find_by_id(db, boatdamage.user_id_created)
.await
.unwrap(),
&format!(
"Du hat einen neuen Bootschaden für Boot '{}' angelegt: {}",
Boat::find_by_id(db, boatdamage.boat_id as i32)
.await
.unwrap()
.name,
boatdamage.desc
),
"Neuer Bootsschaden angelegt",
None,
None,
)
.await;
Ok(()) Ok(())
} }
pub async fn fixed( pub async fn fixed(&self, db: &SqlitePool, boat: BoatDamageFixed<'_>) -> Result<(), String> {
&self, Log::create(db, format!("Fixed boat damage: {boat:?}")).await;
db: &SqlitePool,
boat_damage: BoatDamageFixed<'_>,
) -> Result<(), String> {
Log::create(db, format!("Fixed boat damage: {boat_damage:?}")).await;
let boat = Boat::find_by_id(db, self.boat_id as i32).await.unwrap();
sqlx::query!( sqlx::query!(
"UPDATE boat_damage SET desc=?, user_id_fixed=?, fixed_at=CURRENT_TIMESTAMP WHERE id=?", "UPDATE boat_damage SET desc=?, user_id_fixed=?, fixed_at=CURRENT_TIMESTAMP WHERE id=?",
boat_damage.desc, boat.desc,
boat_damage.user_id_fixed, boat.user_id_fixed,
self.id self.id
) )
.execute(db) .execute(db)
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
let user = User::find_by_id(db, boat_damage.user_id_fixed) let user = User::find_by_id(db, boat.user_id_fixed).await.unwrap();
.await
.unwrap();
if user.has_role(db, "tech").await { if user.has_role(db, "tech").await {
return self return self
.verified( .verified(
db, db,
BoatDamageVerified { BoatDamageVerified {
desc: boat_damage.desc, desc: boat.desc,
user_id_verified: user.id as i32, user_id_verified: user.id as i32,
}, },
) )
.await; .await;
} }
let technicals =
User::all_with_role(db, &Role::find_by_name(db, "tech").await.unwrap()).await;
for technical in technicals {
if technical.id as i32 != boat_damage.user_id_fixed {
Notification::create(
db,
&technical,
&format!(
"{} hat den Bootschaden '{}' beim Boot '{}' repariert. Könntest du das bei Gelegenheit verifizieren?",
User::find_by_id(db, boat_damage.user_id_fixed)
.await
.unwrap()
.name,
boat_damage.desc,
boat.name,
),
"Bootsschaden repariert",
None,None
)
.await;
}
}
if boat_damage.user_id_fixed != self.user_id_created as i32 {
let user_fixed = User::find_by_id(db, boat_damage.user_id_fixed)
.await
.unwrap();
let user_created = User::find_by_id(db, self.user_id_created as i32)
.await
.unwrap();
// Boatdamage is also directly verified, if a tech has repaired it. We don't want to
// send 2 notifications.
if !user_fixed.has_role(db, "tech").await {
Notification::create(
db,
&user_created,
&format!(
"{} hat den von dir eingetragenen Bootschaden '{}' beim Boot '{}' repariert. Dieser muss nun noch von unseren Bootswarten bestätigt werden.",
user_fixed.name,
boat_damage.desc, boat.name,
),
"Bootsschaden repariert",
None,None
)
.await;
}
}
Ok(()) Ok(())
} }
pub async fn verified( pub async fn verified(
&self, &self,
db: &SqlitePool, db: &SqlitePool,
boat_form: BoatDamageVerified<'_>, boat: BoatDamageVerified<'_>,
) -> Result<(), String> { ) -> Result<(), String> {
if let Some(verifier) = User::find_by_id(db, boat_form.user_id_verified).await { if let Some(verifier) = User::find_by_id(db, boat.user_id_verified).await {
if !verifier.has_role(db, "tech").await { if !verifier.has_role(db, "tech").await {
Log::create(db, format!("User {verifier:?} tried to verify boat {boat_form:?}. The user is no tech. Manually craftted request?")).await; Log::create(db, format!("User {verifier:?} tried to verify boat {boat:?}. The user is no tech. Manually craftted request?")).await;
return Err("You are not allowed to verify the boat!".into()); return Err("You are not allowed to verify the boat!".into());
} }
} else { } else {
Log::create(db, format!("Someone tried to verify the boat {boat_form:?} with user_id={} which does not exist. Manually craftted request?", boat_form.user_id_verified)).await; Log::create(db, format!("Someone tried to verify the boat {boat:?} with user_id={} which does not exist. Manually craftted request?", boat.user_id_verified)).await;
return Err("Could not find user".into()); return Err("Could not find user".into());
} }
let Some(boat) = Boat::find_by_id(db, self.boat_id as i32).await else { Log::create(db, format!("Verified boat damage: {boat:?}")).await;
return Err("Boot gibt's ned".into());
};
let was_unusable_before = boat.is_locked(db).await;
Log::create(db, format!("Verified boat damage: {boat_form:?}")).await;
sqlx::query!( sqlx::query!(
"UPDATE boat_damage SET desc=?, user_id_verified=?, verified_at=CURRENT_TIMESTAMP WHERE id=?", "UPDATE boat_damage SET desc=?, user_id_verified=?, verified_at=CURRENT_TIMESTAMP WHERE id=?",
boat_form.desc, boat.desc,
boat_form.user_id_verified, boat.user_id_verified,
self.id self.id
) )
.execute(db) .execute(db)
.await.map_err(|e| e.to_string())?; .await.map_err(|e| e.to_string())?;
if boat_form.user_id_verified != self.user_id_created as i32 {
let user_verified = User::find_by_id(db, boat_form.user_id_verified)
.await
.unwrap();
let user_created = User::find_by_id(db, self.user_id_created as i32)
.await
.unwrap();
if user_verified.id == self.user_id_fixed.unwrap() {
Notification::create(
db,
&user_created,
&format!(
"{} hat den von dir eingetragenen Bootschaden '{}' beim Boot '{}' repariert und verifiziert.",
user_verified.name,
self.desc, boat.name,
),
"Bootsschaden repariert & verifiziert",
None,
None
)
.await;
} else {
Notification::create(
db,
&user_created,
&format!(
"{} hat verifiziert, dass der von dir eingetragenen Bootschaden '{}' beim Boot '{}' korrekt repariert wurde.",
user_verified.name,
self.desc, boat.name,
),
"Bootsschaden verifiziert",
None,
None
).await;
}
}
if was_unusable_before && !boat.is_locked(db).await {
let cox = Role::find_by_name(db, "cox").await.unwrap();
Notification::create_for_role(db, &cox, &format!("Liebe Steuerberechtigte, {} wurde repariert und freut sich ab sofort wieder gerudert zu werden :-)", boat.name), "Boot repariert", None, None).await;
}
Ok(()) Ok(())
} }
} }

View File

@@ -1,172 +0,0 @@
use rocket::serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool};
use crate::{
model::{log::Log, user::AllowedToUpdateBoathouse},
tera::board::boathouse::FormBoathouseToAdd,
};
use super::boat::Boat;
#[derive(Debug, Serialize, Deserialize)]
pub struct BoathousePlace {
boat: Boat,
boathouse_id: i64,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BoathouseRack {
boats: [Option<BoathousePlace>; 12],
}
impl BoathouseRack {
fn new() -> Self {
let boats = [
None, None, None, None, None, None, None, None, None, None, None, None,
];
Self { boats }
}
async fn add(&mut self, db: &SqlitePool, boathouse: Boathouse) {
self.boats[boathouse.level as usize] = Some(BoathousePlace {
boat: Boat::find_by_id(db, boathouse.boat_id as i32)
.await
.unwrap(),
boathouse_id: boathouse.id,
});
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BoathouseSide {
mountain: BoathouseRack,
water: BoathouseRack,
}
impl BoathouseSide {
fn new() -> Self {
Self {
mountain: BoathouseRack::new(),
water: BoathouseRack::new(),
}
}
async fn add(&mut self, db: &SqlitePool, boathouse: Boathouse) {
match boathouse.side.as_str() {
"mountain" => self.mountain.add(db, boathouse).await,
"water" => self.water.add(db, boathouse).await,
_ => panic!("db constraint failed"),
}
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct BoathouseAisles {
mountain: BoathouseSide,
middle: BoathouseSide,
water: BoathouseSide,
}
impl BoathouseAisles {
fn new() -> Self {
Self {
mountain: BoathouseSide::new(),
middle: BoathouseSide::new(),
water: BoathouseSide::new(),
}
}
async fn add(&mut self, db: &SqlitePool, boathouse: Boathouse) {
match boathouse.aisle.as_str() {
"water" => self.water.add(db, boathouse).await,
"middle" => self.middle.add(db, boathouse).await,
"mountain" => self.mountain.add(db, boathouse).await,
_ => panic!("db constraint failed"),
};
}
pub async fn from(db: &SqlitePool, boathouses: Vec<Boathouse>) -> Self {
let mut ret = BoathouseAisles::new();
for boathouse in boathouses {
ret.add(db, boathouse).await;
}
ret
}
}
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct Boathouse {
pub id: i64,
pub boat_id: i64,
pub aisle: String,
pub side: String,
pub level: i64,
}
impl Boathouse {
pub async fn get(db: &SqlitePool) -> BoathouseAisles {
let boathouses = sqlx::query_as!(
Boathouse,
"SELECT id, boat_id, aisle, side, level FROM boathouse"
)
.fetch_all(db)
.await
.unwrap(); //TODO: fixme
BoathouseAisles::from(db, boathouses).await
}
pub async fn create(
db: &SqlitePool,
changed_by: &AllowedToUpdateBoathouse,
data: FormBoathouseToAdd,
) -> Result<(), String> {
sqlx::query!(
"INSERT INTO boathouse(boat_id, aisle, side, level) VALUES (?,?,?,?)",
data.boat_id,
data.aisle,
data.side,
data.level
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
let boat = Boat::find_by_id(db, data.boat_id).await.unwrap();
Log::create(
db,
format!(
"{changed_by} hat das Boot {boat} auf den Gang {}, Seite {}, und Höhe {} 'gelegt'.",
data.aisle, data.side, data.level
),
)
.await;
Ok(())
}
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!(Self, "SELECT * FROM boathouse WHERE id like ?", id)
.fetch_one(db)
.await
.ok()
}
pub async fn delete(&self, db: &SqlitePool, changed_by: &AllowedToUpdateBoathouse) {
sqlx::query!("DELETE FROM boathouse WHERE id=?", self.id)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a Boat of a valid id
let boat = Boat::find_by_id(db, self.boat_id as i32).await.unwrap();
Log::create(
db,
format!(
"{changed_by} hat das Boot {boat} von Gang {}, Seite {}, und Höhe {} gelöscht.",
self.aisle, self.side, self.level
),
)
.await;
}
}

View File

@@ -1,272 +0,0 @@
use std::collections::HashMap;
use crate::model::{boat::Boat, user::User};
use crate::tera::boatreservation::ReservationEditForm;
use chrono::NaiveDate;
use chrono::NaiveDateTime;
use rocket::serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool};
use super::log::Log;
use super::notification::Notification;
use super::role::Role;
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct BoatReservation {
pub id: i64,
pub boat_id: i64,
pub start_date: NaiveDate,
pub end_date: NaiveDate,
pub time_desc: String,
pub usage: String,
pub user_id_applicant: i64,
pub user_id_confirmation: Option<i64>,
pub created_at: NaiveDateTime,
}
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct BoatReservationWithDetails {
#[serde(flatten)]
reservation: BoatReservation,
boat: Boat,
user_applicant: User,
user_confirmation: Option<User>,
}
#[derive(Debug)]
pub struct BoatReservationToAdd<'r> {
pub boat: &'r Boat,
pub start_date: NaiveDate,
pub end_date: NaiveDate,
pub time_desc: &'r str,
pub usage: &'r str,
pub user_applicant: &'r User,
}
impl BoatReservation {
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!(
Self,
"SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
FROM boat_reservation
WHERE id like ?",
id
)
.fetch_one(db)
.await
.ok()
}
pub async fn for_day(db: &SqlitePool, day: NaiveDate) -> Vec<BoatReservationWithDetails> {
let boatreservations = sqlx::query_as!(
Self,
"
SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
FROM boat_reservation
WHERE end_date >= ? AND start_date <= ?
", day, day
)
.fetch_all(db)
.await
.unwrap(); //TODO: fixme
let mut res = Vec::new();
for reservation in boatreservations {
let user_confirmation = match reservation.user_id_confirmation {
Some(id) => {
let user = User::find_by_id(db, id as i32).await;
Some(user.unwrap())
}
None => None,
};
let user_applicant = User::find_by_id(db, reservation.user_id_applicant as i32)
.await
.unwrap();
let boat = Boat::find_by_id(db, reservation.boat_id as i32)
.await
.unwrap();
res.push(BoatReservationWithDetails {
reservation,
boat,
user_applicant,
user_confirmation,
});
}
res
}
pub async fn all_future(db: &SqlitePool) -> Vec<BoatReservationWithDetails> {
let boatreservations = sqlx::query_as!(
Self,
"
SELECT id, boat_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
FROM boat_reservation
WHERE end_date >= CURRENT_DATE ORDER BY end_date
"
)
.fetch_all(db)
.await
.unwrap(); //TODO: fixme
let mut res = Vec::new();
for reservation in boatreservations {
let user_confirmation = match reservation.user_id_confirmation {
Some(id) => {
let user = User::find_by_id(db, id as i32).await;
Some(user.unwrap())
}
None => None,
};
let user_applicant = User::find_by_id(db, reservation.user_id_applicant as i32)
.await
.unwrap();
let boat = Boat::find_by_id(db, reservation.boat_id as i32)
.await
.unwrap();
res.push(BoatReservationWithDetails {
reservation,
boat,
user_applicant,
user_confirmation,
});
}
res
}
pub fn with_groups(
reservations: Vec<BoatReservationWithDetails>,
) -> HashMap<String, Vec<BoatReservationWithDetails>> {
let mut grouped_reservations: HashMap<String, Vec<BoatReservationWithDetails>> =
HashMap::new();
for reservation in reservations {
let key = format!(
"{}-{}-{}-{}-{}",
reservation.reservation.start_date,
reservation.reservation.end_date,
reservation.reservation.time_desc,
reservation.reservation.usage,
reservation.user_applicant.name
);
grouped_reservations
.entry(key)
.or_default()
.push(reservation);
}
grouped_reservations
}
pub async fn all_future_with_groups(
db: &SqlitePool,
) -> HashMap<String, Vec<BoatReservationWithDetails>> {
let reservations = Self::all_future(db).await;
Self::with_groups(reservations)
}
pub async fn create(
db: &SqlitePool,
boatreservation: BoatReservationToAdd<'_>,
) -> Result<(), String> {
if Self::boat_reserved_between_dates(
db,
boatreservation.boat,
&boatreservation.start_date,
&boatreservation.end_date,
)
.await
{
return Err("Boot in diesem Zeitraum bereits reserviert.".into());
}
Log::create(db, format!("New boat reservation: {boatreservation:?}")).await;
sqlx::query!(
"INSERT INTO boat_reservation(boat_id, start_date, end_date, time_desc, usage, user_id_applicant) VALUES (?,?,?,?,?,?)",
boatreservation.boat.id,
boatreservation.start_date,
boatreservation.end_date,
boatreservation.time_desc,
boatreservation.usage,
boatreservation.user_applicant.id,
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
let board =
User::all_with_role(db, &Role::find_by_name(db, "Vorstand").await.unwrap()).await;
for user in board {
let date = if boatreservation.start_date == boatreservation.end_date {
format!("am {}", boatreservation.start_date)
} else {
format!(
"von {} bis {}",
boatreservation.start_date, boatreservation.end_date
)
};
Notification::create(
db,
&user,
&format!(
"{} hat eine neue Bootsreservierung für Boot '{}' {} angelegt. Zeit: {}; Zweck: {}",
boatreservation.user_applicant.name,
boatreservation.boat.name,
date,
boatreservation.time_desc,
boatreservation.usage
),
"Neue Bootsreservierung",
None,None
)
.await;
}
Ok(())
}
pub async fn boat_reserved_between_dates(
db: &SqlitePool,
boat: &Boat,
start_date: &NaiveDate,
end_date: &NaiveDate,
) -> bool {
sqlx::query!(
"SELECT COUNT(*) AS reservation_count
FROM boat_reservation
WHERE boat_id = ?
AND start_date <= ? AND end_date >= ?;",
boat.id,
end_date,
start_date
)
.fetch_one(db)
.await
.unwrap()
.reservation_count
> 0
}
pub async fn update(&self, db: &SqlitePool, data: ReservationEditForm) {
let time_desc = data.time_desc.trim();
let usage = data.usage.trim();
sqlx::query!(
"UPDATE boat_reservation SET time_desc = ?, usage = ? where id = ?",
time_desc,
usage,
self.id
)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
}
pub async fn delete(&self, db: &SqlitePool) {
sqlx::query!("DELETE FROM boat_reservation WHERE id=?", self.id)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a Boat of a valid id
}
}

View File

@@ -1,33 +0,0 @@
use serde::Serialize;
use sqlx::{FromRow, SqlitePool};
#[derive(FromRow, Serialize, Clone, Debug)]
pub struct Distance {
pub id: i64,
pub destination: String,
pub distance_in_km: i64,
}
impl Distance {
/// Return all default `distance`s, ordered by usage in logbook entries
pub async fn all(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(
Self,
"SELECT
d.id,
d.destination,
d.distance_in_km
FROM
distance d
LEFT JOIN
logbook l ON d.destination = l.destination AND d.distance_in_km = l.distance_in_km
GROUP BY
d.id, d.destination, d.distance_in_km
ORDER BY
COUNT(l.id) DESC, d.destination ASC;"
)
.fetch_all(db)
.await
.unwrap()
}
}

View File

@@ -1,108 +0,0 @@
use std::ops::DerefMut;
use serde::Serialize;
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction, sqlite::SqliteQueryResult};
use super::user::User;
#[derive(FromRow, Serialize, Clone)]
pub struct Family {
pub(crate) id: i64,
}
#[derive(Serialize, Clone)]
pub struct FamilyWithMembers {
id: i64,
names: Option<String>,
}
impl Family {
pub async fn all(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(Self, "SELECT id FROM role")
.fetch_all(db)
.await
.unwrap()
}
pub async fn insert_tx(db: &mut Transaction<'_, Sqlite>) -> i64 {
let result: SqliteQueryResult = sqlx::query("INSERT INTO family DEFAULT VALUES")
.execute(db.deref_mut())
.await
.unwrap();
result.last_insert_rowid()
}
pub async fn insert(db: &SqlitePool) -> i64 {
let result: SqliteQueryResult = sqlx::query("INSERT INTO family DEFAULT VALUES")
.execute(db)
.await
.unwrap();
result.last_insert_rowid()
}
pub async fn all_with_members(db: &SqlitePool) -> Vec<FamilyWithMembers> {
sqlx::query_as!(
FamilyWithMembers,
"
SELECT
family.id as id,
GROUP_CONCAT(user.name, ', ') as names
FROM family
LEFT JOIN
user ON family.id = user.family_id
GROUP BY family.id;"
)
.fetch_all(db)
.await
.unwrap()
}
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!(Self, "SELECT id FROM family WHERE id like ?", id)
.fetch_one(db)
.await
.ok()
}
pub async fn find_by_opt_id(db: &SqlitePool, id: Option<i64>) -> Option<Self> {
if let Some(id) = id {
Self::find_by_id(db, id).await
} else {
None
}
}
pub async fn amount_family_members(&self, db: &SqlitePool) -> i64 {
sqlx::query!(
"SELECT COUNT(*) as count FROM user WHERE family_id = ?",
self.id
)
.fetch_one(db)
.await
.unwrap()
.count
}
pub async fn members(&self, db: &SqlitePool) -> Vec<User> {
sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, phone, address, family_id, user_token FROM user WHERE family_id = ?", self.id)
.fetch_all(db)
.await
.unwrap()
}
pub async fn clean_families_without_members(db: &SqlitePool) {
sqlx::query(
"DELETE FROM family
WHERE id NOT IN (
SELECT DISTINCT family_id
FROM user
WHERE family_id IS NOT NULL
);",
)
.execute(db)
.await
.unwrap();
}
}

View File

@@ -1,6 +1,5 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, SqlitePool};
use std::ops::DerefMut;
#[derive(FromRow, Debug, Serialize, Deserialize)] #[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct Location { pub struct Location {
@@ -38,20 +37,6 @@ impl Location {
.await .await
.ok() .ok()
} }
pub async fn find_by_name_tx(db: &mut Transaction<'_, Sqlite>, name: String) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name
FROM location
WHERE name=?
",
name
)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn all(db: &SqlitePool) -> Vec<Self> { pub async fn all(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(Self, "SELECT id, name FROM location") sqlx::query_as!(Self, "SELECT id, name FROM location")

View File

@@ -1,16 +1,74 @@
use super::activity::ActivityBuilder; use std::ops::DerefMut;
use sqlx::{Sqlite, SqlitePool, Transaction};
pub struct Log {} use chrono::{DateTime, Local, NaiveDateTime, TimeZone, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct Log {
pub msg: String,
pub created_at: NaiveDateTime,
}
// TODO: remove and convert to proper acitvities
impl Log { impl Log {
pub async fn create(db: &SqlitePool, msg: String) -> bool { pub async fn create(db: &SqlitePool, msg: String) -> bool {
ActivityBuilder::new(&msg).save(db).await; sqlx::query!("INSERT INTO log(msg) VALUES (?)", msg,)
true .execute(db)
.await
.is_ok()
} }
pub async fn create_with_tx(db: &mut Transaction<'_, Sqlite>, msg: String) -> bool { pub async fn create_with_tx(db: &mut Transaction<'_, Sqlite>, msg: String) -> bool {
ActivityBuilder::new(&msg).save_tx(db).await; sqlx::query!("INSERT INTO log(msg) VALUES (?)", msg,)
true .execute(db.deref_mut())
.await
.is_ok()
}
async fn last(db: &SqlitePool) -> Vec<Log> {
sqlx::query_as!(
Log,
"
SELECT msg, created_at
FROM log
ORDER BY id DESC
LIMIT 1000
"
)
.fetch_all(db)
.await
.unwrap()
}
pub async fn generate_feed(db: &SqlitePool) -> String {
let mut ret = String::from(
r#"<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Ruder App Admin Feed</title>
<link>app.rudernlinz.at</link>
<description>An RSS feed with activities from app.rudernlinz.at</description>"#,
);
for log in Self::last(db).await {
let utc_time: DateTime<Utc> = Utc::from_utc_datetime(&Utc, &log.created_at);
let local_time = utc_time.with_timezone(&Local);
ret.push_str("<item><title>");
ret.push_str(&format!("({}) {}", local_time, log.msg));
ret.push_str("</title></item>");
}
ret.push_str("</channel>");
ret.push_str("</rss>");
ret.replace('\n', "")
}
pub async fn show(db: &SqlitePool) -> String {
let mut ret = String::new();
for log in Self::last(db).await {
let utc_time: DateTime<Utc> = Utc::from_utc_datetime(&Utc, &log.created_at);
let local_time = utc_time.with_timezone(&Local);
ret.push_str(&format!("- {} - {}\n", local_time, log.msg));
}
ret
} }
} }

View File

@@ -1,22 +1,13 @@
use std::{fmt::Display, ops::DerefMut}; use std::ops::DerefMut;
use chrono::{Datelike, Duration, Local, NaiveDateTime}; use chrono::{Datelike, NaiveDateTime, Utc};
use rocket::FromForm; use rocket::FromForm;
use serde::{Deserialize, Serialize}; use serde::Serialize;
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
use super::{ use super::{boat::Boat, log::Log, rower::Rower, user::User};
activity::{ActivityBuilder, ReasonLogbook},
boat::Boat,
log::Log,
notification::Notification,
role::Role,
rower::Rower,
user::{User, VorstandUser},
};
use crate::model::user::VecUser;
#[derive(FromRow, Serialize, Deserialize, Clone, Debug)] #[derive(FromRow, Serialize, Clone, Debug)]
pub struct Logbook { pub struct Logbook {
pub id: i64, pub id: i64,
pub boat_id: i64, pub boat_id: i64,
@@ -38,11 +29,6 @@ impl PartialEq for Logbook {
} }
} }
pub(crate) enum Filter {
SingleDayOnly,
MultiDayOnly,
}
#[derive(FromForm, Debug, Clone)] #[derive(FromForm, Debug, Clone)]
pub struct LogToAdd { pub struct LogToAdd {
pub boat_id: i32, pub boat_id: i32,
@@ -72,22 +58,6 @@ pub struct LogToFinalize {
pub rowers: Vec<i64>, pub rowers: Vec<i64>,
} }
#[derive(FromForm, Debug, Clone)]
pub struct LogToUpdate {
pub id: i64,
pub boat_id: i64,
pub shipmaster: i64,
pub steering_person: i64,
pub shipmaster_only_steering: bool,
pub departure: String,
pub arrival: Option<String>,
pub destination: Option<String>,
pub distance_in_km: Option<i64>,
pub comments: Option<String>,
pub logtype: Option<i64>,
pub rowers: Vec<i64>,
}
impl TryFrom<LogToAdd> for LogToFinalize { impl TryFrom<LogToAdd> for LogToFinalize {
type Error = String; type Error = String;
@@ -112,7 +82,7 @@ impl TryFrom<LogToAdd> for LogToFinalize {
} }
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Debug)]
pub struct LogbookWithBoatAndRowers { pub struct LogbookWithBoatAndRowers {
#[serde(flatten)] #[serde(flatten)]
pub logbook: Logbook, pub logbook: Logbook,
@@ -122,77 +92,6 @@ pub struct LogbookWithBoatAndRowers {
pub rowers: Vec<User>, pub rowers: Vec<User>,
} }
impl Display for LogbookWithBoatAndRowers {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(arrival) = self.logbook.arrival {
let departure_date = format!("{}", self.logbook.departure.format("%Y-%m-%d"));
let arrival_date = format!("{}", arrival.format("%Y-%m-%d"));
if departure_date == arrival_date {
write!(
f,
"Datum: {}: Start: {}, Ende: {}; ",
&self.logbook.departure.format("%d. %m. %Y"),
&self.logbook.departure.format("%H:%M"),
&arrival.format("%H:%M")
)?;
} else {
write!(
f,
"{} - {}; ",
&self.logbook.departure.format("%d. %m. %Y"),
&arrival.format("%d. %m. %Y"),
)?;
}
} else {
write!(
f,
"Start: {}",
&self.logbook.departure.format("%d. %m. %Y %H:%M")
)?;
}
if let Some(destination) = &self.logbook.destination {
write!(f, "Ziel: {destination}; ")?;
}
write!(f, "Boot: {}; ", self.boat)?;
if let Some(distance) = self.logbook.distance_in_km {
write!(f, "Distanz: {distance} km; ")?;
}
write!(f, "Schiffsführer: {}; ", self.shipmaster_user)?;
write!(f, "Steuerperson: {}; ", self.steering_user)?;
write!(f, "Rudernde: {}; ", VecUser(&self.rowers))?;
if let Some(comments) = &self.logbook.comments {
if !comments.trim().is_empty() {
write!(f, "Kommentar: {comments}; ")?;
}
}
Ok(())
}
}
impl LogbookWithBoatAndRowers {
pub(crate) async fn from(db: &SqlitePool, log: Logbook) -> Self {
let mut tx = db.begin().await.unwrap();
let ret = Self::from_tx(&mut tx, log).await;
tx.commit().await.unwrap();
ret
}
pub(crate) async fn from_tx(db: &mut Transaction<'_, Sqlite>, log: Logbook) -> Self {
Self {
rowers: Rower::for_log_tx(db, &log).await,
boat: Boat::find_by_id_tx(db, log.boat_id as i32).await.unwrap(),
shipmaster_user: User::find_by_id_tx(db, log.shipmaster as i32)
.await
.unwrap(),
steering_user: User::find_by_id_tx(db, log.steering_person as i32)
.await
.unwrap(),
logbook: log,
}
}
}
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum LogbookUpdateError { pub enum LogbookUpdateError {
NotYourEntry, NotYourEntry,
@@ -203,10 +102,6 @@ pub enum LogbookUpdateError {
SteeringPersonNotInRowers, SteeringPersonNotInRowers,
UserNotAllowedToUseBoat, UserNotAllowedToUseBoat,
OnlyAllowedToEndTripsEndingToday, OnlyAllowedToEndTripsEndingToday,
TooFast(i64, i64),
AlreadyFinalized,
ExternalSteeringPersonMustSteerOrShipmaster,
BoatAlreadyOnWater,
} }
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
@@ -221,7 +116,7 @@ pub enum LogbookCreateError {
BoatLocked, BoatLocked,
BoatNotFound, BoatNotFound,
TooManyRowers(usize, usize), TooManyRowers(usize, usize),
RowerAlreadyOnWater(Box<User>), RowerAlreadyOnWater(User),
RowerCreateError(i64, String), RowerCreateError(i64, String),
ArrivalNotAfterDeparture, ArrivalNotAfterDeparture,
SteeringPersonNotInRowers, SteeringPersonNotInRowers,
@@ -229,10 +124,6 @@ pub enum LogbookCreateError {
NotYourEntry, NotYourEntry,
ArrivalSetButNotRemainingTwo, ArrivalSetButNotRemainingTwo,
OnlyAllowedToEndTripsEndingToday, OnlyAllowedToEndTripsEndingToday,
CantChangeHandoperatableStatusForThisBoat,
TooFast(i64, i64),
AlreadyFinalized,
ExternalSteeringPersonMustSteerOrShipmaster,
} }
impl From<LogbookUpdateError> for LogbookCreateError { impl From<LogbookUpdateError> for LogbookCreateError {
@@ -256,12 +147,6 @@ impl From<LogbookUpdateError> for LogbookCreateError {
LogbookUpdateError::OnlyAllowedToEndTripsEndingToday => { LogbookUpdateError::OnlyAllowedToEndTripsEndingToday => {
LogbookCreateError::OnlyAllowedToEndTripsEndingToday LogbookCreateError::OnlyAllowedToEndTripsEndingToday
} }
LogbookUpdateError::TooFast(km, min) => LogbookCreateError::TooFast(km, min),
LogbookUpdateError::AlreadyFinalized => LogbookCreateError::AlreadyFinalized,
LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster => {
LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster
}
LogbookUpdateError::BoatAlreadyOnWater => LogbookCreateError::BoatAlreadyOnWater,
} }
} }
} }
@@ -281,7 +166,7 @@ impl Logbook {
.await .await
.ok() .ok()
} }
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> { pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!( sqlx::query_as!(
Self, Self,
" "
@@ -328,142 +213,27 @@ ORDER BY departure DESC
let mut ret = Vec::new(); let mut ret = Vec::new();
for log in logs { for log in logs {
ret.push(LogbookWithBoatAndRowers::from(db, log).await); ret.push(LogbookWithBoatAndRowers {
rowers: Rower::for_log(db, &log).await,
boat: Boat::find_by_id(db, log.boat_id as i32).await.unwrap(),
shipmaster_user: User::find_by_id(db, log.shipmaster as i32).await.unwrap(),
steering_user: User::find_by_id(db, log.steering_person as i32)
.await
.unwrap(),
logbook: log,
});
} }
ret ret
} }
pub async fn completed_with_user(
db: &SqlitePool,
user: &User,
) -> Vec<LogbookWithBoatAndRowers> {
let mut tx = db.begin().await.unwrap();
let ret = Self::completed_with_user_tx(&mut tx, user).await;
tx.commit().await.unwrap();
ret
}
pub async fn completed_with_user_tx(
db: &mut Transaction<'_, Sqlite>,
user: &User,
) -> Vec<LogbookWithBoatAndRowers> {
let logs = sqlx::query_as(
&format!("
SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
FROM logbook
JOIN rower ON logbook.id = rower.logbook_id
WHERE arrival is not null AND rower_id = {}
ORDER BY arrival DESC
", user.id)
)
.fetch_all(db.deref_mut())
.await
.unwrap(); //TODO: fixme
let mut ret = Vec::new();
for log in logs {
ret.push(LogbookWithBoatAndRowers::from_tx(db, log).await);
}
ret
}
pub async fn year_first_logbook_entry(db: &SqlitePool, user: &User) -> Option<i32> {
let log: Option<Self> = sqlx::query_as(
&format!("
SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
FROM logbook
JOIN rower ON logbook.id = rower.logbook_id
WHERE arrival is not null AND rower_id = {}
ORDER BY arrival
LIMIT 1
", user.id)
)
.fetch_optional(db)
.await
.unwrap(); //TODO: fixme
if let Some(log) = log {
Some(log.arrival.unwrap().year())
} else {
None
}
}
pub async fn year_last_logbook_entry(db: &SqlitePool, user: &User) -> Option<i32> {
let log: Option<Self> = sqlx::query_as(
&format!("
SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
FROM logbook
JOIN rower ON logbook.id = rower.logbook_id
WHERE arrival is not null AND rower_id = {}
ORDER BY arrival DESC
LIMIT 1
", user.id)
)
.fetch_optional(db)
.await
.unwrap(); //TODO: fixme
if let Some(log) = log {
Some(log.arrival.unwrap().year())
} else {
None
}
}
pub(crate) async fn completed_wanderfahrten_with_user_over_km_in_year_tx(
db: &mut Transaction<'_, Sqlite>,
user: &User,
min_distance: i32,
year: i32,
filter: Filter,
) -> Vec<LogbookWithBoatAndRowers> {
let logs: Vec<Logbook> = sqlx::query_as(
&format!("
SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
FROM logbook
JOIN rower ON logbook.id = rower.logbook_id
WHERE arrival is not null AND rower_id = {} AND logtype = 1 AND distance_in_km >= {} AND arrival like '{}-%'
ORDER BY arrival DESC
", user.id, min_distance, year)
)
.fetch_all(db.deref_mut())
.await
.unwrap(); //TODO: fixme
let mut ret = Vec::new();
for log in logs {
let trip_days = log.arrival.unwrap() - log.departure;
let trip_days = trip_days.num_days();
match filter {
Filter::SingleDayOnly => {
if trip_days == 0 {
ret.push(LogbookWithBoatAndRowers::from_tx(db, log).await);
}
}
Filter::MultiDayOnly => {
if trip_days > 0 {
ret.push(LogbookWithBoatAndRowers::from_tx(db, log).await);
}
}
}
}
ret
}
pub async fn completed(db: &SqlitePool) -> Vec<LogbookWithBoatAndRowers> { pub async fn completed(db: &SqlitePool) -> Vec<LogbookWithBoatAndRowers> {
let year = chrono::Local::now().year(); let year = chrono::Utc::now().year();
Self::completed_in_year(db, year).await
}
pub async fn completed_in_year(db: &SqlitePool, year: i32) -> Vec<LogbookWithBoatAndRowers> {
let logs = sqlx::query_as( let logs = sqlx::query_as(
&format!(" &format!("
SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
FROM logbook FROM logbook
WHERE arrival is not null AND arrival LIKE '{}-%' WHERE arrival is not null AND arrival LIKE '{}-%'
ORDER BY arrival DESC ORDER BY departure DESC
", year) ", year)
) )
.fetch_all(db) .fetch_all(db)
@@ -472,7 +242,15 @@ ORDER BY departure DESC
let mut ret = Vec::new(); let mut ret = Vec::new();
for log in logs { for log in logs {
ret.push(LogbookWithBoatAndRowers::from(db, log).await); ret.push(LogbookWithBoatAndRowers {
rowers: Rower::for_log(db, &log).await,
boat: Boat::find_by_id(db, log.boat_id as i32).await.unwrap(),
shipmaster_user: User::find_by_id(db, log.shipmaster as i32).await.unwrap(),
steering_user: User::find_by_id(db, log.steering_person as i32)
.await
.unwrap(),
logbook: log,
});
} }
ret ret
} }
@@ -481,28 +259,19 @@ ORDER BY departure DESC
db: &SqlitePool, db: &SqlitePool,
mut log: LogToAdd, mut log: LogToAdd,
created_by_user: &User, created_by_user: &User,
smtp_pw: &str, ) -> Result<(), LogbookCreateError> {
) -> Result<String, LogbookCreateError> {
let Some(boat) = Boat::find_by_id(db, log.boat_id).await else { let Some(boat) = Boat::find_by_id(db, log.boat_id).await else {
return Err(LogbookCreateError::BoatNotFound); return Err(LogbookCreateError::BoatNotFound);
}; };
if log.shipmaster_only_steering != boat.default_shipmaster_only_steering
&& !boat.convert_handoperated_possible
{
return Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat);
}
if boat.amount_seats == 1 && log.rowers.is_empty() {
log.rowers = vec![created_by_user.id];
}
if boat.amount_seats == 1 { if boat.amount_seats == 1 {
log.shipmaster = Some(log.rowers[0]); log.shipmaster = Some(log.rowers[0]);
log.steering_person = Some(log.rowers[0]); log.steering_person = Some(log.rowers[0]);
} }
if let Ok(log_to_finalize) = TryInto::<LogToFinalize>::try_into(log.clone()) { if let Ok(log_to_finalize) = TryInto::<LogToFinalize>::try_into(log.clone()) {
//TODO: fix clone() above
if !boat.shipmaster_allowed(db, created_by_user).await { if !boat.shipmaster_allowed(db, created_by_user).await {
return Err(LogbookCreateError::UserNotAllowedToUseBoat); return Err(LogbookCreateError::UserNotAllowedToUseBoat);
} }
@@ -510,12 +279,13 @@ ORDER BY departure DESC
let mut tx = db.begin().await.unwrap(); let mut tx = db.begin().await.unwrap();
let inserted_row = sqlx::query!( let inserted_row = sqlx::query!(
"INSERT INTO logbook(boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, destination, distance_in_km, comments, logtype) VALUES (?,?,?,?,?,?,?,?,?) RETURNING id", "INSERT INTO logbook(boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype) VALUES (?,?,?,?,?,?,?,?,?,?) RETURNING id",
log.boat_id, log.boat_id,
log.shipmaster, log.shipmaster,
log.steering_person, log.steering_person,
log.shipmaster_only_steering, log.shipmaster_only_steering,
log.departure, log.departure,
log.arrival,
log.destination, log.destination,
log.distance_in_km, log.distance_in_km,
log.comments, log.comments,
@@ -529,12 +299,12 @@ ORDER BY departure DESC
.unwrap(); //ok .unwrap(); //ok
return match logbook return match logbook
.home_with_transaction(&mut tx, created_by_user, log_to_finalize, smtp_pw) .home_with_transaction(&mut tx, created_by_user, log_to_finalize)
.await .await
{ {
Ok(_) => { Ok(_) => {
tx.commit().await.unwrap(); tx.commit().await.unwrap();
Ok(String::new()) Ok(())
} }
Err(a) => Err(a.into()), Err(a) => Err(a.into()),
}; };
@@ -569,19 +339,7 @@ ORDER BY departure DESC
let user = User::find_by_id(db, *rower as i32).await.unwrap(); let user = User::find_by_id(db, *rower as i32).await.unwrap();
if user.on_water(db).await { if user.on_water(db).await {
return Err(LogbookCreateError::RowerAlreadyOnWater(Box::new(user))); return Err(LogbookCreateError::RowerAlreadyOnWater(user));
}
if user.name == "Externe Steuerperson" {
if let (Some(steering_id), Some(shipmaster_id)) =
(log.steering_person, log.shipmaster)
{
if steering_id != user.id && shipmaster_id != user.id {
return Err(
LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster,
);
}
}
} }
} }
@@ -618,40 +376,26 @@ ORDER BY departure DESC
tx.commit().await.unwrap(); tx.commit().await.unwrap();
let mut ret = String::new(); Ok(())
for rower in &log.rowers {
let user = User::find_by_id(db, *rower as i32).await.unwrap();
if let Some(msg) = user.close_thousands_trip(db).await {
ret.push_str(&format!("{msg}"));
}
}
Ok(ret)
} }
pub async fn update(&self, db: &SqlitePool, data: LogToUpdate, changed_by: &VorstandUser) { pub async fn distances(db: &SqlitePool) -> Vec<(String, i64)> {
sqlx::query!( let result = sqlx::query!("SELECT destination, distance_in_km FROM logbook WHERE id IN (SELECT MIN(id) FROM logbook GROUP BY destination) AND destination IS NOT NULL AND distance_in_km IS NOT NULL;")
"UPDATE logbook SET boat_id=?, shipmaster=?, steering_person=?, shipmaster_only_steering=?, departure=?, arrival=?, destination=?, distance_in_km=?, comments=?, logtype=? WHERE id=?", .fetch_all(db)
data.boat_id, .await
data.shipmaster, .unwrap();
data.steering_person,
data.shipmaster_only_steering,
data.departure,
data.arrival,
data.destination,
data.distance_in_km,
data.comments,
data.logtype,
self.id
)
.execute(db)
.await.unwrap();
Log::create( result
db, .into_iter()
format!("{changed_by} updated log entry={:?} to {:?}", self, data), .filter_map(|r| {
) if let (Some(destination), Some(distance_in_km)) = (r.destination, r.distance_in_km)
.await; {
Some((destination, distance_in_km))
} else {
None
}
})
.collect()
} }
async fn remove_rowers(&self, db: &mut Transaction<'_, Sqlite>) { async fn remove_rowers(&self, db: &mut Transaction<'_, Sqlite>) {
@@ -676,11 +420,9 @@ ORDER BY departure DESC
db: &SqlitePool, db: &SqlitePool,
user: &User, user: &User,
log: LogToFinalize, log: LogToFinalize,
smtp_pw: &str,
) -> Result<(), LogbookUpdateError> { ) -> Result<(), LogbookUpdateError> {
let mut tx = db.begin().await.unwrap(); let mut tx = db.begin().await.unwrap();
self.home_with_transaction(&mut tx, user, log, smtp_pw) self.home_with_transaction(&mut tx, user, log).await?;
.await?;
tx.commit().await.unwrap(); tx.commit().await.unwrap();
Ok(()) Ok(())
} }
@@ -690,17 +432,12 @@ ORDER BY departure DESC
db: &mut Transaction<'_, Sqlite>, db: &mut Transaction<'_, Sqlite>,
user: &User, user: &User,
mut log: LogToFinalize, mut log: LogToFinalize,
smtp_pw: &str,
) -> Result<(), LogbookUpdateError> { ) -> Result<(), LogbookUpdateError> {
//TODO: extract common tests with `create()` //TODO: extract common tests with `create()`
if !user.has_role_tx(db, "Vorstand").await && user.id != self.shipmaster { if user.id != self.shipmaster {
return Err(LogbookUpdateError::NotYourEntry); return Err(LogbookUpdateError::NotYourEntry);
} }
if self.arrival.is_some() {
return Err(LogbookUpdateError::AlreadyFinalized);
}
let boat = Boat::find_by_id_tx(db, self.boat_id as i32).await.unwrap(); //ok let boat = Boat::find_by_id_tx(db, self.boat_id as i32).await.unwrap(); //ok
if boat.amount_seats == 1 { if boat.amount_seats == 1 {
@@ -730,36 +467,11 @@ ORDER BY departure DESC
let dep = NaiveDateTime::parse_from_str(&log.departure, "%Y-%m-%dT%H:%M").unwrap(); let dep = NaiveDateTime::parse_from_str(&log.departure, "%Y-%m-%dT%H:%M").unwrap();
let arr = NaiveDateTime::parse_from_str(&log.arrival, "%Y-%m-%dT%H:%M").unwrap(); let arr = NaiveDateTime::parse_from_str(&log.arrival, "%Y-%m-%dT%H:%M").unwrap();
if arr.and_utc().timestamp() < dep.and_utc().timestamp() { if arr.timestamp() <= dep.timestamp() {
return Err(LogbookUpdateError::ArrivalNotAfterDeparture); return Err(LogbookUpdateError::ArrivalNotAfterDeparture);
} }
let today = Utc::now().date_naive();
if !boat.external && boat.on_water_between(db, dep, arr).await { if arr.date() != today && !user.has_role_tx(db, "admin").await {
return Err(LogbookUpdateError::BoatAlreadyOnWater);
}
let duration_in_mins = (arr.and_utc().timestamp() - dep.and_utc().timestamp()) / 60;
// Not possible to row < 1 min / 500 m = < 2 min / km
let possible_distance_km = duration_in_mins / 2;
if log.distance_in_km > possible_distance_km {
return Err(LogbookUpdateError::TooFast(
log.distance_in_km,
duration_in_mins,
));
}
let today = Local::now().date_naive();
let day_diff = today - arr.date();
let day_diff = day_diff.num_days();
if day_diff >= 7
&& !user.has_role_tx(db, "admin").await
&& !user
.has_role_tx(db, "allow-retroactive-logbookentries")
.await
{
return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday);
}
if day_diff < 0 && !user.has_role_tx(db, "admin").await {
return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday); return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday);
} }
@@ -767,40 +479,9 @@ ORDER BY departure DESC
self.remove_rowers(db).await; self.remove_rowers(db).await;
for rower in &log.rowers { for rower in &log.rowers {
let user = User::find_by_id_tx(db, *rower as i32).await.unwrap();
if user.name == "Externe Steuerperson" {
if let (Some(steering_id), Some(shipmaster_id)) =
(log.steering_person, log.shipmaster)
{
if steering_id != user.id && shipmaster_id != user.id {
return Err(
LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster,
);
}
}
}
Rower::create(db, self.id, *rower) Rower::create(db, self.id, *rower)
.await .await
.map_err(|e| LogbookUpdateError::RowerCreateError(*rower, e.to_string()))?; .map_err(|e| LogbookUpdateError::RowerCreateError(*rower, e.to_string()))?;
let user = User::find_by_id_tx(db, *rower as i32).await.unwrap();
Notification::create_with_tx(
db,
&user,
&format!(
"Ausfahrt am {}.{}.{}; Ziel: {} ({} km)",
dep.day(),
dep.month(),
dep.year(),
log.destination,
log.distance_in_km
),
"Neuer Logbucheintrag",
None,
None,
)
.await;
} }
sqlx::query!( sqlx::query!(
@@ -819,94 +500,18 @@ ORDER BY departure DESC
.execute(db.deref_mut()) .execute(db.deref_mut())
.await.unwrap(); //TODO: fixme .await.unwrap(); //TODO: fixme
let duration = arr - dep;
if duration.num_days() > 0 {
let vorstand = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
Notification::create_for_role_tx(
db,
&vorstand,
&format!("'{}' hat eine mehrtägige Ausfahrt vom {} bis {} eingetragen ({} km; Ziel: {}; Anmerkungen: {}). Falls das nicht stimmen sollte, bitte nachhaken.",user.name,log.departure, log.arrival, log.distance_in_km, log.destination, log.comments.clone().unwrap_or("".into())),
"Mehrtägige Ausfahrt eingetragen",
None,None
).await;
}
if boat.external {
let vorstand = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
Notification::create_for_role_tx(
db,
&vorstand,
&format!("'{}' hat eine Ausfahrt mit externem Boot '{}' am {} eingetragen ({} km; Ziel: {}; Anmerkungen: {}). Falls das nicht stimmen sollte, bitte nachhaken.",user.name,boat.name,log.departure,log.distance_in_km, log.destination, log.comments.unwrap_or("".into())),
"Ausfahrt mit externem Boot eingetragen",
None,None,
).await;
}
for rower in &log.rowers {
let user = User::find_by_id_tx(db, *rower as i32).await.unwrap();
user.received_new_logentry(db, smtp_pw).await;
}
Ok(()) Ok(())
} }
pub async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), LogbookDeleteError> { pub async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), LogbookDeleteError> {
if self.arrival.is_none() { Log::create(db, format!("{user:?} deleted trip: {self:?}")).await;
if user.has_role(db, "admin").await
|| user.has_role(db, "Vorstand").await
|| user.id == self.shipmaster
{
Log::create(db, format!("{} deleted trip: {self:?}", user.name)).await;
let now = Local::now().naive_local();
let difference = now - self.departure;
if difference > Duration::hours(1) {
let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap();
let logbook = LogbookWithBoatAndRowers::from(db, self.clone()).await;
Notification::create_for_role( if user.has_role(db, "admin").await || user.id == self.shipmaster {
db, sqlx::query!("DELETE FROM logbook WHERE id=?", self.id)
&vorstand, .execute(db)
&format!("{user} hat folgenden Logbuch-Eintrag jetzt gelöscht, welcher bereits vor über einer Stunde begonnen wurde: {logbook}"), .await
"Ungewöhnliches Verhalten", .unwrap(); //Okay, because we can only create a Logbook of a valid id
None, return Ok(());
None,
)
.await;
}
sqlx::query!("DELETE FROM logbook WHERE id=?", self.id)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a Logbook of a valid id
return Ok(());
}
} else {
// Only admins+Vorstand can delete completed logbook entries
if user.has_role(db, "admin").await || user.has_role(db, "Vorstand").await {
let logbookdetails = LogbookWithBoatAndRowers::from(db, self.clone()).await;
ActivityBuilder::from(ReasonLogbook::BoardOrAdminDeleted(user, &logbookdetails))
.save(db)
.await;
let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap();
Notification::create_for_role(
db,
&vorstand,
&format!("{user} hat den Logbuch-Eintrag gelöscht: {logbookdetails}"),
"Logbuch gelöscht",
None,
None,
)
.await;
sqlx::query!("DELETE FROM logbook WHERE id=?", self.id)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a Logbook of a valid id
return Ok(());
}
} }
Err(LogbookDeleteError::NotYourEntry) Err(LogbookDeleteError::NotYourEntry)
} }
@@ -918,7 +523,6 @@ mod test {
use crate::model::user::User; use crate::model::user::User;
use crate::testdb; use crate::testdb;
use chrono::Duration;
use sqlx::SqlitePool; use sqlx::SqlitePool;
#[sqlx::test] #[sqlx::test]
@@ -970,7 +574,7 @@ mod test {
fn test_succ_create() { fn test_succ_create() {
let pool = testdb!(); let pool = testdb!();
let msg = Logbook::create( Logbook::create(
&pool, &pool,
LogToAdd { LogToAdd {
boat_id: 3, boat_id: 3,
@@ -986,67 +590,9 @@ mod test {
rowers: vec![4], rowers: vec![4],
}, },
&User::find_by_id(&pool, 4).await.unwrap(), &User::find_by_id(&pool, 4).await.unwrap(),
"",
) )
.await .await
.unwrap(); .unwrap()
assert_eq!(msg, String::from(""));
}
#[sqlx::test]
fn test_succ_create_with_thousands_msg() {
let pool = testdb!();
let logbook = Logbook::find_by_id(&pool, 1).await.unwrap();
let user = User::find_by_id(&pool, 2).await.unwrap();
let current_date = chrono::Local::now().format("%Y-%m-%d").to_string();
let start_date = chrono::Local::now() - Duration::days(3);
let start_date = start_date.format("%Y-%m-%d").to_string();
logbook
.home(
&pool,
&user,
super::LogToFinalize {
destination: "new-destination".into(),
distance_in_km: 995,
comments: Some("Perfect water".into()),
logtype: None,
rowers: vec![2],
shipmaster: Some(2),
steering_person: Some(2),
shipmaster_only_steering: false,
departure: format!("{}T10:00", start_date),
arrival: format!("{}T12:00", current_date),
},
"",
)
.await
.unwrap();
let msg = Logbook::create(
&pool,
LogToAdd {
boat_id: 3,
shipmaster: Some(2),
steering_person: Some(2),
shipmaster_only_steering: false,
departure: "2128-05-20T12:00".into(),
arrival: None,
destination: None,
distance_in_km: None,
comments: None,
logtype: None,
rowers: vec![2],
},
&User::find_by_id(&pool, 1).await.unwrap(),
"",
)
.await
.unwrap();
assert_eq!(
msg,
String::from(" • rower braucht nur mehr 5 km bis die 1000 km voll sind 🤑")
);
} }
#[sqlx::test] #[sqlx::test]
@@ -1069,7 +615,6 @@ mod test {
rowers: vec![5], rowers: vec![5],
}, },
&User::find_by_id(&pool, 4).await.unwrap(), &User::find_by_id(&pool, 4).await.unwrap(),
"",
) )
.await; .await;
@@ -1096,7 +641,6 @@ mod test {
rowers: vec![5], rowers: vec![5],
}, },
&User::find_by_id(&pool, 4).await.unwrap(), &User::find_by_id(&pool, 4).await.unwrap(),
"",
) )
.await; .await;
@@ -1123,7 +667,6 @@ mod test {
rowers: vec![5], rowers: vec![5],
}, },
&User::find_by_id(&pool, 5).await.unwrap(), &User::find_by_id(&pool, 5).await.unwrap(),
"",
) )
.await; .await;
@@ -1150,7 +693,6 @@ mod test {
rowers: vec![5], rowers: vec![5],
}, },
&User::find_by_id(&pool, 5).await.unwrap(), &User::find_by_id(&pool, 5).await.unwrap(),
"",
) )
.await; .await;
@@ -1177,7 +719,6 @@ mod test {
rowers: Vec::new(), rowers: Vec::new(),
}, },
&User::find_by_id(&pool, 2).await.unwrap(), &User::find_by_id(&pool, 2).await.unwrap(),
"",
) )
.await; .await;
@@ -1204,7 +745,6 @@ mod test {
rowers: vec![5], rowers: vec![5],
}, },
&User::find_by_id(&pool, 5).await.unwrap(), &User::find_by_id(&pool, 5).await.unwrap(),
"",
) )
.await; .await;
@@ -1231,13 +771,27 @@ mod test {
rowers: vec![1, 5], rowers: vec![1, 5],
}, },
&User::find_by_id(&pool, 5).await.unwrap(), &User::find_by_id(&pool, 5).await.unwrap(),
"",
) )
.await; .await;
assert_eq!(res, Err(LogbookCreateError::TooManyRowers(1, 2))); assert_eq!(res, Err(LogbookCreateError::TooManyRowers(1, 2)));
} }
#[sqlx::test]
fn test_distances() {
let pool = testdb!();
let res = Logbook::distances(&pool).await;
assert_eq!(
res,
vec![
("Ottensheim".into(), 25 as i64),
("Ottensheim + Regattastrecke".into(), 29 as i64),
]
);
}
#[sqlx::test] #[sqlx::test]
fn test_succ_home() { fn test_succ_home() {
let pool = testdb!(); let pool = testdb!();
@@ -1263,7 +817,6 @@ mod test {
departure: format!("{}T10:00", current_date), departure: format!("{}T10:00", current_date),
arrival: format!("{}T12:00", current_date), arrival: format!("{}T12:00", current_date),
}, },
"",
) )
.await .await
.unwrap(); .unwrap();
@@ -1292,7 +845,6 @@ mod test {
departure: "1990-01-01T10:00".into(), departure: "1990-01-01T10:00".into(),
arrival: "1990-01-01T12:00".into(), arrival: "1990-01-01T12:00".into(),
}, },
"",
) )
.await; .await;
@@ -1322,7 +874,6 @@ mod test {
departure: "1990-01-01T10:00".into(), departure: "1990-01-01T10:00".into(),
arrival: "1990-01-01T12:00".into(), arrival: "1990-01-01T12:00".into(),
}, },
"",
) )
.await; .await;

View File

@@ -1,410 +0,0 @@
use std::{error::Error, fs};
use lettre::{
Address, Message, SmtpTransport, Transport,
message::{Attachment, MultiPart, SinglePart, header::ContentType},
transport::smtp::authentication::Credentials,
};
use sqlx::{Sqlite, SqlitePool, Transaction};
use crate::tera::admin::mail::MailToSend;
use super::{activity::ActivityBuilder, family::Family, log::Log, role::Role, user::User};
pub struct Mail {}
impl Mail {
pub async fn send_single(
db: &SqlitePool,
to: &str,
subject: &str,
body: String,
smtp_pw: &str,
) -> Result<(), String> {
let mut tx = db.begin().await.unwrap();
let ret = Self::send_single_tx(&mut tx, to, subject, body, smtp_pw).await;
tx.commit().await.unwrap();
ret
}
pub async fn send_single_tx(
db: &mut Transaction<'_, Sqlite>,
to: &str,
subject: &str,
body: String,
smtp_pw: &str,
) -> Result<(), String> {
let mut email = Message::builder()
.from(
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap(),
)
.reply_to(
"ASKÖ Ruderverein Donau Linz <info@rudernlinz.at>"
.parse()
.unwrap(),
)
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap());
let splitted = to.split(',');
for single_rec in splitted {
match single_rec.parse() {
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
Err(_) => {
Log::create_with_tx(
db,
format!("Mail not sent to {single_rec}, because it could not be parsed"),
)
.await;
return Err(format!(
"Mail nicht versandt, da '{single_rec}' keine gültige Mailadresse ist."
));
}
}
}
let email = email
.subject(subject)
.header(ContentType::TEXT_PLAIN)
.body(body)
.unwrap();
let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw.into());
let mailer = SmtpTransport::relay("mail.your-server.de")
.unwrap()
.credentials(creds)
.build();
// Send the email
if let Err(e) = mailer.send(&email) {
Log::create_with_tx(db, format!("Mail nicht versandt: {e:?}")).await;
}
Ok(())
}
pub async fn send(db: &SqlitePool, data: MailToSend<'_>, smtp_pw: String) -> bool {
let mut email = Message::builder()
.from(
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap(),
)
.reply_to(
"ASKÖ Ruderverein Donau Linz <info@rudernlinz.at>"
.parse()
.unwrap(),
)
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap());
let role = Role::find_by_id(db, data.role_id).await.unwrap();
for rec in role.mails_from_role(db).await {
let splitted = rec.split(',');
for single_rec in splitted {
match single_rec.parse() {
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
Err(_) => {
Log::create(
db,
format!("Mail not sent to {rec}, because it could not be parsed"),
)
.await;
}
}
}
}
let mut multipart = MultiPart::mixed().singlepart(SinglePart::plain(data.body));
for temp_file in &data.files {
let content = fs::read(temp_file.path().unwrap()).unwrap();
let media_type = format!("{}", temp_file.content_type().unwrap().media_type());
let content_type = ContentType::parse(&media_type).unwrap();
if let Some(name) = temp_file.name() {
let attachment = Attachment::new(format!(
"{}.{}",
name,
temp_file.content_type().unwrap().extension().unwrap()
))
.body(content, content_type);
multipart = multipart.singlepart(attachment);
}
}
let email = email.subject(data.subject).multipart(multipart).unwrap();
let creds = Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw);
let mailer = SmtpTransport::relay("mail.your-server.de")
.unwrap()
.credentials(creds)
.build();
// Send the email
match mailer.send(&email) {
Ok(_) => return true,
Err(e) => println!("{:?}", e.source()),
};
false
}
pub async fn fees(db: &SqlitePool, smtp_pw: String, test: Option<User>) {
let users = User::all_payer_groups(db).await;
for user in users {
if let Some(test) = &test {
if user.id != test.id {
continue;
}
}
if user.has_role(db, "schnupperant").await || user.has_role(db, "scheckbuch").await {
continue;
}
if !user.has_role(db, "paid").await || test.is_some() {
let mut is_family = false;
let mut send_to = String::new();
match Family::find_by_opt_id(db, user.family_id).await {
Some(family) => {
is_family = true;
for member in family.members(db).await {
if let Some(mail) = member.mail {
send_to.push_str(&format!("{mail},"))
}
}
}
None => {
if let Some(mail) = &user.mail {
send_to.push_str(mail)
}
}
}
let fees = user.fee(db).await;
if let Some(fees) = fees {
let mut content = format!(
"Liebes Vereinsmitglied, \n\n\
dein Vereinsbeitrag für das aktuelle Jahr beträgt {}",
fees.sum_in_cents / 100,
);
if fees.parts.len() == 1 {
content.push_str(&format!(" ({}).\n", fees.parts[0].0))
} else {
content.push_str(". Dieser setzt sich aus folgenden Teilen zusammen: \n");
for (desc, fee_in_cents) in fees.parts {
content.push_str(&format!("- {}: {}\n", desc, fee_in_cents / 100))
}
}
if is_family {
content.push_str(&format!(
"Dieser gilt für die gesamte Familie ({}).\n",
fees.name
))
}
content.push_str("\nBitte überweise diesen auf folgendes Konto: IBAN: AT58 2032 0321 0072 9256. Auf https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.\n\n\
Falls die Berechnung nicht stimmt (korrekte Preise findest du unter https://rudernlinz.at/unser-verein/gebuhren/) melde dich bitte bei kassier@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an kassier@rudernlinz.at schicken.\n\n\
Wenn du die Vereinsgebühren schon bezahlt hast, kannst du diese Mail einfach ignorieren.\n\n
Beste Grüße\n\
Der Vorstand");
let mut email = Message::builder()
.from(
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap(),
)
.reply_to(
"ASKÖ Ruderverein Donau Linz <kassier@rudernlinz.at>"
.parse()
.unwrap(),
)
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap());
let splitted = send_to.split(',');
let mut send_mail = false;
for single_rec in splitted {
let single_rec = single_rec.trim();
match single_rec.parse() {
Ok(val) => {
email = email.bcc(val);
send_mail = true;
}
Err(_) => {
println!("Error in mail: {single_rec}");
}
}
}
if send_mail {
let email = email
.subject("ASKÖ Ruderverein Donau Linz | Vereinsgebühren")
.header(ContentType::TEXT_PLAIN)
.body(content)
.unwrap();
let creds =
Credentials::new("no-reply@rudernlinz.at".to_owned(), smtp_pw.clone());
let mailer = SmtpTransport::relay("mail.your-server.de")
.unwrap()
.credentials(creds)
.build();
// Send the email
mailer.send(&email).unwrap();
ActivityBuilder::new(&format!(
"{user} hat die Info-Mail bzgl. Gebühren gesendet bekommen."
))
.user(&user)
.save(db)
.await;
}
}
}
}
}
pub async fn fees_final(db: &SqlitePool, smtp_pw: String, test: Option<User>) {
let users = User::all_payer_groups(db).await;
for user in users {
if let Some(test) = &test {
if user.id != test.id {
continue;
}
}
if user.has_role(db, "schnupperant").await || user.has_role(db, "scheckbuch").await {
continue;
}
if let Some(fee) = user.fee(db).await {
if !fee.paid || test.is_some() {
let mut is_family = false;
let mut send_to = String::new();
match Family::find_by_opt_id(db, user.family_id).await {
Some(family) => {
is_family = true;
for member in family.members(db).await {
if let Some(mail) = member.mail {
send_to.push_str(&format!("{mail},"))
}
}
}
None => {
if let Some(mail) = &user.mail {
send_to.push_str(mail)
}
}
}
let fees = user.fee(db).await;
if let Some(fees) = fees {
let mut content = format!(
"Liebes Vereinsmitglied, \n\n\
wir möchten darauf hinweisen, dass wir deinen Mitgliedsbeitrag für das laufende Jahr bislang nicht verbuchen konnten. Es besteht die Möglichkeit, dass es sich hierbei um ein Versehen unsererseits handelt. Solltest du den Betrag bereits überwiesen haben, bitte kurz auf diese E-Mail antworten, damit wir es richtigstellen können.
Falls die Zahlung noch nicht erfolgt ist, bitten wir um umgehende Überweisung des ausstehenden Betrags, spätestens jedoch binnen 14 Tagen, auf unser Bankkonto.\n\n\
Dein Vereinsbeitrag für das aktuelle Jahr beträgt {}",
fees.sum_in_cents / 100,
);
if fees.parts.len() == 1 {
content.push_str(&format!(" ({}).\n", fees.parts[0].0))
} else {
content
.push_str(". Dieser setzt sich aus folgenden Teilen zusammen: \n");
for (desc, fee_in_cents) in fees.parts {
content.push_str(&format!("- {}: {}\n", desc, fee_in_cents / 100))
}
}
if is_family {
content.push_str(&format!(
"Dieser gilt für die gesamte Familie ({}). Diese Mail wird an alle Familienmitglieder verschickt, bezahlen müsst ihr natürlich nur 1x.\n",
fees.name
))
}
content.push_str("\n\
Gemäß § 7 Abs. 3 lit. c unseres Status behalten wir uns vor, bei ausbleibender Zahlung die Mitgliedschaft zu beenden. Dies möchten wir vermeiden und hoffen auf deine Unterstützung.\n\n\
Bei Fragen oder Problemen stehen wir gerne zur Verfügung.
Bankverbindung: IBAN: AT58 2032 0321 0072 9256 (Unter https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.)
Mit freundlichen Grüßen,\n\
Der Vorstand");
let mut email = Message::builder()
.from(
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap(),
)
.reply_to(
"ASKÖ Ruderverein Donau Linz <kassier@rudernlinz.at>"
.parse()
.unwrap(),
)
.to("ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
.parse()
.unwrap());
let splitted = send_to.split(',');
let mut send_mail = false;
for single_rec in splitted {
let single_rec = single_rec.trim();
match single_rec.parse() {
Ok(val) => {
email = email.bcc(val);
send_mail = true;
}
Err(_) => {
println!("Error in mail: {single_rec}");
}
}
}
if send_mail {
let email = email
.subject("Mahnung Vereinsgebühren | ASKÖ Ruderverein Donau Linz")
.header(ContentType::TEXT_PLAIN)
.body(content)
.unwrap();
let creds = Credentials::new(
"no-reply@rudernlinz.at".to_owned(),
smtp_pw.clone(),
);
let mailer = SmtpTransport::relay("mail.your-server.de")
.unwrap()
.credentials(creds)
.build();
// Send the email
mailer.send(&email).unwrap();
ActivityBuilder::new(&format!(
"{user} hat die Mahn-Mail bzgl. Gebühren gesendet bekommen."
))
.user(&user)
.save(db)
.await;
}
}
}
}
}
}
}
pub(crate) fn valid_mails(mails: &str) -> bool {
let splitted = mails.split(',');
for single_rec in splitted {
if single_rec.parse::<Address>().is_err() {
return false;
}
}
true
}

View File

@@ -1,91 +1,58 @@
use chrono::{Local, NaiveDate}; use chrono::NaiveDate;
use serde::Serialize; use serde::Serialize;
use sqlx::SqlitePool; use sqlx::SqlitePool;
use waterlevel::WaterlevelDay;
use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD; use self::{
planned_event::{PlannedEvent, PlannedEventWithUserAndTriptype},
use self::{waterlevel::Waterlevel, weather::Weather}; trip::{Trip, TripWithUserAndType},
use boatreservation::{BoatReservation, BoatReservationWithDetails};
use planned::{
event::{Event, EventWithDetails},
trip::{Trip, TripWithDetails},
}; };
use std::collections::HashMap;
pub mod activity;
pub mod boat; pub mod boat;
pub mod boatdamage; pub mod boatdamage;
pub mod boathouse;
pub mod boatreservation;
pub mod distance;
pub mod family;
pub mod location; pub mod location;
pub mod log; pub mod log;
pub mod logbook; pub mod logbook;
pub mod logtype; pub mod logtype;
pub mod mail; pub mod planned_event;
pub mod notification;
pub mod personal;
pub mod planned;
pub mod role; pub mod role;
pub mod rower; pub mod rower;
pub mod stat; pub mod stat;
pub mod trailer; pub mod trip;
pub mod trailerreservation; pub mod tripdetails;
pub mod triptype;
pub mod user; pub mod user;
pub mod waterlevel; pub mod usertrip;
pub mod weather;
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub struct Day { pub struct Day {
day: NaiveDate, day: NaiveDate,
events: Vec<EventWithDetails>, planned_events: Vec<PlannedEventWithUserAndTriptype>,
trips: Vec<TripWithDetails>, trips: Vec<TripWithUserAndType>,
is_pinned: bool, is_pinned: bool,
regular_sees_this_day: bool,
max_waterlevel: Option<WaterlevelDay>,
weather: Option<Weather>,
boat_reservations: HashMap<String, Vec<BoatReservationWithDetails>>,
} }
impl Day { impl Day {
pub async fn new(db: &SqlitePool, day: NaiveDate, is_pinned: bool) -> Self { pub async fn new(db: &SqlitePool, day: NaiveDate, is_pinned: bool) -> Self {
let today = Local::now().date_naive();
let day_diff = (day - today).num_days() + 1;
let regular_sees_this_day = day_diff <= AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD;
if is_pinned { if is_pinned {
Self { Self {
day, day,
events: Event::get_pinned_for_day(db, day).await, planned_events: PlannedEvent::get_pinned_for_day(db, day).await,
trips: Trip::get_pinned_for_day(db, day).await, trips: Trip::get_pinned_for_day(db, day).await,
is_pinned, is_pinned,
regular_sees_this_day,
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
weather: Weather::find_by_day(db, day).await,
boat_reservations: BoatReservation::with_groups(
BoatReservation::for_day(db, day).await,
),
} }
} else { } else {
Self { Self {
day, day,
events: Event::get_for_day(db, day).await, planned_events: PlannedEvent::get_for_day(db, day).await,
trips: Trip::get_for_day(db, day).await, trips: Trip::get_for_day(db, day).await,
is_pinned, is_pinned,
regular_sees_this_day,
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
weather: Weather::find_by_day(db, day).await,
boat_reservations: BoatReservation::with_groups(
BoatReservation::for_day(db, day).await,
),
} }
} }
} }
pub async fn new_guest(db: &SqlitePool, day: NaiveDate, is_pinned: bool) -> Self { pub async fn new_guest(db: &SqlitePool, day: NaiveDate, is_pinned: bool) -> Self {
let mut day = Self::new(db, day, is_pinned).await; let mut day = Self::new(db, day, is_pinned).await;
day.events.retain(|e| e.event.allow_guests); day.planned_events.retain(|e| e.planned_event.allow_guests);
day.trips.retain(|t| t.trip.allow_guests); day.trips.retain(|t| t.trip.allow_guests);
day day

View File

@@ -1,341 +1,36 @@
use std::ops::DerefMut; use chrono::{DateTime, Local, NaiveDateTime, TimeZone, Utc};
use chrono::NaiveDateTime;
use regex::Regex;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, SqlitePool};
use super::{planned::usertrip::UserTrip, role::Role, user::User}; #[derive(FromRow, Debug, Serialize, Deserialize)]
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
pub struct Notification { pub struct Notification {
pub id: i64, pub id: i64,
pub user_id: i64, pub user_id: i64,
pub message: String, pub message: String,
pub read_at: Option<NaiveDateTime>, pub read_at: NaiveDateTime,
pub created_at: NaiveDateTime,
pub category: String, pub category: String,
pub link: Option<String>,
pub action_after_reading: Option<String>,
} }
impl Notification { impl Notification {
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> { //pub async fn create(db: &SqlitePool, msg: String) -> bool {
sqlx::query_as!(Self, "SELECT id, user_id, message, read_at, created_at, category, link, action_after_reading FROM notification WHERE id like ?", id) // sqlx::query!("INSERT INTO log(msg) VALUES (?)", msg,)
.fetch_one(db) // .execute(db)
.await // .await
.ok() // .is_ok()
} //}
pub async fn create_with_tx(
db: &mut Transaction<'_, Sqlite>,
user: &User,
message: &str,
category: &str,
link: Option<&str>,
action_after_reading: Option<&str>,
) {
sqlx::query!(
"INSERT INTO notification(user_id, message, category, link, action_after_reading) VALUES (?, ?, ?, ?, ?)",
user.id,
message,
category,
link,
action_after_reading
)
.execute(db.deref_mut())
.await
.unwrap();
}
pub async fn create( async fn for_user(db: &SqlitePool, user: &User) -> Vec<Self> {
db: &SqlitePool, sqlx::query_as!(
user: &User, Log,
message: &str,
category: &str,
link: Option<&str>,
action_after_reading: Option<&str>,
) {
let mut tx = db.begin().await.unwrap();
Self::create_with_tx(&mut tx, user, message, category, link, action_after_reading).await;
tx.commit().await.unwrap();
}
pub async fn create_for_role_tx(
db: &mut Transaction<'_, Sqlite>,
role: &Role,
message: &str,
category: &str,
link: Option<&str>,
action_after_reading: Option<&str>,
) {
let users = User::all_with_role_tx(db, role).await;
for user in users {
Self::create_with_tx(db, &user, message, category, link, action_after_reading).await;
}
}
pub async fn create_for_role(
db: &SqlitePool,
role: &Role,
message: &str,
category: &str,
link: Option<&str>,
action_after_reading: Option<&str>,
) {
let mut tx = db.begin().await.unwrap();
Self::create_for_role_tx(&mut tx, role, message, category, link, action_after_reading)
.await;
tx.commit().await.unwrap();
}
pub async fn create_for_steering_people_tx(
db: &mut Transaction<'_, Sqlite>,
message: &str,
category: &str,
link: Option<&str>,
action_after_reading: Option<&str>,
) {
let cox = Role::find_by_name_tx(db, "cox").await.unwrap();
Self::create_for_role_tx(db, &cox, message, category, link, action_after_reading).await;
let bootsf = Role::find_by_name_tx(db, "Bootsführer").await.unwrap();
Self::create_for_role_tx(db, &bootsf, message, category, link, action_after_reading).await;
}
pub async fn create_for_steering_people(
db: &SqlitePool,
message: &str,
category: &str,
link: Option<&str>,
action_after_reading: Option<&str>,
) {
let cox = Role::find_by_name(db, "cox").await.unwrap();
Self::create_for_role(db, &cox, message, category, link, action_after_reading).await;
let bootsf = Role::find_by_name(db, "Bootsführer").await.unwrap();
Self::create_for_role(db, &bootsf, message, category, link, action_after_reading).await;
}
pub async fn for_user(db: &SqlitePool, user: &User) -> Vec<Self> {
let rows = sqlx::query!(
" "
SELECT id, user_id, message, read_at, datetime(created_at, 'localtime') as created_at, category, link, action_after_reading FROM notification SELECT id, user_id, message, read_at, category
WHERE FROM notification
user_id = ? WHERE user_id = {}
AND ( ",
read_at IS NULL
OR read_at >= datetime('now', '-14 days')
)
AND created_at is not NULL
ORDER BY read_at DESC, created_at DESC;
",
user.id user.id
) )
.fetch_all(db) .fetch_all(db)
.await .await
.unwrap(); .unwrap()
rows.into_iter()
.map(|rec| Notification {
id: rec.id,
user_id: rec.user_id,
message: rec.message,
read_at: rec.read_at,
created_at: NaiveDateTime::parse_from_str(
&rec.created_at.unwrap(),
"%Y-%m-%d %H:%M:%S",
)
.unwrap(),
category: rec.category,
link: rec.link,
action_after_reading: rec.action_after_reading,
})
.collect()
}
pub async fn mark_read(self, db: &SqlitePool) {
sqlx::query!(
"UPDATE notification SET read_at=CURRENT_TIMESTAMP WHERE id=?",
self.id
)
.execute(db)
.await
.unwrap();
if let Some(action) = self.action_after_reading.as_ref() {
// User read notification about cancelled trip/event
let re = Regex::new(r"^remove_user_trip_with_trip_details_id:(\d+)$").unwrap();
if let Some(caps) = re.captures(action) {
if let Some(matched) = caps.get(1) {
if let Ok(number) = matched.as_str().parse::<i64>() {
if let Some(usertrip) =
UserTrip::find_by_userid_and_trip_detail_id(db, self.user_id, number)
.await
{
let _ = usertrip.self_delete(db).await;
}
}
}
}
// Cox read notification about cancelled event
let re = Regex::new(r"^remove_trip_by_event:(\d+)$").unwrap();
if let Some(caps) = re.captures(action) {
if let Some(matched) = caps.get(1) {
if let Ok(number) = matched.as_str().parse::<i32>() {
let _ = sqlx::query!(
"DELETE FROM trip WHERE cox_id = ? AND planned_event_id = ?",
self.user_id,
number
)
.execute(db)
.await
.unwrap();
}
}
}
}
}
pub(crate) async fn mark_all_read(db: &SqlitePool, user: &User) {
let notifications = Self::for_user(db, user).await;
for notification in notifications {
notification.mark_read(db).await;
}
}
pub(crate) async fn delete_by_action(db: &sqlx::Pool<Sqlite>, action: &str) {
sqlx::query!(
"DELETE FROM notification WHERE action_after_reading=? and read_at is null",
action
)
.execute(db)
.await
.unwrap();
}
pub(crate) async fn delete_by_link(db: &sqlx::Pool<Sqlite>, link: &str) {
let link = Some(link);
sqlx::query!("DELETE FROM notification WHERE link=?", link)
.execute(db)
.await
.unwrap();
}
}
#[cfg(test)]
mod test {
use crate::{
model::{
notification::Notification,
planned::{
event::{Event, EventUpdate, Registration},
trip::Trip,
tripdetails::{TripDetails, TripDetailsToAdd},
usertrip::UserTrip,
},
user::{EventUser, SteeringUser, User},
},
testdb,
};
use chrono::Local;
use sqlx::SqlitePool;
#[sqlx::test]
fn event_canceled() {
let pool = testdb!();
// Create event
let add_tripdetails = TripDetailsToAdd {
planned_starting_time: "10:00",
max_people: 4,
day: Local::now().date_naive().format("%Y-%m-%d").to_string(),
notes: None,
trip_type: None,
allow_guests: false,
};
let tripdetails_id = TripDetails::create(&pool, add_tripdetails).await;
let trip_details = TripDetails::find_by_id(&pool, tripdetails_id)
.await
.unwrap();
let user = EventUser::new(&pool, &User::find_by_id(&pool, 1).await.unwrap())
.await
.unwrap();
Event::create(&pool, &user, "new-event".into(), 2, false, &trip_details).await;
let event = Event::find_by_trip_details(&pool, trip_details.id)
.await
.unwrap();
// Rower + Cox joins
let rower = User::find_by_name(&pool, "rower").await.unwrap();
UserTrip::create(&pool, &rower, &trip_details, None)
.await
.unwrap();
let cox = SteeringUser::new(&pool, &User::find_by_name(&pool, "cox").await.unwrap())
.await
.unwrap();
Trip::new_join(&pool, &cox, &event).await.unwrap();
// Cancel Event
let cancel_update = EventUpdate {
name: &event.name,
planned_amount_cox: event.planned_amount_cox as i32,
max_people: -1,
notes: event.notes.as_deref(),
always_show: event.always_show,
is_locked: event.is_locked,
trip_type_id: None,
};
event.update(&pool, &user, &cancel_update).await;
// Rower received notification
let notifications = Notification::for_user(&pool, &rower).await;
let rower_notification = notifications[0].clone();
assert_eq!(rower_notification.category, "Absage Ausfahrt");
assert_eq!(
rower_notification.action_after_reading.as_deref(),
Some("remove_user_trip_with_trip_details_id:4")
);
// Cox received notification
let notifications = Notification::for_user(&pool, &cox.user).await;
let cox_notification = notifications[0].clone();
assert_eq!(cox_notification.category, "Absage Ausfahrt");
assert_eq!(
cox_notification.action_after_reading.as_deref(),
Some("remove_trip_by_event:2")
);
// Notification removed if cancellation is cancelled
let update = EventUpdate {
name: &event.name,
planned_amount_cox: event.planned_amount_cox as i32,
max_people: 3,
notes: event.notes.as_deref(),
always_show: event.always_show,
is_locked: event.is_locked,
trip_type_id: None,
};
event.update(&pool, &user, &update).await;
assert!(Notification::for_user(&pool, &rower).await.is_empty());
assert!(Notification::for_user(&pool, &cox.user).await.is_empty());
// Cancel event again
event.update(&pool, &user, &cancel_update).await;
// Rower is removed if notification is accepted
assert!(event.is_rower_registered(&pool, &rower).await);
rower_notification.mark_read(&pool).await;
assert!(!event.is_rower_registered(&pool, &rower).await);
// Cox is removed if notification is accepted
let registration = Registration::all_cox(&pool, event.id).await;
assert_eq!(registration.len(), 1);
assert_eq!(registration[0].name, "cox");
cox_notification.mark_read(&pool).await;
let registration = Registration::all_cox(&pool, event.id).await;
assert!(registration.is_empty());
} }
} }

View File

@@ -1,157 +0,0 @@
use std::io::Write;
use ics::{
ICalendar,
components::Property,
properties::{DtEnd, DtStart, Summary},
};
use sqlx::SqlitePool;
use crate::model::{
planned::{event::Event, trip::Trip},
user::User,
};
use chrono::{Duration, NaiveTime};
pub(crate) async fn get_personal_cal(db: &SqlitePool, user: &User) -> String {
let mut calendar = ICalendar::new("2.0", "ics-rs");
calendar.push(Property::new(
"X-WR-CALNAME",
"Donau Linz - Deine Ausfahrten",
));
let events = Event::all_with_user(db, user).await;
for event in events {
calendar.add_event(event.get_vevent(db).await);
}
let trips = Trip::all_with_user(db, user).await;
for trip in trips {
calendar.add_event(trip.get_vevent(db, user).await);
}
let mut buf = Vec::new();
write!(&mut buf, "{}", calendar).unwrap();
String::from_utf8(buf).unwrap()
}
impl Trip {
pub(crate) async fn get_vevent<'a>(self, db: &'a SqlitePool, user: &'a User) -> ics::Event<'a> {
let mut vevent =
ics::Event::new(format!("trip-{}@rudernlinz.at", self.id), "19900101T180000");
let time_str = self.planned_starting_time.replace(':', "");
let formatted_time = if time_str.len() == 3 {
format!("0{}", time_str)
} else {
time_str
};
vevent.push(DtStart::new(format!(
"{}T{}00",
self.day.replace('-', ""),
formatted_time
)));
let original_time = NaiveTime::parse_from_str(&self.planned_starting_time, "%H:%M")
.expect("Failed to parse time");
let long_trip = match self.trip_type(db).await {
Some(a) if a.name == "Lange Ausfahrt" => true,
_ => false,
};
let later_time = if long_trip {
original_time + Duration::hours(6)
} else {
original_time + Duration::hours(3)
};
if later_time > original_time {
// Check if no day-overflow
let time_three_hours_later = later_time.format("%H%M").to_string();
vevent.push(DtEnd::new(format!(
"{}T{}00",
self.day.replace('-', ""),
time_three_hours_later
)));
}
let mut name = String::new();
if self.is_cancelled() {
name.push_str("ABGESAGT");
if let Some(notes) = &self.notes {
if !notes.is_empty() {
name.push_str(&format!(" (Grund: {notes})"))
}
}
name.push_str("! :-( ");
}
if self.cox_id == user.id {
name.push_str("Ruderausfahrt (selber ausgeschrieben)");
} else {
name.push_str(&format!("Ruderausfahrt mit {} ", self.cox_name));
}
vevent.push(Summary::new(name));
vevent
}
}
impl Event {
pub(crate) async fn get_vevent(self, db: &SqlitePool) -> ics::Event {
let mut vevent = ics::Event::new(
format!("event-{}@rudernlinz.at", self.id),
"19900101T180000",
);
let time_str = self.planned_starting_time.replace(':', "");
let formatted_time = if time_str.len() == 3 {
format!("0{}", time_str)
} else {
time_str.clone() // TODO: remove again
};
vevent.push(DtStart::new(format!(
"{}T{}00",
self.day.replace('-', ""),
formatted_time
)));
let original_time = NaiveTime::parse_from_str(&self.planned_starting_time, "%H:%M")
.expect("Failed to parse time");
let long_trip = match self.trip_type(db).await {
Some(a) if a.name == "Lange Ausfahrt" => true,
_ => false,
};
let later_time = if long_trip {
original_time + Duration::hours(6)
} else {
original_time + Duration::hours(3)
};
if later_time > original_time {
// Check if no day-overflow
let time_three_hours_later = later_time.format("%H%M").to_string();
vevent.push(DtEnd::new(format!(
"{}T{}00",
self.day.replace('-', ""),
time_three_hours_later
)));
}
let tripdetails = self.trip_details(db).await;
let mut name = String::new();
if self.is_cancelled() {
name.push_str("ABGESAGT");
if let Some(notes) = &tripdetails.notes {
if !notes.is_empty() {
name.push_str(&format!(" (Grund: {notes})"))
}
}
name.push_str("! :-( ");
}
name.push_str(&format!("{} ", self.name));
if let Some(triptype) = tripdetails.triptype(db).await {
name.push_str(&format!("{} ", triptype.name))
}
vevent.push(Summary::new(name));
vevent
}
}

View File

@@ -1,104 +0,0 @@
use crate::model::{logbook::Logbook, stat::Stat, user::User};
use serde::Serialize;
#[derive(Serialize, PartialEq, Debug)]
pub(crate) enum Level {
None,
Bronze,
Silver,
Gold,
Diamond,
Done,
}
impl Level {
fn required_km(&self) -> i32 {
match self {
Level::Bronze => 40_000,
Level::Silver => 80_000,
Level::Gold => 100_000,
Level::Diamond => 200_000,
Level::Done => 0,
Level::None => 0,
}
}
fn next_level(km: i32) -> Self {
if km < Level::Bronze.required_km() {
Level::Bronze
} else if km < Level::Silver.required_km() {
Level::Silver
} else if km < Level::Gold.required_km() {
Level::Gold
} else if km < Level::Diamond.required_km() {
Level::Diamond
} else {
Level::Done
}
}
pub(crate) fn curr_level(km: i32) -> Self {
if km < Level::Bronze.required_km() {
Level::None
} else if km < Level::Silver.required_km() {
Level::Bronze
} else if km < Level::Gold.required_km() {
Level::Silver
} else if km < Level::Diamond.required_km() {
Level::Gold
} else {
Level::Diamond
}
}
pub(crate) fn desc(&self) -> &str {
match self {
Level::Bronze => "Bronze",
Level::Silver => "Silber",
Level::Gold => "Gold",
Level::Diamond => "Diamant",
Level::Done => "",
Level::None => "-",
}
}
}
#[derive(Serialize)]
pub(crate) struct Next {
level: Level,
desc: String,
missing_km: i32,
required_km: i32,
rowed_km: i32,
}
impl Next {
pub(crate) fn new(rowed_km: i32) -> Self {
let level = Level::next_level(rowed_km);
let required_km = level.required_km();
let missing_km = required_km - rowed_km;
Self {
desc: level.desc().to_string(),
level,
missing_km,
required_km,
rowed_km,
}
}
}
pub(crate) async fn new_level_with_last_log(
db: &mut sqlx::Transaction<'_, sqlx::Sqlite>,
user: &User,
) -> Option<String> {
let rowed_km = Stat::total_km_tx(db, user).await.rowed_km;
if let Some(last_logbookentry) = Logbook::completed_with_user_tx(db, user).await.last() {
let last_trip_km = last_logbookentry.logbook.distance_in_km.unwrap();
if Level::curr_level(rowed_km) != Level::curr_level(rowed_km - last_trip_km as i32) {
return Some(Level::curr_level(rowed_km).desc().to_string());
}
}
None
}

View File

@@ -1,52 +0,0 @@
use chrono::{Datelike, Local};
use equatorprice::Level;
use serde::Serialize;
use sqlx::SqlitePool;
use super::{logbook::Logbook, stat::Stat, user::User};
pub(crate) mod cal;
pub(crate) mod equatorprice;
pub(crate) mod rowingbadge;
#[derive(Serialize)]
pub(crate) struct Achievements {
pub(crate) equatorprice: equatorprice::Next,
pub(crate) curr_equatorprice_name: String,
pub(crate) new_equatorprice_this_season: bool,
pub(crate) rowingbadge: Option<rowingbadge::Status>,
pub(crate) all_time_km: i32,
pub(crate) year_first_mentioned: Option<i32>,
pub(crate) year_last_mentioned: Option<i32>,
}
impl Achievements {
pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Self {
let rowed_km = Stat::total_km(db, user).await.rowed_km;
let rowed_km_this_season = if Local::now().month() == 1 {
Stat::person(db, Some(Local::now().year() - 1), user)
.await
.rowed_km
+ Stat::person(db, Some(Local::now().year()), user)
.await
.rowed_km
} else {
Stat::person(db, Some(Local::now().year()), user)
.await
.rowed_km
};
let new_equatorprice_this_season =
Level::curr_level(rowed_km) != Level::curr_level(rowed_km - rowed_km_this_season);
Self {
equatorprice: equatorprice::Next::new(rowed_km),
curr_equatorprice_name: equatorprice::Level::curr_level(rowed_km).desc().to_string(),
new_equatorprice_this_season,
rowingbadge: rowingbadge::Status::for_user(db, user).await,
all_time_km: rowed_km,
year_first_mentioned: Logbook::year_first_logbook_entry(db, user).await,
year_last_mentioned: Logbook::year_last_logbook_entry(db, user).await,
}
}
}

View File

@@ -1,223 +0,0 @@
use std::cmp;
use chrono::{Datelike, Local, NaiveDate};
use serde::Serialize;
use sqlx::{Acquire, Sqlite, SqlitePool, Transaction};
use crate::model::{
logbook::{Filter, Logbook, LogbookWithBoatAndRowers},
stat::Stat,
user::User,
};
enum AgeBracket {
Till14,
From14Till18,
From19Till30,
From31Till60,
From61Till75,
From76,
}
impl AgeBracket {
fn cat(&self) -> &str {
match self {
AgeBracket::Till14 => "Schülerinnen und Schüler bis 14 Jahre",
AgeBracket::From14Till18 => "Juniorinnen und Junioren, Para-Ruderer bis 18 Jahre",
AgeBracket::From19Till30 => "Frauen und Männer, Para-Ruderer bis 30 Jahre",
AgeBracket::From31Till60 => "Frauen und Männer, Para-Ruderer von 31 bis 60 Jahre",
AgeBracket::From61Till75 => "Frauen und Männer, Para-Ruderer von 61 bis 75 Jahre",
AgeBracket::From76 => "Frauen und Männer, Para-Ruderer ab 76 Jahre",
}
}
fn dist_in_km(&self) -> i32 {
match self {
AgeBracket::Till14 => 500,
AgeBracket::From14Till18 => 1000,
AgeBracket::From19Till30 => 1200,
AgeBracket::From31Till60 => 1000,
AgeBracket::From61Till75 => 800,
AgeBracket::From76 => 600,
}
}
fn required_dist_multi_day_in_km(&self) -> i32 {
match self {
AgeBracket::Till14 => 60,
AgeBracket::From14Till18 => 60,
AgeBracket::From19Till30 => 80,
AgeBracket::From31Till60 => 80,
AgeBracket::From61Till75 => 80,
AgeBracket::From76 => 80,
}
}
fn required_dist_single_day_in_km(&self) -> i32 {
match self {
AgeBracket::Till14 => 30,
AgeBracket::From14Till18 => 30,
AgeBracket::From19Till30 => 40,
AgeBracket::From31Till60 => 40,
AgeBracket::From61Till75 => 40,
AgeBracket::From76 => 40,
}
}
}
impl TryFrom<&User> for AgeBracket {
type Error = String;
fn try_from(value: &User) -> Result<Self, Self::Error> {
let Some(birthdate) = value.birthdate.clone() else {
return Err("User has no birthdate".to_string());
};
let Ok(birthdate) = NaiveDate::parse_from_str(&birthdate, "%Y-%m-%d") else {
return Err("Birthdate in wrong format...".to_string());
};
let today = Local::now().date_naive();
let age = today.year() - birthdate.year();
if age <= 14 {
Ok(AgeBracket::Till14)
} else if age <= 18 {
Ok(AgeBracket::From14Till18)
} else if age <= 30 {
Ok(AgeBracket::From19Till30)
} else if age <= 60 {
Ok(AgeBracket::From31Till60)
} else if age <= 75 {
Ok(AgeBracket::From61Till75)
} else {
Ok(AgeBracket::From76)
}
}
}
#[derive(Serialize)]
pub(crate) struct Status {
pub(crate) year: i32,
rowed_km: i32,
category: String,
required_km: i32,
missing_km: i32,
multi_day_trips_over_required_distance: Vec<LogbookWithBoatAndRowers>,
multi_day_trips_required_distance: i32,
single_day_trips_over_required_distance: Vec<LogbookWithBoatAndRowers>,
single_day_trips_required_distance: i32,
achieved: bool,
}
impl Status {
fn calc(
agebracket: &AgeBracket,
rowed_km: i32,
single_day_trips_over_required_distance: usize,
multi_day_trips_over_required_distance: usize,
year: i32,
) -> Self {
let category = agebracket.cat().to_string();
let required_km = agebracket.dist_in_km();
let missing_km = cmp::max(required_km - rowed_km, 0);
let achieved = missing_km == 0
&& (multi_day_trips_over_required_distance >= 1
|| single_day_trips_over_required_distance >= 2);
Self {
year,
rowed_km,
category,
required_km,
missing_km,
multi_day_trips_over_required_distance: vec![],
single_day_trips_over_required_distance: vec![],
multi_day_trips_required_distance: agebracket.required_dist_multi_day_in_km(),
single_day_trips_required_distance: agebracket.required_dist_single_day_in_km(),
achieved,
}
}
pub(crate) async fn for_user_tx(db: &mut Transaction<'_, Sqlite>, user: &User) -> Option<Self> {
let Ok(agebracket) = AgeBracket::try_from(user) else {
return None;
};
let year = if Local::now().month() == 1 {
Local::now().year() - 1
} else {
Local::now().year()
};
let rowed_km = Stat::person_tx(db, Some(year), user).await.rowed_km;
let single_day_trips_over_required_distance =
Logbook::completed_wanderfahrten_with_user_over_km_in_year_tx(
db,
user,
agebracket.required_dist_single_day_in_km(),
year,
Filter::SingleDayOnly,
)
.await;
let multi_day_trips_over_required_distance =
Logbook::completed_wanderfahrten_with_user_over_km_in_year_tx(
db,
user,
agebracket.required_dist_multi_day_in_km(),
year,
Filter::MultiDayOnly,
)
.await;
let ret = Self::calc(
&agebracket,
rowed_km,
single_day_trips_over_required_distance.len(),
multi_day_trips_over_required_distance.len(),
year,
);
Some(Self {
multi_day_trips_over_required_distance,
single_day_trips_over_required_distance,
..ret
})
}
pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Option<Self> {
let mut tx = db.begin().await.unwrap();
let ret = Self::for_user_tx(&mut tx, user).await;
tx.commit().await.unwrap();
ret
}
pub(crate) async fn completed_with_last_log(
db: &mut Transaction<'_, Sqlite>,
user: &User,
) -> bool {
if let Some(status) = Self::for_user_tx(db, user).await {
// if user has agebracket...
if status.achieved {
// ... and has achieved the 'Fahrtenabzeichen'
let mut without_last = db.begin().await.unwrap();
let last = Logbook::completed_with_user_tx(&mut without_last, user).await;
let last = last.last().unwrap();
sqlx::query!("DELETE FROM logbook WHERE id=?", last.logbook.id)
.execute(&mut *without_last)
.await
.unwrap(); //Okay, because we can only create a Logbook of a valid id
let without_last_entry = Self::for_user_tx(&mut without_last, user).await.unwrap();
if !without_last_entry.achieved {
// ... and this wasn't the case before the last logentry
return true;
}
}
}
false
}
}

View File

@@ -1,542 +0,0 @@
use std::io::Write;
use chrono::NaiveDate;
use ics::ICalendar;
use serde::Serialize;
use sqlx::{FromRow, Row, SqlitePool};
use super::{tripdetails::TripDetails, triptype::TripType};
use crate::model::{
log::Log,
notification::Notification,
role::Role,
user::{EventUser, User},
};
/// DB structure of an event
#[derive(Serialize, Clone, FromRow, Debug, PartialEq)]
pub struct Event {
pub id: i64,
pub name: String,
pub(crate) planned_amount_cox: i64,
trip_details_id: i64,
pub planned_starting_time: String,
pub(crate) max_people: i64,
pub day: String,
pub notes: Option<String>,
pub allow_guests: bool,
trip_type_id: Option<i64>,
pub(crate) always_show: bool,
pub(crate) is_locked: bool,
}
#[derive(Serialize, Debug)]
pub struct EventWithDetails {
#[serde(flatten)]
pub event: Event,
trip_type: Option<TripType>,
tripdetails: TripDetails,
cox_needed: bool,
cancelled: bool,
cox: Vec<Registration>,
rower: Vec<Registration>,
}
//TODO: move to appropriate place
#[derive(Serialize, Debug)]
pub struct Registration {
pub name: String,
pub registered_at: String,
pub is_guest: bool,
pub is_real_guest: bool,
}
impl Registration {
pub async fn all_rower(db: &SqlitePool, trip_details_id: i64) -> Vec<Registration> {
sqlx::query(
&format!(
r#"
SELECT
(SELECT name FROM user WHERE user_trip.user_id = user.id) as "name?",
user_note,
user_id,
(SELECT created_at FROM user WHERE user_trip.user_id = user.id) as registered_at,
(SELECT EXISTS (SELECT 1 FROM user_role WHERE user_role.user_id = user_trip.user_id AND user_role.role_id = (SELECT id FROM role WHERE name = 'scheckbuch'))) as is_guest
FROM user_trip WHERE trip_details_id = {}
"#,trip_details_id),
)
.fetch_all(db)
.await
.unwrap()
.into_iter()
.map(|r|
Registration {
name: r.get::<Option<String>, usize>(0).or(r.get::<Option<String>, usize>(1)).unwrap(), //Ok, either name or user_note needs to be set
registered_at: r.get::<String,usize>(3),
is_guest: r.get::<bool, usize>(4),
is_real_guest: r.get::<Option<i64>, usize>(2).is_none(),
})
.collect()
}
pub async fn all_cox(db: &SqlitePool, event_id: i64) -> Vec<Registration> {
//TODO: switch to join
sqlx::query!(
"
SELECT
(SELECT name FROM user WHERE cox_id = id) as name,
(SELECT created_at FROM user WHERE cox_id = id) as registered_at
FROM trip WHERE planned_event_id = ?
",
event_id
)
.fetch_all(db)
.await
.unwrap()
.into_iter()
.map(|r| Registration {
name: r.name.unwrap(),
registered_at: r.registered_at.unwrap(),
is_guest: false,
is_real_guest: false,
})
.collect() //Okay, as Event can only be created with proper DB backing
}
}
#[derive(Debug)]
pub struct EventUpdate<'a> {
pub name: &'a str,
pub planned_amount_cox: i32,
pub max_people: i32,
pub notes: Option<&'a str>,
pub always_show: bool,
pub is_locked: bool,
pub trip_type_id: Option<i64>,
}
impl EventUpdate<'_> {
fn cancelled(&self) -> bool {
self.max_people == -1
}
}
impl Event {
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT
planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id, always_show, is_locked
FROM planned_event
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
WHERE planned_event.id like ?
",
id
)
.fetch_one(db)
.await
.ok()
}
pub(crate) async fn trip_type(&self, db: &SqlitePool) -> Option<TripType> {
if let Some(trip_type_id) = self.trip_type_id {
TripType::find_by_id(db, trip_type_id).await
} else {
None
}
}
pub async fn get_pinned_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<EventWithDetails> {
let mut events = Self::get_for_day(db, day).await;
events.retain(|e| e.event.always_show);
events
}
pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<EventWithDetails> {
let day = format!("{day}");
let events = sqlx::query_as!(
Event,
"SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, always_show, max_people, day, notes, allow_guests, trip_type_id, is_locked
FROM planned_event
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
WHERE day=?",
day
)
.fetch_all(db)
.await
.unwrap(); //TODO: fixme
let mut ret = Vec::new();
for event in events {
let cox = Registration::all_cox(db, event.id).await;
let mut trip_type = None;
if let Some(trip_type_id) = event.trip_type_id {
trip_type = TripType::find_by_id(db, trip_type_id).await;
}
let tripdetails = TripDetails::find_by_id(db, event.trip_details_id)
.await
.expect("db constraints");
ret.push(EventWithDetails {
cox_needed: event.planned_amount_cox > cox.len() as i64,
cox,
rower: Registration::all_rower(db, event.trip_details_id).await,
cancelled: tripdetails.cancelled(),
tripdetails,
event,
trip_type,
});
}
ret
}
pub async fn all(db: &SqlitePool) -> Vec<Event> {
sqlx::query_as!(
Event,
"SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, always_show, max_people, day, notes, allow_guests, trip_type_id, is_locked
FROM planned_event
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
)
.fetch_all(db)
.await
.unwrap() //TODO: fixme
}
pub async fn all_with_user(db: &SqlitePool, user: &User) -> Vec<Event> {
let mut ret = Vec::new();
let events = Self::all(db).await;
for event in events {
if event.is_rower_registered(db, user).await || event.is_cox_registered(db, user).await
{
ret.push(event);
}
}
ret
}
//TODO: add tests
pub async fn is_rower_registered(&self, db: &SqlitePool, user: &User) -> bool {
let is_rower = sqlx::query!(
"SELECT count(*) as amount
FROM user_trip
WHERE trip_details_id =
(SELECT trip_details_id FROM planned_event WHERE id = ?)
AND user_id = ?",
self.id,
user.id
)
.fetch_one(db)
.await
.unwrap(); //Okay, bc planned_event can only be created with proper DB backing
is_rower.amount > 0
}
pub async fn is_cox_registered(&self, db: &SqlitePool, user: &User) -> bool {
let is_rower = sqlx::query!(
"SELECT count(*) as amount
FROM trip
WHERE planned_event_id = ?
AND cox_id = ?",
self.id,
user.id
)
.fetch_one(db)
.await
.unwrap(); //Okay, bc planned_event can only be created with proper DB backing
is_rower.amount > 0
}
pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, always_show, max_people, day, notes, allow_guests, trip_type_id, is_locked
FROM planned_event
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
WHERE trip_details.id=?
",
tripdetails_id
)
.fetch_one(db)
.await
.ok()
}
async fn advertise(db: &SqlitePool, day: &str, planned_starting_time: &str, name: &str) {
let donau = Role::find_by_name(db, "Donau Linz").await.unwrap();
Notification::create_for_role(
db,
&donau,
&format!("Am {} um {} wurde ein neues Event angelegt: {} Wir freuen uns wenn du dabei mitmachst, die Anmeldung ist ab sofort offen :-)", day, planned_starting_time, name),
"Neues Event",
Some(&format!("/planned#{day}")),
None,
)
.await;
}
pub async fn create(
db: &SqlitePool,
user: &EventUser,
name: &str,
planned_amount_cox: i32,
always_show: bool,
trip_details: &TripDetails,
) {
if trip_details.always_show {
Self::advertise(
db,
&trip_details.day,
&trip_details.planned_starting_time,
name,
)
.await;
}
if always_show && !trip_details.always_show {
trip_details.set_always_show(db, true).await;
}
sqlx::query!(
"INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)",
name,
planned_amount_cox,
trip_details.id,
)
.execute(db)
.await
.unwrap(); //Okay, as TripDetails can only be created with proper DB backing
Log::create(
db,
format!(
"{} created event {} on {} at {}.",
user.user.name, name, trip_details.day, trip_details.planned_starting_time
),
)
.await;
}
//TODO: create unit test
pub async fn update(&self, db: &SqlitePool, user: &EventUser, update: &EventUpdate<'_>) {
sqlx::query!(
"UPDATE planned_event SET name = ?, planned_amount_cox = ? WHERE id = ?",
update.name,
update.planned_amount_cox,
self.id
)
.execute(db)
.await
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
let tripdetails = self.trip_details(db).await;
let was_already_cancelled = tripdetails.cancelled();
sqlx::query!(
"UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ?, trip_type_id = ? WHERE id = ?",
update.max_people,
update.notes,
update.always_show,
update.is_locked,
update.trip_type_id,
self.trip_details_id
)
.execute(db)
.await
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
Log::create(
db,
format!(
"{} updated the event {} on {} at {} from {:?} to {:?}",
user.user.name,
self.name,
tripdetails.day,
tripdetails.planned_starting_time,
self,
update
),
)
.await;
if !tripdetails.always_show && update.always_show {
Self::advertise(
db,
&tripdetails.day,
&tripdetails.planned_starting_time,
update.name,
)
.await;
}
if update.cancelled() && !was_already_cancelled {
let coxes = Registration::all_cox(db, self.id).await;
for user in coxes {
if let Some(user) = User::find_by_name(db, &user.name).await {
let notes = match update.notes {
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
_ => String::from(""),
};
Notification::create(
db,
&user,
&format!(
"Die Ausfahrt {} am {} um {} wurde abgesagt. {}",
self.name, self.day, self.planned_starting_time, notes
),
"Absage Ausfahrt",
None,
Some(&format!("remove_trip_by_event:{}", self.id)),
)
.await;
}
}
let rower = Registration::all_rower(db, self.trip_details_id).await;
for user in rower {
if let Some(user) = User::find_by_name(db, &user.name).await {
let notes = match update.notes {
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
_ => String::from(""),
};
Notification::create(
db,
&user,
&format!(
"Die Ausfahrt {} am {} um {} wurde abgesagt. {}",
self.name, self.day, self.planned_starting_time, notes
),
"Absage Ausfahrt",
None,
Some(&format!(
"remove_user_trip_with_trip_details_id:{}",
tripdetails.id
)),
)
.await;
}
}
}
if !update.cancelled() && was_already_cancelled {
Notification::delete_by_action(
db,
&format!("remove_user_trip_with_trip_details_id:{}", tripdetails.id),
)
.await;
Notification::delete_by_action(db, &format!("remove_trip_by_event:{}", self.id)).await;
}
}
pub async fn delete(&self, db: &SqlitePool) -> Result<(), String> {
if !Registration::all_rower(db, self.trip_details_id)
.await
.is_empty()
{
return Err(
"Event kann nicht gelöscht werden, weil mind. 1 Ruderer angemeldet ist.".into(),
);
}
if !Registration::all_cox(db, self.trip_details_id)
.await
.is_empty()
{
return Err(
"Event kann nicht gelöscht werden, weil mind. 1 Steuerperson angemeldet ist."
.into(),
);
}
sqlx::query!("DELETE FROM planned_event WHERE id = ?", self.id)
.execute(db)
.await
.unwrap(); //Okay, as Event can only be created with proper DB backing
Ok(())
}
pub fn is_cancelled(&self) -> bool {
self.max_people == -1
}
pub async fn get_ics_feed(db: &SqlitePool) -> String {
let mut calendar = ICalendar::new("2.0", "ics-rs");
let events = Event::all(db).await;
for event in events {
calendar.add_event(event.get_vevent(db).await);
}
let mut buf = Vec::new();
write!(&mut buf, "{}", calendar).unwrap();
String::from_utf8(buf).unwrap()
}
pub async fn trip_details(&self, db: &SqlitePool) -> TripDetails {
TripDetails::find_by_id(db, self.trip_details_id)
.await
.unwrap() //ok, not null in db
}
}
#[cfg(test)]
mod test {
use crate::{
model::{
planned::tripdetails::TripDetails,
user::{EventUser, User},
},
testdb,
};
use super::Event;
use chrono::Local;
use sqlx::SqlitePool;
#[sqlx::test]
fn test_get_day() {
let pool = testdb!();
let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
assert_eq!(res.len(), 1);
}
#[sqlx::test]
fn test_create() {
let pool = testdb!();
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
let admin = EventUser::new(&pool, &User::find_by_id(&pool, 1).await.unwrap())
.await
.unwrap();
Event::create(&pool, &admin, "new-event".into(), 2, false, &trip_details).await;
let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
assert_eq!(res.len(), 2);
}
#[sqlx::test]
fn test_delete() {
let pool = testdb!();
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
planned_event.delete(&pool).await.unwrap();
let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
assert_eq!(res.len(), 0);
}
#[sqlx::test]
fn test_ics() {
let pool = testdb!();
let today = Local::now().date_naive().format("%Y%m%d").to_string();
let actual = Event::get_ics_feed(&pool).await;
assert_eq!(
format!(
"BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:event-1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:{today}T100000\r\nDTEND:{today}T130000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"
),
actual
);
}
}

View File

@@ -1,19 +0,0 @@
//! This module contains everything for managing planned trips and events.
//! `Cox` can create trips, `EventUser` can create events. Rowers can join those.
/// Events can be created by everyone who has the `manage_events` role. They are used if multiple coxes are needed, e.g. for "Fetzenfahrt", "Anrudern", .... Additionally, events are shown in public calendar (e.g. on the website).
pub mod event;
/// Trips can be created by every cox. They are "simple", every-day trips.
pub mod trip;
/// Extracts the common data for both Trips and Events. Rower can register using this.
pub mod tripdetails;
/// Type of the trip
pub mod triptype;
/// Associative table between `User` and `TripDetails`. Its functionality should probably move into
/// those files.
// TODO: make this mod unnecessary
pub mod usertrip;

View File

@@ -1,79 +0,0 @@
use super::Trip;
use crate::model::{
log::Log,
notification::Notification,
planned::{tripdetails::TripDetails, triptype::TripType},
user::{ErgoUser, SteeringUser, User},
};
use sqlx::SqlitePool;
impl Trip {
/// Cox decides to create own trip.
pub async fn new_own(db: &SqlitePool, cox: &SteeringUser, trip_details: TripDetails) {
Self::perform_new(db, &cox.user, trip_details).await
}
/// ErgoUser decides to create ergo 'trip'. Returns false, if trip is not a ergo-session (and
/// thus User is not allowed to create such a trip)
pub async fn new_own_ergo(db: &SqlitePool, ergo: &ErgoUser, trip_details: TripDetails) -> bool {
if let Some(typ) = trip_details.triptype(db).await {
let allowed_type = TripType::find_by_id(db, 4).await.unwrap();
if typ == allowed_type {
Self::perform_new(db, &ergo.user, trip_details).await;
return true;
}
}
false
}
async fn perform_new(db: &SqlitePool, user: &User, trip_details: TripDetails) {
let _ = sqlx::query!(
"INSERT INTO trip (cox_id, trip_details_id) VALUES(?, ?)",
user.id,
trip_details.id
)
.execute(db)
.await;
Log::create(db, format!("{user} created a new trip: {trip_details}")).await;
Self::notify_trips_same_datetime(db, trip_details, user).await;
}
async fn notify_trips_same_datetime(db: &SqlitePool, trip_details: TripDetails, user: &User) {
let same_starting_datetime = TripDetails::find_by_startingdatetime(
db,
trip_details.day,
trip_details.planned_starting_time,
)
.await;
for notify in same_starting_datetime {
// don't notify oneself
if notify.id == trip_details.id {
continue;
}
// don't notify people who have cancelled their trip
if notify.cancelled() {
continue;
}
if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await {
let user_earlier_trip = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
Notification::create(
db,
&user_earlier_trip,
&format!(
"{user} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
trip.day, trip.planned_starting_time
),
"Neue Ausfahrt zur selben Zeit",
None,
None,
)
.await;
}
}
}
}

View File

@@ -1,697 +0,0 @@
use chrono::{Local, NaiveDate};
use serde::Serialize;
use sqlx::SqlitePool;
mod create;
use super::{
event::{Event, Registration},
tripdetails::TripDetails,
triptype::TripType,
usertrip::UserTrip,
};
use crate::model::{
log::Log,
notification::Notification,
user::{SteeringUser, User},
};
#[derive(Serialize, Clone, Debug)]
pub struct Trip {
pub id: i64,
pub cox_id: i64,
pub cox_name: String,
trip_details_id: Option<i64>,
pub planned_starting_time: String,
pub max_people: i64,
pub day: String,
pub notes: Option<String>,
pub allow_guests: bool,
trip_type_id: Option<i64>,
always_show: bool,
is_locked: bool,
}
#[derive(Serialize, Debug)]
pub struct TripWithDetails {
#[serde(flatten)]
pub trip: Trip,
pub rower: Vec<Registration>,
trip_type: Option<TripType>,
cancelled: bool,
}
pub struct TripUpdate<'a> {
pub cox: &'a User,
pub trip: &'a Trip,
pub max_people: i32,
pub notes: Option<&'a str>,
pub trip_type: Option<i64>, //TODO: Move to `TripType`
pub is_locked: bool,
}
impl TripUpdate<'_> {
fn cancelled(&self) -> bool {
self.max_people == -1
}
}
impl TripWithDetails {
pub async fn from(db: &SqlitePool, trip: Trip) -> Self {
let mut trip_type = None;
if let Some(trip_type_id) = trip.trip_type_id {
trip_type = TripType::find_by_id(db, trip_type_id).await;
}
Self {
rower: Registration::all_rower(db, trip.trip_details_id.unwrap()).await,
trip_type,
cancelled: trip.is_cancelled(),
trip,
}
}
}
impl Trip {
pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked
FROM trip
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
INNER JOIN user ON trip.cox_id = user.id
WHERE trip_details.id=?
",
tripdetails_id
)
.fetch_one(db)
.await
.ok()
}
pub(crate) async fn trip_type(&self, db: &SqlitePool) -> Option<TripType> {
if let Some(trip_type_id) = self.trip_type_id {
TripType::find_by_id(db, trip_type_id).await
} else {
None
}
}
pub async fn all(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(
Self,
"
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked
FROM trip
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
INNER JOIN user ON trip.cox_id = user.id
",
)
.fetch_all(db)
.await
.unwrap() //TODO: fixme
}
pub async fn all_with_user(db: &SqlitePool, user: &User) -> Vec<Self> {
let mut ret = Vec::new();
let trips = Self::all(db).await;
for trip in trips {
if user.id == trip.cox_id {
ret.push(trip.clone());
}
if let Some(trip_details_id) = trip.trip_details_id {
if UserTrip::find_by_userid_and_trip_detail_id(db, user.id, trip_details_id)
.await
.is_some()
{
ret.push(trip);
}
}
}
ret
}
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked
FROM trip
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
INNER JOIN user ON trip.cox_id = user.id
WHERE trip.id=?
",
id
)
.fetch_one(db)
.await
.ok()
}
/// Cox decides to help in a event.
pub async fn new_join(
db: &SqlitePool,
cox: &SteeringUser,
event: &Event,
) -> Result<(), CoxHelpError> {
if event.is_rower_registered(db, cox).await {
return Err(CoxHelpError::AlreadyRegisteredAsRower);
}
if event.trip_details(db).await.is_locked {
return Err(CoxHelpError::DetailsLocked);
}
if event.is_cancelled() {
return Err(CoxHelpError::CanceledEvent);
}
match sqlx::query!(
"INSERT INTO trip (cox_id, planned_event_id) VALUES(?, ?)",
cox.id,
event.id
)
.execute(db)
.await
{
Ok(_) => Ok(()),
Err(_) => Err(CoxHelpError::AlreadyRegisteredAsCox),
}
}
pub async fn get_for_today(db: &SqlitePool) -> Vec<TripWithDetails> {
let today = Local::now().date_naive();
Self::get_for_day(db, today).await
}
pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<TripWithDetails> {
let day = format!("{day}");
let trips = sqlx::query_as!(
Trip,
"
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked
FROM trip
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
INNER JOIN user ON trip.cox_id = user.id
WHERE day=?
",
day
)
.fetch_all(db)
.await
.unwrap(); //Okay, as Trip can only be created with proper DB backing
let mut ret = Vec::new();
for trip in trips {
ret.push(TripWithDetails::from(db, trip).await);
}
ret
}
/// Cox decides to update own trip.
pub async fn update_own(
db: &SqlitePool,
update: &TripUpdate<'_>,
) -> Result<(), TripUpdateError> {
if !update.trip.is_trip_from_user(update.cox.id) {
return Err(TripUpdateError::NotYourTrip);
}
if update.trip_type != Some(4) && !update.cox.allowed_to_steer(db).await {
return Err(TripUpdateError::TripTypeNotAllowed);
}
let Some(trip_details_id) = update.trip.trip_details_id else {
return Err(TripUpdateError::TripDetailsDoesNotExist); //TODO: Remove?
};
let tripdetails = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
let was_already_cancelled = tripdetails.cancelled();
let is_locked = if update.cancelled() {
false
} else {
update.is_locked
};
sqlx::query!(
"UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, is_locked = ? WHERE id = ?",
update.max_people,
update.notes,
update.trip_type,
is_locked,
trip_details_id
)
.execute(db)
.await
.unwrap(); //Okay, as trip_details can only be created with proper DB backing
if update.cancelled() && !was_already_cancelled {
let rowers = TripWithDetails::from(db, update.trip.clone()).await.rower;
for user in rowers {
if let Some(user) = User::find_by_name(db, &user.name).await {
let notes = match update.notes {
Some(n) if !n.is_empty() => format!("Grund der Absage: {n}"),
_ => String::from(""),
};
Notification::create(
db,
&user,
&format!(
"Die Ausfahrt von {} am {} um {} wurde abgesagt. {} Bitte gib Bescheid, dass du die Info erhalten hast indem du auf ✓ klickst.",
update.cox.name,
update.trip.day,
update.trip.planned_starting_time,
notes
),
"Absage Ausfahrt",
None,
Some(&format!(
"remove_user_trip_with_trip_details_id:{}",
trip_details_id
)),
)
.await;
}
}
} else {
Notification::delete_by_action(
db,
&format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
)
.await;
}
if !update.cancelled() && was_already_cancelled {
Notification::delete_by_action(
db,
&format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
)
.await;
}
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
trip_details.check_free_spaces(db).await;
Ok(())
}
pub async fn trip_details(&self, db: &SqlitePool) -> Option<TripDetails> {
if let Some(trip_details_id) = self.trip_type_id {
return TripDetails::find_by_id(db, trip_details_id).await;
}
None
}
pub async fn delete_by_planned_event(
db: &SqlitePool,
cox: &SteeringUser,
event: &Event,
) -> Result<(), TripHelpDeleteError> {
if event.trip_details(db).await.is_locked {
return Err(TripHelpDeleteError::DetailsLocked);
}
let affected_rows = sqlx::query!(
"DELETE FROM trip WHERE cox_id = ? AND planned_event_id = ?",
cox.id,
event.id
)
.execute(db)
.await
.unwrap()
.rows_affected();
if affected_rows == 0 {
return Err(TripHelpDeleteError::CoxNotHelping);
}
Ok(())
}
pub(crate) async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), TripDeleteError> {
let registered_rower = Registration::all_rower(db, self.trip_details_id.unwrap()).await;
if !registered_rower.is_empty() {
return Err(TripDeleteError::SomebodyAlreadyRegistered);
}
if !self.is_trip_from_user(user.id) && !user.has_role(db, "admin").await {
return Err(TripDeleteError::NotYourTrip);
}
Log::create(db, format!("{} deleted trip: {:#?}", user.name, self)).await;
sqlx::query!("DELETE FROM trip WHERE id = ?", self.id)
.execute(db)
.await
.unwrap(); //TODO: fixme
Ok(())
}
fn is_trip_from_user(&self, user_id: i64) -> bool {
self.cox_id == user_id
}
pub(crate) async fn toggle_always_show(&self, db: &SqlitePool) {
if let Some(trip_details) = self.trip_details_id {
let new_state = !self.always_show;
sqlx::query!(
"UPDATE trip_details SET always_show = ? WHERE id = ?",
new_state,
trip_details
)
.execute(db)
.await
.unwrap();
}
}
pub(crate) async fn get_pinned_for_day(
db: &sqlx::Pool<sqlx::Sqlite>,
day: NaiveDate,
) -> Vec<TripWithDetails> {
let mut trips = Self::get_for_day(db, day).await;
trips.retain(|e| e.trip.always_show);
trips
}
pub(crate) fn is_cancelled(&self) -> bool {
self.max_people == -1
}
}
#[derive(Debug)]
pub enum CoxHelpError {
AlreadyRegisteredAsRower,
AlreadyRegisteredAsCox,
DetailsLocked,
CanceledEvent,
}
#[derive(Debug, PartialEq)]
pub enum TripHelpDeleteError {
DetailsLocked,
CoxNotHelping,
}
#[derive(Debug, PartialEq)]
pub enum TripDeleteError {
SomebodyAlreadyRegistered,
NotYourTrip,
}
#[derive(Debug)]
pub enum TripUpdateError {
NotYourTrip,
TripDetailsDoesNotExist,
TripTypeNotAllowed,
}
#[cfg(test)]
mod test {
use crate::{
model::{
notification::Notification,
planned::{
event::Event,
trip::{self, TripDeleteError},
tripdetails::TripDetails,
usertrip::UserTrip,
},
user::{SteeringUser, User},
},
testdb,
};
use chrono::Local;
use sqlx::SqlitePool;
use super::Trip;
#[sqlx::test]
fn test_new_own() {
let pool = testdb!();
let cox = SteeringUser::new(
&pool,
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
)
.await
.unwrap();
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
Trip::new_own(&pool, &cox, trip_details).await;
assert!(Trip::find_by_id(&pool, 1).await.is_some());
}
#[sqlx::test]
fn test_notification_cox_if_same_datetime() {
let pool = testdb!();
let cox = SteeringUser::new(
&pool,
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
)
.await
.unwrap();
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
Trip::new_own(&pool, &cox, trip_details).await;
let cox2 = SteeringUser::new(
&pool,
&User::find_by_name(&pool, "cox2".into()).await.unwrap(),
)
.await
.unwrap();
let trip_details = TripDetails::find_by_id(&pool, 3).await.unwrap();
Trip::new_own(&pool, &cox2, trip_details).await;
let last_notification = &Notification::for_user(&pool, &cox).await[0];
assert!(
last_notification
.message
.starts_with("cox2 hat eine Ausfahrt zur selben Zeit")
);
}
#[sqlx::test]
fn test_get_day_cox_trip() {
let pool = testdb!();
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
let res = Trip::get_for_day(&pool, tomorrow).await;
assert_eq!(res.len(), 1);
}
#[sqlx::test]
fn test_new_succ_join() {
let pool = testdb!();
let cox = SteeringUser::new(
&pool,
&User::find_by_name(&pool, "cox2".into()).await.unwrap(),
)
.await
.unwrap();
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
assert!(Trip::new_join(&pool, &cox, &planned_event).await.is_ok());
}
#[sqlx::test]
fn test_new_failed_join_already_cox() {
let pool = testdb!();
let cox = SteeringUser::new(
&pool,
&User::find_by_name(&pool, "cox2".into()).await.unwrap(),
)
.await
.unwrap();
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();
assert!(Trip::new_join(&pool, &cox, &planned_event).await.is_err());
}
#[sqlx::test]
fn test_succ_update_own() {
let pool = testdb!();
let cox = SteeringUser::new(
&pool,
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
)
.await
.unwrap();
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
let update = trip::TripUpdate {
cox: &cox,
trip: &trip,
max_people: 10,
notes: None,
trip_type: None,
is_locked: false,
};
assert!(Trip::update_own(&pool, &update).await.is_ok());
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
assert_eq!(trip.max_people, 10);
}
#[sqlx::test]
fn test_succ_update_own_with_triptype() {
let pool = testdb!();
let cox = SteeringUser::new(
&pool,
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
)
.await
.unwrap();
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
let update = trip::TripUpdate {
cox: &cox,
trip: &trip,
max_people: 10,
notes: None,
trip_type: Some(1),
is_locked: false,
};
assert!(Trip::update_own(&pool, &update).await.is_ok());
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
assert_eq!(trip.max_people, 10);
assert_eq!(trip.trip_type_id, Some(1));
}
#[sqlx::test]
fn test_fail_update_own_not_your_trip() {
let pool = testdb!();
let cox = SteeringUser::new(
&pool,
&User::find_by_name(&pool, "cox2".into()).await.unwrap(),
)
.await
.unwrap();
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
let update = trip::TripUpdate {
cox: &cox,
trip: &trip,
max_people: 10,
notes: None,
trip_type: None,
is_locked: false,
};
assert!(Trip::update_own(&pool, &update).await.is_err());
assert_eq!(trip.max_people, 1);
}
#[sqlx::test]
fn test_succ_delete_by_planned_event() {
let pool = testdb!();
let cox = SteeringUser::new(
&pool,
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
)
.await
.unwrap();
let planned_event = Event::find_by_id(&pool, 1).await.unwrap();
Trip::new_join(&pool, &cox, &planned_event).await.unwrap();
//TODO: check why following assert fails
//assert!(Trip::find_by_id(&pool, 2).await.is_some());
Trip::delete_by_planned_event(&pool, &cox, &planned_event)
.await
.unwrap();
assert!(Trip::find_by_id(&pool, 2).await.is_none());
}
#[sqlx::test]
fn test_succ_delete() {
let pool = testdb!();
let cox = SteeringUser::new(
&pool,
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
)
.await
.unwrap();
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
trip.delete(&pool, &cox).await.unwrap();
assert!(Trip::find_by_id(&pool, 1).await.is_none());
}
#[sqlx::test]
fn test_fail_delete_diff_cox() {
let pool = testdb!();
let cox = SteeringUser::new(
&pool,
&User::find_by_name(&pool, "cox2".into()).await.unwrap(),
)
.await
.unwrap();
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
let result = trip
.delete(&pool, &cox)
.await
.expect_err("It should not be possible to delete trips from others");
let expected = TripDeleteError::NotYourTrip;
assert_eq!(result, expected);
}
#[sqlx::test]
fn test_fail_delete_someone_registered() {
let pool = testdb!();
let cox = SteeringUser::new(
&pool,
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
)
.await
.unwrap();
let trip = Trip::find_by_id(&pool, 1).await.unwrap();
let trip_details = TripDetails::find_by_id(&pool, trip.trip_details_id.unwrap())
.await
.unwrap();
let user = User::find_by_name(&pool, "rower".into()).await.unwrap();
UserTrip::create(&pool, &user, &trip_details, None)
.await
.unwrap();
let result = trip
.delete(&pool, &cox)
.await
.expect_err("It should not be possible to delete trips if somebody already registered");
let expected = TripDeleteError::SomebodyAlreadyRegistered;
assert_eq!(result, expected);
}
}

323
src/model/planned_event.rs Normal file
View File

@@ -0,0 +1,323 @@
use std::io::Write;
use chrono::NaiveDate;
use ics::{
properties::{DtStart, Summary},
Event, ICalendar,
};
use serde::Serialize;
use sqlx::{FromRow, SqlitePool, Row};
use super::{tripdetails::TripDetails, triptype::TripType, user::User};
#[derive(Serialize, Clone, FromRow, Debug, PartialEq)]
pub struct PlannedEvent {
pub id: i64,
pub name: String,
planned_amount_cox: i64,
trip_details_id: i64,
pub planned_starting_time: String,
max_people: i64,
pub day: String,
pub notes: Option<String>,
pub allow_guests: bool,
trip_type_id: Option<i64>,
always_show: bool,
is_locked: bool,
}
#[derive(Serialize, Debug)]
pub struct PlannedEventWithUserAndTriptype {
#[serde(flatten)]
pub planned_event: PlannedEvent,
trip_type: Option<TripType>,
cox_needed: bool,
cox: Vec<Registration>,
rower: Vec<Registration>,
}
//TODO: move to appropriate place
#[derive(Serialize, Debug)]
pub struct Registration {
pub name: String,
pub registered_at: String,
pub is_guest: bool,
pub is_real_guest: bool,
}
impl Registration {
pub async fn all_rower(db: &SqlitePool, trip_details_id: i64) -> Vec<Registration> {
sqlx::query(
&format!(
r#"
SELECT
(SELECT name FROM user WHERE user_trip.user_id = user.id) as "name?",
user_note,
user_id,
(SELECT created_at FROM user WHERE user_trip.user_id = user.id) as registered_at,
(SELECT EXISTS (SELECT 1 FROM user_role WHERE user_role.user_id = user_trip.user_id AND user_role.role_id = (SELECT id FROM role WHERE name = 'scheckbuch'))) as is_guest
FROM user_trip WHERE trip_details_id = {}
"#,trip_details_id),
)
.fetch_all(db)
.await
.unwrap()
.into_iter()
.map(|r|
Registration {
name: r.get::<Option<String>, usize>(0).or(r.get::<Option<String>, usize>(1)).unwrap(), //Ok, either name or user_note needs to be set
registered_at: r.get::<String,usize>(3),
is_guest: r.get::<bool, usize>(4),
is_real_guest: r.get::<Option<i64>, usize>(2).is_none(),
})
.collect()
}
pub async fn all_cox(db: &SqlitePool, trip_details_id: i64) -> Vec<Registration> {
//TODO: switch to join
sqlx::query!(
"
SELECT
(SELECT name FROM user WHERE cox_id = id) as name,
(SELECT created_at FROM user WHERE cox_id = id) as registered_at
FROM trip WHERE planned_event_id = ?
",
trip_details_id
)
.fetch_all(db)
.await
.unwrap()
.into_iter()
.map(|r| Registration {
name: r.name,
registered_at: r.registered_at,
is_guest: false,
is_real_guest: false,
})
.collect() //Okay, as PlannedEvent can only be created with proper DB backing
}
}
impl PlannedEvent {
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT
planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id, always_show, is_locked
FROM planned_event
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
WHERE planned_event.id like ?
",
id
)
.fetch_one(db)
.await
.ok()
}
pub async fn get_pinned_for_day(
db: &SqlitePool,
day: NaiveDate,
) -> Vec<PlannedEventWithUserAndTriptype> {
let mut events = Self::get_for_day(db, day).await;
events.retain(|e| e.planned_event.always_show);
events
}
pub async fn get_for_day(
db: &SqlitePool,
day: NaiveDate,
) -> Vec<PlannedEventWithUserAndTriptype> {
let day = format!("{day}");
let events = sqlx::query_as!(
PlannedEvent,
"SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, always_show, max_people, day, notes, allow_guests, trip_type_id, is_locked
FROM planned_event
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id
WHERE day=?",
day
)
.fetch_all(db)
.await
.unwrap(); //TODO: fixme
let mut ret = Vec::new();
for event in events {
let cox = Registration::all_cox(db, event.id).await;
let mut trip_type = None;
if let Some(trip_type_id) = event.trip_type_id {
trip_type = TripType::find_by_id(db, trip_type_id).await;
}
ret.push(PlannedEventWithUserAndTriptype {
cox_needed: event.planned_amount_cox > cox.len() as i64,
cox,
rower: Registration::all_rower(db, event.trip_details_id).await,
planned_event: event,
trip_type,
});
}
ret
}
pub async fn all(db: &SqlitePool) -> Vec<PlannedEvent> {
sqlx::query_as!(
PlannedEvent,
"SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, always_show, max_people, day, notes, allow_guests, trip_type_id, is_locked
FROM planned_event
INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
)
.fetch_all(db)
.await
.unwrap() //TODO: fixme
}
//TODO: add tests
pub async fn is_rower_registered(&self, db: &SqlitePool, user: &User) -> bool {
let is_rower = sqlx::query!(
"SELECT count(*) as amount
FROM user_trip
WHERE trip_details_id =
(SELECT trip_details_id FROM planned_event WHERE id = ?)
AND user_id = ?",
self.id,
user.id
)
.fetch_one(db)
.await
.unwrap(); //Okay, bc planned_event can only be created with proper DB backing
is_rower.amount > 0
}
pub async fn create(
db: &SqlitePool,
name: &str,
planned_amount_cox: i32,
trip_details: TripDetails,
) {
sqlx::query!(
"INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)",
name,
planned_amount_cox,
trip_details.id,
)
.execute(db)
.await
.unwrap(); //Okay, as TripDetails can only be created with proper DB backing
}
//TODO: create unit test
pub async fn update(
&self,
db: &SqlitePool,
planned_amount_cox: i32,
max_people: i32,
notes: Option<&str>,
always_show: bool,
is_locked: bool,
) {
sqlx::query!(
"UPDATE planned_event SET planned_amount_cox = ? WHERE id = ?",
planned_amount_cox,
self.id
)
.execute(db)
.await
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
sqlx::query!(
"UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ? WHERE id = ?",
max_people,
notes,
always_show,
is_locked,
self.trip_details_id
)
.execute(db)
.await
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
}
pub async fn delete(&self, db: &SqlitePool) {
sqlx::query!("DELETE FROM planned_event WHERE id = ?", self.id)
.execute(db)
.await
.unwrap(); //Okay, as PlannedEvent can only be created with proper DB backing
}
pub async fn get_ics_feed(db: &SqlitePool) -> String {
let mut calendar = ICalendar::new("2.0", "ics-rs");
let events = PlannedEvent::all(db).await;
for event in events {
let mut vevent = Event::new(format!("{}@rudernlinz.at", event.id), "19900101T180000");
vevent.push(DtStart::new(format!(
"{}T{}00",
event.day.replace('-', ""),
event.planned_starting_time.replace(':', "")
)));
vevent.push(Summary::new(event.name));
calendar.add_event(vevent);
}
let mut buf = Vec::new();
write!(&mut buf, "{}", calendar).unwrap();
String::from_utf8(buf).unwrap()
}
pub async fn trip_details(&self, db: &SqlitePool) -> TripDetails {
TripDetails::find_by_id(db, self.trip_details_id)
.await
.unwrap() //ok, not null in db
}
}
#[cfg(test)]
mod test {
use crate::{model::tripdetails::TripDetails, testdb};
use super::PlannedEvent;
use chrono::NaiveDate;
use sqlx::SqlitePool;
#[sqlx::test]
fn test_get_day() {
let pool = testdb!();
let res =
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
assert_eq!(res.len(), 1);
}
#[sqlx::test]
fn test_create() {
let pool = testdb!();
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
PlannedEvent::create(&pool, "new-event".into(), 2, trip_details).await;
let res =
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
assert_eq!(res.len(), 2);
}
#[sqlx::test]
fn test_delete() {
let pool = testdb!();
let planned_event = PlannedEvent::find_by_id(&pool, 1).await.unwrap();
planned_event.delete(&pool).await;
let res =
PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
assert_eq!(res.len(), 0);
}
#[sqlx::test]
fn test_ics() {
let pool = testdb!();
let actual = PlannedEvent::get_ics_feed(&pool).await;
assert_eq!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:19700101T100000\r\nSUMMARY:test-planned-event\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", actual);
}
}

View File

@@ -1,191 +1,17 @@
use std::{cmp::Ordering, fmt::Display, ops::DerefMut}; use serde::Serialize;
use sqlx::{FromRow, SqlitePool};
use super::{activity::ActivityBuilder, user::AdminUser}; #[derive(FromRow, Serialize, Clone)]
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
#[derive(FromRow, Serialize, Clone, Deserialize, Debug)]
pub struct Role { pub struct Role {
pub(crate) id: i64, id: i64,
pub(crate) name: String, name: String,
pub(crate) formatted_name: Option<String>,
pub(crate) desc: Option<String>,
pub(crate) hide_in_lists: bool,
pub(crate) cluster: Option<String>,
}
// Implement PartialEq to compare roles based only on id
impl PartialEq for Role {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
}
}
// Implement Eq to indicate that equality is reflexive
impl Eq for Role {}
// Implement PartialOrd if you need to sort or compare roles
impl PartialOrd for Role {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.id.cmp(&other.id))
}
}
// Implement Ord if you need total ordering (for sorting)
impl Ord for Role {
fn cmp(&self, other: &Self) -> Ordering {
self.id.cmp(&other.id)
}
}
impl Display for Role {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if let Some(formatted_name) = &self.formatted_name {
write!(f, "{}", formatted_name)
} else {
write!(f, "{}", self.name)
}
}
} }
impl Role { impl Role {
pub async fn all(db: &SqlitePool) -> Vec<Role> { pub async fn all(db: &SqlitePool) -> Vec<Role> {
sqlx::query_as!( sqlx::query_as!(Role, "SELECT id, name FROM role")
Role, .fetch_all(db)
"SELECT id, name, formatted_name, desc, hide_in_lists, cluster FROM role" .await
) .unwrap()
.fetch_all(db)
.await
.unwrap()
}
pub async fn all_cluster(db: &SqlitePool, cluster: &str) -> Vec<Role> {
sqlx::query_as!(
Role,
r#"SELECT id,
CASE WHEN formatted_name IS NOT NULL AND formatted_name != ''
THEN formatted_name
ELSE name
END AS "name!: String",
'' as formatted_name,
desc,
hide_in_lists,
cluster
FROM role
WHERE cluster = ?"#,
cluster
)
.fetch_all(db)
.await
.unwrap()
}
pub async fn find_by_id(db: &SqlitePool, name: i32) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name, formatted_name, desc, hide_in_lists, cluster
FROM role
WHERE id like ?
",
name
)
.fetch_one(db)
.await
.ok()
}
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, name: i32) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name, formatted_name, desc, hide_in_lists, cluster
FROM role
WHERE id like ?
",
name
)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn find_by_name(db: &SqlitePool, name: &str) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name, formatted_name, desc, hide_in_lists, cluster
FROM role
WHERE name like ?
",
name
)
.fetch_one(db)
.await
.ok()
}
pub async fn find_by_name_tx(db: &mut Transaction<'_, Sqlite>, name: &str) -> Option<Self> {
sqlx::query_as!(
Self,
"
SELECT id, name, formatted_name, desc, hide_in_lists, cluster
FROM role
WHERE name like ?
",
name
)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn update(
&self,
db: &SqlitePool,
updated_by: &AdminUser,
formatted_name: &str,
desc: &str,
) -> Result<(), String> {
sqlx::query!(
"UPDATE role SET formatted_name=?, desc=? WHERE id=?",
formatted_name,
desc,
self.id
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
ActivityBuilder::new(&format!(
"{updated_by} hat Rolle {self} von {self:#?} auf FORMATTED_NAME={formatted_name}, DESC={desc} aktualisiert."
)).role(self).save(db).await;
Ok(())
}
pub async fn names_from_role(&self, db: &SqlitePool) -> Vec<String> {
let query = format!(
"SELECT u.name
FROM user u
JOIN user_role ur ON u.id = ur.user_id
JOIN role r ON ur.role_id = r.id
WHERE r.id = {} AND deleted=0;",
self.id
);
sqlx::query_scalar(&query).fetch_all(db).await.unwrap()
}
pub async fn mails_from_role(&self, db: &SqlitePool) -> Vec<String> {
let query = format!(
"SELECT u.mail
FROM user u
JOIN user_role ur ON u.id = ur.user_id
JOIN role r ON ur.role_id = r.id
WHERE r.id = {} AND deleted=0;",
self.id
);
sqlx::query_scalar(&query).fetch_all(db).await.unwrap()
} }
} }

View File

@@ -13,23 +13,16 @@ pub struct Rower {
impl Rower { impl Rower {
pub async fn for_log(db: &SqlitePool, log: &Logbook) -> Vec<User> { pub async fn for_log(db: &SqlitePool, log: &Logbook) -> Vec<User> {
let mut tx = db.begin().await.unwrap();
let ret = Self::for_log_tx(&mut tx, log).await;
tx.commit().await.unwrap();
ret
}
pub async fn for_log_tx(db: &mut Transaction<'_, Sqlite>, log: &Logbook) -> Vec<User> {
sqlx::query_as!( sqlx::query_as!(
User, User,
" "
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, phone, address, family_id, user_token SELECT id, name, pw, deleted, last_access, dob, weight, sex
FROM user FROM user
WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?) WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?)
", ",
log.id log.id
) )
.fetch_all(db.deref_mut()) .fetch_all(db)
.await .await
.unwrap() .unwrap()
} }

View File

@@ -1,119 +1,50 @@
use std::{collections::HashMap, ops::DerefMut};
use crate::model::user::User; use crate::model::user::User;
use chrono::Datelike; use chrono::Datelike;
use serde::Serialize; use serde::Serialize;
use sqlx::{FromRow, Row, Sqlite, SqlitePool, Transaction}; use sqlx::{FromRow, Row, SqlitePool};
use super::boat::Boat;
#[derive(Serialize, Clone)]
pub struct BoatStat {
pot_years: Vec<i32>,
boats: Vec<SingleBoatStat>,
}
#[derive(Serialize, Clone)]
pub struct SingleBoatStat {
name: String,
cat: String,
location: String,
owner: String,
years: HashMap<String, i32>,
}
impl BoatStat {
pub async fn get(db: &SqlitePool) -> BoatStat {
let mut years = Vec::new();
let mut boat_stats_map: HashMap<String, SingleBoatStat> = HashMap::new();
let rows = sqlx::query(
"
SELECT
boat.id,
location.name AS location,
CAST(strftime('%Y', COALESCE(arrival, 'now')) AS INTEGER) AS year,
CAST(SUM(COALESCE(distance_in_km, 0)) AS INTEGER) AS rowed_km
FROM
boat
LEFT JOIN
logbook ON boat.id = logbook.boat_id AND logbook.arrival IS NOT NULL
LEFT JOIN
location ON boat.location_id = location.id
WHERE
not boat.external
GROUP BY
boat.id, year
ORDER BY
boat.name, year DESC;
",
)
.fetch_all(db)
.await
.unwrap();
for row in rows {
let id: i32 = row.get("id");
let boat = Boat::find_by_id(db, id).await.unwrap();
let owner = if let Some(owner) = boat.owner(db).await {
owner.name
} else {
String::from("Verein")
};
let name = boat.name.clone();
let location: String = row.get("location");
let year: i32 = row.get("year");
if year == 0 {
continue; // Boat still on water
}
if !years.contains(&year) {
years.push(year);
}
let year: String = format!("{year}");
let cat = boat.cat();
let rowed_km: i32 = row.get("rowed_km");
let boat_stat = boat_stats_map
.entry(name.clone())
.or_insert(SingleBoatStat {
name,
location,
owner,
cat,
years: HashMap::new(),
});
boat_stat.years.insert(year, rowed_km);
}
BoatStat {
pot_years: years,
boats: boat_stats_map.into_values().collect(),
}
}
}
#[derive(FromRow, Serialize, Clone)] #[derive(FromRow, Serialize, Clone)]
pub struct Stat { pub struct Stat {
name: String, name: String,
pub(crate) amount_trips: i32, rowed_km: i32,
pub(crate) rowed_km: i32,
} }
impl Stat { impl Stat {
pub async fn boats(db: &SqlitePool, year: Option<i32>) -> Vec<Stat> {
let year = match year {
Some(year) => year,
None => chrono::Utc::now().year(),
};
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
sqlx::query(&format!(
"
SELECT (SELECT name FROM boat WHERE id=logbook.boat_id) as name, CAST(SUM(distance_in_km) AS INTEGER) AS rowed_km
FROM logbook
WHERE arrival LIKE '{year}-%'
GROUP BY boat_id
ORDER BY rowed_km DESC;
")
)
.fetch_all(db)
.await
.unwrap()
.into_iter()
.map(|row| Stat {
name: row.get("name"),
rowed_km: row.get("rowed_km"),
})
.collect()
}
pub async fn guest(db: &SqlitePool, year: Option<i32>) -> Stat { pub async fn guest(db: &SqlitePool, year: Option<i32>) -> Stat {
let year = match year { let year = match year {
Some(year) => year, Some(year) => year,
None => chrono::Local::now().year(), None => chrono::Utc::now().year(),
}; };
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server) //TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
// proper guests let rowed_km = sqlx::query(&format!(
let guests = sqlx::query(&format!(
" "
SELECT SUM((b.amount_seats - COALESCE(m.member_count, 0)) * l.distance_in_km) as total_guest_km, SELECT SUM((b.amount_seats - COALESCE(m.member_count, 0)) * l.distance_in_km) as total_guest_km
SUM(b.amount_seats - COALESCE(m.member_count, 0)) AS amount_trips
FROM logbook l FROM logbook l
JOIN boat b ON l.boat_id = b.id JOIN boat b ON l.boat_id = b.id
LEFT JOIN ( LEFT JOIN (
@@ -121,89 +52,35 @@ LEFT JOIN (
FROM rower FROM rower
GROUP BY logbook_id GROUP BY logbook_id
) m ON l.id = m.logbook_id ) m ON l.id = m.logbook_id
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND not b.external; WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND b.name != 'Externes Boot';
" "
)) ))
.fetch_one(db) .fetch_one(db)
.await .await
.unwrap(); .unwrap()
.get::<i64, usize>(0);
let guest_km: i32 = guests.get(0);
let guest_amount_trips: i32 = guests.get(1);
// e.g. scheckbücher
let guest_user = sqlx::query(&format!(
"
SELECT CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips
FROM user u
INNER JOIN rower r ON u.id = r.rower_id
INNER JOIN logbook l ON r.logbook_id = l.id
WHERE u.id NOT IN (
SELECT ur.user_id
FROM user_role ur
INNER JOIN role ro ON ur.role_id = ro.id
WHERE ro.name = 'Donau Linz'
)
AND l.distance_in_km IS NOT NULL
AND l.arrival LIKE '{year}-%'
AND u.name != 'Externe Steuerperson';
"
))
.fetch_one(db)
.await
.unwrap();
let guest_user_km: i32 = guest_user.get(0);
let guest_user_amount_trips: i32 = guest_user.get(1);
Stat { Stat {
name: "Gäste".into(), name: "Gäste".into(),
amount_trips: guest_amount_trips + guest_user_amount_trips, rowed_km: rowed_km as i32,
rowed_km: guest_km + guest_user_km,
} }
} }
pub async fn trips_people(db: &SqlitePool, year: Option<i32>) -> i32 {
let stats = Self::people(db, year).await;
let mut sum = 0;
for stat in stats {
sum += stat.amount_trips;
}
sum
}
pub async fn sum_people(db: &SqlitePool, year: Option<i32>) -> i32 {
let stats = Self::people(db, year).await;
let mut sum = 0;
for stat in stats {
sum += stat.rowed_km;
}
sum
}
pub async fn people(db: &SqlitePool, year: Option<i32>) -> Vec<Stat> { pub async fn people(db: &SqlitePool, year: Option<i32>) -> Vec<Stat> {
let year = match year { let year = match year {
Some(year) => year, Some(year) => year,
None => chrono::Local::now().year(), None => chrono::Utc::now().year(),
}; };
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server) //TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
sqlx::query(&format!( sqlx::query(&format!(
" "
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
FROM ( FROM user u
SELECT * FROM user
WHERE id IN (
SELECT user_id FROM user_role
JOIN role ON user_role.role_id = role.id
WHERE role.name = 'Donau Linz'
)
) u
INNER JOIN rower r ON u.id = r.rower_id INNER JOIN rower r ON u.id = r.rower_id
INNER JOIN logbook l ON r.logbook_id = l.id INNER JOIN logbook l ON r.logbook_id = l.id
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND u.name != 'Externe Steuerperson' WHERE u.is_guest = 0 AND l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%'
GROUP BY u.name GROUP BY u.name
ORDER BY rowed_km DESC, u.name; ORDER BY rowed_km DESC;
" "
)) ))
.fetch_all(db) .fetch_all(db)
@@ -212,85 +89,10 @@ ORDER BY rowed_km DESC, u.name;
.into_iter() .into_iter()
.map(|row| Stat { .map(|row| Stat {
name: row.get("name"), name: row.get("name"),
amount_trips: row.get("amount_trips"),
rowed_km: row.get("rowed_km"), rowed_km: row.get("rowed_km"),
}) })
.collect() .collect()
} }
pub async fn total_km_tx(db: &mut Transaction<'_, Sqlite>, user: &User) -> Stat {
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
let row = sqlx::query(&format!(
"
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips
FROM (
SELECT * FROM user
WHERE id={}
) u
INNER JOIN rower r ON u.id = r.rower_id
INNER JOIN logbook l ON r.logbook_id = l.id
WHERE l.distance_in_km IS NOT NULL;
",
user.id
))
.fetch_one(db.deref_mut())
.await
.unwrap();
Stat {
name: row.get("name"),
amount_trips: row.get("amount_trips"),
rowed_km: row.get("rowed_km"),
}
}
pub async fn total_km(db: &SqlitePool, user: &User) -> Stat {
let mut tx = db.begin().await.unwrap();
let ret = Self::total_km_tx(&mut tx, user).await;
tx.commit().await.unwrap();
ret
}
pub async fn person_tx(
db: &mut Transaction<'_, Sqlite>,
year: Option<i32>,
user: &User,
) -> Stat {
let year = match year {
Some(year) => year,
None => chrono::Local::now().year(),
};
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
let row = sqlx::query(&format!(
"
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips
FROM (
SELECT * FROM user
WHERE id={}
) u
INNER JOIN rower r ON u.id = r.rower_id
INNER JOIN logbook l ON r.logbook_id = l.id
WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%';
",
user.id
))
.fetch_one(db.deref_mut())
.await
.unwrap();
Stat {
name: row.get("name"),
amount_trips: row.get("amount_trips"),
rowed_km: row.get("rowed_km"),
}
}
pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat {
let mut tx = db.begin().await.unwrap();
let ret = Self::person_tx(&mut tx, year, user).await;
tx.commit().await.unwrap();
ret
}
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@@ -314,7 +116,7 @@ FROM (
LEFT JOIN LEFT JOIN
rower r ON l.id = r.logbook_id rower r ON l.id = r.logbook_id
WHERE WHERE
r.rower_id = {} l.shipmaster = {0} OR r.rower_id = {0}
GROUP BY GROUP BY
departure_date departure_date
) as subquery ) as subquery

View File

@@ -1,31 +0,0 @@
use std::ops::DerefMut;
use rocket::serde::{Deserialize, Serialize};
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
#[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)]
pub struct Trailer {
pub id: i64,
pub name: String,
}
impl Trailer {
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!(Self, "SELECT id, name FROM trailer WHERE id like ?", id)
.fetch_one(db)
.await
.ok()
}
pub async fn find_by_id_tx(db: &mut Transaction<'_, Sqlite>, id: i32) -> Option<Self> {
sqlx::query_as!(Self, "SELECT id, name FROM trailer WHERE id like ?", id)
.fetch_one(db.deref_mut())
.await
.ok()
}
pub async fn all(db: &SqlitePool) -> Vec<Self> {
sqlx::query_as!(Self, "SELECT id, name FROM trailer")
.fetch_all(db)
.await
.unwrap()
}
}

View File

@@ -1,233 +0,0 @@
use std::collections::HashMap;
use chrono::NaiveDate;
use chrono::NaiveDateTime;
use rocket::serde::{Deserialize, Serialize};
use sqlx::{FromRow, SqlitePool};
use super::log::Log;
use super::notification::Notification;
use super::role::Role;
use super::trailer::Trailer;
use super::user::User;
use crate::tera::trailerreservation::ReservationEditForm;
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct TrailerReservation {
pub id: i64,
pub trailer_id: i64,
pub start_date: NaiveDate,
pub end_date: NaiveDate,
pub time_desc: String,
pub usage: String,
pub user_id_applicant: i64,
pub user_id_confirmation: Option<i64>,
pub created_at: NaiveDateTime,
}
#[derive(FromRow, Debug, Serialize, Deserialize)]
pub struct TrailerReservationWithDetails {
#[serde(flatten)]
reservation: TrailerReservation,
trailer: Trailer,
user_applicant: User,
user_confirmation: Option<User>,
}
#[derive(Debug)]
pub struct TrailerReservationToAdd<'r> {
pub trailer: &'r Trailer,
pub start_date: NaiveDate,
pub end_date: NaiveDate,
pub time_desc: &'r str,
pub usage: &'r str,
pub user_applicant: &'r User,
}
impl TrailerReservation {
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
sqlx::query_as!(
Self,
"SELECT id, trailer_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
FROM trailer_reservation
WHERE id like ?",
id
)
.fetch_one(db)
.await
.ok()
}
pub async fn all_future(db: &SqlitePool) -> Vec<TrailerReservationWithDetails> {
let trailerreservations = sqlx::query_as!(
Self,
"
SELECT id, trailer_id, start_date, end_date, time_desc, usage, user_id_applicant, user_id_confirmation, created_at
FROM trailer_reservation
WHERE end_date >= CURRENT_DATE ORDER BY end_date
"
)
.fetch_all(db)
.await
.unwrap(); //TODO: fixme
let mut res = Vec::new();
for reservation in trailerreservations {
let user_confirmation = match reservation.user_id_confirmation {
Some(id) => {
let user = User::find_by_id(db, id as i32).await;
Some(user.unwrap())
}
None => None,
};
let user_applicant = User::find_by_id(db, reservation.user_id_applicant as i32)
.await
.unwrap();
let trailer = Trailer::find_by_id(db, reservation.trailer_id as i32)
.await
.unwrap();
res.push(TrailerReservationWithDetails {
reservation,
trailer,
user_applicant,
user_confirmation,
});
}
res
}
pub async fn all_future_with_groups(
db: &SqlitePool,
) -> HashMap<String, Vec<TrailerReservationWithDetails>> {
let mut grouped_reservations: HashMap<String, Vec<TrailerReservationWithDetails>> =
HashMap::new();
let reservations = Self::all_future(db).await;
for reservation in reservations {
let key = format!(
"{}-{}-{}-{}-{}",
reservation.reservation.start_date,
reservation.reservation.end_date,
reservation.reservation.time_desc,
reservation.reservation.usage,
reservation.user_applicant.name
);
grouped_reservations
.entry(key)
.or_default()
.push(reservation);
}
grouped_reservations
}
pub async fn create(
db: &SqlitePool,
trailerreservation: TrailerReservationToAdd<'_>,
) -> Result<(), String> {
if Self::trailer_reserved_between_dates(
db,
trailerreservation.trailer,
&trailerreservation.start_date,
&trailerreservation.end_date,
)
.await
{
return Err("Hänger in diesem Zeitraum bereits reserviert.".into());
}
Log::create(
db,
format!("New trailer reservation: {trailerreservation:?}"),
)
.await;
sqlx::query!(
"INSERT INTO trailer_reservation(trailer_id, start_date, end_date, time_desc, usage, user_id_applicant) VALUES (?,?,?,?,?,?)",
trailerreservation.trailer.id,
trailerreservation.start_date,
trailerreservation.end_date,
trailerreservation.time_desc,
trailerreservation.usage,
trailerreservation.user_applicant.id,
)
.execute(db)
.await
.map_err(|e| e.to_string())?;
let board =
User::all_with_role(db, &Role::find_by_name(db, "Vorstand").await.unwrap()).await;
for user in board {
let date = if trailerreservation.start_date == trailerreservation.end_date {
format!("am {}", trailerreservation.start_date)
} else {
format!(
"von {} bis {}",
trailerreservation.start_date, trailerreservation.end_date
)
};
Notification::create(
db,
&user,
&format!(
"{} hat eine neue Hängerreservierung für Hänger '{}' {} angelegt. Zeit: {}; Zweck: {}",
trailerreservation.user_applicant.name,
trailerreservation.trailer.name,
date,
trailerreservation.time_desc,
trailerreservation.usage
),
"Neue Hängerreservierung",
None,None
)
.await;
}
Ok(())
}
pub async fn trailer_reserved_between_dates(
db: &SqlitePool,
trailer: &Trailer,
start_date: &NaiveDate,
end_date: &NaiveDate,
) -> bool {
sqlx::query!(
"SELECT COUNT(*) AS reservation_count
FROM trailer_reservation
WHERE trailer_id = ?
AND start_date <= ? AND end_date >= ?;",
trailer.id,
end_date,
start_date
)
.fetch_one(db)
.await
.unwrap()
.reservation_count
> 0
}
pub async fn update(&self, db: &SqlitePool, data: ReservationEditForm) {
let time_desc = data.time_desc.trim();
let usage = data.usage.trim();
sqlx::query!(
"UPDATE trailer_reservation SET time_desc = ?, usage = ? where id = ?",
time_desc,
usage,
self.id
)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a User of a valid id
}
pub async fn delete(&self, db: &SqlitePool) {
sqlx::query!("DELETE FROM trailer_reservation WHERE id=?", self.id)
.execute(db)
.await
.unwrap(); //Okay, because we can only create a Boat of a valid id
}
}

Some files were not shown because too many files have changed in this diff Show More