Initial commit
This commit is contained in:
@@ -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')
|
||||
}
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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 }
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user