Compare commits
117 Commits
3a7cc0e3be
...
update-car
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcaa3730ce | ||
| fbb55dd829 | |||
|
|
908853be9b | ||
| 490df19e96 | |||
|
|
6d50752fa1 | ||
| 6cd2b5f10f | |||
|
|
e8ef82763f | ||
| 9d917663a3 | |||
|
|
b894394049 | ||
| 66ce59fa24 | |||
|
|
c8801d35e3 | ||
| 82e7d56da4 | |||
|
|
bc61631b6b | ||
| fc437d5f8b | |||
|
|
290663476b | ||
| f475fcba9e | |||
|
|
899751b6a2 | ||
| 3f7891c170 | |||
|
|
1d4f42d4fb | ||
| d8472d9d09 | |||
|
|
aa0c40ce48 | ||
| de0f816978 | |||
|
|
07cb8325a7 | ||
| 0dc8be0a8f | |||
|
|
03e558d2ba | ||
| 86c8272f1f | |||
|
|
b37724d3bc | ||
| 7fbb95699b | |||
|
|
0756f5900c | ||
| 74ff8ea996 | |||
|
|
0f89f0d3f5 | ||
| 369a099a8d | |||
|
|
3f76f8d823 | ||
| 5581f2245a | |||
|
|
6b2f8fc005 | ||
| e572f557f8 | |||
|
|
0238815003 | ||
| 44a71ab79f | |||
|
|
22b8dcb6b8 | ||
| 527c82ea12 | |||
|
|
04ad2bac85 | ||
| 2658b8a21c | |||
|
|
eae69e1d0b | ||
| 1128e2fd29 | |||
|
|
bab0a13cf0 | ||
| 275a0e407b | |||
|
|
105060a0f1 | ||
| b0b53a9c69 | |||
|
|
94df20c26f | ||
| fb81866033 | |||
|
|
745cb37a53 | ||
| aa458f53df | |||
|
|
3e976d161b | ||
| c7dd97ef82 | |||
|
|
842871c423 | ||
| e6172ec163 | |||
|
|
12f123c7df | ||
| bcf9baa1f0 | |||
|
|
71bb35d47e | ||
| 6ced525310 | |||
|
|
1cfee40b01 | ||
| ec105d1f4a | |||
|
|
721435dacd | ||
| 9411e366bb | |||
|
|
5b8278c8d8 | ||
| e1069a2160 | |||
|
|
49419c027a | ||
| 73ecfc3494 | |||
|
|
383bd41762 | ||
| 1ede24fd32 | |||
|
|
59ff71dedc | ||
| c0e0fedc52 | |||
| 03c34c5c66 | |||
| 3abb449f22 | |||
| 3a99946787 | |||
|
|
7ebff2628e | ||
| 8d763d92ab | |||
| 0ab98d4ed9 | |||
| 50cc71250a | |||
|
|
97617dabd6 | ||
| bdfa2cc4dc | |||
| c1ad6c443d | |||
| da309e65ad | |||
| ff02048326 | |||
| 0e6eedcb21 | |||
| 0b13c34369 | |||
| 97dbd4fcae | |||
| a46cf6ed97 | |||
| 2a0098b0cb | |||
| 69aed3be27 | |||
| 79d22a0ad1 | |||
| 059976cb98 | |||
| 97c176dbd5 | |||
| f49af30c62 | |||
| e9168e3440 | |||
| 365340f956 | |||
| 1ae879e234 | |||
| 6078161b2c | |||
| 8d023e5dce | |||
| 5503be439c | |||
| bd4ec88ed9 | |||
| b9eef60872 | |||
| 9cfeaa77cd | |||
| 80e16754a3 | |||
| e066f3d065 | |||
| 41311beb72 | |||
| 7500f818a6 | |||
| 42a20c1f73 | |||
| 37114e299a | |||
| 51d3b91dbb | |||
| e575051010 | |||
| c8fde325db | |||
| 934d23ab2f | |||
| f71bcf35c3 | |||
| a73e5259ae | |||
| 07e19c968b | |||
| 8ba69aa074 |
@@ -72,4 +72,5 @@ jobs:
|
||||
ssh $SSH_USER@$SSH_HOST 'mv /home/startest/star-test-updating /home/startest/star-test'
|
||||
ssh $SSH_USER@$SSH_HOST 'rm /home/startest/db.sqlite'
|
||||
scp -C db.sqlite $SSH_USER@$SSH_HOST:/home/startest/db.sqlite
|
||||
ssh $SSH_USER@$SSH_HOST 'sqlite3 /home/startest/db.sqlite "PRAGMA max_page_count = 10000;"'
|
||||
ssh $SSH_USER@$SSH_HOST 'sudo systemctl start startest'
|
||||
|
||||
34
.gitea/workflows/update.yml
Normal file
34
.gitea/workflows/update.yml
Normal file
@@ -0,0 +1,34 @@
|
||||
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
|
||||
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: true
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
/target
|
||||
db.sqlite
|
||||
.history
|
||||
|
||||
1875
Cargo.lock
generated
1875
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,7 @@ dotenv = "0.15"
|
||||
maud = { version = "0.27", features = ["axum"] }
|
||||
serde = "1.0"
|
||||
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls", "macros", "chrono"] }
|
||||
tokio = { version = "1.44", features = ["macros", "rt-multi-thread"] }
|
||||
tokio = { version = "1.50", features = ["macros", "rt-multi-thread"] }
|
||||
tower-sessions = "0.14"
|
||||
rust-i18n = "3"
|
||||
thiserror = "2.0"
|
||||
@@ -29,7 +29,7 @@ zune-inflate = { version = "0.2", default-features = false, features = [
|
||||
"std",
|
||||
] }
|
||||
tar = "0.4"
|
||||
ureq = "3.0"
|
||||
ureq = "3.2"
|
||||
time = "0.3"
|
||||
typst-kit = { version = "0.13", features = ["embed-fonts", "vendor-openssl"] }
|
||||
typst-pdf = "0.13"
|
||||
|
||||
88
README.md
88
README.md
@@ -1,14 +1,84 @@
|
||||
# Stationslauf
|
||||
# STAR (STAtion Run)
|
||||

