Compare commits
10 Commits
main
...
6fdeee0e71
Author | SHA1 | Date | |
---|---|---|---|
6fdeee0e71 | |||
10d220773b | |||
aae206d8d9 | |||
0057a58eff | |||
ca51ba420c | |||
24a874f888 | |||
9bacbb9395 | |||
9881627186 | |||
91bf71cf00 | |||
f8f18def90 |
@ -11,7 +11,7 @@ env:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
container: git.hofer.link/philipp/ci-images:rust-latest
|
||||
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240419
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Run Test DB Script
|
||||
@ -37,7 +37,7 @@ jobs:
|
||||
|
||||
deploy-staging:
|
||||
runs-on: ubuntu-latest
|
||||
container: git.hofer.link/philipp/ci-images:rust-latest
|
||||
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240419
|
||||
needs: [test]
|
||||
if: github.ref == 'refs/heads/staging'
|
||||
steps:
|
||||
@ -63,12 +63,12 @@ jobs:
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
|
||||
scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing-staging/rot-updating
|
||||
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing-staging/rot-updating
|
||||
|
||||
scp -C staging-diff.sql $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
scp -C -r static $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
scp -C -r templates $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
scp -C -r svelte $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
scp staging-diff.sql $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
scp -r static $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
scp -r templates $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
scp -r svelte $SSH_USER@$SSH_HOST:/home/rowing-staging/
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rotstaging'
|
||||
ssh $SSH_USER@$SSH_HOST 'rm /home/rowing-staging/db.sqlite && cp /home/rowing/db.sqlite /home/rowing-staging/db.sqlite && mkdir -p /home/rowing-staging/svelte/build && mkdir -p /home/rowing-staging/data-ergo/thirty && mkdir -p /home/rowing-staging/data-ergo/dozen && sqlite3 /home/rowing-staging/db.sqlite < /home/rowing-staging/staging-diff.sql'
|
||||
ssh $SSH_USER@$SSH_HOST 'mv /home/rowing-staging/rot-updating /home/rowing-staging/rot'
|
||||
@ -80,7 +80,7 @@ jobs:
|
||||
|
||||
deploy-main:
|
||||
runs-on: ubuntu-latest
|
||||
container: git.hofer.link/philipp/ci-images:rust-latest
|
||||
container: git.hofer.link/ruderverein-donau-linz/rowing-ci:20240419
|
||||
needs: [test]
|
||||
if: github.ref == 'refs/heads/main'
|
||||
steps:
|
||||
@ -106,10 +106,10 @@ jobs:
|
||||
echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
|
||||
scp -C target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing/rot-updating
|
||||
scp -C -r static $SSH_USER@$SSH_HOST:/home/rowing/
|
||||
scp -C -r templates $SSH_USER@$SSH_HOST:/home/rowing/
|
||||
scp -C -r svelte $SSH_USER@$SSH_HOST:/home/rowing/
|
||||
scp target/$CARGO_TARGET/release/rot $SSH_USER@$SSH_HOST:/home/rowing/rot-updating
|
||||
scp -r static $SSH_USER@$SSH_HOST:/home/rowing/
|
||||
scp -r templates $SSH_USER@$SSH_HOST:/home/rowing/
|
||||
scp -r svelte $SSH_USER@$SSH_HOST:/home/rowing/
|
||||
ssh $SSH_USER@$SSH_HOST 'mkdir -p /home/rowing/svelte/build && mkdir -p /home/rowing/data-ergo/thirty && mkdir -p /home/rowing/data-ergo/dozen'
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl stop rot'
|
||||
ssh $SSH_USER@$SSH_HOST 'mv /home/rowing/rot-updating /home/rowing/rot'
|
||||
|
@ -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
|
1601
Cargo.lock
generated
1601
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
18
Cargo.toml
18
Cargo.toml
@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "rot"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
edition = "2021"
|
||||
|
||||
[features]
|
||||
default = ["rest", "rowing-tera" ]
|
||||
@ -9,25 +9,25 @@ rowing-tera = ["rocket_dyn_templates", "tera"]
|
||||
rest = []
|
||||
|
||||
[dependencies]
|
||||
rocket = { version = "0.5", features = ["secrets"]}
|
||||
rocket = { version = "0.5.0", features = ["secrets"]}
|
||||
rocket_dyn_templates = {version = "0.2", features = [ "tera" ], optional = true }
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
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"
|
||||
serde = { version = "1.0", features = [ "derive" ]}
|
||||
serde_json = "1.0"
|
||||
chrono = { version = "0.4", features = ["serde"]}
|
||||
chrono-tz = "0.10"
|
||||
tera = { version = "1.20", features = ["date-locale"], optional = true}
|
||||
chrono-tz = "0.9"
|
||||
tera = { version = "1.18", features = ["date-locale"], optional = true}
|
||||
ics = "0.5"
|
||||
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"
|
||||
itertools = "0.13"
|
||||
job_scheduler_ng = "2.0"
|
||||
ureq = { version = "2.9", features = ["json"] }
|
||||
regex = "1.10"
|
||||
urlencoding = "2.1"
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
|
25
Dockerfile
Normal file
25
Dockerfile
Normal file
@ -0,0 +1,25 @@
|
||||
# This dockerfile is used as basis for the CI jobs.
|
||||
# Process to renew it:
|
||||
# 0. Login to gitea docker registry: `docker login git.hofer.link`
|
||||
# 1. Build the image `docker build .`
|
||||
# 2. Tag the image: `docker tag <id> git.hofer.link/ruderverein-donau-linz/rowing-ci:<date>`
|
||||
# 3. Push the image: `docker push git.hofer.link/ruderverein-donau-linz/rowing-ci:<date>`
|
||||
|
||||
FROM rust:1.81.0
|
||||
|
||||
RUN apt-get update && apt-get install -y sqlite3
|
||||
|
||||
# nodejs
|
||||
RUN apt-get install -y curl && \
|
||||
curl -sL https://deb.nodesource.com/setup_22.x | bash - && \
|
||||
apt-get install -y nodejs
|
||||
|
||||
# playwright
|
||||
RUN npx playwright install --with-deps
|
||||
|
||||
# deployment
|
||||
RUN rustup target add x86_64-unknown-linux-musl
|
||||
RUN apt-get install -y -qq pkg-config sshpass musl musl-tools curl gnupg libssl-dev
|
||||
|
||||
# TEMPORARY act workaround (otherwise gitea cache is not working)
|
||||
RUN apt-get install -y zstd
|
287
LICENSE
287
LICENSE
@ -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.
|
@ -2,7 +2,7 @@
|
||||
secret_key = "/NtVGizglEoyoxBLzsRDWTy4oAG1qDw4J4O+CWJSv+fypD7W9sam8hUY4j90EZsbZk8wEradS5zBoWtWKi3k8w=="
|
||||
rss_key = "rss-key-for-ci"
|
||||
limits = { file = "10 MiB", data-form = "10 MiB"}
|
||||
smtp_pw = "my-smtp-password"
|
||||
smtp_pw = "8kIjlLH79Ky6D3j"
|
||||
usage_log_path = "./usage.txt"
|
||||
openweathermap_key = "openweather-key"
|
||||
openweathermap_key = "c8dab8f91b5b815d76e9879cbaecd8d5"
|
||||
wordpress_key = "pw-to-allow-sending-notifications"
|
||||
|
@ -1,6 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
rm -f db.sqlite
|
||||
touch db.sqlite
|
||||
sqlite3 db.sqlite < migration.sql
|
||||
sqlite3 db.sqlite < seeds_demo.sql
|
@ -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;`
|
@ -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 |
@ -23,8 +23,6 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
addRelationMagic(<HTMLElement>document.querySelector("body"));
|
||||
reloadPage();
|
||||
setCurrentdate(<HTMLInputElement>document.querySelector("#departure"));
|
||||
initDropdown();
|
||||
editReadOnlyField();
|
||||
});
|
||||
|
||||
function changeTheme() {
|
||||
@ -41,30 +39,6 @@ function changeTheme() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function editReadOnlyField() {
|
||||
const editBtns = document.querySelectorAll(
|
||||
'.edit-js'
|
||||
);
|
||||
if (editBtns) {
|
||||
Array.prototype.forEach.call(editBtns, (btn: HTMLButtonElement) => {
|
||||
btn.addEventListener("click", function () {
|
||||
let wrapper = btn.parentElement;
|
||||
let input = <HTMLInputElement> wrapper?.querySelector('input.input'),
|
||||
select = <HTMLSelectElement> wrapper?.querySelector('select.input'),
|
||||
attribute = 'readonly';
|
||||
|
||||
if(select) attribute = 'disabled';
|
||||
let element = input ? input : select;
|
||||
|
||||
element?.toggleAttribute(attribute);
|
||||
if(!element?.hasAttribute(attribute)) element?.focus();
|
||||
wrapper?.classList.toggle('editable');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/***
|
||||
* init javascript
|
||||
* 1) detect native color scheme or use set theme in local storage
|
||||
@ -821,21 +795,3 @@ function replaceStrings() {
|
||||
weekday.innerHTML = weekday.innerHTML.replace("Freitag", "Markttag");
|
||||
});
|
||||
}
|
||||
|
||||
function initDropdown() {
|
||||
const popoverTriggerList = document.querySelectorAll('[data-dropdown]');
|
||||
|
||||
popoverTriggerList.forEach((popoverTriggerEl: Element) => {
|
||||
const id = popoverTriggerEl.getAttribute('data-dropdown');
|
||||
|
||||
if (id) {
|
||||
const element = document.getElementById(id);
|
||||
if (element) {
|
||||
// Toggle visibility of the dropdown when clicked
|
||||
popoverTriggerEl.addEventListener('click', () => {
|
||||
element.classList.toggle('hidden');
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -28,8 +28,4 @@
|
||||
&[aria-pressed='true'] {
|
||||
@apply outline outline-2 outline-offset-2 outline-primary-600 bg-primary-100 text-primary-950;
|
||||
}
|
||||
|
||||
&-hidden {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
|
@ -4,8 +4,4 @@
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.h3 {
|
||||
@apply text-center text-xl uppercase tracking-wide font-bold text-primary-900 dark:text-white;
|
||||
}
|
@ -2,12 +2,3 @@
|
||||
border-top-left-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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
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;
|
||||
|
@ -10,12 +10,4 @@
|
||||
&-white {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { test, expect, Page } from "@playwright/test";
|
||||
|
||||
test("cox can create and delete trip", async ({ page }) => {
|
||||
await page.goto("/auth");
|
||||
@ -121,69 +121,18 @@ test.describe("cox can edit trips", () => {
|
||||
});
|
||||
|
||||
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("#sidebar")).toContainText(
|
||||
"Freie Plätze: 3",
|
||||
);
|
||||
await sharedPage.getByRole("spinbutton").click();
|
||||
await sharedPage.getByRole("spinbutton").fill("0");
|
||||
await sharedPage.getByRole("button", { name: "Speichern" }).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 () => {
|
||||
|
@ -115,7 +115,7 @@ test("Cox can start and finish trip", async ({ page }, testInfo) => {
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
|
||||
await page.goto("/log/show");
|
||||
await page.getByRole('link', { name: 'Joe' }).nth(1).click();
|
||||
await page.getByText('(cox2)').click();
|
||||
page.once("dialog", (dialog) => {
|
||||
dialog.accept().catch(() => {});
|
||||
});
|
||||
@ -208,6 +208,7 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
|
||||
|
||||
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('Ottensheim (25 km)');
|
||||
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
|
||||
|
||||
@ -224,7 +225,7 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
|
||||
await page.goto("/log/show");
|
||||
await page.getByRole('link', { name: 'Joe' }).nth(1).click();
|
||||
await page.getByText('(cox2)').click();
|
||||
page.once("dialog", (dialog) => {
|
||||
dialog.accept().catch(() => {});
|
||||
});
|
||||
@ -285,6 +286,7 @@ test("Cox can start and finish trip with cox steering only", async ({ page }, te
|
||||
|
||||
await page.goto('/log/show');
|
||||
await expect(page.locator('body')).toContainText('cox_only_steering_boat');
|
||||
await expect(page.locator('body')).toContainText('(cox2 - handgesteuert)');
|
||||
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
|
||||
|
||||
|
||||
@ -300,7 +302,7 @@ test("Cox can start and finish trip with cox steering only", async ({ page }, te
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
|
||||
await page.goto("/log/show");
|
||||
await page.getByRole("link", { name: "cox_only_steering_boat" }).click();
|
||||
await page.getByText('(cox2 - handgesteuert)').click();
|
||||
page.once("dialog", (dialog) => {
|
||||
dialog.accept().catch(() => {});
|
||||
});
|
||||
@ -369,7 +371,7 @@ test("Kiosk can start and finish trip in one stop", async ({ page }, testInfo) =
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
|
||||
await page.goto("/log/show");
|
||||
await page.getByRole('link', { name: 'Joe' }).nth(1).click();
|
||||
await page.getByText('(cox2)').click();
|
||||
page.once("dialog", (dialog) => {
|
||||
dialog.accept().catch(() => {});
|
||||
});
|
||||
|
@ -28,10 +28,7 @@ CREATE TABLE IF NOT EXISTS "family" (
|
||||
CREATE TABLE IF NOT EXISTS "role" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"name" text NOT NULL UNIQUE,
|
||||
"formatted_name" text,
|
||||
"desc" text,
|
||||
"cluster" text,
|
||||
"hide_in_lists" BOOLEAN NOT NULL DEFAULT false
|
||||
"cluster" text
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "user_role" (
|
||||
@ -225,15 +222,6 @@ CREATE TABLE IF NOT EXISTS "distance" (
|
||||
);
|
||||
|
||||
|
||||
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
|
||||
|
@ -9,7 +9,6 @@ Environment="ROCKET_ENV=prod"
|
||||
Environment="ROCKET_ADDRESS=127.0.0.1"
|
||||
Environment="ROCKET_PORT=8001"
|
||||
Environment="RUST_LOG=info"
|
||||
Environment="DATABASE_URL=sqliteL///home/stationslauf/db.sqlite"
|
||||
ExecStart=/home/rowing/rot
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
@ -13,7 +13,6 @@ 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);
|
||||
@ -53,7 +52,6 @@ INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('
|
||||
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" (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?', '🏅');
|
||||
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Lange Ausfahrt', 'Lange Ausfahrt!', 'Das ist eine lange Ausfahrt! Willst du wirklich teilnehmen?', '💪');
|
||||
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?', '⛱');
|
||||
|
104
seeds_demo.sql
104
seeds_demo.sql
@ -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?', '🏅');
|
||||
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Lange Ausfahrt', 'Lange Ausfahrt!', 'Das ist eine lange Ausfahrt! Willst du wirklich teilnehmen?', '💪');
|
||||
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?', '⛱');
|
||||
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);
|
||||
|
93
src/lib.rs
93
src/lib.rs
@ -1,7 +1,5 @@
|
||||
#![allow(clippy::blocks_in_conditions)]
|
||||
|
||||
use std::ops::Deref;
|
||||
|
||||
pub mod model;
|
||||
|
||||
#[cfg(feature = "rowing-tera")]
|
||||
@ -13,87 +11,16 @@ 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
pub(crate) const RENNRUDERBEITRAG: i32 = 11000;
|
||||
pub(crate) const BOAT_STORAGE: i32 = 4500;
|
||||
pub(crate) const FAMILY_TWO: i32 = 30000;
|
||||
pub(crate) const FAMILY_THREE_OR_MORE: i32 = 35000;
|
||||
pub(crate) const STUDENT_OR_PUPIL: i32 = 8000;
|
||||
pub(crate) const REGULAR: i32 = 22000;
|
||||
pub(crate) const UNTERSTUETZEND: i32 = 2500;
|
||||
pub(crate) const FOERDERND: i32 = 8500;
|
||||
pub(crate) const SCHECKBUCH: i32 = 3000;
|
||||
pub(crate) const EINSCHREIBGEBUEHR: i32 = 3000;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_export]
|
||||
|
@ -8,7 +8,7 @@ use rot::rest;
|
||||
use rot::tera;
|
||||
use rot::{scheduled, tera::Config};
|
||||
|
||||
use sqlx::{ConnectOptions, pool::PoolOptions, sqlite::SqliteConnectOptions};
|
||||
use sqlx::{pool::PoolOptions, sqlite::SqliteConnectOptions, ConnectOptions};
|
||||
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use itertools::Itertools;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::FromForm;
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
@ -9,7 +10,6 @@ use crate::model::boathouse::Boathouse;
|
||||
|
||||
use super::location::Location;
|
||||
use super::user::User;
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Eq, Hash, PartialEq, Clone)]
|
||||
pub struct Boat {
|
||||
@ -32,17 +32,6 @@ pub struct Boat {
|
||||
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)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum BoatDamage {
|
||||
@ -113,27 +102,12 @@ impl Boat {
|
||||
}
|
||||
|
||||
pub async fn shipmaster_allowed(&self, db: &SqlitePool, user: &User) -> bool {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
let ret = self.shipmaster_allowed_tx(&mut tx, user).await;
|
||||
tx.commit().await.unwrap();
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn shipmaster_allowed_tx(
|
||||
&self,
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
user: &User,
|
||||
) -> bool {
|
||||
if user.has_role_tx(db, "admin").await {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(owner_id) = self.owner {
|
||||
return owner_id == user.id;
|
||||
}
|
||||
|
||||
if user.has_role_tx(db, "Rennrudern").await {
|
||||
let ottensheim = Location::find_by_name_tx(db, "Ottensheim".into())
|
||||
if user.has_role(db, "Rennrudern").await {
|
||||
let ottensheim = Location::find_by_name(db, "Ottensheim".into())
|
||||
.await
|
||||
.unwrap();
|
||||
if self.location_id == ottensheim.id {
|
||||
@ -141,15 +115,27 @@ impl Boat {
|
||||
}
|
||||
}
|
||||
|
||||
if self.name == "Externes Boot" {
|
||||
if self.amount_seats == 1 {
|
||||
return true;
|
||||
}
|
||||
|
||||
user.has_role(db, "cox").await
|
||||
}
|
||||
|
||||
pub async fn shipmaster_allowed_tx(
|
||||
&self,
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
user: &User,
|
||||
) -> bool {
|
||||
if let Some(owner_id) = self.owner {
|
||||
return owner_id == user.id;
|
||||
}
|
||||
|
||||
if self.amount_seats == 1 {
|
||||
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 {
|
||||
@ -190,10 +176,8 @@ AND date('now') BETWEEN start_date AND end_date;",
|
||||
"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)
|
||||
format!("{}x", self.amount_seats)
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,16 +257,58 @@ ORDER BY
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
if user.has_role(db, "admin").await {
|
||||
return Self::all(db).await;
|
||||
}
|
||||
let mut boats = if user.has_role(db, "cox").await {
|
||||
sqlx::query_as!(
|
||||
Boat,
|
||||
"
|
||||
SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
FROM boat
|
||||
WHERE (owner is null or owner = ?) AND deleted = 0
|
||||
ORDER BY amount_seats DESC
|
||||
",
|
||||
user.id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.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, deleted, convert_handoperated_possible
|
||||
FROM boat
|
||||
WHERE (owner = ? OR (owner is null and amount_seats = 1)) AND deleted = 0
|
||||
ORDER BY amount_seats DESC
|
||||
",
|
||||
user.id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap() //TODO: fixme
|
||||
};
|
||||
|
||||
filtered_boats
|
||||
if user.has_role(db, "Rennrudern").await {
|
||||
let ottensheim = Location::find_by_name(db, "Ottensheim".into())
|
||||
.await
|
||||
.unwrap();
|
||||
let boats_in_ottensheim = sqlx::query_as!(
|
||||
Boat,
|
||||
"SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external, deleted, convert_handoperated_possible
|
||||
FROM boat
|
||||
WHERE (owner is null and location_id = ?) AND deleted = 0
|
||||
ORDER BY amount_seats DESC
|
||||
",ottensheim.id)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
boats.extend(boats_in_ottensheim.into_iter());
|
||||
}
|
||||
let boats = boats.into_iter().unique().collect();
|
||||
|
||||
Self::boats_to_details(db, boats).await
|
||||
}
|
||||
|
||||
pub async fn all_at_location(db: &SqlitePool, location: String) -> Vec<BoatWithDetails> {
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::model::{boat::Boat, user::User};
|
||||
use chrono::NaiveDateTime;
|
||||
use rocket::FromForm;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::FromForm;
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::log::Log;
|
||||
@ -136,7 +136,8 @@ ORDER BY created_at DESC
|
||||
.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 cox = Role::find_by_name(db, "cox").await.unwrap();
|
||||
Notification::create_for_role(db, &cox, &format!("Liebe Steuerberechtigte, bitte beachten, dass {} bis auf weiteres aufgrund von Reparaturarbeiten gesperrt ist.", boat.name), "Boot gesperrt", None, None).await;
|
||||
}
|
||||
|
||||
let technicals =
|
||||
|
@ -1,100 +1,12 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use crate::{
|
||||
model::{log::Log, user::AllowedToUpdateBoathouse},
|
||||
tera::board::boathouse::FormBoathouseToAdd,
|
||||
};
|
||||
use crate::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,
|
||||
@ -105,7 +17,54 @@ pub struct Boathouse {
|
||||
}
|
||||
|
||||
impl Boathouse {
|
||||
pub async fn get(db: &SqlitePool) -> BoathouseAisles {
|
||||
pub async fn get(db: &SqlitePool) -> HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 12]>> {
|
||||
let mut ret: HashMap<&str, HashMap<&str, [Option<(i64, Boat)>; 12]>> = HashMap::new();
|
||||
|
||||
let mut mountain = HashMap::new();
|
||||
mountain.insert(
|
||||
"mountain",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
mountain.insert(
|
||||
"water",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
ret.insert("mountain-aisle", mountain);
|
||||
|
||||
let mut middle = HashMap::new();
|
||||
middle.insert(
|
||||
"mountain",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
middle.insert(
|
||||
"water",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
ret.insert("middle-aisle", middle);
|
||||
|
||||
let mut water = HashMap::new();
|
||||
water.insert(
|
||||
"mountain",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
water.insert(
|
||||
"water",
|
||||
[
|
||||
None, None, None, None, None, None, None, None, None, None, None, None,
|
||||
],
|
||||
);
|
||||
ret.insert("water-aisle", water);
|
||||
|
||||
let boathouses = sqlx::query_as!(
|
||||
Boathouse,
|
||||
"SELECT id, boat_id, aisle, side, level FROM boathouse"
|
||||
@ -114,14 +73,24 @@ impl Boathouse {
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
BoathouseAisles::from(db, boathouses).await
|
||||
for boathouse in boathouses {
|
||||
let aisle = ret
|
||||
.get_mut(format!("{}-aisle", boathouse.aisle).as_str())
|
||||
.unwrap();
|
||||
let side = aisle.get_mut(boathouse.side.as_str()).unwrap();
|
||||
|
||||
side[boathouse.level as usize] = Some((
|
||||
boathouse.id,
|
||||
Boat::find_by_id(db, boathouse.boat_id as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
db: &SqlitePool,
|
||||
changed_by: &AllowedToUpdateBoathouse,
|
||||
data: FormBoathouseToAdd,
|
||||
) -> Result<(), String> {
|
||||
pub async fn create(db: &SqlitePool, data: FormBoathouseToAdd) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO boathouse(boat_id, aisle, side, level) VALUES (?,?,?,?)",
|
||||
data.boat_id,
|
||||
@ -132,17 +101,6 @@ impl Boathouse {
|
||||
.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(())
|
||||
}
|
||||
|
||||
@ -153,20 +111,10 @@ impl Boathouse {
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool, changed_by: &AllowedToUpdateBoathouse) {
|
||||
pub async fn delete(&self, db: &SqlitePool) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -56,44 +56,6 @@ impl BoatReservation {
|
||||
.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!(
|
||||
@ -133,13 +95,13 @@ WHERE end_date >= CURRENT_DATE ORDER BY end_date
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub fn with_groups(
|
||||
reservations: Vec<BoatReservationWithDetails>,
|
||||
pub async fn all_future_with_groups(
|
||||
db: &SqlitePool,
|
||||
) -> HashMap<String, Vec<BoatReservationWithDetails>> {
|
||||
let mut grouped_reservations: HashMap<String, Vec<BoatReservationWithDetails>> =
|
||||
HashMap::new();
|
||||
|
||||
let reservations = Self::all_future(db).await;
|
||||
for reservation in reservations {
|
||||
let key = format!(
|
||||
"{}-{}-{}-{}-{}",
|
||||
@ -158,12 +120,6 @@ WHERE end_date >= CURRENT_DATE ORDER BY end_date
|
||||
|
||||
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,
|
||||
|
@ -1,19 +1,18 @@
|
||||
use std::io::Write;
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use ics::ICalendar;
|
||||
use ics::{
|
||||
properties::{DtStart, Summary},
|
||||
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},
|
||||
use super::{
|
||||
notification::Notification, role::Role, tripdetails::TripDetails, triptype::TripType,
|
||||
user::User,
|
||||
};
|
||||
|
||||
/// DB structure of an event
|
||||
#[derive(Serialize, Clone, FromRow, Debug, PartialEq)]
|
||||
pub struct Event {
|
||||
pub id: i64,
|
||||
@ -31,13 +30,11 @@ pub struct Event {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct EventWithDetails {
|
||||
pub struct EventWithUserAndTriptype {
|
||||
#[serde(flatten)]
|
||||
pub event: Event,
|
||||
trip_type: Option<TripType>,
|
||||
tripdetails: TripDetails,
|
||||
cox_needed: bool,
|
||||
cancelled: bool,
|
||||
cox: Vec<Registration>,
|
||||
rower: Vec<Registration>,
|
||||
}
|
||||
@ -95,8 +92,8 @@ FROM trip WHERE planned_event_id = ?
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|r| Registration {
|
||||
name: r.name.unwrap(),
|
||||
registered_at: r.registered_at.unwrap(),
|
||||
name: r.name,
|
||||
registered_at: r.registered_at,
|
||||
is_guest: false,
|
||||
is_real_guest: false,
|
||||
})
|
||||
@ -115,12 +112,6 @@ pub struct EventUpdate<'a> {
|
||||
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!(
|
||||
@ -139,21 +130,16 @@ WHERE planned_event.id like ?
|
||||
.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> {
|
||||
pub async fn get_pinned_for_day(
|
||||
db: &SqlitePool,
|
||||
day: NaiveDate,
|
||||
) -> Vec<EventWithUserAndTriptype> {
|
||||
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> {
|
||||
pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<EventWithUserAndTriptype> {
|
||||
let day = format!("{day}");
|
||||
let events = sqlx::query_as!(
|
||||
Event,
|
||||
@ -174,15 +160,10 @@ WHERE day=?",
|
||||
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 {
|
||||
ret.push(EventWithUserAndTriptype {
|
||||
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,
|
||||
});
|
||||
@ -206,8 +187,7 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
||||
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
|
||||
{
|
||||
if event.is_rower_registered(db, user).await {
|
||||
ret.push(event);
|
||||
}
|
||||
}
|
||||
@ -231,21 +211,6 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
||||
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,
|
||||
@ -277,7 +242,6 @@ WHERE trip_details.id=?
|
||||
|
||||
pub async fn create(
|
||||
db: &SqlitePool,
|
||||
user: &EventUser,
|
||||
name: &str,
|
||||
planned_amount_cox: i32,
|
||||
always_show: bool,
|
||||
@ -306,19 +270,10 @@ WHERE 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<'_>) {
|
||||
pub async fn update(&self, db: &SqlitePool, update: &EventUpdate<'_>) {
|
||||
sqlx::query!(
|
||||
"UPDATE planned_event SET name = ?, planned_amount_cox = ? WHERE id = ?",
|
||||
update.name,
|
||||
@ -330,7 +285,7 @@ WHERE trip_details.id=?
|
||||
.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();
|
||||
let was_already_cancelled = tripdetails.max_people == 0;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ?, trip_type_id = ? WHERE id = ?",
|
||||
@ -345,20 +300,6 @@ WHERE trip_details.id=?
|
||||
.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,
|
||||
@ -369,7 +310,7 @@ WHERE trip_details.id=?
|
||||
.await;
|
||||
}
|
||||
|
||||
if update.cancelled() && !was_already_cancelled {
|
||||
if update.max_people == 0 && !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 {
|
||||
@ -418,7 +359,7 @@ WHERE trip_details.id=?
|
||||
}
|
||||
}
|
||||
}
|
||||
if !update.cancelled() && was_already_cancelled {
|
||||
if update.max_people > 0 && was_already_cancelled {
|
||||
Notification::delete_by_action(
|
||||
db,
|
||||
&format!("remove_user_trip_with_trip_details_id:{}", tripdetails.id),
|
||||
@ -456,7 +397,7 @@ WHERE trip_details.id=?
|
||||
}
|
||||
|
||||
pub fn is_cancelled(&self) -> bool {
|
||||
self.max_people == -1
|
||||
self.max_people == 0
|
||||
}
|
||||
|
||||
pub async fn get_ics_feed(db: &SqlitePool) -> String {
|
||||
@ -471,6 +412,34 @@ WHERE trip_details.id=?
|
||||
String::from_utf8(buf).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) async fn get_vevent(self, db: &SqlitePool) -> ics::Event {
|
||||
let mut vevent = ics::Event::new(format!("{}@rudernlinz.at", self.id), "19900101T180000");
|
||||
vevent.push(DtStart::new(format!(
|
||||
"{}T{}00",
|
||||
self.day.replace('-', ""),
|
||||
self.planned_starting_time.replace(':', "")
|
||||
)));
|
||||
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
|
||||
}
|
||||
|
||||
pub async fn trip_details(&self, db: &SqlitePool) -> TripDetails {
|
||||
TripDetails::find_by_id(db, self.trip_details_id)
|
||||
.await
|
||||
@ -480,13 +449,7 @@ WHERE trip_details.id=?
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
model::{
|
||||
planned::tripdetails::TripDetails,
|
||||
user::{EventUser, User},
|
||||
},
|
||||
testdb,
|
||||
};
|
||||
use crate::{model::tripdetails::TripDetails, testdb};
|
||||
|
||||
use super::Event;
|
||||
use chrono::Local;
|
||||
@ -506,10 +469,7 @@ mod test {
|
||||
|
||||
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;
|
||||
Event::create(&pool, "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);
|
||||
@ -532,11 +492,6 @@ mod test {
|
||||
|
||||
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
|
||||
);
|
||||
assert_eq!(format!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:{today}T100000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"), actual);
|
||||
}
|
||||
}
|
@ -1,13 +1,13 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction, sqlite::SqliteQueryResult};
|
||||
use sqlx::{sqlite::SqliteQueryResult, FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use super::user::User;
|
||||
|
||||
#[derive(FromRow, Serialize, Clone)]
|
||||
pub struct Family {
|
||||
pub(crate) id: i64,
|
||||
id: i64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone)]
|
||||
@ -74,7 +74,7 @@ GROUP BY family.id;"
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn amount_family_members(&self, db: &SqlitePool) -> i64 {
|
||||
pub async fn amount_family_members(&self, db: &SqlitePool) -> i32 {
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) as count FROM user WHERE family_id = ?",
|
||||
self.id
|
||||
@ -86,23 +86,9 @@ GROUP BY family.id;"
|
||||
}
|
||||
|
||||
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)
|
||||
sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, 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();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
use std::ops::DerefMut;
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct Location {
|
||||
@ -38,20 +37,6 @@ impl Location {
|
||||
.await
|
||||
.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> {
|
||||
sqlx::query_as!(Self, "SELECT id, name FROM location")
|
||||
|
@ -1,16 +1,74 @@
|
||||
use super::activity::ActivityBuilder;
|
||||
use sqlx::{Sqlite, SqlitePool, Transaction};
|
||||
use std::ops::DerefMut;
|
||||
|
||||
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 {
|
||||
pub async fn create(db: &SqlitePool, msg: String) -> bool {
|
||||
ActivityBuilder::new(&msg).save(db).await;
|
||||
true
|
||||
sqlx::query!("INSERT INTO log(msg) VALUES (?)", msg,)
|
||||
.execute(db)
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
pub async fn create_with_tx(db: &mut Transaction<'_, Sqlite>, msg: String) -> bool {
|
||||
ActivityBuilder::new(&msg).save_tx(db).await;
|
||||
true
|
||||
sqlx::query!("INSERT INTO log(msg) VALUES (?)", msg,)
|
||||
.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
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +1,15 @@
|
||||
use std::{fmt::Display, ops::DerefMut};
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use chrono::{Datelike, Duration, Local, NaiveDateTime};
|
||||
use rocket::FromForm;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use super::{
|
||||
activity::{ActivityBuilder, ReasonLogbook},
|
||||
boat::Boat,
|
||||
log::Log,
|
||||
notification::Notification,
|
||||
role::Role,
|
||||
rower::Rower,
|
||||
user::{User, VorstandUser},
|
||||
boat::Boat, log::Log, notification::Notification, role::Role, rower::Rower, user::User,
|
||||
};
|
||||
use crate::model::user::VecUser;
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize, Clone, Debug)]
|
||||
#[derive(FromRow, Serialize, Clone, Debug)]
|
||||
pub struct Logbook {
|
||||
pub id: i64,
|
||||
pub boat_id: i64,
|
||||
@ -40,7 +33,8 @@ impl PartialEq for Logbook {
|
||||
|
||||
pub(crate) enum Filter {
|
||||
SingleDayOnly,
|
||||
MultiDayOnly,
|
||||
MultiDazOnly,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug, Clone)]
|
||||
@ -112,7 +106,7 @@ impl TryFrom<LogToAdd> for LogToFinalize {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct LogbookWithBoatAndRowers {
|
||||
#[serde(flatten)]
|
||||
pub logbook: Logbook,
|
||||
@ -122,54 +116,6 @@ pub struct LogbookWithBoatAndRowers {
|
||||
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();
|
||||
@ -193,6 +139,11 @@ impl LogbookWithBoatAndRowers {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum LogbookAdminUpdateError {
|
||||
NotAllowed,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum LogbookUpdateError {
|
||||
NotYourEntry,
|
||||
@ -411,8 +362,8 @@ ORDER BY departure DESC
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn completed_wanderfahrten_with_user_over_km_in_year_tx(
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
pub(crate) async fn completed_wanderfahrten_with_user_over_km_in_year(
|
||||
db: &SqlitePool,
|
||||
user: &User,
|
||||
min_distance: i32,
|
||||
year: i32,
|
||||
@ -427,7 +378,7 @@ ORDER BY departure DESC
|
||||
ORDER BY arrival DESC
|
||||
", user.id, min_distance, year)
|
||||
)
|
||||
.fetch_all(db.deref_mut())
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
@ -438,17 +389,19 @@ ORDER BY departure DESC
|
||||
match filter {
|
||||
Filter::SingleDayOnly => {
|
||||
if trip_days == 0 {
|
||||
ret.push(LogbookWithBoatAndRowers::from_tx(db, log).await);
|
||||
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
|
||||
}
|
||||
}
|
||||
Filter::MultiDayOnly => {
|
||||
Filter::MultiDazOnly => {
|
||||
if trip_days > 0 {
|
||||
ret.push(LogbookWithBoatAndRowers::from_tx(db, log).await);
|
||||
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
|
||||
}
|
||||
}
|
||||
Filter::None => {
|
||||
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
|
||||
@ -629,7 +582,16 @@ ORDER BY departure DESC
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, data: LogToUpdate, changed_by: &VorstandUser) {
|
||||
pub async fn update(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
data: LogToUpdate,
|
||||
user: &User,
|
||||
) -> Result<(), LogbookAdminUpdateError> {
|
||||
if !user.has_role(db, "Vorstand").await {
|
||||
return Err(LogbookAdminUpdateError::NotAllowed);
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE logbook SET boat_id=?, shipmaster=?, steering_person=?, shipmaster_only_steering=?, departure=?, arrival=?, destination=?, distance_in_km=?, comments=?, logtype=? WHERE id=?",
|
||||
data.boat_id,
|
||||
@ -646,12 +608,7 @@ ORDER BY departure DESC
|
||||
)
|
||||
.execute(db)
|
||||
.await.unwrap();
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("{changed_by} updated log entry={:?} to {:?}", self, data),
|
||||
)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_rowers(&self, db: &mut Transaction<'_, Sqlite>) {
|
||||
@ -853,22 +810,37 @@ ORDER BY departure DESC
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), LogbookDeleteError> {
|
||||
Log::create(db, format!("{} deleted trip: {self:?}", user.name)).await;
|
||||
|
||||
if self.arrival.is_none() {
|
||||
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;
|
||||
let mut msg = format!("{} hat folgenden Logbuch-Eintrag jetzt gelöscht, welcher bereits vor über einer Stunde begonnen wurde: Schiffsführer: {}, Steuerperson: {}, Abfahrt: {}", user.name, logbook.steering_user.name, logbook.steering_user.name, logbook.logbook.departure.format("%Y-%m-%d %H:%M"));
|
||||
if let Some(destination) = logbook.logbook.destination {
|
||||
msg.push_str(&format!(", Ziel: {}", destination));
|
||||
} else {
|
||||
msg.push_str(", kein Ziel eingegeben");
|
||||
}
|
||||
msg.push_str(", Ruderer: ");
|
||||
let mut it = logbook.rowers.clone().into_iter().peekable();
|
||||
while let Some(rower) = it.next() {
|
||||
msg.push_str(&rower.name);
|
||||
if it.peek().is_some() {
|
||||
msg.push_str(" + ");
|
||||
}
|
||||
}
|
||||
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!("{user} hat folgenden Logbuch-Eintrag jetzt gelöscht, welcher bereits vor über einer Stunde begonnen wurde: {logbook}"),
|
||||
&msg,
|
||||
"Ungewöhnliches Verhalten",
|
||||
None,
|
||||
None,
|
||||
@ -883,24 +855,8 @@ ORDER BY departure DESC
|
||||
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;
|
||||
|
||||
// Only admins can delete completed logbook entries
|
||||
if user.has_role(db, "admin").await {
|
||||
sqlx::query!("DELETE FROM logbook WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
|
@ -1,15 +1,15 @@
|
||||
use std::{error::Error, fs};
|
||||
|
||||
use lettre::{
|
||||
Address, Message, SmtpTransport, Transport,
|
||||
message::{Attachment, MultiPart, SinglePart, header::ContentType},
|
||||
message::{header::ContentType, Attachment, MultiPart, SinglePart},
|
||||
transport::smtp::authentication::Credentials,
|
||||
Message, SmtpTransport, Transport,
|
||||
};
|
||||
use sqlx::{Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use crate::tera::admin::mail::MailToSend;
|
||||
|
||||
use super::{activity::ActivityBuilder, family::Family, log::Log, role::Role, user::User};
|
||||
use super::{family::Family, log::Log, role::Role, user::User};
|
||||
|
||||
pub struct Mail {}
|
||||
|
||||
@ -79,9 +79,7 @@ impl Mail {
|
||||
.build();
|
||||
|
||||
// Send the email
|
||||
if let Err(e) = mailer.send(&email) {
|
||||
Log::create_with_tx(db, format!("Mail nicht versandt: {e:?}")).await;
|
||||
}
|
||||
mailer.send(&email).unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -153,20 +151,10 @@ impl Mail {
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn fees(db: &SqlitePool, smtp_pw: String, test: Option<User>) {
|
||||
pub async fn fees(db: &SqlitePool, smtp_pw: String) {
|
||||
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() {
|
||||
if !user.has_role(db, "paid").await {
|
||||
let mut is_family = false;
|
||||
let mut send_to = String::new();
|
||||
match Family::find_by_opt_id(db, user.family_id).await {
|
||||
@ -208,10 +196,11 @@ dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
|
||||
))
|
||||
}
|
||||
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\
|
||||
Falls die Berechnung nicht stimmt (korrekte Preise findest du unter https://rudernlinz.at/unser-verein/gebuhren/) melde dich bitte bei it@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an it@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");
|
||||
Der Vorstand
|
||||
");
|
||||
let mut email = Message::builder()
|
||||
.from(
|
||||
"ASKÖ Ruderverein Donau Linz <no-reply@rudernlinz.at>"
|
||||
@ -219,7 +208,7 @@ Der Vorstand");
|
||||
.unwrap(),
|
||||
)
|
||||
.reply_to(
|
||||
"ASKÖ Ruderverein Donau Linz <kassier@rudernlinz.at>"
|
||||
"ASKÖ Ruderverein Donau Linz <it@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
@ -258,33 +247,17 @@ Der Vorstand");
|
||||
|
||||
// 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>) {
|
||||
pub async fn fees_final(db: &SqlitePool, smtp_pw: String) {
|
||||
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() {
|
||||
if !fee.paid {
|
||||
let mut is_family = false;
|
||||
let mut send_to = String::new();
|
||||
match Family::find_by_opt_id(db, user.family_id).await {
|
||||
@ -309,7 +282,7 @@ Der Vorstand");
|
||||
"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\
|
||||
Falls die Zahlung noch nicht erfolgt ist, bitten wir um umgehende Überweisung des ausstehenden Betrags, spätestens jedoch bis zum 31. März, auf unser Bankkonto.\n\n\
|
||||
Dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
|
||||
fees.sum_in_cents / 100,
|
||||
);
|
||||
@ -325,7 +298,7 @@ Dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
|
||||
}
|
||||
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",
|
||||
"Dieser gilt für die gesamte Familie ({}).\n",
|
||||
fees.name
|
||||
))
|
||||
}
|
||||
@ -344,7 +317,7 @@ Der Vorstand");
|
||||
.unwrap(),
|
||||
)
|
||||
.reply_to(
|
||||
"ASKÖ Ruderverein Donau Linz <kassier@rudernlinz.at>"
|
||||
"ASKÖ Ruderverein Donau Linz <it@rudernlinz.at>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
@ -385,12 +358,6 @@ Der Vorstand");
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -398,13 +365,3 @@ Der Vorstand");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -5,20 +5,19 @@ use waterlevel::WaterlevelDay;
|
||||
|
||||
use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD;
|
||||
|
||||
use self::{waterlevel::Waterlevel, weather::Weather};
|
||||
use boatreservation::{BoatReservation, BoatReservationWithDetails};
|
||||
use planned::{
|
||||
event::{Event, EventWithDetails},
|
||||
trip::{Trip, TripWithDetails},
|
||||
use self::{
|
||||
event::{Event, EventWithUserAndTriptype},
|
||||
trip::{Trip, TripWithUserAndType},
|
||||
waterlevel::Waterlevel,
|
||||
weather::Weather,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub mod activity;
|
||||
pub mod boat;
|
||||
pub mod boatdamage;
|
||||
pub mod boathouse;
|
||||
pub mod boatreservation;
|
||||
pub mod distance;
|
||||
pub mod event;
|
||||
pub mod family;
|
||||
pub mod location;
|
||||
pub mod log;
|
||||
@ -27,26 +26,28 @@ pub mod logtype;
|
||||
pub mod mail;
|
||||
pub mod notification;
|
||||
pub mod personal;
|
||||
pub mod planned;
|
||||
pub mod role;
|
||||
pub mod rower;
|
||||
pub mod stat;
|
||||
pub mod trailer;
|
||||
pub mod trailerreservation;
|
||||
pub mod trip;
|
||||
pub mod tripdetails;
|
||||
pub mod triptype;
|
||||
pub mod user;
|
||||
pub mod usertrip;
|
||||
pub mod waterlevel;
|
||||
pub mod weather;
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct Day {
|
||||
day: NaiveDate,
|
||||
events: Vec<EventWithDetails>,
|
||||
trips: Vec<TripWithDetails>,
|
||||
events: Vec<EventWithUserAndTriptype>,
|
||||
trips: Vec<TripWithUserAndType>,
|
||||
is_pinned: bool,
|
||||
regular_sees_this_day: bool,
|
||||
max_waterlevel: Option<WaterlevelDay>,
|
||||
weather: Option<Weather>,
|
||||
boat_reservations: HashMap<String, Vec<BoatReservationWithDetails>>,
|
||||
}
|
||||
|
||||
impl Day {
|
||||
@ -63,9 +64,6 @@ impl Day {
|
||||
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 {
|
||||
Self {
|
||||
@ -76,9 +74,6 @@ impl Day {
|
||||
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,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use super::{planned::usertrip::UserTrip, role::Role, user::User};
|
||||
use super::{role::Role, user::User, usertrip::UserTrip};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Notification {
|
||||
@ -89,32 +89,6 @@ impl Notification {
|
||||
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!(
|
||||
"
|
||||
@ -194,15 +168,6 @@ ORDER BY read_at DESC, created_at DESC;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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",
|
||||
@ -226,14 +191,12 @@ ORDER BY read_at DESC, created_at DESC;
|
||||
mod test {
|
||||
use crate::{
|
||||
model::{
|
||||
event::{Event, EventUpdate, Registration},
|
||||
notification::Notification,
|
||||
planned::{
|
||||
event::{Event, EventUpdate, Registration},
|
||||
trip::Trip,
|
||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||
usertrip::UserTrip,
|
||||
},
|
||||
user::{EventUser, SteeringUser, User},
|
||||
trip::Trip,
|
||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||
user::{CoxUser, User},
|
||||
usertrip::UserTrip,
|
||||
},
|
||||
testdb,
|
||||
};
|
||||
@ -258,10 +221,7 @@ mod test {
|
||||
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;
|
||||
Event::create(&pool, "new-event".into(), 2, false, &trip_details).await;
|
||||
let event = Event::find_by_trip_details(&pool, trip_details.id)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -271,7 +231,7 @@ mod test {
|
||||
UserTrip::create(&pool, &rower, &trip_details, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let cox = SteeringUser::new(&pool, &User::find_by_name(&pool, "cox").await.unwrap())
|
||||
let cox = CoxUser::new(&pool, User::find_by_name(&pool, "cox").await.unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
Trip::new_join(&pool, &cox, &event).await.unwrap();
|
||||
@ -280,13 +240,13 @@ mod test {
|
||||
let cancel_update = EventUpdate {
|
||||
name: &event.name,
|
||||
planned_amount_cox: event.planned_amount_cox as i32,
|
||||
max_people: -1,
|
||||
max_people: 0,
|
||||
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;
|
||||
event.update(&pool, &cancel_update).await;
|
||||
|
||||
// Rower received notification
|
||||
let notifications = Notification::for_user(&pool, &rower).await;
|
||||
@ -294,7 +254,7 @@ mod test {
|
||||
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")
|
||||
Some("remove_user_trip_with_trip_details_id:3")
|
||||
);
|
||||
|
||||
// Cox received notification
|
||||
@ -316,12 +276,12 @@ mod test {
|
||||
is_locked: event.is_locked,
|
||||
trip_type_id: None,
|
||||
};
|
||||
event.update(&pool, &user, &update).await;
|
||||
event.update(&pool, &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;
|
||||
event.update(&pool, &cancel_update).await;
|
||||
|
||||
// Rower is removed if notification is accepted
|
||||
assert!(event.is_rower_registered(&pool, &rower).await);
|
||||
|
@ -1,17 +1,9 @@
|
||||
use std::io::Write;
|
||||
|
||||
use ics::{
|
||||
ICalendar,
|
||||
components::Property,
|
||||
properties::{DtEnd, DtStart, Summary},
|
||||
};
|
||||
use ics::{components::Property, ICalendar};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{
|
||||
planned::{event::Event, trip::Trip},
|
||||
user::User,
|
||||
};
|
||||
use chrono::{Duration, NaiveTime};
|
||||
use crate::model::{event::Event, trip::Trip, user::User};
|
||||
|
||||
pub(crate) async fn get_personal_cal(db: &SqlitePool, user: &User) -> String {
|
||||
let mut calendar = ICalendar::new("2.0", "ics-rs");
|
||||
@ -27,131 +19,9 @@ pub(crate) async fn get_personal_cal(db: &SqlitePool, user: &User) -> String {
|
||||
|
||||
let trips = Trip::all_with_user(db, user).await;
|
||||
for trip in trips {
|
||||
calendar.add_event(trip.get_vevent(db, user).await);
|
||||
calendar.add_event(trip.get_vevent(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
|
||||
}
|
||||
}
|
||||
|
@ -1,64 +1,63 @@
|
||||
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,
|
||||
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,
|
||||
Level::BRONZE => 40000,
|
||||
Level::SILVER => 80000,
|
||||
Level::GOLD => 100000,
|
||||
Level::DIAMOND => 200000,
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
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 => "-",
|
||||
Level::BRONZE => "Bronze",
|
||||
Level::SILVER => "Silber",
|
||||
Level::GOLD => "Gold",
|
||||
Level::DIAMOND => "Diamant",
|
||||
Level::DONE => "",
|
||||
Level::NONE => "-",
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -86,19 +85,3 @@ impl Next {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ use std::cmp;
|
||||
|
||||
use chrono::{Datelike, Local, NaiveDate};
|
||||
use serde::Serialize;
|
||||
use sqlx::{Acquire, Sqlite, SqlitePool, Transaction};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{
|
||||
logbook::{Filter, Logbook, LogbookWithBoatAndRowers},
|
||||
@ -111,40 +111,11 @@ pub(crate) struct Status {
|
||||
}
|
||||
|
||||
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> {
|
||||
pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Option<Self> {
|
||||
let Ok(agebracket) = AgeBracket::try_from(user) else {
|
||||
return None;
|
||||
};
|
||||
let category = agebracket.cat().to_string();
|
||||
|
||||
let year = if Local::now().month() == 1 {
|
||||
Local::now().year() - 1
|
||||
@ -152,9 +123,12 @@ impl Status {
|
||||
Local::now().year()
|
||||
};
|
||||
|
||||
let rowed_km = Stat::person_tx(db, Some(year), user).await.rowed_km;
|
||||
let rowed_km = Stat::person(db, Some(year), user).await.rowed_km;
|
||||
let required_km = agebracket.dist_in_km();
|
||||
let missing_km = cmp::max(required_km - rowed_km, 0);
|
||||
|
||||
let single_day_trips_over_required_distance =
|
||||
Logbook::completed_wanderfahrten_with_user_over_km_in_year_tx(
|
||||
Logbook::completed_wanderfahrten_with_user_over_km_in_year(
|
||||
db,
|
||||
user,
|
||||
agebracket.required_dist_single_day_in_km(),
|
||||
@ -163,61 +137,30 @@ impl Status {
|
||||
)
|
||||
.await;
|
||||
let multi_day_trips_over_required_distance =
|
||||
Logbook::completed_wanderfahrten_with_user_over_km_in_year_tx(
|
||||
Logbook::completed_wanderfahrten_with_user_over_km_in_year(
|
||||
db,
|
||||
user,
|
||||
agebracket.required_dist_multi_day_in_km(),
|
||||
year,
|
||||
Filter::MultiDayOnly,
|
||||
Filter::MultiDazOnly,
|
||||
)
|
||||
.await;
|
||||
|
||||
let ret = Self::calc(
|
||||
&agebracket,
|
||||
rowed_km,
|
||||
single_day_trips_over_required_distance.len(),
|
||||
multi_day_trips_over_required_distance.len(),
|
||||
year,
|
||||
);
|
||||
let achieved = missing_km == 0
|
||||
&& (multi_day_trips_over_required_distance.len() >= 1
|
||||
|| single_day_trips_over_required_distance.len() >= 2);
|
||||
|
||||
Some(Self {
|
||||
year,
|
||||
rowed_km,
|
||||
category,
|
||||
required_km,
|
||||
missing_km,
|
||||
multi_day_trips_over_required_distance,
|
||||
single_day_trips_over_required_distance,
|
||||
..ret
|
||||
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(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
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
use std::{cmp::Ordering, fmt::Display, ops::DerefMut};
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use super::{activity::ActivityBuilder, user::AdminUser};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
@ -8,83 +7,22 @@ use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
pub struct Role {
|
||||
pub(crate) id: i64,
|
||||
pub(crate) 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 {
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Role> {
|
||||
sqlx::query_as!(
|
||||
Role,
|
||||
"SELECT id, name, formatted_name, desc, hide_in_lists, cluster FROM role"
|
||||
)
|
||||
.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()
|
||||
sqlx::query_as!(Role, "SELECT id, name, cluster FROM role")
|
||||
.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
|
||||
SELECT id, name, cluster
|
||||
FROM role
|
||||
WHERE id like ?
|
||||
",
|
||||
@ -98,7 +36,7 @@ WHERE id like ?
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, formatted_name, desc, hide_in_lists, cluster
|
||||
SELECT id, name, cluster
|
||||
FROM role
|
||||
WHERE id like ?
|
||||
",
|
||||
@ -109,11 +47,26 @@ WHERE id like ?
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn find_by_cluster_tx(db: &mut Transaction<'_, Sqlite>, name: i32) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, cluster
|
||||
FROM role
|
||||
WHERE cluster = ?
|
||||
",
|
||||
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
|
||||
SELECT id, name, cluster
|
||||
FROM role
|
||||
WHERE name like ?
|
||||
",
|
||||
@ -128,7 +81,7 @@ WHERE name like ?
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, formatted_name, desc, hide_in_lists, cluster
|
||||
SELECT id, name, cluster
|
||||
FROM role
|
||||
WHERE name like ?
|
||||
",
|
||||
@ -139,30 +92,6 @@ WHERE name like ?
|
||||
.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
|
||||
|
@ -23,7 +23,7 @@ impl Rower {
|
||||
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
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
FROM user
|
||||
WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?)
|
||||
",
|
||||
|
@ -1,9 +1,9 @@
|
||||
use std::{collections::HashMap, ops::DerefMut};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::model::user::User;
|
||||
use chrono::Datelike;
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, Row, Sqlite, SqlitePool, Transaction};
|
||||
use sqlx::{FromRow, Row, SqlitePool};
|
||||
|
||||
use super::boat::Boat;
|
||||
|
||||
@ -98,7 +98,6 @@ ORDER BY
|
||||
#[derive(FromRow, Serialize, Clone)]
|
||||
pub struct Stat {
|
||||
name: String,
|
||||
pub(crate) amount_trips: i32,
|
||||
pub(crate) rowed_km: i32,
|
||||
}
|
||||
|
||||
@ -109,11 +108,9 @@ impl Stat {
|
||||
None => chrono::Local::now().year(),
|
||||
};
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
// proper guests
|
||||
let guests = sqlx::query(&format!(
|
||||
let rowed_km = sqlx::query(&format!(
|
||||
"
|
||||
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
|
||||
SELECT SUM((b.amount_seats - COALESCE(m.member_count, 0)) * l.distance_in_km) as total_guest_km
|
||||
FROM logbook l
|
||||
JOIN boat b ON l.boat_id = b.id
|
||||
LEFT JOIN (
|
||||
@ -126,15 +123,12 @@ WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%' AND not b.exter
|
||||
))
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
.unwrap()
|
||||
.get::<i64, usize>(0) as i32;
|
||||
|
||||
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!(
|
||||
let rowed_km_guests = sqlx::query(&format!(
|
||||
"
|
||||
SELECT CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km, COUNT(*) AS amount_trips
|
||||
SELECT CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM user u
|
||||
INNER JOIN rower r ON u.id = r.rower_id
|
||||
INNER JOIN logbook l ON r.logbook_id = l.id
|
||||
@ -151,27 +145,15 @@ 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);
|
||||
.unwrap()
|
||||
.get::<i64, usize>(0) as i32;
|
||||
|
||||
Stat {
|
||||
name: "Gäste".into(),
|
||||
amount_trips: guest_amount_trips + guest_user_amount_trips,
|
||||
rowed_km: guest_km + guest_user_km,
|
||||
rowed_km: rowed_km + rowed_km_guests,
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@ -190,7 +172,7 @@ AND u.name != 'Externe Steuerperson';
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
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 (
|
||||
SELECT * FROM user
|
||||
WHERE id IN (
|
||||
@ -212,17 +194,16 @@ ORDER BY rowed_km DESC, u.name;
|
||||
.into_iter()
|
||||
.map(|row| Stat {
|
||||
name: row.get("name"),
|
||||
amount_trips: row.get("amount_trips"),
|
||||
rowed_km: row.get("rowed_km"),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn total_km_tx(db: &mut Transaction<'_, Sqlite>, user: &User) -> Stat {
|
||||
pub async fn total_km(db: &SqlitePool, 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
|
||||
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM (
|
||||
SELECT * FROM user
|
||||
WHERE id={}
|
||||
@ -233,29 +214,17 @@ WHERE l.distance_in_km IS NOT NULL;
|
||||
",
|
||||
user.id
|
||||
))
|
||||
.fetch_one(db.deref_mut())
|
||||
.fetch_one(db)
|
||||
.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 {
|
||||
pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat {
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
None => chrono::Local::now().year(),
|
||||
@ -263,7 +232,7 @@ WHERE l.distance_in_km IS NOT NULL;
|
||||
//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
|
||||
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM (
|
||||
SELECT * FROM user
|
||||
WHERE id={}
|
||||
@ -274,23 +243,15 @@ WHERE l.distance_in_km IS NOT NULL AND l.arrival LIKE '{year}-%';
|
||||
",
|
||||
user.id
|
||||
))
|
||||
.fetch_one(db.deref_mut())
|
||||
.fetch_one(db)
|
||||
.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)]
|
||||
|
@ -1,28 +1,25 @@
|
||||
use chrono::{Local, NaiveDate};
|
||||
use ics::properties::{DtStart, Summary};
|
||||
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},
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
user::{CoxUser, User},
|
||||
usertrip::UserTrip,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
pub struct Trip {
|
||||
pub id: i64,
|
||||
id: i64,
|
||||
pub cox_id: i64,
|
||||
pub cox_name: String,
|
||||
cox_name: String,
|
||||
trip_details_id: Option<i64>,
|
||||
pub planned_starting_time: String,
|
||||
planned_starting_time: String,
|
||||
pub max_people: i64,
|
||||
pub day: String,
|
||||
pub notes: Option<String>,
|
||||
@ -33,16 +30,15 @@ pub struct Trip {
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
pub struct TripWithDetails {
|
||||
pub struct TripWithUserAndType {
|
||||
#[serde(flatten)]
|
||||
pub trip: Trip,
|
||||
pub rower: Vec<Registration>,
|
||||
trip_type: Option<TripType>,
|
||||
cancelled: bool,
|
||||
}
|
||||
|
||||
pub struct TripUpdate<'a> {
|
||||
pub cox: &'a User,
|
||||
pub cox: &'a CoxUser,
|
||||
pub trip: &'a Trip,
|
||||
pub max_people: i32,
|
||||
pub notes: Option<&'a str>,
|
||||
@ -50,13 +46,7 @@ pub struct TripUpdate<'a> {
|
||||
pub is_locked: bool,
|
||||
}
|
||||
|
||||
impl TripUpdate<'_> {
|
||||
fn cancelled(&self) -> bool {
|
||||
self.max_people == -1
|
||||
}
|
||||
}
|
||||
|
||||
impl TripWithDetails {
|
||||
impl TripWithUserAndType {
|
||||
pub async fn from(db: &SqlitePool, trip: Trip) -> Self {
|
||||
let mut trip_type = None;
|
||||
if let Some(trip_type_id) = trip.trip_type_id {
|
||||
@ -64,14 +54,60 @@ impl TripWithDetails {
|
||||
}
|
||||
Self {
|
||||
rower: Registration::all_rower(db, trip.trip_details_id.unwrap()).await,
|
||||
trip_type,
|
||||
cancelled: trip.is_cancelled(),
|
||||
trip,
|
||||
trip_type,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Trip {
|
||||
/// Cox decides to create own trip.
|
||||
pub async fn new_own(db: &SqlitePool, cox: &CoxUser, trip_details: TripDetails) {
|
||||
let _ = sqlx::query!(
|
||||
"INSERT INTO trip (cox_id, trip_details_id) VALUES(?, ?)",
|
||||
cox.id,
|
||||
trip_details.id
|
||||
)
|
||||
.execute(db)
|
||||
.await;
|
||||
|
||||
let same_starting_datetime = TripDetails::find_by_startingdatetime(
|
||||
db,
|
||||
trip_details.day,
|
||||
trip_details.planned_starting_time,
|
||||
)
|
||||
.await;
|
||||
if same_starting_datetime.len() > 1 {
|
||||
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 = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
|
||||
cox.user.name, trip.day, trip.planned_starting_time
|
||||
),
|
||||
"Neue Ausfahrt zur selben Zeit",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_by_trip_details(db: &SqlitePool, tripdetails_id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
@ -89,12 +125,32 @@ WHERE trip_details.id=?
|
||||
.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(crate) async fn get_vevent(self, user: &User) -> ics::Event {
|
||||
let mut vevent = ics::Event::new(format!("{}@rudernlinz.at", self.id), "19900101T180000");
|
||||
vevent.push(DtStart::new(format!(
|
||||
"{}T{}00",
|
||||
self.day.replace('-', ""),
|
||||
self.planned_starting_time.replace(':', "")
|
||||
)));
|
||||
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
|
||||
}
|
||||
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
@ -151,7 +207,7 @@ WHERE trip.id=?
|
||||
/// Cox decides to help in a event.
|
||||
pub async fn new_join(
|
||||
db: &SqlitePool,
|
||||
cox: &SteeringUser,
|
||||
cox: &CoxUser,
|
||||
event: &Event,
|
||||
) -> Result<(), CoxHelpError> {
|
||||
if event.is_rower_registered(db, cox).await {
|
||||
@ -162,7 +218,7 @@ WHERE trip.id=?
|
||||
return Err(CoxHelpError::DetailsLocked);
|
||||
}
|
||||
|
||||
if event.is_cancelled() {
|
||||
if event.max_people == 0 {
|
||||
return Err(CoxHelpError::CanceledEvent);
|
||||
}
|
||||
|
||||
@ -179,12 +235,12 @@ WHERE trip.id=?
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_for_today(db: &SqlitePool) -> Vec<TripWithDetails> {
|
||||
pub async fn get_for_today(db: &SqlitePool) -> Vec<TripWithUserAndType> {
|
||||
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> {
|
||||
pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<TripWithUserAndType> {
|
||||
let day = format!("{day}");
|
||||
let trips = sqlx::query_as!(
|
||||
Trip,
|
||||
@ -203,7 +259,7 @@ WHERE day=?
|
||||
|
||||
let mut ret = Vec::new();
|
||||
for trip in trips {
|
||||
ret.push(TripWithDetails::from(db, trip).await);
|
||||
ret.push(TripWithUserAndType::from(db, trip).await);
|
||||
}
|
||||
ret
|
||||
}
|
||||
@ -217,18 +273,14 @@ WHERE day=?
|
||||
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 was_already_cancelled = tripdetails.max_people == 0;
|
||||
|
||||
let is_locked = if update.cancelled() {
|
||||
let is_locked = if update.max_people == 0 {
|
||||
false
|
||||
} else {
|
||||
update.is_locked
|
||||
@ -246,8 +298,10 @@ WHERE day=?
|
||||
.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;
|
||||
if update.max_people == 0 && !was_already_cancelled {
|
||||
let rowers = TripWithUserAndType::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 {
|
||||
@ -260,7 +314,7 @@ WHERE day=?
|
||||
&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.cox.user.name,
|
||||
update.trip.day,
|
||||
update.trip.planned_starting_time,
|
||||
notes
|
||||
@ -283,7 +337,7 @@ WHERE day=?
|
||||
.await;
|
||||
}
|
||||
|
||||
if !update.cancelled() && was_already_cancelled {
|
||||
if update.max_people > 0 && was_already_cancelled {
|
||||
Notification::delete_by_action(
|
||||
db,
|
||||
&format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
|
||||
@ -306,7 +360,7 @@ WHERE day=?
|
||||
|
||||
pub async fn delete_by_planned_event(
|
||||
db: &SqlitePool,
|
||||
cox: &SteeringUser,
|
||||
cox: &CoxUser,
|
||||
event: &Event,
|
||||
) -> Result<(), TripHelpDeleteError> {
|
||||
if event.trip_details(db).await.is_locked {
|
||||
@ -330,7 +384,11 @@ WHERE day=?
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), TripDeleteError> {
|
||||
pub(crate) async fn delete(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
user: &CoxUser,
|
||||
) -> Result<(), TripDeleteError> {
|
||||
let registered_rower = Registration::all_rower(db, self.trip_details_id.unwrap()).await;
|
||||
if !registered_rower.is_empty() {
|
||||
return Err(TripDeleteError::SomebodyAlreadyRegistered);
|
||||
@ -340,7 +398,7 @@ WHERE day=?
|
||||
return Err(TripDeleteError::NotYourTrip);
|
||||
}
|
||||
|
||||
Log::create(db, format!("{} deleted trip: {:#?}", user.name, self)).await;
|
||||
Log::create(db, format!("{} deleted trip: {:#?}", user.user.name, self)).await;
|
||||
|
||||
sqlx::query!("DELETE FROM trip WHERE id = ?", self.id)
|
||||
.execute(db)
|
||||
@ -371,14 +429,14 @@ WHERE day=?
|
||||
pub(crate) async fn get_pinned_for_day(
|
||||
db: &sqlx::Pool<sqlx::Sqlite>,
|
||||
day: NaiveDate,
|
||||
) -> Vec<TripWithDetails> {
|
||||
) -> Vec<TripWithUserAndType> {
|
||||
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
|
||||
fn is_cancelled(&self) -> bool {
|
||||
self.max_people == 0
|
||||
}
|
||||
}
|
||||
|
||||
@ -406,21 +464,17 @@ pub enum TripDeleteError {
|
||||
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},
|
||||
event::Event,
|
||||
trip::{self, TripDeleteError},
|
||||
tripdetails::TripDetails,
|
||||
user::{CoxUser, User},
|
||||
usertrip::UserTrip,
|
||||
},
|
||||
testdb,
|
||||
};
|
||||
@ -434,9 +488,9 @@ mod test {
|
||||
fn test_new_own() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -448,36 +502,6 @@ mod test {
|
||||
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!();
|
||||
@ -491,9 +515,9 @@ mod test {
|
||||
fn test_new_succ_join() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -507,9 +531,9 @@ mod test {
|
||||
fn test_new_failed_join_already_cox() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -524,9 +548,9 @@ mod test {
|
||||
fn test_succ_update_own() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -552,9 +576,9 @@ mod test {
|
||||
fn test_succ_update_own_with_triptype() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -580,9 +604,9 @@ mod test {
|
||||
fn test_fail_update_own_not_your_trip() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -605,9 +629,9 @@ mod test {
|
||||
fn test_succ_delete_by_planned_event() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -628,9 +652,9 @@ mod test {
|
||||
fn test_succ_delete() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -646,9 +670,9 @@ mod test {
|
||||
fn test_fail_delete_diff_cox() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
User::find_by_name(&pool, "cox2".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -668,9 +692,9 @@ mod test {
|
||||
fn test_fail_delete_someone_registered() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
@ -1,14 +1,14 @@
|
||||
use crate::model::{notification::Notification, user::User};
|
||||
use crate::model::user::User;
|
||||
use chrono::{Local, NaiveDate};
|
||||
use rocket::FromForm;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::{
|
||||
trip::{Trip, TripWithDetails},
|
||||
notification::Notification,
|
||||
trip::{Trip, TripWithUserAndType},
|
||||
triptype::TripType,
|
||||
};
|
||||
use std::fmt::Display;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub struct TripDetails {
|
||||
@ -23,20 +23,6 @@ pub struct TripDetails {
|
||||
pub is_locked: bool,
|
||||
}
|
||||
|
||||
impl Display for TripDetails {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(&format!(
|
||||
"Ausfahrt am {} um {} mit {} Personen",
|
||||
self.day, self.planned_starting_time, self.max_people
|
||||
))?;
|
||||
if let Some(notes) = &self.notes {
|
||||
f.write_str(&format!(" Notizen: {notes}"))?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Serialize)]
|
||||
pub struct TripDetailsToAdd<'r> {
|
||||
//TODO: properly parse `planned_starting_time`
|
||||
@ -109,7 +95,7 @@ WHERE day = ? AND planned_starting_time = ?
|
||||
}
|
||||
|
||||
pub fn cancelled(&self) -> bool {
|
||||
self.max_people == -1
|
||||
self.max_people == 0
|
||||
}
|
||||
|
||||
/// This function is called when a person registers to a trip or when the cox changes the
|
||||
@ -152,7 +138,7 @@ WHERE day = ? AND planned_starting_time = ?
|
||||
// This trip_details belongs to a planned_event, no need to do anything
|
||||
continue;
|
||||
};
|
||||
let pot_coxes = TripWithDetails::from(db, trip.clone()).await;
|
||||
let pot_coxes = TripWithUserAndType::from(db, trip.clone()).await;
|
||||
let pot_coxes = pot_coxes.rower;
|
||||
for user in pot_coxes {
|
||||
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
@ -160,7 +146,7 @@ WHERE day = ? AND planned_starting_time = ?
|
||||
// User is a guest, no need to bother.
|
||||
continue;
|
||||
};
|
||||
if !user.allowed_to_steer(db).await {
|
||||
if !user.has_role(db, "cox").await {
|
||||
// User is no cox, no need to bother
|
||||
continue;
|
||||
}
|
||||
@ -210,7 +196,7 @@ WHERE day = ? AND planned_starting_time = ?
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
let amount_currently_registered = amount_currently_registered.count;
|
||||
let amount_currently_registered = i64::from(amount_currently_registered.count);
|
||||
|
||||
amount_currently_registered >= self.max_people
|
||||
}
|
||||
@ -317,7 +303,7 @@ pub(crate) enum Action {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{model::planned::tripdetails::TripDetailsToAdd, testdb};
|
||||
use crate::{model::tripdetails::TripDetailsToAdd, testdb};
|
||||
|
||||
use super::TripDetails;
|
||||
use sqlx::SqlitePool;
|
||||
@ -353,7 +339,7 @@ mod test {
|
||||
}
|
||||
)
|
||||
.await,
|
||||
4,
|
||||
3,
|
||||
);
|
||||
assert_eq!(
|
||||
TripDetails::create(
|
||||
@ -368,7 +354,7 @@ mod test {
|
||||
}
|
||||
)
|
||||
.await,
|
||||
5,
|
||||
4,
|
||||
);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct TripType {
|
||||
pub id: i64,
|
||||
pub name: String,
|
File diff suppressed because it is too large
Load Diff
@ -1,581 +0,0 @@
|
||||
// TODO: put back in `src/model/user/mod.rs` once that is cleaned up
|
||||
|
||||
use super::{AllowedToEditPaymentStatusUser, ManageUserUser, User};
|
||||
use crate::model::{
|
||||
activity::{self, ActivityBuilder},
|
||||
family::Family,
|
||||
mail::valid_mails,
|
||||
notification::Notification,
|
||||
role::Role,
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use rocket::{fs::TempFile, tokio::io::AsyncReadExt};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
impl User {
|
||||
pub(crate) async fn add_note(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &ManageUserUser,
|
||||
note: &str,
|
||||
) -> Result<(), String> {
|
||||
let note = note.trim();
|
||||
|
||||
ActivityBuilder::from(activity::Reason::UserDataChange(
|
||||
updated_by,
|
||||
self,
|
||||
note.to_string(),
|
||||
))
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn update_mail(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &ManageUserUser,
|
||||
new_mail: &str,
|
||||
) -> Result<(), String> {
|
||||
let new_mail = new_mail.trim();
|
||||
|
||||
if !valid_mails(new_mail) {
|
||||
return Err(format!(
|
||||
"{new_mail} ist kein gültiges Format für eine Mailadresse"
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!("UPDATE user SET mail = ? where id = ?", new_mail, self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
|
||||
let msg = match &self.mail {
|
||||
Some(old_mail) => format!("Mail-Adresse von {old_mail} auf {new_mail} geändert."),
|
||||
None => format!("Neue Mail-Adresse für: {new_mail}"),
|
||||
};
|
||||
|
||||
ActivityBuilder::from(activity::Reason::UserDataChange(updated_by, self, msg))
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn update_phone(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &ManageUserUser,
|
||||
new_phone: &str,
|
||||
) {
|
||||
let new_phone = new_phone.trim();
|
||||
|
||||
let query = if new_phone.is_empty() {
|
||||
if self.phone.is_none() {
|
||||
return; // nothing to do
|
||||
}
|
||||
sqlx::query!("UPDATE user SET phone = NULL where id = ?", self.id)
|
||||
} else {
|
||||
if let Some(old_phone) = &self.phone {
|
||||
if old_phone == new_phone {
|
||||
return; //nothing to do
|
||||
}
|
||||
}
|
||||
sqlx::query!("UPDATE user SET phone = ? where id = ?", new_phone, self.id)
|
||||
};
|
||||
query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
|
||||
let msg = match &self.phone {
|
||||
Some(old_phone) if new_phone.is_empty() => {
|
||||
format!("Telefonnummer wurde entfernt (alte Nummer: {old_phone})")
|
||||
}
|
||||
Some(old_phone) => {
|
||||
format!("Telefonnummer wurde von {old_phone} auf {new_phone} geändert.")
|
||||
}
|
||||
None => format!("Neue Telefonnummer hinzugefügt: {new_phone}"),
|
||||
};
|
||||
|
||||
ActivityBuilder::from(activity::Reason::UserDataChange(updated_by, self, msg))
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn update_address(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &ManageUserUser,
|
||||
new_address: &str,
|
||||
) {
|
||||
let new_address = new_address.trim();
|
||||
|
||||
let query = if new_address.is_empty() {
|
||||
if self.address.is_none() {
|
||||
return; // nothing to do
|
||||
}
|
||||
sqlx::query!("UPDATE user SET address = NULL where id = ?", self.id)
|
||||
} else {
|
||||
if let Some(old_address) = &self.address {
|
||||
if old_address == new_address {
|
||||
return; //nothing to do
|
||||
}
|
||||
}
|
||||
sqlx::query!(
|
||||
"UPDATE user SET address = ? where id = ?",
|
||||
new_address,
|
||||
self.id
|
||||
)
|
||||
};
|
||||
query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
|
||||
let msg = match &self.address {
|
||||
Some(old_address) if new_address.is_empty() => format!(
|
||||
"{updated_by} hat die Adresse von {self} entfernt (alte Adresse: {old_address})"
|
||||
),
|
||||
Some(old_address) => format!(
|
||||
"{updated_by} hat die Adresse von {self} von {old_address} auf {new_address} geändert."
|
||||
),
|
||||
None => format!("{updated_by} hat eine Adresse für {self} hinzugefügt: {new_address}"),
|
||||
};
|
||||
|
||||
ActivityBuilder::new(&msg).user(self).save(db).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn update_nickname(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &ManageUserUser,
|
||||
new_nickname: &str,
|
||||
) -> Result<(), String> {
|
||||
let new_nickname = new_nickname.trim();
|
||||
|
||||
let query = if new_nickname.is_empty() {
|
||||
sqlx::query!("UPDATE user SET nickname = NULL where id = ?", self.id)
|
||||
} else {
|
||||
sqlx::query!(
|
||||
"UPDATE user SET nickname = ? where id = ?",
|
||||
new_nickname,
|
||||
self.id
|
||||
)
|
||||
};
|
||||
query.execute(db).await.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
|
||||
let msg = match &self.nickname {
|
||||
Some(old_nickname) if new_nickname.is_empty() => format!(
|
||||
"{updated_by} hat den Sitznamen von {self} entfernt (alter Spitzname: {old_nickname})"
|
||||
),
|
||||
Some(old_nickname) => format!(
|
||||
"{updated_by} hat den Spitznamen von {self} von {old_nickname} auf {new_nickname} geändert."
|
||||
),
|
||||
None => format!(
|
||||
"{updated_by} hat einen neuen Spitznamen für {self} hinzugefügt: {new_nickname}"
|
||||
),
|
||||
};
|
||||
ActivityBuilder::new(&msg).user(self).save(db).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn update_member_since(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &ManageUserUser,
|
||||
new_member_since_date: &NaiveDate,
|
||||
) {
|
||||
sqlx::query!(
|
||||
"UPDATE user SET member_since_date = ? where id = ?",
|
||||
new_member_since_date,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
|
||||
let msg = match &self.member_since_date {
|
||||
Some(old_member_since_date) => format!(
|
||||
"{updated_by} hat das Beitrittsdatum von {self} von {old_member_since_date} auf {new_member_since_date} geändert."
|
||||
),
|
||||
None => format!(
|
||||
"{updated_by} hat ein neues Beitrittsdatum für {self} hinzugefügt: {new_member_since_date}"
|
||||
),
|
||||
};
|
||||
|
||||
ActivityBuilder::new(&msg).user(self).save(db).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn update_birthdate(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &ManageUserUser,
|
||||
new_birthdate: &NaiveDate,
|
||||
) {
|
||||
sqlx::query!(
|
||||
"UPDATE user SET birthdate = ? where id = ?",
|
||||
new_birthdate,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
|
||||
let msg = match &self.birthdate {
|
||||
Some(old_birthdate) => format!(
|
||||
"{updated_by} hat das Geburtsdatum von {self} von {old_birthdate} auf {new_birthdate} geändert."
|
||||
),
|
||||
None => {
|
||||
format!("{updated_by} hat ein Geburtsdatum für {self} hinzugefügt: {new_birthdate}")
|
||||
}
|
||||
};
|
||||
|
||||
ActivityBuilder::new(&msg).user(self).save(db).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn update_family(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &ManageUserUser,
|
||||
family: Option<Family>,
|
||||
) {
|
||||
if let Some(family) = family {
|
||||
let family_id = family.id;
|
||||
sqlx::query!(
|
||||
"UPDATE user SET family_id = ? where id = ?",
|
||||
family_id,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat {self} zu einer Familie hinzugefügt."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
} else {
|
||||
sqlx::query!("UPDATE user SET family_id = NULL where id = ?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat die Familienzugehörigkeit von {self} gelöscht."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
};
|
||||
|
||||
Family::clean_families_without_members(db).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn change_skill(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &ManageUserUser,
|
||||
skill: Option<Role>,
|
||||
) -> Result<(), String> {
|
||||
let old_skill = self.skill(db).await;
|
||||
|
||||
let member = Role::find_by_name(db, "Donau Linz").await.unwrap();
|
||||
let cox = Role::find_by_name(db, "cox").await.unwrap();
|
||||
let bootsfuehrer = Role::find_by_name(db, "Bootsführer").await.unwrap();
|
||||
|
||||
match (old_skill, skill) {
|
||||
(None, new) if new == Some(cox.clone()) => {
|
||||
self.add_role(db, updated_by, &cox).await?;
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&member,
|
||||
&format!(
|
||||
"Liebes Vereinsmitglied, {self} ist ab sofort Steuerperson 🎉 Hip hip ...!"
|
||||
),
|
||||
"Neue Steuerperson",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
Notification::create(
|
||||
db,
|
||||
self,
|
||||
&format!(
|
||||
"Liebe neue Steuerperson, gratuliere zur geschafften Steuerprüfung 💪. Du kannst ab sofort selber Ausfahrten ausschreiben und der Steuerpersonen Signal-Gruppe beitreten: https://signal.group/#CjQKIHJInNb3zSVW7ipLo7_ygIqVxhxUaaNYx4sy2jdklLsIEhBHJNM2KZM1UnBdQxWy_Gdp"
|
||||
),
|
||||
"Gratulation",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
ActivityBuilder::new(&format!("{updated_by} hat {self} zur Steuerperson gemacht"))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
(old, new) if old == Some(cox.clone()) && new == Some(bootsfuehrer.clone()) => {
|
||||
self.remove_role(db, updated_by, &cox).await?;
|
||||
self.add_role(db, updated_by, &bootsfuehrer).await?;
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&member,
|
||||
&format!(
|
||||
"Liebes Vereinsmitglied, {self} ist ab sofort Bootsführer:in 🎉 Hip hip ...!"
|
||||
),
|
||||
"Neue:r Bootsführer:in",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
ActivityBuilder::new(&format!("{updated_by} hat {self} zum Bootsführer gemacht"))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
(old, None) => {
|
||||
if let Some(old) = old {
|
||||
self.remove_role(db, updated_by, &old).await?;
|
||||
let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap();
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!("Lieber Vorstand, {self} ist ab sofort kein {old} mehr."),
|
||||
"Steuerperson--;",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
ActivityBuilder::new(&format!("{updated_by} hat {self} zum normalen Mitglied gemacht (keine Steuerperson/Bootsführer mehr)"))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
(old, new) if old == Some(bootsfuehrer.clone()) && new == Some(cox.clone()) => {
|
||||
self.remove_role(db, updated_by, &bootsfuehrer).await?;
|
||||
self.add_role(db, updated_by, &cox).await?;
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&member,
|
||||
&format!(
|
||||
"Lieber Vorstand, {self} ist ab sofort kein Bootsführer:in mehr, sondern 'nur' mehr eine Steuerperson."
|
||||
),
|
||||
"Bootsführer--",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat {self} zur Steuerperson gemacht (kein Bootsführer mehr)"
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
(old, new) => return Err(format!("Not allowed to change from {old:?} to {new:?}")),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn change_financial(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &ManageUserUser,
|
||||
financial: Option<Role>,
|
||||
) -> Result<(), String> {
|
||||
let mut new = String::new();
|
||||
let mut old = String::new();
|
||||
|
||||
if let Some(old_financial) = self.financial(db).await {
|
||||
self.remove_role(db, updated_by, &old_financial).await?;
|
||||
old.push_str(&old_financial.to_string());
|
||||
} else {
|
||||
old.push_str("Keine Ermäßigung");
|
||||
}
|
||||
|
||||
if let Some(new_financial) = financial {
|
||||
self.add_role(db, updated_by, &new_financial).await?;
|
||||
new.push_str(&new_financial.to_string());
|
||||
} else {
|
||||
new.push_str("Keine Ermäßigung");
|
||||
}
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat die Ermäßigung von {self} von {old} auf {new} geändert"
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_role(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &ManageUserUser,
|
||||
role: &Role,
|
||||
) -> Result<(), String> {
|
||||
if !self.has_role(db, &role.name).await {
|
||||
return Err(format!(
|
||||
"Kann Rolle {role} von User {self} nicht entfernen, da der User die Rolle gar nicht hat"
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
|
||||
self.id,
|
||||
role.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if !role.hide_in_lists && role.cluster.is_none() {
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat die Rolle {role} von {self} entfernt."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn has_not_paid(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &AllowedToEditPaymentStatusUser,
|
||||
) {
|
||||
let paid = Role::find_by_name(db, "paid").await.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
|
||||
self.id,
|
||||
paid.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat den Bezahlstatus von {self} auf 'nicht bezahlt' gesetzt."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
pub(crate) async fn has_paid(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &AllowedToEditPaymentStatusUser,
|
||||
) {
|
||||
let paid = Role::find_by_name(db, "paid").await.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
|
||||
self.id,
|
||||
paid.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.expect("paid role has no group");
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat den Bezahlstatus von {self} auf 'bezahlt' gesetzt."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn add_role(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &ManageUserUser,
|
||||
role: &Role,
|
||||
) -> Result<(), String> {
|
||||
if self.has_role(db, &role.name).await {
|
||||
return Err(format!(
|
||||
"Kann Rolle {role} von User {self} nicht hinzufügen, da der User die Rolle schon hat"
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
|
||||
self.id,
|
||||
role.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
format!(
|
||||
"User already has a role in the cluster '{}'",
|
||||
role.cluster
|
||||
.clone()
|
||||
.expect("db trigger can't activate on empty string")
|
||||
)
|
||||
})?;
|
||||
|
||||
if !role.hide_in_lists && role.cluster.is_none() {
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat die Rolle '{role}' dem Benutzer {self} hinzugefügt."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn remove_membership_pdf(&self, db: &SqlitePool, updated_by: &ManageUserUser) {
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat die Beitrittserklärung vom Beutzer gelöscht."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE user SET membership_pdf = null where id = ?",
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
}
|
||||
pub(crate) async fn add_membership_pdf(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
updated_by: &ManageUserUser,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
if self.has_membership_pdf(db).await {
|
||||
return Err(format!("User {self} hat bereits eine Beitrittserklärung."));
|
||||
}
|
||||
if membership_pdf.len() == 0 {
|
||||
return Err("Keine Beitrittserklärung mitgeschickt.".to_string());
|
||||
}
|
||||
|
||||
let mut stream = membership_pdf.open().await.unwrap();
|
||||
let mut buffer = Vec::new();
|
||||
stream.read_to_end(&mut buffer).await.unwrap();
|
||||
sqlx::query!(
|
||||
"UPDATE user SET membership_pdf = ? where id = ?",
|
||||
buffer,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a User of a valid id
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{updated_by} hat die Mitgliedserklärung (PDF) für user {self} hinzugefügt."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,167 +0,0 @@
|
||||
use super::User;
|
||||
use crate::{
|
||||
model::{
|
||||
activity::ActivityBuilder, notification::Notification, role::Role, user::ManageUserUser,
|
||||
},
|
||||
special_user,
|
||||
};
|
||||
use rocket::async_trait;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
special_user!(ClubMemberUser, +"Donau Linz", +"Förderndes Mitglied", +"Unterstützend");
|
||||
|
||||
impl ClubMemberUser {
|
||||
async fn add_membership_role(&self, db: &SqlitePool, role: &Role) {
|
||||
sqlx::query!(
|
||||
"INSERT INTO user_role(user_id, role_id) VALUES (?, ?)",
|
||||
self.id,
|
||||
role.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn remove_membership_role(&self, db: &SqlitePool) {
|
||||
let role = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap();
|
||||
sqlx::query!(
|
||||
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
|
||||
self.id,
|
||||
role.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let role = Role::find_by_name(db, "Unterstützend").await.unwrap();
|
||||
sqlx::query!(
|
||||
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
|
||||
self.id,
|
||||
role.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let role = Role::find_by_name(db, "Donau Linz").await.unwrap();
|
||||
sqlx::query!(
|
||||
"DELETE FROM user_role WHERE user_id = ? and role_id = ?",
|
||||
self.id,
|
||||
role.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
async fn new_membership_role(&self, db: &SqlitePool, role: &str) -> Result<(), String> {
|
||||
let role = Role::find_by_name(db, role).await.unwrap();
|
||||
self.remove_membership_role(db).await;
|
||||
self.add_membership_role(db, &role).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn move_to_regular(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
modified_by: &ManageUserUser,
|
||||
) -> Result<(), String> {
|
||||
if self.has_role(db, "Donau Linz").await {
|
||||
return Err(format!("User {self} ist bereits reguläres Mitglied."));
|
||||
}
|
||||
|
||||
self.new_membership_role(db, "Donau Linz").await?;
|
||||
|
||||
Notification::create_for_steering_people(
|
||||
db,
|
||||
&format!(
|
||||
"Liebe Steuerberechtigte, {} hat upgegraded und ist nun ein neues reguläres Mitglied. 🎉",
|
||||
self.name,
|
||||
),
|
||||
"Neues Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{modified_by} hat {self} zu einem regulären hochgestuft."
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn move_to_unterstuetzend(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
modified_by: &ManageUserUser,
|
||||
) -> Result<(), String> {
|
||||
if self.has_role(db, "Unterstützend").await {
|
||||
return Err(format!("User {self} ist bereits unterstützendes Mitglied."));
|
||||
}
|
||||
|
||||
self.new_membership_role(db, "Unterstützend").await?;
|
||||
|
||||
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!(
|
||||
"Lieber Vorstand, {} ist nun ein unterstützendes Mitglied.",
|
||||
self.name,
|
||||
),
|
||||
"Neues unterstützendes Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{modified_by} hat {self} zu einem unterstützenden Mitglied gemacht."
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn move_to_foerdernd(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
modified_by: &ManageUserUser,
|
||||
) -> Result<(), String> {
|
||||
if self.has_role(db, "Förderndes Mitglied").await {
|
||||
return Err(format!("User {self} ist bereits förderndes Mitglied."));
|
||||
}
|
||||
|
||||
self.new_membership_role(db, "Förderndes Mitglied").await?;
|
||||
|
||||
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!(
|
||||
"Lieber Vorstand, {} ist nun ein förderndes Mitglied.",
|
||||
self.name,
|
||||
),
|
||||
"Neues förderndes Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{modified_by} hat {self} zu ein förderndes Mitglied gemacht."
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,216 +0,0 @@
|
||||
use super::User;
|
||||
use crate::{
|
||||
BOAT_STORAGE, DUAL_MEMBERSHIP, EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND,
|
||||
REGULAR, RENNRUDERBEITRAG, SCHECKBUCH, STUDENT_OR_PUPIL, TRIAL_ROWING, TRIAL_ROWING_REDUCED,
|
||||
UNTERSTUETZEND, model::family::Family,
|
||||
};
|
||||
use chrono::{Datelike, Local, NaiveDate};
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct Fee {
|
||||
pub sum_in_cents: i64,
|
||||
pub parts: Vec<(String, i64)>,
|
||||
pub name: String,
|
||||
pub user_ids: String,
|
||||
pub paid: bool,
|
||||
pub users: Vec<User>,
|
||||
}
|
||||
|
||||
impl Default for Fee {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Fee {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
sum_in_cents: 0,
|
||||
name: "".into(),
|
||||
parts: Vec::new(),
|
||||
user_ids: "".into(),
|
||||
users: Vec::new(),
|
||||
paid: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add(&mut self, desc: String, price_in_cents: i64) {
|
||||
self.sum_in_cents += price_in_cents;
|
||||
|
||||
self.parts.push((desc, price_in_cents));
|
||||
}
|
||||
|
||||
pub fn add_person(&mut self, user: &User) {
|
||||
if !self.name.is_empty() {
|
||||
self.name.push_str(" + ");
|
||||
self.user_ids.push('&');
|
||||
}
|
||||
self.name.push_str(&user.name);
|
||||
|
||||
self.user_ids.push_str(&format!("user_ids[]={}", user.id));
|
||||
self.users.push(user.clone());
|
||||
}
|
||||
|
||||
pub fn paid(&mut self) {
|
||||
self.paid = true;
|
||||
}
|
||||
|
||||
pub fn merge(&mut self, fee: Fee) {
|
||||
for (desc, price_in_cents) in fee.parts {
|
||||
self.add(desc, price_in_cents);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub async fn fee(&self, db: &SqlitePool) -> Option<Fee> {
|
||||
if !self.has_role(db, "Donau Linz").await
|
||||
&& !self.has_role(db, "Unterstützend").await
|
||||
&& !self.has_role(db, "Förderndes Mitglied").await
|
||||
&& !self.has_role(db, "schnupperant").await
|
||||
&& !self.has_role(db, "scheckbuch").await
|
||||
{
|
||||
return None;
|
||||
}
|
||||
if self.deleted {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut fee = Fee::new();
|
||||
|
||||
if let Some(family) = Family::find_by_opt_id(db, self.family_id).await {
|
||||
for member in family.members(db).await {
|
||||
fee.add_person(&member);
|
||||
if member.has_role(db, "paid").await {
|
||||
fee.paid();
|
||||
}
|
||||
fee.merge(member.fee_without_families(db).await);
|
||||
}
|
||||
if family.amount_family_members(db).await > 2 {
|
||||
fee.add("Familie 3+ Personen".into(), FAMILY_THREE_OR_MORE);
|
||||
} else {
|
||||
fee.add("Familie 2 Personen".into(), FAMILY_TWO);
|
||||
}
|
||||
} else {
|
||||
fee.add_person(self);
|
||||
if self.has_role(db, "paid").await {
|
||||
fee.paid();
|
||||
}
|
||||
fee.merge(self.fee_without_families(db).await);
|
||||
}
|
||||
|
||||
Some(fee)
|
||||
}
|
||||
|
||||
async fn fee_without_families(&self, db: &SqlitePool) -> Fee {
|
||||
let mut fee = Fee::new();
|
||||
|
||||
if !self.has_role(db, "Donau Linz").await
|
||||
&& !self.has_role(db, "Unterstützend").await
|
||||
&& !self.has_role(db, "Förderndes Mitglied").await
|
||||
&& !self.has_role(db, "schnupperant").await
|
||||
&& !self.has_role(db, "scheckbuch").await
|
||||
{
|
||||
return fee;
|
||||
}
|
||||
if self.has_role(db, "Rennrudern").await {
|
||||
if self.has_role(db, "half-rennrudern").await {
|
||||
fee.add("Rennruderbeitrag (1/2 Preis) ".into(), RENNRUDERBEITRAG / 2);
|
||||
} else if !self.has_role(db, "renntrainer").await {
|
||||
fee.add("Rennruderbeitrag".into(), RENNRUDERBEITRAG);
|
||||
}
|
||||
}
|
||||
|
||||
let amount_boats = self.amount_boats(db).await;
|
||||
if amount_boats > 0 {
|
||||
fee.add(
|
||||
format!("{}x Bootsplatz", amount_boats),
|
||||
amount_boats * BOAT_STORAGE,
|
||||
);
|
||||
}
|
||||
|
||||
if !self.has_role(db, "schnupperant").await {
|
||||
if let Some(member_since_date) = &self.member_since_date {
|
||||
if let Ok(member_since_date) =
|
||||
NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d")
|
||||
{
|
||||
if member_since_date.year() == Local::now().year()
|
||||
&& !self.has_role(db, "no-einschreibgebuehr").await
|
||||
{
|
||||
fee.add("Einschreibgebühr".into(), EINSCHREIBGEBUEHR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let halfprice = if let Some(member_since_date) = &self.member_since_date {
|
||||
match NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d") {
|
||||
Ok(member_since_date) => {
|
||||
let halfprice_startdate =
|
||||
NaiveDate::from_ymd_opt(Local::now().year(), 7, 1).unwrap();
|
||||
member_since_date >= halfprice_startdate
|
||||
}
|
||||
Err(_) => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if self.has_role(db, "schnupperant").await {
|
||||
if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await {
|
||||
fee.add("Schnupperkurs (reduziert)".into(), TRIAL_ROWING_REDUCED);
|
||||
} else {
|
||||
fee.add("Schnupperkurs".into(), TRIAL_ROWING);
|
||||
}
|
||||
} else if self.has_role(db, "scheckbuch").await {
|
||||
fee.add("Scheckbuch".into(), SCHECKBUCH);
|
||||
} else if self.has_role(db, "Unterstützend").await {
|
||||
fee.add("Unterstützendes Mitglied".into(), UNTERSTUETZEND);
|
||||
} else if self.has_role(db, "Förderndes Mitglied").await {
|
||||
fee.add("Förderndes Mitglied".into(), FOERDERND);
|
||||
} else if Family::find_by_opt_id(db, self.family_id).await.is_none() {
|
||||
if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await {
|
||||
if halfprice {
|
||||
fee.add("Schüler/Student (Halbpreis)".into(), STUDENT_OR_PUPIL / 2);
|
||||
} else {
|
||||
fee.add("Schüler/Student".into(), STUDENT_OR_PUPIL);
|
||||
}
|
||||
} else if self.has_role(db, "Ehrenmitglied").await {
|
||||
fee.add("Ehrenmitglied".into(), 0);
|
||||
} else if self.has_role(db, "dual_membership").await {
|
||||
if halfprice {
|
||||
fee.add(
|
||||
"Doppelmitgliedschaft mit anderem österr. Ruderverein (Halbpreis)".into(),
|
||||
DUAL_MEMBERSHIP / 2,
|
||||
);
|
||||
} else {
|
||||
fee.add(
|
||||
"Doppelmitgliedschaft mit anderem österr. Ruderverein".into(),
|
||||
DUAL_MEMBERSHIP,
|
||||
);
|
||||
}
|
||||
} else if halfprice {
|
||||
fee.add("Mitgliedsbeitrag (Halbpreis)".into(), REGULAR / 2);
|
||||
} else {
|
||||
fee.add("Mitgliedsbeitrag".into(), REGULAR);
|
||||
}
|
||||
}
|
||||
|
||||
if !self.has_role(db, "schnupperant").await
|
||||
&& self.has_role(db, "participated_schnupperkurs").await
|
||||
{
|
||||
if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await {
|
||||
fee.add(
|
||||
"Anrechnung reduzierter Schnupperkurs".into(),
|
||||
-TRIAL_ROWING_REDUCED,
|
||||
);
|
||||
} else {
|
||||
fee.add("Anrechnung Schnupperkurs".into(), -TRIAL_ROWING);
|
||||
}
|
||||
}
|
||||
|
||||
fee
|
||||
}
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
use super::{ManageUserUser, User, regular::ClubMember};
|
||||
use crate::{
|
||||
NonEmptyString,
|
||||
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
|
||||
special_user,
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use rocket::{async_trait, fs::TempFile};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
special_user!(FoerderndUser, +"Förderndes Mitglied");
|
||||
|
||||
impl ClubMember for FoerderndUser {}
|
||||
|
||||
impl FoerderndUser {
|
||||
pub(crate) async fn send_welcome_mail_to_user(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let Some(mail) = &self.mail else {
|
||||
return Err(format!(
|
||||
"Couldn't send welcome mail, as the user {self} has no mail..."
|
||||
));
|
||||
};
|
||||
|
||||
Mail::send_single(
|
||||
db,
|
||||
mail,
|
||||
"Willkommen im ASKÖ Ruderverein Donau Linz!",
|
||||
format!(
|
||||
"Hallo {0},
|
||||
|
||||
herzlich willkommen im ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dich als neues Mitglied in unserem Verein begrüßen zu dürfen.
|
||||
|
||||
Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at (für allgemeine Fragen) und it@rudernlinz.at (bei technischen Fragen) jederzeit zur Verfügung.
|
||||
|
||||
Damit du dich noch mehr verbunden fühlst (:-)), haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz' eingerichtet. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
|
||||
|
||||
Riemen- & Dollenbruch
|
||||
ASKÖ Ruderverein Donau Linz", self.name),
|
||||
smtp_pw,
|
||||
).await?;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"User {self} hat die Info-Mail bzgl. neues förderndes Mitglied (Handbuch und WLAN Infos) an {mail} gesendet bekommen"
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn create(
|
||||
db: &SqlitePool,
|
||||
created_by: &ManageUserUser,
|
||||
smtp_pw: &str,
|
||||
name: NonEmptyString,
|
||||
mail: &str,
|
||||
financial: Option<Role>,
|
||||
birthdate: &NaiveDate,
|
||||
member_since: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
let role = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap();
|
||||
let user = Self::create_member(
|
||||
db,
|
||||
created_by,
|
||||
&role,
|
||||
name,
|
||||
mail,
|
||||
financial,
|
||||
birthdate,
|
||||
member_since,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let user = Self::new(db, &user).await.unwrap();
|
||||
user.send_welcome_mail_to_user(db, smtp_pw).await?;
|
||||
|
||||
if let Some(vorstand) = Role::find_by_name(db, "Vorstand").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!("Lieber Vorstand, es gibt ein neues förderndes Mitglied: {user}"),
|
||||
"Neues unterstützendes Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
use super::ScheckbuchUser;
|
||||
use crate::model::{
|
||||
logbook::{Logbook, LogbookWithBoatAndRowers},
|
||||
user::User,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub(crate) enum Member {
|
||||
SchnupperInterest(User),
|
||||
Schnupperant(User),
|
||||
Scheckbuch(Vec<LogbookWithBoatAndRowers>),
|
||||
Regular(User),
|
||||
Foerdernd(User),
|
||||
Unterstuetzend(User),
|
||||
}
|
||||
|
||||
impl Member {
|
||||
pub(crate) async fn from(db: &SqlitePool, user: User) -> Self {
|
||||
if ScheckbuchUser::new(db, &user).await.is_some() {
|
||||
Self::Scheckbuch(Logbook::completed_with_user(db, &user).await)
|
||||
} else if user.has_role(db, "schnupper-interessierte").await {
|
||||
Self::SchnupperInterest(user)
|
||||
} else if user.has_role(db, "schnupperant").await {
|
||||
Self::Schnupperant(user)
|
||||
} else if user.has_role(db, "Donau Linz").await {
|
||||
Self::Regular(user)
|
||||
} else if user.has_role(db, "Förderndes Mitglied").await {
|
||||
Self::Foerdernd(user)
|
||||
} else if user.has_role(db, "Unterstützend").await {
|
||||
Self::Unterstuetzend(user)
|
||||
} else {
|
||||
panic!("User {user} has no membership_type!!");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_club_member(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Member::Regular(_) | Member::Foerdernd(_) | Member::Unterstuetzend(_)
|
||||
)
|
||||
}
|
||||
pub(crate) fn supposed_to_pay(&self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Member::Schnupperant(_)
|
||||
| Member::Scheckbuch(_)
|
||||
| Member::Regular(_)
|
||||
| Member::Foerdernd(_)
|
||||
| Member::Unterstuetzend(_)
|
||||
)
|
||||
}
|
||||
}
|
@ -1,157 +0,0 @@
|
||||
use super::{ManageUserUser, User};
|
||||
use crate::{
|
||||
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
|
||||
special_user, NonEmptyString,
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use rocket::{async_trait, fs::TempFile, tokio::io::AsyncReadExt};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
special_user!(RegularUser, +"Donau Linz");
|
||||
|
||||
pub trait ClubMember {
|
||||
async fn create_member(
|
||||
db: &SqlitePool,
|
||||
created_by: &ManageUserUser,
|
||||
role: &Role,
|
||||
name: NonEmptyString,
|
||||
mail: &str,
|
||||
financial: Option<Role>,
|
||||
birthdate: &NaiveDate,
|
||||
member_since: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<User, String> {
|
||||
if membership_pdf.len() == 0 {
|
||||
return Err("Keine Beitrittserklärung mitgeschickt.".to_string());
|
||||
}
|
||||
|
||||
let mut stream = membership_pdf.open().await.unwrap();
|
||||
let mut buffer = Vec::new();
|
||||
stream.read_to_end(&mut buffer).await.unwrap();
|
||||
|
||||
let name = name.as_str();
|
||||
let phone = phone.as_str();
|
||||
let address = address.as_str();
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO user(name, member_since_date, birthdate, mail, phone, address, membership_pdf)
|
||||
VALUES (?,?,?,?,?,?,?)",
|
||||
name, member_since, birthdate, mail, phone, address,buffer
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let user = User::find_by_name(db, name).await.unwrap();
|
||||
user.change_financial(db, created_by, financial).await?;
|
||||
user.add_role(db, created_by, role).await?;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{created_by} hat Mitglied {user} mit der Rolle {role} angelegt."
|
||||
))
|
||||
.user(&user)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(user)
|
||||
}
|
||||
}
|
||||
|
||||
impl ClubMember for RegularUser {}
|
||||
|
||||
impl RegularUser {
|
||||
pub(crate) async fn send_welcome_mail_to_user(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let Some(mail) = &self.mail else {
|
||||
return Err(format!(
|
||||
"Couldn't send welcome mail, as the user {self} has no mail..."
|
||||
));
|
||||
};
|
||||
|
||||
Mail::send_single(
|
||||
db,
|
||||
mail,
|
||||
"Willkommen im ASKÖ Ruderverein Donau Linz!",
|
||||
format!(
|
||||
"Hallo {self},
|
||||
|
||||
herzlich willkommen im ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dich als neues Mitglied in unserem Verein begrüßen zu dürfen.
|
||||
|
||||
Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at (für allgemeine Fragen) und it@rudernlinz.at (bei technischen Fragen) jederzeit zur Verfügung.
|
||||
|
||||
Du kannst auch gerne unserer Signal-Gruppe beitreten, um auf dem Laufenden zu bleiben und dich mit anderen Mitgliedern auszutauschen: https://signal.group/#CjQKICFrq6zSsRHxrucS3jEcQn6lknEXacAykwwLV3vNLKxPEhA17jxz7cpjfu3JZokLq1TH
|
||||
|
||||
Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge dich einfach mit deinem Namen ('{self}' ohne Anführungszeichen) ein, beim ersten Mal kannst du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst du dich jederzeit zu den Ausfahrten anmelden.
|
||||
|
||||
Beim nächsten Treffen im Verein, erinnere jemand vom Vorstand (https://rudernlinz.at/unser-verein/vorstand/) bitte daran, deinen Fingerabdruck zu registrieren, damit du Zugang zum Bootshaus erhältst.
|
||||
|
||||
Damit du dich noch mehr verbunden fühlst (:-)), haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz' eingerichtet. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
|
||||
|
||||
Falls du deinen Mitgliedsbeitrag noch nicht bezahlt hast, erledige dies bitte demnächst. Den genauen Betrag und einen QR Code, den du mit deiner Bankapp scannen kannst findest du unter https://app.rudernlinz.at/planned
|
||||
|
||||
Wenn du alle Ausfahrten, zu denen du dich angemeldet hast in deinem eigenen Kalender sehen willst, füge folgenden Link hinzu: https://app.rudernlinz.at/cal/personal/{}/{}
|
||||
|
||||
Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
|
||||
|
||||
Riemen- & Dollenbruch
|
||||
ASKÖ Ruderverein Donau Linz", self.user.id, self.user.user_token),
|
||||
smtp_pw,
|
||||
).await?;
|
||||
|
||||
ActivityBuilder::new(&format!("Willkommensmail für {self} wurde an {mail} verschickt (Handbuch, Signal-Gruppe, App-Info, Fingerprint, WLAN)."))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn create(
|
||||
db: &SqlitePool,
|
||||
created_by: &ManageUserUser,
|
||||
smtp_pw: &str,
|
||||
name: NonEmptyString,
|
||||
mail: &str,
|
||||
financial: Option<Role>,
|
||||
birthdate: &NaiveDate,
|
||||
member_since: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
let role = Role::find_by_name(db, "Donau Linz").await.unwrap();
|
||||
let user = Self::create_member(
|
||||
db,
|
||||
created_by,
|
||||
&role,
|
||||
name,
|
||||
mail,
|
||||
financial,
|
||||
birthdate,
|
||||
member_since,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let user = Self::new(db, &user).await.unwrap();
|
||||
user.send_welcome_mail_to_user(db, smtp_pw).await?;
|
||||
|
||||
Notification::create_for_steering_people(
|
||||
db,
|
||||
&format!("Liebe Steuerberechtigte, es gibt ein neues Mitglied: {user} 🎉"),
|
||||
"Neues Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,304 +0,0 @@
|
||||
use super::foerdernd::FoerderndUser;
|
||||
use super::regular::RegularUser;
|
||||
use super::unterstuetzend::UnterstuetzendUser;
|
||||
use super::{ManageUserUser, User};
|
||||
use crate::NonEmptyString;
|
||||
use crate::model::activity::ActivityBuilder;
|
||||
use crate::model::role::Role;
|
||||
use crate::{
|
||||
SCHECKBUCH,
|
||||
model::{mail::Mail, notification::Notification},
|
||||
special_user,
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use rocket::async_trait;
|
||||
use rocket::fs::TempFile;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
special_user!(ScheckbuchUser, +"scheckbuch");
|
||||
|
||||
impl ScheckbuchUser {
|
||||
async fn set_data_for_clubmember(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
self.user.update_birthdate(db, changed_by, birthdate).await;
|
||||
self.user
|
||||
.update_member_since(db, changed_by, member_since)
|
||||
.await;
|
||||
|
||||
self.user.update_phone(db, changed_by, &phone).await;
|
||||
self.user.update_address(db, changed_by, &address).await;
|
||||
self.user
|
||||
.add_membership_pdf(db, changed_by, membership_pdf)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) async fn convert_to_regular_user(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
self.set_data_for_clubmember(
|
||||
db,
|
||||
changed_by,
|
||||
member_since,
|
||||
birthdate,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Change roles
|
||||
let regular = Role::find_by_name(db, "Donau Linz").await.unwrap();
|
||||
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
|
||||
self.user.remove_role(db, changed_by, &scheckbook).await?;
|
||||
self.user.add_role(db, changed_by, ®ular).await?;
|
||||
|
||||
// Notify
|
||||
let regular = RegularUser::new(db, &self.user).await.unwrap();
|
||||
regular.send_welcome_mail_to_user(db, smtp_pw).await?;
|
||||
Notification::create_for_steering_people(
|
||||
db,
|
||||
&format!(
|
||||
"Liebe Steuerberechtigte, {} hatte ein Scheckbuch und ist nun seit {} ein neues reguläres Mitglied. 🎉",
|
||||
self.name,
|
||||
member_since
|
||||
),
|
||||
"Neues Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat den Scheckbuch-User {self} auf ein reguläres Mitglied upgegraded! Die Steuerpersonen wurden via Notification informiert."
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn convert_to_unterstuetzend_user(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
// Set data
|
||||
self.set_data_for_clubmember(
|
||||
db,
|
||||
changed_by,
|
||||
member_since,
|
||||
birthdate,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Change roles
|
||||
let unterstuetzend = Role::find_by_name(db, "Unterstützend").await.unwrap();
|
||||
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
|
||||
self.user.remove_role(db, changed_by, &scheckbook).await?;
|
||||
self.user.add_role(db, changed_by, &unterstuetzend).await?;
|
||||
|
||||
let unterstuetzend = UnterstuetzendUser::new(db, &self.user).await.unwrap();
|
||||
unterstuetzend
|
||||
.send_welcome_mail_to_user(db, smtp_pw)
|
||||
.await?;
|
||||
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!(
|
||||
"Lieber Vorstand, {} hatte ein Scheckbuch und ist nun seit {} ein neues unterstützendes Mitglied.",
|
||||
self.name,
|
||||
member_since
|
||||
),
|
||||
"Neues unterstützendes Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActivityBuilder::new(&format!("{changed_by} hat den Scheckbuch-User {self} auf ein unterstützendes Mitglied upgegraded!"))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn convert_to_foerdernd_user(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
// Set data
|
||||
self.set_data_for_clubmember(
|
||||
db,
|
||||
changed_by,
|
||||
member_since,
|
||||
birthdate,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Change roles
|
||||
let unterstuetzend = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap();
|
||||
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
|
||||
self.user.remove_role(db, changed_by, &scheckbook).await?;
|
||||
self.user.add_role(db, changed_by, &unterstuetzend).await?;
|
||||
|
||||
let foerdernd = FoerderndUser::new(db, &self.user).await.unwrap();
|
||||
foerdernd.send_welcome_mail_to_user(db, smtp_pw).await?;
|
||||
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!(
|
||||
"Lieber Vorstand, {} hatte ein Scheckbuch und ist nun seit {} ein neues förderndes Mitglied.",
|
||||
self.name,
|
||||
member_since
|
||||
),
|
||||
"Neues förderndes Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat den Scheckbuch-User {self} auf ein förderndes Mitglied upgegraded!"
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: make private
|
||||
pub(crate) async fn notify(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
|
||||
self.notify_coxes_about_new_scheckbuch(db).await;
|
||||
self.send_welcome_mail_to_user(db, smtp_pw).await?;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{self} hat eine Info-Mail bekommen (Erklärung Scheckbuch, Ruderapp) und alle Steuerberechtigten wurden informiert."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn send_welcome_mail_to_user(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let Some(mail) = &self.mail else {
|
||||
return Err(
|
||||
"Kann Mail nicht versenden, weil der User keine Mailadresse hinterlegt hat.".into(),
|
||||
);
|
||||
};
|
||||
Mail::send_single(
|
||||
db,
|
||||
mail,
|
||||
"ASKÖ Ruderverein Donau Linz | Dein Scheckbuch wartet auf Dich",
|
||||
format!(
|
||||
"Hallo {0},
|
||||
|
||||
herzlich willkommen beim ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dass Du Dich entschieden hast, das Rudern bei uns auszuprobieren. Mit Deinem Scheckbuch kannst Du jetzt an fünf Ausfahrten teilnehmen und so diesen Sport in seiner vollen Vielfalt erleben. Falls du die {1} € noch nicht bezahlt hast, nimm diese bitte zur nächsten Ausfahrt mit (oder überweise sie auf unser Bankkonto [dieses findest du auf https://rudernlinz.at]).
|
||||
|
||||
Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge Dich bitte mit Deinem Namen ('{0}', ohne Anführungszeichen) ein. Beim ersten Mal kannst Du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst Du Dich jederzeit für eine Ausfahrt anmelden. Wir bieten mindestens einmal pro Woche Ausfahrten an, sowohl für Anfänger als auch für Fortgeschrittene (A+F Rudern). Zusätzliche Ausfahrten werden von unseren Steuerleuten ausgeschrieben, öfters reinschauen kann sich also lohnen :-)
|
||||
|
||||
Nach deinen 5 Ausfahrten würden wir uns freuen, dich als Mitglied in unserem Verein begrüßen zu dürfen.
|
||||
|
||||
Wir freuen uns darauf, Dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
|
||||
|
||||
Riemen- & Dollenbruch,
|
||||
ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
|
||||
smtp_pw,
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn notify_coxes_about_new_scheckbuch(&self, db: &SqlitePool) {
|
||||
Notification::create_for_steering_people(
|
||||
db,
|
||||
&format!(
|
||||
"Liebe Steuerberechtigte, {} hat nun ein Scheckbuch. Wie immer, freuen wir uns wenn du uns beim A+F Rudern unterstützt oder selber Ausfahrten ausschreibst. Bitte beachte, dass Scheckbuch-Personen nur Ausfahrten sehen, bei denen 'Scheckbuch-Anmeldungen erlauben' ausgewählt wurde.",
|
||||
self.name
|
||||
),
|
||||
"Neues Scheckbuch",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub(crate) async fn create(
|
||||
db: &SqlitePool,
|
||||
created_by: &ManageUserUser,
|
||||
smtp_pw: &str,
|
||||
name: NonEmptyString,
|
||||
mail: &str,
|
||||
) -> Result<(), String> {
|
||||
let role = Role::find_by_name(db, "scheckbuch").await.unwrap();
|
||||
|
||||
let name = name.as_str();
|
||||
sqlx::query!(
|
||||
"INSERT INTO user(name, mail)
|
||||
VALUES (?,?)",
|
||||
name,
|
||||
mail
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let user = User::find_by_name(db, name).await.unwrap();
|
||||
user.add_role(db, created_by, &role).await?;
|
||||
|
||||
let user = Self::new(db, &user).await.unwrap();
|
||||
user.notify(db, smtp_pw).await?;
|
||||
|
||||
ActivityBuilder::new(&format!("{created_by} hat Scheckbuch {user} angelegt."))
|
||||
.user(&user)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,432 +0,0 @@
|
||||
use super::foerdernd::FoerderndUser;
|
||||
use super::regular::RegularUser;
|
||||
use super::scheckbuch::ScheckbuchUser;
|
||||
use super::schnupperinterest::SchnupperInterestUser;
|
||||
use super::unterstuetzend::UnterstuetzendUser;
|
||||
use super::{ManageUserUser, User};
|
||||
use crate::NonEmptyString;
|
||||
use crate::model::activity::ActivityBuilder;
|
||||
use crate::model::role::Role;
|
||||
use crate::{
|
||||
model::{mail::Mail, notification::Notification},
|
||||
special_user,
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use rocket::async_trait;
|
||||
use rocket::fs::TempFile;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
special_user!(SchnupperantUser, +"schnupperant");
|
||||
|
||||
impl SchnupperantUser {
|
||||
async fn set_data_for_clubmember(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
self.user.update_birthdate(db, changed_by, birthdate).await;
|
||||
self.user
|
||||
.update_member_since(db, changed_by, member_since)
|
||||
.await;
|
||||
|
||||
self.user.update_phone(db, changed_by, &phone).await;
|
||||
self.user.update_address(db, changed_by, &address).await;
|
||||
self.user
|
||||
.add_membership_pdf(db, changed_by, membership_pdf)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub(crate) async fn convert_to_regular_user(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
self.set_data_for_clubmember(
|
||||
db,
|
||||
changed_by,
|
||||
member_since,
|
||||
birthdate,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Change roles
|
||||
let paid = Role::find_by_name(db, "paid").await.unwrap();
|
||||
if self.user.remove_role(db, changed_by, &paid).await.is_err() {
|
||||
self.remove_membership_pdf(db, changed_by).await;
|
||||
return Err("Kann noch kein normales Mitglied werden, da die Schnupperkurs-Gebühr noch nicht bezahlt wurde.".into());
|
||||
}
|
||||
let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap();
|
||||
self.user.remove_role(db, changed_by, &scheckbook).await?;
|
||||
|
||||
let regular = Role::find_by_name(db, "Donau Linz").await.unwrap();
|
||||
self.user.add_role(db, changed_by, ®ular).await?;
|
||||
|
||||
let participated_schnupperkurs = Role::find_by_name(db, "participated_schnupperkurs")
|
||||
.await
|
||||
.unwrap();
|
||||
self.user
|
||||
.add_role(db, changed_by, &participated_schnupperkurs)
|
||||
.await?;
|
||||
|
||||
// Notify
|
||||
let regular = RegularUser::new(db, &self.user).await.unwrap();
|
||||
regular.send_welcome_mail_to_user(db, smtp_pw).await?;
|
||||
Notification::create_for_steering_people(
|
||||
db,
|
||||
&format!(
|
||||
"Liebe Steuerberechtigte, {} nahm an unserem Schnupperkurs teil und ist nun seit {member_since} ein neues reguläres Mitglied. 🎉",
|
||||
self.name
|
||||
),
|
||||
"Neues Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat den Schnupperant {self} auf ein reguläres Mitglied upgegraded!"
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn move_to_scheckbook(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
changed_by: &ManageUserUser,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
|
||||
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
|
||||
self.user.remove_role(db, changed_by, &schnupperant).await?;
|
||||
self.user.add_role(db, changed_by, &scheckbook).await?;
|
||||
|
||||
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
|
||||
self.add_role(db, changed_by, &no_einschreibgebuehr)
|
||||
.await
|
||||
.expect("role doesn't have a group");
|
||||
}
|
||||
|
||||
let scheckbook = ScheckbuchUser::new(db, &self.user).await.unwrap();
|
||||
scheckbook.notify(db, smtp_pw).await?;
|
||||
|
||||
Notification::create_for_steering_people(
|
||||
db,
|
||||
&format!(
|
||||
"Liebe Steuerberechtigte, {} hat unseren Schnupperkurs absolviert und nun ein Scheckbuch. Wie immer, freuen wir uns wenn du uns beim A+F Rudern unterstützt oder selber Ausfahrten ausschreibst. Bitte beachte, dass Scheckbuch-Personen nur Ausfahrten sehen, bei denen 'Scheckbuch-Anmeldungen erlauben' ausgewählt wurde.",
|
||||
self.name
|
||||
),
|
||||
"Neues Scheckbuch",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat dem ehemaligen Schnupperant {self} nun ein Scheckbuch gegeben"
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn move_to_schnupperinterest(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
changed_by: &ManageUserUser,
|
||||
) -> Result<(), String> {
|
||||
let schnupperinterest = Role::find_by_name(db, "schnupper-interessierte")
|
||||
.await
|
||||
.unwrap();
|
||||
let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
|
||||
self.user.remove_role(db, changed_by, &schnupperant).await?;
|
||||
self.user
|
||||
.add_role(db, changed_by, &schnupperinterest)
|
||||
.await?;
|
||||
|
||||
let schnupperinterest = SchnupperInterestUser::new(db, &self.user).await.unwrap();
|
||||
schnupperinterest.notify(db).await?;
|
||||
|
||||
if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&role,
|
||||
&format!(
|
||||
"Lieber Schnupperbetreuer, {} hat sich vom Schnupperkurs abgemeldet.",
|
||||
self.name
|
||||
),
|
||||
"Schnupperkurs Abmeldung",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat dem eigentlichen Schnupperanten {self} wieder auf die 'Interessierten'-Liste zurückgegeben."
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn convert_to_unterstuetzend_user(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
// Set data
|
||||
self.set_data_for_clubmember(
|
||||
db,
|
||||
changed_by,
|
||||
member_since,
|
||||
birthdate,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Change roles
|
||||
let paid = Role::find_by_name(db, "paid").await.unwrap();
|
||||
if self.user.remove_role(db, changed_by, &paid).await.is_err() {
|
||||
self.remove_membership_pdf(db, changed_by).await;
|
||||
return Err("Kann noch kein normales Mitglied werden, da die Schnupperkurs-Gebühr noch nicht bezahlt wurde.".into());
|
||||
}
|
||||
let unterstuetzend = Role::find_by_name(db, "Unterstützend").await.unwrap();
|
||||
let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap();
|
||||
self.user.remove_role(db, changed_by, &scheckbook).await?;
|
||||
self.user.add_role(db, changed_by, &unterstuetzend).await?;
|
||||
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
|
||||
self.add_role(db, changed_by, &no_einschreibgebuehr)
|
||||
.await
|
||||
.expect("role doesn't have a group");
|
||||
}
|
||||
|
||||
let unterstuetzend = UnterstuetzendUser::new(db, &self.user).await.unwrap();
|
||||
unterstuetzend
|
||||
.send_welcome_mail_to_user(db, smtp_pw)
|
||||
.await?;
|
||||
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!(
|
||||
"Lieber Vorstand, {} nahm am Schnupperkurs teil und ist nun seit {} es ein neues unterstützendes Mitglied.",
|
||||
self.name,
|
||||
self.member_since_date.clone().unwrap()
|
||||
),
|
||||
"Neues unterstützendes Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat den Schnupperant {self} auf ein unterstützendes Mitglied upgegraded!"
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn convert_to_foerdernd_user(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
changed_by: &ManageUserUser,
|
||||
member_since: &NaiveDate,
|
||||
birthdate: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
// Set data
|
||||
self.set_data_for_clubmember(
|
||||
db,
|
||||
changed_by,
|
||||
member_since,
|
||||
birthdate,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Change roles
|
||||
let paid = Role::find_by_name(db, "paid").await.unwrap();
|
||||
if self.user.remove_role(db, changed_by, &paid).await.is_err() {
|
||||
self.remove_membership_pdf(db, changed_by).await;
|
||||
return Err("Kann noch kein normales Mitglied werden, da die Schnupperkurs-Gebühr noch nicht bezahlt wurde.".into());
|
||||
}
|
||||
let unterstuetzend = Role::find_by_name(db, "Förderndes Mitglied").await.unwrap();
|
||||
let scheckbook = Role::find_by_name(db, "schnupperant").await.unwrap();
|
||||
self.user.remove_role(db, changed_by, &scheckbook).await?;
|
||||
self.user.add_role(db, changed_by, &unterstuetzend).await?;
|
||||
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
|
||||
self.add_role(db, changed_by, &no_einschreibgebuehr)
|
||||
.await
|
||||
.expect("role doesn't have a group");
|
||||
}
|
||||
|
||||
let foerdernd = FoerderndUser::new(db, &self.user).await.unwrap();
|
||||
foerdernd.send_welcome_mail_to_user(db, smtp_pw).await?;
|
||||
if let Some(vorstand) = Role::find_by_name(db, "vorstand").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!(
|
||||
"Lieber Vorstand, {} nahm am Schnupperkurs teil und ist nun seit {} es ein neues förderndes Mitglied.",
|
||||
self.name,
|
||||
self.member_since_date.clone().unwrap()
|
||||
),
|
||||
"Neues förderndes Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{changed_by} hat den Schnupperant {self} auf ein förderndes Mitglied upgegraded!"
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: make private
|
||||
pub(crate) async fn notify(&self, db: &SqlitePool, smtp_pw: &str) -> Result<(), String> {
|
||||
self.notify_coxes_about_new_schnupperant(db).await;
|
||||
self.send_welcome_mail_to_user(db, smtp_pw).await?;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{self} hat eine Mail bekommen (Inhalt: wir freuen uns auf ihn + senden detailliertere Infos später zu) und die Schnupperbetreuer wurden via Notification informiert."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_welcome_mail_to_user(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let Some(mail) = &self.mail else {
|
||||
return Err(format!(
|
||||
"Couldn't send mail, because user {self} has no mail"
|
||||
));
|
||||
};
|
||||
Mail::send_single(
|
||||
db,
|
||||
mail,
|
||||
"ASKÖ Ruderverein Donau Linz | Anmeldung Schnupperkurs",
|
||||
format!(
|
||||
"Hallo {0},
|
||||
|
||||
es freut uns sehr, dich bei unserem Schnupperkurs willkommen heißen zu dürfen.
|
||||
|
||||
Bitte überweise die {1} € auf unser Bankkonto (IBAN: AT58 2032 0321 0072 9256) und gib beim Verwendungszweck 'Schnupperkurs {0}' an.
|
||||
|
||||
Detaillierte Informationen folgen noch, du wirst sie ein paar Tage vor dem Termin bekommen (wenn das Wetter/Wasserstand/... abschätzbar ist).
|
||||
|
||||
Riemen- & Dollenbruch,
|
||||
ASKÖ Ruderverein Donau Linz",
|
||||
self.name,
|
||||
self.fee(db).await.unwrap().sum_in_cents/100
|
||||
),
|
||||
smtp_pw,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn notify_coxes_about_new_schnupperant(&self, db: &SqlitePool) {
|
||||
if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&role,
|
||||
&format!(
|
||||
"Lieber Schnupperbetreuer, {} hat sich zum Schnupperkurs angemeldet.",
|
||||
self.name
|
||||
),
|
||||
"Neuer Schnupperant",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn create(
|
||||
db: &SqlitePool,
|
||||
created_by: &ManageUserUser,
|
||||
smtp_pw: &str,
|
||||
name: NonEmptyString,
|
||||
mail: &str,
|
||||
) -> Result<(), String> {
|
||||
let role = Role::find_by_name(db, "schnupperant").await.unwrap();
|
||||
|
||||
let name = name.as_str();
|
||||
sqlx::query!(
|
||||
"INSERT INTO user(name, mail)
|
||||
VALUES (?,?)",
|
||||
name,
|
||||
mail
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let user = User::find_by_name(db, name).await.unwrap();
|
||||
user.add_role(db, created_by, &role).await?;
|
||||
|
||||
let user = Self::new(db, &user).await.unwrap();
|
||||
user.notify(db, smtp_pw).await?;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{created_by} hat {user} zur fixen Schnupperkurs-Anmeldung hinzugefügt."
|
||||
))
|
||||
.user(&user)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,162 +0,0 @@
|
||||
use super::scheckbuch::ScheckbuchUser;
|
||||
use super::schnupperant::SchnupperantUser;
|
||||
use super::{ManageUserUser, User};
|
||||
use crate::NonEmptyString;
|
||||
use crate::model::activity::ActivityBuilder;
|
||||
use crate::model::role::Role;
|
||||
use crate::{model::notification::Notification, special_user};
|
||||
use rocket::async_trait;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
special_user!(SchnupperInterestUser, +"schnupper-interessierte");
|
||||
|
||||
impl SchnupperInterestUser {
|
||||
pub(crate) async fn move_to_scheckbook(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
changed_by: &ManageUserUser,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let schnupperinterest = Role::find_by_name(db, "schnupper-interessierte")
|
||||
.await
|
||||
.unwrap();
|
||||
let scheckbook = Role::find_by_name(db, "scheckbuch").await.unwrap();
|
||||
self.user
|
||||
.remove_role(db, changed_by, &schnupperinterest)
|
||||
.await?;
|
||||
self.user.add_role(db, changed_by, &scheckbook).await?;
|
||||
|
||||
let scheckbook = ScheckbuchUser::new(db, &self.user).await.unwrap();
|
||||
scheckbook.notify(db, smtp_pw).await?;
|
||||
|
||||
Notification::create_for_steering_people(
|
||||
db,
|
||||
&format!(
|
||||
"Liebe Steuerberechtigte, {} wollte unseren Schnupperkurs absolviert und nun ein Scheckbuch. Wie immer, freuen wir uns wenn du uns beim A+F Rudern unterstützt oder selber Ausfahrten ausschreibst. Bitte beachte, dass Scheckbuch-Personen nur Ausfahrten sehen, bei denen 'Scheckbuch-Anmeldungen erlauben' ausgewählt wurde.",
|
||||
self.name
|
||||
),
|
||||
"Neues Scheckbuch",
|
||||
None,
|
||||
None
|
||||
)
|
||||
.await;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"Der Schnupperinteressierte {self} hat sich (ohne Schnupperkurs) doch gleich direkt für ein Scheckbuch entschieden. {changed_by} hat dieses eingerichtet."
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn move_to_schnupperant(
|
||||
self,
|
||||
db: &SqlitePool,
|
||||
changed_by: &ManageUserUser,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let schnupperinterest = Role::find_by_name(db, "schnupper-interessierte")
|
||||
.await
|
||||
.unwrap();
|
||||
let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
|
||||
self.user
|
||||
.remove_role(db, changed_by, &schnupperinterest)
|
||||
.await?;
|
||||
self.user.add_role(db, changed_by, &schnupperant).await?;
|
||||
|
||||
let schnupperant = SchnupperantUser::new(db, &self.user).await.unwrap();
|
||||
schnupperant.notify(db, smtp_pw).await?;
|
||||
|
||||
if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&role,
|
||||
&format!(
|
||||
"Lieber Schnupperbetreuer, {} hat sich zum Schnupperkurs angemeldet.",
|
||||
self.name
|
||||
),
|
||||
"Neuer Schnupper-Interessierte:r",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
ActivityBuilder::new(&format!(
|
||||
"Der Schnupperinteressierte {self} hat sich zum Schnupperkurs angemeldet."
|
||||
))
|
||||
.user(&self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn notify(&self, db: &SqlitePool) -> Result<(), String> {
|
||||
self.notify_schnupperbetreuer_about_new_interest(db).await;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"Der Schnupperbetreuer hat eine Info via Notification bekommen, dass {self} Interesse an einen Schnupperkurs hat."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn notify_schnupperbetreuer_about_new_interest(&self, db: &SqlitePool) {
|
||||
if let Some(role) = Role::find_by_name(db, "schnupper-betreuer").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&role,
|
||||
&format!(
|
||||
"Lieber Schnupperbetreuer, {} hat Interesse zum Schnupperkurs bekundet.",
|
||||
self.name
|
||||
),
|
||||
"Neuer Schnupper-Interessierte:r",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn create(
|
||||
db: &SqlitePool,
|
||||
created_by: &ManageUserUser,
|
||||
name: NonEmptyString,
|
||||
mail: &str,
|
||||
) -> Result<(), String> {
|
||||
let role = Role::find_by_name(db, "schnupper-interessierte")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let name = name.as_str();
|
||||
sqlx::query!(
|
||||
"INSERT INTO user(name, mail)
|
||||
VALUES (?,?)",
|
||||
name,
|
||||
mail
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let user = User::find_by_name(db, name).await.unwrap();
|
||||
user.add_role(db, created_by, &role).await?;
|
||||
|
||||
let user = Self::new(db, &user).await.unwrap();
|
||||
user.notify(db).await?;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{created_by} hat Schnupper-Interessierten {user} angelegt."
|
||||
))
|
||||
.user(&user)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -1,101 +0,0 @@
|
||||
use super::{ManageUserUser, User, regular::ClubMember};
|
||||
use crate::{
|
||||
NonEmptyString,
|
||||
model::{activity::ActivityBuilder, mail::Mail, notification::Notification, role::Role},
|
||||
special_user,
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use rocket::{async_trait, fs::TempFile};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
special_user!(UnterstuetzendUser, +"Unterstützend");
|
||||
|
||||
impl ClubMember for UnterstuetzendUser {}
|
||||
|
||||
impl UnterstuetzendUser {
|
||||
pub(crate) async fn send_welcome_mail_to_user(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let Some(mail) = &self.mail else {
|
||||
return Err(format!(
|
||||
"Couldn't send welcome mail, as the user {self} has no mail..."
|
||||
));
|
||||
};
|
||||
|
||||
Mail::send_single(
|
||||
db,
|
||||
mail,
|
||||
"Willkommen im ASKÖ Ruderverein Donau Linz!",
|
||||
format!(
|
||||
"Hallo {0},
|
||||
|
||||
herzlich willkommen im ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dich als neues Mitglied in unserem Verein begrüßen zu dürfen.
|
||||
|
||||
Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at (für allgemeine Fragen) und it@rudernlinz.at (bei technischen Fragen) jederzeit zur Verfügung.
|
||||
|
||||
Damit du dich noch mehr verbunden fühlst (:-)), haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz' eingerichtet. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
|
||||
|
||||
Riemen- & Dollenbruch
|
||||
ASKÖ Ruderverein Donau Linz", self.name),
|
||||
smtp_pw,
|
||||
).await?;
|
||||
|
||||
ActivityBuilder::new(&format!(
|
||||
"{self} hat eine Mail an {mail} bekommen, mit Infos dass er/sie nun ein unterstützendes Mitglied ist (Handbuch, WLAN)."
|
||||
))
|
||||
.user(self)
|
||||
.save(db)
|
||||
.await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn create(
|
||||
db: &SqlitePool,
|
||||
created_by: &ManageUserUser,
|
||||
smtp_pw: &str,
|
||||
name: NonEmptyString,
|
||||
mail: &str,
|
||||
financial: Option<Role>,
|
||||
birthdate: &NaiveDate,
|
||||
member_since: &NaiveDate,
|
||||
phone: NonEmptyString,
|
||||
address: NonEmptyString,
|
||||
membership_pdf: &TempFile<'_>,
|
||||
) -> Result<(), String> {
|
||||
let role = Role::find_by_name(db, "Unterstützend").await.unwrap();
|
||||
let user = Self::create_member(
|
||||
db,
|
||||
created_by,
|
||||
&role,
|
||||
name,
|
||||
mail,
|
||||
financial,
|
||||
birthdate,
|
||||
member_since,
|
||||
phone,
|
||||
address,
|
||||
membership_pdf,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let user = Self::new(db, &user).await.unwrap();
|
||||
user.send_welcome_mail_to_user(db, smtp_pw).await?;
|
||||
|
||||
if let Some(vorstand) = Role::find_by_name(db, "Vorstand").await {
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&format!("Lieber Vorstand, es gibt ein neues unterstützendes Mitglied: {user}"),
|
||||
"Neues unterstützendes Vereinsmitglied",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -2,14 +2,12 @@ use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::{
|
||||
trip::{Trip, TripWithDetails},
|
||||
tripdetails::TripDetails,
|
||||
};
|
||||
use crate::model::{
|
||||
notification::Notification,
|
||||
planned::tripdetails::{Action, CoxAtTrip::Yes},
|
||||
user::{SteeringUser, User},
|
||||
trip::{Trip, TripWithUserAndType},
|
||||
tripdetails::TripDetails,
|
||||
user::{CoxUser, User},
|
||||
};
|
||||
use crate::model::tripdetails::{Action, CoxAtTrip::Yes};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UserTrip {
|
||||
@ -160,7 +158,7 @@ impl UserTrip {
|
||||
.unwrap()
|
||||
.cancelled()
|
||||
{
|
||||
let trip = TripWithDetails::from(db, trip.clone()).await;
|
||||
let trip = TripWithUserAndType::from(db, trip.clone()).await;
|
||||
if trip.rower.len() == 1 {
|
||||
trip_to_delete = Some(trip.trip);
|
||||
}
|
||||
@ -199,7 +197,7 @@ impl UserTrip {
|
||||
let mut add_info = "";
|
||||
if let Some(trip) = &trip_to_delete {
|
||||
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
trip.delete(db, &SteeringUser::new(db, &cox).await.unwrap())
|
||||
trip.delete(db, &CoxUser::new(db, cox).await.unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
add_info = " Das war die letzte angemeldete Person. Nachdem nun alle Bescheid wissen, wird die Ausfahrt ab sofort nicht mehr angezeigt.";
|
||||
@ -272,10 +270,8 @@ pub enum UserTripDeleteError {
|
||||
mod test {
|
||||
use crate::{
|
||||
model::{
|
||||
planned::{
|
||||
event::Event, trip::Trip, tripdetails::TripDetails, usertrip::UserTripError,
|
||||
},
|
||||
user::SteeringUser,
|
||||
event::Event, trip::Trip, tripdetails::TripDetails, user::CoxUser,
|
||||
usertrip::UserTripError,
|
||||
},
|
||||
testdb,
|
||||
};
|
||||
@ -357,9 +353,9 @@ mod test {
|
||||
fn test_fail_create_is_cox_planned_event() {
|
||||
let pool = testdb!();
|
||||
|
||||
let cox = SteeringUser::new(
|
||||
let cox = CoxUser::new(
|
||||
&pool,
|
||||
&User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
User::find_by_name(&pool, "cox".into()).await.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
@ -1,4 +1,4 @@
|
||||
use rocket::{Build, FromForm, Rocket, State, form::Form, post, routes};
|
||||
use rocket::{form::Form, post, routes, Build, FromForm, Rocket, State};
|
||||
use serde_json::json;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
|
@ -80,8 +80,8 @@ fn fetch() -> Result<Station, String> {
|
||||
let url = "https://hydro.ooe.gv.at/daten/internet/stations/OG/207068/S/forecast.json";
|
||||
|
||||
match ureq::get(url).call() {
|
||||
Ok(mut response) => {
|
||||
let forecast: Result<Vec<Station>, _> = response.body_mut().read_json();
|
||||
Ok(response) => {
|
||||
let forecast: Result<Vec<Station>, _> = response.into_json();
|
||||
|
||||
if let Ok(data) = forecast {
|
||||
if data.len() == 1 {
|
||||
|
@ -96,13 +96,11 @@ struct DailyWeather {
|
||||
}
|
||||
|
||||
fn fetch(api_key: &str) -> Result<Data, String> {
|
||||
let url = format!(
|
||||
"https://api.openweathermap.org/data/3.0/onecall?lat=48.31970&lon=14.29451&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}"
|
||||
);
|
||||
let url = format!("https://api.openweathermap.org/data/3.0/onecall?lat=48.31970&lon=14.29451&units=metric&exclude=current,minutely,hourly,alert&appid={api_key}");
|
||||
|
||||
match ureq::get(&url).call() {
|
||||
Ok(mut response) => {
|
||||
let data: Result<Data, _> = response.body_mut().read_json();
|
||||
Ok(response) => {
|
||||
let data: Result<Data, _> = response.into_json();
|
||||
|
||||
if let Ok(data) = data {
|
||||
Ok(data)
|
||||
|
@ -2,23 +2,22 @@ use crate::model::{
|
||||
boat::{Boat, BoatToAdd, BoatToUpdate},
|
||||
location::Location,
|
||||
log::Log,
|
||||
user::{User, UserWithDetails, VorstandUser},
|
||||
user::{AdminUser, User, UserWithDetails},
|
||||
};
|
||||
use rocket::{
|
||||
Route, State,
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
routes, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::{Template, tera::Context};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[get("/boat")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
admin: VorstandUser,
|
||||
admin: AdminUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let boats = Boat::all(db).await;
|
||||
@ -41,7 +40,7 @@ async fn index(
|
||||
}
|
||||
|
||||
#[get("/boat/<boat>/delete")]
|
||||
async fn delete(db: &State<SqlitePool>, admin: VorstandUser, boat: i32) -> Flash<Redirect> {
|
||||
async fn delete(db: &State<SqlitePool>, admin: AdminUser, boat: i32) -> Flash<Redirect> {
|
||||
let boat = Boat::find_by_id(db, boat).await;
|
||||
Log::create(db, format!("{} deleted boat: {boat:?}", admin.user.name)).await;
|
||||
|
||||
@ -62,7 +61,7 @@ async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<BoatToUpdate<'_>>,
|
||||
boat_id: i32,
|
||||
_admin: VorstandUser,
|
||||
_admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
let boat = Boat::find_by_id(db, boat_id).await;
|
||||
let Some(boat) = boat else {
|
||||
@ -79,7 +78,7 @@ async fn update(
|
||||
async fn create(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<BoatToAdd<'_>>,
|
||||
_admin: VorstandUser,
|
||||
_admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
match Boat::create(db, data.into_inner()).await {
|
||||
Ok(_) => Flash::success(Redirect::to("/admin/boat"), "Boot hinzugefügt"),
|
||||
@ -246,11 +245,9 @@ mod test {
|
||||
|
||||
let rocket = rocket::build().manage(db.clone());
|
||||
let rocket = crate::tera::config(rocket);
|
||||
assert!(
|
||||
Boat::find_by_name(&db, "completely-new-boat".into())
|
||||
.await
|
||||
.is_none()
|
||||
);
|
||||
assert!(Boat::find_by_name(&db, "completely-new-boat".into())
|
||||
.await
|
||||
.is_none());
|
||||
|
||||
let client = Client::tracked(rocket).await.unwrap();
|
||||
let login = client
|
||||
|
@ -1,18 +1,15 @@
|
||||
use rocket::{
|
||||
FromForm, Route, State,
|
||||
form::Form,
|
||||
get, post, put,
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{
|
||||
planned::{
|
||||
event::{self, Event},
|
||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||
},
|
||||
event::{self, Event},
|
||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||
user::EventUser,
|
||||
};
|
||||
|
||||
@ -29,18 +26,17 @@ struct AddEventForm<'r> {
|
||||
async fn create(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<AddEventForm<'_>>,
|
||||
user: EventUser,
|
||||
_admin: EventUser,
|
||||
) -> Flash<Redirect> {
|
||||
let data = data.into_inner();
|
||||
|
||||
let trip_details_id = TripDetails::create(db, data.tripdetails).await;
|
||||
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc. we
|
||||
//just created
|
||||
//the object
|
||||
//just created
|
||||
//the object
|
||||
|
||||
Event::create(
|
||||
db,
|
||||
&user,
|
||||
data.name,
|
||||
data.planned_amount_cox,
|
||||
data.always_show,
|
||||
@ -68,7 +64,7 @@ struct UpdateEventForm<'r> {
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<UpdateEventForm<'_>>,
|
||||
user: EventUser,
|
||||
_admin: EventUser,
|
||||
) -> Flash<Redirect> {
|
||||
let update = event::EventUpdate {
|
||||
name: data.name,
|
||||
@ -81,7 +77,7 @@ async fn update(
|
||||
};
|
||||
match Event::find_by_id(db, data.id).await {
|
||||
Some(planned_event) => {
|
||||
planned_event.update(db, &user, &update).await;
|
||||
planned_event.update(db, &update).await;
|
||||
Flash::success(Redirect::to("/planned"), "Event erfolgreich bearbeitet")
|
||||
}
|
||||
None => Flash::error(Redirect::to("/planned"), "Planned event id not found"),
|
||||
|
@ -1,16 +1,16 @@
|
||||
use rocket::form::Form;
|
||||
use rocket::fs::TempFile;
|
||||
use rocket::response::{Flash, Redirect};
|
||||
use rocket::{FromForm, post};
|
||||
use rocket::{Route, State, get, request::FlashMessage, routes};
|
||||
use rocket_dyn_templates::{Template, tera::Context};
|
||||
use rocket::{get, request::FlashMessage, routes, Route, State};
|
||||
use rocket::{post, FromForm};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::log::Log;
|
||||
use crate::model::mail::Mail;
|
||||
use crate::model::role::Role;
|
||||
use crate::model::user::VorstandUser;
|
||||
use crate::model::user::{AllowedToSendFeeReminderUser, UserWithDetails};
|
||||
use crate::model::user::UserWithDetails;
|
||||
use crate::model::user::{AdminUser, VorstandUser};
|
||||
use crate::tera::Config;
|
||||
|
||||
#[get("/mail")]
|
||||
@ -35,51 +35,21 @@ async fn index(
|
||||
}
|
||||
|
||||
#[get("/mail/fee")]
|
||||
async fn fee(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AllowedToSendFeeReminderUser,
|
||||
config: &State<Config>,
|
||||
) -> Flash<Redirect> {
|
||||
async fn fee(db: &State<SqlitePool>, admin: AdminUser, config: &State<Config>) -> &'static str {
|
||||
Log::create(db, format!("{admin:?} trying to send fee")).await;
|
||||
Mail::fees(db, config.smtp_pw.clone(), None).await;
|
||||
Log::create(db, "Mail successfully sent".into()).await;
|
||||
Flash::success(Redirect::to("/admin/mail"), "Mail versendet")
|
||||
}
|
||||
|
||||
#[get("/mail/fee/test")]
|
||||
async fn fee_test(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AllowedToSendFeeReminderUser,
|
||||
config: &State<Config>,
|
||||
) -> Flash<Redirect> {
|
||||
Log::create(db, format!("{admin:?} trying to send test fee")).await;
|
||||
Mail::fees(db, config.smtp_pw.clone(), Some(admin.user)).await;
|
||||
Log::create(db, "Mail successfully sent".into()).await;
|
||||
Flash::success(Redirect::to("/admin/mail"), "Mail versendet")
|
||||
Mail::fees(db, config.smtp_pw.clone()).await;
|
||||
"SUCC"
|
||||
}
|
||||
|
||||
#[get("/mail/fee-final")]
|
||||
async fn fee_final(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AllowedToSendFeeReminderUser,
|
||||
admin: AdminUser,
|
||||
config: &State<Config>,
|
||||
) -> Flash<Redirect> {
|
||||
) -> &'static str {
|
||||
Log::create(db, format!("{admin:?} trying to send fee_final")).await;
|
||||
Mail::fees_final(db, config.smtp_pw.clone(), None).await;
|
||||
Log::create(db, "Mail successfully sent".into()).await;
|
||||
Flash::success(Redirect::to("/admin/mail"), "Mail versendet")
|
||||
}
|
||||
|
||||
#[get("/mail/fee-final/test")]
|
||||
async fn fee_final_test(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AllowedToSendFeeReminderUser,
|
||||
config: &State<Config>,
|
||||
) -> Flash<Redirect> {
|
||||
Log::create(db, format!("{admin:?} trying to send test fee_final")).await;
|
||||
Mail::fees_final(db, config.smtp_pw.clone(), Some(admin.user)).await;
|
||||
Log::create(db, "Mail successfully sent".into()).await;
|
||||
Flash::success(Redirect::to("/admin/mail"), "Mail versendet")
|
||||
Mail::fees_final(db, config.smtp_pw.clone()).await;
|
||||
"SUCC"
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
@ -109,7 +79,7 @@ async fn update(
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index, update, fee, fee_test, fee_final, fee_final_test]
|
||||
routes![index, update, fee, fee_final]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -1,21 +1,32 @@
|
||||
use csv::ReaderBuilder;
|
||||
use rocket::{FromForm, Route, State, form::Form, get, post, routes};
|
||||
use rocket_dyn_templates::{Template, context};
|
||||
use rocket::{form::Form, get, post, routes, FromForm, Route, State};
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{activity::Activity, role::Role, user::AdminUser};
|
||||
use crate::{
|
||||
model::{log::Log, role::Role, user::AdminUser},
|
||||
tera::Config,
|
||||
};
|
||||
|
||||
pub mod boat;
|
||||
pub mod event;
|
||||
pub mod mail;
|
||||
pub mod notification;
|
||||
pub mod role;
|
||||
pub mod schnupper;
|
||||
pub mod user;
|
||||
|
||||
#[get("/rss?<key>")]
|
||||
async fn rss(db: &State<SqlitePool>, key: &str, config: &State<Config>) -> String {
|
||||
if key.eq(&config.rss_key) {
|
||||
Log::generate_feed(db).await
|
||||
} else {
|
||||
"Not allowed".into()
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/rss", rank = 2)]
|
||||
async fn show_activities(db: &State<SqlitePool>, _admin: AdminUser) -> String {
|
||||
Activity::show(db).await
|
||||
async fn show_rss(db: &State<SqlitePool>, _admin: AdminUser) -> String {
|
||||
Log::show(db).await
|
||||
}
|
||||
|
||||
#[get("/list")]
|
||||
@ -70,7 +81,6 @@ pub fn routes() -> Vec<Route> {
|
||||
ret.append(&mut notification::routes());
|
||||
ret.append(&mut mail::routes());
|
||||
ret.append(&mut event::routes());
|
||||
ret.append(&mut role::routes());
|
||||
ret.append(&mut routes![show_activities, show_list, list]);
|
||||
ret.append(&mut routes![rss, show_rss, show_list, list]);
|
||||
ret
|
||||
}
|
||||
|
@ -6,14 +6,13 @@ use crate::model::{
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use rocket::{
|
||||
FromForm, Route, State,
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::{Template, tera::Context};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[get("/notification")]
|
||||
|
@ -1,65 +0,0 @@
|
||||
use crate::model::{
|
||||
role::Role,
|
||||
user::{AdminUser, UserWithDetails, VorstandUser},
|
||||
};
|
||||
use rocket::{
|
||||
FromForm, Route, State,
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
};
|
||||
use rocket_dyn_templates::{Template, tera::Context};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[get("/role")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
admin: VorstandUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let roles = Role::all(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("roles", &roles);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(admin.user, db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/role", context.into_json())
|
||||
}
|
||||
#[derive(FromForm)]
|
||||
pub struct RoleToUpdate<'r> {
|
||||
pub formatted_name: &'r str,
|
||||
pub desc: &'r str,
|
||||
}
|
||||
|
||||
#[post("/role/<role_id>", data = "<data>")]
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<RoleToUpdate<'_>>,
|
||||
role_id: i32,
|
||||
admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
let role = Role::find_by_id(db, role_id).await;
|
||||
let Some(role) = role else {
|
||||
return Flash::error(Redirect::to("/admin/role"), "Role does not exist!");
|
||||
};
|
||||
|
||||
match role
|
||||
.update(db, &admin, &data.formatted_name, &data.desc)
|
||||
.await
|
||||
{
|
||||
Ok(_) => Flash::success(Redirect::to("/admin/role"), "Rolle bearbeitet"),
|
||||
Err(e) => Flash::error(Redirect::to("/admin/role"), e),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index, update]
|
||||
}
|
@ -3,8 +3,8 @@ use crate::model::{
|
||||
user::{SchnupperBetreuerUser, User, UserWithDetails},
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use rocket::{Route, State, get, request::FlashMessage, routes};
|
||||
use rocket_dyn_templates::{Template, tera::Context};
|
||||
use rocket::{get, request::FlashMessage, routes, Route, State};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[get("/schnupper")]
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,5 +1,4 @@
|
||||
use rocket::{
|
||||
FromForm, Request, Route, State,
|
||||
form::Form,
|
||||
get,
|
||||
http::{Cookie, CookieJar},
|
||||
@ -9,12 +8,12 @@ use rocket::{
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
time::{Duration, OffsetDateTime},
|
||||
FromForm, Request, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::{Template, context, tera};
|
||||
use rocket_dyn_templates::{context, tera, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{
|
||||
activity::{ActivityBuilder, ReasonAuth},
|
||||
log::Log,
|
||||
user::{LoginError, User},
|
||||
};
|
||||
@ -74,18 +73,20 @@ async fn login(
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
return Flash::error(
|
||||
Redirect::to("/auth"),
|
||||
"Falscher Benutzername/Passwort. Du bist Vereinsmitglied und der Login klappt nicht? Kontaktiere unseren Schriftführer oder schreibe eine Mail an info@rudernlinz.at!",
|
||||
);
|
||||
return Flash::error(Redirect::to("/auth"), "Falscher Benutzername/Passwort. Du bist Vereinsmitglied und der Login klappt nicht? Kontaktiere Philipp H. (Tel.nr. siehe Signalgruppe) oder schreibe eine Mail an it@rudernlinz.at!");
|
||||
}
|
||||
};
|
||||
|
||||
cookies.add_private(Cookie::new("loggedin_user", format!("{}", user.id)));
|
||||
|
||||
ActivityBuilder::from(ReasonAuth::SuccLogin(&user, agent.0))
|
||||
.save(db)
|
||||
.await;
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"Succ login of {} with this useragent: {}",
|
||||
login.name, agent.0
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Check for redirect_url cookie and redirect accordingly
|
||||
match cookies.get_private("redirect_url") {
|
||||
|
@ -3,8 +3,8 @@ use crate::model::{
|
||||
role::Role,
|
||||
user::{User, UserWithDetails, VorstandUser},
|
||||
};
|
||||
use rocket::{Route, State, get, request::FlashMessage, routes};
|
||||
use rocket_dyn_templates::{Template, tera::Context};
|
||||
use rocket::{get, request::FlashMessage, routes, Route, State};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[get("/achievement")]
|
||||
@ -18,12 +18,12 @@ async fn index(
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
let role = Role::find_by_name(db, "Donau Linz").await.unwrap();
|
||||
let users = User::all_with_role(db, &role).await;
|
||||
let role = Role::find_by_name(&db, "Donau Linz").await.unwrap();
|
||||
let users = User::all_with_role(&db, &role).await;
|
||||
let mut people = Vec::new();
|
||||
let mut rowingbadge_year = None;
|
||||
for user in users {
|
||||
let achievement = Achievements::for_user(db, &user).await;
|
||||
let achievement = Achievements::for_user(&db, &user).await;
|
||||
if let Some(badge) = &achievement.rowingbadge {
|
||||
rowingbadge_year = Some(badge.year);
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::model::{
|
||||
boat::Boat,
|
||||
boathouse::Boathouse,
|
||||
user::{AllowedToUpdateBoathouse, UserWithDetails, VorstandUser},
|
||||
user::{AdminUser, UserWithDetails, VorstandUser},
|
||||
};
|
||||
use rocket::{
|
||||
form::Form,
|
||||
@ -37,11 +37,6 @@ async fn index(
|
||||
let boathouse = Boathouse::get(db).await;
|
||||
context.insert("boathouse", &boathouse);
|
||||
|
||||
let allowed_to_edit = AllowedToUpdateBoathouse::new(db, &admin.user)
|
||||
.await
|
||||
.is_some();
|
||||
context.insert("allowed_to_edit", &allowed_to_edit);
|
||||
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(admin.into_inner(), db).await,
|
||||
@ -61,29 +56,36 @@ pub struct FormBoathouseToAdd {
|
||||
async fn new<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormBoathouseToAdd>,
|
||||
user: AllowedToUpdateBoathouse,
|
||||
_admin: AdminUser,
|
||||
) -> Flash<Redirect> {
|
||||
match Boathouse::create(db, &user, data.into_inner()).await {
|
||||
match Boathouse::create(db, data.into_inner()).await {
|
||||
Ok(_) => Flash::success(Redirect::to("/board/boathouse"), "Boot hinzugefügt"),
|
||||
Err(e) => Flash::error(Redirect::to("/board/boathouse"), e),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/boathouse/<boathouse_id>/delete")]
|
||||
async fn delete(
|
||||
db: &State<SqlitePool>,
|
||||
user: AllowedToUpdateBoathouse,
|
||||
boathouse_id: i32,
|
||||
) -> Flash<Redirect> {
|
||||
async fn delete(db: &State<SqlitePool>, _admin: AdminUser, boathouse_id: i32) -> Flash<Redirect> {
|
||||
let boat = Boathouse::find_by_id(db, boathouse_id).await;
|
||||
match boat {
|
||||
Some(boat) => {
|
||||
boat.delete(db, &user).await;
|
||||
boat.delete(db).await;
|
||||
Flash::success(Redirect::to("/board/boathouse"), "Bootsplatz gelöscht")
|
||||
}
|
||||
None => Flash::error(Redirect::to("/board/boathouse"), "Boatplace does not exist"),
|
||||
}
|
||||
}
|
||||
//#[post("/boat/new", data = "<data>")]
|
||||
//async fn create(
|
||||
// db: &State<SqlitePool>,
|
||||
// data: Form<BoatToAdd<'_>>,
|
||||
// _admin: AdminUser,
|
||||
//) -> Flash<Redirect> {
|
||||
// match Boat::create(db, data.into_inner()).await {
|
||||
// Ok(_) => Flash::success(Redirect::to("/admin/boat"), "Boot hinzugefügt"),
|
||||
// Err(e) => Flash::error(Redirect::to("/admin/boat"), e),
|
||||
// }
|
||||
//}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index, new, delete]
|
||||
|
@ -1,10 +1,9 @@
|
||||
use rocket::{
|
||||
FromForm, Route, State,
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use sqlx::SqlitePool;
|
||||
@ -14,7 +13,7 @@ use crate::{
|
||||
model::{
|
||||
boat::Boat,
|
||||
boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified},
|
||||
user::{DonauLinzUser, SteeringUser, TechUser, User, UserWithDetails},
|
||||
user::{CoxUser, DonauLinzUser, TechUser, User, UserWithDetails},
|
||||
},
|
||||
tera::log::KioskCookie,
|
||||
};
|
||||
@ -134,7 +133,7 @@ async fn fixed<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormBoatDamageFixed<'r>>,
|
||||
boatdamage_id: i32,
|
||||
coxuser: SteeringUser,
|
||||
coxuser: CoxUser,
|
||||
) -> Flash<Redirect> {
|
||||
let boatdamage = BoatDamage::find_by_id(db, boatdamage_id).await.unwrap(); //TODO: Fix
|
||||
let boatdamage_fixed = BoatDamageFixed {
|
||||
@ -149,13 +148,13 @@ async fn fixed<'r>(
|
||||
|
||||
#[derive(FromForm)]
|
||||
pub struct FormBoatDamageVerified<'r> {
|
||||
desc: &'r str,
|
||||
pub desc: &'r str,
|
||||
}
|
||||
|
||||
#[post("/<boatdamage_id>/verified", data = "<data>")]
|
||||
async fn verified<'r>(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<FormBoatDamageVerified<'r>>,
|
||||
data: Form<FormBoatDamageFixed<'r>>,
|
||||
boatdamage_id: i32,
|
||||
techuser: TechUser,
|
||||
) -> Flash<Redirect> {
|
||||
|
@ -1,11 +1,10 @@
|
||||
use chrono::NaiveDate;
|
||||
use rocket::{
|
||||
FromForm, Route, State,
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use sqlx::SqlitePool;
|
||||
|
@ -1,48 +1,39 @@
|
||||
use rocket::{
|
||||
FromForm, Route, State,
|
||||
form::Form,
|
||||
get, post,
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{
|
||||
event::Event,
|
||||
log::Log,
|
||||
planned::{
|
||||
event::Event,
|
||||
trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
|
||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||
},
|
||||
user::{AllowedToUpdateTripToAlwaysBeShownUser, ErgoUser, SteeringUser, User},
|
||||
trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
|
||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||
user::{AllowedToUpdateTripToAlwaysBeShownUser, CoxUser},
|
||||
};
|
||||
|
||||
#[post("/trip", data = "<data>", rank = 2)]
|
||||
async fn create_ergo(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<TripDetailsToAdd<'_>>,
|
||||
cox: ErgoUser,
|
||||
) -> Flash<Redirect> {
|
||||
let trip_details_id = TripDetails::create(db, data.into_inner()).await;
|
||||
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc just
|
||||
//created
|
||||
Trip::new_own_ergo(db, &cox, trip_details).await; //TODO: fix
|
||||
|
||||
Flash::success(Redirect::to("/planned"), "Ausfahrt erfolgreich erstellt.")
|
||||
}
|
||||
|
||||
/// SteeringUser created new trip
|
||||
#[post("/trip", data = "<data>")]
|
||||
async fn create(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<TripDetailsToAdd<'_>>,
|
||||
cox: SteeringUser,
|
||||
cox: CoxUser,
|
||||
) -> Flash<Redirect> {
|
||||
let trip_details_id = TripDetails::create(db, data.into_inner()).await;
|
||||
let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc just
|
||||
//created
|
||||
//created
|
||||
Trip::new_own(db, &cox, trip_details).await; //TODO: fix
|
||||
|
||||
//Log::create(
|
||||
// db,
|
||||
// format!(
|
||||
// "Cox {} created trip on {} @ {} for {} rower",
|
||||
// cox.name, trip_details.day, trip_details.planned_starting_time, trip_details.max_people,
|
||||
// ),
|
||||
//)
|
||||
//.await;
|
||||
|
||||
Flash::success(Redirect::to("/planned"), "Ausfahrt erfolgreich erstellt.")
|
||||
}
|
||||
|
||||
@ -59,7 +50,7 @@ async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<EditTripForm<'_>>,
|
||||
trip_id: i64,
|
||||
cox: User,
|
||||
cox: CoxUser,
|
||||
) -> Flash<Redirect> {
|
||||
if let Some(trip) = Trip::find_by_id(db, trip_id).await {
|
||||
let update = trip::TripUpdate {
|
||||
@ -78,10 +69,6 @@ async fn update(
|
||||
Err(TripUpdateError::NotYourTrip) => {
|
||||
Flash::error(Redirect::to("/planned"), "Nicht deine Ausfahrt!")
|
||||
}
|
||||
Err(TripUpdateError::TripTypeNotAllowed) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Du darfst nur Ergo-Events erstellen",
|
||||
),
|
||||
Err(TripUpdateError::TripDetailsDoesNotExist) => {
|
||||
Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht")
|
||||
}
|
||||
@ -109,7 +96,7 @@ async fn toggle_always_show(
|
||||
}
|
||||
|
||||
#[get("/join/<planned_event_id>")]
|
||||
async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: SteeringUser) -> Flash<Redirect> {
|
||||
async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
|
||||
if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await {
|
||||
match Trip::new_join(db, &cox, &planned_event).await {
|
||||
Ok(_) => {
|
||||
@ -123,10 +110,9 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: SteeringUser)
|
||||
.await;
|
||||
Flash::success(Redirect::to("/planned"), "Danke für's helfen!")
|
||||
}
|
||||
Err(CoxHelpError::CanceledEvent) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Die Ausfahrt wurde leider abgesagt...",
|
||||
),
|
||||
Err(CoxHelpError::CanceledEvent) => {
|
||||
Flash::error(Redirect::to("/planned"), "Die Ausfahrt wurde leider abgesagt...")
|
||||
}
|
||||
Err(CoxHelpError::AlreadyRegisteredAsCox) => {
|
||||
Flash::error(Redirect::to("/planned"), "Du hilfst bereits aus!")
|
||||
}
|
||||
@ -134,10 +120,9 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: SteeringUser)
|
||||
Redirect::to("/planned"),
|
||||
"Du hast dich bereits als Ruderer angemeldet!",
|
||||
),
|
||||
Err(CoxHelpError::DetailsLocked) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Die Bootseinteilung wurde bereits gemacht. Wenn du noch steuern möchtest, frag bitte bei einer bereits angemeldeten Steuerperson nach, ob das noch möglich ist.",
|
||||
),
|
||||
Err(CoxHelpError::DetailsLocked) => {
|
||||
Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du noch steuern möchtest, frag bitte bei einer bereits angemeldeten Steuerperson nach, ob das noch möglich ist.")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Flash::error(Redirect::to("/planned"), "Event gibt's nicht")
|
||||
@ -145,7 +130,7 @@ async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: SteeringUser)
|
||||
}
|
||||
|
||||
#[get("/remove/trip/<trip_id>")]
|
||||
async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: User) -> Flash<Redirect> {
|
||||
async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: CoxUser) -> Flash<Redirect> {
|
||||
let trip = Trip::find_by_id(db, trip_id).await;
|
||||
match trip {
|
||||
None => Flash::error(Redirect::to("/planned"), "Trip gibt's nicht!"),
|
||||
@ -166,11 +151,7 @@ async fn remove_trip(db: &State<SqlitePool>, trip_id: i64, cox: User) -> Flash<R
|
||||
}
|
||||
|
||||
#[get("/remove/<planned_event_id>")]
|
||||
async fn remove(
|
||||
db: &State<SqlitePool>,
|
||||
planned_event_id: i64,
|
||||
cox: SteeringUser,
|
||||
) -> Flash<Redirect> {
|
||||
async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
|
||||
if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await {
|
||||
match Trip::delete_by_planned_event(db, &cox, &planned_event).await {
|
||||
Ok(_) => {
|
||||
@ -185,10 +166,9 @@ async fn remove(
|
||||
|
||||
Flash::success(Redirect::to("/planned"), "Erfolgreich abgemeldet!")
|
||||
}
|
||||
Err(TripHelpDeleteError::DetailsLocked) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht steuern kannst, melde dich bitte unbedingt schnellstmöglich bei einer anderen Steuerperson!",
|
||||
),
|
||||
Err(TripHelpDeleteError::DetailsLocked) => {
|
||||
Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht steuern kannst, melde dich bitte unbedingt schnellstmöglich bei einer anderen Steuerperson!")
|
||||
}
|
||||
Err(TripHelpDeleteError::CoxNotHelping) => {
|
||||
Flash::error(Redirect::to("/planned"), "Steuermann hilft nicht aus...")
|
||||
}
|
||||
@ -201,7 +181,6 @@ async fn remove(
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
create,
|
||||
create_ergo,
|
||||
join,
|
||||
remove,
|
||||
remove_trip,
|
||||
@ -219,7 +198,7 @@ mod test {
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::{model::planned::trip::Trip, testdb};
|
||||
use crate::{model::trip::Trip, testdb};
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_trip_create() {
|
||||
|
168
src/tera/ergo.rs
168
src/tera/ergo.rs
@ -2,7 +2,6 @@ use std::env;
|
||||
|
||||
use chrono::Utc;
|
||||
use rocket::{
|
||||
FromForm, Route, State,
|
||||
form::Form,
|
||||
fs::TempFile,
|
||||
get,
|
||||
@ -10,17 +9,15 @@ use rocket::{
|
||||
post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::{Template, context};
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::model::{
|
||||
log::Log,
|
||||
notification::Notification,
|
||||
role::Role,
|
||||
user::{AdminUser, User, UserWithDetails},
|
||||
};
|
||||
|
||||
@ -53,7 +50,7 @@ async fn send(db: &State<SqlitePool>, _user: AdminUser) -> Template {
|
||||
.unwrap();
|
||||
|
||||
Template::render(
|
||||
"ergo/final",
|
||||
"ergo.final",
|
||||
context!(loggedin_user: &UserWithDetails::from_user(_user.user, db).await, thirty, dozen),
|
||||
)
|
||||
}
|
||||
@ -101,19 +98,6 @@ async fn update(
|
||||
|
||||
#[get("/")]
|
||||
async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.clone(), db).await,
|
||||
);
|
||||
|
||||
if !user.has_role(db, "ergo").await {
|
||||
return Template::render("ergo/missing-data", context.into_json());
|
||||
}
|
||||
|
||||
let users = User::ergo(db).await;
|
||||
|
||||
let thirty = sqlx::query_as!(
|
||||
@ -132,62 +116,18 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
context.insert("users", &users);
|
||||
context.insert("thirty", &thirty);
|
||||
context.insert("dozen", &dozen);
|
||||
|
||||
Template::render("ergo/index", context.into_json())
|
||||
Template::render("ergo", context.into_json())
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct UserAdd {
|
||||
birthyear: i32,
|
||||
weight: i64,
|
||||
sex: String,
|
||||
}
|
||||
|
||||
//#[post("/set-data", data = "<data>")]
|
||||
//async fn new_user(db: &State<SqlitePool>, data: Form<UserAdd>, user: User) -> Flash<Redirect> {
|
||||
// if user.has_role(db, "ergo").await {
|
||||
// return Flash::error(Redirect::to("/ergo"), "Du hast deine Daten schon eingegeben. Wenn du sie updaten willst, melde dich bitte bei it@rudernlinz.at");
|
||||
// }
|
||||
//
|
||||
// // check data
|
||||
// if data.birthyear < 1900 || data.birthyear > chrono::Utc::now().year() - 5 {
|
||||
// return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geburtsjahr...");
|
||||
// }
|
||||
// if data.weight < 20 || data.weight > 200 {
|
||||
// return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Gewicht...");
|
||||
// }
|
||||
// if &data.sex != "f" && &data.sex != "m" {
|
||||
// return Flash::error(Redirect::to("/ergo"), "Bitte überprüfe dein Geschlecht...");
|
||||
// }
|
||||
//
|
||||
// // set data
|
||||
// user.update_ergo(db, data.birthyear, data.weight, &data.sex)
|
||||
// .await;
|
||||
//
|
||||
// // inform all other `ergo` users
|
||||
// let ergo = Role::find_by_name(db, "ergo").await.unwrap();
|
||||
// Notification::create_for_role(
|
||||
// db,
|
||||
// &ergo,
|
||||
// &format!("{} nimmt heuer an der Ergochallenge teil 💪", user.name),
|
||||
// "Ergo Challenge",
|
||||
// None,
|
||||
// None,
|
||||
// )
|
||||
// .await;
|
||||
//
|
||||
// // add to `ergo` group
|
||||
// user.add_role(db, &ergo).await.unwrap();
|
||||
//
|
||||
// Flash::success(
|
||||
// Redirect::to("/ergo"),
|
||||
// "Du hast deine Daten erfolgreich eingegeben. Viel Spaß beim Schwitzen :-)",
|
||||
// )
|
||||
//}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
pub struct ErgoToAdd<'a> {
|
||||
user: i64,
|
||||
@ -218,11 +158,9 @@ async fn new_thirty(
|
||||
eprintln!("Failed to persist file: {:?}", e);
|
||||
}
|
||||
|
||||
let result = data.result.trim_start_matches(['0', ' ']);
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE user SET dirty_thirty = ? where id = ?",
|
||||
result,
|
||||
data.result,
|
||||
data.user
|
||||
)
|
||||
.execute(db.inner())
|
||||
@ -235,68 +173,9 @@ async fn new_thirty(
|
||||
)
|
||||
.await;
|
||||
|
||||
let ergo = Role::find_by_name(db, "ergo").await.unwrap();
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&ergo,
|
||||
&format!(
|
||||
"{} ist gerade die Dirty Thirty Challenge gefahren 🥵",
|
||||
user.name
|
||||
),
|
||||
"Ergo Challenge",
|
||||
Some("/ergo"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::success(Redirect::to("/ergo"), "Erfolgreich eingetragen")
|
||||
}
|
||||
|
||||
fn format_time(input: &str) -> String {
|
||||
let input = if input.starts_with(":") {
|
||||
&format!("00{input}")
|
||||
} else {
|
||||
input
|
||||
};
|
||||
let mut parts: Vec<&str> = input.split(':').collect();
|
||||
|
||||
// If there's only seconds (e.g., "24.2"), treat it as "00:00:24.2"
|
||||
if parts.len() == 1 {
|
||||
parts.insert(0, "0"); // Add "0" for hours
|
||||
parts.insert(0, "0"); // Add "0" for minutes
|
||||
}
|
||||
|
||||
// If there are two parts (e.g., "4:24.2"), treat it as "00:04:24.2"
|
||||
if parts.len() == 2 {
|
||||
parts.insert(0, "0"); // Add "0" for hours
|
||||
}
|
||||
|
||||
// Now parts should have [hours, minutes, seconds]
|
||||
let hours = if parts[0].len() == 1 {
|
||||
format!("0{}", parts[0])
|
||||
} else {
|
||||
parts[0].to_string()
|
||||
};
|
||||
let minutes = if parts[1].len() == 1 {
|
||||
format!("0{}", parts[1])
|
||||
} else {
|
||||
parts[1].to_string()
|
||||
};
|
||||
let seconds = parts[2];
|
||||
|
||||
// Split seconds into whole and fractional parts
|
||||
let (sec_int, sec_frac) = seconds.split_once('.').unwrap_or((seconds, "0"));
|
||||
|
||||
// Format the time as "hh:mm:ss.s"
|
||||
format!(
|
||||
"{}:{}:{}.{:1}",
|
||||
hours,
|
||||
minutes,
|
||||
sec_int,
|
||||
sec_frac.chars().next().unwrap_or('0')
|
||||
)
|
||||
}
|
||||
|
||||
#[post("/dozen", data = "<data>", format = "multipart/form-data")]
|
||||
async fn new_dozen(
|
||||
db: &State<SqlitePool>,
|
||||
@ -319,16 +198,10 @@ async fn new_dozen(
|
||||
if let Err(e) = data.proof.move_copy_to(file_path).await {
|
||||
eprintln!("Failed to persist file: {:?}", e);
|
||||
}
|
||||
let result = data.result.trim_start_matches(['0', ' ']);
|
||||
let result = if result.contains(":") || result.contains(".") {
|
||||
format_time(result)
|
||||
} else {
|
||||
result.to_string()
|
||||
};
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE user SET dirty_dozen = ? where id = ?",
|
||||
result,
|
||||
data.result,
|
||||
data.user
|
||||
)
|
||||
.execute(db.inner())
|
||||
@ -341,28 +214,11 @@ async fn new_dozen(
|
||||
)
|
||||
.await;
|
||||
|
||||
let ergo = Role::find_by_name(db, "ergo").await.unwrap();
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&ergo,
|
||||
&format!(
|
||||
"{} ist gerade die Dirty Dozen Challenge gefahren 🥵",
|
||||
user.name
|
||||
),
|
||||
"Ergo Challenge",
|
||||
Some("/ergo"),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::success(Redirect::to("/ergo"), "Erfolgreich eingetragen")
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
index, new_thirty, new_dozen, send, reset, update,
|
||||
// new_user
|
||||
]
|
||||
routes![index, new_thirty, new_dozen, send, reset, update]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
346
src/tera/log.rs
346
src/tera/log.rs
@ -22,12 +22,12 @@ use crate::{
|
||||
distance::Distance,
|
||||
log::Log,
|
||||
logbook::{
|
||||
LogToAdd, LogToFinalize, LogToUpdate, Logbook, LogbookCreateError, LogbookDeleteError,
|
||||
LogbookUpdateError,
|
||||
LogToAdd, LogToFinalize, LogToUpdate, Logbook, LogbookAdminUpdateError,
|
||||
LogbookCreateError, LogbookDeleteError, LogbookUpdateError,
|
||||
},
|
||||
logtype::LogType,
|
||||
planned::trip::Trip,
|
||||
user::{DonauLinzUser, User, UserWithDetails, VorstandUser},
|
||||
trip::Trip,
|
||||
user::{AdminUser, DonauLinzUser, User, UserWithDetails, VorstandUser},
|
||||
},
|
||||
tera::Config,
|
||||
};
|
||||
@ -47,44 +47,12 @@ impl<'r> FromRequest<'r> for KioskCookie {
|
||||
}
|
||||
|
||||
#[get("/", rank = 2)]
|
||||
async fn index_loggedin(
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
|
||||
let boats = Boat::for_user(db, &user).await;
|
||||
context.insert("boats", &boats);
|
||||
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into_inner(), db).await,
|
||||
);
|
||||
|
||||
index(db, flash, context).await
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn index_kiosk(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
_kiosk: KioskCookie,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
|
||||
let boats = Boat::all(db).await;
|
||||
context.insert("boats", &boats);
|
||||
|
||||
context.insert("show_kiosk_header", &true);
|
||||
|
||||
index(db, flash, context).await
|
||||
}
|
||||
|
||||
async fn index(db: &SqlitePool, flash: Option<FlashMessage<'_>>, mut context: Context) -> Template {
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
let mut coxes: Vec<UserWithDetails> = futures::future::join_all(
|
||||
User::cox(db)
|
||||
@ -93,7 +61,9 @@ async fn index(db: &SqlitePool, flash: Option<FlashMessage<'_>>, mut context: Co
|
||||
.map(|user| UserWithDetails::from_user(user, db)),
|
||||
)
|
||||
.await;
|
||||
coxes.retain(|u| u.roles.contains(&"Donau Linz".into()));
|
||||
coxes.retain(|u| {
|
||||
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
|
||||
});
|
||||
|
||||
let mut users: Vec<UserWithDetails> = futures::future::join_all(
|
||||
User::all(db)
|
||||
@ -102,13 +72,23 @@ async fn index(db: &SqlitePool, flash: Option<FlashMessage<'_>>, mut context: Co
|
||||
.map(|user| UserWithDetails::from_user(user, db)),
|
||||
)
|
||||
.await;
|
||||
users.retain(|u| u.allowed_to_row());
|
||||
users.retain(|u| {
|
||||
u.roles.contains(&"Donau Linz".into())
|
||||
|| u.roles.contains(&"scheckbuch".into())
|
||||
|| u.user.name == "Externe Steuerperson"
|
||||
});
|
||||
|
||||
let logtypes = LogType::all(db).await;
|
||||
let distances = Distance::all(db).await;
|
||||
|
||||
let on_water = Logbook::on_water(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
context.insert("boats", &boats);
|
||||
context.insert("planned_trips", &Trip::get_for_today(db).await);
|
||||
context.insert(
|
||||
"reservations",
|
||||
@ -117,57 +97,34 @@ async fn index(db: &SqlitePool, flash: Option<FlashMessage<'_>>, mut context: Co
|
||||
context.insert("coxes", &coxes);
|
||||
context.insert("users", &users);
|
||||
context.insert("logtypes", &logtypes);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into_inner(), db).await,
|
||||
);
|
||||
context.insert("on_water", &on_water);
|
||||
context.insert("distances", &distances);
|
||||
|
||||
Template::render("kiosk", context.into_json())
|
||||
Template::render("log", context.into_json())
|
||||
}
|
||||
|
||||
#[get("/show", rank = 3)]
|
||||
async fn show(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Template {
|
||||
async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
|
||||
let logs = Logbook::completed(db).await;
|
||||
let boats = Boat::all(db).await;
|
||||
let users = User::all(db).await;
|
||||
let logtypes = LogType::all(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("logs", &logs);
|
||||
context.insert("boats", &boats);
|
||||
context.insert("users", &users);
|
||||
context.insert("logtypes", &logtypes);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into_inner(), db).await,
|
||||
);
|
||||
Template::render("log.completed", context.into_json())
|
||||
Template::render(
|
||||
"log.completed",
|
||||
context!(logs, loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/show?<year>", rank = 2)]
|
||||
async fn show_for_year(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
user: VorstandUser,
|
||||
year: i32,
|
||||
) -> Template {
|
||||
async fn show_for_year(db: &State<SqlitePool>, user: AdminUser, year: i32) -> Template {
|
||||
let logs = Logbook::completed_in_year(db, year).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
context.insert("logs", &logs);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into_inner(), db).await,
|
||||
);
|
||||
Template::render("log.completed", context.into_json())
|
||||
Template::render(
|
||||
"log.completed",
|
||||
context!(logs, loggedin_user: &UserWithDetails::from_user(user.user, db).await),
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/show")]
|
||||
@ -195,83 +152,94 @@ async fn new_kiosk(
|
||||
Redirect::to("/log")
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn kiosk(
|
||||
db: &State<SqlitePool>,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
_kiosk: KioskCookie,
|
||||
) -> Template {
|
||||
let boats = Boat::all(db).await;
|
||||
let mut coxes: Vec<UserWithDetails> = futures::future::join_all(
|
||||
User::cox(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|user| UserWithDetails::from_user(user, db)),
|
||||
)
|
||||
.await;
|
||||
|
||||
coxes.retain(|u| {
|
||||
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
|
||||
});
|
||||
|
||||
let mut users: Vec<UserWithDetails> = futures::future::join_all(
|
||||
User::all(db)
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|user| UserWithDetails::from_user(user, db)),
|
||||
)
|
||||
.await;
|
||||
|
||||
users.retain(|u| {
|
||||
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
|
||||
});
|
||||
|
||||
let logtypes = LogType::all(db).await;
|
||||
let distances = Distance::all(db).await;
|
||||
|
||||
let on_water = Logbook::on_water(db).await;
|
||||
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
context.insert("planned_trips", &Trip::get_for_today(db).await);
|
||||
context.insert("boats", &boats);
|
||||
context.insert(
|
||||
"reservations",
|
||||
&BoatReservation::all_future_with_groups(db).await,
|
||||
);
|
||||
context.insert("coxes", &coxes);
|
||||
context.insert("users", &users);
|
||||
context.insert("logtypes", &logtypes);
|
||||
context.insert("on_water", &on_water);
|
||||
context.insert("distances", &distances);
|
||||
context.insert("show_kiosk_header", &true);
|
||||
|
||||
Template::render("kiosk", context.into_json())
|
||||
}
|
||||
|
||||
async fn create_logbook(
|
||||
db: &SqlitePool,
|
||||
data: Form<LogToAdd>,
|
||||
user: &DonauLinzUser,
|
||||
smtp_pw: &str,
|
||||
) -> Flash<Redirect> {
|
||||
match Logbook::create(db, data.into_inner(), user, smtp_pw).await {
|
||||
Ok(msg) => Flash::success(
|
||||
Redirect::to("/log"),
|
||||
format!("Ausfahrt erfolgreich hinzugefügt{msg}"),
|
||||
),
|
||||
Err(LogbookCreateError::BoatAlreadyOnWater) => {
|
||||
Flash::error(Redirect::to("/log"), "Boot schon am Wasser")
|
||||
}
|
||||
Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!("Ruderer {} schon am Wasser", rower.name),
|
||||
),
|
||||
Err(LogbookCreateError::BoatLocked) => Flash::error(Redirect::to("/log"), "Boot gesperrt"),
|
||||
Err(LogbookCreateError::BoatNotFound) => {
|
||||
Flash::error(Redirect::to("/log"), "Boot gibt's ned")
|
||||
}
|
||||
Err(LogbookCreateError::TooManyRowers(expected, actual)) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!(
|
||||
"Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)"
|
||||
),
|
||||
),
|
||||
Err(LogbookCreateError::RowerCreateError(rower, e)) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!("Fehler bei Ruderer {rower}: {e}"),
|
||||
),
|
||||
Err(LogbookCreateError::ArrivalNotAfterDeparture) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Ankunftszeit kann nicht vor der Abfahrtszeit sein",
|
||||
),
|
||||
Err(LogbookCreateError::UserNotAllowedToUseBoat) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Schiffsführer darf dieses Boot nicht verwenden",
|
||||
),
|
||||
Err(LogbookCreateError::SteeringPersonNotInRowers) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Steuerperson nicht in Liste der Ruderer!",
|
||||
),
|
||||
Err(LogbookCreateError::ShipmasterNotInRowers) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Schiffsführer nicht in Liste der Ruderer!",
|
||||
),
|
||||
Err(LogbookCreateError::NotYourEntry) => {
|
||||
Flash::error(Redirect::to("/log"), "Nicht deine Ausfahrt!")
|
||||
}
|
||||
Err(LogbookCreateError::ArrivalSetButNotRemainingTwo) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Ankunftszeit gesetzt aber nicht Distanz + Strecke",
|
||||
),
|
||||
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten an den Vorstand (info@rudernlinz.at).",
|
||||
),
|
||||
Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Handsteuer-Status dieses Boots kann nicht verändert werden.",
|
||||
),
|
||||
Err(LogbookCreateError::TooFast(km, min)) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!(
|
||||
"KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut."
|
||||
),
|
||||
),
|
||||
Err(LogbookCreateError::AlreadyFinalized) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Logbucheintrag wurde bereits abgeschlossen.",
|
||||
),
|
||||
Err(LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!",
|
||||
),
|
||||
match Logbook::create(
|
||||
db,
|
||||
data.into_inner(),
|
||||
user, smtp_pw
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(msg) => Flash::success(Redirect::to("/log"), format!("Ausfahrt erfolgreich hinzugefügt{msg}")),
|
||||
Err(LogbookCreateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Boot schon am Wasser"),
|
||||
Err(LogbookCreateError::RowerAlreadyOnWater(rower)) => Flash::error(Redirect::to("/log"), format!("Ruderer {} schon am Wasser", rower.name)),
|
||||
Err(LogbookCreateError::BoatLocked) => Flash::error(Redirect::to("/log"),"Boot gesperrt"),
|
||||
Err(LogbookCreateError::BoatNotFound) => Flash::error(Redirect::to("/log"), "Boot gibt's ned"),
|
||||
Err(LogbookCreateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
|
||||
Err(LogbookCreateError::RowerCreateError(rower, e)) => Flash::error(Redirect::to("/log"), format!("Fehler bei Ruderer {rower}: {e}")),
|
||||
Err(LogbookCreateError::ArrivalNotAfterDeparture) => Flash::error(Redirect::to("/log"), "Ankunftszeit kann nicht vor der Abfahrtszeit sein"),
|
||||
Err(LogbookCreateError::UserNotAllowedToUseBoat) => Flash::error(Redirect::to("/log"), "Schiffsführer darf dieses Boot nicht verwenden"),
|
||||
Err(LogbookCreateError::SteeringPersonNotInRowers) => Flash::error(Redirect::to("/log"), "Steuerperson nicht in Liste der Ruderer!"),
|
||||
Err(LogbookCreateError::ShipmasterNotInRowers) => Flash::error(Redirect::to("/log"), "Schiffsführer nicht in Liste der Ruderer!"),
|
||||
Err(LogbookCreateError::NotYourEntry) => Flash::error(Redirect::to("/log"), "Nicht deine Ausfahrt!"),
|
||||
Err(LogbookCreateError::ArrivalSetButNotRemainingTwo) => Flash::error(Redirect::to("/log"), "Ankunftszeit gesetzt aber nicht Distanz + Strecke"),
|
||||
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
|
||||
Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat) => Flash::error(Redirect::to("/log"), "Handsteuer-Status dieses Boots kann nicht verändert werden."),
|
||||
Err(LogbookCreateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
|
||||
Err(LogbookCreateError::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."),
|
||||
Err(LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(Redirect::to("/log"), "Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -328,7 +296,7 @@ async fn create_kiosk(
|
||||
create_logbook(
|
||||
db,
|
||||
data,
|
||||
&DonauLinzUser::new(db, &creator).await.unwrap(),
|
||||
&DonauLinzUser::new(db, creator).await.unwrap(),
|
||||
&config.smtp_pw,
|
||||
)
|
||||
.await
|
||||
@ -344,21 +312,30 @@ async fn update(
|
||||
let data = data.into_inner();
|
||||
|
||||
let Some(logbook) = Logbook::find_by_id(db, data.id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!(
|
||||
"Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt",
|
||||
data.id
|
||||
),
|
||||
);
|
||||
return Flash::error(Redirect::to("/log"), &format!("Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt", data.id));
|
||||
};
|
||||
|
||||
logbook.update(db, data.clone(), &user).await;
|
||||
match logbook.update(db, data.clone(), &user.user).await {
|
||||
Ok(()) => {
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"User {} updated log entry={:?} to {:?}",
|
||||
&user.name, logbook, data
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::success(
|
||||
Redirect::to("/log/show"),
|
||||
"Logbucheintrag erfolgreich bearbeitet".to_string(),
|
||||
)
|
||||
Flash::success(
|
||||
Redirect::to("/log/show"),
|
||||
"Logbucheintrag erfolgreich bearbeitet".to_string(),
|
||||
)
|
||||
}
|
||||
Err(LogbookAdminUpdateError::NotAllowed) => Flash::error(
|
||||
Redirect::to("/log/show"),
|
||||
"Du hast keine Erlaubnis, diesen Logbucheintrag zu bearbeiten!".to_string(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
async fn home_logbook(
|
||||
@ -376,36 +353,14 @@ async fn home_logbook(
|
||||
);
|
||||
};
|
||||
|
||||
match logbook.home(db, user, data.into_inner(), smtp_pw).await {
|
||||
match logbook.home(db,user, data.into_inner(), smtp_pw).await {
|
||||
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"),
|
||||
Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!(
|
||||
"Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)"
|
||||
),
|
||||
),
|
||||
Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten dem Vorstand an info@rudernlinz.at.",
|
||||
),
|
||||
Err(LogbookUpdateError::TooFast(km, min)) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!(
|
||||
"KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut."
|
||||
),
|
||||
),
|
||||
Err(LogbookUpdateError::AlreadyFinalized) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Logbucheintrag wurde bereits abgeschlossen.",
|
||||
),
|
||||
Err(LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!",
|
||||
),
|
||||
Err(LogbookUpdateError::BoatAlreadyOnWater) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
"Das Boot war in diesem Zeitraum schon am Wasser. Bitte überprüfe das Datum und die Zeit.",
|
||||
),
|
||||
Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
|
||||
Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
|
||||
Err(LogbookUpdateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
|
||||
Err(LogbookUpdateError::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."),
|
||||
Err(LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(Redirect::to("/log"), "Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!"),
|
||||
Err(LogbookUpdateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Das Boot war in diesem Zeitraum schon am Wasser. Bitte überprüfe das Datum und die Zeit."),
|
||||
Err(e) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"),
|
||||
@ -435,7 +390,7 @@ async fn home_kiosk(
|
||||
logbook_id,
|
||||
&DonauLinzUser::new(
|
||||
db,
|
||||
&User::find_by_id(db, logbook.shipmaster as i32)
|
||||
User::find_by_id(db, logbook.shipmaster as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
)
|
||||
@ -481,7 +436,10 @@ async fn delete(db: &State<SqlitePool>, logbook_id: i64, user: DonauLinzUser) ->
|
||||
)
|
||||
.await;
|
||||
match logbook.delete(db, &user).await {
|
||||
Ok(_) => Flash::success(Redirect::to(redirect), "Erfolgreich gelöscht"),
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to(redirect),
|
||||
format!("Eintrag {} von {} gelöscht!", logbook_id, user.name),
|
||||
),
|
||||
Err(LogbookDeleteError::NotYourEntry) => Flash::error(
|
||||
Redirect::to(redirect),
|
||||
"Du hast nicht die Berechtigung, den Eintrag zu löschen!",
|
||||
@ -527,11 +485,11 @@ async fn delete_kiosk(
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
index_loggedin,
|
||||
index_kiosk,
|
||||
index,
|
||||
create,
|
||||
create_kiosk,
|
||||
home,
|
||||
kiosk,
|
||||
home_kiosk,
|
||||
new_kiosk,
|
||||
show,
|
||||
|
@ -1,7 +1,7 @@
|
||||
use rocket::{Route, State, get, http::ContentType, routes};
|
||||
use rocket::{get, http::ContentType, routes, Route, State};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{personal::cal::get_personal_cal, planned::event::Event, user::User};
|
||||
use crate::model::{event::Event, personal::cal::get_personal_cal, user::User};
|
||||
|
||||
#[get("/cal")]
|
||||
async fn cal(db: &State<SqlitePool>) -> (ContentType, String) {
|
||||
@ -19,7 +19,7 @@ async fn cal_registered(
|
||||
return Err("Invalid".into());
|
||||
};
|
||||
|
||||
if user.user_token != uuid {
|
||||
if &user.user_token != uuid {
|
||||
return Err("Invalid".into());
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
use std::{fs::OpenOptions, io::Write};
|
||||
|
||||
use chrono::{Datelike, Local};
|
||||
use chrono::Local;
|
||||
use rocket::{
|
||||
Build, Data, FromForm, Request, Rocket, State, catch, catchers,
|
||||
catch, catchers,
|
||||
fairing::{AdHoc, Fairing, Info, Kind},
|
||||
form::Form,
|
||||
fs::FileServer,
|
||||
@ -13,6 +13,7 @@ use rocket::{
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
time::{Duration, OffsetDateTime},
|
||||
Build, Data, FromForm, Request, Rocket, State,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use serde::Deserialize;
|
||||
@ -20,7 +21,6 @@ use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::{
|
||||
SCHECKBUCH,
|
||||
model::{
|
||||
logbook::Logbook,
|
||||
notification::Notification,
|
||||
@ -28,6 +28,7 @@ use crate::{
|
||||
role::Role,
|
||||
user::{User, UserWithDetails},
|
||||
},
|
||||
SCHECKBUCH,
|
||||
};
|
||||
|
||||
pub(crate) mod admin;
|
||||
@ -62,11 +63,6 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
|
||||
context.insert("last_trips", &last_trips);
|
||||
}
|
||||
|
||||
let date = chrono::Utc::now();
|
||||
if date.month() <= 3 || date.month() >= 10 {
|
||||
context.insert("show_quick_ergo_button", "yes");
|
||||
}
|
||||
|
||||
context.insert("achievements", &Achievements::for_user(db, &user).await);
|
||||
context.insert("notifications", &Notification::for_user(db, &user).await);
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
@ -97,6 +93,8 @@ async fn steering(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage
|
||||
User::all_with_role(db, &Role::find_by_name(db, "Bootsführer").await.unwrap()).await;
|
||||
|
||||
let mut coxes = User::all_with_role(db, &Role::find_by_name(db, "cox").await.unwrap()).await;
|
||||
|
||||
coxes.retain(|user| !bootskundige.contains(user)); // Remove bootskundige from coxes list
|
||||
coxes.retain(|user| user.name != "Externe Steuerperson");
|
||||
|
||||
context.insert("coxes", &coxes);
|
||||
@ -108,37 +106,17 @@ async fn steering(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage
|
||||
|
||||
#[post("/", data = "<login>")]
|
||||
async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String {
|
||||
if let Ok(user) = User::login(db, login.name, login.password).await {
|
||||
if user.has_role(db, "allow_website_login").await {
|
||||
return String::from("SUCC");
|
||||
}
|
||||
if user.has_role(db, "admin").await {
|
||||
return String::from("SUCC");
|
||||
}
|
||||
if user.has_role(db, "Vorstand").await {
|
||||
return String::from("SUCC");
|
||||
}
|
||||
match User::login(db, login.name, login.password).await {
|
||||
Ok(_) => "SUCC".into(),
|
||||
Err(_) => "FAIL".into(),
|
||||
}
|
||||
"FAIL".into()
|
||||
}
|
||||
|
||||
#[post("/", data = "<login>")]
|
||||
async fn nextcloud_auth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String {
|
||||
if let Ok(user) = User::login(db, login.name, login.password).await {
|
||||
if user.has_role(db, "admin").await {
|
||||
return String::from("SUCC");
|
||||
}
|
||||
if user.has_role(db, "Vorstand").await {
|
||||
return String::from("SUCC");
|
||||
}
|
||||
}
|
||||
"FAIL".into()
|
||||
}
|
||||
|
||||
#[catch(401)] //Unauthorized
|
||||
fn unauthorized_error(req: &Request) -> Redirect {
|
||||
// Save the URL the user tried to access, to be able to go there once logged in
|
||||
let mut redirect_cookie = Cookie::new("redirect_url", format!("{}", req.uri()));
|
||||
println!("{}", req.uri());
|
||||
redirect_cookie.set_expires(OffsetDateTime::now_utc() + Duration::hours(1));
|
||||
req.cookies().add_private(redirect_cookie);
|
||||
|
||||
@ -201,10 +179,7 @@ async fn blogpost_unpublished(
|
||||
|
||||
#[catch(403)] //forbidden
|
||||
fn forbidden_error() -> Flash<Redirect> {
|
||||
Flash::error(
|
||||
Redirect::to("/"),
|
||||
"Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.",
|
||||
)
|
||||
Flash::error(Redirect::to("/"), "Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.")
|
||||
}
|
||||
|
||||
struct Usage {}
|
||||
@ -279,7 +254,6 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
.mount("/", routes![index, steering, impressum])
|
||||
.mount("/auth", auth::routes())
|
||||
.mount("/wikiauth", routes![wikiauth])
|
||||
.mount("/nxauth", routes![nextcloud_auth])
|
||||
.mount("/new-blogpost", routes![new_blogpost])
|
||||
.mount("/blogpost-unpublished", routes![blogpost_unpublished])
|
||||
.mount("/log", log::routes())
|
||||
@ -330,13 +304,11 @@ mod test {
|
||||
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
|
||||
assert!(
|
||||
response
|
||||
.into_string()
|
||||
.await
|
||||
.unwrap()
|
||||
.contains("Ruderassistent")
|
||||
);
|
||||
assert!(response
|
||||
.into_string()
|
||||
.await
|
||||
.unwrap()
|
||||
.contains("Ruderassistent"));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
|
@ -1,7 +1,7 @@
|
||||
use rocket::{
|
||||
Route, State, get,
|
||||
get,
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
routes, Route, State,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
@ -27,12 +27,6 @@ async fn mark_read(db: &State<SqlitePool>, user: User, notification_id: i64) ->
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/read/all")]
|
||||
async fn mark_all_read(db: &State<SqlitePool>, user: User) -> Flash<Redirect> {
|
||||
Notification::mark_all_read(db, &user).await;
|
||||
Flash::success(Redirect::to("/"), "Alle Nachrichten als gelesen markiert")
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![mark_read, mark_all_read]
|
||||
routes![mark_read]
|
||||
}
|
||||
|
@ -1,24 +1,22 @@
|
||||
use rocket::{
|
||||
Route, State, get,
|
||||
get,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
routes, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::{
|
||||
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD,
|
||||
model::{
|
||||
log::Log,
|
||||
planned::{
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
|
||||
},
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
user::{AllowedForPlannedTripsUser, User, UserWithDetails},
|
||||
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
|
||||
},
|
||||
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD,
|
||||
};
|
||||
|
||||
#[get("/")]
|
||||
@ -31,10 +29,7 @@ async fn index(
|
||||
|
||||
let mut context = Context::new();
|
||||
|
||||
if user.allowed_to_steer(db).await
|
||||
|| user.has_role(db, "manage_events").await
|
||||
|| user.has_role(db, "ergo").await
|
||||
{
|
||||
if user.has_role(db, "cox").await || user.has_role(db, "manage_events").await {
|
||||
let triptypes = TripType::all(db).await;
|
||||
context.insert("trip_types", &triptypes);
|
||||
}
|
||||
@ -83,15 +78,14 @@ async fn join(
|
||||
),
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
}else{
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"User {} registered the guest '{}' for trip_details.id={}",
|
||||
user.name, registered_user, trip_details_id
|
||||
),
|
||||
)
|
||||
.await;
|
||||
).await;
|
||||
}
|
||||
Flash::success(Redirect::to("/planned"), "Erfolgreich angemeldet!")
|
||||
}
|
||||
@ -101,10 +95,9 @@ async fn join(
|
||||
Err(UserTripError::AlreadyRegistered) => {
|
||||
Flash::error(Redirect::to("/planned"), "Du nimmst bereits teil!")
|
||||
}
|
||||
Err(UserTripError::AlreadyRegisteredAsCox) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Du hilfst bereits als Steuerperson aus!",
|
||||
),
|
||||
Err(UserTripError::AlreadyRegisteredAsCox) => {
|
||||
Flash::error(Redirect::to("/planned"), "Du hilfst bereits als Steuerperson aus!")
|
||||
}
|
||||
Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)",
|
||||
@ -164,10 +157,7 @@ async fn remove_guest(
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht mitrudern kannst, melde dich bitte unbedingt schnellstmöglich bei einer angemeldeten Steuerperson!",
|
||||
)
|
||||
Flash::error(Redirect::to("/planned"), "Die Bootseinteilung wurde bereits gemacht. Wenn du doch nicht mitrudern kannst, melde dich bitte unbedingt schnellstmöglich bei einer angemeldeten Steuerperson!")
|
||||
}
|
||||
Err(UserTripDeleteError::GuestNotParticipating) => {
|
||||
Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.")
|
||||
@ -218,10 +208,7 @@ async fn remove(
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.",
|
||||
)
|
||||
Flash::error(Redirect::to("/planned"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
|
||||
}
|
||||
Err(UserTripDeleteError::NotVisibleToUser) => {
|
||||
Log::create(
|
||||
@ -233,10 +220,7 @@ async fn remove(
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Abmeldung nicht möglich, da du dieses Event eigentlich gar nicht sehen solltest...",
|
||||
)
|
||||
Flash::error(Redirect::to("/planned"), "Abmeldung nicht möglich, da du dieses Event eigentlich gar nicht sehen solltest...")
|
||||
}
|
||||
Err(_) => {
|
||||
panic!("Not possible to be here");
|
||||
|
@ -1,5 +1,5 @@
|
||||
use rocket::{Route, State, get, routes};
|
||||
use rocket_dyn_templates::{Template, context};
|
||||
use rocket::{get, routes, Route, State};
|
||||
use rocket_dyn_templates::{context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{
|
||||
@ -32,14 +32,13 @@ async fn index_boat_kiosk(db: &State<SqlitePool>, _kiosk: KioskCookie) -> Templa
|
||||
async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -> Template {
|
||||
let stat = Stat::people(db, year).await;
|
||||
let club_km = Stat::sum_people(db, year).await;
|
||||
let club_trips = Stat::trips_people(db, year).await;
|
||||
let guest_km = Stat::guest(db, year).await;
|
||||
let personal = stat::get_personal(db, &user).await;
|
||||
let kiosk = false;
|
||||
|
||||
Template::render(
|
||||
"stat.people",
|
||||
context!(loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await, stat, personal, kiosk, guest_km, club_km, club_trips),
|
||||
context!(loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await, stat, personal, kiosk, guest_km, club_km),
|
||||
)
|
||||
}
|
||||
|
||||
@ -47,13 +46,12 @@ async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -
|
||||
async fn index_kiosk(db: &State<SqlitePool>, _kiosk: KioskCookie, year: Option<i32>) -> Template {
|
||||
let stat = Stat::people(db, year).await;
|
||||
let club_km = Stat::sum_people(db, year).await;
|
||||
let club_trips = Stat::trips_people(db, year).await;
|
||||
let guest_km = Stat::guest(db, year).await;
|
||||
let kiosk = true;
|
||||
|
||||
Template::render(
|
||||
"stat.people",
|
||||
context!(stat, kiosk, show_kiosk_header: true, guest_km, club_km, club_trips),
|
||||
context!(stat, kiosk, show_kiosk_header: true, guest_km, club_km),
|
||||
)
|
||||
}
|
||||
|
||||
@ -62,30 +60,4 @@ pub fn routes() -> Vec<Route> {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use rocket::{http::Status, local::asynchronous::Client};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::testdb;
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_kiosk_stat() {
|
||||
let db = testdb!();
|
||||
|
||||
let rocket = rocket::build().manage(db.clone());
|
||||
let rocket = crate::tera::config(rocket);
|
||||
|
||||
let client = Client::tracked(rocket).await.unwrap();
|
||||
// "Log in"
|
||||
let req = client.get("/log/kiosk/ekrv2019/Linz");
|
||||
let _ = req.dispatch().await;
|
||||
|
||||
// `/stat` should be viewable
|
||||
let req = client.get("/stat");
|
||||
let response = req.dispatch().await;
|
||||
|
||||
assert_eq!(response.status(), Status::Ok);
|
||||
let text = response.into_string().await.unwrap();
|
||||
assert!(text.contains("Statistik"));
|
||||
}
|
||||
}
|
||||
mod test {}
|
||||
|
@ -1,11 +1,10 @@
|
||||
use chrono::NaiveDate;
|
||||
use rocket::{
|
||||
FromForm, Route, State,
|
||||
form::Form,
|
||||
get, post,
|
||||
request::FlashMessage,
|
||||
response::{Flash, Redirect},
|
||||
routes,
|
||||
routes, FromForm, Route, State,
|
||||
};
|
||||
use rocket_dyn_templates::Template;
|
||||
use sqlx::SqlitePool;
|
||||
|
@ -22,30 +22,6 @@
|
||||
<input type="submit" class="btn btn-primary" value="Abschicken" />
|
||||
</form>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Mitglieds-Beitrags-Info</h2>
|
||||
<div class="p-3 grid gap-3">
|
||||
<a class="btn btn-primary" href="/admin/mail/fee/test">Test-Mail an mich versenden</a>
|
||||
<a class="btn btn-alert"
|
||||
href="/admin/mail/fee"
|
||||
onclick="return confirm('Hast du die Gebührenauflistung geprüft und willst du die Mail an alle ausschicken?');">
|
||||
An ALLE Mitglieder versenden
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Unfreundliche Zahlungsaufforderung</h2>
|
||||
<div class="p-3 grid gap-3">
|
||||
<a class="btn btn-primary" href="/admin/mail/fee-final/test">Test-Mail an mich versenden</a>
|
||||
<a class="btn btn-alert"
|
||||
href="/admin/mail/fee-final"
|
||||
onclick="return confirm('Hast du die Gebührenauflistung geprüft, gecheckt ob alle die bereits bezahlt haben auch eingetragen wurden und willst du die Mail an alle, die noch nicht bezahlt haben ausschicken?');">
|
||||
An ALLE Mitglieder versenden, die noch nicht bezahlt haben
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
@ -1,44 +0,0 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% import "includes/forms/boat" as boat %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full dark:text-white">
|
||||
<h1 class="h1">Rollen</h1>
|
||||
<div class="search-wrapper">
|
||||
<label for="name" class="sr-only">Suche</label>
|
||||
<input type="search"
|
||||
name="name"
|
||||
id="filter-js"
|
||||
class="search-bar"
|
||||
placeholder="Suchen nach Namen...">
|
||||
</div>
|
||||
<div id="filter-result-js" class="search-result"></div>
|
||||
<div class="border-r border-l border-gray-200 dark:border-primary-600">
|
||||
{% for role in roles %}
|
||||
<div data-filterable="true"
|
||||
data-filter="{{ role.name }} {{ role.formatted_name }}"
|
||||
class="w-full border-t">
|
||||
<form action="/admin/role/{{ role.id }}"
|
||||
data-filterable="true"
|
||||
method="post"
|
||||
class="bg-white dark:bg-primary-900 p-4 w-full">
|
||||
<div class="w-full">
|
||||
<input type="hidden" name="id" value="{{ role.id }}" />
|
||||
<div class="font-bold mb-1 text-black dark:text-white">
|
||||
{{ role.name }}
|
||||
<br />
|
||||
</div>
|
||||
<div class="grid md:grid-cols-3 gap-3">
|
||||
{{ macros::input(label='Name (formatiert)', name='formatted_name', type='text', value=role.formatted_name) }}
|
||||
{{ macros::input(label='Beschreibung', name='desc', type='text', value=role.desc) }}
|
||||
<div class="flex items-end">
|
||||
<input value="Ändern" type="submit" class="w-full btn btn-primary" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -4,189 +4,122 @@
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Users</h1>
|
||||
{% if allowed_to_edit %}
|
||||
<details class="mt-5 bg-gray-200 dark:bg-primary-600 p-3 rounded-md">
|
||||
<summary class="px-3 cursor-pointer text-md font-bold text-primary-950 dark:text-white">
|
||||
Neue Person hinzufügen
|
||||
</summary>
|
||||
<div class="grid sm:grid-cols-3 gap-3 mt-3">
|
||||
<button type="button"
|
||||
onclick="document.getElementById('add-clubuser').showModal()"
|
||||
class="btn btn-primary">🥳 Vereinsmitglied</button>
|
||||
<button type="button"
|
||||
onclick="document.getElementById('add-scheckbuch').showModal()"
|
||||
class="btn btn-dark">🧑🏫 Scheckbuch</button>
|
||||
<button type="button"
|
||||
onclick="document.getElementById('add-schnupperkurs').showModal()"
|
||||
class="btn btn-dark">👨🎓 Schnupperkurs</button>
|
||||
<form action="/admin/user/new"
|
||||
method="post"
|
||||
class="mt-4 bg-primary-900 rounded-md text-white px-3 pb-3 pt-2 sm:flex items-end justify-between">
|
||||
<div class="w-full">
|
||||
<h2 class="text-md font-bold mb-2 uppercase tracking-wide">Neuen User hinzufügen</h2>
|
||||
<div class="grid md:grid-cols-3">
|
||||
<div>
|
||||
<label for="name" class="sr-only">Name</label>
|
||||
<input type="text"
|
||||
name="name"
|
||||
class="relative block rounded-md border-0 py-1.5 px-2 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:text-sm sm:leading-6 mb-2 md:mb-0"
|
||||
placeholder="Name" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="add-clubuser"
|
||||
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
|
||||
onclick="document.getElementById('add-clubuser').close()">
|
||||
<div onclick="event.stopPropagation();" class="p-3">
|
||||
<button type="button"
|
||||
onclick="document.getElementById('add-clubuser').close()"
|
||||
title="Schließen"
|
||||
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
|
||||
<svg class="inline h-5 w-5"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="mt-8">
|
||||
<h2 class="h3 mb-3">Neues Vereinsmitglied</h2>
|
||||
<form action="/admin/user/new/clubmember"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="grid gap-3">
|
||||
<div>
|
||||
<label for="membertype" class="text-sm text-gray-600 dark:text-gray-100">Mitgliedstyp</label>
|
||||
<select name="membertype" id="membertype" class="input rounded-md ">
|
||||
<option selected="" value="regular">Reguläres Vereinsmitglied</option>
|
||||
<option value="unterstuetzend">Unterstützendes Vereinsmitglied</option>
|
||||
<option value="foerdernd">Förderndes Vereinsmitglied</option>
|
||||
</select>
|
||||
</div>
|
||||
{{ macros::input(label='Name', name='name', type="text", required=true) }}
|
||||
{{ macros::input(label='Mailadresse', name='mail', type="email", required=true, placeholder='user@mail.at') }}
|
||||
{{ macros::select(label="Finanzielles", data=financial, name='financial_id', display=['name'], default="Keine Ermäßigung") }}
|
||||
{{ macros::input(label='Mitglied seit', name='member_since', type="date", value=now() | date(), required=true) }}
|
||||
{{ macros::input(label='Geburtsdatum', name='birthdate', type="date", required=true) }}
|
||||
{{ macros::input(label='Telefonnummer', name='phone', type="text", required=true) }}
|
||||
{{ macros::input(label='Adresse', name='address', type="text", required=true) }}
|
||||
{{ macros::input(label='Beitrittserklärung', name='membership_pdf', type="file", accept='application/pdf', required=true) }}
|
||||
<input value="Neues Vereinsmitglied anlegen"
|
||||
type="submit"
|
||||
class="btn btn-primary" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog id="add-scheckbuch"
|
||||
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
|
||||
onclick="document.getElementById('add-scheckbuch').close()">
|
||||
<div onclick="event.stopPropagation();" class="p-3">
|
||||
<button type="button"
|
||||
onclick="document.getElementById('add-scheckbuch').close()"
|
||||
title="Schließen"
|
||||
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
|
||||
<svg class="inline h-5 w-5"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="mt-8">
|
||||
<h2 class="h3 mb-3">Neues Scheckbuch</h2>
|
||||
<form action="/admin/user/new/scheckbuch"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="grid gap-3">
|
||||
{{ macros::input(label='Name', name='name', type="text", required=true) }}
|
||||
{{ macros::input(label='Mailadresse', name='mail', type="email", required=true, placeholder='user@mail.at') }}
|
||||
<input value="Neues Scheckbuch anlegen"
|
||||
type="submit"
|
||||
class="btn btn-primary" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog id="add-schnupperkurs"
|
||||
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
|
||||
onclick="document.getElementById('add-schnupperkurs').close()">
|
||||
<div onclick="event.stopPropagation();" class="p-3">
|
||||
<button type="button"
|
||||
onclick="document.getElementById('add-schnupperkurs').close()"
|
||||
title="Schließen"
|
||||
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
|
||||
<svg class="inline h-5 w-5"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="mt-8">
|
||||
<form action="/admin/user/new/schnupper"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="grid gap-3">
|
||||
<h2 class="h3 mb-3">Neuer Schnupperant</h2>
|
||||
<div>
|
||||
<label for="schnupper_type" class="text-sm text-gray-600 dark:text-gray-100">Typ</label>
|
||||
<select name="schnupper_type" id="schnupper_type" class="input rounded-md ">
|
||||
<option value="schnupperInterested">Interessiert am Schnupperkurs</option>
|
||||
<option value="schnupperant">Fixe Schnupperkurs-Anmeldung</option>
|
||||
</select>
|
||||
</div>
|
||||
{{ macros::input(label='Name', name='name', type="text", required=true) }}
|
||||
{{ macros::input(label='Mailadresse', name='mail', type="email", required=true, placeholder='user@mail.at') }}
|
||||
{{ macros::select(label="Finanzielles", data=financial, name='financial_id', display=['name'], default="Keine Ermäßigung") }}
|
||||
<input value="Hinzufügen" type="submit" class="btn btn-primary" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</details>
|
||||
<div class="text-right">
|
||||
<input value="Hinzufügen"
|
||||
type="submit"
|
||||
class="w-28 mt-2 sm:mt-0 rounded-md bg-primary-500 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer" />
|
||||
</div>
|
||||
</form>
|
||||
{% endif %}
|
||||
<!-- START filterBar -->
|
||||
<div class="search-wrapper flex">
|
||||
<div class="search-wrapper">
|
||||
<label for="name" class="sr-only">Suche</label>
|
||||
<input type="search"
|
||||
name="name"
|
||||
id="filter-js"
|
||||
class="search-bar"
|
||||
placeholder="Suchen nach (Name, [yes|no]-role:<name>, has-[no-]membership-pdf)" />
|
||||
<div class="relative">
|
||||
<button id="dropdownbtn"
|
||||
data-dropdown="dropdown"
|
||||
class="btn btn-dark ml-3"
|
||||
type="button">Sortieren</button>
|
||||
<!-- Dropdown menu -->
|
||||
<div id="dropdown"
|
||||
class="z-10 hidden bg-white divide-y divide-gray-100 text-secondary-900 rounded-lg shadow-sm w-44 absolute right-0">
|
||||
<ul class="py-2 text-sm" aria-labelledby="dropdownbtn">
|
||||
<li>
|
||||
<a href="./user"
|
||||
class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Zuletzt eingeloggt</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="?sort=name&asc"
|
||||
class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Name A-Z</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="?sort=name"
|
||||
class="block px-4 py-2 hover:bg-gray-100 hover:text-secondary-950">Name Z-A</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- END filterBar -->
|
||||
<div id="filter-result-js" class="search-result"></div>
|
||||
{% for user in users %}
|
||||
<div data-filterable="true"
|
||||
data-filter="{{ user.name }} {% for role in roles %} {% if role.name in user.roles %} yes-role:{{ role.name }} {% else %} no-role:{{ role.name }} {% endif %} role-{{ role }} {% endfor %} {% if user.membership_pdf %}has-membership-pdf{% else %}has-no-membership-pdf{% endif %}"
|
||||
class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative flex justify-between items-center">
|
||||
<span class="text-black dark:text-white">
|
||||
<span class="font-bold">
|
||||
{{ user.name }}
|
||||
{% if user.last_access %}• ⏳ {{ user.last_access | date }}{% endif %}
|
||||
</span>
|
||||
<small class="block text-gray-600 dark:text-gray-100">
|
||||
{% for role in user.roles -%}
|
||||
{{ role }}
|
||||
{%- if not loop.last %},
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
</small>
|
||||
</span>
|
||||
<a href="/admin/user/{{ user.id }}" class="btn btn-dark ml-3">{% include "includes/pencil" %}</a>
|
||||
class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative">
|
||||
<details class="block dark:text-white w-full">
|
||||
<summary>
|
||||
<span class="text-black dark:text-white cursor-pointer">
|
||||
<span class="font-bold">
|
||||
{{ user.name }}
|
||||
{% if not user.last_access and "admin" in loggedin_user.roles and user.mail %}
|
||||
<form action="/admin/user"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="inline">
|
||||
• <a class="font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
|
||||
href="/admin/user/{{ user.id }}/send-welcome-mail"
|
||||
onclick="return confirm('Willst du wirklich das Willkommensmail an {{ user.name }} ausschicken?');">Willkommensmail verschicken</a>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if user.last_access %}• ⏳ {{ user.last_access | date }}{% endif %}
|
||||
</span>
|
||||
<small class="block text-gray-600 dark:text-gray-100">
|
||||
{% for role in user.roles -%}
|
||||
{{ role }}
|
||||
{%- if not loop.last %},
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
</small>
|
||||
</span>
|
||||
</summary>
|
||||
<form action="/admin/user"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="w-full mt-2">
|
||||
{% if user.pw %}
|
||||
<a class="block my-1 font-normal text-[#f43f5e] dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
|
||||
href="/admin/user/{{ user.id }}/reset-pw"
|
||||
onclick="return confirm('Willst du wirklich das Passwort zurücksetzen?');">Passwort zurücksetzen</a>
|
||||
{% endif %}
|
||||
<div class="w-full grid gap-3 mt-3">
|
||||
<input type="hidden" name="id" value="{{ user.id }}" />
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
{# for cluster, r in roles | group_by(attribute="cluster") #}
|
||||
{# cluster #}
|
||||
{% for role in roles %}
|
||||
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false) }}
|
||||
{% endfor %}
|
||||
{# endfor #}
|
||||
<hr class="sm:col-span-2 lg:col-span-4 my-3" />
|
||||
{% if user.membership_pdf %}
|
||||
<a href="/admin/user/{{ user.id }}/membership"
|
||||
class="text-black dark:text-white">Beitrittserklärung herunterladen</a>
|
||||
{% else %}
|
||||
{{ macros::input(label='Beitrittserklärung', name='membership_pdf', id=loop.index, type="file", readonly=allowed_to_edit == false, accept='application/pdf') }}
|
||||
{% endif %}
|
||||
{{ macros::input(label='DOB', name='dob', id=loop.index, type="text", value=user.dob, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Weight (kg)', name='weight', id=loop.index, type="text", value=user.weight, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Sex', name='sex', id=loop.index, type="text", value=user.sex, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Mitglied seit', name='member_since_date', id=loop.index, type="text", value=user.member_since_date, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Geburtsdatum', name='birthdate', id=loop.index, type="text", value=user.birthdate, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Mail', name='mail', id=loop.index, type="text", value=user.mail, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Nickname', name='nickname', id=loop.index, type="text", value=user.nickname, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Notizen', name='notes', id=loop.index, type="text", value=user.notes, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Telefon', name='phone', id=loop.index, type="text", value=user.phone, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::input(label='Adresse', name='address', id=loop.index, type="text", value=user.address, readonly=allowed_to_edit == false) }}
|
||||
{% if allowed_to_edit %}
|
||||
{{ macros::select(label="Familie", data=families, name='family_id', selected_id=user.family_id, display=['names'], default="Keine Familie", new_last_entry='Neue Familie anlegen') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if allowed_to_edit %}
|
||||
<div class="mt-3 text-right">
|
||||
<a href="/admin/user/{{ user.id }}/delete"
|
||||
class="w-28 btn btn-alert"
|
||||
onclick="return confirm('Wirklich löschen?');">
|
||||
{% include "includes/delete-icon" %}
|
||||
Löschen
|
||||
</a>
|
||||
<input value="Ändern" type="submit" class="w-28 btn btn-primary ml-1" />
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
</details>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
@ -55,11 +55,9 @@
|
||||
<div class="grid sm:grid-cols-1 gap-3">
|
||||
<div style="width: 100%" class="col-span-2">
|
||||
<b>{{ user.name }} - Ausfahrten: {{ trips | length }}</b>
|
||||
<ul class="list-disc ms-4">
|
||||
{% for trip in trips %}
|
||||
<li>{{ log::show_old(log=trip, state="completed", only_ones=false, index=loop.index) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% for trip in trips %}
|
||||
<li>{{ log::show_old(log=trip, state="completed", only_ones=false, index=loop.index) }}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if "admin" in loggedin_user.roles or "kassier" in loggedin_user.roles %}
|
||||
<a href="/admin/user/fees/paid?user_ids[]={{ user.id }}">Zahlungsstatus ändern</a>
|
||||
|
@ -1,437 +0,0 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% import "includes/forms/log" as log %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full">
|
||||
{% if "admin" in loggedin_user.roles or "Vorstand" in loggedin_user.roles %}
|
||||
<div class="mb-5 lg:mb-0">
|
||||
<a href="/admin/user" class="link link-primary link-no-underline">← Userverwaltung</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h1 class="h1">{{ user.name }}</h1>
|
||||
<div class="grid sm:grid-cols-2 gap-8 my-8">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow">
|
||||
<h2 class="h2">
|
||||
Grunddaten
|
||||
<br />
|
||||
<small class="inline-block text-xs text-gray-500 dark:text-gray-100 ">
|
||||
{% if user.last_access %}
|
||||
Zuletzt eingeloggt am {{ user.last_access | date(format="%d. %m. %Y") }}
|
||||
{% else %}
|
||||
App-Boykott 😢
|
||||
{% endif %}
|
||||
</small>
|
||||
</h2>
|
||||
<div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600">
|
||||
<div class="py-3 grid gap-3">
|
||||
<form action="/admin/user/{{ user.id }}/change-mail" method="post">
|
||||
{{ macros::inputgroup(label='Mailadresse', name='mail', type="text", value=user.mail, readonly=not allowed_to_edit) }}
|
||||
</form>
|
||||
<form action="/admin/user/{{ user.id }}/change-phone" method="post">
|
||||
{{ macros::inputgroup(label='Telefonnummer', name='phone', type="text", value=user.phone, readonly=not allowed_to_edit) }}
|
||||
</form>
|
||||
<form action="/admin/user/{{ user.id }}/change-nickname" method="post">
|
||||
{{ macros::inputgroup(label='Spitzname', name='nickname', type="text", value=user.nickname, readonly=not allowed_to_edit) }}
|
||||
</form>
|
||||
<form action="/admin/user/{{ user.id }}/change-financial" method="post">
|
||||
{% if user_financial %}
|
||||
{{ macros::selectgroup(label="Finanzielles", data=financial, selected_id=user_financial.id, name='financial_id', display=['name'], default="Keine Ermäßigung", readonly=not allowed_to_edit) }}
|
||||
{% else %}
|
||||
{{ macros::selectgroup(label="Finanzielles", data=financial, name='financial_id', display=['name'], default="Keine Ermäßigung", readonly=not allowed_to_edit) }}
|
||||
{% endif %}
|
||||
</form>
|
||||
{% if allowed_to_edit %}
|
||||
<form action="/admin/user/{{ user.id }}/new-note" method="post">
|
||||
{{ macros::inputgroup(label='Neue Notiz', name='note', type="text") }}
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if user.pw and allowed_to_edit %}
|
||||
<div>
|
||||
<a class="block my-1 font-normal text-[#f43f5e] dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
|
||||
href="/admin/user/{{ user.id }}/reset-pw"
|
||||
onclick="return confirm('Willst du wirklich das Passwort zurücksetzen?');">Passwort zurücksetzen</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow">
|
||||
<h2 class="h2">
|
||||
Mitgliedschaft
|
||||
<br />
|
||||
<small class="inline-block text-xs text-gray-500 dark:text-gray-100 ">
|
||||
{% if "SchnupperInterest" in member %}
|
||||
Interessiert am Schnupperkurs
|
||||
{% elif "Schnupperant" in member %}
|
||||
Beim nächsten Schnupperkurs angemeldet
|
||||
{% elif "Scheckbuch" in member %}
|
||||
{% set logbook = member["Scheckbuch"] %}
|
||||
Scheckbuch (Ausfahrten: {{ logbook | length }})
|
||||
{% elif "Regular" in member %}
|
||||
Reguläres Vereinsmitglied
|
||||
{% elif "Foerdernd" in member %}
|
||||
Förderndes Vereinsmitglied
|
||||
{% elif "Unterstuetzend" in member %}
|
||||
Unterstützendes Vereinsmitglied
|
||||
{% endif %}
|
||||
</small>
|
||||
</h2>
|
||||
<div class="mx-3">
|
||||
{% if is_clubmember %}
|
||||
<div class="py-3 grid gap-3">
|
||||
<form action="/admin/user/{{ user.id }}/change-member-since" method="post">
|
||||
{{ macros::inputgroup(label='Mitglied seit', name='member_since', type="date", value=user.member_since_date, readonly=not allowed_to_edit) }}
|
||||
</form>
|
||||
<form action="/admin/user/{{ user.id }}/change-birthdate" method="post">
|
||||
{{ macros::inputgroup(label='Geburtsdatum', name='birthdate', type="date", value=user.birthdate, readonly=not allowed_to_edit) }}
|
||||
</form>
|
||||
<form action="/admin/user/{{ user.id }}/change-address" method="post">
|
||||
{{ macros::inputgroup(label='Adresse', name='address', type="text", value=user.address, readonly=not allowed_to_edit) }}
|
||||
</form>
|
||||
<form action="/admin/user/{{ user.id }}/change-skill" method="post">
|
||||
{% if user_skill %}
|
||||
{{ macros::selectgroup(label="Steuererlaubnis", data=skill, selected_id=user_skill.id, name='skill_id', display=['name'], default="Keine Steuerberechtigung", readonly=not allowed_to_edit) }}
|
||||
{% else %}
|
||||
{{ macros::selectgroup(label="Steuererlaubnis", data=skill, name='skill_id', display=['name'], default="Keine Steuerberechtigung", readonly=not allowed_to_edit) }}
|
||||
{% endif %}
|
||||
</form>
|
||||
<form action="/admin/user/{{ user.id }}/change-family" method="post">
|
||||
{{ macros::selectgroup(label="Familie", data=families, name='family_id', selected_id=user.family_id, display=['names'], default="Keine Familie", new_last_entry='Neue Familie anlegen', readonly=not allowed_to_edit) }}
|
||||
</form>
|
||||
</div>
|
||||
<div class="py-3">
|
||||
{% if user.membership_pdf %}
|
||||
<a href="/admin/user/{{ user.id }}/membership"
|
||||
class="link link-primary link-no-underline">Beitrittserklärung herunterladen ↓</a>
|
||||
{% else %}
|
||||
⚠️ Aktuell gibt's keine Beitrittserklärung 😢
|
||||
{% if allowed_to_edit %}
|
||||
Das kannst du hier ändern ⤵️
|
||||
<form action="/admin/user/{{ user.id }}/add-membership-pdf"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="grid gap-3">
|
||||
<fieldset>
|
||||
{{ macros::input(label='Neue Beitrittserklärung hochladen', name='membership_pdf', type="file", accept='application/pdf') }}
|
||||
</fieldset>
|
||||
<input value="Hochladen" type="submit" class="btn btn-primary" />
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if allowed_to_edit %}
|
||||
<div class="py-3">
|
||||
<div class="text-right">
|
||||
<button type="button"
|
||||
onclick="document.getElementById('change-member-type').showModal()"
|
||||
class="btn btn-dark">Mitgliedsstatus ändern</button>
|
||||
<a href="/admin/user/{{ user.id }}/delete"
|
||||
class="btn btn-alert mt-3"
|
||||
onclick="return confirm('Ist {{ user.name }} wirklich aus dem Verein ausgetreten?');">
|
||||
{% include "includes/delete-icon" %}
|
||||
Mitglied ist ausgetreten
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="change-member-type"
|
||||
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
|
||||
onclick="document.getElementById('change-member-type').close()">
|
||||
<div onclick="event.stopPropagation();" class="p-3">
|
||||
<button type="button"
|
||||
onclick="document.getElementById('change-member-type').close()"
|
||||
title="Schließen"
|
||||
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
|
||||
<svg class="inline h-5 w-5"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="mt-8">
|
||||
<form action="/admin/user/{{ user.id }}/change-membertype"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="grid gap-3">
|
||||
<div>
|
||||
<label for="membertype" class="text-sm text-gray-600 dark:text-gray-100">Mitgliedsstatus</label>
|
||||
<select name="membertype" id="membertype" class="input rounded-md ">
|
||||
<option selected="" value="regular">Reguläres Vereinsmitglied</option>
|
||||
<option value="unterstuetzend">Unterstützendes Vereinsmitglied</option>
|
||||
<option value="foerdernd">Förderndes Vereinsmitglied</option>
|
||||
</select>
|
||||
</div>
|
||||
<input value="Ändern" type="submit" class="btn btn-primary" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
{% endif %}
|
||||
{% elif "Scheckbuch" in member %}
|
||||
{% if allowed_to_edit %}
|
||||
<div class="grid gap-3 pb-3">
|
||||
<div class="max-h-60 overflow-y-scroll">
|
||||
{% for log in logbook %}
|
||||
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, allowed_to_edit=false) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid pt-3">
|
||||
<a href="/admin/user/{{ user.id }}/delete"
|
||||
class="btn btn-alert"
|
||||
onclick="return confirm('Willst du die Daten von {{ user.name }} wirklich? Seine restlichen Scheckbuch-Ausfahrten entfallen damit...');">
|
||||
{% include "includes/delete-icon" %}
|
||||
Daten löschen
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif "SchnupperInterest" in member %}
|
||||
{% if allowed_to_edit %}
|
||||
<div class="grid pt-3">
|
||||
<a href="/admin/user/{{ user.id }}/schnupperinterest-to-schnupperant"
|
||||
class="btn btn-dark"
|
||||
onclick="return confirm('Hat sich \'{{ user.name }}\' wirklich zum Kurs angemeldet?');">Zum Schnupperkurs angemeldet</a>
|
||||
</div>
|
||||
<div class="grid pt-3">
|
||||
<a href="/admin/user/{{ user.id }}/schnupperinterest-to-scheckbuch"
|
||||
class="btn btn-dark"
|
||||
onclick="return confirm('Willst du \'{{ user.name }}\' wirklich auf ein Scheckbuch umwandeln?');">In Scheckbuch umwandeln</a>
|
||||
</div>
|
||||
<div class="grid pt-3">
|
||||
<a href="/admin/user/{{ user.id }}/delete"
|
||||
class="btn btn-alert"
|
||||
onclick="return confirm('Ist {{ user.name }} wirklich nicht mehr am Schnupperkurs interessiert?');">
|
||||
{% include "includes/delete-icon" %}
|
||||
Daten löschen
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% elif "Schnupperant" in member %}
|
||||
{% if allowed_to_edit %}
|
||||
<div class="grid pt-3">
|
||||
<a href="/admin/user/{{ user.id }}/schnupperant-to-schnupperinterest"
|
||||
class="btn btn-dark"
|
||||
onclick="return confirm('Hat sich \'{{ user.name }}\' wirklich vom Schnupperkurs abgemeldet?');">Vom Kurs abgemeldet</a>
|
||||
</div>
|
||||
<div class="grid pt-3">
|
||||
<a href="/admin/user/{{ user.id }}/schnupperant-to-scheckbuch"
|
||||
class="btn btn-dark"
|
||||
onclick="return confirm('Willst du \'{{ user.name }}\' wirklich auf ein Scheckbuch umwandeln?');">In Scheckbuch umwandeln</a>
|
||||
</div>
|
||||
<div class="grid pt-3">
|
||||
<a href="/admin/user/{{ user.id }}/delete"
|
||||
class="btn btn-alert"
|
||||
onclick="return confirm('Ist {{ user.name }} wirklich nicht mehr am Schnupperkurs interessiert?');">
|
||||
{% include "includes/delete-icon" %}
|
||||
Daten löschen
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if "Scheckbuch" in member or "Schnupperant" in member %}
|
||||
{% if allowed_to_edit %}
|
||||
<div class="grid gap-3 pb-3 mt-3">
|
||||
<button type="button"
|
||||
onclick="document.getElementById('call-for-action').showModal()"
|
||||
class="btn btn-primary">Zu Vereinsmitglied umwandeln</button>
|
||||
</div>
|
||||
<dialog id="call-for-action"
|
||||
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
|
||||
onclick="document.getElementById('call-for-action').close()">
|
||||
<div onclick="event.stopPropagation();" class="p-3">
|
||||
<button type="button"
|
||||
onclick="document.getElementById('call-for-action').close()"
|
||||
title="Schließen"
|
||||
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
|
||||
<svg class="inline h-5 w-5"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="mt-8">
|
||||
{% if "Scheckbuch" in member %}
|
||||
{% set action = "scheckbook-to-regular" %}
|
||||
{% elif "Schnupperant" in member %}
|
||||
{% set action = "schnupperant-to-regular" %}
|
||||
{% endif %}
|
||||
<form action="/admin/user/{{ user.id }}/{{ action }}"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="grid gap-3">
|
||||
<div>
|
||||
<label for="membertype" class="text-sm text-gray-600 dark:text-gray-100">Mitgliedstyp</label>
|
||||
<select name="membertype" id="membertype" class="input rounded-md ">
|
||||
<option selected="" value="regular">Reguläres Vereinsmitglied</option>
|
||||
<option value="unterstuetzend">Unterstützendes Vereinsmitglied</option>
|
||||
<option value="foerdernd">Förderndes Vereinsmitglied</option>
|
||||
</select>
|
||||
</div>
|
||||
{{ macros::input(label='Mitglied seit', name='member_since', type="date", value=now() | date(), required=true) }}
|
||||
{{ macros::input(label='Geburtsdatum', name='birthdate', type="date", value=user.birthdate, required=true) }}
|
||||
{{ macros::input(label='Telefonnummer', name='phone', type="text", value=user.phone, required=true) }}
|
||||
{{ macros::input(label='Adresse', name='address', type="text", value=user.address, required=true) }}
|
||||
{{ macros::input(label='Beitrittserklärung', name='membership_pdf', type="file", accept='application/pdf', required=true) }}
|
||||
<input value="Als neues, reguläres Mitglied anlegen"
|
||||
type="submit"
|
||||
class="btn btn-primary" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if is_clubmember %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow">
|
||||
<h2 class="h2">Rollen</h2>
|
||||
<div>
|
||||
<ul class="divide-y divide-gray-200 dark:divide-primary-60 w-full">
|
||||
{% for role in user.proper_roles -%}
|
||||
{% if not role.cluster and not role.hide_in_lists %}
|
||||
<li class="flex w-full justify-between items-center p-3 {% if allowed_to_edit %}hover:bg-gray-100 dark:hover:bg-primary-950{% endif %}">
|
||||
<span>
|
||||
<strong>
|
||||
{% if role.formatted_name %}
|
||||
{{ role.formatted_name }}
|
||||
{% else %}
|
||||
{{ role.name }}
|
||||
{% endif %}
|
||||
</strong>
|
||||
<br />
|
||||
<small>{{ role.desc }}</small>
|
||||
</span>
|
||||
{% if allowed_to_edit %}
|
||||
<a href="/admin/user/{{ user.id }}/remove-role/{{ role.id }}"
|
||||
onclick="return confirm('Willst du die Rolle \'{{ role.name }}\' von {{ user.name }} wirklich entfernen?');">🗑️</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% if allowed_to_edit %}
|
||||
<div class="m-3">
|
||||
<button type="button"
|
||||
onclick="document.getElementById('role-modal').showModal()"
|
||||
class="btn btn-primary w-full">Rolle hinzufügen</button>
|
||||
</div>
|
||||
<dialog id="role-modal"
|
||||
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
|
||||
onclick="document.getElementById('role-modal').close()">
|
||||
<div onclick="event.stopPropagation();" class="p-3">
|
||||
<button type="button"
|
||||
onclick="document.getElementById('role-modal').close()"
|
||||
title="Schließen"
|
||||
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
|
||||
<svg class="inline h-5 w-5"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="mt-8">
|
||||
<form action="/admin/user/{{ user.id }}/add-role"
|
||||
method="post"
|
||||
class="grid gap-3">
|
||||
<div>
|
||||
<label for="role_id" class="text-sm text-gray-600 dark:text-gray-100">Rollen</label>
|
||||
<select name="role_id" id="role_id" class="input rounded-md ">
|
||||
{% for role in roles %}
|
||||
{% if not role.cluster and role not in user.proper_roles and not role.hide_in_lists %}
|
||||
<option value="{{ role.id }}">
|
||||
{% if role.formatted_name %}
|
||||
{{ role.formatted_name }}
|
||||
{% else %}
|
||||
{{ role.name }}
|
||||
{% endif %}
|
||||
</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<input value="Rolle hinzufügen" type="submit" class="btn btn-primary" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if supposed_to_pay %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow">
|
||||
<h2 class="h2">💸-Beitrag</h2>
|
||||
<div class="mx-3 divide-y divide-gray-200 dark:divide-primary-600">
|
||||
<div class="py-3">
|
||||
{% if fee %}
|
||||
<div>
|
||||
<strong>{{ fee.name }}</strong>
|
||||
<span class="block">{{ fee.sum_in_cents / 100 }}€</span>
|
||||
</div>
|
||||
<div>
|
||||
{% for p in fee.parts %}
|
||||
{{ p.0 }} ({{ p.1 / 100 }}€)
|
||||
{% if not loop.last %}+{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if "paid" in user.roles %}
|
||||
✅ bezahlt
|
||||
{% else %}
|
||||
❌ Zahlung ausständig
|
||||
{% endif %}
|
||||
{% else %}
|
||||
{% if "paid" in user.roles %}
|
||||
✅
|
||||
{% for key, value in member %}
|
||||
{% if loop.first %}{{ key }}{% endif %}
|
||||
{% endfor %}
|
||||
hat schon bezahlt
|
||||
{% else %}
|
||||
❌
|
||||
{% for key, value in member %}
|
||||
{% if loop.first %}{{ key }}{% endif %}
|
||||
{% endfor %}
|
||||
hat noch nicht bezahlt
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow">
|
||||
<h2 class="h2">Aktivitäten</h2>
|
||||
<div class="mx-3 max-h-60 overflow-y-scroll">
|
||||
<div class="py-3">
|
||||
<ul class="list-disc ms-4">
|
||||
{% for activity in activities %}
|
||||
<li>
|
||||
<strong>{{ activity.created_at | date(format="%d. %m. %Y") }}:</strong> <small>{{ activity.text }}
|
||||
{% if activity.keep_until_days %}(⏳ {{ activity.keep_until_days }} Tage){% endif %}
|
||||
</small>
|
||||
</li>
|
||||
{% else %}
|
||||
<li>Noch keine Aktivität... Stay tuned 😆</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow">
|
||||
<h2 class="h2">Ergo-Challenge</h2>
|
||||
<div class="mx-3">
|
||||
<div class="grid gap-3 pb-3 mt-3">
|
||||
{{ macros::inputgroup(label='DOB', name='dob', type="text", value=user.dob, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::inputgroup(label='Weight (kg)', name='weight', type="text", value=user.weight, readonly=allowed_to_edit == false) }}
|
||||
{{ macros::inputgroup(label='Sex', name='sex', type="text", value=user.sex, readonly=allowed_to_edit == false) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -4,15 +4,16 @@
|
||||
{% extends "base" %}
|
||||
{% macro show_place(aisle_name, side_name, level) %}
|
||||
<li class="truncate p-2 flex relative w-full">
|
||||
{% set place = boathouse[aisle_name][side_name].boats %}
|
||||
{% set aisle = aisle_name ~ "-aisle" %}
|
||||
{% set place = boathouse[aisle][side_name] %}
|
||||
{% if place[level] %}
|
||||
{{ place[level].boat.name }}
|
||||
{% if allowed_to_edit %}
|
||||
{{ place[level].1.name }}
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<a class="btn btn-primary absolute end-0"
|
||||
href="/board/boathouse/{{ place[level].boathouse_id }}/delete">X</a>
|
||||
href="/board/boathouse/{{ place[level].0 }}/delete">X</a>
|
||||
{% endif %}
|
||||
{% elif boats | length > 0 %}
|
||||
{% if allowed_to_edit %}
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<details>
|
||||
<summary>Kein Boot</summary>
|
||||
<form action="/board/boathouse" method="post" class="grid gap-3">
|
||||
|
@ -56,7 +56,7 @@
|
||||
{% if boatdamage.fixed_at %}
|
||||
<small class="block text-gray-600 dark:text-gray-100">Repariert von {{ boatdamage.user_fixed.name }} am/um {{ boatdamage.fixed_at | date(format='%d.%m.%Y (%H:%M)') }}</small>
|
||||
{% else %}
|
||||
{% if loggedin_user and loggedin_user.allowed_to_steer %}
|
||||
{% if loggedin_user and "cox" in loggedin_user.roles %}
|
||||
<form action="/boatdamage/{{ boatdamage.id }}/fixed"
|
||||
method="post"
|
||||
class="flex justify-between mt-3">
|
||||
|
39
templates/ergo.final.html.tera
Normal file
39
templates/ergo.final.html.tera
Normal file
@ -0,0 +1,39 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Aktuelle Woche</h1>
|
||||
<details>
|
||||
<summary>Dirty Thirty</summary>
|
||||
<p>
|
||||
<div class="border-r border-l">
|
||||
{% for stat in thirty %}
|
||||
{% set names = stat.name | split(pat=" ") %}{% set lastname_index = names | length - 1 %}{% set lastname = names[lastname_index] %}{{ lastname }}	
|
||||
{% for name in names %}
|
||||
{% if loop.index != lastname_index +1 %}{{ name }}{% endif %}
|
||||
{% endfor %}
|
||||
	{{ stat.dob }}	{{ stat.weight }}	{{ stat.sex }}		DLI	{{ stat.result }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Dirty Dozen</summary>
|
||||
<p>
|
||||
<div class="border-r border-l">
|
||||
{% for stat in dozen %}
|
||||
{% set names = stat.name | split(pat=" ") %}
|
||||
{% set lastname_index = names | length - 1 %}
|
||||
{% set lastname = names[lastname_index] %}
|
||||
{{ lastname }};
|
||||
{% for name in names %}
|
||||
{% if loop.index != lastname_index +1 %}{{ name }}{% endif %}
|
||||
{% endfor %}
|
||||
;{{ stat.dob }};{{ stat.weight }};{{ stat.sex }};DLI;{{ stat.result }}
|
||||
<br />
|
||||
{% endfor %}
|
||||
</div>
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
{% endblock content %}
|
205
templates/ergo.html.tera
Normal file
205
templates/ergo.html.tera
Normal file
@ -0,0 +1,205 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Ergo Challenges</h1>
|
||||
<div class="grid gap-3">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Ergo-Challenge?!</h2>
|
||||
<div class="text-sm p-3">
|
||||
<ul class="list-disc ms-2">
|
||||
<li class="py-1">
|
||||
<a href="https://rudernlinz.at/termin"
|
||||
target="_blank"
|
||||
class="link-primary">Überblick der Challenges</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
Eintragung ist jederzeit möglich, alle Daten die bis Sonntag 23:59 hier hochgeladen wurden, werden gesammelt an die Ister Ergo Challenge geschickt
|
||||
<li class="py-1">
|
||||
Dienstag + Donnerstag → gemeinsames Training; bitte um <a href="/" class="link-primary">Anmeldung</a>, damit jeder einen Ergo hat
|
||||
</li>
|
||||
<li class="py-1">
|
||||
Offizielle Ergebnisse: <a href="https://rudernlinz.at/dt"
|
||||
target="_blank"
|
||||
style="text-decoration: underline">Dirty Thirty (rudernlinz.at/dt)</a> / <a href="https://rudernlinz.at/dd"
|
||||
target="_blank"
|
||||
style="text-decoration: underline">Dirty Dozen (rudernlinz.at/dd)</a>, bei Fehlern direkt mit <a href="mailto:office@ergochallenge.at"
|
||||
style="text-decoration: underline">Christian (Ister)</a> Kontakt aufnehmen
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="https://cloud.rudernlinz.at/s/m7mPQdwSWscpaXT"
|
||||
target="_blank"
|
||||
class="link-primary">Noch mehr Infos zur Ergo-Challenge findest du hier</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<details class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md p-2">
|
||||
<summary class="cursor-pointer">Deine Daten</summary>
|
||||
<div class="pt-3">
|
||||
<p>
|
||||
Folgende Daten hat der Ruderassistent von dir. Wenn diese nicht mehr aktuell sind, bitte gewünschte Änderungen an Philipp melden (Tel. nr siehe Signal, oder an <a href="mailto:it@rudernlinz.at"
|
||||
class="text-primary-600 dark:text-primary-200 hover:text-primary-950 hover:dark:text-primary-300 underline"
|
||||
target="_blank">it@rudernlinz.at</a>).
|
||||
<br />
|
||||
<br />
|
||||
<ul>
|
||||
<li>Geburtsdatum: {{ loggedin_user.dob }}</li>
|
||||
<li>Gewicht: {{ loggedin_user.weight }} kg</li>
|
||||
<li>Geschlecht: {{ loggedin_user.sex }}</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3">
|
||||
<h2 class="h2">
|
||||
Neuer Eintrag
|
||||
</h1>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">Dirty Thirty</summary>
|
||||
<div class="mt-3">
|
||||
<form action="/ergo/thirty"
|
||||
class="grid gap-3"
|
||||
method="post"
|
||||
enctype="multipart/form-data">
|
||||
<div>
|
||||
<label for="user-thirty" class="text-sm text-gray-600 dark:text-gray-100">Ergo-Fahrer</label>
|
||||
<select name="user" id="user-thirty" class="input rounded-md">
|
||||
<option disabled="disabled">User auswählen</option>
|
||||
{% for user in users %}
|
||||
{% if user.id == loggedin_user.id %}
|
||||
<option value="{{ user.id }}" selected="selected">{{ user.name }}</option>
|
||||
{% else %}
|
||||
<option value="{{ user.id }}">{{ user.name }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{{ macros::input(label="Distanz [m]", name="result", required=true, type="number", class="input rounded-md") }}
|
||||
<div>
|
||||
<label for="file-thirty" class="text-sm text-gray-600 dark:text-gray-100">Ergebnis-Foto vom Ergo-Display</label>
|
||||
<input type="file"
|
||||
id="file-thirty"
|
||||
name="proof"
|
||||
class="input rounded-md"
|
||||
accept="image/*">
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<input type="submit" value="Speichern" class="btn btn-primary btn-fw m-auto" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">Dirty Dozen</summary>
|
||||
<div class="mt-3">
|
||||
<form action="/ergo/dozen"
|
||||
class="grid gap-3"
|
||||
method="post"
|
||||
enctype="multipart/form-data">
|
||||
<div>
|
||||
<label for="user-dozen" class="text-sm text-gray-600 dark:text-gray-100">Ergo-Fahrer</label>
|
||||
<select name="user" id="user-dozen" class="input rounded-md">
|
||||
<option disabled="disabled">User auswählen</option>
|
||||
{% for user in users %}
|
||||
{% if user.id == loggedin_user.id %}
|
||||
<option value="{{ user.id }}" selected="selected">{{ user.name }}</option>
|
||||
{% else %}
|
||||
<option value="{{ user.id }}">{{ user.name }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{{ macros::input(label="Zeit [hh:mm:ss.s] oder Distanz [m]", name="result", required=true, type="text", class="input rounded-md", pattern="(?:\d+:\d{2}:\d{2}\.\d+|\d{1,2}:\d{2}\.\d+|\d+(\.\d+)?)") }}
|
||||
<div>
|
||||
<label for="file-dozen" class="text-sm text-gray-600 dark:text-gray-100">Ergebnis-Foto vom Ergo-Display</label>
|
||||
<input type="file"
|
||||
id="file-dozen"
|
||||
name="proof"
|
||||
class="input rounded-md"
|
||||
accept="image/*">
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<input type="submit" value="Speichern" class="btn btn-primary btn-fw m-auto" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3">
|
||||
<h2 class="h2">Aktuelle Woche</h2>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">
|
||||
Dirty Thirty <small class="text-gray-600 dark:text-white">({{ thirty | length }})</small>
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
<ol>
|
||||
{% for stat in thirty %}
|
||||
<li>
|
||||
<strong>{{ stat.name }}:</strong> {{ stat.result }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">
|
||||
Dirty Dozen <small class="text-gray-600 dark:text-white">({{ dozen | length }})</small>
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
<ol>
|
||||
{% for stat in dozen %}
|
||||
<li>
|
||||
<strong>{{ stat.name }}:</strong> {{ stat.result }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3">
|
||||
<h2 class="h2">Update</h2>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">
|
||||
Dirty Thirty <small class="text-gray-600 dark:text-white">({{ thirty | length }})</small>
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
<ol>
|
||||
{% for stat in thirty %}
|
||||
<li>
|
||||
<form action="/ergo/thirty/user/{{ stat.id }}/new" method="get">
|
||||
{{ stat.name }}:
|
||||
<input type="text" value="{{ stat.result }}" name="new" style="color: black" />
|
||||
<input type="submit" />
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">
|
||||
Dirty Dozen <small class="text-gray-600 dark:text-white">({{ dozen | length }})</small>
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
<ol>
|
||||
{% for stat in dozen %}
|
||||
<li>
|
||||
<form action="/ergo/dozen/user/{{ stat.id }}/new" method="get">
|
||||
{{ stat.name }}:
|
||||
<input type="text" value="{{ stat.result }}" name="new" style="color: black" />
|
||||
<input type="submit" />
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -1,39 +0,0 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Aktuelle Woche</h1>
|
||||
<details>
|
||||
<summary>Dirty Thirty</summary>
|
||||
<p>
|
||||
<div class="border-r border-l">
|
||||
<textarea style="width: 100%; height: 300px;">
|
||||
{%- for stat in thirty %}
|
||||
{%- set names = stat.name | split(pat=" ") %}{% set lastname_index = names | length - 1 %}{% set lastname = names[lastname_index] %}{{ lastname }}	
|
||||
{%- for name in names -%}
|
||||
{% if loop.index != lastname_index +1 %}{{ name }}{% endif %}
|
||||
{%- endfor -%}
|
||||
	{{ stat.dob }}	{{ stat.weight }}	{{ stat.sex }}		DLI	{{ stat.result }}
|
||||
{%- endfor -%}
|
||||
</textarea>
|
||||
</div>
|
||||
</p>
|
||||
</details>
|
||||
<details>
|
||||
<summary>Dirty Dozen</summary>
|
||||
<p>
|
||||
<div class="border-r border-l">
|
||||
<textarea style="width: 100%; height: 300px;">
|
||||
{%- for stat in dozen -%}
|
||||
{%- set names = stat.name | split(pat=" ") %}{% set lastname_index = names | length - 1 %}{% set lastname = names[lastname_index] %}{{ lastname }}	
|
||||
{%- for name in names -%}
|
||||
{% if loop.index != lastname_index +1 %}{{ name }}{% endif %}
|
||||
{%- endfor -%}
|
||||
	{{ stat.dob }}	{{ stat.weight }}	{{ stat.sex }}		DLI	{{ stat.result }}
|
||||
{%- endfor -%}
|
||||
</textarea>
|
||||
</div>
|
||||
</p>
|
||||
</details>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -1,241 +0,0 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">Ergo Challenges</h1>
|
||||
<div class="grid gap-3">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Ergo Challenge?!</h2>
|
||||
<div class="p-3">
|
||||
<ul class="list-disc ms-2">
|
||||
<li class="py-1">
|
||||
<a href="https://rudernlinz.at/termin"
|
||||
target="_blank"
|
||||
class="link-primary">Überblick der Challenges</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
Eintragung ist jederzeit möglich, alle Daten die bis Sonntag 23:59 hier hochgeladen wurden, werden gesammelt an die Ister Ergo Challenge geschickt
|
||||
<li class="py-1">
|
||||
Montag → gemeinsames Training; bitte um <a href="/planned" class="link-primary">Anmeldung</a>, damit jeder einen Ergo hat
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="https://data.ergochallenge.at"
|
||||
target="_blank"
|
||||
style="text-decoration: underline">Offizielle Ergebnisse</a>, bei Fehlern direkt mit <a href="mailto:office@ergochallenge.at"
|
||||
style="text-decoration: underline">Christian (Ister)</a> Kontakt aufnehmen
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<details class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md p-2">
|
||||
<summary class="cursor-pointer">
|
||||
<strong>Um was geht es bei den Ergochallenges?</strong>
|
||||
</summary>
|
||||
<p class="py-2 ">
|
||||
Der Linzer Verein Ister veranstaltet seit einigen Jahren zwei Challenges im Winter, ‘Dirty Thirty’ (6x im Winter) und ‘Dirty Dozen’ (12 Wochen lang).
|
||||
<ul class="list-decimal ms-4">
|
||||
<li class="py-1">
|
||||
Bei <strong>Dirty Thirty</strong> geht es darum so viele Kilometer wie möglich in 30 Minuten zu fahren.
|
||||
</li>
|
||||
<li class="py-1">
|
||||
Bei <strong>Dirty Dozen</strong> werden jede Woche neue Ziele ausgeschrieben, gestartet wird mit einem Halbmarathon und es geht runter bis auf 100m.
|
||||
</li>
|
||||
</ul>
|
||||
<p class="py-2">
|
||||
Ihr könnt gerne bei allen Challenges mitmachen und es ist möglich jederzeit ein- bzw. auszusteigen. Für alle komplett neuen Teilnehmer würde ich allerdings empfehlen die ersten beiden Dirty Dozen Challenges (Halbmarathon und 16 Kilometer) auszulassen und es am Anfang etwas ruhiger anzugehen. Es steht der Spaß und die Festigung der Technik im Vordergrund, nicht Rekorde.
|
||||
</p>
|
||||
<strong>Video Tipps 🐞</strong>
|
||||
<ul class="list-disc ms-3">
|
||||
<li class="py-1">
|
||||
<a href="https://www.youtube.com/watch?v=TJsQPV6LNPI"
|
||||
target="_blank"
|
||||
style="text-decoration: underline">Intro</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="https://www.youtube.com/watch?v=VE663Kg0c00"
|
||||
target="_blank"
|
||||
style="text-decoration: underline">Grundlagen</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="https://www.youtube.com/watch?v=KOacKLOpWkI"
|
||||
target="_blank"
|
||||
style="text-decoration: underline">Schlagaufbau</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="https://www.youtube.com/watch?v=m6VP11EDjcM"
|
||||
target="_blank"
|
||||
style="text-decoration: underline">PM5 Monitor</a>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
<details class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md p-2">
|
||||
<summary class="cursor-pointer">
|
||||
<strong>Deine Daten</strong>
|
||||
</summary>
|
||||
<div class="pt-3">
|
||||
<p>
|
||||
Folgende Daten hat der Ruderassistent von dir. Wenn diese nicht mehr aktuell sind, bitte gewünschte Änderungen an unseren Schriftführer melden (<a href="mailto:info@rudernlinz.at"
|
||||
class="text-primary-600 dark:text-primary-200 hover:text-primary-950 hover:dark:text-primary-300 underline"
|
||||
target="_blank">it@rudernlinz.at</a>).
|
||||
<br />
|
||||
<br />
|
||||
<ul>
|
||||
<li>Geburtsdatum: {{ loggedin_user.dob }}</li>
|
||||
<li>Gewicht: {{ loggedin_user.weight }} kg</li>
|
||||
<li>Geschlecht: {{ loggedin_user.sex }}</li>
|
||||
</ul>
|
||||
</p>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3">
|
||||
<h2 class="h2">
|
||||
Neuer Eintrag
|
||||
</h1>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">Dirty Thirty</summary>
|
||||
<div class="mt-3">
|
||||
<form action="/ergo/thirty"
|
||||
class="grid gap-3"
|
||||
method="post"
|
||||
enctype="multipart/form-data">
|
||||
<div>
|
||||
<label for="user-thirty" class="text-sm text-gray-600 dark:text-gray-100">Ergo-Fahrer</label>
|
||||
<select name="user" id="user-thirty" class="input rounded-md">
|
||||
<option disabled="disabled">User auswählen</option>
|
||||
{% for user in users %}
|
||||
{% if user.id == loggedin_user.id %}
|
||||
<option value="{{ user.id }}" selected="selected">{{ user.name }}</option>
|
||||
{% else %}
|
||||
<option value="{{ user.id }}">{{ user.name }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{{ macros::input(label="Distanz [m]", name="result", required=true, type="number", class="input rounded-md") }}
|
||||
<div>
|
||||
<label for="file-thirty" class="text-sm text-gray-600 dark:text-gray-100">Ergebnis-Foto vom Ergo-Display</label>
|
||||
<input type="file"
|
||||
id="file-thirty"
|
||||
name="proof"
|
||||
class="input rounded-md"
|
||||
accept="image/*">
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<input type="submit" value="Speichern" class="btn btn-primary btn-fw m-auto" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">Dirty Dozen</summary>
|
||||
<div class="mt-3">
|
||||
<form action="/ergo/dozen"
|
||||
class="grid gap-3"
|
||||
method="post"
|
||||
enctype="multipart/form-data">
|
||||
<div>
|
||||
<label for="user-dozen" class="text-sm text-gray-600 dark:text-gray-100">Ergo-Fahrer</label>
|
||||
<select name="user" id="user-dozen" class="input rounded-md">
|
||||
<option disabled="disabled">User auswählen</option>
|
||||
{% for user in users %}
|
||||
{% if user.id == loggedin_user.id %}
|
||||
<option value="{{ user.id }}" selected="selected">{{ user.name }}</option>
|
||||
{% else %}
|
||||
<option value="{{ user.id }}">{{ user.name }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{{ macros::input(label="Zeit [hh:mm:ss.s] oder Distanz [m]", name="result", required=true, type="text", class="input rounded-md", pattern="(?:\d+:\d{2}:\d{2}\.\d+|\d{1,2}:\d{2}\.\d+|\d+(\.\d+)?)") }}
|
||||
<div>
|
||||
<label for="file-dozen" class="text-sm text-gray-600 dark:text-gray-100">Ergebnis-Foto vom Ergo-Display</label>
|
||||
<input type="file"
|
||||
id="file-dozen"
|
||||
name="proof"
|
||||
class="input rounded-md"
|
||||
accept="image/*">
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<input type="submit" value="Speichern" class="btn btn-primary btn-fw m-auto" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3">
|
||||
<h2 class="h2">Aktuelle Woche</h2>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">
|
||||
Dirty Thirty <small class="text-gray-600 dark:text-white">({{ thirty | length }})</small>
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
<ol>
|
||||
{% for stat in thirty %}
|
||||
<li>
|
||||
<strong>{{ stat.name }}:</strong> {{ stat.result }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">
|
||||
Dirty Dozen <small class="text-gray-600 dark:text-white">({{ dozen | length }})</small>
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
<ol>
|
||||
{% for stat in dozen %}
|
||||
<li>
|
||||
<strong>{{ stat.name }}:</strong> {{ stat.result }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow grid gap-3">
|
||||
<h2 class="h2">Update</h2>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">
|
||||
Dirty Thirty <small class="text-gray-600 dark:text-white">({{ thirty | length }})</small>
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
<ol>
|
||||
{% for stat in thirty %}
|
||||
<li>
|
||||
<form action="/ergo/thirty/user/{{ stat.id }}/new" method="get">
|
||||
{{ stat.name }}:
|
||||
<input type="text" value="{{ stat.result }}" name="new" style="color: black" />
|
||||
<input type="submit" />
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
<details class="p-2">
|
||||
<summary class="cursor-pointer">
|
||||
Dirty Dozen <small class="text-gray-600 dark:text-white">({{ dozen | length }})</small>
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
<ol>
|
||||
{% for stat in dozen %}
|
||||
<li>
|
||||
<form action="/ergo/dozen/user/{{ stat.id }}/new" method="get">
|
||||
{{ stat.name }}:
|
||||
<input type="text" value="{{ stat.result }}" name="new" style="color: black" />
|
||||
<input type="submit" />
|
||||
</form>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -1,37 +0,0 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% import "includes/forms/boat" as boat %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full dark:text-white">
|
||||
<h1 class="h1">Ergo Challenge</h1>
|
||||
<div class="grid ">
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<p class="px-3 pt-3">
|
||||
Schön, dass du heuer bei der Ergo Challenge mitmachen willst!
|
||||
Dafür benötigen wir 3 Daten: Geburtsjahr, Gewicht und Geschlecht.
|
||||
{% if loggedin_user.weight %}Wir haben von dir schon Daten, bitte überprüfe (und aktualisiere) diese kurz:{% endif %}
|
||||
</p>
|
||||
<form action="/ergo/set-data" method="post" class="grid gap-3 p-3">
|
||||
{{ macros::input(label="Geburtsjahr [YYYY]", name="birthyear", required=true, type="number", class="input rounded-md", value=loggedin_user.dob) }}
|
||||
{{ macros::input(label="Gewicht [kg]", name="weight", required=true, type="number", class="input rounded-md", value=loggedin_user.weight) }}
|
||||
<div>
|
||||
<label for="sex" class="text-sm text-gray-600 dark:text-gray-100">Geschlecht</label>
|
||||
<select name="sex" id="sex" class="input rounded-md" required>
|
||||
<option disabled="disabled"
|
||||
{% if loggedin_user.sex != 'f' and loggedin_user.sex != 'm' %}selected="selected"{% endif %}>
|
||||
Geschlecht auswählen
|
||||
</option>
|
||||
<option value="f"
|
||||
{% if loggedin_user.sex == 'f' %}selected="selected"{% endif %}>weiblich</option>
|
||||
<option value="m"
|
||||
{% if loggedin_user.sex == 'm' %}selected="selected"{% endif %}>männlich</option>
|
||||
</select>
|
||||
<small class="block py-1">Du fühlst dich beim Geschlecht nicht angesprochen? Dann melde dich bitte direkt beim Ergo-Christian, Kontaktmöglichkeit auf der nächsten Seite.</small>
|
||||
</div>
|
||||
<input type="submit" class="btn btn-primary" value="Abschicken" />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
@ -6,11 +6,7 @@
|
||||
{{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='max_people', type='number', required=true, min='0') }}
|
||||
{{ macros::checkbox(label='Scheckbuch-Anmeldungen erlauben', name='allow_guests') }}
|
||||
{{ macros::input(label='Anmerkungen', name='notes', type='input') }}
|
||||
{% if loggedin_user.allowed_to_steer %}
|
||||
{{ macros::select(label='Typ', data=trip_types, name='trip_type', default='Reguläre Ausfahrt') }}
|
||||
{% else %}
|
||||
{{ macros::select(label='Typ', data=trip_types, name='trip_type', only_ergo=true) }}
|
||||
{% endif %}
|
||||
{{ macros::select(label='Typ', data=trip_types, name='trip_type', default='Reguläre Ausfahrt') }}
|
||||
<input value="Erstellen" class="w-full btn btn-primary" type="submit" />
|
||||
</form>
|
||||
</div>
|
||||
|
@ -1,4 +1,4 @@
|
||||
{% if loggedin_user.allowed_to_steer %}
|
||||
{% if "cox" in loggedin_user.roles %}
|
||||
<div class="sm:col-span-2 lg:col-span-3 grid md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-3">
|
||||
<button type="button"
|
||||
title="Toggle View"
|
||||
|
@ -82,13 +82,7 @@
|
||||
<label for="{{ id }}" class="text-sm text-gray-600 dark:text-gray-100">
|
||||
Ruderer (inkl. Schiffsführer und Steuerperson)
|
||||
</label>
|
||||
<select style="width: 100%"
|
||||
multiple
|
||||
name="rowers[]"
|
||||
id="{{ id }}"
|
||||
class="w-full"
|
||||
data-seats="{{ amount_seats }}"
|
||||
data-init="{{ init }}">
|
||||
<select style="width: 100%;" multiple name="rowers[]" id="{{ id }}" class="w-full" data-seats="{{ amount_seats }}" data-init={{ init }}>
|
||||
{% for user in users %}
|
||||
{% set_global sel = false %}
|
||||
{% for rower in selected %}
|
||||
@ -99,7 +93,7 @@
|
||||
<option value="{{ user.id }}"
|
||||
{% if sel %}selected{% endif %}
|
||||
{% if user.on_water %}disabled="disabled"{% endif %}
|
||||
data-custom-properties='{"is_cox": {{ user.allowed_to_steer }}, "is_racing": {{ "Rennrudern" in user.roles }}, "steers": {{ user.id == steering_person_id }}, "cox_on_boat": {{ user.id == cox_on_boat }}, "searchableText": "{{ user.nickname }}"}'>
|
||||
data-custom-properties='{"is_cox": {{ "cox" in user.roles }}, "is_racing": {{ "Rennrudern" in user.roles }}, "steers": {{ user.id == steering_person_id }}, "cox_on_boat": {{ user.id == cox_on_boat }}, "searchableText": "{{ user.nickname }}"}'>
|
||||
{{ user.name }}
|
||||
{% if user.on_water %}(am Wasser){% endif %}
|
||||
</option>
|
||||
@ -183,131 +177,103 @@
|
||||
<div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative"
|
||||
data-filterable="true"
|
||||
data-filter="{{ log.boat.name }} {% for rower in log.rowers %}{{ rower.name }}{% endfor %}">
|
||||
{% if log.logtype and not hide_type %}
|
||||
<div class="absolute top-0 right-0 bg-primary-100 rounded-bl-md text-primary-950 text-xs w-32 px-2 py-1 text-center font-bold">
|
||||
{% if log.logtype == 1 %}
|
||||
Wanderfahrt
|
||||
{% else %}
|
||||
{% if log.logtype == 2 %}
|
||||
Regatta
|
||||
{% else %}
|
||||
{{ log.logtype }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div {% if log.logtype %}class="mt-4 sm:mt-0"{% endif %}>
|
||||
{% if allowed_to_edit %}
|
||||
<a href="#"
|
||||
onclick="document.getElementById('change-{{ log.id }}').showModal()"
|
||||
class="link link-black font-bold">{{ log.boat.name }}</a>
|
||||
{% else %}
|
||||
<strong class="text-black dark:text-white">{{ log.boat.name }}</strong>
|
||||
{% endif %}
|
||||
<small class="text-gray-600 dark:text-gray-100">({{ log.shipmaster_user.name -}}
|
||||
{% if log.shipmaster_only_steering %}
|
||||
- handgesteuert
|
||||
{%- endif -%}
|
||||
)</small>
|
||||
<small class="block text-gray-600 dark:text-gray-100">
|
||||
{% if state == "completed" and log.departure | date(format='%d.%m.%Y') == log.arrival | date(format='%d.%m.%Y') %}
|
||||
{{ log.departure | date(format='%d.%m.%Y') }}
|
||||
({{ log.departure | date(format='%H:%M') }}
|
||||
-
|
||||
{{ log.arrival | date(format='%H:%M') }})
|
||||
{% else %}
|
||||
{{ log.departure | date(format='%d.%m.%Y (%H:%M)') }}
|
||||
{% if state == "completed" %}
|
||||
-
|
||||
{{ log.arrival | date(format='%d.%m.%Y (%H:%M)') }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</small>
|
||||
{% set amount_rowers = log.rowers | length %}
|
||||
{% set amount_guests = log.boat.amount_seats - amount_rowers %}
|
||||
{% if allowed_to_close and state == "on_water" %}
|
||||
{{ log::home(log=log) }}
|
||||
{% else %}
|
||||
<div class="text-black dark:text-white">
|
||||
{{ log.destination }}
|
||||
{% if state == "completed" %}
|
||||
<small class="text-gray-600 dark:text-gray-100">({{ log.distance_in_km }}
|
||||
km)</small>
|
||||
{% endif %}
|
||||
{% if log.comments %}<span class="text-sm italic">- "{{ log.comments }}"</span>{% endif %}
|
||||
</div>
|
||||
{% if amount_guests > 0 or log.rowers | length > 0 %}
|
||||
{% if not log.boat.amount_seats == 1 %}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-100">
|
||||
Ruderer:
|
||||
{% for rower in log.rowers -%}
|
||||
{{ rower.name }}
|
||||
{%- if rower.id == log.steering_user.id and rower.id != log.shipmaster_user.id %}
|
||||
(Steuerperson){%- endif -%}
|
||||
{%- if not loop.last or amount_guests > 0 and not log.boat.external %},{% endif %}
|
||||
{% endfor -%}
|
||||
{% if amount_guests > 0 and not log.boat.external %}
|
||||
Gäste
|
||||
<small class="text-gray-600 dark:text-gray-100">(ohne Account)</small>:
|
||||
{{ amount_guests }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<details>
|
||||
<summary style="list-style: none;">
|
||||
{% if log.logtype and not hide_type %}
|
||||
<div class="absolute top-0 right-0 bg-primary-100 rounded-bl-md text-primary-950 text-xs w-32 px-2 py-1 text-center font-bold">
|
||||
{% if log.logtype == 1 %}
|
||||
Wanderfahrt
|
||||
{% else %}
|
||||
{% if log.logtype == 2 %}
|
||||
Regatta
|
||||
{% else %}
|
||||
{{ log.logtype }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if allowed_to_edit %}
|
||||
<dialog id="change-{{ log.id }}"
|
||||
class="max-w-screen-sm w-full dark:bg-primary-900 dark:text-white rounded-md"
|
||||
onclick="document.getElementById('change-{{ log.id }}').close()">
|
||||
<div onclick="event.stopPropagation();" class="p-3">
|
||||
<button type="button"
|
||||
onclick="document.getElementById('change-{{ log.id }}').close()"
|
||||
title="Schließen"
|
||||
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
|
||||
<svg class="inline h-5 w-5"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="mt-8">
|
||||
<h2 class="h3">Eintrag '{{ log.boat.name }}' ändern</h2>
|
||||
<p class="text-center mb-3">{{ log.id }}</p>
|
||||
<form action="/log/update" method="post" class="grid gap-3">
|
||||
<input type="hidden" name="id" value="{{ log.id }}" />
|
||||
<input type="hidden" name="boat_id" value="{{ log.boat_id }}" />
|
||||
<input type="hidden" name="shipmaster" value="{{ log.shipmaster }}" />
|
||||
<input type="hidden"
|
||||
name="steering_person"
|
||||
value="{{ log.steering_person }}" />
|
||||
{{ macros::checkbox(label='Handgesteuert', name='shipmaster_only_steering', id=log.shipmaster_only_steering,checked=log.shipmaster_only_steering) }}
|
||||
<input type="datetime-local"
|
||||
class="input rounded-md"
|
||||
name="departure"
|
||||
value="{{ log.departure }}" />
|
||||
<input type="datetime-local"
|
||||
class="input rounded-md"
|
||||
name="arrival"
|
||||
value="{{ log.arrival }}" />
|
||||
<input type="hidden" name="destination" value="{{ log.destination }}" />
|
||||
<input type="hidden" name="distance_in_km" value="{{ log.distance_in_km }}" />
|
||||
<input type="hidden" name="comments" value="{{ log.comments }}" />
|
||||
<input type="hidden" name="logtype" value="{{ log.logtype }}" />
|
||||
<input type="submit" class="btn btn-primary" value="Updaten" />
|
||||
</form>
|
||||
<a href="/log/{{ log.id }}/delete"
|
||||
class="w-28 btn btn-alert mt-3"
|
||||
onclick="return confirm('Willst du diesen Logbucheintrag wirklich löschen?');">
|
||||
{% include "includes/delete-icon" %}
|
||||
Löschen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div {% if log.logtype %}class="mt-4 sm:mt-0"{% endif %}>
|
||||
<strong class="text-black dark:text-white">{{ log.boat.name }}</strong>
|
||||
<small class="text-gray-600 dark:text-gray-100">({{ log.shipmaster_user.name -}}
|
||||
{% if log.shipmaster_only_steering %}
|
||||
- handgesteuert
|
||||
{%- endif -%}
|
||||
)</small>
|
||||
<small class="block text-gray-600 dark:text-gray-100">
|
||||
{% if state == "completed" and log.departure | date(format='%d.%m.%Y') == log.arrival | date(format='%d.%m.%Y') %}
|
||||
{{ log.departure | date(format='%d.%m.%Y') }}
|
||||
({{ log.departure | date(format='%H:%M') }}
|
||||
-
|
||||
{{ log.arrival | date(format='%H:%M') }})
|
||||
{% else %}
|
||||
{{ log.departure | date(format='%d.%m.%Y (%H:%M)') }}
|
||||
{% if state == "completed" %}
|
||||
-
|
||||
{{ log.arrival | date(format='%d.%m.%Y (%H:%M)') }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</small>
|
||||
{% set amount_rowers = log.rowers | length %}
|
||||
{% set amount_guests = log.boat.amount_seats - amount_rowers %}
|
||||
{% if allowed_to_close and state == "on_water" %}
|
||||
{{ log::home(log=log) }}
|
||||
{% else %}
|
||||
<div class="text-black dark:text-white">
|
||||
{{ log.destination }}
|
||||
{% if state == "completed" %}
|
||||
<small class="text-gray-600 dark:text-gray-100">({{ log.distance_in_km }}
|
||||
km)</small>
|
||||
{% endif %}
|
||||
{% if log.comments %}<span class="text-sm italic">- "{{ log.comments }}"</span>{% endif %}
|
||||
</div>
|
||||
{% if amount_guests > 0 or log.rowers | length > 0 %}
|
||||
{% if not log.boat.amount_seats == 1 %}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-100">
|
||||
Ruderer:
|
||||
{% for rower in log.rowers -%}
|
||||
{{ rower.name }}
|
||||
{%- if rower.id == log.steering_user.id and rower.id != log.shipmaster_user.id %}
|
||||
(Steuerperson){%- endif -%}
|
||||
{%- if not loop.last or amount_guests > 0 and not log.boat.external %},{% endif %}
|
||||
{% endfor -%}
|
||||
{% if amount_guests > 0 and not log.boat.external %}
|
||||
Gäste
|
||||
<small class="text-gray-600 dark:text-gray-100">(ohne Account)</small>:
|
||||
{{ amount_guests }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</summary>
|
||||
{% if allowed_to_edit %}
|
||||
<form action="/log/update" method="post">
|
||||
<input type="hidden" name="id" value="{{ log.id }}" />
|
||||
<input type="hidden" name="boat_id" value="{{ log.boat_id }}" />
|
||||
<input type="hidden" name="shipmaster" value="{{ log.shipmaster }}" />
|
||||
<input type="hidden"
|
||||
name="steering_person"
|
||||
value="{{ log.steering_person }}" />
|
||||
<input type="hidden"
|
||||
name="shipmaster_only_steering"
|
||||
value="{{ log.shipmaster_only_steering }}" />
|
||||
<input type="datetime-local" name="departure" value="{{ log.departure }}" />
|
||||
<input type="datetime-local" name="arrival" value="{{ log.arrival }}" />
|
||||
<input type="hidden" name="destination" value="{{ log.destination }}" />
|
||||
<input type="hidden" name="distance_in_km" value="{{ log.distance_in_km }}" />
|
||||
<input type="hidden" name="comments" value="{{ log.comments }}" />
|
||||
<input type="hidden" name="logtype" value="{{ log.logtype }}" />
|
||||
<input type="submit" value="Updaten" />
|
||||
</form>
|
||||
<a href="/log/{{ log.id }}/delete"
|
||||
class="w-28 btn btn-alert"
|
||||
onclick="return confirm('Willst du diesen Logbucheintrag wirklich löschen?');">
|
||||
{% include "includes/delete-icon" %}
|
||||
Löschen
|
||||
</a>
|
||||
{% endif %}
|
||||
</details>
|
||||
</div>
|
||||
{% endmacro show_old %}
|
||||
{% macro home(log) %}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user