First full release

This commit is contained in:
Luka Romih
2023-03-10 12:50:23 +01:00
parent 257f3c354f
commit 9867875ef2
285 changed files with 10980 additions and 4692 deletions
+248 -12
View File
@@ -1,11 +1,18 @@
const ConsultancyEntry = require('../../../models/consultancy-entry')
const Domain = require('../../../models/domain')
const User = require('../../../models/user')
const { promisify } = require('util')
const i18next = require('i18next')
const {
deleteConsultancyEntryFromIndex
} = require('../../../models/search-engine')
const email = require('../../../models/email')
const helper = require('../../../models/helpers')
const { getInstanceSetting } = require('../../../models/helpers')
const { searchConsultancyEntryIndex } = require('../../../models/search-engine')
const { prepareConsultancyEntries } = require('../../../models/helpers/search')
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
const generateQuery = require('../../../models/helpers/search/generate-query')
const consultancy = {}
@@ -23,6 +30,43 @@ consultancy.listNewEntries = async (req, res) => {
res.send(data)
}
consultancy.sendPaginationData = async (req, res) => {
let requestType = req.query.type
const isAdminPage = req.query.isAdmin === 'true'
let page = +req.query.p || 1
if (page < 1) {
page = 1
}
if (isAdminPage) {
if (
req.user &&
(req.user.hasRole('portal admin') ||
req.user.hasRole('consultancy admin'))
) {
// Everything is allowed, nothing to do here
} else if (req.user && req.user.hasRole('consultant')) {
if (!(requestType === 'in progress' || requestType === 'published')) {
return res.status(400).send()
}
} else {
// for now, public can only see published entries
requestType = 'published'
}
}
return await consultancyRequestItems(
req,
res,
requestType || 'published',
'components/consultancy/api/consultancy-item-rendered',
isAdminPage,
!isAdminPage, // -> In current implementation, it is just the inverse of isAdminPage
page
)
}
consultancy.createQuestion = async (req, res) => {
const q = req.body
const consultancyEntry = {}
@@ -35,7 +79,7 @@ consultancy.createQuestion = async (req, res) => {
const { description } = q
if (!description) {
return res.status(400).send('description is a required parameter!')
return res.status(400).send('Description is a required parameter!')
}
consultancyEntry.description = description
@@ -52,24 +96,46 @@ consultancy.createQuestion = async (req, res) => {
consultancyEntry[key] = helper.removeHtmlTags(consultancyEntry[key]).trim()
})
const questionId = await ConsultancyEntry.createQuestion(consultancyEntry)
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
const isOwnConsultancyEnabled =
(await getInstanceSetting('consultancy_type')) === 'own'
// TODO SEND EMAIL
// TODOOOOOOOOO
let domainNameSl
if (consultancyEntry.domainPrimaryIdInitial) {
domainNameSl = (
await Domain.fetchById(consultancyEntry.domainPrimaryIdInitial)
).nameSl
} else {
domainNameSl = ''
}
const emails = await ConsultancyEntry.fetchConsultancyAdminEmails()
let emails
let subjectText
if (isOwnConsultancyEnabled) {
const questionId = await ConsultancyEntry.createQuestion(consultancyEntry)
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
subjectText = req.t('Ustvarjeno novo vprašanje v svetovalnici')
emails = await ConsultancyEntry.fetchConsultancyAdminEmails()
} else {
subjectText = req.t('Novo vprašanje za Terminološko svetovalnico')
emails = await getInstanceSetting('zrc_email')
}
const renderAsync = promisify(req.app.render.bind(req.app))
const emailHtml = await renderAsync('email/consultancy-creation-notify', {
propertyToPassGoesHere: 'test1234'
nameAndSurname: `${res.locals.user.firstName} ${res.locals.user.lastName}`,
email: res.locals.user.email,
domain: domainNameSl,
institution: consultancyEntry.institution,
question: consultancyEntry.description,
existingSolutions: consultancyEntry.existingSolutions,
examplesOfUse: consultancyEntry.examplesOfUse
})
// TODO i18n - What language are the email title and content (we already have email translated)
await email.send({
to: emails,
subject: 'Ustvarjeno novo vprašanje v svetovalnici',
subject: subjectText,
html: emailHtml
})
/// /////////////////
res.status(201).send()
}
@@ -179,9 +245,10 @@ consultancy.assign = async (req, res) => {
const renderAsync = promisify(req.app.render.bind(req.app))
const emailHtml = await renderAsync('email/consultancy-assigned')
// TODO i18n - What language are the email title and content (we already have email translated)
await email.send({
to: emails,
subject: 'Novo terminološko vprašanje',
subject: req.t('Novo terminološko vprašanje'),
html: emailHtml
})
@@ -234,9 +301,10 @@ consultancy.sendToReview = async (req, res) => {
const renderAsync = promisify(req.app.render.bind(req.app))
const emailHtml = await renderAsync('email/consultancy-item-review')
// TODO i18n - What language are the email title and content (we already have email translated)
await email.send({
to: emails,
subject: 'Potrditev objave',
subject: req.t('Potrditev objave'),
html: emailHtml
})
@@ -254,6 +322,19 @@ consultancy.publish = async (req, res) => {
if (!entry.title) {
return res.status(400).send('Answer not completed')
}
const author = await User.fetchUser(entry.authorId)
const portalName = await getInstanceSetting(`portal_name_${author.language}`)
const renderAsync = promisify(req.app.render.bind(req.app))
const emailHtml = await renderAsync('email/consultancy-publish-notify', {
portalName
})
await email.send({
to: author.email,
subject: i18next.t('Objava terminološkega vprašanja', {
lng: author.language
}),
html: emailHtml
})
await ConsultancyEntry.publish(questionId, answerAuthors)
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
@@ -267,7 +348,9 @@ consultancy.updateQuestion = async (req, res) => {
if (!id) return res.status(400).send({})
if (questionTitle === '' || question === '' || answer === '') {
return res.status(422).send('Polja naslov, vprašanje in mnenje so obvezna!')
return res
.status(422)
.send(req.t('Polja naslov, vprašanje in mnenje so obvezna!'))
}
const entry = await ConsultancyEntry.fetchById(id)
@@ -318,4 +401,157 @@ consultancy.deleteQuestion = async (req, res) => {
res.send()
}
async function consultancyRequestItems(
req,
res,
type,
url,
isAdminPage = false,
privilegeToSeAll = true, // this method seperates consultancy main from admin, so all results get visible TO ALL REGISTERED USERS, not just admins
page
) {
const searchString = req.query.q?.trim() ?? ''
/// //////////////////////////////////////////
// let allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
/// filter selected domains for the prompt ///
// Not duplicates of this code arise...
// const pdList = intoDbArray(req.query.pd, 'always')
// allPrimaryDomains = allPrimaryDomains.map(entry => {
// if (pdList.includes(`${entry.id}`)) {
// entry.selected = true
// }
// return entry
// })
/// //////////////////////////////////////////
let assignedConsultant
if (
!privilegeToSeAll &&
req.user &&
!(req.user.hasRole('portal admin') || req.user.hasRole('consultancy admin'))
) {
assignedConsultant = req.user.id
} else {
assignedConsultant = undefined
}
const filters = {
status: type,
assignedConsultant,
primaryDomain: req.query.pd
}
if (!isAdminPage) {
filters.assignedConsultant = undefined
filters.status = 'published' // guard
}
const hitsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
// const page = +req.query.p > 0 ? +req.query.p : 1
const hitsQuery = generateQuery.consultancy(
searchString,
filters,
hitsPerPage,
page
)
const hits = await searchConsultancyEntryIndex(hitsQuery)
const numberOfAllHits = hits.body.hits.total.value
const numberOfAllPages = Math.ceil(numberOfAllHits / hitsPerPage)
let entries = prepareConsultancyEntries(hits)
// console.log({ entries, numberOfAllHits, numberOfAllPages })
entries = entries.map(entry => {
entry.primaryDomain = entry.primaryDomain
? entry.primaryDomain.nameSl
: req.t('nedefinirano')
if (entry.assignedConsultants) {
entry.firstName = entry.assignedConsultants[0]?.firstName
entry.lastName = entry.assignedConsultants[0]?.lastName
}
if (entry.assignedConsultants && entry.assignedConsultants.length > 1) {
entry.sharedAuthors = []
for (let i = 1; i < entry.assignedConsultants.length; i++) {
// skip first element (moderator)
entry.sharedAuthors.push(
`${entry.assignedConsultants[i].firstName} ${entry.assignedConsultants[i].lastName}`
)
}
}
if (entry.timeCreated) {
// TODO i18n date format
const date = new Date(entry.timeCreated)
entry.formattedTimeCreated = `${date.getDate()}. ${
date.getMonth() + 1
}. ${date.getFullYear()}`
}
return entry
})
// const entryList = await mapEntryList(inProgressEntryList)
const userList = await User.fetchConsultants()
if (!isAdminPage) {
entries.map(entry => {
const MAX_CHARACTER_LENGTH = 200
let appendAnswer = ''
let appendQuestion = ''
if (entry.answer && entry.answer.length > MAX_CHARACTER_LENGTH) {
appendAnswer = '...'
}
if (entry.question && entry.question.length > MAX_CHARACTER_LENGTH) {
appendQuestion = '...'
}
entry.answerSummary = `${entry.answer.slice(
0,
MAX_CHARACTER_LENGTH
)}${appendAnswer}`
entry.question = `${entry.question.slice(
0,
MAX_CHARACTER_LENGTH
)}${appendQuestion}`
return entry
})
}
res.append('page', page)
res.append('number-of-all-pages', numberOfAllPages)
res.render(url, {
entries, // entryList,
userList,
numberOfAllPages,
isAdminPage,
section: consultancyAdminPageMapper(type),
queryCount: numberOfAllHits
})
}
// mapping required due to the inconsistent naming convention
function consultancyAdminPageMapper(type) {
if (type === 'in progress') {
return { inProgress: true }
}
if (type === 'review') {
return { prepared: true }
}
return { type: true }
}
module.exports = consultancy
+141 -8
View File
@@ -1,5 +1,7 @@
const { rm } = require('fs/promises')
const Dictionary = require('../../../models/dictionary')
const Entry = require('../../../models/entry')
const Extraction = require('../../../models/extraction')
const {
searchEntryIndex,
deleteEntryFromIndex,
@@ -9,6 +11,7 @@ const genEditorAllQuery = require('../../../models/helpers/search/generate-query
const { prepareEditorEntries } = require('../../../models/helpers/search')
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
const { minEntriesRequirementCheckAndAct } = require('../../helpers/dictionary')
const { getExportFilesPath } = require('../../../models/helpers/dictionary')
const dictionary = {}
@@ -115,6 +118,8 @@ dictionary.delete = async (req, res) => {
const dictionaryId = +req.params.dictionaryId
await Dictionary.delete(dictionaryId)
await deleteDictionaryEntriesFromIndex(dictionaryId)
const exportFilesPath = getExportFilesPath(dictionaryId)
await rm(exportFilesPath, { recursive: true, force: true })
res.end()
}
@@ -167,6 +172,56 @@ dictionary.listDomainLabels = async (req, res) => {
res.send({ page, numberOfAllPages, results })
}
dictionary.listFilteredDomainLabels = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const { dictionaryId } = req.params
const page = +req.query.p > 0 ? +req.query.p : 1
let { q } = req.query
if (!q) {
q = ''
}
const { pages_total: numberOfAllPages, results } =
await Dictionary.fetchFilteredPaginationDomainLabels(
dictionaryId,
q,
resultsPerPage,
page
)
res.append('page', page)
res.append('number-of-all-pages', numberOfAllPages)
res.render('utilities/response-pug-wrapper/domainLabelLister', {
dictionary: { id: dictionaryId },
numberOfAllPages,
results
})
}
dictionary.listSecondaryDomainData = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
let { q } = req.query
const page = +req.query.p > 0 ? +req.query.p : 1
if (!q) {
q = ''
}
const { pages_total: numberOfAllPages, results } =
await Dictionary.fetchFilteredSecondaryDomains(q, resultsPerPage, page)
res.append('page', page)
res.append('number-of-all-pages', numberOfAllPages)
res.render('utilities/response-pug-wrapper/secondaryDomainLister', {
page,
numberOfAllPages,
results
})
}
dictionary.listSecondaryDomains = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const page = +req.query.p > 0 ? +req.query.p : 1
@@ -176,14 +231,92 @@ dictionary.listSecondaryDomains = async (req, res) => {
res.send({ page, numberOfAllPages, results })
}
dictionary.extractionImport = async (req, res) => {
// TODO Import logic (Luka's task)
// const { id: dictionaryId, extractionId } = req.params
// const { from, to } = req.query
// const fromIndex = +from > 1 ? Math.floor(from) - 1 : 0
// const toIndex = Number.isInteger(+to) ? Math.abs(to) : undefined
// console.log({ dictionaryId, extractionId, fromIndex, toIndex })
res.send('IMPORTING')
dictionary.showImportFromFileForm = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const { dictionaryId } = req.params
const page = +req.query.p > 0 ? +req.query.p : 1
const { pages_total: numberOfAllPages, results } =
await Dictionary.fetchAllImports(dictionaryId, resultsPerPage, page)
res.send({ page, numberOfAllPages, results })
}
dictionary.showExportToFileForm = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const { dictionaryId } = req.params
const page = +req.query.p > 0 ? +req.query.p : 1
const { pages_total: numberOfAllPages, results } =
await Dictionary.fetchExports(dictionaryId, resultsPerPage, page)
res.send({ page, numberOfAllPages, results })
}
dictionary.importFromExtraction = async (req, res) => {
const { id: dictionaryId, extractionId } = req.params
const { from, to } = req.body
const fromIndex = +from > 1 ? Math.floor(from) - 1 : 0
const toIndex = Number.isInteger(+(to === '' ? undefined : to))
? Math.abs(to)
: undefined
// TODO Authentication, authorization, validation.
const termCandidatesToImport = await Extraction.fetchTermCandidatesSlice(
extractionId,
fromIndex,
toIndex
)
await Dictionary.importFromExtraction(
dictionaryId,
req.user.id,
termCandidatesToImport
)
await Dictionary.indexIntoSearchEngine(dictionaryId)
res.end()
}
dictionary.exportBegin = async (req, res) => {
const dictionaryId = req.params.id
const exportParams = {
isValidFilter:
req.body.isValidFilter === 'on' ? undefined : req.body.isValidFilter,
isPublishedFilter:
req.body.isPublishedFilter === 'on'
? undefined
: req.body.isPublishedFilter,
statusFilter:
req.body.statusFilter === 'complete'
? 'complete'
: req.body.statusFilter === 'inEdit'
? 'in_edit'
: undefined,
isTerminologyReviewedFilter:
req.body.isTerminologyReviewedFilter === 'on'
? undefined
: req.body.isTerminologyReviewedFilter,
isLanguageReviewedFilter:
req.body.isLanguageReviewedFilter === 'on'
? undefined
: req.body.isLanguageReviewedFilter,
exportFileFormat: req.body.exportFileFormat
}
const exportId = await Dictionary.beginExport(dictionaryId, exportParams)
res.end()
// Explicitcly catch any errors after the response has been sent
// since the the final error handler won't be able to send another.
try {
// TODO Consider delegating processing of export to a seperate process or at least a seperate thread (much like importing, extraction, ...).
await Dictionary.processExport(exportId)
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
}
}
module.exports = dictionary
+102 -52
View File
@@ -1,22 +1,27 @@
const { unlink, rm, mkdir } = require('fs/promises')
const { unlink, rm, mkdir, writeFile } = require('fs/promises')
const { promisify } = require('util')
const { URLSearchParams } = require('url')
const multer = require('multer')
const i18next = require('i18next')
const validator = require('validator')
const axios = require('axios')
const {
getExtractionFilesPath,
getDocumentsPath,
getStopTermsPath,
getConllusPath
getConllusPath,
getFileStats
} = require('../../../models/helpers/extraction')
const { checkIfcanBegin } = require('../../helpers/extraction')
const Extraction = require('../../../models/extraction')
const Domain = require('../../../models/domain')
const email = require('../../../models/email')
const { intoDbArray } = require('../../../models/helpers')
const { origin } = require('../../../config/keys')
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
const { origin, extractionApiOrigin } = require('../../../config/keys')
const {
DEFAULT_HITS_PER_PAGE,
TEMP_EXPORT_PATH
} = require('../../../config/settings')
const MAX_FILE_NAME_LENGTH = 100
const MAX_FILE_SIZE = 10 ** 9 // 1 GB
@@ -77,16 +82,19 @@ extraction.docsList = async (req, res) => {
extraction.docsUpdate = async (req, res) => {
try {
await parseExtractionFileBody(req, res)
const fileStats = await getFileStats(req.file.path)
res.send(fileStats)
} catch (error) {
if (
error instanceof multer.MulterError &&
error.code === 'LIMIT_FILE_SIZE'
) {
throw Error('File too large. Must not be over 1 GB.')
const customError = Error('File too large. Must not be over 1 GB.')
customError.displayInProd = true
throw customError
}
throw error
}
res.end()
}
extraction.docDelete = async (req, res) => {
@@ -108,16 +116,19 @@ extraction.stopTermsList = async (req, res) => {
extraction.stopTermsUpdate = async (req, res) => {
try {
await parseExtractionFileBody(req, res)
const fileStats = await getFileStats(req.file.path)
res.send(fileStats)
} catch (error) {
if (
error instanceof multer.MulterError &&
error.code === 'LIMIT_FILE_SIZE'
) {
throw Error('File too large. Must not be over 1 GB.')
const customError = Error('File too large. Must not be over 1 GB.')
customError.displayInProd = true
throw customError
}
throw error
}
res.end()
}
extraction.stopTermDelete = async (req, res) => {
@@ -141,7 +152,7 @@ extraction.ossSearch = [
...(ossParams.keywords && { kljucneBesede: ossParams.keywords }),
...(ossParams.domainUdk && { udk: ossParams.domainUdk })
})
const searchApiUrl = `http://rsdo.lhrs.feri.um.si:8080/oss/steviloBesedilPoIskanju?${searchParams}`
const searchApiUrl = `${extractionApiOrigin}/oss/steviloBesedilPoIskanju?${searchParams}`
const { data: documentCount } = await axios.get(searchApiUrl)
const canSave = documentCount && documentCount <= MAX_OSS_DOCUMENT_COUNT
@@ -183,28 +194,39 @@ extraction.begin = async (req, res) => {
res.send(timeStarted)
if (ossParams) {
// TODO This next method is only a temporary solution.
// TODO It should be called before response and its execution delegated to a seperate process or at least a seperate thread.
await Extraction.processOss(extractionId, ossParams.params)
} else {
// TODO This next method is only a temporary solution.
// TODO It should be called before response and its execution delegated to a seperate process or at least a seperate thread.
await Extraction.processOwn(extractionId, extractionName)
}
// Explicitcly catch any errors after the response has been sent
// since the the final error handler won't be able to send another.
try {
if (ossParams) {
// TODO This next method is only a temporary solution.
// TODO It should be called before response and its execution delegated to a seperate process or at least a seperate thread.
await Extraction.processOss(extractionId, ossParams.params)
} else {
// TODO This next method is only a temporary solution.
// TODO It should be called before response and its execution delegated to a seperate process or at least a seperate thread.
await Extraction.processOwn(extractionId, extractionName)
}
const extractionLink = new URL('/luscenje', origin)
const renderAsync = promisify(req.app.render.bind(req.app))
const authorEmail = await Extraction.fetchAuthorEmail(extractionId)
const emailHtml = await renderAsync('email/extraction-done', {
extractionName,
extractionLink
})
await email.send({
to: authorEmail,
subject: 'Luščenje končano',
html: emailHtml
})
const extractionLink = new URL('/luscenje', origin)
const renderAsync = promisify(req.app.render.bind(req.app))
const { email: authorEmail, language: authorLanguage } =
await Extraction.fetchAuthorData(extractionId)
const emailHtml = await renderAsync(
`email/extraction-done_${authorLanguage}`,
{
extractionName,
extractionLink
}
)
await email.send({
to: authorEmail,
subject: i18next.t('Luščenje končano', { lng: authorLanguage }),
html: emailHtml
})
} catch (error) {
// eslint-disable-next-line no-console
console.error(error)
}
}
extraction.duplicate = async (req, res) => {
@@ -214,13 +236,36 @@ extraction.duplicate = async (req, res) => {
}
extraction.termCandidatesExport = async (req, res) => {
// TODO CSV logic (Luka's task)
// const extractionId = req.params.id
// const { from, to } = req.query
// const fromIndex = +from > 1 ? Math.floor(from) - 1 : 0
// const toIndex = Number.isInteger(+to) ? Math.abs(to) : undefined
// console.log({ extractionId, fromIndex, toIndex })
res.download('public/images/help-amebis-logo-pug-demo.png')
// TODO More formats (CSV, TSV, TXT, ...)
const extractionId = req.params.id
const { from, to } = req.query
const fromIndex = +from > 1 ? Math.floor(from) - 1 : 0
const toIndex = Number.isInteger(+(to === '' ? undefined : to))
? Math.abs(to)
: undefined
// TODO Authentication, authorization, validation.
const termCandidatesToExport = await Extraction.fetchTermCandidatesSlice(
extractionId,
fromIndex,
toIndex
)
const exportFileName = `term_candidates_${extractionId}`
const exportFilePath = `${TEMP_EXPORT_PATH}/${exportFileName}`
await writeFile(
exportFilePath,
JSON.stringify({ terminoloski_kandidati: termCandidatesToExport })
)
const downloadAsync = promisify(res.download.bind(res))
try {
await downloadAsync(exportFilePath, 'term_candidates.json')
} finally {
await unlink(exportFilePath)
}
}
extraction.listFinishedForUser = async (req, res) => {
@@ -251,8 +296,11 @@ function extractionFileFilter(req, file, cb) {
fileType = 'stopTerms'
break
default:
return cb(Error('Invalid API endpoint'))
default: {
const customError = Error('Invalid API endpoint')
customError.displayInProd = true
return cb(customError)
}
}
const filenamePartsArray = file.originalname.split('.')
@@ -264,30 +312,32 @@ function extractionFileFilter(req, file, cb) {
(fileType === 'stopTerms' &&
fileExtension !== VALID_STOP_TERMS_FILE_EXTENSION)
) {
return cb(Error('Invalid file type'))
const customError = Error('Invalid file type')
customError.displayInProd = true
return cb(customError)
}
const fileName = filenamePartsArray.join('.')
if (!fileName || fileName.length > MAX_FILE_NAME_LENGTH) {
return cb(
Error(
`Filename must be between 1 and ${MAX_FILE_NAME_LENGTH} characters long.`
)
const customError = Error(
`Filename must be between 1 and ${MAX_FILE_NAME_LENGTH} characters long.`
)
customError.displayInProd = true
return cb(customError)
}
if (!validator.isAlphanumeric(fileName[0], 'sl-SI', { ignore: '_' })) {
return cb(
Error(
'Filename must begin with an alphanumeric character or an underscore.'
)
const customError = Error(
'Filename must begin with an alphanumeric character or an underscore.'
)
customError.displayInProd = true
return cb(customError)
}
if (!validator.isAlphanumeric(fileName, 'sl-SI', { ignore: ' _-.' })) {
return cb(
Error(
'Filename can only contain alphanumeric characters, spaces, underscores, minuses and periods.'
)
const customError = Error(
'Filename can only contain alphanumeric characters, spaces, underscores, minuses and periods.'
)
customError.displayInProd = true
return cb(customError)
}
req.fileType = fileType
+21
View File
@@ -66,6 +66,27 @@ exports.listMainEntries = async (req, res) => {
{}
)
// duplicated code below for filtering foreignent, optimize later
const categoriesLabels = Object.keys(entriesByCategory)
for (
let categoryIndex = 0;
categoryIndex < categoriesLabels.length;
categoryIndex++
) {
entriesByCategory[categoriesLabels[categoryIndex]] = entriesByCategory[
categoriesLabels[categoryIndex]
].map(entry => {
entry.foreignEntries = entry.foreignEntries?.filter(foreignEntry => {
if (filters.targetLanguages.length > 0) {
return filters.targetLanguages.includes(`${foreignEntry.lang.id}`)
} else {
return true
}
})
return entry
})
}
// res.send({ page, numberOfAllPages, entries })
res.append('page', page)
res.append('number-of-all-pages', numberOfAllPages)
+62 -18
View File
@@ -8,6 +8,7 @@ const { DEFAULT_HITS_PER_PAGE } = require('../config/settings')
const generateQuery = require('../models/helpers/search/generate-query')
const { searchConsultancyEntryIndex } = require('../models/search-engine')
const { prepareConsultancyEntries } = require('../models/helpers/search')
const { getInstanceSetting, intoDbArray } = require('../models/helpers')
// const { minEntriesRequirementCheckAndAct } = require('./helpers/dictionary')
const consultancy = {}
@@ -16,6 +17,9 @@ const consultancyAdmin = {}
consultancy.index = async (req, res) => {
req.indexHitPageAmount = '5'
res.locals.isOwnConsultancyEnabled =
(await getInstanceSetting('consultancy_type')) === 'own'
return await consultancyRequest(
req,
res,
@@ -25,17 +29,24 @@ consultancy.index = async (req, res) => {
}
consultancy.search = async (req, res) => {
res.locals.isOwnConsultancyEnabled =
(await getInstanceSetting('consultancy_type')) === 'own'
res.locals.queryKey = req.query.q
return await consultancyRequest(
req,
res,
'published',
'pages/consultancy/search'
'pages/consultancy/search',
req.t('Odgovori')
)
}
consultancy.specificQuestion = async (req, res) => {
const { id } = req.params
// TODO i18n TIME FORMAT
const entry = await ConsultancyEntry.fetchByIdWithFormattedTime(id)
// const author = await User.fetchUser(entry.authorId)
@@ -44,12 +55,13 @@ consultancy.specificQuestion = async (req, res) => {
entry.answerAuthors = entry.answerAuthors.filter(author => author !== '')
let authorString
// TODO I18n
if (entry.answerAuthors.length === 1) {
authorString = 'Avtor'
authorString = req.t('Avtor')
} else if (entry.answerAuthors.length === 2) {
authorString = 'Avtorja'
authorString = req.t('Avtorja')
} else {
authorString = 'Avtorji'
authorString = req.t('Avtorji')
}
entry.domain = allPrimaryDomains.filter(
@@ -62,18 +74,26 @@ consultancy.specificQuestion = async (req, res) => {
entry.domain = false
}
res.locals.isOwnConsultancyEnabled =
(await getInstanceSetting('consultancy_type')) === 'own'
res.render('pages/consultancy/item-details', {
allPrimaryDomains,
authorString,
entry
entry,
title: req.t('Odgovor')
})
}
consultancy.new = async (req, res) => {
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
res.locals.isOwnConsultancyEnabled =
(await getInstanceSetting('consultancy_type')) === 'own'
res.render('pages/consultancy/ask', {
allPrimaryDomains
allPrimaryDomains,
title: req.t('Novo vprašanje')
})
}
@@ -83,6 +103,7 @@ consultancyAdmin.new = async (req, res) => {
res,
'new',
'pages/consultancy/admin/index',
req.t('Novo'),
true,
false
)
@@ -95,7 +116,8 @@ consultancyAdmin.users = async (req, res) => {
res.render('pages/consultancy/admin/users', {
allPrimaryDomains,
users
users,
title: req.t('Svetovalci')
})
}
@@ -105,6 +127,7 @@ consultancyAdmin.rejected = async (req, res) => {
res,
'rejected',
'pages/consultancy/admin/rejected',
req.t('Zavrnjeno'),
true,
false
)
@@ -116,8 +139,10 @@ consultancyAdmin.published = async (req, res) => {
res,
'published',
'pages/consultancy/admin/published',
req.t('Objavljeno'),
true,
false
false,
'published'
)
}
@@ -127,6 +152,7 @@ consultancyAdmin.prepared = async (req, res) => {
res,
'review',
'pages/consultancy/admin/prepared',
req.t('Pripravljeno'),
true,
false
)
@@ -142,6 +168,7 @@ consultancyAdmin.inProgress = async (req, res) => {
res,
'in progress',
'pages/consultancy/admin/in-progress',
req.t('V delu'),
true,
false
)
@@ -171,6 +198,7 @@ consultancyAdmin.edit = async (req, res) => {
} 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 author = await User.fetchUser(entry.authorId)
@@ -186,7 +214,8 @@ consultancyAdmin.edit = async (req, res) => {
author,
isPublished,
// TODO Luka: I suspect this will not work as intended on staging or production environments. Test.
urlPrefix: req.protocol + '://' + req.get('host')
urlPrefix: req.protocol + '://' + req.get('host'),
title: req.t('Urejanje')
})
}
@@ -198,14 +227,14 @@ function dateMap(obj) {
return obj
}
async function mapDomainIdToDomainNameSlovene(obj) {
async function mapDomainIdToDomainNameSlovene(obj, t) {
try {
const area = await Domain.fetchById(
obj.domainPrimaryId ? obj.domainPrimaryId : obj.domainPrimaryIdInitial
)
obj.area = area.nameSl
} catch {
obj.area = 'Ni področja'
obj.area = t('Ni področja')
}
return obj
@@ -227,14 +256,14 @@ function mapInitialValuesAsEmpty(obj) {
return obj
}
async function mapEntryList(list) {
async function mapEntryList(list, t) {
return await Promise.all(
list.map(entry => {
let entity = utils.compose(dateMap, mapInitialValuesAsEmpty)(entry)
// TODO Each mapDomainIdToDomainNameSlovene call leads to one DB query.
// TODO Test if and what scenarios can lead to too many calls and how it can be avoided.
entity = utils.composeAsync(mapDomainIdToDomainNameSlovene)(entry)
entity = utils.composeAsync(mapDomainIdToDomainNameSlovene)(entry, t)
return entity
})
@@ -283,11 +312,26 @@ async function consultancyRequest(
res,
type,
url,
title = req.t('Svetovanje'),
isAdminPage = false,
privilegeToSeAll = true // this method seperates consultancy main from admin, so all results get visible TO ALL REGISTERED USERS, not just admins
) {
const searchString = req.query.q?.trim() ?? ''
let allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
/// filter selected domains for the prompt ///
// Not duplicates of this code arise...
const pdList = intoDbArray(req.query.pd, 'always')
allPrimaryDomains = allPrimaryDomains.map(entry => {
if (pdList.includes(`${entry.id}`)) {
entry.selected = true
}
return entry
})
/// //////////////////////////////////////////
let assignedConsultant
if (
@@ -333,11 +377,11 @@ async function consultancyRequest(
let entries = prepareConsultancyEntries(hits)
// console.log({ entries, numberOfAllHits, numberOfAllPages })
// TODO I18n - nameSl
entries = entries.map(entry => {
entry.primaryDomain = entry.primaryDomain
? entry.primaryDomain.nameSl
: 'nedefinirano'
: req.t('nedefinirano')
if (entry.assignedConsultants) {
entry.firstName = entry.assignedConsultants[0]?.firstName
@@ -365,8 +409,6 @@ async function consultancyRequest(
return entry
})
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
// const entryList = await mapEntryList(inProgressEntryList)
const userList = await User.fetchConsultants()
@@ -401,7 +443,9 @@ async function consultancyRequest(
entries, // entryList,
userList,
numberOfAllPages,
queryCount: numberOfAllHits
queryCount: numberOfAllHits,
consultancyPageType: type,
title
})
}
+88 -37
View File
@@ -9,7 +9,8 @@ const Comment = require('../models/comment')
const genEditorAllQuery = require('../models/helpers/search/generate-query/editor/all')
const { searchEntryIndex } = require('../models/search-engine')
const { prepareEditorEntries } = require('../models/helpers/search')
const { getInstanceSetting } = require('../models/helpers')
const { getInstanceSetting, intoDbArray } = require('../models/helpers')
const { getExportFilesPath } = require('../models/helpers/dictionary')
const { DEFAULT_HITS_PER_PAGE, DATA_FILES_PATH } = require('../config/settings')
const {
statusChangeCheckAndAct,
@@ -17,6 +18,7 @@ const {
} = require('./helpers/dictionary')
const SFDSuggestionImporter = require('./helpers/search-filter-data-suggestion-importer')
const Extraction = require('../models/extraction')
const { isGeneratorFunction } = require('util/types')
const importFileBodyParser = multer({
dest: `${DATA_FILES_PATH}/dict_import_temp`,
@@ -39,7 +41,7 @@ dictionary.list = async (req, res) => {
dictionaries = await Dictionary.fetchAllByUser(req.user.id)
}
res.render('pages/dictionaries/list', {
title: 'Seznam slovarjev',
title: req.t('Seznam slovarjev'),
dictionaries
})
}
@@ -51,11 +53,11 @@ dictionary.new = async (req, res) => {
await Promise.all([
Dictionary.fetchAllPrimaryDomains(),
Dictionary.fetchAllApprovedSecondaryDomains(),
Dictionary.fetchAllLanguages(language)
Dictionary.fetchAllLanguages(language, true)
])
res.render('pages/dictionaries/new', {
title: 'Nov slovar',
title: req.t('Nov slovar'),
allPrimaryDomains,
allSecondaryDomains,
allLanguages
@@ -82,7 +84,7 @@ dictionary.editDescription = async (req, res) => {
])
res.render('pages/dictionaries/description', {
title: 'Ime in opis',
title: req.t('Osnovni podatki'),
allPrimaryDomains,
allSecondaryDomains,
dictionary,
@@ -125,7 +127,7 @@ dictionary.editUsers = async (req, res) => {
}
res.render(viewPath, {
title: 'Uporabniki',
title: req.t('Uporabniki'),
dictionary,
userRights,
entriesCount,
@@ -142,6 +144,7 @@ dictionary.updateUsers = async (req, res) => {
const newDictStatus = await determineNewStatus(isPublished)
// TODO I18n - nameSl
const { nameSl, status: oldDictStatus } = await Dictionary.fetchEditUsers(
dictionaryId
)
@@ -172,12 +175,13 @@ dictionary.updateUsers = async (req, res) => {
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)
Dictionary.fetchAllLanguages(language, true)
])
let viewPath
@@ -190,7 +194,7 @@ dictionary.editStructure = async (req, res) => {
}
res.render(viewPath, {
title: 'Struktura slovarskega sestavka',
title: req.t('Struktura slovarskega sestavka'),
dictionary,
associatedLanguages,
allLanguages
@@ -223,7 +227,7 @@ dictionary.editAdvanced = async (req, res) => {
}
res.render(viewPath, {
title: 'Napredno',
title: req.t('Napredno'),
dictionary: { id: req.params.dictionaryId },
dictionaryName
})
@@ -251,7 +255,7 @@ dictionary.comments = async (req, res) => {
}
res.render(viewPath, {
title: 'Komentarji',
title: req.t('Komentarji'),
numberOfAllPages,
dictionary: { id: req.params.dictionaryId },
comments,
@@ -261,10 +265,12 @@ dictionary.comments = async (req, res) => {
dictionary.showImportFromFileForm = async (req, res) => {
const { dictionaryId } = req.params
const [imports, dictionaryName] = await Promise.all([
Dictionary.fetchAllImports(dictionaryId),
Dictionary.fetchName(dictionaryId)
])
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const [{ pages_total: numberOfAllPages, results }, dictionaryName] =
await Promise.all([
Dictionary.fetchAllImports(dictionaryId, resultsPerPage, 1),
Dictionary.fetchName(dictionaryId)
])
let viewPath
switch (req.baseUrl) {
case '/slovarji':
@@ -275,9 +281,10 @@ dictionary.showImportFromFileForm = async (req, res) => {
}
res.render(viewPath, {
title: 'Uvoz iz datoteke',
title: req.t('Uvoz iz datoteke'),
dictionary: { id: dictionaryId },
imports,
numberOfAllPages,
results,
dictionaryName
})
}
@@ -289,7 +296,7 @@ dictionary.listAdminDictionaries = async (req, res) => {
await Dictionary.fetchAllAdminDictionaries(resultsPerPage, 1)
res.render('pages/admin/dictionaries-list', {
title: 'Struktura slovarjev',
title: req.t('Seznam slovarjev'),
numberOfAllPages,
results
})
@@ -312,7 +319,7 @@ dictionary.adminEditDescription = async (req, res) => {
])
res.render('pages/admin/dictionary-description', {
title: 'Podatki',
title: req.t('Osnovni podatki'),
allPrimaryDomains,
allSecondaryDomains,
dictionary,
@@ -364,11 +371,11 @@ dictionary.showImportFromExtractionForm = async (req, res) => {
switch (req.baseUrl) {
case '/slovarji':
viewPath = 'pages/dictionaries/extraction-import'
title = 'Uvoz'
title = req.t('Uvoz iz luščilnika')
break
case '/admin':
viewPath = 'pages/admin/dictionary-extraction-import'
title = 'Uvoz luščenje'
title = req.t('Uvoz iz luščilnika')
}
res.render(viewPath, {
@@ -381,7 +388,12 @@ dictionary.showImportFromExtractionForm = async (req, res) => {
dictionary.showExportToFileForm = async (req, res) => {
const { dictionaryId } = req.params
const dictionaryName = await Dictionary.fetchName(dictionaryId)
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const [dictionaryName, { pages_total: numberOfAllPages, results }] =
await Promise.all([
Dictionary.fetchName(dictionaryId),
Dictionary.fetchExports(dictionaryId, resultsPerPage, 1)
])
let viewPath
switch (req.baseUrl) {
case '/slovarji':
@@ -390,11 +402,12 @@ dictionary.showExportToFileForm = async (req, res) => {
case '/admin':
viewPath = 'pages/admin/dictionary-export'
}
res.render(viewPath, {
title: 'Izvoz',
dictionary: { id: req.params.dictionaryId },
dictionaryName
title: req.t('Izvoz'),
dictionary: { id: dictionaryId },
dictionaryName,
numberOfAllPages,
results
})
}
@@ -418,7 +431,7 @@ dictionary.editDomainLabels = async (req, res) => {
}
res.render(viewPath, {
title: 'Področne oznake',
title: req.t('Področne oznake'),
dictionary: { id: dictionaryId },
numberOfAllPages,
results,
@@ -448,7 +461,7 @@ dictionary.showContent = async (req, res) => {
const terms = prepareEditorEntries(hits)
res.render('pages/dictionaries/content', {
title: 'Vsebina slovarja',
title: req.t('Vsebina slovarja'),
terms,
canPublishEntriesInEdit,
dictionaryName,
@@ -466,7 +479,7 @@ dictionary.showSecondaryDomains = async (req, res) => {
await Dictionary.fetchAllSecondaryDomains(resultsPerPage, 1)
res.render('pages/admin/areas', {
title: 'Podpodročja',
title: req.t('Področne oznake'),
numberOfAllPages,
results
})
@@ -501,12 +514,27 @@ dictionary.dictionaryList = async (req, res) => {
)
const numberOfAllHits = parseInt(
(await Dictionary.fetchAllDictionariesCount()).count
(await Dictionary.fetchAllDictionariesPublishedCount()).count
)
const numberOfAllPages = Math.ceil(numberOfAllHits / hitsPerPage)
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
/*
/// filter selected domains for the prompt ///
// Not duplicates of this code arise...
const pdList = intoDbArray(req.query.pd, 'always')
allPrimaryDomains = allPrimaryDomains.map(entry => {
if (pdList.includes(`${entry.id}`)) {
entry.selected = true
}
return entry
})
/// //////////////////////////////////////////
*/
dictionaries = dictionaries.map(e => {
if (!e.portalcode) {
e.portalcode = defaultportalcode
@@ -518,7 +546,7 @@ dictionary.dictionaryList = async (req, res) => {
const isDictionaryListPage = true
res.render('pages/dictionaries/dictlist', {
title: 'Seznam slovarjev',
title: req.t('Seznam slovarjev'),
dictionaries,
allPrimaryDomains,
numberOfAllPages,
@@ -532,6 +560,7 @@ dictionary.dictionaryList = async (req, res) => {
dictionary.dictionaryDetails = async (req, res) => {
const { absolutePrevPath, sentFromEntryId } = req.query
const dictId = req.params.dictionaryId
const title = req.t('O slovarju')
const {
allPrimaryDomains,
sourceLanguages,
@@ -555,7 +584,7 @@ dictionary.dictionaryDetails = async (req, res) => {
// check if it is a local dictionary
if (!dictionaryData.portalname && !dictionaryData.portalcode) {
dictionaryData[0].portalname = await getInstanceSetting('portal_name')
dictionaryData[0].portalname = await getInstanceSetting('portal_name_sl')
dictionaryData[0].portalcode = await getInstanceSetting('portal_code')
}
@@ -579,7 +608,7 @@ dictionary.dictionaryDetails = async (req, res) => {
)
const structData = {
prevWindowTitle: 'Nazaj',
prevWindowTitle: req.t('Nazaj'),
dictName: dictionaryData[0].dictionarysl,
portalCode: dictionaryData[0].portalcode,
portalName: dictionaryData[0].portalname,
@@ -589,13 +618,14 @@ dictionary.dictionaryDetails = async (req, res) => {
languages: reducedData.languages ? reducedData.languages.join(', ') : ''
}
// TODO I18n
if (reducedData.author) {
if (reducedData.author.length > 2) {
structData.authorLabel = 'Avtorji'
structData.authorLabel = req.t('Avtorji')
} else if (reducedData.author.length === 2) {
structData.authorLabel = 'Avtorja'
structData.authorLabel = req.t('Avtorja')
} else if (reducedData.author.length === 1) {
structData.authorLabel = 'Avtor'
structData.authorLabel = req.t('Avtor')
}
}
@@ -623,7 +653,8 @@ dictionary.dictionaryDetails = async (req, res) => {
finalData,
numberOfAllPages,
comments,
commentCount
commentCount,
title
}) // todo
}
@@ -680,8 +711,28 @@ dictionary.importFromFile = async (req, res) => {
}
}
dictionary.exportDownload = async (req, res) => {
// TODO Add authentication and authorization.
const { exportId } = req.params
const { exportStatus, dictionaryId, nameString, timeString, fileFormat } =
await Dictionary.fetchExportDownloadMetadata(exportId)
if (exportStatus !== 'finished') {
throw Error("Can't request file for unfinished export")
}
const exportFilesPath = getExportFilesPath(dictionaryId)
const exportFilePath = `${exportFilesPath}/${exportId}`
const exportFileName = `${nameString}_${timeString}.${fileFormat}`
res.download(exportFilePath, exportFileName)
}
function importFileFilter(req, file, cb) {
if (file.mimetype !== 'text/xml') return cb(Error('Invalid file type'))
if (file.mimetype !== 'text/xml') {
const customError = Error('Invalid file type')
customError.displayInProd = true
return cb(customError)
}
cb(null, true)
}
+10 -4
View File
@@ -29,7 +29,10 @@ extraction.list = async (req, res) => {
)
}
res.render('pages/extraction/list', { title: 'Luščenje seznam', extractions })
res.render('pages/extraction/list', {
title: req.t('Seznam luščenj'),
extractions
})
}
extraction.create = async (req, res) => {
@@ -39,7 +42,7 @@ extraction.create = async (req, res) => {
return res.redirect(303, 'back')
}
const extractionName = `Luščenje ${extractionCount + 1}`
const extractionName = req.t('Luščenje') + `${extractionCount + 1}`
const { extractionType } = req.body
let extractionId
@@ -81,7 +84,7 @@ extraction.edit = async (req, res) => {
extraction.keywords = intoDbArray(params.keywords, 'always')
res.render('pages/extraction/edit-oss', {
title: 'KAS + dokumenti',
title: req.t('Besedila'),
id: extractionId,
extraction,
allPrimaryDomains,
@@ -93,7 +96,7 @@ extraction.edit = async (req, res) => {
Extraction.fetchAllStopTermsFilesStats(extractionId)
])
res.render('pages/extraction/edit-own', {
title: 'Besedila',
title: req.t('Besedila'),
id: extractionId,
extraction,
extractionDocuments,
@@ -117,6 +120,7 @@ extraction.docsEdit = async (req, res) => {
)
res.render('pages/extraction/docs-edit', {
title: req.t('Besedila'),
id: extractionId,
extractionDocuments
})
@@ -129,6 +133,7 @@ extraction.stopTermsEdit = async (req, res) => {
)
res.render('pages/extraction/stop-terms-edit', {
title: req.t('Stop termini'),
id: extractionId,
stopTermsFiles
})
@@ -145,6 +150,7 @@ extraction.listTermCandidates = async (req, res) => {
const firstPageOfTermCandidates = termCandidates.slice(0, hitsPerPage)
res.render('pages/extraction/term-candidates', {
title: req.t('Terminološki kandidati'),
extractionId,
termCandidatesJson,
firstPageOfTermCandidates,
+13 -3
View File
@@ -1,3 +1,5 @@
/* global __ */
const { getInstanceSetting } = require('../../models/helpers')
const Dictionary = require('../../models/dictionary')
const cache = require('../../models/cache')
@@ -56,6 +58,7 @@ exports.minEntriesRequirementCheckAndAct = {
if (!isBelowMinEntriesThreshold || wasEmailAlreadySent) return
// Prepare and send notification emails.
// TODO I18n - nameSl
const [nameSl, adminEmails, dictionariesAdminEmails] = await Promise.all([
Dictionary.fetchName(dictionaryId),
Dictionary.fetchAdminEmails(dictionaryId),
@@ -68,14 +71,16 @@ 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
})
// TODO i18n - What language are the email title and content (we already have email translated)
await email.send({
to: allEmails,
subject: 'Obvestilo o številu gesel',
subject: __('Obvestilo o številu gesel'),
html: emailHtml
})
@@ -118,6 +123,7 @@ 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,
@@ -133,7 +139,7 @@ exports.statusChangeCheckAndAct = {
const dictionariesAdminEmails =
await Dictionary.fetchDictionariesAdminEmails()
const type = 'unpublish'
// TODO I18n - nameSl
await renderAndSendStatusChangeEmails(
appRef,
type,
@@ -151,6 +157,7 @@ exports.statusChangeCheckAndAct = {
const type =
isApprovalRequired === 'T' ? 'publish-approval' : 'publish-no-approval'
// TODO I18n - nameSl
await renderAndSendStatusChangeEmails(
appRef,
type,
@@ -171,6 +178,7 @@ exports.statusChangeCheckAndAct = {
user
) {
if (statusOld === 'reviewed' && statusNew !== 'reviewed') {
// TODO I18n - nameSl
const [nameSl, adminEmails] = await Promise.all([
Dictionary.fetchName(dictionaryId),
Dictionary.fetchAdminEmails(dictionaryId)
@@ -189,6 +197,7 @@ exports.statusChangeCheckAndAct = {
}
// Helper function used by statusChangeCheckAndAct methods.
// TODO I18n - nameSl
async function renderAndSendStatusChangeEmails(
appRef,
type,
@@ -203,9 +212,10 @@ async function renderAndSendStatusChangeEmails(
nameSl
})
// TODO i18n - What language are the email title and content (we already have email translated)
await email.send({
to: targetEmails,
subject: 'Sprememba stanja slovarja',
subject: __('Sprememba stanja slovarja'),
html: emailHtml
})
}
@@ -8,12 +8,15 @@ helper.initialize = async () => {
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 = initializers.sourceLanguages.filter(
initializers.targetLanguages = (
await Dictionary.fetchAllLanguages(language)
).filter(
// drop slovene language
l => l.id !== 32
)
@@ -22,7 +25,7 @@ helper.initialize = async () => {
initializers.portals = []
initializers.portals.push({
name: await getInstanceSetting('portal_name'),
name: await getInstanceSetting('portal_name_sl'),
code: await getInstanceSetting('portal_code')
})
+211 -17
View File
@@ -2,12 +2,16 @@ const Entry = require('../models/entry')
const { getInstanceSetting } = require('../models/helpers')
const Dictionary = require('../models/dictionary')
const Comment = require('../models/comment')
const { searchEntryIndex } = require('../models/search-engine')
const {
searchEntryIndex,
searchConsultancyEntryIndex
} = require('../models/search-engine')
const { intoDbArray } = require('../models/helpers')
const {
prepareEntries,
prepareAggregation,
prepareSeachFilterData
prepareSeachFilterData,
prepareConsultancyEntries
} = require('../models/helpers/search')
const generateQuery = require('../models/helpers/search/generate-query')
const { DEFAULT_HITS_PER_PAGE } = require('../config/settings')
@@ -16,6 +20,7 @@ const User = require('../models/user')
// TODO Luka (note to self): Measure performance, consider caching.
exports.index = async (req, res) => {
const { language } = req
const {
allPrimaryDomains,
sourceLanguages,
@@ -30,8 +35,10 @@ exports.index = async (req, res) => {
englishLanguageEnabled
)
const portalName = await getInstanceSetting('portal_name')
const portalDescription = await getInstanceSetting('portal_description')
const portalName = await getInstanceSetting(`portal_name_${language}`)
const portalDescription = await getInstanceSetting(
`portal_description_${language}`
)
const isRoot = true
@@ -53,7 +60,9 @@ exports.search = async (req, res) => {
if (!searchString) return res.redirect('/')
const {
const title = req.t('Iskanje')
let {
allPrimaryDomains,
sourceLanguages,
targetLanguages,
@@ -81,6 +90,32 @@ exports.search = async (req, res) => {
true
)
// Consultancy
const hitsQueryConsultancy = generateQuery.consultancy(
searchString,
{
status: 'published',
primaryDomain: filters.primaryDomains[0]
},
hitsPerPage,
page
)
const hitsConsultancy = await searchConsultancyEntryIndex(
hitsQueryConsultancy
)
const consultancyHits = hitsConsultancy.body.hits.total.value
const consultancyURL = `/svetovanje/iskanje?q=${searchString}${
filters.primaryDomains.length > 0 ? '&pd=' + filters.primaryDomains[0] : '' // consultancy filtering only supports one domain
}`
// console.log(entries)
//
const [hits, aggregationRaw] = await Promise.all([
searchEntryIndex(hitsQuery),
searchEntryIndex(aggregateQuery)
@@ -143,6 +178,30 @@ exports.search = async (req, res) => {
sources: false
}
// Keep selected items on refresh
sourceLanguages = addSelectedIdentifierToFilters(
sourceLanguages,
filters.sourceLanguages
)
targetLanguages = addSelectedIdentifierToFilters(
targetLanguages,
filters.targetLanguages
)
allPrimaryDomains = addSelectedIdentifierToFilters(
allPrimaryDomains,
filters.primaryDomains
)
allDictionaryNames = addSelectedIdentifierToFilters(
allDictionaryNames,
filters.dictionaries
)
/// //////////////
portals = addSelectedIdentifierToPortals(portals, filters.sources)
if (count < 1) {
return res.render('pages/search/no-results', {
allPrimaryDomains,
@@ -154,6 +213,8 @@ exports.search = async (req, res) => {
entriesByCategory,
searchFilterData,
disabledSideMenuFilters,
consultancyHits,
consultancyURL,
// allAggregation,
numberOfAllHits,
numberOfAllPages,
@@ -161,6 +222,74 @@ exports.search = async (req, res) => {
})
}
// duplicated code below for filtering foreignentries, optimize later
// Below code filters target languages based on source and target filters for each category
const categoriesLabels = Object.keys(entriesByCategory)
for (
let categoryIndex = 0;
categoryIndex < categoriesLabels.length;
categoryIndex++
) {
entriesByCategory[categoriesLabels[categoryIndex]] = entriesByCategory[
categoriesLabels[categoryIndex]
].map(entry => {
entry.foreignEntries = entry.foreignEntries?.filter(foreignEntry => {
if (filters.targetLanguages.length > 0) {
if (aggregation.sourceLanguages.length === 1) {
filters.sourceLanguages = [aggregation.sourceLanguages[0].id]
}
return (
filters.targetLanguages.includes(`${foreignEntry.lang.id}`) ||
filters.sourceLanguages?.includes(`${foreignEntry.lang.id}`)
)
} else {
return true
}
})
return entry
})
}
// Disable target languages logic
if (
filters.sourceLanguages.length > 1 || // more or equal than 2 source languages
(filters.sourceLanguages.length < 1 && // No filters, but more than 1 source language filters returned
aggregation.sourceLanguages.length > 1) ||
(aggregation.targetLanguages.length === 1 &&
aggregation.sourceLanguages.length === 1 &&
aggregation.targetLanguages[0].id === aggregation.sourceLanguages[0].id) //
) {
// predicate 1 Disable if 2 or more filters in Source languages are selected
// predicate 2 If none are selected, then check if displayed filters are more than 2
aggregation.targetLanguages = []
filters.targetLanguages = []
targetLanguages = []
searchFilterData.targetLanguages = []
disabledSideMenuFilters.targetLanguages = true
}
// remove source language from target language
filters.sourceLanguages.forEach(entry => {
if (
aggregation.targetLanguages.filter(toFilter => toFilter.id === entry)
.length > 0
) {
aggregation.targetLanguages = aggregation.targetLanguages.filter(
toFilter => toFilter.id !== entry
)
searchFilterData.targetLanguages =
searchFilterData.targetLanguages.filter(
toFilter => toFilter.id !== entry
)
}
})
if (aggregation.targetLanguages.length < 0) {
searchFilterData.targetLanguages = []
disabledSideMenuFilters.targetLanguages = true
}
// TODO Add a page title?
res.render('pages/search/results', {
allPrimaryDomains,
@@ -172,15 +301,19 @@ exports.search = async (req, res) => {
entriesByCategory,
searchFilterData,
disabledSideMenuFilters,
consultancyHits,
consultancyURL,
// allAggregation,
numberOfAllHits,
numberOfAllPages,
page
page,
title
})
}
exports.entryDetails = async (req, res) => {
const termId = req.params.entryId
const title = req.t('Termin')
/* const [entry, domainLabels] = await Promise.all([
Entry.fetchFullWithOrderedForeignLanguages(termId),
@@ -188,12 +321,16 @@ exports.entryDetails = async (req, res) => {
]) */
const entry = await Entry.fetchFullWithOrderedForeignLanguages(termId)
/* const entryData = {
entry
// allDomainLabelsJoined: domainLabels.map(e => e.name).join(', ')
} */
// If external url, just redirect
if (entry.external_url) {
return res.redirect(entry.external_url)
}
// unnecessary legacy assigment, refactor when time is available
const entryData = entry
@@ -228,8 +365,8 @@ exports.entryDetails = async (req, res) => {
///
// check if it is a local dictionary
if (!dictionaryData.portalname && !dictionaryData.portalcode) {
dictionaryData[0].portalname = await getInstanceSetting('portal_name')
if (!dictionaryData.portalnamesl && !dictionaryData.portalcode) {
dictionaryData[0].portalname = await getInstanceSetting('portal_name_sl')
dictionaryData[0].portalcode = await getInstanceSetting('portal_code')
}
@@ -258,7 +395,7 @@ exports.entryDetails = async (req, res) => {
// struct data contains important data and re-maps for unification (maybe refactor later)
const structData = {
termId: termId,
prevWindowTitle: 'Iskanje',
prevWindowTitle: req.t('Iskanje'),
prevHref: '/iskanje',
portalCode: dictionaryData[0].portalcode,
portalName: dictionaryData[0].portalname,
@@ -270,13 +407,14 @@ exports.entryDetails = async (req, res) => {
languages: reducedData.languages ? reducedData.languages.join(', ') : ''
}
// TODO I18n
if (reducedData.author) {
if (reducedData.author.length > 2) {
structData.authorLabel = 'Avtorji'
structData.authorLabel = req.t('Avtorji')
} else if (reducedData.author.length === 2) {
structData.authorLabel = 'Avtorja'
structData.authorLabel = req.t('Avtorja')
} else if (reducedData.author.length === 1) {
structData.authorLabel = 'Avtor'
structData.authorLabel = req.t('Avtor')
}
}
@@ -321,27 +459,63 @@ exports.entryDetails = async (req, res) => {
selectedDomainLabelsForEntryString,
numberOfAllPages,
comments,
commentCount
commentCount,
title
})
}
exports.myProfile = async (req, res) => {
res.render('pages/profile/my-profile', { title: 'Moj račun' })
res.render('pages/profile/my-profile', { title: req.t('Osnovni podatki') })
}
exports.deleteProfile = async (req, res) => {
res.render('pages/profile/delete-profile', { title: req.t('Izbriši račun') })
}
exports.changePassword = async (req, res) => {
res.render('pages/profile/change-password', { title: 'Spremeni geslo' })
res.render('pages/profile/change-password', {
title: req.t('Spremeni geslo')
})
}
exports.resetPassword = async (req, res) => {
const { token } = req.query
// console.log(token)
res.render('pages/reset-password/reset-password', {
// title: 'Pozabljeno geslo'
isValidToken: token === '123',
token
})
}
exports.userSettings = async (req, res) => {
const hitsPerPageArr = await User.fetchAllowedHitsPerPage()
res.render('pages/profile/change-profile-settings', {
title: 'Nastavitve računa',
title: req.t('Nastavitve računa'),
hitsPerPageArr,
hitsForUser: req.user?.hitsPerPage
})
}
exports.changeUserLanguage = async (req, res) => {
const { languageCode } = req.params
const validCodes = ['sl', 'en']
if (!validCodes.includes(languageCode)) {
throw Error(`Invalid language: ${languageCode}`)
}
const { user } = req
if (user) {
await User.updateLanguage(user.id, languageCode)
} else {
req.session.language = languageCode
}
res.redirect('/')
}
function mergeDomains(
domainList,
aggregationFn = (acc, n) => {
@@ -384,3 +558,23 @@ function filterResults(entries) {
return [termLst, ftermLst, otherLst]
}
function addSelectedIdentifierToFilters(source, selectedIDs) {
return source.map(entry => {
if (selectedIDs.includes(`${entry.id}`)) {
entry.selected = true
}
return entry
})
}
function addSelectedIdentifierToPortals(source, selectedIDs) {
return source.map(entry => {
if (selectedIDs.includes(`${entry.code}`)) {
entry.selected = true
}
return entry
})
}
+12 -9
View File
@@ -8,7 +8,7 @@ portal.instanceSettings = async (req, res) => {
const portal = await Portal.fetchInstanceSettings()
res.render('pages/admin/portal', {
title: 'Nastavitve portala',
title: req.t('Nastavitve portala'),
portal
})
}
@@ -24,7 +24,7 @@ portal.instanceDictSettings = async (req, res) => {
const dictionary = await Portal.fetchInstanceDictSettings()
res.render('pages/admin/settings-dictionaries', {
title: 'Nastavitve slovarjev',
title: req.t('Nastavitve slovarjev'),
dictionary
})
}
@@ -39,7 +39,7 @@ portal.updateInstanceDictSettings = async (req, res) => {
portal.instanceConsultancySettings = async (req, res) => {
const consultancy = await Portal.fetchInstanceConsultancySettings()
res.render('pages/admin/portal-consultancy-settings', {
title: 'Nastavitve svetovalnice',
title: req.t('Nastavitve svetovalnice'),
consultancy
})
}
@@ -53,7 +53,7 @@ portal.updateInstanceConusltacySettings = async (req, res) => {
portal.new = async (req, res) => {
res.render('pages/admin/new-connection', {
title: 'Nova povezava'
title: req.t('Nova povezava')
})
}
@@ -61,7 +61,7 @@ portal.list = async (req, res) => {
const allLinkedPortals = await Portal.fetchAll()
res.render('pages/admin/connections-list', {
title: 'Seznam povezav',
title: req.t('Seznam povezav'),
allLinkedPortals
})
}
@@ -70,7 +70,10 @@ portal.fetchPortal = async (req, res) => {
const portalId = req.params.portalId
const portal = await Portal.fetchPortal(portalId)
res.render('pages/admin/portal-edit', { title: 'Uredi povezavo', portal })
res.render('pages/admin/portal-edit', {
title: req.t('Uredi povezavo'),
portal
})
}
portal.updatePortal = async (req, res) => {
@@ -88,7 +91,7 @@ portal.fetchSelectedLinkedDictionaries = async (req, res) => {
await Portal.fetchSelectedLinkedDictionaries(linkedId, resultsPerPage, 1)
res.render('pages/admin/portal-list-dict', {
title: 'Slovarji portala',
title: req.t('Slovarji portala'),
linkedId,
numberOfAllPages,
results
@@ -108,7 +111,7 @@ portal.fetchAllLinkedDictionaries = async (req, res) => {
await Portal.fetchAllLinkedDictionaries(resultsPerPage, 1)
res.render('pages/admin/portals-all-linked-dictionaries', {
title: 'Povezani',
title: req.t('Povezani slovarji'),
numberOfAllPages,
results
})
@@ -130,7 +133,7 @@ portal.comments = async (req, res) => {
1
)
res.render('pages/admin/comments', {
title: 'Komentarji',
title: req.t('Komentarji'),
numberOfAllPages,
dictionary: { id: req.params.dictionaryId },
comments
+29 -9
View File
@@ -12,7 +12,10 @@ const user = {}
user.register = async (req, res) => {
// TODO Add validation.
const userId = await User.create(req.body)
const userId = await User.create({
...req.body,
language: req.session.language
})
const activationToken = (await RandomBytesAsync(32)).toString('hex')
await User.saveActivationToken(userId, activationToken)
const { email: userEmail, username } = req.body
@@ -20,16 +23,16 @@ user.register = async (req, res) => {
activationLink.searchParams.set('token', activationToken)
activationLink = activationLink.href
const renderAsync = promisify(req.app.render.bind(req.app))
const emailHtml = await renderAsync('email/user-activation', {
const emailHtml = await renderAsync(`email/user-activation_${req.language}`, {
username,
activationLink
})
await email.send({
to: userEmail,
subject: 'Aktivacija računa',
subject: req.t('Aktivacija računa'),
html: emailHtml
})
res.send('Registracija uspešna')
res.send(req.t('Registracija uspešna'))
}
user.activateAccount = async (req, res) => {
@@ -39,6 +42,12 @@ user.activateAccount = async (req, res) => {
await User.activateAccount(user)
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.redirect('/')
}
@@ -46,8 +55,9 @@ user.login = async (req, res, next) => {
passport.authenticate(
'local',
{
badRequestMessage:
badRequestMessage: req.t(
'Nepravilno uporabniško ime, elektronski naslov ali geslo.'
)
},
async (err, user, info) => {
if (err) return next(err)
@@ -69,7 +79,12 @@ user.login = async (req, res, next) => {
res.cookie('remember_me', rememberMeToken, rememberMeCookieSettings)
}
res.send('Prijava uspešna')
if (req.session.language) {
await User.updateLanguage(user.id, req.session.language)
delete req.session.language
}
res.send(req.t('Prijava uspešna'))
}
)(req, res, next)
}
@@ -82,6 +97,8 @@ user.logout = async (req, res) => {
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
@@ -96,7 +113,7 @@ user.list = async (req, res) => {
1
)
res.render('pages/admin/user-list', {
title: 'Seznam slovarjeva',
title: req.t('Seznam uporabnikov'),
numberOfAllPages,
results
})
@@ -104,7 +121,10 @@ user.list = async (req, res) => {
user.listAllWithPortalRoles = async (req, res) => {
const users = await User.fetchAllWithPortalRoles()
res.render('pages/admin/user-portals', { title: 'Seznam slovarjev', users })
res.render('pages/admin/user-portals', {
title: req.t('Skrbniki portala'),
users
})
}
user.findByUsernameOrEmail = async (req, res) => {
@@ -125,7 +145,7 @@ user.adminEdit = async (req, res) => {
User.fetchUserRoles(userId)
])
res.render('pages/admin/user-edit', {
title: 'Urejanje uporabnikov',
title: req.t('Uporabnik'),
userData,
userRoles
})