Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Test error handling #43

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
12 changes: 8 additions & 4 deletions backend/dev_config.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
sources:
geonetwork_instances:
# une seule instance geonetwork source supportée dans la v1
# ce nom sera utilisé dans une V2 (non financée) pour que l'utilisateur final choisisse l'instance GN source
# ce nom sera utilisé dans une V2 (non financée) pour que l'utilisateur final choisisse l'instance GN source
- name: "GeonetworkDemo"
# api_url: https://mastergn.rennesmetropole.fr/geonetwork/srv/api
api_url: "https://demo.georchestra.org/geonetwork/srv/api"
Expand All @@ -11,12 +11,11 @@ sources:
api_url: "https://public.sig.rennesmetropole.fr/geonetwork/srv/api"
login: "${XXX}"
password: "${XXX}"
geoserver_instances:
geoserver_instances:
- url: "https://demo.georchestra.org/geoserver"
login: "${DEMO_LOGIN}"
password: "${DEMO_LOGIN}"



destinations:
# nom de l'instance geOrchestra destination, telle qu'il apparaitra dans la UI
"CompoLocale":
Expand All @@ -33,6 +32,11 @@ destinations:
api_url: "http://proxy:8080/geonetwork/srv/api"
geoserver:
url: "http://proxy:8080/geoserver"
"CompoLocaleNoAuth":
geonetwork:
api_url: "http://proxy:8080/geonetwork/srv/api"
geoserver:
url: "http://proxy:8080/geoserver"

db_logging:
host: "database"
Expand Down
81 changes: 81 additions & 0 deletions backend/maelstro/core/clone.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,87 @@ def gn_dst(self) -> GnApi:
def gs_dst(self) -> RestService:
return self.geo_hnd.get_gs_service(self.dst_name, is_source=False)

def involved_resources(
self,
copy_meta: bool,
copy_layers: bool,
copy_styles: bool,
) -> dict[str, list[dict[str, Any]]]:
self.copy_meta = copy_meta
self.copy_layers = copy_layers
self.copy_styles = copy_styles

with get_georchestra_handler() as geo_hnd:
self.geo_hnd = geo_hnd

zipdata = self.gn_src.get_record_zip(self.uuid).read()
self.meta = Meta(zipdata)

resources: dict[str, list[dict[str, Any]]] = {
"metadata": [],
"data": [],
}

src_gn_info = self.geo_hnd.get_service_info(
self.src_name, is_source=True, is_geonetwork=True
)
src_gn_url = src_gn_info["url"]
dst_gn_info = self.geo_hnd.get_service_info(
self.dst_name, is_source=False, is_geonetwork=True
)
dst_gn_url = dst_gn_info["url"]

resources["metadata"].append(
{
"src": src_gn_url,
"dst": dst_gn_url,
"metadata": (
[
{
"title": self.meta.get_title(),
}
]
if self.copy_meta
else []
),
}
)

dst_gs_info = self.geo_hnd.get_service_info(
self.dst_name, is_source=False, is_geonetwork=False
)
dst_gs_url = dst_gs_info["url"]

geoservers = self.meta.get_gs_layers(config.get_gs_sources())
for server_url, layer_names in geoservers.items():
styles: set[str] = set()
for layer_name in layer_names:

gs_src = self.geo_hnd.get_gs_service(server_url, True)
layers = {}
for layer_name in layer_names:
resp = gs_src.rest_client.get(f"/rest/layers/{layer_name}.json")
raise_for_status(resp)
layers[layer_name] = resp.json()

for layer in layers.values():
styles.update(self.get_styles_from_layer(layer).keys())

resources["data"].append(
{
"src": server_url,
"dst": dst_gs_url,
"layers": (
[str(layer_name) for layer_name in layer_names]
if self.copy_layers
else []
),
"styles": list(styles) if self.copy_styles else [],
}
)

return resources

