#134 [pl][census] polski cenzus płci
This commit is contained in:
parent
81e2e29f15
commit
cf7b7b7b33
|
@ -24,7 +24,7 @@
|
||||||
@import "~bootstrap/scss/badge";
|
@import "~bootstrap/scss/badge";
|
||||||
//@import "~bootstrap/scss/jumbotron";
|
//@import "~bootstrap/scss/jumbotron";
|
||||||
@import "~bootstrap/scss/alert";
|
@import "~bootstrap/scss/alert";
|
||||||
//@import "~bootstrap/scss/progress";
|
@import "~bootstrap/scss/progress";
|
||||||
//@import "~bootstrap/scss/media";
|
//@import "~bootstrap/scss/media";
|
||||||
@import "~bootstrap/scss/list-group";
|
@import "~bootstrap/scss/list-group";
|
||||||
//@import "~bootstrap/scss/close";
|
//@import "~bootstrap/scss/close";
|
||||||
|
|
|
@ -128,6 +128,15 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.config.census.enabled) {
|
||||||
|
links.push({
|
||||||
|
link: '/' + this.config.census.route,
|
||||||
|
icon: 'user-chart',
|
||||||
|
text: this.$t('census.header'),
|
||||||
|
textLong: this.$t('census.headerLong'),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (this.config.contact.enabled) {
|
if (this.config.contact.enabled) {
|
||||||
links.push({
|
links.push({
|
||||||
link: '/' + this.config.contact.route,
|
link: '/' + this.config.contact.route,
|
||||||
|
|
|
@ -426,6 +426,56 @@ profile:
|
||||||
flags:
|
flags:
|
||||||
defaultPronoun: 'on_'
|
defaultPronoun: 'on_'
|
||||||
|
|
||||||
|
census:
|
||||||
|
enabled: false
|
||||||
|
route: 'cenzus'
|
||||||
|
open: true
|
||||||
|
edition: '2021'
|
||||||
|
start: '2021-03-01'
|
||||||
|
end: '2021-03-31'
|
||||||
|
questions:
|
||||||
|
-
|
||||||
|
type: 'radio'
|
||||||
|
question: 'Gdzie obecnie mieszkasz?'
|
||||||
|
options:
|
||||||
|
- 'w Polsce'
|
||||||
|
- 'za granicą'
|
||||||
|
-
|
||||||
|
type: 'number'
|
||||||
|
min: 12
|
||||||
|
max: 120
|
||||||
|
question: 'Ile masz lat?'
|
||||||
|
-
|
||||||
|
type: 'checkbox'
|
||||||
|
question: 'Jakimi słowami opisujesz swoją płeć?'
|
||||||
|
randomise: true
|
||||||
|
writein: true
|
||||||
|
options:
|
||||||
|
- 'niebinarn_'
|
||||||
|
- 'agender'
|
||||||
|
- 'agenderow_'
|
||||||
|
- 'bigender'
|
||||||
|
- 'bigenderow_'
|
||||||
|
- 'enby'
|
||||||
|
- 'niebinie'
|
||||||
|
- 'queer'
|
||||||
|
-
|
||||||
|
type: 'checkbox'
|
||||||
|
question: 'Jakich zaimków i innych form nacechowanych płciowo używasz?'
|
||||||
|
randomise: true
|
||||||
|
writein: true
|
||||||
|
options:
|
||||||
|
- 'on/jego'
|
||||||
|
- 'ona/jej'
|
||||||
|
- 'ono/jego'
|
||||||
|
- 'ono/jej'
|
||||||
|
- 'onu/jenu'
|
||||||
|
- 'maskulatywy'
|
||||||
|
- 'feminatywy'
|
||||||
|
- 'neutratywy'
|
||||||
|
- 'dukatywy'
|
||||||
|
- 'osobatywy'
|
||||||
|
|
||||||
redirects:
|
redirects:
|
||||||
- { from: '^/neutratywy', to: '/s%C5%82ownik' }
|
- { from: '^/neutratywy', to: '/s%C5%82ownik' }
|
||||||
- { from: '^/rzeczowniki', to: '/s%C5%82ownik' }
|
- { from: '^/rzeczowniki', to: '/s%C5%82ownik' }
|
||||||
|
|
|
@ -886,6 +886,36 @@ profile:
|
||||||
meh: 'Spoko'
|
meh: 'Spoko'
|
||||||
no: 'Nie'
|
no: 'Nie'
|
||||||
|
|
||||||
|
census:
|
||||||
|
header: 'Cenzus'
|
||||||
|
headerLong: 'Cenzus płci'
|
||||||
|
description:
|
||||||
|
- >
|
||||||
|
Ciekawi Cię, jakiego języka używają polskie osoby niebinarne?
|
||||||
|
Które zaimki, rodzaje gramatyczne i inne formy są najpopularniejsze?
|
||||||
|
Jakimi etykietkami się opisujemy?
|
||||||
|
Jak te trendy zmieniają się w czasie?
|
||||||
|
- >
|
||||||
|
To super, bo nas też! Dlatego chcemy co roku przeprowadzać cenzus, w którym to zbadamy.
|
||||||
|
Serdecznie zapraszamy do udziału <strong>wszystkie osoby niebinarne posługujące się językiem polskim</strong>.
|
||||||
|
- >
|
||||||
|
Ankieta składa się z <strong>%questions% pytań</strong> i jest otwarta <strong>od %start% do %end%</strong>.
|
||||||
|
W pytaniach wielokrotnego wyboru można zaznaczyć wiele odpowiedzi, jak również dopisać własne.
|
||||||
|
By uniknąć tendencyjności, kolejność propozycji jest losowa.
|
||||||
|
agree: >
|
||||||
|
Wyrażam zgodę na przetwarzanie moich odpowiedzi
|
||||||
|
oraz na użycie zanonimizowanej wersji mojego adresu IP oraz fingerprintu przeglądarki
|
||||||
|
do przeciwdziałania wandalizmom i spamowi.
|
||||||
|
Dane te nie są w stanie zidentyfikować żadnej osoby,
|
||||||
|
zostaną użyte wyłącznie w celu zapewnienia unikalności odpowiedzi
|
||||||
|
i usunięte po zamknięciu cenzusu.
|
||||||
|
start: 'Rozpocznij ankietę'
|
||||||
|
finished: >
|
||||||
|
Dziękujemy za wzięcie udziału w cenzusie!
|
||||||
|
Wyniki i wnioski ogłosimy wkrótce {/kontakt=w mediach społecznościowych}.
|
||||||
|
prev: 'Poprzednie pytanie'
|
||||||
|
next: 'Następne pytanie'
|
||||||
|
|
||||||
share: 'Udostępnij'
|
share: 'Udostępnij'
|
||||||
|
|
||||||
crud:
|
crud:
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
-- Up
|
||||||
|
|
||||||
|
CREATE TABLE census (
|
||||||
|
id TEXT NOT NULL PRIMARY KEY,
|
||||||
|
locale TEXT NOT NULL,
|
||||||
|
edition TEXT NOT NULL,
|
||||||
|
userId TEXT NULL,
|
||||||
|
fingerprint TEXT NULL,
|
||||||
|
answers TEXT NOT NULL,
|
||||||
|
writins TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Down
|
||||||
|
|
||||||
|
DROP TABLE census;
|
|
@ -175,6 +175,10 @@ export default {
|
||||||
routes.push({ path: '/' + config.contact.route, component: resolve(__dirname, 'routes/contact.vue') });
|
routes.push({ path: '/' + config.contact.route, component: resolve(__dirname, 'routes/contact.vue') });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.census.enabled) {
|
||||||
|
routes.push({ path: '/' + config.census.route, component: resolve(__dirname, 'routes/census.vue') });
|
||||||
|
}
|
||||||
|
|
||||||
if (config.user.enabled) {
|
if (config.user.enabled) {
|
||||||
routes.push({path: '/' + config.user.route, component: resolve(__dirname, 'routes/user.vue')});
|
routes.push({path: '/' + config.user.route, component: resolve(__dirname, 'routes/user.vue')});
|
||||||
routes.push({path: '/' + config.user.termsRoute, component: resolve(__dirname, 'routes/terms.vue')});
|
routes.push({path: '/' + config.user.termsRoute, component: resolve(__dirname, 'routes/terms.vue')});
|
||||||
|
|
|
@ -0,0 +1,191 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2>
|
||||||
|
<Icon v="user-chart"/>
|
||||||
|
<T>census.headerLong</T>
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<template v-if="q === null">
|
||||||
|
<section>
|
||||||
|
<T :params='{
|
||||||
|
questions: questions.length,
|
||||||
|
start: config.census.start,
|
||||||
|
end: config.census.end,
|
||||||
|
}'>census.description</T>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<Share :title="$t('census.headerLong')"/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section v-if="config.census.open">
|
||||||
|
<div v-if="finished" class="alert alert-success">
|
||||||
|
<Icon v="badge-check"/>
|
||||||
|
<T>census.finished</T>
|
||||||
|
</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-check">
|
||||||
|
<label class="form-check-label small">
|
||||||
|
<input type="checkbox" class="form-check-input" v-model="agreement">
|
||||||
|
<T>census.agree</T>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<button class="btn btn-primary btn-lg" :disabled="!agreement" @click="start">
|
||||||
|
<T>census.start</T>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="q < questions.length">
|
||||||
|
<div class="progress my-3">
|
||||||
|
<div class="progress-bar" role="progressbar" :style="`width: ${progress}%`" :aria-valuenow="q" aria-valuemin="0" :aria-valuemax="questions.length">
|
||||||
|
{{q}}/{{questions.length}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="h4">{{q+1}}. {{question.question}}</p>
|
||||||
|
<form>
|
||||||
|
<div v-if="question.type === 'radio'" class="form-group">
|
||||||
|
<div class="form-check" v-for="option in question.options">
|
||||||
|
<label class="form-check-label small">
|
||||||
|
<input type="radio" class="form-check-input" v-model="answers[q]" :name="'question' + q" :value="option" required/>
|
||||||
|
{{option}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="question.type === 'checkbox'" class="form-group">
|
||||||
|
<div class="form-check" v-for="option in question.options">
|
||||||
|
<label class="form-check-label small">
|
||||||
|
<input type="checkbox" class="form-check-input" v-model="answers[q]" :value="option"/>
|
||||||
|
{{option}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="question.type === 'etxt'" class="form-group">
|
||||||
|
<input type="text" class="form-control" v-model="answers[q]" required/>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="question.type === 'number'" class="form-group">
|
||||||
|
<input type="number" class="form-control" :min="question.min" :max="question.max" v-model="answers[q]" required/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="question.writein" class="form-group">
|
||||||
|
<input type="text" class="form-control form-control-sm" v-model="writins[q]"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="btn-group btn-block">
|
||||||
|
<button class="btn btn-outline-primary" :disabled="q === 0" @click="q--">
|
||||||
|
<Icon v="arrow-alt-left"/>
|
||||||
|
<T>census.prev</T>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary" :disabled="!stepValid" @click="q++">
|
||||||
|
<T>census.next</T>
|
||||||
|
<Icon v="arrow-alt-right"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<Icon v="badge-check"/>
|
||||||
|
<T>census.finished</T>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {buildDict, head, shuffle} from "../src/helpers";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
const questions = this.config.census.questions.map(q => {
|
||||||
|
if (q.randomise) {
|
||||||
|
q.options = shuffle(q.options);
|
||||||
|
}
|
||||||
|
return q;
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
agreement: false,
|
||||||
|
q: null,
|
||||||
|
questions,
|
||||||
|
answers: buildDict(function* () {
|
||||||
|
let i = 0;
|
||||||
|
for (let question of questions) {
|
||||||
|
yield [i, question.type === 'checkbox' ? [] : null]
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
writins: buildDict(function* () {
|
||||||
|
let i = 0;
|
||||||
|
for (let question of questions) {
|
||||||
|
yield [i, '']
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async asyncData({ app, store }) {
|
||||||
|
const finished = await app.$axios.$get(`/census/finished`);
|
||||||
|
|
||||||
|
return {
|
||||||
|
finished,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
start() {
|
||||||
|
this.q = 0;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
progress() {
|
||||||
|
return Math.round(100 * (this.q || 0) / this.questions.length);
|
||||||
|
},
|
||||||
|
question() {
|
||||||
|
return this.questions[this.q];
|
||||||
|
},
|
||||||
|
stepValid() {
|
||||||
|
if (!this.question) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.writins[this.q] !== '') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.question.type === 'radio') {
|
||||||
|
return this.answers[this.q] !== undefined;
|
||||||
|
}
|
||||||
|
if (this.question.type === 'checkbox') {
|
||||||
|
return this.answers[this.q] !== undefined && this.answers[this.q].length > 0;
|
||||||
|
}
|
||||||
|
if (this.question.type === 'number') {
|
||||||
|
const v = parseInt(this.answers[this.q]);
|
||||||
|
return this.answers[this.q] !== '' && v >= this.question.min && v <= this.question.max;
|
||||||
|
}
|
||||||
|
if (this.question.type === 'text') {
|
||||||
|
return this.answers[this.q] !== '';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
async q() {
|
||||||
|
if (this.q === this.questions.length) {
|
||||||
|
await this.$axios.$post(`/census/submit`, {
|
||||||
|
answers: JSON.stringify(this.answers),
|
||||||
|
writins: JSON.stringify(this.writins),
|
||||||
|
});
|
||||||
|
this.finished = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
head() {
|
||||||
|
return head({
|
||||||
|
title: this.$t('census.headerLong'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -45,6 +45,7 @@ app.use(require('./routes/sources').default);
|
||||||
app.use(require('./routes/nouns').default);
|
app.use(require('./routes/nouns').default);
|
||||||
app.use(require('./routes/inclusive').default);
|
app.use(require('./routes/inclusive').default);
|
||||||
app.use(require('./routes/pronounce').default);
|
app.use(require('./routes/pronounce').default);
|
||||||
|
app.use(require('./routes/census').default);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
path: '/api',
|
path: '/api',
|
||||||
|
|
|
@ -0,0 +1,64 @@
|
||||||
|
import { Router } from 'express';
|
||||||
|
import SQL from 'sql-template-strings';
|
||||||
|
import sha1 from 'sha1';
|
||||||
|
import {ulid} from "ulid";
|
||||||
|
|
||||||
|
const buildFingerprint = req => sha1(`
|
||||||
|
${req.ip}
|
||||||
|
${req.headers['user-agent']}
|
||||||
|
${req.headers['accept-language']}
|
||||||
|
`);
|
||||||
|
|
||||||
|
const hasFinished = async req => {
|
||||||
|
if (req.user) {
|
||||||
|
const byUser = await req.db.get(SQL`
|
||||||
|
SELECT * FROM census
|
||||||
|
WHERE locale = ${req.config.locale}
|
||||||
|
AND edition = ${req.config.census.edition}
|
||||||
|
AND userId = ${req.user.id}
|
||||||
|
`);
|
||||||
|
if (byUser) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fingerprint = buildFingerprint(req);
|
||||||
|
const byFingerprint = await req.db.get(SQL`
|
||||||
|
SELECT * FROM census
|
||||||
|
WHERE locale = ${req.config.locale}
|
||||||
|
AND edition = ${req.config.census.edition}
|
||||||
|
AND fingerprint = ${fingerprint}
|
||||||
|
`);
|
||||||
|
if (byFingerprint) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.get('/census/finished', async (req, res) => {
|
||||||
|
return res.json(await hasFinished(req));
|
||||||
|
});
|
||||||
|
|
||||||
|
router.post('/census/submit', async (req, res) => {
|
||||||
|
if (await hasFinished(req)) {
|
||||||
|
return res.status(401).json({error: 'Unauthorised'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = ulid();
|
||||||
|
await req.db.get(SQL`INSERT INTO census (id, locale, edition, userId, fingerprint, answers, writins) VALUES (
|
||||||
|
${id},
|
||||||
|
${req.config.locale},
|
||||||
|
${req.config.census.edition},
|
||||||
|
${req.user ? req.user.id : null},
|
||||||
|
${buildFingerprint(req)},
|
||||||
|
${req.body.answers},
|
||||||
|
${req.body.writins}
|
||||||
|
)`);
|
||||||
|
|
||||||
|
return res.json(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
|
@ -163,3 +163,11 @@ export const sortByValue = (obj, reverse = false) => {
|
||||||
}
|
}
|
||||||
return zip(sortedArray.sort((a, b) => reverse ? b[0] - a[0] : a[0] - b[0]), true);
|
return zip(sortedArray.sort((a, b) => reverse ? b[0] - a[0] : a[0] - b[0]), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const shuffle = a => {
|
||||||
|
for (let i = a.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[a[i], a[j]] = [a[j], a[i]];
|
||||||
|
}
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,9 @@ export default (key, params = {}, warn = true) => {
|
||||||
|
|
||||||
for (let k in params) {
|
for (let k in params) {
|
||||||
if (params.hasOwnProperty(k)) {
|
if (params.hasOwnProperty(k)) {
|
||||||
value = value.replace(new RegExp('%' + k + '%', 'g'), params[k])
|
value = Array.isArray(value)
|
||||||
|
? value.map(v => v.replace(new RegExp('%' + k + '%', 'g'), params[k]))
|
||||||
|
: value.replace(new RegExp('%' + k + '%', 'g'), params[k]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Reference in New Issue