FEATURE: proper mailing list mode

once enable_mailing_list_mode is enabled any user can elect
to get every post via email unless they opt out of category or topic
This commit is contained in:
Sam 2014-02-07 11:06:35 +11:00
parent 3a1c5ed39d
commit 227873df78
23 changed files with 134 additions and 68 deletions

View File

@ -178,7 +178,7 @@ Discourse.User = Discourse.Model.extend({
'digest_after_days',
'new_topic_duration_minutes',
'external_links_in_new_tab',
'watch_new_topics',
'mailing_list_mode',
'enable_quoting');
_.each(['muted','watched','tracked'], function(s){

View File

@ -83,6 +83,13 @@
<div class="control-group">
<label class="control-label">{{i18n user.email_settings}}</label>
<div class="controls">
{{#if Discourse.SiteSettings.enable_mailing_list_mode}}
<label>
{{view Ember.Checkbox checkedBinding="mailing_list_mode"}}
{{i18n user.mailing_list_mode}}
</label>
{{/if}}
<label>{{view Ember.Checkbox checkedBinding="email_digests"}}
{{i18n user.email_digests.title}}</label>
@ -116,15 +123,6 @@
{{combobox valueAttribute="value" content=considerNewTopicOptions value=new_topic_duration_minutes}}
</div>
{{#if Discourse.SiteSettings.enable_watch_new_topics}}
<div class="controls">
<label>
{{view Ember.Checkbox checkedBinding="watch_new_topics"}}
{{i18n user.watch_new_topics}}
</label>
</div>
{{/if}}
<div class="controls">
<label>{{view Ember.Checkbox checkedBinding="external_links_in_new_tab"}}
{{i18n user.external_links_in_new_tab}}</label>

View File

@ -0,0 +1,37 @@
module Jobs
class NotifyMailingListSubscribers < Jobs::Base
def execute(args)
post_id = args[:post_id]
post = Post.find(post_id) if post_id
raise Discourse::InvalidParameters.new(:post_id) unless post
User.not_suspended
.not_blocked
.real
.where(mailing_list_mode: true)
.where('NOT EXISTS(
SELECT 1
FROM topic_users tu
WHERE
tu.topic_id = ? AND
tu.user_id = users.id AND
tu.notification_level = ?
)', post.topic_id, TopicUser.notification_levels[:muted])
.where('NOT EXISTS(
SELECT 1
FROM category_users cu
WHERE
cu.category_id = ? AND
cu.user_id = users.id AND
cu.notification_level = ?
)', post.topic.category_id, CategoryUser.notification_levels[:muted])
.each do |user|
UserNotifications.mailing_list_notify(user, post).deliver
end
end
end
end

View File

@ -93,6 +93,17 @@ class UserNotifications < ActionMailer::Base
notification_email(user, opts)
end
def mailing_list_notify(user, post)
send_notification_email(
title: post.topic.title,
post: post,
from_alias: post.user.username,
allow_reply_by_email: true,
notification_type: "posted",
user: user
)
end
protected
def email_post_markdown(post)
@ -130,9 +141,34 @@ class UserNotifications < ActionMailer::Base
username = @notification.data_hash[:original_username]
notification_type = opts[:notification_type] || Notification.types[@notification.notification_type].to_s
return if SiteSetting.enable_mailing_list_mode &&
["replied", "mentioned", "quoted", "posted"].include?(notification_type)
title = @notification.data_hash[:topic_title]
allow_reply_by_email = opts[:allow_reply_by_email]
send_notification_email(
title: title,
post: @post,
from_alias: username,
allow_reply_by_email: allow_reply_by_email,
notification_type: notification_type,
user: user
)
end
def send_notification_email(opts)
post = opts[:post]
title = opts[:title]
allow_reply_by_email = opts[:allow_reply_by_email]
from_alias = opts[:from_alias]
notification_type = opts[:notification_type]
user = opts[:user]
context = ""
tu = TopicUser.get(@post.topic_id, user)
context_posts = self.class.get_context_posts(@post, tu)
tu = TopicUser.get(post.topic_id, user)
context_posts = self.class.get_context_posts(post, tu)
# make .present? cheaper
context_posts = context_posts.to_a
@ -147,37 +183,36 @@ class UserNotifications < ActionMailer::Base
html = UserNotificationRenderer.new(Rails.configuration.paths["app/views"]).render(
template: 'email/notification',
format: :html,
locals: { context_posts: context_posts, post: @post }
locals: { context_posts: context_posts, post: post }
)
template = "user_notifications.user_#{notification_type}"
if @post.topic.private_message?
if post.topic.private_message?
template << "_pm"
end
email_opts = {
topic_title: @notification.data_hash[:topic_title],
message: email_post_markdown(@post),
url: @post.url,
post_id: @post.id,
topic_id: @post.topic_id,
topic_title: title,
message: email_post_markdown(post),
url: post.url,
post_id: post.id,
topic_id: post.topic_id,
context: context,
username: username,
username: from_alias,
add_unsubscribe_link: true,
allow_reply_by_email: opts[:allow_reply_by_email],
allow_reply_by_email: allow_reply_by_email,
template: template,
html_override: html,
style: :notification
}
# If we have a display name, change the from address
if username.present?
email_opts[:from_alias] = username
if from_alias.present?
email_opts[:from_alias] = from_alias
end
TopicUser.change(user.id, @post.topic_id, last_emailed_post_number: @post.post_number)
TopicUser.change(user.id, post.topic_id, last_emailed_post_number: post.post_number)
build_email(user.email, email_opts)
end
end

View File

@ -72,23 +72,6 @@ class TopicUser < ActiveRecord::Base
TopicUser.where('topic_id = ? and user_id = ?', topic, user).first
end
def auto_watch_new_topic(topic_id)
# Can not afford to slow down creation of topics when a pile of users are watching new topics, reverting to SQL for max perf here
sql = <<SQL
INSERT INTO topic_users(user_id, topic_id, notification_level, notifications_reason_id)
SELECT id, :topic_id, :level, :reason
FROM users
WHERE watch_new_topics AND
NOT EXISTS(SELECT 1 FROM topic_users WHERE topic_id = :topic_id AND user_id = users.id)
SQL
exec_sql(
sql,
topic_id: topic_id,
level: notification_levels[:watching],
reason: notification_reasons[:auto_watch]
)
end
# Change attributes for a user (creates a record when none is present). First it tries an update
# since there's more likely to be an existing record than not. If the update returns 0 rows affected

View File

@ -83,6 +83,7 @@ class User < ActiveRecord::Base
attr_accessor :notification_channel_position
scope :blocked, -> { where(blocked: true) } # no index
scope :not_blocked, -> { where(blocked: false) } # no index
scope :suspended, -> { where('suspended_till IS NOT NULL AND suspended_till > ?', Time.zone.now) } # no index
scope :not_suspended, -> { where('suspended_till IS NULL') }
# excluding fake users like the community user

View File

@ -51,7 +51,7 @@ class UserSerializer < BasicUserSerializer
:email_direct,
:email_always,
:digest_after_days,
:watch_new_topics,
:mailing_list_mode,
:auto_track_topics_after_msecs,
:new_topic_duration_minutes,
:external_links_in_new_tab,

View File

@ -14,7 +14,7 @@ class UserUpdater
:external_links_in_new_tab,
:enable_quoting,
:dynamic_favicon,
:watch_new_topics
:mailing_list_mode
]
def initialize(actor, user)

View File

@ -229,7 +229,6 @@ da:
deleted: "(slettet)"
suspended_notice: "Denne bruger er suspenderet indtil {{date}}."
suspended_reason: "Begrundelse: "
watch_new_topics: "Overvåg automatisk alle nye emner i forummet"
watched_categories: "Overvåget"
watched_categories_instructions: "Du overvåger automatisk alle emner i disse kategorier"
tracked_categories: "Fulgt"

View File

@ -230,7 +230,6 @@ de:
deleted: "(gelöscht)"
suspended_notice: "Diser Benutzer ist bis zum {{date}} susperndiert."
suspended_reason: "Grund: "
watch_new_topics: "Automatische alle neuen Themen dieses Forums beobachten"
watched_categories: "Beobachten"
watched_categories_instructions: "Das selbe wie 'verfolgen', zusätzlich wirst du über alle neuen Beiträge und Themen informiert."
tracked_categories: "Verfolgen"

View File

@ -231,7 +231,7 @@ en:
deleted: "(deleted)"
suspended_notice: "This user is suspended until {{date}}."
suspended_reason: "Reason: "
watch_new_topics: "Automatically watch all new topics posted on the forum"
mailing_list_mode: "Automatically recieve an email every time a post is made on the forum (unless you have the topic or category muted)"
watched_categories: "Watched"
watched_categories_instructions: "Same as Tracking, plus you will be notified of all new posts and topics."
tracked_categories: "Tracked"

View File

@ -229,7 +229,6 @@ fr:
deleted: "(supprimé)"
suspended_notice: "L'utilisateur est suspendu jusqu'au {{date}}."
suspended_reason: "Raison :"
watch_new_topics: "Surveiller automatiquement toutes les nouvelles discussions postées sur le forum"
watched_categories: "Surveillées"
watched_categories_instructions: "Vous allez surveiller automatiquement toutes les discussions de ces catégories"
tracked_categories: "Suivies"

View File

@ -233,7 +233,6 @@ nl:
deleted: (verwijderd)
suspended_notice: "Deze gebruiker is geschorst tot {{date}}."
suspended_reason: "Reden: "
watch_new_topics: "Houd alle nieuwe topics in het forum automatisch in de gaten"
watched_categories: "In de gaten gehouden"
watched_categories_instructions: "Zelfde als volgen, daarnaast zou je een notificatie ontvangen voor alle nieuwe berichten en topics"
tracked_categories: "Gevolgd"

View File

@ -229,7 +229,6 @@ zh_CN:
deleted: "(已删除)"
suspended_notice: "该用户将被禁止登录,直至{{date}}."
suspended_reason: "原因:"
watch_new_topics: "自动关注论坛中所有新发表的主题"
watched_categories: "已关注"
watched_categories_instructions: "和跟踪相同,此外你还将收到新的帖子和主题的通知。"
tracked_categories: "已跟踪"

View File

@ -749,7 +749,6 @@ da:
pop3s_polling_host: "Host til polling af POP3S-konto."
pop3s_polling_username: "Brugernavn til polling af POP3S-konto."
pop3s_polling_password: "Adgangskode til polling af POP3S-konto."
enable_watch_new_topics: "Lad brugerne overvåge alle nye emner (valgfrit) via en brugerindstilling"
minimum_topics_similar: "Hvor emner der skal eksistere i databasen før der vises tilsvarende emner."

View File

@ -761,7 +761,7 @@ en:
pop3s_polling_host: "The host to poll for email via POP3S"
pop3s_polling_username: "The username for the POP3S account to poll for email"
pop3s_polling_password: "The password for the POP3S account to poll for email"
enable_watch_new_topics: "Allow users to watch all new topics (optionally) via a user preference"
enable_mailing_list_mode: "Allow users to (optionally) opt-in to mailing list mode via a user preference"
minimum_topics_similar: "How many topics need to exist in the database before similar topics are presented."

View File

@ -663,7 +663,6 @@ fr:
pop3s_polling_host: "L'hôte utilisé pour le polling pour l'email via POP3S"
pop3s_polling_username: "Le nom d'utilisateur pour le polling POPS3 par email"
pop3s_polling_password: "Le mot de passe pour le polling POPS3 par email"
enable_watch_new_topics: "Autoriser les utilisateurs à suivre toutes les nouvelles discussions (optionnellement) via une préférence de l'utilisateur"
minimum_topics_similar: "Combien de topics ont besoin d'exister dans la base de données avant que des topics similaires soit présentés."
relative_date_duration: "Nombre de jours après la création d'un message à partir desquels les dates seront affichées en absolu plutôt qu'en relatif. Exemple: relatif : 7j, absolut : 20 Fév"
delete_user_max_age: "L'age maximum d'un utilisateur, en jours, qui permet à un administrateur de le supprimer."

View File

@ -753,7 +753,6 @@ nl:
pop3s_polling_host: "De host voor POP3S"
pop3s_polling_username: "De username voor het POP3S e-mailaccount"
pop3s_polling_password: "Het wachtwoord voor het POP3S e-mailaccount"
enable_watch_new_topics: "Sta toe dat gebruikers alle nieuwe topics in de gaten houden (optioneel) via hun gebruikersinstellingen"
minimum_topics_similar: "Hoeveel topics moeten er in de database staan voordat er vergelijkbare topics getoond worden?"

View File

@ -232,7 +232,7 @@ email:
pop3s_polling_port: 995
pop3s_polling_username: ''
pop3s_polling_password: ''
enable_watch_new_topics:
enable_mailing_list_mode:
default: false
client: true

View File

@ -0,0 +1,5 @@
class AddMailingListModeToUsers < ActiveRecord::Migration
def change
rename_column :users, :watch_new_topics, :mailing_list_mode
end
end

View File

@ -135,8 +135,17 @@ class PostCreator
end
def after_post_create
if !@topic.private_message? && @post.post_number > 1 && @post.post_type != Post.types[:moderator_action]
TopicTrackingState.publish_unread(@post)
if !@topic.private_message? && @post.post_type != Post.types[:moderator_action]
if @post.post_number > 1
TopicTrackingState.publish_unread(@post)
end
if SiteSetting.enable_mailing_list_mode
Jobs.enqueue_in(
SiteSetting.email_time_window_mins.minutes,
:notify_mailing_list_subscribers,
post_id: @post.id
)
end
end
end

View File

@ -39,7 +39,6 @@ class TopicCreator
@topic.notifier.watch_topic!(id, nil)
end
TopicUser.auto_watch_new_topic(@topic.id)
CategoryUser.auto_watch_new_topic(@topic)
end

View File

@ -268,21 +268,28 @@ describe TopicUser do
tu.seen_post_count.should == 2
end
describe "auto_watch_new_topic" do
describe "mailing_list_mode" do
it "will receive email notification for every topic" do
SiteSetting.stubs(:enable_mailing_list_mode).returns(true)
it "auto watches topics when called" do
user1 = Fabricate(:user)
user2 = Fabricate(:user, watch_new_topics: true)
user3 = Fabricate(:user, watch_new_topics: true)
TopicUser.auto_watch_new_topic(topic.id)
user2 = Fabricate(:user, mailing_list_mode: true)
post = create_post
user3 = Fabricate(:user, mailing_list_mode: true)
create_post(topic_id: post.topic_id)
TopicUser.get(topic, user1).should == nil
# mails posts from earlier topics
tu = TopicUser.where(user_id: user3.id, topic_id: post.topic_id).first
tu.last_emailed_post_number.should == 2
tu = TopicUser.get(topic, user2)
tu.notification_level.should == TopicUser.notification_levels[:watching]
tu.notifications_reason_id.should == TopicUser.notification_reasons[:auto_watch]
# mails nothing to random users
tu = TopicUser.where(user_id: user1.id, topic_id: post.topic_id).first
tu.should be_nil
TopicUser.get(topic, user3).notification_level.should == TopicUser.notification_levels[:watching]
# mails other user
tu = TopicUser.where(user_id: user2.id, topic_id: post.topic_id).first
tu.last_emailed_post_number.should == 2
end
end