diff --git a/Gemfile b/Gemfile index 175813183272eb4f2579ec17e2efc59ab69a80e4..2f9c1e56f5d9b6b9f20048faa6243fcb16f47547 100644 --- a/Gemfile +++ b/Gemfile @@ -25,6 +25,7 @@ gem 'jbuilder', '~> 2.7' # gem 'bcrypt', '~> 3.1.7' gem 'rsolr' +gem 'sidekiq', '~> 6.0' # Use Active Storage variant # gem 'image_processing', '~> 1.2' diff --git a/Gemfile.lock b/Gemfile.lock index 20736405122386c0f4f8f8dbb5c37db1172ce373..104aa984d89fc4d942b5da6d3bfe24e96804d9a9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -78,6 +78,7 @@ GEM xpath (~> 3.2) childprocess (3.0.0) concurrent-ruby (1.1.9) + connection_pool (2.2.5) crass (1.0.6) devise (4.8.0) bcrypt (~> 3.0) @@ -166,6 +167,7 @@ GEM rb-fsevent (0.11.0) rb-inotify (0.10.1) ffi (~> 1.0) + redis (4.4.0) regexp_parser (2.1.1) responders (3.0.1) actionpack (>= 5.0) @@ -189,6 +191,10 @@ GEM childprocess (>= 0.5, < 4.0) rubyzip (>= 1.2.2) semantic_range (3.0.0) + sidekiq (6.2.2) + connection_pool (>= 2.2.2) + rack (~> 2.0) + redis (>= 4.2.0) spring (2.1.1) sprockets (4.0.2) concurrent-ruby (~> 1.0) @@ -244,6 +250,7 @@ DEPENDENCIES rsolr sass-rails (>= 6) selenium-webdriver + sidekiq (~> 6.0) spring turbolinks (~> 5) tzinfo-data diff --git a/README.md b/README.md index 27891ff73bc7ad68d8398b76180f30fa914b4f0e..65efc25ba5cb054d3925af5d6d94e6c417377f89 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,18 @@ Create a docker container and create an empty database named "newspapers" `docker exec -it newspapers_platform_database createdb -U postgres newspapers` -Modify the content of `config/database.yml` accordingly. +Modify the content of `config/database.yml` according to your configuration. + +## Setting up Redis +Used by Sidekiq and Rails + +`docker run --name newspapers_redis -p 127.0.0.1:6379:6379 -d redis` ## Setting up a IIIF server -Cantaloupe \ No newline at end of file +Cantaloupe + + +## Starting the server +`bundle exec sidekiq` + +`rails s` diff --git a/app/channels/application_cable/connection.rb b/app/channels/application_cable/connection.rb index 0ff5442f476f98d578f77221b57164cffcf08de0..6b5519ae2d542f3f6c6bde5df2eda21f4e02efdc 100644 --- a/app/channels/application_cable/connection.rb +++ b/app/channels/application_cable/connection.rb @@ -1,4 +1,19 @@ module ApplicationCable class Connection < ActionCable::Connection::Base + identified_by :current_user + + def connect + self.current_user = find_user + end + + def find_user + user_id = cookies.signed["user.id"] + current_user = User.find_by(id: user_id) + if current_user + current_user + else + reject_unauthorized_connection + end + end end end diff --git a/app/channels/notification_channel.rb b/app/channels/notification_channel.rb new file mode 100644 index 0000000000000000000000000000000000000000..206652aa51701dc3b3588ded3fa025d14d762852 --- /dev/null +++ b/app/channels/notification_channel.rb @@ -0,0 +1,9 @@ +class NotificationChannel < ApplicationCable::Channel + def subscribed + stream_from "notifications.#{current_user.id}" + end + + def unsubscribed + # Any cleanup needed when channel is unsubscribed + end +end diff --git a/app/controllers/experiment_controller.rb b/app/controllers/experiment_controller.rb index ed3635db4716eb8acdd0da77467845918e148f79..805530491b6208a5dccabe1df3d07ac61b1052de 100644 --- a/app/controllers/experiment_controller.rb +++ b/app/controllers/experiment_controller.rb @@ -5,19 +5,70 @@ class ExperimentController < ApplicationController def index end + def create + experiment = Experiment.new + experiment.user = current_user + experiment.title = params[:title] + begin + experiment.save! + render json: {status: 'ok'} + rescue ActiveRecord::RecordNotUnique + render json: {status: "error", message: "An experiment with this title already exists."} + rescue ActiveRecord::RecordInvalid + render json: {status: "error", message: "The title should not be blank."} + end + end + def show @experiment = Experiment.find params[:id] + # TODO: I'm not satisfied with the following @tools_description = File.read("#{Rails.root}/public/newspapers_tools.json") end - def save - experiment = Experiment.find(params[:id]) - experiment.description = JSON.parse(params[:description]) - experiment.save + def update_experiments_list + respond_to do |format| + format.js + end + end + + def add_tool + @experiment = Experiment.find(params[:id]) + tool_params = JSON.parse params[:tool] + tool = Tool.new + tool.tool_type = tool_params['type'] + tool.parameters = tool_params['parameters'] + tool.status = "created" + tool.experiment = @experiment + tool.save! + @experiment.add_tool(params[:parent_id].to_i, tool) + @experiment.save! + render 'experiment/update_experiment_area' + end + + def delete_tool + @experiment = Experiment.find(params[:id]) + tools_to_destroy_ids = @experiment.delete_tool(params[:tool_id].to_i) + @experiment.save! + Tool.destroy(tools_to_destroy_ids) + render 'experiment/update_experiment_area' + end + + def edit_tool_form + @experiment = Experiment.find(params[:id]) + @tool = Tool.find(params[:tool_id]) + render partial: 'tool/parameters', locals: {tool: @tool} end - def load - experiment = Experiment.find(params[:id]) - render json: experiment.description.to_json + def edit_tool + @experiment = Experiment.find(params[:id]) + @tool = Tool.find(params[:tool_id]) + @tool.parameters.map! do |param| + param['value'] = params[:parameters][param['name']] + param + end + @tool.save! + @experiment.update_tool(params[:tool_id].to_i, params[:parameters]) + @experiment.save! + render json: {} end end diff --git a/app/controllers/tool_controller.rb b/app/controllers/tool_controller.rb new file mode 100644 index 0000000000000000000000000000000000000000..d9fbf32fe8fd70f9fdd4e0191763b1bc5e9a62ff --- /dev/null +++ b/app/controllers/tool_controller.rb @@ -0,0 +1,26 @@ +class ToolController < ApplicationController + + before_action :authenticate_user! + + def show + + end + + def create + + end + + def update + + end + + def destroy + + end + + private + + def tool_params + params.require(:tool).permit(:parameters, :results, :status) + end +end diff --git a/app/helpers/experiment_helper.rb b/app/helpers/experiment_helper.rb new file mode 100644 index 0000000000000000000000000000000000000000..352a16d53f8dd0b97220d6d2af48ea32577d0cb7 --- /dev/null +++ b/app/helpers/experiment_helper.rb @@ -0,0 +1,17 @@ +module ExperimentHelper + + def recursive_display(tree) + if tree.has_key? "tool" + concat "
  • ".html_safe + concat render partial: 'tool/canvas_tool', locals: {tool: tree['tool']} + concat "".html_safe + concat "
  • ".html_safe if tree.has_key? "tool" + end + +end \ No newline at end of file diff --git a/app/javascript/channels/notification_channel.js b/app/javascript/channels/notification_channel.js new file mode 100644 index 0000000000000000000000000000000000000000..8855245cd7446f535059362920d2e4a1fefcd8c4 --- /dev/null +++ b/app/javascript/channels/notification_channel.js @@ -0,0 +1,17 @@ +import consumer from "./consumer" + +consumer.subscriptions.create("NotificationChannel", { + connected() { + // Called when the subscription is ready for use on the server + console.log("connected") + }, + + disconnected() { + // Called when the subscription has been terminated by the server + }, + + received(data) { + // Called when there's incoming data on the websocket for this channel + console.log("received: ", data) + } +}); diff --git a/app/javascript/packs/controllers/datasets_controller.js b/app/javascript/packs/controllers/datasets_controller.js index 40abca336e0fcaeebbbda19100a902055656e333..76f28175a1f9ee9bbbb3fdf91d16caf7746438ae 100644 --- a/app/javascript/packs/controllers/datasets_controller.js +++ b/app/javascript/packs/controllers/datasets_controller.js @@ -16,6 +16,8 @@ export default class extends Controller { $("#dataset-title").val("") modalButton.innerHTML = "Create" modalButton.removeAttribute('disabled') + $(document.body).removeClass("modal-open") + $(".modal-backdrop").remove() }) modal.addEventListener('shown.bs.modal', (event) => { document.getElementById('dataset-title').focus() diff --git a/app/javascript/packs/controllers/experiment_controller.js b/app/javascript/packs/controllers/experiment_controller.js index 42cd0247e5dde9a38e8f6733dafceb053cb4c4e0..2736a9e15cfb057fd813d466d845e7dba982b499 100644 --- a/app/javascript/packs/controllers/experiment_controller.js +++ b/app/javascript/packs/controllers/experiment_controller.js @@ -5,51 +5,45 @@ import { ServerAPI } from "../utils/server_api" export default class extends Controller { static targets = [] - static values = {lastId: Number, experimentId: Number} + static values = {experimentId: Number} connect() { - this.initPanzoom() + this.panzoom = this.initPanzoom() this.draggable = this.initDraggable() new bootstrap.Offcanvas($("#params_offcanvas")[0]) - this.loadExperiment() } display_tool_config(event) { - let toolSlot = $(event.target).parents(".tool-slot-occupied") - let offcanvasElement = $("#params_offcanvas")[0] - offcanvasElement.innerHTML = Templates.toolParamsOffcanvas(toolSlot.data("tool-params")) - offcanvasElement.setAttribute("data-tool-slot-id", toolSlot.attr('id')) - - let offcanvas = bootstrap.Offcanvas.getInstance(offcanvasElement) - offcanvas.show() + const toolId = $(event.target).closest(".tool-slot-occupied").attr('id').substring(5) + ServerAPI.openToolConfig(toolId, this.experimentIdValue, (data) => { + const offcanvasElement = $("#params_offcanvas")[0] + offcanvasElement.innerHTML = data + offcanvasElement.setAttribute("data-tool-slot-id", toolId) + bootstrap.Offcanvas.getInstance(offcanvasElement).show() + }) } apply_tool_config(event) { // TODO: Check if all config fields are valid - let offcanvasElement = $("#params_offcanvas") - let toolSlot = $(`#${offcanvasElement.attr("data-tool-slot-id")}`) - - const tool_params = toolSlot.data("tool-params") - for(let param of offcanvasElement.find(".tool-param").find("input,select")) { - param = $(param) - const current_param = param.data('param') - tool_params.parameters.filter( (p) => p.name == current_param )[0].value = param.val() - } - toolSlot.data("tool-params", tool_params) - - let current_status = toolSlot.data("status") - toolSlot.data("status","configured") - toolSlot.find("span.tool-status").removeClass(`tool-status-${current_status}`) - toolSlot.find("span.tool-status").addClass(`tool-status-configured`) - this.saveExperiment() - - let offcanvas = bootstrap.Offcanvas.getInstance(offcanvasElement[0]) - offcanvas.hide() + const offcanvasElement = $("#params_offcanvas") + const toolId = offcanvasElement.attr("data-tool-slot-id") + const parameters = {} + $("#params_offcanvas").find(".tool-param").find("input,select").map( (i, e) => { + parameters[e.getAttribute("data-param")] = $(e).val() + }) + ServerAPI.editTool(toolId, parameters, this.experimentIdValue, () => { + bootstrap.Offcanvas.getInstance(offcanvasElement[0]).hide() + }) } delete_tool(event) { - $(event.currentTarget).parents(".tool-slot-occupied").parent().remove() - this.saveExperiment() + const toolId = $(event.target).closest('.tool-slot-occupied').attr('id').substring(5) + ServerAPI.deleteTool(toolId, this.experimentIdValue, (data) => { + this.panzoom.destroy() + this.panzoom = this.initPanzoom() + this.draggable.destroy() + this.draggable = this.initDraggable() + }) } initPanzoom() { @@ -60,6 +54,7 @@ export default class extends Controller { canvas_elem.parentElement.addEventListener('wheel', panzoom.zoomWithWheel) // panzoom.pan(10, 10) // panzoom.zoom(2, { animate: true }) + return panzoom } initDraggable() { @@ -108,21 +103,16 @@ export default class extends Controller { $('html,body').css('cursor','auto') if(final_dropzone != null) { if($(final_dropzone).hasClass("possible-tool-slot")) { - this.lastIdValue++ - const defaultsParams = $(event.originalSource).data('tool') - defaultsParams['toolId'] = this.lastIdValue - final_dropzone.outerHTML = Templates.canvasTool(defaultsParams) - const tool = $(`#tool_${this.lastIdValue}`) - tool.data("tool-params", defaultsParams) - const toolSlot1 = $("
  • ")[0] - const toolSlot2 = $("")[0] - tool[0].parentElement.parentElement.appendChild(toolSlot1) - tool[0].parentElement.appendChild(toolSlot2) - final_dropzone = null - draggable.destroy() - this.draggable.destroy() - this.draggable = this.initDraggable() - this.saveExperiment() + const tool = $(event.originalSource).data('tool') + const parent = final_dropzone.parentElement.parentElement.previousElementSibling + const parentId = (parent == null) ? null : parent.getAttribute('id').substring(5) + ServerAPI.addTool(JSON.stringify(tool), parentId, this.experimentIdValue, (data) => { + final_dropzone = null + this.panzoom.destroy() + this.panzoom = this.initPanzoom() + this.draggable.destroy() + this.draggable = this.initDraggable() + }) } else { $(final_dropzone).html("") @@ -132,62 +122,4 @@ export default class extends Controller { }) return draggable } - - saveExperiment() { - function buildJSON(li) { - let subObj = {} - subObj.tool = li.children().data('tool-params') - li.children('ul').children().each(function() { - const currentLi = $(this) - if (!subObj.children) { - subObj.children = [] - } - if(!currentLi.children().hasClass("tool-slot")) - subObj.children.push(buildJSON(currentLi)) - }) - return subObj - } - const graph = {children: []} - $('ul.tree > li').each(function() { - const currentLi = $(this) - if(!currentLi.children().hasClass("tool-slot")) - graph.children.push(buildJSON(currentLi)) - }) - ServerAPI.saveExperiment(this.experimentIdValue, JSON.stringify(graph), ()=>{}) - } - - loadExperiment() { - function buildList(data, self) { - if(data.tool.toolId > self.lastIdValue) { - self.lastIdValue = data.tool.toolId - } - const li = $("
  • ") - const toolElt = $(Templates.canvasTool(data.tool)) - toolElt.data("tool-params", data.tool) - li.append(toolElt) - const ul = $("") - if(data.children && data.children.length > 0) { - for(const child of data.children) { - ul.append(buildList(child, self)) - } - } - ul.append($("
  • ")) - li.append(ul) - return li - } - - ServerAPI.loadExperiment(this.experimentIdValue, (data) => { - const tree = $("") - if(data.children && data.children.length > 0) { - for(const source_tool of data.children) { - tree.append(buildList(source_tool, this)) - } - } - tree.append($("
  • ")) - $("#experiment_canvas ul.tree").empty() - $("#experiment_canvas ul.tree").append(tree.children()) - this.draggable.destroy() - this.draggable = this.initDraggable() - }) - } } \ No newline at end of file diff --git a/app/javascript/packs/controllers/experiments_controller.js b/app/javascript/packs/controllers/experiments_controller.js new file mode 100644 index 0000000000000000000000000000000000000000..f62e9f27aebe705c21503bf57a63558fec5c1531 --- /dev/null +++ b/app/javascript/packs/controllers/experiments_controller.js @@ -0,0 +1,46 @@ +import { Controller } from "stimulus" +import {ServerAPI} from "../utils/server_api" + +export default class extends Controller { + static targets = [] + static values = {} + + connect() { + this.initModal() + } + + initModal() { + const modal = document.getElementById('createExperimentModal') + const modalButton = document.getElementById('create-experiment-button') + modal.addEventListener('hidden.bs.modal', (event) => { + $("#experiment-title").val("") + modalButton.innerHTML = "Create" + modalButton.removeAttribute('disabled') + $(document.body).removeClass("modal-open") + $(".modal-backdrop").remove() + }) + modal.addEventListener('shown.bs.modal', (event) => { + document.getElementById('experiment-title').focus() + }) + } + + createExperiment(event) { + $("#message").html("") + const title = $("#experiment-title").val() + event.target.setAttribute('disabled', 'disabled') + event.target.innerHTML = `Loading` + ServerAPI.create_experiment(title, (data) => { + if(data['status'] === 'ok') { + //close modal + update list + bootstrap.Modal.getInstance(document.getElementById('createExperimentModal')).hide() + ServerAPI.update_experiments_list((data) => {}) + + } + else { + $("#message").html(data['message']) + event.target.innerHTML = "Create" + event.target.removeAttribute('disabled') + } + }) + } +} \ No newline at end of file diff --git a/app/javascript/packs/controllers/hello_controller.js b/app/javascript/packs/controllers/hello_controller.js deleted file mode 100644 index 5ccd246e5c8e4444454c2f62ee11fab5b6377a66..0000000000000000000000000000000000000000 --- a/app/javascript/packs/controllers/hello_controller.js +++ /dev/null @@ -1,10 +0,0 @@ -import { Controller } from "stimulus" - -export default class extends Controller { - connect() { - console.log("Connected.") - } - greet() { - console.log("Hello, Stimulus!", this.element) - } -} \ No newline at end of file diff --git a/app/javascript/packs/utils/server_api.js b/app/javascript/packs/utils/server_api.js index 2d1854da77d4b2e88ed1ba92b54d21fcc2574bce..e82be8502b0c0fcc7f49df26e9b4c8666237e19e 100644 --- a/app/javascript/packs/utils/server_api.js +++ b/app/javascript/packs/utils/server_api.js @@ -1,9 +1,9 @@ export class ServerAPI { - static create_dataset(title, callback) { + static create_experiment(title, callback) { $.ajax({ type: "POST", - url: "/dataset/create", + url: "/experiment/create", data: {title: title}, headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') @@ -17,10 +17,10 @@ export class ServerAPI { }) } - static update_datasets_list(callback) { + static update_experiments_list(callback) { $.ajax({ type: "GET", - url: "/datasets/update", + url: "/experiments/update", headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') }, @@ -34,64 +34,134 @@ export class ServerAPI { }) } - static setCurrentWorkingDataset(datasetId, callback) { + static addTool(tool, parentId, experimentId, callback) { $.ajax({ type: "POST", - url: "/datasets/working_dataset", + url: `/experiment/${experimentId}/add_tool`, + data: {tool: tool, parent_id: parentId}, headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') }, - data: { - dataset_id: datasetId + success: (data, textStatus, jqXHR) => { + callback() + }, + error: (jqXHR, textStatus, errorThrown) => { + + } + }) + } + + static deleteTool(toolId, experimentId, callback) { + $.ajax({ + type: "POST", + url: `/experiment/${experimentId}/delete_tool`, + data: {tool_id: toolId}, + headers: { + 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') + }, + success: (data, textStatus, jqXHR) => { + callback() + }, + error: (jqXHR, textStatus, errorThrown) => { + + } + }) + } + + static openToolConfig(toolId, experimentId, callback) { + $.ajax({ + type: "POST", + url: `/experiment/${experimentId}/edit_tool_form`, + data: {tool_id: toolId}, + headers: { + 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') }, - dataType: "script", success: (data, textStatus, jqXHR) => { callback(data) + }, + error: (jqXHR, textStatus, errorThrown) => { + } }) } - static addSelectedDocumentsToWorkingDataset(documentsIds, callback) { + static editTool(toolId, parameters, experimentId, callback) { $.ajax({ type: "POST", - url: "/datasets/add_documents", + url: `/experiment/${experimentId}/edit_tool`, + data: {tool_id: toolId, parameters: parameters}, headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') }, - data: { - documents_ids: documentsIds + success: (data, textStatus, jqXHR) => { + callback() + }, + error: (jqXHR, textStatus, errorThrown) => { + + } + }) + } + + static create_dataset(title, callback) { + $.ajax({ + type: "POST", + url: "/dataset/create", + data: {title: title}, + headers: { + 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') + }, + success: (data, textStatus, jqXHR) => { + callback(data) + }, + error: (jqXHR, textStatus, errorThrown) => { + + } + }) + } + + static update_datasets_list(callback) { + $.ajax({ + type: "GET", + url: "/datasets/update", + headers: { + 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') }, dataType: "script", success: (data, textStatus, jqXHR) => { callback(data) + }, + error: (jqXHR, textStatus, errorThrown) => { + } }) } - static paginateDataset(datasetId, page, per_page, sort, sort_order, type, callback) { + static setCurrentWorkingDataset(datasetId, callback) { $.ajax({ type: "POST", - url: `/dataset/${datasetId}/paginate`, + url: "/datasets/working_dataset", headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') }, data: { - page: page, per_page: per_page, sort: sort, sort_order: sort_order, type: type + dataset_id: datasetId }, - dataType: "script", success: (data, textStatus, jqXHR) => { callback(data) } }) } - static getDatasets(callback) { + static addSelectedDocumentsToWorkingDataset(documentsIds, callback) { $.ajax({ - type: "GET", - url: `/datasets/list`, + type: "POST", + url: "/datasets/add_documents", headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') }, + data: { + documents_ids: documentsIds + }, dataType: "script", success: (data, textStatus, jqXHR) => { callback(data) @@ -99,29 +169,31 @@ export class ServerAPI { }) } - static saveExperiment(experimentId, experimentGraph, callback) { + static paginateDataset(datasetId, page, per_page, sort, sort_order, type, callback) { $.ajax({ type: "POST", - url: `/experiment/${experimentId}/save`, + url: `/dataset/${datasetId}/paginate`, headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') }, - data: {description: experimentGraph}, - dataType: "json", + data: { + page: page, per_page: per_page, sort: sort, sort_order: sort_order, type: type + }, + dataType: "script", success: (data, textStatus, jqXHR) => { callback(data) } }) } - static loadExperiment(experimentId, callback) { + static getDatasets(callback) { $.ajax({ type: "GET", - url: `/experiment/${experimentId}/load`, + url: `/datasets/list`, headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') }, - dataType: "json", + dataType: "script", success: (data, textStatus, jqXHR) => { callback(data) } diff --git a/app/javascript/packs/utils/templates.js b/app/javascript/packs/utils/templates.js index 6291cead2a1ac1b6d44a6ce8e299f04f4d8d7ddf..82f1e387c9ba0dbc0f37370e22f44ced424d4106 100644 --- a/app/javascript/packs/utils/templates.js +++ b/app/javascript/packs/utils/templates.js @@ -9,16 +9,15 @@ export class Templates {
    ${ tool.name } - +
    -
    +
    -
    diff --git a/app/models/experiment.rb b/app/models/experiment.rb index a196a468642fcd3465721b70555f5c478f3756ed..b7de592af713198200ba92ff6f0a7758697c6562 100644 --- a/app/models/experiment.rb +++ b/app/models/experiment.rb @@ -1,7 +1,85 @@ class Experiment < ActiveRecord::Base belongs_to :user, optional: false + validates :title, length: { minimum: 1 } - + def add_tool(parent_id, tool) + if parent_id != 0 + self.locate_tool(self.description, parent_id) do |t| + t['children'] << tool.to_h + end + else + self.description['children'] << tool.to_h + end + end + + def delete_tool(tool_id) + ids = detach_tool(self.description, nil, tool_id) + puts ids + return ids + end + + def update_tool(tool_id, parameters) + self.locate_tool(self.description, tool_id) do |t| + t['tool']['parameters'].map! do |param| + param['value'] = parameters[param['name']] + param + end + end + end + + private + + def detach_tool(tree, parent_array, tool_id, &block) + if tree.has_key?('tool') + if tree['tool']['id'] == tool_id + ids = gather_ids(tree) + parent_array.delete(tree) unless parent_array.nil? + return ids + else + tree['children'].each do |subtree| + res = detach_tool(subtree, tree['children'], tool_id, &block) + return res unless res.nil? + end + end + else + tree['children'].each do |subtree| + res = detach_tool(subtree, tree['children'], tool_id, &block) + return res unless res.nil? + end + end + nil + end + + def gather_ids(tree, ids=[]) + if tree.has_key?('tool') + ids << tree['tool']['id'] + tree['children'].each do |subtree| + ids.concat(gather_ids(subtree)) + end + end + return ids + end + + def locate_tool(tree_part, parent_id, &block) + if tree_part.has_key?('tool') + if tree_part['tool']['id'] == parent_id + yield tree_part + return true + else + tree_part['children'].each do |subtree| + break if locate_tool(subtree, parent_id, &block) + end + end + else + if tree_part['children'].empty? + yield tree_part + end + tree_part['children'].each do |subtree| + break if locate_tool(subtree, parent_id, &block) + end + end + + end end diff --git a/app/models/tool.rb b/app/models/tool.rb new file mode 100644 index 0000000000000000000000000000000000000000..9441d1c495400a07f790305ac5215a25003d3448 --- /dev/null +++ b/app/models/tool.rb @@ -0,0 +1,16 @@ +class Tool < ActiveRecord::Base + + belongs_to :experiment, optional: false + + def to_h + { + "tool": { + "id": self.id, + "type": self.tool_type, + "parameters": self.parameters + }, + "children": [] + } + end + +end diff --git a/app/views/experiment/_canvas_tool.html.erb b/app/views/experiment/_canvas_tool.html.erb deleted file mode 100644 index 819cd29d5b74a0d53b491dcd826c8ae2d665423b..0000000000000000000000000000000000000000 --- a/app/views/experiment/_canvas_tool.html.erb +++ /dev/null @@ -1,17 +0,0 @@ -
    -
    -
    - <%= tool_data[:name] %> - X -
    -
    -
    -
    <%= tool_data[:title] %>
    -

    blablabla

    -
    -
    - -
    -
    -
    -
    \ No newline at end of file diff --git a/app/views/experiment/_experiment_menu.html.erb b/app/views/experiment/_experiment_menu.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..215c796cc4e78e5eb63416f3b2184945d7865f55 --- /dev/null +++ b/app/views/experiment/_experiment_menu.html.erb @@ -0,0 +1,8 @@ +
    +
    + Experiment +
    +
    +

    <%= @experiment.title %>

    +
    +
    \ No newline at end of file diff --git a/app/views/experiment/_experiments_list.html.erb b/app/views/experiment/_experiments_list.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..268b63be6b52f90a648ddeed067554473366b518 --- /dev/null +++ b/app/views/experiment/_experiments_list.html.erb @@ -0,0 +1,16 @@ +
    +
    + Experiments +
    +
    +
      + <% current_user.experiments.each do |experiment| %> +
    • + <%= experiment.title %> + Access experiment + 1 +
    • + <% end %> +
    +
    +
    \ No newline at end of file diff --git a/app/views/experiment/_tools_menu.html.erb b/app/views/experiment/_tools_menu.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..1b6eb32f2788984f6ea09e78f77a6ac148b45a26 --- /dev/null +++ b/app/views/experiment/_tools_menu.html.erb @@ -0,0 +1,29 @@ +
    +
    + Tools +
    +
    + +
    + + + +
    +
    +
    \ No newline at end of file diff --git a/app/views/experiment/_tree.html.erb b/app/views/experiment/_tree.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..d335a1bcfe87091aa7b8f4873caf54817754a1dc --- /dev/null +++ b/app/views/experiment/_tree.html.erb @@ -0,0 +1,5 @@ +
    + +
    \ No newline at end of file diff --git a/app/views/experiment/index.html.erb b/app/views/experiment/index.html.erb index 26289897b20c95e95e6f28e18c6b21fbeb925772..ed9d20fc835b144222d152b4470c88ea0c93c797 100644 --- a/app/views/experiment/index.html.erb +++ b/app/views/experiment/index.html.erb @@ -1,32 +1,43 @@ <% set_page_title "Experiments" %> -
    +
    Utilities
    - + +
    -
    -
    - Experiments -
    -
    -
      - <% current_user.experiments.each do |experiment| %> -
    • - <%= experiment.id %> - Access experiment - 1 -
    • - <% end %> -
    -
    +
    + <%= render partial: 'experiments_list' %>
    \ No newline at end of file diff --git a/app/views/experiment/show.html.erb b/app/views/experiment/show.html.erb index d16def5a69ba66684edd0b3f8d073c33a39208f1..f71cc05dccd3358cc666292a0f23f3ff92767074 100644 --- a/app/views/experiment/show.html.erb +++ b/app/views/experiment/show.html.erb @@ -2,54 +2,19 @@ <% tools = JSON.parse(@tools_description) %>
    + data-experiment-experiment-id-value="<%= @experiment.id %>">
    -
    -
    -
    - Tools -
    -
    - -
    - - - -
    -
    +
    +
    + <%= render partial: "experiment/experiment_menu" %> +
    +
    + <%= render partial: "experiment/tools_menu", locals: {tools: tools} %>
    -
    -
    -
      -
    • -
      - -
      -
    • -
    -
    + <%= render partial: 'experiment/tree', locals: {experiment: @experiment} %>
    diff --git a/app/views/experiment/update_experiment_area.js.erb b/app/views/experiment/update_experiment_area.js.erb new file mode 100644 index 0000000000000000000000000000000000000000..d443d6599bff3a8c84de115a26524e954b5cbdd3 --- /dev/null +++ b/app/views/experiment/update_experiment_area.js.erb @@ -0,0 +1 @@ +$("#experiment_area").html("<%= j render(partial: "tree", locals: {experiment: @experiment}) %>") \ No newline at end of file diff --git a/app/views/experiment/update_experiments_list.js.erb b/app/views/experiment/update_experiments_list.js.erb new file mode 100644 index 0000000000000000000000000000000000000000..4cee926c6f32ccd192a6594b532dc6953c161f7f --- /dev/null +++ b/app/views/experiment/update_experiments_list.js.erb @@ -0,0 +1 @@ +$("#experiments_list").html("<%= j render(partial: "experiments_list") %>") \ No newline at end of file diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 0e0ceada36e2308c5d74cb32772557c85c3ae560..a320cffb951b85dcd61a2047b664127a0b73d213 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -5,6 +5,7 @@ <%= csrf_meta_tags %> <%= csp_meta_tag %> + <%= action_cable_meta_tag %> <%#= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> <%= stylesheet_pack_tag 'application', media: 'all', 'data-turbolinks-track': 'reload' %> diff --git a/app/views/named_entities/_named_entities.html.erb b/app/views/named_entities/_named_entities.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/views/tool/_canvas_tool.html.erb b/app/views/tool/_canvas_tool.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..11cfa3f335526953b4f5938192c433e6d9637007 --- /dev/null +++ b/app/views/tool/_canvas_tool.html.erb @@ -0,0 +1,19 @@ +
    +
    +
    + <%= t("newspapers.tools.type.#{tool['type']}") %> + +
    +
    +
    + +
    +
    +
    +
    \ No newline at end of file diff --git a/app/views/experiment/_menu_tool.html.erb b/app/views/tool/_menu_tool.html.erb similarity index 100% rename from app/views/experiment/_menu_tool.html.erb rename to app/views/tool/_menu_tool.html.erb diff --git a/app/views/tool/_parameters.html.erb b/app/views/tool/_parameters.html.erb new file mode 100644 index 0000000000000000000000000000000000000000..2c2cdeb781e1d233aba68036a801318f868975a2 --- /dev/null +++ b/app/views/tool/_parameters.html.erb @@ -0,0 +1,37 @@ +
    +
    <%= t("newspapers.tools.type.#{tool.tool_type}") %>
    + +
    +
    + <% tool.parameters.each do |param| %> +
    + <%= param['name'] %> + <% case param['type'] %> + <% when "string" %> + + <% when "float" %> + + <% when "integer" %> + + <% when "select" %> + + <% end %> +
    + <% end %> + +
    \ No newline at end of file diff --git a/app/workers/tool_runner_worker.rb b/app/workers/tool_runner_worker.rb new file mode 100644 index 0000000000000000000000000000000000000000..52c227ea06f5d0573125c16188f314a5d72f1122 --- /dev/null +++ b/app/workers/tool_runner_worker.rb @@ -0,0 +1,7 @@ +class ToolRunnerWorker + include Sidekiq::Worker + + def perform(user_id) + ActionCable.server.broadcast("notifications.#{user_id}", {test: 'done'}) + end +end diff --git a/config/application.rb b/config/application.rb index 9609f05f198cb5c38b09b23e93ecb482f1557af3..66dde983efadf38f3a22828e8b3652f47189d794 100644 --- a/config/application.rb +++ b/config/application.rb @@ -7,17 +7,19 @@ require "rails/all" Bundler.require(*Rails.groups) module NewspapersPlatform - class Application < Rails::Application - # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 6.1 + class Application < Rails::Application + # Initialize configuration defaults for originally generated Rails version. + config.load_defaults 6.1 - # Configuration for the application, engines, and railties goes here. - # - # These settings can be overridden in specific environments using the files - # in config/environments, which are processed later. - # - # config.time_zone = "Central Time (US & Canada)" - # config.eager_load_paths << Rails.root.join("extras") - config.solr = config_for('solr') - end + # Configuration for the application, engines, and railties goes here. + # + # These settings can be overridden in specific environments using the files + # in config/environments, which are processed later. + # + # config.time_zone = "Central Time (US & Canada)" + # config.eager_load_paths << Rails.root.join("extras") + config.solr = config_for('solr') + + config.active_job.queue_adapter = :sidekiq + end end diff --git a/config/cable.yml b/config/cable.yml index c721a49b75cb6d2eec24acd4137f8d6b1f558ac5..d9a6136f61f5d9a4c97f928a1cfc22760d0d3eb7 100644 --- a/config/cable.yml +++ b/config/cable.yml @@ -1,5 +1,7 @@ development: - adapter: async + adapter: redis + url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> + channel_prefix: newspapers_platform_production_dev test: adapter: test @@ -7,4 +9,4 @@ test: production: adapter: redis url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %> - channel_prefix: newspapers_platform_production + channel_prefix: newspapers_platform_production_prod diff --git a/config/initializers/warden_hooks.rb b/config/initializers/warden_hooks.rb new file mode 100644 index 0000000000000000000000000000000000000000..a8165d3761fea022807db6a67b0361a8e8804edb --- /dev/null +++ b/config/initializers/warden_hooks.rb @@ -0,0 +1,9 @@ +Warden::Manager.after_set_user do |user, auth, opts| + auth.cookies.signed["user.id"] = user.id + auth.cookies.signed["user.expires_at"] = 30.minutes.from_now +end + +Warden::Manager.before_logout do |user, auth, opts| + auth.cookies.signed["user.id"] = nil + auth.cookies.signed["user.expires_at"] = nil +end \ No newline at end of file diff --git a/config/locales/newspapers.en.yml b/config/locales/newspapers.en.yml index 3207d01e4f31baa283964f7b3eb03767f9ca1190..cb1845daffc054332c326d44725f06494d217a0f 100644 --- a/config/locales/newspapers.en.yml +++ b/config/locales/newspapers.en.yml @@ -25,4 +25,7 @@ en: language: "Language" newspaper: "Newspaper" persons: "Persons" - locations: "Locations" \ No newline at end of file + locations: "Locations" + tools: + type: + source_dataset: "Source Dataset" \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index dc0968c2b606b76b7333291973761f60b9a3a145..4348cde3b4501e290889599148295ae3946c604c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,25 +1,38 @@ # frozen_string_literal: true - +require 'sidekiq/web' Rails.application.routes.draw do - devise_for :users - # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html - root to: 'catalog#home' + devise_for :users + # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html + root to: 'catalog#home' + + get '/search', to: 'catalog#index' + get '/catalog/:id', to: 'catalog#show' + post '/paginate_facets', to: 'catalog#paginate_facets' + + get '/datasets', to: 'dataset#index' + get '/datasets/update', to: 'dataset#update_datasets_list' + post '/datasets/working_dataset', to: 'dataset#set_working_dataset' + post "/datasets/add_documents", to: "dataset#add_documents" + get "/datasets/list", to: "dataset#list_datasets" + get '/dataset/:id', to: 'dataset#show' + post "/dataset/:id/paginate", to: "dataset#paginate" + post '/dataset/create', to: 'dataset#create_dataset' - get '/search', to: 'catalog#index' - get '/catalog/:id', to: 'catalog#show' - post '/paginate_facets', to: 'catalog#paginate_facets' + get '/experiments', to: 'experiment#index' + get '/experiments/update', to: 'experiment#update_experiments_list' + post '/experiment/create', to: 'experiment#create' + get '/experiment/:id', to: "experiment#show" + get '/experiment/:id/load', to: "experiment#load" + post '/experiment/:id/save', to: "experiment#save" + post '/experiment/:id/add_tool', to: "experiment#add_tool" + post '/experiment/:id/delete_tool', to: "experiment#delete_tool" + post '/experiment/:id/edit_tool', to: "experiment#edit_tool" + post '/experiment/:id/edit_tool_form', to: "experiment#edit_tool_form" - get '/datasets', to: 'dataset#index' - get '/datasets/update', to: 'dataset#update_datasets_list' - post '/datasets/working_dataset', to: 'dataset#set_working_dataset' - post "/datasets/add_documents", to: "dataset#add_documents" - get "/datasets/list", to: "dataset#list_datasets" - get '/dataset/:id', to: 'dataset#show' - post "/dataset/:id/paginate", to: "dataset#paginate" - post '/dataset/create', to: 'dataset#create_dataset' + resources :tool, only: [:show, :create, :update, :destroy] - get '/experiments', to: 'experiment#index' - get '/experiment/:id', to: "experiment#show" - get '/experiment/:id/load', to: "experiment#load" - post '/experiment/:id/save', to: "experiment#save" + mount ActionCable.server => '/cable' + if Rails.env.development? + mount Sidekiq::Web => '/sidekiq' + end end diff --git a/db/migrate/20210903194218_create_experiment.rb b/db/migrate/20210903194218_create_experiment.rb index d0ed77c4acfcc314177a7b6d7be9adb5eccaf549..8c0761e3ecd7f5342a104fc383aa0912e81df7c9 100644 --- a/db/migrate/20210903194218_create_experiment.rb +++ b/db/migrate/20210903194218_create_experiment.rb @@ -3,8 +3,9 @@ class CreateExperiment < ActiveRecord::Migration[6.1] create_table :experiments do |t| t.string :title t.references :user, foreign_key: true - t.jsonb :description + t.jsonb :description, default: {children:[]} t.timestamps end + add_index :experiments, [:title, :user_id], unique: true end end diff --git a/db/migrate/20210915140752_add_tool.rb b/db/migrate/20210915140752_add_tool.rb new file mode 100644 index 0000000000000000000000000000000000000000..cee1661c2ea3c2ae2d14fb95585534d421e414c5 --- /dev/null +++ b/db/migrate/20210915140752_add_tool.rb @@ -0,0 +1,12 @@ +class AddTool < ActiveRecord::Migration[6.1] + def change + create_table :tools do |t| + t.references :experiment, foreign_key: true + t.string :tool_type + t.jsonb :parameters, default: {} + t.jsonb :results, default: {} + t.string :status, default: "created" + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index ffe507c529b3ee2c509e95e7876f3df9e50e7d04..ffa7dc9df820b3a8548555170af5c68f9b8301e1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2021_09_09_142841) do +ActiveRecord::Schema.define(version: 2021_09_15_140752) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -29,12 +29,24 @@ ActiveRecord::Schema.define(version: 2021_09_09_142841) do create_table "experiments", force: :cascade do |t| t.string "title" t.bigint "user_id" - t.jsonb "description" + t.jsonb "description", default: {"children"=>[]} t.datetime "created_at", precision: 6, null: false t.datetime "updated_at", precision: 6, null: false + t.index ["title", "user_id"], name: "index_experiments_on_title_and_user_id", unique: true t.index ["user_id"], name: "index_experiments_on_user_id" end + create_table "tools", force: :cascade do |t| + t.bigint "experiment_id" + t.string "tool_type" + t.jsonb "parameters", default: {} + t.jsonb "results", default: {} + t.string "status", default: "created" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["experiment_id"], name: "index_tools_on_experiment_id" + end + create_table "users", force: :cascade do |t| t.string "email", default: "", null: false t.string "encrypted_password", default: "", null: false @@ -49,4 +61,5 @@ ActiveRecord::Schema.define(version: 2021_09_09_142841) do add_foreign_key "datasets", "users" add_foreign_key "experiments", "users" + add_foreign_key "tools", "experiments" end diff --git a/test/channels/notification_channel_test.rb b/test/channels/notification_channel_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..a2c6b4418c1ca7261984d0cc52df9c1db1afcee2 --- /dev/null +++ b/test/channels/notification_channel_test.rb @@ -0,0 +1,8 @@ +require "test_helper" + +class NotificationChannelTest < ActionCable::Channel::TestCase + # test "subscribes" do + # subscribe + # assert subscription.confirmed? + # end +end diff --git a/test/workers/tool_runner_worker_test.rb b/test/workers/tool_runner_worker_test.rb new file mode 100644 index 0000000000000000000000000000000000000000..29db595b6f15a2c8dfe004c73b50df039d4f577c --- /dev/null +++ b/test/workers/tool_runner_worker_test.rb @@ -0,0 +1,6 @@ +require 'test_helper' +class ToolRunnerWorkerTest < Minitest::Test + def test_example + skip "add some examples to (or delete) #{__FILE__}" + end +end