Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ catalog:
# All 4 Trees - Inventaire forestier

coordo load kobotoolbox --action add data/all4trees/inventaire_for/20260519_InventaireForestier_QuestionnaireK.xlsx data/all4trees/inventaire_for/20260422_InventaireForestier_DonneesK.xlsx --package catalog/inventaire_for
coordo load file --action add data/all4trees/inventaire_for/20260422_InventaireForestier_DonneesExternes.xlsx --package catalog/inventaire_for
coordo load file --action add data/all4trees/inventaire_for/20260703_InventaireForestier_DonneesExternes.xlsx --package catalog/inventaire_for

coordo add-foreignkey adu.decay for_dw.decay --package catalog/inventaire_for
coordo add-foreignkey inv_for.proj for_samp.proj --package catalog/inventaire_for
Expand Down
3 changes: 2 additions & 1 deletion backend/all4trees/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
path("admin/", admin.site.urls),
path("api/", include("users.urls")),
path("api/maps/", include("maps.urls")),
path("api/catalog/<layer_id>/<resource_name>", views.catalog_view, name="catalog-data"),
path("api/catalog/<layer_id>/<resource_name>", views.resource_view, name="get-catalog-resource"),
path("api/catalog/<layer_id>", views.resource_list_view, name="get-catalog-resources-list"),
path("api-auth/", include("rest_framework.urls", namespace="rest_framework")),
]
94 changes: 81 additions & 13 deletions backend/all4trees/views.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,91 @@
from django.conf import settings
import json
import pandas as pd
from pathlib import Path

from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.decorators import api_view
from rest_framework.exceptions import NotFound, ValidationError
from rest_framework.response import Response

import pandas as pd
import json

catalog_path = settings.BASE_DIR / "catalog"


@api_view(["GET"])
@permission_classes([IsAuthenticated])
def catalog_view(request, layer_id, resource_name):
def resource_view(request, layer_id, resource_name):
resource_path = get_resource_path(catalog_path, layer_id, resource_name)
df = pd.read_parquet(resource_path)
return Response(json.loads(df.to_json(orient="records")))


@api_view(["POST"])
def resource_list_view(request, layer_id):
resource_list = parse_body(request.body)
package_path = get_package_path(catalog_path, layer_id)

resources_payload, results, errors = load_resources(layer_id, package_path, resource_list)
if not resources_payload:
raise NotFound({"error": "None of the resources could be loaded", "results": results})
elif errors:
return Response({"results": results}, status=status.HTTP_207_MULTI_STATUS)

return Response(resources_payload)

def parse_body(body):
try:
payload = json.loads(body.decode("utf-8"))
except (UnicodeDecodeError, json.JSONDecodeError):
raise ValidationError("Request body must be valid JSON")

resource_list = payload.get("resources")
if not isinstance(resource_list, list) or len(resource_list) < 1:
raise ValidationError("Body should contain a non-empty list of resource names")

return resource_list


def load_resources(layer_id, package_path, resource_list):
resources_payload = {}
results = []
errors = False
for resource_name in resource_list:
resource_path = package_path / f"{resource_name}.parquet"
if not Path(resource_path).exists():
errors=True
results.append(not_found_result(layer_id, resource_name))
continue
resource_df = pd.read_parquet(resource_path)
resources_payload[resource_name] = json.loads(resource_df.to_json(orient="records"))
results.append(ok_result(layer_id, resource_name))

return resources_payload, results, errors

def get_resource_path(catalog_path, layer_id, resource_name):
package_path = get_package_path(catalog_path, layer_id)
resource_path = package_path / f"{resource_name}.parquet"

if not Path(resource_path).exists():
raise NotFound(f"Resource with name '{resource_name}' was not found in the package {layer_id}")

return resource_path

def get_package_path(catalog_path, layer_id):
package_path = catalog_path / f"{layer_id}"

if not Path(package_path).exists():
raise NotFound(f"Package with name '{layer_id}' was not found in the catalog")

return package_path

# authorization: ensure user has the custom add_data permission
if not request.user.has_perm("users.view_data"):
return Response(status=status.HTTP_403_FORBIDDEN)
def not_found_result(layer_id, resource_name):
return {
"uri": f"{layer_id} / {resource_name}",
"status": status.HTTP_404_NOT_FOUND,
"reason": f"Resource with name '{resource_name}' was not found in the package {layer_id}"
}

