From 53ea57330e661d1faa8ca7e25d3a5e25d1f67ce5 Mon Sep 17 00:00:00 2001 From: Pablo Brasero Date: Tue, 7 Apr 2026 13:15:46 +0100 Subject: [PATCH] Ruby API to manage notification preferences --- app/models/user_notification_preferences.rb | 58 ++++++++++++++ .../user_notification_preferences_test.rb | 79 +++++++++++++++++++ 2 files changed, 137 insertions(+) create mode 100644 app/models/user_notification_preferences.rb create mode 100644 test/models/user_notification_preferences_test.rb diff --git a/app/models/user_notification_preferences.rb b/app/models/user_notification_preferences.rb new file mode 100644 index 000000000..348d7fa60 --- /dev/null +++ b/app/models/user_notification_preferences.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +class UserNotificationPreferences + EVENTS = %w[ + changeset_comment + diary_comment + direct_message + gpx_import_failure + gpx_import_success + new_follower + note_comment + ].freeze + + MECHANISMS = %w[ + email + ].freeze + + def initialize(user) + @user = user + end + + # Receives a hash in the form `event => [mechanisms...]`. Eg: + # `{:changeset_comment => ["email"], :new_follower => []}` + def update(new_prefs) + updated_records = + EVENTS.map do |event_name| + MECHANISMS.filter_map do |mechanism| + next unless new_prefs.key?(event_name) + + record = @user.preferences.find_or_initialize_by(:k => "notification.#{event_name}.#{mechanism}") + record.v = Array.wrap(new_prefs[event_name]).include?(mechanism) + record + end + end.flatten + + UserPreference.transaction do + updated_records.each(&:save!) + true + end + end + + # One getter method for each event. + EVENTS.each do |event_name| + define_method event_name do + prefs = + @user + .preferences + .where("k LIKE 'notification.#{event_name}.%'") + .pluck(:k, :v) + .to_h + .transform_keys { |k| k.split(".").last } + + MECHANISMS.filter do |mechanism| + prefs.key?(mechanism) ? ActiveModel::Type::Boolean.new.cast(prefs[mechanism]) : true + end + end + end +end diff --git a/test/models/user_notification_preferences_test.rb b/test/models/user_notification_preferences_test.rb new file mode 100644 index 000000000..473a9eb6c --- /dev/null +++ b/test/models/user_notification_preferences_test.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +require "test_helper" + +class UserNotificationPreferencesTest < ActiveSupport::TestCase + def test_all_enabled_by_default + preferences = UserNotificationPreferences.new(create(:user)) + assert_equal ["email"], preferences.changeset_comment + assert_equal ["email"], preferences.diary_comment + assert_equal ["email"], preferences.direct_message + assert_equal ["email"], preferences.gpx_import_failure + assert_equal ["email"], preferences.gpx_import_success + assert_equal ["email"], preferences.new_follower + assert_equal ["email"], preferences.note_comment + end + + def test_update + preferences = UserNotificationPreferences.new(create(:user)) + preferences.update( + "changeset_comment" => ["email"], + "diary_comment" => [], + "direct_message" => ["email"], + "gpx_import_failure" => ["email"], + "gpx_import_success" => nil, + "new_follower" => [] + ) + + assert_equal ["email"], preferences.changeset_comment + assert_equal [], preferences.diary_comment + assert_equal ["email"], preferences.direct_message + assert_equal ["email"], preferences.gpx_import_failure + assert_equal [], preferences.gpx_import_success + assert_equal [], preferences.new_follower + + # Default value + assert_equal ["email"], preferences.note_comment + end + + def test_update_ignore_invalid_values + preferences = UserNotificationPreferences.new(create(:user)) + + preferences.update("changeset_comment" => ["whatsapp"]) + assert_equal [], preferences.changeset_comment + assert_equal 0, UserPreference.where("k LIKE '%whatsapp%'").count + + preferences.update("changeset_comment" => %w[whatsapp email]) + assert_equal ["email"], preferences.changeset_comment + assert_equal 0, UserPreference.where("k LIKE '%whatsapp%'").count + + preferences.update("imaginary_event" => ["email"]) + assert_equal 0, UserPreference.where("k LIKE '%imaginary_event%'").count + end + + def test_update_ignore_unmentioned_events + preferences = UserNotificationPreferences.new(create(:user)) + preferences.update( + "changeset_comment" => ["email"], + "diary_comment" => [] + ) + assert_equal 1, count_event_preferences_in_db("changeset_comment") + assert_equal 1, count_event_preferences_in_db("diary_comment") + assert_equal 0, count_event_preferences_in_db("direct_message") + assert_equal 0, count_event_preferences_in_db("gpx_import_failure") + assert_equal 0, count_event_preferences_in_db("gpx_import_success") + assert_equal 0, count_event_preferences_in_db("new_follower") + assert_equal 0, count_event_preferences_in_db("note_comment") + assert_equal 0, count_event_preferences_in_db("note_comment") + end + + private + + def count_event_preferences_in_db(event_name) + # A bit paranoid, but want to avoid misspellings, etc that produce + # false positives. + raise "Unknown event #{event_name.inspect}" unless UserNotificationPreferences::EVENTS.include?(event_name) + + UserPreference.where("k LIKE 'notification.#{event_name}.%'").count + end +end -- 2.39.5