Add an accommodations reservations module (#188)
All checks were successful
continuous-integration/drone/push Build is passing

Add a new module to enable accommodations reservation

![](https://gitea.communiquons.org/attachments/de1f5b12-0a93-40f8-b29d-97665daa6fd5)

Reviewed-on: #188
This commit is contained in:
2024-06-22 21:30:26 +00:00
parent 8ecacbe622
commit 1a890844ef
54 changed files with 4230 additions and 33 deletions

View File

@ -711,8 +711,10 @@ checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-targets 0.52.5",
]
@ -1404,13 +1406,16 @@ dependencies = [
"anyhow",
"base64 0.22.1",
"bcrypt",
"chrono",
"clap",
"diesel",
"diesel_migrations",
"env_logger",
"futures-util",
"httpdate",
"ical",
"image",
"lazy-regex",
"lazy_static",
"lettre",
"light-openid",
@ -1774,6 +1779,15 @@ dependencies = [
"cc",
]
[[package]]
name = "ical"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7cab7543a8b7729a19e2c04309f902861293dcdae6558dfbeb634454d279f6"
dependencies = [
"thiserror",
]
[[package]]
name = "ident_case"
version = "1.0.1"
@ -1939,6 +1953,29 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
[[package]]
name = "lazy-regex"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d12be4595afdf58bd19e4a9f4e24187da2a66700786ff660a418e9059937a4c"
dependencies = [
"lazy-regex-proc_macros",
"once_cell",
"regex",
]
[[package]]
name = "lazy-regex-proc_macros"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44bcd58e6c97a7fcbaffcdc95728b393b8d98933bfadad49ed4097845b57ef0b"
dependencies = [
"proc-macro2",
"quote",
"regex",
"syn 2.0.63",
]
[[package]]
name = "lazy_static"
version = "1.4.0"

View File

@ -10,6 +10,7 @@ log = "0.4.21"
env_logger = "0.11.3"
clap = { version = "4.5.4", features = ["derive", "env"] }
lazy_static = "1.4.0"
lazy-regex = "3.1.0"
anyhow = "1.0.83"
actix-web = "4.5.1"
actix-cors = "0.7.0"
@ -38,3 +39,5 @@ zip = "2.0.0"
mime_guess = "2.0.4"
tempfile = "3.10.1"
base64 = "0.22.0"
ical = { version = "0.11.0", features = ["generator", "ical", "vcard"] }
chrono = "0.4.38"

View File

@ -0,0 +1,6 @@
ALTER TABLE public.families
DROP COLUMN enable_accommodations;
DROP TABLE IF EXISTS accommodations_reservations_cals_urls;
DROP TABLE IF EXISTS accommodations_reservations;
DROP TABLE IF EXISTS accommodations_list;

View File

@ -0,0 +1,52 @@
-- Add column to toggle accommodations module
ALTER TABLE public.families
ADD enable_accommodations boolean NOT NULL DEFAULT false;
COMMENT
ON COLUMN public.families.enable_accommodations IS 'Specify whether accommodations feature is enabled for the family';
-- Create tables
CREATE TABLE IF NOT EXISTS accommodations_list
(
id SERIAL PRIMARY KEY,
family_id integer NOT NULL REFERENCES families,
time_create BIGINT NOT NULL,
time_update BIGINT NOT NULL,
name VARCHAR(50) NOT NULL,
need_validation BOOLEAN NOT NULL DEFAULT true,
description text NULL,
color VARCHAR(6) NULL,
open_to_reservations BOOLEAN NOT NULL DEFAULT false
);
COMMENT ON COLUMN accommodations_list.need_validation is 'true if family admin review is required for validation. False otherwise';
COMMENT ON COLUMN accommodations_list.open_to_reservations is 'true if reservations can be created / updated. False otherwise';
CREATE TABLE IF NOT EXISTS accommodations_reservations
(
id SERIAL PRIMARY KEY,
family_id integer NOT NULL REFERENCES families ON DELETE CASCADE,
accommodation_id integer NOT NULL REFERENCES accommodations_list ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
time_create BIGINT NOT NULL,
time_update BIGINT NOT NULL,
reservation_start BIGINT NOT NULL,
reservation_end BIGINT NOT NULL,
validated BOOLEAN NULL
);
COMMENT ON COLUMN accommodations_reservations.validated is 'null if not reviewed yet. true if reservation is accepted. false if reservation is rejected';
CREATE TABLE IF NOT EXISTS accommodations_reservations_cals_urls
(
id SERIAL PRIMARY KEY,
family_id integer NOT NULL REFERENCES families ON DELETE CASCADE,
accommodation_id integer NULL REFERENCES accommodations_list ON DELETE CASCADE,
user_id INTEGER NOT NULL REFERENCES users ON DELETE CASCADE,
name VARCHAR(50) NOT NULL,
token VARCHAR(50) NOT NULL,
time_create BIGINT NOT NULL,
time_used BIGINT NOT NULL
);
COMMENT ON COLUMN accommodations_reservations_cals_urls.accommodation_id is 'null to get reservations of all accommodations. otherwise get the reservations of the specified accommodation only';

View File

@ -60,6 +60,10 @@ pub struct StaticConstraints {
pub member_country: SizeConstraint,
pub member_sex: SizeConstraint,
pub member_note: SizeConstraint,
pub accommodation_name_len: SizeConstraint,
pub accommodation_description_len: SizeConstraint,
pub accommodation_calendar_name_len: SizeConstraint,
}
impl Default for StaticConstraints {
@ -91,6 +95,10 @@ impl Default for StaticConstraints {
member_country: SizeConstraint::new(0, 2),
member_sex: SizeConstraint::new(0, 1),
member_note: SizeConstraint::new(0, 35000),
accommodation_name_len: SizeConstraint::new(1, 50),
accommodation_description_len: SizeConstraint::new(0, 500),
accommodation_calendar_name_len: SizeConstraint::new(2, 50),
}
}
}
@ -134,3 +142,10 @@ pub const THUMB_WIDTH: u32 = 350;
/// Thumbnail height
pub const THUMB_HEIGHT: u32 = 350;
/// Accommodations reservations calendars tokens len
pub const ACCOMMODATIONS_RESERVATIONS_CALENDARS_TOKENS_LEN: usize = 50;
/// Minimum interval before calendar used time update
pub const ACCOMMODATIONS_RESERVATIONS_CAL_URL_TIME_USED_UPDATE_MIN_INTERVAL: Duration =
Duration::from_secs(60);

View File

@ -0,0 +1,115 @@
use crate::constants::StaticConstraints;
use crate::controllers::HttpResult;
use crate::extractors::accommodation_extractor::FamilyAndAccommodationInPath;
use crate::extractors::family_extractor::{FamilyInPath, FamilyInPathWithAdminMembership};
use crate::models::Accommodation;
use crate::services::accommodations_list_service;
use actix_web::{web, HttpResponse};
#[derive(thiserror::Error, Debug)]
enum AccommodationListControllerErr {
#[error("Invalid name length!")]
InvalidNameLength,
#[error("Invalid description length!")]
InvalidDescriptionLength,
#[error("Malformed color!")]
MalformedColor,
}
#[derive(serde::Deserialize, Clone)]
pub struct AccommodationRequest {
pub name: String,
pub need_validation: bool,
pub description: Option<String>,
pub color: Option<String>,
pub open_to_reservations: bool,
}
impl AccommodationRequest {
pub async fn to_accommodation(self, accommodation: &mut Accommodation) -> anyhow::Result<()> {
let c = StaticConstraints::default();
if !c.accommodation_name_len.validate(&self.name) {
return Err(AccommodationListControllerErr::InvalidNameLength.into());
}
accommodation.name = self.name;
if let Some(d) = &self.description {
if !c.accommodation_description_len.validate(d) {
return Err(AccommodationListControllerErr::InvalidDescriptionLength.into());
}
}
accommodation.description.clone_from(&self.description);
if let Some(c) = &self.color {
if !lazy_regex::regex!("[a-fA-F0-9]{6}").is_match(c) {
return Err(AccommodationListControllerErr::MalformedColor.into());
}
}
accommodation.color.clone_from(&self.color);
accommodation.need_validation = self.need_validation;
accommodation.open_to_reservations = self.open_to_reservations;
Ok(())
}
}
/// Create a new accommodation
pub async fn create(
m: FamilyInPathWithAdminMembership,
req: web::Json<AccommodationRequest>,
) -> HttpResult {
let mut accommodation = accommodations_list_service::create(m.family_id()).await?;
if let Err(e) = req.0.to_accommodation(&mut accommodation).await {
log::error!("Failed to apply accommodation information! {e}");
accommodations_list_service::delete(&mut accommodation).await?;
return Ok(HttpResponse::BadRequest().body(e.to_string()));
}
if let Err(e) = accommodations_list_service::update(&mut accommodation).await {
log::error!("Failed to update accommodation information! {e}");
accommodations_list_service::delete(&mut accommodation).await?;
return Ok(HttpResponse::InternalServerError().finish());
}
Ok(HttpResponse::Ok().json(accommodation))
}
/// Get the full list of accommodations
pub async fn get_full_list(m: FamilyInPath) -> HttpResult {
Ok(HttpResponse::Ok()
.json(accommodations_list_service::get_all_of_family(m.family_id()).await?))
}
/// Get the information of a single accommodation
pub async fn get_single(m: FamilyAndAccommodationInPath) -> HttpResult {
Ok(HttpResponse::Ok().json(&m.to_accommodation()))
}
/// Update an accommodation
pub async fn update(
m: FamilyAndAccommodationInPath,
req: web::Json<AccommodationRequest>,
_admin: FamilyInPathWithAdminMembership,
) -> HttpResult {
let mut accommodation = m.to_accommodation();
if let Err(e) = req.0.to_accommodation(&mut accommodation).await {
log::error!("Failed to parse accommodation information! {e}");
return Ok(HttpResponse::BadRequest().body(e.to_string()));
}
accommodations_list_service::update(&mut accommodation).await?;
Ok(HttpResponse::Accepted().finish())
}
/// Delete an accommodation
pub async fn delete(
m: FamilyAndAccommodationInPath,
_admin: FamilyInPathWithAdminMembership,
) -> HttpResult {
accommodations_list_service::delete(&mut m.to_accommodation()).await?;
Ok(HttpResponse::Ok().finish())
}

View File

@ -0,0 +1,163 @@
use ical::{generator::*, *};
use actix_web::{web, HttpResponse};
use chrono::DateTime;
use crate::constants::StaticConstraints;
use crate::controllers::HttpResult;
use crate::extractors::accommodation_reservation_calendar_extractor::FamilyAndAccommodationReservationCalendarInPath;
use crate::extractors::family_extractor::FamilyInPath;
use crate::models::{AccommodationID, ReservationStatus};
use crate::services::{
accommodations_list_service, accommodations_reservations_calendars_service,
accommodations_reservations_service, families_service,
};
use crate::utils::time_utils::time;
#[derive(serde::Deserialize)]
pub struct CreateCalendarQuery {
accommodation_id: Option<AccommodationID>,
name: String,
}
/// Create a calendar
pub async fn create(a: FamilyInPath, req: web::Json<CreateCalendarQuery>) -> HttpResult {
let accommodation_id = match req.accommodation_id {
Some(i) => {
let accommodation = match accommodations_list_service::get_by_id(i).await {
Ok(a) => a,
Err(e) => {
log::error!("Failed to get accommodation information! {e}");
return Ok(HttpResponse::NotFound()
.json("The accommodation was not found in the family!"));
}
};
if accommodation.family_id() != a.family_id() {
return Ok(
HttpResponse::NotFound().json("The accommodation was not found in the family!")
);
}
Some(accommodation.id())
}
None => None,
};
let conf = StaticConstraints::default();
if !conf.accommodation_calendar_name_len.validate(&req.name) {
return Ok(HttpResponse::BadRequest().json("Invalid accommodation name!"));
}
let calendar = accommodations_reservations_calendars_service::create(
a.user_id(),
a.family_id(),
accommodation_id,
&req.name,
)
.await?;
Ok(HttpResponse::Ok().json(calendar))
}
/// Get the list of calendars of a user
pub async fn get_list(a: FamilyInPath) -> HttpResult {
let users =
accommodations_reservations_calendars_service::get_all_of_user(a.user_id(), a.family_id())
.await?;
Ok(HttpResponse::Ok().json(users))
}
/// Delete a calendar
pub async fn delete(resa: FamilyAndAccommodationReservationCalendarInPath) -> HttpResult {
accommodations_reservations_calendars_service::delete(resa.to_reservation()).await?;
Ok(HttpResponse::Ok().json("Calendar successfully deleted"))
}
fn fmt_date(time: i64) -> String {
let res = DateTime::from_timestamp(time, 0).expect("Failed to parse date");
/*format!(
"{:0>4}{:0>2}{:0>2}T{:0>2}{:0>2}",
res.year(),
res.month(),
res.day(),
res.minute(),
res.second()
)*/
res.format("%Y%m%dT%H%M%S").to_string()
}
#[derive(serde::Deserialize)]
pub struct AnonymousAccessURL {
token: String,
}
/// Get the content of the calendar
pub async fn anonymous_access(req: web::Path<AnonymousAccessURL>) -> HttpResult {
let mut calendar =
match accommodations_reservations_calendars_service::get_by_token(&req.token).await {
Ok(c) => c,
Err(e) => {
log::error!("Calendar information could not be retrieved: {e}");
return Ok(HttpResponse::NotFound().body("Calendar not found!"));
}
};
let accommodations =
accommodations_list_service::get_all_of_family(calendar.family_id()).await?;
let members = families_service::get_memberships_of_family(calendar.family_id()).await?;
// Get calendar associated events
let events = match calendar.accommodation_id() {
None => {
accommodations_reservations_service::get_all_of_family(calendar.family_id()).await?
}
Some(a) => accommodations_reservations_service::get_all_of_accommodation(a).await?,
};
let mut cal = IcalCalendarBuilder::version("2.0")
.gregorian()
.prodid("-//geneit//")
.build();
for ev in events {
let accommodation = accommodations
.iter()
.find(|a| a.id() == ev.accommodation_id())
.unwrap();
let member_name = members
.iter()
.find(|a| a.membership.user_id() == ev.user_id())
.map(|m| m.user_name.as_str())
.unwrap_or("other user");
let event = IcalEventBuilder::tzid("Europe/Paris")
.uid(format!("resa-{}", ev.id().0))
.changed(fmt_date(ev.time_update))
.start(fmt_date(ev.reservation_start))
.end(fmt_date(ev.reservation_end))
.set(ical_property!("SUMMARY", member_name))
.set(ical_property!("LOCATION", &accommodation.name))
.set(ical_property!(
"STATUS",
match ev.status() {
ReservationStatus::Pending => "TENTATIVE",
ReservationStatus::Accepted => "CONFIRMED",
ReservationStatus::Rejected => "CANCELLED",
}
))
.build();
cal.events.push(event);
}
if calendar.should_update_last_used() {
calendar.time_used = time() as i64;
accommodations_reservations_calendars_service::update(&calendar).await?;
}
Ok(HttpResponse::Ok()
.content_type("text/calendar")
.body(cal.generate()))
}

View File

@ -0,0 +1,223 @@
use crate::controllers::HttpResult;
use crate::extractors::accommodation_extractor::FamilyAndAccommodationInPath;
use crate::extractors::accommodation_reservation_extractor::FamilyAndAccommodationReservationInPath;
use crate::extractors::family_extractor::FamilyInPath;
use crate::models::{Accommodation, AccommodationReservationID, NewAccommodationReservation};
use crate::services::accommodations_reservations_service;
use crate::utils::time_utils::time;
use actix_web::{web, HttpResponse};
#[derive(serde::Deserialize)]
pub struct UpdateReservationQuery {
start: usize,
end: usize,
}
impl UpdateReservationQuery {
/// Check whether a reservation request is valid or not
async fn validate(
&self,
a: &Accommodation,
resa_id: Option<AccommodationReservationID>,
) -> anyhow::Result<Option<&str>> {
if !a.open_to_reservations {
return Ok(Some(
"The accommodation is not open to reservations creation / update!",
));
}
if (self.start as i64) < (time() as i64 - 3600 * 24 * 30) {
return Ok(Some("Start time is too far in the past!"));
}
if self.start == self.end {
return Ok(Some("Start and end time must be different!"));
}
if self.start > self.end {
return Ok(Some("End time happens before start time!"));
}
let existing = accommodations_reservations_service::get_reservations_for_time_interval(
a.id(),
self.start,
self.end,
)
.await?;
if existing
.iter()
.any(|r| r.validated != Some(false) && resa_id != Some(r.id()))
{
return Ok(Some("This reservation is in conflict with another one!"));
}
Ok(None)
}
}
/// Create a reservation
pub async fn create_reservation(
a: FamilyAndAccommodationInPath,
req: web::Json<UpdateReservationQuery>,
) -> HttpResult {
if let Some(err) = req.validate(&a, None).await? {
return Ok(HttpResponse::BadRequest().json(err));
}
let mut reservation =
accommodations_reservations_service::create(&NewAccommodationReservation {
family_id: a.family_id().0,
accommodation_id: a.id().0,
user_id: a.membership().user_id().0,
time_create: time() as i64,
time_update: time() as i64,
reservation_start: req.start as i64,
reservation_end: req.end as i64,
})
.await?;
// Auto validate reservation if requested
if !a.need_validation {
reservation.validated = Some(true);
accommodations_reservations_service::update(&mut reservation).await?;
}
Ok(HttpResponse::Ok().json(reservation))
}
/// Get the reservations for a given accommodation
pub async fn get_accommodation_reservations(a: FamilyAndAccommodationInPath) -> HttpResult {
Ok(HttpResponse::Ok()
.json(accommodations_reservations_service::get_all_of_accommodation(a.id()).await?))
}
#[derive(serde::Deserialize)]
pub struct CheckAvailabilityQuery {
start: usize,
end: usize,
}
/// Check reservation availability
pub async fn get_accommodation_reservations_for_interval(
a: FamilyAndAccommodationInPath,
req: web::Query<CheckAvailabilityQuery>,
) -> HttpResult {
if req.start > req.end {
return Ok(HttpResponse::BadRequest().json("start should be smaller than end!"));
}
let res = accommodations_reservations_service::get_reservations_for_time_interval(
a.id(),
req.start,
req.end,
)
.await?;
Ok(HttpResponse::Ok().json(res))
}
/// Get the full list of accommodations reservations for a family
pub async fn full_list(m: FamilyInPath) -> HttpResult {
Ok(HttpResponse::Ok()
.json(accommodations_reservations_service::get_all_of_family(m.family_id()).await?))
}
/// Get a single accommodation reservation
pub async fn get_single(m: FamilyAndAccommodationReservationInPath) -> HttpResult {
Ok(HttpResponse::Ok().json(m.to_reservation()))
}
/// Update a reservation
pub async fn update_single(
m: FamilyAndAccommodationReservationInPath,
req: web::Json<UpdateReservationQuery>,
) -> HttpResult {
if let Some(err) = req.validate(m.as_accommodation(), Some(m.id())).await? {
return Ok(HttpResponse::BadRequest().json(err));
}
if m.membership().user_id() != m.user_id() {
return Ok(
HttpResponse::BadRequest().json("Only the owner of a reservation can change it!")
);
}
let need_validation = m.as_accommodation().need_validation;
let mut reservation = m.to_reservation();
reservation.reservation_start = req.start as i64;
reservation.reservation_end = req.end as i64;
if need_validation {
reservation.validated = None;
}
accommodations_reservations_service::update(&mut reservation).await?;
Ok(HttpResponse::Accepted().finish())
}
/// Delete a reservation
pub async fn delete(m: FamilyAndAccommodationReservationInPath) -> HttpResult {
if m.membership().user_id() != m.user_id() {
return Ok(
HttpResponse::BadRequest().json("Only the owner of a reservation can delete it!")
);
}
accommodations_reservations_service::delete(m.to_reservation()).await?;
Ok(HttpResponse::Accepted().finish())
}
#[derive(serde::Deserialize)]
pub struct ValidateQuery {
validate: bool,
}
/// Validate or reject a reservation
pub async fn validate_or_reject(
m: FamilyAndAccommodationReservationInPath,
q: web::Json<ValidateQuery>,
) -> HttpResult {
if !m.membership().is_admin {
return Ok(
HttpResponse::BadRequest().json("Only a family admin can validate a reservation!")
);
}
if m.validated == Some(q.validate) {
return Ok(
HttpResponse::AlreadyReported().json("This reservation has already been processed!")
);
}
// In case of re-validation, check that the time is still available
if m.validated == Some(false) && q.validate {
let potential_conflicts =
accommodations_reservations_service::get_reservations_for_time_interval(
m.accommodation_id(),
m.reservation_start as usize,
m.reservation_end as usize,
)
.await?;
if potential_conflicts
.iter()
.any(|a| a.validated != Some(false))
{
return Ok(HttpResponse::Conflict().json(
"This cannot be accepted as it would create a conflict with another reservation!",
));
}
}
// Update reservation validation status
let mut reservation = m.to_reservation();
reservation.validated = Some(q.validate);
accommodations_reservations_service::update(&mut reservation).await?;
Ok(HttpResponse::Accepted().finish())
}

View File

@ -80,6 +80,7 @@ struct RichFamilyInfo {
#[serde(flatten)]
membership: FamilyMembership,
enable_genealogy: bool,
enable_accommodations: bool,
disable_couple_photos: bool,
}
@ -90,6 +91,7 @@ pub async fn single_info(f: FamilyInPath) -> HttpResult {
Ok(HttpResponse::Ok().json(RichFamilyInfo {
membership,
enable_genealogy: family.enable_genealogy,
enable_accommodations: family.enable_accommodations,
disable_couple_photos: family.disable_couple_photos,
}))
}
@ -105,6 +107,7 @@ pub async fn leave(f: FamilyInPath) -> HttpResult {
pub struct UpdateFamilyBody {
name: Option<String>,
enable_genealogy: Option<bool>,
enable_accommodations: Option<bool>,
disable_couple_photos: Option<bool>,
}
@ -127,6 +130,10 @@ pub async fn update(
family.enable_genealogy = enable_genealogy;
}
if let Some(enable_accommodations) = req.enable_accommodations {
family.enable_accommodations = enable_accommodations;
}
if let Some(disable_couple_photos) = req.disable_couple_photos {
family.disable_couple_photos = disable_couple_photos;
}

View File

@ -5,6 +5,9 @@ use actix_web::HttpResponse;
use std::fmt::{Debug, Display, Formatter};
use zip::result::ZipError;
pub mod accommodations_list_controller;
pub mod accommodations_reservations_calendars_controller;
pub mod accommodations_reservations_controller;
pub mod auth_controller;
pub mod couples_controller;
pub mod data_controller;

View File

@ -0,0 +1,83 @@
use crate::extractors::family_extractor::FamilyInPath;
use crate::models::{Accommodation, AccommodationID, FamilyID, Membership};
use crate::services::accommodations_list_service;
use actix_web::dev::Payload;
use actix_web::{FromRequest, HttpRequest};
use serde::Deserialize;
use std::ops::Deref;
#[derive(thiserror::Error, Debug)]
enum AccommodationExtractorErr {
#[error("Accommodation {0:?} does not belong to family {1:?}!")]
AccommodationNotInFamily(AccommodationID, FamilyID),
}
#[derive(Debug)]
pub struct FamilyAndAccommodationInPath(Membership, Accommodation);
impl FamilyAndAccommodationInPath {
async fn load_accommodation_from_path(
family: FamilyInPath,
accommodation_id: AccommodationID,
) -> anyhow::Result<Self> {
let accommodation = accommodations_list_service::get_by_id(accommodation_id).await?;
if accommodation.family_id() != family.family_id() {
return Err(AccommodationExtractorErr::AccommodationNotInFamily(
accommodation.id(),
family.family_id(),
)
.into());
}
Ok(Self(family.into(), accommodation))
}
}
impl Deref for FamilyAndAccommodationInPath {
type Target = Accommodation;
fn deref(&self) -> &Self::Target {
&self.1
}
}
impl FamilyAndAccommodationInPath {
pub fn membership(&self) -> &Membership {
&self.0
}
pub fn to_accommodation(self) -> Accommodation {
self.1
}
}
#[derive(Deserialize)]
struct AccommodationIDInPath {
accommodation_id: AccommodationID,
}
impl FromRequest for FamilyAndAccommodationInPath {
type Error = actix_web::Error;
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
let req = req.clone();
Box::pin(async move {
let family = FamilyInPath::extract(&req).await?;
let accommodation_id = actix_web::web::Path::<AccommodationIDInPath>::from_request(
&req,
&mut Payload::None,
)
.await?
.accommodation_id;
Self::load_accommodation_from_path(family, accommodation_id)
.await
.map_err(|e| {
log::error!("Failed to extract accommodation ID from URL! {}", e);
actix_web::error::ErrorNotFound("Could not fetch accommodation information!")
})
})
}
}

View File

@ -0,0 +1,93 @@
use crate::extractors::family_extractor::FamilyInPath;
use crate::models::{
AccommodationReservationCalendar, AccommodationReservationCalendarID, FamilyID, Membership,
};
use crate::services::accommodations_reservations_calendars_service;
use actix_web::dev::Payload;
use actix_web::{FromRequest, HttpRequest};
use serde::Deserialize;
use std::ops::Deref;
#[derive(thiserror::Error, Debug)]
enum AccommodationCalendarExtractorErr {
#[error("Calendar {0:?} does not belong to user or family {1:?}!")]
CalendarNotOfUserOrFamily(AccommodationReservationCalendarID, FamilyID),
}
#[derive(Debug)]
pub struct FamilyAndAccommodationReservationCalendarInPath(
Membership,
AccommodationReservationCalendar,
);
impl FamilyAndAccommodationReservationCalendarInPath {
async fn load_calendar_from_path(
family: FamilyInPath,
calendar_id: AccommodationReservationCalendarID,
) -> anyhow::Result<Self> {
let accommodation =
accommodations_reservations_calendars_service::get_by_id(calendar_id).await?;
if accommodation.family_id() != family.family_id()
|| accommodation.user_id() != family.user_id()
{
return Err(
AccommodationCalendarExtractorErr::CalendarNotOfUserOrFamily(
accommodation.id(),
family.family_id(),
)
.into(),
);
}
Ok(Self(family.into(), accommodation))
}
}
impl Deref for FamilyAndAccommodationReservationCalendarInPath {
type Target = AccommodationReservationCalendar;
fn deref(&self) -> &Self::Target {
&self.1
}
}
impl FamilyAndAccommodationReservationCalendarInPath {
pub fn membership(&self) -> &Membership {
&self.0
}
pub fn to_reservation(self) -> AccommodationReservationCalendar {
self.1
}
}
#[derive(Deserialize)]
struct AccommodationIDInPath {
cal_id: AccommodationReservationCalendarID,
}
impl FromRequest for FamilyAndAccommodationReservationCalendarInPath {
type Error = actix_web::Error;
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
let req = req.clone();
Box::pin(async move {
let family = FamilyInPath::extract(&req).await?;
let accommodation_id = actix_web::web::Path::<AccommodationIDInPath>::from_request(
&req,
&mut Payload::None,
)
.await?
.cal_id;
Self::load_calendar_from_path(family, accommodation_id)
.await
.map_err(|e| {
log::error!("Failed to extract calendar ID from URL! {}", e);
actix_web::error::ErrorNotFound("Could not fetch calendar information!")
})
})
}
}

