diff --git a/components/Literature.vue b/components/Literature.vue index 52d839c0..abfaa35f 100644 --- a/components/Literature.vue +++ b/components/Literature.vue @@ -21,7 +21,7 @@ - + diff --git a/components/SourceList.vue b/components/SourceList.vue index 5353630e..f5898d3f 100644 --- a/components/SourceList.vue +++ b/components/SourceList.vue @@ -2,7 +2,7 @@
    -
  • +
@@ -10,51 +10,13 @@ diff --git a/routes/pronoun.vue b/routes/pronoun.vue index 7a95f76e..f2054a30 100644 --- a/routes/pronoun.vue +++ b/routes/pronoun.vue @@ -90,8 +90,8 @@ -
- +
+
@@ -112,6 +112,7 @@ import {head} from "../src/helpers"; import GrammarTables from "../data/pronouns/GrammarTables"; import LinkedText from "../components/LinkedText"; + import {SourceLibrary} from "../src/classes"; export default { components: {LinkedText, GrammarTables }, @@ -132,6 +133,11 @@ counter: 0, } }, + async asyncData({app}) { + return { + sources: await app.$axios.$get(`/sources`), + } + }, mounted() { if (process.client) { setInterval(_ => this.counter++, 1000); @@ -149,8 +155,8 @@ }, }, computed: { - sources() { - return getSources(this.selectedPronoun); + sourceLibrary() { + return new SourceLibrary(this.sources); }, }, } diff --git a/routes/sources.vue b/routes/sources.vue index b7f7639d..22ea84c2 100644 --- a/routes/sources.vue +++ b/routes/sources.vue @@ -49,7 +49,14 @@ -
  • +
  • +

    + + pronouns.alt.header + +

    +
  • +
  • pronouns.others @@ -87,8 +94,8 @@

  • -
    - +
    +

    {{ pronoun.description }} @@ -98,8 +105,9 @@

    -
    - + +
    +

    pronouns.alt.header @@ -109,8 +117,8 @@

    -
    - +
    +

    pronouns.others

    @@ -120,16 +128,14 @@ diff --git a/server/routes/inclusive.js b/server/routes/inclusive.js index fd568cad..ae0783f6 100644 --- a/server/routes/inclusive.js +++ b/server/routes/inclusive.js @@ -23,23 +23,25 @@ const router = Router(); router.get('/inclusive', async (req, res) => { return res.json(await req.db.all(SQL` - SELECT * FROM inclusive - WHERE locale = ${req.config.locale} - AND approved >= ${req.admin ? 0 : 1} - AND deleted = 0 - ORDER BY approved, insteadOf + SELECT i.*, u.username AS author FROM inclusive i + LEFT JOIN users u ON i.author_id = u.id + WHERE i.locale = ${req.config.locale} + AND i.approved >= ${req.admin ? 0 : 1} + AND i.deleted = 0 + ORDER BY i.approved, i.insteadOf `)); }); router.get('/inclusive/search/:term', async (req, res) => { const term = '%' + req.params.term + '%'; return res.json(await req.db.all(SQL` - SELECT * FROM inclusive - WHERE locale = ${req.config.locale} - AND approved >= ${req.admin ? 0 : 1} - AND deleted = 0 - AND (insteadOf like ${term} OR say like ${term}) - ORDER BY approved, insteadOf + SELECT i.*, u.username AS author FROM inclusive i + LEFT JOIN users u ON i.author_id = u.id + WHERE i.locale = ${req.config.locale} + AND i.approved >= ${req.admin ? 0 : 1} + AND i.deleted = 0 + AND (i.insteadOf like ${term} OR i.say like ${term}) + ORDER BY i.approved, i.insteadOf `)); }); diff --git a/server/routes/nouns.js b/server/routes/nouns.js index 6dbd6abe..4ebae9f6 100644 --- a/server/routes/nouns.js +++ b/server/routes/nouns.js @@ -30,7 +30,7 @@ router.get('/nouns', async (req, res) => { SELECT n.*, u.username AS author FROM nouns n LEFT JOIN users u ON n.author_id = u.id WHERE n.locale = ${req.config.locale} - AND deleted = 0 + AND n.deleted = 0 AND n.approved >= ${req.admin ? 0 : 1} ORDER BY n.approved, n.masc `)); @@ -39,12 +39,13 @@ router.get('/nouns', async (req, res) => { router.get('/nouns/search/:term', async (req, res) => { const term = '%' + req.params.term + '%'; return res.json(await req.db.all(SQL` - SELECT * FROM nouns - WHERE locale = ${req.config.locale} - AND approved >= ${req.admin ? 0 : 1} - AND deleted = 0 - AND (masc like ${term} OR fem like ${term} OR neutr like ${term} OR mascPl like ${term} OR femPl like ${term} OR neutrPl like ${term}) - ORDER BY approved, masc + SELECT n.*, u.username AS author FROM nouns n + LEFT JOIN users u ON n.author_id = u.id + WHERE n.locale = ${req.config.locale} + AND n.approved >= ${req.admin ? 0 : 1} + AND n.deleted = 0 + AND (n.masc like ${term} OR n.fem like ${term} OR n.neutr like ${term} OR n.mascPl like ${term} OR n.femPl like ${term} OR n.neutrPl like ${term}) + ORDER BY n.approved, n.masc `)); }); diff --git a/server/routes/sources.js b/server/routes/sources.js index 706e834f..638328b7 100644 --- a/server/routes/sources.js +++ b/server/routes/sources.js @@ -1,56 +1,105 @@ import { Router } from 'express'; -import mailer from "../../src/mailer"; -import { camelCase } from "../../src/helpers"; -import { loadTsv } from '../loader'; +import SQL from "sql-template-strings"; +import {ulid} from "ulid"; -const generateId = title => { - return camelCase(title.split(' ').slice(0, 2)); -} - -const buildEmail = (data, user) => { - const human = [ - `
  • user: ${user ? user.username : ''}
  • `, - `
  • pronouns: ${data.pronouns}
  • `, - ]; - const tsv = [generateId(data.title) || '???']; - - for (let field of ['type','author','title','extra','year','fragments','comment','link']) { - human.push(`
  • ${field}: ${field === 'fragments' ? `
    ${data[field]}
    `: data[field]}
  • `); - tsv.push(field === 'fragments' ? (data[field].join('@').replace(/\n/g, '|')) : data[field]); +const approve = async (db, id) => { + const { base_id } = await db.get(SQL`SELECT base_id FROM sources WHERE id=${id}`); + if (base_id) { + await db.get(SQL` + UPDATE sources + SET deleted=1 + WHERE id = ${base_id} + `); } - tsv.push(user ? user.id : ''); - - return `
      ${human.join('')}
    ${tsv.join('\t')}
    `; -} - -const loadSources = () => { - return loadTsv('sources/sources').map(s => { - if (s.author) { - s.author = s.author.replace('^', ''); - } - s.fragments = s.fragments.split('@').map(f => f.replace(/\|/g, '\n')); - return s; - }); + await db.get(SQL` + UPDATE sources + SET approved = 1, base_id = NULL + WHERE id = ${id} + `); } const router = Router(); router.get('/sources', async (req, res) => { - return res.json(loadSources()); + return res.json(await req.db.all(SQL` + SELECT s.*, u.username AS submitter FROM sources s + LEFT JOIN users u ON s.submitter_id = u.id + WHERE s.locale = ${req.config.locale} + AND s.deleted = 0 + AND s.approved >= ${req.admin ? 0 : 1} + `)); }); -router.get('/sources/:key', async (req, res) => { - return res.json([...loadSources().filter(s => s.key === req.params.key), null][0]); +router.get('/sources/:id', async (req, res) => { + return res.json(await req.db.all(SQL` + SELECT s.*, u.username AS submitter FROM sources s + LEFT JOIN users u ON s.submitter_id = u.id + WHERE s.locale = ${req.config.locale} + AND s.deleted = 0 + AND s.approved >= ${req.admin ? 0 : 1} + AND s.id = ${req.params.id} + `)); }); router.post('/sources/submit', async (req, res) => { - const emailBody = buildEmail(req.body, req.user); + console.log(req.body.fragments); + console.log(req.body.fragments.join('@')); + console.log(req.body.fragments.join('@').replace(/\n/g, '|')); - for (let admin of process.env.MAILER_ADMINS.split(',')) { - mailer(admin, `[Pronouns][${req.config.locale}] Source submission`, undefined, emailBody); + const id = ulid(); + await req.db.get(SQL` + INSERT INTO sources (id, locale, pronouns, type, author, title, extra, year, fragments, comment, link, submitter_id, base_id) + VALUES ( + ${id}, ${req.config.locale}, ${req.body.pronouns.join(';')}, + ${req.body.type}, ${req.body.author}, ${req.body.title}, ${req.body.extra}, ${req.body.year}, + ${req.body.fragments.join('@').replace(/\n/g, '|')}, ${req.body.comment}, ${req.body.link}, + ${req.user ? req.user.id : null}, ${req.body.base} + ) + `); + + if (req.admin) { + await approve(req.db, id); } - return res.json({ result: 'ok' }); + return res.json('ok'); +}); + +router.post('/sources/hide/:id', async (req, res) => { + if (!req.admin) { + res.status(401).json({error: 'Unauthorised'}); + } + + await req.db.get(SQL` + UPDATE sources + SET approved = 0 + WHERE id = ${req.params.id} + `); + + return res.json('ok'); +}); + +router.post('/sources/approve/:id', async (req, res) => { + if (!req.admin) { + res.status(401).json({error: 'Unauthorised'}); + } + + await approve(req.db, req.params.id); + + return res.json('ok'); +}); + +router.post('/sources/remove/:id', async (req, res) => { + if (!req.admin) { + res.status(401).json({error: 'Unauthorised'}); + } + + await req.db.get(SQL` + UPDATE sources + SET deleted=1 + WHERE id = ${req.params.id} + `); + + return res.json('ok'); }); export default router; diff --git a/src/buildPronoun.js b/src/buildPronoun.js index dfc525cd..4351abf0 100644 --- a/src/buildPronoun.js +++ b/src/buildPronoun.js @@ -21,6 +21,26 @@ export const getPronoun = (pronouns, id) => { return addAliasesToPronouns(pronouns)[id]; } +const buildPronounFromTemplate = (key, template) => { + return new Pronoun( + key, + template.description, + template.normative || false, + buildDict(function*(morphemes) { + for (let k in morphemes) { + if (morphemes.hasOwnProperty(k)) { + yield [k, morphemes[k].replace(/#/g, key)]; + } + } + }, template.morphemes), + [template.plural || false], + [template.pluralHonorific || false], + template.aliases || [], + template.history || '', + false, + ); +} + export const buildPronoun = (pronouns, path) => { const pronounsWithAliases = addAliasesToPronouns(pronouns); @@ -66,7 +86,6 @@ export const buildPronoun = (pronouns, path) => { [ p[p.length - 1].endsWith('selves') ], // TODO English specific, extract somewhere [ false ], [], - [], null, false, ) @@ -93,7 +112,6 @@ export const parsePronouns = (pronounsRaw) => { }), [t.plural], [t.pluralHonorific], - t.sources ? t.sources.split(',') : [], aliases.slice(1), t.history, t.pronounceable, @@ -103,23 +121,3 @@ export const parsePronouns = (pronounsRaw) => { }); } -export const buildPronounFromTemplate = (key, template) => { - return new Pronoun( - key, - template.description, - template.normative || false, - buildDict(function*(morphemes) { - for (let k in morphemes) { - if (morphemes.hasOwnProperty(k)) { - yield [k, morphemes[k].replace(/#/g, key)]; - } - } - }, template.morphemes), - [template.plural || false], - [template.pluralHonorific || false], - template.sources || [], - template.aliases || [], - template.history || null, - false, - ) -} diff --git a/src/classes.js b/src/classes.js index abe8c8e3..41f9ef3a 100644 --- a/src/classes.js +++ b/src/classes.js @@ -96,16 +96,20 @@ function clone(mainObject) { } export class Source { - constructor (type, author, title, extra, year, fragments = [], comment = null, link = null, submitter = null) { + constructor ({id, pronouns, type, author, title, extra, year, fragments = '', comment = null, link = null, submitter = null, approved, base_id = null,}) { + this.id = id; + this.pronouns = pronouns ? pronouns.split(';') : []; this.type = type; this.author = author; this.title = title; this.extra = extra; this.year = year; - this.fragments = fragments; + this.fragments = fragments ? fragments.replace(/\|/g, '\n').split('@') : []; this.comment = comment; this.link = link; this.submitter = submitter; + this.approved = approved; + this.base_id = base_id; } static get TYPES() { @@ -138,6 +142,101 @@ export class Source { } } + +export class SourceLibrary { + constructor(rawSources) { + this.sources = rawSources.map(s => new Source(s)); + this.map = {}; + const multiple = new Set(); + const pronouns = new Set(); + + for (let source of this.sources) { + if (!source.pronouns.length) { + if (this.map[''] === undefined) { this.map[''] = []; } + this.map[''].push(source); + continue; + } + for (let pronoun of source.pronouns) { + if (this.map[pronoun] === undefined) { this.map[pronoun] = []; } + this.map[pronoun].push(source); + + pronouns.add(pronoun); + if (pronoun.includes('&')) { + multiple.add(pronoun); + } + } + } + this.pronouns = [...pronouns]; + this.multiple = [...multiple]; + this.cache = {} + } + + getForPronoun(pronoun) { + if (this.cache[pronoun] === undefined) { + let sources = this.map[pronoun] || []; + + if (pronoun === '') { + for (let p of this.pronouns) { + // add pronouns that have never been requested to "other" + if (this.cache[p] === undefined) { + sources = [...sources, ...this.map[p]]; + } + } + } + + this.cache[pronoun] = sources + .map(s => this.addMetaData(s)) + .sort((a, b) => { + if (a.typePriority !== b.typePriority) { + return b.typePriority - a.typePriority; + } + + return a.sortString.localeCompare(b.sortString); + }); + } + + return this.cache[pronoun]; + } + + getForPronounExtended(pronoun) { + let sources = {}; + const s = this.getForPronoun(pronoun); + sources[pronoun] = s.length ? s : undefined; + + if (pronoun.includes('&')) { + for (let option of pronoun.split('&')) { + const s = this.getForPronoun(option); + sources[option] = s.length ? s : undefined; + } + } + + return sources; + } + + addMetaData(source) { + source.typePriority = Source.TYPES_PRIORITIES[source.type]; + + source.sortString = source.author || 'ZZZZZ' + source.title; // if no author, put on the end + if (source.sortString.includes('^')) { + const index = source.sortString.indexOf('^'); + source.sortString = source.sortString.substring(index + 1) + ' ' + source.sortString.substring(0, index); + } + + source.index = [ + (source.author || '').replace('^', ''), + source.title, + source.extra, + source.year, + //...source.fragments, + source.comment, + source.link, + ].join(' ').toLowerCase().replace(/<\/?[^>]+(>|$)/g, ''); + + return source; + } +} + + const escape = s => { if (Array.isArray(s)) { s = s.join('&'); @@ -152,7 +251,7 @@ const escape = s => { } export class Pronoun { - constructor (canonicalName, description, normative, morphemes, plural, pluralHonorific, sources = [], aliases = [], history = null, pronounceable = true) { + constructor (canonicalName, description, normative, morphemes, plural, pluralHonorific, aliases = [], history = '', pronounceable = true) { this.canonicalName = canonicalName; this.description = description; this.normative = normative; @@ -166,7 +265,6 @@ export class Pronoun { } this.plural = plural; this.pluralHonorific = pluralHonorific; - this.sources = sources; this.aliases = aliases; this.history = history; this.pronounceable = pronounceable; @@ -194,7 +292,17 @@ export class Pronoun { } clone() { - return new Pronoun(this.canonicalName, this.description, this.normative, clone(this.morphemes), [...this.plural], [...this.pluralHonorific], this.pronounceable); + return new Pronoun( + this.canonicalName, + this.description, + this.normative, + clone(this.morphemes), + [...this.plural], + [...this.pluralHonorific], + [...this.aliases], + this.history, + this.pronounceable + ); } equals(other) { @@ -214,6 +322,8 @@ export class Pronoun { }, this, other), [...this.plural, ...other.plural], [...this.pluralHonorific, ...other.pluralHonorific], + [], + '', false, ); } @@ -319,6 +429,8 @@ export class Pronoun { m, data[MORPHEMES.length].split('').map(p => parseInt(p) === 1), data[MORPHEMES.length + 1].split('').map(p => parseInt(p) === 1), + [], + null, false, ) } diff --git a/src/data.js b/src/data.js index 6acc8d59..3b256d23 100644 --- a/src/data.js +++ b/src/data.js @@ -1,7 +1,6 @@ import {Source, Example, NounTemplate, PronounGroup, PronounLibrary, Name, Person, NounDeclension} from './classes' import { buildDict, buildList } from './helpers'; -import { parsePronouns, getPronoun } from './buildPronoun'; -import sourcesForMultipleForms from '../data/sources/sourcesMultiple'; +import { parsePronouns } from './buildPronoun'; export const socialProviders = { twitter: { name: 'Twitter' }, @@ -23,55 +22,6 @@ export const examples = buildList(function* () { } }); -import sourcesRaw from '../data/sources/sources.tsv'; -export const sources = buildDict(function* () { - for (let s of sourcesRaw) { - yield [ - s.key, - new Source( - s.type, - s.author, - s.title, - s.extra, - s.year, - s.fragments ? s.fragments.replace(/\|/g, '\n').split('@') : [], - s.comment, - s.link, - ) - ]; - } -}); - -export const getSources = (selectedPronoun) => { - if (!selectedPronoun) { - return {}; - } - - let sources = {}; - for (let multiple in sourcesForMultipleForms) { - if (sourcesForMultipleForms.hasOwnProperty(multiple)) { - if (multiple === selectedPronoun.canonicalName) { - sources[multiple] = sourcesForMultipleForms[multiple]; - } - } - } - for (let option of selectedPronoun.nameOptions()) { - const pronoun = getPronoun(pronouns, option); - if (pronoun && pronoun.sources.length) { - sources[option] = pronoun.sources; - } - } - - if (Object.keys(sources).length === 0) { - const pronoun = getPronoun(pronouns, selectedPronoun.canonicalName); - if (pronoun && pronoun.sources.length) { - sources[selectedPronoun.canonicalName] = pronoun.sources; - } - } - - return sources; -} - import nounTemplatesRaw from '../data/nouns/nounTemplates.tsv'; export const nounTemplates = buildList(function* () { for (let t of nounTemplatesRaw) {