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
.idea
express/public/stylesheets
/draft/
-1
View File
@@ -1,3 +1,2 @@
old_express
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
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
+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.
# Change if needed.
# PORTAL PARAMETERS
# 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
# Set it to true for production and properly configure a reverse proxy in front of it.
EXPRESS_IS_BEHIND_PROXY=false
# It is used for cookie signing.
EXPRESS_SECRET="weak_secret"
# Used for administration.
POSTGRES_ADMIN_PASSWORD="weak_admin_password" # user: postgres
# Used by the express webserver service.
POSTGRES_EXPRESS_PASSWORD="weak_express_password" # user: express
# Used by the concordancer service.
POSTGRES_CONCORDANCER_PASSWORD="weak_concordancer_password" # user: concordancer
# Exposed on host's localhost for administration purposes.
POSTGRES_LISTEN_PORT=5432
# Configuration for webserver to communicate with your SMTP provider.
SMTP_HOST="maildev"
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>"
# 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
# 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
# Development only settings
MAILDEV_WEB_GUI_PORT=3001
# Production only settings
# The origin, where the portal will be made available.
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
SMTP_HOST: "${SMTP_HOST:?}"
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:?}"
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:
- express-data:/usr/src/app/data_files
ports:
- "127.0.0.1:${EXPRESS_LISTEN_PORT:?}:3000"
postgres:
image: postgres:15-alpine
image: postgres:15.1-alpine3.17
restart: always
environment:
POSTGRES_PASSWORD: "${POSTGRES_ADMIN_PASSWORD:?}"
@@ -58,18 +65,18 @@ services:
environment:
- cluster.name=term-portal
- node.name=node-1
# - 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
- 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
- "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
- "discovery.type=single-node" # disables bootstrap checks that are enabled when network.host is set to a non-loopback address
# ulimits:
# memlock:
# soft: -1
# hard: -1
# nofile:
# soft: 65536 # maximum number of open files for the OpenSearch user, set to at least 65536 on modern systems
# hard: 65536
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536 # maximum number of open files for the OpenSearch user, set to at least 65536 on modern systems
hard: 65536
volumes:
- opensearch-data:/usr/share/opensearch/data
@@ -88,8 +95,10 @@ services:
- "127.0.0.1:${OS_DASHBOARDS_LISTEN_PORT:?}:5601"
concordancer:
# image: ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.0
build: concordancer
# image: ghcr.io/clarinsi/rsdo-concordancer-api-term-portal:v1.0.4
build:
context: concordancer
dockerfile: Dockerfile.prod
depends_on:
- postgres
- opensearch
@@ -106,7 +115,7 @@ services:
# Disabled by default. Enable if needed.
concordancer-manager:
image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.0
image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.4
profiles:
- concordancer-manager
depends_on:
@@ -137,7 +146,11 @@ services:
PGDATABASE: term_portal
SMTP_HOST: "${SMTP_HOST:?}"
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:?}"
entrypoint: ["scheduled/entrypoint.sh"]
command: ["crond", "-f", "-l", "2"]
+16 -5
View File
@@ -20,9 +20,16 @@ services:
PGDATABASE: term_portal
SMTP_HOST: "${SMTP_HOST:?}"
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:?}"
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:
# When developing/debugging concordancer, disable express bind mount and node_modules volume and enable express-data.
# - express-data:/usr/src/app/data_files
@@ -35,7 +42,7 @@ services:
tty: true
postgres:
image: postgres:15-alpine
image: postgres:15.1-alpine3.17
# Use any of the two custom Dockerfiles below for debugging PL/pgSQL functions.
# Disable docker-entrypoint-initdb.d bind mount when doing so.
# build:
@@ -95,7 +102,7 @@ services:
- "127.0.0.1:${OS_DASHBOARDS_LISTEN_PORT:?}:5601"
concordancer:
# image: ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.0
# image: ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.4
build: concordancer
profiles:
- develop-concordancer
@@ -114,7 +121,7 @@ services:
- sloleks:/sloleks
concordancer-manager:
image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.0
image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.4
profiles:
- develop-concordancer
depends_on:
@@ -146,7 +153,11 @@ services:
# PGDATABASE: term_portal
# SMTP_HOST: "${SMTP_HOST:?}"
# 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:?}"
# volumes:
# - /usr/src/app/node_modules
+30 -7
View File
@@ -6,16 +6,21 @@ const helmet = require('helmet')
const favicon = require('serve-favicon')
const cookieParser = require('cookie-parser')
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.
const { isBehindProxy, secret } = require('./config/keys')
const helmetConfig = require('./config/helmet')
const session = require('./middleware/session')
const i18n = require('./middleware/i18n')
const passport = require('./middleware/auth')
const user = require('./middleware/user')
const settings = require('./middleware/settings')
const { enhanceLocals } = require('./middleware')
const { enhanceLocals, adjustHeaders } = require('./middleware')
const { capitalize } = require('./utils')
// Import Routers.
const apiRouter = require('./routes/api')
@@ -37,22 +42,39 @@ app.locals.basedir = viewsPath
// Other settings.
const inDevEnv = app.get('env') === 'development'
if (isBehindProxy) app.set('trust proxy', 1) // Trust first proxy.
app.locals.inDevEnv = inDevEnv
app.locals.capitalize = capitalize
// Mount middleware.
app.use(logger('dev'))
app.use(helmet(helmetConfig))
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.urlencoded({ extended: true }))
app.use(cookieParser(secret))
app.use(session)
app.use(flash())
app.use(passport.initialize())
app.use(passport.session())
app.use(passport.authenticate('remember-me'))
app.use(i18n.determineRequestLanguage)
app.use(i18nextMiddleware.handle(i18next))
app.use(user.enhance)
app.use(settings.prepareRequiredSettings)
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.
// 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.
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.
let message, error
@@ -86,8 +109,8 @@ app.use((err, req, res, next) => {
} else {
message =
err.status === 404
? 'Stran ne obstaja'
: 'Prišlo je do strežniške napake. Poskusite kasneje.'
? req.t('Stran ne obstaja')
: req.t('Prišlo je do strežniške napake. Poskusite kasneje.')
error = {}
}
@@ -99,7 +122,7 @@ app.use((err, req, res, next) => {
}
// Render the error page.
res.render('error', { title: 'Napaka', message, error })
res.render('error', { title: req.t('Napaka'), message, error })
})
module.exports = app
+9 -5
View File
@@ -4,12 +4,14 @@
* Module dependencies.
*/
// Temporary noop function to be used until i18n is fully in place.
global.__ = str => str
const db = require('../models/db')
const cache = require('../models/cache')
const searchEngine = require('../models/search-engine')
const email = require('../models/email')
const { seedDummyData } = require('../models/comment')
const { initDemoData } = require('../models/demo-paginacija')
const init = require('../config/init')
const app = require('../app')
const debug = require('debug')('termPortal:server')
const http = require('http')
@@ -37,12 +39,12 @@ const server = http.createServer(app)
db.waitForConnection(),
cache.waitForConnection(),
searchEngine.waitForConnection(),
email.waitForConnection()
email.waitForConnection(),
init.fsStructure()
])
await searchEngine.initEntryIndex()
await searchEngine.initConsultancyEntryIndex()
seedDummyData()
// initDemoData()
await init.adminUser()
server.listen(port)
})()
@@ -102,5 +104,7 @@ function onError(error) {
function onListening() {
const addr = server.address()
const bind = typeof addr === 'string' ? 'pipe ' + addr : 'port ' + addr.port
// eslint-disable-next-line no-console
console.log('Started listening')
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',
smtpHost: process.env.SMTP_HOST,
smtpPort: process.env.SMTP_PORT,
smtpTlsRejectUnauthorized:
process.env.SMTP_TLS_REJECT_UNAUTHORIZED === 'true',
smtpUser: process.env.SMTP_USER,
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,
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
// 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'
// If you change this one, don't forget to also update the volume mount in docker-compose.prod.yml
// 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.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
}
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 {
pages_total: numberOfAllPages,
comments,
@@ -38,20 +57,55 @@ exports.createComment = async (req, res) => {
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) => {
const commentId = req.body.params.id
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)
res.send('Visibility changed')
}
+261 -26
View File
@@ -1,26 +1,57 @@
const user = require('../../../middleware/user')
const ConsultancyEntry = require('../../../models/consultancy-entry')
const Domain = require('../../../models/domain')
const User = require('../../../models/user')
const { promisify } = require('util')
const i18next = require('i18next')
const {
deleteConsultancyEntryFromIndex
} = require('../../../models/search-engine')
const email = require('../../../models/email')
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 = {}
consultancy.listEntries = async (req, res) => {
const consultancyEntryList = await ConsultancyEntry.fetchAll()
const data = {}
data.consEntryList = consultancyEntryList
res.send(data)
}
consultancy.sendPaginationData = async (req, res) => {
let requestType = req.query.type
const isAdminPage = req.query.isAdmin === 'true'
let page = +req.query.p || 1
consultancy.listNewEntries = async (req, res) => {
const consultancyNewEntryList = await ConsultancyEntry.fetchAllNew()
const data = {}
data.consultancyNewEntryList = consultancyNewEntryList
res.send(data)
if (page < 1) {
page = 1
}
if (isAdminPage) {
if (
req.user &&
(req.user.hasRole('portal admin') ||
req.user.hasRole('consultancy admin'))
) {
// Everything is allowed, nothing to do here
} else if (req.user && req.user.hasRole('consultant')) {
if (!(requestType === 'in progress' || requestType === 'published')) {
return res.status(400).send()
}
} else {
// for now, public can only see published entries
requestType = 'published'
}
}
return await consultancyRequestItems(
req,
res,
requestType || 'published',
'components/consultancy/api/consultancy-item-rendered',
isAdminPage,
!isAdminPage, // -> In current implementation, it is just the inverse of isAdminPage
page
)
}
consultancy.createQuestion = async (req, res) => {
@@ -35,7 +66,7 @@ consultancy.createQuestion = async (req, res) => {
const { description } = q
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
@@ -52,24 +83,46 @@ consultancy.createQuestion = async (req, res) => {
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)
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
// TODO SEND EMAIL
// TODOOOOOOOOO
const emails = await ConsultancyEntry.fetchConsultancyAdminEmails()
subjectText = req.t('Ustvarjeno novo vprašanje v svetovalnici')
emails = await ConsultancyEntry.fetchConsultancyAdminEmails()
} else {
subjectText = req.t('Novo vprašanje za Terminološko svetovalnico')
emails = await getInstanceSetting('zrc_email')
}
const renderAsync = promisify(req.app.render.bind(req.app))
const 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({
to: emails,
subject: 'Ustvarjeno novo vprašanje v svetovalnici',
subject: subjectText,
html: emailHtml
})
/// /////////////////
res.status(201).send()
}
@@ -179,9 +232,10 @@ consultancy.assign = async (req, res) => {
const renderAsync = promisify(req.app.render.bind(req.app))
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({
to: emails,
subject: 'Novo terminološko vprašanje',
subject: req.t('Novo terminološko vprašanje'),
html: emailHtml
})
@@ -234,9 +288,10 @@ consultancy.sendToReview = async (req, res) => {
const renderAsync = promisify(req.app.render.bind(req.app))
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({
to: emails,
subject: 'Potrditev objave',
subject: req.t('Potrditev objave'),
html: emailHtml
})
@@ -254,6 +309,22 @@ consultancy.publish = async (req, res) => {
if (!entry.title) {
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.indexIntoSearchEngine(questionId, true)
@@ -261,13 +332,23 @@ consultancy.publish = async (req, res) => {
res.send()
}
consultancy.updateQuestion = async (req, res) => {
const { id, questionTitle, domain: domainId, question, answer } = req.body
consultancy.updateQuestion = [
(req, res, next) => {
const { id } = req.body
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 === '') {
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)
@@ -281,7 +362,8 @@ consultancy.updateQuestion = async (req, res) => {
await ConsultancyEntry.indexIntoSearchEngine(id, true)
res.send({})
}
}
]
consultancy.insertNonModerator = async (req, res) => {
const questionId = req.body.question_id
@@ -318,4 +400,157 @@ consultancy.deleteQuestion = async (req, res) => {
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
@@ -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 })
}
+159 -15
View File
@@ -1,5 +1,8 @@
const { rm } = require('fs/promises')
const user = require('../../../middleware/user')
const Dictionary = require('../../../models/dictionary')
const Entry = require('../../../models/entry')
const Extraction = require('../../../models/extraction')
const {
searchEntryIndex,
deleteEntryFromIndex,
@@ -9,6 +12,7 @@ const genEditorAllQuery = require('../../../models/helpers/search/generate-query
const { prepareEditorEntries } = require('../../../models/helpers/search')
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
const { minEntriesRequirementCheckAndAct } = require('../../helpers/dictionary')
const { getExportFilesPath } = require('../../../models/helpers/dictionary')
const dictionary = {}
@@ -66,7 +70,11 @@ dictionary.deleteEntry = async (req, res) => {
await Promise.all([
Dictionary.updateMetadataAfterModifyingEntries(dictionaryId),
deleteEntryFromIndex(entryId, true),
minEntriesRequirementCheckAndAct.onDelete(dictionaryId, req.app)
minEntriesRequirementCheckAndAct.onDelete(
dictionaryId,
req.app,
req.determinedLanguage
)
])
res.end()
@@ -81,7 +89,11 @@ dictionary.deleteAllEntries = async (req, res) => {
await Promise.all([
Dictionary.updateMetadataAfterModifyingEntries(dictionaryId),
deleteDictionaryEntriesFromIndex(dictionaryId),
minEntriesRequirementCheckAndAct.onDelete(dictionaryId, req.app)
minEntriesRequirementCheckAndAct.onDelete(
dictionaryId,
req.app,
req.determinedLanguage
)
])
res.end()
@@ -115,16 +127,25 @@ dictionary.delete = async (req, res) => {
const dictionaryId = +req.params.dictionaryId
await Dictionary.delete(dictionaryId)
await deleteDictionaryEntriesFromIndex(dictionaryId)
const exportFilesPath = getExportFilesPath(dictionaryId)
await rm(exportFilesPath, { recursive: true, force: true })
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
await Dictionary.updateDomainLabel(dictionaryId, payload)
res.end()
}
}
]
dictionary.renovateSecondaryDomains = async (req, res) => {
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 { pages_total: numberOfAllPages, results } =
await Dictionary.fetchAllAdminDictionaries(resultsPerPage, page)
await Dictionary.fetchAllAdminDictionaries(
req.determinedLanguage,
resultsPerPage,
page
)
res.send({ page, numberOfAllPages, results })
}
@@ -167,23 +192,142 @@ dictionary.listDomainLabels = async (req, res) => {
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 { 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 { pages_total: numberOfAllPages, results } =
await Dictionary.fetchAllSecondaryDomains(resultsPerPage, page)
await Dictionary.fetchAllImports(dictionaryId, resultsPerPage, page)
res.send({ page, numberOfAllPages, results })
}
dictionary.extractionImport = async (req, res) => {
// TODO Import logic (Luka's task)
// const { id: dictionaryId, extractionId } = req.params
// 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({ dictionaryId, extractionId, fromIndex, toIndex })
res.send('IMPORTING')
dictionary.showExportToFileForm = 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 { pages_total: numberOfAllPages, results } =
await Dictionary.fetchExports(dictionaryId, resultsPerPage, page)
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
+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 { URLSearchParams } = require('url')
const multer = require('multer')
const i18next = require('i18next')
const validator = require('validator')
const axios = require('axios')
const {
getExtractionFilesPath,
getDocumentsPath,
getStopTermsPath,
getConllusPath
getConllusPath,
getFileStats
} = require('../../../models/helpers/extraction')
const { checkIfcanBegin } = require('../../helpers/extraction')
const Extraction = require('../../../models/extraction')
const Domain = require('../../../models/domain')
const email = require('../../../models/email')
const { intoDbArray } = require('../../../models/helpers')
const { origin } = require('../../../config/keys')
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
const { origin, extractionApiOrigin } = require('../../../config/keys')
const {
DEFAULT_HITS_PER_PAGE,
TEMP_EXPORT_PATH
} = require('../../../config/settings')
const MAX_FILE_NAME_LENGTH = 100
const MAX_FILE_SIZE = 10 ** 9 // 1 GB
@@ -77,16 +82,19 @@ extraction.docsList = async (req, res) => {
extraction.docsUpdate = async (req, res) => {
try {
await parseExtractionFileBody(req, res)
const fileStats = await getFileStats(req.file.path)
res.send(fileStats)
} catch (error) {
if (
error instanceof multer.MulterError &&
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
}
res.end()
}
extraction.docDelete = async (req, res) => {
@@ -108,16 +116,19 @@ extraction.stopTermsList = async (req, res) => {
extraction.stopTermsUpdate = async (req, res) => {
try {
await parseExtractionFileBody(req, res)
const fileStats = await getFileStats(req.file.path)
res.send(fileStats)
} catch (error) {
if (
error instanceof multer.MulterError &&
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
}
res.end()
}
extraction.stopTermDelete = async (req, res) => {
@@ -141,7 +152,7 @@ extraction.ossSearch = [
...(ossParams.keywords && { kljucneBesede: ossParams.keywords }),
...(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 canSave = documentCount && documentCount <= MAX_OSS_DOCUMENT_COUNT
@@ -155,7 +166,7 @@ extraction.ossSearch = [
extraction.ossConfirmParams = async (req, res) => {
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')
await Extraction.updateOssParams(extractionId, {
params: ossParams.params,
@@ -166,7 +177,7 @@ extraction.ossConfirmParams = async (req, res) => {
extraction.begin = async (req, res) => {
const extractionId = req.params.id
const extraction = await Extraction.fetch(extractionId)
const extraction = req.extractionData
const canBegin = await checkIfcanBegin(extraction)
if (!canBegin) throw Error('Extraction does not qualify to be ran')
@@ -183,6 +194,9 @@ extraction.begin = async (req, res) => {
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) {
// 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.
@@ -195,16 +209,24 @@ extraction.begin = async (req, res) => {
const extractionLink = new URL('/luscenje', origin)
const renderAsync = promisify(req.app.render.bind(req.app))
const authorEmail = await Extraction.fetchAuthorEmail(extractionId)
const emailHtml = await renderAsync('email/extraction-done', {
const { email: authorEmail, language: authorLanguage } =
await Extraction.fetchAuthorData(extractionId)
const emailHtml = await renderAsync(
`email/extraction-done_${authorLanguage}`,
{
extractionName,
extractionLink
})
}
)
await email.send({
to: authorEmail,
subject: 'Luščenje končano',
subject: i18next.t('Luščenje končano', { lng: authorLanguage }),
html: emailHtml
})
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
}
}
extraction.duplicate = async (req, res) => {
@@ -214,18 +236,36 @@ extraction.duplicate = async (req, res) => {
}
extraction.termCandidatesExport = async (req, res) => {
// TODO CSV logic (Luka's task)
// 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')
}
// TODO More formats (CSV, TSV, TXT, ...)
extraction.listFinishedForUser = async (req, res) => {
const extractions = await Extraction.fetchFinishedForUser(req.user.id)
res.send(extractions)
const extractionId = req.params.id
const { from, to } = req.query
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) => {
@@ -251,8 +291,11 @@ function extractionFileFilter(req, file, cb) {
fileType = 'stopTerms'
break
default:
return cb(Error('Invalid API endpoint'))
default: {
const customError = Error('Invalid API endpoint')
customError.displayInProd = true
return cb(customError)
}
}
const filenamePartsArray = file.originalname.split('.')
@@ -264,30 +307,32 @@ function extractionFileFilter(req, file, cb) {
(fileType === 'stopTerms' &&
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('.')
if (!fileName || fileName.length > MAX_FILE_NAME_LENGTH) {
return cb(
Error(
const customError = Error(
`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: '_' })) {
return cb(
Error(
const customError = Error(
'Filename must begin with an alphanumeric character or an underscore.'
)
)
customError.displayInProd = true
return cb(customError)
}
if (!validator.isAlphanumeric(fileName, 'sl-SI', { ignore: ' _-.' })) {
return cb(
Error(
const customError = Error(
'Filename can only contain alphanumeric characters, spaces, underscores, minuses and periods.'
)
)
customError.displayInProd = true
return cb(customError)
}
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.append('page', page)
res.append('number-of-all-pages', numberOfAllPages)
@@ -157,7 +178,8 @@ exports.listFilteredDictionaries = async (req, res) => {
hitsPerPage,
page,
orderAttribute,
orderIndex
orderIndex,
req.determinedLanguage
)
dictionaries = dictionaries.map(e => {
@@ -225,6 +247,9 @@ exports.showModalFilterResults = async (req, res) => {
const aggregationRaw = await searchEntryIndex(aggregateQuery)
const aggregation = await prepareAggregation(aggregationRaw)
const aggregation = await prepareAggregation(
aggregationRaw,
req.determinedLanguage
)
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 debug = require('debug')('termPortal:controllers/api/v1/system')
exports.handleCspReports = (req, res) => {
debug(req.body)
res.sendStatus(200)
// eslint-disable-next-line no-console
console.error(req.body)
res.end()
}
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 { 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 = {}
users.listUsers = async (req, res) => {
@@ -23,13 +32,103 @@ users.updateHitsPerPage = async (req, res) => {
res.status(200).send()
}
users.updateFristNameAndSurname = async (req, res) => {
const firstname = req.body.name
const surname = req.body.surname
users.updateBasicData = async (req, res) => {
const { firstName, lastName, email: newEmail } = req.body
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
+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 { searchConsultancyEntryIndex } = require('../models/search-engine')
const { prepareConsultancyEntries } = require('../models/helpers/search')
const { getInstanceSetting, intoDbArray } = require('../models/helpers')
// const { minEntriesRequirementCheckAndAct } = require('./helpers/dictionary')
const consultancy = {}
@@ -16,6 +17,9 @@ const consultancyAdmin = {}
consultancy.index = async (req, res) => {
req.indexHitPageAmount = '5'
res.locals.isOwnConsultancyEnabled =
(await getInstanceSetting('consultancy_type')) === 'own'
return await consultancyRequest(
req,
res,
@@ -25,31 +29,43 @@ consultancy.index = 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(
req,
res,
'published',
'pages/consultancy/search'
'pages/consultancy/search',
req.t('Odgovori')
)
}
consultancy.specificQuestion = async (req, res) => {
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 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 !== '')
let authorString
// TODO I18n
if (entry.answerAuthors.length === 1) {
authorString = 'Avtor'
authorString = req.t('Avtor')
} else if (entry.answerAuthors.length === 2) {
authorString = 'Avtorja'
authorString = req.t('Avtorja')
} else {
authorString = 'Avtorji'
authorString = req.t('Avtorji')
}
entry.domain = allPrimaryDomains.filter(
@@ -62,18 +78,28 @@ consultancy.specificQuestion = async (req, res) => {
entry.domain = false
}
res.locals.isOwnConsultancyEnabled =
(await getInstanceSetting('consultancy_type')) === 'own'
res.render('pages/consultancy/item-details', {
allPrimaryDomains,
authorString,
entry
entry,
title: req.t('Odgovor')
})
}
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', {
allPrimaryDomains
allPrimaryDomains,
title: req.t('Novo vprašanje')
})
}
@@ -83,19 +109,18 @@ consultancyAdmin.new = async (req, res) => {
res,
'new',
'pages/consultancy/admin/index',
req.t('Novo'),
true,
false
)
}
consultancyAdmin.users = async (req, res) => {
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
const users = await User.fetchConsultants()
res.render('pages/consultancy/admin/users', {
allPrimaryDomains,
users
users,
title: req.t('Svetovalci')
})
}
@@ -105,6 +130,7 @@ consultancyAdmin.rejected = async (req, res) => {
res,
'rejected',
'pages/consultancy/admin/rejected',
req.t('Zavrnjeno'),
true,
false
)
@@ -116,8 +142,10 @@ consultancyAdmin.published = async (req, res) => {
res,
'published',
'pages/consultancy/admin/published',
req.t('Objavljeno'),
true,
false
false,
'published'
)
}
@@ -127,6 +155,7 @@ consultancyAdmin.prepared = async (req, res) => {
res,
'review',
'pages/consultancy/admin/prepared',
req.t('Pripravljeno'),
true,
false
)
@@ -142,17 +171,14 @@ consultancyAdmin.inProgress = async (req, res) => {
res,
'in progress',
'pages/consultancy/admin/in-progress',
req.t('V delu'),
true,
false
)
}
consultancyAdmin.statistics = async (req, res) => {
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
res.render('pages/consultancy/admin/statistics', {
allPrimaryDomains
})
res.render('pages/consultancy/admin/statistics')
}
consultancyAdmin.edit = async (req, res) => {
@@ -162,17 +188,11 @@ consultancyAdmin.edit = async (req, res) => {
sentFrom[key] = true
const moderator = await ConsultancyEntry.getModerator(id)
const editors = await ConsultancyEntry.getEditors(id)
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')
}
// TODO i18n TIME FORMAT
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 isPublished = entry.status === 'published'
@@ -186,7 +206,8 @@ consultancyAdmin.edit = async (req, res) => {
author,
isPublished,
// 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
}
async function mapDomainIdToDomainNameSlovene(obj) {
async function mapDomainIdToDomainNameSlovene(obj, t) {
try {
const area = await Domain.fetchById(
obj.domainPrimaryId ? obj.domainPrimaryId : obj.domainPrimaryIdInitial
)
obj.area = area.nameSl
} catch {
obj.area = 'Ni področja'
obj.area = t('Ni področja')
}
return obj
@@ -227,14 +248,14 @@ function mapInitialValuesAsEmpty(obj) {
return obj
}
async function mapEntryList(list) {
async function mapEntryList(list, t) {
return await Promise.all(
list.map(entry => {
let entity = utils.compose(dateMap, mapInitialValuesAsEmpty)(entry)
// 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.
entity = utils.composeAsync(mapDomainIdToDomainNameSlovene)(entry)
entity = utils.composeAsync(mapDomainIdToDomainNameSlovene)(entry, t)
return entity
})
@@ -283,11 +304,28 @@ async function consultancyRequest(
res,
type,
url,
title = req.t('Svetovanje'),
isAdminPage = false,
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() ?? ''
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
if (
@@ -333,11 +371,11 @@ async function consultancyRequest(
let entries = prepareConsultancyEntries(hits)
// console.log({ entries, numberOfAllHits, numberOfAllPages })
// TODO I18n - nameSl
entries = entries.map(entry => {
entry.primaryDomain = entry.primaryDomain
? entry.primaryDomain.nameSl
: 'nedefinirano'
: req.t('nedefinirano')
if (entry.assignedConsultants) {
entry.firstName = entry.assignedConsultants[0]?.firstName
@@ -365,8 +403,6 @@ async function consultancyRequest(
return entry
})
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
// const entryList = await mapEntryList(inProgressEntryList)
const userList = await User.fetchConsultants()
@@ -401,7 +437,9 @@ async function consultancyRequest(
entries, // entryList,
userList,
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 multer = require('multer')
const debug = require('debug')('termPortal:controllers/dictionary')
const user = require('../middleware/user')
const Dictionary = require('../models/dictionary')
const Entry = require('../models/entry')
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 { searchEntryIndex } = require('../models/search-engine')
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 {
statusChangeCheckAndAct,
@@ -17,6 +19,8 @@ const {
} = require('./helpers/dictionary')
const SFDSuggestionImporter = require('./helpers/search-filter-data-suggestion-importer')
const Extraction = require('../models/extraction')
const { isGeneratorFunction } = require('util/types')
const { capitalize } = require('../utils')
const importFileBodyParser = multer({
dest: `${DATA_FILES_PATH}/dict_import_temp`,
@@ -36,26 +40,27 @@ const dictionary = {}
dictionary.list = async (req, res) => {
let dictionaries
if (req.isAuthenticated()) {
dictionaries = await Dictionary.fetchAllByUser(req.user.id)
dictionaries = await Dictionary.fetchAllByUser(
req.user.id,
req.determinedLanguage
)
}
res.render('pages/dictionaries/list', {
title: 'Seznam slovarjev',
title: req.t('Seznam slovarjev'),
dictionaries
})
}
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] =
await Promise.all([
Dictionary.fetchAllPrimaryDomains(),
Dictionary.fetchAllPrimaryDomains(req.determinedLanguage),
Dictionary.fetchAllApprovedSecondaryDomains(),
Dictionary.fetchAllLanguages(language)
Dictionary.fetchAllLanguages(req.determinedLanguage, true)
])
res.render('pages/dictionaries/new', {
title: 'Nov slovar',
title: req.t('Nov slovar'),
allPrimaryDomains,
allSecondaryDomains,
allLanguages
@@ -64,7 +69,7 @@ dictionary.new = async (req, res) => {
dictionary.create = async (req, res) => {
await Dictionary.create(req.body, req.user.id)
res.redirect('/slovarji/moji')
res.redirect(303, '/slovarji/moji')
}
dictionary.editDescription = async (req, res) => {
@@ -75,14 +80,14 @@ dictionary.editDescription = async (req, res) => {
dictionary,
associatedSecondaryDomains
] = await Promise.all([
Dictionary.fetchAllPrimaryDomains(),
Dictionary.fetchAllPrimaryDomains(req.determinedLanguage),
Dictionary.fetchAllApprovedSecondaryDomains(),
Dictionary.fetchEditDescription(dictionaryId),
Dictionary.fetchSecondaryDomains(dictionaryId)
])
res.render('pages/dictionaries/description', {
title: 'Ime in opis',
title: req.t('Osnovni podatki'),
allPrimaryDomains,
allSecondaryDomains,
dictionary,
@@ -101,14 +106,14 @@ dictionary.updateDescription = async (req, res) => {
Dictionary.updateSecondaryDomains(dictionaryId, body)
])
res.redirect('back')
res.redirect(303, 'back')
}
dictionary.editUsers = async (req, res) => {
const dictionaryId = req.params.dictionaryId
const [dictionary, userRights, entriesCount, minEntries, publishApproval] =
await Promise.all([
Dictionary.fetchEditUsers(dictionaryId),
Dictionary.fetchEditUsers(dictionaryId, req.determinedLanguage),
User.fetchAllWithDictionaryRights(dictionaryId),
Dictionary.countPublishedEntries(dictionaryId),
getInstanceSetting('min_entries_per_dictionary'),
@@ -125,7 +130,7 @@ dictionary.editUsers = async (req, res) => {
}
res.render(viewPath, {
title: 'Uporabniki',
title: req.t('Uporabniki'),
dictionary,
userRights,
entriesCount,
@@ -142,8 +147,9 @@ dictionary.updateUsers = async (req, res) => {
const newDictStatus = await determineNewStatus(isPublished)
const { nameSl, status: oldDictStatus } = await Dictionary.fetchEditUsers(
dictionaryId
const { name, status: oldDictStatus } = await Dictionary.fetchEditUsers(
dictionaryId,
req.determinedLanguage
)
await Promise.all([
@@ -162,22 +168,20 @@ dictionary.updateUsers = async (req, res) => {
dictionaryId,
isPublished,
oldDictStatus,
nameSl,
name,
req.app,
req.user
)
res.redirect('back')
res.redirect(303, 'back')
}
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 [dictionary, associatedLanguages, allLanguages] = await Promise.all([
Dictionary.fetchEditStructure(dictionaryId),
Dictionary.fetchLanguages(dictionaryId),
Dictionary.fetchAllLanguages(language)
Dictionary.fetchLanguages(dictionaryId, req.determinedLanguage),
Dictionary.fetchAllLanguages(req.determinedLanguage, true)
])
let viewPath
@@ -190,7 +194,7 @@ dictionary.editStructure = async (req, res) => {
}
res.render(viewPath, {
title: 'Struktura slovarskega sestavka',
title: req.t('Struktura slovarskega sestavka'),
dictionary,
associatedLanguages,
allLanguages
@@ -207,12 +211,15 @@ dictionary.updateStructure = async (req, res) => {
Dictionary.deleteLanguages(dictionaryId),
Dictionary.updateLanguages(dictionaryId, body)
])
res.redirect('back')
res.redirect(303, 'back')
}
dictionary.editAdvanced = async (req, res) => {
const { dictionaryId } = req.params
const dictionaryName = await Dictionary.fetchName(dictionaryId)
const dictionaryName = await Dictionary.fetchName(
dictionaryId,
req.determinedLanguage
)
let viewPath
switch (req.baseUrl) {
case '/slovarji':
@@ -223,7 +230,7 @@ dictionary.editAdvanced = async (req, res) => {
}
res.render(viewPath, {
title: 'Napredno',
title: req.t('Napredno'),
dictionary: { id: req.params.dictionaryId },
dictionaryName
})
@@ -239,7 +246,7 @@ dictionary.comments = async (req, res) => {
const [{ comments, pages_total: numberOfAllPages }, dictionaryName] =
await Promise.all([
Comment.list(filters, req.user, resultsPerPage, 1),
Dictionary.fetchName(dictionaryId)
Dictionary.fetchName(dictionaryId, req.determinedLanguage)
])
switch (req.baseUrl) {
@@ -251,7 +258,7 @@ dictionary.comments = async (req, res) => {
}
res.render(viewPath, {
title: 'Komentarji',
title: req.t('Komentarji'),
numberOfAllPages,
dictionary: { id: req.params.dictionaryId },
comments,
@@ -261,9 +268,11 @@ dictionary.comments = async (req, res) => {
dictionary.showImportFromFileForm = async (req, res) => {
const { dictionaryId } = req.params
const [imports, dictionaryName] = await Promise.all([
Dictionary.fetchAllImports(dictionaryId),
Dictionary.fetchName(dictionaryId)
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const [{ pages_total: numberOfAllPages, results }, dictionaryName] =
await Promise.all([
Dictionary.fetchAllImports(dictionaryId, resultsPerPage, 1),
Dictionary.fetchName(dictionaryId, req.determinedLanguage)
])
let viewPath
switch (req.baseUrl) {
@@ -275,9 +284,10 @@ dictionary.showImportFromFileForm = async (req, res) => {
}
res.render(viewPath, {
title: 'Uvoz iz datoteke',
title: req.t('Uvoz iz datoteke'),
dictionary: { id: dictionaryId },
imports,
numberOfAllPages,
results,
dictionaryName
})
}
@@ -286,10 +296,14 @@ dictionary.listAdminDictionaries = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const { pages_total: numberOfAllPages, results } =
await Dictionary.fetchAllAdminDictionaries(resultsPerPage, 1)
await Dictionary.fetchAllAdminDictionaries(
req.determinedLanguage,
resultsPerPage,
1
)
res.render('pages/admin/dictionaries-list', {
title: 'Struktura slovarjev',
title: req.t('Seznam slovarjev'),
numberOfAllPages,
results
})
@@ -304,7 +318,7 @@ dictionary.adminEditDescription = async (req, res) => {
associatedSecondaryDomains,
status
] = await Promise.all([
Dictionary.fetchAllPrimaryDomains(),
Dictionary.fetchAllPrimaryDomains(req.determinedLanguage),
Dictionary.fetchAllApprovedSecondaryDomains(),
Dictionary.fetchEditDescription(dictionaryId),
Dictionary.fetchSecondaryDomains(dictionaryId),
@@ -312,7 +326,7 @@ dictionary.adminEditDescription = async (req, res) => {
])
res.render('pages/admin/dictionary-description', {
title: 'Podatki',
title: req.t('Osnovni podatki'),
allPrimaryDomains,
allSecondaryDomains,
dictionary,
@@ -350,25 +364,29 @@ dictionary.updateAdminDescription = async (req, res) => {
newDictStatus,
oldDictStatus,
req.app,
req.determinedLanguage,
req.user
)
res.redirect('back')
res.redirect(303, 'back')
}
dictionary.showImportFromExtractionForm = async (req, res) => {
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)
let viewPath, title
switch (req.baseUrl) {
case '/slovarji':
viewPath = 'pages/dictionaries/extraction-import'
title = 'Uvoz'
title = req.t('Uvoz iz luščilnika')
break
case '/admin':
viewPath = 'pages/admin/dictionary-extraction-import'
title = 'Uvoz luščenje'
title = req.t('Uvoz iz luščilnika')
}
res.render(viewPath, {
@@ -381,7 +399,12 @@ dictionary.showImportFromExtractionForm = async (req, res) => {
dictionary.showExportToFileForm = async (req, res) => {
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
switch (req.baseUrl) {
case '/slovarji':
@@ -390,11 +413,12 @@ dictionary.showExportToFileForm = async (req, res) => {
case '/admin':
viewPath = 'pages/admin/dictionary-export'
}
res.render(viewPath, {
title: 'Izvoz',
dictionary: { id: req.params.dictionaryId },
dictionaryName
title: req.t('Izvoz'),
dictionary: { id: dictionaryId },
dictionaryName,
numberOfAllPages,
results
})
}
@@ -405,7 +429,7 @@ dictionary.editDomainLabels = async (req, res) => {
const [{ pages_total: numberOfAllPages, results }, dictionaryName] =
await Promise.all([
Dictionary.fetchPaginationDomainLabels(dictionaryId, resultsPerPage, 1),
Dictionary.fetchName(dictionaryId)
Dictionary.fetchName(dictionaryId, req.determinedLanguage)
])
let viewPath
@@ -418,7 +442,7 @@ dictionary.editDomainLabels = async (req, res) => {
}
res.render(viewPath, {
title: 'Področne oznake',
title: req.t('Področne oznake'),
dictionary: { id: dictionaryId },
numberOfAllPages,
results,
@@ -432,26 +456,24 @@ dictionary.showContent = async (req, res) => {
const [
hits,
canPublishEntriesInEdit,
dictionaryName,
structure,
languages,
entryDomainLabels
] = await Promise.all([
searchEntryIndex(hitsQuery),
getInstanceSetting('can_publish_entries_in_edit'),
Dictionary.fetchName(dictionaryId),
Dictionary.fetchEditStructure(dictionaryId),
Dictionary.fetchLanguages(dictionaryId),
Dictionary.fetchLanguages(dictionaryId, req.determinedLanguage),
Dictionary.fetchDomainLabels(dictionaryId)
])
const terms = prepareEditorEntries(hits)
res.render('pages/dictionaries/content', {
title: 'Vsebina slovarja',
title: req.t('Vsebina slovarja'),
terms,
canPublishEntriesInEdit,
dictionaryName,
dictionaryName: structure[`name${capitalize(req.determinedLanguage)}`],
structure,
languages,
dictionaryId,
@@ -466,7 +488,7 @@ dictionary.showSecondaryDomains = async (req, res) => {
await Dictionary.fetchAllSecondaryDomains(resultsPerPage, 1)
res.render('pages/admin/areas', {
title: 'Podpodročja',
title: req.t('Področne oznake'),
numberOfAllPages,
results
})
@@ -497,15 +519,33 @@ dictionary.dictionaryList = async (req, res) => {
hitsPerPage,
page,
orderAttribute,
orderIndex
orderIndex,
req.determinedLanguage
)
const numberOfAllHits = parseInt(
(await Dictionary.fetchAllDictionariesCount()).count
(await Dictionary.fetchAllDictionariesPublishedCount()).count
)
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 => {
if (!e.portalcode) {
@@ -518,7 +558,7 @@ dictionary.dictionaryList = async (req, res) => {
const isDictionaryListPage = true
res.render('pages/dictionaries/dictlist', {
title: 'Seznam slovarjev',
title: req.t('Seznam slovarjev'),
dictionaries,
allPrimaryDomains,
numberOfAllPages,
@@ -532,15 +572,19 @@ dictionary.dictionaryList = async (req, res) => {
dictionary.dictionaryDetails = async (req, res) => {
const { absolutePrevPath, sentFromEntryId } = req.query
const dictId = req.params.dictionaryId
const title = req.t('O slovarju')
const {
allPrimaryDomains,
sourceLanguages,
targetLanguages,
allDictionaryNames,
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 }
// TODO: integrate numberOfAllPages, commentCount with pug
@@ -555,7 +599,7 @@ dictionary.dictionaryDetails = async (req, res) => {
// check if it is a local dictionary
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')
}
@@ -579,7 +623,7 @@ dictionary.dictionaryDetails = async (req, res) => {
)
const structData = {
prevWindowTitle: 'Nazaj',
prevWindowTitle: req.t('Nazaj'),
dictName: dictionaryData[0].dictionarysl,
portalCode: dictionaryData[0].portalcode,
portalName: dictionaryData[0].portalname,
@@ -589,13 +633,14 @@ dictionary.dictionaryDetails = async (req, res) => {
languages: reducedData.languages ? reducedData.languages.join(', ') : ''
}
// TODO I18n
if (reducedData.author) {
if (reducedData.author.length > 2) {
structData.authorLabel = 'Avtorji'
structData.authorLabel = req.t('Avtorji')
} else if (reducedData.author.length === 2) {
structData.authorLabel = 'Avtorja'
structData.authorLabel = req.t('Avtorja')
} else if (reducedData.author.length === 1) {
structData.authorLabel = 'Avtor'
structData.authorLabel = req.t('Avtor')
}
}
@@ -623,7 +668,8 @@ dictionary.dictionaryDetails = async (req, res) => {
finalData,
numberOfAllPages,
comments,
commentCount
commentCount,
title
}) // 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) {
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)
}
-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) => {
@@ -39,7 +42,7 @@ extraction.create = async (req, res) => {
return res.redirect(303, 'back')
}
const extractionName = `Luščenje ${extractionCount + 1}`
const extractionName = req.t('Luščenje') + `${extractionCount + 1}`
const { extractionType } = req.body
let extractionId
@@ -60,31 +63,46 @@ extraction.create = async (req, res) => {
}
// 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) => {
const extractionId = req.params.id
const extraction = await Extraction.fetch(extractionId)
const extraction = req.extractionData
if (extraction.ossParams) {
const [allPrimaryDomains, stopTermsFiles] = await Promise.all([
Dictionary.fetchAllPrimaryDomains(),
const [allPrimaryDomains, ossDocumentTypes, stopTermsFiles] =
await Promise.all([
Dictionary.fetchAllPrimaryDomains(req.determinedLanguage),
Extraction.fetchOssDocumentTypes(req.determinedLanguage),
Extraction.fetchAllStopTermsFilesStats(extractionId)
])
const { params } = extraction.ossParams
const domainUdk = params?.domainUdk?.[0]
if (domainUdk)
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('pages/extraction/edit-oss', {
title: 'KAS + dokumenti',
title: req.t('Besedila'),
id: extractionId,
extraction,
allPrimaryDomains,
ossDocumentTypes,
stopTermsFiles
})
} else {
@@ -93,7 +111,7 @@ extraction.edit = async (req, res) => {
Extraction.fetchAllStopTermsFilesStats(extractionId)
])
res.render('pages/extraction/edit-own', {
title: 'Besedila',
title: req.t('Besedila'),
id: extractionId,
extraction,
extractionDocuments,
@@ -107,7 +125,7 @@ extraction.updateOwn = async (req, res) => {
await Extraction.update(extractionId, req.body.name)
// Reload page.
res.redirect(`./${extractionId}`)
res.redirect(303, `./${extractionId}`)
}
extraction.docsEdit = async (req, res) => {
@@ -117,6 +135,7 @@ extraction.docsEdit = async (req, res) => {
)
res.render('pages/extraction/docs-edit', {
title: req.t('Besedila'),
id: extractionId,
extractionDocuments
})
@@ -129,6 +148,7 @@ extraction.stopTermsEdit = async (req, res) => {
)
res.render('pages/extraction/stop-terms-edit', {
title: req.t('Stop termini'),
id: extractionId,
stopTermsFiles
})
@@ -145,6 +165,7 @@ extraction.listTermCandidates = async (req, res) => {
const firstPageOfTermCandidates = termCandidates.slice(0, hitsPerPage)
res.render('pages/extraction/term-candidates', {
title: req.t('Terminološki kandidati'),
extractionId,
termCandidatesJson,
firstPageOfTermCandidates,
+19 -15
View File
@@ -1,3 +1,5 @@
/* global __ */
const { getInstanceSetting } = require('../../models/helpers')
const Dictionary = require('../../models/dictionary')
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.minEntriesRequirementCheckAndAct = {
// 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')
// Only proceed if a valid and positive minimum entries per dictionary setting is set.
@@ -56,8 +58,8 @@ exports.minEntriesRequirementCheckAndAct = {
if (!isBelowMinEntriesThreshold || wasEmailAlreadySent) return
// Prepare and send notification emails.
const [nameSl, adminEmails, dictionariesAdminEmails] = await Promise.all([
Dictionary.fetchName(dictionaryId),
const [name, adminEmails, dictionariesAdminEmails] = await Promise.all([
Dictionary.fetchName(dictionaryId, determinedLanguage),
Dictionary.fetchAdminEmails(dictionaryId),
Dictionary.fetchDictionariesAdminEmails()
])
@@ -70,12 +72,13 @@ exports.minEntriesRequirementCheckAndAct = {
const renderAsync = promisify(appRef.render.bind(appRef))
const emailHtml = await renderAsync('email/dictionary-status-change', {
type,
nameSl
name
})
// TODO i18n - What language are the email title and content (we already have email translated)
await email.send({
to: allEmails,
subject: 'Obvestilo o številu gesel',
subject: __('Obvestilo o številu gesel'),
html: emailHtml
})
@@ -122,7 +125,7 @@ exports.statusChangeCheckAndAct = {
dictionaryId,
isPublishedNew,
oldDictStatus,
nameSl,
name,
appRef,
user
) {
@@ -133,12 +136,11 @@ exports.statusChangeCheckAndAct = {
const dictionariesAdminEmails =
await Dictionary.fetchDictionariesAdminEmails()
const type = 'unpublish'
await renderAndSendStatusChangeEmails(
appRef,
type,
user.email,
nameSl,
name,
dictionariesAdminEmails
)
@@ -155,7 +157,7 @@ exports.statusChangeCheckAndAct = {
appRef,
type,
user.email,
nameSl,
name,
dictionariesAdminEmails
)
}
@@ -168,11 +170,12 @@ exports.statusChangeCheckAndAct = {
statusNew,
statusOld,
appRef,
determinedLanguage,
user
) {
if (statusOld === 'reviewed' && statusNew !== 'reviewed') {
const [nameSl, adminEmails] = await Promise.all([
Dictionary.fetchName(dictionaryId),
const [name, adminEmails] = await Promise.all([
Dictionary.fetchName(dictionaryId, determinedLanguage),
Dictionary.fetchAdminEmails(dictionaryId)
])
const type = 'status'
@@ -181,7 +184,7 @@ exports.statusChangeCheckAndAct = {
appRef,
type,
user.email,
nameSl,
name,
adminEmails
)
}
@@ -193,19 +196,20 @@ async function renderAndSendStatusChangeEmails(
appRef,
type,
changerEmail,
nameSl,
name,
targetEmails
) {
const renderAsync = promisify(appRef.render.bind(appRef))
const emailHtml = await renderAsync('email/dictionary-status-change', {
type,
changerEmail,
nameSl
name
})
// TODO i18n - What language are the email title and content (we already have email translated)
await email.send({
to: targetEmails,
subject: 'Sprememba stanja slovarja',
subject: __('Sprememba stanja slovarja'),
html: emailHtml
})
}
@@ -4,25 +4,27 @@ const { getInstanceSetting } = require('../../models/helpers')
const helper = {}
helper.initialize = async () => {
helper.initialize = async determinedLanguage => {
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?
initializers.allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
initializers.sourceLanguages = await Dictionary.fetchAllLanguages(language)
initializers.targetLanguages = initializers.sourceLanguages.filter(
initializers.allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains(
determinedLanguage
)
const allLanguages = await Dictionary.fetchAllLanguages(determinedLanguage)
initializers.sourceLanguages = allLanguages
initializers.targetLanguages = allLanguages.filter(
// drop slovene language
l => l.id !== 32
)
initializers.allDictionaryNames = await Dictionary.fetchAll()
initializers.allDictionaryNames = await Dictionary.fetchAll(
determinedLanguage
)
initializers.portals = []
initializers.portals.push({
name: await getInstanceSetting('portal_name'),
name: await getInstanceSetting('portal_name_sl'),
code: await getInstanceSetting('portal_code')
})
+261 -30
View File
@@ -1,37 +1,45 @@
const { promisify } = require('util')
const Entry = require('../models/entry')
const { getInstanceSetting } = require('../models/helpers')
const Dictionary = require('../models/dictionary')
const Comment = require('../models/comment')
const { searchEntryIndex } = require('../models/search-engine')
const {
searchEntryIndex,
searchConsultancyEntryIndex
} = require('../models/search-engine')
const { intoDbArray } = require('../models/helpers')
const {
prepareEntries,
prepareAggregation,
prepareSeachFilterData
prepareSeachFilterData,
prepareConsultancyEntries
} = require('../models/helpers/search')
const email = require('../models/email')
const generateQuery = require('../models/helpers/search/generate-query')
const { DEFAULT_HITS_PER_PAGE } = require('../config/settings')
const SFDSuggestionImporter = require('./helpers/search-filter-data-suggestion-importer')
const User = require('../models/user')
const { capitalize } = require('../utils')
// TODO Luka (note to self): Measure performance, consider caching.
exports.index = async (req, res) => {
const { language } = req
const {
allPrimaryDomains,
sourceLanguages,
targetLanguages,
allDictionaryNames,
portals
} = await SFDSuggestionImporter.initialize()
const englishLanguageEnabled = false // dummy variable for future edit
} = await SFDSuggestionImporter.initialize(req.determinedLanguage)
const latestDicts = await Dictionary.fetchLatest3DictsByPublishDate(
englishLanguageEnabled
req.determinedLanguage
)
const portalName = await getInstanceSetting('portal_name')
const portalDescription = await getInstanceSetting('portal_description')
const portalName = await getInstanceSetting(`portal_name_${language}`)
const portalDescription = await getInstanceSetting(
`portal_description_${language}`
)
const isRoot = true
@@ -51,15 +59,17 @@ exports.index = async (req, res) => {
exports.search = async (req, res) => {
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,
sourceLanguages,
targetLanguages,
allDictionaryNames,
portals
} = await SFDSuggestionImporter.initialize()
} = await SFDSuggestionImporter.initialize(req.determinedLanguage)
const filters = {
sourceLanguages: intoDbArray(req.query.sl, 'always'),
@@ -81,6 +91,32 @@ exports.search = async (req, res) => {
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([
searchEntryIndex(hitsQuery),
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.
// const allAggregation = await indexAllResultsNoFiltering(req, res)
const aggregation = await prepareAggregation(aggregationRaw)
const aggregation = await prepareAggregation(
aggregationRaw,
req.determinedLanguage
)
const searchFilterData = prepareSeachFilterData(aggregation, filters)
// 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
}
// 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) {
return res.render('pages/search/no-results', {
allPrimaryDomains,
@@ -154,6 +217,8 @@ exports.search = async (req, res) => {
entriesByCategory,
searchFilterData,
disabledSideMenuFilters,
consultancyHits,
consultancyURL,
// allAggregation,
numberOfAllHits,
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?
res.render('pages/search/results', {
allPrimaryDomains,
@@ -172,15 +305,19 @@ exports.search = async (req, res) => {
entriesByCategory,
searchFilterData,
disabledSideMenuFilters,
consultancyHits,
consultancyURL,
// allAggregation,
numberOfAllHits,
numberOfAllPages,
page
page,
title
})
}
exports.entryDetails = async (req, res) => {
const termId = req.params.entryId
const title = req.t('Termin')
/* const [entry, domainLabels] = await Promise.all([
Entry.fetchFullWithOrderedForeignLanguages(termId),
@@ -188,12 +325,16 @@ exports.entryDetails = async (req, res) => {
]) */
const entry = await Entry.fetchFullWithOrderedForeignLanguages(termId)
/* const entryData = {
entry
// 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
const entryData = entry
@@ -203,12 +344,15 @@ exports.entryDetails = async (req, res) => {
targetLanguages,
allDictionaryNames,
portals
} = await SFDSuggestionImporter.initialize()
} = await SFDSuggestionImporter.initialize(req.determinedLanguage)
const [dictStruct, dictionaryData, selectedDomainLabelsForEntry] =
await Promise.all([
Dictionary.fetchDictionaryWithEditStructure(entry.dictionary_id),
Dictionary.fetchDictionaryBasicInfo(entry.dictionary_id),
Dictionary.fetchDictionaryBasicInfo(
entry.dictionary_id,
req.determinedLanguage
),
Entry.fetchDomainLabels(termId)
])
@@ -228,8 +372,8 @@ exports.entryDetails = async (req, res) => {
///
// check if it is a local dictionary
if (!dictionaryData.portalname && !dictionaryData.portalcode) {
dictionaryData[0].portalname = await getInstanceSetting('portal_name')
if (!dictionaryData.portalnamesl && !dictionaryData.portalcode) {
dictionaryData[0].portalname = await getInstanceSetting('portal_name_sl')
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)
const structData = {
termId: termId,
prevWindowTitle: 'Iskanje',
prevWindowTitle: req.t('Iskanje'),
prevHref: '/iskanje',
portalCode: dictionaryData[0].portalcode,
portalName: dictionaryData[0].portalname,
dictName: structure.nameSl,
dictName: structure[`name${capitalize(req.determinedLanguage)}`],
dictHref: `/slovarji/${structure.id}/o-slovarju?sentFromEntryId=${termId}`,
fullAuthorName: reducedData.author ? reducedData.author.join(', ') : '',
areas: dictionaryData[0].domain_primary,
@@ -270,13 +414,14 @@ exports.entryDetails = async (req, res) => {
languages: reducedData.languages ? reducedData.languages.join(', ') : ''
}
// TODO I18n
if (reducedData.author) {
if (reducedData.author.length > 2) {
structData.authorLabel = 'Avtorji'
structData.authorLabel = req.t('Avtorji')
} else if (reducedData.author.length === 2) {
structData.authorLabel = 'Avtorja'
structData.authorLabel = req.t('Avtorja')
} 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
}) */
const langs = await Dictionary.fetchLanguages(entryData.dictionary_id)
const langs = await Dictionary.fetchLanguages(
entryData.dictionary_id,
req.determinedLanguage
)
entryData.foreign_entries.forEach((val, idx) => {
try {
entry.foreign_entries[idx].name_sl = langs[idx].nameSl
entry.foreign_entries[idx].name_en = langs[idx].nameEn
entry.foreign_entries[idx].name = langs[idx].name
} catch (e) {}
})
@@ -321,27 +468,91 @@ exports.entryDetails = async (req, res) => {
selectedDomainLabelsForEntryString,
numberOfAllPages,
comments,
commentCount
commentCount,
title
})
}
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) => {
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) => {
const hitsPerPageArr = await User.fetchAllowedHitsPerPage()
res.render('pages/profile/change-profile-settings', {
title: 'Nastavitve računa',
title: req.t('Nastavitve računa'),
hitsPerPageArr,
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(
domainList,
aggregationFn = (acc, n) => {
@@ -384,3 +595,23 @@ function filterResults(entries) {
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()
res.render('pages/admin/portal', {
title: 'Nastavitve portala',
title: req.t('Nastavitve portala'),
portal
})
}
@@ -17,14 +17,14 @@ portal.updateInstaceSettings = async (req, res) => {
const payload = req.body
await Portal.updateInstaceSettings(payload)
await clearCachedInstanceSettings()
res.redirect('/admin/nastavitve/portal')
res.redirect(303, '/admin/nastavitve/portal')
}
portal.instanceDictSettings = async (req, res) => {
const dictionary = await Portal.fetchInstanceDictSettings()
res.render('pages/admin/settings-dictionaries', {
title: 'Nastavitve slovarjev',
title: req.t('Nastavitve slovarjev'),
dictionary
})
}
@@ -33,13 +33,13 @@ portal.updateInstanceDictSettings = async (req, res) => {
const payload = req.body
await Portal.updateInstaceDictSettings(payload)
await clearCachedInstanceSettings()
res.redirect('/admin/nastavitve/slovarji')
res.redirect(303, '/admin/nastavitve/slovarji')
}
portal.instanceConsultancySettings = async (req, res) => {
const consultancy = await Portal.fetchInstanceConsultancySettings()
res.render('pages/admin/portal-consultancy-settings', {
title: 'Nastavitve svetovalnice',
title: req.t('Nastavitve svetovalnice'),
consultancy
})
}
@@ -48,12 +48,12 @@ portal.updateInstanceConusltacySettings = async (req, res) => {
const payload = req.body
await Portal.updateInstaceConsultancySettings(payload)
await clearCachedInstanceSettings()
res.redirect('/admin/nastavitve/svetovalnica')
res.redirect(303, '/admin/nastavitve/svetovalnica')
}
portal.new = async (req, res) => {
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()
res.render('pages/admin/connections-list', {
title: 'Seznam povezav',
title: req.t('Seznam povezav'),
allLinkedPortals
})
}
@@ -70,14 +70,17 @@ portal.fetchPortal = async (req, res) => {
const portalId = req.params.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) => {
const portalId = req.params.portalId
const payload = req.body
await Portal.update(portalId, payload)
res.redirect('/admin/povezave/seznam')
res.redirect(303, '/admin/povezave/seznam')
}
portal.fetchSelectedLinkedDictionaries = async (req, res) => {
@@ -88,7 +91,7 @@ portal.fetchSelectedLinkedDictionaries = async (req, res) => {
await Portal.fetchSelectedLinkedDictionaries(linkedId, resultsPerPage, 1)
res.render('pages/admin/portal-list-dict', {
title: 'Slovarji portala',
title: req.t('Slovarji portala'),
linkedId,
numberOfAllPages,
results
@@ -98,7 +101,7 @@ portal.fetchSelectedLinkedDictionaries = async (req, res) => {
portal.updateSelectedDictionaries = async (req, res) => {
const linkedId = req.params.portalId
await Portal.updateSelectedDictionaries(linkedId, req.body)
res.redirect('back')
res.redirect(303, 'back')
}
portal.fetchAllLinkedDictionaries = async (req, res) => {
@@ -108,7 +111,7 @@ portal.fetchAllLinkedDictionaries = async (req, res) => {
await Portal.fetchAllLinkedDictionaries(resultsPerPage, 1)
res.render('pages/admin/portals-all-linked-dictionaries', {
title: 'Povezani',
title: req.t('Povezani slovarji'),
numberOfAllPages,
results
})
@@ -116,7 +119,7 @@ portal.fetchAllLinkedDictionaries = async (req, res) => {
portal.updateAllDictionaries = async (req, res) => {
await Portal.updateAllDictionaries(req.body)
res.redirect('back')
res.redirect(303, 'back')
}
portal.comments = async (req, res) => {
@@ -130,7 +133,7 @@ portal.comments = async (req, res) => {
1
)
res.render('pages/admin/comments', {
title: 'Komentarji',
title: req.t('Komentarji'),
numberOfAllPages,
dictionary: { id: req.params.dictionaryId },
comments
+122 -23
View File
@@ -3,6 +3,7 @@ const { promisify } = require('util')
const passport = require('passport')
const User = require('../models/user')
const email = require('../models/email')
const { logout: logoutUser } = require('../middleware/user')
const { origin } = require('../config/keys')
const { rememberMeCookieSettings } = require('../config/settings')
const RandomBytesAsync = promisify(randomBytes)
@@ -12,7 +13,14 @@ const user = {}
user.register = async (req, res) => {
// 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')
await User.saveActivationToken(userId, activationToken)
const { email: userEmail, username } = req.body
@@ -20,34 +28,46 @@ user.register = async (req, res) => {
activationLink.searchParams.set('token', activationToken)
activationLink = activationLink.href
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,
activationLink
})
await email.send({
to: userEmail,
subject: 'Aktivacija računa',
subject: req.t('Aktivacija računa'),
html: emailHtml
})
res.send('Registracija uspešna')
res.send(req.t('Registracija uspešna'))
}
user.activateAccount = async (req, res) => {
// TODO Add validation. What if user is already logged in? What if account is already active? ...
const { token } = req.query
const user = await User.fetchByActivationToken(token)
await User.activateAccount(user)
if (!token) return res.redirect(303, '/')
const user = await User.activateAccountWithToken(token, req.t)
const loginAsync = promisify(req.login.bind(req))
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) => {
passport.authenticate(
'local',
{
badRequestMessage:
badRequestMessage: req.t(
'Nepravilno uporabniško ime, elektronski naslov ali geslo.'
)
},
async (err, user, info) => {
if (err) return next(err)
@@ -69,23 +89,96 @@ user.login = async (req, res, next) => {
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)
}
user.logout = async (req, res) => {
const rememberMeToken = req.signedCookies.remember_me
user.logout = [logoutUser, (req, res) => res.redirect(303, '/')]
if (rememberMeToken) {
res.clearCookie('remember_me')
await User.clearRememberMeToken(rememberMeToken)
user.generateResetPasswordToken = async (req, res) => {
const { usernameOrEmail } = req.body
const user = await User.fetchByUsernameOrEmail(usernameOrEmail)
if (!user) {
return res
.status(400)
.send(req.t('Nepravilno uporabniško ime ali elektronski naslov.'))
}
req.logout()
// Manually clear session.passport due to bug in current passport version.
delete req.session.passport.user
res.redirect('/')
if (user.status !== 'active') {
return res
.status(400)
.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) => {
@@ -96,7 +189,7 @@ user.list = async (req, res) => {
1
)
res.render('pages/admin/user-list', {
title: 'Seznam slovarjeva',
title: req.t('Seznam uporabnikov'),
numberOfAllPages,
results
})
@@ -104,7 +197,10 @@ user.list = async (req, res) => {
user.listAllWithPortalRoles = async (req, res) => {
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) => {
@@ -115,7 +211,7 @@ user.findByUsernameOrEmail = async (req, res) => {
user.updateRoles = async (req, res) => {
await User.updatePortalRoles(req.body.rolesPerUser)
res.redirect('back')
res.redirect(303, 'back')
}
user.adminEdit = async (req, res) => {
@@ -124,8 +220,11 @@ user.adminEdit = async (req, res) => {
User.fetchUser(userId),
User.fetchUserRoles(userId)
])
if (userData.status === 'closed') return res.redirect(303, '/')
res.render('pages/admin/user-edit', {
title: 'Urejanje uporabnikov',
title: req.t('Uporabnik'),
userData,
userRoles
})
@@ -136,7 +235,7 @@ user.adminEdit = async (req, res) => {
user.adminUpdate = async (req, res) => {
const { userId } = req.params
await User.updateUser(userId, req.body)
res.redirect('back')
res.redirect(303, 'back')
}
module.exports = user
+14 -10
View File
@@ -21,6 +21,9 @@ passport.deserializeUser(async (id, done) => {
const user = await User.fetchDeserializedDataById(id)
// 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)
} catch (error) {
done(error)
@@ -29,25 +32,24 @@ passport.deserializeUser(async (id, done) => {
passport.use(
new LocalStrategy(
{ usernameField: 'usernameOrEmail' },
async (usernameOrEmail, password, done) => {
{ passReqToCallback: true, usernameField: 'usernameOrEmail' },
async (req, usernameOrEmail, password, done) => {
try {
const { rows } = await db.query(
'SELECT id, status, bcrypt_hash FROM "user" WHERE username = $1 OR email = $1',
[usernameOrEmail]
)
const user = rows[0]
const user = await User.fetchByUsernameOrEmail(usernameOrEmail)
if (!user) {
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') {
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.'
)
})
}
@@ -57,7 +59,9 @@ passport.use(
)
if (!isCorrectPassword) {
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) => {
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()
}
+99 -3
View File
@@ -1,3 +1,5 @@
const User = require('../models/user')
const user = {}
user.enhance = (req, res, next) => {
@@ -14,6 +16,22 @@ user.enhance = (req, res, 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) => {
const { dictionaryId } = req.params
const isAdmin = req.user.hasDictionaryRole(dictionaryId, 'administration')
@@ -21,17 +39,95 @@ user.isDictionaryAdmin = (req, res, next) => {
if (isAdmin) return next()
if (req.isAjax) return res.status(400).end()
res.redirect(req.baseUrl)
res.redirect(303, req.baseUrl || '/')
}
user.isDictionaryEditor = (req, res, next) => {
const { dictionaryId } = req.params
const dictionaryId = req.params.dictionaryId || req.dictionaryId
const isEditor = req.user.hasAnyDictionaryRole(dictionaryId)
if (isEditor) return next()
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 debug = require('debug')('termPortal:models/comment')
const User = require('../models/user')
const Entry = require('./entry')
// const debug = require('debug')('termPortal:models/comment')
class Comment {
// Deserialize flat data into an organized comment object.
@@ -80,15 +80,18 @@ class Comment {
}
break
case 'entry_dict_ext':
case 'entry_dict_ext': {
const { dictionary_id: dictionaryId } = await Entry.fetch(ctxId)
if (
user.hasRole('portal admin') ||
user.hasRole('consultancy admin') ||
user.hasDictionaryRole(ctxId, 'administration')
user.hasRole('dictionaries admin') ||
user.hasDictionaryRole(dictionaryId, 'administration')
) {
isCommentModerator = true
}
break
}
default:
throw Error('Invalid context type')
@@ -185,6 +188,18 @@ class Comment {
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) {
const values = [id, status]
const text = `
@@ -193,240 +208,6 @@ class Comment {
WHERE id = $2`
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
+2 -51
View File
@@ -55,21 +55,9 @@ class ConsultancyEntry {
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
// to_char(time_created,'HH24:MI:SS DD/MM/YYYY')
// TODO i18n date format
static async fetchByIdWithFormattedTime(id) {
const { rows: fetchedConsEntry } = await db.query(
`
@@ -161,24 +149,6 @@ class ConsultancyEntry {
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.
static async fetchAllByStatusCount(status) {
// TODO Luka: Miha, define specific fields instead of using *.
@@ -310,12 +280,6 @@ class ConsultancyEntry {
return deserializedConsEntries
}
// Fetch all new consultancy entries from DB.
static async fetchAllNew() {
const newEntries = await this.fetchAllByStatus('new')
return newEntries
}
static async fetchAllRejected() {
const newEntries = await this.fetchAllByStatusWithAuthorData('rejected')
return newEntries
@@ -437,20 +401,6 @@ class ConsultancyEntry {
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) {
const sqlQuery = `SELECT answer_authors FROM "consultancy_entry"
WHERE id=$1;`
@@ -679,6 +629,7 @@ class ConsultancyEntry {
}
// (Re)index specific consultancy entry into consultancy search index.
// TODO i18n name_sl
static async indexIntoSearchEngine(entryId, shouldWait) {
const values = [entryId]
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 {
smtpHost,
smtpPort,
smtpTlsRejectUnauthorized,
smtpUser,
smtpPassword,
smtpSecure,
smtpRequireTls,
smtpAllowInvalidCerts,
smtpFrom
} = require('../config/keys')
const options = {
host: smtpHost,
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 transporter = nodemailer.createTransport(options, defaults)
+67 -100
View File
@@ -1,7 +1,10 @@
const db = require('./db')
const { searchEngineClient, ENTRY_INDEX } = require('./search-engine')
const { intoDbArray, getInstanceSetting, removeHtmlTags } = require('./helpers')
const { prepareEntryForIndexing } = require('./helpers/dictionary')
const {
prepareEntryForIndexing,
sanitizeField
} = require('./helpers/dictionary')
const Entry = {}
@@ -10,7 +13,7 @@ Entry.create = async (userId, dictionaryId, entry) => {
const pickedLinks = intoDbArray(entry.links, 'always')
const pickedType = intoDbArray(entry.type, 'always')
const links = pickedLinks.map((link, index) => ({
link,
link: sanitizeField.toMixedBasic(link),
type: pickedType[index]
}))
const foreign = intoDbArray(entry.foreign, 'always')
@@ -18,9 +21,13 @@ Entry.create = async (userId, dictionaryId, entry) => {
if (row.term || row.definition || row.synonym) {
agg.push({
language: row.code,
terms: intoDbArray(row.term, 'undefined'),
definition: row.definition || null,
synonyms: intoDbArray(row.synonym, 'undefined')
terms: intoDbArray(row.term, 'undefined')?.map(term =>
sanitizeField.toMixedBasic(term)
),
definition: sanitizeField.toMixedExtended(row.definition) || null,
synonyms: intoDbArray(row.synonym, 'undefined')?.map(synonym =>
sanitizeField.toMixedBasic(synonym)
)
})
}
return agg
@@ -34,22 +41,26 @@ Entry.create = async (userId, dictionaryId, entry) => {
dictionaryId,
isValid,
entry.status,
entry.term || null,
sanitizeField.toMixedBasic(entry.term) || null,
userId,
entry.homonymSort || null,
entry.wordforms || null,
entry.accent || null,
entry.pronunciation,
intoDbArray(entry.domainLabels, 'always'),
entry.label || null,
entry.definition || null,
intoDbArray(entry.synonyms),
entry.pronunciation || null,
intoDbArray(entry.domainLabels, 'always').map(label =>
sanitizeField.toText(label)
),
sanitizeField.toMixedExtended(entry.label) || null,
sanitizeField.toMixedExtended(entry.definition) || null,
intoDbArray(entry.synonyms)?.map(synonym =>
sanitizeField.toMixedBasic(synonym)
),
links,
entry.other || null,
sanitizeField.toMixedOther(entry.other) || null,
foreignLanguageContent,
intoDbArray(entry.image),
intoDbArray(entry.audio),
intoDbArray(entry.video)
intoDbArray(entry.image)?.map(image => sanitizeField.toText(image)),
intoDbArray(entry.audio)?.map(audio => sanitizeField.toText(audio)),
intoDbArray(entry.video)?.map(video => sanitizeField.toText(video))
]
const text = `SELECT entry_new (${db.genParamStr(values)})`
@@ -60,71 +71,6 @@ Entry.create = async (userId, dictionaryId, entry) => {
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.
Entry.fetchFull = async entryId => {
const text = `
@@ -184,7 +130,12 @@ Entry.fetchFull = async entryId => {
SELECT jsonb_strip_nulls(
jsonb_build_object(
'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
@@ -232,6 +183,7 @@ Entry.fetchFullWithOrderedForeignLanguages = async entryId => {
'audio', e.audio,
'video', e.video,
'time_modified', e.time_modified,
'external_url', e.external_url,
'domain_labels', ARRAY(
SELECT name
FROM entry_domain_label edl
@@ -421,6 +373,8 @@ Entry.deleteAllLinks = async dictionaryId => {
// (Re)index specific entry into entry search index.
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 text = `
SELECT
@@ -465,7 +419,7 @@ Entry.indexIntoSearchEngine = async (entryId, shouldWait) => {
)
)
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
)
)
@@ -495,10 +449,11 @@ Entry.indexIntoSearchEngine = async (entryId, shouldWait) => {
const { dictionary, primary_domain: primaryDomain } = 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 = {
code: await getInstanceSetting('portal_code'),
name: await getInstanceSetting('portal_name')
name: await getInstanceSetting('portal_name_sl')
}
entry = prepareEntryForIndexing(entry)
@@ -556,7 +511,7 @@ Entry.update = async (userId, entry) => {
const pickedLinks = intoDbArray(entry.links, 'always')
const pickedType = intoDbArray(entry.type, 'always')
const links = pickedLinks.map((link, index) => ({
link,
link: sanitizeField.toMixedBasic(link),
type: pickedType[index]
}))
const foreign = intoDbArray(entry.foreign, 'always')
@@ -564,9 +519,13 @@ Entry.update = async (userId, entry) => {
if (row.term || row.definition || row.synonym) {
agg.push({
language: row.code,
terms: intoDbArray(row.term, 'undefined'),
definition: row.definition || null,
synonyms: intoDbArray(row.synonym, 'undefined')
terms: intoDbArray(row.term, 'undefined')?.map(term =>
sanitizeField.toMixedBasic(term)
),
definition: sanitizeField.toMixedExtended(row.definition) || null,
synonyms: intoDbArray(row.synonym, 'undefined')?.map(synonym =>
sanitizeField.toMixedBasic(synonym)
)
})
}
return agg
@@ -582,19 +541,23 @@ Entry.update = async (userId, entry) => {
!!entry.isTerminologyReviewed,
!!entry.isLanguageReviewed,
entry.status,
entry.term || null,
sanitizeField.toMixedBasic(entry.term) || null,
userId,
entry.homonymSort || null,
intoDbArray(entry.domainLabels, 'always'),
entry.label || null,
entry.definition || null,
intoDbArray(entry.synonyms),
intoDbArray(entry.domainLabels, 'always').map(label =>
sanitizeField.toText(label)
),
sanitizeField.toMixedExtended(entry.label) || null,
sanitizeField.toMixedExtended(entry.definition) || null,
intoDbArray(entry.synonyms)?.map(synonym =>
sanitizeField.toMixedBasic(synonym)
),
links,
entry.other || null,
sanitizeField.toMixedOther(entry.other) || null,
foreignLanguageContent,
intoDbArray(entry.image),
intoDbArray(entry.audio),
intoDbArray(entry.video)
intoDbArray(entry.image)?.map(image => sanitizeField.toText(image)),
intoDbArray(entry.audio)?.map(audio => sanitizeField.toText(audio)),
intoDbArray(entry.video)?.map(video => sanitizeField.toText(video))
]
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.
Entry.fetchVersionSnapshot = async (entryId, version) => {
const {
rows: [{ version_snapshot: historySnapshot }]
rows: [{ version_snapshot: historySnapshot, author }]
} = 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]
)
return historySnapshot
return { data: historySnapshot, author }
}
/* Fetch by language and entry Id. Note that this version includes the language name */
+275 -60
View File
@@ -12,13 +12,14 @@ const {
getFileNamesInFolder,
getFileStatsInFolder
} = require('./helpers/extraction')
const { extractionApiOrigin } = require('../config/keys')
const Extraction = {}
// Fetch all extractions for a specific user.
Extraction.fetchAllForUser = async userId => {
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]
)
@@ -67,26 +68,35 @@ Extraction.fetch = async id => {
const {
rows: [fetchedExtraction]
} = 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]
)
return deserialize.extraction(fetchedExtraction)
}
// Fetch author email of a specific extraction entry from DB.
Extraction.fetchAuthorEmail = async id => {
// Fetch oss document types from DB.
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 {
rows: [{ email }]
rows: [authorData]
} = await db.query(
`SELECT u.email
`SELECT u.email, u.language
FROM extraction e
LEFT JOIN "user" u ON u.id = e.user_id
WHERE e.id = $1`,
[id]
)
return email
return authorData
}
// Update extraction entry in DB.
@@ -160,6 +170,18 @@ Extraction.fetchTermCandidatesCount = async function (extractionId) {
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.
Extraction.beginOwn = async (extractionId, documentsNames) => {
let timeStarted
@@ -195,20 +217,20 @@ Extraction.beginOwn = async (extractionId, documentsNames) => {
Extraction.beginOss = async extractionId => {
let timeStarted
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 }]
}
] = 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
@@ -235,24 +257,34 @@ Extraction.processOwn = async function (extractionId, extractionName) {
const documentNames = await this.fetchAllDocumentsNames(extractionId)
const conllusPath = getConllusPath(extractionId)
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.
for (const documentName of documentNames) {
const filePath = `${documentsPath}/${documentName}`
try {
const { data: data1 } = await retry(
async () => {
const form = new FormData()
form.append('file', createReadStream(filePath), documentName)
try {
const { data: data1 } = await axios.post(
'http://rsdo.lhrs.feri.um.si:8080/datotekaVConlluAsync',
return await axios.post(
`${extractionApiOrigin}/datotekaVConlluAsync`,
form,
{
headers: {
...form.getHeaders()
},
maxBodyLength: MAX_BODY_LENGTH
}
}
)
},
RETRY_SECONDS_INTERVAL,
RETRY_SECONDS_MAX
)
const remotejobId = +data1.check_job_url.split('/').at(-1)
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]
)
@@ -261,11 +293,21 @@ Extraction.processOwn = async function (extractionId, extractionName) {
// Poll job until finished.
while (true) {
await sleep(5)
const { data: data2 } = await axios.get(
`http://rsdo.lhrs.feri.um.si:8080/job/${remotejobId}`
const { data: data2 } = await retry(
async () => {
return await axios.get(`${extractionApiOrigin}/job/${remotejobId}`)
},
RETRY_SECONDS_INTERVAL,
RETRY_SECONDS_MAX
)
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?).
const fileSavePath = `${conllusPath}/${documentName}.conllu`
await writeFile(fileSavePath, data2.job_result)
@@ -277,7 +319,8 @@ Extraction.processOwn = async function (extractionId, extractionName) {
break
}
}
} catch {
} catch (error) {
logExtractionError(error, 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()))
}
stopTermsSet.delete('')
const stopTermsArr = Array.from(stopTermsSet)
const termCandidatesPath = getTermCandidatesPath(extractionId)
try {
const { data: data3 } = await axios.post(
'http://rsdo.lhrs.feri.um.si:8080/izlusciAsync',
const { data: data3 } = await retry(
async () => {
return await axios.post(
`${extractionApiOrigin}/izlusciAsync`,
{
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)
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', '']
)
// Poll job until finished.
while (true) {
await sleep(5)
const { data: data4 } = await axios.get(
`http://rsdo.lhrs.feri.um.si:8080/job/${remotejobId}`
const { data: data4 } = await retry(
async () => {
return await axios.get(`${extractionApiOrigin}/job/${remotejobId}`)
},
RETRY_SECONDS_INTERVAL,
RETRY_SECONDS_MAX
)
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?).
await writeFile(termCandidatesPath, JSON.stringify(data4.job_result))
// TODO Once returned JSON is properly formed, use the bottom line instead.
// await writeFile(termCandidatesPath, data4.job_result.terminoloski_kandidati)
await writeFile(termCandidatesPath, JSON.stringify(jobResult))
await db.query(
"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', '']
@@ -340,8 +404,10 @@ Extraction.processOwn = async function (extractionId, extractionName) {
break
}
}
} catch {
} catch (error) {
logExtractionError(error, extractionId, 'conllus to term candidates')
await failTheJob(extractionId, 'conllus to term candidates', '')
await skipConcordancerJob(extractionId)
await failExtraction(extractionId)
return
}
@@ -350,7 +416,7 @@ Extraction.processOwn = async function (extractionId, extractionName) {
// Start concondancer corpus processing.
try {
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', '']
)
console.log('CREATING CORPUS')
@@ -361,45 +427,107 @@ Extraction.processOwn = async function (extractionId, extractionName) {
} = await axios.post('http://concordancer:5000/dashboard/corpus', {
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('SLEEP FOR 10 SECS')
await sleep(10)
const inProgressStatusList = [
'Waiting',
'Importing',
'ImportingCompleted',
'Indexing',
'IndexingCompleted'
]
for (const conlluPath of conllusPaths) {
const textPathParts = conlluPath.split('/')
textPathParts[0] = '/data'
const textPath = textPathParts.join('/')
console.log('ADDING TEXT')
await axios.post(
const {
data: {
entityInfo: { id: textId }
}
} = await axios.post(
`http://concordancer:5000/dashboard/corpus/${corpusId}/text`,
{ 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('SLEEP FOR 10 SECS')
await sleep(10)
}
const termListPathParts = termCandidatesPath.split('/')
termListPathParts[0] = '/data'
const termListPath = termListPathParts.join('/')
console.log('ADDING TERMS')
await axios.post(
const {
data: {
entityInfo: { id: termListId }
}
} = await axios.post(
`http://concordancer:5000/dashboard/corpus/${corpusId}/termList`,
{ 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')
await db.query(
"UPDATE extraction_job SET status = 'finished', time_finished = NOW() WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
[extractionId, 'concordancer', '']
)
await db.query(
"UPDATE extraction SET status = 'finished', time_finished = NOW(), corpus_id = $1 WHERE id = $2",
[corpusId, extractionId]
"UPDATE extraction SET status = 'finished', time_finished = NOW() WHERE id = $1",
[extractionId]
)
console.log('EXTRACTION SUCCESSFUL')
} catch (e) {
console.log('EXTRACTION ERROR')
console.log(e)
} catch (error) {
logExtractionError(error, extractionId, 'concordancer')
await failTheJob(extractionId, 'concordancer', '')
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 Also consider refactoring certain parts,
// 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 stopTermsFilesNames = await this.fetchAllStopTermsFilesNames(
extractionId
@@ -435,31 +565,49 @@ Extraction.processOss = async function (extractionId, ossParams) {
...(ossParams.documentType && { vrste: ossParams.documentType }),
...(ossParams.keywords && { kljucneBesede: ossParams.keywords }),
...(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 {
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)
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', '']
)
// Poll job until finished.
while (true) {
await sleep(5)
const { data: data2 } = await axios.get(
`http://rsdo.lhrs.feri.um.si:8080/job/${remotejobId}`
const { data: data2 } = await retry(
async () => {
return await axios.get(`${extractionApiOrigin}/job/${remotejobId}`)
},
RETRY_SECONDS_INTERVAL,
RETRY_SECONDS_MAX
)
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?).
const termCandidatesPath = getTermCandidatesPath(extractionId)
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(
"UPDATE extraction_job SET status = 'finished', time_finished = NOW() WHERE extraction_id = $1 AND job_type = $2 AND filename = $3",
[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",
[extractionId]
)
} catch {
} catch (error) {
logExtractionError(error, extractionId, 'oss term candidates')
await failTheJob(extractionId, 'oss term candidates', '')
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) {
await db.query(
"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))
}
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
@@ -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 xmlFlow = require('xml-flow')
const xss = require('xss')
const debug = require('debug')('termPortal:models/helpers/dictionary')
const db = require('../../db')
const { intoDbArray } = require('..')
const { sanitizeField } = require('./index')
const JOBS_MAX = 50
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) {
return markupFilter.noMixed
.process(xmlFlow.toXml(markupObj))
.replace(/\s+/g, ' ')
.trim()
return sanitizeField.toText(xmlFlow.toXml(markupObj))
}
function toMixedBasic(markupObj) {
return markupFilter.mixedBasic
.process(xmlFlow.toXml(markupObj))
.replace(/\s+/g, ' ')
.trim()
return sanitizeField.toMixedBasic(xmlFlow.toXml(markupObj))
}
function toMixedExtended(markupObj) {
return markupFilter.mixedExtended
.process(xmlFlow.toXml(markupObj))
.replace(/\s+/g, ' ')
.trim()
return sanitizeField.toMixedExtended(xmlFlow.toXml(markupObj))
}
function toMixedOther(markupObj) {
return markupFilter.mixedOther
.process(xmlFlow.toXml(markupObj))
.replace(/\s+/g, ' ')
.trim()
return sanitizeField.toMixedOther(xmlFlow.toXml(markupObj))
}
+177 -18
View File
@@ -1,7 +1,23 @@
const xss = require('xss')
const { removeHtmlTags } = require('../../helpers')
const { searchEngineClient, ENTRY_INDEX } = require('../../search-engine')
const { DATA_FILES_PATH } = require('../../../config/settings')
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) {
const deserializedDomain = {
id: domain.id,
@@ -33,16 +49,16 @@ exports.deserialize = {
return deserializedDomain
},
language(language) {
const deserializedLanguage = {
id: language.id,
code: language.code,
nameSl: language.name_sl,
nameEn: language.name_en
}
// language(language) {
// const deserializedLanguage = {
// id: language.id,
// code: language.code,
// nameSl: language.name_sl,
// nameEn: language.name_en
// }
return deserializedLanguage
},
// return deserializedLanguage
// },
editDescription(dictionary) {
const deserializedDictionary = {
@@ -62,7 +78,7 @@ exports.deserialize = {
editUsers(dictionary) {
const deserializedDictionary = {
id: dictionary.id,
nameSl: dictionary.name_sl,
name: dictionary.name,
terminologyReviewFlag: dictionary.entries_have_terminology_review_flag,
languageReviewFlag: dictionary.entries_have_language_review_flag,
status: dictionary.status
@@ -75,6 +91,7 @@ exports.deserialize = {
const deserializedDictionary = {
id: dictionary.id,
nameSl: dictionary.name_sl,
nameEn: dictionary.name_en,
hasDomainLabels: dictionary.entries_have_domain_labels,
hasLabel: dictionary.entries_have_label,
hasDefinition: dictionary.entries_have_definition,
@@ -102,16 +119,78 @@ exports.deserialize = {
return deserializedDomainLabel
},
imports(oneImport) {
const deserializedImports = {
timeStarted: oneImport.time_started,
status: oneImport.status,
deleteExisting: oneImport.delete_existing_entries,
fileFormat: oneImport.file_format,
countValidEntries: oneImport.count_valid_entries
// imports(oneImport) {
// const deserializedImports = {
// timeStarted: oneImport.time_started,
// status: oneImport.status,
// deleteExisting: oneImport.delete_existing_entries,
// fileFormat: oneImport.file_format,
// 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.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 path = require('path')
const { partial } = require('filesize')
const { DATA_FILES_PATH } = require('../../config/settings')
@@ -8,6 +9,7 @@ exports.deserialize = {
extraction(extraction) {
const deserializedExtraction = {
id: extraction.id,
userId: extraction.user_id,
name: extraction.name,
status: extraction.status,
corpusId: extraction.corpus_id,
@@ -43,18 +45,22 @@ exports.getFileNamesInFolder = async folderPath => {
return filenames
}
exports.getFileStats = getFileStats
exports.getFileStatsInFolder = async folderPath => {
const filenames = await readdir(folderPath)
const fileStats = await Promise.all(
filenames.map(async filename => {
const filePath = `${folderPath}/${filename}`
const filePaths = filenames.map(filename => `${folderPath}/${filename}`)
const fileStats = await Promise.all(filePaths.map(getFileStats))
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 sizeHumanReadable = formatFileSize(size)
const fileStats = { filename, size: sizeHumanReadable, timeModified }
return fileStats
})
)
return fileStats
}
function getExtractionFilesPath(extractionId) {
+4 -2
View File
@@ -9,8 +9,10 @@ exports.aggregateSettings = settings => {
exports.deserialize = {
settings(settings) {
const deserializedSettings = {
name: settings.portal_name,
description: settings.portal_description,
nameSl: settings.portal_name_sl,
nameEn: settings.portal_name_en,
descriptionSl: settings.portal_description_sl,
descriptionEn: settings.portal_description_en,
code: settings.portal_code,
isExtractionEnabled: settings.is_extraction_enabled,
isDictionariesEnabled: settings.is_dictionaries_enabled,
@@ -9,7 +9,7 @@ module.exports = function (filters, hitsPerPage, page) {
filter: []
}
},
sort: ['_score', 'timeCreated']
sort: ['_score', { timeCreated: 'desc' }]
}
if (filters.status) {
@@ -45,7 +45,7 @@ module.exports = function (searchString, filters, hitsPerPage, page) {
filter: []
}
},
sort: ['_score', 'timeCreated']
sort: ['_score', { timeCreated: 'desc' }]
}
if (filters.status) {
@@ -39,7 +39,7 @@ module.exports = function (searchString, filters, hitsPerPage, page) {
filter: []
}
},
sort: ['_score', 'timeCreated']
sort: ['_score', { timeCreated: 'desc' }]
}
if (filters.status) {
@@ -54,7 +54,7 @@ module.exports = function (searchString, filters, hitsPerPage, page) {
filter: []
}
},
sort: ['_score', 'timeCreated']
sort: ['_score', { timeCreated: 'desc' }]
}
if (filters.status) {
@@ -2,7 +2,7 @@ const { EDITOR_MAX_HITS } = require('../../../../../config/settings')
module.exports = function (dictionaryId, filters, searchFieldFilters) {
const queryDsl = {
_source: ['id', 'isValid', 'isPublished', 'term'],
_source: ['id', 'isValid', 'isPublished', 'term', 'homonymSort'],
fields: ['foreignEntries.terms'],
script_fields: {
commentActivityIndicator: {
+3 -2
View File
@@ -50,7 +50,7 @@ exports.prepareEntries = hits => {
}
// 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 hitsCount = hits.total.value
@@ -107,7 +107,8 @@ exports.prepareAggregation = async aggregationRaw => {
const names = await Portal.getSearchAggregateNames(
primaryDomainIds,
dictionaryIds,
languageIds
languageIds,
determinedLanguage
)
const aggregation = {
+4 -1
View File
@@ -6,7 +6,9 @@ exports.deserialize = {
firstName: user.first_name,
lastName: user.last_name,
email: user.email,
status: user.status,
hitsPerPage: user.hits_per_page,
language: user.language,
userRoles: user.user_roles,
assignedConsultancyEntries: user.assigned_consultancy_entries
}
@@ -21,7 +23,8 @@ exports.deserialize = {
firstName: userData.first_name,
lastName: userData.last_name,
email: userData.email,
password: userData.password
status: userData.status,
language: userData.language
}
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' +
' INNER JOIN language AS l ON l.id = ef.language_id' +
' 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, [
dictionaryId,
since
@@ -83,6 +86,7 @@ class InterInstanceSync {
const { rows: translationList } = await db.query(sqlTranslations, [
entryIds
])
const { rows: linkList } = await db.query(sqlLinks, [entryIds])
let currentEntryId = 0
let lastEntryId = 0
translationList.forEach(t => {
@@ -102,6 +106,23 @@ class InterInstanceSync {
}
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
}
}
+23 -14
View File
@@ -12,9 +12,11 @@ Portal.fetchInstanceSettings = async () => {
WHERE
name
IN (
'portal_name',
'portal_name_sl',
'portal_name_en',
'portal_code',
'portal_description',
'portal_description_sl',
'portal_description_en',
'is_consultancy_enabled',
'is_dictionaries_enabled',
'is_extraction_enabled')`
@@ -32,9 +34,11 @@ Portal.updateInstaceSettings = async payload => {
const isConsultancyEnabled = payload.isConsultancyEnabled ? 'T' : 'F'
const values = [
payload.portalName,
payload.portalNameSl,
payload.portalNameEn,
payload.portalCode,
payload.portalDescription,
payload.portalDescriptionSl,
payload.portalDescriptionEn,
isExtractionEnabled,
isDictionariesEnabled,
isConsultancyEnabled
@@ -47,17 +51,21 @@ Portal.updateInstaceSettings = async payload => {
value
= CASE name
WHEN
'portal_name' THEN $1
'portal_name_sl' THEN $1
WHEN
'portal_code' THEN $2
'portal_name_en' THEN $2
WHEN
'portal_description' THEN $3
'portal_code' THEN $3
WHEN
'is_extraction_enabled' THEN $4
'portal_description_sl' THEN $4
WHEN
'is_dictionaries_enabled' THEN $5
'portal_description_en' THEN $5
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
END`
@@ -447,27 +455,28 @@ Portal.getSlovenianLanguageId = async () => {
Portal.getSearchAggregateNames = async (
primaryDomainIds,
dictionaryIds,
languageIds
languageIds,
determinedLanguage
) => {
const text = `
SELECT jsonb_build_object(
'primaryDomains', jsonb_object(
ARRAY(
SELECT ARRAY [id, name_sl]::TEXT[]
SELECT ARRAY [id, name_${determinedLanguage}]::TEXT[]
FROM domain_primary
WHERE id = ANY ($1)
)
),
'dictionaries', jsonb_object(
ARRAY(
SELECT ARRAY [id, name_sl]::TEXT[]
SELECT ARRAY [id, name_${determinedLanguage}]::TEXT[]
FROM dictionary
WHERE id = ANY ($2)
)
),
'languages', jsonb_object(
ARRAY(
SELECT ARRAY [id, name_sl]::TEXT[]
SELECT ARRAY [id, name_${determinedLanguage}]::TEXT[]
FROM language
WHERE id = ANY ($3)
)
+1 -1
View File
@@ -47,7 +47,7 @@ Eurotermbank.push = async () => {
'definition', ef.definition,
'synonyms', ef.synonym)
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
) foreign_entries
FROM entry e
+347 -41
View File
@@ -1,13 +1,60 @@
const { randomBytes } = require('crypto')
const { promisify } = require('util')
const RandomBytesAsync = promisify(randomBytes)
const db = require('./db')
const bcrypt = require('bcrypt')
const uid = require('uid-safe')
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 = {}
// 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.
User.create = async user => {
const SALT_ROUNDS = 12
User.create = async (user, t) => {
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 values = [
@@ -17,16 +64,31 @@ User.create = async user => {
user.email || 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,
first_name,
last_name,
email,
bcrypt_hash
${user.language ? ', language' : ''}
)
VALUES (${db.genParamStr(values)})
RETURNING id`
}
const { rows } = await db.query(text, values)
@@ -43,33 +105,42 @@ User.saveActivationToken = async (userId, activationToken) => {
)
}
// Fetch user from DB by (valid) activation token.
User.fetchByActivationToken = async activationToken => {
const TOKEN_VALID_PERIOD = '1 week'
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]
// Activate user account using the provided activation token.
User.activateAccountWithToken = async (token, t) => {
let user
const { rows } = await db.query(text, values)
const user = rows[0]
await db.transaction(async dbClient => {
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 (!user) throw Error('Povezava je neveljavna ali pa je že potekla')
if (rows.length === 0) {
const err = Error(
t('Povezava ni (več) veljavna. Prosimo, da se ponovno registrirate.')
)
err.status = 403
err.displayInProd = true
throw err
}
const userId = rows[0].user_id
;({
rows: [user]
} = await dbClient.query(
`UPDATE "user" SET status = 'active', time_activated = NOW() WHERE id = $1 RETURNING id`,
[userId]
))
await dbClient.query('DELETE FROM user_token_activation WHERE token = $1', [
token
])
})
return user
}
// Activate user account.
User.activateAccount = async user => {
await db.query(`UPDATE "user" SET status = 'active' WHERE id = $1`, [user.id])
}
// Generate a user remember me token.
User.generateRememberMeToken = async () => {
const token = await uid(32)
@@ -91,6 +162,130 @@ User.clearRememberMeToken = async rememberMeToken => {
])
}
// Save a password reset token for a single user in DB.
User.saveResetPasswordToken = async (userId, resetPasswordToken) => {
await db.query(
'INSERT INTO user_token_reset_password (token, user_id) VALUES ($1, $2)',
[resetPasswordToken, userId]
)
}
// Check existance and validity of password reset token in DB.
User.isResetPasswordTokenValid = async token => {
const { rows } = await db.query(
`SELECT 1 exists FROM user_token_reset_password WHERE token = $1 AND NOW() - time_created < '${PASSWORD_RESET_VALID_INTERVAL}'`,
[token]
)
const isValid = rows.length > 0
return isValid
}
// Set new password for user using the provided reset password token.
User.resetPasswordWithToken = async (token, password, t) => {
let user
await db.transaction(async dbClient => {
const { rows } = await dbClient.query(
`SELECT user_id FROM user_token_reset_password WHERE token = $1 AND NOW() - time_created < '${PASSWORD_RESET_VALID_INTERVAL}'`,
[token]
)
if (rows.length === 0) {
const err = Error(
t(
'Povezava ni (več) veljavna. Prosimo, da ponovno zahtevate ponastavitev gesla.'
)
)
err.status = 403
err.displayInProd = true
throw err
}
const bcryptHash = await bcrypt.hash(password, SALT_ROUNDS)
const userId = rows[0].user_id
;({
rows: [user]
} = await dbClient.query(
'UPDATE "user" SET bcrypt_hash = $1 WHERE id = $2 RETURNING id, username, email',
[bcryptHash, userId]
))
await dbClient.query(
'DELETE FROM user_token_reset_password WHERE token = $1',
[token]
)
})
return user
}
// Save change email token for a single user in DB.
User.saveChangeEmailToken = async (userId, changeEmailToken, newEmail) => {
await db.query(
'INSERT INTO user_token_change_email (token, user_id, new_email) VALUES ($1, $2, $3)',
[changeEmailToken, userId, newEmail]
)
}
// Set new email for user using the provided change email token.
User.changeEmailWithToken = async function (token, t) {
let user
await db.transaction(async dbClient => {
const { rows } = await dbClient.query(
`SELECT user_id, new_email FROM user_token_change_email WHERE token = $1 AND NOW() - time_created < '${CHANGE_EMAIL_TOKEN_VALID_DAYS} days'`,
[token]
)
if (rows.length === 0) {
const err = Error(
t('Povezava ni (več) veljavna. Elektronski naslov ni bil spremenjen.')
)
err.status = 403
err.displayInProd = true
throw err
}
const { user_id: userId, new_email: newEmail } = rows[0]
if (await this.isEmailAlreadyTaken(newEmail)) {
const err = Error(t('Elektronski naslov uporablja že drug uporabnik.'))
err.status = 403
err.displayInProd = true
throw err
}
;({
rows: [user]
} = await dbClient.query(
'UPDATE "user" SET email = $1 WHERE id = $2 RETURNING id, username, email',
[newEmail, userId]
))
await dbClient.query(
'DELETE FROM user_token_change_email WHERE token = $1',
[token]
)
})
return user
}
// Fetch user from DB by username or email.
User.fetchByUsernameOrEmail = async usernameOrEmail => {
const {
rows: [user]
} = await db.query(
'SELECT id, username, email, status, bcrypt_hash FROM "user" WHERE username = $1 OR email = $1',
[usernameOrEmail]
)
return user
}
// Fetch user data that should be available on every request from DB by id.
User.fetchDeserializedDataById = async userId => {
const text = `
@@ -100,7 +295,9 @@ User.fetchDeserializedDataById = async userId => {
u.first_name,
u.last_name,
u.email,
u.status,
u.hits_per_page,
u.language,
ARRAY(
SELECT jsonb_build_object(
'roleName', r.role_name,
@@ -142,6 +339,7 @@ User.fetchAll = async (resultsPerPage, page) => {
'pages_total', (
SELECT CEIL(COUNT(*) / $1::float)
FROM "user"
WHERE status <> 'closed'
),
'results', ARRAY(
SELECT jsonb_build_object(
@@ -151,6 +349,7 @@ User.fetchAll = async (resultsPerPage, page) => {
'status', status
)
FROM "user"
WHERE status <> 'closed'
ORDER BY username
LIMIT $1
OFFSET $2
@@ -258,9 +457,9 @@ User.updatePortalRoles = async rolesPerUser => {
User.fetchUser = async userId => {
const text = `
SELECT id, username, first_name, last_name, email
SELECT id, username, first_name, last_name, email, status, language
FROM "user"
WHERE id=$1`
WHERE id = $1`
const value = [userId]
const { rows } = await db.query(text, value)
@@ -271,17 +470,38 @@ User.fetchUser = async userId => {
}
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"
SET
username = $2,
first_name = $3,
last_name = $4
last_name = $4,
status = $5
${setTimeActivated ? ', time_activated = NOW()' : ''}
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 => {
@@ -426,12 +646,9 @@ User.insertNewConsultantWithDomain = async (userId, domains) => {
// Insert new consultant role with domain of
User.insertNewConsultantWithDomainByUsername = async (username, domains) => {
const { rows } = await db.query(
'SELECT id FROM "user" WHERE username = $1 or email = $1',
[username]
)
const user = await User.fetchByUsernameOrEmail(username)
await User.insertNewConsultantWithDomain(rows[0].id, domains)
await User.insertNewConsultantWithDomain(user.id, domains)
}
// Remove consultant role
@@ -449,13 +666,15 @@ User.fetchAllowedHitsPerPage = async () => {
).rows.map(e => e.unnest)
}
User.updateFirstNameAndLastName = async (username, firstName, lastName) => {
return await db.query(
`UPDATE "user"
SET first_name=$2, last_name=$3
WHERE username=$1;`,
[username, firstName, lastName]
User.updateFirstNameAndLastName = async (userId, firstName, LastName) => {
const {
rows: [{ email }]
} = await db.query(
'UPDATE "user" SET first_name = $1, last_name = $2 WHERE id = $3 RETURNING email',
[firstName, LastName, userId]
)
return email
}
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
+96
View File
@@ -12,6 +12,7 @@
"async": "^3.2.3",
"axios": "^0.26.1",
"bcrypt": "^5.0.1",
"connect-flash-plus": "^0.2.1",
"connect-redis": "^6.0.0",
"cookie-parser": "^1.4.5",
"debug": "^4.3.2",
@@ -22,6 +23,9 @@
"form-data": "^4.0.0",
"helmet": "^5.0.2",
"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",
"morgan": "^1.10.0",
"multer": "^1.4.3",
@@ -64,6 +68,17 @@
"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": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
@@ -780,6 +795,14 @@
"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": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-6.1.3.tgz",
@@ -1678,6 +1701,38 @@
"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": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -3039,6 +3094,11 @@
"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": {
"version": "4.2.1",
"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",
"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": {
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.17.0.tgz",
@@ -4359,6 +4427,11 @@
"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": {
"version": "6.1.3",
"resolved": "https://registry.npmjs.org/connect-redis/-/connect-redis-6.1.3.tgz",
@@ -5032,6 +5105,24 @@
"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": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -6100,6 +6191,11 @@
"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": {
"version": "4.2.1",
"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,
"scripts": {
"start": "node ./bin/www",
"devstart": "nodemon --ignore data_files/ --inspect=0.0.0.0:9229 ./bin/www",
"devstart-wait": "nodemon --ignore data_files/ --inspect-brk=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/ --ignore public/locales/dev --inspect-brk=0.0.0.0:9229 ./bin/www"
},
"dependencies": {
"@opensearch-project/opensearch": "^2.1.0",
"async": "^3.2.3",
"axios": "^0.26.1",
"bcrypt": "^5.0.1",
"connect-flash-plus": "^0.2.1",
"connect-redis": "^6.0.0",
"cookie-parser": "^1.4.5",
"debug": "^4.3.2",
@@ -22,6 +23,9 @@
"form-data": "^4.0.0",
"helmet": "^5.0.2",
"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",
"morgan": "^1.10.0",
"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

+168 -179
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
let queryBattery = ''
window.addEventListener('load', () => {
initAdmin()
})
@@ -62,11 +64,11 @@ function initAdmin() {
renderResults(results)
updatePager(page, numberOfAllPages)
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response?.data) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
updatePager()
@@ -130,11 +132,11 @@ function initAdmin() {
ifUserExists(data, type)
event.target.reset()
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
console.log(message)
}
@@ -142,6 +144,7 @@ function initAdmin() {
}
if (currentPagePath === '/admin/uporabniki/seznam') {
isI18nReady.then(t => {
const resultsListEl = document.getElementById('page-results')
const updatePager = initPagination('pagination', onPageChange)
@@ -155,11 +158,11 @@ function initAdmin() {
renderResults(results)
updatePager(page, numberOfAllPages)
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response?.data) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
updatePager()
@@ -184,20 +187,21 @@ function initAdmin() {
const spanEl = document.createElement('span')
td1.textContent = result.userName
td2.textContent = result.email
td3.textContent = result.status
td3.textContent = t(`userStatus${result.status}`)
aEl.classList.add('image-link')
aEl.type = 'link'
aEl.href = `/admin/uporabniki/${result.id}/urejanje`
imgEl.src = '/images/u_edit-alt.svg'
imgEl.alt = 'Uredi'
imgEl.alt = i18next.t('Uredi')
spanEl.className = 'normal-gray ms-1'
spanEl.textContent = 'Uredi'
spanEl.textContent = i18next.t('Uredi')
td4.append(aEl)
aEl.append(imgEl, spanEl)
rowEl.append(td1, td2, td3, td4)
resultsListEl.appendChild(rowEl)
})
}
})
}
if (currentPagePath === '/admin/slovarji') {
@@ -214,11 +218,11 @@ function initAdmin() {
renderResults(results)
updatePager(page, numberOfAllPages)
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response?.data) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
updatePager()
@@ -267,9 +271,9 @@ function initAdmin() {
aEl.type = 'link'
aEl.href = `/admin/slovarji/${result.id}/podatki`
imgEl.src = '/images/u_edit-alt.svg'
imgEl.alt = 'Uredi'
imgEl.alt = i18next.t('Uredi')
spanEl.className = 'normal-gray ms-1'
spanEl.textContent = 'Uredi'
spanEl.textContent = i18next.t('Uredi')
td6.append(aEl)
aEl.append(imgEl, spanEl)
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 imgTrashIcon = document.querySelectorAll('.delete-author-btn')
const inputNewAuthorEl = document.getElementById('input-new-author')
const inputNewAreaEl = document.getElementById('input-new-area')
const dictSideMenu = document.querySelector('.admin-nav-content')
$('.without-addition').on('change', enableButton)
unsavedData(formEl, dictSideMenu)
formEl.addEventListener('input', enableButton)
if (imgTrashIcon !== null) {
@@ -343,18 +351,20 @@ function initAdmin() {
async function onPageChange(newPage) {
try {
const { page, numberOfAllPages, results } = await getDataForPage(
newPage
)
const results = await getDataForPage(newPage)
const numberOfAllPages = +results.headers['number-of-all-pages']
const page = +results.headers.page
removeAllChildNodes(resultsListEl)
renderResults(results)
renderResults(results.data)
updatePager(page, numberOfAllPages)
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response?.data) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
updatePager()
@@ -362,56 +372,46 @@ function initAdmin() {
}
async function getDataForPage(page) {
const url = `/api/v1/dictionaries/listSecondaryDomains?p=${page}`
const { data } = await axios.get(url)
return data
const url = `/api/v1/dictionaries/secondaryDomains?q=${queryBattery}&p=${page}`
return await axios.get(url)
}
function renderResults(results) {
results.forEach(result => {
const rowEl = document.createElement('tr')
const input = document.createElement('input')
const th = document.createElement('th')
const input2 = document.createElement('input')
const td1 = document.createElement('td')
const 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)
replaceContainer('page-results', results)
}
rowEl.append(input, th, td1, tdTrans, td2)
resultsListEl.appendChild(rowEl)
const updatePaginationOnFilter = axiosResult => {
const numberOfAllPages = +axiosResult.headers['number-of-all-pages']
const page = +axiosResult.headers.page
// removeAllChildNodes(resultsListEl)
// renderResults(results.data)
// console.log(numberOfAllPages)
// console.log(page)
// console.log(updatePager)
updatePager(page, numberOfAllPages)
}
/// / Due to unsual design, the function was moved inside
function searchController() {
// if (window.location.pathname.includes('podrocne-oznake')) { // 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)
event.target.reset()
} 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)
}
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') {
const formAdminPortal = document.getElementById('admin-portal-settings')
formAdminPortal.addEventListener('input', enableButton)
@@ -537,7 +526,7 @@ function createNewAuthorInput(pageForm) {
divAuthor.className = 'author mt-sm-4 added-field'
divSubjectName.className = 'subject-name'
spanName.className = 'smaller-gray-uppercase'
spanName.textContent = 'AVTOR'
spanName.textContent = i18next.t('AVTOR')
divRow.className = 'row align-items-center'
divColSm5.className = 'col-sm-6'
inputGroup.className = 'input-group'
@@ -553,7 +542,7 @@ function createNewAuthorInput(pageForm) {
divColSm.className = 'col-sm'
spanNameInfoTxt.className =
'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)
divSubjectName.appendChild(spanName)
@@ -594,9 +583,9 @@ function createNewAreaInput(pageForm) {
divSmallNameArea.className = 'author mt-4 added-field'
divSubjectName.className = 'subject-name'
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.textContent = 'NOVO PODPODROČJE (angleško)'
spanInputNameTxtEng.textContent = i18next.t('NOVO PODPODROČJE (angleško)')
divRow.className = 'row align-items-center'
divRow2.className = 'row align-items-center'
divEnglishInput.className = 'mt-4'
@@ -617,11 +606,12 @@ function createNewAreaInput(pageForm) {
divColSm.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.textContent =
spanNameInfoTxt.textContent = i18next.t(
'Vpišite novo podpodročje. Na seznamu podpodročij bo vidno takoj po potrditvi administratorja portala.'
)
spanNameInfoTxtEng.className =
'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)
divSubjectName.appendChild(spanInputNameTxtSlo)
@@ -782,7 +772,7 @@ function handleAreasClick({ target }) {
const newButtonGroup = document.createElement('div')
const cancelButton = document.createElement('button')
const saveButton = document.createElement('button')
cancelButton.textContent = 'Prekliči'
cancelButton.textContent = i18next.t('Prekliči')
cancelButton.type = 'button'
cancelButton.className = 'btn btn-secondary me-2'
cancelButton.style.height = '33px'
@@ -790,7 +780,7 @@ function handleAreasClick({ target }) {
cancelButton.addEventListener('click', () =>
abortEditing(newButtonGroup, tableButtons, tDataArea, tDataTranslation)
)
saveButton.textContent = 'POTRDI'
saveButton.textContent = i18next.t('POTRDI')
saveButton.type = 'button'
saveButton.className = 'btn btn-primary'
saveButton.style.height = '33px'
@@ -962,7 +952,7 @@ function mobileMoveContent() {
// primaryButton.style.marginRight = '10px'
primaryButton.style.whiteSpace = 'nowrap'
}
navTitle.textContent = siteHeadingTextContent
if (navTitle) navTitle.textContent = siteHeadingTextContent
siteHeading.style.display = 'none'
}
if (document.body.clientWidth > 1200) {
@@ -982,80 +972,18 @@ function mobileMoveContent() {
primaryButton.style.whiteSpace = ''
}
if (navTitle) {
if (
currentPagePath.includes('slovarji') &&
!currentPagePath.includes('admin')
)
navTitle.textContent = 'Urejanje'
else navTitle.textContent = 'Administrator'
navTitle.textContent = i18next.t('Urejanje')
else navTitle.textContent = i18next.t('Administracija')
}
siteHeading.style.display = 'block'
}
}
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) {
const { terminologyReviewCb, languageReviewCb } = window.adminElements
const termRevCbs = document.querySelectorAll('.terminology-review-cb')
@@ -1109,12 +1037,18 @@ function ifUserExists(data, type) {
}
}
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()
} else createNewUserArea(data, type)
} else createNewUserArea(data, type)
} 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()
}
}
@@ -1172,24 +1106,24 @@ function createNewUserArea(data, type) {
inputConsultAdmin.type = 'checkbox'
if (type === 'portal') {
tdPortAdminOrAdmin.dataset.label = 'Skrbnik portala'
tdPortAdminOrAdmin.dataset.label = i18next.t('Skrbnik portala')
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]`
tdConsultAdmin.dataset.label = 'Skrbnik svetovalnice'
tdConsultAdmin.dataset.label = i18next.t('Skrbnik svetovalnice')
inputConsultAdmin.name = `rolesPerUser['${data[0].id}'][isConsultancyAdmin]`
} else {
tdPortAdminOrAdmin.dataset.label = 'Administrator'
tdPortAdminOrAdmin.dataset.label = i18next.t('Administrator')
inputPortAdminOrAdmin.classList.add('administration-cb')
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.classList.add('edit-cb')
tdConsultAdmin.dataset.label = 'Strokovni pregled'
tdConsultAdmin.dataset.label = i18next.t('Strokovni pregled')
inputConsultAdmin.classList.add('terminology-review-cb')
inputConsultAdmin.name = `rightsPerUser['${data[0].id}'][isTerminologyReview]`
tdLanguageRev.className = 'pt-1 pb-1'
tdLanguageRev.dataset.label = 'Jezikovni pregled'
tdLanguageRev.dataset.label = i18next.t('Jezikovni pregled')
divLanguageRev.className =
'form-check d-flex justify-content-left justify-content-xl-center'
inputLanguageRev.className = 'language-review-cb form-check-input'
@@ -1235,7 +1169,7 @@ function createNewUserArea(data, type) {
// 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,
minheight: 150,
toolbar: [
@@ -1254,7 +1188,7 @@ $('.summernote').summernote({
const profileForm = document.getElementById('profileForm')
if (profileForm) {
profileForm.addEventListener('change', e => {
profileForm.addEventListener('input', e => {
enableButton()
})
@@ -1263,12 +1197,30 @@ $('.summernote').summernote({
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) {
await handleUpdateHitsPerPage(data.numberOfHits)
}
if (data.name && data.surname) {
await handleUpdateUsersName(data.name, data.surname)
if (data.firstName && data.lastName && data.email) {
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)
@@ -1278,23 +1230,60 @@ $('.summernote').summernote({
async function handleUpdateHitsPerPage(hitAmount) {
try {
await axios.post('/api/v1/users/hitsPerPage', { hitAmount })
} catch (error) {
// console.log(error)
} finally {
location.reload(true)
} catch (error) {
displayError(error)
}
}
async function handleUpdateUsersName(name, surname) {
async function handleUpdateBasicData(payload) {
try {
await axios.post('/api/v1/users/nameAndSurname', {
name,
surname
})
} catch (error) {
// console.log(error)
} finally {
location.reload(true)
if (!validator.isEmail(payload.email)) {
document.getElementById('fpi-text').innerHTML =
i18next.t('Neveljavna e-pošta')
$('#reset-pass-info').modal('show')
return
}
await axios.post('/api/v1/users/basic-data', payload)
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 dropImage = document.querySelector('#dropImage')
const commentCount = document.querySelector('#comments-count')
const collapsableHR = document.querySelector('.collapable-hr')
const hide = () => {
pager.className += ' d-none'
pager.className += ' invisible'
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 => {
isVisible = !isVisible
if (isVisible) {
pager.className = 'pager'
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'
show()
} else {
dropImage.src = '/images/arrow_drop_down.svg'
hide()
+10 -9
View File
@@ -1,6 +1,6 @@
// TODO Remove no-console ignore rule once things are out of rapid dev phase.
/* eslint no-console: 0 */
/* global axios, initPagination */
/* global axios, initPagination, i18next */
const pageURL = location.pathname
window.addEventListener('load', () => {
@@ -127,20 +127,21 @@ function renderCommentCount(commentCount) {
let displayText = `${commentCount} `
// TODO Let a i18n library handle the following logic.
// TODO I18n
switch (commentCount % 100) {
case 1:
displayText += 'komentar'
displayText += i18next.t('komentar')
break
case 2:
displayText += 'komentarja'
displayText += i18next.t('komentarja')
break
case 3:
case 4:
displayText += 'komentarji'
displayText += i18next.t('komentarji')
break
default:
displayText += 'komentarjev'
displayText += i18next.t('komentarjev')
break
}
@@ -309,7 +310,7 @@ function submitComment() {
let ctxId = null
if (ctxData.ctxId !== undefined) ctxId = ctxData.ctxId
if (!message) {
alert('Vaš komentar je brez vsebine.')
alert(i18next.t('Vaš komentar je brez vsebine.'))
} else {
const payload = { message, ctxType, ctxId, quoteId: null }
createComment(payload)
@@ -326,7 +327,7 @@ function submitCommentReply() {
let ctxId = null
if (ctxData.ctxId !== undefined) ctxId = ctxData.ctxId
if (!message) {
alert('Vaš komentar je brez vsebine.')
alert(i18next.t('Vaš komentar je brez vsebine.'))
} else {
const payload = { message, ctxType, ctxId, quoteId }
createComment(payload)
@@ -577,11 +578,11 @@ async function onPageChange(newPage) {
const { page, numberOfAllPages } = await displayComments(newPage)
updatePager(page, numberOfAllPages)
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response?.data) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
updatePager()
@@ -11,6 +11,7 @@ function routeConsultancy(searchString) {
} else {
url = new URL(location)
url.searchParams.delete('p')
const sq = document.getElementById('search-query')
if (sq) {
url.searchParams.set('q', sq.value)
@@ -65,3 +66,14 @@ if (inputMainSearch) {
propagateFunctionalityToASearchButton(searchButton)
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
@@ -9,6 +10,10 @@ let selectedID = -1
// consultancy FORM
let offsetMain
let OFFSET_PADDING_MASK = 16
if (window.location.pathname === '/svetovanje/vprasanje/admin/svetovalci') {
OFFSET_PADDING_MASK = 0
}
function adjustOffsetBy() {
offsetMain = document.querySelector('#offset-main')
const fixedTopSection = document.querySelector('#fixed-top-section')
@@ -22,15 +27,21 @@ function adjustOffsetBy() {
if (offsetHeaderPadding !== null) offsetMain.style.paddingTop = `0px`
} else {
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) {
const headerPaddingHeight = headerPadding.offsetHeight
offsetMain.style.paddingTop = `${headerPaddingHeight}px`
offsetMain.style.paddingTop = `${
headerPaddingHeight - OFFSET_PADDING_MASK
}px`
}
if (offsetHeaderPadding !== null) {
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
/* copy source admin.js in case of refactoring */
if (currentPagePath === '/svetovanje/vprasanje/admin/uporabniki') {
if (currentPagePath === '/svetovanje/vprasanje/admin/svetovalci') {
offsetMain.addEventListener('click', handleDomainsClickForConsultancy)
ce.name = document.getElementById('name-input')
if (ce.name) {
@@ -108,7 +119,7 @@ try {
const summernote = $('.summernote')
if (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,
minheight: 150,
toolbar: [
@@ -383,7 +394,17 @@ if (createAnswerForm) {
// console.log(data)
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'
})
}
}
@@ -407,7 +428,7 @@ if (insertConsultantForm) {
})
.then(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)
}
/// 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) {
console.log(tooltipList)
@@ -438,3 +696,10 @@ $(document).ready(() => {
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).
@@ -6,6 +6,8 @@ window.addEventListener('load', () => {
initDictionaries()
})
let queryBattery = ''
function initDictionaries() {
const ce = {}
window.dictionaryElements = ce
@@ -166,7 +168,7 @@ function initDictionaries() {
if (ce.fileUploadInput.files.item(0) !== null)
chosenFile.textContent = ce.fileUploadInput.files.item(0).name
else {
chosenFile.textContent = 'Izberi datoteko'
chosenFile.textContent = i18next.t('Izberi datoteko')
}
}
})
@@ -185,12 +187,140 @@ function initDictionaries() {
await axios.post(event.target.action, payload)
alert('SLOVAR UVOŽEN')
alert(i18next.t('SLOVAR UVOŽEN'))
} 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)) {
ce.formEditContent = document.getElementById('form-edit-content')
ce.formEditContent.addEventListener('submit', updateEntry)
@@ -233,6 +363,7 @@ function initDictionaries() {
ce.clearSearchInput = document.querySelector('.clear-search-input')
ce.collapsablePart = document.querySelectorAll('.collapsable-entry-part')
ce.scrollEl = document.querySelector('.content-nav-content')
$('.multiple').on('select2:select', enableButton)
ce.collapsablePart.forEach(el => {
el.addEventListener('hide.bs.collapse', () =>
changePageContent('collapse-hidden')
@@ -257,7 +388,7 @@ function initDictionaries() {
})
ce.statusEditEl = document.getElementById('status-edit')
// ce.deleteEntryBtn.addEventListener('click', () => ajaxDeleteEntry())
ce.newEntryBtn.addEventListener('click', () => {
ce.newEntryBtn.addEventListener('click', e => {
ce.formEditContent.reset()
changePageContent('new-entry-btn')
})
@@ -307,6 +438,7 @@ function initDictionaries() {
if (mcBtnsGrp.length) {
mcBtnsGrp.forEach(e => e.addEventListener('click', enableButton))
}
if (ce.newHeadwordGroupBtn)
ce.newHeadwordGroupBtn.addEventListener('click', createHeadword)
if (ce.newConnectionBtn)
ce.newConnectionBtn.addEventListener('click', () =>
@@ -408,7 +540,7 @@ function initDictionaries() {
if (termListMenu.childElementCount > 1) {
const termListLabels = termListMenu.querySelectorAll('label')
termListLabels[0].click()
if (termListLabels.length) termListLabels[0].click()
}
function handleClick({ target }) {
@@ -423,10 +555,11 @@ function initDictionaries() {
const okBtnText = modalUseBtn.textContent
const cnclBtnTxt = modalCnclBtn.textContent
const modalMainTxt = modalMain.textContent
modalUseBtn.textContent = 'Shrani'
modalCnclBtn.textContent = 'Ne shrani'
modalMain.textContent =
modalUseBtn.textContent = i18next.t('Shrani')
modalCnclBtn.textContent = i18next.t('Ne shrani')
modalMain.textContent = i18next.t(
'Imate neshranjene spremembe. Ali jih želite shraniti?'
)
const alertModal = new bootstrap.Modal(
document.getElementById('alert-modal')
)
@@ -478,8 +611,8 @@ function initDictionaries() {
if (deleteEntryEl) {
const modalUseBtn = document.getElementById('modal-del-btn')
const modalCnclBtn = document.getElementById('cancel-btn')
modalUseBtn.textContent = 'Izbriši'
modalCnclBtn.textContent = 'Ne izbriši'
modalUseBtn.textContent = i18next.t('Izbriši')
modalCnclBtn.textContent = i18next.t('Ne izbriši')
const alertModal = new bootstrap.Modal(
document.getElementById('delete-modal')
)
@@ -497,8 +630,8 @@ function initDictionaries() {
const { data } = await axios.get(
`/api/v1/entries/${entryId}/version-snapshots/${versionId}`
)
const payload = { entry: data }
insertTermData(entryId, payload)
const payload = { entry: data.data }
insertTermData(entryId, payload, data.author)
const isPublishedEl = payload.entry.is_published
const info = new FormData(formEditContent)
info.append('isPublishedEl', isPublishedEl)
@@ -531,6 +664,7 @@ function initDictionaries() {
info.append('isPublishedEl', isPublishedEl)
loadPreview(info)
changeVersionList(data.entry.versions)
setLatestVersion(data.entry)
ce.formEditContent.addEventListener('input', () => (unsaved = true))
formEditContent.dataset.entryId = entryId
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 localeOptions = {
day: '2-digit',
@@ -609,12 +743,20 @@ function initDictionaries() {
}
else
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.versionEl.textContent = `Verzija ${data.entry.version}`
ce.previewVersion.textContent = `Verzija ${data.entry.version}`
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
? data.entry.term.replace(/&quot;/g, '"')
: ''
@@ -703,7 +845,7 @@ function initDictionaries() {
}
}
if (listForeignSynonyms.length) {
if (el.synonym != null)
if (el.synonym != null) {
el.synonym.forEach(element => {
if (element.length) {
// eslint-disable-next-line
@@ -721,6 +863,7 @@ function initDictionaries() {
})
}
}
}
})
}
})
@@ -757,18 +900,19 @@ function initDictionaries() {
const payload = new URLSearchParams(new FormData(ce.formEditContent))
payload.set('entryId', entryId)
try {
saveBtn.textContent = i18next.t('Shranjujem ...')
await axios.post(ce.formEditContent.action, payload)
spinnerEl.classList.remove('d-none')
saveBtn.classList.add('saved-entry-btn')
saveBtn.textContent = 'Shranjeno'
saveBtn.textContent = i18next.t('Shranjeno')
if (id != null) entryId = id
ajaxSideMenu(entryId)
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
messageContainer.textContent = message
}
@@ -776,19 +920,20 @@ function initDictionaries() {
event.preventDefault()
const payload = new URLSearchParams(new FormData(ce.formEditContent))
try {
saveBtn.textContent = i18next.t('Shranjujem ...')
const res = await axios.post(ce.formEditContent.action, payload)
spinnerEl.classList.remove('d-none')
saveBtn.classList.add('saved-entry-btn')
saveBtn.textContent = 'Shranjeno'
saveBtn.textContent = i18next.t('Shranjeno')
let entryId = res.data.entryId
if (id != null) entryId = id
ajaxSideMenu(entryId)
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
messageContainer.textContent = message
} finally {
@@ -806,11 +951,11 @@ function initDictionaries() {
})
renderTerms(data, entryId)
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
messageContainer.textContent = message
}
@@ -829,7 +974,7 @@ function initDictionaries() {
selectNextEntry(selectedIndex)
} catch (error) {
console.log(error)
const message = 'Prišlo je do napake.'
const message = i18next.t('Prišlo je do napake.')
messageContainer.textContent = message
}
}
@@ -870,21 +1015,27 @@ function initDictionaries() {
/&quot;/g,
'"'
)}`
} else labelElement.textContent = '[ni termina]'
} else labelElement.textContent = '[ni termina]'
} else labelElement.textContent = i18next.t('[ni termina]')
} else labelElement.textContent = i18next.t('[ni termina]')
} else {
if (el.isValid) {
if (el.isPublished) {
labelElement.textContent = sloTerm
labelElement.textContent = `${sloTerm} ${
el.homonymSort ? '(' + el.homonymSort + ')' : ''
}`
labelElement.className =
'terms-label term-good-btn btn p-2 ms-2 me-3 text-truncate justify-content-start d-inline-block'
} else {
labelElement.className =
'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 {
labelElement.textContent = sloTerm
labelElement.textContent = `${sloTerm} ${
el.homonymSort ? '(' + el.homonymSort + ')' : ''
}`
labelElement.className =
'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 dictionaryId = document.getElementById('subareas-dict-id').value
@@ -957,18 +1107,19 @@ function initDictionaries() {
async function onPageChange(newPage) {
try {
const { page, numberOfAllPages, results } = await getDataForPage(
newPage
)
const results = await getDataForPage(newPage)
const numberOfAllPages = +results.headers['number-of-all-pages']
const page = +results.headers.page
removeAllChildNodes(resultsListEl)
renderResults(results)
renderResults(results.data)
updatePager(page, numberOfAllPages)
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response?.data) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
updatePager()
@@ -976,57 +1127,56 @@ function initDictionaries() {
}
async function getDataForPage(page) {
const url = `/api/v1/dictionaries/${dictionaryId}/listDomainLabels?p=${page}`
const { data } = await axios.get(url)
return data
// const url = `/api/v1/dictionaries/${dictionaryId}/listDomainLabels?p=${page}`
const url = `/api/v1/dictionaries/${dictionaryId}/domainLabels?q=${queryBattery}&p=${page}`
// const { data } = await axios.get(url)
return await axios.get(url)
}
function renderResults(results) {
results.forEach(result => {
const rowEl = document.createElement('tr')
const input = document.createElement('input')
const th = document.createElement('th')
const input2 = document.createElement('input')
const td1 = document.createElement('td')
const td2 = document.createElement('td')
const div = document.createElement('div')
const editBtn = document.createElement('button')
const deleteBtn = document.createElement('button')
const imgEditEl = document.createElement('img')
const imgDelEl = document.createElement('img')
input.value = result.id
input.type = 'hidden'
input.name = 'domainLabelId'
th.scope = 'row'
input2.className = 'form-check checkbox-table'
input2.type = 'checkbox'
input2.name = 'isVisible'
input2.disabled = true
input2.checked = !!result.isVisible
th.append(input2)
td1.className = 'tdata-area'
td1.textContent = result.name
replaceContainer('page-results', results)
}
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)
const updatePaginationOnFilter = axiosResult => {
const numberOfAllPages = +axiosResult.headers['number-of-all-pages']
const page = +axiosResult.headers.page
// removeAllChildNodes(resultsListEl)
// renderResults(results.data)
// console.log(numberOfAllPages)
// console.log(page)
// console.log(updatePager)
updatePager(page, numberOfAllPages)
}
rowEl.append(input, th, td1, td2)
resultsListEl.appendChild(rowEl)
/// / Due to unsual design, the function was moved inside
function searchController() {
if (window.location.pathname.includes('podrocne-oznake')) {
queryBattery = document.getElementById('input-search').value
// console.log(queryBattery)
axios
.get(
`/api/v1/dictionaries/${
window.location.pathname.split('/')[2]
}/domainLabels?q=${queryBattery}&p=${1}`
)
.then(result => {
updatePaginationOnFilter(result)
replaceContainer('page-results', result.data)
})
}
}
const inlineSearchButton = document.getElementById('inline-search-btn')
if (inlineSearchButton) {
inlineSearchButton.addEventListener('click', searchController)
$('#input-search').on('keyup', function (e) {
if (e.code === 'Enter' || e.code === 'NumpadEnter') {
searchController()
}
})
}
/// //
}
if (/\/slovarji\/\d+\/napredno/.test(currentPagePath)) {
@@ -1217,8 +1367,8 @@ function mobileMoveContent() {
currentPagePath.includes('slovarji') &&
!currentPagePath.includes('admin')
)
navTitle.textContent = 'Urejanje'
else navTitle.textContent = 'Administrator'
navTitle.textContent = i18next.t('Urejanje')
else navTitle.textContent = i18next.t('Administracija')
siteHeading.style.display = 'block'
}
}
@@ -1310,9 +1460,9 @@ function createNewAreaInput(pageForm) {
divSmallNameArea.className = 'author mt-4 added-field'
divSubjectName.className = 'subject-name'
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.textContent = 'NOVO PODPODROČJE (angleško)'
spanInputNameTxtEng.textContent = i18next.t('NOVO PODPODROČJE (angleško)')
divRow.className = 'row align-items-center'
divRow2.className = 'row align-items-center'
divEnglishInput.className = 'mt-4'
@@ -1333,11 +1483,12 @@ function createNewAreaInput(pageForm) {
divColSm.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.textContent =
spanNameInfoTxt.textContent = i18next.t(
'Vpišite novo podpodročje. Na seznamu podpodročij bo vidno takoj po potrditvi administratorja portala.'
)
spanNameInfoTxtEng.className =
'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)
divSubjectName.appendChild(spanInputNameTxtSlo)
@@ -1468,6 +1619,10 @@ function changePageContent(el) {
const collapsableBtn = document.querySelectorAll('.collapsable-entry-btn')
const addedFields = document.querySelectorAll('.added-field')
const collapsibleData = document.querySelectorAll('.collapsible-data')
const responseModal = new bootstrap.Modal(
document.getElementById('duplicate-modal'),
{ keyboard: false }
)
switch (el) {
case 'content-preview':
loadPreview(info)
@@ -1530,6 +1685,7 @@ function changePageContent(el) {
break
case 'new-entry-btn': {
// formEditContent.reset()
formEditContent.removeAttribute('data-entry-id')
formEditContent.action = '/api/v1/entries/create'
if (editSection.classList.contains('d-none'))
@@ -1546,6 +1702,16 @@ function changePageContent(el) {
previewBtnEl.disabled = true
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 = ''
versionEl.textContent = 'Verzija 1'
previewAuthor.textContent = ''
@@ -1558,15 +1724,19 @@ function changePageContent(el) {
deleteEntry.disabled = true
changeCollapsedContent()
if (addedFields.length) addedFields.forEach(el => el.remove())
editBtnEl.click()
termInputField.focus()
const messageContainer = document.querySelectorAll('.message-container')
messageContainer.forEach(el => el.classList.add('d-none'))
window.scrollTo({ top: 0, behavior: 'smooth' })
selectedEntry.classList.remove('selected-term-btn')
$('.multiple').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
}
case 'first':
@@ -1610,7 +1780,6 @@ function changePageContent(el) {
commentsBtnEl.disabled = true
commentsBtnEl.classList.add('disabled')
showDates.disabled = true
duplicateEntry.disabled = true
deleteEntry.disabled = true
changeCollapsedContent()
editBtnEl.click()
@@ -1618,6 +1787,8 @@ function changePageContent(el) {
window.scrollTo({ top: 0, behavior: 'smooth' })
selectedEntry.classList.remove('selected-term-btn')
editBtnEl.click()
responseModal.toggle()
duplicateEntry.disabled = true
break
case 'true':
spanFilterText.classList.remove('normal-gray-label')
@@ -1798,8 +1969,9 @@ function loadPreview(info) {
: linksArr[i][key] === 'broader'
? 'BT:'
: ''
linkedTerms.innerHTML += keyTxt + ' ' + key + ' '
linkedTerms.innerHTML += keyTxt + ' ' + key
if (parseInt(i) !== linksArr.length - 1) linkedTerms.innerHTML += ', '
else linkedTerms.innerHTML += ' '
}
}
} else changeClasses(previewLinkedTerms, 'hide')
@@ -1864,19 +2036,21 @@ function loadPreview(info) {
}
const languageContainers = document.querySelectorAll('.preview-one-language')
const langLine = document.querySelector('.language-line')
const langLine = document.querySelector('.start-line')
if (languageContainers) {
const arrLang = Array.from(languageContainers)
arrLang.forEach(el => {
const arrChildren = Array.from(el.children)
if (arrChildren.filter(e => e.classList.contains('d-none')).length > 2) {
el.classList.add('d-none')
langLine.classList.add('d-none')
} else {
el.classList.remove('d-none')
langLine.classList.remove('d-none')
}
} else el.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()
}
@@ -1924,6 +2098,7 @@ function changeVersionList(versions) {
dateLabel.dataset.bsCustomClass = 'dark-gray-tooltip'
dateLabel.dataset.bsPlacement = 'bottom'
dateLabel.dataset.bsToggle = 'tooltip'
dateLabel.dataset.bsHtml = 'true'
const date = new Date(el.version_time).toLocaleDateString(
'sl-SL',
localeOptions
@@ -1932,9 +2107,16 @@ function changeVersionList(versions) {
dateRadio.id = `date${el.version}`
dateRadio.value = `${el.version}`
dateLabel.htmlFor = `date${el.version}`
// new bootstrap.Tooltip(dateLabel, {
// title: `Verzija ${el.version} <br> Avtor: ${el.version_author}`
// })
// eslint-disable-next-line
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.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) {
if (images !== null || audio !== null || video !== null) {
const media = [images, audio, video]
@@ -1998,7 +2204,7 @@ function showSelectedDateData(date) {
// classicOverview.classList.add('d-none')
// selectedOverview.classList.remove('d-none')
btnSaveIcon.firstChild.src = '/images/u_redo.svg'
btnSaveIcon.children[1].textContent = 'Obnovi'
btnSaveIcon.children[1].textContent = i18next.t('Obnovi')
btnSaveIcon.id = 'redo-action'
}
@@ -2038,7 +2244,7 @@ function changeCollapsedContent() {
function createHeadword() {
const { termInputField, headerwordTable } = window.dictionaryElements
if (!termInputField.value.length) alert('Vnesti morate termin')
if (!termInputField.value.length) alert(i18next.t('Vnesti morate termin'))
else {
const createHeadwordGroup = document.getElementById('create-headword-group')
const createdHeadwordGroup = document.getElementById(
@@ -2181,21 +2387,21 @@ function addField(form, element, content) {
if (formEditContent) {
switch (element) {
case newImageBtn:
spanName.textContent = 'SLIKA'
spanName.textContent = i18next.t('SLIKA')
inputField.name = 'image'
spanNameInfoTxt.textContent = 'Nova slika.'
spanNameInfoTxt.textContent = i18next.t('Nova slika.')
if (content) inputField.value = content
break
case newAudioBtn:
spanName.textContent = 'ZVOK'
spanName.textContent = i18next.t('ZVOK')
inputField.name = 'audio'
spanNameInfoTxt.textContent = 'Nov zvok.'
spanNameInfoTxt.textContent = i18next.t('Nov zvok.')
if (content) inputField.value = content
break
case newVideoBtn:
spanName.textContent = 'VIDEO'
spanName.textContent = i18next.t('VIDEO')
inputField.name = 'video'
spanNameInfoTxt.textContent = 'Nov video.'
spanNameInfoTxt.textContent = i18next.t('Nov video.')
if (content) inputField.value = content
break
}
@@ -2204,10 +2410,11 @@ function addField(form, element, content) {
element.parentElement.parentElement.parentElement
)
} else {
spanName.textContent = 'AVTOR'
spanName.textContent = i18next.t('AVTOR')
inputField.name = 'author'
spanNameInfoTxt.textContent =
'Dodajte ime in priimek naslednjega avtorja slovarja..'
spanNameInfoTxt.textContent = i18next.t(
'Dodajte ime in priimek naslednjega avtorja slovarja.'
)
form.insertBefore(
divMarginTop,
element.parentElement.parentElement.parentElement
@@ -2224,7 +2431,6 @@ function addConnectionField(form, element, data) {
const divRow = document.createElement('div')
const divColLg2 = document.createElement('div')
const divColLg4 = document.createElement('div')
const divColLg = document.createElement('div')
const colLg2Input = document.createElement('select')
const inputGroup = document.createElement('div')
const inputField = document.createElement('input')
@@ -2243,7 +2449,6 @@ function addConnectionField(form, element, data) {
divColLg2.appendChild(colLg2Input)
colLg2Input.appendChild(opt)
divRow.appendChild(divColLg4)
divRow.appendChild(divColLg)
divColLg4.appendChild(inputGroup)
inputGroup.appendChild(inputField)
inputGroup.appendChild(spanInputGroup)
@@ -2252,32 +2457,34 @@ function addConnectionField(form, element, data) {
divColSm.appendChild(spanNameInfoTxt)
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'
divRow.className = 'row align-items-center'
divColLg2.className = 'col-lg-2 col-6'
divColLg4.className = 'col-lg-4 mt-2 mt-lg-0'
divColLg.className = 'col-lg align-items-center'
divColLg2.className = 'col-xl-2 col-4'
divColLg4.className = 'col-8 col-xl-6 col-xxl-4'
colLg2Input.className = 'name-input form-select d-inline'
colLg2Input.name = 'type'
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.id = 'trash-icon-btn'
spanInputGroup.addEventListener('click', deleteField)
imgTrashIcon.className = 'delete-author p-0'
imgTrashIcon.src = '/images/red-trash-icon.svg'
imgTrashIcon.alt = 'Delete'
divColSm.className = 'col-sm'
spanNameInfoTxt.className = 'name-info-txt mt-3'
spanName.textContent = 'POVEZAVA'
divColSm.className = 'col d-none d-xl-flex align-items-center'
spanNameInfoTxt.className = 'd-md-inline d-block name-info-txt mt-3 mt-sm-0'
spanNameInfoTxt.textContent = i18next.t('Nov povezan termin.')
spanName.textContent = i18next.t('POVEZANI TERMIN')
inputField.name = 'links'
opt.value = 'broader'
opt.text = 'Širši'
opt.text = i18next.t('Širši')
opt2.value = 'related'
opt3.value = 'narrow'
opt2.text = 'Sorodni'
opt3.text = 'Ožji'
opt2.text = i18next.t('Sorodni')
opt3.text = i18next.t('Ožji')
colLg2Input.add(opt2)
colLg2Input.add(opt)
colLg2Input.add(opt3)
@@ -2286,6 +2493,32 @@ function addConnectionField(form, element, data) {
divMarginTop,
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) {
inputField.value = data.link
colLg2Input.value = data.type
@@ -2336,14 +2569,14 @@ function handleAreasClick({ target }) {
const saveButton = document.createElement('button')
saveButton.type = 'button'
cancelButton.type = 'button'
cancelButton.textContent = 'Prekliči'
cancelButton.textContent = i18next.t('Prekliči')
cancelButton.className = 'btn btn-secondary me-2'
cancelButton.style.height = '33px'
cancelButton.style.width = '105px'
cancelButton.addEventListener('click', () =>
abortEditing(newButtonGroup, tableButtons, tDataArea)
)
saveButton.textContent = 'POTRDI'
saveButton.textContent = i18next.t('POTRDI')
saveButton.type = 'button'
saveButton.className = 'btn btn-primary'
saveButton.style.height = '33px'
@@ -2593,6 +2826,12 @@ function deleteContentData(
linkType.value = 'related'
linkText.value = ''
}
if (listForeignDefinitions) {
const foreignDefinitionsFields = document.querySelectorAll(
'.foreign-definition-el'
)
foreignDefinitionsFields.forEach(el => (el.value = ''))
}
}
function activateMe(term) {
@@ -2666,7 +2905,7 @@ function checkLanguages() {
}
$('.summernote').summernote({
placeholder: 'Na kratko opišite zasnovo in namen slovarja.',
placeholder: i18next.t('Na kratko opišite zasnovo in namen slovarja.'),
height: 300,
minheight: 150,
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')
selectorEl.addEventListener('change', () => loadCandidates(selectorEl.value))
const resultsListEl = document.getElementById('page-results')
const importFormEl = document.getElementById('import-form')
const importButtonEl = document.getElementById('import-btn')
let termCandidates
let hitsPerPage
let numberOfAllPages
importFormEl.addEventListener('submit', submitForm)
async function loadCandidates(id) {
try {
const { data } = await axios.get(
@@ -18,6 +23,8 @@
const results = getDataForFirstPage(data)
renderResults(results)
updateDemoPager(1, numberOfAllPages)
importFormEl.action = `/api/v1/dictionaries/${dictionaryId}/import-extraction/${id}`
importButtonEl.disabled = false
} catch (error) {
console.log(error)
}
@@ -55,6 +62,10 @@
}
function renderResults(results) {
const tableContainer = document.querySelector(
'.list-terminology-candidates'
)
tableContainer.classList.remove('d-none')
results.forEach(([sequentialCount, candidate]) => {
const rowEl = document.createElement('tr')
const tdId = document.createElement('td')
@@ -64,9 +75,27 @@
tdId.textContent = sequentialCount
tdName.textContent = candidate.kanonicnaoblika
tdSize.textContent = candidate.ranking
tdDate.textContent = candidate.pogostostpojavljanja
tdDate.textContent = candidate.pogostostpojavljanja[0]
rowEl.append(tdId, tdName, tdSize, tdDate)
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 fileInputEl = fileUploadForm.querySelector('input[type="file"]')
const filesListEl = document.getElementById('files-list')
const dropArea = document.querySelector('.drag-area')
const dragButton = dropArea.querySelector('button')
const dragInput = dropArea.querySelector('input')
const modalSpinner = new bootstrap.Modal(
document.getElementById('modal-spinner')
)
const modalAlert = new bootstrap.Modal(document.getElementById('alert-modal'))
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 = () => {
dragInput.click()
@@ -68,96 +72,216 @@ fileInputEl.addEventListener('change', submitFiles)
filesListEl.addEventListener('click', handleFileClick)
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 filesToUpload = []
const failedUploads = []
modalSpinner.toggle()
const succeededFileListEls = []
let didRemoveAnyFileListEls = false
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) {
const failedUpload = {
filename: file.name,
filename,
message: 'File too large. Must not be over 1 GB.'
}
failedUploads.push(failedUpload)
updateFileListEl(fileListEl, { status: 'failed' })
continue
}
filesToUpload.push([file, fileListEl])
}
if (didRemoveAnyFileListEls) reindexFileListEls()
for (const [file, fileListEl] of filesToUpload) {
try {
const payload = new FormData()
payload.set(fileInputEl.name, file)
try {
await axios.put(apiEndpointBase, payload)
const { data: fileStats } = await axios.put(apiEndpointBase, payload, {
onUploadProgress: displayUploadProgress(fileListEl)
})
succeededFileListEls.push(fileListEl)
updateFileListEl(fileListEl, { status: 'success', fileStats })
} catch (error) {
const failedUpload = {
filename: file.name,
message: error.response.data
}
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 = ''
displayFailedUploads(failedUploads)
modalSpinner.toggle()
succeededFileListEls.forEach(el => updateFileListEl(el, { status: 'done' }))
if (failedUploads.length) displayFailedUploads(failedUploads)
isUploadInProgress = false
fileUploadContainerEl.hidden = false
}
function updateFilesList(files) {
removeAllChildNodes(filesListEl)
files.forEach(({ filename, size, timeModified }, index) => {
const rowEl = document.createElement('tr')
const tdId = document.createElement('td')
const tdName = document.createElement('td')
const tdSize = document.createElement('td')
const tdDate = document.createElement('td')
const tdDelete = document.createElement('td')
tdId.textContent = index + 1
tdName.textContent = filename
tdName.className = 'filename'
tdSize.textContent = size
const formattedDate = new Date(timeModified).toLocaleDateString('sl-SL')
tdDate.textContent = formattedDate
const delBtn = document.createElement('button')
delBtn.className = 'p-0 delete-file delete-btn-table'
delBtn.type = 'button'
const deleteImg = document.createElement('img')
deleteImg.src = '/images/red-trash-icon.svg'
deleteImg.alt = 'Izbriši'
const delSpan = document.createElement('span')
delSpan.className = 'ms-2'
delSpan.textContent = 'Briši'
delBtn.appendChild(deleteImg)
delBtn.appendChild(delSpan)
tdDelete.appendChild(delBtn)
rowEl.append(tdId, tdName, tdSize, tdDate, tdDelete)
filesListEl.appendChild(rowEl)
})
function createFileListEl(filename) {
const indexEl = document.createElement('td')
indexEl.className = 'file-index'
indexEl.textContent = ++fileListElCount
const nameEl = document.createElement('td')
nameEl.className = 'file-name'
nameEl.textContent = filename
const sizeEl = document.createElement('td')
sizeEl.className = 'file-size'
const dateEl = document.createElement('td')
dateEl.className = 'file-date-modified'
const progressBarId = `progress-bar-${fileListElCount}`
const progressLabelEl = document.createElement('label')
progressLabelEl.className = 'me-1'
progressLabelEl.for = progressBarId
progressLabelEl.textContent = '0%'
const progressBarEl = document.createElement('progress')
progressBarEl.id = progressBarId
progressBarEl.className = 'upload-progress'
progressBarEl.value = 0
progressBarEl.max = 100
const lastEl = document.createElement('td')
lastEl.className = 'file-last-cell'
lastEl.append(progressLabelEl, progressBarEl)
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) {
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 }) => {
const alertText = modalAlert.querySelector('#alert-text')
alertText.textContent = `NAPAKA - Ime datoteke: ${filename}, razlog: ${message}`
modalAlert.toggle()
const bulletEl = document.createElement('li')
bulletEl.textContent = `${filename} - ${message}`
listEl.appendChild(bulletEl)
})
modalAlert.toggle()
}
async function handleFileClick(e) {
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 filename = fileEl.querySelector('.filename').textContent
const filename = fileEl.querySelector('.file-name').textContent
try {
await axios.delete(`${apiEndpointBase}/${filename}`)
fileEl.remove()
existingFileNames.splice(existingFileNames.indexOf(filename), 1)
reindexFileListEls()
isDeletionInProgress = false
} 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 () => {
await axios.delete(`/api/v1/extraction/${extractionId}`)
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 extractionId = extractionEl.dataset.id
await axios.put(`/api/v1/extraction/${extractionId}/begin`)
const responseModal = new bootstrap.Modal(
document.getElementById('begin-response')
)
const beginExtractionModalEl = document.getElementById('begin-response')
const responseModal = new bootstrap.Modal(beginExtractionModalEl)
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')) {
const extractionEl = target.closest('.task')
const extractionId = extractionEl.dataset.id
@@ -1,4 +1,4 @@
/* global $, axios, bootstrap */
/* global $, axios, bootstrap, i18next */
$('.pick-multiple').select2()
$('.enter-multiple').select2({
@@ -41,8 +41,8 @@ async function handleSearch() {
}
function handleSearchError() {
const alertText = modalAlert.querySelector('#alert-text')
alertText.textContent = `NAPAKA pri iskanju`
const alertText = modalAlert._element.querySelector('#alert-text')
alertText.textContent = i18next.t('NAPAKA pri iskanju')
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
tdName.textContent = candidate.kanonicnaoblika
tdSize.textContent = candidate.ranking
tdDate.textContent = candidate.pogostostpojavljanja
tdDate.textContent = candidate.pogostostpojavljanja[0]
rowEl.append(tdId, tdName, tdSize, tdDate)
resultsListEl.appendChild(rowEl)
})
+6 -6
View File
@@ -1,4 +1,4 @@
/* global currentPagePath */
/* global currentPagePath, i18next */
// const currentPagePath = location.pathname
@@ -103,22 +103,22 @@ function initExtraction() {
btnStart.type = 'button'
btnImg.src = '/images/fi_arrow-right-circle.svg'
btnSpan.className = 'ms-1'
btnSpan.textContent = 'Začni'
btnSpan.textContent = i18next.t('Začni')
hr.className = 'mt-2 mb-3'
divSmFlex2.className = 'd-flex justify-content-between'
divSmFlexACMb.className = 'd-sm-flex align-items-center mb-0'
imgAlertCircle.src = '/images/alert-circle.svg'
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'
divEditTask.className = 'align-items-center me-3 edit-task'
imgEditAlt.src = '/images/u_edit-alt.svg'
spanEdit.className = 'ms-1 normal-gray'
spanEdit.textContent = 'Uredi'
spanEdit.textContent = i18next.t('Uredi')
divDeleteTask.className = 'ms-3 align-items-center delete-task'
imgDeleteAlt.src = '/images/red-trash-icon.svg'
spanDelete.className = 'ms-1 normal-gray'
spanDelete.textContent = 'Briši'
spanDelete.textContent = i18next.t('Briši')
taskNewContainer.appendChild(divSmFlex)
divSmFlex.appendChild(divSmGrid)
@@ -233,7 +233,7 @@ function mobileMoveContent() {
primaryButton.style.whiteSpace = ''
}
navTitle.textContent = 'Urejanje'
navTitle.textContent = i18next.t('Urejanje')
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'
regDescription.className = regDescription.className.replace('d-none', '')
loginDescription.className = loginDescription.className + ' d-none'
clearErrorView()
}
const listenLoginBtn = event => {
@@ -38,6 +39,7 @@ const listenLoginBtn = event => {
regHeader.className = regHeader.className + ' d-none'
loginDescription.className = loginDescription.className.replace('d-none', '')
regDescription.className = regDescription.className + ' d-none'
clearErrorView()
}
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
Includes error X icons for login
@@ -152,14 +171,23 @@ function updateRegisterWindowOnSuccess(
footer.appendChild(document.createElement('button'))
const btn = footer.firstElementChild
btn.classList = 'btn btn-secondary text-secondary'
btn.textContent = 'ZAPRI'
btn.textContent = i18next.t('ZAPRI')
btn.ariaLabel = 'Close'
btn.dataset.bsDismiss = 'modal'
const successDescriptionPararaph = body.children[1]
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.
Povezava za potrditev je veljavna ${days} dni.`
// 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.
// 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
@@ -199,7 +227,7 @@ function updateRegisterWindowOnSuccess(
updateLoginRegisterErrorView(
registerErrorLabels[iterator],
true,
'Prazno obvezno polje'
i18next.t('Prazno obvezno polje')
)
failFrontendValidation = true
} else {
@@ -215,7 +243,7 @@ function updateRegisterWindowOnSuccess(
updateLoginRegisterErrorView(
'error-email',
true,
'Neveljavna e-pošta'
i18next.t('Neveljavna e-pošta')
)
} else {
updateLoginRegisterErrorView('error-email', false)
@@ -226,7 +254,7 @@ function updateRegisterWindowOnSuccess(
updateLoginRegisterErrorView(
'error-password',
true,
'Geslo je prekratko'
i18next.t('Geslo je prekratko')
)
} else {
updateLoginRegisterErrorView('error-password', false)
@@ -237,7 +265,7 @@ function updateRegisterWindowOnSuccess(
updateLoginRegisterErrorView(
'error-password-repeat',
true,
'Geslo se ne ujema'
i18next.t('Geslo se ne ujema')
)
}
}
@@ -263,11 +291,11 @@ function updateRegisterWindowOnSuccess(
// event.target.messageBind.textContent = data
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
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
}
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
// search filter functions
@@ -173,7 +188,11 @@ function sbmFn(sbm) {
inputString = '*'
}
if (sbm === document.querySelector('.search-btn-a')) {
searchQuery(inputString, searchFilterDOM)
} else {
searchQueryWithExistingFilters(inputString)
}
})
}
}
+57 -19
View File
@@ -1,21 +1,42 @@
const allMixedContentFields = document.querySelectorAll('.mc-field')
allMixedContentFields.forEach(el =>
el.addEventListener('focusin', () => showMCButtons(el))
el.addEventListener('focus', () => showMCButtons(el))
)
document.addEventListener('keypress', e => {
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()
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) {
const parent = element.parentElement.parentElement.parentElement
function showMCButtons(element, linkTerm) {
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')
// 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')
const btnGrpChildren = btnGrp.children
const childrenBtns = Array.from(btnGrpChildren)
@@ -29,10 +50,10 @@ function showMCButtons(element) {
{ once: true }
)
)
document.addEventListener('click', function (event) {
if (parent !== event.target && !parent.contains(event.target)) {
btnGrp.classList.add('d-none')
}
document.addEventListener('keyup', function (e) {
const selectedEl = document.activeElement
if (!parent.contains(selectedEl)) btnGrp.classList.add('d-none')
})
}
@@ -50,8 +71,9 @@ function addMixedContentInput(el, selectedEl) {
inputText.substring(0, selectedStart) +
'<b>' +
inputText.substring(selectedStart, selectedEnd) +
'</b> ' +
'</b>' +
inputText.substring(selectedEnd)
placeCaret(selectedEl, selectedEnd + 7)
}
if (el.classList.contains('mc-italic') && selectedText.length) {
@@ -59,8 +81,9 @@ function addMixedContentInput(el, selectedEl) {
inputText.substring(0, selectedStart) +
'<i>' +
inputText.substring(selectedStart, selectedEnd) +
'</i> ' +
'</i>' +
inputText.substring(selectedEnd)
placeCaret(selectedEl, selectedEnd + 7)
}
if (el.classList.contains('mc-supscript') && selectedText.length) {
@@ -68,8 +91,9 @@ function addMixedContentInput(el, selectedEl) {
inputText.substring(0, selectedStart) +
'<sup>' +
inputText.substring(selectedStart, selectedEnd) +
'</sup> ' +
'</sup>' +
inputText.substring(selectedEnd)
placeCaret(selectedEl, selectedEnd + 11)
}
if (el.classList.contains('mc-subscript') && selectedText.length) {
@@ -77,27 +101,41 @@ function addMixedContentInput(el, selectedEl) {
inputText.substring(0, selectedStart) +
'<sub>' +
inputText.substring(selectedStart, selectedEnd) +
'</sub> ' +
'</sub>' +
inputText.substring(selectedEnd)
placeCaret(selectedEl, selectedEnd + 11)
}
if (el.classList.contains('mc-hyperlink') && selectedText.length) {
selectedEl.value =
inputText.substring(0, selectedStart) +
'<link url="">' +
'<a href="">' +
inputText.substring(selectedStart, selectedEnd) +
'</link> ' +
'</a>' +
inputText.substring(selectedEnd)
}
if (el.classList.contains('mc-line-break')) {
selectedEl.value =
inputText.substring(0, selectedStart) +
'<br />' +
'<br/>' +
inputText.substring(selectedEnd)
placeCaret(selectedEl, selectedEnd + 5)
el.removeEventListener('click', () => addMixedContentInput, false)
}
}
// selectedEl.focus()
// selectedEl.setSelectionRange(selectedStart, selectedEnd)
function placeCaret(elem, caretPos) {
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)
} */
// TODO MARK FOR REVIEW WHETHER THIS METHODS ARE STILL NEEDED
function prepareQueryArrayForArrayWithIDs(page, searchQuery, filters = {}) {
const qParams = new URL(location).searchParams
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']
@@ -302,7 +312,7 @@ function renderSuggestions(
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.classList = ['w-100']
preElement.classList += ` ${type}-sugg`
@@ -646,11 +656,26 @@ document.querySelectorAll('.loginregisterlabel').forEach(label => {
/**
* 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.
* @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 btnFirstPage = rootEl.querySelector('.first-page')
const btnPreviousPage = rootEl.querySelector('.previous-page')
@@ -660,17 +685,24 @@ function initPagination(paginationRootElId, onPageChange, currentPage = 1) {
const pageInputEl = formEl.querySelector('input')
const pagesCountDisplayEl = formEl.querySelector('.pages-total')
let reqLock = false
numOfAllPages = +pagesCountDisplayEl.textContent
rootEl.addEventListener('click', handleButtonClick)
formEl.addEventListener('submit', handleFormSubmit)
backwardBtnEls.push(btnFirstPage, btnPreviousPage)
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
const buttonEl = target.closest(`#${paginationRootElId} button`)
if (!buttonEl) return
const numOfAllPages = +pagesCountDisplayEl.textContent
if (buttonEl.classList.contains('first-page')) {
if (currentPage === 1) return
@@ -691,13 +723,13 @@ function initPagination(paginationRootElId, onPageChange, currentPage = 1) {
}
}
function handleFormSubmit(e) {
function handleFormSubmit(e, pageInputEl) {
e.preventDefault()
if (reqLock) return
const inputValue = +pageInputEl.value
if (!(inputValue > 0 && inputValue <= pagesCountDisplayEl.textContent)) {
alert('Nepravilna vrednost strani')
if (!(inputValue > 0 && inputValue <= numOfAllPages)) {
alert(i18next.t('Nepravilna vrednost strani'))
pageInputEl.value = currentPage
return
}
@@ -708,20 +740,12 @@ function initPagination(paginationRootElId, onPageChange, currentPage = 1) {
function enableLock() {
reqLock = true
btnFirstPage.disabled = true
btnPreviousPage.disabled = true
btnNextPage.disabled = true
btnLastPage.disabled = true
pageInputEl.disabled = true
allControlEls.forEach(el => (el.disabled = true))
}
function disableLock() {
reqLock = false
btnFirstPage.disabled = false
btnPreviousPage.disabled = false
btnNextPage.disabled = false
btnLastPage.disabled = false
pageInputEl.disabled = false
allControlEls.forEach(el => (el.disabled = false))
}
function updatePagerUi(newCurrentPage, newNumOfAllPages) {
@@ -729,23 +753,20 @@ function initPagination(paginationRootElId, onPageChange, currentPage = 1) {
if (!newCurrentPage) return
currentPage = newCurrentPage
pageInputEl.value = newCurrentPage
pagesCountDisplayEl.textContent = newNumOfAllPages
numOfAllPages = newNumOfAllPages
pageInputEls.forEach(el => (el.value = newCurrentPage))
pagesCountDisplayEls.forEach(el => (el.textContent = newNumOfAllPages))
if (newCurrentPage === 1) {
btnFirstPage.disabled = true
btnPreviousPage.disabled = true
backwardBtnEls.forEach(el => (el.disabled = true))
} else {
btnFirstPage.disabled = false
btnPreviousPage.disabled = false
backwardBtnEls.forEach(el => (el.disabled = false))
}
if (newCurrentPage === newNumOfAllPages) {
btnNextPage.disabled = true
btnLastPage.disabled = true
forwardBtnEls.forEach(el => (el.disabled = true))
} else {
btnNextPage.disabled = false
btnLastPage.disabled = false
forwardBtnEls.forEach(el => (el.disabled = false))
}
}
@@ -923,22 +944,24 @@ if (registerSwithcButton) {
function initSelect2(querySelector, placeholder) {
$(querySelector).select2({
allowClear: true,
// allowClear: true,
placeholder: placeholder
})
$(querySelector).val(null).trigger('change')
// $(querySelector).val(null).trigger('change')
}
$(document).ready(function () {
// TODO refactor wth specific select2
isI18nReady.then(t => {
$('.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')
initSelect2('.select-domain-field', t('Področje'))
initSelect2('.select-src-lang-field', t('Jezik iskanja'))
initSelect2('.select-dest-lang-field', t('Ciljni jezik'))
initSelect2('.select-dict-field', t('Slovar'))
initSelect2('.select-source-field', t('Vir'))
$('b[role="presentation"]').hide()
$('.select2-selection__arrow').append(
@@ -970,48 +993,6 @@ if (registerSwithcButton) {
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 */
@@ -1097,7 +1078,7 @@ function transferText(sideMenuText, removeOptionalBreak = false) {
const optionalBreak = document.getElementById('disposable-break')
if (document.body.clientWidth <= 1200) {
if (window.innerWidth < 1200) {
navTitle.textContent = siteHeadingTextContent
siteHeading.style.display = 'none'
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
} catch (error) {
let message = 'Prišlo je do napake.'
let message = i18next.t('Prišlo je do napake.')
if (error.response?.data) {
message = error.response.data
} else if (error.request) {
message = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
updatePager()
+58 -312
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
function adjustOffsetBy() {
// const { offsetMain, fixedTopSection } = window.dictionaryElements
const BROWSER_UNUSUAL_OFFSET = 17 // computer from chrome
const offsetMain = document.querySelector('#offset-main')
const fixedTopSection = document.querySelector('#fixed-top-section')
@@ -14,7 +19,7 @@ function adjustOffsetBy() {
// const adminNavMobile = document.getElementsByClassName('admin-nav')
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`
} else {
for (let i = 0; i < offsetHeader.length; i++) {
@@ -80,8 +85,14 @@ function mobileMoveContent() {
'.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 {
if (document.body.clientWidth <= 1200) {
if (window.innerWidth < 1200) {
if (secondaryButton) {
mobileRightHolder.appendChild(secondaryButton)
// secondaryButton.style.height = '28px'
@@ -98,9 +109,11 @@ function mobileMoveContent() {
primaryButton.style.whiteSpace = 'nowrap'
}
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) {
headerContainerRight.appendChild(secondaryButton)
secondaryButton.style.height = ''
@@ -117,7 +130,7 @@ function mobileMoveContent() {
primaryButton.style.whiteSpace = ''
}
navTitle.textContent = 'Urejanje'
navTitle.textContent = i18next.t('Urejanje')
siteHeading.style.display = 'block'
}
} catch (e) {}
@@ -128,7 +141,11 @@ if (resultsListEl) {
resultsListEl.addEventListener('click', onResultClick)
}
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) {
const receivedPageNumber = await changePage(newPage)
@@ -146,7 +163,16 @@ async function changePage(newPage) {
removeAllChildNodes(resultsListEl)
renderResults(resultsMarkup)
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 {
@@ -160,12 +186,12 @@ async function changePage(newPage) {
return page
} catch (error) {
// console.log(error)
let message = 'Prišlo je do napake.'
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 = 'Strežnik ni dosegljiv. Poskusite kasneje.'
message = i18next.t('Strežnik ni dosegljiv. Poskusite kasneje.')
}
alert(message)
updatePager()
@@ -210,119 +236,10 @@ function onResultClick(e) {
try {
if (tooltipTriggerList.length > 0) {
// 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) {}
/*
// 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
function largeStringsOnSmallScreen(alwaysRefresh) {
let headwordTexts
@@ -336,30 +253,6 @@ function largeStringsOnSmallScreen(alwaysRefresh) {
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()
}
@@ -379,175 +272,28 @@ function largeStringsOnSmallScreen(alwaysRefresh) {
ttElement.tooltip('dispose')
} catch (e) {}
}
/*
function initTooltips() {
try {
// if (dynamicTooltipList.length || alwaysRefresh) {
setupToolTips()
// }
} catch (e) {}
}
function removeTooltips() {
try {
$('.rihw').tooltip('dispose')
$('.term-h').children().tooltip('dispose')
$('.syn-h').tooltip('dispose')
$('.risy').tooltip('dispose')
} catch (e) {}
}
*/
/* let OLD_TEXT_LIMIT = 5000 // Guard to not prevent overdisposing
function manageResize() {
const tmpTerms = [...$('.term-h')].map(e => {
return e.children[0]
})
const elements = [...tmpTerms, ...$('.syn-h')]
// console.log(document.documentElement.clientWidth) */
/*
$('.word-constraint').each((i, element) => {
// WRONG const wordBr = $(`.word-constraint:nth-child(${i}) > .word-breakable`)
const wordList = $(element).find('.word-breakable')
wordList.each((i, wordElement) => {
// console.log(wordElement.innerText)
if (
wordElement.clientWidth > element.clientWidth &&
wordElement.innerText.split(' ').length <= 1
) {
console.log('Lion')
createTooltip($(wordElement))
wordElement.classList.push('text-ellipsis')
} else {
removeTooltip($(wordElement))
// console.log(wordElement.classList)
wordElement.className = Array.from(wordElement.classList)
.filter(e => e !== 'text-ellipsis')
.join(' ')
// console.log(wordElement.classList)
}
})
// console.log(wordBr)
})
*/
/* const dcw = document.documentElement.clientWidth
// const checkBorders = (dcw > 420 && dcw < 750) || dcw < 370
let borderIndex
// check _common.scc long-text-strip and make sure the widths are aligned
if (dcw < 420) {
borderIndex = 0 // width = 8 chars (<420)
} else if (dcw < 750 || (dcw > 990 && dcw < 1750)) {
borderIndex = 1 // width = 12 chars (420 - 750, 990-1750)
} else if (dcw > 1750) {
borderIndex = 3 // width = 100% (>1759)
} else {
borderIndex = 2 // width = 25 chars (750-990)
}
let TEXT_LIMIT
if (borderIndex === 0) {
TEXT_LIMIT = 8
} else if (borderIndex === 1) {
TEXT_LIMIT = 12
} else if (borderIndex === 2) {
TEXT_LIMIT = 25
} else {
TEXT_LIMIT = null
}
if (OLD_TEXT_LIMIT !== TEXT_LIMIT) {
elements.forEach(el => {
// console.log(el.innerHTML)
if (TEXT_LIMIT && el.innerHTML.length > TEXT_LIMIT) {
console.log(`DEBUG TEXT LIMIT: ${TEXT_LIMIT}`)
console.log(`DEBUG EL LEN: ${el.innerHTML.length}`)
console.log(`DEBUG EL TEXT: ${el.innerHTML}`)
console.log('TOOLTIP CREATED')
createTooltipEl(el, 'gray-tooltip')
} else {
removeTooltip($(el))
}
})
}
OLD_TEXT_LIMIT = TEXT_LIMIT */
/*
if (checkBorders) {
console.log('HEYOO')
const TEXT_LIMIT = 17
elements.forEach(el => {
el.dataset.textStore = el.innerHTML.includes('...')
? el.dataset.textStore
: el.innerHTML
el.innerHTML = `${el.innerHTML.slice(0, TEXT_LIMIT)}${
el.innerHTML.length > TEXT_LIMIT ? '...' : ''
}`
})
initTooltips()
} else {
elements.forEach(el => {
if (el.dataset.textStore) {
el.innerHTML = el.dataset.textStore
}
})
removeTooltips()
}
*/
/* }
return {
init: function () {
init()
},
refresh: function () {
init()
},
manageResize: manageResize
} */
}
// const textsScreenManager = largeStringsOnSmallScreen(true)
// textsScreenManager.manageResize()
// window.onresize = textsScreenManager.manageResize
/*
function transferText(sideMenuText) {
const siteHeading = document.getElementById('site-heading')
const siteHeadingTextContent = siteHeading.textContent
const navTitle = document.getElementById('nav-title')
if (document.body.clientWidth <= 1200) {
navTitle.textContent = siteHeadingTextContent
siteHeading.style.display = 'none'
} else {
navTitle.textContent = sideMenuText
}
}
*/
function handleProperTextDisplay() {
if (/\/iskanje/.test(currentPagePath)) {
transferText('Iskanje po slovarjih', true)
} else if (/\/termin/.test(currentPagePath)) {
transferText('', true)
}
}
window.addEventListener('resize', () => {
handleProperTextDisplay()
adjustOffsetBy()
window.addEventListener('load', () => {
$('[data-toggle="tooltip"]').tooltip()
})
handleProperTextDisplay()
isI18nReady.then(t => {
function handleProperTextDisplay() {
// const BROWSER_UNUSUAL_OFFSET = 17
if (/\/iskanje/.test(currentPagePath)) {
transferText(t('Iskanje po slovarjih'), true, 'site-heading') //,
// BROWSER_UNUSUAL_OFFSET
// )
} else if (/\/termin/.test(currentPagePath)) {
transferText('', true, 'site-heading') // , BROWSER_UNUSUAL_OFFSET)
}
}
window.addEventListener('resize', () => {
handleProperTextDisplay()
adjustOffsetBy()
})
handleProperTextDisplay()
})
+6 -6
View File
@@ -1,4 +1,4 @@
/* global $, currentPagePath */
/* global $, currentPagePath, i18next */
// const currentPagePath = location.pathname
@@ -60,13 +60,13 @@ function initSelect() {
const userPassword2 = ce.userConfirmationPassEl.value
if (userPassword1 === '') {
alert('Prosim, vnesite geslo')
alert(i18next.t('Prosim, vnesite geslo'))
} else if (userPassword2 === '') {
alert('Prosim, ponovno vnesite geslo')
alert(i18next.t('Prosim, ponovno vnesite geslo'))
} else if (userPassword1 !== userPassword2) {
alert('Gesli se ne ujemata')
alert(i18next.t('Gesli se ne ujemata'))
} else {
alert('Gesli se ujemata!')
alert(i18next.t('Gesli se ujemata!'))
}
}
}
@@ -115,7 +115,7 @@ function deleteField(ele) {
// 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,
minheight: 150,
toolbar: [

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