View File

@ -0,0 +1,103 @@
use crate::extractors::family_extractor::FamilyInPath;
use crate::models::{
Accommodation, AccommodationReservation, AccommodationReservationID, FamilyID, Membership,
};
use crate::services::{accommodations_list_service, accommodations_reservations_service};
use actix_web::dev::Payload;
use actix_web::{FromRequest, HttpRequest};
use serde::Deserialize;
use std::fmt::Debug;
use std::ops::Deref;
#[derive(thiserror::Error, Debug)]
enum AccommodationReservationExtractorErr {
#[error("Accommodation reservation {0:?} does not belong to family {1:?}!")]
AccommodationNotInFamily(AccommodationReservationID, FamilyID),
}
#[derive(Debug)]
pub struct FamilyAndAccommodationReservationInPath(
Membership,
Accommodation,
AccommodationReservation,
);
impl FamilyAndAccommodationReservationInPath {
async fn load_accommodation_reservation_from_path(
family: FamilyInPath,
reservation_id: AccommodationReservationID,
) -> anyhow::Result<Self> {
let reservation = accommodations_reservations_service::get_by_id(reservation_id).await?;
let accommodation =
accommodations_list_service::get_by_id(reservation.accommodation_id()).await?;
if accommodation.family_id() != family.family_id()
|| reservation.family_id() != family.family_id()
{
return Err(
AccommodationReservationExtractorErr::AccommodationNotInFamily(
reservation.id(),
family.family_id(),
)
.into(),
);
}
Ok(Self(family.into(), accommodation, reservation))
}
}
impl Deref for FamilyAndAccommodationReservationInPath {
type Target = AccommodationReservation;
fn deref(&self) -> &Self::Target {
&self.2
}
}
impl FamilyAndAccommodationReservationInPath {
pub fn membership(&self) -> &Membership {
&self.0
}
pub fn as_accommodation(&self) -> &Accommodation {
&self.1
}
pub fn to_accommodation(self) -> Accommodation {
self.1
}
pub fn to_reservation(self) -> AccommodationReservation {
self.2
}
}
#[derive(Deserialize)]
struct ReservationIDInPath {
reservation_id: AccommodationReservationID,
}
impl FromRequest for FamilyAndAccommodationReservationInPath {
type Error = actix_web::Error;
type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future {
let req = req.clone();
Box::pin(async move {
let family = FamilyInPath::extract(&req).await?;
let reservation_id =
actix_web::web::Path::<ReservationIDInPath>::from_request(&req, &mut Payload::None)
.await?
.reservation_id;
Self::load_accommodation_reservation_from_path(family, reservation_id)
.await
.map_err(|e| {
log::error!("Failed to extract accommodation ID from URL! {}", e);
actix_web::error::ErrorNotFound("Could not fetch accommodation information!")
})
})
}
}