target_path = catalog_path / f"{layer_id}" / f"{resource_name}.parquet"
df = pd.read_parquet(target_path)
return Response(json.loads(df.to_json(orient="records")))
def ok_result(layer_id, resource_name):
return {
"uri": f"{layer_id} / {resource_name}",
"status": status.HTTP_200_OK
}
2 changes: 1 addition & 1 deletion backend/configs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
"cod": "cod",
"ecos": "ecos",
"taille_placette": "20^2*pi()/10000",
"projet": "proj",
"project": "proj",
"tree_pop": "count(1 if adu.cond = 1)",
"biomass_volume": "adu.sum(0.0673 * (for_sp.dens * (meas1 + meas2 + meas3 + meas4 + meas5 + meas6 + meas7 + meas8 + meas9 + meas10)^2 * hgt_perp)^0.976 if cond = 1) / (20^2 * pi() / 10000) / 1000",
"tree_density": "count(1 if adu.cond = 1) / (20^2 * pi() / 10000)",
Expand Down
3 changes: 1 addition & 2 deletions backend/maps/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,5 +103,4 @@ def add_data_view(request):
return Response({
'message': 'File uploaded successfully',
'filename': uploaded_file.name,
}, status=status.HTTP_200_OK)

}, status=status.HTTP_200_OK)
2 changes: 1 addition & 1 deletion webapp/src/app/providers/ApiProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ export function ApiProvider({ children }: ApiProviderProps) {
const { token } = useAuth();
const api = useMemo(() => createApiClient(token), [token]);

return <ApiContext value={api}>{children}</ApiContext>;
return <ApiContext.Provider value={api}>{children}</ApiContext.Provider>;
}
8 changes: 4 additions & 4 deletions webapp/src/app/providers/AuthProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,16 @@ export function AuthProvider({ children }: AuthProviderProps) {
try {
const isValid = await verifyToken(token);
if (isValid) {
setToken(storedToken);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch 🤦

setToken(token);
setIsAuthenticated(true);
} else {
// Token invalide, nettoyer le localStorage
localStorage.removeItem("token");
localStorage.removeItem("user");
localStorage.removeItem("username");
}
} catch {
localStorage.removeItem("token");
localStorage.removeItem("user");
localStorage.removeItem("username");
}

setIsAuthLoading(false);
Expand All @@ -50,7 +50,7 @@ export function AuthProvider({ children }: AuthProviderProps) {

const logout = () => {
localStorage.removeItem("token");
localStorage.removeItem("user");
localStorage.removeItem("username");
setToken(null);
setIsAuthenticated(false);
};
Expand Down
2 changes: 2 additions & 0 deletions webapp/src/features/auth/authClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const fetchToken = async (username: string, password: string) => {
throw new Error(`Erreur de connexion: ${res.status} ${res.statusText}`);
}
const data = await res.json();

return data.access;
};

Expand All @@ -19,6 +20,7 @@ export const verifyToken = async (token: string) => {
headers: { "Content-Type": "application/json" },
method: "POST",
});

