Fix most glaring bugs and vulnerabilities

This commit is contained in:
Luka Romih
2023-05-08 23:25:25 +02:00
parent 9867875ef2
commit d0e53fee3b
144 changed files with 2231 additions and 2981 deletions
-1
View File
@@ -1,3 +1,2 @@
old_express
express/public/stylesheets
help-pug-demo.pug
+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.3
FROM ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.4
COPY --from=builder /sloleks /sloleks
+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-term-portal:v1.0.3
FROM ghcr.io/clarinsi/rsdo-concordancer-api-term-portal:v1.0.4
COPY --from=builder /sloleks /sloleks
+2 -2
View File
@@ -95,7 +95,7 @@ services:
- "127.0.0.1:${OS_DASHBOARDS_LISTEN_PORT:?}:5601"
concordancer:
# image: ghcr.io/clarinsi/rsdo-concordancer-api-term-portal:v1.0.3
# image: ghcr.io/clarinsi/rsdo-concordancer-api-term-portal:v1.0.4
build:
context: concordancer
dockerfile: Dockerfile.prod
@@ -115,7 +115,7 @@ services:
# Disabled by default. Enable if needed.
concordancer-manager:
image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.3
image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.4
profiles:
- concordancer-manager
depends_on:
+2 -2
View File
@@ -102,7 +102,7 @@ services:
- "127.0.0.1:${OS_DASHBOARDS_LISTEN_PORT:?}:5601"
concordancer:
# image: ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.3
# image: ghcr.io/clarinsi/rsdo-concordancer-api:v1.0.4
build: concordancer
profiles:
- develop-concordancer
@@ -121,7 +121,7 @@ services:
- sloleks:/sloleks
concordancer-manager:
image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.3
image: ghcr.io/clarinsi/rsdo-concordancer-systemmanager:v1.0.4
profiles:
- develop-concordancer
depends_on:
+11 -2
View File
@@ -6,6 +6,7 @@ const helmet = require('helmet')
const favicon = require('serve-favicon')
const cookieParser = require('cookie-parser')
const createError = require('http-errors')
const flash = require('connect-flash-plus')
const i18next = require('i18next')
const i18nextMiddleware = require('i18next-http-middleware')
// const debug = require('debug')('termPortal:app')
@@ -18,7 +19,8 @@ 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')
@@ -41,16 +43,22 @@ app.locals.basedir = viewsPath
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'))
@@ -59,6 +67,7 @@ app.use(i18nextMiddleware.handle(i18next))
app.use(user.enhance)
app.use(settings.prepareRequiredSettings)
app.use(enhanceLocals)
app.use(adjustHeaders)
if (inDevEnv) {
app.post(
+2 -5
View File
@@ -12,8 +12,6 @@ const cache = require('../models/cache')
const searchEngine = require('../models/search-engine')
const email = require('../models/email')
const init = require('../config/init')
const { seedDummyData } = require('../models/comment')
// const { initDemoData } = require('../models/demo-paginacija')
const app = require('../app')
const debug = require('debug')('termPortal:server')
const http = require('http')
@@ -42,12 +40,11 @@ const server = http.createServer(app)
cache.waitForConnection(),
searchEngine.waitForConnection(),
email.waitForConnection(),
init()
init.fsStructure()
])
await searchEngine.initEntryIndex()
await searchEngine.initConsultancyEntryIndex()
seedDummyData()
// initDemoData()
await init.adminUser()
server.listen(port)
})()
+55 -1
View File
@@ -1,6 +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')
module.exports = async () => {
exports.fsStructure = async () => {
await mkdir(TEMP_EXPORT_PATH, { recursive: true })
}
async function createPortalAdmin() {
const {
rows: [{ exists }]
} = await db.query(
"SELECT EXISTS (SELECT 1 FROM user_role WHERE role_name = 'portal admin')"
)
if (exists) return 'Skipping creation of portal admin (already exists)'
const ADMIN_BASE = 'admin'
const adminUser = {
username: ADMIN_BASE,
firstName: ADMIN_BASE,
lastName: ADMIN_BASE,
password: portalAdminInitialPassword,
email: portalAdminInitialEmail
}
const userId = await User.create(adminUser)
const assignAdminRole = db.query(
`INSERT INTO user_role (user_id, role_name)
VALUES
($1, 'portal admin'),
($1, 'dictionaries admin'),
($1, 'consultancy admin'),
($1, 'consultant')`,
[userId]
)
const activateAdminUser = db.query(
`UPDATE "user" SET status = 'active', time_activated = time_registered WHERE username = $1`,
[adminUser.username]
)
await Promise.all([assignAdminRole, activateAdminUser])
return `Successfully created portal admin (username: ${adminUser.username}, password: ${adminUser.password})`
}
exports.adminUser = async () => {
// Generate portal admin user in empty DB.
// TODO Replace with a more robust solution for production.
try {
const message = await createPortalAdmin()
debug(message)
} catch (error) {
debug('Portal admin not seeded.')
debug(error)
}
}
+4
View File
@@ -24,3 +24,7 @@ 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')
}
+17 -18
View File
@@ -1,3 +1,4 @@
const user = require('../../../middleware/user')
const ConsultancyEntry = require('../../../models/consultancy-entry')
const Domain = require('../../../models/domain')
const User = require('../../../models/user')
@@ -16,20 +17,6 @@ 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.listNewEntries = async (req, res) => {
const consultancyNewEntryList = await ConsultancyEntry.fetchAllNew()
const data = {}
data.consultancyNewEntryList = consultancyNewEntryList
res.send(data)
}
consultancy.sendPaginationData = async (req, res) => {
let requestType = req.query.type
const isAdminPage = req.query.isAdmin === 'true'
@@ -325,9 +312,12 @@ consultancy.publish = async (req, res) => {
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', {
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', {
@@ -342,11 +332,19 @@ 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)
@@ -365,6 +363,7 @@ consultancy.updateQuestion = async (req, res) => {
res.send({})
}
]
consultancy.insertNonModerator = async (req, res) => {
const questionId = req.body.question_id
@@ -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 })
}
+26 -15
View File
@@ -1,4 +1,5 @@
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')
@@ -69,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()
@@ -84,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()
@@ -124,12 +133,19 @@ dictionary.delete = async (req, res) => {
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
@@ -152,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 })
}
@@ -222,15 +242,6 @@ dictionary.listSecondaryDomainData = async (req, res) => {
})
}
dictionary.listSecondaryDomains = 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 Dictionary.fetchAllSecondaryDomains(resultsPerPage, page)
res.send({ page, numberOfAllPages, results })
}
dictionary.showImportFromFileForm = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const { dictionaryId } = req.params
@@ -252,7 +263,7 @@ dictionary.showExportToFileForm = async (req, res) => {
}
dictionary.importFromExtraction = async (req, res) => {
const { id: dictionaryId, extractionId } = req.params
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))
@@ -279,7 +290,7 @@ dictionary.importFromExtraction = async (req, res) => {
}
dictionary.exportBegin = async (req, res) => {
const dictionaryId = req.params.id
const { dictionaryId } = req.params
const exportParams = {
isValidFilter:
req.body.isValidFilter === 'on' ? undefined : req.body.isValidFilter,
+2 -7
View File
@@ -166,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,
@@ -177,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')
@@ -268,11 +268,6 @@ extraction.termCandidatesExport = async (req, res) => {
}
}
extraction.listFinishedForUser = async (req, res) => {
const extractions = await Extraction.fetchFinishedForUser(req.user.id)
res.send(extractions)
}
extraction.listTermCandidates = async (req, res) => {
const extractionId = req.params.id
const termCandidatesJson = await Extraction.fetchTermCandidatesJson(
+6 -2
View File
@@ -178,7 +178,8 @@ exports.listFilteredDictionaries = async (req, res) => {
hitsPerPage,
page,
orderAttribute,
orderIndex
orderIndex,
req.determinedLanguage
)
dictionaries = dictionaries.map(e => {
@@ -246,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
+15 -21
View File
@@ -46,11 +46,15 @@ consultancy.search = async (req, res) => {
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 !== '')
@@ -86,7 +90,9 @@ consultancy.specificQuestion = async (req, res) => {
}
consultancy.new = async (req, res) => {
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains(
req.determinedLanguage
)
res.locals.isOwnConsultancyEnabled =
(await getInstanceSetting('consultancy_type')) === 'own'
@@ -110,12 +116,9 @@ consultancyAdmin.new = async (req, res) => {
}
consultancyAdmin.users = async (req, res) => {
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
const users = await User.fetchConsultants()
res.render('pages/consultancy/admin/users', {
allPrimaryDomains,
users,
title: req.t('Svetovalci')
})
@@ -175,11 +178,7 @@ consultancyAdmin.inProgress = async (req, res) => {
}
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) => {
@@ -189,18 +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'
@@ -318,7 +310,9 @@ async function consultancyRequest(
) {
const searchString = req.query.q?.trim() ?? ''
let allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
let allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains(
req.determinedLanguage
)
/// filter selected domains for the prompt ///
// Not duplicates of this code arise...
-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
+68 -40
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')
@@ -19,6 +20,7 @@ const {
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`,
@@ -38,7 +40,10 @@ 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: req.t('Seznam slovarjev'),
@@ -47,13 +52,11 @@ dictionary.list = async (req, res) => {
}
dictionary.new = async (req, res) => {
// TODO Once english language is implemented, gather selected language (sl/en) from request ~ (cookies?)
const language = 'name_sl'
const [allPrimaryDomains, allSecondaryDomains, allLanguages] =
await Promise.all([
Dictionary.fetchAllPrimaryDomains(),
Dictionary.fetchAllPrimaryDomains(req.determinedLanguage),
Dictionary.fetchAllApprovedSecondaryDomains(),
Dictionary.fetchAllLanguages(language, true)
Dictionary.fetchAllLanguages(req.determinedLanguage, true)
])
res.render('pages/dictionaries/new', {
@@ -66,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) => {
@@ -77,7 +80,7 @@ dictionary.editDescription = async (req, res) => {
dictionary,
associatedSecondaryDomains
] = await Promise.all([
Dictionary.fetchAllPrimaryDomains(),
Dictionary.fetchAllPrimaryDomains(req.determinedLanguage),
Dictionary.fetchAllApprovedSecondaryDomains(),
Dictionary.fetchEditDescription(dictionaryId),
Dictionary.fetchSecondaryDomains(dictionaryId)
@@ -103,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'),
@@ -144,9 +147,9 @@ dictionary.updateUsers = async (req, res) => {
const newDictStatus = await determineNewStatus(isPublished)
// TODO I18n - nameSl
const { nameSl, status: oldDictStatus } = await Dictionary.fetchEditUsers(
dictionaryId
const { name, status: oldDictStatus } = await Dictionary.fetchEditUsers(
dictionaryId,
req.determinedLanguage
)
await Promise.all([
@@ -165,23 +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?)
// TODO I18n - nameSl
const language = 'name_sl'
const { dictionaryId } = req.params
const [dictionary, associatedLanguages, allLanguages] = await Promise.all([
Dictionary.fetchEditStructure(dictionaryId),
Dictionary.fetchLanguages(dictionaryId),
Dictionary.fetchAllLanguages(language, true)
Dictionary.fetchLanguages(dictionaryId, req.determinedLanguage),
Dictionary.fetchAllLanguages(req.determinedLanguage, true)
])
let viewPath
@@ -211,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':
@@ -243,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) {
@@ -269,7 +272,7 @@ dictionary.showImportFromFileForm = async (req, res) => {
const [{ pages_total: numberOfAllPages, results }, dictionaryName] =
await Promise.all([
Dictionary.fetchAllImports(dictionaryId, resultsPerPage, 1),
Dictionary.fetchName(dictionaryId)
Dictionary.fetchName(dictionaryId, req.determinedLanguage)
])
let viewPath
switch (req.baseUrl) {
@@ -293,7 +296,11 @@ 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: req.t('Seznam slovarjev'),
@@ -311,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),
@@ -357,15 +364,19 @@ 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) {
@@ -391,7 +402,7 @@ dictionary.showExportToFileForm = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const [dictionaryName, { pages_total: numberOfAllPages, results }] =
await Promise.all([
Dictionary.fetchName(dictionaryId),
Dictionary.fetchName(dictionaryId, req.determinedLanguage),
Dictionary.fetchExports(dictionaryId, resultsPerPage, 1)
])
let viewPath
@@ -418,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
@@ -445,16 +456,14 @@ 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)
])
@@ -464,7 +473,7 @@ dictionary.showContent = async (req, res) => {
title: req.t('Vsebina slovarja'),
terms,
canPublishEntriesInEdit,
dictionaryName,
dictionaryName: structure[`name${capitalize(req.determinedLanguage)}`],
structure,
languages,
dictionaryId,
@@ -510,7 +519,8 @@ dictionary.dictionaryList = async (req, res) => {
hitsPerPage,
page,
orderAttribute,
orderIndex
orderIndex,
req.determinedLanguage
)
const numberOfAllHits = parseInt(
@@ -518,7 +528,9 @@ dictionary.dictionaryList = async (req, res) => {
)
const numberOfAllPages = Math.ceil(numberOfAllHits / hitsPerPage)
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains(
req.determinedLanguage
)
/*
/// filter selected domains for the prompt ///
@@ -567,9 +579,12 @@ dictionary.dictionaryDetails = async (req, res) => {
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
@@ -711,12 +726,24 @@ dictionary.importFromFile = async (req, res) => {
}
}
dictionary.exportDownload = async (req, res) => {
// TODO Add authentication and authorization.
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 } =
await Dictionary.fetchExportDownloadMetadata(exportId)
req.exportDownloadMetadata
if (exportStatus !== 'finished') {
throw Error("Can't request file for unfinished export")
}
@@ -726,6 +753,7 @@ dictionary.exportDownload = async (req, res) => {
res.download(exportFilePath, exportFileName)
}
]
function importFileFilter(req, file, cb) {
if (file.mimetype !== 'text/xml') {
-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
+21 -6
View File
@@ -63,22 +63,36 @@ 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')
@@ -88,6 +102,7 @@ extraction.edit = async (req, res) => {
id: extractionId,
extraction,
allPrimaryDomains,
ossDocumentTypes,
stopTermsFiles
})
} else {
@@ -110,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) => {
+13 -19
View File
@@ -42,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.
@@ -58,9 +58,8 @@ exports.minEntriesRequirementCheckAndAct = {
if (!isBelowMinEntriesThreshold || wasEmailAlreadySent) return
// Prepare and send notification emails.
// TODO I18n - nameSl
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()
])
@@ -71,10 +70,9 @@ exports.minEntriesRequirementCheckAndAct = {
const type = 'delete'
const renderAsync = promisify(appRef.render.bind(appRef))
// TODO I18n - nameSl
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)
@@ -123,12 +121,11 @@ exports.determineNewStatus = async isPublished => {
// Exports actions related to checking and acting on dictionary status changes.
exports.statusChangeCheckAndAct = {
// Notify dictionaries admins by email on dictionary status changes.
// TODO I18n - nameSl
async updateUsers(
dictionaryId,
isPublishedNew,
oldDictStatus,
nameSl,
name,
appRef,
user
) {
@@ -139,12 +136,11 @@ exports.statusChangeCheckAndAct = {
const dictionariesAdminEmails =
await Dictionary.fetchDictionariesAdminEmails()
const type = 'unpublish'
// TODO I18n - nameSl
await renderAndSendStatusChangeEmails(
appRef,
type,
user.email,
nameSl,
name,
dictionariesAdminEmails
)
@@ -157,12 +153,11 @@ exports.statusChangeCheckAndAct = {
const type =
isApprovalRequired === 'T' ? 'publish-approval' : 'publish-no-approval'
// TODO I18n - nameSl
await renderAndSendStatusChangeEmails(
appRef,
type,
user.email,
nameSl,
name,
dictionariesAdminEmails
)
}
@@ -175,12 +170,12 @@ exports.statusChangeCheckAndAct = {
statusNew,
statusOld,
appRef,
determinedLanguage,
user
) {
if (statusOld === 'reviewed' && statusNew !== 'reviewed') {
// TODO I18n - nameSl
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'
@@ -189,7 +184,7 @@ exports.statusChangeCheckAndAct = {
appRef,
type,
user.email,
nameSl,
name,
adminEmails
)
}
@@ -197,19 +192,18 @@ exports.statusChangeCheckAndAct = {
}
// Helper function used by statusChangeCheckAndAct methods.
// TODO I18n - nameSl
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)
@@ -4,24 +4,23 @@ 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?)
// TODO i18n name_sl
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 = (
await Dictionary.fetchAllLanguages(language)
).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({
+55 -18
View File
@@ -1,3 +1,4 @@
const { promisify } = require('util')
const Entry = require('../models/entry')
const { getInstanceSetting } = require('../models/helpers')
const Dictionary = require('../models/dictionary')
@@ -13,10 +14,12 @@ const {
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) => {
@@ -27,12 +30,10 @@ exports.index = async (req, res) => {
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_${language}`)
@@ -58,7 +59,7 @@ 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 title = req.t('Iskanje')
@@ -68,7 +69,7 @@ exports.search = async (req, res) => {
targetLanguages,
allDictionaryNames,
portals
} = await SFDSuggestionImporter.initialize()
} = await SFDSuggestionImporter.initialize(req.determinedLanguage)
const filters = {
sourceLanguages: intoDbArray(req.query.sl, 'always'),
@@ -131,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.
@@ -328,7 +332,7 @@ exports.entryDetails = async (req, res) => {
// If external url, just redirect
if (entry.external_url) {
return res.redirect(entry.external_url)
return res.redirect(303, entry.external_url)
}
// unnecessary legacy assigment, refactor when time is available
@@ -340,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)
])
@@ -399,7 +406,7 @@ exports.entryDetails = async (req, res) => {
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,
@@ -438,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) {}
})
@@ -479,16 +488,44 @@ exports.changePassword = async (req, res) => {
}
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)
// console.log(token)
res.render('pages/reset-password/reset-password', {
// title: 'Pozabljeno geslo'
isValidToken: token === '123',
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', {
@@ -513,7 +550,7 @@ exports.changeUserLanguage = async (req, res) => {
req.session.language = languageCode
}
res.redirect('/')
res.redirect(303, '/')
}
function mergeDomains(
+6 -6
View File
@@ -17,7 +17,7 @@ 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) => {
@@ -33,7 +33,7 @@ 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) => {
@@ -48,7 +48,7 @@ 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) => {
@@ -80,7 +80,7 @@ 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) => {
@@ -101,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) => {
@@ -119,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) => {
+96 -17
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,10 +13,14 @@ const user = {}
user.register = async (req, res) => {
// TODO Add validation.
const userId = await User.create({
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
@@ -38,8 +43,9 @@ user.register = async (req, res) => {
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)
@@ -48,7 +54,11 @@ user.activateAccount = async (req, res) => {
delete req.session.language
}
res.redirect('/')
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) => {
@@ -89,20 +99,86 @@ user.login = async (req, res, next) => {
)(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.session.language = req.user.language
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.'
)
)
}
req.logout()
// Manually clear session.passport due to bug in current passport version.
delete req.session.passport.user
res.redirect('/')
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) => {
@@ -135,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) => {
@@ -144,6 +220,9 @@ 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: req.t('Uporabnik'),
userData,
@@ -156,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
+4 -5
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)
@@ -32,11 +35,7 @@ passport.use(
{ 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, {
+7
View File
@@ -2,6 +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()
}
+83 -5
View File
@@ -1,3 +1,5 @@
const User = require('../models/user')
const user = {}
user.enhance = (req, res, next) => {
@@ -18,7 +20,16 @@ user.isAuthenticated = (req, res, next) => {
if (req.isAuthenticated()) return next()
if (req.isAjax) return res.status(400).end()
res.redirect(req.baseUrl || '/')
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) => {
@@ -28,17 +39,37 @@ 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) => {
@@ -49,7 +80,54 @@ user.canContentEdit = (req, res, next) => {
if (isEditor || isPortalAdmin || isDictionariesAdmin) return next()
if (req.isAjax) return res.status(400).end()
res.redirect(req.baseUrl || '/')
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 -240
View File
@@ -1,10 +1,6 @@
const db = require('./db')
const debug = require('debug')('termPortal:models/comment')
const User = require('../models/user')
const {
portalAdminInitialEmail,
portalAdminInitialPassword
} = require('../config/keys')
const Entry = require('./entry')
// const debug = require('debug')('termPortal:models/comment')
class Comment {
// Deserialize flat data into an organized comment object.
@@ -84,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')
@@ -189,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 = `
@@ -197,237 +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 {
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 MOCK_ADMIN_BASE = 'admin'
const adminUser = {
username: MOCK_ADMIN_BASE,
firstName: MOCK_ADMIN_BASE,
lastName: MOCK_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})`
}
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
-51
View File
@@ -55,19 +55,6 @@ 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
@@ -162,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 *.
@@ -311,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
@@ -438,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;`
-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
+49 -75
View File
@@ -17,14 +17,18 @@ class Dictionary {
// TODO Add (language sensitive) dictionary status text conversion.
constructor({
id,
name,
name_sl: nameSl,
name_en: nameEn,
time_modified: timeModified,
status,
count_entries: countEntries,
count_comments: countComments
}) {
this.id = id
this.name = name
this.nameSl = nameSl
this.nameEn = nameEn
this.timeModified = timeModified
this.status = status
this.countEntries = countEntries
@@ -32,13 +36,12 @@ class Dictionary {
}
// Fetch all dictionaries from DB.
// TODO i18n name_sl
static async fetchAll() {
static async fetchAll(determinedLanguage) {
// TODO Implement SQL stored procedures or functions.
const { rows: fetchedDictionaries } = await db.query(`
SELECT
id,
name_sl,
name_${determinedLanguage} name,
time_modified,
status,
count_entries,
@@ -51,36 +54,13 @@ class Dictionary {
return deserializedDictionaries
}
/*
// Fetch all dictionaries from DB by it's ID.
static async fetchAllByUser(id) {
// TODO Implement SQL stored procedures or functions.
const text = `
SELECT
id,
name_sl,
time_modified,
status,
count_entries,
count_comments
FROM dictionary
WHERE id=$1
`
const values = [id]
const { rows: fetchedDictionaries } = await db.query(text, values)
const deserializedDictionary = new this(fetchedDictionaries[0])
return deserializedDictionary
}
*/
// TODO i18n name_sl
// Fetch all dictionaries from DB for which the user has at least one dictionary role.
static async fetchAllByUser(userId) {
static async fetchAllByUser(userId, determinedLanguage) {
// TODO Implement SQL stored procedures or functions.
const text = `
SELECT
id,
name_sl,
name_${determinedLanguage} name,
time_modified,
status,
count_entries,
@@ -107,16 +87,14 @@ class Dictionary {
}
// Fetch all primary domains from DB.
static async fetchAllPrimaryDomains() {
static async fetchAllPrimaryDomains(determinedLanguage) {
// TODO Implement SQL stored procedures or functions.
const { rows: fetchedDomains } = await db.query(`
SELECT id, name_sl, name_en
SELECT id, name_${determinedLanguage} name
FROM domain_primary
ORDER BY id`)
const deserializedDomains = fetchedDomains.map(domain =>
deserialize.primaryDomain(domain)
)
return deserializedDomains
ORDER BY name_${determinedLanguage}`)
return fetchedDomains
}
// Fetch primary domain for a specific dictionary from DB.
@@ -152,18 +130,16 @@ class Dictionary {
}
// Fetch all languages from DB.
static async fetchAllLanguages(lang, excludeSlovene) {
static async fetchAllLanguages(determinedLanguage, excludeSlovene) {
// TODO Implement SQL stored procedures or functions.
const { rows: fetchedLanguages } = await db.query(`
SELECT
id,
${lang}
name_${determinedLanguage} name
FROM language${excludeSlovene ? "\nWHERE code <> 'sl'" : ''}
ORDER BY ${lang}`)
const deserializedLanguages = fetchedLanguages.map(language =>
deserialize.language(language)
)
return deserializedLanguages
ORDER BY name_${determinedLanguage}`)
return fetchedLanguages
}
// Create new dictionary in DB and assign the creator as its admin.
@@ -304,11 +280,11 @@ class Dictionary {
}
// Fetch single dictionary data for editing users from DB.
static async fetchEditUsers(dictionaryId) {
static async fetchEditUsers(dictionaryId, determinedLanguage) {
const text = `
SELECT
id,
name_sl,
name_${determinedLanguage} name,
entries_have_terminology_review_flag,
entries_have_language_review_flag,
status
@@ -385,6 +361,7 @@ class Dictionary {
SELECT
id,
name_sl,
name_en,
entries_have_domain_labels,
entries_have_label,
entries_have_definition,
@@ -412,21 +389,19 @@ class Dictionary {
}
// Fetch single dictionary data for viewing details about it
static async fetchDictionaryBasicInfo(dictionaryId) {
const isInEnglish = false
const lang = isInEnglish ? 'd.name_en' : 'd.name_sl'
static async fetchDictionaryBasicInfo(dictionaryId, determinedLanguage) {
const text = `
SELECT
d.id,
${lang} dictionarysl,
d.name_${determinedLanguage} dictionarysl,
d.count_entries,
to_char(d.time_modified,'YYYY-MM-DD') time_modified,
d.issn,
d.author,
dp.name_sl domain_primary,
dp.name_${determinedLanguage} domain_primary,
description,
l.name_sl languageSl,
ds.name_sl domainSecondarySl,
l.name_${determinedLanguage} languageSl,
ds.name_${determinedLanguage} domainSecondarySl,
lp.name portalnamesl,
lp.code portalcode
FROM
@@ -551,7 +526,8 @@ class Dictionary {
resultsPerPage,
page,
orderType,
orderIndex
orderIndex,
determinedLanguage
) {
let queryAppend = ''
if (domainQuery.length > 0) {
@@ -580,22 +556,21 @@ class Dictionary {
}
}
const isInEnglish = false
const lang = isInEnglish ? 'name_en' : 'name_sl'
const orderBy = `${orderType}.${lang} ${orderIndex ? 'DESC' : 'ASC'}`
const orderBy = `${orderType}.name_${determinedLanguage} ${
orderIndex ? 'DESC' : 'ASC'
}`
// TODO domain primary language toggle!
const text = `
SELECT
distinct (d.id),
d.${lang} dictionarysl,
d.name_${determinedLanguage} dictionarysl,
d.count_entries,
d.count_comments,
to_char(d.time_modified,'YYYY-MM-DD') time_modified,
d.issn,
d.author,
dp.name_sl domain_primary,
dp.name_${determinedLanguage} domain_primary,
description,
lp.name portalnamesl,
lp.code portalcode
@@ -605,7 +580,7 @@ class Dictionary {
INNER JOIN domain_primary dp ON d.domain_primary_id = dp.id
LEFT JOIN linked_dictionary ld ON ld.target_dictionary_id = d.id
LEFT JOIN linked_portal lp ON ld.linked_portal_id = lp.id
WHERE d.status = 'published' AND LOWER(d.name_sl) LIKE '%' || LOWER($1) || '%' ${queryAppend}
WHERE d.status = 'published' AND LOWER(d.name_${determinedLanguage}) LIKE '%' || LOWER($1) || '%' ${queryAppend}
ORDER BY ${orderBy}
LIMIT $2
OFFSET $3`
@@ -737,13 +712,12 @@ class Dictionary {
}
// Fetch languages associated with a single dictionary from DB.
static async fetchLanguages(dictionaryId) {
static async fetchLanguages(dictionaryId, determinedLanguage) {
const text = `
SELECT
l.id,
l.code,
l.name_sl,
l.name_en
l.name_${determinedLanguage} name
FROM dictionary d
INNER JOIN dictionary_language dl ON d.id = dl.dictionary_id
INNER JOIN language l ON dl.language_id = l.id
@@ -753,20 +727,16 @@ class Dictionary {
const { rows: fetchedLanguages } = await db.query(text, value)
const deserializedLanguages = fetchedLanguages.map(language =>
deserialize.language(language)
)
return deserializedLanguages
return fetchedLanguages
}
// Fetch latest 3 dictionaries by publish date
static async fetchLatest3DictsByPublishDate(isInEnglish) {
const lang = isInEnglish ? 'd.name_en' : 'd.name_sl'
static async fetchLatest3DictsByPublishDate(determinedLanguage) {
const text = `
SELECT
d.id,
${lang} dictionarysl,
dp.name_sl domain_primary,
d.name_${determinedLanguage} dictionarysl,
dp.name_${determinedLanguage} domain_primary,
d.count_comments
FROM dictionary d
INNER JOIN domain_primary dp ON d.domain_primary_id = dp.id
@@ -842,7 +812,11 @@ class Dictionary {
await db.query(text, values)
}
static async fetchAllAdminDictionaries(resultsPerPage, page) {
static async fetchAllAdminDictionaries(
determinedLanguage,
resultsPerPage,
page
) {
// TODO Implement SQL stored procedures or functions.
const {
rows: [{ result }]
@@ -856,7 +830,7 @@ class Dictionary {
'results', ARRAY(
SELECT jsonb_build_object(
'id', id,
'name', name_sl,
'name', name_${determinedLanguage},
'timeCreated', time_created,
'timeModified', time_modified,
'status', status
@@ -1162,12 +1136,12 @@ class Dictionary {
}
// Fetch single dictionary's name from DB.
static async fetchName(dictionaryId) {
static async fetchName(dictionaryId, determinedLanguage) {
const { rows } = await db.query(
'SELECT name_sl FROM dictionary WHERE id = $1',
`SELECT name_${determinedLanguage} name FROM dictionary WHERE id = $1`,
[dictionaryId]
)
const dictionaryName = rows[0].name_sl
const dictionaryName = rows[0].name
return dictionaryName
}
+47 -93
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 = `
@@ -565,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')
@@ -573,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
@@ -591,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)})`
+109 -14
View File
@@ -68,13 +68,22 @@ 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 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 {
@@ -249,13 +258,17 @@ Extraction.processOwn = async function (extractionId, extractionName) {
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(
return await axios.post(
`${extractionApiOrigin}/datotekaVConlluAsync`,
form,
{
@@ -265,6 +278,10 @@ Extraction.processOwn = async function (extractionId, extractionName) {
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, time_started = NOW() WHERE extraction_id = $2 AND job_type = $3 AND filename = $4",
@@ -276,9 +293,14 @@ Extraction.processOwn = async function (extractionId, extractionName) {
// Poll job until finished.
while (true) {
await sleep(5)
const { data: data2 } = await axios.get(
`${extractionApiOrigin}/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(
@@ -326,19 +348,26 @@ 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(
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, time_started = NOW() WHERE extraction_id = $2 AND job_type = $3 AND filename = $4",
@@ -348,8 +377,12 @@ Extraction.processOwn = async function (extractionId, extractionName) {
// Poll job until finished.
while (true) {
await sleep(5)
const { data: data4 } = await axios.get(
`${extractionApiOrigin}/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) {
const { job_result: jobResult } = data4
@@ -395,6 +428,11 @@ Extraction.processOwn = async function (extractionId, extractionName) {
title: extractionName
})
await db.query('UPDATE extraction SET corpus_id = $1 WHERE id = $2', [
corpusId,
extractionId
])
// Wait for creation of corpus.
while (true) {
console.log('SLEEP FOR 5 SECS')
@@ -484,8 +522,8 @@ Extraction.processOwn = async function (extractionId, extractionName) {
)
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 (error) {
@@ -503,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
@@ -532,7 +572,13 @@ Extraction.processOss = async function (extractionId, ossParams) {
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, time_started = NOW() WHERE extraction_id = $2 AND job_type = $3 AND filename = $4",
@@ -542,8 +588,12 @@ Extraction.processOss = async function (extractionId, ossParams) {
// Poll job until finished.
while (true) {
await sleep(5)
const { data: data2 } = await axios.get(
`${extractionApiOrigin}/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 (
@@ -618,4 +668,49 @@ function logExtractionError(error, extractionId, jobType, filename) {
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
@@ -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 || ''}" 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))
}
+89 -11
View File
@@ -1,3 +1,4 @@
const xss = require('xss')
const { removeHtmlTags } = require('../../helpers')
const { searchEngineClient, ENTRY_INDEX } = require('../../search-engine')
const { DATA_FILES_PATH } = require('../../../config/settings')
@@ -6,7 +7,7 @@ exports.deserialize = {
dictionary(dictionary) {
const deserializedDictionary = {
id: dictionary.id,
nameSl: dictionary.name_sl,
name: dictionary.name,
timeModified: dictionary.time_modified,
status: dictionary.status,
countEntries: dictionary.count_entries,
@@ -48,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 = {
@@ -77,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
@@ -90,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,
@@ -275,3 +277,79 @@ exports.prepareEntryForIndexing = prepareEntryForIndexing
exports.getExportFilesPath = dictId => {
return `${DATA_FILES_PATH}/dict_export/${dictId}`
}
const markupFilter = {
noMixed: new xss.FilterXSS({
whiteList: {},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style']
}),
mixedBasic: new xss.FilterXSS({
whiteList: {
sup: [],
sub: []
},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style']
}),
mixedExtended: new xss.FilterXSS({
whiteList: {
sup: [],
sub: [],
b: [],
i: [],
a: ['href']
},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style'],
onTag: customTagHandler
}),
mixedOther: new xss.FilterXSS({
whiteList: {
sup: [],
sub: [],
b: [],
i: [],
a: ['href'],
br: []
},
stripIgnoreTag: true,
stripIgnoreTagBody: ['script', 'style'],
onTag: customTagHandler
})
}
function customTagHandler(tag, html, { isWhite, isClosing }) {
// Special treatment only for whitelisted opening anchor tags.
if (tag !== 'a' || !isWhite || isClosing) return
const matchUrl = html.match(/href="?(?<url>https?:\/\/.*?)"?[\s>]/)
const url = matchUrl ? xss.escapeAttrValue(matchUrl.groups.url) : undefined
return `<a href="${url || ''}" target="_blank">`
}
function sanitize(string, filter) {
return filter.process(string).replace(/\s+/g, ' ').trim()
}
exports.sanitizeField = {
toText(string) {
return sanitize(string, markupFilter.noMixed)
},
toMixedBasic(string) {
return sanitize(string, markupFilter.mixedBasic)
},
toMixedExtended(string) {
return sanitize(string, markupFilter.mixedExtended)
},
toMixedOther(string) {
return sanitize(string, markupFilter.mixedOther)
}
}
+1
View File
@@ -9,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,
+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 = {
+1
View File
@@ -6,6 +6,7 @@ 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,
+5 -4
View File
@@ -455,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)
)
+313 -37
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 = [
@@ -19,7 +66,19 @@ User.create = async user => {
]
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,
@@ -29,6 +88,7 @@ User.create = async user => {
)
VALUES (${db.genParamStr(values)})
RETURNING id`
}
const { rows } = await db.query(text, values)
@@ -45,34 +105,40 @@ 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
return user
throw err
}
// Activate user account.
User.activateAccount = async user => {
await db.query(
`UPDATE "user" SET status = 'active', time_activated = NOW() WHERE id = $1`,
[user.id]
)
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
}
// Generate a user remember me token.
@@ -96,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 = `
@@ -105,6 +295,7 @@ User.fetchDeserializedDataById = async userId => {
u.first_name,
u.last_name,
u.email,
u.status,
u.hits_per_page,
u.language,
ARRAY(
@@ -148,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(
@@ -157,6 +349,7 @@ User.fetchAll = async (resultsPerPage, page) => {
'status', status
)
FROM "user"
WHERE status <> 'closed'
ORDER BY username
LIMIT $1
OFFSET $2
@@ -281,8 +474,12 @@ User.updateUser = async (userId, payload) => {
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'
@@ -293,6 +490,7 @@ User.updateUser = async (userId, payload) => {
first_name = $3,
last_name = $4,
status = $5
${setTimeActivated ? ', time_activated = NOW()' : ''}
WHERE id = $1`
const values = [
@@ -448,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
@@ -471,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) => {
@@ -497,4 +694,83 @@ User.updateLanguage = async (userId, languageCode) => {
])
}
// 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
+14
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",
@@ -794,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",
@@ -4418,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",
+1
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",
@@ -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

+81 -18
View File
@@ -1,4 +1,4 @@
/* global $, axios, bootstrap, currentPagePath, initPagination, removeAllChildNodes, unsavedData, replaceContainer, i18next */
/* global $, axios, bootstrap, currentPagePath, initPagination, removeAllChildNodes, unsavedData, replaceContainer, isI18nReady, i18next, validator */
// const currentPagePath = location.pathname
@@ -144,6 +144,7 @@ function initAdmin() {
}
if (currentPagePath === '/admin/uporabniki/seznam') {
isI18nReady.then(t => {
const resultsListEl = document.getElementById('page-results')
const updatePager = initPagination('pagination', onPageChange)
@@ -186,7 +187,7 @@ 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`
@@ -200,6 +201,7 @@ function initAdmin() {
resultsListEl.appendChild(rowEl)
})
}
})
}
if (currentPagePath === '/admin/slovarji') {
@@ -280,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) {
@@ -946,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) {
@@ -966,12 +972,14 @@ function mobileMoveContent() {
primaryButton.style.whiteSpace = ''
}
if (navTitle) {
if (
currentPagePath.includes('slovarji') &&
!currentPagePath.includes('admin')
)
navTitle.textContent = i18next.t('Urejanje')
else navTitle.textContent = i18next.t('Administracija')
}
siteHeading.style.display = 'block'
}
}
@@ -1180,7 +1188,7 @@ $('.summernote').summernote({
const profileForm = document.getElementById('profileForm')
if (profileForm) {
profileForm.addEventListener('change', e => {
profileForm.addEventListener('input', e => {
enableButton()
})
@@ -1189,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)
@@ -1204,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 {
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')
}
}
@@ -1,188 +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.
// UPDATE: Sedaj sprejme tudi array idjev, če je pager kontrolerjev več (npr. en zgoraj (#pagination-top), en spodaj (#pagination-bottom)).
// 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-top', 'pagination-bottom'],
onPageChange
)
// Za samo en kontroler je bilo:
// 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(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')
const btnNextPage = rootEl.querySelector('.next-page')
const btnLastPage = rootEl.querySelector('.last-page')
const formEl = rootEl.querySelector('form')
const pageInputEl = formEl.querySelector('input')
const pagesCountDisplayEl = formEl.querySelector('.pages-total')
numOfAllPages = +pagesCountDisplayEl.textContent
backwardBtnEls.push(btnFirstPage, btnPreviousPage)
forwardBtnEls.push(btnNextPage, btnLastPage)
pageInputEls.push(pageInputEl)
pagesCountDisplayEls.push(pagesCountDisplayEl)
rootEl.addEventListener('click', e => {
handleButtonClick(e, paginationRootElId)
})
formEl.addEventListener('submit', e => handleFormSubmit(e, pageInputEl))
}
function handleButtonClick({ target }, paginationRootElId) {
if (reqLock) return
const buttonEl = target.closest(`#${paginationRootElId} button`)
if (!buttonEl) return
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, pageInputEl) {
e.preventDefault()
if (reqLock) return
const inputValue = +pageInputEl.value
if (!(inputValue > 0 && inputValue <= numOfAllPages)) {
alert('Nepravilna vrednost strani')
pageInputEl.value = currentPage
return
}
enableLock()
onPageChange(inputValue)
}
function enableLock() {
reqLock = true
allControlEls.forEach(el => (el.disabled = true))
}
function disableLock() {
reqLock = false
allControlEls.forEach(el => (el.disabled = false))
}
function updatePagerUi(newCurrentPage, newNumOfAllPages) {
disableLock()
if (!newCurrentPage) return
currentPage = newCurrentPage
pageInputEls.forEach(el => (el.value = newCurrentPage))
pagesCountDisplayEls.forEach(el => (el.textContent = newNumOfAllPages))
if (newCurrentPage === 1) {
backwardBtnEls.forEach(el => (el.disabled = true))
} else {
backwardBtnEls.forEach(el => (el.disabled = false))
}
if (newCurrentPage === newNumOfAllPages) {
forwardBtnEls.forEach(el => (el.disabled = true))
} else {
forwardBtnEls.forEach(el => (el.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)
}
}
+17 -8
View File
@@ -845,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
@@ -863,6 +863,7 @@ function initDictionaries() {
})
}
}
}
})
}
})
@@ -2035,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()
}
@@ -2110,7 +2113,7 @@ function changeVersionList(versions) {
// eslint-disable-next-line
new bootstrap.Tooltip(dateLabel, {
title:
i18next.t('Verzija') +
i18next.t('Verzija:') +
` ${el.version} <br>` +
i18next.t('Avtor:') +
` ${el.version_author}`
@@ -2823,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) {
@@ -62,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')
@@ -197,7 +197,7 @@ function updateFileListEl(fileListEl, { status, fileStats, index }) {
deleteImgEl.alt = ''
const deleteSpanEl = document.createElement('span')
deleteSpanEl.className = 'ms-2'
deleteSpanEl.textContent = 'Briši'
deleteSpanEl.textContent = i18next.t('Briši')
const deleteButtonEl = document.createElement('button')
deleteButtonEl.className = 'p-0 delete-file delete-btn-table'
deleteButtonEl.append(deleteImgEl, deleteSpanEl)
@@ -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)
}
}
+25 -2
View File
@@ -1,11 +1,34 @@
/* global isI18nReady */
/* global isI18nReady, i18next, $, axios */
isI18nReady.then(t => {
const alertText = document.querySelector('#alert-text')
alertText.textContent = t(
'Ali želite zbrisati ta profil. S tem bodo izbrisani vsi podatki, ki ste jih ustvarili'
'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)
}
})
+41 -34
View File
@@ -1,59 +1,66 @@
/* global $, axios, i18next */
/* 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')
// 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', event => {
.addEventListener('submit', async event => {
event.preventDefault()
if (verifyPassword()) {
const token = document.getElementById('token').value
const password = document.getElementById('reset-password').value
const repeatPassword = document.getElementById(
const passwordRepeat = document.getElementById(
'reset-password-repeat'
).value
const token = document.getElementById('token').value
// const email = document.getElementById('email').value
// const data = { password, repeatPassword, token, email }
axios
.post('/api/v1/users/reset-passwordPLACEHOLDER_URL', {
try {
await axios.post('/api/v1/users/reset-password-submit', {
token,
password,
repeatPassword,
token
passwordRepeat
})
.then(response => {
if (response.data.success) {
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')
// window.location = '/'
} else {
alert(response.data.message)
}
})
.catch(error => {
// Very unlikely, but can happen
alert(i18next.t('Napaka na strežniku'))
console.log(error)
// DEBUG SUCCESS DUE TO NO ENDPOINT $('#reset-pass-info').modal('show') <<- REMOVE
})
}
})
// Handle cancel event
document.querySelector('#cancel-btn').addEventListener('click', event => {
event.preventDefault()
window.location = '/'
})
// Handle redirect on success
document.querySelector('#modal-fp-info-close').addEventListener('click', () => {
window.location = '/'
})
+29 -49
View File
@@ -953,13 +953,15 @@ if (registerSwithcButton) {
$(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', 'Jezik iskanja')
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(
@@ -991,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', 'Jezik iskanja')
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 */
@@ -1192,7 +1152,7 @@ const forgottenPasswordInvoker = new StateManagerInvoker(stateManager, () => {
}
function apllyDescriptionAccordingToState() {
$('#alert-text').text(stateManager.getState().fpassWindowDescription)
$('#fpi-text').text(stateManager.getState().fpassWindowDescription)
}
closeAllFPRelatedModals()
@@ -1233,8 +1193,21 @@ function onCancelForgotPassword() {
}
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('/REPLACETHISDUMMYURL', {
.post('/api/v1/users/reset-password-init', {
usernameOrEmail: document.getElementById('forgot-pass-input').value
})
.then(res => {
@@ -1253,11 +1226,18 @@ function onSendForgottenEmailRequest() {
})
})
.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
})
+4 -2
View File
@@ -1,7 +1,7 @@
/* global $, axios, currentPagePath, initPagination,
removeAllChildNodes, transferText, tooltipTriggerList,
tooltipList, createTooltip, resetTooltipTriggerList,
transferTextExtended, i18next */
transferTextExtended, isI18nReady, i18next */
// position correction functions
@@ -278,10 +278,11 @@ window.addEventListener('load', () => {
$('[data-toggle="tooltip"]').tooltip()
})
isI18nReady.then(t => {
function handleProperTextDisplay() {
// const BROWSER_UNUSUAL_OFFSET = 17
if (/\/iskanje/.test(currentPagePath)) {
transferText('Iskanje po slovarjih', true, 'site-heading') //,
transferText(t('Iskanje po slovarjih'), true, 'site-heading') //,
// BROWSER_UNUSUAL_OFFSET
// )
} else if (/\/termin/.test(currentPagePath)) {
@@ -295,3 +296,4 @@ window.addEventListener('resize', () => {
})
handleProperTextDisplay()
})
+35 -5
View File
@@ -222,7 +222,7 @@
"Modul za svetovanje pri terminoloških zagatah.": "Terminology consulting module.",
"Modul za urejanje terminoloških slovarjev.": "Module for editing terminology dictionaries.",
"MODULI": "MODULES",
"Moj Profil": "My Profile",
"Moj Račun": "My Account",
"Moji slovarji": "My dictionaries",
"Morebitne že obstoječe poimenovalne rešitve, če obstajajo.": "Any already existing naming solutions.",
"Morebitni primeri rabe termina v besedilih ali povezave do njih, če obstajajo.": "Any examples of term use in texts or links to text where the term is used.",
@@ -234,7 +234,7 @@
"Na tem mestu lahko določite področne oznake, če želite posamezne termine v svojem terminološkem slovarju razvrstiti še podrobneje.": "Here you can define domain labels, if you want to classify individual terms in your terminology dictionary in more detail.",
"Na tem mestu so zbrani vsi komentarji, povezani s terminološkim portalom.": "All comments related to the terminology portal can be found here.",
"Na tem mestu so zbrani vsi komentarji, povezani z izbranim terminološkim slovarjem.": "All comments related to the selected terminology dictionary can be found here.",
"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 ": "we a message with a link to verify your Termonology Portal user account to your e-mail address. The verification link is valid.",
"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 ": "we have sent a message with a link to verify your Termonology Portal user account to your e-mail address. The verification link will be valid ",
"Na vaš elektronski naslov smo vam posredovali povezavo za ponastavitev gesla. Prosimo preverite svoj elektronski predal.": "We sent a password reset link to your e-mail address. Please, check your inbox.",
"Nabor vseh luščenj, s katerimi lahko uporabnik iz izbranih besedil pridobi sezname terminoloških kandidatov. Posamezne terminološke kandidate lahko preveri tudi v konkordančniku.": "All extractions the users can use to obtain a list of term candidates from the selected texts. The user can also check individual term candidates using the concordance tool.",
"Nabor vseh luščenj, s pomočjo katerih uporabnik iz vhodnih besedil pridobi sezname terminoloških kandidatov in dostop do konkordančnika po teh besedilih.": "All extractions the users can use to obtain a list of term candidates from the selected texts and access to the concordance tools for these texts.",
@@ -407,7 +407,7 @@
"POVEZANI TERMIN": "RELATED TERM",
"POVEZANI TERMIN:": "RELATED TERM:",
"POVEZAVA": "LINK",
"Povezava ni (več) veljavna. Prosimo, da ponovno zahtevate ponastavitev gesla.": "The link is (no longer) valid. Please, request new password reset link.",
"Povezava ni (več) veljavna. Prosimo, da ponovno zahtevate ponastavitev gesla.": "This link is invalid or expired. Please, request new password reset link.",
"Povezave": "Links",
"Povezave s portali": "Links to other portals",
"Pozabljeno geslo": "Forgoten password",
@@ -540,7 +540,7 @@
"Strokovni pregled": "Terminology review",
"Strokovno pregledano": "Terminologically reviewed",
"Struktura": "Structure",
"STRUKTURA - ELEMENTI SLOVARSKEGA SESTAVKA:": "STRUCTURE - ELEMENTS OF THA DICTIONARY ENTRY:",
"STRUKTURA - ELEMENTI SLOVARSKEGA SESTAVKA:": "STRUCTURE - ELEMENTS OF THE DICTIONARY ENTRY:",
"Struktura slovarskega sestavka": "The structure of the dictionary entry",
"Svetovalci": "Consultants",
"Svetovalec": "Consultant",
@@ -706,5 +706,35 @@
"ZVOK": "AUDIO",
"ZVOK:": "AUDIO:",
"titleTermsOfUse": "Terms of Use",
"titlePrivacyPolicy": "Privacy Policy"
"titlePrivacyPolicy": "Privacy Policy",
"userStatusregistered": "registered",
"userStatusactive": "active",
"userStatusinactive": "inactive",
"userStatusclosed": "closed",
"Nepravilno uporabniško ime ali elektronski naslov.": "Invalid user name or e-mail address.",
"Ponastavitev gesla": "Password reset",
"Uspešna ponastavitev gesla": "Password successfully reset",
"Vaše geslo je bilo uspešno ponastavljeno.": "Your password has been successfully reset.",
"Prosimo vnesite vaš elektronski naslov.": "Please, enter your e-mail address.",
"Sprememba elektronskega naslova": "Change e-mail address",
"Na vaš elektronski naslov smo vam poslali sporočilo s povezavo, s katero boste potrdili menjavo elektronskega naslova. Povezava za potrditev je veljavna ": "We have sent a message with a link to confirm the change of your e-mail address to your new e-mail adress. The verification link will be valid ",
"Sprememba elektronskega naslova - uspeh": "E-mail address change successful",
"Uspešno ste spremenili svoj elektronski naslov.": "You have successfully changed your e-mail address.",
"Elektronski naslov uporablja že drug uporabnik.": "This e-mail address is already used by another user.",
"Povezava ni (več) veljavna. Elektronski naslov ni bil spremenjen.": "This link is invalid or expired. The e-mail address has not been changed.",
"Vaš uporabniški račun je bil uspešno izbrisan.": "Your user account has been successfully deleted.",
"Ali res želite izbrisati svoj račun? S tem boste trajno izgubili dostop do podatkov, ki ste jih ustvarili.": "Do you really want to delete your account? If you delete your account, you will permanently lose access to all data you created.",
"Uspešno ste aktivirali svoj uporabniški račun in se prijavili.": "You have successfully activated your user account and signed in.",
"Povezava ni (več) veljavna. Prosimo, da se ponovno registrirate.": "This link is invalid or expired. Please, register again.",
"Nepravilno staro geslo.": "Invalid current password.",
"Sprememba gesla": "Change password",
"Izbrano uporabniško ime uporablja že drug uporabnik.": "This user name is already taken.",
"Izpolnite vsa prazna polja.": "Fill-in all empty fields.",
"Jezik iskanja": "Search language",
"Ciljni jezik": "Target language",
"Spremenjen": "Changed",
"Število slovarskih sestavkov": "Number of dictionary entries",
"VPRAŠANJE": "QUESTION",
"MNENJE": "OPINION",
"ANGLEŠKI PREVOD": "ENGLISH TRANSLATION"
}
+32 -2
View File
@@ -222,7 +222,7 @@
"Modul za svetovanje pri terminoloških zagatah.": "Modul za svetovanje pri terminoloških zagatah.",
"Modul za urejanje terminoloških slovarjev.": "Modul za urejanje terminoloških slovarjev.",
"MODULI": "MODULI",
"Moj Profil": "Moj Profil",
"Moj Račun": "Moj Račun",
"Moji slovarji": "Moji slovarji",
"Morebitne že obstoječe poimenovalne rešitve, če obstajajo.": "Morebitne že obstoječe poimenovalne rešitve, če obstajajo.",
"Morebitni primeri rabe termina v besedilih ali povezave do njih, če obstajajo.": "Morebitni primeri rabe termina v besedilih ali povezave do njih, če obstajajo.",
@@ -706,5 +706,35 @@
"ZVOK": "ZVOK",
"ZVOK:": "ZVOK:",
"titleTermsOfUse": "Pogoji uporabe",
"titlePrivacyPolicy": "Politika zasebnosti"
"titlePrivacyPolicy": "Politika zasebnosti",
"userStatusregistered": "registriran",
"userStatusactive": "aktiven",
"userStatusinactive": "neaktiven",
"userStatusclosed": "zaprt",
"Nepravilno uporabniško ime ali elektronski naslov.": "Nepravilno uporabniško ime ali elektronski naslov.",
"Ponastavitev gesla": "Ponastavitev gesla",
"Uspešna ponastavitev gesla": "Uspešna ponastavitev gesla",
"Vaše geslo je bilo uspešno ponastavljeno.": "Vaše geslo je bilo uspešno ponastavljeno.",
"Prosimo vnesite vaš elektronski naslov.": "Prosimo vnesite vaš elektronski naslov.",
"Sprememba elektronskega naslova": "Sprememba elektronskega naslova",
"Na vaš elektronski naslov smo vam poslali sporočilo s povezavo, s katero boste potrdili menjavo elektronskega naslova. Povezava za potrditev je veljavna ": "Na vaš elektronski naslov smo vam poslali sporočilo s povezavo, s katero boste potrdili menjavo elektronskega naslova. Povezava za potrditev je veljavna ",
"Sprememba elektronskega naslova - uspeh": "Sprememba elektronskega naslova - uspeh",
"Uspešno ste spremenili svoj elektronski naslov.": "Uspešno ste spremenili svoj elektronski naslov.",
"Elektronski naslov uporablja že drug uporabnik.": "Elektronski naslov uporablja že drug uporabnik.",
"Povezava ni (več) veljavna. Elektronski naslov ni bil spremenjen.": "Povezava ni (več) veljavna. Elektronski naslov ni bil spremenjen.",
"Vaš uporabniški račun je bil uspešno izbrisan.": "Vaš uporabniški račun je bil uspešno izbrisan.",
"Ali res želite izbrisati svoj račun? S tem boste trajno izgubili dostop do podatkov, ki ste jih ustvarili.": "Ali res želite izbrisati svoj račun? S tem boste trajno izgubili dostop do podatkov, ki ste jih ustvarili.",
"Uspešno ste aktivirali svoj uporabniški račun in se prijavili.": "Uspešno ste aktivirali svoj uporabniški račun in se prijavili.",
"Povezava ni (več) veljavna. Prosimo, da se ponovno registrirate.": "Povezava ni (več) veljavna. Prosimo, da se ponovno registrirate.",
"Nepravilno staro geslo.": "Nepravilno staro geslo.",
"Sprememba gesla": "Sprememba gesla",
"Izbrano uporabniško ime uporablja že drug uporabnik.": "Izbrano uporabniško ime uporablja že drug uporabnik.",
"Izpolnite vsa prazna polja.": "Izpolnite vsa prazna polja.",
"Jezik iskanja": "Jezik iskanja",
"Ciljni jezik": "Ciljni jezik",
"Spremenjen": "Spremenjen",
"Število slovarskih sestavkov": "Število slovarskih sestavkov",
"VPRAŠANJE": "VPRAŠANJE",
"MNENJE": "MNENJE",
"ANGLEŠKI PREVOD": "ANGLEŠKI PREVOD"
}
+9 -9
View File
@@ -6,18 +6,18 @@ const portal = require('../controllers/portals')
// All routes require an authenticated user.
router.use((req, res, next) => {
if (req.isAuthenticated()) return next()
res.redirect('/')
res.redirect(303, '/')
})
// Menu entry point. Redirect based on role.
router.get('/', (req, res, next) => {
const { user, baseUrl } = req
if (user.hasRole('portal admin')) {
return res.redirect(`${baseUrl}/nastavitve/portal`)
return res.redirect(303, `${baseUrl}/nastavitve/portal`)
} else if (user.hasRole('dictionaries admin')) {
return res.redirect(`${baseUrl}/slovarji`)
return res.redirect(303, `${baseUrl}/slovarji`)
}
res.redirect('/')
res.redirect(303, '/')
})
// Authorize endpoints for portal admin.
@@ -25,14 +25,14 @@ router.use(
['/nastavitve', '/povezave', '/uporabniki', '/komentarji'],
(req, res, next) => {
if (req.user.hasRole('portal admin')) return next()
res.redirect('/')
res.redirect(303, '/')
}
)
// Authorize endpoints for dictionaries admin.
router.use(['/slovarji', '/podrocja'], (req, res, next) => {
router.use(['/slovarji', '/podpodrocja'], (req, res, next) => {
if (req.user.hasRole('dictionaries admin')) return next()
res.redirect('/')
res.redirect(303, '/')
})
router.get('/nastavitve/portal', portal.instanceSettings)
@@ -59,9 +59,9 @@ router.get('/povezave/seznam/:portalId', portal.fetchSelectedLinkedDictionaries)
router.post('/povezave/seznam/:portalId', portal.updateSelectedDictionaries)
router.get('/povezava/:portalId/urejanje', portal.fetchPortal)
router.get('/povezave/:portalId/urejanje', portal.fetchPortal)
router.post('/povezava/:portalId/urejanje', portal.updatePortal)
router.post('/povezave/:portalId/urejanje', portal.updatePortal)
// Show a list of user's dictionaries.
router.get('/slovarji', dictionary.listAdminDictionaries)
-8
View File
@@ -3,20 +3,12 @@ const router = require('express-promise-router')()
const {
listComments,
createComment,
seedComments,
clearComments,
updateStatus
} = require('../../../controllers/api/v1/comments')
// List comments (one page).
router.get('/', listComments)
// TEMP - Seed comments.
router.get('/seed/:commentCount', seedComments)
// TEMP - Clear comments.
router.get('/clear', clearComments)
// All further routes are only available to authenticated users.
router.use((req, res, next) => {
if (req.isAuthenticated()) return next()
-6
View File
@@ -2,8 +2,6 @@ const router = require('express-promise-router')()
const {
assign,
listEntries,
listNewEntries,
createQuestion,
deleteQuestion,
updateDomain,
@@ -23,10 +21,6 @@ const {
router.get('/entry-pagination', sendPaginationData)
router.get('/entry', listEntries)
router.get('/new-entry', listNewEntries)
// All routes require an authenticated user.
router.use((req, res, next) => {
if (req.isAuthenticated()) return next()
-7
View File
@@ -1,7 +0,0 @@
const router = require('express-promise-router')()
const demoPaginacija = require('../../../controllers/api/v1/demo-paginacija')
// Tale endpoint vrača rezultate.
router.get('/list', demoPaginacija.list)
module.exports = router
+50 -13
View File
@@ -1,54 +1,91 @@
const router = require('express-promise-router')()
const dictionaries = require('../../../controllers/api/v1/dictionaries')
const user = require('../../../middleware/user')
// Update domain labels.
router.post('/update-domain-labels', dictionaries.updateDomainLabels)
// Update secondary domains
router.post('/renovateSecondaryDomains', dictionaries.renovateSecondaryDomains)
router.post(
'/renovateSecondaryDomains',
user.canAdministrateDictionaries,
dictionaries.renovateSecondaryDomains
)
// Delete specific dictionary.
router.delete('/:dictionaryId', dictionaries.delete)
router.delete(
'/:dictionaryId',
user.canAdministrateDictionary,
dictionaries.delete
)
// Change page in pagination
router.get('/listAllDictionaries', dictionaries.listDictionaries)
router.get(
'/listAllDictionaries',
user.canAdministrateDictionaries,
dictionaries.listDictionaries
)
// Change page in pagination
router.get('/:dictionaryId/listDomainLabels', dictionaries.listDomainLabels)
// Change page in pagination
router.get('/listSecondaryDomains', dictionaries.listSecondaryDomains)
router.get(
'/:dictionaryId/listDomainLabels',
user.canAdministrateDictionary,
dictionaries.listDomainLabels
)
// Change page in pagination
router.get(
'/:dictionaryId/showImportFromFileForm',
user.canContentEdit,
dictionaries.showImportFromFileForm
)
// Change page in pagination
router.get(
'/:dictionaryId/showExportToFileForm',
user.canContentEdit,
dictionaries.showExportToFileForm
)
// Delete all entries of specific dictionary.
router.delete('/:dictionaryId/entries/all', dictionaries.deleteAllEntries)
router.delete(
'/:dictionaryId/entries/all',
user.canAdministrateDictionary,
dictionaries.deleteAllEntries
)
// Publish all entries of specific dictionary.
router.put('/:dictionaryId/entries/all/publish', dictionaries.publishAllEntries)
router.put(
'/:dictionaryId/entries/all/publish',
user.canAdministrateDictionary,
dictionaries.publishAllEntries
)
// Import term candidates from extraction.
router.post(
'/:id/import-extraction/:extractionId',
'/:dictionaryId/import-extraction/:extractionId',
user.canContentEdit,
dictionaries.importFromExtraction
)
// Export dictionary into a file.
router.post('/:id/export-begin', dictionaries.exportBegin)
router.post(
'/:dictionaryId/export-begin',
user.canContentEdit,
dictionaries.exportBegin
)
router.get('/:dictionaryId/domainLabels', dictionaries.listFilteredDomainLabels)
router.get(
'/:dictionaryId/domainLabels',
user.canContentEdit,
dictionaries.listFilteredDomainLabels
)
router.get('/secondaryDomains', dictionaries.listSecondaryDomainData)
router.get(
'/secondaryDomains',
user.canAdministrateDictionaries,
dictionaries.listSecondaryDomainData
)
module.exports = router
+3 -1
View File
@@ -1,5 +1,6 @@
// This page refers to dictionary entries, not consultancy or other entries
const router = require('express-promise-router')()
const user = require('../../../middleware/user')
const {
getEntry,
@@ -10,7 +11,8 @@ const {
getEntryVersionSnapshot
} = require('../../../controllers/api/v1/dictionaries')
// TODO Add authorization.
// All further routes are only available to authenticated users with dictionary content editing rights.
router.use(user.isAuthenticated, user.canContentEdit)
// Render entry data for the selected entry
router.get('/', getEntry)
+42 -20
View File
@@ -1,54 +1,76 @@
const router = require('express-promise-router')()
const extraction = require('../../../controllers/api/v1/extraction')
const { validateOwnership } = require('../../../controllers/extraction')
// TODO In final version test authentication and authorization.
// TODO Also try to lock file editing or other operations once extraction was started.
// TODO Try to lock file editing or other operations once extraction was started.
// All further routes are only available to authenticated users.
router.use((req, res, next) => {
if (req.isAuthenticated()) return next()
res.status(400).end()
})
// Delete extraction.
router.delete('/:id', extraction.delete)
router.delete('/:id', validateOwnership, extraction.delete)
// List documents.
router.get('/:id/documents', extraction.docsList)
router.get('/:id/documents', validateOwnership, extraction.docsList)
// Update documents.
router.put('/:id/documents', extraction.docsUpdate)
router.put('/:id/documents', validateOwnership, extraction.docsUpdate)
// Delete a specific document.
router.delete('/:id/documents/:filename', extraction.docDelete)
router.delete(
'/:id/documents/:filename',
validateOwnership,
extraction.docDelete
)
// List stop term files.
router.get('/:id/stop-terms', extraction.stopTermsList)
router.get('/:id/stop-terms', validateOwnership, extraction.stopTermsList)
// Update stop term files.
router.put('/:id/stop-terms', extraction.stopTermsUpdate)
router.put('/:id/stop-terms', validateOwnership, extraction.stopTermsUpdate)
// Delete a specific stop term file.
router.delete('/:id/stop-terms/:filename', extraction.stopTermDelete)
router.delete(
'/:id/stop-terms/:filename',
validateOwnership,
extraction.stopTermDelete
)
// Save oss params.
router.post('/:id/oss-save-params', extraction.ossSaveParams)
router.post('/:id/oss-save-params', validateOwnership, extraction.ossSaveParams)
// Execute search by oss params.
router.put('/:id/oss-search', extraction.ossSearch)
router.put('/:id/oss-search', validateOwnership, extraction.ossSearch)
// Confirm oss params.
router.put('/:id/oss-confirm-params', extraction.ossConfirmParams)
router.put(
'/:id/oss-confirm-params',
validateOwnership,
extraction.ossConfirmParams
)
// Begin extraction.
router.put('/:id/begin', extraction.begin)
router.put('/:id/begin', validateOwnership, extraction.begin)
// Duplicate extraction.
router.post('/:id/duplicate', extraction.duplicate)
router.post('/:id/duplicate', validateOwnership, extraction.duplicate)
// Export term candidates.
router.get('/:id/term-candidates-export', extraction.termCandidatesExport)
// List all finished extractions for current user.
// TODO Is this enpoint even necessarry?
router.get('/finished-for-user', extraction.listFinishedForUser)
router.get(
'/:id/term-candidates-export',
validateOwnership,
extraction.termCandidatesExport
)
// List term candidates for specific extraction.
router.get('/:id/term-candidates', extraction.listTermCandidates)
router.get(
'/:id/term-candidates',
validateOwnership,
extraction.listTermCandidates
)
module.exports = router
-2
View File
@@ -7,7 +7,6 @@ const dictionariesRouter = require('./dictionaries')
const portalsRouter = require('./portals')
const searchRouter = require('./search')
const consultancyRouter = require('./consultancy')
const demoPaginacijaRouter = require('./demo-paginacija')
const extractionRouter = require('./extraction')
router.use('/comments', commentsRouter)
@@ -18,7 +17,6 @@ router.use('/dictionaries', dictionariesRouter)
router.use('/portals', portalsRouter)
router.use('/search', searchRouter)
router.use('/consultancy', consultancyRouter)
router.use('/demo-paginacija', demoPaginacijaRouter)
router.use('/extraction', extractionRouter)
module.exports = router
+4
View File
@@ -1,4 +1,5 @@
const router = require('express-promise-router')()
const user = require('../../../middleware/user')
const {
create,
@@ -10,6 +11,9 @@ const {
deleteLinkedDictionary
} = require('../../../controllers/api/v1/portals')
// All further routes are only available to portal admin.
router.use(user.isAuthenticated, user.isPortalAdmin)
// Create new portal connection
router.post('/createPortal', create)
+2 -2
View File
@@ -5,11 +5,11 @@ const user = require('../../../middleware/user')
// Return search results for main search.
router.get('/main', search.listMainEntries)
// TODO This route assumes existance of user. Make sure to pre check it.
// Return search results for editor search.
router.get(
'/editor/:dictionaryId',
user.isDictionaryEditor,
user.isAuthenticated,
user.canContentEdit,
search.listEditorEntries
)
+7 -1
View File
@@ -4,12 +4,18 @@ const {
listDictionaries,
syncDictionary
} = require('../../../controllers/api/v1/inter_instance_sync')
const user = require('../../../middleware/user')
// Receive Content Security Policy violation reports.
router.post('/csp-reports', system.handleCspReports)
// Trigger synchronization with Eurotermbank.
router.get('/eurotermbank-sync-push', system.syncWithEurotermbank)
router.get(
'/eurotermbank-sync-push',
user.isAuthenticated,
user.isPortalAdmin,
system.syncWithEurotermbank
)
// List dictionaries (one page).
router.get('/inter-instance-sync/dictionaries', listDictionaries)
+30 -3
View File
@@ -1,6 +1,7 @@
const router = require('express-promise-router')()
const user = require('../../../controllers/users')
const users = require('../../../controllers/api/v1/users')
const userAuth = require('../../../middleware/user')
// Validate and create a new user.
router.post('/register', user.register)
@@ -8,12 +9,38 @@ router.post('/register', user.register)
// Validate and log the user in.
router.post('/login', user.login)
router.get('/addUser', user.findByUsernameOrEmail)
// Supply user with the token to set a new password.
router.post('/reset-password-init', user.generateResetPasswordToken)
router.get('/listAllUsers', users.listUsers)
// Validate new passwords, change if successful and sign the user in.
router.post('/reset-password-submit', user.changePassword)
// Find user by username or email in dictionary or admin console.
router.get(
'/addUser',
userAuth.isAuthenticated,
userAuth.canAdministrateDictionary,
user.findByUsernameOrEmail
)
// List all users in the admin console.
router.get(
'/listAllUsers',
userAuth.isAuthenticated,
userAuth.isPortalAdmin,
users.listUsers
)
// Update hits per page setting of the requesting user.
router.post('/hitsPerPage', users.updateHitsPerPage)
router.post('/nameAndSurname', users.updateFristNameAndSurname)
// Update first name, last name and email of the requesting user.
router.post('/basic-data', users.updateBasicData)
// Update password of the requesting user.
router.post('/password', users.updatePassword)
// Delete user profile of the requesting user.
router.delete('/current', users.deleteCurrent)
module.exports = router
+38 -9
View File
@@ -1,5 +1,6 @@
const router = require('express-promise-router')()
const user = require('../middleware/user')
const { consultancy, consultancyAdmin } = require('../controllers/consultancy')
router.get('/', consultancy.index)
@@ -12,23 +13,51 @@ router.get('/vprasanje/:id', consultancy.specificQuestion)
router.use((req, res, next) => {
if (req.isAuthenticated()) return next()
res.redirect('/')
res.redirect(303, '/')
})
router.get('/vprasanje/admin/novo', consultancyAdmin.new)
router.get(
'/vprasanje/admin/novo',
user.canAdministrateConsultancy,
consultancyAdmin.new
)
router.get('/vprasanje/admin/svetovalci', consultancyAdmin.users)
router.get(
'/vprasanje/admin/svetovalci',
user.canAdministrateConsultancy,
consultancyAdmin.users
)
router.get('/vprasanje/admin/zavrnjeno', consultancyAdmin.rejected)
router.get(
'/vprasanje/admin/zavrnjeno',
user.canAdministrateConsultancy,
consultancyAdmin.rejected
)
router.get('/vprasanje/admin/objavljeno', consultancyAdmin.published)
router.get(
'/vprasanje/admin/objavljeno',
user.canConsult,
consultancyAdmin.published
)
router.get('/vprasanje/admin/pripravljeno', consultancyAdmin.prepared)
router.get(
'/vprasanje/admin/pripravljeno',
user.canAdministrateConsultancy,
consultancyAdmin.prepared
)
router.get('/vprasanje/admin/v-delu', consultancyAdmin.inProgress)
router.get(
'/vprasanje/admin/v-delu',
user.canConsult,
consultancyAdmin.inProgress
)
router.get('/vprasanje/admin/statistika', consultancyAdmin.statistics)
// router.get('/vprasanje/admin/statistika', consultancyAdmin.statistics)
router.get('/vprasanje/admin/urejanje/:id', consultancyAdmin.edit)
router.get(
'/vprasanje/admin/urejanje/:id',
user.canConsultEntry,
consultancyAdmin.edit
)
module.exports = router
+1 -8
View File
@@ -13,7 +13,7 @@ router.get('/:dictionaryId/o-slovarju', dictionary.dictionaryDetails)
// All further routes are only available to authenticated users.
router.use((req, res, next) => {
if (req.isAuthenticated()) return next()
res.redirect(req.baseUrl)
res.redirect(303, req.baseUrl)
})
// Show a form to create a new dictionary.
@@ -92,13 +92,6 @@ router.get(
dictionary.showContent
)
// Validate and update dictionary content entries.
// router.post(
// '/:dictionaryId/vsebina',
// user.isDictionaryEditor,
// dictionary.updateContent
// )
// Show a form to export a dictionary.
router.get(
'/:dictionaryId/izvoz',
-27
View File
@@ -1,27 +0,0 @@
const router = require('express-promise-router')()
const extraction = require('../controllers/extraction-poc')
// TODO In final version test authentication and authorization.
// List all extractions.
router.get('/', extraction.list)
// Create new extraction.
router.post('/', extraction.create)
// Edit extraction.
router.get('/:id', extraction.edit)
// Update extraction (own documents).
router.post('/:id', extraction.updateOwn)
// Edit documents.
router.get('/:id/besedila', extraction.docsEdit)
// Edit stop terms.
router.get('/:id/stop-termini', extraction.stopTermsEdit)
// List term candidates for extraction and display export interface.
router.get('/:id/kandidati', extraction.listTermCandidates)
module.exports = router
+14 -36
View File
@@ -1,6 +1,5 @@
const router = require('express-promise-router')()
const extraction = require('../controllers/extraction')
const extractionPocRouter = require('./extraction-poc')
// List all extractions.
router.get('/', extraction.list)
@@ -8,54 +7,33 @@ router.get('/', extraction.list)
// All further routes are only available to authenticated users.
router.use((req, res, next) => {
if (req.isAuthenticated()) return next()
res.redirect(req.baseUrl)
res.redirect(303, req.baseUrl)
})
// Create new extraction.
router.post('/', extraction.create)
router.use('/poc', extractionPocRouter)
// Edit extraction.
router.get('/:id', extraction.edit)
router.get('/:id', extraction.validateOwnership, extraction.edit)
// Update extraction (own documents).
router.post('/:id', extraction.updateOwn)
router.post('/:id', extraction.validateOwnership, extraction.updateOwn)
// Edit documents.
router.get('/:id/besedila', extraction.docsEdit)
router.get('/:id/besedila', extraction.validateOwnership, extraction.docsEdit)
// Edit stop terms.
router.get('/:id/stop-termini', extraction.stopTermsEdit)
router.get(
'/:id/stop-termini',
extraction.validateOwnership,
extraction.stopTermsEdit
)
// List term candidates for extraction and display export interface.
router.get('/:id/kandidati', extraction.listTermCandidates)
// // TODO: Ask Luka or Miro what to do with this page.
// router.get('/id_luscenja/kandidati', (req, res) => {
// res.render('pages/extraction/terms', {
// title: 'Terminološki kandidati'
// })
// })
// // Show a form to export a dictionary
// router.get('/id_luscenja/dokumenti/besedila', (req, res) => {
// res.render('pages/extraction/docs-edit', {
// title: 'Besedila'
// })
// })
// // Show a form to export a dictionary
// router.get('/id_luscenja/oss', (req, res) => {
// res.render('pages/extraction/edit-oss', {
// title: 'KAS + dokumenti'
// })
// })
// router.get('/id_luscenja/dokumenti/termini', (req, res) => {
// res.render('pages/extraction/stop-terms-edit', {
// title: 'Termini'
// })
// })
router.get(
'/:id/kandidati',
extraction.validateOwnership,
extraction.listTermCandidates
)
module.exports = router
+11 -64
View File
@@ -1,11 +1,7 @@
const router = require('express-promise-router')()
const { isAuthenticated: isUserAuthenticated } = require('../middleware/user')
const db = require('../models/db')
const cache = require('../models/cache')
const { searchEngineClient } = require('../models/search-engine')
const index = require('../controllers/index')
const user = require('../controllers/users')
const demoPaginacija = require('../controllers/demo-paginacija')
// TODO Some of these endpoints might need authorization protection. Review and clean up at a later point.
@@ -18,54 +14,6 @@ router.get('/iskanje', index.search)
// Show a list of search results.
router.get('/termin/:entryId', index.entryDetails)
/*
router.get('/termin/id_termina', (req, res) => {
res.render('pages/search/result-detail')
})
*/
router.get('/keys', (req, res) => {
res.render('pages/search/special-keys-demo-rm-this')
})
router.get('/demo', (req, res) => {
res.render('pages/admin')
})
router.get('/select', (req, res) => {
res.render('pages/search-root')
})
router.get('/dictionaries', (req, res) => {
res.render('pages/my-dictionaries-root')
})
router.get('/db-query-demo', async (req, res) => {
const queryResult = await db.query("SELECT 'yes' as does_it_work")
res.send(queryResult.rows[0])
})
router.get('/test-seje', (req, res) => {
// req.session.before = { a: 1, b: 'yes' }
const { sessionID, session } = req
// req.session.after = { c: 2, d: 'no' }
res.send({ sessionID, session })
})
router.get('/test-cacha', async (req, res, next) => {
const sRes = await cache.set('my key', 'also my val')
const gRes = await cache.get('my key')
res.send({ sRes, gRes })
})
router.get('/test-iskalnika', async (req, res) => {
const response = await searchEngineClient.ping()
res.send(response)
})
// Stran z demo implementacijo paginirane vsebine.
router.get('/demo-paginacija', demoPaginacija.izrišiStran)
// Render help page
router.get('/pomoc', (req, res) => {
res.render('pages/help', { title: req.t('Pomoč') })
@@ -86,17 +34,9 @@ router.get('/slovarji/xml-schema', (req, res) => {
res.download('public/documents/dictionary_schema.xsd')
})
// Render demo help pug page.
router.get('/pomoc-pug-demo', (req, res) => {
res.render('pages/help-pug-demo-frame', { title: 'Pomoč - pug demo' })
})
// Activate user account.
router.get('/users/activate', user.activateAccount)
// Log the user out.
router.post('/users/logout', user.logout)
// Render privacy policy page
router.get('/politika-zasebnosti', (req, res) => {
res.render(`pages/privacy-policy_${req.language}`, {
@@ -111,6 +51,15 @@ router.get('/pogoji-uporabe', (req, res) => {
})
})
// Form for user to reset their password.
router.get('/ponastavitev-gesla', index.resetPassword)
// Confirm user email change.
router.get('/sprememba-elektronskega-naslova', index.changeEmail)
// Change user locale.
router.get('/spremeni-jezik/:languageCode', index.changeUserLanguage)
router.get('/moj-racun', isUserAuthenticated, index.myProfile)
router.get('/izbrisi-racun', isUserAuthenticated, index.deleteProfile)
@@ -119,9 +68,7 @@ router.get('/spremeni-geslo', isUserAuthenticated, index.changePassword)
router.get('/nastavitve-racuna', isUserAuthenticated, index.userSettings)
router.get('/ponastavitev-gesla', index.resetPassword)
// Change user locale.
router.get('/spremeni-jezik/:languageCode', index.changeUserLanguage)
// Log the user out.
router.post('/users/logout', isUserAuthenticated, user.logout)
module.exports = router
+2
View File
@@ -7,4 +7,6 @@ utils.pipe = (...fns) => (initialVal) => fns.reduce((val, fn) => fn(val), initia
utils.composeAsync = (...fns) => input => fns.reduceRight((chain, func) => chain.then(func), Promise.resolve(input));
utils.pipeAsync = (...fns) => input => fns.reduce((chain, func) => chain.then(func), Promise.resolve(input));
utils.capitalize = string => string[0].toUpperCase() + string.slice(1)
module.exports = utils
@@ -7,7 +7,7 @@ mixin sideNavigation(metaData)
src="/images/burger-menu-button-icon.svg"
alt="Meni"
)
span#nav-title.nav-title= 'Svetovalnica'
span#nav-title.nav-title= t('Svetovalnica')
#mobile-right-holder
nav
ul.admin-nav-content.slidable.z-20.scroller-style
@@ -70,7 +70,7 @@
type="checkbox"
)
label.ps-2.align-middle.rememberloginlabel.text-header-description-gray.pe-2(
for="login-remember-me"
for="login-remember"
)= t('Zapomni si prijavo')
.col-5.pt-2.d-flex-and-align-end.pe-0
a#forgotten-password.ms-auto.forgotten-password.gray1-text.no-text-decoration.align-middle(
@@ -58,18 +58,18 @@ mixin main-navigation-right(languageToolbarPresent=false, menuTextPresent=false,
a.dropdown-item.ps-0.pb-2.navigation-text-color(
href="/nastavitve-racuna"
)
.row
.col-3
img(src="/images/cog.svg" alt="")
.col-9
.d-flex.flex-row
.d-flex.flex-shrink-1.flex-grow-0
img.pe-2(src="/images/cog.svg" alt="")
.d-flex.flex-grow-1
span.text-header-description-gray.d-block= t('Nastavitve')
li.pb-0.mb-0.ps-3
form(action="/users/logout" method="post")
button.profile-menu-button
.row
.col-3
img(src="/images/log-out.svg" alt="")
.col-9
button.profile-menu-button.pb-2.pt-1
.d-flex.flex-row
.d-flex.flex-shrink-1.flex-grow-0
img.pe-2(src="/images/log-out.svg" alt="")
.d-flex.flex-grow-1
span.text-header-description-gray.d-block= t('Odjava')
else
button#login-static-backdrop-button.col-sm.nav-entry.no-border.no-bg(
@@ -15,7 +15,7 @@ mixin consultancySM(adminFields=false)
)
//- option.d-none(value="-1")= ' '
each domain in allPrimaryDomains
option(value=domain.id selected=domain.selected) #{ domain.nameSl }
option(value=domain.id selected=domain.selected)= domain.name
span.advanced-input-label
//- span.advanced-input-label Področje
//- img#area-img(src="/images/chevron-down-darker.svg" alt="")
@@ -23,7 +23,7 @@
//- input#src-lang-input.box-sizing-content.bg-transparent
select#src-lang-select.select-src-lang-field(multiple="multiple")
each language in sourceLanguages
option(value=language.id selected=language.selected) #{ language.nameSl }
option(value=language.id selected=language.selected)= language.name
span.advanced-input-label
//- span.advanced-input-label Jezik iskanja
//- img#src-lang-img(src="/images/chevron-down-darker.svg" alt="")
@@ -35,7 +35,7 @@
//- input#dest-lang-input.box-sizing-content.bg-transparent
select#dest-lang-select.select-dest-lang-field(multiple="multiple")
each language in targetLanguages
option(value=language.id selected=language.selected) #{ language.nameSl }
option(value=language.id selected=language.selected)= language.name
span.advanced-input-label
//- span.advanced-input-label Ciljni jezik
//- img#dest-lang-img(src="/images/chevron-down-darker.svg" alt="")
@@ -62,7 +62,7 @@
multiple="multiple"
)
each domain in allPrimaryDomains
option(value=domain.id selected=domain.selected) #{ domain.nameSl }
option(value=domain.id selected=domain.selected)= domain.name
span.advanced-input-label
//- span.advanced-input-label Področje
//- img#area-img(src="/images/chevron-down-darker.svg" alt="")
@@ -78,7 +78,7 @@
multiple="multiple"
)
each dictionary in allDictionaryNames
option(value=dictionary.id selected=dictionary.selected) #{ dictionary.nameSl }
option(value=dictionary.id selected=dictionary.selected)= dictionary.name
span.advanced-input-label
//- span.advanced-input-label Slovar
//- img#dict-img(src="/images/chevron-down-darker.svg" alt="")
@@ -91,7 +91,7 @@
//- input#source-input.box-sizing-content.bg-transparent
select#src-select.select-source-field(multiple="multiple")
each portal in portals
option(value=portal.code selected=portal.selected) #{ portal.name }
option(value=portal.code selected=portal.selected)= portal.name
span.advanced-input-label
//- span.advanced-input-label Vir
//- img#source-img(src="/images/chevron-down-darker.svg" alt="")
@@ -2,7 +2,7 @@ mixin result-item(headword, entry, isLast=false)
-
const foreignEntries = entry.foreignEntries ? entry.foreignEntries : []
const synonyms = entry.synonyms?entry.synonyms:[]
const domain = entry.primaryDomain&&entry.primaryDomain.nameSl?entry.primaryDomain.nameSl:""
const domain = entry.primaryDomain?entry.primaryDomain[`name${capitalize(determinedLanguage)}`]:""
const slovenianDefinitionExists = !!entry.definition
let foreignDefinitionExists = !!entry.foreignEntries && entry.foreignEntries.length > 0
let checkAllForeignDefinitions = false
@@ -92,7 +92,7 @@ mixin result-item(headword, entry, isLast=false)
.results-sources.d-flex.flex-shrink-1.justify-content-end.align-items-center
.results-dict.d-flex.justify-content-end.align-items-center.me-2.px-2(
data-bs-toggle="tooltip"
title=`${entry.dictionary.nameSl}`
title=`${entry.dictionary[`name${capitalize(determinedLanguage)}`]}`
) #{ entry.dictionary.nameSlShort }
.results-portal.d-flex.justify-content-end.align-items-center.te(
data-bs-toggle="tooltip"
@@ -22,7 +22,7 @@ section.filter-modal-root
span.d-flex.flex-grow-1.align-content-center
a#ccf.clear-filter-modal-section(href="#")
img(src="/images/square-minus.svg" alt="")
span.ps-2 POČISTI FILTRE
span.ps-2= t('POČISTI FILTRE')
.modal-body.pb-2
ul#modal-list.nav-modal-section-content.mb-0.pb-0
// Added dynamically...
@@ -31,8 +31,6 @@ section.filter-modal-root
button.btn.btn-light.border.border-dark.close(
type="button"
data-bs-dismiss="modal"
)
| Zapri
)= t('Zapri')
.justify-content-end
button#sf-modal-button.btn.btn-primary.use(type="button")
| Uporabi
button#sf-modal-button.btn.btn-primary.use(type="button")= t('Uporabi')
@@ -1,12 +1,12 @@
p Pozdravljeni,
case type
when 'status'
p oseba: #[b #{ changerEmail }] je spremenila stanje vašega slovarja: #[b #{ nameSl }].
p oseba: #[b #{ changerEmail }] je spremenila stanje vašega slovarja: #[b #{ name }].
when 'unpublish'
p oseba: #[b #{ changerEmail }] je spremenila status slovarja #[b #{ nameSl }], ki ni več objavljen.
p oseba: #[b #{ changerEmail }] je spremenila status slovarja #[b #{ name }], ki ni več objavljen.
when 'delete'
p slovar: #[b #{ nameSl }] ima manjše število gesel kot je nastavljeno.
p slovar: #[b #{ name }] ima manjše število gesel kot je nastavljeno.
when 'publish-approval'
p slovar #[b #{ nameSl }] je bil dan v stanje preverjanja. Prosimo, da ga odprete/zaprete.
p slovar #[b #{ name }] je bil dan v stanje preverjanja. Prosimo, da ga odprete/zaprete.
when 'publish-no-approval'
p slovar #[b #{ nameSl }] je bil objavljen.
p slovar #[b #{ name }] je bil objavljen.
@@ -0,0 +1,2 @@
p Dear #{ username },
p you have successfully changed the e-mail address used for your Slovenian Terminology Portal user account.
@@ -0,0 +1,2 @@
p Pozdravljeni #{ username },
p to sporočilo ste prejeli, ker ste na Slovenskem terminološkem portalu uspešno spremenili elektronski naslov.
@@ -0,0 +1,4 @@
p Dear #{ username },
p you have received this message because you want to change the e-mail address used for your Slovenian Terminology Portal account.
p To change the e-mail address, please confirm that this is your new e-mail address. The link will be valid for 7 days.
a(href=changeEmailLink) I confirm my e-mail address
@@ -0,0 +1,4 @@
p Pozdravljeni #{ username },
p to sporočilo ste prejeli, ker na Slovenskem terminološkem portalu želite spremeniti elektronski naslov.
p Za spremembo naslova potrebujete le še potrditi, da je to res vaš novi elektronski naslov. Povezava je veljavna 7 dni.
a(href=changeEmailLink) Potrjujem svoj naslov
@@ -0,0 +1,2 @@
p Dear #{ username },
p you have changed the password for your Slovenian Terminology Portal user account.
@@ -0,0 +1,2 @@
p Pozdravljeni #{ username },
p to sporočilo ste prejeli, ker ste na Slovenskem terminološkem portalu spremenili geslo.
@@ -0,0 +1,2 @@
p Dear #{ username },
p you have successfully reset the password for your Slovenian Terminology Portal user account.
@@ -0,0 +1,2 @@
p Pozdravljeni #{ username },
p to sporočilo ste prejeli, ker ste na Slovenskem terminološkem portalu uspešno ponastavili geslo.
@@ -0,0 +1,4 @@
p Dear #{ username },
p you have received this message because you want to change the password for the Slovenian Terminology Portal user account connected to this e-mail address.
p To reset the password, click the link below. The link will be valid for 1 day.
a(href=resetPasswordLink) Password reset
@@ -0,0 +1,4 @@
p Pozdravljeni #{ username },
p to sporočilo ste prejeli, ker na Slovenskem terminološkem portalu želite ponastaviti geslo uporabniškega računa s tem elektronskim naslovom.
p Za ponastavitev sledite spodnji povezavi. Povezava je veljavna 1 dan.
a(href=resetPasswordLink) Ponastavitev gesla
@@ -1,29 +0,0 @@
//- TODO MARK FOR DELETION
h1 Dokumenti za luščenje #{ id }
h2 Seznam datotek
ul#files-list
each document in extractionDocuments
li DATOTEKA - Ime: #[span.filename= document.filename], velikost: #{ document.size }, datum: #{ new Date(document.timeModified).toLocaleDateString('sl-SL') } #[a.delete-file(href="#") BRIŠI]
h2 Dodaj nov(e) datoteke(e)
form#upload-files(method="post" enctype="multipart/form-data")
input(type="hidden" name="extractionId" value=id)
label Datoteka(e):
input(
type="file"
name="extractionFile"
accept=".txt, .doc, .docx, .pdf"
multiple
)
a(href=`../${id}`) Nazaj
p Obestila:
ul#messages
script(
nonce=cspNonce
src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"
)
script(nonce=cspNonce src="/javascripts/extraction-poc-files-edit.js")
-128
View File
@@ -1,128 +0,0 @@
h1 Luščenje #{ id } - uredi
form(method="post")
div
label Ime:
input(name="name" value=extraction.name)
div
label Glavno področje:
select(name="domain")
option
each domain in allPrimaryDomains
option(value=domain.id selected=domain.id === extraction.domainId)= domain.nameSl
div
label Vrsta dokumenta:
select.pick-multiple(name="documentType" multiple)
option
option(value=101 selected=extraction.documentType.includes(101)) Izvirni znanstveni članek
option(value=102 selected=extraction.documentType.includes(102)) Pregledni znanstveni članek
option(value=103 selected=extraction.documentType.includes(103)) Kratki znanstveni prispevek
option(value=104 selected=extraction.documentType.includes(104)) Strokovni članek
option(value=105 selected=extraction.documentType.includes(105)) Poljudni članek
option(value=106 selected=extraction.documentType.includes(106)) Objavljeni znanstveni prispevek na konferenci (vabljeno predavanje)
option(value=107 selected=extraction.documentType.includes(107)) Objavljeni strokovni prispevek na konferenci (vabljeno predavanje)
option(value=108 selected=extraction.documentType.includes(108)) Objavljeni znanstveni prispevek na konferenci
option(value=109 selected=extraction.documentType.includes(109)) Objavljeni strokovni prispevek na konferenci
option(value=110 selected=extraction.documentType.includes(110)) Objavljeni povzetek znanstvenega prispevka na konferenci (vabljeno predavanje)
option(value=112 selected=extraction.documentType.includes(112)) Objavljeni povzetek znanstvenega prispevka na konferenci
option(value=113 selected=extraction.documentType.includes(113)) Objavljeni povzetek strokovnega prispevka na konferenci
option(value=116 selected=extraction.documentType.includes(116)) Samostojni znanstveni sestavek ali poglavje v monografski publikaciji
option(value=117 selected=extraction.documentType.includes(117)) Samostojni strokovni sestavek ali poglavje v monografski publikaciji
option(value=118 selected=extraction.documentType.includes(118)) Geslo sestavek v enciklopediji, leksikonu, slovarju...
option(value=119 selected=extraction.documentType.includes(119)) Recenzija, prikaz knjige, kritika
option(value=120 selected=extraction.documentType.includes(120)) Predgovor, spremna beseda
option(value=121 selected=extraction.documentType.includes(121)) Polemika, diskusijski prispevek
option(value=122 selected=extraction.documentType.includes(122)) Intervju
option(value=123 selected=extraction.documentType.includes(123)) Umetniški sestavek
option(value=124 selected=extraction.documentType.includes(124)) Bibliografija, kazalo ipd.
option(value=125 selected=extraction.documentType.includes(125)) Drugi članki ali sestavki
option(value=201 selected=extraction.documentType.includes(201)) Znanstvena monografija
option(value=202 selected=extraction.documentType.includes(202)) Strokovna monografija
option(value=203 selected=extraction.documentType.includes(203)) Univerzitetni, visokošolski ali višješolski učbenik z recenzijo
option(value=204 selected=extraction.documentType.includes(204)) Srednješolski, osnovnošolski ali drugi učbenik z recenzijo
option(value=205 selected=extraction.documentType.includes(205)) Video in druga učna gradiva
option(value=206 selected=extraction.documentType.includes(206)) Enciklopedija, slovar, leksikon, priročnik, atlas, zemljevid
option(value=207 selected=extraction.documentType.includes(207)) Bibliografija
option(value=208 selected=extraction.documentType.includes(208)) Doktorska disertacija
option(value=209 selected=extraction.documentType.includes(209)) Magistrsko delo
option(value=210 selected=extraction.documentType.includes(210)) Specialistično delo
option(value=211 selected=extraction.documentType.includes(211)) Diplomsko delo
option(value=212 selected=extraction.documentType.includes(212)) Končno poročilo o rezultatih raziskav
option(value=213 selected=extraction.documentType.includes(213)) Elaborat, predštudija, študija
option(value=214 selected=extraction.documentType.includes(214)) Projektna dokumentacija (idejni projekt, izvedbeni projekt)
option(value=215 selected=extraction.documentType.includes(215)) Izvedensko mnenje, arbitražna odločba
option(value=216 selected=extraction.documentType.includes(216)) Umetniško delo
option(value=217 selected=extraction.documentType.includes(217)) Katalog razstave
option(value=218 selected=extraction.documentType.includes(218)) Raziskovalni ali dokumentarni film, zvočni ali video posnetek
option(value=219 selected=extraction.documentType.includes(219)) Radijska ali televizijska oddaja
option(value=220 selected=extraction.documentType.includes(220)) Raziskovalni podatki
option(value=221 selected=extraction.documentType.includes(221)) Programska oprema
option(value=222 selected=extraction.documentType.includes(222)) Nova sorta
option(value=223 selected=extraction.documentType.includes(223)) Patentna prijava
option(value=224 selected=extraction.documentType.includes(224)) Patent
option(value=225 selected=extraction.documentType.includes(225)) Druge monografije in druga zaključena dela
option(value=227 selected=extraction.documentType.includes(227)) Znanstveni terminološki slovar, enciklopedija ali tematski leksikon
option(value=230 selected=extraction.documentType.includes(230)) Zbornik strokovnih ali nerecenziranih znanstvenih prispevkov na konferenci
option(value=231 selected=extraction.documentType.includes(231)) Zbornik recenziranih znanstvenih prispevkov na mednarodni ali tuji konferenci
option(value=232 selected=extraction.documentType.includes(232)) Zbornik recenziranih znanstvenih prispevkov na domači konferenci
option(value=310 selected=extraction.documentType.includes(310)) Umetniška poustvaritev
option(value=311 selected=extraction.documentType.includes(311)) Radijski ali TV dogodek
option(value=312 selected=extraction.documentType.includes(312)) Razstava
option(value=313 selected=extraction.documentType.includes(313)) Organiziranje znanstvenih in strokovnih sestankov
option(value=314 selected=extraction.documentType.includes(314)) Predavanje na tuji univerzi
option(value=315 selected=extraction.documentType.includes(315)) Prispevek na konferenci brez natisa
option(value=316 selected=extraction.documentType.includes(316)) Vabljeno predavanje na konferenci brez natisa
option(value=320 selected=extraction.documentType.includes(320)) Druga dela
option(value=325 selected=extraction.documentType.includes(325)) Druga izvedena dela
div
label Leto:
select.enter-multiple(name="year" multiple style="width: 500px")
each year in extraction.year
option(value=year selected)= year
div
label Ključne besede:
select.enter-multiple(name="keywords" multiple style="width: 500px")
each keyword in extraction.keywords
option(value=keyword selected)= keyword
h2 Stop termini:
ul
each file in stopTermsFiles
li DATOTEKA - Ime: #[span.filename= file.filename], velikost: #{ file.size }, datum: #{ new Date(file.timeModified).toLocaleDateString('sl-SL') }
a#edit-stop-terms(href=`${id}/stop-termini`) Uredi
br
br
button#search-btn Najdi
#search-result
br
br
a(href="../poc") Nazaj
p Obestila:
ul#messages
link(
href="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/css/select2.min.css"
rel="stylesheet"
integrity="sha256-zaSoHBhwFdle0scfGEFUCwggPN7F+ip9XRglo8IWb4w="
crossorigin="anonymous"
)
script(
nonce=cspNonce
src="https://code.jquery.com/jquery-3.6.0.slim.min.js"
integrity="sha256-u7e5khyithlIdTpu22PHhENmPcRdFiHRjhAuHcs05RI="
crossorigin="anonymous"
)
script(
nonce=cspNonce
src="https://cdn.jsdelivr.net/npm/select2@4.1.0-rc.0/dist/js/select2.min.js"
integrity="sha256-9yRP/2EFlblE92vzCA10469Ctd0jT48HnmmMw5rJZrA="
crossorigin="anonymous"
)
script(
nonce=cspNonce
src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"
)
script(nonce=cspNonce src="/javascripts/extraction-poc-oss-edit.js")
-20
View File
@@ -1,20 +0,0 @@
h1 Luščenje #{ id } - uredi
form(method="post")
label Ime:
input(name="name" value=extraction.name)
button Potrdi
h2 Lastni dokumenti:
ul
each document in extractionDocuments
li DATOTEKA - Ime: #[span.filename= document.filename], velikost: #{ document.size }, datum: #{ new Date(document.timeModified).toLocaleDateString('sl-SL') }
a(href=`${id}/besedila`) Uredi
h2 Stop termini:
ul
each file in stopTermsFiles
li DATOTEKA - Ime: #[span.filename= file.filename], velikost: #{ file.size }, datum: #{ new Date(file.timeModified).toLocaleDateString('sl-SL') }
a(href=`${id}/stop-termini`) Uredi
br
a(href="../poc") Nazaj

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