|
||||
|
||||
## Demo-Instance
|
||||
STAR is a streamlined web application for organizing and managing station-based team events. Perfect for rallies, orienteering events, team-building activities, educational tours, and scavenger hunts.
|
||||
|
||||
## Overview
|
||||
|
||||
STAR is a single-binary application that helps event organizers manage teams as they progress through a series of stations along predefined routes. The app provides:
|
||||
|
||||
- **Real-time tracking** of team movements between stations
|
||||
- **Rating system** for evaluating team performance
|
||||
- **Flexible route planning** with multiple possible paths
|
||||
- **Map integration** for station locations
|
||||
- **QR code generation** for easy station access
|
||||
|
||||
## Key Features
|
||||
|
||||
- **Team Management**: Create teams, assign routes, track progress, and monitor ratings
|
||||
- **Station Control**: Station supervisors can record when teams arrive, start activities, and depart
|
||||
- **Route Planning**: Define custom routes with specific station sequences
|
||||
- **Performance Rating**: Score teams at each station and generate overall rankings
|
||||
- **Admin Dashboard**: Comprehensive overview of event progress and team status
|
||||
- **Multi-language Support**: Currently in German with i18n infrastructure
|
||||
|
||||
## For Station Supervisors
|
||||
|
||||
Station supervisors can:
|
||||
1. Check in teams when they arrive at a station
|
||||
2. Record when teams begin their activities
|
||||
3. Mark when teams complete the station and leave
|
||||
4. Rate team performance and add notes
|
||||
5. View which teams should be arriving next
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Demo Instance
|
||||
|
||||
- [startest.it-results.at](https://startest.it-results.at)
|
||||
- DB resets on every commit/deployment
|
||||
- Default admin: "a"/"123"
|
||||
- Database resets on every commit/deployment
|
||||
- Default admin credentials: "a"/"123"
|
||||
|
||||
## Localization tests
|
||||
To test if there are any errors in the localization string, use the [i18n-checker](https://git.hofer.link/philipp/i18n-checker): e.g. `cargo r -r -- --locale-file /home/ph/p/stationslauf/locales/de-AT.yml --rust-src-to-check /home/ph/p/stationslauf/src`
|
||||
### Development
|
||||
|
||||
## Marketing
|
||||
- single-binary (+ db + .env)
|
||||
- Teams werden automatisch (start)stationen zugewiesen
|
||||
- Install ```inotifywatch``` on your system
|
||||
- Use ```./watch.sh``` for automatic re-compilation upon changes
|
||||
|
||||
### Localization Testing
|
||||
|
||||
To test for errors in localization strings:
|
||||
```bash
|
||||
cargo r -r -- --locale-file /path/to/stationslauf/locales/de-AT.yml --rust-src-to-check /path/to/stationslauf/src
|
||||
```
|
||||
|
||||
## Technical Details
|
||||
|
||||
- **Implementation**: Single-binary application (plus database and .env file)
|
||||
- **Automatic Assignment**: Teams are automatically assigned to start stations to balance workload
|
||||
- **PDF Generation**: Create printable documents with login QR codes for station supervisors
|
||||
- **Maps Integration**: Navigation and station location visualization
|
||||
|
||||
## Project Structure
|
||||
|
||||
- **Teams**: Groups of participants following specific routes
|
||||
- **Stations**: Checkpoints where teams perform activities and get rated
|
||||
- **Routes**: Defined paths connecting stations in specific sequences
|
||||
- **Admins**: Users who can manage the entire system, create entities, and view results
|
||||
|
||||
## License
|
||||
|
||||
STAR is licensed under the European Union Public License (EUPL) 1.2, a free software license. This license allows you to use, modify, and distribute the software. If you distribute modified versions, you must release your changes under a compatible open-source license. The EUPL 1.2 provides compatibility with several other open-source licenses.
|
||||
|
||||
## Hosting options
|
||||
|
||||
You're welcome to self-host STAR on your own servers or infrastructure. This gives you complete control over your deployment, data, and customizations. Should you share your modified version, the EUPL 1.2 simply asks that you contribute your improvements back to the open-source community.
|
||||
|
||||
Alternatively, if you'd prefer a hassle-free experience without managing servers and updates, I can host the application for you. This option provides you with support and maintenance while letting you focus on organizing your event rather than technical details. You can find more information at https://star.it-results.at
|
||||
|
||||
## Contribution
|
||||
|
||||
Got ideas to make STAR even better? I'd love to hear from you! Whether it's a small suggestion, a bug report, or a brilliant new feature concept, please drop me a line at philipp@hofer.link.
|
||||
|
||||
If you're interested in contributing code, documentation, or other improvements directly, I'm happy to set up a Gitea account here to make collaboration smooth and easy. Just reach out and we can get you started.
|
||||
|
||||
Your feedback and contributions help make this project better for everyone—so don't be shy! Let's make STAR the best station-based event management tool together.
|
||||
|
||||
@@ -11,6 +11,12 @@
|
||||
color: #1b5e20;
|
||||
}
|
||||
|
||||
.danger {
|
||||
background-color: #b71c1c;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* no background on leaflet marker */
|
||||
.leaflet-container [role="button"],
|
||||
.leaflet-container [type="button"],
|
||||
@@ -23,6 +29,12 @@
|
||||
box-shadow: unset;
|
||||
}
|
||||
|
||||
.logo,.logo-inv {
|
||||
height: 5em;
|
||||
max-width: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
[data-theme="light"],
|
||||
:root:not([data-theme="dark"]),
|
||||
:host(:not([data-theme="dark"])) {
|
||||
@@ -42,3 +54,59 @@
|
||||
.logo {display:none};
|
||||
.logo-inv {display:block};
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*STICKY TABLE*/
|
||||
.sticky-table {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.sticky-table thead th {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.sticky-table tbody td:first-child,
|
||||
.sticky-table thead th:first-child {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
background-color: var(--pico-background-color); /* Background color for first column */
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
table.sticky-table tbody tr:nth-child(2n+1) td:first-child,
|
||||
table.sticky-table tbody tr:nth-child(2n+1) th:first-child
|
||||
{
|
||||
/* background-color: red; /* Background color for first column */
|
||||
background-color: var(--pico-background-color);
|
||||
|
||||
}
|
||||
|
||||
/* For the top-left cell - needs higher z-index */
|
||||
.sticky-table thead th:first-child {
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.flex-center-between {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
summary.flex-center-between::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ml-1 {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.mr-1 {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.mb-0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -21,7 +21,6 @@ confirm_end_run: "Willst du den Stationslauf wirklich beenden? Jedes Team wird d
|
||||
confirm_restart_run: "Willst du den Stationslauf wirklich wieder aufnehmen?"
|
||||
run_ended: "Stationslauf erfolgreich beendet"
|
||||
run_restarted: "Stationslauf erfolgreich wieder aufgenommen"
|
||||
come_home_with_these_groups: "Gruppen mitnehmen"
|
||||
station_info: "Schön, dass du uns als Stationsbetreuer hilfst."
|
||||
info_crewless_station: "Wenn das eine unbemannte Station ist, wähle hier 0 Personen aus. Dann werden dieser Station keine Startteams zugeteilt und es wird kein PDF generiert. Ansonsten gib die geplante Anzahl an Stationsbetreuern ein."
|
||||
info_currently_crewless_station: "Das ist aktuell eine unbemannte Station. Dieser Station werden keine Startteams zugeteilt und es wird kein Stations-PDF erzeugt. Wenn diese Station doch Stationsbetreuer hat, gib hier dessen Anzahl ein."
|
||||
@@ -29,6 +28,8 @@ info_currently_crewful_station: "Das ist aktuell eine bemannte Station. Dieser S
|
||||
time: "Uhrzeit"
|
||||
google_maps_navigation: "Google Maps Navigation..."
|
||||
highscore: "Highscore"
|
||||
station_should_take_these_teams_home: "Der Stationslauf ist vorbei, die Stationen sollten diese Teams zurück zum Start mitnehmen:"
|
||||
station_should_take_these_teams_to_first_station: "Die Stationen sollen diese Teams zur ersten Station mitnehmen:"
|
||||
|
||||
|
||||
#
|
||||
@@ -53,7 +54,8 @@ button_station_ready: "Sobald du bei deiner Station bist und bereit zu starten b
|
||||
station_not_yet_ready: "Bin mit der Station doch noch nicht bereit..."
|
||||
one_team_should_come_to_station: "Insgesamt sollte 1 Team zu deiner Station kommen."
|
||||
n_teams_should_come_to_station: "Insgesamt sollten %{amount} Teams zu deiner Station kommen."
|
||||
team_on_the_way_to_your_station: "Team %{team} ist seit %{time} auf dem Weg zu deiner Station."
|
||||
team_on_the_way_to_your_station: "Das Team %{team} ist seit %{time} auf dem Weg zu deiner Station."
|
||||
station_done: "Puh. 😮💨 Das war vermutlich viel Arbeit, aber jetzt ist es vorbei. ✅🎉"
|
||||
team_is_here: "Team ist da"
|
||||
info_single_team_not_yet_rated: "Noch keine Punkte für diese Gruppe vergeben ⤵️"
|
||||
info_multiple_teams_not_yet_rated: "Noch keine Punkte für diese Gruppen vergeben ⤵️"
|
||||
@@ -88,7 +90,7 @@ state_rated: "Schon bewertet"
|
||||
state_rated_icon: "✅"
|
||||
since_time: "seit %{time}"
|
||||
left_at: "um %{time} gegangen"
|
||||
arrived_at_started_at_left_at: "um %{arrived} eingetroffen, um %{active} gestarted und um %{left} gegangen"
|
||||
arrived_at_started_at_left_at: "um %{arrived} eingetroffen, um %{active} gestartet und um %{left} gegangen"
|
||||
team_finished: "Team fertig"
|
||||
team_starting: "Team startet"
|
||||
notes: "Notizen"
|
||||
@@ -96,6 +98,7 @@ save_notes: "Notizen speichern"
|
||||
confirm_station_cancel_team_active: "Bist du sicher, dass das Team %{team} noch nicht bei dir arbeitet? Das Team wird zurück auf die Warte-Position gesetzt"
|
||||
confirm_station_cancel_team_waiting: "Bist du sicher, dass das Team %{team} noch nicht bei dir ist? Das kann _NICHT_ mehr rückgängig gemacht werden."
|
||||
confirm_station_cancel_team_finished: "Bist du sicher, dass das Team noch nicht bei dir fertig ist? Das Team wird zurück auf die Arbeits-Position gesetzt."
|
||||
confirm_station_cancel_team_rated: "Bist du sicher, dass das Team noch nicht bei dir fertig ist? Das Team wird zurück auf die Arbeits-Position gesetzt und die aktuelle Bewertung gelöscht."
|
||||
|
||||
|
||||
#
|
||||
@@ -110,14 +113,16 @@ confirm_station_cancel_team_finished: "Bist du sicher, dass das Team noch nicht
|
||||
station: "Station"
|
||||
stations: "Stationen"
|
||||
stations_expl_without_first_word: "sind festgelegte Orte mit spezifischen Aufgaben."
|
||||
station_expl_for_everyone: "<p>In diesem Tool solltest du diese 3 Dinge vermerken:</p> <ol> <li> <b>Ein Team kommt zu deiner Station:</b> Du wählst das entsprechende Team aus und klickst auf <em>Team ist da</em>. Das Team ist nun im Wartemodus (⏳). </li> <li> <b>Das Team beginnt mit der Aufgabe bei deiner Station:</b> Du klickst beim entsprechenden Team auf <em>Team startet</em>. Das Team ist nun im aktiven Modus (🎬). </li> <li> <b>Das Team hat deine Station beendet und ist gegangen:</b> Du klickst beim entsprechenden Team auf <em>Team fertig</em>. Bitte schau, dass du das immer zeitnah erledigst, damit die nächste Station informiert werden kann, dass ein Team auf dem Weg ist. </li> </ol> <p>Zu jedem Zeitpunkt kannst du mit Klick auf ✏️ Notizen zu den Teams machen. In aller Ruhe kannst du unter dem Punkt <em>Zu bewerten</em> die Teams, die schon bei dir waren, bewerten.</p>"
|
||||
station_notes_expl: "Diese Notizen werden nur hier angezeigt. Du kannst diese verwenden um zB Kontaktmöglichkeiten (Telefonnummer) zu hinterlegen."
|
||||
station_expl_for_everyone: "<p>In diesem Tool solltest du diese 3 Dinge vermerken:</p> <ol> <li> <b>Ein Team kommt zu deiner Station:</b> Du wählst das entsprechende Team aus und klickst auf <em>Team ist da</em>. Das Team ist nun im Wartemodus (⏳). </li> <li> <b>Das Team beginnt mit der Aufgabe bei deiner Station:</b> Du klickst beim entsprechenden Team auf <em>Team startet</em>. Das Team ist nun im aktiven Modus (🎬). </li> <li> <b>Das Team hat deine Station beendet und ist gegangen:</b> Du klickst beim entsprechenden Team auf <em>Team fertig</em>. Bitte schau, dass du das immer zeitnah erledigst, damit die nächste Station informiert werden kann, dass ein Team auf dem Weg ist. </li> </ol> <p>Zu jedem Zeitpunkt kannst du mit Klick auf ✏️ Notizen zu den Teams machen und das Team wieder entfernen, solltest du dich verklickt haben. In aller Ruhe kannst du unter dem Punkt <em>Zu bewerten</em> die Teams, die schon bei dir waren, bewerten.</p>"
|
||||
nonexisting_station: "Station mit ID %{id} existiert nicht."
|
||||
station_url: "Stations-Link"
|
||||
station_url_info: "Diesen Link nur Betreuern der Station %{station} geben! Mit diesem Link erhält man die Berechtigung, Teams zu bewerten."
|
||||
login_link: "Login-Link"
|
||||
station_name_edit: "Routennamen bearbeiten"
|
||||
station_name_edit: "Stationsnamen bearbeiten"
|
||||
go_to_stations: "Zu den Stationen"
|
||||
crewless_station: "Station ohne Stationsbetreuer"
|
||||
last_station_has_to_be_crewful: "Die letzte Station der Route muss Stationsbetreuer haben, sonst gibt's keine erste Station für die Teams"
|
||||
amount_crew: "Anzahl Stationsbetreuer"
|
||||
last_access_crew: "Letzter Zugriff eines Stationsbetreuers"
|
||||
not_loggedin_yet: "Noch nicht eingeloggt :-("
|
||||
@@ -144,7 +149,7 @@ station_delete_succ: "Station %{name} erfolgreich gelöscht"
|
||||
station_delete_err_already_used: "Station %{name} konnte nicht gelöscht werden, da sie bereits verwendet wird (%{err})"
|
||||
station_has_not_rated_team_yet: "Station hat Team noch nicht bewertet" # should be short -> tooltip
|
||||
station_move_up: "%{name} nach vor reihen" # should be short -> tooltip
|
||||
generate_station_pdf: "Stations PDF generieren"
|
||||
generate_station_pdf: "Stations-Dokument zum Ausdrucken der Login-Links (QR Code)"
|
||||
station_new_name: "Station %{old} heißt ab sofort %{new}."
|
||||
station_new_notes: "Notizen für die Station %{station} erfolgreich bearbeitet"
|
||||
station_new_crew_amount: "Anzahl an Betreuer für die Station %{station} erfolgreich bearbeitet"
|
||||
@@ -182,6 +187,7 @@ team: "Team"
|
||||
teams: "Teams"
|
||||
teams_expl_without_first_word: "sind eine Menge an Personen, die verschiedene <a href='/admin/station'>Stationen</a> ablaufen. Welche Stationen, entscheidet sich je nachdem, welcher <a href='/admin/route'>Route</a> sie zugewiesen sind."
|
||||
nonexisting_team: "Team mit ID %{id} existiert nicht."
|
||||
team_notes_expl: "Diese Notizen werden den Stationen angezeigt, die diese Teams am Anfang mitnehmen sollen."
|
||||
select_team: "Team auswählen"
|
||||
new_team: "Neues Team"
|
||||
edit_teamname: "Teamname bearbeiten"
|
||||
@@ -192,6 +198,9 @@ not_yet_done: "nocht nicht fertig"
|
||||
team_created: "Team %{team} erstellt"
|
||||
team_deleted: "Team %{team} gelöscht"
|
||||
add_new_note: "Neue Notiz hinzufügen"
|
||||
team_waiting_step_back: "Team doch nicht da"
|
||||
team_active_step_back: "Team doch nicht gestartet"
|
||||
team_done_step_back: "Team doch nicht fertig"
|
||||
team_not_created_duplicate_name: "Team %{team} konnte nicht erstellt werden, da es bereits ein Team mit diesem Namen gibt (%{err})"
|
||||
team_not_created_no_station_in_route: "Team %{team} konnte nicht erstellt werden, da in der angegebenen Route %{route} keine Station vorkommt und daher die Startstation nicht festgelegt werden kann."
|
||||
team_not_deleted_already_in_use: "Team %{team} kann nicht gelöscht werden, da es bereits verwendet wird. (%{err})"
|
||||
@@ -211,7 +220,7 @@ last_contact_team: "Letzter Stationskontakt der Teams"
|
||||
not_yet_seen: "Noch nicht gesehen"
|
||||
no_teams: "Es gibt noch keine Teams."
|
||||
route_needed_before_creating_teams: "Bevor du ein Team erstellen kannst, musst du zumindest eine Route erstellen, die das Team gehen kann."
|
||||
have_i_lost_groups: "Hab ich eine Gruppe verloren? 😳"
|
||||
have_i_lost_teams: "Hab ich ein Team verloren? 😳"
|
||||
confirm_delete_team: "Bist du sicher, dass das Team gelöscht werden soll? Das kann _NICHT_ mehr rückgängig gemacht werden."
|
||||
|
||||
|
||||
@@ -242,6 +251,7 @@ edit_username: "Username bearbeiten"
|
||||
new_admin_link: "Passwort vergessen: Neuen Loginlink generieren"
|
||||
confirm_new_admin_link: "Bist du sicher, dass du einen neuen Passwort-Link generieren willst? Mit dem alten Passwort kann man sich dann nicht mehr einloggen."
|
||||
new_user_name: "Admin %{old} heißt ab sofort %{new}"
|
||||
user_name_already_exists: "Es gibt bereits einen Admin %{new}."
|
||||
succ_new_admin_link: "Neuer Loginlink für User %{user} wurde generiert"
|
||||
new_admin: "Neuer Admin"
|
||||
confirm_admin_delete: "Bist du sicher, dass der User gelöscht werden soll? Das kann _NICHT_ mehr rückgängig gemacht werden."
|
||||
|
||||
134
src/admin/mod.rs
134
src/admin/mod.rs
@@ -1,18 +1,23 @@
|
||||
use crate::{auth::Backend, models::rating::Rating, page, suc, AppState, Station};
|
||||
use crate::{
|
||||
AppState, PageBuilder, Station,
|
||||
auth::{AuthSession, Backend},
|
||||
rating::Rating,
|
||||
suc,
|
||||
};
|
||||
use axum::{
|
||||
Router,
|
||||
extract::State,
|
||||
response::{IntoResponse, Redirect},
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use axum_login::login_required;
|
||||
use maud::{html, Markup};
|
||||
use maud::{Markup, PreEscaped, html};
|
||||
use rand::{
|
||||
distr::{Distribution, Uniform},
|
||||
rng,
|
||||
};
|
||||
use route::Route;
|
||||
use sqlx::SqlitePool;
|
||||
use sqlx::{SqliteConnection, SqlitePool};
|
||||
use std::sync::Arc;
|
||||
use team::Team;
|
||||
use tower_sessions::Session;
|
||||
@@ -31,7 +36,9 @@ fn generate_random_alphanumeric(length: usize) -> String {
|
||||
}
|
||||
|
||||
async fn highscore(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
let routes = Route::all(&db).await;
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let routes = Route::all(db).await;
|
||||
|
||||
let content = html! {
|
||||
h1 {
|
||||
@@ -42,26 +49,25 @@ async fn highscore(State(db): State<Arc<SqlitePool>>, session: Session) -> Marku
|
||||
details open[idx==0] {
|
||||
summary { (route.name) }
|
||||
|
||||
div class="overflow-auto" {
|
||||
table {
|
||||
div class="overflow-auto" style="max-height: 100vh" {
|
||||
table class="striped sticky-table" {
|
||||
thead {
|
||||
tr {
|
||||
td { (t!("team")) }
|
||||
@for station in route.stations(&db).await {
|
||||
td {
|
||||
th { (t!("team")) }
|
||||
@for station in route.stations(db).await {
|
||||
th {
|
||||
(station)
|
||||
}
|
||||
}
|
||||
td { (t!("total_points")) }
|
||||
td { (t!("rank")) }
|
||||
td { (t!("team")) }
|
||||
th { (t!("total_points")) }
|
||||
th { (t!("rank")) }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
@let mut rank = 0;
|
||||
@let mut amount_teams_iterated = 0;
|
||||
@let mut prev_points = i64::MAX;
|
||||
@for team in route.teams_ordered_by_points(&db).await {
|
||||
@for team in route.teams_ordered_by_points(db).await {
|
||||
@let mut total_points = 0;
|
||||
({ amount_teams_iterated += 1;"" })
|
||||
tr {
|
||||
@@ -70,9 +76,9 @@ async fn highscore(State(db): State<Arc<SqlitePool>>, session: Session) -> Marku
|
||||
(team.name)
|
||||
}
|
||||
}
|
||||
@for station in route.stations(&db).await {
|
||||
@for station in route.stations(db).await {
|
||||
td {
|
||||
@if let Some(rating) = Rating::find_by_team_and_station(&db, &team, &station).await {
|
||||
@if let Some(rating) = Rating::find_by_team_and_station(db, &team, &station).await {
|
||||
@if let (Some(notes), Some(points)) = (rating.notes, rating.points) {
|
||||
({total_points += points;""})
|
||||
em data-placement="bottom" data-tooltip=(notes) { (points) }
|
||||
@@ -96,11 +102,6 @@ async fn highscore(State(db): State<Arc<SqlitePool>>, session: Session) -> Marku
|
||||
(rank)
|
||||
"."
|
||||
}
|
||||
td {
|
||||
a href=(format!("/admin/team/{}", team.id)) {
|
||||
(team.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -110,7 +111,10 @@ async fn highscore(State(db): State<Arc<SqlitePool>>, session: Session) -> Marku
|
||||
}
|
||||
};
|
||||
|
||||
page(content, session, false).await
|
||||
PageBuilder::new(content, session)
|
||||
.full_page()
|
||||
.markup()
|
||||
.await
|
||||
}
|
||||
|
||||
#[derive(PartialEq)]
|
||||
@@ -121,7 +125,7 @@ pub enum RunStatus {
|
||||
}
|
||||
|
||||
impl RunStatus {
|
||||
pub async fn curr(db: &SqlitePool) -> Self {
|
||||
pub async fn curr(db: &mut SqliteConnection) -> Self {
|
||||
let stations = Station::all(db).await;
|
||||
if stations.is_empty() {
|
||||
return RunStatus::NoStationsYet;
|
||||
@@ -134,15 +138,26 @@ impl RunStatus {
|
||||
}
|
||||
}
|
||||
|
||||
async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
let status = RunStatus::curr(&db).await;
|
||||
async fn index(
|
||||
State(db): State<Arc<SqlitePool>>,
|
||||
session: Session,
|
||||
auth_session: AuthSession,
|
||||
) -> Markup {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let user = auth_session
|
||||
.user
|
||||
.expect("Can only be called by loggedin people");
|
||||
let status = RunStatus::curr(db).await;
|
||||
let content = html! {
|
||||
nav {
|
||||
ul {
|
||||
img class="logo" src="/logo-hor.svg" style="max-width: 100%; width: 25em; margin:auto;";
|
||||
img class="logo-inv" src="/logo-hor-inv.svg" width="max-width: 100%; width: 25em; margin:auto;";
|
||||
img class="logo" src="/logo-hor.svg";
|
||||
img class="logo-inv" src="/logo-hor-inv.svg";
|
||||
}
|
||||
ul {
|
||||
"👋" (user.name)" •"
|
||||
(PreEscaped(" "))
|
||||
a href="/auth/logout" {
|
||||
(t!("logout"))
|
||||
}
|
||||
@@ -188,24 +203,60 @@ async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
}
|
||||
},
|
||||
RunStatus::Active => {
|
||||
@let stations = Station::all(db).await;
|
||||
a href="/admin/end-run" onclick=(format!("return confirm('{}');", t!("confirm_end_run"))) {
|
||||
button style="background-color: red;" {
|
||||
(t!("end_run"))
|
||||
}
|
||||
}
|
||||
},
|
||||
RunStatus::HasEnded => {
|
||||
@let stations = Station::all(&db).await;
|
||||
a href="/admin/restart-run" onclick=(format!("return confirm('{}');", t!("confirm_restart_run"))) {
|
||||
button style="background-color: red;" {
|
||||
(t!("restart_run"))
|
||||
}
|
||||
p {
|
||||
(t!("station_should_take_these_teams_to_first_station"))
|
||||
}
|
||||
table {
|
||||
thead {
|
||||
tr {
|
||||
th { (t!("stations")) }
|
||||
th { (t!("come_home_with_these_groups")) }
|
||||
th { (t!("teams")) }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
@for station in stations {
|
||||
tr {
|
||||
td {
|
||||
(station)
|
||||
@if let Some(notes) = &station.notes {
|
||||
article {
|
||||
(notes)
|
||||
}
|
||||
}
|
||||
}
|
||||
td {
|
||||
ol {
|
||||
@for team in Team::all_with_first_station(db, &station).await {
|
||||
li { (team) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
RunStatus::HasEnded => {
|
||||
@let stations = Station::all(db).await;
|
||||
a href="/admin/restart-run" onclick=(format!("return confirm('{}');", t!("confirm_restart_run"))) {
|
||||
button style="background-color: red;" {
|
||||
(t!("restart_run"))
|
||||
}
|
||||
}
|
||||
p {
|
||||
(t!("station_should_take_these_teams_home"))
|
||||
}
|
||||
table {
|
||||
thead {
|
||||
tr {
|
||||
th { (t!("stations")) }
|
||||
th { (t!("teams")) }
|
||||
}
|
||||
}
|
||||
tbody {
|
||||
@@ -214,7 +265,7 @@ async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
td { (station) }
|
||||
td {
|
||||
ol {
|
||||
@for team in Team::all_with_last_station(&db, &station).await {
|
||||
@for team in Team::all_with_last_station(db, &station).await {
|
||||
li { (team) }
|
||||
}
|
||||
}
|
||||
@@ -226,16 +277,21 @@ async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
}
|
||||
}
|
||||
};
|
||||
page(content, session, false).await
|
||||
|
||||
PageBuilder::new(content, session).markup().await
|
||||
}
|
||||
|
||||
async fn end_run(State(db): State<Arc<SqlitePool>>, session: Session) -> impl IntoResponse {
|
||||
Team::end_run(&db).await;
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
Team::end_run(db).await;
|
||||
suc!(session, t!("run_ended"));
|
||||
Redirect::to("/admin")
|
||||
}
|
||||
async fn restart_run(State(db): State<Arc<SqlitePool>>, session: Session) -> impl IntoResponse {
|
||||
Team::restart_run(&db).await;
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
Team::restart_run(db).await;
|
||||
suc!(session, t!("run_restarted"));
|
||||
Redirect::to("/admin")
|
||||
}
|
||||
|
||||
@@ -1,44 +1,43 @@
|
||||
use crate::{
|
||||
admin::{station::Station, team::Team},
|
||||
AppState,
|
||||
admin::{station::Station, team::Team},
|
||||
};
|
||||
use axum::Router;
|
||||
use futures::future::join_all;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Row, SqlitePool};
|
||||
use sqlx::{FromRow, Row, SqliteConnection};
|
||||
|
||||
mod web;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct Route {
|
||||
pub(crate) id: i64,
|
||||
pub(crate) name: String,
|
||||
}
|
||||
|
||||
impl Route {
|
||||
pub(crate) async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
pub(crate) async fn all(db: &mut SqliteConnection) -> Vec<Self> {
|
||||
sqlx::query_as::<_, Self>("SELECT id, name FROM route;")
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
pub async fn find_by_id(db: &mut SqliteConnection, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT id, name FROM route WHERE id = ?", id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
async fn create(db: &SqlitePool, name: &str) -> Result<(), String> {
|
||||
sqlx::query!("INSERT INTO route(name) VALUES (?)", name)
|
||||
.execute(db)
|
||||
pub(crate) async fn create(db: &mut SqliteConnection, name: &str) -> Result<Self, String> {
|
||||
let route = sqlx::query!("INSERT INTO route(name) VALUES (?) RETURNING id", name)
|
||||
.fetch_one(&mut *db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
Ok(())
|
||||
Ok(Self::find_by_id(db, route.id).await.expect("just created"))
|
||||
}
|
||||
|
||||
async fn update_name(&self, db: &SqlitePool, name: &str) {
|
||||
async fn update_name(&self, db: &mut SqliteConnection, name: &str) {
|
||||
sqlx::query!("UPDATE route SET name = ? WHERE id = ?", name, self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
@@ -47,7 +46,7 @@ impl Route {
|
||||
|
||||
pub(crate) async fn add_station(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
db: &mut SqliteConnection,
|
||||
station: &Station,
|
||||
) -> Result<(), String> {
|
||||
sqlx::query!(
|
||||
@@ -70,7 +69,7 @@ impl Route {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn delete_station(&self, db: &SqlitePool, station: &Station) -> bool {
|
||||
async fn delete_station(&self, db: &mut SqliteConnection, station: &Station) -> bool {
|
||||
let result = sqlx::query!(
|
||||
"DELETE FROM route_station WHERE route_id = ? AND station_id = ?",
|
||||
self.id,
|
||||
@@ -83,13 +82,13 @@ impl Route {
|
||||
result.rows_affected() > 0
|
||||
}
|
||||
|
||||
async fn move_station_higher(&self, db: &SqlitePool, station: &Station) -> bool {
|
||||
async fn move_station_higher(&self, db: &mut SqliteConnection, station: &Station) -> bool {
|
||||
let result = sqlx::query!(
|
||||
"UPDATE route_station SET pos = pos-3 WHERE route_id = ? AND station_id = ?",
|
||||
self.id,
|
||||
station.id
|
||||
)
|
||||
.execute(db)
|
||||
.execute(&mut *db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -125,7 +124,7 @@ DROP TABLE temp_pos;",
|
||||
true
|
||||
}
|
||||
|
||||
async fn delete(&self, db: &SqlitePool) -> Result<(), String> {
|
||||
async fn delete(&self, db: &mut SqliteConnection) -> Result<(), String> {
|
||||
sqlx::query!("DELETE FROM route WHERE id = ?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
@@ -133,7 +132,7 @@ DROP TABLE temp_pos;",
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn stations(&self, db: &SqlitePool) -> Vec<Station> {
|
||||
pub(crate) async fn stations(&self, db: &mut SqliteConnection) -> Vec<Station> {
|
||||
// TODO: switch to macro
|
||||
sqlx::query_as::<_, Station>(
|
||||
"
|
||||
@@ -150,7 +149,7 @@ DROP TABLE temp_pos;",
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub(crate) async fn crewless_stations(&self, db: &SqlitePool) -> Vec<Station> {
|
||||
pub(crate) async fn crewless_stations(&self, db: &mut SqliteConnection) -> Vec<Station> {
|
||||
self.stations(db)
|
||||
.await
|
||||
.into_iter()
|
||||
@@ -158,7 +157,7 @@ DROP TABLE temp_pos;",
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn crewful_stations(&self, db: &SqlitePool) -> Vec<Station> {
|
||||
pub(crate) async fn crewful_stations(&self, db: &mut SqliteConnection) -> Vec<Station> {
|
||||
self.stations(db)
|
||||
.await
|
||||
.into_iter()
|
||||
@@ -166,7 +165,7 @@ DROP TABLE temp_pos;",
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn stations_not_in_route(&self, db: &SqlitePool) -> Vec<Station> {
|
||||
async fn stations_not_in_route(&self, db: &mut SqliteConnection) -> Vec<Station> {
|
||||
// TODO: switch to macro
|
||||
sqlx::query_as::<_, Station>(
|
||||
"
|
||||
@@ -186,15 +185,18 @@ DROP TABLE temp_pos;",
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub(crate) async fn teams(&self, db: &SqlitePool) -> Vec<Team> {
|
||||
pub(crate) async fn teams(&self, db: &mut SqliteConnection) -> Vec<Team> {
|
||||
Team::all_with_route(db, self).await
|
||||
}
|
||||
|
||||
pub(crate) async fn teams_ordered_by_points(&self, db: &SqlitePool) -> Vec<Team> {
|
||||
pub(crate) async fn teams_ordered_by_points(&self, db: &mut SqliteConnection) -> Vec<Team> {
|
||||
let teams = Team::all_with_route(db, self).await;
|
||||
|
||||
// First, collect all the points
|
||||
let points_futures: Vec<_> = teams.iter().map(|team| team.get_curr_points(db)).collect();
|
||||
let points = join_all(points_futures).await;
|
||||
let mut points = Vec::new();
|
||||
for team in &teams {
|
||||
points.push(team.get_curr_points(&mut *db).await);
|
||||
}
|
||||
|
||||
// Create pairs of (team, points)
|
||||
let mut team_with_points: Vec<_> = teams.into_iter().zip(points).collect();
|
||||
@@ -206,7 +208,10 @@ DROP TABLE temp_pos;",
|
||||
team_with_points.into_iter().map(|(team, _)| team).collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn get_next_first_station(&self, db: &SqlitePool) -> Option<Station> {
|
||||
pub(crate) async fn get_next_first_station(
|
||||
&self,
|
||||
db: &mut SqliteConnection,
|
||||
) -> Option<Station> {
|
||||
let Ok(row) = sqlx::query(&format!(
|
||||
"
|
||||
SELECT
|
||||
@@ -222,7 +227,7 @@ DROP TABLE temp_pos;",
|
||||
LIMIT 1",
|
||||
self.id
|
||||
))
|
||||
.fetch_one(db)
|
||||
.fetch_one(&mut *db)
|
||||
.await
|
||||
else {
|
||||
return None; // No station for route exists
|
||||
@@ -236,7 +241,11 @@ DROP TABLE temp_pos;",
|
||||
)
|
||||
}
|
||||
|
||||
pub async fn next_station(&self, db: &SqlitePool, target_station: &Station) -> Option<Station> {
|
||||
pub async fn next_station(
|
||||
&self,
|
||||
db: &mut SqliteConnection,
|
||||
target_station: &Station,
|
||||
) -> Option<Station> {
|
||||
let stations = Station::all(db).await;
|
||||
for station in stations {
|
||||
if let Some(prev_station) = self.prev_station(db, &station).await {
|
||||
@@ -248,11 +257,19 @@ DROP TABLE temp_pos;",
|
||||
None
|
||||
}
|
||||
|
||||
pub async fn prev_station(&self, db: &SqlitePool, station: &Station) -> Option<Station> {
|
||||
pub async fn prev_station(
|
||||
&self,
|
||||
db: &mut SqliteConnection,
|
||||
station: &Station,
|
||||
) -> Option<Station> {
|
||||
if station.crewless() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.stations_not_in_route(db).await.contains(station) {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.stations(db).await.len() <= 1 {
|
||||
return None;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
use super::Route;
|
||||
use crate::{admin::station::Station, er, page, suc, AppState};
|
||||
use crate::{AppState, PageBuilder, admin::station::Station, er, suc};
|
||||
use axum::{
|
||||
Form, Router,
|
||||
extract::State,
|
||||
response::{IntoResponse, Redirect},
|
||||
routing::{get, post},
|
||||
Form, Router,
|
||||
};
|
||||
use maud::{html, Markup, PreEscaped};
|
||||
use maud::{Markup, PreEscaped, html};
|
||||
use serde::Deserialize;
|
||||
use sqlx::SqlitePool;
|
||||
use std::sync::Arc;
|
||||
use tower_sessions::Session;
|
||||
|
||||
async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
let routes = Route::all(&db).await;
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let routes = Route::all(db).await;
|
||||
|
||||
let content = html! {
|
||||
h1 {
|
||||
@@ -28,7 +30,7 @@ async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
ol {
|
||||
@for route in &routes{
|
||||
li {
|
||||
@if route.stations(&db).await.is_empty() {
|
||||
@if route.stations(db).await.is_empty() {
|
||||
em data-tooltip=(t!("route_has_no_station_assigned")) {
|
||||
"⚠️"
|
||||
}
|
||||
@@ -58,7 +60,8 @@ async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
}
|
||||
}
|
||||
};
|
||||
page(content, session, false).await
|
||||
|
||||
PageBuilder::new(content, session).markup().await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -71,8 +74,10 @@ async fn create(
|
||||
session: Session,
|
||||
Form(form): Form<CreateForm>,
|
||||
) -> impl IntoResponse {
|
||||
match Route::create(&db, &form.name).await {
|
||||
Ok(()) => suc!(session, t!("route_create_succ", name = form.name)),
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
match Route::create(db, &form.name).await {
|
||||
Ok(_) => suc!(session, t!("route_create_succ", name = form.name)),
|
||||
Err(e) => er!(
|
||||
session,
|
||||
t!("route_create_err_duplicate_name", name = form.name, err = e)
|
||||
@@ -87,12 +92,14 @@ async fn delete(
|
||||
session: Session,
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(route) = Route::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(route) = Route::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_route", id = id));
|
||||
return Redirect::to("/admin/route");
|
||||
};
|
||||
|
||||
match route.delete(&db).await {
|
||||
match route.delete(db).await {
|
||||
Ok(()) => suc!(session, t!("route_delete_succ", name = route.name)),
|
||||
Err(e) => er!(
|
||||
session,
|
||||
@@ -108,15 +115,17 @@ async fn view(
|
||||
session: Session,
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
) -> Result<Markup, impl IntoResponse> {
|
||||
let Some(route) = Route::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(route) = Route::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_route", id = id));
|
||||
return Err(Redirect::to("/admin/route"));
|
||||
};
|
||||
|
||||
let cur_stations = route.stations(&db).await;
|
||||
let stations_not_in_route = route.stations_not_in_route(&db).await;
|
||||
let cur_stations = route.stations(db).await;
|
||||
let stations_not_in_route = route.stations_not_in_route(db).await;
|
||||
|
||||
let teams = route.teams(&db).await;
|
||||
let teams = route.teams(db).await;
|
||||
|
||||
let content = html! {
|
||||
h1 {
|
||||
@@ -207,7 +216,8 @@ async fn view(
|
||||
}
|
||||
|
||||
};
|
||||
Ok(page(content, session, false).await)
|
||||
|
||||
Ok(PageBuilder::new(content, session).markup().await)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -220,12 +230,14 @@ async fn update_name(
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
Form(form): Form<UpdateNameForm>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(route) = Route::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(route) = Route::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_route", id = id));
|
||||
return Redirect::to("/admin/route");
|
||||
};
|
||||
|
||||
route.update_name(&db, &form.name).await;
|
||||
route.update_name(db, &form.name).await;
|
||||
|
||||
suc!(
|
||||
session,
|
||||
@@ -245,16 +257,18 @@ async fn add_station(
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
Form(form): Form<AddStationForm>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(route) = Route::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(route) = Route::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_route", id = id));
|
||||
return Redirect::to("/admin/route");
|
||||
};
|
||||
let Some(station) = Station::find_by_id(&db, form.station).await else {
|
||||
let Some(station) = Station::find_by_id(&mut *db, form.station).await else {
|
||||
er!(session, t!("nonexisting_station", id = form.station));
|
||||
return Redirect::to(&format!("/admin/route/{id}"));
|
||||
};
|
||||
|
||||
match route.add_station(&db, &station).await {
|
||||
match route.add_station(db, &station).await {
|
||||
Ok(()) => suc!(
|
||||
session,
|
||||
t!(
|
||||
@@ -282,16 +296,18 @@ async fn delete_station(
|
||||
session: Session,
|
||||
axum::extract::Path((route_id, station_id)): axum::extract::Path<(i64, i64)>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(route) = Route::find_by_id(&db, route_id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(route) = Route::find_by_id(db, route_id).await else {
|
||||
er!(session, t!("nonexisting_route", id = route_id));
|
||||
return Redirect::to("/admin/route");
|
||||
};
|
||||
let Some(station) = Station::find_by_id(&db, station_id).await else {
|
||||
let Some(station) = Station::find_by_id(&mut *db, station_id).await else {
|
||||
er!(session, t!("nonexisting_station", id = station_id));
|
||||
return Redirect::to(&format!("/admin/route/{route_id}"));
|
||||
};
|
||||
|
||||
if route.delete_station(&db, &station).await {
|
||||
if route.delete_station(db, &station).await {
|
||||
suc!(
|
||||
session,
|
||||
t!(
|
||||
@@ -319,16 +335,18 @@ async fn move_station_higher(
|
||||
session: Session,
|
||||
axum::extract::Path((route_id, station_id)): axum::extract::Path<(i64, i64)>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(route) = Route::find_by_id(&db, route_id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(route) = Route::find_by_id(db, route_id).await else {
|
||||
er!(session, t!("nonexisting_route", id = route_id));
|
||||
return Redirect::to("/admin/route");
|
||||
};
|
||||
let Some(station) = Station::find_by_id(&db, station_id).await else {
|
||||
let Some(station) = Station::find_by_id(&mut *db, station_id).await else {
|
||||
er!(session, t!("nonexisting_station", id = station_id));
|
||||
return Redirect::to(&format!("/admin/route/{route_id}"));
|
||||
};
|
||||
|
||||
if route.move_station_higher(&db, &station).await {
|
||||
if route.move_station_higher(db, &station).await {
|
||||
suc!(
|
||||
session,
|
||||
t!(
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
use super::{generate_random_alphanumeric, team::Team};
|
||||
use crate::{
|
||||
admin::route::Route,
|
||||
models::rating::{Rating, TeamsAtStationLocation},
|
||||
AppState,
|
||||
admin::route::Route,
|
||||
rating::{Rating, TeamsAtStationLocation},
|
||||
};
|
||||
use axum::Router;
|
||||
use chrono::{DateTime, Local, NaiveDateTime, Utc};
|
||||
use futures::{stream, StreamExt};
|
||||
use maud::{html, Markup, Render};
|
||||
use maud::{Markup, Render, html};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
use sqlx::{FromRow, SqliteConnection};
|
||||
|
||||
pub(crate) mod model;
|
||||
pub(crate) mod print;
|
||||
mod typst;
|
||||
mod web;
|
||||
@@ -19,7 +19,7 @@ mod web;
|
||||
pub(crate) struct Station {
|
||||
pub(crate) id: i64,
|
||||
pub(crate) name: String,
|
||||
notes: Option<String>,
|
||||
pub(crate) notes: Option<String>,
|
||||
pub(crate) amount_people: Option<i64>,
|
||||
last_login: Option<NaiveDateTime>,
|
||||
pub(crate) pw: String,
|
||||
@@ -50,7 +50,7 @@ impl Render for Station {
|
||||
}
|
||||
|
||||
impl Station {
|
||||
pub(crate) async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
pub(crate) async fn all(db: &mut SqliteConnection) -> Vec<Self> {
|
||||
sqlx::query_as::<_, Self>(
|
||||
"SELECT id, name, notes, amount_people, last_login, ready, pw, lat, lng FROM station;",
|
||||
)
|
||||
@@ -59,7 +59,7 @@ impl Station {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
pub async fn find_by_id(db: &mut SqliteConnection, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT id, name, notes, amount_people, last_login, ready, pw, lat, lng FROM station WHERE id = ?",
|
||||
@@ -77,13 +77,13 @@ impl Station {
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn login(db: &SqlitePool, id: i64, code: &str) -> Option<Self> {
|
||||
pub async fn login(db: &mut SqliteConnection, id: i64, code: &str) -> Option<Self> {
|
||||
let station = sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT id, name, notes, amount_people, last_login, ready, pw, lat, lng FROM station WHERE id = ? AND pw = ?",
|
||||
id, code
|
||||
)
|
||||
.fetch_one(db)
|
||||
.fetch_one(&mut *db)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
@@ -98,7 +98,7 @@ impl Station {
|
||||
Some(station)
|
||||
}
|
||||
|
||||
pub async fn switch_ready(&self, db: &SqlitePool) {
|
||||
pub async fn switch_ready(&self, db: &mut SqliteConnection) {
|
||||
let new_ready_status = !self.ready;
|
||||
sqlx::query!(
|
||||
"UPDATE station SET ready = ? WHERE id = ?",
|
||||
@@ -110,34 +110,34 @@ impl Station {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub(crate) async fn create(db: &SqlitePool, name: &str) -> Result<(), String> {
|
||||
pub(crate) async fn create(db: &mut SqliteConnection, name: &str) -> Result<Self, String> {
|
||||
let code = generate_random_alphanumeric(8);
|
||||
let station_id = sqlx::query!(
|
||||
"INSERT INTO station(name, pw) VALUES (?, ?) RETURNING id",
|
||||
name,
|
||||
code
|
||||
)
|
||||
.fetch_one(db)
|
||||
.fetch_one(&mut *db)
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let station = Station::find_by_id(&mut *db, station_id.id)
|
||||
.await
|
||||
.expect("just created");
|
||||
|
||||
let mut routes = Route::all(db).await.into_iter();
|
||||
if let Some(route) = routes.next() {
|
||||
if routes.next().is_none() {
|
||||
// Just one route exists -> use it for new station
|
||||
let station = Station::find_by_id(db, station_id.id)
|
||||
.await
|
||||
.expect("just created");
|
||||
|
||||
route.add_station(db, &station).await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
Ok(station)
|
||||
}
|
||||
|
||||
pub(crate) async fn new_team_waiting(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
db: &mut SqliteConnection,
|
||||
team: &Team,
|
||||
) -> Result<(), String> {
|
||||
let teams = TeamsAtStationLocation::for_station(db, self).await;
|
||||
@@ -157,7 +157,7 @@ impl Station {
|
||||
|
||||
pub(crate) async fn team_update(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
db: &mut SqliteConnection,
|
||||
team: &Team,
|
||||
points: Option<i64>,
|
||||
notes: Option<String>,
|
||||
@@ -167,7 +167,7 @@ impl Station {
|
||||
Some(n) => Some(n),
|
||||
None => None,
|
||||
};
|
||||
let teams = TeamsAtStationLocation::for_station(db, self).await;
|
||||
let teams = TeamsAtStationLocation::for_station(&mut *db, self).await;
|
||||
|
||||
let waiting_teams: Vec<&Team> = teams.waiting.iter().map(|(team, _)| team).collect();
|
||||
let doing_teams: Vec<&Team> = teams.doing.iter().map(|(team, _)| team).collect();
|
||||
@@ -194,7 +194,7 @@ impl Station {
|
||||
|
||||
pub(crate) async fn remove_team_waiting(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
db: &mut SqliteConnection,
|
||||
team: &Team,
|
||||
) -> Result<(), String> {
|
||||
let teams = TeamsAtStationLocation::for_station(db, self).await;
|
||||
@@ -210,7 +210,11 @@ impl Station {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn team_starting(&self, db: &SqlitePool, team: &Team) -> Result<(), String> {
|
||||
pub(crate) async fn team_starting(
|
||||
&self,
|
||||
db: &mut SqliteConnection,
|
||||
team: &Team,
|
||||
) -> Result<(), String> {
|
||||
let teams = TeamsAtStationLocation::for_station(db, self).await;
|
||||
|
||||
let waiting_teams: Vec<&Team> = teams.waiting.iter().map(|(team, _)| team).collect();
|
||||
@@ -233,7 +237,7 @@ impl Station {
|
||||
|
||||
pub(crate) async fn remove_team_doing(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
db: &mut SqliteConnection,
|
||||
team: &Team,
|
||||
) -> Result<(), String> {
|
||||
let teams = TeamsAtStationLocation::for_station(db, self).await;
|
||||
@@ -260,7 +264,11 @@ impl Station {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn team_finished(&self, db: &SqlitePool, team: &Team) -> Result<(), String> {
|
||||
pub(crate) async fn team_finished(
|
||||
&self,
|
||||
db: &mut SqliteConnection,
|
||||
team: &Team,
|
||||
) -> Result<(), String> {
|
||||
let teams = TeamsAtStationLocation::for_station(db, self).await;
|
||||
|
||||
let doing_teams: Vec<&Team> = teams.doing.iter().map(|(team, _)| team).collect();
|
||||
@@ -283,7 +291,7 @@ impl Station {
|
||||
|
||||
pub(crate) async fn remove_team_left(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
db: &mut SqliteConnection,
|
||||
team: &Team,
|
||||
) -> Result<(), String> {
|
||||
let teams = TeamsAtStationLocation::for_station(db, self).await;
|
||||
@@ -312,63 +320,7 @@ impl Station {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_name(&self, db: &SqlitePool, name: &str) {
|
||||
sqlx::query!("UPDATE station SET name = ? WHERE id = ?", name, self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn update_notes(&self, db: &SqlitePool, notes: &str) {
|
||||
sqlx::query!("UPDATE station SET notes = ? WHERE id = ?", notes, self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn update_amount_people(&self, db: &SqlitePool, amount_people: i64) {
|
||||
sqlx::query!(
|
||||
"UPDATE station SET amount_people = ? WHERE id = ?",
|
||||
amount_people,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn update_amount_people_reset(&self, db: &SqlitePool) {
|
||||
sqlx::query!(
|
||||
"UPDATE station SET amount_people = NULL WHERE id = ?",
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn update_location(&self, db: &SqlitePool, lat: f64, lng: f64) {
|
||||
sqlx::query!(
|
||||
"UPDATE station SET lat = ?, lng = ? WHERE id = ?",
|
||||
lat,
|
||||
lng,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
async fn update_location_clear(&self, db: &SqlitePool) {
|
||||
sqlx::query!(
|
||||
"UPDATE station SET lat = NULL, lng=NULL WHERE id = ?",
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn delete(&self, db: &SqlitePool) -> Result<(), String> {
|
||||
async fn delete(&self, db: &mut SqliteConnection) -> Result<(), String> {
|
||||
sqlx::query!("DELETE FROM station WHERE id = ?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
@@ -376,7 +328,7 @@ impl Station {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn routes(&self, db: &SqlitePool) -> Vec<Route> {
|
||||
async fn routes(&self, db: &mut SqliteConnection) -> Vec<Route> {
|
||||
sqlx::query_as::<_, Route>(
|
||||
"
|
||||
SELECT r.id, r.name
|
||||
@@ -392,7 +344,7 @@ impl Station {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn is_in_route(&self, db: &SqlitePool, route: &Route) -> bool {
|
||||
pub async fn is_in_route(&self, db: &mut SqliteConnection, route: &Route) -> bool {
|
||||
for r in self.routes(db).await {
|
||||
if r.id == route.id {
|
||||
return true;
|
||||
@@ -410,7 +362,7 @@ impl Station {
|
||||
Some(datetime_utc.with_timezone(&Local))
|
||||
}
|
||||
|
||||
pub(crate) async fn teams(&self, db: &SqlitePool) -> Vec<Team> {
|
||||
pub(crate) async fn teams(&self, db: &mut SqliteConnection) -> Vec<Team> {
|
||||
sqlx::query_as::<_, Team>(
|
||||
"SELECT DISTINCT t.id, t.name, t.notes, t.amount_people, t.first_station_id, t.last_station_id, t.route_id
|
||||
FROM team t
|
||||
@@ -424,7 +376,7 @@ ORDER BY LOWER(t.name);",
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub(crate) async fn left_teams(&self, db: &SqlitePool) -> Vec<Team> {
|
||||
pub(crate) async fn left_teams(&self, db: &mut SqliteConnection) -> Vec<Team> {
|
||||
sqlx::query_as::<_, Team>(
|
||||
"SELECT t.id, t.name, t.notes, t.amount_people, t.first_station_id, t.last_station_id, t.route_id
|
||||
FROM team t
|
||||
@@ -438,24 +390,20 @@ AND r.left_at IS NOT NULL;",
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn teams_on_the_way(&self, db: &SqlitePool) -> Vec<TeamOnTheWay> {
|
||||
pub async fn teams_on_the_way(&self, db: &mut SqliteConnection) -> Vec<TeamOnTheWay> {
|
||||
let mut ret = Vec::new();
|
||||
|
||||
let teams = self.teams(db).await;
|
||||
let teams = self.teams(&mut *db).await;
|
||||
|
||||
let missing_teams: Vec<Team> = stream::iter(teams)
|
||||
.filter_map(|entry| async move {
|
||||
if entry.been_at_station(db, self).await {
|
||||
None
|
||||
} else {
|
||||
Some(entry)
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
.await;
|
||||
let mut missing_teams = Vec::new();
|
||||
for team in teams {
|
||||
if !team.been_at_station(&mut *db, self).await {
|
||||
missing_teams.push(team);
|
||||
}
|
||||
}
|
||||
|
||||
for team in missing_teams {
|
||||
let route = team.route(db).await;
|
||||
let route = team.route(&mut *db).await;
|
||||
let Some(prev_station) = route.prev_station(db, self).await else {
|
||||
continue;
|
||||
};
|
||||
@@ -476,7 +424,7 @@ AND r.left_at IS NOT NULL;",
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn some_team_has_last_station_id(db: &SqlitePool) -> bool {
|
||||
pub async fn some_team_has_last_station_id(db: &mut SqliteConnection) -> bool {
|
||||
sqlx::query_scalar!("SELECT 1 FROM team WHERE last_station_id IS NOT NULL")
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
|
||||
1
src/admin/station/model/mod.rs
Normal file
1
src/admin/station/model/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub(crate) mod update;
|
||||
136
src/admin/station/model/update.rs
Normal file
136
src/admin/station/model/update.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use crate::Station;
|
||||
use serde::Serialize;
|
||||
use sqlx::Acquire;
|
||||
use sqlx::SqliteConnection;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, PartialEq, Serialize)]
|
||||
pub(crate) enum UpdateAmountPeopleError {
|
||||
#[error(
|
||||
"Last station can't be crewless, if there is already a team with a route where this station is part of, otherwise team can't be assigned a first station."
|
||||
)]
|
||||
LastStationCantBeCrewlessIfTeamExists,
|
||||
}
|
||||
|
||||
impl Station {
|
||||
pub(crate) async fn update_name(&self, db: &mut SqliteConnection, name: &str) {
|
||||
sqlx::query!("UPDATE station SET name = ? WHERE id = ?", name, self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub(crate) async fn update_notes(&self, db: &mut SqliteConnection, notes: &str) {
|
||||
sqlx::query!("UPDATE station SET notes = ? WHERE id = ?", notes, self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub(crate) async fn update_amount_people(
|
||||
&self,
|
||||
db: &mut SqliteConnection,
|
||||
amount_people: i64,
|
||||
) -> Result<(), UpdateAmountPeopleError> {
|
||||
let mut transaction = db.begin().await.unwrap();
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE station SET amount_people = ? WHERE id = ?",
|
||||
amount_people,
|
||||
self.id
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
if amount_people == 0 {
|
||||
let teams = self.teams(&mut transaction).await;
|
||||
for team in teams {
|
||||
let route = team.route(&mut transaction).await;
|
||||
let Some(station) = route.get_next_first_station(&mut transaction).await else {
|
||||
return Err(UpdateAmountPeopleError::LastStationCantBeCrewlessIfTeamExists);
|
||||
};
|
||||
team.update_first_station(transaction.as_mut(), &station)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
transaction.commit().await.unwrap();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn update_amount_people_reset(&self, db: &mut SqliteConnection) {
|
||||
sqlx::query!(
|
||||
"UPDATE station SET amount_people = NULL WHERE id = ?",
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub(crate) async fn update_location(&self, db: &mut SqliteConnection, lat: f64, lng: f64) {
|
||||
sqlx::query!(
|
||||
"UPDATE station SET lat = ?, lng = ? WHERE id = ?",
|
||||
lat,
|
||||
lng,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub(crate) async fn update_location_clear(&self, db: &mut SqliteConnection) {
|
||||
sqlx::query!(
|
||||
"UPDATE station SET lat = NULL, lng=NULL WHERE id = ?",
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
Station,
|
||||
admin::{route::Route, station::model::update::UpdateAmountPeopleError, team::Team},
|
||||
testdb,
|
||||
};
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[sqlx::test]
|
||||
async fn succ_update_not_last_crewful_station() {
|
||||
let pool = testdb!();
|
||||
let db = &mut *pool.acquire().await.unwrap();
|
||||
|
||||
let station = Station::create(db, "Teststation").await.unwrap();
|
||||
let crew_station = Station::create(db, "Bemannte Teststation").await.unwrap();
|
||||
let route = Route::create(db, "Testroute").await.unwrap();
|
||||
route.add_station(db, &station).await.unwrap();
|
||||
route.add_station(db, &crew_station).await.unwrap();
|
||||
let _ = Team::create(db, "Testteam", &route).await.unwrap();
|
||||
|
||||
assert_eq!(station.update_amount_people(db, 0).await, Ok(()));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
async fn fail_update_last_crewful_station() {
|
||||
let pool = testdb!();
|
||||
let db = &mut *pool.acquire().await.unwrap();
|
||||
|
||||
let station = Station::create(db, "Teststation").await.unwrap();
|
||||
let route = Route::create(db, "Testroute").await.unwrap();
|
||||
route.add_station(db, &station).await.unwrap();
|
||||
let _ = Team::create(db, "Testteam", &route).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
station.update_amount_people(db, 0).await,
|
||||
Err(UpdateAmountPeopleError::LastStationCantBeCrewlessIfTeamExists)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
use crate::{admin::station::typst::TypstWrapperWorld, url, AppState, Station};
|
||||
use crate::{AppState, Station, admin::station::typst::TypstWrapperWorld, url};
|
||||
use axum::{
|
||||
Router,
|
||||
extract::State,
|
||||
http::{header, StatusCode},
|
||||
http::{StatusCode, header},
|
||||
response::IntoResponse,
|
||||
routing::get,
|
||||
Router,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
use std::{fmt::Write, sync::Arc};
|
||||
@@ -73,9 +73,9 @@ pub(crate) async fn station_pdf(stations: Vec<Station>) -> Vec<u8> {
|
||||
|
||||
write!(
|
||||
content,
|
||||
r#")
|
||||
r")
|
||||
|
||||
#create_card_grid(cards)"#
|
||||
#create_card_grid(cards)"
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -92,7 +92,9 @@ pub(crate) async fn station_pdf(stations: Vec<Station>) -> Vec<u8> {
|
||||
}
|
||||
|
||||
async fn index(State(db): State<Arc<SqlitePool>>) -> impl IntoResponse {
|
||||
let stations = Station::all(&db).await;
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let stations = Station::all(db).await;
|
||||
let pdf = station_pdf(stations).await;
|
||||
|
||||
(
|
||||
|
||||
@@ -2,13 +2,13 @@ use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use typst::diag::{eco_format, FileError, FileResult, PackageError, PackageResult};
|
||||
use typst::Library;
|
||||
use typst::diag::{FileError, FileResult, PackageError, PackageResult, eco_format};
|
||||
use typst::foundations::{Bytes, Datetime};
|
||||
use typst::syntax::package::PackageSpec;
|
||||
use typst::syntax::{FileId, Source};
|
||||
use typst::text::{Font, FontBook};
|
||||
use typst::utils::LazyHash;
|
||||
use typst::Library;
|
||||
use typst_kit::fonts::{FontSearcher, FontSlot};
|
||||
|
||||
/// Main interface that determines the environment for Typst.
|
||||
@@ -54,15 +54,14 @@ impl TypstWrapperWorld {
|
||||
source: Source::detached(source),
|
||||
time: time::OffsetDateTime::now_utc(),
|
||||
cache_directory: std::env::var_os("CACHE_DIRECTORY")
|
||||
.map(|os_path| os_path.into())
|
||||
.unwrap_or(std::env::temp_dir()),
|
||||
.map_or(std::env::temp_dir(), std::convert::Into::into),
|
||||
http: ureq::Agent::new_with_defaults(),
|
||||
files: Arc::new(Mutex::new(HashMap::new())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A File that will be stored in the HashMap.
|
||||
/// A File that will be stored in the `HashMap`.
|
||||
#[derive(Clone, Debug)]
|
||||
struct FileEntry {
|
||||
bytes: Bytes,
|
||||
@@ -216,11 +215,7 @@ impl typst::World for TypstWrapperWorld {
|
||||
}
|
||||
|
||||
fn retry<T, E>(mut f: impl FnMut() -> Result<T, E>) -> Result<T, E> {
|
||||
if let Ok(ok) = f() {
|
||||
Ok(ok)
|
||||
} else {
|
||||
f()
|
||||
}
|
||||
if let Ok(ok) = f() { Ok(ok) } else { f() }
|
||||
}
|
||||
|
||||
fn http_successful(status: u16) -> bool {
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
use super::model::update::UpdateAmountPeopleError;
|
||||
use crate::{
|
||||
AppState, PageBuilder,
|
||||
admin::{station::Station, team::Team},
|
||||
er,
|
||||
models::rating::{Rating, TeamsAtStationLocation},
|
||||
partials::page,
|
||||
suc, AppState,
|
||||
rating::{Rating, TeamsAtStationLocation},
|
||||
suc,
|
||||
};
|
||||
use axum::{
|
||||
Form, Router,
|
||||
extract::State,
|
||||
response::{IntoResponse, Redirect},
|
||||
routing::{get, post},
|
||||
Form, Router,
|
||||
};
|
||||
use maud::{html, Markup};
|
||||
use maud::{Markup, html};
|
||||
use serde::Deserialize;
|
||||
use sqlx::SqlitePool;
|
||||
use std::fmt::Write;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tower_sessions::Session;
|
||||
|
||||
@@ -27,8 +29,10 @@ async fn create(
|
||||
session: Session,
|
||||
Form(form): Form<CreateForm>,
|
||||
) -> impl IntoResponse {
|
||||
match Station::create(&db, &form.name).await {
|
||||
Ok(()) => suc!(session, t!("station_create_succ", name = form.name)),
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
match Station::create(db, &form.name).await {
|
||||
Ok(_) => suc!(session, t!("station_create_succ", name = form.name)),
|
||||
Err(e) => er!(
|
||||
session,
|
||||
t!(
|
||||
@@ -47,12 +51,14 @@ async fn delete(
|
||||
session: Session,
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(station) = Station::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(station) = Station::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_station", id = id));
|
||||
return Redirect::to("/admin/station");
|
||||
};
|
||||
|
||||
match station.delete(&db).await {
|
||||
match station.delete(db).await {
|
||||
Ok(()) => suc!(session, t!("station_delete_succ", name = station.name)),
|
||||
Err(e) => er!(
|
||||
session,
|
||||
@@ -72,12 +78,14 @@ async fn view(
|
||||
session: Session,
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
) -> Result<Markup, impl IntoResponse> {
|
||||
let Some(station) = Station::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(station) = Station::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_station", id = id));
|
||||
return Err(Redirect::to("/admin/station"));
|
||||
};
|
||||
|
||||
let ratings = Rating::for_station(&db, &station).await;
|
||||
let ratings = Rating::for_station(db, &station).await;
|
||||
|
||||
// maybe switch to maud-display impl of station
|
||||
let content = html! {
|
||||
@@ -100,7 +108,12 @@ async fn view(
|
||||
table {
|
||||
tbody {
|
||||
tr {
|
||||
th scope="row" { (t!("notes")) };
|
||||
th scope="row" {
|
||||
(t!("notes"))
|
||||
article {
|
||||
(t!("station_notes_expl"))
|
||||
}
|
||||
};
|
||||
td {
|
||||
@match station.notes {
|
||||
Some(ref notes) => {
|
||||
@@ -220,7 +233,7 @@ async fn view(
|
||||
tr {
|
||||
td {
|
||||
a href=(format!("/admin/team/{}", rating.team_id)) {
|
||||
(rating.team(&db).await.name)
|
||||
(rating.team(db).await.name)
|
||||
}
|
||||
}
|
||||
td {
|
||||
@@ -329,7 +342,10 @@ async fn view(
|
||||
}
|
||||
|
||||
};
|
||||
Ok(page(content, session, true).await)
|
||||
Ok(PageBuilder::new(content, session)
|
||||
.with_leaflet()
|
||||
.markup()
|
||||
.await)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -342,12 +358,14 @@ async fn update_name(
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
Form(form): Form<UpdateNameForm>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(station) = Station::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(station) = Station::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_station", id = id));
|
||||
return Redirect::to("/admin/station");
|
||||
};
|
||||
|
||||
station.update_name(&db, &form.name).await;
|
||||
station.update_name(db, &form.name).await;
|
||||
|
||||
suc!(
|
||||
session,
|
||||
@@ -367,12 +385,14 @@ async fn update_notes(
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
Form(form): Form<UpdateNotesForm>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(station) = Station::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(station) = Station::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_station", id = id));
|
||||
return Redirect::to("/admin/station");
|
||||
};
|
||||
|
||||
station.update_notes(&db, &form.notes).await;
|
||||
station.update_notes(db, &form.notes).await;
|
||||
|
||||
suc!(session, t!("station_new_notes", station = station.name));
|
||||
|
||||
@@ -389,17 +409,22 @@ async fn update_amount_people(
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
Form(form): Form<UpdateAmountPeopleForm>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(station) = Station::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(station) = Station::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_station", id = id));
|
||||
return Redirect::to("/admin/station");
|
||||
};
|
||||
|
||||
station.update_amount_people(&db, form.amount_people).await;
|
||||
|
||||
suc!(
|
||||
session,
|
||||
t!("station_new_crew_amount", station = station.name)
|
||||
);
|
||||
match station.update_amount_people(db, form.amount_people).await {
|
||||
Ok(()) => suc!(
|
||||
session,
|
||||
t!("station_new_crew_amount", station = station.name)
|
||||
),
|
||||
Err(UpdateAmountPeopleError::LastStationCantBeCrewlessIfTeamExists) => {
|
||||
er!(session, t!("last_station_has_to_be_crewful"));
|
||||
}
|
||||
}
|
||||
|
||||
Redirect::to(&format!("/admin/station/{id}"))
|
||||
}
|
||||
@@ -409,12 +434,14 @@ async fn update_amount_people_reset(
|
||||
session: Session,
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(station) = Station::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(station) = Station::find_by_id(&mut *db, id).await else {
|
||||
er!(session, t!("nonexisting_station", id = id));
|
||||
return Redirect::to("/admin/station");
|
||||
};
|
||||
|
||||
station.update_amount_people_reset(&db).await;
|
||||
station.update_amount_people_reset(db).await;
|
||||
|
||||
suc!(
|
||||
session,
|
||||
@@ -429,12 +456,14 @@ async fn quick(
|
||||
session: Session,
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
) -> Result<Markup, impl IntoResponse> {
|
||||
let Some(station) = Station::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(station) = Station::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_station", id = id));
|
||||
return Err(Redirect::to("/admin/station"));
|
||||
};
|
||||
|
||||
let teams = station.teams(&db).await;
|
||||
let teams = station.teams(db).await;
|
||||
|
||||
// maybe switch to maud-display impl of team
|
||||
let content = html! {
|
||||
@@ -461,7 +490,7 @@ async fn quick(
|
||||
}
|
||||
}
|
||||
td {
|
||||
@if let Some(rating) = Rating::find_by_team_and_station(&db, team, &station).await {
|
||||
@if let Some(rating) = Rating::find_by_team_and_station(db, team, &station).await {
|
||||
a href=(format!("/s/{}/{}", station.id, station.pw)){
|
||||
@if let Some(points) = rating.points {
|
||||
em data-tooltip=(t!("already_entered")) {
|
||||
@@ -483,7 +512,10 @@ async fn quick(
|
||||
input type="submit" value=(t!("save"));
|
||||
}
|
||||
};
|
||||
Ok(page(content, session, true).await)
|
||||
Ok(PageBuilder::new(content, session)
|
||||
.with_leaflet()
|
||||
.markup()
|
||||
.await)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -497,7 +529,9 @@ async fn quick_post(
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
Form(form): Form<QuickUpdate>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(station) = Station::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(station) = Station::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_station", id = id));
|
||||
return Redirect::to("/admin/station");
|
||||
};
|
||||
@@ -507,25 +541,27 @@ async fn quick_post(
|
||||
|
||||
for (team_id, points) in &form.fields {
|
||||
let Ok(team_id) = team_id.parse::<i64>() else {
|
||||
ret.push_str(&format!(
|
||||
let _ = write!(
|
||||
ret,
|
||||
"Skipped team_id={team_id} because this id can't be parsed as i64"
|
||||
));
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let Ok(points) = points.parse::<i64>() else {
|
||||
ret.push_str(&format!(
|
||||
"Skipped team_id={team_id} because points {} can't be parsed as i64",
|
||||
points
|
||||
));
|
||||
let _ = write!(
|
||||
ret,
|
||||
"Skipped team_id={team_id} because {points} points can't be parsed as i64",
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let Some(team) = Team::find_by_id(&db, team_id).await else {
|
||||
ret.push_str(&format!(
|
||||
let Some(team) = Team::find_by_id(db, team_id).await else {
|
||||
let _ = write!(
|
||||
ret,
|
||||
"Skipped team_id={team_id} because this team does not exist"
|
||||
));
|
||||
);
|
||||
continue;
|
||||
};
|
||||
if Rating::find_by_team_and_station(&db, &team, &station)
|
||||
if Rating::find_by_team_and_station(db, &team, &station)
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
@@ -539,7 +575,7 @@ async fn quick_post(
|
||||
continue;
|
||||
}
|
||||
|
||||
Rating::create_quick(&db, &team, &station, points).await;
|
||||
Rating::create_quick(db, &team, &station, points).await;
|
||||
amount_succ += 1;
|
||||
}
|
||||
|
||||
@@ -568,12 +604,14 @@ async fn update_location(
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
Form(form): Form<UpdateLocationForm>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(station) = Station::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(station) = Station::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_station", id = id));
|
||||
return Redirect::to("/admin/station");
|
||||
};
|
||||
|
||||
station.update_location(&db, form.lat, form.lng).await;
|
||||
station.update_location(db, form.lat, form.lng).await;
|
||||
|
||||
suc!(session, t!("location_changed", station = station.name));
|
||||
|
||||
@@ -585,12 +623,14 @@ async fn update_location_clear(
|
||||
session: Session,
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(station) = Station::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(station) = Station::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_station", id = id));
|
||||
return Redirect::to("/admin/station");
|
||||
};
|
||||
|
||||
station.update_location_clear(&db).await;
|
||||
station.update_location_clear(db).await;
|
||||
|
||||
suc!(session, t!("location_deleted", station = station.name));
|
||||
|
||||
@@ -598,7 +638,9 @@ async fn update_location_clear(
|
||||
}
|
||||
|
||||
async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
let stations = Station::all(&db).await;
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let stations = Station::all(db).await;
|
||||
|
||||
let content = html! {
|
||||
h1 {
|
||||
@@ -632,7 +674,7 @@ async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
}
|
||||
tbody {
|
||||
@for station in &stations {
|
||||
@let status = TeamsAtStationLocation::for_station(&db, station).await;
|
||||
@let status = TeamsAtStationLocation::for_station(db, station).await;
|
||||
tr {
|
||||
td {
|
||||
@if station.ready {
|
||||
@@ -640,7 +682,7 @@ async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
small { "🟢 " }
|
||||
}
|
||||
}
|
||||
@if station.routes(&db).await.is_empty() {
|
||||
@if station.routes(db).await.is_empty() {
|
||||
a href="/admin/route" {
|
||||
em data-tooltip=(t!("station_warning_not_assigned_route")) {
|
||||
"⚠️ "
|
||||
@@ -658,7 +700,15 @@ async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
}
|
||||
td {
|
||||
em data-tooltip=(t!("station_team_progress", arrived=status.total_teams-status.not_yet_here.len() as i64, total=status.total_teams, waiting= status.waiting.len(), active=status.doing.len() )) {
|
||||
progress value=(status.total_teams-status.not_yet_here.len() as i64) max=(status.total_teams) {}
|
||||
@if status.not_yet_here.is_empty() {
|
||||
@if status.waiting.is_empty() && status.doing.is_empty() {
|
||||
"✅"
|
||||
}@else{
|
||||
"🔜"
|
||||
}
|
||||
} @else {
|
||||
progress value=(status.total_teams-status.not_yet_here.len() as i64) max=(status.total_teams) {}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -682,7 +732,7 @@ async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
}
|
||||
}
|
||||
};
|
||||
page(content, session, false).await
|
||||
PageBuilder::new(content, session).markup().await
|
||||
}
|
||||
|
||||
pub(super) fn routes() -> Router<AppState> {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::{
|
||||
admin::{route::Route, station::Station},
|
||||
models::rating::Rating,
|
||||
AppState,
|
||||
admin::{route::Route, station::Station},
|
||||
rating::Rating,
|
||||
};
|
||||
use axum::Router;
|
||||
use chrono::{DateTime, Local, NaiveDateTime, Utc};
|
||||
use maud::{html, Markup, Render};
|
||||
use maud::{Markup, Render, html};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
use sqlx::{FromRow, SqliteConnection};
|
||||
|
||||
mod web;
|
||||
|
||||
@@ -19,7 +19,7 @@ pub(crate) struct Team {
|
||||
pub(crate) amount_people: Option<i64>,
|
||||
first_station_id: i64,
|
||||
last_station_id: Option<i64>,
|
||||
route_id: i64,
|
||||
pub(crate) route_id: i64,
|
||||
}
|
||||
|
||||
impl Render for Team {
|
||||
@@ -40,7 +40,7 @@ pub(crate) struct LastContactTeam {
|
||||
}
|
||||
|
||||
impl LastContactTeam {
|
||||
pub(crate) async fn all_sort_missing(db: &SqlitePool) -> Vec<Self> {
|
||||
pub(crate) async fn all_sort_missing(db: &mut SqliteConnection) -> Vec<Self> {
|
||||
let rows = sqlx::query_as::<_, (i64, i64, Option<NaiveDateTime>)>(
|
||||
"SELECT
|
||||
t.id AS team_id,
|
||||
@@ -64,13 +64,15 @@ LEFT JOIN station s ON last_contact.station_id = s.id
|
||||
ORDER BY
|
||||
last_contact.last_contact_time DESC",
|
||||
)
|
||||
.fetch_all(db)
|
||||
.fetch_all(&mut *db)
|
||||
.await
|
||||
.unwrap();
|
||||
let mut ret = Vec::new();
|
||||
for (team_id, station_id, last_contact_time) in rows {
|
||||
ret.push(LastContactTeam {
|
||||
team: Team::find_by_id(db, team_id).await.expect("db constraints"),
|
||||
team: Team::find_by_id(&mut *db, team_id)
|
||||
.await
|
||||
.expect("db constraints"),
|
||||
station: Station::find_by_id(db, station_id).await,
|
||||
last_contact_time,
|
||||
});
|
||||
@@ -87,13 +89,14 @@ ORDER BY
|
||||
}
|
||||
}
|
||||
|
||||
enum CreateError {
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum CreateError {
|
||||
NoStationForRoute,
|
||||
DuplicateName(String),
|
||||
}
|
||||
|
||||
impl Team {
|
||||
pub(crate) async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
pub(crate) async fn all(db: &mut SqliteConnection) -> Vec<Self> {
|
||||
sqlx::query_as::<_, Self>(
|
||||
"SELECT id, name, notes, amount_people, first_station_id, last_station_id, route_id FROM team ORDER BY name;",
|
||||
)
|
||||
@@ -102,7 +105,7 @@ impl Team {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub(crate) async fn all_with_route(db: &SqlitePool, route: &Route) -> Vec<Self> {
|
||||
pub(crate) async fn all_with_route(db: &mut SqliteConnection, route: &Route) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Team,
|
||||
"SELECT id, name, notes, amount_people, first_station_id, last_station_id, route_id FROM team WHERE route_id = ?;",
|
||||
@@ -113,7 +116,10 @@ impl Team {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub(crate) async fn all_with_first_station(db: &SqlitePool, station: &Station) -> Vec<Self> {
|
||||
pub(crate) async fn all_with_first_station(
|
||||
db: &mut SqliteConnection,
|
||||
station: &Station,
|
||||
) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Team,
|
||||
"select id, name, notes, amount_people, first_station_id, last_station_id, route_id from team where first_station_id = ?;",
|
||||
@@ -124,7 +130,10 @@ impl Team {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub(crate) async fn all_with_last_station(db: &SqlitePool, station: &Station) -> Vec<Self> {
|
||||
pub(crate) async fn all_with_last_station(
|
||||
db: &mut SqliteConnection,
|
||||
station: &Station,
|
||||
) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Team,
|
||||
"select id, name, notes, amount_people, first_station_id, last_station_id, route_id from team where last_station_id = ?;",
|
||||
@@ -135,7 +144,7 @@ impl Team {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
pub async fn find_by_id(db: &mut SqliteConnection, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT id, name, notes, amount_people, first_station_id, last_station_id, route_id FROM team WHERE id = ?",
|
||||
@@ -146,7 +155,11 @@ impl Team {
|
||||
.ok()
|
||||
}
|
||||
|
||||
async fn create(db: &SqlitePool, name: &str, route: &Route) -> Result<i64, CreateError> {
|
||||
pub(crate) async fn create(
|
||||
db: &mut SqliteConnection,
|
||||
name: &str,
|
||||
route: &Route,
|
||||
) -> Result<i64, CreateError> {
|
||||
// get next station id which has the lowest amount of teams to have in the first place
|
||||
// assigned
|
||||
let Some(station) = route.get_next_first_station(db).await else {
|
||||
@@ -165,14 +178,14 @@ impl Team {
|
||||
Ok(result.id.unwrap())
|
||||
}
|
||||
|
||||
async fn update_name(&self, db: &SqlitePool, name: &str) {
|
||||
async fn update_name(&self, db: &mut SqliteConnection, name: &str) {
|
||||
sqlx::query!("UPDATE team SET name = ? WHERE id = ?", name, self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn update_end_station(&self, db: &SqlitePool, station: &Station) {
|
||||
async fn update_end_station(&self, db: &mut SqliteConnection, station: &Station) {
|
||||
sqlx::query!(
|
||||
"UPDATE team SET last_station_id = ? WHERE id = ?",
|
||||
station.id,
|
||||
@@ -183,14 +196,14 @@ impl Team {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn update_notes(&self, db: &SqlitePool, notes: &str) {
|
||||
async fn update_notes(&self, db: &mut SqliteConnection, notes: &str) {
|
||||
sqlx::query!("UPDATE team SET notes = ? WHERE id = ?", notes, self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn update_amount_people(&self, db: &SqlitePool, amount_people: i64) {
|
||||
async fn update_amount_people(&self, db: &mut SqliteConnection, amount_people: i64) {
|
||||
sqlx::query!(
|
||||
"UPDATE team SET amount_people = ? WHERE id = ?",
|
||||
amount_people,
|
||||
@@ -201,7 +214,7 @@ impl Team {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn update_route(&self, db: &SqlitePool, route: &Route) -> Result<String, ()> {
|
||||
async fn update_route(&self, db: &mut SqliteConnection, route: &Route) -> Result<String, ()> {
|
||||
let Some(station) = route.get_next_first_station(db).await else {
|
||||
return Err(());
|
||||
};
|
||||
@@ -219,7 +232,7 @@ impl Team {
|
||||
Ok(station.name)
|
||||
}
|
||||
|
||||
async fn update_first_station(&self, db: &SqlitePool, station: &Station) {
|
||||
pub(crate) async fn update_first_station(&self, db: &mut SqliteConnection, station: &Station) {
|
||||
sqlx::query!(
|
||||
"UPDATE team SET first_station_id = ? WHERE id = ?",
|
||||
station.id,
|
||||
@@ -230,7 +243,7 @@ impl Team {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn update_last_station(&self, db: &SqlitePool, station: &Station) {
|
||||
async fn update_last_station(&self, db: &mut SqliteConnection, station: &Station) {
|
||||
sqlx::query!(
|
||||
"UPDATE team SET last_station_id = ? WHERE id = ?",
|
||||
station.id,
|
||||
@@ -241,14 +254,14 @@ impl Team {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn update_amount_people_reset(&self, db: &SqlitePool) {
|
||||
async fn update_amount_people_reset(&self, db: &mut SqliteConnection) {
|
||||
sqlx::query!("UPDATE team SET amount_people = NULL WHERE id = ?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn delete(&self, db: &SqlitePool) -> Result<(), String> {
|
||||
async fn delete(&self, db: &mut SqliteConnection) -> Result<(), String> {
|
||||
sqlx::query!("DELETE FROM team WHERE id = ?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
@@ -256,13 +269,13 @@ impl Team {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn first_station(&self, db: &SqlitePool) -> Station {
|
||||
pub async fn first_station(&self, db: &mut SqliteConnection) -> Station {
|
||||
Station::find_by_id(db, self.first_station_id)
|
||||
.await
|
||||
.expect("db constraints")
|
||||
}
|
||||
|
||||
pub async fn last_station(&self, db: &SqlitePool) -> Option<Station> {
|
||||
pub async fn last_station(&self, db: &mut SqliteConnection) -> Option<Station> {
|
||||
if let Some(last_station_id) = self.last_station_id {
|
||||
Station::find_by_id(db, last_station_id).await
|
||||
} else {
|
||||
@@ -270,13 +283,13 @@ impl Team {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn route(&self, db: &SqlitePool) -> Route {
|
||||
pub async fn route(&self, db: &mut SqliteConnection) -> Route {
|
||||
Route::find_by_id(db, self.route_id)
|
||||
.await
|
||||
.expect("db constraints")
|
||||
}
|
||||
|
||||
pub async fn get_curr_points(&self, db: &SqlitePool) -> i64 {
|
||||
pub async fn get_curr_points(&self, db: &mut SqliteConnection) -> i64 {
|
||||
sqlx::query!(
|
||||
"SELECT IFNULL(sum(points), 0) as points FROM rating WHERE team_id = ?",
|
||||
self.id
|
||||
@@ -287,13 +300,13 @@ impl Team {
|
||||
.points
|
||||
}
|
||||
|
||||
pub async fn been_at_station(&self, db: &SqlitePool, station: &Station) -> bool {
|
||||
pub async fn been_at_station(&self, db: &mut SqliteConnection, station: &Station) -> bool {
|
||||
Rating::find_by_team_and_station(db, self, station)
|
||||
.await
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub(crate) async fn end_station(&self, db: &SqlitePool) -> Station {
|
||||
pub(crate) async fn end_station(&self, db: &mut SqliteConnection) -> Station {
|
||||
match LastContactTeam::all_sort_missing(db)
|
||||
.await
|
||||
.into_iter()
|
||||
@@ -334,7 +347,7 @@ impl Team {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn end_run(db: &SqlitePool) {
|
||||
pub async fn end_run(db: &mut SqliteConnection) {
|
||||
// set `last_station_id` to the next station where `left_at` is not null
|
||||
let teams = Team::all(db).await;
|
||||
for team in teams {
|
||||
@@ -343,7 +356,7 @@ impl Team {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn restart_run(db: &SqlitePool) {
|
||||
pub async fn restart_run(db: &mut SqliteConnection) {
|
||||
sqlx::query!("UPDATE team SET last_station_id = null")
|
||||
.execute(db)
|
||||
.await
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
use super::{CreateError, LastContactTeam, Team};
|
||||
use crate::{
|
||||
AppState, PageBuilder,
|
||||
admin::{route::Route, station::Station},
|
||||
er,
|
||||
models::rating::Rating,
|
||||
partials::page,
|
||||
suc, AppState,
|
||||
rating::Rating,
|
||||
suc,
|
||||
};
|
||||
use axum::{
|
||||
Form, Router,
|
||||
extract::State,
|
||||
response::{IntoResponse, Redirect},
|
||||
routing::{get, post},
|
||||
Form, Router,
|
||||
};
|
||||
use maud::{html, Markup, PreEscaped};
|
||||
use maud::{Markup, PreEscaped, html};
|
||||
use serde::Deserialize;
|
||||
use sqlx::SqlitePool;
|
||||
use sqlx::{SqliteConnection, SqlitePool};
|
||||
use std::fmt::Write;
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
use tower_sessions::Session;
|
||||
|
||||
@@ -29,13 +30,15 @@ async fn create(
|
||||
session: Session,
|
||||
Form(form): Form<CreateForm>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(route) = Route::find_by_id(&db, form.route_id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(route) = Route::find_by_id(db, form.route_id).await else {
|
||||
er!(session, t!("nonexisting_route", id = form.route_id));
|
||||
|
||||
return Redirect::to("/admin/team");
|
||||
};
|
||||
|
||||
let id = match Team::create(&db, &form.name, &route).await {
|
||||
let id = match Team::create(db, &form.name, &route).await {
|
||||
Ok(id) => {
|
||||
suc!(session, t!("team_created", team = form.name));
|
||||
id
|
||||
@@ -70,12 +73,14 @@ async fn delete(
|
||||
session: Session,
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(team) = Team::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(team) = Team::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_team", id = id));
|
||||
return Redirect::to("/admin/team");
|
||||
};
|
||||
|
||||
match team.delete(&db).await {
|
||||
match team.delete(db).await {
|
||||
Ok(()) => suc!(session, t!("team_deleted", team = team.name)),
|
||||
Err(e) => er!(
|
||||
session,
|
||||
@@ -86,7 +91,12 @@ async fn delete(
|
||||
Redirect::to("/admin/team")
|
||||
}
|
||||
|
||||
async fn quick(db: Arc<SqlitePool>, team: &Team, stations: Vec<Station>, redirect: &str) -> Markup {
|
||||
async fn quick(
|
||||
db: &mut SqliteConnection,
|
||||
team: &Team,
|
||||
stations: Vec<Station>,
|
||||
redirect: &str,
|
||||
) -> Markup {
|
||||
html! {
|
||||
h1 {
|
||||
a href=(format!("/admin/team/{}", team.id)) { "↩️" }
|
||||
@@ -114,7 +124,7 @@ async fn quick(db: Arc<SqlitePool>, team: &Team, stations: Vec<Station>, redirec
|
||||
}
|
||||
}
|
||||
td {
|
||||
@if let Some(rating) = Rating::find_by_team_and_station(&db, team, station).await {
|
||||
@if let Some(rating) = Rating::find_by_team_and_station(db, team, station).await {
|
||||
a href=(format!("/s/{}/{}", station.id, station.pw)){
|
||||
@if let Some(points) = rating.points {
|
||||
em data-tooltip=(t!("already_entered")) {
|
||||
@@ -142,16 +152,21 @@ async fn quick_crewless(
|
||||
session: Session,
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
) -> Result<Markup, impl IntoResponse> {
|
||||
let Some(team) = Team::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(team) = Team::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_team", id = id));
|
||||
return Err(Redirect::to("/admin/team"));
|
||||
};
|
||||
|
||||
let stations: Vec<Station> = team.route(&db).await.crewless_stations(&db).await;
|
||||
let stations: Vec<Station> = team.route(db).await.crewless_stations(db).await;
|
||||
|
||||
let content = quick(db, &team, stations, "/crewless").await;
|
||||
|
||||
Ok(page(content, session, true).await)
|
||||
Ok(PageBuilder::new(content, session)
|
||||
.with_leaflet()
|
||||
.markup()
|
||||
.await)
|
||||
}
|
||||
|
||||
async fn quick_all(
|
||||
@@ -159,16 +174,21 @@ async fn quick_all(
|
||||
session: Session,
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
) -> Result<Markup, impl IntoResponse> {
|
||||
let Some(team) = Team::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(team) = Team::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_team", id = id));
|
||||
return Err(Redirect::to("/admin/team"));
|
||||
};
|
||||
|
||||
let stations = team.route(&db).await.stations(&db).await;
|
||||
let stations = team.route(db).await.stations(db).await;
|
||||
|
||||
let content = quick(db, &team, stations, "").await;
|
||||
|
||||
Ok(page(content, session, true).await)
|
||||
Ok(PageBuilder::new(content, session)
|
||||
.with_leaflet()
|
||||
.markup()
|
||||
.await)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -183,7 +203,9 @@ async fn quick_post(
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
Form(form): Form<QuickUpdate>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(team) = Team::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(team) = Team::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_team", id = id));
|
||||
return Redirect::to("/admin/team");
|
||||
};
|
||||
@@ -193,24 +215,27 @@ async fn quick_post(
|
||||
|
||||
for (station_id, points) in &form.fields {
|
||||
let Ok(station_id) = station_id.parse::<i64>() else {
|
||||
ret.push_str(&format!(
|
||||
let _ = write!(
|
||||
ret,
|
||||
"Skipped stationid={station_id} because this id can't be parsed as i64"
|
||||
));
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let Ok(points) = points.parse::<i64>() else {
|
||||
ret.push_str(&format!(
|
||||
"Skipped stationid={station_id} because points {points} can't be parsed as i64",
|
||||
));
|
||||
let _ = write!(
|
||||
ret,
|
||||
"Skipped stationid={station_id} because {points} points can't be parsed as i64",
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let Some(station) = Station::find_by_id(&db, station_id).await else {
|
||||
ret.push_str(&format!(
|
||||
let Some(station) = Station::find_by_id(&mut *db, station_id).await else {
|
||||
let _ = write!(
|
||||
ret,
|
||||
"Skipped stationid={station_id} because this station does not exist"
|
||||
));
|
||||
);
|
||||
continue;
|
||||
};
|
||||
if Rating::find_by_team_and_station(&db, &team, &station)
|
||||
if Rating::find_by_team_and_station(db, &team, &station)
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
@@ -224,7 +249,7 @@ async fn quick_post(
|
||||
continue;
|
||||
}
|
||||
|
||||
Rating::create_quick(&db, &team, &station, points).await;
|
||||
Rating::create_quick(db, &team, &station, points).await;
|
||||
amount_succ += 1;
|
||||
}
|
||||
|
||||
@@ -247,15 +272,17 @@ async fn view(
|
||||
session: Session,
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
) -> Result<Markup, impl IntoResponse> {
|
||||
let Some(team) = Team::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(team) = Team::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_team", id = id));
|
||||
return Err(Redirect::to("/admin/team"));
|
||||
};
|
||||
let first_station = team.first_station(&db).await;
|
||||
let last_station = team.last_station(&db).await;
|
||||
let routes = Route::all(&db).await;
|
||||
let first_station = team.first_station(db).await;
|
||||
let last_station = team.last_station(db).await;
|
||||
let routes = Route::all(db).await;
|
||||
|
||||
let stations = team.route(&db).await.crewful_stations(&db).await;
|
||||
let stations = team.route(db).await.crewful_stations(db).await;
|
||||
|
||||
// maybe switch to maud-display impl of team
|
||||
let content = html! {
|
||||
@@ -281,7 +308,12 @@ async fn view(
|
||||
table {
|
||||
tbody {
|
||||
tr {
|
||||
th scope="row" { (t!("notes")) };
|
||||
th scope="row" {
|
||||
(t!("notes"))
|
||||
article {
|
||||
(t!("team_notes_expl"))
|
||||
}
|
||||
};
|
||||
td {
|
||||
@match &team.notes {
|
||||
Some(notes) => {
|
||||
@@ -330,8 +362,8 @@ async fn view(
|
||||
tr {
|
||||
th scope="row" { (t!("route")) };
|
||||
td {
|
||||
a href=(format!("/admin/route/{}", &team.route(&db).await.id)) {
|
||||
(&team.route(&db).await.name)
|
||||
a href=(format!("/admin/route/{}", &team.route(db).await.id)) {
|
||||
(&team.route(db).await.name)
|
||||
}
|
||||
@if routes.len() > 1 {
|
||||
details {
|
||||
@@ -339,7 +371,7 @@ async fn view(
|
||||
form action=(format!("/admin/team/{}/update-route", team.id)) method="post" {
|
||||
select name="route_id" aria-label=(t!("select_route")) required {
|
||||
@for route in &routes {
|
||||
@if route.id != team.route(&db).await.id {
|
||||
@if route.id != team.route(db).await.id {
|
||||
option value=(route.id) {
|
||||
(route.name)
|
||||
}
|
||||
@@ -372,12 +404,16 @@ async fn view(
|
||||
@if station.id != first_station.id {
|
||||
option value=(station.id) {
|
||||
(station.name)
|
||||
@let amount_start_teams = Team::all_with_first_station(&db, station).await.len();
|
||||
@let amount_start_teams = Team::all_with_first_station(db, station).await.len();
|
||||
@if amount_start_teams > 0 {
|
||||
@if amount_start_teams == 1 {
|
||||
" ("
|
||||
(t!("already_has_1_start_team"))
|
||||
")"
|
||||
}@else{
|
||||
" ("
|
||||
(t!("already_has_n_start_team", amount=amount_start_teams))
|
||||
")"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -434,7 +470,10 @@ async fn view(
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(page(content, session, true).await)
|
||||
Ok(PageBuilder::new(content, session)
|
||||
.with_leaflet()
|
||||
.markup()
|
||||
.await)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -447,12 +486,14 @@ async fn update_name(
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
Form(form): Form<UpdateNameForm>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(team) = Team::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(team) = Team::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_team", id = id));
|
||||
return Redirect::to("/admin/team");
|
||||
};
|
||||
|
||||
team.update_name(&db, &form.name).await;
|
||||
team.update_name(db, &form.name).await;
|
||||
|
||||
suc!(
|
||||
session,
|
||||
@@ -472,12 +513,14 @@ async fn update_notes(
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
Form(form): Form<UpdateNotesForm>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(team) = Team::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(team) = Team::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_team", id = id));
|
||||
return Redirect::to("/admin/team");
|
||||
};
|
||||
|
||||
team.update_notes(&db, &form.notes).await;
|
||||
team.update_notes(db, &form.notes).await;
|
||||
|
||||
suc!(session, t!("notes_edited", team = team.name));
|
||||
|
||||
@@ -494,12 +537,14 @@ async fn update_amount_people(
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
Form(form): Form<UpdateAmountPeopleForm>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(team) = Team::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(team) = Team::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_team", id = id));
|
||||
return Redirect::to("/admin/team");
|
||||
};
|
||||
|
||||
team.update_amount_people(&db, form.amount_people).await;
|
||||
team.update_amount_people(db, form.amount_people).await;
|
||||
|
||||
suc!(session, t!("amount_teammembers_edited", team = team.name));
|
||||
|
||||
@@ -516,18 +561,20 @@ async fn update_route(
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
Form(form): Form<UpdateRouteForm>,
|
||||
) -> impl IntoResponse {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
// TODO: move sanity checks into mod.rs
|
||||
let Some(team) = Team::find_by_id(&db, id).await else {
|
||||
let Some(team) = Team::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_team", id = id));
|
||||
return Redirect::to("/admin/team");
|
||||
};
|
||||
|
||||
let Some(route) = Route::find_by_id(&db, form.route_id).await else {
|
||||
let Some(route) = Route::find_by_id(db, form.route_id).await else {
|
||||
er!(session, t!("nonexisting_route", id = form.route_id));
|
||||
return Redirect::to(&format!("/admin/team/{id}"));
|
||||
};
|
||||
|
||||
match team.update_route(&db, &route).await {
|
||||
match team.update_route(db, &route).await {
|
||||
Ok(new_first_station_name) => suc!(
|
||||
session,
|
||||
t!(
|
||||
@@ -560,12 +607,14 @@ async fn update_first_station(
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
Form(form): Form<UpdateFirstStationForm>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(team) = Team::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(team) = Team::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_team", id = id));
|
||||
return Redirect::to("/admin/team");
|
||||
};
|
||||
|
||||
let Some(station) = Station::find_by_id(&db, form.first_station_id).await else {
|
||||
let Some(station) = Station::find_by_id(&mut *db, form.first_station_id).await else {
|
||||
er!(
|
||||
session,
|
||||
t!("nonexisting_station", id = form.first_station_id)
|
||||
@@ -574,21 +623,22 @@ async fn update_first_station(
|
||||
return Redirect::to(&format!("/admin/team/{id}"));
|
||||
};
|
||||
|
||||
if !station.is_in_route(&db, &team.route(&db).await).await {
|
||||
let route = team.route(db).await;
|
||||
if !station.is_in_route(db, &route).await {
|
||||
er!(
|
||||
session,
|
||||
t!(
|
||||
"first_station_not_edited_not_on_route",
|
||||
station = station.name,
|
||||
team = team.name,
|
||||
route = team.route(&db).await.name
|
||||
route = team.route(db).await.name
|
||||
)
|
||||
);
|
||||
|
||||
return Redirect::to(&format!("/admin/team/{id}"));
|
||||
}
|
||||
|
||||
team.update_first_station(&db, &station).await;
|
||||
team.update_first_station(db, &station).await;
|
||||
|
||||
suc!(
|
||||
session,
|
||||
@@ -612,12 +662,14 @@ async fn update_last_station(
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
Form(form): Form<UpdateLastStationForm>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(team) = Team::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(team) = Team::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_team", id = id));
|
||||
return Redirect::to("/admin/team");
|
||||
};
|
||||
|
||||
let Some(station) = Station::find_by_id(&db, form.last_station_id).await else {
|
||||
let Some(station) = Station::find_by_id(&mut *db, form.last_station_id).await else {
|
||||
er!(
|
||||
session,
|
||||
t!("nonexisting_station", id = form.last_station_id)
|
||||
@@ -625,21 +677,22 @@ async fn update_last_station(
|
||||
return Redirect::to(&format!("/admin/team/{id}"));
|
||||
};
|
||||
|
||||
if !station.is_in_route(&db, &team.route(&db).await).await {
|
||||
let route = team.route(db).await;
|
||||
if !station.is_in_route(db, &route).await {
|
||||
er!(
|
||||
session,
|
||||
t!(
|
||||
"last_station_not_edited_not_on_route",
|
||||
station = station.name,
|
||||
team = team.name,
|
||||
route = team.route(&db).await.name
|
||||
route = team.route(db).await.name
|
||||
)
|
||||
);
|
||||
|
||||
return Redirect::to(&format!("/admin/team/{id}"));
|
||||
}
|
||||
|
||||
team.update_last_station(&db, &station).await;
|
||||
team.update_last_station(db, &station).await;
|
||||
|
||||
suc!(
|
||||
session,
|
||||
@@ -658,12 +711,14 @@ async fn update_amount_people_reset(
|
||||
session: Session,
|
||||
axum::extract::Path(id): axum::extract::Path<i64>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(team) = Team::find_by_id(&db, id).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(team) = Team::find_by_id(db, id).await else {
|
||||
er!(session, t!("nonexisting_team", id = id));
|
||||
return Redirect::to("/admin/team");
|
||||
};
|
||||
|
||||
team.update_amount_people_reset(&db).await;
|
||||
team.update_amount_people_reset(db).await;
|
||||
|
||||
suc!(session, t!("amount_teammembers_edited", team = team.name));
|
||||
|
||||
@@ -671,7 +726,9 @@ async fn update_amount_people_reset(
|
||||
}
|
||||
|
||||
async fn lost(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
let losts = LastContactTeam::all_sort_missing(&db).await;
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let losts = LastContactTeam::all_sort_missing(db).await;
|
||||
|
||||
let content = html! {
|
||||
h1 {
|
||||
@@ -697,7 +754,7 @@ async fn lost(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
}
|
||||
td {
|
||||
@if let Some(time) = lost.local_last_contact() {
|
||||
(time)
|
||||
(time.format("%H:%M"))
|
||||
}@else{
|
||||
(t!("not_yet_seen"))
|
||||
}
|
||||
@@ -718,12 +775,14 @@ async fn lost(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
}
|
||||
};
|
||||
|
||||
page(content, session, false).await
|
||||
PageBuilder::new(content, session).markup().await
|
||||
}
|
||||
|
||||
async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
let teams = Team::all(&db).await;
|
||||
let routes = Route::all(&db).await;
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let teams = Team::all(db).await;
|
||||
let routes = Route::all(db).await;
|
||||
|
||||
let content = html! {
|
||||
h1 {
|
||||
@@ -777,13 +836,13 @@ async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
}
|
||||
a href="/admin/team/lost" {
|
||||
button class="outline" {
|
||||
(t!("have_i_lost_groups"))
|
||||
(t!("have_i_lost_teams"))
|
||||
}
|
||||
}
|
||||
@for route in &routes {
|
||||
h2 { (route.name) }
|
||||
ol {
|
||||
@for team in &route.teams(&db).await{
|
||||
@for team in &route.teams(db).await{
|
||||
li {
|
||||
a href=(format!("/admin/team/{}", team.id)){
|
||||
(team.name)
|
||||
@@ -797,7 +856,8 @@ async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
}
|
||||
}
|
||||
};
|
||||
page(content, session, false).await
|
||||
|
||||
PageBuilder::new(content, session).markup().await
|
||||
}
|
||||
|
||||
pub(super) fn routes() -> Router<AppState> {
|
||||
|
||||
@@ -41,11 +41,11 @@ impl User {
|
||||
Ok(result.id)
|
||||
}
|
||||
|
||||
async fn update_name(&self, db: &SqlitePool, name: &str) {
|
||||
async fn update_name(&self, db: &SqlitePool, name: &str) -> bool {
|
||||
sqlx::query!("UPDATE user SET name = ? WHERE id = ?", name, self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
async fn new_pw(&self, db: &SqlitePool) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{auth::User, er, partials::page, suc, AppState};
|
||||
use crate::{auth::User, er, suc, AppState, PageBuilder};
|
||||
use axum::{
|
||||
extract::State,
|
||||
response::{IntoResponse, Redirect},
|
||||
@@ -119,7 +119,8 @@ async fn view(
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(page(content, session, true).await)
|
||||
|
||||
Ok(PageBuilder::new(content, session).markup().await)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -137,12 +138,14 @@ async fn update_name(
|
||||
return Redirect::to("/admin/user");
|
||||
};
|
||||
|
||||
user.update_name(&db, &form.name).await;
|
||||
|
||||
suc!(
|
||||
session,
|
||||
t!("new_user_name", old = user.name, new = form.name)
|
||||
);
|
||||
if user.update_name(&db, &form.name).await {
|
||||
suc!(
|
||||
session,
|
||||
t!("new_user_name", old = user.name, new = form.name)
|
||||
);
|
||||
} else {
|
||||
er!(session, t!("user_name_already_exists", new = form.name));
|
||||
}
|
||||
|
||||
Redirect::to(&format!("/admin/user/{id}"))
|
||||
}
|
||||
@@ -200,7 +203,8 @@ async fn index(State(db): State<Arc<SqlitePool>>, session: Session) -> Markup {
|
||||
}
|
||||
}
|
||||
};
|
||||
page(content, session, false).await
|
||||
|
||||
PageBuilder::new(content, session).markup().await
|
||||
}
|
||||
|
||||
pub(super) fn routes() -> Router<AppState> {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use crate::{er, page, suc, AppState};
|
||||
use crate::{AppState, PageBuilder, er, suc};
|
||||
use async_trait::async_trait;
|
||||
use axum::{
|
||||
Form, Router,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Redirect},
|
||||
routing::{get, post},
|
||||
Form, Router,
|
||||
};
|
||||
use axum_login::{AuthUser, AuthnBackend};
|
||||
use maud::{html, Markup};
|
||||
use maud::{Markup, html};
|
||||
use password_auth::verify_password;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
@@ -119,7 +119,7 @@ async fn login(session: Session) -> Markup {
|
||||
}
|
||||
};
|
||||
|
||||
page(content, session, false).await
|
||||
PageBuilder::new(content, session).markup().await
|
||||
}
|
||||
|
||||
pub async fn login_post(
|
||||
|
||||
16
src/lib.rs
16
src/lib.rs
@@ -16,29 +16,29 @@ macro_rules! testdb {
|
||||
|
||||
i18n!("locales", fallback = "de-AT");
|
||||
|
||||
use admin::station::{print::station_pdf, Station};
|
||||
use admin::station::{Station, print::station_pdf};
|
||||
use auth::{AuthSession, Backend, User};
|
||||
use axum::{
|
||||
Form, Router,
|
||||
body::Body,
|
||||
extract::{FromRef, State},
|
||||
response::{IntoResponse, Redirect, Response},
|
||||
routing::{get, post},
|
||||
Form, Router,
|
||||
};
|
||||
use axum_login::AuthManagerLayerBuilder;
|
||||
use maud::{html, Markup};
|
||||
use partials::page;
|
||||
use maud::{Markup, html};
|
||||
use partials::PageBuilder;
|
||||
use serde::Deserialize;
|
||||
use sqlx::SqlitePool;
|
||||
use std::{env, sync::Arc};
|
||||
use tokio::net::TcpListener;
|
||||
use tower_sessions::{cookie::time::Duration, Expiry, Session, SessionManagerLayer};
|
||||
use tower_sessions::{Expiry, Session, SessionManagerLayer, cookie::time::Duration};
|
||||
use tower_sessions_sqlx_store_chrono::SqliteStore;
|
||||
|
||||
pub(crate) mod admin;
|
||||
mod auth;
|
||||
pub(crate) mod models;
|
||||
mod partials;
|
||||
pub(crate) mod rating;
|
||||
pub(crate) mod station;
|
||||
|
||||
pub(crate) fn test_version() -> bool {
|
||||
@@ -212,7 +212,7 @@ async fn set_pw(
|
||||
}
|
||||
};
|
||||
|
||||
Ok(page(content, session, false).await)
|
||||
Ok(PageBuilder::new(content, session).markup().await)
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
struct NewPwForm {
|
||||
@@ -302,7 +302,7 @@ pub async fn start(listener: TcpListener, db: SqlitePool) {
|
||||
|
||||
tokio::spawn(async move {
|
||||
// Kick-off typst compilation, to reduce wait time for 1st load
|
||||
let stations = Station::all(&db).await;
|
||||
let stations = Station::all(&mut db.acquire().await.unwrap()).await;
|
||||
station_pdf(stations).await;
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use dotenv::dotenv;
|
||||
use sqlx::{pool::PoolOptions, SqlitePool};
|
||||
use sqlx::{SqlitePool, pool::PoolOptions};
|
||||
use std::env;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub(crate) mod rating;
|
||||
127
src/partials.rs
127
src/partials.rs
@@ -2,34 +2,71 @@ use crate::test_version;
|
||||
use maud::{DOCTYPE, Markup, html};
|
||||
use tower_sessions::Session;
|
||||
|
||||
pub(crate) async fn page(content: Markup, session: Session, leaflet: bool) -> Markup {
|
||||
// Get and clear flash message
|
||||
let succ_msg = session.get::<String>("succ").await.unwrap_or(None);
|
||||
if succ_msg.is_some() {
|
||||
session.remove::<String>("succ").await.unwrap();
|
||||
}
|
||||
let warn_msg = session.get::<String>("warn").await.unwrap_or(None);
|
||||
if warn_msg.is_some() {
|
||||
session.remove::<String>("warn").await.unwrap();
|
||||
}
|
||||
let err_msg = session.get::<String>("err").await.unwrap_or(None);
|
||||
if err_msg.is_some() {
|
||||
session.remove::<String>("err").await.unwrap();
|
||||
pub(crate) struct PageBuilder {
|
||||
content: Markup,
|
||||
session: Session,
|
||||
leaflet: bool,
|
||||
full: bool,
|
||||
}
|
||||
|
||||
impl PageBuilder {
|
||||
pub fn new(content: Markup, session: Session) -> Self {
|
||||
Self {
|
||||
content,
|
||||
session,
|
||||
leaflet: false,
|
||||
full: false,
|
||||
}
|
||||
}
|
||||
|
||||
html! {
|
||||
(DOCTYPE)
|
||||
head {
|
||||
meta charset="utf-8";
|
||||
meta name="viewport" content="width=device-width, initial-scale=1.0";
|
||||
link rel="stylesheet" href="/pico.css";
|
||||
link rel="stylesheet" href="/style.css";
|
||||
@if leaflet {
|
||||
link rel="stylesheet" href="/leaflet.css";
|
||||
}
|
||||
@if test_version() {
|
||||
style {
|
||||
r#"
|
||||
#[must_use]
|
||||
pub fn with_leaflet(self) -> Self {
|
||||
Self {
|
||||
leaflet: true,
|
||||
..self
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn set_leaflet(self, leaflet: bool) -> Self {
|
||||
Self { leaflet, ..self }
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn full_page(self) -> Self {
|
||||
Self { full: true, ..self }
|
||||
}
|
||||
|
||||
pub async fn markup(self) -> Markup {
|
||||
// Get and clear flash message
|
||||
let succ_msg = self.session.get::<String>("succ").await.unwrap_or(None);
|
||||
if succ_msg.is_some() {
|
||||
self.session.remove::<String>("succ").await.unwrap();
|
||||
}
|
||||
let warn_msg = self.session.get::<String>("warn").await.unwrap_or(None);
|
||||
if warn_msg.is_some() {
|
||||
self.session.remove::<String>("warn").await.unwrap();
|
||||
}
|
||||
let err_msg = self.session.get::<String>("err").await.unwrap_or(None);
|
||||
if err_msg.is_some() {
|
||||
self.session.remove::<String>("err").await.unwrap();
|
||||
}
|
||||
|
||||
let main_style = if self.full { "max-width: 98%;" } else { "" };
|
||||
|
||||
html! {
|
||||
(DOCTYPE)
|
||||
head {
|
||||
meta charset="utf-8";
|
||||
meta name="viewport" content="width=device-width, initial-scale=1.0";
|
||||
link rel="stylesheet" href="/pico.css";
|
||||
link rel="stylesheet" href="/style.css";
|
||||
@if self.leaflet {
|
||||
link rel="stylesheet" href="/leaflet.css";
|
||||
}
|
||||
@if test_version() {
|
||||
style {
|
||||
r#"
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
@@ -42,30 +79,32 @@ body {
|
||||
rgba(255, 255, 0, 0.3) 40px
|
||||
);
|
||||
}"#
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
body {
|
||||
main class="container" {
|
||||
@if let Some(message) = err_msg {
|
||||
article class="error" {
|
||||
(message)
|
||||
body {
|
||||
main class="container" style=(main_style)
|
||||
{
|
||||
@if let Some(message) = err_msg {
|
||||
article class="error" {
|
||||
(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@if let Some(message) = warn_msg {
|
||||
article class="warning" {
|
||||
(message)
|
||||
@if let Some(message) = warn_msg {
|
||||
article class="warning" {
|
||||
(message)
|
||||
}
|
||||
}
|
||||
}
|
||||
@if let Some(message) = succ_msg {
|
||||
article class="succ" {
|
||||
(message)
|
||||
@if let Some(message) = succ_msg {
|
||||
article class="succ" {
|
||||
(message)
|
||||
}
|
||||
}
|
||||
@if self.leaflet {
|
||||
script src="/leaflet.js" {};
|
||||
}
|
||||
(self.content)
|
||||
}
|
||||
@if leaflet {
|
||||
script src="/leaflet.js" {};
|
||||
}
|
||||
(content)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
use crate::{admin::team::Team, Station};
|
||||
use crate::{
|
||||
Station,
|
||||
admin::{route::Route, team::Team},
|
||||
};
|
||||
use chrono::{DateTime, Local, NaiveDateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
use sqlx::{FromRow, SqliteConnection};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct Rating {
|
||||
@@ -16,7 +20,7 @@ pub(crate) struct Rating {
|
||||
|
||||
impl Rating {
|
||||
pub(crate) async fn create(
|
||||
db: &SqlitePool,
|
||||
db: &mut SqliteConnection,
|
||||
station: &Station,
|
||||
team: &Team,
|
||||
) -> Result<(), String> {
|
||||
@@ -31,7 +35,12 @@ impl Rating {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn create_quick(db: &SqlitePool, team: &Team, station: &Station, points: i64) {
|
||||
pub(crate) async fn create_quick(
|
||||
db: &mut SqliteConnection,
|
||||
team: &Team,
|
||||
station: &Station,
|
||||
points: i64,
|
||||
) {
|
||||
sqlx::query!(
|
||||
"INSERT INTO rating(team_id, station_id, points, arrived_at, started_at, left_at) VALUES (?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)",
|
||||
team.id,
|
||||
@@ -43,19 +52,19 @@ impl Rating {
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub(crate) async fn team(&self, db: &SqlitePool) -> Team {
|
||||
pub(crate) async fn team(&self, db: &mut SqliteConnection) -> Team {
|
||||
Team::find_by_id(db, self.team_id)
|
||||
.await
|
||||
.expect("db constraints")
|
||||
}
|
||||
|
||||
pub(crate) async fn station(&self, db: &SqlitePool) -> Station {
|
||||
pub(crate) async fn station(&self, db: &mut SqliteConnection) -> Station {
|
||||
Station::find_by_id(db, self.station_id)
|
||||
.await
|
||||
.expect("db constraints")
|
||||
}
|
||||
|
||||
pub(crate) async fn for_station(db: &SqlitePool, station: &Station) -> Vec<Self> {
|
||||
pub(crate) async fn for_station(db: &mut SqliteConnection, station: &Station) -> Vec<Self> {
|
||||
sqlx::query_as::<_, Self>("SELECT team_id, station_id, points, notes, arrived_at, started_at, left_at FROM rating WHERE station_id = ?;")
|
||||
.bind(station.id)
|
||||
.fetch_all(db)
|
||||
@@ -64,7 +73,7 @@ impl Rating {
|
||||
}
|
||||
|
||||
pub(crate) async fn update(
|
||||
db: &SqlitePool,
|
||||
db: &mut SqliteConnection,
|
||||
station: &Station,
|
||||
team: &Team,
|
||||
points: Option<i64>,
|
||||
@@ -84,7 +93,7 @@ impl Rating {
|
||||
}
|
||||
|
||||
pub(crate) async fn delete(
|
||||
db: &SqlitePool,
|
||||
db: &mut SqliteConnection,
|
||||
station: &Station,
|
||||
team: &Team,
|
||||
) -> Result<(), String> {
|
||||
@@ -99,7 +108,7 @@ impl Rating {
|
||||
Ok(())
|
||||
}
|
||||
pub async fn find_by_team_and_station(
|
||||
db: &SqlitePool,
|
||||
db: &mut SqliteConnection,
|
||||
team: &Team,
|
||||
station: &Station,
|
||||
) -> Option<Self> {
|
||||
@@ -139,14 +148,19 @@ impl Rating {
|
||||
pub(crate) struct TeamsAtStationLocation {
|
||||
pub(crate) total_teams: i64,
|
||||
pub(crate) not_yet_here: Vec<Team>,
|
||||
pub(crate) not_yet_here_by_route: HashMap<Route, Vec<Team>>,
|
||||
pub(crate) waiting: Vec<(Team, Rating)>,
|
||||
pub(crate) doing: Vec<(Team, Rating)>,
|
||||
pub(crate) left_not_yet_rated: Vec<(Team, Rating)>,
|
||||
pub(crate) left_and_rated: Vec<(Team, Rating)>,
|
||||
pub(crate) done: bool,
|
||||
}
|
||||
|
||||
impl TeamsAtStationLocation {
|
||||
pub(crate) async fn for_station(db: &SqlitePool, station: &Station) -> TeamsAtStationLocation {
|
||||
pub(crate) async fn for_station(
|
||||
db: &mut SqliteConnection,
|
||||
station: &Station,
|
||||
) -> TeamsAtStationLocation {
|
||||
let teams = station.teams(db).await;
|
||||
let total_teams = teams.len() as i64;
|
||||
|
||||
@@ -156,32 +170,47 @@ impl TeamsAtStationLocation {
|
||||
let mut left_not_yet_rated = Vec::new();
|
||||
let mut left_and_rated = Vec::new();
|
||||
|
||||
let mut done = true;
|
||||
|
||||
for team in teams {
|
||||
match Rating::find_by_team_and_station(db, &team, station).await {
|
||||
Some(rating) => {
|
||||
if rating.left_at.is_some() {
|
||||
if rating.points.is_some() {
|
||||
left_and_rated.push((team, rating));
|
||||
} else {
|
||||
left_not_yet_rated.push((team, rating));
|
||||
}
|
||||
} else if rating.started_at.is_some() {
|
||||
doing.push((team, rating));
|
||||
if let Some(rating) = Rating::find_by_team_and_station(db, &team, station).await {
|
||||
if rating.left_at.is_some() {
|
||||
if rating.points.is_some() {
|
||||
left_and_rated.push((team, rating));
|
||||
} else {
|
||||
waiting.push((team, rating));
|
||||
done = false;
|
||||
left_not_yet_rated.push((team, rating));
|
||||
}
|
||||
} else if rating.started_at.is_some() {
|
||||
done = false;
|
||||
doing.push((team, rating));
|
||||
} else {
|
||||
done = false;
|
||||
waiting.push((team, rating));
|
||||
}
|
||||
None => not_yet_here.push(team),
|
||||
} else {
|
||||
done = false;
|
||||
not_yet_here.push(team);
|
||||
}
|
||||
}
|
||||
|
||||
let mut not_yet_here_by_route = HashMap::new();
|
||||
for team in ¬_yet_here {
|
||||
not_yet_here_by_route
|
||||
.entry(team.route(db).await)
|
||||
.or_insert_with(Vec::new)
|
||||
.push(team.clone());
|
||||
}
|
||||
|
||||
TeamsAtStationLocation {
|
||||
total_teams,
|
||||
not_yet_here,
|
||||
not_yet_here_by_route,
|
||||
waiting,
|
||||
doing,
|
||||
left_not_yet_rated,
|
||||
left_and_rated,
|
||||
done,
|
||||
}
|
||||
}
|
||||
}
|
||||
540
src/station.rs
540
src/station.rs
@@ -1,16 +1,17 @@
|
||||
use crate::{
|
||||
admin::{team::Team, RunStatus},
|
||||
AppState, PageBuilder, Station,
|
||||
admin::{RunStatus, team::Team},
|
||||
er, err,
|
||||
models::rating::TeamsAtStationLocation,
|
||||
partials, suc, AppState, Station,
|
||||
rating::TeamsAtStationLocation,
|
||||
suc,
|
||||
};
|
||||
use axum::{
|
||||
Form, Router,
|
||||
extract::State,
|
||||
response::{IntoResponse, Redirect},
|
||||
routing::{get, post},
|
||||
Form, Router,
|
||||
};
|
||||
use maud::{html, Markup, PreEscaped};
|
||||
use maud::{Markup, PreEscaped, html};
|
||||
use serde::Deserialize;
|
||||
use sqlx::SqlitePool;
|
||||
use std::sync::Arc;
|
||||
@@ -21,33 +22,34 @@ async fn view(
|
||||
session: Session,
|
||||
axum::extract::Path((id, code)): axum::extract::Path<(i64, String)>,
|
||||
) -> Markup {
|
||||
let Some(station) = Station::login(&db, id, &code).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(station) = Station::login(db, id, &code).await else {
|
||||
let content = html! {
|
||||
article class="error" {
|
||||
(t!("invalid_rating_code"))
|
||||
}
|
||||
};
|
||||
return partials::page(content, session, false).await;
|
||||
return PageBuilder::new(content, session).markup().await;
|
||||
};
|
||||
|
||||
let teams = TeamsAtStationLocation::for_station(&db, &station).await;
|
||||
let teams_on_the_way = station.teams_on_the_way(&db).await;
|
||||
let status = RunStatus::curr(&db).await;
|
||||
let teams = TeamsAtStationLocation::for_station(db, &station).await;
|
||||
let teams_on_the_way = station.teams_on_the_way(db).await;
|
||||
let status = RunStatus::curr(db).await;
|
||||
|
||||
let content = html! {
|
||||
h1 {
|
||||
(t!("station"))
|
||||
" "
|
||||
(station.name)
|
||||
}
|
||||
@if let (Some(lat), Some(lng)) = (station.lat, station.lng) {
|
||||
h1 {
|
||||
(t!("station"))
|
||||
" "
|
||||
(station.name)
|
||||
}
|
||||
article {
|
||||
details open[(!station.ready)]{
|
||||
details class="mb-0" open[(!station.ready)]{
|
||||
summary { (t!("infos")) }
|
||||
"👋"
|
||||
"👋 "
|
||||
(t!("station_info"))
|
||||
" "
|
||||
@let first_teams = Team::all_with_first_station(&db, &station).await;
|
||||
@let first_teams = Team::all_with_first_station(db, &station).await;
|
||||
@if first_teams.is_empty() {
|
||||
(t!("station_has_no_teams_to_take_to_start"))
|
||||
} @else{
|
||||
@@ -72,20 +74,31 @@ async fn view(
|
||||
}
|
||||
}
|
||||
}
|
||||
@if let Some(notes) = team.notes {
|
||||
@if let Some(notes) = &team.notes {
|
||||
li {
|
||||
(notes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@if teams.not_yet_here.contains(&team) {
|
||||
form action=(format!("/s/{id}/{code}/new-waiting")) method="post" {
|
||||
input type="hidden" name="team_id" value=(team.id);
|
||||
input type="submit" value=(t!("team_is_here"));
|
||||
}
|
||||
} @else if teams.waiting.iter().any(|(t, _)| t == &team) {
|
||||
a href=(format!("/s/{id}/{code}/remove-waiting/{}", team.id))
|
||||
onclick=(format!("return confirm('{}');", t!("confirm_station_cancel_team_waiting", team=team.name))) {
|
||||
"🗑️"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(t!("your_station_is_here"))
|
||||
div id="map" style="height: 500px" {}
|
||||
script { (format!("
|
||||
@if let (Some(lat), Some(lng)) = (station.lat, station.lng) {
|
||||
(t!("your_station_is_here"))
|
||||
div id="map" style="height: 500px" {}
|
||||
script { (format!("
|
||||
const map = L.map('map').setView([{lat}, {lng}], 14);
|
||||
|
||||
L.tileLayer('https://{{s}}.tile.openstreetmap.org/{{z}}/{{x}}/{{y}}.png', {{
|
||||
@@ -100,11 +113,12 @@ async fn view(
|
||||
currentMarker = L.marker([{lat}, {lng}], {{icon: myIcon}}).addTo(map);
|
||||
map.setView([{lat}, {lng}], 14);
|
||||
"))
|
||||
}
|
||||
div {
|
||||
sub {
|
||||
a href=(format!("https://www.google.com/maps?q={lat},{lng}")) target="_blank" {
|
||||
(t!("google_maps_navigation"))
|
||||
}
|
||||
div {
|
||||
sub {
|
||||
a href=(format!("https://www.google.com/maps?q={lat},{lng}")) target="_blank" {
|
||||
(t!("google_maps_navigation"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,205 +134,175 @@ async fn view(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
article {
|
||||
@if teams.total_teams == 1 {
|
||||
(t!("one_team_should_come_to_station"))
|
||||
} @else{
|
||||
(t!("n_teams_should_come_to_station", amount=teams.total_teams))
|
||||
}
|
||||
progress value=(teams.total_teams-teams.not_yet_here.len() as i64) max=(teams.total_teams) {}
|
||||
@if status == RunStatus::HasEnded {
|
||||
@let teams_to_take_home = Team::all_with_last_station(&db, &station).await;
|
||||
@if !teams_to_take_home.is_empty() {
|
||||
@if teams_to_take_home.len() == 1 {
|
||||
(t!("take_home_the_following_team"))
|
||||
} @else {
|
||||
(t!("take_home_the_following_teams"))
|
||||
}
|
||||
ol {
|
||||
@for team in teams_to_take_home {
|
||||
li { (team.name) }
|
||||
}
|
||||
}
|
||||
}@else {
|
||||
(t!("no_team_to_take_home"))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@for team in teams_on_the_way {
|
||||
article {
|
||||
(t!("team_on_the_way_to_your_station", team=team.team.name, time=team.left))
|
||||
form action=(format!("/s/{id}/{code}/new-waiting")) method="post" {
|
||||
input type="hidden" name="team_id" value=(team.team.id);
|
||||
input type="submit" value=(t!("team_is_here"));
|
||||
@if teams.total_teams == 1 {
|
||||
(t!("one_team_should_come_to_station"))
|
||||
} @else{
|
||||
(t!("n_teams_should_come_to_station", amount=teams.total_teams))
|
||||
}
|
||||
}
|
||||
}
|
||||
@if !teams.not_yet_here.is_empty() {
|
||||
form action=(format!("/s/{id}/{code}/new-waiting")) method="post" {
|
||||
fieldset role="group" {
|
||||
select name="team_id" aria-label=(t!("select_team")) required {
|
||||
@for team in &teams.not_yet_here {
|
||||
option value=(team.id) {
|
||||
(team.name)
|
||||
}
|
||||
div {
|
||||
em data-tooltip=(t!("station_team_progress", arrived=teams.total_teams-teams.not_yet_here.len() as i64, total=teams.total_teams, waiting=teams.waiting.len(), active=teams.doing.len())) {
|
||||
progress value=(teams.total_teams-teams.not_yet_here.len() as i64) max=(teams.total_teams) {}
|
||||
}
|
||||
}
|
||||
@if status == RunStatus::HasEnded {
|
||||
(t!("station_done"))
|
||||
@let teams_to_take_home = Team::all_with_last_station(db, &station).await;
|
||||
@if !teams_to_take_home.is_empty() {
|
||||
@if teams_to_take_home.len() == 1 {
|
||||
(t!("take_home_the_following_team"))
|
||||
} @else {
|
||||
(t!("take_home_the_following_teams"))
|
||||
}
|
||||
ol {
|
||||
@for team in teams_to_take_home {
|
||||
li { (team.name) }
|
||||
}
|
||||
}
|
||||
}@else {
|
||||
(t!("no_team_to_take_home"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@if teams.done {
|
||||
article {
|
||||
(t!("station_done"))
|
||||
}
|
||||
}
|
||||
|
||||
@for team in teams_on_the_way {
|
||||
article {
|
||||
(t!("team_on_the_way_to_your_station", team=team.team.name, time=team.left))
|
||||
form action=(format!("/s/{id}/{code}/new-waiting")) method="post" {
|
||||
input type="hidden" name="team_id" value=(team.team.id);
|
||||
input type="submit" value=(t!("team_is_here"));
|
||||
}
|
||||
}
|
||||
}
|
||||
h2 { (t!("teams_at_your_station")) }
|
||||
@if !teams.doing.is_empty() {
|
||||
@for (team, rating) in teams.doing {
|
||||
article {
|
||||
details {
|
||||
summary {
|
||||
em data-tooltip=(t!("state_active")) { (t!("state_active_icon")) " " }
|
||||
(team.name)
|
||||
small {
|
||||
" ("
|
||||
(t!("since_time", time=rating.local_time_doing()))
|
||||
")"
|
||||
}
|
||||
"✏️"
|
||||
a href=(format!("/s/{id}/{code}/team-finished/{}", team.id)) {
|
||||
button { (t!("team_finished")) }
|
||||
}
|
||||
}
|
||||
form action=(format!("/s/{id}/{code}/team-update/{}", team.id)) method="post" {
|
||||
label {
|
||||
(t!("notes"))
|
||||
@if let Some(notes) = &rating.notes {
|
||||
input type="text" name="notes" value=(notes);
|
||||
} @else {
|
||||
input type="text" name="notes";
|
||||
}
|
||||
}
|
||||
input type="submit" value=(t!("save_notes"));
|
||||
}
|
||||
a href=(format!("/s/{id}/{code}/remove-doing/{}", team.id))
|
||||
onclick=(format!("return confirm('{}');", t!("confirm_station_cancel_team_active", team=team.name))) {
|
||||
"🗑️"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@if !teams.waiting.is_empty() {
|
||||
@for (team, rating) in teams.waiting {
|
||||
article {
|
||||
details {
|
||||
summary {
|
||||
em data-tooltip=(t!("state_waiting")) { (t!("state_waiting_icon")) " "}
|
||||
(team.name)
|
||||
small {
|
||||
" ("
|
||||
(t!("since_time", time=rating.local_time_arrived_at()))
|
||||
")"
|
||||
}
|
||||
"✏️"
|
||||
a href=(format!("/s/{id}/{code}/team-starting/{}", team.id)) {
|
||||
button { (t!("team_starting")) }
|
||||
}
|
||||
}
|
||||
form action=(format!("/s/{id}/{code}/team-update/{}", team.id)) method="post" {
|
||||
label {
|
||||
(t!("notes"))
|
||||
@if let Some(notes) = &rating.notes {
|
||||
input type="text" name="notes" value=(notes);
|
||||
} @else {
|
||||
input type="text" name="notes";
|
||||
}
|
||||
}
|
||||
input type="submit" value=(t!("save_notes"));
|
||||
}
|
||||
a href=(format!("/s/{id}/{code}/remove-waiting/{}", team.id))
|
||||
onclick=(format!("return confirm('{}');", t!("confirm_station_cancel_team_waiting", team=team.name))) {
|
||||
"🗑️"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@if !teams.left_not_yet_rated.is_empty() {
|
||||
h2 { (t!("to_rate")) }
|
||||
article class="warning" {
|
||||
@if teams.left_not_yet_rated.len() == 1 {
|
||||
(t!("info_single_team_not_yet_rated"))
|
||||
} @else {
|
||||
(t!("info_multiple_teams_not_yet_rated"))
|
||||
}
|
||||
}
|
||||
@for (team, rating) in teams.left_not_yet_rated {
|
||||
article {
|
||||
em data-tooltip=(t!("state_to_rate")) { (t!("state_to_rate_icon")) " " }
|
||||
(team.name)
|
||||
small {
|
||||
" ("
|
||||
(t!("left_at", time=rating.local_time_left()))
|
||||
")"
|
||||
}
|
||||
form action=(format!("/s/{id}/{code}/team-update/{}", team.id)) method="post" {
|
||||
label {
|
||||
@if let Some(points) = rating.points {
|
||||
span { (points) " " (t!("points")) }
|
||||
input type="range" name="points" min="0" max="10" value=(points)
|
||||
onchange=(format!("if(!confirm('{}')) {{ this.value = this.defaultValue; this.previousElementSibling.textContent = this.defaultValue + ' Punkte'; }}", t!("confirm_rating_change")))
|
||||
oninput=(format!("this.previousElementSibling.textContent = this.value + ' {}'", t!("points"))) {}
|
||||
} @else {
|
||||
span { "0 " (t!("points")) }
|
||||
input type="range" name="points" min="0" max="10" value="0" oninput="this.previousElementSibling.textContent = this.value + ' Punkte'" {}
|
||||
}
|
||||
|
||||
}
|
||||
label {
|
||||
(t!("notes"))
|
||||
@if let Some(notes) = &rating.notes {
|
||||
input type="text" name="notes" value=(notes);
|
||||
} @else {
|
||||
input type="text" name="notes";
|
||||
}
|
||||
}
|
||||
input type="submit" value=(t!("save_notes"));
|
||||
}
|
||||
a href=(format!("/s/{id}/{code}/remove-left/{}", team.id))
|
||||
onclick=(format!("return confirm('{}');", t!("confirm_station_cancel_team_finished"))) {
|
||||
"🗑️"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
h2 { (t!("history")) }
|
||||
@if !teams.left_and_rated.is_empty() {
|
||||
@for (team, rating) in teams.left_and_rated {
|
||||
article {
|
||||
details {
|
||||
summary {
|
||||
em data-tooltip=(t!("state_rated")) { (t!("state_rated_icon")) " " }
|
||||
(team.name)
|
||||
(PreEscaped(" → "))
|
||||
(rating.points.unwrap())
|
||||
" "
|
||||
(t!("points"))
|
||||
@if !teams.not_yet_here.is_empty() {
|
||||
form action=(format!("/s/{id}/{code}/new-waiting")) method="post" {
|
||||
fieldset role="group" {
|
||||
select name="team_id" aria-label=(t!("select_team")) required {
|
||||
@for (route, teams) in &teams.not_yet_here_by_route {
|
||||
optgroup label=(route.name) {
|
||||
@for team in teams {
|
||||
option value=(team.id) {
|
||||
(team.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
input type="submit" value=(t!("team_is_here"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@if !teams.doing.is_empty() || !teams.waiting.is_empty() {
|
||||
h2 { (t!("teams_at_your_station")) }
|
||||
}
|
||||
@if !teams.doing.is_empty() {
|
||||
@for (team, rating) in teams.doing {
|
||||
article {
|
||||
details class="mb-0" {
|
||||
summary class="flex-center-between" {
|
||||
span {
|
||||
em class="mr-1" data-tooltip=(t!("state_active")) { (t!("state_active_icon")) " " }
|
||||
(team.name)
|
||||
small class="mr-1" {
|
||||
" ("
|
||||
(t!("since_time", time=rating.local_time_doing()))
|
||||
")"
|
||||
}
|
||||
"✏️"
|
||||
}
|
||||
a class="contrast" role="button" href=(format!("/s/{id}/{code}/team-finished/{}", team.id)) {
|
||||
(t!("team_finished"))
|
||||
}
|
||||
}
|
||||
form action=(format!("/s/{id}/{code}/team-update/{}", team.id)) method="post" {
|
||||
fieldset role="group" {
|
||||
@if let Some(notes) = &rating.notes {
|
||||
input type="text" name="notes" value=(notes) placeholder=(t!("notes"));
|
||||
} @else {
|
||||
input type="text" name="notes" placeholder=(t!("notes"));
|
||||
}
|
||||
input type="submit" value=(t!("save_notes"));
|
||||
}
|
||||
}
|
||||
a role="button" class="danger" href=(format!("/s/{id}/{code}/remove-doing/{}", team.id))
|
||||
onclick=(format!("return confirm('{}');", t!("confirm_station_cancel_team_active", team=team.name))) {
|
||||
(t!("team_active_step_back"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@if !teams.waiting.is_empty() {
|
||||
@for (team, rating) in teams.waiting {
|
||||
article {
|
||||
details class="mb-0" {
|
||||
summary class="flex-center-between" {
|
||||
span {
|
||||
em class="mr-1" data-tooltip=(t!("state_waiting")) { (t!("state_waiting_icon")) " "}
|
||||
(team.name)
|
||||
small class="mr-1" {
|
||||
" ("
|
||||
(t!("since_time", time=rating.local_time_arrived_at()))
|
||||
")"
|
||||
}
|
||||
"✏️"
|
||||
}
|
||||
a role="button" href=(format!("/s/{id}/{code}/team-starting/{}", team.id)) {
|
||||
(t!("team_starting"))
|
||||
}
|
||||
}
|
||||
form action=(format!("/s/{id}/{code}/team-update/{}", team.id)) method="post" {
|
||||
fieldset role="group" {
|
||||
@if let Some(notes) = &rating.notes {
|
||||
input type="text" name="notes" value=(notes) placeholder=(t!("notes"));
|
||||
} @else {
|
||||
input type="text" name="notes" placeholder=(t!("notes"));
|
||||
}
|
||||
input type="submit" value=(t!("save_notes"));
|
||||
}
|
||||
}
|
||||
a role="button" class="danger" href=(format!("/s/{id}/{code}/remove-waiting/{}", team.id))
|
||||
onclick=(format!("return confirm('{}');", t!("confirm_station_cancel_team_waiting", team=team.name))) {
|
||||
(t!("team_waiting_step_back"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@if !teams.left_not_yet_rated.is_empty() {
|
||||
h2 { (t!("to_rate")) }
|
||||
article class="warning" {
|
||||
@if teams.left_not_yet_rated.len() == 1 {
|
||||
(t!("info_single_team_not_yet_rated"))
|
||||
} @else {
|
||||
(t!("info_multiple_teams_not_yet_rated"))
|
||||
}
|
||||
}
|
||||
@for (team, rating) in teams.left_not_yet_rated {
|
||||
article {
|
||||
em data-tooltip=(t!("state_to_rate")) { (t!("state_to_rate_icon")) " " }
|
||||
(team.name)
|
||||
small {
|
||||
" ("
|
||||
(t!("arrived_at_started_at_left_at", arrived=rating.local_time_arrived_at(), active=rating.local_time_doing(), left=rating.local_time_left()))
|
||||
")"
|
||||
(t!("left_at", time=rating.local_time_left()))
|
||||
")"
|
||||
}
|
||||
form action=(format!("/s/{id}/{code}/team-update/{}", team.id)) method="post" {
|
||||
label {
|
||||
@if let Some(points) = rating.points {
|
||||
span { (points) " Punkte" }
|
||||
span { (points) " " (t!("points")) }
|
||||
input type="range" name="points" min="0" max="10" value=(points)
|
||||
onchange=(format!("if(!confirm('{}')) {{ this.value = this.defaultValue; this.previousElementSibling.textContent = this.defaultValue + ' Punkte'; }}", t!("confirm_rating_change")))
|
||||
oninput=(format!("this.previousElementSibling.textContent = this.value + ' {}'", t!("points"))) {}
|
||||
} @else {
|
||||
span { "0 " (t!("points")) }
|
||||
input type="range" name="points" min="0" max="10" value="0" oninput="this.previousElementSibling.textContent = this.value + ' Punkte'" {}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -330,24 +314,74 @@ async fn view(
|
||||
input type="text" name="notes";
|
||||
}
|
||||
}
|
||||
input type="submit" value=(t!("save_notes"));
|
||||
input type="submit" value=(t!("save"));
|
||||
}
|
||||
a href=(format!("/s/{id}/{code}/remove-left/{}", team.id))
|
||||
onclick=(format!("return confirm('{}');", t!("confirm_station_cancel_team_finished"))) {
|
||||
"🗑️"
|
||||
a role="button" class="danger" href=(format!("/s/{id}/{code}/remove-left/{}", team.id))
|
||||
onclick=(format!("return confirm('{}');", t!("confirm_station_cancel_team_finished"))) {
|
||||
(t!("team_done_step_back"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
h2 { (t!("history")) }
|
||||
@if !teams.left_and_rated.is_empty() {
|
||||
@for (team, rating) in teams.left_and_rated {
|
||||
article {
|
||||
details class="mb-0" {
|
||||
summary {
|
||||
em class="mr-1" data-tooltip=(t!("state_rated")) { (t!("state_rated_icon")) " " }
|
||||
(team.name)
|
||||
(PreEscaped(" → "))
|
||||
(rating.points.unwrap())
|
||||
" "
|
||||
(t!("points"))
|
||||
}
|
||||
small {
|
||||
" ("
|
||||
(t!("arrived_at_started_at_left_at", arrived=rating.local_time_arrived_at(), active=rating.local_time_doing(), left=rating.local_time_left()))
|
||||
")"
|
||||
}
|
||||
form action=(format!("/s/{id}/{code}/team-update/{}", team.id)) method="post" {
|
||||
label {
|
||||
@if let Some(points) = rating.points {
|
||||
span { (points) " Punkte" }
|
||||
input type="range" name="points" min="0" max="10" value=(points)
|
||||
onchange=(format!("if(!confirm('{}')) {{ this.value = this.defaultValue; this.previousElementSibling.textContent = this.defaultValue + ' Punkte'; }}", t!("confirm_rating_change")))
|
||||
oninput=(format!("this.previousElementSibling.textContent = this.value + ' {}'", t!("points"))) {}
|
||||
}
|
||||
|
||||
}
|
||||
label {
|
||||
(t!("notes"))
|
||||
@if let Some(notes) = &rating.notes {
|
||||
input type="text" name="notes" value=(notes);
|
||||
} @else {
|
||||
input type="text" name="notes";
|
||||
}
|
||||
}
|
||||
input type="submit" value=(t!("save"));
|
||||
}
|
||||
a role="button" class="danger" href=(format!("/s/{id}/{code}/remove-left/{}", team.id))
|
||||
onclick=(format!("return confirm('{}');", t!("confirm_station_cancel_team_rated"))) {
|
||||
(t!("team_done_step_back"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
(t!("no_teams_rated_yet"))
|
||||
}
|
||||
} @else {
|
||||
(t!("no_teams_rated_yet"))
|
||||
}
|
||||
|
||||
};
|
||||
};
|
||||
|
||||
let use_map = station.lat.is_some() && station.lng.is_some();
|
||||
|
||||
partials::page(content, session, use_map).await
|
||||
PageBuilder::new(content, session)
|
||||
.set_leaflet(use_map)
|
||||
.markup()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn ready(
|
||||
@@ -355,12 +389,14 @@ async fn ready(
|
||||
session: Session,
|
||||
axum::extract::Path((id, code)): axum::extract::Path<(i64, String)>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(station) = Station::login(&db, id, &code).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(station) = Station::login(db, id, &code).await else {
|
||||
er!(session, t!("invalid_rating_code"));
|
||||
return Redirect::to("/s/{id}/{code}");
|
||||
};
|
||||
|
||||
station.switch_ready(&db).await;
|
||||
station.switch_ready(db).await;
|
||||
|
||||
suc!(session, t!("succ_change"));
|
||||
Redirect::to(&format!("/s/{id}/{code}"))
|
||||
@@ -376,16 +412,18 @@ async fn new_waiting(
|
||||
axum::extract::Path((id, code)): axum::extract::Path<(i64, String)>,
|
||||
Form(form): Form<NewWaitingForm>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(station) = Station::login(&db, id, &code).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(station) = Station::login(db, id, &code).await else {
|
||||
er!(session, t!("invalid_rating_code"));
|
||||
return Redirect::to("/s/{id}/{code}");
|
||||
};
|
||||
let Some(team) = Team::find_by_id(&db, form.team_id).await else {
|
||||
let Some(team) = Team::find_by_id(db, form.team_id).await else {
|
||||
er!(session, t!("nonexisting_team", id = form.team_id));
|
||||
return Redirect::to("/s/{id}/{code}");
|
||||
};
|
||||
|
||||
match station.new_team_waiting(&db, &team).await {
|
||||
match station.new_team_waiting(db, &team).await {
|
||||
Ok(()) => suc!(session, t!("team_added_to_waiting", team = team.name)),
|
||||
Err(e) => err!(session, "{e}"),
|
||||
}
|
||||
@@ -398,17 +436,19 @@ async fn remove_waiting(
|
||||
session: Session,
|
||||
axum::extract::Path((id, code, team_id)): axum::extract::Path<(i64, String, i64)>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(station) = Station::login(&db, id, &code).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(station) = Station::login(db, id, &code).await else {
|
||||
er!(session, t!("invalid_rating_code"));
|
||||
return Redirect::to("/s/{id}/{code}");
|
||||
};
|
||||
|
||||
let Some(team) = Team::find_by_id(&db, team_id).await else {
|
||||
let Some(team) = Team::find_by_id(db, team_id).await else {
|
||||
er!(session, t!("nonexisting_team", id = team_id));
|
||||
return Redirect::to("/s/{id}/{code}");
|
||||
};
|
||||
|
||||
match station.remove_team_waiting(&db, &team).await {
|
||||
match station.remove_team_waiting(db, &team).await {
|
||||
Ok(()) => suc!(session, t!("team_removed_from_waiting", team = team.name)),
|
||||
Err(e) => err!(session, "{e}"),
|
||||
}
|
||||
@@ -421,17 +461,18 @@ async fn team_starting(
|
||||
session: Session,
|
||||
axum::extract::Path((id, code, team_id)): axum::extract::Path<(i64, String, i64)>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(station) = Station::login(&db, id, &code).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
let Some(station) = Station::login(&mut *db, id, &code).await else {
|
||||
er!(session, t!("invalid_rating_code"));
|
||||
return Redirect::to("/s/{id}/{code}");
|
||||
};
|
||||
|
||||
let Some(team) = Team::find_by_id(&db, team_id).await else {
|
||||
let Some(team) = Team::find_by_id(&mut *db, team_id).await else {
|
||||
er!(session, t!("nonexisting_team", id = team_id));
|
||||
return Redirect::to("/s/{id}/{code}");
|
||||
};
|
||||
|
||||
match station.team_starting(&db, &team).await {
|
||||
match station.team_starting(&mut *db, &team).await {
|
||||
Ok(()) => suc!(session, t!("team_added_to_active", team = team.name)),
|
||||
Err(e) => err!(session, "{e}"),
|
||||
}
|
||||
@@ -444,17 +485,19 @@ async fn remove_doing(
|
||||
session: Session,
|
||||
axum::extract::Path((id, code, team_id)): axum::extract::Path<(i64, String, i64)>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(station) = Station::login(&db, id, &code).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(station) = Station::login(db, id, &code).await else {
|
||||
er!(session, t!("invalid_rating_code"));
|
||||
return Redirect::to("/s/{id}/{code}");
|
||||
};
|
||||
|
||||
let Some(team) = Team::find_by_id(&db, team_id).await else {
|
||||
let Some(team) = Team::find_by_id(db, team_id).await else {
|
||||
er!(session, t!("nonexisting_team", id = id));
|
||||
return Redirect::to("/s/{id}/{code}");
|
||||
};
|
||||
|
||||
match station.remove_team_doing(&db, &team).await {
|
||||
match station.remove_team_doing(db, &team).await {
|
||||
Ok(()) => suc!(session, t!("team_removed_from_active", team = team.name)),
|
||||
Err(e) => err!(session, "{e}"),
|
||||
}
|
||||
@@ -467,17 +510,19 @@ async fn team_finished(
|
||||
session: Session,
|
||||
axum::extract::Path((id, code, team_id)): axum::extract::Path<(i64, String, i64)>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(station) = Station::login(&db, id, &code).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(station) = Station::login(db, id, &code).await else {
|
||||
er!(session, t!("invalid_rating_code"));
|
||||
return Redirect::to("/s/{id}/{code}");
|
||||
};
|
||||
|
||||
let Some(team) = Team::find_by_id(&db, team_id).await else {
|
||||
let Some(team) = Team::find_by_id(db, team_id).await else {
|
||||
er!(session, t!("nonexisting_team", id = id));
|
||||
return Redirect::to("/s/{id}/{code}");
|
||||
};
|
||||
|
||||
match station.team_finished(&db, &team).await {
|
||||
match station.team_finished(db, &team).await {
|
||||
Ok(()) => suc!(session, t!("team_added_to_finished", team = team.name)),
|
||||
Err(e) => err!(session, "{e}"),
|
||||
}
|
||||
@@ -490,17 +535,19 @@ async fn remove_left(
|
||||
session: Session,
|
||||
axum::extract::Path((id, code, team_id)): axum::extract::Path<(i64, String, i64)>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(station) = Station::login(&db, id, &code).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
|
||||
let Some(station) = Station::login(db, id, &code).await else {
|
||||
er!(session, t!("invalid_rating_code"));
|
||||
return Redirect::to("/s/{id}/{code}");
|
||||
};
|
||||
|
||||
let Some(team) = Team::find_by_id(&db, team_id).await else {
|
||||
let Some(team) = Team::find_by_id(db, team_id).await else {
|
||||
er!(session, t!("nonexisting_team", id = id));
|
||||
return Redirect::to("/s/{id}/{code}");
|
||||
};
|
||||
|
||||
match station.remove_team_left(&db, &team).await {
|
||||
match station.remove_team_left(db, &team).await {
|
||||
Ok(()) => suc!(session, t!("team_removed_from_finished", team = team.name)),
|
||||
Err(e) => err!(session, "{e}"),
|
||||
}
|
||||
@@ -519,17 +566,18 @@ async fn team_update(
|
||||
axum::extract::Path((id, code, team_id)): axum::extract::Path<(i64, String, i64)>,
|
||||
Form(form): Form<TeamUpdateForm>,
|
||||
) -> impl IntoResponse {
|
||||
let Some(station) = Station::login(&db, id, &code).await else {
|
||||
let db = &mut *db.acquire().await.unwrap();
|
||||
let Some(station) = Station::login(db, id, &code).await else {
|
||||
er!(session, t!("invalid_rating_code"));
|
||||
return Redirect::to("/s/{id}/{code}");
|
||||
};
|
||||
let Some(team) = Team::find_by_id(&db, team_id).await else {
|
||||
let Some(team) = Team::find_by_id(db, team_id).await else {
|
||||
er!(session, t!("nonexisting_team", id = id));
|
||||
return Redirect::to("/s/{id}/{code}");
|
||||
};
|
||||
|
||||
match station
|
||||
.team_update(&db, &team, form.points, form.notes)
|
||||
.team_update(db, &team, form.points, form.notes)
|
||||
.await
|
||||
{
|
||||
Ok(()) => suc!(session, t!("rating_updated", team = team.name)),
|
||||
@@ -554,7 +602,7 @@ pub(super) fn routes() -> Router<AppState> {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{router, testdb, Station};
|
||||
use crate::{Station, router, testdb};
|
||||
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
@@ -563,8 +611,9 @@ mod test {
|
||||
#[sqlx::test]
|
||||
async fn test_wrong_station() {
|
||||
let pool = testdb!();
|
||||
let db = &mut *pool.acquire().await.unwrap();
|
||||
|
||||
Station::create(&pool, "Teststation").await.unwrap();
|
||||
Station::create(db, "Teststation").await.unwrap();
|
||||
|
||||
let server = TestServer::new(router(pool)).unwrap();
|
||||
|
||||
@@ -576,9 +625,10 @@ mod test {
|
||||
#[sqlx::test]
|
||||
async fn test_correct_station() {
|
||||
let pool = testdb!();
|
||||
let db = &mut *pool.acquire().await.unwrap();
|
||||
|
||||
Station::create(&pool, "42-Station").await.unwrap();
|
||||
let stations = Station::all(&pool).await;
|
||||
Station::create(db, "42-Station").await.unwrap();
|
||||
let stations = Station::all(db).await;
|
||||
let station = stations.last().unwrap();
|
||||
|
||||
let server = TestServer::new(router(pool)).unwrap();
|
||||
|
||||
52
watch.sh
Executable file
52
watch.sh
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Function to kill the previous cargo process
|
||||
kill_cargo() {
|
||||
if [ -n "$pid" ] && ps -p $pid > /dev/null; then
|
||||
echo "Terminating previous cargo process..."
|
||||
pkill -P $pid
|
||||
kill -TERM $pid 2>/dev/null
|
||||
wait $pid 2>/dev/null
|
||||
fi
|
||||
}
|
||||
|
||||
# Cleanup on script exit
|
||||
trap 'kill_cargo; echo "Monitor stopped."; exit' INT TERM EXIT
|
||||
|
||||
echo "Monitoring directory for changes..."
|
||||
echo "Press Ctrl+C to exit"
|
||||
|
||||
# Initial run
|
||||
cargo r &
|
||||
pid=$!
|
||||
|
||||
# Check if we're on macOS
|
||||
if [[ "$(uname)" == "Darwin" ]]; then
|
||||
# Use fswatch for macOS
|
||||
fswatch -o src/ assets/ | while read; do
|
||||
echo -e "\n--- Changes detected, restarting cargo r ---"
|
||||
|
||||
# Kill the previous cargo process
|
||||
kill_cargo
|
||||
|
||||
# Start a new cargo process
|
||||
cargo r &
|
||||
pid=$!
|
||||
done
|
||||
else
|
||||
# Use inotifywait for Linux
|
||||
while true; do
|
||||
# Wait for changes in the directory
|
||||
inotifywait -q -e modify,create,delete,move -r src/ assets/ 2>/dev/null
|
||||
|
||||
echo -e "\n--- Changes detected, restarting cargo r ---"
|
||||
|
||||
# Kill the previous cargo process
|
||||
kill_cargo
|
||||
|
||||
# Start a new cargo process
|
||||
cargo r &
|
||||
pid=$!
|
||||
done
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user