mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06: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:
@@ -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
|
||||
Reference in New Issue
Block a user