From 7e4dfea919542e3ed3d95399d4592ee298579333 Mon Sep 17 00:00:00 2001 From: Quentin Jouet Date: Wed, 9 Jun 2021 09:33:41 +0200 Subject: [PATCH 01/12] Fix encoding pb loading json schema --- backend/gncitizen/core/sites/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/gncitizen/core/sites/routes.py b/backend/gncitizen/core/sites/routes.py index 4c23b8ce..3e282685 100644 --- a/backend/gncitizen/core/sites/routes.py +++ b/backend/gncitizen/core/sites/routes.py @@ -85,7 +85,7 @@ def get_site(pk): def get_site_jsonschema(pk): try: site = SiteModel.query.get(pk) - with site.site_type.form_schema.open() as json_data: + with site.site_type.form_schema.open(encoding='utf-8') as json_data: data_dict = json.load(json_data) return data_dict, 200 except Exception as e: From 5f52cae8085c21846e8429aae755d628aff56ce6 Mon Sep 17 00:00:00 2001 From: Quentin Jouet Date: Wed, 8 Dec 2021 16:10:08 +0100 Subject: [PATCH 02/12] Update utils-flask-sqlalchemy version in requirements.txt --- backend/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/requirements.txt b/backend/requirements.txt index 4d3d73f0..99570476 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -43,7 +43,7 @@ sqlalchemy==1.4.15; (python_version >= "2.7" and python_full_version < "3.0.0") toml==0.10.2; (python_version >= "2.6" and python_full_version < "3.0.0") or (python_full_version >= "3.3.0") typing-extensions==3.10.0.0; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.6.0" and python_version < "3.8" and python_version >= "3.6" urllib3==1.26.4; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version < "4" -utils-flask-sqlalchemy @ git+https://github.com/hypsug0/Utils-Flask-SQLAlchemy@master +utils-flask-sqlalchemy @ git+https://github.com/PnX-SI/Utils-Flask-SQLAlchemy@master utils-flask-sqlalchemy-geo @ git+https://github.com/PnX-SI/Utils-Flask-SQLAlchemy-Geo@0.2.1 werkzeug==2.0.0; python_version >= "3.6" and python_full_version < "3.0.0" and python_version < "4" or python_full_version >= "3.5.0" and python_version >= "3.6" and python_version < "4" wtforms==2.3.3 From 50dde90f48895c49bc3bbfbcb5b3ad312cbf1eda Mon Sep 17 00:00:00 2001 From: Quentin Jouet Date: Mon, 13 Dec 2021 14:51:15 +0100 Subject: [PATCH 03/12] Auto select sites tab on user dashboard if no obs --- .../src/app/auth/user-dashboard/user-dashboard.component.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/app/auth/user-dashboard/user-dashboard.component.ts b/frontend/src/app/auth/user-dashboard/user-dashboard.component.ts index ad27c0cb..6e7d477e 100644 --- a/frontend/src/app/auth/user-dashboard/user-dashboard.component.ts +++ b/frontend/src/app/auth/user-dashboard/user-dashboard.component.ts @@ -167,6 +167,9 @@ export class UserDashboardComponent implements OnInit { // this.rowData(obs, coords); // this.obsExport(obs); }); + if (this.observations.length === 0 && this.mysites.features.length > 0) { + this.tab = 'sites' + } } else { this.observations = data[0].features; this.observations.forEach((obs) => { From 60ebeaaf2d358089f9ab8b97a76857f636411842 Mon Sep 17 00:00:00 2001 From: Quentin Jouet Date: Tue, 14 Dec 2021 09:41:07 +0100 Subject: [PATCH 04/12] Fix delete site feature --- backend/gncitizen/core/sites/routes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/gncitizen/core/sites/routes.py b/backend/gncitizen/core/sites/routes.py index 133e1cb8..b5874a11 100644 --- a/backend/gncitizen/core/sites/routes.py +++ b/backend/gncitizen/core/sites/routes.py @@ -442,7 +442,7 @@ def delete_site(site_id): .join(UserModel, SiteModel.id_role == UserModel.id_user, full=True) .first() ) - if current_user.id_user == site.id_role: + if current_user.id_user == site.SiteModel.id_role: SiteModel.query.filter_by(id_site=site_id).delete() db.session.commit() return ("Site deleted successfully"), 200 From 96c4078c0308a346a4df4b4625db4638b84c4121 Mon Sep 17 00:00:00 2001 From: Quentin Jouet Date: Tue, 14 Dec 2021 10:11:23 +0100 Subject: [PATCH 05/12] Auto redirect on program page if only 1 active program --- frontend/src/app/home/home.component.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/frontend/src/app/home/home.component.ts b/frontend/src/app/home/home.component.ts index 430b4a63..994cb594 100644 --- a/frontend/src/app/home/home.component.ts +++ b/frontend/src/app/home/home.component.ts @@ -47,6 +47,14 @@ export class HomeComponent implements OnInit, AfterViewChecked { this.observationsService .getStat() .subscribe((stats) => (this.stats = stats)); + if (this.programs.length === 1) { + const p = this.programs[0] + this.router.navigate([ + '/programs', + p.id_program, + p.module.name + ]); + } }); this.route.fragment.subscribe((fragment) => { this.fragment = fragment; From a81a1f1d58ad392dc61d5dcae62ad2111e73b2d4 Mon Sep 17 00:00:00 2001 From: Quentin Jouet Date: Fri, 7 Jan 2022 15:27:26 +0100 Subject: [PATCH 06/12] Add delete site visit feature --- backend/gncitizen/core/sites/routes.py | 21 +++ .../user-dashboard/user.service.service.ts | 6 + .../base/detail/detail.component.html | 55 ++++++++ .../programs/base/detail/detail.component.ts | 1 + .../programs/sites/detail/detail.component.ts | 124 +++++++++++------- 5 files changed, 163 insertions(+), 44 deletions(-) diff --git a/backend/gncitizen/core/sites/routes.py b/backend/gncitizen/core/sites/routes.py index b5874a11..ceaa1a43 100644 --- a/backend/gncitizen/core/sites/routes.py +++ b/backend/gncitizen/core/sites/routes.py @@ -452,6 +452,27 @@ def delete_site(site_id): return {"message": str(e)}, 500 +@sites_api.route("/visit/", methods=["DELETE"]) +@json_resp +@jwt_required() +def delete_visit(visit_id): + current_user = get_user_if_exists() + # try: + visit = ( + db.session.query(VisitModel) + .filter(VisitModel.id_visit == visit_id) + .first() + ) + if current_user.id_user == visit.id_role: + VisitModel.query.filter_by(id_visit=visit_id).delete() + db.session.commit() + return ("Site deleted successfully"), 200 + else: + return ("delete unauthorized"), 403 + # except Exception as e: + # return {"message": str(e)}, 500 + + @sites_api.route("/export/", methods=["GET"]) @jwt_required() def export_sites_xls(user_id): diff --git a/frontend/src/app/auth/user-dashboard/user.service.service.ts b/frontend/src/app/auth/user-dashboard/user.service.service.ts index 168aeef4..9bc65315 100644 --- a/frontend/src/app/auth/user-dashboard/user.service.service.ts +++ b/frontend/src/app/auth/user-dashboard/user.service.service.ts @@ -65,6 +65,12 @@ export class UserService { ); } + deleteSiteVisit(idVisit: number) { + return this.http.delete( + `${AppConfig.API_ENDPOINT}/sites/visit/${idVisit}` + ); + } + ConvertToCSV(objArray, headerList) { let array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray; diff --git a/frontend/src/app/programs/base/detail/detail.component.html b/frontend/src/app/programs/base/detail/detail.component.html index e9ab192e..de7f92eb 100644 --- a/frontend/src/app/programs/base/detail/detail.component.html +++ b/frontend/src/app/programs/base/detail/detail.component.html @@ -121,11 +121,41 @@

