updates #714

Merged
philipp merged 11 commits from updates into staging 2024-09-02 13:26:45 +02:00
11 changed files with 139 additions and 42 deletions
Showing only changes of commit 99a49dbec9 - Show all commits

View File

@ -575,10 +575,8 @@ ORDER BY departure DESC
return Err(LogbookUpdateError::ArrivalNotAfterDeparture);
}
if !boat.external {
if boat.on_water_between(db, dep, arr).await {
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;
@ -594,15 +592,14 @@ 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 {
if !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 {
return Err(LogbookUpdateError::OnlyAllowedToEndTripsEndingToday);
}

View File

@ -40,7 +40,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,
}
@ -210,11 +209,10 @@ WHERE day=?
let was_already_cancelled = tripdetails.max_people == 0;
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,
trip_details_id
)
@ -338,6 +336,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,

View File

@ -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,
@ -142,14 +159,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
@ -305,7 +321,6 @@ mod test {
notes: None,
allow_guests: false,
trip_type: None,
always_show: false
}
)
.await,
@ -321,7 +336,6 @@ mod test {
notes: None,
allow_guests: false,
trip_type: None,
always_show: false
}
)
.await,

View File

@ -450,6 +450,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)
@ -879,7 +885,7 @@ 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
@ -890,7 +896,7 @@ ORDER BY last_access DESC
.num_days()
+ 1;
if days_left_in_year < 30 {
if days_left_in_year <= 31 {
let end_of_next_year =
NaiveDate::from_ymd_opt(Local::now().year() + 1, 12, 31).unwrap(); //Ok,
//december
@ -1028,6 +1034,7 @@ special_user!(VorstandUser, +"Vorstand");
special_user!(EventUser, +"manage_events");
special_user!(AllowedToEditPaymentStatusUser, +"kassier", +"admin");
special_user!(ManageUserUser, +"admin", +"schriftfuehrer");
special_user!(AllowedToUpdateTripToAlwaysBeShownUser, +"admin");
#[derive(FromRow, Serialize, Deserialize, Clone, Debug)]
pub struct UserWithRolesAndMembershipPdf {

View File

@ -24,6 +24,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;
@ -96,6 +100,10 @@ impl UserTrip {
return Err(UserTripDeleteError::DetailsLocked);
}
if !trip_details.user_sees_trip(db, user).await {
return Err(UserTripDeleteError::NotVisibleToUser);
}
if let Some(name) = name {
if !trip_details.user_allowed_to_change(db, user).await {
return Err(UserTripDeleteError::NotAllowedToDeleteGuest);
@ -137,6 +145,7 @@ pub enum UserTripError {
CantRegisterAtOwnEvent,
GuestNotAllowedForThisEvent,
NotAllowedToAddGuest,
NotVisibleToUser,
}
#[derive(Debug, PartialEq)]
@ -144,6 +153,7 @@ pub enum UserTripDeleteError {
DetailsLocked,
GuestNotParticipating,
NotAllowedToDeleteGuest,
NotVisibleToUser,
}
#[cfg(test)]

View File

@ -374,7 +374,7 @@ async fn create_scheckbuch(
if mail.parse::<Address>().is_err() {
return Flash::error(
Redirect::to("/admin/user/scheckbuch"),
format!("Keine gültige Mailadresse"),
"Keine gültige Mailadresse".to_string(),
);
}
@ -383,9 +383,8 @@ async fn create_scheckbuch(
if User::find_by_name(db, name).await.is_some() {
return Flash::error(
Redirect::to("/admin/user/scheckbuch"),
format!(
"Kann kein Scheckbuch erstellen, der Name wird bereits von einem User verwendet"
),
.to_string(),
);
}
@ -418,14 +417,14 @@ async fn schnupper_to_scheckbuch(
let Some(user) = User::find_by_id(db, id).await else {
return Flash::error(
Redirect::to("/admin/schnupper"),
format!("user id not found"),
"user id not found".to_string(),
);
};
if !user.has_role(db, "schnupperant").await {
return Flash::error(
Redirect::to("/admin/schnupper"),
format!("kein schnupperant..."),
"kein schnupperant...".to_string(),
);
}

View File

@ -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,7 +179,14 @@ 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)]

View File

@ -131,13 +131,13 @@ async fn new_blogpost(
blogpost: Form<NewBlogpostForm<'_>>,
config: &State<Config>,
) -> String {
if blogpost.pw == &config.wordpress_key {
let member = Role::find_by_name(&db, "Donau Linz").await.unwrap();
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"),
&format!("Neuer Blogpost"),
"Neuer Blogpost",
Some(blogpost.article_url),
None,
)
@ -160,9 +160,9 @@ async fn blogpost_unpublished(
blogpost: Form<BlogpostUnpublishedForm<'_>>,
config: &State<Config>,
) -> String {
if blogpost.pw == &config.wordpress_key {
if blogpost.pw == config.wordpress_key {
Notification::delete_by_link(
&db,
db,
&urlencoding::decode(blogpost.article_url).expect("UTF-8"),
)
.await;

View File

@ -37,6 +37,10 @@ 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("loggedin_user", &UserWithDetails::from_user(user, db).await);
context.insert("days", &days);
@ -99,6 +103,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.",
@ -147,6 +155,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.",
@ -191,6 +203,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");
}

View File

@ -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" />

View File

@ -67,7 +67,8 @@
{% endif %}
{% endfor %}
{% endif %}
<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"
<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') }}"
@ -346,7 +347,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 +369,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 +378,20 @@
{% 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>
<form action="/cox/trip/{{ trip.id }}/toggle-always-show"
method="get"
class="grid gap-3">
<input value="{% if trip.always_show %}Normal anzeigen{% else %}Immer anzeigen{% endif %}"
class="btn btn-primary"
type="submit" />
</form>
</div>
{% endif %}
{# --- END Admin Form --- #}
</div>
</div>
{# --- END Sidebar Content --- #}