Merge commit 'e5088bddb0ab694abac5f6d2ab2d76a5042e52bb' into feature/frontend-triptype
# Conflicts: # templates/index.html.tera
This commit is contained in:
		
							
								
								
									
										37
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										37
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
								
							| @@ -1238,9 +1238,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "linux-raw-sys" | ||||
| version = "0.3.4" | ||||
| version = "0.3.6" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "36eb31c1778188ae1e64398743890d0877fef36d11521ac60406b42016e8c2cf" | ||||
| checksum = "b64f40e5e03e0d54f03845c8197d0291253cdbedfb1cb46b13c2c117554a9f4c" | ||||
|  | ||||
| [[package]] | ||||
| name = "lock_api" | ||||
| @@ -1540,9 +1540,9 @@ checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" | ||||
|  | ||||
| [[package]] | ||||
| name = "pest" | ||||
| version = "2.5.7" | ||||
| version = "2.6.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "7b1403e8401ad5dedea73c626b99758535b342502f8d1e361f4a2dd952749122" | ||||
| checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70" | ||||
| dependencies = [ | ||||
|  "thiserror", | ||||
|  "ucd-trie", | ||||
| @@ -1550,9 +1550,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "pest_derive" | ||||
| version = "2.5.7" | ||||
| version = "2.6.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "be99c4c1d2fc2769b1d00239431d711d08f6efedcecb8b6e30707160aee99c15" | ||||
| checksum = "6b79d4c71c865a25a4322296122e3924d30bc8ee0834c8bfc8b95f7f054afbfb" | ||||
| dependencies = [ | ||||
|  "pest", | ||||
|  "pest_generator", | ||||
| @@ -1560,9 +1560,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "pest_generator" | ||||
| version = "2.5.7" | ||||
| version = "2.6.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "e56094789873daa36164de2e822b3888c6ae4b4f9da555a1103587658c805b1e" | ||||
| checksum = "6c435bf1076437b851ebc8edc3a18442796b30f1728ffea6262d59bbe28b077e" | ||||
| dependencies = [ | ||||
|  "pest", | ||||
|  "pest_meta", | ||||
| @@ -1573,9 +1573,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "pest_meta" | ||||
| version = "2.5.7" | ||||
| version = "2.6.0" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "6733073c7cff3d8459fda0e42f13a047870242aed8b509fe98000928975f359e" | ||||
| checksum = "745a452f8eb71e39ffd8ee32b3c5f51d03845f99786fa9b68db6ff509c505411" | ||||
| dependencies = [ | ||||
|  "once_cell", | ||||
|  "pest", | ||||
| @@ -1655,9 +1655,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" | ||||
|  | ||||
| [[package]] | ||||
| name = "pkg-config" | ||||
| version = "0.3.26" | ||||
| version = "0.3.27" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" | ||||
| checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" | ||||
|  | ||||
| [[package]] | ||||
| name = "polyval" | ||||
| @@ -1964,9 +1964,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "rustix" | ||||
| version = "0.37.15" | ||||
| version = "0.37.18" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "a0661814f891c57c930a610266415528da53c4933e6dea5fb350cbfe048a9ece" | ||||
| checksum = "8bbfc1d1c7c40c01715f47d71444744a81669ca84e8b63e25a55e169b1f86433" | ||||
| dependencies = [ | ||||
|  "bitflags 1.3.2", | ||||
|  "errno", | ||||
| @@ -2489,9 +2489,9 @@ dependencies = [ | ||||
|  | ||||
| [[package]] | ||||
| name = "tokio-stream" | ||||
| version = "0.1.13" | ||||
| version = "0.1.14" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "76cd2598a37719e3cd4c28af93f978506a97a2920ef4d96e4b12e38b8cbc8940" | ||||
| checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" | ||||
| dependencies = [ | ||||
|  "futures-core", | ||||
|  "pin-project-lite", | ||||
| @@ -2529,10 +2529,11 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" | ||||
|  | ||||
| [[package]] | ||||
| name = "tracing" | ||||
| version = "0.1.38" | ||||
| version = "0.1.37" | ||||
| source = "registry+https://github.com/rust-lang/crates.io-index" | ||||
| checksum = "cf9cf6a813d3f40c88b0b6b6f29a5c95c6cdbf97c1f9cc53fb820200f5ad814d" | ||||
| checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" | ||||
| dependencies = [ | ||||
|  "cfg-if", | ||||
|  "pin-project-lite", | ||||
|  "tracing-attributes", | ||||
|  "tracing-core", | ||||
|   | ||||
							
								
								
									
										20
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										20
									
								
								README.md
									
									
									
									
									
								
							| @@ -1,20 +1,26 @@ | ||||
| # TODO | ||||
| - [ ] Allow sign-outs only >2h before event | ||||
|  | ||||
| # Icons | ||||
| - Regatta: 🏅 | ||||
| - Lange Ausfahrt: 💪 | ||||
| - Wanderfahrt: ⛱ | ||||
|  | ||||
| # Notes / Bugfixes | ||||
| - [] max_people = 0 -> Rot hervorheben, dass Ausfahrt abgesagt wurde? | ||||
| - [] my trips for cox | ||||
| - [] add `trip_type` (id, name, desc, question, icon) with a FK to `trip_details` | ||||
| ## Frontend | ||||
| - [] add UI for `trip_type` | ||||
| - [] support esc to close sidebar | ||||
| - [] FAQ page  | ||||
|  | ||||
| ## Backend | ||||
| - [] Allow sign-outs only >2h before event | ||||
| - [] add `always_show` to `planned_trips` (e.g. for wanderfahrten) | ||||
| - [] exactly same time -> deny registration | ||||
| - [] `planned_events` -> ICS feed to be [integrated](https://icscalendar.com/shortcode-overview/) in homepage calendar | ||||
| - [] Notification system (-> signal msgs?) e.g. if new event; somebody unregistered | ||||
|  | ||||
| # Nice to have | ||||
| ## Frontend | ||||
| - [] my trips for cox | ||||
|  | ||||
| ## Backend | ||||
| - [] exactly same time -> deny registration | ||||
| - [] automatically add regular planned trip | ||||
| - [] User sync w/ nextcloud | ||||
| - [] remove key from src/rest/admin/rss.rs (line 8); at least before putting code somewhere public | ||||
|   | ||||
| @@ -8,19 +8,29 @@ CREATE TABLE IF NOT EXISTS "user" ( | ||||
| 	"deleted" boolean NOT NULL DEFAULT FALSE | ||||
| ); | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS "trip_type" ( | ||||
| 	"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, | ||||
| 	"name" text NOT NULL UNIQUE, | ||||
| 	"desc" text NOT NULL, | ||||
| 	"question" text NOT NULL, | ||||
| 	"icon" text NOT NULL | ||||
| ); | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS "trip_details" ( | ||||
| 	"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, | ||||
| 	"planned_starting_time" text NOT NULL, | ||||
| 	"max_people" INTEGER NOT NULL, | ||||
| 	"day" TEXT NOT NULL, | ||||
| 	"notes" TEXT | ||||
| 	"allow_guests" boolean NOT NULL default false, | ||||
| 	"notes" TEXT, | ||||
| 	"trip_type_id" INTEGER, | ||||
| 	FOREIGN KEY(trip_type_id) REFERENCES trip_type(id) | ||||
| ); | ||||
|  | ||||
| CREATE TABLE IF NOT EXISTS "planned_event" ( | ||||
| 	"id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, | ||||
| 	"name" text NOT NULL, | ||||
| 	"planned_amount_cox" INTEGER unsigned NOT NULL, | ||||
| 	"allow_guests" boolean NOT NULL default false, | ||||
| 	"trip_details_id" INTEGER NOT NULL, | ||||
| 	"created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||||
| 	FOREIGN KEY(trip_details_id) REFERENCES trip_details(id) ON DELETE CASCADE | ||||
| @@ -52,3 +62,4 @@ CREATE TABLE IF NOT EXISTS "log" ( | ||||
| 	"msg" text NOT NULL, | ||||
| 	"created_at" text NOT NULL DEFAULT CURRENT_TIMESTAMP | ||||
| ); | ||||
|  | ||||
|   | ||||
| @@ -12,3 +12,4 @@ INSERT INTO "planned_event" (name, planned_amount_cox, trip_details_id) VALUES(' | ||||
| INSERT INTO "trip_details" (planned_starting_time, max_people, day, notes) VALUES('11:00', 1, '1970-01-02', 'trip_details for trip from cox'); | ||||
| INSERT INTO "trip" (cox_id, trip_details_id) VALUES(4, 2); | ||||
|  | ||||
| INSERT INTO "trip_type" (name, desc, question, icon) VALUES ('Regatta', 'Regatta!', 'Kein normales Event. Das ist eine Regatta! Willst du wirklich teilnehmen?', '🏅') | ||||
|   | ||||
| @@ -3,22 +3,23 @@ use serde::Serialize; | ||||
| use sqlx::SqlitePool; | ||||
|  | ||||
| use self::{ | ||||
|     planned_event::{PlannedEvent, PlannedEventWithUser}, | ||||
|     trip::{Trip, TripWithUser}, | ||||
|     planned_event::{PlannedEvent, PlannedEventWithUserAndTriptype}, | ||||
|     trip::{Trip, TripWithUserAndType}, | ||||
| }; | ||||
|  | ||||
| pub mod log; | ||||
| pub mod planned_event; | ||||
| pub mod trip; | ||||
| pub mod tripdetails; | ||||
| pub mod triptype; | ||||
| pub mod user; | ||||
| pub mod usertrip; | ||||
|  | ||||
| #[derive(Serialize)] | ||||
| pub struct Day { | ||||
|     day: NaiveDate, | ||||
|     planned_events: Vec<PlannedEventWithUser>, | ||||
|     trips: Vec<TripWithUser>, | ||||
|     planned_events: Vec<PlannedEventWithUserAndTriptype>, | ||||
|     trips: Vec<TripWithUserAndType>, | ||||
| } | ||||
|  | ||||
| impl Day { | ||||
| @@ -29,4 +30,21 @@ impl Day { | ||||
|             trips: Trip::get_for_day(db, day).await, | ||||
|         } | ||||
|     } | ||||
|     pub async fn new_guest(db: &SqlitePool, day: NaiveDate) -> Self { | ||||
|         let mut day = Self::new(db, day).await; | ||||
|  | ||||
|         day.planned_events = day | ||||
|             .planned_events | ||||
|             .into_iter() | ||||
|             .filter(|e| e.planned_event.allow_guests) | ||||
|             .collect(); | ||||
|  | ||||
|         day.trips = day | ||||
|             .trips | ||||
|             .into_iter() | ||||
|             .filter(|t| t.trip.allow_guests) | ||||
|             .collect(); | ||||
|  | ||||
|         day | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -1,26 +1,28 @@ | ||||
| use chrono::NaiveDate; | ||||
| use serde::Serialize; | ||||
| use sqlx::SqlitePool; | ||||
| use sqlx::{FromRow, SqlitePool}; | ||||
|  | ||||
| use super::tripdetails::TripDetails; | ||||
| use super::{tripdetails::TripDetails, triptype::TripType, user::User}; | ||||
|  | ||||
| #[derive(Serialize, Clone)] | ||||
| #[derive(Serialize, Clone, FromRow)] | ||||
| pub struct PlannedEvent { | ||||
|     pub id: i64, | ||||
|     name: String, | ||||
|     planned_amount_cox: i64, | ||||
|     allow_guests: bool, | ||||
|     trip_details_id: i64, | ||||
|     planned_starting_time: String, | ||||
|     max_people: i64, | ||||
|     day: String, | ||||
|     notes: Option<String>, | ||||
|     pub allow_guests: bool, | ||||
|     trip_type_id: Option<i64>, | ||||
| } | ||||
|  | ||||
| #[derive(Serialize)] | ||||
| pub struct PlannedEventWithUser { | ||||
| pub struct PlannedEventWithUserAndTriptype { | ||||
|     #[serde(flatten)] | ||||
|     planned_event: PlannedEvent, | ||||
|     pub planned_event: PlannedEvent, | ||||
|     trip_type: Option<TripType>, | ||||
|     cox_needed: bool, | ||||
|     cox: Vec<Registration>, | ||||
|     rower: Vec<Registration>, | ||||
| @@ -39,7 +41,8 @@ impl PlannedEvent { | ||||
|         sqlx::query_as!( | ||||
|             Self, | ||||
|             " | ||||
| SELECT planned_event.id, name, planned_amount_cox, allow_guests, trip_details_id, planned_starting_time, max_people, day, notes  | ||||
| SELECT | ||||
|     planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id | ||||
| FROM planned_event  | ||||
| INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id | ||||
| WHERE planned_event.id like ? | ||||
| @@ -51,11 +54,14 @@ WHERE planned_event.id like ? | ||||
|         .ok() | ||||
|     } | ||||
|  | ||||
|     pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<PlannedEventWithUser> { | ||||
|     pub async fn get_for_day( | ||||
|         db: &SqlitePool, | ||||
|         day: NaiveDate, | ||||
|     ) -> Vec<PlannedEventWithUserAndTriptype> { | ||||
|         let day = format!("{day}"); | ||||
|         let events = sqlx::query_as!( | ||||
|             PlannedEvent, | ||||
|             "SELECT planned_event.id, name, planned_amount_cox, allow_guests, trip_details_id, planned_starting_time, max_people, day, notes | ||||
|             "SELECT planned_event.id, planned_event.name, planned_amount_cox, trip_details_id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id | ||||
| FROM planned_event | ||||
| INNER JOIN trip_details ON planned_event.trip_details_id = trip_details.id | ||||
| WHERE day=?", | ||||
| @@ -68,17 +74,23 @@ WHERE day=?", | ||||
|         let mut ret = Vec::new(); | ||||
|         for event in events { | ||||
|             let cox = event.get_all_cox(db).await; | ||||
|             ret.push(PlannedEventWithUser { | ||||
|             let mut trip_type = None; | ||||
|             if let Some(trip_type_id) = event.trip_type_id { | ||||
|                 trip_type = TripType::find_by_id(db, trip_type_id).await; | ||||
|             } | ||||
|             ret.push(PlannedEventWithUserAndTriptype { | ||||
|                 planned_event: event.clone(), | ||||
|                 cox_needed: event.planned_amount_cox > cox.len() as i64, | ||||
|                 cox, | ||||
|                 rower: event.get_all_rower(db).await, | ||||
|                 trip_type, | ||||
|             }); | ||||
|         } | ||||
|         ret | ||||
|     } | ||||
|  | ||||
|     async fn get_all_cox(&self, db: &SqlitePool) -> Vec<Registration> { | ||||
|         //TODO: switch to join | ||||
|         sqlx::query_as!( | ||||
|             Registration, | ||||
|             " | ||||
| @@ -96,6 +108,7 @@ FROM trip WHERE planned_event_id = ? | ||||
|     } | ||||
|  | ||||
|     async fn get_all_rower(&self, db: &SqlitePool) -> Vec<Registration> { | ||||
|         //TODO: switch to join | ||||
|         sqlx::query_as!( | ||||
|             Registration, | ||||
|             " | ||||
| @@ -112,16 +125,34 @@ FROM user_trip WHERE trip_details_id = (SELECT trip_details_id FROM planned_even | ||||
|         .unwrap() //Okay, as PlannedEvent can only be created with proper DB backing | ||||
|     } | ||||
|  | ||||
|     //TODO: add tests | ||||
|     pub async fn is_rower_registered(&self, db: &SqlitePool, user: &User) -> bool { | ||||
|         let is_rower = sqlx::query!( | ||||
|             "SELECT count(*) as amount | ||||
|             FROM user_trip | ||||
|             WHERE trip_details_id = | ||||
|                 (SELECT trip_details_id FROM planned_event WHERE id = ?) | ||||
|             AND user_id = ?", | ||||
|             self.id, | ||||
|             user.id | ||||
|         ) | ||||
|         .fetch_one(db) | ||||
|         .await | ||||
|         .unwrap(); //Okay, bc planned_event can only be created with proper DB backing | ||||
|         is_rower.amount > 0 | ||||
|     } | ||||
|  | ||||
|     pub async fn create( | ||||
|         db: &SqlitePool, | ||||
|         name: String, | ||||
|         planned_amount_cox: i32, | ||||
|         allow_guests: bool, | ||||
|         trip_details: TripDetails, | ||||
|     ) { | ||||
|         sqlx::query!( | ||||
|             "INSERT INTO planned_event(name, planned_amount_cox, allow_guests, trip_details_id) VALUES(?, ?, ?, ?)", | ||||
|             name, planned_amount_cox, allow_guests, trip_details.id | ||||
|             "INSERT INTO planned_event(name, planned_amount_cox, trip_details_id) VALUES(?, ?, ?)", | ||||
|             name, | ||||
|             planned_amount_cox, | ||||
|             trip_details.id | ||||
|         ) | ||||
|         .execute(db) | ||||
|         .await | ||||
| @@ -159,7 +190,7 @@ mod test { | ||||
|  | ||||
|         let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap(); | ||||
|  | ||||
|         PlannedEvent::create(&pool, "new-event".into(), 2, false, trip_details).await; | ||||
|         PlannedEvent::create(&pool, "new-event".into(), 2, trip_details).await; | ||||
|  | ||||
|         let res = | ||||
|             PlannedEvent::get_for_day(&pool, NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).await; | ||||
|   | ||||
| @@ -5,6 +5,7 @@ use sqlx::SqlitePool; | ||||
| use super::{ | ||||
|     planned_event::{PlannedEvent, Registration}, | ||||
|     tripdetails::TripDetails, | ||||
|     triptype::TripType, | ||||
|     user::CoxUser, | ||||
| }; | ||||
|  | ||||
| @@ -18,13 +19,16 @@ pub struct Trip { | ||||
|     max_people: i64, | ||||
|     day: String, | ||||
|     notes: Option<String>, | ||||
|     pub allow_guests: bool, | ||||
|     trip_type_id: Option<i64>, | ||||
| } | ||||
|  | ||||
| #[derive(Serialize)] | ||||
| pub struct TripWithUser { | ||||
| pub struct TripWithUserAndType { | ||||
|     #[serde(flatten)] | ||||
|     trip: Trip, | ||||
|     pub trip: Trip, | ||||
|     rower: Vec<Registration>, | ||||
|     trip_type: Option<TripType>, | ||||
| } | ||||
|  | ||||
| impl Trip { | ||||
| @@ -44,7 +48,7 @@ impl Trip { | ||||
|         sqlx::query_as!( | ||||
|             Self, | ||||
|             " | ||||
| SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, notes | ||||
| SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id | ||||
| FROM trip  | ||||
| INNER JOIN trip_details ON trip.trip_details_id = trip_details.id | ||||
| INNER JOIN user ON trip.cox_id = user.id | ||||
| @@ -63,19 +67,7 @@ WHERE trip.id=? | ||||
|         cox: &CoxUser, | ||||
|         planned_event: &PlannedEvent, | ||||
|     ) -> Result<(), CoxHelpError> { | ||||
|         let is_rower = sqlx::query!( | ||||
|             "SELECT count(*) as amount | ||||
|             FROM user_trip | ||||
|             WHERE trip_details_id = | ||||
|                 (SELECT trip_details_id FROM planned_event WHERE id = ?) | ||||
|             AND user_id = ?", | ||||
|             planned_event.id, | ||||
|             cox.id | ||||
|         ) | ||||
|         .fetch_one(db) | ||||
|         .await | ||||
|         .unwrap(); //Okay, bc planned_event can only be created with proper DB backing | ||||
|         if is_rower.amount > 0 { | ||||
|         if planned_event.is_rower_registered(db, &cox).await { | ||||
|             return Err(CoxHelpError::AlreadyRegisteredAsRower); | ||||
|         } | ||||
|  | ||||
| @@ -92,12 +84,12 @@ WHERE trip.id=? | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<TripWithUser> { | ||||
|     pub async fn get_for_day(db: &SqlitePool, day: NaiveDate) -> Vec<TripWithUserAndType> { | ||||
|         let day = format!("{day}"); | ||||
|         let trips = sqlx::query_as!( | ||||
|             Trip, | ||||
|             " | ||||
| SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, notes | ||||
| SELECT trip.id, cox_id, user.name as cox_name, trip_details_id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id | ||||
| FROM trip  | ||||
| INNER JOIN trip_details ON trip.trip_details_id = trip_details.id | ||||
| INNER JOIN user ON trip.cox_id = user.id | ||||
| @@ -108,10 +100,16 @@ WHERE day=? | ||||
|         .fetch_all(db) | ||||
|         .await | ||||
|         .unwrap(); //TODO: fixme | ||||
|  | ||||
|         let mut ret = Vec::new(); | ||||
|         for trip in trips { | ||||
|             ret.push(TripWithUser { | ||||
|             let mut trip_type = None; | ||||
|             if let Some(trip_type_id) = trip.trip_type_id { | ||||
|                 trip_type = TripType::find_by_id(db, trip_type_id).await; | ||||
|             } | ||||
|             ret.push(TripWithUserAndType { | ||||
|                 trip: trip.clone(), | ||||
|                 trip_type, | ||||
|                 rower: trip.get_all_rower(db).await, | ||||
|             }); | ||||
|         } | ||||
|   | ||||
| @@ -8,6 +8,8 @@ pub struct TripDetails { | ||||
|     max_people: i64, | ||||
|     day: String, | ||||
|     notes: Option<String>, | ||||
|     pub allow_guests: bool, | ||||
|     trip_type_id: Option<i64>, | ||||
| } | ||||
|  | ||||
| impl TripDetails { | ||||
| @@ -15,7 +17,7 @@ impl TripDetails { | ||||
|         sqlx::query_as!( | ||||
|             TripDetails, | ||||
|             " | ||||
| SELECT id, planned_starting_time, max_people, day, notes  | ||||
| SELECT id, planned_starting_time, max_people, day, notes, allow_guests, trip_type_id | ||||
| FROM trip_details  | ||||
| WHERE id like ? | ||||
|         ", | ||||
| @@ -33,13 +35,17 @@ WHERE id like ? | ||||
|         max_people: i32, | ||||
|         day: String, | ||||
|         notes: Option<String>, | ||||
|         allow_guests: bool, | ||||
|         trip_type_id: Option<i64>, | ||||
|     ) -> i64 { | ||||
|         let query = sqlx::query!( | ||||
|             "INSERT INTO trip_details(planned_starting_time, max_people, day, notes) VALUES(?, ?, ?, ?)" , | ||||
|             "INSERT INTO trip_details(planned_starting_time, max_people, day, notes, allow_guests, trip_type_id) VALUES(?, ?, ?, ?, ?, ?)" , | ||||
|             planned_starting_time, | ||||
|             max_people, | ||||
|             day, | ||||
|             notes | ||||
|             notes, | ||||
|             allow_guests, | ||||
|             trip_type_id | ||||
|         ) | ||||
|         .execute(db) | ||||
|         .await | ||||
| @@ -57,14 +63,7 @@ WHERE id like ? | ||||
|         .unwrap(); //TODO: fixme | ||||
|         let amount_currently_registered = i64::from(amount_currently_registered.count); | ||||
|  | ||||
|         let amount_allowed_to_register = | ||||
|             sqlx::query!("SELECT max_people FROM trip_details WHERE id = ?", self.id) | ||||
|                 .fetch_one(db) | ||||
|                 .await | ||||
|                 .unwrap(); //Okay, TripDetails can only be created if self.id exists | ||||
|         let amount_allowed_to_register = amount_allowed_to_register.max_people; | ||||
|  | ||||
|         amount_currently_registered >= amount_allowed_to_register | ||||
|         amount_currently_registered >= self.max_people | ||||
|     } | ||||
| } | ||||
|  | ||||
| @@ -94,11 +93,29 @@ mod test { | ||||
|         let pool = testdb!(); | ||||
|  | ||||
|         assert_eq!( | ||||
|             TripDetails::create(&pool, "10:00".into(), 2, "1970-01-01".into(), None).await, | ||||
|             TripDetails::create( | ||||
|                 &pool, | ||||
|                 "10:00".into(), | ||||
|                 2, | ||||
|                 "1970-01-01".into(), | ||||
|                 None, | ||||
|                 false, | ||||
|                 None | ||||
|             ) | ||||
|             .await, | ||||
|             3, | ||||
|         ); | ||||
|         assert_eq!( | ||||
|             TripDetails::create(&pool, "10:00".into(), 2, "1970-01-01".into(), None).await, | ||||
|             TripDetails::create( | ||||
|                 &pool, | ||||
|                 "10:00".into(), | ||||
|                 2, | ||||
|                 "1970-01-01".into(), | ||||
|                 None, | ||||
|                 false, | ||||
|                 None | ||||
|             ) | ||||
|             .await, | ||||
|             4, | ||||
|         ); | ||||
|     } | ||||
| @@ -115,4 +132,6 @@ mod test { | ||||
|     fn test_true_full() { | ||||
|         //TODO: register user for trip_details = 1; check if is_full returns true | ||||
|     } | ||||
|  | ||||
|     //TODO: add new tripdetails test with trip_type != None | ||||
| } | ||||
|   | ||||
							
								
								
									
										55
									
								
								src/model/triptype.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/model/triptype.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use sqlx::{FromRow, SqlitePool}; | ||||
|  | ||||
| #[derive(FromRow, Debug, Serialize, Deserialize, Clone)] | ||||
| pub struct TripType { | ||||
|     pub id: i64, | ||||
|     name: String, | ||||
|     desc: String, | ||||
|     question: String, | ||||
|     icon: String, | ||||
| } | ||||
|  | ||||
| impl TripType { | ||||
|     pub async fn find_by_id(db: &SqlitePool, id: i64) -> Option<Self> { | ||||
|         sqlx::query_as!( | ||||
|             Self, | ||||
|             " | ||||
| SELECT id, name, desc, question, icon  | ||||
| FROM trip_type | ||||
| WHERE id like ? | ||||
|         ", | ||||
|             id | ||||
|         ) | ||||
|         .fetch_one(db) | ||||
|         .await | ||||
|         .ok() | ||||
|     } | ||||
|  | ||||
|     pub async fn all(db: &SqlitePool) -> Vec<Self> { | ||||
|         sqlx::query_as!( | ||||
|             Self, | ||||
|             " | ||||
| SELECT id, name, desc, question, icon | ||||
| FROM trip_type | ||||
|         " | ||||
|         ) | ||||
|         .fetch_all(db) | ||||
|         .await | ||||
|         .unwrap() //TODO: fixme | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod test { | ||||
|     use crate::testdb; | ||||
|  | ||||
|     use sqlx::SqlitePool; | ||||
|  | ||||
|     #[sqlx::test] | ||||
|     fn test_find_true() { | ||||
|         let pool = testdb!(); | ||||
|     } | ||||
|  | ||||
|     //TODO: write tests | ||||
| } | ||||
| @@ -16,8 +16,8 @@ pub struct User { | ||||
|     pub name: String, | ||||
|     pw: Option<String>, | ||||
|     pub is_cox: bool, | ||||
|     is_admin: bool, | ||||
|     is_guest: bool, | ||||
|     pub is_admin: bool, | ||||
|     pub is_guest: bool, | ||||
|     #[serde(default = "bool::default")] | ||||
|     deleted: bool, | ||||
| } | ||||
|   | ||||
| @@ -14,6 +14,10 @@ impl UserTrip { | ||||
|             return Err(UserTripError::EventAlreadyFull); | ||||
|         } | ||||
|  | ||||
|         if user.is_guest && !trip_details.allow_guests { | ||||
|             return Err(UserTripError::GuestNotAllowedForThisEvent); | ||||
|         } | ||||
|  | ||||
|         //check if cox if own event | ||||
|         let is_cox = sqlx::query!( | ||||
|             "SELECT count(*) as amount | ||||
| @@ -30,6 +34,7 @@ impl UserTrip { | ||||
|             return Err(UserTripError::CantRegisterAtOwnEvent); | ||||
|         } | ||||
|  | ||||
|         //TODO: can probably move to trip.rs? | ||||
|         //check if cox if planned_event | ||||
|         let is_cox = sqlx::query!( | ||||
|             "SELECT count(*) as amount | ||||
| @@ -80,6 +85,7 @@ pub enum UserTripError { | ||||
|     AlreadyRegisteredAsCox, | ||||
|     EventAlreadyFull, | ||||
|     CantRegisterAtOwnEvent, | ||||
|     GuestNotAllowedForThisEvent, | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| @@ -180,4 +186,19 @@ mod test { | ||||
|  | ||||
|         assert_eq!(result, UserTripError::AlreadyRegisteredAsCox); | ||||
|     } | ||||
|  | ||||
|     #[sqlx::test] | ||||
|     fn test_fail_create_guest() { | ||||
|         let pool = testdb!(); | ||||
|  | ||||
|         let user = User::find_by_name(&pool, "guest".into()).await.unwrap(); | ||||
|  | ||||
|         let trip_details = TripDetails::find_by_id(&pool, 1).await.unwrap(); | ||||
|  | ||||
|         let result = UserTrip::create(&pool, &user, &trip_details) | ||||
|             .await | ||||
|             .expect_err("Not allowed for guests"); | ||||
|  | ||||
|         assert_eq!(result, UserTripError::GuestNotAllowedForThisEvent); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -18,6 +18,7 @@ struct AddPlannedEventForm { | ||||
|     planned_starting_time: String, | ||||
|     max_people: i32, | ||||
|     notes: Option<String>, | ||||
|     trip_type: Option<i64>, | ||||
| } | ||||
|  | ||||
| #[post("/planned-event", data = "<data>")] | ||||
| @@ -33,6 +34,8 @@ async fn create( | ||||
|         data.max_people, | ||||
|         data.day.clone(), | ||||
|         data.notes.clone(), | ||||
|         data.allow_guests, | ||||
|         data.trip_type, | ||||
|     ) | ||||
|     .await; | ||||
|  | ||||
| @@ -41,14 +44,7 @@ async fn create( | ||||
|                                                                                     //the object | ||||
|  | ||||
|     //TODO: fix clone() | ||||
|     PlannedEvent::create( | ||||
|         db, | ||||
|         data.name.clone(), | ||||
|         data.planned_amount_cox, | ||||
|         data.allow_guests, | ||||
|         trip_details, | ||||
|     ) | ||||
|     .await; | ||||
|     PlannedEvent::create(db, data.name.clone(), data.planned_amount_cox, trip_details).await; | ||||
|  | ||||
|     Flash::success(Redirect::to("/"), "Successfully planned the event") | ||||
| } | ||||
|   | ||||
| @@ -41,7 +41,6 @@ async fn login( | ||||
| ) -> Flash<Redirect> { | ||||
|     let user = User::login(db, login.name.clone(), login.password.clone()).await; | ||||
|  | ||||
|     //TODO: be able to use ? for login. This would get rid of the following match clause. | ||||
|     let user = match user { | ||||
|         Ok(user) => user, | ||||
|         Err(LoginError::NoPasswordSet(user)) => { | ||||
|   | ||||
| @@ -14,13 +14,16 @@ use crate::model::{ | ||||
|     user::CoxUser, | ||||
| }; | ||||
|  | ||||
| //TODO: add constraints (e.g. planned_amount_cox > 0) | ||||
| #[derive(FromForm)] | ||||
| struct AddTripForm { | ||||
|     day: String, | ||||
|     //TODO: properly parse `planned_starting_time` | ||||
|     planned_starting_time: String, | ||||
|     #[field(validate = range(1..))] | ||||
|     max_people: i32, | ||||
|     notes: Option<String>, | ||||
|     trip_type: Option<i64>, | ||||
|     allow_guests: bool, | ||||
| } | ||||
|  | ||||
| #[post("/trip", data = "<data>")] | ||||
| @@ -32,6 +35,8 @@ async fn create(db: &State<SqlitePool>, data: Form<AddTripForm>, cox: CoxUser) - | ||||
|         data.max_people, | ||||
|         data.day.clone(), | ||||
|         data.notes.clone(), | ||||
|         data.allow_guests, | ||||
|         data.trip_type, | ||||
|     ) | ||||
|     .await; | ||||
|     let trip_details = TripDetails::find_by_id(db, trip_details_id).await.unwrap(); //Okay, bc just | ||||
|   | ||||
| @@ -13,6 +13,7 @@ use sqlx::SqlitePool; | ||||
| use crate::model::{ | ||||
|     log::Log, | ||||
|     tripdetails::TripDetails, | ||||
|     triptype::TripType, | ||||
|     user::User, | ||||
|     usertrip::{UserTrip, UserTripError}, | ||||
|     Day, | ||||
| @@ -22,25 +23,39 @@ mod admin; | ||||
| mod auth; | ||||
| mod cox; | ||||
|  | ||||
| fn amount_days_to_show(is_cox: bool) -> i64 { | ||||
|     if is_cox { | ||||
|         let end_of_year = NaiveDate::from_ymd_opt(Local::now().year(), 12, 31).unwrap(); | ||||
|         end_of_year | ||||
|             .signed_duration_since(Local::now().date_naive()) | ||||
|             .num_days() | ||||
|             + 1 | ||||
|     } else { | ||||
|         6 | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[get("/")] | ||||
| async fn index(db: &State<SqlitePool>, user: User, flash: Option<FlashMessage<'_>>) -> Template { | ||||
|     let mut days = Vec::new(); | ||||
|  | ||||
|     let mut show_next_n_days = 6; | ||||
|     if user.is_cox { | ||||
|         let end_of_year = NaiveDate::from_ymd_opt(Local::now().year(), 12, 31).unwrap(); | ||||
|         show_next_n_days = end_of_year | ||||
|             .signed_duration_since(Local::now().date_naive()) | ||||
|             .num_days() | ||||
|             + 1; | ||||
|     let mut context = Context::new(); | ||||
|  | ||||
|     if user.is_cox || user.is_admin { | ||||
|         let triptypes = TripType::all(db).await; | ||||
|         context.insert("trip_types", &triptypes); | ||||
|     } | ||||
|  | ||||
|     let show_next_n_days = amount_days_to_show(user.is_cox); | ||||
|     for i in 0..show_next_n_days { | ||||
|         let date = (Local::now() + Duration::days(i)).date_naive(); | ||||
|         days.push(Day::new(db, date).await); | ||||
|     } | ||||
|  | ||||
|     let mut context = Context::new(); | ||||
|         if user.is_guest { | ||||
|             days.push(Day::new_guest(db, date).await); | ||||
|         } else { | ||||
|             days.push(Day::new(db, date).await); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     if let Some(msg) = flash { | ||||
|         context.insert("flash", &msg.into_inner()); | ||||
| @@ -81,6 +96,10 @@ async fn join(db: &State<SqlitePool>, trip_details_id: i64, user: User) -> Flash | ||||
|             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.", | ||||
|         ), | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -50,10 +50,13 @@ | ||||
|        {% if user.pw %} | ||||
|         <a class="inline-block mt-1 text-primary-600 hover:text-primary-900 underline" href="/admin/user/{{ user.id }}/reset-pw">Passwort zurücksetzen</a> | ||||
|       {% endif %} | ||||
|         <a class="inline-block mt-1 text-primary-600 hover:text-primary-900 underline" href="/admin/user/{{ user.id }}/delete" onclick="return confirm('Really delete user?');">User löschen</a> | ||||
|     </div> | ||||
| 		<div> | ||||
|       <input value="Ändern" type="submit" class="w-28 rounded-md bg-primary-600 px-3 py-2 text-sm font-semibold text-white hover:bg-primary-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-600 cursor-pointer"/> | ||||
| 		<div class="grid gap-3"> | ||||
|       <a href="/admin/user/{{ user.id }}/delete" class="inline-block btn btn-alert" onclick="return confirm('Wirklich löschen?');"> | ||||
|         {% include "includes/delete-icon" %} | ||||
|         Löschen | ||||
|       </a> | ||||
|       <input value="Ändern" type="submit" class="w-28 btn btn-primary"/> | ||||
|     </div> | ||||
| 	</form> | ||||
| {% endfor %} | ||||
|   | ||||
| @@ -14,10 +14,10 @@ | ||||
|       <input type="hidden" name="remember" value="true"> | ||||
|       <div class="-space-y-px rounded-md shadow-sm"> | ||||
|         <div> | ||||
|           {{ macros::input(label='Name', name='name', type='input', required=true, class='rounded-t-md') }} | ||||
|           {{ macros::input(label='Name', name='name', type='input', required=true, class='rounded-t-md',hide_label=true) }} | ||||
|         </div> | ||||
|         <div> | ||||
|           {{ macros::input(label='Passwort', name='password', type='password', class='rounded-b-md') }} | ||||
|           {{ macros::input(label='Passwort', name='password', type='password', class='rounded-b-md',hide_label=true) }} | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|   | ||||
| @@ -9,10 +9,10 @@ | ||||
|     <input type="hidden" name="userid" value="{{ userid }}" /> | ||||
|     <div class="-space-y-px rounded-md shadow-sm"> | ||||
|       <div> | ||||
|         {{ macros::input(label='Passwort', name='password', type='password', required=true, class='rounded-t-md') }} | ||||
|         {{ macros::input(label='Passwort', name='password', type='password', required=true, class='rounded-t-md',hide_label=true) }} | ||||
|       </div> | ||||
|       <div> | ||||
|         {{ macros::input(label='Passwort bestätigen', name='password_confirm', type='password', required=true, class='rounded-b-md') }} | ||||
|         {{ macros::input(label='Passwort bestätigen', name='password_confirm', type='password', required=true, class='rounded-b-md',hide_label=true) }} | ||||
|       </div> | ||||
|     </div> | ||||
|    | ||||
|   | ||||
| @@ -7,7 +7,7 @@ | ||||
|     {{ macros::input(label='Startzeit', name='planned_starting_time', type='time', required=true) }} | ||||
|     {{ macros::input(label='Anzahl Steuerleute', name='planned_amount_cox', type='number', required=true, min='0') }} | ||||
|     {{ macros::input(label='Anzahl Ruderer (ohne Steuerperson)', name='max_people', type='number', required=true, min='0') }} | ||||
|     {{ macros::checkbox(label='Gäste erlauben', name='max_allow_guestspeople') }} | ||||
|     {{ macros::checkbox(label='Gäste erlauben', name='allow_guests') }} | ||||
|     {{ macros::input(label='Anmerkungen', name='notes', type='input') }} | ||||
|     <select name="trip_type"> | ||||
| 	      <option selected value>Reguläre Ausfahrt</option> | ||||
|   | ||||
| @@ -5,6 +5,7 @@ | ||||
|         <input class="day-js" type="hidden" name="day" value="" /> | ||||
|         {{ 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='Gäste erlauben', name='allow_guests') }} | ||||
|         {{ macros::input(label='Anmerkungen', name='notes', type='input') }} | ||||
| 	    <select name="trip_type"> | ||||
| 		      <option selected value>Reguläre Ausfahrt</option> | ||||
|   | ||||
| @@ -24,9 +24,11 @@ | ||||
|     <div class="h-8"></div> | ||||
| {% endmacro header %} | ||||
|  | ||||
| {% macro input(label, name, type, required=false, class='rounded-md', value='', min='') %} | ||||
|   <label for="{{ name }}" class="sr-only">{{ label }}</label> | ||||
|   <input id="{{ name }}" name="{{ name }}" type="{{ type }}" {% if required %} required {% endif %} value="{{ value }}" class="input {{ class }}" placeholder="{{ label }}" {% if min %} min="{{ min }}" {% endif %}> | ||||
| {% macro input(label, name, type, required=false, class='rounded-md', value='', min='', hide_label=false) %} | ||||
| <div> | ||||
|   <label for="{{ name }}" class="{% if hide_label %} sr-only {% else %} small text-gray-600 {% endif %}">{{ label }}</label> | ||||
|   <input id="{{ name }}" name="{{ name }}" type="{{ type }}" {% if required %} required {% endif %} value="{{ value }}" class="input {{ class }}" placeholder="{% if hide_label %}{{ label }}{% endif %}" {% if min %} min="{{ min }}" {% endif %}> | ||||
| </div> | ||||
| {% endmacro input %} | ||||
|  | ||||
| {% macro checkbox(label, name, id='', checked=false) %} | ||||
| @@ -41,7 +43,7 @@ | ||||
| </div> | ||||
| {% endmacro alert %} | ||||
|  | ||||
| {% macro box(participants, empty_seats, header='Freie Plätze:', text='Keine Ruderer angemeldet', bg='primary-600', color='white') %} | ||||
| {% macro box(participants, empty_seats='', header='Freie Plätze:', text='Keine Ruderer angemeldet', bg='primary-600', color='white') %} | ||||
| <div class="text-{{ color }} bg-{{ bg }} text-center p-1 mt-1 rounded-t-md">{{ header }} {{ empty_seats }}</div> | ||||
| <div class="p-2 border border-t-0 border-{{ bg }} mb-4 rounded-b-md"> | ||||
|   {% if participants | length > 0 %} | ||||
|   | ||||
| @@ -142,13 +142,19 @@ | ||||
|         <div class="pt-2 reset-js" data-coxneeded="false"> | ||||
|           <div class="flex justify-between items-center"> | ||||
|             <div> | ||||
|               <strong class="text-primary-900">{{ trip.planned_starting_time }} Uhr</strong> <small | ||||
|                 class="text-gray-600">({{ trip.cox_name }})</small><br /> | ||||
| 		{% if trip.trip_type %} | ||||
| 			Spezielles Event: {{ trip.trip_type.name }} | ||||
| 		{% endif %} | ||||
|               {% if trip.max_people == 0 %} | ||||
|                 <strong class="text-[#f43f5e]">⚠ {{ trip.planned_starting_time }} Uhr</strong> | ||||
|                 <small class="text-[#f43f5e]">(Absage {{ trip.cox_name }})</small> | ||||
|               {% else %} | ||||
|                 <strong class="text-primary-900">{{ trip.planned_starting_time }} Uhr</strong>  | ||||
|                 <small class="text-gray-600">({{ trip.cox_name }})</small> | ||||
|               {% endif %} | ||||
|               <br /> | ||||
|               {% if trip.trip_type %} | ||||
|                 Spezielles Event: {{ trip.trip_type.name }} | ||||
|               {% endif %} | ||||
|               <a href="#" data-sidebar="true" data-trigger="sidebar"  | ||||
|                 data-header="<strong>{{ trip.planned_starting_time }} Uhr</strong> ({{ trip.cox_name }}){% if trip.notes %}<small class='block'>{{ trip.notes }}</small>{% endif %}" | ||||
|                 data-header="<strong>{% if trip.max_people == 0 %}⚠ {% endif %}{{ trip.planned_starting_time }} Uhr</strong> ({{ trip.cox_name }}){% 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 | ||||
| @@ -177,9 +183,15 @@ | ||||
|         {# --- START Sidebar Content --- #} | ||||
|         <div class="hidden"> | ||||
|             <div id="trip{{ trip.trip_details_id }}"> | ||||
|               {% 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') }} | ||||
|             {# --- START Edit Form --- #} | ||||
|               {% 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') }} | ||||
|               {% endif %} | ||||
|  | ||||
|                  {# --- START Edit Form --- #} | ||||
|             {% if trip.cox_id == loggedin_user.id %} | ||||
|             <div class="bg-gray-100 p-3 mt-4 rounded-md"> | ||||
|               <h3 class="text-primary-950 font-bold uppercase tracking-wide mb-2">Ausfahrt bearbeiten</h3> | ||||
| @@ -246,7 +258,13 @@ | ||||
| </div> | ||||
|  | ||||
| {% include "dynamics/sidebar" %} | ||||
| {% include "forms/trip" %} | ||||
| {% include "forms/event" %} | ||||
|  | ||||
| {% if loggedin_user.is_cox %} | ||||
|   {% include "forms/trip" %} | ||||
| {% endif %} | ||||
|  | ||||
| {% if loggedin_user.is_admin %} | ||||
|   {% include "forms/event" %} | ||||
| {% endif %} | ||||
|  | ||||
| {% endblock content %} | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Marie Birner
					Marie Birner