mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-27 09:36:19 -05:00
FEATURE: track statistics around post creation
- how long were people typing? - how long was composer open? - how many drafts were created? - correct, draft saved to go away after you continue typing store in Post.find(xyz).post_stat
This commit is contained in:
parent
5d40695908
commit
7b8b96446e
11 changed files with 148 additions and 12 deletions
|
@ -413,7 +413,7 @@ export default Ember.ObjectController.extend(Presence, {
|
|||
}
|
||||
|
||||
// we need a draft sequence for the composer to work
|
||||
if (opts.draftSequence === void 0) {
|
||||
if (opts.draftSequence === undefined) {
|
||||
return Discourse.Draft.get(opts.draftKey).then(function(data) {
|
||||
opts.draftSequence = data.draft_sequence;
|
||||
opts.draft = data.draft;
|
||||
|
|
|
@ -22,7 +22,9 @@ const CLOSED = 'closed',
|
|||
topic_id: 'topic.id',
|
||||
is_warning: 'isWarning',
|
||||
archetype: 'archetypeId',
|
||||
target_usernames: 'targetUsernames'
|
||||
target_usernames: 'targetUsernames',
|
||||
typing_duration_msecs: 'typingTime',
|
||||
composer_open_duration_msecs: 'composerTime'
|
||||
},
|
||||
|
||||
_edit_topic_serializer = {
|
||||
|
@ -52,6 +54,31 @@ const Composer = RestModel.extend({
|
|||
viewOpen: Em.computed.equal('composeState', OPEN),
|
||||
viewDraft: Em.computed.equal('composeState', DRAFT),
|
||||
|
||||
composeStateChanged: function() {
|
||||
var oldOpen = this.get('composerOpened');
|
||||
|
||||
if (this.get('composeState') === OPEN) {
|
||||
this.set('composerOpened', oldOpen || new Date());
|
||||
} else {
|
||||
if (oldOpen) {
|
||||
var oldTotal = this.get('composerTotalOpened') || 0;
|
||||
this.set('composerTotalOpened', oldTotal + (new Date() - oldOpen));
|
||||
}
|
||||
this.set('composerOpened', null);
|
||||
}
|
||||
}.observes('composeState'),
|
||||
|
||||
composerTime: function() {
|
||||
var total = this.get('composerTotalOpened') || 0;
|
||||
|
||||
var oldOpen = this.get('composerOpened');
|
||||
if (oldOpen) {
|
||||
total += (new Date() - oldOpen);
|
||||
}
|
||||
|
||||
return total;
|
||||
}.property().volatile(),
|
||||
|
||||
archetype: function() {
|
||||
return this.get('archetypes').findProperty('id', this.get('archetypeId'));
|
||||
}.property('archetypeId'),
|
||||
|
@ -60,6 +87,12 @@ const Composer = RestModel.extend({
|
|||
return this.set('metaData', Em.Object.create());
|
||||
}.observes('archetype'),
|
||||
|
||||
// view detected user is typing
|
||||
typing: _.throttle(function(){
|
||||
var typingTime = this.get("typingTime") || 0;
|
||||
this.set("typingTime", typingTime + 100);
|
||||
}, 100, {leading: false, trailing: true}),
|
||||
|
||||
editingFirstPost: Em.computed.and('editingPost', 'post.firstPost'),
|
||||
canEditTitle: Em.computed.or('creatingTopic', 'creatingPrivateMessage', 'editingFirstPost'),
|
||||
canCategorize: Em.computed.and('canEditTitle', 'notCreatingPrivateMessage'),
|
||||
|
@ -349,7 +382,9 @@ const Composer = RestModel.extend({
|
|||
composeState: opts.composerState || OPEN,
|
||||
action: opts.action,
|
||||
topic: opts.topic,
|
||||
targetUsernames: opts.usernames
|
||||
targetUsernames: opts.usernames,
|
||||
composerTotalOpened: opts.composerTime,
|
||||
typingTime: opts.typingTime
|
||||
});
|
||||
|
||||
if (opts.post) {
|
||||
|
@ -420,7 +455,10 @@ const Composer = RestModel.extend({
|
|||
post: null,
|
||||
title: null,
|
||||
editReason: null,
|
||||
stagedPost: false
|
||||
stagedPost: false,
|
||||
typingTime: 0,
|
||||
composerOpened: null,
|
||||
composerTotalOpened: 0
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -502,7 +540,9 @@ const Composer = RestModel.extend({
|
|||
admin: user.get('admin'),
|
||||
yours: true,
|
||||
read: true,
|
||||
wiki: false
|
||||
wiki: false,
|
||||
typingTime: this.get('typingTime'),
|
||||
composerTime: this.get('composerTime')
|
||||
});
|
||||
|
||||
this.serialize(_create_serializer, createdPost);
|
||||
|
@ -603,13 +643,20 @@ const Composer = RestModel.extend({
|
|||
postId: this.get('post.id'),
|
||||
archetypeId: this.get('archetypeId'),
|
||||
metaData: this.get('metaData'),
|
||||
usernames: this.get('targetUsernames')
|
||||
usernames: this.get('targetUsernames'),
|
||||
composerTime: this.get('composerTime'),
|
||||
typingTime: this.get('typingTime')
|
||||
};
|
||||
|
||||
this.set('draftStatus', I18n.t('composer.saving_draft_tip'));
|
||||
|
||||
const composer = this;
|
||||
|
||||
if (this._clearingStatus) {
|
||||
Em.run.cancel(this._clearingStatus);
|
||||
this._clearingStatus = null;
|
||||
}
|
||||
|
||||
// try to save the draft
|
||||
return Discourse.Draft.save(this.get('draftKey'), this.get('draftSequence'), data)
|
||||
.then(function() {
|
||||
|
@ -617,7 +664,20 @@ const Composer = RestModel.extend({
|
|||
}).catch(function() {
|
||||
composer.set('draftStatus', I18n.t('composer.drafts_offline'));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
dataChanged: function(){
|
||||
const draftStatus = this.get('draftStatus');
|
||||
const self = this;
|
||||
|
||||
if (draftStatus && !this._clearingStatus) {
|
||||
|
||||
this._clearingStatus = Em.run.later(this, function(){
|
||||
self.set('draftStatus', null);
|
||||
self._clearingStatus = null;
|
||||
}, 1000);
|
||||
}
|
||||
}.observes('title','reply')
|
||||
|
||||
});
|
||||
|
||||
|
@ -657,7 +717,9 @@ Composer.reopenClass({
|
|||
metaData: draft.metaData,
|
||||
usernames: draft.usernames,
|
||||
draft: true,
|
||||
composerState: DRAFT
|
||||
composerState: DRAFT,
|
||||
composerTime: draft.composerTime,
|
||||
typingTime: draft.typingTime
|
||||
});
|
||||
}
|
||||
},
|
||||
|
|
|
@ -85,6 +85,8 @@ const ComposerView = Discourse.View.extend(Ember.Evented, {
|
|||
const controller = this.get('controller');
|
||||
controller.checkReplyLength();
|
||||
|
||||
this.get('controller.model').typing();
|
||||
|
||||
const lastKeyUp = new Date();
|
||||
this.set('lastKeyUp', lastKeyUp);
|
||||
|
||||
|
|
|
@ -403,7 +403,6 @@ class PostsController < ApplicationController
|
|||
# Awful hack, but you can't seem to remove the `default_scope` when joining
|
||||
# So instead I grab the topics separately
|
||||
topic_ids = posts.dup.pluck(:topic_id)
|
||||
secured_category_ids = guardian.secure_category_ids
|
||||
topics = Topic.where(id: topic_ids).with_deleted.where.not(archetype: 'private_message')
|
||||
topics = topics.secured(guardian)
|
||||
|
||||
|
@ -422,7 +421,9 @@ class PostsController < ApplicationController
|
|||
:category,
|
||||
:target_usernames,
|
||||
:reply_to_post_number,
|
||||
:auto_track
|
||||
:auto_track,
|
||||
:typing_duration_msecs,
|
||||
:composer_open_duration_msecs
|
||||
]
|
||||
|
||||
# param munging for WordPress
|
||||
|
|
|
@ -7,7 +7,11 @@ class Draft < ActiveRecord::Base
|
|||
d = find_draft(user,key)
|
||||
if d
|
||||
return if d.sequence > sequence
|
||||
d.update_columns(data: data, sequence: sequence)
|
||||
exec_sql("UPDATE drafts
|
||||
SET data = :data,
|
||||
sequence = :sequence,
|
||||
revisions = revisions + 1
|
||||
WHERE id = :id", id: d.id, sequence: sequence, data: data)
|
||||
else
|
||||
Draft.create!(user_id: user.id, draft_key: key, data: data, sequence: sequence)
|
||||
end
|
||||
|
|
|
@ -37,6 +37,7 @@ class Post < ActiveRecord::Base
|
|||
has_many :uploads, through: :post_uploads
|
||||
|
||||
has_one :post_search_data
|
||||
has_one :post_stat
|
||||
|
||||
has_many :post_details
|
||||
|
||||
|
|
3
app/models/post_stat.rb
Normal file
3
app/models/post_stat.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
class PostStat < ActiveRecord::Base
|
||||
belongs_to :post
|
||||
end
|
16
db/migrate/20150802233112_add_post_stats.rb
Normal file
16
db/migrate/20150802233112_add_post_stats.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
class AddPostStats < ActiveRecord::Migration
|
||||
def change
|
||||
|
||||
add_column :drafts, :revisions, :int, null: false, default: 1
|
||||
|
||||
create_table :post_stats do |t|
|
||||
t.integer :post_id
|
||||
t.integer :drafts_saved
|
||||
t.integer :typing_duration_msecs
|
||||
t.integer :composer_open_duration_msecs
|
||||
t.timestamps
|
||||
end
|
||||
|
||||
add_index :post_stats, [:post_id]
|
||||
end
|
||||
end
|
|
@ -113,6 +113,7 @@ class PostCreator
|
|||
def create
|
||||
if valid?
|
||||
transaction do
|
||||
build_post_stats
|
||||
create_topic
|
||||
save_post
|
||||
extract_links
|
||||
|
@ -146,6 +147,14 @@ class PostCreator
|
|||
@post
|
||||
end
|
||||
|
||||
def self.track_post_stats
|
||||
Rails.env != "test".freeze || @track_post_stats
|
||||
end
|
||||
|
||||
def self.track_post_stats=(val)
|
||||
@track_post_stats = val
|
||||
end
|
||||
|
||||
def self.create(user, opts)
|
||||
PostCreator.new(user, opts).create
|
||||
end
|
||||
|
@ -172,6 +181,23 @@ class PostCreator
|
|||
|
||||
protected
|
||||
|
||||
def build_post_stats
|
||||
if PostCreator.track_post_stats
|
||||
draft_key = @topic ? "topic_#{@topic.id}" : "new_topic"
|
||||
|
||||
sequence = DraftSequence.current(@user, draft_key)
|
||||
revisions = Draft.where(sequence: sequence,
|
||||
user_id: @user.id,
|
||||
draft_key: draft_key).pluck(:revisions).first || 0
|
||||
|
||||
@post.build_post_stat(
|
||||
drafts_saved: revisions,
|
||||
typing_duration_msecs: @opts[:typing_duration_msecs] || 0,
|
||||
composer_open_duration_msecs: @opts[:composer_open_duration_msecs] || 0
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def trigger_after_events(post)
|
||||
DiscourseEvent.trigger(:topic_created, post.topic, @opts, @user) unless @opts[:topic_id]
|
||||
DiscourseEvent.trigger(:post_created, post, @opts, @user)
|
||||
|
|
|
@ -213,6 +213,21 @@ describe PostCreator do
|
|||
}.to_not change { topic.excerpt }
|
||||
end
|
||||
|
||||
it 'creates post stats' do
|
||||
|
||||
Draft.set(user, 'new_topic', 0, "test")
|
||||
Draft.set(user, 'new_topic', 0, "test1")
|
||||
|
||||
begin
|
||||
PostCreator.track_post_stats = true
|
||||
post = creator.create
|
||||
expect(post.post_stat.typing_duration_msecs).to eq(0)
|
||||
expect(post.post_stat.drafts_saved).to eq(2)
|
||||
ensure
|
||||
PostCreator.track_post_stats = false
|
||||
end
|
||||
end
|
||||
|
||||
describe "topic's auto close" do
|
||||
|
||||
it "doesn't update topic's auto close when it's not based on last post" do
|
||||
|
|
|
@ -109,6 +109,12 @@ describe Draft do
|
|||
expect(Draft.get(p.user, p.topic.draft_key, s)).to eq nil
|
||||
end
|
||||
|
||||
it 'increases the sequence number when a post is revised'
|
||||
it 'increases revision each time you set' do
|
||||
u = User.first
|
||||
Draft.set(u, 'new_topic', 0, 'hello')
|
||||
Draft.set(u, 'new_topic', 0, 'goodbye')
|
||||
|
||||
expect(Draft.find_draft(u, 'new_topic').revisions).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue