Skip to content

Commit

Permalink
FEATURE: staff can set a timer to remind them about a topic
Browse files Browse the repository at this point in the history
  • Loading branch information
nlalonde committed May 16, 2017
1 parent 2e152f4 commit 7821400
Show file tree
Hide file tree
Showing 20 changed files with 255 additions and 42 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const CLOSE_STATUS_TYPE = 'close';
const OPEN_STATUS_TYPE = 'open';
const PUBLISH_TO_CATEGORY_STATUS_TYPE = 'publish_to_category';
const DELETE_STATUS_TYPE = 'delete';
const REMINDER_TYPE = 'reminder';

export default Ember.Controller.extend(ModalFunctionality, {
loading: false,
Expand All @@ -17,16 +18,18 @@ export default Ember.Controller.extend(ModalFunctionality, {
autoClose: Ember.computed.equal('selection', CLOSE_STATUS_TYPE),
autoDelete: Ember.computed.equal('selection', DELETE_STATUS_TYPE),
publishToCategory: Ember.computed.equal('selection', PUBLISH_TO_CATEGORY_STATUS_TYPE),
reminder: Ember.computed.equal('selection', REMINDER_TYPE),

showTimeOnly: Ember.computed.or('autoOpen', 'autoDelete'),
showTimeOnly: Ember.computed.or('autoOpen', 'autoDelete', 'reminder'),

@computed("model.closed")
timerTypes(closed) {
return [
{ id: CLOSE_STATUS_TYPE, name: I18n.t(closed ? 'topic.temp_open.title' : 'topic.auto_close.title'), },
{ id: OPEN_STATUS_TYPE, name: I18n.t(closed ? 'topic.auto_reopen.title' : 'topic.temp_close.title') },
{ id: PUBLISH_TO_CATEGORY_STATUS_TYPE, name: I18n.t('topic.publish_to_category.title') },
{ id: DELETE_STATUS_TYPE, name: I18n.t('topic.auto_delete.title') }
{ id: DELETE_STATUS_TYPE, name: I18n.t('topic.auto_delete.title') },
{ id: REMINDER_TYPE, name: I18n.t('topic.reminder.title') }
];
},

Expand Down
26 changes: 26 additions & 0 deletions app/jobs/regular/topic_reminder.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module Jobs
class TopicReminder < Jobs::Base

def execute(args)
topic_timer = TopicTimer.find_by(id: args[:topic_timer_id])

topic = topic_timer&.topic
user = topic_timer&.user

if topic_timer.blank? || topic.blank? || user.blank? ||
topic_timer.execute_at > Time.zone.now
return
end

user.notifications.create!(
notification_type: Notification.types[:topic_reminder],
topic_id: topic.id,
post_number: 1,
data: { topic_title: topic.title, display_username: user.username }.to_json
)

topic_timer.trash!(Discourse.system_user)
end

end
end
3 changes: 2 additions & 1 deletion app/models/notification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@ def self.types
custom: 14,
group_mentioned: 15,
group_message_summary: 16,
watching_first_post: 17
watching_first_post: 17,
topic_reminder: 18
)
end

Expand Down
19 changes: 8 additions & 11 deletions app/models/topic.rb
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ def inherit_auto_close_from_category
if !@ignore_category_auto_close &&
self.category &&
self.category.auto_close_hours &&
!topic_timer&.execute_at
!public_topic_timer&.execute_at

self.set_or_create_timer(
TopicTimer.types[:close],
Expand Down Expand Up @@ -953,12 +953,8 @@ def self.ensure_consistency!
Topic.where("pinned_until < now()").update_all(pinned_at: nil, pinned_globally: false, pinned_until: nil)
end

def topic_timer
@topic_timer ||= topic_timers.first
end

def topic_status_update
topic_timer # will be used to filter timers unrelated to topic status
def public_topic_timer
topic_timers.where(deleted_at: nil, public_type: true).first
end

# Valid arguments for the time:
Expand All @@ -974,10 +970,11 @@ def topic_status_update
# * based_on_last_post: True if time should be based on timestamp of the last post.
# * category_id: Category that the update will apply to.
def set_or_create_timer(status_type, time, by_user: nil, timezone_offset: 0, based_on_last_post: false, category_id: SiteSetting.uncategorized_category_id)
topic_timer = TopicTimer.find_or_initialize_by(
status_type: status_type,
topic: self
)
topic_timer = if TopicTimer.public_types[status_type]
TopicTimer.find_or_initialize_by( status_type: status_type, topic: self )
else
TopicTimer.find_or_initialize_by( status_type: status_type, topic: self, user: by_user )
end

if time.blank?
topic_timer.trash!(trashed_by: by_user || Discourse.system_user)
Expand Down
31 changes: 29 additions & 2 deletions app/models/topic_timer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,15 @@ class TopicTimer < ActiveRecord::Base
validates :topic_id, presence: true
validates :execute_at, presence: true
validates :status_type, presence: true
validates :status_type, uniqueness: { scope: [:topic_id, :deleted_at] }
validates :status_type, uniqueness: { scope: [:topic_id, :deleted_at] }, if: :public_type?
validates :status_type, uniqueness: { scope: [:topic_id, :deleted_at, :user_id] }, if: :private_type?
validates :category_id, presence: true, if: :publishing_to_category?

validate :ensure_update_will_happen

before_save do
self.created_at ||= Time.zone.now if execute_at
self.public_type = self.public_type?

if (execute_at_changed? && !execute_at_was.nil?) || user_id_changed?
self.send("cancel_auto_#{self.class.types[status_type]}_job")
Expand All @@ -36,10 +38,19 @@ def self.types
close: 1,
open: 2,
publish_to_category: 3,
delete: 4
delete: 4,
reminder: 5
)
end

def self.public_types
@_public_types ||= types.except(:reminder)
end

def self.private_types
@_private_types ||= types.only(:reminder)
end

def self.ensure_consistency!
TopicTimer.where("topic_timers.execute_at < ?", Time.zone.now)
.find_each do |topic_timer|
Expand All @@ -59,6 +70,14 @@ def duration
end
end

def public_type?
!!self.class.public_types[self.status_type]
end

def private_type?
!!self.class.private_types[self.status_type]
end

private

def ensure_update_will_happen
Expand All @@ -82,6 +101,10 @@ def cancel_auto_delete_job
Jobs.cancel_scheduled_job(:delete_topic, topic_timer_id: id)
end

def cancel_auto_reminder_job
Jobs.cancel_scheduled_job(:topic_reminder, topic_timer_id: id)
end

def schedule_auto_open_job(time)
return unless topic
topic.update_status('closed', true, user) if !topic.closed
Expand Down Expand Up @@ -113,6 +136,10 @@ def publishing_to_category?
def schedule_auto_delete_job(time)
Jobs.enqueue_at(time, :delete_topic, topic_timer_id: id)
end

def schedule_auto_reminder_job(time)
Jobs.enqueue_at(time, :topic_reminder, topic_timer_id: id)
end
end

# == Schema Information
Expand Down
2 changes: 1 addition & 1 deletion app/serializers/topic_view_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ def include_tags?

def topic_timer
TopicTimerSerializer.new(
object.topic.topic_timer, root: false
object.topic.public_topic_timer, root: false
)
end

Expand Down
2 changes: 1 addition & 1 deletion app/services/topic_status_updater.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
def update!(status, enabled, opts={})
status = Status.new(status, enabled)

@topic_status_update = topic.topic_status_update
@topic_status_update = topic.public_topic_timer

updated = nil
Topic.transaction do
Expand Down
4 changes: 4 additions & 0 deletions config/locales/client.en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1240,6 +1240,7 @@ en:
moved_post: "<i title='moved post' class='fa fa-sign-out'></i><p><span>{{username}}</span> moved {{description}}</p>"
linked: "<i title='linked post' class='fa fa-link'></i><p><span>{{username}}</span> {{description}}</p>"
granted_badge: "<i title='badge granted' class='fa fa-certificate'></i><p>Earned '{{description}}'</p>"
topic_reminder: "<i title='topic reminder' class='fa fa-hand-o-right'></i><p><span>{{username}}</span> {{description}}</p>"

watching_first_post: "<i title='new topic' class='fa fa-dot-circle-o'></i><p><span>New Topic</span> {{description}}</p>"

Expand Down Expand Up @@ -1527,13 +1528,16 @@ en:
based_on_last_post: "Don't close until the last post in the topic is at least this old."
auto_delete:
title: "Auto-Delete Topic"
reminder:
title: "Remind Me"

status_update_notice:
auto_open: "This topic will automatically open %{timeLeft}."
auto_close: "This topic will automatically close %{timeLeft}."
auto_publish_to_category: "This topic will be published to <a href=%{categoryUrl}>#%{categoryName}</a> %{timeLeft}."
auto_close_based_on_last_post: "This topic will close %{duration} after the last reply."
auto_delete: "This topic will be automatically deleted %{timeLeft}."
auto_reminder: "You will be reminded about this topic %{timeLeft}."
auto_close_title: 'Auto-Close Settings'
auto_close_immediate:
one: "The last post in the topic is already 1 hour old, so the topic will be closed immediately."
Expand Down
27 changes: 27 additions & 0 deletions db/migrate/20170515203721_add_public_type_to_topic_timers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
class AddPublicTypeToTopicTimers < ActiveRecord::Migration
def up
add_column :topic_timers, :public_type, :boolean, default: true

execute("drop index idx_topic_id_status_type_deleted_at") rescue nil

# Only one public timer per topic (close, open, delete):
execute <<~SQL
CREATE UNIQUE INDEX idx_topic_id_public_type_deleted_at
ON topic_timers (topic_id)
WHERE public_type = TRUE
AND deleted_at IS NULL
SQL
end

def down
execute "DROP INDEX idx_topic_id_public_type_deleted_at"

execute <<~SQL
CREATE UNIQUE INDEX idx_topic_id_status_type_deleted_at
ON topic_timers (topic_id, status_type)
WHERE deleted_at IS NULL
SQL

remove_column :topic_timers, :public_type
end
end
2 changes: 1 addition & 1 deletion lib/post_creator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ def update_topic_stats
end

def update_topic_auto_close
topic_timer = @topic.topic_timer
topic_timer = @topic.public_topic_timer

if topic_timer &&
topic_timer.based_on_last_post &&
Expand Down
2 changes: 1 addition & 1 deletion spec/components/topic_creator_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
it "ignores auto_close_time without raising an error" do
topic = TopicCreator.create(user, Guardian.new(user), valid_attrs.merge(auto_close_time: '24'))
expect(topic).to be_valid
expect(topic.topic_status_update).to eq(nil)
expect(topic.public_topic_timer).to eq(nil)
end

it "category name is case insensitive" do
Expand Down
2 changes: 1 addition & 1 deletion spec/integration/managing_topic_status_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
status_type: TopicTimer.types[1]

expect(response).to be_success
expect(topic.reload.topic_status_update).to eq(nil)
expect(topic.reload.public_topic_timer).to eq(nil)

json = JSON.parse(response.body)

Expand Down
10 changes: 5 additions & 5 deletions spec/integration/topic_auto_close_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
let(:category) { nil }

it 'should not schedule the topic to auto-close' do
expect(topic.topic_status_update).to eq(nil)
expect(topic.public_topic_timer).to eq(nil)
expect(job_klass.jobs).to eq([])
end
end
Expand All @@ -25,7 +25,7 @@
let(:category) { Fabricate(:category, auto_close_hours: nil) }

it 'should not schedule the topic to auto-close' do
expect(topic.topic_status_update).to eq(nil)
expect(topic.public_topic_timer).to eq(nil)
expect(job_klass.jobs).to eq([])
end
end
Expand All @@ -49,11 +49,11 @@
topic_status_update = TopicTimer.last

expect(topic_status_update.topic).to eq(topic)
expect(topic.topic_status_update.execute_at).to be_within_one_second_of(2.hours.from_now)
expect(topic.public_topic_timer.execute_at).to be_within_one_second_of(2.hours.from_now)

args = job_klass.jobs.last['args'].first

expect(args["topic_timer_id"]).to eq(topic.topic_status_update.id)
expect(args["topic_timer_id"]).to eq(topic.public_topic_timer.id)
expect(args["state"]).to eq(true)
end

Expand All @@ -79,7 +79,7 @@
context 'topic is closed manually' do
it 'should remove the schedule to auto-close the topic' do
Timecop.freeze do
topic_timer_id = staff_topic.topic_timer.id
topic_timer_id = staff_topic.public_topic_timer.id

staff_topic.update_status('closed', true, admin)

Expand Down
10 changes: 5 additions & 5 deletions spec/jobs/delete_topic_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
first_post

Timecop.freeze(2.hours.from_now) do
described_class.new.execute(topic_timer_id: topic.topic_timer.id)
described_class.new.execute(topic_timer_id: topic.public_topic_timer.id)
expect(topic.reload).to be_trashed
expect(first_post.reload).to be_trashed
expect(topic.reload.topic_timer).to eq(nil)
expect(topic.reload.public_topic_timer).to eq(nil)
end
end

Expand All @@ -31,7 +31,7 @@
topic.trash!
Timecop.freeze(2.hours.from_now) do
Topic.any_instance.expects(:trash!).never
described_class.new.execute(topic_timer_id: topic.topic_timer.id)
described_class.new.execute(topic_timer_id: topic.public_topic_timer.id)
end
end

Expand All @@ -41,7 +41,7 @@
)
create_post(topic: t)
Timecop.freeze(4.hours.from_now) do
described_class.new.execute(topic_timer_id: t.topic_timer.id)
described_class.new.execute(topic_timer_id: t.public_topic_timer.id)
expect(t.reload).to_not be_trashed
end
end
Expand All @@ -56,7 +56,7 @@
it "shouldn't delete the topic" do
create_post(topic: topic)
Timecop.freeze(2.hours.from_now) do
described_class.new.execute(topic_timer_id: topic.topic_timer.id)
described_class.new.execute(topic_timer_id: topic.public_topic_timer.id)
expect(topic.reload).to_not be_trashed
end
end
Expand Down
8 changes: 4 additions & 4 deletions spec/jobs/publish_topic_to_category_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
Timecop.travel(1.hour.ago) { topic }
topic.trash!

described_class.new.execute(topic_timer_id: topic.topic_timer.id)
described_class.new.execute(topic_timer_id: topic.public_topic_timer.id)

topic.reload
expect(topic.category).to eq(category)
Expand All @@ -41,13 +41,13 @@
Timecop.travel(1.hour.ago) { topic.update!(visible: false) }

message = MessageBus.track_publish do
described_class.new.execute(topic_timer_id: topic.topic_timer.id)
described_class.new.execute(topic_timer_id: topic.public_topic_timer.id)
end.first

topic.reload
expect(topic.category).to eq(another_category)
expect(topic.visible).to eq(true)
expect(topic.topic_timer).to eq(nil)
expect(topic.public_topic_timer).to eq(nil)

%w{created_at bumped_at updated_at last_posted_at}.each do |attribute|
expect(topic.public_send(attribute)).to be_within(1.second).of(Time.zone.now)
Expand All @@ -68,7 +68,7 @@

it 'should publish the topic to the new category' do
message = MessageBus.track_publish do
described_class.new.execute(topic_timer_id: topic.topic_timer.id)
described_class.new.execute(topic_timer_id: topic.public_topic_timer.id)
end.last

topic.reload
Expand Down
Loading

0 comments on commit 7821400

Please sign in to comment.