forked from Ruderverein-Donau-Linz/rowt
		
	rebase to rowt #12
@@ -2,7 +2,7 @@
 | 
			
		||||
secret_key = "/NtVGizglEoyoxBLzsRDWTy4oAG1qDw4J4O+CWJSv+fypD7W9sam8hUY4j90EZsbZk8wEradS5zBoWtWKi3k8w=="
 | 
			
		||||
rss_key = "rss-key-for-ci"
 | 
			
		||||
limits = { file = "10 MiB", data-form = "10 MiB"}
 | 
			
		||||
smtp_pw = "8kIjlLH79Ky6D3j"
 | 
			
		||||
smtp_pw = "my-smtp-password"
 | 
			
		||||
usage_log_path = "./usage.txt"
 | 
			
		||||
openweathermap_key = "c8dab8f91b5b815d76e9879cbaecd8d5"
 | 
			
		||||
openweathermap_key = "openweather-key"
 | 
			
		||||
wordpress_key = "pw-to-allow-sending-notifications"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										94
									
								
								doc/nextcloud-notes.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								doc/nextcloud-notes.md
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
			
		||||
# Nextcloud integration
 | 
			
		||||
 | 
			
		||||
- Based on [this plugin](https://github.com/nextcloud/user_external)
 | 
			
		||||
- Install that plugin via web
 | 
			
		||||
- Connect to server, enter nextcloud-docker-image: `docker exec -it nextcloud-aio-nextcloud bash`
 | 
			
		||||
- Adapt `/var/www/html/custom_apps/user_external/lib/BasicAuth.php` to switch from BasicAuth to RowtAuth:
 | 
			
		||||
```php
 | 
			
		||||
<?php
 | 
			
		||||
/**
 | 
			
		||||
 * Copyright (c) 2019 Lutz Freitag <lutz.freitag@gottliebtfreitag.de>
 | 
			
		||||
 * This file is licensed under the Affero General Public License version 3 or
 | 
			
		||||
 * later.
 | 
			
		||||
 * See the COPYING-README file.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
namespace OCA\UserExternal;
 | 
			
		||||
 | 
			
		||||
class BasicAuth extends Base {
 | 
			
		||||
    private $authUrl;
 | 
			
		||||
 | 
			
		||||
    public function __construct($authUrl) {
 | 
			
		||||
        parent::__construct($authUrl);
 | 
			
		||||
        $this->authUrl = $authUrl;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * Check if the password is correct without logging in the user
 | 
			
		||||
     *
 | 
			
		||||
     * @param string $uid      The username
 | 
			
		||||
     * @param string $password The password
 | 
			
		||||
     *
 | 
			
		||||
     * @return true/false
 | 
			
		||||
     */
 | 
			
		||||
    public function checkPassword($uid, $password) {
 | 
			
		||||
        // Prepare POST data with credentials
 | 
			
		||||
        $postData = http_build_query([
 | 
			
		||||
            'name' => $uid,
 | 
			
		||||
            'password' => $password
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Create context with POST method
 | 
			
		||||
        $context = stream_context_create([
 | 
			
		||||
            'http' => [
 | 
			
		||||
                'method' => 'POST',
 | 
			
		||||
                'header' => 'Content-Type: application/x-www-form-urlencoded',
 | 
			
		||||
                'content' => $postData,
 | 
			
		||||
                'follow_location' => 0
 | 
			
		||||
            ]
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        // Get the content of the response
 | 
			
		||||
        $content = @file_get_contents($this->authUrl, false, $context);
 | 
			
		||||
 | 
			
		||||
        if ($content === false) {
 | 
			
		||||
            \OC::$server->getLogger()->error(
 | 
			
		||||
                'ERROR: Failed to get content from Auth Url: '.$this->authUrl,
 | 
			
		||||
                ['app' => 'user_external']
 | 
			
		||||
            );
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Check if the content is "SUCC"
 | 
			
		||||
        if (trim($content) === "SUCC") {
 | 
			
		||||
            $this->storeUser($uid);
 | 
			
		||||
            return $uid;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
```
 | 
			
		||||
- In `/var/www/html/config/config.php` add this:
 | 
			
		||||
```
 | 
			
		||||
 'user_backends' => 
 | 
			
		||||
  array (
 | 
			
		||||
    0 => 
 | 
			
		||||
    array (
 | 
			
		||||
      'class' => '\\OCA\\UserExternal\\BasicAuth',
 | 
			
		||||
      'arguments' => 
 | 
			
		||||
      array (
 | 
			
		||||
        0 => 'https://app.rudernlinz.at/nxauth',
 | 
			
		||||
      ),
 | 
			
		||||
    ),
 | 
			
		||||
  ),
 | 
			
		||||
```
 | 
			
		||||
- In `/var/www/html/config/config.php` add this `'skeletondirectory' => '',` to disable default folders for new users
 | 
			
		||||
- To automatically add users to a group (e.g. `vorstand`), use the `Auto Groups` plugin
 | 
			
		||||
- Shared folders are not shared with new members due to [this bug](https://github.com/nextcloud/server/issues/25062#issuecomment-766445043)
 | 
			
		||||
	- Find DB config: `docker exec nextcloud-aio-database env | grep POSTGRES`
 | 
			
		||||
	- Workaround: Connect to docker-db: `docker exec -it nextcloud-aio-database bash`
 | 
			
		||||
	- Connect to db: `psql -U nextcloud -d nextcloud_database`
 | 
			
		||||
	- (with `\l` you see all dbs)
 | 
			
		||||
	- Connect to nextcloud db: `\c nextcloud_database`
 | 
			
		||||
	- Do query from issue: `UPDATE oc_share SET accepted = 1 WHERE share_type = 1;`
 | 
			
		||||
@@ -120,18 +120,69 @@ test.describe("cox can edit trips", () => {
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test("call off trip", async () => {
 | 
			
		||||
    await sharedPage.goto("/");
 | 
			
		||||
    // Someone registers...
 | 
			
		||||
    await sharedPage.goto("/auth/logout");
 | 
			
		||||
    await sharedPage.goto("/auth");
 | 
			
		||||
    await sharedPage.getByPlaceholder("Name").click();
 | 
			
		||||
    await sharedPage.getByPlaceholder("Name").fill("rower");
 | 
			
		||||
    await sharedPage.getByPlaceholder("Name").press("Tab");
 | 
			
		||||
    await sharedPage.getByPlaceholder("Passwort").fill("rower");
 | 
			
		||||
    await sharedPage.getByPlaceholder("Passwort").press("Enter");
 | 
			
		||||
 | 
			
		||||
    await sharedPage.goto("/planned");
 | 
			
		||||
    await sharedPage.getByRole('link', { name: 'Mitrudern' }).nth(1).click();
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // Login as cox again
 | 
			
		||||
    await sharedPage.goto("/auth/logout");
 | 
			
		||||
    await sharedPage.goto("/auth");
 | 
			
		||||
    await sharedPage.getByPlaceholder("Name").click();
 | 
			
		||||
    await sharedPage.getByPlaceholder("Name").fill("cox");
 | 
			
		||||
    await sharedPage.getByPlaceholder("Name").press("Tab");
 | 
			
		||||
    await sharedPage.getByPlaceholder("Passwort").fill("cox");
 | 
			
		||||
    await sharedPage.getByPlaceholder("Passwort").press("Enter");
 | 
			
		||||
 | 
			
		||||
    await sharedPage.goto("/planned");
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // ... now I can cancel trip
 | 
			
		||||
    await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
 | 
			
		||||
    await expect(sharedPage.locator("#sidebar")).toContainText(
 | 
			
		||||
      "Freie Plätze: 3",
 | 
			
		||||
    );
 | 
			
		||||
    await sharedPage.getByRole("spinbutton").click();
 | 
			
		||||
    await sharedPage.getByRole("spinbutton").fill("0");
 | 
			
		||||
    await sharedPage.getByRole("button", { name: "Speichern" }).click();
 | 
			
		||||
    await sharedPage.getByRole("button", { name: "Ausfahrt absagen" }).click();
 | 
			
		||||
    await expect(sharedPage.locator("body")).toContainText(
 | 
			
		||||
      "Ausfahrt erfolgreich aktualisiert.",
 | 
			
		||||
    );
 | 
			
		||||
    await expect(sharedPage.locator("body")).toContainText("(Absage cox)");
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // Done with the test -> cancel the cancellation of the trip, otherwise the afterAll function below fails
 | 
			
		||||
    await sharedPage.getByRole("link", { name: "Details" }).nth(1).click();
 | 
			
		||||
    await sharedPage.getByRole("spinbutton").click();
 | 
			
		||||
    await sharedPage.getByRole("spinbutton").fill("3");
 | 
			
		||||
    await sharedPage.getByRole("button", { name: "Speichern" }).click();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // deregistering
 | 
			
		||||
    await sharedPage.goto("/auth/logout");
 | 
			
		||||
    await sharedPage.goto("/auth");
 | 
			
		||||
    await sharedPage.getByPlaceholder("Name").click();
 | 
			
		||||
    await sharedPage.getByPlaceholder("Name").fill("rower");
 | 
			
		||||
    await sharedPage.getByPlaceholder("Name").press("Tab");
 | 
			
		||||
    await sharedPage.getByPlaceholder("Passwort").fill("rower");
 | 
			
		||||
    await sharedPage.getByPlaceholder("Passwort").press("Enter");
 | 
			
		||||
 | 
			
		||||
    await sharedPage.goto("/planned");
 | 
			
		||||
    await sharedPage.getByRole('link', { name: 'Abmelden' }).click();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    // now cox can delete trip again in afterAll
 | 
			
		||||
    await sharedPage.goto("/auth/logout");
 | 
			
		||||
    await sharedPage.goto("/auth");
 | 
			
		||||
    await sharedPage.getByPlaceholder("Name").click();
 | 
			
		||||
    await sharedPage.getByPlaceholder("Name").fill("cox");
 | 
			
		||||
    await sharedPage.getByPlaceholder("Name").press("Tab");
 | 
			
		||||
    await sharedPage.getByPlaceholder("Passwort").fill("cox");
 | 
			
		||||
    await sharedPage.getByPlaceholder("Passwort").press("Enter");
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  test.afterAll(async () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -34,11 +34,13 @@ pub struct Event {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Debug)]
 | 
			
		||||
pub struct EventWithUserAndTriptype {
 | 
			
		||||
pub struct EventWithDetails {
 | 
			
		||||
    #[serde(flatten)]
 | 
			
		||||
    pub event: Event,
 | 
			
		||||
    trip_type: Option<TripType>,
 | 
			
		||||
    tripdetails: TripDetails,
 | 
			
		||||
    cox_needed: bool,
 | 
			
		||||
    cancelled: bool,
 | 
			
		||||
    cox: Vec<Registration>,
 | 
			
		||||
    rower: Vec<Registration>,
 | 
			
		||||
}
 | 
			
		||||
@@ -116,6 +118,12 @@ pub struct EventUpdate<'a> {
 | 
			
		||||
    pub trip_type_id: Option<i64>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl EventUpdate<'_> {
 | 
			
		||||
    fn cancelled(&self) -> bool {
 | 
			
		||||
        self.max_people == -1
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Event {
 | 
			
		||||
    pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> {
 | 
			
		||||
        sqlx::query_as!(
 | 
			
		||||
@@ -134,16 +142,13 @@ WHERE planned_event.id like ?
 | 
			
		||||
        .ok()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_pinned_for_day(
 | 
			
		||||
        db: &SqlitePool,
 | 
			
		||||
        day: NaiveDate,
 | 
			
		||||
    ) -> Vec<EventWithUserAndTriptype> {
 | 
			
		||||
    pub async fn get_pinned_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<EventWithDetails> {
 | 
			
		||||
        let mut events = Self::get_for_day(db, day).await;
 | 
			
		||||
        events.retain(|e| e.event.always_show);
 | 
			
		||||
        events
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<EventWithUserAndTriptype> {
 | 
			
		||||
    pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<EventWithDetails> {
 | 
			
		||||
        let day = format!("{day}");
 | 
			
		||||
        let events = sqlx::query_as!(
 | 
			
		||||
            Event,
 | 
			
		||||
@@ -164,10 +169,15 @@ WHERE day=?",
 | 
			
		||||
            if let Some(trip_type_id) = event.trip_type_id {
 | 
			
		||||
                trip_type = TripType::find_by_id(db, trip_type_id).await;
 | 
			
		||||
            }
 | 
			
		||||
            ret.push(EventWithUserAndTriptype {
 | 
			
		||||
            let tripdetails = TripDetails::find_by_id(db, event.trip_details_id)
 | 
			
		||||
                .await
 | 
			
		||||
                .expect("db constraints");
 | 
			
		||||
            ret.push(EventWithDetails {
 | 
			
		||||
                cox_needed: event.planned_amount_cox > cox.len() as i64,
 | 
			
		||||
                cox,
 | 
			
		||||
                rower: Registration::all_rower(db, event.trip_details_id).await,
 | 
			
		||||
                cancelled: tripdetails.cancelled(),
 | 
			
		||||
                tripdetails,
 | 
			
		||||
                event,
 | 
			
		||||
                trip_type,
 | 
			
		||||
            });
 | 
			
		||||
@@ -313,7 +323,7 @@ WHERE trip_details.id=?
 | 
			
		||||
        .unwrap(); //Okay, as planned_event can only be created with proper DB backing
 | 
			
		||||
 | 
			
		||||
        let tripdetails = self.trip_details(db).await;
 | 
			
		||||
        let was_already_cancelled = tripdetails.max_people == 0;
 | 
			
		||||
        let was_already_cancelled = tripdetails.cancelled();
 | 
			
		||||
 | 
			
		||||
        sqlx::query!(
 | 
			
		||||
            "UPDATE trip_details SET max_people = ?, notes = ?, always_show = ?, is_locked = ?, trip_type_id = ? WHERE id = ?",
 | 
			
		||||
@@ -338,7 +348,7 @@ WHERE trip_details.id=?
 | 
			
		||||
            .await;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if update.max_people == 0 && !was_already_cancelled {
 | 
			
		||||
        if update.cancelled() && !was_already_cancelled {
 | 
			
		||||
            let coxes = Registration::all_cox(db, self.id).await;
 | 
			
		||||
            for user in coxes {
 | 
			
		||||
                if let Some(user) = User::find_by_name(db, &user.name).await {
 | 
			
		||||
@@ -387,7 +397,7 @@ WHERE trip_details.id=?
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        if update.max_people > 0 && was_already_cancelled {
 | 
			
		||||
        if !update.cancelled() && was_already_cancelled {
 | 
			
		||||
            Notification::delete_by_action(
 | 
			
		||||
                db,
 | 
			
		||||
                &format!("remove_user_trip_with_trip_details_id:{}", tripdetails.id),
 | 
			
		||||
@@ -425,7 +435,7 @@ WHERE trip_details.id=?
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn is_cancelled(&self) -> bool {
 | 
			
		||||
        self.max_people == 0
 | 
			
		||||
        self.max_people == -1
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_ics_feed(db: &SqlitePool) -> String {
 | 
			
		||||
@@ -442,10 +452,16 @@ WHERE trip_details.id=?
 | 
			
		||||
 | 
			
		||||
    pub(crate) async fn get_vevent(self, db: &SqlitePool) -> ics::Event {
 | 
			
		||||
        let mut vevent = ics::Event::new(format!("event-{}@ruad.at", self.id), "19900101T180000");
 | 
			
		||||
        let time_str = self.planned_starting_time.replace(':', "");
 | 
			
		||||
        let formatted_time = if time_str.len() == 3 {
 | 
			
		||||
            format!("0{}", time_str)
 | 
			
		||||
        } else {
 | 
			
		||||
            time_str.clone() // TODO: remove again
 | 
			
		||||
        };
 | 
			
		||||
        vevent.push(DtStart::new(format!(
 | 
			
		||||
            "{}T{}00",
 | 
			
		||||
            self.day.replace('-', ""),
 | 
			
		||||
            self.planned_starting_time.replace(':', "")
 | 
			
		||||
            formatted_time
 | 
			
		||||
        )));
 | 
			
		||||
 | 
			
		||||
        let original_time = NaiveTime::parse_from_str(&self.planned_starting_time, "%H:%M")
 | 
			
		||||
 
 | 
			
		||||
@@ -6,8 +6,8 @@ use waterlevel::WaterlevelDay;
 | 
			
		||||
use crate::AMOUNT_DAYS_TO_SHOW_TRIPS_AHEAD;
 | 
			
		||||
 | 
			
		||||
use self::{
 | 
			
		||||
    event::{Event, EventWithUserAndTriptype},
 | 
			
		||||
    trip::{Trip, TripWithUserAndType},
 | 
			
		||||
    event::{Event, EventWithDetails},
 | 
			
		||||
    trip::{Trip, TripWithDetails},
 | 
			
		||||
    waterlevel::Waterlevel,
 | 
			
		||||
    weather::Weather,
 | 
			
		||||
};
 | 
			
		||||
@@ -28,8 +28,8 @@ pub mod weather;
 | 
			
		||||
#[derive(Serialize, Debug)]
 | 
			
		||||
pub struct Day {
 | 
			
		||||
    day: NaiveDate,
 | 
			
		||||
    events: Vec<EventWithUserAndTriptype>,
 | 
			
		||||
    trips: Vec<TripWithUserAndType>,
 | 
			
		||||
    events: Vec<EventWithDetails>,
 | 
			
		||||
    trips: Vec<TripWithDetails>,
 | 
			
		||||
    is_pinned: bool,
 | 
			
		||||
    regular_sees_this_day: bool,
 | 
			
		||||
    max_waterlevel: Option<WaterlevelDay>,
 | 
			
		||||
 
 | 
			
		||||
@@ -284,7 +284,7 @@ mod test {
 | 
			
		||||
        let cancel_update = EventUpdate {
 | 
			
		||||
            name: &event.name,
 | 
			
		||||
            planned_amount_cox: event.planned_amount_cox as i32,
 | 
			
		||||
            max_people: 0,
 | 
			
		||||
            max_people: -1,
 | 
			
		||||
            notes: event.notes.as_deref(),
 | 
			
		||||
            always_show: event.always_show,
 | 
			
		||||
            is_locked: event.is_locked,
 | 
			
		||||
 
 | 
			
		||||
@@ -30,11 +30,12 @@ pub struct Trip {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Debug)]
 | 
			
		||||
pub struct TripWithUserAndType {
 | 
			
		||||
pub struct TripWithDetails {
 | 
			
		||||
    #[serde(flatten)]
 | 
			
		||||
    pub trip: Trip,
 | 
			
		||||
    pub rower: Vec<Registration>,
 | 
			
		||||
    trip_type: Option<TripType>,
 | 
			
		||||
    cancelled: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub struct TripUpdate<'a> {
 | 
			
		||||
@@ -46,7 +47,13 @@ pub struct TripUpdate<'a> {
 | 
			
		||||
    pub is_locked: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TripWithUserAndType {
 | 
			
		||||
impl<'a> TripUpdate<'a> {
 | 
			
		||||
    fn cancelled(&self) -> bool {
 | 
			
		||||
        self.max_people == -1
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl TripWithDetails {
 | 
			
		||||
    pub async fn from(db: &SqlitePool, trip: Trip) -> Self {
 | 
			
		||||
        let mut trip_type = None;
 | 
			
		||||
        if let Some(trip_type_id) = trip.trip_type_id {
 | 
			
		||||
@@ -54,8 +61,9 @@ impl TripWithUserAndType {
 | 
			
		||||
        }
 | 
			
		||||
        Self {
 | 
			
		||||
            rower: Registration::all_rower(db, trip.trip_details_id.unwrap()).await,
 | 
			
		||||
            trip,
 | 
			
		||||
            trip_type,
 | 
			
		||||
            cancelled: trip.is_cancelled(),
 | 
			
		||||
            trip,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -129,10 +137,17 @@ WHERE trip_details.id=?
 | 
			
		||||
 | 
			
		||||
    pub(crate) async fn get_vevent(self, user: &User) -> ics::Event {
 | 
			
		||||
        let mut vevent = ics::Event::new(format!("trip-{}@ruad.at", self.id), "19900101T180000");
 | 
			
		||||
        let time_str = self.planned_starting_time.replace(':', "");
 | 
			
		||||
        let formatted_time = if time_str.len() == 3 {
 | 
			
		||||
            format!("0{}", time_str)
 | 
			
		||||
        } else {
 | 
			
		||||
            time_str
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        vevent.push(DtStart::new(format!(
 | 
			
		||||
            "{}T{}00",
 | 
			
		||||
            self.day.replace('-', ""),
 | 
			
		||||
            self.planned_starting_time.replace(':', "")
 | 
			
		||||
            formatted_time
 | 
			
		||||
        )));
 | 
			
		||||
 | 
			
		||||
        let original_time = NaiveTime::parse_from_str(&self.planned_starting_time, "%H:%M")
 | 
			
		||||
@@ -234,7 +249,7 @@ WHERE trip.id=?
 | 
			
		||||
            return Err(CoxHelpError::DetailsLocked);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if event.max_people == 0 {
 | 
			
		||||
        if event.is_cancelled() {
 | 
			
		||||
            return Err(CoxHelpError::CanceledEvent);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -251,12 +266,12 @@ WHERE trip.id=?
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_for_today(db: &SqlitePool) -> Vec<TripWithUserAndType> {
 | 
			
		||||
    pub async fn get_for_today(db: &SqlitePool) -> Vec<TripWithDetails> {
 | 
			
		||||
        let today = Local::now().date_naive();
 | 
			
		||||
        Self::get_for_day(db, today).await
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<TripWithUserAndType> {
 | 
			
		||||
    pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<TripWithDetails> {
 | 
			
		||||
        let day = format!("{day}");
 | 
			
		||||
        let trips = sqlx::query_as!(
 | 
			
		||||
            Trip,
 | 
			
		||||
@@ -275,7 +290,7 @@ WHERE day=?
 | 
			
		||||
 | 
			
		||||
        let mut ret = Vec::new();
 | 
			
		||||
        for trip in trips {
 | 
			
		||||
            ret.push(TripWithUserAndType::from(db, trip).await);
 | 
			
		||||
            ret.push(TripWithDetails::from(db, trip).await);
 | 
			
		||||
        }
 | 
			
		||||
        ret
 | 
			
		||||
    }
 | 
			
		||||
@@ -298,9 +313,9 @@ WHERE day=?
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        let tripdetails = TripDetails::find_by_id(db, trip_details_id).await.unwrap();
 | 
			
		||||
        let was_already_cancelled = tripdetails.max_people == 0;
 | 
			
		||||
        let was_already_cancelled = tripdetails.cancelled();
 | 
			
		||||
 | 
			
		||||
        let is_locked = if update.max_people == 0 {
 | 
			
		||||
        let is_locked = if update.cancelled() {
 | 
			
		||||
            false
 | 
			
		||||
        } else {
 | 
			
		||||
            update.is_locked
 | 
			
		||||
@@ -318,10 +333,8 @@ WHERE day=?
 | 
			
		||||
        .await
 | 
			
		||||
        .unwrap(); //Okay, as trip_details can only be created with proper DB backing
 | 
			
		||||
 | 
			
		||||
        if update.max_people == 0 && !was_already_cancelled {
 | 
			
		||||
            let rowers = TripWithUserAndType::from(db, update.trip.clone())
 | 
			
		||||
                .await
 | 
			
		||||
                .rower;
 | 
			
		||||
        if update.cancelled() && !was_already_cancelled {
 | 
			
		||||
            let rowers = TripWithDetails::from(db, update.trip.clone()).await.rower;
 | 
			
		||||
            for user in rowers {
 | 
			
		||||
                if let Some(user) = User::find_by_name(db, &user.name).await {
 | 
			
		||||
                    let notes = match update.notes {
 | 
			
		||||
@@ -357,7 +370,7 @@ WHERE day=?
 | 
			
		||||
            .await;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if update.max_people > 0 && was_already_cancelled {
 | 
			
		||||
        if !update.cancelled() && was_already_cancelled {
 | 
			
		||||
            Notification::delete_by_action(
 | 
			
		||||
                db,
 | 
			
		||||
                &format!("remove_user_trip_with_trip_details_id:{}", trip_details_id),
 | 
			
		||||
@@ -445,14 +458,14 @@ WHERE day=?
 | 
			
		||||
    pub(crate) async fn get_pinned_for_day(
 | 
			
		||||
        db: &sqlx::Pool<sqlx::Sqlite>,
 | 
			
		||||
        day: NaiveDate,
 | 
			
		||||
    ) -> Vec<TripWithUserAndType> {
 | 
			
		||||
    ) -> Vec<TripWithDetails> {
 | 
			
		||||
        let mut trips = Self::get_for_day(db, day).await;
 | 
			
		||||
        trips.retain(|e| e.trip.always_show);
 | 
			
		||||
        trips
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn is_cancelled(&self) -> bool {
 | 
			
		||||
        self.max_people == 0
 | 
			
		||||
        self.max_people == -1
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ use sqlx::{FromRow, SqlitePool};
 | 
			
		||||
 | 
			
		||||
use super::{
 | 
			
		||||
    notification::Notification,
 | 
			
		||||
    trip::{Trip, TripWithUserAndType},
 | 
			
		||||
    trip::{Trip, TripWithDetails},
 | 
			
		||||
    triptype::TripType,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -95,7 +95,7 @@ WHERE day = ? AND planned_starting_time = ?
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    pub fn cancelled(&self) -> bool {
 | 
			
		||||
        self.max_people == 0
 | 
			
		||||
        self.max_people == -1
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// This function is called when a person registers to a trip or when the cox changes the
 | 
			
		||||
@@ -138,7 +138,7 @@ WHERE day = ? AND planned_starting_time = ?
 | 
			
		||||
                // This trip_details belongs to a planned_event, no need to do anything
 | 
			
		||||
                continue;
 | 
			
		||||
            };
 | 
			
		||||
            let pot_coxes = TripWithUserAndType::from(db, trip.clone()).await;
 | 
			
		||||
            let pot_coxes = TripWithDetails::from(db, trip.clone()).await;
 | 
			
		||||
            let pot_coxes = pot_coxes.rower;
 | 
			
		||||
            for user in pot_coxes {
 | 
			
		||||
                let cox = User::find_by_id(db, trip.cox_id as i32).await.unwrap();
 | 
			
		||||
@@ -196,7 +196,7 @@ WHERE day = ? AND planned_starting_time = ?
 | 
			
		||||
        .fetch_one(db)
 | 
			
		||||
        .await
 | 
			
		||||
        .unwrap(); //TODO: fixme
 | 
			
		||||
        let amount_currently_registered = i64::from(amount_currently_registered.count);
 | 
			
		||||
        let amount_currently_registered = amount_currently_registered.count;
 | 
			
		||||
 | 
			
		||||
        amount_currently_registered >= self.max_people
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@ use sqlx::{FromRow, SqlitePool};
 | 
			
		||||
 | 
			
		||||
use super::{
 | 
			
		||||
    notification::Notification,
 | 
			
		||||
    trip::{Trip, TripWithUserAndType},
 | 
			
		||||
    trip::{Trip, TripWithDetails},
 | 
			
		||||
    tripdetails::TripDetails,
 | 
			
		||||
    user::{SteeringUser, User},
 | 
			
		||||
};
 | 
			
		||||
@@ -158,7 +158,7 @@ impl UserTrip {
 | 
			
		||||
                .unwrap()
 | 
			
		||||
                .cancelled()
 | 
			
		||||
            {
 | 
			
		||||
                let trip = TripWithUserAndType::from(db, trip.clone()).await;
 | 
			
		||||
                let trip = TripWithDetails::from(db, trip.clone()).await;
 | 
			
		||||
                if trip.rower.len() == 1 {
 | 
			
		||||
                    trip_to_delete = Some(trip.trip);
 | 
			
		||||
                }
 | 
			
		||||
 
 | 
			
		||||
@@ -71,7 +71,6 @@ async fn steering(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage
 | 
			
		||||
fn unauthorized_error(req: &Request) -> Redirect {
 | 
			
		||||
    // Save the URL the user tried to access, to be able to go there once logged in
 | 
			
		||||
    let mut redirect_cookie = Cookie::new("redirect_url", format!("{}", req.uri()));
 | 
			
		||||
    println!("{}", req.uri());
 | 
			
		||||
    redirect_cookie.set_expires(OffsetDateTime::now_utc() + Duration::hours(1));
 | 
			
		||||
    req.cookies().add_private(redirect_cookie);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,7 @@
 | 
			
		||||
                                                {% if event.always_show and not day.regular_sees_this_day %}
 | 
			
		||||
                                                    <span title="Du siehst diese Ausfahrt schon, obwohl sie mehr als {{ amount_days_to_show_trips_ahead }} Tage in der Zukunft liegt. Du Magier!">🔮</span>
 | 
			
		||||
                                                {% endif -%}
 | 
			
		||||
                                                {%- if event.max_people == 0 %}
 | 
			
		||||
                                                {%- if event.cancelled %}
 | 
			
		||||
                                                    <strong class="text-[#f43f5e]">⚠ Absage
 | 
			
		||||
                                                        {{ event.planned_starting_time }}
 | 
			
		||||
                                                        Uhr
 | 
			
		||||
@@ -129,7 +129,7 @@
 | 
			
		||||
                                            <div id="event{{ event.trip_details_id }}">
 | 
			
		||||
                                                {# --- START List Coxes --- #}
 | 
			
		||||
                                                {% if event.planned_amount_cox > 0 %}
 | 
			
		||||
                                                    {% if event.max_people == 0 %}
 | 
			
		||||
                                                    {% if event.cancelled %}
 | 
			
		||||
                                                        {{ macros::box(participants=event.cox, empty_seats="", header='Absage', bg='[#f43f5e]') }}
 | 
			
		||||
                                                    {% else %}
 | 
			
		||||
                                                        {% if amount_cox_missing > 0 %}
 | 
			
		||||
@@ -142,9 +142,9 @@
 | 
			
		||||
                                                {# --- END List Coxes --- #}
 | 
			
		||||
                                                {# --- START List Rowers --- #}
 | 
			
		||||
                                                {% set amount_cur_rower = event.rower | length %}
 | 
			
		||||
                                                {% if event.max_people == 0 %}
 | 
			
		||||
                                                {% if event.cancelled %}
 | 
			
		||||
                                                    {{ macros::box(header='Absage', bg='[#f43f5e]', participants=event.rower, trip_details_id=event.trip_details_id, allow_removing="manage_events" in loggedin_user.roles) }}
 | 
			
		||||
                                                {% else %}
 | 
			
		||||
                                                {% elif event.max_people > 0  %}
 | 
			
		||||
                                                    {{ macros::box(participants=event.rower, empty_seats=event.max_people - amount_cur_rower, bg='primary-100', color='black', trip_details_id=event.trip_details_id, allow_removing="manage_events" in loggedin_user.roles) }}
 | 
			
		||||
                                                {% endif %}
 | 
			
		||||
                                                {# --- END List Rowers --- #}
 | 
			
		||||
@@ -167,7 +167,11 @@
 | 
			
		||||
                                                        <input type="hidden" name="_method" value="put" />
 | 
			
		||||
                                                        <input type="hidden" name="id" value="{{ event.id }}" />
 | 
			
		||||
                                                        {{ macros::input(label='Titel', name='name', type='input', value=event.name) }}
 | 
			
		||||
                                                        {{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=event.max_people, min='1') }}
 | 
			
		||||
                                                        {% if event.cancelled %} 
 | 
			
		||||
                                                          <input type="hidden" name="max_people" value="-1" />
 | 
			
		||||
                                                        {% else %}
 | 
			
		||||
                                                          {{ macros::input(label='Anzahl Ruderer', name='max_people', type='number', required=true, value=event.max_people, min='1') }}
 | 
			
		||||
                                                        {% endif %}
 | 
			
		||||
                                                        {{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', value=event.planned_amount_cox, required=true, min='0') }}
 | 
			
		||||
                                                        {{ macros::checkbox(label='Immer anzeigen', name='always_show', id=event.id,checked=event.always_show, help="Grundsätzlich sehen Rudernde Ausfahrten 10 Tage im vorhinein. Wenn du diese Option aktivierst, ist diese Ausfahrt sofort allen ersichtlich.") }}
 | 
			
		||||
                                                        {{ macros::checkbox(label='Gesperrt', name='is_locked', id=event.id,checked=event.is_locked, help="Wenn diese Option aktiviert ist, kann sich keiner mehr an- und abmelden. Sinnvoll, wenn zB bereits die Bootseinteilung vorgenommen wurde") }}
 | 
			
		||||
@@ -187,7 +191,7 @@
 | 
			
		||||
                                                        </a>
 | 
			
		||||
                                                    </div>
 | 
			
		||||
                                                {% else %}
 | 
			
		||||
                                                    {% if event.max_people == 0 %}
 | 
			
		||||
                                                    {% if event.cancelled %}
 | 
			
		||||
                                                        Wenn du deine Absage absagen (:^)) willst, einfach entsprechende Anzahl an Ruderer oben eintragen.
 | 
			
		||||
                                                    {% else %}
 | 
			
		||||
                                                        <div class="bg-gray-100 dark:bg-primary-900 p-3 mt-4 rounded-md">
 | 
			
		||||
@@ -196,9 +200,8 @@
 | 
			
		||||
                                                                <input type="hidden" name="_method" value="put" />
 | 
			
		||||
                                                                <input type="hidden" name="id" value="{{ event.id }}" />
 | 
			
		||||
                                                                {{ macros::input(label='Grund der Absage', name='notes', type='input', value='') }}
 | 
			
		||||
                                                                {{ macros::input(label='', name='max_people', type='hidden', value=0) }}
 | 
			
		||||
                                                                {{ macros::input(label='', name='max_people', type='hidden', value=-1) }}
 | 
			
		||||
                                                                {{ macros::input(label='', name='name', type='hidden', value=event.name) }}
 | 
			
		||||
                                                                {{ macros::input(label='', name='max_people', type='hidden', value=event.max_people) }}
 | 
			
		||||
                                                                {{ macros::input(label='', name='planned_amount_cox', type='hidden', value=event.planned_amount_cox) }}
 | 
			
		||||
                                                                {{ macros::input(label='', name='always_show', type='hidden', value=event.always_show) }}
 | 
			
		||||
                                                                {{ macros::input(label='', name='is_locked', type='hidden', value=event.is_locked) }}
 | 
			
		||||
@@ -228,7 +231,7 @@
 | 
			
		||||
                                            {% if trip.always_show and not day.regular_sees_this_day %}
 | 
			
		||||
                                                <span title="Du siehst diese Ausfahrt schon, obwohl sie mehr als {{ amount_days_to_show_trips_ahead }} Tage in der Zukunft liegt. Du Magier!">🔮</span>
 | 
			
		||||
                                            {% endif -%}
 | 
			
		||||
                                            {% if trip.max_people == 0 %}
 | 
			
		||||
                                            {% if trip.cancelled %}
 | 
			
		||||
                                                <strong class="text-[#f43f5e]">⚠
 | 
			
		||||
                                                    {{ trip.planned_starting_time }}
 | 
			
		||||
                                                Uhr</strong>
 | 
			
		||||
@@ -250,7 +253,7 @@
 | 
			
		||||
                                            {% endif %}
 | 
			
		||||
                                            <br />
 | 
			
		||||
                                            <a href="#" data-sidebar="true" data-trigger="sidebar" data-header="<strong>
 | 
			
		||||
                                                {% if trip.max_people == 0 %}⚠{% endif %}
 | 
			
		||||
                                                {% if trip.cancelled %}⚠{% 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 %}
 | 
			
		||||
@@ -279,7 +282,7 @@
 | 
			
		||||
                                    {# --- START Sidebar Content --- #}
 | 
			
		||||
                                    <div class="hidden">
 | 
			
		||||
                                        <div id="trip{{ trip.trip_details_id }}">
 | 
			
		||||
                                            {% if trip.max_people == 0 %}
 | 
			
		||||
                                            {% if trip.cancelled %}
 | 
			
		||||
                                                {# --- border-[#f43f5e] bg-[#f43f5e] --- #}
 | 
			
		||||
                                                {{ macros::box(participants=trip.rower,bg='[#f43f5e]',header='Absage', trip_details_id=trip.trip_details_id, allow_removing=loggedin_user.id == trip.cox_id) }}
 | 
			
		||||
                                            {% else %}
 | 
			
		||||
@@ -319,13 +322,13 @@
 | 
			
		||||
                                                    </a>
 | 
			
		||||
                                                </div>
 | 
			
		||||
                                            {% else %}
 | 
			
		||||
                                                {% if trip.max_people == 0 %}
 | 
			
		||||
                                                {% if trip.cancelled %}
 | 
			
		||||
                                                    Wenn du deine Absage absagen (:^)) willst, einfach entsprechende Anzahl an Ruderer oben eintragen.
 | 
			
		||||
                                                {% else %}
 | 
			
		||||
                                                    <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 absagen</h3>
 | 
			
		||||
                                                        <form action="/cox/trip/{{ trip.id }}" method="post" class="grid">
 | 
			
		||||
                                                            {{ macros::input(label='', name='max_people', type='hidden', value=0) }}
 | 
			
		||||
                                                            {{ macros::input(label='', name='max_people', type='hidden', value=-1) }}
 | 
			
		||||
                                                            {{ macros::input(label='Grund der Absage', name='notes', type='input', value='') }}
 | 
			
		||||
                                                            {{ 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) }}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user