diff --git a/.vscode/launch.json b/.vscode/launch.json index 385af8d09..054626503 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -10,7 +10,8 @@ "name": "Debug", "program": "${workspaceFolder}/backend/target/debug/backend", "args": [], - "cwd": "${workspaceFolder}/backend" + "cwd": "${workspaceFolder}/backend", + "envFile": "${workspaceFolder}/backend/.env" }, { "type": "chrome", diff --git a/backend/migrations/2024-01-24-184709_plantings_auditability/down.sql b/backend/migrations/2024-01-24-184709_plantings_auditability/down.sql new file mode 100644 index 000000000..8c2b4889a --- /dev/null +++ b/backend/migrations/2024-01-24-184709_plantings_auditability/down.sql @@ -0,0 +1,33 @@ +-- seeds +ALTER TABLE seeds +RENAME COLUMN created_by TO owner_id; + +-- plantings +ALTER TABLE plantings +DROP COLUMN created_at, +DROP COLUMN modified_at, +DROP COLUMN created_by, +DROP COLUMN modified_by; + +ALTER TABLE plantings +ALTER COLUMN notes DROP NOT NULL, +ALTER COLUMN notes DROP DEFAULT; + +-- maps +ALTER TABLE maps +ADD COLUMN creation_date date; + +UPDATE maps +SET + creation_date = created_at; + +ALTER TABLE maps +DROP COLUMN created_at, +DROP COLUMN modified_at, +DROP COLUMN modified_by; + +ALTER TABLE maps +RENAME COLUMN created_by TO owner_id; + +ALTER TABLE maps +ALTER COLUMN creation_date SET NOT NULL; diff --git a/backend/migrations/2024-01-24-184709_plantings_auditability/up.sql b/backend/migrations/2024-01-24-184709_plantings_auditability/up.sql new file mode 100644 index 000000000..5ca8e2d3f --- /dev/null +++ b/backend/migrations/2024-01-24-184709_plantings_auditability/up.sql @@ -0,0 +1,56 @@ +-- maps +ALTER TABLE maps +RENAME COLUMN owner_id TO created_by; + +ALTER TABLE maps +ADD COLUMN created_at timestamp (0) without time zone, +ADD COLUMN modified_at timestamp (0) without time zone, +ADD COLUMN modified_by uuid; + +UPDATE maps +SET + created_at = date_trunc('day', creation_date) + interval '12:00:00', + modified_at = date_trunc('day', creation_date) + interval '12:00:00', + modified_by = created_by; + +ALTER TABLE maps +DROP COLUMN creation_date; + +ALTER TABLE maps +ALTER COLUMN created_at SET DEFAULT now(), +ALTER COLUMN created_at SET NOT NULL, +ALTER COLUMN modified_at SET DEFAULT now(), +ALTER COLUMN modified_at SET NOT NULL, +ALTER COLUMN created_by SET NOT NULL, +ALTER COLUMN modified_by SET NOT NULL; + +-- plantings +ALTER TABLE plantings +ADD COLUMN created_at timestamp (0) without time zone, +ADD COLUMN modified_at timestamp (0) without time zone, +ADD COLUMN created_by uuid, +ADD COLUMN modified_by uuid; + +UPDATE plantings +SET + created_at = maps.created_at, + modified_at = maps.modified_at, + created_by = maps.created_by, + modified_by = maps.created_by +FROM layers +INNER JOIN maps ON layers.map_id = maps.id +WHERE plantings.layer_id = layers.id; + +ALTER TABLE plantings +ALTER COLUMN created_at SET DEFAULT now(), +ALTER COLUMN created_at SET NOT NULL, +ALTER COLUMN modified_at SET DEFAULT now(), +ALTER COLUMN modified_at SET NOT NULL, +ALTER COLUMN created_by SET NOT NULL, +ALTER COLUMN modified_by SET NOT NULL, +ALTER COLUMN notes SET DEFAULT '', +ALTER COLUMN notes SET NOT NULL; + +-- seeds +ALTER TABLE seeds +RENAME COLUMN owner_id TO created_by; diff --git a/backend/src/config/api_doc.rs b/backend/src/config/api_doc.rs index b611150fc..8903fe967 100644 --- a/backend/src/config/api_doc.rs +++ b/backend/src/config/api_doc.rs @@ -21,9 +21,8 @@ use crate::{ TimelinePagePlantingsDto, }, plantings::{ - MovePlantingDto, NewPlantingDto, PlantingDto, TransformPlantingDto, - UpdateAddDatePlantingDto, UpdatePlantingDto, UpdatePlantingNoteDto, - UpdateRemoveDatePlantingDto, + MovePlantingDto, PlantingDto, TransformPlantingDto, UpdateAddDatePlantingDto, + UpdatePlantingDto, UpdatePlantingNoteDto, UpdateRemoveDatePlantingDto, }, timeline::{TimelineDto, TimelineEntryDto}, BaseLayerImageDto, ConfigDto, Coordinates, GainedBlossomsDto, GuidedToursDto, LayerDto, @@ -173,9 +172,8 @@ struct BaseLayerImagesApiDoc; ), components( schemas( - PlantingDto, TimelinePagePlantingsDto, - NewPlantingDto, + PlantingDto, UpdatePlantingDto, TransformPlantingDto, MovePlantingDto, diff --git a/backend/src/controller/plantings.rs b/backend/src/controller/plantings.rs index 8b12c669b..3e582c9d9 100644 --- a/backend/src/controller/plantings.rs +++ b/backend/src/controller/plantings.rs @@ -58,7 +58,7 @@ pub async fn find( ), request_body = ActionDtoWrapperNewPlantings, responses( - (status = 201, description = "Create plantings", body = Vec) + (status = 201, description = "Create plantings", body = Vec) ), security( ("oauth2" = []) @@ -75,7 +75,7 @@ pub async fn create( let ActionDtoWrapper { action_id, dto } = new_plantings.into_inner(); - let created_plantings = plantings::create(dto, &app_data).await?; + let created_plantings = plantings::create(dto, map_id, user_info.id, &app_data).await?; app_data .broadcaster @@ -116,7 +116,7 @@ pub async fn update( let ActionDtoWrapper { action_id, dto } = update_planting.into_inner(); - let updated_plantings = plantings::update(dto.clone(), &app_data).await?; + let updated_plantings = plantings::update(dto.clone(), map_id, user_info.id, &app_data).await?; let action = match &dto { UpdatePlantingDto::Transform(dto) => { @@ -169,7 +169,7 @@ pub async fn delete( let ActionDtoWrapper { action_id, dto } = delete_planting.into_inner(); - plantings::delete_by_ids(dto.clone(), &app_data).await?; + plantings::delete_by_ids(dto.clone(), map_id, user_info.id, &app_data).await?; app_data .broadcaster diff --git a/backend/src/model/dto.rs b/backend/src/model/dto.rs index 27c07ca18..2259f388b 100644 --- a/backend/src/model/dto.rs +++ b/backend/src/model/dto.rs @@ -1,7 +1,7 @@ //! DTOs of `PermaplanT`. #![allow(clippy::module_name_repetitions)] // There needs to be a difference between DTOs and entities otherwise imports will be messy. -use chrono::NaiveDate; +use chrono::{NaiveDate, NaiveDateTime}; use postgis_diesel::types::{Point, Polygon}; use serde::{Deserialize, Serialize}; use typeshare::typeshare; @@ -79,8 +79,8 @@ pub struct SeedDto { pub price: Option, /// Notes about the seeds. pub notes: Option, - /// The id of the owner of the seed. - pub owner_id: Uuid, + /// The id of the creator of the seed. + pub created_by: Uuid, /// Timestamp indicating when the seed was archived. /// Empty if the seed was not archived. pub archived_at: Option, @@ -215,8 +215,14 @@ pub struct MapDto { pub id: i32, /// The name of the map. pub name: String, - /// The date the map was created. - pub creation_date: NaiveDate, + /// When the map was created. + pub created_at: NaiveDateTime, + /// When a map was last modified, e.g by modifying plantings. + pub modified_at: NaiveDateTime, + /// The id of the creator of the map. + pub created_by: Uuid, + /// By whom the map was last modified. + pub modified_by: Uuid, /// The date the map is supposed to be deleted. pub deletion_date: Option, /// The date the last time the map view was opened by any user. @@ -237,8 +243,6 @@ pub struct MapDto { pub description: Option, /// The location of the map as a latitude/longitude point. pub location: Option, - /// The id of the owner of the map. - pub owner_id: Uuid, /// The geometry of the map. /// /// E.g. `{"rings": [[{"x": 0.0,"y": 0.0},{"x": 1000.0,"y": 0.0},{"x": 1000.0,"y": 1000.0},{"x": 0.0,"y": 1000.0},{"x": 0.0,"y": 0.0}]],"srid": 4326}` @@ -253,8 +257,6 @@ pub struct MapDto { pub struct NewMapDto { /// The name of the map. pub name: String, - /// The date the map was created. - pub creation_date: NaiveDate, /// The date the map is supposed to be deleted. pub deletion_date: Option, /// The date the last time the map view was opened by any user. @@ -317,8 +319,8 @@ pub struct MapSearchParameters { pub name: Option, /// Whether or not the map is active. pub is_inactive: Option, - /// The owner of the map. - pub owner_id: Option, + /// The creator of the map. + pub created_by: Option, /// The selected privacy of the map. pub privacy: Option, } diff --git a/backend/src/model/dto/actions.rs b/backend/src/model/dto/actions.rs index 4f8fa95bd..5531f323f 100644 --- a/backend/src/model/dto/actions.rs +++ b/backend/src/model/dto/actions.rs @@ -23,6 +23,9 @@ use super::{ BaseLayerImageDto, UpdateMapGeometryDto, }; +/// Actions broadcast events to other users viewing the same map, +/// so that they can update the map state appropriately. +/// It keeps all users on the map in sync via [`crate::sse::broadcaster::Broadcaster`] #[typeshare] #[derive(Debug, Serialize, Clone)] #[serde(rename_all = "camelCase")] @@ -34,44 +37,46 @@ pub struct Action { #[typeshare] #[derive(Debug, Serialize, Clone)] -// Use the name of the enum variant as the type field looking like { "type": "CreatePlanting", ... }. -/// An enum representing all the actions that can be broadcasted via [`crate::sse::broadcaster::Broadcaster`]. +// Use the name of the enum variant as the field "type" looking like +/// { "type": "CreatePlanting", "payload": ... }. #[serde(tag = "type", content = "payload")] pub enum ActionType { - /// An action used to broadcast creation of a plant. - CreatePlanting(Vec), - /// An action used to broadcast deletion of a plant. + /// New plantings have been created. + CreatePlanting(Vec), + /// Existing plantings have been deleted. DeletePlanting(Vec), - /// An action used to broadcast movement of a plant. + /// Plantings have been moved (panned). MovePlanting(Vec), - /// An action used to broadcast transformation of a plant. + /// Plantings have been transformation. TransformPlanting(Vec), - /// An action used to update the `add_date` of a plant. + /// The `add_date` field of plantings has been changed. UpdatePlantingAddDate(Vec), - /// An action used to update the `remove_date` of a plant. + /// The `remove_date` field of plantings has been changed. UpdatePlantingRemoveDate(Vec), - /// An action used to broadcast updating a Markdown notes of a plant. + /// Note (markdown) of plantings had been changed. UpdatePlatingNotes(Vec), - /// An action used to broadcast creation of a baseLayerImage. + /// The `additional_name` field of one planting has been changed. + UpdatePlantingAdditionalName(UpdatePlantingAdditionalNamePayload), + + /// A new base layer image has been created. CreateBaseLayerImage(CreateBaseLayerImageActionPayload), - /// An action used to broadcast update of a baseLayerImage. + /// A base layer image has been update. UpdateBaseLayerImage(UpdateBaseLayerImageActionPayload), - /// An action used to broadcast deletion of a baseLayerImage. + /// A base later image has been deleted. DeleteBaseLayerImage(DeleteBaseLayerImageActionPayload), - /// An action used to broadcast an update to the map geometry. + + /// The map geometry has been changed. UpdateMapGeometry(UpdateMapGeometryActionPayload), - /// An action used to update the `additional_name` of a plant. - UpdatePlantingAdditionalName(UpdatePlantingAdditionalNamePayload), - /// An action used to broadcast creation of a new drawing shape. + /// New drawings have been created. CreateDrawing(Vec), - /// An action used to broadcast deletion of an existing drawing shape. + /// Existing drawings have been deleted. DeleteDrawing(Vec), - /// An action used to broadcast the update of an existing drawing shape. + /// Drawings have been updated. UpdateDrawing(Vec), - /// An action used to update the `add_date` of a drawing. + /// The `add_date` field of drawings has been changed. UpdateDrawingAddDate(Vec), - /// An action used to update the `remove_date` of a drawing. + /// The `remove_date` of drawings has changed. UpdateDrawingRemoveDate(Vec), } @@ -85,25 +90,7 @@ impl Action { Self { action_id, user_id, - action: ActionType::CreatePlanting( - dtos.iter() - .map(|dto| CreatePlantActionPayload { - id: dto.id, - layer_id: dto.layer_id, - plant_id: dto.plant_id, - x: dto.x, - y: dto.y, - rotation: dto.rotation, - size_x: dto.size_x, - size_y: dto.size_y, - add_date: dto.add_date, - remove_date: dto.remove_date, - seed_id: dto.seed_id, - additional_name: dto.additional_name.clone(), - is_area: dto.is_area, - }) - .collect(), - ), + action: ActionType::CreatePlanting(Vec::from(dtos)), } } @@ -230,27 +217,6 @@ impl Action { } } -#[typeshare] -#[derive(Debug, Serialize, Clone)] -/// The payload of the [`ActionType::CreatePlanting`]. -/// This struct should always match [`PlantingDto`]. -#[serde(rename_all = "camelCase")] -pub struct CreatePlantActionPayload { - id: Uuid, - layer_id: i32, - plant_id: i32, - x: i32, - y: i32, - rotation: f32, - size_x: i32, - size_y: i32, - add_date: Option, - remove_date: Option, - seed_id: Option, - additional_name: Option, - is_area: bool, -} - #[typeshare] #[derive(Debug, Serialize, Clone)] /// The payload of the [`ActionType::DeletePlanting`]. diff --git a/backend/src/model/dto/core.rs b/backend/src/model/dto/core.rs index 911a7ff92..5ca76b67c 100644 --- a/backend/src/model/dto/core.rs +++ b/backend/src/model/dto/core.rs @@ -7,13 +7,13 @@ use typeshare::typeshare; use utoipa::ToSchema; use uuid::Uuid; -use super::plantings::{DeletePlantingDto, NewPlantingDto, PlantingDto, UpdatePlantingDto}; +use super::plantings::{DeletePlantingDto, PlantingDto, UpdatePlantingDto}; /// A wrapper for a dto that is used to perform an action. #[typeshare] #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[aliases( - ActionDtoWrapperNewPlantings = ActionDtoWrapper>, + ActionDtoWrapperNewPlantings = ActionDtoWrapper>, ActionDtoWrapperUpdatePlantings = ActionDtoWrapper, ActionDtoWrapperDeletePlantings = ActionDtoWrapper>, ActionDtoWrapperDeleteDrawings = ActionDtoWrapper>, diff --git a/backend/src/model/dto/map_impl.rs b/backend/src/model/dto/map_impl.rs index 2f64c4101..47cca2032 100644 --- a/backend/src/model/dto/map_impl.rs +++ b/backend/src/model/dto/map_impl.rs @@ -9,7 +9,10 @@ impl From for MapDto { Self { id: map.id, name: map.name, - creation_date: map.creation_date, + created_at: map.created_at, + modified_at: map.modified_at, + created_by: map.created_by, + modified_by: map.modified_by, deletion_date: map.deletion_date, last_visit: map.last_visit, is_inactive: map.is_inactive, @@ -20,7 +23,6 @@ impl From for MapDto { privacy: map.privacy, description: map.description, location: map.location.map(From::from), - owner_id: map.owner_id, geometry: map.geometry, } } diff --git a/backend/src/model/dto/new_map_impl.rs b/backend/src/model/dto/new_map_impl.rs index 28c14582f..641a31e11 100644 --- a/backend/src/model/dto/new_map_impl.rs +++ b/backend/src/model/dto/new_map_impl.rs @@ -7,10 +7,9 @@ use crate::model::entity::NewMap; use super::NewMapDto; impl From<(NewMapDto, Uuid)> for NewMap { - fn from((new_map, owner_id): (NewMapDto, Uuid)) -> Self { + fn from((new_map, user_id): (NewMapDto, Uuid)) -> Self { Self { name: new_map.name, - creation_date: new_map.creation_date, deletion_date: new_map.deletion_date, last_visit: new_map.last_visit, is_inactive: new_map.is_inactive, @@ -21,7 +20,8 @@ impl From<(NewMapDto, Uuid)> for NewMap { privacy: new_map.privacy, description: new_map.description, location: new_map.location.map(From::from), - owner_id, + created_by: user_id, + modified_by: user_id, geometry: new_map.geometry, } } diff --git a/backend/src/model/dto/new_seed_impl.rs b/backend/src/model/dto/new_seed_impl.rs index d3709362c..976893740 100644 --- a/backend/src/model/dto/new_seed_impl.rs +++ b/backend/src/model/dto/new_seed_impl.rs @@ -7,7 +7,7 @@ use crate::model::entity::NewSeed; use super::NewSeedDto; impl From<(NewSeedDto, Uuid)> for NewSeed { - fn from((new_seed, owner_id): (NewSeedDto, Uuid)) -> Self { + fn from((new_seed, user_id): (NewSeedDto, Uuid)) -> Self { Self { name: new_seed.name, plant_id: new_seed.plant_id, @@ -21,7 +21,7 @@ impl From<(NewSeedDto, Uuid)> for NewSeed { quality: new_seed.quality, price: new_seed.price, notes: new_seed.notes, - owner_id, + created_by: user_id, } } } diff --git a/backend/src/model/dto/plantings.rs b/backend/src/model/dto/plantings.rs index cb3b7c785..57700ce3f 100644 --- a/backend/src/model/dto/plantings.rs +++ b/backend/src/model/dto/plantings.rs @@ -1,13 +1,13 @@ //! All DTOs associated with [`PlantingDto`]. -use chrono::NaiveDate; +use chrono::{NaiveDate, NaiveDateTime}; use serde::{Deserialize, Serialize}; use typeshare::typeshare; use utoipa::{IntoParams, ToSchema}; use uuid::Uuid; -/// Represents plant planted on a map. -/// E.g. a user drags a plant from the search results and drops it on the map. +/// Represents a plant on a map. +/// E.g. a user selects a plant from the search results and plants it on the map. #[typeshare] #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] @@ -18,6 +18,14 @@ pub struct PlantingDto { pub layer_id: i32, /// The plant that is planted. pub plant_id: i32, + /// The datetime the planting was created. + pub created_at: NaiveDateTime, + /// The uuid of the user that created the planting. + pub created_by: Uuid, + /// The datetime the planting was last modified. + pub modified_at: NaiveDateTime, + /// The uuid of the user that last modified the planting. + pub modified_by: Uuid, /// The x coordinate of the position on the map. pub x: i32, /// The y coordinate of the position on the map. @@ -43,7 +51,7 @@ pub struct PlantingDto { /// Is the planting an area of plants. pub is_area: bool, /// Notes about the planting in Markdown. - pub planting_notes: Option, + pub planting_notes: String, } /// Used to create a new planting. @@ -52,7 +60,7 @@ pub struct PlantingDto { #[serde(rename_all = "camelCase")] pub struct NewPlantingDto { /// The id of the planting. - pub id: Option, + pub id: Uuid, /// The plant layer the plantings is on. pub layer_id: i32, /// The plant that is planted. diff --git a/backend/src/model/dto/plantings_impl.rs b/backend/src/model/dto/plantings_impl.rs index 10880ff25..cfa5d4898 100644 --- a/backend/src/model/dto/plantings_impl.rs +++ b/backend/src/model/dto/plantings_impl.rs @@ -3,7 +3,7 @@ //use chrono::Utc; use uuid::Uuid; -use crate::model::entity::plantings::{Planting, UpdatePlanting}; +use crate::model::entity::plantings::{NewPlanting, Planting, UpdatePlanting}; use super::plantings::{ MovePlantingDto, NewPlantingDto, PlantingDto, TransformPlantingDto, UpdateAddDatePlantingDto, @@ -23,6 +23,10 @@ impl From for PlantingDto { fn from(planting: Planting) -> Self { Self { id: planting.id, + created_at: planting.created_at, + created_by: planting.created_by, + modified_at: planting.modified_at, + modified_by: planting.modified_by, plant_id: planting.plant_id, layer_id: planting.layer_id, x: planting.x, @@ -40,24 +44,22 @@ impl From for PlantingDto { } } -impl From for Planting { - fn from(dto: NewPlantingDto) -> Self { +impl From<(NewPlantingDto, Uuid)> for NewPlanting { + fn from((dto, user_id): (NewPlantingDto, Uuid)) -> Self { Self { - id: dto.id.unwrap_or_else(Uuid::new_v4), + id: dto.id, plant_id: dto.plant_id, layer_id: dto.layer_id, + created_by: user_id, + modified_by: user_id, x: dto.x, y: dto.y, size_x: dto.size_x, size_y: dto.size_y, rotation: dto.rotation, - add_date: dto.add_date, - remove_date: None, seed_id: dto.seed_id, is_area: dto.is_area, - //create_date: Utc::now().date_naive(), - //delete_date: None, - notes: None, + add_date: dto.add_date, } } } diff --git a/backend/src/model/dto/seed_impl.rs b/backend/src/model/dto/seed_impl.rs index 7f52bd1b7..835cef253 100644 --- a/backend/src/model/dto/seed_impl.rs +++ b/backend/src/model/dto/seed_impl.rs @@ -20,7 +20,7 @@ impl From for SeedDto { quality: seed.quality, price: seed.price, notes: seed.notes, - owner_id: seed.owner_id, + created_by: seed.created_by, archived_at: seed.archived_at.map(|date| format!("{date}")), } } @@ -42,7 +42,7 @@ impl From<(f32, Seed)> for SeedDto { quality: seed.quality, price: seed.price, notes: seed.notes, - owner_id: seed.owner_id, + created_by: seed.created_by, archived_at: seed.archived_at.map(|date| format!("{date}")), } } diff --git a/backend/src/model/entity.rs b/backend/src/model/entity.rs index 6824cb686..6bafd9027 100644 --- a/backend/src/model/entity.rs +++ b/backend/src/model/entity.rs @@ -716,8 +716,8 @@ pub struct Seed { pub notes: Option, /// The id of the plant this seed belongs to. pub plant_id: Option, - /// The id of the owner of the seed. - pub owner_id: Uuid, + /// The id of the creator of the seed. + pub created_by: Uuid, /// Timestamp indicating when the seed was archived. /// Empty if the seed was not archived. pub archived_at: Option, @@ -740,7 +740,7 @@ pub struct NewSeed { pub price: Option, pub generation: Option, pub notes: Option, - pub owner_id: Uuid, + pub created_by: Uuid, } /// The `Map` entity. @@ -751,8 +751,6 @@ pub struct Map { pub id: i32, /// The name of the map. pub name: String, - /// The date the map was created. - pub creation_date: NaiveDate, /// The date the map is supposed to be deleted. pub deletion_date: Option, /// The date the last time the map view was opened by any user. @@ -773,10 +771,16 @@ pub struct Map { pub description: Option, /// The location of the map as a latitude/longitude point. pub location: Option, - /// The id of the owner of the map. - pub owner_id: Uuid, + /// The id of the creator of the map. + pub created_by: Uuid, /// The geometry of the map. pub geometry: Polygon, + /// When the map was created. + pub created_at: NaiveDateTime, + /// When a map was last modified, e.g., by modifying plantings. + pub modified_at: NaiveDateTime, + /// By whom the map was last modified. + pub modified_by: Uuid, } /// The `NewMap` entity. @@ -785,9 +789,7 @@ pub struct Map { pub struct NewMap { /// The name of the map. pub name: String, - /// The date the map was created. - pub creation_date: NaiveDate, - /// The date the map is supposed to be deleted. + /// For a new map the same as created_by. pub deletion_date: Option, /// The date the last time the map view was opened by any user. pub last_visit: Option, @@ -807,10 +809,12 @@ pub struct NewMap { pub description: Option, /// The location of the map as a latitude/longitude point. pub location: Option, - /// The id of the owner of the map. - pub owner_id: Uuid, + /// The id of the creator of the map. + pub created_by: Uuid, /// The geometry of the map. pub geometry: Polygon, + /// The user who last modified the planting. + pub modified_by: Uuid, } /// The `UpdateMap` entity. diff --git a/backend/src/model/entity/map_impl.rs b/backend/src/model/entity/map_impl.rs index 8ffb0f15e..16d3b29e1 100644 --- a/backend/src/model/entity/map_impl.rs +++ b/backend/src/model/entity/map_impl.rs @@ -1,5 +1,6 @@ //! Contains the implementation of [`Map`]. +use chrono::NaiveDateTime; use diesel::dsl::sql; use diesel::pg::Pg; use diesel::sql_types::Float; @@ -20,7 +21,7 @@ use crate::model::entity::{UpdateMap, UpdateMapGeometry}; use crate::schema::maps::name; use crate::{ model::dto::{MapDto, NewMapDto}, - schema::maps::{self, all_columns, is_inactive, owner_id, privacy}, + schema::maps::{self, all_columns, created_by, is_inactive, privacy}, }; use super::{Map, NewMap}; @@ -28,7 +29,7 @@ use super::{Map, NewMap}; impl Map { /// Get the top maps matching the search query. /// - /// Can be filtered by `is_inactive` and `owner_id` if provided in `search_parameters`. + /// Can be filtered by `is_inactive` and `created_by` if provided in `search_parameters`. /// This will be done with equals and is additional functionality for maps (when compared to plant search). /// /// Uses `pg_trgm` to find matches in `name`. @@ -62,8 +63,8 @@ impl Map { if let Some(privacy_search) = search_parameters.privacy { query = query.filter(privacy.eq(privacy_search)); } - if let Some(owner_id_search) = search_parameters.owner_id { - query = query.filter(owner_id.eq(owner_id_search)); + if let Some(created_by_search) = search_parameters.created_by { + query = query.filter(created_by.eq(created_by_search)); } let query = query @@ -131,4 +132,21 @@ impl Map { debug!("{}", debug_query::(&query)); query.get_result::(conn).await.map(Into::into) } + + /// Update modified metadate (`modified_at`, `modified_by`) of the map. + /// + /// # Errors + /// * Unknown, diesel doesn't say why it might error. + pub async fn update_modified_metadata( + map_id: i32, + user_id: Uuid, + time: NaiveDateTime, + conn: &mut AsyncPgConnection, + ) -> QueryResult<()> { + diesel::update(maps::table.find(map_id)) + .set((maps::modified_at.eq(time), maps::modified_by.eq(user_id))) + .execute(conn) + .await?; + Ok(()) + } } diff --git a/backend/src/model/entity/plantings.rs b/backend/src/model/entity/plantings.rs index 0c8decb6e..8ca279e8a 100644 --- a/backend/src/model/entity/plantings.rs +++ b/backend/src/model/entity/plantings.rs @@ -1,13 +1,13 @@ //! All entities associated with [`Planting`]. -use chrono::NaiveDate; +use chrono::{NaiveDate, NaiveDateTime}; use diesel::{AsChangeset, Identifiable, Insertable, Queryable}; use uuid::Uuid; use crate::schema::plantings; /// The `Planting` entity. -#[derive(Debug, Clone, Identifiable, Queryable, Insertable)] +#[derive(Debug, Clone, Identifiable, Queryable)] #[diesel(table_name = plantings)] pub struct Planting { /// The id of the planting. @@ -45,7 +45,50 @@ pub struct Planting { //pub delete_date: Option, */ /// Notes about the planting in Markdown. - pub notes: Option, + pub notes: String, + + /// The datetime the planting was created. + pub created_at: NaiveDateTime, + /// The datetime the planting was last modified. + pub modified_at: NaiveDateTime, + /// The uuid of the user that created the planting. + pub created_by: Uuid, + /// The uuid of the user that last modified the planting. + pub modified_by: Uuid, +} + +/// The `NewPlanting` entity. +#[derive(Insertable)] +#[diesel(table_name = plantings)] +pub struct NewPlanting { + /// The id of the planting (set by the frontend) + pub id: Uuid, + /// The plant layer the plantings is on. + pub layer_id: i32, + /// The plant that is planted. + pub plant_id: i32, + + /// The x coordinate of the position on the map. + pub x: i32, + /// The y coordinate of the position on the map. + pub y: i32, + /// The size of the planting on the map in x direction. + pub size_x: i32, + /// The size of the planting on the map in y direction. + pub size_y: i32, + /// The rotation in degrees (0-360) of the plant on the map. + pub rotation: f32, + /// The date the planting was added to the map. + /// If None, the planting always existed. + pub add_date: Option, + /// Plantings may be linked with a seed. + pub seed_id: Option, + /// Is the planting an area of plants. + pub is_area: bool, + /// The uuid of the user that created the planting. + pub created_by: Uuid, + /// The user who last modified the planting. + pub modified_by: Uuid, } /// The `UpdatePlanting` entity. diff --git a/backend/src/model/entity/plantings_impl.rs b/backend/src/model/entity/plantings_impl.rs index 1bb89b3c0..664aafa5c 100644 --- a/backend/src/model/entity/plantings_impl.rs +++ b/backend/src/model/entity/plantings_impl.rs @@ -1,6 +1,6 @@ //! Contains the implementation of [`Planting`]. -use chrono::NaiveDate; +use chrono::{NaiveDate, Utc}; use diesel::pg::Pg; use diesel::{ debug_query, BoolExpressionMethods, ExpressionMethods, NullableExpressionMethods, QueryDsl, @@ -11,15 +11,15 @@ use futures_util::Future; use log::debug; use uuid::Uuid; +use super::plantings::UpdatePlanting; +use super::Map; use crate::model::dto::plantings::{ DeletePlantingDto, NewPlantingDto, PlantingDto, UpdatePlantingDto, }; -use crate::model::entity::plantings::Planting; +use crate::model::entity::plantings::{NewPlanting, Planting}; use crate::schema::plantings::{self, layer_id, plant_id}; use crate::schema::seeds; -use super::plantings::UpdatePlanting; - /// Arguments for the database layer find plantings function. pub struct FindPlantingsParameters { /// The id of the plant to find plantings for. @@ -101,14 +101,19 @@ impl Planting { /// * Unknown, diesel doesn't say why it might error. pub async fn create( dto_vec: Vec, + map_id: i32, + user_id: Uuid, conn: &mut AsyncPgConnection, ) -> QueryResult> { - let planting_creations: Vec = dto_vec.into_iter().map(Into::into).collect(); + let planting_creations: Vec = dto_vec + .into_iter() + .map(|dto| NewPlanting::from((dto, user_id))) + .collect(); let query = diesel::insert_into(plantings::table).values(&planting_creations); debug!("{}", debug_query::(&query)); - let query_result: Vec = query.get_results(conn).await?; + let query_result = query.get_results::(conn).await?; let seed_ids = query_result .iter() @@ -140,6 +145,10 @@ impl Planting { }) .collect::>(); + if let Some(first) = result_vec.get(0) { + Map::update_modified_metadata(map_id, user_id, first.created_at, conn).await?; + } + Ok(result_vec) } @@ -149,6 +158,8 @@ impl Planting { /// * Unknown, diesel doesn't say why it might error. pub async fn update( dto: UpdatePlantingDto, + map_id: i32, + user_id: Uuid, conn: &mut AsyncPgConnection, ) -> QueryResult> { let planting_updates = Vec::from(dto); @@ -156,10 +167,20 @@ impl Planting { let result = conn .transaction(|transaction| { Box::pin(async { - let futures = Self::do_update(planting_updates, transaction); + let futures = Self::do_update(planting_updates, user_id, transaction); let results = futures_util::future::try_join_all(futures).await?; + if let Some(first) = results.get(0) { + Map::update_modified_metadata( + map_id, + user_id, + first.modified_at, + transaction, + ) + .await?; + } + Ok(results) as QueryResult> }) }) @@ -174,13 +195,19 @@ impl Planting { /// this helper function is needed, with explicit type annotations. fn do_update( updates: Vec, + user_id: Uuid, conn: &mut AsyncPgConnection, ) -> Vec>> { let mut futures = Vec::with_capacity(updates.len()); + let now = Utc::now().naive_utc(); for update in updates { let updated_planting = diesel::update(plantings::table.find(update.id)) - .set(update) + .set(( + update, + plantings::modified_at.eq(now), + plantings::modified_by.eq(user_id), + )) .get_result::(conn); futures.push(updated_planting); @@ -195,12 +222,23 @@ impl Planting { /// * Unknown, diesel doesn't say why it might error. pub async fn delete_by_ids( dto: Vec, + map_id: i32, + user_id: Uuid, conn: &mut AsyncPgConnection, ) -> QueryResult { let ids: Vec = dto.iter().map(|&DeletePlantingDto { id }| id).collect(); - let query = diesel::delete(plantings::table.filter(plantings::id.eq_any(ids))); - debug!("{}", debug_query::(&query)); - query.execute(conn).await + conn.transaction(|transaction| { + Box::pin(async { + let query = diesel::delete(plantings::table.filter(plantings::id.eq_any(ids))); + debug!("{}", debug_query::(&query)); + let deleted_plantings = query.execute(transaction).await?; + + Map::update_modified_metadata(map_id, user_id, Utc::now().naive_utc(), transaction) + .await?; + Ok(deleted_plantings) + }) + }) + .await } } diff --git a/backend/src/model/entity/seed_impl.rs b/backend/src/model/entity/seed_impl.rs index ca9b5b6b0..84ff6fb9a 100644 --- a/backend/src/model/entity/seed_impl.rs +++ b/backend/src/model/entity/seed_impl.rs @@ -15,7 +15,7 @@ use crate::{ model::dto::{NewSeedDto, SeedDto}, schema::{ plants::{self, common_name_de, common_name_en, unique_name}, - seeds::{self, all_columns, archived_at, harvest_year, name, owner_id, use_by}, + seeds::{self, all_columns, created_by, harvest_year, name, use_by}, }, }; @@ -97,13 +97,13 @@ impl Seed { // Don't filter the query if IncludeArchivedSeeds::Both is selected. if include_archived == IncludeArchivedSeeds::Archived { - query = query.filter(archived_at.is_not_null()); + query = query.filter(seeds::archived_at.is_not_null()); } else if include_archived == IncludeArchivedSeeds::NotArchived { - query = query.filter(archived_at.is_null()); + query = query.filter(seeds::archived_at.is_null()); } // Only return seeds that belong to the user. - query = query.filter(owner_id.eq(user_id)); + query = query.filter(created_by.eq(user_id)); let query = query .paginate(page_parameters.page) @@ -127,7 +127,7 @@ impl Seed { let mut query = seeds::table.select(all_columns).into_boxed(); // Only return seeds that belong to the user. - query = query.filter(owner_id.eq(user_id).and(seeds::id.eq(id))); + query = query.filter(created_by.eq(user_id).and(seeds::id.eq(id))); debug!("{}", debug_query::(&query)); query.first::(conn).await.map(Into::into) @@ -189,7 +189,7 @@ impl Seed { conn: &mut AsyncPgConnection, ) -> QueryResult { // Only delete seeds that belong to the user. - let source = seeds::table.filter(owner_id.eq(user_id).and(seeds::id.eq(id))); + let source = seeds::table.filter(created_by.eq(user_id).and(seeds::id.eq(id))); let query = diesel::delete(source); debug!("{}", debug_query::(&query)); @@ -202,14 +202,14 @@ impl Seed { /// If the connection to the database could not be established. pub async fn archive( id: i32, - archived_at_: Option, + archived_at: Option, user_id: Uuid, conn: &mut AsyncPgConnection, ) -> QueryResult { - let source = seeds::table.filter(owner_id.eq(user_id).and(seeds::id.eq(id))); + let source = seeds::table.filter(created_by.eq(user_id).and(seeds::id.eq(id))); let query_result = diesel::update(source) - .set((seeds::archived_at.eq(archived_at_),)) + .set(seeds::archived_at.eq(archived_at)) .get_result::(conn) .await; query_result.map(Into::into) diff --git a/backend/src/schema.patch b/backend/src/schema.patch index 88f0f4345..f9cf95e38 100644 --- a/backend/src/schema.patch +++ b/backend/src/schema.patch @@ -1,15 +1,12 @@ -diff --git a/backend/src/schema.rs b/backend/src/schema.rs 2023-07-20 -index 54f26f46..68427977 100644 ---- a/backend/src/schema.rs -+++ b/backend/src/schema.rs -@@ -10,20 +10,12 @@ pub mod sql_types { - pub struct ExternalSource; +--- schema_old.rs 2024-01-29 15:27:13.298391417 +0000 ++++ schema.rs 2024-01-29 15:28:30.420393299 +0000 +@@ -15,20 +15,12 @@ #[derive(diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "fertility"))] pub struct Fertility; -- #[derive(diesel::sql_types::SqlType)] + #[derive(diesel::sql_types::SqlType)] - #[diesel(postgres_type(name = "geography"))] - pub struct Geography; - @@ -17,13 +14,14 @@ index 54f26f46..68427977 100644 - #[diesel(postgres_type(name = "geometry"))] - pub struct Geometry; - - #[derive(diesel::sql_types::SqlType)] +- #[derive(diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "growth_rate"))] pub struct GrowthRate; #[derive(diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "herbaceous_or_woody"))] -@@ -100,16 +92,15 @@ diesel::table! { + pub struct HerbaceousOrWoody; +@@ -165,16 +157,15 @@ is_alternative -> Bool, } } @@ -39,5 +37,5 @@ index 54f26f46..68427977 100644 maps (id) { id -> Int4, name -> Text, - creation_date -> Date, deletion_date -> Nullable, + last_visit -> Nullable, diff --git a/backend/src/service/map.rs b/backend/src/service/map.rs index cdd0c919b..424cb9833 100644 --- a/backend/src/service/map.rs +++ b/backend/src/service/map.rs @@ -109,7 +109,7 @@ pub async fn update( ) -> Result { let mut conn = app_data.pool.get().await?; let map = Map::find_by_id(id, &mut conn).await?; - if map.owner_id != user_id { + if map.created_by != user_id { return Err(ServiceError { status_code: StatusCode::FORBIDDEN, reason: "No permission to update data".to_owned(), @@ -133,7 +133,7 @@ pub async fn update_geomtery( ) -> Result { let mut conn = app_data.pool.get().await?; let map = Map::find_by_id(id, &mut conn).await?; - if map.owner_id != user_id { + if map.created_by != user_id { return Err(ServiceError { status_code: StatusCode::FORBIDDEN, reason: "No permission to update data".to_owned(), diff --git a/backend/src/service/plantings.rs b/backend/src/service/plantings.rs index 86ca42a68..4c94b2aa8 100644 --- a/backend/src/service/plantings.rs +++ b/backend/src/service/plantings.rs @@ -3,6 +3,7 @@ use actix_http::StatusCode; use actix_web::web::Data; use chrono::Days; +use uuid::Uuid; use crate::config::data::AppDataInner; use crate::error::ServiceError; @@ -81,10 +82,12 @@ pub async fn find_by_seed_id( /// If the connection to the database could not be established. pub async fn create( dtos: Vec, + map_id: i32, + user_id: Uuid, app_data: &Data, ) -> Result, ServiceError> { let mut conn = app_data.pool.get().await?; - let result = Planting::create(dtos, &mut conn).await?; + let result = Planting::create(dtos, map_id, user_id, &mut conn).await?; Ok(result) } @@ -94,10 +97,12 @@ pub async fn create( /// If the connection to the database could not be established. pub async fn update( dto: UpdatePlantingDto, + map_id: i32, + user_id: Uuid, app_data: &Data, ) -> Result, ServiceError> { let mut conn = app_data.pool.get().await?; - let result = Planting::update(dto, &mut conn).await?; + let result = Planting::update(dto, map_id, user_id, &mut conn).await?; Ok(result) } @@ -107,9 +112,11 @@ pub async fn update( /// If the connection to the database could not be established. pub async fn delete_by_ids( dtos: Vec, + map_id: i32, + user_id: Uuid, app_data: &Data, ) -> Result<(), ServiceError> { let mut conn = app_data.pool.get().await?; - let _ = Planting::delete_by_ids(dtos, &mut conn).await?; + let _ = Planting::delete_by_ids(dtos, map_id, user_id, &mut conn).await?; Ok(()) } diff --git a/backend/src/test/base_layer_image.rs b/backend/src/test/base_layer_image.rs index 162b4608b..753e87c6c 100644 --- a/backend/src/test/base_layer_image.rs +++ b/backend/src/test/base_layer_image.rs @@ -6,7 +6,7 @@ use crate::{ error::ServiceError, model::{ dto::{BaseLayerImageDto, UpdateBaseLayerImageDto}, - r#enum::{layer_type::LayerType, privacy_option::PrivacyOption}, + r#enum::layer_type::LayerType, }, test::util::{init_test_app, init_test_database}, }; @@ -17,30 +17,24 @@ use actix_web::{ }, test, }; -use chrono::Utc; use diesel::ExpressionMethods; use diesel_async::{scoped_futures::ScopedFutureExt, AsyncPgConnection, RunQueryDsl}; use postgis_diesel::types::{Point, Polygon}; use uuid::Uuid; +use super::util::data::TestInsertableMap; + async fn initial_db_values( conn: &mut AsyncPgConnection, polygon: Polygon, ) -> Result<(), ServiceError> { diesel::insert_into(crate::schema::maps::table) - .values(vec![( - &crate::schema::maps::id.eq(-1), - &crate::schema::maps::name.eq("MyMap"), - &crate::schema::maps::creation_date.eq(Utc::now().date_naive()), - &crate::schema::maps::is_inactive.eq(false), - &crate::schema::maps::zoom_factor.eq(0), - &crate::schema::maps::honors.eq(0), - &crate::schema::maps::visits.eq(0), - &crate::schema::maps::harvested.eq(0), - &crate::schema::maps::owner_id.eq(Uuid::default()), - &crate::schema::maps::privacy.eq(PrivacyOption::Private), - &crate::schema::maps::geometry.eq(polygon), - )]) + .values(TestInsertableMap { + id: -1, + name: "MyMap".to_owned(), + geometry: polygon, + ..Default::default() + }) .execute(conn) .await?; diesel::insert_into(crate::schema::layers::table) diff --git a/backend/src/test/blossoms.rs b/backend/src/test/blossoms.rs index 0ef27b4fc..2384989c2 100644 --- a/backend/src/test/blossoms.rs +++ b/backend/src/test/blossoms.rs @@ -29,7 +29,7 @@ async fn test_can_gain_blossom() { let (token, app) = init_test_app(pool.clone()).await; let gained_blossom = GainedBlossomsDto { - blossom: "Brave Tester".to_string(), + blossom: "Brave Tester".to_owned(), times_gained: 1, gained_date: NaiveDate::from_ymd_opt(2023, 7, 18).expect("Could not parse date!"), }; diff --git a/backend/src/test/layers.rs b/backend/src/test/layers.rs index 3e596271b..51052fe61 100644 --- a/backend/src/test/layers.rs +++ b/backend/src/test/layers.rs @@ -4,7 +4,7 @@ use crate::{ error::ServiceError, model::{ dto::{LayerDto, NewLayerDto}, - r#enum::{layer_type::LayerType, privacy_option::PrivacyOption}, + r#enum::layer_type::LayerType, }, test::util::{init_test_app, init_test_database}, }; @@ -15,46 +15,31 @@ use actix_web::{ }, test, }; -use chrono::Utc; -use diesel::ExpressionMethods; use diesel_async::{scoped_futures::ScopedFutureExt, AsyncPgConnection, RunQueryDsl}; -use uuid::Uuid; -use super::util::dummy_map_polygons::tall_rectangle; +use super::util::data::{TestInsertableLayer, TestInsertableMap}; async fn initial_db_values(conn: &mut AsyncPgConnection) -> Result<(), ServiceError> { diesel::insert_into(crate::schema::maps::table) - .values(vec![( - &crate::schema::maps::id.eq(-1), - &crate::schema::maps::name.eq("MyMap"), - &crate::schema::maps::creation_date.eq(Utc::now().date_naive()), - &crate::schema::maps::is_inactive.eq(false), - &crate::schema::maps::zoom_factor.eq(0), - &crate::schema::maps::honors.eq(0), - &crate::schema::maps::visits.eq(0), - &crate::schema::maps::harvested.eq(0), - &crate::schema::maps::owner_id.eq(Uuid::default()), - &crate::schema::maps::privacy.eq(PrivacyOption::Private), - &crate::schema::maps::geometry.eq(tall_rectangle()), - )]) + .values(TestInsertableMap::default()) .execute(conn) .await?; diesel::insert_into(crate::schema::layers::table) .values(vec![ - ( - &crate::schema::layers::id.eq(-1), - &crate::schema::layers::map_id.eq(-1), - &crate::schema::layers::type_.eq(LayerType::Plants), - &crate::schema::layers::name.eq("My Map"), - &crate::schema::layers::is_alternative.eq(false), - ), - ( - &crate::schema::layers::id.eq(-2), - &crate::schema::layers::map_id.eq(-1), - &crate::schema::layers::type_.eq(LayerType::Plants), - &crate::schema::layers::name.eq("MyMap2"), - &crate::schema::layers::is_alternative.eq(true), - ), + TestInsertableLayer { + id: -1, + map_id: -1, + type_: LayerType::Plants, + name: "My Map".to_owned(), + is_alternative: false, + }, + TestInsertableLayer { + id: -2, + map_id: -1, + type_: LayerType::Plants, + name: "My Map".to_owned(), + is_alternative: true, + }, ]) .execute(conn) .await?; @@ -117,7 +102,7 @@ async fn test_create_layer_succeeds() { .set_json(NewLayerDto { map_id: -1, type_: LayerType::Base, - name: "MyBaseLayer".to_string(), + name: "MyBaseLayer".to_owned(), is_alternative: false, }) .send_request(&app) @@ -137,7 +122,7 @@ async fn test_create_layer_with_invalid_map_id_fails() { .set_json(NewLayerDto { map_id: -2, type_: LayerType::Base, - name: "MyBaseLayer2".to_string(), + name: "MyBaseLayer2".to_owned(), is_alternative: false, }) .send_request(&app) diff --git a/backend/src/test/map.rs b/backend/src/test/map.rs index 2b5fc0cc9..be9108c58 100644 --- a/backend/src/test/map.rs +++ b/backend/src/test/map.rs @@ -1,6 +1,16 @@ //! Tests for [`crate::controller::map`]. +use std::time::Duration; + +use crate::model::dto::core::{ + ActionDtoWrapper, ActionDtoWrapperDeletePlantings, ActionDtoWrapperNewPlantings, + ActionDtoWrapperUpdatePlantings, +}; +use crate::model::dto::plantings::{ + DeletePlantingDto, PlantingDto, TransformPlantingDto, UpdatePlantingDto, +}; use crate::model::dto::UpdateMapGeometryDto; +use crate::test::util::data::{self, TestInsertableMap}; use crate::test::util::dummy_map_polygons::small_rectangle; use crate::{ model::{ @@ -9,13 +19,18 @@ use crate::{ }, test::util::{dummy_map_polygons::tall_rectangle, init_test_app, init_test_database}, }; +use actix_http::Request; +use actix_service::Service; +use actix_web::body::MessageBody; +use actix_web::dev::ServiceResponse; +use actix_web::Error; use actix_web::{ http::{header, StatusCode}, test, }; -use chrono::NaiveDate; -use diesel::ExpressionMethods; +use chrono::Utc; use diesel_async::{scoped_futures::ScopedFutureExt, RunQueryDsl}; +use tokio::time::sleep; use uuid::Uuid; #[actix_rt::test] @@ -23,33 +38,18 @@ async fn test_can_search_maps() { let pool = init_test_database(|conn| { async { diesel::insert_into(crate::schema::maps::table) - .values(vec![( - &crate::schema::maps::id.eq(-1), - &crate::schema::maps::name.eq("Test Map: can find map"), - &crate::schema::maps::creation_date - .eq(NaiveDate::from_ymd_opt(2023, 5, 8).expect("Could not parse date!")), - &crate::schema::maps::is_inactive.eq(false), - &crate::schema::maps::zoom_factor.eq(100), - &crate::schema::maps::honors.eq(0), - &crate::schema::maps::visits.eq(0), - &crate::schema::maps::harvested.eq(0), - &crate::schema::maps::privacy.eq(PrivacyOption::Public), - &crate::schema::maps::owner_id.eq(Uuid::new_v4()), - &crate::schema::maps::geometry.eq(tall_rectangle()), - ),( - &crate::schema::maps::id.eq(-2), - &crate::schema::maps::name.eq("Other"), - &crate::schema::maps::creation_date - .eq(NaiveDate::from_ymd_opt(2023, 5, 8).expect("Could not parse date!")), - &crate::schema::maps::is_inactive.eq(false), - &crate::schema::maps::zoom_factor.eq(100), - &crate::schema::maps::honors.eq(0), - &crate::schema::maps::visits.eq(0), - &crate::schema::maps::harvested.eq(0), - &crate::schema::maps::privacy.eq(PrivacyOption::Public), - &crate::schema::maps::owner_id.eq(Uuid::new_v4()), - &crate::schema::maps::geometry.eq(tall_rectangle()), - )]) + .values(vec![ + TestInsertableMap { + name: "Test Map: can find map".to_owned(), + ..Default::default() + }, + TestInsertableMap { + id: -2, + name: "Other".to_owned(), + created_by: Uuid::new_v4(), + ..Default::default() + }, + ]) .execute(conn) .await?; Ok(()) @@ -60,32 +60,30 @@ async fn test_can_search_maps() { let (token, app) = init_test_app(pool.clone()).await; let resp = test::TestRequest::get() - .uri("/api/maps") + .uri("/api/maps?per_page=100000") .insert_header((header::AUTHORIZATION, token.clone())) .send_request(&app) .await; assert_eq!(resp.status(), StatusCode::OK); - let result = test::read_body(resp).await; - let result_string = std::str::from_utf8(&result).unwrap(); - let page: Page = serde_json::from_str(result_string).unwrap(); - - assert!(page.results.len() == 2); + let page: Page = test::read_body_json(resp).await; + let result_ids: Vec = page.results.iter().map(|item| item.id).collect(); + assert!(result_ids.contains(&-1)); + assert!(result_ids.contains(&-2)); let resp = test::TestRequest::get() - .uri("/api/maps?name=Other") + .uri("/api/maps?name=Other&per_page=100000") .insert_header((header::AUTHORIZATION, token)) .send_request(&app) .await; assert_eq!(resp.status(), StatusCode::OK); - let result = test::read_body(resp).await; - let result_string = std::str::from_utf8(&result).unwrap(); - let page: Page = serde_json::from_str(result_string).unwrap(); - - assert!(page.results.len() == 1); + let page: Page = test::read_body_json(resp).await; + let result_ids: Vec = page.results.iter().map(|item| item.id).collect(); + assert!(!result_ids.contains(&-1)); + assert!(result_ids.contains(&-2)); } #[actix_rt::test] @@ -93,20 +91,11 @@ async fn test_can_find_map_by_id() { let pool = init_test_database(|conn| { async { diesel::insert_into(crate::schema::maps::table) - .values(( - &crate::schema::maps::id.eq(-1), - &crate::schema::maps::name.eq("Test Map: can search map"), - &crate::schema::maps::creation_date - .eq(NaiveDate::from_ymd_opt(2023, 5, 8).expect("Could not parse date!")), - &crate::schema::maps::is_inactive.eq(false), - &crate::schema::maps::zoom_factor.eq(100), - &crate::schema::maps::honors.eq(0), - &crate::schema::maps::visits.eq(0), - &crate::schema::maps::harvested.eq(0), - &crate::schema::maps::privacy.eq(PrivacyOption::Public), - &crate::schema::maps::owner_id.eq(Uuid::new_v4()), - &crate::schema::maps::geometry.eq(tall_rectangle()), - )) + .values(TestInsertableMap { + id: -1, + name: "Test Map: can find map".to_owned(), + ..Default::default() + }) .execute(conn) .await?; Ok(()) @@ -131,8 +120,7 @@ async fn test_can_create_map() { let (token, app) = init_test_app(pool.clone()).await; let new_map = NewMapDto { - name: "Test Map: can create map".to_string(), - creation_date: NaiveDate::from_ymd_opt(2023, 5, 8).expect("Could not parse date!"), + name: "Test Map: can create map".to_owned(), deletion_date: None, last_visit: None, is_inactive: false, @@ -170,20 +158,11 @@ async fn test_update_fails_for_not_owner() { let pool = init_test_database(|conn| { async { diesel::insert_into(crate::schema::maps::table) - .values(( - &crate::schema::maps::id.eq(-1), - &crate::schema::maps::name.eq("Test Map: no update permission"), - &crate::schema::maps::creation_date - .eq(NaiveDate::from_ymd_opt(2023, 5, 8).expect("Could not parse date!")), - &crate::schema::maps::is_inactive.eq(false), - &crate::schema::maps::zoom_factor.eq(100), - &crate::schema::maps::honors.eq(0), - &crate::schema::maps::visits.eq(0), - &crate::schema::maps::harvested.eq(0), - &crate::schema::maps::privacy.eq(PrivacyOption::Public), - &crate::schema::maps::owner_id.eq(Uuid::new_v4()), - &crate::schema::maps::geometry.eq(tall_rectangle()), - )) + .values(TestInsertableMap { + id: -1, + name: "Test Map: no update permission".to_owned(), + ..Default::default() + }) .execute(conn) .await?; Ok(()) @@ -194,7 +173,7 @@ async fn test_update_fails_for_not_owner() { let (token, app) = init_test_app(pool.clone()).await; let map_update = UpdateMapDto { - name: Some("This will fail".to_string()), + name: Some("This will fail".to_owned()), privacy: None, description: None, location: None, @@ -215,8 +194,7 @@ async fn test_can_update_map() { let (token, app) = init_test_app(pool.clone()).await; let new_map = NewMapDto { - name: "Test Map: can update map".to_string(), - creation_date: NaiveDate::from_ymd_opt(2023, 5, 8).expect("Could not parse date!"), + name: "Test Map: can update map".to_owned(), deletion_date: None, last_visit: None, is_inactive: false, @@ -239,7 +217,7 @@ async fn test_can_update_map() { let map: MapDto = test::read_body_json(resp).await; let map_update = UpdateMapDto { - name: Some("This will succeed".to_string()), + name: Some("This will succeed".to_owned()), privacy: None, description: None, location: None, @@ -255,14 +233,14 @@ async fn test_can_update_map() { let updated_map: MapDto = test::read_body_json(resp).await; assert_ne!(updated_map.name, map.name) } + #[actix_rt::test] async fn test_can_update_map_geometry() { let pool = init_test_database(|_| async { Ok(()) }.scope_boxed()).await; let (token, app) = init_test_app(pool.clone()).await; let new_map = NewMapDto { - name: "Test Map: can update map geomety".to_string(), - creation_date: NaiveDate::from_ymd_opt(2023, 5, 8).expect("Could not parse date!"), + name: "Test Map: can update map geometry".to_owned(), deletion_date: None, last_visit: None, is_inactive: false, @@ -296,5 +274,124 @@ async fn test_can_update_map_geometry() { .await; let updated_map: MapDto = test::read_body_json(resp).await; - assert_ne!(updated_map.geometry, map.geometry) + assert_ne!(updated_map.geometry, map.geometry); +} + +async fn _get_map( + map_id: i32, + token: String, + app: &impl Service, Error = Error>, +) -> MapDto { + let resp = test::TestRequest::get() + .uri(&format!("/api/maps/{map_id}")) + .insert_header((header::AUTHORIZATION, token)) + .send_request(app) + .await; + assert_eq!(resp.status(), StatusCode::OK); + test::read_body_json(resp).await +} + +#[actix_rt::test] +async fn test_update_plantings_map_sets_updated_at() { + // This test asserts that when we add, update or remove plantings on a map, + // the modified_at field on that map updates. + let pool = init_test_database(|conn| { + async { + diesel::insert_into(crate::schema::maps::table) + .values(data::TestInsertableMap::default()) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::layers::table) + .values(data::TestInsertableLayer::default()) + .execute(conn) + .await?; + diesel::insert_into(crate::schema::plants::table) + .values(data::TestInsertablePlant::default()) + .execute(conn) + .await?; + Ok(()) + } + .scope_boxed() + }) + .await; + let (token, app) = init_test_app(pool.clone()).await; + + let planting = PlantingDto { + id: Uuid::new_v4(), + layer_id: -1, + plant_id: -1, + x: 0, + y: 0, + rotation: 0.0, + size_x: 3, + size_y: 3, + add_date: None, + seed_id: None, + is_area: false, + created_at: Utc::now().naive_utc(), + created_by: Uuid::new_v4(), + modified_at: Utc::now().naive_utc(), + modified_by: Uuid::new_v4(), + remove_date: Some(Utc::now().date_naive()), + additional_name: None, + planting_notes: String::new(), + }; + let planting_id = planting.id; + + // Updated modified_at after insert a planting + let map_at_start: MapDto = _get_map(-1, token.clone(), &app).await; + let new_planting_action: ActionDtoWrapperNewPlantings = ActionDtoWrapper { + action_id: Uuid::new_v4(), + dto: vec![planting], + }; + sleep(Duration::from_secs(1)).await; + let resp = test::TestRequest::post() + .uri("/api/maps/-1/layers/plants/plantings") + .insert_header((header::AUTHORIZATION, token.clone())) + .set_json(new_planting_action) + .send_request(&app) + .await; + + assert_eq!(resp.status(), StatusCode::CREATED); + let map_after_adding_planting: MapDto = _get_map(-1, token.clone(), &app).await; + assert!(map_after_adding_planting.modified_at > map_at_start.modified_at); + + // Updated modified_at after modifying a planting + let move_planting_action: ActionDtoWrapperUpdatePlantings = ActionDtoWrapper { + action_id: Uuid::new_v4(), + dto: UpdatePlantingDto::Transform(vec![TransformPlantingDto { + id: planting_id, + x: 20, + y: 20, + rotation: 0.3, + size_x: 10, + size_y: 10, + }]), + }; + sleep(Duration::from_secs(1)).await; + let resp = test::TestRequest::patch() + .uri("/api/maps/-1/layers/plants/plantings") + .insert_header((header::AUTHORIZATION, token.clone())) + .set_json(move_planting_action) + .send_request(&app) + .await; + assert_eq!(resp.status(), StatusCode::OK); + let map_after_update_planting = _get_map(-1, token.clone(), &app).await; + assert!(map_after_update_planting.modified_at > map_after_adding_planting.modified_at); + + // Updated modified_at after deleting planting + let delete_planting_action: ActionDtoWrapperDeletePlantings = ActionDtoWrapper { + action_id: Uuid::new_v4(), + dto: vec![DeletePlantingDto { id: planting_id }], + }; + sleep(Duration::from_secs(1)).await; + let resp = test::TestRequest::delete() + .uri("/api/maps/-1/layers/plants/plantings") + .insert_header((header::AUTHORIZATION, token.clone())) + .set_json(delete_planting_action) + .send_request(&app) + .await; + assert_eq!(resp.status(), StatusCode::OK); + let map_after_deleting_planting: MapDto = _get_map(-1, token, &app).await; + assert!(map_after_deleting_planting.modified_at > map_after_update_planting.modified_at); } diff --git a/backend/src/test/pagination.rs b/backend/src/test/pagination.rs index b6dbff76b..96ae0dcab 100644 --- a/backend/src/test/pagination.rs +++ b/backend/src/test/pagination.rs @@ -25,7 +25,7 @@ async fn test_seeds_pagination_succeeds() { &crate::schema::plants::id.eq(-1), &crate::schema::plants::unique_name.eq("Testia testia"), &crate::schema::plants::common_name_en - .eq(Some(vec![Some("Testplant".to_string())])), + .eq(Some(vec![Some("Testplant".to_owned())])), )) .execute(conn) .await?; @@ -39,7 +39,7 @@ async fn test_seeds_pagination_succeeds() { crate::schema::seeds::harvest_year.eq(2022), crate::schema::seeds::plant_id.eq(-1), crate::schema::seeds::quantity.eq(Quantity::Enough), - crate::schema::seeds::owner_id.eq(user_id), + crate::schema::seeds::created_by.eq(user_id), ) }) .collect::>(); diff --git a/backend/src/test/plant.rs b/backend/src/test/plant.rs index 2a06eafff..4bb4d43b6 100644 --- a/backend/src/test/plant.rs +++ b/backend/src/test/plant.rs @@ -21,7 +21,7 @@ async fn test_get_all_plants_succeeds() { &crate::schema::plants::id.eq(-1), &crate::schema::plants::unique_name.eq("Testia testia"), &crate::schema::plants::common_name_en - .eq(Some(vec![Some("Testplant".to_string())])), + .eq(Some(vec![Some("Testplant".to_owned())])), &crate::schema::plants::spread.eq(50), )) .execute(conn) @@ -48,15 +48,12 @@ async fn test_get_all_plants_succeeds() { let test_plant = PlantsSummaryDto { id: -1, - unique_name: "Testia testia".to_string(), - common_name_en: Some(vec![Some("Testplant".to_string())]), + unique_name: "Testia testia".to_owned(), + common_name_en: Some(vec![Some("Testplant".to_owned())]), spread: Some(50), }; - let result = test::read_body(resp).await; - let result_string = std::str::from_utf8(&result).unwrap(); - - let page: Page = serde_json::from_str(result_string).unwrap(); + let page: Page = test::read_body_json(resp).await; assert!(page.results.contains(&test_plant)); } @@ -70,7 +67,7 @@ async fn test_get_one_plant_succeeds() { &crate::schema::plants::id.eq(-1), &crate::schema::plants::unique_name.eq("Testia testia"), &crate::schema::plants::common_name_en - .eq(Some(vec![Some("Testplant".to_string())])), + .eq(Some(vec![Some("Testplant".to_owned())])), &crate::schema::plants::spread.eq(50), )) .execute(conn) @@ -97,8 +94,8 @@ async fn test_get_one_plant_succeeds() { let test_plant = PlantsSummaryDto { id: -1, - unique_name: "Testia testia".to_string(), - common_name_en: Some(vec![Some("Testplant".to_string())]), + unique_name: "Testia testia".to_owned(), + common_name_en: Some(vec![Some("Testplant".to_owned())]), spread: Some(50), }; @@ -119,7 +116,7 @@ async fn test_search_plants_succeeds() { &crate::schema::plants::id.eq(-1), &crate::schema::plants::unique_name.eq("Testia testia"), &crate::schema::plants::common_name_en - .eq(Some(vec![Some("Testplant".to_string())])), + .eq(Some(vec![Some("Testplant".to_owned())])), &crate::schema::plants::spread.eq(50), )) .execute(conn) @@ -146,8 +143,8 @@ async fn test_search_plants_succeeds() { let test_plant = PlantsSummaryDto { id: -1, - unique_name: "Testia testia".to_string(), - common_name_en: Some(vec![Some("Testplant".to_string())]), + unique_name: "Testia testia".to_owned(), + common_name_en: Some(vec![Some("Testplant".to_owned())]), spread: Some(50), }; diff --git a/backend/src/test/plant_layer_heatmap.rs b/backend/src/test/plant_layer_heatmap.rs index 61bbcae97..608530d73 100644 --- a/backend/src/test/plant_layer_heatmap.rs +++ b/backend/src/test/plant_layer_heatmap.rs @@ -32,38 +32,23 @@ async fn initial_db_values( polygon: Polygon, ) -> Result<(), ServiceError> { diesel::insert_into(crate::schema::maps::table) - .values(( - &crate::schema::maps::id.eq(-1), - &crate::schema::maps::name.eq("Test Map: can search map"), - &crate::schema::maps::creation_date - .eq(NaiveDate::from_ymd_opt(2023, 5, 8).expect("Could not parse date!")), - &crate::schema::maps::is_inactive.eq(false), - &crate::schema::maps::zoom_factor.eq(100), - &crate::schema::maps::honors.eq(0), - &crate::schema::maps::visits.eq(0), - &crate::schema::maps::harvested.eq(0), - &crate::schema::maps::privacy.eq(PrivacyOption::Public), - &crate::schema::maps::owner_id.eq(Uuid::new_v4()), - &crate::schema::maps::geometry.eq(polygon), - )) + .values(TestInsertableMap { + id: -1, + name: "Test Map: can search map", + polygon, + ..Default::default() + }) .execute(conn) .await?; diesel::insert_into(crate::schema::layers::table) - .values(( - &crate::schema::layers::id.eq(-1), - &crate::schema::layers::map_id.eq(-1), - &crate::schema::layers::type_.eq(LayerType::Plants), - &crate::schema::layers::name.eq("Some name"), - &crate::schema::layers::is_alternative.eq(false), - )) + .values(TestInsertableLayer { + id: -1, + ..Default::default() + }) .execute(conn) .await?; diesel::insert_into(crate::schema::plants::table) - .values(( - &crate::schema::plants::id.eq(-1), - &crate::schema::plants::unique_name.eq("Testia testia"), - &crate::schema::plants::common_name_en.eq(Some(vec![Some("T".to_owned())])), - )) + .values(TestInsertablePlant::default()) .execute(conn) .await?; Ok(()) diff --git a/backend/src/test/plantings.rs b/backend/src/test/plantings.rs index ad22d3894..10ac9709c 100644 --- a/backend/src/test/plantings.rs +++ b/backend/src/test/plantings.rs @@ -4,7 +4,7 @@ use std::ops::Add; use actix_http::StatusCode; use actix_web::{http::header, test}; -use chrono::{Days, NaiveDate}; +use chrono::{Days, NaiveDate, Utc}; use diesel_async::{scoped_futures::ScopedFutureExt, RunQueryDsl}; use uuid::Uuid; @@ -12,9 +12,7 @@ use crate::{ model::{ dto::{ core::{ActionDtoWrapper, TimelinePage}, - plantings::{ - DeletePlantingDto, MovePlantingDto, NewPlantingDto, PlantingDto, UpdatePlantingDto, - }, + plantings::{DeletePlantingDto, MovePlantingDto, PlantingDto, UpdatePlantingDto}, }, r#enum::layer_type::LayerType, }, @@ -124,8 +122,8 @@ async fn test_create_fails_with_invalid_layer() { let data = ActionDtoWrapper { action_id: Uuid::new_v4(), - dto: vec![NewPlantingDto { - id: Some(Uuid::new_v4()), + dto: vec![PlantingDto { + id: Uuid::new_v4(), layer_id: -1, plant_id: -1, x: 0, @@ -136,6 +134,13 @@ async fn test_create_fails_with_invalid_layer() { add_date: None, seed_id: None, is_area: false, + created_at: Utc::now().naive_utc(), + created_by: Uuid::new_v4(), + modified_at: Utc::now().naive_utc(), + modified_by: Uuid::new_v4(), + remove_date: Some(Utc::now().date_naive()), + additional_name: None, + planting_notes: String::new(), }], }; @@ -173,8 +178,8 @@ async fn test_can_create_plantings() { let data = ActionDtoWrapper { action_id: Uuid::new_v4(), - dto: vec![NewPlantingDto { - id: Some(Uuid::new_v4()), + dto: vec![PlantingDto { + id: Uuid::new_v4(), layer_id: -1, plant_id: -1, x: 0, @@ -185,6 +190,13 @@ async fn test_can_create_plantings() { add_date: None, seed_id: None, is_area: false, + created_at: Utc::now().naive_utc(), + created_by: Uuid::new_v4(), + modified_at: Utc::now().naive_utc(), + modified_by: Uuid::new_v4(), + remove_date: Some(Utc::now().date_naive()), + additional_name: None, + planting_notes: String::new(), }], }; diff --git a/backend/src/test/seed.rs b/backend/src/test/seed.rs index 2095a9b5c..4f72cb1ce 100644 --- a/backend/src/test/seed.rs +++ b/backend/src/test/seed.rs @@ -30,7 +30,7 @@ async fn test_find_two_seeds_succeeds() { &crate::schema::plants::id.eq(-1), &crate::schema::plants::unique_name.eq("Testia testia"), &crate::schema::plants::common_name_en - .eq(Some(vec![Some("Testplant".to_string())])), + .eq(Some(vec![Some("Testplant".to_owned())])), )) .execute(conn) .await?; @@ -42,7 +42,7 @@ async fn test_find_two_seeds_succeeds() { &crate::schema::seeds::name.eq("Testia testia"), &crate::schema::seeds::harvest_year.eq(2022), &crate::schema::seeds::quantity.eq(Quantity::Enough), - &crate::schema::seeds::owner_id.eq(user_id), + &crate::schema::seeds::created_by.eq(user_id), &crate::schema::seeds::plant_id.eq(-1), &crate::schema::seeds::use_by.eq(NaiveDate::from_ymd_opt(2023, 01, 01)), ), @@ -51,7 +51,7 @@ async fn test_find_two_seeds_succeeds() { &crate::schema::seeds::name.eq("Testia testium"), &crate::schema::seeds::harvest_year.eq(2022), &crate::schema::seeds::quantity.eq(Quantity::NotEnough), - &crate::schema::seeds::owner_id.eq(user_id), + &crate::schema::seeds::created_by.eq(user_id), &crate::schema::seeds::plant_id.eq(-1), &crate::schema::seeds::use_by.eq(NaiveDate::from_ymd_opt(2022, 01, 01)), ), @@ -108,7 +108,7 @@ async fn test_search_seeds_succeeds() { &crate::schema::plants::id.eq(-1), &crate::schema::plants::unique_name.eq("Testia testia"), &crate::schema::plants::common_name_en - .eq(Some(vec![Some("Testplant".to_string())])), + .eq(Some(vec![Some("Testplant".to_owned())])), )) .execute(conn) .await?; @@ -121,7 +121,7 @@ async fn test_search_seeds_succeeds() { &crate::schema::seeds::harvest_year.eq(2022), &crate::schema::seeds::quantity.eq(Quantity::Enough), &crate::schema::seeds::plant_id.eq(-1), - &crate::schema::seeds::owner_id.eq(user_id), + &crate::schema::seeds::created_by.eq(user_id), ), ( &crate::schema::seeds::id.eq(-2), @@ -129,7 +129,7 @@ async fn test_search_seeds_succeeds() { &crate::schema::seeds::harvest_year.eq(2023), &crate::schema::seeds::quantity.eq(Quantity::NotEnough), &crate::schema::seeds::plant_id.eq(-1), - &crate::schema::seeds::owner_id.eq(user_id), + &crate::schema::seeds::created_by.eq(user_id), ), ]) .execute(conn) @@ -176,7 +176,7 @@ async fn test_find_by_id_succeeds() { &crate::schema::plants::id.eq(-1), &crate::schema::plants::unique_name.eq("Testia testia"), &crate::schema::plants::common_name_en - .eq(Some(vec![Some("Testplant".to_string())])), + .eq(Some(vec![Some("Testplant".to_owned())])), )) .execute(conn) .await?; @@ -189,7 +189,7 @@ async fn test_find_by_id_succeeds() { &crate::schema::seeds::harvest_year.eq(2022), &crate::schema::seeds::quantity.eq(Quantity::Enough), &crate::schema::seeds::plant_id.eq(-1), - &crate::schema::seeds::owner_id.eq(user_id), + &crate::schema::seeds::created_by.eq(user_id), ), ( &crate::schema::seeds::id.eq(-2), @@ -197,7 +197,7 @@ async fn test_find_by_id_succeeds() { &crate::schema::seeds::harvest_year.eq(2023), &crate::schema::seeds::quantity.eq(Quantity::NotEnough), &crate::schema::seeds::plant_id.eq(-1), - &crate::schema::seeds::owner_id.eq(user_id), + &crate::schema::seeds::created_by.eq(user_id), ), ]) .execute(conn) @@ -241,7 +241,7 @@ async fn test_find_by_non_existing_id_fails() { &crate::schema::plants::id.eq(-1), &crate::schema::plants::unique_name.eq("Testia testia"), &crate::schema::plants::common_name_en - .eq(Some(vec![Some("Testplant".to_string())])), + .eq(Some(vec![Some("Testplant".to_owned())])), )) .execute(conn) .await?; @@ -254,7 +254,7 @@ async fn test_find_by_non_existing_id_fails() { &crate::schema::seeds::harvest_year.eq(2022), &crate::schema::seeds::quantity.eq(Quantity::Enough), &crate::schema::seeds::plant_id.eq(-1), - &crate::schema::seeds::owner_id.eq(user_id), + &crate::schema::seeds::created_by.eq(user_id), ), ( &crate::schema::seeds::id.eq(-2), @@ -262,7 +262,7 @@ async fn test_find_by_non_existing_id_fails() { &crate::schema::seeds::harvest_year.eq(2023), &crate::schema::seeds::quantity.eq(Quantity::NotEnough), &crate::schema::seeds::plant_id.eq(-1), - &crate::schema::seeds::owner_id.eq(user_id), + &crate::schema::seeds::created_by.eq(user_id), ), ]) .execute(conn) @@ -359,7 +359,7 @@ async fn test_create_seed_ok() { &crate::schema::plants::id.eq(-1), &crate::schema::plants::unique_name.eq("Testia testia"), &crate::schema::plants::common_name_en - .eq(Some(vec![Some("Testplant".to_string())])), + .eq(Some(vec![Some("Testplant".to_owned())])), )) .execute(conn) .await?; @@ -371,7 +371,7 @@ async fn test_create_seed_ok() { let (token, app) = init_test_app(pool.clone()).await; let new_seed = NewSeedDto { - name: "tomato test".to_string(), + name: "tomato test".to_owned(), plant_id: Some(-1), harvest_year: 2022, quantity: Quantity::Nothing, @@ -414,7 +414,7 @@ async fn test_delete_by_id_succeeds() { &crate::schema::seeds::name.eq("Testia testia"), &crate::schema::seeds::harvest_year.eq(2022), &crate::schema::seeds::quantity.eq(Quantity::Enough), - &crate::schema::seeds::owner_id.eq(user_id), + &crate::schema::seeds::created_by.eq(user_id), )) .execute(conn) .await?; @@ -445,7 +445,7 @@ async fn test_delete_by_non_existing_id_succeeds() { &crate::schema::seeds::name.eq("Testia testia"), &crate::schema::seeds::harvest_year.eq(2022), &crate::schema::seeds::quantity.eq(Quantity::Enough), - &crate::schema::seeds::owner_id.eq(user_id), + &crate::schema::seeds::created_by.eq(user_id), )) .execute(conn) .await?; @@ -475,7 +475,7 @@ async fn test_archive_seed_succeeds() { &crate::schema::plants::id.eq(-1), &crate::schema::plants::unique_name.eq("Testia testia"), &crate::schema::plants::common_name_en - .eq(Some(vec![Some("Testplant".to_string())])), + .eq(Some(vec![Some("Testplant".to_owned())])), )) .execute(conn) .await?; @@ -488,7 +488,7 @@ async fn test_archive_seed_succeeds() { &crate::schema::seeds::harvest_year.eq(2022), &crate::schema::seeds::quantity.eq(Quantity::Enough), &crate::schema::seeds::plant_id.eq(-1), - &crate::schema::seeds::owner_id.eq(user_id), + &crate::schema::seeds::created_by.eq(user_id), &crate::schema::seeds::use_by.eq(NaiveDate::from_ymd_opt(2023, 01, 01)), ), ( @@ -497,7 +497,7 @@ async fn test_archive_seed_succeeds() { &crate::schema::seeds::harvest_year.eq(2022), &crate::schema::seeds::quantity.eq(Quantity::NotEnough), &crate::schema::seeds::plant_id.eq(-1), - &crate::schema::seeds::owner_id.eq(user_id), + &crate::schema::seeds::created_by.eq(user_id), &crate::schema::seeds::use_by.eq(NaiveDate::from_ymd_opt(2022, 01, 01)), ), ]) diff --git a/backend/src/test/users.rs b/backend/src/test/users.rs index 88570a30f..659627a99 100644 --- a/backend/src/test/users.rs +++ b/backend/src/test/users.rs @@ -16,7 +16,7 @@ async fn test_can_create_user_data() { let user_data = UsersDto { salutation: Salutation::Mr, title: None, - country: "Austria".to_string(), + country: "Austria".to_owned(), phone: None, website: None, organization: None, diff --git a/backend/src/test/util.rs b/backend/src/test/util.rs index 6849908c6..73d927cd2 100644 --- a/backend/src/test/util.rs +++ b/backend/src/test/util.rs @@ -73,6 +73,7 @@ pub async fn init_test_app( String, impl Service, Error = Error>, ) { + let _ = env_logger::try_init(); let app = init_test_app_impl(pool).await; let token = setup_auth(); diff --git a/backend/src/test/util/data.rs b/backend/src/test/util/data.rs index 3e92c6c4a..6ba5287ef 100644 --- a/backend/src/test/util/data.rs +++ b/backend/src/test/util/data.rs @@ -1,6 +1,6 @@ //! Dummy-Data for tests. -use chrono::NaiveDate; +use chrono::{NaiveDate, NaiveDateTime}; use diesel::Insertable; use postgis_diesel::types::{Point, Polygon}; use uuid::Uuid; @@ -14,15 +14,21 @@ use super::dummy_map_polygons::tall_rectangle; pub struct TestInsertableMap { pub id: i32, pub name: String, - pub creation_date: NaiveDate, + pub deletion_date: Option, + pub last_visit: Option, pub is_inactive: bool, pub zoom_factor: i16, pub honors: i16, pub visits: i16, pub harvested: i16, pub privacy: PrivacyOption, - pub owner_id: Uuid, + pub description: Option, + pub location: Option, + pub created_by: Uuid, pub geometry: Polygon, + pub created_at: NaiveDateTime, + pub modified_at: NaiveDateTime, + pub modified_by: Uuid, } impl Default for TestInsertableMap { @@ -30,19 +36,30 @@ impl Default for TestInsertableMap { Self { id: -1, name: "Test Map 1".to_owned(), - creation_date: NaiveDate::from_ymd_opt(2023, 5, 8).expect("Could not parse date!"), is_inactive: false, zoom_factor: 100, honors: 0, visits: 0, harvested: 0, privacy: PrivacyOption::Public, - owner_id: Uuid::default(), + created_by: Uuid::default(), + modified_by: Uuid::default(), geometry: tall_rectangle(), + deletion_date: NaiveDate::from_ymd_opt(2000, 1, 1), + last_visit: NaiveDate::from_ymd_opt(2000, 1, 1), + description: Some(String::new()), + location: None, + created_at: NaiveDate::from_ymd_opt(2016, 7, 8) + .unwrap() + .and_hms_opt(9, 10, 11) + .unwrap(), + modified_at: NaiveDate::from_ymd_opt(2016, 7, 8) + .unwrap() + .and_hms_opt(9, 10, 11) + .unwrap(), } } } - #[derive(Insertable)] #[diesel(table_name = crate::schema::layers)] pub struct TestInsertableLayer { @@ -96,6 +113,13 @@ pub struct TestInsertablePlanting { pub rotation: f32, pub add_date: Option, pub remove_date: Option, + pub seed_id: Option, + pub is_area: bool, + pub notes: String, + pub created_at: NaiveDateTime, + pub modified_at: NaiveDateTime, + pub created_by: Uuid, + pub modified_by: Uuid, } impl Default for TestInsertablePlanting { @@ -111,6 +135,19 @@ impl Default for TestInsertablePlanting { rotation: 0.0, add_date: None, remove_date: None, + seed_id: None, + is_area: false, + notes: String::new(), + created_at: NaiveDate::from_ymd_opt(2016, 7, 8) + .unwrap() + .and_hms_opt(9, 10, 11) + .unwrap(), + modified_at: NaiveDate::from_ymd_opt(2016, 7, 8) + .unwrap() + .and_hms_opt(9, 10, 11) + .unwrap(), + created_by: Uuid::default(), + modified_by: Uuid::default(), } } } diff --git a/backend/src/test/util/jwks.rs b/backend/src/test/util/jwks.rs index d7d0eb135..402800396 100644 --- a/backend/src/test/util/jwks.rs +++ b/backend/src/test/util/jwks.rs @@ -15,7 +15,7 @@ pub fn init_auth() -> JsonWebKey { // Init application auth settings Config::set(Config { openid_configuration: OpenIDEndpointConfiguration::default(), - client_id: "PermaplanT".to_string(), + client_id: "PermaplanT".to_owned(), jwk_set: JwkSet { keys: vec![jwk1] }, }); diff --git a/backend/typeshare.toml b/backend/typeshare.toml index b5be91d8e..50816d788 100644 --- a/backend/typeshare.toml +++ b/backend/typeshare.toml @@ -1,4 +1,5 @@ [typescript.type_mappings] "NaiveDate" = "string" +"NaiveDateTime" = "string" "Uuid" = "string" "Value" = "object" diff --git a/doc/changelog.md b/doc/changelog.md index a3a34ab1e..ad0286888 100644 --- a/doc/changelog.md +++ b/doc/changelog.md @@ -11,6 +11,7 @@ Syntax: `- short text describing the change _(Your Name)_` - needs new migrations - needs new scraper data (integer for plant spread and height) - pin python package versions for e2e tests #1200 _(4ydan)_ +- Add auditability metadata to plantings and maps _(Jannis Adamek)_ - Remove variety from table seeds _(Jannis Adamek)_ - Add timeline API that summarizes plantings #967 _(Jannis Adamek)_ - Refactor react query error handling _(Daniel Steinkogler)_ diff --git a/doc/database/schemata/01er_diagram.md b/doc/database/schemata/01er_diagram.md index f0251828a..f71e7c967 100644 --- a/doc/database/schemata/01er_diagram.md +++ b/doc/database/schemata/01er_diagram.md @@ -96,7 +96,7 @@ maps { INT honors INT visits INT harvested - DATE creation_date + DATE created_at DATE deletion_date DATE inactivity_date INT zoom_factor diff --git a/doc/database/schemata/02table_descriptions.md b/doc/database/schemata/02table_descriptions.md index 61e640490..e78b9badd 100644 --- a/doc/database/schemata/02table_descriptions.md +++ b/doc/database/schemata/02table_descriptions.md @@ -72,7 +72,7 @@ Store relations between plants. | **_Column name_** | **_Example_** | **_Description_** | | :------------------ | :------------ | :--------------------------------------------------------------------------------------------- | | **id** | 1 | -| **owner_id** | 1 | +| **created_by** | 1 | | **name** | My Map | only alphanumerical values | | **is_inactive** | false | | **last_visit** | 2023-04-04 | @@ -80,7 +80,7 @@ Store relations between plants. | **visits** | 0 | 0 to infinity | | **harvested** | 0 | 0 to infinity, amount of plants harvested on this map | | **version_date** | 2023-04-04 | the date the snapshot for this version was taken | -| **creation_date** | 2023-04-04 | +| **created_at** | 2023-04-04 | | **deletion_date** | 2023-04-04 | | **inactivity_date** | 2023-04-04 | | **zoom_factor** | 100 | value used in formula "X by X cm", e.g. 100 would mean "100 x 100 cm", range from 10 to 100000 | @@ -160,7 +160,7 @@ Store relations between plants. | **_Column name_** | **_Example_** | **_Description_** | | :---------------- | :------------------------------------- | :----------------------------------------------------------------------------- | | **id** | 1 | list id | -| **owner_id** | 1 | user id | +| **created_by** | 1 | user id | | **map_id** | 1 | id of the map this list is for | | **name** | Smoothie Ingredients | name of the list | | **description** | Ingredients for my strawberry smoothie | description of the list | diff --git a/frontend/src/features/map_planning/api/plantingApi.ts b/frontend/src/features/map_planning/api/plantingApi.ts index b59963cef..43ce8a172 100644 --- a/frontend/src/features/map_planning/api/plantingApi.ts +++ b/frontend/src/features/map_planning/api/plantingApi.ts @@ -2,7 +2,6 @@ import { ActionDtoWrapper, DeletePlantingDto, MovePlantingDto, - NewPlantingDto, PlantingDto, TimelinePage, TransformPlantingDto, @@ -37,10 +36,10 @@ export async function getPlantings( } } -export async function createPlanting(mapId: number, actionId: string, data: NewPlantingDto[]) { +export async function createPlanting(mapId: number, actionId: string, data: PlantingDto[]) { const http = createAPI(); - const dto: ActionDtoWrapper = { + const dto: ActionDtoWrapper = { actionId: actionId, dto: data, }; diff --git a/frontend/src/features/map_planning/layers/plant/PlantsLayer.tsx b/frontend/src/features/map_planning/layers/plant/PlantsLayer.tsx index f935a2cb5..00d7f0403 100644 --- a/frontend/src/features/map_planning/layers/plant/PlantsLayer.tsx +++ b/frontend/src/features/map_planning/layers/plant/PlantsLayer.tsx @@ -79,6 +79,11 @@ function usePlantLayerListeners(listening: boolean) { isArea: args.isArea, // This `satisfies` gives us type safety while omitting the `sizeX` and `sizeY` properties // they get set later in this function + modifiedAt: '', + modifiedBy: '', + createdAt: '', + createdBy: '', + plantingNotes: '', } satisfies Omit< ConstructorParameters[0][number], 'sizeX' | 'sizeY' diff --git a/frontend/src/features/map_planning/layers/plant/actions.ts b/frontend/src/features/map_planning/layers/plant/actions.ts index 55b5c2dfb..79bf8838a 100644 --- a/frontend/src/features/map_planning/layers/plant/actions.ts +++ b/frontend/src/features/map_planning/layers/plant/actions.ts @@ -4,7 +4,6 @@ import { v4 } from 'uuid'; import { PlantingDto, - CreatePlantActionPayload, DeletePlantActionPayload, MovePlantActionPayload, TransformPlantActionPayload, @@ -40,7 +39,7 @@ export class CreatePlantAction return this._ids; } - constructor(private readonly _data: CreatePlantActionPayload[], public actionId = v4()) { + constructor(private readonly _data: PlantingDto[], public actionId = v4()) { this._ids = _data.map(({ id }) => id); } diff --git a/frontend/src/features/map_planning/store/MapStore.test.ts b/frontend/src/features/map_planning/store/MapStore.test.ts index 91e20a33b..c4170ef37 100644 --- a/frontend/src/features/map_planning/store/MapStore.test.ts +++ b/frontend/src/features/map_planning/store/MapStore.test.ts @@ -45,7 +45,7 @@ describe('MapHistoryStore', () => { it('adds a history entry for each call to executeAction', () => { const { executeAction } = useMapStore.getState(); - const createAction = new CreatePlantAction([createPlantTestObject(1)]); + const createAction = new CreatePlantAction([createNewPlantingTestObject(1)]); executeAction(createAction); @@ -56,7 +56,7 @@ describe('MapHistoryStore', () => { it('it adds an entry to the history that is the inverse of the action', () => { const { executeAction } = useMapStore.getState(); - const createAction = new CreatePlantAction([createPlantTestObject(1)]); + const createAction = new CreatePlantAction([createNewPlantingTestObject(1)]); executeAction(createAction); @@ -68,7 +68,7 @@ describe('MapHistoryStore', () => { it('does not add a history entry for a remote action', () => { const { __applyRemoteAction } = useMapStore.getState(); - const createAction = new CreatePlantAction([createPlantTestObject(1)]); + const createAction = new CreatePlantAction([createNewPlantingTestObject(1)]); __applyRemoteAction(createAction); @@ -79,8 +79,8 @@ describe('MapHistoryStore', () => { it('adds plant objects to the plants layer on CreatePlantAction', () => { const { executeAction } = useMapStore.getState(); - const createAction1 = new CreatePlantAction([createPlantTestObject(1)]); - const createAction2 = new CreatePlantAction([createPlantTestObject(2)]); + const createAction1 = new CreatePlantAction([createNewPlantingTestObject(1)]); + const createAction2 = new CreatePlantAction([createNewPlantingTestObject(2)]); executeAction(createAction1); executeAction(createAction2); @@ -101,7 +101,7 @@ describe('MapHistoryStore', () => { it("updates a single plants's position on MovePlantAction", () => { const { executeAction } = useMapStore.getState(); - const createAction = new CreatePlantAction([createPlantTestObject(1)]); + const createAction = new CreatePlantAction([createNewPlantingTestObject(1)]); const moveAction = new MovePlantAction([ { id: '1', @@ -124,7 +124,7 @@ describe('MapHistoryStore', () => { it("updates a single plants's transform on TransformPlantAction", () => { const { executeAction } = useMapStore.getState(); - const createAction = new CreatePlantAction([createPlantTestObject(1)]); + const createAction = new CreatePlantAction([createNewPlantingTestObject(1)]); const transformAction = new TransformPlantAction([ { id: '1', @@ -153,8 +153,8 @@ describe('MapHistoryStore', () => { it('updates multiple plants on MovePlantAction', () => { const { executeAction } = useMapStore.getState(); - const createAction1 = new CreatePlantAction([createPlantTestObject(1)]); - const createAction2 = new CreatePlantAction([createPlantTestObject(2)]); + const createAction1 = new CreatePlantAction([createNewPlantingTestObject(1)]); + const createAction2 = new CreatePlantAction([createNewPlantingTestObject(2)]); const moveAction = new MovePlantAction([ { id: '1', @@ -188,8 +188,8 @@ describe('MapHistoryStore', () => { it('updates multiple objects on TransformPlatAction', () => { const { executeAction } = useMapStore.getState(); - const createAction1 = new CreatePlantAction([createPlantTestObject(1)]); - const createAction2 = new CreatePlantAction([createPlantTestObject(2)]); + const createAction1 = new CreatePlantAction([createNewPlantingTestObject(1)]); + const createAction2 = new CreatePlantAction([createNewPlantingTestObject(2)]); const transformAction = new TransformPlantAction([ { id: '1', @@ -235,7 +235,7 @@ describe('MapHistoryStore', () => { it('reverts one action on undo()', () => { const { executeAction } = useMapStore.getState(); - const createAction = new CreatePlantAction([createPlantTestObject(1)]); + const createAction = new CreatePlantAction([createNewPlantingTestObject(1)]); const moveAction = new MovePlantAction([ { id: '1', @@ -250,7 +250,7 @@ describe('MapHistoryStore', () => { const { trackedState: newState } = useMapStore.getState(); expect(newState.layers.plants.objects).toHaveLength(1); - expect(newState.layers.plants.objects[0]).toEqual(createPlantTestObject(1)); + expect(newState.layers.plants.objects[0]).toEqual(createNewPlantingTestObject(1)); useMapStore.getState().undo(); @@ -260,8 +260,8 @@ describe('MapHistoryStore', () => { it('reverts multiple plants to their original position on undo()', () => { const { executeAction } = useMapStore.getState(); - const createAction1 = new CreatePlantAction([createPlantTestObject(1)]); - const createAction2 = new CreatePlantAction([createPlantTestObject(2)]); + const createAction1 = new CreatePlantAction([createNewPlantingTestObject(1)]); + const createAction2 = new CreatePlantAction([createNewPlantingTestObject(2)]); const moveAction = new MovePlantAction([ { id: '1', @@ -283,14 +283,14 @@ describe('MapHistoryStore', () => { const { trackedState: newState } = useMapStore.getState(); expect(newState.layers.plants.objects).toHaveLength(2); - expect(newState.layers.plants.objects[0]).toEqual(createPlantTestObject(1)); - expect(newState.layers.plants.objects[1]).toEqual(createPlantTestObject(2)); + expect(newState.layers.plants.objects[0]).toEqual(createNewPlantingTestObject(1)); + expect(newState.layers.plants.objects[1]).toEqual(createNewPlantingTestObject(2)); }); it('repeats one action per redo() after undo()', () => { const { executeAction } = useMapStore.getState(); - const createAction = new CreatePlantAction([createPlantTestObject(1)]); + const createAction = new CreatePlantAction([createNewPlantingTestObject(1)]); const moveAction = new MovePlantAction([ { id: '1', @@ -349,7 +349,7 @@ describe('MapHistoryStore', () => { useMapStore.getState().updateSelectedLayer(createTestLayerObject()); const { executeAction } = useMapStore.getState(); - const createAction = new CreatePlantAction([createPlantTestObject(1)]); + const createAction = new CreatePlantAction([createNewPlantingTestObject(1)]); executeAction(createAction); useMapStore.getState().undo(); @@ -387,7 +387,7 @@ function createTestLayerObject(): LayerDto { return { id: -1, map_id: -1, type_: LayerType.Soil, name: 'Test Layer', is_alternative: false }; } -function createPlantTestObject(testValue: number): PlantingDto { +function createNewPlantingTestObject(testValue: number): PlantingDto { return { id: testValue.toString(), layerId: 1, @@ -398,5 +398,10 @@ function createPlantTestObject(testValue: number): PlantingDto { y: testValue, rotation: testValue, isArea: false, + createdAt: '', + createdBy: '', + modifiedAt: '', + modifiedBy: '', + plantingNotes: '', }; } diff --git a/frontend/src/features/map_planning/store/TrackedMapStore.ts b/frontend/src/features/map_planning/store/TrackedMapStore.ts index 7070ce959..516eba6f8 100644 --- a/frontend/src/features/map_planning/store/TrackedMapStore.ts +++ b/frontend/src/features/map_planning/store/TrackedMapStore.ts @@ -21,7 +21,6 @@ export const createTrackedMapSlice: StateCreator< undo: () => undo(set, get), redo: () => redo(set, get), __applyRemoteAction: (action: Action) => applyAction(action, set, get), - initPlantLayer: (plants: PlantingDto[]) => { set((state) => ({ ...state, diff --git a/frontend/src/features/maps/api/findAllMaps.ts b/frontend/src/features/maps/api/findAllMaps.ts index 1982c2b21..9a7c35fa8 100644 --- a/frontend/src/features/maps/api/findAllMaps.ts +++ b/frontend/src/features/maps/api/findAllMaps.ts @@ -16,8 +16,8 @@ export const findAllMaps = async ( if (mapSearchParameters.name) { searchParams.append('name', mapSearchParameters.name); } - if (mapSearchParameters.owner_id) { - searchParams.append('owner_id', mapSearchParameters.owner_id); + if (mapSearchParameters.created_by) { + searchParams.append('created_by', mapSearchParameters.created_by); } if (mapSearchParameters.privacy) { searchParams.append('privacy', mapSearchParameters.privacy); diff --git a/frontend/src/features/maps/components/MapCard.tsx b/frontend/src/features/maps/components/MapCard.tsx index 8a03b35e1..68dbd91cc 100644 --- a/frontend/src/features/maps/components/MapCard.tsx +++ b/frontend/src/features/maps/components/MapCard.tsx @@ -30,7 +30,7 @@ export default function MapCard({ map }: MapCardProps) { {map.name} - {map.creation_date} + {map.created_at} ({t(`privacyOptions:${map.privacy}`)})
diff --git a/frontend/src/features/maps/routes/MapCreateForm.tsx b/frontend/src/features/maps/routes/MapCreateForm.tsx index 6f918a94f..87a2bfbac 100644 --- a/frontend/src/features/maps/routes/MapCreateForm.tsx +++ b/frontend/src/features/maps/routes/MapCreateForm.tsx @@ -117,7 +117,6 @@ export default function MapCreateForm() { } const newMap: NewMapDto = { name: mapInput.name, - creation_date: new Date().toISOString().split('T')[0], is_inactive: false, zoom_factor: 100, honors: 0, diff --git a/frontend/src/features/maps/routes/MapOverview.tsx b/frontend/src/features/maps/routes/MapOverview.tsx index 433d90a6f..b368bd06f 100644 --- a/frontend/src/features/maps/routes/MapOverview.tsx +++ b/frontend/src/features/maps/routes/MapOverview.tsx @@ -23,7 +23,7 @@ export default function MapOverview() { const [infoMessage, setInfoMessage] = useState(initialMessage); const searchParams: MapSearchParameters = { - owner_id: user?.profile.sub, + created_by: user?.profile.sub, }; const { data } = useMapsSearch(searchParams); @@ -48,7 +48,6 @@ export default function MapOverview() { ).length; const mapCopy: NewMapDto = { name: `${targetMap.name.replace(/ \([0123456789]+\)$/, '')} (${copyNumber})`, - creation_date: new Date().toISOString().split('T')[0], deletion_date: targetMap.deletion_date, last_visit: targetMap.last_visit, is_inactive: targetMap.is_inactive, diff --git a/frontend/src/utils/plant-naming.test.tsx b/frontend/src/utils/plant-naming.test.tsx index 4cc6632f2..24c42203c 100644 --- a/frontend/src/utils/plant-naming.test.tsx +++ b/frontend/src/utils/plant-naming.test.tsx @@ -215,6 +215,6 @@ function generateTestSeed(seed_name: string): SeedDto { name: seed_name, harvest_year: 2022, quantity: Quantity.Enough, - owner_id: '00000000-0000-0000-0000-000000000000', + created_by: '00000000-0000-0000-0000-000000000000', }; }