From f75c9f0f8bd7fa98b26159b5c4fe07cf2c1c566c Mon Sep 17 00:00:00 2001 From: andriacap <111564663+andriacap@users.noreply.github.com> Date: Mon, 12 Dec 2022 11:25:24 +0100 Subject: [PATCH] =?UTF-8?q?Refonte=20monitoring=20phase=201=20(#117)=20:?= =?UTF-8?q?=20DB=20cr=C3=A9ation=20mod=C3=A8le=20mutiplrotocole;=20BACK=20?= =?UTF-8?q?:=20cr=C3=A9ation=20routes=20sp=C3=A9cifiques=20+=20impl=C3=A9m?= =?UTF-8?q?entation=20CRUVED;=20FRONT:=20gestionnaire=20de=20site=20feat:?= =?UTF-8?q?=20[6.2]=20Page=20d'accueil=20module=20monitoring=20layout=20et?= =?UTF-8?q?=20config=20title=20et=20description=20#2=20(#15)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: [6.2] Page d'accueil modul monitoring layout et config title et description #2 * chore(config): applied black & removed imports Feat/monitoring sites (#16) * feat(api): wip began add site routes + tests With site categories Also add tests * feat(api): add more routes * test(api): add tests and fixtures * style(api): applied black * feat(db): add migration to remove id_module Column in t_sites_groups * refactor(api): move utils for routes from sites * feat(api): wip: add sites groups route * test(api): wip: begin adding fixture site_groups * fix: remove id_module in all models * chore: rename route for better consistency * tests: moved site_groups in tests and add tests * chore(api): applied black * refactor(api): add filter params function And refact routes to use it Feat/site type categories and module categorie (#18) * feat(api): add association table with alembic Add model in backend and alembic migration Reviewed-by: andriac [Refs ticket]: #3 * test: WIP add test to see new relationship Adding test to see if categories are showing up when we call module Reviewed-by: andriacap [Refs ticket]: #3 * feat: add type site - categorie relation WIP - add selectfield to get type site in admin module Reviewed-by: andriac [Refs ticket]: #3 * feat(api): Flask admin and routes categories Clean code for change label list and form selectfield for the BibCategorieView in Flask Admin Add utils routes to get all subtable relationship in order to get back the label type site Review-by: andriac [Refs ticket]: #3 * refactor: remove paginate_nested For depth in as_dict() * test: fix tests due to as_dict depth * style: applied black and isort * chore: remove unused import Co-authored-by: Andria Capai Feat/edit categories module (#19) * tests: make tests and fixtures work for modules By changing the way a monitoring module is created in the fixture Add two tests to check the relationship * feat(api): add categories in edit module * style: applied black test: refactor fixtures to load them automatically (#20) Need to set the current working directory of pytest the directory where the gn_monitoring_module repo is (so with the frontend etc.) test: move test_route in parent dir (#17) clean: Merge la PR move test_route Co-authored-by: Andria Capai Feat/create marshmallow schemas and remove id_module (#21) * feat(api): remove id_module from sites_complements Co-authored-by: andriacap * feat(api): create schema for sites_groups * test: add sites_group to site fixture * test: wip add test for sites_groups schemas * chore(api): remove depth parameter from paginate * test: updated to work with sites_group schema * feat: categorie site with marshmallow Test and marshmallow create/refactor to adapt for bibcategorie site paginate WIP : Adapt load_only site_type in test to "assert" same object when initiate BibCategorieSite [Refs ticket]: #3 * feat(api): route /sites/categories/id with schema Changing the route to return a dump Marshmallow schema BibCategorieSitesSchema Reviewed-by: andriac [Refs ticket]: #3 * test(api): routes get categoires label Change the "as_dict" by schema.dump in order to use the Marshmallow schema created Reviewed-by: andriac [Refs ticket]: #3 * feat(api): Sites: cols to geoserializable + schema * style(api): applied black * test(api): add test for Site Schema * style(api): applied black to test_site * refactor(api): instantiate schema once Instead of for each all() iteration * chore(api): remove useless comments * chore(api): remove useless comments and imports Co-authored-by: andriacap Co-authored-by: Andria Capai refactor(api): remove id_type in admin (#22) To replace it with a bib nomenclature type: TYPE_SITE style(config): rename attribut label of categories (#23) Fix/paginate utils (#24) * fix(api): add int conversion for limit/offset * test(api): add test for get_limit_offset * fix(api): max_per_page => per_page & test * test(api): update test with changes on schema Since marshmallow schemas, the json returned by pagination has changed for items and not sites Feat/improve filter (#25) * feat(api): add Query Class to sites, grps & cats * feat(api): add sort and fix _get_model Via _get_entity * test(api): test sort query * feat(api): add api sort/sort_dir params To be able to sort through REST Api * fix(api): check if integer to avoid using ilike In filter_by_params * test(api): add test to check filter integer fix(config): changed categories into items (#29) * Merged feat/package-module-alembic commit 70b01395d5031487a4e74e9cbe60a87ceabe2a70 Author: Maxime Vergez Date: Tue Dec 6 15:22:56 2022 +0100 style: apply black commit 2badedf9976435319cc577ec3e4b861e1a4ec3c1 Author: Maxime Vergez Date: Tue Dec 6 15:11:41 2022 +0100 chore(db): removed unused import commit 57d9b523d620cc55580002a53f2883cdf692ba5f Author: Maxime Vergez Date: Tue Dec 6 14:47:48 2022 +0100 feat(db): improved & finished downgrade func commit 211780f4467f27f0e188623b03c46c25823c34bc Author: Maxime Vergez Date: Tue Dec 6 09:32:12 2022 +0100 feat(db): wip: add upgrade and began downgrade Downgrade migration need a little bit more work due to the use of sql alchemy and not plain text sql commit 0b5a3b883fe8f08e0f8d34198ab3c44b39728159 Author: Maxime Vergez Date: Mon Dec 5 11:40:32 2022 +0100 feat: packaged module * Feat/edit categories module (#19) * tests: make tests and fixtures work for modules By changing the way a monitoring module is created in the fixture Add two tests to check the relationship * feat(api): add categories in edit module * style: applied black * fix(config): changed categories into items Following the change with marshmallow schemas Fix/db migrations (#31) * feat(db): upgrade down_revision following rebase Since rebase with develop: changed the down_revision number * fix(db): fix bind params enabling downgrade Beforehand the downgrade was not possible... * refactor(db): removed cor_site_type_category * refactor(db): changed category into type in cor * refactor(db): create cor_type_site * fix(db): renamed column * refactor(api): update models to fit migrations * fix(db):change bib_categorie_site to bib_type_site Adding : cor_site_module cor_site_type revision alembic to create function and trigger in order to add bib_type_site but only with nomenclature 'TYPE_SITE' upgrade and downgrade works [Refs ticket]: #3 Reviewed-by: andriac * fix(api): updated models from migrations * fix(api): wip: fix admin following migrations * fix(api): update routes and tests To match migration changes * feat: flask admin bib_type_site Change bib_categories to bib_type_site into flask admin Adding filtering in list label_fr of type_site to secure the unique constraint Reviewed-by: andriac [Refs ticket]: #3 * fix(api): updated schema to match models * fix(api): module edition * style(api): uniformize type_site * style(api): change relationship name for type_site * feat(api): validator admin * fix(api): make unique BibTypeSite in admin * test(api): fix test when existing nomenclatures In database Co-authored-by: Andria Capai perf(api): improved loading of modules (#30) * Merged feat/package-module-alembic commit 70b01395d5031487a4e74e9cbe60a87ceabe2a70 Author: Maxime Vergez Date: Tue Dec 6 15:22:56 2022 +0100 style: apply black commit 2badedf9976435319cc577ec3e4b861e1a4ec3c1 Author: Maxime Vergez Date: Tue Dec 6 15:11:41 2022 +0100 chore(db): removed unused import commit 57d9b523d620cc55580002a53f2883cdf692ba5f Author: Maxime Vergez Date: Tue Dec 6 14:47:48 2022 +0100 feat(db): improved & finished downgrade func commit 211780f4467f27f0e188623b03c46c25823c34bc Author: Maxime Vergez Date: Tue Dec 6 09:32:12 2022 +0100 feat(db): wip: add upgrade and began downgrade Downgrade migration need a little bit more work due to the use of sql alchemy and not plain text sql commit 0b5a3b883fe8f08e0f8d34198ab3c44b39728159 Author: Maxime Vergez Date: Mon Dec 5 11:40:32 2022 +0100 feat: packaged module * perf(api): improved loading of modules When there are loads of datasets, the relationship takes a lot of time to load, and is useless for the route that uses it. * fix: due to wrong rebase Fix: newline on data_utils.py Fix: relationships are not joined anymore since there is no need of them Chore: update comment Fix/pagination (#28) * fix(api): page - 1 that returned the wrong page * test(api): add test for paginate style(api): restore data_utils spaces (#33) Fix/db migrations checkconstrainton bib_type_site.id_nomenclature (#34) * fix(db): change trigger to constraint (migrations) Delete the trigger and create check constraint on id_nomenclature column Reviewed-by: andriacap [Refs ticket]: #3 * fix(db) : apply black on migration file Apply black [Refs_ticket]: #3 fix(api): invert filter condition with Unicode (#35) So that it will fallback to == most of the time and ilike when just Unicode fix(db): add NOT VALID in constraint for bib_type_site (#36) Cannot use alembic to do this because need sqlalchemy >= 1.4.32 Fix/review checkpoint1 (#37) * refactor(api): change offset to page * refactor(api): rename id_nomenclature * fix(admin): add compare field to Unique Since now, BibTypeSite and TNomenclature do not share the same column anymore (id_nomenclature_type_site vs id_nomenclature) Add comments on new table improve downgrade migration Black Fix/db migrations (#31) * feat(db): upgrade down_revision following rebase Since rebase with develop: changed the down_revision number * fix(db): fix bind params enabling downgrade Beforehand the downgrade was not possible... * refactor(db): removed cor_site_type_category * refactor(db): changed category into type in cor * refactor(db): create cor_type_site * fix(db): renamed column * refactor(api): update models to fit migrations * fix(db):change bib_categorie_site to bib_type_site Adding : cor_site_module cor_site_type revision alembic to create function and trigger in order to add bib_type_site but only with nomenclature 'TYPE_SITE' upgrade and downgrade works [Refs ticket]: #3 Reviewed-by: andriac * fix(api): updated models from migrations * fix(api): wip: fix admin following migrations * fix(api): update routes and tests To match migration changes * feat: flask admin bib_type_site Change bib_categories to bib_type_site into flask admin Adding filtering in list label_fr of type_site to secure the unique constraint Reviewed-by: andriac [Refs ticket]: #3 * fix(api): updated schema to match models * fix(api): module edition * style(api): uniformize type_site * style(api): change relationship name for type_site * feat(api): validator admin * fix(api): make unique BibTypeSite in admin * test(api): fix test when existing nomenclatures In database Co-authored-by: Andria Capai Feat/get all sites (#26) * test(api): test all_geometries route * feat(api): geojson instead of geobuf for sites * feat(api): add all_sites_group_geometry route To return the geometries of all sites groups * test(api): refactor fixture To add a new one: site_group_with_sites since not all sites_groups have sites * test(api): test get_sites_groups route * feat(api): add possibility to filter On id_base_site, base_site_name and id_sites_group * test(api): add fixture to get group without site Feat/crud/gp sites components (#38) * feat(front): wip sites_groups component and svc * WIP feat(front): DataTable sites_groups - Table with specific data value (OK) - Table with sort column (OK) - Datatable , select row event and change color (wip). Reviewed-by: andriac [Refs ticket]: #4 * feat(front): Datatable format and selected row - Get and display data from group_site database (OK) - Selecting row and get id of row table (OK) Improve : - improve assign colname table outside the component (maybe into the class folder ?) Todo/next: - Filtering table - Add action column to table - refactor code by creating component for the ngx-datable in order to reuse component for other data Reviewed-by: andriac [Refs ticket]: #4 * feat(front): Filtering table (OK) Function filtering is working on each column Button filtering hide/display filter inputs (OK) TODO: - check for "help" icon and description for each column if description present - Check if multiple rows and pages if it's working Reviewed-by: andriac [Refs ticket]: #4 * feat(front): wip server pagination & filtering * feat(front): add sorting capability and fixes Fix: keep filters (sort and filters) on when changing pages Feat: remove useless id column Feat: add sorting * fix(db): change trigger to constraint (migrations) Delete the trigger and create check constraint on id_nomenclature column Reviewed-by: andriacap [Refs ticket]: #3 * refactor: Custom type Geojson and group properties Create custom type geojson and build properties into marshmallow schema according to this type of Geojson [Refs ticket]: #3 * refactor: site component with site-service add function to site.services.ts [Refs ticket]: #4 * refactor: create datatable component and service Separate of concern for all about table (service and component) and all about sites (service and component) reviewed_by: andriacap [Refs ticket]: #3 * fix: change offset to page change offset name to page for paginated Reviewed-by: andriacap [Refs ticket]: #4 * merge: merge interface and type merge site_group.service.ts delete site.service.ts add interfaces re arrange code from branch front-site-interface Reviewed-by: andriacap [Refs ticket]: #4 * feat: details properties sites groups Create properties components to display properties of sites_groups Adding route with id according to the site group selected Reviewed-by: andriacap [Refs ticket]: #4 * feat: display groups sites's child Display group site child into table and uder properties Use routing and id params to display property of groups site Create service site Add logic to check routing and child route to display reactive component properties and table Reviewed-by: andriacap [Refs ticket]: #4 * refactor(front): rename interfaces, remove classes Better types * feat(front): get all geometries For sites and sites_groups * feat(front): WIP: geojson service to create layers And features groups since geojson component accumulated layers without cleaning them... * feat(front): implemented select capability Need to refact a lot! * feat(api): add route to get one site_group by id * fix(front): too much / * feat(front): add get sites_group from id * fix(front): add possibility to provide Geometry To setMapData * refactor(front): sites and sites_groups component To extend a base component to gather the same methods at one place To use route children and router-outlet properly To add some rxjs operators To move some common interfaces/functions * fix(front): fix filters by adding baseFilters * feat: edit sitegroups Create edit-service Create observable to update object to edit according to form component Adapt function service of monitoring object to sites and sitesgroups WIP: - adapt remaining function of monitoring object inside form-component-g - create an object which inherit of patch/post/delete method with - think about object with ObjectType of sites_group , sites, observation ? , visit ? - reload properties or update data of properties table after update data Reviewed-by:andriacap [Refs_ticket]: #4 * feat: edit sitegroups Improving route 'patch' Adding reload properties when sitegroups is updated Refactoring EditService for more readability Reviewed-by: andriacap [Ref_ticket]: #4 * feat: improve edit Add method to define objectType according to service used (site or gpsite) Rewied-by: andriacap [Refs_ticket]: #4 * feat: improve rendering front "edit" and "add" Adding global object service to set object type and variable "add" or "edit" button Reviewed-by: andriacap [Refs_ticket]: #4 * feat: improving patch method object Adding service to share request patch,get in order to to re-use the service in form component according to the type pass inside the formcomponent Adding decorator errorhandler for blueprint routes Reviewed-by:andriacap [Refs_ticket]: #4 * feat: create site group method with form Route create with form component Using location pacakage to choose between edit form or init sitegroup (see to improve that) Rewiewed-by: andriacap [Refs ticket]: #4 * feat: delete site_group component Add route back Add site_group from front form component Reviewed-by:andriac [Refs_ticket]: #4 * chore(api): removed unused code * style(config): apply formatter * chore(front): removed unused code & console.log * feat(front): removed display map button As it was ugly * refactor(front): remove Object for keys As there is an Angular pipe to get keys and values Also removed console.log Removed unused code * style(front): reformat routes * refactor(front): add create component To isolate functionnalities * chore(front): remove unused services * chore(front): removed usused code * chore(api): remove string package And fix if that cannot be reached * chore(api): removed unused comment * chore(front): removed console.log and comments * chore(api): removed unused code and log --------- Co-authored-by: Andria Capai fix: remove no existing function and component visit (#39) Hotfix because no compilable with no existing component and no existing function Reviewed-by: andriacap Feat/dynamic form/site (#42) * refactor: object.service with observable obj Change the objectType string to objectType obj in order to subscribe to multilple properties link to this object Reviewed-by: andriacap [Refs_ticket]: #40 * feat: adapt service to get config json Get fieldsName and fieldsLabel for display properties (properties component) Add logic to setItem in LocalStorage to keepLast Value of observable on reload page TODO: - [ ] see if Localstorage is really necessary with the configService used inside api-geom.service ) - [ ] adapt monitoring-form-g.component with the new service Reviewed-by: andriacap [Refs ticket]: #4 * feat: column datatable from config json Add logic into sitegroups service in order to use config json to diplay column datatable (called "display_list") Review-by: andriac [Refs ticket]: #4 * feat: adjust backend to load config json Adjust backend code to use existing code in order to load config from file json and by starting with "sites_group" Fix Media load and upload for site_group TODO: - [ ] check if config should be find from file or backend - [ ] Optimize logic backend (use generic model with class method ?) Reviewed-by: andriacap [Refs ticket]: #4 * feat: add button multiselect with filter on backend filter with params backend (new route -> see if it's possible to change that) Add button multiselect above form type site TODO: - improve Input / condition of use case of btn multiselect Reviewed-by: andriac * feat: btn multiselect option -Add @Input fn , placeholder, title, paramsToFilt -Remove empty option -prevent adding from keyboard input or not including in list -Store config json into object Reviewed-by: @andriac [Refs-ticket]: #4 * refactor: change form-service and object-service refactor form-service add observable into object-service (WIP: futur use for refresh page ?) Rieviewed-by: andriac [Refs_ticket]: #40 * refactor: test refresh page and comportment obs refresh page seems to works with localstorage of different obj in object_service Reviewed-by: andriac * feat: dynamic form - Add: - pass config json to form.component-g.ts - add config json to this.obj and refresh form into form.component-g.ts - add css for form component to deal when long form selected -fix : - refresh page form component (this._configService.init is necessary ...) - comportment different between add or edit into form component and form service Reviewed-by: andriac [Refs_ticket]: #4 * feat: dynamic form - Correction PR Remove unused console.log Rxjs : use concatMap to avoid subscribe inside subscribe Apply: prettier and sort prettier for import ts file Reviewed-by: @andriac [Refs_PR]: #42 * feat: dynamic create site - Add change current-object when click "Add " from datatable component - api-geom.service: change the way to init the api-geom - WIP: two subscribes in one .. (the subscription is call only once ..) - WIP : get config json from backend Reviewed-by: andriac * feat: add relationship id into sendData form site Add type_site and id_site_group to the form site Reviewed-by: andriac [Refs_ticket]: #5 et #6 * feat(back): add custom config when post site Change backend files in order to choose specific config when site is created. Reviewed-by: andriac [Refs_ticket]: #5 , #6 * fix: problem of type object when loading form Add endPoint and objectType to the observable using by the formservice in order to use _apiGeomService with good context (endPoint) [Refs_ticket]: #5 et #6 * fix: todo comments about refactoring code Add todo comment in order to don't forget what is to improve inside the code * chore(front): add comment and remove console.log * chore(api): put back old formatting * chore(api): remove useless comment * refactor(api): move and rename create_update fct * fix: put back inventor/digitiser & fix typo * chore: remove useless files Since columns are now loaded from json, they are not needed anymore in separate typescript files * chore: remove definition from sites_group.json * fix: column problem on sites * chore: remove useless comment * refactor(front): remove comment and use rxjs * chore(front): remove useless file Since class BtnMultiSelectChipClass is not used anymore * chore(front): remove useless comments/imports * style(front): applied prettier * chore(front): remove useless code * refactor(front): regroup types into an interface * style(front): applied prettier * refactor(front): inherit config-json from config --------- Co-authored-by: Andria Capai Feat/prepare edit site component & some visits (#43) * refactor: object.service with observable obj Change the objectType string to objectType obj in order to subscribe to multilple properties link to this object Reviewed-by: andriacap [Refs_ticket]: #40 * feat: adapt service to get config json Get fieldsName and fieldsLabel for display properties (properties component) Add logic to setItem in LocalStorage to keepLast Value of observable on reload page TODO: - [ ] see if Localstorage is really necessary with the configService used inside api-geom.service ) - [ ] adapt monitoring-form-g.component with the new service Reviewed-by: andriacap [Refs ticket]: #4 * feat: column datatable from config json Add logic into sitegroups service in order to use config json to diplay column datatable (called "display_list") Review-by: andriac [Refs ticket]: #4 * feat: adjust backend to load config json Adjust backend code to use existing code in order to load config from file json and by starting with "sites_group" Fix Media load and upload for site_group TODO: - [ ] check if config should be find from file or backend - [ ] Optimize logic backend (use generic model with class method ?) Reviewed-by: andriacap [Refs ticket]: #4 * feat: add button multiselect with filter on backend filter with params backend (new route -> see if it's possible to change that) Add button multiselect above form type site TODO: - improve Input / condition of use case of btn multiselect Reviewed-by: andriac * feat: btn multiselect option -Add @Input fn , placeholder, title, paramsToFilt -Remove empty option -prevent adding from keyboard input or not including in list -Store config json into object Reviewed-by: @andriac [Refs-ticket]: #4 * refactor: change form-service and object-service refactor form-service add observable into object-service (WIP: futur use for refresh page ?) Rieviewed-by: andriac [Refs_ticket]: #40 * refactor: test refresh page and comportment obs refresh page seems to works with localstorage of different obj in object_service Reviewed-by: andriac * feat: dynamic form - Add: - pass config json to form.component-g.ts - add config json to this.obj and refresh form into form.component-g.ts - add css for form component to deal when long form selected -fix : - refresh page form component (this._configService.init is necessary ...) - comportment different between add or edit into form component and form service Reviewed-by: andriac [Refs_ticket]: #4 * feat: dynamic form - Correction PR Remove unused console.log Rxjs : use concatMap to avoid subscribe inside subscribe Apply: prettier and sort prettier for import ts file Reviewed-by: @andriac [Refs_PR]: #42 * feat: dynamic create site - Add change current-object when click "Add " from datatable component - api-geom.service: change the way to init the api-geom - WIP: two subscribes in one .. (the subscription is call only once ..) - WIP : get config json from backend Reviewed-by: andriac * feat: add relationship id into sendData form site Add type_site and id_site_group to the form site Reviewed-by: andriac [Refs_ticket]: #5 et #6 * feat(back): add custom config when post site Change backend files in order to choose specific config when site is created. Reviewed-by: andriac [Refs_ticket]: #5 , #6 * fix: problem of type object when loading form Add endPoint and objectType to the observable using by the formservice in order to use _apiGeomService with good context (endPoint) [Refs_ticket]: #5 et #6 * fix: tooltip and label inside datatable-component Add a childLabel inside interface objObs.ts in order to use this tooltip inside datatable-component * feat: get back work visitcomponent get back work from feat/visit to use visitcomponent but adapted to current branch [Refs_ticket]: #5 , #6 * fix: forgot 3 modifications needed from feat/site Add conftest visit Add test_get_visits Add get_site_by_id * fix: passing data between components and property - Action button "consult..." is working for group_site and site - Action with button "cancel" , "come back history routing" send to the right component - Label and tooltip are according to the parent and child object loaded in the corresponding component - Action "Add site" directly from datatable-g.component (WIP: need to remove send to sites_group/:id/create [Refs_ticket]: #40 , #4 , #5 and #6 * fix: error of url relative when using form Fix problem of id_module has ["id_module"] in create_or_update_object_api function Fix path "sites/:id" to "site/:id" Add urlRelative when editing component * fix: problem route navigation Remove unused "urlRelative" create in last commit Fix the action "add children" in datatable to redirect to the good component Change the way to redirect when cancel and delete on form component based on last url [Refs_ticket]: #40 * refactor: object.service with observable obj Change the objectType string to objectType obj in order to subscribe to multilple properties link to this object Reviewed-by: andriacap [Refs_ticket]: #40 * feat: adapt service to get config json Get fieldsName and fieldsLabel for display properties (properties component) Add logic to setItem in LocalStorage to keepLast Value of observable on reload page TODO: - [ ] see if Localstorage is really necessary with the configService used inside api-geom.service ) - [ ] adapt monitoring-form-g.component with the new service Reviewed-by: andriacap [Refs ticket]: #4 * feat: column datatable from config json Add logic into sitegroups service in order to use config json to diplay column datatable (called "display_list") Review-by: andriac [Refs ticket]: #4 * feat: adjust backend to load config json Adjust backend code to use existing code in order to load config from file json and by starting with "sites_group" Fix Media load and upload for site_group TODO: - [ ] check if config should be find from file or backend - [ ] Optimize logic backend (use generic model with class method ?) Reviewed-by: andriacap [Refs ticket]: #4 * feat: add button multiselect with filter on backend filter with params backend (new route -> see if it's possible to change that) Add button multiselect above form type site TODO: - improve Input / condition of use case of btn multiselect Reviewed-by: andriac * feat: btn multiselect option -Add @Input fn , placeholder, title, paramsToFilt -Remove empty option -prevent adding from keyboard input or not including in list -Store config json into object Reviewed-by: @andriac [Refs-ticket]: #4 * refactor: change form-service and object-service refactor form-service add observable into object-service (WIP: futur use for refresh page ?) Rieviewed-by: andriac [Refs_ticket]: #40 * refactor: test refresh page and comportment obs refresh page seems to works with localstorage of different obj in object_service Reviewed-by: andriac * feat: dynamic form - Add: - pass config json to form.component-g.ts - add config json to this.obj and refresh form into form.component-g.ts - add css for form component to deal when long form selected -fix : - refresh page form component (this._configService.init is necessary ...) - comportment different between add or edit into form component and form service Reviewed-by: andriac [Refs_ticket]: #4 * feat: dynamic form - Correction PR Remove unused console.log Rxjs : use concatMap to avoid subscribe inside subscribe Apply: prettier and sort prettier for import ts file Reviewed-by: @andriac [Refs_PR]: #42 * feat: dynamic create site - Add change current-object when click "Add " from datatable component - api-geom.service: change the way to init the api-geom - WIP: two subscribes in one .. (the subscription is call only once ..) - WIP : get config json from backend Reviewed-by: andriac * feat: add relationship id into sendData form site Add type_site and id_site_group to the form site Reviewed-by: andriac [Refs_ticket]: #5 et #6 * feat(back): add custom config when post site Change backend files in order to choose specific config when site is created. Reviewed-by: andriac [Refs_ticket]: #5 , #6 * fix: tooltip and label inside datatable-component Add a childLabel inside interface objObs.ts in order to use this tooltip inside datatable-component * feat: get back work visitcomponent get back work from feat/visit to use visitcomponent but adapted to current branch [Refs_ticket]: #5 , #6 * fix: forgot 3 modifications needed from feat/site Add conftest visit Add test_get_visits Add get_site_by_id * fix: passing data between components and property - Action button "consult..." is working for group_site and site - Action with button "cancel" , "come back history routing" send to the right component - Label and tooltip are according to the parent and child object loaded in the corresponding component - Action "Add site" directly from datatable-g.component (WIP: need to remove send to sites_group/:id/create [Refs_ticket]: #40 , #4 , #5 and #6 * fix: error of url relative when using form Fix problem of id_module has ["id_module"] in create_or_update_object_api function Fix path "sites/:id" to "site/:id" Add urlRelative when editing component * fix: fix things broken by rebase * chore: remove useless file * chore(front): remove file/comment/console.log --------- Co-authored-by: Andria Capai Refactor to prepare for visits (#44) * Fix/db migrations (#31) * feat(db): upgrade down_revision following rebase Since rebase with develop: changed the down_revision number * fix(db): fix bind params enabling downgrade Beforehand the downgrade was not possible... * refactor(db): removed cor_site_type_category * refactor(db): changed category into type in cor * refactor(db): create cor_type_site * fix(db): renamed column * refactor(api): update models to fit migrations * fix(db):change bib_categorie_site to bib_type_site Adding : cor_site_module cor_site_type revision alembic to create function and trigger in order to add bib_type_site but only with nomenclature 'TYPE_SITE' upgrade and downgrade works [Refs ticket]: #3 Reviewed-by: andriac * fix(api): updated models from migrations * fix(api): wip: fix admin following migrations * fix(api): update routes and tests To match migration changes * feat: flask admin bib_type_site Change bib_categories to bib_type_site into flask admin Adding filtering in list label_fr of type_site to secure the unique constraint Reviewed-by: andriac [Refs ticket]: #3 * fix(api): updated schema to match models * fix(api): module edition * style(api): uniformize type_site * style(api): change relationship name for type_site * feat(api): validator admin * fix(api): make unique BibTypeSite in admin * test(api): fix test when existing nomenclatures In database Co-authored-by: Andria Capai * refactor: object.service with observable obj Change the objectType string to objectType obj in order to subscribe to multilple properties link to this object Reviewed-by: andriacap [Refs_ticket]: #40 * feat: adapt service to get config json Get fieldsName and fieldsLabel for display properties (properties component) Add logic to setItem in LocalStorage to keepLast Value of observable on reload page TODO: - [ ] see if Localstorage is really necessary with the configService used inside api-geom.service ) - [ ] adapt monitoring-form-g.component with the new service Reviewed-by: andriacap [Refs ticket]: #4 * refactor(front): svc: better use of types * feat(front): add Visits component and service * feat(back): add get site by id route & test * feat(api): add visits routes and schema * fix(api): join modules to have the modulecode For the frontend to be able to redirect to the correct route * feat(api): add sites/id/module route To retrieve all the modules compatibles with this site This lead to add a relationship. Set to noload so that it is not loaded by other not "raiseloaded" queries * test(api): add test to test the /sites/id/module Route. Also changed some fixture to be able to write these tests * fix(front): remove double def of IGeomService * refactor(front): remove Resp interface Since it is not usefull anymore * fix(api): exclude sites relationship * fix(config): change KeyValue Since id changed * feat(front): make datatable accepts other add btn To be able to customize the "add object" button * feat(front): add btn select protocole for visit Add new component: select-btn Add call to route to get modules from a base site id Call the new component in the visit component Add interfaces Add type in config service * feat(front): parameter for label and placeholder For the btn component. Style menu and form-field to make them larger * refactor(front): api service with generic types * fix(front): div removed following rebase * refactor(front): add service as input for formComp So that formComp is more type generic * fix(config): remove "s" from sites_group * refactor(front): put initConfig in ApiService * fix(front): remove "s" from visit * fix(front): put back condition on css class To make ng-content conditionnal * fix: following rebase Fix imports, double declarations... * fix(api): redirect to url * refactor(front): rename select-btn to option-list * fix: remove contrib --------- Co-authored-by: Andria Capai feat(front): add seeObjLabel (#45) Displayed in datatable as a link to the obj Feat/get sites sites group models (#46) * fix(api): remove monitoring_g_definitions Since it prevents the module to load... * feat(api): add join relationships For sites and sites_groups * fix(config): put back id_sites_group * refactor(api): move function to prevent circular Imports! * fix(api): load sites groups from module_id * chore: remove useless code test: routes for type site and post site (#57) * test: routes for type site and post site Reviewed-by: andriac * test: apply black and change query string - Keep site beauce it's specific one cannot re use existing - Fix mnemonique "None" with existing label_default Reviewed-by:andriac style(front): no data support & line return (#58) When no data is available: show a message When the displayed option is too long: line break fix: make breadcrumb work from sites and modules (#61) feat: delete site (#62) * feat: delete site Add code from backend to delete site (frontend is already implemented) Reviewed-by: andriac [Refs_ticket]: #5 , #6 * test: add test for delete item site Reviewed-by: andriac [Refs_ticket]: #5 , #6 Feat/edit site (#56) * feat: edit site (init value btn-select) - Change button select to accept incoming intial values - Change visit component to add btn-select and form-g inside html - WIP: update form component when initial values is up to date Reviewed-by: andriac [Refs_ticket] : #5 , #6 * feat: get information when edit site - Get all fields from specific site.json into editform - Fix problem redirection if edit object site - WIP : check how to update specific fields from object - WIP : check how to manage listOption types site when reload or when come back into component after first init Reviewed-by: andriac [Refs_ticket]: #5 , #6 * fix: forgot add apiService property binding Reviewed-by:andriac * feat: update site with good properties Change types_site object to array of ids Remove extra key "dataComplement" to assign config Reviewed-by:andriac [Refs_ticket]: #5 , #6 * feat: prevent form appear if no type-site - Hide form if btn-list type-site not selected - Add custom error message mat-error if not selected (directive + custom message) WIP: error message is showing up only if not selected after touched . Maybe need to use asyncValidator ? Reviewed-by: andriac [Refs_tickets]: #5 , #6 , #54 * feat: change order to emit event bEdit - Change order between change formService.changeData and bEditChange - Preprocess for types_site seems to be useless (see todo) Reviewed-by: andriac [Refs_ticket]: : #5 , #6 , #54 * feat: solve request changes - Apply prettier - Change object key [''] by type properties - Remove unused preprocess_data lines [Refs_ticket] : closes #54 style: formatter json in flask admin (#63) - Create jsonformatter inside utils.py - Add formatter to column "config" Reviewed-by: andriac [Refs_ticket] : #59 feat(front): try to make breadcrumb work In the site entry part feat(front): make breadcrumn logic inside component - Get all information from loading component to build breadcrumb information - Fix call twice siteService (delete the one with snake_case) - Use logic from observable and snapshot url to satisfy all case to access to route (by component, directly by url etc) Reviewed-by: andriacap feat(wip): merge chainInput and display-data-info Need dev to the two other branches Try to add logic into map-list-component Check if it is the best way to manage map Reviewed-by: andriac feat: remove unused function Subscription to initConfig useless Reviewed-by: andricap fix: put outside updateBreadCrumb in sitegp comp Merge work from branch breadcrumb branhc and resolve problem with other feature like problem initialization of sites groups (resolver) Reviewed-by: andriacap feat: remove onInit to mapListComponent Back component as it was Reviewed-by: andriacap feat(wip): manage geometry type into form Put back alert to create geometry if geometry_type precise in config (json) Reviewed-by: andricap feat(wip) change way to manage map obj component Create into formService an observable and subscribe into mapListComponent to load and connect the form with geometry info Reviewed-by: andriacap fix: rename obj Form and fix pb load confi obj - Change the name of form Object for homogeneity - Fix problem to load config of siteService in sitesComponents Reviewed-by: andriacap fix: remove unused edit site component Reviewed-by: andriacap fix: loading config object site Alternative to the "resolver" in order to load config and necessary items to display site properties and data table Reviewed-by: andriacap fix: existing little problem - description when delete object - change selectedObject on component site - remove duplicate "bEdit" - add "ngIf=!bEdit" on site component (to prevent display datatable on edition) - fix upper case letter in variable Reviewed-by: andriacap fix: problem to load specific site - change the way to load config on visit-component (=site/:id) - fix problem on mergeMap that doesn't complete (wrong observable called) Reviewed-by: andriacap feat(wip) : manage map - keep trying to send geometry to "objForm" object (FormGroup) Reviewed-by: andriacap fix: error on form with obj properties/config Reviewed-by: andriacap fix(css): height of form zone scrollable fix: problem when delete site child Fix request DELETE in backend for site and change the way to get_sites_group_by_id by using the custom query get_or_404 Reviewed-by: andriacap fix: problem adding site child by data-table comp When trying to add siteGroup's site child data no pass between the form component and the component of sites-groups . Creation of resolver to load information of siteGroup selected in order to updateBreadCrumb but also to specify the siteGroup Parent. Reviewed-by: andriac fix: problem redirection on delete site action Create new method in form component to redirect to the good url according the fact we delete site. Reviewed-by: andriac fix: untracked gitlab-ci.yml fix: load specific properties obj Specific properties were not loaded , add check specific schema to load if specific properties exist Reviewed-by: andriac feat: page not found specific to monitoring Add page not found in order to manage into monitoring module Reviewed-by: andriac feat: not found working for request status 404 Reviewed-by:andriac fix: change format scc to css not-found comp feat: display types_site label in table and prop Display types_site label into data-table component and properties-component Reviewed-by: andriac feat(api): return label of nomenclature type site style(config): types site attribut label feat(wip): display specific properties - change setControl to setValue - add html inside site.json (local) - (wip) addSpecific config to second tab for properties component feat: ignore html field when submit form - destructuration object to ignore html to submit form - ignore html field to specific config in order to match with submit form Reviewed-by: andricap feat(wip): display specific properties Display only field not "undefined" to get properties component readable feat: display specific properties Display all value from specific config even if it's null Reviewed-by: andriacap fix: remove call api for types_site Useless call api in visit-component (already get by another request) Remove '/' from start of request to avoid double '//' Reviewed-by: andriacap feat(back): json formatter on edit column Add class JSON to format json in edit column [Refs_ticket]: Closes #75 Reviewed-by: andriacap feat(back): edition site work for module enter Change the "types_site" properties by id_nomenclature (before label_fr) just before post/patch data in "preprocess_data" WIP: see if it's possible to add different "types_site" ? Currently the way is to change type_site only when you enter by site Reviewed-by: andriacap refact: remove useless call service in btn-select the service to add Extra formController to formGroup was call too many time for nothing. Reviewed-by: andriacap fix: error on tab spec properties When "selectedObj.data" is empty we do not display tab and table (adding this part of code to remove the error) Reviewed-by: andriacap feat(front): architecture form with dynamic field Change the way to place the component "btn-select" into the form-component-g. Also add another dynamic form generator in order to update separetely all specific fields and static fields Reviewed-by: andriacap feat: update form from btn-select Change the way to update the form , only dynamic fields are updated ExhaustMap is used in order to wait complete config subscribe then launch other subscribe Reviewed-by: andriacap feat: create form according to btn-select comp Change the way to update form with btn-select component when we want to create a site Reviewed-by: andriacap fix: remove unused property binding FormGroup The new way to build form doesn't need anymor propertybinding FormGroup Reviewed-by: andriacap feat: redirection on delete prevent to reload obj Before even the delete action reload the entire object whereas now we just want to redirect to parent page in case of deleted object. Reviewed-by: andriacap feat: using service to update and create dyn form Use observable and service in order to update and create specific fiedls (dynamic Form) Reviewed-by: andriacap feat(front): displaying dynamic and static form Separate static field and dynamic fieds (specific to type site) in order to manage easily fields and controls. Reviewed-by: andriacap fix: check obj initialized for FormGroup Add more check before initialize form component Reviewed-by: andriacap fix: error on build Fix errors about html, library , and argument in service cause of : new angular version and rebased branch Reviewed-by: andriacap fix: change down revision and error id_nomenclatur Because of rebase needed to change down revision of "declare_available_permission" Fix problem id_nomenclature to id_nomenclature_type_site Reviewed-by: andriacap fix: icon edition geometry problem Change html ObjForm in monitoring-map-list.ts component Reviewed-by: andriacap test: move conftest.py to root project Need to move this file because crashing the pytest collect fix: error constructor failed for this service Missing argument in constructor Reviewed-by: andriacap feat(db): alembic revision (ATTENTION) Add CRUVED into monitoring admin module TODO: to see when we will talk about permission Reviewed-by: andriacap fix: error default argument in route Before using GN 2.13.0b , it worked , now I need to remove default argument in blueprint route : site_groups/config and site/config Reviewed-by: andriacap fix(front): angular-material matchiplist **Themes**: angular-material Adapt btn-select component in order to make it again working . MatChipList and MatChip change to MatChipGrid and MatChipRow Reviewed-by: andricap fix: material angulr tab (group site and site) Refactoring du code pour pouvoir : - Afficher les groupes de site ET les sites - Adapter les tabs au style de la librairie Material-Angular Modifications des composants impactés : resolver , data-table(service et composant) , geom(interface), objObs(interface), breadcrumb (statique pour le moment) WIP/TODO: - Re adapter les composants visit et site pour qu'ils soient compatibles avcec la nouvelle structure de data-table-g - Composant "properties" à changer pour avoir le style "mterial-angular" FIX: - Pouvoir voir un site directement depuis la visualistion du premier tableau de sites (PB à résoudre avant : voir comment faire pour voir un site orphelin = sans groupe de site --> nouvelle route à créer ) - Voir les objets : rowStatus et objectStatus du composant data-table Reviewed-by: andriacap fix: angular material adapt for properties comp Changement des "tabs" et du bouton "Editer pour les adapter à la librairie angular-material utilisé dans GN Reviewed-by: andriacap fix: problem action button to add obj Après avoir adapté le style angular material et la possiblité d'avoir l'entrée par soit Site ou Groupe de site , l'ajout de l'objet du Tab Actif a été impacté . Pb résolu pour les groupes de site mais l'ajout d'un site depuis la page "acceuil" des liste de site pose un problème de fond (à voir/ discuter) Reviewed-by: andriacap fix: adapt visit, site component to datatable Changement des composants "monitoring-sites" et "monitoring-visits" pour les adapter à la nouvelle manière de gérer les données dans le composant "data-table" (dû au changements avec material-angular et la double entrée site/groupe de site) Correction de quelques bugs (exemple si un site n'a aucune visite, ça renvoyait une erreur) On ignore le "html" dans l'affichage des propriétés spécifiques Reviewed-by: andriacap fix: correction display btn to add protocol Correction au niveau de l'insertion du composant "bouton" pour l'ajout de visite qui nécessite la demande d'un protocole: - Adaptation à material-angular - Changement pour l'affichage conditionnel de ce bouton Correction de l'UX (style des mat-chip-row , pour la sélection des types de sites) Reviewed-by: andriacap fix: error when loading page with specific field Ajout d'un check de condition pour executer l'initilisation des propriétés spécifiques (type de site) liées aux sites Reviewed-by: andriacap fix: error localstorage cyclic object L'utilisation du localstorage pour le moment n'est pas utilisable (probleme d'objet cyclique JSON.stringify) Reviewed-by: andriacap Feat/show map according to tab (#79) * feat: conditional display site or sitegroup Ajout d'un Event Binding pour pouvoir charger les bonnes géométries en fonction du "tab" actif Soit pour afficher toutes les géométries de groupes de site , ou afficher toutes les géomtries de site Reviewed-by: andriacap * fix: error sho details site from map Changement de la variable baseUrl pour obtenir la bonne route pour visualiser le site sélectionné sur la carte Reviewed-by: andriacap lint(back): apply black Reviewed-by: andriacap lint(front): apply prettier Reviewed-by: andriacap feat: prevent user to delete site and gp site (#81) Prevent user to delete site and gp site when enter through protocol Reviewed-by: andriacap Feat/enter tab site features (#80) * feat: enter through site site_gp - Ajout des routes si on entre par site (tab) - Adaptation du breadcrumb si on entre par tab : site - Ajout de champ de formulaire 'id_site_group' pour pouvoir associer un site à un groupe de site - Correction des redirections des boutons associé au tab "site" (edition , ajout) Reviewed-by: andriacap * feat: types site (entrance by protocol) - Création de site à patir de l'entrée par protocole avec champ dynamique - Création de route pour avoir uniquement les types de site associé au protocole sélectionné - TODO : A l'édition d'un site (entrée par protocole) pouvoir charger les champs spécifiques lié à ce site Reviewed-by: andriacap * feat(WIP): edit site (entrance protocol) Reviewed-by: andriacap * fix: route according to tab selected Quand on entre par site , la route lié aux sites ou groupes de site sur la map ne s'actualisait pas . Reviewed-by: andriacap * fix: multiple reload observable subscription Ajout d'un "subject" pour "unsubscribe" les observables du formulaire (entrée par site) Reviewed-by: andriacap * feat: edit with pre load properties and dyn form Ajout de dev pour finaliser la possibilité de charger les data complement liés aux types de site (et donc à des fichiers de configs présent sur la table bib_types_site ) Reviewed-by: andriacap * fix: filter on colum "Type(s) de site" Reviewed-by: andriacap * style: apply black Reviewed-by: andriacap * fix: error on empty tab groupe site Problème lorsqu'un des deux tabs était vide , alors le composant tableau ne se mettait pas à jour par rapport au tab sélectionné . Reviewed-by: andriacap * fix: link to edit and create object from table Reviewed-by: andriacap * feat: prevent delete sitegp and site from protocol On empeche la suppression de site de groupe et de site via l'entrée par protocole Reviewed-by: andriacap * fix: do not load typessite by siteid if edit Modification de la condition du chargement des types de site en fonction de l'édition ou la création de site dans le composant form Reviewed-by: andriacap * fix: allow add site group if exist in config Laisser la possibilité d'ajouter des groupes de site si dans l'arbre de config du module (site_group est configuré) Reviewed-by: andriacap * feat: add visit from table Possibilité d'ajouter une visite depuis le composant data-table-g Passage d'évenement du composant option-btn-list aux composant 'grand-parent' Affichage conditionnel au click du bouton "+' dans le composant data-table Reviewed-by: andriacap * feat(front): add variable RouteBase to object Entrée par site: Ajout d'une variable 'RouteBse' pour que ce soit plus verbeux pour la redirection des routes en fonction du nombre de tab sur la page (Résoud la problématique d'une stratégie de non "reloading" au changement de route pour ne pas avoir à tout recharger lorsqu'on change de tabs sites_group/site ) Reviewed-by: andriacap feat: edit and create geom - Suppression des layers dans les ngOndestroy des composants - json.loads des geometry pour avoir l'objet geometry prêt pour les map et les parentFormControl du composant map - Ajout d'une layer de site pour le composant "visit" (layer du site parent aux visites) - Chane strategie d'écoute du formService FormMap Reviewed-by: andriacap fix: error displaying types site Error when editing the second time a site (fields types site were not displayed) Reviewed-by: andriacap style : improve ux/ui for form component Amélioration et uniformisation de l'ux entre les composant formulaires (entrée par site et par protocoles) Ajout d'une alerte en cas de suppression d'un type de site sur les champs saisies Ajout des titres à chaque page pour l'édition , la création ou le détail d'un site / groupe de site Reviewed-by: andriacap feat: observers displaying and editing form Observers is now editable in form with the component pnx-observers Observers is displayed as "nom_complet" in components "properties" and "data-table" Review-by: andriacap Feat/edit delete through table (#86) * feat(wip): edit obj from data-table Allo edition directly from data-table component (entrance by site) Delete from data table (wip) Reviewed-by: andriacap * feat(wip): delete site groups from table Use eventemitter and subscription (see if it could be better) Reviewed-by: andriacap * feat: delete object from table comp - Delete site groupe and site from table (reload route params in order to reload resolver) Reviewed-by: andriacap Feat/filter columns relationship (#87) * feat(wip) : filter on relationship Filter and sort on "observers" by "nom_complet" --> OK Filter on "last_visit" --> WIP Reviewed-by: andriacap * feat: sort and filter on id_inventor and date Filter on data table component (enter through site) need to manage relationship column. Change DateTime to Char and filter on "nom_complet" for observers Reviewed-by: andriacap style: apply black and prettier Reviewed-by: andriacap feat: use code list observers for observers site (#88) Using config gn_config and add CODE_OBSERVERS_LIST to monitoring config to get list of observers (enter through site) Reviewed-by: andriacap style(front): apply prettier Feat/conditional edit and delete entrance protocol (#89) * feat: conditional delete site - Delete icon is not showing if visit > 0 Reviewed-by: andriacap * feat: edit table object (from protocol) Edit icon in data-table component. Allow user to directly edit from a datatable component Reviewed-by: andriacap * feat: delete visit from protocol and site Change relationship for module on t_visit_complements model (fix error dependency rules blank) Change visit-component in order to allow delete when enter through site Add hide-spinner class to improve UX for delete modal (enter through site) Reviewed-by: andriacap fix: specific form and properties types site (#90) IMPORTANT : need dev from GeoNature (see demo test VM) Fix problem loading specific config (types site) entrance through protocol Fix problem loading specific fields in creation form site Reviewed-by: andriacap feat: add variable config in json (#91) Rendre la possibilité d'extraire les variables de configs depuis le front et de les remplacer par les valeurs de la config. FIX: bug de redirection à la création d'un site à partir de l'onglet "sites" Reviewed-by: andriacap feat: optional observers_txt field (#92) Add multiselect to false for field observers in site.json (only one id_inventor in DB) Add conditional `required` and `hidden` to `observers` field for `visit.json` Reviewed-by: andriacap Feat/observers txt last (#93) * feat: add observers_txt to visit.json Add field observers_txt to visit in order to allow config in submodule Reviewed-by: andriacap * style : lint json Feat/permission (#94) * feat: permission guard and on route back - Creation d'un Guard pour empêcher l'accès aux routes -Création d'un service Permission pour permettre d'obtenir les permissions liés aux objets (GNM_SITES etc) du module Monitorings et ainsi "bloquer" l'action sur les composants - Ajout des "check_cruved_scope" coté backend pour empêcher l'éxécution d'action CRUD directement depuis les routes API WIP: continuer à implémenter coté frontend l'affichage conditionnel en fonction des permissions de l'utilisateurs (affichage ou non des tabs sites, groupes de site, grisage et désactivation des icones liées aux actions CRUD, désactivation conditionnelle des boutons liés aux actions CRUD Reviewed-by: andriacap * feat: add ux (permission) icon and button - UX permission to not display sites or sites_groups if not allowed - UX permission to disable button/icon and add tooltip to warn the user that he doesn't get rights to do the action WIP: add UX to button delete in Form component Reviewed-by: andriacap * feat: add scope_filter to monitorings obj permiss Set true in revision alembic to allow admin to choose 'mon organisme' , 'mes données' Reviewed-by: andriacap * feat: add variable config in json Rendre la possibilité d'extraire les variables de configs depuis le front et de les remplacer par les valeurs de la config. FIX: bug de redirection à la création d'un site à partir de l'onglet "sites" Reviewed-by: andriacap * feat: permission with paginate Create paginate_scope in order to send object with permission for each object Create function model and function query to filter on object and check scope (based on TDataset model) Integration of permission in data-table Reviewed-by: andriacap * feat: add filter readable query to geom site Add filter_by_readable for site geom Reviewed-by: andriacap * feat(back): add object_code to BibTypeSites Specify object_code to BibTypeSite in order to make work permission on this object Reviewed-by: andriacap * feat(back): permission on different object Add permission for: - GET sites_group geom - GET sites_group - GET submodules list when we want to create visit through site entrance Create PermissonModel to use function created in GN Core Create function with parameter list object, module_code(opt), object_code (opt) in order to return CRUVED for each object of list object (need to test to see if works with any object : Modules --> ok, sites, sites_group, visits,observations to check Reviewed-by: andriacap * feat: change the way to get cruved_object fix problem from frontend to GET cruved_object with object of boolean (no more object of integer) Reviewed-by: andriacap * feat: add permission logic (entrance site) WIP : try to make working VisitQuery readable for organism Reviewed-by: andriacap * feat: query visit filtering scope Fix problem function filter_by_scope by using Models Reviewed-by: andriacap * style(backend): apply black Reviewed-by: andriacap * feat(back): add observation query and fix Fix: has/any on filter_by_scope function Add: observation query and id_digitiser to model Reviewed-by: andriacap * feat(back): R,U,D for specific object scope Prevent user to R,U,D for specific object according to his scope Reviewed-by: andriacap * fix: error on route default id not needed Reviewed-by: andriacap * feat: permission entrance through protocol Front - Remove unused 'userCruved' (front) - Set permission for object and child with 'cruved' object get from backend (disable btn and icon everywhere cruved is checked) Back: - add boolean cruved object for each objectType inside serializer - add ObservtionQuery to model TMOnitoringOBservation and hybrid_property and has_instance_permission method - FIX: * name "visite" to "visit" * change get_id by get_id_name for GenericModel * problem inside get_object_with_permission_boolean WIP: - Entrance protocol : * filter_by_readable "GET" on list object * Prevent access to edit page - Entrance site: * UX according to permission on button "Supprimer" into Form Component Reviewed-by: andriacap * feat: filter on only readable object Entrance through protocol : filter_by_scope (Readable object) Reviewed-by: andriacap * feat: prevent user to edit object if not allowed Add boolean canUpdate in form component in case object is readable but not editable and user change param edit=true in ulr directly Reviewed-by: andriacap * fix: add module_code in get_readable_list_object Reviewed-by: andriacap * feat: add check permission to route (entrance site) BACK: - Add to all route decorator permission and check has_intance_permission (entrance site) - Add 'cruved' to all object return for get_by_id (site and sites_group) FIX (BACK): - error when no child (check len childs_model) - method get_object_with_permission_boolean condition for module need to change the call method FRONT: - change canUpdate, canDelete for form and property component in order to check if specific object has permission FIX: - initForm need to wait for 'config' loaded for this.obj - same thing for form.service Reviewed-by: andriacap * feat: add guard for other frontend route Add guard for specific route Check canRead with canCreate Reviewed-by: andriacap fix: redirection , permission form, reloading (#95) - Fix reloading component on delete from databa-table - Fix set cruved condition (not module_code 'generic') - Add condition read object above create for canActivate Guard - Remove some TODO notes Reviewed-by: andriacap Feat/change model migration (#96) * feat: change db structure Add id_digitiser for t_sites_groups Drop constraint NOT NULL on t_observations.cd_nom (check if keep or not) Reviewed-by: andriacap * feat(back): add revision to add observer_txt Add observers_txt in database via alembic revision. Use alembic function to aler column Reviewed-by: andriacap * feat: add id_digitiser to t_observations Create alembic revision and add on model Reviewed-by: andriacap Reorder migration and add data migration fix: redirection and permission grp_site Precise object_code for paginate_scope with grp_site Change route redirection when create object Delete get item with marshmallow in delete request Reviewed-by: andriacap fix: paginate not found Need to rename key object send by setPage and onSortEvent Reviewed-by: andriacap style: apply lint front and back Reviewed-by: andriacap fix: change id on delete route Problem with id_g --> change to specific id for site and site_group Reviewed-by: andriacap hotfix: problem refreshing page with guard Remove guard and canActivate on route (entrance through site) In addtition of problem of refreshing route with canActivate parmas render blank page , this guard is not necessary because of other warning and security on all action (CRUD) Reviewed-by: andriacap --- backend/gn_module_monitoring/blueprint.py | 4 +- .../gn_module_monitoring/conf_schema_toml.py | 7 +- .../config/generic/config.json | 14 +- .../config/generic/module.json | 15 + .../config/generic/site.json | 75 +- .../config/generic/sites_group.json | 5 - .../config/generic/visit.json | 12 +- .../config/repositories.py | 52 +- ...3_add_observers_txt_column_t_base_visit.py | 36 + ...remove_id_module_from_sites_complements.py | 73 ++ .../a54bafb13ce8_create_cor_module_type.py | 63 ++ ...3e4c_add_id_digitiser_to_t_observations.py | 52 ++ .../b53bafb13ce8_create_bib_categorie_site.py | 60 -- .../b53bafb13ce8_create_bib_type_site.py | 76 ++ .../ce54ba49ce5c_create_cor_type_site.py | 64 ++ ...850b5ee_add_digitiser_to_t_sites_groups.py | 51 ++ ...3460441_correction_t_observation_detail.py | 2 +- ...1f54_remove_id_module_from_sites_groups.py | 73 ++ ...0d31c677f_declare_available_permissions.py | 27 +- .../modules/repositories.py | 1 + .../gn_module_monitoring/monitoring/admin.py | 80 +- .../gn_module_monitoring/monitoring/base.py | 1 + .../gn_module_monitoring/monitoring/geom.py | 6 +- .../gn_module_monitoring/monitoring/models.py | 399 +++++++++- .../monitoring/objects.py | 30 +- .../monitoring/queries.py | 146 ++++ .../monitoring/repositories.py | 21 +- .../monitoring/schemas.py | 104 +++ .../monitoring/serializer.py | 60 +- .../gn_module_monitoring/monitoring/utils.py | 10 + .../gn_module_monitoring/routes/data_utils.py | 25 +- .../gn_module_monitoring/routes/modules.py | 34 +- .../gn_module_monitoring/routes/monitoring.py | 161 +++- backend/gn_module_monitoring/routes/site.py | 257 ++++++ .../routes/sites_groups.py | 169 ++++ backend/gn_module_monitoring/routes/visit.py | 52 ++ .../gn_module_monitoring/tests/__init__.py | 0 .../fixtures/TestData/config_type_site.json | 106 +++ .../tests/fixtures/__init__.py | 0 .../tests/fixtures/module.py | 39 + .../tests/fixtures/site.py | 91 +++ .../tests/fixtures/sites_groups.py | 26 + .../tests/fixtures/type_site.py | 53 ++ .../tests/fixtures/visit.py | 26 + .../tests/test_monitoring/__init__.py | 0 .../test_monitoring/test_models/__init__.py | 0 .../test_models/test_bib_type_site.py | 23 + .../test_models/test_module.py | 19 + .../test_models/test_sites_groups.py | 36 + .../test_schemas/test_bib_site_type_schema.py | 14 + .../tests/test_routes/__init__.py | 0 .../tests/test_routes/test_site.py | 191 +++++ .../tests/test_routes/test_sites_groups.py | 66 ++ .../tests/test_routes/test_visit.py | 29 + .../tests/test_utils/__init__.py | 0 .../tests/test_utils/test_routes.py | 27 + .../utils/errors/__init__.py | 0 .../utils/errors/errorHandler.py | 19 + backend/gn_module_monitoring/utils/routes.py | 263 ++++++ conftest.py | 10 + frontend/.prettierrc | 2 +- frontend/app/class/breadCrumb.ts | 28 + .../app/class/monitoring-geom-component.ts | 30 + frontend/app/class/monitoring-object-base.ts | 26 +- frontend/app/class/monitoring-object.ts | 49 +- frontend/app/class/monitoring-visit.ts | 6 + .../breadcrumbs/breadcrumbs.component.css | 4 +- .../breadcrumbs/breadcrumbs.component.ts | 53 +- .../btn-select/btn-select.component.css | 5 + .../btn-select/btn-select.component.html | 34 + .../btn-select/btn-select.component.ts | 149 ++++ .../draw-form/draw-form.component.css | 1 - .../draw-form/draw-form.component.html | 15 +- .../draw-form/draw-form.component.spec.ts | 3 +- .../draw-form/draw-form.component.ts | 16 +- .../modal-msg/modal-msg.component.css | 6 +- .../modal-msg/modal-msg.component.html | 17 +- .../modal-msg/modal-msg.component.spec.ts | 3 +- .../components/modules/modules.component.css | 40 +- .../components/modules/modules.component.html | 92 ++- .../components/modules/modules.component.ts | 56 +- .../monitoring-datatable-g.component.css | 81 ++ .../monitoring-datatable-g.component.html | 219 +++++ .../monitoring-datatable-g.component.spec.ts | 23 + .../monitoring-datatable-g.component.ts | 429 ++++++++++ .../monitoring-datatable.component.css | 53 +- .../monitoring-datatable.component.html | 88 ++- .../monitoring-datatable.component.spec.ts | 3 +- .../monitoring-datatable.component.ts | 41 +- .../monitoring-form.component-g.css | 56 ++ .../monitoring-form.component-g.html | 157 ++++ .../monitoring-form.component-g.spec.ts | 23 + .../monitoring-form.component-g.ts | 748 ++++++++++++++++++ .../monitoring-form.component.css | 31 +- .../monitoring-form.component.html | 76 +- .../monitoring-form.component.spec.ts | 3 +- .../monitoring-form.component.ts | 386 ++++++++- .../monitoring-list.component.spec.ts | 3 +- .../monitoring-lists.component.css | 16 +- .../monitoring-lists.component.html | 40 +- .../monitoring-lists.component.ts | 11 + .../monitoring-map-list.component.css | 81 ++ .../monitoring-map-list.component.html | 23 + .../monitoring-map-list.component.ts | 35 + .../monitoring-map.component.css | 4 +- .../monitoring-map.component.html | 11 +- .../monitoring-map.component.spec.ts | 3 +- .../monitoring-object.component.css | 1 - .../monitoring-object.component.html | 12 +- .../monitoring-object.component.ts | 79 +- .../monitoring-properties-g.component.css | 37 + .../monitoring-properties-g.component.html | 234 ++++++ .../monitoring-properties-g.component.spec.ts | 23 + .../monitoring-properties-g.component.ts | 96 +++ .../monitoring-properties.component.css | 8 +- .../monitoring-properties.component.html | 107 ++- .../monitoring-properties.component.spec.ts | 3 +- .../monitoring-properties.component.ts | 13 + .../monitoring-sites-create.component.css | 9 + .../monitoring-sites-create.component.html | 20 + .../monitoring-sites-create.component.ts | 144 ++++ .../monitoring-sites.component.css | 9 + .../monitoring-sites.component.html | 43 + .../monitoring-sites.component.ts | 336 ++++++++ ...onitoring-sitesgroups-create.component.css | 9 + ...nitoring-sitesgroups-create.component.html | 10 + ...monitoring-sitesgroups-create.component.ts | 52 ++ .../monitoring-sitesgroups.component.css | 0 .../monitoring-sitesgroups.component.html | 25 + .../monitoring-sitesgroups.component.ts | 369 +++++++++ .../monitoring-visits.component.css | 9 + .../monitoring-visits.component.html | 66 ++ .../monitoring-visits.component.ts | 493 ++++++++++++ .../option-list-btn.component.css | 35 + .../option-list-btn.component.html | 51 ++ .../option-list-btn.component.ts | 62 ++ .../page-not-found.component.css | 0 .../page-not-found.component.html | 1 + .../page-not-found.component.ts | 12 + frontend/app/constants/api.ts | 1 + frontend/app/constants/guard.ts | 1 + frontend/app/enum/endpoints.ts | 5 + frontend/app/enum/objectPermission.ts | 4 + frontend/app/functions/popup.ts | 14 + frontend/app/gnModule.module.ts | 121 ++- frontend/app/interfaces/column.ts | 5 + frontend/app/interfaces/form.ts | 6 + frontend/app/interfaces/geom.ts | 66 ++ frontend/app/interfaces/module.ts | 9 + frontend/app/interfaces/objObs.ts | 34 + frontend/app/interfaces/object.ts | 38 + frontend/app/interfaces/page.ts | 17 + frontend/app/interfaces/permission.ts | 6 + frontend/app/interfaces/visit.ts | 18 + frontend/app/resolver/create-site.resolver.ts | 24 + .../app/resolver/sites-groups.resolver.ts | 108 +++ frontend/app/services/api-geom.service.ts | 322 ++++++++ frontend/app/services/cache.service.ts | 20 +- frontend/app/services/config-json.service.ts | 112 +++ frontend/app/services/config.service.ts | 134 +++- .../data-monitoring-object.service.ts | 9 +- frontend/app/services/data-table.service.ts | 88 +++ frontend/app/services/data-utils.service.ts | 6 + frontend/app/services/form.service.ts | 168 ++++ frontend/app/services/geojson.service.ts | 116 +++ .../app/services/monitoring-object.service.ts | 29 +- frontend/app/services/object.service.ts | 96 +++ frontend/app/services/permission.service.ts | 59 ++ frontend/app/types/jsondata.ts | 1 + frontend/app/types/permission.ts | 4 + .../app/utils/matErrorMessages.directive.ts | 35 + frontend/app/utils/utils.ts | 8 + monitorings_config.toml.example | 7 + 173 files changed, 10172 insertions(+), 521 deletions(-) create mode 100644 backend/gn_module_monitoring/migrations/126aca9e5503_add_observers_txt_column_t_base_visit.py create mode 100644 backend/gn_module_monitoring/migrations/6673266fb79c_remove_id_module_from_sites_complements.py create mode 100644 backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py create mode 100644 backend/gn_module_monitoring/migrations/a5dce2633e4c_add_id_digitiser_to_t_observations.py delete mode 100644 backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_categorie_site.py create mode 100644 backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py create mode 100644 backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py create mode 100644 backend/gn_module_monitoring/migrations/e2b66850b5ee_add_digitiser_to_t_sites_groups.py create mode 100644 backend/gn_module_monitoring/migrations/f24adb481f54_remove_id_module_from_sites_groups.py create mode 100644 backend/gn_module_monitoring/monitoring/queries.py create mode 100644 backend/gn_module_monitoring/monitoring/schemas.py create mode 100644 backend/gn_module_monitoring/routes/site.py create mode 100644 backend/gn_module_monitoring/routes/sites_groups.py create mode 100644 backend/gn_module_monitoring/routes/visit.py create mode 100644 backend/gn_module_monitoring/tests/__init__.py create mode 100644 backend/gn_module_monitoring/tests/fixtures/TestData/config_type_site.json create mode 100644 backend/gn_module_monitoring/tests/fixtures/__init__.py create mode 100644 backend/gn_module_monitoring/tests/fixtures/module.py create mode 100644 backend/gn_module_monitoring/tests/fixtures/site.py create mode 100644 backend/gn_module_monitoring/tests/fixtures/sites_groups.py create mode 100644 backend/gn_module_monitoring/tests/fixtures/type_site.py create mode 100644 backend/gn_module_monitoring/tests/fixtures/visit.py create mode 100644 backend/gn_module_monitoring/tests/test_monitoring/__init__.py create mode 100644 backend/gn_module_monitoring/tests/test_monitoring/test_models/__init__.py create mode 100644 backend/gn_module_monitoring/tests/test_monitoring/test_models/test_bib_type_site.py create mode 100644 backend/gn_module_monitoring/tests/test_monitoring/test_models/test_module.py create mode 100644 backend/gn_module_monitoring/tests/test_monitoring/test_models/test_sites_groups.py create mode 100644 backend/gn_module_monitoring/tests/test_monitoring/test_schemas/test_bib_site_type_schema.py create mode 100644 backend/gn_module_monitoring/tests/test_routes/__init__.py create mode 100644 backend/gn_module_monitoring/tests/test_routes/test_site.py create mode 100644 backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py create mode 100644 backend/gn_module_monitoring/tests/test_routes/test_visit.py create mode 100644 backend/gn_module_monitoring/tests/test_utils/__init__.py create mode 100644 backend/gn_module_monitoring/tests/test_utils/test_routes.py create mode 100644 backend/gn_module_monitoring/utils/errors/__init__.py create mode 100644 backend/gn_module_monitoring/utils/errors/errorHandler.py create mode 100644 backend/gn_module_monitoring/utils/routes.py create mode 100644 conftest.py create mode 100644 frontend/app/class/breadCrumb.ts create mode 100644 frontend/app/class/monitoring-geom-component.ts create mode 100644 frontend/app/class/monitoring-visit.ts create mode 100644 frontend/app/components/btn-select/btn-select.component.css create mode 100644 frontend/app/components/btn-select/btn-select.component.html create mode 100644 frontend/app/components/btn-select/btn-select.component.ts create mode 100644 frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css create mode 100644 frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html create mode 100644 frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.spec.ts create mode 100644 frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts create mode 100644 frontend/app/components/monitoring-form-g/monitoring-form.component-g.css create mode 100644 frontend/app/components/monitoring-form-g/monitoring-form.component-g.html create mode 100644 frontend/app/components/monitoring-form-g/monitoring-form.component-g.spec.ts create mode 100644 frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts create mode 100644 frontend/app/components/monitoring-map-list/monitoring-map-list.component.css create mode 100644 frontend/app/components/monitoring-map-list/monitoring-map-list.component.html create mode 100644 frontend/app/components/monitoring-map-list/monitoring-map-list.component.ts create mode 100644 frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.css create mode 100644 frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html create mode 100644 frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.spec.ts create mode 100644 frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts create mode 100644 frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.css create mode 100644 frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.html create mode 100644 frontend/app/components/monitoring-sites-create/monitoring-sites-create.component.ts create mode 100644 frontend/app/components/monitoring-sites/monitoring-sites.component.css create mode 100644 frontend/app/components/monitoring-sites/monitoring-sites.component.html create mode 100644 frontend/app/components/monitoring-sites/monitoring-sites.component.ts create mode 100644 frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.css create mode 100644 frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.html create mode 100644 frontend/app/components/monitoring-sitesgroups-create/monitoring-sitesgroups-create.component.ts create mode 100644 frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.css create mode 100644 frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.html create mode 100644 frontend/app/components/monitoring-sitesgroups/monitoring-sitesgroups.component.ts create mode 100644 frontend/app/components/monitoring-visits/monitoring-visits.component.css create mode 100644 frontend/app/components/monitoring-visits/monitoring-visits.component.html create mode 100644 frontend/app/components/monitoring-visits/monitoring-visits.component.ts create mode 100644 frontend/app/components/option-list-btn/option-list-btn.component.css create mode 100644 frontend/app/components/option-list-btn/option-list-btn.component.html create mode 100644 frontend/app/components/option-list-btn/option-list-btn.component.ts create mode 100644 frontend/app/components/page-not-found/page-not-found.component.css create mode 100644 frontend/app/components/page-not-found/page-not-found.component.html create mode 100644 frontend/app/components/page-not-found/page-not-found.component.ts create mode 100644 frontend/app/constants/api.ts create mode 100644 frontend/app/constants/guard.ts create mode 100644 frontend/app/enum/endpoints.ts create mode 100644 frontend/app/enum/objectPermission.ts create mode 100644 frontend/app/functions/popup.ts create mode 100644 frontend/app/interfaces/column.ts create mode 100644 frontend/app/interfaces/form.ts create mode 100644 frontend/app/interfaces/geom.ts create mode 100644 frontend/app/interfaces/module.ts create mode 100644 frontend/app/interfaces/objObs.ts create mode 100644 frontend/app/interfaces/object.ts create mode 100644 frontend/app/interfaces/page.ts create mode 100644 frontend/app/interfaces/permission.ts create mode 100644 frontend/app/interfaces/visit.ts create mode 100644 frontend/app/resolver/create-site.resolver.ts create mode 100644 frontend/app/resolver/sites-groups.resolver.ts create mode 100644 frontend/app/services/api-geom.service.ts create mode 100644 frontend/app/services/config-json.service.ts create mode 100644 frontend/app/services/data-table.service.ts create mode 100644 frontend/app/services/form.service.ts create mode 100644 frontend/app/services/geojson.service.ts create mode 100644 frontend/app/services/object.service.ts create mode 100644 frontend/app/services/permission.service.ts create mode 100644 frontend/app/types/jsondata.ts create mode 100644 frontend/app/types/permission.ts create mode 100644 frontend/app/utils/matErrorMessages.directive.ts diff --git a/backend/gn_module_monitoring/blueprint.py b/backend/gn_module_monitoring/blueprint.py index 267b8fbba..116e2a03f 100644 --- a/backend/gn_module_monitoring/blueprint.py +++ b/backend/gn_module_monitoring/blueprint.py @@ -7,7 +7,7 @@ from geonature.core.admin.admin import admin as flask_admin from geonature.utils.env import DB -from gn_module_monitoring.monitoring.admin import BibCategorieSiteView +from gn_module_monitoring.monitoring.admin import BibTypeSiteView from .command.cmd import commands blueprint = Blueprint( @@ -19,4 +19,4 @@ for cmd in commands: blueprint.cli.add_command(cmd) -flask_admin.add_view(BibCategorieSiteView(DB.session, name="Catégories de sites", category="Monitorings")) +flask_admin.add_view(BibTypeSiteView(DB.session, name="Types de site", category="Monitorings")) diff --git a/backend/gn_module_monitoring/conf_schema_toml.py b/backend/gn_module_monitoring/conf_schema_toml.py index 3c27b1173..230abfd48 100644 --- a/backend/gn_module_monitoring/conf_schema_toml.py +++ b/backend/gn_module_monitoring/conf_schema_toml.py @@ -4,7 +4,7 @@ Fichier à ne pas modifier. Paramètres surcouchables dans config/config_gn_module.tml """ -from marshmallow import Schema, fields, validates_schema, ValidationError +from marshmallow import Schema, fields # Permissions associés à chaque objet monitoring @@ -19,11 +19,14 @@ class GnModuleSchemaConf(Schema): + DESCRIPTION_MODULE = fields.String(default="Vous trouverez ici la liste des modules") + TITLE_MODULE = fields.String(default="Module de suivi") + CODE_OBSERVERS_LIST = fields.String(default="obsocctax") + PERMISSION_LEVEL = fields.Dict( keys=fields.Str(), values=fields.Str(), load_default=PERMISSION_LEVEL_DEFAULT ) - # AREA_TYPE = fields.List(fields.String(), missing=["COM", "M1", "M5", "M10"]) # BORNE_OBS = fields.List(fields.Integer(), missing=[1, 20, 40, 60, 80, 100, 120]) # BORNE_TAXON = fields.List(fields.Integer(), missing=[1, 5, 10, 15]) diff --git a/backend/gn_module_monitoring/config/generic/config.json b/backend/gn_module_monitoring/config/generic/config.json index f2dafb345..dc748a66e 100644 --- a/backend/gn_module_monitoring/config/generic/config.json +++ b/backend/gn_module_monitoring/config/generic/config.json @@ -1,20 +1,22 @@ { "tree": { "module": { - "site": { - "visit": { - "observation": null + "sites_group": { + "site": { + "visit": { + "observation": null + } } } } }, - "synthese" : "__MODULE.B_SYNTHESE", + "synthese": "__MODULE.B_SYNTHESE", "default_display_field_names": { "user": "nom_complet", "nomenclature": "label_fr", "dataset": "dataset_name", "observer_list": "nom_liste", - "taxonomy" : "__MODULE.TAXONOMY_DISPLAY_FIELD_NAME", + "taxonomy": "__MODULE.TAXONOMY_DISPLAY_FIELD_NAME", "taxonomy_list": "nom_liste", "sites_group": "sites_group_name", "habitat": "lb_hab_fr", @@ -22,4 +24,4 @@ "municipality": "nom_com_dept", "site": "base_site_name" } -} +} \ No newline at end of file diff --git a/backend/gn_module_monitoring/config/generic/module.json b/backend/gn_module_monitoring/config/generic/module.json index 352a5dacf..d4444e013 100644 --- a/backend/gn_module_monitoring/config/generic/module.json +++ b/backend/gn_module_monitoring/config/generic/module.json @@ -120,6 +120,21 @@ "attribut_label": "Afficher dans le menu ?", "definition": "Afficher le module dans le menu de GeoNature. (Recharger la page pour voir les modifications)." }, + + "types_site": { + "type_widget": "datalist", + "attribut_label": "Types de sites", + "type_util": "types_site", + "keyValue": "id_nomenclature_type_site", + "keyLabel": "label", + "multiple": true, + "api" : "__MONITORINGS_PATH/sites/types", + "application": "GeoNature", + "required": true, + "data_path": "items", + "definition": "Permet de paramétrer la compatibilité de ce module avec les types de sites" + }, + "medias": { "type_widget": "medias", "attribut_label": "Médias", diff --git a/backend/gn_module_monitoring/config/generic/site.json b/backend/gn_module_monitoring/config/generic/site.json index f9c7f44b0..2f956c8b1 100644 --- a/backend/gn_module_monitoring/config/generic/site.json +++ b/backend/gn_module_monitoring/config/generic/site.json @@ -11,23 +11,27 @@ "base_site_name", "base_site_code", "base_site_description", - "id_nomenclature_type_site", "id_inventor", "first_use_date", "last_visit", "nb_visits", "altitude_min", - "altitude_max" + "altitude_max", + "types_site" ], "display_list": [ "base_site_name", "base_site_code", - "id_nomenclature_type_site", "last_visit", - "nb_visits" + "id_inventor", + "nb_visits", + "types_site" ], "sorts": [ - {"prop": "last_visit", "dir": "desc"} + { + "prop": "last_visit", + "dir": "desc" + } ], "generic": { "id_base_site": { @@ -35,11 +39,6 @@ "attribut_label": "Id site", "hidden": true }, - "id_module": { - "type_widget": "text", - "attribut_label": "ID Module", - "hidden": true - }, "base_site_code": { "type_widget": "text", "attribut_label": "Code", @@ -53,32 +52,21 @@ "base_site_description": { "type_widget": "textarea", "attribut_label": "Description" - }, - "id_nomenclature_type_site": { - "type_widget": "text", - "attribut_label": "Type site", - "type_util": "nomenclature", - "value": { - "code_nomenclature_type": "TYPE_SITE" - }, - "required": true }, "id_inventor": { - "type_widget": "datalist", - "attribut_label": "Descripteur", - "api": "users/menu/__MODULE.ID_LIST_OBSERVER", - "application": "GeoNature", - "keyValue": "id_role", - "keyLabel": "nom_complet", + "type_widget": "observers", + "attribut_label": "Observateur", "type_util": "user", - "required": true - }, + "code_list": "CODE_OBSERVERS_LIST", + "required": true, + "multi_select": false + }, "id_digitiser": { "type_widget": "text", - "attribut_label": "Numérisateur", + "attribut_label": "Digitiser", + "type_util": "user", "required": true, - "hidden": true, - "type_util": "user" + "hidden": true }, "first_use_date": { "type_widget": "date", @@ -103,10 +91,29 @@ "altitude_min": { "type_widget": "integer", "attribut_label": "Altitude (min)" - }, - "altitude_max": { + }, + "altitude_max": { "type_widget": "integer", "attribut_label": "Altitude (max)" - } + }, + "types_site": { + "type_widget": "datalist", + "attribut_label": "Type(s) de site", + "type_util": "types_site", + "keyValue": "id_nomenclature_type_site", + "keyLabel": "label", + "multiple": true, + "api": "__MONITORINGS_PATH/modules/__MODULE.MODULE_CODE/types_sites", + "application": "GeoNature", + "required": true, + "nullDefault": true, + "definition": "Permet de n'avoir que les types de site lié au module" + }, + "id_sites_group": { + "type_widget": "integer", + "attribut_label": "ID Sites Groups", + "hidden": true, + "schema_dot_table": "gn_monitoring.t_base_sites" + } } -} +} \ No newline at end of file diff --git a/backend/gn_module_monitoring/config/generic/sites_group.json b/backend/gn_module_monitoring/config/generic/sites_group.json index 68bdf820d..c1efb7c09 100644 --- a/backend/gn_module_monitoring/config/generic/sites_group.json +++ b/backend/gn_module_monitoring/config/generic/sites_group.json @@ -22,11 +22,6 @@ "attribut_label": "Id site", "hidden": true }, - "id_module": { - "type_widget": "text", - "attribut_label": "ID Module", - "hidden": true - }, "sites_group_name": { "type_widget": "text", "attribut_label": "Nom", diff --git a/backend/gn_module_monitoring/config/generic/visit.json b/backend/gn_module_monitoring/config/generic/visit.json index 68afdd982..f57c7ce14 100644 --- a/backend/gn_module_monitoring/config/generic/visit.json +++ b/backend/gn_module_monitoring/config/generic/visit.json @@ -22,9 +22,7 @@ "nb_observations" ], - "sorts": [ - {"prop": "visit_date_min", "dir": "desc"} - ], + "sorts": [{ "prop": "visit_date_min", "dir": "desc" }], "generic": { "id_base_visit": { "type_widget": "text", @@ -50,9 +48,15 @@ "keyLabel": "nom_complet", "type_util": "user", "multiple": true, + "hidden": false, "required": true }, - + "observers_txt": { + "type_widget": "observers-text", + "attribut_label": "Observateur(s) (extérieur)", + "required": false, + "hidden": true + }, "id_digitiser": { "type_widget": "text", "attribut_label": "Digitiser", diff --git a/backend/gn_module_monitoring/config/repositories.py b/backend/gn_module_monitoring/config/repositories.py index f196ebede..f54fc2a92 100644 --- a/backend/gn_module_monitoring/config/repositories.py +++ b/backend/gn_module_monitoring/config/repositories.py @@ -25,7 +25,7 @@ config_cache_name = "MONITORINGS_CONFIG" -def get_config_objects(module_code, config, tree=None, parent_type=None): +def get_config_objects(module_code, config, tree=None, parent_type=None, customSpecConfig=None): """ recupere la config de chaque object present dans tree pour le module """ @@ -33,10 +33,18 @@ def get_config_objects(module_code, config, tree=None, parent_type=None): # initial tree tree = config["tree"] + if "module" in config["tree"]: + is_sites_group_child = "sites_group" in list(dict.fromkeys(config["tree"]["module"])) + for object_type in tree: # config object if not object_type in config: - config[object_type] = config_object_from_files(module_code, object_type) + if object_type == "site": + config[object_type] = config_object_from_files( + module_code, object_type, customSpecConfig, is_sites_group_child + ) + else: + config[object_type] = config_object_from_files(module_code, object_type) # tree children_types = tree[object_type] and list(tree[object_type].keys()) or [] @@ -77,25 +85,45 @@ def get_config_objects(module_code, config, tree=None, parent_type=None): # recursif if tree[object_type]: - get_config_objects(module_code, config, tree[object_type], object_type) + get_config_objects( + module_code, config, tree[object_type], object_type, customSpecConfig + ) -def config_object_from_files(module_code, object_type): +def config_object_from_files(module_code, object_type, custom=None, is_sites_group_child=False): """ recupere la configuration d'un object de type pour le module """ generic_config_object = json_config_from_file("generic", object_type) specific_config_object = ( - {} if module_code == "generic" else json_config_from_file(module_code, object_type) + {"specific": {}} + if module_code == "generic" + else json_config_from_file(module_code, object_type) ) + # NOTE: Ici on pop la clé "id_sites_group" dans le cas ou l'entre par protocole car l'association de site à un groupe de site doit se faire par l'entrée par site + if module_code != "generic" and object_type == "site" and not is_sites_group_child: + generic_config_object["generic"].pop("id_sites_group") + + if module_code == "generic" and object_type == "site": + generic_config_object["generic"]["types_site"] = { + "type_widget": "datalist", + "attribut_label": "Type(s) de site", + } + + if object_type == "site" and custom is not None: + if "specific" in custom and "specific" in specific_config_object: + for key in custom["specific"]: + if key not in specific_config_object["specific"]: + specific_config_object["specific"][key] = custom["specific"][key] + config_object = generic_config_object config_object.update(specific_config_object) return config_object -def get_config(module_code=None, force=False): +def get_config(module_code=None, force=False, customSpecConfig=None): """ recupere la configuration pour le module monitoring @@ -134,7 +162,8 @@ def get_config(module_code=None, force=False): # return config config = config_from_files("config", module_code) - get_config_objects(module_code, config) + get_config_objects(module_code, config, customSpecConfig=customSpecConfig) + # customize config if module: custom = {} @@ -242,3 +271,12 @@ def config_schema(module_code, object_type, type_schema="all"): def get_config_frontend(module_code=None, force=True): config = dict(get_config(module_code, force)) return config + + +# def get_config_from_backend(module_code=None, force=False): + +# module_code = 'generic' +# #TODO: voir la sortie de cette fonction +# config = config_from_backend('config', module_code) +# #TODO: voir également à quoi sert cette fonction +# get_config_objects(module_code, config) diff --git a/backend/gn_module_monitoring/migrations/126aca9e5503_add_observers_txt_column_t_base_visit.py b/backend/gn_module_monitoring/migrations/126aca9e5503_add_observers_txt_column_t_base_visit.py new file mode 100644 index 000000000..680db0d19 --- /dev/null +++ b/backend/gn_module_monitoring/migrations/126aca9e5503_add_observers_txt_column_t_base_visit.py @@ -0,0 +1,36 @@ +"""add_observers_txt_column_t_base_visit + +Revision ID: 126aca9e5503 +Revises: a5dce2633e4c +Create Date: 2023-09-12 11:49:24.535949 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "126aca9e5503" +down_revision = "a5dce2633e4c" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" +table = "t_base_visits" +column = "observers_txt" + + +def upgrade(): + op.add_column( + table, + sa.Column( + column, + sa.Text(), + nullable=True, + ), + schema=monitorings_schema, + ) + + +def downgrade(): + op.drop_column(table, column, schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/migrations/6673266fb79c_remove_id_module_from_sites_complements.py b/backend/gn_module_monitoring/migrations/6673266fb79c_remove_id_module_from_sites_complements.py new file mode 100644 index 000000000..535789c4c --- /dev/null +++ b/backend/gn_module_monitoring/migrations/6673266fb79c_remove_id_module_from_sites_complements.py @@ -0,0 +1,73 @@ +"""remove_id_module_from_sites_complements + +Revision ID: 6673266fb79c +Revises: a54bafb13ce8 +Create Date: 2022-12-13 16:00:00.512562 + +""" +import sqlalchemy as sa +from alembic import op + +from gn_module_monitoring import MODULE_CODE + +# revision identifiers, used by Alembic. +revision = "6673266fb79c" +down_revision = "a54bafb13ce8" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" + + +def upgrade(): + # Transfert data to core_site_module table + statement = sa.text( + f""" + INSERT INTO {monitorings_schema}.cor_site_module (id_module, id_base_site) + SELECT tsc.id_module, tsc.id_base_site + FROM {monitorings_schema}.t_site_complements AS tsc + LEFT JOIN {monitorings_schema}.cor_site_module AS csm + ON tsc.id_base_site = csm.id_base_site + WHERE csm.id_base_site IS NULL; + """ + ) + op.execute(statement) + + # Drop column id_module + op.drop_column("t_site_complements", "id_module", schema=monitorings_schema) + + +def downgrade(): + op.add_column( + "t_site_complements", + sa.Column( + "id_module", + sa.Integer(), + sa.ForeignKey( + f"gn_commons.t_modules.id_module", + name="fk_t_site_complements_id_module", + ondelete="CASCADE", + onupdate="CASCADE", + ), + nullable=True, + ), + schema=monitorings_schema, + ) + # Cannot use orm here because need the model to be "downgraded" as well + # Need to set nullable True above for existing rows + # LIMITATION: Assume that current use is one site associated to one module + statement = sa.text( + f""" + WITH sm AS ( + SELECT min(id_module) AS first_id_module, id_base_site + FROM {monitorings_schema}.cor_site_module AS csm + GROUP BY id_base_site + ) + UPDATE {monitorings_schema}.t_site_complements sc + SET id_module = sm.first_id_module + FROM sm + WHERE sm.id_base_site = sc.id_base_site; + """ + ) + op.execute(statement) + op.alter_column("t_site_complements", "id_module", nullable=False, schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py b/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py new file mode 100644 index 000000000..ad8e500fa --- /dev/null +++ b/backend/gn_module_monitoring/migrations/a54bafb13ce8_create_cor_module_type.py @@ -0,0 +1,63 @@ +"""create_cor_module_type + +Revision ID: a54bafb13ce8 +Revises: ce54ba49ce5c +Create Date: 2022-12-06 16:18:24.512562 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "a54bafb13ce8" +down_revision = "ce54ba49ce5c" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" +referent_schema = "gn_commons" + + +def upgrade(): + op.create_table( + "cor_module_type", + sa.Column( + "id_type_site", + sa.Integer(), + sa.ForeignKey( + f"{monitorings_schema}.bib_type_site.id_nomenclature_type_site", + name="fk_cor_module_type_id_nomenclature", + ondelete="CASCADE", + onupdate="CASCADE", + ), + nullable=False, + ), + sa.Column( + "id_module", + sa.Integer(), + sa.ForeignKey( + f"{referent_schema}.t_modules.id_module", + name="fk_cor_module_type_id_module", + ondelete="CASCADE", + onupdate="CASCADE", + ), + nullable=False, + ), + sa.PrimaryKeyConstraint("id_type_site", "id_module", name="pk_cor_module_type"), + schema=monitorings_schema, + ) + + # Insertion des données a partir de cor_site_module + op.execute( + """ + INSERT INTO gn_monitoring.cor_module_type (id_module, id_type_site ) + SELECT DISTINCT csm.id_module, cts.id_type_site + FROM gn_monitoring.cor_site_module AS csm + JOIN gn_monitoring.cor_type_site AS cts + ON Cts.id_base_site = csm.id_base_site ; + """ + ) + + +def downgrade(): + op.drop_table("cor_module_type", schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/migrations/a5dce2633e4c_add_id_digitiser_to_t_observations.py b/backend/gn_module_monitoring/migrations/a5dce2633e4c_add_id_digitiser_to_t_observations.py new file mode 100644 index 000000000..7a828fe0d --- /dev/null +++ b/backend/gn_module_monitoring/migrations/a5dce2633e4c_add_id_digitiser_to_t_observations.py @@ -0,0 +1,52 @@ +"""add_id_digitiser_to_t_observations + +Revision ID: a5dce2633e4c +Revises: e2b66850b5ee +Create Date: 2023-09-15 16:44:29.133863 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "a5dce2633e4c" +down_revision = "e2b66850b5ee" +branch_labels = None +depends_on = None + + +monitorings_schema = "gn_monitoring" +table = "t_observations" +column = "id_digitiser" + +foreign_schema = "utilisateurs" +table_foreign = "t_roles" +foreign_key = "id_role" + + +def upgrade(): + op.add_column( + table, + sa.Column( + column, + sa.Integer(), + sa.ForeignKey( + f"{foreign_schema}.{table_foreign}.{foreign_key}", + name=f"fk_{table}_{column}", + onupdate="CASCADE", + ), + nullable=False, + ), + schema=monitorings_schema, + ) + + +def downgrade(): + statement = sa.text( + f""" + ALTER TABLE {monitorings_schema}.{table} DROP CONSTRAINT fk_t_sites_groups_id_digitiser; + """ + ) + op.execute(statement) + op.drop_column(table, column, schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_categorie_site.py b/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_categorie_site.py deleted file mode 100644 index 610269dd5..000000000 --- a/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_categorie_site.py +++ /dev/null @@ -1,60 +0,0 @@ -"""create_bib_categorie_site - -Revision ID: b53bafb13ce8 -Revises: -Create Date: 2022-12-06 16:18:24.512562 - -""" -from alembic import op -import sqlalchemy as sa - -# revision identifiers, used by Alembic. -revision = "b53bafb13ce8" -down_revision = "362cf9d504ec" -branch_labels = None -depends_on = None - -monitorings_schema = "gn_monitoring" - - -def upgrade(): - op.create_table( - "bib_categorie_site", - sa.Column("id_categorie", sa.Integer(), nullable=False), - sa.Column("label", sa.String(), nullable=False), - sa.Column("config", sa.JSON(), nullable=True), - sa.PrimaryKeyConstraint("id_categorie"), - schema=monitorings_schema, - ) - op.create_index( - op.f("ix_bib_categorie_site_id"), - "bib_categorie_site", - ["id_categorie"], - unique=False, - schema=monitorings_schema, - ) - op.add_column( - "t_base_sites", - sa.Column( - "id_categorie", - sa.Integer(), - sa.ForeignKey( - f"{monitorings_schema}.bib_categorie_site.id_categorie", - name="fk_t_base_sites_id_categorie", - ondelete="CASCADE", - ), - nullable=True, # TODO: see migration? nullable is conservative here - ), - schema=monitorings_schema, - ) - - -def downgrade(): - op.drop_constraint("fk_t_base_sites_id_categorie", "t_base_sites", schema=monitorings_schema) - op.drop_column("t_base_sites", "id_categorie", schema=monitorings_schema) - op.drop_index( - op.f("ix_bib_categorie_site_id"), - table_name="bib_categorie_site", - schema=monitorings_schema, - ) - op.drop_table("bib_categorie_site", schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py b/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py new file mode 100644 index 000000000..8178524dc --- /dev/null +++ b/backend/gn_module_monitoring/migrations/b53bafb13ce8_create_bib_type_site.py @@ -0,0 +1,76 @@ +"""create_bib_type_site + +Revision ID: b53bafb13ce8 +Revises: e78003460441 +Create Date: 2022-12-06 16:18:24.512562 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "b53bafb13ce8" +down_revision = "126aca9e5503" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" +nomenclature_schema = "ref_nomenclatures" + + +def upgrade(): + op.create_table( + "bib_type_site", + sa.Column( + "id_nomenclature_type_site", + sa.Integer(), + sa.ForeignKey( + f"{nomenclature_schema}.t_nomenclatures.id_nomenclature", + name="fk_t_nomenclatures_id_nomenclature_type_site", + ), + nullable=False, + unique=True, + ), + sa.PrimaryKeyConstraint("id_nomenclature_type_site"), + sa.Column("config", sa.JSON(), nullable=True), + schema=monitorings_schema, + ) + + # FIXME: if sqlalchemy >= 1.4.32, it should work with postgresql_not_valid=True: cleaner + # op.create_check_constraint( + # "ck_bib_type_site_id_nomenclature_type_site", + # "bib_type_site", + # f"{nomenclature_schema}.check_nomenclature_type_by_mnemonique(id_nomenclature_type_site,'TYPE_SITE')", + # schema=monitorings_schema, + # postgresql_not_valid=True + # ) + statement = sa.text( + f""" + ALTER TABLE {monitorings_schema}.bib_type_site + ADD + CONSTRAINT ck_bib_type_site_id_nomenclature_type_site CHECK ( + {nomenclature_schema}.check_nomenclature_type_by_mnemonique( + id_nomenclature_type_site, 'TYPE_SITE' :: character varying + ) + ) NOT VALID + """ + ) + op.execute(statement) + op.create_table_comment( + "bib_type_site", + "Table de définition des champs associés aux types de sites", + schema=monitorings_schema, + ) + + # Récupération de la liste des types de site avec ceux déja présents dans la table t_base_site + op.execute( + """ + INSERT INTO gn_monitoring.bib_type_site AS bts (id_nomenclature_type_site) + SELECT DISTINCT id_nomenclature_type_site + FROM gn_monitoring.t_base_sites AS tbs ; + """ + ) + + +def downgrade(): + op.drop_table("bib_type_site", schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py b/backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py new file mode 100644 index 000000000..ea6d9bae6 --- /dev/null +++ b/backend/gn_module_monitoring/migrations/ce54ba49ce5c_create_cor_type_site.py @@ -0,0 +1,64 @@ +"""create_cor_type_site + +Revision ID: ce54ba49ce5c +Revises: b53bafb13ce8 +Create Date: 2022-12-06 16:18:24.512562 + +""" +from alembic import op +import sqlalchemy as sa + +# revision identifiers, used by Alembic. +revision = "ce54ba49ce5c" +down_revision = "b53bafb13ce8" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" + + +def upgrade(): + op.create_table( + "cor_type_site", + sa.Column( + "id_type_site", + sa.Integer(), + sa.ForeignKey( + f"{monitorings_schema}.bib_type_site.id_nomenclature_type_site", + name="fk_cor_type_site_id_nomenclature_type_site", + ondelete="CASCADE", + onupdate="CASCADE", + ), + nullable=False, + ), + sa.Column( + "id_base_site", + sa.Integer(), + sa.ForeignKey( + f"{monitorings_schema}.t_base_sites.id_base_site", + name="fk_cor_type_site_id_base_site", + ondelete="CASCADE", + onupdate="CASCADE", + ), + nullable=False, + ), + sa.PrimaryKeyConstraint("id_type_site", "id_base_site", name="pk_cor_type_site"), + schema=monitorings_schema, + ) + op.create_table_comment( + "cor_type_site", + "Table d'association entre les sites et les types de sites", + schema=monitorings_schema, + ) + + op.execute( + """ + INSERT INTO gn_monitoring.cor_type_site + SELECT id_nomenclature_type_site , id_base_site d + FROM gn_monitoring.t_base_sites ; + """ + ) + + +def downgrade(): + op.drop_table("cor_type_site", schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/migrations/e2b66850b5ee_add_digitiser_to_t_sites_groups.py b/backend/gn_module_monitoring/migrations/e2b66850b5ee_add_digitiser_to_t_sites_groups.py new file mode 100644 index 000000000..2b748a8ba --- /dev/null +++ b/backend/gn_module_monitoring/migrations/e2b66850b5ee_add_digitiser_to_t_sites_groups.py @@ -0,0 +1,51 @@ +"""Add digitiser to t_sites_groups + +Revision ID: e2b66850b5ee +Revises: e78003460441 +Create Date: 2023-09-11 12:17:17.280948 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "e2b66850b5ee" +down_revision = "e78003460441" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" +table = "t_sites_groups" +column = "id_digitiser" + +foreign_schema = "utilisateurs" +table_foreign = "t_roles" +foreign_key = "id_role" + + +def upgrade(): + op.add_column( + table, + sa.Column( + column, + sa.Integer(), + sa.ForeignKey( + f"{foreign_schema}.{table_foreign}.{foreign_key}", + name=f"fk_{table}_{column}", + onupdate="CASCADE", + ), + nullable=False, + ), + schema=monitorings_schema, + ) + + +def downgrade(): + statement = sa.text( + f""" + ALTER TABLE {monitorings_schema}.{table} DROP CONSTRAINT fk_{table}_{column}; + """ + ) + op.execute(statement) + op.drop_column(table, column, schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/migrations/e78003460441_correction_t_observation_detail.py b/backend/gn_module_monitoring/migrations/e78003460441_correction_t_observation_detail.py index a111c88f3..cfe362fde 100644 --- a/backend/gn_module_monitoring/migrations/e78003460441_correction_t_observation_detail.py +++ b/backend/gn_module_monitoring/migrations/e78003460441_correction_t_observation_detail.py @@ -8,7 +8,7 @@ from alembic import op import sqlalchemy as sa - +# TODO: voir les discussions sur element patrinat pour voir si on a statué # revision identifiers, used by Alembic. revision = "e78003460441" down_revision = "2003e18f248a" diff --git a/backend/gn_module_monitoring/migrations/f24adb481f54_remove_id_module_from_sites_groups.py b/backend/gn_module_monitoring/migrations/f24adb481f54_remove_id_module_from_sites_groups.py new file mode 100644 index 000000000..bf5839dd4 --- /dev/null +++ b/backend/gn_module_monitoring/migrations/f24adb481f54_remove_id_module_from_sites_groups.py @@ -0,0 +1,73 @@ +"""remove_id_module_from_sites_groups + +Revision ID: f24adb481f54 +Revises: 6673266fb79c +Create Date: 2022-12-13 16:00:00.512562 + +""" +import sqlalchemy as sa +from alembic import op + +from gn_module_monitoring import MODULE_CODE + +# revision identifiers, used by Alembic. +revision = "f24adb481f54" +down_revision = "6673266fb79c" +branch_labels = None +depends_on = None + +monitorings_schema = "gn_monitoring" + + +def upgrade(): + op.drop_column("t_sites_groups", "id_module", schema=monitorings_schema) + + +def downgrade(): + op.add_column( + "t_sites_groups", + sa.Column( + "id_module", + sa.Integer(), + sa.ForeignKey( + f"gn_commons.t_modules.id_module", + name="fk_t_sites_groups_id_module", + ondelete="CASCADE", + onupdate="CASCADE", + ), + nullable=True, + ), + schema=monitorings_schema, + ) + # Cannot use orm here because need the model to be "downgraded" as well + # Need to set nullable True above for existing rows + # Get data from core_site_module + # LIMITATION: Assume that current use is one site associated to one module associated to one site_group + statement = sa.text( + f""" + WITH sgm AS ( + SELECT id_sites_group , csm.id_module + FROM gn_monitoring.t_site_complements AS tsc + JOIN gn_monitoring.cor_site_module AS csm + ON tsc.id_base_site = csm.id_base_site + WHERE NOT id_sites_group IS NULL + ) + UPDATE gn_monitoring.t_sites_groups AS tsg + SET id_module = sgm.id_module + FROM sgm + WHERE tsg.id_sites_group = sgm.id_sites_group; + """ + ) + op.execute(statement) + + statement = sa.text( + f""" + UPDATE {monitorings_schema}.t_sites_groups + SET id_module = (select id_module + from gn_commons.t_modules tm + where module_code = :module_code) + WHERE id_module IS NULL; + """ + ).bindparams(module_code=MODULE_CODE) + op.execute(statement) + op.alter_column("t_sites_groups", "id_module", nullable=False, schema=monitorings_schema) diff --git a/backend/gn_module_monitoring/migrations/fc90d31c677f_declare_available_permissions.py b/backend/gn_module_monitoring/migrations/fc90d31c677f_declare_available_permissions.py index eba68daea..05dfe34a1 100644 --- a/backend/gn_module_monitoring/migrations/fc90d31c677f_declare_available_permissions.py +++ b/backend/gn_module_monitoring/migrations/fc90d31c677f_declare_available_permissions.py @@ -11,12 +11,23 @@ # revision identifiers, used by Alembic. revision = "fc90d31c677f" -down_revision = "e78003460441" +down_revision = "f24adb481f54" branch_labels = None depends_on = ("f051b88a57fd",) def upgrade(): + op.execute( + """ + INSERT INTO + gn_permissions.t_objects( + code_object, + description_object + ) + VALUES + ('TYPES_SITES','Types de sites à associer aux protocoles du module MONITORINGS') + """ + ) op.execute( """ INSERT INTO @@ -36,7 +47,19 @@ def upgrade(): FROM ( VALUES - ('MONITORINGS', 'ALL', 'R', False, 'Accéder au module') + ('MONITORINGS', 'ALL', 'R', False, 'Accéder au module'), + ('MONITORINGS', 'TYPES_SITES', 'R', False, 'Accéder aux types de site'), + ('MONITORINGS', 'TYPES_SITES', 'C', False, 'Créer des types de site'), + ('MONITORINGS', 'TYPES_SITES', 'U', False, 'Modifier des types de site'), + ('MONITORINGS', 'TYPES_SITES', 'D', False, 'Supprimer des types de site'), + ('MONITORINGS', 'GNM_SITES', 'R', True, 'Accéder aux sites'), + ('MONITORINGS', 'GNM_SITES', 'C', True, 'Créer des sites'), + ('MONITORINGS', 'GNM_SITES', 'U', True, 'Modifier des sites'), + ('MONITORINGS', 'GNM_SITES', 'D', True, 'Supprimer des sites'), + ('MONITORINGS', 'GNM_GRP_SITES', 'R', True, 'Accéder aux groupes de sites'), + ('MONITORINGS', 'GNM_GRP_SITES', 'C', True, 'Créer des groupes de sites'), + ('MONITORINGS', 'GNM_GRP_SITES', 'U', True, 'Modifier des groupes de sites'), + ('MONITORINGS', 'GNM_GRP_SITES', 'D', True, 'Supprimer des groupes de sites') ) AS v (module_code, object_code, action_code, scope_filter, label) JOIN gn_commons.t_modules m ON m.module_code = v.module_code diff --git a/backend/gn_module_monitoring/modules/repositories.py b/backend/gn_module_monitoring/modules/repositories.py index e93052fb5..98264b29f 100644 --- a/backend/gn_module_monitoring/modules/repositories.py +++ b/backend/gn_module_monitoring/modules/repositories.py @@ -4,6 +4,7 @@ get_modules """ +from sqlalchemy.orm import Load from sqlalchemy.orm.exc import MultipleResultsFound, NoResultFound from geonature.utils.env import DB diff --git a/backend/gn_module_monitoring/monitoring/admin.py b/backend/gn_module_monitoring/monitoring/admin.py index b71e81f9e..d7931dd53 100644 --- a/backend/gn_module_monitoring/monitoring/admin.py +++ b/backend/gn_module_monitoring/monitoring/admin.py @@ -1,18 +1,88 @@ +from flask import json from flask_admin.contrib.sqla import ModelView +from flask_admin.form import fields from geonature.core.admin.admin import CruvedProtectedMixin +from geonature.utils.env import DB +from pypnnomenclature.models import BibNomenclaturesTypes, TNomenclatures +from wtforms.validators import ValidationError +from gn_module_monitoring.monitoring.models import BibTypeSite +from gn_module_monitoring.monitoring.utils import json_formatter -from gn_module_monitoring.monitoring.models import BibCategorieSite +SITE_TYPE = "TYPE_SITE" -class BibCategorieSiteView(CruvedProtectedMixin, ModelView): +class Unique: + """validator that checks field uniqueness""" + + def __init__(self, model, field, compare_field, message=None): + self.model = model + self.field = field + self.compare_field = compare_field + if not message: + message = "A type is already created with this nomenclature" + self.message = message + + def __call__(self, form, field): + if field.object_data == field.data: + return + if self.model.query.filter( + getattr(self.model, self.field) == getattr(field.data, self.compare_field) + ).first(): + raise ValidationError(self.message) + + +class JSONField(fields.JSONField): + def _value(self): + if self.raw_data: + return self.raw_data[0] + elif self.data: + return json.dumps(self.data, ensure_ascii=False, indent=2) + else: + return "" + + +class BibTypeSiteView(CruvedProtectedMixin, ModelView): """ - Surcharge de l'administration des catégories de sites + Surcharge de l'administration des types de sites """ module_code = "MONITORINGS" - object_code = None + object_code = "TYPES_SITES" def __init__(self, session, **kwargs): # Référence au model utilisé - super(BibCategorieSiteView, self).__init__(BibCategorieSite, session, **kwargs) + super(BibTypeSiteView, self).__init__(BibTypeSite, session, **kwargs) + + def get_only_nomenclature_asc(): + return ( + DB.session.query(TNomenclatures) + .join(TNomenclatures.nomenclature_type) + .filter(BibNomenclaturesTypes.mnemonique == SITE_TYPE) + .order_by(TNomenclatures.label_fr.asc()) + ) + + def get_label_fr_nomenclature(x): + return x.label_fr + + def list_label_nomenclature_formatter(view, _context, model, _name): + return model.nomenclature.label_fr + + # Nom de colonne user friendly + column_labels = dict(nomenclature="Types de site") + # Description des colonnes + column_descriptions = dict(nomenclature="Nomenclature de type de site à choisir") + + column_hide_backrefs = False + + form_args = dict( + nomenclature=dict( + query_factory=get_only_nomenclature_asc, + get_label=get_label_fr_nomenclature, + validators=[Unique(BibTypeSite, "id_nomenclature_type_site", "id_nomenclature")], + ) + ) + form_overrides = {"config": JSONField} + column_list = ("nomenclature", "config") + column_formatters = dict(nomenclature=list_label_nomenclature_formatter, config=json_formatter) + form_excluded_columns = "sites" diff --git a/backend/gn_module_monitoring/monitoring/base.py b/backend/gn_module_monitoring/monitoring/base.py index ab331e005..0c80f4cf1 100644 --- a/backend/gn_module_monitoring/monitoring/base.py +++ b/backend/gn_module_monitoring/monitoring/base.py @@ -64,6 +64,7 @@ class MonitoringObjectBase: _model = None _children = {} _parent = None + cruved = {} def __init__(self, module_code, object_type, id=None, model=None): self._module_code = module_code diff --git a/backend/gn_module_monitoring/monitoring/geom.py b/backend/gn_module_monitoring/monitoring/geom.py index b520c1226..fdd790405 100644 --- a/backend/gn_module_monitoring/monitoring/geom.py +++ b/backend/gn_module_monitoring/monitoring/geom.py @@ -31,5 +31,9 @@ def serialize(self, depth): geometry = None monitoring_object_dict["geometry"] = geometry - + if len(monitoring_object_dict["properties"].get("types_site", [])) != 0: + if hasattr(self._model, "types_site"): + # TODO: performance? + types_site = [typ.nomenclature.label_fr for typ in self._model.types_site] + monitoring_object_dict["properties"]["types_site"] = types_site return monitoring_object_dict diff --git a/backend/gn_module_monitoring/monitoring/models.py b/backend/gn_module_monitoring/monitoring/models.py index 916799a56..880155de1 100644 --- a/backend/gn_module_monitoring/monitoring/models.py +++ b/backend/gn_module_monitoring/monitoring/models.py @@ -1,8 +1,23 @@ """ Modèles SQLAlchemy pour les modules de suivi """ -from sqlalchemy import select, func, and_ -from sqlalchemy.orm import column_property +from flask import g +from sqlalchemy import join, select, func, and_, or_, false +from sqlalchemy.inspection import inspect +from sqlalchemy.orm import ( + column_property, + ColumnProperty, + RelationshipProperty, + class_mapper, + aliased, +) +from sqlalchemy.orm import ( + column_property, + ColumnProperty, + RelationshipProperty, + class_mapper, + aliased, +) from sqlalchemy.dialects.postgresql import JSONB, UUID from uuid import uuid4 @@ -10,8 +25,9 @@ from utils_flask_sqla_geo.serializers import geoserializable from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.ext.declarative import declared_attr - +from pypnnomenclature.models import TNomenclatures, BibNomenclaturesTypes from geonature.core.gn_commons.models import TMedias from geonature.core.gn_monitoring.models import TBaseSites, TBaseVisits from geonature.core.gn_meta.models import TDatasets @@ -19,6 +35,147 @@ from geonature.core.gn_commons.models import TModules, cor_module_dataset from pypnusershub.db.models import User from geonature.core.gn_monitoring.models import corVisitObserver +from gn_module_monitoring.monitoring.queries import ( + Query as MonitoringQuery, + SitesQuery, + SitesGroupsQuery, + VisitQuery, + ObservationsQuery, +) +from geonature.core.gn_permissions.tools import has_any_permissions_by_action + + +class GenericModel: + @declared_attr + def __tablename__(cls): + return cls.__name__.lower() + + @classmethod + def set_id(cls) -> None: + pk_string = class_mapper(cls).primary_key[0].name + if hasattr(cls, "id_g") == False: + pk_value = getattr(cls, pk_string) + setattr(cls, "id_g", pk_value) + if hasattr(cls, "id_g") == False: + pk_value = getattr(cls, pk_string) + setattr(cls, "id_g", pk_value) + + @classmethod + def get_id_name(cls) -> None: + pk_string = class_mapper(cls).primary_key[0].name + # print('======= ==>', pk_string) + if hasattr(cls, "id_g") == False: + pk_value = getattr(cls, pk_string) + setattr(cls, "id_g", pk_value) + if hasattr(cls, "id_g") == False: + pk_value = getattr(cls, pk_string) + setattr(cls, "id_g", pk_value) + return pk_string + + @classmethod + def find_by_id(cls, _id: int) -> "GenericModel": + cls.set_id() + return cls.query.get_or_404(_id) + + @classmethod + def attribute_names(cls): + return [ + prop.key + for prop in class_mapper(cls).iterate_properties + if isinstance(prop, ColumnProperty) + ] + + # TODO: Voir si on garde cette méthode pour simplifier la recherche des relationship lors des filtres + @classmethod + def attribute_names_relationship(cls): + relationship_cols = inspect(cls).relationships.items() + return relationship_cols + # return [ cols[0] for cols in relationship_cols] + # return [ + # prop.key + # for prop in class_mapper(cls).iterate_properties + # if isinstance(prop, RelationshipProperty) + # ] + + +class PermissionModel(GenericModel): + def has_permission( + self, + cruved_object={"C": False, "R": False, "U": False, "D": False, "E": False, "V": False}, + ): + cruved_object_out = {} + for action_key, action_value in cruved_object.items(): + cruved_object_out[action_key] = self.has_instance_permission(scope=action_value) + return cruved_object_out + + def get_permission_by_action(self, module_code=None, object_code=None): + return has_any_permissions_by_action(module_code=module_code, object_code=object_code) + + +cor_module_type = DB.Table( + "cor_module_type", + DB.Column( + "id_module", + DB.Integer, + DB.ForeignKey("gn_commons.t_modules.id_module"), + primary_key=True, + ), + DB.Column( + DB.Column( + "id_type_site", + DB.Integer, + DB.ForeignKey("gn_monitoring.bib_type_site.id_nomenclature_type_site"), + primary_key=True, + ), + schema="gn_monitoring", +) + +cor_type_site = DB.Table( + "cor_type_site", + DB.Column( + "id_base_site", + DB.Integer, + DB.ForeignKey("gn_monitoring.t_base_sites.id_base_site"), + primary_key=True, + ), + DB.Column( + DB.Column( + "id_type_site", + DB.Integer, + DB.ForeignKey("gn_monitoring.bib_type_site.id_nomenclature_type_site"), + primary_key=True, + ), + schema="gn_monitoring", +) + + +@serializable +class BibTypeSite(DB.Model, PermissionModel): + __tablename__ = "bib_type_site" + __table_args__ = {"schema": "gn_monitoring"} + query_class = MonitoringQuery + + id_nomenclature_type_site = DB.Column( + DB.ForeignKey("ref_nomenclatures.t_nomenclatures.id_nomenclature"), + nullable=False, + primary_key=True, + ) + + id_nomenclature_type_site = DB.Column( + DB.ForeignKey("ref_nomenclatures.t_nomenclatures.id_nomenclature"), + nullable=False, + primary_key=True, + ) + config = DB.Column(JSONB) + nomenclature = DB.relationship( + TNomenclatures, uselist=False, backref=DB.backref("bib_type_site", uselist=False) + TNomenclatures, uselist=False, backref=DB.backref("bib_type_site", uselist=False) + ) + + sites = DB.relationship("TMonitoringSites", secondary=cor_type_site, lazy="noload") + + + sites = DB.relationship("TMonitoringSites", secondary=cor_type_site, lazy="noload") @serializable @@ -41,12 +198,15 @@ class TMonitoringObservationDetails(DB.Model): @serializable -class TObservations(DB.Model): +class TObservations(DB.Model, PermissionModel): __tablename__ = "t_observations" __table_args__ = {"schema": "gn_monitoring"} - id_observation = DB.Column(DB.Integer, primary_key=True, nullable=False, unique=True) id_base_visit = DB.Column(DB.ForeignKey("gn_monitoring.t_base_visits.id_base_visit")) + id_digitiser = DB.Column(DB.Integer, DB.ForeignKey("utilisateurs.t_roles.id_role")) + digitiser = DB.relationship( + User, primaryjoin=(User.id_role == id_digitiser), foreign_keys=[id_digitiser] + ) cd_nom = DB.Column(DB.Integer) comments = DB.Column(DB.String) uuid_observation = DB.Column(UUID(as_uuid=True), default=uuid4) @@ -67,13 +227,15 @@ class TObservations(DB.Model): @serializable -class TMonitoringObservations(TObservations): +class TMonitoringObservations(TObservations, PermissionModel): __tablename__ = "t_observation_complements" __table_args__ = {"schema": "gn_monitoring"} __mapper_args__ = { "polymorphic_identity": "monitoring_observation", } + query_class = ObservationsQuery + data = DB.Column(JSONB) id_observation = DB.Column( @@ -82,18 +244,45 @@ class TMonitoringObservations(TObservations): nullable=False, ) + @hybrid_property + def organism_actors(self): + # return self.digitiser.id_organisme + actors_organism_list = [] + if isinstance(self.digitiser, list): + for actor in self.digitiser: + if actor.id_organisme is not None: + actors_organism_list.append(actor.id_organisme) + elif isinstance(self.digitiser, User): + actors_organism_list.append(self.digitiser.id_organisme) + else: + return + return actors_organism_list + + def has_instance_permission(self, scope): + if scope == 0: + return False + elif scope in (1, 2): + if ( + g.current_user.id_role == self.id_digitiser + ): # or g.current_user in self.user_actors: + return True + if scope == 2 and g.current_user.organisme in self.organism_actors: + return True + elif scope == 3: + return True + TBaseVisits.dataset = DB.relationship(TDatasets) @serializable -class TMonitoringVisits(TBaseVisits): +class TMonitoringVisits(TBaseVisits, PermissionModel): __tablename__ = "t_visit_complements" __table_args__ = {"schema": "gn_monitoring"} __mapper_args__ = { "polymorphic_identity": "monitoring_visit", } - + query_class = VisitQuery id_base_visit = DB.Column( DB.ForeignKey("gn_monitoring.t_base_visits.id_base_visit"), nullable=False, @@ -125,23 +314,59 @@ class TMonitoringVisits(TBaseVisits): ) ) -@geoserializable -class TMonitoringSites(TBaseSites): + module = DB.relationship( + TModules, + lazy="select", + primaryjoin=(TModules.id_module == TBaseVisits.id_module), + foreign_keys=[TBaseVisits.id_module], + uselist=False, + ) + + @hybrid_property + def organism_actors(self): + # return self.digitiser.id_organisme + actors_organism_list = [] + if isinstance(self.digitiser, list): + for actor in self.digitiser: + if actor.id_organisme is not None: + actors_organism_list.append(actor.id_organisme) + elif isinstance(self.observers, list): + for actor in self.observers: + if actor.id_organisme is not None: + actors_organism_list.append(actor.id_organisme) + elif isinstance(self.digitiser, User): + actors_organism_list.append(self.digitiser.id_organisme) + else: + return actors_organism_list + return actors_organism_list + + def has_instance_permission(self, scope): + if scope == 0: + return False + elif scope in (1, 2): + if g.current_user.id_role == self.id_digitiser or any( + observer.id_role == g.current_user.id_role for observer in self.observers + ): # or g.current_user in self.user_actors: + return True + if scope == 2 and g.current_user.organisme in self.organism_actors: + return True + elif scope == 3: + return True + + +@geoserializable(geoCol="geom", idCol="id_base_site") +class TMonitoringSites(TBaseSites, PermissionModel): __tablename__ = "t_site_complements" __table_args__ = {"schema": "gn_monitoring"} __mapper_args__ = { "polymorphic_identity": "monitoring_site", } + query_class = SitesQuery id_base_site = DB.Column( DB.ForeignKey("gn_monitoring.t_base_sites.id_base_site"), nullable=False, primary_key=True ) - id_module = DB.Column( - DB.ForeignKey("gn_commons.t_modules.id_module"), - nullable=False, - ) - id_sites_group = DB.Column( DB.ForeignKey( "gn_monitoring.t_sites_groups.id_sites_group", @@ -180,20 +405,52 @@ class TMonitoringSites(TBaseSites): ) ) - geom_geojson = column_property(func.ST_AsGeoJSON(TBaseSites.geom), deferred=True) + geom_geojson = column_property( + select([func.st_asgeojson(TBaseSites.geom)]) + .where(TBaseSites.id_base_site == id_base_site) + .correlate_except(TBaseSites) + ) + types_site = DB.relationship("BibTypeSite", secondary=cor_type_site, lazy="joined") + + @hybrid_property + def organism_actors(self): + # return self.inventor.id_organisme + actors_organism_list = [] + if isinstance(self.inventor, list): + for actor in self.inventor: + if actor.id_organisme is not None: + actors_organism_list.append(actor.id_organisme) + else: + if hasattr(self.inventor, "id_organisme"): + actors_organism_list.append(self.inventor.id_organisme) + + def has_instance_permission(self, scope): + if scope == 0: + return False + elif scope in (1, 2): + if ( + g.current_user.id_role == self.id_digitiser + or g.current_user.id_role == self.id_inventor + ): # or g.current_user in self.user_actors: + return True + if scope == 2 and g.current_user.organisme in self.organism_actors: + return True + elif scope == 3: + return True @serializable -class TMonitoringSitesGroups(DB.Model): +class TMonitoringSitesGroups(DB.Model, PermissionModel): __tablename__ = "t_sites_groups" __table_args__ = {"schema": "gn_monitoring"} + query_class = SitesGroupsQuery id_sites_group = DB.Column(DB.Integer, primary_key=True, nullable=False, unique=True) + id_digitiser = DB.Column(DB.Integer, DB.ForeignKey("utilisateurs.t_roles.id_role")) - id_module = DB.Column( - DB.ForeignKey("gn_commons.t_modules.id_module"), nullable=False, unique=True + digitiser = DB.relationship( + User, primaryjoin=(User.id_role == id_digitiser), foreign_keys=[id_digitiser] ) - uuid_sites_group = DB.Column(UUID(as_uuid=True), default=uuid4) sites_group_name = DB.Column(DB.Unicode) @@ -234,14 +491,42 @@ class TMonitoringSitesGroups(DB.Model): ) ) + @hybrid_property + def organism_actors(self): + # return self.digitiser.id_organisme + actors_organism_list = [] + if isinstance(self.digitiser, list): + for actor in self.digitiser: + if actor.id_organisme is not None: + actors_organism_list.append(actor.id_organisme) + elif isinstance(self.digitiser, User): + actors_organism_list.append(self.digitiser.id_organisme) + else: + return + return actors_organism_list + + def has_instance_permission(self, scope): + if scope == 0: + return False + elif scope in (1, 2): + if ( + g.current_user.id_role == self.id_digitiser + ): # or g.current_user in self.user_actors: + return True + if scope == 2 and g.current_user.organisme in self.organism_actors: + return True + elif scope == 3: + return True + @serializable -class TMonitoringModules(TModules): +class TMonitoringModules(TModules, PermissionModel): __tablename__ = "t_module_complements" __table_args__ = {"schema": "gn_monitoring"} __mapper_args__ = { "polymorphic_identity": "monitoring_module", } + query_class = MonitoringQuery id_module = DB.Column( DB.ForeignKey("gn_commons.t_modules.id_module"), @@ -266,21 +551,22 @@ class TMonitoringModules(TModules): lazy="joined", ) - sites = DB.relationship( - "TMonitoringSites", - uselist=True, # pourquoi pas par defaut ? - primaryjoin=TMonitoringSites.id_module == id_module, - foreign_keys=[id_module], - lazy="select", - ) + # TODO: restore it with CorCategorySite + # sites = DB.relationship( + # 'TMonitoringSites', + # uselist=True, # pourquoi pas par defaut ? + # primaryjoin=TMonitoringSites.id_module == id_module, + # foreign_keys=[id_module], + # lazy="select", + # ) - sites_groups = DB.relationship( - "TMonitoringSitesGroups", - uselist=True, # pourquoi pas par defaut ? - primaryjoin=TMonitoringSitesGroups.id_module == id_module, - foreign_keys=[id_module], - lazy="select", - ) + # sites_groups = DB.relationship( + # 'TMonitoringSitesGroups', + # uselist=True, # pourquoi pas par defaut ? + # primaryjoin=TMonitoringSitesGroups.id_module == id_module, + # foreign_keys=[id_module], + # lazy="select", + # ) datasets = DB.relationship( "TDatasets", @@ -289,6 +575,8 @@ class TMonitoringModules(TModules): lazy="joined", ) + types_site = DB.relationship("BibTypeSite", secondary=cor_module_type, lazy="joined") + data = DB.Column(JSONB) # visits = DB.relationship( @@ -300,6 +588,47 @@ class TMonitoringModules(TModules): # ) + +# Use alias since there is already a FROM caused by count (column_properties) +sites_alias = aliased(TMonitoringSites) +TMonitoringModules.sites_groups = DB.relationship( + "TMonitoringSitesGroups", + uselist=True, # pourquoi pas par defaut ? + primaryjoin=TMonitoringModules.id_module == cor_module_type.c.id_module, + secondaryjoin=and_( + TMonitoringSitesGroups.id_sites_group == sites_alias.id_sites_group, + sites_alias.id_base_site == cor_type_site.c.id_base_site, + ), + secondaryjoin=and_( + TMonitoringSitesGroups.id_sites_group == sites_alias.id_sites_group, + sites_alias.id_base_site == cor_type_site.c.id_base_site, + ), + secondary=join( + cor_type_site, + cor_module_type, + cor_type_site.c.id_type_site == cor_module_type.c.id_type_site, + ), + foreign_keys=[cor_type_site.c.id_base_site, cor_module_type.c.id_module], + lazy="select", + viewonly=True, +) + + +TMonitoringModules.sites = DB.relationship( + "TMonitoringSites", + uselist=True, # pourquoi pas par defaut ? + primaryjoin=TMonitoringModules.id_module == cor_module_type.c.id_module, + secondaryjoin=TMonitoringSites.id_base_site == cor_type_site.c.id_base_site, + secondary=join( + cor_type_site, + cor_module_type, + cor_type_site.c.id_type_site == cor_module_type.c.id_type_site, + ), + foreign_keys=[cor_type_site.c.id_base_site, cor_module_type.c.id_module], + lazy="select", + viewonly=True, +) + TMonitoringModules.visits = DB.relationship( TMonitoringVisits, lazy="select", diff --git a/backend/gn_module_monitoring/monitoring/objects.py b/backend/gn_module_monitoring/monitoring/objects.py index edd51870e..ce7b86087 100644 --- a/backend/gn_module_monitoring/monitoring/objects.py +++ b/backend/gn_module_monitoring/monitoring/objects.py @@ -25,10 +25,28 @@ class MonitoringSite(MonitoringObjectGeom): avec la méthode from_dict """ - def preprocess_data(self, data): - module_ids = [module.id_module for module in self._model.modules] - id_module = int(data["id_module"]) - if id_module not in module_ids: - module_ids.append(id_module) + def preprocess_data(self, properties, data=[]): + if len(data) != 0: + if len(data["types_site"]) > 0 and all(isinstance(x, int) for x in data["types_site"]): + properties["id_nomenclature_type_site"] = data["types_site"][0] + properties["types_site"] = data["types_site"] - data["modules"] = module_ids + elif "data" in data and data["data"]["id_nomenclature_type_site"]: + properties["id_nomenclature_type_site"] = data["data"]["id_nomenclature_type_site"] + else: + properties["id_nomenclature_type_site"] = data["types_site"][0][ + "id_nomenclature_type_site" + ] + else: + if len(properties.get("types_site", [])) != 0: + if hasattr(self._model, "types_site"): + properties["id_nomenclature_type_site"] = properties["types_site"][0] + # properties["types_site"] = [] + # # TODO: performance? + # # for type in properties['types_site']: + # # properties['types_site'].append(types_site) + # types_site = [ + # typ.nomenclature.id_nomenclature for typ in self._model.types_site + # ] + # properties["types_site"] = types_site + # TODO: A enlever une fois qu'on aura enelever le champ "id_nomenclature_type_site" du model et de la bdd diff --git a/backend/gn_module_monitoring/monitoring/queries.py b/backend/gn_module_monitoring/monitoring/queries.py new file mode 100644 index 000000000..754e1fe78 --- /dev/null +++ b/backend/gn_module_monitoring/monitoring/queries.py @@ -0,0 +1,146 @@ +from flask import g +from flask_sqlalchemy import BaseQuery +from sqlalchemy import Unicode, and_, Unicode, func, or_, false +from sqlalchemy.types import DateTime +from werkzeug.datastructures import MultiDict +from geonature.core.gn_permissions.tools import get_scopes_by_action +import gn_module_monitoring.monitoring.models as Models + + +class Query(BaseQuery): + def _get_entity(self, entity): + if hasattr(entity, "_entities"): + return self._get_entity(entity._entities[0]) + return entity.entities[0] + + def _get_model(self): + # When sqlalchemy is updated: + # return self._raw_columns[0].entity_namespace + # But for now: + entity = self._get_entity(self) + return entity.c + + def filter_by_params(self, params: MultiDict = None): + model = self._get_model() + and_list = [] + for key, value in params.items(): + column = getattr(model, key) + if isinstance(column.type, Unicode): + and_list.append(column.ilike(f"%{value}%")) + elif isinstance(column.type, DateTime): + and_list.append(func.to_char(column, "YYYY-MM-DD").ilike(f"%{value}%")) + else: + and_list.append(column == value) + and_query = and_(*and_list) + return self.filter(and_query) + + def sort(self, label: str, direction: str): + model = self._get_model() + order_by = getattr(model, label) + if direction == "desc": + order_by = order_by.desc() + + return self.order_by(order_by) + + def _get_cruved_scope(self, module_code=None, object_code=None, user=None): + if user is None: + user = g.current_user + cruved = get_scopes_by_action( + id_role=user.id_role, module_code=module_code, object_code=object_code + ) + return cruved + + def _get_read_scope(self, module_code="MONITORINGS", object_code=None, user=None): + if user is None: + user = g.current_user + cruved = get_scopes_by_action( + id_role=user.id_role, module_code=module_code, object_code=object_code + ) + return cruved["R"] + + def filter_by_readable(self, module_code="MONITORINGS", object_code=None, user=None): + """ + Return the object where the user has autorization via its CRUVED + """ + return self.filter_by_scope( + self._get_read_scope(module_code=module_code, object_code=object_code, user=user) + ) + + +class SitesQuery(Query): + def filter_by_scope(self, scope, user=None): + if user is None: + user = g.current_user + if scope == 0: + self = self.filter(false()) + elif scope in (1, 2): + ors = [ + Models.TMonitoringSites.id_digitiser == user.id_role, + Models.TMonitoringSites.id_inventor == user.id_role, + ] + # if organism is None => do not filter on id_organism even if level = 2 + if scope == 2 and user.id_organisme is not None: + ors += [ + Models.TMonitoringSites.inventor.has(id_organisme=user.id_organisme), + Models.TMonitoringSites.digitiser.has(id_organisme=user.id_organisme), + ] + self = self.filter(or_(*ors)) + return self + + +class SitesGroupsQuery(Query): + def filter_by_scope(self, scope, user=None): + if user is None: + user = g.current_user + if scope == 0: + self = self.filter(false()) + elif scope in (1, 2): + ors = [ + Models.TMonitoringSitesGroups.id_digitiser == user.id_role, + ] + # if organism is None => do not filter on id_organism even if level = 2 + if scope == 2 and user.id_organisme is not None: + ors += [ + Models.TMonitoringSitesGroups.digitiser.has(id_organisme=user.id_organisme) + ] + self = self.filter(or_(*ors)) + return self + + +class VisitQuery(Query): + def filter_by_scope(self, scope, user=None): + # Problem pas le même comportement que pour les sites et groupes de site + if user is None: + user = g.current_user + if scope == 0: + self = self.filter(false()) + elif scope in (1, 2): + ors = [ + Models.TMonitoringVisits.id_digitiser == user.id_role, + Models.TMonitoringVisits.observers.any(id_role=user.id_role), + ] + # if organism is None => do not filter on id_organism even if level = 2 + if scope == 2 and user.id_organisme is not None: + ors += [ + Models.TMonitoringVisits.observers.any(id_organisme=user.id_organisme), + Models.TMonitoringVisits.digitiser.has(id_organisme=user.id_organisme), + ] + self = self.filter(or_(*ors)) + return self + + +class ObservationsQuery(Query): + def filter_by_scope(self, scope, user=None): + if user is None: + user = g.current_user + if scope == 0: + self = self.filter(false()) + elif scope in (1, 2): + ors = [ + Models.TObservations.id_digitiser == user.id_role, + ] + # if organism is None => do not filter on id_organism even if level = 2 + if scope == 2 and user.id_organisme is not None: + ors += [Models.TObservations.digitiser.has(id_organisme=user.id_organisme)] + self = self.filter(or_(*ors)) + return self diff --git a/backend/gn_module_monitoring/monitoring/repositories.py b/backend/gn_module_monitoring/monitoring/repositories.py index fa536efa2..9fb436acd 100644 --- a/backend/gn_module_monitoring/monitoring/repositories.py +++ b/backend/gn_module_monitoring/monitoring/repositories.py @@ -7,6 +7,9 @@ from ..utils.utils import to_int from sqlalchemy.orm import joinedload +from gn_module_monitoring.utils.routes import get_objet_with_permission_boolean +from gn_module_monitoring.monitoring.models import PermissionModel, TMonitoringModules +import gn_module_monitoring.monitoring.definitions as MonitoringDef log = logging.getLogger(__name__) @@ -38,6 +41,14 @@ def get(self, value=None, field_name=None, depth=0): self._model = req.filter(getattr(Model, field_name) == value).one() self._id = getattr(self._model, self.config_param("id_field_name")) + if isinstance(self._model, PermissionModel) and not isinstance( + self._model, TMonitoringModules + ): + cruved_item_dict = get_objet_with_permission_boolean( + [self._model], + object_code=MonitoringDef.MonitoringPermissions_dict[self._object_type], + ) + self.cruved = cruved_item_dict[0]["cruved"] return self @@ -165,10 +176,12 @@ def breadcrumbs(self, params): if params["parents_path"]: object_type = params.get("parents_path", []).pop() next = MonitoringObject(self._module_code, object_type) - - id_field_name = next.config_param("id_field_name") - next._id = self.get_value(id_field_name) or params.get(id_field_name) - next.get(0) + if next._object_type == "module": + next.get(field_name="module_code", value=self._module_code) + else: + id_field_name = next.config_param("id_field_name") + next._id = self.get_value(id_field_name) or params.get(id_field_name) + next.get(0) else: next = self.get_parent() diff --git a/backend/gn_module_monitoring/monitoring/schemas.py b/backend/gn_module_monitoring/monitoring/schemas.py new file mode 100644 index 000000000..41ccd06b6 --- /dev/null +++ b/backend/gn_module_monitoring/monitoring/schemas.py @@ -0,0 +1,104 @@ +import json + +import geojson +from geonature.utils.env import MA +from marshmallow import Schema, fields, validate +from geonature.core.gn_commons.schemas import MediaSchema, ModuleSchema + +from gn_module_monitoring.monitoring.models import ( + BibTypeSite, + TMonitoringSites, + TMonitoringSitesGroups, + TMonitoringVisits, +) + + +def paginate_schema(schema): + class PaginationSchema(Schema): + count = fields.Integer() + limit = fields.Integer() + page = fields.Integer() + items = fields.Nested(schema, many=True, dump_only=True) + + return PaginationSchema + + +class MonitoringSitesGroupsSchema(MA.SQLAlchemyAutoSchema): + sites_group_name = fields.String( + validate=validate.Length(min=3, error="Length must be greater than 3"), + ) + + class Meta: + model = TMonitoringSitesGroups + exclude = ("geom_geojson",) + load_instance = True + + medias = MA.Nested(MediaSchema, many=True) + pk = fields.Method("set_pk", dump_only=True) + geometry = fields.Method("serialize_geojson", dump_only=True) + id_digitiser = fields.Method("get_id_digitiser") + + def get_id_digitiser(self, obj): + return obj.id_digitiser + + def set_pk(self, obj): + return self.Meta.model.get_id_name() + + def serialize_geojson(self, obj): + if obj.geom_geojson is not None: + return json.loads(obj.geom_geojson) + + +class BibTypeSiteSchema(MA.SQLAlchemyAutoSchema): + label = fields.Method("get_label_from_type_site") + # See if useful in the future: + # type_site = fields.Nested(NomenclatureSchema(only=("label_fr",)), dump_only=True) + + def get_label_from_type_site(self, obj): + return obj.nomenclature.label_fr + + class Meta: + model = BibTypeSite + include_fk = True + load_instance = True + + +class MonitoringSitesSchema(MA.SQLAlchemyAutoSchema): + class Meta: + model = TMonitoringSites + exclude = ("geom_geojson", "geom") + + geometry = fields.Method("serialize_geojson", dump_only=True) + pk = fields.Method("set_pk", dump_only=True) + types_site = MA.Nested(BibTypeSiteSchema, many=True) + id_sites_group = fields.Method("get_id_sites_group") + id_inventor = fields.Method("get_id_inventor") + inventor = fields.Method("get_inventor_name") + + def serialize_geojson(self, obj): + if obj.geom is not None: + return geojson.dumps(obj.as_geofeature().get("geometry")) + + def set_pk(self, obj): + return self.Meta.model.get_id_name() + + def get_id_sites_group(self, obj): + return obj.id_sites_group + + def get_id_inventor(self, obj): + return obj.id_inventor + + def get_inventor_name(self, obj): + if obj.inventor: + return [obj.inventor.nom_complet] + + +class MonitoringVisitsSchema(MA.SQLAlchemyAutoSchema): + class Meta: + model = TMonitoringVisits + + pk = fields.Method("set_pk", dump_only=True) + module = MA.Nested(ModuleSchema) + + def set_pk(self, obj): + return self.Meta.model.get_id_name() diff --git a/backend/gn_module_monitoring/monitoring/serializer.py b/backend/gn_module_monitoring/monitoring/serializer.py index 75bb82758..53f877b0e 100644 --- a/backend/gn_module_monitoring/monitoring/serializer.py +++ b/backend/gn_module_monitoring/monitoring/serializer.py @@ -5,10 +5,13 @@ import uuid from flask import current_app from .base import MonitoringObjectBase, monitoring_definitions +import gn_module_monitoring.monitoring.definitions as MonitoringDef from ..utils.utils import to_int from ..routes.data_utils import id_field_name_dict from geonature.utils.env import DB from geonature.core.gn_permissions.tools import get_scopes_by_action +from gn_module_monitoring.utils.routes import get_objet_with_permission_boolean +from gn_module_monitoring.monitoring.models import PermissionModel, TMonitoringModules class MonitoringObjectSerializer(MonitoringObjectBase): @@ -49,13 +52,33 @@ def flatten_specific_properties(self, properties): def unflatten_specific_properties(self, properties): data = {} - for attribut_name in self.config_schema("specific"): - val = properties.pop(attribut_name) - data[attribut_name] = val + for attribut_name, attribut_value in self.config_schema("specific").items(): + if "type_widget" in attribut_value and attribut_value["type_widget"] != "html": + val = properties.pop(attribut_name) + data[attribut_name] = val if data: properties["data"] = data + def get_readable_list_object(self, relation_name, children_type): + childs_model = getattr(self._model, relation_name) + if ( + len(childs_model) > 0 + and isinstance(childs_model[0], PermissionModel) + and not isinstance(childs_model[0], TMonitoringModules) + ): + all_object_readable = ( + childs_model[0] + .query.filter_by_readable( + module_code=self._module_code, + object_code=MonitoringDef.MonitoringPermissions_dict[children_type], + ) + .all() + ) + child_object_readable = [v for v in childs_model if v in all_object_readable] + return child_object_readable + return childs_model + def serialize_children(self, depth): children_types = self.config_param("children_types") @@ -73,7 +96,10 @@ def serialize_children(self, depth): children_of_type = [] - for child_model in getattr(self._model, relation_name): + childs_object_readable = self.get_readable_list_object( + relation_name, children_type=children_type + ) + for child_model in childs_object_readable: child = monitoring_definitions.monitoring_object_instance( self._module_code, children_type, model=child_model ) @@ -83,6 +109,23 @@ def serialize_children(self, depth): return children + def get_cruved_by_object(self): + list_model = [] + list_model.append(self._model) + if ( + isinstance(list_model[0], PermissionModel) + and not isinstance(list_model[0], TMonitoringModules) + and self._module_code != "generic" + ): + id_name = list_model[0].get_id_name() + cruved_item_dict = get_objet_with_permission_boolean( + list_model, object_code=MonitoringDef.MonitoringPermissions_dict[self._object_type] + ) + for cruved_item in cruved_item_dict: + if self._id == cruved_item[id_name]: + self.cruved = cruved_item["cruved"] + return self.cruved + def properties_names(self): generic = list(self.config_schema("generic").keys()) data = ["data"] if hasattr(self._model, "data") else [] @@ -145,6 +188,7 @@ def serialize(self, depth=1): "module_code": self._module_code, "site_id": self.get_site_id(), "id": self._id, + "cruved": self.get_cruved_by_object(), } properties["id_parent"] = to_int(self.id_parent()) @@ -166,10 +210,16 @@ def populate(self, post_data): self.unflatten_specific_properties(properties) # pretraitement (pour t_base_site et cor_site_module) - self.preprocess_data(properties) + if "dataComplement" in post_data: + self.preprocess_data(properties, post_data["dataComplement"]) + else: + self.preprocess_data(properties) # ajout des données en base if hasattr(self._model, "from_geofeature"): + for key in list(post_data): + if key not in ("properties", "geometry", "type"): + post_data.pop(key) self._model.from_geofeature(post_data, True) else: self._model.from_dict(properties, True) diff --git a/backend/gn_module_monitoring/monitoring/utils.py b/backend/gn_module_monitoring/monitoring/utils.py index e69de29bb..05f0dc18b 100644 --- a/backend/gn_module_monitoring/monitoring/utils.py +++ b/backend/gn_module_monitoring/monitoring/utils.py @@ -0,0 +1,10 @@ +import json + +from jinja2.utils import markupsafe + + +def json_formatter(view, context, model, name): + """Prettify JSON data in flask admin lists""" + value = getattr(model, name) + json_value = json.dumps(value, ensure_ascii=False, indent=2) + return markupsafe.Markup("
{}
".format(json_value)) diff --git a/backend/gn_module_monitoring/routes/data_utils.py b/backend/gn_module_monitoring/routes/data_utils.py index 2c0fae73b..7180ae389 100644 --- a/backend/gn_module_monitoring/routes/data_utils.py +++ b/backend/gn_module_monitoring/routes/data_utils.py @@ -3,6 +3,7 @@ d'utilisateurs de nomenclature de taxonomie + TODO cache """ @@ -32,8 +33,13 @@ from ..blueprint import blueprint from ..config.repositories import get_config - -from ..monitoring.models import TMonitoringSitesGroups, TMonitoringSites +from gn_module_monitoring.utils.routes import get_sites_groups_from_module_id +from gn_module_monitoring.monitoring.schemas import MonitoringSitesGroupsSchema +from gn_module_monitoring.monitoring.models import ( + BibTypeSite, + TMonitoringSites, + TMonitoringSitesGroups, +) model_dict = { "habitat": Habref, @@ -41,6 +47,7 @@ "user": User, "taxonomy": Taxref, "dataset": TDatasets, + "types_site": BibTypeSite, "observer_list": UserList, "taxonomy_list": BibListes, "sites_group": TMonitoringSitesGroups, @@ -91,10 +98,9 @@ def get_init_data(module_code): # sites_group if "sites_group" in config: - res_sites_group = ( - DB.session.query(TMonitoringSitesGroups).filter_by(id_module=id_module).all() - ) - out["sites_group"] = [sites_group.as_dict() for sites_group in res_sites_group] + sites_groups = get_sites_groups_from_module_id(id_module) + schema = MonitoringSitesGroupsSchema() + out["sites_group"] = [schema.dump(sites_group) for sites_group in sites_groups] # dataset (cruved ??) res_dataset = ( @@ -115,6 +121,7 @@ def get_util_nomenclature_api(code_nomenclature_type, cd_nomenclature): revoie un champ d'un object de type nomenclature à partir de son type et de son cd_nomenclature renvoie l'objet entier si field_name renseigné en paramètre de route est 'all' + :param code_nomenclature_type: :param cd_nomenclature: :return object entier si field_name = all, la valeur du champs defini par field_name sinon @@ -164,6 +171,7 @@ def get_util_from_id_api(type_util, id): """ revoie un champ d'un object de type nomenclature, taxonomy, utilisateur, ... renvoie l'objet entier si field_name renseigné en paramètre de route est 'all' + :param type_util: 'nomenclature' | 'taxonomy' | 'utilisateur' | etc.... :param id: id de l'object requis :type type_util: str @@ -207,6 +215,7 @@ def get_util_from_ids_api(type_util, ids): """ variante de get_util_from_id_api pour plusieurs id renvoie un tableau de valeur (ou de dictionnaire si key est 'all') + parametre get key: all renvoie tout l'objet sinon renvoie un champ @@ -214,11 +223,13 @@ def get_util_from_ids_api(type_util, ids): pour reformer une chaine de caractere a partir du tableau résultat de la requete si separator_out == ' ,' alors ['jean', 'pierre', 'paul'].join(separator_out) -> 'jean, pierre, paul' + :param type_util: 'nomenclature' | 'taxonomy' | 'utilisateur' :param ids: plusieurs id reliée par des '-' (ex: 1-123-3-4) :type type_util: str :type ids: str :return list si key=all ou chaine de caratere + """ field_name = request.args.get("field_name", "all") @@ -244,4 +255,4 @@ def get_util_from_ids_api(type_util, ids): return [r.as_dict() for r in res] # renvoie une chaine de caratère - return separator_out.join([r[0] for r in res]) \ No newline at end of file + return separator_out.join([r[0] for r in res]) diff --git a/backend/gn_module_monitoring/routes/modules.py b/backend/gn_module_monitoring/routes/modules.py index c12d7d3fa..28c94d139 100644 --- a/backend/gn_module_monitoring/routes/modules.py +++ b/backend/gn_module_monitoring/routes/modules.py @@ -8,15 +8,21 @@ from ..blueprint import blueprint from ..utils.utils import to_int -from geonature.core.gn_permissions.tools import get_scopes_by_action +from geonature.core.gn_permissions.tools import get_scopes_by_action, has_any_permissions_by_action from geonature.core.gn_permissions.decorators import check_cruved_scope +from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema from gn_module_monitoring import MODULE_CODE from ..modules.repositories import ( get_module, get_modules, ) +from ..config.repositories import get_config +from gn_module_monitoring.utils.routes import ( + query_all_types_site_from_module_id, + get_object_list_monitorings, +) @blueprint.route("/module/", methods=["GET"]) @@ -44,6 +50,23 @@ def get_module_api(value): return module_out +@blueprint.route("/cruved_object", methods=["GET"]) +@check_cruved_scope("R", module_code=MODULE_CODE) +def get_cruved_monitorings(): + """ + Renvoie la liste des modules de suivi + """ + dic_object_cruved = {} + object_list_tuples = get_object_list_monitorings() + object_list = [value for (value,) in object_list_tuples] + for object in object_list: + dic_object_cruved[object] = has_any_permissions_by_action( + module_code=MODULE_CODE, object_code=object + ) + + return dic_object_cruved + + @blueprint.route("/modules", methods=["GET"]) @check_cruved_scope("R", module_code=MODULE_CODE) @json_resp_accept_empty_list @@ -65,3 +88,12 @@ def get_modules_api(): modules_out.append(module_out) return modules_out + + +@blueprint.route("/modules//types_sites", methods=["GET"]) +def get_all_types_site_from_module_id(module_code): + config = get_config(module_code, True) + id_module = config["custom"]["__MODULE.ID_MODULE"] + types_site = query_all_types_site_from_module_id(id_module) + schema = BibTypeSiteSchema() + return [schema.dump(res) for res in types_site] diff --git a/backend/gn_module_monitoring/routes/monitoring.py b/backend/gn_module_monitoring/routes/monitoring.py index 0eca32a6b..3c4f2d97c 100644 --- a/backend/gn_module_monitoring/routes/monitoring.py +++ b/backend/gn_module_monitoring/routes/monitoring.py @@ -5,7 +5,7 @@ from pathlib import Path -from werkzeug.exceptions import NotFound +from werkzeug.exceptions import NotFound, Forbidden from flask import request, send_from_directory, url_for, g, current_app import datetime as dt @@ -18,7 +18,7 @@ from ..blueprint import blueprint - +from geonature.core.gn_permissions import decorators as permissions from geonature.core.gn_permissions.decorators import check_cruved_scope from geonature.core.gn_commons.models.base import TModules from geonature.core.gn_permissions.models import TObjects, Permission @@ -31,6 +31,9 @@ from ..modules.repositories import get_module from ..utils.utils import to_int from ..config.repositories import get_config +from gn_module_monitoring.utils.routes import ( + query_all_types_site_from_site_id, +) @blueprint.url_value_preprocessor @@ -71,17 +74,15 @@ def set_current_module(endpoint, values): @blueprint.route("/object///", methods=["GET"]) -@blueprint.route( - "/object//", defaults={"id": None}, methods=["GET"] -) +@blueprint.route("/object//", methods=["GET"]) @blueprint.route( "/object/module", - defaults={"module_code": None, "object_type": "module", "id": None}, methods=["GET"], ) @check_cruved_scope("R") @json_resp -def get_monitoring_object_api(module_code, object_type, id): +@permissions.check_cruved_scope("R", get_scope=True) +def get_monitoring_object_api(scope, module_code=None, object_type="module", id=None): """ renvoie un object, à partir de type de l'object et de son id @@ -98,9 +99,30 @@ def get_monitoring_object_api(module_code, object_type, id): # field_name = param.get('field_name') # value = module_code if object_type == 'module' - get_config(module_code, force=True) depth = to_int(request.args.get("depth", 1)) + if id != None: + object = monitoring_definitions.monitoring_object_instance( + module_code, object_type, id + ).get(depth=depth) + if not object._model.has_instance_permission(scope=scope): + raise Forbidden(f"User {g.current_user} cannot read {object_type} {object._id}") + + if id != None and object_type == "site": + types_site_obj = query_all_types_site_from_site_id(id) + list_types_sites_dict = [ + values + for res in types_site_obj + for (key_type_site, values) in res.as_dict().items() + if key_type_site == "config" + ] + customConfig = {"specific": {}} + for specific_config in list_types_sites_dict: + customConfig["specific"].update(specific_config["specific"]) + + get_config(module_code, force=True, customSpecConfig=customConfig) + else: + get_config(module_code, force=True) return ( monitoring_definitions.monitoring_object_instance(module_code, object_type, id).get( @@ -142,6 +164,77 @@ def create_or_update_object_api(module_code, object_type, id): ) +def create_or_update_object_api_sites_sites_group(module_code, object_type, id=None): + """ + route pour la création ou la modification d'un objet + si id est renseigné, c'est une création (PATCH) + sinon c'est une modification (POST) + + :param module_code: reference le module concerne + :param object_type: le type d'object (site, visit, obervation) + :param id : l'identifiant de l'object (de id_base_site pour site) + :type module_code: str + :type object_type: str + :type id: int + :return: renvoie l'object crée ou modifié + :rtype: dict + """ + depth = to_int(request.args.get("depth", 1)) + + # recupération des données post + post_data = dict(request.get_json()) + if module_code != "generic": + module = get_module("module_code", module_code) + else: + module = {"id_module": "generic"} + # TODO : A enlever une fois que le post_data contiendra geometry et type depuis le front + if object_type == "site" and not "geometry" in post_data: + post_data["geometry"] = {"type": "Point", "coordinates": [2.5, 50]} + post_data["type"] = "Feature" + # on rajoute id_module s'il n'est pas renseigné par défaut ?? + if "id_module" not in post_data["properties"]: + module["id_module"] = "generic" + post_data["properties"]["id_module"] = module["id_module"] + else: + post_data["properties"]["id_module"] = module.id_module + + return ( + monitoring_definitions.monitoring_object_instance(module_code, object_type, id) + .create_or_update(post_data) + .serialize(depth) + ) + + +def get_config_object(module_code, object_type, id): + """ + renvoie un object, à partir de type de l'object et de son id + + :param module_code: reference le module concerne + :param object_type: le type d'object (site, visit, obervation) + :param id : l'identifiant de l'object (de id_base_site pour site) + :type module_code: str + :type object_type: str + :type id: int + + :return: renvoie l'object requis + :rtype: dict + """ + + # field_name = param.get('field_name') + # value = module_code if object_type == 'module' + get_config(module_code, force=True) + + depth = to_int(request.args.get("depth", 1)) + + return ( + monitoring_definitions.monitoring_object_instance(module_code, object_type, id).get( + depth=depth + ) + # .get(value=value, field_name = field_name) + .serialize(depth) + ) + + # update object @blueprint.route("object///", methods=["PATCH"]) @blueprint.route( @@ -151,8 +244,27 @@ def create_or_update_object_api(module_code, object_type, id): ) @check_cruved_scope("U") @json_resp -def update_object_api(module_code, object_type, id): - get_config(module_code, force=True) +@permissions.check_cruved_scope("U", get_scope=True) +def update_object_api(scope, module_code, object_type, id): + depth = to_int(request.args.get("depth", 1)) + if id != None: + object = monitoring_definitions.monitoring_object_instance( + module_code, object_type, id + ).get(depth=depth) + if not object._model.has_instance_permission(scope=scope): + raise Forbidden(f"User {g.current_user} cannot update {object_type} {object._id}") + + customConfig = {"specific": {}} + post_data = dict(request.get_json()) + if "dataComplement" in post_data: + for keys in post_data["dataComplement"].keys(): + if "config" in post_data["dataComplement"][keys]: + customConfig["specific"].update( + post_data["dataComplement"][keys]["config"]["specific"] + ) + get_config(module_code, force=True, customSpecConfig=customConfig) + else: + get_config(module_code, force=True) return create_or_update_object_api(module_code, object_type, id) @@ -168,7 +280,17 @@ def update_object_api(module_code, object_type, id): @check_cruved_scope("C") @json_resp def create_object_api(module_code, object_type, id): - get_config(module_code, force=True) + customConfig = {"specific": {}} + post_data = dict(request.get_json()) + if "dataComplement" in post_data: + for keys in post_data["dataComplement"].keys(): + if "config" in post_data["dataComplement"][keys]: + customConfig["specific"].update( + post_data["dataComplement"][keys]["config"]["specific"] + ) + get_config(module_code, force=True, customSpecConfig=customConfig) + else: + get_config(module_code, force=True) return create_or_update_object_api(module_code, object_type, id) @@ -181,9 +303,22 @@ def create_object_api(module_code, object_type, id): ) @check_cruved_scope("D") @json_resp -def delete_object_api(module_code, object_type, id): +@permissions.check_cruved_scope("D", get_scope=True) +def delete_object_api(scope, module_code, object_type, id): + depth = to_int(request.args.get("depth", 1)) + if id != None: + object = monitoring_definitions.monitoring_object_instance( + module_code, object_type, id + ).get(depth=depth) + if not object._model.has_instance_permission(scope=scope): + raise Forbidden(f"User {g.current_user} cannot delete {object_type} {object._id}") + + if object_type in ("site", "sites_group"): + raise Exception( + f"No right to delete {object_type} from protocol. The {object_type} with id: {id} could be linked with others protocols" + ) get_config(module_code, force=True) - + # NOTE: normalement on ne peut plus supprimer les groupes de site / sites par l'entrée protocoles return monitoring_definitions.monitoring_object_instance(module_code, object_type, id).delete() diff --git a/backend/gn_module_monitoring/routes/site.py b/backend/gn_module_monitoring/routes/site.py new file mode 100644 index 000000000..cc2c4f978 --- /dev/null +++ b/backend/gn_module_monitoring/routes/site.py @@ -0,0 +1,257 @@ +from flask import request, g +from flask.json import jsonify +import json +from geonature.core.gn_commons.schemas import ModuleSchema +from geonature.utils.env import db +from sqlalchemy import and_ +from sqlalchemy.orm import Load, joinedload +from sqlalchemy.sql import func +from werkzeug.datastructures import MultiDict +from werkzeug.exceptions import Forbidden + +from geonature.core.gn_permissions import decorators as permissions +from pypnusershub.db.models import User +from gn_module_monitoring.blueprint import blueprint +from gn_module_monitoring.config.repositories import get_config +from gn_module_monitoring.monitoring.models import ( + BibTypeSite, + TMonitoringModules, + TMonitoringSites, + TNomenclatures, +) +from gn_module_monitoring import MODULE_CODE +from geonature.core.gn_permissions.decorators import check_cruved_scope +from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema, MonitoringSitesSchema +from gn_module_monitoring.routes.monitoring import ( + create_or_update_object_api_sites_sites_group, + get_config_object, +) +from gn_module_monitoring.routes.modules import get_modules +from gn_module_monitoring.utils.routes import ( + filter_params, + geojson_query, + get_limit_page, + get_sort, + paginate, + paginate_scope, + sort, + query_all_types_site_from_site_id, + filter_according_to_column_type_for_site, + sort_according_to_column_type_for_site, + get_objet_with_permission_boolean, +) + + +@blueprint.route("/sites/config", methods=["GET"]) +def get_config_sites(id=None, module_code="generic", object_type="site"): + obj = get_config_object(module_code, object_type, id) + return obj["properties"] + + +@blueprint.route("/sites/types", methods=["GET"]) +def get_types_site(): + params = MultiDict(request.args) + limit, page = get_limit_page(params=params) + sort_label, sort_dir = get_sort( + params=params, default_sort="id_nomenclature_type_site", default_direction="desc" + ) + + query = filter_params(query=BibTypeSite.query, params=params) + query = sort(query=query, sort=sort_label, sort_dir=sort_dir) + + return paginate( + query=query, + schema=BibTypeSiteSchema, + limit=limit, + page=page, + ) + + +@blueprint.route("/sites/types/label", methods=["GET"]) +def get_types_site_by_label(): + params = MultiDict(request.args) + limit, page = get_limit_page(params=params) + sort_label, sort_dir = get_sort( + params=params, default_sort="label_fr", default_direction="desc" + ) + joinquery = BibTypeSite.query.join(BibTypeSite.nomenclature).filter( + TNomenclatures.label_fr.ilike(f"%{params['label_fr']}%") + ) + if sort_dir == "asc": + joinquery = joinquery.order_by(TNomenclatures.label_fr.asc()) + + # See if there are not too much labels since they are used + # in select in the frontend side. And an infinite select is not + # implemented + return paginate( + query=joinquery, + schema=BibTypeSiteSchema, + limit=limit, + page=page, + ) + + +@blueprint.route("/sites/types/", methods=["GET"]) +def get_type_site_by_id(id_type_site): + res = BibTypeSite.find_by_id(id_type_site) + schema = BibTypeSiteSchema() + return schema.dump(res) + + +@blueprint.route("/sites//types", methods=["GET"], defaults={"object_type": "site"}) +def get_all_types_site_from_site_id(id_site, object_type): + types_site = query_all_types_site_from_site_id(id_site) + schema = BibTypeSiteSchema() + return [schema.dump(res) for res in types_site] + + +@blueprint.route("/sites", methods=["GET"], defaults={"object_type": "site"}) +@check_cruved_scope("R", module_code=MODULE_CODE, object_code="GNM_SITES") +def get_sites(object_type): + object_code = "GNM_SITES" + params = MultiDict(request.args) + # TODO: add filter support + limit, page = get_limit_page(params=params) + sort_label, sort_dir = get_sort( + params=params, default_sort="id_base_site", default_direction="desc" + ) + + query = TMonitoringSites.query + query = filter_according_to_column_type_for_site(query, params) + query = sort_according_to_column_type_for_site(query, sort_label, sort_dir) + + query_allowed = query.filter_by_readable(object_code=object_code) + return paginate_scope( + query=query_allowed, + schema=MonitoringSitesSchema, + limit=limit, + page=page, + object_code=object_code, + ) + # return paginate( + # query=query, + # schema=MonitoringSitesSchema, + # limit=limit, + # page=page, + # ) + + +@blueprint.route("/sites/", methods=["GET"], defaults={"object_type": "site"}) +@permissions.check_cruved_scope( + "R", get_scope=True, module_code=MODULE_CODE, object_code="GNM_SITES" +) +def get_site_by_id(scope, id, object_type): + site = TMonitoringSites.query.get_or_404(id) + if not site.has_instance_permission(scope=scope): + raise Forbidden(f"User {g.current_user} cannot read site {site.id_base_site}") + schema = MonitoringSitesSchema() + response = schema.dump(site) + response["cruved"] = get_objet_with_permission_boolean([site], object_code="GNM_SITES")[0][ + "cruved" + ] + response["geometry"] = json.loads(response["geometry"]) + return response + + +@blueprint.route("/sites/geometries", methods=["GET"], defaults={"object_type": "site"}) +@check_cruved_scope("R", module_code=MODULE_CODE, object_code="GNM_SITES") +def get_all_site_geometries(object_type): + object_code = "GNM_SITES" + params = MultiDict(request.args) + query = TMonitoringSites.query + query_allowed = query.filter_by_readable(object_code=object_code) + subquery = ( + query_allowed.with_entities( + TMonitoringSites.id_base_site, + TMonitoringSites.base_site_name, + TMonitoringSites.geom, + TMonitoringSites.id_sites_group, + ) + .filter_by_params(params) + .subquery() + ) + + result = geojson_query(subquery) + + return jsonify(result) + + +@blueprint.route("/sites//modules", methods=["GET"]) +@check_cruved_scope("R", module_code=MODULE_CODE, object_code="GNM_SITES") +def get_module_by_id_base_site(id_base_site: int): + modules_object = get_modules() + modules = get_objet_with_permission_boolean(modules_object, depth=0) + ids_modules_allowed = [module["id_module"] for module in modules if module["cruved"]["R"]] + query = TMonitoringModules.query.options( + Load(TMonitoringModules).raiseload("*"), + joinedload(TMonitoringModules.types_site).options(joinedload(BibTypeSite.sites)), + ).filter( + and_( + TMonitoringModules.id_module.in_(ids_modules_allowed), + TMonitoringModules.types_site.any(BibTypeSite.sites.any(id_base_site=id_base_site)), + ) + ) + schema = ModuleSchema() + result = query.all() + # TODO: Is it usefull to put a limit here? Will there be more than 200 modules? + # If limit here, implement paginated/infinite scroll on frontend side + return [schema.dump(res) for res in result] + + +# TODO: vérfier si c'est utilisé +@blueprint.route("/sites/module/", methods=["GET"]) +def get_module_sites(module_code: str): + # TODO: load with site_categories.json API + return jsonify({"module_code": module_code}) + + +@blueprint.route("/sites", methods=["POST"], defaults={"object_type": "site"}) +@check_cruved_scope("C", module_code=MODULE_CODE, object_code="GNM_SITES") +def post_sites(object_type): + module_code = "generic" + object_type = "site" + customConfig = {"specific": {}} + post_data = dict(request.get_json()) + for keys in post_data["dataComplement"].keys(): + if "config" in post_data["dataComplement"][keys]: + customConfig["specific"].update( + post_data["dataComplement"][keys]["config"]["specific"] + ) + get_config(module_code, force=True, customSpecConfig=customConfig) + return create_or_update_object_api_sites_sites_group(module_code, object_type), 201 + + +@blueprint.route("/sites/", methods=["DELETE"], defaults={"object_type": "site"}) +@permissions.check_cruved_scope( + "D", get_scope=True, module_code=MODULE_CODE, object_code="GNM_SITES" +) +def delete_site(scope, _id, object_type): + site = TMonitoringSites.query.get_or_404(_id) + if not site.has_instance_permission(scope=scope): + raise Forbidden(f"User {g.current_user} cannot delete site {site.id_base_site}") + TMonitoringSites.query.filter_by(id_base_site=_id).delete() + db.session.commit() + return {"success": "Item is successfully deleted"}, 200 + + +@blueprint.route("/sites/", methods=["PATCH"], defaults={"object_type": "site"}) +@permissions.check_cruved_scope( + "U", get_scope=True, module_code=MODULE_CODE, object_code="GNM_SITES" +) +def patch_sites(scope, _id, object_type): + site = TMonitoringSites.query.get_or_404(_id) + if not site.has_instance_permission(scope=scope): + raise Forbidden(f"User {g.current_user} cannot update site {site.id_base_site}") + module_code = "generic" + customConfig = {"specific": {}} + post_data = dict(request.get_json()) + # TODO: vérifier si utile et si oui mettre dans route POST + if "geometry" in post_data: + post_data["geometry"] = json.dumps(post_data["geometry"]) + for keys in post_data["dataComplement"].keys(): + if "config" in post_data["dataComplement"][keys]: + customConfig["specific"].update( + post_data["dataComplement"][keys]["config"]["specific"] + ) + get_config(module_code, force=True, customSpecConfig=customConfig) + return create_or_update_object_api_sites_sites_group(module_code, object_type, _id), 201 diff --git a/backend/gn_module_monitoring/routes/sites_groups.py b/backend/gn_module_monitoring/routes/sites_groups.py new file mode 100644 index 000000000..75fbba027 --- /dev/null +++ b/backend/gn_module_monitoring/routes/sites_groups.py @@ -0,0 +1,169 @@ +from flask import jsonify, request, g +from geonature.utils.env import db +from marshmallow import ValidationError +from sqlalchemy import func +from werkzeug.datastructures import MultiDict +from werkzeug.exceptions import Forbidden +from geonature.core.gn_permissions import decorators as permissions +from gn_module_monitoring.blueprint import blueprint +from gn_module_monitoring.config.repositories import get_config +from gn_module_monitoring.modules.repositories import get_module +from gn_module_monitoring.monitoring.definitions import monitoring_definitions +from gn_module_monitoring import MODULE_CODE +from geonature.core.gn_permissions.decorators import check_cruved_scope +from gn_module_monitoring.monitoring.models import TMonitoringSites, TMonitoringSitesGroups +from gn_module_monitoring.monitoring.schemas import MonitoringSitesGroupsSchema +from gn_module_monitoring.utils.errors.errorHandler import InvalidUsage +from gn_module_monitoring.utils.routes import ( + filter_params, + geojson_query, + get_limit_page, + get_sort, + paginate, + paginate_scope, + sort, + get_objet_with_permission_boolean, +) +from gn_module_monitoring.routes.monitoring import ( + create_or_update_object_api_sites_sites_group, + get_config_object, +) +from gn_module_monitoring.utils.utils import to_int + + +@blueprint.route("/sites_groups/config", methods=["GET"]) +def get_config_sites_groups(id=None, module_code="generic", object_type="sites_group"): + obj = get_config_object(module_code, object_type, id) + return obj["properties"] + + +@blueprint.route("/sites_groups", methods=["GET"], defaults={"object_type": "sites_group"}) +@check_cruved_scope("R", module_code=MODULE_CODE, object_code="GNM_GRP_SITES") +def get_sites_groups(object_type: str): + object_code = "GNM_GRP_SITES" + params = MultiDict(request.args) + limit, page = get_limit_page(params=params) + sort_label, sort_dir = get_sort( + params=params, default_sort="id_sites_group", default_direction="desc" + ) + query = filter_params(query=TMonitoringSitesGroups.query, params=params) + + query = sort(query=query, sort=sort_label, sort_dir=sort_dir) + + query_allowed = query.filter_by_readable(object_code=object_code) + return paginate_scope( + query=query_allowed, + schema=MonitoringSitesGroupsSchema, + limit=limit, + page=page, + object_code=object_code, + ) + # return paginate( + # query=query, + # schema=MonitoringSitesGroupsSchema, + # limit=limit, + # page=page, + # ) + + +@blueprint.route( + "/sites_groups/", methods=["GET"], defaults={"object_type": "sites_group"} +) +@check_cruved_scope("R", module_code=MODULE_CODE, object_code="GNM_GRP_SITES") +@permissions.check_cruved_scope( + "R", get_scope=True, module_code=MODULE_CODE, object_code="GNM_GRP_SITES" +) +def get_sites_group_by_id(scope, id_sites_group: int, object_type: str): + sites_group = TMonitoringSitesGroups.query.get_or_404(id_sites_group) + if not sites_group.has_instance_permission(scope=scope): + raise Forbidden( + f"User {g.current_user} cannot read site group {sites_group.id_sites_group}" + ) + schema = MonitoringSitesGroupsSchema() + result = TMonitoringSitesGroups.query.get_or_404(id_sites_group) + response = schema.dump(result) + response["cruved"] = get_objet_with_permission_boolean([result], object_code="GNM_GRP_SITES")[ + 0 + ]["cruved"] + return jsonify(response) + + +@blueprint.route( + "/sites_groups/geometries", methods=["GET"], defaults={"object_type": "sites_group"} +) +@check_cruved_scope("R", module_code=MODULE_CODE, object_code="GNM_GRP_SITES") +def get_sites_group_geometries(object_type: str): + object_code = "GNM_GRP_SITES" + query = TMonitoringSitesGroups.query + query_allowed = query.filter_by_readable(object_code=object_code) + subquery = ( + query_allowed.with_entities( + TMonitoringSitesGroups.id_sites_group, + TMonitoringSitesGroups.sites_group_name, + func.st_convexHull(func.st_collect(TMonitoringSites.geom)), + ) + .group_by(TMonitoringSitesGroups.id_sites_group, TMonitoringSitesGroups.sites_group_name) + .join( + TMonitoringSites, + TMonitoringSites.id_sites_group == TMonitoringSitesGroups.id_sites_group, + ) + .subquery() + ) + + result = geojson_query(subquery) + + return jsonify(result) + + +@blueprint.route( + "/sites_groups/", methods=["PATCH"], defaults={"object_type": "sites_group"} +) +@permissions.check_cruved_scope( + "U", get_scope=True, module_code=MODULE_CODE, object_code="GNM_GRP_SITES" +) +def patch(scope, _id: int, object_type: str): + # ###############################"" + # FROM route/monitorings + sites_group = TMonitoringSitesGroups.query.get_or_404(_id) + if not sites_group.has_instance_permission(scope=scope): + raise Forbidden( + f"User {g.current_user} cannot update site group {sites_group.id_sites_group}" + ) + + module_code = "generic" + get_config(module_code, force=True) + return create_or_update_object_api_sites_sites_group(module_code, object_type, _id), 201 + + +@blueprint.route( + "/sites_groups/", methods=["DELETE"], defaults={"object_type": "sites_group"} +) +@permissions.check_cruved_scope( + "D", get_scope=True, module_code=MODULE_CODE, object_code="GNM_GRP_SITES" +) +def delete(scope, _id: int, object_type: str): + sites_group = TMonitoringSitesGroups.query.get_or_404(_id) + if not sites_group.has_instance_permission(scope=scope): + raise Forbidden( + f"User {g.current_user} cannot delete site group {sites_group.id_sites_group}" + ) + TMonitoringSitesGroups.query.filter_by(id_sites_group=_id).delete() + db.session.commit() + return {"success": "Item is successfully deleted"}, 200 + + +@blueprint.route("/sites_groups", methods=["POST"], defaults={"object_type": "sites_group"}) +@check_cruved_scope("C", module_code=MODULE_CODE, object_code="GNM_GRP_SITES") +def post(object_type: str): + module_code = "generic" + get_config(module_code, force=True) + return create_or_update_object_api_sites_sites_group(module_code, object_type), 201 + + +@blueprint.errorhandler(ValidationError) +def handle_validation_error(error): + return InvalidUsage( + "Fields cannot be validated, message : {}".format(error.messages), + status_code=422, + payload=error.data, + ).to_dict() diff --git a/backend/gn_module_monitoring/routes/visit.py b/backend/gn_module_monitoring/routes/visit.py new file mode 100644 index 000000000..cafdd8fdc --- /dev/null +++ b/backend/gn_module_monitoring/routes/visit.py @@ -0,0 +1,52 @@ +from flask import request +from sqlalchemy.orm import joinedload +from werkzeug.datastructures import MultiDict + +from gn_module_monitoring.blueprint import blueprint +from gn_module_monitoring.monitoring.models import TMonitoringVisits +from gn_module_monitoring.monitoring.schemas import MonitoringVisitsSchema +from gn_module_monitoring.utils.routes import ( + filter_params, + get_limit_page, + get_sort, + paginate, + paginate_scope, + sort, + get_objet_with_permission_boolean, +) +from gn_module_monitoring.routes.modules import get_modules +from gn_module_monitoring.monitoring.definitions import MonitoringPermissions_dict + +# Retrieves visits that do not depend on modules +OBJECT_CODE = MonitoringPermissions_dict["visit"] + + +@blueprint.route("/visits", methods=["GET"], defaults={"object_type": "visit"}) +def get_visits(object_type): + params = MultiDict(request.args) + limit, page = get_limit_page(params=params) + sort_label, sort_dir = get_sort( + params=params, default_sort="id_base_visit", default_direction="desc" + ) + modules_object = get_modules() + modules = get_objet_with_permission_boolean(modules_object, object_code=OBJECT_CODE) + ids_modules_allowed = [module["id_module"] for module in modules if module["cruved"]["R"]] + query = TMonitoringVisits.query + query = query.options(joinedload(TMonitoringVisits.module)).filter( + TMonitoringVisits.id_module.in_(ids_modules_allowed) + ) + query = filter_params(query=query, params=params) + query = sort(query=query, sort=sort_label, sort_dir=sort_dir) + query_allowed = query + for module in modules: + if module["id_module"] in ids_modules_allowed: + query_allowed = query_allowed.filter_by_readable( + module_code=module["module_code"], object_code=OBJECT_CODE + ) + return paginate_scope( + query=query_allowed, + schema=MonitoringVisitsSchema, + limit=limit, + page=page, + object_code=OBJECT_CODE, + ) diff --git a/backend/gn_module_monitoring/tests/__init__.py b/backend/gn_module_monitoring/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/tests/fixtures/TestData/config_type_site.json b/backend/gn_module_monitoring/tests/fixtures/TestData/config_type_site.json new file mode 100644 index 000000000..05b07e4c5 --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/TestData/config_type_site.json @@ -0,0 +1,106 @@ +{ + "cruved": { + "C": 1, + "U": 1, + "D": 3 + }, + "sorts": [ + { + "prop": "base_site_name", + "dir": "asc" + } + ], + "filters": {}, + "label": "Gite", + "label_list": "Gites", + "genre": "M", + "geometry_type": "Point", + "display_properties": [ + "base_site_name", + "base_site_code", + "roost_type", + "nb_visits", + "threat", + "recommandation", + "opening", + "owner_name", + "owner_adress", + "owner_tel", + "owner_mail", + "medias" + ], + "display_list": [ + "base_site_name", + "roost_type", + "nb_visits", + "owner_name" + ], + "specific": { + "roost_type": { + "type_widget": "select", + "required": true, + "attribut_label": "Type de gite", + "values": [ + "barrage", + "bâtiment", + "cave", + "Ebouli", + "église", + "façade", + "four", + "garage", + "Gîte artificiel", + "grange", + "grenier", + "grotte", + "maison", + "mine", + "mur", + "panneau", + "plancher", + "pont", + "ruine", + "toit", + "toit et volet", + "transformateur", + "tunnel", + "volet", + "Autre" + ] + }, + "place_name": { + "type_widget": "text", + "attribut_label": "Lieux-dit" + }, + "owner_name": { + "type_widget": "text", + "attribut_label": "Nom propriétaire" + }, + "owner_adress": { + "type_widget": "text", + "attribut_label": "Adresse propriétaire" + }, + "owner_tel": { + "type_widget": "text", + "attribut_label": "Tel propriétaire" + }, + "owner_mail": { + "type_widget": "text", + "attribut_label": "Email propriétaire" + }, + "opening": { + "type_widget": "textarea", + "attribut_label": "Ouverture" + }, + "threat": { + "type_widget": "textarea", + "attribut_label": "Menace(s)", + "rows": 3 + }, + "recommandation": { + "type_widget": "textarea", + "attribut_label": "Mesure(s) préconisé(s)", + "rows": 3 + } + } + } \ No newline at end of file diff --git a/backend/gn_module_monitoring/tests/fixtures/__init__.py b/backend/gn_module_monitoring/tests/fixtures/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/tests/fixtures/module.py b/backend/gn_module_monitoring/tests/fixtures/module.py new file mode 100644 index 000000000..3a925cbcc --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/module.py @@ -0,0 +1,39 @@ +from uuid import uuid4 + +import pytest +from geonature.utils.env import db + +from gn_module_monitoring.monitoring.models import TMonitoringModules + + +@pytest.fixture +def monitoring_module(types_site): + t_monitoring_module = TMonitoringModules( + module_code=uuid4(), + module_label="test", + active_frontend=True, + active_backend=False, + module_path="test", + types_site=list(types_site.values()), + ) + + with db.session.begin_nested(): + db.session.add(t_monitoring_module) + + return t_monitoring_module + + +@pytest.fixture +def monitoring_module_wo_types_site(): + t_monitoring_module = TMonitoringModules( + module_code=uuid4(), + module_label="NoType", + active_frontend=True, + active_backend=False, + module_path="NoType", + ) + + with db.session.begin_nested(): + db.session.add(t_monitoring_module) + + return t_monitoring_module diff --git a/backend/gn_module_monitoring/tests/fixtures/site.py b/backend/gn_module_monitoring/tests/fixtures/site.py new file mode 100644 index 000000000..8c9f6ad1c --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/site.py @@ -0,0 +1,91 @@ +import pytest +from geoalchemy2.shape import from_shape +from geonature.utils.env import db +from shapely.geometry import Point + +from gn_module_monitoring.monitoring.models import TMonitoringSites +from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema, MonitoringSitesSchema + + +@pytest.fixture() +def sites(users, types_site, site_group_with_sites): + user = users["user"] + geom_4326 = from_shape(Point(43, 24), srid=4326) + sites = {} + for i, key in enumerate(types_site.keys()): + sites[key] = TMonitoringSites( + id_inventor=user.id_role, + id_digitiser=user.id_role, + base_site_name=f"Site{i}", + base_site_description=f"Description{i}", + base_site_code=f"Code{i}", + geom=geom_4326, + id_nomenclature_type_site=types_site[key].id_nomenclature_type_site, + types_site=[types_site[key]], + id_sites_group=site_group_with_sites.id_sites_group, + ) + + # Add a special site that has no type + sites["no-type"] = TMonitoringSites( + id_inventor=user.id_role, + id_digitiser=user.id_role, + base_site_name=f"no-type", + base_site_description=f"Description-no-type", + base_site_code=f"Code-no-type", + geom=geom_4326, + # Random id_nomenclature_type_site + # FIXME: when id_nomenclature_type_site disapears => remove this line + id_nomenclature_type_site=list(types_site.values())[0].id_nomenclature_type_site, + types_site=[], + id_sites_group=site_group_with_sites.id_sites_group, + ) + + with db.session.begin_nested(): + db.session.add_all(sites.values()) + return sites + + +@pytest.fixture() +def site_to_post_with_types(users, types_site, site_group_without_sites): + user = users["user"] + geom_4326 = from_shape(Point(43, 24), srid=4326) + list_nomenclature_id = [] + specific_dic = {"owner_name": "Propriétaire", "threat": "Menaces", "owner_tel": "0609090909"} + schema_type_site = BibTypeSiteSchema() + mock_db_type_site = [schema_type_site.dump(type) for type in types_site.values()] + + for type in mock_db_type_site: + list_nomenclature_id.append(type["id_nomenclature_type_site"]) + + site_to_post_with_types = TMonitoringSites( + id_inventor=user.id_role, + id_digitiser=user.id_role, + base_site_name=f"New Site", + base_site_description=f"New Description", + base_site_code=f"New Code", + geom=geom_4326, + id_nomenclature_type_site=list_nomenclature_id[0], + types_site=list_nomenclature_id, + id_sites_group=site_group_without_sites.id_sites_group, + ) + + post_data = dict() + post_data["dataComplement"] = {} + for type_site_dic in mock_db_type_site: + copy_dic = type_site_dic.copy() + copy_dic.pop("label") + post_data["dataComplement"][type_site_dic["label"]] = copy_dic + + post_data["dataComplement"]["types_site"] = list_nomenclature_id + post_data["properties"] = MonitoringSitesSchema().dump(site_to_post_with_types) + post_data["properties"]["types_site"] = list_nomenclature_id + + for type_site in mock_db_type_site: + specific_config = type_site["config"]["specific"] + for key_specific in specific_config: + if key_specific in specific_dic.keys(): + post_data["properties"][key_specific] = specific_dic[key_specific] + else: + post_data["properties"][key_specific] = None + + return post_data diff --git a/backend/gn_module_monitoring/tests/fixtures/sites_groups.py b/backend/gn_module_monitoring/tests/fixtures/sites_groups.py new file mode 100644 index 000000000..eb86a2128 --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/sites_groups.py @@ -0,0 +1,26 @@ +import pytest +from geonature.utils.env import db + +from gn_module_monitoring.monitoring.models import TMonitoringSitesGroups + + +@pytest.fixture +def sites_groups(): + names = ["Site_eolien", "Site_Groupe"] + + groups = {name: TMonitoringSitesGroups(sites_group_name=name) for name in names} + + with db.session.begin_nested(): + db.session.add_all(groups.values()) + + return groups + + +@pytest.fixture +def site_group_with_sites(sites_groups): + return sites_groups["Site_Groupe"] + + +@pytest.fixture +def site_group_without_sites(sites_groups): + return sites_groups["Site_eolien"] diff --git a/backend/gn_module_monitoring/tests/fixtures/type_site.py b/backend/gn_module_monitoring/tests/fixtures/type_site.py new file mode 100644 index 000000000..9aa191673 --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/type_site.py @@ -0,0 +1,53 @@ +import json +import os + +import pytest +from geonature.utils.env import db +from pypnnomenclature.models import BibNomenclaturesTypes, TNomenclatures + +from gn_module_monitoring.monitoring.models import BibTypeSite + + +def get_test_data(filename): + folder_path = os.path.abspath(os.path.dirname(__file__)) + folder = os.path.join(folder_path, "TestData") + jsonfile = os.path.join(folder, filename) + with open(jsonfile) as file: + data = json.load(file) + return data + + +@pytest.fixture +def nomenclature_types_site(): + mnemoniques = ("Test_Grotte", "Test_Mine") + nomenclatures = [] + type_site = BibNomenclaturesTypes.query.filter( + BibNomenclaturesTypes.mnemonique == "TYPE_SITE" + ).first() + for mnemo in mnemoniques: + nomenclatures.append( + TNomenclatures( + id_type=type_site.id_type, + cd_nomenclature=mnemo, + label_default=mnemo, + label_fr=mnemo, + active=True, + ) + ) + with db.session.begin_nested(): + db.session.add_all(nomenclatures) + return nomenclatures + + +@pytest.fixture +def types_site(nomenclature_types_site): + config_type_site = get_test_data("config_type_site.json") + types_site = { + nomenc_type_site.label_default: BibTypeSite( + id_nomenclature_type_site=nomenc_type_site.id_nomenclature, config=config_type_site + ) + for nomenc_type_site in nomenclature_types_site + } + with db.session.begin_nested(): + db.session.add_all(types_site.values()) + return types_site diff --git a/backend/gn_module_monitoring/tests/fixtures/visit.py b/backend/gn_module_monitoring/tests/fixtures/visit.py new file mode 100644 index 000000000..5404952b7 --- /dev/null +++ b/backend/gn_module_monitoring/tests/fixtures/visit.py @@ -0,0 +1,26 @@ +import datetime + +import pytest +from geonature.utils.env import db + +from gn_module_monitoring.monitoring.models import TMonitoringVisits + + +@pytest.fixture +def visits(module, users, types_site, sites, datasets): + now = datetime.datetime.now() + dataset = datasets["orphan_dataset"] + db_visits = [] + for site in sites.values(): + db_visits.append( + TMonitoringVisits( + id_base_site=site.id_base_site, + id_module=module.id_module, + id_dataset=dataset.id_dataset, + visit_date_min=now, + ) + ) + with db.session.begin_nested(): + db.session.add_all(db_visits) + + return db_visits diff --git a/backend/gn_module_monitoring/tests/test_monitoring/__init__.py b/backend/gn_module_monitoring/tests/test_monitoring/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_models/__init__.py b/backend/gn_module_monitoring/tests/test_monitoring/test_models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_bib_type_site.py b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_bib_type_site.py new file mode 100644 index 000000000..ae580ccbf --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_bib_type_site.py @@ -0,0 +1,23 @@ +import pytest + +from gn_module_monitoring.monitoring.models import BibTypeSite + + +@pytest.mark.usefixtures("temporary_transaction") +class TestBibTypeSite: + def test_get_bib_type_site(self, types_site): + type_site = list(types_site.values())[0] + get_type_site = BibTypeSite.query.filter_by( + id_nomenclature_type_site=type_site.id_nomenclature_type_site + ).one() + + assert get_type_site.id_nomenclature_type_site == type_site.id_nomenclature_type_site + + def test_get_all_bib_type_site(self, types_site): + get_types_site = BibTypeSite.query.all() + + assert all( + type_site.id_nomenclature_type_site + in [get_type_site.id_nomenclature_type_site for get_type_site in get_types_site] + for type_site in types_site.values() + ) diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_module.py b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_module.py new file mode 100644 index 000000000..978b9d1ca --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_module.py @@ -0,0 +1,19 @@ +import pytest +from geonature.utils.env import db + +from gn_module_monitoring.monitoring.models import TMonitoringModules + + +@pytest.mark.usefixtures("temporary_transaction") +class TestModule: + def test_module(self, monitoring_module, types_site): + types = monitoring_module.types_site + assert types == list(types_site.values()) + + def test_remove_categorie_from_module(self, monitoring_module, types_site): + with db.session.begin_nested(): + monitoring_module.types_site.pop(0) + + mon = TMonitoringModules.query.filter_by(id_module=monitoring_module.id_module).one() + + assert len(mon.types_site) == len(types_site) - 1 diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_sites_groups.py b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_sites_groups.py new file mode 100644 index 000000000..99a54f811 --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_monitoring/test_models/test_sites_groups.py @@ -0,0 +1,36 @@ +import pytest + +from gn_module_monitoring.monitoring.models import TMonitoringSitesGroups + + +@pytest.mark.usefixtures("temporary_transaction") +class TestTMonitoringSitesGroups: + def test_sort_desc(self, sites_groups): + if len(sites_groups) < 2: + pytest.xfail( + "This test cannot work if there is less than 2 sites_groups in database (via fixtures or not)" + ) + + query = TMonitoringSitesGroups.query.filter( + TMonitoringSitesGroups.id_sites_group.in_( + group.id_sites_group for group in sites_groups.values() + ) + ).sort(label="id_sites_group", direction="desc") + result = query.all() + + assert result[0].id_sites_group > result[1].id_sites_group + + def test_sort_asc(self, sites_groups): + if len(sites_groups) < 2: + pytest.xfail( + "This test cannot work if there is less than 2 sites_groups in database (via fixtures or not)" + ) + + query = TMonitoringSitesGroups.query.filter( + TMonitoringSitesGroups.id_sites_group.in_( + group.id_sites_group for group in sites_groups.values() + ) + ).sort(label="id_sites_group", direction="asc") + result = query.all() + + assert result[0].id_sites_group < result[1].id_sites_group diff --git a/backend/gn_module_monitoring/tests/test_monitoring/test_schemas/test_bib_site_type_schema.py b/backend/gn_module_monitoring/tests/test_monitoring/test_schemas/test_bib_site_type_schema.py new file mode 100644 index 000000000..91aa19e0f --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_monitoring/test_schemas/test_bib_site_type_schema.py @@ -0,0 +1,14 @@ +import pytest + +from gn_module_monitoring.monitoring.models import BibTypeSite +from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema + + +@pytest.mark.usefixtures("temporary_transaction") +class TestBibSiteTypeSchema: + def test_dump(self, types_site): + one_type_site = BibTypeSite.query.first() + schema = BibTypeSiteSchema() + type_site = schema.dump(one_type_site) + + assert type_site["id_nomenclature_type_site"] == one_type_site.id_nomenclature_type_site diff --git a/backend/gn_module_monitoring/tests/test_routes/__init__.py b/backend/gn_module_monitoring/tests/test_routes/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/tests/test_routes/test_site.py b/backend/gn_module_monitoring/tests/test_routes/test_site.py new file mode 100644 index 000000000..ea296fc03 --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_routes/test_site.py @@ -0,0 +1,191 @@ +import pytest +from flask import url_for + +from gn_module_monitoring.monitoring.models import TMonitoringSites +from gn_module_monitoring.monitoring.schemas import BibTypeSiteSchema, MonitoringSitesSchema +from gn_module_monitoring.monitoring.models import TMonitoringSites + + +@pytest.mark.usefixtures("client_class", "temporary_transaction") +class TestSite: + def test_get_type_site_by_id(self, types_site): + for type_site in types_site.values(): + r = self.client.get( + url_for( + "monitorings.get_type_site_by_id", + id_type_site=type_site.id_nomenclature_type_site, + ) + ) + assert r.json["id_nomenclature_type_site"] == type_site.id_nomenclature_type_site + + def test_get_types_site(self, types_site): + schema = BibTypeSiteSchema() + + r = self.client.get(url_for("monitorings.get_types_site")) + + assert r.json["count"] >= len(types_site) + assert all([schema.dump(cat) in r.json["items"] for cat in types_site.values()]) + + def test_get_sites(self, sites): + schema = MonitoringSitesSchema() + + r = self.client.get(url_for("monitorings.get_sites")) + + assert r.json["count"] >= len(sites) + assert any([schema.dump(site) in r.json["items"] for site in sites.values()]) + + def test_get_sites_limit(self, sites): + limit = 34 + + r = self.client.get(url_for("monitorings.get_sites", limit=limit)) + + assert len(r.json["items"]) == limit + + def test_get_sites_base_site_name(self, sites): + site = list(sites.values())[0] + base_site_name = site.base_site_name + + r = self.client.get(url_for("monitorings.get_sites", base_site_name=base_site_name)) + + assert len(r.json["items"]) == 1 + assert r.json["items"][0]["base_site_name"] == base_site_name + + def test_get_sites_id_base_site(self, sites): + site = list(sites.values())[0] + id_base_site = site.id_base_site + + r = self.client.get(url_for("monitorings.get_sites", id_base_site=id_base_site)) + + assert len(r.json["items"]) == 1 + assert r.json["items"][0]["id_base_site"] == id_base_site + + def test_get_sites_by_id(self, sites): + site = list(sites.values())[0] + id_base_site = site.id_base_site + + r = self.client.get(url_for("monitorings.get_site_by_id", id_base_site=id_base_site)) + + assert r.json["id_base_site"] == id_base_site + + def test_get_all_site_geometries(self, sites): + r = self.client.get(url_for("monitorings.get_all_site_geometries")) + + json_resp = r.json + features = json_resp.get("features") + sites_values = list(sites.values()) + assert r.content_type == "application/json" + assert json_resp.get("type") == "FeatureCollection" + assert len(features) >= len(sites_values) + for site in sites_values: + id_ = [ + obj["properties"] + for obj in features + if obj["properties"]["base_site_name"] == site.base_site_name + ][0]["id_base_site"] + assert id_ == site.id_base_site + + def test_get_all_site_geometries_filter_site_group(self, sites, site_group_without_sites): + r = self.client.get( + url_for( + "monitorings.get_all_site_geometries", + id_sites_group=site_group_without_sites.id_sites_group, + ) + ) + json_resp = r.json + features = json_resp.get("features") + assert features is None + + def test_get_module_by_id_base_site(self, sites, monitoring_module): + site = list(sites.values())[0] + id_base_site = site.id_base_site + + r = self.client.get( + url_for("monitorings.get_module_by_id_base_site", id_base_site=id_base_site) + ) + + expected_modules = {monitoring_module.id_module} + current_modules = {module["id_module"] for module in r.json} + assert expected_modules.issubset(current_modules) + + def test_get_module_by_id_base_site_no_type_module( + self, sites, monitoring_module_wo_types_site + ): + site = list(sites.values())[0] + id_base_site = site.id_base_site + + r = self.client.get( + url_for("monitorings.get_module_by_id_base_site", id_base_site=id_base_site) + ) + + expected_absent_modules = {monitoring_module_wo_types_site.id_module} + current_modules = {module["id_module"] for module in r.json} + assert expected_absent_modules.isdisjoint(current_modules) + + def test_get_module_by_id_base_site_no_type_site(self, sites, monitoring_module): + id_base_site = sites["no-type"].id_base_site + + r = self.client.get( + url_for("monitorings.get_module_by_id_base_site", id_base_site=id_base_site) + ) + + expected_modules = {monitoring_module.id_module} + current_modules = {module["id_module"] for module in r.json} + assert expected_modules.isdisjoint(current_modules) + + def test_get_module_sites(self): + module_code = "TEST" + r = self.client.get(url_for("monitorings.get_module_sites", module_code=module_code)) + + assert r.json["module_code"] == module_code + + def test_get_types_site_by_label(self, types_site): + schema = BibTypeSiteSchema() + mock_db_type_site = [schema.dump(type) for type in types_site.values()] + string_contains = "e" + string_missing = "a" + + query_string = { + "limit": 100, + "page": 1, + "sort_label": "label_fr", + "sort_dir": "asc", + "label_fr": string_contains, + } + r = self.client.get( + url_for("monitorings.get_types_site_by_label"), query_string=query_string + ) + assert all([string_contains in item["label"] for item in r.json["items"]]) + assert all([type in r.json["items"] for type in mock_db_type_site]) + + query_string["label_fr"] = string_missing + r = self.client.get( + url_for("monitorings.get_types_site_by_label"), query_string=query_string + ) + assert all([type not in r.json["items"] for type in mock_db_type_site]) + + def test_post_sites(self, site_to_post_with_types, types_site, site_group_without_sites): + response = self.client.post( + url_for("monitorings.post_sites"), data=site_to_post_with_types + ) + assert response.status_code == 201 + + obj_created = response.json + res = TMonitoringSites.find_by_id(obj_created["id"]) + assert ( + res.as_dict()["base_site_name"] + == site_to_post_with_types["properties"]["base_site_name"] + ) + + def test_delete_site(self, sites): + site = list(sites.values())[0] + id_base_site = site.id_base_site + item = TMonitoringSites.find_by_id(id_base_site) + r = self.client.delete(url_for("monitorings.delete_site", _id=id_base_site)) + + assert ( + r.json["success"] + == f"Item with {item.id_g} from table {item.__tablename__} is successfully deleted" + ) + with pytest.raises(Exception) as e: + TMonitoringSites.query.get_or_404(id_base_site) + assert "404 Not Found" in str(e.value) diff --git a/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py b/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py new file mode 100644 index 000000000..0d5d4f2e3 --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_routes/test_sites_groups.py @@ -0,0 +1,66 @@ +import pytest +from flask import url_for + +from gn_module_monitoring.monitoring.models import TMonitoringSitesGroups +from gn_module_monitoring.monitoring.schemas import MonitoringSitesGroupsSchema + + +@pytest.mark.usefixtures("client_class", "temporary_transaction") +class TestSitesGroups: + def test_get_sites_group_by_id(self, sites_groups): + sites_group = list(sites_groups.values())[0] + id_sites_group = sites_group.id_sites_group + r = self.client.get( + url_for("monitorings.get_sites_group_by_id", id_sites_group=id_sites_group) + ) + + assert r.json["id_sites_group"] == id_sites_group + assert r.json["sites_group_name"] == sites_group.sites_group_name + + def test_get_sites_groups(self, sites_groups): + r = self.client.get(url_for("monitorings.get_sites_groups")) + + assert r.json["count"] >= len(sites_groups) + assert all( + [ + MonitoringSitesGroupsSchema().dump(group) in r.json["items"] + for group in sites_groups.values() + ] + ) + + def test_get_sites_groups_filter_name(self, sites_groups): + name, name_not_present = list(sites_groups.keys()) + schema = MonitoringSitesGroupsSchema() + + r = self.client.get( + url_for("monitorings.get_sites_groups"), query_string={"sites_group_name": name} + ) + + assert r.json["count"] >= 1 + json_sites_groups = r.json["items"] + assert schema.dump(sites_groups[name]) in json_sites_groups + assert schema.dump(sites_groups[name_not_present]) not in json_sites_groups + + def test_serialize_sites_groups(self, sites_groups, sites): + groups = TMonitoringSitesGroups.query.filter( + TMonitoringSitesGroups.id_sites_group.in_( + [s.id_sites_group for s in sites_groups.values()] + ) + ).all() + schema = MonitoringSitesGroupsSchema() + assert [schema.dump(site) for site in groups] + + def test_get_sites_groups_geometries(self, sites, site_group_with_sites): + r = self.client.get(url_for("monitorings.get_sites_group_geometries")) + + json_resp = r.json + features = json_resp.get("features") + assert r.content_type == "application/json" + assert json_resp.get("type") == "FeatureCollection" + assert len(features) >= 1 + id_ = [ + obj["properties"] + for obj in features + if obj["properties"]["sites_group_name"] == site_group_with_sites.sites_group_name + ][0]["id_sites_group"] + assert id_ == site_group_with_sites.id_sites_group diff --git a/backend/gn_module_monitoring/tests/test_routes/test_visit.py b/backend/gn_module_monitoring/tests/test_routes/test_visit.py new file mode 100644 index 000000000..67de0f658 --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_routes/test_visit.py @@ -0,0 +1,29 @@ +import pytest +from flask import url_for + + +@pytest.mark.usefixtures("client_class", "temporary_transaction") +class TestVisits: + def test_get_visits(self, visits): + r = self.client.get( + url_for( + "monitorings.get_visits", + ) + ) + + expected_visits = {visit.id_base_visit for visit in visits} + current_visits = {visit["id_base_visit"] for visit in r.json["items"]} + assert expected_visits.issubset(current_visits) + assert all(visit["module"] is not None for visit in r.json["items"]) + + def test_get_visits_with_site(self, visits, sites): + site = list(sites.values())[0] + + r = self.client.get(url_for("monitorings.get_visits", id_base_site=site.id_base_site)) + + expected_visits = { + visit.id_base_visit for visit in visits if visit.id_base_site == site.id_base_site + } + current_visits = {visit["id_base_visit"] for visit in r.json["items"]} + + assert expected_visits.issubset(current_visits) diff --git a/backend/gn_module_monitoring/tests/test_utils/__init__.py b/backend/gn_module_monitoring/tests/test_utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/tests/test_utils/test_routes.py b/backend/gn_module_monitoring/tests/test_utils/test_routes.py new file mode 100644 index 000000000..e81cb8017 --- /dev/null +++ b/backend/gn_module_monitoring/tests/test_utils/test_routes.py @@ -0,0 +1,27 @@ +import pytest +from werkzeug.datastructures import MultiDict + +from gn_module_monitoring.monitoring.models import TMonitoringSites +from gn_module_monitoring.monitoring.schemas import MonitoringSitesSchema +from gn_module_monitoring.utils.routes import get_limit_page, paginate + + +@pytest.mark.parametrize("limit, page", [("1", "2"), (1, 2), ("1", 2), (1, "2")]) +def test_get_limit_page(limit, page): + multi_dict = MultiDict([("limit", limit), ("page", page)]) + + comp_limit, comp_page = get_limit_page(params=multi_dict) + + assert isinstance(comp_limit, int) + assert isinstance(comp_page, int) + + +def test_paginate(sites): + limit = 1 + page = 2 + + res = paginate( + query=TMonitoringSites.query, schema=MonitoringSitesSchema, limit=limit, page=page + ) + + assert res.json["page"] == page diff --git a/backend/gn_module_monitoring/utils/errors/__init__.py b/backend/gn_module_monitoring/utils/errors/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/gn_module_monitoring/utils/errors/errorHandler.py b/backend/gn_module_monitoring/utils/errors/errorHandler.py new file mode 100644 index 000000000..a10b13987 --- /dev/null +++ b/backend/gn_module_monitoring/utils/errors/errorHandler.py @@ -0,0 +1,19 @@ +from geonature.utils.errors import GeonatureApiError + + +class InvalidUsage(GeonatureApiError): + status_code = 400 + + def __init__(self, message, status_code=None, payload=None): + GeonatureApiError.__init__(self, message, status_code) + self.message = message + if status_code is not None: + self.status_code = status_code + self.payload = payload + + def to_dict(self): + rv = {} + rv["payload"] = self.payload + rv["message"] = self.message + rv["status_code"] = self.status_code + return (rv, self.status_code) diff --git a/backend/gn_module_monitoring/utils/routes.py b/backend/gn_module_monitoring/utils/routes.py new file mode 100644 index 000000000..1870371ca --- /dev/null +++ b/backend/gn_module_monitoring/utils/routes.py @@ -0,0 +1,263 @@ +from typing import Tuple + +from sqlalchemy import and_ +from flask import Response, g +from flask.json import jsonify +from geonature.utils.env import DB +from pypnusershub.db.models import User +from gn_module_monitoring.monitoring.models import ( + BibTypeSite, + TMonitoringSites, + TMonitoringSitesGroups, + cor_type_site, + TBaseSites, + cor_module_type, + TModules, + TNomenclatures, +) +from geonature.core.gn_permissions.models import TObjects, PermObject, PermissionAvailable +from geonature.utils.errors import GeoNatureError +from marshmallow import Schema +from sqlalchemy import cast, func, text +from sqlalchemy.dialects.postgresql import JSON +from sqlalchemy.orm import Query, load_only, joinedload +from werkzeug.datastructures import MultiDict + +from gn_module_monitoring.monitoring.queries import Query as MonitoringQuery +from gn_module_monitoring.monitoring.schemas import paginate_schema + + +def get_limit_page(params: MultiDict) -> Tuple[int]: + return int(params.pop("limit", 50)), int(params.pop("page", 1)) + + +def get_sort(params: MultiDict, default_sort: str, default_direction) -> Tuple[str]: + return params.pop("sort", default_sort), params.pop("sort_dir", default_direction) + + +def paginate(query: Query, schema: Schema, limit: int, page: int) -> Response: + result = query.paginate(page=page, error_out=False, per_page=limit) + pagination_schema = paginate_schema(schema) + data = pagination_schema().dump( + dict(items=result.items, count=result.total, limit=limit, page=page) + ) + return jsonify(data) + + +def paginate_scope( + query: Query, schema: Schema, limit: int, page: int, object_code=None +) -> Response: + result = query.paginate(page=page, error_out=False, per_page=limit) + pagination_schema = paginate_schema(schema) + datas_allowed = pagination_schema().dump( + dict(items=result.items, count=result.total, limit=limit, page=page) + ) + cruved_item_dict = get_objet_with_permission_boolean(result.items, object_code=object_code) + for cruved_item in cruved_item_dict: + for i, data in enumerate(datas_allowed["items"]): + if data[data["pk"]] == cruved_item[data["pk"]]: + datas_allowed["items"][i]["cruved"] = cruved_item["cruved"] + return jsonify(datas_allowed) + + +def filter_params(query: MonitoringQuery, params: MultiDict) -> MonitoringQuery: + if len(params) != 0: + query = query.filter_by_params(params) + return query + + +def sort(query: MonitoringQuery, sort: str, sort_dir: str) -> MonitoringQuery: + if sort_dir in ["desc", "asc"]: + query = query.sort(label=sort, direction=sort_dir) + return query + + +def geojson_query(subquery) -> bytes: + subquery_name = "q" + subquery = subquery.alias(subquery_name) + query = DB.session.query( + func.json_build_object( + text("'type'"), + text("'FeatureCollection'"), + text("'features'"), + func.json_agg(cast(func.st_asgeojson(subquery), JSON)), + ) + ) + result = query.first() + if len(result) > 0: + return result[0] + return b"" + + +def get_sites_groups_from_module_id(module_id: int): + query = ( + TMonitoringSitesGroups.query.options( + # Load(TMonitoringSitesGroups).raiseload("*"), + load_only(TMonitoringSitesGroups.id_sites_group) + ) + .join( + TMonitoringSites, + TMonitoringSites.id_sites_group == TMonitoringSitesGroups.id_sites_group, + ) + .join(cor_type_site, cor_type_site.c.id_base_site == TBaseSites.id_base_site) + .join( + BibTypeSite, + BibTypeSite.id_nomenclature_type_site == cor_type_site.c.id_type_site, + ) + .join( + cor_module_type, + cor_module_type.c.id_type_site == BibTypeSite.id_nomenclature_type_site, + ) + .join(TModules, TModules.id_module == cor_module_type.c.id_module) + .filter(TModules.id_module == module_id) + ) + + return query.all() + + +def query_all_types_site_from_site_id(id_site: int): + query = ( + BibTypeSite.query.join( + cor_type_site, + BibTypeSite.id_nomenclature_type_site == cor_type_site.c.id_type_site, + ) + .join(TBaseSites, cor_type_site.c.id_base_site == TBaseSites.id_base_site) + .filter(cor_type_site.c.id_base_site == id_site) + ) + return query.all() + + +def query_all_types_site_from_module_id(id_module: int): + query = ( + BibTypeSite.query.join( + cor_module_type, + BibTypeSite.id_nomenclature_type_site == cor_module_type.c.id_type_site, + ) + .join(TModules, cor_module_type.c.id_module == TModules.id_module) + .filter(cor_module_type.c.id_module == id_module) + ) + return query.all() + + +def filter_according_to_column_type_for_site(query, params): + if "types_site" in params: + params_types_site = params.pop("types_site") + query = ( + query.join(TMonitoringSites.types_site) + .join(BibTypeSite.nomenclature) + .filter(TNomenclatures.label_fr.ilike(f"%{params_types_site}%")) + ) + elif "id_inventor" in params: + params_inventor = params.pop("id_inventor") + query = query.join( + User, + User.id_role == TMonitoringSites.id_inventor, + ).filter(User.nom_complet.ilike(f"%{params_inventor}%")) + if len(params) != 0: + query = filter_params(query=query, params=params) + + return query + + +def sort_according_to_column_type_for_site(query, sort_label, sort_dir): + if sort_label == "types_site": + if sort_dir == "asc": + query = query.order_by(TNomenclatures.label_fr.asc()) + else: + query = query.order_by(TNomenclatures.label_fr.desc()) + elif sort_label == "id_inventor": + if sort_dir == "asc": + query = query.order_by(User.nom_complet.asc()) + else: + query = query.order_by(User.nom_complet.desc()) + else: + query = sort(query=query, sort=sort_label, sort_dir=sort_dir) + return query + + +def get_object_list_monitorings(): + """ + récupère objets permissions liés au module MONITORINGS + + :return: + """ + try: + object_list_monitorings = ( + DB.session.query( + PermObject.code_object, + ) + .join(PermissionAvailable, PermissionAvailable.id_object == PermObject.id_object) + .join( + TModules, + and_( + TModules.id_module == PermissionAvailable.id_module, + TModules.module_code == "MONITORINGS", + ), + ) + .group_by(PermObject.code_object) + .all() + ) + return object_list_monitorings + except Exception as e: + raise GeoNatureError("MONITORINGS - get_object_list_monitorings : {}".format(str(e))) + + +def get_objet_with_permission_boolean( + objects, depth: int = 0, module_code=None, object_code=None, id_role=None +): + if id_role is None: + id_role = g.current_user.id_role + objects_out = [] + for object in objects: + if hasattr(object, "module"): + cruved_object = object.query._get_cruved_scope( + module_code=object.module.module_code, object_code=object_code + ) + else: + cruved_object = object.query._get_cruved_scope(object_code=object_code) + object_out = object.as_dict(depth=depth) + if hasattr(object, "module_code"): + object_out["cruved"] = object.get_permission_by_action( + module_code=object.module_code, object_code=object_code + ) + else: + object_out["cruved"] = object.has_permission(cruved_object=cruved_object) + objects_out.append(object_out) + + return objects_out + + +# from gn_module_monitoring.monitoring.definitions import MonitoringPermissions_dict +# from gn_module_monitoring import MODULE_CODE +# def set_permission_global_session(module_code=None,object_type=None): +# # requested_module_code = values.get("module_code") or MODULE_CODE +# module_code = module_code or MODULE_CODE +# current_module = ( +# TModules.query.options(joinedload(TModules.objects)) +# .filter_by(module_code=module_code) +# .first_or_404(f"No module with code {module_code}") +# ) +# g.current_module = current_module + +# # recherche de l'object de permission courrant +# # object_type = values.get("object_type") +# if object_type: +# requested_permission_object_code = MonitoringPermissions_dict.get(object_type) + +# if requested_permission_object_code is None: +# # error ? +# return + +# # Test si l'object de permission existe +# requested_permission_object = TObjects.query.filter_by( +# code_object=requested_permission_object_code +# ).first_or_404( +# f"No permission object with code {requested_permission_object_code}" +# ) + +# # si l'object de permission est associé au module => il devient l'objet courant +# # - sinon se sera 'ALL' par defaut +# for module_perm_object in current_module.objects: +# if module_perm_object == requested_permission_object: +# g.current_object = requested_permission_object +# return diff --git a/conftest.py b/conftest.py new file mode 100644 index 000000000..92ab61efc --- /dev/null +++ b/conftest.py @@ -0,0 +1,10 @@ +from geonature.tests.fixtures import * +from geonature.tests.fixtures import _session, app, users + +pytest_plugins = [ + "gn_module_monitoring.tests.fixtures.module", + "gn_module_monitoring.tests.fixtures.site", + "gn_module_monitoring.tests.fixtures.sites_groups", + "gn_module_monitoring.tests.fixtures.type_site", + "gn_module_monitoring.tests.fixtures.visit", +] diff --git a/frontend/.prettierrc b/frontend/.prettierrc index 6365e3241..21f11dea9 100644 --- a/frontend/.prettierrc +++ b/frontend/.prettierrc @@ -5,5 +5,5 @@ "tabWidth": 2, "semi": true, "bracketSpacing": true, - "trailingComma" : "es5" + "trailingComma": "es5" } diff --git a/frontend/app/class/breadCrumb.ts b/frontend/app/class/breadCrumb.ts new file mode 100644 index 000000000..33c8e312f --- /dev/null +++ b/frontend/app/class/breadCrumb.ts @@ -0,0 +1,28 @@ +const getElementSiteGroupBC = { + description: 'Liste des groupes de site', + label: '', + url: 'sites_group', +}; + +const getElementSiteBC = { + description: 'Liste des sites', + label: '', + url: 'sites', +}; + +export class breadCrumbBase { + static readonly baseBreadCrumbSiteGroups = new breadCrumbBase( + 'baseBreadCrumbSiteGroups', + getElementSiteGroupBC + ); + static readonly baseBreadCrumbSites = new breadCrumbBase('baseBreadCrumbSites', getElementSiteBC); + // private to disallow creating other instances of this type + private constructor( + private readonly key: string, + public readonly value: any + ) {} + + toString() { + return this.key; + } +} diff --git a/frontend/app/class/monitoring-geom-component.ts b/frontend/app/class/monitoring-geom-component.ts new file mode 100644 index 000000000..cabf070cd --- /dev/null +++ b/frontend/app/class/monitoring-geom-component.ts @@ -0,0 +1,30 @@ +import { PageInfo } from '../interfaces/page'; +import { JsonData } from '../types/jsondata'; + +const LIMIT = 10; + +type callbackFunction = (pageNumber: number, filters: JsonData, tabObj: string) => void; + +export class MonitoringGeomComponent { + protected getAllItemsCallback: callbackFunction; + protected limit = LIMIT; + public filters = {}; + public baseFilters = {}; + + constructor() {} + + setPage({ page, tabObj = '' }) { + this.getAllItemsCallback(page.offset + 1, this.filters, tabObj); + } + + setSort({ filters, tabObj = '' }) { + this.filters = { ...this.baseFilters, ...filters }; + const pageNumber = 1; + this.getAllItemsCallback(pageNumber, this.filters, tabObj); + } + + setFilter({ filters, tabObj = '' }) { + this.filters = { ...this.baseFilters, ...filters }; + this.getAllItemsCallback(1, this.filters, tabObj); + } +} diff --git a/frontend/app/class/monitoring-object-base.ts b/frontend/app/class/monitoring-object-base.ts index b40b2b2c6..4341c281b 100644 --- a/frontend/app/class/monitoring-object-base.ts +++ b/frontend/app/class/monitoring-object-base.ts @@ -1,18 +1,15 @@ -import { Observable, of } from 'rxjs'; -import { concatMap } from 'rxjs/operators'; -import { threadId } from 'worker_threads'; -import { forkJoin } from 'rxjs'; import { MonitoringObjectService } from '../services/monitoring-object.service'; import { Utils } from '../utils/utils'; - +import { Observable, of } from 'rxjs'; +import { forkJoin } from 'rxjs'; +import { concatMap } from 'rxjs/operators'; export class MonitoringObjectBase { moduleCode: string; objectType: string; id: number; // id de l'objet - + cruved: Object; parentsPath = []; - userCruved; userCruvedObject; deleted = false; @@ -33,6 +30,7 @@ export class MonitoringObjectBase { siteId; template = {}; + template_specific = {}; // configParams = ["geometry_type", "chained"]; config = {}; @@ -123,7 +121,6 @@ export class MonitoringObjectBase { } setData(data) { - this.userCruved = data.cruved; this.userCruvedObject = data.cruved_objects; this.properties = data.properties || {}; this.geometry = data.geometry; @@ -131,6 +128,7 @@ export class MonitoringObjectBase { this.medias = data.medias; this.siteId = data.site_id; this.idTableLocation = data.id_table_location; + this.cruved = data.cruved; } idFieldName() { @@ -171,6 +169,10 @@ export class MonitoringObjectBase { setResolvedProperties(): Observable { const observables = {}; const schema = this.schema(); + + if (Object.keys(this.template_specific).length > 0) { + Object.assign(schema, this.template_specific['schema']); + } for (const attribut_name of Object.keys(schema)) { observables[attribut_name] = this.resolveProperty( schema[attribut_name], @@ -193,11 +195,6 @@ export class MonitoringObjectBase { .configModuleObjectParam(this.moduleCode, this.objectType, fieldName); } - cruved(c = null) { - const cruved = this.configParam('cruved') || {}; - return c ? (![undefined, null].includes(cruved[c]) ? cruved[c] : 1) : cruved; - } - childrenTypes(configParam: string = null): Array { let childrenTypes = this.configParam('children_types') || []; @@ -375,9 +372,10 @@ export class MonitoringObjectBase { ); } - navigateToDetail(id = null) { + navigateToDetail(id = null, toEdit = false) { this._objService.navigate('object', this.moduleCode, this.objectType, id || this.id, { parents_path: this.parentsPath, + edit: toEdit, }); } diff --git a/frontend/app/class/monitoring-object.ts b/frontend/app/class/monitoring-object.ts index 93a48a91e..e396ff453 100644 --- a/frontend/app/class/monitoring-object.ts +++ b/frontend/app/class/monitoring-object.ts @@ -1,10 +1,8 @@ -import { Observable, of, forkJoin } from 'rxjs'; -import { mergeMap, concatMap } from 'rxjs/operators'; - import { MonitoringObjectService } from '../services/monitoring-object.service'; import { Utils } from '../utils/utils'; - import { MonitoringObjectBase } from './monitoring-object-base'; +import { Observable, of, forkJoin } from 'rxjs'; +import { mergeMap, concatMap } from 'rxjs/operators'; export class MonitoringObject extends MonitoringObjectBase { myClass = MonitoringObject; @@ -94,10 +92,10 @@ export class MonitoringObject extends MonitoringObjectBase { ); } - post(formValue): Observable { + post(formValue, dataComplement = {}): Observable { return this._objService .dataMonitoringObjectService() - .postObject(this.moduleCode, this.objectType, this.postData(formValue)) + .postObject(this.moduleCode, this.objectType, this.postData(formValue, dataComplement)) .pipe( mergeMap((postData) => { this.id = postData['id']; @@ -107,10 +105,15 @@ export class MonitoringObject extends MonitoringObjectBase { ); } - patch(formValue) { + patch(formValue, dataComplement = {}) { return this._objService .dataMonitoringObjectService() - .patchObject(this.moduleCode, this.objectType, this.id, this.postData(formValue)) + .patchObject( + this.moduleCode, + this.objectType, + this.id, + this.postData(formValue, dataComplement) + ) .pipe( mergeMap((postData) => { this._objService.setCache(this, postData); @@ -196,10 +199,16 @@ export class MonitoringObject extends MonitoringObjectBase { /** formValues: obj -> from */ - formValues(): Observable { + formValues(schemaUpdate = {}): Observable { const properties = Utils.copy(this.properties); const observables = {}; - const schema = this.schema(); + let schema = {}; + if (Object.keys(schemaUpdate).length == 0) { + schema = this.schema(); + } else { + schema = schemaUpdate; + } + for (const attribut_name of Object.keys(schema)) { const elem = schema[attribut_name]; if (!elem.type_widget) { @@ -222,7 +231,7 @@ export class MonitoringObject extends MonitoringObjectBase { /** postData: obj -> from */ - postData(formValue) { + postData(formValue, dataComplement) { const propertiesData = {}; const schema = this.schema(); for (const attribut_name of Object.keys(schema)) { @@ -233,10 +242,19 @@ export class MonitoringObject extends MonitoringObjectBase { propertiesData[attribut_name] = this._objService.fromForm(elem, formValue[attribut_name]); } - const postData = { - properties: propertiesData, - // id_parent: this.parentId - }; + let postData = {}; + if (Object.keys(dataComplement).length == 0) { + postData = { + properties: propertiesData, + // id_parent: this.parentId + }; + } else { + postData = { + properties: propertiesData, + dataComplement: dataComplement, + // id_parent: this.parentId + }; + } if (this.config['geometry_type']) { postData['geometry'] = formValue['geometry']; @@ -291,6 +309,7 @@ export class MonitoringObject extends MonitoringObjectBase { (fieldName) => child.resolvedProperties[fieldName] ); row['id'] = child.id; + row['cruved'] = child.cruved; return row; }); } diff --git a/frontend/app/class/monitoring-visit.ts b/frontend/app/class/monitoring-visit.ts new file mode 100644 index 000000000..5f6c5b4fa --- /dev/null +++ b/frontend/app/class/monitoring-visit.ts @@ -0,0 +1,6 @@ +export enum columnNameVisit { + id_module = 'Protocol ID', + visit_date_max = 'Date max', + visit_date_min = 'Date min', + nb_observations = 'Nb. observations', +} diff --git a/frontend/app/components/breadcrumbs/breadcrumbs.component.css b/frontend/app/components/breadcrumbs/breadcrumbs.component.css index bda26d020..db4e0b17a 100644 --- a/frontend/app/components/breadcrumbs/breadcrumbs.component.css +++ b/frontend/app/components/breadcrumbs/breadcrumbs.component.css @@ -8,8 +8,8 @@ .breadcrumbs .link { cursor: pointer; color: rgb(0, 123, 255); -} +} .breadcrumbs .link:hover { text-decoration: underline; -} +} diff --git a/frontend/app/components/breadcrumbs/breadcrumbs.component.ts b/frontend/app/components/breadcrumbs/breadcrumbs.component.ts index 0465da827..854c824b7 100644 --- a/frontend/app/components/breadcrumbs/breadcrumbs.component.ts +++ b/frontend/app/components/breadcrumbs/breadcrumbs.component.ts @@ -9,6 +9,12 @@ import { ConfigService } from '../../services/config.service'; import { MonitoringObject } from '../../class/monitoring-object'; import { Router } from '@angular/router'; import { ActivatedRoute } from '@angular/router'; +import { ObjectService } from '../../services/object.service'; +import { SiteSiteGroup } from '../../interfaces/objObs'; +import { IBreadCrumb } from '../../interfaces/object'; +import { breadCrumbBase } from '../../class/breadCrumb'; + +export const breadCrumbElementBase: IBreadCrumb = breadCrumbBase.baseBreadCrumbSiteGroups.value; @Component({ selector: 'pnx-monitoring-breadcrumbs', @@ -16,24 +22,32 @@ import { ActivatedRoute } from '@angular/router'; styleUrls: ['./breadcrumbs.component.css'], }) export class BreadcrumbsComponent implements OnInit { - public breadcrumbs; - + public breadcrumbs: IBreadCrumb[] = []; @Input() bEdit: boolean; @Output() bEditChange = new EventEmitter(); public frontendModuleMonitoringUrl: string; - + public newLabel: string; + public new_desc: string; @Input() obj: MonitoringObject; + // Specific to the site access + siteSiteGroup: SiteSiteGroup | null = null; constructor( private _dataMonitoringObjectService: DataMonitoringObjectService, private _configService: ConfigService, private _router: Router, - private _route: ActivatedRoute + private _route: ActivatedRoute, + private _objectService: ObjectService ) {} ngOnInit() { - // this.initBreadcrumbs(); + if (this.obj === undefined) { + this._objectService.currentDataBreadCrumb.subscribe( + (breadCrumb) => (this.breadcrumbs = breadCrumb) + ); + return; + } } initBreadcrumbs() { @@ -68,18 +82,23 @@ export class BreadcrumbsComponent implements OnInit { this.bEditChange.emit(false); setTimeout(() => { if (elem) { - this._router.navigate( - [ - this._configService.frontendModuleMonitoringUrl(), - 'object', - elem.module_code, - elem.object_type, - elem.id, - ], - { - queryParams: elem.params, - } - ); + if (this.obj == undefined) { + const url = [this._configService.frontendModuleMonitoringUrl(), elem.url].join('/'); + this._router.navigateByUrl(url); + } else { + this._router.navigate( + [ + this._configService.frontendModuleMonitoringUrl(), + 'object', + elem.module_code, + elem.object_type, + elem.id, + ], + { + queryParams: elem.params, + } + ); + } } else { this._router.navigate([this._configService.frontendModuleMonitoringUrl()]); } diff --git a/frontend/app/components/btn-select/btn-select.component.css b/frontend/app/components/btn-select/btn-select.component.css new file mode 100644 index 000000000..025878545 --- /dev/null +++ b/frontend/app/components/btn-select/btn-select.component.css @@ -0,0 +1,5 @@ +.example-chip-list { + width: 100%; + background-color: white; + border-radius: 5px; +} diff --git a/frontend/app/components/btn-select/btn-select.component.html b/frontend/app/components/btn-select/btn-select.component.html new file mode 100644 index 000000000..ad3106bc2 --- /dev/null +++ b/frontend/app/components/btn-select/btn-select.component.html @@ -0,0 +1,34 @@ + + {{ titleBtn }} + + + {{ optionSelected }} + + + + + + + + {{ option.name }} + + + + + diff --git a/frontend/app/components/btn-select/btn-select.component.ts b/frontend/app/components/btn-select/btn-select.component.ts new file mode 100644 index 000000000..09ad7a167 --- /dev/null +++ b/frontend/app/components/btn-select/btn-select.component.ts @@ -0,0 +1,149 @@ +import { COMMA, ENTER } from '@angular/cdk/keycodes'; +import { + Component, + ElementRef, + EventEmitter, + Input, + OnInit, + Output, + ViewChild, +} from '@angular/core'; +import { FormControl, Validators } from '@angular/forms'; +import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { Observable, iif, of } from 'rxjs'; +import { debounceTime, distinctUntilChanged, map, startWith, switchMap } from 'rxjs/operators'; + +import { JsonData } from '../../types/jsondata'; +import { FormService } from '../../services/form.service'; + +export interface EmptyObject { + name: string; +} + +@Component({ + selector: 'btn-select', + templateUrl: './btn-select.component.html', + styleUrls: ['./btn-select.component.css'], +}) +export class BtnSelectComponent implements OnInit { + selectable = true; + removable = true; + isInit = false; + separatorKeysCodes: number[] = [ENTER, COMMA]; + myControl = new FormControl(); + listOpNeeded = new FormControl([], [Validators.required, Validators.minLength(1)]); + @Input() placeholderText: string = 'Selectionnez vos options dans la liste'; + @Input() titleBtn: string = 'Choix des options'; + + filteredOptions: Observable; + listOptionChosen: string[] = []; + configObjAdded: JsonData = {}; + genericResponse: JsonData = {}; + objToEdit: JsonData; + + @Input() bEdit: boolean; + @Input() isInitialValues: boolean; + @Input() paramToFilt: string; + @Input() callBackFunction: ( + pageNumber: number, + limit: number, + valueToFilter: string + ) => Observable; + @Input() initValueFunction: () => JsonData; + @ViewChild('optionInput') optionInput: ElementRef; + + @Output() public sendobject = new EventEmitter(); + + constructor(private _formService: FormService) {} + + ngOnInit() { + if (this.isInitialValues && !this.isInit) { + this.initFromExistingObj(this.paramToFilt); + this.objToEdit.map((val) => this.addObject(val)); + this.isInit = true; + } + this.filteredOptions = this.myControl.valueChanges.pipe( + startWith(''), + debounceTime(400), + distinctUntilChanged(), + switchMap((val: string) => { + return iif( + () => val == '', + of([{ name: val }]), + this.filterOnRequest(val, this.paramToFilt) + ); + }), + map((res) => (res.length > 0 ? res : [{ name: 'Pas de résultats' }])) + ); + this.listOpNeeded.setValue(this.listOptionChosen); + this._formService.changeExtraFormControl(this.listOpNeeded, 'listOptBtnSelect'); + } + + remove(option: string): void { + const index = this.listOptionChosen.indexOf(option); + + if (index >= 0) { + this.listOptionChosen.splice(index, 1); + } + + if (this.configObjAdded && this.configObjAdded[option] !== undefined) { + delete this.configObjAdded[option]; + } + this.sendobject.emit(this.configObjAdded); + this.listOpNeeded.setValue(this.listOptionChosen); + } + + selected(event: MatAutocompleteSelectedEvent): void { + const shouldAddValue = this.checkBeforeAdding(event.option.viewValue); + shouldAddValue + ? this.listOptionChosen.push(event.option.viewValue) && this.addObject(event.option.value) + : null; + this.optionInput.nativeElement.value = ''; + this.myControl.setValue(null); + this.listOpNeeded.setValue(this.listOptionChosen); + } + + filterOnRequest(val: string, keyToFilt: string): Observable { + return this.callBackFunction(1, 100, val).pipe( + // Ici on map pour créer une liste d'objet contenant la valeur entré + map((response) => + response.items.filter((option) => { + return option[keyToFilt].toLowerCase().includes(val.toLowerCase()); + }) + ), + // Ici on map pour uniformiser la "key" utilisé pour afficher les options (default Key : 'name') + map((response) => + response.filter((obj) => { + Object.assign(obj, { name: obj[keyToFilt] })[keyToFilt]; + delete obj[keyToFilt]; + return obj; + }) + ) + ); + } + + checkBeforeAdding(valToAdd: string) { + const noValidInput = [null, '', 'Pas de résultats']; + if (noValidInput.includes(valToAdd) || this.listOptionChosen.includes(valToAdd)) { + return false; + } else { + return true; + } + } + + addObject(obj: JsonData) { + const { name, ...configAndId } = obj; + this.configObjAdded[name] = configAndId; + this.sendobject.emit(this.configObjAdded); + } + + initFromExistingObj(keyToFilt: string) { + const objInput = this.initValueFunction(); + this.objToEdit = objInput.filter((obj) => { + Object.assign(obj, { name: obj[keyToFilt] })[keyToFilt]; + delete obj[keyToFilt]; + return obj; + }); + this.objToEdit.map((obj) => this.listOptionChosen.push(obj.name)); + } +} diff --git a/frontend/app/components/draw-form/draw-form.component.css b/frontend/app/components/draw-form/draw-form.component.css index 56b14a974..a7682532d 100644 --- a/frontend/app/components/draw-form/draw-form.component.css +++ b/frontend/app/components/draw-form/draw-form.component.css @@ -1,4 +1,3 @@ - /* :host ::ng-deep .leaflet-draw-edit-edit { display: none; } */ diff --git a/frontend/app/components/draw-form/draw-form.component.html b/frontend/app/components/draw-form/draw-form.component.html index c21de5584..f89228ccf 100644 --- a/frontend/app/components/draw-form/draw-form.component.html +++ b/frontend/app/components/draw-form/draw-form.component.html @@ -7,13 +7,12 @@ [zoomLevelOnPoint]="zoomLevelOnPoint" [bEnable]="bEdit" > - - - - + + + diff --git a/frontend/app/components/draw-form/draw-form.component.spec.ts b/frontend/app/components/draw-form/draw-form.component.spec.ts index 0eb90169a..c879e1c3c 100644 --- a/frontend/app/components/draw-form/draw-form.component.spec.ts +++ b/frontend/app/components/draw-form/draw-form.component.spec.ts @@ -1,6 +1,5 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - import { DrawFormComponent } from './draw-form.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; describe('DrawFormComponent', () => { let component: DrawFormComponent; diff --git a/frontend/app/components/draw-form/draw-form.component.ts b/frontend/app/components/draw-form/draw-form.component.ts index 9c0818e53..002292fb3 100644 --- a/frontend/app/components/draw-form/draw-form.component.ts +++ b/frontend/app/components/draw-form/draw-form.component.ts @@ -1,8 +1,9 @@ import { Component, OnInit, Input, Output, EventEmitter, ViewEncapsulation } from '@angular/core'; -import { FormControl, FormGroup } from '@angular/forms'; +import { FormControl } from '@angular/forms'; import { leafletDrawOptions } from './leaflet-draw.options'; import { CustomMarkerIcon } from '@geonature_common/map/marker/marker.component'; +import { FormService } from '../../services/form.service'; @Component({ selector: 'pnx-draw-form', @@ -30,7 +31,7 @@ export class DrawFormComponent implements OnInit { @Input() bEdit; - constructor() {} + constructor(private _formService: FormService) {} ngOnInit() { // choix du type de geometrie @@ -99,7 +100,16 @@ export class DrawFormComponent implements OnInit { // suivi composant => formControl bindGeojsonForm(geojson) { this.geojson = geojson; - this.parentFormControl.setValue(geojson.geometry); + if (!this.parentFormControl) { + this._formService.currentFormMap.subscribe((dataForm) => { + if ('geometry' in dataForm.frmGp.controls) { + this.parentFormControl = dataForm.frmGp.controls['geometry'] as FormControl; + this.parentFormControl.setValue(geojson.geometry); + } + }); + } else { + this.parentFormControl.setValue(geojson.geometry); + } } ngOnChanges(changes) { diff --git a/frontend/app/components/modal-msg/modal-msg.component.css b/frontend/app/components/modal-msg/modal-msg.component.css index a9a108880..bd31d5919 100644 --- a/frontend/app/components/modal-msg/modal-msg.component.css +++ b/frontend/app/components/modal-msg/modal-msg.component.css @@ -9,7 +9,7 @@ } .cmodal-background { - background-color: lightgray ; + background-color: lightgray; opacity: 0.5; } @@ -34,7 +34,9 @@ .hide-modal { opacity: 0; visibility: hidden; - transition: visibility 0.6s, opacity 0.5s; + transition: + visibility 0.6s, + opacity 0.5s; } .show-modal { diff --git a/frontend/app/components/modal-msg/modal-msg.component.html b/frontend/app/components/modal-msg/modal-msg.component.html index 55a358214..df19f9f77 100644 --- a/frontend/app/components/modal-msg/modal-msg.component.html +++ b/frontend/app/components/modal-msg/modal-msg.component.html @@ -1,11 +1,10 @@ -
-
-
-
-
-
- -
-
+
+
+
+
+
+ +
+
diff --git a/frontend/app/components/modal-msg/modal-msg.component.spec.ts b/frontend/app/components/modal-msg/modal-msg.component.spec.ts index 1cb48b052..b184d972f 100644 --- a/frontend/app/components/modal-msg/modal-msg.component.spec.ts +++ b/frontend/app/components/modal-msg/modal-msg.component.spec.ts @@ -1,6 +1,5 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - import { ModalMsgComponent } from './modal-msg.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; describe('ModalMsgComponent', () => { let component: ModalMsgComponent; diff --git a/frontend/app/components/modules/modules.component.css b/frontend/app/components/modules/modules.component.css index 5ce13655f..40e65174d 100644 --- a/frontend/app/components/modules/modules.component.css +++ b/frontend/app/components/modules/modules.component.css @@ -16,7 +16,6 @@ h2 { display: flex; align-items: center; justify-content: center; - } .flex-item { @@ -34,36 +33,37 @@ h2 { justify-content: center; box-shadow: 0px 0px 10px grey; opacity: 0.7; - transition: opacity 0.2s, box-shadow 0.2s; + transition: + opacity 0.2s, + box-shadow 0.2s; } a { - text-decoration:none; - color:initial; + text-decoration: none; + color: initial; } .flex-item:hover { - opacity: 1; - box-shadow: 0px 0px 10px black; - transition: opacity 0.2s, box-shadow 0.2s; + opacity: 1; + box-shadow: 0px 0px 10px black; + transition: + opacity 0.2s, + box-shadow 0.2s; } .module h2 { text-align: center; } - -.module-card:hover -{ - border: 1px solid #303030; - padding: 5px; - color:gray; - transition: 1s ease; +.module-card:hover { + border: 1px solid #303030; + padding: 5px; + color: gray; + transition: 1s ease; } -.module-card -{ - background-color:#71717129; +.module-card { + background-color: #71717129; transition: 1s ease; } @@ -71,4 +71,8 @@ a { height: 200px; width: 100%; object-fit: cover; -} \ No newline at end of file +} + +.isDisableBtn { + opacity: 90%; +} diff --git a/frontend/app/components/modules/modules.component.html b/frontend/app/components/modules/modules.component.html index 207e94f5e..ba15207b8 100644 --- a/frontend/app/components/modules/modules.component.html +++ b/frontend/app/components/modules/modules.component.html @@ -1,38 +1,76 @@ -
- -

Chargement en cours

-
- -
-
- -

Modules de suivi

+ +

Chargement en cours

+
+ +
+
-
-
- diff --git a/frontend/app/components/modules/modules.component.ts b/frontend/app/components/modules/modules.component.ts index f75d28bb6..20cab6027 100644 --- a/frontend/app/components/modules/modules.component.ts +++ b/frontend/app/components/modules/modules.component.ts @@ -1,11 +1,16 @@ import { Utils } from './../../utils/utils'; import { Component, OnInit } from '@angular/core'; -import { mergeMap } from 'rxjs/operators'; +import { concatMap, map, mergeMap } from 'rxjs/operators'; /** services */ import { DataMonitoringObjectService } from '../../services/data-monitoring-object.service'; import { ConfigService } from '../../services/config.service'; import { get } from 'https'; +import { AuthService, User } from '@geonature/components/auth/auth.service'; +import { TPermission } from '../../types/permission'; +import { ObjectsPermissionMonitorings } from '../../enum/objectPermission'; +import { PermissionService } from '../../services/permission.service'; +import { TOOLTIPMESSAGEALERT } from '../../constants/guard'; @Component({ selector: 'pnx-monitoring-modules', @@ -13,6 +18,25 @@ import { get } from 'https'; styleUrls: ['./modules.component.css'], }) export class ModulesComponent implements OnInit { + currentUser: User; + canAccessSite: boolean = false; + currentPermission: TPermission = { + [ObjectsPermissionMonitorings.GNM_GRP_SITES]: { + canCreate: false, + canRead: false, + canUpdate: false, + canDelete: false, + }, + [ObjectsPermissionMonitorings.GNM_SITES]: { + canCreate: false, + canRead: false, + canUpdate: false, + canDelete: false, + }, + }; + + description: string; + titleModule: string; modules: Array = []; backendUrl: string; @@ -23,9 +47,13 @@ export class ModulesComponent implements OnInit { bLoading = false; + toolTipNotAllowed: string = TOOLTIPMESSAGEALERT; + constructor( + private _auth: AuthService, private _dataMonitoringObjectService: DataMonitoringObjectService, - private _configService: ConfigService + private _configService: ConfigService, + private _permissionService: PermissionService ) {} ngOnInit() { @@ -33,7 +61,19 @@ export class ModulesComponent implements OnInit { this._configService .init() .pipe( - mergeMap( + concatMap(() => + this._dataMonitoringObjectService.getCruvedMonitoring().pipe( + map((listObjectCruved: Object) => { + this._permissionService.setPermissionMonitorings(listObjectCruved); + }) + ) + ), + concatMap(() => + this._permissionService.currentPermissionObj.pipe( + map((permissionObject: TPermission) => (this.currentPermission = permissionObject)) + ) + ), + concatMap( this._dataMonitoringObjectService.getModules.bind(this._dataMonitoringObjectService) ) ) @@ -47,6 +87,16 @@ export class ModulesComponent implements OnInit { this._configService.appConfig.MEDIA_URL }/monitorings/`; this.bLoading = false; + this.description = this._configService.descriptionModule(); + this.titleModule = this._configService.titleModule(); + + this.canAccessSite = + this.currentPermission.GNM_SITES.canRead || this.currentPermission.GNM_GRP_SITES.canRead; }); + + this.currentUser = this._auth.getCurrentUser(); + + this.currentUser['cruved'] = {}; + this.currentUser['cruved_objects'] = {}; } } diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css new file mode 100644 index 000000000..68f0c1a1a --- /dev/null +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.css @@ -0,0 +1,81 @@ +.cell-link { + cursor: pointer; +} + +:host::ng-deep .datatable-body-row.active .datatable-row-group { + background-color: rgb(117, 227, 118) !important; +} + +.link:hover { + background-color: rgba(0, 0, 0, 0.2) !important; + transition: background-color 0.5; +} + +.link { + display: inline; + transition: background-color 0.5s; + border-radius: 5px; + padding: 0.5rem; +} + +.header-filter-span > input { + width: 100%; +} + +.header-sort-span { + /* width: 100%; */ + cursor: pointer; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.header-sort-span:hover { + background-color: rgb(245, 245, 245); +} + +.icon-sort { + font-size: 1.2em; + float: right; +} + +:host::ng-deep .sort-btn { + display: none !important; +} + +.custom-dt { + box-shadow: none !important; +} + +/* */ + +.object-link:hover { + color: lightblue; +} + +.btn-height { + height: 40px; +} + +.btn-float-right { + margin: 5px 0; +} + +.hide-spinner { + display: none; +} + +button:disabled { + cursor: not-allowed; + pointer-events: all !important; +} + +.isDisableBtn { + cursor: not-allowed; + text-decoration: none; +} + +.isDisableIcon { + opacity: 90%; + color: gray; +} diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html new file mode 100644 index 000000000..d025f6f3a --- /dev/null +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.html @@ -0,0 +1,219 @@ + + +

Attention

+

+ + Vous êtes sur le point de supprimer : + {{ rowSelected['name_object'] }} +
+

+ + + +
+
+
+ + + +
+
+ + + + + + + + + + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + help + + {{ column.name }} +
+
+ +
+
+
+
+
+
+
+
diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.spec.ts b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.spec.ts new file mode 100644 index 000000000..128db3e6f --- /dev/null +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.spec.ts @@ -0,0 +1,23 @@ +import { MonitoringDatatableComponent } from './monitoring-datatable-g.component'; +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; + +describe('MonitoringDatatableComponent', () => { + let component: MonitoringDatatableComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [MonitoringDatatableComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MonitoringDatatableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts new file mode 100644 index 000000000..ae3e53353 --- /dev/null +++ b/frontend/app/components/monitoring-datatable-g/monitoring-datatable-g.component.ts @@ -0,0 +1,429 @@ +import { + Component, + EventEmitter, + Input, + OnInit, + Output, + SimpleChanges, + TemplateRef, + ViewChild, +} from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { DatatableComponent } from '@swimlane/ngx-datatable'; +import { Subject, Subscription } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; + +import { TOOLTIPMESSAGEALERT } from '../../constants/guard'; +import { IColumn } from '../../interfaces/column'; +import { IobjObs, ObjDataType } from '../../interfaces/objObs'; +import { IPage } from '../../interfaces/page'; +import { DataTableService } from '../../services/data-table.service'; +import { ObjectService } from '../../services/object.service'; +import { Utils } from '../../utils/utils'; +import { SelectObject } from '../../interfaces/object'; +import { CommonService } from '@geonature_common/service/common.service'; +import { TPermission } from '../../types/permission'; +import { ObjectsPermissionMonitorings } from '../../enum/objectPermission'; + +interface ItemObjectTable { + id: number | null; + selected: boolean; + visible: boolean; + current: boolean; +} +type ItemsObjectTable = { [key: string]: ItemObjectTable[] }; + +@Component({ + selector: 'pnx-monitoring-datatable-g', + templateUrl: './monitoring-datatable-g.component.html', + styleUrls: ['./monitoring-datatable-g.component.css'], +}) +export class MonitoringDatatableGComponent implements OnInit { + @Input() rows; + @Input() colsname: IColumn[]; + @Input() page: IPage = { count: 0, limit: 0, page: 0 }; + @Input() obj; + @Input() dataTableObj; + @Input() dataTableArray; + + @Input() rowStatus: Array; + @Output() rowStatusChange = new EventEmitter(); + @Output() addFromTable = new EventEmitter(); + @Output() saveOptionChildren = new EventEmitter(); + @Output() bEditChanged = new EventEmitter(); + @Input() currentUser; + @Input() permission: TPermission; + + @Output() onSort = new EventEmitter(); + @Output() onFilter = new EventEmitter(); + @Output() onSetPage = new EventEmitter(); + @Output() onDetailsRow = new EventEmitter(); + @Output() addEvent = new EventEmitter(); + @Output() tabChanged = new EventEmitter(); + + @Output() onDeleteEvent = new EventEmitter(); + @Output() onEditEvent = new EventEmitter(); + + @Input() bDeleteModalEmitter: EventEmitter; + bDeleteModal: boolean = false; + bDeleteSpinner: boolean = false; + + private subscription: Subscription; + + private filterSubject: Subject = new Subject(); + displayFilter: boolean = false; + objectsStatus: ItemsObjectTable = {}; + + objectType: IobjObs; + columns; + row_save; + selected = []; + filters = {}; + + rowSelected; + + canCreateObj: boolean; + canCreateChild: boolean; + + toolTipNotAllowed: string = TOOLTIPMESSAGEALERT; + + @Input() activetabIndex: number = 0; + activetabType: string; + + @Output() objectsStatusChange: EventEmitter = new EventEmitter(); + + @ViewChild(DatatableComponent) table: DatatableComponent; + @ViewChild('actionsTemplate') actionsTemplate: TemplateRef; + @ViewChild('hdrTpl') hdrTpl: TemplateRef; + + constructor( + private _dataTableService: DataTableService, + private _objService: ObjectService, + private router: Router, + private _Activatedroute: ActivatedRoute, + private _commonService: CommonService + ) {} + + ngOnInit() { + this.subscribeToParentEmitter(); + this.initDatatable(); + } + subscribeToParentEmitter(): void { + if (this.bDeleteModalEmitter) { + this.subscription = this.bDeleteModalEmitter.subscribe((data: boolean) => { + this.bDeleteModal = this.bDeleteSpinner = false; + }); + } + } + + initDatatable() { + // IF prefered observable compare to ngOnChanges uncomment this: + // this._dataTableService.currentCols.subscribe(newCols => { this.columns = newCols }) + // this._objService.currentObjectType.subscribe((newObjType) => { + // this.objectType = newObjType; + // }); + + this.filters = {}; + this.filterSubject.pipe(debounceTime(500)).subscribe(() => { + this.filter(); + }); + } + + changeActiveTab(tab) { + this.activetabIndex = tab.index; + // Réinitialisation des données selectés + this.activetabType = this.dataTableArray[tab.index].objectType; + this.dataTableObj[this.activetabType].rows.length > 0 + ? (this.columns = this._dataTableService.colsTable( + this.dataTableObj[this.activetabType].columns, + this.dataTableObj[this.activetabType].rows[0] + )) + : null; + this.rows = this.dataTableObj[this.activetabType].rows; + this.page = this.dataTableObj[this.activetabType].page; + this.objectsStatusChange.emit(this.reInitStatut()); + this.tabChanged.emit(this.activetabType); + this.initPermissionAction(); + } + + reInitStatut() { + let status_type = Utils.copy(this.objectsStatus); + for (let typeObject in status_type) { + if (Array.isArray(status_type[typeObject])) { + for (let i in status_type[typeObject]) { + try { + status_type[typeObject][i]['selected'] = false; + } catch (error) { + console.error(error.message, status_type[typeObject][i]); + } + } + } + } + return status_type; + } + + displayNumber(chidrenType) { + if (!this.objectsStatus[chidrenType]) { + return ''; + } + const visibles = this.objectsStatus[chidrenType].filter((s) => s.visible); + // const nbSelected = visibles.length; + const nbSelected = this.dataTableObj[chidrenType].page.count; + const nb = this.dataTableObj[chidrenType].page.total; + return nb == nbSelected ? `(${nb})` : `(${nbSelected}/${nb})`; + } + + onSortEvent($event) { + this.filters = { + ...this.filters, + sort: $event.column.prop, + sort_dir: $event.newValue, + }; + this.onSort.emit({ filters: this.filters, tabObj: this.activetabType }); + } + + setPage($event) { + this.onSetPage.emit({ page: $event, tabObj: this.activetabType }); + } + + filterInput($event) { + this.filterSubject.next(); + } + + filter(bInitFilter = false) { + // filter all + const oldFilters = this.filters; + this.filters = Object.keys(oldFilters).reduce(function (r, e) { + if (![undefined, '', null].includes(oldFilters[e])) r[e] = oldFilters[e]; + return r; + }, {}); + this.onFilter.emit({ filters: this.filters, tabObj: this.activetabType }); + } + + onSelectEvent({ selected }) { + const id = selected[0][selected[0]['pk']]; + + if (!this.rowStatus) { + return; + } + + this.rowStatus.forEach((status) => { + const bCond = status.id === id; + status['selected'] = bCond && !status['selected']; + }); + + this.setSelected(); + this.rowStatusChange.emit(this.rowStatus); + } + + addChildren(selected) { + this.addFromTable.emit({ rowSelected: selected, objectType: this.activetabType }); + } + + saveOptionChild($event: SelectObject) { + this.saveOptionChildren.emit($event); + } + + setSelected() { + // this.table._internalRows permet d'avoir les ligne triées et d'avoir les bons index + if (!this.rowStatus) { + return; + } + + const status_selected = this.rowStatus.find((status) => status.selected); + if (!status_selected) { + return; + } + + const index_row_selected = this.table._internalRows.findIndex( + (row) => row.id === status_selected.id + ); + if (index_row_selected === -1) { + return; + } + + this.selected = [this.table._internalRows[index_row_selected]]; + this.table.offset = Math.floor(index_row_selected / this.table._limit); + } + + initPermissionAction() { + let objectType: ObjectsPermissionMonitorings | string; + let objectTypeChild: ObjectsPermissionMonitorings | string; + switch (this.activetabType) { + case 'sites_group': + objectType = ObjectsPermissionMonitorings.GNM_GRP_SITES; + objectTypeChild = ObjectsPermissionMonitorings.GNM_SITES; + this.canCreateChild = this.permission[objectTypeChild].canCreate ? true : false; + break; + case 'site': + objectType = ObjectsPermissionMonitorings.GNM_SITES; + objectTypeChild = 'visit'; + this.canCreateChild = true; + break; + case 'visit': + objectType = 'visit'; + objectTypeChild = 'undefined'; + this.canCreateObj = true; + this.canCreateChild = true; + break; + default: + objectType = 'undefined'; + objectTypeChild = 'undefined'; + this.canCreateObj = false; + this.canCreateChild = false; + } + + if (!['undefined', 'visit'].includes(objectType)) { + this.canCreateObj = this.permission[objectType].canCreate ? true : false; + } + } + + ngOnDestroy() { + this.filterSubject.unsubscribe(); + if (this.subscription) { + this.subscription.unsubscribe(); + } + } + + // tooltip(column) { + // return this.child0.template.fieldDefinitions[column.prop] + // ? column.name + " : " + this.child0.template.fieldDefinitions[column.prop] + // : column.name; + // } + + ngOnChanges(changes: SimpleChanges) { + // IF prefered ngOnChanges compare to observable uncomment this: + if (changes['dataTableObj'] && this.dataTableObj && Object.keys(this.dataTableObj).length > 0) { + this.filters = {}; + for (const objType in this.dataTableObj) { + this.objectsStatus[objType] = this._dataTableService.initObjectsStatus( + this.dataTableObj[objType].rows, + objType + ); + } + + this.activetabType = this.dataTableArray[this.activetabIndex].objectType; + this.dataTableObj[this.activetabType].rows.length > 0 + ? (this.columns = this._dataTableService.colsTable( + this.dataTableObj[this.activetabType].columns, + this.dataTableObj[this.activetabType].rows[0] + )) + : null; + this.rows = this.dataTableObj[this.activetabType].rows; + this.page = this.dataTableObj[this.activetabType].page; + this.initPermissionAction(); + } + + if (changes['rows'] && this.rows && this.rows.length > 0) { + this.activetabType = this.dataTableArray[this.activetabIndex].objectType; + this.rows = this.dataTableObj[this.activetabType].rows; + this.page = this.dataTableObj[this.activetabType].page; + this.initPermissionAction(); + } + + for (const propName of Object.keys(changes)) { + switch (propName) { + case 'rowStatus': + this.setSelected(); + break; + } + } + } + navigateToAddChildren(_, row) { + this.addEvent.emit(row); + this._objService.changeObjectType(this.dataTableArray[this.activetabIndex]); + if (row && this.dataTableArray.length == 1) { + row['id'] = row[row.pk]; + this.router.navigate([row.id, 'create'], { + relativeTo: this._Activatedroute, + }); + } + } + + navigateToAddObj() { + this._objService.changeObjectType(this.dataTableArray[this.activetabIndex]); + if (this.dataTableArray.length == 1) { + this.router.navigate(['create'], { + relativeTo: this._Activatedroute, + }); + } else { + this.router.navigate([ + 'monitorings', + this.dataTableArray[this.activetabIndex].routeBase, + 'create', + ]); + } + + // TODO: gérer la gestion de l'ajout (et ajout d'objet enfant) d'objet de type "site" depuis la page d'accueil de visualisation de groupe de site/ site + // + } + + navigateToDetail(row) { + row['id'] = row.pk; + this.onDetailsRow.emit(row); + } + + editSelectedItem(row) { + row['id'] = row.pk; + this.onEditEvent.emit(row); + } + + msgToaster(action) { + // return `${action} ${this.obj.labelDu()} ${this.obj.description()} effectuée`.trim(); + return `${action} effectuée`.trim(); + } + + onDelete(row) { + this.bDeleteSpinner = true; + row['id'] = row[row.pk]; + this._commonService.regularToaster('info', this.msgToaster('Suppression')); + this.onDeleteEvent.emit({ rowSelected: row, objectType: this.activetabType }); + } + + alertMessage(row) { + row['id'] = row[row.pk]; + this.rowSelected = row; + const varNameObjet = this.dataTableArray[this.activetabIndex].config.description_field_name; + + this.rowSelected['name_object'] = row[varNameObjet]; + this.bDeleteModal = true; + } + + // TODO: Comprendre le fonctionnement de ObjectStatuts et RowsStatus + // initObjectsStatus() { + // const objectsStatus = {}; + // for (const childrenType of Object.keys(this.obj.children)) { + // objectsStatus[childrenType] = this.obj.children[childrenType].map( + // (child) => { + // return { + // id: child.id, + // selected: false, + // visible: true, + // current: false, + // }; + // } + // ); + // } + + // // init site status + // if (this.obj.siteId) { + // objectsStatus["site"] = []; + // this.sites["features"].forEach((f) => { + // // determination du site courrant + // let cur = false; + // if (f.properties.id_base_site == this.obj.siteId) { + // cur = true; + // } + + // objectsStatus["site"].push({ + // id: f.properties.id_base_site, + // selected: false, + // visible: true, + // current: cur, + // }); + // }); + // } + + // this.objectsStatus = objectsStatus; + // } +} diff --git a/frontend/app/components/monitoring-datatable/monitoring-datatable.component.css b/frontend/app/components/monitoring-datatable/monitoring-datatable.component.css index 6017b44ab..1e5cd3eef 100644 --- a/frontend/app/components/monitoring-datatable/monitoring-datatable.component.css +++ b/frontend/app/components/monitoring-datatable/monitoring-datatable.component.css @@ -1,48 +1,63 @@ .cell-link { - cursor: pointer; + cursor: pointer; } :host::ng-deep .datatable-body-row.active .datatable-row-group { - background-color: rgb(117, 227, 118) !important; + background-color: rgb(117, 227, 118) !important; } .link:hover { - background-color: rgba(0, 0, 0, 0.2) !important; - transition: background-color 0.5; + background-color: rgba(0, 0, 0, 0.2) !important; + transition: background-color 0.5; } .link { - display: inline; - transition: background-color 0.5s; - border-radius: 5px; + display: inline; + transition: background-color 0.5s; + border-radius: 5px; + padding: 0.5rem; } .header-filter-span > input { - width: 100%; + width: 100%; } .header-sort-span { - /* width: 100%; */ - cursor: pointer; - text-overflow: ellipsis; - overflow: hidden; - white-space:nowrap + /* width: 100%; */ + cursor: pointer; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; } - .header-sort-span:hover { - background-color: rgb(245, 245, 245); + background-color: rgb(245, 245, 245); } .icon-sort { - font-size: 1.2em; - float: right; + font-size: 1.2em; + float: right; } :host::ng-deep .sort-btn { - display: none !important; + display: none !important; } .custom-dt { - box-shadow: none !important; + box-shadow: none !important; +} + +button:disabled { + cursor: not-allowed; + pointer-events: all !important; +} + +.isDisableBtn { + cursor: not-allowed; + text-decoration: none; +} + +.isDisableIcon { + opacity: 90%; + color: gray; } diff --git a/frontend/app/components/monitoring-datatable/monitoring-datatable.component.html b/frontend/app/components/monitoring-datatable/monitoring-datatable.component.html index 40257d7dd..2ac9ae95d 100644 --- a/frontend/app/components/monitoring-datatable/monitoring-datatable.component.html +++ b/frontend/app/components/monitoring-datatable/monitoring-datatable.component.html @@ -1,3 +1,21 @@ + +

Attention

+

+ Vous êtes sur le point de supprimer + {{ this.child0.template['label_art_def'] }} + {{ this.child0.template['description'] }} +

+ + +
+ + + + - + + + + + + @@ -61,18 +114,19 @@
- {{column.definition}} - + {{ column.definition }} + help - {{ column.name }} + >help + {{ column.name }}
{ let component: MonitoringDatatableComponent; diff --git a/frontend/app/components/monitoring-datatable/monitoring-datatable.component.ts b/frontend/app/components/monitoring-datatable/monitoring-datatable.component.ts index 8784b6b35..4c4687669 100644 --- a/frontend/app/components/monitoring-datatable/monitoring-datatable.component.ts +++ b/frontend/app/components/monitoring-datatable/monitoring-datatable.component.ts @@ -13,6 +13,9 @@ import { Router } from '@angular/router'; import { MonitoringObjectService } from './../../services/monitoring-object.service'; import { Subject } from 'rxjs'; import { catchError, map, tap, take, debounceTime } from 'rxjs/operators'; +import { CommonService } from '@geonature_common/service/common.service'; +import { ObjectService } from '../../services/object.service'; +import { TOOLTIPMESSAGEALERT } from '../../constants/guard'; @Component({ selector: 'pnx-monitoring-datatable', @@ -48,10 +51,27 @@ export class MonitoringDatatableComponent implements OnInit { @ViewChild('actionsTemplate') actionsTemplate: TemplateRef; @ViewChild('hdrTpl') hdrTpl: TemplateRef; - constructor(private _monitoring: MonitoringObjectService) {} + rowSelected; + bDeleteModal: boolean = false; + bDeleteSpinner: boolean = false; + toolTipNotAllowed: string = TOOLTIPMESSAGEALERT; + canCreateChild: boolean = false; + + constructor( + private _monitoring: MonitoringObjectService, + private _commonService: CommonService, + private _objectService: ObjectService + ) {} ngOnInit() { this.initDatatable(); + this.initPermission(); + } + + initPermission() { + // TODO: Attention ici l'ajout avec l'icon ne se fait que sur un enfant (si plusieurs enfants au même niveau , le premier sera pris pour le moment) + const childrenType = this.child0.config.children_types[0]; + this.canCreateChild = this.currentUser?.moduleCruved[childrenType]['C']; } initDatatable() { @@ -234,4 +254,23 @@ export class MonitoringDatatableComponent implements OnInit { return out; }; } + + msgToaster(action) { + // return `${action} ${this.obj.labelDu()} ${this.obj.description()} effectuée`.trim(); + return `${action} effectuée`.trim(); + } + + onDelete(row) { + this._commonService.regularToaster('info', this.msgToaster('Suppression')); + this._objectService.changeDisplayingDeleteModal(this.bDeleteModal); + this._objectService.changeSelectRow({ rowSelected: row, objectType: this.child0.objectType }); + this._objectService.currentDeleteModal.subscribe( + (deletedModal) => (this.bDeleteModal = deletedModal) + ); + } + + alertMessage(row) { + this.rowSelected = row; + this.bDeleteModal = true; + } } diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.css b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.css new file mode 100644 index 000000000..18d916f88 --- /dev/null +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.css @@ -0,0 +1,56 @@ +:host ::ng-deep .obj-form { + margin: 0; + margin-bottom: 10px; + padding: 0; +} + +.hide-spinner { + display: none; +} + +.btn-height { + height: 39px; +} + +.float-right { + margin-left: 5px; +} + +.float-left { + margin-right: 10px; + float: left; +} + +form:invalid { + outline: none; +} + +form.ng-invalid { + border: 0px !important; +} + +.form-scroll-info-geom { + overflow-y: auto; + max-height: 60vh; +} + +.form-scroll { + overflow-y: auto; + max-height: 70vh; +} + +.btn-child { + border: 0px solid #202020; + padding-top: 2px; + padding-bottom: auto; + -webkit-box-shadow: 0px -2px 2px rgba(50, 50, 50, 0.3); + -moz-box-shadow: 0px -2px 2px rgba(50, 50, 50, 0.3); + box-shadow: 0px -2px 2px rgba(50, 50, 50, 0.3); + overflow: hidden; + height: fit-content; +} + +button:disabled { + cursor: not-allowed; + pointer-events: all !important; +} diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html new file mode 100644 index 000000000..14fa6203f --- /dev/null +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.html @@ -0,0 +1,157 @@ + +

Attention

+

+ + Vous êtes sur le point de supprimer {{obj.config["label"]}} : + {{obj.properties[obj.config["description_field_name"]]}} +
(id :{{obj.properties[obj.config["id_field_name"]]}}) +

+ + +
+ +
+
+ + + + + + + + + + + +
+ +

+ Veuillez saisir une géométrie sur la carte +

+ + +
+ + + + + + + error La suppression de types de site entraine la + réinitialisation des champs associés + + + + +
+ +
+ + + + + + + + +
+
+
+
+
+
diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.spec.ts b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.spec.ts new file mode 100644 index 000000000..f3bc4dd70 --- /dev/null +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.spec.ts @@ -0,0 +1,23 @@ +import { MonitoringFormComponent } from './monitoring-form.component'; +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; + +describe('MonitoringFormComponent', () => { + let component: MonitoringFormComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [MonitoringFormComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MonitoringFormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts new file mode 100644 index 000000000..9dcd6e7fd --- /dev/null +++ b/frontend/app/components/monitoring-form-g/monitoring-form.component-g.ts @@ -0,0 +1,748 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormBuilder, FormControl, FormGroup, Validators } from '@angular/forms'; +import { + tap, + mergeMap, + map, + take, + switchMap, + concatMap, + takeUntil, + distinctUntilChanged, +} from 'rxjs/operators'; +import { ActivatedRoute } from '@angular/router'; +import { Router } from '@angular/router'; +import { DynamicFormService } from '@geonature_common/form/dynamic-form-generator/dynamic-form.service'; +import { CommonService } from '@geonature_common/service/common.service'; + +import { IDataForm } from '../../interfaces/form'; +import { ApiGeomService } from '../../services/api-geom.service'; +import { ConfigJsonService } from '../../services/config-json.service'; +import { FormService } from '../../services/form.service'; +import { IExtraForm } from '../../interfaces/object'; +import { JsonData } from '../../types/jsondata'; +import { Observable, ReplaySubject, Subject, of } from 'rxjs'; +import { TOOLTIPMESSAGEALERT } from '../../constants/guard'; + +@Component({ + selector: 'pnx-monitoring-form-g', + templateUrl: './monitoring-form.component-g.html', + styleUrls: ['./monitoring-form.component-g.css'], +}) +export class MonitoringFormComponentG implements OnInit { + @Input() currentUser; + + @Input() objForm: { + static: FormGroup; + dynamic?: FormGroup; + }; + // @Input() objForm: FormGroup; + objFormStatic: FormGroup; + objFormDynamic: FormGroup; + + // @Input() obj: any; + @Output() objChanged = new EventEmitter(); + + @Input() objectsStatus; + @Output() objectsStatusChange = new EventEmitter(); + + @Input() bEdit: boolean; + @Output() bEditChange = new EventEmitter(); + + @Input() sites: {}; + @Input() apiService: ApiGeomService; + @Input() isExtraForm: boolean = false; + + extraFormCtrl: IExtraForm; + geomCtrl: { frmCtrl: FormControl; frmName: string }; + dataForm: IDataForm; + searchSite = ''; + + isExtraControlChange: boolean; + isUndefinedChange: boolean; + obj: JsonData; + objFormsDefinition: { + static: JsonData[]; + dynamic?: JsonData[]; + }; + + objFormGroups: FormGroup; + prop: JsonData; + specificForm$: Observable; + createSpecificForm$: Observable; + meta: {}; + private destroyed$: ReplaySubject = new ReplaySubject(1); + + public bSaveSpinner = false; + public bSaveAndAddChildrenSpinner = false; + public bDeleteSpinner = false; + public bDeleteModal = false; + public bChainInput = false; + public bAddChildren = false; + public chainShow = []; + public queryParams = {}; + + canDelete: boolean = false; + canUpdate: boolean = false; + canCreateOrUpdate: boolean = false; + + toolTipNotAllowed: string = TOOLTIPMESSAGEALERT; + constructor( + private _formBuilder: FormBuilder, + private _route: ActivatedRoute, + private _configService: ConfigJsonService, + private _commonService: CommonService, + private _dynformService: DynamicFormService, + private _formService: FormService, + private _router: Router + ) {} + + ngOnInit() { + if (!this.objFormStatic) { + this.objFormStatic = this._formBuilder.group({}); + } + if (!this.objFormDynamic && this.isExtraForm) { + this.objFormDynamic = this._formBuilder.group({}); + } + this.isExtraForm + ? (this.objForm = { static: this.objFormStatic, dynamic: this.objFormDynamic }) + : (this.objForm = { static: this.objFormStatic }); + this.isExtraForm + ? (this.objFormsDefinition = { static: [{}], dynamic: [{}] }) + : (this.objFormsDefinition = { static: [{}] }); + + this.specificForm$ = this._formService.currentDataSpec.pipe( + mergeMap((newObj) => { + return this.apiService.getConfig().pipe( + map((prop) => { + this.prop = prop; + return { newObj, prop: prop }; + }) + ); + }) + ); + + this.createSpecificForm$ = this._formService.currentDataSpecToCreate.pipe( + mergeMap((specConfig) => { + return this.apiService.getConfig().pipe( + map((prop) => { + this.prop = prop; + return { specConfig: specConfig, prop: prop }; + }) + ); + }) + ); + + this._formService.currentData + .pipe( + distinctUntilChanged((prev, curr) => prev['pk'] === curr['pk']), + takeUntil(this.destroyed$), + tap((data) => { + this.obj = data; + this.obj.id = this.obj[this.obj.pk]; + this.initPermission(); + }), + concatMap((data: any) => this._configService.init(data.moduleCode)), + concatMap((data) => { + return this.apiService.getConfig().pipe( + map((prop) => { + this.prop = prop; + return { prop: prop }; + }) + ); + }), + switchMap(({ prop }) => { + if (this.isExtraForm) { + return this._formService.currentExtraFormCtrl.pipe( + map((frmCtrl) => { + return { prop: prop, frmCtrl: frmCtrl }; + }) + ); + } else { + return of({ prop: prop }); + } + }) + ) + .subscribe((data) => { + console.log(data.prop); + this.initObj(data.prop); + this.obj.config = this._configService.configModuleObject( + this.obj.moduleCode, + this.obj.objectType + ); + const schema = this._configService.schema(this.obj.moduleCode, this.obj.objectType); + this.obj[this.obj.moduleCode] = schema; + this.obj.specific == undefined ? (this.obj.specific = {}) : null; + this.obj.bIsInitialized = true; + + this.queryParams = this._route.snapshot.queryParams || {}; + this.bChainInput = this._configService.frontendParams().bChainInput; + // meta pour les parametres dynamiques + // ici pour avoir acces aux nomenclatures + this.meta = { + // nomenclatures: this._dataUtilsService.getDataUtil('nomenclature'), + // dataset: this._dataUtilsService.getDataUtil('dataset'), + id_role: this.currentUser.id_role, + bChainInput: this.bChainInput, + parents: this.obj.parents, + }; + + Object.keys(this.objFormsDefinition).forEach((key) => { + let configType: string; + key == 'static' ? (configType = 'generic') : (configType = 'specific'); + + this.obj.config + ? (this.objFormsDefinition[key] = this._dynformService + .formDefinitionsdictToArray(this.obj[configType], this.meta) + .filter((formDef) => formDef.type_widget) + .sort((a, b) => { + // medias à la fin + return a.attribut_name === 'medias' ? +1 : b.attribut_name === 'medias' ? -1 : 0; + })) + : null; + }); + + // display_form pour customiser l'ordre dans le formulaire + // les éléments de display form sont placé en haut dans l'ordre du tableau + // tous les éléments non cachés restent affichés + + let displayProperties = [ + ...(this._configService.configModuleObjectParam( + this.obj.moduleCode, + this.obj.objectType, + 'display_properties' + ) || []), + ]; + if (displayProperties && displayProperties.length) { + displayProperties.reverse(); + Object.keys(this.objFormsDefinition).forEach((key) => { + this.objFormsDefinition[key].sort((a, b) => { + let indexA = displayProperties.findIndex((e) => e == a.attribut_name); + let indexB = displayProperties.findIndex((e) => e == b.attribut_name); + return indexB - indexA; + }); + }); + } + this.objForm.static.addControl('patch_update', this._formBuilder.control(0)); + + this.isExtraForm ? this.addExtraFormCtrl(data['frmCtrl']) : null; + // set geometry + if (this.obj.config && this.obj.config['geometry_type']) { + let frmCtrlGeom = { + frmCtrl: this._formBuilder.control('', Validators.required), + frmName: 'geometry', + }; + this.addGeomFormCtrl(frmCtrlGeom); + } + this.initForm(); + this.isExtraForm && this.bEdit ? this.updateSpecificForm() : null; + this.isExtraForm && !this.bEdit ? this.createSpecificForm() : null; + }); + } + + /** pour réutiliser des paramètres déjà saisis */ + keepDefinitions() { + return Object.keys(this.objFormsDefinition).forEach((key) => { + this.objFormsDefinition[key].filter((def) => + this._configService + .configModuleObjectParam(this.obj.moduleCode, this.obj.objectType, 'keep') + .includes(def.attribut_name) + ); + }); + } + + setQueryParams() { + // par le biais des parametre query de route on donne l'id du ou des parents + // permet d'avoir un 'tree' ou un objet est sur plusieurs branches + // on attend des ids d'où test avec parseInt + for (const key of Object.keys(this.queryParams)) { + const strToInt = parseInt(this.queryParams[key]); + if (!Number.isNaN(strToInt)) { + this.obj.properties[key] = strToInt; + } + } + } + + /** initialise le formulaire quand le formulaire est prêt ou l'object est prêt */ + + initForm() { + if (!(this.objForm.static && this.obj.bIsInitialized && this.obj.config)) { + return; + } + + this.setQueryParams(); + + this._formService.formValues(this.obj).subscribe((formValue) => { + const allKeysForm = Object.keys(formValue); + const allKeysStatic = Object.keys(this.obj.config.generic); + if (this.obj.config['geometry_type'] && allKeysForm.includes('geometry')) { + allKeysStatic.push('geometry'); + } + // const allKeysSpecific = Object.keys(this.obj.specific); + let formValueStatic = {}; + for (let key of allKeysForm) { + if (allKeysStatic.includes(key)) { + formValueStatic[key] = formValue[key]; + } + } + this.objForm.static.patchValue(formValueStatic); + this._formService.changeFormMapObj({ + frmGp: this.objForm.static, + bEdit: true, + obj: this.obj, + }); + this.setDefaultFormValue(); + }); + } + + initValueFormDynamic() { + if (!(this.objForm.dynamic && this.obj.bIsInitialized && this.obj.config)) { + return; + } + + this.setQueryParams(); + this._formService.formValues(this.obj).subscribe((formValue) => { + const allKeysForm = Object.keys(formValue); + const allKeysSpecific = Object.keys(this.obj.specific); + let formValueSpecific = {}; + for (let key of allKeysForm) { + if (allKeysSpecific.includes(key) || key == 'types_site') { + formValueSpecific[key] = formValue[key]; + } + } + this.objForm.dynamic.patchValue(formValueSpecific); + this.setDefaultFormValue(); + // this.dataForm = propertiesValues; + }); + } + + keepNames() { + return ( + this._configService.configModuleObjectParam( + this.obj.moduleCode, + this.obj.objectType, + 'keep' + ) || [] + ); + } + + idFieldName() { + return this._configService.configModuleObjectParam( + this.obj.moduleCode, + this.obj.objectType, + 'id_field_Name' + ); + } + + resetObjForm() { + //NEW- setResolvedProperties + + // quand on enchaine les relevés + // const chainShow = this.obj.configParam('chain_show'); + + //TODO: Ici chain_show est présent que dans le fichier de config visit.json + // --> voir à quoi correspond ce chainShow où on utilise les propriétés (id_base_site, num_passage etc) + const chainShow = this._configService.configModuleObjectParam( + this.obj.moduleCode, + this.obj.objectType, + 'chain_show' + ); + if (chainShow) { + this.chainShow.push(chainShow.map((key) => this.obj.resolvedProperties[key])); + this.chainShow.push(this.obj.resolvedProperties); + } + + // les valeur que l'on garde d'une saisie à l'autre + const keep = {}; + for (const key of this.keepNames()) { + keep[key] = this.obj.properties[key]; + } + + this.obj = { + bIsInitialized: false, + moduleCode: this.obj.moduleCode, + objectType: this.obj.objectType, + endPoint: this.obj.endPoint, + properties: {}, + generic: this.obj.generic, + }; + this.obj.config = this._configService.configModuleObject( + this.obj.moduleCode, + this.obj.objectType + ); + this.obj.properties[this.idFieldName()] = null; + + // pq get ????? + // this.obj.get(0).subscribe(() => { + this.obj.bIsInitialized = true; + for (const key of this.keepNames()) { + this.obj.properties[key] = keep[key]; + } + + this.objChanged.emit(this.obj); + this.objForm.static.patchValue({ geometry: null }); + this.objForm.dynamic?.patchValue({}); + this.initForm(); + this.isExtraForm ? this.initValueFormDynamic() : null; + // }; + } + + /** Pour donner des valeurs par defaut si la valeur n'est pas définie + * id_digitiser => current_user.id_role + * id_inventor => current_user.id_role + * date => today + */ + setDefaultFormValue() { + const value = this.objForm.static.value; + const date = new Date(); + const defaultValue = { + id_digitiser: value['id_digitiser'] || this.currentUser.id_role, + id_inventor: value['id_inventor'] || this.currentUser.id_role, + first_use_date: value['first_use_date'] || { + year: date.getUTCFullYear(), + month: date.getUTCMonth() + 1, + day: date.getUTCDate(), + }, + }; + this.objForm.static.patchValue(defaultValue); + } + + /** + * TODO faire des fonctions dans monitoring objet (ou moniotring objet service pour naviguer + */ + + /** + * Valider et renseigner les enfants + */ + navigateToAddChildren() { + this.bEditChange.emit(false); + this.obj.navigateToAddChildren(); + } + + /** + * Valider et aller à la page de l'objet + */ + navigateToDetail(id, objectType, queryParams) { + // patch bug navigation + // this._router.navigate( + // ['monitorings', objectType, id].filter((s) => !!s), + // { + // queryParams, + // } + // ); + // TODO: this commented code works only if ".." is not based url (example working : sites_group/:id/site/:id , not working if create site_group) + // this._router.navigate(['..',objectType,id], {relativeTo: this._route}); + const urlSegment = + this.obj.urlRelative == '/monitorings' + ? [this.apiService.objectObs.routeBase, id] + : [objectType, id].filter((s) => !!s); + const urlPathDetail = [this.obj.urlRelative].concat(urlSegment).join('/'); + this.objChanged.emit(this.obj); + const urlRelative = this.obj.urlRelative ? true : false; + if (urlRelative) { + this._router.navigateByUrl(urlPathDetail); + } else { + const urlTree = this._router.parseUrl(this._router.url); + const urlWithoutParams = urlTree.root.children['primary'].segments + .map((it) => it.path) + .join('/'); + this._router.navigate([urlWithoutParams]); + } + this.bEditChange.emit(false); + } + + /** + * Valider et aller à la page de l'objet + */ + navigateToParent() { + // FIXME:: voir erreur de redirection (comparaison avec branche où ça fonctionnait ?) + this.bEditChange.emit(false); // patch bug navigation + if (!this.bEdit) { + this._router.navigate(['..'], { relativeTo: this._route }); + } + } + + navigateToParentAfterDelete() { + this.bEditChange.emit(false); // patch bug navigation + if (this.obj.objectType == 'site' && this._router.url.includes('sites_group')) { + this._router.navigate(['monitorings', 'sites_group', this._route.parent.snapshot.params.id]); + } else { + this._router.navigate(['..'], { relativeTo: this._route }); + } + } + + msgToaster(action) { + // return `${action} ${this.obj.labelDu()} ${this.obj.description()} effectuée`.trim(); + return `${action} effectuée`.trim(); + } + + /** TODO améliorer site etc.. */ + onSubmit(isAddChildrend = false) { + isAddChildrend + ? (this.bSaveAndAddChildrenSpinner = this.bAddChildren = true) + : (this.bSaveSpinner = true); + const { patch_update, ...sendValue } = this.dataForm; + const objToUpdateOrCreate = this._formService.postData(sendValue, this.obj); + const action = this.obj.id + ? this.apiService.patch(this.obj.id, objToUpdateOrCreate) + : this.apiService.create(objToUpdateOrCreate); + const actionLabel = this.obj.id ? 'Modification' : 'Création'; + action.subscribe((objData) => { + this._commonService.regularToaster('success', this.msgToaster(actionLabel)); + this.bSaveSpinner = this.bSaveAndAddChildrenSpinner = false; + + Object.entries(objData['properties']).forEach(([key, value]) => { + this.obj['properties'][key] = value; + }); + + if (objData.hasOwnProperty('id')) { + this.obj.id = objData['id']; + } + this.objChanged.emit(this.obj); + + /** si c'est un module : reset de la config */ + if (this.obj.objectType === 'module') { + this._configService.loadConfig(this.obj.moduleCode).subscribe(); + } + + if (this.bChainInput) { + this.resetObjForm(); + } else if (this.bAddChildren) { + this.navigateToAddChildren(); + } else { + if ( + this._configService.configModuleObjectParam( + this.obj.moduleCode, + this.obj.objectType, + 'redirect_to_parent' + ) + ) { + this.navigateToParent(); + } else { + this.navigateToDetail(this.obj.id, this.obj.objectType, this.queryParams); + } + } + }); + } + + onCancelEdit() { + if (this.bEdit) { + const urlTree = this._router.parseUrl(this._router.url); + const urlWithoutParams = urlTree.root.children['primary'].segments + .map((it) => it.path) + .join('/'); + this._router.navigate([urlWithoutParams]); + this.bEditChange.emit(false); + } else { + this.navigateToParent(); + } + } + + onDelete() { + this.bDeleteSpinner = true; + // : this.obj.post(this.objForm.value); + this.apiService.delete(this.obj.id).subscribe((del) => { + this.bDeleteSpinner = this.bDeleteModal = false; + this.objChanged.emit('deleted'); + setTimeout(() => { + this._commonService.regularToaster('info', this.msgToaster('Suppression')); + this.navigateToParentAfterDelete(); + }, 100); + }); + } + + onObjFormValueChange(event) { + // let {id_module,medias, ...rest} = this.objForm.value; + // this.dataForm = rest + this.dataForm = { ...this.objForm.static.value, ...this.objForm.dynamic?.value }; + const change = this._configService.change(this.obj.moduleCode, this.obj.objectType); + // if('geometry' in this.objForm.controls){ + // this._formService.changeFormMapObj({frmGp:this.objForm,bEdit:true, obj: this.obj}) + // } + + if (!change) { + return; + } + setTimeout(() => { + change({ objForm: this.objForm.static, meta: this.meta }); + }, 100); + } + + onObjFormDynamicValueChange(event) { + // let {id_module,medias, ...rest} = this.objForm.value; + // this.dataForm = rest + this.dataForm = { ...this.objForm.static.value, ...this.objForm.dynamic.value }; + const change = this._configService.change(this.obj.moduleCode, this.obj.objectType); + // if('geometry' in this.objForm.controls){ + // this._formService.changeFormMapObj({frmGp:this.objForm,bEdit:true, obj: this.obj}) + // } + if (this.extraFormCtrl && !this.extraFormCtrl.frmCtrl.valid) { + this.extraFormCtrl.frmCtrl.markAllAsTouched(); + } + if (!change) { + return; + } + setTimeout(() => { + change({ objForm: this.objForm.static, meta: this.meta }); + }, 100); + } + + procesPatchUpdateForm() { + Object.keys(this.objForm).forEach((form) => + this.objForm[form].patchValue({ + patch_update: this.objForm[form].value.patch_update + 1, + }) + ); + } + + /** bChainInput gardé dans config service */ + bChainInputChanged() { + for (const formDef of this.objFormsDefinition.static) { + formDef.meta.bChainInput = this.bChainInput; + } + this._configService.setFrontendParams('bChainInput', this.bChainInput); + // patch pour recalculers + this.procesPatchUpdateForm(); + } + + addExtraFormCtrl(frmCtrl: IExtraForm) { + if (frmCtrl.frmName in this.objFormDynamic.controls) { + } else { + this.objForm.dynamic.addControl(frmCtrl.frmName, frmCtrl.frmCtrl); + } + + this.extraFormCtrl = frmCtrl; + } + + addGeomFormCtrl(frmCtrl: { frmCtrl: FormControl; frmName: string }) { + if (frmCtrl.frmName in this.objForm.static.controls) { + } else { + this.objForm.static.addControl(frmCtrl.frmName, frmCtrl.frmCtrl); + } + this.geomCtrl = frmCtrl; + } + + getConfigFromBtnSelect(configSpec) { + // TODO: Ajout de tous les id_parents ["id_sites_groups" etc ] dans l'objet obj.dataComplement + // Check if specific and dataComplement already exist + this.obj.specific = {}; + this.obj.dataComplement = {}; + + Object.entries(configSpec).forEach(([key, value]) => { + if (this.obj.dataComplement[key] && key != 'types_site') { + return; + } + if (configSpec[key].config != undefined) { + if (Object.keys(configSpec[key].config).length !== 0) { + Object.assign(this.obj.specific, configSpec[key].config.specific); + if ('keep' in configSpec[key].config) { + this.obj.config.keep ? null : (this.obj.config.keep = []); + !this.obj.config.keep.includes(configSpec[key].config.keep) + ? this.obj.config.keep.push(...configSpec[key].config.keep) + : null; + } + } + this.obj.dataComplement[key] = value; + } else { + this.obj.dataComplement[key] ? null : (this.obj.dataComplement[key] = []); + this.obj.dataComplement[key] = configSpec[key]; + } + }); + } + + initObj(prop) { + this.obj['properties'] = prop; + Object.entries(this.obj).forEach(([key, value]) => { + if (key != 'properties' && key in this.obj['properties']) { + this.obj['properties'][key] = value; + } + }); + this.obj.resolvedProperties = this._configService.setResolvedProperties(this.obj); + } + + updateExtraFormOnly() { + if (!(this.objForm.dynamic && this.obj.bIsInitialized)) { + return; + } + // pour donner la valeur de l'objet au formulaire + this._formService.formValues(this.obj).subscribe((formValue) => { + const allKeysForm = Object.keys(formValue); + const allKeysGeneric = Object.keys(this.obj.config.generic); + const allKeysSpecific = Object.keys(this.obj.specific); + let formValueSpecific = {}; + for (let key of allKeysForm) { + if (allKeysSpecific.includes(key)) { + formValueSpecific[key] = formValue[key]; + } + } + this.objForm.dynamic.patchValue(formValueSpecific); + // this.setDefaultFormValue(); + // this.dataForm = propertiesValues; + // reset geom ? + }); + } + + createSpecificForm() { + this.createSpecificForm$.subscribe(({ specConfig, prop }) => { + this.obj.bIsInitialized = true; + this.obj.id = this.obj[this.obj.pk]; + this.getConfigFromBtnSelect(specConfig); + this.initObj(prop); + this.initElementFormDynamic(); + this.initValueFormDynamic(); + }); + } + updateSpecificForm() { + this.specificForm$.subscribe(({ newObj, prop }) => { + this.obj.bIsInitialized = true; + this.obj.id = this.obj[this.obj.pk]; + this.obj.dataComplement = newObj.newObj.dataComplement; + this.obj.specific = newObj.newObj.specific; + Object.assign(this.obj, newObj.propSpec); + this.initObj(prop); + this.initElementFormDynamic(); + this.initValueFormDynamic(); + }); + } + + initElementFormDynamic() { + const schema = this._configService.schema(this.obj.moduleCode, this.obj.objectType); + if (Object.keys(this.obj.specific).length !== 0) { + Object.assign(schema, this.obj.specific); + } + + this.obj[this.obj.moduleCode] = schema; + this.objFormsDefinition.dynamic = this._dynformService + .formDefinitionsdictToArray(this.obj.specific, this.meta) + .filter((formDef) => formDef.type_widget) + .sort((a, b) => { + // medias à la fin + return a.attribut_name === 'medias' ? +1 : b.attribut_name === 'medias' ? -1 : 0; + }); + } + + initPermission() { + !this.bEdit + ? (this.canCreateOrUpdate = true) + : ((this.canDelete = this.obj.cruved['D']), + (this.canCreateOrUpdate = this.canUpdate = this.obj.cruved['U'])); + } + + notAllowedMessage() { + this._commonService.translateToaster( + 'warning', + "Vous n'avez pas les permissions nécessaires pour éditer l'objet" + ); + } + + ngOnDestroy() { + this._formService.changeFormMapObj({ + frmGp: this._formBuilder.group({}), + bEdit: false, + obj: {}, + }); + this.destroyed$.next(true); + this.destroyed$.complete(); + this.obj = {}; + } +} diff --git a/frontend/app/components/monitoring-form/monitoring-form.component.css b/frontend/app/components/monitoring-form/monitoring-form.component.css index a5c26b9ca..18d916f88 100644 --- a/frontend/app/components/monitoring-form/monitoring-form.component.css +++ b/frontend/app/components/monitoring-form/monitoring-form.component.css @@ -13,10 +13,9 @@ } .float-right { -margin-left: 5px; + margin-left: 5px; } - .float-left { margin-right: 10px; float: left; @@ -28,4 +27,30 @@ form:invalid { form.ng-invalid { border: 0px !important; -} \ No newline at end of file +} + +.form-scroll-info-geom { + overflow-y: auto; + max-height: 60vh; +} + +.form-scroll { + overflow-y: auto; + max-height: 70vh; +} + +.btn-child { + border: 0px solid #202020; + padding-top: 2px; + padding-bottom: auto; + -webkit-box-shadow: 0px -2px 2px rgba(50, 50, 50, 0.3); + -moz-box-shadow: 0px -2px 2px rgba(50, 50, 50, 0.3); + box-shadow: 0px -2px 2px rgba(50, 50, 50, 0.3); + overflow: hidden; + height: fit-content; +} + +button:disabled { + cursor: not-allowed; + pointer-events: all !important; +} diff --git a/frontend/app/components/monitoring-form/monitoring-form.component.html b/frontend/app/components/monitoring-form/monitoring-form.component.html index fceecbca6..3fb32eefb 100644 --- a/frontend/app/components/monitoring-form/monitoring-form.component.html +++ b/frontend/app/components/monitoring-form/monitoring-form.component.html @@ -2,8 +2,8 @@

Attention

Vous êtes sur le point de supprimer - {{ this.obj.template["label_art_def"] }} - {{ this.obj.template["description"] }} + {{ this.obj.template['label_art_def'] }} + {{ this.obj.template['description'] }}

- +
-
+
Attention

Veuillez saisir une géométrie sur la carte

- - - -
+
+ + + + +
+
@@ -123,10 +129,12 @@

Attention

diff --git a/frontend/app/components/monitoring-form/monitoring-form.component.spec.ts b/frontend/app/components/monitoring-form/monitoring-form.component.spec.ts index f34b4c007..4db2a6a8d 100644 --- a/frontend/app/components/monitoring-form/monitoring-form.component.spec.ts +++ b/frontend/app/components/monitoring-form/monitoring-form.component.spec.ts @@ -1,6 +1,5 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - import { MonitoringFormComponent } from './monitoring-form.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; describe('MonitoringFormComponent', () => { let component: MonitoringFormComponent; diff --git a/frontend/app/components/monitoring-form/monitoring-form.component.ts b/frontend/app/components/monitoring-form/monitoring-form.component.ts index 3e0ae5634..0ac9e2c2d 100644 --- a/frontend/app/components/monitoring-form/monitoring-form.component.ts +++ b/frontend/app/components/monitoring-form/monitoring-form.component.ts @@ -7,6 +7,21 @@ import { DataUtilsService } from '../../services/data-utils.service'; import { CommonService } from '@geonature_common/service/common.service'; import { DynamicFormService } from '@geonature_common/form/dynamic-form-generator/dynamic-form.service'; import { ActivatedRoute } from '@angular/router'; +import { JsonData } from '../../types/jsondata'; +import { SitesService } from '../../services/api-geom.service'; +import { + concatMap, + distinctUntilChanged, + exhaustMap, + mergeMap, + switchMap, + tap, + toArray, +} from 'rxjs/operators'; +import { EMPTY, from, iif, of } from 'rxjs'; +import { FormService } from '../../services/form.service'; +import { Router } from '@angular/router'; +import { TOOLTIPMESSAGEALERT } from '../../constants/guard'; @Component({ selector: 'pnx-monitoring-form', @@ -35,6 +50,15 @@ export class MonitoringFormComponent implements OnInit { meta: {}; + objFormDynamic: FormGroup = this._formBuilder.group({}); + objFormsDefinitionDynamic; + typesSiteConfig: JsonData = {}; + schemaUpdate = {}; + idsTypesSite: number[] = []; + lastGeom = {}; + dataComplement = {}; + schemaGeneric = {}; + public bSaveSpinner = false; public bSaveAndAddChildrenSpinner = false; public bDeleteSpinner = false; @@ -45,28 +69,49 @@ export class MonitoringFormComponent implements OnInit { public queryParams = {}; + canDelete: boolean; + canUpdate: boolean; + toolTipNotAllowed: string = TOOLTIPMESSAGEALERT; + constructor( private _formBuilder: FormBuilder, private _route: ActivatedRoute, private _configService: ConfigService, private _commonService: CommonService, private _dataUtilsService: DataUtilsService, - private _dynformService: DynamicFormService + private _dynformService: DynamicFormService, + private _siteService: SitesService, + private _formService: FormService, + private _router: Router ) {} ngOnInit() { + this.initPermission(); this._configService .init(this.obj.moduleCode) - .pipe() - .subscribe(() => { + .pipe( + mergeMap(() => + iif( + () => this.obj.objectType == 'site' && this.obj.id != undefined, + this._siteService.getTypesSiteByIdSite(this.obj.id), + of(null) + ) + ) + ) + .subscribe((typesSites) => { // return this._route.queryParamMap; // }) // .subscribe((queryParams) => { this.queryParams = this._route.snapshot.queryParams || {}; this.bChainInput = this._configService.frontendParams()['bChainInput']; - const schema = this.obj.schema(); + this.schemaGeneric = this.obj.schema(); + this.obj.objectType == 'site' ? delete this.schemaGeneric['types_site'] : null; + this.obj.id != undefined && this.obj.objectType == 'site' + ? this.initExtraSchema(typesSites) + : null; // init objFormsDefinition + const schema = this.schemaGeneric; // meta pour les parametres dynamiques // ici pour avoir acces aux nomenclatures this.meta = { @@ -107,8 +152,8 @@ export class MonitoringFormComponent implements OnInit { } // pour donner la valeur de idParent - - this.initForm(); + this.obj.objectType == 'site' ? this.initObjFormDef() : null; + this.obj.objectType == 'site' ? this.firstInitForm() : this.initForm(); }); } @@ -147,6 +192,49 @@ export class MonitoringFormComponent implements OnInit { }); } + firstInitForm() { + if ( + !(this.objFormDynamic && this.obj.bIsInitialized) && + !(this.objForm && this.obj.bIsInitialized) + ) { + return; + } + + this.setQueryParams(); + // pour donner la valeur de l'objet au formulaire + this.obj + .formValues() + .pipe( + exhaustMap((formValue) => { + this.objForm.patchValue(formValue); + this.setDefaultFormValue(); + return of(true); + }), + concatMap(() => { + return this.obj.formValues(this.schemaUpdate); + }) + ) + .subscribe((formValue) => { + formValue.types_site = this.idsTypesSite; + this.objFormDynamic.disable(); + this.objFormDynamic.patchValue(formValue, { onlySelf: true, emitEvent: false }); + this.objFormDynamic.enable(); + }); + } + + initFormDynamic() { + if (!(this.objFormDynamic && this.obj.bIsInitialized)) { + return; + } + // pour donner la valeur de l'objet au formulaire + this.obj.formValues(this.schemaUpdate).subscribe((formValue) => { + formValue.types_site = this.idsTypesSite; + this.objFormDynamic.disable(); + this.objFormDynamic.patchValue(formValue, { onlySelf: true, emitEvent: false }); + this.objFormDynamic.enable(); + // reset geom ? + }); + } keepNames() { return this.obj.configParam('keep') || []; } @@ -242,10 +330,23 @@ export class MonitoringFormComponent implements OnInit { } /** TODO améliorer site etc.. */ - onSubmit() { + onSubmit(isAddChildrend = false) { + isAddChildrend + ? (this.bSaveAndAddChildrenSpinner = this.bAddChildren = true) + : (this.bSaveSpinner = true); + if (this.obj.objectType == 'site') { + this.dataComplement = { ...this.typesSiteConfig, types_site: this.idsTypesSite }; + } + let objFormValueGroup = {}; + this.obj.objectType == 'site' + ? (objFormValueGroup = { ...this.objForm.value, ...this.objFormDynamic.value }) + : (objFormValueGroup = this.objForm.value); + this.obj.objectType == 'site' + ? Object.assign(this.obj.config['specific'], this.schemaUpdate) + : null; const action = this.obj.id - ? this.obj.patch(this.objForm.value) - : this.obj.post(this.objForm.value); + ? this.obj.patch(objFormValueGroup, this.dataComplement) + : this.obj.post(objFormValueGroup, this.dataComplement); const actionLabel = this.obj.id ? 'Modification' : 'Création'; action.subscribe((objData) => { this._commonService.regularToaster('success', this.msgToaster(actionLabel)); @@ -273,6 +374,11 @@ export class MonitoringFormComponent implements OnInit { onCancelEdit() { if (this.obj.id) { + const urlTree = this._router.parseUrl(this._router.url); + const urlWithoutParams = urlTree.root.children['primary'].segments + .map((it) => it.path) + .join('/'); + this._router.navigate([urlWithoutParams]); this.bEditChange.emit(false); } else { this.navigateToParent(); @@ -281,13 +387,11 @@ export class MonitoringFormComponent implements OnInit { onDelete() { this.bDeleteSpinner = true; - this._commonService.regularToaster('info', this.msgToaster('Suppression')); - this.obj.delete().subscribe((objData) => { this.bDeleteSpinner = this.bDeleteModal = false; this.obj.deleted = true; this.objChanged.emit(this.obj); - + this._commonService.regularToaster('info', this.msgToaster('Suppression')); setTimeout(() => { this.navigateToParent(); }, 100); @@ -295,6 +399,21 @@ export class MonitoringFormComponent implements OnInit { } onObjFormValueChange(event) { + // Check si types_site est modifié + const change = this.obj.change(); + if (!change) { + return; + } + setTimeout(() => { + change({ objForm: this.objForm, meta: this.meta }); + }, 100); + } + + onObjFormValueChangeDynamic(event) { + // Check si types_site est modifié + if (event.types_site != null && event.types_site.length != this.idsTypesSite.length) { + this.checkChangedTypeSite(); + } const change = this.obj.change(); if (!change) { return; @@ -317,4 +436,247 @@ export class MonitoringFormComponent implements OnInit { // patch pour recalculers this.procesPatchUpdateForm(); } + + initExtraSchema(typeSiteObj) { + let keysConfigToExclude: string[] = []; + for (const typeSite of typeSiteObj) { + this.idsTypesSite.push(typeSite.id_nomenclature_type_site); + this.typesSiteConfig[typeSite.label] = typeSite; + keysConfigToExclude.push( + ...Object.keys(this.typesSiteConfig[typeSite.label].config.specific) + ); + Object.assign(this.schemaUpdate, this.typesSiteConfig[typeSite.label].config.specific); + } + if (!this.obj.id) { + this.schemaUpdate = keysConfigToExclude + .filter((key) => !Object.keys(this.schemaGeneric).includes(key)) + .reduce((obj, key) => { + obj[key] = this.schemaUpdate[key]; + return obj; + }, {}); + } + + this.schemaGeneric = Object.keys(this.schemaGeneric) + .filter((key) => !keysConfigToExclude.includes(key)) + .reduce((obj, key) => { + obj[key] = this.schemaGeneric[key]; + return obj; + }, {}); + } + + checkChangedTypeSite() { + console.log(this.typesSiteConfig); + if ('types_site' in this.objFormDynamic.controls) { + this.objFormDynamic.controls['types_site'].valueChanges + .pipe( + distinctUntilChanged(), + switchMap((idsTypesSite) => + iif( + () => idsTypesSite == undefined || idsTypesSite.length == 0, + of(null), + from(idsTypesSite).pipe( + mergeMap((idTypeSite: number) => { + return this._siteService.getTypesSiteById(idTypeSite); + }), + toArray() + ) + ) + ) + ) + .subscribe( + (typeSiteArray) => { + if (typeSiteArray == null) { + this.removExtrForm(); + } else { + for (const typeSite of typeSiteArray) { + this.typesSiteConfig[typeSite.label] = typeSite; + } + this.updateObj(); + } + }, + (err) => { + console.log(err); + } + ); + } + } + + updateObj() { + this.dataComplement = {}; + const currentIdsTypeSite = this.objFormDynamic.controls.types_site.value; + let schema = {}; + let objKeysFormToRemove: string[] = []; + let objKeysFormToAdd = []; + let schemObjToUpdate = {}; + let objFormToAdd = {}; + let htmlToIgnore: string[] = []; + if (this.idsTypesSite.length == 0) { + schema = this.schemaUpdate; + this.idsTypesSite = []; + for (const keysType of Object.keys(this.typesSiteConfig)) { + for (const keysConfig of Object.keys(this.typesSiteConfig[keysType].config.specific)) { + if (this.typesSiteConfig[keysType].config.specific[keysConfig].type_widget != 'html') { + objFormToAdd[keysConfig] = null; + } + // schema[keysConfig] = this.typesSiteConfig[keysType].config.specific[keysConfig] + } + Object.assign(schema, this.typesSiteConfig[keysType].config.specific); + let idNomencalature = this.typesSiteConfig[keysType]['id_nomenclature_type_site']; + this.idsTypesSite.push(idNomencalature); + } + this.objFormDynamic.disable(); + this.objFormDynamic.patchValue(objFormToAdd, { onlySelf: true, emitEvent: false }); + this.objFormDynamic.enable(); + } else if ( + this.idsTypesSite.length > 0 && + currentIdsTypeSite.length < this.idsTypesSite.length + ) { + schema = {}; + const schemaObj = this.obj.schema(); + schema['types_site'] = schemaObj['types_site']; + let newTypeSiteConfig = {}; + for (const keysType of Object.keys(this.typesSiteConfig)) { + // for (const keysConfig of Object.keys(this.typesSiteConfig[keysType].config.specific)){ + if ( + !currentIdsTypeSite.includes(this.typesSiteConfig[keysType]['id_nomenclature_type_site']) + ) { + objKeysFormToRemove.push(...Object.keys(this.typesSiteConfig[keysType].config.specific)); + } else { + newTypeSiteConfig[keysType] = this.typesSiteConfig[keysType]; + newTypeSiteConfig['types_site'] = + this.typesSiteConfig[keysType]['id_nomenclature_type_site']; + Object.assign(schema, this.typesSiteConfig[keysType].config.specific); + } + } + this.idsTypesSite = this.idsTypesSite.filter((elem) => currentIdsTypeSite.includes(elem)); + const objFiltered = Object.keys(this.objFormDynamic.value) + .filter((key) => !objKeysFormToRemove.includes(key)) + .reduce((obj, key) => { + obj[key] = this.objFormDynamic.value[key]; + return obj; + }, {}); + this.typesSiteConfig = newTypeSiteConfig; + this.objFormDynamic.disable(); + this.objFormDynamic.patchValue(objFiltered, { onlySelf: true, emitEvent: false }); + this.objFormDynamic.enable(); + } else { + schema = this.schemaUpdate; + for (const keysType of Object.keys(this.typesSiteConfig)) { + for (const keysConfig of Object.keys(this.typesSiteConfig[keysType].config.specific)) { + if (this.typesSiteConfig[keysType].config.specific[keysConfig].type_widget == 'html') + htmlToIgnore.push(keysConfig); + } + objKeysFormToAdd.push(...Object.keys(this.typesSiteConfig[keysType].config.specific)); + Object.assign(schemObjToUpdate, this.typesSiteConfig[keysType].config.specific); + } + const schemaObjFilter = Object.keys(schemObjToUpdate) + .filter((key) => !Object.keys(schema).includes(key) && key) + .reduce((obj, key) => { + obj[key] = schemObjToUpdate[key]; + return obj; + }, {}); + + Object.assign(schema, schemaObjFilter); + this.idsTypesSite = currentIdsTypeSite; + const objFormToAdd = objKeysFormToAdd + .filter((key) => !Object.keys(this.objFormDynamic.value).includes(key)) + .reduce((obj, key) => { + obj[key] = null; + return obj; + }, {}); + Object.keys(objFormToAdd).length == 0 + ? null + : (this.objFormDynamic.disable(), + this.objFormDynamic.patchValue(objFormToAdd, { onlySelf: true, emitEvent: false }), + this.objFormDynamic.enable()); + } + + this.initObjFormDef(schema); + // this.objFormsDefinitionDynamic = this._dynformService + // .formDefinitionsdictToArray(schema, this.meta) + // .filter((formDef) => formDef.type_widget) + // .sort((a, b) => { + // // medias à la fin + // return a.attribut_name === 'medias' ? +1 : b.attribut_name === 'medias' ? -1 : 0; + // }); + + // display_form pour customiser l'ordre dans le formulaire + // les éléments de display form sont placé en haut dans l'ordre du tableau + // tous les éléments non cachés restent affichés + let displayProperties = [...(this.obj.configParam('display_properties') || [])]; + if (displayProperties && displayProperties.length) { + displayProperties.reverse(); + this.objFormsDefinitionDynamic.sort((a, b) => { + let indexA = displayProperties.findIndex((e) => e == a.attribut_name); + let indexB = displayProperties.findIndex((e) => e == b.attribut_name); + return indexB - indexA; + }); + // this.initForm() + } + this.dataComplement = { ...this.typesSiteConfig, types_site: this.idsTypesSite }; + } + + removExtrForm() { + this.schemaUpdate = {}; + let objKeysFormToRemove: string[] = []; + const currentIdsTypeSite = this.objFormDynamic.controls.types_site.value; + // Cas ou plus aucun types site + if (currentIdsTypeSite.length == 0) { + this.idsTypesSite = []; + for (const keysType of Object.keys(this.typesSiteConfig)) { + if (keysType != 'types_site') { + objKeysFormToRemove.push(...Object.keys(this.typesSiteConfig[keysType].config.specific)); + } + } + } + + const objFiltered = Object.keys(this.objFormDynamic.value) + .filter((key) => !objKeysFormToRemove.includes(key)) + .reduce((obj, key) => { + obj[key] = this.objFormDynamic.value[key]; + return obj; + }, {}); + + this.initObjFormDef(); + + this.objFormDynamic.disable(); + this.objFormDynamic.patchValue(objFiltered, { onlySelf: true, emitEvent: false }); + this.objFormDynamic.enable(); + this.typesSiteConfig = {}; + this.dataComplement = {}; + } + + initObjFormDef(schema = null) { + if (schema) { + this.schemaUpdate = schema; + } else { + const schema = this.obj.schema(); + this.schemaUpdate['types_site'] = schema['types_site']; + } + + this.objFormsDefinitionDynamic = this._dynformService + .formDefinitionsdictToArray(this.schemaUpdate, this.meta) + .filter((formDef) => formDef.type_widget) + .sort((a, b) => { + return a.attribut_name === 'types_site' ? -1 : b.attribut_name === 'types_site' ? +1 : 0; + }); + } + + initPermission() { + this.canDelete = + this.obj.objectType == 'module' + ? this.currentUser?.moduleCruved[this.obj.objectType]['D'] > 0 + : this.obj.cruved['D'] && !['site', 'sites_group'].includes(this.obj.objectType); + this.canUpdate = + this.obj.objectType == 'module' + ? this.currentUser?.moduleCruved[this.obj.objectType]['U'] > 0 + : this.obj.cruved['U']; + } + + notAllowedMessage() { + this._commonService.translateToaster( + 'warning', + "Vous n'avez pas les permissions nécessaires pour éditer l'objet" + ); + } } diff --git a/frontend/app/components/monitoring-lists/monitoring-list.component.spec.ts b/frontend/app/components/monitoring-lists/monitoring-list.component.spec.ts index d9be7770d..d7a5337cd 100644 --- a/frontend/app/components/monitoring-lists/monitoring-list.component.spec.ts +++ b/frontend/app/components/monitoring-lists/monitoring-list.component.spec.ts @@ -1,6 +1,5 @@ -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; - import { MonitoringListComponent } from './monitoring-lists.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; describe('MonitoringListComponent', () => { let component: MonitoringListComponent; diff --git a/frontend/app/components/monitoring-lists/monitoring-lists.component.css b/frontend/app/components/monitoring-lists/monitoring-lists.component.css index 47d8bad7e..535d451fb 100644 --- a/frontend/app/components/monitoring-lists/monitoring-lists.component.css +++ b/frontend/app/components/monitoring-lists/monitoring-lists.component.css @@ -1,11 +1,21 @@ .object-link:hover { - color:lightblue; + color: lightblue; } .btn-height { - height: 40px; + height: 40px; } .btn-float-right { - margin: 5px 0; + margin: 5px 0; +} + +button:disabled { + cursor: not-allowed; + pointer-events: all !important; +} + +.isDisableBtn { + cursor: not-allowed; + text-decoration: none; } diff --git a/frontend/app/components/monitoring-lists/monitoring-lists.component.html b/frontend/app/components/monitoring-lists/monitoring-lists.component.html index f317a7990..3e7e5ce11 100644 --- a/frontend/app/components/monitoring-lists/monitoring-lists.component.html +++ b/frontend/app/components/monitoring-lists/monitoring-lists.component.html @@ -1,7 +1,5 @@
- - + label="{{ child0.template['label_list'] }} {{ displayNumber(child0.objectType) }}" + >
-
+
+ filter_alt + + + Ajouter + {{ child0.template['label_art_undef_new'] || '' }} +
@@ -56,6 +64,4 @@ - -
diff --git a/frontend/app/components/monitoring-lists/monitoring-lists.component.ts b/frontend/app/components/monitoring-lists/monitoring-lists.component.ts index 50bb1a8d2..6461e49a7 100644 --- a/frontend/app/components/monitoring-lists/monitoring-lists.component.ts +++ b/frontend/app/components/monitoring-lists/monitoring-lists.component.ts @@ -5,6 +5,7 @@ import { ConfigService } from '../../services/config.service'; import { MonitoringObject } from '../../class/monitoring-object'; import { Utils } from '../../utils/utils'; +import { TOOLTIPMESSAGEALERT } from '../../constants/guard'; @Component({ selector: 'pnx-monitoring-lists', @@ -37,6 +38,8 @@ export class MonitoringListComponent implements OnInit { @Input() objectsStatus: Object; @Output() objectsStatusChange: EventEmitter = new EventEmitter(); + canCreateChild: { [key: string]: boolean } = {}; + toolTipNotAllowed: string = TOOLTIPMESSAGEALERT; constructor(private _configService: ConfigService) {} ngOnInit() { @@ -62,9 +65,17 @@ export class MonitoringListComponent implements OnInit { // datatable this.childrenDataTable = this.obj.childrenColumnsAndRows('display_list'); + this.initPermission(); // this.medias = this.obj.children['media'] && this.obj.children['media'].map(e => e.properties); } + initPermission() { + for (const child of this.children0Array) { + const childType = child['objectType']; + this.canCreateChild[childType] = this.currentUser?.moduleCruved[childType].C > 0; + } + } + onSelectedChildren(typeObject, event) { this.objectsStatus[typeObject] = event; let status_type = Utils.copy(this.objectsStatus); diff --git a/frontend/app/components/monitoring-map-list/monitoring-map-list.component.css b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.css new file mode 100644 index 000000000..f8f5803fb --- /dev/null +++ b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.css @@ -0,0 +1,81 @@ +.flex-container { + background-color: rgb(240, 240, 240); + display: flex; +} + +.flex-container > div { + width: 50%; + padding: 10px; + margin: 10px; +} + +.flex-container > div:first-child { + margin-right: 0; +} + +.scroll { + overflow-y: scroll; +} + +:host ::ng-deep .cadre { + background-color: white; + /* border: 1px solid grey;*/ + border-radius: 5px; + padding: 5px; + margin: 5px; + /* display: inline-block; */ +} + +/* TABLE */ + +.cell-link { + cursor: pointer; +} + +:host::ng-deep .datatable-body-row.active .datatable-row-group { + background-color: rgb(117, 227, 118) !important; +} + +.link:hover { + background-color: rgba(0, 0, 0, 0.2) !important; + transition: background-color 0.5; +} + +.link { + display: inline; + transition: background-color 0.5s; + border-radius: 5px; +} + +.header-filter-span > input { + width: 100%; +} + +.header-sort-span { + /* width: 100%; */ + cursor: pointer; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; +} + +.header-sort-span:hover { + background-color: rgb(245, 245, 245); +} + +.icon-sort { + font-size: 1.2em; + float: right; +} + +:host::ng-deep .sort-btn { + display: none !important; +} + +.custom-dt { + box-shadow: none !important; +} + +:host ::ng-deep .hide-draw-form .leaflet-top.leaflet-left { + display: none; +} diff --git a/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html new file mode 100644 index 000000000..0dee9d84c --- /dev/null +++ b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.html @@ -0,0 +1,23 @@ +
+
+
+ + + +
+
+
+
+
+ + + +
+
+
+
diff --git a/frontend/app/components/monitoring-map-list/monitoring-map-list.component.ts b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.ts new file mode 100644 index 000000000..a64919a8d --- /dev/null +++ b/frontend/app/components/monitoring-map-list/monitoring-map-list.component.ts @@ -0,0 +1,35 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { tap, mergeMap, map, distinctUntilChanged } from 'rxjs/operators'; +import { SiteSiteGroup } from '../../interfaces/objObs'; +import { MonitoringSitesComponent } from '../monitoring-sites/monitoring-sites.component'; +import { FormService } from '../../services/form.service'; +import { ApiGeomService } from '../../services/api-geom.service'; +import { ConfigJsonService } from '../../services/config-json.service'; +import { ObjectService } from '../../services/object.service'; +import { FormGroup } from '@angular/forms'; +@Component({ + selector: 'monitoring-map-list.component', + templateUrl: './monitoring-map-list.component.html', + styleUrls: ['./monitoring-map-list.component.css'], +}) +export class MonitoringMapListComponent { + // TODO: object needed to manage map + obj: any; + bEdit: boolean; + objForm: FormGroup; + // + displayMap: boolean = true; + siteSiteGroup: SiteSiteGroup | null = null; + apiService: ApiGeomService; + constructor(private _formService: FormService) {} + + onActivate(component) { + this._formService.currentFormMap + .pipe(distinctUntilChanged((prev, curr) => prev.obj === curr.obj)) + .subscribe((formMapObj) => { + this.obj = formMapObj.obj; + this.bEdit = formMapObj.bEdit; + this.objForm = formMapObj.frmGp; + }); + } +} diff --git a/frontend/app/components/monitoring-map/monitoring-map.component.css b/frontend/app/components/monitoring-map/monitoring-map.component.css index 36556bbab..0aa58a60a 100644 --- a/frontend/app/components/monitoring-map/monitoring-map.component.css +++ b/frontend/app/components/monitoring-map/monitoring-map.component.css @@ -1,3 +1,3 @@ :host ::ng-deep .hide-draw-form .leaflet-top.leaflet-left { - display: none; - } + display: none; +} diff --git a/frontend/app/components/monitoring-map/monitoring-map.component.html b/frontend/app/components/monitoring-map/monitoring-map.component.html index 98b74ab15..45a74de0f 100644 --- a/frontend/app/components/monitoring-map/monitoring-map.component.html +++ b/frontend/app/components/monitoring-map/monitoring-map.component.html @@ -1,10 +1,10 @@
- - + { let component: MonitoringMapComponent; diff --git a/frontend/app/components/monitoring-object/monitoring-object.component.css b/frontend/app/components/monitoring-object/monitoring-object.component.css index a48080d41..effbbfb96 100644 --- a/frontend/app/components/monitoring-object/monitoring-object.component.css +++ b/frontend/app/components/monitoring-object/monitoring-object.component.css @@ -35,7 +35,6 @@ font-weight: bold; } - :host ::ng-deep .cadre { background-color: white; /* border: 1px solid grey;*/ diff --git a/frontend/app/components/monitoring-object/monitoring-object.component.html b/frontend/app/components/monitoring-object/monitoring-object.component.html index b34e54fff..9a646485b 100644 --- a/frontend/app/components/monitoring-object/monitoring-object.component.html +++ b/frontend/app/components/monitoring-object/monitoring-object.component.html @@ -2,11 +2,7 @@

Chargement en cours

- +
@@ -32,7 +28,11 @@

Chargement en cours

>
- +

Le module n'est pas encore configuré.

diff --git a/frontend/app/components/monitoring-object/monitoring-object.component.ts b/frontend/app/components/monitoring-object/monitoring-object.component.ts index eab3efe1a..ecda56484 100644 --- a/frontend/app/components/monitoring-object/monitoring-object.component.ts +++ b/frontend/app/components/monitoring-object/monitoring-object.component.ts @@ -1,5 +1,15 @@ import { Observable, of, forkJoin } from 'rxjs'; -import { mergeMap, concatMap } from 'rxjs/operators'; +import { + mergeMap, + concatMap, + map, + tap, + take, + takeUntil, + distinctUntilChanged, + catchError, + skipWhile, +} from 'rxjs/operators'; import { MonitoringObject } from '../../class/monitoring-object'; import { Component, OnInit } from '@angular/core'; @@ -13,8 +23,10 @@ import { DataUtilsService } from '../../services/data-utils.service'; import { AuthService, User } from '@geonature/components/auth/auth.service'; import { CommonService } from '@geonature_common/service/common.service'; import { MapService } from '@geonature_common/map/map.service'; +import { ObjectService } from '../../services/object.service'; import { Utils } from '../../utils/utils'; +import { ConfigJsonService } from '../../services/config-json.service'; @Component({ selector: 'pnx-object', templateUrl: './monitoring-object.component.html', @@ -31,6 +43,7 @@ export class MonitoringObjectComponent implements OnInit { objForm: FormGroup; + checkEditParam: boolean; bEdit = false; bLoadingModal = false; @@ -40,6 +53,7 @@ export class MonitoringObjectComponent implements OnInit { heightMap; moduleSet = false; + bDeleteModal = false; constructor( private _route: ActivatedRoute, @@ -49,7 +63,8 @@ export class MonitoringObjectComponent implements OnInit { private _formBuilder: FormBuilder, public mapservice: MapService, private _auth: AuthService, - private _commonService: CommonService + private _commonService: CommonService, + private _evtObjService: ObjectService ) {} ngAfterViewInit() { @@ -90,6 +105,7 @@ export class MonitoringObjectComponent implements OnInit { .subscribe(() => { this.obj.initTemplate(); // pour le html + this.bEdit = this.checkEditParam == true ? true : false; // si on est sur une création (pas d'id et id_parent ou pas de module_code pour module (root)) this.bEdit = this.bEdit || @@ -103,6 +119,8 @@ export class MonitoringObjectComponent implements OnInit { } else { this.initObjectsStatus(); } + + this.evenListnerTable(); }); } @@ -229,6 +247,11 @@ export class MonitoringObjectComponent implements OnInit { ); this.objForm = this._formBuilder.group({}); + if (params.get('edit')) { + this.checkEditParam = Boolean(params.get('edit')); + } else { + this.checkEditParam = false; + } // query param snapshot // this.obj.parentId = params.get('parentId') && parseInt(params.get('parentId')); @@ -239,6 +262,22 @@ export class MonitoringObjectComponent implements OnInit { initConfig(): Observable { return this._configService.init(this.obj.moduleCode).pipe( + concatMap(() => { + if (this.obj.objectType == 'site' && this.obj.id != null) { + return this._objService + .configService() + .loadConfigSpecificConfig(this.obj) + .pipe( + tap((config) => { + this.obj.template_specific = this._objService + .configService() + .addSpecificConfig(config); + }) + ); + } else { + return of(null); + } + }), mergeMap(() => { this.frontendModuleMonitoringUrl = this._configService.frontendModuleMonitoringUrl(); this.backendUrl = this._configService.backendUrl(); @@ -278,4 +317,40 @@ export class MonitoringObjectComponent implements OnInit { } this.getModuleSet(); } + + onDeleteFromTable(event) { + return this._objService + .dataMonitoringObjectService() + .deleteObject(this.obj.moduleCode, event.objectType, event.rowSelected.id); + } + + evenListnerTable() { + const $displayModal = this._evtObjService.currentDeleteModal; + const $rowSelected = this._evtObjService.currentRowSelected; + + $displayModal + .pipe( + distinctUntilChanged((prev, curr) => prev === curr), + tap((displayModal) => { + this.bDeleteModal = displayModal; + }), + concatMap(() => { + return $rowSelected; + }), + concatMap((rowSelected) => { + return this.onDeleteFromTable(rowSelected).pipe( + distinctUntilChanged((prev, curr) => prev.rowSelected === curr.rowSelected) + ); + }), + catchError((err) => { + console.log(err); + this._evtObjService.changeDisplayingDeleteModal(false); + return of(null); + }) + ) + .subscribe((deletedObj) => { + this.initSites(); + this._evtObjService.changeDisplayingDeleteModal(false); + }); + } } diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.css b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.css new file mode 100644 index 000000000..14aa0b367 --- /dev/null +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.css @@ -0,0 +1,37 @@ +table { + font-size: small; +} + +th { + text-align: right; +} + +.key { + font-weight: bold; +} + +td { + padding-left: 20px; +} + +.small-icon { + font-size: 18px; +} + +.medias-tab { + margin: 10px; +} + +.hide-spinner { + display: none; +} + +::ng-deep .cdk-global-overlay-wrapper, +::ng-deep .cdk-overlay-container { + z-index: 99999 !important; +} + +button:disabled { + cursor: not-allowed; + pointer-events: all !important; +} diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html new file mode 100644 index 000000000..fa0ca2450 --- /dev/null +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.html @@ -0,0 +1,234 @@ +
+
+ + +
+ + + + + +
+ {{ fields[fieldName] }} + help + {{ selectedObj[fieldName] }}
+
+
+ +
+ + + + + +
+ {{ specificFields[fieldName.value] }} + help + {{ selectedObj.data[fieldName.value] }}
+
+
+ + +
+ + +
+
+ + + + + + diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.spec.ts b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.spec.ts new file mode 100644 index 000000000..5a0b578e5 --- /dev/null +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.spec.ts @@ -0,0 +1,23 @@ +import { MonitoringPropertiesComponent } from './monitoring-properties-g.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +describe('MonitoringPropertiesGComponent', () => { + let component: MonitoringPropertiesComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [MonitoringPropertiesComponent], + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(MonitoringPropertiesComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts new file mode 100644 index 000000000..d41b3c3da --- /dev/null +++ b/frontend/app/components/monitoring-properties-g/monitoring-properties-g.component.ts @@ -0,0 +1,96 @@ +import { Component, EventEmitter, Input, OnInit, Output, SimpleChanges } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { Subscription } from 'rxjs'; + +import { TOOLTIPMESSAGEALERT } from '../../constants/guard'; +import { ISitesGroup } from '../../interfaces/geom'; +import { IobjObs, ObjDataType } from '../../interfaces/objObs'; +import { FormService } from '../../services/form.service'; +import { ObjectService } from '../../services/object.service'; +import { JsonData } from '../../types/jsondata'; +import { TPermission } from '../../types/permission'; +import { ObjectsPermissionMonitorings } from '../../enum/objectPermission'; + +@Component({ + selector: 'pnx-monitoring-properties-g', + templateUrl: './monitoring-properties-g.component.html', + styleUrls: ['./monitoring-properties-g.component.css'], +}) +export class MonitoringPropertiesGComponent implements OnInit { + // selectedObj: ISitesGroup; + @Input() selectedObj: ObjDataType; + @Input() selectedObjRaw: ObjDataType; + @Input() bEdit: boolean; + @Output() bEditChange = new EventEmitter(); + objectType: IobjObs; + + @Input() newParentType; + color: string = 'white'; + dataDetails: ISitesGroup; + fields: JsonData; + fieldDefinitions: JsonData = {}; + fieldsNames: string[]; + endPoint: string; + datasetForm = new FormControl(); + _sub: Subscription; + + specificFields: JsonData; + specificFieldDefinitions: JsonData = {}; + specificFieldsNames: string[]; + + @Input() permission: TPermission; + + canUpdateObj: boolean; + + toolTipNotAllowed: string; + + constructor( + private _formService: FormService, + private _objService: ObjectService + ) {} + + ngOnInit() { + this.toolTipNotAllowed = TOOLTIPMESSAGEALERT; + } + + initProperties() { + this.objectType = this.newParentType; + this.fieldsNames = this.newParentType.template.fieldNames; + this.fields = this.newParentType.template.fieldLabels; + this.fieldDefinitions = this.newParentType.template.fieldDefinitions; + this.objectType.properties = this.selectedObj; + this.endPoint = this.newParentType.endPoint; + } + + initSpecificProperties() { + this.specificFieldsNames = this.newParentType.template_specific.fieldNames; + this.specificFields = this.newParentType.template_specific.fieldLabels; + this.specificFieldDefinitions = this.newParentType.template_specific.fieldDefinitions; + } + + onEditClick() { + this.selectedObjRaw['id'] = this.selectedObjRaw[this.selectedObjRaw.pk]; + this._formService.changeDataSub( + this.selectedObjRaw, + this.objectType.objectType, + this.objectType.endPoint + ); + this.bEditChange.emit(true); + } + + ngOnChanges(changes: SimpleChanges): void { + if (this.newParentType && this.newParentType.template.fieldNames.length != 0) { + this.initProperties(); + if (this.selectedObj) { + this.canUpdateObj = this.selectedObj['cruved']['U']; + } + if ( + this.newParentType.template_specific && + this.newParentType.template_specific.fieldNames && + this.newParentType.template_specific.fieldNames.length != 0 + ) { + this.initSpecificProperties(); + } + } + } +} diff --git a/frontend/app/components/monitoring-properties/monitoring-properties.component.css b/frontend/app/components/monitoring-properties/monitoring-properties.component.css index c2fc53c60..14aa0b367 100644 --- a/frontend/app/components/monitoring-properties/monitoring-properties.component.css +++ b/frontend/app/components/monitoring-properties/monitoring-properties.component.css @@ -26,8 +26,12 @@ td { display: none; } - ::ng-deep .cdk-global-overlay-wrapper, ::ng-deep .cdk-overlay-container { - z-index: 99999 !important; + z-index: 99999 !important; +} + +button:disabled { + cursor: not-allowed; + pointer-events: all !important; } diff --git a/frontend/app/components/monitoring-properties/monitoring-properties.component.html b/frontend/app/components/monitoring-properties/monitoring-properties.component.html index 667e8f286..ddc5190c3 100644 --- a/frontend/app/components/monitoring-properties/monitoring-properties.component.html +++ b/frontend/app/components/monitoring-properties/monitoring-properties.component.html @@ -1,10 +1,10 @@
-
+
- - + + +
+ {{ obj.template.fieldLabels[fieldName] }} help {{ obj.resolvedProperties[fieldName] }}{{ obj.resolvedProperties[fieldName] }}
+
+ + +
+ + + +
+ {{ fieldName.value }} + help + {{ obj.resolvedProperties[fieldName.key] }}
+
{{ media.title_fr }} - ({{ ms.typeMedia(media) - }} par {{ media.author }} par {{ media.author }})
-

{{media.description_fr}}

+

{{ media.description_fr }}

-