Compare commits

..

12 Commits

Author SHA1 Message Date
5cc8f1ff48 Merge pull request 'limit-users-to-proper-roles' (#156) from limit-users-to-proper-roles into staging
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #156
2024-01-10 14:55:41 +01:00
a4b8bf1e3f improve code w/ clippy
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m22s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Has been skipped
2024-01-10 14:24:58 +01:00
3e2e058bcc limit users to proper role, Fixes #135
Some checks are pending
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Successful in 15m47s
2024-01-10 14:08:15 +01:00
c9e163c92c Merge pull request 'update-deps' (#155) from update-deps into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m45s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 19m20s
Reviewed-on: #155
2024-01-09 16:15:59 +01:00
882665cbd9 Merge pull request 'remove-external-cox' (#153) from remove-external-cox into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m14s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 17m58s
Reviewed-on: #153
2024-01-05 22:24:27 +01:00
17119999c9 Merge pull request 'hide external boat in stats' (#151) from hide-external-boat-in-stats into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 14m59s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 17m46s
Reviewed-on: #151
2024-01-05 20:43:11 +01:00
5cfaf44a79 Merge pull request 'fix-stat' (#149) from fix-stat into main
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #149
2024-01-05 20:39:02 +01:00
251c83eb30 Merge pull request 'show guests km' (#147) from show-guests-km into main
Some checks failed
CI/CD Pipeline / deploy-staging (push) Blocked by required conditions
CI/CD Pipeline / deploy-main (push) Blocked by required conditions
CI/CD Pipeline / test (push) Has been cancelled
Reviewed-on: #147
2024-01-05 20:09:59 +01:00
2e91925b83 Merge pull request 'fix tests, as we now show all boats in kiosk mode' (#143) from fix-ci into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m9s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 17m37s
Reviewed-on: #143
2024-01-04 20:48:11 +01:00
2726db433e Merge pull request 'allow to add logbook entries up to a week late; show all boats in boatshouse; don't show guests for external boats' (#141) from fixes into main
Some checks failed
CI/CD Pipeline / test (push) Has been cancelled
CI/CD Pipeline / deploy-staging (push) Has been cancelled
CI/CD Pipeline / deploy-main (push) Has been cancelled
Reviewed-on: #141
2024-01-04 20:44:26 +01:00
86075b72c6 Merge pull request 'fix weird iphone issue' (#136) from fix-weird-iphone-issue into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m40s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 18m4s
Reviewed-on: #136
2024-01-03 09:59:50 +01:00
5b087baf02 Merge pull request 'staging' (#125) from staging into main
All checks were successful
CI/CD Pipeline / test (push) Successful in 15m15s
CI/CD Pipeline / deploy-staging (push) Has been skipped
CI/CD Pipeline / deploy-main (push) Successful in 17m44s
Reviewed-on: #125
2024-01-01 21:05:58 +01:00
13 changed files with 769 additions and 567 deletions

View File

@ -2,18 +2,26 @@ INSERT INTO "role" (name) VALUES ('admin');
INSERT INTO "role" (name) VALUES ('cox');
INSERT INTO "role" (name) VALUES ('scheckbuch');
INSERT INTO "role" (name) VALUES ('tech');
INSERT INTO "role" (name) VALUES ('Donau Linz');
INSERT INTO "user" (name, pw) VALUES('admin', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$4P4NCw4Ukhv80/eQYTsarHhnw61JuL1KMx/L9dm82YM');
INSERT INTO "user_role" (user_id, role_id) VALUES(1,1);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,2);
INSERT INTO "user_role" (user_id, role_id) VALUES(1,5);
INSERT INTO "user" (name, pw) VALUES('rower', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
INSERT INTO "user_role" (user_id, role_id) VALUES(2,5);
INSERT INTO "user" (name, pw) VALUES('guest', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$GF6gizbI79Bh0zA9its8S0gram956v+YIV8w8VpwJnQ');
INSERT INTO "user_role" (user_id, role_id) VALUES(3,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(3,3);
INSERT INTO "user" (name, pw) VALUES('cox', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs');
INSERT INTO "user_role" (user_id, role_id) VALUES(4,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(4,2);
INSERT INTO "user" (name) VALUES('new');
INSERT INTO "user_role" (user_id, role_id) VALUES(5,5);
INSERT INTO "user" (name, pw) VALUES('cox2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$lnWzHx3DdqS9GQyWYel82kIotZuK2wk9EyfhPFtjNzs');
INSERT INTO "user_role" (user_id, role_id) VALUES(6,5);
INSERT INTO "user_role" (user_id, role_id) VALUES(6,2);
INSERT INTO "user" (name, pw) VALUES('rower2', '$argon2id$v=19$m=19456,t=2,p=1$dS/X5/sPEKTj4Rzs/CuvzQ$jWKzDmI0jqT2dqINFt6/1NjVF4Dx15n07PL1ZMBmFsY');
INSERT INTO "user_role" (user_id, role_id) VALUES(7,5);
INSERT INTO "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);

View File

@ -264,6 +264,10 @@ ORDER BY departure DESC
return Err(LogbookCreateError::BoatNotFound);
};
if boat.amount_seats == 1 && log.rowers.is_empty() {
log.rowers = vec![created_by_user.id];
}
if boat.amount_seats == 1 {
log.shipmaster = Some(log.rowers[0]);
log.steering_person = Some(log.rowers[0]);

View File

@ -440,23 +440,20 @@ impl<'r> FromRequest<'r> for User {
Ok(user_id) => {
let db = req.rocket().state::<SqlitePool>().unwrap();
let Some(user) = User::find_by_id(db, user_id).await else {
return Outcome::Error((Status::Unauthorized, LoginError::UserNotFound));
return Outcome::Error((Status::Forbidden, LoginError::UserNotFound));
};
if user.deleted {
return Outcome::Error((Status::Unauthorized, LoginError::UserDeleted));
return Outcome::Error((Status::Forbidden, LoginError::UserDeleted));
}
user.logged_in(db).await;
let mut cookie = Cookie::new("loggedin_user", format!("{}", user.id));
cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(12));
cookie.set_expires(OffsetDateTime::now_utc() + Duration::weeks(2));
req.cookies().add_private(cookie);
Outcome::Success(user)
}
Err(_) => {
println!("{:?}", user_id.value());
Outcome::Error((Status::Unauthorized, LoginError::DeserializationError))
}
Err(_) => Outcome::Error((Status::Unauthorized, LoginError::DeserializationError)),
},
None => Outcome::Error((Status::Unauthorized, LoginError::NotLoggedIn)),
}
@ -487,7 +484,7 @@ impl<'r> FromRequest<'r> for TechUser {
if user.has_role(db, "tech").await {
Outcome::Success(TechUser { user })
} else {
Outcome::Error((Status::Unauthorized, LoginError::NotACox))
Outcome::Error((Status::Forbidden, LoginError::NotACox))
}
}
Outcome::Error(f) => Outcome::Error(f),
@ -530,7 +527,7 @@ impl<'r> FromRequest<'r> for CoxUser {
if user.has_role(db, "cox").await {
Outcome::Success(CoxUser { user })
} else {
Outcome::Error((Status::Unauthorized, LoginError::NotACox))
Outcome::Error((Status::Forbidden, LoginError::NotACox))
}
}
Outcome::Error(f) => Outcome::Error(f),
@ -555,7 +552,7 @@ impl<'r> FromRequest<'r> for AdminUser {
if user.has_role(db, "admin").await {
Outcome::Success(AdminUser { user })
} else {
Outcome::Error((Status::Unauthorized, LoginError::NotACox))
Outcome::Error((Status::Forbidden, LoginError::NotACox))
}
}
Outcome::Error(f) => Outcome::Error(f),
@ -565,22 +562,65 @@ impl<'r> FromRequest<'r> for AdminUser {
}
#[derive(Debug, Serialize, Deserialize)]
pub struct NonGuestUser {
pub(crate) user: User,
}
pub struct AllowedForPlannedTripsUser(pub(crate) User);
#[async_trait]
impl<'r> FromRequest<'r> for NonGuestUser {
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, "scheckbuch").await {
Outcome::Success(NonGuestUser { user })
if user.has_role(db, "Donau Linz").await {
Outcome::Success(AllowedForPlannedTripsUser(user))
} else if user.has_role(db, "scheckbuch").await {
Outcome::Success(AllowedForPlannedTripsUser(user))
} else {
Outcome::Error((Status::Unauthorized, LoginError::NotACox))
Outcome::Error((Status::Forbidden, LoginError::NotACox))
}
}
Outcome::Error(f) => Outcome::Error(f),
Outcome::Forward(f) => Outcome::Forward(f),
}
}
}
impl Into<User> for AllowedForPlannedTripsUser {
fn into(self) -> User {
self.0
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DonauLinzUser(pub(crate) User);
impl Into<User> for DonauLinzUser {
fn into(self) -> User {
self.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 {
Outcome::Success(DonauLinzUser(user))
} else {
Outcome::Error((Status::Forbidden, LoginError::NotACox))
}
}
Outcome::Error(f) => Outcome::Error(f),

View File

@ -50,9 +50,9 @@ async fn update(
) -> Flash<Redirect> {
let d = data.into_inner();
if Mail::send(db, d, config.smtp_pw.clone()).await {
return Flash::success(Redirect::to("/admin/mail"), "Mail versendet");
Flash::success(Redirect::to("/admin/mail"), "Mail versendet")
} else {
return Flash::error(Redirect::to("/admin/mail"), "Fehler");
Flash::error(Redirect::to("/admin/mail"), "Fehler")
}
}

View File

@ -13,7 +13,7 @@ use crate::{
model::{
boat::Boat,
boatdamage::{BoatDamage, BoatDamageFixed, BoatDamageToAdd, BoatDamageVerified},
user::{CoxUser, NonGuestUser, TechUser, User, UserWithRoles},
user::{CoxUser, DonauLinzUser, TechUser, User, UserWithRoles},
},
tera::log::KioskCookie,
};
@ -45,7 +45,7 @@ async fn index_kiosk(
async fn index(
db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>,
user: NonGuestUser,
user: DonauLinzUser,
) -> Template {
let boatdamages = BoatDamage::all(db).await;
let boats = Boat::all(db).await;
@ -59,7 +59,7 @@ async fn index(
context.insert("boats", &boats);
context.insert(
"loggedin_user",
&UserWithRoles::from_user(user.user, db).await,
&UserWithRoles::from_user(user.into(), db).await,
);
Template::render("boatdamages", context.into_json())
@ -76,13 +76,14 @@ pub struct FormBoatDamageToAdd<'r> {
async fn create<'r>(
db: &State<SqlitePool>,
data: Form<FormBoatDamageToAdd<'r>>,
user: NonGuestUser,
user: DonauLinzUser,
) -> Flash<Redirect> {
let user: User = user.into();
let boatdamage_to_add = BoatDamageToAdd {
boat_id: data.boat_id,
desc: data.desc,
lock_boat: data.lock_boat,
user_id_created: user.user.id as i32,
user_id_created: user.id as i32,
};
match BoatDamage::create(db, boatdamage_to_add).await {
Ok(_) => Flash::success(

View File

@ -391,7 +391,7 @@ mod test {
.body("name=cox&password=cox"); // Add the form data to the request body;
login.dispatch().await;
let req = client.get("/join/1");
let req = client.get("/planned/join/1");
let _ = req.dispatch().await;
let req = client.get("/cox/join/1");

View File

@ -23,7 +23,7 @@ use crate::model::{
LogbookUpdateError,
},
logtype::LogType,
user::{NonGuestUser, User, UserWithRoles, UserWithWaterStatus},
user::{DonauLinzUser, User, UserWithRoles, UserWithWaterStatus},
};
pub struct KioskCookie(String);
@ -44,9 +44,9 @@ impl<'r> FromRequest<'r> for KioskCookie {
async fn index(
db: &State<SqlitePool>,
flash: Option<FlashMessage<'_>>,
user: NonGuestUser,
user: DonauLinzUser,
) -> Template {
let boats = Boat::for_user(db, &user.user).await;
let boats = Boat::for_user(db, &user).await;
let coxes: Vec<UserWithWaterStatus> = futures::future::join_all(
User::cox(db)
@ -78,7 +78,7 @@ async fn index(
context.insert("logtypes", &logtypes);
context.insert(
"loggedin_user",
&UserWithRoles::from_user(user.user, db).await,
&UserWithRoles::from_user(user.into(), db).await,
);
context.insert("on_water", &on_water);
context.insert("distances", &distances);
@ -87,12 +87,12 @@ async fn index(
}
#[get("/show", rank = 2)]
async fn show(db: &State<SqlitePool>, user: NonGuestUser) -> Template {
async fn show(db: &State<SqlitePool>, user: DonauLinzUser) -> Template {
let logs = Logbook::completed(db).await;
Template::render(
"log.completed",
context!(logs, loggedin_user: &UserWithRoles::from_user(user.user, db).await),
context!(logs, loggedin_user: &UserWithRoles::from_user(user.into(), db).await),
)
}
@ -166,12 +166,12 @@ async fn kiosk(
async fn create_logbook(
db: &SqlitePool,
data: Form<LogToAdd>,
user: &NonGuestUser,
user: &DonauLinzUser,
) -> Flash<Redirect> {
match Logbook::create(
db,
data.into_inner(),
&user.user
&user
)
.await
{
@ -197,14 +197,11 @@ async fn create_logbook(
async fn create(
db: &State<SqlitePool>,
data: Form<LogToAdd>,
user: NonGuestUser,
user: DonauLinzUser,
) -> Flash<Redirect> {
Log::create(
db,
format!(
"User {} tries to create log entry={:?}",
user.user.name, data
),
format!("User {} tries to create log entry={:?}", &user.name, data),
)
.await;
@ -238,14 +235,14 @@ async fn create_kiosk(
)
.await;
create_logbook(db, data, &NonGuestUser { user: creator }).await //TODO: fixme
create_logbook(db, data, &DonauLinzUser(creator)).await //TODO: fixme
}
async fn home_logbook(
db: &SqlitePool,
data: Form<LogToFinalize>,
logbook_id: i32,
user: &NonGuestUser,
user: &DonauLinzUser,
) -> Flash<Redirect> {
let logbook: Option<Logbook> = Logbook::find_by_id(db, logbook_id).await;
let Some(logbook) = logbook else {
@ -255,7 +252,7 @@ async fn home_logbook(
);
};
match logbook.home(db, &user.user, data.into_inner()).await {
match logbook.home(db,user, data.into_inner()).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)."),
@ -285,11 +282,11 @@ async fn home_kiosk(
db,
data,
logbook_id,
&NonGuestUser {
user: User::find_by_id(db, logbook.shipmaster as i32)
&DonauLinzUser(
User::find_by_id(db, logbook.shipmaster as i32)
.await
.unwrap(), //TODO: fixme
},
.unwrap(),
), //TODO: fixme
)
.await
}
@ -299,13 +296,13 @@ async fn home(
db: &State<SqlitePool>,
data: Form<LogToFinalize>,
logbook_id: i32,
user: NonGuestUser,
user: DonauLinzUser,
) -> Flash<Redirect> {
Log::create(
db,
format!(
"User {} tries to finish log entry {logbook_id} {data:?}",
user.user.name
&user.name
),
)
.await;
@ -314,12 +311,12 @@ async fn home(
}
#[get("/<logbook_id>/delete", rank = 2)]
async fn delete(db: &State<SqlitePool>, logbook_id: i32, user: User) -> Flash<Redirect> {
async fn delete(db: &State<SqlitePool>, logbook_id: i32, user: DonauLinzUser) -> Flash<Redirect> {
let logbook = Logbook::find_by_id(db, logbook_id).await;
if let Some(logbook) = logbook {
Log::create(
db,
format!("User {} tries to delete log entry {logbook_id}", user.name),
format!("User {} tries to delete log entry {logbook_id}", &user.name),
)
.await;
match logbook.delete(db, &user).await {

View File

@ -8,17 +8,12 @@ use rocket::{
response::{Flash, Redirect},
routes, Build, FromForm, Rocket, State,
};
use rocket_dyn_templates::{tera::Context, Template};
use rocket_dyn_templates::Template;
use serde::Deserialize;
use sqlx::SqlitePool;
use tera::Context;
use crate::model::{
log::Log,
tripdetails::TripDetails,
triptype::TripType,
user::{User, UserWithRoles},
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
};
use crate::model::user::{User, UserWithRoles};
pub(crate) mod admin;
mod auth;
@ -27,6 +22,7 @@ mod cox;
mod ergo;
mod log;
mod misc;
mod planned;
mod stat;
#[derive(FromForm, Debug)]
@ -35,6 +31,16 @@ struct LoginForm<'r> {
password: &'r str,
}
#[get("/")]
async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
let mut context = Context::new();
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
Template::render("index", context.into_json())
}
#[post("/", data = "<login>")]
async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String {
match User::login(db, login.name, login.password).await {
@ -43,164 +49,16 @@ async fn wikiauth(db: &State<SqlitePool>, login: Form<LoginForm<'_>>) -> String
}
}
#[get("/")]
async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template {
let mut context = Context::new();
if user.has_role(db, "cox").await || user.has_role(db, "admin").await {
let triptypes = TripType::all(db).await;
context.insert("trip_types", &triptypes);
}
let days = user.get_days(db).await;
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
context.insert("days", &days);
Template::render("index", context.into_json())
}
#[get("/join/<trip_details_id>?<user_note>")]
async fn join(
db: &State<SqlitePool>,
trip_details_id: i64,
user: User,
user_note: Option<String>,
) -> Flash<Redirect> {
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/"), "Trip_details do not exist.");
};
match UserTrip::create(db, &user, &trip_details, user_note).await {
Ok(_) => {
Log::create(
db,
format!(
"User {} registered for trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::success(Redirect::to("/"), "Erfolgreich angemeldet!")
}
Err(UserTripError::EventAlreadyFull) => {
Flash::error(Redirect::to("/"), "Event bereits ausgebucht!")
}
Err(UserTripError::AlreadyRegistered) => {
Flash::error(Redirect::to("/"), "Du nimmst bereits teil!")
}
Err(UserTripError::AlreadyRegisteredAsCox) => {
Flash::error(Redirect::to("/"), "Du hilfst bereits als Steuerperson aus!")
}
Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error(
Redirect::to("/"),
"Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)",
),
Err(UserTripError::GuestNotAllowedForThisEvent) => Flash::error(
Redirect::to("/"),
"Bei dieser Ausfahrt können leider keine Gäste mitfahren.",
),
Err(UserTripError::NotAllowedToAddGuest) => Flash::error(
Redirect::to("/"),
"Du darfst keine Gäste hinzufügen.",
),
Err(UserTripError::DetailsLocked) => Flash::error(
Redirect::to("/"),
"Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.",
),
}
}
#[get("/remove/<trip_details_id>/<name>")]
async fn remove_guest(
db: &State<SqlitePool>,
trip_details_id: i64,
user: User,
name: String,
) -> Flash<Redirect> {
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/"), "TripDetailsId does not exist");
};
match UserTrip::delete(db, &user, &trip_details, Some(name)).await {
Ok(_) => {
Log::create(
db,
format!(
"User {} unregistered for trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!")
}
Err(UserTripDeleteError::DetailsLocked) => {
Log::create(
db,
format!(
"User {} tried to unregister for locked trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::error(Redirect::to("/"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
}
Err(UserTripDeleteError::GuestNotParticipating) => {
Flash::error(Redirect::to("/"), "Gast nicht angemeldet.")
}
Err(UserTripDeleteError::NotAllowedToDeleteGuest) => Flash::error(
Redirect::to("/"),
"Keine Berechtigung um den Gast zu entfernen.",
),
}
}
#[get("/remove/<trip_details_id>")]
async fn remove(db: &State<SqlitePool>, trip_details_id: i64, user: User) -> Flash<Redirect> {
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/"), "TripDetailsId does not exist");
};
match UserTrip::delete(db, &user, &trip_details, None).await {
Ok(_) => {
Log::create(
db,
format!(
"User {} unregistered for trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!")
}
Err(UserTripDeleteError::DetailsLocked) => {
Log::create(
db,
format!(
"User {} tried to unregister for locked trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::error(Redirect::to("/"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
}
Err(_) => {
panic!("Not possible to be here");
}
}
}
#[catch(401)] //unauthorized
#[catch(401)] //Unauthorized
fn unauthorized_error() -> Redirect {
Redirect::to("/auth")
}
#[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.")
}
#[derive(Deserialize)]
#[serde(crate = "rocket::serde")]
pub struct Config {
@ -210,10 +68,11 @@ pub struct Config {
pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
rocket
.mount("/", routes![index, join, remove, remove_guest])
.mount("/", routes![index])
.mount("/auth", auth::routes())
.mount("/wikiauth", routes![wikiauth])
.mount("/log", log::routes())
.mount("/planned", planned::routes())
.mount("/ergo", ergo::routes())
.mount("/stat", stat::routes())
.mount("/boatdamage", boatdamage::routes())
@ -221,7 +80,7 @@ pub fn config(rocket: Rocket<Build>) -> Rocket<Build> {
.mount("/admin", admin::routes())
.mount("/", misc::routes())
.mount("/public", FileServer::from("static/"))
.register("/", catchers![unauthorized_error])
.register("/", catchers![unauthorized_error, forbidden_error])
.attach(Template::fairing())
.attach(AdHoc::config::<Config>())
}
@ -255,7 +114,11 @@ mod test {
assert_eq!(response.status(), Status::Ok);
assert!(response.into_string().await.unwrap().contains("Ausfahrten"));
assert!(response
.into_string()
.await
.unwrap()
.contains("Ruderassistent"));
}
#[sqlx::test]
@ -274,75 +137,6 @@ mod test {
assert_eq!(response.headers().get("Location").next(), Some("/auth"));
}
#[sqlx::test]
fn test_join_and_remove() {
let db = testdb!();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
let client = Client::tracked(rocket).await.unwrap();
let login = client
.post("/auth")
.header(ContentType::Form) // Set the content type to form
.body("name=rower&password=rower"); // Add the form data to the request body;
login.dispatch().await;
let req = client.get("/join/1");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "7:successErfolgreich angemeldet!");
let req = client.get("/remove/1");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "7:successErfolgreich abgemeldet!");
}
#[sqlx::test]
fn test_join_invalid_event() {
let db = testdb!();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
let client = Client::tracked(rocket).await.unwrap();
let login = client
.post("/auth")
.header(ContentType::Form) // Set the content type to form
.body("name=rower&password=rower"); // Add the form data to the request body;
login.dispatch().await;
let req = client.get("/join/9999");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "5:errorTrip_details do not exist.");
}
#[sqlx::test]
fn test_public() {
let db = testdb!();

270
src/tera/planned.rs Normal file
View File

@ -0,0 +1,270 @@
use rocket::{
get,
request::FlashMessage,
response::{Flash, Redirect},
routes, Route, State,
};
use rocket_dyn_templates::Template;
use sqlx::SqlitePool;
use tera::Context;
use crate::model::{
log::Log,
tripdetails::TripDetails,
triptype::TripType,
user::{AllowedForPlannedTripsUser, User, UserWithRoles},
usertrip::{UserTrip, UserTripDeleteError, UserTripError},
};
#[get("/")]
async fn index(
db: &State<SqlitePool>,
user: AllowedForPlannedTripsUser,
flash: Option<FlashMessage<'_>>,
) -> Template {
let user: User = user.into();
let mut context = Context::new();
if user.has_role(db, "cox").await || user.has_role(db, "admin").await {
let triptypes = TripType::all(db).await;
context.insert("trip_types", &triptypes);
}
let days = user.get_days(db).await;
if let Some(msg) = flash {
context.insert("flash", &msg.into_inner());
}
context.insert("loggedin_user", &UserWithRoles::from_user(user, db).await);
context.insert("days", &days);
Template::render("planned", context.into_json())
}
#[get("/join/<trip_details_id>?<user_note>")]
async fn join(
db: &State<SqlitePool>,
trip_details_id: i64,
user: AllowedForPlannedTripsUser,
user_note: Option<String>,
) -> Flash<Redirect> {
let user: User = user.into();
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/"), "Trip_details do not exist.");
};
match UserTrip::create(db, &user, &trip_details, user_note).await {
Ok(_) => {
Log::create(
db,
format!(
"User {} registered for trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::success(Redirect::to("/"), "Erfolgreich angemeldet!")
}
Err(UserTripError::EventAlreadyFull) => {
Flash::error(Redirect::to("/"), "Event bereits ausgebucht!")
}
Err(UserTripError::AlreadyRegistered) => {
Flash::error(Redirect::to("/"), "Du nimmst bereits teil!")
}
Err(UserTripError::AlreadyRegisteredAsCox) => {
Flash::error(Redirect::to("/"), "Du hilfst bereits als Steuerperson aus!")
}
Err(UserTripError::CantRegisterAtOwnEvent) => Flash::error(
Redirect::to("/"),
"Du kannst bei einer selbst ausgeschriebenen Fahrt nicht mitrudern ;)",
),
Err(UserTripError::GuestNotAllowedForThisEvent) => Flash::error(
Redirect::to("/"),
"Bei dieser Ausfahrt können leider keine Gäste mitfahren.",
),
Err(UserTripError::NotAllowedToAddGuest) => Flash::error(
Redirect::to("/"),
"Du darfst keine Gäste hinzufügen.",
),
Err(UserTripError::DetailsLocked) => Flash::error(
Redirect::to("/"),
"Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.",
),
}
}
#[get("/remove/<trip_details_id>/<name>")]
async fn remove_guest(
db: &State<SqlitePool>,
trip_details_id: i64,
user: AllowedForPlannedTripsUser,
name: String,
) -> Flash<Redirect> {
let user: User = user.into();
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/"), "TripDetailsId does not exist");
};
match UserTrip::delete(db, &user, &trip_details, Some(name)).await {
Ok(_) => {
Log::create(
db,
format!(
"User {} unregistered for trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!")
}
Err(UserTripDeleteError::DetailsLocked) => {
Log::create(
db,
format!(
"User {} tried to unregister for locked trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::error(Redirect::to("/"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
}
Err(UserTripDeleteError::GuestNotParticipating) => {
Flash::error(Redirect::to("/"), "Gast nicht angemeldet.")
}
Err(UserTripDeleteError::NotAllowedToDeleteGuest) => Flash::error(
Redirect::to("/"),
"Keine Berechtigung um den Gast zu entfernen.",
),
}
}
#[get("/remove/<trip_details_id>")]
async fn remove(
db: &State<SqlitePool>,
trip_details_id: i64,
user: AllowedForPlannedTripsUser,
) -> Flash<Redirect> {
let user: User = user.into();
let Some(trip_details) = TripDetails::find_by_id(db, trip_details_id).await else {
return Flash::error(Redirect::to("/"), "TripDetailsId does not exist");
};
match UserTrip::delete(db, &user, &trip_details, None).await {
Ok(_) => {
Log::create(
db,
format!(
"User {} unregistered for trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::success(Redirect::to("/"), "Erfolgreich abgemeldet!")
}
Err(UserTripDeleteError::DetailsLocked) => {
Log::create(
db,
format!(
"User {} tried to unregister for locked trip_details.id={}",
user.name, trip_details_id
),
)
.await;
Flash::error(Redirect::to("/"), "Das Boot ist bereits eingeteilt. Bitte kontaktiere den Schiffsführer (Nummern siehe Signalgruppe) falls du dich doch abmelden willst.")
}
Err(_) => {
panic!("Not possible to be here");
}
}
}
pub fn routes() -> Vec<Route> {
routes![index, join, remove, remove_guest]
}
#[cfg(test)]
mod test {
use rocket::{
http::{ContentType, Status},
local::asynchronous::Client,
};
use sqlx::SqlitePool;
use crate::testdb;
#[sqlx::test]
fn test_join_and_remove() {
let db = testdb!();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
let client = Client::tracked(rocket).await.unwrap();
let login = client
.post("/auth")
.header(ContentType::Form) // Set the content type to form
.body("name=rower&password=rower"); // Add the form data to the request body;
login.dispatch().await;
let req = client.get("/planned/join/1");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "7:successErfolgreich angemeldet!");
let req = client.get("/planned/remove/1");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "7:successErfolgreich abgemeldet!");
}
#[sqlx::test]
fn test_join_invalid_event() {
let db = testdb!();
let rocket = rocket::build().manage(db.clone());
let rocket = crate::tera::config(rocket);
let client = Client::tracked(rocket).await.unwrap();
let login = client
.post("/auth")
.header(ContentType::Form) // Set the content type to form
.body("name=rower&password=rower"); // Add the form data to the request body;
login.dispatch().await;
let req = client.get("/planned/join/9999");
let response = req.dispatch().await;
assert_eq!(response.status(), Status::SeeOther);
assert_eq!(response.headers().get("Location").next(), Some("/"));
let flash_cookie = response
.cookies()
.get("_flash")
.expect("Expected flash cookie");
assert_eq!(flash_cookie.value(), "5:errorTrip_details do not exist.");
}
}

View File

@ -4,19 +4,19 @@ use sqlx::SqlitePool;
use crate::model::{
stat::{self, Stat},
user::{NonGuestUser, UserWithRoles},
user::{DonauLinzUser, UserWithRoles},
};
use super::log::KioskCookie;
#[get("/boats?<year>", rank = 2)]
async fn index_boat(db: &State<SqlitePool>, user: NonGuestUser, year: Option<i32>) -> Template {
async fn index_boat(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -> Template {
let stat = Stat::boats(db, year).await;
let kiosk = false;
Template::render(
"stat.boats",
context!(loggedin_user: &UserWithRoles::from_user(user.user, db).await, stat, kiosk),
context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, kiosk),
)
}
@ -33,15 +33,15 @@ async fn index_boat_kiosk(
}
#[get("/?<year>", rank = 2)]
async fn index(db: &State<SqlitePool>, user: NonGuestUser, year: Option<i32>) -> Template {
async fn index(db: &State<SqlitePool>, user: DonauLinzUser, year: Option<i32>) -> Template {
let stat = Stat::people(db, year).await;
let guest_km = Stat::guest(db, year).await;
let personal = stat::get_personal(db, &user.user).await;
let personal = stat::get_personal(db, &user).await;
let kiosk = false;
Template::render(
"stat.people",
context!(loggedin_user: &UserWithRoles::from_user(user.user, db).await, stat, personal, kiosk, guest_km),
context!(loggedin_user: &UserWithRoles::from_user(user.into(), db).await, stat, personal, kiosk, guest_km),
)
}

View File

@ -10,7 +10,7 @@
</a>
</div>
<div>
<div><!--
<a
href="https://wiki.rudernlinz.at/ruderassistent#faq"
target="_blank"
@ -121,7 +121,7 @@
</svg>
<span class="sr-only">Userverwaltung</span>
</a>
{% endif %}
{% endif %}-->
<a
href="/auth/logout"
class="inline-flex justify-center rounded-md bg-gray-200 ml-1 px-3 py-2 text-sm font-semibold text-primary-950 hover:bg-gray-300 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"

View File

@ -3,292 +3,86 @@
{% extends "base" %}
{% block content %}
<div class="max-w-screen-xl w-full grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
<div class="max-w-screen-lg w-full">
{% if flash %}
{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }}
{% endif %}
<h1 class="h1">Ruderassistent</h1>
<h1 class="h1 sm:col-span-2 lg:col-span-3">Ausfahrten</h1>
{% include "includes/buttons" %}
{% for day in days %}
{% set amount_trips = day.planned_events | length + day.trips | length %}
{% set_global day_cox_needed = false %}
{% if day.planned_events | length > 0 %}
{% for planned_event in day.planned_events %}
{% if planned_event.cox_needed %}
{% set_global day_cox_needed = true %}
{% endif %}
{% endfor %}
{% endif %}
<div 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') }}" data-coxneeded="{{ day_cox_needed }}">
<div>
<h2 class="font-bold uppercase tracking-wide text-center rounded-t-md {% if day.is_pinned %} text-white bg-primary-950 {% else %} text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 {% endif %} text-lg px-3 py-3 ">{{ day.day| date(format="%d.%m.%Y") }}
<small class="inline-block ml-1 text-xs {% if day.is_pinned %} text-gray-200 {% else %} text-gray-500 dark:text-gray-100 {% endif %}">{{ day.day | date(format="%A", locale="de_AT") }}</small>
</h2>
{% if day.planned_events | length > 0 or day.trips | length > 0 %}
<div
class="grid grid-cols-1 gap-3 mb-3">
{# --- START Events --- #}
{% if day.planned_events | length > 0 %}
{% for planned_event in day.planned_events | sort(attribute="planned_starting_time") %}
{% set amount_cur_cox = planned_event.cox | length %}
{% set amount_cox_missing = planned_event.planned_amount_cox - amount_cur_cox %}
<div class="pt-2 px-3 border-t border-gray-200" style="order: {{ planned_event.planned_starting_time | replace(from=":", to="") }}">
<div class="flex justify-between items-center">
<div class="mr-1">
<strong class="text-primary-900 dark:text-white">
{{ planned_event.planned_starting_time }}
Uhr
</strong>
<small class="text-gray-600 dark:text-gray-100">({{ planned_event.name }}{% if planned_event.trip_type %} - {{ planned_event.trip_type.icon | safe }} {{ planned_event.trip_type.name }}{% endif %})</small><br/>
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{{ planned_event.planned_starting_time }} Uhr</strong> ({{ planned_event.name }}){% if planned_event.trip_type %}<small class='block'>{{ planned_event.trip_type.desc }}</small>{% endif %}{% if planned_event.notes %}<small class='block'>{{ planned_event.notes }}</small>{% endif %}" data-body="#event{{ planned_event.trip_details_id }}" class="inline-block link-primary mr-3">
Details
</a>
<div class="grid gap-3">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
<h2 class="h2">Allgemein</h2>
<div class="text-sm p-3">
<ul class="list-disc ms-2">
<li class="py-1"><a href="https://rudernlinz.at/termin" target="_blank" class="link-primary">FAQ (extern)</a></li>
</ul>
</div>
<div
class="text-right grid gap-2">
{# --- START Row Buttons --- #}
{% set_global cur_user_participates = false %}
{% for rower in planned_event.rower%}
{% if rower.name == loggedin_user.name %}
{% set_global cur_user_participates = true %}
{% endif %}
{% endfor %}
{% if cur_user_participates %}
<a href="/remove/{{ planned_event.trip_details_id }}" class="btn btn-attention btn-fw">Abmelden</a>
{% endif %}
{% if planned_event.max_people > planned_event.rower | length %}
{% if cur_user_participates == false %}
<a href="/join/{{ planned_event.trip_details_id }}" class="btn btn-primary btn-fw" {% if planned_event.trip_type %} onclick="return confirm('{{ planned_event.trip_type.question }}');" {% endif %}>Mitrudern</a>
{% endif %}
{% endif %}
{# --- END Row Buttons --- #}
{# --- START Cox Buttons --- #}
{% if "cox" in loggedin_user.roles %}
{% set_global cur_user_participates = false %}
{% for cox in planned_event.cox %}
{% if cox.name == loggedin_user.name %}
{% set_global cur_user_participates = true %}
{% endif %}
{% endfor %}
{% if cur_user_participates %}
<a href="/cox/remove/{{ planned_event.id }}" class="block btn btn-attention btn-fw">
{% include "includes/cox-icon" %}
Abmelden
</a>
{% else %}
<a href="/cox/join/{{ planned_event.id }}" class="block btn {% if amount_cox_missing > 0 %} btn-dark {% else %} btn-gray {% endif %} btn-fw" {% if planned_event.trip_type %} onclick="return confirm('{{ planned_event.trip_type.question }}');" {% endif %}>
{% include "includes/cox-icon" %}
Steuern
</a>
{% endif %}
{% endif %}
{# --- END Cox Buttons --- #}
</div>
</div>
{# --- START Sidebar Content --- #}
<div class="hidden">
<div
id="event{{ planned_event.trip_details_id }}">
{# --- START List Coxes --- #}
{% if planned_event.planned_amount_cox > 0 %}
{% if amount_cox_missing > 0 %}
{{ macros::box(participants=planned_event.cox, empty_seats=planned_event.planned_amount_cox - amount_cur_cox, header='Noch benötigte Steuerleute:', text='Keine Steuerleute angemeldet') }}
{% else %}
{{ macros::box(participants=planned_event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }}
{% if loggedin_user.weight and loggedin_user.sex and loggedin_user.dob %}
<div class="grid gap-3">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
<h2 class="h2">Ergo</h2>
<div class="text-sm p-3">
<ul class="list-disc ms-2">
<li class="py-1"><a href="/ergo" class="link-primary">Ergo</a></li>
</ul>
</div>
</div>
</div>
{% endif %}
{% endif %}
{# --- END List Coxes --- #}
{# --- START List Rowers --- #}
{% if planned_event.max_people > 0 %}
{% set amount_cur_rower = planned_event.rower | length %}
{{ macros::box(participants=planned_event.rower, empty_seats=planned_event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=planned_event.trip_details_id, allow_removing="admin" in loggedin_user.roles) }}
{% endif %}
{# --- END List Rowers --- #}
{% if "admin" in loggedin_user.roles %}
<form action="/join/{{ planned_event.trip_details_id }}" method="get" />
{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }}
<input value="Gast hinzufügen" class="btn btn-primary w-full rounded-t-none-important" type="submit"/>
</form>
{% if "Donau Linz" in loggedin_user.roles %}
<div class="grid gap-3">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
<h2 class="h2">Vereinsmitglieder</h2>
<div class="text-sm p-3">
<ul class="list-disc ms-2">
<li class="py-1"><a href="/planned" class="link-primary">Geplante Ausfahrten</a></li>
<li class="py-1"><a href="/log" class="link-primary">Ausfahrt eintragen</a></li>
<li class="py-1"><a href="/log/show" class="link-primary">Logbuch</a></li>
<li class="py-1"><a href="/stat" class="link-primary">Statstik</a></li>
<li class="py-1"><a href="/stat/boats" class="link-primary">Bootsauswertung</a></li>
<li class="py-1"><a href="/boatdamage" class="link-primary">Bootsschaden</a></li>
</ul>
</div>
</div>
</div>
{% endif %}
{% if planned_event.allow_guests %}
<div class="text-primary-900 bg-primary-50 text-center p-1 mb-4">Gäste willkommen!</div>
{% if "scheckbuch" in loggedin_user.roles %}
<div class="grid gap-3">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
<h2 class="h2">Scheckbuch</h2>
<div class="text-sm p-3">
<ul class="list-disc ms-2">
<li class="py-1"><a href="/planned" class="link-primary">Geplante Ausfahrten</a></li>
</ul>
</div>
</div>
</div>
{% endif %}
{% if "admin" in loggedin_user.roles %}
{# --- START Edit Form --- #}
<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">Ausfahrt bearbeiten</h3>
<form action="/admin/planned-event" method="post" class="grid gap-3">
<input type="hidden" name="_method" value="put"/>
<input type="hidden" name="id" value="{{ planned_event.id }}"/>
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=planned_event.max_people, min='0') }}
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', value=planned_event.planned_amount_cox, required=true, min='0') }}
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=planned_event.id,checked=planned_event.always_show) }}
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=planned_event.id,checked=planned_event.is_locked) }}
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=planned_event.notes) }}
<input value="Speichern" class="btn btn-primary" type="submit"/>
</form>
</div>
{# --- END Edit Form --- #}
{# --- START Delete Btn --- #}
<div class="text-right">
<a href="/admin/planned-event/{{ planned_event.id }}/delete" class="inline-block btn btn-alert">
{% include "includes/delete-icon" %}
Termin löschen
</a>
</div>
{% endif %}
{# --- END Delete Btn --- #}
<div class="grid gap-3">
<div class="bg-white dark:bg-primary-900 text-black dark:text-white rounded-md block shadow mt-5" role="alert">
<h2 class="h2">Admin</h2>
<div class="text-sm p-3">
<ul class="list-disc ms-2">
<li class="py-1"><a href="/admin/boat" class="link-primary">Boote</a></li>
<li class="py-1"><a href="/admin/user" class="link-primary">User</a></li>
<li class="py-1"><a href="/admin/mail" class="link-primary">Mail</a></li>
</ul>
</div>
</div>
{# --- END Sidebar Content --- #}
</div>
{% endfor %}
{% endif %}
{# --- END Events --- #}
{# --- START Trips --- #}
{% if day.trips | length > 0 %}
{% for trip in day.trips | sort(attribute="planned_starting_time") %}
<div class="pt-2 px-3 reset-js border-t border-gray-200" style="order: {{ trip.planned_starting_time | replace(from=":", to="") }}" data-coxneeded="false">
<div class="flex justify-between items-center">
<div class="mr-1">
{% if trip.max_people == 0 %}
<strong class="text-[#f43f5e]">&#9888;
{{ trip.planned_starting_time }}
Uhr</strong>
<small class="text-[#f43f5e]">(Absage
{{ trip.cox_name }}
{% if trip.trip_type %}
-
{{ trip.trip_type.icon | safe }}{{ trip.trip_type.name }}
{% endif %})</small>
{% else %}
<strong class="text-primary-900 dark:text-white">{{ trip.planned_starting_time }}
Uhr</strong>
<small class="text-gray-600 dark:text-gray-100">({{ trip.cox_name }}{% if trip.trip_type %} - {{ trip.trip_type.icon | safe }} {{ trip.trip_type.name }}{% endif %})</small>
{% endif %}
<br/>
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{% if trip.max_people == 0 %}&#9888; {% endif %}{{ trip.planned_starting_time }} Uhr</strong> ({{ trip.cox_name }}){% if trip.trip_type %}<small class='block'>{{ trip.trip_type.desc }}</small>{% endif %}{% if trip.notes %}<small class='block'>{{ trip.notes }}</small>{% endif %}" data-body="#trip{{ trip.trip_details_id }}" class="inline-block link-primary mr-3">
Details
</a>
</div>
<div>
{% set_global cur_user_participates = false %}
{% for rower in trip.rower %}
{% if rower.name == loggedin_user.name %}
{% set_global cur_user_participates = true %}
{% endif %}
{% endfor %}
{% if cur_user_participates %}
<a href="/remove/{{ trip.trip_details_id }}" class="btn btn-attention btn-fw">Abmelden</a>
{% endif %}
{% if trip.max_people > trip.rower | length and trip.cox_id != loggedin_user.id and cur_user_participates == false%}
<a href="/join/{{ trip.trip_details_id }}" class="btn btn-primary btn-fw" {% if trip.trip_type %} onclick="return confirm('{{ trip.trip_type.question }}');" {% endif %}>Mitrudern</a>
{% endif %}
</div>
</div>
{# --- START Sidebar Content --- #}
<div class="hidden">
<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') }}
{% 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) }}
{% if trip.cox_id == loggedin_user.id %}
<form action="/join/{{ trip.trip_details_id }}" method="get" />
{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }}
<input value="Gast hinzufügen" class="btn btn-primary w-full rounded-t-none-important" type="submit"/>
</form>
{% endif %}
{% endif %}
{# --- START Edit Form --- #}
{% if trip.cox_id == loggedin_user.id %}
<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">Ausfahrt bearbeiten</h3>
<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='0') }}
{{ 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"/>
</form>
</div>
{% if trip.rower | length == 0 %}
<div class="text-right mt-6">
<a href="/cox/remove/trip/{{ trip.id }}" class="inline-block btn btn-alert">
{% include "includes/delete-icon" %}
Termin löschen
</a>
</div>
{% endif %}
{% endif %}
{# --- END Edit Form --- #}
</div>
</div>
{# --- END Sidebar Content --- #}
</div>
{% endfor %}
{% endif %}
{# --- END Trips --- #}
</div>
{% endif %}
</div>
{# --- START Add Buttons --- #}
{% if "admin" in loggedin_user.roles or "cox" in loggedin_user.roles %}
<div class="grid {% if "admin" in loggedin_user.roles %} grid-cols-2 {% endif %} text-center">
{% if "admin" 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">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
{% include "includes/plus-icon" %}
</span>
Event
</a>
{% endif %}
{% if "cox" in loggedin_user.roles %}
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Ausfahrt</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#sidebarForm" class="relative inline-block w-full py-2 text-primary-900 hover:text-primary-950 dark:bg-primary-600 dark:text-white dark:hover:bg-primary-500 dark:hover:text-white focus:text-primary-950 text-sm font-semibold bg-gray-100 hover:bg-gray-200 focus:bg-gray-200 {% if "admin" in loggedin_user.roles %} rounded-br-md {% else %} rounded-b-md {% endif %}">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
{% include "includes/plus-icon" %}
</span>
Ausfahrt
</a>
{% endif %}
</div>
{% endif %}
{# --- END Add Buttons --- #}
</div>
{% endfor %}
</div>
</div>
{% if "cox" in loggedin_user.roles %}
{% include "forms/trip" %}
{% endif %}
{% if "admin" in loggedin_user.roles %}
{% include "forms/event" %}
{% endif %}{% endblock content %}
{% endblock content%}

294
templates/planned.html.tera Normal file
View File

@ -0,0 +1,294 @@
{% import "includes/macros" as macros %}
{% extends "base" %}
{% block content %}
<div class="max-w-screen-xl w-full grid sm:grid-cols-2 lg:grid-cols-3 gap-4">
{% if flash %}
{{ macros::alert(message=flash.1, type=flash.0, class="sm:col-span-2 lg:col-span-3") }}
{% endif %}
<h1 class="h1 sm:col-span-2 lg:col-span-3">Ausfahrten</h1>
{% include "includes/buttons" %}
{% for day in days %}
{% set amount_trips = day.planned_events | length + day.trips | length %}
{% set_global day_cox_needed = false %}
{% if day.planned_events | length > 0 %}
{% for planned_event in day.planned_events %}
{% if planned_event.cox_needed %}
{% set_global day_cox_needed = true %}
{% endif %}
{% endfor %}
{% endif %}
<div 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') }}" data-coxneeded="{{ day_cox_needed }}">
<div>
<h2 class="font-bold uppercase tracking-wide text-center rounded-t-md {% if day.is_pinned %} text-white bg-primary-950 {% else %} text-primary-950 dark:text-white bg-gray-200 dark:bg-primary-950 bg-opacity-80 {% endif %} text-lg px-3 py-3 ">{{ day.day| date(format="%d.%m.%Y") }}
<small class="inline-block ml-1 text-xs {% if day.is_pinned %} text-gray-200 {% else %} text-gray-500 dark:text-gray-100 {% endif %}">{{ day.day | date(format="%A", locale="de_AT") }}</small>
</h2>
{% if day.planned_events | length > 0 or day.trips | length > 0 %}
<div
class="grid grid-cols-1 gap-3 mb-3">
{# --- START Events --- #}
{% if day.planned_events | length > 0 %}
{% for planned_event in day.planned_events | sort(attribute="planned_starting_time") %}
{% set amount_cur_cox = planned_event.cox | length %}
{% set amount_cox_missing = planned_event.planned_amount_cox - amount_cur_cox %}
<div class="pt-2 px-3 border-t border-gray-200" style="order: {{ planned_event.planned_starting_time | replace(from=":", to="") }}">
<div class="flex justify-between items-center">
<div class="mr-1">
<strong class="text-primary-900 dark:text-white">
{{ planned_event.planned_starting_time }}
Uhr
</strong>
<small class="text-gray-600 dark:text-gray-100">({{ planned_event.name }}{% if planned_event.trip_type %} - {{ planned_event.trip_type.icon | safe }} {{ planned_event.trip_type.name }}{% endif %})</small><br/>
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{{ planned_event.planned_starting_time }} Uhr</strong> ({{ planned_event.name }}){% if planned_event.trip_type %}<small class='block'>{{ planned_event.trip_type.desc }}</small>{% endif %}{% if planned_event.notes %}<small class='block'>{{ planned_event.notes }}</small>{% endif %}" data-body="#event{{ planned_event.trip_details_id }}" class="inline-block link-primary mr-3">
Details
</a>
</div>
<div
class="text-right grid gap-2">
{# --- START Row Buttons --- #}
{% set_global cur_user_participates = false %}
{% for rower in planned_event.rower%}
{% if rower.name == loggedin_user.name %}
{% set_global cur_user_participates = true %}
{% endif %}
{% endfor %}
{% if cur_user_participates %}
<a href="/planned/remove/{{ planned_event.trip_details_id }}" class="btn btn-attention btn-fw">Abmelden</a>
{% endif %}
{% if planned_event.max_people > planned_event.rower | length %}
{% if cur_user_participates == false %}
<a href="/planned/join/{{ planned_event.trip_details_id }}" class="btn btn-primary btn-fw" {% if planned_event.trip_type %} onclick="return confirm('{{ planned_event.trip_type.question }}');" {% endif %}>Mitrudern</a>
{% endif %}
{% endif %}
{# --- END Row Buttons --- #}
{# --- START Cox Buttons --- #}
{% if "cox" in loggedin_user.roles %}
{% set_global cur_user_participates = false %}
{% for cox in planned_event.cox %}
{% if cox.name == loggedin_user.name %}
{% set_global cur_user_participates = true %}
{% endif %}
{% endfor %}
{% if cur_user_participates %}
<a href="/cox/remove/{{ planned_event.id }}" class="block btn btn-attention btn-fw">
{% include "includes/cox-icon" %}
Abmelden
</a>
{% else %}
<a href="/cox/join/{{ planned_event.id }}" class="block btn {% if amount_cox_missing > 0 %} btn-dark {% else %} btn-gray {% endif %} btn-fw" {% if planned_event.trip_type %} onclick="return confirm('{{ planned_event.trip_type.question }}');" {% endif %}>
{% include "includes/cox-icon" %}
Steuern
</a>
{% endif %}
{% endif %}
{# --- END Cox Buttons --- #}
</div>
</div>
{# --- START Sidebar Content --- #}
<div class="hidden">
<div
id="event{{ planned_event.trip_details_id }}">
{# --- START List Coxes --- #}
{% if planned_event.planned_amount_cox > 0 %}
{% if amount_cox_missing > 0 %}
{{ macros::box(participants=planned_event.cox, empty_seats=planned_event.planned_amount_cox - amount_cur_cox, header='Noch benötigte Steuerleute:', text='Keine Steuerleute angemeldet') }}
{% else %}
{{ macros::box(participants=planned_event.cox, empty_seats="", header='Genügend Steuerleute haben sich angemeldet :-)', text='Keine Steuerleute angemeldet') }}
{% endif %}
{% endif %}
{# --- END List Coxes --- #}
{# --- START List Rowers --- #}
{% if planned_event.max_people > 0 %}
{% set amount_cur_rower = planned_event.rower | length %}
{{ macros::box(participants=planned_event.rower, empty_seats=planned_event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=planned_event.trip_details_id, allow_removing="admin" in loggedin_user.roles) }}
{% endif %}
{# --- END List Rowers --- #}
{% if "admin" in loggedin_user.roles %}
<form action="/join/{{ planned_event.trip_details_id }}" method="get" />
{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }}
<input value="Gast hinzufügen" class="btn btn-primary w-full rounded-t-none-important" type="submit"/>
</form>
{% endif %}
{% if planned_event.allow_guests %}
<div class="text-primary-900 bg-primary-50 text-center p-1 mb-4">Gäste willkommen!</div>
{% endif %}
{% if "admin" in loggedin_user.roles %}
{# --- START Edit Form --- #}
<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">Ausfahrt bearbeiten</h3>
<form action="/admin/planned-event" method="post" class="grid gap-3">
<input type="hidden" name="_method" value="put"/>
<input type="hidden" name="id" value="{{ planned_event.id }}"/>
{{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=planned_event.max_people, min='0') }}
{{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', value=planned_event.planned_amount_cox, required=true, min='0') }}
{{ macros::checkbox(label='Immer anzeigen', name='always_show', id=planned_event.id,checked=planned_event.always_show) }}
{{ macros::checkbox(label='Gesperrt', name='is_locked', id=planned_event.id,checked=planned_event.is_locked) }}
{{ macros::input(label='Anmerkungen', name='notes', type='input', value=planned_event.notes) }}
<input value="Speichern" class="btn btn-primary" type="submit"/>
</form>
</div>
{# --- END Edit Form --- #}
{# --- START Delete Btn --- #}
<div class="text-right">
<a href="/admin/planned-event/{{ planned_event.id }}/delete" class="inline-block btn btn-alert">
{% include "includes/delete-icon" %}
Termin löschen
</a>
</div>
{% endif %}
{# --- END Delete Btn --- #}
</div>
</div>
{# --- END Sidebar Content --- #}
</div>
{% endfor %}
{% endif %}
{# --- END Events --- #}
{# --- START Trips --- #}
{% if day.trips | length > 0 %}
{% for trip in day.trips | sort(attribute="planned_starting_time") %}
<div class="pt-2 px-3 reset-js border-t border-gray-200" style="order: {{ trip.planned_starting_time | replace(from=":", to="") }}" data-coxneeded="false">
<div class="flex justify-between items-center">
<div class="mr-1">
{% if trip.max_people == 0 %}
<strong class="text-[#f43f5e]">&#9888;
{{ trip.planned_starting_time }}
Uhr</strong>
<small class="text-[#f43f5e]">(Absage
{{ trip.cox_name }}
{% if trip.trip_type %}
-
{{ trip.trip_type.icon | safe }}{{ trip.trip_type.name }}
{% endif %})</small>
{% else %}
<strong class="text-primary-900 dark:text-white">{{ trip.planned_starting_time }}
Uhr</strong>
<small class="text-gray-600 dark:text-gray-100">({{ trip.cox_name }}{% if trip.trip_type %} - {{ trip.trip_type.icon | safe }} {{ trip.trip_type.name }}{% endif %})</small>
{% endif %}
<br/>
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>{% if trip.max_people == 0 %}&#9888; {% endif %}{{ trip.planned_starting_time }} Uhr</strong> ({{ trip.cox_name }}){% if trip.trip_type %}<small class='block'>{{ trip.trip_type.desc }}</small>{% endif %}{% if trip.notes %}<small class='block'>{{ trip.notes }}</small>{% endif %}" data-body="#trip{{ trip.trip_details_id }}" class="inline-block link-primary mr-3">
Details
</a>
</div>
<div>
{% set_global cur_user_participates = false %}
{% for rower in trip.rower %}
{% if rower.name == loggedin_user.name %}
{% set_global cur_user_participates = true %}
{% endif %}
{% endfor %}
{% if cur_user_participates %}
<a href="/remove/{{ trip.trip_details_id }}" class="btn btn-attention btn-fw">Abmelden</a>
{% endif %}
{% if trip.max_people > trip.rower | length and trip.cox_id != loggedin_user.id and cur_user_participates == false%}
<a href="/join/{{ trip.trip_details_id }}" class="btn btn-primary btn-fw" {% if trip.trip_type %} onclick="return confirm('{{ trip.trip_type.question }}');" {% endif %}>Mitrudern</a>
{% endif %}
</div>
</div>
{# --- START Sidebar Content --- #}
<div class="hidden">
<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') }}
{% 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) }}
{% if trip.cox_id == loggedin_user.id %}
<form action="/join/{{ trip.trip_details_id }}" method="get" />
{{ macros::input(label='Gast', class="input rounded-t", name='user_note', type='text', required=true) }}
<input value="Gast hinzufügen" class="btn btn-primary w-full rounded-t-none-important" type="submit"/>
</form>
{% endif %}
{% endif %}
{# --- START Edit Form --- #}
{% if trip.cox_id == loggedin_user.id %}
<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">Ausfahrt bearbeiten</h3>
<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='0') }}
{{ 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"/>
</form>
</div>
{% if trip.rower | length == 0 %}
<div class="text-right mt-6">
<a href="/cox/remove/trip/{{ trip.id }}" class="inline-block btn btn-alert">
{% include "includes/delete-icon" %}
Termin löschen
</a>
</div>
{% endif %}
{% endif %}
{# --- END Edit Form --- #}
</div>
</div>
{# --- END Sidebar Content --- #}
</div>
{% endfor %}
{% endif %}
{# --- END Trips --- #}
</div>
{% endif %}
</div>
{# --- START Add Buttons --- #}
{% if "admin" in loggedin_user.roles or "cox" in loggedin_user.roles %}
<div class="grid {% if "admin" in loggedin_user.roles %} grid-cols-2 {% endif %} text-center">
{% if "admin" 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">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
{% include "includes/plus-icon" %}
</span>
Event
</a>
{% endif %}
{% if "cox" in loggedin_user.roles %}
<a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>Ausfahrt</strong> am {{ day.day| date(format='%d.%m.%Y') }} erstellen" data-day="{{ day.day }}" data-body="#sidebarForm" class="relative inline-block w-full py-2 text-primary-900 hover:text-primary-950 dark:bg-primary-600 dark:text-white dark:hover:bg-primary-500 dark:hover:text-white focus:text-primary-950 text-sm font-semibold bg-gray-100 hover:bg-gray-200 focus:bg-gray-200 {% if "admin" in loggedin_user.roles %} rounded-br-md {% else %} rounded-b-md {% endif %}">
<span class="absolute inset-y-0 left-0 flex items-center pl-3">
{% include "includes/plus-icon" %}
</span>
Ausfahrt
</a>
{% endif %}
</div>
{% endif %}
{# --- END Add Buttons --- #}
</div>
{% endfor %}
</div>
</div>
{% if "cox" in loggedin_user.roles %}
{% include "forms/trip" %}
{% endif %}
{% if "admin" in loggedin_user.roles %}
{% include "forms/event" %}
{% endif %}{% endblock content %}