Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0e53fee3b | ||
|
|
9867875ef2 |
@@ -3,3 +3,4 @@ node_modules/
|
|||||||
/.env
|
/.env
|
||||||
.idea
|
.idea
|
||||||
express/public/stylesheets
|
express/public/stylesheets
|
||||||
|
/draft/
|
||||||
|
|||||||
@@ -1,3 +1,2 @@
|
|||||||
old_express
|
old_express
|
||||||
express/public/stylesheets
|
express/public/stylesheets
|
||||||
help-pug-demo.pug
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2023 Amebis, d.o.o.
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -1,5 +1,166 @@
|
|||||||
# Terminološki portal
|
# Terminology Portal
|
||||||
|
|
||||||
## Navodila za namesitev
|
## Quick Overview
|
||||||
|
|
||||||
Navodila so zaradi dinamičnih sprememb v integriranih zunanjih komponentah še v pripravi in bodo pripravljena, ko bo celota stabilnejša.
|
An opensource Terminology portal which anybody can set up (hopefully without much trouble).
|
||||||
|
|
||||||
|
The main instance is available at [https://terminoloski.slovenscina.eu](https://terminoloski.slovenscina.eu).
|
||||||
|
|
||||||
|
You can use your own instance completely independently or link it with other such instances, including the main one, and have them share dictionaries and their entries.
|
||||||
|
|
||||||
|
Likewise, the included consultancy module can use its built-in, fully featured tools for your consultants to process terminology questions (default)
|
||||||
|
or simply be linked with the one at [ISJFR, ZRC SAZU](https://isjfr.zrc-sazu.si) so that their professional consultants will answer the questions.
|
||||||
|
|
||||||
|
## Setting up your own instance of the portal
|
||||||
|
|
||||||
|
_The guide is written for Ubuntu 22.04 LTS. Adjust according to your OS._
|
||||||
|
|
||||||
|
### 1. Install external dependencies first
|
||||||
|
|
||||||
|
1. Install the [Docker Engine](https://docs.docker.com/engine/install/).
|
||||||
|
2. Install the [Term Candidate Extraction API](https://github.com/clarinsi/rsdo_luscilnik).
|
||||||
|
3. Make sure you have a SMTP server/service available, which the portal will require.
|
||||||
|
4. Install a reverse proxy (e.g. [Nginx](https://nginx.org/en/)).
|
||||||
|
|
||||||
|
### 2. Reconfigure the docker engine
|
||||||
|
|
||||||
|
By default, docker engine doesn't perform log rotation, which will eventually lead to disk exhaustion.
|
||||||
|
Search indexes will prevent all write operations at 90% full which will make many operations on the portal fail until
|
||||||
|
disk usage falls under 85% again.
|
||||||
|
|
||||||
|
Also, by default, image build process is not optimized.
|
||||||
|
|
||||||
|
To address both of the above, it is suggested to add the following to the _/etc/docker/daemon.json_ :
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"log-driver": "local",
|
||||||
|
"features": {
|
||||||
|
"buildkit": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Even better, set `"log-driver": "journald"` instead, so the logs get sent straight to your journald.
|
||||||
|
Alternatively, set your own preferred log aggregator as per [the official documentation](https://docs.docker.com/config/containers/logging/configure/).
|
||||||
|
|
||||||
|
**You need to restart the engine for the settings to take effect. Run:** `sudo systemctl restart docker.service`
|
||||||
|
|
||||||
|
### 3. Get and configure the portal
|
||||||
|
|
||||||
|
1. Using [git](https://git-scm.com/) transfer the portal code into desired directory with `git clone https://github.com/clarinsi/rsdo_term_portal.git <directory path>`
|
||||||
|
2. Navigate into that directory and copy the _.env_ file from the _dev_ folder one level up (into the root directory of the portal).
|
||||||
|
3. Open the copied _.env_ file in a text editor and adjust the parameters of your portal.
|
||||||
|
|
||||||
|
### 4. (Advanced/optional) Adjust data storage locations
|
||||||
|
|
||||||
|
By default, all data is stored and persisted inside Docker's [volumes](https://docs.docker.com/storage/volumes/)
|
||||||
|
on your host's root partition.
|
||||||
|
If you have a setup where for any reason you want to store data elsewhere,
|
||||||
|
you can reconfigure the docker-compose.prod.yml to use [bind mounts](https://docs.docker.com/storage/bind-mounts/),
|
||||||
|
just be mindful not to break shared volume dependencies.
|
||||||
|
|
||||||
|
_Using bind mounts has not been tested yet and might cause additional complications
|
||||||
|
regarding file system permissions._
|
||||||
|
|
||||||
|
### 5. Start the portal service cluster
|
||||||
|
|
||||||
|
1. Inside the portal directory, run the following command in CLI: `docker compose -f docker-compose.prod.yml --profile concordancer-manager up -d --build`
|
||||||
|
2. It's going to take some time for all the resources to get downloaded and built.
|
||||||
|
Wait for control to be returned to CLI before continuing.
|
||||||
|
|
||||||
|
### 6. First time concordancer setup
|
||||||
|
|
||||||
|
1. Open a bash session inside the _concordancer-manager_ container by running:
|
||||||
|
`docker exec -it $(docker ps --format "{{.Names}}" | grep concordancer-manager) bash`
|
||||||
|
2. In that session, run: `dotnet Rsdo.Concordancer.SystemManager.dll createMasterDb --connectionString="Host=postgres;Username=concordancer;Password=<POSTGRES_CONCORDANCER_PASSWORD>;Database=postgres"`.
|
||||||
|
Replace `<POSTGRES_CONCORDANCER_PASSWORD>` with the value you set for it in the _.env_ file.
|
||||||
|
3. After that, also in that session, run: `dotnet Rsdo.Concordancer.SystemManager.dll importSloleks --sourceFile=/sloleks/Sloleks2.0.LMF/sloleks_clarin_2.0.xml`.
|
||||||
|
This command takes a bit more time than the previous one to finish.
|
||||||
|
4. Close the session _(Ctrl + D)_
|
||||||
|
|
||||||
|
### 7. Reverse proxy setup
|
||||||
|
|
||||||
|
1. Forward the majority of trafic to the the main port,
|
||||||
|
set appropriate headers, needed for normal functioning of the webserver
|
||||||
|
and increase the maximum allowed request body size.
|
||||||
|
Example location block for Nginx config:
|
||||||
|
|
||||||
|
```
|
||||||
|
location / {
|
||||||
|
proxy_set_header X-Forwarded-Host $host:$server_port;
|
||||||
|
proxy_set_header X-Forwarded-Server $host;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|
||||||
|
client_max_body_size 1100M;
|
||||||
|
proxy_pass http://127.0.0.1:<EXPRESS_LISTEN_PORT>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace <EXPRESS_LISTEN_PORT> with whichever port you set as EXPRESS_LISTEN_PORT in the .env file,
|
||||||
|
or even change the IP address or protocol if you've done done some changes to docker-compose.prod.yml or other system wide configuration and generally know, what you're doing.
|
||||||
|
|
||||||
|
2. Forward concordancer client trafic to the _/korpus_ path
|
||||||
|
Example location block for Nginx config:
|
||||||
|
|
||||||
|
```
|
||||||
|
location /korpus/ {
|
||||||
|
proxy_pass http://127.0.0.1:<CONCORDANCER_LISTEN_PORT>/;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Same as above, except replace <CONCORDANCER_LISTEN_PORT> with the value of CONCORDANCER_LISTEN_PORT.
|
||||||
|
|
||||||
|
3. Don't forget setting up proper SSL termination, compression, ...
|
||||||
|
|
||||||
|
4. Also don't forget to make your changes take effect.
|
||||||
|
For Nginx, `sudo systemctl reload nginx.service` is usually the easiest way.
|
||||||
|
|
||||||
|
### 8. Done
|
||||||
|
|
||||||
|
Your portal should now be available at whichever address/domain your reverse proxy is set to listen to.
|
||||||
|
|
||||||
|
## Linking your instance with others
|
||||||
|
|
||||||
|
1. Using a web browser, navigate to your portal's main page.
|
||||||
|
2. In the upper right corner, change language to English.
|
||||||
|
(the instructions are written using english UI labels)
|
||||||
|
3. Log in as the portal admin user.
|
||||||
|
4. Navigate Menu -> Administration -> Links -> Add link.
|
||||||
|
5. Fill in all the fields:
|
||||||
|
|
||||||
|
- **Portal name** - your choice; it'll be displayed as the source of found entries, when the users use the portal's search tool.
|
||||||
|
|
||||||
|
- **Portal label** - a 2 letter code of your choice; same as portal name, except a shorter version.
|
||||||
|
|
||||||
|
- **URL (dictionaries sync)** - Format: <target_portal_origin>/api/v1/system/inter-instance-sync/dictionaries
|
||||||
|
|
||||||
|
Replace <target_portal_origin> with the origin of the portal, the dictionaries of which you wish to add to your portal.
|
||||||
|
|
||||||
|
To link the main instance of the portal, for example, you would enter:
|
||||||
|
https://terminoloski.slovenscina.eu/api/v1/system/inter-instance-sync/dictionaries
|
||||||
|
|
||||||
|
- **URL (dictionary entries sync)** - Format: <target_portal_origin>/api/v1/system/inter-instance-sync/dictionary/$SOURCE_ID/entries?lastSynced=$SINCE
|
||||||
|
|
||||||
|
<target_portal_origin> should be the same as for previous URL.
|
||||||
|
|
||||||
|
To link the main instance of the portal, for example, you would enter:
|
||||||
|
https://terminoloski.slovenscina.eu/api/v1/system/inter-instance-sync/dictionary/$SOURCE_ID/entries?lastSynced=$SINCE
|
||||||
|
|
||||||
|
6. Click _Add link_. You will be returned to the list of linked portals, where the newly created portal will now be listed.
|
||||||
|
7. Click _Dictionaries_ to see the list of all available dictionaries of the target portal. Enable the ones you wish to be added to your portal, click _Save_.
|
||||||
|
8. The selected dictionaries and all their entries will be automatically synchronized each night.
|
||||||
|
|
||||||
|
## Linking to ZRC SAZU Terminological counselling
|
||||||
|
|
||||||
|
If you wish professional counselors to answer your users' terminological questions,
|
||||||
|
you can do so by simply toggling a switch in the administration console.
|
||||||
|
|
||||||
|
1. As previously described, navigate to the Menu -> Administration -> Basic settings -> Consultancy.
|
||||||
|
2. Select _ZRC SAZU Terminological counselling_.
|
||||||
|
3. Click Save.
|
||||||
|
|
||||||
|
That's it.
|
||||||
|
Your questions will now be automatically forwarded to ZRC SAZU Terminological counselling
|
||||||
|
and their answers back to your portal.
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ FROM alpine:3.16 AS builder
|
|||||||
WORKDIR /sloleks
|
WORKDIR /sloleks
|
||||||
RUN wget -qO- "https://www.clarin.si/repository/xmlui/bitstream/handle/11356/1230/Sloleks2.0.LMF.zip?sequence=3&isAllowed=y" | unzip -
|
RUN wget -qO- "https://www.clarin.si/repository/xmlui/bitstream/handle/11356/1230/Sloleks2.0.LMF.zip?sequence=3&isAllowed=y" | unzip -
|
||||||
|
|
||||||
FROM ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.0
|
FROM ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.4
|
||||||
COPY --from=builder /sloleks /sloleks
|
COPY --from=builder /sloleks /sloleks
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
FROM alpine:3.16 AS builder
|
||||||
|
WORKDIR /sloleks
|
||||||
|
RUN wget -qO- "https://www.clarin.si/repository/xmlui/bitstream/handle/11356/1230/Sloleks2.0.LMF.zip?sequence=3&isAllowed=y" | unzip -
|
||||||
|
|
||||||
|
FROM ghcr.io/clarinsi/rsdo-concordancer-api-term-portal:v1.0.4
|
||||||
|
COPY --from=builder /sloleks /sloleks
|
||||||
@@ -1,26 +1,66 @@
|
|||||||
# Initial values are default vaules.
|
# PORTAL PARAMETERS
|
||||||
# Change if needed.
|
|
||||||
|
|
||||||
|
# Initial values are default vaules.
|
||||||
|
# Changing at least all passwords and secrets is strongly advised.
|
||||||
|
# Preferably at least 16 chars longs and randomly generated.
|
||||||
|
|
||||||
|
|
||||||
|
# Initial values for the admin user (portal administrator role),
|
||||||
|
# that will be created on first startup.
|
||||||
|
PORTAL_ADMIN_INITIAL_EMAIL="admin@email.com"
|
||||||
|
PORTAL_ADMIN_INITIAL_PASSWORD="admin"
|
||||||
|
|
||||||
|
|
||||||
|
# The endpoint where you've made Term Candidate Extraction API available.
|
||||||
|
# It is required for the extraction module to work properly.
|
||||||
|
EXTRACTION_API_ORIGIN="http://rsdo.lhrs.feri.um.si:8080"
|
||||||
|
|
||||||
|
|
||||||
|
# The main port, which will be exposed on localhost of the of host running the Docker Engine.
|
||||||
|
# If you need to expose it on all of host's network interfaces,
|
||||||
|
# modify docker-compose.prod.yml in project's root directory where it is used.
|
||||||
EXPRESS_LISTEN_PORT=3000
|
EXPRESS_LISTEN_PORT=3000
|
||||||
|
# Set it to true for production and properly configure a reverse proxy in front of it.
|
||||||
EXPRESS_IS_BEHIND_PROXY=false
|
EXPRESS_IS_BEHIND_PROXY=false
|
||||||
|
# It is used for cookie signing.
|
||||||
EXPRESS_SECRET="weak_secret"
|
EXPRESS_SECRET="weak_secret"
|
||||||
|
|
||||||
|
|
||||||
|
# Used for administration.
|
||||||
POSTGRES_ADMIN_PASSWORD="weak_admin_password" # user: postgres
|
POSTGRES_ADMIN_PASSWORD="weak_admin_password" # user: postgres
|
||||||
|
# Used by the express webserver service.
|
||||||
POSTGRES_EXPRESS_PASSWORD="weak_express_password" # user: express
|
POSTGRES_EXPRESS_PASSWORD="weak_express_password" # user: express
|
||||||
|
# Used by the concordancer service.
|
||||||
POSTGRES_CONCORDANCER_PASSWORD="weak_concordancer_password" # user: concordancer
|
POSTGRES_CONCORDANCER_PASSWORD="weak_concordancer_password" # user: concordancer
|
||||||
|
# Exposed on host's localhost for administration purposes.
|
||||||
POSTGRES_LISTEN_PORT=5432
|
POSTGRES_LISTEN_PORT=5432
|
||||||
|
|
||||||
|
|
||||||
|
# Configuration for webserver to communicate with your SMTP provider.
|
||||||
SMTP_HOST="maildev"
|
SMTP_HOST="maildev"
|
||||||
SMTP_PORT=1025
|
SMTP_PORT=1025
|
||||||
SMTP_TLS_REJECT_UNAUTHORIZED=true
|
SMTP_USER=""
|
||||||
|
SMTP_PASSWORD=""
|
||||||
|
SMTP_SECURE=false # Most often only "true" for port 465.
|
||||||
|
SMTP_REQUIRE_TLS=false # If "true" and SMTP_SECURE is "false", only send messages if server supports STARTTLS.
|
||||||
|
SMTP_TLS_ALLOW_INVALID_CERTS=false
|
||||||
SMTP_FROM="Sender name <sender@server.com>"
|
SMTP_FROM="Sender name <sender@server.com>"
|
||||||
|
|
||||||
|
|
||||||
|
# Exposed on host's localhost for search index administration purposes.
|
||||||
|
# Used only if docker compose was ran with opensearch-dashboards profile.
|
||||||
OS_DASHBOARDS_LISTEN_PORT=3002
|
OS_DASHBOARDS_LISTEN_PORT=3002
|
||||||
|
|
||||||
|
|
||||||
|
# The second port, which will be exposed on localhost of the of host running the Docker Engine.
|
||||||
|
# Used by the concordancer client.
|
||||||
CONCORDANCER_LISTEN_PORT=3003
|
CONCORDANCER_LISTEN_PORT=3003
|
||||||
|
|
||||||
# Development only settings
|
|
||||||
MAILDEV_WEB_GUI_PORT=3001
|
|
||||||
|
|
||||||
# Production only settings
|
# Production only settings
|
||||||
|
# The origin, where the portal will be made available.
|
||||||
URL_ORIGIN="https://mywebportal.com"
|
URL_ORIGIN="https://mywebportal.com"
|
||||||
|
|
||||||
|
|
||||||
|
# Development only settings (not used in production)
|
||||||
|
MAILDEV_WEB_GUI_PORT=3001
|
||||||
|
|||||||
@@ -21,16 +21,23 @@ services:
|
|||||||
PGDATABASE: term_portal
|
PGDATABASE: term_portal
|
||||||
SMTP_HOST: "${SMTP_HOST:?}"
|
SMTP_HOST: "${SMTP_HOST:?}"
|
||||||
SMTP_PORT: "${SMTP_PORT:?}"
|
SMTP_PORT: "${SMTP_PORT:?}"
|
||||||
SMTP_TLS_REJECT_UNAUTHORIZED: "${SMTP_TLS_REJECT_UNAUTHORIZED:?}"
|
SMTP_USER: "${SMTP_USER?}"
|
||||||
|
SMTP_PASSWORD: "${SMTP_PASSWORD?}"
|
||||||
|
SMTP_SECURE: "${SMTP_SECURE:?}"
|
||||||
|
SMTP_REQUIRE_TLS: "${SMTP_REQUIRE_TLS:?}"
|
||||||
|
SMTP_TLS_ALLOW_INVALID_CERTS: "${SMTP_TLS_ALLOW_INVALID_CERTS:?}"
|
||||||
SMTP_FROM: "${SMTP_FROM:?}"
|
SMTP_FROM: "${SMTP_FROM:?}"
|
||||||
ORIGIN: "${URL_ORIGIN:?}"
|
ORIGIN: "${URL_ORIGIN:?}"
|
||||||
|
PORTAL_ADMIN_INITIAL_EMAIL: "${PORTAL_ADMIN_INITIAL_EMAIL:?}"
|
||||||
|
PORTAL_ADMIN_INITIAL_PASSWORD: "${PORTAL_ADMIN_INITIAL_PASSWORD:?}"
|
||||||
|
EXTRACTION_API_ORIGIN: "${EXTRACTION_API_ORIGIN:?}"
|
||||||
volumes:
|
volumes:
|
||||||
- express-data:/usr/src/app/data_files
|
- express-data:/usr/src/app/data_files
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${EXPRESS_LISTEN_PORT:?}:3000"
|
- "127.0.0.1:${EXPRESS_LISTEN_PORT:?}:3000"
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15-alpine
|
image: postgres:15.1-alpine3.17
|
||||||
restart: always
|
restart: always
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: "${POSTGRES_ADMIN_PASSWORD:?}"
|
POSTGRES_PASSWORD: "${POSTGRES_ADMIN_PASSWORD:?}"
|
||||||
@@ -58,18 +65,18 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- cluster.name=term-portal
|
- cluster.name=term-portal
|
||||||
- node.name=node-1
|
- node.name=node-1
|
||||||
# - bootstrap.memory_lock=true # along with the memlock settings below, disables swapping
|
- bootstrap.memory_lock=true # along with the memlock settings below, disables swapping
|
||||||
# - "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM
|
- "OPENSEARCH_JAVA_OPTS=-Xms512m -Xmx512m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM
|
||||||
- "DISABLE_INSTALL_DEMO_CONFIG=true" # disables execution of install_demo_configuration.sh bundled with security plugin, which installs demo certificates and security configurations to OpenSearch
|
- "DISABLE_INSTALL_DEMO_CONFIG=true" # disables execution of install_demo_configuration.sh bundled with security plugin, which installs demo certificates and security configurations to OpenSearch
|
||||||
- "DISABLE_SECURITY_PLUGIN=true" # disables security plugin entirely in OpenSearch by setting plugins.security.disabled: true in opensearch.yml
|
- "DISABLE_SECURITY_PLUGIN=true" # disables security plugin entirely in OpenSearch by setting plugins.security.disabled: true in opensearch.yml
|
||||||
- "discovery.type=single-node" # disables bootstrap checks that are enabled when network.host is set to a non-loopback address
|
- "discovery.type=single-node" # disables bootstrap checks that are enabled when network.host is set to a non-loopback address
|
||||||
# ulimits:
|
ulimits:
|
||||||
# memlock:
|
memlock:
|
||||||
# soft: -1
|
soft: -1
|
||||||
# hard: -1
|
hard: -1
|
||||||
# nofile:
|
nofile:
|
||||||
# soft: 65536 # maximum number of open files for the OpenSearch user, set to at least 65536 on modern systems
|
soft: 65536 # maximum number of open files for the OpenSearch user, set to at least 65536 on modern systems
|
||||||
# hard: 65536
|
hard: 65536
|
||||||
volumes:
|
volumes:
|
||||||
- opensearch-data:/usr/share/opensearch/data
|
- opensearch-data:/usr/share/opensearch/data
|
||||||
|
|
||||||
@@ -88,8 +95,10 @@ services:
|
|||||||
- "127.0.0.1:${OS_DASHBOARDS_LISTEN_PORT:?}:5601"
|
- "127.0.0.1:${OS_DASHBOARDS_LISTEN_PORT:?}:5601"
|
||||||
|
|
||||||
concordancer:
|
concordancer:
|
||||||
# image: ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.0
|
# image: ghcr.io/clarinsi/rsdo-concordancer-api-term-portal:v1.0.4
|
||||||
build: concordancer
|
build:
|
||||||
|
context: concordancer
|
||||||
|
dockerfile: Dockerfile.prod
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgres
|
- postgres
|
||||||
- opensearch
|
- opensearch
|
||||||
@@ -106,7 +115,7 @@ services:
|
|||||||
|
|
||||||
# Disabled by default. Enable if needed.
|
# Disabled by default. Enable if needed.
|
||||||
concordancer-manager:
|
concordancer-manager:
|
||||||
image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.0
|
image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.4
|
||||||
profiles:
|
profiles:
|
||||||
- concordancer-manager
|
- concordancer-manager
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -137,7 +146,11 @@ services:
|
|||||||
PGDATABASE: term_portal
|
PGDATABASE: term_portal
|
||||||
SMTP_HOST: "${SMTP_HOST:?}"
|
SMTP_HOST: "${SMTP_HOST:?}"
|
||||||
SMTP_PORT: "${SMTP_PORT:?}"
|
SMTP_PORT: "${SMTP_PORT:?}"
|
||||||
SMTP_TLS_REJECT_UNAUTHORIZED: "${SMTP_TLS_REJECT_UNAUTHORIZED:?}"
|
SMTP_USER: "${SMTP_USER?}"
|
||||||
|
SMTP_PASSWORD: "${SMTP_PASSWORD?}"
|
||||||
|
SMTP_SECURE: "${SMTP_SECURE:?}"
|
||||||
|
SMTP_REQUIRE_TLS: "${SMTP_REQUIRE_TLS:?}"
|
||||||
|
SMTP_TLS_ALLOW_INVALID_CERTS: "${SMTP_TLS_ALLOW_INVALID_CERTS:?}"
|
||||||
SMTP_FROM: "${SMTP_FROM:?}"
|
SMTP_FROM: "${SMTP_FROM:?}"
|
||||||
entrypoint: ["scheduled/entrypoint.sh"]
|
entrypoint: ["scheduled/entrypoint.sh"]
|
||||||
command: ["crond", "-f", "-l", "2"]
|
command: ["crond", "-f", "-l", "2"]
|
||||||
|
|||||||
@@ -20,9 +20,16 @@ services:
|
|||||||
PGDATABASE: term_portal
|
PGDATABASE: term_portal
|
||||||
SMTP_HOST: "${SMTP_HOST:?}"
|
SMTP_HOST: "${SMTP_HOST:?}"
|
||||||
SMTP_PORT: "${SMTP_PORT:?}"
|
SMTP_PORT: "${SMTP_PORT:?}"
|
||||||
SMTP_TLS_REJECT_UNAUTHORIZED: "${SMTP_TLS_REJECT_UNAUTHORIZED:?}"
|
SMTP_USER: "${SMTP_USER?}"
|
||||||
|
SMTP_PASSWORD: "${SMTP_PASSWORD?}"
|
||||||
|
SMTP_SECURE: "${SMTP_SECURE:?}"
|
||||||
|
SMTP_REQUIRE_TLS: "${SMTP_REQUIRE_TLS:?}"
|
||||||
|
SMTP_TLS_ALLOW_INVALID_CERTS: "${SMTP_TLS_ALLOW_INVALID_CERTS:?}"
|
||||||
SMTP_FROM: "${SMTP_FROM:?}"
|
SMTP_FROM: "${SMTP_FROM:?}"
|
||||||
ORIGIN: http://localhost:${EXPRESS_LISTEN_PORT:?}
|
ORIGIN: http://localhost:${EXPRESS_LISTEN_PORT:?}
|
||||||
|
PORTAL_ADMIN_INITIAL_EMAIL: "${PORTAL_ADMIN_INITIAL_EMAIL:?}"
|
||||||
|
PORTAL_ADMIN_INITIAL_PASSWORD: "${PORTAL_ADMIN_INITIAL_PASSWORD:?}"
|
||||||
|
EXTRACTION_API_ORIGIN: "${EXTRACTION_API_ORIGIN:?}"
|
||||||
volumes:
|
volumes:
|
||||||
# When developing/debugging concordancer, disable express bind mount and node_modules volume and enable express-data.
|
# When developing/debugging concordancer, disable express bind mount and node_modules volume and enable express-data.
|
||||||
# - express-data:/usr/src/app/data_files
|
# - express-data:/usr/src/app/data_files
|
||||||
@@ -35,7 +42,7 @@ services:
|
|||||||
tty: true
|
tty: true
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:15-alpine
|
image: postgres:15.1-alpine3.17
|
||||||
# Use any of the two custom Dockerfiles below for debugging PL/pgSQL functions.
|
# Use any of the two custom Dockerfiles below for debugging PL/pgSQL functions.
|
||||||
# Disable docker-entrypoint-initdb.d bind mount when doing so.
|
# Disable docker-entrypoint-initdb.d bind mount when doing so.
|
||||||
# build:
|
# build:
|
||||||
@@ -95,7 +102,7 @@ services:
|
|||||||
- "127.0.0.1:${OS_DASHBOARDS_LISTEN_PORT:?}:5601"
|
- "127.0.0.1:${OS_DASHBOARDS_LISTEN_PORT:?}:5601"
|
||||||
|
|
||||||
concordancer:
|
concordancer:
|
||||||
# image: ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.0
|
# image: ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.4
|
||||||
build: concordancer
|
build: concordancer
|
||||||
profiles:
|
profiles:
|
||||||
- develop-concordancer
|
- develop-concordancer
|
||||||
@@ -114,7 +121,7 @@ services:
|
|||||||
- sloleks:/sloleks
|
- sloleks:/sloleks
|
||||||
|
|
||||||
concordancer-manager:
|
concordancer-manager:
|
||||||
image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.0
|
image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.4
|
||||||
profiles:
|
profiles:
|
||||||
- develop-concordancer
|
- develop-concordancer
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -146,7 +153,11 @@ services:
|
|||||||
# PGDATABASE: term_portal
|
# PGDATABASE: term_portal
|
||||||
# SMTP_HOST: "${SMTP_HOST:?}"
|
# SMTP_HOST: "${SMTP_HOST:?}"
|
||||||
# SMTP_PORT: "${SMTP_PORT:?}"
|
# SMTP_PORT: "${SMTP_PORT:?}"
|
||||||
# SMTP_TLS_REJECT_UNAUTHORIZED: "${SMTP_TLS_REJECT_UNAUTHORIZED:?}"
|
# SMTP_USER: "${SMTP_USER?}"
|
||||||
|
# SMTP_PASSWORD: "${SMTP_PASSWORD?}"
|
||||||
|
# SMTP_SECURE: "${SMTP_SECURE:?}"
|
||||||
|
# SMTP_REQUIRE_TLS: "${SMTP_REQUIRE_TLS:?}"
|
||||||
|
# SMTP_TLS_ALLOW_INVALID_CERTS: "${SMTP_TLS_ALLOW_INVALID_CERTS:?}"
|
||||||
# SMTP_FROM: "${SMTP_FROM:?}"
|
# SMTP_FROM: "${SMTP_FROM:?}"
|
||||||
# volumes:
|
# volumes:
|
||||||
# - /usr/src/app/node_modules
|
# - /usr/src/app/node_modules
|
||||||
|
|||||||
@@ -6,16 +6,21 @@ const helmet = require('helmet')
|
|||||||
const favicon = require('serve-favicon')
|
const favicon = require('serve-favicon')
|
||||||
const cookieParser = require('cookie-parser')
|
const cookieParser = require('cookie-parser')
|
||||||
const createError = require('http-errors')
|
const createError = require('http-errors')
|
||||||
const debug = require('debug')('termPortal:app')
|
const flash = require('connect-flash-plus')
|
||||||
|
const i18next = require('i18next')
|
||||||
|
const i18nextMiddleware = require('i18next-http-middleware')
|
||||||
|
// const debug = require('debug')('termPortal:app')
|
||||||
|
|
||||||
// Import own modules.
|
// Import own modules.
|
||||||
const { isBehindProxy, secret } = require('./config/keys')
|
const { isBehindProxy, secret } = require('./config/keys')
|
||||||
const helmetConfig = require('./config/helmet')
|
const helmetConfig = require('./config/helmet')
|
||||||
const session = require('./middleware/session')
|
const session = require('./middleware/session')
|
||||||
|
const i18n = require('./middleware/i18n')
|
||||||
const passport = require('./middleware/auth')
|
const passport = require('./middleware/auth')
|
||||||
const user = require('./middleware/user')
|
const user = require('./middleware/user')
|
||||||
const settings = require('./middleware/settings')
|
const settings = require('./middleware/settings')
|
||||||
const { enhanceLocals } = require('./middleware')
|
const { enhanceLocals, adjustHeaders } = require('./middleware')
|
||||||
|
const { capitalize } = require('./utils')
|
||||||
|
|
||||||
// Import Routers.
|
// Import Routers.
|
||||||
const apiRouter = require('./routes/api')
|
const apiRouter = require('./routes/api')
|
||||||
@@ -37,22 +42,39 @@ app.locals.basedir = viewsPath
|
|||||||
// Other settings.
|
// Other settings.
|
||||||
const inDevEnv = app.get('env') === 'development'
|
const inDevEnv = app.get('env') === 'development'
|
||||||
if (isBehindProxy) app.set('trust proxy', 1) // Trust first proxy.
|
if (isBehindProxy) app.set('trust proxy', 1) // Trust first proxy.
|
||||||
|
app.locals.inDevEnv = inDevEnv
|
||||||
|
app.locals.capitalize = capitalize
|
||||||
|
|
||||||
// Mount middleware.
|
// Mount middleware.
|
||||||
app.use(logger('dev'))
|
app.use(logger('dev'))
|
||||||
app.use(helmet(helmetConfig))
|
app.use(helmet(helmetConfig))
|
||||||
app.use(favicon(path.join(__dirname, 'public', 'images', 'favicon.ico')))
|
app.use(favicon(path.join(__dirname, 'public', 'images', 'favicon.ico')))
|
||||||
app.use(express.static(path.join(__dirname, 'public')))
|
app.use(
|
||||||
|
express.static(path.join(__dirname, 'public'), {
|
||||||
|
maxAge: inDevEnv ? 0 : '1h'
|
||||||
|
})
|
||||||
|
)
|
||||||
app.use(express.json({ type: ['application/json', 'application/csp-report'] }))
|
app.use(express.json({ type: ['application/json', 'application/csp-report'] }))
|
||||||
app.use(express.urlencoded({ extended: true }))
|
app.use(express.urlencoded({ extended: true }))
|
||||||
app.use(cookieParser(secret))
|
app.use(cookieParser(secret))
|
||||||
app.use(session)
|
app.use(session)
|
||||||
|
app.use(flash())
|
||||||
app.use(passport.initialize())
|
app.use(passport.initialize())
|
||||||
app.use(passport.session())
|
app.use(passport.session())
|
||||||
app.use(passport.authenticate('remember-me'))
|
app.use(passport.authenticate('remember-me'))
|
||||||
|
app.use(i18n.determineRequestLanguage)
|
||||||
|
app.use(i18nextMiddleware.handle(i18next))
|
||||||
app.use(user.enhance)
|
app.use(user.enhance)
|
||||||
app.use(settings.prepareRequiredSettings)
|
app.use(settings.prepareRequiredSettings)
|
||||||
app.use(enhanceLocals)
|
app.use(enhanceLocals)
|
||||||
|
app.use(adjustHeaders)
|
||||||
|
|
||||||
|
if (inDevEnv) {
|
||||||
|
app.post(
|
||||||
|
'/locales/add/:lng/:ns',
|
||||||
|
i18nextMiddleware.missingKeyHandler(i18next)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Apparently express-debug can't be run inside another middleware and must be run in this file.
|
// Apparently express-debug can't be run inside another middleware and must be run in this file.
|
||||||
// To help you debug, temporarily uncomment the next line, but comment the helmet line due to strict CSP.
|
// To help you debug, temporarily uncomment the next line, but comment the helmet line due to strict CSP.
|
||||||
@@ -75,7 +97,8 @@ app.use((req, res, next) => next(createError(404)))
|
|||||||
|
|
||||||
// Error handler.
|
// Error handler.
|
||||||
app.use((err, req, res, next) => {
|
app.use((err, req, res, next) => {
|
||||||
if (err.status !== 404) debug(err)
|
// eslint-disable-next-line no-console
|
||||||
|
if (err.status !== 404) console.error(err)
|
||||||
|
|
||||||
// Set error info to be displayed to user depending on environment.
|
// Set error info to be displayed to user depending on environment.
|
||||||
let message, error
|
let message, error
|
||||||
@@ -86,8 +109,8 @@ app.use((err, req, res, next) => {
|
|||||||
} else {
|
} else {
|
||||||
message =
|
message =
|
||||||
err.status === 404
|
err.status === 404
|
||||||
? 'Stran ne obstaja'
|
? req.t('Stran ne obstaja')
|
||||||
: 'Prišlo je do strežniške napake. Poskusite kasneje.'
|
: req.t('Prišlo je do strežniške napake. Poskusite kasneje.')
|
||||||
error = {}
|
error = {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +122,7 @@ app.use((err, req, res, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render the error page.
|
// Render the error page.
|
||||||
res.render('error', { title: 'Napaka', message, error })
|
res.render('error', { title: req.t('Napaka'), message, error })
|
||||||
})
|
})
|
||||||
|
|
||||||
module.exports = app
|
module.exports = app
|
||||||
|
|||||||
@@ -4,12 +4,14 @@
|
|||||||
* Module dependencies.
|
* Module dependencies.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// Temporary noop function to be used until i18n is fully in place.
|
||||||
|
global.__ = str => str
|
||||||
|
|
||||||
const db = require('../models/db')
|
const db = require('../models/db')
|
||||||
const cache = require('../models/cache')
|
const cache = require('../models/cache')
|
||||||
const searchEngine = require('../models/search-engine')
|
const searchEngine = require('../models/search-engine')
|
||||||
const email = require('../models/email')
|
const email = require('../models/email')
|
||||||
const { seedDummyData } = require('../models/comment')
|
const init = require('../config/init')
|
||||||
const { initDemoData } = require('../models/demo-paginacija')
|
|
||||||
const app = require('../app')
|
const app = require('../app')
|
||||||
const debug = require('debug')('termPortal:server')
|
const debug = require('debug')('termPortal:server')
|
||||||
const http = require('http')
|
const http = require('http')
|
||||||
@@ -37,12 +39,12 @@ const server = http.createServer(app)
|
|||||||
db.waitForConnection(),
|
db.waitForConnection(),
|
||||||
cache.waitForConnection(),
|
cache.waitForConnection(),
|
||||||
searchEngine.waitForConnection(),
|
searchEngine.waitForConnection(),
|
||||||
email.waitForConnection()
|
email.waitForConnection(),
|
||||||
|
init.fsStructure()
|
||||||
])
|
])
|
||||||
await searchEngine.initEntryIndex()
|
await searchEngine.initEntryIndex()
|
||||||
await searchEngine.initConsultancyEntryIndex()
|
await searchEngine.initConsultancyEntryIndex()
|
||||||
seedDummyData()
|
await init.adminUser()
|
||||||
// initDemoData()
|
|
||||||
server.listen(port)
|
server.listen(port)
|
||||||
})()
|
})()
|
||||||
|
|
||||||
@@ -102,5 +104,7 @@ function onError(error) {
|
|||||||
function onListening() {
|
function onListening() {
|
||||||
const addr = server.address()
|
const addr = server.address()
|
||||||
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
|
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.log('Started listening')
|
||||||
debug('Listening on ' + bind)
|
debug('Listening on ' + bind)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
const { mkdir } = require('fs/promises')
|
||||||
|
const db = require('../models/db')
|
||||||
|
const User = require('../models/user')
|
||||||
|
const {
|
||||||
|
portalAdminInitialEmail,
|
||||||
|
portalAdminInitialPassword
|
||||||
|
} = require('../config/keys')
|
||||||
|
const { TEMP_EXPORT_PATH } = require('./settings')
|
||||||
|
const debug = require('debug')('termPortal:config/init')
|
||||||
|
|
||||||
|
exports.fsStructure = async () => {
|
||||||
|
await mkdir(TEMP_EXPORT_PATH, { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createPortalAdmin() {
|
||||||
|
const {
|
||||||
|
rows: [{ exists }]
|
||||||
|
} = await db.query(
|
||||||
|
"SELECT EXISTS (SELECT 1 FROM user_role WHERE role_name = 'portal admin')"
|
||||||
|
)
|
||||||
|
|
||||||
|
if (exists) return 'Skipping creation of portal admin (already exists)'
|
||||||
|
|
||||||
|
const ADMIN_BASE = 'admin'
|
||||||
|
const adminUser = {
|
||||||
|
username: ADMIN_BASE,
|
||||||
|
firstName: ADMIN_BASE,
|
||||||
|
lastName: ADMIN_BASE,
|
||||||
|
password: portalAdminInitialPassword,
|
||||||
|
email: portalAdminInitialEmail
|
||||||
|
}
|
||||||
|
const userId = await User.create(adminUser)
|
||||||
|
const assignAdminRole = db.query(
|
||||||
|
`INSERT INTO user_role (user_id, role_name)
|
||||||
|
VALUES
|
||||||
|
($1, 'portal admin'),
|
||||||
|
($1, 'dictionaries admin'),
|
||||||
|
($1, 'consultancy admin'),
|
||||||
|
($1, 'consultant')`,
|
||||||
|
[userId]
|
||||||
|
)
|
||||||
|
const activateAdminUser = db.query(
|
||||||
|
`UPDATE "user" SET status = 'active', time_activated = time_registered WHERE username = $1`,
|
||||||
|
[adminUser.username]
|
||||||
|
)
|
||||||
|
await Promise.all([assignAdminRole, activateAdminUser])
|
||||||
|
return `Successfully created portal admin (username: ${adminUser.username}, password: ${adminUser.password})`
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.adminUser = async () => {
|
||||||
|
// Generate portal admin user in empty DB.
|
||||||
|
// TODO Replace with a more robust solution for production.
|
||||||
|
try {
|
||||||
|
const message = await createPortalAdmin()
|
||||||
|
debug(message)
|
||||||
|
} catch (error) {
|
||||||
|
debug('Portal admin not seeded.')
|
||||||
|
debug(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,8 +4,14 @@ module.exports = {
|
|||||||
cookiesSecure: process.env.COOKIES_SECURE === 'true',
|
cookiesSecure: process.env.COOKIES_SECURE === 'true',
|
||||||
smtpHost: process.env.SMTP_HOST,
|
smtpHost: process.env.SMTP_HOST,
|
||||||
smtpPort: process.env.SMTP_PORT,
|
smtpPort: process.env.SMTP_PORT,
|
||||||
smtpTlsRejectUnauthorized:
|
smtpUser: process.env.SMTP_USER,
|
||||||
process.env.SMTP_TLS_REJECT_UNAUTHORIZED === 'true',
|
smtpPassword: process.env.SMTP_PASSWORD,
|
||||||
|
smtpSecure: process.env.SMTP_SECURE === 'true',
|
||||||
|
smtpRequireTls: process.env.SMTP_REQUIRE_TLS === 'true',
|
||||||
|
smtpAllowInvalidCerts: process.env.SMTP_TLS_ALLOW_INVALID_CERTS === 'true',
|
||||||
smtpFrom: process.env.SMTP_FROM,
|
smtpFrom: process.env.SMTP_FROM,
|
||||||
origin: process.env.ORIGIN
|
origin: process.env.ORIGIN,
|
||||||
|
portalAdminInitialEmail: process.env.PORTAL_ADMIN_INITIAL_EMAIL,
|
||||||
|
portalAdminInitialPassword: process.env.PORTAL_ADMIN_INITIAL_PASSWORD,
|
||||||
|
extractionApiOrigin: process.env.EXTRACTION_API_ORIGIN
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,15 @@ exports.DEFAULT_HITS_PER_PAGE = 10
|
|||||||
|
|
||||||
exports.EDITOR_MAX_HITS = 10000
|
exports.EDITOR_MAX_HITS = 10000
|
||||||
|
|
||||||
// If you change this one, don't forget to also update the volume mount in docker-compose.prod.yml.
|
// If you change this one, don't forget to also update the volume mount in docker-compose.prod.yml
|
||||||
exports.DATA_FILES_PATH = 'data_files'
|
// and nodemon ignore flag in package.json scripts.
|
||||||
|
const DATA_FILES_PATH = 'data_files'
|
||||||
|
exports.DATA_FILES_PATH = DATA_FILES_PATH
|
||||||
|
|
||||||
|
exports.TEMP_EXPORT_PATH = `${DATA_FILES_PATH}/export_temp`
|
||||||
|
|
||||||
exports.MAX_EXTRACTIONS_PER_USER = 5
|
exports.MAX_EXTRACTIONS_PER_USER = 5
|
||||||
|
|
||||||
|
exports.ACTIVATION_TOKEN_VALID_DAYS = 7
|
||||||
|
|
||||||
|
exports.CHANGE_EMAIL_TOKEN_VALID_DAYS = 7
|
||||||
|
|||||||
@@ -11,6 +11,25 @@ exports.listComments = async (req, res) => {
|
|||||||
filters.ctxId = null
|
filters.ctxId = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (filters.ctxType === 'entry_dict_int') {
|
||||||
|
const { dictionary_id: dictionaryId } = await Entry.fetch(filters.ctxId)
|
||||||
|
const isEditor = req.user.hasAnyDictionaryRole(dictionaryId)
|
||||||
|
const isPortalAdmin = req.user.hasRole('portal admin')
|
||||||
|
const isDictionariesAdmin = req.user.hasRole('dictionaries admin')
|
||||||
|
if (!(isEditor || isPortalAdmin || isDictionariesAdmin)) {
|
||||||
|
return res.status(400).end()
|
||||||
|
}
|
||||||
|
} else if (filters.ctxType === 'entry_consult_int') {
|
||||||
|
const isConsultantForEntry = req.user.isEditorOfConsultancyEntry(
|
||||||
|
filters.ctxId
|
||||||
|
)
|
||||||
|
const isPortalAdmin = req.user.hasRole('portal admin')
|
||||||
|
const isConsultancyAdmin = req.user.hasRole('consultancy admin')
|
||||||
|
if (!(isConsultantForEntry || isPortalAdmin || isConsultancyAdmin)) {
|
||||||
|
return res.status(400).end()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
pages_total: numberOfAllPages,
|
pages_total: numberOfAllPages,
|
||||||
comments,
|
comments,
|
||||||
@@ -38,20 +57,55 @@ exports.createComment = async (req, res) => {
|
|||||||
res.send({ comments, pagesTotal })
|
res.send({ comments, pagesTotal })
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.seedComments = async (req, res) => {
|
|
||||||
const { commentCount } = req.params
|
|
||||||
await Comment.seed(commentCount)
|
|
||||||
res.send(`${commentCount} new comments generated`)
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.clearComments = async (req, res) => {
|
|
||||||
await Comment.clear()
|
|
||||||
res.send('All comments cleared')
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.updateStatus = async (req, res) => {
|
exports.updateStatus = async (req, res) => {
|
||||||
const commentId = req.body.params.id
|
const commentId = req.body.params.id
|
||||||
const commentStatus = req.body.params.status
|
const commentStatus = req.body.params.status
|
||||||
|
|
||||||
|
const { ctxType, ctxId } = await Comment.fetchContextById(commentId)
|
||||||
|
|
||||||
|
let canUpdateStatus = false
|
||||||
|
switch (ctxType) {
|
||||||
|
case 'portal':
|
||||||
|
if (req.user.hasRole('portal admin')) canUpdateStatus = true
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'dictionary':
|
||||||
|
if (
|
||||||
|
req.user.hasRole('portal admin') ||
|
||||||
|
req.user.hasRole('dictionaries admin')
|
||||||
|
) {
|
||||||
|
canUpdateStatus = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'consultancy':
|
||||||
|
if (
|
||||||
|
req.user.hasRole('portal admin') ||
|
||||||
|
req.user.hasRole('consultancy admin')
|
||||||
|
) {
|
||||||
|
canUpdateStatus = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'entry_dict_ext': {
|
||||||
|
const { dictionary_id: dictionaryId } = await Entry.fetch(ctxId)
|
||||||
|
|
||||||
|
if (
|
||||||
|
req.user.hasRole('portal admin') ||
|
||||||
|
req.user.hasRole('dictionaries admin') ||
|
||||||
|
req.user.hasDictionaryRole(dictionaryId, 'administration')
|
||||||
|
) {
|
||||||
|
canUpdateStatus = true
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw Error('Invalid context type')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canUpdateStatus) return res.status(400).end()
|
||||||
|
|
||||||
await Comment.updateStatus(commentId, commentStatus)
|
await Comment.updateStatus(commentId, commentStatus)
|
||||||
res.send('Visibility changed')
|
res.send('Visibility changed')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,57 @@
|
|||||||
|
const user = require('../../../middleware/user')
|
||||||
const ConsultancyEntry = require('../../../models/consultancy-entry')
|
const ConsultancyEntry = require('../../../models/consultancy-entry')
|
||||||
|
const Domain = require('../../../models/domain')
|
||||||
const User = require('../../../models/user')
|
const User = require('../../../models/user')
|
||||||
const { promisify } = require('util')
|
const { promisify } = require('util')
|
||||||
|
const i18next = require('i18next')
|
||||||
const {
|
const {
|
||||||
deleteConsultancyEntryFromIndex
|
deleteConsultancyEntryFromIndex
|
||||||
} = require('../../../models/search-engine')
|
} = require('../../../models/search-engine')
|
||||||
const email = require('../../../models/email')
|
const email = require('../../../models/email')
|
||||||
const helper = require('../../../models/helpers')
|
const helper = require('../../../models/helpers')
|
||||||
|
const { getInstanceSetting } = require('../../../models/helpers')
|
||||||
|
const { searchConsultancyEntryIndex } = require('../../../models/search-engine')
|
||||||
|
const { prepareConsultancyEntries } = require('../../../models/helpers/search')
|
||||||
|
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
|
||||||
|
const generateQuery = require('../../../models/helpers/search/generate-query')
|
||||||
|
|
||||||
const consultancy = {}
|
const consultancy = {}
|
||||||
|
|
||||||
consultancy.listEntries = async (req, res) => {
|
consultancy.sendPaginationData = async (req, res) => {
|
||||||
const consultancyEntryList = await ConsultancyEntry.fetchAll()
|
let requestType = req.query.type
|
||||||
const data = {}
|
const isAdminPage = req.query.isAdmin === 'true'
|
||||||
data.consEntryList = consultancyEntryList
|
let page = +req.query.p || 1
|
||||||
res.send(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
consultancy.listNewEntries = async (req, res) => {
|
if (page < 1) {
|
||||||
const consultancyNewEntryList = await ConsultancyEntry.fetchAllNew()
|
page = 1
|
||||||
const data = {}
|
}
|
||||||
data.consultancyNewEntryList = consultancyNewEntryList
|
|
||||||
res.send(data)
|
if (isAdminPage) {
|
||||||
|
if (
|
||||||
|
req.user &&
|
||||||
|
(req.user.hasRole('portal admin') ||
|
||||||
|
req.user.hasRole('consultancy admin'))
|
||||||
|
) {
|
||||||
|
// Everything is allowed, nothing to do here
|
||||||
|
} else if (req.user && req.user.hasRole('consultant')) {
|
||||||
|
if (!(requestType === 'in progress' || requestType === 'published')) {
|
||||||
|
return res.status(400).send()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// for now, public can only see published entries
|
||||||
|
requestType = 'published'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await consultancyRequestItems(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
requestType || 'published',
|
||||||
|
'components/consultancy/api/consultancy-item-rendered',
|
||||||
|
isAdminPage,
|
||||||
|
!isAdminPage, // -> In current implementation, it is just the inverse of isAdminPage
|
||||||
|
page
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
consultancy.createQuestion = async (req, res) => {
|
consultancy.createQuestion = async (req, res) => {
|
||||||
@@ -35,7 +66,7 @@ consultancy.createQuestion = async (req, res) => {
|
|||||||
|
|
||||||
const { description } = q
|
const { description } = q
|
||||||
if (!description) {
|
if (!description) {
|
||||||
return res.status(400).send('description is a required parameter!')
|
return res.status(400).send('Description is a required parameter!')
|
||||||
}
|
}
|
||||||
consultancyEntry.description = description
|
consultancyEntry.description = description
|
||||||
|
|
||||||
@@ -52,24 +83,46 @@ consultancy.createQuestion = async (req, res) => {
|
|||||||
consultancyEntry[key] = helper.removeHtmlTags(consultancyEntry[key]).trim()
|
consultancyEntry[key] = helper.removeHtmlTags(consultancyEntry[key]).trim()
|
||||||
})
|
})
|
||||||
|
|
||||||
const questionId = await ConsultancyEntry.createQuestion(consultancyEntry)
|
const isOwnConsultancyEnabled =
|
||||||
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
|
(await getInstanceSetting('consultancy_type')) === 'own'
|
||||||
|
|
||||||
// TODO SEND EMAIL
|
let domainNameSl
|
||||||
// TODOOOOOOOOO
|
if (consultancyEntry.domainPrimaryIdInitial) {
|
||||||
|
domainNameSl = (
|
||||||
|
await Domain.fetchById(consultancyEntry.domainPrimaryIdInitial)
|
||||||
|
).nameSl
|
||||||
|
} else {
|
||||||
|
domainNameSl = ''
|
||||||
|
}
|
||||||
|
|
||||||
const emails = await ConsultancyEntry.fetchConsultancyAdminEmails()
|
let emails
|
||||||
|
let subjectText
|
||||||
|
if (isOwnConsultancyEnabled) {
|
||||||
|
const questionId = await ConsultancyEntry.createQuestion(consultancyEntry)
|
||||||
|
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
|
||||||
|
subjectText = req.t('Ustvarjeno novo vprašanje v svetovalnici')
|
||||||
|
emails = await ConsultancyEntry.fetchConsultancyAdminEmails()
|
||||||
|
} else {
|
||||||
|
subjectText = req.t('Novo vprašanje za Terminološko svetovalnico')
|
||||||
|
emails = await getInstanceSetting('zrc_email')
|
||||||
|
}
|
||||||
|
|
||||||
const renderAsync = promisify(req.app.render.bind(req.app))
|
const renderAsync = promisify(req.app.render.bind(req.app))
|
||||||
const emailHtml = await renderAsync('email/consultancy-creation-notify', {
|
const emailHtml = await renderAsync('email/consultancy-creation-notify', {
|
||||||
propertyToPassGoesHere: 'test1234'
|
nameAndSurname: `${res.locals.user.firstName} ${res.locals.user.lastName}`,
|
||||||
|
email: res.locals.user.email,
|
||||||
|
domain: domainNameSl,
|
||||||
|
institution: consultancyEntry.institution,
|
||||||
|
question: consultancyEntry.description,
|
||||||
|
existingSolutions: consultancyEntry.existingSolutions,
|
||||||
|
examplesOfUse: consultancyEntry.examplesOfUse
|
||||||
})
|
})
|
||||||
|
// TODO i18n - What language are the email title and content (we already have email translated)
|
||||||
await email.send({
|
await email.send({
|
||||||
to: emails,
|
to: emails,
|
||||||
subject: 'Ustvarjeno novo vprašanje v svetovalnici',
|
subject: subjectText,
|
||||||
html: emailHtml
|
html: emailHtml
|
||||||
})
|
})
|
||||||
/// /////////////////
|
|
||||||
|
|
||||||
res.status(201).send()
|
res.status(201).send()
|
||||||
}
|
}
|
||||||
@@ -179,9 +232,10 @@ consultancy.assign = async (req, res) => {
|
|||||||
const renderAsync = promisify(req.app.render.bind(req.app))
|
const renderAsync = promisify(req.app.render.bind(req.app))
|
||||||
const emailHtml = await renderAsync('email/consultancy-assigned')
|
const emailHtml = await renderAsync('email/consultancy-assigned')
|
||||||
|
|
||||||
|
// TODO i18n - What language are the email title and content (we already have email translated)
|
||||||
await email.send({
|
await email.send({
|
||||||
to: emails,
|
to: emails,
|
||||||
subject: 'Novo terminološko vprašanje',
|
subject: req.t('Novo terminološko vprašanje'),
|
||||||
html: emailHtml
|
html: emailHtml
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -234,9 +288,10 @@ consultancy.sendToReview = async (req, res) => {
|
|||||||
const renderAsync = promisify(req.app.render.bind(req.app))
|
const renderAsync = promisify(req.app.render.bind(req.app))
|
||||||
const emailHtml = await renderAsync('email/consultancy-item-review')
|
const emailHtml = await renderAsync('email/consultancy-item-review')
|
||||||
|
|
||||||
|
// TODO i18n - What language are the email title and content (we already have email translated)
|
||||||
await email.send({
|
await email.send({
|
||||||
to: emails,
|
to: emails,
|
||||||
subject: 'Potrditev objave',
|
subject: req.t('Potrditev objave'),
|
||||||
html: emailHtml
|
html: emailHtml
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -254,6 +309,22 @@ consultancy.publish = async (req, res) => {
|
|||||||
if (!entry.title) {
|
if (!entry.title) {
|
||||||
return res.status(400).send('Answer not completed')
|
return res.status(400).send('Answer not completed')
|
||||||
}
|
}
|
||||||
|
const author = await User.fetchUser(entry.authorId)
|
||||||
|
const portalName = await getInstanceSetting(`portal_name_${author.language}`)
|
||||||
|
const renderAsync = promisify(req.app.render.bind(req.app))
|
||||||
|
const emailHtml = await renderAsync(
|
||||||
|
`email/consultancy-publish-notify_${author.language}`,
|
||||||
|
{
|
||||||
|
portalName
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await email.send({
|
||||||
|
to: author.email,
|
||||||
|
subject: i18next.t('Objava terminološkega vprašanja', {
|
||||||
|
lng: author.language
|
||||||
|
}),
|
||||||
|
html: emailHtml
|
||||||
|
})
|
||||||
|
|
||||||
await ConsultancyEntry.publish(questionId, answerAuthors)
|
await ConsultancyEntry.publish(questionId, answerAuthors)
|
||||||
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
|
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
|
||||||
@@ -261,27 +332,38 @@ consultancy.publish = async (req, res) => {
|
|||||||
res.send()
|
res.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
consultancy.updateQuestion = async (req, res) => {
|
consultancy.updateQuestion = [
|
||||||
const { id, questionTitle, domain: domainId, question, answer } = req.body
|
(req, res, next) => {
|
||||||
|
const { id } = req.body
|
||||||
|
|
||||||
if (!id) return res.status(400).send({})
|
if (!id) return res.status(400).send({})
|
||||||
|
|
||||||
if (questionTitle === '' || question === '' || answer === '') {
|
req.entryId = id
|
||||||
return res.status(422).send('Polja naslov, vprašanje in mnenje so obvezna!')
|
next()
|
||||||
|
},
|
||||||
|
user.canConsultEntry,
|
||||||
|
async (req, res) => {
|
||||||
|
const { id, questionTitle, domain: domainId, question, answer } = req.body
|
||||||
|
|
||||||
|
if (questionTitle === '' || question === '' || answer === '') {
|
||||||
|
return res
|
||||||
|
.status(422)
|
||||||
|
.send(req.t('Polja naslov, vprašanje in mnenje so obvezna!'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const entry = await ConsultancyEntry.fetchById(id)
|
||||||
|
entry.domainPrimaryId = domainId > 0 ? domainId : null
|
||||||
|
entry.question = question
|
||||||
|
entry.answer = answer // helper.removeHtmlTags(answer).trim()
|
||||||
|
entry.title = questionTitle
|
||||||
|
|
||||||
|
// TODO Luka: Miha, update only fields that were updated.
|
||||||
|
await ConsultancyEntry.updateQuestion(entry)
|
||||||
|
await ConsultancyEntry.indexIntoSearchEngine(id, true)
|
||||||
|
|
||||||
|
res.send({})
|
||||||
}
|
}
|
||||||
|
]
|
||||||
const entry = await ConsultancyEntry.fetchById(id)
|
|
||||||
entry.domainPrimaryId = domainId > 0 ? domainId : null
|
|
||||||
entry.question = question
|
|
||||||
entry.answer = answer // helper.removeHtmlTags(answer).trim()
|
|
||||||
entry.title = questionTitle
|
|
||||||
|
|
||||||
// TODO Luka: Miha, update only fields that were updated.
|
|
||||||
await ConsultancyEntry.updateQuestion(entry)
|
|
||||||
await ConsultancyEntry.indexIntoSearchEngine(id, true)
|
|
||||||
|
|
||||||
res.send({})
|
|
||||||
}
|
|
||||||
|
|
||||||
consultancy.insertNonModerator = async (req, res) => {
|
consultancy.insertNonModerator = async (req, res) => {
|
||||||
const questionId = req.body.question_id
|
const questionId = req.body.question_id
|
||||||
@@ -318,4 +400,157 @@ consultancy.deleteQuestion = async (req, res) => {
|
|||||||
res.send()
|
res.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function consultancyRequestItems(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
type,
|
||||||
|
url,
|
||||||
|
isAdminPage = false,
|
||||||
|
privilegeToSeAll = true, // this method seperates consultancy main from admin, so all results get visible TO ALL REGISTERED USERS, not just admins
|
||||||
|
page
|
||||||
|
) {
|
||||||
|
const searchString = req.query.q?.trim() ?? ''
|
||||||
|
|
||||||
|
/// //////////////////////////////////////////
|
||||||
|
// let allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
|
||||||
|
|
||||||
|
/// filter selected domains for the prompt ///
|
||||||
|
// Not duplicates of this code arise...
|
||||||
|
// const pdList = intoDbArray(req.query.pd, 'always')
|
||||||
|
|
||||||
|
// allPrimaryDomains = allPrimaryDomains.map(entry => {
|
||||||
|
// if (pdList.includes(`${entry.id}`)) {
|
||||||
|
// entry.selected = true
|
||||||
|
// }
|
||||||
|
// return entry
|
||||||
|
// })
|
||||||
|
/// //////////////////////////////////////////
|
||||||
|
|
||||||
|
let assignedConsultant
|
||||||
|
|
||||||
|
if (
|
||||||
|
!privilegeToSeAll &&
|
||||||
|
req.user &&
|
||||||
|
!(req.user.hasRole('portal admin') || req.user.hasRole('consultancy admin'))
|
||||||
|
) {
|
||||||
|
assignedConsultant = req.user.id
|
||||||
|
} else {
|
||||||
|
assignedConsultant = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const filters = {
|
||||||
|
status: type,
|
||||||
|
assignedConsultant,
|
||||||
|
primaryDomain: req.query.pd
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isAdminPage) {
|
||||||
|
filters.assignedConsultant = undefined
|
||||||
|
filters.status = 'published' // guard
|
||||||
|
}
|
||||||
|
|
||||||
|
const hitsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
|
||||||
|
|
||||||
|
// const page = +req.query.p > 0 ? +req.query.p : 1
|
||||||
|
|
||||||
|
const hitsQuery = generateQuery.consultancy(
|
||||||
|
searchString,
|
||||||
|
filters,
|
||||||
|
hitsPerPage,
|
||||||
|
page
|
||||||
|
)
|
||||||
|
|
||||||
|
const hits = await searchConsultancyEntryIndex(hitsQuery)
|
||||||
|
|
||||||
|
const numberOfAllHits = hits.body.hits.total.value
|
||||||
|
const numberOfAllPages = Math.ceil(numberOfAllHits / hitsPerPage)
|
||||||
|
|
||||||
|
let entries = prepareConsultancyEntries(hits)
|
||||||
|
// console.log({ entries, numberOfAllHits, numberOfAllPages })
|
||||||
|
|
||||||
|
entries = entries.map(entry => {
|
||||||
|
entry.primaryDomain = entry.primaryDomain
|
||||||
|
? entry.primaryDomain.nameSl
|
||||||
|
: req.t('nedefinirano')
|
||||||
|
|
||||||
|
if (entry.assignedConsultants) {
|
||||||
|
entry.firstName = entry.assignedConsultants[0]?.firstName
|
||||||
|
entry.lastName = entry.assignedConsultants[0]?.lastName
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.assignedConsultants && entry.assignedConsultants.length > 1) {
|
||||||
|
entry.sharedAuthors = []
|
||||||
|
for (let i = 1; i < entry.assignedConsultants.length; i++) {
|
||||||
|
// skip first element (moderator)
|
||||||
|
entry.sharedAuthors.push(
|
||||||
|
`${entry.assignedConsultants[i].firstName} ${entry.assignedConsultants[i].lastName}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.timeCreated) {
|
||||||
|
// TODO i18n date format
|
||||||
|
const date = new Date(entry.timeCreated)
|
||||||
|
|
||||||
|
entry.formattedTimeCreated = `${date.getDate()}. ${
|
||||||
|
date.getMonth() + 1
|
||||||
|
}. ${date.getFullYear()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry
|
||||||
|
})
|
||||||
|
|
||||||
|
// const entryList = await mapEntryList(inProgressEntryList)
|
||||||
|
const userList = await User.fetchConsultants()
|
||||||
|
|
||||||
|
if (!isAdminPage) {
|
||||||
|
entries.map(entry => {
|
||||||
|
const MAX_CHARACTER_LENGTH = 200
|
||||||
|
let appendAnswer = ''
|
||||||
|
let appendQuestion = ''
|
||||||
|
if (entry.answer && entry.answer.length > MAX_CHARACTER_LENGTH) {
|
||||||
|
appendAnswer = '...'
|
||||||
|
}
|
||||||
|
if (entry.question && entry.question.length > MAX_CHARACTER_LENGTH) {
|
||||||
|
appendQuestion = '...'
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.answerSummary = `${entry.answer.slice(
|
||||||
|
0,
|
||||||
|
MAX_CHARACTER_LENGTH
|
||||||
|
)}${appendAnswer}`
|
||||||
|
|
||||||
|
entry.question = `${entry.question.slice(
|
||||||
|
0,
|
||||||
|
MAX_CHARACTER_LENGTH
|
||||||
|
)}${appendQuestion}`
|
||||||
|
|
||||||
|
return entry
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
res.append('page', page)
|
||||||
|
res.append('number-of-all-pages', numberOfAllPages)
|
||||||
|
res.render(url, {
|
||||||
|
entries, // entryList,
|
||||||
|
userList,
|
||||||
|
numberOfAllPages,
|
||||||
|
isAdminPage,
|
||||||
|
section: consultancyAdminPageMapper(type),
|
||||||
|
queryCount: numberOfAllHits
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// mapping required due to the inconsistent naming convention
|
||||||
|
function consultancyAdminPageMapper(type) {
|
||||||
|
if (type === 'in progress') {
|
||||||
|
return { inProgress: true }
|
||||||
|
}
|
||||||
|
if (type === 'review') {
|
||||||
|
return { prepared: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { type: true }
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = consultancy
|
module.exports = consultancy
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
const DemoPaginacija = require('../../../models/demo-paginacija')
|
|
||||||
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
|
|
||||||
|
|
||||||
exports.list = async (req, res) => {
|
|
||||||
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
|
|
||||||
|
|
||||||
const page = +req.query.p > 0 ? +req.query.p : 1
|
|
||||||
|
|
||||||
const { pages_total: numberOfAllPages, results } = await DemoPaginacija.fetch(
|
|
||||||
resultsPerPage,
|
|
||||||
page
|
|
||||||
)
|
|
||||||
|
|
||||||
res.send({ page, numberOfAllPages, results })
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
|
const { rm } = require('fs/promises')
|
||||||
|
const user = require('../../../middleware/user')
|
||||||
const Dictionary = require('../../../models/dictionary')
|
const Dictionary = require('../../../models/dictionary')
|
||||||
const Entry = require('../../../models/entry')
|
const Entry = require('../../../models/entry')
|
||||||
|
const Extraction = require('../../../models/extraction')
|
||||||
const {
|
const {
|
||||||
searchEntryIndex,
|
searchEntryIndex,
|
||||||
deleteEntryFromIndex,
|
deleteEntryFromIndex,
|
||||||
@@ -9,6 +12,7 @@ const genEditorAllQuery = require('../../../models/helpers/search/generate-query
|
|||||||
const { prepareEditorEntries } = require('../../../models/helpers/search')
|
const { prepareEditorEntries } = require('../../../models/helpers/search')
|
||||||
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
|
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
|
||||||
const { minEntriesRequirementCheckAndAct } = require('../../helpers/dictionary')
|
const { minEntriesRequirementCheckAndAct } = require('../../helpers/dictionary')
|
||||||
|
const { getExportFilesPath } = require('../../../models/helpers/dictionary')
|
||||||
|
|
||||||
const dictionary = {}
|
const dictionary = {}
|
||||||
|
|
||||||
@@ -66,7 +70,11 @@ dictionary.deleteEntry = async (req, res) => {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
Dictionary.updateMetadataAfterModifyingEntries(dictionaryId),
|
Dictionary.updateMetadataAfterModifyingEntries(dictionaryId),
|
||||||
deleteEntryFromIndex(entryId, true),
|
deleteEntryFromIndex(entryId, true),
|
||||||
minEntriesRequirementCheckAndAct.onDelete(dictionaryId, req.app)
|
minEntriesRequirementCheckAndAct.onDelete(
|
||||||
|
dictionaryId,
|
||||||
|
req.app,
|
||||||
|
req.determinedLanguage
|
||||||
|
)
|
||||||
])
|
])
|
||||||
|
|
||||||
res.end()
|
res.end()
|
||||||
@@ -81,7 +89,11 @@ dictionary.deleteAllEntries = async (req, res) => {
|
|||||||
await Promise.all([
|
await Promise.all([
|
||||||
Dictionary.updateMetadataAfterModifyingEntries(dictionaryId),
|
Dictionary.updateMetadataAfterModifyingEntries(dictionaryId),
|
||||||
deleteDictionaryEntriesFromIndex(dictionaryId),
|
deleteDictionaryEntriesFromIndex(dictionaryId),
|
||||||
minEntriesRequirementCheckAndAct.onDelete(dictionaryId, req.app)
|
minEntriesRequirementCheckAndAct.onDelete(
|
||||||
|
dictionaryId,
|
||||||
|
req.app,
|
||||||
|
req.determinedLanguage
|
||||||
|
)
|
||||||
])
|
])
|
||||||
|
|
||||||
res.end()
|
res.end()
|
||||||
@@ -115,16 +127,25 @@ dictionary.delete = async (req, res) => {
|
|||||||
const dictionaryId = +req.params.dictionaryId
|
const dictionaryId = +req.params.dictionaryId
|
||||||
await Dictionary.delete(dictionaryId)
|
await Dictionary.delete(dictionaryId)
|
||||||
await deleteDictionaryEntriesFromIndex(dictionaryId)
|
await deleteDictionaryEntriesFromIndex(dictionaryId)
|
||||||
|
const exportFilesPath = getExportFilesPath(dictionaryId)
|
||||||
|
await rm(exportFilesPath, { recursive: true, force: true })
|
||||||
|
|
||||||
res.end()
|
res.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
dictionary.updateDomainLabels = async (req, res) => {
|
dictionary.updateDomainLabels = [
|
||||||
const { dictionaryId, payload } = req.body.params
|
(req, res, next) => {
|
||||||
|
req.dictionaryId = req.body.params.dictionaryId
|
||||||
|
next()
|
||||||
|
},
|
||||||
|
user.canAdministrateDictionary,
|
||||||
|
async (req, res) => {
|
||||||
|
const { dictionaryId, payload } = req.body.params
|
||||||
|
|
||||||
await Dictionary.updateDomainLabel(dictionaryId, payload)
|
await Dictionary.updateDomainLabel(dictionaryId, payload)
|
||||||
res.end()
|
res.end()
|
||||||
}
|
}
|
||||||
|
]
|
||||||
|
|
||||||
dictionary.renovateSecondaryDomains = async (req, res) => {
|
dictionary.renovateSecondaryDomains = async (req, res) => {
|
||||||
const data = req.body.params.payload
|
const data = req.body.params.payload
|
||||||
@@ -147,7 +168,11 @@ dictionary.listDictionaries = async (req, res) => {
|
|||||||
const page = +req.query.p > 0 ? +req.query.p : 1
|
const page = +req.query.p > 0 ? +req.query.p : 1
|
||||||
|
|
||||||
const { pages_total: numberOfAllPages, results } =
|
const { pages_total: numberOfAllPages, results } =
|
||||||
await Dictionary.fetchAllAdminDictionaries(resultsPerPage, page)
|
await Dictionary.fetchAllAdminDictionaries(
|
||||||
|
req.determinedLanguage,
|
||||||
|
resultsPerPage,
|
||||||
|
page
|
||||||
|
)
|
||||||
|
|
||||||
res.send({ page, numberOfAllPages, results })
|
res.send({ page, numberOfAllPages, results })
|
||||||
}
|
}
|
||||||
@@ -167,23 +192,142 @@ dictionary.listDomainLabels = async (req, res) => {
|
|||||||
res.send({ page, numberOfAllPages, results })
|
res.send({ page, numberOfAllPages, results })
|
||||||
}
|
}
|
||||||
|
|
||||||
dictionary.listSecondaryDomains = async (req, res) => {
|
dictionary.listFilteredDomainLabels = async (req, res) => {
|
||||||
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
|
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
|
||||||
|
const { dictionaryId } = req.params
|
||||||
|
const page = +req.query.p > 0 ? +req.query.p : 1
|
||||||
|
|
||||||
|
let { q } = req.query
|
||||||
|
|
||||||
|
if (!q) {
|
||||||
|
q = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pages_total: numberOfAllPages, results } =
|
||||||
|
await Dictionary.fetchFilteredPaginationDomainLabels(
|
||||||
|
dictionaryId,
|
||||||
|
q,
|
||||||
|
resultsPerPage,
|
||||||
|
page
|
||||||
|
)
|
||||||
|
|
||||||
|
res.append('page', page)
|
||||||
|
res.append('number-of-all-pages', numberOfAllPages)
|
||||||
|
res.render('utilities/response-pug-wrapper/domainLabelLister', {
|
||||||
|
dictionary: { id: dictionaryId },
|
||||||
|
numberOfAllPages,
|
||||||
|
results
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
dictionary.listSecondaryDomainData = async (req, res) => {
|
||||||
|
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
|
||||||
|
let { q } = req.query
|
||||||
|
const page = +req.query.p > 0 ? +req.query.p : 1
|
||||||
|
|
||||||
|
if (!q) {
|
||||||
|
q = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pages_total: numberOfAllPages, results } =
|
||||||
|
await Dictionary.fetchFilteredSecondaryDomains(q, resultsPerPage, page)
|
||||||
|
|
||||||
|
res.append('page', page)
|
||||||
|
res.append('number-of-all-pages', numberOfAllPages)
|
||||||
|
|
||||||
|
res.render('utilities/response-pug-wrapper/secondaryDomainLister', {
|
||||||
|
page,
|
||||||
|
numberOfAllPages,
|
||||||
|
results
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
dictionary.showImportFromFileForm = async (req, res) => {
|
||||||
|
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
|
||||||
|
const { dictionaryId } = req.params
|
||||||
const page = +req.query.p > 0 ? +req.query.p : 1
|
const page = +req.query.p > 0 ? +req.query.p : 1
|
||||||
const { pages_total: numberOfAllPages, results } =
|
const { pages_total: numberOfAllPages, results } =
|
||||||
await Dictionary.fetchAllSecondaryDomains(resultsPerPage, page)
|
await Dictionary.fetchAllImports(dictionaryId, resultsPerPage, page)
|
||||||
|
|
||||||
res.send({ page, numberOfAllPages, results })
|
res.send({ page, numberOfAllPages, results })
|
||||||
}
|
}
|
||||||
|
|
||||||
dictionary.extractionImport = async (req, res) => {
|
dictionary.showExportToFileForm = async (req, res) => {
|
||||||
// TODO Import logic (Luka's task)
|
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
|
||||||
// const { id: dictionaryId, extractionId } = req.params
|
const { dictionaryId } = req.params
|
||||||
// const { from, to } = req.query
|
const page = +req.query.p > 0 ? +req.query.p : 1
|
||||||
// const fromIndex = +from > 1 ? Math.floor(from) - 1 : 0
|
const { pages_total: numberOfAllPages, results } =
|
||||||
// const toIndex = Number.isInteger(+to) ? Math.abs(to) : undefined
|
await Dictionary.fetchExports(dictionaryId, resultsPerPage, page)
|
||||||
// console.log({ dictionaryId, extractionId, fromIndex, toIndex })
|
|
||||||
res.send('IMPORTING')
|
res.send({ page, numberOfAllPages, results })
|
||||||
|
}
|
||||||
|
|
||||||
|
dictionary.importFromExtraction = async (req, res) => {
|
||||||
|
const { dictionaryId, extractionId } = req.params
|
||||||
|
const { from, to } = req.body
|
||||||
|
const fromIndex = +from > 1 ? Math.floor(from) - 1 : 0
|
||||||
|
const toIndex = Number.isInteger(+(to === '' ? undefined : to))
|
||||||
|
? Math.abs(to)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
// TODO Authentication, authorization, validation.
|
||||||
|
|
||||||
|
const termCandidatesToImport = await Extraction.fetchTermCandidatesSlice(
|
||||||
|
extractionId,
|
||||||
|
fromIndex,
|
||||||
|
toIndex
|
||||||
|
)
|
||||||
|
|
||||||
|
await Dictionary.importFromExtraction(
|
||||||
|
dictionaryId,
|
||||||
|
req.user.id,
|
||||||
|
termCandidatesToImport
|
||||||
|
)
|
||||||
|
|
||||||
|
await Dictionary.indexIntoSearchEngine(dictionaryId)
|
||||||
|
|
||||||
|
res.end()
|
||||||
|
}
|
||||||
|
|
||||||
|
dictionary.exportBegin = async (req, res) => {
|
||||||
|
const { dictionaryId } = req.params
|
||||||
|
const exportParams = {
|
||||||
|
isValidFilter:
|
||||||
|
req.body.isValidFilter === 'on' ? undefined : req.body.isValidFilter,
|
||||||
|
isPublishedFilter:
|
||||||
|
req.body.isPublishedFilter === 'on'
|
||||||
|
? undefined
|
||||||
|
: req.body.isPublishedFilter,
|
||||||
|
statusFilter:
|
||||||
|
req.body.statusFilter === 'complete'
|
||||||
|
? 'complete'
|
||||||
|
: req.body.statusFilter === 'inEdit'
|
||||||
|
? 'in_edit'
|
||||||
|
: undefined,
|
||||||
|
isTerminologyReviewedFilter:
|
||||||
|
req.body.isTerminologyReviewedFilter === 'on'
|
||||||
|
? undefined
|
||||||
|
: req.body.isTerminologyReviewedFilter,
|
||||||
|
isLanguageReviewedFilter:
|
||||||
|
req.body.isLanguageReviewedFilter === 'on'
|
||||||
|
? undefined
|
||||||
|
: req.body.isLanguageReviewedFilter,
|
||||||
|
exportFileFormat: req.body.exportFileFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportId = await Dictionary.beginExport(dictionaryId, exportParams)
|
||||||
|
|
||||||
|
res.end()
|
||||||
|
|
||||||
|
// Explicitcly catch any errors after the response has been sent
|
||||||
|
// since the the final error handler won't be able to send another.
|
||||||
|
try {
|
||||||
|
// TODO Consider delegating processing of export to a seperate process or at least a seperate thread (much like importing, extraction, ...).
|
||||||
|
await Dictionary.processExport(exportId)
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = dictionary
|
module.exports = dictionary
|
||||||
|
|||||||
@@ -1,22 +1,27 @@
|
|||||||
const { unlink, rm, mkdir } = require('fs/promises')
|
const { unlink, rm, mkdir, writeFile } = require('fs/promises')
|
||||||
const { promisify } = require('util')
|
const { promisify } = require('util')
|
||||||
const { URLSearchParams } = require('url')
|
const { URLSearchParams } = require('url')
|
||||||
const multer = require('multer')
|
const multer = require('multer')
|
||||||
|
const i18next = require('i18next')
|
||||||
const validator = require('validator')
|
const validator = require('validator')
|
||||||
const axios = require('axios')
|
const axios = require('axios')
|
||||||
const {
|
const {
|
||||||
getExtractionFilesPath,
|
getExtractionFilesPath,
|
||||||
getDocumentsPath,
|
getDocumentsPath,
|
||||||
getStopTermsPath,
|
getStopTermsPath,
|
||||||
getConllusPath
|
getConllusPath,
|
||||||
|
getFileStats
|
||||||
} = require('../../../models/helpers/extraction')
|
} = require('../../../models/helpers/extraction')
|
||||||
const { checkIfcanBegin } = require('../../helpers/extraction')
|
const { checkIfcanBegin } = require('../../helpers/extraction')
|
||||||
const Extraction = require('../../../models/extraction')
|
const Extraction = require('../../../models/extraction')
|
||||||
const Domain = require('../../../models/domain')
|
const Domain = require('../../../models/domain')
|
||||||
const email = require('../../../models/email')
|
const email = require('../../../models/email')
|
||||||
const { intoDbArray } = require('../../../models/helpers')
|
const { intoDbArray } = require('../../../models/helpers')
|
||||||
const { origin } = require('../../../config/keys')
|
const { origin, extractionApiOrigin } = require('../../../config/keys')
|
||||||
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
|
const {
|
||||||
|
DEFAULT_HITS_PER_PAGE,
|
||||||
|
TEMP_EXPORT_PATH
|
||||||
|
} = require('../../../config/settings')
|
||||||
|
|
||||||
const MAX_FILE_NAME_LENGTH = 100
|
const MAX_FILE_NAME_LENGTH = 100
|
||||||
const MAX_FILE_SIZE = 10 ** 9 // 1 GB
|
const MAX_FILE_SIZE = 10 ** 9 // 1 GB
|
||||||
@@ -77,16 +82,19 @@ extraction.docsList = async (req, res) => {
|
|||||||
extraction.docsUpdate = async (req, res) => {
|
extraction.docsUpdate = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await parseExtractionFileBody(req, res)
|
await parseExtractionFileBody(req, res)
|
||||||
|
const fileStats = await getFileStats(req.file.path)
|
||||||
|
res.send(fileStats)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (
|
if (
|
||||||
error instanceof multer.MulterError &&
|
error instanceof multer.MulterError &&
|
||||||
error.code === 'LIMIT_FILE_SIZE'
|
error.code === 'LIMIT_FILE_SIZE'
|
||||||
) {
|
) {
|
||||||
throw Error('File too large. Must not be over 1 GB.')
|
const customError = Error('File too large. Must not be over 1 GB.')
|
||||||
|
customError.displayInProd = true
|
||||||
|
throw customError
|
||||||
}
|
}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
res.end()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extraction.docDelete = async (req, res) => {
|
extraction.docDelete = async (req, res) => {
|
||||||
@@ -108,16 +116,19 @@ extraction.stopTermsList = async (req, res) => {
|
|||||||
extraction.stopTermsUpdate = async (req, res) => {
|
extraction.stopTermsUpdate = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
await parseExtractionFileBody(req, res)
|
await parseExtractionFileBody(req, res)
|
||||||
|
const fileStats = await getFileStats(req.file.path)
|
||||||
|
res.send(fileStats)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (
|
if (
|
||||||
error instanceof multer.MulterError &&
|
error instanceof multer.MulterError &&
|
||||||
error.code === 'LIMIT_FILE_SIZE'
|
error.code === 'LIMIT_FILE_SIZE'
|
||||||
) {
|
) {
|
||||||
throw Error('File too large. Must not be over 1 GB.')
|
const customError = Error('File too large. Must not be over 1 GB.')
|
||||||
|
customError.displayInProd = true
|
||||||
|
throw customError
|
||||||
}
|
}
|
||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
res.end()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extraction.stopTermDelete = async (req, res) => {
|
extraction.stopTermDelete = async (req, res) => {
|
||||||
@@ -141,7 +152,7 @@ extraction.ossSearch = [
|
|||||||
...(ossParams.keywords && { kljucneBesede: ossParams.keywords }),
|
...(ossParams.keywords && { kljucneBesede: ossParams.keywords }),
|
||||||
...(ossParams.domainUdk && { udk: ossParams.domainUdk })
|
...(ossParams.domainUdk && { udk: ossParams.domainUdk })
|
||||||
})
|
})
|
||||||
const searchApiUrl = `http://rsdo.lhrs.feri.um.si:8080/oss/steviloBesedilPoIskanju?${searchParams}`
|
const searchApiUrl = `${extractionApiOrigin}/oss/steviloBesedilPoIskanju?${searchParams}`
|
||||||
|
|
||||||
const { data: documentCount } = await axios.get(searchApiUrl)
|
const { data: documentCount } = await axios.get(searchApiUrl)
|
||||||
const canSave = documentCount && documentCount <= MAX_OSS_DOCUMENT_COUNT
|
const canSave = documentCount && documentCount <= MAX_OSS_DOCUMENT_COUNT
|
||||||
@@ -155,7 +166,7 @@ extraction.ossSearch = [
|
|||||||
|
|
||||||
extraction.ossConfirmParams = async (req, res) => {
|
extraction.ossConfirmParams = async (req, res) => {
|
||||||
const { id: extractionId } = req.params
|
const { id: extractionId } = req.params
|
||||||
const { ossParams } = await Extraction.fetch(extractionId)
|
const { ossParams } = req.extractionData
|
||||||
if (ossParams.status !== 'valid') throw Error('OSS params not valid')
|
if (ossParams.status !== 'valid') throw Error('OSS params not valid')
|
||||||
await Extraction.updateOssParams(extractionId, {
|
await Extraction.updateOssParams(extractionId, {
|
||||||
params: ossParams.params,
|
params: ossParams.params,
|
||||||
@@ -166,7 +177,7 @@ extraction.ossConfirmParams = async (req, res) => {
|
|||||||
|
|
||||||
extraction.begin = async (req, res) => {
|
extraction.begin = async (req, res) => {
|
||||||
const extractionId = req.params.id
|
const extractionId = req.params.id
|
||||||
const extraction = await Extraction.fetch(extractionId)
|
const extraction = req.extractionData
|
||||||
const canBegin = await checkIfcanBegin(extraction)
|
const canBegin = await checkIfcanBegin(extraction)
|
||||||
if (!canBegin) throw Error('Extraction does not qualify to be ran')
|
if (!canBegin) throw Error('Extraction does not qualify to be ran')
|
||||||
|
|
||||||
@@ -183,28 +194,39 @@ extraction.begin = async (req, res) => {
|
|||||||
|
|
||||||
res.send(timeStarted)
|
res.send(timeStarted)
|
||||||
|
|
||||||
if (ossParams) {
|
// Explicitcly catch any errors after the response has been sent
|
||||||
// TODO This next method is only a temporary solution.
|
// since the the final error handler won't be able to send another.
|
||||||
// TODO It should be called before response and its execution delegated to a seperate process or at least a seperate thread.
|
try {
|
||||||
await Extraction.processOss(extractionId, ossParams.params)
|
if (ossParams) {
|
||||||
} else {
|
// TODO This next method is only a temporary solution.
|
||||||
// TODO This next method is only a temporary solution.
|
// TODO It should be called before response and its execution delegated to a seperate process or at least a seperate thread.
|
||||||
// TODO It should be called before response and its execution delegated to a seperate process or at least a seperate thread.
|
await Extraction.processOss(extractionId, ossParams.params)
|
||||||
await Extraction.processOwn(extractionId, extractionName)
|
} else {
|
||||||
}
|
// TODO This next method is only a temporary solution.
|
||||||
|
// TODO It should be called before response and its execution delegated to a seperate process or at least a seperate thread.
|
||||||
|
await Extraction.processOwn(extractionId, extractionName)
|
||||||
|
}
|
||||||
|
|
||||||
const extractionLink = new URL('/luscenje', origin)
|
const extractionLink = new URL('/luscenje', origin)
|
||||||
const renderAsync = promisify(req.app.render.bind(req.app))
|
const renderAsync = promisify(req.app.render.bind(req.app))
|
||||||
const authorEmail = await Extraction.fetchAuthorEmail(extractionId)
|
const { email: authorEmail, language: authorLanguage } =
|
||||||
const emailHtml = await renderAsync('email/extraction-done', {
|
await Extraction.fetchAuthorData(extractionId)
|
||||||
extractionName,
|
const emailHtml = await renderAsync(
|
||||||
extractionLink
|
`email/extraction-done_${authorLanguage}`,
|
||||||
})
|
{
|
||||||
await email.send({
|
extractionName,
|
||||||
to: authorEmail,
|
extractionLink
|
||||||
subject: 'Luščenje končano',
|
}
|
||||||
html: emailHtml
|
)
|
||||||
})
|
await email.send({
|
||||||
|
to: authorEmail,
|
||||||
|
subject: i18next.t('Luščenje končano', { lng: authorLanguage }),
|
||||||
|
html: emailHtml
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extraction.duplicate = async (req, res) => {
|
extraction.duplicate = async (req, res) => {
|
||||||
@@ -214,18 +236,36 @@ extraction.duplicate = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extraction.termCandidatesExport = async (req, res) => {
|
extraction.termCandidatesExport = async (req, res) => {
|
||||||
// TODO CSV logic (Luka's task)
|
// TODO More formats (CSV, TSV, TXT, ...)
|
||||||
// const extractionId = req.params.id
|
|
||||||
// const { from, to } = req.query
|
|
||||||
// const fromIndex = +from > 1 ? Math.floor(from) - 1 : 0
|
|
||||||
// const toIndex = Number.isInteger(+to) ? Math.abs(to) : undefined
|
|
||||||
// console.log({ extractionId, fromIndex, toIndex })
|
|
||||||
res.download('public/images/help-amebis-logo-pug-demo.png')
|
|
||||||
}
|
|
||||||
|
|
||||||
extraction.listFinishedForUser = async (req, res) => {
|
const extractionId = req.params.id
|
||||||
const extractions = await Extraction.fetchFinishedForUser(req.user.id)
|
const { from, to } = req.query
|
||||||
res.send(extractions)
|
const fromIndex = +from > 1 ? Math.floor(from) - 1 : 0
|
||||||
|
const toIndex = Number.isInteger(+(to === '' ? undefined : to))
|
||||||
|
? Math.abs(to)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
// TODO Authentication, authorization, validation.
|
||||||
|
|
||||||
|
const termCandidatesToExport = await Extraction.fetchTermCandidatesSlice(
|
||||||
|
extractionId,
|
||||||
|
fromIndex,
|
||||||
|
toIndex
|
||||||
|
)
|
||||||
|
|
||||||
|
const exportFileName = `term_candidates_${extractionId}`
|
||||||
|
const exportFilePath = `${TEMP_EXPORT_PATH}/${exportFileName}`
|
||||||
|
await writeFile(
|
||||||
|
exportFilePath,
|
||||||
|
JSON.stringify({ terminoloski_kandidati: termCandidatesToExport })
|
||||||
|
)
|
||||||
|
|
||||||
|
const downloadAsync = promisify(res.download.bind(res))
|
||||||
|
try {
|
||||||
|
await downloadAsync(exportFilePath, 'term_candidates.json')
|
||||||
|
} finally {
|
||||||
|
await unlink(exportFilePath)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extraction.listTermCandidates = async (req, res) => {
|
extraction.listTermCandidates = async (req, res) => {
|
||||||
@@ -251,8 +291,11 @@ function extractionFileFilter(req, file, cb) {
|
|||||||
fileType = 'stopTerms'
|
fileType = 'stopTerms'
|
||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default: {
|
||||||
return cb(Error('Invalid API endpoint'))
|
const customError = Error('Invalid API endpoint')
|
||||||
|
customError.displayInProd = true
|
||||||
|
return cb(customError)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filenamePartsArray = file.originalname.split('.')
|
const filenamePartsArray = file.originalname.split('.')
|
||||||
@@ -264,30 +307,32 @@ function extractionFileFilter(req, file, cb) {
|
|||||||
(fileType === 'stopTerms' &&
|
(fileType === 'stopTerms' &&
|
||||||
fileExtension !== VALID_STOP_TERMS_FILE_EXTENSION)
|
fileExtension !== VALID_STOP_TERMS_FILE_EXTENSION)
|
||||||
) {
|
) {
|
||||||
return cb(Error('Invalid file type'))
|
const customError = Error('Invalid file type')
|
||||||
|
customError.displayInProd = true
|
||||||
|
return cb(customError)
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileName = filenamePartsArray.join('.')
|
const fileName = filenamePartsArray.join('.')
|
||||||
if (!fileName || fileName.length > MAX_FILE_NAME_LENGTH) {
|
if (!fileName || fileName.length > MAX_FILE_NAME_LENGTH) {
|
||||||
return cb(
|
const customError = Error(
|
||||||
Error(
|
`Filename must be between 1 and ${MAX_FILE_NAME_LENGTH} characters long.`
|
||||||
`Filename must be between 1 and ${MAX_FILE_NAME_LENGTH} characters long.`
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
customError.displayInProd = true
|
||||||
|
return cb(customError)
|
||||||
}
|
}
|
||||||
if (!validator.isAlphanumeric(fileName[0], 'sl-SI', { ignore: '_' })) {
|
if (!validator.isAlphanumeric(fileName[0], 'sl-SI', { ignore: '_' })) {
|
||||||
return cb(
|
const customError = Error(
|
||||||
Error(
|
'Filename must begin with an alphanumeric character or an underscore.'
|
||||||
'Filename must begin with an alphanumeric character or an underscore.'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
customError.displayInProd = true
|
||||||
|
return cb(customError)
|
||||||
}
|
}
|
||||||
if (!validator.isAlphanumeric(fileName, 'sl-SI', { ignore: ' _-.' })) {
|
if (!validator.isAlphanumeric(fileName, 'sl-SI', { ignore: ' _-.' })) {
|
||||||
return cb(
|
const customError = Error(
|
||||||
Error(
|
'Filename can only contain alphanumeric characters, spaces, underscores, minuses and periods.'
|
||||||
'Filename can only contain alphanumeric characters, spaces, underscores, minuses and periods.'
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
customError.displayInProd = true
|
||||||
|
return cb(customError)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.fileType = fileType
|
req.fileType = fileType
|
||||||
|
|||||||
@@ -66,6 +66,27 @@ exports.listMainEntries = async (req, res) => {
|
|||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// duplicated code below for filtering foreignent, optimize later
|
||||||
|
const categoriesLabels = Object.keys(entriesByCategory)
|
||||||
|
for (
|
||||||
|
let categoryIndex = 0;
|
||||||
|
categoryIndex < categoriesLabels.length;
|
||||||
|
categoryIndex++
|
||||||
|
) {
|
||||||
|
entriesByCategory[categoriesLabels[categoryIndex]] = entriesByCategory[
|
||||||
|
categoriesLabels[categoryIndex]
|
||||||
|
].map(entry => {
|
||||||
|
entry.foreignEntries = entry.foreignEntries?.filter(foreignEntry => {
|
||||||
|
if (filters.targetLanguages.length > 0) {
|
||||||
|
return filters.targetLanguages.includes(`${foreignEntry.lang.id}`)
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return entry
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// res.send({ page, numberOfAllPages, entries })
|
// res.send({ page, numberOfAllPages, entries })
|
||||||
res.append('page', page)
|
res.append('page', page)
|
||||||
res.append('number-of-all-pages', numberOfAllPages)
|
res.append('number-of-all-pages', numberOfAllPages)
|
||||||
@@ -157,7 +178,8 @@ exports.listFilteredDictionaries = async (req, res) => {
|
|||||||
hitsPerPage,
|
hitsPerPage,
|
||||||
page,
|
page,
|
||||||
orderAttribute,
|
orderAttribute,
|
||||||
orderIndex
|
orderIndex,
|
||||||
|
req.determinedLanguage
|
||||||
)
|
)
|
||||||
|
|
||||||
dictionaries = dictionaries.map(e => {
|
dictionaries = dictionaries.map(e => {
|
||||||
@@ -225,6 +247,9 @@ exports.showModalFilterResults = async (req, res) => {
|
|||||||
|
|
||||||
const aggregationRaw = await searchEntryIndex(aggregateQuery)
|
const aggregationRaw = await searchEntryIndex(aggregateQuery)
|
||||||
|
|
||||||
const aggregation = await prepareAggregation(aggregationRaw)
|
const aggregation = await prepareAggregation(
|
||||||
|
aggregationRaw,
|
||||||
|
req.determinedLanguage
|
||||||
|
)
|
||||||
res.send(prepareSeachFilterData(aggregation, filters))
|
res.send(prepareSeachFilterData(aggregation, filters))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
const debug = require('debug')('termPortal:controllers/api/v1/system')
|
|
||||||
const Eurotermbank = require('../../../models/system/eurotermbank')
|
const Eurotermbank = require('../../../models/system/eurotermbank')
|
||||||
|
// const debug = require('debug')('termPortal:controllers/api/v1/system')
|
||||||
|
|
||||||
exports.handleCspReports = (req, res) => {
|
exports.handleCspReports = (req, res) => {
|
||||||
debug(req.body)
|
// eslint-disable-next-line no-console
|
||||||
res.sendStatus(200)
|
console.error(req.body)
|
||||||
|
res.end()
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.syncWithEurotermbank = async (req, res) => {
|
exports.syncWithEurotermbank = async (req, res) => {
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
|
const { randomBytes } = require('crypto')
|
||||||
|
const { promisify } = require('util')
|
||||||
|
const RandomBytesAsync = promisify(randomBytes)
|
||||||
const User = require('../../../models/user')
|
const User = require('../../../models/user')
|
||||||
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
|
const email = require('../../../models/email')
|
||||||
|
const { logout: logoutUser } = require('../../../middleware/user')
|
||||||
|
const { origin } = require('../../../config/keys')
|
||||||
|
const {
|
||||||
|
DEFAULT_HITS_PER_PAGE,
|
||||||
|
CHANGE_EMAIL_TOKEN_VALID_DAYS
|
||||||
|
} = require('../../../config/settings')
|
||||||
|
|
||||||
const users = {}
|
const users = {}
|
||||||
users.listUsers = async (req, res) => {
|
users.listUsers = async (req, res) => {
|
||||||
@@ -23,13 +32,103 @@ users.updateHitsPerPage = async (req, res) => {
|
|||||||
res.status(200).send()
|
res.status(200).send()
|
||||||
}
|
}
|
||||||
|
|
||||||
users.updateFristNameAndSurname = async (req, res) => {
|
users.updateBasicData = async (req, res) => {
|
||||||
const firstname = req.body.name
|
const { firstName, lastName, email: newEmail } = req.body
|
||||||
const surname = req.body.surname
|
|
||||||
|
|
||||||
await User.updateFirstNameAndLastName(req.user.userName, firstname, surname)
|
// TODO Validation (valid email format, ...).
|
||||||
|
|
||||||
res.status(200).send()
|
const oldEmail = await User.updateFirstNameAndLastName(
|
||||||
|
req.user.id,
|
||||||
|
firstName,
|
||||||
|
lastName
|
||||||
|
)
|
||||||
|
|
||||||
|
if (newEmail === oldEmail) return res.send()
|
||||||
|
|
||||||
|
if (await User.isEmailAlreadyTaken(newEmail)) {
|
||||||
|
req.flash('info', req.t('Elektronski naslov uporablja že drug uporabnik.'))
|
||||||
|
return res.send()
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeEmailToken = (await RandomBytesAsync(32)).toString('hex')
|
||||||
|
await User.saveChangeEmailToken(req.user.id, changeEmailToken, newEmail)
|
||||||
|
let changeEmailLink = new URL('/sprememba-elektronskega-naslova', origin)
|
||||||
|
changeEmailLink.searchParams.set('token', changeEmailToken)
|
||||||
|
changeEmailLink = changeEmailLink.href
|
||||||
|
const renderAsync = promisify(req.app.render.bind(req.app))
|
||||||
|
const emailHtml = await renderAsync(
|
||||||
|
`email/user-change-email-token_${req.language}`,
|
||||||
|
{
|
||||||
|
username: req.user.userName,
|
||||||
|
changeEmailLink
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await email.send({
|
||||||
|
to: newEmail,
|
||||||
|
subject: req.t('Sprememba elektronskega naslova'),
|
||||||
|
html: emailHtml
|
||||||
|
})
|
||||||
|
|
||||||
|
const message =
|
||||||
|
req.t(
|
||||||
|
'Na vaš elektronski naslov smo vam poslali sporočilo s povezavo, s katero boste potrdili menjavo elektronskega naslova. Povezava za potrditev je veljavna '
|
||||||
|
) +
|
||||||
|
`${CHANGE_EMAIL_TOKEN_VALID_DAYS} ` +
|
||||||
|
req.t('dni.')
|
||||||
|
|
||||||
|
req.flash('info', message)
|
||||||
|
res.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
users.updatePassword = async (req, res) => {
|
||||||
|
const { passwordOld, passwordNew, passwordNewRepeat } = req.body
|
||||||
|
|
||||||
|
// TODO Validation (mirror front end validation, ...).
|
||||||
|
if (passwordNew !== passwordNewRepeat) {
|
||||||
|
const err = Error(req.t('Gesli se ne ujemata'))
|
||||||
|
err.status = 403
|
||||||
|
err.displayInProd = true
|
||||||
|
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
await User.changePassword(req.user.id, passwordOld, passwordNew, req.t)
|
||||||
|
|
||||||
|
// TODO Invalidate or log out all session for this user. More details in deleteCurrent method TODO.
|
||||||
|
|
||||||
|
const renderAsync = promisify(req.app.render.bind(req.app))
|
||||||
|
const emailHtml = await renderAsync(
|
||||||
|
`email/user-change-password_${req.language}`,
|
||||||
|
{
|
||||||
|
username: req.user.userName
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await email.send({
|
||||||
|
to: req.user.email,
|
||||||
|
subject: req.t('Sprememba gesla'),
|
||||||
|
html: emailHtml
|
||||||
|
})
|
||||||
|
|
||||||
|
req.flash('info', 'Geslo je bilo spremenjeno.')
|
||||||
|
res.send()
|
||||||
|
}
|
||||||
|
|
||||||
|
users.deleteCurrent = [
|
||||||
|
async (req, res, next) => {
|
||||||
|
await User.closeAccount(req.user.id)
|
||||||
|
next()
|
||||||
|
},
|
||||||
|
logoutUser,
|
||||||
|
(req, res) => {
|
||||||
|
// TODO Invalidate or log out all session for this user. Current workaround is in passport.deserializeUser.
|
||||||
|
// You can probably do it in 1 of 3 ways:
|
||||||
|
// 1. Brute force; loop through all sessions (using session store's all or ids methods),
|
||||||
|
// look up their values and remote the ones with user's id
|
||||||
|
// 2. Include user Id as part of session key; something similar to https://github.com/tj/connect-redis/issues/210#issuecomment-1336545115
|
||||||
|
// 3. Create some kind of inverse index, mapping user id to his/hers sessions (also mentioned in issue linked above)
|
||||||
|
req.flash('info', req.t('Vaš uporabniški račun je bil uspešno izbrisan.'))
|
||||||
|
res.send()
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
module.exports = users
|
module.exports = users
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const { DEFAULT_HITS_PER_PAGE } = require('../config/settings')
|
|||||||
const generateQuery = require('../models/helpers/search/generate-query')
|
const generateQuery = require('../models/helpers/search/generate-query')
|
||||||
const { searchConsultancyEntryIndex } = require('../models/search-engine')
|
const { searchConsultancyEntryIndex } = require('../models/search-engine')
|
||||||
const { prepareConsultancyEntries } = require('../models/helpers/search')
|
const { prepareConsultancyEntries } = require('../models/helpers/search')
|
||||||
|
const { getInstanceSetting, intoDbArray } = require('../models/helpers')
|
||||||
// const { minEntriesRequirementCheckAndAct } = require('./helpers/dictionary')
|
// const { minEntriesRequirementCheckAndAct } = require('./helpers/dictionary')
|
||||||
|
|
||||||
const consultancy = {}
|
const consultancy = {}
|
||||||
@@ -16,6 +17,9 @@ const consultancyAdmin = {}
|
|||||||
consultancy.index = async (req, res) => {
|
consultancy.index = async (req, res) => {
|
||||||
req.indexHitPageAmount = '5'
|
req.indexHitPageAmount = '5'
|
||||||
|
|
||||||
|
res.locals.isOwnConsultancyEnabled =
|
||||||
|
(await getInstanceSetting('consultancy_type')) === 'own'
|
||||||
|
|
||||||
return await consultancyRequest(
|
return await consultancyRequest(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
@@ -25,31 +29,43 @@ consultancy.index = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
consultancy.search = async (req, res) => {
|
consultancy.search = async (req, res) => {
|
||||||
|
res.locals.isOwnConsultancyEnabled =
|
||||||
|
(await getInstanceSetting('consultancy_type')) === 'own'
|
||||||
|
|
||||||
|
res.locals.queryKey = req.query.q
|
||||||
|
|
||||||
return await consultancyRequest(
|
return await consultancyRequest(
|
||||||
req,
|
req,
|
||||||
res,
|
res,
|
||||||
'published',
|
'published',
|
||||||
'pages/consultancy/search'
|
'pages/consultancy/search',
|
||||||
|
req.t('Odgovori')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
consultancy.specificQuestion = async (req, res) => {
|
consultancy.specificQuestion = async (req, res) => {
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
|
|
||||||
|
// TODO validation: is id of proper format and does a question with it actually exist.
|
||||||
|
|
||||||
|
// TODO i18n TIME FORMAT
|
||||||
const entry = await ConsultancyEntry.fetchByIdWithFormattedTime(id)
|
const entry = await ConsultancyEntry.fetchByIdWithFormattedTime(id)
|
||||||
// const author = await User.fetchUser(entry.authorId)
|
// const author = await User.fetchUser(entry.authorId)
|
||||||
|
|
||||||
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
|
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains(
|
||||||
|
req.determinedLanguage
|
||||||
|
)
|
||||||
|
|
||||||
entry.answerAuthors = entry.answerAuthors.filter(author => author !== '')
|
entry.answerAuthors = entry.answerAuthors.filter(author => author !== '')
|
||||||
|
|
||||||
let authorString
|
let authorString
|
||||||
|
// TODO I18n
|
||||||
if (entry.answerAuthors.length === 1) {
|
if (entry.answerAuthors.length === 1) {
|
||||||
authorString = 'Avtor'
|
authorString = req.t('Avtor')
|
||||||
} else if (entry.answerAuthors.length === 2) {
|
} else if (entry.answerAuthors.length === 2) {
|
||||||
authorString = 'Avtorja'
|
authorString = req.t('Avtorja')
|
||||||
} else {
|
} else {
|
||||||
authorString = 'Avtorji'
|
authorString = req.t('Avtorji')
|
||||||
}
|
}
|
||||||
|
|
||||||
entry.domain = allPrimaryDomains.filter(
|
entry.domain = allPrimaryDomains.filter(
|
||||||
@@ -62,18 +78,28 @@ consultancy.specificQuestion = async (req, res) => {
|
|||||||
entry.domain = false
|
entry.domain = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res.locals.isOwnConsultancyEnabled =
|
||||||
|
(await getInstanceSetting('consultancy_type')) === 'own'
|
||||||
|
|
||||||
res.render('pages/consultancy/item-details', {
|
res.render('pages/consultancy/item-details', {
|
||||||
allPrimaryDomains,
|
allPrimaryDomains,
|
||||||
authorString,
|
authorString,
|
||||||
entry
|
entry,
|
||||||
|
title: req.t('Odgovor')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
consultancy.new = async (req, res) => {
|
consultancy.new = async (req, res) => {
|
||||||
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
|
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains(
|
||||||
|
req.determinedLanguage
|
||||||
|
)
|
||||||
|
|
||||||
|
res.locals.isOwnConsultancyEnabled =
|
||||||
|
(await getInstanceSetting('consultancy_type')) === 'own'
|
||||||
|
|
||||||
res.render('pages/consultancy/ask', {
|
res.render('pages/consultancy/ask', {
|
||||||
allPrimaryDomains
|
allPrimaryDomains,
|
||||||
|
title: req.t('Novo vprašanje')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,19 +109,18 @@ consultancyAdmin.new = async (req, res) => {
|
|||||||
res,
|
res,
|
||||||
'new',
|
'new',
|
||||||
'pages/consultancy/admin/index',
|
'pages/consultancy/admin/index',
|
||||||
|
req.t('Novo'),
|
||||||
true,
|
true,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
consultancyAdmin.users = async (req, res) => {
|
consultancyAdmin.users = async (req, res) => {
|
||||||
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
|
|
||||||
|
|
||||||
const users = await User.fetchConsultants()
|
const users = await User.fetchConsultants()
|
||||||
|
|
||||||
res.render('pages/consultancy/admin/users', {
|
res.render('pages/consultancy/admin/users', {
|
||||||
allPrimaryDomains,
|
users,
|
||||||
users
|
title: req.t('Svetovalci')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +130,7 @@ consultancyAdmin.rejected = async (req, res) => {
|
|||||||
res,
|
res,
|
||||||
'rejected',
|
'rejected',
|
||||||
'pages/consultancy/admin/rejected',
|
'pages/consultancy/admin/rejected',
|
||||||
|
req.t('Zavrnjeno'),
|
||||||
true,
|
true,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
@@ -116,8 +142,10 @@ consultancyAdmin.published = async (req, res) => {
|
|||||||
res,
|
res,
|
||||||
'published',
|
'published',
|
||||||
'pages/consultancy/admin/published',
|
'pages/consultancy/admin/published',
|
||||||
|
req.t('Objavljeno'),
|
||||||
true,
|
true,
|
||||||
false
|
false,
|
||||||
|
'published'
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +155,7 @@ consultancyAdmin.prepared = async (req, res) => {
|
|||||||
res,
|
res,
|
||||||
'review',
|
'review',
|
||||||
'pages/consultancy/admin/prepared',
|
'pages/consultancy/admin/prepared',
|
||||||
|
req.t('Pripravljeno'),
|
||||||
true,
|
true,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
@@ -142,17 +171,14 @@ consultancyAdmin.inProgress = async (req, res) => {
|
|||||||
res,
|
res,
|
||||||
'in progress',
|
'in progress',
|
||||||
'pages/consultancy/admin/in-progress',
|
'pages/consultancy/admin/in-progress',
|
||||||
|
req.t('V delu'),
|
||||||
true,
|
true,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
consultancyAdmin.statistics = async (req, res) => {
|
consultancyAdmin.statistics = async (req, res) => {
|
||||||
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
|
res.render('pages/consultancy/admin/statistics')
|
||||||
|
|
||||||
res.render('pages/consultancy/admin/statistics', {
|
|
||||||
allPrimaryDomains
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
consultancyAdmin.edit = async (req, res) => {
|
consultancyAdmin.edit = async (req, res) => {
|
||||||
@@ -162,17 +188,11 @@ consultancyAdmin.edit = async (req, res) => {
|
|||||||
sentFrom[key] = true
|
sentFrom[key] = true
|
||||||
|
|
||||||
const moderator = await ConsultancyEntry.getModerator(id)
|
const moderator = await ConsultancyEntry.getModerator(id)
|
||||||
const editors = await ConsultancyEntry.getEditors(id)
|
// TODO i18n TIME FORMAT
|
||||||
if (
|
|
||||||
req.user.hasRole('consultancy admin') ||
|
|
||||||
req.user.hasRole('portal admin')
|
|
||||||
) {
|
|
||||||
console.log('Editor guard omitted due to being administrator')
|
|
||||||
} else if (editors.filter(editors => editors.id === req.user.id) < 1) {
|
|
||||||
return res.send('You do not have permsisions to edit this answer')
|
|
||||||
}
|
|
||||||
const entry = await ConsultancyEntry.fetchByIdWithFormattedTime(id)
|
const entry = await ConsultancyEntry.fetchByIdWithFormattedTime(id)
|
||||||
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
|
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains(
|
||||||
|
req.determinedLanguage
|
||||||
|
)
|
||||||
const author = await User.fetchUser(entry.authorId)
|
const author = await User.fetchUser(entry.authorId)
|
||||||
|
|
||||||
const isPublished = entry.status === 'published'
|
const isPublished = entry.status === 'published'
|
||||||
@@ -186,7 +206,8 @@ consultancyAdmin.edit = async (req, res) => {
|
|||||||
author,
|
author,
|
||||||
isPublished,
|
isPublished,
|
||||||
// TODO Luka: I suspect this will not work as intended on staging or production environments. Test.
|
// TODO Luka: I suspect this will not work as intended on staging or production environments. Test.
|
||||||
urlPrefix: req.protocol + '://' + req.get('host')
|
urlPrefix: req.protocol + '://' + req.get('host'),
|
||||||
|
title: req.t('Urejanje')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,14 +219,14 @@ function dateMap(obj) {
|
|||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mapDomainIdToDomainNameSlovene(obj) {
|
async function mapDomainIdToDomainNameSlovene(obj, t) {
|
||||||
try {
|
try {
|
||||||
const area = await Domain.fetchById(
|
const area = await Domain.fetchById(
|
||||||
obj.domainPrimaryId ? obj.domainPrimaryId : obj.domainPrimaryIdInitial
|
obj.domainPrimaryId ? obj.domainPrimaryId : obj.domainPrimaryIdInitial
|
||||||
)
|
)
|
||||||
obj.area = area.nameSl
|
obj.area = area.nameSl
|
||||||
} catch {
|
} catch {
|
||||||
obj.area = 'Ni področja'
|
obj.area = t('Ni področja')
|
||||||
}
|
}
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
@@ -227,14 +248,14 @@ function mapInitialValuesAsEmpty(obj) {
|
|||||||
return obj
|
return obj
|
||||||
}
|
}
|
||||||
|
|
||||||
async function mapEntryList(list) {
|
async function mapEntryList(list, t) {
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
list.map(entry => {
|
list.map(entry => {
|
||||||
let entity = utils.compose(dateMap, mapInitialValuesAsEmpty)(entry)
|
let entity = utils.compose(dateMap, mapInitialValuesAsEmpty)(entry)
|
||||||
|
|
||||||
// TODO Each mapDomainIdToDomainNameSlovene call leads to one DB query.
|
// TODO Each mapDomainIdToDomainNameSlovene call leads to one DB query.
|
||||||
// TODO Test if and what scenarios can lead to too many calls and how it can be avoided.
|
// TODO Test if and what scenarios can lead to too many calls and how it can be avoided.
|
||||||
entity = utils.composeAsync(mapDomainIdToDomainNameSlovene)(entry)
|
entity = utils.composeAsync(mapDomainIdToDomainNameSlovene)(entry, t)
|
||||||
|
|
||||||
return entity
|
return entity
|
||||||
})
|
})
|
||||||
@@ -283,11 +304,28 @@ async function consultancyRequest(
|
|||||||
res,
|
res,
|
||||||
type,
|
type,
|
||||||
url,
|
url,
|
||||||
|
title = req.t('Svetovanje'),
|
||||||
isAdminPage = false,
|
isAdminPage = false,
|
||||||
privilegeToSeAll = true // this method seperates consultancy main from admin, so all results get visible TO ALL REGISTERED USERS, not just admins
|
privilegeToSeAll = true // this method seperates consultancy main from admin, so all results get visible TO ALL REGISTERED USERS, not just admins
|
||||||
) {
|
) {
|
||||||
const searchString = req.query.q?.trim() ?? ''
|
const searchString = req.query.q?.trim() ?? ''
|
||||||
|
|
||||||
|
let allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains(
|
||||||
|
req.determinedLanguage
|
||||||
|
)
|
||||||
|
|
||||||
|
/// filter selected domains for the prompt ///
|
||||||
|
// Not duplicates of this code arise...
|
||||||
|
const pdList = intoDbArray(req.query.pd, 'always')
|
||||||
|
|
||||||
|
allPrimaryDomains = allPrimaryDomains.map(entry => {
|
||||||
|
if (pdList.includes(`${entry.id}`)) {
|
||||||
|
entry.selected = true
|
||||||
|
}
|
||||||
|
return entry
|
||||||
|
})
|
||||||
|
/// //////////////////////////////////////////
|
||||||
|
|
||||||
let assignedConsultant
|
let assignedConsultant
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -333,11 +371,11 @@ async function consultancyRequest(
|
|||||||
|
|
||||||
let entries = prepareConsultancyEntries(hits)
|
let entries = prepareConsultancyEntries(hits)
|
||||||
// console.log({ entries, numberOfAllHits, numberOfAllPages })
|
// console.log({ entries, numberOfAllHits, numberOfAllPages })
|
||||||
|
// TODO I18n - nameSl
|
||||||
entries = entries.map(entry => {
|
entries = entries.map(entry => {
|
||||||
entry.primaryDomain = entry.primaryDomain
|
entry.primaryDomain = entry.primaryDomain
|
||||||
? entry.primaryDomain.nameSl
|
? entry.primaryDomain.nameSl
|
||||||
: 'nedefinirano'
|
: req.t('nedefinirano')
|
||||||
|
|
||||||
if (entry.assignedConsultants) {
|
if (entry.assignedConsultants) {
|
||||||
entry.firstName = entry.assignedConsultants[0]?.firstName
|
entry.firstName = entry.assignedConsultants[0]?.firstName
|
||||||
@@ -365,8 +403,6 @@ async function consultancyRequest(
|
|||||||
return entry
|
return entry
|
||||||
})
|
})
|
||||||
|
|
||||||
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
|
|
||||||
|
|
||||||
// const entryList = await mapEntryList(inProgressEntryList)
|
// const entryList = await mapEntryList(inProgressEntryList)
|
||||||
const userList = await User.fetchConsultants()
|
const userList = await User.fetchConsultants()
|
||||||
|
|
||||||
@@ -401,7 +437,9 @@ async function consultancyRequest(
|
|||||||
entries, // entryList,
|
entries, // entryList,
|
||||||
userList,
|
userList,
|
||||||
numberOfAllPages,
|
numberOfAllPages,
|
||||||
queryCount: numberOfAllHits
|
queryCount: numberOfAllHits,
|
||||||
|
consultancyPageType: type,
|
||||||
|
title
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
const DemoPaginacija = require('../models/demo-paginacija')
|
|
||||||
const { DEFAULT_HITS_PER_PAGE } = require('../config/settings')
|
|
||||||
|
|
||||||
const demoPaginacija = {}
|
|
||||||
|
|
||||||
demoPaginacija.izrišiStran = async (req, res) => {
|
|
||||||
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
|
|
||||||
|
|
||||||
const { pages_total: numberOfAllPages, results } = await DemoPaginacija.fetch(
|
|
||||||
resultsPerPage,
|
|
||||||
1
|
|
||||||
)
|
|
||||||
|
|
||||||
res.render('pages/demo-paginacija', { numberOfAllPages, results })
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = demoPaginacija
|
|
||||||
@@ -2,6 +2,7 @@ const { unlink } = require('fs/promises')
|
|||||||
const { promisify } = require('util')
|
const { promisify } = require('util')
|
||||||
const multer = require('multer')
|
const multer = require('multer')
|
||||||
const debug = require('debug')('termPortal:controllers/dictionary')
|
const debug = require('debug')('termPortal:controllers/dictionary')
|
||||||
|
const user = require('../middleware/user')
|
||||||
const Dictionary = require('../models/dictionary')
|
const Dictionary = require('../models/dictionary')
|
||||||
const Entry = require('../models/entry')
|
const Entry = require('../models/entry')
|
||||||
const User = require('../models/user')
|
const User = require('../models/user')
|
||||||
@@ -9,7 +10,8 @@ const Comment = require('../models/comment')
|
|||||||
const genEditorAllQuery = require('../models/helpers/search/generate-query/editor/all')
|
const genEditorAllQuery = require('../models/helpers/search/generate-query/editor/all')
|
||||||
const { searchEntryIndex } = require('../models/search-engine')
|
const { searchEntryIndex } = require('../models/search-engine')
|
||||||
const { prepareEditorEntries } = require('../models/helpers/search')
|
const { prepareEditorEntries } = require('../models/helpers/search')
|
||||||
const { getInstanceSetting } = require('../models/helpers')
|
const { getInstanceSetting, intoDbArray } = require('../models/helpers')
|
||||||
|
const { getExportFilesPath } = require('../models/helpers/dictionary')
|
||||||
const { DEFAULT_HITS_PER_PAGE, DATA_FILES_PATH } = require('../config/settings')
|
const { DEFAULT_HITS_PER_PAGE, DATA_FILES_PATH } = require('../config/settings')
|
||||||
const {
|
const {
|
||||||
statusChangeCheckAndAct,
|
statusChangeCheckAndAct,
|
||||||
@@ -17,6 +19,8 @@ const {
|
|||||||
} = require('./helpers/dictionary')
|
} = require('./helpers/dictionary')
|
||||||
const SFDSuggestionImporter = require('./helpers/search-filter-data-suggestion-importer')
|
const SFDSuggestionImporter = require('./helpers/search-filter-data-suggestion-importer')
|
||||||
const Extraction = require('../models/extraction')
|
const Extraction = require('../models/extraction')
|
||||||
|
const { isGeneratorFunction } = require('util/types')
|
||||||
|
const { capitalize } = require('../utils')
|
||||||
|
|
||||||
const importFileBodyParser = multer({
|
const importFileBodyParser = multer({
|
||||||
dest: `${DATA_FILES_PATH}/dict_import_temp`,
|
dest: `${DATA_FILES_PATH}/dict_import_temp`,
|
||||||
@@ -36,26 +40,27 @@ const dictionary = {}
|
|||||||
dictionary.list = async (req, res) => {
|
dictionary.list = async (req, res) => {
|
||||||
let dictionaries
|
let dictionaries
|
||||||
if (req.isAuthenticated()) {
|
if (req.isAuthenticated()) {
|
||||||
dictionaries = await Dictionary.fetchAllByUser(req.user.id)
|
dictionaries = await Dictionary.fetchAllByUser(
|
||||||
|
req.user.id,
|
||||||
|
req.determinedLanguage
|
||||||
|
)
|
||||||
}
|
}
|
||||||
res.render('pages/dictionaries/list', {
|
res.render('pages/dictionaries/list', {
|
||||||
title: 'Seznam slovarjev',
|
title: req.t('Seznam slovarjev'),
|
||||||
dictionaries
|
dictionaries
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
dictionary.new = async (req, res) => {
|
dictionary.new = async (req, res) => {
|
||||||
// TODO Once english language is implemented, gather selected language (sl/en) from request ~ (cookies?)
|
|
||||||
const language = 'name_sl'
|
|
||||||
const [allPrimaryDomains, allSecondaryDomains, allLanguages] =
|
const [allPrimaryDomains, allSecondaryDomains, allLanguages] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
Dictionary.fetchAllPrimaryDomains(),
|
Dictionary.fetchAllPrimaryDomains(req.determinedLanguage),
|
||||||
Dictionary.fetchAllApprovedSecondaryDomains(),
|
Dictionary.fetchAllApprovedSecondaryDomains(),
|
||||||
Dictionary.fetchAllLanguages(language)
|
Dictionary.fetchAllLanguages(req.determinedLanguage, true)
|
||||||
])
|
])
|
||||||
|
|
||||||
res.render('pages/dictionaries/new', {
|
res.render('pages/dictionaries/new', {
|
||||||
title: 'Nov slovar',
|
title: req.t('Nov slovar'),
|
||||||
allPrimaryDomains,
|
allPrimaryDomains,
|
||||||
allSecondaryDomains,
|
allSecondaryDomains,
|
||||||
allLanguages
|
allLanguages
|
||||||
@@ -64,7 +69,7 @@ dictionary.new = async (req, res) => {
|
|||||||
|
|
||||||
dictionary.create = async (req, res) => {
|
dictionary.create = async (req, res) => {
|
||||||
await Dictionary.create(req.body, req.user.id)
|
await Dictionary.create(req.body, req.user.id)
|
||||||
res.redirect('/slovarji/moji')
|
res.redirect(303, '/slovarji/moji')
|
||||||
}
|
}
|
||||||
|
|
||||||
dictionary.editDescription = async (req, res) => {
|
dictionary.editDescription = async (req, res) => {
|
||||||
@@ -75,14 +80,14 @@ dictionary.editDescription = async (req, res) => {
|
|||||||
dictionary,
|
dictionary,
|
||||||
associatedSecondaryDomains
|
associatedSecondaryDomains
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
Dictionary.fetchAllPrimaryDomains(),
|
Dictionary.fetchAllPrimaryDomains(req.determinedLanguage),
|
||||||
Dictionary.fetchAllApprovedSecondaryDomains(),
|
Dictionary.fetchAllApprovedSecondaryDomains(),
|
||||||
Dictionary.fetchEditDescription(dictionaryId),
|
Dictionary.fetchEditDescription(dictionaryId),
|
||||||
Dictionary.fetchSecondaryDomains(dictionaryId)
|
Dictionary.fetchSecondaryDomains(dictionaryId)
|
||||||
])
|
])
|
||||||
|
|
||||||
res.render('pages/dictionaries/description', {
|
res.render('pages/dictionaries/description', {
|
||||||
title: 'Ime in opis',
|
title: req.t('Osnovni podatki'),
|
||||||
allPrimaryDomains,
|
allPrimaryDomains,
|
||||||
allSecondaryDomains,
|
allSecondaryDomains,
|
||||||
dictionary,
|
dictionary,
|
||||||
@@ -101,14 +106,14 @@ dictionary.updateDescription = async (req, res) => {
|
|||||||
Dictionary.updateSecondaryDomains(dictionaryId, body)
|
Dictionary.updateSecondaryDomains(dictionaryId, body)
|
||||||
])
|
])
|
||||||
|
|
||||||
res.redirect('back')
|
res.redirect(303, 'back')
|
||||||
}
|
}
|
||||||
|
|
||||||
dictionary.editUsers = async (req, res) => {
|
dictionary.editUsers = async (req, res) => {
|
||||||
const dictionaryId = req.params.dictionaryId
|
const dictionaryId = req.params.dictionaryId
|
||||||
const [dictionary, userRights, entriesCount, minEntries, publishApproval] =
|
const [dictionary, userRights, entriesCount, minEntries, publishApproval] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
Dictionary.fetchEditUsers(dictionaryId),
|
Dictionary.fetchEditUsers(dictionaryId, req.determinedLanguage),
|
||||||
User.fetchAllWithDictionaryRights(dictionaryId),
|
User.fetchAllWithDictionaryRights(dictionaryId),
|
||||||
Dictionary.countPublishedEntries(dictionaryId),
|
Dictionary.countPublishedEntries(dictionaryId),
|
||||||
getInstanceSetting('min_entries_per_dictionary'),
|
getInstanceSetting('min_entries_per_dictionary'),
|
||||||
@@ -125,7 +130,7 @@ dictionary.editUsers = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.render(viewPath, {
|
res.render(viewPath, {
|
||||||
title: 'Uporabniki',
|
title: req.t('Uporabniki'),
|
||||||
dictionary,
|
dictionary,
|
||||||
userRights,
|
userRights,
|
||||||
entriesCount,
|
entriesCount,
|
||||||
@@ -142,8 +147,9 @@ dictionary.updateUsers = async (req, res) => {
|
|||||||
|
|
||||||
const newDictStatus = await determineNewStatus(isPublished)
|
const newDictStatus = await determineNewStatus(isPublished)
|
||||||
|
|
||||||
const { nameSl, status: oldDictStatus } = await Dictionary.fetchEditUsers(
|
const { name, status: oldDictStatus } = await Dictionary.fetchEditUsers(
|
||||||
dictionaryId
|
dictionaryId,
|
||||||
|
req.determinedLanguage
|
||||||
)
|
)
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -162,22 +168,20 @@ dictionary.updateUsers = async (req, res) => {
|
|||||||
dictionaryId,
|
dictionaryId,
|
||||||
isPublished,
|
isPublished,
|
||||||
oldDictStatus,
|
oldDictStatus,
|
||||||
nameSl,
|
name,
|
||||||
req.app,
|
req.app,
|
||||||
req.user
|
req.user
|
||||||
)
|
)
|
||||||
|
|
||||||
res.redirect('back')
|
res.redirect(303, 'back')
|
||||||
}
|
}
|
||||||
|
|
||||||
dictionary.editStructure = async (req, res) => {
|
dictionary.editStructure = async (req, res) => {
|
||||||
// TODO Once english language is implemented, gather selected language (sl/en) from request ~ (cookies?)
|
|
||||||
const language = 'name_sl'
|
|
||||||
const { dictionaryId } = req.params
|
const { dictionaryId } = req.params
|
||||||
const [dictionary, associatedLanguages, allLanguages] = await Promise.all([
|
const [dictionary, associatedLanguages, allLanguages] = await Promise.all([
|
||||||
Dictionary.fetchEditStructure(dictionaryId),
|
Dictionary.fetchEditStructure(dictionaryId),
|
||||||
Dictionary.fetchLanguages(dictionaryId),
|
Dictionary.fetchLanguages(dictionaryId, req.determinedLanguage),
|
||||||
Dictionary.fetchAllLanguages(language)
|
Dictionary.fetchAllLanguages(req.determinedLanguage, true)
|
||||||
])
|
])
|
||||||
|
|
||||||
let viewPath
|
let viewPath
|
||||||
@@ -190,7 +194,7 @@ dictionary.editStructure = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.render(viewPath, {
|
res.render(viewPath, {
|
||||||
title: 'Struktura slovarskega sestavka',
|
title: req.t('Struktura slovarskega sestavka'),
|
||||||
dictionary,
|
dictionary,
|
||||||
associatedLanguages,
|
associatedLanguages,
|
||||||
allLanguages
|
allLanguages
|
||||||
@@ -207,12 +211,15 @@ dictionary.updateStructure = async (req, res) => {
|
|||||||
Dictionary.deleteLanguages(dictionaryId),
|
Dictionary.deleteLanguages(dictionaryId),
|
||||||
Dictionary.updateLanguages(dictionaryId, body)
|
Dictionary.updateLanguages(dictionaryId, body)
|
||||||
])
|
])
|
||||||
res.redirect('back')
|
res.redirect(303, 'back')
|
||||||
}
|
}
|
||||||
|
|
||||||
dictionary.editAdvanced = async (req, res) => {
|
dictionary.editAdvanced = async (req, res) => {
|
||||||
const { dictionaryId } = req.params
|
const { dictionaryId } = req.params
|
||||||
const dictionaryName = await Dictionary.fetchName(dictionaryId)
|
const dictionaryName = await Dictionary.fetchName(
|
||||||
|
dictionaryId,
|
||||||
|
req.determinedLanguage
|
||||||
|
)
|
||||||
let viewPath
|
let viewPath
|
||||||
switch (req.baseUrl) {
|
switch (req.baseUrl) {
|
||||||
case '/slovarji':
|
case '/slovarji':
|
||||||
@@ -223,7 +230,7 @@ dictionary.editAdvanced = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.render(viewPath, {
|
res.render(viewPath, {
|
||||||
title: 'Napredno',
|
title: req.t('Napredno'),
|
||||||
dictionary: { id: req.params.dictionaryId },
|
dictionary: { id: req.params.dictionaryId },
|
||||||
dictionaryName
|
dictionaryName
|
||||||
})
|
})
|
||||||
@@ -239,7 +246,7 @@ dictionary.comments = async (req, res) => {
|
|||||||
const [{ comments, pages_total: numberOfAllPages }, dictionaryName] =
|
const [{ comments, pages_total: numberOfAllPages }, dictionaryName] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
Comment.list(filters, req.user, resultsPerPage, 1),
|
Comment.list(filters, req.user, resultsPerPage, 1),
|
||||||
Dictionary.fetchName(dictionaryId)
|
Dictionary.fetchName(dictionaryId, req.determinedLanguage)
|
||||||
])
|
])
|
||||||
|
|
||||||
switch (req.baseUrl) {
|
switch (req.baseUrl) {
|
||||||
@@ -251,7 +258,7 @@ dictionary.comments = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.render(viewPath, {
|
res.render(viewPath, {
|
||||||
title: 'Komentarji',
|
title: req.t('Komentarji'),
|
||||||
numberOfAllPages,
|
numberOfAllPages,
|
||||||
dictionary: { id: req.params.dictionaryId },
|
dictionary: { id: req.params.dictionaryId },
|
||||||
comments,
|
comments,
|
||||||
@@ -261,10 +268,12 @@ dictionary.comments = async (req, res) => {
|
|||||||
|
|
||||||
dictionary.showImportFromFileForm = async (req, res) => {
|
dictionary.showImportFromFileForm = async (req, res) => {
|
||||||
const { dictionaryId } = req.params
|
const { dictionaryId } = req.params
|
||||||
const [imports, dictionaryName] = await Promise.all([
|
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
|
||||||
Dictionary.fetchAllImports(dictionaryId),
|
const [{ pages_total: numberOfAllPages, results }, dictionaryName] =
|
||||||
Dictionary.fetchName(dictionaryId)
|
await Promise.all([
|
||||||
])
|
Dictionary.fetchAllImports(dictionaryId, resultsPerPage, 1),
|
||||||
|
Dictionary.fetchName(dictionaryId, req.determinedLanguage)
|
||||||
|
])
|
||||||
let viewPath
|
let viewPath
|
||||||
switch (req.baseUrl) {
|
switch (req.baseUrl) {
|
||||||
case '/slovarji':
|
case '/slovarji':
|
||||||
@@ -275,9 +284,10 @@ dictionary.showImportFromFileForm = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.render(viewPath, {
|
res.render(viewPath, {
|
||||||
title: 'Uvoz iz datoteke',
|
title: req.t('Uvoz iz datoteke'),
|
||||||
dictionary: { id: dictionaryId },
|
dictionary: { id: dictionaryId },
|
||||||
imports,
|
numberOfAllPages,
|
||||||
|
results,
|
||||||
dictionaryName
|
dictionaryName
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -286,10 +296,14 @@ dictionary.listAdminDictionaries = async (req, res) => {
|
|||||||
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
|
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
|
||||||
|
|
||||||
const { pages_total: numberOfAllPages, results } =
|
const { pages_total: numberOfAllPages, results } =
|
||||||
await Dictionary.fetchAllAdminDictionaries(resultsPerPage, 1)
|
await Dictionary.fetchAllAdminDictionaries(
|
||||||
|
req.determinedLanguage,
|
||||||
|
resultsPerPage,
|
||||||
|
1
|
||||||
|
)
|
||||||
|
|
||||||
res.render('pages/admin/dictionaries-list', {
|
res.render('pages/admin/dictionaries-list', {
|
||||||
title: 'Struktura slovarjev',
|
title: req.t('Seznam slovarjev'),
|
||||||
numberOfAllPages,
|
numberOfAllPages,
|
||||||
results
|
results
|
||||||
})
|
})
|
||||||
@@ -304,7 +318,7 @@ dictionary.adminEditDescription = async (req, res) => {
|
|||||||
associatedSecondaryDomains,
|
associatedSecondaryDomains,
|
||||||
status
|
status
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
Dictionary.fetchAllPrimaryDomains(),
|
Dictionary.fetchAllPrimaryDomains(req.determinedLanguage),
|
||||||
Dictionary.fetchAllApprovedSecondaryDomains(),
|
Dictionary.fetchAllApprovedSecondaryDomains(),
|
||||||
Dictionary.fetchEditDescription(dictionaryId),
|
Dictionary.fetchEditDescription(dictionaryId),
|
||||||
Dictionary.fetchSecondaryDomains(dictionaryId),
|
Dictionary.fetchSecondaryDomains(dictionaryId),
|
||||||
@@ -312,7 +326,7 @@ dictionary.adminEditDescription = async (req, res) => {
|
|||||||
])
|
])
|
||||||
|
|
||||||
res.render('pages/admin/dictionary-description', {
|
res.render('pages/admin/dictionary-description', {
|
||||||
title: 'Podatki',
|
title: req.t('Osnovni podatki'),
|
||||||
allPrimaryDomains,
|
allPrimaryDomains,
|
||||||
allSecondaryDomains,
|
allSecondaryDomains,
|
||||||
dictionary,
|
dictionary,
|
||||||
@@ -350,25 +364,29 @@ dictionary.updateAdminDescription = async (req, res) => {
|
|||||||
newDictStatus,
|
newDictStatus,
|
||||||
oldDictStatus,
|
oldDictStatus,
|
||||||
req.app,
|
req.app,
|
||||||
|
req.determinedLanguage,
|
||||||
req.user
|
req.user
|
||||||
)
|
)
|
||||||
|
|
||||||
res.redirect('back')
|
res.redirect(303, 'back')
|
||||||
}
|
}
|
||||||
|
|
||||||
dictionary.showImportFromExtractionForm = async (req, res) => {
|
dictionary.showImportFromExtractionForm = async (req, res) => {
|
||||||
const { dictionaryId } = req.params
|
const { dictionaryId } = req.params
|
||||||
const dictionaryName = await Dictionary.fetchName(dictionaryId)
|
const dictionaryName = await Dictionary.fetchName(
|
||||||
|
dictionaryId,
|
||||||
|
req.determinedLanguage
|
||||||
|
)
|
||||||
const extractions = await Extraction.fetchFinishedForUser(req.user.id)
|
const extractions = await Extraction.fetchFinishedForUser(req.user.id)
|
||||||
let viewPath, title
|
let viewPath, title
|
||||||
switch (req.baseUrl) {
|
switch (req.baseUrl) {
|
||||||
case '/slovarji':
|
case '/slovarji':
|
||||||
viewPath = 'pages/dictionaries/extraction-import'
|
viewPath = 'pages/dictionaries/extraction-import'
|
||||||
title = 'Uvoz'
|
title = req.t('Uvoz iz luščilnika')
|
||||||
break
|
break
|
||||||
case '/admin':
|
case '/admin':
|
||||||
viewPath = 'pages/admin/dictionary-extraction-import'
|
viewPath = 'pages/admin/dictionary-extraction-import'
|
||||||
title = 'Uvoz luščenje'
|
title = req.t('Uvoz iz luščilnika')
|
||||||
}
|
}
|
||||||
|
|
||||||
res.render(viewPath, {
|
res.render(viewPath, {
|
||||||
@@ -381,7 +399,12 @@ dictionary.showImportFromExtractionForm = async (req, res) => {
|
|||||||
|
|
||||||
dictionary.showExportToFileForm = async (req, res) => {
|
dictionary.showExportToFileForm = async (req, res) => {
|
||||||
const { dictionaryId } = req.params
|
const { dictionaryId } = req.params
|
||||||
const dictionaryName = await Dictionary.fetchName(dictionaryId)
|
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
|
||||||
|
const [dictionaryName, { pages_total: numberOfAllPages, results }] =
|
||||||
|
await Promise.all([
|
||||||
|
Dictionary.fetchName(dictionaryId, req.determinedLanguage),
|
||||||
|
Dictionary.fetchExports(dictionaryId, resultsPerPage, 1)
|
||||||
|
])
|
||||||
let viewPath
|
let viewPath
|
||||||
switch (req.baseUrl) {
|
switch (req.baseUrl) {
|
||||||
case '/slovarji':
|
case '/slovarji':
|
||||||
@@ -390,11 +413,12 @@ dictionary.showExportToFileForm = async (req, res) => {
|
|||||||
case '/admin':
|
case '/admin':
|
||||||
viewPath = 'pages/admin/dictionary-export'
|
viewPath = 'pages/admin/dictionary-export'
|
||||||
}
|
}
|
||||||
|
|
||||||
res.render(viewPath, {
|
res.render(viewPath, {
|
||||||
title: 'Izvoz',
|
title: req.t('Izvoz'),
|
||||||
dictionary: { id: req.params.dictionaryId },
|
dictionary: { id: dictionaryId },
|
||||||
dictionaryName
|
dictionaryName,
|
||||||
|
numberOfAllPages,
|
||||||
|
results
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,7 +429,7 @@ dictionary.editDomainLabels = async (req, res) => {
|
|||||||
const [{ pages_total: numberOfAllPages, results }, dictionaryName] =
|
const [{ pages_total: numberOfAllPages, results }, dictionaryName] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
Dictionary.fetchPaginationDomainLabels(dictionaryId, resultsPerPage, 1),
|
Dictionary.fetchPaginationDomainLabels(dictionaryId, resultsPerPage, 1),
|
||||||
Dictionary.fetchName(dictionaryId)
|
Dictionary.fetchName(dictionaryId, req.determinedLanguage)
|
||||||
])
|
])
|
||||||
|
|
||||||
let viewPath
|
let viewPath
|
||||||
@@ -418,7 +442,7 @@ dictionary.editDomainLabels = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
res.render(viewPath, {
|
res.render(viewPath, {
|
||||||
title: 'Področne oznake',
|
title: req.t('Področne oznake'),
|
||||||
dictionary: { id: dictionaryId },
|
dictionary: { id: dictionaryId },
|
||||||
numberOfAllPages,
|
numberOfAllPages,
|
||||||
results,
|
results,
|
||||||
@@ -432,26 +456,24 @@ dictionary.showContent = async (req, res) => {
|
|||||||
const [
|
const [
|
||||||
hits,
|
hits,
|
||||||
canPublishEntriesInEdit,
|
canPublishEntriesInEdit,
|
||||||
dictionaryName,
|
|
||||||
structure,
|
structure,
|
||||||
languages,
|
languages,
|
||||||
entryDomainLabels
|
entryDomainLabels
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
searchEntryIndex(hitsQuery),
|
searchEntryIndex(hitsQuery),
|
||||||
getInstanceSetting('can_publish_entries_in_edit'),
|
getInstanceSetting('can_publish_entries_in_edit'),
|
||||||
Dictionary.fetchName(dictionaryId),
|
|
||||||
Dictionary.fetchEditStructure(dictionaryId),
|
Dictionary.fetchEditStructure(dictionaryId),
|
||||||
Dictionary.fetchLanguages(dictionaryId),
|
Dictionary.fetchLanguages(dictionaryId, req.determinedLanguage),
|
||||||
Dictionary.fetchDomainLabels(dictionaryId)
|
Dictionary.fetchDomainLabels(dictionaryId)
|
||||||
])
|
])
|
||||||
|
|
||||||
const terms = prepareEditorEntries(hits)
|
const terms = prepareEditorEntries(hits)
|
||||||
|
|
||||||
res.render('pages/dictionaries/content', {
|
res.render('pages/dictionaries/content', {
|
||||||
title: 'Vsebina slovarja',
|
title: req.t('Vsebina slovarja'),
|
||||||
terms,
|
terms,
|
||||||
canPublishEntriesInEdit,
|
canPublishEntriesInEdit,
|
||||||
dictionaryName,
|
dictionaryName: structure[`name${capitalize(req.determinedLanguage)}`],
|
||||||
structure,
|
structure,
|
||||||
languages,
|
languages,
|
||||||
dictionaryId,
|
dictionaryId,
|
||||||
@@ -466,7 +488,7 @@ dictionary.showSecondaryDomains = async (req, res) => {
|
|||||||
await Dictionary.fetchAllSecondaryDomains(resultsPerPage, 1)
|
await Dictionary.fetchAllSecondaryDomains(resultsPerPage, 1)
|
||||||
|
|
||||||
res.render('pages/admin/areas', {
|
res.render('pages/admin/areas', {
|
||||||
title: 'Podpodročja',
|
title: req.t('Področne oznake'),
|
||||||
numberOfAllPages,
|
numberOfAllPages,
|
||||||
results
|
results
|
||||||
})
|
})
|
||||||
@@ -497,15 +519,33 @@ dictionary.dictionaryList = async (req, res) => {
|
|||||||
hitsPerPage,
|
hitsPerPage,
|
||||||
page,
|
page,
|
||||||
orderAttribute,
|
orderAttribute,
|
||||||
orderIndex
|
orderIndex,
|
||||||
|
req.determinedLanguage
|
||||||
)
|
)
|
||||||
|
|
||||||
const numberOfAllHits = parseInt(
|
const numberOfAllHits = parseInt(
|
||||||
(await Dictionary.fetchAllDictionariesCount()).count
|
(await Dictionary.fetchAllDictionariesPublishedCount()).count
|
||||||
)
|
)
|
||||||
const numberOfAllPages = Math.ceil(numberOfAllHits / hitsPerPage)
|
const numberOfAllPages = Math.ceil(numberOfAllHits / hitsPerPage)
|
||||||
|
|
||||||
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
|
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains(
|
||||||
|
req.determinedLanguage
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
/// filter selected domains for the prompt ///
|
||||||
|
// Not duplicates of this code arise...
|
||||||
|
const pdList = intoDbArray(req.query.pd, 'always')
|
||||||
|
|
||||||
|
allPrimaryDomains = allPrimaryDomains.map(entry => {
|
||||||
|
if (pdList.includes(`${entry.id}`)) {
|
||||||
|
entry.selected = true
|
||||||
|
}
|
||||||
|
return entry
|
||||||
|
})
|
||||||
|
|
||||||
|
/// //////////////////////////////////////////
|
||||||
|
*/
|
||||||
|
|
||||||
dictionaries = dictionaries.map(e => {
|
dictionaries = dictionaries.map(e => {
|
||||||
if (!e.portalcode) {
|
if (!e.portalcode) {
|
||||||
@@ -518,7 +558,7 @@ dictionary.dictionaryList = async (req, res) => {
|
|||||||
const isDictionaryListPage = true
|
const isDictionaryListPage = true
|
||||||
|
|
||||||
res.render('pages/dictionaries/dictlist', {
|
res.render('pages/dictionaries/dictlist', {
|
||||||
title: 'Seznam slovarjev',
|
title: req.t('Seznam slovarjev'),
|
||||||
dictionaries,
|
dictionaries,
|
||||||
allPrimaryDomains,
|
allPrimaryDomains,
|
||||||
numberOfAllPages,
|
numberOfAllPages,
|
||||||
@@ -532,15 +572,19 @@ dictionary.dictionaryList = async (req, res) => {
|
|||||||
dictionary.dictionaryDetails = async (req, res) => {
|
dictionary.dictionaryDetails = async (req, res) => {
|
||||||
const { absolutePrevPath, sentFromEntryId } = req.query
|
const { absolutePrevPath, sentFromEntryId } = req.query
|
||||||
const dictId = req.params.dictionaryId
|
const dictId = req.params.dictionaryId
|
||||||
|
const title = req.t('O slovarju')
|
||||||
const {
|
const {
|
||||||
allPrimaryDomains,
|
allPrimaryDomains,
|
||||||
sourceLanguages,
|
sourceLanguages,
|
||||||
targetLanguages,
|
targetLanguages,
|
||||||
allDictionaryNames,
|
allDictionaryNames,
|
||||||
portals
|
portals
|
||||||
} = await SFDSuggestionImporter.initialize()
|
} = await SFDSuggestionImporter.initialize(req.determinedLanguage)
|
||||||
|
|
||||||
const dictionaryData = await Dictionary.fetchDictionaryBasicInfo(dictId)
|
const dictionaryData = await Dictionary.fetchDictionaryBasicInfo(
|
||||||
|
dictId,
|
||||||
|
req.determinedLanguage
|
||||||
|
)
|
||||||
|
|
||||||
const filters = { ctxType: 'dictionary', ctxId: dictId }
|
const filters = { ctxType: 'dictionary', ctxId: dictId }
|
||||||
// TODO: integrate numberOfAllPages, commentCount with pug
|
// TODO: integrate numberOfAllPages, commentCount with pug
|
||||||
@@ -555,7 +599,7 @@ dictionary.dictionaryDetails = async (req, res) => {
|
|||||||
|
|
||||||
// check if it is a local dictionary
|
// check if it is a local dictionary
|
||||||
if (!dictionaryData.portalname && !dictionaryData.portalcode) {
|
if (!dictionaryData.portalname && !dictionaryData.portalcode) {
|
||||||
dictionaryData[0].portalname = await getInstanceSetting('portal_name')
|
dictionaryData[0].portalname = await getInstanceSetting('portal_name_sl')
|
||||||
dictionaryData[0].portalcode = await getInstanceSetting('portal_code')
|
dictionaryData[0].portalcode = await getInstanceSetting('portal_code')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -579,7 +623,7 @@ dictionary.dictionaryDetails = async (req, res) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
const structData = {
|
const structData = {
|
||||||
prevWindowTitle: 'Nazaj',
|
prevWindowTitle: req.t('Nazaj'),
|
||||||
dictName: dictionaryData[0].dictionarysl,
|
dictName: dictionaryData[0].dictionarysl,
|
||||||
portalCode: dictionaryData[0].portalcode,
|
portalCode: dictionaryData[0].portalcode,
|
||||||
portalName: dictionaryData[0].portalname,
|
portalName: dictionaryData[0].portalname,
|
||||||
@@ -589,13 +633,14 @@ dictionary.dictionaryDetails = async (req, res) => {
|
|||||||
languages: reducedData.languages ? reducedData.languages.join(', ') : ''
|
languages: reducedData.languages ? reducedData.languages.join(', ') : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO I18n
|
||||||
if (reducedData.author) {
|
if (reducedData.author) {
|
||||||
if (reducedData.author.length > 2) {
|
if (reducedData.author.length > 2) {
|
||||||
structData.authorLabel = 'Avtorji'
|
structData.authorLabel = req.t('Avtorji')
|
||||||
} else if (reducedData.author.length === 2) {
|
} else if (reducedData.author.length === 2) {
|
||||||
structData.authorLabel = 'Avtorja'
|
structData.authorLabel = req.t('Avtorja')
|
||||||
} else if (reducedData.author.length === 1) {
|
} else if (reducedData.author.length === 1) {
|
||||||
structData.authorLabel = 'Avtor'
|
structData.authorLabel = req.t('Avtor')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,7 +668,8 @@ dictionary.dictionaryDetails = async (req, res) => {
|
|||||||
finalData,
|
finalData,
|
||||||
numberOfAllPages,
|
numberOfAllPages,
|
||||||
comments,
|
comments,
|
||||||
commentCount
|
commentCount,
|
||||||
|
title
|
||||||
}) // todo
|
}) // todo
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,8 +726,41 @@ dictionary.importFromFile = async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dictionary.exportDownload = [
|
||||||
|
async (req, res, next) => {
|
||||||
|
const { exportId } = req.params
|
||||||
|
const exportDownloadMetadata = await Dictionary.fetchExportDownloadMetadata(
|
||||||
|
exportId
|
||||||
|
)
|
||||||
|
|
||||||
|
req.dictionaryId = exportDownloadMetadata.dictionaryId
|
||||||
|
req.exportDownloadMetadata = exportDownloadMetadata
|
||||||
|
|
||||||
|
next()
|
||||||
|
},
|
||||||
|
user.isDictionaryEditor,
|
||||||
|
(req, res) => {
|
||||||
|
const { exportId } = req.params
|
||||||
|
const { exportStatus, dictionaryId, nameString, timeString, fileFormat } =
|
||||||
|
req.exportDownloadMetadata
|
||||||
|
|
||||||
|
if (exportStatus !== 'finished') {
|
||||||
|
throw Error("Can't request file for unfinished export")
|
||||||
|
}
|
||||||
|
const exportFilesPath = getExportFilesPath(dictionaryId)
|
||||||
|
const exportFilePath = `${exportFilesPath}/${exportId}`
|
||||||
|
const exportFileName = `${nameString}_${timeString}.${fileFormat}`
|
||||||
|
|
||||||
|
res.download(exportFilePath, exportFileName)
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
function importFileFilter(req, file, cb) {
|
function importFileFilter(req, file, cb) {
|
||||||
if (file.mimetype !== 'text/xml') return cb(Error('Invalid file type'))
|
if (file.mimetype !== 'text/xml') {
|
||||||
|
const customError = Error('Invalid file type')
|
||||||
|
customError.displayInProd = true
|
||||||
|
return cb(customError)
|
||||||
|
}
|
||||||
cb(null, true)
|
cb(null, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
const { mkdir } = require('fs/promises')
|
|
||||||
const {
|
|
||||||
getDocumentsPath,
|
|
||||||
getStopTermsPath
|
|
||||||
} = require('../models/helpers/extraction')
|
|
||||||
const { checkIfcanBegin } = require('./helpers/extraction')
|
|
||||||
const { MAX_EXTRACTIONS_PER_USER } = require('../config/settings')
|
|
||||||
const Extraction = require('../models/extraction')
|
|
||||||
const Dictionary = require('../models/dictionary')
|
|
||||||
const Domain = require('../models/domain')
|
|
||||||
const { intoDbArray } = require('../models/helpers')
|
|
||||||
const { DEFAULT_HITS_PER_PAGE } = require('../config/settings')
|
|
||||||
|
|
||||||
const extraction = {}
|
|
||||||
|
|
||||||
extraction.list = async (req, res) => {
|
|
||||||
let extractions = await Extraction.fetchAllForUser(req.user.id)
|
|
||||||
extractions = await Promise.all(
|
|
||||||
extractions.map(async extraction => {
|
|
||||||
extraction.canBegin = await checkIfcanBegin(extraction)
|
|
||||||
if (extraction.status === 'finished') {
|
|
||||||
extraction.termCandidatesCount =
|
|
||||||
await Extraction.fetchTermCandidatesCount(extraction.id)
|
|
||||||
}
|
|
||||||
return extraction
|
|
||||||
})
|
|
||||||
)
|
|
||||||
res.render('extraction-poc/list', { extractions })
|
|
||||||
}
|
|
||||||
|
|
||||||
extraction.create = async (req, res) => {
|
|
||||||
const extractionCount = await Extraction.countAllForUser(req.user.id)
|
|
||||||
if (extractionCount >= MAX_EXTRACTIONS_PER_USER) {
|
|
||||||
// TODO Tukaj bo treba prikazati tudi obvestilo uporabniku skladno s trenutno metodologijo prikaza obvestil.
|
|
||||||
return res.redirect(303, 'back')
|
|
||||||
}
|
|
||||||
|
|
||||||
const extractionName = `Luščenje ${extractionCount + 1}`
|
|
||||||
const { extractionType } = req.body
|
|
||||||
|
|
||||||
let extractionId
|
|
||||||
if (extractionType === 'own') {
|
|
||||||
extractionId = await Extraction.createOwn(req.user.id, extractionName)
|
|
||||||
|
|
||||||
const documentsPath = getDocumentsPath(extractionId)
|
|
||||||
const stopTermsPath = getStopTermsPath(extractionId)
|
|
||||||
await Promise.all([
|
|
||||||
mkdir(documentsPath, { recursive: true }),
|
|
||||||
mkdir(stopTermsPath, { recursive: true })
|
|
||||||
])
|
|
||||||
} else {
|
|
||||||
extractionId = await Extraction.createOss(req.user.id, extractionName)
|
|
||||||
|
|
||||||
const stopTermsPath = getStopTermsPath(extractionId)
|
|
||||||
await mkdir(stopTermsPath, { recursive: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Redirect to extraction edit page.
|
|
||||||
res.redirect(`poc/${extractionId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
extraction.edit = async (req, res) => {
|
|
||||||
const extractionId = req.params.id
|
|
||||||
const extraction = await Extraction.fetch(extractionId)
|
|
||||||
|
|
||||||
if (extraction.ossParams) {
|
|
||||||
const [allPrimaryDomains, stopTermsFiles] = await Promise.all([
|
|
||||||
Dictionary.fetchAllPrimaryDomains(),
|
|
||||||
Extraction.fetchAllStopTermsFilesStats(extractionId)
|
|
||||||
])
|
|
||||||
const { params } = extraction.ossParams
|
|
||||||
const domainUdk = params?.domainUdk?.[0]
|
|
||||||
if (domainUdk)
|
|
||||||
extraction.domainId = await Domain.fetchIdByUdkCode(domainUdk)
|
|
||||||
extraction.documentType = intoDbArray(params.documentType, 'always')
|
|
||||||
extraction.year = intoDbArray(params.year, 'always')
|
|
||||||
extraction.keywords = intoDbArray(params.keywords, 'always')
|
|
||||||
|
|
||||||
res.render('extraction-poc/edit-oss', {
|
|
||||||
id: extractionId,
|
|
||||||
extraction,
|
|
||||||
allPrimaryDomains,
|
|
||||||
stopTermsFiles
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
const [extractionDocuments, stopTermsFiles] = await Promise.all([
|
|
||||||
Extraction.fetchAllDocumentsStats(extractionId),
|
|
||||||
Extraction.fetchAllStopTermsFilesStats(extractionId)
|
|
||||||
])
|
|
||||||
res.render('extraction-poc/edit-own', {
|
|
||||||
id: extractionId,
|
|
||||||
extraction,
|
|
||||||
extractionDocuments,
|
|
||||||
stopTermsFiles
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extraction.updateOwn = async (req, res) => {
|
|
||||||
const extractionId = req.params.id
|
|
||||||
await Extraction.update(extractionId, req.body.name)
|
|
||||||
|
|
||||||
// Reload page.
|
|
||||||
res.redirect(`./${extractionId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
extraction.docsEdit = async (req, res) => {
|
|
||||||
const extractionId = req.params.id
|
|
||||||
const extractionDocuments = await Extraction.fetchAllDocumentsStats(
|
|
||||||
extractionId
|
|
||||||
)
|
|
||||||
|
|
||||||
res.render('extraction-poc/docs-edit', {
|
|
||||||
id: extractionId,
|
|
||||||
extractionDocuments
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
extraction.stopTermsEdit = async (req, res) => {
|
|
||||||
const extractionId = req.params.id
|
|
||||||
const stopTermsFiles = await Extraction.fetchAllStopTermsFilesStats(
|
|
||||||
extractionId
|
|
||||||
)
|
|
||||||
|
|
||||||
res.render('extraction-poc/stop-terms-edit', {
|
|
||||||
id: extractionId,
|
|
||||||
stopTermsFiles
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
extraction.listTermCandidates = async (req, res) => {
|
|
||||||
const extractionId = req.params.id
|
|
||||||
const termCandidatesJson = await Extraction.fetchTermCandidatesJson(
|
|
||||||
extractionId
|
|
||||||
)
|
|
||||||
const termCandidates = JSON.parse(termCandidatesJson).terminoloski_kandidati
|
|
||||||
const hitsPerPage = +req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
|
|
||||||
const numberOfAllPages = Math.ceil(termCandidates.length / hitsPerPage)
|
|
||||||
const firstPageOfTermCandidates = termCandidates.slice(0, hitsPerPage)
|
|
||||||
|
|
||||||
res.render('extraction-poc/term-candidates', {
|
|
||||||
extractionId,
|
|
||||||
termCandidatesJson,
|
|
||||||
firstPageOfTermCandidates,
|
|
||||||
hitsPerPage,
|
|
||||||
numberOfAllPages
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = extraction
|
|
||||||
@@ -29,7 +29,10 @@ extraction.list = async (req, res) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.render('pages/extraction/list', { title: 'Luščenje seznam', extractions })
|
res.render('pages/extraction/list', {
|
||||||
|
title: req.t('Seznam luščenj'),
|
||||||
|
extractions
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
extraction.create = async (req, res) => {
|
extraction.create = async (req, res) => {
|
||||||
@@ -39,7 +42,7 @@ extraction.create = async (req, res) => {
|
|||||||
return res.redirect(303, 'back')
|
return res.redirect(303, 'back')
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractionName = `Luščenje ${extractionCount + 1}`
|
const extractionName = req.t('Luščenje') + `${extractionCount + 1}`
|
||||||
const { extractionType } = req.body
|
const { extractionType } = req.body
|
||||||
|
|
||||||
let extractionId
|
let extractionId
|
||||||
@@ -60,31 +63,46 @@ extraction.create = async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Redirect to extraction edit page.
|
// Redirect to extraction edit page.
|
||||||
res.redirect(`luscenje/${extractionId}`)
|
res.redirect(303, `luscenje/${extractionId}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
extraction.validateOwnership = async (req, res, next) => {
|
||||||
|
const { id: extractionId } = req.params
|
||||||
|
if (!extractionId) return res.redirect(303, '/')
|
||||||
|
|
||||||
|
const extraction = await Extraction.fetch(extractionId)
|
||||||
|
if (extraction.userId !== req.user.id) return res.redirect(303, '/')
|
||||||
|
|
||||||
|
req.extractionData = extraction
|
||||||
|
next()
|
||||||
}
|
}
|
||||||
|
|
||||||
extraction.edit = async (req, res) => {
|
extraction.edit = async (req, res) => {
|
||||||
const extractionId = req.params.id
|
const extractionId = req.params.id
|
||||||
const extraction = await Extraction.fetch(extractionId)
|
const extraction = req.extractionData
|
||||||
|
|
||||||
if (extraction.ossParams) {
|
if (extraction.ossParams) {
|
||||||
const [allPrimaryDomains, stopTermsFiles] = await Promise.all([
|
const [allPrimaryDomains, ossDocumentTypes, stopTermsFiles] =
|
||||||
Dictionary.fetchAllPrimaryDomains(),
|
await Promise.all([
|
||||||
Extraction.fetchAllStopTermsFilesStats(extractionId)
|
Dictionary.fetchAllPrimaryDomains(req.determinedLanguage),
|
||||||
])
|
Extraction.fetchOssDocumentTypes(req.determinedLanguage),
|
||||||
|
Extraction.fetchAllStopTermsFilesStats(extractionId)
|
||||||
|
])
|
||||||
const { params } = extraction.ossParams
|
const { params } = extraction.ossParams
|
||||||
const domainUdk = params?.domainUdk?.[0]
|
const domainUdk = params?.domainUdk?.[0]
|
||||||
if (domainUdk)
|
if (domainUdk) {
|
||||||
extraction.domainId = await Domain.fetchIdByUdkCode(domainUdk)
|
extraction.domainId = await Domain.fetchIdByUdkCode(domainUdk)
|
||||||
|
}
|
||||||
extraction.documentType = intoDbArray(params.documentType, 'always')
|
extraction.documentType = intoDbArray(params.documentType, 'always')
|
||||||
extraction.year = intoDbArray(params.year, 'always')
|
extraction.year = intoDbArray(params.year, 'always')
|
||||||
extraction.keywords = intoDbArray(params.keywords, 'always')
|
extraction.keywords = intoDbArray(params.keywords, 'always')
|
||||||
|
|
||||||
res.render('pages/extraction/edit-oss', {
|
res.render('pages/extraction/edit-oss', {
|
||||||
title: 'KAS + dokumenti',
|
title: req.t('Besedila'),
|
||||||
id: extractionId,
|
id: extractionId,
|
||||||
extraction,
|
extraction,
|
||||||
allPrimaryDomains,
|
allPrimaryDomains,
|
||||||
|
ossDocumentTypes,
|
||||||
stopTermsFiles
|
stopTermsFiles
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
@@ -93,7 +111,7 @@ extraction.edit = async (req, res) => {
|
|||||||
Extraction.fetchAllStopTermsFilesStats(extractionId)
|
Extraction.fetchAllStopTermsFilesStats(extractionId)
|
||||||
])
|
])
|
||||||
res.render('pages/extraction/edit-own', {
|
res.render('pages/extraction/edit-own', {
|
||||||
title: 'Besedila',
|
title: req.t('Besedila'),
|
||||||
id: extractionId,
|
id: extractionId,
|
||||||
extraction,
|
extraction,
|
||||||
extractionDocuments,
|
extractionDocuments,
|
||||||
@@ -107,7 +125,7 @@ extraction.updateOwn = async (req, res) => {
|
|||||||
await Extraction.update(extractionId, req.body.name)
|
await Extraction.update(extractionId, req.body.name)
|
||||||
|
|
||||||
// Reload page.
|
// Reload page.
|
||||||
res.redirect(`./${extractionId}`)
|
res.redirect(303, `./${extractionId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
extraction.docsEdit = async (req, res) => {
|
extraction.docsEdit = async (req, res) => {
|
||||||
@@ -117,6 +135,7 @@ extraction.docsEdit = async (req, res) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
res.render('pages/extraction/docs-edit', {
|
res.render('pages/extraction/docs-edit', {
|
||||||
|
title: req.t('Besedila'),
|
||||||
id: extractionId,
|
id: extractionId,
|
||||||
extractionDocuments
|
extractionDocuments
|
||||||
})
|
})
|
||||||
@@ -129,6 +148,7 @@ extraction.stopTermsEdit = async (req, res) => {
|
|||||||
)
|
)
|
||||||
|
|
||||||
res.render('pages/extraction/stop-terms-edit', {
|
res.render('pages/extraction/stop-terms-edit', {
|
||||||
|
title: req.t('Stop termini'),
|
||||||
id: extractionId,
|
id: extractionId,
|
||||||
stopTermsFiles
|
stopTermsFiles
|
||||||
})
|
})
|
||||||
@@ -145,6 +165,7 @@ extraction.listTermCandidates = async (req, res) => {
|
|||||||
const firstPageOfTermCandidates = termCandidates.slice(0, hitsPerPage)
|
const firstPageOfTermCandidates = termCandidates.slice(0, hitsPerPage)
|
||||||
|
|
||||||
res.render('pages/extraction/term-candidates', {
|
res.render('pages/extraction/term-candidates', {
|
||||||
|
title: req.t('Terminološki kandidati'),
|
||||||
extractionId,
|
extractionId,
|
||||||
termCandidatesJson,
|
termCandidatesJson,
|
||||||
firstPageOfTermCandidates,
|
firstPageOfTermCandidates,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
/* global __ */
|
||||||
|
|
||||||
const { getInstanceSetting } = require('../../models/helpers')
|
const { getInstanceSetting } = require('../../models/helpers')
|
||||||
const Dictionary = require('../../models/dictionary')
|
const Dictionary = require('../../models/dictionary')
|
||||||
const cache = require('../../models/cache')
|
const cache = require('../../models/cache')
|
||||||
@@ -40,7 +42,7 @@ const minEntriesEmailBookmark = {
|
|||||||
// Exports actions related to checking and acting on minimum entries per dictionary setting.
|
// Exports actions related to checking and acting on minimum entries per dictionary setting.
|
||||||
exports.minEntriesRequirementCheckAndAct = {
|
exports.minEntriesRequirementCheckAndAct = {
|
||||||
// Checks required criteria and sends notifications emails if required.
|
// Checks required criteria and sends notifications emails if required.
|
||||||
async onDelete(dictionaryId, appRef) {
|
async onDelete(dictionaryId, appRef, determinedLanguage) {
|
||||||
const minEntries = await getInstanceSetting('min_entries_per_dictionary')
|
const minEntries = await getInstanceSetting('min_entries_per_dictionary')
|
||||||
|
|
||||||
// Only proceed if a valid and positive minimum entries per dictionary setting is set.
|
// Only proceed if a valid and positive minimum entries per dictionary setting is set.
|
||||||
@@ -56,8 +58,8 @@ exports.minEntriesRequirementCheckAndAct = {
|
|||||||
if (!isBelowMinEntriesThreshold || wasEmailAlreadySent) return
|
if (!isBelowMinEntriesThreshold || wasEmailAlreadySent) return
|
||||||
|
|
||||||
// Prepare and send notification emails.
|
// Prepare and send notification emails.
|
||||||
const [nameSl, adminEmails, dictionariesAdminEmails] = await Promise.all([
|
const [name, adminEmails, dictionariesAdminEmails] = await Promise.all([
|
||||||
Dictionary.fetchName(dictionaryId),
|
Dictionary.fetchName(dictionaryId, determinedLanguage),
|
||||||
Dictionary.fetchAdminEmails(dictionaryId),
|
Dictionary.fetchAdminEmails(dictionaryId),
|
||||||
Dictionary.fetchDictionariesAdminEmails()
|
Dictionary.fetchDictionariesAdminEmails()
|
||||||
])
|
])
|
||||||
@@ -70,12 +72,13 @@ exports.minEntriesRequirementCheckAndAct = {
|
|||||||
const renderAsync = promisify(appRef.render.bind(appRef))
|
const renderAsync = promisify(appRef.render.bind(appRef))
|
||||||
const emailHtml = await renderAsync('email/dictionary-status-change', {
|
const emailHtml = await renderAsync('email/dictionary-status-change', {
|
||||||
type,
|
type,
|
||||||
nameSl
|
name
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO i18n - What language are the email title and content (we already have email translated)
|
||||||
await email.send({
|
await email.send({
|
||||||
to: allEmails,
|
to: allEmails,
|
||||||
subject: 'Obvestilo o številu gesel',
|
subject: __('Obvestilo o številu gesel'),
|
||||||
html: emailHtml
|
html: emailHtml
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -122,7 +125,7 @@ exports.statusChangeCheckAndAct = {
|
|||||||
dictionaryId,
|
dictionaryId,
|
||||||
isPublishedNew,
|
isPublishedNew,
|
||||||
oldDictStatus,
|
oldDictStatus,
|
||||||
nameSl,
|
name,
|
||||||
appRef,
|
appRef,
|
||||||
user
|
user
|
||||||
) {
|
) {
|
||||||
@@ -133,12 +136,11 @@ exports.statusChangeCheckAndAct = {
|
|||||||
const dictionariesAdminEmails =
|
const dictionariesAdminEmails =
|
||||||
await Dictionary.fetchDictionariesAdminEmails()
|
await Dictionary.fetchDictionariesAdminEmails()
|
||||||
const type = 'unpublish'
|
const type = 'unpublish'
|
||||||
|
|
||||||
await renderAndSendStatusChangeEmails(
|
await renderAndSendStatusChangeEmails(
|
||||||
appRef,
|
appRef,
|
||||||
type,
|
type,
|
||||||
user.email,
|
user.email,
|
||||||
nameSl,
|
name,
|
||||||
dictionariesAdminEmails
|
dictionariesAdminEmails
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -155,7 +157,7 @@ exports.statusChangeCheckAndAct = {
|
|||||||
appRef,
|
appRef,
|
||||||
type,
|
type,
|
||||||
user.email,
|
user.email,
|
||||||
nameSl,
|
name,
|
||||||
dictionariesAdminEmails
|
dictionariesAdminEmails
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -168,11 +170,12 @@ exports.statusChangeCheckAndAct = {
|
|||||||
statusNew,
|
statusNew,
|
||||||
statusOld,
|
statusOld,
|
||||||
appRef,
|
appRef,
|
||||||
|
determinedLanguage,
|
||||||
user
|
user
|
||||||
) {
|
) {
|
||||||
if (statusOld === 'reviewed' && statusNew !== 'reviewed') {
|
if (statusOld === 'reviewed' && statusNew !== 'reviewed') {
|
||||||
const [nameSl, adminEmails] = await Promise.all([
|
const [name, adminEmails] = await Promise.all([
|
||||||
Dictionary.fetchName(dictionaryId),
|
Dictionary.fetchName(dictionaryId, determinedLanguage),
|
||||||
Dictionary.fetchAdminEmails(dictionaryId)
|
Dictionary.fetchAdminEmails(dictionaryId)
|
||||||
])
|
])
|
||||||
const type = 'status'
|
const type = 'status'
|
||||||
@@ -181,7 +184,7 @@ exports.statusChangeCheckAndAct = {
|
|||||||
appRef,
|
appRef,
|
||||||
type,
|
type,
|
||||||
user.email,
|
user.email,
|
||||||
nameSl,
|
name,
|
||||||
adminEmails
|
adminEmails
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -193,19 +196,20 @@ async function renderAndSendStatusChangeEmails(
|
|||||||
appRef,
|
appRef,
|
||||||
type,
|
type,
|
||||||
changerEmail,
|
changerEmail,
|
||||||
nameSl,
|
name,
|
||||||
targetEmails
|
targetEmails
|
||||||
) {
|
) {
|
||||||
const renderAsync = promisify(appRef.render.bind(appRef))
|
const renderAsync = promisify(appRef.render.bind(appRef))
|
||||||
const emailHtml = await renderAsync('email/dictionary-status-change', {
|
const emailHtml = await renderAsync('email/dictionary-status-change', {
|
||||||
type,
|
type,
|
||||||
changerEmail,
|
changerEmail,
|
||||||
nameSl
|
name
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO i18n - What language are the email title and content (we already have email translated)
|
||||||
await email.send({
|
await email.send({
|
||||||
to: targetEmails,
|
to: targetEmails,
|
||||||
subject: 'Sprememba stanja slovarja',
|
subject: __('Sprememba stanja slovarja'),
|
||||||
html: emailHtml
|
html: emailHtml
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,25 +4,27 @@ const { getInstanceSetting } = require('../../models/helpers')
|
|||||||
|
|
||||||
const helper = {}
|
const helper = {}
|
||||||
|
|
||||||
helper.initialize = async () => {
|
helper.initialize = async determinedLanguage => {
|
||||||
const initializers = {}
|
const initializers = {}
|
||||||
|
|
||||||
// TODO Once english language is implemented, gather selected language (sl/en) from request ~ (cookies?)
|
|
||||||
const language = 'name_sl'
|
|
||||||
|
|
||||||
// TODO Consider parallelizing following queries. Single vs pooled clients?
|
// TODO Consider parallelizing following queries. Single vs pooled clients?
|
||||||
initializers.allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
|
initializers.allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains(
|
||||||
initializers.sourceLanguages = await Dictionary.fetchAllLanguages(language)
|
determinedLanguage
|
||||||
initializers.targetLanguages = initializers.sourceLanguages.filter(
|
)
|
||||||
|
const allLanguages = await Dictionary.fetchAllLanguages(determinedLanguage)
|
||||||
|
initializers.sourceLanguages = allLanguages
|
||||||
|
initializers.targetLanguages = allLanguages.filter(
|
||||||
// drop slovene language
|
// drop slovene language
|
||||||
l => l.id !== 32
|
l => l.id !== 32
|
||||||
)
|
)
|
||||||
|
|
||||||
initializers.allDictionaryNames = await Dictionary.fetchAll()
|
initializers.allDictionaryNames = await Dictionary.fetchAll(
|
||||||
|
determinedLanguage
|
||||||
|
)
|
||||||
|
|
||||||
initializers.portals = []
|
initializers.portals = []
|
||||||
initializers.portals.push({
|
initializers.portals.push({
|
||||||
name: await getInstanceSetting('portal_name'),
|
name: await getInstanceSetting('portal_name_sl'),
|
||||||
code: await getInstanceSetting('portal_code')
|
code: await getInstanceSetting('portal_code')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,37 +1,45 @@
|
|||||||
|
const { promisify } = require('util')
|
||||||
const Entry = require('../models/entry')
|
const Entry = require('../models/entry')
|
||||||
const { getInstanceSetting } = require('../models/helpers')
|
const { getInstanceSetting } = require('../models/helpers')
|
||||||
const Dictionary = require('../models/dictionary')
|
const Dictionary = require('../models/dictionary')
|
||||||
const Comment = require('../models/comment')
|
const Comment = require('../models/comment')
|
||||||
const { searchEntryIndex } = require('../models/search-engine')
|
const {
|
||||||
|
searchEntryIndex,
|
||||||
|
searchConsultancyEntryIndex
|
||||||
|
} = require('../models/search-engine')
|
||||||
const { intoDbArray } = require('../models/helpers')
|
const { intoDbArray } = require('../models/helpers')
|
||||||
const {
|
const {
|
||||||
prepareEntries,
|
prepareEntries,
|
||||||
prepareAggregation,
|
prepareAggregation,
|
||||||
prepareSeachFilterData
|
prepareSeachFilterData,
|
||||||
|
prepareConsultancyEntries
|
||||||
} = require('../models/helpers/search')
|
} = require('../models/helpers/search')
|
||||||
|
const email = require('../models/email')
|
||||||
const generateQuery = require('../models/helpers/search/generate-query')
|
const generateQuery = require('../models/helpers/search/generate-query')
|
||||||
const { DEFAULT_HITS_PER_PAGE } = require('../config/settings')
|
const { DEFAULT_HITS_PER_PAGE } = require('../config/settings')
|
||||||
const SFDSuggestionImporter = require('./helpers/search-filter-data-suggestion-importer')
|
const SFDSuggestionImporter = require('./helpers/search-filter-data-suggestion-importer')
|
||||||
const User = require('../models/user')
|
const User = require('../models/user')
|
||||||
|
const { capitalize } = require('../utils')
|
||||||
|
|
||||||
// TODO Luka (note to self): Measure performance, consider caching.
|
// TODO Luka (note to self): Measure performance, consider caching.
|
||||||
exports.index = async (req, res) => {
|
exports.index = async (req, res) => {
|
||||||
|
const { language } = req
|
||||||
const {
|
const {
|
||||||
allPrimaryDomains,
|
allPrimaryDomains,
|
||||||
sourceLanguages,
|
sourceLanguages,
|
||||||
targetLanguages,
|
targetLanguages,
|
||||||
allDictionaryNames,
|
allDictionaryNames,
|
||||||
portals
|
portals
|
||||||
} = await SFDSuggestionImporter.initialize()
|
} = await SFDSuggestionImporter.initialize(req.determinedLanguage)
|
||||||
|
|
||||||
const englishLanguageEnabled = false // dummy variable for future edit
|
|
||||||
|
|
||||||
const latestDicts = await Dictionary.fetchLatest3DictsByPublishDate(
|
const latestDicts = await Dictionary.fetchLatest3DictsByPublishDate(
|
||||||
englishLanguageEnabled
|
req.determinedLanguage
|
||||||
)
|
)
|
||||||
|
|
||||||
const portalName = await getInstanceSetting('portal_name')
|
const portalName = await getInstanceSetting(`portal_name_${language}`)
|
||||||
const portalDescription = await getInstanceSetting('portal_description')
|
const portalDescription = await getInstanceSetting(
|
||||||
|
`portal_description_${language}`
|
||||||
|
)
|
||||||
|
|
||||||
const isRoot = true
|
const isRoot = true
|
||||||
|
|
||||||
@@ -51,15 +59,17 @@ exports.index = async (req, res) => {
|
|||||||
exports.search = async (req, res) => {
|
exports.search = async (req, res) => {
|
||||||
const searchString = req.query.q?.trim()
|
const searchString = req.query.q?.trim()
|
||||||
|
|
||||||
if (!searchString) return res.redirect('/')
|
if (!searchString) return res.redirect(303, '/')
|
||||||
|
|
||||||
const {
|
const title = req.t('Iskanje')
|
||||||
|
|
||||||
|
let {
|
||||||
allPrimaryDomains,
|
allPrimaryDomains,
|
||||||
sourceLanguages,
|
sourceLanguages,
|
||||||
targetLanguages,
|
targetLanguages,
|
||||||
allDictionaryNames,
|
allDictionaryNames,
|
||||||
portals
|
portals
|
||||||
} = await SFDSuggestionImporter.initialize()
|
} = await SFDSuggestionImporter.initialize(req.determinedLanguage)
|
||||||
|
|
||||||
const filters = {
|
const filters = {
|
||||||
sourceLanguages: intoDbArray(req.query.sl, 'always'),
|
sourceLanguages: intoDbArray(req.query.sl, 'always'),
|
||||||
@@ -81,6 +91,32 @@ exports.search = async (req, res) => {
|
|||||||
true
|
true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Consultancy
|
||||||
|
|
||||||
|
const hitsQueryConsultancy = generateQuery.consultancy(
|
||||||
|
searchString,
|
||||||
|
{
|
||||||
|
status: 'published',
|
||||||
|
primaryDomain: filters.primaryDomains[0]
|
||||||
|
},
|
||||||
|
hitsPerPage,
|
||||||
|
page
|
||||||
|
)
|
||||||
|
|
||||||
|
const hitsConsultancy = await searchConsultancyEntryIndex(
|
||||||
|
hitsQueryConsultancy
|
||||||
|
)
|
||||||
|
|
||||||
|
const consultancyHits = hitsConsultancy.body.hits.total.value
|
||||||
|
|
||||||
|
const consultancyURL = `/svetovanje/iskanje?q=${searchString}${
|
||||||
|
filters.primaryDomains.length > 0 ? '&pd=' + filters.primaryDomains[0] : '' // consultancy filtering only supports one domain
|
||||||
|
}`
|
||||||
|
|
||||||
|
// console.log(entries)
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
const [hits, aggregationRaw] = await Promise.all([
|
const [hits, aggregationRaw] = await Promise.all([
|
||||||
searchEntryIndex(hitsQuery),
|
searchEntryIndex(hitsQuery),
|
||||||
searchEntryIndex(aggregateQuery)
|
searchEntryIndex(aggregateQuery)
|
||||||
@@ -96,7 +132,10 @@ exports.search = async (req, res) => {
|
|||||||
// Similar to search query without any extra filters, this is required for modal to diplay ALL res.
|
// Similar to search query without any extra filters, this is required for modal to diplay ALL res.
|
||||||
// const allAggregation = await indexAllResultsNoFiltering(req, res)
|
// const allAggregation = await indexAllResultsNoFiltering(req, res)
|
||||||
|
|
||||||
const aggregation = await prepareAggregation(aggregationRaw)
|
const aggregation = await prepareAggregation(
|
||||||
|
aggregationRaw,
|
||||||
|
req.determinedLanguage
|
||||||
|
)
|
||||||
const searchFilterData = prepareSeachFilterData(aggregation, filters)
|
const searchFilterData = prepareSeachFilterData(aggregation, filters)
|
||||||
|
|
||||||
// TODO Luka (note to self): Consider reworking Miha's logic below. It's hacky and possibly error prone.
|
// TODO Luka (note to self): Consider reworking Miha's logic below. It's hacky and possibly error prone.
|
||||||
@@ -143,6 +182,30 @@ exports.search = async (req, res) => {
|
|||||||
sources: false
|
sources: false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Keep selected items on refresh
|
||||||
|
sourceLanguages = addSelectedIdentifierToFilters(
|
||||||
|
sourceLanguages,
|
||||||
|
filters.sourceLanguages
|
||||||
|
)
|
||||||
|
|
||||||
|
targetLanguages = addSelectedIdentifierToFilters(
|
||||||
|
targetLanguages,
|
||||||
|
filters.targetLanguages
|
||||||
|
)
|
||||||
|
allPrimaryDomains = addSelectedIdentifierToFilters(
|
||||||
|
allPrimaryDomains,
|
||||||
|
filters.primaryDomains
|
||||||
|
)
|
||||||
|
|
||||||
|
allDictionaryNames = addSelectedIdentifierToFilters(
|
||||||
|
allDictionaryNames,
|
||||||
|
filters.dictionaries
|
||||||
|
)
|
||||||
|
|
||||||
|
/// //////////////
|
||||||
|
|
||||||
|
portals = addSelectedIdentifierToPortals(portals, filters.sources)
|
||||||
|
|
||||||
if (count < 1) {
|
if (count < 1) {
|
||||||
return res.render('pages/search/no-results', {
|
return res.render('pages/search/no-results', {
|
||||||
allPrimaryDomains,
|
allPrimaryDomains,
|
||||||
@@ -154,6 +217,8 @@ exports.search = async (req, res) => {
|
|||||||
entriesByCategory,
|
entriesByCategory,
|
||||||
searchFilterData,
|
searchFilterData,
|
||||||
disabledSideMenuFilters,
|
disabledSideMenuFilters,
|
||||||
|
consultancyHits,
|
||||||
|
consultancyURL,
|
||||||
// allAggregation,
|
// allAggregation,
|
||||||
numberOfAllHits,
|
numberOfAllHits,
|
||||||
numberOfAllPages,
|
numberOfAllPages,
|
||||||
@@ -161,6 +226,74 @@ exports.search = async (req, res) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// duplicated code below for filtering foreignentries, optimize later
|
||||||
|
// Below code filters target languages based on source and target filters for each category
|
||||||
|
const categoriesLabels = Object.keys(entriesByCategory)
|
||||||
|
for (
|
||||||
|
let categoryIndex = 0;
|
||||||
|
categoryIndex < categoriesLabels.length;
|
||||||
|
categoryIndex++
|
||||||
|
) {
|
||||||
|
entriesByCategory[categoriesLabels[categoryIndex]] = entriesByCategory[
|
||||||
|
categoriesLabels[categoryIndex]
|
||||||
|
].map(entry => {
|
||||||
|
entry.foreignEntries = entry.foreignEntries?.filter(foreignEntry => {
|
||||||
|
if (filters.targetLanguages.length > 0) {
|
||||||
|
if (aggregation.sourceLanguages.length === 1) {
|
||||||
|
filters.sourceLanguages = [aggregation.sourceLanguages[0].id]
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
filters.targetLanguages.includes(`${foreignEntry.lang.id}`) ||
|
||||||
|
filters.sourceLanguages?.includes(`${foreignEntry.lang.id}`)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return entry
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable target languages logic
|
||||||
|
|
||||||
|
if (
|
||||||
|
filters.sourceLanguages.length > 1 || // more or equal than 2 source languages
|
||||||
|
(filters.sourceLanguages.length < 1 && // No filters, but more than 1 source language filters returned
|
||||||
|
aggregation.sourceLanguages.length > 1) ||
|
||||||
|
(aggregation.targetLanguages.length === 1 &&
|
||||||
|
aggregation.sourceLanguages.length === 1 &&
|
||||||
|
aggregation.targetLanguages[0].id === aggregation.sourceLanguages[0].id) //
|
||||||
|
) {
|
||||||
|
// predicate 1 Disable if 2 or more filters in Source languages are selected
|
||||||
|
// predicate 2 If none are selected, then check if displayed filters are more than 2
|
||||||
|
aggregation.targetLanguages = []
|
||||||
|
filters.targetLanguages = []
|
||||||
|
targetLanguages = []
|
||||||
|
searchFilterData.targetLanguages = []
|
||||||
|
disabledSideMenuFilters.targetLanguages = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove source language from target language
|
||||||
|
filters.sourceLanguages.forEach(entry => {
|
||||||
|
if (
|
||||||
|
aggregation.targetLanguages.filter(toFilter => toFilter.id === entry)
|
||||||
|
.length > 0
|
||||||
|
) {
|
||||||
|
aggregation.targetLanguages = aggregation.targetLanguages.filter(
|
||||||
|
toFilter => toFilter.id !== entry
|
||||||
|
)
|
||||||
|
searchFilterData.targetLanguages =
|
||||||
|
searchFilterData.targetLanguages.filter(
|
||||||
|
toFilter => toFilter.id !== entry
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if (aggregation.targetLanguages.length < 0) {
|
||||||
|
searchFilterData.targetLanguages = []
|
||||||
|
disabledSideMenuFilters.targetLanguages = true
|
||||||
|
}
|
||||||
|
|
||||||
// TODO Add a page title?
|
// TODO Add a page title?
|
||||||
res.render('pages/search/results', {
|
res.render('pages/search/results', {
|
||||||
allPrimaryDomains,
|
allPrimaryDomains,
|
||||||
@@ -172,15 +305,19 @@ exports.search = async (req, res) => {
|
|||||||
entriesByCategory,
|
entriesByCategory,
|
||||||
searchFilterData,
|
searchFilterData,
|
||||||
disabledSideMenuFilters,
|
disabledSideMenuFilters,
|
||||||
|
consultancyHits,
|
||||||
|
consultancyURL,
|
||||||
// allAggregation,
|
// allAggregation,
|
||||||
numberOfAllHits,
|
numberOfAllHits,
|
||||||
numberOfAllPages,
|
numberOfAllPages,
|
||||||
page
|
page,
|
||||||
|
title
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.entryDetails = async (req, res) => {
|
exports.entryDetails = async (req, res) => {
|
||||||
const termId = req.params.entryId
|
const termId = req.params.entryId
|
||||||
|
const title = req.t('Termin')
|
||||||
|
|
||||||
/* const [entry, domainLabels] = await Promise.all([
|
/* const [entry, domainLabels] = await Promise.all([
|
||||||
Entry.fetchFullWithOrderedForeignLanguages(termId),
|
Entry.fetchFullWithOrderedForeignLanguages(termId),
|
||||||
@@ -188,12 +325,16 @@ exports.entryDetails = async (req, res) => {
|
|||||||
]) */
|
]) */
|
||||||
|
|
||||||
const entry = await Entry.fetchFullWithOrderedForeignLanguages(termId)
|
const entry = await Entry.fetchFullWithOrderedForeignLanguages(termId)
|
||||||
|
|
||||||
/* const entryData = {
|
/* const entryData = {
|
||||||
entry
|
entry
|
||||||
// allDomainLabelsJoined: domainLabels.map(e => e.name).join(', ')
|
// allDomainLabelsJoined: domainLabels.map(e => e.name).join(', ')
|
||||||
} */
|
} */
|
||||||
|
|
||||||
|
// If external url, just redirect
|
||||||
|
if (entry.external_url) {
|
||||||
|
return res.redirect(303, entry.external_url)
|
||||||
|
}
|
||||||
|
|
||||||
// unnecessary legacy assigment, refactor when time is available
|
// unnecessary legacy assigment, refactor when time is available
|
||||||
const entryData = entry
|
const entryData = entry
|
||||||
|
|
||||||
@@ -203,12 +344,15 @@ exports.entryDetails = async (req, res) => {
|
|||||||
targetLanguages,
|
targetLanguages,
|
||||||
allDictionaryNames,
|
allDictionaryNames,
|
||||||
portals
|
portals
|
||||||
} = await SFDSuggestionImporter.initialize()
|
} = await SFDSuggestionImporter.initialize(req.determinedLanguage)
|
||||||
|
|
||||||
const [dictStruct, dictionaryData, selectedDomainLabelsForEntry] =
|
const [dictStruct, dictionaryData, selectedDomainLabelsForEntry] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
Dictionary.fetchDictionaryWithEditStructure(entry.dictionary_id),
|
Dictionary.fetchDictionaryWithEditStructure(entry.dictionary_id),
|
||||||
Dictionary.fetchDictionaryBasicInfo(entry.dictionary_id),
|
Dictionary.fetchDictionaryBasicInfo(
|
||||||
|
entry.dictionary_id,
|
||||||
|
req.determinedLanguage
|
||||||
|
),
|
||||||
Entry.fetchDomainLabels(termId)
|
Entry.fetchDomainLabels(termId)
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -228,8 +372,8 @@ exports.entryDetails = async (req, res) => {
|
|||||||
///
|
///
|
||||||
|
|
||||||
// check if it is a local dictionary
|
// check if it is a local dictionary
|
||||||
if (!dictionaryData.portalname && !dictionaryData.portalcode) {
|
if (!dictionaryData.portalnamesl && !dictionaryData.portalcode) {
|
||||||
dictionaryData[0].portalname = await getInstanceSetting('portal_name')
|
dictionaryData[0].portalname = await getInstanceSetting('portal_name_sl')
|
||||||
dictionaryData[0].portalcode = await getInstanceSetting('portal_code')
|
dictionaryData[0].portalcode = await getInstanceSetting('portal_code')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -258,11 +402,11 @@ exports.entryDetails = async (req, res) => {
|
|||||||
// struct data contains important data and re-maps for unification (maybe refactor later)
|
// struct data contains important data and re-maps for unification (maybe refactor later)
|
||||||
const structData = {
|
const structData = {
|
||||||
termId: termId,
|
termId: termId,
|
||||||
prevWindowTitle: 'Iskanje',
|
prevWindowTitle: req.t('Iskanje'),
|
||||||
prevHref: '/iskanje',
|
prevHref: '/iskanje',
|
||||||
portalCode: dictionaryData[0].portalcode,
|
portalCode: dictionaryData[0].portalcode,
|
||||||
portalName: dictionaryData[0].portalname,
|
portalName: dictionaryData[0].portalname,
|
||||||
dictName: structure.nameSl,
|
dictName: structure[`name${capitalize(req.determinedLanguage)}`],
|
||||||
dictHref: `/slovarji/${structure.id}/o-slovarju?sentFromEntryId=${termId}`,
|
dictHref: `/slovarji/${structure.id}/o-slovarju?sentFromEntryId=${termId}`,
|
||||||
fullAuthorName: reducedData.author ? reducedData.author.join(', ') : '',
|
fullAuthorName: reducedData.author ? reducedData.author.join(', ') : '',
|
||||||
areas: dictionaryData[0].domain_primary,
|
areas: dictionaryData[0].domain_primary,
|
||||||
@@ -270,13 +414,14 @@ exports.entryDetails = async (req, res) => {
|
|||||||
languages: reducedData.languages ? reducedData.languages.join(', ') : ''
|
languages: reducedData.languages ? reducedData.languages.join(', ') : ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO I18n
|
||||||
if (reducedData.author) {
|
if (reducedData.author) {
|
||||||
if (reducedData.author.length > 2) {
|
if (reducedData.author.length > 2) {
|
||||||
structData.authorLabel = 'Avtorji'
|
structData.authorLabel = req.t('Avtorji')
|
||||||
} else if (reducedData.author.length === 2) {
|
} else if (reducedData.author.length === 2) {
|
||||||
structData.authorLabel = 'Avtorja'
|
structData.authorLabel = req.t('Avtorja')
|
||||||
} else if (reducedData.author.length === 1) {
|
} else if (reducedData.author.length === 1) {
|
||||||
structData.authorLabel = 'Avtor'
|
structData.authorLabel = req.t('Avtor')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,11 +445,13 @@ exports.entryDetails = async (req, res) => {
|
|||||||
entry.foreignEntries[idx] = await
|
entry.foreignEntries[idx] = await
|
||||||
}) */
|
}) */
|
||||||
|
|
||||||
const langs = await Dictionary.fetchLanguages(entryData.dictionary_id)
|
const langs = await Dictionary.fetchLanguages(
|
||||||
|
entryData.dictionary_id,
|
||||||
|
req.determinedLanguage
|
||||||
|
)
|
||||||
entryData.foreign_entries.forEach((val, idx) => {
|
entryData.foreign_entries.forEach((val, idx) => {
|
||||||
try {
|
try {
|
||||||
entry.foreign_entries[idx].name_sl = langs[idx].nameSl
|
entry.foreign_entries[idx].name = langs[idx].name
|
||||||
entry.foreign_entries[idx].name_en = langs[idx].nameEn
|
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -321,27 +468,91 @@ exports.entryDetails = async (req, res) => {
|
|||||||
selectedDomainLabelsForEntryString,
|
selectedDomainLabelsForEntryString,
|
||||||
numberOfAllPages,
|
numberOfAllPages,
|
||||||
comments,
|
comments,
|
||||||
commentCount
|
commentCount,
|
||||||
|
title
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.myProfile = async (req, res) => {
|
exports.myProfile = async (req, res) => {
|
||||||
res.render('pages/profile/my-profile', { title: 'Moj račun' })
|
res.render('pages/profile/my-profile', { title: req.t('Osnovni podatki') })
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.deleteProfile = async (req, res) => {
|
||||||
|
res.render('pages/profile/delete-profile', { title: req.t('Izbriši račun') })
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.changePassword = async (req, res) => {
|
exports.changePassword = async (req, res) => {
|
||||||
res.render('pages/profile/change-password', { title: 'Spremeni geslo' })
|
res.render('pages/profile/change-password', {
|
||||||
|
title: req.t('Spremeni geslo')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.resetPassword = async (req, res) => {
|
||||||
|
if (req.user) return res.redirect(303, '/spremeni-geslo')
|
||||||
|
|
||||||
|
const { token } = req.query
|
||||||
|
if (!token) return res.redirect(303, '/')
|
||||||
|
|
||||||
|
const isTokenValid = await User.isResetPasswordTokenValid(token)
|
||||||
|
|
||||||
|
res.render('pages/reset-password/reset-password', {
|
||||||
|
title: 'Ponastavitev gesla',
|
||||||
|
isTokenValid,
|
||||||
|
token
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.changeEmail = async (req, res) => {
|
||||||
|
const { token } = req.query
|
||||||
|
if (!token) return res.redirect(303, '/')
|
||||||
|
|
||||||
|
const user = await User.changeEmailWithToken(token, req.t)
|
||||||
|
|
||||||
|
const renderAsync = promisify(req.app.render.bind(req.app))
|
||||||
|
const emailHtml = await renderAsync(
|
||||||
|
`email/user-change-email-success_${req.language}`,
|
||||||
|
{
|
||||||
|
username: user.username
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await email.send({
|
||||||
|
to: user.email,
|
||||||
|
subject: req.t('Sprememba elektronskega naslova - uspeh'),
|
||||||
|
html: emailHtml
|
||||||
|
})
|
||||||
|
|
||||||
|
req.flash('info', req.t('Uspešno ste spremenili svoj elektronski naslov.'))
|
||||||
|
if (req.user) return res.redirect(303, '/moj-racun')
|
||||||
|
res.redirect(303, '/')
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.userSettings = async (req, res) => {
|
exports.userSettings = async (req, res) => {
|
||||||
const hitsPerPageArr = await User.fetchAllowedHitsPerPage()
|
const hitsPerPageArr = await User.fetchAllowedHitsPerPage()
|
||||||
res.render('pages/profile/change-profile-settings', {
|
res.render('pages/profile/change-profile-settings', {
|
||||||
title: 'Nastavitve računa',
|
title: req.t('Nastavitve računa'),
|
||||||
hitsPerPageArr,
|
hitsPerPageArr,
|
||||||
hitsForUser: req.user?.hitsPerPage
|
hitsForUser: req.user?.hitsPerPage
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.changeUserLanguage = async (req, res) => {
|
||||||
|
const { languageCode } = req.params
|
||||||
|
const validCodes = ['sl', 'en']
|
||||||
|
|
||||||
|
if (!validCodes.includes(languageCode)) {
|
||||||
|
throw Error(`Invalid language: ${languageCode}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user } = req
|
||||||
|
if (user) {
|
||||||
|
await User.updateLanguage(user.id, languageCode)
|
||||||
|
} else {
|
||||||
|
req.session.language = languageCode
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect(303, '/')
|
||||||
|
}
|
||||||
|
|
||||||
function mergeDomains(
|
function mergeDomains(
|
||||||
domainList,
|
domainList,
|
||||||
aggregationFn = (acc, n) => {
|
aggregationFn = (acc, n) => {
|
||||||
@@ -384,3 +595,23 @@ function filterResults(entries) {
|
|||||||
|
|
||||||
return [termLst, ftermLst, otherLst]
|
return [termLst, ftermLst, otherLst]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function addSelectedIdentifierToFilters(source, selectedIDs) {
|
||||||
|
return source.map(entry => {
|
||||||
|
if (selectedIDs.includes(`${entry.id}`)) {
|
||||||
|
entry.selected = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function addSelectedIdentifierToPortals(source, selectedIDs) {
|
||||||
|
return source.map(entry => {
|
||||||
|
if (selectedIDs.includes(`${entry.code}`)) {
|
||||||
|
entry.selected = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ portal.instanceSettings = async (req, res) => {
|
|||||||
const portal = await Portal.fetchInstanceSettings()
|
const portal = await Portal.fetchInstanceSettings()
|
||||||
|
|
||||||
res.render('pages/admin/portal', {
|
res.render('pages/admin/portal', {
|
||||||
title: 'Nastavitve portala',
|
title: req.t('Nastavitve portala'),
|
||||||
portal
|
portal
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -17,14 +17,14 @@ portal.updateInstaceSettings = async (req, res) => {
|
|||||||
const payload = req.body
|
const payload = req.body
|
||||||
await Portal.updateInstaceSettings(payload)
|
await Portal.updateInstaceSettings(payload)
|
||||||
await clearCachedInstanceSettings()
|
await clearCachedInstanceSettings()
|
||||||
res.redirect('/admin/nastavitve/portal')
|
res.redirect(303, '/admin/nastavitve/portal')
|
||||||
}
|
}
|
||||||
|
|
||||||
portal.instanceDictSettings = async (req, res) => {
|
portal.instanceDictSettings = async (req, res) => {
|
||||||
const dictionary = await Portal.fetchInstanceDictSettings()
|
const dictionary = await Portal.fetchInstanceDictSettings()
|
||||||
|
|
||||||
res.render('pages/admin/settings-dictionaries', {
|
res.render('pages/admin/settings-dictionaries', {
|
||||||
title: 'Nastavitve slovarjev',
|
title: req.t('Nastavitve slovarjev'),
|
||||||
dictionary
|
dictionary
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -33,13 +33,13 @@ portal.updateInstanceDictSettings = async (req, res) => {
|
|||||||
const payload = req.body
|
const payload = req.body
|
||||||
await Portal.updateInstaceDictSettings(payload)
|
await Portal.updateInstaceDictSettings(payload)
|
||||||
await clearCachedInstanceSettings()
|
await clearCachedInstanceSettings()
|
||||||
res.redirect('/admin/nastavitve/slovarji')
|
res.redirect(303, '/admin/nastavitve/slovarji')
|
||||||
}
|
}
|
||||||
|
|
||||||
portal.instanceConsultancySettings = async (req, res) => {
|
portal.instanceConsultancySettings = async (req, res) => {
|
||||||
const consultancy = await Portal.fetchInstanceConsultancySettings()
|
const consultancy = await Portal.fetchInstanceConsultancySettings()
|
||||||
res.render('pages/admin/portal-consultancy-settings', {
|
res.render('pages/admin/portal-consultancy-settings', {
|
||||||
title: 'Nastavitve svetovalnice',
|
title: req.t('Nastavitve svetovalnice'),
|
||||||
consultancy
|
consultancy
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -48,12 +48,12 @@ portal.updateInstanceConusltacySettings = async (req, res) => {
|
|||||||
const payload = req.body
|
const payload = req.body
|
||||||
await Portal.updateInstaceConsultancySettings(payload)
|
await Portal.updateInstaceConsultancySettings(payload)
|
||||||
await clearCachedInstanceSettings()
|
await clearCachedInstanceSettings()
|
||||||
res.redirect('/admin/nastavitve/svetovalnica')
|
res.redirect(303, '/admin/nastavitve/svetovalnica')
|
||||||
}
|
}
|
||||||
|
|
||||||
portal.new = async (req, res) => {
|
portal.new = async (req, res) => {
|
||||||
res.render('pages/admin/new-connection', {
|
res.render('pages/admin/new-connection', {
|
||||||
title: 'Nova povezava'
|
title: req.t('Nova povezava')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ portal.list = async (req, res) => {
|
|||||||
const allLinkedPortals = await Portal.fetchAll()
|
const allLinkedPortals = await Portal.fetchAll()
|
||||||
|
|
||||||
res.render('pages/admin/connections-list', {
|
res.render('pages/admin/connections-list', {
|
||||||
title: 'Seznam povezav',
|
title: req.t('Seznam povezav'),
|
||||||
allLinkedPortals
|
allLinkedPortals
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -70,14 +70,17 @@ portal.fetchPortal = async (req, res) => {
|
|||||||
const portalId = req.params.portalId
|
const portalId = req.params.portalId
|
||||||
const portal = await Portal.fetchPortal(portalId)
|
const portal = await Portal.fetchPortal(portalId)
|
||||||
|
|
||||||
res.render('pages/admin/portal-edit', { title: 'Uredi povezavo', portal })
|
res.render('pages/admin/portal-edit', {
|
||||||
|
title: req.t('Uredi povezavo'),
|
||||||
|
portal
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
portal.updatePortal = async (req, res) => {
|
portal.updatePortal = async (req, res) => {
|
||||||
const portalId = req.params.portalId
|
const portalId = req.params.portalId
|
||||||
const payload = req.body
|
const payload = req.body
|
||||||
await Portal.update(portalId, payload)
|
await Portal.update(portalId, payload)
|
||||||
res.redirect('/admin/povezave/seznam')
|
res.redirect(303, '/admin/povezave/seznam')
|
||||||
}
|
}
|
||||||
|
|
||||||
portal.fetchSelectedLinkedDictionaries = async (req, res) => {
|
portal.fetchSelectedLinkedDictionaries = async (req, res) => {
|
||||||
@@ -88,7 +91,7 @@ portal.fetchSelectedLinkedDictionaries = async (req, res) => {
|
|||||||
await Portal.fetchSelectedLinkedDictionaries(linkedId, resultsPerPage, 1)
|
await Portal.fetchSelectedLinkedDictionaries(linkedId, resultsPerPage, 1)
|
||||||
|
|
||||||
res.render('pages/admin/portal-list-dict', {
|
res.render('pages/admin/portal-list-dict', {
|
||||||
title: 'Slovarji portala',
|
title: req.t('Slovarji portala'),
|
||||||
linkedId,
|
linkedId,
|
||||||
numberOfAllPages,
|
numberOfAllPages,
|
||||||
results
|
results
|
||||||
@@ -98,7 +101,7 @@ portal.fetchSelectedLinkedDictionaries = async (req, res) => {
|
|||||||
portal.updateSelectedDictionaries = async (req, res) => {
|
portal.updateSelectedDictionaries = async (req, res) => {
|
||||||
const linkedId = req.params.portalId
|
const linkedId = req.params.portalId
|
||||||
await Portal.updateSelectedDictionaries(linkedId, req.body)
|
await Portal.updateSelectedDictionaries(linkedId, req.body)
|
||||||
res.redirect('back')
|
res.redirect(303, 'back')
|
||||||
}
|
}
|
||||||
|
|
||||||
portal.fetchAllLinkedDictionaries = async (req, res) => {
|
portal.fetchAllLinkedDictionaries = async (req, res) => {
|
||||||
@@ -108,7 +111,7 @@ portal.fetchAllLinkedDictionaries = async (req, res) => {
|
|||||||
await Portal.fetchAllLinkedDictionaries(resultsPerPage, 1)
|
await Portal.fetchAllLinkedDictionaries(resultsPerPage, 1)
|
||||||
|
|
||||||
res.render('pages/admin/portals-all-linked-dictionaries', {
|
res.render('pages/admin/portals-all-linked-dictionaries', {
|
||||||
title: 'Povezani',
|
title: req.t('Povezani slovarji'),
|
||||||
numberOfAllPages,
|
numberOfAllPages,
|
||||||
results
|
results
|
||||||
})
|
})
|
||||||
@@ -116,7 +119,7 @@ portal.fetchAllLinkedDictionaries = async (req, res) => {
|
|||||||
|
|
||||||
portal.updateAllDictionaries = async (req, res) => {
|
portal.updateAllDictionaries = async (req, res) => {
|
||||||
await Portal.updateAllDictionaries(req.body)
|
await Portal.updateAllDictionaries(req.body)
|
||||||
res.redirect('back')
|
res.redirect(303, 'back')
|
||||||
}
|
}
|
||||||
|
|
||||||
portal.comments = async (req, res) => {
|
portal.comments = async (req, res) => {
|
||||||
@@ -130,7 +133,7 @@ portal.comments = async (req, res) => {
|
|||||||
1
|
1
|
||||||
)
|
)
|
||||||
res.render('pages/admin/comments', {
|
res.render('pages/admin/comments', {
|
||||||
title: 'Komentarji',
|
title: req.t('Komentarji'),
|
||||||
numberOfAllPages,
|
numberOfAllPages,
|
||||||
dictionary: { id: req.params.dictionaryId },
|
dictionary: { id: req.params.dictionaryId },
|
||||||
comments
|
comments
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ const { promisify } = require('util')
|
|||||||
const passport = require('passport')
|
const passport = require('passport')
|
||||||
const User = require('../models/user')
|
const User = require('../models/user')
|
||||||
const email = require('../models/email')
|
const email = require('../models/email')
|
||||||
|
const { logout: logoutUser } = require('../middleware/user')
|
||||||
const { origin } = require('../config/keys')
|
const { origin } = require('../config/keys')
|
||||||
const { rememberMeCookieSettings } = require('../config/settings')
|
const { rememberMeCookieSettings } = require('../config/settings')
|
||||||
const RandomBytesAsync = promisify(randomBytes)
|
const RandomBytesAsync = promisify(randomBytes)
|
||||||
@@ -12,7 +13,14 @@ const user = {}
|
|||||||
|
|
||||||
user.register = async (req, res) => {
|
user.register = async (req, res) => {
|
||||||
// TODO Add validation.
|
// TODO Add validation.
|
||||||
const userId = await User.create(req.body)
|
|
||||||
|
const userId = await User.create(
|
||||||
|
{
|
||||||
|
...req.body,
|
||||||
|
language: req.session.language
|
||||||
|
},
|
||||||
|
req.t
|
||||||
|
)
|
||||||
const activationToken = (await RandomBytesAsync(32)).toString('hex')
|
const activationToken = (await RandomBytesAsync(32)).toString('hex')
|
||||||
await User.saveActivationToken(userId, activationToken)
|
await User.saveActivationToken(userId, activationToken)
|
||||||
const { email: userEmail, username } = req.body
|
const { email: userEmail, username } = req.body
|
||||||
@@ -20,34 +28,46 @@ user.register = async (req, res) => {
|
|||||||
activationLink.searchParams.set('token', activationToken)
|
activationLink.searchParams.set('token', activationToken)
|
||||||
activationLink = activationLink.href
|
activationLink = activationLink.href
|
||||||
const renderAsync = promisify(req.app.render.bind(req.app))
|
const renderAsync = promisify(req.app.render.bind(req.app))
|
||||||
const emailHtml = await renderAsync('email/user-activation', {
|
const emailHtml = await renderAsync(`email/user-activation_${req.language}`, {
|
||||||
username,
|
username,
|
||||||
activationLink
|
activationLink
|
||||||
})
|
})
|
||||||
await email.send({
|
await email.send({
|
||||||
to: userEmail,
|
to: userEmail,
|
||||||
subject: 'Aktivacija računa',
|
subject: req.t('Aktivacija računa'),
|
||||||
html: emailHtml
|
html: emailHtml
|
||||||
})
|
})
|
||||||
res.send('Registracija uspešna')
|
res.send(req.t('Registracija uspešna'))
|
||||||
}
|
}
|
||||||
|
|
||||||
user.activateAccount = async (req, res) => {
|
user.activateAccount = async (req, res) => {
|
||||||
// TODO Add validation. What if user is already logged in? What if account is already active? ...
|
// TODO Add validation. What if user is already logged in? What if account is already active? ...
|
||||||
const { token } = req.query
|
const { token } = req.query
|
||||||
const user = await User.fetchByActivationToken(token)
|
if (!token) return res.redirect(303, '/')
|
||||||
await User.activateAccount(user)
|
|
||||||
|
const user = await User.activateAccountWithToken(token, req.t)
|
||||||
const loginAsync = promisify(req.login.bind(req))
|
const loginAsync = promisify(req.login.bind(req))
|
||||||
await loginAsync(user)
|
await loginAsync(user)
|
||||||
res.redirect('/')
|
|
||||||
|
if (req.session.language) {
|
||||||
|
await User.updateLanguage(user.id, req.session.language)
|
||||||
|
delete req.session.language
|
||||||
|
}
|
||||||
|
|
||||||
|
req.flash(
|
||||||
|
'info',
|
||||||
|
req.t('Uspešno ste aktivirali svoj uporabniški račun in se prijavili.')
|
||||||
|
)
|
||||||
|
res.redirect(303, '/')
|
||||||
}
|
}
|
||||||
|
|
||||||
user.login = async (req, res, next) => {
|
user.login = async (req, res, next) => {
|
||||||
passport.authenticate(
|
passport.authenticate(
|
||||||
'local',
|
'local',
|
||||||
{
|
{
|
||||||
badRequestMessage:
|
badRequestMessage: req.t(
|
||||||
'Nepravilno uporabniško ime, elektronski naslov ali geslo.'
|
'Nepravilno uporabniško ime, elektronski naslov ali geslo.'
|
||||||
|
)
|
||||||
},
|
},
|
||||||
async (err, user, info) => {
|
async (err, user, info) => {
|
||||||
if (err) return next(err)
|
if (err) return next(err)
|
||||||
@@ -69,23 +89,96 @@ user.login = async (req, res, next) => {
|
|||||||
res.cookie('remember_me', rememberMeToken, rememberMeCookieSettings)
|
res.cookie('remember_me', rememberMeToken, rememberMeCookieSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.send('Prijava uspešna')
|
if (req.session.language) {
|
||||||
|
await User.updateLanguage(user.id, req.session.language)
|
||||||
|
delete req.session.language
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(req.t('Prijava uspešna'))
|
||||||
}
|
}
|
||||||
)(req, res, next)
|
)(req, res, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
user.logout = async (req, res) => {
|
user.logout = [logoutUser, (req, res) => res.redirect(303, '/')]
|
||||||
const rememberMeToken = req.signedCookies.remember_me
|
|
||||||
|
|
||||||
if (rememberMeToken) {
|
user.generateResetPasswordToken = async (req, res) => {
|
||||||
res.clearCookie('remember_me')
|
const { usernameOrEmail } = req.body
|
||||||
await User.clearRememberMeToken(rememberMeToken)
|
const user = await User.fetchByUsernameOrEmail(usernameOrEmail)
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.send(req.t('Nepravilno uporabniško ime ali elektronski naslov.'))
|
||||||
}
|
}
|
||||||
|
|
||||||
req.logout()
|
if (user.status !== 'active') {
|
||||||
// Manually clear session.passport due to bug in current passport version.
|
return res
|
||||||
delete req.session.passport.user
|
.status(400)
|
||||||
res.redirect('/')
|
.send(
|
||||||
|
req.t(
|
||||||
|
'Uporabniški račun še ni aktiviran. Kliknite aktivacijsko povezavo, katero smo vam poslali po elektronski pošti.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetPasswordToken = (await RandomBytesAsync(32)).toString('hex')
|
||||||
|
await User.saveResetPasswordToken(user.id, resetPasswordToken)
|
||||||
|
const { email: userEmail, username } = user
|
||||||
|
let resetPasswordLink = new URL('/ponastavitev-gesla', origin)
|
||||||
|
resetPasswordLink.searchParams.set('token', resetPasswordToken)
|
||||||
|
resetPasswordLink = resetPasswordLink.href
|
||||||
|
const renderAsync = promisify(req.app.render.bind(req.app))
|
||||||
|
// TODO i18n - prepare proper slovenian an english email templates
|
||||||
|
const emailHtml = await renderAsync(
|
||||||
|
`email/user-reset-password-token_${req.language}`,
|
||||||
|
{
|
||||||
|
username,
|
||||||
|
resetPasswordLink
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await email.send({
|
||||||
|
to: userEmail,
|
||||||
|
subject: req.t('Ponastavitev gesla'),
|
||||||
|
html: emailHtml
|
||||||
|
})
|
||||||
|
res.send()
|
||||||
|
}
|
||||||
|
|
||||||
|
user.changePassword = async (req, res) => {
|
||||||
|
const { token, password, passwordRepeat } = req.body
|
||||||
|
|
||||||
|
if (password !== passwordRepeat) {
|
||||||
|
return res.status(400).send(req.t('Gesli se ne ujemata'))
|
||||||
|
}
|
||||||
|
// TODO Add additional password validation (min length, ...)
|
||||||
|
|
||||||
|
const user = await User.resetPasswordWithToken(token, password, req.t)
|
||||||
|
|
||||||
|
const renderAsync = promisify(req.app.render.bind(req.app))
|
||||||
|
// TODO i18n - prepare proper slovenian an english email templates
|
||||||
|
const emailHtml = await renderAsync(
|
||||||
|
`email/user-reset-password-success_${req.language}`,
|
||||||
|
{
|
||||||
|
username: user.username
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await email.send({
|
||||||
|
to: user.email,
|
||||||
|
subject: req.t('Uspešna ponastavitev gesla'),
|
||||||
|
html: emailHtml
|
||||||
|
})
|
||||||
|
|
||||||
|
req.flash('info', req.t('Vaše geslo je bilo uspešno ponastavljeno.'))
|
||||||
|
|
||||||
|
const loginAsync = promisify(req.login.bind(req))
|
||||||
|
await loginAsync(user)
|
||||||
|
|
||||||
|
if (req.session.language) {
|
||||||
|
await User.updateLanguage(user.id, req.session.language)
|
||||||
|
delete req.session.language
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send()
|
||||||
}
|
}
|
||||||
|
|
||||||
user.list = async (req, res) => {
|
user.list = async (req, res) => {
|
||||||
@@ -96,7 +189,7 @@ user.list = async (req, res) => {
|
|||||||
1
|
1
|
||||||
)
|
)
|
||||||
res.render('pages/admin/user-list', {
|
res.render('pages/admin/user-list', {
|
||||||
title: 'Seznam slovarjeva',
|
title: req.t('Seznam uporabnikov'),
|
||||||
numberOfAllPages,
|
numberOfAllPages,
|
||||||
results
|
results
|
||||||
})
|
})
|
||||||
@@ -104,7 +197,10 @@ user.list = async (req, res) => {
|
|||||||
|
|
||||||
user.listAllWithPortalRoles = async (req, res) => {
|
user.listAllWithPortalRoles = async (req, res) => {
|
||||||
const users = await User.fetchAllWithPortalRoles()
|
const users = await User.fetchAllWithPortalRoles()
|
||||||
res.render('pages/admin/user-portals', { title: 'Seznam slovarjev', users })
|
res.render('pages/admin/user-portals', {
|
||||||
|
title: req.t('Skrbniki portala'),
|
||||||
|
users
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
user.findByUsernameOrEmail = async (req, res) => {
|
user.findByUsernameOrEmail = async (req, res) => {
|
||||||
@@ -115,7 +211,7 @@ user.findByUsernameOrEmail = async (req, res) => {
|
|||||||
|
|
||||||
user.updateRoles = async (req, res) => {
|
user.updateRoles = async (req, res) => {
|
||||||
await User.updatePortalRoles(req.body.rolesPerUser)
|
await User.updatePortalRoles(req.body.rolesPerUser)
|
||||||
res.redirect('back')
|
res.redirect(303, 'back')
|
||||||
}
|
}
|
||||||
|
|
||||||
user.adminEdit = async (req, res) => {
|
user.adminEdit = async (req, res) => {
|
||||||
@@ -124,8 +220,11 @@ user.adminEdit = async (req, res) => {
|
|||||||
User.fetchUser(userId),
|
User.fetchUser(userId),
|
||||||
User.fetchUserRoles(userId)
|
User.fetchUserRoles(userId)
|
||||||
])
|
])
|
||||||
|
|
||||||
|
if (userData.status === 'closed') return res.redirect(303, '/')
|
||||||
|
|
||||||
res.render('pages/admin/user-edit', {
|
res.render('pages/admin/user-edit', {
|
||||||
title: 'Urejanje uporabnikov',
|
title: req.t('Uporabnik'),
|
||||||
userData,
|
userData,
|
||||||
userRoles
|
userRoles
|
||||||
})
|
})
|
||||||
@@ -136,7 +235,7 @@ user.adminEdit = async (req, res) => {
|
|||||||
user.adminUpdate = async (req, res) => {
|
user.adminUpdate = async (req, res) => {
|
||||||
const { userId } = req.params
|
const { userId } = req.params
|
||||||
await User.updateUser(userId, req.body)
|
await User.updateUser(userId, req.body)
|
||||||
res.redirect('back')
|
res.redirect(303, 'back')
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = user
|
module.exports = user
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ passport.deserializeUser(async (id, done) => {
|
|||||||
const user = await User.fetchDeserializedDataById(id)
|
const user = await User.fetchDeserializedDataById(id)
|
||||||
|
|
||||||
// TODO Consider what to do if no user was found?
|
// TODO Consider what to do if no user was found?
|
||||||
|
|
||||||
|
// TODO This is a current workaround until session invalidation is implemented.
|
||||||
|
if (user.status !== 'active') return done(null, false)
|
||||||
done(null, user)
|
done(null, user)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
done(error)
|
done(error)
|
||||||
@@ -29,25 +32,24 @@ passport.deserializeUser(async (id, done) => {
|
|||||||
|
|
||||||
passport.use(
|
passport.use(
|
||||||
new LocalStrategy(
|
new LocalStrategy(
|
||||||
{ usernameField: 'usernameOrEmail' },
|
{ passReqToCallback: true, usernameField: 'usernameOrEmail' },
|
||||||
async (usernameOrEmail, password, done) => {
|
async (req, usernameOrEmail, password, done) => {
|
||||||
try {
|
try {
|
||||||
const { rows } = await db.query(
|
const user = await User.fetchByUsernameOrEmail(usernameOrEmail)
|
||||||
'SELECT id, status, bcrypt_hash FROM "user" WHERE username = $1 OR email = $1',
|
|
||||||
[usernameOrEmail]
|
|
||||||
)
|
|
||||||
const user = rows[0]
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return done(null, false, {
|
return done(null, false, {
|
||||||
message: 'Nepravilno uporabniško ime, elektronski naslov ali geslo.'
|
message: req.t(
|
||||||
|
'Nepravilno uporabniško ime, elektronski naslov ali geslo.'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user.status !== 'active') {
|
if (user.status !== 'active') {
|
||||||
return done(null, false, {
|
return done(null, false, {
|
||||||
message:
|
message: req.t(
|
||||||
'Uporabniški račun še ni aktiviran. Kliknite aktivacijsko povezavo, katero smo vam poslali po elektronski pošti.'
|
'Uporabniški račun še ni aktiviran. Kliknite aktivacijsko povezavo, katero smo vam poslali po elektronski pošti.'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +59,9 @@ passport.use(
|
|||||||
)
|
)
|
||||||
if (!isCorrectPassword) {
|
if (!isCorrectPassword) {
|
||||||
return done(null, false, {
|
return done(null, false, {
|
||||||
message: 'Nepravilno uporabniško ime, elektronski naslov ali geslo.'
|
message: req.t(
|
||||||
|
'Nepravilno uporabniško ime, elektronski naslov ali geslo.'
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
const i18next = require('i18next')
|
||||||
|
const i18nextMiddleware = require('i18next-http-middleware')
|
||||||
|
const i18nextBackend = require('i18next-fs-backend')
|
||||||
|
|
||||||
|
// If you change this one, don't forget to also update the nodemon ignore flag in package.json scripts.
|
||||||
|
const LOCALES_PATH = 'public/locales'
|
||||||
|
const DEFAULT_LANGUAGE = 'sl'
|
||||||
|
|
||||||
|
exports.determineRequestLanguage = (req, res, next) => {
|
||||||
|
const language =
|
||||||
|
req.user?.language || req.session.language || DEFAULT_LANGUAGE
|
||||||
|
|
||||||
|
req.determinedLanguage = language
|
||||||
|
res.locals.determinedLanguage = language
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
const customLanguageDetector = {
|
||||||
|
name: 'ownDetector',
|
||||||
|
lookup(req) {
|
||||||
|
return req.determinedLanguage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const languageDetector = new i18nextMiddleware.LanguageDetector()
|
||||||
|
languageDetector.addDetector(customLanguageDetector)
|
||||||
|
|
||||||
|
const inDevEnv = process.env.NODE_ENV === 'development'
|
||||||
|
i18next
|
||||||
|
.use(languageDetector)
|
||||||
|
.use(i18nextBackend)
|
||||||
|
.init({
|
||||||
|
// debug: true,
|
||||||
|
supportedLngs: ['sl', 'en', 'dev'],
|
||||||
|
preload: ['sl', 'en'],
|
||||||
|
ns: ['core', 'extended'],
|
||||||
|
defaultNS: 'core',
|
||||||
|
// TODO Reenable separators once keys are refactored (also in frontend init in scripts.js)
|
||||||
|
nsSeparator: false,
|
||||||
|
keySeparator: false,
|
||||||
|
...(inDevEnv && { saveMissing: true }),
|
||||||
|
...(!inDevEnv && { fallbackLng: false }),
|
||||||
|
detection: { order: ['ownDetector'] },
|
||||||
|
backend: {
|
||||||
|
loadPath: `${LOCALES_PATH}/{{lng}}/{{ns}}.json`,
|
||||||
|
addPath: `${LOCALES_PATH}/{{lng}}/{{ns}}.json`
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -2,5 +2,13 @@ const { getInstanceSetting } = require('../models/helpers')
|
|||||||
|
|
||||||
exports.enhanceLocals = async (req, res, next) => {
|
exports.enhanceLocals = async (req, res, next) => {
|
||||||
res.locals.portalCode = await getInstanceSetting('portal_code')
|
res.locals.portalCode = await getInstanceSetting('portal_code')
|
||||||
|
res.locals.flashInfo = req.flash('info')
|
||||||
|
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.adjustHeaders = async (req, res, next) => {
|
||||||
|
res.set('Cache-Control', 'no-cache, private')
|
||||||
|
|
||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
const User = require('../models/user')
|
||||||
|
|
||||||
const user = {}
|
const user = {}
|
||||||
|
|
||||||
user.enhance = (req, res, next) => {
|
user.enhance = (req, res, next) => {
|
||||||
@@ -14,6 +16,22 @@ user.enhance = (req, res, next) => {
|
|||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user.isAuthenticated = (req, res, next) => {
|
||||||
|
if (req.isAuthenticated()) return next()
|
||||||
|
|
||||||
|
if (req.isAjax) return res.status(400).end()
|
||||||
|
res.redirect(303, req.baseUrl || '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
user.isPortalAdmin = (req, res, next) => {
|
||||||
|
const isPortalAdmin = req.user.hasRole('portal admin')
|
||||||
|
|
||||||
|
if (isPortalAdmin) return next()
|
||||||
|
|
||||||
|
if (req.isAjax) return res.status(400).end()
|
||||||
|
res.redirect(303, req.baseUrl || '/')
|
||||||
|
}
|
||||||
|
|
||||||
user.isDictionaryAdmin = (req, res, next) => {
|
user.isDictionaryAdmin = (req, res, next) => {
|
||||||
const { dictionaryId } = req.params
|
const { dictionaryId } = req.params
|
||||||
const isAdmin = req.user.hasDictionaryRole(dictionaryId, 'administration')
|
const isAdmin = req.user.hasDictionaryRole(dictionaryId, 'administration')
|
||||||
@@ -21,17 +39,95 @@ user.isDictionaryAdmin = (req, res, next) => {
|
|||||||
if (isAdmin) return next()
|
if (isAdmin) return next()
|
||||||
|
|
||||||
if (req.isAjax) return res.status(400).end()
|
if (req.isAjax) return res.status(400).end()
|
||||||
res.redirect(req.baseUrl)
|
res.redirect(303, req.baseUrl || '/')
|
||||||
}
|
}
|
||||||
|
|
||||||
user.isDictionaryEditor = (req, res, next) => {
|
user.isDictionaryEditor = (req, res, next) => {
|
||||||
const { dictionaryId } = req.params
|
const dictionaryId = req.params.dictionaryId || req.dictionaryId
|
||||||
const isEditor = req.user.hasAnyDictionaryRole(dictionaryId)
|
const isEditor = req.user.hasAnyDictionaryRole(dictionaryId)
|
||||||
|
|
||||||
if (isEditor) return next()
|
if (isEditor) return next()
|
||||||
|
|
||||||
if (req.isAjax) return res.status(400).end()
|
if (req.isAjax) return res.status(400).end()
|
||||||
res.redirect(req.baseUrl)
|
res.redirect(303, req.baseUrl || '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
user.canAdministrateDictionary = (req, res, next) => {
|
||||||
|
const dictionaryId = req.params.dictionaryId || req.dictionaryId
|
||||||
|
const isAdmin = req.user.hasDictionaryRole(dictionaryId, 'administration')
|
||||||
|
const isPortalAdmin = req.user.hasRole('portal admin')
|
||||||
|
const isDictionariesAdmin = req.user.hasRole('dictionaries admin')
|
||||||
|
if (isAdmin || isPortalAdmin || isDictionariesAdmin) return next()
|
||||||
|
|
||||||
|
if (req.isAjax) return res.status(400).end()
|
||||||
|
res.redirect(303, req.baseUrl || '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
user.canAdministrateDictionaries = (req, res, next) => {
|
||||||
|
const isPortalAdmin = req.user.hasRole('portal admin')
|
||||||
|
const isDictionariesAdmin = req.user.hasRole('dictionaries admin')
|
||||||
|
if (isPortalAdmin || isDictionariesAdmin) return next()
|
||||||
|
|
||||||
|
if (req.isAjax) return res.status(400).end()
|
||||||
|
res.redirect(303, req.baseUrl || '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
user.canContentEdit = (req, res, next) => {
|
||||||
|
const { dictionaryId } = req.params
|
||||||
|
const isEditor = req.user.hasAnyDictionaryRole(dictionaryId)
|
||||||
|
const isPortalAdmin = req.user.hasRole('portal admin')
|
||||||
|
const isDictionariesAdmin = req.user.hasRole('dictionaries admin')
|
||||||
|
if (isEditor || isPortalAdmin || isDictionariesAdmin) return next()
|
||||||
|
|
||||||
|
if (req.isAjax) return res.status(400).end()
|
||||||
|
res.redirect(303, req.baseUrl || '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
user.canAdministrateConsultancy = (req, res, next) => {
|
||||||
|
const isPortalAdmin = req.user.hasRole('portal admin')
|
||||||
|
const isConsultancyAdmin = req.user.hasRole('consultancy admin')
|
||||||
|
if (isPortalAdmin || isConsultancyAdmin) return next()
|
||||||
|
|
||||||
|
if (req.isAjax) return res.status(400).end()
|
||||||
|
res.redirect(303, req.baseUrl || '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
user.canConsult = (req, res, next) => {
|
||||||
|
const isConsultant = req.user.hasRole('consultant')
|
||||||
|
const isPortalAdmin = req.user.hasRole('portal admin')
|
||||||
|
const isConsultancyAdmin = req.user.hasRole('consultancy admin')
|
||||||
|
if (isConsultant || isPortalAdmin || isConsultancyAdmin) return next()
|
||||||
|
|
||||||
|
if (req.isAjax) return res.status(400).end()
|
||||||
|
res.redirect(303, req.baseUrl || '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
user.canConsultEntry = (req, res, next) => {
|
||||||
|
const id = req.params.id || req.entryId
|
||||||
|
const isConsultantForEntry = req.user.isEditorOfConsultancyEntry(id)
|
||||||
|
const isPortalAdmin = req.user.hasRole('portal admin')
|
||||||
|
const isConsultancyAdmin = req.user.hasRole('consultancy admin')
|
||||||
|
if (isConsultantForEntry || isPortalAdmin || isConsultancyAdmin) return next()
|
||||||
|
|
||||||
|
if (req.isAjax) return res.status(400).end()
|
||||||
|
res.redirect(303, req.baseUrl || '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
user.logout = async (req, res, next) => {
|
||||||
|
const rememberMeToken = req.signedCookies.remember_me
|
||||||
|
|
||||||
|
if (rememberMeToken) {
|
||||||
|
res.clearCookie('remember_me')
|
||||||
|
await User.clearRememberMeToken(rememberMeToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.session.language = req.user.language
|
||||||
|
|
||||||
|
req.logout()
|
||||||
|
// Manually clear session.passport due to bug in current passport version.
|
||||||
|
delete req.session.passport.user
|
||||||
|
|
||||||
|
next()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
const debug = require('debug')('termPortal:models/comment')
|
const Entry = require('./entry')
|
||||||
const User = require('../models/user')
|
// const debug = require('debug')('termPortal:models/comment')
|
||||||
|
|
||||||
class Comment {
|
class Comment {
|
||||||
// Deserialize flat data into an organized comment object.
|
// Deserialize flat data into an organized comment object.
|
||||||
@@ -80,15 +80,18 @@ class Comment {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'entry_dict_ext':
|
case 'entry_dict_ext': {
|
||||||
|
const { dictionary_id: dictionaryId } = await Entry.fetch(ctxId)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
user.hasRole('portal admin') ||
|
user.hasRole('portal admin') ||
|
||||||
user.hasRole('consultancy admin') ||
|
user.hasRole('dictionaries admin') ||
|
||||||
user.hasDictionaryRole(ctxId, 'administration')
|
user.hasDictionaryRole(dictionaryId, 'administration')
|
||||||
) {
|
) {
|
||||||
isCommentModerator = true
|
isCommentModerator = true
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw Error('Invalid context type')
|
throw Error('Invalid context type')
|
||||||
@@ -185,6 +188,18 @@ class Comment {
|
|||||||
await db.query(text, values)
|
await db.query(text, values)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch context info for a specific comment.
|
||||||
|
static async fetchContextById(id) {
|
||||||
|
const {
|
||||||
|
rows: [{ context_type: ctxType, context_id: ctxId }]
|
||||||
|
} = await db.query(
|
||||||
|
'SELECT context_type, context_id FROM comment WHERE id = $1',
|
||||||
|
[id]
|
||||||
|
)
|
||||||
|
|
||||||
|
return { ctxType, ctxId }
|
||||||
|
}
|
||||||
|
|
||||||
static async updateStatus(status, id) {
|
static async updateStatus(status, id) {
|
||||||
const values = [id, status]
|
const values = [id, status]
|
||||||
const text = `
|
const text = `
|
||||||
@@ -193,240 +208,6 @@ class Comment {
|
|||||||
WHERE id = $2`
|
WHERE id = $2`
|
||||||
await db.query(text, values)
|
await db.query(text, values)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert a new demo comment into DB.
|
|
||||||
static async createDemo(comment) {
|
|
||||||
const text =
|
|
||||||
"INSERT INTO comment (message, author_id, context_type, quoted_comment_id) VALUES ($1, $2, 'portal', $3) RETURNING id"
|
|
||||||
const values = [comment.message, pickRandomMockUserId(), comment.quoteId]
|
|
||||||
const { rows } = await db.query(text, values)
|
|
||||||
const idOfInsertedComment = rows[0].id
|
|
||||||
const text2 = `${selectAllCommentsQueryString} WHERE c.id = ${idOfInsertedComment}`
|
|
||||||
const { rows: rows2 } = await db.query(text2)
|
|
||||||
|
|
||||||
const insertedComment = rows2[0]
|
|
||||||
|
|
||||||
const deserializedComment = new this(insertedComment)
|
|
||||||
|
|
||||||
return deserializedComment
|
|
||||||
}
|
|
||||||
|
|
||||||
// Seed DB with <commentCount> random comments.
|
|
||||||
static async seed(commentCount) {
|
|
||||||
const seedTasks = []
|
|
||||||
|
|
||||||
for (let i = 0; i < commentCount; i++) {
|
|
||||||
seedTasks.push(this.createDemo({ message: pickRandomMockMessage() }))
|
|
||||||
}
|
|
||||||
|
|
||||||
const seededComments = await Promise.all(seedTasks)
|
|
||||||
debug(`Successfully seeded ${commentCount} comments`)
|
|
||||||
debug('Comments:')
|
|
||||||
seededComments.forEach(comment => debug(comment))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear all comments from DB.
|
|
||||||
static async clear() {
|
|
||||||
await db.query('TRUNCATE comment')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Base SQL query string to fetch all comments.
|
|
||||||
// Can be extended with a WHEN filter clause.
|
|
||||||
const selectAllCommentsQueryString = `SELECT
|
|
||||||
c.id,
|
|
||||||
c.message,
|
|
||||||
cu.first_name author_first_name,
|
|
||||||
cu.last_name author_last_name,
|
|
||||||
c.time_created,
|
|
||||||
c.status,
|
|
||||||
q.message quote_message,
|
|
||||||
qu.first_name quote_author_first_name,
|
|
||||||
qu.last_name quote_author_last_name,
|
|
||||||
q.time_created quote_time_created
|
|
||||||
FROM comment c
|
|
||||||
LEFT JOIN "user" cu
|
|
||||||
ON cu.id = c.author_id
|
|
||||||
LEFT JOIN comment q
|
|
||||||
ON q.id = c.quoted_comment_id
|
|
||||||
LEFT JOIN "user" qu
|
|
||||||
ON qu.id = q.author_id`
|
|
||||||
|
|
||||||
// A list of messages of varying length for DB seeding.
|
|
||||||
const mockMessageVariations = [
|
|
||||||
'Lorem ipsum dolor sit amet.',
|
|
||||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit.',
|
|
||||||
'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Dolorem obcaecati ut reprehenderit explicabo, adipisci atque! Repudiandae eos facilis veniam modi.',
|
|
||||||
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Ab dicta error architecto id soluta laborum pariatur saepe doloribus voluptatem voluptas totam placeat, inventore rem! Tempore illum deleniti esse nemo. Amet.',
|
|
||||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur tristique at sem eu ultricies. Curabitur cursus efficitur ipsum, et iaculis ipsum egestas vel egestas vestibulum nec odio posuere, mollis diam et, bibendum velit. Proin non velit nec dui luctus dolor.',
|
|
||||||
'Lorem ipsum dolor sit, amet consectetur adipisicing elit. Eveniet deleniti ad quasi, ea, recusandae esse autem expedita tempora molestiae ipsa labore magnam dolorem nostrum, corrupti sint obcaecati. Voluptatum molestiae, qui laudantium voluptatibus eius, ratione voluptate, eaque quae alias dicta pariatur?'
|
|
||||||
]
|
|
||||||
|
|
||||||
// A list of users for DB seeding and random asigning to new
|
|
||||||
// comments until authentication and session mechanism are in place.
|
|
||||||
const mockUsers = [
|
|
||||||
{ firstName: 'Primož', lastName: 'Roglič' },
|
|
||||||
{ firstName: 'Tadej', lastName: 'Pogačar' },
|
|
||||||
{ firstName: 'Krištof', lastName: 'Kolumb' },
|
|
||||||
{ firstName: 'Rudolf', lastName: 'Maister' },
|
|
||||||
{ firstName: 'Ricky', lastName: 'Rickardo' },
|
|
||||||
{ firstName: 'Freddy', lastName: 'Mercury' },
|
|
||||||
{ firstName: 'Roger', lastName: 'Moore' },
|
|
||||||
{ firstName: 'Michael', lastName: 'Jackson' },
|
|
||||||
{ firstName: 'John', lastName: 'Elton' },
|
|
||||||
{ firstName: 'Harry', lastName: 'Potter' }
|
|
||||||
]
|
|
||||||
|
|
||||||
function pickRandomMockMessage() {
|
|
||||||
return mockMessageVariations[
|
|
||||||
Math.floor(Math.random() * mockMessageVariations.length)
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
function pickRandomMockUserId() {
|
|
||||||
return Math.ceil(Math.random() * mockUsers.length)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function seedMockUsersInDb() {
|
|
||||||
const { rows } = await db.query('SELECT COUNT(*) user_count FROM "user"')
|
|
||||||
const userCount = rows[0].user_count
|
|
||||||
if (+userCount) return 'Users already exist'
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
mockUsers.map(async user => {
|
|
||||||
const text =
|
|
||||||
'INSERT INTO "user"(username, first_name, last_name, email, bcrypt_hash) VALUES ($1, $2, $3, $4, \'dummyBcryptHash\') RETURNING *'
|
|
||||||
const values = [
|
|
||||||
`${user.firstName}_${user.lastName}`,
|
|
||||||
user.firstName,
|
|
||||||
user.lastName,
|
|
||||||
`${user.firstName}.${user.lastName}@rsdo.com`
|
|
||||||
]
|
|
||||||
const { rows } = await db.query(text, values)
|
|
||||||
const createdUser = rows[0]
|
|
||||||
debug(`Created user: ${JSON.stringify(createdUser)}`)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
return 'Successfully seeded all users'
|
|
||||||
}
|
|
||||||
|
|
||||||
async function seedPortalAdmin() {
|
|
||||||
const MOCK_ADMIN_BASE = 'admin'
|
|
||||||
|
|
||||||
const {
|
|
||||||
rows: [mockAdmin]
|
|
||||||
} = await db.query('SELECT id FROM "user" WHERE username = $1', [
|
|
||||||
MOCK_ADMIN_BASE
|
|
||||||
])
|
|
||||||
|
|
||||||
if (mockAdmin) {
|
|
||||||
return `Portal admin already exists (username: ${MOCK_ADMIN_BASE}, password: ${MOCK_ADMIN_BASE})`
|
|
||||||
}
|
|
||||||
|
|
||||||
const adminUser = {
|
|
||||||
username: MOCK_ADMIN_BASE,
|
|
||||||
firstName: MOCK_ADMIN_BASE,
|
|
||||||
lastName: MOCK_ADMIN_BASE,
|
|
||||||
password: MOCK_ADMIN_BASE,
|
|
||||||
email: `${MOCK_ADMIN_BASE}@rsdo.com`
|
|
||||||
}
|
|
||||||
const userId = await User.create(adminUser)
|
|
||||||
const assignAdminRole = db.query(
|
|
||||||
`INSERT INTO user_role (user_id, role_name)
|
|
||||||
VALUES
|
|
||||||
($1, 'portal admin'),
|
|
||||||
($1, 'dictionaries admin'),
|
|
||||||
($1, 'consultancy admin'),
|
|
||||||
($1, 'consultant')`,
|
|
||||||
[userId]
|
|
||||||
)
|
|
||||||
const activateAdminUser = db.query(
|
|
||||||
`UPDATE "user" SET status = 'active', time_activated = time_registered WHERE username = $1`,
|
|
||||||
[adminUser.username]
|
|
||||||
)
|
|
||||||
await Promise.all([assignAdminRole, activateAdminUser])
|
|
||||||
return `Successfully seeded portal admin (username: ${MOCK_ADMIN_BASE}, password: ${MOCK_ADMIN_BASE})`
|
|
||||||
}
|
|
||||||
|
|
||||||
async function seedConsultants() {
|
|
||||||
const {
|
|
||||||
rows: [mockConsultancyAdmin]
|
|
||||||
} = await db.query('SELECT id FROM "user" WHERE username = $1', ['cadmin'])
|
|
||||||
|
|
||||||
if (mockConsultancyAdmin) {
|
|
||||||
return "Consultants already exist: 'cadmin', 'consultant1', 'consultant2', 'consultant3'"
|
|
||||||
}
|
|
||||||
|
|
||||||
const cadminUser = {
|
|
||||||
username: 'cadmin',
|
|
||||||
firstName: 'cadmin',
|
|
||||||
lastName: 'cadmin',
|
|
||||||
password: 'cadmin',
|
|
||||||
email: 'cadmin@rsdo.com'
|
|
||||||
}
|
|
||||||
const userId = await User.create(cadminUser)
|
|
||||||
const assignConsultancyAdminRole = db.query(
|
|
||||||
`INSERT INTO user_role (user_id, role_name)
|
|
||||||
VALUES
|
|
||||||
($1, 'consultancy admin')`,
|
|
||||||
[userId]
|
|
||||||
)
|
|
||||||
const activateConsultancyAdminUser = db.query(
|
|
||||||
`UPDATE "user" SET status = 'active', time_activated = time_registered WHERE username = $1`,
|
|
||||||
[cadminUser.username]
|
|
||||||
)
|
|
||||||
await Promise.all([assignConsultancyAdminRole, activateConsultancyAdminUser])
|
|
||||||
|
|
||||||
for (let i = 1; i <= 3; i++) {
|
|
||||||
const consultant = {
|
|
||||||
username: `consultant${i}`,
|
|
||||||
firstName: `consultant${i}`,
|
|
||||||
lastName: `consultant${i}`,
|
|
||||||
password: `consultant${i}`,
|
|
||||||
email: `consultant${i}@rsdo.com`
|
|
||||||
}
|
|
||||||
const userId = await User.create(consultant)
|
|
||||||
const assignConsultantRole = db.query(
|
|
||||||
`INSERT INTO user_role (user_id, role_name)
|
|
||||||
VALUES
|
|
||||||
($1, 'consultant')`,
|
|
||||||
[userId]
|
|
||||||
)
|
|
||||||
const activateConsultant = db.query(
|
|
||||||
`UPDATE "user" SET status = 'active', time_activated = time_registered WHERE username = $1`,
|
|
||||||
[consultant.username]
|
|
||||||
)
|
|
||||||
await Promise.all([assignConsultantRole, activateConsultant])
|
|
||||||
}
|
|
||||||
|
|
||||||
return `Successfully seeded consultants 'cadmin', 'consultant1', 'consultant2', 'consultant3'`
|
|
||||||
}
|
|
||||||
|
|
||||||
Comment.seedDummyData = () => {
|
|
||||||
// Seed DB with mock users on empty DB.
|
|
||||||
// seedMockUsersInDb()
|
|
||||||
// .then(debug)
|
|
||||||
// .catch(err => {
|
|
||||||
// debug('Users not seeded.')
|
|
||||||
// debug(err)
|
|
||||||
// })
|
|
||||||
|
|
||||||
// Seed DB with mock portal admin user on empty DB.
|
|
||||||
// TODO Replace with a more robust solution for production.
|
|
||||||
seedPortalAdmin()
|
|
||||||
.then(debug)
|
|
||||||
.catch(err => {
|
|
||||||
debug('Portal admin not seeded.')
|
|
||||||
debug(err)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Seed DB with mock consultancy admin and consultant users on empty DB.
|
|
||||||
// seedConsultants()
|
|
||||||
// .then(debug)
|
|
||||||
// .catch(err => {
|
|
||||||
// debug('Consultants not seeded.')
|
|
||||||
// debug(err)
|
|
||||||
// })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Comment
|
module.exports = Comment
|
||||||
|
|||||||
@@ -55,21 +55,9 @@ class ConsultancyEntry {
|
|||||||
this.formattedTimePublished = formattedTimePublished
|
this.formattedTimePublished = formattedTimePublished
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch all consultancy entries from DB.
|
|
||||||
static async fetchAll() {
|
|
||||||
// TODO Luka: Miha, define specific fields instead of using *.
|
|
||||||
const { rows: fetchedConsEntries } = await db.query(`
|
|
||||||
SELECT *
|
|
||||||
FROM consultancy_entry
|
|
||||||
ORDER BY time_created DESC`)
|
|
||||||
const deserializedConsEntries = fetchedConsEntries.map(
|
|
||||||
consEntry => new this(consEntry)
|
|
||||||
)
|
|
||||||
return deserializedConsEntries
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch consultancy entry by ID
|
// Fetch consultancy entry by ID
|
||||||
// to_char(time_created,'HH24:MI:SS DD/MM/YYYY')
|
// to_char(time_created,'HH24:MI:SS DD/MM/YYYY')
|
||||||
|
// TODO i18n date format
|
||||||
static async fetchByIdWithFormattedTime(id) {
|
static async fetchByIdWithFormattedTime(id) {
|
||||||
const { rows: fetchedConsEntry } = await db.query(
|
const { rows: fetchedConsEntry } = await db.query(
|
||||||
`
|
`
|
||||||
@@ -161,24 +149,6 @@ class ConsultancyEntry {
|
|||||||
return emails
|
return emails
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch all consultancy entries filtered by status from DB.
|
|
||||||
static async fetchAllByStatus(status) {
|
|
||||||
// TODO Luka: Miha, define specific fields instead of using *.
|
|
||||||
const sqlQuery = `
|
|
||||||
SELECT *, to_char(time_created, 'FMDD. FMMM. YYYY') formatted_time_created
|
|
||||||
FROM consultancy_entry
|
|
||||||
WHERE status=$1
|
|
||||||
ORDER BY time_created DESC`
|
|
||||||
|
|
||||||
const values = [status]
|
|
||||||
const { rows: fetchedConsEntries } = await db.query(sqlQuery, values)
|
|
||||||
|
|
||||||
const deserializedConsEntries = fetchedConsEntries.map(
|
|
||||||
consEntry => new this(consEntry)
|
|
||||||
)
|
|
||||||
return deserializedConsEntries
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch all consultancy entries filtered by status from DB.
|
// Fetch all consultancy entries filtered by status from DB.
|
||||||
static async fetchAllByStatusCount(status) {
|
static async fetchAllByStatusCount(status) {
|
||||||
// TODO Luka: Miha, define specific fields instead of using *.
|
// TODO Luka: Miha, define specific fields instead of using *.
|
||||||
@@ -310,12 +280,6 @@ class ConsultancyEntry {
|
|||||||
return deserializedConsEntries
|
return deserializedConsEntries
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch all new consultancy entries from DB.
|
|
||||||
static async fetchAllNew() {
|
|
||||||
const newEntries = await this.fetchAllByStatus('new')
|
|
||||||
return newEntries
|
|
||||||
}
|
|
||||||
|
|
||||||
static async fetchAllRejected() {
|
static async fetchAllRejected() {
|
||||||
const newEntries = await this.fetchAllByStatusWithAuthorData('rejected')
|
const newEntries = await this.fetchAllByStatusWithAuthorData('rejected')
|
||||||
return newEntries
|
return newEntries
|
||||||
@@ -437,20 +401,6 @@ class ConsultancyEntry {
|
|||||||
return rows[0]
|
return rows[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
static async getEditors(entryId) {
|
|
||||||
const sqlQuery = `
|
|
||||||
SELECT u.id, u.first_name, u.last_name
|
|
||||||
FROM "consultancy_entry" ce
|
|
||||||
INNER JOIN "consultancy_entry_consultant" cec ON ce.id = cec.entry_id
|
|
||||||
INNER JOIN "user" u ON u.id = cec.user_id
|
|
||||||
WHERE ce.id=$1`
|
|
||||||
|
|
||||||
const values = [entryId]
|
|
||||||
|
|
||||||
const { rows } = await db.query(sqlQuery, values)
|
|
||||||
return rows
|
|
||||||
}
|
|
||||||
|
|
||||||
static async getSharedAuthorsArray(entryId) {
|
static async getSharedAuthorsArray(entryId) {
|
||||||
const sqlQuery = `SELECT answer_authors FROM "consultancy_entry"
|
const sqlQuery = `SELECT answer_authors FROM "consultancy_entry"
|
||||||
WHERE id=$1;`
|
WHERE id=$1;`
|
||||||
@@ -679,6 +629,7 @@ class ConsultancyEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// (Re)index specific consultancy entry into consultancy search index.
|
// (Re)index specific consultancy entry into consultancy search index.
|
||||||
|
// TODO i18n name_sl
|
||||||
static async indexIntoSearchEngine(entryId, shouldWait) {
|
static async indexIntoSearchEngine(entryId, shouldWait) {
|
||||||
const values = [entryId]
|
const values = [entryId]
|
||||||
const text = `
|
const text = `
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
const db = require('./db')
|
|
||||||
|
|
||||||
const DemoPaginacija = {}
|
|
||||||
|
|
||||||
// Metoda za generacijo demo podatkov.
|
|
||||||
DemoPaginacija.initDemoData = async () => {
|
|
||||||
await db.query(`
|
|
||||||
CREATE TABLE IF NOT EXISTS demo_paginacija (zanimivo TEXT, nezanimivo1 TEXT, nezanimivo2 TEXT);
|
|
||||||
|
|
||||||
DO $$
|
|
||||||
BEGIN
|
|
||||||
IF (SELECT COUNT(*) FROM demo_paginacija) = 0 THEN
|
|
||||||
FOR stevec IN 1..9993 LOOP
|
|
||||||
INSERT INTO demo_paginacija VALUES ('vrednost' || stevec, 'brezveze', 'tega res ne rabimo');
|
|
||||||
END LOOP;
|
|
||||||
END IF;
|
|
||||||
END $$`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Metoda za poizvedbo demo podatkov za določeno stran.
|
|
||||||
DemoPaginacija.fetch = async (resultsPerPage, page) => {
|
|
||||||
const {
|
|
||||||
rows: [{ result }]
|
|
||||||
} = await db.query(
|
|
||||||
`
|
|
||||||
SELECT jsonb_build_object(
|
|
||||||
'pages_total', (
|
|
||||||
SELECT CEIL(COUNT(*) / $1::float)
|
|
||||||
FROM demo_paginacija
|
|
||||||
),
|
|
||||||
'results', ARRAY(
|
|
||||||
SELECT jsonb_build_object(
|
|
||||||
'zanimivo', zanimivo,
|
|
||||||
'nezanimivo1', nezanimivo1,
|
|
||||||
'nezanimivo2', nezanimivo2
|
|
||||||
)
|
|
||||||
FROM demo_paginacija
|
|
||||||
LIMIT $1
|
|
||||||
OFFSET $2
|
|
||||||
)
|
|
||||||
) result
|
|
||||||
`,
|
|
||||||
[resultsPerPage, resultsPerPage * (page - 1)]
|
|
||||||
)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = DemoPaginacija
|
|
||||||
@@ -3,15 +3,28 @@ const htmlToText = require('nodemailer-html-to-text').htmlToText()
|
|||||||
const {
|
const {
|
||||||
smtpHost,
|
smtpHost,
|
||||||
smtpPort,
|
smtpPort,
|
||||||
smtpTlsRejectUnauthorized,
|
smtpUser,
|
||||||
|
smtpPassword,
|
||||||
|
smtpSecure,
|
||||||
|
smtpRequireTls,
|
||||||
|
smtpAllowInvalidCerts,
|
||||||
smtpFrom
|
smtpFrom
|
||||||
} = require('../config/keys')
|
} = require('../config/keys')
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
host: smtpHost,
|
host: smtpHost,
|
||||||
port: smtpPort,
|
port: smtpPort,
|
||||||
tls: { rejectUnauthorized: smtpTlsRejectUnauthorized }
|
secure: smtpSecure,
|
||||||
|
requireTLS: smtpRequireTls,
|
||||||
|
tls: { rejectUnauthorized: !smtpAllowInvalidCerts }
|
||||||
}
|
}
|
||||||
|
if (smtpUser || smtpPassword) {
|
||||||
|
options.auth = {
|
||||||
|
user: smtpUser,
|
||||||
|
pass: smtpPassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const defaults = { from: smtpFrom }
|
const defaults = { from: smtpFrom }
|
||||||
|
|
||||||
const transporter = nodemailer.createTransport(options, defaults)
|
const transporter = nodemailer.createTransport(options, defaults)
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
const { searchEngineClient, ENTRY_INDEX } = require('./search-engine')
|
const { searchEngineClient, ENTRY_INDEX } = require('./search-engine')
|
||||||
const { intoDbArray, getInstanceSetting, removeHtmlTags } = require('./helpers')
|
const { intoDbArray, getInstanceSetting, removeHtmlTags } = require('./helpers')
|
||||||
const { prepareEntryForIndexing } = require('./helpers/dictionary')
|
const {
|
||||||
|
prepareEntryForIndexing,
|
||||||
|
sanitizeField
|
||||||
|
} = require('./helpers/dictionary')
|
||||||
|
|
||||||
const Entry = {}
|
const Entry = {}
|
||||||
|
|
||||||
@@ -10,7 +13,7 @@ Entry.create = async (userId, dictionaryId, entry) => {
|
|||||||
const pickedLinks = intoDbArray(entry.links, 'always')
|
const pickedLinks = intoDbArray(entry.links, 'always')
|
||||||
const pickedType = intoDbArray(entry.type, 'always')
|
const pickedType = intoDbArray(entry.type, 'always')
|
||||||
const links = pickedLinks.map((link, index) => ({
|
const links = pickedLinks.map((link, index) => ({
|
||||||
link,
|
link: sanitizeField.toMixedBasic(link),
|
||||||
type: pickedType[index]
|
type: pickedType[index]
|
||||||
}))
|
}))
|
||||||
const foreign = intoDbArray(entry.foreign, 'always')
|
const foreign = intoDbArray(entry.foreign, 'always')
|
||||||
@@ -18,9 +21,13 @@ Entry.create = async (userId, dictionaryId, entry) => {
|
|||||||
if (row.term || row.definition || row.synonym) {
|
if (row.term || row.definition || row.synonym) {
|
||||||
agg.push({
|
agg.push({
|
||||||
language: row.code,
|
language: row.code,
|
||||||
terms: intoDbArray(row.term, 'undefined'),
|
terms: intoDbArray(row.term, 'undefined')?.map(term =>
|
||||||
definition: row.definition || null,
|
sanitizeField.toMixedBasic(term)
|
||||||
synonyms: intoDbArray(row.synonym, 'undefined')
|
),
|
||||||
|
definition: sanitizeField.toMixedExtended(row.definition) || null,
|
||||||
|
synonyms: intoDbArray(row.synonym, 'undefined')?.map(synonym =>
|
||||||
|
sanitizeField.toMixedBasic(synonym)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return agg
|
return agg
|
||||||
@@ -34,22 +41,26 @@ Entry.create = async (userId, dictionaryId, entry) => {
|
|||||||
dictionaryId,
|
dictionaryId,
|
||||||
isValid,
|
isValid,
|
||||||
entry.status,
|
entry.status,
|
||||||
entry.term || null,
|
sanitizeField.toMixedBasic(entry.term) || null,
|
||||||
userId,
|
userId,
|
||||||
entry.homonymSort || null,
|
entry.homonymSort || null,
|
||||||
entry.wordforms || null,
|
entry.wordforms || null,
|
||||||
entry.accent || null,
|
entry.accent || null,
|
||||||
entry.pronunciation,
|
entry.pronunciation || null,
|
||||||
intoDbArray(entry.domainLabels, 'always'),
|
intoDbArray(entry.domainLabels, 'always').map(label =>
|
||||||
entry.label || null,
|
sanitizeField.toText(label)
|
||||||
entry.definition || null,
|
),
|
||||||
intoDbArray(entry.synonyms),
|
sanitizeField.toMixedExtended(entry.label) || null,
|
||||||
|
sanitizeField.toMixedExtended(entry.definition) || null,
|
||||||
|
intoDbArray(entry.synonyms)?.map(synonym =>
|
||||||
|
sanitizeField.toMixedBasic(synonym)
|
||||||
|
),
|
||||||
links,
|
links,
|
||||||
entry.other || null,
|
sanitizeField.toMixedOther(entry.other) || null,
|
||||||
foreignLanguageContent,
|
foreignLanguageContent,
|
||||||
intoDbArray(entry.image),
|
intoDbArray(entry.image)?.map(image => sanitizeField.toText(image)),
|
||||||
intoDbArray(entry.audio),
|
intoDbArray(entry.audio)?.map(audio => sanitizeField.toText(audio)),
|
||||||
intoDbArray(entry.video)
|
intoDbArray(entry.video)?.map(video => sanitizeField.toText(video))
|
||||||
]
|
]
|
||||||
const text = `SELECT entry_new (${db.genParamStr(values)})`
|
const text = `SELECT entry_new (${db.genParamStr(values)})`
|
||||||
|
|
||||||
@@ -60,71 +71,6 @@ Entry.create = async (userId, dictionaryId, entry) => {
|
|||||||
return entryId
|
return entryId
|
||||||
}
|
}
|
||||||
|
|
||||||
// // Fetch all entry terms of a single dictionary from DB.
|
|
||||||
// Entry.fetchAll = async dictionaryId => {
|
|
||||||
// const text = `
|
|
||||||
// SELECT
|
|
||||||
// e.id,
|
|
||||||
// e.is_valid as valid,
|
|
||||||
// e.is_published as published,
|
|
||||||
// e.term as term,
|
|
||||||
// MAX(ef.term) as fterm,
|
|
||||||
// CASE
|
|
||||||
// WHEN NOW() - e.time_most_recent_comment < INTERVAL '1 week' THEN 'T'
|
|
||||||
// WHEN NOW() - e.time_most_recent_comment < INTERVAL '1 month' THEN 'M'
|
|
||||||
// WHEN NOW() - e.time_most_recent_comment < INTERVAL '1 year' THEN 'L'
|
|
||||||
// ELSE ''
|
|
||||||
// END comment_age
|
|
||||||
// FROM entry e
|
|
||||||
// LEFT JOIN entry_foreign ef ON e.id = ef.entry_id
|
|
||||||
// WHERE dictionary_id = $1
|
|
||||||
// GROUP BY id, is_valid, is_published, e.term, comment_age
|
|
||||||
// ORDER BY e.term`
|
|
||||||
// const value = [dictionaryId]
|
|
||||||
|
|
||||||
// const { rows: fetchedTerms } = await db.query(text, value)
|
|
||||||
// return fetchedTerms
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Metoda za poizvedbo demo podatkov za določeno stran.
|
|
||||||
// Entry.fetchPaginated = async (resultsPerPage, page) => {
|
|
||||||
// const {
|
|
||||||
// rows: [{ result }]
|
|
||||||
// } = await db.query(
|
|
||||||
// `
|
|
||||||
// SELECT jsonb_build_object(
|
|
||||||
// 'pages_total', (
|
|
||||||
// SELECT CEIL(COUNT(*) / $1::float)
|
|
||||||
// FROM demo_paginacija
|
|
||||||
// ),
|
|
||||||
// 'results', ARRAY(
|
|
||||||
// SELECT jsonb_build_object(
|
|
||||||
// dictionary_id,
|
|
||||||
// term,
|
|
||||||
// is_published,
|
|
||||||
// is_terminology_reviewed,
|
|
||||||
// is_language_reviewed,
|
|
||||||
// status,
|
|
||||||
// label,
|
|
||||||
// definition,
|
|
||||||
// synonym,
|
|
||||||
// other,
|
|
||||||
// image,
|
|
||||||
// audio,
|
|
||||||
// video
|
|
||||||
// )
|
|
||||||
// FROM entry
|
|
||||||
// LIMIT $1
|
|
||||||
// OFFSET $2
|
|
||||||
// )
|
|
||||||
// ) result
|
|
||||||
// `,
|
|
||||||
// [resultsPerPage, resultsPerPage * (page - 1)]
|
|
||||||
// )
|
|
||||||
|
|
||||||
// return result
|
|
||||||
// }
|
|
||||||
|
|
||||||
// Fetch all data, related to single entry from DB.
|
// Fetch all data, related to single entry from DB.
|
||||||
Entry.fetchFull = async entryId => {
|
Entry.fetchFull = async entryId => {
|
||||||
const text = `
|
const text = `
|
||||||
@@ -184,7 +130,12 @@ Entry.fetchFull = async entryId => {
|
|||||||
SELECT jsonb_strip_nulls(
|
SELECT jsonb_strip_nulls(
|
||||||
jsonb_build_object(
|
jsonb_build_object(
|
||||||
'version', version,
|
'version', version,
|
||||||
'version_time', version_time
|
'version_time', version_time,
|
||||||
|
'version_author', (
|
||||||
|
SELECT username
|
||||||
|
FROM "user"
|
||||||
|
WHERE id = (version_snapshot['version_author'])::int
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
FROM entry_version_history
|
FROM entry_version_history
|
||||||
@@ -232,6 +183,7 @@ Entry.fetchFullWithOrderedForeignLanguages = async entryId => {
|
|||||||
'audio', e.audio,
|
'audio', e.audio,
|
||||||
'video', e.video,
|
'video', e.video,
|
||||||
'time_modified', e.time_modified,
|
'time_modified', e.time_modified,
|
||||||
|
'external_url', e.external_url,
|
||||||
'domain_labels', ARRAY(
|
'domain_labels', ARRAY(
|
||||||
SELECT name
|
SELECT name
|
||||||
FROM entry_domain_label edl
|
FROM entry_domain_label edl
|
||||||
@@ -421,6 +373,8 @@ Entry.deleteAllLinks = async dictionaryId => {
|
|||||||
|
|
||||||
// (Re)index specific entry into entry search index.
|
// (Re)index specific entry into entry search index.
|
||||||
Entry.indexIntoSearchEngine = async (entryId, shouldWait) => {
|
Entry.indexIntoSearchEngine = async (entryId, shouldWait) => {
|
||||||
|
// TODO If this method is ever used for linked portals/dictionaries,
|
||||||
|
// TODO rework the source object below (already done in Dictionary.indexIntoSearchEngine).
|
||||||
const values = [entryId]
|
const values = [entryId]
|
||||||
const text = `
|
const text = `
|
||||||
SELECT
|
SELECT
|
||||||
@@ -465,7 +419,7 @@ Entry.indexIntoSearchEngine = async (entryId, shouldWait) => {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
FROM entry_foreign ef
|
FROM entry_foreign ef
|
||||||
LEFT JOIN LANGUAGE l ON l.id = ef.language_id
|
LEFT JOIN language l ON l.id = ef.language_id
|
||||||
WHERE entry_id = e.id
|
WHERE entry_id = e.id
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -495,10 +449,11 @@ Entry.indexIntoSearchEngine = async (entryId, shouldWait) => {
|
|||||||
|
|
||||||
const { dictionary, primary_domain: primaryDomain } = dataToIndex
|
const { dictionary, primary_domain: primaryDomain } = dataToIndex
|
||||||
let { entry } = dataToIndex
|
let { entry } = dataToIndex
|
||||||
// TODO Luka: I expect "source" needing a rework once linked portals and dictionaries start working.
|
|
||||||
|
// TODO i18n Luka: index portal name for both languages?
|
||||||
const source = {
|
const source = {
|
||||||
code: await getInstanceSetting('portal_code'),
|
code: await getInstanceSetting('portal_code'),
|
||||||
name: await getInstanceSetting('portal_name')
|
name: await getInstanceSetting('portal_name_sl')
|
||||||
}
|
}
|
||||||
|
|
||||||
entry = prepareEntryForIndexing(entry)
|
entry = prepareEntryForIndexing(entry)
|
||||||
@@ -556,7 +511,7 @@ Entry.update = async (userId, entry) => {
|
|||||||
const pickedLinks = intoDbArray(entry.links, 'always')
|
const pickedLinks = intoDbArray(entry.links, 'always')
|
||||||
const pickedType = intoDbArray(entry.type, 'always')
|
const pickedType = intoDbArray(entry.type, 'always')
|
||||||
const links = pickedLinks.map((link, index) => ({
|
const links = pickedLinks.map((link, index) => ({
|
||||||
link,
|
link: sanitizeField.toMixedBasic(link),
|
||||||
type: pickedType[index]
|
type: pickedType[index]
|
||||||
}))
|
}))
|
||||||
const foreign = intoDbArray(entry.foreign, 'always')
|
const foreign = intoDbArray(entry.foreign, 'always')
|
||||||
@@ -564,9 +519,13 @@ Entry.update = async (userId, entry) => {
|
|||||||
if (row.term || row.definition || row.synonym) {
|
if (row.term || row.definition || row.synonym) {
|
||||||
agg.push({
|
agg.push({
|
||||||
language: row.code,
|
language: row.code,
|
||||||
terms: intoDbArray(row.term, 'undefined'),
|
terms: intoDbArray(row.term, 'undefined')?.map(term =>
|
||||||
definition: row.definition || null,
|
sanitizeField.toMixedBasic(term)
|
||||||
synonyms: intoDbArray(row.synonym, 'undefined')
|
),
|
||||||
|
definition: sanitizeField.toMixedExtended(row.definition) || null,
|
||||||
|
synonyms: intoDbArray(row.synonym, 'undefined')?.map(synonym =>
|
||||||
|
sanitizeField.toMixedBasic(synonym)
|
||||||
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return agg
|
return agg
|
||||||
@@ -582,19 +541,23 @@ Entry.update = async (userId, entry) => {
|
|||||||
!!entry.isTerminologyReviewed,
|
!!entry.isTerminologyReviewed,
|
||||||
!!entry.isLanguageReviewed,
|
!!entry.isLanguageReviewed,
|
||||||
entry.status,
|
entry.status,
|
||||||
entry.term || null,
|
sanitizeField.toMixedBasic(entry.term) || null,
|
||||||
userId,
|
userId,
|
||||||
entry.homonymSort || null,
|
entry.homonymSort || null,
|
||||||
intoDbArray(entry.domainLabels, 'always'),
|
intoDbArray(entry.domainLabels, 'always').map(label =>
|
||||||
entry.label || null,
|
sanitizeField.toText(label)
|
||||||
entry.definition || null,
|
),
|
||||||
intoDbArray(entry.synonyms),
|
sanitizeField.toMixedExtended(entry.label) || null,
|
||||||
|
sanitizeField.toMixedExtended(entry.definition) || null,
|
||||||
|
intoDbArray(entry.synonyms)?.map(synonym =>
|
||||||
|
sanitizeField.toMixedBasic(synonym)
|
||||||
|
),
|
||||||
links,
|
links,
|
||||||
entry.other || null,
|
sanitizeField.toMixedOther(entry.other) || null,
|
||||||
foreignLanguageContent,
|
foreignLanguageContent,
|
||||||
intoDbArray(entry.image),
|
intoDbArray(entry.image)?.map(image => sanitizeField.toText(image)),
|
||||||
intoDbArray(entry.audio),
|
intoDbArray(entry.audio)?.map(audio => sanitizeField.toText(audio)),
|
||||||
intoDbArray(entry.video)
|
intoDbArray(entry.video)?.map(video => sanitizeField.toText(video))
|
||||||
]
|
]
|
||||||
const text = `SELECT entry_update (${db.genParamStr(values)})`
|
const text = `SELECT entry_update (${db.genParamStr(values)})`
|
||||||
|
|
||||||
@@ -618,13 +581,17 @@ Entry.update = async (userId, entry) => {
|
|||||||
// Fetch a single version snapshot of a single entry from DB.
|
// Fetch a single version snapshot of a single entry from DB.
|
||||||
Entry.fetchVersionSnapshot = async (entryId, version) => {
|
Entry.fetchVersionSnapshot = async (entryId, version) => {
|
||||||
const {
|
const {
|
||||||
rows: [{ version_snapshot: historySnapshot }]
|
rows: [{ version_snapshot: historySnapshot, author }]
|
||||||
} = await db.query(
|
} = await db.query(
|
||||||
'SELECT version_snapshot FROM entry_version_history WHERE entry_id = $1 and version = $2',
|
`SELECT v.version_snapshot, (
|
||||||
|
SELECT u.username
|
||||||
|
FROM "user" u
|
||||||
|
WHERE u.id = (v.version_snapshot['version_author'])::int) as author
|
||||||
|
FROM entry_version_history v WHERE v.entry_id = $1 and v.version = $2`,
|
||||||
[entryId, version]
|
[entryId, version]
|
||||||
)
|
)
|
||||||
|
|
||||||
return historySnapshot
|
return { data: historySnapshot, author }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fetch by language and entry Id. Note that this version includes the language name */
|
/* Fetch by language and entry Id. Note that this version includes the language name */
|
||||||
|
|||||||
@@ -12,13 +12,14 @@ const {
|
|||||||
getFileNamesInFolder,
|
getFileNamesInFolder,
|
||||||
getFileStatsInFolder
|
getFileStatsInFolder
|
||||||
} = require('./helpers/extraction')
|
} = require('./helpers/extraction')
|
||||||
|
const { extractionApiOrigin } = require('../config/keys')
|
||||||
|
|
||||||
const Extraction = {}
|
const Extraction = {}
|
||||||
|
|
||||||
// Fetch all extractions for a specific user.
|
// Fetch all extractions for a specific user.
|
||||||
Extraction.fetchAllForUser = async userId => {
|
Extraction.fetchAllForUser = async userId => {
|
||||||
const { rows: fetchedExtractions } = await db.query(
|
const { rows: fetchedExtractions } = await db.query(
|
||||||
'SELECT id, name, status, corpus_id, oss_params, time_started, time_finished FROM extraction WHERE user_id = $1 ORDER BY id',
|
'SELECT id, name, status, corpus_id, oss_params, time_started, time_finished FROM extraction WHERE user_id = $1 ORDER BY status ASC, id DESC, time_finished DESC',
|
||||||
[userId]
|
[userId]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -67,26 +68,35 @@ Extraction.fetch = async id => {
|
|||||||
const {
|
const {
|
||||||
rows: [fetchedExtraction]
|
rows: [fetchedExtraction]
|
||||||
} = await db.query(
|
} = await db.query(
|
||||||
'SELECT id, name, status, corpus_id, oss_params, time_started, time_finished FROM extraction WHERE id = $1',
|
'SELECT id, user_id, name, status, corpus_id, oss_params, time_started, time_finished FROM extraction WHERE id = $1',
|
||||||
[id]
|
[id]
|
||||||
)
|
)
|
||||||
|
|
||||||
return deserialize.extraction(fetchedExtraction)
|
return deserialize.extraction(fetchedExtraction)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch author email of a specific extraction entry from DB.
|
// Fetch oss document types from DB.
|
||||||
Extraction.fetchAuthorEmail = async id => {
|
Extraction.fetchOssDocumentTypes = async determinedLanguage => {
|
||||||
|
const { rows: fetchedDocumentTypes } = await db.query(
|
||||||
|
`SELECT id, name_${determinedLanguage} name FROM extraction_oss_document_types`
|
||||||
|
)
|
||||||
|
|
||||||
|
return fetchedDocumentTypes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch data of the author of a specific extraction entry from DB.
|
||||||
|
Extraction.fetchAuthorData = async id => {
|
||||||
const {
|
const {
|
||||||
rows: [{ email }]
|
rows: [authorData]
|
||||||
} = await db.query(
|
} = await db.query(
|
||||||
`SELECT u.email
|
`SELECT u.email, u.language
|
||||||
FROM extraction e
|
FROM extraction e
|
||||||
LEFT JOIN "user" u ON u.id = e.user_id
|
LEFT JOIN "user" u ON u.id = e.user_id
|
||||||
WHERE e.id = $1`,
|
WHERE e.id = $1`,
|
||||||
[id]
|
[id]
|
||||||
)
|
)
|
||||||
|
|
||||||
return email
|
return authorData
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update extraction entry in DB.
|
// Update extraction entry in DB.
|
||||||
@@ -160,6 +170,18 @@ Extraction.fetchTermCandidatesCount = async function (extractionId) {
|
|||||||
return termCandidates.length
|
return termCandidates.length
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch term candidates slice for a specific extraction.
|
||||||
|
Extraction.fetchTermCandidatesSlice = async function (
|
||||||
|
extractionId,
|
||||||
|
fromIndex,
|
||||||
|
toIndex
|
||||||
|
) {
|
||||||
|
const termCandidatesJson = await this.fetchTermCandidatesJson(extractionId)
|
||||||
|
const termCandidates = JSON.parse(termCandidatesJson).terminoloski_kandidati
|
||||||
|
const termCandidatesSlice = termCandidates.slice(fromIndex, toIndex)
|
||||||
|
return termCandidatesSlice
|
||||||
|
}
|
||||||
|
|
||||||
// Mark extraction from own documents as began.
|
// Mark extraction from own documents as began.
|
||||||
Extraction.beginOwn = async (extractionId, documentsNames) => {
|
Extraction.beginOwn = async (extractionId, documentsNames) => {
|
||||||
let timeStarted
|
let timeStarted
|
||||||
@@ -195,20 +217,20 @@ Extraction.beginOwn = async (extractionId, documentsNames) => {
|
|||||||
Extraction.beginOss = async extractionId => {
|
Extraction.beginOss = async extractionId => {
|
||||||
let timeStarted
|
let timeStarted
|
||||||
await db.transaction(async dbClient => {
|
await db.transaction(async dbClient => {
|
||||||
const insertExtractionJob = dbClient.query(
|
|
||||||
'INSERT INTO extraction_job (extraction_id, job_type, filename) VALUES ($1, $2, $3)',
|
|
||||||
[extractionId, 'oss term candidates', '']
|
|
||||||
)
|
|
||||||
const updateExtraction = dbClient.query(
|
|
||||||
"UPDATE extraction SET status = 'in progress', time_started = NOW() WHERE id = $1 RETURNING time_started",
|
|
||||||
[extractionId]
|
|
||||||
)
|
|
||||||
|
|
||||||
;[
|
;[
|
||||||
{
|
{
|
||||||
rows: [{ time_started: timeStarted }]
|
rows: [{ time_started: timeStarted }]
|
||||||
}
|
}
|
||||||
] = await Promise.all([updateExtraction, insertExtractionJob])
|
] = await Promise.all([
|
||||||
|
dbClient.query(
|
||||||
|
"UPDATE extraction SET status = 'in progress', time_started = NOW() WHERE id = $1 RETURNING time_started",
|
||||||
|
[extractionId]
|
||||||
|
),
|
||||||
|
dbClient.query(
|
||||||
|
'INSERT INTO extraction_job (extraction_id, job_type, filename) VALUES ($1, $2, $3)',
|
||||||
|
[extractionId, 'oss term candidates', '']
|
||||||
|
)
|
||||||
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
return timeStarted
|
return timeStarted
|
||||||
@@ -235,24 +257,34 @@ Extraction.processOwn = async function (extractionId, extractionName) {
|
|||||||
const documentNames = await this.fetchAllDocumentsNames(extractionId)
|
const documentNames = await this.fetchAllDocumentsNames(extractionId)
|
||||||
const conllusPath = getConllusPath(extractionId)
|
const conllusPath = getConllusPath(extractionId)
|
||||||
const conllusPaths = []
|
const conllusPaths = []
|
||||||
|
const MAX_BODY_LENGTH = 10 ** 9 // 1 GB
|
||||||
|
const RETRY_SECONDS_INTERVAL = 60 // 1 minute
|
||||||
|
const RETRY_SECONDS_MAX = 60 * 60 * 24 // 1 day
|
||||||
// Using remote API, transform each document into conllu format.
|
// Using remote API, transform each document into conllu format.
|
||||||
for (const documentName of documentNames) {
|
for (const documentName of documentNames) {
|
||||||
const filePath = `${documentsPath}/${documentName}`
|
const filePath = `${documentsPath}/${documentName}`
|
||||||
const form = new FormData()
|
|
||||||
form.append('file', createReadStream(filePath), documentName)
|
|
||||||
try {
|
try {
|
||||||
const { data: data1 } = await axios.post(
|
const { data: data1 } = await retry(
|
||||||
'http://rsdo.lhrs.feri.um.si:8080/datotekaVConlluAsync',
|
async () => {
|
||||||
form,
|
const form = new FormData()
|
||||||
{
|
form.append('file', createReadStream(filePath), documentName)
|
||||||
headers: {
|
return await axios.post(
|
||||||
...form.getHeaders()
|
`${extractionApiOrigin}/datotekaVConlluAsync`,
|
||||||
}
|
form,
|
||||||
}
|
{
|
||||||
|
headers: {
|
||||||
|
...form.getHeaders()
|
||||||
|
},
|
||||||
|
maxBodyLength: MAX_BODY_LENGTH
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
RETRY_SECONDS_INTERVAL,
|
||||||
|
RETRY_SECONDS_MAX
|
||||||
)
|
)
|
||||||
const remotejobId = +data1.check_job_url.split('/').at(-1)
|
const remotejobId = +data1.check_job_url.split('/').at(-1)
|
||||||
await db.query(
|
await db.query(
|
||||||
"UPDATE extraction_job SET status = 'in progress', remote_job_id = $1 WHERE extraction_id = $2 AND job_type = $3 AND filename = $4",
|
"UPDATE extraction_job SET status = 'in progress', remote_job_id = $1, time_started = NOW() WHERE extraction_id = $2 AND job_type = $3 AND filename = $4",
|
||||||
[remotejobId, extractionId, 'doc to conllu', documentName]
|
[remotejobId, extractionId, 'doc to conllu', documentName]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -261,11 +293,21 @@ Extraction.processOwn = async function (extractionId, extractionName) {
|
|||||||
// Poll job until finished.
|
// Poll job until finished.
|
||||||
while (true) {
|
while (true) {
|
||||||
await sleep(5)
|
await sleep(5)
|
||||||
const { data: data2 } = await axios.get(
|
const { data: data2 } = await retry(
|
||||||
`http://rsdo.lhrs.feri.um.si:8080/job/${remotejobId}`
|
async () => {
|
||||||
|
return await axios.get(`${extractionApiOrigin}/job/${remotejobId}`)
|
||||||
|
},
|
||||||
|
RETRY_SECONDS_INTERVAL,
|
||||||
|
RETRY_SECONDS_MAX
|
||||||
)
|
)
|
||||||
|
|
||||||
if (data2.finished_on) {
|
if (data2.finished_on) {
|
||||||
if (data2.job_status !== 'finished processing (OK)') throw Error()
|
if (data2.job_status !== 'finished processing (OK)') {
|
||||||
|
throw Error(
|
||||||
|
`Remote job with id ${remotejobId} failed with result:\n${data2.job_result}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO Read the response as a stream and try to parse it's contents into a file (write stream)('stream-json' package?).
|
// TODO Read the response as a stream and try to parse it's contents into a file (write stream)('stream-json' package?).
|
||||||
const fileSavePath = `${conllusPath}/${documentName}.conllu`
|
const fileSavePath = `${conllusPath}/${documentName}.conllu`
|
||||||
await writeFile(fileSavePath, data2.job_result)
|
await writeFile(fileSavePath, data2.job_result)
|
||||||
@@ -277,7 +319,8 @@ Extraction.processOwn = async function (extractionId, extractionName) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
logExtractionError(error, extractionId, 'doc to conllu', documentName)
|
||||||
await failTheJob(extractionId, 'doc to conllu', documentName)
|
await failTheJob(extractionId, 'doc to conllu', documentName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,34 +348,55 @@ Extraction.processOwn = async function (extractionId, extractionName) {
|
|||||||
stopTerms.forEach(stopTerm => stopTermsSet.add(stopTerm.trim()))
|
stopTerms.forEach(stopTerm => stopTermsSet.add(stopTerm.trim()))
|
||||||
}
|
}
|
||||||
stopTermsSet.delete('')
|
stopTermsSet.delete('')
|
||||||
|
const stopTermsArr = Array.from(stopTermsSet)
|
||||||
const termCandidatesPath = getTermCandidatesPath(extractionId)
|
const termCandidatesPath = getTermCandidatesPath(extractionId)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: data3 } = await axios.post(
|
const { data: data3 } = await retry(
|
||||||
'http://rsdo.lhrs.feri.um.si:8080/izlusciAsync',
|
async () => {
|
||||||
{
|
return await axios.post(
|
||||||
conllus: conllusArr,
|
`${extractionApiOrigin}/izlusciAsync`,
|
||||||
prepovedaneBesede: Array.from(stopTermsSet)
|
{
|
||||||
}
|
conllus: conllusArr,
|
||||||
|
prepovedaneBesede: stopTermsArr,
|
||||||
|
// TODO Enabled for all cases. Add a switch for users later.
|
||||||
|
definicije: true
|
||||||
|
},
|
||||||
|
{ maxBodyLength: MAX_BODY_LENGTH }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
RETRY_SECONDS_INTERVAL,
|
||||||
|
RETRY_SECONDS_MAX
|
||||||
)
|
)
|
||||||
const remotejobId = +data3.check_job_url.split('/').at(-1)
|
const remotejobId = +data3.check_job_url.split('/').at(-1)
|
||||||
await db.query(
|
await db.query(
|
||||||
"UPDATE extraction_job SET status = 'in progress', remote_job_id = $1 WHERE extraction_id = $2 AND job_type = $3 AND filename = $4",
|
"UPDATE extraction_job SET status = 'in progress', remote_job_id = $1, time_started = NOW() WHERE extraction_id = $2 AND job_type = $3 AND filename = $4",
|
||||||
[remotejobId, extractionId, 'conllus to term candidates', '']
|
[remotejobId, extractionId, 'conllus to term candidates', '']
|
||||||
)
|
)
|
||||||
|
|
||||||
// Poll job until finished.
|
// Poll job until finished.
|
||||||
while (true) {
|
while (true) {
|
||||||
await sleep(5)
|
await sleep(5)
|
||||||
const { data: data4 } = await axios.get(
|
const { data: data4 } = await retry(
|
||||||
`http://rsdo.lhrs.feri.um.si:8080/job/${remotejobId}`
|
async () => {
|
||||||
|
return await axios.get(`${extractionApiOrigin}/job/${remotejobId}`)
|
||||||
|
},
|
||||||
|
RETRY_SECONDS_INTERVAL,
|
||||||
|
RETRY_SECONDS_MAX
|
||||||
)
|
)
|
||||||
if (data4.finished_on) {
|
if (data4.finished_on) {
|
||||||
if (data4.job_status !== 'finished processing (OK)') throw Error()
|
const { job_result: jobResult } = data4
|
||||||
|
if (
|
||||||
|
data4.job_status !== 'finished processing (OK)' ||
|
||||||
|
!jobResult.terminoloski_kandidati
|
||||||
|
) {
|
||||||
|
throw Error(
|
||||||
|
`Remote job with id ${remotejobId} failed with result:\n${jobResult}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO Read the response as a stream and try to parse it's contents into a file (write stream)('stream-json' package?).
|
// TODO Read the response as a stream and try to parse it's contents into a file (write stream)('stream-json' package?).
|
||||||
await writeFile(termCandidatesPath, JSON.stringify(data4.job_result))
|
await writeFile(termCandidatesPath, JSON.stringify(jobResult))
|
||||||
// TODO Once returned JSON is properly formed, use the bottom line instead.
|
|
||||||
// await writeFile(termCandidatesPath, data4.job_result.terminoloski_kandidati)
|
|
||||||
await db.query(
|
await db.query(
|
||||||
"UPDATE extraction_job SET status = 'finished', time_finished = NOW() WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
|
"UPDATE extraction_job SET status = 'finished', time_finished = NOW() WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
|
||||||
[extractionId, 'conllus to term candidates', '']
|
[extractionId, 'conllus to term candidates', '']
|
||||||
@@ -340,8 +404,10 @@ Extraction.processOwn = async function (extractionId, extractionName) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
logExtractionError(error, extractionId, 'conllus to term candidates')
|
||||||
await failTheJob(extractionId, 'conllus to term candidates', '')
|
await failTheJob(extractionId, 'conllus to term candidates', '')
|
||||||
|
await skipConcordancerJob(extractionId)
|
||||||
await failExtraction(extractionId)
|
await failExtraction(extractionId)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -350,7 +416,7 @@ Extraction.processOwn = async function (extractionId, extractionName) {
|
|||||||
// Start concondancer corpus processing.
|
// Start concondancer corpus processing.
|
||||||
try {
|
try {
|
||||||
await db.query(
|
await db.query(
|
||||||
"UPDATE extraction_job SET status = 'in progress' WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
|
"UPDATE extraction_job SET status = 'in progress', time_started = NOW() WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
|
||||||
[extractionId, 'concordancer', '']
|
[extractionId, 'concordancer', '']
|
||||||
)
|
)
|
||||||
console.log('CREATING CORPUS')
|
console.log('CREATING CORPUS')
|
||||||
@@ -361,45 +427,107 @@ Extraction.processOwn = async function (extractionId, extractionName) {
|
|||||||
} = await axios.post('http://concordancer:5000/dashboard/corpus', {
|
} = await axios.post('http://concordancer:5000/dashboard/corpus', {
|
||||||
title: extractionName
|
title: extractionName
|
||||||
})
|
})
|
||||||
|
|
||||||
|
await db.query('UPDATE extraction SET corpus_id = $1 WHERE id = $2', [
|
||||||
|
corpusId,
|
||||||
|
extractionId
|
||||||
|
])
|
||||||
|
|
||||||
|
// Wait for creation of corpus.
|
||||||
|
while (true) {
|
||||||
|
console.log('SLEEP FOR 5 SECS')
|
||||||
|
await sleep(5)
|
||||||
|
const {
|
||||||
|
data: { status }
|
||||||
|
} = await axios.get(
|
||||||
|
`http://concordancer:5000/dashboard/corpus/${corpusId}`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (status === 'Creating') continue
|
||||||
|
if (status === 'Active') break
|
||||||
|
throw Error('Error creating concorcander corpus')
|
||||||
|
}
|
||||||
console.log('CORPUS CREATED')
|
console.log('CORPUS CREATED')
|
||||||
console.log('SLEEP FOR 10 SECS')
|
|
||||||
await sleep(10)
|
const inProgressStatusList = [
|
||||||
|
'Waiting',
|
||||||
|
'Importing',
|
||||||
|
'ImportingCompleted',
|
||||||
|
'Indexing',
|
||||||
|
'IndexingCompleted'
|
||||||
|
]
|
||||||
for (const conlluPath of conllusPaths) {
|
for (const conlluPath of conllusPaths) {
|
||||||
const textPathParts = conlluPath.split('/')
|
const textPathParts = conlluPath.split('/')
|
||||||
textPathParts[0] = '/data'
|
textPathParts[0] = '/data'
|
||||||
const textPath = textPathParts.join('/')
|
const textPath = textPathParts.join('/')
|
||||||
console.log('ADDING TEXT')
|
console.log('ADDING TEXT')
|
||||||
await axios.post(
|
const {
|
||||||
|
data: {
|
||||||
|
entityInfo: { id: textId }
|
||||||
|
}
|
||||||
|
} = await axios.post(
|
||||||
`http://concordancer:5000/dashboard/corpus/${corpusId}/text`,
|
`http://concordancer:5000/dashboard/corpus/${corpusId}/text`,
|
||||||
{ sourceFile: textPath }
|
{ sourceFile: textPath }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Wait for text ingestion.
|
||||||
|
while (true) {
|
||||||
|
console.log('SLEEP FOR 5 SECS')
|
||||||
|
await sleep(5)
|
||||||
|
const {
|
||||||
|
data: { status }
|
||||||
|
} = await axios.get(
|
||||||
|
`http://concordancer:5000/dashboard/corpus/${corpusId}/text/${textId}`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (inProgressStatusList.includes(status)) continue
|
||||||
|
if (status === 'Active') break
|
||||||
|
throw Error('Error importing concorcander text')
|
||||||
|
}
|
||||||
console.log('TEXT ADDED')
|
console.log('TEXT ADDED')
|
||||||
console.log('SLEEP FOR 10 SECS')
|
|
||||||
await sleep(10)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const termListPathParts = termCandidatesPath.split('/')
|
const termListPathParts = termCandidatesPath.split('/')
|
||||||
termListPathParts[0] = '/data'
|
termListPathParts[0] = '/data'
|
||||||
const termListPath = termListPathParts.join('/')
|
const termListPath = termListPathParts.join('/')
|
||||||
console.log('ADDING TERMS')
|
console.log('ADDING TERMS')
|
||||||
await axios.post(
|
const {
|
||||||
|
data: {
|
||||||
|
entityInfo: { id: termListId }
|
||||||
|
}
|
||||||
|
} = await axios.post(
|
||||||
`http://concordancer:5000/dashboard/corpus/${corpusId}/termList`,
|
`http://concordancer:5000/dashboard/corpus/${corpusId}/termList`,
|
||||||
{ sourceFile: termListPath }
|
{ sourceFile: termListPath }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Wait for term list ingestion.
|
||||||
|
while (true) {
|
||||||
|
console.log('SLEEP FOR 5 SECS')
|
||||||
|
await sleep(5)
|
||||||
|
const {
|
||||||
|
data: { status }
|
||||||
|
} = await axios.get(
|
||||||
|
`http://concordancer:5000/dashboard/corpus/${corpusId}/termList/${termListId}`
|
||||||
|
)
|
||||||
|
|
||||||
|
if (inProgressStatusList.includes(status)) continue
|
||||||
|
if (status === 'Active') break
|
||||||
|
throw Error('Error importing concorcander text')
|
||||||
|
}
|
||||||
console.log('TERMS ADDED')
|
console.log('TERMS ADDED')
|
||||||
|
|
||||||
await db.query(
|
await db.query(
|
||||||
"UPDATE extraction_job SET status = 'finished', time_finished = NOW() WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
|
"UPDATE extraction_job SET status = 'finished', time_finished = NOW() WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
|
||||||
[extractionId, 'concordancer', '']
|
[extractionId, 'concordancer', '']
|
||||||
)
|
)
|
||||||
|
|
||||||
await db.query(
|
await db.query(
|
||||||
"UPDATE extraction SET status = 'finished', time_finished = NOW(), corpus_id = $1 WHERE id = $2",
|
"UPDATE extraction SET status = 'finished', time_finished = NOW() WHERE id = $1",
|
||||||
[corpusId, extractionId]
|
[extractionId]
|
||||||
)
|
)
|
||||||
console.log('EXTRACTION SUCCESSFUL')
|
console.log('EXTRACTION SUCCESSFUL')
|
||||||
} catch (e) {
|
} catch (error) {
|
||||||
console.log('EXTRACTION ERROR')
|
logExtractionError(error, extractionId, 'concordancer')
|
||||||
console.log(e)
|
|
||||||
await failTheJob(extractionId, 'concordancer', '')
|
await failTheJob(extractionId, 'concordancer', '')
|
||||||
await failExtraction(extractionId)
|
await failExtraction(extractionId)
|
||||||
}
|
}
|
||||||
@@ -413,6 +541,8 @@ Extraction.processOss = async function (extractionId, ossParams) {
|
|||||||
// TODO Probably not, at least not while the the OSS enpoint is GET, due to limited length of URLs.
|
// TODO Probably not, at least not while the the OSS enpoint is GET, due to limited length of URLs.
|
||||||
// TODO Also consider refactoring certain parts,
|
// TODO Also consider refactoring certain parts,
|
||||||
// TODO as some are identical or similar to Own variants or used earlier in the same pipeline.
|
// TODO as some are identical or similar to Own variants or used earlier in the same pipeline.
|
||||||
|
const RETRY_SECONDS_INTERVAL = 60 // 1 minute
|
||||||
|
const RETRY_SECONDS_MAX = 60 * 60 * 24 // 1 day
|
||||||
const stopTermsPath = getStopTermsPath(extractionId)
|
const stopTermsPath = getStopTermsPath(extractionId)
|
||||||
const stopTermsFilesNames = await this.fetchAllStopTermsFilesNames(
|
const stopTermsFilesNames = await this.fetchAllStopTermsFilesNames(
|
||||||
extractionId
|
extractionId
|
||||||
@@ -435,31 +565,49 @@ Extraction.processOss = async function (extractionId, ossParams) {
|
|||||||
...(ossParams.documentType && { vrste: ossParams.documentType }),
|
...(ossParams.documentType && { vrste: ossParams.documentType }),
|
||||||
...(ossParams.keywords && { kljucneBesede: ossParams.keywords }),
|
...(ossParams.keywords && { kljucneBesede: ossParams.keywords }),
|
||||||
...(ossParams.domainUdk && { udk: ossParams.domainUdk }),
|
...(ossParams.domainUdk && { udk: ossParams.domainUdk }),
|
||||||
...(stopTerms.length && { prepovedaneBesede: stopTerms })
|
...(stopTerms.length && { prepovedaneBesede: stopTerms }),
|
||||||
|
// TODO Enabled for all cases. Add a switch for users later.
|
||||||
|
definicije: true
|
||||||
})
|
})
|
||||||
|
|
||||||
const extractApiUrl = `http://rsdo.lhrs.feri.um.si:8080/oss/izlusciPoIskanjuAsync?${searchParams}`
|
const extractApiUrl = `${extractionApiOrigin}/oss/izlusciPoIskanjuAsync?${searchParams}`
|
||||||
try {
|
try {
|
||||||
const { data: data1 } = await axios.get(extractApiUrl)
|
const { data: data1 } = await retry(
|
||||||
|
async () => {
|
||||||
|
return await axios.get(extractApiUrl)
|
||||||
|
},
|
||||||
|
RETRY_SECONDS_INTERVAL,
|
||||||
|
RETRY_SECONDS_MAX
|
||||||
|
)
|
||||||
const remotejobId = +data1.check_job_url.split('/').at(-1)
|
const remotejobId = +data1.check_job_url.split('/').at(-1)
|
||||||
await db.query(
|
await db.query(
|
||||||
"UPDATE extraction_job SET status = 'in progress', remote_job_id = $1 WHERE extraction_id = $2 AND job_type = $3 AND filename = $4",
|
"UPDATE extraction_job SET status = 'in progress', remote_job_id = $1, time_started = NOW() WHERE extraction_id = $2 AND job_type = $3 AND filename = $4",
|
||||||
[remotejobId, extractionId, 'oss term candidates', '']
|
[remotejobId, extractionId, 'oss term candidates', '']
|
||||||
)
|
)
|
||||||
|
|
||||||
// Poll job until finished.
|
// Poll job until finished.
|
||||||
while (true) {
|
while (true) {
|
||||||
await sleep(5)
|
await sleep(5)
|
||||||
const { data: data2 } = await axios.get(
|
const { data: data2 } = await retry(
|
||||||
`http://rsdo.lhrs.feri.um.si:8080/job/${remotejobId}`
|
async () => {
|
||||||
|
return await axios.get(`${extractionApiOrigin}/job/${remotejobId}`)
|
||||||
|
},
|
||||||
|
RETRY_SECONDS_INTERVAL,
|
||||||
|
RETRY_SECONDS_MAX
|
||||||
)
|
)
|
||||||
if (data2.finished_on) {
|
if (data2.finished_on) {
|
||||||
if (data2.job_status !== 'finished processing (OK)') throw Error()
|
if (
|
||||||
|
data2.job_status !== 'finished processing (OK)' ||
|
||||||
|
!Array.isArray(data2.job_result?.terminoloski_kandidati)
|
||||||
|
) {
|
||||||
|
throw Error(
|
||||||
|
`Remote job with id ${remotejobId} failed with result:\n${data2.job_result}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// TODO Read the response as a stream and try to parse it's contents into a file (write stream)('stream-json' package?).
|
// TODO Read the response as a stream and try to parse it's contents into a file (write stream)('stream-json' package?).
|
||||||
const termCandidatesPath = getTermCandidatesPath(extractionId)
|
const termCandidatesPath = getTermCandidatesPath(extractionId)
|
||||||
await writeFile(termCandidatesPath, JSON.stringify(data2.job_result))
|
await writeFile(termCandidatesPath, JSON.stringify(data2.job_result))
|
||||||
// TODO Once returned JSON is properly formed, use the bottom line instead.
|
|
||||||
// await writeFile(termCandidatesPath, data4.job_result.terminoloski_kandidati)
|
|
||||||
await db.query(
|
await db.query(
|
||||||
"UPDATE extraction_job SET status = 'finished', time_finished = NOW() WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
|
"UPDATE extraction_job SET status = 'finished', time_finished = NOW() WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
|
||||||
[extractionId, 'oss term candidates', '']
|
[extractionId, 'oss term candidates', '']
|
||||||
@@ -474,7 +622,8 @@ Extraction.processOss = async function (extractionId, ossParams) {
|
|||||||
"UPDATE extraction SET status = 'finished', time_finished = NOW() WHERE id = $1",
|
"UPDATE extraction SET status = 'finished', time_finished = NOW() WHERE id = $1",
|
||||||
[extractionId]
|
[extractionId]
|
||||||
)
|
)
|
||||||
} catch {
|
} catch (error) {
|
||||||
|
logExtractionError(error, extractionId, 'oss term candidates')
|
||||||
await failTheJob(extractionId, 'oss term candidates', '')
|
await failTheJob(extractionId, 'oss term candidates', '')
|
||||||
await failExtraction(extractionId)
|
await failExtraction(extractionId)
|
||||||
}
|
}
|
||||||
@@ -487,6 +636,13 @@ async function failTheJob(extractionId, jobType, documentName) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function skipConcordancerJob(extractionId) {
|
||||||
|
await db.query(
|
||||||
|
"UPDATE extraction_job SET status = 'skipped' WHERE extraction_id = $1 AND job_type = $2",
|
||||||
|
[extractionId, 'concordancer']
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function failExtraction(extractionId) {
|
async function failExtraction(extractionId) {
|
||||||
await db.query(
|
await db.query(
|
||||||
"UPDATE extraction SET status = 'failed', time_finished = NOW() WHERE id = $1",
|
"UPDATE extraction SET status = 'failed', time_finished = NOW() WHERE id = $1",
|
||||||
@@ -498,4 +654,63 @@ function sleep(seconds) {
|
|||||||
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
|
return new Promise(resolve => setTimeout(resolve, seconds * 1000))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function logExtractionError(error, extractionId, jobType, filename) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(
|
||||||
|
Error(`Failed extraction job:
|
||||||
|
extractionId: ${extractionId},
|
||||||
|
jobType: ${jobType},
|
||||||
|
filename: ${filename}`)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error.isAxiosError) error = Error(`Axios message: ${error.message}`)
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function retry(callback, everySeconds, maxSeconds) {
|
||||||
|
const startTime = new Date()
|
||||||
|
let numOfRetries = 0
|
||||||
|
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
while (true) {
|
||||||
|
try {
|
||||||
|
const result = await callback()
|
||||||
|
if (numOfRetries) {
|
||||||
|
console.log(
|
||||||
|
`Recovered after ${numOfRetries} retries and ${Math.floor(
|
||||||
|
(new Date() - startTime) / 1000
|
||||||
|
)} seconds`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
} catch (error) {
|
||||||
|
const secondsSinceStart = Math.floor((new Date() - startTime) / 1000)
|
||||||
|
const nextRetrySeconds = secondsSinceStart + everySeconds
|
||||||
|
|
||||||
|
console.log('Failed inside retry')
|
||||||
|
console.log(
|
||||||
|
error.isAxiosError ? `Axios message: ${error.message}` : error
|
||||||
|
)
|
||||||
|
console.log({
|
||||||
|
numOfRetries,
|
||||||
|
secondsSinceStart,
|
||||||
|
everySeconds,
|
||||||
|
nextRetrySeconds,
|
||||||
|
maxSeconds
|
||||||
|
})
|
||||||
|
|
||||||
|
if (nextRetrySeconds > maxSeconds) {
|
||||||
|
console.log('FAILING RETRIES')
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`RETRYING IN ${everySeconds} SECONDS`)
|
||||||
|
numOfRetries++
|
||||||
|
await sleep(everySeconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* eslint-enable no-console */
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = Extraction
|
module.exports = Extraction
|
||||||
|
|||||||
@@ -0,0 +1,486 @@
|
|||||||
|
const xmlFlow = require('xml-flow')
|
||||||
|
const xss = require('xss')
|
||||||
|
|
||||||
|
exports.transformAndAppend = async (
|
||||||
|
entries,
|
||||||
|
exportFields,
|
||||||
|
exportFile,
|
||||||
|
exportFileFormat,
|
||||||
|
dsvConfig
|
||||||
|
) => {
|
||||||
|
let transformEntry
|
||||||
|
if (exportFileFormat === 'xml') transformEntry = intoXml
|
||||||
|
else if (dsvConfig) transformEntry = intoDsv(dsvConfig)
|
||||||
|
else if (exportFileFormat === 'tbx') transformEntry = intoTbx
|
||||||
|
else throw Error('Specified export format not supported yet')
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const transformedEntry = transformEntry(entry, exportFields)
|
||||||
|
await exportFile.write(`${transformedEntry}\n`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function intoXml(entry, exportFields) {
|
||||||
|
const entryObj = { $name: 'entry', $markup: [] }
|
||||||
|
|
||||||
|
if (entry.term) {
|
||||||
|
entryObj.$markup.push({ term: entry.term })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.domainLabels && entry.domain_labels.length) {
|
||||||
|
const domainLabelsObj = {
|
||||||
|
$name: 'domainLabels',
|
||||||
|
$markup: entry.domain_labels.map(domainLabel => {
|
||||||
|
return { domainLabel }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
entryObj.$markup.push(domainLabelsObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.label && entry.label) {
|
||||||
|
entryObj.$markup.push({ label: entry.label })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.definition && entry.definition) {
|
||||||
|
entryObj.$markup.push({ def: entry.definition })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.synonyms && entry.synonyms?.length) {
|
||||||
|
const SynonymsObj = {
|
||||||
|
$name: 'syns',
|
||||||
|
$markup: entry.synonyms.map(synonym => {
|
||||||
|
return { syn: synonym }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
entryObj.$markup.push(SynonymsObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.links && entry.links.length) {
|
||||||
|
const LinksObj = {
|
||||||
|
$name: 'links',
|
||||||
|
$markup: entry.links.map(linkObj => {
|
||||||
|
return {
|
||||||
|
$name: 'link',
|
||||||
|
$attrs: { type: linkObj.type },
|
||||||
|
$text: linkObj.link
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
entryObj.$markup.push(LinksObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.other && entry.other) {
|
||||||
|
entryObj.$markup.push({ other: entry.other })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.foreignTerms && entry.foreign_entries.length) {
|
||||||
|
const fLangsObj = {
|
||||||
|
$name: 'fLangs',
|
||||||
|
$markup: entry.foreign_entries.map(fEntryObj => {
|
||||||
|
const fLangObj = {
|
||||||
|
$name: 'fLang',
|
||||||
|
$attrs: { lang: fEntryObj.lang_code },
|
||||||
|
$markup: []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fEntryObj.terms) {
|
||||||
|
const fTermsObj = {
|
||||||
|
$name: 'fTerms',
|
||||||
|
$markup: fEntryObj.terms.map(term => {
|
||||||
|
return { fTerm: term }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fLangObj.$markup.push(fTermsObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.foreignDefinitions && fEntryObj.definition) {
|
||||||
|
fLangObj.$markup.push({ fDef: fEntryObj.definition })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.foreignSynonyms && fEntryObj.synonyms?.length) {
|
||||||
|
const fSynsObj = {
|
||||||
|
$name: 'fSyns',
|
||||||
|
$markup: fEntryObj.synonyms.map(synonym => {
|
||||||
|
return { fSyn: synonym }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
fLangObj.$markup.push(fSynsObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fLangObj
|
||||||
|
})
|
||||||
|
}
|
||||||
|
entryObj.$markup.push(fLangsObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldExportImages = exportFields.images && entry.image?.length
|
||||||
|
const shouldExportAudio = exportFields.audio && entry.audio?.length
|
||||||
|
const shouldExportVideos = exportFields.videos && entry.video?.length
|
||||||
|
const shouldCreateMm =
|
||||||
|
shouldExportImages || shouldExportAudio || shouldExportVideos
|
||||||
|
|
||||||
|
if (shouldCreateMm) {
|
||||||
|
const MmObj = {
|
||||||
|
$name: 'mm',
|
||||||
|
$markup: []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldExportImages) {
|
||||||
|
entry.image.forEach(el => {
|
||||||
|
MmObj.$markup.push({ image: el })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldExportAudio) {
|
||||||
|
entry.audio.forEach(el => {
|
||||||
|
MmObj.$markup.push({ audio: el })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldExportVideos) {
|
||||||
|
entry.video.forEach(el => {
|
||||||
|
MmObj.$markup.push({ video: el })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
entryObj.$markup.push(MmObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
return xmlFlow.toXml(entryObj, { escape: str => str })
|
||||||
|
}
|
||||||
|
|
||||||
|
function intoDsv({ delimiter, languageCodes }) {
|
||||||
|
return function (entry, exportFields) {
|
||||||
|
const fieldsArr = [intoDsvField(entry.term)]
|
||||||
|
|
||||||
|
if (exportFields.domainLabels) {
|
||||||
|
fieldsArr.push(
|
||||||
|
intoDsvField(
|
||||||
|
entry.domain_labels.reduce((agg, domainLabel, index) => {
|
||||||
|
agg += `${index ? '\n' : ''}${domainLabel}`
|
||||||
|
return agg
|
||||||
|
}, '')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.label) fieldsArr.push(intoDsvField(entry.label))
|
||||||
|
|
||||||
|
if (exportFields.definition) fieldsArr.push(intoDsvField(entry.definition))
|
||||||
|
|
||||||
|
if (exportFields.synonyms) {
|
||||||
|
fieldsArr.push(
|
||||||
|
intoDsvField(
|
||||||
|
entry.synonyms?.reduce((agg, synonym, index) => {
|
||||||
|
agg += `${index ? '\n' : ''}${synonym}`
|
||||||
|
return agg
|
||||||
|
}, '')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.links) {
|
||||||
|
fieldsArr.push(
|
||||||
|
intoDsvField(
|
||||||
|
entry.links.reduce((agg, linkObj, index) => {
|
||||||
|
agg += `${index ? '\n' : ''}[${linkObj.type[0]}t]${linkObj.link}`
|
||||||
|
return agg
|
||||||
|
}, '')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.other) fieldsArr.push(intoDsvField(entry.other))
|
||||||
|
|
||||||
|
languageCodes?.forEach(languageCode => {
|
||||||
|
const fEntryObj = entry.foreign_entries.find(
|
||||||
|
entryObj => entryObj.lang_code === languageCode
|
||||||
|
)
|
||||||
|
|
||||||
|
fieldsArr.push(
|
||||||
|
intoDsvField(
|
||||||
|
fEntryObj?.terms?.reduce((agg, term, index) => {
|
||||||
|
agg += `${index ? '\n' : ''}${term}`
|
||||||
|
return agg
|
||||||
|
}, '')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (exportFields.foreignDefinitions) {
|
||||||
|
fieldsArr.push(intoDsvField(fEntryObj?.definition))
|
||||||
|
}
|
||||||
|
if (exportFields.foreignSynonyms) {
|
||||||
|
fieldsArr.push(
|
||||||
|
intoDsvField(
|
||||||
|
fEntryObj?.synonyms?.reduce((agg, synonym, index) => {
|
||||||
|
agg += `${index ? '\n' : ''}${synonym}`
|
||||||
|
return agg
|
||||||
|
}, '')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (exportFields.images) {
|
||||||
|
fieldsArr.push(
|
||||||
|
intoDsvField(
|
||||||
|
entry.image?.reduce((agg, el, index) => {
|
||||||
|
agg += `${index ? '\n' : ''}${el}`
|
||||||
|
return agg
|
||||||
|
}, '')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.audio) {
|
||||||
|
fieldsArr.push(
|
||||||
|
intoDsvField(
|
||||||
|
entry.audio?.reduce((agg, el, index) => {
|
||||||
|
agg += `${index ? '\n' : ''}${el}`
|
||||||
|
return agg
|
||||||
|
}, '')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.videos) {
|
||||||
|
fieldsArr.push(
|
||||||
|
intoDsvField(
|
||||||
|
entry.video?.reduce((agg, el, index) => {
|
||||||
|
agg += `${index ? '\n' : ''}${el}`
|
||||||
|
return agg
|
||||||
|
}, '')
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fieldsArr.join(delimiter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function intoDsvField(fieldString) {
|
||||||
|
if (!fieldString) return ''
|
||||||
|
return `"${fieldString.replaceAll('"', '""')}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const tbxLinkTypeMap = {
|
||||||
|
related: 'relatedConcept',
|
||||||
|
broader: 'relatedConceptBroader',
|
||||||
|
narrow: 'relatedConceptNarrower'
|
||||||
|
}
|
||||||
|
const brTagPattern = /<br[^>]*>/
|
||||||
|
function intoTbx(entry, exportFields) {
|
||||||
|
const entryObj = { $name: 'termEntry', $markup: [] }
|
||||||
|
|
||||||
|
if (exportFields.images) {
|
||||||
|
entry.image?.forEach(el => {
|
||||||
|
entryObj.$markup.push({
|
||||||
|
$name: 'xref',
|
||||||
|
$attrs: { type: 'xGraphic' },
|
||||||
|
$text: el
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.audio) {
|
||||||
|
entry.audio?.forEach(el => {
|
||||||
|
entryObj.$markup.push({
|
||||||
|
$name: 'xref',
|
||||||
|
$attrs: { type: 'xAudio' },
|
||||||
|
$text: el
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.videos) {
|
||||||
|
entry.video?.forEach(el => {
|
||||||
|
entryObj.$markup.push({
|
||||||
|
$name: 'xref',
|
||||||
|
$attrs: { type: 'xVideo' },
|
||||||
|
$text: el
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const slLangObj = {
|
||||||
|
$name: 'langSet',
|
||||||
|
$attrs: { 'xml:lang': 'sl' },
|
||||||
|
$markup: []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.label && entry.label) {
|
||||||
|
slLangObj.$markup.push({
|
||||||
|
$name: 'descrip',
|
||||||
|
$attrs: { type: 'explanation' },
|
||||||
|
$text: intoTbxMixed(entry.label)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.definition && entry.definition) {
|
||||||
|
slLangObj.$markup.push({
|
||||||
|
$name: 'descrip',
|
||||||
|
$attrs: { type: 'definition' },
|
||||||
|
$text: intoTbxMixed(entry.definition)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.links) {
|
||||||
|
entry.links.forEach(linkObj => {
|
||||||
|
slLangObj.$markup.push({
|
||||||
|
$name: 'descrip',
|
||||||
|
$attrs: { type: tbxLinkTypeMap[linkObj.type] },
|
||||||
|
$text: intoTbxMixed(linkObj.link)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.other && entry.other) {
|
||||||
|
const otherLines = entry.other.split(brTagPattern)
|
||||||
|
otherLines.forEach(line => {
|
||||||
|
slLangObj.$markup.push({
|
||||||
|
$name: 'descrip',
|
||||||
|
$attrs: { type: 'other' },
|
||||||
|
$text: intoTbxMixed(line)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const shouldExportTerm = entry.term
|
||||||
|
const shouldExportDomainLabels =
|
||||||
|
exportFields.domainLabels && entry.domain_labels.length
|
||||||
|
const shouldCreateTermNtig = shouldExportTerm || shouldExportDomainLabels
|
||||||
|
|
||||||
|
if (shouldCreateTermNtig) {
|
||||||
|
const termNtigObj = {
|
||||||
|
$name: 'ntig',
|
||||||
|
$markup: [{ $name: 'termGrp', $markup: [] }]
|
||||||
|
}
|
||||||
|
const termGrpMarkup = termNtigObj.$markup[0].$markup
|
||||||
|
|
||||||
|
if (shouldExportTerm) {
|
||||||
|
termGrpMarkup.push({ term: intoTbxMixed(entry.term) })
|
||||||
|
termGrpMarkup.push({
|
||||||
|
$name: 'termNote',
|
||||||
|
$attrs: { type: 'termType' },
|
||||||
|
$text: 'entryTerm'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldExportDomainLabels) {
|
||||||
|
entry.domain_labels.forEach(domainLabel => {
|
||||||
|
termGrpMarkup.push({
|
||||||
|
$name: 'termNote',
|
||||||
|
$attrs: { type: 'domain' },
|
||||||
|
$text: domainLabel
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
slLangObj.$markup.push(termNtigObj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.synonyms) {
|
||||||
|
entry.synonyms?.forEach(synonym => {
|
||||||
|
slLangObj.$markup.push({
|
||||||
|
$name: 'ntig',
|
||||||
|
$markup: [
|
||||||
|
{
|
||||||
|
$name: 'termGrp',
|
||||||
|
$markup: [
|
||||||
|
{ term: intoTbxMixed(synonym) },
|
||||||
|
{
|
||||||
|
$name: 'termNote',
|
||||||
|
$attrs: { type: 'termType' },
|
||||||
|
$text: 'synonym'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
entryObj.$markup.push(slLangObj)
|
||||||
|
|
||||||
|
if (exportFields.foreignTerms) {
|
||||||
|
entry.foreign_entries.forEach(fEntryObj => {
|
||||||
|
const langSetObj = {
|
||||||
|
$name: 'langSet',
|
||||||
|
$attrs: { 'xml:lang': fEntryObj.lang_code },
|
||||||
|
$markup: []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (exportFields.foreignDefinitions && fEntryObj.definition) {
|
||||||
|
langSetObj.$markup.push({
|
||||||
|
$name: 'descrip',
|
||||||
|
$attrs: { type: 'definition' },
|
||||||
|
$text: intoTbxMixed(fEntryObj.definition)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fEntryObj.terms?.forEach(term => {
|
||||||
|
langSetObj.$markup.push({
|
||||||
|
$name: 'ntig',
|
||||||
|
$markup: [
|
||||||
|
{
|
||||||
|
$name: 'termGrp',
|
||||||
|
$markup: [{ term: intoTbxMixed(term) }]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
if (exportFields.foreignSynonyms) {
|
||||||
|
fEntryObj.synonyms?.forEach(synonym => {
|
||||||
|
langSetObj.$markup.push({
|
||||||
|
$name: 'ntig',
|
||||||
|
$markup: [
|
||||||
|
{
|
||||||
|
$name: 'termGrp',
|
||||||
|
$markup: [
|
||||||
|
{ term: intoTbxMixed(synonym) },
|
||||||
|
{
|
||||||
|
$name: 'termNote',
|
||||||
|
$attrs: { type: 'termType' },
|
||||||
|
$text: 'synonym'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
entryObj.$markup.push(langSetObj)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return xmlFlow.toXml(entryObj, { escape: str => str })
|
||||||
|
}
|
||||||
|
|
||||||
|
const tbxRichFilter = new xss.FilterXSS({
|
||||||
|
whiteList: {
|
||||||
|
sup: [],
|
||||||
|
sub: [],
|
||||||
|
b: [],
|
||||||
|
i: []
|
||||||
|
},
|
||||||
|
stripIgnoreTag: true,
|
||||||
|
stripIgnoreTagBody: ['script', 'style']
|
||||||
|
})
|
||||||
|
const tagPattern = /<\s*(\/)?\s*([^\s/>]+)\s*>/g
|
||||||
|
function intoTbxMixed(mixedContentStr) {
|
||||||
|
return tbxRichFilter
|
||||||
|
.process(mixedContentStr)
|
||||||
|
.replace(tagPattern, tbxMixedReplacer)
|
||||||
|
}
|
||||||
|
|
||||||
|
const tbxMixedTypeMap = {
|
||||||
|
sup: 'superscript',
|
||||||
|
sub: 'subscript',
|
||||||
|
b: 'bold',
|
||||||
|
i: 'italics'
|
||||||
|
}
|
||||||
|
function tbxMixedReplacer(match, closingSlash, tagName) {
|
||||||
|
if (closingSlash) return '</hi>'
|
||||||
|
return `<hi type="${tbxMixedTypeMap[tagName]}">`
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
const fs = require('fs')
|
const fs = require('fs')
|
||||||
const xmlFlow = require('xml-flow')
|
const xmlFlow = require('xml-flow')
|
||||||
const xss = require('xss')
|
|
||||||
const debug = require('debug')('termPortal:models/helpers/dictionary')
|
const debug = require('debug')('termPortal:models/helpers/dictionary')
|
||||||
const db = require('../../db')
|
const db = require('../../db')
|
||||||
const { intoDbArray } = require('..')
|
const { intoDbArray } = require('..')
|
||||||
|
const { sanitizeField } = require('./index')
|
||||||
|
|
||||||
const JOBS_MAX = 50
|
const JOBS_MAX = 50
|
||||||
const JOBS_MIN = 15
|
const JOBS_MIN = 15
|
||||||
@@ -199,84 +199,18 @@ function handleEntryXml(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const markupFilter = {
|
|
||||||
noMixed: new xss.FilterXSS({
|
|
||||||
whiteList: {},
|
|
||||||
stripIgnoreTag: true,
|
|
||||||
stripIgnoreTagBody: ['script', 'style']
|
|
||||||
}),
|
|
||||||
|
|
||||||
mixedBasic: new xss.FilterXSS({
|
|
||||||
whiteList: {
|
|
||||||
sup: [],
|
|
||||||
sub: []
|
|
||||||
},
|
|
||||||
stripIgnoreTag: true,
|
|
||||||
stripIgnoreTagBody: ['script', 'style']
|
|
||||||
}),
|
|
||||||
|
|
||||||
mixedExtended: new xss.FilterXSS({
|
|
||||||
whiteList: {
|
|
||||||
sup: [],
|
|
||||||
sub: [],
|
|
||||||
b: [],
|
|
||||||
i: [],
|
|
||||||
a: ['href']
|
|
||||||
},
|
|
||||||
stripIgnoreTag: true,
|
|
||||||
stripIgnoreTagBody: ['script', 'style'],
|
|
||||||
onTag: customTagHandler
|
|
||||||
}),
|
|
||||||
|
|
||||||
mixedOther: new xss.FilterXSS({
|
|
||||||
whiteList: {
|
|
||||||
sup: [],
|
|
||||||
sub: [],
|
|
||||||
b: [],
|
|
||||||
i: [],
|
|
||||||
a: ['href'],
|
|
||||||
br: []
|
|
||||||
},
|
|
||||||
stripIgnoreTag: true,
|
|
||||||
stripIgnoreTagBody: ['script', 'style'],
|
|
||||||
onTag: customTagHandler
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function customTagHandler(tag, html, { isWhite, isClosing }) {
|
|
||||||
// Special treatment only for whitelisted opening anchor tags.
|
|
||||||
if (tag !== 'a' || !isWhite || isClosing) return
|
|
||||||
|
|
||||||
const matchUrl = html.match(/href="?(?<url>https?:\/\/.*?)"?[\s>]/)
|
|
||||||
const url = matchUrl ? xss.escapeAttrValue(matchUrl.groups.url) : undefined
|
|
||||||
|
|
||||||
return `<a href${url ? `="${url}" target="_blank"` : ''}>`
|
|
||||||
}
|
|
||||||
|
|
||||||
function toText(markupObj) {
|
function toText(markupObj) {
|
||||||
return markupFilter.noMixed
|
return sanitizeField.toText(xmlFlow.toXml(markupObj))
|
||||||
.process(xmlFlow.toXml(markupObj))
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toMixedBasic(markupObj) {
|
function toMixedBasic(markupObj) {
|
||||||
return markupFilter.mixedBasic
|
return sanitizeField.toMixedBasic(xmlFlow.toXml(markupObj))
|
||||||
.process(xmlFlow.toXml(markupObj))
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toMixedExtended(markupObj) {
|
function toMixedExtended(markupObj) {
|
||||||
return markupFilter.mixedExtended
|
return sanitizeField.toMixedExtended(xmlFlow.toXml(markupObj))
|
||||||
.process(xmlFlow.toXml(markupObj))
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toMixedOther(markupObj) {
|
function toMixedOther(markupObj) {
|
||||||
return markupFilter.mixedOther
|
return sanitizeField.toMixedOther(xmlFlow.toXml(markupObj))
|
||||||
.process(xmlFlow.toXml(markupObj))
|
|
||||||
.replace(/\s+/g, ' ')
|
|
||||||
.trim()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,23 @@
|
|||||||
|
const xss = require('xss')
|
||||||
const { removeHtmlTags } = require('../../helpers')
|
const { removeHtmlTags } = require('../../helpers')
|
||||||
const { searchEngineClient, ENTRY_INDEX } = require('../../search-engine')
|
const { searchEngineClient, ENTRY_INDEX } = require('../../search-engine')
|
||||||
|
const { DATA_FILES_PATH } = require('../../../config/settings')
|
||||||
|
|
||||||
exports.deserialize = {
|
exports.deserialize = {
|
||||||
|
dictionary(dictionary) {
|
||||||
|
const deserializedDictionary = {
|
||||||
|
id: dictionary.id,
|
||||||
|
name: dictionary.name,
|
||||||
|
timeModified: dictionary.time_modified,
|
||||||
|
status: dictionary.status,
|
||||||
|
countEntries: dictionary.count_entries,
|
||||||
|
countComments: dictionary.count_comments,
|
||||||
|
isAdmin: dictionary.is_admin
|
||||||
|
}
|
||||||
|
|
||||||
|
return deserializedDictionary
|
||||||
|
},
|
||||||
|
|
||||||
primaryDomain(domain) {
|
primaryDomain(domain) {
|
||||||
const deserializedDomain = {
|
const deserializedDomain = {
|
||||||
id: domain.id,
|
id: domain.id,
|
||||||
@@ -33,16 +49,16 @@ exports.deserialize = {
|
|||||||
return deserializedDomain
|
return deserializedDomain
|
||||||
},
|
},
|
||||||
|
|
||||||
language(language) {
|
// language(language) {
|
||||||
const deserializedLanguage = {
|
// const deserializedLanguage = {
|
||||||
id: language.id,
|
// id: language.id,
|
||||||
code: language.code,
|
// code: language.code,
|
||||||
nameSl: language.name_sl,
|
// nameSl: language.name_sl,
|
||||||
nameEn: language.name_en
|
// nameEn: language.name_en
|
||||||
}
|
// }
|
||||||
|
|
||||||
return deserializedLanguage
|
// return deserializedLanguage
|
||||||
},
|
// },
|
||||||
|
|
||||||
editDescription(dictionary) {
|
editDescription(dictionary) {
|
||||||
const deserializedDictionary = {
|
const deserializedDictionary = {
|
||||||
@@ -62,7 +78,7 @@ exports.deserialize = {
|
|||||||
editUsers(dictionary) {
|
editUsers(dictionary) {
|
||||||
const deserializedDictionary = {
|
const deserializedDictionary = {
|
||||||
id: dictionary.id,
|
id: dictionary.id,
|
||||||
nameSl: dictionary.name_sl,
|
name: dictionary.name,
|
||||||
terminologyReviewFlag: dictionary.entries_have_terminology_review_flag,
|
terminologyReviewFlag: dictionary.entries_have_terminology_review_flag,
|
||||||
languageReviewFlag: dictionary.entries_have_language_review_flag,
|
languageReviewFlag: dictionary.entries_have_language_review_flag,
|
||||||
status: dictionary.status
|
status: dictionary.status
|
||||||
@@ -75,6 +91,7 @@ exports.deserialize = {
|
|||||||
const deserializedDictionary = {
|
const deserializedDictionary = {
|
||||||
id: dictionary.id,
|
id: dictionary.id,
|
||||||
nameSl: dictionary.name_sl,
|
nameSl: dictionary.name_sl,
|
||||||
|
nameEn: dictionary.name_en,
|
||||||
hasDomainLabels: dictionary.entries_have_domain_labels,
|
hasDomainLabels: dictionary.entries_have_domain_labels,
|
||||||
hasLabel: dictionary.entries_have_label,
|
hasLabel: dictionary.entries_have_label,
|
||||||
hasDefinition: dictionary.entries_have_definition,
|
hasDefinition: dictionary.entries_have_definition,
|
||||||
@@ -102,16 +119,78 @@ exports.deserialize = {
|
|||||||
return deserializedDomainLabel
|
return deserializedDomainLabel
|
||||||
},
|
},
|
||||||
|
|
||||||
imports(oneImport) {
|
// imports(oneImport) {
|
||||||
const deserializedImports = {
|
// const deserializedImports = {
|
||||||
timeStarted: oneImport.time_started,
|
// timeStarted: oneImport.time_started,
|
||||||
status: oneImport.status,
|
// status: oneImport.status,
|
||||||
deleteExisting: oneImport.delete_existing_entries,
|
// deleteExisting: oneImport.delete_existing_entries,
|
||||||
fileFormat: oneImport.file_format,
|
// fileFormat: oneImport.file_format,
|
||||||
countValidEntries: oneImport.count_valid_entries
|
// countValidEntries: oneImport.count_valid_entries
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return deserializedImports
|
||||||
|
// },
|
||||||
|
|
||||||
|
exports(oneExport) {
|
||||||
|
const deserializedExports = {
|
||||||
|
id: oneExport.id,
|
||||||
|
status: oneExport.status,
|
||||||
|
dateCreated: oneExport.date_created,
|
||||||
|
entryCount: oneExport.entry_count ?? '',
|
||||||
|
typeString: `${
|
||||||
|
oneExport.is_valid_filter === true
|
||||||
|
? 2
|
||||||
|
: oneExport.is_valid_filter === false
|
||||||
|
? 3
|
||||||
|
: 1
|
||||||
|
}${
|
||||||
|
oneExport.is_published_filter === true
|
||||||
|
? 2
|
||||||
|
: oneExport.is_published_filter === false
|
||||||
|
? 3
|
||||||
|
: 1
|
||||||
|
}${
|
||||||
|
oneExport.status_filter === 'complete'
|
||||||
|
? 2
|
||||||
|
: oneExport.status_filter === 'in_edit'
|
||||||
|
? 3
|
||||||
|
: 1
|
||||||
|
}${
|
||||||
|
oneExport.is_terminology_reviewed_filter === true
|
||||||
|
? 2
|
||||||
|
: oneExport.is_terminology_reviewed_filter === false
|
||||||
|
? 3
|
||||||
|
: 1
|
||||||
|
}${
|
||||||
|
oneExport.is_language_reviewed_filter === true
|
||||||
|
? 2
|
||||||
|
: oneExport.is_language_reviewed_filter === false
|
||||||
|
? 3
|
||||||
|
: 1
|
||||||
|
}-${
|
||||||
|
oneExport.export_file_format === 'xml'
|
||||||
|
? 1
|
||||||
|
: oneExport.export_file_format === 'csv'
|
||||||
|
? 2
|
||||||
|
: oneExport.export_file_format === 'tsv'
|
||||||
|
? 3
|
||||||
|
: 0
|
||||||
|
}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return deserializedImports
|
return deserializedExports
|
||||||
|
},
|
||||||
|
|
||||||
|
exportDownloadMetadata(metadata) {
|
||||||
|
const deserializedMetadata = {
|
||||||
|
exportStatus: metadata.status,
|
||||||
|
dictionaryId: metadata.dictionary_id,
|
||||||
|
nameString: metadata.name_string,
|
||||||
|
timeString: metadata.time_string,
|
||||||
|
fileFormat: metadata.export_file_format
|
||||||
|
}
|
||||||
|
|
||||||
|
return deserializedMetadata
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,3 +273,83 @@ function prepareEntryForIndexing(entry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
exports.prepareEntryForIndexing = prepareEntryForIndexing
|
exports.prepareEntryForIndexing = prepareEntryForIndexing
|
||||||
|
|
||||||
|
exports.getExportFilesPath = dictId => {
|
||||||
|
return `${DATA_FILES_PATH}/dict_export/${dictId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
const markupFilter = {
|
||||||
|
noMixed: new xss.FilterXSS({
|
||||||
|
whiteList: {},
|
||||||
|
stripIgnoreTag: true,
|
||||||
|
stripIgnoreTagBody: ['script', 'style']
|
||||||
|
}),
|
||||||
|
|
||||||
|
mixedBasic: new xss.FilterXSS({
|
||||||
|
whiteList: {
|
||||||
|
sup: [],
|
||||||
|
sub: []
|
||||||
|
},
|
||||||
|
stripIgnoreTag: true,
|
||||||
|
stripIgnoreTagBody: ['script', 'style']
|
||||||
|
}),
|
||||||
|
|
||||||
|
mixedExtended: new xss.FilterXSS({
|
||||||
|
whiteList: {
|
||||||
|
sup: [],
|
||||||
|
sub: [],
|
||||||
|
b: [],
|
||||||
|
i: [],
|
||||||
|
a: ['href']
|
||||||
|
},
|
||||||
|
stripIgnoreTag: true,
|
||||||
|
stripIgnoreTagBody: ['script', 'style'],
|
||||||
|
onTag: customTagHandler
|
||||||
|
}),
|
||||||
|
|
||||||
|
mixedOther: new xss.FilterXSS({
|
||||||
|
whiteList: {
|
||||||
|
sup: [],
|
||||||
|
sub: [],
|
||||||
|
b: [],
|
||||||
|
i: [],
|
||||||
|
a: ['href'],
|
||||||
|
br: []
|
||||||
|
},
|
||||||
|
stripIgnoreTag: true,
|
||||||
|
stripIgnoreTagBody: ['script', 'style'],
|
||||||
|
onTag: customTagHandler
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function customTagHandler(tag, html, { isWhite, isClosing }) {
|
||||||
|
// Special treatment only for whitelisted opening anchor tags.
|
||||||
|
if (tag !== 'a' || !isWhite || isClosing) return
|
||||||
|
|
||||||
|
const matchUrl = html.match(/href="?(?<url>https?:\/\/.*?)"?[\s>]/)
|
||||||
|
const url = matchUrl ? xss.escapeAttrValue(matchUrl.groups.url) : undefined
|
||||||
|
|
||||||
|
return `<a href="${url || ''}" target="_blank">`
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitize(string, filter) {
|
||||||
|
return filter.process(string).replace(/\s+/g, ' ').trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.sanitizeField = {
|
||||||
|
toText(string) {
|
||||||
|
return sanitize(string, markupFilter.noMixed)
|
||||||
|
},
|
||||||
|
|
||||||
|
toMixedBasic(string) {
|
||||||
|
return sanitize(string, markupFilter.mixedBasic)
|
||||||
|
},
|
||||||
|
|
||||||
|
toMixedExtended(string) {
|
||||||
|
return sanitize(string, markupFilter.mixedExtended)
|
||||||
|
},
|
||||||
|
|
||||||
|
toMixedOther(string) {
|
||||||
|
return sanitize(string, markupFilter.mixedOther)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
const { readdir, stat } = require('fs/promises')
|
const { readdir, stat } = require('fs/promises')
|
||||||
|
const path = require('path')
|
||||||
const { partial } = require('filesize')
|
const { partial } = require('filesize')
|
||||||
const { DATA_FILES_PATH } = require('../../config/settings')
|
const { DATA_FILES_PATH } = require('../../config/settings')
|
||||||
|
|
||||||
@@ -8,6 +9,7 @@ exports.deserialize = {
|
|||||||
extraction(extraction) {
|
extraction(extraction) {
|
||||||
const deserializedExtraction = {
|
const deserializedExtraction = {
|
||||||
id: extraction.id,
|
id: extraction.id,
|
||||||
|
userId: extraction.user_id,
|
||||||
name: extraction.name,
|
name: extraction.name,
|
||||||
status: extraction.status,
|
status: extraction.status,
|
||||||
corpusId: extraction.corpus_id,
|
corpusId: extraction.corpus_id,
|
||||||
@@ -43,17 +45,21 @@ exports.getFileNamesInFolder = async folderPath => {
|
|||||||
return filenames
|
return filenames
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.getFileStats = getFileStats
|
||||||
|
|
||||||
exports.getFileStatsInFolder = async folderPath => {
|
exports.getFileStatsInFolder = async folderPath => {
|
||||||
const filenames = await readdir(folderPath)
|
const filenames = await readdir(folderPath)
|
||||||
const fileStats = await Promise.all(
|
const filePaths = filenames.map(filename => `${folderPath}/${filename}`)
|
||||||
filenames.map(async filename => {
|
const fileStats = await Promise.all(filePaths.map(getFileStats))
|
||||||
const filePath = `${folderPath}/${filename}`
|
fileStats.sort((a, b) => a.timeModified - b.timeModified)
|
||||||
const { mtimeMs: timeModified, size } = await stat(filePath)
|
return fileStats
|
||||||
const sizeHumanReadable = formatFileSize(size)
|
}
|
||||||
const fileStats = { filename, size: sizeHumanReadable, timeModified }
|
|
||||||
return fileStats
|
async function getFileStats(filePath) {
|
||||||
})
|
const filename = path.basename(filePath)
|
||||||
)
|
const { mtimeMs: timeModified, size } = await stat(filePath)
|
||||||
|
const sizeHumanReadable = formatFileSize(size)
|
||||||
|
const fileStats = { filename, size: sizeHumanReadable, timeModified }
|
||||||
return fileStats
|
return fileStats
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ exports.aggregateSettings = settings => {
|
|||||||
exports.deserialize = {
|
exports.deserialize = {
|
||||||
settings(settings) {
|
settings(settings) {
|
||||||
const deserializedSettings = {
|
const deserializedSettings = {
|
||||||
name: settings.portal_name,
|
nameSl: settings.portal_name_sl,
|
||||||
description: settings.portal_description,
|
nameEn: settings.portal_name_en,
|
||||||
|
descriptionSl: settings.portal_description_sl,
|
||||||
|
descriptionEn: settings.portal_description_en,
|
||||||
code: settings.portal_code,
|
code: settings.portal_code,
|
||||||
isExtractionEnabled: settings.is_extraction_enabled,
|
isExtractionEnabled: settings.is_extraction_enabled,
|
||||||
isDictionariesEnabled: settings.is_dictionaries_enabled,
|
isDictionariesEnabled: settings.is_dictionaries_enabled,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ module.exports = function (filters, hitsPerPage, page) {
|
|||||||
filter: []
|
filter: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sort: ['_score', 'timeCreated']
|
sort: ['_score', { timeCreated: 'desc' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters.status) {
|
if (filters.status) {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ module.exports = function (searchString, filters, hitsPerPage, page) {
|
|||||||
filter: []
|
filter: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sort: ['_score', 'timeCreated']
|
sort: ['_score', { timeCreated: 'desc' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters.status) {
|
if (filters.status) {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ module.exports = function (searchString, filters, hitsPerPage, page) {
|
|||||||
filter: []
|
filter: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sort: ['_score', 'timeCreated']
|
sort: ['_score', { timeCreated: 'desc' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters.status) {
|
if (filters.status) {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ module.exports = function (searchString, filters, hitsPerPage, page) {
|
|||||||
filter: []
|
filter: []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
sort: ['_score', 'timeCreated']
|
sort: ['_score', { timeCreated: 'desc' }]
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filters.status) {
|
if (filters.status) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ const { EDITOR_MAX_HITS } = require('../../../../../config/settings')
|
|||||||
|
|
||||||
module.exports = function (dictionaryId, filters, searchFieldFilters) {
|
module.exports = function (dictionaryId, filters, searchFieldFilters) {
|
||||||
const queryDsl = {
|
const queryDsl = {
|
||||||
_source: ['id', 'isValid', 'isPublished', 'term'],
|
_source: ['id', 'isValid', 'isPublished', 'term', 'homonymSort'],
|
||||||
fields: ['foreignEntries.terms'],
|
fields: ['foreignEntries.terms'],
|
||||||
script_fields: {
|
script_fields: {
|
||||||
commentActivityIndicator: {
|
commentActivityIndicator: {
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ exports.prepareEntries = hits => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Transform search engine's aggregation raw output into correct and friendly format.
|
// Transform search engine's aggregation raw output into correct and friendly format.
|
||||||
exports.prepareAggregation = async aggregationRaw => {
|
exports.prepareAggregation = async (aggregationRaw, determinedLanguage) => {
|
||||||
const { aggregations, hits } = aggregationRaw.body
|
const { aggregations, hits } = aggregationRaw.body
|
||||||
|
|
||||||
const hitsCount = hits.total.value
|
const hitsCount = hits.total.value
|
||||||
@@ -107,7 +107,8 @@ exports.prepareAggregation = async aggregationRaw => {
|
|||||||
const names = await Portal.getSearchAggregateNames(
|
const names = await Portal.getSearchAggregateNames(
|
||||||
primaryDomainIds,
|
primaryDomainIds,
|
||||||
dictionaryIds,
|
dictionaryIds,
|
||||||
languageIds
|
languageIds,
|
||||||
|
determinedLanguage
|
||||||
)
|
)
|
||||||
|
|
||||||
const aggregation = {
|
const aggregation = {
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ exports.deserialize = {
|
|||||||
firstName: user.first_name,
|
firstName: user.first_name,
|
||||||
lastName: user.last_name,
|
lastName: user.last_name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
status: user.status,
|
||||||
hitsPerPage: user.hits_per_page,
|
hitsPerPage: user.hits_per_page,
|
||||||
|
language: user.language,
|
||||||
userRoles: user.user_roles,
|
userRoles: user.user_roles,
|
||||||
assignedConsultancyEntries: user.assigned_consultancy_entries
|
assignedConsultancyEntries: user.assigned_consultancy_entries
|
||||||
}
|
}
|
||||||
@@ -21,7 +23,8 @@ exports.deserialize = {
|
|||||||
firstName: userData.first_name,
|
firstName: userData.first_name,
|
||||||
lastName: userData.last_name,
|
lastName: userData.last_name,
|
||||||
email: userData.email,
|
email: userData.email,
|
||||||
password: userData.password
|
status: userData.status,
|
||||||
|
language: userData.language
|
||||||
}
|
}
|
||||||
|
|
||||||
return deserializedData
|
return deserializedData
|
||||||
|
|||||||
@@ -73,6 +73,9 @@ class InterInstanceSync {
|
|||||||
'SELECT l.code AS language_code, ef.entry_id, ef.term, ef.definition, ef.synonym FROM entry_foreign AS ef' +
|
'SELECT l.code AS language_code, ef.entry_id, ef.term, ef.definition, ef.synonym FROM entry_foreign AS ef' +
|
||||||
' INNER JOIN language AS l ON l.id = ef.language_id' +
|
' INNER JOIN language AS l ON l.id = ef.language_id' +
|
||||||
' WHERE entry_id = ANY($1::int[]) ORDER BY ef.entry_id, l.code'
|
' WHERE entry_id = ANY($1::int[]) ORDER BY ef.entry_id, l.code'
|
||||||
|
const sqlLinks =
|
||||||
|
'SELECT entry_id, link, type FROM entry_link WHERE entry_id = ANY($1::int[]) ORDER BY entry_id, type, link'
|
||||||
|
|
||||||
const { rows: entryList } = await db.query(sqlEntries, [
|
const { rows: entryList } = await db.query(sqlEntries, [
|
||||||
dictionaryId,
|
dictionaryId,
|
||||||
since
|
since
|
||||||
@@ -83,6 +86,7 @@ class InterInstanceSync {
|
|||||||
const { rows: translationList } = await db.query(sqlTranslations, [
|
const { rows: translationList } = await db.query(sqlTranslations, [
|
||||||
entryIds
|
entryIds
|
||||||
])
|
])
|
||||||
|
const { rows: linkList } = await db.query(sqlLinks, [entryIds])
|
||||||
let currentEntryId = 0
|
let currentEntryId = 0
|
||||||
let lastEntryId = 0
|
let lastEntryId = 0
|
||||||
translationList.forEach(t => {
|
translationList.forEach(t => {
|
||||||
@@ -102,6 +106,23 @@ class InterInstanceSync {
|
|||||||
}
|
}
|
||||||
entry.translations.push(t)
|
entry.translations.push(t)
|
||||||
})
|
})
|
||||||
|
linkList.forEach(l => {
|
||||||
|
currentEntryId = l.entry_id
|
||||||
|
const entry = entryList.find(e => {
|
||||||
|
return e.id === currentEntryId
|
||||||
|
})
|
||||||
|
if (!entry) return
|
||||||
|
if (lastEntryId === 0) {
|
||||||
|
entry.links = []
|
||||||
|
lastEntryId = l.entry_id
|
||||||
|
} else if (currentEntryId !== lastEntryId) {
|
||||||
|
// new entry translations : save previous
|
||||||
|
entry.links = []
|
||||||
|
lastEntryId = currentEntryId
|
||||||
|
currentEntryId = l.entry_id
|
||||||
|
}
|
||||||
|
entry.links.push({ type: l.type, link: l.link })
|
||||||
|
})
|
||||||
return entryList
|
return entryList
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ Portal.fetchInstanceSettings = async () => {
|
|||||||
WHERE
|
WHERE
|
||||||
name
|
name
|
||||||
IN (
|
IN (
|
||||||
'portal_name',
|
'portal_name_sl',
|
||||||
|
'portal_name_en',
|
||||||
'portal_code',
|
'portal_code',
|
||||||
'portal_description',
|
'portal_description_sl',
|
||||||
|
'portal_description_en',
|
||||||
'is_consultancy_enabled',
|
'is_consultancy_enabled',
|
||||||
'is_dictionaries_enabled',
|
'is_dictionaries_enabled',
|
||||||
'is_extraction_enabled')`
|
'is_extraction_enabled')`
|
||||||
@@ -32,9 +34,11 @@ Portal.updateInstaceSettings = async payload => {
|
|||||||
const isConsultancyEnabled = payload.isConsultancyEnabled ? 'T' : 'F'
|
const isConsultancyEnabled = payload.isConsultancyEnabled ? 'T' : 'F'
|
||||||
|
|
||||||
const values = [
|
const values = [
|
||||||
payload.portalName,
|
payload.portalNameSl,
|
||||||
|
payload.portalNameEn,
|
||||||
payload.portalCode,
|
payload.portalCode,
|
||||||
payload.portalDescription,
|
payload.portalDescriptionSl,
|
||||||
|
payload.portalDescriptionEn,
|
||||||
isExtractionEnabled,
|
isExtractionEnabled,
|
||||||
isDictionariesEnabled,
|
isDictionariesEnabled,
|
||||||
isConsultancyEnabled
|
isConsultancyEnabled
|
||||||
@@ -47,17 +51,21 @@ Portal.updateInstaceSettings = async payload => {
|
|||||||
value
|
value
|
||||||
= CASE name
|
= CASE name
|
||||||
WHEN
|
WHEN
|
||||||
'portal_name' THEN $1
|
'portal_name_sl' THEN $1
|
||||||
WHEN
|
WHEN
|
||||||
'portal_code' THEN $2
|
'portal_name_en' THEN $2
|
||||||
WHEN
|
WHEN
|
||||||
'portal_description' THEN $3
|
'portal_code' THEN $3
|
||||||
WHEN
|
WHEN
|
||||||
'is_extraction_enabled' THEN $4
|
'portal_description_sl' THEN $4
|
||||||
WHEN
|
WHEN
|
||||||
'is_dictionaries_enabled' THEN $5
|
'portal_description_en' THEN $5
|
||||||
WHEN
|
WHEN
|
||||||
'is_consultancy_enabled' THEN $6
|
'is_extraction_enabled' THEN $6
|
||||||
|
WHEN
|
||||||
|
'is_dictionaries_enabled' THEN $7
|
||||||
|
WHEN
|
||||||
|
'is_consultancy_enabled' THEN $8
|
||||||
ELSE value
|
ELSE value
|
||||||
END`
|
END`
|
||||||
|
|
||||||
@@ -447,27 +455,28 @@ Portal.getSlovenianLanguageId = async () => {
|
|||||||
Portal.getSearchAggregateNames = async (
|
Portal.getSearchAggregateNames = async (
|
||||||
primaryDomainIds,
|
primaryDomainIds,
|
||||||
dictionaryIds,
|
dictionaryIds,
|
||||||
languageIds
|
languageIds,
|
||||||
|
determinedLanguage
|
||||||
) => {
|
) => {
|
||||||
const text = `
|
const text = `
|
||||||
SELECT jsonb_build_object(
|
SELECT jsonb_build_object(
|
||||||
'primaryDomains', jsonb_object(
|
'primaryDomains', jsonb_object(
|
||||||
ARRAY(
|
ARRAY(
|
||||||
SELECT ARRAY [id, name_sl]::TEXT[]
|
SELECT ARRAY [id, name_${determinedLanguage}]::TEXT[]
|
||||||
FROM domain_primary
|
FROM domain_primary
|
||||||
WHERE id = ANY ($1)
|
WHERE id = ANY ($1)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
'dictionaries', jsonb_object(
|
'dictionaries', jsonb_object(
|
||||||
ARRAY(
|
ARRAY(
|
||||||
SELECT ARRAY [id, name_sl]::TEXT[]
|
SELECT ARRAY [id, name_${determinedLanguage}]::TEXT[]
|
||||||
FROM dictionary
|
FROM dictionary
|
||||||
WHERE id = ANY ($2)
|
WHERE id = ANY ($2)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
'languages', jsonb_object(
|
'languages', jsonb_object(
|
||||||
ARRAY(
|
ARRAY(
|
||||||
SELECT ARRAY [id, name_sl]::TEXT[]
|
SELECT ARRAY [id, name_${determinedLanguage}]::TEXT[]
|
||||||
FROM language
|
FROM language
|
||||||
WHERE id = ANY ($3)
|
WHERE id = ANY ($3)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ Eurotermbank.push = async () => {
|
|||||||
'definition', ef.definition,
|
'definition', ef.definition,
|
||||||
'synonyms', ef.synonym)
|
'synonyms', ef.synonym)
|
||||||
FROM entry_foreign ef
|
FROM entry_foreign ef
|
||||||
LEFT JOIN LANGUAGE l ON l.id = ef.language_id
|
LEFT JOIN language l ON l.id = ef.language_id
|
||||||
WHERE entry_id = e.id
|
WHERE entry_id = e.id
|
||||||
) foreign_entries
|
) foreign_entries
|
||||||
FROM entry e
|
FROM entry e
|
||||||
|
|||||||
@@ -1,13 +1,60 @@
|
|||||||
|
const { randomBytes } = require('crypto')
|
||||||
|
const { promisify } = require('util')
|
||||||
|
const RandomBytesAsync = promisify(randomBytes)
|
||||||
const db = require('./db')
|
const db = require('./db')
|
||||||
const bcrypt = require('bcrypt')
|
const bcrypt = require('bcrypt')
|
||||||
const uid = require('uid-safe')
|
const uid = require('uid-safe')
|
||||||
const { deserialize } = require('./helpers/user')
|
const { deserialize } = require('./helpers/user')
|
||||||
|
const {
|
||||||
|
ACTIVATION_TOKEN_VALID_DAYS,
|
||||||
|
CHANGE_EMAIL_TOKEN_VALID_DAYS
|
||||||
|
} = require('../config/settings')
|
||||||
|
|
||||||
|
const SALT_ROUNDS = 12
|
||||||
|
const PASSWORD_RESET_VALID_INTERVAL = '1 day'
|
||||||
|
|
||||||
const User = {}
|
const User = {}
|
||||||
|
|
||||||
|
// Check if a user with provided email already exists.
|
||||||
|
User.isEmailAlreadyTaken = async email => {
|
||||||
|
const { rows } = await db.query('SELECT 1 FROM "user" WHERE email = $1', [
|
||||||
|
email
|
||||||
|
])
|
||||||
|
|
||||||
|
const isTaken = rows.length > 0
|
||||||
|
|
||||||
|
return isTaken
|
||||||
|
}
|
||||||
|
|
||||||
// Create new user in DB.
|
// Create new user in DB.
|
||||||
User.create = async user => {
|
User.create = async (user, t) => {
|
||||||
const SALT_ROUNDS = 12
|
const {
|
||||||
|
rows: [userWithSameEmail]
|
||||||
|
} = await db.query('SELECT status FROM "user" WHERE email = $1', [user.email])
|
||||||
|
|
||||||
|
if (userWithSameEmail && userWithSameEmail.status !== 'registered') {
|
||||||
|
const err = Error(t('Elektronski naslov uporablja že drug uporabnik.'))
|
||||||
|
err.status = 400
|
||||||
|
err.displayInProd = true
|
||||||
|
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
rows: [isUsernameAlreadyTakenByOther]
|
||||||
|
} = await db.query(
|
||||||
|
'SELECT 1 FROM "user" WHERE username = $1 AND email <> $2',
|
||||||
|
[user.username, user.email]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isUsernameAlreadyTakenByOther) {
|
||||||
|
const err = Error(t('Izbrano uporabniško ime uporablja že drug uporabnik.'))
|
||||||
|
err.status = 400
|
||||||
|
err.displayInProd = true
|
||||||
|
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
const bcryptHash = await bcrypt.hash(user.password, SALT_ROUNDS)
|
const bcryptHash = await bcrypt.hash(user.password, SALT_ROUNDS)
|
||||||
|
|
||||||
const values = [
|
const values = [
|
||||||
@@ -17,16 +64,31 @@ User.create = async user => {
|
|||||||
user.email || null,
|
user.email || null,
|
||||||
bcryptHash || null
|
bcryptHash || null
|
||||||
]
|
]
|
||||||
|
if (user.language) values.push(user.language)
|
||||||
|
|
||||||
const text = `INSERT INTO "user" (
|
let text
|
||||||
|
|
||||||
|
if (userWithSameEmail) {
|
||||||
|
text = `UPDATE "user" SET
|
||||||
|
username = $1,
|
||||||
|
first_name = $2,
|
||||||
|
last_name = $3,
|
||||||
|
bcrypt_hash = $5
|
||||||
|
${user.language ? ', language = $6' : ''}
|
||||||
|
WHERE email = $4
|
||||||
|
RETURNING id`
|
||||||
|
} else {
|
||||||
|
text = `INSERT INTO "user" (
|
||||||
username,
|
username,
|
||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
email,
|
email,
|
||||||
bcrypt_hash
|
bcrypt_hash
|
||||||
|
${user.language ? ', language' : ''}
|
||||||
)
|
)
|
||||||
VALUES (${db.genParamStr(values)})
|
VALUES (${db.genParamStr(values)})
|
||||||
RETURNING id`
|
RETURNING id`
|
||||||
|
}
|
||||||
|
|
||||||
const { rows } = await db.query(text, values)
|
const { rows } = await db.query(text, values)
|
||||||
|
|
||||||
@@ -43,33 +105,42 @@ User.saveActivationToken = async (userId, activationToken) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch user from DB by (valid) activation token.
|
// Activate user account using the provided activation token.
|
||||||
User.fetchByActivationToken = async activationToken => {
|
User.activateAccountWithToken = async (token, t) => {
|
||||||
const TOKEN_VALID_PERIOD = '1 week'
|
let user
|
||||||
const text = `
|
|
||||||
SELECT u.id
|
|
||||||
FROM user_token_activation t
|
|
||||||
INNER JOIN "user" u ON u.id = t.user_id
|
|
||||||
WHERE
|
|
||||||
t.token = $1
|
|
||||||
AND AGE(NOW(), t.time_created) < INTERVAL '${TOKEN_VALID_PERIOD}'
|
|
||||||
`
|
|
||||||
const values = [activationToken]
|
|
||||||
|
|
||||||
const { rows } = await db.query(text, values)
|
await db.transaction(async dbClient => {
|
||||||
const user = rows[0]
|
const { rows } = await dbClient.query(
|
||||||
|
`SELECT user_id FROM user_token_activation WHERE token = $1 AND NOW() - time_created < '${ACTIVATION_TOKEN_VALID_DAYS} days'`,
|
||||||
|
[token]
|
||||||
|
)
|
||||||
|
|
||||||
// TODO Perhaps suggest to the user to request another one and make a shortcut.
|
if (rows.length === 0) {
|
||||||
if (!user) throw Error('Povezava je neveljavna ali pa je že potekla')
|
const err = Error(
|
||||||
|
t('Povezava ni (več) veljavna. Prosimo, da se ponovno registrirate.')
|
||||||
|
)
|
||||||
|
err.status = 403
|
||||||
|
err.displayInProd = true
|
||||||
|
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = rows[0].user_id
|
||||||
|
;({
|
||||||
|
rows: [user]
|
||||||
|
} = await dbClient.query(
|
||||||
|
`UPDATE "user" SET status = 'active', time_activated = NOW() WHERE id = $1 RETURNING id`,
|
||||||
|
[userId]
|
||||||
|
))
|
||||||
|
|
||||||
|
await dbClient.query('DELETE FROM user_token_activation WHERE token = $1', [
|
||||||
|
token
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
return user
|
return user
|
||||||
}
|
}
|
||||||
|
|
||||||
// Activate user account.
|
|
||||||
User.activateAccount = async user => {
|
|
||||||
await db.query(`UPDATE "user" SET status = 'active' WHERE id = $1`, [user.id])
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a user remember me token.
|
// Generate a user remember me token.
|
||||||
User.generateRememberMeToken = async () => {
|
User.generateRememberMeToken = async () => {
|
||||||
const token = await uid(32)
|
const token = await uid(32)
|
||||||
@@ -91,6 +162,130 @@ User.clearRememberMeToken = async rememberMeToken => {
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save a password reset token for a single user in DB.
|
||||||
|
User.saveResetPasswordToken = async (userId, resetPasswordToken) => {
|
||||||
|
await db.query(
|
||||||
|
'INSERT INTO user_token_reset_password (token, user_id) VALUES ($1, $2)',
|
||||||
|
[resetPasswordToken, userId]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check existance and validity of password reset token in DB.
|
||||||
|
User.isResetPasswordTokenValid = async token => {
|
||||||
|
const { rows } = await db.query(
|
||||||
|
`SELECT 1 exists FROM user_token_reset_password WHERE token = $1 AND NOW() - time_created < '${PASSWORD_RESET_VALID_INTERVAL}'`,
|
||||||
|
[token]
|
||||||
|
)
|
||||||
|
const isValid = rows.length > 0
|
||||||
|
|
||||||
|
return isValid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new password for user using the provided reset password token.
|
||||||
|
User.resetPasswordWithToken = async (token, password, t) => {
|
||||||
|
let user
|
||||||
|
|
||||||
|
await db.transaction(async dbClient => {
|
||||||
|
const { rows } = await dbClient.query(
|
||||||
|
`SELECT user_id FROM user_token_reset_password WHERE token = $1 AND NOW() - time_created < '${PASSWORD_RESET_VALID_INTERVAL}'`,
|
||||||
|
[token]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
const err = Error(
|
||||||
|
t(
|
||||||
|
'Povezava ni (več) veljavna. Prosimo, da ponovno zahtevate ponastavitev gesla.'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
err.status = 403
|
||||||
|
err.displayInProd = true
|
||||||
|
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
const bcryptHash = await bcrypt.hash(password, SALT_ROUNDS)
|
||||||
|
const userId = rows[0].user_id
|
||||||
|
;({
|
||||||
|
rows: [user]
|
||||||
|
} = await dbClient.query(
|
||||||
|
'UPDATE "user" SET bcrypt_hash = $1 WHERE id = $2 RETURNING id, username, email',
|
||||||
|
[bcryptHash, userId]
|
||||||
|
))
|
||||||
|
|
||||||
|
await dbClient.query(
|
||||||
|
'DELETE FROM user_token_reset_password WHERE token = $1',
|
||||||
|
[token]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save change email token for a single user in DB.
|
||||||
|
User.saveChangeEmailToken = async (userId, changeEmailToken, newEmail) => {
|
||||||
|
await db.query(
|
||||||
|
'INSERT INTO user_token_change_email (token, user_id, new_email) VALUES ($1, $2, $3)',
|
||||||
|
[changeEmailToken, userId, newEmail]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new email for user using the provided change email token.
|
||||||
|
User.changeEmailWithToken = async function (token, t) {
|
||||||
|
let user
|
||||||
|
|
||||||
|
await db.transaction(async dbClient => {
|
||||||
|
const { rows } = await dbClient.query(
|
||||||
|
`SELECT user_id, new_email FROM user_token_change_email WHERE token = $1 AND NOW() - time_created < '${CHANGE_EMAIL_TOKEN_VALID_DAYS} days'`,
|
||||||
|
[token]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (rows.length === 0) {
|
||||||
|
const err = Error(
|
||||||
|
t('Povezava ni (več) veljavna. Elektronski naslov ni bil spremenjen.')
|
||||||
|
)
|
||||||
|
err.status = 403
|
||||||
|
err.displayInProd = true
|
||||||
|
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
const { user_id: userId, new_email: newEmail } = rows[0]
|
||||||
|
if (await this.isEmailAlreadyTaken(newEmail)) {
|
||||||
|
const err = Error(t('Elektronski naslov uporablja že drug uporabnik.'))
|
||||||
|
err.status = 403
|
||||||
|
err.displayInProd = true
|
||||||
|
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
;({
|
||||||
|
rows: [user]
|
||||||
|
} = await dbClient.query(
|
||||||
|
'UPDATE "user" SET email = $1 WHERE id = $2 RETURNING id, username, email',
|
||||||
|
[newEmail, userId]
|
||||||
|
))
|
||||||
|
|
||||||
|
await dbClient.query(
|
||||||
|
'DELETE FROM user_token_change_email WHERE token = $1',
|
||||||
|
[token]
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch user from DB by username or email.
|
||||||
|
User.fetchByUsernameOrEmail = async usernameOrEmail => {
|
||||||
|
const {
|
||||||
|
rows: [user]
|
||||||
|
} = await db.query(
|
||||||
|
'SELECT id, username, email, status, bcrypt_hash FROM "user" WHERE username = $1 OR email = $1',
|
||||||
|
[usernameOrEmail]
|
||||||
|
)
|
||||||
|
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch user data that should be available on every request from DB by id.
|
// Fetch user data that should be available on every request from DB by id.
|
||||||
User.fetchDeserializedDataById = async userId => {
|
User.fetchDeserializedDataById = async userId => {
|
||||||
const text = `
|
const text = `
|
||||||
@@ -100,7 +295,9 @@ User.fetchDeserializedDataById = async userId => {
|
|||||||
u.first_name,
|
u.first_name,
|
||||||
u.last_name,
|
u.last_name,
|
||||||
u.email,
|
u.email,
|
||||||
|
u.status,
|
||||||
u.hits_per_page,
|
u.hits_per_page,
|
||||||
|
u.language,
|
||||||
ARRAY(
|
ARRAY(
|
||||||
SELECT jsonb_build_object(
|
SELECT jsonb_build_object(
|
||||||
'roleName', r.role_name,
|
'roleName', r.role_name,
|
||||||
@@ -142,6 +339,7 @@ User.fetchAll = async (resultsPerPage, page) => {
|
|||||||
'pages_total', (
|
'pages_total', (
|
||||||
SELECT CEIL(COUNT(*) / $1::float)
|
SELECT CEIL(COUNT(*) / $1::float)
|
||||||
FROM "user"
|
FROM "user"
|
||||||
|
WHERE status <> 'closed'
|
||||||
),
|
),
|
||||||
'results', ARRAY(
|
'results', ARRAY(
|
||||||
SELECT jsonb_build_object(
|
SELECT jsonb_build_object(
|
||||||
@@ -151,6 +349,7 @@ User.fetchAll = async (resultsPerPage, page) => {
|
|||||||
'status', status
|
'status', status
|
||||||
)
|
)
|
||||||
FROM "user"
|
FROM "user"
|
||||||
|
WHERE status <> 'closed'
|
||||||
ORDER BY username
|
ORDER BY username
|
||||||
LIMIT $1
|
LIMIT $1
|
||||||
OFFSET $2
|
OFFSET $2
|
||||||
@@ -258,9 +457,9 @@ User.updatePortalRoles = async rolesPerUser => {
|
|||||||
|
|
||||||
User.fetchUser = async userId => {
|
User.fetchUser = async userId => {
|
||||||
const text = `
|
const text = `
|
||||||
SELECT id, username, first_name, last_name, email
|
SELECT id, username, first_name, last_name, email, status, language
|
||||||
FROM "user"
|
FROM "user"
|
||||||
WHERE id=$1`
|
WHERE id = $1`
|
||||||
const value = [userId]
|
const value = [userId]
|
||||||
|
|
||||||
const { rows } = await db.query(text, value)
|
const { rows } = await db.query(text, value)
|
||||||
@@ -271,17 +470,38 @@ User.fetchUser = async userId => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
User.updateUser = async (userId, payload) => {
|
User.updateUser = async (userId, payload) => {
|
||||||
const text = `
|
const previousStatusText = 'SELECT status FROM "user" WHERE id = $1'
|
||||||
|
const { rows } = await db.query(previousStatusText, [userId])
|
||||||
|
const previousStatus = rows[0].status
|
||||||
|
|
||||||
|
if (previousStatus === 'closed') throw Error()
|
||||||
|
|
||||||
|
let statusValue
|
||||||
|
let setTimeActivated = false
|
||||||
|
if (previousStatus === 'registered') {
|
||||||
|
setTimeActivated = !!payload.status
|
||||||
|
statusValue = !payload.status ? 'registered' : 'active'
|
||||||
|
} else statusValue = !payload.status ? 'inactive' : 'active'
|
||||||
|
|
||||||
|
const updateText = `
|
||||||
UPDATE "user"
|
UPDATE "user"
|
||||||
SET
|
SET
|
||||||
username = $2,
|
username = $2,
|
||||||
first_name = $3,
|
first_name = $3,
|
||||||
last_name = $4
|
last_name = $4,
|
||||||
|
status = $5
|
||||||
|
${setTimeActivated ? ', time_activated = NOW()' : ''}
|
||||||
WHERE id = $1`
|
WHERE id = $1`
|
||||||
|
|
||||||
const values = [userId, payload.username, payload.firstName, payload.lastName]
|
const values = [
|
||||||
|
userId,
|
||||||
|
payload.username,
|
||||||
|
payload.firstName,
|
||||||
|
payload.lastName,
|
||||||
|
statusValue
|
||||||
|
]
|
||||||
|
|
||||||
await db.query(text, values)
|
await db.query(updateText, values)
|
||||||
}
|
}
|
||||||
|
|
||||||
User.fetchUserRoles = async userId => {
|
User.fetchUserRoles = async userId => {
|
||||||
@@ -426,12 +646,9 @@ User.insertNewConsultantWithDomain = async (userId, domains) => {
|
|||||||
|
|
||||||
// Insert new consultant role with domain of
|
// Insert new consultant role with domain of
|
||||||
User.insertNewConsultantWithDomainByUsername = async (username, domains) => {
|
User.insertNewConsultantWithDomainByUsername = async (username, domains) => {
|
||||||
const { rows } = await db.query(
|
const user = await User.fetchByUsernameOrEmail(username)
|
||||||
'SELECT id FROM "user" WHERE username = $1 or email = $1',
|
|
||||||
[username]
|
|
||||||
)
|
|
||||||
|
|
||||||
await User.insertNewConsultantWithDomain(rows[0].id, domains)
|
await User.insertNewConsultantWithDomain(user.id, domains)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove consultant role
|
// Remove consultant role
|
||||||
@@ -449,13 +666,15 @@ User.fetchAllowedHitsPerPage = async () => {
|
|||||||
).rows.map(e => e.unnest)
|
).rows.map(e => e.unnest)
|
||||||
}
|
}
|
||||||
|
|
||||||
User.updateFirstNameAndLastName = async (username, firstName, lastName) => {
|
User.updateFirstNameAndLastName = async (userId, firstName, LastName) => {
|
||||||
return await db.query(
|
const {
|
||||||
`UPDATE "user"
|
rows: [{ email }]
|
||||||
SET first_name=$2, last_name=$3
|
} = await db.query(
|
||||||
WHERE username=$1;`,
|
'UPDATE "user" SET first_name = $1, last_name = $2 WHERE id = $3 RETURNING email',
|
||||||
[username, firstName, lastName]
|
[firstName, LastName, userId]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return email
|
||||||
}
|
}
|
||||||
|
|
||||||
User.updateHitsPerPage = async (username, hitsPerPageAmount) => {
|
User.updateHitsPerPage = async (username, hitsPerPageAmount) => {
|
||||||
@@ -467,4 +686,91 @@ User.updateHitsPerPage = async (username, hitsPerPageAmount) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update user's language.
|
||||||
|
User.updateLanguage = async (userId, languageCode) => {
|
||||||
|
await db.query('UPDATE "user" SET language = $1 WHERE id = $2', [
|
||||||
|
languageCode,
|
||||||
|
userId
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change user's password.
|
||||||
|
User.changePassword = async (userId, passwordOld, passwordNew, t) => {
|
||||||
|
await db.transaction(async dbClient => {
|
||||||
|
const {
|
||||||
|
rows: [{ bcrypt_hash: bcryptHashOld }]
|
||||||
|
} = await dbClient.query('SELECT bcrypt_hash FROM "user" WHERE id = $1', [
|
||||||
|
userId
|
||||||
|
])
|
||||||
|
|
||||||
|
const isOldPasswordCorrect = await bcrypt.compare(
|
||||||
|
passwordOld,
|
||||||
|
bcryptHashOld
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!isOldPasswordCorrect) {
|
||||||
|
const err = Error(t('Nepravilno staro geslo.'))
|
||||||
|
err.status = 403
|
||||||
|
err.displayInProd = true
|
||||||
|
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
const bcryptHashNew = await bcrypt.hash(passwordNew, SALT_ROUNDS)
|
||||||
|
|
||||||
|
await dbClient.query('UPDATE "user" SET bcrypt_hash = $1 WHERE id = $2', [
|
||||||
|
bcryptHashNew,
|
||||||
|
userId
|
||||||
|
])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close user's account and anonymize any personal data.
|
||||||
|
User.closeAccount = async userId => {
|
||||||
|
const maskString = '#####'
|
||||||
|
const randomString = (await RandomBytesAsync(10)).toString('hex')
|
||||||
|
|
||||||
|
const anonymizedUsername = randomString
|
||||||
|
const anonymizedFirstName = maskString
|
||||||
|
const anonymizedLastName = maskString
|
||||||
|
const anonymizedEmail = randomString
|
||||||
|
|
||||||
|
await db.transaction(async dbClient => {
|
||||||
|
await Promise.all([
|
||||||
|
dbClient.query(
|
||||||
|
`
|
||||||
|
UPDATE "user"
|
||||||
|
SET
|
||||||
|
username = $1,
|
||||||
|
first_name = $2,
|
||||||
|
last_name = $3,
|
||||||
|
email = $4,
|
||||||
|
status = 'closed',
|
||||||
|
time_closed = NOW()
|
||||||
|
WHERE id = $5`,
|
||||||
|
[
|
||||||
|
anonymizedUsername,
|
||||||
|
anonymizedFirstName,
|
||||||
|
anonymizedLastName,
|
||||||
|
anonymizedEmail,
|
||||||
|
userId
|
||||||
|
]
|
||||||
|
),
|
||||||
|
dbClient.query('DELETE FROM user_token_activation WHERE user_id = $1', [
|
||||||
|
userId
|
||||||
|
]),
|
||||||
|
dbClient.query('DELETE FROM user_token_remember_me WHERE user_id = $1', [
|
||||||
|
userId
|
||||||
|
]),
|
||||||
|
dbClient.query(
|
||||||
|
'DELETE FROM user_token_reset_password WHERE user_id = $1',
|
||||||
|
[userId]
|
||||||
|
),
|
||||||
|
dbClient.query('DELETE FROM user_token_change_email WHERE user_id = $1', [
|
||||||
|
userId
|
||||||
|
])
|
||||||
|
])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = User
|
module.exports = User
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"async": "^3.2.3",
|
"async": "^3.2.3",
|
||||||
"axios": "^0.26.1",
|
"axios": "^0.26.1",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
|
"connect-flash-plus": "^0.2.1",
|
||||||
"connect-redis": "^6.0.0",
|
"connect-redis": "^6.0.0",
|
||||||
"cookie-parser": "^1.4.5",
|
"cookie-parser": "^1.4.5",
|
||||||
"debug": "^4.3.2",
|
"debug": "^4.3.2",
|
||||||
@@ -22,6 +23,9 @@
|
|||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"helmet": "^5.0.2",
|
"helmet": "^5.0.2",
|
||||||
"http-errors": "^2.0.0",
|
"http-errors": "^2.0.0",
|
||||||
|
"i18next": "^22.4.10",
|
||||||
|
"i18next-fs-backend": "^2.1.1",
|
||||||
|
"i18next-http-middleware": "^3.2.2",
|
||||||
"ioredis": "^4.27.8",
|
"ioredis": "^4.27.8",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^1.4.3",
|
"multer": "^1.4.3",
|
||||||
@@ -64,6 +68,17 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/runtime": {
|
||||||
|
"version": "7.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz",
|
||||||
|
"integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
|
||||||
|
"dependencies": {
|
||||||
|
"regenerator-runtime": "^0.13.11"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/types": {
|
"node_modules/@babel/types": {
|
||||||
"version": "7.17.0",
|
"version": "7.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
|
||||||
@@ -780,6 +795,14 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/connect-flash-plus": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/connect-flash-plus/-/connect-flash-plus-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-MqnJms7FpZFFlLMaooLviOWwc04chmcPKaqwStjFIYd8MthE99f71yTzUcoyx1XIx87VsJuxzraL/ScbfrUUfQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/connect-redis": {
|
"node_modules/connect-redis": {
|
||||||
"version": "6.1.3",
|
"version": "6.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-6.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-6.1.3.tgz",
|
||||||
@@ -1678,6 +1701,38 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/i18next": {
|
||||||
|
"version": "22.4.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.10.tgz",
|
||||||
|
"integrity": "sha512-3EqgGK6fAJRjnGgfkNSStl4mYLCjUoJID338yVyLMj5APT67HUtWoqSayZewiiC5elzMUB1VEUwcmSCoeQcNEA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://locize.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://locize.com/i18next.html"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.20.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/i18next-fs-backend": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-FTnj+UmNgT3YRml5ruRv0jMZDG7odOL/OP5PF5mOqvXud2vHrPOOs68Zdk6iqzL47cnnM0ZVkK2BAvpFeDJToA=="
|
||||||
|
},
|
||||||
|
"node_modules/i18next-http-middleware": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/i18next-http-middleware/-/i18next-http-middleware-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-OW2sWnbns+PuLi77T+/ni4Mi+TNJ6Q6XNGdZicMv9FD+QfZrFrynBVqryHB/bfflszx2Zswg/kxtCildVLdhSA=="
|
||||||
|
},
|
||||||
"node_modules/iconv-lite": {
|
"node_modules/iconv-lite": {
|
||||||
"version": "0.4.24",
|
"version": "0.4.24",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
@@ -3039,6 +3094,11 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/regenerator-runtime": {
|
||||||
|
"version": "0.13.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||||
|
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||||
|
},
|
||||||
"node_modules/registry-auth-token": {
|
"node_modules/registry-auth-token": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz",
|
||||||
@@ -3789,6 +3849,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.8.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.17.8.tgz",
|
||||||
"integrity": "sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ=="
|
"integrity": "sha512-BoHhDJrJXqcg+ZL16Xv39H9n+AqJ4pcDrQBGZN+wHxIysrLZ3/ECwCBUch/1zUNhnsXULcONU3Ei5Hmkfk6kiQ=="
|
||||||
},
|
},
|
||||||
|
"@babel/runtime": {
|
||||||
|
"version": "7.21.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.0.tgz",
|
||||||
|
"integrity": "sha512-xwII0//EObnq89Ji5AKYQaRYiW/nZ3llSv29d49IuxPhKbtJoLP+9QUUZ4nVragQVtaVGeZrpB+ZtG/Pdy/POw==",
|
||||||
|
"requires": {
|
||||||
|
"regenerator-runtime": "^0.13.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@babel/types": {
|
"@babel/types": {
|
||||||
"version": "7.17.0",
|
"version": "7.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
|
||||||
@@ -4359,6 +4427,11 @@
|
|||||||
"xdg-basedir": "^4.0.0"
|
"xdg-basedir": "^4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"connect-flash-plus": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/connect-flash-plus/-/connect-flash-plus-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-MqnJms7FpZFFlLMaooLviOWwc04chmcPKaqwStjFIYd8MthE99f71yTzUcoyx1XIx87VsJuxzraL/ScbfrUUfQ=="
|
||||||
|
},
|
||||||
"connect-redis": {
|
"connect-redis": {
|
||||||
"version": "6.1.3",
|
"version": "6.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-6.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-6.1.3.tgz",
|
||||||
@@ -5032,6 +5105,24 @@
|
|||||||
"debug": "4"
|
"debug": "4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"i18next": {
|
||||||
|
"version": "22.4.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/i18next/-/i18next-22.4.10.tgz",
|
||||||
|
"integrity": "sha512-3EqgGK6fAJRjnGgfkNSStl4mYLCjUoJID338yVyLMj5APT67HUtWoqSayZewiiC5elzMUB1VEUwcmSCoeQcNEA==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.20.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"i18next-fs-backend": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/i18next-fs-backend/-/i18next-fs-backend-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-FTnj+UmNgT3YRml5ruRv0jMZDG7odOL/OP5PF5mOqvXud2vHrPOOs68Zdk6iqzL47cnnM0ZVkK2BAvpFeDJToA=="
|
||||||
|
},
|
||||||
|
"i18next-http-middleware": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/i18next-http-middleware/-/i18next-http-middleware-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-OW2sWnbns+PuLi77T+/ni4Mi+TNJ6Q6XNGdZicMv9FD+QfZrFrynBVqryHB/bfflszx2Zswg/kxtCildVLdhSA=="
|
||||||
|
},
|
||||||
"iconv-lite": {
|
"iconv-lite": {
|
||||||
"version": "0.4.24",
|
"version": "0.4.24",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||||
@@ -6100,6 +6191,11 @@
|
|||||||
"redis-errors": "^1.0.0"
|
"redis-errors": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"regenerator-runtime": {
|
||||||
|
"version": "0.13.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
|
||||||
|
"integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
|
||||||
|
},
|
||||||
"registry-auth-token": {
|
"registry-auth-token": {
|
||||||
"version": "4.2.1",
|
"version": "4.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.1.tgz",
|
||||||
|
|||||||
@@ -4,14 +4,15 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./bin/www",
|
"start": "node ./bin/www",
|
||||||
"devstart": "nodemon --ignore data_files/ --inspect=0.0.0.0:9229 ./bin/www",
|
"devstart": "nodemon --ignore data_files/ --ignore public/locales/dev --inspect=0.0.0.0:9229 ./bin/www",
|
||||||
"devstart-wait": "nodemon --ignore data_files/ --inspect-brk=0.0.0.0:9229 ./bin/www"
|
"devstart-wait": "nodemon --ignore data_files/ --ignore public/locales/dev --inspect-brk=0.0.0.0:9229 ./bin/www"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opensearch-project/opensearch": "^2.1.0",
|
"@opensearch-project/opensearch": "^2.1.0",
|
||||||
"async": "^3.2.3",
|
"async": "^3.2.3",
|
||||||
"axios": "^0.26.1",
|
"axios": "^0.26.1",
|
||||||
"bcrypt": "^5.0.1",
|
"bcrypt": "^5.0.1",
|
||||||
|
"connect-flash-plus": "^0.2.1",
|
||||||
"connect-redis": "^6.0.0",
|
"connect-redis": "^6.0.0",
|
||||||
"cookie-parser": "^1.4.5",
|
"cookie-parser": "^1.4.5",
|
||||||
"debug": "^4.3.2",
|
"debug": "^4.3.2",
|
||||||
@@ -22,6 +23,9 @@
|
|||||||
"form-data": "^4.0.0",
|
"form-data": "^4.0.0",
|
||||||
"helmet": "^5.0.2",
|
"helmet": "^5.0.2",
|
||||||
"http-errors": "^2.0.0",
|
"http-errors": "^2.0.0",
|
||||||
|
"i18next": "^22.4.10",
|
||||||
|
"i18next-fs-backend": "^2.1.1",
|
||||||
|
"i18next-http-middleware": "^3.2.2",
|
||||||
"ioredis": "^4.27.8",
|
"ioredis": "^4.27.8",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"multer": "^1.4.3",
|
"multer": "^1.4.3",
|
||||||
|
|||||||
@@ -0,0 +1,285 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
|
||||||
|
<xs:element name="dictionary">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Dictionary (root element)</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="entry" maxOccurs="unbounded">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Entry</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="term" type="mixedBasic">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Slovenian Term</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="hwGrp" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Headword Group</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:attribute name="wfs" type="headwordStatus" default="no content">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Wordforms</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="acc" type="headwordStatus" default="no content">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Accent</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
<xs:attribute name="pron" type="headwordStatus" default="no content">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Pronunciation</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:attribute>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="domainLabels" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Domain Labels</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="domainLabel" type="xs:token" maxOccurs="unbounded">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Domain Label</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="label" type="mixedExtended" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Label</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="def" type="mixedExtended" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Definition</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="syns" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Synonyms</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="syn" type="mixedBasic" maxOccurs="unbounded">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Synonym</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="links" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Links</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="link" maxOccurs="unbounded">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Link</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexType mixed="true">
|
||||||
|
<xs:complexContent mixed="true">
|
||||||
|
<xs:extension base="mixedBasic">
|
||||||
|
<xs:attribute name="type" type="linkType" use="required"/>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="other" type="mixedOther" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Other</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="fLangs" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Foreign Languages</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="fLang" maxOccurs="unbounded">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Foreign Language</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="fTerms">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Foreign Terms</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="fTerm" type="mixedBasic" maxOccurs="unbounded">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Foreign Term</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="fDef" type="mixedExtended" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Foreign Definition</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="fSyns" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Foreign Synonims</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:sequence>
|
||||||
|
<xs:element name="fSyn" type="mixedBasic" maxOccurs="unbounded">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Foreign Synonim</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
<xs:attribute name="lang" type="xs:language" use="required"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="mm" minOccurs="0">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Multimedia</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexType>
|
||||||
|
<xs:choice maxOccurs="unbounded">
|
||||||
|
<xs:element name="image" type="xs:anyURI">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Image</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="audio" type="xs:anyURI">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Audio</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="video" type="xs:anyURI">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Video</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
</xs:choice>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
</xs:sequence>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="sup" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Superscript</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="sub" type="xs:string">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Subscript</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="b">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Bold</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexType mixed="true">
|
||||||
|
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xs:element ref="sup"/>
|
||||||
|
<xs:element ref="sub"/>
|
||||||
|
<xs:element ref="i"/>
|
||||||
|
<xs:element ref="a"/>
|
||||||
|
</xs:choice>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="i">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Italic</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexType mixed="true">
|
||||||
|
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xs:element ref="sup"/>
|
||||||
|
<xs:element ref="sub"/>
|
||||||
|
<xs:element ref="b"/>
|
||||||
|
<xs:element ref="a"/>
|
||||||
|
</xs:choice>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="a">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>Link</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexType mixed="true">
|
||||||
|
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xs:element ref="sup"/>
|
||||||
|
<xs:element ref="sub"/>
|
||||||
|
<xs:element ref="b"/>
|
||||||
|
<xs:element ref="i"/>
|
||||||
|
</xs:choice>
|
||||||
|
<xs:attribute name="href" type="xs:anyURI" use="required"/>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:element>
|
||||||
|
<xs:element name="br">
|
||||||
|
<xs:annotation>
|
||||||
|
<xs:documentation>New Line</xs:documentation>
|
||||||
|
</xs:annotation>
|
||||||
|
<xs:complexType/>
|
||||||
|
</xs:element>
|
||||||
|
<xs:simpleType name="headwordStatus">
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:enumeration value="no content"/>
|
||||||
|
<xs:enumeration value="unverified"/>
|
||||||
|
<xs:enumeration value="unconfirmed"/>
|
||||||
|
<xs:enumeration value="confirmed"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:simpleType name="linkType">
|
||||||
|
<xs:restriction base="xs:string">
|
||||||
|
<xs:enumeration value="related"/>
|
||||||
|
<xs:enumeration value="broader"/>
|
||||||
|
<xs:enumeration value="narrow"/>
|
||||||
|
</xs:restriction>
|
||||||
|
</xs:simpleType>
|
||||||
|
<xs:complexType name="mixedBasic" mixed="true">
|
||||||
|
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xs:element ref="sup"/>
|
||||||
|
<xs:element ref="sub"/>
|
||||||
|
</xs:choice>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="mixedExtended" mixed="true">
|
||||||
|
<xs:complexContent mixed="true">
|
||||||
|
<xs:extension base="mixedBasic">
|
||||||
|
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xs:element ref="b"/>
|
||||||
|
<xs:element ref="i"/>
|
||||||
|
<xs:element ref="a"/>
|
||||||
|
</xs:choice>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
<xs:complexType name="mixedOther" mixed="true">
|
||||||
|
<xs:complexContent mixed="true">
|
||||||
|
<xs:extension base="mixedExtended">
|
||||||
|
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||||
|
<xs:element ref="br"/>
|
||||||
|
</xs:choice>
|
||||||
|
</xs:extension>
|
||||||
|
</xs:complexContent>
|
||||||
|
</xs:complexType>
|
||||||
|
</xs:schema>
|
||||||
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="98" height="98" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M12 1C18.1 1 23 5.9 23 12C23 18.1 18.1 23 12 23C5.9 23 1 18.1 1 12C1 5.9 5.9 1 12 1ZM12 21C17 21 21 17 21 12C21 7 17 3 12 3C7 3 3 7 3 12C3 17 7 21 12 21Z" fill="#057ED1"/>
|
||||||
|
<path d="M12 11C12.6 11 13 11.4 13 12L13 16C13 16.6 12.6 17 12 17C11.4 17 11 16.6 11 16L11 12C11 11.4 11.4 11 12 11Z" fill="#057ED1"/>
|
||||||
|
<path d="M12 7C12.3 7 12.5 7.1 12.7 7.3C12.9 7.5 13 7.7 13 8C13 8.1 13 8.3 12.9 8.4C12.8 8.5 12.8 8.6 12.7 8.7C12.4 9 12 9.1 11.6 8.9C11.5 8.9 11.5 8.9 11.4 8.8C11.4 8.8 11.3 8.7 11.2 8.7C11.1 8.6 11 8.5 11 8.4C11 8.3 11 8.1 11 8C11 7.9 11 7.7 11.1 7.6C11.2 7.5 11.2 7.4 11.3 7.3C11.5 7.1 11.7 7 12 7Z" fill="#057ED1"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 739 B |
@@ -0,0 +1,5 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M3.49999 22C2.99999 22 2.49999 21.9 1.99999 21.6C0.599987 20.8 0.0999871 18.9 0.899987 17.5L9.39999 3.30002C9.69999 2.90002 9.99999 2.50002 10.4 2.30002C11.1 1.90002 11.9 1.80002 12.7 2.00002C13.5 2.20002 14.1 2.70002 14.6 3.40002L23 17.5C23.3 18 23.4 18.5 23.4 19C23.4 19.8 23.1 20.6 22.5 21.1C22 21.7 21.3 22 20.5 22H3.49999ZM11.1 4.40002L2.69999 18.5C2.39999 19 2.59999 19.6 3.09999 19.9C3.19999 20 3.39999 20 3.49999 20H20.4C20.7 20 20.9 19.9 21.1 19.7C21.3 19.5 21.4 19.3 21.4 19C21.4 18.8 21.4 18.7 21.3 18.5L12.9 4.40002C12.6 3.90002 12 3.80002 11.5 4.00002C11.3 4.10002 11.2 4.20002 11.1 4.40002Z" fill="white"/>
|
||||||
|
<path d="M12 14C11.4 14 11 13.6 11 13V9.00002C11 8.40002 11.4 8.00002 12 8.00002C12.6 8.00002 13 8.40002 13 9.00002V13C13 13.6 12.6 14 12 14Z" fill="white"/>
|
||||||
|
<path d="M12 18C11.7 18 11.5 17.9 11.3 17.7C11.1 17.5 11 17.3 11 17C11 16.9 11 16.7 11.1 16.6C11.2 16.5 11.2 16.4 11.3 16.3C11.4 16.2 11.5 16.1 11.6 16.1C11.8 16 12 16 12.2 16C12.3 16 12.3 16 12.4 16.1C12.5 16.1 12.5 16.1 12.6 16.2C12.6 16.2 12.7 16.3 12.8 16.3C12.9 16.4 13 16.5 13 16.6C13 16.7 13.1 16.9 13.1 17C13.1 17.3 13 17.5 12.8 17.7C12.5 17.9 12.3 18 12 18Z" fill="white"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.2 KiB |
@@ -0,0 +1,6 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M1.46447 15.4645C2.40215 14.5268 3.67392 14 5 14H12C13.3261 14 14.5979 14.5268 15.5355 15.4645C16.4732 16.4021 17 17.6739 17 19V21C17 21.5523 16.5523 22 16 22C15.4477 22 15 21.5523 15 21V19C15 18.2044 14.6839 17.4413 14.1213 16.8787C13.5587 16.3161 12.7956 16 12 16H5C4.20435 16 3.44129 16.3161 2.87868 16.8787C2.31607 17.4413 2 18.2044 2 19V21C2 21.5523 1.55228 22 1 22C0.447715 22 0 21.5523 0 21V19C0 17.6739 0.526784 16.4021 1.46447 15.4645Z" fill="#46535B"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M8.5 4C6.84315 4 5.5 5.34315 5.5 7C5.5 8.65685 6.84315 10 8.5 10C10.1569 10 11.5 8.65685 11.5 7C11.5 5.34315 10.1569 4 8.5 4ZM3.5 7C3.5 4.23858 5.73858 2 8.5 2C11.2614 2 13.5 4.23858 13.5 7C13.5 9.76142 11.2614 12 8.5 12C5.73858 12 3.5 9.76142 3.5 7Z" fill="#46535B"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.2929 7.29289C17.6834 6.90237 18.3166 6.90237 18.7071 7.29289L23.7071 12.2929C24.0976 12.6834 24.0976 13.3166 23.7071 13.7071C23.3166 14.0976 22.6834 14.0976 22.2929 13.7071L17.2929 8.70711C16.9024 8.31658 16.9024 7.68342 17.2929 7.29289Z" fill="#46535B"/>
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M23.7071 7.29289C24.0976 7.68342 24.0976 8.31658 23.7071 8.70711L18.7071 13.7071C18.3166 14.0976 17.6834 14.0976 17.2929 13.7071C16.9024 13.3166 16.9024 12.6834 17.2929 12.2929L22.2929 7.29289C22.6834 6.90237 23.3166 6.90237 23.7071 7.29289Z" fill="#46535B"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -1,7 +1,9 @@
|
|||||||
/* global $, axios, bootstrap, currentPagePath, initPagination, removeAllChildNodes, unsavedData */
|
/* global $, axios, bootstrap, currentPagePath, initPagination, removeAllChildNodes, unsavedData, replaceContainer, isI18nReady, i18next, validator */
|
||||||
|
|
||||||
// const currentPagePath = location.pathname
|
// const currentPagePath = location.pathname
|
||||||
|
|
||||||
|
let queryBattery = ''
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
initAdmin()
|
initAdmin()
|
||||||
})
|
})
|
||||||
@@ -62,11 +64,11 @@ function initAdmin() {
|
|||||||
renderResults(results)
|
renderResults(results)
|
||||||
updatePager(page, numberOfAllPages)
|
updatePager(page, numberOfAllPages)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let message = 'Prišlo je do napake.'
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
if (error.response?.data) {
|
if (error.response?.data) {
|
||||||
message = error.response.data
|
message = error.response.data
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
}
|
}
|
||||||
alert(message)
|
alert(message)
|
||||||
updatePager()
|
updatePager()
|
||||||
@@ -130,11 +132,11 @@ function initAdmin() {
|
|||||||
ifUserExists(data, type)
|
ifUserExists(data, type)
|
||||||
event.target.reset()
|
event.target.reset()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let message = 'Prišlo je do napake.'
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
message = error.response.data
|
message = error.response.data
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
}
|
}
|
||||||
console.log(message)
|
console.log(message)
|
||||||
}
|
}
|
||||||
@@ -142,62 +144,64 @@ function initAdmin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (currentPagePath === '/admin/uporabniki/seznam') {
|
if (currentPagePath === '/admin/uporabniki/seznam') {
|
||||||
const resultsListEl = document.getElementById('page-results')
|
isI18nReady.then(t => {
|
||||||
|
const resultsListEl = document.getElementById('page-results')
|
||||||
|
|
||||||
const updatePager = initPagination('pagination', onPageChange)
|
const updatePager = initPagination('pagination', onPageChange)
|
||||||
|
|
||||||
async function onPageChange(newPage) {
|
async function onPageChange(newPage) {
|
||||||
try {
|
try {
|
||||||
const { page, numberOfAllPages, results } = await getDataForPage(
|
const { page, numberOfAllPages, results } = await getDataForPage(
|
||||||
newPage
|
newPage
|
||||||
)
|
)
|
||||||
removeAllChildNodes(resultsListEl)
|
removeAllChildNodes(resultsListEl)
|
||||||
renderResults(results)
|
renderResults(results)
|
||||||
updatePager(page, numberOfAllPages)
|
updatePager(page, numberOfAllPages)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let message = 'Prišlo je do napake.'
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
if (error.response?.data) {
|
if (error.response?.data) {
|
||||||
message = error.response.data
|
message = error.response.data
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
|
}
|
||||||
|
alert(message)
|
||||||
|
updatePager()
|
||||||
}
|
}
|
||||||
alert(message)
|
|
||||||
updatePager()
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
async function getDataForPage(page) {
|
async function getDataForPage(page) {
|
||||||
const url = `/api/v1/users/listAllUsers?p=${page}`
|
const url = `/api/v1/users/listAllUsers?p=${page}`
|
||||||
const { data } = await axios.get(url)
|
const { data } = await axios.get(url)
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderResults(results) {
|
function renderResults(results) {
|
||||||
results.forEach(result => {
|
results.forEach(result => {
|
||||||
const rowEl = document.createElement('tr')
|
const rowEl = document.createElement('tr')
|
||||||
const td1 = document.createElement('td')
|
const td1 = document.createElement('td')
|
||||||
const td2 = document.createElement('td')
|
const td2 = document.createElement('td')
|
||||||
const td3 = document.createElement('td')
|
const td3 = document.createElement('td')
|
||||||
const td4 = document.createElement('td')
|
const td4 = document.createElement('td')
|
||||||
const aEl = document.createElement('a')
|
const aEl = document.createElement('a')
|
||||||
const imgEl = document.createElement('img')
|
const imgEl = document.createElement('img')
|
||||||
const spanEl = document.createElement('span')
|
const spanEl = document.createElement('span')
|
||||||
td1.textContent = result.userName
|
td1.textContent = result.userName
|
||||||
td2.textContent = result.email
|
td2.textContent = result.email
|
||||||
td3.textContent = result.status
|
td3.textContent = t(`userStatus${result.status}`)
|
||||||
aEl.classList.add('image-link')
|
aEl.classList.add('image-link')
|
||||||
aEl.type = 'link'
|
aEl.type = 'link'
|
||||||
aEl.href = `/admin/uporabniki/${result.id}/urejanje`
|
aEl.href = `/admin/uporabniki/${result.id}/urejanje`
|
||||||
imgEl.src = '/images/u_edit-alt.svg'
|
imgEl.src = '/images/u_edit-alt.svg'
|
||||||
imgEl.alt = 'Uredi'
|
imgEl.alt = i18next.t('Uredi')
|
||||||
spanEl.className = 'normal-gray ms-1'
|
spanEl.className = 'normal-gray ms-1'
|
||||||
spanEl.textContent = 'Uredi'
|
spanEl.textContent = i18next.t('Uredi')
|
||||||
td4.append(aEl)
|
td4.append(aEl)
|
||||||
aEl.append(imgEl, spanEl)
|
aEl.append(imgEl, spanEl)
|
||||||
rowEl.append(td1, td2, td3, td4)
|
rowEl.append(td1, td2, td3, td4)
|
||||||
resultsListEl.appendChild(rowEl)
|
resultsListEl.appendChild(rowEl)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentPagePath === '/admin/slovarji') {
|
if (currentPagePath === '/admin/slovarji') {
|
||||||
@@ -214,11 +218,11 @@ function initAdmin() {
|
|||||||
renderResults(results)
|
renderResults(results)
|
||||||
updatePager(page, numberOfAllPages)
|
updatePager(page, numberOfAllPages)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let message = 'Prišlo je do napake.'
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
if (error.response?.data) {
|
if (error.response?.data) {
|
||||||
message = error.response.data
|
message = error.response.data
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
}
|
}
|
||||||
alert(message)
|
alert(message)
|
||||||
updatePager()
|
updatePager()
|
||||||
@@ -267,9 +271,9 @@ function initAdmin() {
|
|||||||
aEl.type = 'link'
|
aEl.type = 'link'
|
||||||
aEl.href = `/admin/slovarji/${result.id}/podatki`
|
aEl.href = `/admin/slovarji/${result.id}/podatki`
|
||||||
imgEl.src = '/images/u_edit-alt.svg'
|
imgEl.src = '/images/u_edit-alt.svg'
|
||||||
imgEl.alt = 'Uredi'
|
imgEl.alt = i18next.t('Uredi')
|
||||||
spanEl.className = 'normal-gray ms-1'
|
spanEl.className = 'normal-gray ms-1'
|
||||||
spanEl.textContent = 'Uredi'
|
spanEl.textContent = i18next.t('Uredi')
|
||||||
td6.append(aEl)
|
td6.append(aEl)
|
||||||
aEl.append(imgEl, spanEl)
|
aEl.append(imgEl, spanEl)
|
||||||
rowEl.append(td1, td2, td3, td4, td5, td6)
|
rowEl.append(td1, td2, td3, td4, td5, td6)
|
||||||
@@ -278,12 +282,16 @@ function initAdmin() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\/admin\/slovarji\/\d+\/podatki/.test(currentPagePath)) {
|
if (
|
||||||
|
/\/admin\/slovarji\/\d+\/podatki/.test(currentPagePath) ||
|
||||||
|
/\/slovarji\/\d+\/podatki/.test(currentPagePath)
|
||||||
|
) {
|
||||||
const formEl = document.getElementById('admin-description')
|
const formEl = document.getElementById('admin-description')
|
||||||
const imgTrashIcon = document.querySelectorAll('.delete-author-btn')
|
const imgTrashIcon = document.querySelectorAll('.delete-author-btn')
|
||||||
const inputNewAuthorEl = document.getElementById('input-new-author')
|
const inputNewAuthorEl = document.getElementById('input-new-author')
|
||||||
const inputNewAreaEl = document.getElementById('input-new-area')
|
const inputNewAreaEl = document.getElementById('input-new-area')
|
||||||
const dictSideMenu = document.querySelector('.admin-nav-content')
|
const dictSideMenu = document.querySelector('.admin-nav-content')
|
||||||
|
$('.without-addition').on('change', enableButton)
|
||||||
unsavedData(formEl, dictSideMenu)
|
unsavedData(formEl, dictSideMenu)
|
||||||
formEl.addEventListener('input', enableButton)
|
formEl.addEventListener('input', enableButton)
|
||||||
if (imgTrashIcon !== null) {
|
if (imgTrashIcon !== null) {
|
||||||
@@ -343,18 +351,20 @@ function initAdmin() {
|
|||||||
|
|
||||||
async function onPageChange(newPage) {
|
async function onPageChange(newPage) {
|
||||||
try {
|
try {
|
||||||
const { page, numberOfAllPages, results } = await getDataForPage(
|
const results = await getDataForPage(newPage)
|
||||||
newPage
|
|
||||||
)
|
const numberOfAllPages = +results.headers['number-of-all-pages']
|
||||||
|
const page = +results.headers.page
|
||||||
|
|
||||||
removeAllChildNodes(resultsListEl)
|
removeAllChildNodes(resultsListEl)
|
||||||
renderResults(results)
|
renderResults(results.data)
|
||||||
updatePager(page, numberOfAllPages)
|
updatePager(page, numberOfAllPages)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let message = 'Prišlo je do napake.'
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
if (error.response?.data) {
|
if (error.response?.data) {
|
||||||
message = error.response.data
|
message = error.response.data
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
}
|
}
|
||||||
alert(message)
|
alert(message)
|
||||||
updatePager()
|
updatePager()
|
||||||
@@ -362,56 +372,46 @@ function initAdmin() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getDataForPage(page) {
|
async function getDataForPage(page) {
|
||||||
const url = `/api/v1/dictionaries/listSecondaryDomains?p=${page}`
|
const url = `/api/v1/dictionaries/secondaryDomains?q=${queryBattery}&p=${page}`
|
||||||
const { data } = await axios.get(url)
|
return await axios.get(url)
|
||||||
return data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderResults(results) {
|
function renderResults(results) {
|
||||||
results.forEach(result => {
|
replaceContainer('page-results', results)
|
||||||
const rowEl = document.createElement('tr')
|
}
|
||||||
const input = document.createElement('input')
|
|
||||||
const th = document.createElement('th')
|
|
||||||
const input2 = document.createElement('input')
|
|
||||||
const td1 = document.createElement('td')
|
|
||||||
const tdTrans = document.createElement('td')
|
|
||||||
const td2 = document.createElement('td')
|
|
||||||
const div = document.createElement('div')
|
|
||||||
const editBtn = document.createElement('button')
|
|
||||||
const deleteBtn = document.createElement('button')
|
|
||||||
const imgEditEl = document.createElement('img')
|
|
||||||
const imgDelEl = document.createElement('img')
|
|
||||||
input.value = result.id
|
|
||||||
input.type = 'hidden'
|
|
||||||
input.name = 'domainLabelId'
|
|
||||||
th.scope = 'row'
|
|
||||||
input2.className = 'form-check checkbox-table'
|
|
||||||
input2.type = 'checkbox'
|
|
||||||
input2.name = 'isVisible'
|
|
||||||
input2.disabled = true
|
|
||||||
input2.checked = !!result.isApproved
|
|
||||||
th.append(input2)
|
|
||||||
td1.className = 'tdata-area'
|
|
||||||
td1.textContent = result.nameSl
|
|
||||||
tdTrans.className = 'tdata-translation'
|
|
||||||
tdTrans.textContent = result.nameEn
|
|
||||||
td2.classList.add('buttons-group')
|
|
||||||
div.classList.add('table-buttons')
|
|
||||||
editBtn.className = 'p-0 table-button-grp me-3 edit-row-btn'
|
|
||||||
editBtn.type = 'button'
|
|
||||||
imgEditEl.src = '/images/u_edit-alt.svg'
|
|
||||||
deleteBtn.className = 'p-0 table-button-grp delete-row-btn'
|
|
||||||
deleteBtn.dataset.bsTarget = '#alert-modal'
|
|
||||||
deleteBtn.dataset.bsToggle = 'modal'
|
|
||||||
deleteBtn.type = 'button'
|
|
||||||
imgDelEl.src = '/images/red-trash-icon.svg'
|
|
||||||
td2.append(div)
|
|
||||||
div.append(editBtn, deleteBtn)
|
|
||||||
editBtn.append(imgEditEl)
|
|
||||||
deleteBtn.append(imgDelEl)
|
|
||||||
|
|
||||||
rowEl.append(input, th, td1, tdTrans, td2)
|
const updatePaginationOnFilter = axiosResult => {
|
||||||
resultsListEl.appendChild(rowEl)
|
const numberOfAllPages = +axiosResult.headers['number-of-all-pages']
|
||||||
|
const page = +axiosResult.headers.page
|
||||||
|
// removeAllChildNodes(resultsListEl)
|
||||||
|
// renderResults(results.data)
|
||||||
|
// console.log(numberOfAllPages)
|
||||||
|
// console.log(page)
|
||||||
|
// console.log(updatePager)
|
||||||
|
updatePager(page, numberOfAllPages)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// / Due to unsual design, the function was moved inside
|
||||||
|
function searchController() {
|
||||||
|
// if (window.location.pathname.includes('podrocne-oznake')) { // No if required since already checkek above
|
||||||
|
queryBattery = document.getElementById('input-search').value
|
||||||
|
// console.log(queryBattery)
|
||||||
|
axios
|
||||||
|
.get(`/api/v1/dictionaries/secondaryDomains?q=${queryBattery}`)
|
||||||
|
.then(result => {
|
||||||
|
updatePaginationOnFilter(result)
|
||||||
|
replaceContainer('page-results', result.data)
|
||||||
|
})
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
const inlineSearchButton = document.getElementById('inline-search-btn')
|
||||||
|
|
||||||
|
if (inlineSearchButton) {
|
||||||
|
inlineSearchButton.addEventListener('click', searchController)
|
||||||
|
$('#input-search').on('keyup', function (e) {
|
||||||
|
if (e.code === 'Enter' || e.code === 'NumpadEnter') {
|
||||||
|
searchController()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -450,7 +450,7 @@ function initAdmin() {
|
|||||||
ifUserExists(data, type)
|
ifUserExists(data, type)
|
||||||
event.target.reset()
|
event.target.reset()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = 'Prišlo je do napake.'
|
const message = i18next.t('Prišlo je do napake.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -460,17 +460,6 @@ function initAdmin() {
|
|||||||
formEditUser.addEventListener('input', enableButton)
|
formEditUser.addEventListener('input', enableButton)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\/slovarji\/\d+\/struktura/.test(currentPagePath)) {
|
|
||||||
changePreview()
|
|
||||||
const switchForm = document.querySelector('.switch-forms')
|
|
||||||
switchForm.addEventListener('click', () => changePreview())
|
|
||||||
const wholeForm = document.getElementById('form-dictionary-structure')
|
|
||||||
wholeForm.addEventListener('input', enableButton)
|
|
||||||
const dictSideMenu = document.querySelector('.admin-nav-content')
|
|
||||||
unsavedData(wholeForm, dictSideMenu)
|
|
||||||
$('#languages-input').on('change', enableButton)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentPagePath === '/admin/nastavitve/portal') {
|
if (currentPagePath === '/admin/nastavitve/portal') {
|
||||||
const formAdminPortal = document.getElementById('admin-portal-settings')
|
const formAdminPortal = document.getElementById('admin-portal-settings')
|
||||||
formAdminPortal.addEventListener('input', enableButton)
|
formAdminPortal.addEventListener('input', enableButton)
|
||||||
@@ -537,7 +526,7 @@ function createNewAuthorInput(pageForm) {
|
|||||||
divAuthor.className = 'author mt-sm-4 added-field'
|
divAuthor.className = 'author mt-sm-4 added-field'
|
||||||
divSubjectName.className = 'subject-name'
|
divSubjectName.className = 'subject-name'
|
||||||
spanName.className = 'smaller-gray-uppercase'
|
spanName.className = 'smaller-gray-uppercase'
|
||||||
spanName.textContent = 'AVTOR'
|
spanName.textContent = i18next.t('AVTOR')
|
||||||
divRow.className = 'row align-items-center'
|
divRow.className = 'row align-items-center'
|
||||||
divColSm5.className = 'col-sm-6'
|
divColSm5.className = 'col-sm-6'
|
||||||
inputGroup.className = 'input-group'
|
inputGroup.className = 'input-group'
|
||||||
@@ -553,7 +542,7 @@ function createNewAuthorInput(pageForm) {
|
|||||||
divColSm.className = 'col-sm'
|
divColSm.className = 'col-sm'
|
||||||
spanNameInfoTxt.className =
|
spanNameInfoTxt.className =
|
||||||
'd-md-inline d-block name-info-txt ms-xxl-3 ms-md-3 mt-3 mt-ms-0'
|
'd-md-inline d-block name-info-txt ms-xxl-3 ms-md-3 mt-3 mt-ms-0'
|
||||||
spanNameInfoTxt.textContent = 'Dodaten avtor.'
|
spanNameInfoTxt.textContent = i18next.t('Dodaten avtor.')
|
||||||
|
|
||||||
divAuthor.appendChild(divSubjectName)
|
divAuthor.appendChild(divSubjectName)
|
||||||
divSubjectName.appendChild(spanName)
|
divSubjectName.appendChild(spanName)
|
||||||
@@ -594,9 +583,9 @@ function createNewAreaInput(pageForm) {
|
|||||||
divSmallNameArea.className = 'author mt-4 added-field'
|
divSmallNameArea.className = 'author mt-4 added-field'
|
||||||
divSubjectName.className = 'subject-name'
|
divSubjectName.className = 'subject-name'
|
||||||
spanInputNameTxtSlo.className = 'smaller-gray-uppercase'
|
spanInputNameTxtSlo.className = 'smaller-gray-uppercase'
|
||||||
spanInputNameTxtSlo.textContent = 'NOVO PODPODROČJE (slovensko)'
|
spanInputNameTxtSlo.textContent = i18next.t('NOVO PODPODROČJE (slovensko)')
|
||||||
spanInputNameTxtEng.className = 'smaller-gray-uppercase mt-4'
|
spanInputNameTxtEng.className = 'smaller-gray-uppercase mt-4'
|
||||||
spanInputNameTxtEng.textContent = 'NOVO PODPODROČJE (angleško)'
|
spanInputNameTxtEng.textContent = i18next.t('NOVO PODPODROČJE (angleško)')
|
||||||
divRow.className = 'row align-items-center'
|
divRow.className = 'row align-items-center'
|
||||||
divRow2.className = 'row align-items-center'
|
divRow2.className = 'row align-items-center'
|
||||||
divEnglishInput.className = 'mt-4'
|
divEnglishInput.className = 'mt-4'
|
||||||
@@ -617,11 +606,12 @@ function createNewAreaInput(pageForm) {
|
|||||||
divColSm.className = 'col-sm mt-3 mt-sm-0'
|
divColSm.className = 'col-sm mt-3 mt-sm-0'
|
||||||
divColSm2.className = 'col-sm mt-3 mt-sm-0'
|
divColSm2.className = 'col-sm mt-3 mt-sm-0'
|
||||||
spanNameInfoTxt.className = 'd-sm-inline name-info-txt ms-xxl-3 ms-md-3'
|
spanNameInfoTxt.className = 'd-sm-inline name-info-txt ms-xxl-3 ms-md-3'
|
||||||
spanNameInfoTxt.textContent =
|
spanNameInfoTxt.textContent = i18next.t(
|
||||||
'Vpišite novo podpodročje. Na seznamu podpodročij bo vidno takoj po potrditvi administratorja portala.'
|
'Vpišite novo podpodročje. Na seznamu podpodročij bo vidno takoj po potrditvi administratorja portala.'
|
||||||
|
)
|
||||||
spanNameInfoTxtEng.className =
|
spanNameInfoTxtEng.className =
|
||||||
'd-sm-inline name-info-txt ms-xxl-3 ms-md-3 mt-4'
|
'd-sm-inline name-info-txt ms-xxl-3 ms-md-3 mt-4'
|
||||||
spanNameInfoTxtEng.textContent = 'Novo podpodročje (angleško).'
|
spanNameInfoTxtEng.textContent = i18next.t('Novo podpodročje (angleško).')
|
||||||
|
|
||||||
divSmallNameArea.appendChild(divSubjectName)
|
divSmallNameArea.appendChild(divSubjectName)
|
||||||
divSubjectName.appendChild(spanInputNameTxtSlo)
|
divSubjectName.appendChild(spanInputNameTxtSlo)
|
||||||
@@ -782,7 +772,7 @@ function handleAreasClick({ target }) {
|
|||||||
const newButtonGroup = document.createElement('div')
|
const newButtonGroup = document.createElement('div')
|
||||||
const cancelButton = document.createElement('button')
|
const cancelButton = document.createElement('button')
|
||||||
const saveButton = document.createElement('button')
|
const saveButton = document.createElement('button')
|
||||||
cancelButton.textContent = 'Prekliči'
|
cancelButton.textContent = i18next.t('Prekliči')
|
||||||
cancelButton.type = 'button'
|
cancelButton.type = 'button'
|
||||||
cancelButton.className = 'btn btn-secondary me-2'
|
cancelButton.className = 'btn btn-secondary me-2'
|
||||||
cancelButton.style.height = '33px'
|
cancelButton.style.height = '33px'
|
||||||
@@ -790,7 +780,7 @@ function handleAreasClick({ target }) {
|
|||||||
cancelButton.addEventListener('click', () =>
|
cancelButton.addEventListener('click', () =>
|
||||||
abortEditing(newButtonGroup, tableButtons, tDataArea, tDataTranslation)
|
abortEditing(newButtonGroup, tableButtons, tDataArea, tDataTranslation)
|
||||||
)
|
)
|
||||||
saveButton.textContent = 'POTRDI'
|
saveButton.textContent = i18next.t('POTRDI')
|
||||||
saveButton.type = 'button'
|
saveButton.type = 'button'
|
||||||
saveButton.className = 'btn btn-primary'
|
saveButton.className = 'btn btn-primary'
|
||||||
saveButton.style.height = '33px'
|
saveButton.style.height = '33px'
|
||||||
@@ -962,7 +952,7 @@ function mobileMoveContent() {
|
|||||||
// primaryButton.style.marginRight = '10px'
|
// primaryButton.style.marginRight = '10px'
|
||||||
primaryButton.style.whiteSpace = 'nowrap'
|
primaryButton.style.whiteSpace = 'nowrap'
|
||||||
}
|
}
|
||||||
navTitle.textContent = siteHeadingTextContent
|
if (navTitle) navTitle.textContent = siteHeadingTextContent
|
||||||
siteHeading.style.display = 'none'
|
siteHeading.style.display = 'none'
|
||||||
}
|
}
|
||||||
if (document.body.clientWidth > 1200) {
|
if (document.body.clientWidth > 1200) {
|
||||||
@@ -982,80 +972,18 @@ function mobileMoveContent() {
|
|||||||
primaryButton.style.whiteSpace = ''
|
primaryButton.style.whiteSpace = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (navTitle) {
|
||||||
currentPagePath.includes('slovarji') &&
|
if (
|
||||||
!currentPagePath.includes('admin')
|
currentPagePath.includes('slovarji') &&
|
||||||
)
|
!currentPagePath.includes('admin')
|
||||||
navTitle.textContent = 'Urejanje'
|
)
|
||||||
else navTitle.textContent = 'Administrator'
|
navTitle.textContent = i18next.t('Urejanje')
|
||||||
|
else navTitle.textContent = i18next.t('Administracija')
|
||||||
|
}
|
||||||
siteHeading.style.display = 'block'
|
siteHeading.style.display = 'block'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function changePreview() {
|
|
||||||
const switchDomainLabel = document.getElementById('domain-labels')
|
|
||||||
const switchLabel = document.getElementById('label-checkbox')
|
|
||||||
const switchDefinition = document.getElementById('definition-check-box')
|
|
||||||
const switchSynonym = document.getElementById('synonyms')
|
|
||||||
const switchLink = document.getElementById('links')
|
|
||||||
const switchForeignLang = document.getElementById('language-group')
|
|
||||||
const switchForeignTerm = document.getElementById('termin-language-subgroup')
|
|
||||||
const switchForeignDef = document.getElementById(
|
|
||||||
'definition-language-subgroup'
|
|
||||||
)
|
|
||||||
const switchForeignSyn = document.getElementById('synonym-language-subgroup')
|
|
||||||
const switchImage = document.getElementById('images')
|
|
||||||
const switchAudio = document.getElementById('audio')
|
|
||||||
const switchVideo = document.getElementById('video')
|
|
||||||
|
|
||||||
const previewDomainSecondary = document.querySelector(
|
|
||||||
'.preview-domain-secondary'
|
|
||||||
)
|
|
||||||
const selectedLabel = document.querySelector('.preview-label')
|
|
||||||
const selectedDef = document.querySelector('.preview-definition')
|
|
||||||
const selectedSynonyms = document.querySelector('.preview-synonym')
|
|
||||||
const linkedTerms = document.getElementById('linked-terms')
|
|
||||||
const foreignLanguages = document.querySelector(
|
|
||||||
'.preview-languages-container'
|
|
||||||
)
|
|
||||||
const listForeignTerm = document.querySelector('.preview-f-term')
|
|
||||||
const listForeignDefinitions = document.querySelector(
|
|
||||||
'.preview-foreign-definition'
|
|
||||||
)
|
|
||||||
const foreignSynonyms = document.querySelector('.preview-foreign-synonyms')
|
|
||||||
const selectedImages = document.querySelector('.preview-images')
|
|
||||||
const selectedAudio = document.querySelector('.preview-audio')
|
|
||||||
const selectedVideo = document.querySelector('.preview-video')
|
|
||||||
|
|
||||||
if (!switchDomainLabel.checked)
|
|
||||||
previewDomainSecondary.classList.add('hide-preview')
|
|
||||||
else previewDomainSecondary.classList.remove('hide-preview')
|
|
||||||
if (!switchLabel.checked) selectedLabel.classList.add('hide-preview')
|
|
||||||
else selectedLabel.classList.remove('hide-preview')
|
|
||||||
if (!switchDefinition.checked) selectedDef.classList.add('hide-preview')
|
|
||||||
else selectedDef.classList.remove('hide-preview')
|
|
||||||
if (!switchSynonym.checked) selectedSynonyms.classList.add('hide-preview')
|
|
||||||
else selectedSynonyms.classList.remove('hide-preview')
|
|
||||||
if (!switchLink.checked) linkedTerms.classList.add('hide-preview')
|
|
||||||
else linkedTerms.classList.remove('hide-preview')
|
|
||||||
if (!switchForeignLang.checked) foreignLanguages.classList.add('hide-preview')
|
|
||||||
else foreignLanguages.classList.remove('hide-preview')
|
|
||||||
if (!switchForeignSyn.checked) foreignSynonyms.classList.add('hide-preview')
|
|
||||||
else foreignSynonyms.classList.remove('hide-preview')
|
|
||||||
if (!switchForeignTerm.checked) listForeignTerm.classList.add('hide-preview')
|
|
||||||
else listForeignTerm.classList.remove('hide-preview')
|
|
||||||
if (!switchForeignDef.checked)
|
|
||||||
listForeignDefinitions.classList.add('hide-preview')
|
|
||||||
else listForeignDefinitions.classList.remove('hide-preview')
|
|
||||||
|
|
||||||
if (!switchImage.checked) selectedImages.classList.add('hide-preview')
|
|
||||||
else selectedImages.classList.remove('hide-preview')
|
|
||||||
if (!switchAudio.checked) selectedAudio.classList.add('hide-preview')
|
|
||||||
else selectedAudio.classList.remove('hide-preview')
|
|
||||||
if (!switchVideo.checked) selectedVideo.classList.add('hide-preview')
|
|
||||||
else selectedVideo.classList.remove('hide-preview')
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkUserRightsCb(el) {
|
function checkUserRightsCb(el) {
|
||||||
const { terminologyReviewCb, languageReviewCb } = window.adminElements
|
const { terminologyReviewCb, languageReviewCb } = window.adminElements
|
||||||
const termRevCbs = document.querySelectorAll('.terminology-review-cb')
|
const termRevCbs = document.querySelectorAll('.terminology-review-cb')
|
||||||
@@ -1109,12 +1037,18 @@ function ifUserExists(data, type) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (bool === true && data[0] !== undefined) {
|
if (bool === true && data[0] !== undefined) {
|
||||||
modalText.textContent = `Uporabnik ${data[0].username} je že v tabeli.`
|
// modalText.textContent = `Uporabnik ${data[0].username} je že v tabeli.`
|
||||||
|
modalText.textContent =
|
||||||
|
i18next.t('Uporabnik') +
|
||||||
|
`${data[0].username}` +
|
||||||
|
i18next.t('je že v tabeli.')
|
||||||
modalEl.toggle()
|
modalEl.toggle()
|
||||||
} else createNewUserArea(data, type)
|
} else createNewUserArea(data, type)
|
||||||
} else createNewUserArea(data, type)
|
} else createNewUserArea(data, type)
|
||||||
} else {
|
} else {
|
||||||
modalText.textContent = `Preverite, če ste pravilno vpisali uporabniško ime. Bodite pozorni na velike in male črke.`
|
modalText.textContent = i18next.t(
|
||||||
|
'Preverite, če ste pravilno vpisali uporabniško ime. Bodite pozorni na velike in male črke.'
|
||||||
|
)
|
||||||
modalEl.toggle()
|
modalEl.toggle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1172,24 +1106,24 @@ function createNewUserArea(data, type) {
|
|||||||
inputConsultAdmin.type = 'checkbox'
|
inputConsultAdmin.type = 'checkbox'
|
||||||
|
|
||||||
if (type === 'portal') {
|
if (type === 'portal') {
|
||||||
tdPortAdminOrAdmin.dataset.label = 'Skrbnik portala'
|
tdPortAdminOrAdmin.dataset.label = i18next.t('Skrbnik portala')
|
||||||
inputPortAdminOrAdmin.name = `rolesPerUser['${data[0].id}'][isPortalAdmin]`
|
inputPortAdminOrAdmin.name = `rolesPerUser['${data[0].id}'][isPortalAdmin]`
|
||||||
tdDictAdmin.dataset.label = 'Skrbnik slovarjev'
|
tdDictAdmin.dataset.label = i18next.t('Skrbnik slovarjev')
|
||||||
inputDictAdmin.name = `rolesPerUser['${data[0].id}'][isDictionariesAdmin]`
|
inputDictAdmin.name = `rolesPerUser['${data[0].id}'][isDictionariesAdmin]`
|
||||||
tdConsultAdmin.dataset.label = 'Skrbnik svetovalnice'
|
tdConsultAdmin.dataset.label = i18next.t('Skrbnik svetovalnice')
|
||||||
inputConsultAdmin.name = `rolesPerUser['${data[0].id}'][isConsultancyAdmin]`
|
inputConsultAdmin.name = `rolesPerUser['${data[0].id}'][isConsultancyAdmin]`
|
||||||
} else {
|
} else {
|
||||||
tdPortAdminOrAdmin.dataset.label = 'Administrator'
|
tdPortAdminOrAdmin.dataset.label = i18next.t('Administrator')
|
||||||
inputPortAdminOrAdmin.classList.add('administration-cb')
|
inputPortAdminOrAdmin.classList.add('administration-cb')
|
||||||
inputPortAdminOrAdmin.name = `rightsPerUser['${data[0].id}'][isAdministration]`
|
inputPortAdminOrAdmin.name = `rightsPerUser['${data[0].id}'][isAdministration]`
|
||||||
tdDictAdmin.dataset.label = 'Urejanje'
|
tdDictAdmin.dataset.label = i18next.t('Urejanje')
|
||||||
inputDictAdmin.name = `rightsPerUser['${data[0].id}'][isEditing]`
|
inputDictAdmin.name = `rightsPerUser['${data[0].id}'][isEditing]`
|
||||||
inputDictAdmin.classList.add('edit-cb')
|
inputDictAdmin.classList.add('edit-cb')
|
||||||
tdConsultAdmin.dataset.label = 'Strokovni pregled'
|
tdConsultAdmin.dataset.label = i18next.t('Strokovni pregled')
|
||||||
inputConsultAdmin.classList.add('terminology-review-cb')
|
inputConsultAdmin.classList.add('terminology-review-cb')
|
||||||
inputConsultAdmin.name = `rightsPerUser['${data[0].id}'][isTerminologyReview]`
|
inputConsultAdmin.name = `rightsPerUser['${data[0].id}'][isTerminologyReview]`
|
||||||
tdLanguageRev.className = 'pt-1 pb-1'
|
tdLanguageRev.className = 'pt-1 pb-1'
|
||||||
tdLanguageRev.dataset.label = 'Jezikovni pregled'
|
tdLanguageRev.dataset.label = i18next.t('Jezikovni pregled')
|
||||||
divLanguageRev.className =
|
divLanguageRev.className =
|
||||||
'form-check d-flex justify-content-left justify-content-xl-center'
|
'form-check d-flex justify-content-left justify-content-xl-center'
|
||||||
inputLanguageRev.className = 'language-review-cb form-check-input'
|
inputLanguageRev.className = 'language-review-cb form-check-input'
|
||||||
@@ -1235,7 +1169,7 @@ function createNewUserArea(data, type) {
|
|||||||
// Summernote
|
// Summernote
|
||||||
|
|
||||||
$('.summernote').summernote({
|
$('.summernote').summernote({
|
||||||
placeholder: 'Na kratko opišite zasnovo in namen slovarja.',
|
placeholder: i18next.t('Na kratko opišite zasnovo in namen slovarja.'),
|
||||||
height: 300,
|
height: 300,
|
||||||
minheight: 150,
|
minheight: 150,
|
||||||
toolbar: [
|
toolbar: [
|
||||||
@@ -1254,7 +1188,7 @@ $('.summernote').summernote({
|
|||||||
const profileForm = document.getElementById('profileForm')
|
const profileForm = document.getElementById('profileForm')
|
||||||
|
|
||||||
if (profileForm) {
|
if (profileForm) {
|
||||||
profileForm.addEventListener('change', e => {
|
profileForm.addEventListener('input', e => {
|
||||||
enableButton()
|
enableButton()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1263,12 +1197,30 @@ $('.summernote').summernote({
|
|||||||
|
|
||||||
const data = Object.fromEntries(new FormData(e.target))
|
const data = Object.fromEntries(new FormData(e.target))
|
||||||
|
|
||||||
|
const notifyAction = () => {
|
||||||
|
document.getElementById('fpi-text').innerHTML = i18next.t(
|
||||||
|
'Izpolnite vsa prazna polja.'
|
||||||
|
)
|
||||||
|
$('#reset-pass-info').modal('show')
|
||||||
|
}
|
||||||
|
|
||||||
if (data.numberOfHits) {
|
if (data.numberOfHits) {
|
||||||
await handleUpdateHitsPerPage(data.numberOfHits)
|
await handleUpdateHitsPerPage(data.numberOfHits)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.name && data.surname) {
|
if (data.firstName && data.lastName && data.email) {
|
||||||
await handleUpdateUsersName(data.name, data.surname)
|
await handleUpdateBasicData(data)
|
||||||
|
} else if (location.pathname === '/moj-racun') {
|
||||||
|
// if missing the required data on endpoint /moj-racun, notify!
|
||||||
|
notifyAction()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.passwordOld && data.passwordNew && data.passwordNewRepeat) {
|
||||||
|
await handleUpdatePassword(data)
|
||||||
|
} else if (location.pathname === '/spremeni-geslo') {
|
||||||
|
// if missing the required data on endpoint /spremeni-geslo, notify!
|
||||||
|
notifyAction()
|
||||||
}
|
}
|
||||||
|
|
||||||
// console.log(data)
|
// console.log(data)
|
||||||
@@ -1278,23 +1230,60 @@ $('.summernote').summernote({
|
|||||||
async function handleUpdateHitsPerPage(hitAmount) {
|
async function handleUpdateHitsPerPage(hitAmount) {
|
||||||
try {
|
try {
|
||||||
await axios.post('/api/v1/users/hitsPerPage', { hitAmount })
|
await axios.post('/api/v1/users/hitsPerPage', { hitAmount })
|
||||||
} catch (error) {
|
|
||||||
// console.log(error)
|
|
||||||
} finally {
|
|
||||||
location.reload(true)
|
location.reload(true)
|
||||||
|
} catch (error) {
|
||||||
|
displayError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleUpdateUsersName(name, surname) {
|
async function handleUpdateBasicData(payload) {
|
||||||
try {
|
try {
|
||||||
await axios.post('/api/v1/users/nameAndSurname', {
|
if (!validator.isEmail(payload.email)) {
|
||||||
name,
|
document.getElementById('fpi-text').innerHTML =
|
||||||
surname
|
i18next.t('Neveljavna e-pošta')
|
||||||
})
|
$('#reset-pass-info').modal('show')
|
||||||
} catch (error) {
|
return
|
||||||
// console.log(error)
|
}
|
||||||
} finally {
|
await axios.post('/api/v1/users/basic-data', payload)
|
||||||
location.reload(true)
|
location.reload(true)
|
||||||
|
} catch (error) {
|
||||||
|
displayError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleUpdatePassword(payload) {
|
||||||
|
try {
|
||||||
|
if (payload.passwordNew !== payload.passwordNewRepeat) {
|
||||||
|
document.getElementById('fpi-text').innerHTML = i18next.t(
|
||||||
|
'Gesli se ne ujemata'
|
||||||
|
)
|
||||||
|
$('#reset-pass-info').modal('show')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validator.isLength(payload.passwordNew, { min: 8 })) {
|
||||||
|
document.getElementById('fpi-text').innerHTML =
|
||||||
|
i18next.t('Geslo je prekratko')
|
||||||
|
$('#reset-pass-info').modal('show')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await axios.post('/api/v1/users/password', payload)
|
||||||
|
location.reload(true)
|
||||||
|
} catch (error) {
|
||||||
|
displayError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayError(error) {
|
||||||
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
|
if (error.response) {
|
||||||
|
message = error.response.data
|
||||||
|
} else if (error.request) {
|
||||||
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('fpi-text').textContent = message
|
||||||
|
$('#reset-pass-info').modal('show')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,21 +4,28 @@
|
|||||||
const commentsContainer = document.querySelector('#comments-container')
|
const commentsContainer = document.querySelector('#comments-container')
|
||||||
const dropImage = document.querySelector('#dropImage')
|
const dropImage = document.querySelector('#dropImage')
|
||||||
const commentCount = document.querySelector('#comments-count')
|
const commentCount = document.querySelector('#comments-count')
|
||||||
|
const collapsableHR = document.querySelector('.collapable-hr')
|
||||||
|
|
||||||
const hide = () => {
|
const hide = () => {
|
||||||
pager.className += ' d-none'
|
pager.className += ' invisible'
|
||||||
commentsContainer.className += ' d-none'
|
commentsContainer.className += ' d-none'
|
||||||
commentCount.className += ' d-none'
|
commentCount.className += ' invisible'
|
||||||
|
collapsableHR.className += ' d-none'
|
||||||
|
}
|
||||||
|
|
||||||
|
const show = () => {
|
||||||
|
pager.className = 'pager d-flex'
|
||||||
|
commentsContainer.className = 'comments-container pe-1'
|
||||||
|
dropImage.src = '/images/arrow_drop_up.svg'
|
||||||
|
commentCount.className = 'comment-count d-flex ms-auto me-4 text-p875rem'
|
||||||
|
collapsableHR.className += 'comments-top-hr mt-3'
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('collapseComments').addEventListener('click', e => {
|
document.getElementById('collapseComments').addEventListener('click', e => {
|
||||||
isVisible = !isVisible
|
isVisible = !isVisible
|
||||||
|
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
pager.className = 'pager'
|
show()
|
||||||
commentsContainer.className = 'comments-container pe-1'
|
|
||||||
dropImage.src = '/images/arrow_drop_up.svg'
|
|
||||||
commentCount.className = 'comment-count d-flex ms-auto me-4 text-p875rem'
|
|
||||||
} else {
|
} else {
|
||||||
dropImage.src = '/images/arrow_drop_down.svg'
|
dropImage.src = '/images/arrow_drop_down.svg'
|
||||||
hide()
|
hide()
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// TODO Remove no-console ignore rule once things are out of rapid dev phase.
|
// TODO Remove no-console ignore rule once things are out of rapid dev phase.
|
||||||
/* eslint no-console: 0 */
|
/* eslint no-console: 0 */
|
||||||
/* global axios, initPagination */
|
/* global axios, initPagination, i18next */
|
||||||
const pageURL = location.pathname
|
const pageURL = location.pathname
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
@@ -127,20 +127,21 @@ function renderCommentCount(commentCount) {
|
|||||||
let displayText = `${commentCount} `
|
let displayText = `${commentCount} `
|
||||||
|
|
||||||
// TODO Let a i18n library handle the following logic.
|
// TODO Let a i18n library handle the following logic.
|
||||||
|
// TODO I18n
|
||||||
switch (commentCount % 100) {
|
switch (commentCount % 100) {
|
||||||
case 1:
|
case 1:
|
||||||
displayText += 'komentar'
|
displayText += i18next.t('komentar')
|
||||||
break
|
break
|
||||||
case 2:
|
case 2:
|
||||||
displayText += 'komentarja'
|
displayText += i18next.t('komentarja')
|
||||||
break
|
break
|
||||||
case 3:
|
case 3:
|
||||||
case 4:
|
case 4:
|
||||||
displayText += 'komentarji'
|
displayText += i18next.t('komentarji')
|
||||||
break
|
break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
displayText += 'komentarjev'
|
displayText += i18next.t('komentarjev')
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +310,7 @@ function submitComment() {
|
|||||||
let ctxId = null
|
let ctxId = null
|
||||||
if (ctxData.ctxId !== undefined) ctxId = ctxData.ctxId
|
if (ctxData.ctxId !== undefined) ctxId = ctxData.ctxId
|
||||||
if (!message) {
|
if (!message) {
|
||||||
alert('Vaš komentar je brez vsebine.')
|
alert(i18next.t('Vaš komentar je brez vsebine.'))
|
||||||
} else {
|
} else {
|
||||||
const payload = { message, ctxType, ctxId, quoteId: null }
|
const payload = { message, ctxType, ctxId, quoteId: null }
|
||||||
createComment(payload)
|
createComment(payload)
|
||||||
@@ -326,7 +327,7 @@ function submitCommentReply() {
|
|||||||
let ctxId = null
|
let ctxId = null
|
||||||
if (ctxData.ctxId !== undefined) ctxId = ctxData.ctxId
|
if (ctxData.ctxId !== undefined) ctxId = ctxData.ctxId
|
||||||
if (!message) {
|
if (!message) {
|
||||||
alert('Vaš komentar je brez vsebine.')
|
alert(i18next.t('Vaš komentar je brez vsebine.'))
|
||||||
} else {
|
} else {
|
||||||
const payload = { message, ctxType, ctxId, quoteId }
|
const payload = { message, ctxType, ctxId, quoteId }
|
||||||
createComment(payload)
|
createComment(payload)
|
||||||
@@ -577,11 +578,11 @@ async function onPageChange(newPage) {
|
|||||||
const { page, numberOfAllPages } = await displayComments(newPage)
|
const { page, numberOfAllPages } = await displayComments(newPage)
|
||||||
updatePager(page, numberOfAllPages)
|
updatePager(page, numberOfAllPages)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let message = 'Prišlo je do napake.'
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
if (error.response?.data) {
|
if (error.response?.data) {
|
||||||
message = error.response.data
|
message = error.response.data
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
}
|
}
|
||||||
alert(message)
|
alert(message)
|
||||||
updatePager()
|
updatePager()
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ function routeConsultancy(searchString) {
|
|||||||
} else {
|
} else {
|
||||||
url = new URL(location)
|
url = new URL(location)
|
||||||
|
|
||||||
|
url.searchParams.delete('p')
|
||||||
const sq = document.getElementById('search-query')
|
const sq = document.getElementById('search-query')
|
||||||
if (sq) {
|
if (sq) {
|
||||||
url.searchParams.set('q', sq.value)
|
url.searchParams.set('q', sq.value)
|
||||||
@@ -65,3 +66,14 @@ if (inputMainSearch) {
|
|||||||
|
|
||||||
propagateFunctionalityToASearchButton(searchButton)
|
propagateFunctionalityToASearchButton(searchButton)
|
||||||
propagateFunctionalityToASearchButton(advancedSearchButton)
|
propagateFunctionalityToASearchButton(advancedSearchButton)
|
||||||
|
|
||||||
|
$(document).ready(() => {
|
||||||
|
const focused = $('#description')
|
||||||
|
if (focused) {
|
||||||
|
focused.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.location.pathname === '/svetovanje/iskanje') {
|
||||||
|
// todo
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/* global $, handleDomainsClickForConsultancy, inputFieldsChecker, currentPagePath, axios, nthParent, tooltipListWOL, tooltipTriggerList */
|
/* global $, bootstrap, handleDomainsClickForConsultancy, initPagination, inputFieldsChecker, currentPagePath,
|
||||||
|
axios, nthParent, tooltipListWOL, tooltipTriggerList, removeAllChildNodes, i18next */
|
||||||
|
|
||||||
/*
|
/*
|
||||||
author: Miha Stele, 2022
|
author: Miha Stele, 2022
|
||||||
@@ -9,6 +10,10 @@ let selectedID = -1
|
|||||||
|
|
||||||
// consultancy FORM
|
// consultancy FORM
|
||||||
let offsetMain
|
let offsetMain
|
||||||
|
let OFFSET_PADDING_MASK = 16
|
||||||
|
if (window.location.pathname === '/svetovanje/vprasanje/admin/svetovalci') {
|
||||||
|
OFFSET_PADDING_MASK = 0
|
||||||
|
}
|
||||||
function adjustOffsetBy() {
|
function adjustOffsetBy() {
|
||||||
offsetMain = document.querySelector('#offset-main')
|
offsetMain = document.querySelector('#offset-main')
|
||||||
const fixedTopSection = document.querySelector('#fixed-top-section')
|
const fixedTopSection = document.querySelector('#fixed-top-section')
|
||||||
@@ -22,15 +27,21 @@ function adjustOffsetBy() {
|
|||||||
if (offsetHeaderPadding !== null) offsetMain.style.paddingTop = `0px`
|
if (offsetHeaderPadding !== null) offsetMain.style.paddingTop = `0px`
|
||||||
} else {
|
} else {
|
||||||
for (let i = 0; i < offsetHeader.length; i++) {
|
for (let i = 0; i < offsetHeader.length; i++) {
|
||||||
offsetHeader[i].style.paddingTop = `${referenceHeight}px`
|
offsetHeader[i].style.paddingTop = `${
|
||||||
|
referenceHeight - OFFSET_PADDING_MASK
|
||||||
|
}px`
|
||||||
}
|
}
|
||||||
if (headerPadding !== null) {
|
if (headerPadding !== null) {
|
||||||
const headerPaddingHeight = headerPadding.offsetHeight
|
const headerPaddingHeight = headerPadding.offsetHeight
|
||||||
offsetMain.style.paddingTop = `${headerPaddingHeight}px`
|
offsetMain.style.paddingTop = `${
|
||||||
|
headerPaddingHeight - OFFSET_PADDING_MASK
|
||||||
|
}px`
|
||||||
}
|
}
|
||||||
if (offsetHeaderPadding !== null) {
|
if (offsetHeaderPadding !== null) {
|
||||||
const offsetHeaderHeight = offsetHeaderPadding.offsetHeight
|
const offsetHeaderHeight = offsetHeaderPadding.offsetHeight
|
||||||
offsetMain.style.paddingTop = `${referenceHeight + offsetHeaderHeight}px`
|
offsetMain.style.paddingTop = `${
|
||||||
|
referenceHeight + offsetHeaderHeight - OFFSET_PADDING_MASK
|
||||||
|
}px`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,7 +57,7 @@ window.addEventListener('load', () => {
|
|||||||
window.adminElements = ce
|
window.adminElements = ce
|
||||||
/* copy source admin.js in case of refactoring */
|
/* copy source admin.js in case of refactoring */
|
||||||
|
|
||||||
if (currentPagePath === '/svetovanje/vprasanje/admin/uporabniki') {
|
if (currentPagePath === '/svetovanje/vprasanje/admin/svetovalci') {
|
||||||
offsetMain.addEventListener('click', handleDomainsClickForConsultancy)
|
offsetMain.addEventListener('click', handleDomainsClickForConsultancy)
|
||||||
ce.name = document.getElementById('name-input')
|
ce.name = document.getElementById('name-input')
|
||||||
if (ce.name) {
|
if (ce.name) {
|
||||||
@@ -108,7 +119,7 @@ try {
|
|||||||
const summernote = $('.summernote')
|
const summernote = $('.summernote')
|
||||||
if (summernote) {
|
if (summernote) {
|
||||||
summernote.summernote({
|
summernote.summernote({
|
||||||
placeholder: 'Na kratko opišite zasnovo in namen slovarja.',
|
placeholder: i18next.t('Na kratko opišite zasnovo in namen slovarja.'),
|
||||||
height: 300,
|
height: 300,
|
||||||
minheight: 150,
|
minheight: 150,
|
||||||
toolbar: [
|
toolbar: [
|
||||||
@@ -383,7 +394,17 @@ if (createAnswerForm) {
|
|||||||
// console.log(data)
|
// console.log(data)
|
||||||
|
|
||||||
if (res.status === 201) {
|
if (res.status === 201) {
|
||||||
window.location.href = '/svetovanje'
|
const infoModal = document.getElementById('begin-response')
|
||||||
|
if (infoModal) {
|
||||||
|
const responseModal = new bootstrap.Modal(infoModal)
|
||||||
|
responseModal.toggle()
|
||||||
|
}
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById('understand-btn')
|
||||||
|
.addEventListener('click', () => {
|
||||||
|
window.location.href = '/svetovanje'
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,7 +428,7 @@ if (insertConsultantForm) {
|
|||||||
})
|
})
|
||||||
.then(result => {
|
.then(result => {
|
||||||
console.log(result)
|
console.log(result)
|
||||||
window.location.href = '/svetovanje/vprasanje/admin/uporabniki'
|
window.location.href = '/svetovanje/vprasanje/admin/svetovalci'
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -421,6 +442,243 @@ try {
|
|||||||
console.log(e)
|
console.log(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Frontend Pagination (as in other pages in this project)
|
||||||
|
|
||||||
|
function onResultClick(e) {
|
||||||
|
const entryEl = e.target.closest(`#${this.id} a.rl`)
|
||||||
|
if (!entryEl) return
|
||||||
|
|
||||||
|
const anchorId = entryEl.querySelector('.anchor').id
|
||||||
|
const anchoredUrl = new URL(location)
|
||||||
|
anchoredUrl.hash = anchorId
|
||||||
|
history.replaceState(null, '', anchoredUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
const resultsListEl = document.getElementById('results-data')
|
||||||
|
if (resultsListEl) {
|
||||||
|
resultsListEl.addEventListener('click', onResultClick)
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialPage = +new URL(location).searchParams.get('p') || 1
|
||||||
|
|
||||||
|
let updatePager
|
||||||
|
try {
|
||||||
|
updatePager = initPagination('pagination', onPageChange, initialPage)
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
async function onPageChange(newPage) {
|
||||||
|
const receivedPageNumber = await changePage(newPage)
|
||||||
|
updateUrlAndHistory(receivedPageNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function changePage(newPage) {
|
||||||
|
try {
|
||||||
|
const res = await getDataForPage(newPage)
|
||||||
|
|
||||||
|
const page = +res.headers.page
|
||||||
|
const numberOfAllPages = +res.headers['number-of-all-pages']
|
||||||
|
const resultsMarkup = res.data
|
||||||
|
|
||||||
|
removeAllChildNodes(resultsListEl)
|
||||||
|
renderResults(resultsMarkup)
|
||||||
|
updatePager(page, numberOfAllPages)
|
||||||
|
|
||||||
|
return page
|
||||||
|
} catch (error) {
|
||||||
|
// console.log(error)
|
||||||
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
|
if (error.response?.data) {
|
||||||
|
message = error.response.data
|
||||||
|
} else if (error.request) {
|
||||||
|
return // return to bypass error caused (assumed) by popstate on iOS/MacOS
|
||||||
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(message)
|
||||||
|
updatePager()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDataForPage(page) {
|
||||||
|
const qParams = new URL(location).searchParams
|
||||||
|
qParams.set('p', page)
|
||||||
|
|
||||||
|
// const url = `/api/v1/search/main?${qParams}`
|
||||||
|
|
||||||
|
// let url = `/api/v1/consultancy/entry-pagination?${qParams}`
|
||||||
|
// URL PATTERN PARSING
|
||||||
|
const PATH = window.location.pathname
|
||||||
|
if (PATH.includes('/admin/')) {
|
||||||
|
// todo ADMIN URL handling
|
||||||
|
qParams.set('isAdmin', 'true')
|
||||||
|
// url = `/api/v1/consultancy/entry-pagination?${qParams}` // required due to updated queryParams
|
||||||
|
|
||||||
|
if (PATH.includes('/novo')) {
|
||||||
|
qParams.set('type', 'new')
|
||||||
|
} else if (PATH.includes('/pripravljeno')) {
|
||||||
|
qParams.set('type', 'review')
|
||||||
|
} else if (PATH.includes('/zavrnjeno')) {
|
||||||
|
qParams.set('type', 'rejected')
|
||||||
|
} else if (PATH.includes('/v-delu')) {
|
||||||
|
qParams.set('type', 'in progress')
|
||||||
|
} else if (PATH.includes('/objavljeno')) {
|
||||||
|
qParams.set('type', 'published')
|
||||||
|
} else {
|
||||||
|
// Invalid case
|
||||||
|
console.log('INVALID CASE, CHECK URL')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// const url = `/api/v1/search/main?${qParams}`
|
||||||
|
|
||||||
|
const url = `/api/v1/consultancy/entry-pagination?${qParams}`
|
||||||
|
console.log(url)
|
||||||
|
|
||||||
|
return await axios.get(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResults(resultsMarkup) {
|
||||||
|
const wrapper = document.createElement('div')
|
||||||
|
wrapper.innerHTML = resultsMarkup
|
||||||
|
resultsListEl.appendChild(wrapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateUrlAndHistory(page) {
|
||||||
|
const newUrl = new URL(location)
|
||||||
|
newUrl.searchParams.set('p', page)
|
||||||
|
history.pushState(null, '', newUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('popstate', () => {
|
||||||
|
const page = +new URL(location).searchParams.get('p') || 1
|
||||||
|
changePage(page)
|
||||||
|
})
|
||||||
|
|
||||||
|
/// // PAGINATION (backend only) /////
|
||||||
|
|
||||||
|
/* const updatePager = initPaginationBE('pagination', onPageChangeBE)
|
||||||
|
|
||||||
|
function onPageChangeBE(newPage) {
|
||||||
|
const newUrl = new URL(location)
|
||||||
|
newUrl.searchParams.set('p', newPage)
|
||||||
|
history.pushState(null, '', newUrl)
|
||||||
|
window.location.href = newUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
function initPaginationBE(paginationRootElId, onPageChange, currentPage = 1) {
|
||||||
|
const rootEl = document.getElementById(paginationRootElId)
|
||||||
|
const btnFirstPage = rootEl.querySelector('.first-page')
|
||||||
|
const btnPreviousPage = rootEl.querySelector('.previous-page')
|
||||||
|
const btnNextPage = rootEl.querySelector('.next-page')
|
||||||
|
const btnLastPage = rootEl.querySelector('.last-page')
|
||||||
|
const formEl = rootEl.querySelector('form')
|
||||||
|
const pageInputEl = formEl.querySelector('input')
|
||||||
|
const pagesCountDisplayEl = formEl.querySelector('.pages-total')
|
||||||
|
|
||||||
|
let reqLock = false
|
||||||
|
|
||||||
|
rootEl.addEventListener('click', handleButtonClick)
|
||||||
|
formEl.addEventListener('submit', handleFormSubmit)
|
||||||
|
|
||||||
|
function handleButtonClick({ target }) {
|
||||||
|
if (reqLock) return
|
||||||
|
|
||||||
|
const buttonEl = target.closest(`#${paginationRootElId} button`)
|
||||||
|
if (!buttonEl) return
|
||||||
|
const numOfAllPages = +pagesCountDisplayEl.textContent
|
||||||
|
|
||||||
|
if (buttonEl.classList.contains('first-page')) {
|
||||||
|
if (currentPage === 1) return
|
||||||
|
enableLock()
|
||||||
|
onPageChange(1)
|
||||||
|
} else if (buttonEl.classList.contains('previous-page')) {
|
||||||
|
if (currentPage === 1) return
|
||||||
|
enableLock()
|
||||||
|
onPageChange(currentPage - 1)
|
||||||
|
} else if (buttonEl.classList.contains('next-page')) {
|
||||||
|
if (currentPage === numOfAllPages) return
|
||||||
|
enableLock()
|
||||||
|
onPageChange(currentPage + 1)
|
||||||
|
} else if (buttonEl.classList.contains('last-page')) {
|
||||||
|
if (currentPage === numOfAllPages) return
|
||||||
|
enableLock()
|
||||||
|
onPageChange(numOfAllPages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFormSubmit(e) {
|
||||||
|
e.preventDefault()
|
||||||
|
if (reqLock) return
|
||||||
|
|
||||||
|
const inputValue = +pageInputEl.value
|
||||||
|
if (!(inputValue > 0 && inputValue <= pagesCountDisplayEl.textContent)) {
|
||||||
|
alert('Nepravilna vrednost strani')
|
||||||
|
pageInputEl.value = currentPage
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
enableLock()
|
||||||
|
onPageChange(inputValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
function enableLock() {
|
||||||
|
reqLock = true
|
||||||
|
btnFirstPage.disabled = true
|
||||||
|
btnPreviousPage.disabled = true
|
||||||
|
btnNextPage.disabled = true
|
||||||
|
btnLastPage.disabled = true
|
||||||
|
pageInputEl.disabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function disableLock() {
|
||||||
|
reqLock = false
|
||||||
|
btnFirstPage.disabled = false
|
||||||
|
btnPreviousPage.disabled = false
|
||||||
|
btnNextPage.disabled = false
|
||||||
|
btnLastPage.disabled = false
|
||||||
|
pageInputEl.disabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePagerUi(newCurrentPage, newNumOfAllPages) {
|
||||||
|
disableLock()
|
||||||
|
if (!newCurrentPage) return
|
||||||
|
|
||||||
|
currentPage = newCurrentPage
|
||||||
|
pageInputEl.value = newCurrentPage
|
||||||
|
pagesCountDisplayEl.textContent = newNumOfAllPages
|
||||||
|
|
||||||
|
if (newCurrentPage === 1) {
|
||||||
|
btnFirstPage.disabled = true
|
||||||
|
btnPreviousPage.disabled = true
|
||||||
|
} else {
|
||||||
|
btnFirstPage.disabled = false
|
||||||
|
btnPreviousPage.disabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newCurrentPage === newNumOfAllPages) {
|
||||||
|
btnNextPage.disabled = true
|
||||||
|
btnLastPage.disabled = true
|
||||||
|
} else {
|
||||||
|
btnNextPage.disabled = false
|
||||||
|
btnLastPage.disabled = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePagerUi(
|
||||||
|
+new URL(location).searchParams.get('p') || 1,
|
||||||
|
+document.querySelector('.pages-total').innerHTML
|
||||||
|
)
|
||||||
|
|
||||||
|
return updatePagerUi
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('popstate', () => {
|
||||||
|
const page = +new URL(location).searchParams.get('p') || 1
|
||||||
|
onPageChangeBE(page)
|
||||||
|
})
|
||||||
|
*/
|
||||||
|
/// ///////////////////
|
||||||
|
|
||||||
/*
|
/*
|
||||||
if (tooltipList) {
|
if (tooltipList) {
|
||||||
console.log(tooltipList)
|
console.log(tooltipList)
|
||||||
@@ -438,3 +696,10 @@ $(document).ready(() => {
|
|||||||
focused.focus()
|
focused.focus()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const askBackButton = document.querySelector('#cancel-cons-btn')
|
||||||
|
if (askBackButton) {
|
||||||
|
askBackButton.addEventListener('click', () => {
|
||||||
|
history.back()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
/* global axios */
|
|
||||||
|
|
||||||
// Priporočam uporabo block scopa okoli paginacijske logike za čimvečjo izolacijo.
|
|
||||||
{
|
|
||||||
// Referenca na element, kamor se izrisuje seznam rezultatov.
|
|
||||||
const resultsListEl = document.getElementById('page-results')
|
|
||||||
|
|
||||||
// Paginacijo inicializiraš s klicem funkcije initPagination:
|
|
||||||
// 1. parameter: id pager elementa. V demo-paginacija.pug je to #pagination.
|
|
||||||
// 2. parameter: callback funkcijo, ki jo pager pokliče vsakič, ko uporabnik zahteva novo stran. Poliče jo s številko zahtevane strani.
|
|
||||||
// vrnjena vrednost: funkcija, ki jo (kasneje) kličeš za posodobitev pagerja. Tu jo poimenujem updateDemoPager.
|
|
||||||
const updateDemoPager = initPagination('pagination', onPageChange)
|
|
||||||
|
|
||||||
// Fukncija, prejme številko nove strani in naj:
|
|
||||||
// 1. Pridobi podatke nove strani.
|
|
||||||
// 2. Izriše seznam elementov te strani.
|
|
||||||
// 3. Posodobi pager, tako, da kliče funkcijo, ki jo je vrnil klic initPagination (updateDemoPager) z novo stranjo in številom vseh strani.
|
|
||||||
async function onPageChange(newPage) {
|
|
||||||
try {
|
|
||||||
const { page, numberOfAllPages, results } = await getDataForPage(newPage)
|
|
||||||
removeAllChildNodes(resultsListEl)
|
|
||||||
renderResults(results)
|
|
||||||
updateDemoPager(page, numberOfAllPages)
|
|
||||||
} catch (error) {
|
|
||||||
let message = 'Prišlo je do napake.'
|
|
||||||
if (error.response?.data) {
|
|
||||||
message = error.response.data
|
|
||||||
} else if (error.request) {
|
|
||||||
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
|
|
||||||
}
|
|
||||||
alert(message)
|
|
||||||
updateDemoPager()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Primer helper funkcije za pridobitev podatkov želene strani.
|
|
||||||
async function getDataForPage(page) {
|
|
||||||
const url = `/api/v1/demo-paginacija/list?p=${page}`
|
|
||||||
const { data } = await axios.get(url)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
// Primer helper funkcije za izris seznama novih podatkov.
|
|
||||||
function renderResults(results) {
|
|
||||||
results.forEach(result => {
|
|
||||||
const newListEl = document.createElement('li')
|
|
||||||
const textNode1 = document.createTextNode('Zanimiva vrednost: ')
|
|
||||||
const boldedEl = document.createElement('b')
|
|
||||||
boldedEl.textContent = result.zanimivo
|
|
||||||
const textNode2 = document.createTextNode(
|
|
||||||
`. Totalno nezanimivo: ${result.nezanimivo1} in ${result.nezanimivo2}`
|
|
||||||
)
|
|
||||||
|
|
||||||
newListEl.append(textNode1, boldedEl, textNode2)
|
|
||||||
resultsListEl.appendChild(newListEl)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** ****************************************************************************************************************************************** **\
|
|
||||||
* Koda od tu navzdol za vaju ni relevantna. Tu je, da dela zgornja koda. Za realno uporabo sem je skopiral tudi v public/javascripts/scripts.js *
|
|
||||||
* Na vsaki strani, kjer bo paginacija, jo inicializiraj in uporabljaj po zgledu zgornje kode. *
|
|
||||||
\** ****************************************************************************************************************************************** **/
|
|
||||||
|
|
||||||
function initPagination(paginationRootElId, onPageChange, currentPage = 1) {
|
|
||||||
const rootEl = document.getElementById(paginationRootElId)
|
|
||||||
const btnFirstPage = rootEl.querySelector('.first-page')
|
|
||||||
const btnPreviousPage = rootEl.querySelector('.previous-page')
|
|
||||||
const btnNextPage = rootEl.querySelector('.next-page')
|
|
||||||
const btnLastPage = rootEl.querySelector('.last-page')
|
|
||||||
const formEl = rootEl.querySelector('form')
|
|
||||||
const pageInputEl = formEl.querySelector('input')
|
|
||||||
const pagesCountDisplayEl = formEl.querySelector('.pages-total')
|
|
||||||
|
|
||||||
let reqLock = false
|
|
||||||
|
|
||||||
rootEl.addEventListener('click', handleButtonClick)
|
|
||||||
formEl.addEventListener('submit', handleFormSubmit)
|
|
||||||
|
|
||||||
function handleButtonClick({ target }) {
|
|
||||||
if (reqLock) return
|
|
||||||
|
|
||||||
const buttonEl = target.closest(`#${paginationRootElId} button`)
|
|
||||||
if (!buttonEl) return
|
|
||||||
const numOfAllPages = +pagesCountDisplayEl.textContent
|
|
||||||
|
|
||||||
if (buttonEl.classList.contains('first-page')) {
|
|
||||||
if (currentPage === 1) return
|
|
||||||
enableLock()
|
|
||||||
onPageChange(1)
|
|
||||||
} else if (buttonEl.classList.contains('previous-page')) {
|
|
||||||
if (currentPage === 1) return
|
|
||||||
enableLock()
|
|
||||||
onPageChange(currentPage - 1)
|
|
||||||
} else if (buttonEl.classList.contains('next-page')) {
|
|
||||||
if (currentPage === numOfAllPages) return
|
|
||||||
enableLock()
|
|
||||||
onPageChange(currentPage + 1)
|
|
||||||
} else if (buttonEl.classList.contains('last-page')) {
|
|
||||||
if (currentPage === numOfAllPages) return
|
|
||||||
enableLock()
|
|
||||||
onPageChange(numOfAllPages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFormSubmit(e) {
|
|
||||||
e.preventDefault()
|
|
||||||
if (reqLock) return
|
|
||||||
|
|
||||||
const inputValue = +pageInputEl.value
|
|
||||||
if (!(inputValue > 0 && inputValue <= pagesCountDisplayEl.textContent)) {
|
|
||||||
alert('Nepravilna vrednost strani')
|
|
||||||
pageInputEl.value = currentPage
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
enableLock()
|
|
||||||
onPageChange(inputValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
function enableLock() {
|
|
||||||
reqLock = true
|
|
||||||
btnFirstPage.disabled = true
|
|
||||||
btnPreviousPage.disabled = true
|
|
||||||
btnNextPage.disabled = true
|
|
||||||
btnLastPage.disabled = true
|
|
||||||
pageInputEl.disabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function disableLock() {
|
|
||||||
reqLock = false
|
|
||||||
btnFirstPage.disabled = false
|
|
||||||
btnPreviousPage.disabled = false
|
|
||||||
btnNextPage.disabled = false
|
|
||||||
btnLastPage.disabled = false
|
|
||||||
pageInputEl.disabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePagerUi(newCurrentPage, newNumOfAllPages) {
|
|
||||||
disableLock()
|
|
||||||
if (!newCurrentPage) return
|
|
||||||
|
|
||||||
currentPage = newCurrentPage
|
|
||||||
pageInputEl.value = newCurrentPage
|
|
||||||
pagesCountDisplayEl.textContent = newNumOfAllPages
|
|
||||||
|
|
||||||
if (newCurrentPage === 1) {
|
|
||||||
btnFirstPage.disabled = true
|
|
||||||
btnPreviousPage.disabled = true
|
|
||||||
} else {
|
|
||||||
btnFirstPage.disabled = false
|
|
||||||
btnPreviousPage.disabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newCurrentPage === newNumOfAllPages) {
|
|
||||||
btnNextPage.disabled = true
|
|
||||||
btnLastPage.disabled = true
|
|
||||||
} else {
|
|
||||||
btnNextPage.disabled = false
|
|
||||||
btnLastPage.disabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatePagerUi
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to easily remove all child nodes. Useful for pagination.
|
|
||||||
function removeAllChildNodes(parent) {
|
|
||||||
while (parent.firstChild) {
|
|
||||||
parent.removeChild(parent.firstChild)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
/* global $, axios, bootstrap, currentPagePath, initPagination, removeAllChildNodes, unsavedData */
|
/* global $, axios, bootstrap, currentPagePath, initPagination, removeAllChildNodes, unsavedData, replaceContainer, i18next */
|
||||||
|
|
||||||
// Temporary workaround (use of currentPagePath).
|
// Temporary workaround (use of currentPagePath).
|
||||||
|
|
||||||
@@ -6,6 +6,8 @@ window.addEventListener('load', () => {
|
|||||||
initDictionaries()
|
initDictionaries()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
let queryBattery = ''
|
||||||
|
|
||||||
function initDictionaries() {
|
function initDictionaries() {
|
||||||
const ce = {}
|
const ce = {}
|
||||||
window.dictionaryElements = ce
|
window.dictionaryElements = ce
|
||||||
@@ -166,7 +168,7 @@ function initDictionaries() {
|
|||||||
if (ce.fileUploadInput.files.item(0) !== null)
|
if (ce.fileUploadInput.files.item(0) !== null)
|
||||||
chosenFile.textContent = ce.fileUploadInput.files.item(0).name
|
chosenFile.textContent = ce.fileUploadInput.files.item(0).name
|
||||||
else {
|
else {
|
||||||
chosenFile.textContent = 'Izberi datoteko'
|
chosenFile.textContent = i18next.t('Izberi datoteko')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -185,12 +187,140 @@ function initDictionaries() {
|
|||||||
|
|
||||||
await axios.post(event.target.action, payload)
|
await axios.post(event.target.action, payload)
|
||||||
|
|
||||||
alert('SLOVAR UVOŽEN')
|
alert(i18next.t('SLOVAR UVOŽEN'))
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
alert('NAPAKA V UVOZU')
|
alert(i18next.t('NAPAKA V UVOZU'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const resultsListEl = document.getElementById('page-results')
|
||||||
|
const dictionaryId = document.getElementById('dictionary-id').value
|
||||||
|
|
||||||
|
const updatePager = initPagination('pagination', onPageChange)
|
||||||
|
|
||||||
|
async function onPageChange(newPage) {
|
||||||
|
try {
|
||||||
|
const { page, numberOfAllPages, results } = await getDataForPage(
|
||||||
|
newPage
|
||||||
|
)
|
||||||
|
removeAllChildNodes(resultsListEl)
|
||||||
|
renderResults(results)
|
||||||
|
updatePager(page, numberOfAllPages)
|
||||||
|
} catch (error) {
|
||||||
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
|
if (error.response?.data) {
|
||||||
|
message = error.response.data
|
||||||
|
} else if (error.request) {
|
||||||
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
|
}
|
||||||
|
alert(message)
|
||||||
|
updatePager()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDataForPage(page) {
|
||||||
|
const url = `/api/v1/dictionaries/${dictionaryId}/showImportFromFileForm?p=${page}`
|
||||||
|
const { data } = await axios.get(url)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResults(results) {
|
||||||
|
const localeOptions = {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}
|
||||||
|
results.forEach(result => {
|
||||||
|
const rowEl = document.createElement('tr')
|
||||||
|
const td1 = document.createElement('td')
|
||||||
|
const td2 = document.createElement('td')
|
||||||
|
const td3 = document.createElement('td')
|
||||||
|
const td4 = document.createElement('td')
|
||||||
|
const date = new Date(result.time_started).toLocaleDateString(
|
||||||
|
'sl-SL',
|
||||||
|
localeOptions
|
||||||
|
)
|
||||||
|
td1.textContent = date
|
||||||
|
td2.textContent = result.file_format
|
||||||
|
td3.textContent = result.count_valid_entries
|
||||||
|
td4.textContent = result.status
|
||||||
|
rowEl.append(td1, td2, td3, td4)
|
||||||
|
resultsListEl.appendChild(rowEl)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (/\/slovarji\/\d+\/izvoz/.test(currentPagePath)) {
|
||||||
|
const resultsListEl = document.getElementById('page-results')
|
||||||
|
const dictionaryId = document.getElementById('dictionary-id').value
|
||||||
|
|
||||||
|
const updatePager = initPagination('pagination', onPageChange)
|
||||||
|
|
||||||
|
async function onPageChange(newPage) {
|
||||||
|
try {
|
||||||
|
const { page, numberOfAllPages, results } = await getDataForPage(
|
||||||
|
newPage
|
||||||
|
)
|
||||||
|
removeAllChildNodes(resultsListEl)
|
||||||
|
renderResults(results)
|
||||||
|
updatePager(page, numberOfAllPages)
|
||||||
|
} catch (error) {
|
||||||
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
|
if (error.response?.data) {
|
||||||
|
message = error.response.data
|
||||||
|
} else if (error.request) {
|
||||||
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
|
}
|
||||||
|
alert(message)
|
||||||
|
updatePager()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDataForPage(page) {
|
||||||
|
const url = `/api/v1/dictionaries/${dictionaryId}/showExportToFileForm?p=${page}`
|
||||||
|
const { data } = await axios.get(url)
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResults(results) {
|
||||||
|
const localeOptions = {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
}
|
||||||
|
results.forEach(result => {
|
||||||
|
const rowEl = document.createElement('tr')
|
||||||
|
const td1 = document.createElement('td')
|
||||||
|
const td2 = document.createElement('td')
|
||||||
|
const td3 = document.createElement('td')
|
||||||
|
const td4 = document.createElement('td')
|
||||||
|
const date = new Date(result.time_created).toLocaleDateString(
|
||||||
|
'sl-SL',
|
||||||
|
localeOptions
|
||||||
|
)
|
||||||
|
td1.textContent = date
|
||||||
|
td2.textContent = result.export_file_format
|
||||||
|
td3.textContent = result.entry_count
|
||||||
|
if (result.status === 'finished') {
|
||||||
|
const aEl = document.createElement('a')
|
||||||
|
aEl.href = `/slovarji/export-download/${result.id}`
|
||||||
|
aEl.textContent = i18next.t('Shrani')
|
||||||
|
aEl.className = 'btn btn-primary'
|
||||||
|
td4.append(aEl)
|
||||||
|
} else td4.textContent = ''
|
||||||
|
|
||||||
|
rowEl.append(td1, td2, td3, td4)
|
||||||
|
resultsListEl.appendChild(rowEl)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (/\/slovarji\/\d+\/vsebina/.test(currentPagePath)) {
|
if (/\/slovarji\/\d+\/vsebina/.test(currentPagePath)) {
|
||||||
ce.formEditContent = document.getElementById('form-edit-content')
|
ce.formEditContent = document.getElementById('form-edit-content')
|
||||||
ce.formEditContent.addEventListener('submit', updateEntry)
|
ce.formEditContent.addEventListener('submit', updateEntry)
|
||||||
@@ -233,6 +363,7 @@ function initDictionaries() {
|
|||||||
ce.clearSearchInput = document.querySelector('.clear-search-input')
|
ce.clearSearchInput = document.querySelector('.clear-search-input')
|
||||||
ce.collapsablePart = document.querySelectorAll('.collapsable-entry-part')
|
ce.collapsablePart = document.querySelectorAll('.collapsable-entry-part')
|
||||||
ce.scrollEl = document.querySelector('.content-nav-content')
|
ce.scrollEl = document.querySelector('.content-nav-content')
|
||||||
|
$('.multiple').on('select2:select', enableButton)
|
||||||
ce.collapsablePart.forEach(el => {
|
ce.collapsablePart.forEach(el => {
|
||||||
el.addEventListener('hide.bs.collapse', () =>
|
el.addEventListener('hide.bs.collapse', () =>
|
||||||
changePageContent('collapse-hidden')
|
changePageContent('collapse-hidden')
|
||||||
@@ -257,7 +388,7 @@ function initDictionaries() {
|
|||||||
})
|
})
|
||||||
ce.statusEditEl = document.getElementById('status-edit')
|
ce.statusEditEl = document.getElementById('status-edit')
|
||||||
// ce.deleteEntryBtn.addEventListener('click', () => ajaxDeleteEntry())
|
// ce.deleteEntryBtn.addEventListener('click', () => ajaxDeleteEntry())
|
||||||
ce.newEntryBtn.addEventListener('click', () => {
|
ce.newEntryBtn.addEventListener('click', e => {
|
||||||
ce.formEditContent.reset()
|
ce.formEditContent.reset()
|
||||||
changePageContent('new-entry-btn')
|
changePageContent('new-entry-btn')
|
||||||
})
|
})
|
||||||
@@ -307,7 +438,8 @@ function initDictionaries() {
|
|||||||
if (mcBtnsGrp.length) {
|
if (mcBtnsGrp.length) {
|
||||||
mcBtnsGrp.forEach(e => e.addEventListener('click', enableButton))
|
mcBtnsGrp.forEach(e => e.addEventListener('click', enableButton))
|
||||||
}
|
}
|
||||||
ce.newHeadwordGroupBtn.addEventListener('click', createHeadword)
|
if (ce.newHeadwordGroupBtn)
|
||||||
|
ce.newHeadwordGroupBtn.addEventListener('click', createHeadword)
|
||||||
if (ce.newConnectionBtn)
|
if (ce.newConnectionBtn)
|
||||||
ce.newConnectionBtn.addEventListener('click', () =>
|
ce.newConnectionBtn.addEventListener('click', () =>
|
||||||
addConnectionField(ce.formEditContent, ce.newConnectionBtn)
|
addConnectionField(ce.formEditContent, ce.newConnectionBtn)
|
||||||
@@ -408,7 +540,7 @@ function initDictionaries() {
|
|||||||
|
|
||||||
if (termListMenu.childElementCount > 1) {
|
if (termListMenu.childElementCount > 1) {
|
||||||
const termListLabels = termListMenu.querySelectorAll('label')
|
const termListLabels = termListMenu.querySelectorAll('label')
|
||||||
termListLabels[0].click()
|
if (termListLabels.length) termListLabels[0].click()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleClick({ target }) {
|
function handleClick({ target }) {
|
||||||
@@ -423,10 +555,11 @@ function initDictionaries() {
|
|||||||
const okBtnText = modalUseBtn.textContent
|
const okBtnText = modalUseBtn.textContent
|
||||||
const cnclBtnTxt = modalCnclBtn.textContent
|
const cnclBtnTxt = modalCnclBtn.textContent
|
||||||
const modalMainTxt = modalMain.textContent
|
const modalMainTxt = modalMain.textContent
|
||||||
modalUseBtn.textContent = 'Shrani'
|
modalUseBtn.textContent = i18next.t('Shrani')
|
||||||
modalCnclBtn.textContent = 'Ne shrani'
|
modalCnclBtn.textContent = i18next.t('Ne shrani')
|
||||||
modalMain.textContent =
|
modalMain.textContent = i18next.t(
|
||||||
'Imate neshranjene spremembe. Ali jih želite shraniti?'
|
'Imate neshranjene spremembe. Ali jih želite shraniti?'
|
||||||
|
)
|
||||||
const alertModal = new bootstrap.Modal(
|
const alertModal = new bootstrap.Modal(
|
||||||
document.getElementById('alert-modal')
|
document.getElementById('alert-modal')
|
||||||
)
|
)
|
||||||
@@ -478,8 +611,8 @@ function initDictionaries() {
|
|||||||
if (deleteEntryEl) {
|
if (deleteEntryEl) {
|
||||||
const modalUseBtn = document.getElementById('modal-del-btn')
|
const modalUseBtn = document.getElementById('modal-del-btn')
|
||||||
const modalCnclBtn = document.getElementById('cancel-btn')
|
const modalCnclBtn = document.getElementById('cancel-btn')
|
||||||
modalUseBtn.textContent = 'Izbriši'
|
modalUseBtn.textContent = i18next.t('Izbriši')
|
||||||
modalCnclBtn.textContent = 'Ne izbriši'
|
modalCnclBtn.textContent = i18next.t('Ne izbriši')
|
||||||
const alertModal = new bootstrap.Modal(
|
const alertModal = new bootstrap.Modal(
|
||||||
document.getElementById('delete-modal')
|
document.getElementById('delete-modal')
|
||||||
)
|
)
|
||||||
@@ -497,8 +630,8 @@ function initDictionaries() {
|
|||||||
const { data } = await axios.get(
|
const { data } = await axios.get(
|
||||||
`/api/v1/entries/${entryId}/version-snapshots/${versionId}`
|
`/api/v1/entries/${entryId}/version-snapshots/${versionId}`
|
||||||
)
|
)
|
||||||
const payload = { entry: data }
|
const payload = { entry: data.data }
|
||||||
insertTermData(entryId, payload)
|
insertTermData(entryId, payload, data.author)
|
||||||
const isPublishedEl = payload.entry.is_published
|
const isPublishedEl = payload.entry.is_published
|
||||||
const info = new FormData(formEditContent)
|
const info = new FormData(formEditContent)
|
||||||
info.append('isPublishedEl', isPublishedEl)
|
info.append('isPublishedEl', isPublishedEl)
|
||||||
@@ -531,6 +664,7 @@ function initDictionaries() {
|
|||||||
info.append('isPublishedEl', isPublishedEl)
|
info.append('isPublishedEl', isPublishedEl)
|
||||||
loadPreview(info)
|
loadPreview(info)
|
||||||
changeVersionList(data.entry.versions)
|
changeVersionList(data.entry.versions)
|
||||||
|
setLatestVersion(data.entry)
|
||||||
ce.formEditContent.addEventListener('input', () => (unsaved = true))
|
ce.formEditContent.addEventListener('input', () => (unsaved = true))
|
||||||
formEditContent.dataset.entryId = entryId
|
formEditContent.dataset.entryId = entryId
|
||||||
formEditContent.action = '/api/v1/entries/update'
|
formEditContent.action = '/api/v1/entries/update'
|
||||||
@@ -553,7 +687,7 @@ function initDictionaries() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertTermData(entryId, data) {
|
function insertTermData(entryId, data, vAuthor) {
|
||||||
const { newConnectionBtn } = window.dictionaryElements
|
const { newConnectionBtn } = window.dictionaryElements
|
||||||
const localeOptions = {
|
const localeOptions = {
|
||||||
day: '2-digit',
|
day: '2-digit',
|
||||||
@@ -609,12 +743,20 @@ function initDictionaries() {
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
for (let i = 0; i < termIdText.length; i++) {
|
for (let i = 0; i < termIdText.length; i++) {
|
||||||
termIdText[i].textContent = `ID: Ni idja`
|
termIdText[i].textContent = i18next.t('ID: Ni idja')
|
||||||
}
|
}
|
||||||
ce.authorEl.textContent = data.entry.version_author
|
if (vAuthor) {
|
||||||
ce.versionEl.textContent = `Verzija ${data.entry.version}`
|
ce.authorEl.textContent = vAuthor
|
||||||
ce.previewVersion.textContent = `Verzija ${data.entry.version}`
|
ce.previewAuthor.textContent = vAuthor
|
||||||
ce.previewAuthor.textContent = data.entry.version_author
|
} else {
|
||||||
|
ce.authorEl.textContent = data.entry.version_author
|
||||||
|
ce.previewAuthor.textContent = data.entry.version_author
|
||||||
|
}
|
||||||
|
// ce.versionEl.textContent = `Verzija ${data.entry.version}`
|
||||||
|
ce.versionEl.textContent = i18next.t('Verzija') + `${data.entry.version}`
|
||||||
|
// ce.previewVersion.textContent = `Verzija ${data.entry.version}`
|
||||||
|
ce.previewVersion.textContent =
|
||||||
|
i18next.t('Verzija') + `${data.entry.version}`
|
||||||
termName.value = data.entry.term
|
termName.value = data.entry.term
|
||||||
? data.entry.term.replace(/"/g, '"')
|
? data.entry.term.replace(/"/g, '"')
|
||||||
: ''
|
: ''
|
||||||
@@ -703,7 +845,7 @@ function initDictionaries() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (listForeignSynonyms.length) {
|
if (listForeignSynonyms.length) {
|
||||||
if (el.synonym != null)
|
if (el.synonym != null) {
|
||||||
el.synonym.forEach(element => {
|
el.synonym.forEach(element => {
|
||||||
if (element.length) {
|
if (element.length) {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
@@ -719,6 +861,7 @@ function initDictionaries() {
|
|||||||
selectSyn.append(newOption).trigger('change')
|
selectSyn.append(newOption).trigger('change')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -757,18 +900,19 @@ function initDictionaries() {
|
|||||||
const payload = new URLSearchParams(new FormData(ce.formEditContent))
|
const payload = new URLSearchParams(new FormData(ce.formEditContent))
|
||||||
payload.set('entryId', entryId)
|
payload.set('entryId', entryId)
|
||||||
try {
|
try {
|
||||||
|
saveBtn.textContent = i18next.t('Shranjujem ...')
|
||||||
await axios.post(ce.formEditContent.action, payload)
|
await axios.post(ce.formEditContent.action, payload)
|
||||||
spinnerEl.classList.remove('d-none')
|
spinnerEl.classList.remove('d-none')
|
||||||
saveBtn.classList.add('saved-entry-btn')
|
saveBtn.classList.add('saved-entry-btn')
|
||||||
saveBtn.textContent = 'Shranjeno'
|
saveBtn.textContent = i18next.t('Shranjeno')
|
||||||
if (id != null) entryId = id
|
if (id != null) entryId = id
|
||||||
ajaxSideMenu(entryId)
|
ajaxSideMenu(entryId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let message = 'Prišlo je do napake.'
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
message = error.response.data
|
message = error.response.data
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
}
|
}
|
||||||
messageContainer.textContent = message
|
messageContainer.textContent = message
|
||||||
}
|
}
|
||||||
@@ -776,19 +920,20 @@ function initDictionaries() {
|
|||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const payload = new URLSearchParams(new FormData(ce.formEditContent))
|
const payload = new URLSearchParams(new FormData(ce.formEditContent))
|
||||||
try {
|
try {
|
||||||
|
saveBtn.textContent = i18next.t('Shranjujem ...')
|
||||||
const res = await axios.post(ce.formEditContent.action, payload)
|
const res = await axios.post(ce.formEditContent.action, payload)
|
||||||
spinnerEl.classList.remove('d-none')
|
spinnerEl.classList.remove('d-none')
|
||||||
saveBtn.classList.add('saved-entry-btn')
|
saveBtn.classList.add('saved-entry-btn')
|
||||||
saveBtn.textContent = 'Shranjeno'
|
saveBtn.textContent = i18next.t('Shranjeno')
|
||||||
let entryId = res.data.entryId
|
let entryId = res.data.entryId
|
||||||
if (id != null) entryId = id
|
if (id != null) entryId = id
|
||||||
ajaxSideMenu(entryId)
|
ajaxSideMenu(entryId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let message = 'Prišlo je do napake.'
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
message = error.response.data
|
message = error.response.data
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
}
|
}
|
||||||
messageContainer.textContent = message
|
messageContainer.textContent = message
|
||||||
} finally {
|
} finally {
|
||||||
@@ -806,11 +951,11 @@ function initDictionaries() {
|
|||||||
})
|
})
|
||||||
renderTerms(data, entryId)
|
renderTerms(data, entryId)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let message = 'Prišlo je do napake.'
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
message = error.response.data
|
message = error.response.data
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
}
|
}
|
||||||
messageContainer.textContent = message
|
messageContainer.textContent = message
|
||||||
}
|
}
|
||||||
@@ -829,7 +974,7 @@ function initDictionaries() {
|
|||||||
selectNextEntry(selectedIndex)
|
selectNextEntry(selectedIndex)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
const message = 'Prišlo je do napake.'
|
const message = i18next.t('Prišlo je do napake.')
|
||||||
messageContainer.textContent = message
|
messageContainer.textContent = message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -870,21 +1015,27 @@ function initDictionaries() {
|
|||||||
/"/g,
|
/"/g,
|
||||||
'"'
|
'"'
|
||||||
)}`
|
)}`
|
||||||
} else labelElement.textContent = '[ni termina]'
|
} else labelElement.textContent = i18next.t('[ni termina]')
|
||||||
} else labelElement.textContent = '[ni termina]'
|
} else labelElement.textContent = i18next.t('[ni termina]')
|
||||||
} else {
|
} else {
|
||||||
if (el.isValid) {
|
if (el.isValid) {
|
||||||
if (el.isPublished) {
|
if (el.isPublished) {
|
||||||
labelElement.textContent = sloTerm
|
labelElement.textContent = `${sloTerm} ${
|
||||||
|
el.homonymSort ? '(' + el.homonymSort + ')' : ''
|
||||||
|
}`
|
||||||
labelElement.className =
|
labelElement.className =
|
||||||
'terms-label term-good-btn btn p-2 ms-2 me-3 text-truncate justify-content-start d-inline-block'
|
'terms-label term-good-btn btn p-2 ms-2 me-3 text-truncate justify-content-start d-inline-block'
|
||||||
} else {
|
} else {
|
||||||
labelElement.className =
|
labelElement.className =
|
||||||
'terms-label btn p-2 ms-2 me-3 text-truncate justify-content-start d-inline-block'
|
'terms-label btn p-2 ms-2 me-3 text-truncate justify-content-start d-inline-block'
|
||||||
labelElement.textContent = sloTerm
|
labelElement.textContent = `${sloTerm} ${
|
||||||
|
el.homonymSort ? '(' + el.homonymSort + ')' : ''
|
||||||
|
}`
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
labelElement.textContent = sloTerm
|
labelElement.textContent = `${sloTerm} ${
|
||||||
|
el.homonymSort ? '(' + el.homonymSort + ')' : ''
|
||||||
|
}`
|
||||||
labelElement.className =
|
labelElement.className =
|
||||||
'terms-label not-valid-not-published btn p-2 ms-2 me-3 text-truncate justify-content-start d-inline-block'
|
'terms-label not-valid-not-published btn p-2 ms-2 me-3 text-truncate justify-content-start d-inline-block'
|
||||||
}
|
}
|
||||||
@@ -949,84 +1100,83 @@ function initDictionaries() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
const resultsListEl = document.getElementById('page-results')
|
||||||
const resultsListEl = document.getElementById('page-results')
|
const dictionaryId = document.getElementById('subareas-dict-id').value
|
||||||
const dictionaryId = document.getElementById('subareas-dict-id').value
|
|
||||||
|
|
||||||
const updatePager = initPagination('pagination', onPageChange)
|
const updatePager = initPagination('pagination', onPageChange)
|
||||||
|
|
||||||
async function onPageChange(newPage) {
|
async function onPageChange(newPage) {
|
||||||
try {
|
try {
|
||||||
const { page, numberOfAllPages, results } = await getDataForPage(
|
const results = await getDataForPage(newPage)
|
||||||
newPage
|
|
||||||
)
|
const numberOfAllPages = +results.headers['number-of-all-pages']
|
||||||
removeAllChildNodes(resultsListEl)
|
const page = +results.headers.page
|
||||||
renderResults(results)
|
removeAllChildNodes(resultsListEl)
|
||||||
updatePager(page, numberOfAllPages)
|
renderResults(results.data)
|
||||||
} catch (error) {
|
updatePager(page, numberOfAllPages)
|
||||||
let message = 'Prišlo je do napake.'
|
} catch (error) {
|
||||||
if (error.response?.data) {
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
message = error.response.data
|
if (error.response?.data) {
|
||||||
} else if (error.request) {
|
message = error.response.data
|
||||||
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
|
} else if (error.request) {
|
||||||
}
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
alert(message)
|
|
||||||
updatePager()
|
|
||||||
}
|
}
|
||||||
}
|
alert(message)
|
||||||
|
updatePager()
|
||||||
async function getDataForPage(page) {
|
|
||||||
const url = `/api/v1/dictionaries/${dictionaryId}/listDomainLabels?p=${page}`
|
|
||||||
const { data } = await axios.get(url)
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderResults(results) {
|
|
||||||
results.forEach(result => {
|
|
||||||
const rowEl = document.createElement('tr')
|
|
||||||
const input = document.createElement('input')
|
|
||||||
const th = document.createElement('th')
|
|
||||||
const input2 = document.createElement('input')
|
|
||||||
const td1 = document.createElement('td')
|
|
||||||
const td2 = document.createElement('td')
|
|
||||||
const div = document.createElement('div')
|
|
||||||
const editBtn = document.createElement('button')
|
|
||||||
const deleteBtn = document.createElement('button')
|
|
||||||
const imgEditEl = document.createElement('img')
|
|
||||||
const imgDelEl = document.createElement('img')
|
|
||||||
input.value = result.id
|
|
||||||
input.type = 'hidden'
|
|
||||||
input.name = 'domainLabelId'
|
|
||||||
th.scope = 'row'
|
|
||||||
input2.className = 'form-check checkbox-table'
|
|
||||||
input2.type = 'checkbox'
|
|
||||||
input2.name = 'isVisible'
|
|
||||||
input2.disabled = true
|
|
||||||
input2.checked = !!result.isVisible
|
|
||||||
th.append(input2)
|
|
||||||
td1.className = 'tdata-area'
|
|
||||||
td1.textContent = result.name
|
|
||||||
|
|
||||||
td2.classList.add('buttons-group')
|
|
||||||
div.classList.add('table-buttons')
|
|
||||||
editBtn.className = 'p-0 table-button-grp me-3 edit-row-btn'
|
|
||||||
editBtn.type = 'button'
|
|
||||||
imgEditEl.src = '/images/u_edit-alt.svg'
|
|
||||||
deleteBtn.className = 'p-0 table-button-grp delete-row-btn'
|
|
||||||
deleteBtn.dataset.bsTarget = '#alert-modal'
|
|
||||||
deleteBtn.dataset.bsToggle = 'modal'
|
|
||||||
deleteBtn.type = 'button'
|
|
||||||
imgDelEl.src = '/images/red-trash-icon.svg'
|
|
||||||
td2.append(div)
|
|
||||||
div.append(editBtn, deleteBtn)
|
|
||||||
editBtn.append(imgEditEl)
|
|
||||||
deleteBtn.append(imgDelEl)
|
|
||||||
|
|
||||||
rowEl.append(input, th, td1, td2)
|
|
||||||
resultsListEl.appendChild(rowEl)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getDataForPage(page) {
|
||||||
|
// const url = `/api/v1/dictionaries/${dictionaryId}/listDomainLabels?p=${page}`
|
||||||
|
const url = `/api/v1/dictionaries/${dictionaryId}/domainLabels?q=${queryBattery}&p=${page}`
|
||||||
|
// const { data } = await axios.get(url)
|
||||||
|
return await axios.get(url)
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderResults(results) {
|
||||||
|
replaceContainer('page-results', results)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatePaginationOnFilter = axiosResult => {
|
||||||
|
const numberOfAllPages = +axiosResult.headers['number-of-all-pages']
|
||||||
|
const page = +axiosResult.headers.page
|
||||||
|
// removeAllChildNodes(resultsListEl)
|
||||||
|
// renderResults(results.data)
|
||||||
|
// console.log(numberOfAllPages)
|
||||||
|
// console.log(page)
|
||||||
|
// console.log(updatePager)
|
||||||
|
updatePager(page, numberOfAllPages)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// / Due to unsual design, the function was moved inside
|
||||||
|
function searchController() {
|
||||||
|
if (window.location.pathname.includes('podrocne-oznake')) {
|
||||||
|
queryBattery = document.getElementById('input-search').value
|
||||||
|
// console.log(queryBattery)
|
||||||
|
axios
|
||||||
|
.get(
|
||||||
|
`/api/v1/dictionaries/${
|
||||||
|
window.location.pathname.split('/')[2]
|
||||||
|
}/domainLabels?q=${queryBattery}&p=${1}`
|
||||||
|
)
|
||||||
|
.then(result => {
|
||||||
|
updatePaginationOnFilter(result)
|
||||||
|
replaceContainer('page-results', result.data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inlineSearchButton = document.getElementById('inline-search-btn')
|
||||||
|
|
||||||
|
if (inlineSearchButton) {
|
||||||
|
inlineSearchButton.addEventListener('click', searchController)
|
||||||
|
$('#input-search').on('keyup', function (e) {
|
||||||
|
if (e.code === 'Enter' || e.code === 'NumpadEnter') {
|
||||||
|
searchController()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/// //
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/\/slovarji\/\d+\/napredno/.test(currentPagePath)) {
|
if (/\/slovarji\/\d+\/napredno/.test(currentPagePath)) {
|
||||||
@@ -1217,8 +1367,8 @@ function mobileMoveContent() {
|
|||||||
currentPagePath.includes('slovarji') &&
|
currentPagePath.includes('slovarji') &&
|
||||||
!currentPagePath.includes('admin')
|
!currentPagePath.includes('admin')
|
||||||
)
|
)
|
||||||
navTitle.textContent = 'Urejanje'
|
navTitle.textContent = i18next.t('Urejanje')
|
||||||
else navTitle.textContent = 'Administrator'
|
else navTitle.textContent = i18next.t('Administracija')
|
||||||
siteHeading.style.display = 'block'
|
siteHeading.style.display = 'block'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1310,9 +1460,9 @@ function createNewAreaInput(pageForm) {
|
|||||||
divSmallNameArea.className = 'author mt-4 added-field'
|
divSmallNameArea.className = 'author mt-4 added-field'
|
||||||
divSubjectName.className = 'subject-name'
|
divSubjectName.className = 'subject-name'
|
||||||
spanInputNameTxtSlo.className = 'input-name-txt'
|
spanInputNameTxtSlo.className = 'input-name-txt'
|
||||||
spanInputNameTxtSlo.textContent = 'NOVO PODPODROČJE (slovensko)'
|
spanInputNameTxtSlo.textContent = i18next.t('NOVO PODPODROČJE (slovensko)')
|
||||||
spanInputNameTxtEng.className = 'input-name-txt mt-4'
|
spanInputNameTxtEng.className = 'input-name-txt mt-4'
|
||||||
spanInputNameTxtEng.textContent = 'NOVO PODPODROČJE (angleško)'
|
spanInputNameTxtEng.textContent = i18next.t('NOVO PODPODROČJE (angleško)')
|
||||||
divRow.className = 'row align-items-center'
|
divRow.className = 'row align-items-center'
|
||||||
divRow2.className = 'row align-items-center'
|
divRow2.className = 'row align-items-center'
|
||||||
divEnglishInput.className = 'mt-4'
|
divEnglishInput.className = 'mt-4'
|
||||||
@@ -1333,11 +1483,12 @@ function createNewAreaInput(pageForm) {
|
|||||||
divColSm.className = 'col-sm mt-3 mt-sm-0'
|
divColSm.className = 'col-sm mt-3 mt-sm-0'
|
||||||
divColSm2.className = 'col-sm mt-3 mt-sm-0'
|
divColSm2.className = 'col-sm mt-3 mt-sm-0'
|
||||||
spanNameInfoTxt.className = 'd-sm-inline name-info-txt ms-xxl-3 ms-md-3'
|
spanNameInfoTxt.className = 'd-sm-inline name-info-txt ms-xxl-3 ms-md-3'
|
||||||
spanNameInfoTxt.textContent =
|
spanNameInfoTxt.textContent = i18next.t(
|
||||||
'Vpišite novo podpodročje. Na seznamu podpodročij bo vidno takoj po potrditvi administratorja portala.'
|
'Vpišite novo podpodročje. Na seznamu podpodročij bo vidno takoj po potrditvi administratorja portala.'
|
||||||
|
)
|
||||||
spanNameInfoTxtEng.className =
|
spanNameInfoTxtEng.className =
|
||||||
'd-sm-inline name-info-txt ms-xxl-3 ms-md-3 mt-4'
|
'd-sm-inline name-info-txt ms-xxl-3 ms-md-3 mt-4'
|
||||||
spanNameInfoTxtEng.textContent = 'Novo podpodročje (angleško).'
|
spanNameInfoTxtEng.textContent = i18next.t('Novo podpodročje (angleško).')
|
||||||
|
|
||||||
divSmallNameArea.appendChild(divSubjectName)
|
divSmallNameArea.appendChild(divSubjectName)
|
||||||
divSubjectName.appendChild(spanInputNameTxtSlo)
|
divSubjectName.appendChild(spanInputNameTxtSlo)
|
||||||
@@ -1468,6 +1619,10 @@ function changePageContent(el) {
|
|||||||
const collapsableBtn = document.querySelectorAll('.collapsable-entry-btn')
|
const collapsableBtn = document.querySelectorAll('.collapsable-entry-btn')
|
||||||
const addedFields = document.querySelectorAll('.added-field')
|
const addedFields = document.querySelectorAll('.added-field')
|
||||||
const collapsibleData = document.querySelectorAll('.collapsible-data')
|
const collapsibleData = document.querySelectorAll('.collapsible-data')
|
||||||
|
const responseModal = new bootstrap.Modal(
|
||||||
|
document.getElementById('duplicate-modal'),
|
||||||
|
{ keyboard: false }
|
||||||
|
)
|
||||||
switch (el) {
|
switch (el) {
|
||||||
case 'content-preview':
|
case 'content-preview':
|
||||||
loadPreview(info)
|
loadPreview(info)
|
||||||
@@ -1530,6 +1685,7 @@ function changePageContent(el) {
|
|||||||
break
|
break
|
||||||
case 'new-entry-btn': {
|
case 'new-entry-btn': {
|
||||||
// formEditContent.reset()
|
// formEditContent.reset()
|
||||||
|
|
||||||
formEditContent.removeAttribute('data-entry-id')
|
formEditContent.removeAttribute('data-entry-id')
|
||||||
formEditContent.action = '/api/v1/entries/create'
|
formEditContent.action = '/api/v1/entries/create'
|
||||||
if (editSection.classList.contains('d-none'))
|
if (editSection.classList.contains('d-none'))
|
||||||
@@ -1546,6 +1702,16 @@ function changePageContent(el) {
|
|||||||
previewBtnEl.disabled = true
|
previewBtnEl.disabled = true
|
||||||
delete newEntryBtn.dataset.term
|
delete newEntryBtn.dataset.term
|
||||||
}
|
}
|
||||||
|
// editBtnEl.click()
|
||||||
|
editBtnEl.classList.add('active-site-link')
|
||||||
|
previewBtnEl.classList.remove('active-site-link')
|
||||||
|
commentsBtnEl.classList.remove('active-site-link')
|
||||||
|
previewButtonsGroup.classList.remove('d-none')
|
||||||
|
commentsButtonsGroup.classList.add('d-none')
|
||||||
|
previewSection.classList.add('d-none')
|
||||||
|
editSection.classList.remove('d-none')
|
||||||
|
commentsSection.classList.add('d-none')
|
||||||
|
resizeFields.forEach(el => autoResize(el))
|
||||||
authorEl.textContent = ''
|
authorEl.textContent = ''
|
||||||
versionEl.textContent = 'Verzija 1'
|
versionEl.textContent = 'Verzija 1'
|
||||||
previewAuthor.textContent = ''
|
previewAuthor.textContent = ''
|
||||||
@@ -1558,15 +1724,19 @@ function changePageContent(el) {
|
|||||||
deleteEntry.disabled = true
|
deleteEntry.disabled = true
|
||||||
changeCollapsedContent()
|
changeCollapsedContent()
|
||||||
if (addedFields.length) addedFields.forEach(el => el.remove())
|
if (addedFields.length) addedFields.forEach(el => el.remove())
|
||||||
editBtnEl.click()
|
|
||||||
termInputField.focus()
|
|
||||||
const messageContainer = document.querySelectorAll('.message-container')
|
const messageContainer = document.querySelectorAll('.message-container')
|
||||||
messageContainer.forEach(el => el.classList.add('d-none'))
|
messageContainer.forEach(el => el.classList.add('d-none'))
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
selectedEntry.classList.remove('selected-term-btn')
|
selectedEntry.classList.remove('selected-term-btn')
|
||||||
$('.multiple').val(null).trigger('change')
|
$('.multiple').val(null).trigger('change')
|
||||||
$('.without-dropdown').val(null).trigger('change')
|
$('.without-dropdown').val(null).trigger('change')
|
||||||
editBtnEl.click()
|
// editBtnEl.click()
|
||||||
|
// termInputField.addEventListener('focus', () =>
|
||||||
|
// showMCButtons(termInputField, 'link')
|
||||||
|
// )
|
||||||
|
termInputField.focus()
|
||||||
|
termInputField.click()
|
||||||
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'first':
|
case 'first':
|
||||||
@@ -1610,7 +1780,6 @@ function changePageContent(el) {
|
|||||||
commentsBtnEl.disabled = true
|
commentsBtnEl.disabled = true
|
||||||
commentsBtnEl.classList.add('disabled')
|
commentsBtnEl.classList.add('disabled')
|
||||||
showDates.disabled = true
|
showDates.disabled = true
|
||||||
duplicateEntry.disabled = true
|
|
||||||
deleteEntry.disabled = true
|
deleteEntry.disabled = true
|
||||||
changeCollapsedContent()
|
changeCollapsedContent()
|
||||||
editBtnEl.click()
|
editBtnEl.click()
|
||||||
@@ -1618,6 +1787,8 @@ function changePageContent(el) {
|
|||||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||||
selectedEntry.classList.remove('selected-term-btn')
|
selectedEntry.classList.remove('selected-term-btn')
|
||||||
editBtnEl.click()
|
editBtnEl.click()
|
||||||
|
responseModal.toggle()
|
||||||
|
duplicateEntry.disabled = true
|
||||||
break
|
break
|
||||||
case 'true':
|
case 'true':
|
||||||
spanFilterText.classList.remove('normal-gray-label')
|
spanFilterText.classList.remove('normal-gray-label')
|
||||||
@@ -1798,8 +1969,9 @@ function loadPreview(info) {
|
|||||||
: linksArr[i][key] === 'broader'
|
: linksArr[i][key] === 'broader'
|
||||||
? 'BT:'
|
? 'BT:'
|
||||||
: ''
|
: ''
|
||||||
|
linkedTerms.innerHTML += keyTxt + ' ' + key
|
||||||
linkedTerms.innerHTML += keyTxt + ' ' + key + ' '
|
if (parseInt(i) !== linksArr.length - 1) linkedTerms.innerHTML += ', '
|
||||||
|
else linkedTerms.innerHTML += ' '
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else changeClasses(previewLinkedTerms, 'hide')
|
} else changeClasses(previewLinkedTerms, 'hide')
|
||||||
@@ -1864,19 +2036,21 @@ function loadPreview(info) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const languageContainers = document.querySelectorAll('.preview-one-language')
|
const languageContainers = document.querySelectorAll('.preview-one-language')
|
||||||
const langLine = document.querySelector('.language-line')
|
const langLine = document.querySelector('.start-line')
|
||||||
if (languageContainers) {
|
if (languageContainers) {
|
||||||
const arrLang = Array.from(languageContainers)
|
const arrLang = Array.from(languageContainers)
|
||||||
arrLang.forEach(el => {
|
arrLang.forEach(el => {
|
||||||
const arrChildren = Array.from(el.children)
|
const arrChildren = Array.from(el.children)
|
||||||
if (arrChildren.filter(e => e.classList.contains('d-none')).length > 2) {
|
if (arrChildren.filter(e => e.classList.contains('d-none')).length > 2) {
|
||||||
el.classList.add('d-none')
|
el.classList.add('d-none')
|
||||||
langLine.classList.add('d-none')
|
} else el.classList.remove('d-none')
|
||||||
} else {
|
|
||||||
el.classList.remove('d-none')
|
|
||||||
langLine.classList.remove('d-none')
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
if (
|
||||||
|
arrLang.filter(el => el.classList.contains('d-none')).length >=
|
||||||
|
arrLang.length
|
||||||
|
) {
|
||||||
|
langLine.classList.add('d-none')
|
||||||
|
} else langLine.classList.remove('d-none')
|
||||||
}
|
}
|
||||||
changeCollapsedContent()
|
changeCollapsedContent()
|
||||||
}
|
}
|
||||||
@@ -1924,6 +2098,7 @@ function changeVersionList(versions) {
|
|||||||
dateLabel.dataset.bsCustomClass = 'dark-gray-tooltip'
|
dateLabel.dataset.bsCustomClass = 'dark-gray-tooltip'
|
||||||
dateLabel.dataset.bsPlacement = 'bottom'
|
dateLabel.dataset.bsPlacement = 'bottom'
|
||||||
dateLabel.dataset.bsToggle = 'tooltip'
|
dateLabel.dataset.bsToggle = 'tooltip'
|
||||||
|
dateLabel.dataset.bsHtml = 'true'
|
||||||
const date = new Date(el.version_time).toLocaleDateString(
|
const date = new Date(el.version_time).toLocaleDateString(
|
||||||
'sl-SL',
|
'sl-SL',
|
||||||
localeOptions
|
localeOptions
|
||||||
@@ -1932,9 +2107,16 @@ function changeVersionList(versions) {
|
|||||||
dateRadio.id = `date${el.version}`
|
dateRadio.id = `date${el.version}`
|
||||||
dateRadio.value = `${el.version}`
|
dateRadio.value = `${el.version}`
|
||||||
dateLabel.htmlFor = `date${el.version}`
|
dateLabel.htmlFor = `date${el.version}`
|
||||||
|
// new bootstrap.Tooltip(dateLabel, {
|
||||||
|
// title: `Verzija ${el.version} <br> Avtor: ${el.version_author}`
|
||||||
|
// })
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
new bootstrap.Tooltip(dateLabel, {
|
new bootstrap.Tooltip(dateLabel, {
|
||||||
title: `Verzija ${el.version}`
|
title:
|
||||||
|
i18next.t('Verzija:') +
|
||||||
|
` ${el.version} <br>` +
|
||||||
|
i18next.t('Avtor:') +
|
||||||
|
` ${el.version_author}`
|
||||||
})
|
})
|
||||||
allDatesEl.append(dateRadio)
|
allDatesEl.append(dateRadio)
|
||||||
allDatesEl.appendChild(dateLabel)
|
allDatesEl.appendChild(dateLabel)
|
||||||
@@ -1942,6 +2124,30 @@ function changeVersionList(versions) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setLatestVersion(data) {
|
||||||
|
const latestVersionLabel = document.getElementById('latest-version-label')
|
||||||
|
// const tooltip = new bootstrap.Tooltip(latestVersionLabel, {
|
||||||
|
// title: `Verzija: ${data.version} <br> Avtor: ${data.version_author}`,
|
||||||
|
// customClass: 'dark-gray-tooltip',
|
||||||
|
// html: true,
|
||||||
|
// placement: 'bottom'
|
||||||
|
// })
|
||||||
|
// eslint-disable-next-line
|
||||||
|
const tooltip = new bootstrap.Tooltip(latestVersionLabel, {
|
||||||
|
title:
|
||||||
|
i18next.t('Verzija:') +
|
||||||
|
` ${data.version} <br>` +
|
||||||
|
i18next.t('Avtor:') +
|
||||||
|
` ${data.version_author}`,
|
||||||
|
customClass: 'dark-gray-tooltip',
|
||||||
|
html: true,
|
||||||
|
placement: 'bottom'
|
||||||
|
})
|
||||||
|
// Can't think of any better solution for the not disapearing label bug
|
||||||
|
const dateArea = document.querySelector('.date-scroller')
|
||||||
|
dateArea.addEventListener('click', () => tooltip.hide())
|
||||||
|
}
|
||||||
|
|
||||||
function removeOldMedia(images, audio, video) {
|
function removeOldMedia(images, audio, video) {
|
||||||
if (images !== null || audio !== null || video !== null) {
|
if (images !== null || audio !== null || video !== null) {
|
||||||
const media = [images, audio, video]
|
const media = [images, audio, video]
|
||||||
@@ -1998,7 +2204,7 @@ function showSelectedDateData(date) {
|
|||||||
// classicOverview.classList.add('d-none')
|
// classicOverview.classList.add('d-none')
|
||||||
// selectedOverview.classList.remove('d-none')
|
// selectedOverview.classList.remove('d-none')
|
||||||
btnSaveIcon.firstChild.src = '/images/u_redo.svg'
|
btnSaveIcon.firstChild.src = '/images/u_redo.svg'
|
||||||
btnSaveIcon.children[1].textContent = 'Obnovi'
|
btnSaveIcon.children[1].textContent = i18next.t('Obnovi')
|
||||||
btnSaveIcon.id = 'redo-action'
|
btnSaveIcon.id = 'redo-action'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2038,7 +2244,7 @@ function changeCollapsedContent() {
|
|||||||
|
|
||||||
function createHeadword() {
|
function createHeadword() {
|
||||||
const { termInputField, headerwordTable } = window.dictionaryElements
|
const { termInputField, headerwordTable } = window.dictionaryElements
|
||||||
if (!termInputField.value.length) alert('Vnesti morate termin')
|
if (!termInputField.value.length) alert(i18next.t('Vnesti morate termin'))
|
||||||
else {
|
else {
|
||||||
const createHeadwordGroup = document.getElementById('create-headword-group')
|
const createHeadwordGroup = document.getElementById('create-headword-group')
|
||||||
const createdHeadwordGroup = document.getElementById(
|
const createdHeadwordGroup = document.getElementById(
|
||||||
@@ -2181,21 +2387,21 @@ function addField(form, element, content) {
|
|||||||
if (formEditContent) {
|
if (formEditContent) {
|
||||||
switch (element) {
|
switch (element) {
|
||||||
case newImageBtn:
|
case newImageBtn:
|
||||||
spanName.textContent = 'SLIKA'
|
spanName.textContent = i18next.t('SLIKA')
|
||||||
inputField.name = 'image'
|
inputField.name = 'image'
|
||||||
spanNameInfoTxt.textContent = 'Nova slika.'
|
spanNameInfoTxt.textContent = i18next.t('Nova slika.')
|
||||||
if (content) inputField.value = content
|
if (content) inputField.value = content
|
||||||
break
|
break
|
||||||
case newAudioBtn:
|
case newAudioBtn:
|
||||||
spanName.textContent = 'ZVOK'
|
spanName.textContent = i18next.t('ZVOK')
|
||||||
inputField.name = 'audio'
|
inputField.name = 'audio'
|
||||||
spanNameInfoTxt.textContent = 'Nov zvok.'
|
spanNameInfoTxt.textContent = i18next.t('Nov zvok.')
|
||||||
if (content) inputField.value = content
|
if (content) inputField.value = content
|
||||||
break
|
break
|
||||||
case newVideoBtn:
|
case newVideoBtn:
|
||||||
spanName.textContent = 'VIDEO'
|
spanName.textContent = i18next.t('VIDEO')
|
||||||
inputField.name = 'video'
|
inputField.name = 'video'
|
||||||
spanNameInfoTxt.textContent = 'Nov video.'
|
spanNameInfoTxt.textContent = i18next.t('Nov video.')
|
||||||
if (content) inputField.value = content
|
if (content) inputField.value = content
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -2204,10 +2410,11 @@ function addField(form, element, content) {
|
|||||||
element.parentElement.parentElement.parentElement
|
element.parentElement.parentElement.parentElement
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
spanName.textContent = 'AVTOR'
|
spanName.textContent = i18next.t('AVTOR')
|
||||||
inputField.name = 'author'
|
inputField.name = 'author'
|
||||||
spanNameInfoTxt.textContent =
|
spanNameInfoTxt.textContent = i18next.t(
|
||||||
'Dodajte ime in priimek naslednjega avtorja slovarja..'
|
'Dodajte ime in priimek naslednjega avtorja slovarja.'
|
||||||
|
)
|
||||||
form.insertBefore(
|
form.insertBefore(
|
||||||
divMarginTop,
|
divMarginTop,
|
||||||
element.parentElement.parentElement.parentElement
|
element.parentElement.parentElement.parentElement
|
||||||
@@ -2224,7 +2431,6 @@ function addConnectionField(form, element, data) {
|
|||||||
const divRow = document.createElement('div')
|
const divRow = document.createElement('div')
|
||||||
const divColLg2 = document.createElement('div')
|
const divColLg2 = document.createElement('div')
|
||||||
const divColLg4 = document.createElement('div')
|
const divColLg4 = document.createElement('div')
|
||||||
const divColLg = document.createElement('div')
|
|
||||||
const colLg2Input = document.createElement('select')
|
const colLg2Input = document.createElement('select')
|
||||||
const inputGroup = document.createElement('div')
|
const inputGroup = document.createElement('div')
|
||||||
const inputField = document.createElement('input')
|
const inputField = document.createElement('input')
|
||||||
@@ -2243,7 +2449,6 @@ function addConnectionField(form, element, data) {
|
|||||||
divColLg2.appendChild(colLg2Input)
|
divColLg2.appendChild(colLg2Input)
|
||||||
colLg2Input.appendChild(opt)
|
colLg2Input.appendChild(opt)
|
||||||
divRow.appendChild(divColLg4)
|
divRow.appendChild(divColLg4)
|
||||||
divRow.appendChild(divColLg)
|
|
||||||
divColLg4.appendChild(inputGroup)
|
divColLg4.appendChild(inputGroup)
|
||||||
inputGroup.appendChild(inputField)
|
inputGroup.appendChild(inputField)
|
||||||
inputGroup.appendChild(spanInputGroup)
|
inputGroup.appendChild(spanInputGroup)
|
||||||
@@ -2252,32 +2457,34 @@ function addConnectionField(form, element, data) {
|
|||||||
divColSm.appendChild(spanNameInfoTxt)
|
divColSm.appendChild(spanNameInfoTxt)
|
||||||
|
|
||||||
divMarginTop.className = 'mt-4 added-field'
|
divMarginTop.className = 'mt-4 added-field'
|
||||||
divSubjectName.className = 'subject-name'
|
divSubjectName.className =
|
||||||
|
'subject-name col-sm-6 d-flex justify-content-between'
|
||||||
spanName.className = 'input-name-txt'
|
spanName.className = 'input-name-txt'
|
||||||
divRow.className = 'row align-items-center'
|
divRow.className = 'row align-items-center'
|
||||||
divColLg2.className = 'col-lg-2 col-6'
|
divColLg2.className = 'col-xl-2 col-4'
|
||||||
divColLg4.className = 'col-lg-4 mt-2 mt-lg-0'
|
divColLg4.className = 'col-8 col-xl-6 col-xxl-4'
|
||||||
divColLg.className = 'col-lg align-items-center'
|
|
||||||
colLg2Input.className = 'name-input form-select d-inline'
|
colLg2Input.className = 'name-input form-select d-inline'
|
||||||
colLg2Input.name = 'type'
|
colLg2Input.name = 'type'
|
||||||
inputGroup.className = 'input-group'
|
inputGroup.className = 'input-group'
|
||||||
inputField.className = 'name-input d-inline form-control icon-trash mc-field'
|
inputField.className =
|
||||||
|
'name-input d-inline form-control icon-trash mc-field dispatch-tab'
|
||||||
spanInputGroup.className = 'input-group-text delete-author-btn'
|
spanInputGroup.className = 'input-group-text delete-author-btn'
|
||||||
spanInputGroup.id = 'trash-icon-btn'
|
spanInputGroup.id = 'trash-icon-btn'
|
||||||
spanInputGroup.addEventListener('click', deleteField)
|
spanInputGroup.addEventListener('click', deleteField)
|
||||||
imgTrashIcon.className = 'delete-author p-0'
|
imgTrashIcon.className = 'delete-author p-0'
|
||||||
imgTrashIcon.src = '/images/red-trash-icon.svg'
|
imgTrashIcon.src = '/images/red-trash-icon.svg'
|
||||||
imgTrashIcon.alt = 'Delete'
|
imgTrashIcon.alt = 'Delete'
|
||||||
divColSm.className = 'col-sm'
|
divColSm.className = 'col d-none d-xl-flex align-items-center'
|
||||||
spanNameInfoTxt.className = 'name-info-txt mt-3'
|
spanNameInfoTxt.className = 'd-md-inline d-block name-info-txt mt-3 mt-sm-0'
|
||||||
spanName.textContent = 'POVEZAVA'
|
spanNameInfoTxt.textContent = i18next.t('Nov povezan termin.')
|
||||||
|
spanName.textContent = i18next.t('POVEZANI TERMIN')
|
||||||
inputField.name = 'links'
|
inputField.name = 'links'
|
||||||
opt.value = 'broader'
|
opt.value = 'broader'
|
||||||
opt.text = 'Širši'
|
opt.text = i18next.t('Širši')
|
||||||
opt2.value = 'related'
|
opt2.value = 'related'
|
||||||
opt3.value = 'narrow'
|
opt3.value = 'narrow'
|
||||||
opt2.text = 'Sorodni'
|
opt2.text = i18next.t('Sorodni')
|
||||||
opt3.text = 'Ožji'
|
opt3.text = i18next.t('Ožji')
|
||||||
colLg2Input.add(opt2)
|
colLg2Input.add(opt2)
|
||||||
colLg2Input.add(opt)
|
colLg2Input.add(opt)
|
||||||
colLg2Input.add(opt3)
|
colLg2Input.add(opt3)
|
||||||
@@ -2286,6 +2493,32 @@ function addConnectionField(form, element, data) {
|
|||||||
divMarginTop,
|
divMarginTop,
|
||||||
element.parentElement.parentElement.parentElement
|
element.parentElement.parentElement.parentElement
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Create mixed content buttons above input field
|
||||||
|
const mcDiv = document.createElement('div')
|
||||||
|
const supscriptBtn = document.createElement('button')
|
||||||
|
const subscriptBtn = document.createElement('button')
|
||||||
|
const supImg = document.createElement('img')
|
||||||
|
const subImg = document.createElement('img')
|
||||||
|
|
||||||
|
mcDiv.appendChild(supscriptBtn)
|
||||||
|
mcDiv.appendChild(subscriptBtn)
|
||||||
|
supscriptBtn.appendChild(supImg)
|
||||||
|
subscriptBtn.appendChild(subImg)
|
||||||
|
|
||||||
|
mcDiv.className = 'mc-buttons-group d-none'
|
||||||
|
supscriptBtn.className = 'mc-button mc-supscript'
|
||||||
|
supscriptBtn.type = 'button'
|
||||||
|
subscriptBtn.className = 'mc-button mc-subscript'
|
||||||
|
subscriptBtn.type = 'button'
|
||||||
|
supImg.src = '/images/superscript.svg'
|
||||||
|
supImg.className = 'mc-center-img'
|
||||||
|
subImg.className = 'mc-center-img'
|
||||||
|
subImg.src = '/images/subscript.svg'
|
||||||
|
|
||||||
|
divSubjectName.appendChild(mcDiv)
|
||||||
|
// eslint-disable-next-line
|
||||||
|
inputField.addEventListener('focus', () => showMCButtons(inputField, 'link'))
|
||||||
if (data) {
|
if (data) {
|
||||||
inputField.value = data.link
|
inputField.value = data.link
|
||||||
colLg2Input.value = data.type
|
colLg2Input.value = data.type
|
||||||
@@ -2336,14 +2569,14 @@ function handleAreasClick({ target }) {
|
|||||||
const saveButton = document.createElement('button')
|
const saveButton = document.createElement('button')
|
||||||
saveButton.type = 'button'
|
saveButton.type = 'button'
|
||||||
cancelButton.type = 'button'
|
cancelButton.type = 'button'
|
||||||
cancelButton.textContent = 'Prekliči'
|
cancelButton.textContent = i18next.t('Prekliči')
|
||||||
cancelButton.className = 'btn btn-secondary me-2'
|
cancelButton.className = 'btn btn-secondary me-2'
|
||||||
cancelButton.style.height = '33px'
|
cancelButton.style.height = '33px'
|
||||||
cancelButton.style.width = '105px'
|
cancelButton.style.width = '105px'
|
||||||
cancelButton.addEventListener('click', () =>
|
cancelButton.addEventListener('click', () =>
|
||||||
abortEditing(newButtonGroup, tableButtons, tDataArea)
|
abortEditing(newButtonGroup, tableButtons, tDataArea)
|
||||||
)
|
)
|
||||||
saveButton.textContent = 'POTRDI'
|
saveButton.textContent = i18next.t('POTRDI')
|
||||||
saveButton.type = 'button'
|
saveButton.type = 'button'
|
||||||
saveButton.className = 'btn btn-primary'
|
saveButton.className = 'btn btn-primary'
|
||||||
saveButton.style.height = '33px'
|
saveButton.style.height = '33px'
|
||||||
@@ -2593,6 +2826,12 @@ function deleteContentData(
|
|||||||
linkType.value = 'related'
|
linkType.value = 'related'
|
||||||
linkText.value = ''
|
linkText.value = ''
|
||||||
}
|
}
|
||||||
|
if (listForeignDefinitions) {
|
||||||
|
const foreignDefinitionsFields = document.querySelectorAll(
|
||||||
|
'.foreign-definition-el'
|
||||||
|
)
|
||||||
|
foreignDefinitionsFields.forEach(el => (el.value = ''))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function activateMe(term) {
|
function activateMe(term) {
|
||||||
@@ -2666,7 +2905,7 @@ function checkLanguages() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$('.summernote').summernote({
|
$('.summernote').summernote({
|
||||||
placeholder: 'Na kratko opišite zasnovo in namen slovarja.',
|
placeholder: i18next.t('Na kratko opišite zasnovo in namen slovarja.'),
|
||||||
height: 300,
|
height: 300,
|
||||||
minheight: 150,
|
minheight: 150,
|
||||||
toolbar: [
|
toolbar: [
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/* globals axios, i18next */
|
||||||
|
|
||||||
|
const exportForm = document.forms['dictionary-export']
|
||||||
|
|
||||||
|
exportForm.addEventListener('submit', async event => {
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const payload = Object.fromEntries(new FormData(event.target))
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(event.target.action, payload)
|
||||||
|
// TODO This (page refresh) is a temporary hack. Replace with proper UI updating logic.
|
||||||
|
location.reload()
|
||||||
|
} catch (error) {
|
||||||
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
|
if (error.response) {
|
||||||
|
message = error.response.data
|
||||||
|
} else if (error.request) {
|
||||||
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
|
}
|
||||||
|
|
||||||
|
alert(message)
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
/* global axios, initPagination, removeAllChildNodes */
|
/* global axios, initPagination, removeAllChildNodes, i18next, dictionaryId */
|
||||||
{
|
{
|
||||||
const selectorEl = document.getElementById('select-extraction-name')
|
const selectorEl = document.getElementById('select-extraction-name')
|
||||||
selectorEl.addEventListener('change', () => loadCandidates(selectorEl.value))
|
selectorEl.addEventListener('change', () => loadCandidates(selectorEl.value))
|
||||||
const resultsListEl = document.getElementById('page-results')
|
const resultsListEl = document.getElementById('page-results')
|
||||||
|
const importFormEl = document.getElementById('import-form')
|
||||||
|
const importButtonEl = document.getElementById('import-btn')
|
||||||
let termCandidates
|
let termCandidates
|
||||||
let hitsPerPage
|
let hitsPerPage
|
||||||
let numberOfAllPages
|
let numberOfAllPages
|
||||||
|
|
||||||
|
importFormEl.addEventListener('submit', submitForm)
|
||||||
|
|
||||||
async function loadCandidates(id) {
|
async function loadCandidates(id) {
|
||||||
try {
|
try {
|
||||||
const { data } = await axios.get(
|
const { data } = await axios.get(
|
||||||
@@ -18,6 +23,8 @@
|
|||||||
const results = getDataForFirstPage(data)
|
const results = getDataForFirstPage(data)
|
||||||
renderResults(results)
|
renderResults(results)
|
||||||
updateDemoPager(1, numberOfAllPages)
|
updateDemoPager(1, numberOfAllPages)
|
||||||
|
importFormEl.action = `/api/v1/dictionaries/${dictionaryId}/import-extraction/${id}`
|
||||||
|
importButtonEl.disabled = false
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error)
|
console.log(error)
|
||||||
}
|
}
|
||||||
@@ -55,6 +62,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderResults(results) {
|
function renderResults(results) {
|
||||||
|
const tableContainer = document.querySelector(
|
||||||
|
'.list-terminology-candidates'
|
||||||
|
)
|
||||||
|
tableContainer.classList.remove('d-none')
|
||||||
results.forEach(([sequentialCount, candidate]) => {
|
results.forEach(([sequentialCount, candidate]) => {
|
||||||
const rowEl = document.createElement('tr')
|
const rowEl = document.createElement('tr')
|
||||||
const tdId = document.createElement('td')
|
const tdId = document.createElement('td')
|
||||||
@@ -64,9 +75,27 @@
|
|||||||
tdId.textContent = sequentialCount
|
tdId.textContent = sequentialCount
|
||||||
tdName.textContent = candidate.kanonicnaoblika
|
tdName.textContent = candidate.kanonicnaoblika
|
||||||
tdSize.textContent = candidate.ranking
|
tdSize.textContent = candidate.ranking
|
||||||
tdDate.textContent = candidate.pogostostpojavljanja
|
tdDate.textContent = candidate.pogostostpojavljanja[0]
|
||||||
rowEl.append(tdId, tdName, tdSize, tdDate)
|
rowEl.append(tdId, tdName, tdSize, tdDate)
|
||||||
resultsListEl.appendChild(rowEl)
|
resultsListEl.appendChild(rowEl)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function submitForm(event) {
|
||||||
|
event.preventDefault()
|
||||||
|
const payload = Object.fromEntries(new FormData(event.target))
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.post(event.target.action, payload)
|
||||||
|
alert(i18next.t('SLOVAR UVOŽEN'))
|
||||||
|
} catch (error) {
|
||||||
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
|
if (error.response) {
|
||||||
|
message = error.response.data
|
||||||
|
} else if (error.request) {
|
||||||
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
|
}
|
||||||
|
alert(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const nameEl = document.getElementById('name')
|
||||||
|
nameEl.addEventListener('input', enableButton)
|
||||||
|
|
||||||
|
function enableButton() {
|
||||||
|
const disabledBtn = document.getElementById('mpbtn')
|
||||||
|
disabledBtn.classList.remove('disabled')
|
||||||
|
}
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
/* global axios, removeAllChildNodes, bootstrap */
|
/* global axios, removeAllChildNodes, bootstrap, i18next */
|
||||||
|
|
||||||
|
const fileUploadContainerEl = document.getElementById('upload-files-container')
|
||||||
const fileUploadForm = document.forms['upload-files']
|
const fileUploadForm = document.forms['upload-files']
|
||||||
const fileInputEl = fileUploadForm.querySelector('input[type="file"]')
|
const fileInputEl = fileUploadForm.querySelector('input[type="file"]')
|
||||||
const filesListEl = document.getElementById('files-list')
|
const filesListEl = document.getElementById('files-list')
|
||||||
const dropArea = document.querySelector('.drag-area')
|
const dropArea = document.querySelector('.drag-area')
|
||||||
const dragButton = dropArea.querySelector('button')
|
const dragButton = dropArea.querySelector('button')
|
||||||
const dragInput = dropArea.querySelector('input')
|
const dragInput = dropArea.querySelector('input')
|
||||||
const modalSpinner = new bootstrap.Modal(
|
|
||||||
document.getElementById('modal-spinner')
|
|
||||||
)
|
|
||||||
const modalAlert = new bootstrap.Modal(document.getElementById('alert-modal'))
|
const modalAlert = new bootstrap.Modal(document.getElementById('alert-modal'))
|
||||||
let file
|
let file
|
||||||
|
let fileListElCount = filesListEl.childElementCount
|
||||||
|
let isUploadInProgress = false
|
||||||
|
let isDeletionInProgress = false
|
||||||
|
const existingFileNames = [...filesListEl.children].map(
|
||||||
|
fileListEl => fileListEl.querySelector('.file-name').textContent
|
||||||
|
)
|
||||||
|
|
||||||
dragButton.onclick = () => {
|
dragButton.onclick = () => {
|
||||||
dragInput.click()
|
dragInput.click()
|
||||||
@@ -68,96 +72,216 @@ fileInputEl.addEventListener('change', submitFiles)
|
|||||||
filesListEl.addEventListener('click', handleFileClick)
|
filesListEl.addEventListener('click', handleFileClick)
|
||||||
|
|
||||||
async function submitFiles() {
|
async function submitFiles() {
|
||||||
// TODO Lock additional submits for the duration of this function execution?
|
if (isUploadInProgress) return
|
||||||
|
isUploadInProgress = true
|
||||||
|
fileUploadContainerEl.hidden = true
|
||||||
const MAX_FILE_SIZE = 10 ** 9 // 1 GB
|
const MAX_FILE_SIZE = 10 ** 9 // 1 GB
|
||||||
|
const filesToUpload = []
|
||||||
const failedUploads = []
|
const failedUploads = []
|
||||||
|
const succeededFileListEls = []
|
||||||
modalSpinner.toggle()
|
let didRemoveAnyFileListEls = false
|
||||||
|
|
||||||
for (const file of fileInputEl.files) {
|
for (const file of fileInputEl.files) {
|
||||||
|
const filename = file.name
|
||||||
|
|
||||||
|
const indexOfSameNamed = existingFileNames.indexOf(filename)
|
||||||
|
if (indexOfSameNamed !== -1) {
|
||||||
|
filesListEl.children[indexOfSameNamed].remove()
|
||||||
|
existingFileNames.splice(indexOfSameNamed, 1)
|
||||||
|
didRemoveAnyFileListEls = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileListEl = createFileListEl(filename)
|
||||||
|
filesListEl.appendChild(fileListEl)
|
||||||
|
existingFileNames.push(filename)
|
||||||
|
|
||||||
if (file.size > MAX_FILE_SIZE) {
|
if (file.size > MAX_FILE_SIZE) {
|
||||||
const failedUpload = {
|
const failedUpload = {
|
||||||
filename: file.name,
|
filename,
|
||||||
message: 'File too large. Must not be over 1 GB.'
|
message: 'File too large. Must not be over 1 GB.'
|
||||||
}
|
}
|
||||||
failedUploads.push(failedUpload)
|
failedUploads.push(failedUpload)
|
||||||
|
updateFileListEl(fileListEl, { status: 'failed' })
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = new FormData()
|
filesToUpload.push([file, fileListEl])
|
||||||
payload.set(fileInputEl.name, file)
|
}
|
||||||
|
|
||||||
|
if (didRemoveAnyFileListEls) reindexFileListEls()
|
||||||
|
|
||||||
|
for (const [file, fileListEl] of filesToUpload) {
|
||||||
try {
|
try {
|
||||||
await axios.put(apiEndpointBase, payload)
|
const payload = new FormData()
|
||||||
|
payload.set(fileInputEl.name, file)
|
||||||
|
const { data: fileStats } = await axios.put(apiEndpointBase, payload, {
|
||||||
|
onUploadProgress: displayUploadProgress(fileListEl)
|
||||||
|
})
|
||||||
|
succeededFileListEls.push(fileListEl)
|
||||||
|
updateFileListEl(fileListEl, { status: 'success', fileStats })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const failedUpload = {
|
const failedUpload = {
|
||||||
filename: file.name,
|
filename: file.name,
|
||||||
message: error.response.data
|
message: error.response.data
|
||||||
}
|
}
|
||||||
failedUploads.push(failedUpload)
|
failedUploads.push(failedUpload)
|
||||||
|
updateFileListEl(fileListEl, { status: 'failed' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
const { data: files } = await axios.get(apiEndpointBase)
|
|
||||||
updateFilesList(files)
|
|
||||||
} catch {
|
|
||||||
alert('Pri posodobljanju seznama naloženih datotek je prišlo do napake.')
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInputEl.value = ''
|
fileInputEl.value = ''
|
||||||
displayFailedUploads(failedUploads)
|
succeededFileListEls.forEach(el => updateFileListEl(el, { status: 'done' }))
|
||||||
modalSpinner.toggle()
|
if (failedUploads.length) displayFailedUploads(failedUploads)
|
||||||
|
isUploadInProgress = false
|
||||||
|
fileUploadContainerEl.hidden = false
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFilesList(files) {
|
function createFileListEl(filename) {
|
||||||
removeAllChildNodes(filesListEl)
|
const indexEl = document.createElement('td')
|
||||||
files.forEach(({ filename, size, timeModified }, index) => {
|
indexEl.className = 'file-index'
|
||||||
const rowEl = document.createElement('tr')
|
indexEl.textContent = ++fileListElCount
|
||||||
const tdId = document.createElement('td')
|
|
||||||
const tdName = document.createElement('td')
|
const nameEl = document.createElement('td')
|
||||||
const tdSize = document.createElement('td')
|
nameEl.className = 'file-name'
|
||||||
const tdDate = document.createElement('td')
|
nameEl.textContent = filename
|
||||||
const tdDelete = document.createElement('td')
|
|
||||||
tdId.textContent = index + 1
|
const sizeEl = document.createElement('td')
|
||||||
tdName.textContent = filename
|
sizeEl.className = 'file-size'
|
||||||
tdName.className = 'filename'
|
|
||||||
tdSize.textContent = size
|
const dateEl = document.createElement('td')
|
||||||
const formattedDate = new Date(timeModified).toLocaleDateString('sl-SL')
|
dateEl.className = 'file-date-modified'
|
||||||
tdDate.textContent = formattedDate
|
|
||||||
const delBtn = document.createElement('button')
|
const progressBarId = `progress-bar-${fileListElCount}`
|
||||||
delBtn.className = 'p-0 delete-file delete-btn-table'
|
const progressLabelEl = document.createElement('label')
|
||||||
delBtn.type = 'button'
|
progressLabelEl.className = 'me-1'
|
||||||
const deleteImg = document.createElement('img')
|
progressLabelEl.for = progressBarId
|
||||||
deleteImg.src = '/images/red-trash-icon.svg'
|
progressLabelEl.textContent = '0%'
|
||||||
deleteImg.alt = 'Izbriši'
|
const progressBarEl = document.createElement('progress')
|
||||||
const delSpan = document.createElement('span')
|
progressBarEl.id = progressBarId
|
||||||
delSpan.className = 'ms-2'
|
progressBarEl.className = 'upload-progress'
|
||||||
delSpan.textContent = 'Briši'
|
progressBarEl.value = 0
|
||||||
delBtn.appendChild(deleteImg)
|
progressBarEl.max = 100
|
||||||
delBtn.appendChild(delSpan)
|
const lastEl = document.createElement('td')
|
||||||
tdDelete.appendChild(delBtn)
|
lastEl.className = 'file-last-cell'
|
||||||
rowEl.append(tdId, tdName, tdSize, tdDate, tdDelete)
|
lastEl.append(progressLabelEl, progressBarEl)
|
||||||
filesListEl.appendChild(rowEl)
|
|
||||||
})
|
const fileListEl = document.createElement('tr')
|
||||||
|
fileListEl.append(indexEl, nameEl, sizeEl, dateEl, lastEl)
|
||||||
|
|
||||||
|
return fileListEl
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateFileListEl(fileListEl, { status, fileStats, index }) {
|
||||||
|
if (status === 'success') {
|
||||||
|
const nameEl = fileListEl.querySelector('.file-name')
|
||||||
|
nameEl.textContent = fileStats.filename
|
||||||
|
|
||||||
|
const sizeEl = fileListEl.querySelector('.file-size')
|
||||||
|
sizeEl.textContent = fileStats.size
|
||||||
|
|
||||||
|
const dateEl = fileListEl.querySelector('.file-date-modified')
|
||||||
|
const formattedDate = new Date(fileStats.timeModified).toLocaleDateString(
|
||||||
|
'sl-SL'
|
||||||
|
)
|
||||||
|
dateEl.textContent = formattedDate
|
||||||
|
|
||||||
|
const successImgEl = document.createElement('img')
|
||||||
|
successImgEl.src = '/images/valid.svg'
|
||||||
|
successImgEl.alt = i18next.t('Datoteka uspešno naložena')
|
||||||
|
const lastEl = fileListEl.querySelector('.file-last-cell')
|
||||||
|
removeAllChildNodes(lastEl)
|
||||||
|
lastEl.append(successImgEl)
|
||||||
|
} else if (status === 'done') {
|
||||||
|
const deleteImgEl = document.createElement('img')
|
||||||
|
deleteImgEl.src = '/images/red-trash-icon.svg'
|
||||||
|
deleteImgEl.alt = ''
|
||||||
|
const deleteSpanEl = document.createElement('span')
|
||||||
|
deleteSpanEl.className = 'ms-2'
|
||||||
|
deleteSpanEl.textContent = i18next.t('Briši')
|
||||||
|
const deleteButtonEl = document.createElement('button')
|
||||||
|
deleteButtonEl.className = 'p-0 delete-file delete-btn-table'
|
||||||
|
deleteButtonEl.append(deleteImgEl, deleteSpanEl)
|
||||||
|
const lastEl = fileListEl.querySelector('.file-last-cell')
|
||||||
|
removeAllChildNodes(lastEl)
|
||||||
|
lastEl.append(deleteButtonEl)
|
||||||
|
} else if (status === 'reindex') {
|
||||||
|
const indexEl = fileListEl.querySelector('.file-index')
|
||||||
|
indexEl.textContent = index + 1
|
||||||
|
} else if (status === 'failed') {
|
||||||
|
const failImgEl = document.createElement('img')
|
||||||
|
failImgEl.src = '/images/x_red.svg'
|
||||||
|
failImgEl.alt = i18next.t('Napaka pri nalaganju datoteke')
|
||||||
|
const lastEl = fileListEl.querySelector('.file-last-cell')
|
||||||
|
removeAllChildNodes(lastEl)
|
||||||
|
lastEl.append(failImgEl)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function displayFailedUploads(failedUploads) {
|
function displayFailedUploads(failedUploads) {
|
||||||
|
const modalContentEl = modalAlert._element.querySelector(
|
||||||
|
'.modal-alert-content'
|
||||||
|
)
|
||||||
|
modalContentEl.classList.remove('d-flex')
|
||||||
|
|
||||||
|
const descriptionEl = modalAlert._element.querySelector('#alert-text')
|
||||||
|
descriptionEl.textContent = i18next.t(
|
||||||
|
'Naslednje datoteke niso bile naložene zaradi navedenih razlogov:'
|
||||||
|
)
|
||||||
|
|
||||||
|
modalContentEl.querySelector('ul')?.remove()
|
||||||
|
|
||||||
|
const listEl = document.createElement('ul')
|
||||||
|
modalContentEl.append(listEl)
|
||||||
|
|
||||||
failedUploads.forEach(({ filename, message }) => {
|
failedUploads.forEach(({ filename, message }) => {
|
||||||
const alertText = modalAlert.querySelector('#alert-text')
|
const bulletEl = document.createElement('li')
|
||||||
alertText.textContent = `NAPAKA - Ime datoteke: ${filename}, razlog: ${message}`
|
bulletEl.textContent = `${filename} - ${message}`
|
||||||
modalAlert.toggle()
|
listEl.appendChild(bulletEl)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
modalAlert.toggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleFileClick(e) {
|
async function handleFileClick(e) {
|
||||||
if (e.target.closest('.delete-file')) {
|
if (e.target.closest('.delete-file')) {
|
||||||
|
if (isUploadInProgress) {
|
||||||
|
alert(
|
||||||
|
i18next.t('Brisanje je onemogočeno, doker se nalagajo nove datoteke.')
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (isDeletionInProgress) {
|
||||||
|
alert(i18next.t('Brisanje je onemogočeno, saj je eno še v procesu.'))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isDeletionInProgress = true
|
||||||
const fileEl = e.target.closest('tr')
|
const fileEl = e.target.closest('tr')
|
||||||
const filename = fileEl.querySelector('.filename').textContent
|
const filename = fileEl.querySelector('.file-name').textContent
|
||||||
try {
|
try {
|
||||||
await axios.delete(`${apiEndpointBase}/${filename}`)
|
await axios.delete(`${apiEndpointBase}/${filename}`)
|
||||||
fileEl.remove()
|
fileEl.remove()
|
||||||
|
existingFileNames.splice(existingFileNames.indexOf(filename), 1)
|
||||||
|
reindexFileListEls()
|
||||||
|
isDeletionInProgress = false
|
||||||
} catch {
|
} catch {
|
||||||
alert('Pri brisanju datoteke je prišlo do napake.')
|
alert(i18next.t('Pri brisanju datoteke je prišlo do napake.'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function displayUploadProgress(fileListEl) {
|
||||||
|
const progressEl = fileListEl.querySelector('.upload-progress')
|
||||||
|
return function ({ loaded, total }) {
|
||||||
|
const percentCompleted = Math.round((loaded / total) * 100)
|
||||||
|
progressEl.setAttribute('value', percentCompleted)
|
||||||
|
progressEl.previousElementSibling.textContent = `${percentCompleted}%`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function reindexFileListEls() {
|
||||||
|
fileListElCount = filesListEl.childElementCount
|
||||||
|
for (let i = 0; i < fileListElCount; i++) {
|
||||||
|
const fileListEl = filesListEl.children[i]
|
||||||
|
updateFileListEl(fileListEl, { status: 'reindex', index: i })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -16,15 +16,20 @@ async function onListClick({ target }) {
|
|||||||
modalUseBtn.addEventListener('click', async () => {
|
modalUseBtn.addEventListener('click', async () => {
|
||||||
await axios.delete(`/api/v1/extraction/${extractionId}`)
|
await axios.delete(`/api/v1/extraction/${extractionId}`)
|
||||||
extractionEl.remove()
|
extractionEl.remove()
|
||||||
|
// TODO This (page refresh) is an unneeded workaround for an otherwise simple user friendlier solution.
|
||||||
|
location.reload()
|
||||||
})
|
})
|
||||||
} else if (target.classList.contains('btn-begin')) {
|
} else if (target.closest('.btn-begin')) {
|
||||||
const extractionEl = target.closest('.task')
|
const extractionEl = target.closest('.task')
|
||||||
const extractionId = extractionEl.dataset.id
|
const extractionId = extractionEl.dataset.id
|
||||||
await axios.put(`/api/v1/extraction/${extractionId}/begin`)
|
await axios.put(`/api/v1/extraction/${extractionId}/begin`)
|
||||||
const responseModal = new bootstrap.Modal(
|
const beginExtractionModalEl = document.getElementById('begin-response')
|
||||||
document.getElementById('begin-response')
|
const responseModal = new bootstrap.Modal(beginExtractionModalEl)
|
||||||
)
|
|
||||||
responseModal.toggle()
|
responseModal.toggle()
|
||||||
|
// TODO This (page refresh) is a temporary hack. Replace with proper UI updating logic.
|
||||||
|
beginExtractionModalEl.addEventListener('hide.bs.modal', () =>
|
||||||
|
location.reload()
|
||||||
|
)
|
||||||
} else if (target.classList.contains('btn-duplicate')) {
|
} else if (target.classList.contains('btn-duplicate')) {
|
||||||
const extractionEl = target.closest('.task')
|
const extractionEl = target.closest('.task')
|
||||||
const extractionId = extractionEl.dataset.id
|
const extractionId = extractionEl.dataset.id
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* global $, axios, bootstrap */
|
/* global $, axios, bootstrap, i18next */
|
||||||
|
|
||||||
$('.pick-multiple').select2()
|
$('.pick-multiple').select2()
|
||||||
$('.enter-multiple').select2({
|
$('.enter-multiple').select2({
|
||||||
@@ -41,8 +41,8 @@ async function handleSearch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleSearchError() {
|
function handleSearchError() {
|
||||||
const alertText = modalAlert.querySelector('#alert-text')
|
const alertText = modalAlert._element.querySelector('#alert-text')
|
||||||
alertText.textContent = `NAPAKA pri iskanju`
|
alertText.textContent = i18next.t('NAPAKA pri iskanju')
|
||||||
modalAlert.toggle()
|
modalAlert.toggle()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,128 +0,0 @@
|
|||||||
/* global axios */
|
|
||||||
|
|
||||||
const fileUploadForm = document.forms['upload-files']
|
|
||||||
const fileInputEl = fileUploadForm.querySelector('input[type="file"]')
|
|
||||||
const messageContainerEl = document.getElementById('messages')
|
|
||||||
const filesListEl = document.getElementById('files-list')
|
|
||||||
|
|
||||||
const extractionId = +fileUploadForm.extractionId.value
|
|
||||||
let apiEndpointBase
|
|
||||||
switch (location.pathname.split('/').at(-1)) {
|
|
||||||
case 'besedila':
|
|
||||||
apiEndpointBase = `/api/v1/extraction/${extractionId}/documents`
|
|
||||||
break
|
|
||||||
|
|
||||||
case 'stop-termini':
|
|
||||||
apiEndpointBase = `/api/v1/extraction/${extractionId}/stop-terms`
|
|
||||||
break
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw Error("apiEndpointBase couldn't be determined")
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInputEl.addEventListener('change', submitFiles)
|
|
||||||
filesListEl.addEventListener('click', handleFileClick)
|
|
||||||
|
|
||||||
async function submitFiles() {
|
|
||||||
// TODO Lock additional submits for the duration of this function execution?
|
|
||||||
const MAX_FILE_SIZE = 10 ** 9 // 1 GB
|
|
||||||
const failedUploads = []
|
|
||||||
|
|
||||||
displaySpinner()
|
|
||||||
|
|
||||||
for (const file of fileInputEl.files) {
|
|
||||||
if (file.size > MAX_FILE_SIZE) {
|
|
||||||
const failedUpload = {
|
|
||||||
filename: file.name,
|
|
||||||
message: 'File too large. Must not be over 1 GB.'
|
|
||||||
}
|
|
||||||
failedUploads.push(failedUpload)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
const payload = new FormData()
|
|
||||||
payload.set(fileInputEl.name, file)
|
|
||||||
try {
|
|
||||||
await axios.put(apiEndpointBase, payload)
|
|
||||||
} catch (error) {
|
|
||||||
const failedUpload = {
|
|
||||||
filename: file.name,
|
|
||||||
message: error.response.data
|
|
||||||
}
|
|
||||||
failedUploads.push(failedUpload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { data: files } = await axios.get(apiEndpointBase)
|
|
||||||
updateFilesList(files)
|
|
||||||
} catch {
|
|
||||||
alert('Pri posodobljanju seznama naloženih datotek je prišlo do napake.')
|
|
||||||
}
|
|
||||||
|
|
||||||
fileInputEl.value = ''
|
|
||||||
displayFailedUploads(failedUploads)
|
|
||||||
hideSpinner()
|
|
||||||
}
|
|
||||||
|
|
||||||
function displaySpinner() {
|
|
||||||
const messageEl = document.createElement('li')
|
|
||||||
messageEl.textContent = 'Spinner on'
|
|
||||||
messageContainerEl.appendChild(messageEl)
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideSpinner() {
|
|
||||||
const messageEl = document.createElement('li')
|
|
||||||
messageEl.textContent = 'Spinner off'
|
|
||||||
messageContainerEl.appendChild(messageEl)
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateFilesList(files) {
|
|
||||||
removeAllChildNodes(filesListEl)
|
|
||||||
files.forEach(({ filename, size, timeModified }) => {
|
|
||||||
const fileEl = document.createElement('li')
|
|
||||||
const filenameSpanEl = document.createElement('span')
|
|
||||||
filenameSpanEl.className = 'filename'
|
|
||||||
filenameSpanEl.textContent = filename
|
|
||||||
const formattedDate = new Date(timeModified).toLocaleDateString('sl-SL')
|
|
||||||
const deleteButtonEl = document.createElement('a')
|
|
||||||
deleteButtonEl.className = 'delete-file'
|
|
||||||
deleteButtonEl.href = '#'
|
|
||||||
deleteButtonEl.textContent = 'BRIŠI'
|
|
||||||
fileEl.append(
|
|
||||||
'DATOTEKA - Ime: ',
|
|
||||||
filenameSpanEl,
|
|
||||||
`, velikost: ${size}, datum: ${formattedDate} `,
|
|
||||||
deleteButtonEl
|
|
||||||
)
|
|
||||||
filesListEl.appendChild(fileEl)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function displayFailedUploads(failedUploads) {
|
|
||||||
failedUploads.forEach(({ filename, message }) => {
|
|
||||||
const messageEl = document.createElement('li')
|
|
||||||
messageEl.textContent = `NAPAKA - Ime datoteke: ${filename}, razlog: ${message}`
|
|
||||||
messageContainerEl.appendChild(messageEl)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleFileClick(e) {
|
|
||||||
if (e.target.closest('.delete-file')) {
|
|
||||||
const fileEl = e.target.closest('li')
|
|
||||||
const filename = fileEl.querySelector('.filename').textContent
|
|
||||||
try {
|
|
||||||
await axios.delete(`${apiEndpointBase}/${filename}`)
|
|
||||||
fileEl.remove()
|
|
||||||
} catch {
|
|
||||||
alert('Pri brisanju datoteke je prišlo do napake.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't copy this one into final JS. It's already defined in scripts.js
|
|
||||||
function removeAllChildNodes(parent) {
|
|
||||||
while (parent.firstChild) {
|
|
||||||
parent.removeChild(parent.firstChild)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
/* global axios */
|
|
||||||
|
|
||||||
const extractionListEl = document.getElementById('extraction-list')
|
|
||||||
|
|
||||||
extractionListEl.addEventListener('click', onListClick)
|
|
||||||
|
|
||||||
async function onListClick({ target }) {
|
|
||||||
if (target.classList.contains('btn-delete')) {
|
|
||||||
const extractionEl = target.closest('li')
|
|
||||||
const extractionId = extractionEl.dataset.id
|
|
||||||
await axios.delete(`/api/v1/extraction/${extractionId}`)
|
|
||||||
extractionEl.remove()
|
|
||||||
} else if (target.classList.contains('btn-begin')) {
|
|
||||||
const extractionEl = target.closest('li')
|
|
||||||
const extractionId = extractionEl.dataset.id
|
|
||||||
await axios.put(`/api/v1/extraction/${extractionId}/begin`)
|
|
||||||
} else if (target.classList.contains('btn-duplicate')) {
|
|
||||||
const extractionEl = target.closest('li')
|
|
||||||
const extractionId = extractionEl.dataset.id
|
|
||||||
await axios.post(`/api/v1/extraction/${extractionId}/duplicate`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
/* global $, axios */
|
|
||||||
|
|
||||||
$('.pick-multiple').select2()
|
|
||||||
$('.enter-multiple').select2({
|
|
||||||
tags: true
|
|
||||||
})
|
|
||||||
|
|
||||||
const editStopTermsLink = document.getElementById('edit-stop-terms')
|
|
||||||
const searchButton = document.getElementById('search-btn')
|
|
||||||
const searchResultEl = document.getElementById('search-result')
|
|
||||||
const messageContainerEl = document.getElementById('messages')
|
|
||||||
const formEl = document.forms[0]
|
|
||||||
const extractionId = +location.pathname.split('/').at(-1)
|
|
||||||
|
|
||||||
editStopTermsLink.addEventListener('click', saveOssParamsFirst)
|
|
||||||
searchButton.addEventListener('click', handleSearch)
|
|
||||||
searchResultEl.addEventListener('click', handleSearchResultsClick)
|
|
||||||
|
|
||||||
async function saveOssParamsFirst() {
|
|
||||||
const payload = new URLSearchParams(new FormData(formEl))
|
|
||||||
navigator.sendBeacon(
|
|
||||||
`/api/v1/extraction/${extractionId}/oss-save-params`,
|
|
||||||
payload
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSearch() {
|
|
||||||
displaySpinner()
|
|
||||||
try {
|
|
||||||
const { data } = await submitSearch()
|
|
||||||
displaySearchResults(data)
|
|
||||||
} catch {
|
|
||||||
handleSearchError()
|
|
||||||
}
|
|
||||||
hideSpinner()
|
|
||||||
}
|
|
||||||
|
|
||||||
function displaySpinner() {
|
|
||||||
const messageEl = document.createElement('li')
|
|
||||||
messageEl.textContent = 'Spinner on'
|
|
||||||
messageContainerEl.appendChild(messageEl)
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideSpinner() {
|
|
||||||
const messageEl = document.createElement('li')
|
|
||||||
messageEl.textContent = 'Spinner off'
|
|
||||||
messageContainerEl.appendChild(messageEl)
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSearchError() {
|
|
||||||
const messageEl = document.createElement('li')
|
|
||||||
messageEl.textContent = 'Notify the user of error that occured during search'
|
|
||||||
messageContainerEl.appendChild(messageEl)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitSearch() {
|
|
||||||
const payload = new URLSearchParams(new FormData(formEl))
|
|
||||||
return await axios.put(
|
|
||||||
`/api/v1/extraction/${extractionId}/oss-search`,
|
|
||||||
payload
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function displaySearchResults({ documentCount, canSave }) {
|
|
||||||
removeAllChildNodes(searchResultEl)
|
|
||||||
searchResultEl.textContent = `Število dokumentov: ${documentCount}`
|
|
||||||
if (canSave) {
|
|
||||||
const saveButton = document.createElement('button')
|
|
||||||
saveButton.id = 'save-params'
|
|
||||||
saveButton.textContent = 'Shrani'
|
|
||||||
searchResultEl.append(saveButton)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSearchResultsClick({ target }) {
|
|
||||||
if (target.closest('#save-params')) confirmParams()
|
|
||||||
}
|
|
||||||
|
|
||||||
async function confirmParams() {
|
|
||||||
displaySpinner()
|
|
||||||
try {
|
|
||||||
await axios.put(`/api/v1/extraction/${extractionId}/oss-confirm-params`)
|
|
||||||
location = '../poc'
|
|
||||||
} catch {
|
|
||||||
alert('Error saving params')
|
|
||||||
hideSpinner()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Don't copy this one into final JS. It's already defined in scripts.js
|
|
||||||
function removeAllChildNodes(parent) {
|
|
||||||
while (parent.firstChild) {
|
|
||||||
parent.removeChild(parent.firstChild)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
/* global termCandidates, hitsPerPage, numberOfAllPages */
|
|
||||||
{
|
|
||||||
const resultsListEl = document.getElementById('page-results')
|
|
||||||
|
|
||||||
const updateDemoPager = initPagination('pagination', onPageChange)
|
|
||||||
|
|
||||||
function onPageChange(newPage) {
|
|
||||||
const results = getDataForPage(newPage)
|
|
||||||
removeAllChildNodes(resultsListEl)
|
|
||||||
renderResults(results)
|
|
||||||
updateDemoPager(newPage, numberOfAllPages)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDataForPage(page) {
|
|
||||||
const sliceStart = (page - 1) * hitsPerPage
|
|
||||||
const sliceEnd = page * hitsPerPage
|
|
||||||
const onePageOfTermCandidates = termCandidates.slice(sliceStart, sliceEnd)
|
|
||||||
const data = onePageOfTermCandidates.map((candidate, index) => {
|
|
||||||
const sequentialCount = sliceStart + index + 1
|
|
||||||
return [sequentialCount, candidate]
|
|
||||||
})
|
|
||||||
return data
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderResults(results) {
|
|
||||||
results.forEach(([sequentialCount, candidate]) => {
|
|
||||||
const newListEl = document.createElement('li')
|
|
||||||
newListEl.textContent = `[${sequentialCount}] ${JSON.stringify(
|
|
||||||
candidate
|
|
||||||
)}`
|
|
||||||
resultsListEl.appendChild(newListEl)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Below code is copied from scripts.js, so it will already be available on the real page. No need to copy it there also.
|
|
||||||
function initPagination(paginationRootElId, onPageChange, currentPage = 1) {
|
|
||||||
const rootEl = document.getElementById(paginationRootElId)
|
|
||||||
const btnFirstPage = rootEl.querySelector('.first-page')
|
|
||||||
const btnPreviousPage = rootEl.querySelector('.previous-page')
|
|
||||||
const btnNextPage = rootEl.querySelector('.next-page')
|
|
||||||
const btnLastPage = rootEl.querySelector('.last-page')
|
|
||||||
const formEl = rootEl.querySelector('form')
|
|
||||||
const pageInputEl = formEl.querySelector('input')
|
|
||||||
const pagesCountDisplayEl = formEl.querySelector('.pages-total')
|
|
||||||
|
|
||||||
let reqLock = false
|
|
||||||
|
|
||||||
rootEl.addEventListener('click', handleButtonClick)
|
|
||||||
formEl.addEventListener('submit', handleFormSubmit)
|
|
||||||
|
|
||||||
function handleButtonClick({ target }) {
|
|
||||||
if (reqLock) return
|
|
||||||
|
|
||||||
const buttonEl = target.closest(`#${paginationRootElId} button`)
|
|
||||||
if (!buttonEl) return
|
|
||||||
const numOfAllPages = +pagesCountDisplayEl.textContent
|
|
||||||
|
|
||||||
if (buttonEl.classList.contains('first-page')) {
|
|
||||||
if (currentPage === 1) return
|
|
||||||
enableLock()
|
|
||||||
onPageChange(1)
|
|
||||||
} else if (buttonEl.classList.contains('previous-page')) {
|
|
||||||
if (currentPage === 1) return
|
|
||||||
enableLock()
|
|
||||||
onPageChange(currentPage - 1)
|
|
||||||
} else if (buttonEl.classList.contains('next-page')) {
|
|
||||||
if (currentPage === numOfAllPages) return
|
|
||||||
enableLock()
|
|
||||||
onPageChange(currentPage + 1)
|
|
||||||
} else if (buttonEl.classList.contains('last-page')) {
|
|
||||||
if (currentPage === numOfAllPages) return
|
|
||||||
enableLock()
|
|
||||||
onPageChange(numOfAllPages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleFormSubmit(e) {
|
|
||||||
e.preventDefault()
|
|
||||||
if (reqLock) return
|
|
||||||
|
|
||||||
const inputValue = +pageInputEl.value
|
|
||||||
if (!(inputValue > 0 && inputValue <= pagesCountDisplayEl.textContent)) {
|
|
||||||
alert('Nepravilna vrednost strani')
|
|
||||||
pageInputEl.value = currentPage
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
enableLock()
|
|
||||||
onPageChange(inputValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
function enableLock() {
|
|
||||||
reqLock = true
|
|
||||||
btnFirstPage.disabled = true
|
|
||||||
btnPreviousPage.disabled = true
|
|
||||||
btnNextPage.disabled = true
|
|
||||||
btnLastPage.disabled = true
|
|
||||||
pageInputEl.disabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function disableLock() {
|
|
||||||
reqLock = false
|
|
||||||
btnFirstPage.disabled = false
|
|
||||||
btnPreviousPage.disabled = false
|
|
||||||
btnNextPage.disabled = false
|
|
||||||
btnLastPage.disabled = false
|
|
||||||
pageInputEl.disabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function updatePagerUi(newCurrentPage, newNumOfAllPages) {
|
|
||||||
disableLock()
|
|
||||||
if (!newCurrentPage) return
|
|
||||||
|
|
||||||
currentPage = newCurrentPage
|
|
||||||
pageInputEl.value = newCurrentPage
|
|
||||||
pagesCountDisplayEl.textContent = newNumOfAllPages
|
|
||||||
|
|
||||||
if (newCurrentPage === 1) {
|
|
||||||
btnFirstPage.disabled = true
|
|
||||||
btnPreviousPage.disabled = true
|
|
||||||
} else {
|
|
||||||
btnFirstPage.disabled = false
|
|
||||||
btnPreviousPage.disabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newCurrentPage === newNumOfAllPages) {
|
|
||||||
btnNextPage.disabled = true
|
|
||||||
btnLastPage.disabled = true
|
|
||||||
} else {
|
|
||||||
btnNextPage.disabled = false
|
|
||||||
btnLastPage.disabled = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatePagerUi
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper function to easily remove all child nodes. Useful for pagination.
|
|
||||||
function removeAllChildNodes(parent) {
|
|
||||||
while (parent.firstChild) {
|
|
||||||
parent.removeChild(parent.firstChild)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
tdId.textContent = sequentialCount
|
tdId.textContent = sequentialCount
|
||||||
tdName.textContent = candidate.kanonicnaoblika
|
tdName.textContent = candidate.kanonicnaoblika
|
||||||
tdSize.textContent = candidate.ranking
|
tdSize.textContent = candidate.ranking
|
||||||
tdDate.textContent = candidate.pogostostpojavljanja
|
tdDate.textContent = candidate.pogostostpojavljanja[0]
|
||||||
rowEl.append(tdId, tdName, tdSize, tdDate)
|
rowEl.append(tdId, tdName, tdSize, tdDate)
|
||||||
resultsListEl.appendChild(rowEl)
|
resultsListEl.appendChild(rowEl)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* global currentPagePath */
|
/* global currentPagePath, i18next */
|
||||||
|
|
||||||
// const currentPagePath = location.pathname
|
// const currentPagePath = location.pathname
|
||||||
|
|
||||||
@@ -103,22 +103,22 @@ function initExtraction() {
|
|||||||
btnStart.type = 'button'
|
btnStart.type = 'button'
|
||||||
btnImg.src = '/images/fi_arrow-right-circle.svg'
|
btnImg.src = '/images/fi_arrow-right-circle.svg'
|
||||||
btnSpan.className = 'ms-1'
|
btnSpan.className = 'ms-1'
|
||||||
btnSpan.textContent = 'Začni'
|
btnSpan.textContent = i18next.t('Začni')
|
||||||
hr.className = 'mt-2 mb-3'
|
hr.className = 'mt-2 mb-3'
|
||||||
divSmFlex2.className = 'd-flex justify-content-between'
|
divSmFlex2.className = 'd-flex justify-content-between'
|
||||||
divSmFlexACMb.className = 'd-sm-flex align-items-center mb-0'
|
divSmFlexACMb.className = 'd-sm-flex align-items-center mb-0'
|
||||||
imgAlertCircle.src = '/images/alert-circle.svg'
|
imgAlertCircle.src = '/images/alert-circle.svg'
|
||||||
spanNew.className = 'normal-gray ms-1'
|
spanNew.className = 'normal-gray ms-1'
|
||||||
spanNew.textContent = 'Nov'
|
spanNew.textContent = i18next.t('Nov')
|
||||||
divSmFlexACMbMe.className = 'd-sm-flex align-content-center mb-0 me-3'
|
divSmFlexACMbMe.className = 'd-sm-flex align-content-center mb-0 me-3'
|
||||||
divEditTask.className = 'align-items-center me-3 edit-task'
|
divEditTask.className = 'align-items-center me-3 edit-task'
|
||||||
imgEditAlt.src = '/images/u_edit-alt.svg'
|
imgEditAlt.src = '/images/u_edit-alt.svg'
|
||||||
spanEdit.className = 'ms-1 normal-gray'
|
spanEdit.className = 'ms-1 normal-gray'
|
||||||
spanEdit.textContent = 'Uredi'
|
spanEdit.textContent = i18next.t('Uredi')
|
||||||
divDeleteTask.className = 'ms-3 align-items-center delete-task'
|
divDeleteTask.className = 'ms-3 align-items-center delete-task'
|
||||||
imgDeleteAlt.src = '/images/red-trash-icon.svg'
|
imgDeleteAlt.src = '/images/red-trash-icon.svg'
|
||||||
spanDelete.className = 'ms-1 normal-gray'
|
spanDelete.className = 'ms-1 normal-gray'
|
||||||
spanDelete.textContent = 'Briši'
|
spanDelete.textContent = i18next.t('Briši')
|
||||||
|
|
||||||
taskNewContainer.appendChild(divSmFlex)
|
taskNewContainer.appendChild(divSmFlex)
|
||||||
divSmFlex.appendChild(divSmGrid)
|
divSmFlex.appendChild(divSmGrid)
|
||||||
@@ -233,7 +233,7 @@ function mobileMoveContent() {
|
|||||||
primaryButton.style.whiteSpace = ''
|
primaryButton.style.whiteSpace = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
navTitle.textContent = 'Urejanje'
|
navTitle.textContent = i18next.t('Urejanje')
|
||||||
siteHeading.style.display = 'block'
|
siteHeading.style.display = 'block'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* global axios, validator, removeJumpLogic */
|
/* global axios, validator, $, i18next */
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ const listenRegisterBtn = event => {
|
|||||||
loginHeader.className = loginHeader.className + ' d-none'
|
loginHeader.className = loginHeader.className + ' d-none'
|
||||||
regDescription.className = regDescription.className.replace('d-none', '')
|
regDescription.className = regDescription.className.replace('d-none', '')
|
||||||
loginDescription.className = loginDescription.className + ' d-none'
|
loginDescription.className = loginDescription.className + ' d-none'
|
||||||
|
clearErrorView()
|
||||||
}
|
}
|
||||||
|
|
||||||
const listenLoginBtn = event => {
|
const listenLoginBtn = event => {
|
||||||
@@ -38,6 +39,7 @@ const listenLoginBtn = event => {
|
|||||||
regHeader.className = regHeader.className + ' d-none'
|
regHeader.className = regHeader.className + ' d-none'
|
||||||
loginDescription.className = loginDescription.className.replace('d-none', '')
|
loginDescription.className = loginDescription.className.replace('d-none', '')
|
||||||
regDescription.className = regDescription.className + ' d-none'
|
regDescription.className = regDescription.className + ' d-none'
|
||||||
|
clearErrorView()
|
||||||
}
|
}
|
||||||
|
|
||||||
regBtn.addEventListener('click', listenRegisterBtn)
|
regBtn.addEventListener('click', listenRegisterBtn)
|
||||||
@@ -106,6 +108,23 @@ function updateLoginRegisterErrorView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearErrorView() {
|
||||||
|
Object.keys(registerErrorIndicatorpairs).forEach(key => {
|
||||||
|
registerErrorIndicatorpairs[key][0].style.visibility = 'hidden'
|
||||||
|
registerErrorIndicatorpairs[key][1].style.visibility = 'hidden'
|
||||||
|
})
|
||||||
|
|
||||||
|
document.querySelectorAll('.error-login-icon').forEach(e => {
|
||||||
|
e.style.visibility = 'hidden'
|
||||||
|
})
|
||||||
|
|
||||||
|
document.querySelector('.error-text').style.visibility = 'hidden'
|
||||||
|
|
||||||
|
document.getElementById('login-remember').checked = false
|
||||||
|
document.getElementById('terms-of-use').checked = false
|
||||||
|
document.getElementById('privacy-policy').checked = false
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Show error message from the label that represents the error returned from the server
|
Show error message from the label that represents the error returned from the server
|
||||||
Includes error X icons for login
|
Includes error X icons for login
|
||||||
@@ -152,14 +171,23 @@ function updateRegisterWindowOnSuccess(
|
|||||||
footer.appendChild(document.createElement('button'))
|
footer.appendChild(document.createElement('button'))
|
||||||
const btn = footer.firstElementChild
|
const btn = footer.firstElementChild
|
||||||
btn.classList = 'btn btn-secondary text-secondary'
|
btn.classList = 'btn btn-secondary text-secondary'
|
||||||
btn.textContent = 'ZAPRI'
|
btn.textContent = i18next.t('ZAPRI')
|
||||||
btn.ariaLabel = 'Close'
|
btn.ariaLabel = 'Close'
|
||||||
btn.dataset.bsDismiss = 'modal'
|
btn.dataset.bsDismiss = 'modal'
|
||||||
const successDescriptionPararaph = body.children[1]
|
const successDescriptionPararaph = body.children[1]
|
||||||
|
|
||||||
successDescriptionPararaph.textContent = `Pozdravljeni ${name} ${surname},
|
// successDescriptionPararaph.textContent = `Pozdravljeni ${name} ${surname},
|
||||||
na vaš elektronski naslov smo vam poslali sporočilo s povezavo, s katero boste potrdili svoj uporabniški račun na Terminološkem portalu.
|
// na vaš elektronski naslov smo vam poslali sporočilo s povezavo, s katero boste potrdili svoj uporabniški račun na Terminološkem portalu.
|
||||||
Povezava za potrditev je veljavna ${days} dni.`
|
// Povezava za potrditev je veljavna ${days} dni.`
|
||||||
|
|
||||||
|
successDescriptionPararaph.textContent =
|
||||||
|
i18next.t('Pozdravljeni ') +
|
||||||
|
`${name} ${surname}, ` +
|
||||||
|
i18next.t(
|
||||||
|
'na vaš elektronski naslov smo vam poslali sporočilo s povezavo, s katero boste potrdili svoj uporabniški račun na Terminološkem portalu. Povezava za potrditev je veljavna '
|
||||||
|
) +
|
||||||
|
`${days} ` +
|
||||||
|
i18next.t('dni.')
|
||||||
}
|
}
|
||||||
|
|
||||||
// login and register
|
// login and register
|
||||||
@@ -199,7 +227,7 @@ function updateRegisterWindowOnSuccess(
|
|||||||
updateLoginRegisterErrorView(
|
updateLoginRegisterErrorView(
|
||||||
registerErrorLabels[iterator],
|
registerErrorLabels[iterator],
|
||||||
true,
|
true,
|
||||||
'Prazno obvezno polje'
|
i18next.t('Prazno obvezno polje')
|
||||||
)
|
)
|
||||||
failFrontendValidation = true
|
failFrontendValidation = true
|
||||||
} else {
|
} else {
|
||||||
@@ -215,7 +243,7 @@ function updateRegisterWindowOnSuccess(
|
|||||||
updateLoginRegisterErrorView(
|
updateLoginRegisterErrorView(
|
||||||
'error-email',
|
'error-email',
|
||||||
true,
|
true,
|
||||||
'Neveljavna e-pošta'
|
i18next.t('Neveljavna e-pošta')
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
updateLoginRegisterErrorView('error-email', false)
|
updateLoginRegisterErrorView('error-email', false)
|
||||||
@@ -226,7 +254,7 @@ function updateRegisterWindowOnSuccess(
|
|||||||
updateLoginRegisterErrorView(
|
updateLoginRegisterErrorView(
|
||||||
'error-password',
|
'error-password',
|
||||||
true,
|
true,
|
||||||
'Geslo je prekratko'
|
i18next.t('Geslo je prekratko')
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
updateLoginRegisterErrorView('error-password', false)
|
updateLoginRegisterErrorView('error-password', false)
|
||||||
@@ -237,7 +265,7 @@ function updateRegisterWindowOnSuccess(
|
|||||||
updateLoginRegisterErrorView(
|
updateLoginRegisterErrorView(
|
||||||
'error-password-repeat',
|
'error-password-repeat',
|
||||||
true,
|
true,
|
||||||
'Geslo se ne ujema'
|
i18next.t('Geslo se ne ujema')
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,11 +291,11 @@ function updateRegisterWindowOnSuccess(
|
|||||||
|
|
||||||
// event.target.messageBind.textContent = data
|
// event.target.messageBind.textContent = data
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let message = 'Prišlo je do napake.'
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
if (error.response) {
|
if (error.response) {
|
||||||
message = error.response.data
|
message = error.response.data
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
}
|
}
|
||||||
|
|
||||||
showErrorReturnedFromServer(
|
showErrorReturnedFromServer(
|
||||||
@@ -279,3 +307,28 @@ function updateRegisterWindowOnSuccess(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function registerConditions() {
|
||||||
|
// Requirement 1: Terms of Use Checkbox checked
|
||||||
|
const requirement1 = document.getElementById('terms-of-use')
|
||||||
|
// Requirement 2: Privacy Policy checked
|
||||||
|
const requirement2 = document.getElementById('privacy-policy')
|
||||||
|
|
||||||
|
const registerButton = document.getElementById('regbtnmain')
|
||||||
|
if (requirement1.checked && requirement2.checked) {
|
||||||
|
registerButton.disabled = false
|
||||||
|
} else {
|
||||||
|
registerButton.disabled = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById('terms-of-use')
|
||||||
|
.addEventListener('change', registerConditions)
|
||||||
|
document
|
||||||
|
.getElementById('privacy-policy')
|
||||||
|
.addEventListener('change', registerConditions)
|
||||||
|
|
||||||
|
$('#staticBackdrop').on('hidden.bs.modal', function () {
|
||||||
|
clearErrorView()
|
||||||
|
})
|
||||||
|
|||||||
@@ -154,6 +154,21 @@ function searchQuery(searchString, searchFilterDOM = {}) {
|
|||||||
document.location.href = url
|
document.location.href = url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function searchQueryWithExistingFilters(searchString) {
|
||||||
|
const stringBuilder = `/iskanje?q=${searchString}`
|
||||||
|
const thisUrl = new URL(location)
|
||||||
|
const url = new URL(stringBuilder, location.protocol + '//' + location.host)
|
||||||
|
|
||||||
|
thisUrl.searchParams.forEach((value, key, parent) => {
|
||||||
|
// console.log(`VALUE: ${value}`)
|
||||||
|
if (!(key === 'q' || key === 'p')) {
|
||||||
|
url.searchParams.append(key, value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
document.location.href = url
|
||||||
|
}
|
||||||
|
|
||||||
// end search implementation functions
|
// end search implementation functions
|
||||||
|
|
||||||
// search filter functions
|
// search filter functions
|
||||||
@@ -173,7 +188,11 @@ function sbmFn(sbm) {
|
|||||||
inputString = '*'
|
inputString = '*'
|
||||||
}
|
}
|
||||||
|
|
||||||
searchQuery(inputString, searchFilterDOM)
|
if (sbm === document.querySelector('.search-btn-a')) {
|
||||||
|
searchQuery(inputString, searchFilterDOM)
|
||||||
|
} else {
|
||||||
|
searchQueryWithExistingFilters(inputString)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,42 @@
|
|||||||
const allMixedContentFields = document.querySelectorAll('.mc-field')
|
const allMixedContentFields = document.querySelectorAll('.mc-field')
|
||||||
|
|
||||||
allMixedContentFields.forEach(el =>
|
allMixedContentFields.forEach(el =>
|
||||||
el.addEventListener('focusin', () => showMCButtons(el))
|
el.addEventListener('focus', () => showMCButtons(el))
|
||||||
)
|
)
|
||||||
|
|
||||||
document.addEventListener('keypress', e => {
|
document.addEventListener('keypress', e => {
|
||||||
if (e.key === 'Enter' && e.target.classList.contains('dispatch-tab')) {
|
if (e.key === 'Enter' && e.target.classList.contains('dispatch-tab')) {
|
||||||
const form = event.target.form
|
|
||||||
const index = Array.prototype.indexOf.call(form, e.target)
|
|
||||||
form.elements[index + 1]?.focus()
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
const currentNode = e.target
|
||||||
|
const allElements = document.querySelectorAll('input, select, textarea')
|
||||||
|
const currentIndex = [...allElements].findIndex(el =>
|
||||||
|
currentNode.isEqualNode(el)
|
||||||
|
)
|
||||||
|
const targetIndex = (currentIndex + 1) % allElements.length
|
||||||
|
allElements[targetIndex].focus()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
function showMCButtons(element) {
|
function showMCButtons(element, linkTerm) {
|
||||||
const parent = element.parentElement.parentElement.parentElement
|
let parent
|
||||||
|
if (linkTerm) {
|
||||||
|
parent = element.parentElement.parentElement.parentElement.parentElement
|
||||||
|
} else {
|
||||||
|
parent = element.parentElement.parentElement.parentElement
|
||||||
|
}
|
||||||
|
const mainContent = document.getElementById('offset-main')
|
||||||
|
const sideMenu = document.querySelector('.content-nav-content')
|
||||||
const btnGrp = parent.querySelector('.mc-buttons-group')
|
const btnGrp = parent.querySelector('.mc-buttons-group')
|
||||||
|
// Warning, do not touch *_*
|
||||||
|
document.addEventListener('click', function (event) {
|
||||||
|
if (mainContent.contains(event.target) || sideMenu.contains(event.target)) {
|
||||||
|
if (element !== event.target && document.activeElement !== element) {
|
||||||
|
if (!parent.contains(event.target)) {
|
||||||
|
btnGrp.classList.add('d-none')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
btnGrp.classList.remove('d-none')
|
btnGrp.classList.remove('d-none')
|
||||||
const btnGrpChildren = btnGrp.children
|
const btnGrpChildren = btnGrp.children
|
||||||
const childrenBtns = Array.from(btnGrpChildren)
|
const childrenBtns = Array.from(btnGrpChildren)
|
||||||
@@ -29,10 +50,10 @@ function showMCButtons(element) {
|
|||||||
{ once: true }
|
{ once: true }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
document.addEventListener('click', function (event) {
|
|
||||||
if (parent !== event.target && !parent.contains(event.target)) {
|
document.addEventListener('keyup', function (e) {
|
||||||
btnGrp.classList.add('d-none')
|
const selectedEl = document.activeElement
|
||||||
}
|
if (!parent.contains(selectedEl)) btnGrp.classList.add('d-none')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,8 +71,9 @@ function addMixedContentInput(el, selectedEl) {
|
|||||||
inputText.substring(0, selectedStart) +
|
inputText.substring(0, selectedStart) +
|
||||||
'<b>' +
|
'<b>' +
|
||||||
inputText.substring(selectedStart, selectedEnd) +
|
inputText.substring(selectedStart, selectedEnd) +
|
||||||
'</b> ' +
|
'</b>' +
|
||||||
inputText.substring(selectedEnd)
|
inputText.substring(selectedEnd)
|
||||||
|
placeCaret(selectedEl, selectedEnd + 7)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.classList.contains('mc-italic') && selectedText.length) {
|
if (el.classList.contains('mc-italic') && selectedText.length) {
|
||||||
@@ -59,8 +81,9 @@ function addMixedContentInput(el, selectedEl) {
|
|||||||
inputText.substring(0, selectedStart) +
|
inputText.substring(0, selectedStart) +
|
||||||
'<i>' +
|
'<i>' +
|
||||||
inputText.substring(selectedStart, selectedEnd) +
|
inputText.substring(selectedStart, selectedEnd) +
|
||||||
'</i> ' +
|
'</i>' +
|
||||||
inputText.substring(selectedEnd)
|
inputText.substring(selectedEnd)
|
||||||
|
placeCaret(selectedEl, selectedEnd + 7)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.classList.contains('mc-supscript') && selectedText.length) {
|
if (el.classList.contains('mc-supscript') && selectedText.length) {
|
||||||
@@ -68,8 +91,9 @@ function addMixedContentInput(el, selectedEl) {
|
|||||||
inputText.substring(0, selectedStart) +
|
inputText.substring(0, selectedStart) +
|
||||||
'<sup>' +
|
'<sup>' +
|
||||||
inputText.substring(selectedStart, selectedEnd) +
|
inputText.substring(selectedStart, selectedEnd) +
|
||||||
'</sup> ' +
|
'</sup>' +
|
||||||
inputText.substring(selectedEnd)
|
inputText.substring(selectedEnd)
|
||||||
|
placeCaret(selectedEl, selectedEnd + 11)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.classList.contains('mc-subscript') && selectedText.length) {
|
if (el.classList.contains('mc-subscript') && selectedText.length) {
|
||||||
@@ -77,27 +101,41 @@ function addMixedContentInput(el, selectedEl) {
|
|||||||
inputText.substring(0, selectedStart) +
|
inputText.substring(0, selectedStart) +
|
||||||
'<sub>' +
|
'<sub>' +
|
||||||
inputText.substring(selectedStart, selectedEnd) +
|
inputText.substring(selectedStart, selectedEnd) +
|
||||||
'</sub> ' +
|
'</sub>' +
|
||||||
inputText.substring(selectedEnd)
|
inputText.substring(selectedEnd)
|
||||||
|
placeCaret(selectedEl, selectedEnd + 11)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.classList.contains('mc-hyperlink') && selectedText.length) {
|
if (el.classList.contains('mc-hyperlink') && selectedText.length) {
|
||||||
selectedEl.value =
|
selectedEl.value =
|
||||||
inputText.substring(0, selectedStart) +
|
inputText.substring(0, selectedStart) +
|
||||||
'<link url="">' +
|
'<a href="">' +
|
||||||
inputText.substring(selectedStart, selectedEnd) +
|
inputText.substring(selectedStart, selectedEnd) +
|
||||||
'</link> ' +
|
'</a>' +
|
||||||
inputText.substring(selectedEnd)
|
inputText.substring(selectedEnd)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (el.classList.contains('mc-line-break')) {
|
if (el.classList.contains('mc-line-break')) {
|
||||||
selectedEl.value =
|
selectedEl.value =
|
||||||
inputText.substring(0, selectedStart) +
|
inputText.substring(0, selectedStart) +
|
||||||
'<br />' +
|
'<br/>' +
|
||||||
inputText.substring(selectedEnd)
|
inputText.substring(selectedEnd)
|
||||||
|
placeCaret(selectedEl, selectedEnd + 5)
|
||||||
el.removeEventListener('click', () => addMixedContentInput, false)
|
el.removeEventListener('click', () => addMixedContentInput, false)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// selectedEl.focus()
|
function placeCaret(elem, caretPos) {
|
||||||
// selectedEl.setSelectionRange(selectedStart, selectedEnd)
|
if (elem != null) {
|
||||||
|
if (elem.createTextRange) {
|
||||||
|
const range = elem.createTextRange()
|
||||||
|
range.move('character', caretPos)
|
||||||
|
range.select()
|
||||||
|
} else {
|
||||||
|
if (elem.selectionStart) {
|
||||||
|
elem.focus()
|
||||||
|
elem.setSelectionRange(caretPos, caretPos)
|
||||||
|
} else elem.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,8 @@
|
|||||||
return await axios.get(url)
|
return await axios.get(url)
|
||||||
} */
|
} */
|
||||||
|
|
||||||
|
// TODO MARK FOR REVIEW WHETHER THIS METHODS ARE STILL NEEDED
|
||||||
|
|
||||||
function prepareQueryArrayForArrayWithIDs(page, searchQuery, filters = {}) {
|
function prepareQueryArrayForArrayWithIDs(page, searchQuery, filters = {}) {
|
||||||
const qParams = new URL(location).searchParams
|
const qParams = new URL(location).searchParams
|
||||||
qParams.set('p', page)
|
qParams.set('p', page)
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/* global isI18nReady, i18next, $, axios */
|
||||||
|
|
||||||
|
isI18nReady.then(t => {
|
||||||
|
const alertText = document.querySelector('#alert-text')
|
||||||
|
alertText.textContent = t(
|
||||||
|
'Ali res želite izbrisati svoj račun? S tem boste trajno izgubili dostop do podatkov, ki ste jih ustvarili.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const redConfirm = document.querySelector('#modal-use-btn')
|
||||||
|
redConfirm.style.backgroundColor = '#AC7171'
|
||||||
|
document.querySelector('#modal-alert-label').style.color = '#AC7171'
|
||||||
|
|
||||||
|
redConfirm.addEventListener('click', async () => {
|
||||||
|
// TODO: Optimize, create a common helper function for example
|
||||||
|
function displayError(error) {
|
||||||
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
|
if (error.response) {
|
||||||
|
message = error.response.data
|
||||||
|
} else if (error.request) {
|
||||||
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('#info-text').textContent = message
|
||||||
|
$('#info-modal').modal('show')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await axios.delete('/api/v1/users/current')
|
||||||
|
window.location = '/'
|
||||||
|
} catch (error) {
|
||||||
|
displayError(error)
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// Note to myself: please optimize and remove the redundacy of the code written in multiple classes to achieve the same result as this
|
||||||
|
|
||||||
|
const focusOnCompletePromtBase = queryInput => {
|
||||||
|
queryInput.focus()
|
||||||
|
queryInput.select()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tradeoff: Declare a variable for the element to reduce duplication?
|
||||||
|
const inputQuery = document.querySelector('#search-query')
|
||||||
|
if (inputQuery) {
|
||||||
|
focusOnCompletePromtBase(inputQuery)
|
||||||
|
|
||||||
|
if (window.location.pathname === '/slovarji') {
|
||||||
|
document
|
||||||
|
.querySelector('#dicts-search-btn')
|
||||||
|
.addEventListener('click', () => {
|
||||||
|
focusOnCompletePromtBase(inputQuery)
|
||||||
|
})
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelector('#search-in-filter-modal')
|
||||||
|
.addEventListener('click', () => {
|
||||||
|
focusOnCompletePromtBase(inputQuery)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
/* global $, axios, validator, i18next */
|
||||||
|
|
||||||
|
// Function to verify password and repeat password
|
||||||
|
function verifyPassword() {
|
||||||
|
const password = document.getElementById('reset-password').value
|
||||||
|
const repeatPassword = document.getElementById('reset-password-repeat').value
|
||||||
|
|
||||||
|
if (password !== repeatPassword) {
|
||||||
|
// alert(i18next.t('Gesli se ne ujemata')) // 'Passwords do not match')
|
||||||
|
document.querySelector('#reset-password-error').textContent = i18next.t(
|
||||||
|
'Gesli se ne ujemata'
|
||||||
|
)
|
||||||
|
document.querySelector('#reset-password-error').style.visibility = 'visible'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!validator.isLength(password, { min: 8 })) {
|
||||||
|
// alert(i18next.t('Geslo je prekratko')) // 'Passwords do not match')
|
||||||
|
document.querySelector('#reset-password-error').textContent =
|
||||||
|
i18next.t('Geslo je prekratko')
|
||||||
|
document.querySelector('#reset-password-error').style.visibility = 'visible'
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('#reset-password-error').style.visibility = 'invisible'
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle submit event
|
||||||
|
document
|
||||||
|
.querySelector('#reset-and-redirect')
|
||||||
|
.addEventListener('submit', async event => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (verifyPassword()) {
|
||||||
|
const token = document.getElementById('token').value
|
||||||
|
const password = document.getElementById('reset-password').value
|
||||||
|
const passwordRepeat = document.getElementById(
|
||||||
|
'reset-password-repeat'
|
||||||
|
).value
|
||||||
|
// const email = document.getElementById('email').value
|
||||||
|
// const data = { password, repeatPassword, token, email }
|
||||||
|
try {
|
||||||
|
await axios.post('/api/v1/users/reset-password-submit', {
|
||||||
|
token,
|
||||||
|
password,
|
||||||
|
passwordRepeat
|
||||||
|
})
|
||||||
|
|
||||||
|
window.location = '/'
|
||||||
|
} catch (error) {
|
||||||
|
displayError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
function displayError(error) {
|
||||||
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
|
if (error.response) {
|
||||||
|
message = error.response.data
|
||||||
|
} else if (error.request) {
|
||||||
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelector('#fpi-text.normal-gray').textContent = message
|
||||||
|
$('#reset-pass-info').modal('show')
|
||||||
|
}
|
||||||
@@ -1,4 +1,14 @@
|
|||||||
/* global $, bootstrap */
|
/* global $, bootstrap, axios, i18next, i18nextHttpBackend */
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const isI18nReady = i18next.use(i18nextHttpBackend).init({
|
||||||
|
lng: document.documentElement.lang ?? 'sl',
|
||||||
|
ns: 'core',
|
||||||
|
nsSeparator: false,
|
||||||
|
keySeparator: false,
|
||||||
|
...(window.inDevEnv && { saveMissing: true }),
|
||||||
|
...(!window.inDevEnv && { fallbackLng: false })
|
||||||
|
})
|
||||||
|
|
||||||
const windows = ['#suggestions-root', '.advanced-search-root', '.kbd-root']
|
const windows = ['#suggestions-root', '.advanced-search-root', '.kbd-root']
|
||||||
|
|
||||||
@@ -302,7 +312,7 @@ function renderSuggestions(
|
|||||||
|
|
||||||
ajustElementSettings(preElement, options)
|
ajustElementSettings(preElement, options)
|
||||||
|
|
||||||
preElement.textContent = 'Za to besedo še ni podatkov.'
|
preElement.textContent = i18next.t('Za to besedo še ni podatkov.')
|
||||||
preElement.disabled = true
|
preElement.disabled = true
|
||||||
preElement.classList = ['w-100']
|
preElement.classList = ['w-100']
|
||||||
preElement.classList += ` ${type}-sugg`
|
preElement.classList += ` ${type}-sugg`
|
||||||
@@ -646,31 +656,53 @@ document.querySelectorAll('.loginregisterlabel').forEach(label => {
|
|||||||
/**
|
/**
|
||||||
* Enables pagination logic for the specified pager UI.
|
* Enables pagination logic for the specified pager UI.
|
||||||
*
|
*
|
||||||
* @param {string} paginationRootElId - Pager element id.
|
* @param {(string|string[])} paginationRootElIds - Pager element id.
|
||||||
* @param {function} onPageChange - Gets called with the number of the page the user requested.
|
* @param {function} onPageChange - Gets called with the number of the page the user requested.
|
||||||
* @returns {function} - Call it with (newCurrentPage, newNumOfAllPages) to update the ui. Or without arguments, after handling a potential error.
|
* @returns {function} - Call it with (newCurrentPage, newNumOfAllPages) to update the ui. Or without arguments, after handling a potential error.
|
||||||
*/
|
*/
|
||||||
function initPagination(paginationRootElId, onPageChange, currentPage = 1) {
|
function initPagination(paginationRootElIds, onPageChange, currentPage = 1) {
|
||||||
const rootEl = document.getElementById(paginationRootElId)
|
|
||||||
const btnFirstPage = rootEl.querySelector('.first-page')
|
|
||||||
const btnPreviousPage = rootEl.querySelector('.previous-page')
|
|
||||||
const btnNextPage = rootEl.querySelector('.next-page')
|
|
||||||
const btnLastPage = rootEl.querySelector('.last-page')
|
|
||||||
const formEl = rootEl.querySelector('form')
|
|
||||||
const pageInputEl = formEl.querySelector('input')
|
|
||||||
const pagesCountDisplayEl = formEl.querySelector('.pages-total')
|
|
||||||
|
|
||||||
let reqLock = false
|
let reqLock = false
|
||||||
|
let numOfAllPages
|
||||||
|
const backwardBtnEls = []
|
||||||
|
const forwardBtnEls = []
|
||||||
|
const pageInputEls = []
|
||||||
|
const pagesCountDisplayEls = []
|
||||||
|
|
||||||
rootEl.addEventListener('click', handleButtonClick)
|
if (Array.isArray(paginationRootElIds)) {
|
||||||
formEl.addEventListener('submit', handleFormSubmit)
|
paginationRootElIds.forEach(id => initControls(id))
|
||||||
|
} else {
|
||||||
|
initControls(paginationRootElIds)
|
||||||
|
}
|
||||||
|
const allControlEls = [...backwardBtnEls, ...forwardBtnEls, ...pageInputEls]
|
||||||
|
|
||||||
function handleButtonClick({ target }) {
|
function initControls(paginationRootElId) {
|
||||||
|
const rootEl = document.getElementById(paginationRootElId)
|
||||||
|
const btnFirstPage = rootEl.querySelector('.first-page')
|
||||||
|
const btnPreviousPage = rootEl.querySelector('.previous-page')
|
||||||
|
const btnNextPage = rootEl.querySelector('.next-page')
|
||||||
|
const btnLastPage = rootEl.querySelector('.last-page')
|
||||||
|
const formEl = rootEl.querySelector('form')
|
||||||
|
const pageInputEl = formEl.querySelector('input')
|
||||||
|
const pagesCountDisplayEl = formEl.querySelector('.pages-total')
|
||||||
|
|
||||||
|
numOfAllPages = +pagesCountDisplayEl.textContent
|
||||||
|
|
||||||
|
backwardBtnEls.push(btnFirstPage, btnPreviousPage)
|
||||||
|
forwardBtnEls.push(btnNextPage, btnLastPage)
|
||||||
|
pageInputEls.push(pageInputEl)
|
||||||
|
pagesCountDisplayEls.push(pagesCountDisplayEl)
|
||||||
|
|
||||||
|
rootEl.addEventListener('click', e => {
|
||||||
|
handleButtonClick(e, paginationRootElId)
|
||||||
|
})
|
||||||
|
formEl.addEventListener('submit', e => handleFormSubmit(e, pageInputEl))
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleButtonClick({ target }, paginationRootElId) {
|
||||||
if (reqLock) return
|
if (reqLock) return
|
||||||
|
|
||||||
const buttonEl = target.closest(`#${paginationRootElId} button`)
|
const buttonEl = target.closest(`#${paginationRootElId} button`)
|
||||||
if (!buttonEl) return
|
if (!buttonEl) return
|
||||||
const numOfAllPages = +pagesCountDisplayEl.textContent
|
|
||||||
|
|
||||||
if (buttonEl.classList.contains('first-page')) {
|
if (buttonEl.classList.contains('first-page')) {
|
||||||
if (currentPage === 1) return
|
if (currentPage === 1) return
|
||||||
@@ -691,13 +723,13 @@ function initPagination(paginationRootElId, onPageChange, currentPage = 1) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFormSubmit(e) {
|
function handleFormSubmit(e, pageInputEl) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
if (reqLock) return
|
if (reqLock) return
|
||||||
|
|
||||||
const inputValue = +pageInputEl.value
|
const inputValue = +pageInputEl.value
|
||||||
if (!(inputValue > 0 && inputValue <= pagesCountDisplayEl.textContent)) {
|
if (!(inputValue > 0 && inputValue <= numOfAllPages)) {
|
||||||
alert('Nepravilna vrednost strani')
|
alert(i18next.t('Nepravilna vrednost strani'))
|
||||||
pageInputEl.value = currentPage
|
pageInputEl.value = currentPage
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -708,20 +740,12 @@ function initPagination(paginationRootElId, onPageChange, currentPage = 1) {
|
|||||||
|
|
||||||
function enableLock() {
|
function enableLock() {
|
||||||
reqLock = true
|
reqLock = true
|
||||||
btnFirstPage.disabled = true
|
allControlEls.forEach(el => (el.disabled = true))
|
||||||
btnPreviousPage.disabled = true
|
|
||||||
btnNextPage.disabled = true
|
|
||||||
btnLastPage.disabled = true
|
|
||||||
pageInputEl.disabled = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function disableLock() {
|
function disableLock() {
|
||||||
reqLock = false
|
reqLock = false
|
||||||
btnFirstPage.disabled = false
|
allControlEls.forEach(el => (el.disabled = false))
|
||||||
btnPreviousPage.disabled = false
|
|
||||||
btnNextPage.disabled = false
|
|
||||||
btnLastPage.disabled = false
|
|
||||||
pageInputEl.disabled = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePagerUi(newCurrentPage, newNumOfAllPages) {
|
function updatePagerUi(newCurrentPage, newNumOfAllPages) {
|
||||||
@@ -729,23 +753,20 @@ function initPagination(paginationRootElId, onPageChange, currentPage = 1) {
|
|||||||
if (!newCurrentPage) return
|
if (!newCurrentPage) return
|
||||||
|
|
||||||
currentPage = newCurrentPage
|
currentPage = newCurrentPage
|
||||||
pageInputEl.value = newCurrentPage
|
numOfAllPages = newNumOfAllPages
|
||||||
pagesCountDisplayEl.textContent = newNumOfAllPages
|
pageInputEls.forEach(el => (el.value = newCurrentPage))
|
||||||
|
pagesCountDisplayEls.forEach(el => (el.textContent = newNumOfAllPages))
|
||||||
|
|
||||||
if (newCurrentPage === 1) {
|
if (newCurrentPage === 1) {
|
||||||
btnFirstPage.disabled = true
|
backwardBtnEls.forEach(el => (el.disabled = true))
|
||||||
btnPreviousPage.disabled = true
|
|
||||||
} else {
|
} else {
|
||||||
btnFirstPage.disabled = false
|
backwardBtnEls.forEach(el => (el.disabled = false))
|
||||||
btnPreviousPage.disabled = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newCurrentPage === newNumOfAllPages) {
|
if (newCurrentPage === newNumOfAllPages) {
|
||||||
btnNextPage.disabled = true
|
forwardBtnEls.forEach(el => (el.disabled = true))
|
||||||
btnLastPage.disabled = true
|
|
||||||
} else {
|
} else {
|
||||||
btnNextPage.disabled = false
|
forwardBtnEls.forEach(el => (el.disabled = false))
|
||||||
btnLastPage.disabled = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -923,94 +944,54 @@ if (registerSwithcButton) {
|
|||||||
|
|
||||||
function initSelect2(querySelector, placeholder) {
|
function initSelect2(querySelector, placeholder) {
|
||||||
$(querySelector).select2({
|
$(querySelector).select2({
|
||||||
allowClear: true,
|
// allowClear: true,
|
||||||
placeholder: placeholder
|
placeholder: placeholder
|
||||||
})
|
})
|
||||||
|
|
||||||
$(querySelector).val(null).trigger('change')
|
// $(querySelector).val(null).trigger('change')
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(document).ready(function () {
|
||||||
// TODO refactor wth specific select2
|
// TODO refactor wth specific select2
|
||||||
$('.select-search-field').select2({})
|
|
||||||
|
|
||||||
initSelect2('.select-domain-field', 'Področje')
|
isI18nReady.then(t => {
|
||||||
initSelect2('.select-src-lang-field', 'Izvorni jezik')
|
$('.select-search-field').select2({})
|
||||||
initSelect2('.select-dest-lang-field', 'Ciljni jezik')
|
|
||||||
initSelect2('.select-dict-field', 'Slovar')
|
|
||||||
initSelect2('.select-source-field', 'Vir')
|
|
||||||
|
|
||||||
$('b[role="presentation"]').hide()
|
initSelect2('.select-domain-field', t('Področje'))
|
||||||
$('.select2-selection__arrow').append(
|
initSelect2('.select-src-lang-field', t('Jezik iskanja'))
|
||||||
'<img src="/images/chevron-down-darker.svg" alt="V"></img>'
|
initSelect2('.select-dest-lang-field', t('Ciljni jezik'))
|
||||||
)
|
initSelect2('.select-dict-field', t('Slovar'))
|
||||||
|
initSelect2('.select-source-field', t('Vir'))
|
||||||
|
|
||||||
$('.select-search-field').on('select2:select', function (e) {
|
$('b[role="presentation"]').hide()
|
||||||
selectActiveElementFromInput(e)
|
$('.select2-selection__arrow').append(
|
||||||
// console.log(activeInput)
|
'<img src="/images/chevron-down-darker.svg" alt="V"></img>'
|
||||||
// console.log(e.target.parentNode.children[2])
|
)
|
||||||
// const label = e.target.parentNode.children[2]
|
|
||||||
|
|
||||||
// console.log(activeInput)
|
$('.select-search-field').on('select2:select', function (e) {
|
||||||
// console.log(activeInput)
|
selectActiveElementFromInput(e)
|
||||||
activeInput.addTag(e.params.data._resultId)
|
// console.log(activeInput)
|
||||||
activeInput.labelJump()
|
// console.log(e.target.parentNode.children[2])
|
||||||
})
|
// const label = e.target.parentNode.children[2]
|
||||||
|
|
||||||
$('.select-search-field').on('select2:unselect', function (e) {
|
// console.log(activeInput)
|
||||||
selectActiveElementFromInput(e)
|
// console.log(activeInput)
|
||||||
// console.log(activeInput)
|
activeInput.addTag(e.params.data._resultId)
|
||||||
// console.log(activeInput)
|
activeInput.labelJump()
|
||||||
activeInput.removeTag(e.params.data._resultId)
|
})
|
||||||
activeInput.labelJump()
|
|
||||||
// console.log('DELETED ' + e)
|
|
||||||
})
|
|
||||||
|
|
||||||
inputs.forEach(e => {
|
$('.select-search-field').on('select2:unselect', function (e) {
|
||||||
e.input = e.domElement.querySelector('.select2-search__field')
|
selectActiveElementFromInput(e)
|
||||||
})
|
// console.log(activeInput)
|
||||||
})
|
// console.log(activeInput)
|
||||||
|
activeInput.removeTag(e.params.data._resultId)
|
||||||
|
activeInput.labelJump()
|
||||||
|
// console.log('DELETED ' + e)
|
||||||
|
})
|
||||||
|
|
||||||
/* TODO REMOVE OTHER SCRIPTS WHEN YOU FINISH MODULARIZING THINGS THAT COULD BE MODULARIZED */
|
inputs.forEach(e => {
|
||||||
|
e.input = e.domElement.querySelector('.select2-search__field')
|
||||||
$(document).ready(function () {
|
})
|
||||||
// TODO refactor wth specific select2
|
|
||||||
$('.select-search-field').select2({})
|
|
||||||
|
|
||||||
initSelect2('.select-domain-field', 'Področje')
|
|
||||||
initSelect2('.select-src-lang-field', 'Izvorni jezik')
|
|
||||||
initSelect2('.select-dest-lang-field', 'Ciljni jezik')
|
|
||||||
initSelect2('.select-dict-field', 'Slovar')
|
|
||||||
initSelect2('.select-source-field', 'Vir')
|
|
||||||
|
|
||||||
$('b[role="presentation"]').hide()
|
|
||||||
$('.select2-selection__arrow').append(
|
|
||||||
'<img src="/images/chevron-down-darker.svg" alt="V"></img>'
|
|
||||||
)
|
|
||||||
|
|
||||||
$('.select-search-field').on('select2:select', function (e) {
|
|
||||||
selectActiveElementFromInput(e)
|
|
||||||
// console.log(activeInput)
|
|
||||||
// console.log(e.target.parentNode.children[2])
|
|
||||||
// const label = e.target.parentNode.children[2]
|
|
||||||
|
|
||||||
// console.log(activeInput)
|
|
||||||
// console.log(activeInput)
|
|
||||||
activeInput.addTag(e.params.data._resultId)
|
|
||||||
activeInput.labelJump()
|
|
||||||
})
|
|
||||||
|
|
||||||
$('.select-search-field').on('select2:unselect', function (e) {
|
|
||||||
selectActiveElementFromInput(e)
|
|
||||||
// console.log(activeInput)
|
|
||||||
// console.log(activeInput)
|
|
||||||
activeInput.removeTag(e.params.data._resultId)
|
|
||||||
activeInput.labelJump()
|
|
||||||
// console.log('DELETED ' + e)
|
|
||||||
})
|
|
||||||
|
|
||||||
inputs.forEach(e => {
|
|
||||||
e.input = e.domElement.querySelector('.select2-search__field')
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1097,7 +1078,7 @@ function transferText(sideMenuText, removeOptionalBreak = false) {
|
|||||||
|
|
||||||
const optionalBreak = document.getElementById('disposable-break')
|
const optionalBreak = document.getElementById('disposable-break')
|
||||||
|
|
||||||
if (document.body.clientWidth <= 1200) {
|
if (window.innerWidth < 1200) {
|
||||||
navTitle.textContent = siteHeadingTextContent
|
navTitle.textContent = siteHeadingTextContent
|
||||||
siteHeading.style.display = 'none'
|
siteHeading.style.display = 'none'
|
||||||
if (removeOptionalBreak) {
|
if (removeOptionalBreak) {
|
||||||
@@ -1111,3 +1092,160 @@ function transferText(sideMenuText, removeOptionalBreak = false) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function replaceContainer(id, HTMLContent) {
|
||||||
|
const el = document.getElementById(id)
|
||||||
|
if (el) {
|
||||||
|
el.innerHTML = HTMLContent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// STATE MANAGER FOR WINDOWS IN FORGOT PASSWORD
|
||||||
|
class StateManager {
|
||||||
|
constructor() {
|
||||||
|
this.state = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(newState) {
|
||||||
|
this.state = { ...this.state, ...newState }
|
||||||
|
}
|
||||||
|
|
||||||
|
getState() {
|
||||||
|
return this.state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StateManagerInvoker {
|
||||||
|
constructor(stateManager, invokeFunction) {
|
||||||
|
this.stateManager = stateManager
|
||||||
|
this.invokeFn = invokeFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(newState) {
|
||||||
|
this.stateManager.setState(newState)
|
||||||
|
this.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
|
invoke() {
|
||||||
|
this.invokeFn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateManager = new StateManager()
|
||||||
|
const forgottenPasswordInvoker = new StateManagerInvoker(stateManager, () => {
|
||||||
|
function closeAllFPRelatedModals() {
|
||||||
|
$('#staticBackdrop').modal('hide')
|
||||||
|
$('#reset-pass-modal').modal('hide')
|
||||||
|
$('#reset-pass-info').modal('hide')
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLoginModal() {
|
||||||
|
$('#staticBackdrop').modal('show')
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResetPasswordModal() {
|
||||||
|
$('#reset-pass-modal').modal('show')
|
||||||
|
}
|
||||||
|
|
||||||
|
function showResetPasswordModalSuccessOrFail() {
|
||||||
|
$('#reset-pass-info').modal('show')
|
||||||
|
}
|
||||||
|
|
||||||
|
function apllyDescriptionAccordingToState() {
|
||||||
|
$('#fpi-text').text(stateManager.getState().fpassWindowDescription)
|
||||||
|
}
|
||||||
|
|
||||||
|
closeAllFPRelatedModals()
|
||||||
|
switch (stateManager.getState().fpassWindowState) {
|
||||||
|
case ForgotPasswordState.LOGIN_SIGNUP:
|
||||||
|
showLoginModal()
|
||||||
|
break
|
||||||
|
case ForgotPasswordState.FORGOT_PASSWORD_MODAL:
|
||||||
|
showResetPasswordModal()
|
||||||
|
break
|
||||||
|
case ForgotPasswordState.FORGOT_PASSWORD_ERROR:
|
||||||
|
case ForgotPasswordState.FORGOT_PASSWORD_SUCCESS:
|
||||||
|
showResetPasswordModalSuccessOrFail()
|
||||||
|
apllyDescriptionAccordingToState()
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
console.log('Unknown state')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const ForgotPasswordState = {
|
||||||
|
LOGIN_SIGNUP: 1,
|
||||||
|
FORGOT_PASSWORD_MODAL: 2,
|
||||||
|
FORGOT_PASSWORD_ERROR: 3,
|
||||||
|
FORGOT_PASSWORD_SUCCESS: 4
|
||||||
|
}
|
||||||
|
|
||||||
|
function onForgotPassword() {
|
||||||
|
forgottenPasswordInvoker.setState({
|
||||||
|
fpassWindowState: ForgotPasswordState.FORGOT_PASSWORD_MODAL
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCancelForgotPassword() {
|
||||||
|
forgottenPasswordInvoker.setState({
|
||||||
|
fpassWindowState: ForgotPasswordState.LOGIN_SIGNUP
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSendForgottenEmailRequest() {
|
||||||
|
if (document.getElementById('forgot-pass-input').value === '') {
|
||||||
|
stateManager.setState({
|
||||||
|
fpassWindowDescription: i18next.t(
|
||||||
|
'Prosimo vnesite vaš elektronski naslov.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
forgottenPasswordInvoker.setState({
|
||||||
|
fpassWindowState: ForgotPasswordState.FORGOT_PASSWORD_ERROR
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post('/api/v1/users/reset-password-init', {
|
||||||
|
usernameOrEmail: document.getElementById('forgot-pass-input').value
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
// const EMAIL = 'DUMMY_EMAIL'
|
||||||
|
|
||||||
|
// State manager because we don't want to invoker twice
|
||||||
|
// it is required to update description first before updating state of the invoker
|
||||||
|
stateManager.setState({
|
||||||
|
fpassWindowDescription: i18next.t(
|
||||||
|
'Na vaš elektronski naslov smo vam posredovali povezavo za ponastavitev gesla. Prosimo preverite svoj elektronski predal.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
forgottenPasswordInvoker.setState({
|
||||||
|
fpassWindowState: ForgotPasswordState.FORGOT_PASSWORD_SUCCESS
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
if (err.response && err.response.data) {
|
||||||
|
stateManager.setState({
|
||||||
|
fpassWindowDescription: err.response.data
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
stateManager.setState({
|
||||||
|
fpassWindowDescription: i18next.t(
|
||||||
|
'Prišlo je do napake pri pošiljanju sporočila na vaš elektronski naslov. Poskusite ponovno.'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
forgottenPasswordInvoker.setState({
|
||||||
|
fpassWindowState: ForgotPasswordState.FORGOT_PASSWORD_ERROR
|
||||||
|
})
|
||||||
|
console.log(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#modal-fp-cancel-btn').on('click', onCancelForgotPassword)
|
||||||
|
$('#modal-fp-use-btn').on('click', onSendForgottenEmailRequest)
|
||||||
|
$('#modal-fp-info-close').on('click', () => $('#reset-pass-info').modal('hide'))
|
||||||
|
$('#forgotten-password').on('click', onForgotPassword)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/* global $, axios, initPagination, removeAllChildNodes, closeUtilityContainers, prepareQueryArrayForArrayWithIDs, getDataFromURLSearchParams, prepareQueryParams, focusOnCompletePrompt */
|
/* global $, axios, initPagination, removeAllChildNodes, closeUtilityContainers, prepareQueryArrayForArrayWithIDs,
|
||||||
|
getDataFromURLSearchParams, prepareQueryParams, focusOnCompletePrompt, i18next */
|
||||||
|
|
||||||
{
|
{
|
||||||
/*
|
/*
|
||||||
@@ -117,11 +118,11 @@
|
|||||||
|
|
||||||
return page
|
return page
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let message = 'Prišlo je do napake.'
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
if (error.response?.data) {
|
if (error.response?.data) {
|
||||||
message = error.response.data
|
message = error.response.data
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
}
|
}
|
||||||
alert(message)
|
alert(message)
|
||||||
updatePager()
|
updatePager()
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
/* global $, axios, currentPagePath, initPagination, removeAllChildNodes, transferText, tooltipTriggerList, tooltipList, reinitalizeDefaultTooltipSet, createTooltip */
|
/* global $, axios, currentPagePath, initPagination,
|
||||||
|
removeAllChildNodes, transferText, tooltipTriggerList,
|
||||||
|
tooltipList, createTooltip, resetTooltipTriggerList,
|
||||||
|
transferTextExtended, isI18nReady, i18next */
|
||||||
|
|
||||||
// position correction functions
|
// position correction functions
|
||||||
|
|
||||||
function adjustOffsetBy() {
|
function adjustOffsetBy() {
|
||||||
// const { offsetMain, fixedTopSection } = window.dictionaryElements
|
// const { offsetMain, fixedTopSection } = window.dictionaryElements
|
||||||
|
|
||||||
|
const BROWSER_UNUSUAL_OFFSET = 17 // computer from chrome
|
||||||
|
|
||||||
const offsetMain = document.querySelector('#offset-main')
|
const offsetMain = document.querySelector('#offset-main')
|
||||||
const fixedTopSection = document.querySelector('#fixed-top-section')
|
const fixedTopSection = document.querySelector('#fixed-top-section')
|
||||||
|
|
||||||
@@ -14,7 +19,7 @@ function adjustOffsetBy() {
|
|||||||
// const adminNavMobile = document.getElementsByClassName('admin-nav')
|
// const adminNavMobile = document.getElementsByClassName('admin-nav')
|
||||||
const headerPadding = document.getElementById('header-padding')
|
const headerPadding = document.getElementById('header-padding')
|
||||||
|
|
||||||
if (document.body.clientWidth < 1200) {
|
if (document.body.clientWidth + BROWSER_UNUSUAL_OFFSET < 1200) {
|
||||||
if (offsetHeaderPadding !== null) offsetMain.style.paddingTop = `0px`
|
if (offsetHeaderPadding !== null) offsetMain.style.paddingTop = `0px`
|
||||||
} else {
|
} else {
|
||||||
for (let i = 0; i < offsetHeader.length; i++) {
|
for (let i = 0; i < offsetHeader.length; i++) {
|
||||||
@@ -80,8 +85,14 @@ function mobileMoveContent() {
|
|||||||
'.header-container-divider-right'
|
'.header-container-divider-right'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// let actualInnerWidth = $("body").prop("clientWidth"); // El. width minus scrollbar width
|
||||||
|
// let actualInnerWidth = $("body").prop("scrollWidth"); // El. width minus scrollbar width
|
||||||
|
|
||||||
|
// const BROWSER_UNUSUAL_OFFSET = 17 // computer from chrome
|
||||||
|
|
||||||
|
// console.log($('body').innerWidth() + BROWSER_UNUSUAL_OFFSET)
|
||||||
try {
|
try {
|
||||||
if (document.body.clientWidth <= 1200) {
|
if (window.innerWidth < 1200) {
|
||||||
if (secondaryButton) {
|
if (secondaryButton) {
|
||||||
mobileRightHolder.appendChild(secondaryButton)
|
mobileRightHolder.appendChild(secondaryButton)
|
||||||
// secondaryButton.style.height = '28px'
|
// secondaryButton.style.height = '28px'
|
||||||
@@ -98,9 +109,11 @@ function mobileMoveContent() {
|
|||||||
primaryButton.style.whiteSpace = 'nowrap'
|
primaryButton.style.whiteSpace = 'nowrap'
|
||||||
}
|
}
|
||||||
navTitle.textContent = siteHeadingTextContent
|
navTitle.textContent = siteHeadingTextContent
|
||||||
siteHeading.style.display = 'none'
|
// siteHeading.style.display = 'none'
|
||||||
|
|
||||||
|
siteHeading.style.display = 'inline' // I know this is hacky, but sadly due to design this is mandatory in order to fix the bug
|
||||||
}
|
}
|
||||||
if (document.body.clientWidth > 1200) {
|
if (window.innerWidth >= 1200) {
|
||||||
if (secondaryButton) {
|
if (secondaryButton) {
|
||||||
headerContainerRight.appendChild(secondaryButton)
|
headerContainerRight.appendChild(secondaryButton)
|
||||||
secondaryButton.style.height = ''
|
secondaryButton.style.height = ''
|
||||||
@@ -117,7 +130,7 @@ function mobileMoveContent() {
|
|||||||
primaryButton.style.whiteSpace = ''
|
primaryButton.style.whiteSpace = ''
|
||||||
}
|
}
|
||||||
|
|
||||||
navTitle.textContent = 'Urejanje'
|
navTitle.textContent = i18next.t('Urejanje')
|
||||||
siteHeading.style.display = 'block'
|
siteHeading.style.display = 'block'
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
@@ -128,7 +141,11 @@ if (resultsListEl) {
|
|||||||
resultsListEl.addEventListener('click', onResultClick)
|
resultsListEl.addEventListener('click', onResultClick)
|
||||||
}
|
}
|
||||||
const initialPage = +new URL(location).searchParams.get('p') || 1
|
const initialPage = +new URL(location).searchParams.get('p') || 1
|
||||||
const updatePager = initPagination('pagination', onPageChange, initialPage)
|
const updatePager = initPagination(
|
||||||
|
['pagination-top', 'pagination-bottom'],
|
||||||
|
onPageChange,
|
||||||
|
initialPage
|
||||||
|
)
|
||||||
|
|
||||||
async function onPageChange(newPage) {
|
async function onPageChange(newPage) {
|
||||||
const receivedPageNumber = await changePage(newPage)
|
const receivedPageNumber = await changePage(newPage)
|
||||||
@@ -146,7 +163,16 @@ async function changePage(newPage) {
|
|||||||
removeAllChildNodes(resultsListEl)
|
removeAllChildNodes(resultsListEl)
|
||||||
renderResults(resultsMarkup)
|
renderResults(resultsMarkup)
|
||||||
updatePager(page, numberOfAllPages)
|
updatePager(page, numberOfAllPages)
|
||||||
reinitalizeDefaultTooltipSet()
|
// reinitalizeDefaultTooltipSet()
|
||||||
|
// console.log($('[data-toggle="tooltip"]'))
|
||||||
|
// $('[data-toggle="tooltip"]').tooltip()
|
||||||
|
|
||||||
|
// toltip reinitialization after moving a page
|
||||||
|
resetTooltipTriggerList()
|
||||||
|
if (tooltipTriggerList.length > 0) {
|
||||||
|
// initialize tooltips
|
||||||
|
tooltipList(null, 'dark-gray-tooltip') // ADD for tooltip debug in the end -> [0].show()
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
try {
|
try {
|
||||||
@@ -160,12 +186,12 @@ async function changePage(newPage) {
|
|||||||
return page
|
return page
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console.log(error)
|
// console.log(error)
|
||||||
let message = 'Prišlo je do napake.'
|
let message = i18next.t('Prišlo je do napake.')
|
||||||
if (error.response?.data) {
|
if (error.response?.data) {
|
||||||
message = error.response.data
|
message = error.response.data
|
||||||
} else if (error.request) {
|
} else if (error.request) {
|
||||||
return // return to bypass error caused (assumed) by popstate on iOS/MacOS
|
return // return to bypass error caused (assumed) by popstate on iOS/MacOS
|
||||||
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
|
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
|
||||||
}
|
}
|
||||||
alert(message)
|
alert(message)
|
||||||
updatePager()
|
updatePager()
|
||||||
@@ -210,119 +236,10 @@ function onResultClick(e) {
|
|||||||
try {
|
try {
|
||||||
if (tooltipTriggerList.length > 0) {
|
if (tooltipTriggerList.length > 0) {
|
||||||
// initialize tooltips
|
// initialize tooltips
|
||||||
tooltipList(null, 'gray-tooltip') // ADD for tooltip debug in the end -> [0].show()
|
tooltipList(null, 'dark-gray-tooltip') // ADD for tooltip debug in the end -> [0].show()
|
||||||
}
|
}
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
|
|
||||||
/*
|
|
||||||
// refresh: bool
|
|
||||||
function largeStringsOnSmallScreen(alwaysRefresh) {
|
|
||||||
let headwordTexts
|
|
||||||
let translationWords
|
|
||||||
let synonymWords
|
|
||||||
function init() {
|
|
||||||
headwordTexts = [...$('.rihw')]
|
|
||||||
translationWords = [...$('.term-h')].map(e => {
|
|
||||||
return e.children[0]
|
|
||||||
})
|
|
||||||
synonymWords = [...$('.syn-h'), ...$('.risy')]
|
|
||||||
}
|
|
||||||
|
|
||||||
function setupToolTips() {
|
|
||||||
setDynamicTooltipList([])
|
|
||||||
if (headwordTexts) {
|
|
||||||
headwordTexts.map(ttElement =>
|
|
||||||
createTooltip(ttElement, ttElement.innerHTML, '')
|
|
||||||
)
|
|
||||||
// setDynamicTooltipList([...dynamicTooltipList, ...tt])
|
|
||||||
}
|
|
||||||
if (translationWords) {
|
|
||||||
translationWords.map(ttElement =>
|
|
||||||
createTooltip(ttElement, ttElement.innerHTML, '')
|
|
||||||
)
|
|
||||||
// setDynamicTooltipList([...dynamicTooltipList, ...tt])
|
|
||||||
}
|
|
||||||
if (synonymWords) {
|
|
||||||
synonymWords.map(ttElement =>
|
|
||||||
createTooltip(ttElement, ttElement.innerHTML, '')
|
|
||||||
)
|
|
||||||
// setDynamicTooltipList([...dynamicTooltipList, ...tt])
|
|
||||||
}
|
|
||||||
// console.log(dynamicTooltipList)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!headwordTexts || !translationWords || !synonymWords || alwaysRefresh) {
|
|
||||||
init()
|
|
||||||
}
|
|
||||||
|
|
||||||
function initTooltips() {
|
|
||||||
try {
|
|
||||||
// if (dynamicTooltipList.length || alwaysRefresh) {
|
|
||||||
setupToolTips()
|
|
||||||
// }
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeTooltips() {
|
|
||||||
try {
|
|
||||||
$('.rihw').tooltip('dispose')
|
|
||||||
$('.term-h').children().tooltip('dispose')
|
|
||||||
$('.syn-h').tooltip('dispose')
|
|
||||||
$('.risy').tooltip('dispose')
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function manageResize() {
|
|
||||||
const tmpTerms = [...$('.term-h')].map(e => {
|
|
||||||
return e.children[0]
|
|
||||||
})
|
|
||||||
const elements = [...tmpTerms, ...$('.syn-h')]
|
|
||||||
// console.log(document.documentElement.clientWidth)
|
|
||||||
|
|
||||||
const dcw = document.documentElement.clientWidth
|
|
||||||
const checkBorders = (dcw > 991 && dcw < 1600) || dcw < 370
|
|
||||||
if (checkBorders) {
|
|
||||||
const TEXT_LIMIT = 17
|
|
||||||
elements.forEach(el => {
|
|
||||||
el.dataset.textStore = el.innerHTML.includes('...')
|
|
||||||
? el.dataset.textStore
|
|
||||||
: el.innerHTML
|
|
||||||
el.innerHTML = `${el.innerHTML.slice(0, TEXT_LIMIT)}${
|
|
||||||
el.innerHTML.length > TEXT_LIMIT ? '...' : ''
|
|
||||||
}`
|
|
||||||
})
|
|
||||||
|
|
||||||
initTooltips()
|
|
||||||
} else {
|
|
||||||
elements.forEach(el => {
|
|
||||||
if (el.dataset.textStore) {
|
|
||||||
el.innerHTML = el.dataset.textStore
|
|
||||||
}
|
|
||||||
})
|
|
||||||
removeTooltips()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
init: function () {
|
|
||||||
init()
|
|
||||||
},
|
|
||||||
refresh: function () {
|
|
||||||
init()
|
|
||||||
try {
|
|
||||||
setupToolTips()
|
|
||||||
} catch (e) {}
|
|
||||||
},
|
|
||||||
manageResize: manageResize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const textsScreenManager = largeStringsOnSmallScreen(true)
|
|
||||||
|
|
||||||
textsScreenManager.manageResize()
|
|
||||||
|
|
||||||
window.onresize = textsScreenManager.manageResize
|
|
||||||
*/
|
|
||||||
|
|
||||||
// refresh: bool
|
// refresh: bool
|
||||||
function largeStringsOnSmallScreen(alwaysRefresh) {
|
function largeStringsOnSmallScreen(alwaysRefresh) {
|
||||||
let headwordTexts
|
let headwordTexts
|
||||||
@@ -336,30 +253,6 @@ function largeStringsOnSmallScreen(alwaysRefresh) {
|
|||||||
synonymWords = [...$('.syn-h'), ...$('.risy')]
|
synonymWords = [...$('.syn-h'), ...$('.risy')]
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
function setupToolTips() {
|
|
||||||
setDynamicTooltipList([])
|
|
||||||
if (headwordTexts) {
|
|
||||||
headwordTexts.map(ttElement =>
|
|
||||||
createTooltip(ttElement, ttElement.innerHTML, '')
|
|
||||||
)
|
|
||||||
// setDynamicTooltipList([...dynamicTooltipList, ...tt])
|
|
||||||
}
|
|
||||||
if (translationWords) {
|
|
||||||
translationWords.map(ttElement =>
|
|
||||||
createTooltip(ttElement, ttElement.innerHTML, '')
|
|
||||||
)
|
|
||||||
// setDynamicTooltipList([...dynamicTooltipList, ...tt])
|
|
||||||
}
|
|
||||||
if (synonymWords) {
|
|
||||||
synonymWords.map(ttElement =>
|
|
||||||
createTooltip(ttElement, ttElement.innerHTML, '')
|
|
||||||
)
|
|
||||||
// setDynamicTooltipList([...dynamicTooltipList, ...tt])
|
|
||||||
}
|
|
||||||
// console.log(dynamicTooltipList)
|
|
||||||
} */
|
|
||||||
|
|
||||||
if (!headwordTexts || !translationWords || !synonymWords || alwaysRefresh) {
|
if (!headwordTexts || !translationWords || !synonymWords || alwaysRefresh) {
|
||||||
init()
|
init()
|
||||||
}
|
}
|
||||||
@@ -379,175 +272,28 @@ function largeStringsOnSmallScreen(alwaysRefresh) {
|
|||||||
ttElement.tooltip('dispose')
|
ttElement.tooltip('dispose')
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
function initTooltips() {
|
|
||||||
try {
|
|
||||||
// if (dynamicTooltipList.length || alwaysRefresh) {
|
|
||||||
setupToolTips()
|
|
||||||
// }
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeTooltips() {
|
|
||||||
try {
|
|
||||||
$('.rihw').tooltip('dispose')
|
|
||||||
$('.term-h').children().tooltip('dispose')
|
|
||||||
$('.syn-h').tooltip('dispose')
|
|
||||||
$('.risy').tooltip('dispose')
|
|
||||||
} catch (e) {}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* let OLD_TEXT_LIMIT = 5000 // Guard to not prevent overdisposing
|
|
||||||
function manageResize() {
|
|
||||||
const tmpTerms = [...$('.term-h')].map(e => {
|
|
||||||
return e.children[0]
|
|
||||||
})
|
|
||||||
const elements = [...tmpTerms, ...$('.syn-h')]
|
|
||||||
// console.log(document.documentElement.clientWidth) */
|
|
||||||
|
|
||||||
/*
|
|
||||||
$('.word-constraint').each((i, element) => {
|
|
||||||
// WRONG const wordBr = $(`.word-constraint:nth-child(${i}) > .word-breakable`)
|
|
||||||
const wordList = $(element).find('.word-breakable')
|
|
||||||
|
|
||||||
wordList.each((i, wordElement) => {
|
|
||||||
// console.log(wordElement.innerText)
|
|
||||||
|
|
||||||
if (
|
|
||||||
wordElement.clientWidth > element.clientWidth &&
|
|
||||||
wordElement.innerText.split(' ').length <= 1
|
|
||||||
) {
|
|
||||||
console.log('Lion')
|
|
||||||
createTooltip($(wordElement))
|
|
||||||
wordElement.classList.push('text-ellipsis')
|
|
||||||
} else {
|
|
||||||
removeTooltip($(wordElement))
|
|
||||||
// console.log(wordElement.classList)
|
|
||||||
wordElement.className = Array.from(wordElement.classList)
|
|
||||||
.filter(e => e !== 'text-ellipsis')
|
|
||||||
.join(' ')
|
|
||||||
// console.log(wordElement.classList)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
// console.log(wordBr)
|
|
||||||
})
|
|
||||||
*/
|
|
||||||
|
|
||||||
/* const dcw = document.documentElement.clientWidth
|
|
||||||
// const checkBorders = (dcw > 420 && dcw < 750) || dcw < 370
|
|
||||||
|
|
||||||
let borderIndex
|
|
||||||
|
|
||||||
// check _common.scc long-text-strip and make sure the widths are aligned
|
|
||||||
if (dcw < 420) {
|
|
||||||
borderIndex = 0 // width = 8 chars (<420)
|
|
||||||
} else if (dcw < 750 || (dcw > 990 && dcw < 1750)) {
|
|
||||||
borderIndex = 1 // width = 12 chars (420 - 750, 990-1750)
|
|
||||||
} else if (dcw > 1750) {
|
|
||||||
borderIndex = 3 // width = 100% (>1759)
|
|
||||||
} else {
|
|
||||||
borderIndex = 2 // width = 25 chars (750-990)
|
|
||||||
}
|
|
||||||
|
|
||||||
let TEXT_LIMIT
|
|
||||||
if (borderIndex === 0) {
|
|
||||||
TEXT_LIMIT = 8
|
|
||||||
} else if (borderIndex === 1) {
|
|
||||||
TEXT_LIMIT = 12
|
|
||||||
} else if (borderIndex === 2) {
|
|
||||||
TEXT_LIMIT = 25
|
|
||||||
} else {
|
|
||||||
TEXT_LIMIT = null
|
|
||||||
}
|
|
||||||
|
|
||||||
if (OLD_TEXT_LIMIT !== TEXT_LIMIT) {
|
|
||||||
elements.forEach(el => {
|
|
||||||
// console.log(el.innerHTML)
|
|
||||||
if (TEXT_LIMIT && el.innerHTML.length > TEXT_LIMIT) {
|
|
||||||
console.log(`DEBUG TEXT LIMIT: ${TEXT_LIMIT}`)
|
|
||||||
console.log(`DEBUG EL LEN: ${el.innerHTML.length}`)
|
|
||||||
console.log(`DEBUG EL TEXT: ${el.innerHTML}`)
|
|
||||||
console.log('TOOLTIP CREATED')
|
|
||||||
createTooltipEl(el, 'gray-tooltip')
|
|
||||||
} else {
|
|
||||||
removeTooltip($(el))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
OLD_TEXT_LIMIT = TEXT_LIMIT */
|
|
||||||
|
|
||||||
/*
|
|
||||||
if (checkBorders) {
|
|
||||||
console.log('HEYOO')
|
|
||||||
const TEXT_LIMIT = 17
|
|
||||||
elements.forEach(el => {
|
|
||||||
el.dataset.textStore = el.innerHTML.includes('...')
|
|
||||||
? el.dataset.textStore
|
|
||||||
: el.innerHTML
|
|
||||||
el.innerHTML = `${el.innerHTML.slice(0, TEXT_LIMIT)}${
|
|
||||||
el.innerHTML.length > TEXT_LIMIT ? '...' : ''
|
|
||||||
}`
|
|
||||||
})
|
|
||||||
|
|
||||||
initTooltips()
|
|
||||||
} else {
|
|
||||||
elements.forEach(el => {
|
|
||||||
if (el.dataset.textStore) {
|
|
||||||
el.innerHTML = el.dataset.textStore
|
|
||||||
}
|
|
||||||
})
|
|
||||||
removeTooltips()
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
/* }
|
|
||||||
|
|
||||||
return {
|
|
||||||
init: function () {
|
|
||||||
init()
|
|
||||||
},
|
|
||||||
refresh: function () {
|
|
||||||
init()
|
|
||||||
},
|
|
||||||
manageResize: manageResize
|
|
||||||
} */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// const textsScreenManager = largeStringsOnSmallScreen(true)
|
window.addEventListener('load', () => {
|
||||||
|
$('[data-toggle="tooltip"]').tooltip()
|
||||||
// textsScreenManager.manageResize()
|
|
||||||
|
|
||||||
// window.onresize = textsScreenManager.manageResize
|
|
||||||
|
|
||||||
/*
|
|
||||||
function transferText(sideMenuText) {
|
|
||||||
const siteHeading = document.getElementById('site-heading')
|
|
||||||
const siteHeadingTextContent = siteHeading.textContent
|
|
||||||
const navTitle = document.getElementById('nav-title')
|
|
||||||
|
|
||||||
if (document.body.clientWidth <= 1200) {
|
|
||||||
navTitle.textContent = siteHeadingTextContent
|
|
||||||
siteHeading.style.display = 'none'
|
|
||||||
} else {
|
|
||||||
navTitle.textContent = sideMenuText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function handleProperTextDisplay() {
|
|
||||||
if (/\/iskanje/.test(currentPagePath)) {
|
|
||||||
transferText('Iskanje po slovarjih', true)
|
|
||||||
} else if (/\/termin/.test(currentPagePath)) {
|
|
||||||
transferText('', true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.addEventListener('resize', () => {
|
|
||||||
handleProperTextDisplay()
|
|
||||||
adjustOffsetBy()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
handleProperTextDisplay()
|
isI18nReady.then(t => {
|
||||||
|
function handleProperTextDisplay() {
|
||||||
|
// const BROWSER_UNUSUAL_OFFSET = 17
|
||||||
|
if (/\/iskanje/.test(currentPagePath)) {
|
||||||
|
transferText(t('Iskanje po slovarjih'), true, 'site-heading') //,
|
||||||
|
// BROWSER_UNUSUAL_OFFSET
|
||||||
|
// )
|
||||||
|
} else if (/\/termin/.test(currentPagePath)) {
|
||||||
|
transferText('', true, 'site-heading') // , BROWSER_UNUSUAL_OFFSET)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
handleProperTextDisplay()
|
||||||
|
adjustOffsetBy()
|
||||||
|
})
|
||||||
|
|
||||||
|
handleProperTextDisplay()
|
||||||
|
})
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
/* global $, currentPagePath */
|
/* global $, currentPagePath, i18next */
|
||||||
|
|
||||||
// const currentPagePath = location.pathname
|
// const currentPagePath = location.pathname
|
||||||
|
|
||||||
@@ -60,13 +60,13 @@ function initSelect() {
|
|||||||
const userPassword2 = ce.userConfirmationPassEl.value
|
const userPassword2 = ce.userConfirmationPassEl.value
|
||||||
|
|
||||||
if (userPassword1 === '') {
|
if (userPassword1 === '') {
|
||||||
alert('Prosim, vnesite geslo')
|
alert(i18next.t('Prosim, vnesite geslo'))
|
||||||
} else if (userPassword2 === '') {
|
} else if (userPassword2 === '') {
|
||||||
alert('Prosim, ponovno vnesite geslo')
|
alert(i18next.t('Prosim, ponovno vnesite geslo'))
|
||||||
} else if (userPassword1 !== userPassword2) {
|
} else if (userPassword1 !== userPassword2) {
|
||||||
alert('Gesli se ne ujemata')
|
alert(i18next.t('Gesli se ne ujemata'))
|
||||||
} else {
|
} else {
|
||||||
alert('Gesli se ujemata!')
|
alert(i18next.t('Gesli se ujemata!'))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,7 +115,7 @@ function deleteField(ele) {
|
|||||||
// Summernote
|
// Summernote
|
||||||
|
|
||||||
$('.summernote').summernote({
|
$('.summernote').summernote({
|
||||||
placeholder: 'Na kratko opišite zasnovo in namen slovarja.',
|
placeholder: i18next.t('Na kratko opišite zasnovo in namen slovarja.'),
|
||||||
height: 300,
|
height: 300,
|
||||||
minheight: 150,
|
minheight: 150,
|
||||||
toolbar: [
|
toolbar: [
|
||||||
|
|||||||