Initial commit

This commit is contained in:
Luka Romih
2022-12-07 04:43:36 +01:00
commit 257f3c354f
496 changed files with 55373 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
const Comment = require('../../../models/comment')
const Entry = require('../../../models/entry')
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
exports.listComments = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const page = +req.query.p > 0 ? +req.query.p : 1
const filters = { ctxType: req.query.ctx_type, ctxId: req.query.ctx_id }
if (filters.ctxId === 'null') {
filters.ctxId = null
}
const {
pages_total: numberOfAllPages,
comments,
comment_count: commentCount
} = await Comment.list(filters, req.user, resultsPerPage, page)
res.send({ page, numberOfAllPages, comments, commentCount })
}
exports.createComment = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const receivedComment = req.body
const { ctxType, ctxId } = receivedComment
await Comment.create(receivedComment, req.user?.id)
const filters = { ctxType, ctxId }
const { pages_total: pagesTotal, comments } = await Comment.list(
filters,
req.user,
resultsPerPage,
'last'
)
if (ctxType === 'entry_dict_int' || ctxType === 'entry_dict_ext') {
await Entry.indexIntoSearchEngine(ctxId)
}
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
await Comment.updateStatus(commentId, commentStatus)
res.send('Visibility changed')
}
+321
View File
@@ -0,0 +1,321 @@
const ConsultancyEntry = require('../../../models/consultancy-entry')
const User = require('../../../models/user')
const { promisify } = require('util')
const {
deleteConsultancyEntryFromIndex
} = require('../../../models/search-engine')
const email = require('../../../models/email')
const helper = require('../../../models/helpers')
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.createQuestion = async (req, res) => {
const q = req.body
const consultancyEntry = {}
consultancyEntry.status = 'new'
consultancyEntry.authorId = req.user.id
consultancyEntry.institution = q.institution
const { description } = q
if (!description) {
return res.status(400).send('description is a required parameter!')
}
consultancyEntry.description = description
consultancyEntry.domainPrimaryIdInitial = q.domainPrimary
if (!consultancyEntry.domainPrimaryIdInitial || q.domainPrimary === '-1') {
consultancyEntry.domainPrimaryIdInitial = null // undefined
}
consultancyEntry.existingSolutions = q.existing_solutions
consultancyEntry.examplesOfUse = q.examples_of_use
Object.keys(consultancyEntry).forEach(key => {
consultancyEntry[key] = helper.removeHtmlTags(consultancyEntry[key]).trim()
})
const questionId = await ConsultancyEntry.createQuestion(consultancyEntry)
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
// TODO SEND EMAIL
// TODOOOOOOOOO
const emails = await ConsultancyEntry.fetchConsultancyAdminEmails()
const renderAsync = promisify(req.app.render.bind(req.app))
const emailHtml = await renderAsync('email/consultancy-creation-notify', {
propertyToPassGoesHere: 'test1234'
})
await email.send({
to: emails,
subject: 'Ustvarjeno novo vprašanje v svetovalnici',
html: emailHtml
})
/// /////////////////
res.status(201).send()
}
consultancy.updateDomain = async (req, res) => {
const { id, value } = req.body
if (!id) return res.status(400).send()
await User.updateConsultancyDomains(id, value)
res.status(204).send()
}
consultancy.insertConsultantAndHisDomainsByUsername = async (req, res) => {
const { username, domains } = req.body
if (!username) {
return res.status(400).send()
}
await User.insertNewConsultantWithDomainByUsername(username, domains)
res.status(204).send()
}
consultancy.removeConsultant = async (req, res) => {
const { id } = req.body
if (!id) return res.status(400).send()
await User.removeConsultant(id)
res.send()
}
consultancy.getSharedAuthors = async (req, res) => {
const { id } = req.query
if (!id) return res.status(400).send()
const authors = await ConsultancyEntry.getSharedAuthorsArray(id)
res.send(authors)
}
consultancy.insertNonModerator = async (req, res) => {
const userId = req.body.user_id
const entryId = req.body.entry_id
if (!userId || !entryId) return res.status(400).send()
await ConsultancyEntry.insertNonModerator(entryId, userId)
await ConsultancyEntry.indexIntoSearchEngine(entryId, true)
res.status(201).send()
}
consultancy.getSharedAuthorsBeforePublish = async (req, res) => {
const { id } = req.query
if (!id) return res.status(400).send()
const authors = await ConsultancyEntry.getSharedAuthorsArrayBeforePublish(id)
res.send(authors)
}
consultancy.updateSharedAuthors = async (req, res) => {
const { id, authors } = req.body
if (!id || !authors.length) return res.status(400).send()
await ConsultancyEntry.updateSharedAuthorsArray(id, authors)
await ConsultancyEntry.indexIntoSearchEngine(id, true)
res.send({})
}
/*
function assign - assigns from any state of the consultancy
question to work in-progress
inputs:
id -> id of the question
*/
consultancy.assign = async (req, res) => {
const questionId = req.body.question_id
let userId = req.body.user_id
if (!questionId) return res.status(400).send()
if (!userId) {
const user = await ConsultancyEntry.getModerator(questionId)
userId = user.id
if (!userId) return res.status(400).send({})
}
await ConsultancyEntry.assignWorkInProgress(questionId, userId)
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
// TODO SEND MAIL TO MODERATOR
/*
Skrbnik svetovalnice vam je v urejanje dodelil novo terminološko vprašanje.
*/
const emails = await ConsultancyEntry.fetchModeratorEmail(questionId)
const renderAsync = promisify(req.app.render.bind(req.app))
const emailHtml = await renderAsync('email/consultancy-assigned')
await email.send({
to: emails,
subject: 'Novo terminološko vprašanje',
html: emailHtml
})
res.send()
}
consultancy.reject = async (req, res) => {
const questionId = req.body.question_id
if (!questionId) return res.status(400).send()
await ConsultancyEntry.rejectEntry(questionId)
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
res.send()
}
consultancy.sendToReview = async (req, res) => {
const questionId = req.body.question_id
if (!questionId) return res.status(400).send()
await ConsultancyEntry.sendToReview(questionId)
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
/* SEND MAIL TO ALL CONSULTANCY ADMINS
Za potrditev objave ste prejeli novo terminološko vprašanje.
const allEmailsSet = new Set([...adminEmails, ...dictionariesAdminEmails])
const allEmails = []
for (const email of allEmailsSet) allEmails.push(email)
const type = 'delete'
const renderAsync = promisify(appRef.render.bind(appRef))
const emailHtml = await renderAsync('email/dictionary-status-change', {
type,
nameSl
})
await email.send({
to: allEmails,
subject: 'Obvestilo o številu gesel',
html: emailHtml
})
*/
const emails = await ConsultancyEntry.fetchConsultancyAdminEmails()
const renderAsync = promisify(req.app.render.bind(req.app))
const emailHtml = await renderAsync('email/consultancy-item-review')
await email.send({
to: emails,
subject: 'Potrditev objave',
html: emailHtml
})
res.send()
}
consultancy.publish = async (req, res) => {
const questionId = req.body.question_id
const answerAuthors = req.body.answer_authors
if (!questionId) return res.status(400).send()
const entry = await ConsultancyEntry.fetchById(questionId)
if (!entry.title) {
return res.status(400).send('Answer not completed')
}
await ConsultancyEntry.publish(questionId, answerAuthors)
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
res.send()
}
consultancy.updateQuestion = async (req, res) => {
const { id, questionTitle, domain: domainId, question, answer } = req.body
if (!id) return res.status(400).send({})
if (questionTitle === '' || question === '' || answer === '') {
return res.status(422).send('Polja naslov, vprašanje in mnenje so obvezna!')
}
const entry = await ConsultancyEntry.fetchById(id)
entry.domainPrimaryId = domainId > 0 ? domainId : null
entry.question = question
entry.answer = answer // helper.removeHtmlTags(answer).trim()
entry.title = questionTitle
// TODO Luka: Miha, update only fields that were updated.
await ConsultancyEntry.updateQuestion(entry)
await ConsultancyEntry.indexIntoSearchEngine(id, true)
res.send({})
}
consultancy.insertNonModerator = async (req, res) => {
const questionId = req.body.question_id
const userId = req.body.user_id
if (!questionId || !userId) return res.status(400).send()
await ConsultancyEntry.insertNonModerator(questionId, userId)
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
res.send({})
}
consultancy.deleteConsultantForEntry = async (req, res) => {
const questionId = req.body.question_id
const userId = req.body.user_id
if (!questionId || !userId) return res.status(400).send()
await ConsultancyEntry.removeConsultantForEntry(questionId, userId)
await ConsultancyEntry.indexIntoSearchEngine(questionId, true)
res.send({})
}
consultancy.deleteQuestion = async (req, res) => {
const { id } = req.body // question/entry id
if (!id) return res.status(400).send()
await ConsultancyEntry.removeEntry(id)
await deleteConsultancyEntryFromIndex(id, true)
res.send()
}
module.exports = consultancy
@@ -0,0 +1,15 @@
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 })
}
+189
View File
@@ -0,0 +1,189 @@
const Dictionary = require('../../../models/dictionary')
const Entry = require('../../../models/entry')
const {
searchEntryIndex,
deleteEntryFromIndex,
deleteDictionaryEntriesFromIndex
} = require('../../../models/search-engine')
const genEditorAllQuery = require('../../../models/helpers/search/generate-query/editor/all')
const { prepareEditorEntries } = require('../../../models/helpers/search')
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
const { minEntriesRequirementCheckAndAct } = require('../../helpers/dictionary')
const dictionary = {}
dictionary.getEntry = async (req, res) => {
const entryId = req.query.id
const [entry, domainLabels] = await Promise.all([
Entry.fetchFull(entryId),
Dictionary.fetchDomainLabelsFromEntryId(entryId)
])
const data = {
entry,
allDomainLabels: domainLabels
}
res.send(data)
}
dictionary.createEntry = async (req, res) => {
const { body } = req
const { dictionaryId } = body
const entryId = await Entry.create(req.user.id, dictionaryId, body)
await Promise.all([
Dictionary.updateMetadataAfterModifyingEntries(dictionaryId),
Entry.indexIntoSearchEngine(entryId, true)
])
res.send({ entryId })
}
dictionary.updateEntry = async (req, res) => {
const { entryId } = req.body
const dictionaryId = await Entry.update(req.user.id, req.body)
await Promise.all([
Dictionary.updateMetadataAfterModifyingEntry(entryId),
Entry.indexIntoSearchEngine(entryId, true),
minEntriesRequirementCheckAndAct.onUpdate(dictionaryId)
])
res.end()
}
dictionary.fetchEntries = async (req, res) => {
const dictionaryId = req.query.id
const hitsQuery = genEditorAllQuery(dictionaryId)
const hits = await searchEntryIndex(hitsQuery)
const terms = prepareEditorEntries(hits)
res.send(terms)
}
// This function deletes selected entry.
dictionary.deleteEntry = async (req, res) => {
const { entryId } = req.query
const dictionaryId = await Entry.delete(entryId)
await Promise.all([
Dictionary.updateMetadataAfterModifyingEntries(dictionaryId),
deleteEntryFromIndex(entryId, true),
minEntriesRequirementCheckAndAct.onDelete(dictionaryId, req.app)
])
res.end()
}
// This function deletes all entries in seleceted dictionary.
dictionary.deleteAllEntries = async (req, res) => {
// TODO This method executes all delete operations before sending the response,
// TODO after which the client tells the user it might take a few minutes.
const dictionaryId = +req.params.dictionaryId
await Entry.deleteAll(dictionaryId)
await Promise.all([
Dictionary.updateMetadataAfterModifyingEntries(dictionaryId),
deleteDictionaryEntriesFromIndex(dictionaryId),
minEntriesRequirementCheckAndAct.onDelete(dictionaryId, req.app)
])
res.end()
}
dictionary.publishAllEntries = async (req, res) => {
// // TODO This method executes all operations before sending the response,
// // TODO after which the client tells the user it might take a few minutes.
// // TODO Also, the message is generic and the same as with deleting a dictionary or all of its entries.
const dictionaryId = +req.params.dictionaryId
await Entry.publishAllQualified(dictionaryId)
// TODO Consider reducing the following two index operations into a single one.
await Promise.all([
Dictionary.updateMetadataAfterModifyingEntries(dictionaryId),
deleteDictionaryEntriesFromIndex(dictionaryId)
])
await Dictionary.indexIntoSearchEngine(dictionaryId)
res.end()
}
dictionary.delete = async (req, res) => {
// TODO This method executes all operations before sending the response,
// TODO after which the client tells the user it might take a few minutes.
// TODO It also keeps the user on a page which shouldn't exist anymore.
// TODO In fact, any value is apparently valid as dictionaryId URL parameter (no validation yet).
const dictionaryId = +req.params.dictionaryId
await Dictionary.delete(dictionaryId)
await deleteDictionaryEntriesFromIndex(dictionaryId)
res.end()
}
dictionary.updateDomainLabels = 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
await Dictionary.renovateSecondaryDomains(data)
res.end()
}
dictionary.getEntryVersionSnapshot = async (req, res) => {
const { entryId, version } = req.params
const historySnapshot = await Entry.fetchVersionSnapshot(entryId, version)
res.send(historySnapshot)
}
dictionary.listDictionaries = 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.fetchAllAdminDictionaries(resultsPerPage, page)
res.send({ page, numberOfAllPages, results })
}
dictionary.listDomainLabels = 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.fetchPaginationDomainLabels(
dictionaryId,
resultsPerPage,
page
)
res.send({ 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
const { pages_total: numberOfAllPages, results } =
await Dictionary.fetchAllSecondaryDomains(resultsPerPage, page)
res.send({ page, numberOfAllPages, results })
}
dictionary.extractionImport = async (req, res) => {
// TODO Import logic (Luka's task)
// const { id: dictionaryId, extractionId } = req.params
// const { from, to } = req.query
// const fromIndex = +from > 1 ? Math.floor(from) - 1 : 0
// const toIndex = Number.isInteger(+to) ? Math.abs(to) : undefined
// console.log({ dictionaryId, extractionId, fromIndex, toIndex })
res.send('IMPORTING')
}
module.exports = dictionary
+328
View File
@@ -0,0 +1,328 @@
const { unlink, rm, mkdir } = require('fs/promises')
const { promisify } = require('util')
const { URLSearchParams } = require('url')
const multer = require('multer')
const validator = require('validator')
const axios = require('axios')
const {
getExtractionFilesPath,
getDocumentsPath,
getStopTermsPath,
getConllusPath
} = 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 MAX_FILE_NAME_LENGTH = 100
const MAX_FILE_SIZE = 10 ** 9 // 1 GB
const VALID_DOCUMENT_EXTENSIONS = ['txt', 'doc', 'docx', 'pdf']
const VALID_STOP_TERMS_FILE_EXTENSION = 'txt'
const MAX_OSS_DOCUMENT_COUNT = 500
const extractionFileStorage = multer.diskStorage({
destination: (req, file, cb) => {
const {
params: { id: extractionId },
fileType
} = req
const destinationPath =
fileType === 'stopTerms'
? getStopTermsPath(extractionId)
: getDocumentsPath(extractionId)
cb(null, destinationPath)
},
filename: (req, file, cb) => cb(null, file.originalname)
})
const extractionFileBodyParser = multer({
storage: extractionFileStorage,
limits: {
fieldNameSize: 15,
fieldSize: 10,
fields: 1,
fileSize: MAX_FILE_SIZE,
headerPairs: 500
},
fileFilter: extractionFileFilter
}).single('extractionFile')
const parseExtractionFileBody = promisify(extractionFileBodyParser)
const extraction = {}
extraction.delete = async (req, res) => {
const extractionId = req.params.id
const corpusId = await Extraction.delete(extractionId)
if (corpusId) {
await axios.delete(`http://concordancer:5000/dashboard/corpus/${corpusId}`)
}
const extractionFilesPath = getExtractionFilesPath(extractionId)
await rm(extractionFilesPath, { recursive: true })
res.end()
}
extraction.docsList = async (req, res) => {
const extractionId = req.params.id
const documentsStats = await Extraction.fetchAllDocumentsStats(extractionId)
res.send(documentsStats)
}
extraction.docsUpdate = async (req, res) => {
try {
await parseExtractionFileBody(req, res)
} catch (error) {
if (
error instanceof multer.MulterError &&
error.code === 'LIMIT_FILE_SIZE'
) {
throw Error('File too large. Must not be over 1 GB.')
}
throw error
}
res.end()
}
extraction.docDelete = async (req, res) => {
const { id: extractionId, filename } = req.params
const documentsPath = getDocumentsPath(extractionId)
const filePath = `${documentsPath}/${filename}`
await unlink(filePath)
res.end()
}
extraction.stopTermsList = async (req, res) => {
const extractionId = req.params.id
const stopTermsFilesStats = await Extraction.fetchAllStopTermsFilesStats(
extractionId
)
res.send(stopTermsFilesStats)
}
extraction.stopTermsUpdate = async (req, res) => {
try {
await parseExtractionFileBody(req, res)
} catch (error) {
if (
error instanceof multer.MulterError &&
error.code === 'LIMIT_FILE_SIZE'
) {
throw Error('File too large. Must not be over 1 GB.')
}
throw error
}
res.end()
}
extraction.stopTermDelete = async (req, res) => {
const { id: extractionId, filename } = req.params
const stopTermsPath = getStopTermsPath(extractionId)
const filePath = `${stopTermsPath}/${filename}`
await unlink(filePath)
res.end()
}
extraction.ossSaveParams = [saveOssParams, (req, res) => res.end()]
extraction.ossSearch = [
saveOssParams,
async (req, res) => {
const { id: extractionId } = req.params
const { ossParams } = req
const searchParams = new URLSearchParams({
...(ossParams.year && { leta: ossParams.year }),
...(ossParams.documentType && { vrste: ossParams.documentType }),
...(ossParams.keywords && { kljucneBesede: ossParams.keywords }),
...(ossParams.domainUdk && { udk: ossParams.domainUdk })
})
const searchApiUrl = `http://rsdo.lhrs.feri.um.si:8080/oss/steviloBesedilPoIskanju?${searchParams}`
const { data: documentCount } = await axios.get(searchApiUrl)
const canSave = documentCount && documentCount <= MAX_OSS_DOCUMENT_COUNT
await Extraction.updateOssParams(extractionId, {
params: ossParams,
status: canSave ? 'valid' : 'invalid'
})
res.send({ documentCount, canSave })
}
]
extraction.ossConfirmParams = async (req, res) => {
const { id: extractionId } = req.params
const { ossParams } = await Extraction.fetch(extractionId)
if (ossParams.status !== 'valid') throw Error('OSS params not valid')
await Extraction.updateOssParams(extractionId, {
params: ossParams.params,
status: 'confirmed'
})
res.end()
}
extraction.begin = async (req, res) => {
const extractionId = req.params.id
const extraction = await Extraction.fetch(extractionId)
const canBegin = await checkIfcanBegin(extraction)
if (!canBegin) throw Error('Extraction does not qualify to be ran')
let timeStarted
const { ossParams, name: extractionName } = extraction
if (ossParams) {
timeStarted = await Extraction.beginOss(extractionId)
} else {
const conllusPath = getConllusPath(extractionId)
await mkdir(conllusPath, { recursive: true })
const documentsNames = await Extraction.fetchAllDocumentsNames(extractionId)
timeStarted = await Extraction.beginOwn(extractionId, documentsNames)
}
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)
}
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
})
}
extraction.duplicate = async (req, res) => {
// TODO Validate, if can be duplicated: status = finished or failed.
// TODO Execute duplication.
res.send('DUPLICATING!')
}
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')
}
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(
extractionId
)
const termCandidates = JSON.parse(termCandidatesJson).terminoloski_kandidati
const hitsPerPage = +req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const numberOfAllPages = Math.ceil(termCandidates.length / hitsPerPage)
res.send({ hitsPerPage, numberOfAllPages, termCandidates })
}
function extractionFileFilter(req, file, cb) {
let fileType
switch (req.path.split('/').at(-1)) {
case 'documents':
fileType = 'document'
break
case 'stop-terms':
fileType = 'stopTerms'
break
default:
return cb(Error('Invalid API endpoint'))
}
const filenamePartsArray = file.originalname.split('.')
const fileExtension = filenamePartsArray.pop()
if (
(fileType === 'document' &&
!VALID_DOCUMENT_EXTENSIONS.includes(fileExtension)) ||
(fileType === 'stopTerms' &&
fileExtension !== VALID_STOP_TERMS_FILE_EXTENSION)
) {
return cb(Error('Invalid file type'))
}
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.`
)
)
}
if (!validator.isAlphanumeric(fileName[0], 'sl-SI', { ignore: '_' })) {
return cb(
Error(
'Filename must begin with an alphanumeric character or an underscore.'
)
)
}
if (!validator.isAlphanumeric(fileName, 'sl-SI', { ignore: ' _-.' })) {
return cb(
Error(
'Filename can only contain alphanumeric characters, spaces, underscores, minuses and periods.'
)
)
}
req.fileType = fileType
cb(null, true)
}
async function saveOssParams(req, res, next) {
const { id: extractionId } = req.params
const { body } = req
const ossParams = {
...(body.domain && {
domainUdk: intoDbArray(
(await Domain.fetchById(body.domain)).udkCode,
'always'
)
}),
...(body.documentType && {
documentType: intoDbArray(body.documentType, 'always').map(type => +type)
}),
...(body.year && {
year: intoDbArray(body.year, 'always').map(year => +year)
}),
...(body.keywords && {
keywords: intoDbArray(body.keywords, 'always')
})
}
await Extraction.update(extractionId, body.name)
await Extraction.updateOssParams(extractionId, {
params: ossParams,
status: 'unvalidated'
})
req.ossParams = ossParams
next()
}
module.exports = extraction
@@ -0,0 +1,18 @@
const InterInstanceSync = require('../../../models/inter_instance_sync')
exports.listDictionaries = async (req, res) => {
const dictionaries = await InterInstanceSync.listDictionaries()
res.send(dictionaries)
}
exports.syncDictionary = async (req, res) => {
const did = req.params.dictionaryId
const since = req.query.lastSynced
? req.query.lastSynced
: '2000-01-01 00:00:00'
const entriesToSync = await InterInstanceSync.getUpdatedEntriesSince(
did,
since
)
res.send(entriesToSync)
}
+63
View File
@@ -0,0 +1,63 @@
const Portal = require('../../../models/portal')
const axios = require('axios')
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
const portal = {}
portal.create = async (req, res) => {
const portal = await Portal.create(req.body)
res.send(portal)
}
portal.syncRemoteDictionaries = async (req, res) => {
const { linkedPortalId } = req.params
const { indexURL } = await Portal.fetchPortal(linkedPortalId)
const { data: dictionaries } = await axios.get(indexURL)
await Portal.syncRemoteDictionaries(linkedPortalId, dictionaries)
res.end()
}
portal.deleteLinkedDictionary = async (req, res) => {
const { linkedPortalId } = req.params
await Portal.deleteLinkedDictionary(linkedPortalId)
res.end()
}
portal.fetchDictionary = async (req, res) => {
const portalId = req.query.id
const dictionaries = await Portal.fetchDictionaries(portalId)
res.send(dictionaries)
}
portal.updatePortalStatus = async (req, res) => {
const portalId = req.body.params.id
const isEnabled = req.body.params.isEnabled
await Portal.updatePortalStatus(portalId, isEnabled)
res.end()
}
portal.listSelectedLinkedDicts = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const { portalId } = req.params
const page = +req.query.p > 0 ? +req.query.p : 1
const { pages_total: numberOfAllPages, results } =
await Portal.fetchSelectedLinkedDictionaries(portalId, resultsPerPage, page)
res.send({ page, numberOfAllPages, results })
}
portal.listAllLinkedDicts = 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 Portal.fetchAllLinkedDictionaries(resultsPerPage, page)
res.send({ page, numberOfAllPages, results })
}
module.exports = portal
+230
View File
@@ -0,0 +1,230 @@
const Dictionary = require('../../../models/dictionary')
const { searchEntryIndex } = require('../../../models/search-engine')
const { intoDbArray } = require('../../../models/helpers')
const {
prepareEntries,
prepareEditorEntries,
prepareAggregation,
prepareSeachFilterData
} = require('../../../models/helpers/search')
const generateQuery = require('../../../models/helpers/search/generate-query')
const { getInstanceSetting } = require('../../../models/helpers')
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
exports.listMainEntries = async (req, res) => {
const searchString = req.query.q?.trim()
if (!searchString) return res.status(400).end()
const filters = {
sourceLanguages: intoDbArray(req.query.sl, 'always'),
targetLanguages: intoDbArray(req.query.tl, 'always'),
primaryDomains: intoDbArray(req.query.pd, 'always'),
dictionaries: intoDbArray(req.query.d, 'always'),
sources: intoDbArray(req.query.s, 'always')
}
const hitsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const page = +req.query.p > 0 ? +req.query.p : 1
const hitsQuery = await generateQuery.main(
searchString,
filters,
hitsPerPage,
page
)
const hits = await searchEntryIndex(hitsQuery)
const numberOfAllHits = hits.body.hits.total.value
const numberOfAllPages = Math.ceil(numberOfAllHits / hitsPerPage)
let entriesByCategory = prepareEntries(hits)
// TODO Luka (note to self): Consider reworking Miha's logic below. It's hacky and possibly error prone.
// TODO copied code below, maybe refactor to a helper function?
// check express/controllers/index.js search
entriesByCategory = Object.entries(entriesByCategory).reduce(
(acc, category, index) => {
acc[category[0]] = category[1].map(entry => {
if (entry.foreignEntries) {
entry.foreignEntries = entry.foreignEntries.map(fe => {
fe.terms = fe.terms ? fe.terms.filter(term => !!term) : []
fe.synonyms = fe.synonyms
? fe.synonyms.filter(synonym => !!synonym)
: []
fe.nbspCount = fe.terms.length + fe.synonyms.length - 1
return fe
})
}
return entry
})
return acc
},
{}
)
// res.send({ page, numberOfAllPages, entries })
res.append('page', page)
res.append('number-of-all-pages', numberOfAllPages)
res.render('utilities/response-pug-wrapper/entryLister', {
entriesByCategory
})
}
exports.listEditorEntries = async (req, res) => {
const { dictionaryId } = req.params
if (!dictionaryId) return res.status(400).end()
const qs = req.query
const searchField = qs.field
const searchString = qs.q?.trim() ?? ''
const filters = {
isValid: qs.isValid === undefined ? undefined : qs.isValid !== 'false',
isPublished:
qs.isPublished === undefined ? undefined : qs.isPublished !== 'false',
hasComments:
qs.hasComments === undefined ? undefined : qs.hasComments !== 'false',
isComplete:
qs.isComplete === undefined ? undefined : qs.isComplete !== 'false',
isTerminologyReviewed:
qs.isTerminologyReviewed === undefined
? undefined
: qs.isTerminologyReviewed !== 'false',
isLanguageReviewed:
qs.isLanguageReviewed === undefined
? undefined
: qs.isLanguageReviewed !== 'false'
}
const hitsQuery = generateQuery.editor(
dictionaryId,
searchField,
searchString,
filters
)
const hits = await searchEntryIndex(hitsQuery)
const entries = prepareEditorEntries(hits)
res.send(entries)
}
exports.listFilteredDictionaries = async (req, res) => {
let searchString = req.query.q?.trim()
if (!searchString) {
searchString = ''
}
const filters = {
// sourceLanguages: intoDbArray(req.query.sl, 'always'),
// targetLanguages: intoDbArray(req.query.tl, 'always'),
primaryDomains: intoDbArray(req.query.pd, 'always')
// sources: intoDbArray(req.query.s, 'always')
}
const orderType = req.query.orderType
const orderIndex = req.query.orderIndex === 'true'
// TODO: User another constant since this one is also user in the search results
const hitsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const page = +req.query.p > 0 ? +req.query.p : 1
// const defaultportalname = await getInstanceSetting('portal_name')
const defaultportalcode = await getInstanceSetting('portal_code')
let dictionaries
// mapping done here to not expose database info to public
let orderAttribute
if (orderType === 'domainName') {
orderAttribute = 'dp'
} else {
orderAttribute = 'd'
}
dictionaries = await Dictionary.fetchBasicInfoPerPageFilteredWithOrdering(
searchString,
filters.primaryDomains,
hitsPerPage,
page,
orderAttribute,
orderIndex
)
dictionaries = dictionaries.map(e => {
if (!e.portalcode) {
e.portalcode = defaultportalcode
// e.portalname = defaultportalname
}
return e
})
const numberOfAllHits = parseInt(
(await Dictionary.fetchFilteredCount(searchString, filters.primaryDomains))
.count
)
const numberOfAllPages = Math.ceil(numberOfAllHits / hitsPerPage)
// res.send({ page, numberOfAllPages, entries })
res.append('page', page)
res.append('number-of-all-pages', numberOfAllPages)
res.render('utilities/response-pug-wrapper/dictionaryLister', {
dictionaries
})
}
exports.showModalFilterResults = async (req, res) => {
const searchString = req.query.q?.trim()
if (!searchString) return res.status(400).end()
const selectedFilter = req.query.selectedFilter
// The selected
const filters = {
sourceLanguages:
selectedFilter === 'sourceLanguages'
? []
: intoDbArray(req.query.sl, 'always'),
targetLanguages:
selectedFilter === 'targetLanguages'
? []
: intoDbArray(req.query.tl, 'always'),
primaryDomains:
selectedFilter === 'primaryDomains'
? []
: intoDbArray(req.query.pd, 'always'),
dictionaries:
selectedFilter === 'dictionaries'
? []
: intoDbArray(req.query.d, 'always'),
sources:
selectedFilter === 'sources' ? [] : intoDbArray(req.query.s, 'always')
}
const hitsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const page = +req.query.p > 0 ? +req.query.p : 1
const [, aggregateQuery] = await generateQuery.main(
searchString,
filters,
hitsPerPage,
page,
true
)
const aggregationRaw = await searchEntryIndex(aggregateQuery)
const aggregation = await prepareAggregation(aggregationRaw)
res.send(prepareSeachFilterData(aggregation, filters))
}
+16
View File
@@ -0,0 +1,16 @@
const debug = require('debug')('termPortal:controllers/api/v1/system')
const Eurotermbank = require('../../../models/system/eurotermbank')
exports.handleCspReports = (req, res) => {
debug(req.body)
res.sendStatus(200)
}
exports.syncWithEurotermbank = async (req, res) => {
try {
await Eurotermbank.push()
res.send('Sync successful')
} catch (error) {
res.send('Sync failed')
}
}
+35
View File
@@ -0,0 +1,35 @@
const User = require('../../../models/user')
const { DEFAULT_HITS_PER_PAGE } = require('../../../config/settings')
const users = {}
users.listUsers = 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 User.fetchAll(
resultsPerPage,
page
)
res.send({ page, numberOfAllPages, results })
}
users.updateHitsPerPage = async (req, res) => {
const hitAmount = req.body.hitAmount
await User.updateHitsPerPage(req.user.userName, hitAmount)
res.status(200).send()
}
users.updateFristNameAndSurname = async (req, res) => {
const firstname = req.body.name
const surname = req.body.surname
await User.updateFirstNameAndLastName(req.user.userName, firstname, surname)
res.status(200).send()
}
module.exports = users
+408
View File
@@ -0,0 +1,408 @@
const Dictionary = require('../models/dictionary')
const ConsultancyEntry = require('../models/consultancy-entry')
const Domain = require('../models/domain')
const User = require('../models/user')
const utils = require('../utils')
// const helpers = require('../models/helpers')
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 { minEntriesRequirementCheckAndAct } = require('./helpers/dictionary')
const consultancy = {}
const consultancyAdmin = {}
consultancy.index = async (req, res) => {
req.indexHitPageAmount = '5'
return await consultancyRequest(
req,
res,
'published',
'pages/consultancy/index'
)
}
consultancy.search = async (req, res) => {
return await consultancyRequest(
req,
res,
'published',
'pages/consultancy/search'
)
}
consultancy.specificQuestion = async (req, res) => {
const { id } = req.params
const entry = await ConsultancyEntry.fetchByIdWithFormattedTime(id)
// const author = await User.fetchUser(entry.authorId)
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
entry.answerAuthors = entry.answerAuthors.filter(author => author !== '')
let authorString
if (entry.answerAuthors.length === 1) {
authorString = 'Avtor'
} else if (entry.answerAuthors.length === 2) {
authorString = 'Avtorja'
} else {
authorString = 'Avtorji'
}
entry.domain = allPrimaryDomains.filter(
filt => filt.id === entry.domainPrimaryId
)
if (entry.domain.length > 0) {
entry.domain = entry.domain[0].nameSl
} else {
entry.domain = false
}
res.render('pages/consultancy/item-details', {
allPrimaryDomains,
authorString,
entry
})
}
consultancy.new = async (req, res) => {
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
res.render('pages/consultancy/ask', {
allPrimaryDomains
})
}
consultancyAdmin.new = async (req, res) => {
return await consultancyRequest(
req,
res,
'new',
'pages/consultancy/admin/index',
true,
false
)
}
consultancyAdmin.users = async (req, res) => {
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
const users = await User.fetchConsultants()
res.render('pages/consultancy/admin/users', {
allPrimaryDomains,
users
})
}
consultancyAdmin.rejected = async (req, res) => {
return await consultancyRequest(
req,
res,
'rejected',
'pages/consultancy/admin/rejected',
true,
false
)
}
consultancyAdmin.published = async (req, res) => {
return await consultancyRequest(
req,
res,
'published',
'pages/consultancy/admin/published',
true,
false
)
}
consultancyAdmin.prepared = async (req, res) => {
return await consultancyRequest(
req,
res,
'review',
'pages/consultancy/admin/prepared',
true,
false
)
}
consultancyAdmin.inProgress = async (req, res) => {
// TODO Below is an example use of consultancy search implemented using search engine.
// TODO Adjust and use it everywhere it's needed and delete these comments.
// *************************************** EXAMPLE START ***************************************
return await consultancyRequest(
req,
res,
'in progress',
'pages/consultancy/admin/in-progress',
true,
false
)
}
consultancyAdmin.statistics = async (req, res) => {
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
res.render('pages/consultancy/admin/statistics', {
allPrimaryDomains
})
}
consultancyAdmin.edit = async (req, res) => {
const { id } = req.params
const key = req.query.sentFrom
const sentFrom = {}
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')
}
const entry = await ConsultancyEntry.fetchByIdWithFormattedTime(id)
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
const author = await User.fetchUser(entry.authorId)
const isPublished = entry.status === 'published'
res.render('pages/consultancy/admin/edit', {
id,
sentFrom: key,
entry: entry,
allPrimaryDomains,
moderator,
author,
isPublished,
// TODO Luka: I suspect this will not work as intended on staging or production environments. Test.
urlPrefix: req.protocol + '://' + req.get('host')
})
}
function dateMap(obj) {
if (!obj.formattedTimeCreated) {
obj.formattedTimeCreated = obj.timeCreated
}
return obj
}
async function mapDomainIdToDomainNameSlovene(obj) {
try {
const area = await Domain.fetchById(
obj.domainPrimaryId ? obj.domainPrimaryId : obj.domainPrimaryIdInitial
)
obj.area = area.nameSl
} catch {
obj.area = 'Ni področja'
}
return obj
}
function mapInitialValuesAsEmpty(obj) {
if (!obj.authors) {
obj.authors = []
}
if (!obj.title) {
obj.title = ''
}
if (!obj.numShared) {
obj.numShared = 0
}
return obj
}
async function mapEntryList(list) {
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)
return entity
})
)
}
function summaryDisplay(str) {
if (str.split('\n').length > 3) {
return splitLine(str, 3)
} else {
return cropLongString(str)
}
}
function splitLine(str, countLines) {
if (!str) {
return str
}
if (countLines <= 0) {
return ''
}
let nlIndex = -1
let newLinesFound = 0
while (newLinesFound < countLines) {
const nextIndex = str.indexOf('\n', nlIndex + 1)
if (nextIndex === -1) {
return str
}
nlIndex = nextIndex
newLinesFound++
}
const nextIndex = str.indexOf('\n', nlIndex + 1)
return str.slice(0, nlIndex) + (nextIndex !== -1 ? '...' : '')
}
function cropLongString(str) {
if (str.length > 500) {
return str.slice(0, 500) + '...'
}
return str
}
async function consultancyRequest(
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
) {
const searchString = req.query.q?.trim() ?? ''
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
}
let hitsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
if (req.indexHitPageAmount) {
hitsPerPage = req.indexHitPageAmount
}
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
: '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) {
const date = new Date(entry.timeCreated)
entry.formattedTimeCreated = `${date.getDate()}. ${
date.getMonth() + 1
}. ${date.getFullYear()}`
}
return entry
})
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
// 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.render(url, {
allPrimaryDomains,
entries, // entryList,
userList,
numberOfAllPages,
queryCount: numberOfAllHits
})
}
module.exports = { consultancy, consultancyAdmin }
+17
View File
@@ -0,0 +1,17 @@
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
+699
View File
@@ -0,0 +1,699 @@
const { unlink } = require('fs/promises')
const { promisify } = require('util')
const multer = require('multer')
const debug = require('debug')('termPortal:controllers/dictionary')
const Dictionary = require('../models/dictionary')
const Entry = require('../models/entry')
const User = require('../models/user')
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 { DEFAULT_HITS_PER_PAGE, DATA_FILES_PATH } = require('../config/settings')
const {
statusChangeCheckAndAct,
determineNewStatus
} = require('./helpers/dictionary')
const SFDSuggestionImporter = require('./helpers/search-filter-data-suggestion-importer')
const Extraction = require('../models/extraction')
const importFileBodyParser = multer({
dest: `${DATA_FILES_PATH}/dict_import_temp`,
limits: {
fieldNameSize: 21,
fieldSize: 9,
fields: 3,
fileSize: 1000 * 1000 * 1000, // 1GB
headerPairs: 500
},
fileFilter: importFileFilter
}).single('dictionaryImportFile')
const parseImportFileBody = promisify(importFileBodyParser)
const dictionary = {}
dictionary.list = async (req, res) => {
let dictionaries
if (req.isAuthenticated()) {
dictionaries = await Dictionary.fetchAllByUser(req.user.id)
}
res.render('pages/dictionaries/list', {
title: 'Seznam slovarjev',
dictionaries
})
}
dictionary.new = async (req, res) => {
// TODO Once english language is implemented, gather selected language (sl/en) from request ~ (cookies?)
const language = 'name_sl'
const [allPrimaryDomains, allSecondaryDomains, allLanguages] =
await Promise.all([
Dictionary.fetchAllPrimaryDomains(),
Dictionary.fetchAllApprovedSecondaryDomains(),
Dictionary.fetchAllLanguages(language)
])
res.render('pages/dictionaries/new', {
title: 'Nov slovar',
allPrimaryDomains,
allSecondaryDomains,
allLanguages
})
}
dictionary.create = async (req, res) => {
await Dictionary.create(req.body, req.user.id)
res.redirect('/slovarji/moji')
}
dictionary.editDescription = async (req, res) => {
const { dictionaryId } = req.params
const [
allPrimaryDomains,
allSecondaryDomains,
dictionary,
associatedSecondaryDomains
] = await Promise.all([
Dictionary.fetchAllPrimaryDomains(),
Dictionary.fetchAllApprovedSecondaryDomains(),
Dictionary.fetchEditDescription(dictionaryId),
Dictionary.fetchSecondaryDomains(dictionaryId)
])
res.render('pages/dictionaries/description', {
title: 'Ime in opis',
allPrimaryDomains,
allSecondaryDomains,
dictionary,
associatedSecondaryDomains
})
}
dictionary.updateDescription = async (req, res) => {
const { dictionaryId } = req.params
const { body } = req
// TODO Consider using a transaction.
await Promise.all([
Dictionary.updateDescription(dictionaryId, body),
Dictionary.deleteSecondaryDomains(dictionaryId, body),
Dictionary.updateSecondaryDomains(dictionaryId, body)
])
res.redirect('back')
}
dictionary.editUsers = async (req, res) => {
const dictionaryId = req.params.dictionaryId
const [dictionary, userRights, entriesCount, minEntries, publishApproval] =
await Promise.all([
Dictionary.fetchEditUsers(dictionaryId),
User.fetchAllWithDictionaryRights(dictionaryId),
Dictionary.countPublishedEntries(dictionaryId),
getInstanceSetting('min_entries_per_dictionary'),
getInstanceSetting('dictionary_publish_approval')
])
let viewPath
switch (req.baseUrl) {
case '/slovarji':
viewPath = 'pages/dictionaries/users'
break
case '/admin':
viewPath = 'pages/admin/dictionary-users'
}
res.render(viewPath, {
title: 'Uporabniki',
dictionary,
userRights,
entriesCount,
minEntries,
publishApproval
})
}
dictionary.updateUsers = async (req, res) => {
// TODO Due to reindexing of all of dictionary entries on status change, this operation might take a while.
// TODO Stress test and consider either a notification to user or alteast a progress indicator while they wait.
const { dictionaryId } = req.params
const isPublished = req.body.isPublished === 'on'
const newDictStatus = await determineNewStatus(isPublished)
const { nameSl, status: oldDictStatus } = await Dictionary.fetchEditUsers(
dictionaryId
)
await Promise.all([
Dictionary.updateUsers(dictionaryId, req.body, newDictStatus),
User.updateUserRights(dictionaryId, req.body.rightsPerUser)
])
if (newDictStatus !== oldDictStatus) {
if (newDictStatus === 'published') {
await Dictionary.updateTimePublished(dictionaryId)
}
await Dictionary.indexIntoSearchEngine(dictionaryId)
}
await statusChangeCheckAndAct.updateUsers(
dictionaryId,
isPublished,
oldDictStatus,
nameSl,
req.app,
req.user
)
res.redirect('back')
}
dictionary.editStructure = async (req, res) => {
// TODO Once english language is implemented, gather selected language (sl/en) from request ~ (cookies?)
const language = 'name_sl'
const { dictionaryId } = req.params
const [dictionary, associatedLanguages, allLanguages] = await Promise.all([
Dictionary.fetchEditStructure(dictionaryId),
Dictionary.fetchLanguages(dictionaryId),
Dictionary.fetchAllLanguages(language)
])
let viewPath
switch (req.baseUrl) {
case '/slovarji':
viewPath = 'pages/dictionaries/structure'
break
case '/admin':
viewPath = 'pages/admin/dictionary-structure'
}
res.render(viewPath, {
title: 'Struktura slovarskega sestavka',
dictionary,
associatedLanguages,
allLanguages
})
}
dictionary.updateStructure = async (req, res) => {
const { body } = req
const { dictionaryId } = req.params
// TODO Consider using a transaction.
await Promise.all([
Dictionary.updateStructure(dictionaryId, body),
Dictionary.deleteLanguages(dictionaryId),
Dictionary.updateLanguages(dictionaryId, body)
])
res.redirect('back')
}
dictionary.editAdvanced = async (req, res) => {
const { dictionaryId } = req.params
const dictionaryName = await Dictionary.fetchName(dictionaryId)
let viewPath
switch (req.baseUrl) {
case '/slovarji':
viewPath = 'pages/dictionaries/advanced'
break
case '/admin':
viewPath = 'pages/admin/dictionary-advanced'
}
res.render(viewPath, {
title: 'Napredno',
dictionary: { id: req.params.dictionaryId },
dictionaryName
})
}
dictionary.comments = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
let viewPath
const { dictionaryId } = req.params
const type = 'dictionary'
const filters = { ctxType: type, ctxId: dictionaryId }
const [{ comments, pages_total: numberOfAllPages }, dictionaryName] =
await Promise.all([
Comment.list(filters, req.user, resultsPerPage, 1),
Dictionary.fetchName(dictionaryId)
])
switch (req.baseUrl) {
case '/slovarji':
viewPath = 'pages/dictionaries/dictionary-comments'
break
case '/admin':
viewPath = 'pages/admin/dictionary-comments'
}
res.render(viewPath, {
title: 'Komentarji',
numberOfAllPages,
dictionary: { id: req.params.dictionaryId },
comments,
dictionaryName
})
}
dictionary.showImportFromFileForm = async (req, res) => {
const { dictionaryId } = req.params
const [imports, dictionaryName] = await Promise.all([
Dictionary.fetchAllImports(dictionaryId),
Dictionary.fetchName(dictionaryId)
])
let viewPath
switch (req.baseUrl) {
case '/slovarji':
viewPath = 'pages/dictionaries/import'
break
case '/admin':
viewPath = 'pages/admin/dictionary-import'
}
res.render(viewPath, {
title: 'Uvoz iz datoteke',
dictionary: { id: dictionaryId },
imports,
dictionaryName
})
}
dictionary.listAdminDictionaries = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const { pages_total: numberOfAllPages, results } =
await Dictionary.fetchAllAdminDictionaries(resultsPerPage, 1)
res.render('pages/admin/dictionaries-list', {
title: 'Struktura slovarjev',
numberOfAllPages,
results
})
}
dictionary.adminEditDescription = async (req, res) => {
const { dictionaryId } = req.params
const [
allPrimaryDomains,
allSecondaryDomains,
dictionary,
associatedSecondaryDomains,
status
] = await Promise.all([
Dictionary.fetchAllPrimaryDomains(),
Dictionary.fetchAllApprovedSecondaryDomains(),
Dictionary.fetchEditDescription(dictionaryId),
Dictionary.fetchSecondaryDomains(dictionaryId),
Dictionary.fetchStatus(dictionaryId)
])
res.render('pages/admin/dictionary-description', {
title: 'Podatki',
allPrimaryDomains,
allSecondaryDomains,
dictionary,
associatedSecondaryDomains,
status
})
}
dictionary.updateAdminDescription = async (req, res) => {
// TODO Due to reindexing of all of dictionary entries on status change, this operation might take a while.
// TODO Stress test and consider either a notification to user or alteast a progress indicator while they wait.
const { dictionaryId } = req.params
const { body } = req
const newDictStatus = body.status
const oldDictStatus = await Dictionary.fetchStatus(dictionaryId)
// TODO Consider using a transaction.
await Promise.all([
Dictionary.updateDescription(dictionaryId, body),
Dictionary.deleteSecondaryDomains(dictionaryId, body),
Dictionary.updateSecondaryDomains(dictionaryId, body),
Dictionary.updateStatus(dictionaryId, body)
])
if (newDictStatus !== oldDictStatus) {
if (newDictStatus === 'published') {
await Dictionary.updateTimePublished(dictionaryId)
}
await Dictionary.indexIntoSearchEngine(dictionaryId)
}
await statusChangeCheckAndAct.updateAdminDescription(
dictionaryId,
newDictStatus,
oldDictStatus,
req.app,
req.user
)
res.redirect('back')
}
dictionary.showImportFromExtractionForm = async (req, res) => {
const { dictionaryId } = req.params
const dictionaryName = await Dictionary.fetchName(dictionaryId)
const extractions = await Extraction.fetchFinishedForUser(req.user.id)
let viewPath, title
switch (req.baseUrl) {
case '/slovarji':
viewPath = 'pages/dictionaries/extraction-import'
title = 'Uvoz'
break
case '/admin':
viewPath = 'pages/admin/dictionary-extraction-import'
title = 'Uvoz luščenje'
}
res.render(viewPath, {
title,
dictionary: { id: dictionaryId },
dictionaryName,
extractions
})
}
dictionary.showExportToFileForm = async (req, res) => {
const { dictionaryId } = req.params
const dictionaryName = await Dictionary.fetchName(dictionaryId)
let viewPath
switch (req.baseUrl) {
case '/slovarji':
viewPath = 'pages/dictionaries/export'
break
case '/admin':
viewPath = 'pages/admin/dictionary-export'
}
res.render(viewPath, {
title: 'Izvoz',
dictionary: { id: req.params.dictionaryId },
dictionaryName
})
}
dictionary.editDomainLabels = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const { dictionaryId } = req.params
const [{ pages_total: numberOfAllPages, results }, dictionaryName] =
await Promise.all([
Dictionary.fetchPaginationDomainLabels(dictionaryId, resultsPerPage, 1),
Dictionary.fetchName(dictionaryId)
])
let viewPath
switch (req.baseUrl) {
case '/slovarji':
viewPath = 'pages/dictionaries/domain-labels'
break
case '/admin':
viewPath = 'pages/admin/dictionary-domain-labels'
}
res.render(viewPath, {
title: 'Področne oznake',
dictionary: { id: dictionaryId },
numberOfAllPages,
results,
dictionaryName
})
}
dictionary.showContent = async (req, res) => {
const { dictionaryId } = req.params
const hitsQuery = genEditorAllQuery(dictionaryId)
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.fetchDomainLabels(dictionaryId)
])
const terms = prepareEditorEntries(hits)
res.render('pages/dictionaries/content', {
title: 'Vsebina slovarja',
terms,
canPublishEntriesInEdit,
dictionaryName,
structure,
languages,
dictionaryId,
entryDomainLabels
})
}
dictionary.showSecondaryDomains = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const { pages_total: numberOfAllPages, results } =
await Dictionary.fetchAllSecondaryDomains(resultsPerPage, 1)
res.render('pages/admin/areas', {
title: 'Podpodročja',
numberOfAllPages,
results
})
}
dictionary.dictionaryList = async (req, res) => {
/* const [allPrimaryDomains, allSecondaryDomains, allLanguages] =
await Promise.all([
Dictionary.fetchAllPrimaryDomains(),
Dictionary.fetchAllApprovedSecondaryDomains(),
Dictionary.fetchAllLanguages()
]) */
// const defaultportalname = await getInstanceSetting('portal_name')
const defaultportalcode = await getInstanceSetting('portal_code')
const hitsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const page = +req.query.p > 0 ? +req.query.p : 1
// initial mapping
// mapping done here to not expose database info to public
const orderIndex = false
const orderAttribute = 'd'
let dictionaries = await Dictionary.fetchBasicInfoPerPageFilteredWithOrdering(
'',
{},
hitsPerPage,
page,
orderAttribute,
orderIndex
)
const numberOfAllHits = parseInt(
(await Dictionary.fetchAllDictionariesCount()).count
)
const numberOfAllPages = Math.ceil(numberOfAllHits / hitsPerPage)
const allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
dictionaries = dictionaries.map(e => {
if (!e.portalcode) {
e.portalcode = defaultportalcode
// e.portalname = defaultportalname
}
return e
})
const isDictionaryListPage = true
res.render('pages/dictionaries/dictlist', {
title: 'Seznam slovarjev',
dictionaries,
allPrimaryDomains,
numberOfAllPages,
isDictionaryListPage
/*
allSecondaryDomains,
allLanguages */
})
}
dictionary.dictionaryDetails = async (req, res) => {
const { absolutePrevPath, sentFromEntryId } = req.query
const dictId = req.params.dictionaryId
const {
allPrimaryDomains,
sourceLanguages,
targetLanguages,
allDictionaryNames,
portals
} = await SFDSuggestionImporter.initialize()
const dictionaryData = await Dictionary.fetchDictionaryBasicInfo(dictId)
const filters = { ctxType: 'dictionary', ctxId: dictId }
// TODO: integrate numberOfAllPages, commentCount with pug
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const page = 1 // +req.query.p > 0 ? +req.query.p : 1
const {
pages_total: numberOfAllPages,
comments,
comment_count: commentCount
} = await Comment.list(filters, req.user, resultsPerPage, page)
// check if it is a local dictionary
if (!dictionaryData.portalname && !dictionaryData.portalcode) {
dictionaryData[0].portalname = await getInstanceSetting('portal_name')
dictionaryData[0].portalcode = await getInstanceSetting('portal_code')
}
const reducedData = dictionaryData.reduce(
(acc, x) => {
if (acc.isbegin) {
x.languages = [x.languagesl]
x.subDomains = [x.domainsecondarysl]
return x
}
if (!acc.languages.includes(x.languagesl)) {
acc.languages.push(x.languagesl)
}
if (!acc.subDomains.includes(x.domainsecondarysl)) {
acc.subDomains.push(x.domainsecondarysl)
}
return acc
},
{ isbegin: true }
)
const structData = {
prevWindowTitle: 'Nazaj',
dictName: dictionaryData[0].dictionarysl,
portalCode: dictionaryData[0].portalcode,
portalName: dictionaryData[0].portalname,
fullAuthorName: reducedData.author ? reducedData.author.join(', ') : '',
areas: dictionaryData[0].domain_primary,
subareas: reducedData.subDomains ? reducedData.subDomains.join(', ') : '',
languages: reducedData.languages ? reducedData.languages.join(', ') : ''
}
if (reducedData.author) {
if (reducedData.author.length > 2) {
structData.authorLabel = 'Avtorji'
} else if (reducedData.author.length === 2) {
structData.authorLabel = 'Avtorja'
} else if (reducedData.author.length === 1) {
structData.authorLabel = 'Avtor'
}
}
if (sentFromEntryId === 'dictsList') {
structData.prevHref = `/slovarji`
} else {
structData.prevHref = `/termin/${sentFromEntryId}`
}
const finalData = {
...reducedData,
...structData
}
res.render('pages/search/result-detail-dictionary', {
allPrimaryDomains,
sourceLanguages,
targetLanguages,
allDictionaryNames,
portals,
sentFromEntryId,
dictId,
absolutePrevPath,
dictionaryData,
finalData,
numberOfAllPages,
comments,
commentCount
}) // todo
}
dictionary.importFromFile = async (req, res) => {
try {
await parseImportFileBody(req, res)
// TODO return JSON error response rather then delegate to express.
// TODO Also return proper status codes.
// TODO File (or other form data) cound not be present. Add validation or fallback/errorhandling.
const { dictionaryId } = req.params
const importFilePath = req.file.path
const { deleteExistingEntries, entryStatus, importFileFormat } = req.body
await Dictionary.openImportFileJob(
dictionaryId,
deleteExistingEntries,
importFileFormat
)
if (deleteExistingEntries) {
await Entry.deleteAllFromIndex(dictionaryId)
await Entry.deleteAll(dictionaryId)
}
await Dictionary.importFromFile(
req.user.id,
dictionaryId,
importFilePath,
entryStatus
)
// TODO Rather then waiting for success/failure, return immediately and continue processing in the background.
// TODO Also add API endpoint for getting progress.
await Dictionary.indexIntoSearchEngine(dictionaryId)
// TODO Error --> import job status: (indexing) error --> User manually triggers reindex.
debug('IMPORT SUCCESSFUL')
const importProcessId = 'DUMMY ID' // TODO You'll get it from DB.
res.status(202).send(importProcessId)
} catch (error) {
// TODO Consider writing a property on req and add an extra error handler for API errors.
debug('IMPORT FAILED')
debug(error)
res.status(400).send(error)
} finally {
try {
await unlink(req.file.path)
} catch (error) {
debug('ERROR REMOVING TEMP IMPORT FILE:')
debug(error)
}
}
}
function importFileFilter(req, file, cb) {
if (file.mimetype !== 'text/xml') return cb(Error('Invalid file type'))
cb(null, true)
}
/*
function commaSeperationReducer(dictionaryData) {
return dictionaryData.reduce((acc, x) => {
if (acc === ':://') {
return x
} else {
return `${acc}, ${x}`
}
}, ':://')
} */
module.exports = dictionary
+150
View File
@@ -0,0 +1,150 @@
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
+156
View File
@@ -0,0 +1,156 @@
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 = []
if (req.user) {
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('pages/extraction/list', { title: 'Luščenje seznam', 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(`luscenje/${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('pages/extraction/edit-oss', {
title: 'KAS + dokumenti',
id: extractionId,
extraction,
allPrimaryDomains,
stopTermsFiles
})
} else {
const [extractionDocuments, stopTermsFiles] = await Promise.all([
Extraction.fetchAllDocumentsStats(extractionId),
Extraction.fetchAllStopTermsFilesStats(extractionId)
])
res.render('pages/extraction/edit-own', {
title: 'Besedila',
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('pages/extraction/docs-edit', {
id: extractionId,
extractionDocuments
})
}
extraction.stopTermsEdit = async (req, res) => {
const extractionId = req.params.id
const stopTermsFiles = await Extraction.fetchAllStopTermsFilesStats(
extractionId
)
res.render('pages/extraction/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('pages/extraction/term-candidates', {
extractionId,
termCandidatesJson,
firstPageOfTermCandidates,
hitsPerPage,
numberOfAllPages
})
}
module.exports = extraction
+211
View File
@@ -0,0 +1,211 @@
const { getInstanceSetting } = require('../../models/helpers')
const Dictionary = require('../../models/dictionary')
const cache = require('../../models/cache')
const { promisify } = require('util')
const email = require('../../models/email')
const MIN_ENTRIES_MAIL_NAMESPACE = 'below_min_entries_mail_sent'
// Helper object for minEntries below. Defines operations on anti-spam bookmarks.
const minEntriesEmailBookmark = {
// Checks whether the anti-spam bookmark (still) exists.
async exists(dictionaryId) {
const bookMarkName = `${MIN_ENTRIES_MAIL_NAMESPACE}:${dictionaryId}`
return !!(await cache.exists(bookMarkName))
},
// Sets the anti-spam bookmark for given dictionary.
async set(dictionaryId) {
const bookMarkName = `${MIN_ENTRIES_MAIL_NAMESPACE}:${dictionaryId}`
await cache.set(bookMarkName, true)
},
// Removes the anti-spam bookmark for given dictionary if conditions are met.
async update(dictionaryId) {
const doesBookmarkExist = await this.exists(dictionaryId)
if (!doesBookmarkExist) return
const minEntries = await getInstanceSetting('min_entries_per_dictionary')
const isBelowMinEntriesThreshold = await checkIfBelowMinEntriesThreshold(
dictionaryId,
minEntries
)
if (isBelowMinEntriesThreshold) return
const bookMarkName = `${MIN_ENTRIES_MAIL_NAMESPACE}:${dictionaryId}`
await cache.unlink(bookMarkName)
}
}
// 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) {
const minEntries = await getInstanceSetting('min_entries_per_dictionary')
// Only proceed if a valid and positive minimum entries per dictionary setting is set.
if (!(+minEntries > 0)) return
const [isBelowMinEntriesThreshold, wasEmailAlreadySent] = await Promise.all(
[
checkIfBelowMinEntriesThreshold(dictionaryId, minEntries),
minEntriesEmailBookmark.exists(dictionaryId)
]
)
if (!isBelowMinEntriesThreshold || wasEmailAlreadySent) return
// Prepare and send notification emails.
const [nameSl, adminEmails, dictionariesAdminEmails] = await Promise.all([
Dictionary.fetchName(dictionaryId),
Dictionary.fetchAdminEmails(dictionaryId),
Dictionary.fetchDictionariesAdminEmails()
])
const allEmailsSet = new Set([...adminEmails, ...dictionariesAdminEmails])
const allEmails = []
for (const email of allEmailsSet) allEmails.push(email)
const type = 'delete'
const renderAsync = promisify(appRef.render.bind(appRef))
const emailHtml = await renderAsync('email/dictionary-status-change', {
type,
nameSl
})
await email.send({
to: allEmails,
subject: 'Obvestilo o številu gesel',
html: emailHtml
})
await minEntriesEmailBookmark.set(dictionaryId)
},
// Use this method after any entry updating operations to release potential anti-spam bookmarks.
onUpdate: minEntriesEmailBookmark.update.bind(minEntriesEmailBookmark)
}
// Helper function to check if a given (published) dictionary has less entries than provided minEntries parameter.
async function checkIfBelowMinEntriesThreshold(dictionaryId, minEntries) {
const [publishedEntries, dictionaryStatus] = await Promise.all([
Dictionary.countPublishedEntries(dictionaryId),
Dictionary.fetchStatus(dictionaryId)
])
const isBelowMinEntriesThreshold =
+publishedEntries < +minEntries && dictionaryStatus === 'published'
return isBelowMinEntriesThreshold
}
// Determine new dictionary status based on dictionary_publish_approval setting.
exports.determineNewStatus = async isPublished => {
const publishApproval = await getInstanceSetting(
'dictionary_publish_approval'
)
let newStatus
if (publishApproval === 'F') {
newStatus = isPublished ? 'published' : 'closed'
} else {
newStatus = isPublished ? 'reviewed' : 'closed'
}
return newStatus
}
// Exports actions related to checking and acting on dictionary status changes.
exports.statusChangeCheckAndAct = {
// Notify dictionaries admins by email on dictionary status changes.
async updateUsers(
dictionaryId,
isPublishedNew,
oldDictStatus,
nameSl,
appRef,
user
) {
const isPublishedOld = oldDictStatus === 'published'
// Published: on -> off.
if (!isPublishedNew && isPublishedOld) {
const dictionariesAdminEmails =
await Dictionary.fetchDictionariesAdminEmails()
const type = 'unpublish'
await renderAndSendStatusChangeEmails(
appRef,
type,
user.email,
nameSl,
dictionariesAdminEmails
)
// Published: off -> on.
} else if (isPublishedNew && !isPublishedOld) {
const [isApprovalRequired, dictionariesAdminEmails] = await Promise.all([
getInstanceSetting('dictionary_publish_approval'),
Dictionary.fetchDictionariesAdminEmails()
])
const type =
isApprovalRequired === 'T' ? 'publish-approval' : 'publish-no-approval'
await renderAndSendStatusChangeEmails(
appRef,
type,
user.email,
nameSl,
dictionariesAdminEmails
)
}
},
// Notify dictionary admins by email after dictionaries admins
// change dictionary status from reviewed to either closed or published.
async updateAdminDescription(
dictionaryId,
statusNew,
statusOld,
appRef,
user
) {
if (statusOld === 'reviewed' && statusNew !== 'reviewed') {
const [nameSl, adminEmails] = await Promise.all([
Dictionary.fetchName(dictionaryId),
Dictionary.fetchAdminEmails(dictionaryId)
])
const type = 'status'
await renderAndSendStatusChangeEmails(
appRef,
type,
user.email,
nameSl,
adminEmails
)
}
}
}
// Helper function used by statusChangeCheckAndAct methods.
async function renderAndSendStatusChangeEmails(
appRef,
type,
changerEmail,
nameSl,
targetEmails
) {
const renderAsync = promisify(appRef.render.bind(appRef))
const emailHtml = await renderAsync('email/dictionary-status-change', {
type,
changerEmail,
nameSl
})
await email.send({
to: targetEmails,
subject: 'Sprememba stanja slovarja',
html: emailHtml
})
}
+11
View File
@@ -0,0 +1,11 @@
const Extraction = require('../../models/extraction')
// Check if extraction process can be started.
exports.checkIfcanBegin = async extraction => {
if (extraction.status !== 'new') return false
if (extraction.ossParams) return extraction.ossParams.status === 'confirmed'
const documentsNames = await Extraction.fetchAllDocumentsNames(extraction.id)
return !!documentsNames.length
}
@@ -0,0 +1,34 @@
const Dictionary = require('../../models/dictionary')
const Portal = require('../../models/portal')
const { getInstanceSetting } = require('../../models/helpers')
const helper = {}
helper.initialize = async () => {
const initializers = {}
// TODO Once english language is implemented, gather selected language (sl/en) from request ~ (cookies?)
const language = 'name_sl'
// TODO Consider parallelizing following queries. Single vs pooled clients?
initializers.allPrimaryDomains = await Dictionary.fetchAllPrimaryDomains()
initializers.sourceLanguages = await Dictionary.fetchAllLanguages(language)
initializers.targetLanguages = initializers.sourceLanguages.filter(
// drop slovene language
l => l.id !== 32
)
initializers.allDictionaryNames = await Dictionary.fetchAll()
initializers.portals = []
initializers.portals.push({
name: await getInstanceSetting('portal_name'),
code: await getInstanceSetting('portal_code')
})
initializers.portals.concat(await Portal.fetchAll())
return initializers
}
module.exports = helper
+386
View File
@@ -0,0 +1,386 @@
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 { intoDbArray } = require('../models/helpers')
const {
prepareEntries,
prepareAggregation,
prepareSeachFilterData
} = require('../models/helpers/search')
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')
// TODO Luka (note to self): Measure performance, consider caching.
exports.index = async (req, res) => {
const {
allPrimaryDomains,
sourceLanguages,
targetLanguages,
allDictionaryNames,
portals
} = await SFDSuggestionImporter.initialize()
const englishLanguageEnabled = false // dummy variable for future edit
const latestDicts = await Dictionary.fetchLatest3DictsByPublishDate(
englishLanguageEnabled
)
const portalName = await getInstanceSetting('portal_name')
const portalDescription = await getInstanceSetting('portal_description')
const isRoot = true
res.render('pages/index', {
allPrimaryDomains,
sourceLanguages,
targetLanguages,
allDictionaryNames,
portals,
latestDicts,
portalName,
portalDescription,
isRoot
})
}
exports.search = async (req, res) => {
const searchString = req.query.q?.trim()
if (!searchString) return res.redirect('/')
const {
allPrimaryDomains,
sourceLanguages,
targetLanguages,
allDictionaryNames,
portals
} = await SFDSuggestionImporter.initialize()
const filters = {
sourceLanguages: intoDbArray(req.query.sl, 'always'),
targetLanguages: intoDbArray(req.query.tl, 'always'),
primaryDomains: intoDbArray(req.query.pd, 'always'),
dictionaries: intoDbArray(req.query.d, 'always'),
sources: intoDbArray(req.query.s, 'always')
}
const hitsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const page = +req.query.p > 0 ? +req.query.p : 1
const [hitsQuery, aggregateQuery] = await generateQuery.main(
searchString,
filters,
hitsPerPage,
page,
true
)
const [hits, aggregationRaw] = await Promise.all([
searchEntryIndex(hitsQuery),
searchEntryIndex(aggregateQuery)
])
const numberOfAllHits = hits.body.hits.total.value
const numberOfAllPages = Math.ceil(numberOfAllHits / hitsPerPage)
// TODO If there's no hits, this is probably the place where suggestions/related queries and processing would happen.
let entriesByCategory = prepareEntries(hits)
// 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 searchFilterData = prepareSeachFilterData(aggregation, filters)
// TODO Luka (note to self): Consider reworking Miha's logic below. It's hacky and possibly error prone.
// TODO copied code below, maybe refactor to a helper function?
// check express/controllers/api/v1/search.js listMainEntries
entriesByCategory = Object.entries(entriesByCategory).reduce(
(acc, category, index) => {
acc[category[0]] = category[1].map(entry => {
if (entry.foreignEntries) {
entry.foreignEntries = entry.foreignEntries.map(fe => {
fe.terms = fe.terms ? fe.terms.filter(term => !!term) : []
fe.synonyms = fe.synonyms
? fe.synonyms.filter(synonym => !!synonym)
: []
fe.nbspCount = fe.terms.length + fe.synonyms.length - 1
return fe
})
}
return entry
})
return acc
},
{}
)
const count = Object.values(entriesByCategory).reduce((acc, cat, idx) => {
return acc + cat.length
}, 0)
// filter to 5 resuts per search filter
Object.entries(searchFilterData).forEach(([key, value]) => {
searchFilterData[key] = value.filter((p, i) => {
return i < 5
})
})
// todo implement ALL disabled in pug view if required
const disabledSideMenuFilters = {
sourceLanguages: searchString === '*',
targetLanguages: false,
primaryDomains: false,
dictionaries: false,
sources: false
}
if (count < 1) {
return res.render('pages/search/no-results', {
allPrimaryDomains,
sourceLanguages,
targetLanguages,
allDictionaryNames,
portals,
searchString,
entriesByCategory,
searchFilterData,
disabledSideMenuFilters,
// allAggregation,
numberOfAllHits,
numberOfAllPages,
page
})
}
// TODO Add a page title?
res.render('pages/search/results', {
allPrimaryDomains,
sourceLanguages,
targetLanguages,
allDictionaryNames,
portals,
searchString,
entriesByCategory,
searchFilterData,
disabledSideMenuFilters,
// allAggregation,
numberOfAllHits,
numberOfAllPages,
page
})
}
exports.entryDetails = async (req, res) => {
const termId = req.params.entryId
/* const [entry, domainLabels] = await Promise.all([
Entry.fetchFullWithOrderedForeignLanguages(termId),
Dictionary.fetchDomainLabelsFromEntryId(termId)
]) */
const entry = await Entry.fetchFullWithOrderedForeignLanguages(termId)
/* const entryData = {
entry
// allDomainLabelsJoined: domainLabels.map(e => e.name).join(', ')
} */
// unnecessary legacy assigment, refactor when time is available
const entryData = entry
const {
allPrimaryDomains,
sourceLanguages,
targetLanguages,
allDictionaryNames,
portals
} = await SFDSuggestionImporter.initialize()
const [dictStruct, dictionaryData, selectedDomainLabelsForEntry] =
await Promise.all([
Dictionary.fetchDictionaryWithEditStructure(entry.dictionary_id),
Dictionary.fetchDictionaryBasicInfo(entry.dictionary_id),
Entry.fetchDomainLabels(termId)
])
///
const filters = { ctxType: 'entry_dict_ext', ctxId: termId }
// TODO: integrate numberOfAllPages, commentCount with pug
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const page = 1 // +req.query.p > 0 ? +req.query.p : 1
const {
pages_total: numberOfAllPages,
comments,
comment_count: commentCount
} = await Comment.list(filters, req.user, resultsPerPage, page)
///
// check if it is a local dictionary
if (!dictionaryData.portalname && !dictionaryData.portalcode) {
dictionaryData[0].portalname = await getInstanceSetting('portal_name')
dictionaryData[0].portalcode = await getInstanceSetting('portal_code')
}
const reducedData = dictionaryData.reduce(
(acc, x) => {
if (acc.isbegin) {
x.languages = [x.languagesl]
x.subDomains = [x.domainsecondarysl]
return x
}
if (!acc.languages.includes(x.languagesl)) {
acc.languages.push(x.languagesl)
}
if (!acc.subDomains.includes(x.domainsecondarysl)) {
acc.subDomains.push(x.domainsecondarysl)
}
return acc
},
{ isbegin: true }
)
const { structure } = dictStruct
// Improved version of entropy, but some data duplications still exist
// struct data contains important data and re-maps for unification (maybe refactor later)
const structData = {
termId: termId,
prevWindowTitle: 'Iskanje',
prevHref: '/iskanje',
portalCode: dictionaryData[0].portalcode,
portalName: dictionaryData[0].portalname,
dictName: structure.nameSl,
dictHref: `/slovarji/${structure.id}/o-slovarju?sentFromEntryId=${termId}`,
fullAuthorName: reducedData.author ? reducedData.author.join(', ') : '',
areas: dictionaryData[0].domain_primary,
subareas: reducedData.subDomains ? reducedData.subDomains.join(', ') : '',
languages: reducedData.languages ? reducedData.languages.join(', ') : ''
}
if (reducedData.author) {
if (reducedData.author.length > 2) {
structData.authorLabel = 'Avtorji'
} else if (reducedData.author.length === 2) {
structData.authorLabel = 'Avtorja'
} else if (reducedData.author.length === 1) {
structData.authorLabel = 'Avtor'
}
}
const finalData = {
...reducedData,
...structData
}
let selectedDomainLabelsForEntryString = ''
if (selectedDomainLabelsForEntry.length) {
selectedDomainLabelsForEntryString = mergeDomains(
selectedDomainLabelsForEntry,
(acc, n) => {
if (acc === '') return n.name
else return acc + ', ' + n.name
}
)
}
/* entryData.entry.foreign_entries.forEach((val, idx) => {
entry.foreignEntries[idx] = await
}) */
const langs = await Dictionary.fetchLanguages(entryData.dictionary_id)
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
} catch (e) {}
})
res.render('pages/search/result-detail', {
allPrimaryDomains,
entryData,
sourceLanguages,
targetLanguages,
allDictionaryNames,
portals,
termId,
structure,
finalData,
selectedDomainLabelsForEntryString,
numberOfAllPages,
comments,
commentCount
})
}
exports.myProfile = async (req, res) => {
res.render('pages/profile/my-profile', { title: 'Moj račun' })
}
exports.changePassword = async (req, res) => {
res.render('pages/profile/change-password', { title: 'Spremeni geslo' })
}
exports.userSettings = async (req, res) => {
const hitsPerPageArr = await User.fetchAllowedHitsPerPage()
res.render('pages/profile/change-profile-settings', {
title: 'Nastavitve računa',
hitsPerPageArr,
hitsForUser: req.user?.hitsPerPage
})
}
function mergeDomains(
domainList,
aggregationFn = (acc, n) => {
if (acc === '') return n
else return acc + ', ' + n
}
) {
return Array.from(domainList).reduce(aggregationFn, '')
}
function filterResults(entries) {
let mode = 0
const termLst = []
const ftermLst = []
const otherLst = []
entries.forEach(entry => {
switch (entry._title) {
case 'term':
mode = 0
break
case 'foreignTerm':
mode = 1
break
case 'other':
mode = 2
}
switch (mode) {
case 0:
termLst.push(entry)
break
case 1:
ftermLst.push(entry)
break
case 2:
otherLst.push(entry)
break
}
})
return [termLst, ftermLst, otherLst]
}
+140
View File
@@ -0,0 +1,140 @@
const portal = {}
const Portal = require('../models/portal')
const Comment = require('../models/comment')
const { clearCachedInstanceSettings } = require('../models/helpers')
const { DEFAULT_HITS_PER_PAGE } = require('../config/settings')
portal.instanceSettings = async (req, res) => {
const portal = await Portal.fetchInstanceSettings()
res.render('pages/admin/portal', {
title: 'Nastavitve portala',
portal
})
}
portal.updateInstaceSettings = async (req, res) => {
const payload = req.body
await Portal.updateInstaceSettings(payload)
await clearCachedInstanceSettings()
res.redirect('/admin/nastavitve/portal')
}
portal.instanceDictSettings = async (req, res) => {
const dictionary = await Portal.fetchInstanceDictSettings()
res.render('pages/admin/settings-dictionaries', {
title: 'Nastavitve slovarjev',
dictionary
})
}
portal.updateInstanceDictSettings = async (req, res) => {
const payload = req.body
await Portal.updateInstaceDictSettings(payload)
await clearCachedInstanceSettings()
res.redirect('/admin/nastavitve/slovarji')
}
portal.instanceConsultancySettings = async (req, res) => {
const consultancy = await Portal.fetchInstanceConsultancySettings()
res.render('pages/admin/portal-consultancy-settings', {
title: 'Nastavitve svetovalnice',
consultancy
})
}
portal.updateInstanceConusltacySettings = async (req, res) => {
const payload = req.body
await Portal.updateInstaceConsultancySettings(payload)
await clearCachedInstanceSettings()
res.redirect('/admin/nastavitve/svetovalnica')
}
portal.new = async (req, res) => {
res.render('pages/admin/new-connection', {
title: 'Nova povezava'
})
}
portal.list = async (req, res) => {
const allLinkedPortals = await Portal.fetchAll()
res.render('pages/admin/connections-list', {
title: 'Seznam povezav',
allLinkedPortals
})
}
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 })
}
portal.updatePortal = async (req, res) => {
const portalId = req.params.portalId
const payload = req.body
await Portal.update(portalId, payload)
res.redirect('/admin/povezave/seznam')
}
portal.fetchSelectedLinkedDictionaries = async (req, res) => {
const linkedId = req.params.portalId
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const { pages_total: numberOfAllPages, results } =
await Portal.fetchSelectedLinkedDictionaries(linkedId, resultsPerPage, 1)
res.render('pages/admin/portal-list-dict', {
title: 'Slovarji portala',
linkedId,
numberOfAllPages,
results
})
}
portal.updateSelectedDictionaries = async (req, res) => {
const linkedId = req.params.portalId
await Portal.updateSelectedDictionaries(linkedId, req.body)
res.redirect('back')
}
portal.fetchAllLinkedDictionaries = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const { pages_total: numberOfAllPages, results } =
await Portal.fetchAllLinkedDictionaries(resultsPerPage, 1)
res.render('pages/admin/portals-all-linked-dictionaries', {
title: 'Povezani',
numberOfAllPages,
results
})
}
portal.updateAllDictionaries = async (req, res) => {
await Portal.updateAllDictionaries(req.body)
res.redirect('back')
}
portal.comments = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const type = 'portal'
const filters = { ctxType: type }
const { comments, pages_total: numberOfAllPages } = await Comment.list(
filters,
req.user,
resultsPerPage,
1
)
res.render('pages/admin/comments', {
title: 'Komentarji',
numberOfAllPages,
dictionary: { id: req.params.dictionaryId },
comments
})
}
module.exports = portal
+142
View File
@@ -0,0 +1,142 @@
const { randomBytes } = require('crypto')
const { promisify } = require('util')
const passport = require('passport')
const User = require('../models/user')
const email = require('../models/email')
const { origin } = require('../config/keys')
const { rememberMeCookieSettings } = require('../config/settings')
const RandomBytesAsync = promisify(randomBytes)
const { DEFAULT_HITS_PER_PAGE } = require('../config/settings')
const user = {}
user.register = async (req, res) => {
// TODO Add validation.
const userId = await User.create(req.body)
const activationToken = (await RandomBytesAsync(32)).toString('hex')
await User.saveActivationToken(userId, activationToken)
const { email: userEmail, username } = req.body
let activationLink = new URL('/users/activate', origin)
activationLink.searchParams.set('token', activationToken)
activationLink = activationLink.href
const renderAsync = promisify(req.app.render.bind(req.app))
const emailHtml = await renderAsync('email/user-activation', {
username,
activationLink
})
await email.send({
to: userEmail,
subject: 'Aktivacija računa',
html: emailHtml
})
res.send('Registracija uspešna')
}
user.activateAccount = async (req, res) => {
// TODO Add validation. What if user is already logged in? What if account is already active? ...
const { token } = req.query
const user = await User.fetchByActivationToken(token)
await User.activateAccount(user)
const loginAsync = promisify(req.login.bind(req))
await loginAsync(user)
res.redirect('/')
}
user.login = async (req, res, next) => {
passport.authenticate(
'local',
{
badRequestMessage:
'Nepravilno uporabniško ime, elektronski naslov ali geslo.'
},
async (err, user, info) => {
if (err) return next(err)
if (!user) {
const err = Error(info.message)
err.status = 403
err.displayInProd = true
return next(err)
}
const loginAsync = promisify(req.login.bind(req))
await loginAsync(user)
if (req.body.rememberMe) {
// TODO Consider what happens if the user already has a remember me token.
const rememberMeToken = await User.generateRememberMeToken()
await User.saveRememberMeToken(user, rememberMeToken)
res.cookie('remember_me', rememberMeToken, rememberMeCookieSettings)
}
res.send('Prijava uspešna')
}
)(req, res, next)
}
user.logout = async (req, res) => {
const rememberMeToken = req.signedCookies.remember_me
if (rememberMeToken) {
res.clearCookie('remember_me')
await User.clearRememberMeToken(rememberMeToken)
}
req.logout()
// Manually clear session.passport due to bug in current passport version.
delete req.session.passport.user
res.redirect('/')
}
user.list = async (req, res) => {
const resultsPerPage = req.user?.hitsPerPage || DEFAULT_HITS_PER_PAGE
const { pages_total: numberOfAllPages, results } = await User.fetchAll(
resultsPerPage,
1
)
res.render('pages/admin/user-list', {
title: 'Seznam slovarjeva',
numberOfAllPages,
results
})
}
user.listAllWithPortalRoles = async (req, res) => {
const users = await User.fetchAllWithPortalRoles()
res.render('pages/admin/user-portals', { title: 'Seznam slovarjev', users })
}
user.findByUsernameOrEmail = async (req, res) => {
const userNameEmail = req.query.userNameEmail
const searchedUser = await User.findByUsernameOrEmail(userNameEmail)
res.send(searchedUser)
}
user.updateRoles = async (req, res) => {
await User.updatePortalRoles(req.body.rolesPerUser)
res.redirect('back')
}
user.adminEdit = async (req, res) => {
const userId = req.params.userId
const [userData, userRoles] = await Promise.all([
User.fetchUser(userId),
User.fetchUserRoles(userId)
])
res.render('pages/admin/user-edit', {
title: 'Urejanje uporabnikov',
userData,
userRoles
})
}
// TODO Aljaž: Luka, please adjust update function for updating user's password and email
// TODO Handle username unique constraint failure.
user.adminUpdate = async (req, res) => {
const { userId } = req.params
await User.updateUser(userId, req.body)
res.redirect('back')
}
module.exports = user