2 Commits
Author SHA1 Message Date
Luka Romih d0e53fee3b Fix most glaring bugs and vulnerabilities 2023-05-08 23:25:25 +02:00
Luka Romih 9867875ef2 First full release 2023-03-10 12:50:23 +01:00
328 changed files with 12894 additions and 7356 deletions
+1
View File
@@ -3,3 +3,4 @@ node_modules/
/.env /.env
.idea .idea
express/public/stylesheets express/public/stylesheets
/draft/
-1
View File
@@ -1,3 +1,2 @@
old_express old_express
express/public/stylesheets express/public/stylesheets
help-pug-demo.pug
+21
View File
@@ -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.
+164 -3
View File
@@ -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.
+1 -1
View File
@@ -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
+6
View File
@@ -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
+45 -5
View File
@@ -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
+28 -15
View File
@@ -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"]
+16 -5
View File
@@ -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
+30 -7
View File
@@ -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
+9 -5
View File
@@ -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)
} }
+60
View File
@@ -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)
}
}
+9 -3
View File
@@ -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
} }
+10 -2
View File
@@ -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
+65 -11
View File
@@ -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')
} }
+259 -24
View File
@@ -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)
if (page < 1) {
page = 1
} }
consultancy.listNewEntries = async (req, res) => { if (isAdminPage) {
const consultancyNewEntryList = await ConsultancyEntry.fetchAllNew() if (
const data = {} req.user &&
data.consultancyNewEntryList = consultancyNewEntryList (req.user.hasRole('portal admin') ||
res.send(data) 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 isOwnConsultancyEnabled =
(await getInstanceSetting('consultancy_type')) === 'own'
let domainNameSl
if (consultancyEntry.domainPrimaryIdInitial) {
domainNameSl = (
await Domain.fetchById(consultancyEntry.domainPrimaryIdInitial)
).nameSl
} else {
domainNameSl = ''
}
let emails
let subjectText
if (isOwnConsultancyEnabled) {
const questionId = await ConsultancyEntry.createQuestion(consultancyEntry) const questionId = await ConsultancyEntry.createQuestion(consultancyEntry)
await ConsultancyEntry.indexIntoSearchEngine(questionId, true) await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
subjectText = req.t('Ustvarjeno novo vprašanje v svetovalnici')
// TODO SEND EMAIL emails = await ConsultancyEntry.fetchConsultancyAdminEmails()
// TODOOOOOOOOO } else {
subjectText = req.t('Novo vprašanje za Terminološko svetovalnico')
const emails = await ConsultancyEntry.fetchConsultancyAdminEmails() 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,13 +332,23 @@ 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({})
req.entryId = id
next()
},
user.canConsultEntry,
async (req, res) => {
const { id, questionTitle, domain: domainId, question, answer } = req.body
if (questionTitle === '' || question === '' || answer === '') { if (questionTitle === '' || question === '' || answer === '') {
return res.status(422).send('Polja naslov, vprašanje in mnenje so obvezna!') return res
.status(422)
.send(req.t('Polja naslov, vprašanje in mnenje so obvezna!'))
} }
const entry = await ConsultancyEntry.fetchById(id) const entry = await ConsultancyEntry.fetchById(id)
@@ -282,6 +363,7 @@ consultancy.updateQuestion = async (req, res) => {
res.send({}) 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 })
}
+158 -14
View File
@@ -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 = [
(req, res, next) => {
req.dictionaryId = req.body.params.dictionaryId
next()
},
user.canAdministrateDictionary,
async (req, res) => {
const { dictionaryId, payload } = req.body.params 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
+83 -38
View File
@@ -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,6 +194,9 @@ extraction.begin = async (req, res) => {
res.send(timeStarted) res.send(timeStarted)
// Explicitcly catch any errors after the response has been sent
// since the the final error handler won't be able to send another.
try {
if (ossParams) { if (ossParams) {
// 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.
@@ -195,16 +209,24 @@ extraction.begin = async (req, res) => {
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)
const emailHtml = await renderAsync(
`email/extraction-done_${authorLanguage}`,
{
extractionName, extractionName,
extractionLink extractionLink
}) }
)
await email.send({ await email.send({
to: authorEmail, to: authorEmail,
subject: 'Luščenje končano', subject: i18next.t('Luščenje končano', { lng: authorLanguage }),
html: emailHtml 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
+27 -2
View File
@@ -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))
} }
+4 -3
View File
@@ -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) => {
+105 -6
View File
@@ -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
+76 -38
View File
@@ -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
}) })
} }
-17
View File
@@ -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
+146 -67
View File
@@ -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,9 +268,11 @@ 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) {
@@ -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)
} }
-150
View File
@@ -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
+31 -10
View File
@@ -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([
Dictionary.fetchAllPrimaryDomains(req.determinedLanguage),
Extraction.fetchOssDocumentTypes(req.determinedLanguage),
Extraction.fetchAllStopTermsFilesStats(extractionId) 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,
+19 -15
View File
@@ -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')
}) })
+261 -30
View File
@@ -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
})
}
+18 -15
View File
@@ -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
+122 -23
View File
@@ -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
+14 -10
View File
@@ -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.'
)
}) })
} }
+47
View File
@@ -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`
}
})
+8
View File
@@ -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()
} }
+99 -3
View File
@@ -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()
} }
/** /**
+20 -239
View File
@@ -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
+2 -51
View File
@@ -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 = `
-49
View File
@@ -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
File diff suppressed because it is too large Load Diff
+15 -2
View File
@@ -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)
+67 -100
View File
@@ -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 */
+275 -60
View File
@@ -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}`
try {
const { data: data1 } = await retry(
async () => {
const form = new FormData() const form = new FormData()
form.append('file', createReadStream(filePath), documentName) form.append('file', createReadStream(filePath), documentName)
try { return await axios.post(
const { data: data1 } = await axios.post( `${extractionApiOrigin}/datotekaVConlluAsync`,
'http://rsdo.lhrs.feri.um.si:8080/datotekaVConlluAsync',
form, form,
{ {
headers: { headers: {
...form.getHeaders() ...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(
`${extractionApiOrigin}/izlusciAsync`,
{ {
conllus: conllusArr, conllus: conllusArr,
prepovedaneBesede: Array.from(stopTermsSet) 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()
} }
+177 -18
View File
@@ -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)
}
}
+12 -6
View File
@@ -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,18 +45,22 @@ 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)
return fileStats
}
async function getFileStats(filePath) {
const filename = path.basename(filePath)
const { mtimeMs: timeModified, size } = await stat(filePath) const { mtimeMs: timeModified, size } = await stat(filePath)
const sizeHumanReadable = formatFileSize(size) const sizeHumanReadable = formatFileSize(size)
const fileStats = { filename, size: sizeHumanReadable, timeModified } const fileStats = { filename, size: sizeHumanReadable, timeModified }
return fileStats return fileStats
})
)
return fileStats
} }
function getExtractionFilesPath(extractionId) { function getExtractionFilesPath(extractionId) {
+4 -2
View File
@@ -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: {
+3 -2
View File
@@ -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 = {
+4 -1
View File
@@ -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
+21
View File
@@ -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
} }
} }
+23 -14
View File
@@ -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)
) )
+1 -1
View File
@@ -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
+345 -39
View File
@@ -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,31 +105,40 @@ 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
return user throw err
} }
// Activate user account. const userId = rows[0].user_id
User.activateAccount = async user => { ;({
await db.query(`UPDATE "user" SET status = 'active' WHERE id = $1`, [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
} }
// Generate a user remember me token. // Generate a user remember me token.
@@ -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,7 +457,7 @@ 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]
@@ -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
+96
View File
@@ -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",
+6 -2
View File
@@ -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",
Binary file not shown.
@@ -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>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

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

+6
View File
@@ -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

+167 -178
View File
@@ -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,6 +144,7 @@ function initAdmin() {
} }
if (currentPagePath === '/admin/uporabniki/seznam') { if (currentPagePath === '/admin/uporabniki/seznam') {
isI18nReady.then(t => {
const resultsListEl = document.getElementById('page-results') const resultsListEl = document.getElementById('page-results')
const updatePager = initPagination('pagination', onPageChange) const updatePager = initPagination('pagination', onPageChange)
@@ -155,11 +158,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()
@@ -184,20 +187,21 @@ function initAdmin() {
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 (navTitle) {
if ( if (
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'
} }
} }
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()
+10 -9
View File
@@ -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
}
})
+272 -7
View File
@@ -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) {
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' 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)
}
}
+367 -128
View File
@@ -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,6 +438,7 @@ function initDictionaries() {
if (mcBtnsGrp.length) { if (mcBtnsGrp.length) {
mcBtnsGrp.forEach(e => e.addEventListener('click', enableButton)) mcBtnsGrp.forEach(e => e.addEventListener('click', enableButton))
} }
if (ce.newHeadwordGroupBtn)
ce.newHeadwordGroupBtn.addEventListener('click', createHeadword) ce.newHeadwordGroupBtn.addEventListener('click', createHeadword)
if (ce.newConnectionBtn) if (ce.newConnectionBtn)
ce.newConnectionBtn.addEventListener('click', () => ce.newConnectionBtn.addEventListener('click', () =>
@@ -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')
} }
if (vAuthor) {
ce.authorEl.textContent = vAuthor
ce.previewAuthor.textContent = vAuthor
} else {
ce.authorEl.textContent = data.entry.version_author ce.authorEl.textContent = data.entry.version_author
ce.versionEl.textContent = `Verzija ${data.entry.version}`
ce.previewVersion.textContent = `Verzija ${data.entry.version}`
ce.previewAuthor.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(/&quot;/g, '"') ? data.entry.term.replace(/&quot;/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
@@ -721,6 +863,7 @@ function initDictionaries() {
}) })
} }
} }
}
}) })
} }
}) })
@@ -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() {
/&quot;/g, /&quot;/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,7 +1100,6 @@ 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
@@ -957,18 +1107,19 @@ function initDictionaries() {
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()
@@ -976,57 +1127,56 @@ function initDictionaries() {
} }
async function getDataForPage(page) { async function getDataForPage(page) {
const url = `/api/v1/dictionaries/${dictionaryId}/listDomainLabels?p=${page}` // const url = `/api/v1/dictionaries/${dictionaryId}/listDomainLabels?p=${page}`
const { data } = await axios.get(url) const url = `/api/v1/dictionaries/${dictionaryId}/domainLabels?q=${queryBattery}&p=${page}`
return data // const { data } = await axios.get(url)
return await axios.get(url)
} }
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 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') const updatePaginationOnFilter = axiosResult => {
div.classList.add('table-buttons') const numberOfAllPages = +axiosResult.headers['number-of-all-pages']
editBtn.className = 'p-0 table-button-grp me-3 edit-row-btn' const page = +axiosResult.headers.page
editBtn.type = 'button' // removeAllChildNodes(resultsListEl)
imgEditEl.src = '/images/u_edit-alt.svg' // renderResults(results.data)
deleteBtn.className = 'p-0 table-button-grp delete-row-btn' // console.log(numberOfAllPages)
deleteBtn.dataset.bsTarget = '#alert-modal' // console.log(page)
deleteBtn.dataset.bsToggle = 'modal' // console.log(updatePager)
deleteBtn.type = 'button' updatePager(page, numberOfAllPages)
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) /// / Due to unsual design, the function was moved inside
resultsListEl.appendChild(rowEl) 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
} }
filesToUpload.push([file, fileListEl])
}
if (didRemoveAnyFileListEls) reindexFileListEls()
for (const [file, fileListEl] of filesToUpload) {
try {
const payload = new FormData() const payload = new FormData()
payload.set(fileInputEl.name, file) payload.set(fileInputEl.name, file)
try { const { data: fileStats } = await axios.put(apiEndpointBase, payload, {
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)
}) })
+6 -6
View File
@@ -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 = '*'
} }
if (sbm === document.querySelector('.search-btn-a')) {
searchQuery(inputString, searchFilterDOM) searchQuery(inputString, searchFilterDOM)
} else {
searchQueryWithExistingFilters(inputString)
}
}) })
} }
} }
+52 -14
View File
@@ -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')
}) })
} }
@@ -52,6 +73,7 @@ function addMixedContentInput(el, selectedEl) {
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) {
@@ -61,6 +83,7 @@ function addMixedContentInput(el, selectedEl) {
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) {
@@ -70,6 +93,7 @@ function addMixedContentInput(el, selectedEl) {
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) {
@@ -79,14 +103,15 @@ function addMixedContentInput(el, selectedEl) {
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)
} }
@@ -95,9 +120,22 @@ function addMixedContentInput(el, selectedEl) {
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')
}
+220 -82
View File
@@ -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,11 +656,26 @@ 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) {
let reqLock = false
let numOfAllPages
const backwardBtnEls = []
const forwardBtnEls = []
const pageInputEls = []
const pagesCountDisplayEls = []
if (Array.isArray(paginationRootElIds)) {
paginationRootElIds.forEach(id => initControls(id))
} else {
initControls(paginationRootElIds)
}
const allControlEls = [...backwardBtnEls, ...forwardBtnEls, ...pageInputEls]
function initControls(paginationRootElId) {
const rootEl = document.getElementById(paginationRootElId) const rootEl = document.getElementById(paginationRootElId)
const btnFirstPage = rootEl.querySelector('.first-page') const btnFirstPage = rootEl.querySelector('.first-page')
const btnPreviousPage = rootEl.querySelector('.previous-page') const btnPreviousPage = rootEl.querySelector('.previous-page')
@@ -660,17 +685,24 @@ function initPagination(paginationRootElId, onPageChange, currentPage = 1) {
const pageInputEl = formEl.querySelector('input') const pageInputEl = formEl.querySelector('input')
const pagesCountDisplayEl = formEl.querySelector('.pages-total') const pagesCountDisplayEl = formEl.querySelector('.pages-total')
let reqLock = false numOfAllPages = +pagesCountDisplayEl.textContent
rootEl.addEventListener('click', handleButtonClick) backwardBtnEls.push(btnFirstPage, btnPreviousPage)
formEl.addEventListener('submit', handleFormSubmit) forwardBtnEls.push(btnNextPage, btnLastPage)
pageInputEls.push(pageInputEl)
pagesCountDisplayEls.push(pagesCountDisplayEl)
function handleButtonClick({ target }) { 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,22 +944,24 @@ 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
isI18nReady.then(t => {
$('.select-search-field').select2({}) $('.select-search-field').select2({})
initSelect2('.select-domain-field', 'Področje') initSelect2('.select-domain-field', t('Področje'))
initSelect2('.select-src-lang-field', 'Izvorni jezik') initSelect2('.select-src-lang-field', t('Jezik iskanja'))
initSelect2('.select-dest-lang-field', 'Ciljni jezik') initSelect2('.select-dest-lang-field', t('Ciljni jezik'))
initSelect2('.select-dict-field', 'Slovar') initSelect2('.select-dict-field', t('Slovar'))
initSelect2('.select-source-field', 'Vir') initSelect2('.select-source-field', t('Vir'))
$('b[role="presentation"]').hide() $('b[role="presentation"]').hide()
$('.select2-selection__arrow').append( $('.select2-selection__arrow').append(
@@ -970,48 +993,6 @@ if (registerSwithcButton) {
e.input = e.domElement.querySelector('.select2-search__field') e.input = e.domElement.querySelector('.select2-search__field')
}) })
}) })
/* TODO REMOVE OTHER SCRIPTS WHEN YOU FINISH MODULARIZING THINGS THAT COULD BE MODULARIZED */
$(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')
})
}) })
/* TODO REMOVE OTHER SCRIPTS WHEN YOU FINISH MODULARIZING THINGS THAT COULD BE MODULARIZED */ /* TODO REMOVE OTHER SCRIPTS WHEN YOU FINISH MODULARIZING THINGS THAT COULD BE MODULARIZED */
@@ -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()
+46 -300
View File
@@ -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,169 +272,21 @@ 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() { window.addEventListener('load', () => {
try { $('[data-toggle="tooltip"]').tooltip()
$('.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() isI18nReady.then(t => {
} 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)
// 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() { function handleProperTextDisplay() {
// const BROWSER_UNUSUAL_OFFSET = 17
if (/\/iskanje/.test(currentPagePath)) { if (/\/iskanje/.test(currentPagePath)) {
transferText('Iskanje po slovarjih', true) transferText(t('Iskanje po slovarjih'), true, 'site-heading') //,
// BROWSER_UNUSUAL_OFFSET
// )
} else if (/\/termin/.test(currentPagePath)) { } else if (/\/termin/.test(currentPagePath)) {
transferText('', true) transferText('', true, 'site-heading') // , BROWSER_UNUSUAL_OFFSET)
} }
} }
@@ -551,3 +296,4 @@ window.addEventListener('resize', () => {
}) })
handleProperTextDisplay() handleProperTextDisplay()
})
+6 -6
View File
@@ -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: [

Some files were not shown because too many files have changed in this diff Show More