[pl][census] moderation tool

This commit is contained in:
Andrea 2022-02-17 15:19:33 +01:00
parent 7618b57074
commit 0fc7e762a2
6 changed files with 166 additions and 13 deletions

View File

@ -1335,9 +1335,17 @@ census:
Wyniki i wnioski ogłosimy wkrótce {/kontakt=w mediach społecznościowych}. Wyniki i wnioski ogłosimy wkrótce {/kontakt=w mediach społecznościowych}.
prev: 'Poprzednie pytanie' prev: 'Poprzednie pytanie'
next: 'Następne pytanie' next: 'Następne pytanie'
replies: 'odpowiedzi' replies: 'odpowiedzi, w tym:'
repliesNonbinary: 'od osób niebinarnych/poszukujących'
repliesUsable: 'użytecznych'
repliesAwaiting: 'oczekujących na moderację'
writein: 'Jeśli Twojej odpowiedzi nie ma powyżej, możesz ją wpisać tutaj.' writein: 'Jeśli Twojej odpowiedzi nie ma powyżej, możesz ją wpisać tutaj.'
leave: 'Czy na pewno chcesz wyjść? Twoje dotychczasowe odpowiedzi nie zostaną zapisane!' leave: 'Czy na pewno chcesz wyjść? Twoje dotychczasowe odpowiedzi nie zostaną zapisane!'
moderation:
troll: 'Troll'
skip: 'Nie wiem, pomiń'
ok: 'Nie troll'
done: 'Wszystkie odpowiedzi są już przejrzane!'
share: 'Udostępnij' share: 'Udostępnij'

View File

@ -0,0 +1,6 @@
-- Up
ALTER TABLE census ADD COLUMN troll INTEGER NULL;
-- Down

View File

