diff --git a/.vscode/settings.json b/.vscode/settings.json index 063aadd0..ba070fe2 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,6 +12,8 @@ "opencatalogi", "organisation", "Organisation", + "organisations", + "Organisations", "pinia", "Toegangs" ], diff --git a/appinfo/routes.php b/appinfo/routes.php index 5593d3a3..3a3e2056 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -21,6 +21,7 @@ ['name' => 'directory#page', 'url' => '/directory', 'verb' => 'GET'], ['name' => 'directory#add', 'url' => '/api/directory/add', 'verb' => 'POST'], ['name' => 'configuration#index', 'url' => '/configuration', 'verb' => 'GET'], - ['name' => 'configuration#create', 'url' => '/configuration', 'verb' => 'POST'] + ['name' => 'configuration#create', 'url' => '/configuration', 'verb' => 'POST'], + ['name' => 'search#preflighted_cors', 'url' => '/api/{path}', 'verb' => 'OPTIONS', 'requirements' => ['path' => '.+']] ], ]; diff --git a/css/main.css b/css/main.css index 3b7d7694..43c4a467 100644 --- a/css/main.css +++ b/css/main.css @@ -121,3 +121,4 @@ .errorMessage { color: var(--color-error); } + diff --git a/docker-compose.yml b/docker-compose.yml index 09fb3ae0..0d866eb2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,11 @@ volumes: config: services: + frontend: + image: acatonl/woo-ui-develop + ports: + - "8081:80" + db: image: mariadb:10.6 restart: always @@ -47,4 +52,4 @@ services: - TZ=Europe/Amsterdam depends_on: init-ubuntu: - condition: service_completed_successfully \ No newline at end of file + condition: service_completed_successfully diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index c0a61925..13d66e28 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -20,8 +20,8 @@ * [Installatie/productie](installatie/README.md) * [Installatie-instructies Nextcloud](installatie/instructies.md) * [Audit en logging](installatie/logging.md) - * [Rollen en Rechten](installatie/rollen-en-rechten.md) - * [SaaS](installatie/saas.md) + * [Rollen en Rechten](installatie/rollen-en-rechten.md.md) + * [SAAS en Dashboarding](installatie/saas.md) * [On-Prem server](installatie/on-prem-server.md) * [Systeemeisen voor Nextcloud](installatie/systeemeisen-voor-nextcloud.md) * [Veel gestelde vragen](veel-gestelde-vragen.md) diff --git a/docs/beheerders/catalogi.md b/docs/beheerders/catalogi.md index 2341a966..44751e22 100644 --- a/docs/beheerders/catalogi.md +++ b/docs/beheerders/catalogi.md @@ -1,6 +1,8 @@ -# Catalogi +# Catalogus -Een catalogus is een verzameling van publicaties. Publicaties behoren dus altijd tot één catalogus en iedere catalogus tot één organisatie. Het is echter wel mogelijk om in één catalogus meerdere metadatatypen te ondersteunen. Binnen de softwarecatalogus worden bijvoorbeeld publiccodes (componenten, code, etc.) als diensten beschikbaar gesteld en binnen de Woo meerdere KOOP/TOOI-typen. +Een catalogus is een verzameling van [publicaties](../gebruikers/publicaties.md). Ze vormen doorgaans een logische versameling rondom een onderwer (bijvoorbeeld WOO) maar kunnen ook worden gebruikt om onderscheid te maken in toegans rechten. + +Publicaties behoren dus altijd tot één catalogus en iedere catalogus tot één organisatie. Het is echter wel mogelijk om in één catalogus meerdere [publicatie typen](../beheerders/metadata.md) te ondersteunen. Binnen de softwarecatalogus worden bijvoorbeeld publiccodes (componenten, code, etc.) als diensten beschikbaar gesteld en binnen de WOO meerdere KOOP/TOOI-typen. > Catalogi zijn onderdeel van de [OpenCatalogi-Standaard](https://github.com/OpenCatalogi/.github/blob/main/docs/Standaard.md) en gebaseerd op het [catalogue object](https://conduction.stoplight.io/docs/open-catalogi/pk8bsjw0539dv-catalogue). diff --git a/docs/beheerders/directory.md b/docs/beheerders/directory.md index 23467fb3..e6134413 100644 --- a/docs/beheerders/directory.md +++ b/docs/beheerders/directory.md @@ -1,11 +1,28 @@ # Directory -De directory vormt het overzicht van alle andere (externe) catalogi die bij jouw installatie bekend zijn. Een Catalogus die bij jouw installatie bekend is noemen een listing (als in is gelist op jouw directory). +De directory vormt het overzicht van alle andere (externe) catalogi die bij jouw installatie bekend zijn. Een Catalogus die bij jouw installatie bekend is noemen een listing (als in is gelist op jouw directory). Dat maakt directories dus een lijst van catalogi die wlders staan. + +![alt text](directory.png) ## Opzetten federatief netwerk -Directories worden tussen installaties onderling uitgewisseld en geupdate. Je hoeft dus nooit handmatig catalogi van andere toe te voegen aan jouw catalogus. +Directories worden tussen installaties onderling uitgewisseld en geupdate. Je hoeft dus nooit handmatig catalogi van andere toe te voegen aan jouw catalogus. Deze worden automatisch ontdekt. Wil je het automatisch ontdeken van catalogi uitzetten? Dan kan dit via instellingen -> Configuratie -> Federatief stelsel + +![alt text](directory_configuratie.png) + +Het is ook mogenlijk om handmatig externe directories (dus de direcotry van een andere installatie) toe te voegen, dit kan via directory inlezen. + +![alt text](directory_inlezen.png) ## Listing Bij een listing kan je de volgende zaken aanpassen. + +* Of de directory beschickbaar is voor zoek opdrachten +* Of de directory standaard moet mee worden genomen in zoek resultaten, dit is met name handig als er spraken is van een samenwerkings verband waarover je woo plichtig bent als overheid + +![alt text](directory_configuratie.png) + +Daarnaast is het mogenlijk om metadata definities van andere catalogi over te nemen en beschickbaar te maken voor je eigen catalogus + +![alt text](directory_metadata.png) diff --git a/docs/beheerders/directory.png b/docs/beheerders/directory.png new file mode 100644 index 00000000..643ee77c Binary files /dev/null and b/docs/beheerders/directory.png differ diff --git a/docs/beheerders/directory_configuratie.png b/docs/beheerders/directory_configuratie.png new file mode 100644 index 00000000..7edda95c Binary files /dev/null and b/docs/beheerders/directory_configuratie.png differ diff --git a/docs/beheerders/directory_inlezen.png b/docs/beheerders/directory_inlezen.png new file mode 100644 index 00000000..247cc938 Binary files /dev/null and b/docs/beheerders/directory_inlezen.png differ diff --git a/docs/beheerders/directory_metadata.png b/docs/beheerders/directory_metadata.png new file mode 100644 index 00000000..6867b6aa Binary files /dev/null and b/docs/beheerders/directory_metadata.png differ diff --git a/docs/beheerders/organisaties.md b/docs/beheerders/organisaties.md index 94d0f6e5..24f5c6f5 100644 --- a/docs/beheerders/organisaties.md +++ b/docs/beheerders/organisaties.md @@ -1 +1,7 @@ # Organisaties + +In organisatie representeerd een volledige organistie of organisatorische eenheid. Hoewel de meeste organisaties één organisaite (namenlijk hun eigen) zullen beheren per open catalogi isntallatie zijn er senario's denkbaar waarin er meerdere organisaties in een installatie zitten. Meest voor de hand liggende zijn + +* SAAS: Een leverancier bied de software aan als SAAS oplossing en keist vier een hosting voor al haar klanten +* Samenwerkings Verband: meerdere orgnaisaties hebben een gemeenschapenlijk ict ondersteuner, en deze odnersteuner kiest voor éém installatie +* Grote organsisaties: De organisatie is dusdanig groot dat het zinnig wordt om hem ook in de installatie op te splitsen in orgniasatorische eenheden diff --git a/docs/beheerders/themas.md b/docs/beheerders/themas.md index 7fd1fd49..dfd65045 100644 --- a/docs/beheerders/themas.md +++ b/docs/beheerders/themas.md @@ -1 +1,7 @@ # Thema's + +Een theme is een overkoepelende verbinding tussen [publicaties](../gebruikers/publicaties.md), in tegenstelling tot [catalogi](../beheerders/catalogi.md) kan een publicatie tot meerdere thema's behoren. Theme's zijn daarmee catalogi overstijgend maar in tegenstelling tot catalogi ook organisatie specifiek. + +Daar waar een catalogus vaak een uistpraak doet over het soort of type van publicaties in de catalogus (doorgaan geordend in wetenlijke kaders zo als de WOO of WHO) doen theme's uispraken over onderwerpen die worden aangeraakt door publicaties. + +Themea's kunnen door de organisatie zelf worden bepaald, en zullen vaak te maken hebben met actualiteit. Bijvoorbeeld de bouw van een nieuwe woonwijk in de gemeente. Gegevens uit verchillende catalogi en publicaite typen (bijvoorbeeld WOO verzoeken, onderzoeks verslagen, raadssstuken en gegevens sets) kunnen dan bij elkaar worden gebracht om voor de bezoekers inzichtenlijk te maken wat een organisatie allemaal heeft gedaan rondom dit thema. diff --git a/docs/dcat_example.json b/docs/dcat_example.json index 67affe06..fe4c4d6b 100644 --- a/docs/dcat_example.json +++ b/docs/dcat_example.json @@ -24,8 +24,8 @@ "type": "application/json", "published": "29-12-2020", "modified": "30 december 2020, 17:09 (UTC+01:00)", - "accessURL": "https://services.arcgis.com/zP1tGdLpGvt2qNJ6/arcgis/rest/services/Voorlopige_Energielabels_BAG/FeatureServer", - "downloadURL": "https://services.arcgis.com/zP1tGdLpGvt2qNJ6/arcgis/rest/services/Voorlopige_Energielabels_BAG/FeatureServer" + "accessUrl": "https://services.arcgis.com/zP1tGdLpGvt2qNJ6/arcgis/rest/services/Voorlopige_Energielabels_BAG/FeatureServer", + "downloadUrl": "https://services.arcgis.com/zP1tGdLpGvt2qNJ6/arcgis/rest/services/Voorlopige_Energielabels_BAG/FeatureServer" }, { "title": "voorlopige energielabels met BAG-kenmerken ", @@ -34,8 +34,8 @@ "type": "application/geopackage+sqlite3", "published": "29-12-2020", "modified": "30 december 2020, 17:10 (UTC+01:00)", - "accessURL": "file:///P:/Geo_Data/SO/SODA/Data/Energielabels/Data/VoorlopigeLabels/GPKG/Voorlopige-labels-december-2019.gpkg", - "downloadURL": "P:\\Geo_Data\\SO\\SODA\\Data\\Energielabels\\Data\\VoorlopigeLabels\\GPKG\\Voorlopige-labels-december-2019.gpkg" + "accessUrl": "file:///P:/Geo_Data/SO/SODA/Data/Energielabels/Data/VoorlopigeLabels/GPKG/Voorlopige-labels-december-2019.gpkg", + "downloadUrl": "P:\\Geo_Data\\SO\\SODA\\Data\\Energielabels\\Data\\VoorlopigeLabels\\GPKG\\Voorlopige-labels-december-2019.gpkg" }, { "title": "voorlopige energielabels met BAG-kenmerken ", @@ -44,8 +44,8 @@ "type": "application/vnd.esri.filegdb", "published": "29-12-2020", "modified": "30 december 2020, 17:11 (UTC+01:00)", - "accessURL": "file:///P:/Geo_Data/SO/SODA/Data/Energielabels/Data/VoorlopigeLabels/GDB/Voorlopige-labels-december-2019.gdb", - "downloadURL": "P:\\Geo_Data\\SO\\SODA\\Data\\Energielabels\\Data\\VoorlopigeLabels\\GDB\\Voorlopige-labels-december-2019.gdb" + "accessUrl": "file:///P:/Geo_Data/SO/SODA/Data/Energielabels/Data/VoorlopigeLabels/GDB/Voorlopige-labels-december-2019.gdb", + "downloadUrl": "P:\\Geo_Data\\SO\\SODA\\Data\\Energielabels\\Data\\VoorlopigeLabels\\GDB\\Voorlopige-labels-december-2019.gdb" }, { "title": "voorlopige energielabels met BAG-kenmerken ", @@ -54,8 +54,8 @@ "type": "application/x-shapefile", "published": "29-12-2020", "modified": "30 december 2020, 17:11 (UTC+01:00)", - "accessURL": "file:///P:/Geo_Data/SO/SODA/Data/Energielabels/Data/VoorlopigeLabels/GDB/Voorlopige-labels-december-2019.gdb", - "downloadURL": "P:\\Geo_Data\\SO\\SODA\\Data\\Energielabels\\Data\\VoorlopigeLabels\\SHP\\*.shp" + "accessUrl": "file:///P:/Geo_Data/SO/SODA/Data/Energielabels/Data/VoorlopigeLabels/GDB/Voorlopige-labels-december-2019.gdb", + "downloadUrl": "P:\\Geo_Data\\SO\\SODA\\Data\\Energielabels\\Data\\VoorlopigeLabels\\SHP\\*.shp" }, { "title": "productbeschrijving: voorlopige energielabels met BAG kenmerken", @@ -64,8 +64,8 @@ "type": "text/html", "published": "7-4-2020", "modified": "30 december 2020, 17:12 (UTC+01:00)", - "accessURL": "https://rio.rotterdam.nl/Project/SODAStadsOntwikkelingData/Pages/ThoZIFnen0KbY6eBjPvh-A", - "downloadURL": "https://rio.rotterdam.nl/Project/SODAStadsOntwikkelingData/Pages/ThoZIFnen0KbY6eBjPvh-A" + "accessUrl": "https://rio.rotterdam.nl/Project/SODAStadsOntwikkelingData/Pages/ThoZIFnen0KbY6eBjPvh-A", + "downloadUrl": "https://rio.rotterdam.nl/Project/SODAStadsOntwikkelingData/Pages/ThoZIFnen0KbY6eBjPvh-A" } ], "attachment_count": 5, diff --git a/docs/developers/aan-de-slag-met-development.md b/docs/developers/aan-de-slag-met-development.md index c7b9e541..841aa293 100644 --- a/docs/developers/aan-de-slag-met-development.md +++ b/docs/developers/aan-de-slag-met-development.md @@ -26,10 +26,63 @@ De Ontwikkelpartijen van [Core](https://documentatie.opencatalogi.nl/Docs/Projec ![alt text](feature_flow.png) +## Known issues + +Iedere applicatie heeft technical debt, hier open en eerlijk over zijn helpt developers die willen bijdragen. Op dit moment hebben wij de volgende aandachtspunten waar we aan werken + +* Test coverage frontend is te laag (is 70% zou 80%) moeten zijn +* Test coverage backend is te laag (is 60% zou 80%) moeten zijn +* Op de frontend bevaten de stores nog busnes logica die naar apparte services moet worden verplaats + ## Application development Omdat de applicatie is ontwikkeld met Nextcloud, is er uitgebreide informatie te vinden in de [Nextcloud-documentatie](https://docs.nextcloud.com/server/latest/developer_manual/index.html) zelf. Dit geldt zowel voor de lay-out van de app als voor de vele componenten die eraan toegevoegd kunnen worden. Tijdens de ontwikkeling van de OpenCatalogi-app is het *documentation-first* principe gehanteerd, waarbij de ontwikkelaars eerst de [Nextcloud-documentatie](https://docs.nextcloud.com/server/latest/developer_manual/index.html) hebben geraadpleegd. +### Gebruikersdocumentatie + +We gebruiken Gitbook voor de gebruikersdocumentatie. Features binnen de app zouden zo veel mogelijk direct moeten doorverwijzen naar deze documentatie. + +Ook voor de documentatie wordt een linter gebruikt namelijk [remarklint](https://github.com/remarkjs/remark-lint). + +De commando's om deze linter in de CLI te gebruiken zijn [hier te vinden](https://github.com/remarkjs/remark-lint?tab=readme-ov-file#what-is-this) voor een uitgebreide output in de terminal. + +### Performance + +Voor een goede gebruikers ervaring voor zowel bezoekers als medewerkers is het belangrijk dat dat de applicatie snel reageerd, we streven daarbij naar een performance van rond de 100 miliseconde (1/10) van een seconde. Maar overall zou de performance odner de 200 miliseconde moeten blijven. Acties die langer dan een seconde duren zijn reden om een purrl request te weigeren. + +## API Development + +De ontwikkeling van de API wordt bijgehouden met de documentatietool [Stoplight.io](https://stoplight.io/), die automatisch een [OpenAPI Specificatie (OAS)](https://www.noraonline.nl/wiki/FS:Openapi-specification#:~:text=Een%20OpenAPI%20Specification%20\(OAS\)%20beschrijft,er%20achter%20de%20API%20schuilgaat.) genereert uit de documentatie. De Stoplight voor OpenCatalogi is [hier](https://conduction.stoplight.io/docs/open-catalogi/6yuj08rgf7w44-open-catalogi-api) te vinden. + +## Frontend Development + +### Storage en Typing + +Om gegevens deelbaar te maken tussen de verschillende Vue-componenten maken we gebruik van [statemanagement](https://vuejs.org/guide/scaling-up/state-management) waarbij we het Action, State, View patroon van Vue zelf volgen. Omdat de applicatie ingewikkeld begint te worden stappen we daarbij over van [simple state management](https://vuejs.org/guide/scaling-up/state-management#simple-state-management-with-reactivity-api) naar [Pinia](https://pinia.vuejs.org/), de door Vue zelf geadviseerde opvolger van [Vuex](https://vuejs.org/guide/scaling-up/state-management#pinia). + +Daarnaast gebruiken we Typescript voor het definiëren van entities. + +### Modals + +* Er mag altijd slechts één modal actief zijn. +* Modals moeten abstract en overal bereikbaar zijn. +* Modals moeten geplaatst worden in de map src/modals. +* Modals moeten getriggerd worden via de state (zodat knoppen die modal openen overal plaatsbaar zijn). +* Modals moeten geïmporteerd worden via `/src/modals/Modals.vue`. + +### Views + +* Views moeten dezelfde bestandsnaam hebben als de geëxporteerde naam en een correlatie hebben met de map waarin het bestand zich bevindt. +* Bijvoorbeeld, als het bestand een detailpagina is en het zich in de map `publications` bevindt, moet het bestand de naam `PublicationDetail.vue` hebben. + +## Documentatie van next cloud + +Het is goed om bij development kennnis te nemen/hebben van de volgende gebruikte Nextcloud onderdelen: + +* [Icons](https://pictogrammers.com/library/mdi/) +* [Layout](https://docs.nextcloud.com/server/latest/developer_manual/design/layout.html)- +* [Componenten](https://nextcloud-vue-components.netlify.app/) + ## Kwaliteit, Stabiliteit en Veiligheid Als onderdeel van de CI/CD-straat voeren we een aantal tests uit, hiermee handhaven we zowel de code kwaliteiteisen van Nextcloud als die van onszelf. Deze testen worden geborgd in een workflow zodat je de resultaten zelf op iedere commit ziet. Let op! het falen van deze tests betekent dat de code niet naar master/main kan worden gemerged en dus niet in productie kan worden genomen. @@ -103,7 +156,7 @@ composer audit ![alt text](composer_audit.png) -Voor beide geldt dat het aantal acceptabele critical vulnerabilities 0 is. +Voor beide geldt dat het aantal acceptabele critical vulnerabilities in *production packadges* 0 is. ### Gebruikersdocumentatie diff --git a/docs/developers/npm_lint.png b/docs/developers/npm_lint.png index 58136835..89df6303 100644 Binary files a/docs/developers/npm_lint.png and b/docs/developers/npm_lint.png differ diff --git a/docs/developers/npm_test.png b/docs/developers/npm_test.png index 956bd8e2..c89e30a3 100644 Binary files a/docs/developers/npm_test.png and b/docs/developers/npm_test.png differ diff --git a/docs/gebruikers/dashboard.md b/docs/gebruikers/dashboard.md index c94f4905..f7594bf7 100644 --- a/docs/gebruikers/dashboard.md +++ b/docs/gebruikers/dashboard.md @@ -18,6 +18,7 @@ Op het dashboard van OpenCatalogi vindt u handige informatie die je meteen verde 1. **Zoekverkeer** Het aantal zoekvragen dat er afgelopen maand aan jouw index (geheel van catalogi) is gesteld. 2. **Metadata** De verdeling over metadata-types van jouw publicaties 3. **Status** De verdeling over statustypes van jouw publicaties +4. **Catalogi** De verdeling over catalogi van jouw publicaties ![app menu](../assets/oc_dashboard.png) diff --git a/docs/gebruikers/image-1.png b/docs/gebruikers/image-1.png new file mode 100644 index 00000000..06c74ffe Binary files /dev/null and b/docs/gebruikers/image-1.png differ diff --git a/docs/gebruikers/image-2.png b/docs/gebruikers/image-2.png new file mode 100644 index 00000000..c3adb042 Binary files /dev/null and b/docs/gebruikers/image-2.png differ diff --git a/docs/gebruikers/image-3.png b/docs/gebruikers/image-3.png new file mode 100644 index 00000000..42d7b8a6 Binary files /dev/null and b/docs/gebruikers/image-3.png differ diff --git a/docs/gebruikers/image.png b/docs/gebruikers/image.png new file mode 100644 index 00000000..d3ee2cf1 Binary files /dev/null and b/docs/gebruikers/image.png differ diff --git a/docs/gebruikers/publicaties.md b/docs/gebruikers/publicaties.md index 87b1a8a1..1c621746 100644 --- a/docs/gebruikers/publicaties.md +++ b/docs/gebruikers/publicaties.md @@ -2,6 +2,14 @@ Publicaties zijn onderdeel van de [Open Catalogi Standaard](https://github.com/OpenCatalogi/.github/blob/main/docs/Standaard.md) en gebaseerd op het [publication object](https://conduction.stoplight.io/docs/open-catalogi/9bebd6bf4fe35-publication). Publicaties kennen eigenschappen zoals gedefinieerd in een publicatietype en kunnen worden gekoppeld aan bijlagen +Een publicatie representeerd iets wat je wilt publiceren, het beschrijft de handeling van publiceren en de spelregels waaronder iets gepubliceerd wordt.het is een soort "verpakking" of "omhulsel" dat zowel de kerngegevens (data) als aanvullende informatie over die gegevens (metadata) bevat. + +Stel je voor dat je een foto hebt. De foto zelf is de data, terwijl de informatie zoals wanneer de foto is genomen, de resolutie van de foto, de camera-instellingen, en de locatie waar de foto is genomen, de metadata vormen. Een publicatie zou in dit geval zowel de foto als al deze aanvullende informatie samen in één pakketje verpakken, zodat je deze als een geheel kunt behandelen en doorzoeken. + +Met andere woorden, een publiatie maakt het mogelijk om zowel de data als de bijbehorende metadata op een gestandaardiseerde manier te bewaren en te verwerken, zonder dat je deze informatie telkens apart hoeft te beheren. Dit is handig omdat je zo alle relevante informatie bij elkaar hebt, wat zorgt voor meer context en daarmee een betere interpretatie van de data. + +Publicaties zijn altijd onderdeel van een collectie in de vorm van een [catalogus](../beheerders/catalogi.md) en behoren tot een [publicatie type](../beheerders/metadata.md) dit laatste zorgt ervoor dat ze voorspelbaar zijn. e.g. fotos hebben altijd een waarde resulutie. + ## Publicaties toevoegen Publicaties kunnen worden toegevoegd via: @@ -34,12 +42,30 @@ Na het opslaan van de publicatie, is deze zichtbaar onder de catalogi "Woo". Om Onder is een voorbeeld van een publicatie en de Actie-mogelijkheden. +
+ +![alt text](image-1.png) + +## Acties + +![alt text](image.png) + +## Bijlagen + +In het merendeel van de gevallen wordt een publicatie opgemaakt om bestanden te delen (bijvoorbeeld vanuit een woo verzoek). Deze bestanden vormen de informatie in de publicaite en worden aan een publicatie gekoppels als bijlagen. Een bijlage kan zowel onderdeel zijn van de publicatie (er in worden geupload) als elders staan (er wordt naar verwezen). + +Naast een bestand kan een bijlage (per verwijzing) bijvoorbeeld ook een website of artikel op een website zijn. + +![alt text](image-3.png) + ## Eigenschappen -@todo +Een tweede manier om informatie op te nemen in een publicaite is via eigenschappen. Eigenschappen zijn voor gedefineerde opties (via [publicatie type](../beheerders/metadata.md)) waar een waarde aan kan worden toegekend. ## Bijlagen +![alt text](image-2.png) + Publicaties hebben vaak bijlagen, zoals een verslag of een besluit. Deze zijn eenvoudig toe te voegen door op de Actie-knop te klikken bij een geselecteerde publicatie, of de drie bolletjes naast een publicatie. Dit opent de Bijlage toevoegen modal.
diff --git a/docs/installatie/saas.md b/docs/installatie/saas.md index 702fb3e0..856e6273 100644 --- a/docs/installatie/saas.md +++ b/docs/installatie/saas.md @@ -1,7 +1,76 @@ -# SaaS +# SAAS en Dashboarding De OpenCatalogi-Nextcloud app is ontworpen om als SaaS-dienst te worden aangeboden aan overheden, ## Multi-tenancy by installion Meerdere tenants in aparte namespaces + +# Dashboarding + +Als je de Open Catalogi app aan de hand van nextcloud aan verschillende gemeenten wil aanbieden is het organiseren van goede tenant overstijgende dashboarding een must. Gelllukig heeft next cloud een goede ondersteuning voor [prometheus](https://grafana.com/grafana/dashboards/11033-nextcloud/), [loki](https://okxo.de/monitor-your-nextcloud-logs-for-suspicious-activities/), [grafana](https://grafana.com/grafana/dashboards/9632-nextcloud/) en [splunk](https://splunkbase) splunk.com/app/3398) + +![alt text](saas_splunk.png) +![alt text](saas_prometheus.png) + +Vamuit dashboardin zijn momenteel de volgende gegevens beschickbaar + +* **Huidige status van de Nextcloud-server** (beschikbaar, in onderhoud, webserver uitgeschakeld, host niet beschikbaar). +* **Statusoverzicht voor tijdsperiode.** +* **Succesvolle en mislukte inlogpogingen en de verhouding daartussen.** +* **Aantal gedefinieerde en actieve gebruikers.** +* **Aantal totale deelacties en aantal bestandsbewerkingen.** + +## Nextcloud Systeeminformatie + +* **Informatie over de huidige configuratie van de Nextcloud-server.** +* **Tijdlijn van Nextcloud-server upgrades.** + +## Gebruikers + +* **Aantal gedefinieerde en actieve gebruikers.** + +## Delen en Opslag + +* **Aantal deelacties, gefedereerde deelacties, bestanden en beschikbare schijfruimte.** +* **Deelacties.** + * **Wie heeft wat gedeeld (of gedeeld ongedaan gemaakt), met wie, hoe (publiek, gebruiker of groep) en wanneer.** + +## Bestands- en Mapactiviteiten + +* **Welke bestands- of mapbewerkingen zijn door wie uitgevoerd, wanneer en van waar.** +* **Mogelijkheid om de bestands- en mapactiviteit te filteren op land en/of gebruiker.** + +## Publieke Bestands-Toegang + +* **Wat is via publieke deelacties geopend, wanneer en van waar.** + +## Hardware Informatie + +* **Gemiddelde CPU-belasting, vrije en gebruikte geheugenruimte, databasegrootte.** + +## Gebruikers/Groepsbewerkingen + +* **Wanneer en door wie zijn gebruikers aangemaakt of verwijderd, uitgeschakeld of ingeschakeld, toegevoegd of verwijderd uit groepen, groepen aangemaakt of verwijderd.** + +## Wachtwoordwijzigingen + +* **Wie heeft of hebben hun wachtwoord gewijzigd, wie heeft het gewijzigd en van waar.** +* **Welke pogingen tot wachtwoordherstel zijn er geweest.** + +## Gebruikers Inlogactiviteit + +* **Inlogactiviteit over tijd en per locatie. Volledig inlogactiviteit-audittraject.** +* **Mogelijkheid om de inlogactiviteit te filteren op land en/of gebruiker.** + +### Mislukte Inlogpogingen + +* **Mislukte inlogpogingen weergegeven vanaf externe en interne IP-adressen, over tijd en per gebruiker.** + +### Bestand-, Map- en Gebruikersaudit + +* **Volledig audittraject voor bestanden en mappen, met antwoorden op vragen zoals "Welke bewerkingen heeft gebruiker X uitgevoerd en wanneer?", "Wie heeft wat gedaan met bestand Y wanneer?" of "Wanneer heeft gebruiker X iets gedaan met bestand Y?"** + +### Virusdetectie + +* **Statistieken en informatie van de app "Anti-virus voor bestanden" voor Nextcloud.** diff --git a/docs/installatie/saas_prometheus.png b/docs/installatie/saas_prometheus.png new file mode 100644 index 00000000..09cff1f7 Binary files /dev/null and b/docs/installatie/saas_prometheus.png differ diff --git a/docs/installatie/saas_splunk.png b/docs/installatie/saas_splunk.png new file mode 100644 index 00000000..cb12c1f2 Binary files /dev/null and b/docs/installatie/saas_splunk.png differ diff --git a/lib/Controller/AttachmentsController.php b/lib/Controller/AttachmentsController.php index 8a2ee33c..be2e3d88 100644 --- a/lib/Controller/AttachmentsController.php +++ b/lib/Controller/AttachmentsController.php @@ -2,6 +2,7 @@ namespace OCA\OpenCatalogi\Controller; +use Exception; use GuzzleHttp\Exception\GuzzleException; use OCA\OpenCatalogi\Db\AttachmentMapper; use OCA\OpenCatalogi\Service\ElasticSearchService; @@ -12,6 +13,7 @@ use OCP\AppFramework\Http\JSONResponse; use OCP\IAppConfig; use OCP\IRequest; +use OCP\IUserSession; use Symfony\Component\Uid\Uuid; class AttachmentsController extends Controller @@ -23,11 +25,11 @@ public function __construct IRequest $request, private readonly IAppConfig $config, private readonly AttachmentMapper $attachmentMapper, - private readonly FileService $fileService + private readonly FileService $fileService, + private readonly IUserSession $userSession, ) { parent::__construct($appName, $request); - $this->fileService->setAppName($appName); } private function insertNestedObjects(array $object, ObjectService $objectService, array $config): array @@ -94,8 +96,8 @@ public function catalog(string|int $id): TemplateResponse */ public function index(ObjectService $objectService): JSONResponse { - if($this->config->hasKey($this->appName, 'mongoStorage') === false - || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' + if ($this->config->hasKey(app: $this->appName, key: 'mongoStorage') === false + || $this->config->getValueString(app: $this->appName, key: 'mongoStorage') !== '1' ) { return new JSONResponse(['results' =>$this->attachmentMapper->findAll()]); } @@ -111,8 +113,6 @@ public function index(ObjectService $objectService): JSONResponse } } - - $filters['_schema'] = 'attachment'; $result = $objectService->findObjects(filters: $filters, config: $dbConfig); @@ -121,14 +121,15 @@ public function index(ObjectService $objectService): JSONResponse return new JSONResponse($results); } + /** * @NoAdminRequired * @NoCSRFRequired */ public function show(string|int $id, ObjectService $objectService): JSONResponse { - if($this->config->hasKey($this->appName, 'mongoStorage') === false - || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' + if ($this->config->hasKey(app: $this->appName, key: 'mongoStorage') === false + || $this->config->getValueString(app: $this->appName, key: 'mongoStorage') !== '1' ) { return new JSONResponse($this->attachmentMapper->find(id: (int) $id)); } @@ -144,23 +145,158 @@ public function show(string|int $id, ObjectService $objectService): JSONResponse } - /** - * @NoAdminRequired - * @NoCSRFRequired - */ - public function create(ObjectService $objectService, ElasticSearchService $elasticSearchService): JSONResponse - { + /** + * Gets info about the uploaded file from the request body, looks specifically for the field '_file'. + * If there is no file or there is an error loading it this will return an error response. + * + * @return JSONResponse|array An error response or an array containing the info about the uploaded file. + */ + private function checkUploadedFile(): JSONResponse|array + { + $uploadedFile = $this->request->getUploadedFile(key: '_file'); + + if (empty($uploadedFile) === true) { + return new JSONResponse(data: ['error' => 'No file uploaded for key "_file"'], statusCode: 400); + } + + // Check for upload errors + if ($uploadedFile['error'] !== UPLOAD_ERR_OK) { + return new JSONResponse(data: ['error' => 'File upload error: '.$uploadedFile['error']], statusCode: 400); + } + return $uploadedFile; + } + + /** + * Gets all params from the request body and then validates if the URL fields are actual valid urls (or null). + * + * @return JSONResponse|array An error response if there are validation errors or an array containing all request body params. + */ + private function checkRequestBody(): JSONResponse|array + { $data = $this->request->getParams(); + $errorMsg = []; + if (empty($data['accessUrl']) === false && filter_var(value: $data['accessUrl'], filter: FILTER_VALIDATE_URL) === false) { + $errorMsg[] = "accessUrl is not a valid url"; + } + + if (empty($data['downloadUrl']) === false && filter_var(value: $data['downloadUrl'], filter: FILTER_VALIDATE_URL) === false) { + $errorMsg[] = "downloadUrl is not a valid url"; + } + + if (empty($errorMsg) === false) { + return new JSONResponse(data: ['validation_errors' => $errorMsg], statusCode: 400); + } + + return $data; + } + + /** + * If it does not already exist creates a folder for the publication the new Attachment belongs to in NextCloud, + * so that the uploaded file(s) for that publication can be saved there. After that saves the uploaded file in that folder. + * If the file is created without error this will return the full path to the file from the root/user folder. + * + * @param array $uploadedFile Information about the uploaded file from the request body. + * + * @return JSONResponse|string An error response if creating the file in NextCloud failed or a string path to the created file. + * @throws Exception In case creating a folder or new file fails. + */ + private function handleFile(array $uploadedFile): JSONResponse|string + { + // Create the Attachments folder and the Publication specific folder. + $this->fileService->createFolder(folderPath: 'Attachments'); + $publicationFolder = '(' . $this->request->getHeader('Publication-Id') . ') ' + . $this->request->getHeader('Publication-Title'); + $this->fileService->createFolder(folderPath: "Attachments/$publicationFolder"); + + // Save the uploaded file + $filePath = "Attachments/$publicationFolder/" . $uploadedFile['name']; // Add a file version to the file name? + $created = $this->fileService->uploadFile( + content: file_get_contents(filename: $uploadedFile['tmp_name']), + filePath: $filePath + ); + + if ($created === false) { + return new JSONResponse(data: ['error' => "Failed to upload file. This file: $filePath might already exist"], statusCode: 400); + } + + return $filePath; + } + + + /** + * Adds information about the uploaded file to the appropriate Attachment fields. And removes fields we do not want to post. + * + * @param array $data The form-data fields and their values (/request body) that we are going to update before posting the Attachment. + * @param array $uploadedFile Information about the uploaded file from the request body. + * @param string $filePath The full file path to where the file is stored in NextCloud. + * + * @return array The updated $data array + * @throws Exception In case creating the share(link) fails. + */ + private function AddFileInfoToData(array $data, array $uploadedFile, string $filePath): array + { + // Update Attachment data + $currentUser = $this->userSession->getUser(); + $userId = $currentUser ? $currentUser->getUID() : 'Guest'; + $data['reference'] = "$userId/$filePath"; + $data['type'] = $uploadedFile['type']; + $data['size'] = $uploadedFile['size']; + $explodedName = explode(separator: '.', string: $uploadedFile['name']); + $data['title'] = $explodedName[0]; + $data['extension'] = end(array: $explodedName); + + // Create ShareLink + $shareLink = $this->fileService->createShareLink(path: $filePath); + if (empty($data['accessUrl']) === true) { + $data['accessUrl'] = $shareLink; + } + $data['downloadUrl'] = "$shareLink/download"; + + // Remove fields we should never post + unset($data['id']); foreach($data as $key => $value) { - if(str_starts_with($key, '_')) { + if(str_starts_with(haystack: $key, needle: '_')) { unset($data[$key]); } } - if($this->config->hasKey($this->appName, 'mongoStorage') === false - || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' + return $data; + } + + + /** + * @NoAdminRequired + * @NoCSRFRequired + * @throws Exception In case creating a new folder, the file upload to NextCloud, or creating the share link fails. + * @throws GuzzleException In case saving the Attachment to MongoDB fails. + */ + public function create(ObjectService $objectService, ElasticSearchService $elasticSearchService): JSONResponse + { + // Check if a file was uploaded + $uploadedFile = $this->checkUploadedFile(); + if ($uploadedFile instanceof JSONResponse) { + return $uploadedFile; + } + + // Get form-data field/request body. + $data = $this->checkRequestBody(); + if ($data instanceof JSONResponse) { + return $data; + } + + // Handle saving the uploaded file in NextCloud + $filePath = $this->handleFile(uploadedFile: $uploadedFile); + if ($filePath instanceof JSONResponse) { + return $filePath; + } + + // Update Attachment data + $data = $this->AddFileInfoToData(data: $data, uploadedFile: $uploadedFile, filePath: $filePath); + + if ($this->config->hasKey(app: $this->appName, key: 'mongoStorage') === false + || $this->config->getValueString(app: $this->appName, key: 'mongoStorage') !== '1' ) { return new JSONResponse($this->attachmentMapper->createFromArray(object: $data)); } @@ -183,22 +319,22 @@ public function create(ObjectService $objectService, ElasticSearchService $elast /** * @NoAdminRequired * @NoCSRFRequired + * @throws GuzzleException In case updating the file in NextCloud fails. */ public function update(string|int $id, ObjectService $objectService, ElasticSearchService $elasticSearchService): JSONResponse { $data = $this->request->getParams(); + // Remove fields we should never post + unset($data['id']); foreach($data as $key => $value) { - if(str_starts_with($key, '_')) { + if(str_starts_with(haystack: $key, needle: '_')) { unset($data[$key]); } } - if (isset($data['id'])) { - unset( $data['id']); - } - if($this->config->hasKey($this->appName, 'mongoStorage') === false - || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' + if ($this->config->hasKey(app: $this->appName, key: 'mongoStorage') === false + || $this->config->getValueString(app: $this->appName, key: 'mongoStorage') !== '1' ) { return new JSONResponse($this->attachmentMapper->updateFromArray(id: (int) $id, object: $data)); } @@ -223,13 +359,31 @@ public function update(string|int $id, ObjectService $objectService, ElasticSear /** * @NoAdminRequired * @NoCSRFRequired + * @throws GuzzleException In case deleting the file from NextCloud fails. + * @throws \OCP\DB\Exception In case deleting attachment from the NextCloud DB fails. */ public function destroy(string|int $id, ObjectService $objectService, ElasticSearchService $elasticSearchService): JSONResponse { - if($this->config->hasKey($this->appName, 'mongoStorage') === false - || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' + $attachment = $this->show(id: $id, objectService: $objectService)->getData(); + if ($this->config->hasKey(app: $this->appName, key: 'mongoStorage') === false + || $this->config->getValueString(app: $this->appName, key: 'mongoStorage') !== '1' + ) { + $attachment = $attachment->jsonSerialize(); + } + + // Todo: are we sure this is the best way to do this (how do we save the full path to this file in nextCloud) +// $publicationFolder = '(' . $this->request->getHeader('Publication-Id') . ') ' +// . $this->request->getHeader('Publication-Title'); +// $this->fileService->deleteFile(filePath: "Attachments/$publicationFolder" . $attachment['title'] . '.' . $attachment['extension']); + $filePath = explode(separator: '/', string: $attachment['reference']); + array_shift(array: $filePath); + $filePath = implode(separator: '/', array: $filePath); + $this->fileService->deleteFile(filePath: $filePath); + + if ($this->config->hasKey(app: $this->appName, key: 'mongoStorage') === false + || $this->config->getValueString(app: $this->appName, key: 'mongoStorage') !== '1' ) { - $this->attachmentMapper->delete($this->attachmentMapper->find((int) $id)); + $this->attachmentMapper->delete(entity: $this->attachmentMapper->find(id: (int) $id)); return new JSONResponse([]); } diff --git a/lib/Controller/CatalogiController.php b/lib/Controller/CatalogiController.php index 855133fe..e964309a 100644 --- a/lib/Controller/CatalogiController.php +++ b/lib/Controller/CatalogiController.php @@ -109,6 +109,8 @@ public function create(ObjectService $objectService, DirectoryService $directory { $data = $this->request->getParams(); + // Remove fields we should never post + unset($data['id']); foreach ($data as $key => $value) { if (str_starts_with($key, '_')) { unset($data[$key]); diff --git a/lib/Controller/ConfigurationController.php b/lib/Controller/ConfigurationController.php index e8eef4af..6d804e88 100644 --- a/lib/Controller/ConfigurationController.php +++ b/lib/Controller/ConfigurationController.php @@ -53,9 +53,7 @@ public function index(): JSONResponse 'elasticIndex' => '', 'organisationName' => 'my-organisation', 'organisationOin' => '', - 'organisationPki' => '', - 'adminUsername' => '', - 'adminPassword' => '' + 'organisationPki' => '' ]; try { diff --git a/lib/Controller/DirectoryController.php b/lib/Controller/DirectoryController.php index 77b383eb..b6af3038 100644 --- a/lib/Controller/DirectoryController.php +++ b/lib/Controller/DirectoryController.php @@ -72,19 +72,6 @@ public function page(?string $getParameter) } - /** - * @PublicPage - * @NoCSRFRequired - */ - public function add(?string $url, DirectoryService $directoryService): JSONResponse - { - $directories = []; - $directoryService->registerToExternalDirectory(url: $url, externalDirectories: $directories); - - return new JSONResponse(['listingsAdded' => $directories]); - } - - /** * @PublicPage * @NoCSRFRequired @@ -143,39 +130,15 @@ public function show(string|int $id, ObjectService $objectService, DirectoryServ /** - * @NoAdminRequired + * @PublicPage * @NoCSRFRequired */ - public function create(ObjectService $objectService, DirectoryService $directoryService): JSONResponse + public function create(string $directory, DirectoryService $directoryService): JSONResponse { + $directories = []; + $directoryService->registerToExternalDirectory(url: $directory, externalDirectories: $directories); - $data = $this->request->getParams(); - - foreach($data as $key => $value) { - if(str_starts_with($key, '_')) { - unset($data[$key]); - } - } - - if($this->config->hasKey($this->appName, 'mongoStorage') === false - || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' - ) { - return new JSONResponse($this->listingMapper->createFromArray(object: $data)); - } - - $dbConfig['base_uri'] = $this->config->getValueString(app: $this->appName, key: 'mongodbLocation'); - $dbConfig['headers']['api-key'] = $this->config->getValueString(app: $this->appName, key: 'mongodbKey'); - $dbConfig['mongodbCluster'] = $this->config->getValueString(app: $this->appName, key: 'mongodbCluster'); - - $data['_schema'] = 'directory'; - - $returnData = $objectService->saveObject( - data: $data, - config: $dbConfig - ); - - // get post from requests - return new JSONResponse($returnData); + return new JSONResponse(['results' => $directories]); } /** @@ -187,14 +150,13 @@ public function update(string|int $id, ObjectService $objectService): JSONRespon $data = $this->request->getParams(); + // Remove fields we should never post + unset($data['id']); foreach($data as $key => $value) { if(str_starts_with($key, '_')) { unset($data[$key]); } } - if (isset($data['id'])) { - unset( $data['id']); - } if($this->config->hasKey($this->appName, 'mongoStorage') === false diff --git a/lib/Controller/MetaDataController.php b/lib/Controller/MetaDataController.php index 8b2c8268..ace7db19 100644 --- a/lib/Controller/MetaDataController.php +++ b/lib/Controller/MetaDataController.php @@ -107,6 +107,8 @@ public function create(ObjectService $objectService): JSONResponse $data = $this->request->getParams(); + // Remove fields we should never post + unset($data['id']); foreach($data as $key => $value) { if(str_starts_with($key, '_')) { unset($data[$key]); @@ -141,14 +143,13 @@ public function update(string|int $id, ObjectService $objectService): JSONRespon { $data = $this->request->getParams(); + // Remove fields we should never post + unset($data['id']); foreach($data as $key => $value) { if(str_starts_with($key, '_')) { unset($data[$key]); } } - if (isset($data['id'])) { - unset( $data['id']); - } if($this->config->hasKey($this->appName, 'mongoStorage') === false || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' diff --git a/lib/Controller/PublicationsController.php b/lib/Controller/PublicationsController.php index d0fe3c44..6534ff1d 100644 --- a/lib/Controller/PublicationsController.php +++ b/lib/Controller/PublicationsController.php @@ -109,14 +109,18 @@ public function index(ObjectService $objectService, SearchService $searchService ) { $searchParams = $searchService->createMySQLSearchParams(filters: $filters); $searchConditions = $searchService->createMySQLSearchConditions(filters: $filters, fieldsToSearch: $fieldsToSearch); + $sort = $searchService->createSortForMySQL(filters: $filters); $filters = $searchService->unsetSpecialQueryParams(filters: $filters); - return new JSONResponse(['results' => $this->publicationMapper->findAll(limit: null, offset: null, filters: $filters, searchConditions: $searchConditions, searchParams: $searchParams)]); + return new JSONResponse(['results' => $this->publicationMapper->findAll(limit: null, offset: null, filters: $filters, searchConditions: $searchConditions, searchParams: $searchParams, sort: $sort)]); } $filters = $searchService->createMongoDBSearchFilter(filters: $filters, fieldsToSearch: $fieldsToSearch); $filters = $searchService->unsetSpecialQueryParams(filters: $filters); + // @todo Fix mongodb sort + // $sort = $searchService->createSortForMongoDB(filters: $filters); + $dbConfig['base_uri'] = $this->config->getValueString(app: $this->appName, key: 'mongodbLocation'); $dbConfig['headers']['api-key'] = $this->config->getValueString(app: $this->appName, key: 'mongodbKey'); $dbConfig['mongodbCluster'] = $this->config->getValueString(app: $this->appName, key: 'mongodbCluster'); @@ -161,6 +165,8 @@ public function create(ObjectService $objectService, ElasticSearchService $elast { $data = $this->request->getParams(); + // Remove fields we should never post + unset($data['id']); foreach($data as $key => $value) { if(str_starts_with($key, '_')) { unset($data[$key]); @@ -215,14 +221,13 @@ public function update(string|int $id, ObjectService $objectService, ElasticSear $data = $this->request->getParams(); + // Remove fields we should never post + unset($data['id']); foreach($data as $key => $value) { if(str_starts_with($key, '_')) { unset($data[$key]); } } - if (isset($data['id'])) { - unset( $data['id']); - } if($this->config->hasKey($this->appName, 'mongoStorage') === false || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' diff --git a/lib/Controller/SearchController.php b/lib/Controller/SearchController.php index 27732845..7e4fcb90 100644 --- a/lib/Controller/SearchController.php +++ b/lib/Controller/SearchController.php @@ -5,36 +5,60 @@ use OCA\OpenCatalogi\Service\ElasticSearchService; use OCA\OpenCatalogi\Db\PublicationMapper; use OCA\OpenCatalogi\Service\SearchService; +use OCP\AppFramework\ApiController; use OCP\AppFramework\Controller; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\PublicPage; +use OCP\AppFramework\Http\Response; use OCP\AppFramework\Http\TemplateResponse; use OCP\AppFramework\Http\JSONResponse; use OCP\IAppConfig; use OCP\IRequest; - class SearchController extends Controller { - const TEST_ARRAY = [ - "d9e1467e-fc55-44c8-bf5c-bf139ac10eda" => [ - "id" => "d9e1467e-fc55-44c8-bf5c-bf139ac10eda", - "name" => "Search one", - "summary" => "summary for one" - ], - "e9d0131b-06c4-4d20-aa17-3b2aaad186d7" => [ - "id" => "e9d0131b-06c4-4d20-aa17-3b2aaad186d7", - "name" => "Search two", - "summary" => "summary for two" - ] - ]; public function __construct( $appName, IRequest $request, private readonly PublicationMapper $publicationMapper, - private readonly IAppConfig $config) - { - parent::__construct($appName, $request); + private readonly IAppConfig $config, + $corsMethods = 'PUT, POST, GET, DELETE, PATCH', + $corsAllowedHeaders = 'Authorization, Content-Type, Accept', + $corsMaxAge = 1728000 + ) { + parent::__construct($appName, $request); + $this->corsMethods = $corsMethods; + $this->corsAllowedHeaders = $corsAllowedHeaders; + $this->corsMaxAge = $corsMaxAge; } + /** + * This method implements a preflighted cors response for you that you can + * link to for the options request + * + * @NoAdminRequired + * @NoCSRFRequired + * @PublicPage + * @since 7.0.0 + */ + #[NoCSRFRequired] + #[PublicPage] + public function preflightedCors() { + if (isset($this->request->server['HTTP_ORIGIN'])) { + $origin = $this->request->server['HTTP_ORIGIN']; + } else { + $origin = '*'; + } + + $response = new Response(); + $response->addHeader('Access-Control-Allow-Origin', $origin); + $response->addHeader('Access-Control-Allow-Methods', $this->corsMethods); + $response->addHeader('Access-Control-Max-Age', (string)$this->corsMaxAge); + $response->addHeader('Access-Control-Allow-Headers', $this->corsAllowedHeaders); + $response->addHeader('Access-Control-Allow-Credentials', 'false'); + return $response; + } + /** * @NoAdminRequired * @NoCSRFRequired @@ -71,14 +95,28 @@ public function index(SearchService $searchService): JSONResponse $fieldsToSearch = ['title', 'description', 'summary']; - if($this->config->hasKey($this->appName, 'mongoStorage') === false - || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' + if($this->config->hasKey($this->appName, 'elasticLocation') === false + || $this->config->getValueString($this->appName, 'elasticLocation') === '' ) { $searchParams = $searchService->createMySQLSearchParams(filters: $filters); $searchConditions = $searchService->createMySQLSearchConditions(filters: $filters, fieldsToSearch: $fieldsToSearch); + + $limit = null; + $offset = null; + + if(isset($filters['_limit']) === true) { + $limit = $filters['_limit']; + } + + if(isset($filters['_page']) === true) { + $offset = ($limit * ($filters['_page'] - 1)); + } + $filters = $searchService->unsetSpecialQueryParams(filters: $filters); - return new JSONResponse(['results' => $this->publicationMapper->findAll(limit: null, offset: null, filters: $filters, searchConditions: $searchConditions, searchParams: $searchParams)]); + + + return new JSONResponse(['results' => $this->publicationMapper->findAll(limit: $limit, offset: $offset, filters: $filters, searchConditions: $searchConditions, searchParams: $searchParams)]); } //@TODO: find a better way to get query params. This fixes it for now. diff --git a/lib/Db/Catalog.php b/lib/Db/Catalog.php index 14e50db8..05958101 100644 --- a/lib/Db/Catalog.php +++ b/lib/Db/Catalog.php @@ -9,13 +9,15 @@ class Catalog extends Entity implements JsonSerializable { - protected ?string $title = null; - protected ?string $summary = null; - protected ?string $description = null; - protected ?string $image = null; - protected ?string $search = null; + protected ?string $title = null; + protected ?string $summary = null; + protected ?string $description = null; + protected ?string $image = null; + protected ?string $search = null; - protected bool $listed = false; + protected bool $listed = false; + protected ?string $organisation = null; + protected ?array $metadata = null; public function __construct() { $this->addType(fieldName: 'title', type: 'string'); @@ -24,6 +26,8 @@ public function __construct() { $this->addType(fieldName: 'image', type: 'string'); $this->addType(fieldName: 'search', type: 'string'); $this->addType(fieldName: 'listed', type: 'boolean'); + $this->addType(fieldName: 'organisation', type: 'string'); + $this->addType(fieldName: 'metadata', type: 'json'); } @@ -38,11 +42,17 @@ public function getJsonFields(): array public function hydrate(array $object): self { + + + if(isset($object['metadata']) === false) { + $object['metadata'] = []; + } + $jsonFields = $this->getJsonFields(); foreach($object as $key => $value) { if (in_array($key, $jsonFields) === true && $value === []) { - $value = null; + $value = []; } $method = 'set'.ucfirst($key); @@ -67,6 +77,8 @@ public function jsonSerialize(): array 'image' => $this->image, 'search' => $this->search, 'listed' => $this->listed, + 'metadata' => $this->metadata, + 'organisation'=> $this->organisation, ]; diff --git a/lib/Db/CatalogMapper.php b/lib/Db/CatalogMapper.php index 27581874..0cc429d2 100644 --- a/lib/Db/CatalogMapper.php +++ b/lib/Db/CatalogMapper.php @@ -38,7 +38,13 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters ->setFirstResult($offset); foreach($filters as $filter => $value) { - $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); + if ($value === 'IS NOT NULL') { + $qb->andWhere($qb->expr()->isNotNull($filter)); + } elseif ($value === 'IS NULL') { + $qb->andWhere($qb->expr()->isNull($filter)); + } else { + $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); + } } if (!empty($searchConditions)) { diff --git a/lib/Db/Listing.php b/lib/Db/Listing.php index edcdc382..de0825ad 100644 --- a/lib/Db/Listing.php +++ b/lib/Db/Listing.php @@ -78,7 +78,7 @@ public function jsonSerialize(): array 'metadata' => $this->metadata, 'catalogId' => $this->catalogId, 'status' => $this->status, - 'lastSync' => $this->lastSync, + 'lastSync' => $this->lastSync->format('c'), 'default' => $this->default, 'available' => $this->available, ]; diff --git a/lib/Db/ListingMapper.php b/lib/Db/ListingMapper.php index 75c8504d..bbe56e0e 100644 --- a/lib/Db/ListingMapper.php +++ b/lib/Db/ListingMapper.php @@ -38,7 +38,13 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters ->setFirstResult($offset); foreach($filters as $filter => $value) { - $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); + if ($value === 'IS NOT NULL') { + $qb->andWhere($qb->expr()->isNotNull($filter)); + } elseif ($value === 'IS NULL') { + $qb->andWhere($qb->expr()->isNull($filter)); + } else { + $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); + } } if (!empty($searchConditions)) { diff --git a/lib/Db/MetaDataMapper.php b/lib/Db/MetaDataMapper.php index 75ddaede..d73d1859 100644 --- a/lib/Db/MetaDataMapper.php +++ b/lib/Db/MetaDataMapper.php @@ -38,7 +38,13 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters ->setFirstResult($offset); foreach($filters as $filter => $value) { - $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); + if ($value === 'IS NOT NULL') { + $qb->andWhere($qb->expr()->isNotNull($filter)); + } elseif ($value === 'IS NULL') { + $qb->andWhere($qb->expr()->isNull($filter)); + } else { + $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); + } } if (!empty($searchConditions)) { diff --git a/lib/Db/OrganisationMapper.php b/lib/Db/OrganisationMapper.php index ba94bcac..2b412b2f 100644 --- a/lib/Db/OrganisationMapper.php +++ b/lib/Db/OrganisationMapper.php @@ -38,7 +38,13 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters ->setFirstResult($offset); foreach($filters as $filter => $value) { - $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); + if ($value === 'IS NOT NULL') { + $qb->andWhere($qb->expr()->isNotNull($filter)); + } elseif ($value === 'IS NULL') { + $qb->andWhere($qb->expr()->isNull($filter)); + } else { + $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); + } } if (!empty($searchConditions)) { diff --git a/lib/Db/Publication.php b/lib/Db/Publication.php index 334c1dac..3dc4edad 100644 --- a/lib/Db/Publication.php +++ b/lib/Db/Publication.php @@ -75,10 +75,16 @@ public function hydrate(array $object): self $this->setAttachments(null); $this->setOrganization(null); $this->setData(null); + $this->setModified(new DateTime()); + + + if(isset($object['published']) === false) { + $object['published'] = null; + } foreach($object as $key => $value) { if (in_array($key, $jsonFields) === true && $value === []) { - $value = []; + $value = null; } $method = 'set'.ucfirst($key); @@ -90,6 +96,7 @@ public function hydrate(array $object): self } } + // Todo: MetaData is depricated, we should use Schema instead. But this needs front-end changes as well. $this->setSchema($this->getMetaData()); $this->setAttachmentCount('0'); @@ -113,8 +120,8 @@ public function jsonSerialize(): array 'portal' => $this->portal, 'catalogi' => $this->catalogi, 'metaData' => $this->metaData, - 'published' => $this->published->format('c'), - 'modified' => $this->modified->format('c'), + 'published' => $this->published?->format('c'), + 'modified' => $this->modified?->format('c'), 'featured' => $this->featured !== null ? (bool) $this->featured : null, 'organization' => $this->organization, 'data' => $this->data, diff --git a/lib/Db/PublicationMapper.php b/lib/Db/PublicationMapper.php index c834dfbf..74f6f0fd 100644 --- a/lib/Db/PublicationMapper.php +++ b/lib/Db/PublicationMapper.php @@ -28,8 +28,14 @@ public function find(int $id): Publication return $this->findEntity(query: $qb); } - public function findAll(?int $limit = null, ?int $offset = null, ?array $filters = [], ?array $searchConditions = [], ?array $searchParams = []): array - { + public function findAll( + ?int $limit = null, + ?int $offset = null, + ?array $filters = [], + ?array $searchConditions = [], + ?array $searchParams = [], + ?array $sort = [] + ): array { $qb = $this->db->getQueryBuilder(); $qb->select('*') @@ -38,16 +44,29 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters ->setFirstResult($offset); foreach($filters as $filter => $value) { - $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); + if ($value === 'IS NOT NULL') { + $qb->andWhere($qb->expr()->isNotNull($filter)); + } elseif ($value === 'IS NULL') { + $qb->andWhere($qb->expr()->isNull($filter)); + } else { + $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); + } } - if (!empty($searchConditions)) { + if (empty($searchConditions) === false) { $qb->andWhere('(' . implode(' OR ', $searchConditions) . ')'); foreach ($searchParams as $param => $value) { $qb->setParameter($param, $value); } } + if (empty($sort) === false) { + foreach ($sort as $field => $direction) { + $direction = strtoupper($direction) === 'DESC' ? 'DESC' : 'ASC'; + $qb->addOrderBy($field, $direction); + } + } + return $this->findEntities(query: $qb); } diff --git a/lib/Db/ThemeMapper.php b/lib/Db/ThemeMapper.php index 31262f43..96f2b37c 100644 --- a/lib/Db/ThemeMapper.php +++ b/lib/Db/ThemeMapper.php @@ -38,7 +38,13 @@ public function findAll(?int $limit = null, ?int $offset = null, ?array $filters ->setFirstResult($offset); foreach($filters as $filter => $value) { - $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); + if ($value === 'IS NOT NULL') { + $qb->andWhere($qb->expr()->isNotNull($filter)); + } elseif ($value === 'IS NULL') { + $qb->andWhere($qb->expr()->isNull($filter)); + } else { + $qb->andWhere($qb->expr()->eq($filter, $qb->createNamedParameter($value))); + } } if (!empty($searchConditions)) { diff --git a/lib/Migration/Version6Date20240723125106.php b/lib/Migration/Version6Date20240723125106.php index 4e18cd45..d12ccc8d 100644 --- a/lib/Migration/Version6Date20240723125106.php +++ b/lib/Migration/Version6Date20240723125106.php @@ -81,15 +81,15 @@ public function changeSchema(IOutput $output, Closure $schemaClosure, array $opt ] ); $table->addColumn(name: 'organization', typeName: TYPES::JSON, options: [ - 'default' => [], + 'default' => 'a:0:{}', 'notnull' => false, ]); $table->addColumn(name: 'data', typeName: TYPES::JSON, options: [ - 'default' => [], + 'default' => 'a:0:{}', 'notnull' => false, ]); $table->addColumn(name: 'attachments', typeName: TYPES::JSON, options: [ - 'default' => [], + 'default' => 'a:0:{}', 'notnull' => false, ]); $table->addColumn(name: 'attachment_count', typeName: TYPES::INTEGER); diff --git a/lib/Migration/Version6Date20240808085441.php b/lib/Migration/Version6Date20240808085441.php new file mode 100644 index 00000000..28e43b3a --- /dev/null +++ b/lib/Migration/Version6Date20240808085441.php @@ -0,0 +1,77 @@ +hasTable(tableName: 'catalogi') === true) { + $table = $schema->getTable(tableName: 'catalogi'); + + if($table->hasColumn(name: 'organization') === false) { + $table->addColumn( + name: 'organization', + typeName: Types::STRING, + options: [ + 'notNull' => false, + 'default' => null + ]); + } + if($table->hasColumn(name: 'metadata') === false) { + $table->addColumn( + name: 'metadata', + typeName: Types::JSON, + options: [ + 'notNull' => false, + 'default' => 'a:0:{}' + ]); + } + + } + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } +} diff --git a/lib/Migration/Version6Date20240808092738.php b/lib/Migration/Version6Date20240808092738.php new file mode 100644 index 00000000..e5babbe7 --- /dev/null +++ b/lib/Migration/Version6Date20240808092738.php @@ -0,0 +1,63 @@ +hasTable(tableName: 'publications') === true) { + $table = $schema->getTable(tableName: 'publications'); + + if($table->hasColumn(name: 'published') === true) { + $column = $table->getColumn(name: 'published'); + $column->setDefault(default: null); + } + + } + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } +} diff --git a/lib/Migration/Version6Date20240808093230.php b/lib/Migration/Version6Date20240808093230.php new file mode 100644 index 00000000..81e9d2bc --- /dev/null +++ b/lib/Migration/Version6Date20240808093230.php @@ -0,0 +1,62 @@ +hasTable(tableName: 'publications') === true) { + $table = $schema->getTable(tableName: 'publications'); + + if($table->hasColumn(name: 'published') === true) { + $column = $table->getColumn(name: 'published'); + $column->setNotnull(notnull: false); + } + + } + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } +} diff --git a/lib/Migration/Version6Date20240808115347.php b/lib/Migration/Version6Date20240808115347.php new file mode 100644 index 00000000..4e60dccb --- /dev/null +++ b/lib/Migration/Version6Date20240808115347.php @@ -0,0 +1,71 @@ +hasTable(tableName: 'catalogi') === true) { + $table = $schema->getTable(tableName: 'catalogi'); + + if($table->hasColumn(name: 'organization') === true) { + $column = $table->dropColumn('organization'); + } + if($table->hasColumn(name: 'organisation') === false) { + $table->addColumn( + name: 'organisation', + typeName: Types::STRING, + options: [ + 'notNull' => false, + 'default' => null + ]); + } + + } + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure(): ISchemaWrapper $schemaClosure + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } +} diff --git a/lib/Service/DirectoryService.php b/lib/Service/DirectoryService.php index 53b06534..dc47b754 100644 --- a/lib/Service/DirectoryService.php +++ b/lib/Service/DirectoryService.php @@ -6,6 +6,7 @@ use GuzzleHttp\Client; use OCA\OpenCatalogi\Db\Catalog; use OCA\OpenCatalogi\Db\CatalogMapper; +use OCA\OpenCatalogi\Db\Listing; use OCA\OpenCatalogi\Db\ListingMapper; use OCP\IAppConfig; use OCP\IURLGenerator; @@ -52,21 +53,26 @@ public function registerToExternalDirectory (array $newDirectory = [], ?string $ if($this->config->getValueString($this->appName, 'mongoStorage') !== '1') { - $catalogi = $this->catalogMapper->findAll(); + $catalogi = $this->listingMapper->findAll(); } else { $dbConfig['base_uri'] = $this->config->getValueString('opencatalogi', 'mongodbLocation'); $dbConfig['headers']['api-key'] = $this->config->getValueString('opencatalogi', 'mongodbKey'); $dbConfig['mongodbCluster'] = $this->config->getValueString('opencatalogi', 'mongodbCluster'); - $catalogi = $this->objectService->findObjects(filters: ['_schema' => 'catalog'], config: $dbConfig)['documents']; + $catalogi = $this->objectService->findObjects(filters: ['_schema' => 'directory'], config: $dbConfig)['documents']; } foreach($catalogi as $catalog) { - if($catalog instanceof Catalog) { + if($catalog instanceof Listing) { $catalog = $catalog->jsonSerialize(); } - $directory = $this->getDirectoryEntry($catalog['id']); - $result = $this->client->post(uri: $url, options: ['json' => $directory, 'http_errors' => false]); + unset($catalog['_id'], $catalog['id'], $catalog['_schema']); + + if($catalog['directory'] !== $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute(routeName:"opencatalogi.directory.index"))) { + continue; + } + + $result = $this->client->post(uri: $url, options: ['json' => $catalog, 'http_errors' => false]); } $externalDirectories = $this->fetchFromExternalDirectory(url: $url); @@ -107,8 +113,6 @@ private function createDirectoryFromResult(array $result): ?array $this->listingMapper->createFromArray($result); } - $this->registerToExternalDirectory(newDirectory: $result); - return $returnData; } @@ -140,6 +144,9 @@ public function listDirectory(array $filters = [], int $limit = 30, int $offset if ($this->config->hasKey($this->appName, 'mongoStorage') === false || $this->config->getValueString($this->appName, 'mongoStorage') !== '1' ) { + $filters['catalog_id'] = $filters['catalogId']; + unset($filters['catalogId']); + return $this->listingMapper->findAll(limit: $limit, offset: $offset, filters: $filters); } $filters['_schema'] = 'directory'; @@ -232,9 +239,9 @@ public function listCatalog (array $catalog): array 'mongodbCluster' => $this->config->getValueString($this->appName, 'mongodbCluster') ]; - $data['_schema'] = 'catalog'; + $listing['_schema'] = 'directory'; - $returnData = $this->objectService->saveObject($data, $dbConfig); + $returnData = $this->objectService->saveObject($listing, $dbConfig); return $catalog; } catch (\Exception $e) { $catalog['listed'] = false; diff --git a/lib/Service/ElasticSearchService.php b/lib/Service/ElasticSearchService.php index da7bcdf6..ef16edd4 100644 --- a/lib/Service/ElasticSearchService.php +++ b/lib/Service/ElasticSearchService.php @@ -91,6 +91,42 @@ public function updateObject(string $id, array $object, array $config): array } } + public function parseFilter(string $name, array $filter): array + { + + if(is_array($filter) === false) { + return ['match' => [$name => $filter]]; + } + + foreach($filter as $key => $value) { + switch($key) { + case 'regexp': + case 'like': + if(preg_match("/^\/.+\/[a-z]*$/i", $value) !== false) { + return ['regexp' => [$name => strtolower($value)]]; + } else { + return ['match' => [$name => $value]]; + } + case '>=': + case 'after': + return ['range' => [$key => ['gte' => $value]]]; + case '>': + case 'strictly_after': + return ['range' => [$key => ['gt' => $value]]]; + case '<=': + case 'before': + return ['range' => [$key => ['lte' => $value]]]; + case '<': + case 'strictly_before': + return ['range' => [$key => ['lt' => $value]]]; + default: + return ['match' => [$name => $value]]; + } + } + + return ['match' => [$name => $filter]]; + } + public function parseFilters (array $filters): array { $body = [ @@ -124,10 +160,23 @@ public function parseFilters (array $filters): array ]; } + if(isset($filters['.limit']) === true) { + $body['size'] = (int) $filters['.limit']; + unset($filters['.limit']); + } + + if(isset($filters['.page']) === true) { + if(isset($body['size']) === true) { + $body['from'] = $body['size'] * ($filters['.page'] - 1); + } + unset($filters['.page']); + } + unset($filters['.search'], $filters['.queries'], $filters['.catalogi']); foreach ($filters as $name => $filter) { - $body['query']['bool']['must'][] = ['match' => [$name => $filter]]; + + $body['query']['bool']['must'][] = $this->parseFilter($name, $filter); } return $body; @@ -177,20 +226,23 @@ public function mapAggregationResults(array $result): array }//end mapAggregationResults() - public function searchObject(array $filters, array $config): array + public function searchObject(array $filters, array $config, int &$totalResults = 0): array { $body = $this->parseFilters(filters: $filters); $client = $this->getClient(config: $config); - $result = $client->search(params: [ 'index' => $config['index'], 'body' => $body ]); + $totalResults = $result['hits']['total']['value']; + $return = ['results' => array_map(callback: [$this, 'formatResults'], array: $result['hits']['hits'])]; if(isset($result['aggregations']) === true) { $return['facets'] = array_map([$this, 'mapAggregationResults'], $result['aggregations']); + } else { + $return['facets'] = []; } return $return; diff --git a/lib/Service/FileService.php b/lib/Service/FileService.php index f2f60655..6d6ec56c 100644 --- a/lib/Service/FileService.php +++ b/lib/Service/FileService.php @@ -2,36 +2,21 @@ namespace OCA\OpenCatalogi\Service; +use DateTime; use Exception; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\GuzzleException; -use OCP\IAppConfig; +use OCP\Files\IRootFolder; use OCP\IUserSession; +use OCP\Share\IManager; use Psr\Log\LoggerInterface; class FileService { - protected string $appName = 'opencatalogi'; - private Client $client; - public function __construct( private readonly IUserSession $userSession, private readonly LoggerInterface $logger, - private readonly IAppConfig $config - ) { - $this->client = new Client(); - } - - /** - * Sets the appName used for getting configuration, this should be set after creating this service! - * - * @param string $appName The appName to set. - * @return void - */ - public function setAppName(string $appName): void - { - $this->appName = $appName; - } + private readonly IRootFolder $rootFolder, + private readonly IManager $shareManager + ) {} /** * Gets and returns the current host / domain with correct protocol. @@ -55,48 +40,179 @@ private function getCurrentDomain(): string * Creates and returns a share link for a file (or folder). * (https://docs.nextcloud.com/server/latest/developer_manual/client_apis/OCS/ocs-share-api.html#create-a-new-share) * - * @param string $path Path to the file/folder which should be shared. + * @param string $path Path (from root) to the file/folder which should be shared. * @param int|null $shareType 0 = user; 1 = group; 3 = public link; 4 = email; 6 = federated cloud share; 7 = circle; 10 = Talk conversation * @param int|null $permissions 1 = read; 2 = update; 4 = create; 8 = delete; 16 = share; 31 = all (default: 31, for public shares: 1) * * @return string The share link. - * @throws GuzzleException|Exception In case the Guzzle call returns an exception. + * @throws Exception In case creating the share(link) fails. */ public function createShareLink(string $path, ?int $shareType = 3, ?int $permissions = null): string { - // API endpoint to create a share - $url = "{$this->getCurrentDomain()}/ocs/v2.php/apps/files_sharing/api/v1/shares"; + if ($permissions === null) { + $permissions = 31; + if ($shareType === 3) { + $permissions = 1; + } + } + + $path = trim(string: $path, characters: '/'); + + // Get the current user. + $currentUser = $this->userSession->getUser(); + $userId = $currentUser ? $currentUser->getUID() : 'Guest'; + try { + $userFolder = $this->rootFolder->getUserFolder(userId: $userId); + } catch(\OCP\Files\NotPermittedException) { + $this->logger->error("Can't create share link for $path because user (folder) couldn't be found"); + + return "User (folder) couldn't be found"; + } + + try { + // Note: if we ever want to create share links for folders instead of files, just remove this try catch and only use setTarget, not setNodeId. + $file = $userFolder->get(path: $path); + } catch(\OCP\Files\NotFoundException $e) { + $this->logger->error("Can't create share link for $path because file doesn't exist"); + + return 'File not found at '.$path; + } + + $share = $this->shareManager->newShare(); + $share->setTarget(target: "/$path"); + $share->setNodeId(fileId: $file->getId()); + $share->setNodeType(type: 'file'); + $share->setShareType(shareType: $shareType); + if ($permissions !== null) { + $share->setPermissions(permissions: $permissions); + } + $share->setSharedBy(sharedBy: $userId); + $share->setShareOwner(shareOwner: $userId); + $share->setShareTime(shareTime: new DateTime()); + $share->setStatus(status: $share::STATUS_ACCEPTED); + + try + { + $share = $this->shareManager->createShare(share: $share); + return $this->getCurrentDomain() . '/index.php/s/' . $share->getToken(); + } catch (Exception $exception) { + $this->logger->error("Can't create share link for $path: " . $exception->getMessage()); + + throw new Exception('Can\'t create share link'); + } + } + + /** + * Uploads a file to NextCloud. Will overwrite a file if it already exists and create a new one if it doesn't exist. + * + * @param mixed $content The content of the file. + * @param string $filePath Path (from root) where to save the file. NOTE: this should include the name and extension/format of the file as well! (example.pdf) + * + * @return bool True if successful. + * @throws Exception In case we can't write to file because it is not permitted. + */ + public function uploadFile(mixed $content, string $filePath): bool + { + $filePath = trim(string: $filePath, characters: '/'); + + // Get the current user. + $currentUser = $this->userSession->getUser(); + $userFolder = $this->rootFolder->getUserFolder(userId: $currentUser ? $currentUser->getUID() : 'Guest'); + + // Check if file exists and create it if not. + try { + try { + $userFolder->get(path: $filePath); + } catch(\OCP\Files\NotFoundException $e) { + $userFolder->newFile(path: $filePath); + $file = $userFolder->get(path: $filePath); + + $file->putContent(data: $content); + + return true; + } + + // File already exists. + $this->logger->warning("File $filePath already exists."); + return false; + + } catch(\OCP\Files\NotPermittedException|\OCP\Files\GenericFileException|\OCP\Lock\LockedException $e) { + $this->logger->error("Can't create file $filePath: " . $e->getMessage()); + + throw new Exception("Can't write to file $filePath"); + } + } + + /** + * Deletes a file from NextCloud. + * + * @param string $filePath Path (from root) to the file you want to delete. + * + * @return bool True if successful. + * @throws Exception In case deleting the file is not permitted. + */ + public function deleteFile(string $filePath): bool + { + $filePath = trim(string: $filePath, characters: '/'); - // Get the admin username & password for auth - $username = $this->config->getValueString(app: $this->appName, key: 'adminUsername', default: 'admin'); - $password = $this->config->getValueString(app: $this->appName, key: 'adminPassword', default: 'admin'); + // Get the current user. + $currentUser = $this->userSession->getUser(); + $userFolder = $this->rootFolder->getUserFolder(userId: $currentUser ? $currentUser->getUID() : 'Guest'); - // Get the current username + // Check if file exists and delete it if it does. + try { + try { + $file = $userFolder->get(path: $filePath); + $file->delete(); + + return true; + } catch(\OCP\Files\NotFoundException $e) { + // File does not exist. + $this->logger->warning("File $filePath does not exist."); + + return false; + } + } catch(\OCP\Files\NotPermittedException|\OCP\Files\InvalidPathException $e) { + $this->logger->error("Can't delete file $filePath: " . $e->getMessage()); + + throw new Exception("Can't delete file $filePath"); + } + } + + /** + * Creates a new folder in NextCloud, unless it already exists. + * + * @param string $folderPath Path (from root) to where you want to create a folder, include the name of the folder. (/Media/exampleFolder) + * + * @return bool True if successfully created a new folder. + * @throws Exception In case we can't create the folder because it is not permitted. + */ + public function createFolder(string $folderPath): bool + { + $folderPath = trim(string: $folderPath, characters: '/'); + + // Get the current user. $currentUser = $this->userSession->getUser(); - $currentUsername = $currentUser ? $currentUser->getUID() : 'Guest'; - - // Data for the POST request - $options = [ - 'auth' => [$username, $password], - 'headers' => [ - 'OCS-APIREQUEST' => 'true', - 'Content-Type' => 'application/x-www-form-urlencoded' - ], - 'form_params' => [ - 'path' => $path, - 'shareType' => $shareType, - 'permissions' => $permissions, - 'shareWith' => $currentUsername - ] - ]; + $userFolder = $this->rootFolder->getUserFolder(userId: $currentUser ? $currentUser->getUID() : 'Guest'); + // Check if folder exists and if not create it. try { - $response = $this->client->post(uri: $url, options: $options); - $data = json_decode($response->getBody()->getContents(), true); - return $data['ocs']['data']['url'] ?? ''; - } catch (Exception $e) { - $this->logger->error('Failed to create share link: ' . $e->getMessage()); - throw $e; + try { + $userFolder->get(path: $folderPath); + } catch(\OCP\Files\NotFoundException $e) { + $userFolder->newFolder(path: $folderPath); + + return true; + } + + // Folder already exists. + $this->logger->info("This folder already exits $folderPath"); + return false; + + } catch(\OCP\Files\NotPermittedException $e) { + $this->logger->error("Can't create folder $folderPath: " . $e->getMessage()); + + throw new Exception("Can\'t create folder $folderPath"); } } diff --git a/lib/Service/ObjectService.php b/lib/Service/ObjectService.php index f1660beb..1bce6687 100644 --- a/lib/Service/ObjectService.php +++ b/lib/Service/ObjectService.php @@ -78,6 +78,11 @@ public function findObjects(array $filters, array $config): array $object['dataSource'] = $config['mongodbCluster']; $object['filter'] = $filters; + // @todo Fix mongodb sort + // if (empty($sort) === false) { + // $object['filter'][] = ['$sort' => $sort]; + // } + $returnData = $client->post( uri: 'action/find', options: ['json' => $object] @@ -117,7 +122,7 @@ public function findObject(array $filters, array $config): array associative: true ); - return ['document' => $result]; + return $result['document']; } diff --git a/lib/Service/SearchService.php b/lib/Service/SearchService.php index 446ac2fd..d31d18b1 100644 --- a/lib/Service/SearchService.php +++ b/lib/Service/SearchService.php @@ -4,6 +4,7 @@ use GuzzleHttp\Client; use GuzzleHttp\Promise\Utils; +use OCP\IURLGenerator; use Symfony\Component\Uid\Uuid; class SearchService @@ -18,6 +19,7 @@ class SearchService public function __construct( private readonly ElasticSearchService $elasticService, private readonly DirectoryService $directoryService, + private readonly IURLGenerator $urlGenerator, ) { $this->client = new Client(); } @@ -81,8 +83,12 @@ public function search(array $parameters, array $elasticConfig, array $dbConfig, $localResults['results'] = []; $localResults['facets'] = []; + $totalResults = 0; + $limit = isset($parameters['.limit']) === true ? $parameters['.limit'] : 30; + $page = isset($parameters['.page']) === true ? $parameters['.page'] : 1; + if($elasticConfig['location'] !== '') { - $localResults = $this->elasticService->searchObject($parameters, $elasticConfig); + $localResults = $this->elasticService->searchObject(filters: $parameters, config: $elasticConfig, totalResults: $totalResults,); } $directory = $this->directoryService->listDirectory(limit: 1000); @@ -90,7 +96,15 @@ public function search(array $parameters, array $elasticConfig, array $dbConfig, // $directory = $this->objectService->findObjects(filters: ['_schema' => 'directory'], config: $dbConfig); if(count($directory) === 0) { - return $localResults; + return [ + 'results' => $localResults['results'], + 'facets' => $localResults['facets'], + 'count' => count($localResults['results']), + 'limit' => $limit, + 'page' => $page, + 'pages' => ceil($totalResults / $limit), + 'total' => $totalResults + ]; } $results = $localResults['results']; @@ -98,12 +112,14 @@ public function search(array $parameters, array $elasticConfig, array $dbConfig, $searchEndpoints = []; + $promises = []; foreach($directory as $instance) { if( $instance['default'] === false - && isset($parameters['.catalogi']) === true + || isset($parameters['.catalogi']) === true && in_array($instance['catalogId'], $parameters['.catalogi']) === false + || $instance['search'] = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->linkToRoute(routeName:"opencatalogi.directory.index")) ) { continue; } @@ -139,7 +155,15 @@ public function search(array $parameters, array $elasticConfig, array $dbConfig, } } - return ['results' => $results, 'facets' => $aggregations]; + return [ + 'results' => $results, + 'facets' => $aggregations, + 'count' => count($results), + 'limit' => $limit, + 'page' => $page, + 'pages' => ceil($totalResults / $limit), + 'total' => $totalResults + ]; } /** @@ -204,6 +228,15 @@ public function createMongoDBSearchFilter(array $filters, array $fieldsToSearch) unset($filters['_search']); } + foreach ($filters as $field => $value) { + if ($value === 'IS NOT NULL') { + $filters[$field] = ['$ne' => null]; + } + if ($value === 'IS NULL') { + $filters[$field] = ['$eq' => null]; + } + } + return $filters; }//end createMongoDBSearchFilter() @@ -266,6 +299,49 @@ public function createMySQLSearchParams(array $filters): array }//end createMongoDBSearchFilter() + /** + * This function creates an sort array based on given order param from request. + * + * @param array $filters Query parameters from request. + * + * @return array $sort + */ + public function createSortForMySQL(array $filters): array + { + $sort = []; + if (isset($filters['_order']) && is_array($filters['_order'])) { + foreach ($filters['_order'] as $field => $direction) { + $direction = strtoupper($direction) === 'DESC' ? 'DESC' : 'ASC'; + $sort[$field] = $direction; + } + } + + return $sort; + + }//end createSortArrayFromParams() + + /** + * This function creates an sort array based on given order param from request. + * + * @todo Not functional yet. Needs to be fixed (see PublicationsController->index). + * + * @param array $filters Query parameters from request. + * + * @return array $sort + */ + public function createSortForMongoDB(array $filters): array + { + $sort = []; + if (isset($filters['_order']) && is_array($filters['_order'])) { + foreach ($filters['_order'] as $field => $direction) { + $sort[$field] = strtoupper($direction) === 'DESC' ? -1 : 1; + } + } + + return $sort; + + }//end createSortForMongoDB() + /** * Parses the request query string and returns it as an array of queries. * diff --git a/package-lock.json b/package-lock.json index df64e74f..7a448696 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,7 +17,9 @@ "@nextcloud/l10n": "^2.0.1", "@nextcloud/router": "^2.0.1", "@nextcloud/vue": "^8.12.0", + "@vueuse/core": "^10.11.0", "apexcharts": "^3.50.0", + "axios": "^1.7.3", "bootstrap-vue": "^2.23.1", "css-loader": "^6.8.1", "lodash": "^4.17.21", @@ -2016,7 +2018,6 @@ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", "dev": true, - "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -2039,15 +2040,13 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" + "dev": true }, "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2058,7 +2057,6 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, - "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -2074,7 +2072,6 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -2087,7 +2084,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2100,7 +2096,6 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, - "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -2113,7 +2108,6 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, - "license": "MIT", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } @@ -2176,7 +2170,6 @@ "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", "deprecated": "Use @eslint/config-array instead", "dev": true, - "license": "Apache-2.0", "dependencies": { "@humanwhocodes/object-schema": "^2.0.2", "debug": "^4.3.1", @@ -2191,7 +2184,6 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2202,7 +2194,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, - "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2228,8 +2219,7 @@ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead", - "dev": true, - "license": "BSD-3-Clause" + "dev": true }, "node_modules/@isaacs/cliui": { "version": "8.0.2", @@ -4983,6 +4973,7 @@ "version": "10.11.0", "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.0.tgz", "integrity": "sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g==", + "license": "MIT", "dependencies": { "@types/web-bluetooth": "^0.0.20", "@vueuse/metadata": "10.11.0", @@ -5761,9 +5752,10 @@ } }, "node_modules/axios": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz", - "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.3.tgz", + "integrity": "sha512-Ar7ND9pU99eJ9GpoGQKhKf58GpUOgnzuaB7ueNQ5BMi0p+LZ5oaEnfF999fAArcTIBwXTCHAmGcHOZJaWPq9Nw==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -8171,7 +8163,6 @@ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", "dev": true, - "license": "Apache-2.0", "dependencies": { "esutils": "^2.0.2" }, @@ -8677,7 +8668,6 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, - "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -9270,8 +9260,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true, - "license": "Python-2.0" + "dev": true }, "node_modules/eslint/node_modules/brace-expansion": { "version": "1.1.11", @@ -9334,7 +9323,6 @@ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", "dev": true, - "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -9351,7 +9339,6 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -9364,7 +9351,6 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -9390,7 +9376,6 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, - "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -9415,7 +9400,6 @@ "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, - "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -9482,7 +9466,6 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, - "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -9899,7 +9882,6 @@ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", "dev": true, - "license": "MIT", "dependencies": { "flat-cache": "^3.0.4" }, diff --git a/package.json b/package.json index 5e021bd2..c94890cb 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,9 @@ "@nextcloud/l10n": "^2.0.1", "@nextcloud/router": "^2.0.1", "@nextcloud/vue": "^8.12.0", + "@vueuse/core": "^10.11.0", "apexcharts": "^3.50.0", + "axios": "^1.7.3", "bootstrap-vue": "^2.23.1", "css-loader": "^6.8.1", "lodash": "^4.17.21", diff --git a/src/dialogs/Dialogs.vue b/src/dialogs/Dialogs.vue index 2b2b40a8..87a8e1cd 100644 --- a/src/dialogs/Dialogs.vue +++ b/src/dialogs/Dialogs.vue @@ -17,6 +17,7 @@ + @@ -33,6 +34,7 @@ import DeleteAttachmentDialog from './attachment/DeleteAttachmentDialog.vue' import DepublishAttachmentDialog from './attachment/DepublishAttachmentDialog.vue' import PublishAttachmentDialog from './attachment/PublishAttachmentDialog.vue' import DeleteCatalogDialog from './catalog/DeleteCatalogDialog.vue' +import DeleteCatalogiMetadata from './catalogiMetadata/DeleteCatalogiMetadata.vue' import DeleteListingDialog from './listing/DeleteListingDialog.vue' import ViewLogDialog from './logs/ViewLogDialog.vue' import CopyMetaDataDialog from './metaData/CopyMetaDataDialog.vue' @@ -54,6 +56,7 @@ export default { name: 'Dialogs', components: { DeleteCatalogDialog, + DeleteCatalogiMetadata, DeleteAttachmentDialog, PublishAttachmentDialog, DepublishAttachmentDialog, diff --git a/src/dialogs/catalogiMetadata/DeleteCatalogiMetadata.vue b/src/dialogs/catalogiMetadata/DeleteCatalogiMetadata.vue new file mode 100644 index 00000000..fb97f08b --- /dev/null +++ b/src/dialogs/catalogiMetadata/DeleteCatalogiMetadata.vue @@ -0,0 +1,126 @@ + + + + + + + diff --git a/src/dialogs/theme/DeleteThemeDialog.vue b/src/dialogs/theme/DeleteThemeDialog.vue index 83564a86..440b9f4a 100644 --- a/src/dialogs/theme/DeleteThemeDialog.vue +++ b/src/dialogs/theme/DeleteThemeDialog.vue @@ -23,6 +23,12 @@ import { navigationStore, themeStore } from '../../store/store.js' {{ succes ? 'Sluiten' : 'Annuleer' }} + + + Help + diff --git a/src/entities/attachment/attachment.mock.ts b/src/entities/attachment/attachment.mock.ts index fe1bd260..9c19129c 100644 --- a/src/entities/attachment/attachment.mock.ts +++ b/src/entities/attachment/attachment.mock.ts @@ -9,8 +9,8 @@ export const mockAttachmentsData = (): TAttachment[] => [ summary: 'a short form summary', description: 'a really really long description about this catalogus', labels: ['label1'], - accessURL: 'https://example.com/access', - downloadURL: 'https://example.com/download', + accessUrl: 'https://example.com/access', + downloadUrl: 'https://example.com/download', type: 'document', extension: 'pdf', size: '1024', @@ -32,8 +32,8 @@ export const mockAttachmentsData = (): TAttachment[] => [ summary: 'a short form summary', description: 'a really really long description about this catalogus', labels: [], - accessURL: '', - downloadURL: '', + accessUrl: '', + downloadUrl: '', type: 'document', extension: 'pdf', size: '1024', @@ -56,8 +56,8 @@ export const mockAttachmentsData = (): TAttachment[] => [ description: 'a really really long description about this catalogus', labels: ['label3'], // this is supposed to be a URL - accessURL: 'non url', - downloadURL: 'https://example.com/download', + accessUrl: 'non url', + downloadUrl: 'https://example.com/download', type: 'document', extension: 'pdf', size: '1024', diff --git a/src/entities/attachment/attachment.ts b/src/entities/attachment/attachment.ts index dcca058d..646d007e 100644 --- a/src/entities/attachment/attachment.ts +++ b/src/entities/attachment/attachment.ts @@ -9,8 +9,8 @@ export class Attachment implements TAttachment { public summary: string public description: string public labels: string[] - public accessURL: string - public downloadURL: string + public accessUrl: string + public downloadUrl: string public type: string public extension: string public size: string @@ -42,8 +42,8 @@ export class Attachment implements TAttachment { this.summary = data.summary || '' this.description = data.description || '' this.labels = data.labels || [] - this.accessURL = data.accessURL || '' - this.downloadURL = data.downloadURL || '' + this.accessUrl = data.accessUrl || '' + this.downloadUrl = data.downloadUrl || '' this.type = data.type || '' this.extension = data.extension || '' this.size = data.size || '' @@ -73,8 +73,8 @@ export class Attachment implements TAttachment { summary: z.string().max(255), description: z.string().max(2555), labels: z.string().array(), - accessURL: z.string().url().or(z.literal('')), - downloadURL: z.string().url().or(z.literal('')), + accessUrl: z.string().url().or(z.literal('')), + downloadUrl: z.string().url().or(z.literal('')), type: z.string(), anonymization: z.object({ anonymized: z.boolean(), diff --git a/src/entities/attachment/attachment.types.ts b/src/entities/attachment/attachment.types.ts index 7e462660..d43a981b 100644 --- a/src/entities/attachment/attachment.types.ts +++ b/src/entities/attachment/attachment.types.ts @@ -5,8 +5,8 @@ export type TAttachment = { summary: string description: string labels: string[] - accessURL: string - downloadURL: string + accessUrl: string + downloadUrl: string type: string extension: string size: string diff --git a/src/entities/catalogi/catalogi.mock.ts b/src/entities/catalogi/catalogi.mock.ts index 57d81e88..b47d847d 100644 --- a/src/entities/catalogi/catalogi.mock.ts +++ b/src/entities/catalogi/catalogi.mock.ts @@ -9,16 +9,8 @@ export const mockCatalogiData = (): TCatalogi[] => [ description: 'a really really long description about this catalogus', image: 'string', listed: false, - organisation: { - id: '2', - title: 'gogle', - summary: 'consultant services', - description: 'a very long description about the consultant company called gogle', - oin: '0012345678', - tooi: 'TECH001', - rsin: '987654321', - pki: 'PKI-12345-67890', - }, + organisation: '23', + metadata: ['1', '3'], }, { id: '2', @@ -27,16 +19,8 @@ export const mockCatalogiData = (): TCatalogi[] => [ description: 'a really really long description about this catalogus', image: '', listed: false, - organisation: { - id: '2', - title: 'gogle', - summary: 'consultant services', - description: 'a very long description about the consultant company called gogle', - oin: '0012345678', - tooi: 'TECH001', - rsin: '987654321', - pki: 'PKI-12345-67890', - }, + organisation: '23', + metadata: [], }, { id: '3', @@ -46,16 +30,8 @@ export const mockCatalogiData = (): TCatalogi[] => [ image: 'string', // @ts-expect-error -- listed needs to be a boolean listed: 0.2, - organisation: { - id: '2', - title: 'gogle', - summary: 'consultant services', - description: 'a very long description about the consultant company called gogle', - oin: '0012345678', - tooi: 'TECH001', - rsin: '987654321', - pki: 'PKI-12345-67890', - }, + organisation: '23', + metadata: ['1', '3'], }, ] diff --git a/src/entities/catalogi/catalogi.ts b/src/entities/catalogi/catalogi.ts index dd1d1202..e260782e 100644 --- a/src/entities/catalogi/catalogi.ts +++ b/src/entities/catalogi/catalogi.ts @@ -9,16 +9,9 @@ export class Catalogi implements TCatalogi { public description: string public image: string public listed: boolean - public organisation: { - id: string - title: string - summary: string - description: string - oin: string - tooi: string - rsin: string - pki: string - } + public organisation: string + + public metadata: string[] constructor(data: TCatalogi) { this.hydrate(data) @@ -32,36 +25,21 @@ export class Catalogi implements TCatalogi { this.description = data?.description || '' this.image = data?.image || '' this.listed = data?.listed || false - this.organisation = data.organisation || { - id: '', - title: '', - summary: '', - description: '', - oin: '', - tooi: '', - rsin: '', - pki: '', - } + this.organisation = data.organisation || '' + this.metadata = (Array.isArray(data.metadata) && data.metadata) || [] } /* istanbul ignore next */ public validate(): SafeParseReturnType { - // https://conduction.stoplight.io/docs/open-catalogi/8azwyic71djee-create-listing + // https://conduction.stoplight.io/docs/open-catalogi/l89lv7ocvq848-create-catalog const schema = z.object({ title: z.string().min(1).max(255), // .min(1) on a string functionally works the same as a nonEmpty check (SHOULD NOT BE COMBINED WITH .OPTIONAL()) summary: z.string().min(1).max(255), description: z.string().max(2555), image: z.string().max(255), listed: z.boolean(), - organisation: z.object({ - title: z.string(), - summary: z.string(), - description: z.string(), - oin: z.string(), - tooi: z.string(), - rsin: z.string(), - pki: z.string(), - }), + organisation: z.string(), + metadata: z.string().array(), }) const result = schema.safeParse({ diff --git a/src/entities/catalogi/catalogi.types.ts b/src/entities/catalogi/catalogi.types.ts index 47750642..7b14a391 100644 --- a/src/entities/catalogi/catalogi.types.ts +++ b/src/entities/catalogi/catalogi.types.ts @@ -5,14 +5,6 @@ export type TCatalogi = { description: string image: string listed: boolean - organisation: { - id: string - title: string - summary: string - description: string - oin: string - tooi: string - rsin: string - pki: string - } + organisation: string + metadata: string[] } diff --git a/src/entities/publication/publication.mock.ts b/src/entities/publication/publication.mock.ts index ad88bb11..2c819d4f 100644 --- a/src/entities/publication/publication.mock.ts +++ b/src/entities/publication/publication.mock.ts @@ -17,7 +17,12 @@ export const mockPublicationsData = (): TPublication[] => [ modified: '2024-01-02', featured: true, data: { - type: '', + key: 'anyvalue', + streetNumber: 1, + object: { + blabla: 'bla' + }, + array: ['appel', 'peer', 0, [], {}] }, attachments: [], attachmentCount: 1, diff --git a/src/entities/publication/publication.ts b/src/entities/publication/publication.ts index 2165a65d..86d06763 100644 --- a/src/entities/publication/publication.ts +++ b/src/entities/publication/publication.ts @@ -18,9 +18,7 @@ export class Publication implements TPublication { public attachments: TAttachment[] public attachmentCount: number public themes: string[] - public data: { - type: string - } + public data: Record public anonymization: { anonymized: boolean @@ -69,11 +67,9 @@ export class Publication implements TPublication { this.schema = data.schema || '' this.status = data.status || 'Concept' this.attachments = data.attachments || [] - this.attachmentCount = this.attachmentCount || data.attachments.length || 0 + this.attachmentCount = this.attachmentCount || data.attachments?.length || 0 this.themes = data.themes || [] - this.data = data.data || { - type: '', - } + this.data = (!Array.isArray(data.data) && data.data) || {} this.anonymization = data.anonymization || { anonymized: false, @@ -121,9 +117,7 @@ export class Publication implements TPublication { attachments: z.object({}).array(), attachmentCount: z.number(), themes: z.string().array(), - data: z.object({ - type: z.string(), - }), + data: z.record(z.string(), z.any()), anonymization: z.object({ anonymized: z.boolean(), results: z.string().max(2500), diff --git a/src/entities/publication/publication.types.ts b/src/entities/publication/publication.types.ts index b8ae4a29..cd3174ac 100644 --- a/src/entities/publication/publication.types.ts +++ b/src/entities/publication/publication.types.ts @@ -17,9 +17,7 @@ export type TPublication = { attachments: TAttachment[] attachmentCount: number themes: string[] - data: { - type: string - } + data: Record anonymization: { anonymized: boolean results: string diff --git a/src/modals/Modals.vue b/src/modals/Modals.vue index 1e1eb9e7..224a7064 100644 --- a/src/modals/Modals.vue +++ b/src/modals/Modals.vue @@ -11,6 +11,7 @@ + @@ -37,6 +38,7 @@ import EditMetaDataPropertyModal from './metaData/EditMetaDataPropertyModal.vue' import AddCatalogModal from './catalog/AddCatalogModal.vue' import EditCatalogModal from './catalog/EditCatalogModal.vue' +import AddCatalogiMetadata from './catalogiMetadata/AddCatalogiMetadata.vue' import AddDirectoryModal from './directory/AddDirectoryModal.vue' import EditListingModal from './directory/EditListingModal.vue' import AddOrganisationModal from './organisation/AddOrganisationModal.vue' @@ -59,6 +61,7 @@ export default { EditMetaDataPropertyModal, AddCatalogModal, EditCatalogModal, + AddCatalogiMetadata, AddDirectoryModal, EditListingModal, AddPublicationDataModal, diff --git a/src/modals/attachment/AddAttachmentModal.vue b/src/modals/attachment/AddAttachmentModal.vue index bd339333..eff8523c 100644 --- a/src/modals/attachment/AddAttachmentModal.vue +++ b/src/modals/attachment/AddAttachmentModal.vue @@ -35,32 +35,60 @@ import { navigationStore, publicationStore } from '../../store/store.js' maxlength="255" :value.sync="publicationStore.attachmentItem.description" /> + :value.sync="publicationStore.attachmentItem.accessUrl" /> + :value.sync="publicationStore.attachmentItem.downloadUrl" /> +
+ + + Bestand toevoegen + + + + + {{ file.name }} + +
+ + + Toevoegen +
- - - Toevoegen - + + + + + + diff --git a/src/modals/directory/AddDirectoryModal.vue b/src/modals/directory/AddDirectoryModal.vue index 38271305..b4560bdd 100644 --- a/src/modals/directory/AddDirectoryModal.vue +++ b/src/modals/directory/AddDirectoryModal.vue @@ -23,14 +23,11 @@ import { navigationStore, directoryStore } from '../../store/store.js'
- -

