s/cohort/collection

This commit is contained in:
lesion 2022-06-18 01:10:27 +02:00
parent c660bd8eb4
commit da0216a606
No known key found for this signature in database
GPG key ID: 352918250B012177
8 changed files with 157 additions and 133 deletions

View file

@ -1,41 +1,41 @@
<template lang='pug'> <template lang='pug'>
v-container v-container
v-card-title {{$t('common.blobs')}} 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.blobs_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('admin.new_blob')}} 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_blob')}} 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')")
@ -52,7 +52,7 @@ 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"
:delimiters="[',', ';']" :delimiters="[',', ';']"
:items="places" :items="places"
@ -68,7 +68,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(
@ -91,17 +91,17 @@ v-container
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>
@ -118,16 +118,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' }
@ -140,7 +140,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: {
@ -150,8 +150,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(' - ')
}, },
@ -159,28 +159,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.name = this.cohort.name.trim() this.collection.name = this.collection.name.trim()
this.cohort = await this.$axios.$post('/cohorts', this.cohort) this.collection = await this.$axios.$post('/collections', this.collection)
this.$fetch() this.$fetch()
this.loading = false this.loading = false
}, },
@ -195,12 +195,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

@ -87,7 +87,7 @@
"import": "Import", "import": "Import",
"max_events": "N. max events", "max_events": "N. max events",
"label": "Label", "label": "Label",
"blobs": "Blobs", "collections": "Collections",
"close": "Close" "close": "Close"
}, },
"login": { "login": {
@ -235,9 +235,9 @@
"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_blob": "New blob", "new_collection": "New collection",
"blobs_description": "Blobs are groupings of events by tags and places. They will be displayed on the home page", "collections_description": "Collections are groupings of events by tags and places. They will be displayed on the home page",
"edit_blob": "Edit Blob" "edit_collection": "Edit Collection"
}, },
"auth": { "auth": {
"not_confirmed": "Not confirmed yet…", "not_confirmed": "Not confirmed yet…",

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.",
@ -232,10 +232,10 @@
"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_blob": "Crea bolla", "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>", "wrong_domain_warning": "Il \"baseurl\" configurato in config.json <b>({baseurl})</b> è diverso da quello che stai visitando <b>({url})</b>",
"blobs_description": "Le bolle sono raggruppamenti di eventi per tag e posti.", "collections_description": "Le bolle sono raggruppamenti di eventi per tag e posti.",
"edit_blob": "Modifica bolla" "edit_collection": "Modifica bolla"
}, },
"auth": { "auth": {
"not_confirmed": "Non ancora confermato…", "not_confirmed": "Non ancora confermato…",

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

@ -1,31 +0,0 @@
<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>{{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,38 +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 { 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
@ -42,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] })
@ -71,21 +67,7 @@ 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({
where, where,
@ -96,7 +78,7 @@ const cohortController = {
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: [] }
}, },
@ -120,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))
@ -183,4 +165,4 @@ const cohortController = {
module.exports = cohortController module.exports = collectionController

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

@ -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');
*/
}
};