Skip to content

"Approve, keep on watchlist" for "Moderate forum posts" #2066

@qubodup

Description

@qubodup

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/

Image

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.py

    • Post.moderation_state is "OK" or "NM" (needs moderation).
    • Thread/forum pages generally only show "OK" posts.
  • forum/views.py

    • Creating posts:

      • new_thread (forum/views.py:318+) and reply (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.
    • Editing posts:

      • post_edit (forum/views.py:465+) always saves the edit and calls add_posts_to_search_engine([post]) even if moderation should apply (currently no moderation-on-edit logic).
    • Moderation UI:

      • moderate_posts (forum/views.py:513+) lists Post.objects.filter(moderation_state="NM") and allows actions via PostModerationForm.
  • forum/forms.py

    • MODERATION_CHOICES = ["Approve", "Delete User", "Delete Post"]
  • Search index helpers:

    • utils/search/search_forum.py has:

      • 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:

  1. Approve that post (moderation_state="OK")

  2. 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

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 (inside class Profile, near is_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))

Admin visibility (recommended)

  • File: accounts/admin.py

    • Update ProfileAdmin.list_display and list_filter to include forum_posts_need_moderation
    • This gives staff a way to remove someone from watchlist later (even if UI doesn’t expose it yet).

Behavior changes in web views

1) Moderation UI action

Add a new choice in the moderation form:

  • File: forum/forms.py

    • Update MODERATION_CHOICES:
MODERATION_CHOICES = [(x, x) for x in ["Approve", "Approve, keep on watchlist", "Delete User", "Delete Post"]]

Then implement handling:

  • File: forum/views.py in moderate_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 = True
    • post.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+) and new_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)

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.py in post_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]) from utils/search/search_forum.py
      • 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() (NOT post.get_absolute_url())

        • because forum/views.py:207 fetches posts with moderation_state="OK" and the post page would 404 after unpublishing.

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:

  1. Deploy code that is tolerant but does not require the field (optional)
  2. Run migration: accounts/migrations/0043...
  3. 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)

  1. Edited first post of a thread
  • If the first post is set to "NM", the thread may become hidden from forum listings (many queries require first_post__moderation_state="OK" in forum/views.py, e.g. thread = get_object_or_404(... first_post__moderation_state="OK") at forum/views.py:221).
  • This is consistent with “unpublish edited post”, but it’s a real UX impact. This design follows the requirement exactly.
  1. 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).
  1. 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 — add Profile.forum_posts_need_moderation
  • accounts/migrations/0043_profile_forum_posts_need_moderation.py — new migration
  • accounts/admin.py — expose field in ProfileAdmin (recommended)

Forum behavior

  • forum/forms.py — add moderation action choice

  • forum/views.py

    • moderate_posts — implement new action; approve+index; set user flag
    • reply and new_thread — force moderation for watched users
    • post_edit — unpublish on watched user self-edit + deindex + redirect thread

Search utilities used

  • utils/search/search_forum.py — use delete_posts_from_search_engine and add_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):

  1. Approve keep on watchlist sets profile flag
  • Create Post with "NM", moderate using new action, assert:

    • post is "OK"
    • post.author.profile.forum_posts_need_moderation is True
  1. Watched user new reply goes to moderation
  • Flag user, POST to reverse("forums-reply", ...), assert created post is "NM"
  1. Watched user edit unpublishes
  • Create an "OK" post, flag user, edit it via reverse("forums-post-edit", ...), assert:

    • post is "NM"
    • response redirects to thread URL (not post URL)

(You can mock out Solr indexing calls if tests currently do, or configure search engine in test settings as elsewhere in the suite.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions