mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-23 23:58:31 -05:00
FEATURE: Allow admin to change timestamp of topic.
This commit is contained in:
parent
90388aa18e
commit
c7a21b7c23
11 changed files with 257 additions and 0 deletions
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
|
@ -476,6 +476,17 @@ Topic.reopenClass({
|
||||||
return promise;
|
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) {
|
bulkOperation(topics, operation) {
|
||||||
return Discourse.ajax("/topics/bulk", {
|
return Discourse.ajax("/topics/bulk", {
|
||||||
type: 'PUT',
|
type: 'PUT',
|
||||||
|
|
|
@ -60,6 +60,10 @@ const TopicRoute = Discourse.Route.extend({
|
||||||
this.controllerFor('modal').set('modalClass', 'edit-auto-close-modal');
|
this.controllerFor('modal').set('modalClass', 'edit-auto-close-modal');
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showChangeTimestamp() {
|
||||||
|
showModal('change-timestamp', { model: this.modelFor('topic'), title: 'topic.change_timestamp.title' });
|
||||||
|
},
|
||||||
|
|
||||||
showFeatureTopic() {
|
showFeatureTopic() {
|
||||||
showModal('featureTopic', { model: this.modelFor('topic'), title: 'topic.feature_topic.title' });
|
showModal('featureTopic', { model: this.modelFor('topic'), title: 'topic.feature_topic.title' });
|
||||||
this.controllerFor('modal').set('modalClass', 'feature-topic-modal');
|
this.controllerFor('modal').set('modalClass', 'feature-topic-modal');
|
||||||
|
|
|
@ -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>
|
|
@ -38,6 +38,10 @@
|
||||||
{{/if}}
|
{{/if}}
|
||||||
{{/unless}}
|
{{/unless}}
|
||||||
|
|
||||||
|
<li>
|
||||||
|
{{d-button action="showChangeTimestamp" icon="calendar" label="topic.change_timestamp.title" class="btn-admin"}}
|
||||||
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
{{#if model.archived}}
|
{{#if model.archived}}
|
||||||
{{d-button action="toggleArchived" icon="folder" label="topic.actions.unarchive" class="btn-admin"}}
|
{{d-button action="toggleArchived" icon="folder" label="topic.actions.unarchive" class="btn-admin"}}
|
||||||
|
|
|
@ -24,6 +24,7 @@ class TopicsController < ApplicationController
|
||||||
:bulk,
|
:bulk,
|
||||||
:reset_new,
|
:reset_new,
|
||||||
:change_post_owners,
|
:change_post_owners,
|
||||||
|
:change_timestamps,
|
||||||
:bookmark,
|
:bookmark,
|
||||||
:unsubscribe]
|
:unsubscribe]
|
||||||
|
|
||||||
|
@ -375,6 +376,22 @@ class TopicsController < ApplicationController
|
||||||
end
|
end
|
||||||
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
|
def clear_pin
|
||||||
topic = Topic.find_by(id: params[:topic_id].to_i)
|
topic = Topic.find_by(id: params[:topic_id].to_i)
|
||||||
guardian.ensure_can_see!(topic)
|
guardian.ensure_can_see!(topic)
|
||||||
|
|
43
app/services/post_timestamp_changer.rb
Normal file
43
app/services/post_timestamp_changer.rb
Normal 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
|
|
@ -1241,6 +1241,13 @@ en:
|
||||||
other: "Please choose the new owner of the {{count}} posts by <b>{{old_user}}</b>."
|
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."
|
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:
|
multi_select:
|
||||||
select: 'select'
|
select: 'select'
|
||||||
selected: 'selected ({{count}})'
|
selected: 'selected ({{count}})'
|
||||||
|
|
|
@ -466,6 +466,7 @@ Discourse::Application.routes.draw do
|
||||||
post "t/:topic_id/move-posts" => "topics#move_posts", constraints: {topic_id: /\d+/}
|
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/merge-topic" => "topics#merge_topic", constraints: {topic_id: /\d+/}
|
||||||
post "t/:topic_id/change-owner" => "topics#change_post_owners", 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+/}
|
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/bookmark" => "topics#bookmark", constraints: {topic_id: /\d+/}
|
||||||
put "t/:topic_id/remove_bookmarks" => "topics#remove_bookmarks", constraints: {topic_id: /\d+/}
|
put "t/:topic_id/remove_bookmarks" => "topics#remove_bookmarks", constraints: {topic_id: /\d+/}
|
||||||
|
|
|
@ -263,6 +263,45 @@ describe TopicsController do
|
||||||
end
|
end
|
||||||
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
|
context 'clear_pin' do
|
||||||
it 'needs you to be logged in' do
|
it 'needs you to be logged in' do
|
||||||
expect { xhr :put, :clear_pin, topic_id: 1 }.to raise_error(Discourse::NotLoggedIn)
|
expect { xhr :put, :clear_pin, topic_id: 1 }.to raise_error(Discourse::NotLoggedIn)
|
||||||
|
|
55
spec/services/post_timestamp_changer_spec.rb
Normal file
55
spec/services/post_timestamp_changer_spec.rb
Normal 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
|
Loading…
Reference in a new issue