mirror of
https://github.com/codeninjasllc/discourse.git
synced 2024-11-30 10:58:31 -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
|
// 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) {
|
return Discourse.Draft.get(opts.draftKey).then(function(data) {
|
||||||
opts.draftSequence = data.draft_sequence;
|
opts.draftSequence = data.draft_sequence;
|
||||||
opts.draft = data.draft;
|
opts.draft = data.draft;
|
||||||
|
|
|
@ -22,7 +22,9 @@ const CLOSED = 'closed',
|
||||||
topic_id: 'topic.id',
|
topic_id: 'topic.id',
|
||||||
is_warning: 'isWarning',
|
is_warning: 'isWarning',
|
||||||
archetype: 'archetypeId',
|
archetype: 'archetypeId',
|
||||||
target_usernames: 'targetUsernames'
|
target_usernames: 'targetUsernames',
|
||||||
|
typing_duration_msecs: 'typingTime',
|
||||||
|
composer_open_duration_msecs: 'composerTime'
|
||||||
},
|
},
|
||||||
|
|
||||||
_edit_topic_serializer = {
|
_edit_topic_serializer = {
|
||||||
|
@ -52,6 +54,31 @@ const Composer = RestModel.extend({
|
||||||
viewOpen: Em.computed.equal('composeState', OPEN),
|
viewOpen: Em.computed.equal('composeState', OPEN),
|
||||||
viewDraft: Em.computed.equal('composeState', DRAFT),
|
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() {
|
archetype: function() {
|
||||||
return this.get('archetypes').findProperty('id', this.get('archetypeId'));
|
return this.get('archetypes').findProperty('id', this.get('archetypeId'));
|
||||||
}.property('archetypeId'),
|
}.property('archetypeId'),
|
||||||
|
@ -60,6 +87,12 @@ const Composer = RestModel.extend({
|
||||||
return this.set('metaData', Em.Object.create());
|
return this.set('metaData', Em.Object.create());
|
||||||
}.observes('archetype'),
|
}.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'),
|
editingFirstPost: Em.computed.and('editingPost', 'post.firstPost'),
|
||||||
canEditTitle: Em.computed.or('creatingTopic', 'creatingPrivateMessage', 'editingFirstPost'),
|
canEditTitle: Em.computed.or('creatingTopic', 'creatingPrivateMessage', 'editingFirstPost'),
|
||||||
canCategorize: Em.computed.and('canEditTitle', 'notCreatingPrivateMessage'),
|
canCategorize: Em.computed.and('canEditTitle', 'notCreatingPrivateMessage'),
|
||||||
|
@ -349,7 +382,9 @@ const Composer = RestModel.extend({
|
||||||
composeState: opts.composerState || OPEN,
|
composeState: opts.composerState || OPEN,
|
||||||
action: opts.action,
|
action: opts.action,
|
||||||
topic: opts.topic,
|
topic: opts.topic,
|
||||||
targetUsernames: opts.usernames
|
targetUsernames: opts.usernames,
|
||||||
|
composerTotalOpened: opts.composerTime,
|
||||||
|
typingTime: opts.typingTime
|
||||||
});
|
});
|
||||||
|
|
||||||
if (opts.post) {
|
if (opts.post) {
|
||||||
|
@ -420,7 +455,10 @@ const Composer = RestModel.extend({
|
||||||
post: null,
|
post: null,
|
||||||
title: null,
|
title: null,
|
||||||
editReason: 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'),
|
admin: user.get('admin'),
|
||||||
yours: true,
|
yours: true,
|
||||||
read: true,
|
read: true,
|
||||||
wiki: false
|
wiki: false,
|
||||||
|
typingTime: this.get('typingTime'),
|
||||||
|
composerTime: this.get('composerTime')
|
||||||
});
|
});
|
||||||
|
|
||||||
this.serialize(_create_serializer, createdPost);
|
this.serialize(_create_serializer, createdPost);
|
||||||
|
@ -603,13 +643,20 @@ const Composer = RestModel.extend({
|
||||||
postId: this.get('post.id'),
|
postId: this.get('post.id'),
|
||||||
archetypeId: this.get('archetypeId'),
|
archetypeId: this.get('archetypeId'),
|
||||||
metaData: this.get('metaData'),
|
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'));
|
this.set('draftStatus', I18n.t('composer.saving_draft_tip'));
|
||||||
|
|
||||||
const composer = this;
|
const composer = this;
|
||||||
|
|
||||||
|
if (this._clearingStatus) {
|
||||||
|
Em.run.cancel(this._clearingStatus);
|
||||||
|
this._clearingStatus = null;
|
||||||
|
}
|
||||||
|
|
||||||
// try to save the draft
|
// try to save the draft
|
||||||
return Discourse.Draft.save(this.get('draftKey'), this.get('draftSequence'), data)
|
return Discourse.Draft.save(this.get('draftKey'), this.get('draftSequence'), data)
|
||||||
.then(function() {
|
.then(function() {
|
||||||
|
@ -617,7 +664,20 @@ const Composer = RestModel.extend({
|
||||||
}).catch(function() {
|
}).catch(function() {
|
||||||
composer.set('draftStatus', I18n.t('composer.drafts_offline'));
|
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,
|
metaData: draft.metaData,
|
||||||
usernames: draft.usernames,
|
usernames: draft.usernames,
|
||||||
draft: true,
|
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');
|
const controller = this.get('controller');
|
||||||
controller.checkReplyLength();
|
controller.checkReplyLength();
|
||||||
|
|
||||||
|
this.get('controller.model').typing();
|
||||||
|
|
||||||
const lastKeyUp = new Date();
|
const lastKeyUp = new Date();
|
||||||
this.set('lastKeyUp', lastKeyUp);
|
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
|
# Awful hack, but you can't seem to remove the `default_scope` when joining
|
||||||
# So instead I grab the topics separately
|
# So instead I grab the topics separately
|
||||||
topic_ids = posts.dup.pluck(:topic_id)
|
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 = Topic.where(id: topic_ids).with_deleted.where.not(archetype: 'private_message')
|
||||||
topics = topics.secured(guardian)
|
topics = topics.secured(guardian)
|
||||||
|
|
||||||
|
@ -422,7 +421,9 @@ class PostsController < ApplicationController
|
||||||
:category,
|
:category,
|
||||||
:target_usernames,
|
:target_usernames,
|
||||||
:reply_to_post_number,
|
:reply_to_post_number,
|
||||||
:auto_track
|
:auto_track,
|
||||||
|
:typing_duration_msecs,
|
||||||
|
:composer_open_duration_msecs
|
||||||
]
|
]
|
||||||
|
|
||||||
# param munging for WordPress
|
# param munging for WordPress
|
||||||
|
|
|
@ -7,7 +7,11 @@ class Draft < ActiveRecord::Base
|
||||||
d = find_draft(user,key)
|
d = find_draft(user,key)
|
||||||
if d
|
if d
|
||||||
return if d.sequence > sequence
|
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
|
else
|
||||||
Draft.create!(user_id: user.id, draft_key: key, data: data, sequence: sequence)
|
Draft.create!(user_id: user.id, draft_key: key, data: data, sequence: sequence)
|
||||||
end
|
end
|
||||||
|
|
|
@ -37,6 +37,7 @@ class Post < ActiveRecord::Base
|
||||||
has_many :uploads, through: :post_uploads
|
has_many :uploads, through: :post_uploads
|
||||||
|
|
||||||
has_one :post_search_data
|
has_one :post_search_data
|
||||||
|
has_one :post_stat
|
||||||
|
|
||||||
has_many :post_details
|
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
|
def create
|
||||||
if valid?
|
if valid?
|
||||||
transaction do
|
transaction do
|
||||||
|
build_post_stats
|
||||||
create_topic
|
create_topic
|
||||||
save_post
|
save_post
|
||||||
extract_links
|
extract_links
|
||||||
|
@ -146,6 +147,14 @@ class PostCreator
|
||||||
@post
|
@post
|
||||||
end
|
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)
|
def self.create(user, opts)
|
||||||
PostCreator.new(user, opts).create
|
PostCreator.new(user, opts).create
|
||||||
end
|
end
|
||||||
|
@ -172,6 +181,23 @@ class PostCreator
|
||||||
|
|
||||||
protected
|
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)
|
def trigger_after_events(post)
|
||||||
DiscourseEvent.trigger(:topic_created, post.topic, @opts, @user) unless @opts[:topic_id]
|
DiscourseEvent.trigger(:topic_created, post.topic, @opts, @user) unless @opts[:topic_id]
|
||||||
DiscourseEvent.trigger(:post_created, post, @opts, @user)
|
DiscourseEvent.trigger(:post_created, post, @opts, @user)
|
||||||
|
|
|
@ -213,6 +213,21 @@ describe PostCreator do
|
||||||
}.to_not change { topic.excerpt }
|
}.to_not change { topic.excerpt }
|
||||||
end
|
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
|
describe "topic's auto close" do
|
||||||
|
|
||||||
it "doesn't update topic's auto close when it's not based on last post" 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
|
expect(Draft.get(p.user, p.topic.draft_key, s)).to eq nil
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue