Merge branch 'recurrent' into dev

This commit is contained in:
les 2020-01-30 15:04:15 +01:00
commit de9b580e22
29 changed files with 529 additions and 374 deletions

View file

@ -1,5 +1,5 @@
<template lang="pug">
nuxt-link.event(:to='`/event/${link}`' :class='{ withImg: event.image_path }')
nuxt-link.event(:to='`/event/${event.id}`' :class='{ withImg: event.image_path }')
//- image
el-image(v-if='showImage && event.image_path' lazy :src='`/media/thumb/${event.image_path}`')
@ -24,7 +24,7 @@ import { mapState } from 'vuex'
export default {
props: {
event: Object,
event: { type: Object, default: () => ({}) },
showTags: {
type: Boolean,
default: true
@ -35,16 +35,7 @@ export default {
}
},
computed: {
...mapState(['settings']),
date () {
return new Date(this.event.start_datetime).getDate()
},
link () {
if (this.event.recurrent) {
return `${this.event.id}_${this.event.start_datetime}`
}
return this.event.id
}
...mapState(['settings'])
}
}
</script>

View file

@ -1,4 +1,4 @@
<template lang="pug">
<template lang='pug'>
div#list
el-divider(v-if='title') {{title}}
el-timeline
@ -6,17 +6,15 @@ div#list
v-for='event in events'
:key='`${event.id}_${event.start_datetime}`'
:timestamp='event|when'
placement='top' icon='el-icon-arrow-down' size='large'
)
placement='top' icon='el-icon-arrow-down' size='large')
div.float-right
small @{{event.place.name}}
a(:href='"/event/" + link(event)' target='_blank') {{event.title}}
a(:href='`/event/${event.id}`' target='_blank') {{event.title}}
hr
</template>
<script>
import { mapGetters } from 'vuex'
export default {
name: 'List',
@ -51,17 +49,6 @@ export default {
type: Boolean,
default: true
}
},
data () {
return { }
},
methods: {
link (event) {
if (event.recurrent) {
return `${event.id}_${event.start_datetime}`
}
return event.id
}
}
}
</script>

View file

@ -1,11 +1,11 @@
<template lang="pug">
el-main
el-switch.mt-1.mb-2.ml-2(
el-switch.mt-1.mb-2.ml-2.d-block(
v-if='recurrentFilter && settings.allow_recurrent_event'
:active-text="$t('event.show_recurrent')"
v-model='showRecurrent')
el-switch.mt-1.mb-2.ml-2(
el-switch.mt-1.mb-2.ml-2.d-block(
v-if='pastFilter'
:active-text="$t('event.show_past')"
v-model='showPast')

View file

@ -72,14 +72,16 @@
"title": "Titolo",
"user": "Utente",
"filter": "Filtra",
"event": "Evento"
"event": "Evento",
"pause": "Pausa",
"start": "Avvia"
},
"login": {
"description": "Entrando puoi pubblicare nuovi eventi.",
"check_email": "Controlla la tua posta (anche lo spam)",
"not_registered": "Non sei registrata?",
"forgot_password": "Dimenticato la password?",
"error": "Errore: ",
"error": "Errore durante il login, controlla i dati.",
"insert_email": "Inserisci la mail",
"ok": "Tutto rego"
},

View file

@ -5,9 +5,9 @@
"author": "lesion",
"scripts": {
"dev:nuxt": "cross-env NODE_ENV=development nuxt dev",
"dev": "cross-env DEBUG=*,-babel*,-follow-redirects,-send,-body-parser:*,-express:*,-connect:*,-sequelize:* NODE_ENV=development node server/index.js",
"dev": "cross-env DEBUG=*,-babel*,-preview-email,-i18n:debug,-email-templates,-follow-redirects,-send,-body-parser:*,-express:*,-connect:*,-sequelize:* NODE_ENV=development node server/index.js",
"build": "nuxt build",
"start": "cross-env DEBUG=*,-babel*,-follow-redirects,-send,-body-parser:*,-express:*,-connect:*,-sequelize:* NODE_ENV=production node server/cli.js",
"start": "cross-env DEBUG=*,-babel*,-preview-email,-i18n:debug,-email-templates,-follow-redirects,-send,-body-parser:*,-express:*,-connect:*,-sequelize:* NODE_ENV=production node server/cli.js",
"lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
"doc": "cd docs && bundle exec jekyll b",
"doc:dev": "cd docs && bundle exec jekyll s --drafts",
@ -82,7 +82,7 @@
"multer": "^1.4.2",
"nuxt": "^2.11.0",
"nuxt-express-module": "^0.0.11",
"pg": "^7.17.1",
"pg": "^7.18.1",
"sanitize-html": "^1.21.1",
"sequelize": "^5.21.3",
"sequelize-cli": "^5.5.1",
@ -101,7 +101,7 @@
"@nuxtjs/eslint-config": "^2.0.0",
"babel-eslint": "^10.0.3",
"eslint": "^6.8.0",
"eslint-config-prettier": "^6.9.0",
"eslint-config-prettier": "^6.10.0",
"eslint-config-standard": ">=14.1.0",
"eslint-loader": "^3.0.3",
"eslint-plugin-import": ">=2.20.0",

View file

@ -24,7 +24,6 @@
<script>
import { mapState } from 'vuex'
import { Message } from 'element-ui'
import get from 'lodash/get'
export default {
name: 'Login',
@ -71,7 +70,7 @@ export default {
Message({ message: this.$t('login.ok'), showClose: true, type: 'success' })
this.$router.replace('/')
} catch (e) {
Message({ message: this.$t('login.error') + this.$t(get(e, 'response.data.message', e)), showClose: true, type: 'error' })
Message({ message: this.$t('login.error'), showClose: true, type: 'error' })
this.loading = false
return
}

View file

@ -42,18 +42,20 @@ export default {
return !this.user.password || !this.user.email || !this.user.description
}
},
mounted () {
this.$refs.email.focus()
},
methods: {
close () {
this.$router.replace('/')
this.$emit('close')
},
async register () {
this.loading = true
try {
const { user } = await this.$axios.$post('/user/register', this.user)
await this.$axios.$post('/user/register', this.user)
Message({
showClose: true,
message: this.$t(`register.${user.is_admin ? 'admin_' : ''}complete`),
message: this.$t('register.complete'),
type: 'success'
})
this.close()

View file

@ -135,7 +135,7 @@ export default {
data.event.type = 'multidate'
} else if (event.recurrent) {
data.event.type = 'recurrent'
data.event.recurrent = JSON.parse(event.recurrent)
data.event.recurrent = event.recurrent
} else {
data.event.type = 'normal'
data.date = moment.unix(event.start_datetime)
@ -184,15 +184,15 @@ export default {
if (!dates || !dates.length) { return '' }
const freq = this.event.recurrent.frequency
const weekDays = _(dates).map(date => moment(date).format('dddd')).uniq()
const weekDays = _(dates).map(date => moment(date).format('dddd')).uniq().value()
if (freq === '1w' || freq === '2w') {
return this.$t(`event.recurrent_${freq}_days`, { days: weekDays.join(', ') })
} else if (freq === '1m' || freq === '2m') {
const days = _(dates).map(date => moment(date).date()).uniq()
const days = _(dates).map(date => moment(date).date()).uniq().value()
const n = Math.floor((days[0] - 1) / 7) + 1
return [
{ label: this.$tc(`event.recurrent_${freq}_days`, days.length, { days }), key: 'ordinal' },
{ label: this.$tc(`event.recurrent_${freq}_ordinal`, days.length, { n: this.$t(`ordinal.${n}`), days: weekDays.join(', ') }), key: 'weekday' }
{ label: this.$tc(`event.recurrent_${freq}_ordinal`, days.length, { n, days: weekDays.join(', ') }), key: 'weekday' }
]
} else if (freq === '1d') {
return this.$t('event.recurrent_each_day')
@ -231,7 +231,6 @@ export default {
.filter(e => !e.multidate && (!e.recurrent || this.event.type === 'recurrent'))
.map(e => ({ key: e.id, dot: { color: this.event.type === 'recurrent' ? 'orange' : 'green' }, dates: moment.unix(e.start_datetime).toDate() })))
console.error(this.event.type)
if (this.event.type === 'recurrent' && this.date && this.date.length) {
attributes.push({
key: 'recurrent',

View file

@ -1,20 +1,19 @@
<template lang="pug">
nuxt-link.embed_event(:to='`/event/${link}`' target='_blank' :class='{ withImg: event.image_path }')
nuxt-link.embed_event(:to='`/event/${id}`' target='_blank' :class='{ withImg: event.image_path }')
//- image
img.float-left(v-if='event.image_path' :src='`/media/thumb/${event.image_path}`')
.event-info
//- image
img.float-left(v-if='event.image_path' :src='`/media/thumb/${event.image_path}`')
.event-info
//- title
.date {{event|when('home')}}<br/>
h4 {{event.title}}
//- title
.date {{event|when('home')}}<br/>
h4 {{event.title}}
//- date / place
.date {{event.place.name}}<br/> {{event.place.address}}
//- date / place
.date {{event.place.name}}<br/> {{event.place.address}}
</template>
<script>
import { mapState } from 'vuex'
import Event from '../../components/Event'
export default {
@ -22,10 +21,8 @@ export default {
components: { Event },
async asyncData ({ $axios, params, error, store }) {
try {
const [id, start_datetime] = params.event_id.split('_')
const event = await $axios.$get(`/event/${id}`)
event.start_datetime = start_datetime ? Number(start_datetime) : event.start_datetime
return { event, id: Number(id) }
const event = await $axios.$get(`/event/${params.event_id}`)
return { event, id: Number(params.event_id) }
} catch (e) {
error({ statusCode: 404, message: 'Event not found' })
}
@ -34,31 +31,10 @@ export default {
return {
loading: true
}
},
computed: {
...mapState(['settings']),
date () {
return new Date(this.event.start_datetime).getDate()
},
link () {
if (this.event.recurrent) {
return `${this.event.id}_${this.event.start_datetime}`
}
return this.event.id
}
}
}
/**
* <style>
.embedded_gancio {
border: none;
width: 450px;
height: 220px;
float: left;
}</style>
<iframe src='http://localhost:13120/embed/1' class='embedded_gancio'></iframe>
*/
// <iframe src='http://localhost:13120/embed/1' class='embedded_gancio'></iframe>
</script>
<style lang='less'>
.embed_event {

View file

@ -29,8 +29,8 @@
//- info & actions for desktop
el-col.menu(:sm='6' :xs='24')
el-menu.menu.mt-2(router)
p <i class='el-icon-time'></i> <b>{{event|when}}</b> <br/><small>{{event|to}}</small>
p <i class='el-icon-map-location'></i> <b>{{event.place.name}}</b> - {{event.place.address}}
p <i class='el-icon-date'></i> <b>{{event|when}}</b> <br/><small>{{event|to}}</small>
p <i class='el-icon-location-outline'></i> <b>{{event.place.name}}</b> - {{event.place.address}}
el-divider {{$t('common.actions')}}
el-menu-item(
v-clipboard:success='copyLink'
@ -80,7 +80,7 @@ import EmbedEvent from './embedEvent'
import FollowMe from './followMe'
import { Message, MessageBox } from 'element-ui'
import moment from 'dayjs'
import moment from 'moment-timezone'
export default {
name: 'Event',
@ -88,17 +88,8 @@ export default {
components: { EventAdmin, EmbedEvent, FollowMe },
async asyncData ({ $axios, params, error, store }) {
try {
const [id, start_datetime] = params.id.split('_')
const event = await $axios.$get(`/event/${id}`)
event.start_datetime = start_datetime
? Number(start_datetime)
: event.start_datetime
// const now = new Date()
// const events = await $axios.$get(
// `/event/${now.getMonth()}/${now.getFullYear()}`
// )
// store.commit('setEvents', events)
return { event, id: Number(id) }
const event = await $axios.$get(`/event/${params.id}`)
return { event, id: Number(params.id) }
} catch (e) {
error({ statusCode: 404, message: 'Event not found' })
}
@ -201,9 +192,6 @@ export default {
if (!event) {
return false
}
if (event.recurrent) {
return `${event.id}_${event.start_datetime}`
}
return event.id
},
prev () {
@ -220,9 +208,6 @@ export default {
if (!event) {
return false
}
if (event.recurrent) {
return `${event.id}_${event.start_datetime}`
}
return event.id
},
imgPath () {
@ -356,8 +341,11 @@ export default {
.menu {
border-right: none;
border-left: 1px solid #e6e6e6;
background-color: transparent;
}
div.menu {
border-left: 1px solid #e6e6e6;
p {
margin-left: 10px;
}
@ -431,7 +419,7 @@ export default {
@media only screen and (max-width: 768px) {
#eventDetail {
.menu {
border: 0px;
border: 0px !important;
}
.title {

View file

@ -1,11 +1,18 @@
<template lang='pug'>
el-menu.menu
div
el-divider {{$t('common.admin')}}
el-menu-item
div(@click.prevents='toggle') {{$t(event.is_visible?'common.hide':'common.confirm')}}
el-menu-item
div(@click.prevent='remove') {{$t('common.remove')}}
el-menu-item(@click='$router.replace(`/add/${event.id}`)') {{$t('common.edit')}}
el-menu.menu
el-menu-item
div(v-if='event.is_visible' @click='toggle(false)') <i class='el-icon-open'/> {{$t('common.hide')}}
div(v-else @click='toggle(false)') <i class='el-icon-turn-off'/> {{$t('common.confirm')}}
el-menu-item(v-if='!event.parentId' @click='remove(false)') <i class='el-icon-delete'/> {{$t('common.remove')}}
el-menu-item(@click='$router.replace(`/add/${event.id}`)') <i class='el-icon-edit'/> {{$t('common.edit')}}
div(v-if='event.parentId')
el-divider {{$t('event.recurrent')}}
el-menu-item(v-if='event.parent.is_visible' @click='toggle(true)') <i class='el-icon-video-pause'/> {{$t('common.pause')}}
el-menu-item(v-else @click='toggle(true)') <i class='el-icon-video-play'/> {{$t('common.start')}}
el-menu-item(@click='remove(true)') <i class='el-icon-delete'/> {{$t('common.remove')}}
el-menu-item(@click='$router.replace(`/add/${event.parentId}`)') <i class='el-icon-edit'/> {{$t('common.edit')}}
</template>
<script>
import { MessageBox } from 'element-ui'
@ -13,30 +20,39 @@ import { mapActions } from 'vuex'
export default {
name: 'EventAdmin',
props: ['event'],
props: {
event: {
type: Object,
default: () => ({})
}
},
methods: {
...mapActions(['delEvent']),
async remove () {
async remove (parent = false) {
try {
await MessageBox.confirm(this.$t('event.remove_confirmation'), this.$t('common.confirm'), {
confirmButtonText: this.$t('common.ok'),
cancelButtonText: this.$t('common.cancel'),
type: 'error' })
await this.$axios.delete(`/user/event/${this.event.id}`)
this.delEvent(Number(this.event.id))
type: 'error'
})
const id = parent ? this.event.parentId : this.event.id
await this.$axios.delete(`/user/event/${id}`)
this.delEvent(Number(id))
this.$router.replace('/')
} catch (e) {
console.error(e)
}
},
async toggle () {
async toggle (parent = false) {
const id = parent ? this.event.parentId : this.event.id
const is_visible = parent ? this.event.parent.is_visible : this.event.is_visible
const method = is_visible ? 'unconfirm' : 'confirm'
try {
if (this.event.is_visible) {
await this.$axios.$get(`/event/unconfirm/${this.event.id}`)
this.event.is_visible = false
await this.$axios.$get(`/event/${method}/${id}`)
if (parent) {
this.event.parent.is_visible = !is_visible
} else {
await this.$axios.$get(`/event/confirm/${this.event.id}`)
this.event.is_visible = true
this.event.is_visible = !is_visible
}
} catch (e) {
console.error(e)

View file

@ -3,7 +3,7 @@
h4(slot='header')
nuxt-link(to='/')
img(src='/favicon.ico')
span {{settings.title}} - {{$t('common.authorize')}}
span {{settings.title}} - {{$t('common.recover_password')}}
div(v-if='valid')
el-input(type='password', :placeholder='$t("common.new_password")' v-model='new_password' prefix-icon='el-icon-lock')
div(v-else) {{$t('recover.not_valid_code')}}

View file

@ -1,6 +1,5 @@
import Vue from 'vue'
import moment from 'moment-timezone'
import url from 'url'
export default ({ app, store }) => {
// set timezone to instance_timezone!!
@ -28,7 +27,7 @@ export default ({ app, store }) => {
const normal = `${start.format('dddd, D MMMM (HH:mm-')}${end.format('HH:mm) ')}`
// recurrent event
if (event.recurrent && where !== 'home') {
const { frequency, days, type } = JSON.parse(event.recurrent)
const { frequency, days, type } = event.recurrent
if (frequency === '1w' || frequency === '2w') {
const recurrent = app.i18n.tc(`event.recurrent_${frequency}_days`, days.length, { days: days.map(d => moment().day(d - 1).format('dddd')) })
return `${normal} - ${recurrent}`

View file

@ -10,6 +10,13 @@ const Auth = {
return oauth.oauthServer.authenticate()(req, res, next)
},
fillUser (req, res, next) {
oauth.oauthServer.authenticate()(req, res, () => {
req.user = res.locals.oauth.token.user
next()
})
},
/** isAdmin middleware */
isAdmin (req, res, next) {
oauth.oauthServer.authenticate()(req, res, () => {

View file

@ -6,6 +6,7 @@ const { event: Event, resource: Resource, tag: Tag, place: Place, notification:
const Sequelize = require('sequelize')
const exportController = require('./export')
const debug = require('debug')('controller:event')
// const { Task, TaskManager } = require('../../taskManager')
const eventController = {
@ -76,17 +77,23 @@ const eventController = {
const format = req.params.format || 'json'
const is_admin = req.user && req.user.is_admin
const id = Number(req.params.event_id)
let event = await Event.findByPk(id, {
attributes: {
exclude: ['createdAt', 'updatedAt']
},
include: [
{ model: Tag, attributes: ['tag', 'weigth'], through: { attributes: [] } },
{ model: Place, attributes: ['name', 'address'] },
{ model: Resource, where: !is_admin && { hidden: false }, required: false }
],
order: [[Resource, 'id', 'DESC']]
})
let event
try {
event = await Event.findByPk(id, {
attributes: {
exclude: ['createdAt', 'updatedAt']
},
include: [
{ model: Tag, attributes: ['tag', 'weigth'], through: { attributes: [] } },
{ model: Place, attributes: ['name', 'address'] },
{ model: Resource, where: !is_admin && { hidden: false }, required: false },
{ model: Event, required: false, as: 'parent' }
],
order: [[Resource, 'id', 'DESC']]
})
} catch (e) {
return res.sendStatus(400)
}
if (event && (event.is_visible || is_admin)) {
event = event.toJSON()
@ -139,8 +146,7 @@ const eventController = {
}
try {
event.is_visible = false
await event.save()
await event.update({ is_visible: false })
res.sendStatus(200)
} catch (e) {
res.sendStatus(404)
@ -185,112 +191,14 @@ const eventController = {
res.sendStatus(200)
},
// async addRecurrent (start, places, where_tags, limit) {
// const where = {
// is_visible: true,
// recurrent: { [Op.ne]: null }
// // placeId: places
// }
// const events = await Event.findAll({
// where,
// limit,
// attributes: {
// exclude: ['slug', 'likes', 'boost', 'userId', 'is_visible', 'description', 'createdAt', 'updatedAt', 'placeId']
// },
// order: ['start_datetime', [Tag, 'weigth', 'DESC']],
// include: [
// { model: Resource, required: false, attributes: ['id'] },
// { model: Tag, ...where_tags, attributes: ['tag'], through: { attributes: [] } },
// { model: Place, required: false, attributes: ['id', 'name', 'address'] }
// ]
// })
// let allEvents = []
// _.forEach(events, e => {
// allEvents = allEvents.concat(eventController.createEventsFromRecurrent(e.get(), start))
// })
// return allEvents
// },
// // build singular events from a recurrent pattern
// createEventsFromRecurrent (e, start, dueTo = null) {
// const events = []
// const recurrent = JSON.parse(e.recurrent)
// if (!recurrent.frequency) { return false }
// if (!dueTo) {
// dueTo = start.add(2, 'month')
// }
// let cursor = start.startOf('week')
// const start_date = moment.unix(e.start_datetime)
// const duration = moment.unix(e.end_datetime).diff(start_date, 's')
// const frequency = recurrent.frequency
// const days = recurrent.days
// const type = recurrent.type
// // default frequency is '1d' => each day
// const toAdd = { n: 1, unit: 'day' }
// // each week or 2 (search for the first specified day)
// if (frequency === '1w' || frequency === '2w') {
// cursor.add(days[0] - 1, 'day')
// if (frequency === '2w') {
// const nWeeks = cursor.diff(e.start_datetime, 'w') % 2
// if (!nWeeks) { cursor.add(1, 'week') }
// }
// toAdd.n = Number(frequency[0])
// toAdd.unit = 'week'
// // cursor.set('hour', start_date.hour()).set('minute', start_date.minutes())
// }
// cursor.set('hour', start_date.hour()).set('minute', start_date.minutes())
// // each month or 2
// if (frequency === '1m' || frequency === '2m') {
// // find first match
// toAdd.n = 1
// toAdd.unit = 'month'
// if (type === 'weekday') {
// } else if (type === 'ordinal') {
// }
// }
// // add event at specified frequency
// while (true) {
// const first_event_of_week = cursor.clone()
// days.forEach(d => {
// if (type === 'ordinal') {
// cursor.date(d)
// } else {
// cursor.day(d - 1)
// }
// if (cursor.isAfter(dueTo) || cursor.isBefore(start)) { return }
// e.start_datetime = cursor.unix()
// e.end_datetime = e.start_datetime + duration
// events.push(Object.assign({}, e))
// })
// if (cursor.isAfter(dueTo)) { break }
// cursor = first_event_of_week.add(toAdd.n, toAdd.unit)
// cursor.set('hour', start_date.hour()).set('minute', start_date.minutes())
// }
// return events
// },
async _select (start = moment.unix(), limit = 100, show_recurrent = true) {
async _select (start = moment.unix(), limit = 100) {
const where = {
// confirmed event only
recurrent: null,
is_visible: true,
start_datetime: { [Op.gt]: start }
}
if (!show_recurrent) {
where.recurrent = null
}
const events = await Event.findAll({
where,
limit,
@ -319,59 +227,104 @@ const eventController = {
async select (req, res) {
const start = req.query.start || moment().unix()
const limit = req.query.limit || 100
const show_recurrent = req.query.show_recurrent || true
res.json(await eventController._select(start, limit, show_recurrent))
// const filter_tags = req.query.tags || ''
// const filter_places = req.query.places || ''
res.json(await eventController._select(start, limit))
},
// debug(`select limit:${limit} rec:${show_recurrent} tags:${filter_tags} places:${filter_places}`)
// let where_tags = {}
// const where = {
// // confirmed event only
// is_visible: true,
// start_datetime: { [Op.gt]: start },
// recurrent: null
// }
/**
* Ensure we have at least 3 instances of recurrent events
*/
_createRecurrentOccurrence (e) {
const event = {
parentId: e.id,
title: e.title,
description: e.description,
image_path: e.image_path,
is_visible: e.is_visible,
userId: e.userId,
placeId: e.placeId
}
// if (filter_tags) {
// where_tags = { where: { tag: filter_tags.split(',') } }
// }
const recurrent = e.recurrent
let left = 3 - e.child.length
const start = e.child.length ? moment.unix(e.child[e.child.length - 1].start_datetime) : moment()
let cursor = start.startOf('week')
const start_date = moment.unix(e.start_datetime)
const duration = moment.unix(e.end_datetime).diff(start_date, 's')
const frequency = recurrent.frequency
const days = recurrent.days
const type = recurrent.type
// if (filter_places) {
// where.placeId = filter_places.split(',')
// }
// default frequency is '1d' => each day
const toAdd = { n: 1, unit: 'day' }
// let events = await Event.findAll({
// where,
// limit,
// attributes: {
// exclude: ['slug', 'likes', 'boost', 'userId', 'is_visible', 'description', 'createdAt', 'updatedAt', 'placeId']
// // include: [[Sequelize.fn('COUNT', Sequelize.col('activitypub_id')), 'ressources']]
// },
// order: ['start_datetime', [Tag, 'weigth', 'DESC']],
// include: [
// { model: Resource, required: false, attributes: ['id'] },
// { model: Tag, ...where_tags, attributes: ['tag'], through: { attributes: [] } },
// { model: Place, required: false, attributes: ['id', 'name', 'address'] }
// ]
// })
// each week or 2 (search for the first specified day)
if (frequency === '1w' || frequency === '2w') {
cursor.add(days[0] - 1, 'day')
if (frequency === '2w') {
const nWeeks = cursor.diff(e.start_datetime, 'w') % 2
if (!nWeeks) { cursor.add(1, 'week') }
}
toAdd.n = Number(frequency[0])
toAdd.unit = 'week'
}
// let recurrentEvents = []
// events = _.map(events, e => e.get())
// if (show_recurrent) {
// recurrentEvents = await eventController.addRecurrent(moment.unix(start), where.placeId, where_tags, limit)
// events = _.concat(events, recurrentEvents)
// }
cursor.set('hour', start_date.hour()).set('minute', start_date.minutes())
// // flat tags
// events = _(events).map(e => {
// e.tags = e.tags.map(t => t.tag)
// return e
// })
// each month or 2
if (frequency === '1m' || frequency === '2m') {
// find first match
toAdd.n = 1
toAdd.unit = 'month'
if (type === 'weekday') {
// res.json(events.sort((a, b) => a.start_datetime - b.start_datetime))
} else if (type === 'ordinal') {
}
}
// add event at specified frequency
while (true) {
if (!left) { break }
left -= 1
const first_event_of_week = cursor.clone()
days.forEach(d => {
debug(cursor)
if (type === 'ordinal') {
cursor.date(d)
} else {
cursor.day(d - 1)
}
event.start_datetime = cursor.unix()
event.end_datetime = event.start_datetime + duration
Event.create(event)
cursor.set('hour', start_date.hour()).set('minute', start_date.minutes())
})
cursor = first_event_of_week.add(toAdd.n, toAdd.unit)
}
},
/**
* Create instances of recurrent events
* Remove old
* @param {*} start_datetime
*/
async _createRecurrent (start_datetime = moment().unix()) {
// select recurrent events
const events = await Event.findAll({
where: { is_visible: true, recurrent: { [Op.ne]: null } },
include: [{ model: Event, as: 'child', required: false, where: { start_datetime: { [Op.gt]: start_datetime } } }],
order: ['start_datetime']
})
const creations = []
events
.filter(e => e.child && e.child.length < 3)
.forEach(e => {
eventController._createRecurrentOccurrence(e)
})
return Promise.all(creations)
}
}
module.exports = eventController

View file

@ -7,6 +7,7 @@ const config = require('config')
const mail = require('../mail')
const { user: User, event: Event, tag: Tag, place: Place } = require('../models')
const settingsController = require('./settings')
const eventController = require('./event')
const debug = require('debug')('user:controller')
const userController = {
@ -37,66 +38,76 @@ const userController = {
* add event
*/
async addEvent (req, res) {
// req.err comes from multer streaming error
if (req.err) {
debug(req.err)
return res.status(400).json(req.err.toString())
}
const body = req.body
const eventDetails = {
title: body.title,
// remove html tags
description: sanitizeHtml(body.description),
multidate: body.multidate,
start_datetime: body.start_datetime,
end_datetime: body.end_datetime,
recurrent: body.recurrent,
// publish this event only if authenticated
is_visible: !!req.user
}
if (req.file) {
eventDetails.image_path = req.file.filename
}
const event = await Event.create(eventDetails)
// create place if needed
let place
try {
place = await Place.findOrCreate({
const body = req.body
const recurrent = body.recurrent ? JSON.parse(body.recurrent) : null
const eventDetails = {
title: body.title,
// remove html tags
description: sanitizeHtml(body.description),
multidate: body.multidate,
start_datetime: body.start_datetime,
end_datetime: body.end_datetime,
recurrent,
// publish this event only if authenticated
is_visible: !!req.user
}
if (req.file) {
eventDetails.image_path = req.file.filename
}
const event = await Event.create(eventDetails)
// create place if needed
const place = await Place.findOrCreate({
where: { name: body.place_name },
defaults: { address: body.place_address }
})
.spread((place, created) => place)
await event.setPlace(place)
event.place = place
// create/assign tags
if (body.tags) {
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true })
const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } })
await Promise.all(tags.map(t => t.update({ weigth: Number(t.weigth) + 1 })))
await event.addTags(tags)
event.tags = tags
}
// associate user to event and reverse
if (req.user) {
await req.user.addEvent(event)
await event.setUser(req.user)
}
// create recurrent instances of event if needed
// without waiting for the task manager
if (event.recurrent) {
eventController._createRecurrent()
}
// return created event to the client
res.json(event)
// send notification (mastodon/email)
// only if user is authenticated
if (req.user) {
const notifier = require('../../notifier')
notifier.notifyEvent('Create', event.id)
}
} catch (e) {
debug(e)
}
// create/assign tags
if (body.tags) {
await Tag.bulkCreate(body.tags.map(t => ({ tag: t })), { ignoreDuplicates: true })
const tags = await Tag.findAll({ where: { tag: { [Op.in]: body.tags } } })
await Promise.all(tags.map(t => t.update({ weigth: Number(t.weigth) + 1 })))
await event.addTags(tags)
event.tags = tags
}
// associate user to event and reverse
if (req.user) {
await req.user.addEvent(event)
await event.setUser(req.user)
}
// return created event to the client
res.json(event)
// send notification (mastodon/email)
// only if user is authenticated
if (req.user) {
const notifier = require('../../notifier')
notifier.notifyEvent('Create', event.id)
res.sendStatus(400)
debug(e.toString())
}
},
@ -151,7 +162,7 @@ const userController = {
if (!user) { return res.sendStatus(200) }
user.recover_code = crypto.randomBytes(16).toString('hex')
mail.send(user.email, 'recover', { user, config })
mail.send(user.email, 'recover', { user, config }, req.settings.locale)
await user.save()
res.sendStatus(200)
@ -205,7 +216,7 @@ const userController = {
if (!req.body.password) { delete req.body.password }
if (!user.is_active && req.body.is_active && user.recover_code) {
mail.send(user.email, 'confirm', { user, config })
mail.send(user.email, 'confirm', { user, config }, req.settings.locale)
}
await user.update(req.body)
@ -226,20 +237,10 @@ const userController = {
req.body.recover_code = crypto.randomBytes(16).toString('hex')
debug('Register user ', req.body.email)
const user = await User.create(req.body)
try {
debug(`Sending registration email to ${user.email}`)
mail.send(user.email, 'register', { user, config })
mail.send(config.admin_email, 'admin_register', { user, config })
} catch (e) {
return res.status(400).json(e)
}
const payload = {
id: user.id,
email: user.email,
scope: [user.is_admin ? 'admin' : 'user']
}
const token = jwt.sign(payload, config.secret)
res.json({ token, user })
debug(`Sending registration email to ${user.email}`)
mail.send(user.email, 'register', { user, config }, req.settings.locale)
mail.send(config.admin_email, 'admin_register', { user, config }, req.settings.locale)
res.sendStatus(200)
} catch (e) {
res.status(404).json(e)
}
@ -250,7 +251,7 @@ const userController = {
req.body.is_active = true
req.body.recover_code = crypto.randomBytes(16).toString('hex')
const user = await User.create(req.body)
mail.send(user.email, 'user_confirm', { user, config })
mail.send(user.email, 'user_confirm', { user, config }, req.settings.locale)
res.json(user)
} catch (e) {
res.status(404).json(e)

View file

@ -2,7 +2,7 @@ const express = require('express')
const multer = require('multer')
const cors = require('cors')()
const { isAuth, isAdmin, hasPerm } = require('./auth')
const { isAuth, isAdmin, hasPerm, fillUser } = require('./auth')
const eventController = require('./controller/event')
const exportController = require('./controller/export')
const userController = require('./controller/user')
@ -46,7 +46,7 @@ api.get('/users', isAdmin, userController.getAll)
api.put('/place', isAdmin, eventController.updatePlace)
// add event
api.post('/user/event', upload.single('image'), userController.addEvent)
api.post('/user/event', fillUser, upload.single('image'), userController.addEvent)
// update event
api.put('/user/event', hasPerm('event:write'), upload.single('image'), userController.updateEvent)

View file

@ -1,15 +1,23 @@
const Email = require('email-templates')
const path = require('path')
const moment = require('moment')
const moment = require('moment-timezone')
const config = require('config')
const settings = require('./controller/settings')
const debug = require('debug')('email')
// TOFIX
moment.locale('it')
const { Task, TaskManager } = require('../taskManager')
const mail = {
send (addresses, template, locals) {
send (addresses, template, locals, locale) {
const task = new Task({
name: 'MAIL',
removable: true,
method: mail._send,
args: [addresses, template, locals, locale]
})
TaskManager.add(task)
},
_send (addresses, template, locales, locale) {
debug(`Send ${template} email to ${addresses}`)
const email = new Email({
views: { root: path.join(__dirname, '..', 'emails') },
@ -43,8 +51,8 @@ const mail = {
bcc: config.admin_email
},
locals: {
...locals,
locale: 'it', // TOFIX
...locales,
locale,
config: { title: config.title, baseurl: config.baseurl, description: config.description },
datetime: datetime => moment.unix(datetime).format('ddd, D MMMM HH:mm')
}

View file

@ -1,5 +1,5 @@
const config = require('config')
const moment = require('moment')
const moment = require('moment-timezone')
module.exports = (sequelize, DataTypes) => {
const Event = sequelize.define('event', {
@ -24,7 +24,7 @@ module.exports = (sequelize, DataTypes) => {
image_path: DataTypes.STRING,
is_visible: DataTypes.BOOLEAN,
recurrent: DataTypes.JSON,
// parent: DataTypes.INTEGER
// parent: DataTypes.INTEGER,
likes: { type: DataTypes.JSON, defaultValue: [] },
boost: { type: DataTypes.JSON, defaultValue: [] }
}, {})
@ -35,6 +35,8 @@ module.exports = (sequelize, DataTypes) => {
Event.belongsToMany(models.tag, { through: 'event_tags' })
Event.belongsToMany(models.notification, { through: 'event_notification' })
Event.hasMany(models.resource)
Event.hasMany(Event, { as: 'child', foreignKey: 'parentId' })
Event.belongsTo(models.event, { as: 'parent' })
}
Event.prototype.toNoteAP = function (username, follower = []) {

View file

@ -21,7 +21,6 @@ module.exports = (sequelize, DataTypes) => {
Notification.associate = function (models) {
Notification.belongsToMany(models.event, { through: 'event_notification' })
// associations can be defined here
}
return Notification
}

View file

@ -1,5 +1,4 @@
const bcrypt = require('bcryptjs')
const debug = require('debug')('model:user')
module.exports = (sequelize, DataTypes) => {
const User = sequelize.define('user', {
@ -38,7 +37,6 @@ module.exports = (sequelize, DataTypes) => {
User.beforeSave(async (user, options) => {
if (user.changed('password')) {
debug('Password for %s modified', user.email)
const salt = await bcrypt.genSalt(10)
const hash = await bcrypt.hash(user.password, salt)
user.password = hash

View file

@ -133,6 +133,7 @@ async function setupQuestionnaire (is_docker, db) {
consola.warn(`"${p}" does not exists, trying to create it`)
try {
mkdirp.sync(p)
consola.info(`${p} succesfully created`)
} catch (e) {
console.error(String(e))
return false
@ -144,7 +145,7 @@ async function setupQuestionnaire (is_docker, db) {
}
questions.push({
name: 'admin.email',
message: `Admin email (a first user with this username will be created)`,
message: 'Admin email (a first user with this username will be created, also used as sender address)',
default: options => {
const baseurl = new url.URL(options.baseurl)
return (
@ -161,24 +162,66 @@ async function setupQuestionnaire (is_docker, db) {
validate: notEmpty
})
questions.push({
name: 'smtp_type',
message: 'How should we send the emails ?',
type: 'list',
choices: ['sendmail', 'SMTP']
})
questions.push({
name: 'smtp.path',
message: 'Where sendmail binary is ?',
default: '/usr/sbin/sendmail',
when: answers => answers.smtp_type === 'sendmail',
validate: sendmail_path => sendmail_path.length > 0 && fs.existsSync(path.resolve(sendmail_path))
})
questions.push({
name: 'smtp.host',
message: 'SMTP Host',
validate: notEmpty
default: 'localhost',
validate: notEmpty,
when: answers => answers.smtp_type !== 'sendmail'
})
questions.push({
name: 'smtp.secure',
message: 'Does SMTP server support TLS?',
when: answers => answers.smtp_type !== 'sendmail' && !['localhost', '127.0.0.1'].includes(answers.smtp.host),
default: true,
type: 'confirm'
})
questions.push({
name: 'smtp.port',
message: 'SMTP Port',
default: answers => ['localhost', '127.0.0.1'].includes(answers.smtp.host) ? 25 : (answers.smtp.secure ? 465 : 587),
when: answers => answers.smtp_type !== 'sendmail'
})
questions.push({
name: 'smtp_need_auth',
message: 'is SMTP authentication needed?',
type: 'confirm',
default: answers => !['localhost', '127.0.0.1'].includes(answers.smtp.host),
when: answers => answers.smtp_type !== 'sendmail'
})
questions.push({
name: 'smtp.auth.user',
message: 'SMTP User',
validate: notEmpty,
default: answers => answers.admin.email
default: answers => answers.admin.email,
when: answers => answers.smtp_type !== 'sendmail' && answers.smtp_need_auth
})
questions.push({
name: 'smtp.auth.pass',
message: 'SMTP Password',
type: 'password',
validate: notEmpty
validate: notEmpty,
when: answers => answers.smtp_type !== 'sendmail' && answers.smtp_need_auth
})
const answers = await inquirer.prompt(questions)
@ -243,7 +286,7 @@ If this is your first run use 'gancio setup --config <CONFIG_FILE.json>' `)
}
async function setup (options) {
consola.info(`You're going to setup gancio on this machine.`)
consola.info('You\'re going to setup gancio on this machine.')
const config = await setupQuestionnaire(options.docker, options.db)
await run_migrations(config.db)
const ret = await firstrun.setup(config, options.config)
@ -251,8 +294,8 @@ async function setup (options) {
process.exit(-1)
}
if (options.docker) {
consola.info(`You can edit ./config.json to modify your configuration.`)
consola.info(`Start the server with "docker-compose up"`)
consola.info('You can edit ./config.json to modify your configuration.')
consola.info('Start the server with "docker-compose up"')
} else {
consola.info(
`You can edit '${options.config}' to modify your configuration. `

View file

@ -16,7 +16,15 @@ module.exports = {
const admin = { email: config.admin.email, password: config.admin.password }
delete config.admin
config.smtp.secure = true
if (config.smtp_type === 'sendmail') {
config.smtp = {
sendmail: true,
newline: 'unix',
path: config.smtp.path
}
}
delete config.smtp_type
delete config.smtp_need_auth
config.admin_email = admin.email
config.db.logging = false
consola.info(`Save configuration to ${config_path}`)

View file

@ -1,18 +1,19 @@
const { Nuxt, Builder } = require('nuxt')
// Import and Set Nuxt.js options
const nuxt_config = require('../nuxt.config.js')
const nuxtConfig = require('../nuxt.config.js')
const config = require('config')
const consola = require('consola')
const { TaskManager } = require('./taskManager')
async function main () {
nuxt_config.server = config.server
nuxtConfig.server = config.server
// Init Nuxt.js
const nuxt = new Nuxt(nuxt_config)
const nuxt = new Nuxt(nuxtConfig)
// Build only in dev mode
if (nuxt_config.dev) {
if (nuxtConfig.dev) {
const builder = new Builder(nuxt)
await builder.build()
} else {
@ -20,9 +21,11 @@ async function main () {
}
nuxt.listen()
consola.info('Listen on %s:%d , visit me here => %s', config.server.host, config.server.port, config.baseurl)
TaskManager.start()
// close connections/port/unix socket
function shutdown () {
TaskManager.stop()
nuxt.close(async () => {
const db = require('./api/models')
await db.sequelize.close()

View file

@ -0,0 +1,19 @@
module.exports = {
up: (queryInterface, Sequelize) => {
// return Promise.resolve(1)
return queryInterface.addColumn('events', 'parentId', {
type: Sequelize.INTEGER,
references: {
model: 'events',
key: 'id'
},
onUpdate: 'CASCADE',
onDelete: 'SET NULL'
})
},
down: (queryInterface, Sequelize) => {
return queryInterface.removeColumn('events', 'parentId')
}
}

View file

@ -0,0 +1,19 @@
const { event: Event } = require('../api/models')
module.exports = {
up: async (queryInterface, Sequelize) => {
const events = await Event.findAll({})
const promises = events.map(e => {
return e.update({ recurrent: JSON.parse(e.recurrent) })
})
return Promise.all(promises)
},
down: async (queryInterface, Sequelize) => {
const events = await Event.findAll({})
const promises = events.map(e => {
return e.update({ recurrent: JSON.stringify(e.recurrent) })
})
return Promise.all(promises)
}
}

View file

@ -0,0 +1,13 @@
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.addIndex('users', {
unique: true,
fields: ['email']
}).catch(e => {})
},
down: (queryInterface, Sequelize) => {
return queryInterface.removeIndex('users', ['email'])
}
}

123
server/taskManager.js Normal file
View file

@ -0,0 +1,123 @@
const debug = require('debug')('TaskManager')
const eventController = require('./api/controller/event')
// const notifier = require('./notifier')
class Task {
constructor ({ name, removable = false, repeatEach = 1, method, args = [] }) {
this.name = name
this.removable = removable
this.repeatEach = repeatEach
this.processInNTick = repeatEach
this.method = method
this.args = args
}
process () {
--this.processInNTick
if (this.processInNTick > 0) {
return
}
this.processInNTick = this.repeatEach
try {
const ret = this.method.apply(this, this.args)
if (ret && typeof ret.then === 'function') {
ret.catch(e => debug('TASK ERROR ', this.name, e))
}
} catch (e) {
debug('TASK ERROR ', this.name, e)
}
}
}
/**
* Manage tasks:
* - Send emails
* - Send AP notifications
* - Create recurrent events
* - Sync AP federation profiles
*/
class TaskManager {
constructor () {
this.interval = 60 * 1000
this.tasks = []
}
start (interval = 60 * 1000) {
this.interval = interval
this.timeout = setTimeout(this.tick.bind(this), interval)
}
stop () {
if (this.timeout) {
debug('STOP')
clearTimeout(this.timeout)
this.timeout = false
}
}
add (task) {
debug('ADD TASK ', task.name)
this.tasks.push(task)
}
process () {
if (!this.tasks.length) {
return
}
this.tasks = this.tasks
.filter(async task => {
if (task.removable) {
await task.process()
} else {
return task
}
})
return Promise.all(this.tasks.map(task => task.process()))
}
async tick () {
debug('TICK')
await this.process()
this.timeout = setTimeout(this.tick.bind(this), this.interval)
}
}
const TS = new TaskManager()
// create and clean recurrent events
TS.add(new Task({
name: 'RECURRENT_EVENT',
method: eventController._createRecurrent,
repeatEach: 10
}))
// daily morning notification
// TS.add(new Task({
// name: 'NOTIFICATION',
// method: notifier._daily,
// repeatEach: 1
// }))
// AP users profile sync
// TaskManager.add(new Task({
// name: 'AP_PROFILE_SYNC',
// method: federation._sync,
// repeatEach: 60 * 24
// }))
// Search for places position via nominatim
// TaskManager.add(new Task({
// name: 'NOMINATIM_QUERY',
// method: places._nominatimQuery,
// repeatEach: 60
// }))
// TS.start()
// TS.add(new Task({ name: 'removable #1', method: daje, args: ['removable #1'], removable: true }))
// TS.add(new Task({ name: 'non removable #2', method: daje, args: ['non removable #2'] }))
// TS.add(new Task({ name: 'non removable and repeat each #2', method: daje, args: ['nn rm and rpt #5'], repeatEach: 5 }))
module.exports = { Task, TaskManager: TS }

View file

@ -44,7 +44,7 @@ export const getters = {
if (!state.filters.show_past_events && e.past) { return false }
// filter recurrent events
if (!state.filters.show_recurrent_events && e.recurrent) { return false }
if (!state.filters.show_recurrent_events && e.parentId) { return false }
if (search_for_places) {
if (search_place_ids.includes(e.place.id)) {
@ -72,7 +72,7 @@ export const getters = {
const match = false
// filter recurrent events
if (!state.filters.show_recurrent_events && e.recurrent) { return false }
if (!state.filters.show_recurrent_events && e.parentId) { return false }
if (!match && search_for_places) {
if (find(state.filters.places, p => p.id === e.place.id)) { return true }