From 4463c75536f991c62221335ea9b60d3091a68365 Mon Sep 17 00:00:00 2001 From: lesion Date: Tue, 13 Dec 2022 15:41:39 +0100 Subject: [PATCH] allow to edit tags in admin panel, fix #170 --- components/admin/Collections.vue | 2 +- components/admin/Places.vue | 2 +- components/admin/Tags.vue | 115 +++++++++++++++++++++++++++++++ locales/en.json | 4 ++ locales/it.json | 2 + pages/Admin.vue | 6 ++ server/api/controller/tag.js | 57 ++++++++++++++- server/api/index.js | 8 ++- tests/app.test.js | 4 +- 9 files changed, 194 insertions(+), 6 deletions(-) create mode 100644 components/admin/Tags.vue diff --git a/components/admin/Collections.vue b/components/admin/Collections.vue index 260a796d..5dd87531 100644 --- a/components/admin/Collections.vue +++ b/components/admin/Collections.vue @@ -33,7 +33,7 @@ v-container :prepend-icon="mdiTagMultiple" chips small-chips multiple deletable-chips hide-no-data hide-selected persistent-hint :disabled="!collection.id" - placeholder='Tutte' + placeholder='All' @input.native='searchTags' @focus='searchTags' :delimiters="[',', ';']" diff --git a/components/admin/Places.vue b/components/admin/Places.vue index 152fff48..6c8c26b7 100644 --- a/components/admin/Places.vue +++ b/components/admin/Places.vue @@ -89,7 +89,7 @@ export default { } }, async fetch() { - this.places = await this.$axios.$get('/place/all') + this.places = await this.$axios.$get('/places') }, computed: { ...mapState(['settings']), diff --git a/components/admin/Tags.vue b/components/admin/Tags.vue new file mode 100644 index 00000000..6670ebca --- /dev/null +++ b/components/admin/Tags.vue @@ -0,0 +1,115 @@ + + diff --git a/locales/en.json b/locales/en.json index ea95bbfd..b074a9f5 100644 --- a/locales/en.json +++ b/locales/en.json @@ -82,6 +82,7 @@ "url": "URL", "place": "Place", "tags": "Tags", + "tag": "Tag", "theme": "Theme", "reset": "Reset", "import": "Import", @@ -213,6 +214,7 @@ "hide_resource": "Hide resource", "delete_resource": "Delete resource", "delete_resource_confirm": "Are you sure you want to delete this resource?", + "delete_tag_confirm": "Are you sure you want to remove the tag \"{tag}\"? The tag will be removed from {n} events.", "block_user": "Block user", "filter_instances": "Filter instances", "filter_users": "Filter users", @@ -244,6 +246,8 @@ "footer_links": "Footer links", "delete_footer_link_confirm": "Sure to remove this link?", "edit_place": "Edit place", + "edit_tag": "Edit tag", + "edit_tag_help": "You can change the tag by replacing it with a new one or merging it with an existing one. The {n} associated events will also be changed.", "new_announcement": "New announcement", "show_smtp_setup": "Email settings", "smtp_hostname": "SMTP Hostname", diff --git a/locales/it.json b/locales/it.json index 1e05c169..a8daeca3 100644 --- a/locales/it.json +++ b/locales/it.json @@ -240,6 +240,8 @@ "footer_links": "Collegamenti del piè di pagina", "delete_footer_link_confirm": "Vuoi eliminare questo collegamento?", "edit_place": "Modifica luogo", + "edit_tag": "Modifica tag", + "edit_tag_help": "Puoi cambiare il tag sostituendolo con uno nuovo o unendolo ad uno gia' esistente. Verranno modificati anche i {n} eventi associati", "new_announcement": "Nuovo annuncio", "show_smtp_setup": "Impostazioni email", "widget": "Widget", diff --git a/pages/Admin.vue b/pages/Admin.vue index 4597fa86..6ddac5bd 100644 --- a/pages/Admin.vue +++ b/pages/Admin.vue @@ -26,6 +26,11 @@ v-container.container.pa-0.pa-md-3 v-tab-item(value='places') Places + //- TAGS + v-tab(href='#tags') {{$t('common.tags')}} + v-tab-item(value='tags') + Tags + //- GEOCODING / MAPS v-tab(href='#geolocation' v-if='settings.allow_geolocation') {{$t('admin.geolocation')}} v-tab-item(value='geolocation') @@ -77,6 +82,7 @@ export default { Users: () => import(/* webpackChunkName: "admin" */'../components/admin/Users'), Events: () => import(/* webpackChunkName: "admin" */'../components/admin/Events'), Places: () => import(/* webpackChunkName: "admin" */'../components/admin/Places'), + Tags: () => import(/* webpackChunkName: "admin" */'../components/admin/Tags'), Collections: () => import(/* webpackChunkName: "admin" */'../components/admin/Collections'), [process.client && 'Geolocation']: () => import(/* webpackChunkName: "admin" */'../components/admin/Geolocation.vue'), Federation: () => import(/* webpackChunkName: "admin" */'../components/admin/Federation.vue'), diff --git a/server/api/controller/tag.js b/server/api/controller/tag.js index 1f2f882b..d581179b 100644 --- a/server/api/controller/tag.js +++ b/server/api/controller/tag.js @@ -1,6 +1,8 @@ const Tag = require('../models/tag') const Event = require('../models/event') const uniq = require('lodash/uniq') +const log = require('../../log') + const { where, fn, col, Op } = require('sequelize') const exportController = require('./export') @@ -45,6 +47,20 @@ module.exports = { } }, + + + async getAll (_req, res) { + const tags = await Tag.findAll({ + order: [[fn('COUNT', col('tag.tag')), 'DESC']], + attributes: ['tag', [fn('COUNT', col('tag.tag')), 'count']], + include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }], + group: ['tag.tag'], + raw: true, + }) + return res.json(tags) + }, + + /** * search for tags by query string * sorted by usage @@ -60,9 +76,48 @@ module.exports = { include: [{ model: Event, where: { is_visible: true }, attributes: [], through: { attributes: [] }, required: true }], group: ['tag.tag'], limit: 10, - subQuery:false + subQuery: false }) return res.json(tags.map(t => t.tag)) + }, + + async updateTag (req, res) { + const tag = await Tag.findByPk(req.body.tag) + await tag.update(req.body) + res.json(place) + }, + + async updateTag (req, res) { + const oldtag = await Tag.findByPk(req.body.tag) + const newtag = await Tag.findByPk(req.body.newTag) + + // if the new tag does not exists, just rename the old one + if (!newtag) { + oldtag.tag = req.body.newTag + await oldtag.update({ tag: req.body.newTag }) + } else { + // in case it exists: + // - search for events with old tag + const events = await oldtag.getEvents() + // - substitute it with the new one + await oldtag.removeEvents(events) + await newtag.addEvents(events) + } + res.sendStatus(200) + }, + + async remove (req, res) { + log.info('Remove tag', req.params.tag) + const tagName = req.params.tag + try { + const tag = await Tag.findByPk(tagName) + await tag.destroy() + res.sendStatus(200) + } catch (e) { + log.error('Tag removal failed:', e) + res.sendStatus(404) + } } + } \ No newline at end of file diff --git a/server/api/index.js b/server/api/index.js index 812fac98..4fbed0a9 100644 --- a/server/api/index.js +++ b/server/api/index.js @@ -162,15 +162,21 @@ if (config.status !== 'READY') { api.get('/export/:type', cors, exportController.export) - api.get('/place/all', isAdmin, placeController.getAll) + // - PLACES + api.get('/places', isAdmin, placeController.getAll) api.get('/place/:placeName', cors, placeController.getEvents) api.get('/place', cors, placeController.search) api.get('/placeOSM/Nominatim/:place_details', cors, placeController._nominatim) api.get('/placeOSM/Photon/:place_details', cors, placeController._photon) api.put('/place', isAdmin, placeController.updatePlace) + // - TAGS + api.get('/tags', isAdmin, tagController.getAll) api.get('/tag', cors, tagController.search) api.get('/tag/:tag', cors, tagController.getEvents) + api.delete('/tag/:tag', isAdmin, tagController.remove) + api.put('/tag', isAdmin, tagController.updateTag) + // - FEDIVERSE INSTANCES, MODERATION, RESOURCES api.get('/instances', isAdmin, instanceController.getAll) diff --git a/tests/app.test.js b/tests/app.test.js index 9ed543ff..8411c6c2 100644 --- a/tests/app.test.js +++ b/tests/app.test.js @@ -285,10 +285,10 @@ describe('Place', () => { }) test('admin should get all places', async () => { - await request(app).get('/api/place/all') + await request(app).get('/api/places') .expect(403) - const response = await request(app).get('/api/place/all') + const response = await request(app).get('/api/places') .auth(token.access_token, { type: 'bearer' }) .expect(200)