-
Notifications
You must be signed in to change notification settings - Fork 95
Description
Note
This issue avoids the word 'whitelist' to provide contrast to the 'whitelist' term in sound moderation. 'keep on watchlist' can be understood as 'do not add to [forum] whitelist'.
Trigger for this report
I just deleted a first post on https://freesound.org/forum/moderate/ . I was only 98% sure that it was a relevant-sounding post, created solely for adding spam links later by editing.
Problems
A volunteer moderator who multiple times a month takes away a user's ability to express themselves, communicate and potentially enrich the community is faced with guilt that builds up over time.
Each individual time a moderator moderates a first post that has OK content but feels like it could be preparation for spam, the moderation consumes more brain power and time than it should.
Solution
Please add the option "Approve, keep on watchlist" to "Moderate forum posts" at https://freesound.org/forum/moderate/
Function
Using "Approve, keep on watchlist" does the following:
- approve the individual forum post
- any new post by that use will not be published and will enter the same moderation process
- [important] if the user edits this or any other of their posts, that edited post will be un-published and will enter the same moderation process
Implementation
As an experiment, an untested generated implementation recommendation follows. This might not be useful. At the very lest it demonstrated to me that adding these features is far from trivial.
Here’s a properly-designed change that matches the issue’s behavior, fits Freesound’s existing forum moderation model, and stays safe for rolling deploy + Postgres + no API break.
What exists today (relevant code paths)
Forum post moderation model
-
forum/models.pyPost.moderation_stateis"OK"or"NM"(needs moderation).- Thread/forum pages generally only show
"OK"posts.
-
forum/views.py-
Creating posts:
-
new_thread(forum/views.py:318+) andreply(forum/views.py:220+) send some first posts to moderation only when:- user has no OK posts yet and
text_may_be_spam(...)is true. - they create with
moderation_state="NM"and do not index.
- user has no OK posts yet and
-
-
Editing posts:
post_edit(forum/views.py:465+) always saves the edit and callsadd_posts_to_search_engine([post])even if moderation should apply (currently no moderation-on-edit logic).
-
Moderation UI:
moderate_posts(forum/views.py:513+) listsPost.objects.filter(moderation_state="NM")and allows actions viaPostModerationForm.
-
-
forum/forms.pyMODERATION_CHOICES = ["Approve", "Delete User", "Delete Post"]
-
Search index helpers:
-
utils/search/search_forum.pyhas:add_posts_to_search_engine(post_objects, ...)delete_posts_from_search_engine(post_ids, ...)
-
Feature to add: “Approve, keep on watchlist”
Required behavior
When a moderator chooses “Approve, keep on watchlist” for a pending post:
-
Approve that post (
moderation_state="OK") -
Mark the user such that:
- Any new post by them is created as
moderation_state="NM"(not published) - Any edit they make to their own posts causes that edited post to become
moderation_state="NM"(unpublished) and re-enter moderation
- Any new post by them is created as
This is a user-level flag, independent of sound “whitelist”.
Design choice: store watchlist state on Profile
Why
- Freesound already stores user trust and moderation-related knobs on
accounts.models.Profile(e.g.is_whitelisted,num_posts,can_post_in_forum()). - It avoids a new table and joins, and is easy to query (
db_index=True).
Schema change
Add field to accounts/models.py Profile:
- File:
accounts/models.py(insideclass Profile, nearis_whitelisted)
forum_posts_need_moderation = models.BooleanField(default=False, db_index=True)-
Add migration:
- File:
accounts/migrations/0043_profile_forum_posts_need_moderation.py(new) - Operation:
AddField(Profile, "forum_posts_need_moderation", BooleanField(default=False, db_index=True))
- File:
Admin visibility (recommended)
-
File:
accounts/admin.py- Update
ProfileAdmin.list_displayandlist_filterto includeforum_posts_need_moderation - This gives staff a way to remove someone from watchlist later (even if UI doesn’t expose it yet).
- Update
Behavior changes in web views
1) Moderation UI action
Add a new choice in the moderation form:
-
File:
forum/forms.py- Update
MODERATION_CHOICES:
- Update
MODERATION_CHOICES = [(x, x) for x in ["Approve", "Approve, keep on watchlist", "Delete User", "Delete Post"]]Then implement handling:
-
File:
forum/views.pyinmoderate_posts(forum/views.py:513+)- Add a new branch:
Key points to implement:
-
For
"Approve"and"Approve, keep on watchlist":-
set
post.moderation_state = "OK" -
post.save() -
index the post via
add_posts_to_search_engine([post])- (Right now moderation approval does not index; this is important so approved posts become searchable, matching the existing “don’t index NM posts” comments.)
-
invalidate caches as already done
-
-
For
"Approve, keep on watchlist"additionally:post.author.profile.forum_posts_need_moderation = Truepost.author.profile.save(update_fields=[...])- optional: add a moderator UI message confirming the user is now watched
2) New thread / reply posting
When a watched user posts anything, it should go to moderation regardless of spam heuristics.
-
File:
forum/views.py-
In
reply(forum/views.py:239+) andnew_thread(forum/views.py:325+), before the current spam logic:-
If
request.user.profile.forum_posts_need_moderation:- create post with
moderation_state="NM" - do not call
add_posts_to_search_engine - set
set_to_moderation = True - show the existing moderation info message (
"Your post won't be shown until...") - call
invalidate_all_moderators_header_cache()(recommended, so moderators see pending counts promptly)
- create post with
-
-
This reuses the existing moderation pipeline:
- pending list remains
Post.objects.filter(moderation_state="NM")(forum/views.py:555) - moderation approval flows unchanged.
3) Editing posts (critical requirement)
If a watched user edits their post, that post must become unpublished and require re-approval.
-
File:
forum/views.pyinpost_edit(forum/views.py:465+)-
Only apply to self-edits:
if request.user == post.author and request.user.profile.forum_posts_need_moderation:
-
On valid form submit:
-
update
post.body -
set
post.moderation_state = "NM" -
save
-
remove from search index:
- call
delete_posts_from_search_engine([post.id])fromutils/search/search_forum.py
- call
-
invalidate moderator header cache
-
show message:
"Your edited post won't be shown until it is manually approved by moderators" -
redirect to
post.thread.get_absolute_url()(NOTpost.get_absolute_url())- because
forum/views.py:207fetches posts withmoderation_state="OK"and the post page would 404 after unpublishing.
- because
-
-
This matches the issue’s “[important] edited post will be un-published”.
Rolling deploy plan (important)
Because this adds a DB column, do not deploy code that writes/reads the new field before the migration is applied.
Recommended deployment order:
- Deploy code that is tolerant but does not require the field (optional)
- Run migration:
accounts/migrations/0043... - Deploy updated app servers using the new field logic
In practice, the simplest safe approach is: apply migration first, then roll app servers.
(If you need extra safety, you can guard reads with getattr(request.user.profile, "forum_posts_need_moderation", False) in the posting/editing views, but note: once the model class includes the field, Django will try to include the column on save(), so the migration still must be applied before any server that might save Profile.)
API impact: “must not break API”
This change:
- adds a boolean field on
Profile - changes forum web behavior
It does not alter:
apiv2/routes, serializers, auth, or response shapes- existing forum URLs / view names
So API remains unaffected unless there are hidden API endpoints for forum posting (this repo’s apiv2/urls.py doesn’t route forum posting; forum is purely web).
Edge cases & risks (and how this design handles them)
- Edited first post of a thread
- If the first post is set to
"NM", the thread may become hidden from forum listings (many queries requirefirst_post__moderation_state="OK"inforum/views.py, e.g.thread = get_object_or_404(... first_post__moderation_state="OK")atforum/views.py:221). - This is consistent with “unpublish edited post”, but it’s a real UX impact. This design follows the requirement exactly.
- Search index correctness
- New posts created as
"NM"are not indexed (existing behavior). - Edited posts that become
"NM"are removed from the index (new). - Moderated approval should add to index (I recommend adding this now in
moderate_posts, because otherwise watchlist edits would never become searchable again after re-approval).
- User posting rate-limits
Profile.can_post_in_forum()blocks posting if the user has any"NM"posts (accounts/models.py:402-408).- With watchlist, the user will generally have a pending post after they post/edit and therefore be blocked from posting more until approved.
- The issue didn’t require allowing multiple pending posts, so this is acceptable and consistent with current policy.
Files you will change (summary)
Schema / admin
accounts/models.py— addProfile.forum_posts_need_moderationaccounts/migrations/0043_profile_forum_posts_need_moderation.py— new migrationaccounts/admin.py— expose field inProfileAdmin(recommended)
Forum behavior
-
forum/forms.py— add moderation action choice -
forum/views.pymoderate_posts— implement new action; approve+index; set user flagreplyandnew_thread— force moderation for watched userspost_edit— unpublish on watched user self-edit + deindex + redirect thread
Search utilities used
utils/search/search_forum.py— usedelete_posts_from_search_engineandadd_posts_to_search_engine
Tests to add (so we don’t regress)
Add to forum/tests.py (there’s already a moderation test class around forums-moderate at the end of the file):
- Approve keep on watchlist sets profile flag
-
Create
Postwith"NM", moderate using new action, assert:- post is
"OK" post.author.profile.forum_posts_need_moderation is True
- post is
- Watched user new reply goes to moderation
- Flag user, POST to
reverse("forums-reply", ...), assert created post is"NM"
- Watched user edit unpublishes
-
Create an
"OK"post, flag user, edit it viareverse("forums-post-edit", ...), assert:- post is
"NM" - response redirects to thread URL (not post URL)
- post is
(You can mock out Solr indexing calls if tests currently do, or configure search engine in test settings as elsewhere in the suite.)
