Compare commits
265 Commits
test
...
cecd5e8106
Author | SHA1 | Date | |
---|---|---|---|
cecd5e8106 | |||
615898ead4 | |||
2663772651 | |||
b7e3c882d8 | |||
56f5b6e8db | |||
102cc90a23 | |||
b429998775 | |||
5727c0c9ce | |||
ece64868fe | |||
1225aeac94 | |||
8408148ead | |||
d0d7da7996 | |||
14bfb695d9 | |||
1da9412904 | |||
abe256af5d | |||
8666b014f2 | |||
af8637d2b7 | |||
c7d3435f4d | |||
3e14b61ce5 | |||
14d546bdc3 | |||
81dbbeac00 | |||
d404636261 | |||
f116b97072 | |||
0eaf3aa92c | |||
9cbbe10e12 | |||
d6c6f8800e | |||
582cfd60c8 | |||
8152822efc | |||
5f21148b3c | |||
a356e7bd08 | |||
b40850626b | |||
d6f354bf34 | |||
6df24f0f22 | |||
f41b5e9fef | |||
b6d58077f6 | |||
1ce3ef9082 | |||
3b3374b0cc | |||
242f4ee266 | |||
0689e75626 | |||
7ab6d95e23 | |||
f38d506fe4 | |||
96dcf2c4ae | |||
c4ca148b54 | |||
a441d99b5e | |||
76022a1f0e | |||
122e5daab2 | |||
63e9597c06 | |||
c7adea88ed | |||
1202b0afec | |||
3c6e938949 | |||
1e9dfa3e70 | |||
2b74b47d06 | |||
bb2771b412 | |||
2dc145e697 | |||
142169d638 | |||
6b88927880 | |||
be94707228 | |||
49b2305cdb | |||
99a49dbec9 | |||
0645103466 | |||
f968d5d03b | |||
6ba97e2631 | |||
a52ee97a80 | |||
ae1091c9a2 | |||
0b46cbf8db | |||
be6d3229a4 | |||
afc23ae519 | |||
fdd9c3bdff | |||
ae6c129fd3 | |||
396aa204a4 | |||
4290010cc6 | |||
bbe4949203 | |||
94130f9230 | |||
14dbe748a3 | |||
010627c91d | |||
36276e5415 | |||
b827bd6996 | |||
4bb0e54635 | |||
83a2c7ab92 | |||
a518023892 | |||
3f06e91e24 | |||
2b4345ba77 | |||
412c45a9df | |||
d971c1504c | |||
cf9b79e56e | |||
5d01d18e70 | |||
1e96a2d6e1 | |||
ff81ab0246 | |||
202e128c98 | |||
ecb347c204 | |||
2bc426be52 | |||
0a130709c7 | |||
6e8a5927a6 | |||
a31bacb3e1 | |||
93c8316543 | |||
6e72e2a753 | |||
c847c3300f | |||
116c7523d2 | |||
eb15421d08 | |||
818cf0d40b | |||
ed9d93410e | |||
c162e0a66f | |||
e965d33a7b | |||
3d77a2325c | |||
afb6af8ece | |||
799e94a50f | |||
c41dc0853a | |||
74edcfa119 | |||
cc7bd3a416 | |||
bfee85a963 | |||
f55f45d960 | |||
c68593a67d | |||
20da86f69e | |||
8588e1f71b | |||
8efb3aea2c | |||
83aa9bc84c | |||
6171bb0f85 | |||
e040764902 | |||
eeab4c167b | |||
25161fc8e9 | |||
dea53d8396 | |||
80ac131fb2 | |||
8e65a6540d | |||
d7e5731753 | |||
1a4d5ac569 | |||
668fc5e295 | |||
4f9778eccf | |||
09d4c5d958 | |||
11dd978135 | |||
d7d0a3fedd | |||
e4333a05d7 | |||
f687e18195 | |||
a4f72d746c | |||
e55f380c4f | |||
bb502a4561 | |||
1340639f91 | |||
ce28c95853 | |||
2c3f69a562 | |||
0bf7094770 | |||
a75c892cfb | |||
f71ab634d7 | |||
0c770f6ddc | |||
6b71449183 | |||
d59b3f4345 | |||
7e41cd3f73 | |||
2a8c339dcd | |||
0dd10e1dd6 | |||
2d2e44126a | |||
def8028446 | |||
db318c23cd | |||
4555391dd3 | |||
23aa6aa0f8 | |||
a682d1e6ce | |||
8aca437eb3 | |||
cd1bf12e68 | |||
5f7591f52a | |||
127d9784ad | |||
bf4ea502d3 | |||
c13dfdaa77 | |||
26aa222bc6 | |||
0bc9e11b9a | |||
2fdcab9030 | |||
7689a39ac5 | |||
b43682ac39 | |||
8d7a1c707d | |||
958dda9f52 | |||
1eea8c9662 | |||
b4b922222c | |||
84e76e8d65 | |||
bdfcc6bc0a | |||
afa88b9529 | |||
4229a4e021 | |||
6cd555298d | |||
f6207e2994 | |||
4da996251a | |||
c44c0d8505 | |||
aa9568f326 | |||
1db09cd8ac | |||
59ef93d6fa | |||
4a3ee5b551 | |||
c73b3e94c3 | |||
4969a0d90a | |||
3efcd99bbc | |||
4f0f509ad6 | |||
8112f1ed2a | |||
b1252e8d5c | |||
9cb9cfe2a1 | |||
a62fd116ea | |||
622bc700f3 | |||
2540a3dc7c | |||
0e5fd25e61 | |||
72b86d4dad | |||
16fbeea81b | |||
bd6fbe772e | |||
647970e1fc | |||
1e1c1bb6d9 | |||
088fe98995 | |||
4237fafdff | |||
6b24008c17 | |||
4fbd3c7717 | |||
ce8a095b31 | |||
6c191cf59e | |||
b0698e70a4 | |||
6f7283f754 | |||
323f721fc0 | |||
d0bcf1f384 | |||
bd7cd0020e | |||
22bfe48d18 | |||
c379a6ca79 | |||
3543ffe9e1 | |||
1d6770f11b | |||
1ad6509568 | |||
705f2ddc52 | |||
dba1e08c5d | |||
1dc0c9c0e1 | |||
45004567ed | |||
3dff956544 | |||
79f8efc34b | |||
5c31fac230 | |||
3e983e05f9 | |||
8f91cc4e88 | |||
c55f9743aa | |||
76290a64ae | |||
4b48fbaa82 | |||
d25cd491d0 | |||
def8affb5f | |||
dfa7be9928 | |||
03a467270d | |||
1c04462c30 | |||
7af53203f8 | |||
b2393eb6ec | |||
a5e82851ba | |||
80725e223b | |||
70be6726db | |||
08fc324cc6 | |||
df007524ed | |||
5720767af3 | |||
1215bdbd84 | |||
734490efe7 | |||
980fcc0c0c | |||
84f23e6e55 | |||
36193e3a64 | |||
c6e3458588 | |||
a0528c1c65 | |||
5e4d708884 | |||
6e41758104 | |||
c916381fb0 | |||
7d44204533 | |||
145892104b | |||
6c0f0e6b04 | |||
e004d81ca1 | |||
41b5aff329 | |||
76c8456380 | |||
95f43a73cf | |||
3d340bf803 | |||
ae096ad602 | |||
25d8b1ea7c | |||
410cd05acc | |||
972811c2cf | |||
e22d2d718e | |||
b55f122f1d | |||
d27489d714 | |||
c340d1a916 | |||
e7732b9e96 | |||
97dc9308bc |
637
Cargo.lock
generated
637
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -28,6 +28,7 @@ itertools = "0.13"
|
||||
job_scheduler_ng = "2.0"
|
||||
ureq = { version = "2.9", features = ["json"] }
|
||||
regex = "1.10"
|
||||
urlencoding = "2.1"
|
||||
|
||||
[target.'cfg(not(windows))'.dependencies]
|
||||
openssl = { version = "0.10", features = [ "vendored" ] }
|
||||
|
@ -2,6 +2,7 @@
|
||||
secret_key = "/NtVGizglEoyoxBLzsRDWTy4oAG1qDw4J4O+CWJSv+fypD7W9sam8hUY4j90EZsbZk8wEradS5zBoWtWKi3k8w=="
|
||||
rss_key = "rss-key-for-ci"
|
||||
limits = { file = "10 MiB", data-form = "10 MiB"}
|
||||
smtp_pw = "8kIjlLH79Ky6D3jQ"
|
||||
smtp_pw = "8kIjlLH79Ky6D3j"
|
||||
usage_log_path = "./usage.txt"
|
||||
openweathermap_key = "c8dab8f91b5b815d76e9879cbaecd8d5"
|
||||
wordpress_key = "pw-to-allow-sending-notifications"
|
||||
|
2
fd
2
fd
@ -1,5 +1,5 @@
|
||||
#!/bin/bash
|
||||
|
||||
scp read@128.140.64.118:/home/rowing/db.sqlite db.sqlite
|
||||
scp root@128.140.64.118:/home/rowing/db.sqlite db.sqlite
|
||||
#sqlite3 db.sqlite < seeds.sql
|
||||
|
||||
|
136
frontend/main.ts
136
frontend/main.ts
@ -8,7 +8,7 @@ export interface choiceMap {
|
||||
declare var loggedin_user_id: string;
|
||||
let choiceObjects: choiceMap = {};
|
||||
let boat_in_ottensheim = true;
|
||||
let boat_reserved_today= true;
|
||||
let boat_reserved_today = true;
|
||||
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
changeTheme();
|
||||
@ -103,7 +103,11 @@ function setTheme(theme: string, setLocalStorage = true) {
|
||||
function setCurrentdate(input: HTMLInputElement) {
|
||||
if (input) {
|
||||
const now = new Date();
|
||||
const formattedDateTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}T${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
||||
const formattedDateTime = `${now.getFullYear()}-${String(
|
||||
now.getMonth() + 1
|
||||
).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}T${String(
|
||||
now.getHours()
|
||||
).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
||||
|
||||
input.value = formattedDateTime;
|
||||
}
|
||||
@ -139,29 +143,33 @@ function selectBoatChange() {
|
||||
boatSelect.addEventListener(
|
||||
"addItem",
|
||||
function (e) {
|
||||
|
||||
const event = e as ChoiceBoatEvent;
|
||||
boat_reserved_today = event.detail.customProperties.boat_reserved_today;
|
||||
if (boat_reserved_today){
|
||||
alert(event.detail.label.trim()+' wurde heute reserviert. Bitte kontrolliere, dass du die Reservierung nicht störst.');
|
||||
}
|
||||
if (boat_reserved_today) {
|
||||
alert(
|
||||
event.detail.label.trim() +
|
||||
" wurde heute reserviert. Bitte kontrolliere, dass du die Reservierung nicht störst."
|
||||
);
|
||||
}
|
||||
boat_in_ottensheim = event.detail.customProperties.boat_in_ottensheim;
|
||||
|
||||
const amount_seats = event.detail.customProperties.amount_seats;
|
||||
setMaxAmountRowers("newrower", amount_seats);
|
||||
|
||||
let only_steering = <HTMLSelectElement>document.querySelector('#shipmaster_only_steering');
|
||||
if (event.detail.customProperties.default_handoperated) {
|
||||
only_steering.setAttribute('checked', 'true');
|
||||
}else {
|
||||
only_steering.removeAttribute('checked');
|
||||
}
|
||||
let only_steering = <HTMLSelectElement>(
|
||||
document.querySelector("#shipmaster_only_steering")
|
||||
);
|
||||
if (event.detail.customProperties.default_handoperated) {
|
||||
only_steering.setAttribute("checked", "true");
|
||||
} else {
|
||||
only_steering.removeAttribute("checked");
|
||||
}
|
||||
|
||||
if (event.detail.customProperties.convert_handoperated_possible) {
|
||||
only_steering.removeAttribute('disabled');
|
||||
}else {
|
||||
only_steering.setAttribute('disabled', 'disabled');
|
||||
}
|
||||
if (event.detail.customProperties.convert_handoperated_possible) {
|
||||
only_steering.removeAttribute("readonly");
|
||||
} else {
|
||||
only_steering.setAttribute("readonly", "readonly");
|
||||
}
|
||||
|
||||
const destination = <HTMLSelectElement>(
|
||||
document.querySelector("#destination")
|
||||
@ -170,22 +178,35 @@ function selectBoatChange() {
|
||||
|
||||
if (event.detail.customProperties.owner) {
|
||||
choiceObjects["newrower"].setChoiceByValue(
|
||||
event.detail.customProperties.owner.toString(),
|
||||
event.detail.customProperties.owner.toString()
|
||||
);
|
||||
|
||||
if(event.detail.value === '36') {
|
||||
/** custom code for Etsch */
|
||||
choiceObjects["newrower"].setChoiceByValue("81");
|
||||
if (event.detail.value === "36") {
|
||||
/** custom code for Etsch */
|
||||
choiceObjects["newrower"].setChoiceByValue("81");
|
||||
}
|
||||
}else if (typeof loggedin_user_id !== 'undefined'){
|
||||
choiceObjects["newrower"].setChoiceByValue(loggedin_user_id);
|
||||
}
|
||||
} else if (typeof loggedin_user_id !== "undefined") {
|
||||
const currentSelection = choiceObjects["newrower"].getValue();
|
||||
let selectedItemsCount: number;
|
||||
if (Array.isArray(currentSelection)) {
|
||||
selectedItemsCount = currentSelection.length;
|
||||
} else {
|
||||
selectedItemsCount = currentSelection !== undefined ? 1 : 0;
|
||||
}
|
||||
if (selectedItemsCount == 0) {
|
||||
choiceObjects["newrower"].setChoiceByValue(loggedin_user_id);
|
||||
}
|
||||
}
|
||||
|
||||
const inputElement = document.getElementById(
|
||||
"departure",
|
||||
"departure"
|
||||
) as HTMLInputElement;
|
||||
const now = new Date();
|
||||
const formattedDateTime = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}T${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
||||
const formattedDateTime = `${now.getFullYear()}-${String(
|
||||
now.getMonth() + 1
|
||||
).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}T${String(
|
||||
now.getHours()
|
||||
).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}`;
|
||||
|
||||
inputElement.value = formattedDateTime;
|
||||
|
||||
@ -194,7 +215,7 @@ function selectBoatChange() {
|
||||
);
|
||||
destinput.dispatchEvent(new Event("input"));
|
||||
},
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
choiceObjects[boatSelect.id] = boatChoice;
|
||||
@ -222,16 +243,16 @@ function reloadPage() {
|
||||
|
||||
function setMaxAmountRowers(name: string, rowers: number) {
|
||||
if (choiceObjects[name]) {
|
||||
choiceObjects[name].removeActiveItems(-1);
|
||||
//let curSelection = choiceObjects[name].getValue(true);
|
||||
//let amount_to_delete = (<any>curSelection).length - rowers;
|
||||
//choiceObjects[name].removeActiveItems(-1);
|
||||
let curSelection = choiceObjects[name].getValue(true);
|
||||
let amount_to_delete = (<any>curSelection).length - rowers;
|
||||
|
||||
//if (amount_to_delete > 0){
|
||||
// let to_delete = (<any>curSelection).slice(-amount_to_delete);
|
||||
// for (let del of to_delete) {
|
||||
// choiceObjects[name].removeActiveItemsByValue(del);
|
||||
// }
|
||||
//}
|
||||
if (amount_to_delete > 0) {
|
||||
let to_delete = (<any>curSelection).slice(-amount_to_delete);
|
||||
for (let del of to_delete) {
|
||||
choiceObjects[name].removeActiveItemsByValue(del);
|
||||
}
|
||||
}
|
||||
|
||||
let input = <HTMLElement>document.querySelector("#" + name);
|
||||
if (input) {
|
||||
@ -239,24 +260,24 @@ function setMaxAmountRowers(name: string, rowers: number) {
|
||||
if (rowers === 0) {
|
||||
choiceObjects[name].disable();
|
||||
input.parentElement?.parentElement?.parentElement?.classList.add(
|
||||
"hidden",
|
||||
"hidden"
|
||||
);
|
||||
input.parentElement?.parentElement?.parentElement?.classList.add(
|
||||
"md:block",
|
||||
"md:block"
|
||||
);
|
||||
input.parentElement?.parentElement?.parentElement?.classList.add(
|
||||
"opacity-50",
|
||||
"opacity-50"
|
||||
);
|
||||
} else {
|
||||
choiceObjects[name].enable();
|
||||
input.parentElement?.parentElement?.parentElement?.classList.remove(
|
||||
"hidden",
|
||||
"hidden"
|
||||
);
|
||||
input.parentElement?.parentElement?.parentElement?.classList.remove(
|
||||
"md:block",
|
||||
"md:block"
|
||||
);
|
||||
input.parentElement?.parentElement?.parentElement?.classList.remove(
|
||||
"opacity-50",
|
||||
"opacity-50"
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -293,7 +314,7 @@ function setMaxAmountRowers(name: string, rowers: number) {
|
||||
|
||||
function initBoatActions() {
|
||||
const boatSelects = document.querySelectorAll(
|
||||
'.boats-js[data-onclick="true"]',
|
||||
'.boats-js[data-onclick="true"]'
|
||||
);
|
||||
if (boatSelects) {
|
||||
Array.prototype.forEach.call(boatSelects, (select: HTMLInputElement) => {
|
||||
@ -366,7 +387,7 @@ function initNewChoice(select: HTMLInputElement) {
|
||||
steering_person.setAttribute("required", "required");
|
||||
}
|
||||
const choice = new Choices(select, {
|
||||
searchFields: ['label', 'value', 'customProperties.searchableText'],
|
||||
searchFields: ["label", "value", "customProperties.searchableText"],
|
||||
removeItemButton: true,
|
||||
loadingText: "Wird geladen...",
|
||||
noResultsText: "Keine Ergebnisse gefunden",
|
||||
@ -449,7 +470,7 @@ function initNewChoice(select: HTMLInputElement) {
|
||||
steeringSelect.add(new Option(name, user_id));
|
||||
}
|
||||
},
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
select.addEventListener(
|
||||
@ -478,7 +499,7 @@ function initNewChoice(select: HTMLInputElement) {
|
||||
}
|
||||
}
|
||||
},
|
||||
false,
|
||||
false
|
||||
);
|
||||
|
||||
choiceObjects[select.id] = choice;
|
||||
@ -508,7 +529,7 @@ function initToggle() {
|
||||
}
|
||||
sessionStorage.setItem(
|
||||
"tripsFilter",
|
||||
JSON.stringify(Array.from(filterMap.entries())),
|
||||
JSON.stringify(Array.from(filterMap.entries()))
|
||||
);
|
||||
}
|
||||
resetFilteredElements();
|
||||
@ -535,7 +556,7 @@ function initToggle() {
|
||||
} else {
|
||||
sessionStorage.setItem(
|
||||
"tripsFilter",
|
||||
JSON.stringify(Array.from(filterObject.entries())),
|
||||
JSON.stringify(Array.from(filterObject.entries()))
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -547,14 +568,14 @@ function resetFilteredElements() {
|
||||
hiddenElements,
|
||||
(hiddenElement: HTMLButtonElement) => {
|
||||
hiddenElement.classList.remove("hidden");
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function triggerFilterAction(activeFilter: any) {
|
||||
const activeBtn = document.querySelector(
|
||||
'button[data-action="' + activeFilter + '"]',
|
||||
'button[data-action="' + activeFilter + '"]'
|
||||
);
|
||||
if (activeBtn) {
|
||||
activeBtn.setAttribute("aria-pressed", "true");
|
||||
@ -654,7 +675,7 @@ function initSidebar() {
|
||||
sidebar.toggle();
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -751,14 +772,15 @@ function addRelationMagic(bodyElement: HTMLElement) {
|
||||
dataList.options,
|
||||
function (option) {
|
||||
return option.value === field.value;
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (option && option.value !== ""){
|
||||
// Get distance
|
||||
const distance = option.getAttribute("distance");
|
||||
if (distance && relatedField.value === "") relatedField.value = distance;
|
||||
}
|
||||
if (option && option.value !== "") {
|
||||
// Get distance
|
||||
const distance = option.getAttribute("distance");
|
||||
if (distance && relatedField.value === "")
|
||||
relatedField.value = distance;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ test("cox can create and delete trip", async ({ page }) => {
|
||||
await expect(page.locator("body")).toContainText("18:00 Uhr (cox) Details");
|
||||
|
||||
await page.goto("/planned");
|
||||
await page.getByRole("link", { name: "Details" }).click();
|
||||
await page.getByRole('link', { name: 'Details' }).nth(1).click();
|
||||
await page.getByRole("link", { name: "Termin löschen" }).click();
|
||||
await expect(page.locator("body")).toContainText("Erfolgreich gelöscht!");
|
||||
});
|
||||
@ -52,11 +52,11 @@ test.describe("cox can edit trips", () => {
|
||||
|
||||
test("edit remarks", async () => {
|
||||
await sharedPage.goto("/planned");
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click();
|
||||
await sharedPage.locator("#sidebar #notes").click();
|
||||
await sharedPage.locator("#sidebar #notes").fill("Meine Anmerkung");
|
||||
await sharedPage.getByRole("button", { name: "Speichern" }).click();
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||
"Meine Anmerkung",
|
||||
);
|
||||
@ -68,14 +68,14 @@ test.describe("cox can edit trips", () => {
|
||||
|
||||
test("add and remove guest", async () => {
|
||||
await sharedPage.goto("/planned");
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||
await sharedPage.locator("#sidebar #user_note").click();
|
||||
await sharedPage.locator("#sidebar #user_note").fill("Mein Gast");
|
||||
await sharedPage.getByRole("button", { name: "Gast hinzufügen" }).click();
|
||||
await expect(sharedPage.locator("body")).toContainText(
|
||||
"Erfolgreich angemeldet!",
|
||||
);
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||
"Freie Plätze: 4",
|
||||
);
|
||||
@ -90,7 +90,7 @@ test.describe("cox can edit trips", () => {
|
||||
await expect(sharedPage.locator("body")).toContainText(
|
||||
"Erfolgreich abgemeldet!",
|
||||
);
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||
"Freie Plätze: 5",
|
||||
);
|
||||
@ -108,7 +108,7 @@ test.describe("cox can edit trips", () => {
|
||||
|
||||
test("change amount rower", async () => {
|
||||
await sharedPage.goto("/planned");
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||
"Freie Plätze: 5",
|
||||
);
|
||||
@ -122,7 +122,7 @@ test.describe("cox can edit trips", () => {
|
||||
|
||||
test("call off trip", async () => {
|
||||
await sharedPage.goto("/planned");
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
|
||||
await expect(sharedPage.locator("#sidebar")).toContainText(
|
||||
"Freie Plätze: 3",
|
||||
);
|
||||
@ -137,7 +137,7 @@ test.describe("cox can edit trips", () => {
|
||||
|
||||
test.afterAll(async () => {
|
||||
await sharedPage.goto("/planned");
|
||||
await sharedPage.getByRole("link", { name: "Details" }).click();
|
||||
await sharedPage.getByRole('link', { name: 'Details' }).nth(1).click();
|
||||
await sharedPage.getByRole("link", { name: "Termin löschen" }).click();
|
||||
await sharedPage.close();
|
||||
});
|
||||
|
@ -102,6 +102,28 @@ test("Cox can start and finish trip", async ({ page }, testInfo) => {
|
||||
await expect(page.locator('body')).toContainText('(cox2)');
|
||||
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
|
||||
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
|
||||
|
||||
|
||||
//Ausloggen...
|
||||
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
|
||||
await page.getByRole('link', { name: 'Ausloggen' }).click();
|
||||
// Login as admin
|
||||
await page.getByPlaceholder("Name").click();
|
||||
await page.getByPlaceholder("Name").fill("main");
|
||||
await page.getByPlaceholder("Name").press("Tab");
|
||||
await page.getByPlaceholder("Passwort").fill("admin");
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
|
||||
await page.goto("/log/show");
|
||||
await page.getByText('(cox2)').click();
|
||||
page.once("dialog", (dialog) => {
|
||||
dialog.accept().catch(() => {});
|
||||
});
|
||||
await page.getByRole('link', { name: 'Löschen' }).click();
|
||||
|
||||
//Ausloggen...
|
||||
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
|
||||
await page.getByRole('link', { name: 'Ausloggen' }).click();
|
||||
});
|
||||
|
||||
test("Kiosk can start and cancel trip", async ({ page }, testInfo) => {
|
||||
@ -189,4 +211,173 @@ test("Kiosk can start and finish trip", async ({ page }, testInfo) => {
|
||||
await expect(page.locator('body')).toContainText('(cox2)');
|
||||
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
|
||||
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
|
||||
|
||||
|
||||
|
||||
//Ausloggen...
|
||||
await page.context().clearCookies();
|
||||
await page.goto("/auth");
|
||||
// Login as admin
|
||||
await page.getByPlaceholder("Name").click();
|
||||
await page.getByPlaceholder("Name").fill("main");
|
||||
await page.getByPlaceholder("Name").press("Tab");
|
||||
await page.getByPlaceholder("Passwort").fill("admin");
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
|
||||
await page.goto("/log/show");
|
||||
await page.getByText('(cox2)').click();
|
||||
page.once("dialog", (dialog) => {
|
||||
dialog.accept().catch(() => {});
|
||||
});
|
||||
await page.getByRole('link', { name: 'Löschen' }).click();
|
||||
|
||||
//Ausloggen...
|
||||
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
|
||||
await page.getByRole('link', { name: 'Ausloggen' }).click();
|
||||
});
|
||||
|
||||
test("Cox can start and finish trip with cox steering only", async ({ page }, testInfo) => {
|
||||
await page.goto("/auth");
|
||||
await page.getByPlaceholder("Name").click();
|
||||
await page.getByPlaceholder("Name").fill("cox2");
|
||||
await page.getByPlaceholder("Name").press("Tab");
|
||||
await page.getByPlaceholder("Passwort").fill("cox");
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
|
||||
await page.goto("/");
|
||||
await page.getByRole("link", { name: "Ausfahrt eintragen" }).click();
|
||||
if (testInfo.project.name.includes("Mobile")) {
|
||||
// No left boat selector on mobile views
|
||||
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
|
||||
await page.getByRole("option", { name: "cox_only_steering_boat" }).click();
|
||||
} else {
|
||||
await page.getByText('2+', { exact: true }).click();
|
||||
await page.getByText("cox_only_steering_boat", { exact: true }).click();
|
||||
}
|
||||
|
||||
// Trip starts 2 hours ago
|
||||
const datetimeSelector = '#departure';
|
||||
const currentValue = await page.$eval(datetimeSelector, el => el.value);
|
||||
const currentDate = new Date(currentValue);
|
||||
currentDate.setMinutes(currentDate.getMinutes());
|
||||
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
|
||||
const newDatetime = currentDate.toISOString().slice(0, 16);
|
||||
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
|
||||
|
||||
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
|
||||
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
|
||||
"cox",
|
||||
);
|
||||
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Ausfahrt erfolgreich hinzugefügt",
|
||||
);
|
||||
await expect(page.locator("body")).toContainText("cox_only_steering_boat");
|
||||
|
||||
await page.goto("/log");
|
||||
await page.locator("div:nth-child(2) > .border-0").click();
|
||||
|
||||
await page.getByRole("combobox", { name: "Destination" }).click();
|
||||
await page.getByRole("combobox", { name: "Destination" }).fill("Ottensheim");
|
||||
await page.getByRole("button", { name: "Ausfahrt beenden" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Ausfahrt korrekt eingetragen",
|
||||
);
|
||||
|
||||
await page.goto('/log/show');
|
||||
await expect(page.locator('body')).toContainText('cox_only_steering_boat');
|
||||
await expect(page.locator('body')).toContainText('(cox2 - handgesteuert)');
|
||||
await expect(page.locator('body')).toContainText('Ottensheim (25 km)');
|
||||
|
||||
|
||||
|
||||
//Ausloggen...
|
||||
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
|
||||
await page.getByRole('link', { name: 'Ausloggen' }).click();
|
||||
// Login as admin
|
||||
await page.getByPlaceholder("Name").click();
|
||||
await page.getByPlaceholder("Name").fill("main");
|
||||
await page.getByPlaceholder("Name").press("Tab");
|
||||
await page.getByPlaceholder("Passwort").fill("admin");
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
|
||||
await page.goto("/log/show");
|
||||
await page.getByText('(cox2 - handgesteuert)').click();
|
||||
page.once("dialog", (dialog) => {
|
||||
dialog.accept().catch(() => {});
|
||||
});
|
||||
await page.getByRole('link', { name: 'Löschen' }).click();
|
||||
|
||||
//Ausloggen...
|
||||
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
|
||||
await page.getByRole('link', { name: 'Ausloggen' }).click();
|
||||
});
|
||||
|
||||
test("Kiosk can start and finish trip in one stop", async ({ page }, testInfo) => {
|
||||
await page.goto("/log/kiosk/ekrv2019/Linz");
|
||||
|
||||
if (testInfo.project.name.includes("Mobile")) {
|
||||
// No left boat selector on mobile views
|
||||
await page.getByText('-- Wähle ein Boot aus ---').nth(1).click();
|
||||
await page.getByRole("option", { name: "Joe" }).click();
|
||||
} else {
|
||||
await page.getByText('2x', { exact: true }).click();
|
||||
await page.getByText("Joe", { exact: true }).click();
|
||||
}
|
||||
await page.getByPlaceholder("Ruderer auswählen").click();
|
||||
await page.getByRole("option", { name: "rower2" }).click();
|
||||
await page.getByRole("option", { name: "cox2" }).click();
|
||||
await expect(page.getByRole("listbox")).toContainText(
|
||||
"Nur 2 Ruderer können hinzugefügt werden",
|
||||
);
|
||||
|
||||
// Trip starts 2 hours ago
|
||||
const datetimeSelector = '#departure';
|
||||
const currentValue = await page.$eval(datetimeSelector, el => el.value);
|
||||
const currentDate = new Date(currentValue);
|
||||
currentDate.setMinutes(currentDate.getMinutes());
|
||||
currentDate.setHours(currentDate.getHours() - new Date().getTimezoneOffset()/60 - 2);
|
||||
const newDatetime = currentDate.toISOString().slice(0, 16);
|
||||
await page.$eval(datetimeSelector, (el, value) => el.value = value, newDatetime);
|
||||
|
||||
await page.getByLabel('Ankunftszeit').click();
|
||||
await page.locator('#destination').fill('a');
|
||||
await page.getByLabel('Distanz').fill('1');
|
||||
|
||||
await expect(page.locator("#shipmaster-newrowerjs")).toContainText("cox");
|
||||
await expect(page.locator("#steering_person-newrowerjs")).toContainText(
|
||||
"rower2 cox",
|
||||
);
|
||||
await page.getByRole("button", { name: "Ausfahrt eintragen" }).click();
|
||||
await expect(page.locator("body")).toContainText(
|
||||
"Ausfahrt erfolgreich hinzugefügt",
|
||||
);
|
||||
await page.getByRole('link', { name: 'Logbuch' }).click();
|
||||
await expect(page.locator('body')).toContainText('Joe');
|
||||
await expect(page.locator('body')).toContainText('(cox2)');
|
||||
await expect(page.locator('body')).toContainText('a (1 km)');
|
||||
await expect(page.locator('body')).toContainText('Ruderer: cox2, rower2');
|
||||
|
||||
|
||||
|
||||
//Ausloggen...
|
||||
await page.context().clearCookies();
|
||||
await page.goto("/auth");
|
||||
// Login as admin
|
||||
await page.getByPlaceholder("Name").click();
|
||||
await page.getByPlaceholder("Name").fill("main");
|
||||
await page.getByPlaceholder("Name").press("Tab");
|
||||
await page.getByPlaceholder("Passwort").fill("admin");
|
||||
await page.getByPlaceholder("Passwort").press("Enter");
|
||||
|
||||
await page.goto("/log/show");
|
||||
await page.getByText('(cox2)').click();
|
||||
page.once("dialog", (dialog) => {
|
||||
dialog.accept().catch(() => {});
|
||||
});
|
||||
await page.getByRole('link', { name: 'Löschen' }).click();
|
||||
|
||||
//Ausloggen...
|
||||
await page.getByRole('banner').getByRole('link', { name: 'Logbuch' }).click();
|
||||
await page.getByRole('link', { name: 'Ausloggen' }).click();
|
||||
});
|
||||
|
@ -17,7 +17,8 @@ CREATE TABLE IF NOT EXISTS "user" (
|
||||
"phone" text,
|
||||
"address" text,
|
||||
"family_id" INTEGER REFERENCES family(id),
|
||||
"membership_pdf" BLOB
|
||||
"membership_pdf" BLOB,
|
||||
"user_token" TEXT NOT NULL DEFAULT (lower(hex(randomblob(16))))
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "family" (
|
||||
@ -213,3 +214,9 @@ CREATE TABLE IF NOT EXISTS "trailer_reservation" (
|
||||
"created_at" datetime not null default CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS "distance" (
|
||||
"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
"destination" text NOT NULL,
|
||||
"distance_in_km" integer NOT NULL
|
||||
);
|
||||
|
||||
|
73
notes.md
73
notes.md
@ -1,73 +0,0 @@
|
||||
# Wordpress auth
|
||||
|
||||
Add the following code to `wp-content/themes/bravada/functions.php`:
|
||||
|
||||
```
|
||||
function rot_auth( $user, $username, $password ){
|
||||
// Make sure a username and password are present for us to work with
|
||||
if($username == '' || $password == '') return;
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, 'https://app.rudernlinz.at/wikiauth');
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, "name=$username&password=$password");
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
// Execute the cURL session and get the response
|
||||
$response = curl_exec($ch);
|
||||
|
||||
// Check for cURL errors
|
||||
if(curl_errno($ch)){
|
||||
$user = new WP_Error( 'denied', __('Curl error: ' . curl_error($ch)) );
|
||||
}
|
||||
|
||||
// Close the cURL session
|
||||
curl_close($ch);
|
||||
|
||||
|
||||
if (strpos($response, 'SUCC') !== false) {
|
||||
$user = get_user_by('login', $username);
|
||||
|
||||
if (!$user) {
|
||||
// User does not exist, create a new one
|
||||
$userdata = array(
|
||||
'user_email' => $username,
|
||||
'user_login' => $username,
|
||||
'first_name' => $username,
|
||||
'last_name' => ''
|
||||
);
|
||||
$new_user_id = wp_insert_user($userdata);
|
||||
|
||||
if (!is_wp_error($new_user_id)) {
|
||||
// Load the new user info
|
||||
$user = new WP_User($new_user_id);
|
||||
|
||||
// Set role based on username
|
||||
if ($username == 'Philipp Hofer' || $username == 'Marie Birner') {
|
||||
$user->set_role('administrator');
|
||||
} else {
|
||||
$user->set_role('editor');
|
||||
}
|
||||
} else {
|
||||
// Handle error in user creation
|
||||
return $new_user_id;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
} else {
|
||||
$user = new WP_Error( 'denied', __("Falscher Benutzername/Passwort. Verwendest du deine Accountdaten vom Ruderassistenten?") );
|
||||
}
|
||||
|
||||
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
// Comment this line if you wish to fall back on WordPress authentication
|
||||
// Useful for times when the external service is offline
|
||||
remove_action('authenticate', 'wp_authenticate_username_password', 20);
|
||||
|
||||
add_filter( 'authenticate', 'rot_auth', 10, 3 );
|
||||
```
|
17
seeds.sql
17
seeds.sql
@ -9,6 +9,9 @@ INSERT INTO "role" (name) VALUES ('paid');
|
||||
INSERT INTO "role" (name) VALUES ('Vorstand');
|
||||
INSERT INTO "role" (name) VALUES ('Bootsführer');
|
||||
INSERT INTO "role" (name) VALUES ('schnupperant');
|
||||
INSERT INTO "role" (name) VALUES ('kassier');
|
||||
INSERT INTO "role" (name) VALUES ('schriftfuehrer');
|
||||
INSERT INTO "role" (name) VALUES ('no-einschreibgebuehr');
|
||||
INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(1,1);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(1,2);
|
||||
@ -35,12 +38,17 @@ INSERT INTO "user_role" (user_id, role_id) VALUES(8,5);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(8,7);
|
||||
INSERT INTO "user" (name, pw) VALUES('Vorstandsmitglied', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(9,5);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(9,9);
|
||||
INSERT INTO "user" (name, pw) VALUES('main', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(10,1);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(10,2);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(10,5);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(10,6);
|
||||
INSERT INTO "user_role" (user_id, role_id) VALUES(10,9);
|
||||
|
||||
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, '1970-01-01', 'trip_details for a planned event');
|
||||
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, date('now'), 'trip_details for a planned event');
|
||||
INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1);
|
||||
|
||||
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('11:00', 1, '1970-01-02', 'trip_details for trip from cox');
|
||||
INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('11:00', 1, date('now', '+1 day'), 'trip_details for trip from cox');
|
||||
INSERT INTO "trip" (cox_id, trip_details_id) VALUES(4, 2);
|
||||
|
||||
INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Regatta', 'Regatta!', 'Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?', '🏅');
|
||||
@ -55,6 +63,7 @@ INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Kaputtes Boot :-('
|
||||
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Sehr kaputtes Boot :-((', 7, 1);
|
||||
INSERT INTO "boat" (name, amount_seats, location_id) VALUES ('Ottensheim Boot', 7, 2);
|
||||
INSERT INTO "boat" (name, amount_seats, location_id, owner) VALUES ('second_private_boat_from_rower', 1, 1, 2);
|
||||
INSERT INTO "boat" (name, amount_seats, location_id, default_shipmaster_only_steering) VALUES ('cox_only_steering_boat', 3, 1, true);
|
||||
INSERT INTO "logbook_type" (name) VALUES ('Wanderfahrt');
|
||||
INSERT INTO "logbook_type" (name) VALUES ('Regatta');
|
||||
INSERT INTO "logbook" (boat_id, shipmaster,steering_person, shipmaster_only_steering, departure) VALUES (2, 2, 2, false, strftime('%Y', 'now') || '-12-24 10:00');
|
||||
@ -66,3 +75,5 @@ INSERT INTO "boat_damage" (boat_id, desc, user_id_created, created_at, lock_boat
|
||||
INSERT INTO "notification" (user_id, message, category) VALUES (1, 'This is a test notification', 'test-cat');
|
||||
INSERT INTO "trailer" (name) VALUES('Großer Hänger');
|
||||
INSERT INTO "trailer" (name) VALUES('Kleiner Hänger');
|
||||
insert into distance(destination, distance_in_km) values('Ottensheim', 25);
|
||||
|
||||
|
12
src/lib.rs
12
src/lib.rs
@ -10,6 +10,18 @@ pub mod rest;
|
||||
|
||||
pub mod scheduled;
|
||||
|
||||
pub(crate) const AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD: i64 = 10;
|
||||
pub(crate) const RENNRUDERBEITRAG: i32 = 11000;
|
||||
pub(crate) const BOAT_STORAGE: i32 = 4500;
|
||||
pub(crate) const FAMILY_TWO: i32 = 30000;
|
||||
pub(crate) const FAMILY_THREE_OR_MORE: i32 = 35000;
|
||||
pub(crate) const STUDENT_OR_PUPIL: i32 = 8000;
|
||||
pub(crate) const REGULAR: i32 = 22000;
|
||||
pub(crate) const UNTERSTUETZEND: i32 = 2500;
|
||||
pub(crate) const FOERDERND: i32 = 8500;
|
||||
pub(crate) const SCHECKBUCH: i32 = 3000;
|
||||
pub(crate) const EINSCHREIBGEBUEHR: i32 = 3000;
|
||||
|
||||
#[cfg(test)]
|
||||
#[macro_export]
|
||||
macro_rules! testdb {
|
||||
|
@ -1,5 +1,6 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use chrono::NaiveDateTime;
|
||||
use itertools::Itertools;
|
||||
use rocket::serde::{Deserialize, Serialize};
|
||||
use rocket::FromForm;
|
||||
@ -170,6 +171,16 @@ AND date('now') BETWEEN start_date AND end_date;",
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub(crate) fn cat(&self) -> String {
|
||||
if self.external {
|
||||
"Vereinsfremde Boote".to_string()
|
||||
} else if self.default_shipmaster_only_steering {
|
||||
format!("{}+", self.amount_seats - 1)
|
||||
} else {
|
||||
format!("{}x", self.amount_seats)
|
||||
}
|
||||
}
|
||||
|
||||
async fn boats_to_details(db: &SqlitePool, boats: Vec<Boat>) -> Vec<BoatWithDetails> {
|
||||
let mut res = Vec::new();
|
||||
for boat in boats {
|
||||
@ -180,13 +191,7 @@ AND date('now') BETWEEN start_date AND end_date;",
|
||||
if boat.is_locked(db).await {
|
||||
damage = BoatDamage::Locked;
|
||||
}
|
||||
let cat = if boat.external {
|
||||
"Vereinsfremde Boote".to_string()
|
||||
} else if boat.default_shipmaster_only_steering {
|
||||
format!("{}+", boat.amount_seats - 1)
|
||||
} else {
|
||||
format!("{}x", boat.amount_seats)
|
||||
};
|
||||
let cat = boat.cat();
|
||||
|
||||
res.push(BoatWithDetails {
|
||||
damage,
|
||||
@ -391,6 +396,39 @@ ORDER BY amount_seats DESC
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn on_water_between(
|
||||
&self,
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
dep: NaiveDateTime,
|
||||
arr: NaiveDateTime,
|
||||
) -> bool {
|
||||
let dep = dep.format("%Y-%m-%dT%H:%M").to_string();
|
||||
let arr = arr.format("%Y-%m-%dT%H:%M").to_string();
|
||||
|
||||
sqlx::query!(
|
||||
"SELECT COUNT(*) AS overlap_count
|
||||
FROM logbook
|
||||
WHERE boat_id = ?
|
||||
AND (
|
||||
(departure <= ? AND arrival >= ?) -- Existing entry covers the entire new period
|
||||
OR (departure >= ? AND departure < ?) -- Existing entry starts during the new period
|
||||
OR (arrival > ? AND arrival <= ?) -- Existing entry ends during the new period
|
||||
);",
|
||||
self.id,
|
||||
arr,
|
||||
arr,
|
||||
dep,
|
||||
dep,
|
||||
dep,
|
||||
arr
|
||||
)
|
||||
.fetch_one(db.deref_mut())
|
||||
.await
|
||||
.unwrap()
|
||||
.overlap_count
|
||||
> 0
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
33
src/model/distance.rs
Normal file
33
src/model/distance.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
#[derive(FromRow, Serialize, Clone, Debug)]
|
||||
pub struct Distance {
|
||||
pub id: i64,
|
||||
pub destination: String,
|
||||
pub distance_in_km: i64,
|
||||
}
|
||||
|
||||
impl Distance {
|
||||
/// Return all default `distance`s, ordered by usage in logbook entries
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"SELECT
|
||||
d.id,
|
||||
d.destination,
|
||||
d.distance_in_km
|
||||
FROM
|
||||
distance d
|
||||
LEFT JOIN
|
||||
logbook l ON d.destination = l.destination AND d.distance_in_km = l.distance_in_km
|
||||
GROUP BY
|
||||
d.id, d.destination, d.distance_in_km
|
||||
ORDER BY
|
||||
COUNT(l.id) DESC, d.destination ASC;"
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
}
|
@ -8,7 +8,10 @@ use ics::{
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, Row, SqlitePool};
|
||||
|
||||
use super::{notification::Notification, tripdetails::TripDetails, triptype::TripType, user::User};
|
||||
use super::{
|
||||
notification::Notification, role::Role, tripdetails::TripDetails, triptype::TripType,
|
||||
user::User,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Clone, FromRow, Debug, PartialEq)]
|
||||
pub struct Event {
|
||||
@ -180,6 +183,17 @@ INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id",
|
||||
.unwrap() //TODO: fixme
|
||||
}
|
||||
|
||||
pub async fn all_with_user(db: &SqlitePool, user: &User) -> Vec<Event> {
|
||||
let mut ret = Vec::new();
|
||||
let events = Self::all(db).await;
|
||||
for event in events {
|
||||
if event.is_rower_registered(db, user).await {
|
||||
ret.push(event);
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
//TODO: add tests
|
||||
pub async fn is_rower_registered(&self, db: &SqlitePool, user: &User) -> bool {
|
||||
let is_rower = sqlx::query!(
|
||||
@ -213,12 +227,40 @@ WHERE trip_details.id=?
|
||||
.ok()
|
||||
}
|
||||
|
||||
async fn advertise(db: &SqlitePool, day: &str, planned_starting_time: &str, name: &str) {
|
||||
let donau = Role::find_by_name(db, "Donau Linz").await.unwrap();
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&donau,
|
||||
&format!("Am {} um {} wurde ein neues Event angelegt: {} Wir freuen uns wenn du dabei mitmachst, die Anmeldung ist ab sofort offen :-)", day, planned_starting_time, name),
|
||||
"Neues Event",
|
||||
Some(&format!("/planned#{day}")),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
pub async fn create(
|
||||
db: &SqlitePool,
|
||||
name: &str,
|
||||
planned_amount_cox: i32,
|
||||
always_show: bool,
|
||||
trip_details: &TripDetails,
|
||||
) {
|
||||
if trip_details.always_show {
|
||||
Self::advertise(
|
||||
db,
|
||||
&trip_details.day,
|
||||
&trip_details.planned_starting_time,
|
||||
name,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if always_show && !trip_details.always_show {
|
||||
trip_details.set_always_show(db, true).await;
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)",
|
||||
name,
|
||||
@ -258,6 +300,16 @@ WHERE trip_details.id=?
|
||||
.await
|
||||
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
|
||||
|
||||
if !tripdetails.always_show && update.always_show {
|
||||
Self::advertise(
|
||||
db,
|
||||
&tripdetails.day,
|
||||
&tripdetails.planned_starting_time,
|
||||
update.name,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if update.max_people == 0 && !was_already_cancelled {
|
||||
let coxes = Registration::all_cox(db, self.id).await;
|
||||
for user in coxes {
|
||||
@ -353,38 +405,41 @@ WHERE trip_details.id=?
|
||||
|
||||
let events = Event::all(db).await;
|
||||
for event in events {
|
||||
let mut vevent =
|
||||
ics::Event::new(format!("{}@rudernlinz.at", event.id), "19900101T180000");
|
||||
vevent.push(DtStart::new(format!(
|
||||
"{}T{}00",
|
||||
event.day.replace('-', ""),
|
||||
event.planned_starting_time.replace(':', "")
|
||||
)));
|
||||
let tripdetails = event.trip_details(db).await;
|
||||
let mut name = String::new();
|
||||
if event.is_cancelled() {
|
||||
name.push_str("ABGESAGT");
|
||||
if let Some(notes) = &tripdetails.notes {
|
||||
if !notes.is_empty() {
|
||||
name.push_str(&format!(" (Grund: {notes})"))
|
||||
}
|
||||
}
|
||||
|
||||
name.push_str("! :-( ");
|
||||
}
|
||||
name.push_str(&format!("{} ", event.name));
|
||||
|
||||
if let Some(triptype) = tripdetails.triptype(db).await {
|
||||
name.push_str(&format!("• {} ", triptype.name))
|
||||
}
|
||||
vevent.push(Summary::new(name));
|
||||
calendar.add_event(vevent);
|
||||
calendar.add_event(event.get_vevent(db).await);
|
||||
}
|
||||
let mut buf = Vec::new();
|
||||
write!(&mut buf, "{}", calendar).unwrap();
|
||||
String::from_utf8(buf).unwrap()
|
||||
}
|
||||
|
||||
pub(crate) async fn get_vevent(self, db: &SqlitePool) -> ics::Event {
|
||||
let mut vevent = ics::Event::new(format!("{}@rudernlinz.at", self.id), "19900101T180000");
|
||||
vevent.push(DtStart::new(format!(
|
||||
"{}T{}00",
|
||||
self.day.replace('-', ""),
|
||||
self.planned_starting_time.replace(':', "")
|
||||
)));
|
||||
let tripdetails = self.trip_details(db).await;
|
||||
let mut name = String::new();
|
||||
if self.is_cancelled() {
|
||||
name.push_str("ABGESAGT");
|
||||
if let Some(notes) = &tripdetails.notes {
|
||||
if !notes.is_empty() {
|
||||
name.push_str(&format!(" (Grund: {notes})"))
|
||||
}
|
||||
}
|
||||
|
||||
name.push_str("! :-( ");
|
||||
}
|
||||
name.push_str(&format!("{} ", self.name));
|
||||
|
||||
if let Some(triptype) = tripdetails.triptype(db).await {
|
||||
name.push_str(&format!("• {} ", triptype.name))
|
||||
}
|
||||
vevent.push(Summary::new(name));
|
||||
vevent
|
||||
}
|
||||
|
||||
pub async fn trip_details(&self, db: &SqlitePool) -> TripDetails {
|
||||
TripDetails::find_by_id(db, self.trip_details_id)
|
||||
.await
|
||||
@ -397,14 +452,14 @@ mod test {
|
||||
use crate::{model::tripdetails::TripDetails, testdb};
|
||||
|
||||
use super::Event;
|
||||
use chrono::NaiveDate;
|
||||
use chrono::Local;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_get_day() {
|
||||
let pool = testdb!();
|
||||
|
||||
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||
let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
|
||||
assert_eq!(res.len(), 1);
|
||||
}
|
||||
|
||||
@ -414,9 +469,9 @@ mod test {
|
||||
|
||||
let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap();
|
||||
|
||||
Event::create(&pool, "new-event".into(), 2, &trip_details).await;
|
||||
Event::create(&pool, "new-event".into(), 2, false, &trip_details).await;
|
||||
|
||||
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||
let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
|
||||
assert_eq!(res.len(), 2);
|
||||
}
|
||||
|
||||
@ -427,7 +482,7 @@ mod test {
|
||||
|
||||
planned_event.delete(&pool).await.unwrap();
|
||||
|
||||
let res = Event::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await;
|
||||
let res = Event::get_for_day(&pool, Local::now().date_naive()).await;
|
||||
assert_eq!(res.len(), 0);
|
||||
}
|
||||
|
||||
@ -435,7 +490,8 @@ mod test {
|
||||
fn test_ics() {
|
||||
let pool = testdb!();
|
||||
|
||||
let today = Local::now().date_naive().format("%Y%m%d").to_string();
|
||||
let actual = Event::get_ics_feed(&pool).await;
|
||||
assert_eq!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:19700101T100000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n", actual);
|
||||
assert_eq!(format!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:ics-rs\r\nBEGIN:VEVENT\r\nUID:1@rudernlinz.at\r\nDTSTAMP:19900101T180000\r\nDTSTART:{today}T100000\r\nSUMMARY:test-planned-event \r\nEND:VEVENT\r\nEND:VCALENDAR\r\n"), actual);
|
||||
}
|
||||
}
|
||||
|
@ -75,7 +75,7 @@ GROUP BY family.id;"
|
||||
}
|
||||
|
||||
pub async fn members(&self, db: &SqlitePool) -> Vec<User> {
|
||||
sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user WHERE family_id = ?", self.id)
|
||||
sqlx::query_as!(User, "SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user WHERE family_id = ?", self.id)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap()
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::ops::DerefMut;
|
||||
|
||||
use chrono::{Datelike, Local, NaiveDateTime};
|
||||
use chrono::{Datelike, Duration, Local, NaiveDateTime};
|
||||
use rocket::FromForm;
|
||||
use serde::Serialize;
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
@ -31,6 +31,12 @@ impl PartialEq for Logbook {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum Filter {
|
||||
SingleDayOnly,
|
||||
MultiDazOnly,
|
||||
None,
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug, Clone)]
|
||||
pub struct LogToAdd {
|
||||
pub boat_id: i32,
|
||||
@ -60,6 +66,22 @@ pub struct LogToFinalize {
|
||||
pub rowers: Vec<i64>,
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug, Clone)]
|
||||
pub struct LogToUpdate {
|
||||
pub id: i64,
|
||||
pub boat_id: i64,
|
||||
pub shipmaster: i64,
|
||||
pub steering_person: i64,
|
||||
pub shipmaster_only_steering: bool,
|
||||
pub departure: String,
|
||||
pub arrival: Option<String>,
|
||||
pub destination: Option<String>,
|
||||
pub distance_in_km: Option<i64>,
|
||||
pub comments: Option<String>,
|
||||
pub logtype: Option<i64>,
|
||||
pub rowers: Vec<i64>,
|
||||
}
|
||||
|
||||
impl TryFrom<LogToAdd> for LogToFinalize {
|
||||
type Error = String;
|
||||
|
||||
@ -94,6 +116,34 @@ pub struct LogbookWithBoatAndRowers {
|
||||
pub rowers: Vec<User>,
|
||||
}
|
||||
|
||||
impl LogbookWithBoatAndRowers {
|
||||
pub(crate) async fn from(db: &SqlitePool, log: Logbook) -> Self {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
let ret = Self::from_tx(&mut tx, log).await;
|
||||
tx.commit().await.unwrap();
|
||||
ret
|
||||
}
|
||||
|
||||
pub(crate) async fn from_tx(db: &mut Transaction<'_, Sqlite>, log: Logbook) -> Self {
|
||||
Self {
|
||||
rowers: Rower::for_log_tx(db, &log).await,
|
||||
boat: Boat::find_by_id_tx(db, log.boat_id as i32).await.unwrap(),
|
||||
shipmaster_user: User::find_by_id_tx(db, log.shipmaster as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
steering_user: User::find_by_id_tx(db, log.steering_person as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
logbook: log,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum LogbookAdminUpdateError {
|
||||
NotAllowed,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum LogbookUpdateError {
|
||||
NotYourEntry,
|
||||
@ -105,6 +155,9 @@ pub enum LogbookUpdateError {
|
||||
UserNotAllowedToUseBoat,
|
||||
OnlyAllowedToEndTripsEndingToday,
|
||||
TooFast(i64, i64),
|
||||
AlreadyFinalized,
|
||||
ExternalSteeringPersonMustSteerOrShipmaster,
|
||||
BoatAlreadyOnWater,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@ -129,6 +182,8 @@ pub enum LogbookCreateError {
|
||||
OnlyAllowedToEndTripsEndingToday,
|
||||
CantChangeHandoperatableStatusForThisBoat,
|
||||
TooFast(i64, i64),
|
||||
AlreadyFinalized,
|
||||
ExternalSteeringPersonMustSteerOrShipmaster,
|
||||
}
|
||||
|
||||
impl From<LogbookUpdateError> for LogbookCreateError {
|
||||
@ -153,6 +208,11 @@ impl From<LogbookUpdateError> for LogbookCreateError {
|
||||
LogbookCreateError::OnlyAllowedToEndTripsEndingToday
|
||||
}
|
||||
LogbookUpdateError::TooFast(km, min) => LogbookCreateError::TooFast(km, min),
|
||||
LogbookUpdateError::AlreadyFinalized => LogbookCreateError::AlreadyFinalized,
|
||||
LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster => {
|
||||
LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster
|
||||
}
|
||||
LogbookUpdateError::BoatAlreadyOnWater => LogbookCreateError::BoatAlreadyOnWater,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -172,7 +232,7 @@ impl Logbook {
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i32) -> Option<Self> {
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
@ -219,15 +279,7 @@ ORDER BY departure DESC
|
||||
|
||||
let mut ret = Vec::new();
|
||||
for log in logs {
|
||||
ret.push(LogbookWithBoatAndRowers {
|
||||
rowers: Rower::for_log(db, &log).await,
|
||||
boat: Boat::find_by_id(db, log.boat_id as i32).await.unwrap(),
|
||||
shipmaster_user: User::find_by_id(db, log.shipmaster as i32).await.unwrap(),
|
||||
steering_user: User::find_by_id(db, log.steering_person as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
logbook: log,
|
||||
});
|
||||
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
|
||||
}
|
||||
ret
|
||||
}
|
||||
@ -235,6 +287,16 @@ ORDER BY departure DESC
|
||||
pub async fn completed_with_user(
|
||||
db: &SqlitePool,
|
||||
user: &User,
|
||||
) -> Vec<LogbookWithBoatAndRowers> {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
let ret = Self::completed_with_user_tx(&mut tx, user).await;
|
||||
tx.commit().await.unwrap();
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn completed_with_user_tx(
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
user: &User,
|
||||
) -> Vec<LogbookWithBoatAndRowers> {
|
||||
let logs = sqlx::query_as(
|
||||
&format!("
|
||||
@ -242,24 +304,103 @@ ORDER BY departure DESC
|
||||
FROM logbook
|
||||
JOIN rower ON logbook.id = rower.logbook_id
|
||||
WHERE arrival is not null AND rower_id = {}
|
||||
ORDER BY departure DESC
|
||||
ORDER BY arrival DESC
|
||||
", user.id)
|
||||
)
|
||||
.fetch_all(db.deref_mut())
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
let mut ret = Vec::new();
|
||||
for log in logs {
|
||||
ret.push(LogbookWithBoatAndRowers::from_tx(db, log).await);
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn year_first_logbook_entry(db: &SqlitePool, user: &User) -> Option<i32> {
|
||||
let log: Option<Self> = sqlx::query_as(
|
||||
&format!("
|
||||
SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
|
||||
FROM logbook
|
||||
JOIN rower ON logbook.id = rower.logbook_id
|
||||
WHERE arrival is not null AND rower_id = {}
|
||||
ORDER BY arrival
|
||||
LIMIT 1
|
||||
", user.id)
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
if let Some(log) = log {
|
||||
Some(log.arrival.unwrap().year())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn year_last_logbook_entry(db: &SqlitePool, user: &User) -> Option<i32> {
|
||||
let log: Option<Self> = sqlx::query_as(
|
||||
&format!("
|
||||
SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
|
||||
FROM logbook
|
||||
JOIN rower ON logbook.id = rower.logbook_id
|
||||
WHERE arrival is not null AND rower_id = {}
|
||||
ORDER BY arrival DESC
|
||||
LIMIT 1
|
||||
", user.id)
|
||||
)
|
||||
.fetch_optional(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
if let Some(log) = log {
|
||||
Some(log.arrival.unwrap().year())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn completed_wanderfahrten_with_user_over_km_in_year(
|
||||
db: &SqlitePool,
|
||||
user: &User,
|
||||
min_distance: i32,
|
||||
year: i32,
|
||||
filter: Filter,
|
||||
) -> Vec<LogbookWithBoatAndRowers> {
|
||||
let logs: Vec<Logbook> = sqlx::query_as(
|
||||
&format!("
|
||||
SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
|
||||
FROM logbook
|
||||
JOIN rower ON logbook.id = rower.logbook_id
|
||||
WHERE arrival is not null AND rower_id = {} AND logtype = 1 AND distance_in_km >= {} AND arrival like '{}-%'
|
||||
ORDER BY arrival DESC
|
||||
", user.id, min_distance, year)
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
let mut ret = Vec::new();
|
||||
for log in logs {
|
||||
ret.push(LogbookWithBoatAndRowers {
|
||||
rowers: Rower::for_log(db, &log).await,
|
||||
boat: Boat::find_by_id(db, log.boat_id as i32).await.unwrap(),
|
||||
shipmaster_user: User::find_by_id(db, log.shipmaster as i32).await.unwrap(),
|
||||
steering_user: User::find_by_id(db, log.steering_person as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
logbook: log,
|
||||
});
|
||||
let trip_days = log.arrival.unwrap() - log.departure;
|
||||
let trip_days = trip_days.num_days();
|
||||
match filter {
|
||||
Filter::SingleDayOnly => {
|
||||
if trip_days == 0 {
|
||||
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
|
||||
}
|
||||
}
|
||||
Filter::MultiDazOnly => {
|
||||
if trip_days > 0 {
|
||||
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
|
||||
}
|
||||
}
|
||||
Filter::None => {
|
||||
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
@ -275,7 +416,7 @@ ORDER BY departure DESC
|
||||
SELECT id, boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype
|
||||
FROM logbook
|
||||
WHERE arrival is not null AND arrival LIKE '{}-%'
|
||||
ORDER BY departure DESC
|
||||
ORDER BY arrival DESC
|
||||
", year)
|
||||
)
|
||||
.fetch_all(db)
|
||||
@ -284,15 +425,7 @@ ORDER BY departure DESC
|
||||
|
||||
let mut ret = Vec::new();
|
||||
for log in logs {
|
||||
ret.push(LogbookWithBoatAndRowers {
|
||||
rowers: Rower::for_log(db, &log).await,
|
||||
boat: Boat::find_by_id(db, log.boat_id as i32).await.unwrap(),
|
||||
shipmaster_user: User::find_by_id(db, log.shipmaster as i32).await.unwrap(),
|
||||
steering_user: User::find_by_id(db, log.steering_person as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
logbook: log,
|
||||
});
|
||||
ret.push(LogbookWithBoatAndRowers::from(db, log).await);
|
||||
}
|
||||
ret
|
||||
}
|
||||
@ -301,6 +434,7 @@ ORDER BY departure DESC
|
||||
db: &SqlitePool,
|
||||
mut log: LogToAdd,
|
||||
created_by_user: &User,
|
||||
smtp_pw: &str,
|
||||
) -> Result<String, LogbookCreateError> {
|
||||
let Some(boat) = Boat::find_by_id(db, log.boat_id).await else {
|
||||
return Err(LogbookCreateError::BoatNotFound);
|
||||
@ -329,13 +463,12 @@ ORDER BY departure DESC
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
|
||||
let inserted_row = sqlx::query!(
|
||||
"INSERT INTO logbook(boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, arrival, destination, distance_in_km, comments, logtype) VALUES (?,?,?,?,?,?,?,?,?,?) RETURNING id",
|
||||
"INSERT INTO logbook(boat_id, shipmaster, steering_person, shipmaster_only_steering, departure, destination, distance_in_km, comments, logtype) VALUES (?,?,?,?,?,?,?,?,?) RETURNING id",
|
||||
log.boat_id,
|
||||
log.shipmaster,
|
||||
log.steering_person,
|
||||
log.shipmaster_only_steering,
|
||||
log.departure,
|
||||
log.arrival,
|
||||
log.destination,
|
||||
log.distance_in_km,
|
||||
log.comments,
|
||||
@ -349,7 +482,7 @@ ORDER BY departure DESC
|
||||
.unwrap(); //ok
|
||||
|
||||
return match logbook
|
||||
.home_with_transaction(&mut tx, created_by_user, log_to_finalize)
|
||||
.home_with_transaction(&mut tx, created_by_user, log_to_finalize, smtp_pw)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
@ -391,6 +524,18 @@ ORDER BY departure DESC
|
||||
if user.on_water(db).await {
|
||||
return Err(LogbookCreateError::RowerAlreadyOnWater(Box::new(user)));
|
||||
}
|
||||
|
||||
if user.name == "Externe Steuerperson" {
|
||||
if let (Some(steering_id), Some(shipmaster_id)) =
|
||||
(log.steering_person, log.shipmaster)
|
||||
{
|
||||
if steering_id != user.id && shipmaster_id != user.id {
|
||||
return Err(
|
||||
LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !boat.shipmaster_allowed(db, created_by_user).await {
|
||||
@ -437,23 +582,33 @@ ORDER BY departure DESC
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
pub async fn distances(db: &SqlitePool) -> Vec<(String, i64)> {
|
||||
let result = sqlx::query!("SELECT destination, distance_in_km FROM logbook WHERE id IN (SELECT MIN(id) FROM logbook GROUP BY destination) AND destination IS NOT NULL AND distance_in_km IS NOT NULL;")
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap();
|
||||
pub async fn update(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
data: LogToUpdate,
|
||||
user: &User,
|
||||
) -> Result<(), LogbookAdminUpdateError> {
|
||||
if !user.has_role(db, "Vorstand").await {
|
||||
return Err(LogbookAdminUpdateError::NotAllowed);
|
||||
}
|
||||
|
||||
result
|
||||
.into_iter()
|
||||
.filter_map(|r| {
|
||||
if let (Some(destination), Some(distance_in_km)) = (r.destination, r.distance_in_km)
|
||||
{
|
||||
Some((destination, distance_in_km))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
sqlx::query!(
|
||||
"UPDATE logbook SET boat_id=?, shipmaster=?, steering_person=?, shipmaster_only_steering=?, departure=?, arrival=?, destination=?, distance_in_km=?, comments=?, logtype=? WHERE id=?",
|
||||
data.boat_id,
|
||||
data.shipmaster,
|
||||
data.steering_person,
|
||||
data.shipmaster_only_steering,
|
||||
data.departure,
|
||||
data.arrival,
|
||||
data.destination,
|
||||
data.distance_in_km,
|
||||
data.comments,
|
||||
data.logtype,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_rowers(&self, db: &mut Transaction<'_, Sqlite>) {
|
||||
@ -478,9 +633,11 @@ ORDER BY departure DESC
|
||||
db: &SqlitePool,
|
||||
user: &User,
|
||||
log: LogToFinalize,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), LogbookUpdateError> {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
self.home_with_transaction(&mut tx, user, log).await?;
|
||||
self.home_with_transaction(&mut tx, user, log, smtp_pw)
|
||||
.await?;
|
||||
tx.commit().await.unwrap();
|
||||
Ok(())
|
||||
}
|
||||
@ -490,12 +647,17 @@ ORDER BY departure DESC
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
user: &User,
|
||||
mut log: LogToFinalize,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), LogbookUpdateError> {
|
||||
//TODO: extract common tests with `create()`
|
||||
if !user.has_role_tx(db, "Vorstand").await && user.id != self.shipmaster {
|
||||
return Err(LogbookUpdateError::NotYourEntry);
|
||||
}
|
||||
|
||||
if self.arrival.is_some() {
|
||||
return Err(LogbookUpdateError::AlreadyFinalized);
|
||||
}
|
||||
|
||||
let boat = Boat::find_by_id_tx(db, self.boat_id as i32).await.unwrap(); //ok
|
||||
|
||||
if boat.amount_seats == 1 {
|
||||
@ -529,6 +691,10 @@ ORDER BY departure DESC
|
||||
return Err(LogbookUpdateError::ArrivalNotAfterDeparture);
|
||||
}
|
||||
|
||||
if !boat.external && boat.on_water_between(db, dep, arr).await {
|
||||
return Err(LogbookUpdateError::BoatAlreadyOnWater);
|
||||
}
|
||||
|
||||
let duration_in_mins = (arr.and_utc().timestamp() - dep.and_utc().timestamp()) / 60;
|
||||
// Not possible to row < 1 min / 500 m = < 2 min / km
|
||||
let possible_distance_km = duration_in_mins / 2;
|
||||
@ -542,7 +708,12 @@ ORDER BY departure DESC
|
||||
let today = Local::now().date_naive();
|
||||
let day_diff = today - arr.date();
|
||||
let day_diff = day_diff.num_days();
|
||||
if day_diff >= 7 && !user.has_role_tx(db, "admin").await {
|
||||
if day_diff >= 7
|
||||
&& !user.has_role_tx(db, "admin").await
|
||||
&& !user
|
||||
.has_role_tx(db, "allow-retroactive-logbookentries")
|
||||
.await
|
||||
{
|
||||
return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday);
|
||||
}
|
||||
if day_diff < 0 && !user.has_role_tx(db, "admin").await {
|
||||
@ -553,6 +724,19 @@ ORDER BY departure DESC
|
||||
|
||||
self.remove_rowers(db).await;
|
||||
for rower in &log.rowers {
|
||||
let user = User::find_by_id_tx(db, *rower as i32).await.unwrap();
|
||||
if user.name == "Externe Steuerperson" {
|
||||
if let (Some(steering_id), Some(shipmaster_id)) =
|
||||
(log.steering_person, log.shipmaster)
|
||||
{
|
||||
if steering_id != user.id && shipmaster_id != user.id {
|
||||
return Err(
|
||||
LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rower::create(db, self.id, *rower)
|
||||
.await
|
||||
.map_err(|e| LogbookUpdateError::RowerCreateError(*rower, e.to_string()))?;
|
||||
@ -617,21 +801,68 @@ ORDER BY departure DESC
|
||||
).await;
|
||||
}
|
||||
|
||||
for rower in &log.rowers {
|
||||
let user = User::find_by_id_tx(db, *rower as i32).await.unwrap();
|
||||
user.received_new_logentry(db, smtp_pw).await;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete(&self, db: &SqlitePool, user: &User) -> Result<(), LogbookDeleteError> {
|
||||
Log::create(db, format!("{} deleted trip: {self:?}", user.name)).await;
|
||||
|
||||
if user.has_role(db, "admin").await
|
||||
|| user.has_role(db, "Vorstand").await
|
||||
|| user.id == self.shipmaster
|
||||
{
|
||||
sqlx::query!("DELETE FROM logbook WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Logbook of a valid id
|
||||
return Ok(());
|
||||
if self.arrival.is_none() {
|
||||
if user.has_role(db, "admin").await
|
||||
|| user.has_role(db, "Vorstand").await
|
||||
|| user.id == self.shipmaster
|
||||
{
|
||||
let now = Local::now().naive_local();
|
||||
let difference = now - self.departure;
|
||||
if difference > Duration::hours(1) {
|
||||
let vorstand = Role::find_by_name(db, "Vorstand").await.unwrap();
|
||||
let logbook = LogbookWithBoatAndRowers::from(db, self.clone()).await;
|
||||
let mut msg = format!("{} hat folgenden Logbuch-Eintrag jetzt gelöscht, welcher bereits vor über einer Stunde begonnen wurde: Schiffsführer: {}, Steuerperson: {}, Abfahrt: {}", user.name, logbook.steering_user.name, logbook.steering_user.name, logbook.logbook.departure.format("%Y-%m-%d %H:%M"));
|
||||
if let Some(destination) = logbook.logbook.destination {
|
||||
msg.push_str(&format!(", Ziel: {}", destination));
|
||||
} else {
|
||||
msg.push_str(", kein Ziel eingegeben");
|
||||
}
|
||||
msg.push_str(", Ruderer: ");
|
||||
let mut it = logbook.rowers.clone().into_iter().peekable();
|
||||
while let Some(rower) = it.next() {
|
||||
msg.push_str(&rower.name);
|
||||
if it.peek().is_some() {
|
||||
msg.push_str(" + ");
|
||||
}
|
||||
}
|
||||
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&vorstand,
|
||||
&msg,
|
||||
"Ungewöhnliches Verhalten",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
sqlx::query!("DELETE FROM logbook WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Logbook of a valid id
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
// Only admins can delete completed logbook entries
|
||||
if user.has_role(db, "admin").await {
|
||||
sqlx::query!("DELETE FROM logbook WHERE id=?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, because we can only create a Logbook of a valid id
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
Err(LogbookDeleteError::NotYourEntry)
|
||||
}
|
||||
@ -711,6 +942,7 @@ mod test {
|
||||
rowers: vec![4],
|
||||
},
|
||||
&User::find_by_id(&pool, 4).await.unwrap(),
|
||||
"",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -742,6 +974,7 @@ mod test {
|
||||
departure: format!("{}T10:00", start_date),
|
||||
arrival: format!("{}T12:00", current_date),
|
||||
},
|
||||
"",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -762,6 +995,7 @@ mod test {
|
||||
rowers: vec![2],
|
||||
},
|
||||
&User::find_by_id(&pool, 1).await.unwrap(),
|
||||
"",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -791,6 +1025,7 @@ mod test {
|
||||
rowers: vec![5],
|
||||
},
|
||||
&User::find_by_id(&pool, 4).await.unwrap(),
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
@ -817,6 +1052,7 @@ mod test {
|
||||
rowers: vec![5],
|
||||
},
|
||||
&User::find_by_id(&pool, 4).await.unwrap(),
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
@ -843,6 +1079,7 @@ mod test {
|
||||
rowers: vec![5],
|
||||
},
|
||||
&User::find_by_id(&pool, 5).await.unwrap(),
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
@ -869,6 +1106,7 @@ mod test {
|
||||
rowers: vec![5],
|
||||
},
|
||||
&User::find_by_id(&pool, 5).await.unwrap(),
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
@ -895,6 +1133,7 @@ mod test {
|
||||
rowers: Vec::new(),
|
||||
},
|
||||
&User::find_by_id(&pool, 2).await.unwrap(),
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
@ -921,6 +1160,7 @@ mod test {
|
||||
rowers: vec![5],
|
||||
},
|
||||
&User::find_by_id(&pool, 5).await.unwrap(),
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
@ -947,27 +1187,13 @@ mod test {
|
||||
rowers: vec![1, 5],
|
||||
},
|
||||
&User::find_by_id(&pool, 5).await.unwrap(),
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
assert_eq!(res, Err(LogbookCreateError::TooManyRowers(1, 2)));
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_distances() {
|
||||
let pool = testdb!();
|
||||
|
||||
let res = Logbook::distances(&pool).await;
|
||||
|
||||
assert_eq!(
|
||||
res,
|
||||
vec![
|
||||
("Ottensheim".into(), 25 as i64),
|
||||
("Ottensheim + Regattastrecke".into(), 29 as i64),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
fn test_succ_home() {
|
||||
let pool = testdb!();
|
||||
@ -993,6 +1219,7 @@ mod test {
|
||||
departure: format!("{}T10:00", current_date),
|
||||
arrival: format!("{}T12:00", current_date),
|
||||
},
|
||||
"",
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
@ -1021,6 +1248,7 @@ mod test {
|
||||
departure: "1990-01-01T10:00".into(),
|
||||
arrival: "1990-01-01T12:00".into(),
|
||||
},
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
@ -1050,6 +1278,7 @@ mod test {
|
||||
departure: "1990-01-01T10:00".into(),
|
||||
arrival: "1990-01-01T12:00".into(),
|
||||
},
|
||||
"",
|
||||
)
|
||||
.await;
|
||||
|
||||
|
@ -5,7 +5,7 @@ use lettre::{
|
||||
transport::smtp::authentication::Credentials,
|
||||
Message, SmtpTransport, Transport,
|
||||
};
|
||||
use sqlx::SqlitePool;
|
||||
use sqlx::{Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use crate::tera::admin::mail::MailToSend;
|
||||
|
||||
@ -20,6 +20,19 @@ impl Mail {
|
||||
subject: &str,
|
||||
body: String,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
let ret = Self::send_single_tx(&mut tx, to, subject, body, smtp_pw).await;
|
||||
tx.commit().await.unwrap();
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn send_single_tx(
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
to: &str,
|
||||
subject: &str,
|
||||
body: String,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
let mut email = Message::builder()
|
||||
.from(
|
||||
@ -40,7 +53,7 @@ impl Mail {
|
||||
match single_rec.parse() {
|
||||
Ok(new_bcc_mail) => email = email.bcc(new_bcc_mail),
|
||||
Err(_) => {
|
||||
Log::create(
|
||||
Log::create_with_tx(
|
||||
db,
|
||||
format!("Mail not sent to {single_rec}, because it could not be parsed"),
|
||||
)
|
||||
@ -182,7 +195,7 @@ dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
|
||||
fees.name
|
||||
))
|
||||
}
|
||||
content.push_str("\nBitte überweise diesen auf folgendes Konto: IBAN: AT13 1200 0804 1300 1200. Auf https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.\n\n\
|
||||
content.push_str("\nBitte überweise diesen auf folgendes Konto: IBAN: AT58 2032 0321 0072 9256. Auf https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.\n\n\
|
||||
Falls die Berechnung nicht stimmt (korrekte Preise findest du unter https://rudernlinz.at/unser-verein/gebuhren/) melde dich bitte bei it@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an it@rudernlinz.at schicken.\n\n\
|
||||
Wenn du die Vereinsgebühren schon bezahlt hast, kannst du diese Mail einfach ignorieren.\n\n
|
||||
Beste Grüße\n\
|
||||
@ -293,7 +306,7 @@ Dein Vereinsbeitrag für das aktuelle Jahr beträgt {}€",
|
||||
Gemäß § 7 Abs. 3 lit. c unseres Status behalten wir uns vor, bei ausbleibender Zahlung die Mitgliedschaft zu beenden. Dies möchten wir vermeiden und hoffen auf deine Unterstützung.\n\n\
|
||||
Bei Fragen oder Problemen stehen wir gerne zur Verfügung.
|
||||
|
||||
Bankverbindung: IBAN: AT13 1200 0804 1300 1200 (Unter https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.)
|
||||
Bankverbindung: IBAN: AT58 2032 0321 0072 9256 (Unter https://app.rudernlinz.at/planned findest du einen QR Code, den du mit deiner Bankapp scannen kannst um alle Eingaben bereits ausgefüllt zu haben.)
|
||||
|
||||
Mit freundlichen Grüßen,\n\
|
||||
Der Vorstand");
|
||||
|
@ -1,8 +1,10 @@
|
||||
use chrono::NaiveDate;
|
||||
use chrono::{Local, NaiveDate};
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
use waterlevel::WaterlevelDay;
|
||||
|
||||
use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD;
|
||||
|
||||
use self::{
|
||||
event::{Event, EventWithUserAndTriptype},
|
||||
trip::{Trip, TripWithUserAndType},
|
||||
@ -14,6 +16,7 @@ pub mod boat;
|
||||
pub mod boatdamage;
|
||||
pub mod boathouse;
|
||||
pub mod boatreservation;
|
||||
pub mod distance;
|
||||
pub mod event;
|
||||
pub mod family;
|
||||
pub mod location;
|
||||
@ -22,6 +25,7 @@ pub mod logbook;
|
||||
pub mod logtype;
|
||||
pub mod mail;
|
||||
pub mod notification;
|
||||
pub mod personal;
|
||||
pub mod role;
|
||||
pub mod rower;
|
||||
pub mod stat;
|
||||
@ -41,18 +45,23 @@ pub struct Day {
|
||||
events: Vec<EventWithUserAndTriptype>,
|
||||
trips: Vec<TripWithUserAndType>,
|
||||
is_pinned: bool,
|
||||
regular_sees_this_day: bool,
|
||||
max_waterlevel: Option<WaterlevelDay>,
|
||||
weather: Option<Weather>,
|
||||
}
|
||||
|
||||
impl Day {
|
||||
pub async fn new(db: &SqlitePool, day: NaiveDate, is_pinned: bool) -> Self {
|
||||
let today = Local::now().date_naive();
|
||||
let day_diff = (day - today).num_days() + 1;
|
||||
let regular_sees_this_day = day_diff <= AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD;
|
||||
if is_pinned {
|
||||
Self {
|
||||
day,
|
||||
events: Event::get_pinned_for_day(db, day).await,
|
||||
trips: Trip::get_pinned_for_day(db, day).await,
|
||||
is_pinned,
|
||||
regular_sees_this_day,
|
||||
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
|
||||
weather: Weather::find_by_day(db, day).await,
|
||||
}
|
||||
@ -62,6 +71,7 @@ impl Day {
|
||||
events: Event::get_for_day(db, day).await,
|
||||
trips: Trip::get_for_day(db, day).await,
|
||||
is_pinned,
|
||||
regular_sees_this_day,
|
||||
max_waterlevel: Waterlevel::max_waterlevel_for_day(db, day).await,
|
||||
weather: Weather::find_by_day(db, day).await,
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use super::{role::Role, user::User};
|
||||
use super::{role::Role, user::User, usertrip::UserTrip};
|
||||
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Notification {
|
||||
@ -140,15 +140,13 @@ ORDER BY read_at DESC, created_at DESC;
|
||||
let re = Regex::new(r"^remove_user_trip_with_trip_details_id:(\d+)$").unwrap();
|
||||
if let Some(caps) = re.captures(action) {
|
||||
if let Some(matched) = caps.get(1) {
|
||||
if let Ok(number) = matched.as_str().parse::<i32>() {
|
||||
let _ = sqlx::query!(
|
||||
"DELETE FROM user_trip WHERE user_id = ? AND trip_details_id = ?",
|
||||
self.user_id,
|
||||
number
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
if let Ok(number) = matched.as_str().parse::<i64>() {
|
||||
if let Some(usertrip) =
|
||||
UserTrip::find_by_userid_and_trip_detail_id(db, self.user_id, number)
|
||||
.await
|
||||
{
|
||||
let _ = usertrip.self_delete(db).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -179,6 +177,14 @@ ORDER BY read_at DESC, created_at DESC;
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub(crate) async fn delete_by_link(db: &sqlx::Pool<Sqlite>, link: &str) {
|
||||
let link = Some(link);
|
||||
sqlx::query!("DELETE FROM notification WHERE link=?", link)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -195,6 +201,7 @@ mod test {
|
||||
testdb,
|
||||
};
|
||||
|
||||
use chrono::Local;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[sqlx::test]
|
||||
@ -205,17 +212,16 @@ mod test {
|
||||
let add_tripdetails = TripDetailsToAdd {
|
||||
planned_starting_time: "10:00",
|
||||
max_people: 4,
|
||||
day: "1970-02-01".into(),
|
||||
day: Local::now().date_naive().format("%Y-%m-%d").to_string(),
|
||||
notes: None,
|
||||
trip_type: None,
|
||||
allow_guests: false,
|
||||
always_show: false,
|
||||
};
|
||||
let tripdetails_id = TripDetails::create(&pool, add_tripdetails).await;
|
||||
let trip_details = TripDetails::find_by_id(&pool, tripdetails_id)
|
||||
.await
|
||||
.unwrap();
|
||||
Event::create(&pool, "new-event".into(), 2, &trip_details).await;
|
||||
Event::create(&pool, "new-event".into(), 2, false, &trip_details).await;
|
||||
let event = Event::find_by_trip_details(&pool, trip_details.id)
|
||||
.await
|
||||
.unwrap();
|
||||
|
27
src/model/personal/cal.rs
Normal file
27
src/model/personal/cal.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use std::io::Write;
|
||||
|
||||
use ics::{components::Property, ICalendar};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{event::Event, trip::Trip, user::User};
|
||||
|
||||
pub(crate) async fn get_personal_cal(db: &SqlitePool, user: &User) -> String {
|
||||
let mut calendar = ICalendar::new("2.0", "ics-rs");
|
||||
calendar.push(Property::new(
|
||||
"X-WR-CALNAME",
|
||||
"Donau Linz - Deine Ausfahrten",
|
||||
));
|
||||
|
||||
let events = Event::all_with_user(db, user).await;
|
||||
for event in events {
|
||||
calendar.add_event(event.get_vevent(db).await);
|
||||
}
|
||||
|
||||
let trips = Trip::all_with_user(db, user).await;
|
||||
for trip in trips {
|
||||
calendar.add_event(trip.get_vevent(user).await);
|
||||
}
|
||||
let mut buf = Vec::new();
|
||||
write!(&mut buf, "{}", calendar).unwrap();
|
||||
String::from_utf8(buf).unwrap()
|
||||
}
|
87
src/model/personal/equatorprice.rs
Normal file
87
src/model/personal/equatorprice.rs
Normal file
@ -0,0 +1,87 @@
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize, PartialEq, Debug)]
|
||||
pub(crate) enum Level {
|
||||
NONE,
|
||||
BRONZE,
|
||||
SILVER,
|
||||
GOLD,
|
||||
DIAMOND,
|
||||
DONE,
|
||||
}
|
||||
|
||||
impl Level {
|
||||
fn required_km(&self) -> i32 {
|
||||
match self {
|
||||
Level::BRONZE => 40000,
|
||||
Level::SILVER => 80000,
|
||||
Level::GOLD => 100000,
|
||||
Level::DIAMOND => 200000,
|
||||
Level::DONE => 0,
|
||||
Level::NONE => 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn next_level(km: i32) -> Self {
|
||||
if km < Level::BRONZE.required_km() {
|
||||
Level::BRONZE
|
||||
} else if km < Level::SILVER.required_km() {
|
||||
Level::SILVER
|
||||
} else if km < Level::GOLD.required_km() {
|
||||
Level::GOLD
|
||||
} else if km < Level::DIAMOND.required_km() {
|
||||
Level::DIAMOND
|
||||
} else {
|
||||
Level::DONE
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn curr_level(km: i32) -> Self {
|
||||
if km < Level::BRONZE.required_km() {
|
||||
Level::NONE
|
||||
} else if km < Level::SILVER.required_km() {
|
||||
Level::BRONZE
|
||||
} else if km < Level::GOLD.required_km() {
|
||||
Level::SILVER
|
||||
} else if km < Level::DIAMOND.required_km() {
|
||||
Level::GOLD
|
||||
} else {
|
||||
Level::DIAMOND
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn desc(&self) -> &str {
|
||||
match self {
|
||||
Level::BRONZE => "Bronze",
|
||||
Level::SILVER => "Silber",
|
||||
Level::GOLD => "Gold",
|
||||
Level::DIAMOND => "Diamant",
|
||||
Level::DONE => "",
|
||||
Level::NONE => "-",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct Next {
|
||||
level: Level,
|
||||
desc: String,
|
||||
missing_km: i32,
|
||||
required_km: i32,
|
||||
rowed_km: i32,
|
||||
}
|
||||
|
||||
impl Next {
|
||||
pub(crate) fn new(rowed_km: i32) -> Self {
|
||||
let level = Level::next_level(rowed_km);
|
||||
let required_km = level.required_km();
|
||||
let missing_km = required_km - rowed_km;
|
||||
Self {
|
||||
desc: level.desc().to_string(),
|
||||
level,
|
||||
missing_km,
|
||||
required_km,
|
||||
rowed_km,
|
||||
}
|
||||
}
|
||||
}
|
52
src/model/personal/mod.rs
Normal file
52
src/model/personal/mod.rs
Normal file
@ -0,0 +1,52 @@
|
||||
use chrono::{Datelike, Local};
|
||||
use equatorprice::Level;
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use super::{logbook::Logbook, stat::Stat, user::User};
|
||||
|
||||
pub(crate) mod cal;
|
||||
pub(crate) mod equatorprice;
|
||||
pub(crate) mod rowingbadge;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct Achievements {
|
||||
pub(crate) equatorprice: equatorprice::Next,
|
||||
pub(crate) curr_equatorprice_name: String,
|
||||
pub(crate) new_equatorprice_this_season: bool,
|
||||
pub(crate) rowingbadge: Option<rowingbadge::Status>,
|
||||
pub(crate) all_time_km: i32,
|
||||
pub(crate) year_first_mentioned: Option<i32>,
|
||||
pub(crate) year_last_mentioned: Option<i32>,
|
||||
}
|
||||
|
||||
impl Achievements {
|
||||
pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Self {
|
||||
let rowed_km = Stat::total_km(db, user).await.rowed_km;
|
||||
let rowed_km_this_season = if Local::now().month() == 1 {
|
||||
Stat::person(db, Some(Local::now().year() - 1), user)
|
||||
.await
|
||||
.rowed_km
|
||||
+ Stat::person(db, Some(Local::now().year()), user)
|
||||
.await
|
||||
.rowed_km
|
||||
} else {
|
||||
Stat::person(db, Some(Local::now().year()), user)
|
||||
.await
|
||||
.rowed_km
|
||||
};
|
||||
|
||||
let new_equatorprice_this_season =
|
||||
Level::curr_level(rowed_km) != Level::curr_level(rowed_km - rowed_km_this_season);
|
||||
|
||||
Self {
|
||||
equatorprice: equatorprice::Next::new(rowed_km),
|
||||
curr_equatorprice_name: equatorprice::Level::curr_level(rowed_km).desc().to_string(),
|
||||
new_equatorprice_this_season,
|
||||
rowingbadge: rowingbadge::Status::for_user(db, user).await,
|
||||
all_time_km: rowed_km,
|
||||
year_first_mentioned: Logbook::year_first_logbook_entry(db, user).await,
|
||||
year_last_mentioned: Logbook::year_last_logbook_entry(db, user).await,
|
||||
}
|
||||
}
|
||||
}
|
166
src/model/personal/rowingbadge.rs
Normal file
166
src/model/personal/rowingbadge.rs
Normal file
@ -0,0 +1,166 @@
|
||||
use std::cmp;
|
||||
|
||||
use chrono::{Datelike, Local, NaiveDate};
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::{
|
||||
logbook::{Filter, Logbook, LogbookWithBoatAndRowers},
|
||||
stat::Stat,
|
||||
user::User,
|
||||
};
|
||||
|
||||
enum AgeBracket {
|
||||
Till14,
|
||||
From14Till18,
|
||||
From19Till30,
|
||||
From31Till60,
|
||||
From61Till75,
|
||||
From76,
|
||||
}
|
||||
|
||||
impl AgeBracket {
|
||||
fn cat(&self) -> &str {
|
||||
match self {
|
||||
AgeBracket::Till14 => "Schülerinnen und Schüler bis 14 Jahre",
|
||||
AgeBracket::From14Till18 => "Juniorinnen und Junioren, Para-Ruderer bis 18 Jahre",
|
||||
AgeBracket::From19Till30 => "Frauen und Männer, Para-Ruderer bis 30 Jahre",
|
||||
AgeBracket::From31Till60 => "Frauen und Männer, Para-Ruderer von 31 bis 60 Jahre",
|
||||
AgeBracket::From61Till75 => "Frauen und Männer, Para-Ruderer von 61 bis 75 Jahre",
|
||||
AgeBracket::From76 => "Frauen und Männer, Para-Ruderer ab 76 Jahre",
|
||||
}
|
||||
}
|
||||
|
||||
fn dist_in_km(&self) -> i32 {
|
||||
match self {
|
||||
AgeBracket::Till14 => 500,
|
||||
AgeBracket::From14Till18 => 1000,
|
||||
AgeBracket::From19Till30 => 1200,
|
||||
AgeBracket::From31Till60 => 1000,
|
||||
AgeBracket::From61Till75 => 800,
|
||||
AgeBracket::From76 => 600,
|
||||
}
|
||||
}
|
||||
|
||||
fn required_dist_multi_day_in_km(&self) -> i32 {
|
||||
match self {
|
||||
AgeBracket::Till14 => 60,
|
||||
AgeBracket::From14Till18 => 60,
|
||||
AgeBracket::From19Till30 => 80,
|
||||
AgeBracket::From31Till60 => 80,
|
||||
AgeBracket::From61Till75 => 80,
|
||||
AgeBracket::From76 => 80,
|
||||
}
|
||||
}
|
||||
|
||||
fn required_dist_single_day_in_km(&self) -> i32 {
|
||||
match self {
|
||||
AgeBracket::Till14 => 30,
|
||||
AgeBracket::From14Till18 => 30,
|
||||
AgeBracket::From19Till30 => 40,
|
||||
AgeBracket::From31Till60 => 40,
|
||||
AgeBracket::From61Till75 => 40,
|
||||
AgeBracket::From76 => 40,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&User> for AgeBracket {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: &User) -> Result<Self, Self::Error> {
|
||||
let Some(birthdate) = value.birthdate.clone() else {
|
||||
return Err("User has no birthdate".to_string());
|
||||
};
|
||||
|
||||
let Ok(birthdate) = NaiveDate::parse_from_str(&birthdate, "%Y-%m-%d") else {
|
||||
return Err("Birthdate in wrong format...".to_string());
|
||||
};
|
||||
|
||||
let today = Local::now().date_naive();
|
||||
|
||||
let age = today.year() - birthdate.year();
|
||||
if age <= 14 {
|
||||
Ok(AgeBracket::Till14)
|
||||
} else if age <= 18 {
|
||||
Ok(AgeBracket::From14Till18)
|
||||
} else if age <= 30 {
|
||||
Ok(AgeBracket::From19Till30)
|
||||
} else if age <= 60 {
|
||||
Ok(AgeBracket::From31Till60)
|
||||
} else if age <= 75 {
|
||||
Ok(AgeBracket::From61Till75)
|
||||
} else {
|
||||
Ok(AgeBracket::From76)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct Status {
|
||||
pub(crate) year: i32,
|
||||
rowed_km: i32,
|
||||
category: String,
|
||||
required_km: i32,
|
||||
missing_km: i32,
|
||||
multi_day_trips_over_required_distance: Vec<LogbookWithBoatAndRowers>,
|
||||
multi_day_trips_required_distance: i32,
|
||||
single_day_trips_over_required_distance: Vec<LogbookWithBoatAndRowers>,
|
||||
single_day_trips_required_distance: i32,
|
||||
achieved: bool,
|
||||
}
|
||||
|
||||
impl Status {
|
||||
pub(crate) async fn for_user(db: &SqlitePool, user: &User) -> Option<Self> {
|
||||
let Ok(agebracket) = AgeBracket::try_from(user) else {
|
||||
return None;
|
||||
};
|
||||
let category = agebracket.cat().to_string();
|
||||
|
||||
let year = if Local::now().month() == 1 {
|
||||
Local::now().year() - 1
|
||||
} else {
|
||||
Local::now().year()
|
||||
};
|
||||
|
||||
let rowed_km = Stat::person(db, Some(year), user).await.rowed_km;
|
||||
let required_km = agebracket.dist_in_km();
|
||||
let missing_km = cmp::max(required_km - rowed_km, 0);
|
||||
|
||||
let single_day_trips_over_required_distance =
|
||||
Logbook::completed_wanderfahrten_with_user_over_km_in_year(
|
||||
db,
|
||||
user,
|
||||
agebracket.required_dist_single_day_in_km(),
|
||||
year,
|
||||
Filter::SingleDayOnly,
|
||||
)
|
||||
.await;
|
||||
let multi_day_trips_over_required_distance =
|
||||
Logbook::completed_wanderfahrten_with_user_over_km_in_year(
|
||||
db,
|
||||
user,
|
||||
agebracket.required_dist_multi_day_in_km(),
|
||||
year,
|
||||
Filter::MultiDazOnly,
|
||||
)
|
||||
.await;
|
||||
|
||||
let achieved = missing_km == 0
|
||||
&& (multi_day_trips_over_required_distance.len() >= 1
|
||||
|| single_day_trips_over_required_distance.len() >= 2);
|
||||
|
||||
Some(Self {
|
||||
year,
|
||||
rowed_km,
|
||||
category,
|
||||
required_km,
|
||||
missing_km,
|
||||
multi_day_trips_over_required_distance,
|
||||
single_day_trips_over_required_distance,
|
||||
multi_day_trips_required_distance: agebracket.required_dist_multi_day_in_km(),
|
||||
single_day_trips_required_distance: agebracket.required_dist_single_day_in_km(),
|
||||
achieved,
|
||||
})
|
||||
}
|
||||
}
|
@ -13,16 +13,23 @@ pub struct Rower {
|
||||
|
||||
impl Rower {
|
||||
pub async fn for_log(db: &SqlitePool, log: &Logbook) -> Vec<User> {
|
||||
let mut tx = db.begin().await.unwrap();
|
||||
let ret = Self::for_log_tx(&mut tx, log).await;
|
||||
tx.commit().await.unwrap();
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn for_log_tx(db: &mut Transaction<'_, Sqlite>, log: &Logbook) -> Vec<User> {
|
||||
sqlx::query_as!(
|
||||
User,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
FROM user
|
||||
WHERE id in (SELECT rower_id FROM rower WHERE logbook_id=?)
|
||||
",
|
||||
log.id
|
||||
)
|
||||
.fetch_all(db)
|
||||
.fetch_all(db.deref_mut())
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ pub struct BoatStat {
|
||||
#[derive(Serialize, Clone)]
|
||||
pub struct SingleBoatStat {
|
||||
name: String,
|
||||
cat: String,
|
||||
location: String,
|
||||
owner: String,
|
||||
years: HashMap<String, i32>,
|
||||
@ -71,6 +72,7 @@ ORDER BY
|
||||
}
|
||||
|
||||
let year: String = format!("{year}");
|
||||
let cat = boat.cat();
|
||||
|
||||
let rowed_km: i32 = row.get("rowed_km");
|
||||
|
||||
@ -80,6 +82,7 @@ ORDER BY
|
||||
name,
|
||||
location,
|
||||
owner,
|
||||
cat,
|
||||
years: HashMap::new(),
|
||||
});
|
||||
boat_stat.years.insert(year, rowed_km);
|
||||
@ -195,6 +198,32 @@ ORDER BY rowed_km DESC, u.name;
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn total_km(db: &SqlitePool, user: &User) -> Stat {
|
||||
//TODO: switch to query! macro again (once upgraded to sqlite 3.42 on server)
|
||||
let row = sqlx::query(&format!(
|
||||
"
|
||||
SELECT u.name, CAST(SUM(l.distance_in_km) AS INTEGER) AS rowed_km
|
||||
FROM (
|
||||
SELECT * FROM user
|
||||
WHERE id={}
|
||||
) u
|
||||
INNER JOIN rower r ON u.id = r.rower_id
|
||||
INNER JOIN logbook l ON r.logbook_id = l.id
|
||||
WHERE l.distance_in_km IS NOT NULL;
|
||||
",
|
||||
user.id
|
||||
))
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Stat {
|
||||
name: row.get("name"),
|
||||
rowed_km: row.get("rowed_km"),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn person(db: &SqlitePool, year: Option<i32>, user: &User) -> Stat {
|
||||
let year = match year {
|
||||
Some(year) => year,
|
||||
|
@ -1,13 +1,16 @@
|
||||
use chrono::NaiveDate;
|
||||
use chrono::{Local, NaiveDate};
|
||||
use ics::properties::{DtStart, Summary};
|
||||
use serde::Serialize;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use super::{
|
||||
event::{Event, Registration},
|
||||
log::Log,
|
||||
notification::Notification,
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
user::{CoxUser, User},
|
||||
usertrip::UserTrip,
|
||||
};
|
||||
|
||||
#[derive(Serialize, Clone, Debug)]
|
||||
@ -40,7 +43,6 @@ pub struct TripUpdate<'a> {
|
||||
pub max_people: i32,
|
||||
pub notes: Option<&'a str>,
|
||||
pub trip_type: Option<i64>, //TODO: Move to `TripType`
|
||||
pub always_show: bool,
|
||||
pub is_locked: bool,
|
||||
}
|
||||
|
||||
@ -77,23 +79,30 @@ impl Trip {
|
||||
.await;
|
||||
if same_starting_datetime.len() > 1 {
|
||||
for notify in same_starting_datetime {
|
||||
if notify.id != trip_details.id {
|
||||
// notify everyone except oneself
|
||||
if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await {
|
||||
let user = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
|
||||
cox.user.name, trip.day, trip.planned_starting_time
|
||||
),
|
||||
"Neue Ausfahrt zur selben Zeit",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
// don't notify oneself
|
||||
if notify.id == trip_details.id {
|
||||
continue;
|
||||
}
|
||||
|
||||
// don't notify people who have cancelled their trip
|
||||
if notify.cancelled() {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(trip) = Trip::find_by_trip_details(db, notify.id).await {
|
||||
let user = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
Notification::create(
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"{} hat eine Ausfahrt zur selben Zeit ({} um {}) wie du erstellt",
|
||||
cox.user.name, trip.day, trip.planned_starting_time
|
||||
),
|
||||
"Neue Ausfahrt zur selben Zeit",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -116,6 +125,68 @@ WHERE trip_details.id=?
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub(crate) async fn get_vevent(self, user: &User) -> ics::Event {
|
||||
let mut vevent = ics::Event::new(format!("{}@rudernlinz.at", self.id), "19900101T180000");
|
||||
vevent.push(DtStart::new(format!(
|
||||
"{}T{}00",
|
||||
self.day.replace('-', ""),
|
||||
self.planned_starting_time.replace(':', "")
|
||||
)));
|
||||
let mut name = String::new();
|
||||
if self.is_cancelled() {
|
||||
name.push_str("ABGESAGT");
|
||||
if let Some(notes) = &self.notes {
|
||||
if !notes.is_empty() {
|
||||
name.push_str(&format!(" (Grund: {notes})"))
|
||||
}
|
||||
}
|
||||
|
||||
name.push_str("! :-( ");
|
||||
}
|
||||
if self.cox_id == user.id {
|
||||
name.push_str("Ruderausfahrt (selber ausgeschrieben)");
|
||||
} else {
|
||||
name.push_str(&format!("Ruderausfahrt mit {} ", self.cox_name));
|
||||
}
|
||||
|
||||
vevent.push(Summary::new(name));
|
||||
vevent
|
||||
}
|
||||
|
||||
pub async fn all(db: &SqlitePool) -> Vec<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, trip_details.notes, allow_guests, trip_type_id, always_show, is_locked
|
||||
FROM trip
|
||||
INNER JOIN trip_details ON trip.trip_details_id = trip_details.id
|
||||
INNER JOIN user ON trip.cox_id = user.id
|
||||
",
|
||||
)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap() //TODO: fixme
|
||||
}
|
||||
|
||||
pub async fn all_with_user(db: &SqlitePool, user: &User) -> Vec<Self> {
|
||||
let mut ret = Vec::new();
|
||||
let trips = Self::all(db).await;
|
||||
for trip in trips {
|
||||
if user.id == trip.cox_id {
|
||||
ret.push(trip.clone());
|
||||
}
|
||||
if let Some(trip_details_id) = trip.trip_details_id {
|
||||
if UserTrip::find_by_userid_and_trip_detail_id(db, user.id, trip_details_id)
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
ret.push(trip);
|
||||
}
|
||||
}
|
||||
}
|
||||
ret
|
||||
}
|
||||
|
||||
pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
@ -164,6 +235,11 @@ WHERE trip.id=?
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_for_today(db: &SqlitePool) -> Vec<TripWithUserAndType> {
|
||||
let today = Local::now().date_naive();
|
||||
Self::get_for_day(db, today).await
|
||||
}
|
||||
|
||||
pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<TripWithUserAndType> {
|
||||
let day = format!("{day}");
|
||||
let trips = sqlx::query_as!(
|
||||
@ -204,13 +280,18 @@ WHERE day=?
|
||||
let tripdetails = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
|
||||
let was_already_cancelled = tripdetails.max_people == 0;
|
||||
|
||||
let is_locked = if update.max_people == 0 {
|
||||
false
|
||||
} else {
|
||||
update.is_locked
|
||||
};
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, always_show = ?, is_locked = ? WHERE id = ?",
|
||||
"UPDATE trip_details SET max_people = ?, notes = ?, trip_type_id = ?, is_locked = ? WHERE id = ?",
|
||||
update.max_people,
|
||||
update.notes,
|
||||
update.trip_type,
|
||||
update.always_show,
|
||||
update.is_locked,
|
||||
is_locked,
|
||||
trip_details_id
|
||||
)
|
||||
.execute(db)
|
||||
@ -232,7 +313,7 @@ WHERE day=?
|
||||
db,
|
||||
&user,
|
||||
&format!(
|
||||
"Die Ausfahrt von {} am {} um {} wurde abgesagt. {}",
|
||||
"Die Ausfahrt von {} am {} um {} wurde abgesagt. {} Bitte gib Bescheid, dass du die Info erhalten hast indem du auf ✓ klickst.",
|
||||
update.cox.user.name,
|
||||
update.trip.day,
|
||||
update.trip.planned_starting_time,
|
||||
@ -313,18 +394,16 @@ WHERE day=?
|
||||
return Err(TripDeleteError::SomebodyAlreadyRegistered);
|
||||
}
|
||||
|
||||
if !self.is_trip_from_user(user.id) {
|
||||
if !self.is_trip_from_user(user.id) && !user.has_role(db, "admin").await {
|
||||
return Err(TripDeleteError::NotYourTrip);
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM trip WHERE cox_id = ? AND id = ?",
|
||||
user.id,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
Log::create(db, format!("{} deleted trip: {:#?}", user.user.name, self)).await;
|
||||
|
||||
sqlx::query!("DELETE FROM trip WHERE id = ?", self.id)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //TODO: fixme
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -333,6 +412,20 @@ WHERE day=?
|
||||
self.cox_id == user_id
|
||||
}
|
||||
|
||||
pub(crate) async fn toggle_always_show(&self, db: &SqlitePool) {
|
||||
if let Some(trip_details) = self.trip_details_id {
|
||||
let new_state = !self.always_show;
|
||||
sqlx::query!(
|
||||
"UPDATE trip_details SET always_show = ? WHERE id = ?",
|
||||
new_state,
|
||||
trip_details
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_pinned_for_day(
|
||||
db: &sqlx::Pool<sqlx::Sqlite>,
|
||||
day: NaiveDate,
|
||||
@ -341,6 +434,10 @@ WHERE day=?
|
||||
trips.retain(|e| e.trip.always_show);
|
||||
trips
|
||||
}
|
||||
|
||||
fn is_cancelled(&self) -> bool {
|
||||
self.max_people == 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -382,7 +479,7 @@ mod test {
|
||||
testdb,
|
||||
};
|
||||
|
||||
use chrono::NaiveDate;
|
||||
use chrono::Local;
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use super::Trip;
|
||||
@ -409,7 +506,8 @@ mod test {
|
||||
fn test_get_day_cox_trip() {
|
||||
let pool = testdb!();
|
||||
|
||||
let res = Trip::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 2).unwrap()).await;
|
||||
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
|
||||
let res = Trip::get_for_day(&pool, tomorrow).await;
|
||||
assert_eq!(res.len(), 1);
|
||||
}
|
||||
|
||||
@ -465,7 +563,6 @@ mod test {
|
||||
max_people: 10,
|
||||
notes: None,
|
||||
trip_type: None,
|
||||
always_show: false,
|
||||
is_locked: false,
|
||||
};
|
||||
|
||||
@ -494,7 +591,6 @@ mod test {
|
||||
max_people: 10,
|
||||
notes: None,
|
||||
trip_type: Some(1),
|
||||
always_show: false,
|
||||
is_locked: false,
|
||||
};
|
||||
assert!(Trip::update_own(&pool, &update).await.is_ok());
|
||||
@ -523,7 +619,6 @@ mod test {
|
||||
max_people: 10,
|
||||
notes: None,
|
||||
trip_type: None,
|
||||
always_show: false,
|
||||
is_locked: false,
|
||||
};
|
||||
assert!(Trip::update_own(&pool, &update).await.is_err());
|
||||
|
@ -1,5 +1,5 @@
|
||||
use crate::model::user::User;
|
||||
use chrono::NaiveDate;
|
||||
use chrono::{Local, NaiveDate};
|
||||
use rocket::FromForm;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
@ -33,7 +33,6 @@ pub struct TripDetailsToAdd<'r> {
|
||||
pub notes: Option<&'r str>,
|
||||
pub trip_type: Option<i64>,
|
||||
pub allow_guests: bool,
|
||||
pub always_show: bool,
|
||||
}
|
||||
|
||||
impl TripDetails {
|
||||
@ -59,6 +58,24 @@ WHERE id like ?
|
||||
}
|
||||
}
|
||||
|
||||
pub fn date(&self) -> NaiveDate {
|
||||
NaiveDate::parse_from_str(&self.day, "%Y-%m-%d").unwrap()
|
||||
}
|
||||
|
||||
pub(crate) async fn user_sees_trip(&self, db: &SqlitePool, user: &User) -> bool {
|
||||
let today = Local::now().date_naive();
|
||||
let day_diff = self.date() - today;
|
||||
let day_diff = day_diff.num_days();
|
||||
if day_diff < 0 {
|
||||
// tripdetails is in past
|
||||
return false;
|
||||
}
|
||||
if day_diff <= user.amount_days_to_show(db).await {
|
||||
return true;
|
||||
}
|
||||
self.always_show
|
||||
}
|
||||
|
||||
pub async fn find_by_startingdatetime(
|
||||
db: &SqlitePool,
|
||||
day: String,
|
||||
@ -77,6 +94,10 @@ WHERE day = ? AND planned_starting_time = ?
|
||||
.await.unwrap()
|
||||
}
|
||||
|
||||
pub fn cancelled(&self) -> bool {
|
||||
self.max_people == 0
|
||||
}
|
||||
|
||||
/// This function is called when a person registers to a trip or when the cox changes the
|
||||
/// amount of free places.
|
||||
pub async fn check_free_spaces(&self, db: &SqlitePool) {
|
||||
@ -85,7 +106,7 @@ WHERE day = ? AND planned_starting_time = ?
|
||||
return;
|
||||
}
|
||||
|
||||
if self.max_people == 0 {
|
||||
if self.cancelled() {
|
||||
// Cox cancelled event, thus it's probably bad weather. Don't bother with sending
|
||||
// notifications
|
||||
return;
|
||||
@ -142,14 +163,13 @@ WHERE day = ? AND planned_starting_time = ?
|
||||
/// Creates a new entry in `trip_details` and returns its id.
|
||||
pub async fn create(db: &SqlitePool, tripdetails: TripDetailsToAdd<'_>) -> i64 {
|
||||
let query = sqlx::query!(
|
||||
"INSERT INTO trip_details(planned_starting_time, max_people, day, notes, allow_guests, trip_type_id, always_show) VALUES(?, ?, ?, ?, ?, ?, ?)" ,
|
||||
"INSERT INTO trip_details(planned_starting_time, max_people, day, notes, allow_guests, trip_type_id) VALUES(?, ?, ?, ?, ?, ?)" ,
|
||||
tripdetails.planned_starting_time,
|
||||
tripdetails.max_people,
|
||||
tripdetails.day,
|
||||
tripdetails.notes,
|
||||
tripdetails.allow_guests,
|
||||
tripdetails.trip_type,
|
||||
tripdetails.always_show
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
@ -157,6 +177,17 @@ WHERE day = ? AND planned_starting_time = ?
|
||||
query.last_insert_rowid()
|
||||
}
|
||||
|
||||
pub async fn set_always_show(&self, db: &SqlitePool, value: bool) {
|
||||
sqlx::query!(
|
||||
"UPDATE trip_details SET always_show = ? WHERE id = ?",
|
||||
value,
|
||||
self.id
|
||||
)
|
||||
.execute(db)
|
||||
.await
|
||||
.unwrap(); //Okay, as planned_event can only be created with proper DB backing
|
||||
}
|
||||
|
||||
pub async fn is_full(&self, db: &SqlitePool) -> bool {
|
||||
let amount_currently_registered = sqlx::query!(
|
||||
"SELECT COUNT(*) as count FROM user_trip WHERE trip_details_id = ?",
|
||||
@ -213,7 +244,7 @@ ORDER BY day;",
|
||||
|
||||
pub(crate) async fn user_allowed_to_change(&self, db: &SqlitePool, user: &User) -> bool {
|
||||
if self.belongs_to_event(db).await {
|
||||
user.has_role(db, "planned_event").await
|
||||
user.has_role(db, "manage_events").await
|
||||
} else {
|
||||
self.user_is_cox(db, user).await != CoxAtTrip::No
|
||||
}
|
||||
@ -305,7 +336,6 @@ mod test {
|
||||
notes: None,
|
||||
allow_guests: false,
|
||||
trip_type: None,
|
||||
always_show: false
|
||||
}
|
||||
)
|
||||
.await,
|
||||
@ -321,7 +351,6 @@ mod test {
|
||||
notes: None,
|
||||
allow_guests: false,
|
||||
trip_type: None,
|
||||
always_show: false
|
||||
}
|
||||
)
|
||||
.await,
|
||||
|
@ -15,20 +15,14 @@ use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Sqlite, SqlitePool, Transaction};
|
||||
|
||||
use super::{
|
||||
family::Family, log::Log, mail::Mail, notification::Notification, role::Role, stat::Stat,
|
||||
tripdetails::TripDetails, Day,
|
||||
family::Family, log::Log, logbook::Logbook, mail::Mail, notification::Notification, role::Role,
|
||||
stat::Stat, tripdetails::TripDetails, Day,
|
||||
};
|
||||
use crate::{
|
||||
tera::admin::user::UserEditForm, AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD, BOAT_STORAGE,
|
||||
EINSCHREIBGEBUEHR, FAMILY_THREE_OR_MORE, FAMILY_TWO, FOERDERND, REGULAR, RENNRUDERBEITRAG,
|
||||
SCHECKBUCH, STUDENT_OR_PUPIL, UNTERSTUETZEND,
|
||||
};
|
||||
use crate::tera::admin::user::UserEditForm;
|
||||
|
||||
const RENNRUDERBEITRAG: i32 = 11000;
|
||||
const BOAT_STORAGE: i32 = 4500;
|
||||
const FAMILY_TWO: i32 = 30000;
|
||||
const FAMILY_THREE_OR_MORE: i32 = 35000;
|
||||
const STUDENT_OR_PUPIL: i32 = 8000;
|
||||
const REGULAR: i32 = 22000;
|
||||
const UNTERSTUETZEND: i32 = 2500;
|
||||
const FOERDERND: i32 = 8500;
|
||||
pub const SCHECKBUCH: i32 = 3000;
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize, Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct User {
|
||||
@ -48,6 +42,7 @@ pub struct User {
|
||||
pub phone: Option<String>,
|
||||
pub address: Option<String>,
|
||||
pub family_id: Option<i64>,
|
||||
pub user_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
@ -256,6 +251,35 @@ ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_end_mail_scheckbuch(
|
||||
&self,
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
mail: &str,
|
||||
smtp_pw: &str,
|
||||
) -> Result<(), String> {
|
||||
Mail::send_single_tx(
|
||||
db,
|
||||
mail,
|
||||
"ASKÖ Ruderverein Donau Linz | Deine Mitgliedschaft wartet auf Dich",
|
||||
format!(
|
||||
"Hallo {0},
|
||||
|
||||
herzlichen Glückwunsch---Du hast Deine fünf Scheckbuch-Ausfahrten erfolgreich absolviert! Wir hoffen, dass Du das Rudern bei uns genauso genossen hast wie wir es genossen haben, Dich auf dem Wasser zu begleiten.
|
||||
|
||||
Wir würden uns sehr freuen, Dich als festes Mitglied in unserem Verein willkommen zu heißen! Als Mitglied stehen Dir dann alle unsere Ausfahrten offen, die von unseren Steuerleuten organisiert werden. Im Sommer erwarten Dich zusätzlich spannende Events: Wanderfahrten, Sternfahrten, Fetzenfahrt, .... Im Winter bieten wir Indoor-Ergo-Challenges an, bei denen Du Deine Fitness auf dem Ruderergometer unter Beweis stellen kannst. Alle Details zu diesen Aktionen erfährst Du, sobald Du Teil unseres Vereins bist! :-)
|
||||
|
||||
Alle Informationen zu den Mitgliedsbeiträgen findest Du unter https://rudernlinz.at/unser-verein/gebuhren/ Falls Du Dich entscheidest, unserem Verein beizutreten, fülle bitte unser Beitrittsformular auf https://rudernlinz.at/unser-verein/downloads/ aus und sende es an info@rudernlinz.at.
|
||||
|
||||
Wir freuen uns, Dich bald wieder auf dem Wasser zu sehen.
|
||||
|
||||
Riemen- & Dollenbruch,
|
||||
ASKÖ Ruderverein Donau Linz", self.name),
|
||||
smtp_pw,
|
||||
).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn send_welcome_mail_full_member(
|
||||
&self,
|
||||
db: &SqlitePool,
|
||||
@ -273,15 +297,15 @@ ASKÖ Ruderverein Donau Linz", self.name, SCHECKBUCH/100),
|
||||
|
||||
herzlich willkommen im ASKÖ Ruderverein Donau Linz! Wir freuen uns sehr, dich als neues Mitglied in unserem Verein begrüßen zu dürfen.
|
||||
|
||||
Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at und it@rudernlinz.at jederzeit zur Verfügung.
|
||||
Um dir den Einstieg zu erleichtern, findest du in unserem Handbuch alle wichtigen Informationen über unseren Verein: https://rudernlinz.at/book. Bei weiteren Fragen stehen dir die Adressen info@rudernlinz.at (für allgemeine Fragen) und it@rudernlinz.at (bei technischen Fragen) jederzeit zur Verfügung.
|
||||
|
||||
Du kannst auch gerne unserer Signal-Gruppe beitreten, um auf dem Laufenden zu bleiben und dich mit anderen Mitgliedern auszutauschen: https://signal.group/#CjQKICFrq6zSsRHxrucS3jEcQn6lknEXacAykwwLV3vNLKxPEhA17jxz7cpjfu3JZokLq1TH
|
||||
|
||||
Für die Organisation unserer Ausfahrten nutzen wir app.rudernlinz.at. Logge dich einfach mit deinem Namen ('{0}' ohne Anführungszeichen) ein, beim ersten Mal kannst du das Passwortfeld leer lassen. Unter 'Geplante Ausfahrten' kannst du dich jederzeit zu den Ausfahrten anmelden.
|
||||
|
||||
Beim nächsten Treffen im Verein, erinnere mich (Philipp Hofer) bitte daran, deinen Fingerabdruck zu registrieren, damit du eigenständig Zugang zum Bootshaus erhältst.
|
||||
Beim nächsten Treffen im Verein, erinnere jemand vom Vorstand (https://rudernlinz.at/unser-verein/vorstand/) bitte daran, deinen Fingerabdruck zu registrieren, damit du Zugang zum Bootshaus erhältst.
|
||||
|
||||
Außerdem haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz'. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
|
||||
Damit du dich noch mehr verbunden fühlst (:-)), haben wir im Bootshaus ein WLAN für Vereinsmitglieder 'ASKÖ Ruderverein Donau Linz' eingerichtet. Das Passwort dafür lautet 'donau1921' (ohne Anführungszeichen). Bitte gib das Passwort an keine vereinsfremden Personen weiter.
|
||||
|
||||
Wir freuen uns darauf, dich bald am Wasser zu sehen und gemeinsam tolle Erfahrungen zu sammeln!
|
||||
|
||||
@ -361,17 +385,49 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(member_since_date) = &self.member_since_date {
|
||||
if let Ok(member_since_date) = NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d")
|
||||
{
|
||||
if member_since_date.year() == Local::now().year()
|
||||
&& !self.has_role(db, "no-einschreibgebuehr").await
|
||||
{
|
||||
fee.add("Einschreibgebühr".into(), EINSCHREIBGEBUEHR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let halfprice = if let Some(member_since_date) = &self.member_since_date {
|
||||
if let Ok(member_since_date) = NaiveDate::parse_from_str(member_since_date, "%Y-%m-%d")
|
||||
{
|
||||
let halfprice_startdate =
|
||||
NaiveDate::from_ymd_opt(Local::now().year(), 7, 1).unwrap();
|
||||
member_since_date >= halfprice_startdate
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
if self.has_role(db, "Unterstützend").await {
|
||||
fee.add("Unterstützendes Mitglied".into(), UNTERSTUETZEND);
|
||||
} else if self.has_role(db, "Förderndes Mitglied").await {
|
||||
fee.add("Förderndes Mitglied".into(), FOERDERND);
|
||||
} else if Family::find_by_opt_id(db, self.family_id).await.is_none() {
|
||||
if self.has_role(db, "Student").await || self.has_role(db, "Schüler").await {
|
||||
fee.add("Schüler/Student".into(), STUDENT_OR_PUPIL);
|
||||
if halfprice {
|
||||
fee.add("Schüler/Student (Halbpreis)".into(), STUDENT_OR_PUPIL / 2);
|
||||
} else {
|
||||
fee.add("Schüler/Student".into(), STUDENT_OR_PUPIL);
|
||||
}
|
||||
} else if self.has_role(db, "Ehrenmitglied").await {
|
||||
fee.add("Ehrenmitglied".into(), 0);
|
||||
} else {
|
||||
fee.add("Mitgliedsbeitrag".into(), REGULAR);
|
||||
if halfprice {
|
||||
fee.add("Mitgliedsbeitrag (Halbpreis)".into(), REGULAR / 2);
|
||||
} else {
|
||||
fee.add("Mitgliedsbeitrag".into(), REGULAR);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -417,6 +473,12 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
false
|
||||
}
|
||||
|
||||
pub async fn allowed_to_update_always_show_trip(&self, db: &SqlitePool) -> bool {
|
||||
AllowedToUpdateTripToAlwaysBeShownUser::new(db, self.clone())
|
||||
.await
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub async fn has_membership_pdf(&self, db: &SqlitePool) -> bool {
|
||||
match sqlx::query_scalar!("SELECT membership_pdf FROM user WHERE id = ?", self.id)
|
||||
.fetch_one(db)
|
||||
@ -461,7 +523,7 @@ ASKÖ Ruderverein Donau Linz", self.name),
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
FROM user
|
||||
WHERE id like ?
|
||||
",
|
||||
@ -476,7 +538,7 @@ WHERE id like ?
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
FROM user
|
||||
WHERE id like ?
|
||||
",
|
||||
@ -493,7 +555,7 @@ WHERE id like ?
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
FROM user
|
||||
WHERE lower(name)=?
|
||||
",
|
||||
@ -535,7 +597,7 @@ WHERE lower(name)=?
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
FROM user
|
||||
WHERE deleted = 0
|
||||
ORDER BY last_access DESC
|
||||
@ -557,7 +619,7 @@ ORDER BY last_access DESC
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
FROM user u
|
||||
JOIN user_role ur ON u.id = ur.user_id
|
||||
WHERE ur.role_id = ? AND deleted = 0
|
||||
@ -573,14 +635,14 @@ ORDER BY name;
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user
|
||||
WHERE family_id IS NOT NULL
|
||||
GROUP BY family_id
|
||||
|
||||
UNION
|
||||
|
||||
-- Select users with a null family_id, without grouping
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id FROM user
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token FROM user
|
||||
WHERE family_id IS NULL;
|
||||
"
|
||||
)
|
||||
@ -593,7 +655,7 @@ WHERE family_id IS NULL;
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
FROM user
|
||||
WHERE deleted = 0 AND dob != '' and weight != '' and sex != ''
|
||||
ORDER BY name
|
||||
@ -608,7 +670,7 @@ ORDER BY name
|
||||
sqlx::query_as!(
|
||||
Self,
|
||||
"
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id
|
||||
SELECT id, name, pw, deleted, last_access, dob, weight, sex, member_since_date, birthdate, mail, nickname, notes, phone, address, family_id, user_token
|
||||
FROM user
|
||||
WHERE deleted = 0 AND (SELECT COUNT(*) FROM user_role WHERE user_id=user.id AND role_id = (SELECT id FROM role WHERE name = 'cox')) > 0
|
||||
ORDER BY last_access DESC
|
||||
@ -627,6 +689,14 @@ ORDER BY last_access DESC
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub async fn create_with_mail(db: &SqlitePool, name: &str, mail: &str) -> bool {
|
||||
let name = name.trim();
|
||||
sqlx::query!("INSERT INTO USER(name, mail) VALUES (?, ?)", name, mail)
|
||||
.execute(db)
|
||||
.await
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
pub async fn update(&self, db: &SqlitePool, data: UserEditForm<'_>) {
|
||||
let mut family_id = data.family_id;
|
||||
|
||||
@ -714,6 +784,7 @@ ORDER BY last_access DESC
|
||||
if ![
|
||||
"n-sageder",
|
||||
"p-hofer",
|
||||
"marie-birner",
|
||||
"daniel-kortschak",
|
||||
"rudernlinz",
|
||||
"m-birner",
|
||||
@ -837,18 +908,32 @@ ORDER BY last_access DESC
|
||||
days
|
||||
}
|
||||
|
||||
async fn amount_days_to_show(&self, db: &SqlitePool) -> i64 {
|
||||
pub(crate) async fn amount_days_to_show(&self, db: &SqlitePool) -> i64 {
|
||||
if self.has_role(db, "cox").await {
|
||||
let end_of_year = NaiveDate::from_ymd_opt(Local::now().year(), 12, 31).unwrap(); //Ok,
|
||||
//december
|
||||
//has 31
|
||||
//days
|
||||
end_of_year
|
||||
let days_left_in_year = end_of_year
|
||||
.signed_duration_since(Local::now().date_naive())
|
||||
.num_days()
|
||||
+ 1
|
||||
+ 1;
|
||||
|
||||
if days_left_in_year <= 31 {
|
||||
let end_of_next_year =
|
||||
NaiveDate::from_ymd_opt(Local::now().year() + 1, 12, 31).unwrap(); //Ok,
|
||||
//december
|
||||
//has 31
|
||||
//days
|
||||
end_of_next_year
|
||||
.signed_duration_since(Local::now().date_naive())
|
||||
.num_days()
|
||||
+ 1
|
||||
} else {
|
||||
days_left_in_year
|
||||
}
|
||||
} else {
|
||||
6
|
||||
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD
|
||||
}
|
||||
}
|
||||
|
||||
@ -864,6 +949,49 @@ ORDER BY last_access DESC
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) async fn received_new_logentry(
|
||||
&self,
|
||||
db: &mut Transaction<'_, Sqlite>,
|
||||
smtp_pw: &str,
|
||||
) {
|
||||
if self.has_role_tx(db, "scheckbuch").await {
|
||||
let amount_trips = Logbook::completed_with_user_tx(db, &self).await.len();
|
||||
if amount_trips == 5 {
|
||||
if let Some(mail) = &self.mail {
|
||||
let _ = self.send_end_mail_scheckbuch(db, mail, smtp_pw).await;
|
||||
}
|
||||
let coxes = Role::find_by_name_tx(db, "cox").await.unwrap();
|
||||
Notification::create_for_role_tx(
|
||||
db,
|
||||
&coxes,
|
||||
&format!(
|
||||
"Liebe Steuerberechtigte, {} hat alle Ausfahrten des Scheckbuchs absolviert. Hoffentlich können wir uns bald über ein neues Mitglied freuen :-)",
|
||||
self.name
|
||||
),
|
||||
"Scheckbuch fertig",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
} else if amount_trips > 5 {
|
||||
let board = Role::find_by_name_tx(db, "Vorstand").await.unwrap();
|
||||
Notification::create_for_role_tx(
|
||||
db,
|
||||
&board,
|
||||
&format!(
|
||||
"Lieber Vorstand, {} hat nun bereits die {}. seiner 5 Scheckbuchausfahrten absolviert.",
|
||||
self.name, amount_trips
|
||||
),
|
||||
"Scheckbuch überfertig",
|
||||
None,None
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: check fahrtenabzeichen fertig?
|
||||
// TODO: check äquatorpreis geschafft?
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
@ -896,266 +1024,84 @@ impl<'r> FromRequest<'r> for User {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TechUser {
|
||||
pub(crate) user: User,
|
||||
}
|
||||
/// Creates a struct named $name. Allows to be created from a user, if one of the specified $roles are active for the user.
|
||||
macro_rules! special_user {
|
||||
($name:ident, $($role:tt)*) => {
|
||||
#[derive(Debug)]
|
||||
pub struct $name {
|
||||
pub(crate) user: User,
|
||||
}
|
||||
|
||||
impl Deref for TechUser {
|
||||
type Target = User;
|
||||
impl Deref for $name {
|
||||
type Target = User;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.user
|
||||
}
|
||||
}
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.user
|
||||
}
|
||||
}
|
||||
impl $name {
|
||||
pub fn into_inner(self) -> User {
|
||||
self.user
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for TechUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "tech").await {
|
||||
Outcome::Success(TechUser { user })
|
||||
} else {
|
||||
Outcome::Error((Status::Forbidden, LoginError::NotACox))
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for $name {
|
||||
type Error = LoginError;
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => {
|
||||
if special_user!(@check_roles user, db, $($role)*) {
|
||||
Outcome::Success($name { user })
|
||||
} else {
|
||||
Outcome::Forward(Status::Forbidden)
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CoxUser {
|
||||
pub(crate) user: User,
|
||||
}
|
||||
|
||||
impl Deref for CoxUser {
|
||||
type Target = User;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.user
|
||||
}
|
||||
}
|
||||
|
||||
impl CoxUser {
|
||||
pub async fn new(db: &SqlitePool, user: User) -> Option<Self> {
|
||||
if user.has_role(db, "cox").await {
|
||||
Some(CoxUser { user })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for CoxUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "cox").await {
|
||||
Outcome::Success(CoxUser { user })
|
||||
impl $name {
|
||||
pub async fn new(db: &SqlitePool, user: User) -> Option<Self> {
|
||||
if special_user!(@check_roles user, db, $($role)*) {
|
||||
Some($name { user })
|
||||
} else {
|
||||
Outcome::Error((Status::Forbidden, LoginError::NotACox))
|
||||
None
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AdminUser {
|
||||
pub(crate) user: User,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for AdminUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "admin").await {
|
||||
Outcome::Success(AdminUser { user })
|
||||
} else {
|
||||
Outcome::Forward(Status::Forbidden)
|
||||
};
|
||||
(@check_roles $user:ident, $db:ident, $(+$role:expr),* $(,-$neg_role:expr)*) => {
|
||||
{
|
||||
let mut has_positive_role = false;
|
||||
$(
|
||||
if $user.has_role($db, $role).await {
|
||||
has_positive_role = true;
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
)*
|
||||
has_positive_role
|
||||
$(
|
||||
&& !$user.has_role($db, $neg_role).await
|
||||
)*
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct AllowedForPlannedTripsUser(pub(crate) User);
|
||||
special_user!(TechUser, +"tech");
|
||||
special_user!(CoxUser, +"cox");
|
||||
special_user!(AdminUser, +"admin");
|
||||
special_user!(AllowedForPlannedTripsUser, +"Donau Linz", +"scheckbuch");
|
||||
special_user!(DonauLinzUser, +"Donau Linz", -"Unterstützend", -"Förderndes Mitglied");
|
||||
special_user!(SchnupperBetreuerUser, +"schnupper-betreuer");
|
||||
special_user!(VorstandUser, +"Vorstand");
|
||||
special_user!(EventUser, +"manage_events");
|
||||
special_user!(AllowedToEditPaymentStatusUser, +"kassier", +"admin");
|
||||
special_user!(ManageUserUser, +"admin", +"schriftfuehrer");
|
||||
special_user!(AllowedToUpdateTripToAlwaysBeShownUser, +"admin");
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for AllowedForPlannedTripsUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "Donau Linz").await | user.has_role(db, "scheckbuch").await {
|
||||
Outcome::Success(AllowedForPlannedTripsUser(user))
|
||||
} else {
|
||||
Outcome::Error((Status::Forbidden, LoginError::NotACox))
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AllowedForPlannedTripsUser> for User {
|
||||
fn from(val: AllowedForPlannedTripsUser) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct DonauLinzUser(pub(crate) User);
|
||||
|
||||
impl From<DonauLinzUser> for User {
|
||||
fn from(val: DonauLinzUser) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for DonauLinzUser {
|
||||
type Target = User;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for DonauLinzUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "Donau Linz").await
|
||||
&& !user.has_role(db, "Unterstützend").await
|
||||
&& !user.has_role(db, "Förderndes Mitglied").await
|
||||
{
|
||||
Outcome::Success(DonauLinzUser(user))
|
||||
} else {
|
||||
Outcome::Error((Status::Forbidden, LoginError::NotACox))
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SchnupperBetreuerUser(pub(crate) User);
|
||||
|
||||
impl From<SchnupperBetreuerUser> for User {
|
||||
fn from(val: SchnupperBetreuerUser) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SchnupperBetreuerUser {
|
||||
type Target = User;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for SchnupperBetreuerUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "schnupper-betreuer").await {
|
||||
Outcome::Success(SchnupperBetreuerUser(user))
|
||||
} else {
|
||||
Outcome::Forward(Status::Forbidden)
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct VorstandUser(pub(crate) User);
|
||||
|
||||
impl From<VorstandUser> for User {
|
||||
fn from(val: VorstandUser) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for VorstandUser {
|
||||
type Target = User;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for VorstandUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "Vorstand").await {
|
||||
Outcome::Success(VorstandUser(user))
|
||||
} else {
|
||||
Outcome::Forward(Status::Forbidden)
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct EventUser(pub(crate) User);
|
||||
|
||||
impl From<EventUser> for User {
|
||||
fn from(val: EventUser) -> Self {
|
||||
val.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for EventUser {
|
||||
type Target = User;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
#[derive(FromRow, Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct UserWithRolesAndMembershipPdf {
|
||||
#[serde(flatten)]
|
||||
@ -1199,25 +1145,6 @@ impl UserWithMembershipPdf {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl<'r> FromRequest<'r> for EventUser {
|
||||
type Error = LoginError;
|
||||
|
||||
async fn from_request(req: &'r Request<'_>) -> request::Outcome<Self, Self::Error> {
|
||||
let db = req.rocket().state::<SqlitePool>().unwrap();
|
||||
match User::from_request(req).await {
|
||||
Outcome::Success(user) => {
|
||||
if user.has_role(db, "manage_events").await {
|
||||
Outcome::Success(EventUser(user))
|
||||
} else {
|
||||
Outcome::Error((Status::Forbidden, LoginError::NotACox))
|
||||
}
|
||||
}
|
||||
Outcome::Error(f) => Outcome::Error(f),
|
||||
Outcome::Forward(f) => Outcome::Forward(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::collections::HashMap;
|
||||
@ -1266,7 +1193,7 @@ mod test {
|
||||
fn test_cox() {
|
||||
let pool = testdb!();
|
||||
let res = User::cox(&pool).await;
|
||||
assert_eq!(res.len(), 3);
|
||||
assert_eq!(res.len(), 4);
|
||||
}
|
||||
|
||||
#[sqlx::test]
|
||||
|
@ -1,9 +1,21 @@
|
||||
use sqlx::SqlitePool;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, SqlitePool};
|
||||
|
||||
use super::{notification::Notification, trip::Trip, tripdetails::TripDetails, user::User};
|
||||
use super::{
|
||||
notification::Notification,
|
||||
trip::{Trip, TripWithUserAndType},
|
||||
tripdetails::TripDetails,
|
||||
user::{CoxUser, User},
|
||||
};
|
||||
use crate::model::tripdetails::{Action, CoxAtTrip::Yes};
|
||||
|
||||
pub struct UserTrip {}
|
||||
#[derive(FromRow, Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct UserTrip {
|
||||
pub user_id: Option<i64>,
|
||||
pub user_note: Option<String>,
|
||||
pub trip_details_id: i64,
|
||||
pub created_at: String, // TODO: switch to NaiveDateTime
|
||||
}
|
||||
|
||||
impl UserTrip {
|
||||
pub async fn create(
|
||||
@ -24,6 +36,10 @@ impl UserTrip {
|
||||
return Err(UserTripError::GuestNotAllowedForThisEvent);
|
||||
}
|
||||
|
||||
if !trip_details.user_sees_trip(db, user).await {
|
||||
return Err(UserTripError::NotVisibleToUser);
|
||||
}
|
||||
|
||||
//TODO: Check if user sees the event (otherwise she could forge trip_details_id)
|
||||
|
||||
let is_cox = trip_details.user_is_cox(db, user).await;
|
||||
@ -62,23 +78,27 @@ impl UserTrip {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
user_note.unwrap()
|
||||
user_note.clone().unwrap()
|
||||
};
|
||||
|
||||
if let Some(trip) = Trip::find_by_trip_details(db, trip_details.id).await {
|
||||
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
Notification::create(
|
||||
db,
|
||||
&cox,
|
||||
&format!(
|
||||
"{} hat sich für deine Ausfahrt am {} registriert",
|
||||
name_newly_registered_person, trip.day
|
||||
),
|
||||
"Registrierung bei deiner Ausfahrt",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
if user_note.is_none() {
|
||||
// Don't show notification if we add guest (as only we are
|
||||
// allowed to do so)
|
||||
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
Notification::create(
|
||||
db,
|
||||
&cox,
|
||||
&format!(
|
||||
"{} hat sich für deine Ausfahrt am {} registriert",
|
||||
name_newly_registered_person, trip.day
|
||||
),
|
||||
"Registrierung bei deiner Ausfahrt",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
trip_details.check_free_spaces(db).await;
|
||||
}
|
||||
@ -86,6 +106,34 @@ impl UserTrip {
|
||||
Ok(name_newly_registered_person)
|
||||
}
|
||||
|
||||
pub async fn tripdetails(&self, db: &SqlitePool) -> TripDetails {
|
||||
TripDetails::find_by_id(db, self.trip_details_id)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn find_by_userid_and_trip_detail_id(
|
||||
db: &SqlitePool,
|
||||
user_id: i64,
|
||||
trip_detail_id: i64,
|
||||
) -> Option<Self> {
|
||||
sqlx::query_as!(Self, "SELECT user_id, user_note, trip_details_id, created_at FROM user_trip WHERE user_id= ? AND trip_details_id = ?", user_id, trip_detail_id)
|
||||
.fetch_one(db)
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn self_delete(&self, db: &SqlitePool) -> Result<(), UserTripDeleteError> {
|
||||
let trip_details = self.tripdetails(db).await;
|
||||
if let Some(id) = self.user_id {
|
||||
let user = User::find_by_id(db, id as i32).await.unwrap();
|
||||
return Self::delete(db, &user, &trip_details, self.user_note.clone()).await;
|
||||
}
|
||||
|
||||
Ok(()) // TODO: fixme
|
||||
}
|
||||
|
||||
//TODO: cleaner code
|
||||
pub async fn delete(
|
||||
db: &SqlitePool,
|
||||
user: &User,
|
||||
@ -96,7 +144,28 @@ impl UserTrip {
|
||||
return Err(UserTripDeleteError::DetailsLocked);
|
||||
}
|
||||
|
||||
if let Some(name) = name {
|
||||
if !trip_details.user_sees_trip(db, user).await {
|
||||
return Err(UserTripDeleteError::NotVisibleToUser);
|
||||
}
|
||||
|
||||
let mut trip_to_delete = None;
|
||||
let mut some_trip = None;
|
||||
if let Some(trip) = Trip::find_by_trip_details(db, trip_details.id).await {
|
||||
some_trip = Some(trip.clone());
|
||||
// If trip is cancelled, and lost rower just unregistered, delete the trip
|
||||
if TripDetails::find_by_id(db, trip_details.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.cancelled()
|
||||
{
|
||||
let trip = TripWithUserAndType::from(db, trip.clone()).await;
|
||||
if trip.rower.len() == 1 {
|
||||
trip_to_delete = Some(trip.trip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(name) = name.clone() {
|
||||
if !trip_details.user_allowed_to_change(db, user).await {
|
||||
return Err(UserTripDeleteError::NotAllowedToDeleteGuest);
|
||||
}
|
||||
@ -124,6 +193,55 @@ impl UserTrip {
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let mut add_info = "";
|
||||
if let Some(trip) = &trip_to_delete {
|
||||
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
trip.delete(db, &CoxUser::new(db, cox).await.unwrap())
|
||||
.await
|
||||
.unwrap();
|
||||
add_info = " Das war die letzte angemeldete Person. Nachdem nun alle Bescheid wissen, wird die Ausfahrt ab sofort nicht mehr angezeigt.";
|
||||
}
|
||||
|
||||
if let Some(trip) = some_trip {
|
||||
let opt_cancelled = if trip_to_delete.is_some() {
|
||||
"abgesagten "
|
||||
} else {
|
||||
""
|
||||
};
|
||||
if let Some(name) = name {
|
||||
if !add_info.is_empty() {
|
||||
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
Notification::create(
|
||||
db,
|
||||
&cox,
|
||||
&format!(
|
||||
"Du hast {} von deiner {}Ausfahrt am {} abgemeldet.{}",
|
||||
name, opt_cancelled, trip.day, add_info
|
||||
),
|
||||
"Abmeldung von deiner Ausfahrt",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
|
||||
Notification::create(
|
||||
db,
|
||||
&cox,
|
||||
&format!(
|
||||
"{} hat sich von deiner {}Ausfahrt am {} abgemeldet.{}",
|
||||
user.name, opt_cancelled, trip.day, add_info
|
||||
),
|
||||
"Abmeldung von deiner Ausfahrt",
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -137,6 +255,7 @@ pub enum UserTripError {
|
||||
CantRegisterAtOwnEvent,
|
||||
GuestNotAllowedForThisEvent,
|
||||
NotAllowedToAddGuest,
|
||||
NotVisibleToUser,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
@ -144,6 +263,7 @@ pub enum UserTripDeleteError {
|
||||
DetailsLocked,
|
||||
GuestNotParticipating,
|
||||
NotAllowedToDeleteGuest,
|
||||
NotVisibleToUser,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -14,8 +14,12 @@ pub fn schedule(db: &SqlitePool, config: &Config) {
|
||||
let openweathermap_key = config.openweathermap_key.clone();
|
||||
|
||||
tokio::task::spawn(async {
|
||||
waterlevel::update(&db).await.unwrap();
|
||||
weather::update(&db, &openweathermap_key).await.unwrap();
|
||||
if let Err(e) = waterlevel::update(&db).await {
|
||||
log::error!("Water level update error: {e}, trying again next time");
|
||||
}
|
||||
if let Err(e) = weather::update(&db, &openweathermap_key).await {
|
||||
log::error!("Weather update error: {e}, trying again next time");
|
||||
}
|
||||
|
||||
let mut sched = JobScheduler::new();
|
||||
|
||||
@ -26,10 +30,12 @@ pub fn schedule(db: &SqlitePool, config: &Config) {
|
||||
// nicer one's rust (stable) support async closures
|
||||
task::block_in_place(|| {
|
||||
tokio::runtime::Handle::current().block_on(async {
|
||||
waterlevel::update(&db_clone).await.unwrap();
|
||||
weather::update(&db_clone, &openweathermap_key)
|
||||
.await
|
||||
.unwrap();
|
||||
if let Err(e) = waterlevel::update(&db_clone).await {
|
||||
log::error!("Water level update error: {e}, trying again next time");
|
||||
}
|
||||
if let Err(e) = weather::update(&db_clone, &openweathermap_key).await {
|
||||
log::error!("Weather update error: {e}, trying again next time");
|
||||
}
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
@ -18,6 +18,7 @@ use crate::model::{
|
||||
struct AddEventForm<'r> {
|
||||
name: &'r str,
|
||||
planned_amount_cox: i32,
|
||||
always_show: bool,
|
||||
tripdetails: TripDetailsToAdd<'r>,
|
||||
}
|
||||
|
||||
@ -34,7 +35,14 @@ async fn create(
|
||||
//just created
|
||||
//the object
|
||||
|
||||
Event::create(db, data.name, data.planned_amount_cox, &trip_details).await;
|
||||
Event::create(
|
||||
db,
|
||||
data.name,
|
||||
data.planned_amount_cox,
|
||||
data.always_show,
|
||||
&trip_details,
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::success(Redirect::to("/planned"), "Event hinzugefügt")
|
||||
}
|
||||
|
@ -9,14 +9,14 @@ use sqlx::SqlitePool;
|
||||
use crate::model::log::Log;
|
||||
use crate::model::mail::Mail;
|
||||
use crate::model::role::Role;
|
||||
use crate::model::user::AdminUser;
|
||||
use crate::model::user::UserWithDetails;
|
||||
use crate::model::user::{AdminUser, VorstandUser};
|
||||
use crate::tera::Config;
|
||||
|
||||
#[get("/mail")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AdminUser,
|
||||
admin: VorstandUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
@ -65,7 +65,7 @@ async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<MailToSend<'_>>,
|
||||
config: &State<Config>,
|
||||
admin: AdminUser,
|
||||
admin: VorstandUser,
|
||||
) -> Flash<Redirect> {
|
||||
let d = data.into_inner();
|
||||
Log::create(db, format!("{admin:?} trying to send this mail: {d:?}")).await;
|
||||
|
@ -2,7 +2,7 @@ use crate::model::{
|
||||
log::Log,
|
||||
notification::Notification,
|
||||
role::Role,
|
||||
user::{AdminUser, User, UserWithDetails},
|
||||
user::{User, UserWithDetails, VorstandUser},
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use rocket::{
|
||||
@ -18,7 +18,7 @@ use sqlx::SqlitePool;
|
||||
#[get("/notification")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
user: AdminUser,
|
||||
user: VorstandUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
@ -62,7 +62,7 @@ pub struct NotificationToSendUser {
|
||||
async fn send_group(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<NotificationToSendGroup>,
|
||||
admin: AdminUser,
|
||||
admin: VorstandUser,
|
||||
) -> Flash<Redirect> {
|
||||
let d = data.into_inner();
|
||||
Log::create(
|
||||
@ -89,7 +89,7 @@ async fn send_group(
|
||||
async fn send_user(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<NotificationToSendUser>,
|
||||
admin: AdminUser,
|
||||
admin: VorstandUser,
|
||||
) -> Flash<Redirect> {
|
||||
let d = data.into_inner();
|
||||
Log::create(
|
||||
|
@ -29,7 +29,7 @@ async fn index(
|
||||
context.insert("schnupperanten", &users);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
&UserWithDetails::from_user(user.user, db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/schnupper/index", context.into_json())
|
||||
|
@ -7,13 +7,14 @@ use crate::{
|
||||
logbook::Logbook,
|
||||
role::Role,
|
||||
user::{
|
||||
AdminUser, User, UserWithDetails, UserWithMembershipPdf, UserWithRolesAndMembershipPdf,
|
||||
VorstandUser,
|
||||
AdminUser, AllowedToEditPaymentStatusUser, ManageUserUser, SchnupperBetreuerUser, User,
|
||||
UserWithDetails, UserWithMembershipPdf, UserWithRolesAndMembershipPdf, VorstandUser,
|
||||
},
|
||||
},
|
||||
tera::Config,
|
||||
};
|
||||
use futures::future::join_all;
|
||||
use lettre::Address;
|
||||
use rocket::{
|
||||
form::Form,
|
||||
fs::TempFile,
|
||||
@ -54,8 +55,8 @@ async fn index(
|
||||
.map(|u| async move { UserWithRolesAndMembershipPdf::from_user(db, u).await })
|
||||
.collect();
|
||||
|
||||
let user: User = user.into();
|
||||
let allowed_to_edit = user.has_role(db, "admin").await;
|
||||
let user: User = user.into_inner();
|
||||
let allowed_to_edit = ManageUserUser::new(db, user.clone()).await.is_some();
|
||||
|
||||
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
|
||||
|
||||
@ -89,7 +90,7 @@ async fn index_admin(
|
||||
let users: Vec<UserWithRolesAndMembershipPdf> = join_all(user_futures).await;
|
||||
|
||||
let user: User = user.user;
|
||||
let allowed_to_edit = user.has_role(db, "admin").await;
|
||||
let allowed_to_edit = ManageUserUser::new(db, user.clone()).await.is_some();
|
||||
|
||||
let roles = Role::all(db).await;
|
||||
let families = Family::all_with_members(db).await;
|
||||
@ -110,7 +111,7 @@ async fn index_admin(
|
||||
#[get("/user/fees")]
|
||||
async fn fees(
|
||||
db: &State<SqlitePool>,
|
||||
admin: VorstandUser,
|
||||
user: VorstandUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
@ -130,7 +131,7 @@ async fn fees(
|
||||
}
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(admin.into(), db).await,
|
||||
&UserWithDetails::from_user(user.into_inner(), db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/user/fees", context.into_json())
|
||||
@ -161,7 +162,7 @@ async fn scheckbuch(
|
||||
}
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
&UserWithDetails::from_user(user.into_inner(), db).await,
|
||||
);
|
||||
|
||||
Template::render("admin/user/scheckbuch", context.into_json())
|
||||
@ -170,7 +171,7 @@ async fn scheckbuch(
|
||||
#[get("/user/fees/paid?<user_ids>")]
|
||||
async fn fees_paid(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AdminUser,
|
||||
calling_user: AllowedToEditPaymentStatusUser,
|
||||
user_ids: Vec<i32>,
|
||||
referer: Referer,
|
||||
) -> Flash<Redirect> {
|
||||
@ -181,7 +182,10 @@ async fn fees_paid(
|
||||
if user.has_role(db, "paid").await {
|
||||
Log::create(
|
||||
db,
|
||||
format!("{} set fees NOT paid for '{}'", admin.user.name, user.name),
|
||||
format!(
|
||||
"{} set fees NOT paid for '{}'",
|
||||
calling_user.user.name, user.name
|
||||
),
|
||||
)
|
||||
.await;
|
||||
user.remove_role(db, &Role::find_by_name(db, "paid").await.unwrap())
|
||||
@ -189,7 +193,10 @@ async fn fees_paid(
|
||||
} else {
|
||||
Log::create(
|
||||
db,
|
||||
format!("{} set fees paid for '{}'", admin.user.name, user.name),
|
||||
format!(
|
||||
"{} set fees paid for '{}'",
|
||||
calling_user.user.name, user.name
|
||||
),
|
||||
)
|
||||
.await;
|
||||
user.add_role(db, &Role::find_by_name(db, "paid").await.unwrap())
|
||||
@ -208,7 +215,7 @@ async fn fees_paid(
|
||||
#[get("/user/<user>/send-welcome-mail")]
|
||||
async fn send_welcome_mail(
|
||||
db: &State<SqlitePool>,
|
||||
_admin: AdminUser,
|
||||
_admin: ManageUserUser,
|
||||
config: &State<Config>,
|
||||
user: i32,
|
||||
) -> Flash<Redirect> {
|
||||
@ -226,7 +233,7 @@ async fn send_welcome_mail(
|
||||
}
|
||||
|
||||
#[get("/user/<user>/reset-pw")]
|
||||
async fn resetpw(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<Redirect> {
|
||||
async fn resetpw(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Flash<Redirect> {
|
||||
let user = User::find_by_id(db, user).await;
|
||||
match user {
|
||||
Some(user) => {
|
||||
@ -246,7 +253,7 @@ async fn resetpw(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<R
|
||||
}
|
||||
|
||||
#[get("/user/<user>/delete")]
|
||||
async fn delete(db: &State<SqlitePool>, admin: AdminUser, user: i32) -> Flash<Redirect> {
|
||||
async fn delete(db: &State<SqlitePool>, admin: ManageUserUser, user: i32) -> Flash<Redirect> {
|
||||
let user = User::find_by_id(db, user).await;
|
||||
Log::create(db, format!("{} deleted user: {user:?}", admin.user.name)).await;
|
||||
match user {
|
||||
@ -283,7 +290,7 @@ pub struct UserEditForm<'a> {
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<UserEditForm<'_>>,
|
||||
admin: AdminUser,
|
||||
admin: ManageUserUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user = User::find_by_id(db, data.id).await;
|
||||
Log::create(
|
||||
@ -306,7 +313,7 @@ async fn update(
|
||||
#[get("/user/<user>/membership")]
|
||||
async fn download_membership_pdf(
|
||||
db: &State<SqlitePool>,
|
||||
admin: AdminUser,
|
||||
admin: ManageUserUser,
|
||||
user: i32,
|
||||
) -> (ContentType, Vec<u8>) {
|
||||
let user = User::find_by_id(db, user).await.unwrap();
|
||||
@ -332,7 +339,7 @@ struct UserAddForm<'r> {
|
||||
async fn create(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<UserAddForm<'_>>,
|
||||
admin: AdminUser,
|
||||
admin: ManageUserUser,
|
||||
) -> Flash<Redirect> {
|
||||
if User::create(db, data.name).await {
|
||||
Log::create(
|
||||
@ -349,6 +356,103 @@ async fn create(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
struct UserAddScheckbuchForm<'r> {
|
||||
name: &'r str,
|
||||
mail: &'r str,
|
||||
}
|
||||
|
||||
#[post("/user/new/scheckbuch", data = "<data>")]
|
||||
async fn create_scheckbuch(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<UserAddScheckbuchForm<'_>>,
|
||||
admin: VorstandUser,
|
||||
config: &State<Config>,
|
||||
) -> Flash<Redirect> {
|
||||
// 1. Check mail adress
|
||||
let mail = data.mail.trim();
|
||||
if mail.parse::<Address>().is_err() {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user/scheckbuch"),
|
||||
"Keine gültige Mailadresse".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Check name
|
||||
let name = data.name.trim();
|
||||
if User::find_by_name(db, name).await.is_some() {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/user/scheckbuch"),
|
||||
"Kann kein Scheckbuch erstellen, der Name wird bereits von einem User verwendet"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
// 3. Create user
|
||||
User::create_with_mail(db, name, mail).await;
|
||||
let user = User::find_by_name(db, name).await.unwrap();
|
||||
|
||||
// 4. Add 'scheckbuch' role
|
||||
let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap();
|
||||
user.add_role(db, &scheckbuch).await;
|
||||
|
||||
// 4. Send welcome mail (+ notification)
|
||||
user.send_welcome_email(db, &config.smtp_pw).await.unwrap();
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!("{} created new scheckbuch: {data:?}", admin.name),
|
||||
)
|
||||
.await;
|
||||
Flash::success(Redirect::to("/admin/user/scheckbuch"), &format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {mail} verschickt."))
|
||||
}
|
||||
|
||||
#[get("/user/move/schnupperant/<id>/to/scheckbuch")]
|
||||
async fn schnupper_to_scheckbuch(
|
||||
db: &State<SqlitePool>,
|
||||
id: i32,
|
||||
admin: SchnupperBetreuerUser,
|
||||
config: &State<Config>,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(user) = User::find_by_id(db, id).await else {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/schnupper"),
|
||||
"user id not found".to_string(),
|
||||
);
|
||||
};
|
||||
|
||||
if !user.has_role(db, "schnupperant").await {
|
||||
return Flash::error(
|
||||
Redirect::to("/admin/schnupper"),
|
||||
"kein schnupperant...".to_string(),
|
||||
);
|
||||
}
|
||||
|
||||
let schnupperant = Role::find_by_name(db, "schnupperant").await.unwrap();
|
||||
let paid = Role::find_by_name(db, "paid").await.unwrap();
|
||||
user.remove_role(db, &schnupperant).await;
|
||||
user.remove_role(db, &paid).await;
|
||||
|
||||
let scheckbuch = Role::find_by_name(db, "scheckbuch").await.unwrap();
|
||||
user.add_role(db, &scheckbuch).await;
|
||||
|
||||
if let Some(no_einschreibgebuehr) = Role::find_by_name(db, "no-einschreibgebuehr").await {
|
||||
user.add_role(db, &no_einschreibgebuehr).await;
|
||||
}
|
||||
|
||||
user.send_welcome_email(db, &config.smtp_pw).await.unwrap();
|
||||
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"{} created new scheckbuch (from schnupperant): {}",
|
||||
admin.name, user.name
|
||||
),
|
||||
)
|
||||
.await;
|
||||
Flash::success(Redirect::to("/admin/schnupper"), &format!("Scheckbuch erfolgreich erstellt. Eine E-Mail in der alles erklärt wird, wurde an {} verschickt.", user.mail.unwrap()))
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![
|
||||
index,
|
||||
@ -356,6 +460,8 @@ pub fn routes() -> Vec<Route> {
|
||||
resetpw,
|
||||
update,
|
||||
create,
|
||||
create_scheckbuch,
|
||||
schnupper_to_scheckbuch,
|
||||
delete,
|
||||
fees,
|
||||
fees_paid,
|
||||
|
46
src/tera/board/achievement.rs
Normal file
46
src/tera/board/achievement.rs
Normal file
@ -0,0 +1,46 @@
|
||||
use crate::model::{
|
||||
personal::Achievements,
|
||||
role::Role,
|
||||
user::{User, UserWithDetails, VorstandUser},
|
||||
};
|
||||
use rocket::{get, request::FlashMessage, routes, Route, State};
|
||||
use rocket_dyn_templates::{tera::Context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
#[get("/achievement")]
|
||||
async fn index(
|
||||
db: &State<SqlitePool>,
|
||||
admin: VorstandUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let mut context = Context::new();
|
||||
if let Some(msg) = flash {
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
let role = Role::find_by_name(&db, "Donau Linz").await.unwrap();
|
||||
let users = User::all_with_role(&db, &role).await;
|
||||
let mut people = Vec::new();
|
||||
let mut rowingbadge_year = None;
|
||||
for user in users {
|
||||
let achievement = Achievements::for_user(&db, &user).await;
|
||||
if let Some(badge) = &achievement.rowingbadge {
|
||||
rowingbadge_year = Some(badge.year);
|
||||
}
|
||||
people.push((user, achievement));
|
||||
}
|
||||
|
||||
context.insert("people", &people);
|
||||
context.insert("rowingbadge_year", &rowingbadge_year.unwrap());
|
||||
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(admin.into_inner(), db).await,
|
||||
);
|
||||
|
||||
Template::render("achievement", context.into_json())
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![index]
|
||||
}
|
@ -39,7 +39,7 @@ async fn index(
|
||||
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(admin.into(), db).await,
|
||||
&UserWithDetails::from_user(admin.into_inner(), db).await,
|
||||
);
|
||||
|
||||
Template::render("board/boathouse", context.into_json())
|
||||
|
@ -1,9 +1,11 @@
|
||||
use rocket::Route;
|
||||
|
||||
pub mod achievement;
|
||||
pub mod boathouse;
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
let mut ret = Vec::new();
|
||||
ret.append(&mut boathouse::routes());
|
||||
ret.append(&mut achievement::routes());
|
||||
ret
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ async fn index(
|
||||
context.insert("boats", &boats);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
&UserWithDetails::from_user(user.into_inner(), db).await,
|
||||
);
|
||||
|
||||
Template::render("boatdamages", context.into_json())
|
||||
@ -78,7 +78,7 @@ async fn create<'r>(
|
||||
data: Form<FormBoatDamageToAdd<'r>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user: User = user.into();
|
||||
let user: User = user.into_inner();
|
||||
let boatdamage_to_add = BoatDamageToAdd {
|
||||
boat_id: data.boat_id,
|
||||
desc: data.desc,
|
||||
|
@ -75,7 +75,7 @@ async fn index(
|
||||
context.insert("user", &User::all(db).await);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
&UserWithDetails::from_user(user.into_inner(), db).await,
|
||||
);
|
||||
|
||||
Template::render("boatreservations", context.into_json())
|
||||
@ -97,7 +97,7 @@ async fn create<'r>(
|
||||
data: Form<FormBoatReservationToAdd<'r>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user_applicant: User = user.into();
|
||||
let user_applicant: User = user.into_inner();
|
||||
let boat = Boat::find_by_id(db, data.boat_id as i32).await.unwrap();
|
||||
let boatreservation_to_add = BoatReservationToAdd {
|
||||
boat: &boat,
|
||||
|
@ -11,7 +11,7 @@ use crate::model::{
|
||||
log::Log,
|
||||
trip::{self, CoxHelpError, Trip, TripDeleteError, TripHelpDeleteError, TripUpdateError},
|
||||
tripdetails::{TripDetails, TripDetailsToAdd},
|
||||
user::CoxUser,
|
||||
user::{AllowedToUpdateTripToAlwaysBeShownUser, CoxUser},
|
||||
};
|
||||
|
||||
#[post("/trip", data = "<data>")]
|
||||
@ -42,7 +42,6 @@ struct EditTripForm<'r> {
|
||||
max_people: i32,
|
||||
notes: Option<&'r str>,
|
||||
trip_type: Option<i64>,
|
||||
always_show: bool,
|
||||
is_locked: bool,
|
||||
}
|
||||
|
||||
@ -60,7 +59,6 @@ async fn update(
|
||||
max_people: data.max_people,
|
||||
notes: data.notes,
|
||||
trip_type: data.trip_type,
|
||||
always_show: data.always_show,
|
||||
is_locked: data.is_locked,
|
||||
};
|
||||
match Trip::update_own(db, &update).await {
|
||||
@ -80,6 +78,23 @@ async fn update(
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/trip/<trip_id>/toggle-always-show")]
|
||||
async fn toggle_always_show(
|
||||
db: &State<SqlitePool>,
|
||||
trip_id: i64,
|
||||
_user: AllowedToUpdateTripToAlwaysBeShownUser,
|
||||
) -> Flash<Redirect> {
|
||||
if let Some(trip) = Trip::find_by_id(db, trip_id).await {
|
||||
trip.toggle_always_show(db).await;
|
||||
Flash::success(
|
||||
Redirect::to("/planned"),
|
||||
"'Immer anzeigen' erfolgreich gesetzt!",
|
||||
)
|
||||
} else {
|
||||
Flash::error(Redirect::to("/planned"), "Ausfahrt gibt's nicht")
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/join/<planned_event_id>")]
|
||||
async fn join(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) -> Flash<Redirect> {
|
||||
if let Some(planned_event) = Event::find_by_id(db, planned_event_id).await {
|
||||
@ -164,12 +179,19 @@ async fn remove(db: &State<SqlitePool>, planned_event_id: i64, cox: CoxUser) ->
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![create, join, remove, remove_trip, update]
|
||||
routes![
|
||||
create,
|
||||
join,
|
||||
remove,
|
||||
remove_trip,
|
||||
update,
|
||||
toggle_always_show
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use chrono::NaiveDate;
|
||||
use chrono::{Local, NaiveDate};
|
||||
use rocket::{
|
||||
http::{ContentType, Status},
|
||||
local::asynchronous::Client,
|
||||
@ -230,7 +252,9 @@ mod test {
|
||||
fn test_trip_update_succ() {
|
||||
let db = testdb!();
|
||||
|
||||
let trip = &Trip::get_for_day(&db, NaiveDate::from_ymd_opt(1970, 01, 02).unwrap()).await[0];
|
||||
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
|
||||
println!("{tomorrow}");
|
||||
let trip = &Trip::get_for_day(&db, tomorrow).await[0];
|
||||
assert_eq!(1, trip.trip.max_people);
|
||||
assert_eq!(
|
||||
"trip_details for trip from cox",
|
||||
@ -266,7 +290,8 @@ mod test {
|
||||
"7:successAusfahrt erfolgreich aktualisiert."
|
||||
);
|
||||
|
||||
let trip = &Trip::get_for_day(&db, NaiveDate::from_ymd_opt(1970, 01, 02).unwrap()).await[0];
|
||||
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
|
||||
let trip = &Trip::get_for_day(&db, tomorrow).await[0];
|
||||
assert_eq!(12, trip.trip.max_people);
|
||||
assert_eq!("my-new-notes", &trip.trip.notes.clone().unwrap());
|
||||
}
|
||||
@ -306,7 +331,9 @@ mod test {
|
||||
fn test_trip_update_wrong_cox() {
|
||||
let db = testdb!();
|
||||
|
||||
let trip = &Trip::get_for_day(&db, NaiveDate::from_ymd_opt(1970, 01, 02).unwrap()).await[0];
|
||||
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
|
||||
|
||||
let trip = &Trip::get_for_day(&db, tomorrow).await[0];
|
||||
assert_eq!(1, trip.trip.max_people);
|
||||
assert_eq!(
|
||||
"trip_details for trip from cox",
|
||||
|
132
src/tera/log.rs
132
src/tera/log.rs
@ -15,16 +15,21 @@ use rocket_dyn_templates::{context, Template};
|
||||
use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::model::{
|
||||
boat::Boat,
|
||||
boatreservation::BoatReservation,
|
||||
log::Log,
|
||||
logbook::{
|
||||
LogToAdd, LogToFinalize, Logbook, LogbookCreateError, LogbookDeleteError,
|
||||
LogbookUpdateError,
|
||||
use crate::{
|
||||
model::{
|
||||
boat::Boat,
|
||||
boatreservation::BoatReservation,
|
||||
distance::Distance,
|
||||
log::Log,
|
||||
logbook::{
|
||||
LogToAdd, LogToFinalize, LogToUpdate, Logbook, LogbookAdminUpdateError,
|
||||
LogbookCreateError, LogbookDeleteError, LogbookUpdateError,
|
||||
},
|
||||
logtype::LogType,
|
||||
trip::Trip,
|
||||
user::{AdminUser, DonauLinzUser, User, UserWithDetails, VorstandUser},
|
||||
},
|
||||
logtype::LogType,
|
||||
user::{AdminUser, DonauLinzUser, User, UserWithDetails},
|
||||
tera::Config,
|
||||
};
|
||||
|
||||
pub struct KioskCookie(());
|
||||
@ -68,11 +73,13 @@ async fn index(
|
||||
)
|
||||
.await;
|
||||
users.retain(|u| {
|
||||
u.roles.contains(&"Donau Linz".into()) || u.roles.contains(&"scheckbuch".into())
|
||||
u.roles.contains(&"Donau Linz".into())
|
||||
|| u.roles.contains(&"scheckbuch".into())
|
||||
|| u.user.name == "Externe Steuerperson"
|
||||
});
|
||||
|
||||
let logtypes = LogType::all(db).await;
|
||||
let distances = Logbook::distances(db).await;
|
||||
let distances = Distance::all(db).await;
|
||||
|
||||
let on_water = Logbook::on_water(db).await;
|
||||
|
||||
@ -82,6 +89,7 @@ async fn index(
|
||||
}
|
||||
|
||||
context.insert("boats", &boats);
|
||||
context.insert("planned_trips", &Trip::get_for_today(db).await);
|
||||
context.insert(
|
||||
"reservations",
|
||||
&BoatReservation::all_future_with_groups(db).await,
|
||||
@ -91,7 +99,7 @@ async fn index(
|
||||
context.insert("logtypes", &logtypes);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
&UserWithDetails::from_user(user.into_inner(), db).await,
|
||||
);
|
||||
context.insert("on_water", &on_water);
|
||||
context.insert("distances", &distances);
|
||||
@ -105,7 +113,7 @@ async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
|
||||
|
||||
Template::render(
|
||||
"log.completed",
|
||||
context!(logs, loggedin_user: &UserWithDetails::from_user(user.into(), db).await),
|
||||
context!(logs, loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await),
|
||||
)
|
||||
}
|
||||
|
||||
@ -176,7 +184,7 @@ async fn kiosk(
|
||||
});
|
||||
|
||||
let logtypes = LogType::all(db).await;
|
||||
let distances = Logbook::distances(db).await;
|
||||
let distances = Distance::all(db).await;
|
||||
|
||||
let on_water = Logbook::on_water(db).await;
|
||||
|
||||
@ -185,6 +193,7 @@ async fn kiosk(
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
context.insert("planned_trips", &Trip::get_for_today(db).await);
|
||||
context.insert("boats", &boats);
|
||||
context.insert(
|
||||
"reservations",
|
||||
@ -204,11 +213,12 @@ async fn create_logbook(
|
||||
db: &SqlitePool,
|
||||
data: Form<LogToAdd>,
|
||||
user: &DonauLinzUser,
|
||||
smtp_pw: &str,
|
||||
) -> Flash<Redirect> {
|
||||
match Logbook::create(
|
||||
db,
|
||||
data.into_inner(),
|
||||
user
|
||||
user, smtp_pw
|
||||
)
|
||||
.await
|
||||
{
|
||||
@ -228,6 +238,8 @@ async fn create_logbook(
|
||||
Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in der letzten Woche enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
|
||||
Err(LogbookCreateError::CantChangeHandoperatableStatusForThisBoat) => Flash::error(Redirect::to("/log"), "Handsteuer-Status dieses Boots kann nicht verändert werden."),
|
||||
Err(LogbookCreateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
|
||||
Err(LogbookCreateError::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."),
|
||||
Err(LogbookCreateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(Redirect::to("/log"), "Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!"),
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,6 +248,7 @@ async fn create(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<LogToAdd>,
|
||||
user: DonauLinzUser,
|
||||
config: &State<Config>,
|
||||
) -> Flash<Redirect> {
|
||||
Log::create(
|
||||
db,
|
||||
@ -243,7 +256,7 @@ async fn create(
|
||||
)
|
||||
.await;
|
||||
|
||||
create_logbook(db, data, &user).await
|
||||
create_logbook(db, data, &user, &config.smtp_pw).await
|
||||
}
|
||||
|
||||
#[post("/", data = "<data>")]
|
||||
@ -251,6 +264,7 @@ async fn create_kiosk(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<LogToAdd>,
|
||||
_kiosk: KioskCookie,
|
||||
config: &State<Config>,
|
||||
) -> Flash<Redirect> {
|
||||
let Some(boat) = Boat::find_by_id(db, data.boat_id).await else {
|
||||
return Flash::error(Redirect::to("/log"), "Boot gibt's nicht");
|
||||
@ -279,14 +293,57 @@ async fn create_kiosk(
|
||||
)
|
||||
.await;
|
||||
|
||||
create_logbook(db, data, &DonauLinzUser(creator)).await //TODO: fixme
|
||||
create_logbook(
|
||||
db,
|
||||
data,
|
||||
&DonauLinzUser::new(db, creator).await.unwrap(),
|
||||
&config.smtp_pw,
|
||||
)
|
||||
.await
|
||||
//TODO: fixme
|
||||
}
|
||||
|
||||
#[post("/update", data = "<data>")]
|
||||
async fn update(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<LogToUpdate>,
|
||||
user: VorstandUser,
|
||||
) -> Flash<Redirect> {
|
||||
let data = data.into_inner();
|
||||
|
||||
let Some(logbook) = Logbook::find_by_id(db, data.id).await else {
|
||||
return Flash::error(Redirect::to("/log"), &format!("Logbucheintrag kann nicht bearbeitet werden, da es einen Logbuch-Eintrag mit ID={} nicht gibt", data.id));
|
||||
};
|
||||
|
||||
match logbook.update(db, data.clone(), &user.user).await {
|
||||
Ok(()) => {
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"User {} updated log entry={:?} to {:?}",
|
||||
&user.name, logbook, data
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::success(
|
||||
Redirect::to("/log/show"),
|
||||
"Logbucheintrag erfolgreich bearbeitet".to_string(),
|
||||
)
|
||||
}
|
||||
Err(LogbookAdminUpdateError::NotAllowed) => Flash::error(
|
||||
Redirect::to("/log/show"),
|
||||
"Du hast keine Erlaubnis, diesen Logbucheintrag zu bearbeiten!".to_string(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
async fn home_logbook(
|
||||
db: &SqlitePool,
|
||||
data: Form<LogToFinalize>,
|
||||
logbook_id: i32,
|
||||
logbook_id: i64,
|
||||
user: &DonauLinzUser,
|
||||
smtp_pw: &str,
|
||||
) -> Flash<Redirect> {
|
||||
let logbook: Option<Logbook> = Logbook::find_by_id(db, logbook_id).await;
|
||||
let Some(logbook) = logbook else {
|
||||
@ -296,11 +353,14 @@ async fn home_logbook(
|
||||
);
|
||||
};
|
||||
|
||||
match logbook.home(db,user, data.into_inner()).await {
|
||||
match logbook.home(db,user, data.into_inner(), smtp_pw).await {
|
||||
Ok(_) => Flash::success(Redirect::to("/log"), "Ausfahrt korrekt eingetragen"),
|
||||
Err(LogbookUpdateError::TooManyRowers(expected, actual)) => Flash::error(Redirect::to("/log"), format!("Zu viele Ruderer (Boot fasst maximal {expected}, es wurden jedoch {actual} Ruderer ausgewählt)")),
|
||||
Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die heute enden dürfen eingetragen werden. Für einen Nachtrag schreibe alle Daten Philipp (Tel. nr. siehe Signal oder it@rudernlinz.at)."),
|
||||
Err(LogbookUpdateError::TooFast(km, min)) => Flash::error(Redirect::to("/log"), format!("KM zu groß für die eingegebene Dauer ({km} km in {min} Minuten). Bitte überprüfe deine Start- und Endzeit und versuche es erneut.")),
|
||||
Err(LogbookUpdateError::AlreadyFinalized) => Flash::error(Redirect::to("/log"), "Logbucheintrag wurde bereits abgeschlossen."),
|
||||
Err(LogbookUpdateError::ExternalSteeringPersonMustSteerOrShipmaster) => Flash::error(Redirect::to("/log"), "Wenn du eine 'Externe Steuerperson' hinzufügst, muss diese steuern oder Schiffsführer sein!"),
|
||||
Err(LogbookUpdateError::BoatAlreadyOnWater) => Flash::error(Redirect::to("/log"), "Das Boot war in diesem Zeitraum schon am Wasser. Bitte überprüfe das Datum und die Zeit."),
|
||||
Err(e) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
format!("Eintrag {logbook_id} konnte nicht abgesendet werden (Fehler: {e:?})!"),
|
||||
@ -312,8 +372,9 @@ async fn home_logbook(
|
||||
async fn home_kiosk(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<LogToFinalize>,
|
||||
logbook_id: i32,
|
||||
logbook_id: i64,
|
||||
_kiosk: KioskCookie,
|
||||
config: &State<Config>,
|
||||
) -> Flash<Redirect> {
|
||||
let logbook = Logbook::find_by_id(db, logbook_id).await.unwrap(); //TODO: fixme
|
||||
|
||||
@ -327,11 +388,15 @@ async fn home_kiosk(
|
||||
db,
|
||||
data,
|
||||
logbook_id,
|
||||
&DonauLinzUser(
|
||||
&DonauLinzUser::new(
|
||||
db,
|
||||
User::find_by_id(db, logbook.shipmaster as i32)
|
||||
.await
|
||||
.unwrap(),
|
||||
), //TODO: fixme
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
&config.smtp_pw,
|
||||
)
|
||||
.await
|
||||
}
|
||||
@ -340,8 +405,9 @@ async fn home_kiosk(
|
||||
async fn home(
|
||||
db: &State<SqlitePool>,
|
||||
data: Form<LogToFinalize>,
|
||||
logbook_id: i32,
|
||||
logbook_id: i64,
|
||||
user: DonauLinzUser,
|
||||
config: &State<Config>,
|
||||
) -> Flash<Redirect> {
|
||||
Log::create(
|
||||
db,
|
||||
@ -352,13 +418,18 @@ async fn home(
|
||||
)
|
||||
.await;
|
||||
|
||||
home_logbook(db, data, logbook_id, &user).await
|
||||
home_logbook(db, data, logbook_id, &user, &config.smtp_pw).await
|
||||
}
|
||||
|
||||
#[get("/<logbook_id>/delete", rank = 2)]
|
||||
async fn delete(db: &State<SqlitePool>, logbook_id: i32, user: DonauLinzUser) -> Flash<Redirect> {
|
||||
async fn delete(db: &State<SqlitePool>, logbook_id: i64, user: DonauLinzUser) -> Flash<Redirect> {
|
||||
let logbook = Logbook::find_by_id(db, logbook_id).await;
|
||||
if let Some(logbook) = logbook {
|
||||
let redirect = if logbook.arrival.is_some() {
|
||||
"/log/show"
|
||||
} else {
|
||||
"/log"
|
||||
};
|
||||
Log::create(
|
||||
db,
|
||||
format!("User {} tries to delete log entry {logbook_id}", &user.name),
|
||||
@ -366,11 +437,11 @@ async fn delete(db: &State<SqlitePool>, logbook_id: i32, user: DonauLinzUser) ->
|
||||
.await;
|
||||
match logbook.delete(db, &user).await {
|
||||
Ok(_) => Flash::success(
|
||||
Redirect::to("/log"),
|
||||
format!("Eintrag {} gelöscht!", logbook_id),
|
||||
Redirect::to(redirect),
|
||||
format!("Eintrag {} von {} gelöscht!", logbook_id, user.name),
|
||||
),
|
||||
Err(LogbookDeleteError::NotYourEntry) => Flash::error(
|
||||
Redirect::to("/log"),
|
||||
Redirect::to(redirect),
|
||||
"Du hast nicht die Berechtigung, den Eintrag zu löschen!",
|
||||
),
|
||||
}
|
||||
@ -385,7 +456,7 @@ async fn delete(db: &State<SqlitePool>, logbook_id: i32, user: DonauLinzUser) ->
|
||||
#[get("/<logbook_id>/delete")]
|
||||
async fn delete_kiosk(
|
||||
db: &State<SqlitePool>,
|
||||
logbook_id: i32,
|
||||
logbook_id: i64,
|
||||
_kiosk: KioskCookie,
|
||||
) -> Flash<Redirect> {
|
||||
let logbook = Logbook::find_by_id(db, logbook_id).await;
|
||||
@ -425,7 +496,8 @@ pub fn routes() -> Vec<Route> {
|
||||
show_kiosk,
|
||||
show_for_year,
|
||||
delete,
|
||||
delete_kiosk
|
||||
delete_kiosk,
|
||||
update
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
use rocket::{get, http::ContentType, routes, Route, State};
|
||||
use sqlx::SqlitePool;
|
||||
|
||||
use crate::model::event::Event;
|
||||
use crate::model::{event::Event, personal::cal::get_personal_cal, user::User};
|
||||
|
||||
#[get("/cal")]
|
||||
async fn cal(db: &State<SqlitePool>) -> (ContentType, String) {
|
||||
@ -9,8 +9,25 @@ async fn cal(db: &State<SqlitePool>) -> (ContentType, String) {
|
||||
(ContentType::Calendar, Event::get_ics_feed(db).await)
|
||||
}
|
||||
|
||||
#[get("/cal/personal/<user_id>/<uuid>")]
|
||||
async fn cal_registered(
|
||||
db: &State<SqlitePool>,
|
||||
user_id: i32,
|
||||
uuid: &str,
|
||||
) -> Result<(ContentType, String), String> {
|
||||
let Some(user) = User::find_by_id(db, user_id).await else {
|
||||
return Err("Invalid".into());
|
||||
};
|
||||
|
||||
if &user.user_token != uuid {
|
||||
return Err("Invalid".into());
|
||||
}
|
||||
|
||||
Ok((ContentType::Calendar, get_personal_cal(db, &user).await))
|
||||
}
|
||||
|
||||
pub fn routes() -> Vec<Route> {
|
||||
routes![cal]
|
||||
routes![cal, cal_registered]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -20,11 +20,15 @@ use serde::Deserialize;
|
||||
use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::model::{
|
||||
logbook::Logbook,
|
||||
notification::Notification,
|
||||
role::Role,
|
||||
user::{User, UserWithDetails, SCHECKBUCH},
|
||||
use crate::{
|
||||
model::{
|
||||
logbook::Logbook,
|
||||
notification::Notification,
|
||||
personal::Achievements,
|
||||
role::Role,
|
||||
user::{User, UserWithDetails},
|
||||
},
|
||||
SCHECKBUCH,
|
||||
};
|
||||
|
||||
pub(crate) mod admin;
|
||||
@ -59,6 +63,7 @@ async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_
|
||||
context.insert("last_trips", &last_trips);
|
||||
}
|
||||
|
||||
context.insert("achievements", &Achievements::for_user(db, &user).await);
|
||||
context.insert("notifications", &Notification::for_user(db, &user).await);
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
context.insert("costs_scheckbuch", &SCHECKBUCH);
|
||||
@ -118,6 +123,60 @@ fn unauthorized_error(req: &Request) -> Redirect {
|
||||
Redirect::to("/auth")
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
struct NewBlogpostForm<'r> {
|
||||
article_url: &'r str,
|
||||
article_title: &'r str,
|
||||
pw: &'r str,
|
||||
}
|
||||
|
||||
#[post("/", data = "<blogpost>")]
|
||||
async fn new_blogpost(
|
||||
db: &State<SqlitePool>,
|
||||
blogpost: Form<NewBlogpostForm<'_>>,
|
||||
config: &State<Config>,
|
||||
) -> String {
|
||||
if blogpost.pw == config.wordpress_key {
|
||||
let member = Role::find_by_name(db, "Donau Linz").await.unwrap();
|
||||
Notification::create_for_role(
|
||||
db,
|
||||
&member,
|
||||
&urlencoding::decode(blogpost.article_title).expect("UTF-8"),
|
||||
"Neuer Blogpost",
|
||||
Some(blogpost.article_url),
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
"ACK".into()
|
||||
} else {
|
||||
"WRONG pw".into()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromForm, Debug)]
|
||||
struct BlogpostUnpublishedForm<'r> {
|
||||
article_url: &'r str,
|
||||
pw: &'r str,
|
||||
}
|
||||
|
||||
#[post("/", data = "<blogpost>")]
|
||||
async fn blogpost_unpublished(
|
||||
db: &State<SqlitePool>,
|
||||
blogpost: Form<BlogpostUnpublishedForm<'_>>,
|
||||
config: &State<Config>,
|
||||
) -> String {
|
||||
if blogpost.pw == config.wordpress_key {
|
||||
Notification::delete_by_link(
|
||||
db,
|
||||
&urlencoding::decode(blogpost.article_url).expect("UTF-8"),
|
||||
)
|
||||
.await;
|
||||
"ACK".into()
|
||||
} else {
|
||||
"WRONG pw".into()
|
||||
}
|
||||
}
|
||||
|
||||
#[catch(403)] //forbidden
|
||||
fn forbidden_error() -> Flash<Redirect> {
|
||||
Flash::error(Redirect::to("/"), "Keine Berechtigung für diese Aktion. Wenn du der Meinung bist, dass du das machen darfst, melde dich bitte bei it@rudernlinz.at.")
|
||||
@ -187,6 +246,7 @@ pub struct Config {
|
||||
smtp_pw: String,
|
||||
usage_log_path: String,
|
||||
pub openweathermap_key: String,
|
||||
wordpress_key: String,
|
||||
}
|
||||
|
||||
pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
@ -194,6 +254,8 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
|
||||
.mount("/", routes![index, steering, impressum])
|
||||
.mount("/auth", auth::routes())
|
||||
.mount("/wikiauth", routes![wikiauth])
|
||||
.mount("/new-blogpost", routes![new_blogpost])
|
||||
.mount("/blogpost-unpublished", routes![blogpost_unpublished])
|
||||
.mount("/log", log::routes())
|
||||
.mount("/planned", planned::routes())
|
||||
.mount("/ergo", ergo::routes())
|
||||
|
@ -8,12 +8,15 @@ use rocket_dyn_templates::Template;
|
||||
use sqlx::SqlitePool;
|
||||
use tera::Context;
|
||||
|
||||
use crate::model::{
|
||||
log::Log,
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
user::{AllowedForPlannedTripsUser, User, UserWithDetails},
|
||||
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
|
||||
use crate::{
|
||||
model::{
|
||||
log::Log,
|
||||
tripdetails::TripDetails,
|
||||
triptype::TripType,
|
||||
user::{AllowedForPlannedTripsUser, User, UserWithDetails},
|
||||
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
|
||||
},
|
||||
AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD,
|
||||
};
|
||||
|
||||
#[get("/")]
|
||||
@ -22,7 +25,7 @@ async fn index(
|
||||
user: AllowedForPlannedTripsUser,
|
||||
flash: Option<FlashMessage<'_>>,
|
||||
) -> Template {
|
||||
let user: User = user.into();
|
||||
let user: User = user.into_inner();
|
||||
|
||||
let mut context = Context::new();
|
||||
|
||||
@ -37,7 +40,15 @@ async fn index(
|
||||
context.insert("flash", &msg.into_inner());
|
||||
}
|
||||
|
||||
context.insert(
|
||||
"allowed_to_update_always_show_trip",
|
||||
&user.allowed_to_update_always_show_trip(db).await,
|
||||
);
|
||||
context.insert("fee", &user.fee(db).await);
|
||||
context.insert(
|
||||
"amount_days_to_show_trips_ahead",
|
||||
&AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD,
|
||||
);
|
||||
context.insert("loggedin_user", &UserWithDetails::from_user(user, db).await);
|
||||
context.insert("days", &days);
|
||||
Template::render("planned", context.into_json())
|
||||
@ -50,7 +61,7 @@ async fn join(
|
||||
user: AllowedForPlannedTripsUser,
|
||||
user_note: Option<String>,
|
||||
) -> Flash<Redirect> {
|
||||
let user: User = user.into();
|
||||
let user: User = user.into_inner();
|
||||
|
||||
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
|
||||
return Flash::error(Redirect::to("/"), "Trip_details do not exist.");
|
||||
@ -99,6 +110,10 @@ async fn join(
|
||||
Redirect::to("/planned"),
|
||||
"Du darfst keine Gäste hinzufügen.",
|
||||
),
|
||||
Err(UserTripError::NotVisibleToUser) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Du kannst dich nicht registrieren, weil du die Ausfahrt gar nicht sehen solltest.",
|
||||
),
|
||||
Err(UserTripError::DetailsLocked) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Die Bootseinteilung wurde bereits gemacht. Wenn du noch mitrudern möchtest, frag bitte bei einer angemeldeten Steuerperson nach, ob das noch möglich ist.",
|
||||
@ -113,7 +128,7 @@ async fn remove_guest(
|
||||
user: AllowedForPlannedTripsUser,
|
||||
name: String,
|
||||
) -> Flash<Redirect> {
|
||||
let user: User = user.into();
|
||||
let user: User = user.into_inner();
|
||||
|
||||
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
|
||||
return Flash::error(Redirect::to("/planned"), "TripDetailsId does not exist");
|
||||
@ -147,6 +162,10 @@ async fn remove_guest(
|
||||
Err(UserTripDeleteError::GuestNotParticipating) => {
|
||||
Flash::error(Redirect::to("/planned"), "Gast nicht angemeldet.")
|
||||
}
|
||||
Err(UserTripDeleteError::NotVisibleToUser) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Du kannst dich nicht abmelden, weil du die Ausfahrt gar nicht sehen solltest.",
|
||||
),
|
||||
Err(UserTripDeleteError::NotAllowedToDeleteGuest) => Flash::error(
|
||||
Redirect::to("/planned"),
|
||||
"Keine Berechtigung um den Gast zu entfernen.",
|
||||
@ -160,7 +179,7 @@ async fn remove(
|
||||
trip_details_id: i64,
|
||||
user: AllowedForPlannedTripsUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user: User = user.into();
|
||||
let user: User = user.into_inner();
|
||||
|
||||
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
|
||||
return Flash::error(Redirect::to("/planned"), "TripDetailsId does not exist");
|
||||
@ -191,6 +210,18 @@ async fn remove(
|
||||
|
||||
Flash::error(Redirect::to("/planned"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
|
||||
}
|
||||
Err(UserTripDeleteError::NotVisibleToUser) => {
|
||||
Log::create(
|
||||
db,
|
||||
format!(
|
||||
"User {} tried to unregister for not-yet-seeable trip_details.id={}",
|
||||
user.name, trip_details_id
|
||||
),
|
||||
)
|
||||
.await;
|
||||
|
||||
Flash::error(Redirect::to("/planned"), "Abmeldung nicht möglich, da du dieses Event eigentlich gar nicht sehen solltest...")
|
||||
}
|
||||
Err(_) => {
|
||||
panic!("Not possible to be here");
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ async fn index_boat(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
|
||||
|
||||
Template::render(
|
||||
"stat.boats",
|
||||
context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, kiosk),
|
||||
context!(loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await, stat, kiosk),
|
||||
)
|
||||
}
|
||||
|
||||
@ -38,7 +38,7 @@ async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -
|
||||
|
||||
Template::render(
|
||||
"stat.people",
|
||||
context!(loggedin_user: &UserWithDetails::from_user(user.into(), db).await, stat, personal, kiosk, guest_km, club_km),
|
||||
context!(loggedin_user: &UserWithDetails::from_user(user.into_inner(), db).await, stat, personal, kiosk, guest_km, club_km),
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -59,7 +59,7 @@ async fn index(
|
||||
context.insert("user", &User::all(db).await);
|
||||
context.insert(
|
||||
"loggedin_user",
|
||||
&UserWithDetails::from_user(user.into(), db).await,
|
||||
&UserWithDetails::from_user(user.into_inner(), db).await,
|
||||
);
|
||||
|
||||
Template::render("trailerreservations", context.into_json())
|
||||
@ -81,7 +81,7 @@ async fn create<'r>(
|
||||
data: Form<FormTrailerReservationToAdd<'r>>,
|
||||
user: DonauLinzUser,
|
||||
) -> Flash<Redirect> {
|
||||
let user_applicant: User = user.into();
|
||||
let user_applicant: User = user.into_inner();
|
||||
let trailer = Trailer::find_by_id(db, data.trailer_id as i32)
|
||||
.await
|
||||
.unwrap();
|
||||
|
100
templates/achievement.html.tera
Normal file
100
templates/achievement.html.tera
Normal file
@ -0,0 +1,100 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% import "includes/forms/log" as log %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<link rel="stylesheet" href="/public/table.css" />
|
||||
<div class="w-full">
|
||||
<h1 class="h1">Abzeichen für {{ rowingbadge_year }}</h1>
|
||||
<div class="text-black dark:text-white">
|
||||
<table id="basic">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Erster Log</th>
|
||||
<th>Letzter Log</th>
|
||||
<th>Gesamt-KM</th>
|
||||
<th>Äquatorpreis (ÄP)</th>
|
||||
<th>
|
||||
ÄP diese
|
||||
<br>
|
||||
Saison bekommen
|
||||
</th>
|
||||
<th>
|
||||
Fahrtenabzeichen (FA)
|
||||
<br>
|
||||
geschafft
|
||||
</th>
|
||||
<th>FA - KM</th>
|
||||
<th>FA - fehlende KM</th>
|
||||
<th>Eintagesausfahrten</th>
|
||||
<th>Mehrtagesausfahrten</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for person in people %}
|
||||
{% set user = person.0 %}
|
||||
{% set achievement = person.1 %}
|
||||
<tr>
|
||||
<td>{{ user.name }}</td>
|
||||
<td>
|
||||
{% if achievement.year_first_mentioned %}{{ achievement.year_first_mentioned }}{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if achievement.year_last_mentioned %}{{ achievement.year_last_mentioned }}{% endif %}
|
||||
</td>
|
||||
<td>{{ achievement.all_time_km }}</td>
|
||||
<td>{{ achievement.curr_equatorprice_name }}</td>
|
||||
<td>
|
||||
{% if achievement.new_equatorprice_this_season %}
|
||||
🎉
|
||||
{% else %}
|
||||
-
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if achievement.rowingbadge %}
|
||||
{% set badge = achievement.rowingbadge %}
|
||||
<td>
|
||||
{% if badge.achieved %}
|
||||
ja
|
||||
{% else %}
|
||||
nein
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ badge.rowed_km }} / {{ badge.required_km }}</td>
|
||||
<td>{{ badge.missing_km }}</td>
|
||||
<td>
|
||||
<details>
|
||||
<summary>
|
||||
> {{ badge.single_day_trips_required_distance }} km: {{ badge.single_day_trips_over_required_distance | length }} / 2
|
||||
</summary>
|
||||
{% for log in badge.single_day_trips_over_required_distance %}
|
||||
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, hide_type=true) }}
|
||||
{% endfor %}
|
||||
</details>
|
||||
</td>
|
||||
<td>
|
||||
<details>
|
||||
<summary>
|
||||
> {{ badge.multi_day_trips_required_distance }} km: {{ badge.multi_day_trips_over_required_distance | length }} / 1
|
||||
</summary>
|
||||
{% for log in badge.multi_day_trips_over_required_distance %}
|
||||
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, hide_type=true) }}
|
||||
{% endfor %}
|
||||
</details>
|
||||
</td>
|
||||
{% else %}
|
||||
<td>Geb.datum fehlt 👻</td>
|
||||
<td>Geb.datum fehlt 👻</td>
|
||||
<td>Geb.datum fehlt 👻</td>
|
||||
<td>Geb.datum fehlt 👻</td>
|
||||
<td>Geb.datum fehlt 👻</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/public/jstable.min.js"></script>
|
||||
<script src="/public/table.js"></script>
|
||||
{% endblock content %}
|
@ -8,11 +8,20 @@
|
||||
role="alert">
|
||||
<h2 class="h2">Angemeldete Personen: {{ schnupperanten | length }}</h2>
|
||||
<div class="text-sm p-3">
|
||||
<ol class="ms-2" style="list-style: number;">
|
||||
<ol class="list-decimal">
|
||||
{% for user in schnupperanten %}
|
||||
<li class="py-1"
|
||||
{% if "paid" in user.roles %}style="background-color: green;"{% endif %}>
|
||||
{{ user.name }} ({{ user.mail }} | {{ user.notes }})
|
||||
<li class="py-1 ml-4">
|
||||
<span class="flex items-center justify-between">
|
||||
<span>
|
||||
<span class="status-damage status-damage-{% if "paid" in user.roles %}none {% else %}locked {% endif %}"></span> {{ user.name }} ({{ user.mail }}
|
||||
{%- if user.notes %} | {{ user.notes }}
|
||||
{% endif -%}
|
||||
)
|
||||
</span>
|
||||
<a class="btn btn-primary"
|
||||
href="/admin/user/move/schnupperant/{{ user.id }}/to/scheckbuch"
|
||||
onclick="return confirm('Willst du wirklich ein Scheckbuch erstellen? Die Person erhält ein Mail mit allen Infos.')">Zu Scheckbuch umwandeln</a>
|
||||
</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
|
@ -1,43 +1,44 @@
|
||||
{% import "includes/macros" as macros %}
|
||||
{% extends "base" %}
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
|
||||
<h1 class="h1">Gebühren</h1>
|
||||
<!-- START filterBar -->
|
||||
<div class="max-w-screen-lg w-full">
|
||||
<h1 class="h1">
|
||||
Gebühren
|
||||
</h1>
|
||||
<div class="search-wrapper">
|
||||
<label for="name" class="sr-only">Suche</label>
|
||||
<input type="search"
|
||||
name="name"
|
||||
id="filter-js"
|
||||
class="search-bar"
|
||||
placeholder="Suchen nach Name" />
|
||||
placeholder="Suchen nach Namen..." />
|
||||
</div>
|
||||
<!-- END filterBar -->
|
||||
<div class="bg-primary-100 dark:bg-primary-950 p-3 rounded-b-md grid gap-4">
|
||||
<div id="filter-result-js"
|
||||
class="text-primary-950 dark:text-white text-right"></div>
|
||||
<div id="filter-result-js" class="search-result"></div>
|
||||
<div class="border-r border-l border-gray-200 dark:border-primary-600">
|
||||
{% for fee in fees | sort(attribute="name") %}
|
||||
<div {% if fee.paid %}style="background-color: green;"{% endif %}
|
||||
<div class="border-t border-gray-200 dark:border-primary-600 {% if fee.paid %}bg-[#15803d] text-white {% else %} bg-white {% endif %} dark:bg-primary-900 text-black dark:text-white flex justify-between items-center px-3 py-1 "
|
||||
data-filterable="true"
|
||||
data-filter="{{ fee.name }} {% if fee.paid %} has-already-paid {% else %} has-not-paid {% endif %}"
|
||||
class="bg-white dark:bg-primary-900 p-3 rounded-md w-full">
|
||||
<div class="grid sm:grid-cols-1 gap-3">
|
||||
<div style="width: 100%" class="col-span-2">
|
||||
<b>{{ fee.name }}</b>
|
||||
<div class="grid md:grid-cols-3 gap-3 w-full py-3">
|
||||
<div>
|
||||
<strong>{{ fee.name }}</strong>
|
||||
<span class="block">{{ fee.sum_in_cents / 100 }}€</span>
|
||||
</div>
|
||||
<div style="width: 100%">{{ fee.sum_in_cents / 100 }}€:</div>
|
||||
<div style="width: 100%">
|
||||
<div>
|
||||
{% for p in fee.parts %}
|
||||
{{ p.0 }} ({{ p.1 / 100 }}€)
|
||||
{% if not loop.last %}+{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
<a href="/admin/user/fees/paid?{{ fee.user_ids }}">Zahlungsstatus ändern</a>
|
||||
{% if "admin" in loggedin_user.roles or "kassier" in loggedin_user.roles %}
|
||||
<div class="text-end"><a href="/admin/user/fees/paid?{{ fee.user_ids }}" class="btn btn-primary">Zahlungsstatus ändern</a></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{% endblock content %}
|
||||
|
@ -36,37 +36,53 @@
|
||||
placeholder="Suchen nach (Name, [yes|no]-role:<name>, has-[no-]membership-pdf)" />
|
||||
</div>
|
||||
<!-- END filterBar -->
|
||||
<div class="bg-primary-100 dark:bg-primary-950 p-3 rounded-b-md grid gap-4">
|
||||
<div id="filter-result-js"
|
||||
class="text-primary-950 dark:text-white text-right"></div>
|
||||
{% for user in users %}
|
||||
<div data-filterable="true"
|
||||
data-filter="{{ user.name }} {% for role in roles %} {% if role.name in user.roles %} yes-role:{{ role.name }} {% else %} no-role:{{ role.name }} {% endif %} role-{{ role }} {% endfor %} {% if user.membership_pdf %}has-membership-pdf{% else %}has-no-membership-pdf{% endif %} ">
|
||||
<div id="filter-result-js" class="search-result"></div>
|
||||
{% for user in users %}
|
||||
<div data-filterable="true"
|
||||
data-filter="{{ user.name }} {% for role in roles %} {% if role.name in user.roles %} yes-role:{{ role.name }} {% else %} no-role:{{ role.name }} {% endif %} role-{{ role }} {% endfor %} {% if user.membership_pdf %}has-membership-pdf{% else %}has-no-membership-pdf{% endif %}"
|
||||
class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative">
|
||||
<details class="block dark:text-white w-full">
|
||||
<summary>
|
||||
<span class="text-black dark:text-white cursor-pointer">
|
||||
<span class="font-bold">
|
||||
{{ user.name }}
|
||||
{% if not user.last_access and "admin" in loggedin_user.roles and user.mail %}
|
||||
<form action="/admin/user"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="inline">
|
||||
• <a class="font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
|
||||
href="/admin/user/{{ user.id }}/send-welcome-mail"
|
||||
onclick="return confirm('Willst du wirklich das Willkommensmail an {{ user.name }} ausschicken?');">Willkommensmail verschicken</a>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if user.last_access %}• ⏳ {{ user.last_access | date }}{% endif %}
|
||||
</span>
|
||||
<small class="block text-gray-600 dark:text-gray-100">
|
||||
{% for role in user.roles -%}
|
||||
{{ role }}
|
||||
{%- if not loop.last %},
|
||||
{% endif -%}
|
||||
{% endfor %}
|
||||
</small>
|
||||
</span>
|
||||
</summary>
|
||||
<form action="/admin/user"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
class="bg-white dark:bg-primary-900 p-3 rounded-md w-full">
|
||||
<div class="w-full grid gap-3">
|
||||
class="w-full mt-2">
|
||||
{% if user.pw %}
|
||||
<a class="block my-1 font-normal text-[#f43f5e] dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
|
||||
href="/admin/user/{{ user.id }}/reset-pw"
|
||||
onclick="return confirm('Willst du wirklich das Passwort zurücksetzen?');">Passwort zurücksetzen</a>
|
||||
{% endif %}
|
||||
<div class="w-full grid gap-3 mt-3">
|
||||
<input type="hidden" name="id" value="{{ user.id }}" />
|
||||
<div class="font-bold mb-1 text-black dark:text-white">
|
||||
{{ user.name }}
|
||||
{% if user.last_access %}
|
||||
(last access:
|
||||
{{ user.last_access | date }})
|
||||
{% endif %}
|
||||
{% if user.pw %}
|
||||
<a class="block mt-1 font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
|
||||
href="/admin/user/{{ user.id }}/reset-pw">Passwort zurücksetzen</a>
|
||||
{% endif %}
|
||||
{% if not user.last_access and "admin" in loggedin_user.roles %}
|
||||
<a class="block mt-1 font-normal text-primary-600 dark:text-primary-200 hover:text-primary-900 dark:hover:text-primary-300 underline"
|
||||
href="/admin/user/{{ user.id }}/send-welcome-mail">Willkommensmail verschicken</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
{% for role in roles %}
|
||||
{{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.roles, disabled=allowed_to_edit == false) }}
|
||||
{% endfor %}
|
||||
<hr class="sm:col-span-2 lg:col-span-4 my-3" />
|
||||
{% if user.membership_pdf %}
|
||||
<a href="/admin/user/{{ user.id }}/membership"
|
||||
class="text-black dark:text-white">Beitrittserklärung herunterladen</a>
|
||||
@ -100,8 +116,8 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
@ -4,6 +4,34 @@
|
||||
{% block content %}
|
||||
<div class="max-w-screen-lg w-full bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5">
|
||||
<h1 class="h1">Scheckbücher</h1>
|
||||
<form action="/admin/user/new/scheckbuch"
|
||||
method="post"
|
||||
class="mt-4 bg-primary-900 rounded-md text-white px-3 pb-3 pt-2 sm:flex items-end justify-between">
|
||||
<div class="w-full">
|
||||
<h2 class="text-md font-bold mb-2 uppercase tracking-wide">Neues Scheckbuch hinzufügen</h2>
|
||||
<div class="grid md:grid-cols-3">
|
||||
<div>
|
||||
<label for="name" class="sr-only">Name</label>
|
||||
<input type="text"
|
||||
name="name"
|
||||
class="relative block rounded-md border-0 py-1.5 px-2 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:text-sm sm:leading-6 mb-2 md:mb-0"
|
||||
placeholder="Name" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="name" class="sr-only">Mail</label>
|
||||
<input type="mail"
|
||||
name="mail"
|
||||
class="relative block rounded-md border-0 py-1.5 px-2 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-primary-600 sm:text-sm sm:leading-6 mb-2 md:mb-0"
|
||||
placeholder="Mail" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<input value="Hinzufügen"
|
||||
type="submit"
|
||||
class="w-28 mt-2 sm:mt-0 rounded-md bg-primary-500 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer" />
|
||||
</div>
|
||||
</form>
|
||||
<!-- START filterBar -->
|
||||
<div class="search-wrapper">
|
||||
<label for="name" class="sr-only">Suche</label>
|
||||
@ -31,7 +59,7 @@
|
||||
<li>{{ log::show_old(log=trip, state="completed", only_ones=false, index=loop.index) }}</li>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if "admin" in loggedin_user.roles %}
|
||||
{% if "admin" in loggedin_user.roles or "kassier" in loggedin_user.roles %}
|
||||
<a href="/admin/user/fees/paid?user_ids[]={{ user.id }}">Zahlungsstatus ändern</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -10,7 +10,7 @@
|
||||
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', required=true, min='0') }}
|
||||
{{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='tripdetails.max_people', type='number', required=true, min='0') }}
|
||||
{{ macros::checkbox(label='Scheckbuch-Anmeldungen erlauben', name='tripdetails.allow_guests') }}
|
||||
{{ macros::checkbox(label='Immer anzeigen', name='tripdetails.always_show') }}
|
||||
{{ macros::checkbox(label='Immer anzeigen', name='always_show') }}
|
||||
{{ macros::input(label='Anmerkungen', name='tripdetails.notes', type='input') }}
|
||||
{{ macros::select(label='Typ', data=trip_types, name='tripdetails.trip_type', default='Reguläre Ausfahrt') }}
|
||||
<input value="Erstellen" class="w-full btn btn-primary" type="submit" />
|
||||
|
@ -5,7 +5,6 @@
|
||||
{{ macros::input(label='Startzeit (zB "10:00")', name='planned_starting_time', type='time', required=true) }}
|
||||
{{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='max_people', type='number', required=true, min='0') }}
|
||||
{{ macros::checkbox(label='Scheckbuch-Anmeldungen erlauben', name='allow_guests') }}
|
||||
{{ macros::checkbox(label='Immer anzeigen', name='always_show') }}
|
||||
{{ macros::input(label='Anmerkungen', name='notes', type='input') }}
|
||||
{{ macros::select(label='Typ', data=trip_types, name='trip_type', default='Reguläre Ausfahrt') }}
|
||||
<input value="Erstellen" class="w-full btn btn-primary" type="submit" />
|
||||
|
@ -29,7 +29,7 @@
|
||||
Folgende Daten werden verarbeitet:
|
||||
<ul>
|
||||
<li>
|
||||
Server-Log Files: IP-Adresse, Adresse der besuchten Seite, Browseragent, Datum und Uhrzeit. Wir nutzen diese Daten nicht und geben Sie in der Regel nicht weiter, können jedoch nicht ausschließen, dass diese Daten beim Vorliegen von rechtswidrigem Verhalten eingesehen werden.
|
||||
Server-Log Files: IP-Adresse, Adresse der besuchten Seite, Browseragent, Datum und Uhrzeit. Wir nutzen diese Daten nicht und geben sie in der Regel nicht weiter, können jedoch nicht ausschließen, dass diese Daten beim Vorliegen von rechtswidrigem Verhalten eingesehen werden.
|
||||
</li>
|
||||
<li>
|
||||
Cookie: Diese Website verwendet nur einen Cookie (loggedin_user), der verschlüsselte Informationen über den Login-Status speichert. Weitere Cookies werden nicht verwendet.
|
||||
|
@ -5,7 +5,7 @@
|
||||
{% macro show_boats() %}
|
||||
{% for cat, grouped_boats in boats | group_by(attribute="cat") %}
|
||||
<details>
|
||||
<summary class="font-bold cursor-pointer text-primary-900 dark:text-white border-t p-3 hover:bg-gray-100 dark:hover:bg-primary-950">
|
||||
<summary class="font-bold cursor-pointer text-primary-900 dark:text-white {% if not loop.first %}border-t{% endif %} p-3 hover:bg-gray-100 dark:hover:bg-primary-950">
|
||||
<span>{{ cat }}</span>
|
||||
<small class="text-gray-500 dark:text-gray-100">({{ grouped_boats | length }})</small>
|
||||
</summary>
|
||||
@ -36,7 +36,7 @@
|
||||
<div class="col-span-4 md:col-span-1">
|
||||
<div class="text-sm text-gray-600 dark:text-gray-100">Bootssteuerung</div>
|
||||
<div class="h-10 flex items-center">
|
||||
{{ macros::checkbox(label='handgesteuert', name='shipmaster_only_steering', disabled=true) }}
|
||||
{{ macros::checkbox(label='handgesteuert', name='shipmaster_only_steering', readonly=true) }}
|
||||
</div>
|
||||
</div>
|
||||
{{ log::rower_select(id="newrower", selected=[], class="col-span-4", init=true) }}
|
||||
@ -53,9 +53,13 @@
|
||||
id="destination"
|
||||
name="destination"
|
||||
value=""
|
||||
data-relation="distance_in_km" />
|
||||
data-relation="distance_in_km"
|
||||
autocomplete="off" />
|
||||
<datalist id="destinations">
|
||||
{% for distance in distances %}<option value="{{ distance.0 }}" distance="{{ distance.1 }}" />{% endfor %}
|
||||
{% for distance in distances %}
|
||||
<option value="{{ distance.destination }}"
|
||||
distance="{{ distance.distance_in_km }}" />
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
</div>
|
||||
<div class="relative col-span-2">
|
||||
@ -169,83 +173,114 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endmacro show %}
|
||||
{% macro show_old(log, state, allowed_to_close=false, index) %}
|
||||
{% macro show_old(log, state, allowed_to_close=false, allowed_to_edit=false, index, hide_type=false) %}
|
||||
<div class="border-t bg-white dark:bg-primary-900 py-3 px-4 relative"
|
||||
data-filterable="true"
|
||||
data-filter="{{ log.boat.name }} {% for rower in log.rowers %}{{ rower.name }}{% endfor %}">
|
||||
{% if log.logtype %}
|
||||
<div class="absolute top-0 right-0 bg-primary-100 rounded-bl-md text-primary-950 text-xs w-32 px-2 py-1 text-center font-bold">
|
||||
{% if log.logtype == 1 %}
|
||||
Wanderfahrt
|
||||
{% else %}
|
||||
{% if log.logtype == 2 %}
|
||||
Regatta
|
||||
{% else %}
|
||||
{{ log.logtype }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div {% if log.logtype %}class="mt-4 sm:mt-0"{% endif %}>
|
||||
<strong class="text-black dark:text-white">{{ log.boat.name }}</strong>
|
||||
<small class="text-gray-600 dark:text-gray-100">({{ log.shipmaster_user.name -}}
|
||||
{% if log.shipmaster_only_steering %}
|
||||
- handgesteuert
|
||||
{%- endif -%}
|
||||
)</small>
|
||||
<small class="block text-gray-600 dark:text-gray-100">
|
||||
{% if state == "completed" and log.departure | date(format='%d.%m.%Y') == log.arrival | date(format='%d.%m.%Y') %}
|
||||
{{ log.departure | date(format='%d.%m.%Y') }}
|
||||
({{ log.departure | date(format='%H:%M') }}
|
||||
-
|
||||
{{ log.arrival | date(format='%H:%M') }})
|
||||
{% else %}
|
||||
{{ log.departure | date(format='%d.%m.%Y (%H:%M)') }}
|
||||
{% if state == "completed" %}
|
||||
-
|
||||
{{ log.arrival | date(format='%d.%m.%Y (%H:%M)') }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</small>
|
||||
{% set amount_rowers = log.rowers | length %}
|
||||
{% set amount_guests = log.boat.amount_seats - amount_rowers %}
|
||||
{% if allowed_to_close and state == "on_water" %}
|
||||
{{ log::home(log=log) }}
|
||||
{% else %}
|
||||
<div class="text-black dark:text-white">
|
||||
{{ log.destination }}
|
||||
{% if state == "completed" %}
|
||||
<small class="text-gray-600 dark:text-gray-100">({{ log.distance_in_km }}
|
||||
km)</small>
|
||||
{% endif %}
|
||||
{% if log.comments %}<span class="text-sm italic">- "{{ log.comments }}"</span>{% endif %}
|
||||
</div>
|
||||
{% if amount_guests > 0 or log.rowers | length > 0 %}
|
||||
{% if not log.boat.amount_seats == 1 %}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-100">
|
||||
Ruderer:
|
||||
{% for rower in log.rowers -%}
|
||||
{{ rower.name }}
|
||||
{%- if rower.id == log.steering_user.id and rower.id != log.shipmaster_user.id %}
|
||||
(Steuerperson){%- endif -%}
|
||||
{%- if not loop.last or amount_guests > 0 and not log.boat.external %},{% endif %}
|
||||
{% endfor -%}
|
||||
{% if amount_guests > 0 and not log.boat.external %}
|
||||
Gäste
|
||||
<small class="text-gray-600 dark:text-gray-100">(ohne Account)</small>:
|
||||
{{ amount_guests }}
|
||||
{% endif %}
|
||||
</div>
|
||||
<details>
|
||||
<summary style="list-style: none;">
|
||||
{% if log.logtype and not hide_type %}
|
||||
<div class="absolute top-0 right-0 bg-primary-100 rounded-bl-md text-primary-950 text-xs w-32 px-2 py-1 text-center font-bold">
|
||||
{% if log.logtype == 1 %}
|
||||
Wanderfahrt
|
||||
{% else %}
|
||||
{% if log.logtype == 2 %}
|
||||
Regatta
|
||||
{% else %}
|
||||
{{ log.logtype }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div {% if log.logtype %}class="mt-4 sm:mt-0"{% endif %}>
|
||||
<strong class="text-black dark:text-white">{{ log.boat.name }}</strong>
|
||||
<small class="text-gray-600 dark:text-gray-100">({{ log.shipmaster_user.name -}}
|
||||
{% if log.shipmaster_only_steering %}
|
||||
- handgesteuert
|
||||
{%- endif -%}
|
||||
)</small>
|
||||
<small class="block text-gray-600 dark:text-gray-100">
|
||||
{% if state == "completed" and log.departure | date(format='%d.%m.%Y') == log.arrival | date(format='%d.%m.%Y') %}
|
||||
{{ log.departure | date(format='%d.%m.%Y') }}
|
||||
({{ log.departure | date(format='%H:%M') }}
|
||||
-
|
||||
{{ log.arrival | date(format='%H:%M') }})
|
||||
{% else %}
|
||||
{{ log.departure | date(format='%d.%m.%Y (%H:%M)') }}
|
||||
{% if state == "completed" %}
|
||||
-
|
||||
{{ log.arrival | date(format='%d.%m.%Y (%H:%M)') }}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</small>
|
||||
{% set amount_rowers = log.rowers | length %}
|
||||
{% set amount_guests = log.boat.amount_seats - amount_rowers %}
|
||||
{% if allowed_to_close and state == "on_water" %}
|
||||
{{ log::home(log=log) }}
|
||||
{% else %}
|
||||
<div class="text-black dark:text-white">
|
||||
{{ log.destination }}
|
||||
{% if state == "completed" %}
|
||||
<small class="text-gray-600 dark:text-gray-100">({{ log.distance_in_km }}
|
||||
km)</small>
|
||||
{% endif %}
|
||||
{% if log.comments %}<span class="text-sm italic">- "{{ log.comments }}"</span>{% endif %}
|
||||
</div>
|
||||
{% if amount_guests > 0 or log.rowers | length > 0 %}
|
||||
{% if not log.boat.amount_seats == 1 %}
|
||||
<div class="text-sm text-gray-600 dark:text-gray-100">
|
||||
Ruderer:
|
||||
{% for rower in log.rowers -%}
|
||||
{{ rower.name }}
|
||||
{%- if rower.id == log.steering_user.id and rower.id != log.shipmaster_user.id %}
|
||||
(Steuerperson){%- endif -%}
|
||||
{%- if not loop.last or amount_guests > 0 and not log.boat.external %},{% endif %}
|
||||
{% endfor -%}
|
||||
{% if amount_guests > 0 and not log.boat.external %}
|
||||
Gäste
|
||||
<small class="text-gray-600 dark:text-gray-100">(ohne Account)</small>:
|
||||
{{ amount_guests }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</summary>
|
||||
{% if allowed_to_edit %}
|
||||
<form action="/log/update" method="post">
|
||||
<input type="hidden" name="id" value="{{ log.id }}" />
|
||||
<input type="hidden" name="boat_id" value="{{ log.boat_id }}" />
|
||||
<input type="hidden" name="shipmaster" value="{{ log.shipmaster }}" />
|
||||
<input type="hidden"
|
||||
name="steering_person"
|
||||
value="{{ log.steering_person }}" />
|
||||
<input type="hidden"
|
||||
name="shipmaster_only_steering"
|
||||
value="{{ log.shipmaster_only_steering }}" />
|
||||
<input type="datetime-local" name="departure" value="{{ log.departure }}" />
|
||||
<input type="datetime-local" name="arrival" value="{{ log.arrival }}" />
|
||||
<input type="hidden" name="destination" value="{{ log.destination }}" />
|
||||
<input type="hidden" name="distance_in_km" value="{{ log.distance_in_km }}" />
|
||||
<input type="hidden" name="comments" value="{{ log.comments }}" />
|
||||
<input type="hidden" name="logtype" value="{{ log.logtype }}" />
|
||||
<input type="submit" value="Updaten" />
|
||||
</form>
|
||||
<a href="/log/{{ log.id }}/delete"
|
||||
class="w-28 btn btn-alert"
|
||||
onclick="return confirm('Willst du diesen Logbucheintrag wirklich löschen?');">
|
||||
{% include "includes/delete-icon" %}
|
||||
Löschen
|
||||
</a>
|
||||
{% endif %}
|
||||
</details>
|
||||
</div>
|
||||
{% endmacro show_old %}
|
||||
{% macro home(log) %}
|
||||
<form class="grid grid-cols-1 gap-3"
|
||||
action="/log/{{ log.id }}"
|
||||
method="post">
|
||||
method="post"
|
||||
onsubmit="var distance = parseFloat(document.getElementById('distance_in_km{{ log.id }}js').value); var logtype = document.getElementById('logtype{{ log.id }}js').value; if (distance > 50 && (!logtype || logtype === 'Normal')) { return confirm('Die eingegebene Distanz beträgt mehr als 50 km und es wurde kein Typ (Wanderfahrt, Regatta, ...) ausgewählt. Wenn es eine Wanderfahrt war, stell dies bitte unter \'Details ändern\' ein. Möchtest du das NICHT tun und den Eintrag OHNE Typ speichern?'); } return true;">
|
||||
{{ macros::input(label='Ankunftszeit', name='arrival', type='datetime-local', required=true, class="change-id-js rounded-md current-date-time") }}
|
||||
<div>
|
||||
<label for="destination" class="text-sm text-gray-600 dark:text-gray-100">Ziel</label>
|
||||
|
@ -1,10 +1,50 @@
|
||||
{% macro plannedtrips() %}
|
||||
{% if planned_trips %}
|
||||
<script>
|
||||
function setChoiceByLabel(choicesInstance, label) {
|
||||
const choices = choicesInstance.config.choices;
|
||||
const normalizedLabel = label.trim().replace(/\n/g, '');
|
||||
|
||||
const choice = choices.find(c => {
|
||||
const choiceLabel = c.label.trim().replace(/\n/g, '');
|
||||
return choiceLabel === normalizedLabel;
|
||||
});
|
||||
|
||||
if (choice) {
|
||||
choicesInstance.setChoiceByValue(choice.value);
|
||||
} else {
|
||||
console.warn(`Choice with label "${normalizedLabel}" not found`);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md shadow pb-2 mt-3">
|
||||
<h2 class="h2">Heute geplante Ausfahrten</h2>
|
||||
<div class="grid grid-cols-1 gap-3 w-full">
|
||||
{% for planned_trip in planned_trips | sort(attribute='planned_starting_time') %}
|
||||
<div class="pt-2 px-3 {% if not loop.first %}border-t{% endif %} text-primary-900 dark:text-white flex justify-between items-center">
|
||||
<strong class="block">
|
||||
{% set amount_members = planned_trip.rower | length + 1 %}
|
||||
{{ planned_trip.cox_name }} ({{ amount_members }} Person{{ amount_members | pluralize(singular="", plural="en") }})
|
||||
<small class="block">{{ planned_trip.planned_starting_time }}</small>
|
||||
</strong>
|
||||
<button class="btn btn-primary ml-3"
|
||||
onclick="choiceObjects['newrower'].removeActiveItems(-2);choiceObjects['newrower'].setChoiceByValue('{{ planned_trip.cox_id }}'); {% for rower in planned_trip.rower %}setChoiceByLabel(choiceObjects['newrower'], '{{ rower.name }}');{% endfor %}window.scrollTo(0,0); ">
|
||||
👥
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endmacro plannedtrips %}
|
||||
{% macro boatreservation() %}
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md shadow pb-2 mt-3">
|
||||
<h2 class="h2">Reservierungen ({{ reservations | length }})</h2>
|
||||
<div class="grid grid-cols-1 gap-3 mb-3 w-full">
|
||||
{% for _, reservations_for_event in reservations %}
|
||||
{% set reservation = reservations_for_event[0] %}
|
||||
<div class="pt-2 px-3 border-t text-primary-900 dark:text-white">
|
||||
<div class="pt-2 px-3 {% if not loop.first %}border-t{% endif %} text-primary-900 dark:text-white">
|
||||
<strong class="block">
|
||||
{{ reservation.start_date | date(format="%d.%m.%Y") }}
|
||||
{% if reservation.end_date != reservation.start_date %}
|
||||
@ -137,7 +177,7 @@
|
||||
{% if readonly %}readonly{% endif %}>
|
||||
</div>
|
||||
{% endmacro input %}
|
||||
{% macro checkbox(label, name, id='', checked=false, class='', disabled=false) %}
|
||||
{% macro checkbox(label, name, id='', checked=false, class='', disabled=false, readonly=false) %}
|
||||
<label for="{{ name }}{{ id }}"
|
||||
class="flex items-center cursor-pointer text-black dark:text-white hover:text-gray-900 dark:hover:text-gray-100 {{ class }}">
|
||||
<input type="checkbox"
|
||||
@ -145,6 +185,7 @@
|
||||
name="{{ name }}"
|
||||
{% if checked %}checked{% endif %}
|
||||
{% if disabled %}disabled{% endif %}
|
||||
{% if readonly %}readonly="readonly"{% endif %}
|
||||
class="h-4 w-4 accent-primary-600 dark:accent-primary-200 mr-2" />
|
||||
{{ label }}
|
||||
</label>
|
||||
|
@ -33,6 +33,11 @@
|
||||
<div class="mt-1">{{ notification.message | safe }}</div>
|
||||
</div>
|
||||
<div>
|
||||
{% if notification.link %}
|
||||
<a href="{{ notification.link }}" class="inline-block">
|
||||
<button class="btn btn-primary" type="button">🔗</button>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if not notification.read_at %}
|
||||
<a href="/notification/{{ notification.id }}/read" class="inline-block">
|
||||
<button class="btn btn-primary" type="button">
|
||||
@ -56,6 +61,11 @@
|
||||
<strong>{{ notification.category }}</strong> • {{ notification.created_at | date(format="%d.%m.%Y %H:%M") }}
|
||||
</small>
|
||||
<div class="mt-1">{{ notification.message | safe }}</div>
|
||||
{% if notification.link %}
|
||||
<a href="{{ notification.link }}" class="inline-block">
|
||||
<button class="btn btn-primary" type="button">🔗</button>
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
@ -64,6 +74,176 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if "Donau Linz" in loggedin_user.roles and "Unterstützend" not in loggedin_user.roles and "Förderndes Mitglied" not in loggedin_user.roles %}
|
||||
<dialog id="call-for-action"
|
||||
class="max-w-screen-sm dark:bg-primary-600 dark:text-white rounded-md"
|
||||
onclick="document.getElementById('call-for-action').close()">
|
||||
<div onclick="event.stopPropagation();" class="p-3">
|
||||
<button type="button"
|
||||
onclick="document.getElementById('call-for-action').close()"
|
||||
title="Schließen"
|
||||
class="sidebar-close border-0 bg-primary-100 focus:bg-primary-50 text-black flex items-center justify-center transform rotate-45 absolute right-0 mr-3">
|
||||
<svg class="inline h-5 w-5"
|
||||
width="16"
|
||||
height="16"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 16 16">
|
||||
<path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="mt-8">
|
||||
<p>
|
||||
Du hast Ideen für sinnvolle neue Funktionen für diese Ruderapp? Melde dich bei Philipp, Marie oder <a href="mailto:it@rudernlinz.at" class="underline">it@rudernlinz.at</a>.
|
||||
</p>
|
||||
<p class="mt-3">
|
||||
Wenn du darüber hinaus Lust hast, deine Skills in ein Projekt zu stecken, das Wellen schlagen wird (😀), dann komm an Bord! Wir sind offen für frische Ideen, haben jedoch auch selber noch genügend; langweilig wird uns bestimmt nicht.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">
|
||||
Deine Ruderkarriere
|
||||
<span class="text-xl"
|
||||
onclick="document.getElementById('call-for-action').showModal()">💡</span>
|
||||
</h2>
|
||||
<div class="mx-2 divide-y divide-gray-200 dark:divide-primary-600">
|
||||
<div class="py-3">
|
||||
<details>
|
||||
<summary>
|
||||
<h3 class="inline">
|
||||
<span class="text-xl">
|
||||
{% if achievements.rowingbadge and achievements.rowingbadge.achieved %}
|
||||
🎉
|
||||
{% else %}
|
||||
📋
|
||||
{% endif %}
|
||||
</span>
|
||||
Fahrtenabzeichen
|
||||
{% if achievements.rowingbadge %}{{ achievements.rowingbadge.year }}{% endif %}
|
||||
<span><a href="http://www.rudern.at/OFFICE/Downloads/Ausschreibungen/2022/Wanderfahrten//Fahrtenabzeichen%20%C3%84quatorpreis%20und%20Danubius%202022.pdf"
|
||||
target="_blank"
|
||||
class="w-6 h-6 inline-flex align-center justify-center rounded-full bg-primary-500 ml-2 text-white">?</a></span>
|
||||
</h3>
|
||||
</summary>
|
||||
{% if achievements.rowingbadge %}
|
||||
{% set badge = achievements.rowingbadge %}
|
||||
<div class="mb-3">{{ badge.category }}</div>
|
||||
<label for="rowingbadge" class="label">Kilometer ({{ badge.rowed_km }} / {{ badge.required_km }} km)</label>
|
||||
<progress id="rowingbadge"
|
||||
class="w-full block my-3"
|
||||
value="{{ badge.rowed_km }}"
|
||||
max="{{ badge.required_km }}"></progress>
|
||||
<h4 class="font-bold mt-4">Wanderfahrten</h4>
|
||||
<div>Nur eine der folgenden Optionen muss erreicht werden:</div>
|
||||
<ol class="list-decimal ml-4 my-3">
|
||||
<li>
|
||||
{% if badge.multi_day_trips_over_required_distance | length >= 1 %}
|
||||
✅
|
||||
{% else %}
|
||||
❌
|
||||
{% endif %}
|
||||
1 mehrtägige Wanderfahrt > {{ badge.multi_day_trips_required_distance }} km
|
||||
</li>
|
||||
<li>
|
||||
{% if badge.single_day_trips_over_required_distance | length >= 2 %}
|
||||
✅
|
||||
{% else %}
|
||||
❌
|
||||
{% endif %}
|
||||
2 eintägige Wanderfahrten > {{ badge.single_day_trips_required_distance }} km
|
||||
</li>
|
||||
</ol>
|
||||
<details>
|
||||
<summary>Details zu den Wanderfahrten</summary>
|
||||
<div class="mt-3">
|
||||
{% for log in badge.single_day_trips_over_required_distance %}
|
||||
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index) }}
|
||||
{% endfor %}
|
||||
{% for log in badge.multi_day_trips_over_required_distance %}
|
||||
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</details>
|
||||
{% else %}
|
||||
<div class="mt-3">
|
||||
Wir haben leider kein Geburtsdatum von dir und können dir leider deinen heurigen Status für das Fahrtenabzeichen nicht anzeigen. Wenn du dein Geburtsdatum an <a href="mailto:it@rudernlinz.at" class="underline">it@rudernlinz.at</a> schreibst, lässt sich das ändern :-)
|
||||
</div>
|
||||
{% endif %}
|
||||
</details>
|
||||
</div>
|
||||
<div class="py-3">
|
||||
<details>
|
||||
<summary>
|
||||
<h3 class="mb-3 inline">
|
||||
{% set price = achievements.equatorprice %}
|
||||
<span class="text-xl">
|
||||
{% if achievements.curr_equatorprice_name == "-" %}
|
||||
📋
|
||||
{% elif achievements.curr_equatorprice_name == "Bronze" %}
|
||||
🥉
|
||||
{% elif achievements.curr_equatorprice_name == "Silber" %}
|
||||
🥈
|
||||
{% elif achievements.curr_equatorprice_name == "Gold" %}
|
||||
🥇
|
||||
{% elif achievements.curr_equatorprice_name == "Diamant" %}
|
||||
💍
|
||||
{% endif %}
|
||||
</span>
|
||||
Äquatorpreis
|
||||
<span><a href="http://www.rudern.at/OFFICE/Downloads/Ausschreibungen/2022/Wanderfahrten//Fahrtenabzeichen%20%C3%84quatorpreis%20und%20Danubius%202022.pdf"
|
||||
target="_blank"
|
||||
class="w-6 h-6 inline-flex align-center justify-center rounded-full bg-primary-500 ml-2 text-white">?</a></span>
|
||||
</h3>
|
||||
</summary>
|
||||
<div class="mt-3">
|
||||
{% if price.level == "DONE" %}
|
||||
Gratuliere, du hast alles in deinem Rudererleben erreicht, was es (beim Äquatorpreis) zu erreichen gibt.
|
||||
{% else %}
|
||||
<label for="equatorprice" class="label">{{ price.desc }} ({{ price.rowed_km }} / {{ price.required_km }} km)</label>
|
||||
<progress id="equatorprice"
|
||||
class="w-full block my-3"
|
||||
value="{{ price.rowed_km }}"
|
||||
max="{{ price.required_km }}"></progress>
|
||||
<details>
|
||||
<summary>Details</summary>
|
||||
Du bist insgesamt {{ price.rowed_km }} km gerudert. Um den Äquatorpreis in {{ price.desc }} zu erhalten, benötigst du noch {{ price.missing_km }} km um die notwendigen {{ price.required_km }} km zu erreichen.
|
||||
</details>
|
||||
{% endif %}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="py-3">
|
||||
<p>
|
||||
<details>
|
||||
<summary>
|
||||
<span class="text-xl"> 📅 </span> Kalender
|
||||
</summary>
|
||||
<p class="mt-3">
|
||||
Du möchtest immer up-to-date mit den Events und Ausfahrten bleiben? Wir bieten 3 verschiedene Arten von Kalender an:
|
||||
</p>
|
||||
<ol class="list-decimal ml-5 my-3">
|
||||
<li>
|
||||
<strong>Alle Events und Ausfahrten</strong>, zu denen du dich angemeldet hast: <a class="underline break-all"
|
||||
href="https://app.rudernlinz.at/cal/personal/{{ loggedin_user.id }}/{{ loggedin_user.user_token }}">https://app.rudernlinz.at/cal/personal/{{ loggedin_user.id }}/{{ loggedin_user.user_token }}</a>
|
||||
<br />
|
||||
<small>Dieser Link enthält einen zufällig generierten Teil, damit nur du (und jene, denen du diesen Link weitergibst) Zugang zu diesen Daten hast.</small>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Allgemeiner Kalender</strong>, zB save-the-dates (Wanderfahrten, ...): <a href="https://rudernlinz.at/cal" class="break-all underline">https://rudernlinz.at/cal</a>
|
||||
</li>
|
||||
<li>
|
||||
<strong>Alle Events</strong>: <a class="break-all underline" href="https://app.rudernlinz.at/cal">https://app.rudernlinz.at/cal</a>
|
||||
<br />
|
||||
<small>Beachte, dass dieser Kalender keine Ausfahrten enthält, die von einzelnen Steuerpersonen augeschrieben werden. Dieser Kalender wird zB auf <a href="https://rudernlinz.at/termine" class="underline">https://rudernlinz.at/termine</a> verwendet und wir möchten keine persönlichen Daten (Namen etc.) leaken.</small>
|
||||
</li>
|
||||
</ol>
|
||||
Du kannst die Kalender einfach in deinen Kalender als "externen Kalender" synchronisieren. Die genauen Schritte hängen von deiner verwendeten Software ab.
|
||||
</details>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5"
|
||||
role="alert">
|
||||
<h2 class="h2">Aktives Vereinsmitglied</h2>
|
||||
@ -155,6 +335,17 @@
|
||||
<a href="/board/boathouse"
|
||||
class="block w-100 py-2 hover:text-primary-600">Bootshaus</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/admin/mail" class="block w-100 py-2 hover:text-primary-600">Mail ausschicken</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/admin/notification"
|
||||
class="block w-100 py-2 hover:text-primary-600">Nachricht ausschreiben</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/board/achievement"
|
||||
class="block w-100 py-2 hover:text-primary-600">Abzeichen</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
@ -169,19 +360,12 @@
|
||||
<li class="py-1">
|
||||
<a href="/admin/user" class="block w-100 py-2 hover:text-primary-600">User</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/admin/mail" class="block w-100 py-2 hover:text-primary-600">Mail</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/admin/rss" class="block w-100 py-2 hover:text-primary-600">Logs</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/admin/list" class="block w-100 py-2 hover:text-primary-600">Fingerabdruck-Liste überprüfen</a>
|
||||
</li>
|
||||
<li class="py-1">
|
||||
<a href="/admin/notification"
|
||||
class="block w-100 py-2 hover:text-primary-600">Nachricht ausschreiben</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
@ -53,6 +53,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{{ macros::boatreservation() }}
|
||||
{{ macros::plannedtrips() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -23,7 +23,15 @@
|
||||
placeholder="Suchen nach Bootsname oder Ruderer...">
|
||||
</div>
|
||||
<div id="filter-result-js" class="search-result"></div>
|
||||
{% for log in logs %}{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index) }}{% endfor %}
|
||||
{% for log in logs %}
|
||||
{% set_global allowed_to_edit = false %}
|
||||
{% if loggedin_user %}
|
||||
{% if "Vorstand" in loggedin_user.roles %}
|
||||
{% set_global allowed_to_edit = true %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{{ log::show_old(log=log, state="completed", only_ones=false, index=loop.index, allowed_to_edit=allowed_to_edit) }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
|
@ -35,6 +35,7 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
{{ macros::boatreservation() }}
|
||||
{{ macros::plannedtrips() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -18,8 +18,8 @@
|
||||
<script type="text/javascript">
|
||||
var sepaqr = new sepaQR({
|
||||
benefName: 'ASKÖ Ruderverein Donau Linz',
|
||||
benefBIC: 'BKAUATWWXXX',
|
||||
benefAccNr: 'AT131200080413001200',
|
||||
benefBIC: 'ASPKAT2LXXX',
|
||||
benefAccNr: 'AT582032032100729256',
|
||||
amountEuro: {{ fee.sum_in_cents/100 }},
|
||||
remittanceInf: 'Vereinsgebühren {{ fee.name }}',
|
||||
});
|
||||
@ -44,13 +44,13 @@
|
||||
</ul>
|
||||
</small>
|
||||
{% endif %}
|
||||
Bitte auf folgendes Konto überweisen: IBAN AT13 1200 0804 1300 1200. Alternativ kannst du auch mit deiner Bankapp den QR Code scannen, damit sollten alle Daten vorausgefüllt sein.
|
||||
Bitte auf folgendes Konto überweisen: IBAN AT58 2032 0321 0072 9256. Alternativ kannst du auch mit deiner Bankapp den QR Code scannen, damit sollten alle Daten vorausgefüllt sein.
|
||||
<br />
|
||||
Falls die Berechnung nicht stimmt (korrekte Preise findest du <a href="https://rudernlinz.at/unser-verein/gebuhren/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">hier</a>) melde dich bitte bei it@rudernlinz.at. @Studenten: Bitte die aktuelle Studienbestätigung an it@rudernlinz.at schicken.
|
||||
<br />
|
||||
<small>Wir aktualisieren den Ruderassistent unregelmäßig mit unserem Bankkonto. Falls du schon bezahlt hast, kannst du diese Nachricht getrost ignorieren :^)</small>
|
||||
<small><a href="https://rudernlinz.at/unser-verein/vorstand/" target="_blank">Unsere Kassiere</a> aktualisieren den Ruderassistent unregelmäßig mit unserem Bankkonto. Falls du schon bezahlt hast, kannst du diese Nachricht getrost ignorieren. Wenn du schon vor "einigen Wochen" bezahlt hast bitte bei kassier@rudernlinz.at nachfragen :^)</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -67,7 +67,8 @@
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
<div class="bg-white dark:bg-primary-900 rounded-md flex justify-between flex-col shadow reset-js"
|
||||
<div id="{{ day.day| date(format="%Y-%m-%d") }}"
|
||||
class="bg-white dark:bg-primary-900 rounded-md flex justify-between flex-col shadow reset-js"
|
||||
style="min-height: 10rem"
|
||||
data-trips="{{ amount_trips }}"
|
||||
data-month="{{ day.day| date(format='%m') }}"
|
||||
@ -99,7 +100,10 @@
|
||||
style="order: {{ event.planned_starting_time | replace(from=":", to="") }}">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="mr-1">
|
||||
{% if event.max_people == 0 %}
|
||||
{% if event.always_show and not day.regular_sees_this_day %}
|
||||
<span title="Du siehst diese Ausfahrt schon, obwohl sie mehr als {{ amount_days_to_show_trips_ahead }} Tage in der Zukunft liegt. Du Magier!">🔮</span>
|
||||
{% endif -%}
|
||||
{%- if event.max_people == 0 %}
|
||||
<strong class="text-[#f43f5e]">⚠ Absage
|
||||
{{ event.planned_starting_time }}
|
||||
Uhr
|
||||
@ -273,6 +277,9 @@
|
||||
data-coxneeded="false">
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="mr-1">
|
||||
{% if trip.always_show and not day.regular_sees_this_day %}
|
||||
<span title="Du siehst diese Ausfahrt schon, obwohl sie mehr als {{ amount_days_to_show_trips_ahead }} Tage in der Zukunft liegt. Du Magier!">🔮</span>
|
||||
{% endif -%}
|
||||
{% if trip.max_people == 0 %}
|
||||
<strong class="text-[#f43f5e]">⚠
|
||||
{{ trip.planned_starting_time }}
|
||||
@ -326,7 +333,7 @@
|
||||
<div id="trip{{ trip.trip_details_id }}">
|
||||
{% if trip.max_people == 0 %}
|
||||
{# --- border-[#f43f5e] bg-[#f43f5e] --- #}
|
||||
{{ macros::box(participants=trip.rower,bg='[#f43f5e]',header='Absage') }}
|
||||
{{ macros::box(participants=trip.rower,bg='[#f43f5e]',header='Absage', trip_details_id=trip.trip_details_id, allow_removing=loggedin_user.id == trip.cox_id) }}
|
||||
{% else %}
|
||||
{% set amount_cur_rower = trip.rower | length %}
|
||||
{{ macros::box(participants=trip.rower, empty_seats=trip.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=trip.trip_details_id, allow_removing=loggedin_user.id == trip.cox_id) }}
|
||||
@ -346,7 +353,6 @@
|
||||
<form action="/cox/trip/{{ trip.id }}" method="post" class="grid gap-3">
|
||||
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=trip.max_people, min=trip.rower | length) }}
|
||||
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=trip.notes) }}
|
||||
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=trip.id,checked=trip.always_show) }}
|
||||
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=trip.id,checked=trip.is_locked) }}
|
||||
{{ macros::select(label='Typ', name='trip_type', data=trip_types, default='Reguläre Ausfahrt', selected_id=trip.trip_type_id) }}
|
||||
<input value="Speichern" class="btn btn-primary" type="submit" />
|
||||
@ -369,7 +375,6 @@
|
||||
<form action="/cox/trip/{{ trip.id }}" method="post" class="grid">
|
||||
{{ macros::input(label='', name='max_people', type='hidden', value=0) }}
|
||||
{{ macros::input(label='Grund der Absage', name='notes', type='input', value='') }}
|
||||
{{ macros::input(label='', name='always_show', type='hidden', value=trip.always_show) }}
|
||||
{{ macros::input(label='', name='is_locked', type='hidden', value=trip.is_locked) }}
|
||||
{{ macros::input(label='', name='trip_type', type='hidden', value=trip.trip_type_id) }}
|
||||
<input value="Ausfahrt absagen" class="btn btn-alert" type="submit" />
|
||||
@ -379,6 +384,32 @@
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{# --- END Edit Form --- #}
|
||||
{# --- START Admin Form --- #}
|
||||
{% if allowed_to_update_always_show_trip %}
|
||||
<div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
|
||||
<h3 class="text-primary-950 dark:text-white font-bold uppercase tracking-wide mb-2">Admin-Modus</h3>
|
||||
{% if not day.regular_sees_this_day %}
|
||||
<form action="/cox/trip/{{ trip.id }}/toggle-always-show"
|
||||
method="get"
|
||||
class="grid gap-3">
|
||||
{% if not trip.always_show %}
|
||||
<small>Diese Ausfahrt sehen aktuell nur Steuerleute (und Admins). {{ amount_days_to_show_trips_ahead }} Tage vorher sehen sie dann alle.</small>
|
||||
{% else %}
|
||||
<small>Diese Ausfahrt sehen alle Mitglieder.</small>
|
||||
{% endif %}
|
||||
<input value="{% if trip.always_show %}Ausfahrt nur Steuerleute (und Admins) anzeigen{% else %}Ausfahrt allen anzeigen{% endif %}"
|
||||
class="btn btn-primary"
|
||||
type="submit" />
|
||||
</form>
|
||||
{% endif %}
|
||||
<a href="/cox/remove/trip/{{ trip.id }}"
|
||||
class="inline-block btn btn-alert mt-5 w-full">
|
||||
{% include "includes/delete-icon" %}
|
||||
Termin löschen
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{# --- END Admin Form --- #}
|
||||
</div>
|
||||
</div>
|
||||
{# --- END Sidebar Content --- #}
|
||||
@ -391,15 +422,15 @@
|
||||
</div>
|
||||
{# --- START Add Buttons --- #}
|
||||
{% if "manage_events" in loggedin_user.roles or "cox" in loggedin_user.roles %}
|
||||
<div class="grid {% if "manage_events" in loggedin_user.roles %}grid-cols-2{% endif %} text-center">
|
||||
<div class="grid {% if "manage_events" in loggedin_user.roles and "cox" in loggedin_user.roles %}grid-cols-2{% endif %} text-center">
|
||||
{% if "manage_events" in loggedin_user.roles %}
|
||||
<a href="#"
|
||||
data-sidebar="true"
|
||||
data-trigger="sidebar"
|
||||
data-header="<strong>Event</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen"
|
||||
data-day="{{ day.day }}"
|
||||
data-body="#addEventForm"
|
||||
class="relative inline-block w-full bg-primary-900 hover:bg-primary-950 focus:bg-primary-950 dark:bg-primary-950 text-white py-2 rounded-bl-md text-sm font-semibold">
|
||||
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Event</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#addEventForm" class="relative inline-block w-full bg-primary-900 hover:bg-primary-950 focus:bg-primary-950 dark:bg-primary-950 text-white py-2 text-sm font-semibold
|
||||
{% if "cox" in loggedin_user.roles %}
|
||||
rounded-bl-md
|
||||
{% else %}
|
||||
rounded-b-md
|
||||
{% endif %}
|
||||
">
|
||||
<span class="absolute inset-y-0 left-0 flex items-center pl-3">{% include "includes/plus-icon" %}</span>
|
||||
Event
|
||||
</a>
|
||||
|
@ -9,6 +9,7 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Art</th>
|
||||
<th>Eigentümer</th>
|
||||
<th>Ort</th>
|
||||
{% for year in stat.pot_years | sort | reverse %}<th>{{ year }}</th>{% endfor %}
|
||||
@ -18,6 +19,7 @@
|
||||
{% for boat in stat.boats %}
|
||||
<tr>
|
||||
<td>{{ boat.name }}</td>
|
||||
<td>{{ boat.cat }}</td>
|
||||
<td>{{ boat.owner }}</td>
|
||||
<td>{{ boat.location }}</td>
|
||||
{% for year in stat.pot_years | sort | reverse %}
|
||||
|
@ -51,7 +51,7 @@
|
||||
data-filter="{{ reservation.user_applicant.name }} {{ reservation.trailer.name }}"
|
||||
class="w-full border-t bg-white dark:bg-primary-900 text-black dark:text-white p-3">
|
||||
<div class="w-full">
|
||||
<strong>Boot:</strong>
|
||||
<strong>Hänger:</strong>
|
||||
{{ reservation.trailer.name }}
|
||||
<br />
|
||||
<strong>Reservierung:</strong>
|
||||
|
156
wordpress-notes.md
Normal file
156
wordpress-notes.md
Normal file
@ -0,0 +1,156 @@
|
||||
# Wordpress auth
|
||||
|
||||
Add the following code to `wp-content/themes/bravada/functions.php`:
|
||||
|
||||
```
|
||||
function rot_auth( $user, $username, $password ){
|
||||
// Make sure a username and password are present for us to work with
|
||||
if($username == '' || $password == '') return;
|
||||
|
||||
$ch = curl_init();
|
||||
|
||||
curl_setopt($ch, CURLOPT_URL, 'https://app.rudernlinz.at/wikiauth');
|
||||
curl_setopt($ch, CURLOPT_POST, 1);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, "name=$username&password=$password");
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
// Execute the cURL session and get the response
|
||||
$response = curl_exec($ch);
|
||||
|
||||
// Check for cURL errors
|
||||
if(curl_errno($ch)){
|
||||
$user = new WP_Error( 'denied', __('Curl error: ' . curl_error($ch)) );
|
||||
}
|
||||
|
||||
// Close the cURL session
|
||||
curl_close($ch);
|
||||
|
||||
|
||||
if (strpos($response, 'SUCC') !== false) {
|
||||
$user = get_user_by('login', $username);
|
||||
|
||||
if (!$user) {
|
||||
// User does not exist, create a new one
|
||||
$userdata = array(
|
||||
'user_email' => $username,
|
||||
'user_login' => $username,
|
||||
'first_name' => $username,
|
||||
'last_name' => ''
|
||||
);
|
||||
$new_user_id = wp_insert_user($userdata);
|
||||
|
||||
if (!is_wp_error($new_user_id)) {
|
||||
// Load the new user info
|
||||
$user = new WP_User($new_user_id);
|
||||
|
||||
// Set role based on username
|
||||
if ($username == 'Philipp Hofer' || $username == 'Marie Birner') {
|
||||
$user->set_role('administrator');
|
||||
} else {
|
||||
$user->set_role('editor');
|
||||
}
|
||||
} else {
|
||||
// Handle error in user creation
|
||||
return $new_user_id;
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
} else {
|
||||
$user = new WP_Error( 'denied', __("Falscher Benutzername/Passwort. Verwendest du deine Accountdaten vom Ruderassistenten?") );
|
||||
}
|
||||
|
||||
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
// Comment this line if you wish to fall back on WordPress authentication
|
||||
// Useful for times when the external service is offline
|
||||
remove_action('authenticate', 'wp_authenticate_username_password', 20);
|
||||
|
||||
add_filter( 'authenticate', 'rot_auth', 10, 3 );
|
||||
```
|
||||
|
||||
|
||||
# Wordpress notify rowt on newly published article
|
||||
|
||||
Add the following code to `wp-content/themes/bravada/functions.php`:
|
||||
|
||||
```
|
||||
function send_article_url_on_publish($new_status, $old_status, $post) {
|
||||
// Check if the post is transitioning to 'publish' status
|
||||
if ($new_status == 'publish' && $old_status != 'publish' && $post->post_type == 'post') {
|
||||
// Get the URL of the newly published article
|
||||
$article_url = get_permalink($post->ID);
|
||||
$article_title = get_the_title($post->ID);
|
||||
|
||||
// URL to send the POST request to
|
||||
$api_url = 'https://app.rudernlinz.at/new-blogpost';
|
||||
|
||||
// Prepare the data for the POST request
|
||||
$body = array(
|
||||
'article_url' => $article_url,
|
||||
'article_title' => $article_title,
|
||||
'pw' => "wordpress_key"
|
||||
);
|
||||
|
||||
// Prepare the arguments for wp_remote_post
|
||||
$args = array(
|
||||
'body' => $body,
|
||||
'timeout' => '5',
|
||||
'redirection' => '5',
|
||||
'httpversion' => '1.0',
|
||||
'blocking' => true,
|
||||
'headers' => array(),
|
||||
'cookies' => array()
|
||||
);
|
||||
|
||||
// Send the POST request
|
||||
$response = wp_remote_post($api_url, $args);
|
||||
|
||||
// Optional: Check if the request was successful
|
||||
if (is_wp_error($response)) {
|
||||
error_log('Failed to send POST request: ' . $response->get_error_message());
|
||||
} else {
|
||||
error_log('POST request sent successfully with article URL: ' . $article_url);
|
||||
}
|
||||
}
|
||||
if ($new_status != 'publish' && $old_status == 'publish' && $post->post_type == 'post') {
|
||||
$article_url = get_permalink($post->ID);
|
||||
// URL to send the POST request to
|
||||
$api_url = 'https://app.rudernlinz.at/blogpost-unpublished';
|
||||
|
||||
// Prepare the data for the POST request
|
||||
$body = array(
|
||||
'article_url' => $article_url,
|
||||
'pw' => "wordpress_key"
|
||||
);
|
||||
|
||||
// Prepare the arguments for wp_remote_post
|
||||
$args = array(
|
||||
'body' => $body,
|
||||
'timeout' => '5',
|
||||
'redirection' => '5',
|
||||
'httpversion' => '1.0',
|
||||
'blocking' => true,
|
||||
'headers' => array(),
|
||||
'cookies' => array()
|
||||
);
|
||||
|
||||
// Send the POST request
|
||||
$response = wp_remote_post($api_url, $args);
|
||||
|
||||
// Optional: Check if the request was successful
|
||||
if (is_wp_error($response)) {
|
||||
error_log('Failed to send POST request: ' . $response->get_error_message());
|
||||
} else {
|
||||
error_log('POST request sent successfully with article URL: ' . $article_url);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Hook the function to the 'transition_post_status' action
|
||||
add_action('transition_post_status', 'send_article_url_on_publish', 10, 3);
|
||||
```
|
Reference in New Issue
Block a user