Merge branch 'master' into gh

This commit is contained in:
lesion 2023-01-09 17:15:21 +01:00
commit c8cc5c6c97
No known key found for this signature in database
GPG key ID: 352918250B012177
74 changed files with 1330 additions and 876 deletions

View file

@ -1,5 +1,9 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
###
- models initialization refactored, better dev xperience as backend hmr is working
### 1.6.1 - 15 dec '22 ### 1.6.1 - 15 dec '22
- allow edit tags in admin panel, fix #170 - allow edit tags in admin panel, fix #170
- fix header / fallback image upload, fix #222 - fix header / fallback image upload, fix #222

View file

@ -8,7 +8,7 @@ export function attributesFromEvents(_events) {
const key = dayjs.unix(e.start_datetime).tz().format('MMDD') // Math.floor(e.start_datetime/(3600*24)) // dayjs.unix(e.start_datetime).tz().format('YYYYMMDD') const key = dayjs.unix(e.start_datetime).tz().format('MMDD') // Math.floor(e.start_datetime/(3600*24)) // dayjs.unix(e.start_datetime).tz().format('YYYYMMDD')
const c = (e.end_datetime || e.start_datetime) < now ? 'vc-past' : '' const c = (e.end_datetime || e.start_datetime) < now ? 'vc-past' : ''
if (e.multidate) { if (e.multidate === true) {
attributes.push({ attributes.push({
dates: { start: new Date(e.start_datetime * 1000), end: new Date(e.end_datetime * 1000) }, dates: { start: new Date(e.start_datetime * 1000), end: new Date(e.end_datetime * 1000) },
highlight: { highlight: {

View file

@ -1,22 +1,20 @@
<template> <template>
<nav> <nav>
<NavHeader />
<NavHeader/>
<!-- title --> <!-- title -->
<div class='text-center'> <div class="text-center">
<nuxt-link id='title' v-text='settings.title' to='/' /> <nuxt-link id="title" v-text="settings.title" to="/" />
<div class='text-body-1 font-weight-light' v-text='settings.description' /> <div
class="text-body-1 font-weight-light"
v-text="settings.description"
/>
</div> </div>
<NavSearch /> <NavSearch />
<NavBar /> <NavBar />
</nav> </nav>
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState } from 'vuex'
@ -27,18 +25,24 @@ import NavSearch from './NavSearch.vue'
export default { export default {
name: 'Appbar', name: 'Appbar',
components: { NavHeader, NavBar, NavSearch }, components: { NavHeader, NavBar, NavSearch },
computed: mapState(['settings']) computed: mapState(['settings']),
} }
</script> </script>
<style> <style>
nav { nav {
background-image: linear-gradient(rgba(0, 0, 0, 0.8), rgba(20, 20, 20, 0.7)), url(/headerimage.png); background-image: linear-gradient(rgba(0, 0, 0, 0.8), rgba(20, 20, 20, 0.7)),
url(/headerimage.png);
background-position: center center; background-position: center center;
background-size: cover; background-size: cover;
} }
.theme--light nav { .theme--light nav {
background-image: linear-gradient(to bottom, rgba(230,230,230,.95), rgba(250,250,250,.95)), url(/headerimage.png); background-image: linear-gradient(
to bottom,
rgba(230, 230, 230, 0.95),
rgba(250, 250, 250, 0.95)
),
url(/headerimage.png);
} }
#title { #title {
@ -46,5 +50,4 @@ nav {
font-weight: 600; font-weight: 600;
text-decoration: none; text-decoration: none;
} }
</style> </style>

View file

@ -15,13 +15,14 @@
aria-label='Calendar' aria-label='Calendar'
is-expanded is-expanded
is-inline) is-inline)
template(v-slot="{ inputValue, inputEvents }") //- template(v-slot="{ inputValue, inputEvents }")
v-btn#calendarButton(v-on='inputEvents' text tile :color='selectedDate ? "primary" : "" ') {{inputValue || $t('common.calendar')}} v-btn#calendarButton(v-on='inputEvents' text tile :color='selectedDate ? "primary" : "" ') {{inputValue || $t('common.calendar')}}
v-icon(v-if='selectedDate' v-text='mdiClose' right small icon @click.prevent.stop='selectedDate = null') v-icon(v-if='selectedDate' v-text='mdiClose' right small icon @click.prevent.stop='selectedDate = null')
v-icon(v-else v-text='mdiChevronDown' right small icon) v-icon(v-else v-text='mdiChevronDown' right small icon)
template(v-slot:placeholder) .calh.d-flex.justify-center.align-center(slot='placeholder')
v-btn#calendarButton(text tile) {{$t('common.calendar')}} v-progress-circular(indeterminate)
v-icon(v-text='mdiChevronDown' right small icon) //- v-btn#calendarButton(text tile) {{$t('common.calendar')}}
//- v-icon(v-text='mdiChevronDown' right small icon)
</template> </template>

View file

@ -9,7 +9,7 @@ v-dialog(v-model='show'
@keydown.esc='cancel') @keydown.esc='cancel')
v-card v-card
v-card-title {{ title }} v-card-title {{ title }}
v-card-text(v-show='!!message') {{ message }} v-card-text(v-show='!!message' v-html='message')
v-card-actions v-card-actions
v-spacer v-spacer
v-btn(outlined color='error' @click='cancel') {{$t('common.cancel')}} v-btn(outlined color='error' @click='cancel') {{$t('common.cancel')}}

View file

@ -24,8 +24,9 @@ v-col(cols=12)
is-inline is-inline
is-expanded is-expanded
:min-date='type !== "recurrent" && new Date()') :min-date='type !== "recurrent" && new Date()')
template(#placeholder) //- template(#placeholder)
span.calc Loading .d-flex.calh.justify-center(slot='placeholder')
v-progress-circular(indeterminate)
div.text-center.mb-2(v-if='type === "recurrent"') div.text-center.mb-2(v-if='type === "recurrent"')
span(v-if='value.recurrent.frequency !== "1m" && value.recurrent.frequency !== "2m"') {{ whenPatterns }} span(v-if='value.recurrent.frequency !== "1m" && value.recurrent.frequency !== "2m"') {{ whenPatterns }}
@ -94,7 +95,7 @@ v-col(cols=12)
</template> </template>
<script> <script>
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { mapState } from 'vuex' import { mapState, mapActions } from 'vuex'
import List from '@/components/List' import List from '@/components/List'
import { attributesFromEvents } from '../assets/helper' import { attributesFromEvents } from '../assets/helper'
import { mdiClockTimeFourOutline, mdiClockTimeEightOutline, mdiClose } from '@mdi/js' import { mdiClockTimeFourOutline, mdiClockTimeEightOutline, mdiClose } from '@mdi/js'
@ -113,7 +114,6 @@ export default {
menuFromHour: false, menuFromHour: false,
menuDueHour: false, menuDueHour: false,
type: this.value.type || 'normal', type: this.value.type || 'normal',
events: [],
frequencies: [ frequencies: [
{ value: '1w', text: this.$t('event.each_week') }, { value: '1w', text: this.$t('event.each_week') },
{ value: '2w', text: this.$t('event.each_2w') }, { value: '2w', text: this.$t('event.each_2w') },
@ -122,7 +122,7 @@ export default {
} }
}, },
computed: { computed: {
...mapState(['settings']), ...mapState(['settings', 'events']),
fromDate () { fromDate () {
if (this.value.from) { if (this.value.from) {
if (this.value.multidate) { if (this.value.multidate) {
@ -138,7 +138,7 @@ export default {
return this.events.filter(e => e.start_datetime >= start && e.start_datetime <= end) return this.events.filter(e => e.start_datetime >= start && e.start_datetime <= end)
}, },
attributes() { attributes() {
return attributesFromEvents(this.events) return attributesFromEvents(this.events.filter(e => e.id !== this.event.id))
}, },
whenPatterns() { whenPatterns() {
if (!this.value.from) { return } if (!this.value.from) { return }
@ -192,13 +192,12 @@ export default {
} else { } else {
this.type = 'normal' this.type = 'normal'
} }
this.events = await this.$api.getEvents({ if (!this.events) {
start: dayjs().unix(), this.getEvents()
show_recurrent: true }
})
this.events = this.events.filter(e => e.id !== this.event.id)
}, },
methods: { methods: {
...mapActions(['getEvents']),
updateRecurrent(value) { updateRecurrent(value) {
this.$emit('input', { ...this.value, recurrent: value || null }) this.$emit('input', { ...this.value, recurrent: value || null })
}, },

View file

@ -1,19 +1,51 @@
<template lang="pug"> <template lang="pug">
#navsearch.mt-2.mt-sm-4(v-if='showCollectionsBar || showSearchBar') #navsearch.mt-2.mt-sm-4(v-if='showCollectionsBar || showSearchBar || showCalendar')
v-text-field.mx-2(v-if='showSearchBar' outlined dense hide-details :placeholder='$t("common.search")' :append-icon='mdiMagnify' @input='search' clearable :clear-icon='mdiClose')
template(v-slot:prepend-inner) div.mx-2
Calendar(v-if='!settings.hide_calendar') client-only(v-if='showSearchBar')
v-btn.ml-2.mt-2.gap-2(v-if='showCollectionsBar' small outlined v-for='collection in collections' color='primary' :key='collection.id' :to='`/collection/${encodeURIComponent(collection.name)}`') {{collection.name}} v-menu(offset-y :close-on-content-click='false' tile)
template(v-slot:activator="{on ,attrs}")
v-text-field(hide-details outlined
:placeholder='$t("common.search")'
@input="v => setFilter(['query', v])" clearable :clear-icon='mdiClose')
template(v-slot:append)
v-icon(v-text='mdiCog' v-bind='attrs' v-on='on')
v-card(outlined :rounded='"0"')
v-card-text
v-row(dense)
v-col(v-if='settings.allow_recurrent_event')
v-switch.mt-0(v-model='show_recurrent' @change="v => setFilter(['show_recurrent', v])"
hide-details :label="$t('event.show_recurrent')" inset)
v-col(v-if='settings.allow_multidate_event')
v-switch.mt-0(v-model='show_multidate' @change="v => setFilter(['show_multidate', v])"
hide-details :label="$t('event.show_multidate')" inset)
v-row(v-if='!showCalendar')
v-col
Calendar.mt-2
v-text-field(slot='placeholder' outlined hide-details :placeholder="$t('common.search')" :append-icon='mdiCog')
span(v-if='showCollectionsBar')
v-btn.mr-2.mt-2(small outlined v-for='collection in collections'
color='primary' :key='collection.id'
:to='`/collection/${encodeURIComponent(collection.name)}`') {{collection.name}}
Calendar.mt-2(v-if='showCalendar')
</template> </template>
<script> <script>
import { mapState } from 'vuex' import { mapState, mapActions } from 'vuex'
import Calendar from '@/components/Calendar' import Calendar from '@/components/Calendar'
import { mdiMagnify, mdiClose } from '@mdi/js' import { mdiClose, mdiCog } from '@mdi/js'
export default { export default {
data: () => ({ data: ({ $store }) => ({
mdiMagnify, mdiClose, oldRoute: '',
collections: [] mdiClose, mdiCog,
collections: [],
show_recurrent: $store.state.settings.recurrent_event_visible,
show_multidate: true,
query: ''
}), }),
async fetch () { async fetch () {
this.collections = await this.$axios.$get('collections').catch(_e => []) this.collections = await this.$axios.$get('collections').catch(_e => [])
@ -23,21 +55,27 @@ export default {
showSearchBar () { showSearchBar () {
return this.$route.name === 'index' return this.$route.name === 'index'
}, },
showCollectionsBar () { showCalendar () {
return ['index', 'collection-collection'].includes(this.$route.name) return (!this.settings.hide_calendar && this.$route.name === 'index')
}, },
...mapState(['settings']) showCollectionsBar () {
const show = ['index', 'collection-collection'].includes(this.$route.name)
if (show && this.oldRoute !== this.$route.name) {
this.oldRoute = this.$route.name
this.$fetch()
}
return show
},
...mapState(['settings', 'filter'])
}, },
methods: { methods: {
search (ev) { ...mapActions(['setFilter']),
this.$root.$emit('search', ev)
}
} }
} }
</script> </script>
<style> <style>
#navsearch { #navsearch {
margin: 0 auto; margin: 0 auto;
max-width: 800px; max-width: 700px;
} }
</style> </style>

View file

@ -125,9 +125,14 @@ export default {
return matches return matches
} }
}, },
mounted () {
this.$nextTick( () => {
this.search()
})
},
methods: { methods: {
search: debounce(async function(ev) { search: debounce(async function(ev) {
const search = ev.target.value.trim().toLowerCase() const search = ev ? ev.target.value.trim().toLowerCase() : ''
this.places = await this.$axios.$get(`place?search=${search}`) this.places = await this.$axios.$get(`place?search=${search}`)
if (!search && this.places.length) { return this.places } if (!search && this.places.length) { return this.places }
const matches = this.places.find(p => search === p.name.toLocaleLowerCase()) const matches = this.places.find(p => search === p.name.toLocaleLowerCase())
@ -256,7 +261,7 @@ export default {
this.addressList = [] this.addressList = []
} }
} else if (this.geocoding_provider_type == "Photon") { } else if (this.geocoding_provider_type == "Photon") {
let photon_properties = ['housenumber', 'street', 'district', 'city', 'county', 'state', 'postcode', 'country'] let photon_properties = ['housenumber', 'street', 'locality', 'district', 'city', 'county', 'state', 'postcode', 'country']
if (ret) { if (ret) {
this.addressList = ret.features.map(v => { this.addressList = ret.features.map(v => {

View file

@ -69,7 +69,7 @@ v-container
//- v-list-item-subtitle(v-text='item.address') //- v-list-item-subtitle(v-text='item.address')
v-col(cols=2) v-col(cols=2)
v-btn(color='primary' text @click='addFilter' :disabled='!collection.id || !filterPlaces.length && !filterTags.length') add <v-icon v-text='mdiPlus'></v-icon> v-btn(color='primary' :loading='loading' text @click='addFilter' :disabled='loading || !collection.id || !filterPlaces.length && !filterTags.length') add <v-icon v-text='mdiPlus'></v-icon>
v-data-table( v-data-table(
:headers='filterHeaders' :headers='filterHeaders'
@ -110,6 +110,9 @@ v-container
<script> <script>
import get from 'lodash/get' import get from 'lodash/get'
import debounce from 'lodash/debounce' import debounce from 'lodash/debounce'
import isEqual from 'lodash/isEqual'
import sortBy from 'lodash/sortBy'
import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiPlus, mdiTagMultiple, mdiMapMarker, mdiDeleteForever, mdiCloseCircle, mdiChevronDown } from '@mdi/js' import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiPlus, mdiTagMultiple, mdiMapMarker, mdiDeleteForever, mdiCloseCircle, mdiChevronDown } from '@mdi/js'
export default { export default {
@ -147,7 +150,7 @@ export default {
methods: { methods: {
searchTags: debounce(async function (ev) { searchTags: debounce(async function (ev) {
this.tags = await this.$axios.$get(`/tag?search=${ev.target.value}`) this.tags = await this.$axios.$get(`/tag?search=${encodeURIComponent(ev.target.value)}`)
}, 100), }, 100),
searchPlaces: debounce(async function (ev) { searchPlaces: debounce(async function (ev) {
this.places = await this.$axios.$get(`/place?search=${ev.target.value}`) this.places = await this.$axios.$get(`/place?search=${ev.target.value}`)
@ -163,9 +166,20 @@ export default {
this.loading = true this.loading = true
const tags = this.filterTags const tags = this.filterTags
const places = this.filterPlaces.map(p => ({ id: p.id, name: p.name })) const places = this.filterPlaces.map(p => ({ id: p.id, name: p.name }))
const filter = await this.$axios.$post('/filter', { collectionId: this.collection.id, tags, places })
const filter = { collectionId: this.collection.id, tags, places }
// tags and places are JSON field and there's no way to use them inside a unique constrain
//
const alreadyExists = this.filters.find(f =>
isEqual(sortBy(f.places, 'id'), sortBy(filter.places, 'id')) && isEqual(sortBy(f.tags), sortBy(filter.tags))
)
if (alreadyExists) return
const ret = await this.$axios.$post('/filter', filter )
this.$fetch() this.$fetch()
this.filters.push(filter) this.filters.push(ret)
this.filterTags = [] this.filterTags = []
this.filterPlaces = [] this.filterPlaces = []
this.loading = false this.loading = false

View file

@ -54,11 +54,10 @@ v-container
<script> <script>
import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiEye, mdiMapSearch, mdiChevronDown, mdiDeleteForever, mdiTag } from '@mdi/js' import { mdiPencil, mdiChevronLeft, mdiChevronRight, mdiMagnify, mdiEye, mdiMapSearch, mdiChevronDown, mdiDeleteForever, mdiTag } from '@mdi/js'
import { mapState } from 'vuex' import { mapState } from 'vuex'
import debounce from 'lodash/debounce'
import get from 'lodash/get' import get from 'lodash/get'
export default { export default {
data( {$store} ) { data() {
return { return {
mdiPencil, mdiChevronRight, mdiChevronLeft, mdiMagnify, mdiEye, mdiMapSearch, mdiChevronDown, mdiDeleteForever, mdiTag, mdiPencil, mdiChevronRight, mdiChevronLeft, mdiMagnify, mdiEye, mdiMapSearch, mdiChevronDown, mdiDeleteForever, mdiTag,
loading: false, loading: false,

View file

@ -106,7 +106,7 @@ docker-compose pull
See [Requirements](#requirements) about downloading the `.osm.pbf` files See [Requirements](#requirements) about downloading the `.osm.pbf` files
```bash ```bash
cd docs/docker/nominatim/ cd docs/docker/nominatim/
wget https://download.geofabrik.de/europe/italy/nord-ovest-updates/nord-ovest-latest.osm.pbf \ wget https://download.geofabrik.de/europe/italy/nord-ovest-latest.osm.pbf \
./nominatim/data/default.osm.pbf ./nominatim/data/default.osm.pbf
``` ```

@ -1 +1 @@
Subproject commit 0486d0852db569b7064535f0d52709e06e9ecd1f Subproject commit 12640461481cc39cdee8efa05e7fd02a6a60e99c

View file

@ -101,7 +101,8 @@
"about": "Sobre aquesta agenda", "about": "Sobre aquesta agenda",
"content": "Contingut", "content": "Contingut",
"admin_actions": "Accions d'administració", "admin_actions": "Accions d'administració",
"recurring_event_actions": "Accions d'activitats recorrents" "recurring_event_actions": "Accions d'activitats recorrents",
"tag": "Etiqueta"
}, },
"login": { "login": {
"description": "Amb la sessió iniciada pots afegir activitats noves.", "description": "Amb la sessió iniciada pots afegir activitats noves.",
@ -278,7 +279,29 @@
"domain": "Domini", "domain": "Domini",
"known_users": "Usuàries conegudes", "known_users": "Usuàries conegudes",
"created_at": "Creada", "created_at": "Creada",
"default_images_help": "<a href='/admin?tab=theme'>Actualitza la pàgina</a> per veure els canvis." "default_images_help": "<a href='/admin?tab=theme'>Actualitza la pàgina</a> per veure els canvis.",
"geocoding_countrycodes": "Codis d'estats",
"admin_email_help": "L'adreça que es posa de remitent per enviar correus. També és l'adreça a la qual s'envien els correus d'administració",
"delete_collection_confirm": "Segur que vols eliminar la coŀlecció <u>{collection}</u>?",
"geocoding_provider_type_help": "Per defecte es resolen les adreces amb Nominatim",
"geocoding_provider": "Proveïdora de geocodificació",
"geocoding_provider_type": "Programari de geocodificació",
"geocoding_provider_help": "Per defecte es geocodifiquen les adreces amb Nominatim",
"geocoding_countrycodes_help": "Permet aplicar un filtre per cercar només dins de les fronteres de l'estat",
"geocoding_test_button": "Prova la geocodificació",
"geocoding_test_success": "La geocodificació de {service_name} funciona",
"geocoding_test_error": "El servei de geocodificació de {service_name} no està funcionant",
"tilelayer_provider": "Proveïdora de tesseŀles del mapa base",
"tilelayer_provider_help": "Per defecte les tesseŀles del mapa es baixen del servidor central d'OpenStreetMap",
"tilelayer_test_button": "Prova el mapa base",
"tilelayer_test_success": "El servei de tesseŀles de {service_name} està funcionant",
"tilelayer_test_error": "El servei de tesseŀles de {service_name} no està funcionant",
"geolocation": "Geolocalització",
"edit_tag_help": "Pots canviar l'etiqueta reanomenant-la o bé fusionant-la amb una d'existent. S'actualitzaran les {n} activitats que la fan servir.",
"allow_multidate_event": "Permet activitats de diversos dies",
"delete_tag_confirm": "Segur que vols eliminar l'etiqueta \"{tag}\"? S'esborrarà del sistema i de {n} activitats.",
"edit_tag": "Canvia l'etiqueta",
"tilelayer_provider_attribution": "Atribució"
}, },
"auth": { "auth": {
"not_confirmed": "Encara no s'ha confirmat…", "not_confirmed": "Encara no s'ha confirmat…",

29
locales/email/ru.json Normal file
View file

@ -0,0 +1,29 @@
{
"recover": {
"subject": "Восстановление пароля",
"content": "Здравствуйте, вы запросили восстановление пароля на {{config.title}}. <a href='{{config.baseurl}}/recover/{{user.recover_code}}'>Нажмите здесь</a> для подтверждения."
},
"confirm": {
"subject": "Теперь вы можете начать публиковать события",
"content": "Здравствуйте, ваша учетная запись <a href='{{config.baseurl}}'>{{config.title}}</a> была подтверждена. Пишите нам по адресу {{config.admin_email}} для получения любой информации."
},
"admin_register": {
"subject": "Новая регистрация",
"content": "{{user.email}} запросил регистрацию на {{config.title}}: <br/><pre>{{user.description}}</pre><br/> Подтвердите это <a href='{{config.baseurl}}/admin'>здесь</a>."
},
"user_confirm": {
"subject": "Теперь вы можете начать публиковать события",
"content": "Здравствуйте, ваша учетная запись <a href='{{config.baseurl}}'>{{config.title}}</a> была создана. <a href='{{config.baseurl}}/user_confirm/{{user.recover_code}}'>Подтвердите это и измените пароль.</a>."
},
"register": {
"content": "Мы получили запрос на регистрацию. Мы подтвердим его как можно скорее.",
"subject": "Запрос на регистрацию получен"
},
"event_confirm": {
"content": "Вы можете подтвердить это событие по адресу <a href='{{url}}'>на этой странице</a>"
},
"test": {
"subject": "Ваша конфигурация SMTP работает",
"content": "Это тестовое письмо, если вы читаете его, ваша конфигурация работает."
}
}

View file

@ -147,6 +147,7 @@
"recurrent": "Recurring", "recurrent": "Recurring",
"edit_recurrent": "Edit recurring event:", "edit_recurrent": "Edit recurring event:",
"show_recurrent": "recurring events", "show_recurrent": "recurring events",
"show_multidate": "multidate events",
"show_past": "also prior events", "show_past": "also prior events",
"only_future": "only upcoming events", "only_future": "only upcoming events",
"recurrent_description": "Choose frequency and select days", "recurrent_description": "Choose frequency and select days",
@ -264,6 +265,7 @@
"new_collection": "New collection", "new_collection": "New collection",
"collections_description": "Collections are groupings of events by tags and places. They will be displayed on the home page", "collections_description": "Collections are groupings of events by tags and places. They will be displayed on the home page",
"edit_collection": "Edit Collection", "edit_collection": "Edit Collection",
"delete_collection_confirm": "Are you sure you want to remove the collection <u>{collection}</u>?",
"config_plugin": "Plugin configuration", "config_plugin": "Plugin configuration",
"plugins_description": "", "plugins_description": "",
"fallback_image": "Fallback image", "fallback_image": "Fallback image",

View file

@ -101,7 +101,8 @@
"calendar": "Calendario", "calendar": "Calendario",
"content": "Contenido", "content": "Contenido",
"admin_actions": "Acciones de administrador", "admin_actions": "Acciones de administrador",
"recurring_event_actions": "Acciones de eventos recurrentes" "recurring_event_actions": "Acciones de eventos recurrentes",
"tag": "Etiqueta"
}, },
"login": { "login": {
"description": "Entrando podrás publicar nuevos eventos.", "description": "Entrando podrás publicar nuevos eventos.",
@ -281,7 +282,25 @@
"admin_email_help": "La dirección que usamos como remitente para enviar emails. Esta es también la dirección a la cual los emails de administración son enviados", "admin_email_help": "La dirección que usamos como remitente para enviar emails. Esta es también la dirección a la cual los emails de administración son enviados",
"allow_multidate_event": "Permitir eventos de múltiples días", "allow_multidate_event": "Permitir eventos de múltiples días",
"geocoding_countrycodes": "Códigos de país", "geocoding_countrycodes": "Códigos de país",
"geocoding_provider_help": "El proveedor por defecto es Nominatim" "geocoding_provider_help": "El proveedor por defecto es Nominatim",
"geocoding_provider_type": "Software de geocodificación",
"geocoding_provider_type_help": "El programa por defecto es Nominatim",
"geocoding_countrycodes_help": "Permite establecer un filtro para las búsquedas basadas en códigos de área",
"geocoding_test_button": "Prueba de geocodificación",
"delete_collection_confirm": "¿Estás seguro de que quieres eliminar la colección <u>{collection}</u>?",
"geocoding_test_success": "El servicio de geocodificación de {service_name} funciona",
"tilelayer_provider": "Proveedor de mosaicos de mapas",
"tilelayer_provider_help": "El proveedor por defecto es OpenStreetMap",
"tilelayer_provider_attribution": "Atribución",
"tilelayer_test_button": "Capa de mosaicos de prueba",
"tilelayer_test_success": "El servicio de mosaicos en {service_name} está funcionando",
"tilelayer_test_error": "No se puede acceder al servicio de mosaico en {service_name}",
"geolocation": "Geolocalización",
"delete_tag_confirm": "¿Está seguro de que desea eliminar la etiqueta \"{tag}\"? La etiqueta se eliminará de {n} eventos.",
"edit_tag": "Editar la etiqueta",
"edit_tag_help": "Puede cambiar la etiqueta sustituyéndola por una nueva o fusionándola con una existente. Los {n} eventos asociados también se modificarán.",
"geolocation_description": "<b>1. Defina un proveedor para el servicio de codificación geográfica</b>.<br>Actualmente, entre los enumerados en el <a href=\"https://wiki.openstreetmap.org/wiki/Nominatim#Alternatives_.2F_Third-party_providers\">wiki de OpenStreetMap </a>, hay soporte para el software <a href=\"https://github.com/osm-search/Nominatim\">Nominatim</a> y <a href=\"https://github.com/komoot /photon\">Photon</a>.<br>Puede usar una de las demostraciones oficiales relacionadas copiando el enlace en el campo 'Proveedor de codificación geográfica':<ul><li>https://nominatim.openstreetmap.org/search (<a href=\"https://operations.osmfoundation.org/policies/nominatim/\">Términos de servicio</a>)</li><li>https://photon.komoot.io/api/ (<a href=\"https://photon.komoot.io/\"> Condiciones de servicio</a>)</li></ul><br><b>2. Defina un proveedor para las capas del mapa.</b><br>Puede encontrar una lista de ellos aquí: <a href=\"https://leaflet-extras.github.io/leaflet-providers/preview/\">https://leaflet-extras.github.io/leaflet-providers/preview/</a>",
"geocoding_test_error": "No se puede acceder al servicio de geocodificación en {service_name}"
}, },
"auth": { "auth": {
"not_confirmed": "Todavía no hemos confirmado este email…", "not_confirmed": "Todavía no hemos confirmado este email…",

View file

@ -101,7 +101,8 @@
"about": "Acerca de", "about": "Acerca de",
"content": "Contido", "content": "Contido",
"admin_actions": "Accións de Admin", "admin_actions": "Accións de Admin",
"recurring_event_actions": "Accións de eventos recurrentes" "recurring_event_actions": "Accións de eventos recurrentes",
"tag": "Etiqueta"
}, },
"recover": { "recover": {
"not_valid_code": "Algo fallou." "not_valid_code": "Algo fallou."
@ -142,7 +143,7 @@
"interact_with_me": "Sígueme", "interact_with_me": "Sígueme",
"remove_recurrent_confirmation": "Tes a certeza de querer eliminar este evento recurrente?\nOs eventos pasados permanecerán, pero non se crearán novos eventos.", "remove_recurrent_confirmation": "Tes a certeza de querer eliminar este evento recurrente?\nOs eventos pasados permanecerán, pero non se crearán novos eventos.",
"import_URL": "Importar desde URL", "import_URL": "Importar desde URL",
"anon": "Anon", "anon": "Anónimo",
"anon_description": "Podes engadir un evento sen rexistrarte nen acceder, pero deberás agardar a que alguén o lea e\nconfirme que é un evento axeitado. Non será posible modificalo.<br/><br/>\nPodes tamén <a href='/login'>acceder</a> ou <a href='/register'>crear unha conta</a>. Mais podes continuar e agardar a que se comprobe. ", "anon_description": "Podes engadir un evento sen rexistrarte nen acceder, pero deberás agardar a que alguén o lea e\nconfirme que é un evento axeitado. Non será posible modificalo.<br/><br/>\nPodes tamén <a href='/login'>acceder</a> ou <a href='/register'>crear unha conta</a>. Mais podes continuar e agardar a que se comprobe. ",
"same_day": "no mesmo día", "same_day": "no mesmo día",
"tag_description": "Cancelo", "tag_description": "Cancelo",
@ -288,7 +289,11 @@
"geolocation": "Xeolocalización", "geolocation": "Xeolocalización",
"allow_multidate_event": "Permitir eventos de varios días", "allow_multidate_event": "Permitir eventos de varios días",
"admin_email_help": "O enderezo que se usará como remitente para os emails. Tamén é o enderezo ao que se envían emails de administración", "admin_email_help": "O enderezo que se usará como remitente para os emails. Tamén é o enderezo ao que se envían emails de administración",
"geolocation_description": "<b>1. Define un provedor para o servizo geocoding</b>.<br>Actualmente, entre os que aparecen na <a href=\"https://wiki.openstreetmap.org/wiki/Nominatim#Alternatives_.2F_Third-party_providers\">wiki de OpenStreetMap</a>, temos soporte para <a href=\"https://github.com/osm-search/Nominatim\">Nominatim</a> e <a href=\"https://github.com/komoot/photon\">Photon</a>.<br>Podes usar unha das demos oficiais indicadas copiando a ligazón no campo 'Provedor Geocoding':<ul><li>https://nominatim.openstreetmap.org/search (<a href=\"https://operations.osmfoundation.org/policies/nominatim/\">Termos do Servizo</a>)</li><li>https://photon.komoot.io/api/ (<a href=\"https://photon.komoot.io/\">Termos do Servizo</a>)</li></ul><br><b>2. Define un provedor para capas do mapa.</b><br>Aquí hai unha lista: <a href=\"https://leaflet-extras.github.io/leaflet-providers/preview/\">https://leaflet-extras.github.io/leaflet-providers/preview/</a>" "geolocation_description": "<b>1. Define un provedor para o servizo geocoding</b>.<br>Actualmente, entre os que aparecen na <a href=\"https://wiki.openstreetmap.org/wiki/Nominatim#Alternatives_.2F_Third-party_providers\">wiki de OpenStreetMap</a>, temos soporte para <a href=\"https://github.com/osm-search/Nominatim\">Nominatim</a> e <a href=\"https://github.com/komoot/photon\">Photon</a>.<br>Podes usar unha das demos oficiais indicadas copiando a ligazón no campo 'Provedor Geocoding':<ul><li>https://nominatim.openstreetmap.org/search (<a href=\"https://operations.osmfoundation.org/policies/nominatim/\">Termos do Servizo</a>)</li><li>https://photon.komoot.io/api/ (<a href=\"https://photon.komoot.io/\">Termos do Servizo</a>)</li></ul><br><b>2. Define un provedor para capas do mapa.</b><br>Aquí hai unha lista: <a href=\"https://leaflet-extras.github.io/leaflet-providers/preview/\">https://leaflet-extras.github.io/leaflet-providers/preview/</a>",
"edit_tag": "Editar etiqueta",
"edit_tag_help": "Podes cambiar a etiqueta substituíndoa por unha nova ou fusionándoa cunha existente. Tamén se cambiarán os {n} eventos asociados.",
"delete_tag_confirm": "Estás seguro de que queres eliminar a etiqueta \"{tag}\"? A etiqueta eliminarase de {n} eventos.",
"delete_collection_confirm": "Estás seguro de que queres eliminar a colección <u>{collection}</u>?"
}, },
"auth": { "auth": {
"not_confirmed": "Aínda non foi confirmado…", "not_confirmed": "Aínda non foi confirmado…",

View file

@ -10,6 +10,7 @@ module.exports = {
nb: 'Norwegian Bokmål', nb: 'Norwegian Bokmål',
pl: 'Polski', pl: 'Polski',
pt: 'Português', pt: 'Português',
ru: 'Русский',
sk: 'Slovak', sk: 'Slovak',
zh: '中国' zh: '中国'
} }

View file

@ -147,6 +147,7 @@
"recurrent": "Ricorrente", "recurrent": "Ricorrente",
"edit_recurrent": "Modifica evento ricorrente:", "edit_recurrent": "Modifica evento ricorrente:",
"show_recurrent": "appuntamenti ricorrenti", "show_recurrent": "appuntamenti ricorrenti",
"show_multidate": "eventi di più giorni",
"show_past": "eventi passati", "show_past": "eventi passati",
"recurrent_description": "Scegli la frequenza e seleziona i giorni", "recurrent_description": "Scegli la frequenza e seleziona i giorni",
"multidate_description": "Un festival o una tre giorni? Scegli quando comincia e quando finisce", "multidate_description": "Un festival o una tre giorni? Scegli quando comincia e quando finisce",

View file

@ -189,7 +189,10 @@
"known_users": "Usuários conhecidos", "known_users": "Usuários conhecidos",
"created_at": "Criado em", "created_at": "Criado em",
"hide_calendar": "Ocultar calendário", "hide_calendar": "Ocultar calendário",
"blocked": "Bloqueado" "blocked": "Bloqueado",
"admin_email": "E-mail do admin",
"tilelayer_provider_attribution": "Atribuição",
"geolocation": "Geolocalização"
}, },
"event": { "event": {
"follow_me_description": "Uma das maneiras de se manter atualizado com os eventos publicados aqui em {title},\né seguir a conta <u>{account}</u> no Fediverso, por exemplo via Mastodon, e possivelmente adicionar recursos para um evento a partir de lá.<br/><br/>\nSe você nunca ouviu falar sobre Mastodon ou do Fediverso nós recomendamos ler <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'>este artigo</a>.<br/><br/>Entre com sua instância abaixo (e.g. mastodon.social)", "follow_me_description": "Uma das maneiras de se manter atualizado com os eventos publicados aqui em {title},\né seguir a conta <u>{account}</u> no Fediverso, por exemplo via Mastodon, e possivelmente adicionar recursos para um evento a partir de lá.<br/><br/>\nSe você nunca ouviu falar sobre Mastodon ou do Fediverso nós recomendamos ler <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'>este artigo</a>.<br/><br/>Entre com sua instância abaixo (e.g. mastodon.social)",

333
locales/ru.json Normal file
View file

@ -0,0 +1,333 @@
{
"common": {
"send": "Отправить",
"where": "Где",
"address": "Адрес",
"when": "Когда",
"what": "Что",
"media": "Медиа",
"login": "Логин",
"email": "E-mail",
"password": "Пароль",
"register": "Зарегистрироваться",
"description": "Описание",
"remove": "Удалить",
"search": "Поиск",
"edit": "Ред.",
"info": "Инфо",
"add_event": "Доб. событие",
"hide": "Скрыть",
"confirm": "Подтвердить",
"admin": "Администратор",
"users": "Пользователи",
"events": "События",
"places": "Места",
"settings": "Опции",
"actions": "Действия",
"deactivate": "Выключить",
"remove_admin": "Удалить администратора",
"activate": "Активировать",
"save": "Сохранить",
"preview": "Предв. просмотр",
"logout": "Выйти",
"share": "Поделиться",
"name": "Имя",
"edit_event": "Ред. событие",
"related": "Связанные",
"add": "Доб.",
"logout_ok": "Вышел из системы",
"copy": "Копировать",
"recover_password": "Восстановить пароль",
"new_password": "Новый пароль",
"new_user": "Новый пользователь",
"ok": "Ок",
"cancel": "Отмена",
"enable": "Включить",
"disable": "Отключить",
"me": "Вы",
"password_updated": "Пароль изменен.",
"resources": "Ресурсы",
"n_resources": "нет ресурсов|ресурс|{n} ресурсы",
"activate_user": "Подтверждено",
"send_via_mail": "Отправить e-mail письмо",
"add_to_calendar": "Доб. в календарь",
"copied": "Скопировано",
"embed": "Встроить",
"embed_title": "Вставьте это событие на свой сайт",
"embed_help": "Скопируйте следующий код на свой сайт, и событие будет отображаться как здесь",
"feed": "RSS-канал",
"feed_url_copied": "Откройте скопированный URL канала в программе чтения RSS-каналов",
"follow_me_title": "Следите за обновлениями от fediverse",
"follow": "Подписаться",
"moderation": "Модерация",
"authorize": "Авторизация",
"filter": "Фильтр",
"start": "Начало",
"fediverse": "Fediverse",
"skip": "Пропустить",
"delete": "Удалить",
"import": "Импорт",
"max_events": "N. максимум событий",
"label": "Этикетка",
"close": "Закрыть",
"plugins": "Плагины",
"home": "Главная",
"content": "Содержание",
"admin_actions": "Действия админа",
"instances": "Экземпляры",
"title": "Название",
"set_password": "Установите пароль",
"copy_link": "Копировать ссылку",
"show_map": "Показать карту",
"about": "О сайте",
"event": "Событие",
"url": "URL",
"tags": "Теги",
"theme": "Тема",
"reset": "Сброс",
"collections": "Коллекции",
"help_translate": "Помогите перевести",
"displayname": "Отображаемое имя",
"federation": "Федерация",
"announcements": "Объявления",
"place": "Место",
"user": "Пользователь",
"pause": "Пауза",
"tag": "Тег",
"calendar": "Календарь",
"next": "След.",
"export": "Экспорт"
},
"login": {
"description": "Войдя в систему, вы можете публиковать новые события.",
"forgot_password": "Забыли пароль?",
"insert_email": "Введите свой адрес электронной почты",
"check_email": "Проверьте свой почтовый ящик и спам.",
"not_registered": "Не зарегистрированы?",
"error": "Не удалось войти в систему. Проверьте информацию для входа.",
"ok": "Вошел в систему"
},
"event": {
"each_2w": "Каждую вторую неделю",
"due": "до",
"from": "От",
"image_too_big": "Изображение не может быть больше 4 МБ",
"interact_with_me_at": "Общайтесь со мной на fediverse по адресу",
"each_month": "Каждый месяц",
"anon": "Анон",
"same_day": "в тот же день",
"what_description": "Название",
"description_description": "Описание",
"tag_description": "Тег",
"media_description": "Вы можете добавить флаер (необязательно)",
"added": "Событие добавлено",
"saved": "Событие сохранено",
"added_anon": "Событие добавлено, но еще не подтверждено.",
"updated": "Событие обновлено",
"where_description": "Где проходит мероприятие? Если его нет, вы можете его создать.",
"address_description": "Какой адрес?",
"address_description_osm": "Какой адрес? (<a href='http://osm.org/copyright'>OpenStreetMap</a> участники)",
"confirmed": "Событие одобрено",
"not_found": "Не удалось найти событие",
"remove_confirmation": "Вы уверены, что хотите удалить это событие?",
"recurrent": "Повторяющийся",
"edit_recurrent": "Ред. повтор. события:",
"show_recurrent": "повторяющ. события",
"show_past": "также предшеств. события",
"only_future": "только предстоящие события",
"recurrent_description": "Выберите частоту и выберите дни",
"multidate_description": "Это фестиваль? Выберите время начала и окончания",
"multidate": "Больше дней",
"normal": "Нормальный",
"normal_description": "Выберите день.",
"recurrent_1w_days": "Каждый {days}",
"interact_with_me": "Подпишись",
"remove_recurrent_confirmation": "Вы уверены, что хотите удалить это повторяющееся событие?\nPast events will be maintained, but no further events will be created.",
"import_URL": "Импорт из URL",
"import_ICS": "Импорт из ICS",
"ics": "ICS",
"alt_text_description": "Описание для людей с нарушениями зрения",
"choose_focal_point": "Выберите фокусную точку",
"remove_media_confirmation": "Вы подтверждаете удаление изображения?",
"download_flyer": "Скачать флаер",
"anon_description": "Вы можете добавить событие, не регистрируясь и не входя в систему, но вам придется подождать, пока кто-то его прочитает,\nподтверждение того, что это подходящее мероприятие. Изменить его будет невозможно.<br/><br/>\nВместо этого вы можете <a href='/login'>войти</a> или <a href='/register'>зарегистрироваться</a>. В противном случае действуйте и получите ответ как можно скорее. ",
"each_week": "Каждую неделю",
"follow_me_description": "Один из способов оставаться в курсе событий, публикуемых здесь на {title},\nследит за аккаунтом<u>{account}</u>из fediverse, например, через Mastodon, и, возможно, добавляет ресурсы в событие оттуда.<br/><br/>\nЕсли вы никогда не слышали о Mastodon и fediverse, рекомендуем прочитать <a href='https://www.savjee.be/videos/simply-explained/mastodon-and-fediverse-explained/'>эта статья</a>.<br/><br/>Введите ниже свой экземпляр (например, mastodon.social)",
"import_description": "Вы можете импортировать события из других платформ и других инстанций с помощью стандартных форматов (ics и h-event)",
"recurrent_1m_ordinal": "{n} {days} каждого месяца",
"recurrent_2w_days": "Каждый второй {days}",
"recurrent_1m_days": "Каждое {days} число месяца"
},
"admin": {
"delete_resource_confirm": "Вы уверены, что хотите удалить этот ресурс?",
"delete_tag_confirm": "Вы уверены, что хотите удалить тег \"{tag}\"? Метка будет удалена из {n} событий.",
"block_user": "Заблок. пользователя",
"filter_instances": "Фильтр экземпляров",
"filter_users": "Фильтр пользователей",
"user_blocked": "Пользователь {user} заблокирован",
"user_block_confirm": "Вы уверены, что хотите заблокировать пользователя {user}?",
"enable_resources_help": "Позволяет добавлять ресурсы в событие из федерации",
"place_description": "Если вы ошиблись с местом или адресом, вы можете изменить его.<br/>Все текущие и прошлые события, связанные с этим местом, изменят адрес.",
"hide_boost_bookmark": "Скрывает буст/закладки",
"favicon": "Логотип",
"resources": "Ресурсы",
"disable_admin_user_confirm": "Вы уверены, что удалили права администратора у {user}?",
"allow_anon_event": "Разрешить анонимные мероприятия (должно быть подтверждение)?",
"recurrent_event_visible": "Показывать повторяющиеся события по умолчанию",
"select_instance_timezone": "Часовой пояс",
"event_confirm_description": "Вы можете подтвердить события, введенные анонимными пользователями, здесь",
"delete_user": "Удалить",
"remove_admin": "Удалить админа",
"disable_user_confirm": "Вы уверены, что хотите отключить {user}?",
"delete_user_confirm": "Вы уверены, что хотите удалить {user}?",
"enable_admin_user_confirm": "Вы уверены, добавить права администратора для {user}?",
"user_remove_ok": "Пользователь удален",
"user_create_ok": "Пользователь создан",
"event_remove_ok": "Событие удалено",
"allow_registration_description": "Разрешить открытую регистрацию?",
"allow_multidate_event": "Разрешить многодневные мероприятия",
"allow_recurrent_event": "Разрешить повторяющиеся события",
"allow_geolocation": "Разрешить геолокацию событий",
"federation": "",
"enable_federation": "Включить федерацию",
"enable_federation_help": "За этим экземпляром можно будет следить из федиверса",
"add_instance": "Доб. экземпляр",
"enable_resources": "Включить ресурсы",
"unblock": "Разблокировать",
"block": "Блокировать",
"user_add_help": "Новому пользователю будет отправлено электронное письмо с инструкциями по подтверждению подписки и выбору пароля",
"instance_name": "Имя экземпляра",
"show_resource": "Показать ресурс",
"hide_resource": "Скрыть ресурс",
"delete_resource": "Удалить ресурс",
"delete_announcement_confirm": "Вы уверены, что хотите удалить объявление?",
"instance_locale": "Язык по умолчанию",
"domain": "Домен",
"known_users": "Известные пользователи",
"delete_footer_link_confirm": "Вы хотите удалить эту ссылку?",
"new_collection": "Новая коллекция",
"default_images_help": "Вы должны <a href='/admin?tab=theme'>обновить</a> страницу, чтобы увидеть изменения.",
"delete_collection_confirm": "Вы уверены, что хотите удалить коллекцию <u>{collection}</u>?",
"geocoding_provider_type_help": "Программное обеспечение по умолчанию - Nominatim",
"created_at": "Создан в",
"instance_block_confirm": "Вы уверены, что хотите блокировать экземпляр {instance}?",
"announcement_remove_ok": "Объявление удалено",
"description_description": "Появляется в заголовке рядом с названием",
"trusted_instances_label_default": "Дружественные экземпляры",
"blocked": "Блокирован",
"geocoding_provider_type": "Программа для геокодирования",
"geocoding_provider": "Провайдер геокодирования",
"geocoding_provider_help": "Поставщиком по умолчанию является Nominatim",
"geolocation": "Геолокация",
"title_description": "Он используется в заголовке страницы, в теме письма для экспорта RSS- и ICS-каналов.",
"instance_place": "Ориентировочное местонахождение данного экземпляра",
"instance_name_help": "Аккаунт ActivityPub, для подписки",
"enable_trusted_instances": "Включите дружественные экземпляры",
"trusted_instances_label": "Навигационная метка для дружественных экземпляров",
"trusted_instances_label_help": "Метка по умолчанию в 'Дружеств. экземплярах'",
"add_trusted_instance": "Доб. дружественный экземпляр",
"instance_place_help": "Метка для отображения в других экземплярах",
"is_dark": "Темная тема",
"add_link": "Добавить ссылку",
"footer_links": "Ссылки в нижнем колонтитуле",
"edit_place": "Ред. место",
"edit_tag": "Ред. тег",
"new_announcement": "Новое объявление",
"show_smtp_setup": "Настройки электронной почты",
"smtp_hostname": "Имя хоста SMTP",
"smtp_port": "Порт SMTP",
"smtp_test_success": "Тестовое письмо отправлено на {admin_email}, пожалуйста, проверьте свой почтовый ящик",
"smtp_test_button": "Отправьте тестовое электронное письмо",
"smtp_use_sendmail": "Используйте sendmail",
"admin_email": "Электронная почта администратора",
"admin_email_help": "Адрес, который мы используем в качестве отправителя для отправки электронных писем. Это также адрес, на который отправляются электронные письма администраторов",
"widget": "Виджет",
"wrong_domain_warning": "Baseurl, настроенный в config.json <b>({baseurl})</b> отличается от того, который вы посещаете <b>({url})</b>",
"collections_description": "Коллекции - это группировка событий по тегам и местам. Они будут отображаться на главной странице",
"edit_collection": "Ред. коллекцию",
"config_plugin": "Конфигурация плагина",
"header_image": "Изображение заголовка",
"hide_thumbs": "Скрыть превью",
"hide_calendar": "Скрыть календарь",
"default_images": "Изображения по умолчанию",
"geocoding_countrycodes": "Коды стран",
"geocoding_countrycodes_help": "Позволяет установить фильтр для поиска на основе кодов городов",
"geocoding_test_button": "Тест геокодирования",
"geocoding_test_success": "Служба геокодирования на {service_name} работает",
"geocoding_test_error": "Служба геокодирования недоступна по адресу {service_name}",
"tilelayer_provider_attribution": "Атрибуция",
"announcement_description": "В этом разделе вы можете вставлять объявления, которые будут оставаться на главной странице",
"delete_trusted_instance_confirm": "Вы действительно хотите удалить этот пункт из меню дружественных экземпляров?",
"edit_tag_help": "Вы можете изменить тег, заменив его новым или объединив с существующим. Связанные с ним {n} события также будут изменены.",
"instance_timezone_description": "Gancio предназначен для сбора событий определенного места, например, города. Все события в этом месте будут отображаться в выбранном для него часовом поясе.",
"instance_locale_description": "Предпочитаемый язык пользователя для страниц. Иногда сообщения должны отображаться на одном языке для всех (например, при публикации через ActivityPub или при отправке некоторых электронных писем). В этих случаях будет использоваться язык, выбранный выше.",
"trusted_instances_help": "Список дружественных экземпляров будет показан в заголовке",
"smtp_description": "<ul><li>Администратор должен получать электронное письмо при добавлении события анонса (если включено).</li><li>Администратор должен получить электронное письмо с запросом на регистрацию (если включено).</li><li>Пользователь должен получить электронное письмо с запросом на регистрацию.</li><li>Пользователь должен получить электронное письмо с подтверждением регистрации.</li><li>Пользователь должен получить подтверждение по электронной почте при подписке непосредственно администратором.</li><li>Пользователи должны получать электронное письмо для восстановления пароля, если они его забыли</li></ul>",
"hide_boost_bookmark_help": "Скрывает маленькие иконки, показывающие количество бустов и закладок, поступающих из fediverse"
},
"recover": {
"not_valid_code": "Что-то пошло не так."
},
"export": {
"email_description": "Вы можете получать интересующие вас события на электронной почте.",
"insert_your_address": "Введите свой адрес электронной почты",
"ical_description": "Компьютеры и смартфоны обычно оснащены приложением календаря, способным импортировать календарь (ical файл).",
"list_description": "Если у вас есть веб-сайт и вы хотите показать список событий, используйте следующий код",
"intro": "В отличие от несоциальных платформ, которые делают все, чтобы удержать пользователей и данные о них, мы считаем, что информация, как и люди, должна быть свободной. Для этого вы можете оставаться в курсе нужных вам событий, без обязательного посещения этого сайта.",
"feed_description": "Чтобы следить за обновлениями с компьютера или смартфона без необходимости периодически открывать этот сайт, используйте RSS-каналы.\n\n<p> С помощью RSS-каналов вы используете специальное приложение для получения обновлений с интересующих вас сайтов. Это хороший способ быстро следить за многими сайтами, без необходимости создавать учетную запись или других сложностей. </p>\n\n<li> Если у вас Android, мы рекомендуем <a href=\"https://f-droid.org/en/packages/net.frju.flym/\">Flym</a> или Feeder </li>\n<li> Для iPhone / iPad вы можете использовать <a href=\"https://itunes.apple.com/ua/app/feeds4u/id1038456442?mt=8\"> Feed4U </a> </li>\n<li> Для настольных компьютеров / ноутбуков мы рекомендуем Feedbro, который должен быть установлен на <a href=\"https://addons.mozilla.org/en-GB/firefox/addon/feedbroreader/\"> Firefox </a> или <a href=\"https://chrome.google.com/webstore/detail/feedbro/mefgmmbdailogpfhfblcnnjfmnpnmdfa\"> Chrome </a>. </li>\n<br/>\nДобавив эту ссылку в свой RSS-канал, вы всегда будете в курсе последних событий."
},
"register": {
"error": "Ошибка: ",
"complete": "Регистрация должна быть подтверждена.",
"first_user": "Администратор создан",
"description": "Общественные движения должны организовываться и самофинансироваться.<br/>\n<br/>Перед публикацией, <strong> аккаунт должен быть подтвержден</strong>, подумайте о том, что <strong> на этом сайте вы найдете реальных людей</strong>, поэтому напишите две строчки, чтобы сообщить нам, какие события вы хотели бы опубликовать."
},
"setup": {
"copy_password_dialog": "Да, вы можете скопировать пароль!",
"https_warning": "Вы посещаете сайт с HTTP, не забудьте изменить baseurl в config.json, если вы перейдете на HTTPS!",
"start": "Начало",
"completed_description": "<p>Вы можете войти в систему под следующим пользователем:<br/><br/>Логин: <b>{email}</b><br/>Пароль: <b>{password}<b/></p>",
"completed": "Установка завершена"
},
"confirm": {
"title": "Подтверждение пользователя",
"not_valid": "Что-то пошло не так.",
"valid": "Ваша учетная запись подтверждена, теперь вы можете <a href=\"/login\">войти</a>"
},
"settings": {
"password_updated": "Пароль изменен.",
"update_confirm": "Хотите ли вы сохранить свою изменение?",
"change_password": "Изменить пароль",
"danger_section": "Опасная опция",
"remove_account": "При нажатии следующей кнопки ваша учетная запись пользователя будет удалена. Опубликованные вами события не будут удалены.",
"remove_account_confirm": "Вы собираетесь навсегда удалить свой аккаунт"
},
"error": {
"email_taken": "Эта электронная почта уже используется.",
"nick_taken": "Это имя пользователя уже используется."
},
"auth": {
"not_confirmed": "Не подтверждено…",
"fail": "Не удалось войти в систему. Вы уверены, что пароль правильный?"
},
"validators": {
"required": "{fieldName} заполните",
"email": "Доб. действующий адрес электронной почты"
},
"oauth": {
"authorization_request": "Приложение <code>{app}</code> запрашивает следующую авторизацию на <code>{instance_name}</code>:",
"scopes": {
"event:write": "Добавляйте и редактируйте свои события"
},
"redirected_to": "После подтверждения вы будете перенаправлены на <code>{url}</code>"
},
"about": "\n <p><a href='https://gancio.org'>Gancio</a> это афиша для местных сообществ.</p>\n ",
"ordinal": {
"1": "первый",
"2": "второй",
"3": "третий",
"4": "четвертый",
"5": "пятый",
"-1": "последний"
}
}

View file

@ -2,7 +2,7 @@ const config = require('./server/config.js')
const minifyTheme = require('minify-css-string').default const minifyTheme = require('minify-css-string').default
const locales = require('./locales/index') const locales = require('./locales/index')
import { ca, de, en, es, eu, fr, gl, it, nb, pl, pt, sk, zhHans } from 'vuetify/lib/locale' import { ca, de, en, es, eu, fr, gl, it, nb, pl, pt, sk, ru, zhHans } from 'vuetify/lib/locale'
const isDev = (process.env.NODE_ENV !== 'production') const isDev = (process.env.NODE_ENV !== 'production')
module.exports = { module.exports = {
@ -141,7 +141,7 @@ module.exports = {
}, },
buildModules: ['@nuxtjs/vuetify'], buildModules: ['@nuxtjs/vuetify'],
vuetify: { vuetify: {
lang: { locales: { ca, de, en, es, eu, fr, gl, it, nb, pl, pt, sk, zhHans } }, lang: { locales: { ca, de, en, es, eu, fr, gl, it, nb, pl, pt, sk, ru, zhHans } },
treeShake: true, treeShake: true,
theme: { theme: {
options: { options: {

View file

@ -1,6 +1,6 @@
{ {
"name": "gancio", "name": "gancio",
"version": "1.6.0", "version": "1.6.1",
"description": "A shared agenda for local communities", "description": "A shared agenda for local communities",
"author": "lesion", "author": "lesion",
"scripts": { "scripts": {
@ -75,7 +75,7 @@
"passport-oauth2-client-password": "^0.1.2", "passport-oauth2-client-password": "^0.1.2",
"passport-oauth2-client-public": "^0.0.1", "passport-oauth2-client-public": "^0.0.1",
"pg": "^8.8.0", "pg": "^8.8.0",
"sequelize": "^6.27.0", "sequelize": "^6.28.0",
"sequelize-slugify": "^1.6.2", "sequelize-slugify": "^1.6.2",
"sharp": "^0.27.2", "sharp": "^0.27.2",
"sqlite3": "^5.1.4", "sqlite3": "^5.1.4",

View file

@ -179,7 +179,6 @@ export default {
filteredTags() { filteredTags() {
if (!this.tagName) { return this.tags.slice(0, 10).map(t => t.tag) } if (!this.tagName) { return this.tags.slice(0, 10).map(t => t.tag) }
const tagName = this.tagName.trim().toLowerCase() const tagName = this.tagName.trim().toLowerCase()
console.log(tagName)
return this.tags.filter(t => t.tag.toLowerCase().includes(tagName)).map(t => t.tag) return this.tags.filter(t => t.tag.toLowerCase().includes(tagName)).map(t => t.tag)
} }
}, },
@ -245,6 +244,8 @@ export default {
if (this.date.dueHour) { if (this.date.dueHour) {
[hour, minute] = this.date.dueHour.split(':') [hour, minute] = this.date.dueHour.split(':')
formData.append('end_datetime', dayjs(this.date.due).hour(Number(hour)).minute(Number(minute)).second(0).unix()) formData.append('end_datetime', dayjs(this.date.due).hour(Number(hour)).minute(Number(minute)).second(0).unix())
} else if (!!this.date.multidate) {
formData.append('end_datetime', dayjs(this.date.due).hour(24).minute(0).second(0).unix())
} }
if (this.edit) { if (this.edit) {

View file

@ -31,7 +31,7 @@ export default {
async asyncData ({ $axios, params, error }) { async asyncData ({ $axios, params, error }) {
try { try {
const collection = params.collection const collection = params.collection
const events = await $axios.$get(`/collections/${collection}`) const events = await $axios.$get(`/collections/${encodeURIComponent(collection)}`)
return { events, collection } return { events, collection }
} catch (e) { } catch (e) {
console.error(e) console.error(e)

View file

@ -3,7 +3,7 @@ v-container#event.pa-0.pa-sm-2
//- EVENT PAGE //- EVENT PAGE
//- gancio supports microformats (http://microformats.org/wiki/h-event) //- gancio supports microformats (http://microformats.org/wiki/h-event)
//- and microdata https://schema.org/Event //- and microdata https://schema.org/Event
v-card.h-event(itemscope itemtype="https://schema.org/Event") v-card.h-event(itemscope itemtype="https://schema.org/Event" v-touch="{ left: goNext, right: goPrev }")
v-card-text v-card-text
v-row v-row
@ -318,12 +318,22 @@ export default {
keyDown (ev) { keyDown (ev) {
if (ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey) { return } if (ev.altKey || ev.ctrlKey || ev.metaKey || ev.shiftKey) { return }
if (ev.key === 'ArrowRight' && this.event.next) { if (ev.key === 'ArrowRight' && this.event.next) {
this.$router.replace(`/event/${this.event.next}`) this.goNext()
} }
if (ev.key === 'ArrowLeft' && this.event.prev) { if (ev.key === 'ArrowLeft' && this.event.prev) {
this.goPrev()
}
},
goPrev () {
if (this.event.prev) {
this.$router.replace(`/event/${this.event.prev}`) this.$router.replace(`/event/${this.event.prev}`)
} }
}, },
goNext () {
if (this.event.next) {
this.$router.replace(`/event/${this.event.next}`)
}
},
showResource (resource) { showResource (resource) {
this.showResources = true this.showResources = true
this.selectedResource = resource this.selectedResource = resource

View file

@ -1,6 +1,5 @@
<template lang="pug"> <template lang="pug">
v-container.px-2.px-sm-6.pt-0 v-container.px-2.px-sm-6.pt-0
//- Announcements //- Announcements
#announcements.mt-2.mt-sm-4(v-if='announcements.length') #announcements.mt-2.mt-sm-4(v-if='announcements.length')
Announcement(v-for='announcement in announcements' :key='`a_${announcement.id}`' :announcement='announcement') Announcement(v-for='announcement in announcements' :key='`a_${announcement.id}`' :announcement='announcement')
@ -41,7 +40,8 @@ export default {
searching: false, searching: false,
tmpEvents: [], tmpEvents: [],
selectedDay: null, selectedDay: null,
show_recurrent: $store.state.settings.recurrent_event_visible, storeUnsubscribe: null
} }
}, },
head () { head () {
@ -63,53 +63,60 @@ export default {
} }
}, },
computed: { computed: {
...mapState(['settings', 'announcements', 'events']), ...mapState(['settings', 'announcements', 'events', 'filter']),
visibleEvents () { visibleEvents () {
if (this.searching) { if (this.filter.query && this.filter.query.length > 2) {
return this.tmpEvents return this.tmpEvents
} }
const now = dayjs().unix() const now = dayjs().unix()
if (this.selectedDay) { if (this.selectedDay) {
const min = dayjs.tz(this.selectedDay).startOf('day').unix() const min = dayjs.tz(this.selectedDay).startOf('day').unix()
const max = dayjs.tz(this.selectedDay).endOf('day').unix() const max = dayjs.tz(this.selectedDay).endOf('day').unix()
return this.events.filter(e => (e.start_datetime <= max && (e.end_datetime || e.start_datetime) >= min) && (this.show_recurrent || !e.parentId)) return this.events.filter(e => (e.start_datetime <= max && (e.end_datetime || e.start_datetime) >= min) && (this.filter.show_recurrent || !e.parentId))
} else if (this.isCurrentMonth) { } else if (this.isCurrentMonth) {
return this.events.filter(e => ((e.end_datetime ? e.end_datetime > now : e.start_datetime + 3 * 60 * 60 > now) && (this.show_recurrent || !e.parentId))) return this.events.filter(e => ((e.end_datetime ? e.end_datetime > now : e.start_datetime + 3 * 60 * 60 > now) && (this.filter.show_recurrent || !e.parentId)))
} else { } else {
return this.events.filter(e => this.show_recurrent || !e.parentId) return this.events.filter(e => this.filter.show_recurrent || !e.parentId)
} }
} }
}, },
created () { mounted () {
this.$root.$on('dayclick', this.dayChange) this.$root.$on('dayclick', this.dayChange)
this.$root.$on('monthchange', this.monthChange) this.$root.$on('monthchange', this.monthChange)
this.$root.$on('search', debounce(this.search, 100)) this.storeUnsubscribe = this.$store.subscribeAction( { after: (action, state) => {
if (action.type === 'setFilter') {
if (this.filter.query && this.filter.query.length > 2) {
this.search()
} else {
this.updateEvents()
}
}
}})
}, },
destroyed () { destroyed () {
this.$root.$off('dayclick') this.$root.$off('dayclick')
this.$root.$off('monthchange') this.$root.$off('monthchange')
this.$root.$off('search') if (typeof this.storeUnsubscribe === 'function') {
this.storeUnsubscribe()
}
}, },
methods: { methods: {
...mapActions(['getEvents']), ...mapActions(['getEvents']),
async search (query) { search: debounce(async function() {
if (query) { this.tmpEvents = await this.$api.getEvents({
this.tmpEvents = await this.$axios.$get(`/event/search?search=${query}`) start: 0,
this.searching = true show_recurrent: this.filter.show_recurrent,
} else { show_multidate: this.filter.show_multidate,
this.tmpEvents = null query: this.filter.query
this.searching = false })
} }, 100),
},
updateEvents () { updateEvents () {
return this.getEvents({ return this.getEvents({
start: this.start, start: this.start,
end: this.end, end: this.end
show_recurrent: true
}) })
}, },
async monthChange ({ year, month }) { async monthChange ({ year, month }) {
this.$nuxt.$loading.start() this.$nuxt.$loading.start()
this.$nextTick( async () => { this.$nextTick( async () => {

View file

@ -18,12 +18,15 @@ export default ({ $axios }, inject) => {
try { try {
const events = await $axios.$get('/events', { const events = await $axios.$get('/events', {
params: { params: {
start: params.start, ...params,
end: params.end, // start: params.start,
// end: params.end,
places: params.places && params.places.join(','), places: params.places && params.places.join(','),
tags: params.tags && params.tags.join(','), tags: params.tags && params.tags.join(','),
show_recurrent: !!params.show_recurrent, // ...(params.show_recurrent !== && {show_recurrent: !!params.show_recurrent}),
max: params.maxs // show_multidate: !!params.show_multidate,
// query: params.query,
// max: params.maxs
} }
}) })
return events.map(e => Object.freeze(e)) return events.map(e => Object.freeze(e))

View file

@ -16,6 +16,7 @@ import 'dayjs/locale/fr'
import 'dayjs/locale/de' import 'dayjs/locale/de'
import 'dayjs/locale/gl' import 'dayjs/locale/gl'
import 'dayjs/locale/sk' import 'dayjs/locale/sk'
import 'dayjs/locale/ru'
import 'dayjs/locale/pt' import 'dayjs/locale/pt'
import 'dayjs/locale/zh' import 'dayjs/locale/zh'

View file

@ -4,7 +4,7 @@ rm -fr node_modules
yarn yarn
yarn build yarn build
yarn pack yarn pack
# yarn publish yarn publish
gpg --pinentry-mode loopback --passphrase `pass underscore/pgp` --detach-sign --local-user 5DAC477D5441B7A15ACBF680BBEB4DD39AC6CCA9 gancio-$RELEASE.tgz gpg --pinentry-mode loopback --passphrase `pass underscore/pgp` --detach-sign --local-user 5DAC477D5441B7A15ACBF680BBEB4DD39AC6CCA9 gancio-$RELEASE.tgz
cp gancio-$RELEASE.tgz releases/ cp gancio-$RELEASE.tgz releases/
mv gancio-$RELEASE.tgz releases/latest.tgz mv gancio-$RELEASE.tgz releases/latest.tgz

View file

@ -1,4 +1,5 @@
const Announcement = require('../models/announcement') const { Announcement } = require('../models/models')
const log = require('../../log') const log = require('../../log')
const announceController = { const announceController = {

View file

@ -1,4 +1,4 @@
const APUser = require('../models/ap_user') const { APUser } = require('../models/models')
const apUserController = { const apUserController = {
async toggleBlock (req, res) { async toggleBlock (req, res) {

View file

@ -1,8 +1,5 @@
const Collection = require('../models/collection') const { Collection, Filter, Event, Tag, Place } = require('../models/models')
const Filter = require('../models/filter')
const Event = require('../models/event')
const Tag = require('../models/tag')
const Place = require('../models/place')
const log = require('../../log') const log = require('../../log')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const { col: Col } = require('../../helpers') const { col: Col } = require('../../helpers')
@ -114,7 +111,7 @@ const collectionController = {
res.json(collection) res.json(collection)
} catch (e) { } catch (e) {
log.error(`Create collection failed ${e}`) log.error(`Create collection failed ${e}`)
res.sendStatus(400) res.status(400).send(e)
} }
}, },
@ -138,15 +135,14 @@ const collectionController = {
}, },
async addFilter (req, res) { async addFilter (req, res) {
const collectionId = req.body.collectionId const { collectionId, tags, places } = req.body
const tags = req.body.tags
const places = req.body.places
try { try {
const filter = await Filter.create({ collectionId, tags, places }) filter = await Filter.create({ collectionId, tags, places })
return res.json(filter) return res.json(filter)
} catch (e) { } catch (e) {
log.error(String(e)) log.error(String(e))
return res.status(500) return res.sendStatus(400)
} }
}, },
@ -170,6 +166,4 @@ const collectionController = {
} }
module.exports = collectionController module.exports = collectionController

View file

@ -3,18 +3,15 @@ const path = require('path')
const config = require('../../config') const config = require('../../config')
const fs = require('fs') const fs = require('fs')
const { Op } = require('sequelize') const { Op } = require('sequelize')
const intersection = require('lodash/intersection')
const linkifyHtml = require('linkify-html') const linkifyHtml = require('linkify-html')
const Sequelize = require('sequelize') const Sequelize = require('sequelize')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const helpers = require('../../helpers') const helpers = require('../../helpers')
const Col = helpers.col const Col = helpers.col
const Event = require('../models/event') const notifier = require('../../notifier')
const Resource = require('../models/resource')
const Tag = require('../models/tag') const { Event, Resource, Tag, Place, Notification, APUser } = require('../models/models')
const Place = require('../models/place')
const Notification = require('../models/notification')
const APUser = require('../models/ap_user')
const exportController = require('./export') const exportController = require('./export')
const tagController = require('./tag') const tagController = require('./tag')
@ -89,99 +86,71 @@ const eventController = {
}, },
async search(req, res) { // async search(req, res) {
const search = req.query.search.trim().toLocaleLowerCase() // const search = req.query.search.trim().toLocaleLowerCase()
const show_recurrent = req.query.show_recurrent || false // const show_recurrent = req.query.show_recurrent || false
const end = req.query.end // const end = req.query.end
const replacements = [] // const replacements = []
const where = { // const where = {
// do not include parent recurrent event // // do not include parent recurrent event
recurrent: null, // recurrent: null,
// confirmed event only // // confirmed event only
is_visible: true, // is_visible: true,
} // }
if (!show_recurrent) { // if (!show_recurrent) {
where.parentId = null // where.parentId = null
} // }
if (end) { // if (end) {
where.start_datetime = { [Op.lte]: end } // where.start_datetime = { [Op.lte]: end }
} // }
if (search) { // if (search) {
replacements.push(search) // replacements.push(search)
where[Op.or] = // where[Op.or] =
[ // [
{ title: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('title')), 'LIKE', '%' + search + '%') }, // { title: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('title')), 'LIKE', '%' + search + '%') },
Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), 'LIKE', '%' + search + '%'), // Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), 'LIKE', '%' + search + '%'),
Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=${Col('event.id')} AND LOWER(${Col('tagTag')}) = ?`)) // Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=${Col('event.id')} AND LOWER(${Col('tagTag')}) = ?`))
] // ]
} // }
const events = await Event.findAll({ // const events = await Event.findAll({
where, // where,
attributes: { // attributes: {
exclude: ['likes', 'boost', 'userId', 'is_visible', 'createdAt', 'updatedAt', 'description', 'resources'] // exclude: ['likes', 'boost', 'userId', 'is_visible', 'createdAt', 'updatedAt', 'description', 'resources']
}, // },
order: [['start_datetime', 'DESC']], // order: [['start_datetime', 'DESC']],
include: [ // include: [
{ // {
model: Tag, // model: Tag,
// order: [Sequelize.literal('(SELECT COUNT("tagTag") FROM event_tags WHERE tagTag = tag) DESC')], // // order: [Sequelize.literal('(SELECT COUNT("tagTag") FROM event_tags WHERE tagTag = tag) DESC')],
attributes: ['tag'], // attributes: ['tag'],
through: { attributes: [] } // through: { attributes: [] }
}, // },
{ model: Place, required: true, attributes: ['id', 'name', 'address', 'latitude', 'longitude'] } // { model: Place, required: true, attributes: ['id', 'name', 'address', 'latitude', 'longitude'] }
], // ],
replacements, // replacements,
limit: 30, // limit: 30,
}).catch(e => { // }).catch(e => {
log.error('[EVENT]', e) // log.error('[EVENT]', e)
return res.json([]) // return res.json([])
}) // })
const ret = events.map(e => { // const ret = events.map(e => {
e = e.get() // e = e.get()
e.tags = e.tags ? e.tags.map(t => t && t.tag) : [] // e.tags = e.tags ? e.tags.map(t => t && t.tag) : []
return e // return e
}) // })
return res.json(ret) // return res.json(ret)
}, // },
async getNotifications(event, action) {
log.debug(`getNotifications ${event.title} ${action}`)
function match(event, filters) {
// matches if no filter specified
if (!filters) { return true }
// check for visibility
if (typeof filters.is_visible !== 'undefined' && filters.is_visible !== event.is_visible) { return false }
if (!filters.tags && !filters.places) { return true }
if (!filters.tags.length && !filters.places.length) { return true }
if (filters.tags.length) {
const m = intersection(event.tags.map(t => t.tag), filters.tags)
if (m.length > 0) { return true }
}
if (filters.places.length) {
if (filters.places.find(p => p === event.place.name)) {
return true
}
}
}
const notifications = await Notification.findAll({ where: { action }, include: [Event] })
// get notification that matches with selected event
return notifications.filter(notification => match(event, notification.filters))
},
async _get(slug) { async _get(slug) {
// retrocompatibility, old events URL does not use slug, use id as fallback // retrocompatibility, old events URL does not use slug, use id as fallback
@ -317,7 +286,6 @@ const eventController = {
res.sendStatus(200) res.sendStatus(200)
// send notification // send notification
const notifier = require('../../notifier')
notifier.notifyEvent('Create', event.id) notifier.notifyEvent('Create', event.id)
} catch (e) { } catch (e) {
log.error('[EVENT]', e) log.error('[EVENT]', e)
@ -631,9 +599,11 @@ const eventController = {
async _select({ async _select({
start = dayjs().unix(), start = dayjs().unix(),
end, end,
query,
tags, tags,
places, places,
show_recurrent, show_recurrent,
show_multidate,
limit, limit,
page, page,
older }) { older }) {
@ -656,6 +626,10 @@ const eventController = {
where.parentId = null where.parentId = null
} }
if (!show_multidate) {
where.multidate = { [Op.not]: true }
}
if (end) { if (end) {
where.start_datetime = { [older ? Op.gte : Op.lte]: end } where.start_datetime = { [older ? Op.gte : Op.lte]: end }
} }
@ -679,6 +653,16 @@ const eventController = {
where.placeId = places.split(',') where.placeId = places.split(',')
} }
if (query) {
replacements.push(query)
where[Op.or] =
[
{ title: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('title')), 'LIKE', '%' + query + '%') },
Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), 'LIKE', '%' + query + '%'),
Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=${Col('event.id')} AND LOWER(${Col('tagTag')}) = ?`))
]
}
let pagination = {} let pagination = {}
if (limit) { if (limit) {
pagination = { pagination = {
@ -723,17 +707,21 @@ const eventController = {
const settings = res.locals.settings const settings = res.locals.settings
const start = req.query.start || dayjs().unix() const start = req.query.start || dayjs().unix()
const end = req.query.end const end = req.query.end
const query = req.query.query
const tags = req.query.tags const tags = req.query.tags
const places = req.query.places const places = req.query.places
const limit = Number(req.query.max) || 0 const limit = Number(req.query.max) || 0
const page = Number(req.query.page) || 0 const page = Number(req.query.page) || 0
const older = req.query.older || false const older = req.query.older || false
const show_multidate = settings.allow_multidate_event &&
typeof req.query.show_multidate !== 'undefined' ? req.query.show_multidate !== 'false' : true
const show_recurrent = settings.allow_recurrent_event && const show_recurrent = settings.allow_recurrent_event &&
typeof req.query.show_recurrent !== 'undefined' ? req.query.show_recurrent === 'true' : settings.recurrent_event_visible typeof req.query.show_recurrent !== 'undefined' ? req.query.show_recurrent === 'true' : settings.recurrent_event_visible
res.json(await eventController._select({ res.json(await eventController._select({
start, end, places, tags, show_recurrent, limit, page, older start, end, query, places, tags, show_recurrent, show_multidate, limit, page, older
})) }))
}, },

View file

@ -1,6 +1,4 @@
const Event = require('../models/event') const { Event, Place, Tag } = require('../models/models')
const Place = require('../models/place')
const Tag = require('../models/tag')
const { htmlToText } = require('html-to-text') const { htmlToText } = require('html-to-text')
const { Op, literal } = require('sequelize') const { Op, literal } = require('sequelize')

View file

@ -1,6 +1,5 @@
const APUser = require('../models/ap_user') const { APUser, Instance, Resource } = require('../models/models')
const Instance = require('../models/instance')
const Resource = require('../models/resource')
const Sequelize = require('sequelize') const Sequelize = require('sequelize')
const instancesController = { const instancesController = {

View file

@ -1,4 +1,4 @@
const User = require('../models/user') const User = require('../models/modles')
const metrics = { const metrics = {

View file

@ -2,12 +2,9 @@ const bodyParser = require('body-parser')
const cookieParser = require('cookie-parser') const cookieParser = require('cookie-parser')
const session = require('cookie-session') const session = require('cookie-session')
const OAuthClient = require('../models/oauth_client') const { OAuthClient, OAuthToken, OAuthCode, User } = require('../models/models')
const OAuthToken = require('../models/oauth_token')
const OAuthCode = require('../models/oauth_code')
const helpers = require('../../helpers.js') const helpers = require('../../helpers.js')
const User = require('../models/user')
const passport = require('passport') const passport = require('passport')
const get = require('lodash/get') const get = require('lodash/get')

View file

@ -1,5 +1,5 @@
const Place = require('../models/place') const { Place, Event } = require('../models/models')
const Event = require('../models/event')
const eventController = require('./event') const eventController = require('./event')
const exportController = require('./export') const exportController = require('./export')

View file

@ -2,11 +2,12 @@ const path = require('path')
const fs = require('fs') const fs = require('fs')
const log = require('../../log') const log = require('../../log')
const config = require('../../config') const config = require('../../config')
const settingsController = require('./settings')
const notifier = require('../../notifier')
const pluginController = { const pluginController = {
plugins: [], plugins: [],
getAll(_req, res) { getAll(_req, res) {
const settingsController = require('./settings')
// return plugins and inner settings // return plugins and inner settings
const plugins = pluginController.plugins.map( ({ configuration }) => { const plugins = pluginController.plugins.map( ({ configuration }) => {
if (settingsController.settings['plugin_' + configuration.name]) { if (settingsController.settings['plugin_' + configuration.name]) {
@ -18,7 +19,6 @@ const pluginController = {
}, },
togglePlugin(req, res) { togglePlugin(req, res) {
const settingsController = require('./settings')
const pluginName = req.params.plugin const pluginName = req.params.plugin
const pluginSettings = settingsController.settings['plugin_' + pluginName] const pluginSettings = settingsController.settings['plugin_' + pluginName]
if (!pluginSettings) { return res.sendStatus(404) } if (!pluginSettings) { return res.sendStatus(404) }
@ -33,7 +33,6 @@ const pluginController = {
}, },
unloadPlugin(pluginName) { unloadPlugin(pluginName) {
const settingsController = require('./settings')
const plugin = pluginController.plugins.find(p => p.configuration.name === pluginName) const plugin = pluginController.plugins.find(p => p.configuration.name === pluginName)
const settings = settingsController.settings['plugin_' + pluginName] const settings = settingsController.settings['plugin_' + pluginName]
if (!plugin) { if (!plugin) {
@ -59,14 +58,12 @@ const pluginController = {
}, },
loadPlugin(pluginName) { loadPlugin(pluginName) {
const settingsController = require('./settings')
const plugin = pluginController.plugins.find(p => p.configuration.name === pluginName) const plugin = pluginController.plugins.find(p => p.configuration.name === pluginName)
const settings = settingsController.settings['plugin_' + pluginName] const settings = settingsController.settings['plugin_' + pluginName]
if (!plugin) { if (!plugin) {
log.warn(`Plugin ${pluginName} not found`) log.warn(`Plugin ${pluginName} not found`)
return return
} }
const notifier = require('../../notifier')
log.info('Load plugin ' + pluginName) log.info('Load plugin ' + pluginName)
if (typeof plugin.onEventCreate === 'function') { if (typeof plugin.onEventCreate === 'function') {
notifier.emitter.on('Create', plugin.onEventCreate) notifier.emitter.on('Create', plugin.onEventCreate)
@ -88,7 +85,6 @@ const pluginController = {
}, },
_load() { _load() {
const settingsController = require('./settings')
// load custom plugins // load custom plugins
const plugins_path = config.plugins_path || path.resolve(process.env.cwd || '', 'plugins') const plugins_path = config.plugins_path || path.resolve(process.env.cwd || '', 'plugins')
log.info(`Loading plugin ${plugins_path}`) log.info(`Loading plugin ${plugins_path}`)

View file

@ -1,6 +1,4 @@
const Resource = require('../models/resource') const { Resource, APUser, Event } = require('../models/models')
const APUser = require('../models/ap_user')
const Event = require('../models/event')
const get = require('lodash/get') const get = require('lodash/get')
const resourceController = { const resourceController = {

View file

@ -1,6 +1,5 @@
const path = require('path') const path = require('path')
const URL = require('url') const URL = require('url')
const fs = require('fs')
const crypto = require('crypto') const crypto = require('crypto')
const { promisify } = require('util') const { promisify } = require('util')
const sharp = require('sharp') const sharp = require('sharp')
@ -9,7 +8,7 @@ const generateKeyPair = promisify(crypto.generateKeyPair)
const log = require('../../log') const log = require('../../log')
// const locales = require('../../../locales/index') // const locales = require('../../../locales/index')
const escape = require('lodash/escape') const escape = require('lodash/escape')
const pluginController = require('./plugins') const DB = require('../models/models')
let defaultHostname let defaultHostname
try { try {
@ -30,7 +29,7 @@ const defaultSettings = {
allow_multidate_event: true, allow_multidate_event: true,
allow_recurrent_event: false, allow_recurrent_event: false,
recurrent_event_visible: false, recurrent_event_visible: false,
allow_geolocation: true, allow_geolocation: false,
geocoding_provider_type: 'Nominatim', geocoding_provider_type: 'Nominatim',
geocoding_provider: 'https://nominatim.openstreetmap.org/search', geocoding_provider: 'https://nominatim.openstreetmap.org/search',
geocoding_countrycodes: [], geocoding_countrycodes: [],
@ -74,8 +73,7 @@ const settingsController = {
// initialize instance settings from db // initialize instance settings from db
// note that this is done only once when the server starts // note that this is done only once when the server starts
// and not for each request // and not for each request
const Setting = require('../models/setting') const settings = await DB.Setting.findAll()
const settings = await Setting.findAll()
settingsController.settings = defaultSettings settingsController.settings = defaultSettings
settings.forEach(s => { settings.forEach(s => {
if (s.is_secret) { if (s.is_secret) {
@ -117,15 +115,14 @@ const settingsController = {
// } // }
// }) // })
// } // }
const pluginController = require('./plugins')
pluginController._load() pluginController._load()
}, },
async set (key, value, is_secret = false) { async set (key, value, is_secret = false) {
const Setting = require('../models/setting')
log.info(`SET ${key} ${is_secret ? '*****' : value}`) log.info(`SET ${key} ${is_secret ? '*****' : value}`)
try { try {
const [setting, created] = await Setting.findOrCreate({ const [setting, created] = await DB.Setting.findOrCreate({
where: { key }, where: { key },
defaults: { value, is_secret } defaults: { value, is_secret }
}) })

View file

@ -7,6 +7,8 @@ const settingsController = require('./settings')
const path = require('path') const path = require('path')
const escape = require('lodash/escape') const escape = require('lodash/escape')
const DB = require('../models/models')
const setupController = { const setupController = {
async _setupDb (dbConf) { async _setupDb (dbConf) {
@ -23,7 +25,10 @@ const setupController = {
// try to connect // try to connect
dbConf.logging = false dbConf.logging = false
await db.connect(dbConf) db.connect(dbConf)
db.loadModels()
db.associates()
await db.sequelize.authenticate()
// is empty ? // is empty ?
const isEmpty = await db.isEmpty() const isEmpty = await db.isEmpty()
@ -69,8 +74,7 @@ const setupController = {
// create admin // create admin
const password = helpers.randomString() const password = helpers.randomString()
const email = `admin` const email = `admin`
const User = require('../models/user') await DB.User.create({
await User.create({
email, email,
password, password,
is_admin: true, is_admin: true,

View file

@ -1,5 +1,4 @@
const Tag = require('../models/tag') const { Tag, Event } = require('../models/models')
const Event = require('../models/event')
const uniq = require('lodash/uniq') const uniq = require('lodash/uniq')
const log = require('../../log') const log = require('../../log')
@ -82,20 +81,22 @@ module.exports = {
return res.json(tags.map(t => t.tag)) return res.json(tags.map(t => t.tag))
}, },
async updateTag (req, res) { // async updateTag (req, res) {
const tag = await Tag.findByPk(req.body.tag) // const tag = await Tag.findByPk(req.body.tag)
await tag.update(req.body) // await tag.update(req.body)
res.json(place) // res.json(place)
}, // },
async updateTag (req, res) { async updateTag (req, res) {
try {
const oldtag = await Tag.findByPk(req.body.tag) const oldtag = await Tag.findByPk(req.body.tag)
const newtag = await Tag.findByPk(req.body.newTag) const newtag = await Tag.findByPk(req.body.newTag)
// if the new tag does not exists, just rename the old one // if the new tag does not exists, just rename the old one
if (!newtag) { if (!newtag) {
oldtag.tag = req.body.newTag log.info(`Rename tag ${oldtag.tag} to ${req.body.newTag}`)
await oldtag.update({ tag: req.body.newTag }) await Tag.update({ tag: req.body.newTag }, { where: { tag: req.body.tag }, raw: true })
} else { } else {
// in case it exists: // in case it exists:
// - search for events with old tag // - search for events with old tag
@ -105,6 +106,10 @@ module.exports = {
await newtag.addEvents(events) await newtag.addEvents(events)
} }
res.sendStatus(200) res.sendStatus(200)
} catch (e) {
console.error(e)
res.sendStatus(400)
}
}, },
async remove (req, res) { async remove (req, res) {

View file

@ -2,7 +2,7 @@ const crypto = require('crypto')
const { Op } = require('sequelize') const { Op } = require('sequelize')
const config = require('../../config') const config = require('../../config')
const mail = require('../mail') const mail = require('../mail')
const User = require('../models/user') const { User } = require('../models/models')
const settingsController = require('./settings') const settingsController = require('./settings')
const log = require('../../log') const log = require('../../log')
const linkify = require('linkifyjs') const linkify = require('linkifyjs')

View file

@ -5,38 +5,41 @@ const cors = require('cors')()
const config = require('../config') const config = require('../config')
const log = require('../log') const log = require('../log')
const api = express.Router() const collectionController = require('./controller/collection')
api.use(express.urlencoded({ extended: false })) const setupController = require('./controller/setup')
api.use(express.json()) const settingsController = require('./controller/settings')
const eventController = require('./controller/event')
const placeController = require('./controller/place')
const tagController = require('./controller/tag')
const exportController = require('./controller/export')
const userController = require('./controller/user')
const instanceController = require('./controller/instance')
const apUserController = require('./controller/ap_user')
const resourceController = require('./controller/resource')
const oauthController = require('./controller/oauth')
const announceController = require('./controller/announce')
const pluginController = require('./controller/plugins')
const helpers = require('../helpers')
const storage = require('./storage')
if (config.status !== 'READY') { module.exports = () => {
const api = express.Router()
api.use(express.urlencoded({ extended: false }))
api.use(express.json())
if (config.status !== 'READY') {
const setupController = require('./controller/setup')
const settingsController = require('./controller/settings')
api.post('/settings', settingsController.setRequest) api.post('/settings', settingsController.setRequest)
api.post('/setup/db', setupController.setupDb) api.post('/setup/db', setupController.setupDb)
api.post('/setup/restart', setupController.restart) api.post('/setup/restart', setupController.restart)
api.post('/settings/smtp', settingsController.testSMTP) api.post('/settings/smtp', settingsController.testSMTP)
} else { } else {
const { isAuth, isAdmin } = require('./auth') const { isAuth, isAdmin } = require('./auth')
const eventController = require('./controller/event')
const placeController = require('./controller/place')
const tagController = require('./controller/tag')
const settingsController = require('./controller/settings')
const exportController = require('./controller/export')
const userController = require('./controller/user')
const instanceController = require('./controller/instance')
const apUserController = require('./controller/ap_user')
const resourceController = require('./controller/resource')
const oauthController = require('./controller/oauth')
const announceController = require('./controller/announce')
const collectionController = require('./controller/collection')
const pluginController = require('./controller/plugins')
const helpers = require('../helpers')
const storage = require('./storage')
const upload = multer({ storage }) const upload = multer({ storage })
/** /**
@ -88,6 +91,7 @@ if (config.status !== 'READY') {
* @type GET * @type GET
* @param {integer} [start] - start timestamp (default: now) * @param {integer} [start] - start timestamp (default: now)
* @param {integer} [end] - end timestamp (optional) * @param {integer} [end] - end timestamp (optional)
* @param {string} [query] - search for this string
* @param {array} [tags] - List of tags * @param {array} [tags] - List of tags
* @param {array} [places] - List of places id * @param {array} [places] - List of places id
* @param {integer} [max] - Limit events * @param {integer} [max] - Limit events
@ -125,7 +129,7 @@ if (config.status !== 'READY') {
// allow anyone to add an event (anon event has to be confirmed, TODO: flood protection) // allow anyone to add an event (anon event has to be confirmed, TODO: flood protection)
api.post('/event', eventController.isAnonEventAllowed, upload.single('image'), eventController.add) api.post('/event', eventController.isAnonEventAllowed, upload.single('image'), eventController.add)
api.get('/event/search', eventController.search) // api.get('/event/search', eventController.search)
api.put('/event', isAuth, upload.single('image'), eventController.update) api.put('/event', isAuth, upload.single('image'), eventController.update)
api.get('/event/import', isAuth, helpers.importURL) api.get('/event/import', isAuth, helpers.importURL)
@ -210,14 +214,15 @@ if (config.status !== 'READY') {
api.get('/clients', isAuth, oauthController.getClients) api.get('/clients', isAuth, oauthController.getClients)
api.get('/client/:client_id', isAuth, oauthController.getClient) api.get('/client/:client_id', isAuth, oauthController.getClient)
api.post('/client', oauthController.createClient) api.post('/client', oauthController.createClient)
} }
api.use((_req, res) => res.sendStatus(404)) api.use((_req, res) => res.sendStatus(404))
// Handle 500 // Handle 500
api.use((error, _req, res, _next) => { api.use((error, _req, res, _next) => {
log.error('[API ERROR]', error) log.error('[API ERROR]', error)
res.status(500).send('500: Internal Server Error') res.status(500).send('500: Internal Server Error')
}) })
module.exports = api return api
}

View file

@ -1,12 +1,6 @@
const sequelize = require('./index').sequelize module.exports = (sequelize, DataTypes) =>
const { Model, DataTypes } = require('sequelize') sequelize.define('announcement', {
class Announcement extends Model {}
Announcement.init({
title: DataTypes.STRING, title: DataTypes.STRING,
announcement: DataTypes.STRING, announcement: DataTypes.STRING,
visible: DataTypes.BOOLEAN visible: DataTypes.BOOLEAN
}, { sequelize, modelName: 'announcement' }) })
module.exports = Announcement

View file

@ -1,9 +1,6 @@
const sequelize = require('./index').sequelize
const { Model, DataTypes } = require('sequelize')
class APUser extends Model {} module.exports = (sequelize, DataTypes) =>
sequelize.define('ap_user', {
APUser.init({
ap_id: { ap_id: {
type: DataTypes.STRING, type: DataTypes.STRING,
primaryKey: true primaryKey: true
@ -11,6 +8,4 @@ APUser.init({
follower: DataTypes.BOOLEAN, follower: DataTypes.BOOLEAN,
blocked: DataTypes.BOOLEAN, blocked: DataTypes.BOOLEAN,
object: DataTypes.JSON object: DataTypes.JSON
}, { sequelize, modelName: 'ap_user' }) })
module.exports = APUser

View file

@ -1,10 +1,5 @@
const { Model, DataTypes } = require('sequelize') module.exports = (sequelize, DataTypes) =>
const sequelize = require('./index').sequelize sequelize.define('collection', {
class Collection extends Model {}
// TODO: slugify!
Collection.init({
id: { id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
autoIncrement: true, autoIncrement: true,
@ -22,7 +17,4 @@ Collection.init({
isTop: { isTop: {
type: DataTypes.BOOLEAN type: DataTypes.BOOLEAN
} }
}, { sequelize, modelName: 'collection', timestamps: false }) }, { timestamps: false })
module.exports = Collection

View file

@ -1,18 +1,5 @@
const config = require('../../config') const config = require('../../config')
const { htmlToText } = require('html-to-text') const { htmlToText } = require('html-to-text')
const { Model, DataTypes } = require('sequelize')
const SequelizeSlugify = require('sequelize-slugify')
const sequelize = require('./index').sequelize
const Resource = require('./resource')
const Notification = require('./notification')
const EventNotification = require('./eventnotification')
const Place = require('./place')
const User = require('./user')
const Tag = require('./tag')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
const utc = require('dayjs/plugin/utc') const utc = require('dayjs/plugin/utc')
@ -20,9 +7,9 @@ const utc = require('dayjs/plugin/utc')
dayjs.extend(utc) dayjs.extend(utc)
dayjs.extend(timezone) dayjs.extend(timezone)
class Event extends Model {} // class Event extends Model {}
module.exports = (sequelize, DataTypes) => {
Event.init({ const Event = sequelize.define('event', {
id: { id: {
allowNull: false, allowNull: false,
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
@ -51,29 +38,9 @@ Event.init({
recurrent: DataTypes.JSON, recurrent: DataTypes.JSON,
likes: { type: DataTypes.JSON, defaultValue: [] }, likes: { type: DataTypes.JSON, defaultValue: [] },
boost: { type: DataTypes.JSON, defaultValue: [] } boost: { type: DataTypes.JSON, defaultValue: [] }
}, { sequelize, modelName: 'event' }) })
Event.belongsTo(Place) Event.prototype.toAP = function (username, locale, to = []) {
Place.hasMany(Event)
Event.belongsTo(User)
User.hasMany(Event)
Event.belongsToMany(Tag, { through: 'event_tags' })
Tag.belongsToMany(Event, { through: 'event_tags' })
Event.belongsToMany(Notification, { through: EventNotification })
Notification.belongsToMany(Event, { through: EventNotification })
Event.hasMany(Resource)
Resource.belongsTo(Event)
Event.hasMany(Event, { as: 'child', foreignKey: 'parentId' })
Event.belongsTo(Event, { as: 'parent' })
SequelizeSlugify.slugifyModel(Event, { source: ['title'], overwrite: false })
Event.prototype.toAP = function (username, locale, to = []) {
const tags = this.tags && this.tags.map(t => t.tag.replace(/[ #]/g, '_')) const tags = this.tags && this.tags.map(t => t.tag.replace(/[ #]/g, '_'))
const plainDescription = htmlToText(this.description && this.description.replace('\n', '').slice(0, 1000)) const plainDescription = htmlToText(this.description && this.description.replace('\n', '').slice(0, 1000))
const content = ` const content = `
@ -122,6 +89,6 @@ Event.prototype.toAP = function (username, locale, to = []) {
content, content,
summary: content summary: content
} }
}
return Event
} }
module.exports = Event

View file

@ -1,15 +1,9 @@
const sequelize = require('./index').sequelize module.exports = (sequelize, DataTypes) =>
const { Model, DataTypes } = require('sequelize') sequelize.define('event_notification', {
class EventNotification extends Model {}
EventNotification.init({
status: { status: {
type: DataTypes.ENUM, type: DataTypes.ENUM,
values: ['new', 'sent', 'error', 'sending'], values: ['new', 'sent', 'error', 'sending'],
defaultValue: 'new', defaultValue: 'new',
index: true index: true
} }
}, { sequelize, modelName: 'event_notification' }) })
module.exports = EventNotification

View file

@ -1,10 +1,6 @@
const { Model, DataTypes } = require('sequelize') module.exports = (sequelize, DataTypes) =>
const Collection = require('./collection') sequelize.define('filter',
const sequelize = require('./index').sequelize {
class Filter extends Model {}
Filter.init({
id: { id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
primaryKey: true, primaryKey: true,
@ -16,9 +12,9 @@ Filter.init({
places: { places: {
type: DataTypes.JSON, type: DataTypes.JSON,
} }
}, { sequelize, modelName: 'filter', timestamps: false }) }, {
indexes: [
Filter.belongsTo(Collection) { fields: ['collectionId', 'tags', 'places'], unique: true }
Collection.hasMany(Filter) ],
timestamps: false
module.exports = Filter })

View file

@ -4,10 +4,77 @@ const Umzug = require('umzug')
const path = require('path') const path = require('path')
const config = require('../../config') const config = require('../../config')
const log = require('../../log') const log = require('../../log')
const settingsController = require('../controller/settings') const SequelizeSlugify = require('sequelize-slugify')
const DB = require('./models')
const models = {
Announcement: require('./announcement'),
APUser: require('./ap_user'),
Collection: require('./collection'),
Event: require('./event'),
EventNotification: require('./eventnotification'),
Filter: require('./filter'),
Instance: require('./instance'),
Notification: require('./notification'),
OAuthClient: require('./oauth_client'),
OAuthCode: require('./oauth_code'),
OAuthToken: require('./oauth_token'),
Place: require('./place'),
Resource: require('./resource'),
Setting: require('./setting'),
Tag: require('./tag'),
User: require('./user'),
}
const db = { const db = {
sequelize: null, sequelize: null,
loadModels () {
for (const modelName in models) {
const m = models[modelName](db.sequelize, Sequelize.DataTypes)
DB[modelName] = m
}
},
associates () {
const { Filter, Collection, APUser, Instance, User, Event, EventNotification, Tag,
OAuthCode, OAuthClient, OAuthToken, Resource, Place, Notification } = DB
Filter.belongsTo(Collection)
Collection.hasMany(Filter)
Instance.hasMany(APUser)
APUser.belongsTo(Instance)
OAuthCode.belongsTo(User)
OAuthCode.belongsTo(OAuthClient, { as: 'client' })
OAuthToken.belongsTo(User)
OAuthToken.belongsTo(OAuthClient, { as: 'client' })
APUser.hasMany(Resource)
Resource.belongsTo(APUser)
Event.belongsTo(Place)
Place.hasMany(Event)
Event.belongsTo(User)
User.hasMany(Event)
Event.belongsToMany(Tag, { through: 'event_tags' })
Tag.belongsToMany(Event, { through: 'event_tags' })
Event.belongsToMany(Notification, { through: EventNotification })
Notification.belongsToMany(Event, { through: EventNotification })
Event.hasMany(Resource)
Resource.belongsTo(Event)
Event.hasMany(Event, { as: 'child', foreignKey: 'parentId' })
Event.belongsTo(Event, { as: 'parent' })
SequelizeSlugify.slugifyModel(Event, { source: ['title'], overwrite: false })
},
close() { close() {
if (db.sequelize) { if (db.sequelize) {
return db.sequelize.close() return db.sequelize.close()
@ -28,7 +95,6 @@ const db = {
} }
} }
db.sequelize = new Sequelize(dbConf) db.sequelize = new Sequelize(dbConf)
return db.sequelize.authenticate()
}, },
async isEmpty() { async isEmpty() {
try { try {
@ -57,13 +123,12 @@ const db = {
}) })
return umzug.up() return umzug.up()
}, },
async initialize() { initialize() {
if (config.status === 'CONFIGURED') { if (config.status === 'CONFIGURED') {
try { try {
await db.connect() db.connect()
log.debug('Running migrations') db.loadModels()
await db.runMigrations() db.associates()
return settingsController.load()
} catch (e) { } catch (e) {
log.warn(` ⚠️ Cannot connect to db, check your configuration => ${e}`) log.warn(` ⚠️ Cannot connect to db, check your configuration => ${e}`)
process.exit(1) process.exit(1)

View file

@ -1,11 +1,5 @@
module.exports = (sequelize, DataTypes) =>
const sequelize = require('./index').sequelize sequelize.define('instance', {
const { Model, DataTypes } = require('sequelize')
const APUser = require('./ap_user')
class Instance extends Model {}
Instance.init({
domain: { domain: {
primaryKey: true, primaryKey: true,
allowNull: false, allowNull: false,
@ -14,9 +8,4 @@ Instance.init({
name: DataTypes.STRING, name: DataTypes.STRING,
blocked: DataTypes.BOOLEAN, blocked: DataTypes.BOOLEAN,
data: DataTypes.JSON data: DataTypes.JSON
}, { sequelize, modelName: 'instance' }) })
Instance.hasMany(APUser)
APUser.belongsTo(Instance)
module.exports = Instance

View file

@ -0,0 +1,2 @@
// export default models
module.exports = {}

View file

@ -1,10 +1,5 @@
module.exports = (sequelize, DataTypes) =>
const sequelize = require('./index').sequelize sequelize.define('notification', {
const { Model, DataTypes } = require('sequelize')
class Notification extends Model {}
Notification.init({
filters: DataTypes.JSON, filters: DataTypes.JSON,
email: DataTypes.STRING, email: DataTypes.STRING,
remove_code: DataTypes.STRING, remove_code: DataTypes.STRING,
@ -25,5 +20,3 @@ Notification.init({
fields: ['action', 'type'] fields: ['action', 'type']
}] }]
}) })
module.exports = Notification

View file

@ -1,10 +1,5 @@
module.exports = (sequelize, DataTypes) =>
const sequelize = require('./index').sequelize sequelize.define('oauth_client', {
const { Model, DataTypes } = require('sequelize')
class OAuthClient extends Model {}
OAuthClient.init({
id: { id: {
type: DataTypes.STRING, type: DataTypes.STRING,
primaryKey: true, primaryKey: true,
@ -15,6 +10,4 @@ OAuthClient.init({
scopes: DataTypes.STRING, scopes: DataTypes.STRING,
redirectUris: DataTypes.STRING, redirectUris: DataTypes.STRING,
website: DataTypes.STRING website: DataTypes.STRING
}, { sequelize, modelName: 'oauth_client' }) })
module.exports = OAuthClient

View file

@ -1,13 +1,5 @@
module.exports = (sequelize, DataTypes) =>
const sequelize = require('./index').sequelize sequelize.define('oauth_code', {
const { Model, DataTypes } = require('sequelize')
const User = require('./user')
const OAuthClient = require('./oauth_client')
class OAuthCode extends Model {}
OAuthCode.init({
authorizationCode: { authorizationCode: {
type: DataTypes.STRING, type: DataTypes.STRING,
primaryKey: true primaryKey: true
@ -15,9 +7,4 @@ OAuthCode.init({
expiresAt: DataTypes.DATE, expiresAt: DataTypes.DATE,
scope: DataTypes.STRING, scope: DataTypes.STRING,
redirect_uri: DataTypes.STRING redirect_uri: DataTypes.STRING
}, { sequelize, modelName: 'oauth_code' }) })
OAuthCode.belongsTo(User)
OAuthCode.belongsTo(OAuthClient, { as: 'client' })
module.exports = OAuthCode

View file

@ -1,13 +1,5 @@
module.exports = (sequelize, DataTypes) =>
const sequelize = require('./index').sequelize sequelize.define('oauth_token', {
const { Model, DataTypes } = require('sequelize')
const User = require('./user')
const OAuthClient = require('./oauth_client')
class OAuthToken extends Model {}
OAuthToken.init({
accessToken: { accessToken: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
@ -27,9 +19,4 @@ OAuthToken.init({
} }
}, },
scope: DataTypes.STRING scope: DataTypes.STRING
}, { sequelize, modelName: 'oauth_token' }) })
OAuthToken.belongsTo(User)
OAuthToken.belongsTo(OAuthClient, { as: 'client' })
module.exports = OAuthToken

View file

@ -1,9 +1,5 @@
const { Model, DataTypes } = require('sequelize') module.exports = (sequelize, DataTypes) =>
const sequelize = require('./index').sequelize sequelize.define('place', {
class Place extends Model {}
Place.init({
name: { name: {
type: DataTypes.STRING, type: DataTypes.STRING,
unique: true, unique: true,
@ -13,6 +9,4 @@ Place.init({
address: DataTypes.STRING, address: DataTypes.STRING,
latitude: DataTypes.FLOAT, latitude: DataTypes.FLOAT,
longitude: DataTypes.FLOAT, longitude: DataTypes.FLOAT,
}, { sequelize, modelName: 'place' }) })
module.exports = Place

View file

@ -1,11 +1,5 @@
const { Model, DataTypes } = require('sequelize') module.exports = (sequelize, DataTypes) =>
const sequelize = require('./index').sequelize sequelize.define('resource', {
const APUser = require('./ap_user')
class Resource extends Model {}
Resource.init({
activitypub_id: { activitypub_id: {
type: DataTypes.STRING, type: DataTypes.STRING,
index: true, index: true,
@ -13,9 +7,4 @@ Resource.init({
}, },
hidden: DataTypes.BOOLEAN, hidden: DataTypes.BOOLEAN,
data: DataTypes.JSON data: DataTypes.JSON
}, { sequelize, modelName: 'resource' }) })
APUser.hasMany(Resource)
Resource.belongsTo(APUser)
module.exports = Resource

View file

@ -1,9 +1,5 @@
const { Model, DataTypes } = require('sequelize') module.exports = (sequelize, DataTypes) =>
const sequelize = require('./index').sequelize sequelize.define('setting', {
class Setting extends Model {}
Setting.init({
key: { key: {
type: DataTypes.STRING, type: DataTypes.STRING,
primaryKey: true, primaryKey: true,
@ -12,6 +8,4 @@ Setting.init({
}, },
value: DataTypes.JSON, value: DataTypes.JSON,
is_secret: DataTypes.BOOLEAN is_secret: DataTypes.BOOLEAN
}, { sequelize, modelName: 'setting' }) })
module.exports = Setting

View file

@ -1,15 +1,9 @@
const { Model, DataTypes } = require('sequelize') module.exports = (sequelize, DataTypes) =>
const sequelize = require('./index').sequelize sequelize.define('tag', {
class Tag extends Model {}
Tag.init({
tag: { tag: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: false, allowNull: false,
index: true, index: true,
primaryKey: true primaryKey: true
} }
}, { sequelize, modelName: 'tag' }) })
module.exports = Tag

View file

@ -1,11 +1,8 @@
const bcrypt = require('bcryptjs') const bcrypt = require('bcryptjs')
const { Model, DataTypes } = require('sequelize')
const sequelize = require('./index').sequelize
class User extends Model {} module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('user', {
User.init({
settings: { settings: {
type: DataTypes.JSON, type: DataTypes.JSON,
defaultValue: [] defaultValue: []
@ -24,9 +21,7 @@ User.init({
recover_code: DataTypes.STRING, recover_code: DataTypes.STRING,
is_admin: DataTypes.BOOLEAN, is_admin: DataTypes.BOOLEAN,
is_active: DataTypes.BOOLEAN is_active: DataTypes.BOOLEAN
}, { }, {
sequelize,
modelName: 'user',
scopes: { scopes: {
withoutPassword: { withoutPassword: {
attributes: { exclude: ['password', 'recover_code'] } attributes: { exclude: ['password', 'recover_code'] }
@ -35,19 +30,20 @@ User.init({
attributes: { exclude: ['password'] } attributes: { exclude: ['password'] }
} }
} }
}) })
User.prototype.comparePassword = async function (pwd) { User.prototype.comparePassword = async function (pwd) {
if (!this.password) { return false } if (!this.password) { return false }
return bcrypt.compare(pwd, this.password) return bcrypt.compare(pwd, this.password)
} }
User.beforeSave(async (user, _options) => { User.beforeSave(async (user, _options) => {
if (user.changed('password')) { if (user.changed('password')) {
const salt = await bcrypt.genSalt(10) const salt = await bcrypt.genSalt(10)
const hash = await bcrypt.hash(user.password, salt) const hash = await bcrypt.hash(user.password, salt)
user.password = hash user.password = hash
} }
}) })
module.exports = User return User
}

View file

@ -9,7 +9,7 @@ function _initializeDB () {
async function modify (args) { async function modify (args) {
await _initializeDB() await _initializeDB()
const helpers = require('../helpers') const helpers = require('../helpers')
const User = require('../api/models/user') const { User } = require('../api/models/models')
const user = await User.findOne({ where: { email: args.account } }) const user = await User.findOne({ where: { email: args.account } })
console.log() console.log()
if (!user) { if (!user) {

View file

@ -1,10 +1,10 @@
const axios = require('axios') const axios = require('axios')
// const request = require('request')
const crypto = require('crypto') const crypto = require('crypto')
const config = require('../config') const config = require('../config')
const httpSignature = require('http-signature') const httpSignature = require('http-signature')
const APUser = require('../api/models/ap_user')
const Instance = require('../api/models/instance') const { APUser, Instance } = require('../api/models/models')
const url = require('url') const url = require('url')
const settingsController = require('../api/controller/settings') const settingsController = require('../api/controller/settings')
const log = require('../log') const log = require('../log')

View file

@ -270,9 +270,9 @@ module.exports = {
}, },
async APRedirect(req, res, next) { async APRedirect(req, res, next) {
const eventController = require('../server/api/controller/event')
const acceptJson = req.accepts('html', 'application/activity+json') === 'application/activity+json' const acceptJson = req.accepts('html', 'application/activity+json') === 'application/activity+json'
if (acceptJson) { if (acceptJson) {
const eventController = require('../server/api/controller/event')
const event = await eventController._get(req.params.slug) const event = await eventController._get(req.params.slug)
if (event) { if (event) {
return res.redirect(`/federation/m/${event.id}`) return res.redirect(`/federation/m/${event.id}`)

View file

@ -1,4 +1,11 @@
const config = require('../server/config') const config = require('../server/config')
const db = require('./api/models/index')
const log = require('../server/log')
db.initialize()
const settingsController = require('./api/controller/settings')
const initialize = { const initialize = {
// close connections/port/unix socket // close connections/port/unix socket
@ -19,14 +26,14 @@ const initialize = {
}, },
async start () { async start () {
const log = require('../server/log')
const settingsController = require('./api/controller/settings')
const db = require('./api/models/index')
const dayjs = require('dayjs') const dayjs = require('dayjs')
const timezone = require('dayjs/plugin/timezone') const timezone = require('dayjs/plugin/timezone')
dayjs.extend(timezone) dayjs.extend(timezone)
if (config.status == 'CONFIGURED') { if (config.status == 'CONFIGURED') {
await db.initialize() await db.sequelize.authenticate()
log.debug('Running migrations')
await db.runMigrations()
await settingsController.load()
config.status = 'READY' config.status = 'READY'
} else { } else {
if (process.env.GANCIO_DB_DIALECT) { if (process.env.GANCIO_DB_DIALECT) {

View file

@ -4,14 +4,10 @@ const mail = require('./api/mail')
const log = require('./log') const log = require('./log')
const fediverseHelpers = require('./federation/helpers') const fediverseHelpers = require('./federation/helpers')
const Event = require('./api/models/event')
const Notification = require('./api/models/notification')
const EventNotification = require('./api/models/eventnotification')
const User = require('./api/models/user')
const Place = require('./api/models/place')
const Tag = require('./api/models/tag')
const eventController = require('./api/controller/event') const { Event, Notification, EventNotification, User, Place, Tag } = require('./api/models/models')
const settingsController = require('./api/controller/settings') const settingsController = require('./api/controller/settings')
const notifier = { const notifier = {
@ -37,7 +33,36 @@ const notifier = {
return Promise.all(promises) return Promise.all(promises)
}, },
async getNotifications(event, action) {
log.debug(`getNotifications ${event.title} ${action}`)
function match(event, filters) {
// matches if no filter specified
if (!filters) { return true }
// check for visibility
if (typeof filters.is_visible !== 'undefined' && filters.is_visible !== event.is_visible) { return false }
if (!filters.tags && !filters.places) { return true }
if (!filters.tags.length && !filters.places.length) { return true }
if (filters.tags.length) {
const m = intersection(event.tags.map(t => t.tag), filters.tags)
if (m.length > 0) { return true }
}
if (filters.places.length) {
if (filters.places.find(p => p === event.place.name)) {
return true
}
}
}
const notifications = await Notification.findAll({ where: { action }, include: [Event] })
// get notification that matches with selected event
return notifications.filter(notification => match(event, notification.filters))
},
async notifyEvent (action, eventId) { async notifyEvent (action, eventId) {
const event = await Event.findByPk(eventId, { const event = await Event.findByPk(eventId, {
include: [Tag, Place, Notification, User] include: [Tag, Place, Notification, User]
}) })
@ -46,7 +71,7 @@ const notifier = {
log.debug(action, event.title) log.debug(action, event.title)
// insert notifications // insert notifications
const notifications = await eventController.getNotifications(event, action) const notifications = await notifier.getNotifications(event, action)
await event.addNotifications(notifications) await event.addNotifications(notifications)
const event_notifications = await event.getNotifications({ through: { where: { status: 'new' } } }) const event_notifications = await event.getNotifications({ through: { where: { status: 'new' } } })

View file

@ -4,23 +4,22 @@ const initialize = require('./initialize.server')
const config = require('./config') const config = require('./config')
const helpers = require('./helpers') const helpers = require('./helpers')
const api = require('./api')
app.use([
helpers.initSettings,
helpers.logRequest,
helpers.serveStatic()
])
async function main () { async function main () {
await initialize.start() await initialize.start()
app.use([
helpers.initSettings,
helpers.logRequest,
helpers.serveStatic()
])
// const metricsController = require('./metrics') // const metricsController = require('./metrics')
// const promBundle = require('express-prom-bundle') // const promBundle = require('express-prom-bundle')
// const metricsMiddleware = promBundle({ includeMethod: true }) // const metricsMiddleware = promBundle({ includeMethod: true })
const log = require('./log') const log = require('./log')
const api = require('./api')
app.enable('trust proxy') app.enable('trust proxy')
@ -60,7 +59,7 @@ async function main () {
} }
// api! // api!
app.use('/api', api) app.use('/api', api())
// // Handle 500 // // Handle 500
app.use((error, _req, res, _next) => { app.use((error, _req, res, _next) => {
@ -87,8 +86,6 @@ if (process.env.NODE_ENV !== 'test') {
main() main()
} }
// app.listen(13120)
module.exports = { module.exports = {
main, main,
handler: app, handler: app,

View file

@ -23,6 +23,11 @@ export const state = () => ({
trusted_instances_label: '', trusted_instances_label: '',
footerLinks: [] footerLinks: []
}, },
filter: {
query: '',
show_recurrent: null,
show_multidate: null
},
announcements: [], announcements: [],
events: [] events: []
}) })
@ -39,6 +44,9 @@ export const mutations = {
}, },
setEvents (state, events) { setEvents (state, events) {
state.events = events state.events = events
},
setFilter (state, { type, value }) {
state.filter[type] = value
} }
} }
@ -47,6 +55,9 @@ export const actions = {
// we use it to get configuration from db, set locale, etc... // we use it to get configuration from db, set locale, etc...
nuxtServerInit ({ commit }, { _req, res }) { nuxtServerInit ({ commit }, { _req, res }) {
commit('setSettings', res.locals.settings) commit('setSettings', res.locals.settings)
commit('setFilter', { type: 'show_recurrent',
value: res.locals.settings.allow_recurrent_event && res.locals.settings.recurrent_event_visible })
if (res.locals.status === 'READY') { if (res.locals.status === 'READY') {
commit('setAnnouncements', res.locals.announcements) commit('setAnnouncements', res.locals.announcements)
} }
@ -62,11 +73,15 @@ export const actions = {
await this.$axios.$post('/settings', setting) await this.$axios.$post('/settings', setting)
commit('setSetting', setting) commit('setSetting', setting)
}, },
setFilter ({ commit }, [type, value]) {
commit('setFilter', { type, value })
},
async getEvents ({ commit, state }, params = {}) { async getEvents ({ commit, state }, params = {}) {
const events = await this.$api.getEvents({ const events = await this.$api.getEvents({
start: params.start || dayjs().startOf('month').unix(), start: params.start || dayjs().startOf('month').unix(),
end: params.end || null, end: params.end || null,
show_recurrent: params.show_recurrent || state.settings.recurrent_event_visible show_recurrent: state.filter.show_recurrent,
show_multidate: state.filter.show_multidate
}) })
commit('setEvents', events) commit('setEvents', events)
return events return events

View file

@ -21,16 +21,21 @@ beforeAll(async () => {
default: default:
process.env.config_path = path.resolve(__dirname, './seeds/config.sqlite.json') process.env.config_path = path.resolve(__dirname, './seeds/config.sqlite.json')
} }
try {
app = await require('../server/routes.js').main() app = await require('../server/routes.js').main()
const { sequelize } = require('../server/api/models/index') const { sequelize } = require('../server/api/models/index')
await sequelize.query('DELETE FROM settings') await sequelize.query('DELETE FROM settings')
await sequelize.query('DELETE FROM events') await sequelize.query('DELETE FROM events')
await sequelize.query('DELETE FROM user_followers')
await sequelize.query('DELETE FROM users') await sequelize.query('DELETE FROM users')
await sequelize.query('DELETE FROM ap_users') await sequelize.query('DELETE FROM ap_users')
await sequelize.query('DELETE FROM tags') await sequelize.query('DELETE FROM tags')
await sequelize.query('DELETE FROM places') await sequelize.query('DELETE FROM places')
await sequelize.query('DELETE FROM collections')
await sequelize.query('DELETE FROM filters') await sequelize.query('DELETE FROM filters')
await sequelize.query('DELETE FROM collections')
} catch (e) {
console.error(e)
}
}) })
afterAll(async () => { afterAll(async () => {

View file

@ -10634,10 +10634,10 @@ ret@~0.1.10:
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
retry-as-promised@^6.1.0: retry-as-promised@^7.0.3:
version "6.1.0" version "7.0.3"
resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-6.1.0.tgz#11eca9a0f97804d552ec8e74bc4eb839bd226dc4" resolved "https://registry.yarnpkg.com/retry-as-promised/-/retry-as-promised-7.0.3.tgz#ca3c13b15525a7bfbf0f56d2996f0e75649d068b"
integrity sha512-Hj/jY+wFC+SB9SDlIIFWiGOHnNG0swYbGYsOj2BJ8u2HKUaobNKab0OIC0zOLYzDy0mb7A4xA5BMo4LMz5YtEA== integrity sha512-SEvMa4khHvpU/o6zgh7sK24qm6rxVgKnrSyzb5POeDvZx5N9Bf0s5sQsQ4Fl+HjRp0X+w2UzACGfUnXtx6cJ9Q==
retry@^0.12.0: retry@^0.12.0:
version "0.12.0" version "0.12.0"
@ -10926,10 +10926,10 @@ sequelize-slugify@^1.6.2:
dependencies: dependencies:
sluglife "^0.9.8" sluglife "^0.9.8"
sequelize@^6.27.0: sequelize@^6.28.0:
version "6.27.0" version "6.28.0"
resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.27.0.tgz#b267e76997df57842cc1e2c1c1d7e02405bcdb9c" resolved "https://registry.yarnpkg.com/sequelize/-/sequelize-6.28.0.tgz#d6bc4e36647e8501635467c0777c45a33f5d5ba8"
integrity sha512-Rm7BM8HQekeABup0KdtSHriu8ppJuHj2TJWCxvZtzU6j8V1LVnBk2rs38P8r4gMWgdLKs5NYoLC4il95KLsv0w== integrity sha512-+WHqvUQgTp19GLkt+gyQ+F6qg+FIEO2O5F9C0TOYV/PjZ2a/XwWvVkL1NCkS4VSIjVVvAUutiW6Wv9ofveGaVw==
dependencies: dependencies:
"@types/debug" "^4.1.7" "@types/debug" "^4.1.7"
"@types/validator" "^13.7.1" "@types/validator" "^13.7.1"
@ -10940,7 +10940,7 @@ sequelize@^6.27.0:
moment "^2.29.1" moment "^2.29.1"
moment-timezone "^0.5.34" moment-timezone "^0.5.34"
pg-connection-string "^2.5.0" pg-connection-string "^2.5.0"
retry-as-promised "^6.1.0" retry-as-promised "^7.0.3"
semver "^7.3.5" semver "^7.3.5"
sequelize-pool "^7.1.0" sequelize-pool "^7.1.0"
toposort-class "^1.0.1" toposort-class "^1.0.1"

1
yunohost Submodule

@ -0,0 +1 @@
Subproject commit 0e3de77d5a7d5e40f3c5ae4c673b73b885a6e9c3