From c4652d82e21a20355616ea35b5855a702734e298 Mon Sep 17 00:00:00 2001 From: Josh Callebaut Date: Mon, 8 Feb 2016 14:24:08 -0800 Subject: [PATCH] Implements the SPADE logger into the SpellView * Updates spade.js vendor file, adds a sublime-project for developers to use * Moves server logic away from handlers * Moves session update logic to middleware, sets up server schema to autorender IDs as ObjectIDs * Modernizes the supermodel loading scheme and switches from constructor to initalize --- app/collections/CodeLogs.coffee | 6 + app/core/Router.coffee | 1 + app/models/CodeLog.coffee | 6 + app/schemas/models/codelog.schema.coffee | 20 ++ app/schemas/models/level_session.coffee | 3 + app/schemas/subscriptions/play.coffee | 2 + app/styles/admin/codelogs-view.sass | 6 + app/templates/admin/codelogs-view.jade | 22 ++ app/views/admin/CodeLogs.coffee | 0 app/views/admin/CodeLogsView.coffee | 43 ++++ app/views/play/level/PlayLevelView.coffee | 1 + app/views/play/level/tome/SpellView.coffee | 44 +++- .../mongodb/queries/playtime-over-10.js | 47 ++++ server/middleware/auth.coffee | 8 +- server/middleware/codelogs.coffee | 22 ++ server/middleware/index.coffee | 1 + server/models/CodeLog.coffee | 34 +++ server/routes/index.coffee | 6 +- spec/server/functional/codelog.spec.coffee | 90 +++++++ sublime-project.json | 12 + vendor/scripts/spade.js | 236 ++++++++++++++++++ 21 files changed, 607 insertions(+), 3 deletions(-) create mode 100644 app/collections/CodeLogs.coffee create mode 100644 app/models/CodeLog.coffee create mode 100644 app/schemas/models/codelog.schema.coffee create mode 100644 app/styles/admin/codelogs-view.sass create mode 100644 app/templates/admin/codelogs-view.jade create mode 100644 app/views/admin/CodeLogs.coffee create mode 100644 app/views/admin/CodeLogsView.coffee create mode 100644 scripts/analytics/mongodb/queries/playtime-over-10.js create mode 100644 server/middleware/codelogs.coffee create mode 100644 server/models/CodeLog.coffee create mode 100644 spec/server/functional/codelog.spec.coffee create mode 100644 sublime-project.json create mode 100644 vendor/scripts/spade.js diff --git a/app/collections/CodeLogs.coffee b/app/collections/CodeLogs.coffee new file mode 100644 index 000000000..fa9d96f6d --- /dev/null +++ b/app/collections/CodeLogs.coffee @@ -0,0 +1,6 @@ +CocoCollection = require 'collections/CocoCollection' +CodeLog = require 'models/CodeLog' + +module.exports = class CodeLogCollection extends CocoCollection + url: '/db/codelogs' + model: CodeLog diff --git a/app/core/Router.coffee b/app/core/Router.coffee index 0fa7425f6..56ec24230 100644 --- a/app/core/Router.coffee +++ b/app/core/Router.coffee @@ -37,6 +37,7 @@ module.exports = class CocoRouter extends Backbone.Router 'admin/trial-requests': go('admin/TrialRequestsView') 'admin/user-code-problems': go('admin/UserCodeProblemsView') 'admin/pending-patches': go('admin/PendingPatchesView') + 'admin/codelogs': go('admin/CodeLogsView') 'beta': go('HomeView') diff --git a/app/models/CodeLog.coffee b/app/models/CodeLog.coffee new file mode 100644 index 000000000..6365546f6 --- /dev/null +++ b/app/models/CodeLog.coffee @@ -0,0 +1,6 @@ +CocoModel = require './CocoModel' + +module.exports = class CodeLog extends CocoModel + @className: 'CodeLog' + @schema: require 'schemas/models/codelog.schema' + urlRoot: '/db/codelogs' diff --git a/app/schemas/models/codelog.schema.coffee b/app/schemas/models/codelog.schema.coffee new file mode 100644 index 000000000..dbb4585cd --- /dev/null +++ b/app/schemas/models/codelog.schema.coffee @@ -0,0 +1,20 @@ +c = require './../schemas' + +LevelVersionSchema = c.object {required: ['original', 'majorVersion'], links: [{rel: 'db', href: '/db/level/{(original)}/version/{(majorVersion)}'}]}, + original: c.objectId() + majorVersion: + type: 'integer' + minimum: 0 + + +CodeLogSchema = + type: 'object' + properties: + sessionID: c.objectId() + level: LevelVersionSchema + levelSlug: {type:'string'} + userID: c.objectId() + log: {type:'string'} + created: c.date() + +module.exports = CodeLogSchema diff --git a/app/schemas/models/level_session.coffee b/app/schemas/models/level_session.coffee index 56fde06a8..61bf5c594 100644 --- a/app/schemas/models/level_session.coffee +++ b/app/schemas/models/level_session.coffee @@ -150,6 +150,9 @@ _.extend LevelSessionSchema.properties, type: 'string' format: 'code' + codeLogs: + type: 'array' + codeLanguage: type: 'string' diff --git a/app/schemas/subscriptions/play.coffee b/app/schemas/subscriptions/play.coffee index 303e0bd2a..1c4adc9b4 100644 --- a/app/schemas/subscriptions/play.coffee +++ b/app/schemas/subscriptions/play.coffee @@ -173,3 +173,5 @@ module.exports = 'level:subscription-required': c.object {} 'level:course-membership-required': c.object {} + + 'level:contact-button-pressed': c.object {title: 'Contact Pressed', description: 'Dispatched when the contact button is pressed in a level.'} diff --git a/app/styles/admin/codelogs-view.sass b/app/styles/admin/codelogs-view.sass new file mode 100644 index 000000000..f9e89db6f --- /dev/null +++ b/app/styles/admin/codelogs-view.sass @@ -0,0 +1,6 @@ +#codelogs-view + #codelogs-tooltip + z-index: 9999 + position: absolute + width: 512px + height: 512px diff --git a/app/templates/admin/codelogs-view.jade b/app/templates/admin/codelogs-view.jade new file mode 100644 index 000000000..affdd978c --- /dev/null +++ b/app/templates/admin/codelogs-view.jade @@ -0,0 +1,22 @@ +extends /templates/base + +block content + #codelogs-view + #codelogtable + table.table.table-striped + tr + th date + th userID + th levelSlug + th + if view.codelogs + for codelog in view.codelogs.models + +codeLogRow(codelog) + +mixin codeLogRow(codelog) + tr + td= codelog.get('created') + td= codelog.get('userID') + td= codelog.get('levelSlug') + td + button.button.playback(data-codelog=codelog.get('log')) Playback diff --git a/app/views/admin/CodeLogs.coffee b/app/views/admin/CodeLogs.coffee new file mode 100644 index 000000000..e69de29bb diff --git a/app/views/admin/CodeLogsView.coffee b/app/views/admin/CodeLogsView.coffee new file mode 100644 index 000000000..82ccfc5ac --- /dev/null +++ b/app/views/admin/CodeLogsView.coffee @@ -0,0 +1,43 @@ +RootView = require 'views/core/RootView' +template = require 'templates/admin/codelogs-view' +CodeLogCollection = require 'collections/CodeLogs' +CodeLog = require 'models/CodeLog' +utils = require 'core/utils' + +module.exports = class CodeLogsView extends RootView + template: template + id: 'codelogs-view' + tooltip: null + events: + 'click .playback': 'onClickPlayback' + + initialize: -> + @spade = new Spade() + @codelogs = new CodeLogCollection() + @supermodel.trackRequest(@codelogs.fetch()) + + onClickPlayback: (e) -> + @deleteTooltip() + events = LZString.decompressFromUTF16($(e.target).data('codelog')) + events = @spade.expand(JSON.parse(events)) + + @tooltip = $(document.createElement('textarea')) + @tooltip.attr('id', "codelogs-tooltip") + @tooltip.css({left: e.pageX + 20, top: e.pageY}) # Position near the cursor + @tooltip.blur @onBlurTooltip + @$('#codelogs-view').append @tooltip + @tooltip.focus() + @spade.play(events, @tooltip.context) + + deleteTooltip: -> + if @tooltip? + @tooltip.off 'blur' + @tooltip.remove() + @tooltip = null + + onBlurTooltip: (e) => + @deleteTooltip() + + destroy: -> + @deleteTooltip() + super() diff --git a/app/views/play/level/PlayLevelView.coffee b/app/views/play/level/PlayLevelView.coffee index b14d6146f..585c01747 100644 --- a/app/views/play/level/PlayLevelView.coffee +++ b/app/views/play/level/PlayLevelView.coffee @@ -593,6 +593,7 @@ module.exports = class PlayLevelView extends RootView session.save {screenshot: screenshot}, {patch: true, type: 'PUT'} onContactClicked: (e) -> + Backbone.Mediator.publish 'level:contact-button-pressed', {} @openModalView contactModal = new ContactModal levelID: @level.get('slug') or @level.id, courseID: @courseID, courseInstanceID: @courseInstanceID screenshot = @surface.screenshot(1, 'image/png', 1.0, 1) body = diff --git a/app/views/play/level/tome/SpellView.coffee b/app/views/play/level/tome/SpellView.coffee index 5582f4ede..7af9134eb 100644 --- a/app/views/play/level/tome/SpellView.coffee +++ b/app/views/play/level/tome/SpellView.coffee @@ -10,6 +10,7 @@ SpellToolbarView = require './SpellToolbarView' LevelComponent = require 'models/LevelComponent' UserCodeProblem = require 'models/UserCodeProblem' utils = require 'core/utils' +CodeLog = require 'models/CodeLog' module.exports = class SpellView extends CocoView id: 'spell-view' @@ -47,6 +48,8 @@ module.exports = class SpellView extends CocoView 'tome:maximize-toggled': 'onMaximizeToggled' 'script:state-changed': 'onScriptStateChange' 'playback:ended-changed': 'onPlaybackEndedChanged' + 'level:contact-button-pressed': 'onContactButtonPressed' + 'level:show-victory': 'onShowVictory' events: 'mouseout': 'onMouseOut' @@ -63,7 +66,6 @@ module.exports = class SpellView extends CocoView @highlightCurrentLine = _.throttle @highlightCurrentLine, 100 $(window).on 'resize', @onWindowResize @observing = @session.get('creator') isnt me.id - afterRender: -> super() @createACE() @@ -106,6 +108,14 @@ module.exports = class SpellView extends CocoView $(@ace.container).find('.ace_gutter').on 'click', @onGutterClick @initAutocomplete aceConfig.liveCompletion ? true + return if @session.get('creator') isnt me.id or @session.fake + # Create a Spade to 'dig' into Ace. + @spade = new Spade() + @spade.track(@ace) + # If a user is taking longer than 10 minutes, let's log it. + saveSpadeDelay = 10 * 60 * 1000 + @saveSpadeTimeout = setTimeout @saveSpade, saveSpadeDelay + createACEShortcuts: -> @aceCommands = aceCommands = [] ace = @ace @@ -636,6 +646,9 @@ module.exports = class SpellView extends CocoView onMouseOut: (e) -> @debugView?.onMouseOut e + onContactButtonPressed: (e) -> + @saveSpade() + getSource: -> @ace.getValue() # could also do @firepad.getText() @@ -705,6 +718,33 @@ module.exports = class SpellView extends CocoView return if @destroyed Backbone.Mediator.publish 'tome:hide-problem-alert', {} + saveSpade: => + return if @destroyed + spadeEvents = @spade.compile() + # Uncomment the below line for a debug panel to display inside the level + #@spade.debugPlay(spadeEvents) + condensedEvents = @spade.condense(spadeEvents) + + return unless condensedEvents.length + compressedEvents = LZString.compressToUTF16(JSON.stringify(condensedEvents)) + + codeLog = new CodeLog({ + sessionID: @options.session.id + level: + original: @options.level.get 'original' + majorVersion: (@options.level.get 'version').major + levelSlug: @options.level.get 'slug' + userID: @options.session.get 'creator' + log: compressedEvents + }) + + codeLog.save() + + onShowVictory: (e) -> + if @saveSpadeTimeout? + window.clearTimeout @saveSpadeTimeout + @saveSpadeTimeout = null + onManualCast: (e) -> cast = @$el.parent().length @recompile cast, e.realTime @@ -1299,6 +1339,8 @@ module.exports = class SpellView extends CocoView @toolbarView?.destroy() @zatanna.addSnippets [], @editorLang if @editorLang? $(window).off 'resize', @onWindowResize + window.clearTimeout @saveSpadeTimeout + @saveSpadeTimeout = null super() commentStarts = diff --git a/scripts/analytics/mongodb/queries/playtime-over-10.js b/scripts/analytics/mongodb/queries/playtime-over-10.js new file mode 100644 index 000000000..276152686 --- /dev/null +++ b/scripts/analytics/mongodb/queries/playtime-over-10.js @@ -0,0 +1,47 @@ +// Print out code language usage based on level session data + +// Usage: +// mongo
:/