Skip to content
Open
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/configs/all4trees_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@
"revenue_change": "avg(0 - inc_dif if inc_evol = '2' else ifnull(inc_dif, 0))",
"assets_idx_raw": "avg( ( ( int(hous_typ) + int(hous_size)) * int(hous_own) + ifnull(list_unique(ass),0) + int(lvst_yn) * ( int(lvst_s) + int(lvst_l) ) + ( int( land_own ) + int( land_titl ) + int( land_size ) if int(land_own) > 0 else 0) ) if ( ( int(hous_typ) + int(hous_size)) * int(hous_own) + list_unique(ass) + int(lvst_yn) * ( int(lvst_s) + int(lvst_l) ) + int( land_own ) + int( land_titl ) + int( land_size ) ) <= int(ass_tot_max) )",
"assets_idx": "10 / int(ass_tot_max) * avg( ( ( int(hous_typ) + int(hous_size)) * int(hous_own) + ifnull(list_unique(ass),0) + int(lvst_yn) * ( int(lvst_s) + int(lvst_l) ) + ( int( land_own ) + int( land_titl ) + int( land_size ) if int(land_own) > 0 else 0) ) if ( ( int(hous_typ) + int(hous_size)) * int(hous_own) + list_unique(ass) + int(lvst_yn) * ( int(lvst_s) + int(lvst_l) ) + int( land_own ) + int( land_titl ) + int( land_size ) ) <= int(ass_tot_max) )",
"nb_income_sources" : "avg(int(inc_no))",
"nb_income_sources": "avg(int(inc_no))",
"nb_additional_incomes": "'N/A'",
"benef_pract1": "count(1 if int(pract1) = 2) / count(1) * 100",
"benef_pract2": "count(1 if int(pract2) = 2) / count(1) * 100",
Expand Down
2 changes: 1 addition & 1 deletion backend/maps/datapackage_manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ def _handle_resources(self, loader_method: LoaderMethod):
try:
params = self.parse_params(loader_method)
except ValueError as e:
return ParseError(e)
raise ParseError(e)

# getting keys to get in self.request.FILES
file_keys = ["data"]
Expand Down
2 changes: 1 addition & 1 deletion backend/maps/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def remove_foreign_key_view(request):
def get_map(user):
user_map = copy(map)
filter = ''
if user.is_authenticated:
if user.is_authenticated and bool(user.project):
project = user.project
if (project.lower() != ADMIN_PROJECT):
filter = f"proj = '{project}' or conf = 1"
Expand Down
19 changes: 19 additions & 0 deletions webapp/src/app/routes/app-router-all4trees.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,38 @@ import { lazy } from "react";

import { MapProviderAll4Trees } from "@app/providers/map-provider-all4trees";

import { LAYERS } from "@shared/api/layers";
import { useTranslation } from "@shared/i18n";

import { AppRouterBase } from "./app-router-base";

const MainPage = lazy(() => import("@pages/all4trees/main"));
const DashboardPage = lazy(() => import("@pages/all4trees/dashboard"));

