Skip to content
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
440e39a
switched UsersController#soft-delete to at_least_global_moderator?
Oaphi Oct 26, 2025
a4740ad
renamed User#do_soft_delete to just soft_delete (naming convention)
Oaphi Oct 26, 2025
3b5d5f2
added a proper CommunityUser#soft_delete method
Oaphi Oct 26, 2025
5b77efd
fixed deletion confirmation layout (missing </li> tag)
Oaphi Oct 26, 2025
22cee25
reworked network_name to be a global site setting
Oaphi Oct 26, 2025
f0ec2e9
switched a bunch more views to the NetworkName site setting
Oaphi Oct 26, 2025
546e22a
removed Codidact mention from keyboard shortcuts
Oaphi Oct 26, 2025
ae0dfa3
a bunch more instances of hardcoded network name removed
Oaphi Oct 26, 2025
879accb
added test for CleanUpSpammyUsers job + improved system user handling
Oaphi Oct 26, 2025
76517e8
removed no longer needed system user fixture (it's seeded) & adjusted…
Oaphi Oct 26, 2025
f265135
added webmock to help with preventing & tracking external requests in…
Oaphi Oct 27, 2025
3724130
prevented CDN requests when testing mailers
Oaphi Oct 27, 2025
4fa820a
fixed backup_code's 2FA URI pointing to the default host
Oaphi Oct 27, 2025
16c0122
added tests for TwoFactorMailer
Oaphi Oct 27, 2025
51d4c18
added network name assertions for TwoFactorMailer tests
Oaphi Oct 27, 2025
4d6ba34
external requests are disabled by default when using WebMock, no need…
Oaphi Oct 27, 2025
adb57ea
ensured all external requests in test cases are stubbed out and clean…
Oaphi Oct 27, 2025
2009d94
fixed PotentialSpamProfilesJob test by added a dedicated profile spam…
Oaphi Oct 27, 2025
397d0c5
allowed network connections in system tests
Oaphi Oct 27, 2025
0a8480d
removed stray non-existent action name from verify_moderator action c…
Oaphi Oct 27, 2025
febeafc
Merge branch 'develop' into ovalt/user-restore
Oaphi Oct 27, 2025
2bbe33c
fixed Filter model test since we don't have a system user fixture any…
Oaphi Oct 27, 2025
1788af1
fixed TwoFactorMailer tests not respecting default url options
Oaphi Oct 27, 2025
acc97b6
fixed missing category for the NetworkName site setting
Oaphi Oct 28, 2025
02d8a45
added User#anonymize method (and tests) to make the intent clear
Oaphi Oct 28, 2025
1c34cd4
added NetworkURL global site setting
Oaphi Oct 29, 2025
757fe46
fixed image without alt text regular expression
Oaphi Nov 2, 2025
7994836
expanded user profile soft delete test to cover all users who are at …
Oaphi Nov 3, 2025
737e382
moved user ping regular expression to a USER_PING_REG_EXP constant to…
Oaphi Nov 5, 2025
1f292b9
allowed user pings to be made to users with negative ids
Oaphi Nov 5, 2025
6d2ad87
fixed Comment#pings method (it's unused but still)
Oaphi Nov 5, 2025
6e6d1a3
added tests for the Comment#pings method
Oaphi Nov 5, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ group :test do
gem 'capybara', '~> 3.38'
gem 'selenium-webdriver', '~> 4.7'
gem 'webdrivers', '~> 5.2'
gem 'webmock', '~> 3.26'
end

group :development, :test do
Expand Down
9 changes: 9 additions & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,9 @@ GEM
counter_culture (3.11.2)
activerecord (>= 4.2)
activesupport (>= 4.2)
crack (1.0.1)
bigdecimal
rexml
crass (1.0.6)
css_parser (1.21.1)
addressable
Expand Down Expand Up @@ -166,6 +169,7 @@ GEM
activesupport (>= 6.1)
groupdate (6.5.1)
activesupport (>= 7)
hashdiff (1.2.1)
hashie (5.0.0)
htmlentities (4.3.4)
i18n (1.14.7)
Expand Down Expand Up @@ -448,6 +452,10 @@ GEM
nokogiri (~> 1.6)
rubyzip (>= 1.3.0)
selenium-webdriver (~> 4.0, < 4.11)
webmock (3.26.0)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.1)
websocket (1.2.11)
websocket-driver (0.8.0)
Expand Down Expand Up @@ -530,6 +538,7 @@ DEPENDENCIES
tzinfo-data (~> 1.2022.3)
web-console (~> 4.2)
webdrivers (~> 5.2)
webmock (~> 3.26)
whenever (~> 1.0)
will_paginate (~> 3.3)
will_paginate-bootstrap (~> 1.0)
Expand Down
2 changes: 1 addition & 1 deletion app/assets/javascripts/keyboard_tools.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ $(() => {

if (isHelp) {
_CodidactKeyboard.dialog(
'Codidact Keyboard Shortcuts\n' +
'Keyboard Shortcuts\n' +
'===========================\n' +
'? Open this help\n' +
'esc Close this help\n' +
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/moderator_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def handle_spammy_users
spam = User.where(id: params[:spam_ids])
spam.each do |user|
user.block('Profile spam', length: 10.years, automatic: false)
user.do_soft_delete(current_user)
user.soft_delete(current_user)
end
flash[:success] = "#{spam.size} users blocked and deleted."
redirect_to mod_spammers_path
Expand Down
2 changes: 1 addition & 1 deletion app/controllers/users/registrations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def do_delete
else
UserMailer.with(user: @user, host: RequestContext.community.host, community: RequestContext.community)
.deletion_confirmation.deliver_later
@user.do_soft_delete(@user)
@user.soft_delete(@user)
flash[:info] = 'Sorry to see you go!'
redirect_to root_path
end
Expand Down
10 changes: 4 additions & 6 deletions app/controllers/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -326,17 +326,15 @@ def soft_delete

case params[:type]
when 'profile'
AuditLog.moderator_audit(event_type: 'profile_delete', related: @user.community_user, user: current_user,
comment: @user.community_user.attributes_print(join: "\n"))
@user.community_user.update(deleted: true, deleted_by: current_user, deleted_at: DateTime.now)
@user.community_user.soft_delete(current_user)
when 'user'
unless current_user.is_global_moderator || current_user.is_global_admin
unless current_user.at_least_global_moderator?
render json: { status: 'failed', message: 'Non-global moderator cannot perform global deletion.' },
status: 403
status: :forbidden
return
end

@user.do_soft_delete(current_user)
@user.soft_delete(current_user)
else
render json: { status: 'failed', message: 'Unrecognised deletion type.' }, status: 400
return
Expand Down
2 changes: 1 addition & 1 deletion app/helpers/application_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ def current_user
# Gets the special System user
# @return [User, nil]
def system_user
User.find(-1)
User.system
end

##
Expand Down
7 changes: 5 additions & 2 deletions app/jobs/clean_up_spammy_users_job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ def perform(created_after: 1.month.ago)
.where('users.created_at >= ?', created_after)
.where(users: { deleted: false }).group('users.id').having('count(posts.id) > 0')
.having('count(distinct if(posts.deleted = true, null, posts.id)) = 0')

possible_spammers.each do |spammer|
all_posts_spam = spammer.posts.all? do |post|
# A post is considered spam if there are any helpful spam flags on it.
post.flags.any? { |flag| flag.post_flag_type.name == "it's spam" && flag.status == 'helpful' }
post.flags.any? do |flag|
flag.post_flag_type.name == "it's spam" && flag.status == 'helpful'
end
end
if all_posts_spam
spammer.block('automatic block from spam cleanup job', length: 2.years)
spammer.do_soft_delete(User.find(-1))
spammer.soft_delete(User.system)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion app/mailers/admin_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def to_moderators
from = "#{SiteSetting['ModeratorDistributionListSenderName', community: @community]} " \
"<#{SiteSetting['ModeratorDistributionListSenderEmail', community: @community]}>"
to = SiteSetting['ModeratorDistributionListSenderEmail', community: @community]
mail subject: "Codidact Moderators: #{@subject}", to: to, from: from, bcc: emails
mail subject: "#{SiteSetting['NetworkName']} Moderators: #{@subject}", to: to, from: from, bcc: emails
end

def to_all_users
Expand Down
2 changes: 1 addition & 1 deletion app/mailers/summary_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def content_summary
@users = params[:users]

mail(from: "#{SiteSetting['NoReplySenderName']} <#{SiteSetting['NoReplySenderEmail']}>",
subject: 'Codidact Content Summary',
subject: "#{SiteSetting['NetworkName']} Content Summary",
to: params[:to])
end
end
6 changes: 3 additions & 3 deletions app/mailers/two_factor_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,20 @@ def disable_email
@host = params[:host]
@token = SecureRandom.urlsafe_base64(64)
user.update(login_token: @token, login_token_expires_at: 5.minutes.from_now)
mail to: user.email, subject: 'Disable two-factor authentication on Codidact'
mail to: user.email, subject: "Disable two-factor authentication on #{SiteSetting['NetworkName']}"
end

def login_email
user = params[:user]
@host = params[:host]
@token = SecureRandom.urlsafe_base64(64)
user.update(login_token: @token, login_token_expires_at: 5.minutes.from_now)
mail to: user.email, subject: 'Your sign in link for Codidact'
mail to: user.email, subject: "Your sign in link for #{SiteSetting['NetworkName']}"
end

def backup_code
@user = params[:user]
@host = params[:host]
mail to: @user.email, subject: 'Your 2FA backup code for Codidact'
mail to: @user.email, subject: "Your 2FA backup code for #{SiteSetting['NetworkName']}"
end
end
2 changes: 1 addition & 1 deletion app/mailers/user_mailer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ def deletion_confirmation
@user = params[:user]
@host = params[:host]
@community = params[:community]
mail to: @user.email, subject: 'Your Codidact account has been deleted as you requested',
mail to: @user.email, subject: "Your #{SiteSetting['NetworkName']} account has been deleted as you requested",
from: "#{SiteSetting['NoReplySenderName', community: @community]} " \
"<#{SiteSetting['NoReplySenderEmail', community: @community]}>"
end
Expand Down
9 changes: 9 additions & 0 deletions app/models/community_user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,13 @@ def recalc_trust_level
update(trust_level: trust)
trust
end

# Soft-deletes the community user
# @param attribute_to [User] user to attribute the action to
def soft_delete(attribute_to)
AuditLog.moderator_audit(event_type: 'profile_delete', related: self, user: attribute_to,
comment: attributes_print(join: "\n"))

update(deleted: true, deleted_by: attribute_to, deleted_at: DateTime.now)
end
end
12 changes: 11 additions & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@ def self.search(term)
where('username LIKE ?', "%#{sanitize_sql_like(term)}%")
end

# Gets the system user
# @return [User, nil]
def self.system
find_by(id: -1)
end

# Safely gets the user's reputation even if they don't have a community user
# @return [Integer] user's reputation
def reputation
Expand Down Expand Up @@ -476,12 +482,16 @@ def active_flags(post)
post.flags.where(user: self, status: nil)
end

def do_soft_delete(attribute_to)
# Soft-deletes the user (username, password, and email are irrevocably reset!)
# @param attribute_to [User] user to attribute the action to
def soft_delete(attribute_to)
AuditLog.moderator_audit(event_type: 'user_delete', related: self, user: attribute_to,
comment: attributes_print(join: "\n"))

assign_attributes(deleted: true, deleted_by_id: attribute_to.id, deleted_at: DateTime.now,
username: "user#{id}", email: "#{id}@deleted.localhost",
password: SecureRandom.hex(32))

skip_reconfirmation!
save
end
Expand Down
132 changes: 66 additions & 66 deletions app/views/advertisement/index.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,74 +3,74 @@
<p><%= t('ads.blurb_types') %></p>

<div class="grid">
<div class="grid--cell is-12 is-4-lg">
<div class="widget">
<div class="widget--header h-ta-center h-fw-bold">
<%= t('g.codidact_network').capitalize %>
</div>
<a href="<%= codidact_ads_url %>"><img src="<%= codidact_ads_url %>" alt="Codidact network ad"></a>
<div class="widget--body">
<p><%= t('ads.image_link') %></p>
<pre><code><%= codidact_ads_url %></code></pre>
<p><%= t('ads.image_html') %></p>
<pre><code><%= ('<img src="' + codidact_ads_url + '" alt="Codidact network">') %></code></pre>
</div>
</div>
<div class="grid--cell is-12 is-4-lg">
<div class="widget">
<div class="widget--header h-ta-center h-fw-bold">
<%= t('g.codidact_network').capitalize %>
</div>
<a href="<%= codidact_ads_url %>"><img src="<%= codidact_ads_url %>" alt="Codidact network ad"></a>
<div class="widget--body">
<p><%= t('ads.image_link') %></p>
<pre><code><%= codidact_ads_url %></code></pre>
<p><%= t('ads.image_html') %></p>
<pre><code><%= ("<img src=\"#{codidact_ads_url}\" alt=\"#{SiteSetting['NetworkName']} network\">") %></code></pre>
</div>
</div>
<div class="grid--cell is-12 is-4-lg">
<div class="widget">
<div class="widget--header h-ta-center h-fw-bold">
<%= t('g.community').capitalize %>
</div>
<a href="<%= community_ads_url %>"><img src="<%= community_ads_url %>" alt="Community ad"></a>
<div class="widget--body">
<p><%= t('ads.image_link') %></p>
<pre><code><%= community_ads_url %></code></pre>
<p><%= t('ads.image_html') %></p>
<pre><code><%= ('<img src="' + community_ads_url + '" alt="' + @community.name + '">') %></code></pre>
</div>
</div>
</div>
<div class="grid--cell is-12 is-4-lg">
<div class="widget">
<div class="widget--header h-ta-center h-fw-bold">
<%= t('g.community').capitalize %>
</div>
<a href="<%= community_ads_url %>"><img src="<%= community_ads_url %>" alt="Community ad"></a>
<div class="widget--body">
<p><%= t('ads.image_link') %></p>
<pre><code><%= community_ads_url %></code></pre>
<p><%= t('ads.image_html') %></p>
<pre><code><%= ('<img src="' + community_ads_url + '" alt="' + @community.name + '">') %></code></pre>
</div>
</div>
<div class="grid--cell is-12 is-4-lg">
<div class="widget">
<div class="widget--header h-ta-center h-fw-bold">
<%= t('ads.random_posts') %>
</div>
<a href="<%= random_question_ads_url %>"><img src="<%= random_question_ads_url %>" alt="Random posts ad"></a>
<div class="widget--body">
<p><%= t('ads.image_link') %></p>
<pre><code><%= random_question_ads_url %></code></pre>
<p><%= t('ads.image_html') %></p>
<pre><code><%= ('<img src="' + random_question_ads_url + '" alt="' + @community.name + ' questions">') %></code></pre>
</div>
</div>
</div>
<div class="grid--cell is-12 is-4-lg">
<div class="widget">
<div class="widget--header h-ta-center h-fw-bold">
<%= t('ads.random_posts') %>
</div>
<a href="<%= random_question_ads_url %>"><img src="<%= random_question_ads_url %>" alt="Random posts ad"></a>
<div class="widget--body">
<p><%= t('ads.image_link') %></p>
<pre><code><%= random_question_ads_url %></code></pre>
<p><%= t('ads.image_html') %></p>
<pre><code><%= ('<img src="' + random_question_ads_url + '" alt="' + @community.name + ' questions">') %></code></pre>
</div>
</div>
<div class="grid--cell is-12 is-4-lg">
<div class="widget">
<div class="widget--header h-ta-center h-fw-bold">
<%= t('ads.specific_post') %>
</div>
<div class="widget--body">
<p><%= t('ads.image_link') %></p>
<pre><code><%= specific_question_ads_url('X') %></code></pre>
<p><%= t('ads.image_html') %></p>
<pre><code><%= ('<img src="' + specific_question_ads_url('X') + '" alt="(choose an alt text)">') %></code></pre>
<p><em><%= t('ads.replace_x.post') %></em></p>
</div>
</div>
</div>
<div class="grid--cell is-12 is-4-lg">
<div class="widget">
<div class="widget--header h-ta-center h-fw-bold">
<%= t('ads.specific_post') %>
</div>
<div class="widget--body">
<p><%= t('ads.image_link') %></p>
<pre><code><%= specific_question_ads_url('X') %></code></pre>
<p><%= t('ads.image_html') %></p>
<pre><code><%= ('<img src="' + specific_question_ads_url('X') + '" alt="(choose an alt text)">') %></code></pre>
<p><em><%= t('ads.replace_x.post') %></em></p>
</div>
</div>
<div class="grid--cell is-12 is-4-lg">
<div class="widget">
<div class="widget--header h-ta-center h-fw-bold">
<%= t('ads.random_from_cat') %>
</div>
<div class="widget--body">
<p><%= t('ads.image_link') %></p>
<pre><code><%= specific_category_ads_url('X') %></code></pre>
<p><%= t('ads.image_html') %></p>
<pre><code><%= ('<img src="' + specific_category_ads_url('X') + '" alt="(choose an alt text)">') %></code></pre>
<p><em><%= t('ads.replace_x.category') %></em></p>
</div>
</div>
</div>
<div class="grid--cell is-12 is-4-lg">
<div class="widget">
<div class="widget--header h-ta-center h-fw-bold">
<%= t('ads.random_from_cat') %>
</div>
<div class="widget--body">
<p><%= t('ads.image_link') %></p>
<pre><code><%= specific_category_ads_url('X') %></code></pre>
<p><%= t('ads.image_html') %></p>
<pre><code><%= ('<img src="' + specific_category_ads_url('X') + '" alt="(choose an alt text)">') %></code></pre>
<p><em><%= t('ads.replace_x.category') %></em></p>
</div>
</div>
</div>
</div>
</div>
10 changes: 7 additions & 3 deletions app/views/application/keyboard_tools.html.erb
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
<h1>Keyboard tools</h1>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't confirmed visually because I don't yet know how to navigate to this page...

Copy link
Member Author

@Oaphi Oaphi Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently, the route got explicitly removed (while the view and the controller action are still there) in 9815057#diff-959bc9abc46a55332bb64d5155a79323afa75a50ec1a2137ddd22d926f62c6c5L215. @ArtOfCode- (just making sure we are all up to speed regarding the removal)?

<p>Codidact supports keyboard tools to enhance user accessibility. You can activate (and then later deactivate) them here on this page:</p>

<p>
<%= SiteSetting['NetworkName'] %> supports keyboard tools to enhance user accessibility.
You can activate (and then later deactivate) them here on this page:
</p>
<p>You'll need JavaScript to be enabled for the tools to work.</p>

<p>Keyboard tools are currently <strong class="js-keyboard_tools-status">unknown</strong>.
<button class="button is-outlined js-keyboard_tools-toggle">Toggle</button>
<button class="button is-outlined js-keyboard_tools-toggle">Toggle</button>
</p>

<p>This setting is stored in your browser and not your user account, so you can set it as you want it on any device.</p>
<p>This setting is stored in your browser and not your user account, so you can set it as you want it on any device.</p>
2 changes: 1 addition & 1 deletion app/views/categories/rss_feed.rss.builder
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ xml.feed xmlns: 'http://www.w3.org/2005/Atom' do
xml.id category_feed_url(@category)
xml.title "New Posts - #{@category.name} - #{SiteSetting['SiteName']}"
xml.author do
xml.name "#{SiteSetting['SiteName']} - Codidact"
xml.name "#{SiteSetting['SiteName']} - #{SiteSetting['NetworkName']}"
end
xml.link nil, rel: 'self', href: category_url(@category)
xml.updated @posts.maximum(:last_activity)&.iso8601 || RequestContext.community.created_at&.iso8601
Expand Down
2 changes: 1 addition & 1 deletion app/views/complaints_mailer/legal_deletion.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@

<p>
Thanks for your understanding.<br/>
<%= t 'platform.network_name' %> Community Team
<%= SiteSetting['NetworkName'] %> Community Team
</p>
Loading