Full-Stack Developer
🎯 Context and goals
- Evolve production-grade naturalist applications across critical flows: import, synthesis, monitoring, taxonomy, and user management.
- Stabilize backend/frontend interfaces through stronger API contracts, filtering/pagination, dynamic forms, and more consistent error handling.
- Reduce manual business-side corrections by improving data model consistency, admin feedback, and UI flows.
🛠️ Deliverables
Projects
- GeoNature-citizen — #project-citizen
- GeoNature — #project-geonature
- GeoNature-Docker-services — #project-geonature-docker-services
- gn_module_export — #project-gn-module-export
- gn_module_monitoring — #project-gn-module-monitoring
- TaxHub — #project-taxhub
- UsersHub — #project-usershub
🧩 Design
- #project-citizen: Merged PRs: #430 reworked TaxHub retrieval to support large lists, reshape taxa/media payloads, and adapt the observation form to autocomplete and automatic preselection; #438 added site count to the home statistics endpoint and propagated the related translations; #436 completed the i18n layer, especially German translations, across authentication, observation, site, and configuration screens; #435 fixed backoffice redirection through Apache configuration. Open PRs: #466 adds an i18n proxy for development mode in webpack/server; #465 introduces a language switcher and frontend/server persistence service; #393 refactors observation and site details to safely render descriptions and nested JSON structures.
- #project-geonature: Merged PRs: #3583 added UUID support to the acquisition framework through backend routes, config schema, and form components; #3343 added a confirmation modal for import edition and adjusted import V3 workflows and tests; #3303 extended monitoring media / marking events with models, routes, migrations, and tests; #3277 introduced a context parameter in import field mapping and propagated it from the frontend to backend upload handling with tests; #3212 fixed required radio button validation in dynamic forms; #3196 preserved drawn geometry during Leaflet edition; #3169 added route-driven navigation to observation detail tabs; #3154 exposed latest discussions on the home page through a backend route, configuration, and Angular component; #3048 added fixture and frontend test coverage for the import list; #2935 fixed statistical labels in the PDF report; #2920 added backend tests for line-number error cases; #2911 removed
taxa_countin favor of computed import report statistics; #2910 added import-data actions directly inside modules; #2899 grouped report fields by entity; #2883 prepared multi-destination import lists; #2875 generalized imports to all destinations; #2779 wiredDRAWSTOPto keep geometry state consistent; #2768 introduced an auto-validation function with migration, task, and config schema; #2701 added a source-module filter to synthesis queries and forms; #2692 made GeoNature compatible with the monitoring module through models, routes, and migrations; #2658 fixed last-geometry-point rendering in OCCTAX; #2637 addedgroup3_inpnto the synthesis export view with migration and tests; #2321 fixed date comparison in the OCCTAX form. Open PRs: #3952 aligns URL prefix handling across config samples and install scripts; #3941 decouples installation into$HOMEand introducesnvmon shared systems; #3663 adds anadditional_datasynthesis filter for monitoring on both backend and Angular form side; #3478 fixes the numeric widget so letters are rejected and validation messages stay consistent; #3432 makes the datalist component explicitly clearable. - #project-geonature-docker-services: Open PRs: #97 implements lightweight observability through Docker
json-filelogs, rotation, configurable log levels, service labels, and overrides that inject an internal CA bundle into runtime and utility containers; #76 industrializes the Compose stack with one-shot services for monitoring, migrations, supergrant, and altitude-raster import, integrates an external GeoNature module into backend images withgdal, and hardens Makefiles, data layouts, permissions, and environment samples. - #project-gn-module-export: Merged PRs: #137 merged the Flask export view with consistent token-based permission handling; #162 filtered GeoNature application users in admin/backend flows; #164 exposed the API token all the way to the frontend export list.
- #project-gn-module-monitoring: Merged PRs: #238 rebased the business branch on the current base and realigned generic configuration, services, routes, and documentation; #310 fixed sorting, filtering, and pagination in the Angular datatable; #328 removed duplicated
formValuehandling across model, component, and service layers; #329 normalized frontend formatting through Prettier and technical documentation; #330 added a GitHub Actions frontend lint workflow; #407 added backend fixtures and tests for individuals, markings, and related schemas; #433 reduced an oversized site query by rewriting the route and covering it with tests; #531 fixed parent object retrieval; #540 removed a hardcoded parent path inseeDetails. Open PRs: #472 introduces dynamic synthesis filters driven by TOML configuration; #429 secures site deletion when children exist in both backend and form behavior; #252 adds a logging migration for complement tables. - #project-taxhub: Merged PR: #684 optimized bulk
cd_nomimport through batched validation, idempotent inserts, and detailed admin feedback. - #project-usershub: no merged PR was identified. Open PR: #241 harmonizes backend error handling through a reusable utility layer across several routes.
💻 Development
- Backend :
- #project-citizen: #430 refactored TaxHub taxon adaptation so the form could consume a normalized payload with
taxref, media, and attributes. Source: PR #430. - #project-geonature: #3277 added incremental import updates through a
fieldmappingpayload sent in the form, making it possible to update an existing import without re-uploading the file every time; #2768, #2701, #2692, and #2637 extended validation, synthesis filters, monitoring compatibility, and export views. Source: PR #3277.input_file = request.files.get("file", None) input_fieldmapping_preset = request.form.get("fieldmapping", None) # file must be set only when new import. otherwise, it's optional. it can also be a request to update the fieldmapping preset if imprt is None and input_file is None: raise BadRequest("File parameter is missig") if imprt is None: imprt = TImports(destination=destination) author = g.current_user imprt.authors.append(author) db.session.add(imprt) if input_fieldmapping_preset: fieldmapping_preset = json.loads(input_fieldmapping_preset) if imprt.fieldmapping is None: imprt.fieldmapping = {} imprt.fieldmapping.update(fieldmapping_preset) if input_file: size = get_file_size(input_file) max_file_size = current_app.config["IMPORT"]["MAX_FILE_SIZE"] * 1024 * 1024 if size > max_file_size: raise BadRequest(description=f"File too big ({size} > {max_file_size}).") if size == 0: raise BadRequest(description="Impossible to upload empty files") - #project-gn-module-export: #137 merged authorized-role management directly into the Flask-Admin export view by adding
allowed_rolesto the admin form; #162 then restricted that selection to users and groups actually attached to the GeoNature application. Source: PR #162.def filter_user_app_and_role(): user_and_gp_from_gn_app = ( User.query.outerjoin(CorRole, User.id_role == CorRole.id_role_utilisateur) .outerjoin( UserApplicationRight, or_( UserApplicationRight.id_role == CorRole.id_role_groupe, UserApplicationRight.id_role == User.id_role, ), ) .join( Application, Application.id_application == UserApplicationRight.id_application, ) .filter(Application.code_application == "GN") ) return user_and_gp_from_gn_app.order_by( User.groupe.desc(), User.nom_role ).filter((User.groupe == True) | (User.identifiant.isnot(None))) - #project-gn-module-monitoring: #433 reduced an oversized site query; #407 established backend test coverage for individuals and markings; #252 opens a path toward business logging through a dedicated migration. No extra backend snippet is kept here so the snippet budget stays focused on the most structuring merged PRs.
- #project-taxhub: #684 optimized bulk
cd_nomimport through batched validation,ON CONFLICT DO NOTHING, and admin-facing feedback counters. Source: PR #684.def _populate_bib_liste_batch(id_list, input_cd_noms): valid_cd_noms = set() unique_input_cd_noms = sorted(set(input_cd_noms)) for batch in _chunked(unique_input_cd_noms, 5000): query = select(Taxref.cd_nom).where(Taxref.cd_nom.in_(batch)) valid_cd_noms.update(db.session.execute(query).scalars().all()) inserted_count = 0 sorted_valid_cd_noms = sorted(valid_cd_noms) for batch in _chunked(sorted_valid_cd_noms, 5000): values = [{"id_liste": id_list, "cd_nom": cd_nom} for cd_nom in batch] insert_stmt = ( pg_insert(cor_nom_liste) .values(values) .on_conflict_do_nothing( index_elements=[cor_nom_liste.c.id_liste, cor_nom_liste.c.cd_nom] ) .returning(cor_nom_liste.c.cd_nom) ) inserted_count += len(db.session.execute(insert_stmt).scalars().all()) - #project-usershub: #241 centralizes error handling in
app/utils/errors.pyand reuses it across several backend routes; no snippet is retained here so code examples remain limited to merged PRs.
- #project-citizen: #430 refactored TaxHub taxon adaptation so the form could consume a normalized payload with
- Frontend :
- #project-citizen: #430 adapted the observation form so it can load the program taxonomy, distinguish between short and long lists, and auto-select the only available taxon. Source: PR #430.
this.surveySpecies$ = this.programService .getProgramTaxonomyList(this.taxonomyListID) .pipe( tap((species) => { this.taxa = species; }), switchMap((species) => this.programService.getAllProgramTaxonomyList().pipe( map((listsTaxonomy) => { this.taxaCount = listsTaxonomy .filter((lt) => lt.id_liste === this.taxonomyListID) .map((lt) => lt.nb_taxons)[0]; if (this.taxaCount >= this.taxonAutocompleteInputThreshold) { this.inputAutoCompleteSetup(); } else if (this.taxaCount === 1) { this.onTaxonSelected(species[0]); } return species; }) ) ), share() ); - #project-geonature: #3154 added an Angular component to fetch, sort, and paginate the latest discussions; #3169, #3212, #3196, #3478, and #3432 complement this frontend scope around modal routing, form validation, Leaflet geometry handling, and reusable input components. Source: PR #3154.
getDiscussions() { const params = this.buildQueryParams(); this.syntheseApi .getReports(params.toString()) .pipe(takeUntil(this.destroy$)) .subscribe((response) => { this.setDiscussions(response); }); } buildQueryParams(): URLSearchParams { const params = new URLSearchParams(); params.set('type', 'discussion'); params.set('sort', this.sort); params.set('orderby', this.orderby); params.set('page', this.currentPage.toString()); params.set('per_page', this.perPage.toString()); params.set('my_reports', this.myReportsOnly.toString()); return params; } - #project-gn-module-export: #164 wired the Angular UI to fetch exports, keep only the first authorized token per export, and drive token display / copy state inside the component. Source: PR #164.
this._exportService .getExports() .pipe( map((exports: Export[]) => { exports.forEach((element) => { element.cor_roles_exports.splice(1); }); return exports; }) ) .subscribe((exports: Export[]) => { exports.forEach((element) => { element.cor_roles_exports.length > 0 ? this.objectToken.push({ token: element.cor_roles_exports[0].token, display: false }) : this.objectToken.push({ token: null, display: false }); }); this.exports = exports; }); - #project-gn-module-monitoring: #328 recentred form initialization on a single dynamically enriched schema, with explicit field ordering and shared
metapreparation; #310 fixed datatable behavior; #540 and #531 improved parent/child navigation consistency. Source: PR #328.this._configService .init(this.obj.moduleCode) .pipe( mergeMap(() => iif( () => this.obj.objectType == 'site' && this.obj.id != undefined, this._siteService.getTypesSiteByIdSite(this.obj.id), of(null) ) ) ) .subscribe((typesSites) => { this.queryParams = this._route.snapshot.queryParams || {}; this.bChainInput = this._configService.frontendParams()['bChainInput']; 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; });
- #project-citizen: #430 adapted the observation form so it can load the program taxonomy, distinguish between short and long lists, and auto-select the only available taxon. Source: PR #430.
🏗️ Infrastructure and deployment
- #project-geonature-docker-services: #76 adds one-shot Compose services to install monitoring protocols, run migrations, grant permissions, and load altitude rasters, while also integrating an external module and
gdalinto backend images and stabilizingdata/geonaturelayouts, permissions, and.envhandling; #97 structures observability around the Dockerjson-filedriver, log rotation, GeoNature service labels, configurable backend/worker log levels, and dedicated overrides for internal certificates. - #project-geonature: #3941 and #3952 focus on install scripts,
nvm, install paths, and URL prefix alignment. - #project-citizen: #466 adds a translation proxy for local development; #465 adds a reusable language switcher in the topbar/server stack.
- #project-gn-module-monitoring: #329 standardizes frontend formatting; #330 adds a GitHub Actions workflow that checks Black on the backend and Prettier on the frontend. Source: PR #330.
name: Lint
on: [push, pull_request]
jobs:
backend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Backend code formatting check (Black)
uses: psf/black@stable
with:
src: "setup.py ./backend/gn_module_monitoring"
frontend:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- name: Frontend code formatting check (Prettier)
run: npm install prettier@~3.1.0 && npm run format:check
🧭 Organization / methodology
- Work is structured through thematic PRs, with deliberately small-scope fixes for targeted issues (#3212, #3196, #531, #540) and broader PRs for architecture or refactoring efforts (#3277, #238, #76).
- Quality coverage is visible in PRs centered on tests and validation: #3048, #2920, #407, #684.
- Cross-repository convergence appears on shared topics: import and field mapping between GeoNature and modules, authentication consistency between export and UsersHub, and local install / observability conventions across Docker and installer scripts.
- Delivery also includes onsite training on the GeoNature stack from January 2026 to June 2026, representing roughly fifteen in-person days, with the goal of raising the client's web-development skills until they can contribute autonomously to GeoNature applications.
📈 Results
- I contributed to a multi-repository scope representing 54 verified PRs, including 40 merged and 14 open, across topics ranging from naturalist data flows to Angular components, SQL migrations, and installation tooling. The overall output results in a more coherent software base for imports, taxonomy, monitoring, administration, and local operations, with stronger PR-level traceability and an expected reduction in manual corrections along business workflows. Since January 2026, I have also been delivering onsite training on the GeoNature stack, planned through June 2026 over roughly fifteen days, to make the client autonomous in application contributions. Main benefit: progressive hardening of input flows, API contracts, and delivery environments, extended by direct skill transfer to the client team.
GeoNature-citizen
- PR count: 7
- Commit count: 34
- Issue count: 3
- Period: November 20, 2024 to June 17, 2025
GeoNature
- PR count: 28
- Commit count: 178
- Issue count: 26
- Period: February 3, 2023 to February 20, 2026
GeoNature-Docker-services
- PR count: 2
- Commit count: 31 commits carried by open PRs #76 and #97
- Issue count: 1
- Period: September 1, 2025 to March 10, 2026
gn_module_export
- PR count: 3
- Commit count: 6
- Issue count: 2
- Period: May 10, 2023 to May 16, 2023
gn_module_monitoring
- PR count: 12
- Commit count: 329
- Issue count: 6
- Period: December 12, 2022 to December 23, 2025
TaxHub
- PR count: 1
- Commit count: 1
- Issue count: 10
- Period: November 21, 2024 to March 9, 2026
UsersHub
- PR count: 1
- Commit count: 2 commits carried by open PR #241
- Issue count: 0
- Period: April 23, 2025 to July 17, 2025
🔧 Technical environment
- Backend observed through the selected PRs: Python, Flask, SQLAlchemy, Alembic, PostgreSQL, REST routes, and
pyteston #430, #3277, #137, #433, #684, and #241. - Frontend observed: Angular, TypeScript, RxJS, dynamic form components, datatables, Leaflet, Cypress, and i18n on #430, #3154, #3169, #164, #328, #310, #3478, and #3432.
- Quality, CI, and operations observed: GitHub Actions, Prettier, shell install scripts, Docker Compose, Makefiles, and local observability on #3048, #2920, #330, #3941, #3952, #76, and #97.
Tech Stack
Backend
Alembic
Celery
Flask
Gunicorn
Marshmallow
Python
Frontend
Angular
Angular Material
Bootstrap
Leaflet
RxJS
TypeScript
Qualite / Tests
Cypress
ESLint
Prettier
Pytest
DevOps
Docker
docker-compose
GitHub Actions
GitLab CI/CD
Bases de donnees (SGBD & SQL)
PostGIS
PostgreSQL
Design Patterns & Architecture
SQLAlchemy