View File

@ -1,3 +1,6 @@
pub mod accommodation_extractor;
pub mod accommodation_reservation_calendar_extractor;
pub mod accommodation_reservation_extractor;
pub mod couple_extractor;
pub mod family_extractor;
pub mod member_extractor;

View File

@ -6,8 +6,10 @@ use actix_web::{web, App, HttpServer};
use geneit_backend::app_config::AppConfig;
use geneit_backend::connections::{db_connection, s3_connection};
use geneit_backend::controllers::{
auth_controller, couples_controller, data_controller, families_controller, members_controller,
photos_controller, server_controller, users_controller,
accommodations_list_controller, accommodations_reservations_calendars_controller,
accommodations_reservations_controller, auth_controller, couples_controller, data_controller,
families_controller, members_controller, photos_controller, server_controller,
users_controller,
};
#[actix_web::main]
@ -204,6 +206,79 @@ async fn main() -> std::io::Result<()> {
"/family/{id}/genealogy/data/import",
web::put().to(data_controller::import_family),
)
// [ACCOMODATIONS] List controller
.route(
"/family/{id}/accommodations/list/create",
web::post().to(accommodations_list_controller::create),
)
.route(
"/family/{id}/accommodations/list/list",
web::get().to(accommodations_list_controller::get_full_list),
)
.route(
"/family/{id}/accommodations/list/{accommodation_id}",
web::get().to(accommodations_list_controller::get_single),
)
.route(
"/family/{id}/accommodations/list/{accommodation_id}",
web::put().to(accommodations_list_controller::update),
)
.route(
"/family/{id}/accommodations/list/{accommodation_id}",
web::delete().to(accommodations_list_controller::delete),
)
// [ACCOMODATIONS] Reservations controller
.route(
"/family/{id}/accommodations/reservations/accommodation/{accommodation_id}",
web::get()
.to(accommodations_reservations_controller::get_accommodation_reservations),
)
.route(
"/family/{id}/accommodations/reservations/accommodation/{accommodation_id}/for_interval",
web::get()
.to(accommodations_reservations_controller::get_accommodation_reservations_for_interval),
)
.route(
"/family/{id}/accommodations/reservations/full_list",
web::get().to(accommodations_reservations_controller::full_list),
)
.route(
"/family/{id}/accommodations/reservations/accommodation/{accommodation_id}/create",
web::post().to(accommodations_reservations_controller::create_reservation),
)
.route(
"/family/{id}/accommodations/reservation/{reservation_id}",
web::get().to(accommodations_reservations_controller::get_single),
)
.route(
"/family/{id}/accommodations/reservation/{reservation_id}",
web::patch().to(accommodations_reservations_controller::update_single),
)
.route(
"/family/{id}/accommodations/reservation/{reservation_id}",
web::delete().to(accommodations_reservations_controller::delete),
)
.route(
"/family/{id}/accommodations/reservation/{reservation_id}/validate",
web::post().to(accommodations_reservations_controller::validate_or_reject),
)
// [ACCOMMODATIONS] Calendars controller
.route(
"/family/{id}/accommodations/reservations_calendars/create",
web::post().to(accommodations_reservations_calendars_controller::create),
)
.route(
"/family/{id}/accommodations/reservations_calendars/list",
web::get().to(accommodations_reservations_calendars_controller::get_list),
)
.route(
"/family/{id}/accommodations/reservations_calendars/{cal_id}",
web::delete().to(accommodations_reservations_calendars_controller::delete),
)
.route(
"/acccommodations_calendar/{token}",
web::get().to(accommodations_reservations_calendars_controller::anonymous_access),
)
// Photos controller
.route(
"/photo/{id}",

View File

@ -1,6 +1,11 @@
use crate::app_config::AppConfig;
use crate::schema::{couples, families, members, memberships, photos, users};
use crate::constants;
use crate::schema::{
accommodations_list, accommodations_reservations, accommodations_reservations_cals_urls,
couples, families, members, memberships, photos, users,
};
use crate::utils::crypt_utils::sha256;
use crate::utils::time_utils::time;
use diesel::prelude::*;
/// User ID holder
@ -66,6 +71,7 @@ pub struct Family {
pub invitation_code: String,
pub disable_couple_photos: bool,
pub enable_genealogy: bool,
pub enable_accommodations: bool,
}
impl Family {
@ -308,7 +314,7 @@ pub struct NewMember {
pub time_update: i64,
}
/// Member ID holder
/// Couple ID holder
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
pub struct CoupleID(pub i32);
@ -441,3 +447,153 @@ pub struct NewCouple {
pub time_create: i64,
pub time_update: i64,
}
/// Accommodation ID holder
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
pub struct AccommodationID(pub i32);
#[derive(Queryable, Debug, serde::Serialize)]
pub struct Accommodation {
id: i32,
family_id: i32,
time_create: i64,
pub time_update: i64,
pub name: String,
pub need_validation: bool,
pub description: Option<String>,
pub color: Option<String>,
pub open_to_reservations: bool,
}
impl Accommodation {
pub fn id(&self) -> AccommodationID {
AccommodationID(self.id)
}
pub fn family_id(&self) -> FamilyID {
FamilyID(self.family_id)
}
}
#[derive(Insertable)]
#[diesel(table_name = accommodations_list)]
pub struct NewAccommodation {
pub family_id: i32,
pub name: String,
pub time_create: i64,
pub time_update: i64,
}
/// Accommodation reservation ID holder
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
pub struct AccommodationReservationID(pub i32);
pub enum ReservationStatus {
Pending,
Accepted,
Rejected,
}
#[derive(Queryable, Debug, serde::Serialize)]
pub struct AccommodationReservation {
id: i32,
family_id: i32,
accommodation_id: i32,
user_id: i32,
time_create: i64,
pub time_update: i64,
pub reservation_start: i64,
pub reservation_end: i64,
pub validated: Option<bool>,
}
impl AccommodationReservation {
pub fn id(&self) -> AccommodationReservationID {
AccommodationReservationID(self.id)
}
pub fn accommodation_id(&self) -> AccommodationID {
AccommodationID(self.accommodation_id)
}
pub fn family_id(&self) -> FamilyID {
FamilyID(self.family_id)
}
pub fn user_id(&self) -> UserID {
UserID(self.user_id)
}
pub fn status(&self) -> ReservationStatus {
match self.validated {
None => ReservationStatus::Pending,
Some(true) => ReservationStatus::Accepted,
Some(false) => ReservationStatus::Rejected,
}
}
}
#[derive(Insertable)]
#[diesel(table_name = accommodations_reservations)]
pub struct NewAccommodationReservation {
pub family_id: i32,
pub accommodation_id: i32,
pub user_id: i32,
pub time_create: i64,
pub time_update: i64,
pub reservation_start: i64,
pub reservation_end: i64,
}
/// Accommodation reservation calendar ID holder
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize, Eq, PartialEq, Hash)]
pub struct AccommodationReservationCalendarID(pub i32);
#[derive(Queryable, Debug, serde::Serialize)]
pub struct AccommodationReservationCalendar {
id: i32,
family_id: i32,
accommodation_id: Option<i32>,
user_id: i32,
name: String,
token: String,
pub time_create: i64,
pub time_used: i64,
}
impl AccommodationReservationCalendar {
pub fn id(&self) -> AccommodationReservationCalendarID {
AccommodationReservationCalendarID(self.id)
}
pub fn accommodation_id(&self) -> Option<AccommodationID> {
self.accommodation_id.map(AccommodationID)
}
pub fn family_id(&self) -> FamilyID {
FamilyID(self.family_id)
}
pub fn user_id(&self) -> UserID {
UserID(self.user_id)
}
pub fn should_update_last_used(&self) -> bool {
(self.time_used
+ constants::ACCOMMODATIONS_RESERVATIONS_CAL_URL_TIME_USED_UPDATE_MIN_INTERVAL.as_secs()
as i64)
< time() as i64
}
}
#[derive(Insertable)]
#[diesel(table_name = accommodations_reservations_cals_urls)]
pub struct NewAccommodationReservationCalendar {
pub family_id: i32,
pub accommodation_id: Option<i32>,
pub user_id: i32,
pub name: String,
pub token: String,
pub time_create: i64,
pub time_used: i64,
}

