Merge branch 'master' into gh

This commit is contained in:
lesion 2022-06-21 23:48:40 +02:00
commit b66feb92e2
No known key found for this signature in database
GPG key ID: 352918250B012177
47 changed files with 998 additions and 781 deletions

8
.snyk Normal file
View file

@ -0,0 +1,8 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.25.0
ignore: {}
# patches apply the minimum changes required to fix a vulnerability
patch:
SNYK-JS-LODASH-567746:
- express-oauth-server > oauth2-server > lodash:
patched: '2022-06-06T14:57:24.390Z'

View file

@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
- new Tag page! - new Tag page!
- new Place page! - new Place page!
- new search flow - new search flow
- new meta-tag-place / group / cohort page! - new meta-tag-place / group / collection page!
- allow footer links reordering - allow footer links reordering
- new Docker image - new Docker image
- add GANCIO_DB_PORT environment - add GANCIO_DB_PORT environment
@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file.
- add dynamic sitemap.xml ! - add dynamic sitemap.xml !
- calendar attributes refactoring (a dot each day, colors represents n. events) - calendar attributes refactoring (a dot each day, colors represents n. events)
- fix event mime type response - fix event mime type response
- new **undocumented** gancio CLI accounts management (list / create / remove / modify accounts)
### 1.4.4 - 10 may '22 ### 1.4.4 - 10 may '22

View file