Er is geen valide URL ingevoerd.

-
- +
@@ -134,7 +144,6 @@ export default { description: '', reference: '', license: '', - modified: new Date(), featured: false, portal: '', category: '', @@ -185,13 +194,21 @@ export default { }) .then((response) => { response.json().then((data) => { + const selectedCatalogus = navigationStore.getTransferData() !== 'ignore selectedCatalogus' + ? data.results.filter((catalogus) => catalogus.id.toString() === navigationStore.selectedCatalogus.toString())[0] + : null this.catalogi = { options: Object.entries(data.results).map((catalog) => ({ id: catalog[1].id, label: catalog[1].title, })), - + value: selectedCatalogus + ? { + id: selectedCatalogus.id, + label: selectedCatalogus.title, + } + : null, } }) this.catalogiLoading = false @@ -256,38 +273,42 @@ export default { publicationStore.refreshPublicationList() response.json().then((data) => { publicationStore.setPublicationItem(data) + navigationStore.setSelectedCatalogus(data?.catalogi) }) navigationStore.setSelected('publication') // Wait for the user to read the feedback then close the model const self = this setTimeout(function() { - self.success = null - navigationStore.setModal(false) - self.publication = { - title: '', - summary: '', - description: '', - reference: '', - license: '', - modified: new Date(), - featured: false, - portal: '', - category: '', - published: new Date(), - image: '', - data: {}, - } - self.catalogi = {} - self.metaData = {} - self.hasUpdated = false + self.closeModal() }, 2000) }) .catch((err) => { this.error = err this.loading = false - self.hasUpdated = false + this.hasUpdated = false }) }, + closeModal() { + this.success = null + navigationStore.selectedCatalogus && navigationStore.setSelected('publication') + navigationStore.setModal(false) + this.publication = { + title: '', + summary: '', + description: '', + reference: '', + license: '', + featured: false, + portal: '', + category: '', + published: new Date(), + image: '', + data: {}, + } + this.catalogi = {} + this.metaData = {} + this.hasUpdated = false + }, }, } @@ -324,4 +345,8 @@ export default { flex-direction: row; align-items: center; } + +.apm-submit-button { + margin-block-start: 1rem +} diff --git a/src/modals/publication/EditPublicationModal.vue b/src/modals/publication/EditPublicationModal.vue index 43a40a87..6f1674ba 100644 --- a/src/modals/publication/EditPublicationModal.vue +++ b/src/modals/publication/EditPublicationModal.vue @@ -41,17 +41,11 @@ import { navigationStore, publicationStore } from '../../store/store.js' label="Portaal" :value.sync="publicationItem.portal" /> -

Published

+

Publicatie datum

- -

Modified

- -
{ response.json().then((data) => { publicationStore.setPublicationItem(data) + this.publicationItem = publicationStore.publicationItem }) this.loading = false }) @@ -180,65 +171,6 @@ export default { this.loading = false }) }, - fetchCatalogi() { - this.catalogiLoading = true - fetch('/index.php/apps/opencatalogi/api/catalogi', { - method: 'GET', - }) - .then((response) => { - response.json().then((data) => { - - const selectedCatalogi = data.results.find((catalogi) => catalogi.id.toString() === this.publicationItem.catalogi.toString()) - - this.catalogi = { - inputLabel: 'Catalogi', - options: data.results.map((catalog) => ({ - id: catalog.id, - label: catalog.title, - })), - // FIXME: for some reason the NcSelect uses the id instead of the label when displaying - value: [{ - id: selectedCatalogi.id ?? '', - label: selectedCatalogi.title ?? '', - }], - } - }) - this.catalogiLoading = false - }) - .catch((err) => { - console.error(err) - this.catalogiLoading = false - }) - }, - fetchMetaData() { - this.metaDataLoading = true - fetch('/index.php/apps/opencatalogi/api/metadata', { - method: 'GET', - }) - .then((response) => { - response.json().then((data) => { - const selectedMetaData = data.results.find((metadata) => metadata.id.toString() === this.publicationItem.metaData.toString()) - - this.metaData = { - inputLabel: 'MetaData', - options: data.results.map((metaData) => ({ - id: metaData.id, - label: metaData.title, - })), - // FIXME: for some reason the NcSelect uses the id instead of the label when displaying - value: { - id: selectedMetaData.id, - label: selectedMetaData.title, - }, - } - }) - this.metaDataLoading = false - }) - .catch((err) => { - console.error(err) - this.metaDataLoading = false - }) - }, updatePublication() { this.loading = true fetch( diff --git a/src/modals/publicationData/AddPublicationDataModal.vue b/src/modals/publicationData/AddPublicationDataModal.vue index a35be621..627f367b 100644 --- a/src/modals/publicationData/AddPublicationDataModal.vue +++ b/src/modals/publicationData/AddPublicationDataModal.vue @@ -87,9 +87,9 @@ export default { }, methods: { AddPublicatieEigenschap() { - publicationStore.publicationItem.data[this.key] = this.value this.loading = true const bodyData = publicationStore.publicationItem + bodyData.data[this.key] = this.value delete bodyData.publicationDate fetch( `/index.php/apps/opencatalogi/api/publications/${publicationStore.publicationItem.id}`, diff --git a/src/navigation/Configuration.vue b/src/navigation/Configuration.vue index 59734691..5a80a561 100644 --- a/src/navigation/Configuration.vue +++ b/src/navigation/Configuration.vue @@ -1,6 +1,6 @@ Catalogus bekijken + + + Metadata toevoegen +