mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-22 12:03:25 -04:00
Merge branch 'inklu' into main
# Conflicts: # server/index.js # server/routes/nouns.js # src/helpers.js
This commit is contained in:
commit
9c2d543267
256
components/InclusiveDictionary.vue
Normal file
256
components/InclusiveDictionary.vue
Normal file
@ -0,0 +1,256 @@
|
||||
<template>
|
||||
<Loading :value="entriesRaw">
|
||||
<section v-if="$admin()" class="px-3">
|
||||
<div class="alert alert-info">
|
||||
<strong>{{ entriesCountApproved() }}</strong> <T>nouns.approved</T>,
|
||||
<strong>{{ entriesCountPending() }}</strong> <T>nouns.pending</T>.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="sticky-top">
|
||||
<div class="input-group mb-3 bg-white">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text">
|
||||
<Icon v="filter"/>
|
||||
</span>
|
||||
</div>
|
||||
<input class="form-control border-primary" v-model="filter" :placeholder="$t('crud.filterLong')" ref="filter"/>
|
||||
<div class="input-group-append" v-if="filter">
|
||||
<button class="btn btn-outline-danger" @click="filter = ''; $refs.filter.focus()">
|
||||
<Icon v="times"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-outline-success" @click="$refs.form.$el.scrollIntoView()">
|
||||
<Icon v="plus-circle"/>
|
||||
<T>nouns.submit.action</T>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<Table :data="visibleEntries()" :columns="$admin() ? 4 : 3" :marked="(el) => !el.approved" fixed ref="dictionarytable">
|
||||
<template v-slot:header>
|
||||
<th class="text-nowrap">
|
||||
<Icon v="comment-times"/>
|
||||
<T>nouns.inclusive.insteadOf</T>
|
||||
</th>
|
||||
<th class="text-nowrap">
|
||||
<Icon v="comment-check"/>
|
||||
<T>nouns.inclusive.say</T>
|
||||
</th>
|
||||
<th class="text-nowrap">
|
||||
<Icon v="comment-dots"/>
|
||||
<T>nouns.inclusive.because</T>
|
||||
</th>
|
||||
<th v-if="$admin()"></th>
|
||||
</template>
|
||||
|
||||
<template v-slot:row="s"><template v-if="s">
|
||||
<td>
|
||||
<ul class="list-untyled">
|
||||
<li v-for="w in s.el.insteadOf">{{w}}</li>
|
||||
</ul>
|
||||
|
||||
<small v-if="s.el.base && entries[s.el.base]">
|
||||
<p><strong><T>nouns.edited</T>:</strong></p>
|
||||
<ul class="list-untyled">
|
||||
<li v-for="w in entries[s.el.base].insteadOf">{{w}}</li>
|
||||
</ul>
|
||||
</small>
|
||||
|
||||
<button v-if="!$admin()" class="btn btn-outline-primary btn-sm m-1 hover-show" @click="edit(s.el)">
|
||||
<Icon v="pen"/>
|
||||
<T>nouns.edit</T>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<ul class="list-untyled">
|
||||
<li v-for="w in s.el.say">{{w}}</li>
|
||||
</ul>
|
||||
|
||||
<small v-if="s.el.base && entries[s.el.base]">
|
||||
<p><strong><T>nouns.edited</T>:</strong></p>
|
||||
<ul class="list-untyled">
|
||||
<li v-for="w in entries[s.el.base].say">{{w}}</li>
|
||||
</ul>
|
||||
</small>
|
||||
</td>
|
||||
<td>
|
||||
<p v-for="p in s.el.because.split('\n\n')">{{p}}</p>
|
||||
|
||||
<small v-if="s.el.base && entries[s.el.base]">
|
||||
<p><strong><T>nouns.edited</T>:</strong></p>
|
||||
<ul class="list-untyled">
|
||||
<p v-for="p in entries[s.el.base].because.split('\n\n')">{{p}}</p>
|
||||
</ul>
|
||||
</small>
|
||||
</td>
|
||||
<td v-if="$admin()">
|
||||
<ul class="list-unstyled">
|
||||
<li v-if="!s.el.approved">
|
||||
<button class="btn btn-success btn-sm m-1" @click="approve(s.el)">
|
||||
<Icon v="check"/>
|
||||
<T>crud.approve</T>
|
||||
</button>
|
||||
</li>
|
||||
<li v-else @click="hide(s.el)">
|
||||
<button class="btn btn-outline-secondary btn-sm m-1">
|
||||
<Icon v="times"/>
|
||||
<T>crud.hide</T>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-outline-danger btn-sm m-1" @click="remove(s.el)">
|
||||
<Icon v="trash"/>
|
||||
<T>crud.remove</T>
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-outline-primary btn-sm m-1" @click="edit(s.el)">
|
||||
<Icon v="pen"/>
|
||||
<T>crud.edit</T>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</td>
|
||||
</template></template>
|
||||
|
||||
<template v-slot:empty>
|
||||
<Icon v="search"/>
|
||||
<T>nouns.empty</T>
|
||||
</template>
|
||||
</Table>
|
||||
|
||||
<template v-if="config.nouns.submit">
|
||||
<Separator icon="plus"/>
|
||||
|
||||
<div class="px-3">
|
||||
<InclusiveSubmitForm ref="form"/>
|
||||
</div>
|
||||
</template>
|
||||
</Loading>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { InclusiveEntry } from "~/src/classes";
|
||||
import { buildDict } from "../src/helpers";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
load: {type: Boolean}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
filter: '',
|
||||
entriesRaw: undefined,
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
if (this.load) {
|
||||
this.loadEntries();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async loadEntries() {
|
||||
if (this.entriesRaw !== undefined) {
|
||||
return;
|
||||
}
|
||||
this.entriesRaw = await this.$axios.$get(`/inclusive`);
|
||||
},
|
||||
async setFilter(filter) {
|
||||
this.filter = filter;
|
||||
await this.loadEntries();
|
||||
this.focus();
|
||||
},
|
||||
focus() {
|
||||
this.$el.focus();
|
||||
this.$el.scrollIntoView();
|
||||
setTimeout(_ => {
|
||||
this.$el.scrollIntoView();
|
||||
}, 1000);
|
||||
},
|
||||
edit(entry) {
|
||||
this.$refs.form.edit(entry);
|
||||
},
|
||||
async approve(entry) {
|
||||
await this.$axios.$post(`/inclusive/approve/${entry.id}`);
|
||||
if (entry.base) {
|
||||
delete this.entries[entry.base];
|
||||
}
|
||||
entry.approved = true;
|
||||
entry.base = null;
|
||||
this.$forceUpdate();
|
||||
},
|
||||
async hide(entry) {
|
||||
await this.$axios.$post(`/inclusive/hide/${entry.id}`);
|
||||
entry.approved = false;
|
||||
this.$forceUpdate();
|
||||
},
|
||||
async remove(entry) {
|
||||
if (!confirm('Czy na pewno usunąć ten wpis?')) {
|
||||
return false;
|
||||
}
|
||||
await this.$axios.$post(`/inclusive/remove/${entry.id}`);
|
||||
delete this.entries[entry.id];
|
||||
this.$forceUpdate();
|
||||
},
|
||||
|
||||
// those must be methods, not computed, because when modified, they don't get updated in the view for some reason
|
||||
visibleEntries() {
|
||||
return Object.values(this.entries).filter(n => n.matches(this.filter));
|
||||
},
|
||||
entriesCountApproved() {
|
||||
return Object.values(this.entries).filter(n => n.approved).length;
|
||||
},
|
||||
entriesCountPending() {
|
||||
return Object.values(this.entries).filter(n => !n.approved).length;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
entries() {
|
||||
if (this.entriesRaw === undefined) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return buildDict(function* (that) {
|
||||
const sorted = that.entriesRaw.sort((a, b) => {
|
||||
if (a.approved && !b.approved) {
|
||||
return 1;
|
||||
}
|
||||
if (!a.approved && b.approved) {
|
||||
return -1;
|
||||
}
|
||||
return a.insteadOf.toLowerCase().localeCompare(b.insteadOf.toLowerCase());
|
||||
});
|
||||
for (let w of sorted) {
|
||||
yield [w.id, new InclusiveEntry(w)];
|
||||
}
|
||||
}, this);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
filter() {
|
||||
if (process.client) {
|
||||
if (this.$refs.dictionarytable) {
|
||||
this.$refs.dictionarytable.reset();
|
||||
this.$refs.dictionarytable.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "assets/variables";
|
||||
|
||||
tr {
|
||||
.hover-show {
|
||||
opacity: 0;
|
||||
}
|
||||
&:hover .hover-show {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
109
components/InclusiveSubmitForm.vue
Normal file
109
components/InclusiveSubmitForm.vue
Normal file
@ -0,0 +1,109 @@
|
||||
<template>
|
||||
<section>
|
||||
<div v-if="afterSubmit" class="alert alert-success text-center">
|
||||
<p>
|
||||
<T>nouns.submit.thanks</T>
|
||||
</p>
|
||||
<p>
|
||||
<button class="btn btn-success" @click="afterSubmit = false">
|
||||
<Icon v="plus"/>
|
||||
<T>nouns.submit.another</T>
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
<form v-else @submit.prevent="submit">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-borderless table-sm table-fixed-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-nowrap">
|
||||
<Icon v="comment-times"/>
|
||||
<T>nouns.inclusive.insteadOf</T>
|
||||
</th>
|
||||
<th class="text-nowrap">
|
||||
<Icon v="comment-check"/>
|
||||
<T>nouns.inclusive.say</T>
|
||||
</th>
|
||||
<th class="text-nowrap">
|
||||
<Icon v="comment-dots"/>
|
||||
<T>nouns.inclusive.because</T>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
<NounForm v-model="form.insteadOf"/>
|
||||
</td>
|
||||
<td>
|
||||
<NounForm v-model="form.say"/>
|
||||
</td>
|
||||
<td>
|
||||
<textarea v-model="form.because" class="form-control form-control-sm" required rows="3"></textarea>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="alert alert-info" v-if="form.base">
|
||||
<Icon v="info-circle"/>
|
||||
<T>nouns.editing</T>
|
||||
<button class="btn btn-sm float-right" @click="form.base = null">
|
||||
<Icon v="times"/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-block" :disabled="submitting">
|
||||
<template v-if="submitting">
|
||||
<Icon v="circle-notch fa-spin"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<Icon v="plus"/>
|
||||
<T>nouns.submit.actionLong</T>
|
||||
</template>
|
||||
</button>
|
||||
<p class="small text-muted mt-1"><T>nouns.submit.moderation</T></p>
|
||||
</form>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
insteadOf: [''],
|
||||
say: [''],
|
||||
because: '',
|
||||
base: null,
|
||||
},
|
||||
submitting: false,
|
||||
afterSubmit: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async submit(event) {
|
||||
this.submitting = true;
|
||||
await this.$axios.$post(`/inclusive/submit`, this.form);
|
||||
|
||||
this.submitting = false;
|
||||
this.afterSubmit = true;
|
||||
this.form = {
|
||||
insteadOf: [''],
|
||||
say: [''],
|
||||
because: '',
|
||||
base: null,
|
||||
};
|
||||
},
|
||||
edit(word) {
|
||||
this.form = {
|
||||
insteadOf: word.insteadOf,
|
||||
say: word.say,
|
||||
because: word.because,
|
||||
base: word.id,
|
||||
}
|
||||
this.$el.scrollIntoView();
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
@ -32,7 +32,6 @@
|
||||
<span class="d-none d-md-inline"><T>nouns.neuter</T></span>
|
||||
<span class="d-md-none"><T>nouns.neuterShort</T></span>
|
||||
</th>
|
||||
<th v-if="$admin()"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
@ -3,10 +3,19 @@
|
||||
<Separator icon="atom-alt"/>
|
||||
|
||||
<h3 :id="$t('nouns.neuterNouns.id')">
|
||||
<Icon v="deer"/>
|
||||
<T>nouns.neuterNouns.header</T>
|
||||
</h3>
|
||||
|
||||
<T>nouns.neuterNouns.info</T>
|
||||
<div class="d-flex flex-column flex-md-row">
|
||||
<div>
|
||||
<T>nouns.neuterNouns.info</T>
|
||||
</div>
|
||||
<figure>
|
||||
<img src="/img/łoś.jpg" :alt="$t('nouns.neuterNouns.flag.alt')"/>
|
||||
<figcaption><T>nouns.neuterNouns.flag.caption</T></figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
|
||||
@ -66,10 +75,19 @@
|
||||
<Separator icon="atom-alt"/>
|
||||
|
||||
<h3 :id="$t('nouns.dukajNouns.id')">
|
||||
<Icon v="ghost"/>
|
||||
<T>nouns.dukajNouns.header</T>
|
||||
</h3>
|
||||
|
||||
<T>nouns.dukajNouns.info</T>
|
||||
<div class="d-flex flex-column flex-md-row">
|
||||
<div>
|
||||
<T>nouns.dukajNouns.info</T>
|
||||
</div>
|
||||
<figure>
|
||||
<img src="/img/dukaizmy.png" :alt="$t('nouns.dukajNouns.flag.alt')"/>
|
||||
<figcaption><T>nouns.dukajNouns.flag.caption</T></figcaption>
|
||||
</figure>
|
||||
</div>
|
||||
|
||||
<details class="border mb-3">
|
||||
<summary class="bg-light p-3">
|
||||
@ -150,6 +168,7 @@
|
||||
<Separator icon="atom-alt"/>
|
||||
|
||||
<h3 :id="$t('nouns.personNouns.id')">
|
||||
<Icon v="user-friends"/>
|
||||
<T>nouns.personNouns.header</T>
|
||||
<small><NormativeBadge/></small>
|
||||
</h3>
|
||||
@ -209,6 +228,27 @@
|
||||
</table>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<Separator icon="atom-alt"/>
|
||||
|
||||
<h3 :id="$t('nouns.inclusive.id')">
|
||||
<Icon v="book-heart"/>
|
||||
<T>nouns.inclusive.headerLong</T>
|
||||
</h3>
|
||||
|
||||
<T>nouns.inclusive.info</T>
|
||||
|
||||
<details class="border mb-3">
|
||||
<summary class="bg-light p-3" @click="$refs.inclusivedictionary.loadEntries()">
|
||||
<h4 class="h5 d-inline">
|
||||
<Icon v="book-heart"/>
|
||||
<T>nouns.inclusive.headerLong</T>
|
||||
</h4>
|
||||
</summary>
|
||||
<div class="border-top">
|
||||
<InclusiveDictionary ref="inclusivedictionary"/>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -401,3 +441,19 @@
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "assets/variables";
|
||||
|
||||
figure {
|
||||
width: 100%;
|
||||
max-width: 24rem;
|
||||
padding: $spacer;
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
figcaption {
|
||||
font-size: $small-font-size;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
@ -1,16 +1,31 @@
|
||||
<template>
|
||||
<section class="btn-group btn-block mb-2">
|
||||
<a :href="'#' + $t('nouns.neuterNouns.id')" class="btn btn-outline-primary">
|
||||
<Icon v="atom-alt"/>
|
||||
<T>nouns.neuterNouns.header</T>
|
||||
</a>
|
||||
<a :href="'#' + $t('nouns.dukajNouns.id')" class="btn btn-outline-primary">
|
||||
<Icon v="atom-alt"/>
|
||||
<T>nouns.dukajNouns.header</T>
|
||||
</a>
|
||||
<a :href="'#' + $t('nouns.personNouns.id')" class="btn btn-outline-primary">
|
||||
<Icon v="atom-alt"/>
|
||||
<T>nouns.personNouns.header</T>
|
||||
</a>
|
||||
<section>
|
||||
<div class="d-none d-md-inline-flex btn-group btn-block mb-2">
|
||||
<a v-for="(icon, name) in links" :href="'#' + $t(`nouns.${name}.id`)" class="btn btn-outline-primary">
|
||||
<Icon :v="icon"/>
|
||||
<T>nouns.{{name}}.header</T>
|
||||
</a>
|
||||
</div>
|
||||
<div class="d-block d-md-none btn-group-vertical btn-block mb-2">
|
||||
<a v-for="(icon, name) in links" :href="'#' + $t(`nouns.${name}.id`)" class="btn btn-outline-primary">
|
||||
<Icon :v="icon"/>
|
||||
<T>nouns.{{name}}.header</T>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
links: {
|
||||
neuterNouns: 'deer',
|
||||
dukajNouns: 'ghost',
|
||||
personNouns: 'user-friends',
|
||||
inclusive: 'book-heart',
|
||||
}
|
||||
};
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -201,6 +201,9 @@ nouns:
|
||||
header: 'Dukatywy'
|
||||
label: 'dukatyw'
|
||||
id: 'dukatywy'
|
||||
flag:
|
||||
alt: 'Flaga osób niebinarnych z naniesionym duszkiem krzyczącym „-łu”.'
|
||||
caption: 'Ze względu na końcówki „-łum”, „-łuś” i „-łu”, flaga dukazimów i dukatywów przedstawia duszka krzyczącego „łu!”.'
|
||||
info:
|
||||
- >
|
||||
Analogicznie do tzw. {/onu=dukaizmów / rodzaju postpłciowego} („zrobiłum”, „poszłuś”)
|
||||
@ -221,6 +224,9 @@ nouns:
|
||||
header: 'Neutratywy'
|
||||
label: 'neutratyw'
|
||||
id: 'neutratywy'
|
||||
flag:
|
||||
alt: 'Flaga osób niebinarnych z naniesionym łosiem trzymającym łom.'
|
||||
caption: 'Ze względu na końcówki „-łom”, „-łoś” i „-ło”, flaga rodzaju nijakiego i neutratywów przedstawia łosia z łomem.'
|
||||
info:
|
||||
- >
|
||||
Są to słowa ukute na nijakie wersje słów nacechowanych płciowo, analogicznie do feminatywów.
|
||||
@ -276,6 +282,23 @@ nouns:
|
||||
plural: 'liczba mnoga'
|
||||
pluralShort: 'l. mn.'
|
||||
|
||||
inclusive:
|
||||
header: 'Inkluzywność'
|
||||
headerLong: 'Słownik inkluzywnego języka'
|
||||
id: 'inkluzywnosc'
|
||||
insteadOf: 'Zamiast'
|
||||
say: 'Lepiej mów'
|
||||
because: 'Ponieważ'
|
||||
info:
|
||||
- >
|
||||
Język jest nośnikiem myśli, nośnikiem kultury, podstawą komunikacji. Wpływa na to, co robimy i jak myślimy.
|
||||
Jeśli chcemy tworzyć społeczeństwo otwarte na różnorodność i akceptujące odmienność,
|
||||
to nasz język też musi być włączający.
|
||||
- >
|
||||
Inkluzywny język to nie tylko rzeczowniki i nie tylko kwestie płciowości.
|
||||
Poniżej przedstawiamy słownik, w którym zbieramy sugestie,
|
||||
jakich konstrukcji lepiej unikać i dlatego, oraz czym je zastępować.
|
||||
|
||||
names:
|
||||
header: 'Imiona'
|
||||
headerLong: 'Neutralne imiona'
|
||||
|
15
migrations/006-inclusive.sql
Normal file
15
migrations/006-inclusive.sql
Normal file
@ -0,0 +1,15 @@
|
||||
-- Up
|
||||
|
||||
CREATE TABLE inclusive (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
insteadOf TEXT NOT NULL,
|
||||
say TEXT NOT NULL,
|
||||
because TEXT NOT NULL,
|
||||
locale TEXT NOT NULL,
|
||||
approved INTEGER NOT NULL,
|
||||
base_id TEXT
|
||||
);
|
||||
|
||||
-- Down
|
||||
|
||||
DROP TABLE inclusive;
|
@ -145,7 +145,7 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "assets/style";
|
||||
@import "assets/variables";
|
||||
|
||||
@include media-breakpoint-up('md') {
|
||||
.w-md-50 {
|
||||
|
@ -174,7 +174,7 @@
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "assets/style";
|
||||
@import "assets/variables";
|
||||
|
||||
.avatar {
|
||||
width: 100%;
|
||||
|
@ -11,7 +11,7 @@
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "assets/style";
|
||||
@import "assets/variables";
|
||||
|
||||
.list-group-item-hoverable {
|
||||
&:hover {
|
||||
|
@ -43,6 +43,7 @@ app.use(require('./routes/admin').default);
|
||||
app.use(require('./routes/pronouns').default);
|
||||
app.use(require('./routes/sources').default);
|
||||
app.use(require('./routes/nouns').default);
|
||||
app.use(require('./routes/inclusive').default);
|
||||
app.use(require('./routes/pronounce').default);
|
||||
|
||||
export default {
|
||||
|
@ -5,15 +5,20 @@ const mailer = require('../src/mailer');
|
||||
async function notify() {
|
||||
const db = await dbConnection();
|
||||
|
||||
const awaitingModeration = (await db.all(`SELECT locale, count(*) as c FROM nouns WHERE approved = 0 GROUP BY locale`));
|
||||
const awaitingModeration = [
|
||||
...(await db.all(`SELECT 'nouns' as type, locale, count(*) as c FROM nouns WHERE approved = 0 GROUP BY locale`)),
|
||||
...(await db.all(`SELECT 'inclusive' as type, locale, count(*) as c FROM inclusive WHERE approved = 0 GROUP BY locale`)),
|
||||
];
|
||||
if (!awaitingModeration.length) {
|
||||
console.log('No entries awaiting moderation');
|
||||
return;
|
||||
}
|
||||
|
||||
const awaitingModerationGrouped = {}
|
||||
let count = 0;
|
||||
for (let m of awaitingModeration) {
|
||||
awaitingModerationGrouped[m.locale] = m.c;
|
||||
awaitingModerationGrouped[m.type + '-' + m.locale] = m.c;
|
||||
count += m.c;
|
||||
}
|
||||
|
||||
console.log('Entries awaiting moderation: ', awaitingModerationGrouped);
|
||||
@ -24,8 +29,8 @@ async function notify() {
|
||||
console.log('Sending email to ' + email)
|
||||
mailer(
|
||||
email,
|
||||
'[Pronouns] Dictionary entries awaiting moderation: ' + JSON.stringify(awaitingModerationGrouped),
|
||||
JSON.stringify(awaitingModerationGrouped)
|
||||
'[Pronouns] Dictionary entries awaiting moderation: ' + count,
|
||||
'Dictionary entries awaiting moderation: \n' + JSON.stringify(awaitingModerationGrouped, null, 4),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
102
server/routes/inclusive.js
Normal file
102
server/routes/inclusive.js
Normal file
@ -0,0 +1,102 @@
|
||||
import { Router } from 'express';
|
||||
import SQL from 'sql-template-strings';
|
||||
import {ulid} from "ulid";
|
||||
import {isTroll} from "../../src/helpers";
|
||||
|
||||
const approve = async (db, id) => {
|
||||
const { base_id } = await db.get(SQL`SELECT base_id FROM inclusive WHERE id=${id}`);
|
||||
if (base_id) {
|
||||
await db.get(SQL`
|
||||
DELETE FROM inclusive
|
||||
WHERE id = ${base_id}
|
||||
`);
|
||||
}
|
||||
await db.get(SQL`
|
||||
UPDATE inclusive
|
||||
SET approved = 1, base_id = NULL
|
||||
WHERE id = ${id}
|
||||
`);
|
||||
}
|
||||
|
||||
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}
|
||||
ORDER BY approved, 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 (insteadOf like ${term} OR say like ${term})
|
||||
ORDER BY approved, insteadOf
|
||||
`));
|
||||
});
|
||||
|
||||
router.post('/inclusive/submit', async (req, res) => {
|
||||
if (!(req.user && req.user.admin) && isTroll(JSON.stringify(req.body))) {
|
||||
return res.json('ok');
|
||||
}
|
||||
|
||||
const id = ulid();
|
||||
await req.db.get(SQL`
|
||||
INSERT INTO inclusive (id, insteadOf, say, because, approved, base_id, locale)
|
||||
VALUES (
|
||||
${id},
|
||||
${req.body.insteadOf.join('|')}, ${req.body.say.join('|')}, ${req.body.because},
|
||||
0, ${req.body.base}, ${req.config.locale}
|
||||
)
|
||||
`);
|
||||
|
||||
if (req.admin) {
|
||||
await approve(req.db, id);
|
||||
}
|
||||
|
||||
return res.json('ok');
|
||||
});
|
||||
|
||||
router.post('/inclusive/hide/:id', async (req, res) => {
|
||||
if (!req.admin) {
|
||||
res.status(401).json({error: 'Unauthorised'});
|
||||
}
|
||||
|
||||
await req.db.get(SQL`
|
||||
UPDATE inclusive
|
||||
SET approved = 0
|
||||
WHERE id = ${req.params.id}
|
||||
`);
|
||||
|
||||
return res.json('ok');
|
||||
});
|
||||
|
||||
router.post('/inclusive/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('/inclusive/remove/:id', async (req, res) => {
|
||||
if (!req.admin) {
|
||||
res.status(401).json({error: 'Unauthorised'});
|
||||
}
|
||||
|
||||
await req.db.get(SQL`
|
||||
DELETE FROM inclusive
|
||||
WHERE id = ${req.params.id}
|
||||
`);
|
||||
|
||||
return res.json('ok');
|
||||
});
|
||||
|
||||
export default router;
|
@ -3,13 +3,10 @@ import SQL from 'sql-template-strings';
|
||||
import {ulid} from "ulid";
|
||||
import {createCanvas, loadImage, registerFont} from "canvas";
|
||||
import {loadSuml} from "../loader";
|
||||
import {isTroll} from "../../src/helpers";
|
||||
|
||||
const translations = loadSuml('translations');
|
||||
|
||||
const isTroll = (body) => {
|
||||
return ['cipeusz', 'feminazi', 'bruksela', 'zboczeń'].some(t => body.indexOf(t) > -1);
|
||||
}
|
||||
|
||||
const approve = async (db, id) => {
|
||||
const { base_id } = await db.get(SQL`SELECT base_id FROM nouns WHERE id=${id}`);
|
||||
if (base_id) {
|
||||
|
@ -502,6 +502,33 @@ export class NounDeclension {
|
||||
}
|
||||
|
||||
|
||||
export class InclusiveEntry {
|
||||
constructor({id, insteadOf, say, because, approved = true, base_id = null}) {
|
||||
this.id = id;
|
||||
this.insteadOf = insteadOf.split('|');
|
||||
this.say = say.split('|');
|
||||
this.because = because;
|
||||
this.approved = !!approved;
|
||||
this.base = base_id;
|
||||
}
|
||||
|
||||
matches(filter) {
|
||||
if (!filter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (let field of ['insteadOf', 'say']) {
|
||||
for (let value of this[field]) {
|
||||
if (value.toLowerCase().indexOf(filter.toLowerCase()) > -1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class Name {
|
||||
constructor(name, origin, meaning, usage, legally, pros, cons, notablePeople, count, links) {
|
||||
this.name = name;
|
||||
|
@ -132,6 +132,10 @@ export const isEmoji = char => {
|
||||
return !!char.match(/^(?:[\u2700-\u27bf]|(?:\ud83c[\udde6-\uddff]){2}|[\ud800-\udbff][\udc00-\udfff]|[\u0023-\u0039]\ufe0f?\u20e3|\u3299|\u3297|\u303d|\u3030|\u24c2|\ud83c[\udd70-\udd71]|\ud83c[\udd7e-\udd7f]|\ud83c\udd8e|\ud83c[\udd91-\udd9a]|\ud83c[\udde6-\uddff]|\ud83c[\ude01-\ude02]|\ud83c\ude1a|\ud83c\ude2f|\ud83c[\ude32-\ude3a]|\ud83c[\ude50-\ude51]|\u203c|\u2049|[\u25aa-\u25ab]|\u25b6|\u25c0|[\u25fb-\u25fe]|\u00a9|\u00ae|\u2122|\u2139|\ud83c\udc04|[\u2600-\u26FF]|\u2b05|\u2b06|\u2b07|\u2b1b|\u2b1c|\u2b50|\u2b55|\u231a|\u231b|\u2328|\u23cf|[\u23e9-\u23f3]|[\u23f8-\u23fa]|\ud83c\udccf|\u2934|\u2935|[\u2190-\u21ff])$/)
|
||||
}
|
||||
|
||||
export const isTroll = (body) => {
|
||||
return ['cipeusz', 'feminazi', 'bruksela', 'zboczeń'].some(t => body.indexOf(t) > -1);
|
||||
}
|
||||
|
||||
export const buildLocaleList = () => {
|
||||
return buildDict(function* () {
|
||||
for (let locale of process.env.LOCALES.split('|')) {
|
||||
|
BIN
static/img/dukaizmy.png
Normal file
BIN
static/img/dukaizmy.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 36 KiB |
Loading…
x
Reference in New Issue
Block a user