use id in places url (ics and rss feeds, place page) with a reasonable backward compatible fallback

This commit is contained in:
lesion 2025-01-10 18:37:22 +01:00
parent 0899e71684
commit 8f9bc7e9de
No known key found for this signature in database
GPG key ID: 352918250B012177
12 changed files with 107 additions and 17 deletions

View file

@ -20,7 +20,7 @@
<div class='p-location' itemprop="location" itemscope itemtype="https://schema.org/Place">
<nuxt-link class='place d-block pl-0' text
:to='`/place/${encodeURIComponent(event.place.name)}`'>
:to='`/place/${event.place.id}/${encodeURIComponent(event.place.name)}`'>
<v-icon v-text='mdiMapMarker'></v-icon>
<span itemprop='name'>{{ event.place.name }}</span>
</nuxt-link>

View file

@ -7,7 +7,7 @@ v-card
v-row.my-4.d-flex.flex-column.align-center.text-center
.text-h6
v-icon(v-text='mdiMapMarker' )
nuxt-link.ml-2.text-decoration-none(v-text="place.name" :to='`/place/${place.name}`')
nuxt-link.ml-2.text-decoration-none(v-text="place.name" :to='`/place/${place.id}/${encodeURIComponent(place.name)}`')
.mx-2(v-text="`${place.address}`")
v-card-actions.py-4
HowToArriveNav.pl-1(:place='place')

View file

@ -84,7 +84,7 @@ v-container
template(v-slot:item.actions='{ item }')
v-btn(@click='editPlace(item)' color='primary' icon)
v-icon(v-text='mdiPencil')
nuxt-link(:to='`/place/${item.name}`')
nuxt-link(:to='`/place/${item.id}/${encodeURIComponent(item.name)}`')
v-icon(v-text='mdiEye')
</template>

View file

@ -24,7 +24,7 @@
.p-location.h-adr(itemprop="location" itemscope itemtype="https://schema.org/Place")
v-icon(v-text='mdiMapMarker' small)
nuxt-link.vcard.ml-2.p-name.text-decoration-none.text-uppercase(:to='`/place/${encodeURIComponent(event?.place?.name)}`')
nuxt-link.vcard.ml-2.p-name.text-decoration-none.text-uppercase(:to='`/place/${event?.place?.id}/${encodeURIComponent(event?.place?.name)}`')
span(itemprop='name') {{event?.place?.name}}
.font-weight-light.p-street-address(v-if='event?.place?.name !=="online"' itemprop='address') {{event?.place?.address}}

View file

@ -0,0 +1,65 @@
<template>
<v-container id='home' class='px-2 px-sm-6 pt-0'>
<h1 class='d-block text-h4 font-weight-black text-center text-uppercase mt-10 mx-auto w-100 text-underline'>
<u>{{ place.name }}</u>
</h1>
<span v-if='place.name!=="online"' class="d-block text-subtitle text-center w-100">{{ place.address }}</span>
<!-- Map -->
<div v-if='settings.allow_geolocation && place.latitude && place.longitude' >
<div class="mt-4 mx-auto px-4" >
<Map :place='place' :height='mapHeight' />
</div>
<div class="mt-4">
<HowToArriveNav :place='place' class="justify-center" />
</div>
</div>
<!-- Events -->
<div id="events" class='mt-14'>
<v-lazy class='event v-card' :value='idx<9' v-for='(event, idx) in events' :key='event.id' :min-height='hide_thumbs ? 105 : undefined' :options="{ threshold: .5, rootMargin: '500px' }" :class="{ 'theme--dark': is_dark }">
<Event :event='event' :lazy='idx > 9' />
</v-lazy>
</div>
</v-container>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import Event from '@/components/Event'
import HowToArriveNav from '@/components/HowToArriveNav.vue'
export default {
name: 'Place',
components: {
Event,
HowToArriveNav,
[process.client && 'Map']: () => import('@/components/Map.vue')
},
data() {
return { mapHeight: "14rem" }
},
head() {
const title = `${this.settings.title} - ${this.place.name}`
return {
title,
link: [
{ rel: 'alternate', type: 'application/rss+xml', title, href: this.settings.baseurl + `/feed/rss/place/${this.place.id}` },
{ rel: 'alternate', type: 'text/calendar', title, href: this.settings.baseurl + `/feed/ics/place/${this.place.id}` }
],
}
},
computed: {
...mapState(['settings']),
...mapGetters(['hide_thumbs', 'is_dark']),
},
async asyncData({ $axios, params, error }) {
try {
const { events, place } = await $axios.$get(`/place/${params.id}`)
return { place, events }
} catch (e) {
error({ statusCode: 404, message: 'Place not found!' })
}
}
}
</script>