Photos

Description

+
{{ a.date | date: 'longDate' }} par {{ a.author }}
+ +   + + @@ -196,3 +226,28 @@
diff --git a/frontend/src/app/programs/sites/detail/detail.component.ts b/frontend/src/app/programs/sites/detail/detail.component.ts index f3c697e9..56f4f4b0 100644 --- a/frontend/src/app/programs/sites/detail/detail.component.ts +++ b/frontend/src/app/programs/sites/detail/detail.component.ts @@ -70,7 +70,12 @@ export class SiteDetailComponent if (this.site.properties.visits) { this.site.properties.visits.forEach((e) => { const data = e.json_data; - const visitData = { date: e.date, author: e.author, id: e.id_visit }; + const visitData = { + date: e.date, + author: e.author, + id: e.id_visit, + json_data: e.json_data + }; this.loadJsonSchema().subscribe((jsonschema: any) => { const schema = jsonschema.schema.properties; const custom_data = []; @@ -110,6 +115,10 @@ export class SiteDetailComponent this.flowService.addSiteVisit(this.site_id); } + editSiteVisit(visit_data) { + this.flowService.editSiteVisit(this.site_id, visit_data.id, visit_data); + } + openDelVisitModal(idVisitToDelete) { this.idVisitToDelete = idVisitToDelete; this.modalDelVisitRef = this.modalService.open(this.visitDeleteModal, { diff --git a/frontend/src/app/programs/sites/form/form.component.html b/frontend/src/app/programs/sites/form/form.component.html index 9c5fdcbb..4ee70999 100644 --- a/frontend/src/app/programs/sites/form/form.component.html +++ b/frontend/src/app/programs/sites/form/form.component.html @@ -60,7 +60,7 @@

diff --git a/frontend/src/app/programs/sites/form/form.component.ts b/frontend/src/app/programs/sites/form/form.component.ts index a345efec..89adc4ec 100644 --- a/frontend/src/app/programs/sites/form/form.component.ts +++ b/frontend/src/app/programs/sites/form/form.component.ts @@ -23,6 +23,7 @@ import { AppConfig } from '../../../../conf/app.config'; import { GNCFrameworkComponent } from '../../base/jsonform/framework/framework.component'; import { ngbDateMaxIsToday } from '../../observations/form/formValidators'; import { SiteService } from '../sites.service'; +import { cpuUsage } from 'process'; declare let $: any; @@ -35,6 +36,8 @@ declare let $: any; export class SiteVisitFormComponent implements OnInit, AfterViewInit { private readonly URL = AppConfig.API_ENDPOINT; @Input() site_id: number; + @Input() visit_id: number; + @Input() visit_data: any; today = new Date(); visitForm = new FormGroup({ date: new FormControl( @@ -78,8 +81,40 @@ export class SiteVisitFormComponent implements OnInit, AfterViewInit { // const that = this; this.loadJsonSchema().subscribe((data: any) => { this.initForm(data); + if (this.visit_id) { + this.initJsonData(this.visit_data.json_data) + const visit_date = new Date(this.visit_data.date); + this.visitForm.controls.date.value = { + year: visit_date.getFullYear(), + month: visit_date.getMonth() + 1, + day: visit_date.getDate() + }; + } }); } + initJsonData(visit_json_data) { + // Visit edition json data initialisation + this.jsonData = {} + if (this.jsonSchema.steps) { + this.jsonSchema.steps.forEach((step, index)=> { + this.jsonData[index + 1] = {} + step.layout.forEach ((elt) => { + if (elt.key && elt.key in visit_json_data) { + this.jsonData[index + 1][elt.key] = visit_json_data[elt.key]; + } else if (elt.type === 'section') { + elt.items.forEach((item) => { + if (item.key in visit_json_data) { + this.jsonData[index + 1][item.key] = visit_json_data[item.key] + } + }); + } + }) + }) + } else { + this.jsonData = visit_json_data // TODO is it correct ? + } + this.updateFormInput(); + } initForm(json_schema) { this.jsonSchema = json_schema; this.updatePartialLayout(); @@ -130,7 +165,7 @@ export class SiteVisitFormComponent implements OnInit, AfterViewInit { invalidStep() { return this.currentStep === 1 && this.visitForm.get('date').invalid; } - yourOnChangesFn(e) { + onJsonFormChange(e) { this.jsonData[this.currentStep] = e; } getTotalJsonData() { @@ -159,16 +194,18 @@ export class SiteVisitFormComponent implements OnInit, AfterViewInit { this.postSiteVisit().subscribe( (data) => { console.debug(data); - const visitId = data['features'][0]['id_visit']; - if (this.photos.length > 0) { - this.postVisitPhotos(visitId).subscribe( - (resp) => { - console.debug(resp); - this.siteService.newSiteCreated.emit(true); - }, - (err) => console.error(err), - () => console.log('photo upload done') - ); + if (!this.visit_id) { + const visitId = data['features'][0]['id_visit']; + if (this.photos.length > 0) { + this.postVisitPhotos(visitId).subscribe( + (resp) => { + console.debug(resp); + this.siteService.newSiteCreated.emit(true); + }, + (err) => console.error(err), + () => console.log('photo upload done') + ); + } } }, (err) => console.error(err), @@ -186,12 +223,21 @@ export class SiteVisitFormComponent implements OnInit, AfterViewInit { const visitDate = NgbDate.from(this.visitForm.controls.date.value); this.visitForm.patchValue({ data: this.getTotalJsonData(), - date: new Date(visitDate.year, visitDate.month, visitDate.day) + date: new Date(visitDate.year, visitDate.month - 1, visitDate.day) .toISOString() .match(/\d{4}-\d{2}-\d{2}/)[0], }); - return this.http.post( - `${this.URL}/sites/${this.site_id}/visits`, + let method = 'post'; + let url = `${this.URL}/sites/${this.site_id}/visits`; + if (this.visit_id) { + this.visitForm.patchValue({ + id_visit: this.visit_id + }); + method = 'patch'; + url = `${this.URL}/sites/visits/${this.visit_id}` + } + return this.http[method]( + url, this.visitForm.value, httpOptions ); diff --git a/frontend/src/app/programs/sites/modalflow/modalflow.service.ts b/frontend/src/app/programs/sites/modalflow/modalflow.service.ts index 67ebbbb2..a5741528 100644 --- a/frontend/src/app/programs/sites/modalflow/modalflow.service.ts +++ b/frontend/src/app/programs/sites/modalflow/modalflow.service.ts @@ -30,7 +30,7 @@ export class SiteModalFlowService extends ModalFlowService { ); } if (!init_data.updateData) { - items.push(new FlowItem(VisitStepComponent)); + items.push(new FlowItem(VisitStepComponent), { ...init_data, service: this }); } // else user only edits the site and do not attach visit // items.push(new FlowItem(RewardComponent, {service: this})); return items; @@ -41,6 +41,15 @@ export class SiteModalFlowService extends ModalFlowService { this.openFormModal(init_data); } + editSiteVisit (site_id, visit_id, visit_data) { + var init_data = { + site_id: site_id, + visit_id: visit_id, + visit_data: visit_data + }; + this.openFormModal(init_data); + } + openFormModal(init_data) { var flowitems = this.getFlowItems(init_data); var modalRef = this.open(FlowComponent); diff --git a/frontend/src/app/programs/sites/modalflow/steps/visit/visit_step.component.html b/frontend/src/app/programs/sites/modalflow/steps/visit/visit_step.component.html index 1456e7e1..5e425049 100644 --- a/frontend/src/app/programs/sites/modalflow/steps/visit/visit_step.component.html +++ b/frontend/src/app/programs/sites/modalflow/steps/visit/visit_step.component.html @@ -12,7 +12,11 @@

From ac6f56087e2d00546e3f59d4195f691bfb5deb3f Mon Sep 17 00:00:00 2001 From: Quentin Jouet Date: Tue, 11 Jan 2022 10:28:32 +0100 Subject: [PATCH 10/12] Update photos on site visit edit / create --- .../programs/sites/detail/detail.component.ts | 20 ++++++++----- .../app/programs/sites/form/form.component.ts | 29 +++++++++---------- 2 files changed, 27 insertions(+), 22 deletions(-) diff --git a/frontend/src/app/programs/sites/detail/detail.component.ts b/frontend/src/app/programs/sites/detail/detail.component.ts index 9797c30e..da51bb3c 100644 --- a/frontend/src/app/programs/sites/detail/detail.component.ts +++ b/frontend/src/app/programs/sites/detail/detail.component.ts @@ -11,6 +11,7 @@ import { } from '../../base/detail/detail.component'; import { UserService } from '../../../auth/user-dashboard/user.service.service'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { SiteService } from '../sites.service'; @Component({ selector: 'app-site-detail', @@ -34,7 +35,8 @@ export class SiteDetailComponent private programService: GncProgramsService, private userService: UserService, private modalService: NgbModal, - public flowService: SiteModalFlowService + public flowService: SiteModalFlowService, + public siteService: SiteService ) { super(); this.route.params.subscribe((params) => { @@ -48,14 +50,12 @@ export class SiteDetailComponent this.updateData(); } }); + this.siteService.siteEdited.subscribe(value => { + this.updateData(); + }); } prepareSiteData() { - this.photos = this.site.properties.photos; - this.photos.forEach((e, i) => { - this.photos[i]['url'] = - AppConfig.API_ENDPOINT + this.photos[i]['url']; - }); // setup map const map = L.map('map'); L.tileLayer('//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { @@ -70,7 +70,13 @@ export class SiteDetailComponent } prepareVisits() { - // prepare data + // photos + this.photos = this.site.properties.photos; + this.photos.forEach((e, i) => { + this.photos[i]['url'] = + AppConfig.API_ENDPOINT + this.photos[i]['url']; + }); + // data this.attributes = [] if (this.site.properties.visits) { this.site.properties.visits.forEach((e) => { diff --git a/frontend/src/app/programs/sites/form/form.component.ts b/frontend/src/app/programs/sites/form/form.component.ts index 89adc4ec..cc1bc031 100644 --- a/frontend/src/app/programs/sites/form/form.component.ts +++ b/frontend/src/app/programs/sites/form/form.component.ts @@ -84,11 +84,11 @@ export class SiteVisitFormComponent implements OnInit, AfterViewInit { if (this.visit_id) { this.initJsonData(this.visit_data.json_data) const visit_date = new Date(this.visit_data.date); - this.visitForm.controls.date.value = { + this.visitForm.controls.date.setValue({ year: visit_date.getFullYear(), month: visit_date.getMonth() + 1, day: visit_date.getDate() - }; + }); } }); } @@ -194,18 +194,17 @@ export class SiteVisitFormComponent implements OnInit, AfterViewInit { this.postSiteVisit().subscribe( (data) => { console.debug(data); - if (!this.visit_id) { - const visitId = data['features'][0]['id_visit']; - if (this.photos.length > 0) { - this.postVisitPhotos(visitId).subscribe( - (resp) => { - console.debug(resp); - this.siteService.newSiteCreated.emit(true); - }, - (err) => console.error(err), - () => console.log('photo upload done') - ); - } + const visitId = this.visit_id || data['features'][0]['id_visit']; + if (this.photos.length > 0) { + this.postVisitPhotos(visitId).subscribe( + (resp) => { + console.debug(resp); + this.siteService.newSiteCreated.emit(true); + this.siteService.siteEdited.emit(true); + }, + (err) => console.error(err), + () => console.log('photo upload done') + ); } }, (err) => console.error(err), @@ -218,7 +217,7 @@ export class SiteVisitFormComponent implements OnInit, AfterViewInit { const httpOptions = { headers: new HttpHeaders({ Accept: 'application/json', - }), + }) }; const visitDate = NgbDate.from(this.visitForm.controls.date.value); this.visitForm.patchValue({ From 10ae5ff3a9e94f4d3e8e0b0884d1218e382795ba Mon Sep 17 00:00:00 2001 From: Quentin Jouet Date: Tue, 11 Jan 2022 10:31:23 +0100 Subject: [PATCH 11/12] Set modalCloseStatus public --- .../app/programs/observations/modalflow/modalflow.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/app/programs/observations/modalflow/modalflow.service.ts b/frontend/src/app/programs/observations/modalflow/modalflow.service.ts index 04e3176a..ff806267 100644 --- a/frontend/src/app/programs/observations/modalflow/modalflow.service.ts +++ b/frontend/src/app/programs/observations/modalflow/modalflow.service.ts @@ -25,7 +25,7 @@ export const MODAL_DEFAULTS: NgbModalOptions = { }) export class ModalFlowService extends FlowService { modalRef: NgbModalRef; - private modalCloseStatus: BehaviorSubject = new BehaviorSubject( + public modalCloseStatus: BehaviorSubject = new BehaviorSubject( null ); display: boolean = false; From 523c2ceb051e4d489ff9510d04fbd105d253feab Mon Sep 17 00:00:00 2001 From: Quentin Jouet Date: Wed, 12 Jan 2022 15:33:29 +0100 Subject: [PATCH 12/12] Delete existing media on site visit edit --- backend/gncitizen/core/sites/routes.py | 22 +++++++++++++++++++ .../programs/sites/detail/detail.component.ts | 1 + .../programs/sites/form/form.component.html | 18 +++++++++++++++ .../app/programs/sites/form/form.component.ts | 10 +++++---- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/backend/gncitizen/core/sites/routes.py b/backend/gncitizen/core/sites/routes.py index 810df943..76d7c5e6 100644 --- a/backend/gncitizen/core/sites/routes.py +++ b/backend/gncitizen/core/sites/routes.py @@ -1,5 +1,6 @@ import io import uuid +import json import xlwt from flask import Blueprint, current_app, make_response, request @@ -113,6 +114,8 @@ def get_site_photos(site_id): "url": "/media/{}".format(p.MediaModel.filename), "date": p.VisitModel.as_dict()["date"], "author": p.VisitModel.obs_txt, + "visit_id": p.VisitModel.id_visit, + "id_media": p.MediaModel.id_media } for p in photos ] @@ -416,6 +419,25 @@ def update_visit(visit_id): visit = VisitModel.query.filter_by(id_visit=visit_id).first() if current_user.id_user != visit.id_role: return ("unauthorized"), 403 + + try: + # Delete selected existing media + id_media_to_delete = json.loads(update_data.get("delete_media")) + if len(id_media_to_delete): + db.session.query(MediaOnVisitModel).filter( + MediaOnVisitModel.id_media.in_( + tuple(id_media_to_delete) + ), + MediaOnVisitModel.id_data_source + == visit_id, + ).delete(synchronize_session="fetch") + db.session.query(MediaModel).filter( + MediaModel.id_media.in_(tuple(id_media_to_delete)) + ).delete(synchronize_session="fetch") + except Exception as e: + current_app.logger.warning("[update_visit] delete media ", e) + raise GeonatureApiError(e) + visit.date = update_data.get("date") visit.json_data = update_data.get("data") db.session.commit() diff --git a/frontend/src/app/programs/sites/detail/detail.component.ts b/frontend/src/app/programs/sites/detail/detail.component.ts index da51bb3c..894f2df2 100644 --- a/frontend/src/app/programs/sites/detail/detail.component.ts +++ b/frontend/src/app/programs/sites/detail/detail.component.ts @@ -134,6 +134,7 @@ export class SiteDetailComponent } editSiteVisit(visit_data) { + visit_data.photos = this.photos.filter((p) => p.visit_id === visit_data.id) this.flowService.editSiteVisit(this.site_id, visit_data.id, visit_data); } diff --git a/frontend/src/app/programs/sites/form/form.component.html b/frontend/src/app/programs/sites/form/form.component.html index 4ee70999..58dacab4 100644 --- a/frontend/src/app/programs/sites/form/form.component.html +++ b/frontend/src/app/programs/sites/form/form.component.html @@ -46,6 +46,24 @@

+
+
+ + + + +
p.checked) + .map((p) => p.id_media); + formData['delete_media'] = JSON.stringify(id_media_to_delete); method = 'patch'; url = `${this.URL}/sites/visits/${this.visit_id}` }