@ -264,6 +264,7 @@ export default {
if (config.census.enabled) { if (config.census.enabled) {
routes.push({ path: '/' + encodeURIComponent(config.census.route), component: resolve(__dirname, 'routes/census.vue') }); routes.push({ path: '/' + encodeURIComponent(config.census.route), component: resolve(__dirname, 'routes/census.vue') });
routes.push({ path: '/' + encodeURIComponent(config.census.route) + '/admin', component: resolve(__dirname, 'routes/censusModeration.vue') });
} }
if (config.user.enabled) { if (config.user.enabled) {

View File

@ -10,12 +10,16 @@
<template v-if="q === null"> <template v-if="q === null">
<section v-if="$isGranted('census')"> <section v-if="$isGranted('census')">
<div class="alert alert-info"> <div class="alert alert-info">
{{countResponses}}
<T>census.replies</T>
<a href="/api/census/export" class="btn btn-outline-secondary btn-sm float-end"> <a href="/api/census/export" class="btn btn-outline-secondary btn-sm float-end">
<Icon v="download"/> <Icon v="download"/>
</a> </a>
<p>{{countResponses.all}} <T>census.replies</T></p>
<ul>
<li>{{countResponses.nonbinary}} <T>census.repliesNonbinary</T></li>
<li>{{countResponses.usable}} <T>census.repliesUsable</T></li>
<li><nuxt-link :to="`/${config.census.route}/admin`">{{countResponses.awaiting}} <T>census.repliesAwaiting</T></nuxt-link></li>
</ul>
</div> </div>
</section> </section>

View File

@ -0,0 +1,73 @@
<template>
<div v-if="$isGranted('census')">
<CommunityNav/>
<h2>
<Icon v="user-chart"/>
<T>census.headerLong</T>
</h2>
<Spinner v-if="queue === undefined" size="5rem"/>
<div v-else-if="queue.count === 0" class="alert alert-success text-center">
<Icon v="check-circle" size="5"/>
<p><T>census.moderation.done</T></p>
</div>
<div v-else>
<div class="alert alert-info">
{{queue.count}} <T>census.repliesAwaiting</T>
</div>
<ol>
<li v-for="(question, i) in config.census.questions">
<p>{{question.question}}</p>
<p><strong>{{queue.next.answers[i.toString()]}}</strong></p>
<p v-if="queue.next.writins[i.toString()]"><strong><em>{{queue.next.writins[i.toString()]}}</em></strong></p>
</li>
</ol>
<div class="d-flex my-5">
<button class="btn btn-danger flex-grow-1 m-2" @click="decide(true)"><T>census.moderation.troll</T></button>
<button class="btn btn-outline-primary flex-grow-1 m-2" @click="skip()"><T>census.moderation.skip</T></button>
<button class="btn btn-success flex-grow-1 m-2" @click="decide(false)"><T>census.moderation.ok</T></button>
</div>
</div>
</div>
<NotFound v-else/>
</template>
<script>
export default {
data() {
return {
queue: undefined,
}
},
async mounted() {
await this.fetch();
},
methods: {
async fetch() {
this.queue = await this.$axios.$get('/census/moderation/queue');
if (this.queue.next) {
this.queue.next.answers = JSON.parse(this.queue.next.answers);
this.queue.next.writins = JSON.parse(this.queue.next.writins);
}
},
async decide(decision) {
const id = this.queue.next.id;
this.queue = undefined;
await this.$post('/census/moderation/decide', {
id,
decision: decision ? 1 : 0,
})
await this.fetch();
window.scrollTo(0, 0);
},
async skip() {
this.queue = undefined;
await this.fetch();
window.scrollTo(0, 0);
}
}
};
</script>

View File

@ -69,11 +69,39 @@ router.post('/census/submit', handleErrorAsync(async (req, res) => {
})); }));
router.get('/census/count', handleErrorAsync(async (req, res) => { router.get('/census/count', handleErrorAsync(async (req, res) => {
return res.json((await req.db.get(SQL` if (!req.isGranted('census')) {
return res.status(401).json({error: 'Unauthorised'});
}
// duplication reason: https://github.com/felixfbecker/node-sql-template-strings/issues/71
return res.json({
all: (await req.db.get(SQL`
SELECT COUNT(*) as c FROM census SELECT COUNT(*) as c FROM census
WHERE locale = ${global.config.locale} WHERE locale = ${global.config.locale}
AND edition = ${global.config.census.edition} AND edition = ${global.config.census.edition}
`)).c); `)).c,
nonbinary: (await req.db.get(SQL`
SELECT COUNT(*) as c FROM census
WHERE locale = ${global.config.locale}
AND edition = ${global.config.census.edition}
AND (answers LIKE '{"0":"osobą niebinarną"%' OR answers LIKE '{"0":"nie wiem"%') -- TODO polish-specific
`)).c,
usable: (await req.db.get(SQL`
SELECT COUNT(*) as c FROM census
WHERE locale = ${global.config.locale}
AND edition = ${global.config.census.edition}
AND (answers LIKE '{"0":"osobą niebinarną"%' OR answers LIKE '{"0":"nie wiem"%') -- TODO polish-specific
AND troll = 0
`)).c,
awaiting: (await req.db.get(SQL`
SELECT COUNT(*) as c FROM census
WHERE locale = ${global.config.locale}
AND edition = ${global.config.census.edition}
AND (answers LIKE '{"0":"osobą niebinarną"%' OR answers LIKE '{"0":"nie wiem"%') -- TODO polish-specific
AND troll IS NULL
`)).c,
});
})); }));
router.get('/census/export', handleErrorAsync(async (req, res) => { router.get('/census/export', handleErrorAsync(async (req, res) => {
@ -82,11 +110,12 @@ router.get('/census/export', handleErrorAsync(async (req, res) => {
} }
const report = []; const report = [];
for (let {answers, writins} of await req.db.all(SQL` for (let {answers, writins, troll} of await req.db.all(SQL`
SELECT answers, writins FROM census SELECT answers, writins FROM census
WHERE locale = ${global.config.locale} WHERE locale = ${global.config.locale}
AND edition = ${global.config.census.edition} AND edition = ${global.config.census.edition}
AND suspicious = 0 AND suspicious = 0
AND troll = 0
`)) { `)) {
answers = JSON.parse(answers); answers = JSON.parse(answers);
writins = JSON.parse(writins); writins = JSON.parse(writins);
@ -113,4 +142,36 @@ router.get('/census/export', handleErrorAsync(async (req, res) => {
return res.set('content-type', 'text/csv').send(Papa.unparse(report)); return res.set('content-type', 'text/csv').send(Papa.unparse(report));
})); }));
router.get('/census/moderation/queue', handleErrorAsync(async (req, res) => {
if (!req.isGranted('census')) {
return res.status(401).json({error: 'Unauthorised'});
}
const queue = await req.db.all(SQL`
SELECT id, answers, writins FROM census
WHERE locale = ${global.config.locale}
AND edition = ${global.config.census.edition}
AND (answers LIKE '{"0":"osobą niebinarną"%' OR answers LIKE '{"0":"nie wiem"%') -- TODO polish-specific
AND troll IS NULL
ORDER BY RANDOM()
`);
return res.json({
count: queue.length,
next: queue.length ? queue[0] : null,
});
}));
router.post('/census/moderation/decide', handleErrorAsync(async (req, res) => {
if (!req.isGranted('census')) {
return res.status(401).json({error: 'Unauthorised'});
}
const queue = await req.db.get(SQL`
UPDATE census SET troll = ${parseInt(req.body.decision)} WHERE id = ${req.body.id}
`);
return res.json('ok');
}));
export default router; export default router;