View file

@ -44,8 +44,8 @@ export default {
return {
title,
link: [
{ rel: 'alternate', type: 'application/rss+xml', title, href: this.settings.baseurl + `/feed/rss/place/${this.place.name}` },
{ rel: 'alternate', type: 'text/calendar', title, href: this.settings.baseurl + `/feed/ics/place/${this.place.name}` }
{ rel: 'alternate', type: 'application/rss+xml', title, href: this.settings.baseurl + `/feed/rss/place/${this.place.id}` },
{ rel: 'alternate', type: 'text/calendar', title, href: this.settings.baseurl + `/feed/ics/place/${this.place.id}` }
],
}
},

View file

@ -9,22 +9,22 @@ const { Op, where, col, fn, cast } = require('sequelize')
module.exports = {
async getEvents (req, res) {
const placeName = req.params.placeName
const place = await Place.findOne({ where: { name: placeName }})
const placeNameOrId = req.params.placeNameOrId
const place = await Place.findOne({ where: { [Op.or]: { id: placeNameOrId, name: placeNameOrId }}})
if (!place) {
log.warn(`Place ${placeName} not found`)
log.warn(`Place ${placeNameOrId} not found`)
return res.sendStatus(404)
}
const format = req.params.format || 'json'
log.debug(`Events for place: ${placeName}`)
log.debug(`Events for place: ${place.name}`)
const events = await eventController._select({ places: String(place.id), show_recurrent: true, show_multidate: true })
switch (format) {
case 'rss':
return exportController.feed(req, res, events,
`${res.locals.settings.title} - Place @${place.name}`,
`${res.locals.settings.baseurl}/feed/rss/place/${place.name}`)
`${res.locals.settings.baseurl}/feed/rss/place/${place.id}`)
case 'ics':
return exportController.ics(req, res, events)
default:

View file

@ -190,7 +190,7 @@ module.exports = () => {
// - PLACES
api.get('/places', isAdmin, placeController.getAll)
api.get('/place/:placeName', cors, placeController.getEvents)
api.get('/place/:placeNameOrId', cors, placeController.getEvents)
api.get('/place', cors, placeController.search)
api.put('/place', isAdmin, placeController.updatePlace)

View file

@ -4,7 +4,6 @@ module.exports = (sequelize, DataTypes) => {
const Place = sequelize.define('place', {
name: {
type: DataTypes.STRING,
unique: true,
index: true,
allowNull: false
},
@ -18,9 +17,10 @@ module.exports = (sequelize, DataTypes) => {
})
/**
* @description WIP -> https://codeberg.org/fediverse/fep/src/commit/4a75a1bc50bc6d19fc1e6112f02c52621bc178fe/fep/8a8e/fep-8a8e.md#location
* @todo support PostalAddress type
* @returns ActivityStream location representation
* @link https://www.w3.org/TR/activitystreams-vocabulary/#places
* @todo support PostalAddress type
* @link WIP -> https://codeberg.org/fediverse/fep/src/commit/4a75a1bc50bc6d19fc1e6112f02c52621bc178fe/fep/8a8e/fep-8a8e.md#location
*/
Place.prototype.toAP = function () {
return {

View file

@ -288,7 +288,9 @@ const Helpers = {
return false
})
await event.setPlace(place)
if (place) {
await event.setPlace(place)
}
// create/assign tags
let tags = []

View file

@ -0,0 +1,23 @@
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
// needed as in sequelize there is no support for alter table and sequelize simulate this by creating a backup table and dropping the old one:
// this will cause a foreign key error
const dialect = queryInterface.sequelize.getDialect()
if (dialect === 'sqlite') {
await queryInterface.sequelize.query('PRAGMA foreign_keys = OFF')
}
await queryInterface.changeColumn('places', 'name', { type: Sequelize.STRING, index: true, allowNull: false, unique: false })
},
async down (queryInterface, Sequelize) {
const dialect = queryInterface.sequelize.getDialect()
if (dialect === 'sqlite') {
await queryInterface.sequelize.query('PRAGMA foreign_keys = OFF')
}
await queryInterface.changeColumn('places', 'name', { type: Sequelize.STRING, index: true, allowNull: false, unique: true })
}
};

View file

@ -42,7 +42,7 @@ async function main () {
// rss / ics feed
app.use(helpers.feedRedirect)
app.get('/feed/:format/tag/:tag', cors(), tagController.getEvents)
app.get('/feed/:format/place/:placeName', cors(), placeController.getEvents)
app.get('/feed/:format/place/:placeNameOrId', cors(), placeController.getEvents)
app.get('/feed/:format/collection/:name', cors(), collectionController.getEvents)
app.get('/feed/:format', cors(), exportController.export)