Allow creators to delete events and restrict private invitations to friends

This commit is contained in:
2026-09-15 20:13:29 +02:00
parent bb28e17220
commit d1bb184b17
7 changed files with 163 additions and 18 deletions
+106
View File
@@ -163,6 +163,8 @@ class NotificationDatabaseTests(unittest.TestCase):
self.assertEqual(result.status_code, 303)
def invitation(self):
with main.get_db_connection() as db:
db.execute("INSERT INTO friendships(requester_id,addressee_id,status) VALUES (1,2,'accepted') ON CONFLICT DO NOTHING")
result = self.clients[0].post('/concerts', data={'artist':'Private test event', 'start_datetime':'2027-04-01T20:00',
'event_type':'other','visibility':'private','invited_user_ids':'2'}, follow_redirects=False)
self.assertEqual(result.status_code, 303)
@@ -232,6 +234,110 @@ class NotificationDatabaseTests(unittest.TestCase):
self.clients[0].post(f'/concerts/{concert}/edit', data=form, follow_redirects=False)
self.assertEqual(self.scalar("SELECT count(*) FROM push_notifications WHERE state='pending'"), 1)
def event(self, owner=1, past=False, kind='other', visibility='public'):
start = datetime.now() + timedelta(days=-10 if past else 10)
with main.get_db_connection() as db:
return db.execute("""INSERT INTO concerts
(artist,start_datetime,end_datetime,event_type,visibility,created_by)
VALUES (%s,%s,%s,%s,%s,%s) RETURNING id""",
('Event access test', start, start + timedelta(days=1) if kind == 'festival' else None,
kind, visibility, owner)).fetchone()[0]
def test_creator_can_delete_own_events_including_past_in_both_languages(self):
for language in ('de', 'en'):
self.clients[0].get('/language/' + language, follow_redirects=False)
for past in (False, True):
for kind in ('concert', 'festival', 'other'):
with self.subTest(language=language, past=past, kind=kind):
event = self.event(past=past, kind=kind)
page = self.clients[0].get(f'/concerts/{event}')
self.assertEqual(page.status_code, 200)
self.assertIn(f'action="/concerts/{event}/delete"', page.text)
self.assertIn('Delete event' if language == 'en' else 'Veranstaltung löschen', page.text)
if past:
self.assertNotIn(f'href="/concerts/{event}/edit"', page.text)
result = self.clients[0].post(f'/concerts/{event}/delete', follow_redirects=False)
self.assertEqual(result.status_code, 303)
self.assertEqual(self.scalar('SELECT count(*) FROM concerts WHERE id=%s', (event,)), 0)
def test_delete_rejects_foreign_and_anonymous_but_preserves_admin_access(self):
for past in (False, True):
event = self.event(owner=2, past=past)
page = self.clients[0].get(f'/concerts/{event}')
self.assertNotIn(f'action="/concerts/{event}/delete"', page.text)
self.assertEqual(self.clients[0].post(f'/concerts/{event}/delete', follow_redirects=False).status_code, 403)
self.assertEqual(self.scalar('SELECT count(*) FROM concerts WHERE id=%s', (event,)), 1)
with TestClient(main.app) as anonymous:
self.assertEqual(anonymous.post(f'/concerts/{event}/delete', follow_redirects=False).status_code, 303)
self.assertEqual(self.clients[1].post(f'/concerts/{event}/delete',
headers={'Origin':'https://evil.invalid'}, follow_redirects=False).status_code, 403)
with main.get_db_connection() as db:
db.execute('UPDATE users SET is_admin=TRUE WHERE id=3')
self.assertIn(f'action="/concerts/{event}/delete"', self.clients[2].get(f'/concerts/{event}').text)
self.assertEqual(self.clients[2].post(f'/concerts/{event}/delete', follow_redirects=False).status_code, 303)
# Deleting a parent does not delete another user's linked event.
parent = self.event(kind='concert')
child = self.event(owner=2)
with main.get_db_connection() as db:
db.execute('UPDATE concerts SET parent_event_id=%s WHERE id=%s', (parent, child))
self.assertEqual(self.clients[0].post(f'/concerts/{parent}/delete', follow_redirects=False).status_code, 303)
self.assertIsNone(self.scalar('SELECT parent_event_id FROM concerts WHERE id=%s', (child,)))
def test_private_picker_shows_only_confirmed_unblocked_owner_friends(self):
event = self.event(visibility='private')
with main.get_db_connection() as db:
db.execute("INSERT INTO friendships(requester_id,addressee_id,status) VALUES (2,1,'accepted'),(1,3,'pending')")
for language, heading in (('de','Freunde einladen'), ('en','Invite friends')):
self.clients[0].get('/language/' + language, follow_redirects=False)
for route in ('/concerts/new', f'/concerts/{event}/edit'):
page = self.clients[0].get(route)
self.assertEqual(page.status_code, 200)
field = re.search(r'<fieldset id="event-invitations-field".*?</fieldset>', page.text, re.S).group()
self.assertIn(heading, field)
self.assertEqual(re.findall(r'name="invited_user_ids" value="(\d+)"', field), ['2'])
with main.get_db_connection() as db:
db.execute('UPDATE users SET is_admin=TRUE WHERE id=3')
admin_page = self.clients[2].get(f'/concerts/{event}/edit')
self.assertEqual(re.findall(r'name="invited_user_ids" value="(\d+)"', admin_page.text), ['2'])
# Verify the other direction of the friendship and a block overriding it.
self.assertEqual([u['id'] for u in main.get_invitable_users(2)], [1])
with main.get_db_connection() as db:
db.execute('INSERT INTO user_blocks(blocker_id,blocked_id) VALUES (2,1)')
self.assertEqual(main.get_invitable_users(1), [])
for language, empty in (('de','Noch keine bestätigten Freunde vorhanden.'), ('en','No confirmed friends yet.')):
self.clients[0].get('/language/' + language, follow_redirects=False)
self.assertIn(empty, self.clients[0].get('/concerts/new').text)
def test_private_invites_reject_forged_ids_and_preserve_existing_hidden_invites(self):
form = {'artist':'Invitation access test', 'start_datetime':'2027-04-01T20:00',
'event_type':'other', 'visibility':'private'}
for language, error in (('de','Es können nur bestätigte Freunde eingeladen werden.'),
('en','Only confirmed friends can be invited.')):
self.clients[0].get('/language/' + language, follow_redirects=False)
for selected in ('1','2','99999'):
result = self.clients[0].post('/concerts', data=dict(form, invited_user_ids=selected), follow_redirects=False)
self.assertEqual(result.status_code, 400)
self.assertIn(error, result.text)
self.assertEqual(self.scalar('SELECT count(*) FROM concerts'), 0)
event = self.invitation()
result = self.clients[0].post(f'/concerts/{event}/edit',
data=dict(form, invited_user_ids=['2','3']), follow_redirects=False)
self.assertEqual(result.status_code, 400)
self.assertEqual(main.get_event_invitee_ids(event), {2})
self.assertEqual(self.scalar('SELECT artist FROM concerts WHERE id=%s', (event,)), 'Private test event')
with main.get_db_connection() as db:
db.execute('DELETE FROM friendships')
# The now hidden legacy invitation survives an unrelated edit, without a new push.
result = self.clients[0].post(f'/concerts/{event}/edit', data=form, follow_redirects=False)
self.assertEqual(result.status_code, 303)
self.assertEqual(main.get_event_invitee_ids(event), {2})
self.assertEqual(self.scalar("SELECT count(*) FROM push_notifications WHERE kind='event_invitation'"), 1)
# An invited non-owner cannot use the edit route to grant access to someone else.
result = self.clients[1].post(f'/concerts/{event}/edit',
data=dict(form, invited_user_ids='3'), follow_redirects=False)
self.assertEqual(result.status_code, 303)
self.assertEqual(main.get_event_invitee_ids(event), {2})
def test_linkable_events_are_chronological_in_create_and_edit_forms(self):
with main.get_db_connection() as db:
rows = db.execute("""