if (!res.ok) {
console.error(`Token invalide: ${res.status} ${res.statusText}`);
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { findLabelInExternalData } from "@features/indicators/utils";
import type { ExternalData } from "@features/popup/forest-inventory/types";

import { useTranslation } from "@shared/i18n";
import type { LayerMetadata } from "@shared/lib/coordo";
import { findCategoricalLabel, precise } from "@shared/lib/utils";
import { precise } from "@shared/lib/utils";
import type { ChartConfig } from "@shared/ui/chart";

import type { ChartComponentType } from "../components/chart-component";
import { PieChartCategorical } from "../components/pie-chart-categorical";

type PieChartProps = {
project: string;
data: [string, number][];
metadata: LayerMetadata;
externalData: ExternalData;
};

export const ChartRelativeAbundance: ChartComponentType<PieChartProps> = ({
project,
data,
metadata,
externalData,
}) => {
const { t } = useTranslation("all4trees");
const smallCategoriesSum = Number(
Expand All @@ -37,8 +41,13 @@ export const ChartRelativeAbundance: ChartComponentType<PieChartProps> = ({
...chartConfig,
[element.name]: {
label:
findCategoricalLabel(metadata, "reg_sp", element.name) ||
element.name,
findLabelInExternalData(
externalData,
"for_label",
project,
"ess",
Number(element.name),
) || element.name,
},
other: {
label: t(
Expand Down
11 changes: 6 additions & 5 deletions webapp/src/features/charts/soil/lib/sunburst.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { LayerMetadata } from "@shared/lib/coordo";
import type { ExternalData } from "@features/popup/forest-inventory/types";

import { getChartPalette } from "@shared/lib/palette";

import type { SunburstNode } from "../types";
import { formatTaxonLevelLabel } from "./taxon";

export function buildSunburstNodes(
dataEntries: [string, number][],
metadata: LayerMetadata,
dataType: "tsbf" | "barbA",
metadata: ExternalData,
project: string,
): SunburstNode[] {
const nodes: SunburstNode[] = [];
const seen = new Set<string>();
Expand All @@ -16,7 +17,7 @@ export function buildSunburstNodes(
const parts = key.split("-");
nodes.push({
id: key,
label: formatTaxonLevelLabel(key, metadata, dataType),
label: formatTaxonLevelLabel(key, metadata, project),
parent: parts.slice(0, -1).join("-") || "",
value,
});
Expand All @@ -27,7 +28,7 @@ export function buildSunburstNodes(
if (!seen.has(parentPath)) {
nodes.push({
id: parentPath,
label: formatTaxonLevelLabel(parentPath, metadata, dataType),
label: formatTaxonLevelLabel(parentPath, metadata, project),
parent: parts.slice(0, i).join("-") || "",
value: 0,
});
Expand Down
38 changes: 28 additions & 10 deletions webapp/src/features/charts/soil/lib/taxon.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,48 @@
import type { LayerMetadata } from "@shared/lib/coordo";
import { findCategoricalLabel } from "@shared/lib/utils";
import { findLabelInExternalData } from "@features/indicators/utils";
import type { ExternalData } from "@features/popup/forest-inventory/types";

export function getTaxonLabels(
element: string,
metadata: LayerMetadata,
dataType: "tsbf" | "barbA",
metadata: ExternalData,
project: string,
): [string, string, string] {
const [taxon1, taxon2, taxon3] = element.split("-");
const taxon1Label =
findCategoricalLabel(metadata, `${dataType}_tax1`, taxon1) || taxon1;
findLabelInExternalData(
metadata,
"for_label",
project,
"tax1",
Number(taxon1),
) || taxon1;
const taxon2Label =
findCategoricalLabel(metadata, `${dataType}_tax2`, taxon2) || taxon2;
findLabelInExternalData(
metadata,
"for_label",
project,
"tax2",
Number(taxon2),
) || taxon2;
const taxon3Label =
findCategoricalLabel(metadata, `${dataType}_tax3`, taxon3) || taxon3;
findLabelInExternalData(
metadata,
"for_label",
project,
"tax3",
Number(taxon3),
) || taxon3;
return [taxon1Label, taxon2Label, taxon3Label];
}

export function formatTaxonLevelLabel(
element: string,
metadata: LayerMetadata,
dataType: "tsbf" | "barbA",
metadata: ExternalData,
project: string,
): string {
const [taxon1Label, taxon2Label, taxon3Label] = getTaxonLabels(
element,
metadata,
dataType,
project,
);
const parts = element.split("-");
return parts.length === 1
Expand Down
6 changes: 3 additions & 3 deletions webapp/src/features/charts/soil/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { LayerMetadata } from "@shared/lib/coordo";
import type { ExternalData } from "@features/popup/forest-inventory/types";

export type PieChartProps = {
data: Record<string, number>;
metadata: LayerMetadata;
dataType: "tsbf" | "barbA";
metadata: ExternalData;
project: string;
};

export type SunburstNode = {
Expand Down
4 changes: 2 additions & 2 deletions webapp/src/features/charts/soil/ui/chart-taxon-abundance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import type { PieChartProps, SunburstTrace } from "../types";
export const ChartTaxonAbundance: ChartComponentType<PieChartProps> = ({
data,
metadata,
dataType,
project,
}) => {
const { t } = useTranslation(["common", "all4trees"]);

Expand All @@ -30,7 +30,7 @@ export const ChartTaxonAbundance: ChartComponentType<PieChartProps> = ({
const filteredDataEntries = dataEntries.filter(
([key]) => key.trim() !== "0",
);
const nodes = buildSunburstNodes(filteredDataEntries, metadata, dataType);
const nodes = buildSunburstNodes(filteredDataEntries, metadata, project);
const nodeColors = buildNodeColors(nodes);

sunburstData = [
Expand Down
Loading
Loading