@ -42,9 +42,10 @@ li {
.v-dialog { .v-dialog {
width: 600px; width: 600px;
max-width: 800px; max-width: 800px;
&.v-dialog--fullscreen { }
max-width: 100%;
} .v-dialog .v-dialog--fullscreen {
max-width: 100%;
} }
.theme--dark.v-list { .theme--dark.v-list {

View file

@ -47,7 +47,6 @@ v-col(cols=12)
:disabled='!value.from' :disabled='!value.from'
:prepend-icon="mdiClockTimeFourOutline" :prepend-icon="mdiClockTimeFourOutline"
:rules="[$validators.required('event.from')]" :rules="[$validators.required('event.from')]"
readonly
v-bind="attrs" v-bind="attrs"
v-on="on") v-on="on")
v-time-picker( v-time-picker(
@ -72,7 +71,6 @@ v-col(cols=12)
:value="dueHour" :value="dueHour"
:disabled='!fromHour' :disabled='!fromHour'
:prepend-icon="mdiClockTimeEightOutline" :prepend-icon="mdiClockTimeEightOutline"
readonly
v-bind="attrs" v-bind="attrs"
v-on="on") v-on="on")
v-time-picker( v-time-picker(
@ -103,7 +101,7 @@ export default {
data () { data () {
return { return {
mdiClockTimeFourOutline, mdiClockTimeEightOutline, mdiClockTimeFourOutline, mdiClockTimeEightOutline,
allowedMinutes: [0, 15, 30, 45], allowedMinutes: [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55],
menuFromHour: false, menuFromHour: false,
menuDueHour: false, menuDueHour: false,
type: 'normal', type: 'normal',

View file

@ -8,7 +8,7 @@
v-card-text.body.pt-0.pb-0 v-card-text.body.pt-0.pb-0
time.dt-start.subtitle-1(:datetime='event.start_datetime|unixFormat("YYYY-MM-DD HH:mm")' itemprop="startDate" :content="event.start_datetime|unixFormat('YYYY-MM-DDTHH:mm')") <v-icon v-text='mdiCalendar'></v-icon> {{ event|when }} time.dt-start.subtitle-1(:datetime='event.start_datetime|unixFormat("YYYY-MM-DD HH:mm")' itemprop="startDate" :content="event.start_datetime|unixFormat('YYYY-MM-DDTHH:mm')") <v-icon v-text='mdiCalendar'></v-icon> {{ event|when }}
.d-none.dt-end(itemprop="endDate" :content="event.end_datetime|unixFormat('YYYY-MM-DDTHH:mm')") {{event.end_datetime|unixFormat('YYYY-MM-DD HH:mm')}} .d-none.dt-end(itemprop="endDate" :content="event.end_datetime|unixFormat('YYYY-MM-DDTHH:mm')") {{event.end_datetime|unixFormat('YYYY-MM-DD HH:mm')}}
nuxt-link.place.d-block.p-location.pl-0(text color='primary' :to='`/p/${event.place.name}`' itemprop="location" :content="event.place.name") <v-icon v-text='mdiMapMarker'></v-icon> {{event.place.name}} nuxt-link.place.d-block.p-location.pl-0(text color='primary' :to='`/place/${event.place.name}`' itemprop="location" :content="event.place.name") <v-icon v-text='mdiMapMarker'></v-icon> {{event.place.name}}
.d-none(itemprop='location.address') {{event.place.address}} .d-none(itemprop='location.address') {{event.place.address}}
v-card-actions.pt-0.actions.justify-space-between v-card-actions.pt-0.actions.justify-space-between

View file

@ -1,55 +1,55 @@
<template lang="pug"> <template lang="pug">
v-app-bar(app aria-label='Menu' height=64) v-app-bar(app aria-label='Menu' height=64)
//- logo, title and description //- logo, title and description
v-list-item.pa-0(:to='$route.name==="index"?"/about":"/"') v-list-item(:to='$route.name==="index"?"/about":"/"')
v-list-item-avatar.ma-xs-1(tile) v-list-item-avatar.ma-xs-1(tile)
img(src='/logo.png' height='40') img(src='/logo.png' height='40')
v-list-item-content.d-flex v-list-item-content
v-list-item-title.d-flex v-list-item-title.d-flex
h2 {{settings.title}} h2 {{settings.title}}
v-list-item-subtitle.d-none.d-sm-flex {{settings.description}} v-list-item-subtitle.d-none.d-sm-flex {{settings.description}}
v-spacer v-spacer
v-btn(v-if='$auth.loggedIn || settings.allow_anon_event' icon nuxt to='/add' :aria-label='$t("common.add_event")' :title='$t("common.add_event")') v-btn(v-if='$auth.loggedIn || settings.allow_anon_event' icon nuxt to='/add' :aria-label='$t("common.add_event")' :title='$t("common.add_event")')
v-icon(large color='primary' v-text='mdiPlus') v-icon(large color='primary' v-text='mdiPlus')
v-btn(icon nuxt to='/export' :title='$t("common.share")' :aria-label='$t("common.share")') v-btn(icon nuxt to='/export' :title='$t("common.share")' :aria-label='$t("common.share")')
v-icon(v-text='mdiShareVariant') v-icon(v-text='mdiShareVariant')
v-btn(v-if='!$auth.loggedIn' icon nuxt to='/login' :title='$t("common.login")' :aria-label='$t("common.login")') v-btn(v-if='!$auth.loggedIn' icon nuxt to='/login' :title='$t("common.login")' :aria-label='$t("common.login")')
v-icon(v-text='mdiLogin') v-icon(v-text='mdiLogin')
client-only client-only
v-menu(v-if='$auth.loggedIn' offset-y eager) v-menu(v-if='$auth.loggedIn' offset-y eager)
template(v-slot:activator="{ on, attrs }") template(v-slot:activator="{ on, attrs }")
v-btn(icon v-bind='attrs' v-on='on' title='Menu' aria-label='Menu') v-btn(icon v-bind='attrs' v-on='on' title='Menu' aria-label='Menu')
v-icon(v-text='mdiDotsVertical')
v-list
v-list-item(nuxt to='/settings')
v-list-item-icon
v-icon(v-text='mdiCog')
v-list-item-content
v-list-item-title {{$t('common.settings')}}
v-list-item(v-if='$auth.user.is_admin' nuxt to='/admin')
v-list-item-icon
v-icon(v-text='mdiAccount')
v-list-item-content
v-list-item-title {{$t('common.admin')}}
v-list-item(@click='logout')
v-list-item-icon
v-icon(v-text='mdiLogout')
v-list-item-content
v-list-item-title {{$t('common.logout')}}
template(#placeholder)
v-btn(v-if='$auth.loggedIn' icon aria-label='Menu' title='Menu')
v-icon(v-text='mdiDotsVertical') v-icon(v-text='mdiDotsVertical')
v-list
v-list-item(nuxt to='/settings')
v-list-item-icon
v-icon(v-text='mdiCog')
v-list-item-content
v-list-item-title {{$t('common.settings')}}
v-list-item(v-if='$auth.user.is_admin' nuxt to='/admin')
v-list-item-icon
v-icon(v-text='mdiAccount')
v-list-item-content
v-list-item-title {{$t('common.admin')}}
v-list-item(@click='logout')
v-list-item-icon
v-icon(v-text='mdiLogout')
v-list-item-content
v-list-item-title {{$t('common.logout')}}
template(#placeholder)
v-btn(v-if='$auth.loggedIn' icon aria-label='Menu' title='Menu')
v-icon(v-text='mdiDotsVertical')
v-btn(icon target='_blank' :href='`${settings.baseurl}/feed/rss`' title='RSS' aria-label='RSS') v-btn(icon target='_blank' :href='`${settings.baseurl}/feed/rss`' title='RSS' aria-label='RSS')
v-icon(color='orange' v-text='mdiRss') v-icon(color='orange' v-text='mdiRss')
</template> </template>
<script> <script>

View file

@ -1,42 +1,48 @@
<template lang="pug"> <template lang="pug">
v-container.pt-0.pt-md-2 v-row
v-switch.mt-0( v-col(cols=12)
v-if='settings.allow_recurrent_event' v-switch(
v-model='showRecurrent' v-if='settings.allow_recurrent_event'
inset color='primary' v-model='show_recurrent'
hide-details @change='change'
:label="$t('event.show_recurrent')") inset color='primary'
v-autocomplete( hide-details
v-model='meta' :label="$t('event.show_recurrent')")
:label='$t("common.search")' v-col.mb-4(cols=12)
:filter='filter' v-autocomplete.p-0(
cache-items v-model='meta'
hide-details :label='$t("common.search")'
color='primary' :filter='filter'
hide-selected cache-items
small-chips hide-details
:items='items' color='primary'
@change='change' hide-selected
hide-no-data small-chips
@input.native='search' @focus='search'
item-text='label' :menu-props="{ maxWidth: '400' }"
return-object :items='items'
chips @change='change'
multiple) hide-no-data
template(v-slot:selection="{ attrs, item }") @input.native='search'
v-chip(v-bind="attrs" item-text='label'
close return-object
@click:close='remove(item)' chips
:close-icon='mdiCloseCircle') multiple)
v-avatar(left) template(v-slot:selection="{ attrs, item }")
v-icon(v-text="item.type === 'place' ? mdiMapMarker : mdiTag") v-chip(v-bind="attrs"
span {{ item.label }} small
template(v-slot:item='{ item }') close
v-list-item-avatar @click:close='remove(item)'
v-icon(v-text="item.type === 'place' ? mdiMapMarker : mdiTag") :close-icon='mdiCloseCircle')
v-list-item-content v-avatar(left)
v-list-item-title(v-text='item.label') v-icon(small v-text="item.type === 'place' ? mdiMapMarker : mdiTag")
v-list-item-subtitle(v-if='item.type ==="place"' v-text='item.address') span {{ item.label }}
template(v-slot:item='{ item }')
v-list-item-avatar
v-icon(v-text="item.type === 'place' ? mdiMapMarker : mdiTag")
v-list-item-content
v-list-item-title(v-text='item.label')
v-list-item-subtitle(v-if='item.type ==="place"' v-text='item.address')
</template> </template>
<script> <script>
@ -47,26 +53,17 @@ import debounce from 'lodash/debounce'
export default { export default {
name: 'Search', name: 'Search',
props: { props: {
filters: { type: Object, default: () => ({}) } filters: { type: Object, default: () => ({ }) }
}, },
data () { data () {
return { return {
mdiTag, mdiMapMarker, mdiCloseCircle, mdiTag, mdiMapMarker, mdiCloseCircle,
meta: [], meta: [],
items: [], items: [],
show_recurrent: this.filters.show_recurrent || false
} }
}, },
computed: { computed: mapState(['settings']),
...mapState(['settings']),
showRecurrent: {
get () {
return this.filters.show_recurrent
},
set (v) {
this.change(v)
}
},
},
methods: { methods: {
filter (item, queryText, itemText) { filter (item, queryText, itemText) {
return itemText.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1 || return itemText.toLocaleLowerCase().indexOf(queryText.toLocaleLowerCase()) > -1 ||
@ -76,14 +73,14 @@ export default {
this.items = await this.$axios.$get(`/event/meta?search=${search.target.value}`) this.items = await this.$axios.$get(`/event/meta?search=${search.target.value}`)
}, 100), }, 100),
remove (item) { remove (item) {
this.meta = this.meta.filter(m => m.type !== item.type || m.type === 'place' ? m.id !== item.id : m.tag !== item.tag) this.meta = this.meta.filter(m => m.type !== item.type || m.type === 'place' ? m.id !== item.id : m.label !== item.label)
this.change() this.change()
}, },
change (show_recurrent) { change () {
const filters = { const filters = {
tags: this.meta.filter(t => t.type === 'tag').map(t => t.label), tags: this.meta.filter(t => t.type === 'tag').map(t => t.label),
places: this.meta.filter(p => p.type === 'place').map(p => p.id), places: this.meta.filter(p => p.type === 'place').map(p => p.id),
show_recurrent: typeof show_recurrent !== 'undefined' ? show_recurrent : this.filters.show_recurrent show_recurrent: this.show_recurrent
} }
this.$emit('update', filters) this.$emit('update', filters)
} }

View file

@ -12,10 +12,11 @@ v-row
@input.native='search' @input.native='search'
persistent-hint persistent-hint
:items="places" :items="places"
@focus='search'
@change='selectPlace') @change='selectPlace')
template(v-slot:item="{ item, attrs, on }") template(v-slot:item="{ item, attrs, on }")
v-list-item(v-bind='attrs' v-on='on') v-list-item(v-bind='attrs' v-on='on')
v-list-item-content(two-line v-if='item.create') v-list-item-content(two-line v-if='item.create && search')
v-list-item-title <v-icon color='primary' v-text='mdiPlus' :aria-label='$t("common.add")'></v-icon> {{item.name}} v-list-item-title <v-icon color='primary' v-text='mdiPlus' :aria-label='$t("common.add")'></v-icon> {{item.name}}
v-list-item-content(two-line v-else) v-list-item-content(two-line v-else)
v-list-item-title(v-text='item.name') v-list-item-title(v-text='item.name')
@ -73,7 +74,7 @@ export default {
search: debounce(async function(ev) { search: debounce(async function(ev) {
const search = ev.target.value.trim().toLowerCase() const search = 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) { 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())
if (!matches) { if (!matches) {
this.places.unshift({ create: true, name: ev.target.value.trim() }) this.places.unshift({ create: true, name: ev.target.value.trim() })

View file

@ -1,44 +1,47 @@
<template lang='pug'> <template lang='pug'>
v-container v-container
v-card-title {{$t('common.cohort')}} v-card-title {{$t('common.collections')}}
v-spacer v-spacer
v-text-field(v-model='search' v-text-field(v-model='search'
:append-icon='mdiMagnify' outlined rounded :append-icon='mdiMagnify' outlined rounded
label='Search' label='Search'
single-line hide-details) single-line hide-details)
v-card-subtitle(v-html="$t('admin.cohort_description')") v-card-subtitle(v-html="$t('admin.collections_description')")
v-btn(color='primary' text @click='newCohort') <v-icon v-text='mdiPlus'></v-icon> {{$t('common.new')}} v-btn(color='primary' text @click='newCollection') <v-icon v-text='mdiPlus'></v-icon> {{$t('admin.new_collection')}}
v-dialog(v-model='dialog' width='800' destroy-on-close :fullscreen='$vuetify.breakpoint.xsOnly') v-dialog(v-model='dialog' width='800' destroy-on-close :fullscreen='$vuetify.breakpoint.xsOnly')
v-card(color='secondary') v-card(color='secondary')
v-card-title {{$t('admin.edit_cohort')}} v-card-title {{$t('admin.edit_collection')}}
v-card-text v-card-text
v-form(v-model='valid' ref='form') v-form(v-model='valid' ref='form')
v-text-field( v-text-field(
v-if='!cohort.id' v-if='!collection.id'
:rules="[$validators.required('common.name')]" :rules="[$validators.required('common.name')]"
:label="$t('common.name')" :label="$t('common.name')"
v-model='cohort.name' v-model='collection.name'
:placeholder='$t("common.name")') :placeholder='$t("common.name")')
template(v-slot:append-outer v-if='!cohort.id') template(v-slot:append-outer v-if='!collection.id')
v-btn(text @click='saveCohort' color='primary' :loading='loading' v-btn(text @click='saveCollection' color='primary' :loading='loading'
:disabled='!valid || loading || !!cohort.id') {{$t('common.save')}} :disabled='!valid || loading || !!collection.id') {{$t('common.save')}}
h3(v-else class='text-h5' v-text='cohort.name') h3(v-else class='text-h5' v-text='collection.name')
v-row v-row
v-col(cols=5) v-col(cols=5)
v-autocomplete(v-model='filterTags' v-autocomplete(v-model='filterTags'
cache-items cache-items
:prepend-icon="mdiTagMultiple" :prepend-icon="mdiTagMultiple"
chips small-chips multiple deletable-chips hide-no-data hide-selected persistent-hint chips small-chips multiple deletable-chips hide-no-data hide-selected persistent-hint
:disabled="!cohort.id" :disabled="!collection.id"
placeholder='Tutte' placeholder='Tutte'
@input.native='searchTags' @input.native='searchTags'
@focus='searchTags'
:delimiters="[',', ';']" :delimiters="[',', ';']"
:items="tags" :items="tags"
:label="$t('common.tags')") :label="$t('common.tags')")
template(v-slot:selection="{ item, on, attrs, selected, parent}")
v-chip(v-bind="attrs" close :close-icon='mdiCloseCircle' @click:close='parent.selectItem(item)'
:input-value="selected" label small) {{item}}
v-col(cols=5) v-col(cols=5)
v-autocomplete(v-model='filterPlaces' v-autocomplete(v-model='filterPlaces'
@ -49,11 +52,16 @@ v-container
clearable clearable
return-object return-object
item-text='name' item-text='name'
:disabled="!cohort.id" :disabled="!collection.id"
@input.native="searchPlaces" @input.native="searchPlaces"
@focus='searchPlaces'
:delimiters="[',', ';']" :delimiters="[',', ';']"
:items="places" :items="places"
:label="$t('common.places')") :label="$t('common.places')")
template(v-slot:selection="{ item, on, attrs, selected, parent}")
v-chip(v-bind="attrs" close :close-icon='mdiCloseCircle' @click:close='parent.selectItem(item)'
:input-value="selected" label small) {{item.name}}
//- template(v-slot:item="{ item, attrs, on }") //- template(v-slot:item="{ item, attrs, on }")
//- v-list-item(v-bind='attrs' v-on='on') //- v-list-item(v-bind='attrs' v-on='on')
//- v-list-item-content(two-line) //- v-list-item-content(two-line)
@ -61,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='!cohort.id || !filterPlaces.length && !filterTags.length') add <v-icon v-text='mdiPlus'></v-icon> v-btn(color='primary' text @click='addFilter' :disabled='!collection.id || !filterPlaces.length && !filterTags.length') add <v-icon v-text='mdiPlus'></v-icon>
v-data-table( v-data-table(
@ -76,28 +84,25 @@ v-container
v-chip.ma-1(small v-for='tag in item.tags' v-text='tag' :key='tag') v-chip.ma-1(small v-for='tag in item.tags' v-text='tag' :key='tag')
template(v-slot:item.places='{item}') template(v-slot:item.places='{item}')
v-chip.ma-1(small v-for='place in item.places' v-text='place.name' :key='place.id' ) v-chip.ma-1(small v-for='place in item.places' v-text='place.name' :key='place.id' )
v-card-actions v-card-actions
v-spacer v-spacer
v-btn(text @click='dialog=false' color='warning') {{$t('common.close')}} v-btn(text @click='dialog=false' color='warning') {{$t('common.close')}}
//- v-btn(text @click='saveCohort' color='primary' :loading='loading'
//- :disable='!valid || loading') {{$t('common.save')}}
v-card-text v-card-text
v-data-table( v-data-table(
:headers='cohortHeaders' :headers='collectionHeaders'
:items='cohorts' :items='collections'
:hide-default-footer='cohorts.length<5' :hide-default-footer='collections.length<5'
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }' :footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
:search='search') :search='search')
template(v-slot:item.filters='{item}') template(v-slot:item.filters='{item}')
span {{cohortFilters(item)}} span {{collectionFilters(item)}}
template(v-slot:item.actions='{item}') template(v-slot:item.actions='{item}')
v-btn(@click='editCohort(item)' color='primary' icon) v-btn(@click='editCollection(item)' color='primary' icon)
v-icon(v-text='mdiPencil') v-icon(v-text='mdiPencil')
v-btn(@click='removeCohort(item)' color='error' icon) v-btn(@click='removeCollection(item)' color='error' icon)
v-icon(v-text='mdiDeleteForever') v-icon(v-text='mdiDeleteForever')
</template> </template>
@ -114,16 +119,16 @@ export default {
dialog: false, dialog: false,
valid: false, valid: false,
search: '', search: '',
cohort: { name: '', id: null }, collection: { name: '', id: null },
filterTags: [], filterTags: [],
filterPlaces: [], filterPlaces: [],
tags: [], tags: [],
places: [], places: [],
cohorts: [], collections: [],
filters: [], filters: [],
tagName: '', tagName: '',
placeName: '', placeName: '',
cohortHeaders: [ collectionHeaders: [
{ value: 'name', text: 'Name' }, { value: 'name', text: 'Name' },
{ value: 'filters', text: 'Filters' }, { value: 'filters', text: 'Filters' },
{ value: 'actions', text: 'Actions', align: 'right' } { value: 'actions', text: 'Actions', align: 'right' }
@ -136,7 +141,7 @@ export default {
} }
}, },
async fetch () { async fetch () {
this.cohorts = await this.$axios.$get('/cohorts?withFilters=true') this.collections = await this.$axios.$get('/collections?withFilters=true')
}, },
methods: { methods: {
@ -146,8 +151,8 @@ export default {
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}`)
}, 100), }, 100),
cohortFilters (cohort) { collectionFilters (collection) {
return cohort.filters.map(f => { return collection.filters.map(f => {
return '(' + f.tags?.join(', ') + f.places?.map(p => p.name).join(', ') + ')' return '(' + f.tags?.join(', ') + f.places?.map(p => p.name).join(', ') + ')'
}).join(' - ') }).join(' - ')
}, },
@ -155,27 +160,28 @@ 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', { cohortId: this.cohort.id, tags, places }) const filter = await this.$axios.$post('/filter', { collectionId: this.collection.id, tags, places })
this.$fetch() this.$fetch()
this.filters.push(filter) this.filters.push(filter)
this.filterTags = [] this.filterTags = []
this.filterPlaces = [] this.filterPlaces = []
this.loading = false this.loading = false
}, },
async editCohort (cohort) { async editCollection (collection) {
this.cohort = { ...cohort } this.collection = { ...collection }
this.filters = await this.$axios.$get(`/filter/${cohort.id}`) this.filters = await this.$axios.$get(`/filter/${collection.id}`)
this.dialog = true this.dialog = true
}, },
newCohort () { newCollection () {
this.cohort = { name: '', id: null }, this.collection = { name: '', id: null }
this.filters = [] this.filters = []
this.dialog = true this.dialog = true
}, },
async saveCohort () { async saveCollection () {
if (!this.$refs.form.validate()) return if (!this.$refs.form.validate()) return
this.loading = true this.loading = true
this.cohort = await this.$axios.$post('/cohorts', this.cohort) this.collection.name = this.collection.name.trim()
this.collection = await this.$axios.$post('/collections', this.collection)
this.$fetch() this.$fetch()
this.loading = false this.loading = false
}, },
@ -190,12 +196,12 @@ export default {
this.loading = false this.loading = false
} }
}, },
async removeCohort (cohort) { async removeCollection (collection) {
const ret = await this.$root.$confirm('admin.delete_cohort_confirm', { cohort: cohort.name }) const ret = await this.$root.$confirm('admin.delete_collection_confirm', { collection: collection.name })
if (!ret) { return } if (!ret) { return }
try { try {
await this.$axios.$delete(`/cohort/${cohort.id}`) await this.$axios.$delete(`/collection/${collection.id}`)
this.cohorts = this.cohorts.filter(c => c.id !== cohort.id) this.collections = this.collections.filter(c => c.id !== collection.id)
} catch (e) { } catch (e) {
const err = get(e, 'response.data.errors[0].message', e) const err = get(e, 'response.data.errors[0].message', e)
this.$root.$message(this.$t(err), { color: 'error' }) this.$root.$message(this.$t(err), { color: 'error' })

View file

@ -1,48 +1,48 @@
<template lang='pug'> <template lang='pug'>
v-container v-container
v-card-title {{$t('common.places')}} v-card-title {{$t('common.places')}}
v-spacer v-spacer
v-text-field(v-model='search' v-text-field(v-model='search'
:append-icon='mdiMagnify' outlined rounded :append-icon='mdiMagnify' outlined rounded
label='Search' label='Search'
single-line hide-details) single-line hide-details)
v-card-subtitle(v-html="$t('admin.place_description')") v-card-subtitle(v-html="$t('admin.place_description')")
v-dialog(v-model='dialog' width='600' :fullscreen='$vuetify.breakpoint.xsOnly') v-dialog(v-model='dialog' width='600' :fullscreen='$vuetify.breakpoint.xsOnly')
v-card(color='secondary') v-card(color='secondary')
v-card-title {{$t('admin.edit_place')}} v-card-title {{$t('admin.edit_place')}}
v-card-text v-card-text
v-form(v-model='valid' ref='form' lazy-validation) v-form(v-model='valid' ref='form' lazy-validation)
v-text-field( v-text-field(
:rules="[$validators.required('common.name')]" :rules="[$validators.required('common.name')]"
:label="$t('common.name')" :label="$t('common.name')"
v-model='place.name' v-model='place.name'
:placeholder='$t("common.name")') :placeholder='$t("common.name")')
v-text-field( v-text-field(
:rules="[$validators.required('common.address')]" :rules="[$validators.required('common.address')]"
:label="$t('common.address')" :label="$t('common.address')"
v-model='place.address' v-model='place.address'
:placeholder='$t("common.address")') :placeholder='$t("common.address")')
v-card-actions v-card-actions
v-spacer v-spacer
v-btn(@click='dialog=false' color='warning') {{$t('common.cancel')}} v-btn(@click='dialog=false' color='warning') {{$t('common.cancel')}}
v-btn(@click='savePlace' color='primary' :loading='loading' v-btn(@click='savePlace' color='primary' :loading='loading'
:disable='!valid || loading') {{$t('common.save')}} :disable='!valid || loading') {{$t('common.save')}}
v-card-text v-card-text
v-data-table( v-data-table(
:headers='headers' :headers='headers'
:items='places' :items='places'
:hide-default-footer='places.length<5' :hide-default-footer='places.length<5'
:footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }' :footer-props='{ prevIcon: mdiChevronLeft, nextIcon: mdiChevronRight }'
:search='search') :search='search')
template(v-slot:item.actions='{item}') template(v-slot:item.actions='{item}')
v-btn(@click='editPlace(item)' color='primary' icon) v-btn(@click='editPlace(item)' color='primary' icon)
v-icon(v-text='mdiPencil') v-icon(v-text='mdiPencil')
nuxt-link(:to='`/p/${item.name}`') nuxt-link(:to='`/place/${item.name}`')
v-icon(v-text='mdiEye') v-icon(v-text='mdiEye')
</template> </template>
<script> <script>
@ -79,7 +79,7 @@ export default {
if (!this.$refs.form.validate()) return if (!this.$refs.form.validate()) return
this.loading = true this.loading = true
await this.$axios.$put('/place', this.place) await this.$axios.$put('/place', this.place)
this.updateMeta() await this.$fetch()
this.loading = false this.loading = false
this.dialog = false this.dialog = false
} }

View file

@ -76,8 +76,8 @@ sudo systemctl start gancio
> Don't be lazy and [backup]({% link install/backup.md %}) your data! > Don't be lazy and [backup]({% link install/backup.md %}) your data!
```bash ```bash
yarn global remove gancio sudo yarn global remove gancio
yarn cache clean sudo yarn cache clean
yarn global add --silent {{site.url}}/latest.tgz sudo yarn global add --silent {{site.url}}/latest.tgz
sudo systemctl restart gancio sudo systemctl restart gancio
``` ```

View file

@ -4,13 +4,15 @@
<Confirm/> <Confirm/>
<Nav/> <Nav/>
<v-main app> <v-main app>
<div class="ml-1 mb-1 mt-1" v-if='showCohorts || showBack'> <v-container fluid class='pa-0'>
<v-btn v-show='showBack' text color='primary' to='/'><v-icon v-text='mdiChevronLeft'/></v-btn> <div v-if='showCollections || showBack'>
<v-btn v-for='cohort in cohorts' text color='primary' :key='cohort.id' :to='`/g/${cohort.name}`'>{{cohort.name}}</v-btn> <v-btn class='ml-2 mt-2' v-if='showBack' outlined color='primary' to='/'><v-icon v-text='mdiChevronLeft'></v-icon></v-btn>
</div> <v-btn class='ml-2 mt-2' outlined v-for='collection in collections' color='primary' :key='collection.id' :to='`/collection/${collection.name}`'>{{collection.name}}</v-btn>
<v-fade-transition hide-on-leave> </div>
<nuxt /> <v-fade-transition hide-on-leave>
</v-fade-transition> <nuxt />
</v-fade-transition>
</v-container>
</v-main> </v-main>
<Footer/> <Footer/>
@ -35,21 +37,21 @@ export default {
} }
}, },
data () { data () {
return { cohorts: [], mdiChevronLeft } return { collections: [], mdiChevronLeft }
}, },
async fetch () { async fetch () {
this.cohorts = await this.$axios.$get('cohorts') this.collections = await this.$axios.$get('collections')
}, },
name: 'Default', name: 'Default',
components: { Nav, Snackbar, Footer, Confirm }, components: { Nav, Snackbar, Footer, Confirm },
computed: { computed: {
...mapState(['settings', 'locale']), ...mapState(['settings', 'locale']),
showBack () { showBack () {
return ['tag-tag', 'g-cohort', 'p-place', 'search', 'announcement-id'].includes(this.$route.name) return ['tag-tag', 'collection-collection', 'place-place', 'search', 'announcement-id'].includes(this.$route.name)
}, },
showCohorts () { showCollections () {
if (!this.cohorts || this.cohorts.length === 0) return false if (!this.collections || this.collections.length === 0) return false
return ['tag-tag', 'index', 'g-cohort', 'p-place'].includes(this.$route.name) return ['tag-tag', 'index', 'g-collection', 'p-place'].includes(this.$route.name)
} }
}, },
created () { created () {

View file

@ -1,9 +1,9 @@
<template lang='pug'> <template lang='pug'>
v-container.p-4.text-center v-container.pa-2.text-center
v-alert(v-if="error.statusCode === 404" type='error' :icon='mdiAlert') ¯\_()_/¯ {{error.message}} v-alert(v-if="error.statusCode === 404" type='error' :icon='mdiAlert') ¯\_()_/¯ {{error.message}}
v-alert(v-else type='error' :icon='mdiAlert') An error occurred: {{error.message}} v-alert.mb-2(v-else type='error' :icon='mdiAlert') An error occurred: {{error.message}}
nuxt-link(to='/') nuxt-link(to='/')
v-btn Back to home v-btn(outlined color='primary') Back to home
</template> </template>
<script> <script>

View file

@ -87,7 +87,8 @@
"import": "Import", "import": "Import",
"max_events": "N. max events", "max_events": "N. max events",
"label": "Label", "label": "Label",
"blobs": "Blobs" "collections": "Collections",
"close": "Close"
}, },
"login": { "login": {
"description": "By logging in you can publish new events.", "description": "By logging in you can publish new events.",
@ -233,7 +234,10 @@
"smtp_test_button": "Send a test email", "smtp_test_button": "Send a test email",
"admin_email": "Admin e-mail", "admin_email": "Admin e-mail",
"widget": "Widget", "widget": "Widget",
"wrong_domain_warning": "The baseurl configured in config.json <b>({baseurl})</b> differs from the one you're visiting <b>({url})</b>" "wrong_domain_warning": "The baseurl configured in config.json <b>({baseurl})</b> differs from the one you're visiting <b>({url})</b>",
"new_collection": "New collection",
"collections_description": "Collections are groupings of events by tags and places. They will be displayed on the home page",
"edit_collection": "Edit Collection"
}, },
"auth": { "auth": {
"not_confirmed": "Not confirmed yet…", "not_confirmed": "Not confirmed yet…",

View file

@ -217,7 +217,8 @@
"show_smtp_setup": "Paramètres d'e-mail", "show_smtp_setup": "Paramètres d'e-mail",
"smtp_test_success": "Un e-mail de test a été envoyé à {admin_email}, veuillez vérifier votre boîte de réception", "smtp_test_success": "Un e-mail de test a été envoyé à {admin_email}, veuillez vérifier votre boîte de réception",
"admin_email": "E-mail de l'administrateur", "admin_email": "E-mail de l'administrateur",
"widget": "Vignette active" "widget": "Vignette active",
"event_remove_ok": "Événement supprimé"
}, },
"oauth": { "oauth": {
"scopes": { "scopes": {

View file

@ -87,7 +87,7 @@
"import": "Importa", "import": "Importa",
"max_events": "N. massimo eventi", "max_events": "N. massimo eventi",
"label": "Etichetta", "label": "Etichetta",
"blobs": "Bolle" "collections": "Bolle"
}, },
"login": { "login": {
"description": "Entrando puoi pubblicare nuovi eventi.", "description": "Entrando puoi pubblicare nuovi eventi.",
@ -231,7 +231,11 @@
"smtp_hostname": "SMTP Hostname", "smtp_hostname": "SMTP Hostname",
"smtp_test_success": "Una mail di test è stata inviata all'indirizzo {admin_email}, controlla la tua casella di posta", "smtp_test_success": "Una mail di test è stata inviata all'indirizzo {admin_email}, controlla la tua casella di posta",
"smtp_test_button": "Invia una mail di prova", "smtp_test_button": "Invia una mail di prova",
"admin_email": "E-mail dell'admin" "admin_email": "E-mail dell'admin",
"new_collection": "Crea bolla",
"wrong_domain_warning": "Il \"baseurl\" configurato in config.json <b>({baseurl})</b> è diverso da quello che stai visitando <b>({url})</b>",
"collections_description": "Le bolle sono raggruppamenti di eventi per tag e posti.",
"edit_collection": "Modifica bolla"
}, },
"auth": { "auth": {
"not_confirmed": "Non ancora confermato…", "not_confirmed": "Non ancora confermato…",

View file

@ -13,7 +13,7 @@ module.exports = {
{ charset: 'utf-8' }, { charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' } { name: 'viewport', content: 'width=device-width, initial-scale=1' }
], ],
link: [{ rel: 'icon', type: 'image/png', href: '/logo.png' }], link: [{ rel: 'icon', type: 'image/png', href: config.baseurl + '/logo.png' }],
script: [{ src: '/gancio-events.es.js', async: true, body: true }], script: [{ src: '/gancio-events.es.js', async: true, body: true }],
}, },
dev: isDev, dev: isDev,
@ -68,6 +68,8 @@ module.exports = {
const Event = require('./server/api/models/event') const Event = require('./server/api/models/event')
const events = await Event.findAll({where: { is_visible: true }}) const events = await Event.findAll({where: { is_visible: true }})
return events.map(e => `/event/${e.slug}`) return events.map(e => `/event/${e.slug}`)
} else {
return []
} }
} }
}, },

View file

@ -1,6 +1,6 @@
{ {
"name": "gancio", "name": "gancio",
"version": "1.5.0-rc.2", "version": "1.5.0-rc.5",
"description": "A shared agenda for local communities", "description": "A shared agenda for local communities",
"author": "lesion", "author": "lesion",
"scripts": { "scripts": {
@ -69,14 +69,14 @@
"vue": "^2.6.14", "vue": "^2.6.14",
"vue-i18n": "^8.26.7", "vue-i18n": "^8.26.7",
"vue-template-compiler": "^2.6.14", "vue-template-compiler": "^2.6.14",
"vuetify": "npm:@vuetify/nightly@dev", "vuetify": "2.6.6",
"winston": "^3.7.2", "winston": "^3.7.2",
"winston-daily-rotate-file": "^4.7.1", "winston-daily-rotate-file": "^4.7.1",
"yargs": "^17.5.0" "yargs": "^17.5.0"
}, },
"devDependencies": { "devDependencies": {
"@nuxtjs/vuetify": "^1.12.3", "@nuxtjs/vuetify": "^1.12.3",
"jest": "^28.1.0", "jest": "^28.1.1",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"pug": "^3.0.2", "pug": "^3.0.2",
"pug-plain-loader": "^1.1.0", "pug-plain-loader": "^1.1.0",
@ -109,5 +109,6 @@
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://framagit.org/les/gancio" "url": "https://framagit.org/les/gancio"
} },
"snyk": true
} }

View file

@ -26,10 +26,10 @@
v-tab-item v-tab-item
Places Places
//- Cohorts //- Collections
v-tab {{$t('common.blobs')}} v-tab {{$t('common.collections')}}
v-tab-item v-tab-item
Cohorts Collections
//- EVENTS //- EVENTS
v-tab v-tab
@ -66,7 +66,7 @@ export default {
Users: () => import(/* webpackChunkName: "admin" */'../components/admin/Users'), Users: () => import(/* webpackChunkName: "admin" */'../components/admin/Users'),
Events: () => import(/* webpackChunkName: "admin" */'../components/admin/Events'), Events: () => import(/* webpackChunkName: "admin" */'../components/admin/Events'),
Places: () => import(/* webpackChunkName: "admin" */'../components/admin/Places'), Places: () => import(/* webpackChunkName: "admin" */'../components/admin/Places'),
Cohorts: () => import(/* webpackChunkName: "admin" */'../components/admin/Cohorts'), Collections: () => import(/* webpackChunkName: "admin" */'../components/admin/Collections'),
Federation: () => import(/* webpackChunkName: "admin" */'../components/admin/Federation.vue'), Federation: () => import(/* webpackChunkName: "admin" */'../components/admin/Federation.vue'),
Moderation: () => import(/* webpackChunkName: "admin" */'../components/admin/Moderation.vue'), Moderation: () => import(/* webpackChunkName: "admin" */'../components/admin/Moderation.vue'),
Announcement: () => import(/* webpackChunkName: "admin" */'../components/admin/Announcement.vue'), Announcement: () => import(/* webpackChunkName: "admin" */'../components/admin/Announcement.vue'),

View file

@ -53,7 +53,7 @@
chips small-chips multiple deletable-chips hide-no-data hide-selected persistent-hint chips small-chips multiple deletable-chips hide-no-data hide-selected persistent-hint
cache-items cache-items
@input.native='searchTags' @input.native='searchTags'
:delimiters="[',', ';']" :delimiters="[',', ';', '#']"
:items="tags" :items="tags"
:menu-props="{ maxWidth: 400, eager: true }" :menu-props="{ maxWidth: 400, eager: true }"
:label="$t('common.tags')") :label="$t('common.tags')")

View file

@ -0,0 +1,43 @@
<template>
<v-container class='px-0' fluid>
<h1 class='d-block text-h3 font-weight-black text-center align-center text-uppercase mt-10 mb-12 mx-auto w-100 text-underline'><u>{{collection}}</u></h1>
<!-- Events -->
<div class='mb-2 mt-1 pl-1 pl-sm-2' id="events">
<Event :event='event' v-for='(event, idx) in events' :lazy='idx>2' :key='event.id'></Event>
</div>
</v-container>
</template>
<script>
import { mapState } from 'vuex'
import Event from '@/components/Event'
export default {
name: 'Collection',
components: { Event },
head () {
const title = `${this.settings.title} - ${this.collection}`
return {
title,
link: [
{ rel: 'alternate', type: 'application/rss+xml', title, href: this.settings.baseurl + `/feed/rss/collection/${this.collection}` },
{ rel: 'alternate', type: 'text/calendar', title, href: this.settings.baseurl + `/feed/ics/collection/${this.collection}` }
]
}
},
computed: mapState(['settings']),
async asyncData ({ $axios, params, error }) {
try {
const collection = params.collection
const events = await $axios.$get(`/collections/${collection}`)
return { events, collection }
} catch (e) {
console.error(e)
error({ statusCode: 400, message: 'Error!' })
}
}
}
</script>

View file

@ -30,7 +30,7 @@ v-container#event.pa-0.pa-sm-2
.text-h6.p-location.h-adr(itemprop="location" itemscope itemtype="https://schema.org/Place") .text-h6.p-location.h-adr(itemprop="location" itemscope itemtype="https://schema.org/Place")
v-icon(v-text='mdiMapMarker') v-icon(v-text='mdiMapMarker')
b.vcard.ml-2.p-name(itemprop="name") {{event.place && event.place.name}} nuxt-link.vcard.ml-2.p-name.text-decoration-none(itemprop="name" :to='`/place/${event.place.name}`') {{event.place && event.place.name}}
.text-subtitle-1.p-street-address(itemprop='address') {{event.place && event.place.address}} .text-subtitle-1.p-street-address(itemprop='address') {{event.place && event.place.address}}
//- tags, hashtags //- tags, hashtags

View file

@ -1,31 +0,0 @@
<template>
<v-container id='home' fluid>
<h1 class='d-block text-h3 font-weight-black text-center align-center text-uppercase mt-10 mb-12 mx-auto w-100 text-underline'><u>{{cohort}}</u></h1>
<!-- Events -->
<div class='mb-2 mt-1 pl-1 pl-sm-2' id="events">
<Event :event='event' v-for='(event, idx) in events' :lazy='idx>2' :key='event.id'></Event>
</div>
</v-container>
</template>
<script>
import Event from '@/components/Event'
export default {
name: 'Tag',
components: { Event },
async asyncData ({ $axios, params, error }) {
try {
const cohort = params.cohort
const events = await $axios.$get(`/cohorts/${cohort}`)
return { events, cohort }
} catch (e) {
console.error(e)
error({ statusCode: 400, message: 'Error!' })
}
}
}
</script>

View file

@ -1,8 +1,8 @@
<template lang="pug"> <template lang="pug">
v-container#home(fluid) v-container.pa-0
//- Announcements //- Announcements
#announcements.mx-1.mt-1(v-if='announcements.length') #announcements.ma-2(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')
//- Calendar and search bar //- Calendar and search bar
@ -68,7 +68,8 @@ export default {
{ property: 'og:image', content: this.settings.baseurl + '/logo.png' } { property: 'og:image', content: this.settings.baseurl + '/logo.png' }
], ],
link: [ link: [
{ rel: 'alternate', type: 'application/rss+xml', title: this.settings.title, href: this.settings.baseurl + '/feed/rss' } { rel: 'alternate', type: 'application/rss+xml', title: this.settings.title, href: this.settings.baseurl + '/feed/rss' },
{ rel: 'alternate', type: 'text/calendar', title: this.settings.title, href: this.settings.baseurl + '/feed/ics' }
] ]
} }
}, },

View file

@ -1,7 +1,6 @@
<template> <template>
<v-container id='home' fluid> <v-container class='px-0' fluid>
<h1 class='d-block text-h4 font-weight-black text-center text-uppercase mt-10 mx-auto w-100 text-underline'><u>{{place.name}}</u></h1>
<h1 class='d-block text-h4 font-weight-black text-center text-uppercase mt-5 mx-auto w-100 text-underline'><u>{{place.name}}</u></h1>
<span class="d-block text-subtitle text-center w-100 mb-14">{{place.address}}</span> <span class="d-block text-subtitle text-center w-100 mb-14">{{place.address}}</span>
<!-- Events --> <!-- Events -->
@ -12,15 +11,27 @@
</template> </template>
<script> <script>
import { mapState } from 'vuex'
import Event from '@/components/Event' import Event from '@/components/Event'
export default { export default {
name: 'Tag', name: 'Place',
components: { Event }, components: { Event },
head () {
const title = `${this.settings.title} - ${this.place.name}`
return {
title,
link: [
{ rel: 'alternate', type: 'application/rss+xml', title, href: this.settings.baseurl + `/feed/rss/place/${this.place.name}` },
{ rel: 'alternate', type: 'text/calendar', title, href: this.settings.baseurl + `/feed/ics/place/${this.place.name}` }
]
}
},
computed: mapState(['settings']),
asyncData ({ $axios, params, error }) { asyncData ({ $axios, params, error }) {
try { try {
const place = params.place const place = params.place
return $axios.$get(`/place/${place}/events`) return $axios.$get(`/place/${place}`)
} catch (e) { } catch (e) {
error({ statusCode: 400, message: 'Error!' }) error({ statusCode: 400, message: 'Error!' })
} }

View file

@ -1,7 +1,7 @@
<template> <template>
<v-container id='home' fluid> <v-container class='px-0' fluid>
<h1 class='d-block text-h3 font-weight-black text-center align-center text-uppercase mt-5 mb-16 mx-auto w-100 text-underline'><u>{{tag}}</u></h1> <h1 class='d-block text-h3 font-weight-black text-center text-uppercase mt-10 mb-16 mx-auto w-100 text-underline'><u>{{tag}}</u></h1>
<!-- Events --> <!-- Events -->
<div class="mb-2 mt-1 pl-1 pl-sm-2" id="events"> <div class="mb-2 mt-1 pl-1 pl-sm-2" id="events">
@ -11,15 +11,28 @@
</template> </template>
<script> <script>
import { mapState } from 'vuex'
import Event from '@/components/Event' import Event from '@/components/Event'
export default { export default {
name: 'Tag', name: 'Tag',
components: { Event }, components: { Event },
head ({ $route }) {
const tag = $route.params.tag
const title = `${this.settings.title} #${tag}`
return {
title,
link: [
{ rel: 'alternate', type: 'application/rss+xml', title, href: this.settings.baseurl + `/feed/rss/tag/${tag}` },
{ rel: 'alternate', type: 'text/calendar', title, href: this.settings.baseurl + `/feed/ics/tag/${tag}` }
]
}
},
computed: mapState(['settings']),
async asyncData ({ $axios, params, error }) { async asyncData ({ $axios, params, error }) {
try { try {
const tag = params.tag const tag = params.tag
const events = await $axios.$get(`/events?tags=${tag}`) const events = await $axios.$get(`/tag/${tag}`)
return { events, tag } return { events, tag }
} catch (e) { } catch (e) {
error({ statusCode: 400, message: 'Error!' }) error({ statusCode: 400, message: 'Error!' })

View file

@ -1,41 +1,42 @@
const Cohort = require('../models/cohort') const Collection = require('../models/collection')
const Filter = require('../models/filter') const Filter = require('../models/filter')
const Event = require('../models/event') const Event = require('../models/event')
const Tag = require('../models/tag') const Tag = require('../models/tag')
const Place = require('../models/place') 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 { sequelize } = require('../models/index')
const { Op, Sequelize } = require('sequelize') const { Op, Sequelize } = require('sequelize')
const cohortController = { const collectionController = {
async getAll (req, res) { async getAll (req, res) {
const withFilters = req.query.withFilters const withFilters = req.query.withFilters
let cohorts let collections
if (withFilters) { if (withFilters) {
cohorts = await Cohort.findAll({ include: [Filter] }) collections = await Collection.findAll({ include: [Filter] })
} else { } else {
cohorts = await Cohort.findAll() collections = await Collection.findAll()
} }
return res.json(cohorts) return res.json(collections)
}, },
// return events from cohort // return events from collection
async getEvents (req, res) { async getEvents (req, res) {
const format = req.params.format || 'json'
const name = req.params.name const name = req.params.name
const cohort = await Cohort.findOne({ where: { name } }) const collection = await Collection.findOne({ where: { name } })
if (!cohort) { if (!collection) {
return res.sendStatus(404) return res.sendStatus(404)
} }
const filters = await Filter.findAll({ where: { cohortId: cohort.id } }) const filters = await Filter.findAll({ where: { collectionId: collection.id } })
if (!filters.length) {
return res.json([])
}
const start = dayjs().unix() const start = dayjs().unix()
const where = { const where = {
// do not include parent recurrent event // do not include parent recurrent event
@ -45,24 +46,16 @@ const cohortController = {
is_visible: true, is_visible: true,
// [Op.or]: { // [Op.or]: {
start_datetime: { [Op.gte]: start }, start_datetime: { [Op.gte]: start },
// end_datetime: { [Op.gte]: start } // end_datetime: { [Op.gte]: start }
// } // }
} }
// if (!show_recurrent) {
// where.parentId = null
// }
// if (end) {
// where.start_datetime = { [Op.lte]: end }
// }
const replacements = [] const replacements = []
const ors = [] const ors = []
filters.forEach(f => { filters.forEach(f => {
if (f.tags && f.tags.length) { if (f.tags && f.tags.length) {
const tags = Sequelize.fn('EXISTS', Sequelize.literal('SELECT 1 FROM event_tags WHERE "event_tags"."eventId"="event".id AND "tagTag" in (?)')) const tags = Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=event.id AND ${Col('tagTag')} in (?)`))
replacements.push(f.tags) replacements.push(f.tags)
if (f.places && f.places.length) { if (f.places && f.places.length) {
ors.push({ [Op.and]: [ { placeId: f.places.map(p => p.id) },tags] }) ors.push({ [Op.and]: [ { placeId: f.places.map(p => p.id) },tags] })
@ -74,34 +67,18 @@ const cohortController = {
} }
}) })
// if (tags && places) { where[Op.and] = { [Op.or]: ors }
// where[Op.or] = {
// placeId: places ? places.split(',') : [],
// // '$tags.tag$': Sequelize.literal(`EXISTS (SELECT 1 FROM event_tags WHERE tagTag in ( ${Sequelize.QueryInterface.escape(tags)} ) )`)
// }
// } else if (tags) {
// where[Op.and] = Sequelize.literal(`EXISTS (SELECT 1 FROM event_tags WHERE event_tags.eventId=event.id AND tagTag in (?))`)
// replacements.push(tags)
// } else if (places) {
// where.placeId = places.split(',')
// }
if (ors.length) {
where[Op.or] = ors
}
const events = await Event.findAll({ const events = await Event.findAll({
logging: console.log,
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'], order: ['start_datetime'],
include: [ include: [
// { model: Resource, required: false, attributes: ['id'] },
{ {
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: [] }
}, },
@ -125,43 +102,43 @@ const cohortController = {
}, },
async add (req, res) { async add (req, res) {
const cohortDetail = { const collectionDetail = {
name: req.body.name, name: req.body.name,
isActor: true, isActor: true,
isTop: true isTop: true
} }
// TODO: validation // TODO: validation
log.info('Create cohort: ' + req.body.name) log.info('Create collection: ' + req.body.name)
const cohort = await Cohort.create(cohortDetail) const collection = await Collection.create(collectionDetail)
res.json(cohort) res.json(collection)
}, },
async remove (req, res) { async remove (req, res) {
const cohort_id = req.params.id const collection_id = req.params.id
log.info('Remove cohort', cohort_id) log.info('Remove collection', collection_id)
try { try {
const cohort = await Cohort.findByPk(cohort_id) const collection = await Collection.findByPk(collection_id)
await cohort.destroy() await collection.destroy()
res.sendStatus(200) res.sendStatus(200)
} catch (e) { } catch (e) {
log.error('Remove cohort failed:', e) log.error('Remove collection failed:', e)
res.sendStatus(404) res.sendStatus(404)
} }
}, },
async getFilters (req, res) { async getFilters (req, res) {
const cohortId = req.params.cohort_id const collectionId = req.params.collection_id
const filters = await Filter.findAll({ where: { cohortId } }) const filters = await Filter.findAll({ where: { collectionId } })
return res.json(filters) return res.json(filters)
}, },
async addFilter (req, res) { async addFilter (req, res) {
const cohortId = req.body.cohortId const collectionId = req.body.collectionId
const tags = req.body.tags const tags = req.body.tags
const places = req.body.places const places = req.body.places
try { try {
const filter = await Filter.create({ cohortId, tags, places }) const 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))
@ -188,4 +165,4 @@ const cohortController = {
module.exports = cohortController module.exports = collectionController

View file

@ -8,7 +8,7 @@ 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 Event = require('../models/event') const Event = require('../models/event')
const Resource = require('../models/resource') const Resource = require('../models/resource')
const Tag = require('../models/tag') const Tag = require('../models/tag')
@ -17,6 +17,7 @@ const Notification = require('../models/notification')
const APUser = require('../models/ap_user') const APUser = require('../models/ap_user')
const exportController = require('./export') const exportController = require('./export')
const tagController = require('./tag')
const log = require('../../log') const log = require('../../log')
@ -29,8 +30,8 @@ const eventController = {
order: [[Sequelize.col('w'), 'DESC']], order: [[Sequelize.col('w'), 'DESC']],
where: { where: {
[Op.or]: [ [Op.or]: [
{ name: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), 'LIKE', '%' + search + '%' )}, Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), 'LIKE', '%' + search + '%' ),
{ address: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('address')), 'LIKE', '%' + search + '%')}, Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('address')), 'LIKE', '%' + search + '%')
] ]
}, },
attributes: [['name', 'label'], 'address', 'id', [Sequelize.cast(Sequelize.fn('COUNT', Sequelize.col('events.placeId')),'INTEGER'), 'w']], attributes: [['name', 'label'], 'address', 'id', [Sequelize.cast(Sequelize.fn('COUNT', Sequelize.col('events.placeId')),'INTEGER'), 'w']],
@ -91,7 +92,7 @@ const eventController = {
[ [
{ 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 "event_tags"."eventId"="event".id AND "tagTag" = ?')) Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=${Col('event.id')} AND LOWER(${Col('tagTag')}) = ?`))
] ]
} }
@ -105,7 +106,7 @@ const eventController = {
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: [] }
}, },
@ -247,7 +248,6 @@ const eventController = {
order: [['start_datetime', 'DESC'], ['id', 'DESC']] order: [['start_datetime', 'DESC'], ['id', 'DESC']]
}) })
// TODO: also check if event is mine
if (event && (event.is_visible || is_admin)) { if (event && (event.is_visible || is_admin)) {
event = event.get() event = event.get()
event.next = next && (next.slug || next.id) event.next = next && (next.slug || next.id)
@ -278,7 +278,7 @@ const eventController = {
return res.sendStatus(404) return res.sendStatus(404)
} }
if (!res.locals.user.is_admin && res.locals.user.id !== event.userId) { if (!res.locals.user.is_admin && res.locals.user.id !== event.userId) {
log.warn(`Someone unallowed is trying to confirm -> "${event.title} `) log.warn(`Someone not allowed is trying to confirm -> "${event.title} `)
return res.sendStatus(403) return res.sendStatus(403)
} }
@ -304,6 +304,7 @@ const eventController = {
const event = await Event.findByPk(id) const event = await Event.findByPk(id)
if (!event) { return req.sendStatus(404) } if (!event) { return req.sendStatus(404) }
if (!res.locals.user.is_admin && res.locals.user.id !== event.userId) { if (!res.locals.user.is_admin && res.locals.user.id !== event.userId) {
log.warn(`Someone not allowed is trying to unconfirm -> "${event.title} `)
return res.sendStatus(403) return res.sendStatus(403)
} }
@ -317,7 +318,7 @@ const eventController = {
}, },
/** get all unconfirmed events */ /** get all unconfirmed events */
async getUnconfirmed (req, res) { async getUnconfirmed (_req, res) {
try { try {
const events = await Event.findAll({ const events = await Event.findAll({
where: { where: {
@ -391,7 +392,7 @@ const eventController = {
if (body.place_id) { if (body.place_id) {
place = await Place.findByPk(body.place_id) place = await Place.findByPk(body.place_id)
} else { } else {
place = await Place.findOne({ where: { name: body.place_name.trim() }}) place = await Place.findOne({ where: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('name')), Op.eq, body.place_name.trim().toLocaleLowerCase() )})
if (!place) { if (!place) {
if (!body.place_address || !body.place_name) { if (!body.place_address || !body.place_name) {
return res.status(400).send(`place_id or place_name and place_address required`) return res.status(400).send(`place_id or place_name and place_address required`)
@ -427,6 +428,7 @@ const eventController = {
height: req.file.height, height: req.file.height,
width: req.file.width, width: req.file.width,
name: body.image_name || body.title || '', name: body.image_name || body.title || '',
size: req.file.size || 0,
focalpoint: [parseFloat(focalpoint[0]), parseFloat(focalpoint[1])] focalpoint: [parseFloat(focalpoint[0]), parseFloat(focalpoint[1])]
}] }]
} else { } else {
@ -438,11 +440,10 @@ const eventController = {
await event.setPlace(place) await event.setPlace(place)
// create/assign tags // create/assign tags
let tags = []
if (body.tags) { if (body.tags) {
body.tags = body.tags.map(t => t.trim()) tags = await tagController._findOrCreate(body.tags)
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true }) await event.setTags(tags)
const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } })
await event.addTags(tags)
} }
// associate user to event and reverse // associate user to event and reverse
@ -452,7 +453,7 @@ const eventController = {
} }
event = event.get() event = event.get()
event.tags = body.tags event.tags = tags.map(t => t.tag)
event.place = place event.place = place
// return created event to the client // return created event to the client
res.json(event) res.json(event)
@ -520,6 +521,7 @@ const eventController = {
height: req.file.height, height: req.file.height,
width: req.file.width, width: req.file.width,
name: body.image_name || body.title || '', name: body.image_name || body.title || '',
size: req.file.size || 0,
focalpoint: [parseFloat(focalpoint[0].slice(0, 6)), parseFloat(focalpoint[1].slice(0, 6))] focalpoint: [parseFloat(focalpoint[0].slice(0, 6)), parseFloat(focalpoint[1].slice(0, 6))]
}] }]
} else if (!body.image) { } else if (!body.image) {
@ -532,12 +534,14 @@ const eventController = {
}) })
await event.setPlace(place) await event.setPlace(place)
await event.setTags([])
// create/assign tags
let tags = []
if (body.tags) { if (body.tags) {
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true }) tags = await tagController._findOrCreate(body.tags)
const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } }) await event.setTags(tags)
await event.addTags(tags)
} }
const newEvent = await Event.findByPk(event.id, { include: [Tag, Place] }) const newEvent = await Event.findByPk(event.id, { include: [Tag, Place] })
res.json(newEvent) res.json(newEvent)
@ -584,60 +588,85 @@ const eventController = {
} }
}, },
async _select ({ start, end, tags, places, show_recurrent, max }) { /**
* Method to search for events with pagination and filtering
* @returns
*/
async _select ({
start = dayjs().unix(),
end,
tags,
places,
show_recurrent,
limit,
page,
older }) {
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,
[Op.or]: { [Op.or]: {
start_datetime: { [Op.gte]: start }, start_datetime: { [older ? Op.lte : Op.gte]: start },
end_datetime: { [Op.gte]: start } end_datetime: { [older ? Op.lte : Op.gte]: start }
} }
} }
// include recurrent events?
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 = { [older ? Op.gte : Op.lte]: end }
}
// normalize tags
if (tags) {
tags = tags.split(',').map(t => t.trim().toLocaleLowerCase())
} }
const replacements = [] const replacements = []
if (tags && places) { if (tags && places) {
where[Op.or] = { where[Op.and] = [
placeId: places ? places.split(',') : [], { placeId: places ? places.split(',') : []},
// '$tags.tag$': Sequelize.literal(`EXISTS (SELECT 1 FROM event_tags WHERE tagTag in ( ${Sequelize.QueryInterface.escape(tags)} ) )`) Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=event.id AND LOWER(${Col('tagTag')}) in (?)`))
} ]
replacements.push(tags)
} else if (tags) { } else if (tags) {
// where[Op.and] = Sequelize.literal(`EXISTS (SELECT 1 FROM event_tags WHERE eventId=event.id AND tagTag in (?))`) where[Op.and] = Sequelize.fn('EXISTS', Sequelize.literal(`SELECT 1 FROM event_tags WHERE ${Col('event_tags.eventId')}=event.id AND LOWER(${Col('tagTag')}) in (?)`))
where[Op.and] = Sequelize.fn('EXISTS', Sequelize.literal('SELECT 1 FROM event_tags WHERE "event_tags"."eventId"="event".id AND "tagTag" in (?)'))
replacements.push(tags) replacements.push(tags)
} else if (places) { } else if (places) {
where.placeId = places.split(',') where.placeId = places.split(',')
} }
let pagination = {}
if (limit) {
pagination = {
limit,
offset: limit * page,
}
}
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', 'description', 'resources', 'recurrent', 'placeId', 'parentId']
}, },
order: ['start_datetime'], order: [['start_datetime', older ? 'DESC' : 'ASC' ]],
include: [ include: [
{ model: Resource, required: false, attributes: ['id'] },
{ {
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'] } { model: Place, required: true, attributes: ['id', 'name', 'address'] }
], ],
limit: max, ...pagination,
replacements replacements
}).catch(e => { }).catch(e => {
log.error('[EVENT]', e) log.error('[EVENT]', e)
@ -660,13 +689,15 @@ const eventController = {
const end = req.query.end const end = req.query.end
const tags = req.query.tags const tags = req.query.tags
const places = req.query.places const places = req.query.places
const max = req.query.max const limit = req.query.max
const page = req.query.page = 0
const older = req.query.older || false
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, max start, end, places, tags, show_recurrent, limit, page, older
})) }))
}, },

View file

@ -10,7 +10,7 @@ const ics = require('ics')
const exportController = { const exportController = {
async export (req, res) { async export (req, res) {
const type = req.params.type const format = req.params.format
const tags = req.query.tags const tags = req.query.tags
const places = req.query.places const places = req.query.places
const show_recurrent = !!req.query.show_recurrent const show_recurrent = !!req.query.show_recurrent
@ -43,7 +43,7 @@ const exportController = {
attributes: { exclude: ['is_visible', 'recurrent', 'createdAt', 'likes', 'boost', 'userId', 'placeId'] }, attributes: { exclude: ['is_visible', 'recurrent', 'createdAt', 'likes', 'boost', 'userId', 'placeId'] },
where: { where: {
is_visible: true, is_visible: true,
recurrent: { [Op.eq]: null }, recurrent: null,
start_datetime: { [Op.gte]: yesterday }, start_datetime: { [Op.gte]: yesterday },
...where ...where
}, },
@ -58,7 +58,7 @@ const exportController = {
{ model: Place, attributes: ['name', 'id', 'address'] }] { model: Place, attributes: ['name', 'id', 'address'] }]
}) })
switch (type) { switch (format) {
case 'rss': case 'rss':
case 'feed': case 'feed':
return exportController.feed(req, res, events.slice(0, 20)) return exportController.feed(req, res, events.slice(0, 20))
@ -69,10 +69,10 @@ const exportController = {
} }
}, },
feed (_req, res, events) { feed (_req, res, events, title = res.locals.settings.title, link = `${res.locals.settings.baseurl}/feed/rss`) {
const settings = res.locals.settings const settings = res.locals.settings
res.type('application/rss+xml; charset=UTF-8') res.type('application/rss+xml; charset=UTF-8')
res.render('feed/rss.pug', { events, settings, moment }) res.render('feed/rss.pug', { events, settings, moment, title, link })
}, },
/** /**

View file

@ -1,22 +1,36 @@
const dayjs = require('dayjs')
const Place = require('../models/place') const Place = require('../models/place')
const Event = require('../models/event') const Event = require('../models/event')
const eventController = require('./event') const eventController = require('./event')
const exportController = require('./export')
const log = require('../../log') const log = require('../../log')
const { Op, where, col, fn, cast } = require('sequelize') const { Op, where, col, fn, cast } = require('sequelize')
module.exports = { module.exports = {
async getEvents (req, res) { async getEvents (req, res) {
const name = req.params.placeName const placeName = req.params.placeName
const place = await Place.findOne({ where: { name }}) const place = await Place.findOne({ where: { name: placeName }})
if (!place) { if (!place) {
log.warn(`Place ${name} not found`) log.warn(`Place ${placeName} not found`)
return res.sendStatus(404) return res.sendStatus(404)
} }
const start = dayjs().unix()
const events = await eventController._select({ start, places: `${place.id}`, show_recurrent: true})
return res.json({ events, place }) const format = req.params.format || 'json'
log.debug(`Events for place: ${placeName}`)
const events = await eventController._select({ places: String(place.id), show_recurrent: true })
switch (format) {
case 'rss':
return exportController.feed(req, res, events,
`${res.locals.settings.title} - Place @${place.name}`,
`${res.locals.settings.baseurl}/feed/rss/place/${place.name}`)
case 'ics':
return exportController.ics(req, res, events)
default:
return res.json({ events, place })
}
}, },
@ -36,7 +50,7 @@ module.exports = {
return res.json(places) return res.json(places)
}, },
async get (req, res) { async search (req, res) {
const search = req.query.search.toLocaleLowerCase() const search = req.query.search.toLocaleLowerCase()
const places = await Place.findAll({ const places = await Place.findAll({
order: [[cast(fn('COUNT', col('events.placeId')),'INTEGER'), 'DESC']], order: [[cast(fn('COUNT', col('events.placeId')),'INTEGER'), 'DESC']],
@ -49,7 +63,9 @@ module.exports = {
attributes: ['name', 'address', 'id'], attributes: ['name', 'address', 'id'],
include: [{ model: Event, where: { is_visible: true }, required: true, attributes: [] }], include: [{ model: Event, where: { is_visible: true }, required: true, attributes: [] }],
group: ['place.id'], group: ['place.id'],
raw: true raw: true,
limit: 10,
subQuery: false
}) })
// TOFIX: don't know why limit does not work // TOFIX: don't know why limit does not work

View file

@ -1,31 +1,60 @@
const dayjs = require('dayjs')
const Tag = require('../models/tag') const Tag = require('../models/tag')
const Event = require('../models/event') const Event = require('../models/event')
const eventController = require('./event')
const Sequelize = require('sequelize') const { where, fn, col, Op } = require('sequelize')
const exportController = require('./export')
module.exports = { module.exports = {
// async getEvents (req, res) {
// const name = req.params.placeName
// const place = await Place.findOne({ where: { name }})
// if (!place) {
// log.warn(`Place ${name} not found`)
// return res.sendStatus(404)
// }
// const start = dayjs().unix()
// const events = await eventController._select({ start, places: `${place.id}`, show_recurrent: true})
// return res.json({ events, place }) async _findOrCreate (tags) {
// }, // trim tags
const trimmedTags = tags.map(t => t.trim())
const lowercaseTags = trimmedTags.map(t => t.toLocaleLowerCase())
async get (req, res) { // search for already existing tags (tag is the same as TaG)
const existingTags = await Tag.findAll({ where: { [Op.and]: where(fn('LOWER', col('tag')), { [Op.in]: lowercaseTags }) } })
const lowercaseExistingTags = existingTags.map(t => t.tag.toLocaleLowerCase())
const remainingTags = trimmedTags.filter(t => ! lowercaseExistingTags.includes(t.toLocaleLowerCase()))
// create remaining tags (cannot use updateOnDuplicate or manage conflicts)
return [].concat(
existingTags,
await Tag.bulkCreate(remainingTags.map(t => ({ tag: t })))
)
},
// /feed/rss/tag/:tagname
// /feed/ics/tag/:tagname
// /feed/json/tag/:tagname
// tag/:tag
async getEvents (req, res) {
const eventController = require('./event')
const format = req.params.format || 'json'
const tags = req.params.tag
const events = await eventController._select({ tags: tags.toLocaleLowerCase(), show_recurrent: true })
switch (format) {
case 'rss':
return exportController.feed(req, res, events,
`${res.locals.settings.title} - Tag #${tags}`,
`${res.locals.settings.baseurl}/feed/rss/tag/${tags}`)
case 'ics':
return exportController.ics(req, res, events)
default:
return res.json(events)
}
},
/**
* search for tags by query string
* sorted by usage
*/
async search (req, res) {
const search = req.query.search const search = req.query.search
console.error(search)
const tags = await Tag.findAll({ const tags = await Tag.findAll({
order: [[Sequelize.fn('COUNT', Sequelize.col('tag.tag')), 'DESC']], order: [[fn('COUNT', col('tag.tag')), 'DESC']],
attributes: ['tag'], attributes: ['tag'],
where: { where: {
tag: Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('tag')), 'LIKE', '%' + search + '%'), tag: where(fn('LOWER', col('tag')), 'LIKE', '%' + search + '%'),
}, },
include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }], include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }],
group: ['tag.tag'], group: ['tag.tag'],
@ -35,13 +64,4 @@ module.exports = {
return res.json(tags.map(t => t.tag)) return res.json(tags.map(t => t.tag))
} }
// async getPlaces (req, res) {
// const search = req.params.search
// const places = await Place.findAll({ where: {
// [Op.or]: [
// { name: }
// ]
// }})
// }
} }

View file

@ -33,7 +33,7 @@ if (config.status !== 'READY') {
const resourceController = require('./controller/resource') const resourceController = require('./controller/resource')
const oauthController = require('./controller/oauth') const oauthController = require('./controller/oauth')
const announceController = require('./controller/announce') const announceController = require('./controller/announce')
const cohortController = require('./controller/cohort') const collectionController = require('./controller/collection')
const helpers = require('../helpers') const helpers = require('../helpers')
const storage = require('./storage') const storage = require('./storage')
const upload = multer({ storage }) const upload = multer({ storage })
@ -58,8 +58,8 @@ if (config.status !== 'READY') {
} }
``` ```
*/ */
api.get('/ping', (req, res) => res.sendStatus(200)) api.get('/ping', (_req, res) => res.sendStatus(200))
api.get('/user', isAuth, (req, res) => res.json(res.locals.user)) api.get('/user', isAuth, (_req, res) => res.json(res.locals.user))
api.post('/user/recover', userController.forgotPassword) api.post('/user/recover', userController.forgotPassword)
@ -88,9 +88,11 @@ if (config.status !== 'READY') {
* @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 {array} [tags] - List of tags * @param {array} [tags] - List of tags
* @param {array} [places] - List of places * @param {array} [places] - List of places id
* @param {integer} [max] - Max events * @param {integer} [max] - Limit events
* @param {boolean} [show_recurrent] - Show also recurrent events (default: as choosen in admin settings) * @param {boolean} [show_recurrent] - Show also recurrent events (default: as choosen in admin settings)
* @param {integer} [page] - Pagination
* @param {boolean} [older] - select <= start instead of >=
* @example ***Example*** * @example ***Example***
* [https://demo.gancio.org/api/events](https://demo.gancio.org/api/events) * [https://demo.gancio.org/api/events](https://demo.gancio.org/api/events)
* [usage example](https://framagit.org/les/gancio/-/blob/master/webcomponents/src/GancioEvents.svelte#L18-42) * [usage example](https://framagit.org/les/gancio/-/blob/master/webcomponents/src/GancioEvents.svelte#L18-42)
@ -131,9 +133,6 @@ if (config.status !== 'READY') {
// get tags/places // get tags/places
api.get('/event/meta', eventController.searchMeta) api.get('/event/meta', eventController.searchMeta)
// get unconfirmed events
api.get('/event/unconfirmed', isAdmin, eventController.getUnconfirmed)
// add event notification TODO // add event notification TODO
api.post('/event/notification', eventController.addNotification) api.post('/event/notification', eventController.addNotification)
api.delete('/event/notification/:code', eventController.delNotification) api.delete('/event/notification/:code', eventController.delNotification)
@ -142,7 +141,10 @@ if (config.status !== 'READY') {
api.post('/settings/logo', isAdmin, multer({ dest: config.upload_path }).single('logo'), settingsController.setLogo) api.post('/settings/logo', isAdmin, multer({ dest: config.upload_path }).single('logo'), settingsController.setLogo)
api.post('/settings/smtp', isAdmin, settingsController.testSMTP) api.post('/settings/smtp', isAdmin, settingsController.testSMTP)
// confirm event // get unconfirmed events
api.get('/event/unconfirmed', isAdmin, eventController.getUnconfirmed)
// [un]confirm event
api.put('/event/confirm/:event_id', isAuth, eventController.confirm) api.put('/event/confirm/:event_id', isAuth, eventController.confirm)
api.put('/event/unconfirm/:event_id', isAuth, eventController.unconfirm) api.put('/event/unconfirm/:event_id', isAuth, eventController.unconfirm)
@ -153,12 +155,13 @@ if (config.status !== 'READY') {
api.get('/export/:type', cors, exportController.export) api.get('/export/:type', cors, exportController.export)
api.get('/place/:placeName/events', cors, placeController.getEvents)
api.get('/place/all', isAdmin, placeController.getAll) api.get('/place/all', isAdmin, placeController.getAll)
api.get('/place', cors, placeController.get) api.get('/place/:placeName', cors, placeController.getEvents)
api.get('/place', cors, placeController.search)
api.put('/place', isAdmin, placeController.updatePlace) api.put('/place', isAdmin, placeController.updatePlace)
api.get('/tag', cors, tagController.get) api.get('/tag', cors, tagController.search)
api.get('/tag/:tag', cors, tagController.getEvents)
// - FEDIVERSE INSTANCES, MODERATION, RESOURCES // - FEDIVERSE INSTANCES, MODERATION, RESOURCES
api.get('/instances', isAdmin, instanceController.getAll) api.get('/instances', isAdmin, instanceController.getAll)
@ -175,14 +178,14 @@ if (config.status !== 'READY') {
api.put('/announcements/:announce_id', isAdmin, announceController.update) api.put('/announcements/:announce_id', isAdmin, announceController.update)
api.delete('/announcements/:announce_id', isAdmin, announceController.remove) api.delete('/announcements/:announce_id', isAdmin, announceController.remove)
// - COHORT // - COLLECTIONS
api.get('/cohorts/:name', cohortController.getEvents) api.get('/collections/:name', cors, collectionController.getEvents)
api.get('/cohorts', cohortController.getAll) api.get('/collections', collectionController.getAll)
api.post('/cohorts', isAdmin, cohortController.add) api.post('/collections', isAdmin, collectionController.add)
api.delete('/cohort/:id', isAdmin, cohortController.remove) api.delete('/collection/:id', isAdmin, collectionController.remove)
api.get('/filter/:cohort_id', isAdmin, cohortController.getFilters) api.get('/filter/:collection_id', isAdmin, collectionController.getFilters)
api.post('/filter', isAdmin, cohortController.addFilter) api.post('/filter', isAdmin, collectionController.addFilter)
api.delete('/filter/:id', isAdmin, cohortController.removeFilter) api.delete('/filter/:id', isAdmin, collectionController.removeFilter)
// OAUTH // OAUTH
api.get('/clients', isAuth, oauthController.getClients) api.get('/clients', isAuth, oauthController.getClients)

View file

@ -1,9 +1,9 @@
const { Model, DataTypes } = require('sequelize') const { Model, DataTypes } = require('sequelize')
const sequelize = require('./index').sequelize const sequelize = require('./index').sequelize
class Cohort extends Model {} class Collection extends Model {}
Cohort.init({ Collection.init({
id: { id: {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
autoIncrement: true, autoIncrement: true,
@ -21,7 +21,7 @@ Cohort.init({
isTop: { isTop: {
type: DataTypes.BOOLEAN type: DataTypes.BOOLEAN
} }
}, { sequelize, modelName: 'cohort', timestamps: false }) }, { sequelize, modelName: 'collection', timestamps: false })
module.exports = Cohort module.exports = Collection

View file

@ -1,5 +1,5 @@
const { Model, DataTypes } = require('sequelize') const { Model, DataTypes } = require('sequelize')
const Cohort = require('./cohort') const Collection = require('./collection')
const sequelize = require('./index').sequelize const sequelize = require('./index').sequelize
class Filter extends Model {} class Filter extends Model {}
@ -18,7 +18,7 @@ Filter.init({
} }
}, { sequelize, modelName: 'filter', timestamps: false }) }, { sequelize, modelName: 'filter', timestamps: false })
Filter.belongsTo(Cohort) Filter.belongsTo(Collection)
Cohort.hasMany(Filter) Collection.hasMany(Filter)
module.exports = Filter module.exports = Filter

View file

@ -39,11 +39,10 @@ User.init({
User.prototype.comparePassword = async function (pwd) { User.prototype.comparePassword = async function (pwd) {
if (!this.password) { return false } if (!this.password) { return false }
const ret = await bcrypt.compare(pwd, this.password) return bcrypt.compare(pwd, this.password)
return ret
} }
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)

View file

@ -1,7 +1,6 @@
let db let db
function _initializeDB () { function _initializeDB () {
const config = require('../config') const config = require('../config')
config.load()
config.log_level = 'error' config.log_level = 'error'
db = require('../api/models/index') db = require('../api/models/index')
return db.initialize() return db.initialize()
@ -29,13 +28,12 @@ async function modify (args) {
async function create (args) { async function create (args) {
await _initializeDB() await _initializeDB()
const User = require('../api/models/user') const User = require('../api/models/user')
console.error(args)
const user = await User.create({ const user = await User.create({
email: args.email, email: args.email,
is_active: true, is_active: true,
is_admin: args.admin || false is_admin: args.admin || false
}) }).catch(e => console.error(String(e)))
console.error(user) console.error(JSON.stringify(user, null, 2))
await db.close() await db.close()
} }

View file

@ -37,4 +37,5 @@ let config = {
} }
} }
config.load()
module.exports = config module.exports = config

View file

@ -1,4 +1,3 @@
// needed by sequelize // needed by sequelize
const config = require('./config') const config = require('./config')
config.load()
module.exports = config.db module.exports = config.db

View file

@ -116,6 +116,14 @@ module.exports = {
next() next()
}, },
col (field) {
if (config.db.dialect === 'postgres') {
return '"' + field.split('.').join('"."') + '"'
} else {
return field
}
},
async getImageFromURL (url) { async getImageFromURL (url) {
log.debug(`getImageFromURL ${url}`) log.debug(`getImageFromURL ${url}`)
if(!/^https?:\/\//.test(url)) { if(!/^https?:\/\//.test(url)) {
@ -233,5 +241,13 @@ module.exports = {
} }
} }
next() next()
},
async feedRedirect (req, res, next) {
const accepted = req.accepts('html', 'application/rss+xml', 'text/calendar')
if (['application/rss+xml', 'text/calendar'].includes(accepted) && /^\/(tag|place|collection)\/.*/.test(req.path)) {
return res.redirect((accepted === 'application/rss+xml' ? '/feed/rss' : '/feed/ics') + req.path)
}
next()
} }
} }

View file

@ -1,5 +1,4 @@
const config = require('../server/config') const config = require('../server/config')
config.load()
const initialize = { const initialize = {
// close connections/port/unix socket // close connections/port/unix socket

View file

@ -0,0 +1,30 @@
'use strict';
module.exports = {
async up (queryInterface, Sequelize) {
return Promise.all(
[
await queryInterface.renameTable('cohorts', 'collections'),
await queryInterface.renameColumn('filters', 'cohortId', 'collectionId'),
await queryInterface.changeColumn('filters', 'collectionId', {
type: Sequelize.INTEGER,
allowNull: true,
references: {
model: 'collections',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL'
}),
])
},
async down (queryInterface, Sequelize) {
/**
* Add reverting commands here.
*
* Example:
* await queryInterface.dropTable('users');
*/
}
};

View file

@ -34,9 +34,17 @@ if (config.status === 'READY') {
const federation = require('./federation') const federation = require('./federation')
const webfinger = require('./federation/webfinger') const webfinger = require('./federation/webfinger')
const exportController = require('./api/controller/export') const exportController = require('./api/controller/export')
const tagController = require('./api/controller/tag')
const placeController = require('./api/controller/place')
const collectionController = require('./api/controller/collection')
// rss / ics feed
app.use(helpers.feedRedirect)
app.get('/feed/:format/tag/:tag', cors(), tagController.getEvents)
app.get('/feed/:format/place/:placeName', cors(), placeController.getEvents)
app.get('/feed/:format/collection/:name', cors(), collectionController.getEvents)
app.get('/feed/:format', cors(), exportController.export)
// rss/ics/atom feed
app.get('/feed/:type', cors(), exportController.export)
app.use('/event/:slug', helpers.APRedirect) app.use('/event/:slug', helpers.APRedirect)
@ -59,7 +67,7 @@ app.use('/api', api)
// // Handle 500 // // Handle 500
app.use((error, _req, res, _next) => { app.use((error, _req, res, _next) => {
log.error('[ERROR]', error) log.error('[ERROR]' + error)
return res.status(500).send('500: Internal Server Error') return res.status(500).send('500: Internal Server Error')
}) })

View file

@ -14,8 +14,8 @@ let token
let app let app
beforeAll( async () => { beforeAll( async () => {
fs.copyFileSync('./starter.sqlite', './testdb.sqlite') fs.copyFileSync('./starter.sqlite', './testdb.sqlite')
await require('../server/initialize.server.js')() await require('../server/initialize.server.js').start()
app = require('../server/routes.js') app = require('../server/routes.js').handler
}) })
describe('Basic', () => { describe('Basic', () => {
@ -96,7 +96,7 @@ describe('Events', () => {
.expect(403) .expect(403)
await request(app).post('/api/event') await request(app).post('/api/event')
.send({ title: 'test title', place_name: 'place name', place_address: 'address', start_datetime: new Date().getTime() * 1000 }) .send({ title: 'test title', place_name: 'place name', place_address: 'address', tags: ['test'], start_datetime: new Date().getTime() * 1000 })
.auth(token.access_token, { type: 'bearer' }) .auth(token.access_token, { type: 'bearer' })
.expect(200) .expect(200)
@ -125,6 +125,7 @@ describe('Events', () => {
.send(event) .send(event)
.expect(200) .expect(200)
.expect('Content-Type', /json/) .expect('Content-Type', /json/)
expect(response.body.tags[0]).toBe('test tag') expect(response.body.tags[0]).toBe('test tag')
}) })
}) })
@ -132,47 +133,97 @@ describe('Events', () => {
describe('Tags', () => { describe('Tags', () => {
test('should create event with tags', async () => { test('should create event with tags', async () => {
const event = await request(app).post('/api/event') const event = await request(app).post('/api/event')
.send({ title: 'test tags', place_id: 2, start_datetime: new Date().getTime() * 1000, tags: ['tag1', 'tag2', 'tag3'] }) .send({ title: 'test tags', place_id: 2, start_datetime: new Date().getTime() * 1000, tags: ['tag1', 'Tag2', 'tAg3'] })
.auth(token.access_token, { type: 'bearer' }) .auth(token.access_token, { type: 'bearer' })
.expect(200) .expect(200)
expect(event.body.tags.length).toBe(3) expect(event.body.tags.length).toBe(3)
}) })
test('should create event trimming tags / ignore sensitiviness', async () => {
const event = await request(app).post('/api/event')
.send({ title: 'test trimming tags', place_id: 2, start_datetime: new Date().getTime() * 1000, tags: ['Tag1', 'taG2 '] })
.auth(token.access_token, { type: 'bearer' })
.expect(200)
expect(event.body.tags.length).toBe(2)
expect(event.body.tags[0]).toBe('tag1')
expect(event.body.tags[1]).toBe('Tag2')
})
test('should return events searching for tags', async () => { test('should return events searching for tags', async () => {
const response = await request(app).get('/api/events?tags=tag1') const response = await request(app).get('/api/events?tags=tAg3')
.expect(200) .expect(200)
console.error(response.body) // console.error(response.body)
console.error(response.body[0].tags) // console.error(response.body[0].tags)
expect(response.body.length).toBe(1) expect(response.body.length).toBe(1)
expect(response.body[0].title).toBe('test tags') // expect(response.body[0].title).toBe('test tags')
expect(response.body[0].tags.length).toBe(3) expect(response.body[0].tags.length).toBe(3)
}) })
}) })
describe ('Cohort', () => { describe('Place', () => {
test('should not create a new cohort if not allowed', () => { test('should get events by place', async () => {
return request(app).post('/api/cohorts') const response = await request(app).get('/api/place/place name 2')
.send({ name: 'test cohort' }) .expect(200)
expect(response.body.place.name).toBe('place name 2')
expect(response.body.events.length).toBe(2)
expect(response.body.events[0].place.name).toBe('place name 2')
})
test('admin should get all places', async () => {
await request(app).get('/api/place/all')
.expect(403)
const response = await request(app).get('/api/place/all')
.auth(token.access_token, { type: 'bearer' })
.expect(200)
expect(response.body.length).toBe(2)
})
test('should search for a place', async () => {
const response = await request(app).get('/api/place?search=place')
.expect(200)
expect(response.body.length).toBe(2)
})
})
describe ('Collection', () => {
test('should not create a new collection if not allowed', () => {
return request(app).post('/api/collections')
.send({ name: 'test collection' })
.expect(403) .expect(403)
}) })
test('should create a new cohort', async () => { test('should create a new collection', async () => {
const response = await request(app).post('/api/cohorts') const response = await request(app).post('/api/collections')
.send({ name: 'test cohort' }) .send({ name: 'test collection' })
.auth(token.access_token, { type: 'bearer' }) .auth(token.access_token, { type: 'bearer' })
.expect(200) .expect(200)
expect(response.body.id).toBe(1) expect(response.body.id).toBe(1)
}) })
test('should do not have any event when no filters', async () => {
const response = await request(app).get('/api/collections/test collection')
.expect(200)
expect(response.body.length).toBe(0)
})
test('should add a new filter', async () => { test('should add a new filter', async () => {
await request(app) await request(app)
.post('/api/filter') .post('/api/filter')
.expect(403) .expect(403)
const response = await request(app).post('/api/filter') const response = await request(app).post('/api/filter')
.send({ cohortId: 1, places: [1] }) .send({ collectionId: 1, tags: ['test'] })
.auth(token.access_token, { type: 'bearer' }) .auth(token.access_token, { type: 'bearer' })
.expect(200) .expect(200)
@ -180,12 +231,12 @@ describe ('Cohort', () => {
}) })
test('should get cohort events', async () => { test('should get collection events', async () => {
const response = await request(app) const response = await request(app)
.get('/api/cohorts/1') .get(`/api/collections/test collection`)
.expect(200) .expect(200)
expect(response.body.length).toBe(2) expect(response.body.length).toBe(1)
}) })
test('should remove filter', async () => { test('should remove filter', async () => {
@ -209,7 +260,7 @@ describe ('Cohort', () => {
test('shoud filter for tags', async () => { test('shoud filter for tags', async () => {
await request(app) await request(app)
.post('/api/filter') .post('/api/filter')
.send({ cohortId: 1, tags: ['test'] }) .send({ collectionId: 1, tags: ['test'] })
.auth(token.access_token, { type: 'bearer' }) .auth(token.access_token, { type: 'bearer' })
.expect(200) .expect(200)
@ -220,8 +271,8 @@ describe ('Cohort', () => {
.expect(200) .expect(200)
expect(response.body.length).toBe(1) expect(response.body.length).toBe(1)
response = await request(app) response = await request(app)
.get('/api/cohorts/1') .get(`/api/collections/test collection`)
.expect(200) .expect(200)
expect(response.body.length).toBe(1) expect(response.body.length).toBe(1)

View file

@ -1,18 +1,18 @@
doctype xml doctype xml
rss(version='2.0' xmlns:atom="http://www.w3.org/2005/Atom") rss(version='2.0' xmlns:atom="http://www.w3.org/2005/Atom")
channel channel
atom:link(href=`${settings.baseurl}/feed/rss` rel="self" type="application/rss+xml") atom:link(href=`${link}` rel="self" type="application/rss+xml")
title #{settings.title} title #{title}
link #{settings.baseurl} link #{settings.baseurl}
description #{settings.description} description #{settings.description}
each event in events each event in events
item item
if (event.media && event.media.length) if (event.media && event.media.length)
<enclosure url="#{settings.baseurl}/media/#{event.media[0].url}" type='image/jpeg'></enclosure> <enclosure url="#{settings.baseurl}/media/#{event.media[0].url}" type='image/jpeg' length="#{event.media[0].size||1}"></enclosure>
title [#{moment.unix(event.start_datetime).format("YY-MM-DD")}] #{event.title} @#{event.place.name} title [#{moment.unix(event.start_datetime).format("YY-MM-DD")}] #{event.title} @#{event.place.name}
link #{settings.baseurl}/event/#{event.slug || event.id} link #{settings.baseurl}/event/#{event.slug || event.id}
each tag in event.tags each tag in event.tags
category #{tag.tag} category #{tag}
description description
| <![CDATA[ | <![CDATA[
| <h4>#{event.title}</h4> | <h4>#{event.title}</h4>
@ -20,7 +20,7 @@ rss(version='2.0' xmlns:atom="http://www.w3.org/2005/Atom")
| <small>(#{moment.unix(event.start_datetime).format("dddd, D MMMM HH:mm")})</small><br/> | <small>(#{moment.unix(event.start_datetime).format("dddd, D MMMM HH:mm")})</small><br/>
if (event.media && event.media.length) if (event.media && event.media.length)
| <img alt="#{event.media[0].name || ''}" src="#{settings.baseurl}/media/#{event.media[0].url}"/> | <img alt="#{event.media[0].name || ''}" src="#{settings.baseurl}/media/#{event.media[0].url}"/>
| <pre>!{event.description}</pre> | !{event.description}
| ]]> | ]]>
pubDate= new Date(event.updatedAt).toUTCString() pubDate= new Date(event.updatedAt).toUTCString()
guid #{settings.baseurl}/event/#{event.slug || event.id} guid #{settings.baseurl}/event/#{event.slug || event.id}

View file

@ -14,7 +14,7 @@ function wpgancio_delete_post ($post_id) {
$gancio_id = get_post_meta($post_id, 'wpgancio_gancio_id', TRUE); $gancio_id = get_post_meta($post_id, 'wpgancio_gancio_id', TRUE);
if ($gancio_id) { if ($gancio_id) {
$http = _wp_http_get_object(); $http = _wp_http_get_object();
$response = $http->request( "${instance_url}/api/event/${gancio_id}", array( $http->request( "${instance_url}/api/event/${gancio_id}", array(
'method' => 'DELETE', 'method' => 'DELETE',
'headers' => array ( 'headers' => array (
'Authorization' => 'Bearer ' . get_option('wpgancio_token') 'Authorization' => 'Bearer ' . get_option('wpgancio_token')
@ -30,6 +30,7 @@ function wpgancio_save_event ($post_id) {
return sanitize_title($tag->name); return sanitize_title($tag->name);
} }
// TODO: merge event tags with post tags
$tmp_tags = get_the_terms( $event, 'event-tag' ); $tmp_tags = get_the_terms( $event, 'event-tag' );
$tags = array_map('tagName', $tmp_tags); $tags = array_map('tagName', $tmp_tags);

View file

@ -80,6 +80,9 @@ function wpgancio_options_page() {
function wpgancio_instance_url_cb( $args ) { function wpgancio_instance_url_cb( $args ) {
// get the value of the setting we've registered with register_setting() // get the value of the setting we've registered with register_setting()
$instance_url = get_option( 'wpgancio_instance_url' ); $instance_url = get_option( 'wpgancio_instance_url' );
if (empty($instance_url)) {
$instance_url = WP_GANCIO_DEFAULT_INSTANCEURL;
}
// output the field // output the field
?> ?>

630
yarn.lock
View file

@ -1132,28 +1132,28 @@
resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98"
integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==
"@jest/console@^28.1.0": "@jest/console@^28.1.1":
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/@jest/console/-/console-28.1.0.tgz#db78222c3d3b0c1db82f1b9de51094c2aaff2176" resolved "https://registry.yarnpkg.com/@jest/console/-/console-28.1.1.tgz#305f8ca50b6e70413839f54c0e002b60a0f2fd7d"
integrity sha512-tscn3dlJFGay47kb4qVruQg/XWlmvU0xp3EJOjzzY+sBaI+YgwKcvAmTcyYU7xEiLLIY5HCdWRooAL8dqkFlDA== integrity sha512-0RiUocPVFEm3WRMOStIHbRWllG6iW6E3/gUPnf4lkrVFyXIIDeCe+vlKeYyFOMhB2EPE6FLFCNADSOOQMaqvyA==
dependencies: dependencies:
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
"@types/node" "*" "@types/node" "*"
chalk "^4.0.0" chalk "^4.0.0"
jest-message-util "^28.1.0" jest-message-util "^28.1.1"
jest-util "^28.1.0" jest-util "^28.1.1"
slash "^3.0.0" slash "^3.0.0"
"@jest/core@^28.1.0": "@jest/core@^28.1.1":
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/@jest/core/-/core-28.1.0.tgz#784a1e6ce5358b46fcbdcfbbd93b1b713ed4ea80" resolved "https://registry.yarnpkg.com/@jest/core/-/core-28.1.1.tgz#086830bec6267accf9af5ca76f794858e9f9f092"
integrity sha512-/2PTt0ywhjZ4NwNO4bUqD9IVJfmFVhVKGlhvSpmEfUCuxYf/3NHcKmRFI+I71lYzbTT3wMuYpETDCTHo81gC/g== integrity sha512-3pYsBoZZ42tXMdlcFeCc/0j9kOlK7MYuXs2B1QbvDgMoW1K9NJ4G/VYvIbMb26iqlkTfPHo7SC2JgjDOk/mxXw==
dependencies: dependencies:
"@jest/console" "^28.1.0" "@jest/console" "^28.1.1"
"@jest/reporters" "^28.1.0" "@jest/reporters" "^28.1.1"
"@jest/test-result" "^28.1.0" "@jest/test-result" "^28.1.1"
"@jest/transform" "^28.1.0" "@jest/transform" "^28.1.1"
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
"@types/node" "*" "@types/node" "*"
ansi-escapes "^4.2.1" ansi-escapes "^4.2.1"
chalk "^4.0.0" chalk "^4.0.0"
@ -1161,80 +1161,80 @@
exit "^0.1.2" exit "^0.1.2"
graceful-fs "^4.2.9" graceful-fs "^4.2.9"
jest-changed-files "^28.0.2" jest-changed-files "^28.0.2"
jest-config "^28.1.0" jest-config "^28.1.1"
jest-haste-map "^28.1.0" jest-haste-map "^28.1.1"
jest-message-util "^28.1.0" jest-message-util "^28.1.1"
jest-regex-util "^28.0.2" jest-regex-util "^28.0.2"
jest-resolve "^28.1.0" jest-resolve "^28.1.1"
jest-resolve-dependencies "^28.1.0" jest-resolve-dependencies "^28.1.1"
jest-runner "^28.1.0" jest-runner "^28.1.1"
jest-runtime "^28.1.0" jest-runtime "^28.1.1"
jest-snapshot "^28.1.0" jest-snapshot "^28.1.1"
jest-util "^28.1.0" jest-util "^28.1.1"
jest-validate "^28.1.0" jest-validate "^28.1.1"
jest-watcher "^28.1.0" jest-watcher "^28.1.1"
micromatch "^4.0.4" micromatch "^4.0.4"
pretty-format "^28.1.0" pretty-format "^28.1.1"
rimraf "^3.0.0" rimraf "^3.0.0"
slash "^3.0.0" slash "^3.0.0"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
"@jest/environment@^28.1.0": "@jest/environment@^28.1.1":
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-28.1.0.tgz#dedf7d59ec341b9292fcf459fd0ed819eb2e228a" resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-28.1.1.tgz#c4cbf85283278d768f816ebd1a258ea6f9e39d4f"
integrity sha512-S44WGSxkRngzHslhV6RoAExekfF7Qhwa6R5+IYFa81mpcj0YgdBnRSmvHe3SNwOt64yXaE5GG8Y2xM28ii5ssA== integrity sha512-9auVQ2GzQ7nrU+lAr8KyY838YahElTX9HVjbQPPS2XjlxQ+na18G113OoBhyBGBtD6ZnO/SrUy5WR8EzOj1/Uw==
dependencies: dependencies:
"@jest/fake-timers" "^28.1.0" "@jest/fake-timers" "^28.1.1"
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
"@types/node" "*" "@types/node" "*"
jest-mock "^28.1.0" jest-mock "^28.1.1"
"@jest/expect-utils@^28.1.0": "@jest/expect-utils@^28.1.1":
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-28.1.0.tgz#a5cde811195515a9809b96748ae8bcc331a3538a" resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-28.1.1.tgz#d84c346025b9f6f3886d02c48a6177e2b0360587"
integrity sha512-5BrG48dpC0sB80wpeIX5FU6kolDJI4K0n5BM9a5V38MGx0pyRvUBSS0u2aNTdDzmOrCjhOg8pGs6a20ivYkdmw== integrity sha512-n/ghlvdhCdMI/hTcnn4qV57kQuV9OTsZzH1TTCVARANKhl6hXJqLKUkwX69ftMGpsbpt96SsDD8n8LD2d9+FRw==
dependencies: dependencies:
jest-get-type "^28.0.2" jest-get-type "^28.0.2"
"@jest/expect@^28.1.0": "@jest/expect@^28.1.1":
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-28.1.0.tgz#2e5a31db692597070932366a1602b5157f0f217c" resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-28.1.1.tgz#ea4fcc8504b45835029221c0dc357c622a761326"
integrity sha512-be9ETznPLaHOmeJqzYNIXv1ADEzENuQonIoobzThOYPuK/6GhrWNIJDVTgBLCrz3Am73PyEU2urQClZp0hLTtA== integrity sha512-/+tQprrFoT6lfkMj4mW/mUIfAmmk/+iQPmg7mLDIFOf2lyf7EBHaS+x3RbeR0VZVMe55IvX7QRoT/2aK3AuUXg==
dependencies: dependencies:
expect "^28.1.0" expect "^28.1.1"
jest-snapshot "^28.1.0" jest-snapshot "^28.1.1"
"@jest/fake-timers@^28.1.0": "@jest/fake-timers@^28.1.1":
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-28.1.0.tgz#ea77878aabd5c5d50e1fc53e76d3226101e33064" resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-28.1.1.tgz#47ce33296ab9d680c76076d51ddbe65ceb3337f1"
integrity sha512-Xqsf/6VLeAAq78+GNPzI7FZQRf5cCHj1qgQxCjws9n8rKw8r1UYoeaALwBvyuzOkpU3c1I6emeMySPa96rxtIg== integrity sha512-BY/3+TyLs5+q87rGWrGUY5f8e8uC3LsVHS9Diz8+FV3ARXL4sNnkLlIB8dvDvRrp+LUCGM+DLqlsYubizGUjIA==
dependencies: dependencies:
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
"@sinonjs/fake-timers" "^9.1.1" "@sinonjs/fake-timers" "^9.1.1"
"@types/node" "*" "@types/node" "*"
jest-message-util "^28.1.0" jest-message-util "^28.1.1"
jest-mock "^28.1.0" jest-mock "^28.1.1"
jest-util "^28.1.0" jest-util "^28.1.1"
"@jest/globals@^28.1.0": "@jest/globals@^28.1.1":
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-28.1.0.tgz#a4427d2eb11763002ff58e24de56b84ba79eb793" resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-28.1.1.tgz#c0a7977f85e26279cc090d9adcdf82b8a34c4061"
integrity sha512-3m7sTg52OTQR6dPhsEQSxAvU+LOBbMivZBwOvKEZ+Rb+GyxVnXi9HKgOTYkx/S99T8yvh17U4tNNJPIEQmtwYw== integrity sha512-dEgl/6v7ToB4vXItdvcltJBgny0xBE6xy6IYQrPJAJggdEinGxCDMivNv7sFzPcTITGquXD6UJwYxfJ/5ZwDSg==
dependencies: dependencies:
"@jest/environment" "^28.1.0" "@jest/environment" "^28.1.1"
"@jest/expect" "^28.1.0" "@jest/expect" "^28.1.1"
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
"@jest/reporters@^28.1.0": "@jest/reporters@^28.1.1":
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-28.1.0.tgz#5183a28b9b593b6000fa9b89b031c7216b58a9a0" resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-28.1.1.tgz#9389f4bb3cce4d9b586f6195f83c79cd2a1c8662"
integrity sha512-qxbFfqap/5QlSpIizH9c/bFCDKsQlM4uAKSOvZrP+nIdrjqre3FmKzpTtYyhsaVcOSNK7TTt2kjm+4BJIjysFA== integrity sha512-597Zj4D4d88sZrzM4atEGLuO7SdA/YrOv9SRXHXRNC+/FwPCWxZhBAEzhXoiJzfRwn8zes/EjS8Lo6DouGN5Gg==
dependencies: dependencies:
"@bcoe/v8-coverage" "^0.2.3" "@bcoe/v8-coverage" "^0.2.3"
"@jest/console" "^28.1.0" "@jest/console" "^28.1.1"
"@jest/test-result" "^28.1.0" "@jest/test-result" "^28.1.1"
"@jest/transform" "^28.1.0" "@jest/transform" "^28.1.1"
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
"@jridgewell/trace-mapping" "^0.3.7" "@jridgewell/trace-mapping" "^0.3.7"
"@types/node" "*" "@types/node" "*"
chalk "^4.0.0" chalk "^4.0.0"
@ -1247,8 +1247,9 @@
istanbul-lib-report "^3.0.0" istanbul-lib-report "^3.0.0"
istanbul-lib-source-maps "^4.0.0" istanbul-lib-source-maps "^4.0.0"
istanbul-reports "^3.1.3" istanbul-reports "^3.1.3"
jest-util "^28.1.0" jest-message-util "^28.1.1"
jest-worker "^28.1.0" jest-util "^28.1.1"
jest-worker "^28.1.1"
slash "^3.0.0" slash "^3.0.0"
string-length "^4.0.1" string-length "^4.0.1"
strip-ansi "^6.0.0" strip-ansi "^6.0.0"
@ -1271,51 +1272,51 @@
callsites "^3.0.0" callsites "^3.0.0"
graceful-fs "^4.2.9" graceful-fs "^4.2.9"
"@jest/test-result@^28.1.0": "@jest/test-result@^28.1.1":
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-28.1.0.tgz#fd149dee123510dd2fcadbbf5f0020f98ad7f12c" resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-28.1.1.tgz#c6f18d1bbb01aa88925dd687872a75f8414b317a"
integrity sha512-sBBFIyoPzrZho3N+80P35A5oAkSKlGfsEFfXFWuPGBsW40UAjCkGakZhn4UQK4iQlW2vgCDMRDOob9FGKV8YoQ== integrity sha512-hPmkugBktqL6rRzwWAtp1JtYT4VHwv8OQ+9lE5Gymj6dHzubI/oJHMUpPOt8NrdVWSrz9S7bHjJUmv2ggFoUNQ==
dependencies: dependencies:
"@jest/console" "^28.1.0" "@jest/console" "^28.1.1"
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
"@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-lib-coverage" "^2.0.0"
collect-v8-coverage "^1.0.0" collect-v8-coverage "^1.0.0"
"@jest/test-sequencer@^28.1.0": "@jest/test-sequencer@^28.1.1":
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-28.1.0.tgz#ce7294bbe986415b9a30e218c7e705e6ebf2cdf2" resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-28.1.1.tgz#f594ee2331df75000afe0d1ae3237630ecec732e"
integrity sha512-tZCEiVWlWNTs/2iK9yi6o3AlMfbbYgV4uuZInSVdzZ7ftpHZhCMuhvk2HLYhCZzLgPFQ9MnM1YaxMnh3TILFiQ== integrity sha512-nuL+dNSVMcWB7OOtgb0EGH5AjO4UBCt68SLP08rwmC+iRhyuJWS9MtZ/MpipxFwKAlHFftbMsydXqWre8B0+XA==
dependencies: dependencies:
"@jest/test-result" "^28.1.0" "@jest/test-result" "^28.1.1"
graceful-fs "^4.2.9" graceful-fs "^4.2.9"
jest-haste-map "^28.1.0" jest-haste-map "^28.1.1"
slash "^3.0.0" slash "^3.0.0"
"@jest/transform@^28.1.0": "@jest/transform@^28.1.1":
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-28.1.0.tgz#224a3c9ba4cc98e2ff996c0a89a2d59db15c74ce" resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-28.1.1.tgz#83541f2a3f612077c8501f49cc4e205d4e4a6b27"
integrity sha512-omy2xe5WxlAfqmsTjTPxw+iXRTRnf+NtX0ToG+4S0tABeb4KsKmPUHq5UBuwunHg3tJRwgEQhEp0M/8oiatLEA== integrity sha512-PkfaTUuvjUarl1EDr5ZQcCA++oXkFCP9QFUkG0yVKVmNObjhrqDy0kbMpMebfHWm3CCDHjYNem9eUSH8suVNHQ==
dependencies: dependencies:
"@babel/core" "^7.11.6" "@babel/core" "^7.11.6"
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
"@jridgewell/trace-mapping" "^0.3.7" "@jridgewell/trace-mapping" "^0.3.7"
babel-plugin-istanbul "^6.1.1" babel-plugin-istanbul "^6.1.1"
chalk "^4.0.0" chalk "^4.0.0"
convert-source-map "^1.4.0" convert-source-map "^1.4.0"
fast-json-stable-stringify "^2.0.0" fast-json-stable-stringify "^2.0.0"
graceful-fs "^4.2.9" graceful-fs "^4.2.9"
jest-haste-map "^28.1.0" jest-haste-map "^28.1.1"
jest-regex-util "^28.0.2" jest-regex-util "^28.0.2"
jest-util "^28.1.0" jest-util "^28.1.1"
micromatch "^4.0.4" micromatch "^4.0.4"
pirates "^4.0.4" pirates "^4.0.4"
slash "^3.0.0" slash "^3.0.0"
write-file-atomic "^4.0.1" write-file-atomic "^4.0.1"
"@jest/types@^28.1.0": "@jest/types@^28.1.1":
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/@jest/types/-/types-28.1.0.tgz#508327a89976cbf9bd3e1cc74641a29fd7dfd519" resolved "https://registry.yarnpkg.com/@jest/types/-/types-28.1.1.tgz#d059bbc80e6da6eda9f081f293299348bd78ee0b"
integrity sha512-xmEggMPr317MIOjjDoZ4ejCSr9Lpbt/u34+dvc99t7DS8YirW5rwZEhzKPC2BMUFkUhI48qs6qLUSGw5FuL0GA== integrity sha512-vRXVqSg1VhDnB8bWcmvLzmg0Bt9CRKVgHPXqYwvWMX3TvAjeO+nRuK6+VdTKCtWOvYlmkF/HqNAL/z+N3B53Kw==
dependencies: dependencies:
"@jest/schemas" "^28.0.2" "@jest/schemas" "^28.0.2"
"@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-lib-coverage" "^2.0.0"
@ -2785,15 +2786,15 @@ axios@^0.27.2:
follow-redirects "^1.14.9" follow-redirects "^1.14.9"
form-data "^4.0.0" form-data "^4.0.0"
babel-jest@^28.1.0: babel-jest@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-28.1.0.tgz#95a67f8e2e7c0042e7b3ad3951b8af41a533b5ea" resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-28.1.1.tgz#2a3a4ae50964695b2d694ccffe4bec537c5a3586"
integrity sha512-zNKk0yhDZ6QUwfxh9k07GII6siNGMJWVUU49gmFj5gfdqDKLqa2RArXOF2CODp4Dr7dLxN2cvAV+667dGJ4b4w== integrity sha512-MEt0263viUdAkTq5D7upHPNxvt4n9uLUGa6pPz3WviNBMtOmStb1lIXS3QobnoqM+qnH+vr4EKlvhe8QcmxIYw==
dependencies: dependencies:
"@jest/transform" "^28.1.0" "@jest/transform" "^28.1.1"
"@types/babel__core" "^7.1.14" "@types/babel__core" "^7.1.14"
babel-plugin-istanbul "^6.1.1" babel-plugin-istanbul "^6.1.1"
babel-preset-jest "^28.0.2" babel-preset-jest "^28.1.1"
chalk "^4.0.0" chalk "^4.0.0"
graceful-fs "^4.2.9" graceful-fs "^4.2.9"
slash "^3.0.0" slash "^3.0.0"
@ -2826,10 +2827,10 @@ babel-plugin-istanbul@^6.1.1:
istanbul-lib-instrument "^5.0.4" istanbul-lib-instrument "^5.0.4"
test-exclude "^6.0.0" test-exclude "^6.0.0"
babel-plugin-jest-hoist@^28.0.2: babel-plugin-jest-hoist@^28.1.1:
version "28.0.2" version "28.1.1"
resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.0.2.tgz#9307d03a633be6fc4b1a6bc5c3a87e22bd01dd3b" resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-28.1.1.tgz#5e055cdcc47894f28341f87f5e35aad2df680b11"
integrity sha512-Kizhn/ZL+68ZQHxSnHyuvJv8IchXD62KQxV77TBDV/xoBFBOfgRAk97GNs6hXdTTCiVES9nB2I6+7MXXrk5llQ== integrity sha512-NovGCy5Hn25uMJSAU8FaHqzs13cFoOI4lhIujiepssjCKRsAo3TA734RDWSGxuFTsUJXerYOqQQodlxgmtqbzw==
dependencies: dependencies:
"@babel/template" "^7.3.3" "@babel/template" "^7.3.3"
"@babel/types" "^7.3.3" "@babel/types" "^7.3.3"
@ -2878,12 +2879,12 @@ babel-preset-current-node-syntax@^1.0.0:
"@babel/plugin-syntax-optional-chaining" "^7.8.3" "@babel/plugin-syntax-optional-chaining" "^7.8.3"
"@babel/plugin-syntax-top-level-await" "^7.8.3" "@babel/plugin-syntax-top-level-await" "^7.8.3"
babel-preset-jest@^28.0.2: babel-preset-jest@^28.1.1:
version "28.0.2" version "28.1.1"
resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-28.0.2.tgz#d8210fe4e46c1017e9fa13d7794b166e93aa9f89" resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-28.1.1.tgz#5b6e5e69f963eb2d70f739c607b8f723c0ee75e4"
integrity sha512-sYzXIdgIXXroJTFeB3S6sNDWtlJ2dllCdTEsnZ65ACrMojj3hVNFRmnJ1HZtomGi+Be7aqpY/HJ92fr8OhKVkQ== integrity sha512-FCq9Oud0ReTeWtcneYf/48981aTfXYuB9gbU4rBNNJVBSQ6ssv7E6v/qvbBxtOWwZFXjLZwpg+W3q7J6vhH25g==
dependencies: dependencies:
babel-plugin-jest-hoist "^28.0.2" babel-plugin-jest-hoist "^28.1.1"
babel-preset-current-node-syntax "^1.0.0" babel-preset-current-node-syntax "^1.0.0"
babel-walk@3.0.0-canary-5: babel-walk@3.0.0-canary-5:
@ -4540,10 +4541,10 @@ dezalgo@1.0.3:
asap "^2.0.0" asap "^2.0.0"
wrappy "1" wrappy "1"
diff-sequences@^28.0.2: diff-sequences@^28.1.1:
version "28.0.2" version "28.1.1"
resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.0.2.tgz#40f8d4ffa081acbd8902ba35c798458d0ff1af41" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-28.1.1.tgz#9989dc731266dc2903457a70e996f3a041913ac6"
integrity sha512-YtEoNynLDFCRznv/XDalsKGSZDoj0U5kLnXvY0JSq3nBboRrZXjD81+eSiwi+nzcZDwedMmcowcxNwwgFW23mQ== integrity sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw==
diffie-hellman@^5.0.0: diffie-hellman@^5.0.0:
version "5.0.3" version "5.0.3"
@ -5110,16 +5111,16 @@ expand-template@^2.0.3:
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c" resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg== integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
expect@^28.1.0: expect@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/expect/-/expect-28.1.0.tgz#10e8da64c0850eb8c39a480199f14537f46e8360" resolved "https://registry.yarnpkg.com/expect/-/expect-28.1.1.tgz#ca6fff65f6517cf7220c2e805a49c19aea30b420"
integrity sha512-qFXKl8Pmxk8TBGfaFKRtcQjfXEnKAs+dmlxdwvukJZorwrAabT7M3h8oLOG01I2utEhkmUTi17CHaPBovZsKdw== integrity sha512-/AANEwGL0tWBwzLNOvO0yUdy2D52jVdNXppOqswC49sxMN2cPWsGCQdzuIf9tj6hHoBQzNvx75JUYuQAckPo3w==
dependencies: dependencies:
"@jest/expect-utils" "^28.1.0" "@jest/expect-utils" "^28.1.1"
jest-get-type "^28.0.2" jest-get-type "^28.0.2"
jest-matcher-utils "^28.1.0" jest-matcher-utils "^28.1.1"
jest-message-util "^28.1.0" jest-message-util "^28.1.1"
jest-util "^28.1.0" jest-util "^28.1.1"
express-oauth-server@lesion/express-oauth-server#master: express-oauth-server@lesion/express-oauth-server#master:
version "2.0.0" version "2.0.0"
@ -6776,180 +6777,180 @@ jest-changed-files@^28.0.2:
execa "^5.0.0" execa "^5.0.0"
throat "^6.0.1" throat "^6.0.1"
jest-circus@^28.1.0: jest-circus@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-28.1.0.tgz#e229f590911bd54d60efaf076f7acd9360296dae" resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-28.1.1.tgz#3d27da6a974d85a466dc0cdc6ddeb58daaa57bb4"
integrity sha512-rNYfqfLC0L0zQKRKsg4n4J+W1A2fbyGH7Ss/kDIocp9KXD9iaL111glsLu7+Z7FHuZxwzInMDXq+N1ZIBkI/TQ== integrity sha512-75+BBVTsL4+p2w198DQpCeyh1RdaS2lhEG87HkaFX/UG0gJExVq2skG2pT7XZEGBubNj2CytcWSPan4QEPNosw==
dependencies: dependencies:
"@jest/environment" "^28.1.0" "@jest/environment" "^28.1.1"
"@jest/expect" "^28.1.0" "@jest/expect" "^28.1.1"
"@jest/test-result" "^28.1.0" "@jest/test-result" "^28.1.1"
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
"@types/node" "*" "@types/node" "*"
chalk "^4.0.0" chalk "^4.0.0"
co "^4.6.0" co "^4.6.0"
dedent "^0.7.0" dedent "^0.7.0"
is-generator-fn "^2.0.0" is-generator-fn "^2.0.0"
jest-each "^28.1.0" jest-each "^28.1.1"
jest-matcher-utils "^28.1.0" jest-matcher-utils "^28.1.1"
jest-message-util "^28.1.0" jest-message-util "^28.1.1"
jest-runtime "^28.1.0" jest-runtime "^28.1.1"
jest-snapshot "^28.1.0" jest-snapshot "^28.1.1"
jest-util "^28.1.0" jest-util "^28.1.1"
pretty-format "^28.1.0" pretty-format "^28.1.1"
slash "^3.0.0" slash "^3.0.0"
stack-utils "^2.0.3" stack-utils "^2.0.3"
throat "^6.0.1" throat "^6.0.1"
jest-cli@^28.1.0: jest-cli@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-28.1.0.tgz#cd1d8adb9630102d5ba04a22895f63decdd7ac1f" resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-28.1.1.tgz#23ddfde8940e1818585ae4a568877b33b0e51cfe"
integrity sha512-fDJRt6WPRriHrBsvvgb93OxgajHHsJbk4jZxiPqmZbMDRcHskfJBBfTyjFko0jjfprP544hOktdSi9HVgl4VUQ== integrity sha512-+sUfVbJqb1OjBZ0OdBbI6OWfYM1i7bSfzYy6gze1F1w3OKWq8ZTEKkZ8a7ZQPq6G/G1qMh/uKqpdWhgl11NFQQ==
dependencies: dependencies:
"@jest/core" "^28.1.0" "@jest/core" "^28.1.1"
"@jest/test-result" "^28.1.0" "@jest/test-result" "^28.1.1"
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
chalk "^4.0.0" chalk "^4.0.0"
exit "^0.1.2" exit "^0.1.2"
graceful-fs "^4.2.9" graceful-fs "^4.2.9"
import-local "^3.0.2" import-local "^3.0.2"
jest-config "^28.1.0" jest-config "^28.1.1"
jest-util "^28.1.0" jest-util "^28.1.1"
jest-validate "^28.1.0" jest-validate "^28.1.1"
prompts "^2.0.1" prompts "^2.0.1"
yargs "^17.3.1" yargs "^17.3.1"
jest-config@^28.1.0: jest-config@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-28.1.0.tgz#fca22ca0760e746fe1ce1f9406f6b307ab818501" resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-28.1.1.tgz#e90b97b984f14a6c24a221859e81b258990fce2f"
integrity sha512-aOV80E9LeWrmflp7hfZNn/zGA4QKv/xsn2w8QCBP0t0+YqObuCWTSgNbHJ0j9YsTuCO08ZR/wsvlxqqHX20iUA== integrity sha512-tASynMhS+jVV85zKvjfbJ8nUyJS/jUSYZ5KQxLUN2ZCvcQc/OmhQl2j6VEL3ezQkNofxn5pQ3SPYWPHb0unTZA==
dependencies: dependencies:
"@babel/core" "^7.11.6" "@babel/core" "^7.11.6"
"@jest/test-sequencer" "^28.1.0" "@jest/test-sequencer" "^28.1.1"
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
babel-jest "^28.1.0" babel-jest "^28.1.1"
chalk "^4.0.0" chalk "^4.0.0"
ci-info "^3.2.0" ci-info "^3.2.0"
deepmerge "^4.2.2" deepmerge "^4.2.2"
glob "^7.1.3" glob "^7.1.3"
graceful-fs "^4.2.9" graceful-fs "^4.2.9"
jest-circus "^28.1.0" jest-circus "^28.1.1"
jest-environment-node "^28.1.0" jest-environment-node "^28.1.1"
jest-get-type "^28.0.2" jest-get-type "^28.0.2"
jest-regex-util "^28.0.2" jest-regex-util "^28.0.2"
jest-resolve "^28.1.0" jest-resolve "^28.1.1"
jest-runner "^28.1.0" jest-runner "^28.1.1"
jest-util "^28.1.0" jest-util "^28.1.1"
jest-validate "^28.1.0" jest-validate "^28.1.1"
micromatch "^4.0.4" micromatch "^4.0.4"
parse-json "^5.2.0" parse-json "^5.2.0"
pretty-format "^28.1.0" pretty-format "^28.1.1"
slash "^3.0.0" slash "^3.0.0"
strip-json-comments "^3.1.1" strip-json-comments "^3.1.1"
jest-diff@^28.1.0: jest-diff@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-28.1.0.tgz#77686fef899ec1873dbfbf9330e37dd429703269" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-28.1.1.tgz#1a3eedfd81ae79810931c63a1d0f201b9120106c"
integrity sha512-8eFd3U3OkIKRtlasXfiAQfbovgFgRDb0Ngcs2E+FMeBZ4rUezqIaGjuyggJBp+llosQXNEWofk/Sz4Hr5gMUhA== integrity sha512-/MUUxeR2fHbqHoMMiffe/Afm+U8U4olFRJ0hiVG2lZatPJcnGxx292ustVu7bULhjV65IYMxRdploAKLbcrsyg==
dependencies: dependencies:
chalk "^4.0.0" chalk "^4.0.0"
diff-sequences "^28.0.2" diff-sequences "^28.1.1"
jest-get-type "^28.0.2" jest-get-type "^28.0.2"
pretty-format "^28.1.0" pretty-format "^28.1.1"
jest-docblock@^28.0.2: jest-docblock@^28.1.1:
version "28.0.2" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-28.0.2.tgz#3cab8abea53275c9d670cdca814fc89fba1298c2" resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-28.1.1.tgz#6f515c3bf841516d82ecd57a62eed9204c2f42a8"
integrity sha512-FH10WWw5NxLoeSdQlJwu+MTiv60aXV/t8KEwIRGEv74WARE1cXIqh1vGdy2CraHuWOOrnzTWj/azQKqW4fO7xg== integrity sha512-3wayBVNiOYx0cwAbl9rwm5kKFP8yHH3d/fkEaL02NPTkDojPtheGB7HZSFY4wzX+DxyrvhXz0KSCVksmCknCuA==
dependencies: dependencies:
detect-newline "^3.0.0" detect-newline "^3.0.0"
jest-each@^28.1.0: jest-each@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-28.1.0.tgz#54ae66d6a0a5b1913e9a87588d26c2687c39458b" resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-28.1.1.tgz#ba5238dacf4f31d9fe23ddc2c44c01e7c23885c4"
integrity sha512-a/XX02xF5NTspceMpHujmOexvJ4GftpYXqr6HhhmKmExtMXsyIN/fvanQlt/BcgFoRKN4OCXxLQKth9/n6OPFg== integrity sha512-A042rqh17ZvEhRceDMi784ppoXR7MWGDEKTXEZXb4svt0eShMZvijGxzKsx+yIjeE8QYmHPrnHiTSQVhN4nqaw==
dependencies: dependencies:
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
chalk "^4.0.0" chalk "^4.0.0"
jest-get-type "^28.0.2" jest-get-type "^28.0.2"
jest-util "^28.1.0" jest-util "^28.1.1"
pretty-format "^28.1.0" pretty-format "^28.1.1"
jest-environment-node@^28.1.0: jest-environment-node@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-28.1.0.tgz#6ed2150aa31babba0c488c5b4f4d813a585c68e6" resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-28.1.1.tgz#1c86c59003a7d319fa06ea3b1bbda6c193715c67"
integrity sha512-gBLZNiyrPw9CSMlTXF1yJhaBgWDPVvH0Pq6bOEwGMXaYNzhzhw2kA/OijNF8egbCgDS0/veRv97249x2CX+udQ== integrity sha512-2aV/eeY/WNgUUJrrkDJ3cFEigjC5fqT1+fCclrY6paqJ5zVPoM//sHmfgUUp7WLYxIdbPwMiVIzejpN56MxnNA==
dependencies: dependencies:
"@jest/environment" "^28.1.0" "@jest/environment" "^28.1.1"
"@jest/fake-timers" "^28.1.0" "@jest/fake-timers" "^28.1.1"
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
"@types/node" "*" "@types/node" "*"
jest-mock "^28.1.0" jest-mock "^28.1.1"
jest-util "^28.1.0" jest-util "^28.1.1"
jest-get-type@^28.0.2: jest-get-type@^28.0.2:
version "28.0.2" version "28.0.2"
resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-28.0.2.tgz#34622e628e4fdcd793d46db8a242227901fcf203" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-28.0.2.tgz#34622e628e4fdcd793d46db8a242227901fcf203"
integrity sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA== integrity sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA==
jest-haste-map@^28.1.0: jest-haste-map@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-28.1.0.tgz#6c1ee2daf1c20a3e03dbd8e5b35c4d73d2349cf0" resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-28.1.1.tgz#471685f1acd365a9394745bb97c8fc16289adca3"
integrity sha512-xyZ9sXV8PtKi6NCrJlmq53PyNVHzxmcfXNVvIRHpHmh1j/HChC4pwKgyjj7Z9us19JMw8PpQTJsFWOsIfT93Dw== integrity sha512-ZrRSE2o3Ezh7sb1KmeLEZRZ4mgufbrMwolcFHNRSjKZhpLa8TdooXOOFlSwoUzlbVs1t0l7upVRW2K7RWGHzbQ==
dependencies: dependencies:
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
"@types/graceful-fs" "^4.1.3" "@types/graceful-fs" "^4.1.3"
"@types/node" "*" "@types/node" "*"
anymatch "^3.0.3" anymatch "^3.0.3"
fb-watchman "^2.0.0" fb-watchman "^2.0.0"
graceful-fs "^4.2.9" graceful-fs "^4.2.9"
jest-regex-util "^28.0.2" jest-regex-util "^28.0.2"
jest-util "^28.1.0" jest-util "^28.1.1"
jest-worker "^28.1.0" jest-worker "^28.1.1"
micromatch "^4.0.4" micromatch "^4.0.4"
walker "^1.0.7" walker "^1.0.8"
optionalDependencies: optionalDependencies:
fsevents "^2.3.2" fsevents "^2.3.2"
jest-leak-detector@^28.1.0: jest-leak-detector@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-28.1.0.tgz#b65167776a8787443214d6f3f54935a4c73c8a45" resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-28.1.1.tgz#537f37afd610a4b3f4cab15e06baf60484548efb"
integrity sha512-uIJDQbxwEL2AMMs2xjhZl2hw8s77c3wrPaQ9v6tXJLGaaQ+4QrNJH5vuw7hA7w/uGT/iJ42a83opAqxGHeyRIA== integrity sha512-4jvs8V8kLbAaotE+wFR7vfUGf603cwYtFf1/PYEsyX2BAjSzj8hQSVTP6OWzseTl0xL6dyHuKs2JAks7Pfubmw==
dependencies: dependencies:
jest-get-type "^28.0.2" jest-get-type "^28.0.2"
pretty-format "^28.1.0" pretty-format "^28.1.1"
jest-matcher-utils@^28.1.0: jest-matcher-utils@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-28.1.0.tgz#2ae398806668eeabd293c61712227cb94b250ccf" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-28.1.1.tgz#a7c4653c2b782ec96796eb3088060720f1e29304"
integrity sha512-onnax0n2uTLRQFKAjC7TuaxibrPSvZgKTcSCnNUz/tOjJ9UhxNm7ZmPpoQavmTDUjXvUQ8KesWk2/VdrxIFzTQ== integrity sha512-NPJPRWrbmR2nAJ+1nmnfcKKzSwgfaciCCrYZzVnNoxVoyusYWIjkBMNvu0RHJe7dNj4hH3uZOPZsQA+xAYWqsw==
dependencies: dependencies:
chalk "^4.0.0" chalk "^4.0.0"
jest-diff "^28.1.0" jest-diff "^28.1.1"
jest-get-type "^28.0.2" jest-get-type "^28.0.2"
pretty-format "^28.1.0" pretty-format "^28.1.1"
jest-message-util@^28.1.0: jest-message-util@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-28.1.0.tgz#7e8f0b9049e948e7b94c2a52731166774ba7d0af" resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-28.1.1.tgz#60aa0b475cfc08c8a9363ed2fb9108514dd9ab89"
integrity sha512-RpA8mpaJ/B2HphDMiDlrAZdDytkmwFqgjDZovM21F35lHGeUeCvYmm6W+sbQ0ydaLpg5bFAUuWG1cjqOl8vqrw== integrity sha512-xoDOOT66fLfmTRiqkoLIU7v42mal/SqwDKvfmfiWAdJMSJiU+ozgluO7KbvoAgiwIrrGZsV7viETjc8GNrA/IQ==
dependencies: dependencies:
"@babel/code-frame" "^7.12.13" "@babel/code-frame" "^7.12.13"
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
"@types/stack-utils" "^2.0.0" "@types/stack-utils" "^2.0.0"
chalk "^4.0.0" chalk "^4.0.0"
graceful-fs "^4.2.9" graceful-fs "^4.2.9"
micromatch "^4.0.4" micromatch "^4.0.4"
pretty-format "^28.1.0" pretty-format "^28.1.1"
slash "^3.0.0" slash "^3.0.0"
stack-utils "^2.0.3" stack-utils "^2.0.3"
jest-mock@^28.1.0: jest-mock@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-28.1.0.tgz#ccc7cc12a9b330b3182db0c651edc90d163ff73e" resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-28.1.1.tgz#37903d269427fa1ef5b2447be874e1c62a39a371"
integrity sha512-H7BrhggNn77WhdL7O1apG0Q/iwl0Bdd5E1ydhCJzL3oBLh/UYxAwR3EJLsBZ9XA3ZU4PA3UNw4tQjduBTCTmLw== integrity sha512-bDCb0FjfsmKweAvE09dZT59IMkzgN0fYBH6t5S45NoJfd2DHkS3ySG2K+hucortryhO3fVuXdlxWcbtIuV/Skw==
dependencies: dependencies:
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
"@types/node" "*" "@types/node" "*"
jest-pnp-resolver@^1.2.2: jest-pnp-resolver@^1.2.2:
@ -6962,149 +6963,149 @@ jest-regex-util@^28.0.2:
resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-28.0.2.tgz#afdc377a3b25fb6e80825adcf76c854e5bf47ead" resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-28.0.2.tgz#afdc377a3b25fb6e80825adcf76c854e5bf47ead"
integrity sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw== integrity sha512-4s0IgyNIy0y9FK+cjoVYoxamT7Zeo7MhzqRGx7YDYmaQn1wucY9rotiGkBzzcMXTtjrCAP/f7f+E0F7+fxPNdw==
jest-resolve-dependencies@^28.1.0: jest-resolve-dependencies@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.0.tgz#167becb8bee6e20b5ef4a3a728ec67aef6b0b79b" resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-28.1.1.tgz#3dffaaa56f4b41bc6b61053899d1756401763a27"
integrity sha512-Ue1VYoSZquPwEvng7Uefw8RmZR+me/1kr30H2jMINjGeHgeO/JgrR6wxj2ofkJ7KSAA11W3cOrhNCbj5Dqqd9g== integrity sha512-p8Y150xYJth4EXhOuB8FzmS9r8IGLEioiaetgdNGb9VHka4fl0zqWlVe4v7mSkYOuEUg2uB61iE+zySDgrOmgQ==
dependencies: dependencies:
jest-regex-util "^28.0.2" jest-regex-util "^28.0.2"
jest-snapshot "^28.1.0" jest-snapshot "^28.1.1"
jest-resolve@^28.1.0: jest-resolve@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-28.1.0.tgz#b1f32748a6cee7d1779c7ef639c0a87078de3d35" resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-28.1.1.tgz#bc2eaf384abdcc1aaf3ba7c50d1adf01e59095e5"
integrity sha512-vvfN7+tPNnnhDvISuzD1P+CRVP8cK0FHXRwPAcdDaQv4zgvwvag2n55/h5VjYcM5UJG7L4TwE5tZlzcI0X2Lhw== integrity sha512-/d1UbyUkf9nvsgdBildLe6LAD4DalgkgZcKd0nZ8XUGPyA/7fsnaQIlKVnDiuUXv/IeZhPEDrRJubVSulxrShA==
dependencies: dependencies:
chalk "^4.0.0" chalk "^4.0.0"
graceful-fs "^4.2.9" graceful-fs "^4.2.9"
jest-haste-map "^28.1.0" jest-haste-map "^28.1.1"
jest-pnp-resolver "^1.2.2" jest-pnp-resolver "^1.2.2"
jest-util "^28.1.0" jest-util "^28.1.1"
jest-validate "^28.1.0" jest-validate "^28.1.1"
resolve "^1.20.0" resolve "^1.20.0"
resolve.exports "^1.1.0" resolve.exports "^1.1.0"
slash "^3.0.0" slash "^3.0.0"
jest-runner@^28.1.0: jest-runner@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-28.1.0.tgz#aefe2a1e618a69baa0b24a50edc54fdd7e728eaa" resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-28.1.1.tgz#9ecdb3f27a00059986797aa6b012ba8306aa436c"
integrity sha512-FBpmuh1HB2dsLklAlRdOxNTTHKFR6G1Qmd80pVDvwbZXTriqjWqjei5DKFC1UlM732KjYcE6yuCdiF0WUCOS2w== integrity sha512-W5oFUiDBgTsCloTAj6q95wEvYDB0pxIhY6bc5F26OucnwBN+K58xGTGbliSMI4ChQal5eANDF+xvELaYkJxTmA==
dependencies: dependencies:
"@jest/console" "^28.1.0" "@jest/console" "^28.1.1"
"@jest/environment" "^28.1.0" "@jest/environment" "^28.1.1"
"@jest/test-result" "^28.1.0" "@jest/test-result" "^28.1.1"
"@jest/transform" "^28.1.0" "@jest/transform" "^28.1.1"
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
"@types/node" "*" "@types/node" "*"
chalk "^4.0.0" chalk "^4.0.0"
emittery "^0.10.2" emittery "^0.10.2"
graceful-fs "^4.2.9" graceful-fs "^4.2.9"
jest-docblock "^28.0.2" jest-docblock "^28.1.1"
jest-environment-node "^28.1.0" jest-environment-node "^28.1.1"
jest-haste-map "^28.1.0" jest-haste-map "^28.1.1"
jest-leak-detector "^28.1.0" jest-leak-detector "^28.1.1"
jest-message-util "^28.1.0" jest-message-util "^28.1.1"
jest-resolve "^28.1.0" jest-resolve "^28.1.1"
jest-runtime "^28.1.0" jest-runtime "^28.1.1"
jest-util "^28.1.0" jest-util "^28.1.1"
jest-watcher "^28.1.0" jest-watcher "^28.1.1"
jest-worker "^28.1.0" jest-worker "^28.1.1"
source-map-support "0.5.13" source-map-support "0.5.13"
throat "^6.0.1" throat "^6.0.1"
jest-runtime@^28.1.0: jest-runtime@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-28.1.0.tgz#4847dcb2a4eb4b0f9eaf41306897e51fb1665631" resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-28.1.1.tgz#569e1dc3c36c6c4c0b29516c1c49b6ad580abdaf"
integrity sha512-wNYDiwhdH/TV3agaIyVF0lsJ33MhyujOe+lNTUiolqKt8pchy1Hq4+tDMGbtD5P/oNLA3zYrpx73T9dMTOCAcg== integrity sha512-J89qEJWW0leOsqyi0D9zHpFEYHwwafFdS9xgvhFHtIdRghbadodI0eA+DrthK/1PebBv3Px8mFSMGKrtaVnleg==
dependencies: dependencies:
"@jest/environment" "^28.1.0" "@jest/environment" "^28.1.1"
"@jest/fake-timers" "^28.1.0" "@jest/fake-timers" "^28.1.1"
"@jest/globals" "^28.1.0" "@jest/globals" "^28.1.1"
"@jest/source-map" "^28.0.2" "@jest/source-map" "^28.0.2"
"@jest/test-result" "^28.1.0" "@jest/test-result" "^28.1.1"
"@jest/transform" "^28.1.0" "@jest/transform" "^28.1.1"
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
chalk "^4.0.0" chalk "^4.0.0"
cjs-module-lexer "^1.0.0" cjs-module-lexer "^1.0.0"
collect-v8-coverage "^1.0.0" collect-v8-coverage "^1.0.0"
execa "^5.0.0" execa "^5.0.0"
glob "^7.1.3" glob "^7.1.3"
graceful-fs "^4.2.9" graceful-fs "^4.2.9"
jest-haste-map "^28.1.0" jest-haste-map "^28.1.1"
jest-message-util "^28.1.0" jest-message-util "^28.1.1"
jest-mock "^28.1.0" jest-mock "^28.1.1"
jest-regex-util "^28.0.2" jest-regex-util "^28.0.2"
jest-resolve "^28.1.0" jest-resolve "^28.1.1"
jest-snapshot "^28.1.0" jest-snapshot "^28.1.1"
jest-util "^28.1.0" jest-util "^28.1.1"
slash "^3.0.0" slash "^3.0.0"
strip-bom "^4.0.0" strip-bom "^4.0.0"
jest-snapshot@^28.1.0: jest-snapshot@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-28.1.0.tgz#4b74fa8816707dd10fe9d551c2c258e5a67b53b6" resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-28.1.1.tgz#ab825c16c8d8b5e883bd57eee6ca8748c42ab848"
integrity sha512-ex49M2ZrZsUyQLpLGxQtDbahvgBjlLPgklkqGM0hq/F7W/f8DyqZxVHjdy19QKBm4O93eDp+H5S23EiTbbUmHw== integrity sha512-1KjqHJ98adRcbIdMizjF5DipwZFbvxym/kFO4g4fVZCZRxH/dqV8TiBFCa6rqic3p0karsy8RWS1y4E07b7P0A==
dependencies: dependencies:
"@babel/core" "^7.11.6" "@babel/core" "^7.11.6"
"@babel/generator" "^7.7.2" "@babel/generator" "^7.7.2"
"@babel/plugin-syntax-typescript" "^7.7.2" "@babel/plugin-syntax-typescript" "^7.7.2"
"@babel/traverse" "^7.7.2" "@babel/traverse" "^7.7.2"
"@babel/types" "^7.3.3" "@babel/types" "^7.3.3"
"@jest/expect-utils" "^28.1.0" "@jest/expect-utils" "^28.1.1"
"@jest/transform" "^28.1.0" "@jest/transform" "^28.1.1"
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
"@types/babel__traverse" "^7.0.6" "@types/babel__traverse" "^7.0.6"
"@types/prettier" "^2.1.5" "@types/prettier" "^2.1.5"
babel-preset-current-node-syntax "^1.0.0" babel-preset-current-node-syntax "^1.0.0"
chalk "^4.0.0" chalk "^4.0.0"
expect "^28.1.0" expect "^28.1.1"
graceful-fs "^4.2.9" graceful-fs "^4.2.9"
jest-diff "^28.1.0" jest-diff "^28.1.1"
jest-get-type "^28.0.2" jest-get-type "^28.0.2"
jest-haste-map "^28.1.0" jest-haste-map "^28.1.1"
jest-matcher-utils "^28.1.0" jest-matcher-utils "^28.1.1"
jest-message-util "^28.1.0" jest-message-util "^28.1.1"
jest-util "^28.1.0" jest-util "^28.1.1"
natural-compare "^1.4.0" natural-compare "^1.4.0"
pretty-format "^28.1.0" pretty-format "^28.1.1"
semver "^7.3.5" semver "^7.3.5"
jest-util@^28.1.0: jest-util@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-28.1.0.tgz#d54eb83ad77e1dd441408738c5a5043642823be5" resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-28.1.1.tgz#ff39e436a1aca397c0ab998db5a51ae2b7080d05"
integrity sha512-qYdCKD77k4Hwkose2YBEqQk7PzUf/NSE+rutzceduFveQREeH6b+89Dc9+wjX9dAwHcgdx4yedGA3FQlU/qCTA== integrity sha512-FktOu7ca1DZSyhPAxgxB6hfh2+9zMoJ7aEQA759Z6p45NuO8mWcqujH+UdHlCm/V6JTWwDztM2ITCzU1ijJAfw==
dependencies: dependencies:
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
"@types/node" "*" "@types/node" "*"
chalk "^4.0.0" chalk "^4.0.0"
ci-info "^3.2.0" ci-info "^3.2.0"
graceful-fs "^4.2.9" graceful-fs "^4.2.9"
picomatch "^2.2.3" picomatch "^2.2.3"
jest-validate@^28.1.0: jest-validate@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-28.1.0.tgz#8a6821f48432aba9f830c26e28226ad77b9a0e18" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-28.1.1.tgz#59b7b339b3c85b5144bd0c06ad3600f503a4acc8"
integrity sha512-Lly7CJYih3vQBfjLeANGgBSBJ7pEa18cxpQfQEq2go2xyEzehnHfQTjoUia8xUv4x4J80XKFIDwJJThXtRFQXQ== integrity sha512-Kpf6gcClqFCIZ4ti5++XemYJWUPCFUW+N2gknn+KgnDf549iLul3cBuKVe1YcWRlaF8tZV8eJCap0eECOEE3Ug==
dependencies: dependencies:
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
camelcase "^6.2.0" camelcase "^6.2.0"
chalk "^4.0.0" chalk "^4.0.0"
jest-get-type "^28.0.2" jest-get-type "^28.0.2"
leven "^3.1.0" leven "^3.1.0"
pretty-format "^28.1.0" pretty-format "^28.1.1"
jest-watcher@^28.1.0: jest-watcher@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-28.1.0.tgz#aaa7b4164a4e77eeb5f7d7b25ede5e7b4e9c9aaf" resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-28.1.1.tgz#533597fb3bfefd52b5cd115cd916cffd237fb60c"
integrity sha512-tNHMtfLE8Njcr2IRS+5rXYA4BhU90gAOwI9frTGOqd+jX0P/Au/JfRSNqsf5nUTcWdbVYuLxS1KjnzILSoR5hA== integrity sha512-RQIpeZ8EIJMxbQrXpJQYIIlubBnB9imEHsxxE41f54ZwcqWLysL/A0ZcdMirf+XsMn3xfphVQVV4EW0/p7i7Ug==
dependencies: dependencies:
"@jest/test-result" "^28.1.0" "@jest/test-result" "^28.1.1"
"@jest/types" "^28.1.0" "@jest/types" "^28.1.1"
"@types/node" "*" "@types/node" "*"
ansi-escapes "^4.2.1" ansi-escapes "^4.2.1"
chalk "^4.0.0" chalk "^4.0.0"
emittery "^0.10.2" emittery "^0.10.2"
jest-util "^28.1.0" jest-util "^28.1.1"
string-length "^4.0.1" string-length "^4.0.1"
jest-worker@^26.5.0: jest-worker@^26.5.0:
@ -7116,23 +7117,24 @@ jest-worker@^26.5.0:
merge-stream "^2.0.0" merge-stream "^2.0.0"
supports-color "^7.0.0" supports-color "^7.0.0"
jest-worker@^28.1.0: jest-worker@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-28.1.0.tgz#ced54757a035e87591e1208253a6e3aac1a855e5" resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-28.1.1.tgz#3480c73247171dfd01eda77200f0063ab6a3bf28"
integrity sha512-ZHwM6mNwaWBR52Snff8ZvsCTqQsvhCxP/bT1I6T6DAnb6ygkshsyLQIMxFwHpYxht0HOoqt23JlC01viI7T03A== integrity sha512-Au7slXB08C6h+xbJPp7VIb6U0XX5Kc9uel/WFc6/rcTzGiaVCBRngBExSYuXSLFPULPSYU3cJ3ybS988lNFQhQ==
dependencies: dependencies:
"@types/node" "*" "@types/node" "*"
merge-stream "^2.0.0" merge-stream "^2.0.0"
supports-color "^8.0.0" supports-color "^8.0.0"
jest@^28.1.0: jest@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/jest/-/jest-28.1.0.tgz#f420e41c8f2395b9a30445a97189ebb57593d831" resolved "https://registry.yarnpkg.com/jest/-/jest-28.1.1.tgz#3c39a3a09791e16e9ef283597d24ab19a0df701e"
integrity sha512-TZR+tHxopPhzw3c3560IJXZWLNHgpcz1Zh0w5A65vynLGNcg/5pZ+VildAd7+XGOu6jd58XMY/HNn0IkZIXVXg== integrity sha512-qw9YHBnjt6TCbIDMPMpJZqf9E12rh6869iZaN08/vpOGgHJSAaLLUn6H8W3IAEuy34Ls3rct064mZLETkxJ2XA==
dependencies: dependencies:
"@jest/core" "^28.1.0" "@jest/core" "^28.1.1"
"@jest/types" "^28.1.1"
import-local "^3.0.2" import-local "^3.0.2"
jest-cli "^28.1.0" jest-cli "^28.1.1"
jiti@^1.12.9, jiti@^1.9.2: jiti@^1.12.9, jiti@^1.9.2:
version "1.13.0" version "1.13.0"
@ -9802,10 +9804,10 @@ pretty-error@^2.1.1:
lodash "^4.17.20" lodash "^4.17.20"
renderkid "^2.0.4" renderkid "^2.0.4"
pretty-format@^28.1.0: pretty-format@^28.1.1:
version "28.1.0" version "28.1.1"
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-28.1.0.tgz#8f5836c6a0dfdb834730577ec18029052191af55" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-28.1.1.tgz#f731530394e0f7fcd95aba6b43c50e02d86b95cb"
integrity sha512-79Z4wWOYCdvQkEoEuSlBhHJqWeZ8D8YRPiPctJFCtvuaClGpiwiQYSCUOE6IEKUbbFukKOTFIUAXE8N4EQTo1Q== integrity sha512-wwJbVTGFHeucr5Jw2bQ9P+VYHyLdAqedFLEkdQUVaBF/eiidDwH5OpilINq4mEfhbCjLnirt6HTTDhv1HaTIQw==
dependencies: dependencies:
"@jest/schemas" "^28.0.2" "@jest/schemas" "^28.0.2"
ansi-regex "^5.0.1" ansi-regex "^5.0.1"
@ -12397,16 +12399,16 @@ vuetify-loader@^1.7.3:
file-loader "^6.2.0" file-loader "^6.2.0"
loader-utils "^2.0.0" loader-utils "^2.0.0"
vuetify@2.6.6:
version "2.6.6"
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-2.6.6.tgz#7af178fece5ccd561639eba425fe6c0a5803b37e"
integrity sha512-H4KtxDFmDN8QiTRiGfBySyjMhVaHAJTKB0llGGKZT5jKxtnx9gvEtMWXKtVuRP0NJJP0H6xBPJHNOH7nT18qiQ==
vuetify@^2.6: vuetify@^2.6:
version "2.6.3" version "2.6.3"
resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-2.6.3.tgz#b33ede2da958a40c6ced0ad1f4eda737984b3967" resolved "https://registry.yarnpkg.com/vuetify/-/vuetify-2.6.3.tgz#b33ede2da958a40c6ced0ad1f4eda737984b3967"
integrity sha512-Zfiz2DZnp1DvxqGaSCGqGjv4mPNAurJJ5Xwy7bzNzIySGLlRdlO8UH6aNWnSgfaAsLP3voxadSGDm6tKM8Ys7w== integrity sha512-Zfiz2DZnp1DvxqGaSCGqGjv4mPNAurJJ5Xwy7bzNzIySGLlRdlO8UH6aNWnSgfaAsLP3voxadSGDm6tKM8Ys7w==
"vuetify@npm:@vuetify/nightly@dev":
version "2.6.0-dev-20211110.0"
resolved "https://registry.yarnpkg.com/@vuetify/nightly/-/nightly-2.6.0-dev-20211110.0.tgz#161e078b674d8350ae316a574a304caecab53849"
integrity sha512-0CSbtDP68YZM9yenPsr8b3fe9d8YHxxJvPD0jzMDM/bm6ksVT+qKeb4Wwi9h8zPIphFG2vKICG21yLByg0+5Kg==
vuex@^3.6.2: vuex@^3.6.2:
version "3.6.2" version "3.6.2"
resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71" resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.6.2.tgz#236bc086a870c3ae79946f107f16de59d5895e71"
@ -12431,7 +12433,7 @@ w3c-xmlserializer@^3.0.0:
dependencies: dependencies:
xml-name-validator "^4.0.0" xml-name-validator "^4.0.0"
walker@^1.0.7: walker@^1.0.8:
version "1.0.8" version "1.0.8"
resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f"
integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==