def clone_dataset(
self,
copy_meta: bool,
Expand Down
11 changes: 9 additions & 2 deletions backend/maelstro/core/operations.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from typing import Any
import logging
from logging import Handler

from fastapi.responses import JSONResponse, PlainTextResponse
from requests import Response
from requests.exceptions import RequestException
from fastapi import FastAPI, HTTPException, Request
Expand All @@ -14,13 +16,18 @@
def setup_exception_handlers(app: FastAPI) -> None:
@app.exception_handler(HTTPException)
async def handle_fastapi_exception(request: Request, err: HTTPException) -> Any:
if "/copy/" in str(request.url):
# Seems that this could be done in copy route
if "/copy" in str(request.url):
log_request_to_db(
err.status_code,
500,
request,
log_handler.properties,
log_handler.get_json_responses(),
)
return JSONResponse(
log_handler.get_json_responses(),
status_code=500,
)
return await http_exception_handler(request, err)

@app.exception_handler(GnException)
Expand Down
10 changes: 6 additions & 4 deletions backend/maelstro/logging/psql_logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ def log_request_to_db(
request: Request,
properties: dict[str, Any],
operations: list[dict[str, Any]],
) -> None:
) -> Log | None:
record = {
"start_time": properties.get("start_time"),
"end_time": datetime.now(),
Expand All @@ -97,15 +97,17 @@ def log_request_to_db(
"copy_styles": to_bool(request.query_params.get("copy_styles")),
"details": operations,
}
log_to_db(record)
return log_to_db(record)


def log_to_db(record: dict[str, Any]) -> None:
def log_to_db(record: dict[str, Any]) -> Log | None:
if not LOGGING_ACTIVE:
return
with Session(get_engine()) as session:
session.add(Log(**record))
log = Log(**record)
session.add(log)
session.commit()
return log


def get_raw_logs(
Expand Down
13 changes: 13 additions & 0 deletions backend/maelstro/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,19 @@ def get_layers(src_name: str, uuid: str) -> list[dict[str, str]]:
return meta.get_ogc_geoserver_layers()


@app.get("/involved_resources")
def get_involved_resources(
src_name: str,
dst_name: str,
metadataUuid: str,
copy_meta: bool = True,
copy_layers: bool = True,
copy_styles: bool = True,
) -> dict[str, Any]:
clone_ds = CloneDataset(src_name, dst_name, metadataUuid)
return clone_ds.involved_resources(copy_meta, copy_layers, copy_styles)


@app.put(
"/copy",
responses={
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ services:
- georchestra_datadir:/etc/georchestra
environment:
MAELSTRO_CONFIG: /app/dev_config.yaml
DEMO_LOGIN: testadmin
DEMO_PASSWORD: testadmin
LOCAL_LOGIN: testadmin
LOCAL_PASSWORD: testadmin
healthcheck:
Expand Down
17 changes: 17 additions & 0 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"vue-router": "^4.5.0"
},
"devDependencies": {
"@pinia/testing": "^0.1.7",
"@tsconfig/node22": "^22.0.0",
"@types/jsdom": "^21.1.7",
"@types/node": "^22.13.1",
Expand Down
49 changes: 49 additions & 0 deletions frontend/src/components/LogsReport.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<script setup lang="ts">
import type { LogDetail } from '@/services/logs.service'

defineProps<{ logs: LogDetail[] }>()

const logClass = (status_code: number) => {
if (status_code >= 200 && status_code < 300) {
return 'log-success'
} else {
return 'log-error'
}
}
</script>

<template>
<div class="mb-4 font-semibold">Details</div>
<div v-for="(log, index) in logs" :key="index">
<div v-if="['POST', 'PUT'].includes(log.method!)" :class="logClass(log.status_code!)">
{{ log.method }} {{ log.url }}
</div>
<div v-if="log.operation" class="log-info">{{ log.operation }}</div>
</div>
</template>

<style scoped>
.log-info {
background-color: #e0f7fa; /* Bleu clair */
border-left: 5px solid #039be5;
padding: 10px;
margin-bottom: 5px;
border-radius: 5px;
}

.log-success {
background-color: #e8f5e9; /* Vert pâle */
border-left: 5px solid #43a047; /* Bordure vert plus foncé */
padding: 10px;
margin-bottom: 5px;
border-radius: 5px;
}

.log-error {
background-color: #ffebee; /* Rouge clair */
border-left: 5px solid #d32f2f;
padding: 10px;
margin-bottom: 5px;
border-radius: 5px;
}
</style>
20 changes: 20 additions & 0 deletions frontend/src/components/__tests__/LogsReport.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import LogsReport from '../LogsReport.vue'

describe('LogsReport', () => {
it('renders properly', () => {
const wrapper = mount(LogsReport, {
props: {
logs: [
{
method: 'PUT',
status_code: 200,
url: 'http://proxy:8080/geoserver/rest/styles/point.sld',
},
],
},
})
expect(wrapper.text()).toContain('http://proxy:8080/geoserver/rest/styles/point.sld')
})
})
15 changes: 12 additions & 3 deletions frontend/src/layouts/BaseLayout.vue
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n'
import { RouterView } from 'vue-router'
import { RouterView, useRouter } from 'vue-router'
import Menubar from 'primevue/menubar'
import { Select } from 'primevue'

const { t } = useI18n()
const router = useRouter()

const menuItems = [
{ label: t('Synchronize'), icon: 'pi pi-sync', command: () => {} },
{ label: t('Operations log'), icon: 'pi pi-list', command: () => {} },
{
label: t('Synchronize'),
icon: 'pi pi-sync',
url: router.resolve({ name: 'synchronize' }).href,
},
{
label: t('Operations log'),
icon: 'pi pi-list',
url: router.resolve({ name: 'logs' }).href,
},
{ label: t('User guide'), icon: 'pi pi-book', command: () => {} },
]
</script>
Expand Down
12 changes: 11 additions & 1 deletion frontend/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,15 @@
"User guide": "Guide d'utilisation",
"Metadata": "Métadonnées",
"Layers": "Couches",
"Styles": "Styles"
"Styles": "Styles",
"Dataset is mandatory": "Le champ métadonnées est obligatoire",
"Destination is mandatory": "Le champ destination est obligatoire",
"Source platform": "Plateforme source",
"Source:": "Source :",
"Destination:": "Destination :",
"Metadata:": "Métadonnées :",
"Layers:": "Couches :",
"Styles:": "Styles :",
"Go back to form": "Retourner au formulaire",
"Confirm": "Confirmer"
}
12 changes: 2 additions & 10 deletions frontend/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import Aura from '@primevue/themes/aura'
import { createPinia } from 'pinia'
import PrimeVue, { usePrimeVue, type PrimeVueLocaleOptions } from 'primevue/config'
import { createApp, watch } from 'vue'
import PrimeVue, { type PrimeVueLocaleOptions } from 'primevue/config'
import { createApp } from 'vue'
import App from './App.vue'
import './assets/main.css'
import router from './router'
Expand Down Expand Up @@ -51,12 +51,4 @@ app.use(PrimeVue, {
locale: primeLocales[i18n.global.locale],
})

watch(
() => i18n.global.locale, // Observe la langue actuelle de Vue I18n
(newLocale) => {
const primevue = usePrimeVue()
primevue.config.locale = primeLocales[newLocale] || primeLocales.en
},
)

app.mount('#app')
Loading
Loading