fix #27, fix #25, fix #26, fix #20, fix #19

This commit is contained in:
lesion 2019-03-18 01:42:42 +01:00
parent b93cf91dbd
commit 7111b4578b
31 changed files with 270 additions and 70 deletions

View file

@ -61,6 +61,7 @@ api.get('/event/:event_id', eventController.get)
// confirm event
api.get('/event/confirm/:event_id', isAuth, isAdmin, eventController.confirm)
api.get('/event/unconfirm/:event_id', isAuth, isAdmin, eventController.unconfirm)
// export events (rss/ics)
api.get('/export/:type', exportController.export)

View file

@ -82,6 +82,18 @@ const eventController = {
}
},
async unconfirm (req, res) {
const id = req.params.event_id
const event = await Event.findByPk(id)
try {
await event.update({ is_visible: false })
res.send(200)
} catch (e) {
res.send(404)
}
},
async getUnconfirmed (req, res) {
const events = await Event.findAll({
where: {

View file

@ -198,6 +198,9 @@ const userController = {
async update (req, res) {
const user = await User.findByPk(req.body.id)
if (user) {
if (!user.is_active && req.body.is_active) {
await mail.send(user.email, 'confirm', { user, config })
}
await user.update(req.body)
res.json(user)
} else {
@ -209,7 +212,7 @@ const userController = {
const n_users = await User.count()
try {
if (n_users === 0) {
// admin will be the first registered user
// the first registered user will be an active admin
req.body.is_active = req.body.is_admin = true
} else {
req.body.is_active = false

View file

@ -14,7 +14,7 @@ async function sendNotification (notification, event, eventNotification) {
const p = mail.send(notification.email, 'event', { event })
promises.push(p)
break
case 'mail_admin':
case 'admin_email':
const admins = await User.findAll({ where: { is_admin: true } })
promises.push(admins.map(admin =>
mail.send(admin.email, 'event', { event, to_confirm: true, notification })))
@ -26,7 +26,7 @@ async function sendNotification (notification, event, eventNotification) {
promises.push(b)
}
// user publish
if (event.user && event.user.mastodon_auth) {
if (event.user && event.user.mastodon_auth && event.user.mastodon_auth.access_token) {
const b = bot.post(event.user.mastodon_auth, event).then(ret => {
event.activitypub_id = ret.id
return event.save()
@ -45,17 +45,24 @@ async function sendNotification (notification, event, eventNotification) {
async function loop () {
settings = await settingsController.settings()
// get all event notification in queue
const eventNotifications = await EventNotification.findAll()
const eventNotifications = await EventNotification.findAll({ where: { status: 'new' } })
const promises = eventNotifications.map(async e => {
const event = await Event.findByPk(e.eventId, { include: [User, Place, Tag] })
if (!event.place) return
const notification = await Notification.findByPk(e.notificationId)
await sendNotification(notification, event, e)
e.destroy()
try {
await sendNotification(notification, event, e)
e.status = 'sent'
e.save()
} catch (e) {
console.error(e)
e.status = 'error'
return e.save()
}
})
return Promise.all(promises)
}
setInterval(loop, 20000)
setInterval(loop, 260000)
loop()

View file

@ -0,0 +1,4 @@
p= t('confirm_email')
hr
small #{config.baseurl}

View file

@ -1,6 +1,4 @@
h4 Gancio
p= t('registration_email')
small --
small https://cisti.org
hr
small #{config.baseurl}

View file

@ -1,3 +0,0 @@
{
"registration_email": "Ciao, la tua registrazione sarà confermata nei prossimi giorni. Riceverai una conferma non temere."
}

View file

@ -1,3 +0,0 @@
{
"registration_email": "registration_email"
}

View file

@ -1,3 +0,0 @@
{
"registration_email": "Ciao, la tua registrazione sarà confermata nei prossimi giorni. Riceverai una conferma non temere."
}

View file

@ -1,3 +0,0 @@
{
"registration_email": "registration_email"
}

View file

@ -0,0 +1,26 @@
'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
/*
Add altering commands here.
Return a promise to correctly handle asynchronicity.
Example:
return queryInterface.createTable('users', { id: Sequelize.INTEGER });
*/
return queryInterface.addColumn('EventNotifications', 'status',
{ type: Sequelize.ENUM, values: ['new', 'sent', 'error'], index: true, defaultValue: 'new' })
},
down: (queryInterface, Sequelize) => {
/*
Add reverting commands here.
Return a promise to correctly handle asynchronicity.
Example:
return queryInterface.dropTable('users');
*/
return queryInterface.removeColumn('EventNotifications', 'status')
}
};

View file

@ -48,7 +48,15 @@ Event.hasMany(Comment)
Event.belongsToMany(Tag, { through: 'tagEvent' })
Tag.belongsToMany(Event, { through: 'tagEvent' })
const EventNotification = db.define('EventNotification')
const EventNotification = db.define('EventNotification', {
status: {
type: Sequelize.ENUM,
values: ['new', 'sent', 'error'],
defaultValue: 'new',
index: true
}
})
Event.belongsToMany(Notification, { through: EventNotification })
Notification.belongsToMany(Event, { through: EventNotification })

37
app/models/index.js Normal file
View file

@ -0,0 +1,37 @@
'use strict';
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const db = {};
let sequelize;
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}
fs
.readdirSync(__dirname)
.filter(file => {
return (file.indexOf('.') !== 0) && (file !== basename) && (file.slice(-3) === '.js');
})
.forEach(file => {
const model = sequelize['import'](path.join(__dirname, file));
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;

View file

@ -1,3 +1,6 @@
module.exports = {
'extends': 'standard'
'extends': 'standard',
"rules": {
"camelcase": 0
}
}

View file

@ -18,6 +18,8 @@ function get (path) {
store.commit('logout')
return false
}
throw e.response && e.response.data &&
e.response.data.errors && e.response.data.errors[0].message
})
}
@ -29,6 +31,8 @@ function post (path, data) {
store.commit('logout')
return false
}
throw e.response && e.response.data &&
e.response.data.errors && e.response.data.errors[0].message
})
}
function put (path, data) {
@ -43,12 +47,18 @@ function del (path) {
export default {
login: (email, password) => post('/login', { email, password }),
register: user => post('/user', user),
getAllEvents: (month, year) => get(`/event/${year}/${month}/`),
getUnconfirmedEvents: () => get('/event/unconfirmed'),
confirmEvent: id => get(`/event/confirm/${id}`),
unconfirmEvent: id => get(`/event/unconfirm/${id}`),
addNotification: notification => post('/event/notification', notification),
addEvent: event => post('/user/event', event),
updateEvent: event => put('/user/event', event),
updatePlace: place => put('/place', place),
delEvent: eventId => del(`/user/event/${eventId}`),
getEvent: eventId => get(`/event/${eventId}`),
@ -59,8 +69,6 @@ export default {
updateUser: user => put('/user', user),
getAuthURL: mastodonInstance => post('/user/getauthurl', mastodonInstance),
setCode: code => post('/user/code', code),
getKnowLocations: () => get('/locations'),
getKnowTags: () => get('/tags'),
getAdminSettings: () => get('/settings')
// setAdminSetting: (key, value) => post('/settings', { key, value })
}

View file

@ -0,0 +1,25 @@
<template lang="pug">
b-modal(hide-footer @hidden='$router.replace("/")' :title='$t("About")'
:visible='true' size='lg')
h5 Chi siamo
p.
Gancio e' un progetto dell'<a href='https://autistici.org/underscore'>underscore hacklab</a> e uno dei
servizi di <a href='https://cisti.org'>cisti.org</a>.
h5 Ok, ma cosa vuol dire?
blockquote.
Se vieni a Torino e dici: "ehi, ci diamo un gancio alle 8?" nessuno si presenterà con i guantoni per fare a mazzate.
Darsi un gancio vuol dire beccarsi alle ore X in un posto Y
p
small A: a che ora è il gancio in radio per andare al presidio?
p
small B: non so ma domani non posso venire, ho gia' un gancio per caricare il bar.
h5 Contatti
p.
Hai scritto una nuova interfaccia per gancio? Vuoi aprire un nuovo nodo di gancio nella tua città?
C'è qualcosa che vorresti migliorare? Aiuti e suggerimenti sono sempre benvenuti, puoi scriverci
su underscore chicciola autistici.org
</template>

View file

@ -1,16 +1,26 @@
<template lang="pug">
b-modal(hide-footer @hidden='$router.replace("/")' :title='$t("Admin")' :visible='true' size='lg')
b-modal(hide-footer @hidden='$router.replace("/")' :title='$t("Admin")'
:visible='true' size='lg')
el-tabs(tabPosition='left' v-model='tab')
//- USERS
el-tab-pane.pt-1
template(slot='label')
v-icon(name='users')
span.ml-1 {{$t('Users')}}
b-table(:items='users' :fields='userFields' striped small hover
:per-page='5' :current-page='userPage')
template(slot='action' slot-scope='data')
el-button.mr-1(size='mini' :type='data.item.is_active?"warning":"success"' @click='toggle(data.item)') {{data.item.is_active?$t('Deactivate'):$t('Activate')}}
el-button(size='mini' :type='data.item.is_admin?"danger":"warning"' @click='toggleAdmin(data.item)') {{data.item.is_admin?$t('Remove Admin'):$t('Admin')}}
el-table(:data='paginatedUsers' small)
el-table-column(label='Email')
template(slot-scope='data')
el-popover(trigger='hover' :content='data.row.description' width='400')
span(slot='reference') {{data.row.email}}
el-table-column(label='Azioni')
template(slot-scope='data')
el-button.mr-1(size='mini'
:type='data.row.is_active?"warning":"success"'
@click='toggle(data.row)') {{data.row.is_active?$t('Deactivate'):$t('Activate')}}
el-button(size='mini'
:type='data.row.is_admin?"danger":"warning"'
@click='toggleAdmin(data.row)') {{data.row.is_admin?$t('Remove Admin'):$t('Admin')}}
el-pagination(:page-size='perPage' :currentPage.sync='userPage' :total='users.length')
//- PLACES
@ -124,8 +134,12 @@ export default {
this.eventPage * this.perPage)
},
paginatedTags () {
return this.tags.slice((this.tagPage-1) * this.perPage,
return this.tags.slice((this.tagPage-1) * this.perPage,
this.tagPage * this.perPage)
},
paginatedUsers () {
return this.users.slice((this.userPage-1) * this.perPage,
this.userPage * this.perPage)
}
},
methods: {

View file

@ -18,6 +18,8 @@
size='mini' :key='tag.tag') {{tag.tag}}
.ml-auto(v-if='mine')
hr
el-button(v-if='event.is_visible' plain type='warning' @click.prevents='toggle' icon='el-icon-remove') {{$t('Unconfirm')}}
el-button(v-else plain type='success' @click.prevents='toggle' icon='el-icon-remove') {{$t('Confirm')}}
el-button(plain type='danger' @click.prevent='remove' icon='el-icon-remove') {{$t('Remove')}}
el-button(plain type='primary' @click='$router.replace("/edit/"+event.id)') <v-icon color='orange' name='edit'/> {{$t('Edit')}}
@ -75,6 +77,20 @@ export default {
await api.delEvent(this.event.id)
this.delEvent(this.event.id)
this.$refs.eventDetail.hide()
},
async toggle () {
try {
if (this.event.is_visible) {
await api.unconfirmEvent(this.id)
this.event.is_visible = false
} else {
await api.confirmEvent(this.id)
this.event.is_visible = true
}
} catch (e) {
}
}
}
}

View file

@ -15,7 +15,8 @@
<script>
import api from '@/api'
import { mapActions } from 'vuex'
import { log } from 'util';
import { Message } from 'element-ui'
export default {
name: 'Login',
@ -29,13 +30,19 @@ export default {
...mapActions(['login']),
async submit (e) {
e.preventDefault()
const user = await api.login(this.email, this.password)
if (!user) {
return;
try {
const user = await api.login(this.email, this.password)
if (!user) {
Message({ message: this.$t('login error'), type: 'error' })
return;
}
this.login(user)
Message({ message: this.$t('Logged'), type: 'success' })
} catch (e) {
Message({ message: this.$t('login error'), type: 'error' })
}
this.login(user)
this.email = this.password = ''
this.$router.go(-1)
this.$router.replace("/")
}
}
}

View file

@ -14,8 +14,12 @@
span.d-md-none {{$t('Admin')}}
b-nav-item(to='/export' v-b-tooltip :title='$t("Export")') <v-icon name='file-export' color='yellow'/>
span.d-md-none {{$t('Export')}}
b-nav-item(v-if='logged' variant='danger' @click='logout' v-b-tooltip :title='$t("Logout")') <v-icon color='red' name='sign-out-alt'/>
b-nav-item(v-if='logged' @click='logout' v-b-tooltip :title='$t("Logout")') <v-icon color='red' name='sign-out-alt'/>
span.d-md-none {{$t('Logout')}}
b-navbar-nav.ml-auto
b-nav-item(to='/about')
span {{$t('Info')}} <v-icon color='#ff9fc4' name='question-circle'/>
</template>
<script>
import {mapState, mapActions} from 'vuex'

View file

@ -35,8 +35,8 @@ export default {
async register () {
try {
const user = await api.register(this.user)
this.$refs.modal.hide()
if (!user.is_admin) {
this.$refs.modal.hide()
Message({
message: this.$t('registration_complete'),
type: 'success'
@ -48,6 +48,10 @@ export default {
})
}
} catch (e) {
Message({
message: e,
type: 'error'
})
console.error(e)
}
}

View file

@ -1,5 +1,5 @@
<template lang="pug">
b-modal(:title="$t('Settings')" hide-footer @hide='$router.go(-1)' :visible='true')
b-modal(:title="$t('Settings')" hide-footer @hidden='$router.replace("/")' :visible='true')
el-form(inline)
el-input(v-model="mastodon_instance" type='success')
span(slot='prepend') Mastodon instance

View file

@ -1,35 +1,40 @@
<template lang="pug">
b-modal(ref='modal' @hidden='$router.replace("/")' size='md' :visible='true'
b-modal(ref='modal' @hidden='$router.replace("/")' size='lg' :visible='true'
:title="edit?$t('Edit event'):$t('New event')" hide-footer)
b-container
el-tabs.mb-2(v-model='activeTab' v-loading='sending' @tab-click.native='changeTab')
//- NOT LOGGED EVENT
el-tab-pane(v-show='!user')
span(slot='label') {{$t('anon_newevent')}} <v-icon name='user-secret'/>
p(v-html="$t('anon_newevent_explanation')")
el-button.float-right(@click='next' :disabled='!couldProceed') Mi sento in colpa
//- WHERE
el-tab-pane
span(slot='label') {{$t('Where')}} <v-icon name='map-marker-alt'/>
p {{$t('where_explanation')}}
el-form(label-width='120px')
el-form-item(:label='$t("Where")')
el-select(v-model='event.place.name' @change='placeChoosed' filterable allow-create default-first-option)
el-option(v-for='place in places_name' :label='place' :value='place' :key='place.id')
el-form-item(:label='$t("Address")')
el-input(ref='address' v-model='event.place.address' @keydown.native.enter='next')
el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('Next')}}
div {{$t('where_explanation')}}
el-select.mb-3(v-model='event.place.name' @change='placeChoosed' filterable allow-create default-first-option)
el-option(v-for='place in places_name' :label='place' :value='place' :key='place.id')
div {{$t("Address")}}
el-input.mb-3(ref='address' v-model='event.place.address' @keydown.native.enter='next')
el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('Next')}}
//- WHEN
el-tab-pane
span(slot='label') {{$t('When')}} <v-icon name='clock'/>
el-form(label-width='120px')
span {{event.multidate ? $t('dates_explanation') : $t('date_explanation')}}
span {{event.multidate ? $t('dates_explanation') : $t('date_explanation')}}
el-switch.float-right(v-model='event.multidate' :active-text="$t('multidate_explanation')")
v-date-picker.mb-3(:mode='event.multidate ? "range" : "single"' v-model='date' is-inline
is-expanded :min-date='new Date()' @input='date ? $refs.time_start.focus() : false')
el-form-item(:label="$t('time_start_explanation')")
el-time-select(ref='time_start'
v-model="time.start"
:picker-options="{ start: '00:00', step: '00:30', end: '24:00'}")
el-form-item(:label="$t('time_end_explanation')")
el-time-select(v-model='time.end'
:picker-options="{start: '00:00', step: '00:30', end: '24:00'}")
el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('Next')}}
v-date-picker.mb-3(:mode='event.multidate ? "range" : "single"' v-model='date' is-inline
is-expanded :min-date='new Date()' @input='date ? $refs.time_start.focus() : false')
div {{$t('time_start_explanation')}}
el-time-select.mb-3(ref='time_start'
v-model="time.start"
:picker-options="{ start: '00:00', step: '00:30', end: '24:00'}")
div {{$t('time_end_explanation')}}
el-time-select(v-model='time.end'
:picker-options="{start: '00:00', step: '00:30', end: '24:00'}")
el-button.float-right(@click='next' :disabled='!couldProceed') {{$t('Next')}}
el-tab-pane
span(slot='label') {{$t('What')}} <v-icon name='file-alt'/>
@ -111,20 +116,23 @@ export default {
...mapState({
tags: state => state.tags.map(t => t.tag ),
places_name: state => state.places.map(p => p.name ),
places: state => state.places
places: state => state.places,
user: state => state.user
}),
couldProceed () {
switch(Number(this.activeTab)) {
case 0:
return true
case 1:
return this.event.place.name.length>0 &&
this.event.place.address.length>0
case 1:
case 2:
if (this.date && this.time.start) return true
break
case 2:
case 3:
return this.event.title.length>0
break
case 3:
case 4:
return true
break
}

View file

@ -77,6 +77,8 @@ const it = {
'admin_place_explanation': 'Puoi modificare i luoghi inseriti',
'Edit event': 'Modifica evento',
'New event': 'Nuovo evento',
anon_newevent: 'Evento senza autore',
anon_newevent_explanation: `Puoi inserire un evento senza registrarti o fare il login, ma in questo caso dovrai aspettare che qualcuno lo legga confermando che si tratta di un evento adatto a questo spazio, delegando questa scelta.<br/><br/>Ti consigliamo quindi di fare il <a href="/login">login</a> o di <a href="/register">registrarti</a>, ma se vuoi continuare fai pure (ti devi sentire un po' in colpa però).`,
'Insert your address': 'Inserisci il tuo indirizzo',
registration_complete: 'Controlla la tua posta (anche la cartella spam)',
registration_email: `Ciao, la tua registrazione sarà confermata nei prossimi giorni. Riceverai una conferma non temere.`,

View file

@ -19,6 +19,8 @@ import 'vue-awesome/icons/users'
import 'vue-awesome/icons/calendar'
import 'vue-awesome/icons/edit'
import 'vue-awesome/icons/envelope-open-text'
import 'vue-awesome/icons/user-secret'
import 'vue-awesome/icons/question-circle'
import Icon from 'vue-awesome/components/Icon'
@ -30,7 +32,7 @@ import 'bootstrap-vue/dist/bootstrap-vue.css'
import { Button, Select, Tag, Option, Table, FormItem, Card,
Form, Tabs, TabPane, Switch, Input, Loading, TimeSelect,
TableColumn, ColorPicker, Pagination } from 'element-ui'
TableColumn, ColorPicker, Pagination, Popover } from 'element-ui'
import ElementLocale from 'element-ui/lib/locale'
import MagicGrid from 'vue-magic-grid'
@ -49,6 +51,7 @@ import itLocale from '@/locale/it'
import enLocale from '@/locale/en'
Vue.use(Button)
Vue.use(Popover)
Vue.use(Card)
Vue.use(Select)
Vue.use(Tag)

View file

@ -8,6 +8,7 @@ import Login from './components/Login'
import Register from './components/Register'
import Export from './components/Export'
import Admin from './components/Admin'
import About from './components/About'
Vue.use(Router)
@ -50,6 +51,10 @@ export default new Router({
{
path: '/admin/oauth',
components: { modal: Admin }
},
{
path: '/about',
components: { modal: About }
}
]
})

View file

@ -17,7 +17,8 @@ export default new Vuex.Store({
token: state => state.token,
filteredEvents: state => {
const events = state.events.map(e => {
const past = (moment().diff(e.start_datetime, 'minutes') > 0)
const end_datetime = e.end_datetime || moment(e.start_datetime).add('3', 'hour')
const past = (moment().diff(end_datetime, 'minutes') > 0)
e.past = past
return e
})

4
locales/en.json Normal file
View file

@ -0,0 +1,4 @@
{
"registration_email": "Ciao, la tua registrazione sarà confermata nei prossimi giorni. Riceverai una conferma non temere.",
"confirm_email": "confirm_email"
}

4
locales/es.json Normal file
View file

@ -0,0 +1,4 @@
{
"registration_email": "registration_email",
"confirm_email": "confirm_email"
}

4
locales/it.json Normal file
View file

@ -0,0 +1,4 @@
{
"registration_email": "Ciao, la tua registrazione sarà confermata nei prossimi giorni. Riceverai una conferma non temere.",
"confirm_email": "Ti abbiamo attivato il tuo utente su gancio. Ora puoi aggiungere eventi."
}

4
locales/zh.json Normal file
View file

@ -0,0 +1,4 @@
{
"registration_email": "registration_email",
"confirm_email": "confirm_email"
}