Merge pull request #3678 from tgxworld/allow_admin_to_change_timestamp

FEATURE: Allow admin to change timestamp of topic.
This commit is contained in:
Sam 2015-08-21 10:34:37 +10:00
commit 2b9ca0de8b
11 changed files with 257 additions and 0 deletions

View file

@ -0,0 +1,58 @@
import ModalFunctionality from 'discourse/mixins/modal-functionality';
import computed from 'ember-addons/ember-computed-decorators';
// Modal related to changing the timestamp of posts
export default Ember.Controller.extend(ModalFunctionality, {
needs: ['topic'],
topicController: Em.computed.alias('controllers.topic'),
saving: false,
date: '',
time: '',
@computed('saving')
buttonTitle(saving) {
return saving ? I18n.t('saving') : I18n.t('topic.change_timestamp.action');
},
@computed('date', 'time')
createdAt(date, time) {
return moment(date + ' ' + time, 'YYYY-MM-DD HH:mm:ss');
},
@computed('createdAt')
validTimestamp(createdAt) {
return moment().diff(createdAt, 'minutes') < 0;
},
@computed('saving', 'date', 'validTimestamp')
buttonDisabled() {
if (this.get('saving') || this.get('validTimestamp')) return true;
return Ember.isEmpty(this.get('date'));
},
onShow: function() {
this.setProperties({
date: moment().format('YYYY-MM-DD')
});
},
actions: {
changeTimestamp: function() {
this.set('saving', true);
const self = this;
Discourse.Topic.changeTimestamp(
this.get('topicController.model.id'),
this.get('createdAt').unix()
).then(function() {
self.send('closeModal');
self.setProperties({ date: '', time: '', saving: false });
}).catch(function() {
self.flash(I18n.t('topic.change_timestamp.error'), 'alert-error');
self.set('saving', false);
});
return false;
}
}
});

View file

@ -480,6 +480,17 @@ Topic.reopenClass({
return promise;
},
changeTimestamp(topicId, timestamp) {
const promise = Discourse.ajax("/t/" + topicId + '/change-timestamp', {
type: 'PUT',
data: { timestamp: timestamp },
}).then(function(result) {
if (result.success) return result;
promise.reject(new Error("error updating timestamp of topic"));
});
return promise;
},
bulkOperation(topics, operation) {
return Discourse.ajax("/topics/bulk", {
type: 'PUT',

View file

@ -60,6 +60,10 @@ const TopicRoute = Discourse.Route.extend({
this.controllerFor('modal').set('modalClass', 'edit-auto-close-modal');
},
showChangeTimestamp() {
showModal('change-timestamp', { model: this.modelFor('topic'), title: 'topic.change_timestamp.title' });
},
showFeatureTopic() {
showModal('featureTopic', { model: this.modelFor('topic'), title: 'topic.feature_topic.title' });
this.controllerFor('modal').set('modalClass', 'feature-topic-modal');

View file

@ -0,0 +1,18 @@
<div class="modal-body">
<p>
{{i18n 'topic.change_timestamp.instructions'}}
</p>
<p {{bind-attr class=":alert :alert-error validTimestamp::hidden"}}>
{{i18n 'topic.change_timestamp.invalid_timestamp'}}
</p>
<form>
{{input type="date" value=date}}
{{input type="time" value=time}}
</form>
</div>
<div class="modal-footer">
<button class="btn btn-primary" {{bind-attr disabled="buttonDisabled"}} {{action "changeTimestamp"}}>{{buttonTitle}}</button>
</div>

View file

@ -38,6 +38,10 @@
{{/if}}
{{/unless}}
<li>
{{d-button action="showChangeTimestamp" icon="calendar" label="topic.change_timestamp.title" class="btn-admin"}}
</li>
<li>
{{#if model.archived}}
{{d-button action="toggleArchived" icon="folder" label="topic.actions.unarchive" class="btn-admin"}}

View file

@ -24,6 +24,7 @@ class TopicsController < ApplicationController
:bulk,
:reset_new,
:change_post_owners,
:change_timestamps,
:bookmark,
:unsubscribe]
@ -375,6 +376,22 @@ class TopicsController < ApplicationController
end
end
def change_timestamps
params.require(:topic_id)
params.require(:timestamp)
guardian.ensure_can_change_post_owner!
begin
PostTimestampChanger.new( topic_id: params[:topic_id].to_i,
timestamp: params[:timestamp].to_i ).change!
render json: success_json
rescue ActiveRecord::RecordInvalid
render json: failed_json, status: 422
end
end
def clear_pin
topic = Topic.find_by(id: params[:topic_id].to_i)
guardian.ensure_can_see!(topic)

View file

@ -0,0 +1,43 @@
class PostTimestampChanger
def initialize(params)
@topic = Topic.with_deleted.find(params[:topic_id])
@posts = @topic.posts
@timestamp = Time.at(params[:timestamp])
@time_difference = calculate_time_difference
end
def change!
ActiveRecord::Base.transaction do
update_topic
@posts.each do |post|
if post.is_first_post?
update_post(post, @timestamp)
else
update_post(post, Time.at(post.created_at.to_f + @time_difference))
end
end
end
# Burst the cache for stats
[AdminDashboardData, About].each { |klass| $redis.del klass.stats_cache_key }
end
private
def calculate_time_difference
@timestamp - @topic.created_at
end
def update_topic
@topic.update_attributes(
created_at: @timestamp,
updated_at: @timestamp,
bumped_at: @timestamp
)
end
def update_post(post, timestamp)
post.update_attributes(created_at: timestamp, updated_at: timestamp)
end
end

View file

@ -1241,6 +1241,13 @@ en:
other: "Please choose the new owner of the {{count}} posts by <b>{{old_user}}</b>."
instructions_warn: "Note that any notifications about this post will not be transferred to the new user retroactively.<br>Warning: Currently, no post-dependent data is transferred over to the new user. Use with caution."
change_timestamp:
title: "Change Timestamp"
action: "change timestamp"
invalid_timestamp: "Timestamp cannot be in the future."
error: "There was an error changing the timestamp of the topic."
instructions: "Please select the new timestamp of the topic. Posts in the topic will be updated to have the same time difference."
multi_select:
select: 'select'
selected: 'selected ({{count}})'

View file

@ -469,6 +469,7 @@ Discourse::Application.routes.draw do
post "t/:topic_id/move-posts" => "topics#move_posts", constraints: {topic_id: /\d+/}
post "t/:topic_id/merge-topic" => "topics#merge_topic", constraints: {topic_id: /\d+/}
post "t/:topic_id/change-owner" => "topics#change_post_owners", constraints: {topic_id: /\d+/}
put "t/:topic_id/change-timestamp" => "topics#change_timestamps", constraints: {topic_id: /\d+/}
delete "t/:topic_id/timings" => "topics#destroy_timings", constraints: {topic_id: /\d+/}
put "t/:topic_id/bookmark" => "topics#bookmark", constraints: {topic_id: /\d+/}
put "t/:topic_id/remove_bookmarks" => "topics#remove_bookmarks", constraints: {topic_id: /\d+/}

View file

@ -263,6 +263,45 @@ describe TopicsController do
end
end
context 'change_timestamps' do
let(:params) { { topic_id: 1, timestamp: Time.zone.now } }
it 'needs you to be logged in' do
expect { xhr :put, :change_timestamps, params }.to raise_error(Discourse::NotLoggedIn)
end
[:moderator, :trust_level_4].each do |user|
describe "forbidden to #{user}" do
let!(user) { log_in(user) }
it 'correctly denies' do
xhr :put, :change_timestamps, params
expect(response).to be_forbidden
end
end
end
describe 'changing timestamps' do
let!(:admin) { log_in(:admin) }
let(:old_timestamp) { Time.zone.now }
let(:new_timestamp) { old_timestamp - 1.day }
let!(:topic) { Fabricate(:topic, created_at: old_timestamp) }
let!(:p1) { Fabricate(:post, topic_id: topic.id, created_at: old_timestamp) }
let!(:p2) { Fabricate(:post, topic_id: topic.id, created_at: old_timestamp + 1.day) }
it 'raises an error with a missing parameter' do
expect { xhr :put, :change_timestamps, topic_id: 1 }.to raise_error(ActionController::ParameterMissing)
end
it 'should update the timestamps of selected posts' do
xhr :put, :change_timestamps, topic_id: topic.id, timestamp: new_timestamp.to_f
expect(topic.reload.created_at.to_s).to eq(new_timestamp.to_s)
expect(p1.reload.created_at.to_s).to eq(new_timestamp.to_s)
expect(p2.reload.created_at.to_s).to eq(old_timestamp.to_s)
end
end
end
context 'clear_pin' do
it 'needs you to be logged in' do
expect { xhr :put, :clear_pin, topic_id: 1 }.to raise_error(Discourse::NotLoggedIn)

View file

@ -0,0 +1,55 @@
require 'spec_helper'
describe PostTimestampChanger do
describe "change!" do
let(:old_timestamp) { Time.zone.now }
let(:new_timestamp) { old_timestamp + 1.day }
let!(:topic) { Fabricate(:topic, created_at: old_timestamp) }
let!(:p1) { Fabricate(:post, topic: topic, created_at: old_timestamp) }
let!(:p2) { Fabricate(:post, topic: topic, created_at: old_timestamp + 1.day) }
let(:params) { { topic_id: topic.id, timestamp: new_timestamp.to_f } }
it 'changes the timestamp of the topic and opening post' do
PostTimestampChanger.new(params).change!
topic.reload
[:created_at, :updated_at, :bumped_at].each do |column|
expect(topic.public_send(column).to_s).to eq(new_timestamp.to_s)
end
p1.reload
[:created_at, :updated_at].each do |column|
expect(p1.public_send(column).to_s).to eq(new_timestamp.to_s)
end
end
describe 'predated timestamp' do
it 'updates the timestamp of posts in the topic with the time difference applied' do
PostTimestampChanger.new(params).change!
p2.reload
[:created_at, :updated_at].each do |column|
expect(p2.public_send(column).to_s).to eq((old_timestamp + 2.day).to_s)
end
end
end
describe 'backdated timestamp' do
let(:new_timestamp) { old_timestamp - 1.day }
it 'updates the timestamp of posts in the topic with the time difference applied' do
PostTimestampChanger.new(params).change!
p2.reload
[:created_at, :updated_at].each do |column|
expect(p2.public_send(column).to_s).to eq((old_timestamp).to_s)
end
end
end
it 'deletes the stats cache' do
$redis.expects(:del).twice
PostTimestampChanger.new(params).change!
end
end
end