start with recurrent events

This commit is contained in:
lesion 2019-07-11 23:31:37 +02:00
parent 5d45c12aaa
commit c3902a6d64
11 changed files with 316 additions and 120 deletions

View file

@ -16,7 +16,7 @@
el-popover(
placement="bottom"
trigger="click")
Search(past-filter)
Search(past-filter recurrent-filter)
el-menu-item(slot='reference' :title="$t('common.search')" icon='el-share-button')
v-icon(color='lightblue' name='search')
el-badge(v-if='filters.tags.length+filters.places.length>0' is-dot type='warning')

View file

@ -8,7 +8,14 @@
//- )
el-switch.mt-1.mb-1.ml-2.d-block(
v-if='pastFilter'
inactive-text='futuri'
inactive-text=''
active-text='anche appuntamenti fissi'
inactive-color='lightgreen'
v-model='showRecurrent'
)
el-switch.mt-1.mb-1.ml-2.d-block(
v-if='recurrentFilter'
inactive-text='solo futuri'
active-text='anche passati'
inactive-color='lightgreen'
v-model='showPast'
@ -33,24 +40,25 @@ export default {
},
name :'Search',
props: {
pastFilter: Boolean
pastFilter: Boolean,
recurrentFilter: Boolean
},
methods: mapActions(['setSearchPlaces', 'setSearchTags', 'showPastEvents']),
methods: mapActions(['setSearchPlaces', 'setSearchTags', 'showPastEvents', 'showRecurrentEvents']),
computed: {
...mapState(['tags', 'places', 'filters', 'show_past_events']),
...mapState(['tags', 'places', 'filters']),
// TOFIX: optimize
keywords () {
const tags = this.tags.map( t => ({ value: 't' + t.tag, label: t.tag, weigth: t.weigth }))
const places = this.places.map( p => ({ value: 'p' + p.id, label: p.name, weigth: p.weigth }))
return tags.concat(places).sort((a, b) => b.weigth-a.weigth)
},
showPast : {
set (value) {
this.showPastEvents(value)
},
get () {
return this.filters.show_past_events
}
showPast: {
set (value) { this.showPastEvents(value) },
get () { return this.filters.show_past_events }
},
showRecurrent: {
set (value) { this.showRecurrentEvents(value) },
get () { return this.filters.show_recurrent_events }
},
filter: {
set (filters) {

View file

@ -103,10 +103,7 @@ const it = {
tratta di un evento adatto a questo spazio, delegando questa scelta. Inoltre non sarà possibile modificarlo.<br/><br/>
Puoi invece fare il <a href='/login'>login</a> o <a href='/registrarti'>registrarti</a>,
altrimenti vai avanti e riceverai una risposta il prima possibile. `,
multidate_description: 'tanti giorni',
date_description: `Quand'è il gancio?`,
dates_description: 'Che giorni?',
same_day: 'stesso giorno',
same_day: 'Stesso giorno',
what_description: 'Nome evento',
description_description: 'Descrizione, dajene di copia/incolla',
tag_description: 'Tag...',
@ -118,7 +115,21 @@ const it = {
where_description: `Dov'è il gancio? Se il posto non è presente, scrivilo e <b>premi invio</b>. `,
confirmed: 'Evento confermato',
not_found: 'Evento non trovato',
remove_confirmation: `Sicura di voler eliminare questo evento?`
remove_confirmation: `Sicura di voler eliminare questo evento?`,
recurrent: `Ricorrente`,
recurrent_description: 'Scegli la frequenza e seleziona i giorni',
multidate_description: 'Un festival o una tre giorni? Scegli quando comincia e quando finisce.',
multidate: 'Più giorni',
normal: 'Normale',
normal_description: 'Scegli il giorno.',
recurrent_1w_days: 'Ogni {days}',
recurrent_2w_days: 'Un {days} ogni due',
recurrent_1m_days: '|Il giorno {days} di ogni mese|I giorni {days} di ogni mese',
recurrent_2m_days: '|Il giorno {days} ogni due mesi|I giorni {days} ogni due mesi',
recurrent_1m_ordinal: 'Il {n} {days} di ogni mese',
recurrent_2m_ordinal: 'Il {n} {days} un mese sì e uno no',
due: 'alle',
from: 'Dalle'
},
admin: {
@ -132,7 +143,9 @@ const it = {
user_remove_ok: 'Utente eliminato',
user_create_ok: 'Utente creato',
allow_registration_description : 'Vuoi abilitare la registrazione?',
allow_anon_event: 'Si possono inserire eventi anonimi (previa conferma)?'
allow_anon_event: 'Si possono inserire eventi anonimi (previa conferma)?',
allow_comments: 'Abilita commenti',
allow_recurrent_event: 'Abilita eventi ricorrenti'
},
auth: {
@ -152,6 +165,15 @@ const it = {
register_error: 'Errore nella registrazione'
},
ordinal: {
1: 'primo',
2: 'secondo',
3: 'terzo',
4: 'quarto',
5: 'quinto',
[-1]: 'ultimo',
},
about: `
<p>
Gancio e' un progetto dell'<a href='https://autistici.org/underscore'>underscore hacklab</a> e uno dei

View file

@ -14,6 +14,23 @@
p(v-html="$t('event.anon_description')")
el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('common.next')}}
//- WHAT
el-tab-pane
span(slot='label') {{$t('common.what')}} <v-icon name='file-alt'/>
span {{$t('event.what_description')}}
el-input.mb-3(v-model='event.title' ref='title')
span {{$t('event.description_description')}}
el-input.mb-3(v-model='event.description' type='textarea' :rows='9')
span {{$t('event.tag_description')}}
br
el-select(v-model='event.tags' multiple filterable allow-create
default-first-option placeholder='Tag')
el-option(v-for='tag in tags' :key='tag'
:label='tag' :value='tag')
el-button.float-right(@click.native='next' :disabled='!couldProceed') {{$t('common.next')}}
//- WHERE
el-tab-pane
span(slot='label') <v-icon name='map-marker-alt'/> {{$t('common.where')}}
@ -34,49 +51,49 @@
//- WHEN
el-tab-pane
span(slot='label') {{$t('common.when')}} <v-icon name='clock'/>
span {{event.multidate ? $t('event.dates_description') : $t('event.date_description')}}
el-switch.float-right(v-model='event.multidate' :active-text="$t('event.multidate_description')")
//- el-switch.float-right(v-model='event.recurrent' :active-text="$t('event.recurrent_description')")
v-date-picker.mb-3(
:mode='event.multidate ? "range" : "single"'
.text-center
el-radio-group(v-model="event.type")
el-radio-button(label="normal") <v-icon name='calendar-day'/> {{$t('event.normal')}}
el-radio-button(label="multidate") <v-icon name='calendar-week'/> {{$t('event.multidate')}}
el-radio-button(label="recurrent") <v-icon name='calendar-alt'/> {{$t('event.recurrent')}}
br
span {{$t(`event.${event.type}_description`)}}
el-select.ml-2(v-if='event.type==="recurrent"' v-model='event.rec_frequency' placeholder='Frequenza')
el-option(label='Tutti i giorni' value='1d' key='1d')
el-option(label='Ogni settimana' value='1w' key='1w')
el-option(label='Ogni due settimane' value='2w' key='2w')
el-option(label='Ogni mese' value='1m' key='1m')
el-option(label='Ogni due mesi' value='2m' key='2m')
v-date-picker.mb-2.mt-3(
:mode='event.type === "multidate" ? "range" : event.type === "recurrent" ? "multiple" : "single"'
:attributes='attributes'
v-model='date'
:locale='$i18n.locale'
:from-page.sync='page'
is-inline
is-expanded
:min-date='new Date()'
:min-date='event.type !== "recurrent" && new Date()'
)
el-row
el-col(:span='12')
div {{$t('event.time_start_description')}}
el-time-select.mb-3(ref='time_start'
div.text-center.mb-2(v-if='event.type === "recurrent"')
span(v-if='event.rec_frequency !== "1m" && event.rec_frequency !== "2m"') {{whenPatterns}}
el-radio-group(v-else v-model='event.rec_detail')
el-radio-button(v-for='whenPattern in whenPatterns' :label='whenPattern.label' :key='whenPatterns.key')
span {{whenPattern.label}}
el-form.text-center(inline)
el-form-item(:label="$t('event.from')")
el-time-select.mr-2(ref='time_start'
v-model="time.start"
:picker-options="{ start: '00:00', step: '00:30', end: '24:00'}")
div {{$t('event.time_end_description')}}
el-form-item(:label="$t('event.due')")
el-time-select(v-model='time.end'
:picker-options="{start: '00:00', step: '00:30', end: '24:00'}")
el-col(:span='12')
List(:events='todayEvents' :title='$t("event.same_day")')
el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('common.next')}}
//- WHAT
el-tab-pane
span(slot='label') {{$t('common.what')}} <v-icon name='file-alt'/>
span {{$t('event.what_description')}}
el-input.mb-3(v-model='event.title' ref='title')
span {{$t('event.description_description')}}
el-input.mb-3(v-model='event.description' type='textarea' :rows='9')
span {{$t('event.tag_description')}}
br
el-select(v-model='event.tags' multiple filterable allow-create
default-first-option placeholder='Tag')
el-option(v-for='tag in tags' :key='tag'
:label='tag' :value='tag')
el-button.float-right(@click.native='next' :disabled='!couldProceed') {{$t('common.next')}}
List(v-if='event.type==="normal"' :events='todayEvents' :title='$t("event.same_day")')
el-button.float-right(@click='next' type='succes' :disabled='!couldProceed') {{$t('common.next')}}
el-tab-pane
span(slot='label') {{$t('common.media')}} <v-icon name='image'/>
@ -96,6 +113,8 @@
</template>
<script>
import { mapActions, mapState, mapGetters } from 'vuex'
import uniq from 'lodash/uniq'
import map from 'lodash/map'
import moment from 'dayjs'
import List from '@/components/List'
import { Message } from 'element-ui'
@ -111,10 +130,15 @@ export default {
const year = moment().year()
return {
event: {
type: 'normal',
place: { name: '', address: '' },
title: '', description: '', tags: [],
multidate: false,
image: false
image: false,
recurrent: false,
rec_frequency: '1w',
rec_when: null,
rec_ordinal: false,
},
page: { month, year},
fileList: [],
@ -129,6 +153,7 @@ export default {
name: 'newEvent',
watch: {
'time.start' (value) {
if (!value) return
let [h, m] = value.split(':')
this.time.end = (Number(h)+1) + ':' + m
},
@ -186,8 +211,27 @@ export default {
places_name: state => state.places.map(p => p.name ).sort((a, b) => b.weigth-a.weigth),
places: state => state.places,
user: state => state.user,
events: state => state.events
events: state => state.events,
}),
whenPatterns () {
const dates = this.date
if (!dates || !dates.length) return
const freq = this.event.rec_frequency
const weekDays = uniq(map(dates, date => moment(date).format('dddd')))
if (freq === '1w' || freq === '2w') {
return this.$t(`event.recurrent_${freq}_days`, {days: weekDays.join(', ')})
}
if (freq === '1m' || freq === '2m') {
const days = uniq(map(dates, date => moment(date).date()))
const n = Math.floor((days[0]-1)/7)+1
return [
{ label: this.$tc(`event.recurrent_${freq}_days`, days.length, {days}) },
{ label: this.$tc(`event.recurrent_${freq}_ordinal`, days.length, {n: this.$t(`ordinal.${n}`), days: weekDays.join(', ')}) }
]
}
},
todayEvents () {
if (this.event.multidate) {
if (!this.date || !this.date.start) return
@ -222,6 +266,29 @@ export default {
.filter(e => e.multidate)
.map( e => ({ key: e.id, highlight: {}, dates: {
start: new Date(e.start_datetime*1000), end: new Date(e.end_datetime*1000) }})))
if (this.event.type==='recurrent' && this.event.rec_frequency && Array.isArray(this.date)) {
const recurrent = {}
if (this.event.rec_frequency === '1w') {
recurrent.weekdays = this.date.map(d => moment(d).day()+1)
recurrent.weeklyInterval = 1
}
if (this.event.rec_frequency === '2w') {
recurrent.weekdays = this.date.map(d => moment(d).day()+1)
recurrent.weeklyInterval = 2
recurrent.start = new Date(this.date[0])
}
if (this.event.rec_frequency === '1m') {
// recurrent.weeks = 1
// recurrent.ordinalWeekdays = { 1: this.date.map(d => moment(d).day()+1) }
recurrent.days = this.date.map(d => moment(d).date())
recurrent.monthlyInterval = 1
recurrent.start = new Date(this.date[0])
}
if (this.event.rec_frequency === '2m') {
}
attributes.push({name: 'recurrent', dates: recurrent, dot: { color: 'red'}})
}
return attributes
},
disableAddress () {
@ -233,12 +300,12 @@ export default {
case 0+t:
return true
case 1+t:
return this.event.title.length>0
case 2+t:
return this.event.place.name.length>0 &&
this.event.place.address.length>0
case 2+t:
if (this.date && this.time.start) return true
case 3+t:
return this.event.title.length>0
if (this.date && this.time.start) return true
case 4+t:
return this.event.place.name.length>0 &&
this.event.place.address.length>0 &&
@ -299,6 +366,16 @@ export default {
formData.append('multidate', this.event.multidate)
formData.append('start_datetime', start_datetime.unix())
formData.append('end_datetime', end_datetime.unix())
if (this.event.type === 'recurrent') {
const recurrent = {
frequency: this.rec_frequency,
days: this.rec_when,
ordinal: this.rec_ordinal,
}
formData.append('recurrent', JSON.stringify(recurrent))
}
if (this.edit) {
formData.append('id', this.event.id)
}

View file

@ -89,20 +89,28 @@
v-icon(name='cog')
span {{$t('common.settings')}}
el-form(inline @submit.native.prevent='associate_mastondon_instance' label-width='140px')
el-form(inline label-width="400px")
//- allow open registration
el-form-item(:label="$t('admin.allow_registration_description')")
el-switch(name='reg' v-model='allow_registration')
//- allow anon event
el-form-item(:label="$t('admin.allow_anon_event')")
el-switch(v-model='allow_anon_event')
el-form-item(:label="$t('admin.allow_recurrent_event')")
el-switch(v-model='allow_recurrent_event')
el-divider {{$t('admin.federation')}}
el-form(inline @submit.native.prevent='associate_mastondon_instance' label-width='240px')
p {{$t('admin.mastodon_description')}}
el-form-item(:label='$t("admin.mastodon_instance")')
el-input(v-model="mastodon_instance")
el-form-item
el-button(native-type='submit' type='success' :disabled='!mastodon_instance') {{$t('common.associate')}}
hr
p {{$t('admin.allow_registration_description')}}
el-form-item(:label="allow_registration?$t('common.disable'):$t('common.enable')")
el-switch(v-model='allow_registration')
p {{$t('admin.allow_anon_event')}}
el-form-item(:label="allow_anon_event?$t('common.disable'):$t('common.enable')")
el-switch(v-model='allow_anon_event')
el-form-item(:label="$t('admin.allow_comments')")
el-switch(v-model='allow_comments')
</template>
<script>

View file

@ -73,7 +73,9 @@ export default {
}
},
methods: {
// TODO
copy (msg) {
this.$copyText(msg).then(e => console.error('ok ', e)).catch(e => console.error('err ',e))
},
async add_notification () {
if (!this.notification.email){
Message({message:'Inserisci una mail', showClose: true, type: 'error'})

View file

@ -25,6 +25,9 @@ import 'vue-awesome/icons/chevron-right'
import 'vue-awesome/icons/chevron-left'
import 'vue-awesome/icons/search'
import 'vue-awesome/icons/times'
import 'vue-awesome/icons/calendar-day'
import 'vue-awesome/icons/calendar-week'
import 'vue-awesome/icons/calendar-alt'
import Icon from 'vue-awesome/components/Icon'

View file

@ -177,13 +177,22 @@ const eventController = {
async getAll(req, res) {
// this is due how v-calendar shows dates
const start = moment().year(req.params.year).month(req.params.month)
.startOf('month').startOf('isoWeek')
let end = moment().utc().year(req.params.year).month(req.params.month).endOf('month')
const start = moment()
.year(req.params.year)
.month(req.params.month)
.startOf('month')
.startOf('isoWeek')
let end = moment()
.year(req.params.year)
.month(req.params.month)
.endOf('month')
const shownDays = end.diff(start, 'days')
if (shownDays <= 35) end = end.add(1, 'week')
end = end.endOf('isoWeek')
const events = await Event.findAll({
let events = await Event.findAll({
where: {
is_visible: true,
[Op.and]: [
@ -200,7 +209,50 @@ const eventController = {
{ model: Place, required: false, attributes: ['id', 'name', 'address'] }
]
})
res.json(events)
events = events.map(e => {
e.start_datetime = e.start_datetime*1000
e.end_datetime = e.end_datetime*1000
e.tags = e.tags.map(t => t.tag)
return e
})
// build singular events from a recurrent pattern from today due to
// specified parameters
function createEventsFromRecurrent(e, dueTo=null, maxEvents=20) {
const events = []
const cursor = moment()
const start_date = moment(e.start_datetime)
const frequency = e.recurrent.frequency
const days = e.recurrent.days
const ordinal = e.recurrent.ordinal
// EACH WEEK
if (frequency === '1w') {
while(true) {
const found = days.indexOf(cursor.day())
if (found) break
cursor.add(1, 'day')
}
e.start_datetime = cursor.set('hour', e.start_datetime.hour()).set('minute', e.start_datetime.minutes())
while (true) {
if ((dueTo && cursor.isAfter(dueTo)) || events.length>maxEvents) break
e.start_datetime = cursor.unix()
events.push(e)
cursors.add(1, 'week')
}
}
// EACH TWO WEEKS
return events
}
const normalEvents = events.filter(e => !e.recurrent)
const recurrentEvents = events.filter(e => e.recurrent).map(createEventsFromRecurrent)
res.json(normalEvents.concat(recurrentEvents))
}
}

View file

@ -1,6 +1,10 @@
'use strict'
module.exports = (sequelize, DataTypes) => {
const event = sequelize.define('event', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
},
title: DataTypes.STRING,
slug: DataTypes.STRING,
description: DataTypes.TEXT,
@ -19,6 +23,8 @@ module.exports = (sequelize, DataTypes) => {
type: DataTypes.STRING(18),
index: true
},
recurrent: DataTypes.JSON,
// parent: DataTypes.INTEGER
}, {})
event.associate = function (models) {

View file

@ -1,4 +1,12 @@
p= t('email.confirm')
<!DOCTYPE html>
html(lang="en")
head
meta(charset="UTF-8")
meta(name="viewport", content="width=device-width, initial-scale=1.0")
meta(http-equiv="X-UA-Compatible", content="ie=edge")
title #{config.title}
body
p= t('email.confirm')
hr
<a href="#{config.baseurl}"> #{config.title} - #{config.description}</a>
hr
<a href="#{config.baseurl}"> #{config.title} - #{config.description}</a>

View file

@ -1,16 +1,15 @@
import moment from 'dayjs'
import intersection from 'lodash/intersection'
import map from 'lodash/map'
import filter from 'lodash/filter'
import find from 'lodash/find'
export const state = () => ({
// config: {},
locale: '',
events: [],
tags: [],
places: [],
settings: {
},
settings: {},
filters: {
tags: [],
places: [],
@ -23,55 +22,60 @@ export const state = () => ({
export const getters = {
// filter matches search tag/place
filteredEvents: state => {
let events = state.events
filteredEvents: state => {
// TOFIX: use lodash
if (state.filters.tags.length || state.filters.places.length) {
events = events.filter((e) => {
if (state.filters.tags.length) {
const m = intersection(e.tags.map(t => t.tag), state.filters.tags)
if (m.length > 0) return true
}
if (state.filters.places.length) {
if (state.filters.places.find(p => p === e.place.id)) {
return true
}
}
return 0
})
}
const search_for_tags = !!state.filters.tags.length
const search_for_places = !!state.filters.places.length
if (!state.filters.show_past_events) {
events = events.filter(e => !e.past)
}
return state.events.filter(e => {
return events
// filter past events
if (!state.filters.show_past_events && e.past) return false
// filter recurrent events
if (!state.filters.show_recurrent_events && e.recurrent) return false
if (search_for_places) {
if (find(state.filters.places, p => p === e.place.id)) return true
}
if (search_for_tags) {
const common_tags = intersection(map(e.tags, t => t.tag), state.filters.tags);
if (common_tags.length > 0) return true
}
if (!search_for_places && !search_for_tags) return true
return false
})
},
// filter matches search tag/place
filteredEventsWithPast: state => {
let events = state.events
// TOFIX: use lodash
if (state.filters.tags.length || state.filters.places.length) {
events = events.filter((e) => {
if (state.filters.tags.length) {
const m = intersection(e.tags.map(t => t.tag), state.filters.tags)
if (m.length > 0) return true
}
if (state.filters.places.length) {
if (state.filters.places.find(p => p === e.place.id)) {
return true
}
}
return 0
})
}
// filter matches search tag/place including past events
filteredEventsWithPast: state => {
return events
const search_for_tags = !!state.filters.tags.length
const search_for_places = !!state.filters.places.length
return state.events.filter(e => {
const match = false
// filter recurrent events
if (!state.filters.show_recurrent_events && e.recurrent) return false
if (!match && search_for_places) {
if (find(state.filters.places, p => p === e.place.id)) return true
}
if (search_for_tags) {
const common_tags = intersection(map(e.tags, t => t.tag), state.filters.tags);
if (common_tags.length > 0) return true
}
if (!search_for_places && !search_for_tags) return true
return false
})
}
}
export const mutations = {
@ -115,6 +119,9 @@ export const mutations = {
showPastEvents(state, show) {
state.filters.show_past_events = show
},
showRecurrentEvents(state, show) {
state.filters.show_recurrent_events = show
},
setSettings(state, settings) {
state.settings = settings
},
@ -128,7 +135,7 @@ export const mutations = {
export const actions = {
// this method is called server side only for each request
// we use it to get configuration from db
// we use it to get configuration from db, setting locale, etc...
async nuxtServerInit ({ commit }, { app, req } ) {
const settings = await app.$axios.$get('/settings')
commit('setSettings', settings)
@ -168,6 +175,9 @@ export const actions = {
showPastEvents({ commit }, show) {
commit('showPastEvents', show)
},
showRecurrentEvents({ commit }, show ) {
commit('showRecurrentEvents', show)
},
async setSetting({ commit }, setting) {
await this.$axios.$post('/settings', setting )
commit('setSetting', setting)