export const AppRouterAll4Trees = () => {
const { t } = useTranslation("all4trees");

return (
<AppRouterBase
DashboardPage={DashboardPage}
MainPage={MainPage}
MapProvider={MapProviderAll4Trees}
rootLayoutProps={{
hasDashboard: true,
layerOptions: [
{
translation: t("filters.categories.actions.forestInventory"),
value: LAYERS.INVENTORY_FOR,
},
{
translation: t("filters.categories.actions.treeDiversity"),
value: LAYERS.INVENTORY_BIO,
},
{
translation: t("filters.categories.actions.socioEco"),
value: LAYERS.ENQUETE,
},
Comment thread
david-bretaud-dev marked this conversation as resolved.
],
logoSrc: "/logo_all4trees.png",
Comment thread
david-bretaud-dev marked this conversation as resolved.
}}
/>
Expand Down
4 changes: 4 additions & 0 deletions webapp/src/app/styles/all4trees.css
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@
--destructive: #dc2626;
--info: var(--custom-color-blue-light);
--info-foreground: var(--custom-color-blue-dark);
--success: rgb(237, 247, 237);
--success-foreground: #2e7d32;

--border: rgba(0, 0, 0, 0.15);
--input: #ffffff;
Expand Down Expand Up @@ -135,6 +137,8 @@
--destructive: #ef4444;
--info: var(--custom-color-blue-dark);
--info-foreground: var(--custom-color-blue-light);
--success: rgb(12, 19, 13);
--success-foreground: #66bb6a;

--border: rgba(255, 255, 255, 0.15);
--input: rgba(255, 255, 255, 0.05);
Expand Down
2 changes: 2 additions & 0 deletions webapp/src/app/styles/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@
--color-destructive: var(--destructive);
--color-info: var(--info);
--color-info-foreground: var(--info-foreground);
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);

--color-border: var(--border);
--color-input: var(--input);
Expand Down
205 changes: 205 additions & 0 deletions webapp/src/features/add-data/add-data-dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
import { AlertCircle, CheckCircle2, UploadIcon } from "lucide-react";
import type { FC } from "react";

import { useTranslation } from "@i18n";

import { Alert, AlertAction, AlertDescription } from "@ui/alert";
import { Button } from "@ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@ui/dialog";
import { Input } from "@ui/input";
import { Label } from "@ui/label";
import { RadioGroup, RadioGroupItem } from "@ui/radio-group";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@ui/select";

import {
ACCEPTED_EXTENSIONS_BY_KIND,
RESOURCE_KINDS,
type ResourceKind,
} from "./constants";
import { useAddData } from "./use-add-data";

// The value is the catalog folder name
export type LayerOptions = Array<{ value: string; translation: string }>;

type AddDataDialogProps = {
layerOptions: LayerOptions;
onOpenChange: (open: boolean) => void;
open: boolean;
};

export const AddDataDialog: FC<AddDataDialogProps> = ({
layerOptions,
onOpenChange,
open,
}) => {
const { t } = useTranslation("common");
const {
error,
form,
isLoading,
isSuccess,
kind,
reset,
setFile,
setForm,
setKind,
submit,
} = useAddData();

const isFormData = kind === RESOURCE_KINDS.FormData;

const handleOpenChange = (next: boolean) => {
if (!next) {
reset();
}
onOpenChange(next);
};

const handleSubmit = (event: React.FormEvent) => {
event.preventDefault();
submit();
};

return (
<Dialog
onOpenChange={handleOpenChange}
open={open}
>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{t("addData.title")}</DialogTitle>
<DialogDescription>{t("addData.description")}</DialogDescription>
</DialogHeader>

<form
className="flex flex-col gap-6"
onSubmit={handleSubmit}
>
<div className="grid gap-3">
<Label htmlFor="add-data-form">{t("addData.field.form")}</Label>
<Select
onValueChange={(value) => setForm(value)}
value={form}
>
<SelectTrigger id="add-data-form">
<SelectValue placeholder={t("addData.field.formPlaceholder")} />
</SelectTrigger>
<SelectContent>
{layerOptions.map(({ value, translation }) => (
<SelectItem
key={value}
value={value}
>
{translation}
</SelectItem>
))}
</SelectContent>
</Select>
</div>

<div className="grid gap-3">
<Label>{t("addData.field.kind")}</Label>
<RadioGroup
className="flex gap-6"
onValueChange={(value) => setKind(value as ResourceKind)}
value={kind}
>
<div className="flex items-center gap-3">
<RadioGroupItem
id="add-data-kind-form"
value={RESOURCE_KINDS.FormData}
/>
<Label
className="font-normal"
htmlFor="add-data-kind-form"
>
{t("addData.kind.formData")}
</Label>
</div>
<div className="flex items-center gap-3">
<RadioGroupItem
id="add-data-kind-external"
value={RESOURCE_KINDS.ExternalData}
/>
<Label
className="font-normal"
htmlFor="add-data-kind-external"
>
{t("addData.kind.externalData")}
</Label>
</div>
</RadioGroup>
</div>

<div className="grid gap-3">
<Label htmlFor="add-data-file">{t("addData.field.file")}</Label>
<Input
accept={ACCEPTED_EXTENSIONS_BY_KIND[kind].join(",")}
className="hover:cursor-pointer"
id="add-data-file"
onChange={(event) => setFile(event.target.files?.[0] ?? null)}
type="file"
/>
<p className="text-sm text-muted-foreground">
{isFormData
? t("addData.hint.formData")
: t("addData.hint.externalData")}
</p>
</div>

{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription style={{ "--tw-translate-y": 0 }}>
{t(`addData.error.${error}`)}
</AlertDescription>
</Alert>
)}

{isSuccess && (
<Alert variant="success">
<CheckCircle2 className="h-4 w-4" />
<AlertDescription style={{ "--tw-translate-y": 0 }}>
{t("addData.success")}
</AlertDescription>
<AlertAction>
<Button
onClick={() => window.location.reload()}
size="sm"
variant="default"
>
{t("addData.refreshData")}
</Button>
</AlertAction>
</Alert>
)}

<DialogFooter>
<Button
disabled={isLoading}
type="submit"
>
<UploadIcon />
{isLoading
? t("addData.button.pending")
: t("addData.button.submit")}
</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
};
31 changes: 31 additions & 0 deletions webapp/src/features/add-data/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Kind of resource the uploaded file represents.
export const RESOURCE_KINDS = {
ExternalData: "external_data",
FormData: "form_data",
} as const;

export type ResourceKind = (typeof RESOURCE_KINDS)[keyof typeof RESOURCE_KINDS];

// coordo loader the backend selects, per resource kind. cf LoaderType
export const RESOURCE_TYPE_BY_KIND = {
[RESOURCE_KINDS.ExternalData]: "file",
[RESOURCE_KINDS.FormData]: "kobotoolbox",
} as const;

// Accepted upload extensions per kind (coordo's file / kobotoolbox loaders).
// Form data is the multi-sheet KoboToolbox answers workbook (.xlsx).
export const ACCEPTED_EXTENSIONS_BY_KIND = {
[RESOURCE_KINDS.ExternalData]: [
".csv",
".tsv",
".tab",
".xlsx",
".xls",
".xlsm",
".xlsb",
".odf",
".ods",
".odt",
],
[RESOURCE_KINDS.FormData]: [".csv", ".xlsx"],
} as const;
1 change: 1 addition & 0 deletions webapp/src/features/add-data/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { AddDataDialog, type LayerOptions } from "./add-data-dialog";
Loading
Loading