You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@allura.apache.org by br...@apache.org on 2026/09/17 17:56:10 UTC
(allura) 03/15: [#8624] remove Feed entries when content is deleted or restricted
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch db/8624b
in repository https://gitbox.apache.org/repos/asf/allura.git
commit 1f9605af1e058796decd7daacdcea9db41d312fa
Author: Dave Brondsema <db...@slashdotmedia.com>
AuthorDate: Tue Sep 8 16:50:43 2026 +0000
[#8624] remove Feed entries when content is deleted or restricted
Feed entries keep a copy of the artifact's title and body and are served
without re-checking the artifact, but nothing removed them once created.
Artifact.delete() now clears them, and a revoke_feed_entries() hook is
called from the tracker/wiki/comment soft-delete and spam paths and when a
ticket is made private. ForgeBlog already did this in BlogPost.delete().
Co-Authored-By: Claude Opus 5 <no...@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CbUqCrWrninjwW3vSrxxbC
---
Allura/allura/model/artifact.py | 9 +++++
Allura/allura/model/discuss.py | 15 +++++++-
Allura/allura/tests/functional/test_feeds.py | 56 ++++++++++++++++++++++++++++
ForgeTracker/forgetracker/model/ticket.py | 2 +
ForgeWiki/forgewiki/model/wiki.py | 1 +
5 files changed, 82 insertions(+), 1 deletion(-)
diff --git a/Allura/allura/model/artifact.py b/Allura/allura/model/artifact.py
index 1ba02de73..6ee7dace9 100644
--- a/Allura/allura/model/artifact.py
+++ b/Allura/allura/model/artifact.py
@@ -126,6 +126,10 @@ def __json__(self, posts_limit=None, is_export=False, user=None):
self.discussion_thread.url()),
)
+ def revoke_feed_entries(self):
+ '''Drop feed entries, for soft-deletion or when access is narrowed.'''
+ Feed.remove_for(self)
+
def parent_security_context(self):
"""Return the :class:`allura.model.project.AppConfig` instance for
this Artifact.
@@ -500,6 +504,7 @@ def delete(self):
idx_id = self.index_id()
ArtifactReference.query.remove(dict(_id=idx_id))
Shortlink.query.remove(dict(ref_id=idx_id))
+ self.revoke_feed_entries() # Post overrides this; its entries aren't filed under its own ref_id
super().delete()
session(self).flush(self)
session(self).imap.expunge(self)
@@ -974,6 +979,10 @@ def clear_user_data(self):
def from_username(cls, username):
return cls.query.find({'author_link': f"/u/{username}/"}).all()
+ @classmethod
+ def remove_for(cls, artifact):
+ cls.query.remove(dict(ref_id=artifact.index_id()))
+
@classmethod
def has_access(cls, artifact):
# Enable only for development.
diff --git a/Allura/allura/model/discuss.py b/Allura/allura/model/discuss.py
index 579a32aa6..f8f341fc8 100644
--- a/Allura/allura/model/discuss.py
+++ b/Allura/allura/model/discuss.py
@@ -291,7 +291,7 @@ def primary(self):
return self.ref.artifact
def post_to_feed(self, post):
- if post.status == 'ok':
+ if post.status == 'ok' and not post.deleted:
Feed.post(
self.primary(),
title=post.subject,
@@ -742,6 +742,16 @@ def traverse(posts):
return f'{url}?limit={limit}#{slug}'
return f'{url}?limit={limit}&page={page}#{slug}'
+ def revoke_feed_entries(self):
+ '''post_to_feed() files these under the thread's artifact, not the post.'''
+ primary = self.thread.primary() if self.thread else None
+ if primary is None: # pragma no cover
+ return
+ suffix = '#' + h.urlquote(self.slug) # the page number in the link can shift
+ for item in Feed.query.find(dict(ref_id=primary.index_id())):
+ if item.link and item.link.endswith(suffix):
+ item.delete()
+
def shorthand_id(self):
if self.thread:
return f'{self.thread.shorthand_id()}#{self.slug}'
@@ -764,6 +774,7 @@ def delete(self):
def soft_delete(self):
self.deleted = True
+ self.revoke_feed_entries()
session(self).flush(self)
self.thread.update_stats()
@@ -836,6 +847,7 @@ def notify(self, file_info=None, notification_text=None):
def spam(self, submit_spam_feedback=True):
self.status = 'spam'
+ self.revoke_feed_entries()
if submit_spam_feedback:
g.spam_checker.submit_spam(self.text, artifact=self, user=self.author())
session(self).flush(self)
@@ -844,6 +856,7 @@ def spam(self, submit_spam_feedback=True):
def undo(self, prev_status):
if prev_status in ('ok', 'pending'):
self.status = prev_status
+ self.thread.post_to_feed(self) # spam() revoked it
session(self).flush(self)
self.thread.update_stats()
diff --git a/Allura/allura/tests/functional/test_feeds.py b/Allura/allura/tests/functional/test_feeds.py
index 9949b5976..a23c06e07 100644
--- a/Allura/allura/tests/functional/test_feeds.py
+++ b/Allura/allura/tests/functional/test_feeds.py
@@ -16,10 +16,18 @@
# under the License.
from formencode.variabledecode import variable_encode
+from ming.odm import ThreadLocalODMSession
+from tg import tmpl_context as c
+
+from allura import model as M
from allura.tests import TestController
from allura.tests import decorators as td
from allura.lib import helpers as h
+from forgetracker import model as TM
+from forgewiki import model as WM
+
+ANON = dict(username='*anonymous')
class TestFeeds(TestController):
@@ -80,6 +88,54 @@ def test_ticket_list_feed(self):
self.app.get('/bugs/feed.rss')
self.app.get('/bugs/feed.atom')
+ @td.with_tracker
+ @td.with_tracker
+ def test_ticket_feed_drops_privatised_ticket(self):
+ assert 'This is a ticket' in self.app.get('/bugs/feed.rss', extra_environ=ANON)
+ TM.Ticket.query.get(ticket_num=1).private = True
+ ThreadLocalODMSession.flush_all()
+ r = self.app.get('/bugs/feed.rss', extra_environ=ANON)
+ assert 'This is a ticket' not in r
+ assert 'This is a description' not in r
+
+ @td.with_wiki
+ def test_wiki_feed_drops_deleted_page(self):
+ self.app.post('/wiki/Doomed/update',
+ params={'title': 'Doomed', 'text': 'sekrit', 'labels': ''})
+ assert 'sekrit' in self.app.get('/wiki/feed.rss', extra_environ=ANON)
+ self.app.post('/wiki/Doomed/delete')
+ assert 'sekrit' not in self.app.get('/wiki/feed.rss', extra_environ=ANON)
+
+ @td.with_wiki
+ def test_wiki_feed_drops_spammed_comment(self):
+ h.set_context('test', 'wiki', neighborhood='Projects')
+ page = WM.Page.query.get(title='Home', app_config_id=c.app.config._id)
+ page.discussion_thread.add_post(text='spammy comment')
+ ThreadLocalODMSession.flush_all()
+ assert 'spammy comment' in self.app.get('/wiki/feed.rss', extra_environ=ANON)
+
+ M.Post.query.get(text='spammy comment').spam(submit_spam_feedback=False)
+ ThreadLocalODMSession.flush_all()
+ assert 'spammy comment' not in self.app.get('/wiki/feed.rss', extra_environ=ANON)
+
+ h.set_context('test', 'wiki', neighborhood='Projects')
+ M.Post.query.get(text='spammy comment').undo('ok')
+ ThreadLocalODMSession.flush_all()
+ assert 'spammy comment' in self.app.get('/wiki/feed.rss', extra_environ=ANON)
+
+ @td.with_wiki
+ def test_wiki_feed_drops_hard_deleted_comment(self):
+ h.set_context('test', 'wiki', neighborhood='Projects')
+ page = WM.Page.query.get(title='Home', app_config_id=c.app.config._id)
+ page.discussion_thread.add_post(text='doomed comment')
+ ThreadLocalODMSession.flush_all()
+ assert 'doomed comment' in self.app.get('/wiki/feed.rss', extra_environ=ANON)
+
+ h.set_context('test', 'wiki', neighborhood='Projects')
+ M.Post.query.get(text='doomed comment').delete()
+ ThreadLocalODMSession.flush_all()
+ assert 'doomed comment' not in self.app.get('/wiki/feed.rss', extra_environ=ANON)
+
@td.with_tracker
def test_ticket_feed(self):
self.app.get('/bugs/1/feed.rss')
diff --git a/ForgeTracker/forgetracker/model/ticket.py b/ForgeTracker/forgetracker/model/ticket.py
index 0434ba2b0..73f3caac1 100644
--- a/ForgeTracker/forgetracker/model/ticket.py
+++ b/ForgeTracker/forgetracker/model/ticket.py
@@ -895,6 +895,7 @@ def _allow_all(role, perms):
acl += _allow_all(role_creator, security.all_allowed(self, role_creator))
acl += [DENY_ALL]
self.acl = acl
+ self.revoke_feed_entries()
else:
self.acl = []
private = property(_get_private, _set_private)
@@ -1399,6 +1400,7 @@ def delete(self):
def soft_delete(self):
require_access(self, 'delete')
Shortlink.query.remove(dict(ref_id=self.index_id()))
+ self.revoke_feed_entries()
self.deleted = True
suffix = " {dt.hour}:{dt.minute}:{dt.second} {dt.day}-{dt.month}-{dt.year}".format(
dt=datetime.utcnow())
diff --git a/ForgeWiki/forgewiki/model/wiki.py b/ForgeWiki/forgewiki/model/wiki.py
index 91da0ca5e..7da34efc4 100644
--- a/ForgeWiki/forgewiki/model/wiki.py
+++ b/ForgeWiki/forgewiki/model/wiki.py
@@ -281,6 +281,7 @@ def soft_delete(self):
Notification.post(
artifact=self, topic='metadata', text=description, subject=subject)
Shortlink.query.remove(dict(ref_id=self.index_id()))
+ self.revoke_feed_entries()
self.deleted = True
suffix = f" {datetime.utcnow():%Y-%m-%d %H:%M:%S.%f}"
self.title += suffix