View File

@ -1,5 +1,50 @@
// @generated automatically by Diesel CLI.
diesel::table! {
accommodations_list (id) {
id -> Int4,
family_id -> Int4,
time_create -> Int8,
time_update -> Int8,
#[max_length = 50]
name -> Varchar,
need_validation -> Bool,
description -> Nullable<Text>,
#[max_length = 6]
color -> Nullable<Varchar>,
open_to_reservations -> Bool,
}
}
diesel::table! {
accommodations_reservations (id) {
id -> Int4,
family_id -> Int4,
accommodation_id -> Int4,
user_id -> Int4,
time_create -> Int8,
time_update -> Int8,
reservation_start -> Int8,
reservation_end -> Int8,
validated -> Nullable<Bool>,
}
}
diesel::table! {
accommodations_reservations_cals_urls (id) {
id -> Int4,
family_id -> Int4,
accommodation_id -> Nullable<Int4>,
user_id -> Int4,
#[max_length = 50]
name -> Varchar,
#[max_length = 50]
token -> Varchar,
time_create -> Int8,
time_used -> Int8,
}
}
diesel::table! {
couples (id) {
id -> Int4,
@ -30,6 +75,7 @@ diesel::table! {
invitation_code -> Varchar,
disable_couple_photos -> Bool,
enable_genealogy -> Bool,
enable_accommodations -> Bool,
}
}
@ -119,6 +165,13 @@ diesel::table! {
}
}
diesel::joinable!(accommodations_list -> families (family_id));
diesel::joinable!(accommodations_reservations -> accommodations_list (accommodation_id));
diesel::joinable!(accommodations_reservations -> families (family_id));
diesel::joinable!(accommodations_reservations -> users (user_id));
diesel::joinable!(accommodations_reservations_cals_urls -> accommodations_list (accommodation_id));
diesel::joinable!(accommodations_reservations_cals_urls -> families (family_id));
diesel::joinable!(accommodations_reservations_cals_urls -> users (user_id));
diesel::joinable!(couples -> families (family_id));
diesel::joinable!(couples -> photos (photo_id));
diesel::joinable!(members -> families (family_id));
@ -127,6 +180,9 @@ diesel::joinable!(memberships -> families (family_id));
diesel::joinable!(memberships -> users (user_id));
diesel::allow_tables_to_appear_in_same_query!(
accommodations_list,
accommodations_reservations,
accommodations_reservations_cals_urls,
couples,
families,
members,

View File

@ -0,0 +1,103 @@
use crate::connections::db_connection;
use crate::models::{Accommodation, AccommodationID, FamilyID, NewAccommodation};
use crate::schema::accommodations_list;
use crate::utils::time_utils::time;
use diesel::prelude::*;
/// Create a new accommodation
pub async fn create(family_id: FamilyID) -> anyhow::Result<Accommodation> {
db_connection::execute(|conn| {
let res: Accommodation = diesel::insert_into(accommodations_list::table)
.values(&NewAccommodation {
family_id: family_id.0,
name: "".to_string(),
time_create: time() as i64,
time_update: time() as i64,
})
.get_result(conn)?;
Ok(res)
})
}
/// Get the information of an accommodation
pub async fn get_by_id(id: AccommodationID) -> anyhow::Result<Accommodation> {
db_connection::execute(|conn| {
accommodations_list::table
.filter(accommodations_list::dsl::id.eq(id.0))
.first(conn)
})
}
/// Get all the accommodations of a family
pub async fn get_all_of_family(id: FamilyID) -> anyhow::Result<Vec<Accommodation>> {
db_connection::execute(|conn| {
accommodations_list::table
.filter(accommodations_list::dsl::family_id.eq(id.0))
.get_results(conn)
})
}
/// Check whether accommodation with a given id exists or not
pub async fn exists(
family_id: FamilyID,
accommodation_id: AccommodationID,
) -> anyhow::Result<bool> {
db_connection::execute(|conn| {
let count: i64 = accommodations_list::table
.filter(
accommodations_list::id
.eq(accommodation_id.0)
.and(accommodations_list::family_id.eq(family_id.0)),
)
.count()
.get_result(conn)?;
Ok(count != 0)
})
}
/// Update the information of an accommodation
pub async fn update(accommodation: &mut Accommodation) -> anyhow::Result<()> {
accommodation.time_update = time() as i64;
db_connection::execute(|conn| {
diesel::update(
accommodations_list::dsl::accommodations_list
.filter(accommodations_list::dsl::id.eq(accommodation.id().0)),
)
.set((
accommodations_list::dsl::time_update.eq(accommodation.time_update),
accommodations_list::dsl::name.eq(accommodation.name.to_string()),
accommodations_list::dsl::need_validation.eq(accommodation.need_validation),
accommodations_list::dsl::description.eq(accommodation.description.clone()),
accommodations_list::dsl::color.eq(accommodation.color.clone()),
accommodations_list::dsl::open_to_reservations.eq(accommodation.open_to_reservations),
))
.execute(conn)
})?;
Ok(())
}
/// Delete an accommodation
pub async fn delete(accommodation: &mut Accommodation) -> anyhow::Result<()> {
// Remove the accommodation
db_connection::execute(|conn| {
diesel::delete(
accommodations_list::dsl::accommodations_list
.filter(accommodations_list::dsl::id.eq(accommodation.id().0)),
)
.execute(conn)
})?;
Ok(())
}
/// Delete all the accommodations of a family
pub async fn delete_all_family(family_id: FamilyID) -> anyhow::Result<()> {
for mut m in get_all_of_family(family_id).await? {
delete(&mut m).await?;
}
Ok(())
}

View File

@ -0,0 +1,98 @@
use crate::connections::db_connection;
use crate::constants;
use crate::models::{
AccommodationID, AccommodationReservationCalendar, AccommodationReservationCalendarID,
FamilyID, NewAccommodationReservationCalendar, UserID,
};
use crate::schema::accommodations_reservations_cals_urls;
use crate::utils::string_utils::rand_str;
use crate::utils::time_utils::time;
use diesel::prelude::*;
/// Create a new reservation calendar entry
pub async fn create(
user_id: UserID,
family_id: FamilyID,
accommodation_id: Option<AccommodationID>,
name: &str,
) -> anyhow::Result<AccommodationReservationCalendar> {
db_connection::execute(|conn| {
let res: AccommodationReservationCalendar =
diesel::insert_into(accommodations_reservations_cals_urls::table)
.values(&NewAccommodationReservationCalendar {
family_id: family_id.0,
accommodation_id: accommodation_id.map(|i| i.0),
user_id: user_id.0,
name: name.to_string(),
token: rand_str(constants::ACCOMMODATIONS_RESERVATIONS_CALENDARS_TOKENS_LEN),
time_create: time() as i64,
time_used: time() as i64,
})
.get_result(conn)?;
Ok(res)
})
}
/// Update the information of a reservations calendar
pub async fn update(cal: &AccommodationReservationCalendar) -> anyhow::Result<()> {
db_connection::execute(|conn| {
diesel::update(
accommodations_reservations_cals_urls::dsl::accommodations_reservations_cals_urls
.filter(accommodations_reservations_cals_urls::dsl::id.eq(cal.id().0)),
)
.set((accommodations_reservations_cals_urls::dsl::time_used.eq(cal.time_used),))
.execute(conn)
})?;
Ok(())
}
/// Get all the calendars of a user
pub async fn get_all_of_user(
user: UserID,
family: FamilyID,
) -> anyhow::Result<Vec<AccommodationReservationCalendar>> {
db_connection::execute(|conn| {
accommodations_reservations_cals_urls::table
.filter(
accommodations_reservations_cals_urls::dsl::family_id
.eq(family.0)
.and(accommodations_reservations_cals_urls::dsl::user_id.eq(user.0)),
)
.get_results(conn)
})
}
/// Get a single calendar by its id
pub async fn get_by_id(
id: AccommodationReservationCalendarID,
) -> anyhow::Result<AccommodationReservationCalendar> {
db_connection::execute(|conn| {
accommodations_reservations_cals_urls::table
.filter(accommodations_reservations_cals_urls::dsl::id.eq(id.0))
.get_result(conn)
})
}
/// Get a single calendar by its token
pub async fn get_by_token(token: &str) -> anyhow::Result<AccommodationReservationCalendar> {
db_connection::execute(|conn| {
accommodations_reservations_cals_urls::table
.filter(accommodations_reservations_cals_urls::dsl::token.eq(token))
.get_result(conn)
})
}
/// Delete a calendar
pub async fn delete(r: AccommodationReservationCalendar) -> anyhow::Result<()> {
db_connection::execute(|conn| {
diesel::delete(
accommodations_reservations_cals_urls::dsl::accommodations_reservations_cals_urls
.filter(accommodations_reservations_cals_urls::dsl::id.eq(r.id().0)),
)
.execute(conn)
})?;
Ok(())
}

View File

@ -0,0 +1,101 @@
use crate::connections::db_connection;
use crate::models::{
AccommodationID, AccommodationReservation, AccommodationReservationID, FamilyID,
NewAccommodationReservation,
};
use crate::schema::accommodations_reservations;
use crate::utils::time_utils::time;
use diesel::prelude::*;
/// Create a new reservation
pub async fn create(new: &NewAccommodationReservation) -> anyhow::Result<AccommodationReservation> {
db_connection::execute(|conn| {
let res: AccommodationReservation = diesel::insert_into(accommodations_reservations::table)
.values(new)
.get_result(conn)?;
Ok(res)
})
}
/// Update a reservation
pub async fn update(r: &mut AccommodationReservation) -> anyhow::Result<()> {
r.time_update = time() as i64;
db_connection::execute(|conn| {
diesel::update(
accommodations_reservations::dsl::accommodations_reservations
.filter(accommodations_reservations::dsl::id.eq(r.id().0)),
)
.set((
accommodations_reservations::dsl::time_update.eq(r.time_update),
accommodations_reservations::dsl::validated.eq(r.validated),
accommodations_reservations::dsl::reservation_start.eq(r.reservation_start),
accommodations_reservations::dsl::reservation_end.eq(r.reservation_end),
))
.execute(conn)
})?;
Ok(())
}
/// Delete a reservation
pub async fn delete(r: AccommodationReservation) -> anyhow::Result<()> {
// Remove the reservation
db_connection::execute(|conn| {
diesel::delete(
accommodations_reservations::dsl::accommodations_reservations
.filter(accommodations_reservations::dsl::id.eq(r.id().0)),
)
.execute(conn)
})?;
Ok(())
}
/// Get all the reservations of an accommodation
pub async fn get_all_of_accommodation(
id: AccommodationID,
) -> anyhow::Result<Vec<AccommodationReservation>> {
db_connection::execute(|conn| {
accommodations_reservations::table
.filter(accommodations_reservations::dsl::accommodation_id.eq(id.0))
.get_results(conn)
})
}
/// Get all the reservations of a family
pub async fn get_all_of_family(id: FamilyID) -> anyhow::Result<Vec<AccommodationReservation>> {
db_connection::execute(|conn| {
accommodations_reservations::table
.filter(accommodations_reservations::dsl::family_id.eq(id.0))
.get_results(conn)
})
}
/// Get a single accommodation reservation by its id
pub async fn get_by_id(id: AccommodationReservationID) -> anyhow::Result<AccommodationReservation> {
db_connection::execute(|conn| {
accommodations_reservations::table
.filter(accommodations_reservations::dsl::id.eq(id.0))
.get_result(conn)
})
}
/// Get the reservations that are between a given interval of time for a given accommodation
pub async fn get_reservations_for_time_interval(
id: AccommodationID,
start: usize,
end: usize,
) -> anyhow::Result<Vec<AccommodationReservation>> {
db_connection::execute(|conn| {
accommodations_reservations::table
.filter(
accommodations_reservations::dsl::accommodation_id
.eq(id.0)
.and(accommodations_reservations::dsl::reservation_start.lt((end) as i64))
.and(accommodations_reservations::dsl::reservation_end.gt((start) as i64)),
)
.get_results(conn)
})
}

View File

@ -5,7 +5,9 @@ use crate::models::{
Family, FamilyID, FamilyMembership, Membership, NewFamily, NewMembership, UserID,
};
use crate::schema::{families, memberships};
use crate::services::{couples_service, members_service, users_service};
use crate::services::{
accommodations_list_service, couples_service, members_service, users_service,
};
use crate::utils::string_utils::rand_str;
use crate::utils::time_utils::time;
use diesel::prelude::*;
@ -127,9 +129,9 @@ pub async fn update_membership(membership: &Membership) -> anyhow::Result<()> {
#[derive(serde::Serialize)]
pub struct FamilyMember {
#[serde(flatten)]
membership: Membership,
user_name: String,
user_mail: String,
pub membership: Membership,
pub user_name: String,
pub user_mail: String,
}
/// Get information about the users of a family
@ -175,6 +177,7 @@ pub async fn update_family(family: &Family) -> anyhow::Result<()> {
families::dsl::name.eq(family.name.clone()),
families::dsl::invitation_code.eq(family.invitation_code.clone()),
families::dsl::enable_genealogy.eq(family.enable_genealogy),
families::dsl::enable_accommodations.eq(family.enable_accommodations),
families::dsl::disable_couple_photos.eq(family.disable_couple_photos),
))
.execute(conn)
@ -185,6 +188,9 @@ pub async fn update_family(family: &Family) -> anyhow::Result<()> {
/// Delete a family
pub async fn delete_family(family_id: FamilyID) -> anyhow::Result<()> {
// Delete all family accommodations
accommodations_list_service::delete_all_family(family_id).await?;
// Delete all family couples
couples_service::delete_all_family(family_id).await?;

View File

@ -1,5 +1,8 @@
//! # Backend services
pub mod accommodations_list_service;
pub mod accommodations_reservations_calendars_service;
pub mod accommodations_reservations_service;
pub mod couples_service;
pub mod families_service;
pub mod login_token_service;