diff --git a/frontend/main.ts b/frontend/main.ts index f0e404f..2ddfe48 100644 --- a/frontend/main.ts +++ b/frontend/main.ts @@ -6,6 +6,7 @@ export interface choiceMap { } let choiceObjects: choiceMap = {}; +let boat_in_ottensheim = true; document.addEventListener('DOMContentLoaded', function() { changeTheme(); @@ -106,6 +107,7 @@ interface ChoiceBoatEvent extends Event{ amount_seats: number, owner: number, default_destination: string, + boat_in_ottensheim: boolean, } }; } @@ -123,6 +125,8 @@ function selectBoatChange() { boatSelect.addEventListener('addItem', function(e) { const event = e as ChoiceBoatEvent; + boat_in_ottensheim = event.detail.customProperties.boat_in_ottensheim; + const amount_seats = event.detail.customProperties.amount_seats; setMaxAmountRowers("newrower", amount_seats); @@ -274,6 +278,7 @@ interface ChoiceEvent extends Event{ is_cox: boolean, steers: boolean, cox_on_boat: boolean, + is_racing: boolean, } }; } @@ -320,6 +325,16 @@ function initNewChoice(select: HTMLInputElement) { }, callbackOnInit: function() { this._currentState.items.forEach(function(obj){ + if (boat_in_ottensheim && obj.customProperties) { + if (obj.customProperties.is_racing) { + const coxSelect = document.querySelector('#shipmaster-' + select.id + 'js'); + var new_option = new Option(obj.label, obj.value); + if (obj.customProperties.cox_on_boat){ + new_option.selected = true; + } + coxSelect.add(new_option); + } + } if (obj.customProperties && obj.customProperties.is_cox){ const coxSelect = document.querySelector('#shipmaster-' + select.id + 'js'); var new_option = new Option(obj.label, obj.value); @@ -346,6 +361,14 @@ function initNewChoice(select: HTMLInputElement) { const user_id = event.detail.value; const name = event.detail.label; + if (boat_in_ottensheim && event.detail.customProperties.is_racing) { + if (event.detail.customProperties.is_racing) { + const coxSelect = document.querySelector('#shipmaster-' + select.id + 'js'); + if (coxSelect){ + coxSelect.add(new Option(name, user_id)); + } + } + } if (event.detail.customProperties.is_cox) { const coxSelect = document.querySelector('#shipmaster-' + select.id + 'js'); if (coxSelect){ diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts index 3828370..691f890 100644 --- a/frontend/playwright.config.ts +++ b/frontend/playwright.config.ts @@ -10,6 +10,7 @@ import { defineConfig, devices } from '@playwright/test'; * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ + timeout: 180000, testDir: './tests', /* Run tests in files in parallel */ fullyParallel: true, diff --git a/frontend/tests/log.spec.ts b/frontend/tests/log.spec.ts new file mode 100644 index 0000000..f714b5c --- /dev/null +++ b/frontend/tests/log.spec.ts @@ -0,0 +1,69 @@ +import { test, expect } from '@playwright/test'; + +test('Cox can start and cancel trip', async ({ page }, testInfo) => { + await page.goto('http://localhost:8000/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('http://localhost:8000/'); + await page.getByRole('link', { name: 'Ausfahrt eintragen' }).click(); + if (testInfo.project.name.includes('Mobile')) { // No left boat selector on mobile views + await page.getByText('Kaputtes Boot :-( (7 x)').nth(1).click(); + await page.getByRole('option', { name: 'Joe' }).click(); + } else{ + await page.getByText('Joe', { exact: true }).click(); + } + await page.getByPlaceholder('Ruderer auswählen').click(); + await page.getByRole('option', { name: 'rower2' }).click(); + await page.getByRole('option', { name: 'cox2' }).click(); + await expect(page.getByRole('listbox')).toContainText('Nur 2 Ruderer können hinzugefügt werden'); + await expect(page.locator('#shipmaster-newrowerjs')).toContainText('cox'); + await expect(page.locator('#steering_person-newrowerjs')).toContainText('rower2 cox'); + await page.getByRole('button', { name: 'Ausfahrt eintragen' }).click(); + await expect(page.locator('body')).toContainText('Ausfahrt erfolgreich hinzugefügt'); + await expect(page.locator('body')).toContainText('Joe'); + + await page.getByRole('link', { name: 'Joe' }).click(); + page.once('dialog', dialog => { + dialog.accept().catch(() => {}); + }); + await page.getByRole('link', { name: 'Löschen' }).click(); +}); + +test('Cox can start and finish trip', async ({ page }, testInfo) => { + await page.goto('http://localhost:8000/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('http://localhost:8000/'); + await page.getByRole('link', { name: 'Ausfahrt eintragen' }).click(); + if (testInfo.project.name.includes('Mobile')) { // No left boat selector on mobile views + await page.getByText('Kaputtes Boot :-( (7 x)').nth(1).click(); + await page.getByRole('option', { name: 'Joe' }).click(); + } else{ + await page.getByText('Joe', { exact: true }).click(); + } + await page.getByPlaceholder('Ruderer auswählen').click(); + await page.getByRole('option', { name: 'rower2' }).click(); + await page.getByRole('option', { name: 'cox2' }).click(); + await expect(page.getByRole('listbox')).toContainText('Nur 2 Ruderer können hinzugefügt werden'); + await expect(page.locator('#shipmaster-newrowerjs')).toContainText('cox'); + await expect(page.locator('#steering_person-newrowerjs')).toContainText('rower2 cox'); + await page.getByRole('button', { name: 'Ausfahrt eintragen' }).click(); + await expect(page.locator('body')).toContainText('Ausfahrt erfolgreich hinzugefügt'); + await expect(page.locator('body')).toContainText('Joe'); + + await page.goto('http://localhost:8000/log'); + await page.waitForTimeout(60000); + 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'); +}); diff --git a/seeds.sql b/seeds.sql index 8a269f6..7999357 100644 --- a/seeds.sql +++ b/seeds.sql @@ -4,6 +4,7 @@ INSERT INTO "role" (name) VALUES ('scheckbuch'); INSERT INTO "role" (name) VALUES ('tech'); INSERT INTO "role" (name) VALUES ('Donau Linz'); INSERT INTO "role" (name) VALUES ('planned_event'); +INSERT INTO "role" (name) VALUES ('Rennrudern'); 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); @@ -24,6 +25,9 @@ INSERT INTO "user_role" (user_id, role_id) VALUES(6,5); INSERT INTO "user_role" (user_id, role_id) VALUES(6,2); INSERT INTO "user" (name, pw) VALUES('rower2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY'); INSERT INTO "user_role" (user_id, role_id) VALUES(7,5); +INSERT INTO "user" (name, pw) VALUES('teen', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY'); +INSERT INTO "user_role" (user_id, role_id) VALUES(8,5); +INSERT INTO "user_role" (user_id, role_id) VALUES(8,7); INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('10:00', 2, '1970-01-01', 'trip_details for a planned event'); INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES('test-planned-event', 2, 1); diff --git a/src/model/boat.rs b/src/model/boat.rs index 5be09a0..d40a50a 100644 --- a/src/model/boat.rs +++ b/src/model/boat.rs @@ -185,7 +185,7 @@ ORDER BY amount_seats DESC if user.has_role(db, "admin").await { return Self::all(db).await; } - let boats = if user.has_role(db, "cox").await { + let mut boats = if user.has_role(db, "cox").await { sqlx::query_as!( Boat, " @@ -215,6 +215,23 @@ ORDER BY amount_seats DESC .unwrap() //TODO: fixme }; + if user.has_role(db, "Rennrudern").await { + let ottensheim = Location::find_by_name(db, "Ottensheim".into()) + .await + .unwrap(); + let boats_in_ottensheim = sqlx::query_as!( + Boat, + "SELECT id, name, amount_seats, location_id, owner, year_built, boatbuilder, default_shipmaster_only_steering, default_destination, skull, external +FROM boat +WHERE owner is null and location_id = ? +ORDER BY amount_seats DESC + ",ottensheim.id) + .fetch_all(db) + .await + .unwrap(); //TODO: fixme + boats.extend(boats_in_ottensheim.into_iter()); + } + Self::boats_to_details(db, boats).await } diff --git a/src/model/logbook.rs b/src/model/logbook.rs index dfb8278..b3e4253 100644 --- a/src/model/logbook.rs +++ b/src/model/logbook.rs @@ -274,8 +274,6 @@ ORDER BY departure DESC } if let Ok(log_to_finalize) = TryInto::::try_into(log.clone()) { - //TODO: fix clone() above - if !boat.shipmaster_allowed(db, created_by_user).await { return Err(LogbookCreateError::UserNotAllowedToUseBoat); } diff --git a/src/tera/admin/user.rs b/src/tera/admin/user.rs index 91e8ee8..be3f7a8 100644 --- a/src/tera/admin/user.rs +++ b/src/tera/admin/user.rs @@ -19,7 +19,7 @@ use sqlx::SqlitePool; #[get("/user")] async fn index( db: &State, - admin: AdminUser, + user: VorstandUser, flash: Option>, ) -> Template { let user_futures: Vec<_> = User::all(db) @@ -28,6 +28,9 @@ async fn index( .map(|u| async move { UserWithRoles::from_user(u, db).await }) .collect(); + let user: User = user.into(); + let allowed_to_edit = user.has_role(db, "admin").await; + let users: Vec = join_all(user_futures).await; let roles = Role::all(db).await; @@ -37,13 +40,11 @@ async fn index( if let Some(msg) = flash { context.insert("flash", &msg.into_inner()); } + context.insert("allowed_to_edit", &allowed_to_edit); context.insert("users", &users); context.insert("roles", &roles); context.insert("families", &families); - context.insert( - "loggedin_user", - &UserWithRoles::from_user(admin.user, db).await, - ); + context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await); Template::render("admin/user/index", context.into_json()) } diff --git a/src/tera/log.rs b/src/tera/log.rs index 09fa2c6..2c47e71 100644 --- a/src/tera/log.rs +++ b/src/tera/log.rs @@ -188,7 +188,7 @@ async fn create_logbook( Err(LogbookCreateError::ShipmasterNotInRowers) => Flash::error(Redirect::to("/log"), "Schiffsführer nicht in Liste der Ruderer!"), Err(LogbookCreateError::NotYourEntry) => Flash::error(Redirect::to("/log"), "Nicht deine Ausfahrt!"), Err(LogbookCreateError::ArrivalSetButNotRemainingTwo) => Flash::error(Redirect::to("/log"), "Ankunftszeit gesetzt aber nicht Distanz + Strecke"), - Err(LogbookCreateError::OnlyAllowedToEndTripsEndingToday) => Flash::error(Redirect::to("/log"), "Nur Ausfahrten, die in den 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::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)."), } } diff --git a/templates/admin/user/index.html.tera b/templates/admin/user/index.html.tera index 496e5c9..619f838 100644 --- a/templates/admin/user/index.html.tera +++ b/templates/admin/user/index.html.tera @@ -10,6 +10,7 @@

Users

+ {% if allowed_to_edit %}

Neuen User hinzufügen

@@ -24,6 +25,7 @@
+ {% endif %}
@@ -60,21 +62,24 @@
{% for role in roles %} - {{ macros::checkbox(label=role.name, name="roles[" ~ role.id ~ "]", id=loop.index , checked=role.name in user.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%} - {{ macros::input(label='DOB', name='dob', id=loop.index, type="text", value=user.dob) }} - {{ macros::input(label='Weight (kg)', name='weight', id=loop.index, type="text", value=user.weight) }} - {{ macros::input(label='Sex', name='sex', id=loop.index, type="text", value=user.sex) }} - {{ macros::input(label='Mitglied seit', name='member_since_date', id=loop.index, type="text", value=user.member_since_date) }} - {{ macros::input(label='Geburtsdatum', name='birthdate', id=loop.index, type="text", value=user.birthdate) }} - {{ macros::input(label='Mail', name='mail', id=loop.index, type="text", value=user.mail) }} - {{ macros::input(label='Nickname', name='nickname', id=loop.index, type="text", value=user.nickname) }} - {{ macros::input(label='Notizen', name='notes', id=loop.index, type="text", value=user.notes) }} - {{ macros::input(label='Telefon', name='phone', id=loop.index, type="text", value=user.phone) }} - {{ macros::input(label='Adresse', name='address', id=loop.index, type="text", value=user.address) }} + {{ macros::input(label='DOB', name='dob', id=loop.index, type="text", value=user.dob, readonly=allowed_to_edit == false) }} + {{ macros::input(label='Weight (kg)', name='weight', id=loop.index, type="text", value=user.weight, readonly=allowed_to_edit == false) }} + {{ macros::input(label='Sex', name='sex', id=loop.index, type="text", value=user.sex, readonly=allowed_to_edit == false) }} + {{ macros::input(label='Mitglied seit', name='member_since_date', id=loop.index, type="text", value=user.member_since_date, readonly=allowed_to_edit == false) }} + {{ macros::input(label='Geburtsdatum', name='birthdate', id=loop.index, type="text", value=user.birthdate, readonly=allowed_to_edit == false) }} + {{ macros::input(label='Mail', name='mail', id=loop.index, type="text", value=user.mail, readonly=allowed_to_edit == false) }} + {{ macros::input(label='Nickname', name='nickname', id=loop.index, type="text", value=user.nickname, readonly=allowed_to_edit == false) }} + {{ macros::input(label='Notizen', name='notes', id=loop.index, type="text", value=user.notes, readonly=allowed_to_edit == false) }} + {{ macros::input(label='Telefon', name='phone', id=loop.index, type="text", value=user.phone, readonly=allowed_to_edit == false) }} + {{ macros::input(label='Adresse', name='address', id=loop.index, type="text", value=user.address, readonly=allowed_to_edit == false) }} + {% if allowed_to_edit %} {{ macros::select(label="Familie", data=families, name='family_id', selected_id=user.family_id, display=['names'], default="Keine Familie", new_last_entry='Neue Familie anlegen') }} + {% endif %}
+ {% if allowed_to_edit %} + {% endif %} {% endfor %} diff --git a/templates/includes/forms/log.html.tera b/templates/includes/forms/log.html.tera index 6707910..f821d88 100644 --- a/templates/includes/forms/log.html.tera +++ b/templates/includes/forms/log.html.tera @@ -1,12 +1,8 @@ {# Shows a fancy, optional lists of boats. They are grouped by boat category. Inputs: boats - Parameters: only_ones: if set, only 1x boats are shown #} -{% macro show_boats(only_ones) %} - {% if only_ones %} - {% set_global boats = boats | filter(attribute="amount_seats", value=1) %} - {% endif %} +{% macro show_boats() %} {% for amount_seats, grouped_boats in boats | group_by(attribute="amount_seats") %}
@@ -27,20 +23,16 @@ {% endmacro show_boats %} {# Shows the form for creating a new logbook entry. #} -{% macro new(only_ones, shipmaster) %} +{% macro new(shipmaster) %}
- {{ log::boat_select(only_ones=only_ones) }} - {% if not only_ones %} + {{ log::boat_select() }}
Bootssteuerung
{{ macros::checkbox(label='handgesteuert', name='shipmaster_only_steering', disabled=true) }}
- {% endif %} - {% if not only_ones %} {{ log::rower_select(id="newrower", selected=[], class="col-span-4", init=true) }} - {% endif %} {{ macros::select(label="Schiffsführer", data=[], name='shipmaster', id="shipmaster-newrowerjs", wrapper_class="col-span-2") }} {{ macros::select(label="Steuerperson", data=[], name='steering_person', id="steering_person-newrowerjs", wrapper_class="col-span-2") }} {{ macros::input(label='Abfahrtszeit', name='departure', type='datetime-local', required=true, wrapper_class='col-span-2') }} @@ -64,13 +56,8 @@ {% endmacro new %} -{% macro boat_select(only_ones, id="boat_id") %} - {% if not only_ones %} +{% macro boat_select(id="boat_id") %} {{ macros::select(label="Boot", data=boats, name="boat_id", id=id, display=["name", " (","amount_seats", " x)"], extras=["default_shipmaster_only_steering", "amount_seats", "on_water", "default_destination"], wrapper_class="col-span-4", show_seats=true) }} - {% else %} - {% set ones = boats | filter(attribute="amount_seats", value=1) %} - {{ macros::select(label="Boot", data=ones, name="boat_id", id=id, display=["name", " (","amount_seats", " x)"], extras=["default_shipmaster_only_steering", "amount_seats", "on_water", "default_destination"], wrapper_class="col-span-4", show_seats=true) }} - {% endif %} {% endmacro boat_select %} {% macro rower_select(id, selected, amount_seats='', class='', init='false', cox_on_boat='', steering_person_id='') %} @@ -85,7 +72,7 @@ {% set_global sel = true %} {% endif %} {% endfor %} -