@ -29,9 +29,10 @@ var CodeOceanEditor = {
|
|||||||
|
|
||||||
lastCopyText: null,
|
lastCopyText: null,
|
||||||
|
|
||||||
|
<% self.class.include Rails.application.routes.url_helpers %>
|
||||||
<% @config ||= CodeOcean::Config.new(:code_ocean).read(erb: false) %>
|
<% @config ||= CodeOcean::Config.new(:code_ocean).read(erb: false) %>
|
||||||
sendLearningAnalyticEvents: <%= @config['codeocean_events'] ? @config['codeocean_events']['enabled'] : false %>,
|
sendEvents: <%= @config['codeocean_events'] ? @config['codeocean_events']['enabled'] : false %>,
|
||||||
learningAnalyticsURL: "<%= @config['codeocean_events'] ? @config['codeocean_events']['url'] : "" %>",
|
eventURL: "<%= @config['codeocean_events'] ? events_path : '' %>",
|
||||||
|
|
||||||
|
|
||||||
configureEditors: function () {
|
configureEditors: function () {
|
||||||
@ -149,15 +150,15 @@ configureEditors: function () {
|
|||||||
},
|
},
|
||||||
|
|
||||||
handlePasteEvent: function (pasteObject) {
|
handlePasteEvent: function (pasteObject) {
|
||||||
var same = (this.lastCopyText === pasteObject.text);
|
var same = (CodeOceanEditor.lastCopyText === pasteObject.text);
|
||||||
|
|
||||||
// if the text is not copied from within the editor (from any file), send an event to lanalytics
|
// if the text is not copied from within the editor (from any file), send an event to the backend
|
||||||
if (!same) {
|
if (!same) {
|
||||||
this.publishCodeOceanEvent("codeocean_editor_paste", {
|
CodeOceanEditor.publishCodeOceanEvent({
|
||||||
text: pasteObject.text,
|
category: 'editor_paste',
|
||||||
codeocean_user_id: $('#editor').data('user-id'),
|
data: pasteObject.text,
|
||||||
exercise: $('#editor').data('exercise-id'),
|
exercise_id: $('#editor').data('exercise-id'),
|
||||||
file_id: "1"
|
file_id: $(this).data('file-id')
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -247,8 +248,8 @@ configureEditors: function () {
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// editor itself
|
// editor itself
|
||||||
editor.on("paste", this.handlePasteEvent.bind(this));
|
editor.on("paste", this.handlePasteEvent.bind(element));
|
||||||
editor.on("copy", this.handleCopyEvent.bind(this));
|
editor.on("copy", this.handleCopyEvent.bind(element));
|
||||||
|
|
||||||
// listener for autosave
|
// listener for autosave
|
||||||
session.on("change", function (deltaObject) {
|
session.on("change", function (deltaObject) {
|
||||||
@ -381,38 +382,18 @@ configureEditors: function () {
|
|||||||
//panel.find('.row .col-sm-9').eq(4).find('a').attr('href', '#output-' + index);
|
//panel.find('.row .col-sm-9').eq(4).find('a').attr('href', '#output-' + index);
|
||||||
},
|
},
|
||||||
|
|
||||||
// move URL to config file, only fire event if desired.
|
publishCodeOceanEvent: function (payload) {
|
||||||
publishCodeOceanEvent: function (eventName, contextData) {
|
if(this.sendEvents){
|
||||||
if(this.sendLearningAnalyticEvents){
|
$.ajax(this.eventURL, {
|
||||||
|
|
||||||
// enhance contextData hash with the user agent
|
|
||||||
contextData['user_agent'] = navigator.userAgent;
|
|
||||||
|
|
||||||
var payload = {
|
|
||||||
user: {
|
|
||||||
uuid: $('#editor').data('user-external-id')
|
|
||||||
},
|
|
||||||
verb: {
|
|
||||||
type: eventName
|
|
||||||
},
|
|
||||||
resource: {
|
|
||||||
type: 'page',
|
|
||||||
uuid: document.location.href
|
|
||||||
},
|
|
||||||
timestamp: new Date().toISOString(),
|
|
||||||
with_result: {},
|
|
||||||
in_context: contextData
|
|
||||||
};
|
|
||||||
|
|
||||||
$.ajax(this.learningAnalyticsURL, {
|
|
||||||
type: 'POST',
|
type: 'POST',
|
||||||
cache: false,
|
cache: false,
|
||||||
dataType: 'JSON',
|
dataType: 'JSON',
|
||||||
data: payload,
|
data: {
|
||||||
success: {},
|
event: payload
|
||||||
error: {}
|
},
|
||||||
})
|
success: _.noop,
|
||||||
|
error: _.noop
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
28
app/controllers/events_controller.rb
Normal file
28
app/controllers/events_controller.rb
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
class EventsController < ApplicationController
|
||||||
|
|
||||||
|
def authorize!
|
||||||
|
authorize(@event || @events)
|
||||||
|
end
|
||||||
|
private :authorize!
|
||||||
|
|
||||||
|
def create
|
||||||
|
@event = Event.new(event_params)
|
||||||
|
authorize!
|
||||||
|
respond_to do |format|
|
||||||
|
if @event.save
|
||||||
|
format.html { head :created }
|
||||||
|
format.json { head :created }
|
||||||
|
else
|
||||||
|
format.html { head :unprocessable_entity }
|
||||||
|
format.json { head :unprocessable_entity }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def event_params
|
||||||
|
params[:event]&.permit(:category, :data, :exercise_id, :file_id)
|
||||||
|
&.merge(user_id: current_user&.id, user_type: current_user&.class.name)
|
||||||
|
end
|
||||||
|
private :event_params
|
||||||
|
|
||||||
|
end
|
8
app/models/event.rb
Normal file
8
app/models/event.rb
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
class Event < ActiveRecord::Base
|
||||||
|
belongs_to :user, polymorphic: true
|
||||||
|
belongs_to :exercise
|
||||||
|
belongs_to :file
|
||||||
|
|
||||||
|
validates :category, presence: true
|
||||||
|
validates :data, presence: true
|
||||||
|
end
|
7
app/policies/event_policy.rb
Normal file
7
app/policies/event_policy.rb
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
class EventPolicy < AdminOnlyPolicy
|
||||||
|
|
||||||
|
def create?
|
||||||
|
everyone
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
@ -168,6 +168,8 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
resources :events, only: [:create]
|
||||||
|
|
||||||
post "/evaluate", to: 'remote_evaluation#evaluate', via: [:post]
|
post "/evaluate", to: 'remote_evaluation#evaluate', via: [:post]
|
||||||
|
|
||||||
end
|
end
|
||||||
|
12
db/migrate/20180814145059_create_events.rb
Normal file
12
db/migrate/20180814145059_create_events.rb
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
class CreateEvents < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
create_table :events do |t|
|
||||||
|
t.string :type
|
||||||
|
t.string :data
|
||||||
|
t.belongs_to :user, polymorphic: true, index: true
|
||||||
|
t.belongs_to :exercise, index: true
|
||||||
|
t.belongs_to :file, index: true
|
||||||
|
t.timestamps null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
@ -0,0 +1,5 @@
|
|||||||
|
class RenameEventsTypeToCategory < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
rename_column :events, :type, :category
|
||||||
|
end
|
||||||
|
end
|
7
db/migrate/20180815115351_remove_event_indices.rb
Normal file
7
db/migrate/20180815115351_remove_event_indices.rb
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
class RemoveEventIndices < ActiveRecord::Migration
|
||||||
|
def change
|
||||||
|
remove_index :events, [:user_type, :user_id]
|
||||||
|
remove_index :events, :exercise_id
|
||||||
|
remove_index :events, :file_id
|
||||||
|
end
|
||||||
|
end
|
13
db/schema.rb
13
db/schema.rb
@ -11,7 +11,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 20180703125302) do
|
ActiveRecord::Schema.define(version: 20180815115351) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "plpgsql"
|
enable_extension "plpgsql"
|
||||||
@ -95,6 +95,17 @@ ActiveRecord::Schema.define(version: 20180703125302) do
|
|||||||
|
|
||||||
add_index "errors", ["submission_id"], name: "index_errors_on_submission_id", using: :btree
|
add_index "errors", ["submission_id"], name: "index_errors_on_submission_id", using: :btree
|
||||||
|
|
||||||
|
create_table "events", force: :cascade do |t|
|
||||||
|
t.string "category"
|
||||||
|
t.string "data"
|
||||||
|
t.integer "user_id"
|
||||||
|
t.string "user_type"
|
||||||
|
t.integer "exercise_id"
|
||||||
|
t.integer "file_id"
|
||||||
|
t.datetime "created_at", null: false
|
||||||
|
t.datetime "updated_at", null: false
|
||||||
|
end
|
||||||
|
|
||||||
create_table "execution_environments", force: :cascade do |t|
|
create_table "execution_environments", force: :cascade do |t|
|
||||||
t.string "docker_image", limit: 255
|
t.string "docker_image", limit: 255
|
||||||
t.string "name", limit: 255
|
t.string "name", limit: 255
|
||||||
|
33
spec/controllers/events_controller_spec.rb
Normal file
33
spec/controllers/events_controller_spec.rb
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
|
||||||
|
describe EventsController do
|
||||||
|
let(:user) { FactoryBot.create(:admin) }
|
||||||
|
let(:exercise) {FactoryBot.create(:fibonacci)}
|
||||||
|
before(:each) { allow(controller).to receive(:current_user).and_return(user) }
|
||||||
|
|
||||||
|
describe 'POST #create' do
|
||||||
|
context 'with a valid event' do
|
||||||
|
let(:request) { proc { post :create, event: {category: 'foo', data: 'bar', exercise_id: exercise.id, file_id: exercise.files[0].id} } }
|
||||||
|
before(:each) { request.call }
|
||||||
|
|
||||||
|
expect_assigns(event: Event)
|
||||||
|
|
||||||
|
it 'creates the Event' do
|
||||||
|
expect { request.call }.to change(Event, :count).by(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
expect_status(201)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with an invalid event' do
|
||||||
|
before(:each) { post :create, event: {exercise_id: 847482} }
|
||||||
|
expect_assigns(event: Event)
|
||||||
|
expect_status(422)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with no event' do
|
||||||
|
before(:each) { post :create }
|
||||||
|
expect_status(422)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Reference in New Issue
Block a user