From 7801df9be07dad2f5c6c4a6c680387a0b2772017 Mon Sep 17 00:00:00 2001 From: Remi Bernard Date: Thu, 23 Jul 2015 17:19:52 -0400 Subject: [PATCH 01/85] JSON task/tool descriptors (boutiques) Main/initial implementation of JSON-based task/tool descriptor integration. Allows adding tools (task types) by specifying a JSON descriptor file (with a corresponding JSON schema) that describes the tool's command-line interface. The CbrainTask subclass generator either generates a set of Ruby files suitable for further modification and inclusion as a plugin or directly defines the task class to integrate the descriptor without using source files. The generated view templates (notably task_params) also sport a somewhat fancier style than what is currently used. --- BrainPortal/Gemfile | 1 + BrainPortal/db/schema.rb | 2 + .../schema_task_generator.rb | 290 +++++++++++ .../schemas/boutiques.schema.json | 191 +++++++ .../templates/bourreau.rb.erb | 343 +++++++++++++ .../templates/edit_help.html.erb | 91 ++++ .../templates/portal.rb.erb | 299 +++++++++++ .../templates/show_params.html.erb.erb | 120 +++++ .../templates/task_params.html.erb.erb | 476 ++++++++++++++++++ BrainPortal/public/stylesheets/cbrain.css | 230 +++++++++ 10 files changed, 2043 insertions(+) create mode 100644 BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb create mode 100644 BrainPortal/lib/cbrain_task_generators/schemas/boutiques.schema.json create mode 100644 BrainPortal/lib/cbrain_task_generators/templates/bourreau.rb.erb create mode 100644 BrainPortal/lib/cbrain_task_generators/templates/edit_help.html.erb create mode 100644 BrainPortal/lib/cbrain_task_generators/templates/portal.rb.erb create mode 100644 BrainPortal/lib/cbrain_task_generators/templates/show_params.html.erb.erb create mode 100644 BrainPortal/lib/cbrain_task_generators/templates/task_params.html.erb.erb diff --git a/BrainPortal/Gemfile b/BrainPortal/Gemfile index 6ccecd58f..58fb76b23 100644 --- a/BrainPortal/Gemfile +++ b/BrainPortal/Gemfile @@ -34,6 +34,7 @@ gem "sys-proctable", :git => 'git://github.com/djberg96/sys-proctable.git' gem "thin" gem "rails_autolink" gem "pbkdf2-ruby" +gem "json-schema" gem "test-unit" group :development do diff --git a/BrainPortal/db/schema.rb b/BrainPortal/db/schema.rb index 250b2fcdc..022f3e81b 100644 --- a/BrainPortal/db/schema.rb +++ b/BrainPortal/db/schema.rb @@ -315,6 +315,7 @@ t.datetime "updated_at" t.integer "group_id" t.integer "ncpus" + t.string "docker_image" end add_index "tool_configs", ["bourreau_id"], :name => "index_tool_configs_on_bourreau_id" @@ -359,6 +360,7 @@ add_index "userfiles", ["data_provider_id"], :name => "index_userfiles_on_data_provider_id" add_index "userfiles", ["group_id"], :name => "index_userfiles_on_group_id" add_index "userfiles", ["hidden", "id"], :name => "index_userfiles_on_hidden_and_id" + add_index "userfiles", ["hidden"], :name => "index_userfiles_on_hidden" add_index "userfiles", ["immutable", "id"], :name => "index_userfiles_on_immutable_and_id" add_index "userfiles", ["name"], :name => "index_userfiles_on_name" add_index "userfiles", ["type"], :name => "index_userfiles_on_type" diff --git a/BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb b/BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb new file mode 100644 index 000000000..511a65a98 --- /dev/null +++ b/BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb @@ -0,0 +1,290 @@ + +# +# CBRAIN Project +# +# Copyright (C) 2008-2012 +# The Royal Institution for the Advancement of Learning +# McGill University +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +require 'fileutils' + +# This module handles generation of CbrainTasks from schema and descriptor +# files. The generated code can be added right away to CBRAIN's available tasks +# or written to file for later modification. +# +# NOTE: Only JSON is currently supported +module SchemaTaskGenerator + + Revision_info=CbrainFileRevision[__FILE__] #:nodoc: + + # Represents a schema to validate task descriptors against + class Schema + + # Creates a new Schema from either a file path, a string or a hash + # representing the schema. + def initialize(schema) + @schema = SchemaTaskGenerator.expand_json(schema) + end + + # Validates +descriptor+ against the schema. Returns a list of validation + # errors or nil if +descriptor+ is valid. + def validate(descriptor) + JSON::Validator.fully_validate( + @schema, + SchemaTaskGenerator.expand_json(descriptor), + :errors_as_objects => true + ) + end + + # Same as +validate+, but throws exceptions on validation errors instead. + # Still returns nil if +descriptor+ is valid. + def validate!(descriptor) + JSON::Validator.validate!( + @schema, + SchemaTaskGenerator.expand_json(descriptor), + :errors_as_objects => true + ) + end + + # A Schema essentially behaves like a hash, as to allow accessing schema + # properties. Forwards all other unknown method calls to Hash, if they + # exist. + def method_missing(method, *args) #:nodoc: + if @schema.respond_to?(method) + @schema.send(method, *args) + else + super + end + end + + end + + # Encapsulates a CbrainTask generated by this generator + class GeneratedTask + + # Name of the generated class. Usually a camel-case form + # of descriptor[:name]. + attr_accessor :name + # Descriptor used to generate this task, in hash form. + attr_accessor :descriptor + # Schema instance used to generate this task. + attr_accessor :schema + # Validation errors produced when validating the task's descriptor, if any. + attr_accessor :validation_errors + # Generated Ruby/HTML source for this task. Hash with at least the keys: + # [:portal] BrainPortal side task class Ruby source. Contains a class + # inheriting/implementing PortalTask. + # [:bourreau] Bourreau side task class Ruby source. Contains a class + # inheriting/implementing ClusterTask. + # [:task_params] Ruby ERB form template for the task's params (edit) page. + # [:show_params] Ruby ERB template for the task's show page. + # [:edit_help] Ruby ERB template for the task's parameter help popup. + attr_accessor :source + + # Create a new encapsulated generated CbrainTask from the output of the + # generator (+SchemaTaskGenerator+::+generate+ method). +attributes+ is + # expected to be a hash matching this object's attributes (:source, :name, + # :descriptor, etc.) + def initialize(attributes) + attributes.each do |name, value| + instance_variable_set("@#{name}", value) + end + end + + # Integrates the encapsulated CbrainTask in this CBRAIN installation. + # Unless +register+ is specified to be false, this method will add the + # required Tool and ToolConfig if necessary for the CbrainTask to be + # useable right away (Since almost all information required to make the + # Tool and ToolConfig objects is available in the spec). + def integrate(register = true) + # As the same code is used to dynamically load tasks descriptors and + # create task templates, the class definitions are generated as strings + # (Otherwise the source wouldn't be available to write down the generated + # templates). This forces the use of eval instead of the much nicer, + # faster and easier to maintain alternatives. :( + eval @source[Rails.root.to_s =~ /BrainPortal$/ ? :portal : :bourreau] + + # Try and retrieve the just-generated task class + task = @name.camelize.constantize rescue nil + task ||= "CbrainTask::#{@name.camelize}".constantize + + # Since the task class doesn't have a matching cbrain_plugins directory + # tree, some methods need to be added/redefined to ensure the cooperation + # of views and controllers. + generated = self + + # The task class has no public_path. + task.define_singleton_method(:public_path) { |public_file| nil } + + # Make sure the task class still has access to its generated source + task.define_singleton_method(:generated_from) { generated } + + # Offer access to the raw string version of the view partials for use + # in views instead of the cbrain_plugins paths. + task.define_singleton_method(:raw_partial) do |partial| + ({ + :task_params => generated.source[:task_params], + :show_params => generated.source[:show_params], + :edit_help => generated.source[:edit_help] + })[partial]; + end + + return unless register + + # With the task class and descriptor, we have enough information to + # generate a Tool and ToolConfig to register the tool into CBRAIN. + # The newly created Tool and ToolConfig (if needed) will initially belong + # to the core admin. + + # Create and save a new Tool for this task, unless theres already one. + Tool.new( + :name => @descriptor['name'], + :user_id => User.admin.id, + :group_id => User.admin.own_group.id, + :category => "scientific tool", + :cbrain_task_class => task.to_s, + :description => @descriptor['description'] + ).save! unless + Tool.exists?(:cbrain_task_class => task.to_s) + + # Create and save a new ToolConfig for this task on this server, unless + # theres already one. Only applies to Bourreaux (as it would make no + # sense on the portal). + return if Rails.root.to_s =~ /BrainPortal$/ + + ToolConfig.new( + :tool_id => task.tool.id, + :bourreau_id => RemoteResource.current_resource.id, + :group_id => Group.everyone.id, + :version_name => @descriptor['tool-version'], + :docker_image => @descriptor['docker-image'] + ).save! unless + ToolConfig.exists?( + :tool_id => task.tool.id, + :bourreau_id => RemoteResource.current_resource.id, + :version_name => @descriptor['tool-version'] + ) + end + + # Writes the encapsulated CbrainTask as a directory tree under +path+ under + # the CBRAIN plugin format; + # source[:portal] -> /portal/.rb + # source[:bourreau] -> /bourreau/.rb + # source[:task_params] -> /views/_task_params.html.erb + # source[:show_params] -> /views/_show_params.html.erb + # source[:edit_help] -> /views/public/edit_params_help.html + def to_directory(path) + name = @name.underscore + path = Pathname.new(path.to_s) + name + + FileUtils.mkpath(path) + Dir.chdir(path) do + ['portal', 'bourreau', 'views/public'].each { |d| FileUtils.mkpath(d) } + + IO.write("portal/#{name}.rb", @source[:portal]) + IO.write("bourreau/#{name}.rb", @source[:bourreau]) + IO.write("views/_task_params.html.erb", @source[:task_params]) + IO.write("views/_show_params.html.erb", @source[:show_params]) + IO.write("views/public/edit_params_help.html", @source[:edit_help]) + end + end + + end + + # Generates a CbrainTask from +descriptor+, which is expected to validate + # against +schema+. +schema+ is expected to be either a +Schema+ instance, + # a path to a schema file, the schema in string format or a hash + # representing the schema. + # Similarly to +schema+, +descriptor+ is expected to be either a path to a + # descriptor file, the descriptor in string format or a hash representing + # the descriptor. + # By default, the validation of +descriptor+ against +schema+ is strict + # and +generate+ will abort at any validation error. Set +strict_validation+ + # to false if you wish for the generator to try and generate the task despite + # validation issues. + def self.generate(schema, descriptor, strict_validation = true) + descriptor = self.expand_json(descriptor) + name = descriptor['name'].camelize + schema = Schema.new(schema) unless schema.is_a?(Schema) + errors = schema.send( + strict_validation ? :'validate!' : :validate, + descriptor + ) || [] + + apply_template = lambda do |template| + ERB.new(IO.read( + Rails.root.join('lib/cbrain_task_generators/templates', template).to_s + ), nil, '%<>>-').result(binding) + end + + GeneratedTask.new( + :name => name, + :descriptor => descriptor, + :schema => schema, + :validation_errors => errors, + :source => { + :portal => apply_template.('portal.rb.erb'), + :bourreau => apply_template.('bourreau.rb.erb'), + :task_params => apply_template.('task_params.html.erb.erb'), + :show_params => apply_template.('show_params.html.erb.erb'), + :edit_help => apply_template.('edit_help.html.erb') + }, + ) + end + + # Utility method to convert a JSON string or file path into a hash. + # Returns the hash directly if a hash is given. + def self.expand_json(obj) + return obj unless obj.is_a?(String) + + JSON.parse!(File.exists?(obj) ? IO.read(obj) : obj) + end + + private + + # Utility/helper methods used in templates. + + # Create a function call formatter for +func+ with possible arguments lists + # +args+. The generated formatter will accept a list of arguments to format + # a call with. (formatter.(['a', 'b']) -> 'func(a, b)') + # If given, +block+ will be used to convert each value in +args+ + # (and the argument passed to the generated function) to an argument list. + # + # Example: + # a = [{ :a => 1, :b => 2 }, { :a => 2, :b => 4 }] + # f = format_call('f', a) { |a| [ a[:a], a[:b] ] } + # f.({ :a => 1, :b => 2}) # gives 'f(1, 2)' + def self.format_call(func, args, &block) + args = args.map { |a| block.(a) } if block + + widths = (args.first rescue []).zip(*args).map do |array| + array.map { |v| v.length rescue 0 }.max + 1 + end + + lambda do |args| + inner = (block ? block.(args) : args) + .reject(&:blank?) + .each_with_index + .map { |v, i| "%-#{widths[i]}s" % (v + ',') } + .join(' ') + .gsub(/,\s*$/, '') + + "#{func}(#{inner})" + end + end + +end diff --git a/BrainPortal/lib/cbrain_task_generators/schemas/boutiques.schema.json b/BrainPortal/lib/cbrain_task_generators/schemas/boutiques.schema.json new file mode 100644 index 000000000..0d3d3f501 --- /dev/null +++ b/BrainPortal/lib/cbrain_task_generators/schemas/boutiques.schema.json @@ -0,0 +1,191 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "http://github.com/boutiques/boutiques-schema", + "type": "object", + "title": "Tool", + "properties": { + "name": { + "id": "http://github.com/boutiques/boutiques-schema/name", + "minLength": 1, + "description": "Tool name.", + "type": "string" + }, + "tool-version": { + "id": "http://github.com/boutiques/boutiques-schema/description", + "minLength": 1, + "description": "Tool version.", + "type": "string" + }, + "description": { + "id": "http://github.com/boutiques/boutiques-schema/description", + "minLength": 1, + "description": "Tool description.", + "type": "string" + }, + "command-line": { + "id": "http://github.com/boutiques/boutiques-schema/command-line", + "minLength": 1, + "description": "A string that describes the tool command line, where input and output values are identified by \"keys\". At runtime, command-line keys are substituted with flags and values.", + "type": "string" + }, + "docker-image": { + "id": "http://github.com/boutiques/boutiques-schema/docker-image", + "minLength": 1, + "description": "Name of a Docker image where tool is installed and configured. Example: docker.io/neurodebian.", + "type": "string" + }, + "docker-index": { + "id": "http://github.com/boutiques/boutiques-schema/docker-index", + "minLength": 1, + "description": "Docker index where Docker image is available.", + "default": "http://index.docker.io", + "type": "string" + }, + "schema-version": { + "id": "http://github.com/boutiques/boutiques-schema/schema-version", + "type": "string", + "description": "Version of the schema used.", + "enum": ["0.2-snapshot"] + }, + "inputs": { + "id": "http://github.com/boutiques/boutiques-schema/inputs", + "type": "array", + "items": { + "id": "http://github.com/boutiques/boutiques-schema/input", + "type": "object", + "properties": { + "id": { + "id": "http://github.com/boutiques/boutiques-schema/input/id", + "minLength": 1, + "description": "A short, unique, informative identifier containing only alphanumeric characters and underscores. Typically used to generate variable names. Example: \"data_file\".", + "type": "string", + "pattern": "^[0-9,_,a-z,A-Z]*$" + }, + "name": { + "id": "http://github.com/boutiques/boutiques-schema/input/name", + "minLength": 1, + "description": "A human-readable input name. Example: 'Data file'.", + "type": "string" + }, + "type": { + "id": "http://github.com/boutiques/boutiques-schema/input/type", + "type": "string", + "description": "Input type.", + "enum": ["String", "File", "Flag" , "Number" ] + }, + "description": { + "id": "http://github.com/boutiques/boutiques-schema/input/description", + "minLength": 1, + "description": "Input description.", + "type": "string" + }, + "command-line-key": { + "id": "http://github.com/boutiques/boutiques-schema/input/command-line-key", + "minLength": 1, + "description": "A string contained in command-line, substituted by the input value and/or flag at runtime.", + "type": "string" + }, + "list": { + "id": "http://github.com/boutiques/boutiques-schema/input/list", + "description":"True if input is a list of value. An input of type \"Flag\" cannot be a list.", + "type": "boolean" + }, + "optional":{ + "id": "http://github.com/boutiques/boutiques-schema/input/optional", + "description": "True if input is optional.", + "type": "boolean" + }, + "command-line-flag":{ + "id": "http://github.com/boutiques/boutiques-schema/input/command-line-flag", + "minLength": 1, + "description": "Option flag of the input, involved in the command-line-key substitution. Inputs of type \"Flag\" have to have a command-line flag. Examples: -v, --force.", + "type": "string" + }, + "default-value":{ + "id": "http://github.com/boutiques/boutiques-schema/input/default-value", + "minLength": 1, + "description": "Default value of the input, used by the tool when no option is specified.", + "type": "string" + } + }, + "required": [ + "name", + "id", + "type" + ] + } + }, + "output-files": { + "id": "http://github.com/boutiques/boutiques-schema/output-files", + "type": "array", + "items": { + "id": "http://github.com/boutiques/boutiques-schema/output", + "type": "object", + "properties": { + "id": { + "id": "http://github.com/boutiques/boutiques-schema/output/id", + "minLength": 1, + "description": "A short, unique, informative identifier containing only alphanumeric characters and underscores. Typically used to generate variable names. Example: \"data_file\"", + "pattern": "^[0-9,_,a-z,A-Z]*$", + "type": "string" + }, + "name": { + "id": "http://github.com/boutiques/boutiques-schema/output/name", + "description": "A human-readable output name. Example: 'Data file'", + "minLength": 1, + "type": "string" + }, + "description": { + "id": "http://github.com/boutiques/boutiques-schema/output/description", + "description": "Output description.", + "minLength": 1, + "type": "string" + }, + "command-line-key": { + "id": "http://github.com/boutiques/boutiques-schema/output/command-line-key", + "description": "A string contained in command-line, substituted by the output value and/or flag at runtime.", + "minLength": 1, + "type": "string" + }, + "path-template": { + "id": "http://github.com/boutiques/boutiques-schema/output/path-template", + "description": "Describes the output file path relatively to the execution directory. May contain input command-line-keys. Example: \"results/[INPUT1]_brain.mnc\".", + "minLength": 1, + "type": "string" + }, + "list": { + "id": "http://github.com/boutiques/boutiques-schema/output/list", + "description": "True if output is a list of value.", + "type": "boolean" + }, + "optional":{ + "id": "http://github.com/boutiques/boutiques-schema/output/optional", + "description": "True if output may not be produced by the tool.", + "type": "boolean" + }, + "command-line-flag":{ + "id": "http://github.com/boutiques/boutiques-schema/output/command-line-flag", + "minLength": 1, + "description": "Option flag of the output, involved in the command-line-key substitution. Examples: -o, --output", + "type": "string" + } + + }, + "required": [ + "id", + "name", + "path-template" + ] + } + } + }, + "required": [ + "name", + "description", + "command-line", + "schema-version", + "inputs", + "output-files" + ] +} + diff --git a/BrainPortal/lib/cbrain_task_generators/templates/bourreau.rb.erb b/BrainPortal/lib/cbrain_task_generators/templates/bourreau.rb.erb new file mode 100644 index 000000000..d7dc7eef2 --- /dev/null +++ b/BrainPortal/lib/cbrain_task_generators/templates/bourreau.rb.erb @@ -0,0 +1,343 @@ + +# +# CBRAIN Project +# +# Copyright (C) 2008-2012 +# The Royal Institution for the Advancement of Learning +# McGill University +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# NOTE: This is a working template generated from a descriptor: +# [Schema] <%= schema['id'] %> +# [Schema version] <%= descriptor['schema-version'] %> +# [Tool] <%= descriptor['name'] %> +# [Version] <%= descriptor['tool-version'] || '?' %> +# See the CbrainTask Programmer Guide (CBRAIN Wiki) for a more complete picture +# of how CbrainTasks are constructed. +% # NOTE: This template's weird indentation is there to try and make the +% # generated code as legible as possible. + +# Bourreau-side CbrainTask subclass to launch <%= name %> +class CbrainTask::<%= name %> < ClusterTask + + Revision_info=CbrainFileRevision[__FILE__] #:nodoc: + + # Descriptor-based tasks are, by default, easily restartable and recoverable + include RestartableTask + include RecoverableTask + +% # Maximum width of a given +key+'s value in a +list+ of hashes +% max_width = lambda do |list, key| +% list.map { |i| i[key].to_s.length rescue 0 }.max +% end +% +% # Parameter groups +% inputs = descriptor['inputs'].dup +% outputs = descriptor['output-files'].dup +% files = inputs.select { |i| i['type'] == 'File' } +% required = outputs.select { |o| ! o['optional'] } +% globs = outputs.select { |o| o['list'] } +% in_keys = inputs.select { |i| i['command-line-key'] } +% out_keys = outputs.select { |o| o['command-line-key'] } +% flags = (inputs + outputs).select { |p| p['command-line-flag'] } +% + # Setup the cluster's environment to execute <%= name %>; create the relevant + # directories, prepare symlinks to input files, set environment variables, + # etc. Returns true if the task was correctly set up. + def setup #:nodoc: +% unless files.empty? + params = self.params + + # An error occured. Log +message+ and return false immediately. + retn = Proc.new { |r| return r } + error = lambda do |message| + self.addlog(message) + retn.(false) + end + + # Resolve all input userfiles IDs to their Userfile instances and set the + # results data provider if missing. +% resolve = format_call('resolve_file_parameter', files) { |file| [ +% ":'#{file['id']}'", +% 'error' +% ] } +% files.each do |file| + <%= resolve.(file) %> +% end + + # And make them all available to <%= name %> +% make_available = format_call('make_available', files) { |file| [ +% "params[:'#{file['id']}']", +% "'.'" +% ] } +% files.each do |file| + <%= make_available.(file) %> +% end + +% end + true + end + + # The set of shell commands to run on the cluster to execute <%= name %>. + # Any output on stdout or stderr will be captured and logged for information + # or debugging purposes. + # Note that this function also generates the list of output filenames + # in params. + def cluster_commands #:nodoc: +% if in_keys.empty? && out_keys.empty? +% unless outputs.empty? + # Output filenames + self.params.merge!({ +% id_width = max_width.(outputs, 'id') + ":''".length +% outputs.each do |output| + <%= + "%-#{id_width}s => %s" % [ + ":'#{output['id']}'", + "'#{output['path-template']}'," + ] + %> +% end + }) + +% end + # Command-line + [ '<%= descriptor['command-line'] %>' ] +% else + params = self.params + + # <%= name %>'s command line and output file names is constructed from a + # set of key-value pairs (keys) which are substituted in the command line + # and output templates. For example, if we have { '[1]' => '5' } for keys, + # a command line such as "foo [1] -e [1]" would turn into "foo 5 -e 5". + +% unless in_keys.empty? + # Substitution keys for input parameters + keys = { +% key_width = max_width.(in_keys, 'command-line-key') + "'".length +% in_keys.each do |key| + <%= + "'%-#{key_width}s => params[:'%s']," % [ + key['command-line-key'] + "'", + key['id'] + ] + %> +% end + } + +% end +% unless out_keys.empty? + # Substitution keys for output files +% if in_keys.empty? + keys = { +% else + keys.merge!({ +% end +% +% key_width = max_width.(out_keys, 'command-line-key') + "''".length +% path_width = max_width.(out_keys, 'path-template') + "'',".length +% out_keys.each do |key| + <%= + "%-#{key_width}s => substitute_keys(%-#{path_width}s keys)," % [ + "'#{key['command-line-key']}'", + "'#{key['path-template']}'," + ] + %> +% end + }) + +% end +% unless flags.empty? + # Input/output command-line flags used with keys in command-line + # substitution. + flags = { +% key_width = max_width.(flags, 'command-line-key') + "''".length +% flags.each do |flag| + <%= + "%-#{key_width}s => %s" % [ + "'#{flag['command-line-key']}'", + "'#{flag['command-line-flag']}'," + ] + %> +% end + } + +% end +% unless outputs.empty? + # Generate output filenames + params.merge!({ +% id_width = max_width.(outputs, 'id') + ":''".length +% path_width = max_width.(outputs, 'path-template') + "'',".length +% outputs.each do |output| + <%= + "%-#{id_width}s => substitute_keys(%-#{path_width}s keys)," % [ + ":'#{output['id']}'", + "'#{output['path-template']}'," + ] + %> +% end + }) + +% end + # Generate the final command-line to run <%= name %> + [ substitute_keys(<<-'CMD', keys<%= flags.empty? ? '' : ', flags' %>) ] + <%= descriptor['command-line'] %> + CMD +% end + end + + # Called after the task is done, this method saves <%= name %>'s output files + # to the Bourreau's cache and registers them into CBRAIN for later retrieval. + # Returns true on success. + def save_results #:nodoc: + # Additional checks to see if <%= name %> succeeded would belong here. + +% if outputs.empty? + # No output files to save; nothing to do + true +% else + # No matter how many errors occur, we need to save as many output + # files as possible and carry the error state to the end. + params = self.params + succeeded = true + + # Extract out the output files parameters from params. They will be re-added + # once their existence is validated and their registration into CBRAIN is + # complete. + outputs = params.extract!(*[ +% outputs.each do |output| + :'<%= output['id'] %>', +% end + ]) + +% unless required.empty? + # Make sure that every required output +path+ actually exists + # (or that its +glob+ matches something). + ensure_exists = lambda do |path| + return if File.exists(path) + self.addlog("Missing output file #{path}") + succeeded &&= false + end + ensure_matches = lambda do |glob| + return unless Dir.glob(glob).empty? + self.addlog("No output files matching #{glob}") + succeeded &&= false + end + +% required.select { |o| ! o['list'] }.each do |output| + ensure_exists(outputs[:'<%= output['id'] %>']) +% end +% required.select { |o| o['list'] }.each do |output| + ensure_matches(outputs[:'<%= output['id'] %>']) +% end + +% end +% unless globs.empty? + # Expand output file globs/patterns inside outputs for output file lists. + [ +% globs.each do |output| + :'<%= output['id'] %>', +% end + ].each do |param| + outputs[param] = Dir.glob(outputs[param]) + end + +% end + # Save (and register) all generated files to the results data provider + outputs.each do |param, paths| + paths = [paths] unless paths.is_a?(Enumerable) + paths.each do |path| + next unless path.present? && File.exists?(path) + + self.addlog("Saving result file #{path}") + name = File.basename(path) + + output = safe_userfile_find_or_new(( + File.directory?(path) ? FileCollection : Userfile.suggested_file_type(name) + ), :name => name) + unless output.save + self.addlog("Failed to save file #{path}") + succeeded &&= false + next + end + + output.cache_copy_from_local_file(path) + params[param] ||= [] + params[param] << output.id + self.addlog("Saved result file #{path}") +% if (single_file = files.first if files.count == 1 && ! files.first['list']) + + # As all output files were generated from a single input file, + # the outputs can all be made children of the one parent input file. + parent = params[:'<%= single_file['id'] %>'] + output.move_to_child_of(parent) + self.addlog_to_userfiles_these_created_these([parent], [output]) +% end + end + end + + succeeded +% end + end + + private + + # Generic helper methods + + # Make a given set of userfiles +files+ available to <%= name %> at + # +directory+. Simple variation on +ClusterTask+::+make_available+ + # to allow +files+ to be an Enumerable of files to make available under + # +directory+. + def make_available(files, directory) + files = [files] unless files.is_a?(Enumerable) + files.compact.each { |file| super(file, directory + '/') } + end + + # Resolve/replace input userfiles IDs for parameter +param+ + # to their Userfile instances, calling +failed+ if a file cannot be resolved. + # Also try and set the results data provider if its missing. + def resolve_file_parameter(param, failed) + value = params[param] + return if value.nil? + + files = (value.is_a?(Enumerable) ? value : [value]).map do |file| + file = Userfile.find_by_id(file) unless file.is_a?(Userfile) + failed.("Could not find file with ID #{file}") unless file + file + end + + params[param] = value.is_a?(Enumerable) ? files : files.first + self.results_data_provider_id ||= files.first.data_provider_id rescue nil + end + + # Substitute each parameter value in +keys+ in +str+ prepended by the + # corresponding flag in +flags+, if available. + # substitute_keys('f -e [1]', { '[1]' => 5 }) => 'f -e 5' + # substitute_keys('f -e [1]', { '[1]' => 5 }, { '[1]' => '-z' }) => 'f -e -z 5' + def substitute_keys(str, keys, flags = {}) + keys.inject(str) do |str, (key, value)| + flag = flags[key] + next str.gsub(key, flag) if flag && value == true + + value = (value.is_a?(Enumerable) ? value.dup : [value]) + .reject(&:nil?) + .map { |v| (v.is_a?(Userfile) ? v.name : v.to_s).bash_escape } + .join(' ') + + str.gsub(key, flag && value.present? ? "#{flag} #{value}" : value) + end + end + +end diff --git a/BrainPortal/lib/cbrain_task_generators/templates/edit_help.html.erb b/BrainPortal/lib/cbrain_task_generators/templates/edit_help.html.erb new file mode 100644 index 000000000..fc3fc3d35 --- /dev/null +++ b/BrainPortal/lib/cbrain_task_generators/templates/edit_help.html.erb @@ -0,0 +1,91 @@ + + + + +<%- +# NOTE: This template's weird indentation is there to try and make the +# generated code as legible as possible. +-%> + +<%- + # Parameter groups + params = descriptor['inputs'].select { |i| i['type'] != 'File' } + inputs = descriptor['inputs'].select { |i| i['type'] == 'File' } + outputs = descriptor['output-files'].dup +-%> +<%# Format a parameter (+param+) attributes as a list element (
  • ) -%> +<%- format_param = lambda do |param| -%> +
  • + + <%= param['name'] %> + <%- if param['command-line-flag']-%> + (<%= param['command-line-flag'] %>) + <%- end -%> + <%- if param['description'] -%> + : + <%= param['description'] %> + <%- else -%> + + <%- end -%> +
  • +<%- end -%> +

    <%= descriptor['name'] %>

    +<%- if descriptor['tool-version'] -%> +

    <%= descriptor['tool-version'] %>

    +<%- end -%> +
    + +<%- if descriptor['description'] -%> +<%= descriptor['description'] %> +
    + +<%- end -%> +<%- unless params.empty? -%> +

    Parameters

    +
      + <%- params.each { |param| format_param.(param) } -%> +
    + +<%- end -%> +<%- unless inputs.empty? -%> +

    Input files

    +
      + <%- inputs.each { |input| format_param.(input) } -%> +
    + +<%- end -%> +<%- unless outputs.empty? -%> +

    Output files

    +
      + <%- outputs.each { |output| format_param.(output) } -%> +
    +<%- end -%> diff --git a/BrainPortal/lib/cbrain_task_generators/templates/portal.rb.erb b/BrainPortal/lib/cbrain_task_generators/templates/portal.rb.erb new file mode 100644 index 000000000..5f9c1b6a7 --- /dev/null +++ b/BrainPortal/lib/cbrain_task_generators/templates/portal.rb.erb @@ -0,0 +1,299 @@ + +# +# CBRAIN Project +# +# Copyright (C) 2008-2012 +# The Royal Institution for the Advancement of Learning +# McGill University +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# NOTE: This is a working template generated from a descriptor: +# [Schema] <%= schema['id'] %> +# [Schema version] <%= descriptor['schema-version'] %> +# [Tool] <%= descriptor['name'] %> +# [Version] <%= descriptor['tool-version'] || '?' %> +# See the CbrainTask Programmer Guide (CBRAIN Wiki) for a more complete picture +# of how CbrainTasks are constructed. +% # NOTE: This template's weird indentation is there to try and make the +% # generated code as legible as possible. + +# Portal-side CbrainTask subclass to launch <%= name %> +class CbrainTask::<%= name %> < PortalTask + + Revision_info=CbrainFileRevision[__FILE__] #:nodoc: + +% # Maximum width of a given +key+'s value in a +list+ of hashes +% max_width = lambda do |list, key| +% list.map { |i| i[key].to_s.length rescue 0 }.max +% end +% +% # Parameter groups +% params = descriptor['inputs'].dup +% outputs = descriptor['output-files'].dup +% required = params.select { |i| ! i['optional'] } +% optional = params.select { |i| i['optional'] } +% defaults = params.select { |i| i['default-value'] } +% files = params.select { |i| i['type'] == 'File' } +% file_lists = files.select { |i| i['list'] } +% +% # Special case; we only have one file input parameter and it only +% # allows single files. +% single_file = files.first if files.count == 1 && file_lists.empty? +% if single_file +% # The parameter's validation is made in final_task_list and is no longer +% # optional if it was. +% params.delete(single_file) +% required.delete(single_file) +% optional.delete(single_file) +% end +% +% unless defaults.empty? + # Default values for some (all?) of <%= name %>'s parameters. Those values + # reflect the defaults taken by the tool's developer; feel free to change + # them to match your platform's requirements. + def self.default_launch_args #:nodoc: + { +% id_width = max_width.(defaults, 'id') + "'".length +% defaults.each do |default| + <%= + ":'%-#{id_width}s => %s," % [ + default['id'] + "'", + default['default-value'].inspect + ] + %> +% end + } + end + +% end + # Callback called just before the task's form is rendered. At this point, + # the task's params hash contains at least the default list of input + # userfiles under the key :interface_userfile_ids. + def before_form #:nodoc: +% file_types = descriptor['inputs'] +% .select { |i| ! i['optional'] && i['type'] == 'File' } +% .map { |i| i['cbrain-file-type'] } +% .uniq +% unless file_types.empty? + # Resolve interface_userfile_ids to actual userfile objects + files = Userfile.find_all_by_id(self.params[:interface_userfile_ids]) + +% if file_types.length == 1 && !file_types.first + # At least one file is required. + cb_error "Error: this task requires at least one input file" if files.empty? +% else + # Some input files are not optional and specific file types are + # required. Make sure the given input files are adequate. + + # Ensure that +files+ contains at least one file of type +type+ + ensure_one = lambda do |files, type| + type = type.constantize unless type.is_a?(Class) + cb_error "Error: this task requires at least one file of type '#{type.name}'" unless + files.any? { |f| f.is_a?(type) } + end + +% file_types.compact.each do |type| + ensure_one.(files, '<%= type %>') +% end +% end + +% end + "" + end + + # Callback called just after the task's form has been submitted by the user. + # At this point, all the task's params will be filled. This is where most + # validations happens. + def after_form #:nodoc: +% unless params.empty? + params = self.params + +% unless file_lists.empty? + # Assign the default input file list from interface_userfile_ids to unset + # file list parameters. + [ +% file_lists.each do |param| + :'<%= param['id'] %>', +% end + ].each do |list| + params[list] = params[:interface_userfile_ids].dup unless + params[list].is_a?(Enumerable) # Allows explicitly empty lists + end + +% end + # Sanitize every input parameter according to their expected type + +% sanitize_param = format_call('sanitize_param', params) { |param| [ +% ":'#{param['id']}'", +% ":#{param['type'].downcase}", +% (param['cbrain-file-type'] ? ":file_type => '#{param['cbrain-file-type']}'" : nil) +% ] } +% +% unless required.empty? + # Required parameters +% required.each do |param| + <%= sanitize_param.(param) %> +% end + +% end +% unless optional.empty? + # Optional parameters +% calls = optional.map { |param| [ sanitize_param.(param), param ] } +% call_width = calls.map { |c, p| c.length }.max +% calls.each do |call, param| + <%= "%-#{call_width}s unless params[:'%s'].nil?" % [ call, param['id'] ] %> +% end + +% end +% end + "" + end + + # Final set of tasks to be launched based on this task's parameters. Only + # useful if the parameters set for this task represent a set of tasks + # instead of just one. + def final_task_list #:nodoc: +% if single_file + # Create a list of tasks out of the default input file list + # (interface_userfile_ids), each file going into parameter '<%= single_file['id'] %>' + self.params[:interface_userfile_ids].map do |id| + task = self.dup + + # Set and sanitize the one file parameter for each id + task.params[:'<%= single_file['id']%>'] = id +% if single_file['cbrain-file-type'] + task.sanitize_param(:'<%= single_file['id'] %>', :file, :file_type => '<%= single_file['cbrain-file-type']%>') +% else + task.sanitize_param(:'<%= single_file['id'] %>', :file) +% end + + task.description ||= '' + task.description += " <%= single_file['id']%>: #{Userfile.find(id).name}" + task.description.strip! + task + end +% else + [ self ] +% end + end + + # Task parameters to leave untouched by the edit task mechanism. Usually + # for parameters added in after_form or final_task_list, as those wouldn't + # be present on the form and thus lost when the task is edited. + def untouchable_params_attributes #:nodoc: +% if outputs.empty? + { } +% else + # Output parameters will be present after the task has run and need to be + # preserved. + { +% id_width = max_width.(outputs, 'id') + "'".length +% outputs.each do |output| + <%= ":'%-#{id_width}s => true," % (output['id'] + "'") %> +% end + } +% end + end + + # Generic helper methods + + # Ensure that the parameter +name+ is not null and matches a generic tool + # parameter +type+ (:file, :numeric, :string or :flag) before converting the + # parameter's value to the corresponding Ruby type (if appropriate). + # For example, sanitize_param(:deviation, :numeric) would validate that + # self.params[:deviation] is a number and then convert it to a Ruby Float or + # Integer. + # + # Available +options+: + # [file_type] Userfile type to validate a parameter of +type+ :file against. + # + # If the parameter's value is an array, every value in the array is checked + # and expected to match +type+. + # + # Raises an exception for task parameter +name+ if the parameter's value + # is not adequate. + def sanitize_param(name, type, options = {}) + # Taken userfile names. An error will be raised if two input files have the + # same name. + @taken_files ||= Set.new + + # Fetch the parameter and convert to an Enumerable if required + values = self.params[name] rescue nil + values = [values] unless values.is_a?(Enumerable) + + # Validate and convert each value + values.map! do |value| + case type + # Try to convert to integer and then float. Cant? then its not a number. + when :number + if (number = Integer(value) rescue Float(value) rescue nil) + value = number + elsif value.blank? + params_errors.add(name, ": value missing") + else + params_errors.add(name, ": not a number (#{value})") + end + + # Nothing special required for strings, bar for symbols being acceptable strings. + when :string + value = value.to_s if value.is_a?(Symbol) + params_errors.add(name, " not a string (#{value})") unless value.is_a?(String) + + # Try to match against various common representation of true and false + when :flag + if value.is_a?(String) + value = true if value =~ /^(true|t|yes|y|on|1)$/i + value = false if value =~ /^(false|f|no|n|off|0|)$/i + end + + if ! [ true, false ].include?(value) + params_errors.add(name, ": not true or false (#{value})") + end + + # Make sure the file ID is valid, accessible, not already used and + # of the correct type. + when :file + unless (id = Integer(value) rescue nil) + params_errors.add(name, ": invalid or missing userfile (ID #{value})") + next value + end + + unless (file = Userfile.find_accessible_by_user(value, self.user)) + params_errors.add(name, ": cannot find userfile (ID #{value})") + next value + end + + if @taken_files.include?(file.name) + params_errors.add(name, ": file name already in use (#{file.name})") + else + @taken_files.add(file.name) + end + + if type = options[:file_type] + type = type.constantize unless type.is_a?(Class) + params_errors.add(name, ": incorrect userfile type (#{file.name})") if + type && ! file.is_a?(type) + end + end + + value + end + + # Store the value back + self.params[name] = values.first unless self.params[name].is_a?(Enumerable) + end + +end diff --git a/BrainPortal/lib/cbrain_task_generators/templates/show_params.html.erb.erb b/BrainPortal/lib/cbrain_task_generators/templates/show_params.html.erb.erb new file mode 100644 index 000000000..80dbe879d --- /dev/null +++ b/BrainPortal/lib/cbrain_task_generators/templates/show_params.html.erb.erb @@ -0,0 +1,120 @@ + +<%%- +# +# CBRAIN Project +# +# Copyright (C) 2008-2012 +# The Royal Institution for the Advancement of Learning +# McGill University +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +-%> + +<%%- +# NOTE: This is a working template generated from a descriptor: +# [Schema] <%= schema['id'] %> +# [Schema version] <%= descriptor['schema-version'] %> +# [Tool] <%= descriptor['name'] %> +# [Version] <%= descriptor['tool-version'] || '?' %> +# See the CbrainTask Programmer Guide (CBRAIN Wiki) for a more complete picture +# of how CbrainTasks are constructed. +-%> +<%- +# NOTE: This template's weird indentation is there to try and make the +# generated code as legible as possible. +-%> + +<%- + # Parameter groups + params = descriptor['inputs'].select { |i| i['type'] != 'File' } + inputs = descriptor['inputs'].select { |i| i['type'] == 'File' } + outputs = descriptor['output-files'].dup +-%> +<%- unless [params, inputs, outputs].all?(&:empty?) -%> +<%% + # Format a parameter (input or output) +value+ for display. Mostly for + # arrays (parameter lists). + format_param = lambda do |value| + value = [value] unless value.is_a?(Enumerable) + value.map(&:to_s).join(', ') + end + + # Format a set of one or more Userfile IDs for display. + # Similar to format_param. + format_files = lambda do |value| + value = [value] unless value.is_a?(Enumerable) + format_param.(value.map { |v| link_to_userfile_if_accessible(v) }).html_safe + end +%%> +<%- end -%> +
    +<%- if [params, inputs, outputs].all?(&:empty?) -%> + No parameters, no inputs, no ouputs. +<%- else -%> + + <%- unless params.empty? -%> + + + + + <%- params.each do |param| -%> + + + + + <%- end -%> + + <%- end -%> + <%- unless inputs.empty? -%> + + + + + <%- inputs.each do |input| -%> + + + + + <%- end -%> + + <%- end -%> + <%- unless outputs.empty? -%> + + + + + <%- outputs.each do |output| -%> + + + + + <%- end -%> + + <%- end -%> +
    + Parameters +
    <%= param['name'] %> + <%%= format_param.(params[:'<%= param['id'] %>']) %> +
    + Input files +
    <%= input['name'] %> + <%%= format_files.(params[:'<%= input['id'] %>']) %> +
    + Output files +
    <%= output['name'] %> + <%%= format_files.(params[:'<%= output['id'] %>']) %> +
    +<%- end -%> +
    diff --git a/BrainPortal/lib/cbrain_task_generators/templates/task_params.html.erb.erb b/BrainPortal/lib/cbrain_task_generators/templates/task_params.html.erb.erb new file mode 100644 index 000000000..c3ef92277 --- /dev/null +++ b/BrainPortal/lib/cbrain_task_generators/templates/task_params.html.erb.erb @@ -0,0 +1,476 @@ + +<%%- +# +# CBRAIN Project +# +# Copyright (C) 2008-2012 +# The Royal Institution for the Advancement of Learning +# McGill University +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +-%> + +<%%- +# NOTE: This is a working template generated from a descriptor: +# [Schema] <%= schema['id'] %> +# [Schema version] <%= descriptor['schema-version'] %> +# [Tool] <%= descriptor['name'] %> +# [Version] <%= descriptor['tool-version'] || '?' %> +# See the CbrainTask Programmer Guide (CBRAIN Wiki) for a more complete picture +# of how CbrainTasks are constructed. +-%> +<%- +# NOTE: This template's weird indentation is there to try and make the +# generated code as legible as possible. +-%> + +<%- + # Parameter groups + params = descriptor['inputs'].dup + required = params.select { |i| ! i['optional'] } + optional = params.select { |i| i['optional'] } + + # Single input file case + files = params.select { |i| i['type'] == 'File' } + single_file = files.first if files.count == 1 && ! files.any? { |f| f['list'] } +-%> +<%% input_files = Userfile.find_all_by_id(params[:interface_userfile_ids]) rescue [] %> + +<%%# + Generate a parameter label for HTML input with id +id+. + +name+ corresponds to the tool parameter name to display, + +optional+ indicates that the parameter is optional (or not) and + +flag+ is the parameter's command-line flag if available. +%%> +<%% label = lambda do |id, name, optional: false, flag: nil| %> + +<%% end %> + +<%%# + Generate a generic parameter input field with HTML id +id+ and name +name+. + +type+ is the kind of input field to generate (text or hidden), + +value+ is the input field's initial value, + +optional+ indicates that the parameter is optional (and the name should not + directly be placed on the tag) and + +placeholder+ is the placeholder text to fill the input with while awaiting + user input +%%> +<%% input = lambda do |id, name, type: 'text', value: nil, optional: false, placeholder: nil| %> + <%% value = @task.params[name.to_sym] if name && value.nil? %> + + id="<%%= id.to_la_id %>" + <%% end %> + class="tsk-prm-in" + type="<%%= type %>" + <%% if name %> + <%%= optional ? 'data-name' : 'name' %>="<%%= name.to_la %>" + <%% end %> + <%% if value %> + value="<%%= value %>" + <%% end %> + <%% if placeholder %> + placeholder="<%%= placeholder %>" + <%% end %> + /> +<%% end %> + +<%%# + Generate a list of inputs suitable for a list parameter with HTML id +id+ + and name +name+. +values+, +optional+ and +placeholder+ work similarly to + input's corresponding arguments. +%%> +<%% input_list = lambda do |id, name, values: nil, optional: false, placeholder: nil| %> + <%% + values = @task.params[name.to_sym] if name && values.nil? + values = ([values] unless values.is_a?(Enumerable)).compact + first = values.pop + %> +
      +
    • + <%% + input.(id, name, + value: first, + optional: optional, + placeholder: placeholder + ) + %> + +
    • + <%% values.each do |value| %> +
    • + <%% + input.(nil, name, + value: value, + optional: optional, + placeholder: placeholder + ) + %> + +
    • + <%% end %> +
    +<%% end %> + +<%%# + Generate a fancy checkbox for flag or optional parameters with HTML +id+. + If +name+ is given, a checkbox suitable for flag parameters is generated + while one for optional arguments is generated otherwhise. The checkbox's + initial state (checked or not) is +check+. +%%> +<%% checkbox = lambda do |id, name: nil, check: nil| %> + <%%# + FIXME for some obscure reason, naming the 'check' parameter 'checked' + makes the first parameter (id) become nil regardless of the value passed + in. This only seems to occur in the context of Rails' template renderer. + %> + <%% + id = name ? id.to_la_id : "tsk-prm-opt-#{id}" + type = name ? 'chk' : 'opt' + %> + <%% if name %> + <%% check = @task.params[name.to_sym] if check.nil? %> + <%%# + As a checkbox's value is not sent if left unchecked, a hidden 'back' field + sends the unchecked state is used to send. Rails overwrites this 'back' + value with the checbox's when the checkbox is checked, as the checkbox is + after the 'back' field. + %> + + + checked="checked" + <%% end %> + /> + <%% else %> + + checked="checked" + <%% end %> + /> + <%% end %> + +<%% end %> + +<%%# + Generate a drop-down list for a set of +options+ (value, label pairs) with + HTML id +id+ and name +name+. + +nothing+ corresponds to the text displayed if +options+ is empty and + +optional+ indicates if the parameter is optional (or not) +%%> +<%% dropdown = lambda do |id, name, options = [], nothing: '(Nothing to select)', optional: false| %> + <%% value = @task.params[name.to_sym] if name %> + <%% + input.(id, name, + type: 'hidden', + value: value, + optional: optional + ) + %> +
    + + <%% if options.empty? %> + + <%%= nothing %> + + <%% else %> + + <%% if (pair = options.select { |o| o.first == value }.first) %> + <%%= pair.last %> + <%% end %> + +
      + <%% options.each do |value, label| %> +
    • <%%= label %>
    • + <%% end %> +
    + <%% end %> +
    +<%% end %> + +<%%# Generate a parameter's description block for +desc+ %> +<%% description = lambda do |desc| %> + + <%%= desc %> + +<%% end %> + +<%# Generate a complete HTML block for a given parameter +param+ -%> +<%- parameter = lambda do |param| -%> + <%- + id = param['id'] + type = param['type'].downcase.to_sym + opt = !!(param['optional'] && type != :flag) + list = param['list'] + -%> + <%- + classes = [ 'tsk-prm', type.to_s ] + classes << 'list' if list + -%> + <%% id = '<%= id %>' %> +
  • + <%% + <%- if type == :flag -%> + # Flag input toggle + checkbox.(id, name: id) + + <%- end -%> + # Name/Label + label.(id, %q{ <%= param['name'] %> }, + <%- if param['command-line-flag'] -%> + optional: <%= param['optional'] %>, + flag: '<%= param['command-line-flag'] %>' + <%- else -%> + optional: <%= param['optional'] %> + <%- end -%> + ) + + <%- if opt -%> + # Optional parameter enable/disable toggle + checkbox.(id) + + <%- end -%> + <%- if type == :file -%> + <%- if single_file -%> + # Automatic single file input + dropdown.(id, id, + [], nothing: '(Automatically selected)', + optional: <%= opt %> + ) + <%- elsif param['cbrain-file-type'] -%> + # File input dropdown + type = '<%= param['cbrain-file-type'] %>'.constantize rescue nil + dropdown.(id, id, + input_files + .select { |f| f.is_a?(type) if type } + .map { |f| [ f.id.to_s, f.name ] }, + optional: <%= opt %> + ) + <%- else -%> + # File input dropdown + dropdown.(id, id, + input_files.map { |f| [ f.id.to_s, f.name ] }, + optional: <%= opt %> + ) + <%- end -%> + + <%- elsif [ :string, :number ].include?(type) -%> + <%- + arg_width = [ + 'optional', + ('placeholder' if type == :number), + ].compact.map(&:length).max + ':'.length + -%> + <%- if list -%> + # Input field list + input_list.(id, id, + <%- else -%> + # Input field + input.(id, id, + <%- end -%> + <%- if type == :number -%> + optional: <%= opt.to_s %>, + placeholder: '0.0' + <%- else -%> + optional: <%= opt.to_s %> + <%- end -%> + ) + + <%- end -%> + <%- if param['description'] -%> + # Description + description.(<<-'DESC') + <%= param['description'] %> + DESC + <%- end -%> + %> +
  • + +<%- end -%> +
    + <%- if params.empty? -%> + <%= name %> has no parameters. + <%- else -%> +
      + <%- required.each(¶meter) -%> + <%- optional.each(¶meter) -%> +
    + <%- end -%> +
    + + diff --git a/BrainPortal/public/stylesheets/cbrain.css b/BrainPortal/public/stylesheets/cbrain.css index 1b325b96c..1d77bf1d9 100644 --- a/BrainPortal/public/stylesheets/cbrain.css +++ b/BrainPortal/public/stylesheets/cbrain.css @@ -541,6 +541,10 @@ h1.warning { float: left; } +.cmd-flag { + color: #0471b4; +} + /* % ######################################################### */ /* % General styles for definition lists */ /* % ######################################################### */ @@ -1595,6 +1599,232 @@ th.center_align_heading { margin-right: 0em; } +/* Show task parameters */ +.task-show > table { + border: 0; +} + +.tsk-sw-hdr, +.tsk-sw-name, +.tsk-sw-val { + background: inherit; + border: 0; + text-align: left; +} + +.tsk-sw-hdr { + font-family: sans-serif; + font-size: 1.1em; + font-weight: bold; + padding: 30px 5px 10px 5px; + text-align: left; +} + +.tsk-sw-name { + padding-right: 40px; +} + +.tsk-sw-param > tr > .tsk-sw-hdr { + padding-top: 5px; +} + +.tsk-sw-param > tr > .tsk-sw-val { + font-family: monospace; +} + +/* Edit task parameters */ +.task-params > ul { + list-style: none; + padding-left: 20px; + padding: 0; +} + +.task-params > ul > li { + margin-bottom: 30px; + margin-top: 10px; +} + +/* label */ +.tsk-prm-lbl { + display: block; + font-size: 1.1em; + font-weight: bold; + margin-bottom: 5px; + margin-left: 25px; +} + +.tsk-prm-lbl > .required { + color: red; +} + +.tsk-prm.flag > .tsk-prm-lbl { + margin-left: 30px; +} + +/* optional */ +.tsk-prm-opt, +.tsk-prm-chk { + display: none; +} + +.tsk-prm-opt-lbl, +.tsk-prm-chk-lbl { + background: #ffffff; + border: 1px solid #888888; + display: inline-block; + float: left; + height: 22px; + width: 22px; +} + +.tsk-prm-opt-lbl { + border-right: 0; +} + +.tsk-prm-opt-lbl > .tsk-prm-opt-icon, +.tsk-prm-chk-lbl > .tsk-prm-chk-icon { + background-image: url("images/ui-icons_ffffff_256x240.png"); + margin-left: 3px; + margin-top: 2px; + visibility: hidden; +} + +.tsk-prm-opt:checked + .tsk-prm-opt-lbl, +.tsk-prm-opt[checked=checked] + .tsk-prm-opt-lbl, +.tsk-prm-chk:checked + .tsk-prm-chk-lbl, +.tsk-prm-chk[checked=checked] + .tsk-prm-chk-lbl { + background: #0471b4; +} + +.tsk-prm-opt:checked + .tsk-prm-opt-lbl > .tsk-prm-opt-icon, +.tsk-prm-opt[checked=checked] + .tsk-prm-opt-lbl > .tsk-prm-opt-icon, +.tsk-prm-chk:checked + .tsk-prm-chk-lbl > .tsk-prm-chk-icon, +.tsk-prm-chk[checked=checked] + .tsk-prm-chk-lbl > .tsk-prm-chk-icon { + visibility: visible; +} + +/* input */ +.tsk-prm-in, +.tsk-prm-sel { + background: #ffffff; + border: 1px solid #888888; + color: #000000; + display: block; + font-family: monospace; + font-size: 15px; + margin-left: 23px; + min-height: 18px; + padding: 2px 5px; + position: relative; + width: auto; +} + +.tsk-prm.string > .tsk-prm-in, +.tsk-prm.string > .tsk-prm-list > li > .tsk-prm-in { + width: 600px; +} + +.tsk-prm.number > .tsk-prm-in, +.tsk-prm.number > .tsk-prm-list > li > .tsk-prm-in, +.tsk-prm-sel { + min-width: 175px; +} + +/* selection */ +.tsk-prm-sel-lbl { + overflow: hidden; + white-space: nowrap; +} + +.tsk-prm-sel-lbl.disabled { + color: #888888; + font-style: italic; +} + +.tsk-prm-sel > .tsk-prm-sel-icon { + background-image: url("images/ui-icons_666666_256x240.png"); + float: right; + margin-top: 2px; +} + +.tsk-prm-sel-opt { + border: 1px solid #888888; + display: none; + left: -1px; + list-style: none; + max-height: 200px; + overflow-y: auto; + padding: 0; + position: absolute; + top: 22px; + min-width: 185px; + z-index: 501; +} + +.tsk-prm-sel-opt > li { + background: #ffffff; + color: #000000; + display: block; + font-family: monospace; + font-size: 15px; + padding: 4px 5px; + width: auto; +} + +.tsk-prm-sel-opt > li:hover { + background: #fff3d3; +} + +.tsk-prm-sel-opt > li:active { + background: #ffe49a; +} + +/* list */ +.tsk-prm-list { + list-style: none; + margin-left: 23px; + padding: 0; +} + +.tsk-prm-list > li { + margin-bottom: 3px; +} + +.tsk-prm-list > li > .tsk-prm-in { + display: inline-block; + margin-left: 0; +} + +.tsk-prm-list > li > .tsk-prm-add, +.tsk-prm-list > li > .tsk-prm-rm { + background-image: url("images/ui-icons_666666_256x240.png"); + display: inline-block; + cursor: pointer; + position: relative; + top: 3px; +} + +.tsk-prm-add:hover, +.tsk-prm-rm:hover { + background-color: #dddddd; +} + +.tsk-prm-add:active, +.tsk-prm-rm:active { + background-color: #ffffff; +} + +/* description */ +.tsk-prm-desc { + display: block; + margin-top: 5px; + margin-left: 25px; + max-width: 600px; +} + +.tsk-prm.flag > .tsk-prm-desc { + margin-left: 30px; +} /* % ######################################################### */ From b0eff7b5443f01744f3d158e8c7bd0fff9587ead Mon Sep 17 00:00:00 2001 From: Remi Bernard Date: Fri, 24 Jul 2015 16:05:37 -0400 Subject: [PATCH 02/85] Automatic JSON descriptor integration (auto-load) Descriptors can now be added to plugins under cbrain_task_descriptors/ in the plugin's root directory. Once the plugin is installed, these descriptors are then automatically loaded as CbrainTask subclasses as with the rest of the plugin's task classes. No Ruby source files are created by the generator; the task views and autoloading process have been modified to avoid that requirement. --- Bourreau/Gemfile | 1 + Bourreau/config/application.rb | 14 +- Bourreau/lib/cbrain_task_generators | 1 + BrainPortal/app/models/cluster_task.rb | 19 +- BrainPortal/app/models/portal_task.rb | 19 +- BrainPortal/app/views/tasks/_params.html.erb | 19 +- BrainPortal/app/views/tasks/show.html.erb | 7 +- BrainPortal/config/application.rb | 14 +- BrainPortal/config/initializers/cbrain.rb | 7 +- .../schema_task_generator.rb | 34 +- .../schemas/boutiques.schema.json | 358 +++++++++--------- .../templates/edit_help.html.erb | 6 +- BrainPortal/lib/tasks/cbrain_plugins.rake | 129 +++---- 13 files changed, 341 insertions(+), 287 deletions(-) create mode 120000 Bourreau/lib/cbrain_task_generators diff --git a/Bourreau/Gemfile b/Bourreau/Gemfile index c4d0edba4..dd632c5f4 100644 --- a/Bourreau/Gemfile +++ b/Bourreau/Gemfile @@ -32,6 +32,7 @@ gem "mysql2" gem "sys-proctable", :git => 'git://github.com/djberg96/sys-proctable.git' gem "thin" gem "pbkdf2-ruby" +gem "json-schema" group :development, :test do gem "wirble" diff --git a/Bourreau/config/application.rb b/Bourreau/config/application.rb index 908d06d1f..736e2560d 100644 --- a/Bourreau/config/application.rb +++ b/Bourreau/config/application.rb @@ -37,6 +37,10 @@ class Application < Rails::Application # Custom directories with classes and modules you want to be autoloadable. # config.autoload_paths += %W(#{config.root}/extras) config.autoload_paths += Dir["#{config.root}/lib"] + config.autoload_paths += Dir["#{config.root}/lib/cbrain_task_generators"] + + # CBRAIN Plugins load paths: add directories for each Userfile model + config.autoload_paths += Dir[ * Dir.glob("#{config.root}/cbrain_plugins/installed-plugins/userfiles/*") ] # CBRAIN Plugins load paths: add directory for the CbrainTask models # This directory contains symbolic links to a special loader code @@ -47,8 +51,14 @@ class Application < Rails::Application # properly set up all tasks installed from plugins (and the defaults tasks). config.autoload_paths += Dir["#{config.root}/cbrain_plugins/installed-plugins/cbrain_task"] - # CBRAIN Plugins load paths: add directories for each Userfile model - config.autoload_paths += Dir[ * Dir.glob("#{config.root}/cbrain_plugins/installed-plugins/userfiles/*") ] + # CBRAIN Plugins load paths: add directory for descriptor-based CbrainTask + # models. This directory, similarly to the one above, contains symbolic + # links to a special loader code which will call a task generator to + # generate the requested CbrainTask subclass on the fly. + # + # The rake task cbrain:plugins:install:all also takes care of creating the + # symlinks for this location. + config.autoload_paths += Dir["#{config.root}/cbrain_plugins/installed-plugins/cbrain_task_descriptors"] # Only load the plugins named here, in the order given (default is alphabetical). # :all can be used as a placeholder for all plugins not explicitly named. diff --git a/Bourreau/lib/cbrain_task_generators b/Bourreau/lib/cbrain_task_generators new file mode 120000 index 000000000..6cc485eef --- /dev/null +++ b/Bourreau/lib/cbrain_task_generators @@ -0,0 +1 @@ +../../BrainPortal/lib/cbrain_task_generators \ No newline at end of file diff --git a/BrainPortal/app/models/cluster_task.rb b/BrainPortal/app/models/cluster_task.rb index 50c0b5f8c..070f07525 100644 --- a/BrainPortal/app/models/cluster_task.rb +++ b/BrainPortal/app/models/cluster_task.rb @@ -1674,14 +1674,17 @@ def task_is_proper_subclass #:nodoc: end # Patch: pre-load all model files for the subclasses -Dir.chdir(CBRAIN::TasksPlugins_Dir) do - Dir.glob("*.rb").each do |model| - next if model == "cbrain_task_class_loader.rb" - model.sub!(/.rb$/,"") - unless CbrainTask.const_defined? model.classify - #puts_blue "Loading CbrainTask subclass #{model.classify} from #{model}.rb ..." - require_dependency "#{CBRAIN::TasksPlugins_Dir}/#{model}.rb" +[ CBRAIN::TasksPlugins_Dir, CBRAIN::TaskDescriptorsPlugins_Dir ].each do |dir| + Dir.chdir(dir) do + Dir.glob("*.rb").each do |model| + next if [ + 'cbrain_task_class_loader.rb', + 'cbrain_task_descriptor_loader.rb' + ].include?(model) + + model.sub!(/.rb$/, '') + require_dependency "#{dir}/#{model}.rb" unless + CbrainTask.const_defined? model.classify end end end - diff --git a/BrainPortal/app/models/portal_task.rb b/BrainPortal/app/models/portal_task.rb index 7c77bdb28..e4c647cf3 100644 --- a/BrainPortal/app/models/portal_task.rb +++ b/BrainPortal/app/models/portal_task.rb @@ -702,14 +702,17 @@ def task_is_proper_subclass #:nodoc: end # Patch: pre-load all model files for the subclasses -Dir.chdir(CBRAIN::TasksPlugins_Dir) do - Dir.glob("*.rb").each do |model| - next if model == "cbrain_task_class_loader.rb" - model.sub!(/.rb$/,"") - unless CbrainTask.const_defined? model.classify -# puts_blue "Loading CbrainTask subclass #{model.classify} from #{model}.rb ..." - require_dependency "#{CBRAIN::TasksPlugins_Dir}/#{model}.rb" +[ CBRAIN::TasksPlugins_Dir, CBRAIN::TaskDescriptorsPlugins_Dir ].each do |dir| + Dir.chdir(dir) do + Dir.glob("*.rb").each do |model| + next if [ + 'cbrain_task_class_loader.rb', + 'cbrain_task_descriptor_loader.rb' + ].include?(model) + + model.sub!(/.rb$/, '') + require_dependency "#{dir}/#{model}.rb" unless + CbrainTask.const_defined? model.classify end end end - diff --git a/BrainPortal/app/views/tasks/_params.html.erb b/BrainPortal/app/views/tasks/_params.html.erb index 909381b61..c5588ddb4 100644 --- a/BrainPortal/app/views/tasks/_params.html.erb +++ b/BrainPortal/app/views/tasks/_params.html.erb @@ -22,9 +22,14 @@ # -%> +<% locals = { :params => @task.params, :form => form } %> +

    Task Parameters - <% public_path = @task.public_path("edit_params_help.html") %> - <% if public_path %> + <% if @task.class.respond_to?(:raw_partial) %> + <%= overlay_content_link "(Help)", :class => "task_help_link", :enclosing_element => "span" do %> + <%= render :inline => @task.class.raw_partial(:edit_help), :locals => locals %> + <% end %> + <% elsif (public_path = @task.public_path("edit_params_help.html")) %> <%= overlay_ajax_link "(Help)", public_path.to_s, :class => "task_help_link" %> <% end %> : @@ -32,7 +37,9 @@
    <%= error_messages_for(@task) %> -<%= render :partial => task_partial('task_params'), - :locals => { :params => @task.params, :form => form } - %> -
    \ No newline at end of file +<% if @task.class.respond_to?(:raw_partial) %> + <%= render :inline => @task.class.raw_partial(:task_params), :locals => locals %> +<% else %> + <%= render :partial => task_partial('task_params'), :locals => locals %> +<% end %> + diff --git a/BrainPortal/app/views/tasks/show.html.erb b/BrainPortal/app/views/tasks/show.html.erb index 00fb7acc2..6be2002a8 100644 --- a/BrainPortal/app/views/tasks/show.html.erb +++ b/BrainPortal/app/views/tasks/show.html.erb @@ -141,7 +141,12 @@ <%= build_tabs do |tb| %> <%= tb.tab("Summary") do %> <% begin %> - <%= render :partial => task_partial('show_params'), :locals => { :task => @task, :params => @task.params } %> + <% locals = { :task => @task, :params => @task.params } %> + <% if @task.class.respond_to?(:raw_partial) %> + <%= render :inline => @task.class.raw_partial(:show_params), :locals => locals %> + <% else %> + <%= render :partial => task_partial('show_params'), :locals => locals %> + <% end %> <% rescue ActionView::MissingTemplate %>
    Problem loading summary view (no template provided by task author).
    <% rescue => ex %> diff --git a/BrainPortal/config/application.rb b/BrainPortal/config/application.rb index a816e6871..32fdf7056 100644 --- a/BrainPortal/config/application.rb +++ b/BrainPortal/config/application.rb @@ -37,6 +37,10 @@ class Application < Rails::Application # Custom directories with classes and modules you want to be autoloadable. # config.autoload_paths += %W(#{config.root}/extras) config.autoload_paths += Dir["#{config.root}/lib"] + config.autoload_paths += Dir["#{config.root}/lib/cbrain_task_generators"] + + # CBRAIN Plugins load paths: add directories for each Userfile model + config.autoload_paths += Dir[ * Dir.glob("#{config.root}/cbrain_plugins/installed-plugins/userfiles/*") ] # CBRAIN Plugins load paths: add directory for the CbrainTask models # This directory contains symbolic links to a special loader code @@ -47,8 +51,14 @@ class Application < Rails::Application # properly set up all tasks installed from plugins (and the defaults tasks). config.autoload_paths += Dir["#{config.root}/cbrain_plugins/installed-plugins/cbrain_task"] - # CBRAIN Plugins load paths: add directories for each Userfile model - config.autoload_paths += Dir[ * Dir.glob("#{config.root}/cbrain_plugins/installed-plugins/userfiles/*") ] + # CBRAIN Plugins load paths: add directory for descriptor-based CbrainTask + # models. This directory, similarly to the one above, contains symbolic + # links to a special loader code which will call a task generator to + # generate the requested CbrainTask subclass on the fly. + # + # The rake task cbrain:plugins:install:all also takes care of creating the + # symlinks for this location. + config.autoload_paths += Dir["#{config.root}/cbrain_plugins/installed-plugins/cbrain_task_descriptors"] # Only load the plugins named here, in the order given (default is alphabetical). # :all can be used as a placeholder for all plugins not explicitly named. diff --git a/BrainPortal/config/initializers/cbrain.rb b/BrainPortal/config/initializers/cbrain.rb index b5f29ace4..1e10fad39 100644 --- a/BrainPortal/config/initializers/cbrain.rb +++ b/BrainPortal/config/initializers/cbrain.rb @@ -43,9 +43,10 @@ class CBRAIN "PID-#{Process.pid}" # CBRAIN plugins locations - Plugins_Dir = "#{Rails.root.to_s}/cbrain_plugins" - TasksPlugins_Dir = "#{Plugins_Dir}/installed-plugins/cbrain_task" # singular; historical - UserfilesPlugins_Dir = "#{Plugins_Dir}/installed-plugins/userfiles" + Plugins_Dir = "#{Rails.root.to_s}/cbrain_plugins" + UserfilesPlugins_Dir = "#{Plugins_Dir}/installed-plugins/userfiles" + TasksPlugins_Dir = "#{Plugins_Dir}/installed-plugins/cbrain_task" # singular; historical + TaskDescriptorsPlugins_Dir = "#{Plugins_Dir}/installed-plugins/cbrain_task_descriptors" $CBRAIN_StartTime_Revision = "???" # Will be filled in by validation script diff --git a/BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb b/BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb index 511a65a98..e5b316d07 100644 --- a/BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb +++ b/BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb @@ -26,11 +26,17 @@ # files. The generated code can be added right away to CBRAIN's available tasks # or written to file for later modification. # -# NOTE: Only JSON is currently supported +# NOTE: Only JSON and a single schema (boutiques) is currently supported module SchemaTaskGenerator Revision_info=CbrainFileRevision[__FILE__] #:nodoc: + # Directory where descriptor schemas are located + SCHEMA_DIR = "#{Rails.root.to_s}/lib/cbrain_task_generators/schemas" + + # Default schema file to use when validating auto-loaded descriptors + DEFAULT_SCHEMA_FILE = 'boutiques.schema.json' + # Represents a schema to validate task descriptors against class Schema @@ -150,14 +156,20 @@ def integrate(register = true) # The newly created Tool and ToolConfig (if needed) will initially belong # to the core admin. + name = @descriptor['name'] + version = @descriptor['tool-version'] || '(unknown)' + description = @descriptor['description'] || '' + docker_image = @descriptor['docker-image'] + resource = RemoteResource.current_resource + # Create and save a new Tool for this task, unless theres already one. Tool.new( - :name => @descriptor['name'], + :name => name, :user_id => User.admin.id, :group_id => User.admin.own_group.id, :category => "scientific tool", :cbrain_task_class => task.to_s, - :description => @descriptor['description'] + :description => description ).save! unless Tool.exists?(:cbrain_task_class => task.to_s) @@ -168,15 +180,16 @@ def integrate(register = true) ToolConfig.new( :tool_id => task.tool.id, - :bourreau_id => RemoteResource.current_resource.id, + :bourreau_id => resource.id, :group_id => Group.everyone.id, - :version_name => @descriptor['tool-version'], - :docker_image => @descriptor['docker-image'] + :version_name => version, + :description => "#{name} #{version} on #{resource.name}", + :docker_image => docker_image ).save! unless ToolConfig.exists?( :tool_id => task.tool.id, :bourreau_id => RemoteResource.current_resource.id, - :version_name => @descriptor['tool-version'] + :version_name => version ) end @@ -246,6 +259,13 @@ def self.generate(schema, descriptor, strict_validation = true) ) end + # Returns the default Schema instance to use when validating descriptors + # without a specific schema or when auto-loading descriptors. + # (constructed from DEFAULT_SCHEMA_FILE) + def self.default_schema + @@default_schema ||= Schema.new("#{SCHEMA_DIR}/#{DEFAULT_SCHEMA_FILE}") + end + # Utility method to convert a JSON string or file path into a hash. # Returns the hash directly if a hash is given. def self.expand_json(obj) diff --git a/BrainPortal/lib/cbrain_task_generators/schemas/boutiques.schema.json b/BrainPortal/lib/cbrain_task_generators/schemas/boutiques.schema.json index 0d3d3f501..22c2c0335 100644 --- a/BrainPortal/lib/cbrain_task_generators/schemas/boutiques.schema.json +++ b/BrainPortal/lib/cbrain_task_generators/schemas/boutiques.schema.json @@ -4,188 +4,186 @@ "type": "object", "title": "Tool", "properties": { - "name": { - "id": "http://github.com/boutiques/boutiques-schema/name", - "minLength": 1, - "description": "Tool name.", - "type": "string" - }, - "tool-version": { - "id": "http://github.com/boutiques/boutiques-schema/description", - "minLength": 1, - "description": "Tool version.", - "type": "string" - }, - "description": { - "id": "http://github.com/boutiques/boutiques-schema/description", - "minLength": 1, - "description": "Tool description.", - "type": "string" - }, - "command-line": { - "id": "http://github.com/boutiques/boutiques-schema/command-line", - "minLength": 1, - "description": "A string that describes the tool command line, where input and output values are identified by \"keys\". At runtime, command-line keys are substituted with flags and values.", - "type": "string" - }, - "docker-image": { - "id": "http://github.com/boutiques/boutiques-schema/docker-image", - "minLength": 1, - "description": "Name of a Docker image where tool is installed and configured. Example: docker.io/neurodebian.", - "type": "string" - }, - "docker-index": { - "id": "http://github.com/boutiques/boutiques-schema/docker-index", - "minLength": 1, - "description": "Docker index where Docker image is available.", - "default": "http://index.docker.io", - "type": "string" - }, - "schema-version": { - "id": "http://github.com/boutiques/boutiques-schema/schema-version", - "type": "string", - "description": "Version of the schema used.", - "enum": ["0.2-snapshot"] - }, - "inputs": { - "id": "http://github.com/boutiques/boutiques-schema/inputs", - "type": "array", - "items": { - "id": "http://github.com/boutiques/boutiques-schema/input", - "type": "object", - "properties": { - "id": { - "id": "http://github.com/boutiques/boutiques-schema/input/id", - "minLength": 1, - "description": "A short, unique, informative identifier containing only alphanumeric characters and underscores. Typically used to generate variable names. Example: \"data_file\".", - "type": "string", - "pattern": "^[0-9,_,a-z,A-Z]*$" - }, - "name": { - "id": "http://github.com/boutiques/boutiques-schema/input/name", - "minLength": 1, - "description": "A human-readable input name. Example: 'Data file'.", - "type": "string" - }, - "type": { - "id": "http://github.com/boutiques/boutiques-schema/input/type", - "type": "string", - "description": "Input type.", - "enum": ["String", "File", "Flag" , "Number" ] - }, - "description": { - "id": "http://github.com/boutiques/boutiques-schema/input/description", - "minLength": 1, - "description": "Input description.", - "type": "string" - }, - "command-line-key": { - "id": "http://github.com/boutiques/boutiques-schema/input/command-line-key", - "minLength": 1, - "description": "A string contained in command-line, substituted by the input value and/or flag at runtime.", - "type": "string" - }, - "list": { - "id": "http://github.com/boutiques/boutiques-schema/input/list", - "description":"True if input is a list of value. An input of type \"Flag\" cannot be a list.", - "type": "boolean" - }, - "optional":{ - "id": "http://github.com/boutiques/boutiques-schema/input/optional", - "description": "True if input is optional.", - "type": "boolean" - }, - "command-line-flag":{ - "id": "http://github.com/boutiques/boutiques-schema/input/command-line-flag", - "minLength": 1, - "description": "Option flag of the input, involved in the command-line-key substitution. Inputs of type \"Flag\" have to have a command-line flag. Examples: -v, --force.", - "type": "string" - }, - "default-value":{ - "id": "http://github.com/boutiques/boutiques-schema/input/default-value", - "minLength": 1, - "description": "Default value of the input, used by the tool when no option is specified.", - "type": "string" - } - }, - "required": [ - "name", - "id", - "type" - ] - } - }, - "output-files": { - "id": "http://github.com/boutiques/boutiques-schema/output-files", - "type": "array", - "items": { - "id": "http://github.com/boutiques/boutiques-schema/output", - "type": "object", - "properties": { - "id": { - "id": "http://github.com/boutiques/boutiques-schema/output/id", - "minLength": 1, - "description": "A short, unique, informative identifier containing only alphanumeric characters and underscores. Typically used to generate variable names. Example: \"data_file\"", - "pattern": "^[0-9,_,a-z,A-Z]*$", - "type": "string" - }, - "name": { - "id": "http://github.com/boutiques/boutiques-schema/output/name", - "description": "A human-readable output name. Example: 'Data file'", - "minLength": 1, - "type": "string" - }, - "description": { - "id": "http://github.com/boutiques/boutiques-schema/output/description", - "description": "Output description.", - "minLength": 1, - "type": "string" - }, - "command-line-key": { - "id": "http://github.com/boutiques/boutiques-schema/output/command-line-key", - "description": "A string contained in command-line, substituted by the output value and/or flag at runtime.", - "minLength": 1, - "type": "string" - }, - "path-template": { - "id": "http://github.com/boutiques/boutiques-schema/output/path-template", - "description": "Describes the output file path relatively to the execution directory. May contain input command-line-keys. Example: \"results/[INPUT1]_brain.mnc\".", - "minLength": 1, - "type": "string" - }, - "list": { - "id": "http://github.com/boutiques/boutiques-schema/output/list", - "description": "True if output is a list of value.", - "type": "boolean" - }, - "optional":{ - "id": "http://github.com/boutiques/boutiques-schema/output/optional", - "description": "True if output may not be produced by the tool.", - "type": "boolean" - }, - "command-line-flag":{ - "id": "http://github.com/boutiques/boutiques-schema/output/command-line-flag", - "minLength": 1, - "description": "Option flag of the output, involved in the command-line-key substitution. Examples: -o, --output", - "type": "string" - } - - }, - "required": [ - "id", - "name", - "path-template" - ] - } - } + "name": { + "id": "http://github.com/boutiques/boutiques-schema/name", + "minLength": 1, + "description": "Tool name.", + "type": "string" + }, + "tool-version": { + "id": "http://github.com/boutiques/boutiques-schema/description", + "minLength": 1, + "description": "Tool version.", + "type": "string" + }, + "description": { + "id": "http://github.com/boutiques/boutiques-schema/description", + "minLength": 1, + "description": "Tool description.", + "type": "string" + }, + "command-line": { + "id": "http://github.com/boutiques/boutiques-schema/command-line", + "minLength": 1, + "description": "A string that describes the tool command line, where input and output values are identified by \"keys\". At runtime, command-line keys are substituted with flags and values.", + "type": "string" + }, + "docker-image": { + "id": "http://github.com/boutiques/boutiques-schema/docker-image", + "minLength": 1, + "description": "Name of a Docker image where tool is installed and configured. Example: docker.io/neurodebian.", + "type": "string" + }, + "docker-index": { + "id": "http://github.com/boutiques/boutiques-schema/docker-index", + "minLength": 1, + "description": "Docker index where Docker image is available.", + "default": "http://index.docker.io", + "type": "string" + }, + "schema-version": { + "id": "http://github.com/boutiques/boutiques-schema/schema-version", + "type": "string", + "description": "Version of the schema used.", + "enum": ["0.2-snapshot"] + }, + "inputs": { + "id": "http://github.com/boutiques/boutiques-schema/inputs", + "type": "array", + "items": { + "id": "http://github.com/boutiques/boutiques-schema/input", + "type": "object", + "properties": { + "id": { + "id": "http://github.com/boutiques/boutiques-schema/input/id", + "minLength": 1, + "description": "A short, unique, informative identifier containing only alphanumeric characters and underscores. Typically used to generate variable names. Example: \"data_file\".", + "type": "string", + "pattern": "^[0-9,_,a-z,A-Z]*$" + }, + "name": { + "id": "http://github.com/boutiques/boutiques-schema/input/name", + "minLength": 1, + "description": "A human-readable input name. Example: 'Data file'.", + "type": "string" + }, + "type": { + "id": "http://github.com/boutiques/boutiques-schema/input/type", + "type": "string", + "description": "Input type.", + "enum": ["String", "File", "Flag" , "Number" ] + }, + "description": { + "id": "http://github.com/boutiques/boutiques-schema/input/description", + "minLength": 1, + "description": "Input description.", + "type": "string" + }, + "command-line-key": { + "id": "http://github.com/boutiques/boutiques-schema/input/command-line-key", + "minLength": 1, + "description": "A string contained in command-line, substituted by the input value and/or flag at runtime.", + "type": "string" + }, + "list": { + "id": "http://github.com/boutiques/boutiques-schema/input/list", + "description":"True if input is a list of value. An input of type \"Flag\" cannot be a list.", + "type": "boolean" + }, + "optional":{ + "id": "http://github.com/boutiques/boutiques-schema/input/optional", + "description": "True if input is optional.", + "type": "boolean" + }, + "command-line-flag":{ + "id": "http://github.com/boutiques/boutiques-schema/input/command-line-flag", + "minLength": 1, + "description": "Option flag of the input, involved in the command-line-key substitution. Inputs of type \"Flag\" have to have a command-line flag. Examples: -v, --force.", + "type": "string" + }, + "default-value":{ + "id": "http://github.com/boutiques/boutiques-schema/input/default-value", + "minLength": 1, + "description": "Default value of the input, used by the tool when no option is specified." + } + }, + "required": [ + "name", + "id", + "type" + ] + } + }, + "output-files": { + "id": "http://github.com/boutiques/boutiques-schema/output-files", + "type": "array", + "items": { + "id": "http://github.com/boutiques/boutiques-schema/output", + "type": "object", + "properties": { + "id": { + "id": "http://github.com/boutiques/boutiques-schema/output/id", + "minLength": 1, + "description": "A short, unique, informative identifier containing only alphanumeric characters and underscores. Typically used to generate variable names. Example: \"data_file\"", + "pattern": "^[0-9,_,a-z,A-Z]*$", + "type": "string" + }, + "name": { + "id": "http://github.com/boutiques/boutiques-schema/output/name", + "description": "A human-readable output name. Example: 'Data file'", + "minLength": 1, + "type": "string" + }, + "description": { + "id": "http://github.com/boutiques/boutiques-schema/output/description", + "description": "Output description.", + "minLength": 1, + "type": "string" + }, + "command-line-key": { + "id": "http://github.com/boutiques/boutiques-schema/output/command-line-key", + "description": "A string contained in command-line, substituted by the output value and/or flag at runtime.", + "minLength": 1, + "type": "string" + }, + "path-template": { + "id": "http://github.com/boutiques/boutiques-schema/output/path-template", + "description": "Describes the output file path relatively to the execution directory. May contain input command-line-keys. Example: \"results/[INPUT1]_brain.mnc\".", + "minLength": 1, + "type": "string" + }, + "list": { + "id": "http://github.com/boutiques/boutiques-schema/output/list", + "description": "True if output is a list of value.", + "type": "boolean" + }, + "optional":{ + "id": "http://github.com/boutiques/boutiques-schema/output/optional", + "description": "True if output may not be produced by the tool.", + "type": "boolean" + }, + "command-line-flag":{ + "id": "http://github.com/boutiques/boutiques-schema/output/command-line-flag", + "minLength": 1, + "description": "Option flag of the output, involved in the command-line-key substitution. Examples: -o, --output", + "type": "string" + } + }, + "required": [ + "id", + "name", + "path-template" + ] + } + } }, "required": [ - "name", - "description", - "command-line", - "schema-version", - "inputs", - "output-files" + "name", + "description", + "command-line", + "schema-version", + "inputs", + "output-files" ] } diff --git a/BrainPortal/lib/cbrain_task_generators/templates/edit_help.html.erb b/BrainPortal/lib/cbrain_task_generators/templates/edit_help.html.erb index fc3fc3d35..8bce1e8f8 100644 --- a/BrainPortal/lib/cbrain_task_generators/templates/edit_help.html.erb +++ b/BrainPortal/lib/cbrain_task_generators/templates/edit_help.html.erb @@ -58,10 +58,12 @@ <%- end -%> <%- end -%> -

    <%= descriptor['name'] %>

    +

    + <%= descriptor['name'] %> <%- if descriptor['tool-version'] -%> -

    <%= descriptor['tool-version'] %>

    + <%= descriptor['tool-version'] %> <%- end -%> +
    <%- if descriptor['description'] -%> diff --git a/BrainPortal/lib/tasks/cbrain_plugins.rake b/BrainPortal/lib/tasks/cbrain_plugins.rake index 24b93365d..32020c8a5 100644 --- a/BrainPortal/lib/tasks/cbrain_plugins.rake +++ b/BrainPortal/lib/tasks/cbrain_plugins.rake @@ -31,10 +31,11 @@ namespace :cbrain do # Unfortunately we don't have access to cbrain.rb where some useful constants are defined in the # CBRAIN class, such as CBRAIN::TasksPlugins_Dir ; if we ever change where plugins are stored, we # have to update this here and the cbrain.rb file too. - plugins_dir = Rails.root + "cbrain_plugins" - installed_plugins_dir = plugins_dir + "installed-plugins" - tasks_plugins_dir = installed_plugins_dir + "cbrain_task" - userfiles_plugins_dir = installed_plugins_dir + "userfiles" + plugins_dir = Rails.root + "cbrain_plugins" + installed_plugins_dir = plugins_dir + "installed-plugins" + userfiles_plugins_dir = installed_plugins_dir + "userfiles" + tasks_plugins_dir = installed_plugins_dir + "cbrain_task" + descriptors_plugins_dir = installed_plugins_dir + "cbrain_task_descriptors" Dir.chdir(plugins_dir.to_s) do packages = Dir.glob('*').reject { |path| path =~ /^(installed-plugins)$/ }.select { |f| File.directory?(f) } @@ -46,64 +47,57 @@ namespace :cbrain do puts "Checking plugins in package '#{package}'..." Dir.chdir(package) do - # Setup each userfile plugin - - files = Dir.glob('userfiles/*').select { |f| File.directory?(f) } - puts "Found #{files.size} file(s) to set up..." - files.each do |u_slash_f| # "userfiles/abcd" - myfile = Pathname.new(u_slash_f).basename.to_s # "abcd" - symlink_location = userfiles_plugins_dir + myfile - plugin_location = plugins_dir + package + u_slash_f - symlink_value = plugin_location.relative_path_from(symlink_location.parent) - #puts "#{u_slash_f} #{myfile}\n TS=#{symlink_location}\n PL=#{plugin_location}\n LL=#{symlink_value}" - - if File.exists?(symlink_location) - if File.symlink?(symlink_location) - if File.readlink(symlink_location) == symlink_value.to_s - puts "-> Userfile already setup: '#{myfile}'." + # Setup a single unit (userfiles, tasks or descriptors) + setup = lambda do |glob, name, directory, condition: nil, after: nil| + files = Dir.glob(glob) + files.select!(&condition) if condition + puts "Found #{files.size} #{name}(s) to set up..." + files.each do |u_slash_f| + plugin = Pathname.new(u_slash_f).basename.to_s + symlink_location = directory + plugin + plugin_location = plugins_dir + package + u_slash_f + symlink_value = plugin_location.relative_path_from(symlink_location.parent) + + if File.exists?(symlink_location) + if File.symlink?(symlink_location) + if File.readlink(symlink_location) == symlink_value.to_s + puts "-> #{name.capitalize} already setup: '#{plugin}'." + next + end + puts "-> Error: there is already a symlink with an unexpected value here:\n #{symlink_location}" next end - puts "-> Error: there is already a symlink with an unexpected value here:\n #{symlink_location}" + puts "-> Error: there is already an entry (file or directory) here:\n #{symlink_location}" next end - puts "-> Error: there is already an entry (file or directory) here:\n #{symlink_location}" - next - end - puts "-> Creating symlink for userfile '#{myfile}'." - File.symlink symlink_value, symlink_location - #puts " #{symlink_value} as #{symlink_location}" + puts "-> Creating symlink for #{name} '#{plugin}'." + File.symlink symlink_value, symlink_location + + after.(symlink_location) if after + end end + # Setup each userfile plugin + setup.('userfiles/*', 'userfile', userfiles_plugins_dir, + condition: lambda { |f| File.directory?(f) } + ) # Setup each cbrain_task plugin - - tasks = Dir.glob('cbrain_task/*').select { |f| File.directory?(f) } - puts "Found #{tasks.size} tasks(s) to set up..." - tasks.each do |u_slash_t| # "cbrain_task/abcd" - mytask = Pathname.new(u_slash_t).basename.to_s # "abcd" - symlink_location = tasks_plugins_dir + mytask - plugin_location = plugins_dir + package + u_slash_t - symlink_value = plugin_location.relative_path_from(symlink_location.parent) - - if File.exists?(symlink_location) - if File.symlink?(symlink_location) - if File.readlink(symlink_location) == symlink_value.to_s - puts "-> Task already setup: '#{mytask}'." - next - end - puts "-> Error: there is already a symlink with an unexpected value here:\n #{symlink_location}" - next - end - puts "-> Error: there is already an entry (file or directory) here:\n #{symlink_location}" - next + setup.('cbrain_task/*', 'task', tasks_plugins_dir, + condition: lambda { |f| File.directory?(f) }, + after: lambda do |symlink_location| + File.symlink "cbrain_task_class_loader.rb", "#{symlink_location}.rb" end + ) - puts "-> Creating symlink for task '#{mytask}'." - File.symlink symlink_value, symlink_location - File.symlink "cbrain_task_class_loader.rb", "#{symlink_location}.rb" # intelligent loader wrapper - #puts " #{symlink_value} as #{symlink_location}" - end + # Setup each cbrain_task descriptor plugin + setup.('cbrain_task_descriptors/*', 'descriptor', descriptors_plugins_dir, + condition: lambda { |f| File.extname(f) == '.json' }, + after: lambda do |symlink_location| + File.symlink "cbrain_task_descriptor_loader.rb", "#{symlink_location.sub(/.json$/, '.rb')}" + end + ) end # chdir package end # each package @@ -203,26 +197,25 @@ namespace :cbrain do # Unfortunately we don't have access to cbrain.rb where some useful constants are defined in the # CBRAIN class, such as CBRAIN::TasksPlugins_Dir ; if we ever change where plugins are stored, we # have to update this here and the cbrain.rb file too. - plugins_dir = Rails.root + "cbrain_plugins" - installed_plugins_dir = plugins_dir + "installed-plugins" - tasks_plugins_dir = installed_plugins_dir + "cbrain_task" - userfiles_plugins_dir = installed_plugins_dir + "userfiles" - - puts "Erasing all symlinks for userfiles installed from CBRAIN plugins..." - Dir.chdir(userfiles_plugins_dir.to_s) do - Dir.glob('*').select { |f| File.symlink?(f) }.each do |f| - puts "-> Erasing link for userfile '#{f}'." - File.unlink(f) + plugins_dir = Rails.root + "cbrain_plugins" + installed_plugins_dir = plugins_dir + "installed-plugins" + userfiles_plugins_dir = installed_plugins_dir + "userfiles" + tasks_plugins_dir = installed_plugins_dir + "cbrain_task" + descriptors_plugins_dir = installed_plugins_dir + "cbrain_task_descriptors" + + erase = lambda do |name, dir| + puts "Erasing all symlinks for #{name.pluralize} installed from CBRAIN plugins..." + Dir.chdir(dir.to_s) do + Dir.glob('*').select { |f| File.symlink?(f) }.each do |f| + puts "-> Erasing link for #{name} '#{f}'." + File.unlink(f) + end end end - puts "Erasing all symlinks for tasks installed from CBRAIN plugins..." - Dir.chdir(tasks_plugins_dir.to_s) do - Dir.glob('*').select { |f| File.symlink?(f) }.each do |f| - puts "-> Erasing link for task '#{f}'." - File.unlink(f) - end - end + erase.('userfile', userfiles_plugins_dir) + erase.('task', tasks_plugins_dir) + erase.('descriptor', descriptors_plugins_dir) end From 57d07e87ce10b2eca93b3568653e4d6c432ee460 Mon Sep 17 00:00:00 2001 From: Remi Bernard Date: Tue, 28 Jul 2015 15:44:49 -0400 Subject: [PATCH 03/85] Version switcher CbrainTask wrapper, minor fixes Switching between multiple tool versions is now supported via a special 'versions switcher' class which wraps the classes generated by the generator and turns itself into the correct generated CbrainTask subclass when a tool config is assigned. Note that this functionality currently breaks if the admin manually adds another tool config with a different version without a matching descriptor. --- BrainPortal/app/models/cluster_task.rb | 2 +- BrainPortal/app/models/portal_task.rb | 2 +- BrainPortal/app/views/tasks/_params.html.erb | 15 +- BrainPortal/app/views/tasks/show.html.erb | 11 +- .../schema_task_generator.rb | 185 ++++++++++++++++-- .../templates/bourreau.rb.erb | 6 +- .../templates/portal.rb.erb | 2 +- 7 files changed, 196 insertions(+), 27 deletions(-) diff --git a/BrainPortal/app/models/cluster_task.rb b/BrainPortal/app/models/cluster_task.rb index 070f07525..347c9a40b 100644 --- a/BrainPortal/app/models/cluster_task.rb +++ b/BrainPortal/app/models/cluster_task.rb @@ -1684,7 +1684,7 @@ def task_is_proper_subclass #:nodoc: model.sub!(/.rb$/, '') require_dependency "#{dir}/#{model}.rb" unless - CbrainTask.const_defined? model.classify + [ model.classify, model.camelize ].any? { |m| CbrainTask.const_defined?(m) rescue nil } end end end diff --git a/BrainPortal/app/models/portal_task.rb b/BrainPortal/app/models/portal_task.rb index e4c647cf3..0085574d0 100644 --- a/BrainPortal/app/models/portal_task.rb +++ b/BrainPortal/app/models/portal_task.rb @@ -712,7 +712,7 @@ def task_is_proper_subclass #:nodoc: model.sub!(/.rb$/, '') require_dependency "#{dir}/#{model}.rb" unless - CbrainTask.const_defined? model.classify + [ model.classify, model.camelize ].any? { |m| CbrainTask.const_defined?(m) rescue nil } end end end diff --git a/BrainPortal/app/views/tasks/_params.html.erb b/BrainPortal/app/views/tasks/_params.html.erb index c5588ddb4..c870f878e 100644 --- a/BrainPortal/app/views/tasks/_params.html.erb +++ b/BrainPortal/app/views/tasks/_params.html.erb @@ -22,12 +22,17 @@ # -%> -<% locals = { :params => @task.params, :form => form } %> +<% + locals = { :params => @task.params, :form => form } + raw_partial = lambda do |partial| + @task.class.raw_partial(partial) if @task.class.respond_to?(:raw_partial) + end +%>

    Task Parameters - <% if @task.class.respond_to?(:raw_partial) %> + <% if raw_partial.(:edit_help) %> <%= overlay_content_link "(Help)", :class => "task_help_link", :enclosing_element => "span" do %> - <%= render :inline => @task.class.raw_partial(:edit_help), :locals => locals %> + <%= render :inline => raw_partial.(:edit_help), :locals => locals %> <% end %> <% elsif (public_path = @task.public_path("edit_params_help.html")) %> <%= overlay_ajax_link "(Help)", public_path.to_s, :class => "task_help_link" %> @@ -37,8 +42,8 @@
    <%= error_messages_for(@task) %> -<% if @task.class.respond_to?(:raw_partial) %> - <%= render :inline => @task.class.raw_partial(:task_params), :locals => locals %> +<% if raw_partial.(:task_params) %> + <%= render :inline => raw_partial.(:task_params), :locals => locals %> <% else %> <%= render :partial => task_partial('task_params'), :locals => locals %> <% end %> diff --git a/BrainPortal/app/views/tasks/show.html.erb b/BrainPortal/app/views/tasks/show.html.erb index 6be2002a8..d3cae6c5c 100644 --- a/BrainPortal/app/views/tasks/show.html.erb +++ b/BrainPortal/app/views/tasks/show.html.erb @@ -141,9 +141,14 @@ <%= build_tabs do |tb| %> <%= tb.tab("Summary") do %> <% begin %> - <% locals = { :task => @task, :params => @task.params } %> - <% if @task.class.respond_to?(:raw_partial) %> - <%= render :inline => @task.class.raw_partial(:show_params), :locals => locals %> + <% + locals = { :task => @task, :params => @task.params } + raw_partial = lambda do |partial| + @task.class.raw_partial(partial) if @task.class.respond_to?(:raw_partial) + end + %> + <% if raw_partial.(:show_params) %> + <%= render :inline => raw_partial.(:show_params), :locals => locals %> <% else %> <%= render :partial => task_partial('show_params'), :locals => locals %> <% end %> diff --git a/BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb b/BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb index e5b316d07..93a2ba1de 100644 --- a/BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb +++ b/BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb @@ -114,9 +114,19 @@ def initialize(attributes) # Integrates the encapsulated CbrainTask in this CBRAIN installation. # Unless +register+ is specified to be false, this method will add the # required Tool and ToolConfig if necessary for the CbrainTask to be - # useable right away (Since almost all information required to make the + # useable right away (since almost all information required to make the # Tool and ToolConfig objects is available in the spec). - def integrate(register = true) + # Also, unless +multi_version+ is specified to be false, this method will + # wrap the encapsulated CbrainTask in a version switcher class to allow + # different CbrainTask classes for each tool version. + # Returns the newly generated CbrainTask subclass. + def integrate(register: true, multi_version: true) + # Make sure the task class about to be generated does not already exist, + # to avoid mixing the classes up. + name = SchemaTaskGenerator.classify(@name) + Object.send(:remove_const, name) if Object.const_defined?(name) + CbrainTask.send(:remove_const, name) if CbrainTask.const_defined?(name) + # As the same code is used to dynamically load tasks descriptors and # create task templates, the class definitions are generated as strings # (Otherwise the source wouldn't be available to write down the generated @@ -125,8 +135,8 @@ def integrate(register = true) eval @source[Rails.root.to_s =~ /BrainPortal$/ ? :portal : :bourreau] # Try and retrieve the just-generated task class - task = @name.camelize.constantize rescue nil - task ||= "CbrainTask::#{@name.camelize}".constantize + task = name.constantize rescue nil + task ||= "CbrainTask::#{name}".constantize # Since the task class doesn't have a matching cbrain_plugins directory # tree, some methods need to be added/redefined to ensure the cooperation @@ -146,23 +156,48 @@ def integrate(register = true) :task_params => generated.source[:task_params], :show_params => generated.source[:show_params], :edit_help => generated.source[:edit_help] - })[partial]; + })[partial] end - return unless register + # If multi-versioning is enabled, replace the task class object constant + # in CbrainTask (or Object) by a version switcher wrapper class. + if multi_version + # Build the corresponding switcher and add the task's version and class + # to it. + version = @descriptor['tool-version'] + switcher = SchemaTaskGenerator.version_switcher(name) + switcher.known_versions[version] = task + + # Redefine the CbrainTask or Object constant pointing to the task's + # class to point to the switcher instead. + mod = [ Object, CbrainTask ] + .select { |m| m.const_defined?(name) } + .first + if mod + mod.send(:remove_const, name) + mod.const_set(name, switcher) + end + end # With the task class and descriptor, we have enough information to # generate a Tool and ToolConfig to register the tool into CBRAIN. - # The newly created Tool and ToolConfig (if needed) will initially belong - # to the core admin. + register(task) if register + + task + end + # Register a newly generated CbrainTask subclass (+task+) in this CBRAIN + # installation, creating the appropriate Tool and ToolConfig objects from + # the information contained in the descriptor. The newly created Tool and + # ToolConfig will initially belong to the core admin. + def register(task) name = @descriptor['name'] version = @descriptor['tool-version'] || '(unknown)' description = @descriptor['description'] || '' docker_image = @descriptor['docker-image'] resource = RemoteResource.current_resource - # Create and save a new Tool for this task, unless theres already one. + # Create and save a new Tool for the task, unless theres already one. Tool.new( :name => name, :user_id => User.admin.id, @@ -173,7 +208,7 @@ def integrate(register = true) ).save! unless Tool.exists?(:cbrain_task_class => task.to_s) - # Create and save a new ToolConfig for this task on this server, unless + # Create and save a new ToolConfig for the task on this server, unless # theres already one. Only applies to Bourreaux (as it would make no # sense on the portal). return if Rails.root.to_s =~ /BrainPortal$/ @@ -188,7 +223,7 @@ def integrate(register = true) ).save! unless ToolConfig.exists?( :tool_id => task.tool.id, - :bourreau_id => RemoteResource.current_resource.id, + :bourreau_id => resource.id, :version_name => version ) end @@ -231,7 +266,7 @@ def to_directory(path) # validation issues. def self.generate(schema, descriptor, strict_validation = true) descriptor = self.expand_json(descriptor) - name = descriptor['name'].camelize + name = self.classify(descriptor['name']) schema = Schema.new(schema) unless schema.is_a?(Schema) errors = schema.send( strict_validation ? :'validate!' : :validate, @@ -259,6 +294,123 @@ def self.generate(schema, descriptor, strict_validation = true) ) end + # Generate (or retrieve if it has been generated already) a version switcher + # class for CbrainTask subclasses named +name+. The version switcher class + # will behave just like a blank CbrainTask subclass until it is assigned + # a ToolConfig. It will then replace its methods with the ones from the + # CbrainTask subclass corresponding to that particular version: + # class A < PortalTask + # def f; :a; end + # end + # + # class B < PortalTask + # def f; :b; end + # end + # + # s = version_switcher('A') + # s.known_versions['1.1'] = A + # s.known_versions['1.2'] = B + # + # s.tool_config = ToolConfig.new(:version => '1.1') + # s.f # :a + def self.version_switcher(name) + base = Rails.root.to_s =~ /BrainPortal$/ ? PortalTask : ClusterTask + @@version_switchers ||= {} + @@version_switchers[name] ||= Class.new(base) do + + # Versions known to this version switcher and their associated CbrainTask + # subclasses. + def self.known_versions + class_variable_set(:@@known_versions, {}) unless + class_variable_defined?(:@@known_versions) + + class_variable_get(:@@known_versions) + end + + # Add a few singleton methods on the object to perform a version switch + # once the tool config is set. + after_initialize do + # FIXME: the simplest and most straightforward way to make the version + # switcher task instance become an instance of the version-specific + # class would be to directly change the instance's class. + # At the time of writing, this is impossible in Ruby. + # This method (+as_version+) tries its best to mimic the missing + # functionality. + + # Convert this blank CbrainTask object (instance of the version switcher + # class) to a more-or-less real instance of the class corresponding to + # +version+ by including all of its methods in, replacing the defaults + # from PortalTask or ClusterTask. + define_singleton_method(:as_version) do |version| + cb_error "Unknown or invalid version '#{version}'" unless + (version_class = self.class.known_versions[version]) + + # Use the Ruby 2.0 refinement API to include version_class methods + # inside this object's singleton class (or metaclass) + metaclass = class << self; self; end + metaclass.include(Module.new { include refine(version_class) { } }) + + # And try to make the object appear to be a version_class. + define_singleton_method(:class) { version_class } + define_singleton_method(:kind_of?) { |klass| is_a?(klass) } + define_singleton_method(:is_a?) do |klass| + klass <= version_class || super(klass) + end + define_singleton_method(:instance_of?) do |klass| + klass == version_class || super(klass) + end + + # An object can only be given methods for a single version, and + # exactly once. Conflicts and odd issues could occur otherwise. + # Thus, there is no longer a need for :as_version or the tool_config + # setter hooks. + [ :as_version, :tool_config=, :tool_config_id= ].each do |m| + metaclass.send(:remove_method, m) rescue nil + end + end + + # If we dont have a tool config already, try to catch the exact moment + # when the version switcher instance gets assigned its tool config and + # invoke as_version when it happens. + if self.tool_config + self.as_version(self.tool_config.version_name) + else + [ :tool_config=, :tool_config_id= ].each do |method| + define_singleton_method(method) do |*args| + value = super(*args) + self.as_version(self.tool_config.version_name) if self.tool_config + value + end + end + end + end + + # Just like generated task classes, the version switcher doesn't have a + # cbrain_plugins directory structure and needs a few methods for views + # and controllers, adjusted to reflect that a ToolConfig is needed to + # access the real task class. + + # No public path + def self.public_path(public_file) + nil + end + + # No generated source (yet) + def self.generated_from + nil + end + + # Stubbed out raw view partials + def self.raw_partial(partial) + ({ + :task_params => %q{ No version specified }, + :show_params => %q{ No version specified } + })[partial] + end + + end + end + # Returns the default Schema instance to use when validating descriptors # without a specific schema or when auto-loading descriptors. # (constructed from DEFAULT_SCHEMA_FILE) @@ -274,6 +426,15 @@ def self.expand_json(obj) JSON.parse!(File.exists?(obj) ? IO.read(obj) : obj) end + # Utility method to convert a string (+str+) to an identifier suitable for a + # Ruby class name. Similar to Rails' classify, but tries to handle more cases. + def self.classify(str) + str.gsub!('-', '_') + str.gsub!(/\W/, '') + str.gsub!(/^\d/, '') + str.camelize + end + private # Utility/helper methods used in templates. diff --git a/BrainPortal/lib/cbrain_task_generators/templates/bourreau.rb.erb b/BrainPortal/lib/cbrain_task_generators/templates/bourreau.rb.erb index d7dc7eef2..b0d84c7fc 100644 --- a/BrainPortal/lib/cbrain_task_generators/templates/bourreau.rb.erb +++ b/BrainPortal/lib/cbrain_task_generators/templates/bourreau.rb.erb @@ -237,10 +237,10 @@ class CbrainTask::<%= name %> < ClusterTask end % required.select { |o| ! o['list'] }.each do |output| - ensure_exists(outputs[:'<%= output['id'] %>']) + ensure_exists.(outputs[:'<%= output['id'] %>']) % end % required.select { |o| o['list'] }.each do |output| - ensure_matches(outputs[:'<%= output['id'] %>']) + ensure_matches.(outputs[:'<%= output['id'] %>']) % end % end @@ -292,8 +292,6 @@ class CbrainTask::<%= name %> < ClusterTask % end end - private - # Generic helper methods # Make a given set of userfiles +files+ available to <%= name %> at diff --git a/BrainPortal/lib/cbrain_task_generators/templates/portal.rb.erb b/BrainPortal/lib/cbrain_task_generators/templates/portal.rb.erb index 5f9c1b6a7..265da8a03 100644 --- a/BrainPortal/lib/cbrain_task_generators/templates/portal.rb.erb +++ b/BrainPortal/lib/cbrain_task_generators/templates/portal.rb.erb @@ -170,7 +170,7 @@ class CbrainTask::<%= name %> < PortalTask # Create a list of tasks out of the default input file list # (interface_userfile_ids), each file going into parameter '<%= single_file['id'] %>' self.params[:interface_userfile_ids].map do |id| - task = self.dup + task = self.clone # Set and sanitize the one file parameter for each id task.params[:'<%= single_file['id']%>'] = id From 43cd13f20a78894cd017415f6939fa7f00f0b03d Mon Sep 17 00:00:00 2001 From: Remi Bernard Date: Tue, 28 Jul 2015 16:10:03 -0400 Subject: [PATCH 04/85] Missing version fallback as_version now tries to fall back to the first defined tool version if the given version cannot be found in known_versions. --- .../schema_task_generator.rb | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb b/BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb index 93a2ba1de..db6d75c5b 100644 --- a/BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb +++ b/BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb @@ -342,8 +342,22 @@ def self.known_versions # +version+ by including all of its methods in, replacing the defaults # from PortalTask or ClusterTask. define_singleton_method(:as_version) do |version| - cb_error "Unknown or invalid version '#{version}'" unless - (version_class = self.class.known_versions[version]) + # Try to get the version-specific class corresponding to +version+, + # falling back on the first defined version in known_versions if it + # cannot be found. + known = self.class.known_versions + unless (version_class = known[version]) + cb_error "No known versions for #{self.class.name}!?" unless + known.present? + + logger.warn( + "WARNING: " + + "Unknown version #{version} for #{self.class.name}, " + + "using #{known.first[0]} instead" + ) + + version, version_class = known.first + end # Use the Ruby 2.0 refinement API to include version_class methods # inside this object's singleton class (or metaclass) From b2f67b0b609a16cda5877491fbc05153be46a535 Mon Sep 17 00:00:00 2001 From: Remi Bernard Date: Tue, 28 Jul 2015 18:00:36 -0400 Subject: [PATCH 05/85] Rails generator for descriptor-based CbrainTasks The new generator, descriptor_task, uses SchemaTaskGenerator to generate a CbrainTask source set and outputs it in the CBRAIN plugin format (just like the to_directory method) under cbrain_plugins/cbrain-plugins-local. The source descriptor, schema and strict mode are specified as command-line arguments. --- .../lib/generators/descriptor_task/USAGE | 23 +++++++ .../descriptor_task_generator.rb | 60 +++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 BrainPortal/lib/generators/descriptor_task/USAGE create mode 100644 BrainPortal/lib/generators/descriptor_task/descriptor_task_generator.rb diff --git a/BrainPortal/lib/generators/descriptor_task/USAGE b/BrainPortal/lib/generators/descriptor_task/USAGE new file mode 100644 index 000000000..e1289b29e --- /dev/null +++ b/BrainPortal/lib/generators/descriptor_task/USAGE @@ -0,0 +1,23 @@ +Description: + Generate a working CbrainTask template from a JSON descriptor. This template + can be used as a working base for adding custom functionality to a CbrainTask + beyond what is automatically generated by CBRAIN's descriptor integration + mechanism. + + If no schema (--schema) is specified, CBRAIN's default JSON descriptor + schema is used. + +Example: + + rails generate descriptor_task ~/descriptors/my_tool.json + + will create: + + cbrain_plugins/cbrain-plugins-local/cbrain_task/portal/my_tool.rb + cbrain_plugins/cbrain-plugins-local/cbrain_task/bourreau/my_tool.rb + cbrain_plugins/cbrain-plugins-local/cbrain_task/views/_task_params.html.erb + cbrain_plugins/cbrain-plugins-local/cbrain_task/views/_show_params.html.erb + cbrain_plugins/cbrain-plugins-local/cbrain_task/views/public/edit_params_help.html + + Note that the path component 'cbrain-plugin-local' is a name given to a + dummy CBRAIN plugins package; feel free to rename it as needed. diff --git a/BrainPortal/lib/generators/descriptor_task/descriptor_task_generator.rb b/BrainPortal/lib/generators/descriptor_task/descriptor_task_generator.rb new file mode 100644 index 000000000..81ebe8fe9 --- /dev/null +++ b/BrainPortal/lib/generators/descriptor_task/descriptor_task_generator.rb @@ -0,0 +1,60 @@ + +# +# CBRAIN Project +# +# Copyright (C) 2008-2012 +# The Royal Institution for the Advancement of Learning +# McGill University +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +class DescriptorTaskGenerator < Rails::Generators::Base + + Revision_info=CbrainFileRevision[__FILE__] #:nodoc: + + source_root File.expand_path('.', __FILE__) + + argument :json_descriptor, :type => :string, + :desc => %q{JSON descriptor to generate the CbrainTask subclass from} + + class_option :schema, :type => :string, :required => false, + :desc => %q{JSON descriptor schema to validate the descriptor with} + + class_option :strict, :type => :boolean, :default => false, + :desc => %q{Abort generation if the provided descriptor doesn't validate} + + def create_task #:nodoc: + generated = SchemaTaskGenerator.generate( + options[:schema] || SchemaTaskGenerator.default_schema, + json_descriptor, + options[:strict] + ) + + name, source = generated.name, generated.source + base = "cbrain_plugins/cbrain-plugins-local/cbrain_task/#{name}" + + empty_directory "#{base}" + empty_directory "#{base}/portal" + empty_directory "#{base}/bourreau" + empty_directory "#{base}/views/public" + + create_file "#{base}/portal/#{generated.name}.rb", source[:portal] + create_file "#{base}/bourreau/#{generated.name}.rb", source[:bourreau] + create_file "#{base}/views/_task_params.html.erb", source[:task_params] + create_file "#{base}/views/_show_params.html.erb", source[:show_params] + create_file "#{base}/views/public/edit_params_help.html", source[:edit_help] + end + +end From f8b26fc1244443d7b1c68844528cdf4b5314b25e Mon Sep 17 00:00:00 2001 From: Remi Bernard Date: Thu, 30 Jul 2015 18:51:38 -0400 Subject: [PATCH 06/85] Disabled version switching, minor fixes The currently implemented way of specializing the 'version switcher' instance to the version-specific class (bulk-importing methods) crashes Ruby when trying to garbage-collect the object. At the time of writing, there is no known good way to implemented the required functionality in Ruby without resorting to even deeper and messier hacks. As such, the functionality has been disabled for now (its off by default, and neither the auto-loader or Rails generator enable it). This changeset also adds a warning when a to-be-generated task class has already been defined before and corrects some minor issues. --- .../schema_task_generator.rb | 63 +++++++++++-------- .../templates/portal.rb.erb | 2 +- 2 files changed, 39 insertions(+), 26 deletions(-) diff --git a/BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb b/BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb index db6d75c5b..6d9b4c9ee 100644 --- a/BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb +++ b/BrainPortal/lib/cbrain_task_generators/schema_task_generator.rb @@ -116,16 +116,22 @@ def initialize(attributes) # required Tool and ToolConfig if necessary for the CbrainTask to be # useable right away (since almost all information required to make the # Tool and ToolConfig objects is available in the spec). - # Also, unless +multi_version+ is specified to be false, this method will - # wrap the encapsulated CbrainTask in a version switcher class to allow - # different CbrainTask classes for each tool version. + # Also, if +multi_version+ is specified, this method will wrap the + # encapsulated CbrainTask in a version switcher class to allow different + # CbrainTask classes for each tool version. # Returns the newly generated CbrainTask subclass. - def integrate(register: true, multi_version: true) + def integrate(register: true, multi_version: false) # Make sure the task class about to be generated does not already exist, # to avoid mixing the classes up. name = SchemaTaskGenerator.classify(@name) - Object.send(:remove_const, name) if Object.const_defined?(name) - CbrainTask.send(:remove_const, name) if CbrainTask.const_defined?(name) + [ Object, CbrainTask ].select { |m| m.const_defined?(name) }.each do |m| + Rails.logger.warn( + "WARNING: #{name} is already defined in #{m.name}; " + + "undefining to avoid collisions" + ) unless multi_version + + m.send(:remove_const, name) + end # As the same code is used to dynamically load tasks descriptors and # create task templates, the class definitions are generated as strings @@ -170,12 +176,9 @@ def integrate(register: true, multi_version: true) # Redefine the CbrainTask or Object constant pointing to the task's # class to point to the switcher instead. - mod = [ Object, CbrainTask ] - .select { |m| m.const_defined?(name) } - .first - if mod - mod.send(:remove_const, name) - mod.const_set(name, switcher) + [ Object, CbrainTask ].select { |m| m.const_defined?(name) }.each do |m| + m.send(:remove_const, name) + m.const_set(name, switcher) end end @@ -337,6 +340,16 @@ def self.known_versions # This method (+as_version+) tries its best to mimic the missing # functionality. + # FIXME: unfortunately, while the technique +as_version+ uses is + # more-or-less sound Ruby-wise (it bulk-imports the version class + # instance methods into the version switcher instance's singleton + # class), it apparently overrides/messes up some sensitive core Ruby + # methods which make Ruby segfault when the object is garbage collected. + + # As such, this version switching functionality is not currently in use, + # for lack of a working technique to try to 'convert' the version + # switcher instance. + # Convert this blank CbrainTask object (instance of the version switcher # class) to a more-or-less real instance of the class corresponding to # +version+ by including all of its methods in, replacing the defaults @@ -351,18 +364,26 @@ def self.known_versions known.present? logger.warn( - "WARNING: " + - "Unknown version #{version} for #{self.class.name}, " + - "using #{known.first[0]} instead" + "WARNING: Unknown version #{version} for #{self.class.name}, " + + "using #{known.first[0]} instead." ) version, version_class = known.first end + # An object can only be given methods for a single version, and + # exactly once. Conflicts and odd issues could occur otherwise. + # Thus, there is no longer a need for :as_version or the tool_config + # setter hooks. + [ :as_version, :tool_config=, :tool_config_id= ].each do |m| + self.singleton_class.send(:remove_method, m) rescue nil + end + # Use the Ruby 2.0 refinement API to include version_class methods # inside this object's singleton class (or metaclass) - metaclass = class << self; self; end - metaclass.include(Module.new { include refine(version_class) { } }) + self.singleton_class.include(Module.new do + include refine(version_class) { } + end) # And try to make the object appear to be a version_class. define_singleton_method(:class) { version_class } @@ -373,14 +394,6 @@ def self.known_versions define_singleton_method(:instance_of?) do |klass| klass == version_class || super(klass) end - - # An object can only be given methods for a single version, and - # exactly once. Conflicts and odd issues could occur otherwise. - # Thus, there is no longer a need for :as_version or the tool_config - # setter hooks. - [ :as_version, :tool_config=, :tool_config_id= ].each do |m| - metaclass.send(:remove_method, m) rescue nil - end end # If we dont have a tool config already, try to catch the exact moment diff --git a/BrainPortal/lib/cbrain_task_generators/templates/portal.rb.erb b/BrainPortal/lib/cbrain_task_generators/templates/portal.rb.erb index 265da8a03..5f9c1b6a7 100644 --- a/BrainPortal/lib/cbrain_task_generators/templates/portal.rb.erb +++ b/BrainPortal/lib/cbrain_task_generators/templates/portal.rb.erb @@ -170,7 +170,7 @@ class CbrainTask::<%= name %> < PortalTask # Create a list of tasks out of the default input file list # (interface_userfile_ids), each file going into parameter '<%= single_file['id'] %>' self.params[:interface_userfile_ids].map do |id| - task = self.clone + task = self.dup # Set and sanitize the one file parameter for each id task.params[:'<%= single_file['id']%>'] = id From d9369f24359a15a7f40eab7f63d26a8327b72471 Mon Sep 17 00:00:00 2001 From: Remi Bernard Date: Fri, 31 Jul 2015 14:40:31 -0400 Subject: [PATCH 07/85] Support for path-template-stripped-extensions As per boutiques' spec, the specified extension list is now stripped from input strings and file names before replacement in the path template. --- .../templates/bourreau.rb.erb | 57 ++++++++++++++----- 1 file changed, 43 insertions(+), 14 deletions(-) diff --git a/BrainPortal/lib/cbrain_task_generators/templates/bourreau.rb.erb b/BrainPortal/lib/cbrain_task_generators/templates/bourreau.rb.erb index b0d84c7fc..0e865cb7d 100644 --- a/BrainPortal/lib/cbrain_task_generators/templates/bourreau.rb.erb +++ b/BrainPortal/lib/cbrain_task_generators/templates/bourreau.rb.erb @@ -149,12 +149,20 @@ class CbrainTask::<%= name %> < ClusterTask % key_width = max_width.(out_keys, 'command-line-key') + "''".length % path_width = max_width.(out_keys, 'path-template') + "'',".length % out_keys.each do |key| +% stripped = key['path-template-stripped-extensions'] <%= - "%-#{key_width}s => substitute_keys(%-#{path_width}s keys)," % [ + "%-#{key_width}s => apply_template(%-#{path_width}s keys%s" % [ "'#{key['command-line-key']}'", - "'#{key['path-template']}'," + "'#{key['path-template']}',", + stripped ? ', strip: [' : '),' ] %> +% if stripped +% stripped.each do |ext| + '<%= ext %>', +% end + ]), +% end % end }) @@ -182,7 +190,7 @@ class CbrainTask::<%= name %> < ClusterTask % path_width = max_width.(outputs, 'path-template') + "'',".length % outputs.each do |output| <%= - "%-#{id_width}s => substitute_keys(%-#{path_width}s keys)," % [ + "%-#{id_width}s => apply_template(%-#{path_width}s keys)," % [ ":'#{output['id']}'", "'#{output['path-template']}'," ] @@ -192,7 +200,7 @@ class CbrainTask::<%= name %> < ClusterTask % end # Generate the final command-line to run <%= name %> - [ substitute_keys(<<-'CMD', keys<%= flags.empty? ? '' : ', flags' %>) ] + [ apply_template(<<-'CMD', keys<%= flags.empty? ? '' : ', flags: flags' %>) ] <%= descriptor['command-line'] %> CMD % end @@ -226,7 +234,7 @@ class CbrainTask::<%= name %> < ClusterTask # Make sure that every required output +path+ actually exists # (or that its +glob+ matches something). ensure_exists = lambda do |path| - return if File.exists(path) + return if File.exists?(path) self.addlog("Missing output file #{path}") succeeded &&= false end @@ -320,21 +328,42 @@ class CbrainTask::<%= name %> < ClusterTask self.results_data_provider_id ||= files.first.data_provider_id rescue nil end - # Substitute each parameter value in +keys+ in +str+ prepended by the - # corresponding flag in +flags+, if available. - # substitute_keys('f -e [1]', { '[1]' => 5 }) => 'f -e 5' - # substitute_keys('f -e [1]', { '[1]' => 5 }, { '[1]' => '-z' }) => 'f -e -z 5' - def substitute_keys(str, keys, flags = {}) - keys.inject(str) do |str, (key, value)| + # Apply substitution keys +keys+ to +template+ in order to format a + # command-line or output file name. + # Substitute each value in +keys+ in +template+, prepended by the + # corresponding flag in +flags+ (if available) and stripped of the + # endings in +strip+: + # apply_template('f [1]', { '[1]' => 5 }) + # => 'f 5' + # + # apply_template('f [1]', { '[1]' => 5 }, + # flags: { '[1]' => '-z' } + # ) => 'f -z 5' + # + # apply_template('f [1]', { '[1]' => '5.z' }, + # flags: { '[1]' => '-z' }, + # strip: [ '.z' ] + # ) => 'f -z 5' + def apply_template(template, keys, flags: {}, strip: []) + keys.inject(template) do |template, (key, value)| flag = flags[key] - next str.gsub(key, flag) if flag && value == true + next template.gsub(key, flag) if flag && value == true value = (value.is_a?(Enumerable) ? value.dup : [value]) .reject(&:nil?) - .map { |v| (v.is_a?(Userfile) ? v.name : v.to_s).bash_escape } + .map do |v| + v = v.name if v.is_a?(Userfile) + v = v.dup if v.is_a?(String) + + strip.find do |e| + v.sub!(/#{Regexp.quote(e)}$/, '') + end if v.is_a?(String) + + v.to_s.bash_escape + end .join(' ') - str.gsub(key, flag && value.present? ? "#{flag} #{value}" : value) + template.gsub(key, flag && value.present? ? "#{flag} #{value}" : value) end end From c3367e7674601afddcbe9ff82813c78324449bfd Mon Sep 17 00:00:00 2001 From: Remi Bernard Date: Fri, 31 Jul 2015 15:00:44 -0400 Subject: [PATCH 08/85] Mark optional fields as active on focus Optional fields are now set as active (and sent to the server) as soon as they are selected (focus event) instead of waiting fo user input (change event). --- .../templates/task_params.html.erb.erb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/BrainPortal/lib/cbrain_task_generators/templates/task_params.html.erb.erb b/BrainPortal/lib/cbrain_task_generators/templates/task_params.html.erb.erb index c3ef92277..f635e1310 100644 --- a/BrainPortal/lib/cbrain_task_generators/templates/task_params.html.erb.erb +++ b/BrainPortal/lib/cbrain_task_generators/templates/task_params.html.erb.erb @@ -348,7 +348,7 @@ $(function () { /* Optional parameters */ /* Clicking on the parameter's checkbox toggles the parameter's state */ - parameters.delegate('.tsk-prm-opt', 'change', function () { + parameters.delegate('.tsk-prm-opt', 'change activate.tsk-prm', function () { var opt = $(this), param = opt.parent(); @@ -391,16 +391,16 @@ $(function () { }); /* Changing a parameter's value automatically marks it as active */ - parameters.delegate('.tsk-prm-in', 'change', function () { + parameters.delegate('.tsk-prm-in', 'focus activate.tsk-prm', function () { $(this) .closest('.tsk-prm') .find('.tsk-prm-opt') .prop('checked', true) - .trigger('change'); + .trigger('activate.tsk-prm'); }); /* Activate optional parameters with default values */ - parameters.find(".tsk-prm-in[value]").trigger('change'); + parameters.find(".tsk-prm-in[value]").trigger('activate.tsk-prm'); /* Drop-down lists */ @@ -432,7 +432,7 @@ $(function () { param .find('.tsk-prm-in') .val(item.data('value')) - .trigger('change'); + .trigger('activate.tsk-prm'); /* Display the newly selected value in the drop-down's label */ param @@ -463,7 +463,7 @@ $(function () { ) .siblings('.tsk-prm-opt') .prop('checked', true) - .trigger('change'); + .trigger('activate.tsk-prm'); }); /* Clicking on a '-' button removes the row */ From 75e82e42f149188d8a6735a68ca84a66f9519b1f Mon Sep 17 00:00:00 2001 From: Remi Bernard Date: Fri, 31 Jul 2015 16:13:55 -0400 Subject: [PATCH 09/85] More intuitive automatic file selection indicator The first few files are now listed in gray instead of displaying '(Automatically selected)' and the selection arrow is no longer displayed. --- .../templates/task_params.html.erb.erb | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/BrainPortal/lib/cbrain_task_generators/templates/task_params.html.erb.erb b/BrainPortal/lib/cbrain_task_generators/templates/task_params.html.erb.erb index f635e1310..461ba596f 100644 --- a/BrainPortal/lib/cbrain_task_generators/templates/task_params.html.erb.erb +++ b/BrainPortal/lib/cbrain_task_generators/templates/task_params.html.erb.erb @@ -199,12 +199,12 @@ ) %>
    - <%% if options.empty? %> <%%= nothing %> <%% else %> + <%% if (pair = options.select { |o| o.first == value }.first) %> <%%= pair.last %> @@ -226,6 +226,14 @@ <%% end %> +<%%# Give a short (up to +max+ characters) representation of +list+ %> +<%% + short_repr = lambda do |list, max| + str = list.map(&:to_s).join(', ') + str.length <= max ? str : (str[0, max - 3] + '...') + end +%%> + <%# Generate a complete HTML block for a given parameter +param+ -%> <%- parameter = lambda do |param| -%> <%- @@ -265,7 +273,7 @@ <%- if single_file -%> # Automatic single file input dropdown.(id, id, - [], nothing: '(Automatically selected)', + [], nothing: short_repr.(input_files.map(&:name), 65), optional: <%= opt %> ) <%- elsif param['cbrain-file-type'] -%> From b0a7124e8b23654e09d9822d4eb260ad93388884 Mon Sep 17 00:00:00 2001 From: Remi Bernard Date: Wed, 5 Aug 2015 16:58:11 -0400 Subject: [PATCH 10/85] Session data controller, CbrainSession refactor to reduce coupling (!) CbrainSession now acts as an independent model wrapping ActiveRecord::SessionStore::Session, and no longer concerns itself with: - Session value format; the undocumented API relying on special suffixes (_clear, _remove, _hash, _array, etc.) in session keys has been removed. - Controller-specific keys; CbrainSession no longer directly considers keys named from controllers (bar a single method kept for backwards compatibility. - Filtering and sorting; all methods related to sorting and filtering have been removed, as they should belong in their own module or class. All methods have been re-worked to be as simple as possible, documented and meet CBRAIN's coding style. This changeset also adds a new controller to allow direct API access to session data, which is intended to be used from client-side JS scripts. Note that as this changeset removes the filtering/sorting methods from CbrainSession, it breaks the filtering and sorting capabilities offered by the basic_filter_helpers and filter_helper modules. This functionality will be restored by the upcoming refactor of those modules. --- .../controllers/data_providers_controller.rb | 2 +- .../controllers/exception_logs_controller.rb | 2 +- .../app/controllers/groups_controller.rb | 8 +- .../app/controllers/messages_controller.rb | 2 +- .../app/controllers/portal_controller.rb | 10 +- .../controllers/session_data_controller.rb | 61 +++ .../app/controllers/sessions_controller.rb | 4 +- .../app/controllers/tasks_controller.rb | 4 +- .../app/controllers/userfiles_controller.rb | 63 +-- .../app/controllers/users_controller.rb | 4 +- BrainPortal/app/models/cbrain_session.rb | 509 ++++++++---------- .../app/views/userfiles/_file_menu.html.erb | 10 +- .../userfiles/_userfiles_display.html.erb | 6 +- BrainPortal/config/routes.rb | 4 +- BrainPortal/lib/basic_filter_helpers.rb | 34 +- BrainPortal/lib/session_helpers.rb | 2 +- 16 files changed, 343 insertions(+), 382 deletions(-) create mode 100644 BrainPortal/app/controllers/session_data_controller.rb diff --git a/BrainPortal/app/controllers/data_providers_controller.rb b/BrainPortal/app/controllers/data_providers_controller.rb index af546a153..0dceb9913 100644 --- a/BrainPortal/app/controllers/data_providers_controller.rb +++ b/BrainPortal/app/controllers/data_providers_controller.rb @@ -377,7 +377,7 @@ def browse end end - current_session.save_preferences_for_user(current_user, :data_providers, :browse_hash) + current_session.save_preferences respond_to do |format| format.html diff --git a/BrainPortal/app/controllers/exception_logs_controller.rb b/BrainPortal/app/controllers/exception_logs_controller.rb index 3f6f8cd8e..6efdbd9de 100644 --- a/BrainPortal/app/controllers/exception_logs_controller.rb +++ b/BrainPortal/app/controllers/exception_logs_controller.rb @@ -35,7 +35,7 @@ def index #:nodoc: @filtered_scope = base_filtered_scope @exception_logs = base_sorted_scope(@filtered_scope).paginate(:page => @current_page, :per_page => @per_page) - current_session.save_preferences_for_user(current_user, :exception_logs, :per_page) + current_session.save_preferences respond_to do |format| format.html # index.html.erb diff --git a/BrainPortal/app/controllers/groups_controller.rb b/BrainPortal/app/controllers/groups_controller.rb index 34f5cb8c6..7e30695a8 100644 --- a/BrainPortal/app/controllers/groups_controller.rb +++ b/BrainPortal/app/controllers/groups_controller.rb @@ -87,7 +87,7 @@ def index #:nodoc: @group_id_2_brain_portal_counts = BrainPortal.group("group_id").count end - current_session.save_preferences_for_user(current_user, :groups, :button_view, :per_page) + current_session.save_preferences respond_to do |format| format.js @@ -235,9 +235,9 @@ def switch #:nodoc: redirect_action = params[:redirect_action] || :index redirect_id = params[:redirect_id] - current_session.param_chain("userfiles", "filter_hash").delete("group_id") - current_session.param_chain("tasks" , "filter_hash").delete("group_id") - current_session.persistent_userfile_ids_clear + current_session.params_for(:userfiles)['filter_hash'].delete('group_id') + current_session.params_for(:tasks)['filter_hash'].delete('group_id') + current_session[:persistent_userfiles].clear rescue nil redirect_path = { :controller => redirect_controller, :action => redirect_action } redirect_path[:id] = redirect_id unless redirect_id.blank? diff --git a/BrainPortal/app/controllers/messages_controller.rb b/BrainPortal/app/controllers/messages_controller.rb index 416c1d863..5b4c4419a 100644 --- a/BrainPortal/app/controllers/messages_controller.rb +++ b/BrainPortal/app/controllers/messages_controller.rb @@ -48,7 +48,7 @@ def index #:nodoc: @messages = scope.paginate(:page => @current_page, :per_page => @per_page) - current_session.save_preferences_for_user(current_user, :messages, :per_page) + current_session.save_preferences respond_to do |format| format.html # index.html.erb diff --git a/BrainPortal/app/controllers/portal_controller.rb b/BrainPortal/app/controllers/portal_controller.rb index 1543c6f6d..72c821503 100644 --- a/BrainPortal/app/controllers/portal_controller.rb +++ b/BrainPortal/app/controllers/portal_controller.rb @@ -51,9 +51,10 @@ def welcome #:nodoc: @active_users = CbrainSession.active_users @active_users.unshift(current_user) unless @active_users.include?(current_user) if request.post? - unless params[:session_clear].blank? - CbrainSession.session_class.where(["updated_at < ?", params[:session_clear].to_i.seconds.ago]).delete_all - end + CbrainSession.clean_sessions + CbrainSession.purge_sessions(params[:session_clear].to_i.seconds.ago) unless + params[:session_clear].blank? + if params[:lock_portal] == "lock" BrainPortal.current_resource.lock! BrainPortal.current_resource.addlog("User #{current_user.login} locked this portal.") @@ -68,9 +69,6 @@ def welcome #:nodoc: flash.now[:error] = "" end end - #elsif current_user.has_role? :site_manager - # @active_users = CbrainSession.active_users.where( :site_id => current_user.site_id ) - # @active_users.unshift(current_user) unless @active_users.include?(current_user) end bourreau_ids = Bourreau.find_all_accessible_by_user(current_user).raw_first_column("remote_resources.id") diff --git a/BrainPortal/app/controllers/session_data_controller.rb b/BrainPortal/app/controllers/session_data_controller.rb new file mode 100644 index 000000000..3ab04eae0 --- /dev/null +++ b/BrainPortal/app/controllers/session_data_controller.rb @@ -0,0 +1,61 @@ + +# +# CBRAIN Project +# +# Copyright (C) 2008-2012 +# The Royal Institution for the Advancement of Learning +# McGill University +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# JSON/XML controller for server-side session data, such as filters, sorting +# options, selections, etc. +class SessionDataController < ApplicationController + + Revision_info=CbrainFileRevision[__FILE__] #:nodoc: + + api_available + before_filter :login_required + + # GET /session_data + def show #:nodoc: + @session = current_session.to_h.reject do |k,v| + CbrainSession.internal_keys.include?(k) + end + + respond_to do |format| + format.xml { render :xml => @session } + format.json { render :json => @session } + end + end + + # POST /session_data + def update #:nodoc: + mode = request.query_parameters[:mode].to_sym rescue :replace + changes = request.request_parameters.reject do |k,v| + CbrainSession.internal_keys.include?(k) + end + + current_session.update(changes, mode) + show + + rescue => ex + respond_to do |format| + format.xml { render :xml => { :error => ex.message }, :status => :unprocessable_entity } + format.json { render :json => { :error => ex.message }, :status => :unprocessable_entity } + end + end + +end diff --git a/BrainPortal/app/controllers/sessions_controller.rb b/BrainPortal/app/controllers/sessions_controller.rb index 0bd5ab01e..16c651315 100644 --- a/BrainPortal/app/controllers/sessions_controller.rb +++ b/BrainPortal/app/controllers/sessions_controller.rb @@ -76,7 +76,7 @@ def destroy #:nodoc: current_session.deactivate if current_session current_user.addlog("Logged out") if current_user portal.addlog("User #{current_user.login} logged out") if current_user - current_session.clear_data! + current_session.clear #reset_session flash[:notice] = "You have been logged out." @@ -235,7 +235,7 @@ def user_tracking(portal) #:nodoc: current_session.activate user = current_user - current_session.load_preferences_for_user(user) + current_session.load_preferences # Record the best guess for browser's remote host name reqenv = request.env diff --git a/BrainPortal/app/controllers/tasks_controller.rb b/BrainPortal/app/controllers/tasks_controller.rb index 294abd0f6..3bb1b8c2d 100644 --- a/BrainPortal/app/controllers/tasks_controller.rb +++ b/BrainPortal/app/controllers/tasks_controller.rb @@ -103,7 +103,7 @@ def index #:nodoc: pager end - current_session.save_preferences_for_user(current_user, :tasks, :per_page) + current_session.save_preferences @bourreau_status = bourreaux.map { |b| [b.id, b.online?] }.to_h respond_to do |format| @@ -210,7 +210,7 @@ def new #:nodoc: @tool_config = @task.tool_config # for acces in view # Filter list of files as provided by the get request - file_ids = (params[:file_ids] || []) | current_session.persistent_userfile_ids_list + file_ids = (params[:file_ids] || []) | (current_session[:persistent_userfiles] || []) @files = Userfile.find_accessible_by_user(file_ids, current_user, :access_requested => :write) rescue [] if @files.empty? flash[:error] = "You must select at least one file to which you have write access." diff --git a/BrainPortal/app/controllers/userfiles_controller.rb b/BrainPortal/app/controllers/userfiles_controller.rb index 7e1dac0eb..15fdc08b8 100644 --- a/BrainPortal/app/controllers/userfiles_controller.rb +++ b/BrainPortal/app/controllers/userfiles_controller.rb @@ -152,7 +152,7 @@ def index #:nodoc: @archived_total = @filtered_scope.where(:archived => true).count @immutable_total = @filtered_scope.where(:immutable => true).count - current_session.save_preferences_for_user(current_user, :userfiles, :view_hidden, :tree_sort, :view_all, :details, :per_page) + current_session.save_preferences respond_to do |format| format.html format.js @@ -948,48 +948,39 @@ def change_provider #:nodoc: # Adds the selected userfile IDs to the session's persistent list def manage_persistent - if (params[:operation] || 'clear') =~ /(clear|add|remove|replace)/i - filelist = params[:file_ids] || [] - operation = Regexp.last_match[1].downcase - elsif (params[:operation]) =~ /select/i - operation = "add" - # Reduce userfiles list according to @filter_params - filelist = [] - header_scope = header_scope(@filter_params) - filtered_scope = filter_scope(@filter_params, header_scope) - filtered_scope.each do |f| - filelist << f.id.to_s if f.available? - end + operation = (params[:operation] || 'clear').downcase + persistent = (current_session[:persistent_userfiles] || Set.new) + + if operation =~ /select/ + files = filter_scope(@filter_params, header_scope(@filter_params)) + .select(&:available?) + .map(&:id) + .map(&:to_s) else - operation = 'clear' + files = params[:file_ids] || [] end - flash[:notice] = "" + case operation + when /add/, /select/ + persistent += files - cleared_count = added_count = removed_count = 0 + when /remove/ + persistent -= files - if operation == 'clear' || operation == 'replace' - cleared_count = current_session.persistent_userfile_ids_clear - flash[:notice] += "#{view_pluralize(cleared_count, "file")} cleared from persistent list.\n" if cleared_count > 0 - end + when /clear/ + persistent.clear - if operation == 'add' || operation == 'replace' - added_count = current_session.persistent_userfile_ids_add(filelist) - flash[:notice] += "#{view_pluralize(added_count, "file")} added to persistent list.\n" if added_count > 0 + when /replace/ + persistent.replace(files) end - if operation == 'remove' - removed_count = current_session.persistent_userfile_ids_remove(filelist) - flash[:notice] += "#{view_pluralize(removed_count, "file")} removed from persistent list.\n" if removed_count > 0 + if persistent.size > 0 + flash[:notice] = "#{view_pluralize(persistent.size, 'file')} now persistently selected." + else + flash[:notice] = "Peristent selection list now empty." end - persistent_ids = current_session.persistent_userfile_ids_list - flash[:notice] += "Total of #{view_pluralize(persistent_ids.size, "file")} now in the persistent list of files.\n" if - persistent_ids.size > 0 && (added_count > 0 || removed_count > 0 || cleared_count > 0) - - flash[:notice] += "No changes made to the persistent list of userfiles." if - added_count == 0 && removed_count == 0 && cleared_count == 0 - + current_session[:persistent_userfiles] = persistent redirect_to :action => :index, :page => params[:page] end @@ -1367,9 +1358,9 @@ def archive_management # Adds the persistent userfile ids to the params[:file_ids] argument def auto_add_persistent_userfile_ids #:nodoc: params[:file_ids] ||= [] - if params[:ignore_persistent].blank? - params[:file_ids] = params[:file_ids] | current_session.persistent_userfile_ids_list - end + params[:file_ids] |= current_session[:persistent_userfiles].to_a if + params[:ignore_persistent].blank? && + current_session[:persistent_userfiles].present? end # Verify that all files selected for an operation diff --git a/BrainPortal/app/controllers/users_controller.rb b/BrainPortal/app/controllers/users_controller.rb index 2fad8c49b..945caecca 100644 --- a/BrainPortal/app/controllers/users_controller.rb +++ b/BrainPortal/app/controllers/users_controller.rb @@ -54,7 +54,7 @@ def index #:nodoc: # Turn the array ordered_real into the final paginated collection @users = @users.paginate(:page => @current_page, :per_page => @per_page) - current_session.save_preferences_for_user(current_user, :users, :per_page) + current_session.save_preferences respond_to do |format| format.html # index.html.erb @@ -271,7 +271,7 @@ def switch #:nodoc: myportal.addlog("Admin user '#{current_user.login}' switching to user '#{@user.login}'") current_user.addlog("Switching to user '#{@user.login}'") @user.addlog("Switched from user '#{current_user.login}'") - current_session.clear_data! + current_session.clear self.current_user = @user current_session[:user_id] = @user.id diff --git a/BrainPortal/app/models/cbrain_session.rb b/BrainPortal/app/models/cbrain_session.rb index ca5a0092e..083c104fe 100644 --- a/BrainPortal/app/models/cbrain_session.rb +++ b/BrainPortal/app/models/cbrain_session.rb @@ -20,351 +20,274 @@ # along with this program. If not, see . # -#Model represeting the current session. The current session object can -#be accessed using the current_session method of the ApplicationController -#class. -# -#This model is meant to act as a wrapper around the session hash. -#It takes care of updating the values of and performing any logic related -#to the following attributes of the current session (mainly related -#to the Userfile index page): -#* currently active filters. -#* whether or not pagination is active. -#* current ordering of the Userfile index. -#* whether to view current user's files or all files on the system (*admin* only). +require 'set' + +# Model representing a CBRAIN user's Rails session. The currently logged in +# user's session object can be accessed using the current_session method of +# ApplicationController (from SessionHelpers). # -#Session attributes can be accessed by calling methods with the attribute name. -#*Example*: calling +current_session+.+current_filters+ will access session[:current_filters] +# Meant as a wrapper around Rails session hash, this model is mostly used +# to add additional reporting/monitoring logic, to cleanly support partial +# updates and to validate certain session attributes. # -#*Note*: this is not a database-backed model. +# NOTE: This model is not database-backed class CbrainSession Revision_info=CbrainFileRevision[__FILE__] #:nodoc: - def initialize(session, params, sess_model) #:nodoc: - @session = session # rails session - @session_model = sess_model # active record model that stores the session - - @session[:persistent_userfile_ids] ||= {} - - controller = params[:proxy_destination_controller] || params[:controller] - @session[controller.to_sym] ||= {} - @session[controller.to_sym]["filter_hash"] ||= {} - @session[controller.to_sym]["sort_hash"] ||= {} + # Create a new CbrainSession object wrapping +session+ (a Rails session) + # backed by +model+, an instance of CbrainSession.session_model (which is + # expected to be an ActiveRecord record). + def initialize(session, model = nil) + @session = session + @model = model end - # Import a user's saved preferences from the db into the session. - def load_preferences_for_user(current_user) - user_preferences = current_user.meta[:preferences] || {} - user_preferences.each { |k, v| @session[k.to_sym] = v || {}} - end - - # Save given preferences from session into the db. - def save_preferences_for_user(current_user, cont, *ks) - controller = cont.to_sym - keys = ks.map(&:to_s) - user_preferences = current_user.meta[:preferences].cb_deep_clone || {} - user_preferences[controller] ||= {} - keys.each do |k| - if @session[controller][k] && user_preferences[controller][k] != @session[controller][k] - if @session[controller][k].is_a? Hash - user_preferences[controller][k] ||= {} - user_preferences[controller][k].merge!(@session[controller][k].cb_deep_clone) - elsif @session[controller][k].is_a? Array - user_preferences[controller][k] ||= [] - user_preferences[controller][k] |= @session[controller][k].cb_deep_clone - else - user_preferences[controller][k] = @session[controller][k].cb_deep_clone - end - end - end - - unless user_preferences[controller].blank? - current_user.meta[:preferences] = user_preferences - end - end - - # Mark this session as active in the database. - def activate - return unless @session_model - # @session_model.update_attributes!(:user_id => @session[:user_id], :active => true) - @session_model.user_id = @session[:user_id] - @session_model.active = true - @session_model.save! + # ActiveRecord model class for Rails sessions + def self.session_model + ActiveRecord::SessionStore::Session end - # Mark this session as inactive in the database. - def deactivate - return unless @session_model - @session_model.active = false - @session_model.save! + # Internal CBRAIN session tracking keys. Invisible to the API and end-user, + # these keys keep track of the user's connection information. + def self.tracking_keys + @tracking_keys ||= Set.new([ + :client_type, + :guessed_remote_host, + :guessed_remote_ip, + :raw_user_agent, + :return_to, + ].map(&:to_s)) end - # Returns the list of currently active users on the system. - def self.active_users(options = {}) - active_sessions = session_class.where( - ["sessions.active = 1 AND sessions.user_id IS NOT NULL AND sessions.updated_at > ?", 10.minutes.ago] - ) - user_ids = active_sessions.map(&:user_id).uniq - scope = User.where(options) - scope.where( :id => user_ids ) + # Internal CBRAIN session authentication, security, tracking and monitoring + # attribute keys. They are invisible to the API and end-user. + def self.internal_keys + @internal_keys ||= Set.new([ + :_csrf_token, + :cbrain_toggle, + :user_id, + ].map(&:to_s) + self.tracking_keys.to_a) end - def self.count(options = {}) #:nodoc: - scope = session_class.where(options) - scope.count + # User this session belongs to, from the :user_id attribute + def user + @user = User.find_by_id(@session[:user_id]) unless + @user && @user.id == @session[:user_id] + @user end - def self.session_class #:nodoc: - ActiveRecord::SessionStore::Session + # Load +user+'s preferences (session attributes) in the session from the + # user's meta storage. + # If +user+ is not specified or nil, load_preferences will try to use the + # user bound to the session, if available. + def load_preferences(user = nil) + user = self.user unless user.is_a?(User) + prefs = (user.meta[:preferences] || {}) + .map { |k,v| [k.to_sym, v] } + .to_h + .reject { |k,v| self.class.internal_keys.include?(k) } + + @session.merge!(prefs) end - def self.all #:nodoc: - self.session_class.all + # Save this session object's attributes as the +user+'s preferences in the + # user's meta storage (opposite of load_preferences). + # If +user+ is not specified or nil, save_preferences will try to use the + # user bound to the session, if available. + def save_preferences(user = nil) + user = self.user unless user.is_a?(User) + prefs = @session + .reject { |k,v| self.class.internal_keys.include?(k) } + .cb_deep_clone + + user.meta[:preferences] = (user.meta[:preferences] || {}).merge(prefs) end - def self.recent_activity(n = 10, options = {}) #:nodoc: - self.clean_sessions - last_sessions = session_class.where( "sessions.user_id IS NOT NULL" ).order("sessions.updated_at DESC") - entries = [] - - last_sessions.each do |sess| - break if entries.size >= n - next if sess.user_id.blank? - user = User.find_by_id(sess.user_id) - next unless user - sessdata = (sess.data || {}) rescue {} - entries << { - :user => user, - :active => sess.active?, - :last_access => sess.updated_at, - :remote_ip => sessdata["guessed_remote_ip"], # can be nil, must be fetched with string not symbol - :remote_host => sessdata["guessed_remote_host"], # can be nil, must be fetched with string not symbol - :raw_user_agent => sessdata["raw_user_agent"], # can be nil, must be fetched with string not symbol - } - end - - entries - end + # Hash-like interface to session attributes - # Remove all spurious sessions entries: - # a) older than 1 hour and - # b) with no user_id and - # c) not active - # These are usually created simply by any access to the - # login page. - def self.clean_sessions #:nodoc: - self.session_class.where("user_id is null").where([ "updated_at < ?", 1.hour.ago]).destroy_all - rescue - nil + # Delegate [] to @session + def [](key) #:nodoc: + @session[key] end - # Erase most of the entries in the data - # section of the session; this is used when the - # user logs out. Some elements are kept - # for tracking no matter what, like the - # :guessed_remote_host and the :raw_user_agent - def clear_data! - @session.each do |k,v| - next if k.to_s =~ /guessed_remote_ip|guessed_remote_host|raw_user_agent|client_type/ - @session.delete(k) - end + # Delegate []= to @session + def []=(key, value) #:nodoc: + @session[key] = value end - # Update attributes of the session object based on the incoming request parameters - # contained in the +params+ hash. - def update(params) - controller = params[:proxy_destination_controller] || params[:controller] - if params[controller] - params[controller].each do |k, v| - if @session[controller.to_sym][k].nil? - if k =~ /_hash/ - @session[controller.to_sym][k] = {} - elsif k =~ /_array/ - @session[controller.to_sym][k] = [] - end - end - if k == "remove" && v.is_a?(Hash) - v.each do |list, item| - if @session[controller.to_sym][list].respond_to? :delete - @session[controller.to_sym][list].delete item - else - @session[controller.to_sym].delete list - end - end - elsif k =~ /^clear_(.+)/ - pattern = Regexp.last_match[1].gsub(/\W/, "") - if pattern == "all" - clear_list = v - clear_list = [v] unless v.is_a? Array - else - clear_list = @session[controller.to_sym].keys.grep(/^#{pattern}/) - end - clear_list.each do |item| - if item == "all" - @session[controller.to_sym].clear - @session[controller.to_sym]["filter_hash"] ||= {} - @session[controller.to_sym]["sort_hash"] ||= {} - elsif @session[controller.to_sym][item].respond_to? :clear - @session[controller.to_sym][item].clear - else - @session[controller.to_sym].delete item - end + # Update sessions attributes from the contents of +hash+. While similiar to + # Hash's merge method, this method has a few key differences: + # + # - Hashes in +hash+ and session attributes are recursively merged: + # @session # { :a => { :b => 1 } } + # update({ :a => { :c => 1 } }) + # @session # { :a => { :b => 1, :c => 1 } } + # + # - update does not accept a block; session attributes are always overwritten + # by their new value in +hash+, if present. + # + # - nil values are automatically removed from hashes to avoid clutter. This + # cleanly allows removing keys from hashes: + # @session # { :a => { :b => 1 } } + # update({ :a => { :b => nil } }) + # @session # { :a => {} } + # + # - collection (Array, Set) collision handling is based on +collection_mode+, + # which is one of: + # + # [:replace] + # Handle collections just like regular values; replace the entire + # collection with the new one in +hash+: + # @session # { :a => [1] } + # update({ :a => [2] }, :replace) + # @session # { :a => [2] } + # + # [:append] + # Append the values in +hash+'s collection to the corresponding one in + # the session attributes. + # @session # { :a => [1] } + # update({ :a => [2] }, :append) + # @session # { :a => [1, 2] } + # + # [:delete] + # Opposite of append; remove the values in +hash+'s collection from the + # corresponding one in the session attributes. + # @session # { :a => [1] } + # update({ :a => [1, 2] }, :delete) + # @session # { :a => [2] } + def update(hash, collection_mode = :replace) + (update = lambda do |base, new| + base.merge!(new) do |key, old, new| + next new unless old.is_a?(new.class) || new.is_a?(old.class) + + case old + when Hash + update.(old, new) + when Set, Array + case collection_mode + when :replace + new + when :append + old + new + when :delete + old - new end else - if @session[controller.to_sym][k].is_a? Hash - @session[controller.to_sym][k].merge!(sanitize_params(k, v) || {}) - @session[controller.to_sym][k].delete_if { |pk, pv| pv.blank? } - elsif @session[controller.to_sym][k].is_a? Array - sanitized_param = sanitize_params(k, v) - @session[controller.to_sym][k] |= [sanitized_param] if sanitized_param - else - @session[controller.to_sym][k] = sanitize_params(k, v) - end + new end end - end - end - - # Returns the params saved for +controller+. - def params_for(controller) - @session[controller.to_sym] || {} - end - # Find nested values without raising an exception. - def param_chain(*keys) - return nil if keys.empty? - final_key = keys.pop - empty_value = nil - empty_value = {} if final_key =~ /_hash$/ - empty_value = [] if final_key =~ /_array$/ - - current_hash = @session - keys.each do |k| - current_hash = current_hash[k] - return empty_value unless current_hash.is_a?(Hash) - end - return empty_value unless current_hash.has_key?(final_key) - current_hash[final_key] + base.delete_if { |k,v| v.nil? } + base + end).( + @session, + hash.reject { |k,v| self.class.internal_keys.include?(k) } + ) end - # Hash-like access to session attributes. - def [](key) - @session[key] + # Clear out all session attributes bar those used for tracking (IP, host, + # user agent, ...). Used when the user logs out. + def clear + @session.select! { |k,v| self.class.tracking_keys.include?(k) } end - # Hash-like assignment to session attributes. - def []=(key, value) - return unless @session_model - if key == :user_id - @session_model.user_id = value - @session_model.save - end - @session[key] = value + # Convert all session attributes directly into a regular hash. + def to_h + @session.to_h end - # The method_missing method has been redefined to allow for simplified access to session parameters. - # - # *Example*: calling +current_session+.+current_filters+ will access session[:current_filters] - def method_missing(key, *args) - @session[key.to_sym] - end + # Reporting/monitoring methods - ########################################### - # Peristent Userfile Ids Management Methods - ########################################### - - # Clear the list of persistent userfile IDs; - # returns the number of userfiles that were there. - def persistent_userfile_ids_clear - persistent_ids = self[:persistent_userfile_ids] ||= {} - original_count = persistent_ids.size - self[:persistent_userfile_ids] = {} - original_count - end + # Active/inactive state; used mainly to mark currently active (logged in) + # users for reporting purposes, as the sessions records sometimes linger + # after the user logs out. - # Add the IDs in the array +id_list+ to the - # list of persistent userfile IDs. - # Returns the number of IDs that were actually added. - def persistent_userfile_ids_add(id_list) - added_count = 0 - persistent_ids = self[:persistent_userfile_ids] ||= {} - size_limit = 2500 - if (persistent_ids.size + id_list.size) > size_limit - cb_error "You cannot have more than a total of #{size_limit} files selected persistently." - return - end - id_list.each do |id| - next if persistent_ids[id] - persistent_ids[id] = true - added_count += 1 - end - added_count - end + # Mark this session as active. + def activate + return unless @model - # Removed the IDs in the array +id_list+ to the - # list of persistent userfile IDs. - # Returns the number of IDs that were actually removed. - def persistent_userfile_ids_remove(id_list) - removed_count = 0 - persistent_ids = self[:persistent_userfile_ids] ||= {} - id_list.each do |id| - next unless persistent_ids[id] - persistent_ids.delete(id) - removed_count += 1 - end - removed_count + @model.user_id = @session[:user_id] + @model.active = true + @model.save! end - # Returns an array of the list of persistent userfile IDs. - def persistent_userfile_ids_list - persistent_ids = self[:persistent_userfile_ids] ||= {} - persistent_ids.keys - end + # Mark this session as inactive. + def deactivate + return unless @model - # Returns the persistent userfile IDs as a hash. - def persistent_userfile_ids - persistent_ids = self[:persistent_userfile_ids] ||= {} - persistent_ids + @model.active = false + @model.save! end - private - - def sanitize_params(k, param) #:nodoc: - key = k.to_sym - - if key == :sort_hash - param["order"] = sanitize_sort_order(param["order"]) - param["dir"] = sanitize_sort_dir(param["dir"]) - end + # User model scope of currently (recently) active users. + # (active and had activity since +since+). + def self.active_users(since: 10.minutes.ago) + sessions = session_model.quoted_table_name + users = User.quoted_table_name - param + User + .joins("INNER JOIN #{sessions} ON #{sessions}.user_id = #{users}.id") + .where("#{sessions}.active = 1") + .where(since ? ["#{sessions}.updated_at > ?", since] : {}) end - def sanitize_sort_order(order) #:nodoc: - table, column = order.strip.split(".") - table = table.tableize - - unless ActiveRecord::Base.connection.tables.include?(table) - cb_error "Invalid sort table: #{table}." - end + # Report (as a list of hashes) the +n+ most recently active users and their + # IP address, host name and user agent. + def self.recent_activity(n = 10) + sessions = session_model.quoted_table_name + users = User.quoted_table_name + + session_model + .joins("INNER JOIN #{users} ON #{users}.id = #{sessions}.user_id") + .order("#{sessions}.updated_at DESC") + .limit(n) + .map do |session| + data = (session.data || {}) rescue {} + { + :user => User.find_by_id(session.user_id), + :active => session.active?, + :last_access => session.updated_at, + :remote_ip => data['guessed_remote_ip'], + :remote_host => data['guessed_remote_host'], + :raw_user_agent => data['raw_user_agent'] + } + end + end - klass = Class.const_get table.classify + # Clean out spurious session entries; entries older than +since+ without + # an attached user. + def self.clean_sessions(since: 1.hour.ago) + session_model + .where('user_id IS NULL') + .where('updated_at < ?', since) + .destroy_all + end - unless klass.column_names.include?(column) || - (klass.respond_to?(:pseudo_sort_columns) && klass.pseudo_sort_columns.include?(column)) - cb_error "Invalid sort column: #{table}.#{column}" - end + # Purge all session entries older than +since+, no matter if theres an + # attached user or not. + def self.purge_sessions(since: 1.hour.ago) + session_model + .where('updated_at < ?', since) + .delete_all + end - "#{table}.#{column}" + # Delegate other calls on CbrainSession to session_model, making CbrainSession + # behave like Rails's session model. + def self.method_missing(method, *args) # :nodoc: + session_model.send(method, *args) end - def sanitize_sort_dir(dir) #:nodoc: - if dir.to_s.strip.upcase == "DESC" - "DESC" - else - "ASC" - end + # Deprecated/old API methods + + # Fetch session parameters specific to controller +controller+. + # Marked as deprecated as session attributes are no longer necessarily bound + # to a controller. + def params_for(controller) #:nodoc: + controller = (@session[controller.to_sym] ||= {}) + controller['filter_hash'] ||= {} + controller['sort_hash'] ||= {} + controller end end diff --git a/BrainPortal/app/views/userfiles/_file_menu.html.erb b/BrainPortal/app/views/userfiles/_file_menu.html.erb index c7acbf005..d3dab34ba 100644 --- a/BrainPortal/app/views/userfiles/_file_menu.html.erb +++ b/BrainPortal/app/views/userfiles/_file_menu.html.erb @@ -135,7 +135,11 @@ <%= hijacker_submit_button("Mark Content As Newer", :url => url_for(:controller => 'userfiles', :action => 'sync_multiple', :operation => 'all_newer'), :ajax_submit => false, :method => :post, :class => "button") %> <% end %> - <% select_label = "Selected Files" + (current_session.persistent_userfile_ids.size != 0 ? " (now: #{current_session.persistent_userfile_ids.size})" : "") %> + <% + select_label = "Selected Files" + select_label += " (now: #{current_session[:persistent_userfiles].size})" if + current_session[:persistent_userfiles].present? + %> <%= button_with_dropdown_menu(select_label) do %>

    This panel manages your list of persistently selected files. @@ -144,10 +148,10 @@ for all operation buttons (move/copy, submit tasks, etc).

    - <% if current_session.persistent_userfile_ids.size == 0 %> + <% if current_session[:persistent_userfiles].blank? %> Currently, no files are selected persistently. <% else %> - <%= html_colorize("#{pluralize(current_session.persistent_userfile_ids.size,"file")} currently selected persistently.") %> + <%= html_colorize("#{pluralize(current_session[:persistent_userfiles].size,"file")} currently selected persistently.") %> <% end %>

    diff --git a/BrainPortal/app/views/userfiles/_userfiles_display.html.erb b/BrainPortal/app/views/userfiles/_userfiles_display.html.erb index 882940485..43b751b14 100644 --- a/BrainPortal/app/views/userfiles/_userfiles_display.html.erb +++ b/BrainPortal/app/views/userfiles/_userfiles_display.html.erb @@ -25,9 +25,9 @@ <%= render :partial => 'shared/active_filters', :locals => { :model => :userfile } %>

    +
    + <%= render :partial => 'userfiles/tag_table_sidebar' %> +
    + +

    Other Filters

    +
      +
    • + <%= + scope_filter_link('Has no parent', + @scope, :set, { 'type' => 'uf.hier', 'operator' => 'no_parent' }, + link: { :ajax => true } + ) + %> +
    • +
    • + <%= + scope_filter_link('Has no children', + @scope, :set, { 'type' => 'uf.hier', 'operator' => 'no_child' }, + link: { :ajax => true } + ) + %> +
    • +
    diff --git a/BrainPortal/app/views/userfiles/_userfiles_display.html.erb b/BrainPortal/app/views/userfiles/_userfiles_display.html.erb index f082c173c..ddc565f80 100644 --- a/BrainPortal/app/views/userfiles/_userfiles_display.html.erb +++ b/BrainPortal/app/views/userfiles/_userfiles_display.html.erb @@ -49,11 +49,11 @@ total.present? && (total.is_a?(String) || total > 0) end %> - <%= pluralize(@userfiles_total," entry") -%>, - <%= colored_pretty_size(@userfiles_total_size) -%> - <%= show_total.(@hidden_total, 'hidden', hidden_icon) -%> - <%= show_total.(@archived_total, 'archived', archived_icon) -%> - <%= show_total.(@immutable_total, 'immutable', immutable_icon) -%> + <%= pluralize(@userfiles_total," entry") %> , + <%= colored_pretty_size(@userfiles_total_size) %> + <%= show_total.(@hidden_total, 'hidden', hidden_icon) %> + <%= show_total.(@archived_total, 'archived', archived_icon) %> + <%= show_total.(@immutable_total, 'immutable', immutable_icon) %> )
    diff --git a/BrainPortal/app/views/userfiles/index.js.erb b/BrainPortal/app/views/userfiles/index.js.erb index 89feef32e..5c0c77eb8 100644 --- a/BrainPortal/app/views/userfiles/index.js.erb +++ b/BrainPortal/app/views/userfiles/index.js.erb @@ -26,4 +26,4 @@ jQuery("#userfiles_display").html(<%= html_for_js(render(:partial => 'userfiles_display')) %>); jQuery("#userfiles_display").trigger("new_content"); jQuery("#view_option_button").html(<%= html_for_js(render(:partial => 'view_option_button')) %>).trigger("new_content"); -jQuery("#userfiles_other_filters").html(<%= html_for_js(render(:partial => 'other_filters')) %>).trigger("new_content"); +jQuery("#userfiles_filters").html(<%= html_for_js(render(:partial => 'filters')) %>).trigger("new_content"); diff --git a/BrainPortal/public/stylesheets/cbrain.css b/BrainPortal/public/stylesheets/cbrain.css index 2dd8502b7..65ee13df3 100644 --- a/BrainPortal/public/stylesheets/cbrain.css +++ b/BrainPortal/public/stylesheets/cbrain.css @@ -1924,39 +1924,19 @@ img { /* % Styles for Data Provider Report page */ /* % ######################################################### */ -#data_provider_issues { - width: 100%; - margin-top: 10px; - border: 0; -} - -#data_provider_issues td { - border: 1px solid gray; - border-width: 1px 0 0 0; -} - -#data_provider_issues td.shrinkable { - display: table; - overflow: hidden; - table-layout: fixed; - text-overflow: ellipsis; - white-space: nowrap; - width: 100%; -} - -#data_provider_issues tr.severity-trivial { +#dp_report_table tr.severity-trivial { background-color: #c9f3f3; } -#data_provider_issues tr.severity-minor { +#dp_report_table tr.severity-minor { background-color: #cef9ce; } -#data_provider_issues tr.severity-major { +#dp_report_table tr.severity-major { background-color: #fff1e6; } -#data_provider_issues tr.severity-critical { +#dp_report_table tr.severity-critical { background-color: #ffd3d3; } From 4e7790424151b7d85cad5ce5a6fd70cd99c049b8 Mon Sep 17 00:00:00 2001 From: Pierre Rioux Date: Mon, 28 Sep 2015 14:46:56 -0400 Subject: [PATCH 54/85] Added 'search for anything' feature --- .../app/controllers/portal_controller.rb | 21 +++ BrainPortal/app/models/admin_user.rb | 4 + BrainPortal/app/models/normal_user.rb | 4 + BrainPortal/app/models/site_manager.rb | 4 + BrainPortal/app/models/user.rb | 5 + .../app/views/layouts/_section_menu.html.erb | 94 ++++++++------ BrainPortal/app/views/portal/search.html.erb | 120 ++++++++++++++++++ .../config/initializers/validation_portal.rb | 2 +- BrainPortal/config/routes.rb | 1 + BrainPortal/lib/models_report.rb | 68 ++++++++++ BrainPortal/public/stylesheets/cbrain.css | 7 + 11 files changed, 289 insertions(+), 41 deletions(-) create mode 100644 BrainPortal/app/views/portal/search.html.erb diff --git a/BrainPortal/app/controllers/portal_controller.rb b/BrainPortal/app/controllers/portal_controller.rb index 98f83570f..25d9d1e95 100644 --- a/BrainPortal/app/controllers/portal_controller.rb +++ b/BrainPortal/app/controllers/portal_controller.rb @@ -356,6 +356,27 @@ def report #:nodoc: @filter_show_proc = (table_op =~ /sum.*size/) ? (Proc.new { |vector| colored_pretty_size(vector[0]) }) : nil end + # This action searches among all sorts of models for IDs or strings, + # and reports links to the matches found. + def search + + @search = params[:search] + @limit = 20 # used by interface only + + results = @search.present? ? ModelsReport.search_for_token(@search, current_user) : {} + + @users = results[:users] || [] + @tasks = results[:tasks] || [] + @groups = results[:groups] || [] + @files = results[:files] || [] + @rrs = results[:rrs] || [] + @dps = results[:dps] || [] + @sites = results[:sites] || [] + @tools = results[:tools] || [] + @tcs = results[:tcs] || [] + + end + private def merge_vals_as_array(*sub_reports) #:nodoc: diff --git a/BrainPortal/app/models/admin_user.rb b/BrainPortal/app/models/admin_user.rb index 33cd10fab..05484c7d0 100644 --- a/BrainPortal/app/models/admin_user.rb +++ b/BrainPortal/app/models/admin_user.rb @@ -42,6 +42,10 @@ def available_users #:nodoc: User.scoped end + def accessible_sites #:nodoc: + Site.scoped + end + def visible_users #:nodoc: User.scoped end diff --git a/BrainPortal/app/models/normal_user.rb b/BrainPortal/app/models/normal_user.rb index b30acea9a..49268c443 100644 --- a/BrainPortal/app/models/normal_user.rb +++ b/BrainPortal/app/models/normal_user.rb @@ -43,6 +43,10 @@ def available_users #:nodoc: User.where( :id => self.id ) end + def accessible_sites #:nodoc: + Site.where( :id => (self.site_id || -1) ) + end + def visible_users #:nodoc: User.where("users.type <> 'AdminUser'") end diff --git a/BrainPortal/app/models/site_manager.rb b/BrainPortal/app/models/site_manager.rb index 98ec22d05..e322dc891 100644 --- a/BrainPortal/app/models/site_manager.rb +++ b/BrainPortal/app/models/site_manager.rb @@ -48,6 +48,10 @@ def available_users #:nodoc: self.site.users end + def accessible_sites #:nodoc: + Site.where( :id => self.site_id ) + end + def visible_users #:nodoc: User.where("users.type <> 'AdminUser'") end diff --git a/BrainPortal/app/models/user.rb b/BrainPortal/app/models/user.rb index be44d288e..69e29fbc3 100644 --- a/BrainPortal/app/models/user.rb +++ b/BrainPortal/app/models/user.rb @@ -322,6 +322,11 @@ def available_users cb_error "#available_users called from User base class! Method must be implement in a subclass." end + # Return the list of sites accessible to the user + def accessible_sites + cb_error "#accessible_sites called from User base class! Method must be implement in a subclass." + end + # Can this user be accessed by +user+? def can_be_accessed_by?(user, access_requested = :read) #:nodoc: return true if user.has_role? :admin_user diff --git a/BrainPortal/app/views/layouts/_section_menu.html.erb b/BrainPortal/app/views/layouts/_section_menu.html.erb index 73766d5b1..4b7d7efc1 100644 --- a/BrainPortal/app/views/layouts/_section_menu.html.erb +++ b/BrainPortal/app/views/layouts/_section_menu.html.erb @@ -18,7 +18,7 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# along with this program. If not, see . # -%> @@ -27,10 +27,11 @@ + + <% if current_user && current_user.has_role?(:admin_user) && + ! (params[:controller] == 'portal' && params[:action] == 'search') %> +
    + <%= form_tag search_path, :method => :get do %> + <%= text_field_tag :search, nil, :placeholder => 'Search for anything' %> + <% end %> +
    + <% end %> + + <% if current_user %> + + + <% end %> + +
    diff --git a/BrainPortal/app/views/portal/search.html.erb b/BrainPortal/app/views/portal/search.html.erb new file mode 100644 index 000000000..cd91b67fc --- /dev/null +++ b/BrainPortal/app/views/portal/search.html.erb @@ -0,0 +1,120 @@ + +<% title 'Search' %> + +<%= form_tag(search_path, :method => :get) do %> + <%= text_field_tag :search, @search.presence, :placeholder => "Search for anything" %> + This will search files, tasks, users, tools, projects, etc by name or description. <%= @limit %> results shown maximum. +<% end %> + +<% if [ @users, @tasks, @groups, @files, @rrs, @dps, @sites, @tools, @tcs ].any? { |var| var.present? } %> + <%#

    Results

    %> +

    +<% end %> + +<% [ :files, :tasks, :users, :groups, :rrs, :dps, :sites, :tools, :tcs ].each do |var| %> + <% + # Compute a @x_size variable for each result set, and truncate results to 20 entries max. + # Should be using reflexive interface instead of an eval, but oh well. + eval " + array = @#{var} + @#{var}_size = array.size + @#{var} = @#{var}[0,#{@limit}] if array.size > #{@limit} + " + %> +<% end %> + +<% # this is used by array_to_table() below + tables_options = { + :rows => 5, + :fill_by_columns => true, + :table_class => "simple", + :td_class => "left_align", + } +%> + +<% if @files.present? %> +

    Files (found: <%= @files_size %>)

    + <%= array_to_table(@files, tables_options) do |file,r,c| %> + <%= link_to_userfile_if_accessible(file) %> (<%= file.user.login %>) + <% end %> +<% end %> + +<% if @tasks.present? %> +

    Tasks (found: <%= @tasks_size %>)

    + <%= array_to_table(@tasks, tables_options) do |task,r,c| %> + <%= link_to_model_if_accessible(CbrainTask,task,:name_and_bourreau) %> (<%= task.user.login %>) + <%= link_to "(edit)", edit_task_path(task), :class => 'action_link' %>
    + <%= overlay_description(task.description, :header_width => 35) %> + <% end %> +<% end %> + +<% if @users.present? %> +

    Users (found: <%= @users_size %>)

    + <%= array_to_table(@users, tables_options) do |user,r,c| %> + <%= user.full_name %> (<%= link_to_user_if_accessible(user) %>) + <%= link_to '(switch)', switch_user_path(user), :method => :post, :class => 'action_link' if current_user.has_role?(:admin_user) %> + <% end %> +<% end %> + +<% if @groups.present? %> +

    Projects (found: <%= @groups_size %>)

    + <%= array_to_table(@groups, tables_options) do |group,r,c| %> + <%= link_to_group_if_accessible(group) %> (<%= group.creator.try(:login) %>) + <%= link_to '(switch)', { :controller => :groups, :action => :switch, :id => group.id } , :method => :post, :class => 'action_link' %> + <% end %> +<% end %> + +<% if @rrs.present? %> +

    Execution Servers (found: <%= @rrs_size %>)

    + <%= array_to_table(@rrs, tables_options) do |rr,r,c| %> + <%= link_to_bourreau_if_accessible(rr) %> + <% task_count = current_user.cbrain_tasks.real_tasks.where(:bourreau_id => rr.id).count %> + <% if task_count > 0 %> + (<%= index_count_filter(task_count, :tasks, + { :user_id => current_user.id, :bourreau_id => rr.id}) %> tasks) + <% end %> + <% end %> +<% end %> + +<% if @dps.present? %> +

    Data Providers (found: <%= @dps_size %>)

    + <%= array_to_table(@dps, tables_options) do |dp,r,c| %> + <%= link_to_data_provider_if_accessible(dp) %> + <%= link_to '(browse)', browse_data_provider_path(dp), :class => 'action_link' if dp.is_browsable? %> + <% file_count = current_user.userfiles.where(:data_provider_id => dp.id).count %> + <% if file_count > 0 %> + (<%= index_count_filter(file_count, :userfiles, + { :user_id => current_user.id, :data_provider_id => dp.id}) %> registered files) + <% end %> + <% end %> +<% end %> + +<% if @sites.present? %> +

    Sites (found: <%= @sites_size %>)

    + <%= array_to_table(@sites, tables_options) do |site,r,c| %> + <%= link_to_site_if_accessible(site) %> + <% end %> +<% end %> + +<% if @tools.present? %> +

    Tools (found: <%= @tools_size %>)

    + <%= array_to_table(@tools, tables_options) do |tool,r,c| %> + <% if current_user.has_role?(:admin_user) %> + <%= link_to tool.name, edit_tool_path(tool) %> + <% else %> + <%= tool.name %> + <% end %> + <% end %> +<% end %> + +<% if @tcs.present? %> +

    Tool Versions (found: <%= @tcs_size %>)

    + <%= array_to_table(@tcs, tables_options) do |tc,r,c| %> + <% if current_user.has_role?(:admin_user) %> + <%= tc.tool.name %>@<%= tc.bourreau.name%> : <%= link_to tc.version_name, edit_tool_config_path(tc) %> + <% else %> + <%= tc.tool.name %>@<%= tc.bourreau.name%> : <%= tc.version_name %> + <% end %> + <% end %> +<% end %> + diff --git a/BrainPortal/config/initializers/validation_portal.rb b/BrainPortal/config/initializers/validation_portal.rb index 42469593c..2a358a893 100644 --- a/BrainPortal/config/initializers/validation_portal.rb +++ b/BrainPortal/config/initializers/validation_portal.rb @@ -72,7 +72,7 @@ # # Rake Exceptions By First Argument # - skip_validations_for = [ /^db:/, /^cbrain:plugins/ ] + skip_validations_for = [ /^db:/, /^cbrain:plugins/, /^route/ ] if skip_validations_for.any? { |p| first_arg =~ p } #------------------------------------------------------------------------------ puts "C> \t- No validations needed. Skipping." diff --git a/BrainPortal/config/routes.rb b/BrainPortal/config/routes.rb index d782dbef4..c0122e30d 100644 --- a/BrainPortal/config/routes.rb +++ b/BrainPortal/config/routes.rb @@ -156,6 +156,7 @@ post '/home' => 'portal#welcome' # lock/unlock service get '/credits' => 'portal#credits' get '/about_us' => 'portal#about_us' + get '/search' => 'portal#search' get '/login' => 'sessions#new' get '/logout' => 'sessions#destroy' get '/session_status' => 'sessions#show' diff --git a/BrainPortal/lib/models_report.rb b/BrainPortal/lib/models_report.rb index 96ab104f0..84187896f 100644 --- a/BrainPortal/lib/models_report.rb +++ b/BrainPortal/lib/models_report.rb @@ -131,5 +131,73 @@ def self.rr_usage_statistics(options) stats end + # This method implements the 'search for anything' for the controller portal action +search+ + # It's also used in the rails console's shortcuts. + # + # If +token+ looks like an ID, the models are searched by ID only. + # Otherwise, models are searched by name, version_name, description, etc. + # + # It returns a hash table with these keys: + # + # { + # :users => [], # array of User objects + # :tasks => [], # CbrainTask objects + # :groups => [], # Group objects + # :files => [], # Userfile objects + # :rrs => [], # RemoteResource objects + # :dps => [], # DataProvider objects + # :sites => [], # Site objects + # :tools => [], # Tool objects + # :tcs => [], # ToolConfig objects + # } + def self.search_for_token(token, user=current_user) #:nodoc: + + token = token.to_s.presence || "-999" + is_numeric = token =~ /^\d+$/ || token == "-999" + + + file_scope = Userfile .find_all_accessible_by_user(user) .order(:name) + task_scope = CbrainTask .find_all_accessible_by_user(user) .order(:id) + rr_scope = RemoteResource.find_all_accessible_by_user(user) .order(:name) + dp_scope = DataProvider .find_all_accessible_by_user(user) .order(:name) + tool_scope = Tool .find_all_accessible_by_user(user) .order(:name) + tc_scope = ToolConfig .find_all_accessible_by_user(user) .order(:tool_id) + + # For the next three, wow: 'available' and 'accessible' have reverse meaning! + # 'available' means user can modify them, 'accessible' means they can only view. + user_scope = user.available_users .order(:login) + group_scope = user.available_groups .order(:name) + site_scope = user.accessible_sites .order(:name) + + results = if (is_numeric) + { + :users => Array(user_scope .find_by_id(token)) , + :tasks => Array(task_scope .find_by_id(token)) , + :groups => Array(group_scope .find_by_id(token)) , + :files => Array(file_scope .find_by_id(token)) , + :rrs => Array(rr_scope .find_by_id(token)) , + :dps => Array(dp_scope .find_by_id(token)) , + :sites => Array(site_scope .find_by_id(token)) , + :tools => Array(tool_scope .find_by_id(token)) , + :tcs => Array(tc_scope .find_by_id(token)) , + } + else + ptoken = "%#{token}%" + { + :users => user_scope .where( [ "login like ? OR full_name like ? OR email like ?", ptoken, ptoken, ptoken ] ).all , + :tasks => task_scope .where( [ "description like ?" , ptoken ] ).all , + :groups => group_scope .where( [ "name like ?" , ptoken ] ).all , + :files => file_scope .where( [ "name like ? OR description like ?" , ptoken, ptoken ] ).all , + :rrs => rr_scope .where( [ "name like ? OR description like ?" , ptoken, ptoken ] ).all , + :dps => dp_scope .where( [ "name like ? OR description like ?" , ptoken, ptoken ] ).all , + :sites => site_scope .where( [ "name like ? OR description like ?" , ptoken, ptoken ] ).all , + :tools => tool_scope .where( [ "name like ? OR description like ?" , ptoken, ptoken ] ).all , + :tcs => tc_scope .where( [ "version_name like ? OR description like ?" , ptoken, ptoken ] ).all , + } + end + + results + end + end diff --git a/BrainPortal/public/stylesheets/cbrain.css b/BrainPortal/public/stylesheets/cbrain.css index 2dd8502b7..4f05c651e 100644 --- a/BrainPortal/public/stylesheets/cbrain.css +++ b/BrainPortal/public/stylesheets/cbrain.css @@ -971,6 +971,13 @@ pre { padding: 0; } +#cbsearch { + padding: 1em 1em 0 0; + text-align: right; + float: right; + vertical-align: top; +} + #main-menu { position: absolute; bottom: 0px; From b9f4d08ea1085f779f1860b0831f886a531b2c7f Mon Sep 17 00:00:00 2001 From: Remi Bernard Date: Mon, 28 Sep 2015 16:09:15 -0400 Subject: [PATCH 55/85] Fix for custom filters + scopes available filters bug The available filters now recognize (somewhat) the presence of custom filters and the filter menus are populated as expected. Note that this fix relies on undo_where, and will probably bring back issue #125, which would've been fixed by relying purely on Scope objects. This changeset can be reverted once custom filters are ported to the Scope API, which will let filter_values_for remove the corresponding filter without having to rely on undo_where (provided it is enhanced to allow using multiple Scope objects). --- BrainPortal/app/helpers/scope_helper.rb | 9 ++++++++- .../relation_extensions/undo_where.rb | 3 ++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/BrainPortal/app/helpers/scope_helper.rb b/BrainPortal/app/helpers/scope_helper.rb index 5da5eb26a..6b25b7703 100644 --- a/BrainPortal/app/helpers/scope_helper.rb +++ b/BrainPortal/app/helpers/scope_helper.rb @@ -572,9 +572,16 @@ def model_filter_values(model, attribute, label, association: nil, scope: nil) # as +attribute+. label_alias = model.connection.quote_column_name('label') + # FIXME: undo_where is somewhat hacky and not exactly correct sometimes, but + # it is still required as some filters (custom filters) are still applied + # directly (without a Scope) and cannot be easily removed. Ideally, those + # filters should be ported to the Scope API and filter_values_for should + # accept a collection of scope objects to generate the separate counts with. + bare_model = model.undo_where(attribute) + # Fetch the main filter values as an array of arrays: # [[value, label, count], [...]] - filters = model + filters = bare_model .where("#{attribute} IS NOT NULL") .order(label, attribute) .group(attribute, label) diff --git a/BrainPortal/lib/cbrain_extensions/active_record_extensions/relation_extensions/undo_where.rb b/BrainPortal/lib/cbrain_extensions/active_record_extensions/relation_extensions/undo_where.rb index c1f22a718..859e5c4e7 100644 --- a/BrainPortal/lib/cbrain_extensions/active_record_extensions/relation_extensions/undo_where.rb +++ b/BrainPortal/lib/cbrain_extensions/active_record_extensions/relation_extensions/undo_where.rb @@ -54,7 +54,8 @@ def undo_where(*args) to_reject = {} # "tab1.col1" => true, "tab1.col2" => true etc... args.map do |colspec| # "col" or "table.col" - raise "Invalid column specification \"#{colspec}\"." unless colspec.to_s =~ /^((\w+)\.)?(\w+)$/ + raise "Invalid column specification \"#{colspec}\"." unless + colspec.to_s =~ /^(\`?(\w+)\`?\.)?\`?(\w+)\`?$/ tab = Regexp.last_match[2].presence || mytable col = Regexp.last_match[3] to_reject["#{tab}.#{col}"] = true From e2b1c2208d0609b38a2d2dc51fcbeb650d9ef893 Mon Sep 17 00:00:00 2001 From: Remi Bernard Date: Fri, 2 Oct 2015 14:51:23 -0400 Subject: [PATCH 56/85] Dual-count (AR relation/Scope) filters Final (hopefully) implementation of scoped/dual-count filters, to restore the missing functionality from the old filtering system. This new implementation also fixes two bugs related to filtering and undo_where (as it is no longer used for filters and will be refactored out in a later commit), and allows for future expansion by supporting both Scope-based filtering (on one or more Scopes) and classical AR filtering. This changeset also fixes a minor bug where custom filters could not be removed by the 'clear' link. --- .../app/controllers/tasks_controller.rb | 8 +- .../app/controllers/userfiles_controller.rb | 7 +- BrainPortal/app/helpers/scope_helper.rb | 353 +++++++++++------- .../messages/_message_index_display.html.erb | 30 +- .../app/views/shared/_active_filters.html.erb | 9 +- .../app/views/tasks/_tasks_display.html.erb | 12 +- .../userfiles/_userfiles_display.html.erb | 8 +- 7 files changed, 261 insertions(+), 166 deletions(-) diff --git a/BrainPortal/app/controllers/tasks_controller.rb b/BrainPortal/app/controllers/tasks_controller.rb index 745cdc3bf..18d5f4334 100644 --- a/BrainPortal/app/controllers/tasks_controller.rb +++ b/BrainPortal/app/controllers/tasks_controller.rb @@ -42,10 +42,10 @@ def index #:nodoc: end @scope.pagination ||= Scope::Pagination.from_hash({ :per_page => 25 }) - @base_scope = custom_scope(user_scope( - current_user.available_tasks.includes([:bourreau, :user, :group]) - )) - @view_scope = @scope.apply(@base_scope) + @base_scope = user_scope(current_user.available_tasks) + .includes([:bourreau, :user, :group]) + @custom_scope = custom_scope(@base_scope) + @view_scope = @scope.apply(@custom_scope) # Display totals @total_tasks = @view_scope.count diff --git a/BrainPortal/app/controllers/userfiles_controller.rb b/BrainPortal/app/controllers/userfiles_controller.rb index 38b012ced..cdfb64947 100644 --- a/BrainPortal/app/controllers/userfiles_controller.rb +++ b/BrainPortal/app/controllers/userfiles_controller.rb @@ -53,10 +53,9 @@ def index #:nodoc: # Apply basic and @scope-based scoping scope_default_order(@scope, 'name') - @base_scope = custom_scope( - base_scope.includes([:user, :data_provider, :sync_status, :tags, :group]) - ) - @view_scope = @scope.apply(@base_scope) + @base_scope = base_scope.includes([:user, :data_provider, :sync_status, :tags, :group]) + @custom_scope = custom_scope(@base_scope) + @view_scope = @scope.apply(@custom_scope) # Generate tag filters tag_counts = @view_scope.joins(:tags).group('tags.name').count diff --git a/BrainPortal/app/helpers/scope_helper.rb b/BrainPortal/app/helpers/scope_helper.rb index 6b25b7703..0c89a0a31 100644 --- a/BrainPortal/app/helpers/scope_helper.rb +++ b/BrainPortal/app/helpers/scope_helper.rb @@ -339,21 +339,6 @@ def pretty_scope_filter(filter, model: nil) # filter values but without having to unpack the +DynamicTable+'s filter # format hash. # - # [scope] - # Generate a separate set of filter value counts (how many times a given - # filter value is found) for +collection+ when scoped under +scope+. For - # example, if a given userfile collection has 30 TextFiles out of 40 and - # 10 of those 30 belong to a given user, the unscoped TextFile filter count - # would be 30 and the user-scoped count 10. When this option is specified, - # +format+ takes a fourth argument; the corresponding scoped count, and, - # if +format+ is unspecified, the two counts (normal and scoped) are appended - # to the default label value. For example, in the above TextFile example, - # the label would look like 'TextFile (10/30)'. - # - # Note that any Filter filtering on +attribute+ in +scope+ will be skipped to - # avoid generating empty filter values (while this behavior is - # counter-intuitive and clunky, it is required for table filtering). - # # NOTE: The filters generated by this method are commonly used with the Scope # and DynamicTable APIs, which impose an hidden restriction on the values # that can be taken for +attribute+'s values; they must be safely @@ -361,28 +346,18 @@ def pretty_scope_filter(filter, model: nil) # types with the notable exception of Symbols. If +attribute+'s values are # symbols, consider converting them the resulting filter values to strings # before using them to construct filtering URLs. - def filter_values_for(collection, attribute, label: nil, association: nil, format: nil, scope: nil) + def filter_values_for(collection, attribute, label: nil, association: nil, format: nil) return [] if collection.blank? - # Remove any +attribute+ filter on +scope+ - if scope - scope = scope.dup - scope.filters.reject! do |f| - f.attribute.to_s.downcase == attribute.to_s.downcase - end - end - # Invoke the model/collection specific method if (collection <= ActiveRecord::Base rescue nil) - filters = model_filter_values( + filters = ScopeHelper.model_filter_values( collection, attribute, label, - association: association, - scope: scope + association: association ) else - filters = collection_filter_values( - collection, attribute, label, - scope: scope + filters = ScopeHelper.collection_filter_values( + collection, attribute, label ) end @@ -390,66 +365,204 @@ def filter_values_for(collection, attribute, label: nil, association: nil, forma return filters.map(&format) if format # Format the generated filter arrays as DynamicTable's filter hashes - filters.map do |value, label, count, *rest| - scoped = rest.first - label = "#{label} (of #{count})" if scoped + filters.map do |value, label, count| { :value => value, :label => label, - :indicator => scoped || count, - :empty => (scoped || count) == 0 + :indicator => count, + :empty => count == 0 } end end - # Fetch the possible values for +attribute+ within +collection+. - # A simple wrapper around +filter_values_for+, this method offers a set - # of commonly used defaults to +filter_values_for+ when generating simple + # Fetch the possible filtering values for +attribute+ (symbol or string) + # within +base+, viewed through +view+. +base+ is expected to be either an + # ActiveRecord model (or scope) or Ruby Enumerable, while +view+ is expected + # to be either the same type as +base+ or one or more Scope objects. This + # method generates filtering values (similarly to +filter_values_for+), + # considering +view+ as a filtered version of +base+ and counting possible + # values in both. + # + # For example, if +base+ is the Userfiles model containing TextFiles, CSVFiles + # and MincFiles and +view+ is a scope of base with just a few CSVFiles and + # MincFiles, +scoped_filters_for+ one could have: + # scoped_filters_for(base, view, :type) # => + # [ + # { :value => 'TextFile', :indicator => 0, :label => '... (of 30)', ... }, + # { :value => 'CSVFile', :indicator => 9, :label => '... (of 10)', ... }, + # { :value => 'MincFile', :indicator => 3, :label => '... (of 53)', ... }, + # ] + # + # This method accepts the following optional (named) arguments: + # [scope] + # One or more Scope objects to add on top of +view+, when +view+ isn't a + # collection of Scopes already. + # + # [label] + # Handled the same way as +filter_values_for+'s own +label+ parameter. Note + # that this includes the 'name' default if +attribute+ is nil and + # +association+ is specified. + # + # [association] + # Handled the same way as +filter_values_for+'s own +association+ parameter. + # + # [format] + # Lambda (or proc) to format the filter list with. Similar in behavior and + # handled the same way as +filter_values_for+'s own +format+ argument. The + # only difference between this method's +format+ and +filter_values_for+'s + # is a fourth argument, the possible value count under +view+. + # The arguments given to +format+ are thus: value, label, base count and + # view count. + # + # [strip_filters] + # Remove any Filter filtering on +attribute+ in all given Scope instances + # before applying them to +view+ (and generating filter counts). This avoids + # generating almost-empty filters (only a single value would have a count + # higher than 0). Defaults to active (true). + # + # Note that if Scope objects are supplied, any of their Filters filtering on + # +attribute+ will be removed to avoid generating empty filters values unless + # the +strip_filters+ argument is specified as false. + # + # Also note that this method shares +filter_values_for+'s special + # interaction between +association+ and +attribute+; if +association+ is + # specified and +attribute+ is nil, +attribute+ is taken as +base+'s + # foreign key on +association+ (see +filter_values_for+'s +association+ + # parameter for more information). + def scoped_filters_for(base, view, attribute, scope: nil, label: nil, association: nil, format: nil, strip_filters: true) + # Handle +filter_values_for+'s special case where +attribute+ is nil and + # corresponds to +base+'s foreign key for +association+ + attribute, label, association = ScopeHelper.find_assoc_key(base, association, label) if + ! attribute && association + + # Normalize +attribute+ to a lower-case string, as per Scope API conventions + attribute = attribute.to_s.downcase + + # Normalize the scopes in +view+ and +scope+ into just scopes + scopes = scope || [] + scopes = [scopes] unless scopes.is_a?(Enumerable) + + if view.is_a?(Scope) || (view.is_a?(Enumerable) && view.all? { |v| v.is_a?(Scope) }) + scopes += Array(view) + view = base + end + + # Filter out +attribute+ filters, if theres any (and strip_filters + # is enabled) + scopes.map! do |scope| + next scope unless scope.filters.map(&:attribute).include?(attribute) + + scope = scope.dup + scope.filters.reject! { |f| f.attribute == attribute } + + scope + end if strip_filters + + # Generate the base filter set, with all possible values for +attribute+ in + # +base+ + filters = filter_values_for( + base, attribute, label: label, + association: association, + format: lambda { |x| x } + ) + + # Generate another filter set under +view+ (and all given scopes), as a + # hash ( => []) and append the generated counts to each + # value in the base filter set + view_filters = filter_values_for( + scopes.inject(view) { |v, s| s.apply(v) }, + attribute, + association: association, + format: lambda { |x| x } + ).index_by { |f| f.first } + + filters.each { |f| f << (view_filters[f.first].last rescue 0) } + + # Use the specified +format+ if there is one + return filters.map(&format) if format + + # Otherwise format the generated filter arrays as DynamicTable's filter + # hashes, just like +filter_values_for+'s default format. + filters.map do |value, label, base, view| + { + :value => value, + :label => "#{label} (of #{base})", + :indicator => view, + :empty => view == 0 + } + end + end + + # Fetch the possible values for an +attribute+ within a given +collection+, + # optionally filtered under +view+. There are two ways to invoke + # +default_filters_for+: + # default_filters_for(collection, attribute) + # and: + # default_filters_for(collection, view, attribute) + # depending on whether or not +view+ is set. + # A simple wrapper around +filter_values_for+ and +scoped_filters_for+, this + # method offers a set of commonly used defaults when generating simple # attribute filters. # - # +collection+ and +attribute+ are passed directly to +filter_values_for+, - # with one exception; if both +collection+ and +attribute+ are AR models - # (or scopes), +default_filters_for+ will pass +attribute+ as an association - # on +collection+ instead: + # +collection+ and +attribute+ are passed directly to +filter_values_for+ or + # +scoped_filters_for+, with one exception; if both +collection+ and + # +attribute+ are AR models (or scopes), +default_filters_for+ will pass + # +attribute+ as an association on +collection+ instead: # default_filters_for(some_scope, SomeModel) # corresponds to: # filter_values_for(scope_scope, nil, association: SomeModel) # # For more information on how filter values are generated, - # see +filter_values_for+. - def default_filters_for(collection, attribute) + # see +filter_values_for+ and +scoped_filters_for+. + def default_filters_for(*args) # Generate a formatting lambda which will call +block+ to format a filter # value's label. formatter = lambda do |block| return unless block lambda do |args| - value, label, count, scoped = args + value, label, base, view = args label = block.(label) rescue label - label = "#{label} (of #{count})" if scoped + label = "#{label} (of #{base})" if view { :value => value, :label => label, - :indicator => scoped || count, - :empty => (scoped || count) == 0 + :indicator => view || base, + :empty => (view || base) == 0 } end end + # Unpack args, respecting the possible ways to call +default_filters_for+ + collection = args.shift + attribute = args.pop + view = args.first + + # Pre-set some defaults + is_assoc = attribute <= ActiveRecord::Base rescue nil + label = 'login' if is_assoc && attribute <= User + format = formatter.(( + proc { |l| l.constantize.pretty_type } if + attribute.to_s.downcase == 'type' + )) unless is_assoc + + # Invoke +filter_values_for+ or +scoped_filters_for+ to generate the actual + # filters, depending or whether or not a scope/view has been specified. filters = ( - # Is +attribute+ an association? - if (attribute <= ActiveRecord::Base rescue nil) - filter_values_for(collection, nil, association: attribute, - scope: @scope, - label: ('login' if attribute <= User) + if view || @scope + scoped_filters_for( + collection, (view || collection), (attribute unless is_assoc), + association: (attribute if is_assoc), + scope: @scope, + label: label, + format: format ) else - filter_values_for(collection, attribute, - scope: @scope, - format: formatter.(( - proc { |l| l.constantize.pretty_type } if - attribute.to_s.downcase == 'type' - )) + filter_values_for( + collection, (attribute unless is_assoc), + association: (attribute if is_assoc), + label: label, + format: format ) end ) @@ -529,33 +642,16 @@ def generic_scope_link(label, url, options) # Fetch the possible values (and their count) for +attribute+ within +model+, # an ActiveRecord model. Internal model-specific implementation of # +filter_values_for+ for AR models; see +filter_values_for+ for more - # information on this method's arguments (which are handled just like - # +filter_values_for+'s, save for +scope+). + # information on this method's arguments. # # Note that the filter values returned by this method are in array format, - # ([value, label, count, (scoped_count)]) as this method is intended for - # internal use by +filter_values_for+ which will perform final formatting. - def model_filter_values(model, attribute, label, association: nil, scope: nil) + # ([value, label, count]) as this method is intended for internal use by + # +filter_values_for+ which will perform final formatting. + def self.model_filter_values(model, attribute, label, association: nil) # Handle the special case where +attribute+ is nil and corresponds to # +model+'s foreign key for +association+ - if ! attribute && association - # Find the matching +association+ reflection on +model+ - assoc = association.respond_to?(:klass) ? association.klass : association - reflection = model - .reflect_on_all_associations - .find { |r| r.klass == assoc } - raise "no associations on '#{model.table_name}' matching '#{assoc.table_name}'" unless - reflection - - # Use +association+'s reflection to set missing argument values - attribute = reflection.foreign_key - label = "#{reflection.table_name}.#{label || 'name'}" - association = [ - assoc, - reflection.association_primary_key, - reflection.association_foreign_key - ] - end + attribute, label, association = ScopeHelper.find_assoc_key(base, association, label) if + ! attribute && association # Resolve and validate the main +attribute+ to fetch the values of attribute, model = ViewScopes.resolve_model_attribute(attribute, model, association) @@ -572,16 +668,9 @@ def model_filter_values(model, attribute, label, association: nil, scope: nil) # as +attribute+. label_alias = model.connection.quote_column_name('label') - # FIXME: undo_where is somewhat hacky and not exactly correct sometimes, but - # it is still required as some filters (custom filters) are still applied - # directly (without a Scope) and cannot be easily removed. Ideally, those - # filters should be ported to the Scope API and filter_values_for should - # accept a collection of scope objects to generate the separate counts with. - bare_model = model.undo_where(attribute) - # Fetch the main filter values as an array of arrays: # [[value, label, count], [...]] - filters = bare_model + model .where("#{attribute} IS NOT NULL") .order(label, attribute) .group(attribute, label) @@ -589,61 +678,67 @@ def model_filter_values(model, attribute, label, association: nil, scope: nil) .reject { |r| r.first.blank? } .map(&:to_a) .to_a - - # No +scope+? Then +filters+ is ready - return filters unless scope - - # Add in the scoped counts - scoped = scope.apply(model) - .where("#{attribute} IS NOT NULL") - .group(attribute) - .raw_rows(attribute, "COUNT(#{attribute})") - .to_h - - filters.map { |f| f << (scoped[f.first] || 0) } end # Fetch the possible values (and their count) for +attribute+ within # +collection+, a generic Ruby collection. Internal collection-specific # implementation of +filter_values_for+ for Ruby collections; see - # +filter_values_for+ for more information on this method's arguments - # (which are handled just like +filter_values_for+'s, save for +scope+). + # +filter_values_for+ for more information on this method's arguments. # # Note that the filter values returned by this method are in array format, - # ([value, label, count, (scoped_count)]) as this method is intended for - # internal use by +filter_values_for+ which will perform final formatting. - def collection_filter_values(collection, attribute, label, scope: nil) + # ([value, label, count]) as this method is intended for internal use by + # +filter_values_for+ which will perform final formatting. + def self.collection_filter_values(collection, attribute, label) # Make sure +attribute+ and +label+ can be accessed in # +collection+'s items. attr_get = ViewScopes.generate_getter(collection.first, attribute) raise "no way to get '#{attribute}' out of collection items" unless attr_get - if label == attribute - lbl_get = attr_get - else - lbl_get = ViewScopes.generate_getter(collection.first, label || attribute) + if label && label != attribute + lbl_get = ViewScopes.generate_getter(collection.first, label) raise "no way to get '#{label}' out of collection items" unless lbl_get + else + lbl_get = attr_get end - # Generate the main filter values as a hash; [value, label] => count - count_values = lambda do |collection| - collection - .map { |i| [attr_get.(i), lbl_get.(i)].freeze } - .reject { |v, l| v.blank? } - .sort_by { |v, l| l } - .inject(Hash.new(0)) { |h, i| h[i] += 1; h } - end - - filters = count_values.(collection) + # Generate the main filter values as an array of arrays: + # [[value, label, count], [...]] + collection + .map { |i| [attr_get.(i), lbl_get.(i)].freeze } + .reject { |v, l| v.blank? } + .sort_by { |v, l| l } + .inject(Hash.new(0)) { |h, i| h[i] += 1; h } + .map { |(v, l), c| [v, l, c] } + end - # Add in the scoped counts, if required, then flatten the hash into - # array format - if scope - scoped = count_values.(scope.apply(collection)) - filters.map { |i, c| [*i, c, scoped[i] || 0] } - else - filters.map { |(v, l), c| [v, l, c] } - end + # Find the first association/relation on +model+ matching +association+, + # if it exists, and return, as an array; + # - The foreign key between +model+ and +association+, + # - A qualified +label+ column name for the association (defaults to 'name'), + # - A fully qualified association specification (assoc and join columns) + # This array format directly corresponds to +scoped_filters_for+ and + # +filter_values_for+'s attribute, label and association parameters. + # + # Internal method to implement +scoped_filters_for+ and +filter_values_for+'s + # special +attribute+-is-nil handling. + def self.find_assoc_key(model, association, label = nil) + # Find the matching +association+ reflection on +model+ + association = association.klass if association.respond_to?(:klass) + reflection = model + .reflect_on_all_associations + .find { |r| r.klass == association } + raise "no associations on '#{model.table_name}' matching '#{association.table_name}'" unless + reflection + + [ + reflection.foreign_key, + "#{reflection.table_name}.#{label || 'name'}", + [ + association, + reflection.association_primary_key, + reflection.association_foreign_key + ] + ] end # Deprecated/old methods diff --git a/BrainPortal/app/views/messages/_message_index_display.html.erb b/BrainPortal/app/views/messages/_message_index_display.html.erb index 84d5f443a..88c11e68c 100644 --- a/BrainPortal/app/views/messages/_message_index_display.html.erb +++ b/BrainPortal/app/views/messages/_message_index_display.html.erb @@ -100,16 +100,15 @@ t.column("⚠".html_safe, :critical, :pretty_name => 'Critical', :hidden => true, - :filters => filter_values_for( - @base_scope, :critical, - scope: @scope, - format: lambda do |value, label, count, scoped| + :filters => scoped_filters_for( + @base_scope, @scope, :critical, + format: lambda do |value, label, base, view| label = (! value || value == 0) ? 'Not critical' : 'Critical' { :value => value, - :label => "#{label} (of #{count})", - :indicator => scoped, - :empty => scoped == 0 + :label => "#{label} (of #{base})", + :indicator => view, + :empty => view == 0 } end ) @@ -118,15 +117,14 @@ t.column("Type", :message_type, :hidden => true, :sortable => true, - :filters => filter_values_for( - @base_scope, :message_type, - scope: @scope, - format: lambda do |value, label, count, scoped| + :filters => scoped_filters_for( + @base_scope, @scope, :message_type, + format: lambda do |value, label, base, view| { :value => value, - :label => "#{label.humanize} (of #{count})", - :indicator => scoped, - :empty => scoped == 0 + :label => "#{label.humanize} (of #{base})", + :indicator => view, + :empty => view == 0 } end ) @@ -145,9 +143,8 @@ :sortable => true, :filters => filter_values_for( @base_scope, :sender_id, - scope: @scope, label: 'users.login', - association: [User, 'id', 'sender_id'], + association: [User, 'id', 'sender_id'] ) + [{ :value => nil, :label => 'System', @@ -161,7 +158,6 @@ :sortable => true, :filters => filter_values_for( @base_scope, :user_id, - scope: @scope, label: 'users.login', association: [User, 'id', 'user_id'] ) diff --git a/BrainPortal/app/views/shared/_active_filters.html.erb b/BrainPortal/app/views/shared/_active_filters.html.erb index 922e353fb..8335ab4c1 100644 --- a/BrainPortal/app/views/shared/_active_filters.html.erb +++ b/BrainPortal/app/views/shared/_active_filters.html.erb @@ -32,8 +32,13 @@ Active Filters (<%= - scope_filter_link('clear', - scope, :clear, nil, + empty = scope.dup + empty.filters.clear + empty.custom[:custom_filters] = [] if + empty.custom.has_key?(:custom_filters) + + scope_link('clear', + scope.name, empty.to_hash, link: { :ajax => true } ) %>) diff --git a/BrainPortal/app/views/tasks/_tasks_display.html.erb b/BrainPortal/app/views/tasks/_tasks_display.html.erb index ff03c9897..d80b14754 100644 --- a/BrainPortal/app/views/tasks/_tasks_display.html.erb +++ b/BrainPortal/app/views/tasks/_tasks_display.html.erb @@ -125,7 +125,7 @@ <% t.column("Task Type", :type, :sortable => true, - :filters => default_filters_for(@base_scope, :type) + :filters => default_filters_for(@base_scope, @custom_scope, :type) ) do |type,id,count,task| %> <% case type %> @@ -146,22 +146,22 @@ t.column("Owner", :owner, :sortable => true, - :filters => default_filters_for(@base_scope, User) + :filters => default_filters_for(@base_scope, @custom_scope, User) ) do |type,id,count,task| link_to_user_if_accessible(task.user) unless type == :loaded_batch end t.column("Project", :project, :sortable => true, - :filters => default_filters_for(@base_scope, Group) + :filters => default_filters_for(@base_scope, @custom_scope, Group) ) do |type,id,count,task| link_to_group_if_accessible(task.group) unless type == :loaded_batch end unless current_project t.column("Execution Server", :server, :sortable => true, - :filters => filter_values_for( - @base_scope, :bourreau_id, + :filters => scoped_filters_for( + @base_scope, @custom_scope, :bourreau_id, scope: @scope, label: 'remote_resources.name', association: [Bourreau, 'id', 'bourreau_id'] @@ -174,7 +174,7 @@ <% t.column("Current Status", :status, :sortable => true, - :filters => default_filters_for(@base_scope, :status) + :filters => default_filters_for(@base_scope, @custom_scope, :status) ) do |type,id,count,task| %> <% diff --git a/BrainPortal/app/views/userfiles/_userfiles_display.html.erb b/BrainPortal/app/views/userfiles/_userfiles_display.html.erb index ddc565f80..38e353900 100644 --- a/BrainPortal/app/views/userfiles/_userfiles_display.html.erb +++ b/BrainPortal/app/views/userfiles/_userfiles_display.html.erb @@ -115,12 +115,12 @@ t.column("File Type", :type, :field_name => :pretty_type, :sortable => true, - :filters => default_filters_for(@base_scope, :type) + :filters => default_filters_for(@base_scope, @custom_scope, :type) ) t.column("Owner", :login, :sortable => true, - :filters => default_filters_for(@base_scope, User) + :filters => default_filters_for(@base_scope, @custom_scope, User) ) { |u| link_to_user_if_accessible(u.user) } t.column("Creation Date", :creation_date, @@ -185,7 +185,7 @@ unless current_project t.column("Project", :group, :sortable => true, - :filters => default_filters_for(@base_scope, Group) + :filters => default_filters_for(@base_scope, @custom_scope, Group) ) do |u| link_to_group_if_accessible(u.group) if u.group end @@ -199,7 +199,7 @@ t.column("Provider", :data_provider, :sortable => true, - :filters => default_filters_for(@base_scope, DataProvider) + :filters => default_filters_for(@base_scope, @custom_scope, DataProvider) ) do |u| link_to_data_provider_if_accessible(u.data_provider) end From 4e4c71f6a5228a379a06bd5cd665bc5d97211a6b Mon Sep 17 00:00:00 2001 From: Remi Bernard Date: Fri, 2 Oct 2015 15:13:59 -0400 Subject: [PATCH 57/85] Removal of undo_where.rb undo_where was mainly used for computing filter counts, and was causing subtle bugs. As this functionality has been completely replaced as of the last commit, this changeset removes undo_where and refactors the only other place it was in use (userfiles controller, for view_hidden). --- .../app/controllers/userfiles_controller.rb | 11 +- .../relation_extensions/undo_where.rb | 101 ------------------ 2 files changed, 7 insertions(+), 105 deletions(-) delete mode 100644 BrainPortal/lib/cbrain_extensions/active_record_extensions/relation_extensions/undo_where.rb diff --git a/BrainPortal/app/controllers/userfiles_controller.rb b/BrainPortal/app/controllers/userfiles_controller.rb index cdfb64947..d73c7665b 100644 --- a/BrainPortal/app/controllers/userfiles_controller.rb +++ b/BrainPortal/app/controllers/userfiles_controller.rb @@ -57,6 +57,12 @@ def index #:nodoc: @custom_scope = custom_scope(@base_scope) @view_scope = @scope.apply(@custom_scope) + # Are hidden files displayed? + unless @scope.custom[:view_hidden] + @hidden_total = @view_scope.where(:hidden => true).count + @view_scope = @view_scope.where(:hidden => false) + end + # Generate tag filters tag_counts = @view_scope.joins(:tags).group('tags.name').count @tag_filters = @base_scope @@ -74,10 +80,8 @@ def index #:nodoc: # Generate display totals @userfiles_total = @view_scope.count('distinct userfiles.id') - @archived_total = @view_scope.where(:archived => true).count + @archived_total = @view_scope.where(:archived => true).count @immutable_total = @view_scope.where(:immutable => true).count - @hidden_total = @view_scope.undo_where(:hidden).where(:hidden => true).count unless - @scope.custom[:view_hidden] @userfiles_total_size = @view_scope.sum(:size) # Prepare the Pagination object @@ -1606,7 +1610,6 @@ def base_scope end base = base.where(:group_id => current_project.id) if current_project - base = base.where(:hidden => false) unless @scope.custom[:view_hidden] base end diff --git a/BrainPortal/lib/cbrain_extensions/active_record_extensions/relation_extensions/undo_where.rb b/BrainPortal/lib/cbrain_extensions/active_record_extensions/relation_extensions/undo_where.rb deleted file mode 100644 index 859e5c4e7..000000000 --- a/BrainPortal/lib/cbrain_extensions/active_record_extensions/relation_extensions/undo_where.rb +++ /dev/null @@ -1,101 +0,0 @@ - -# -# CBRAIN Project -# -# Copyright (C) 2008-2012 -# The Royal Institution for the Advancement of Learning -# McGill University -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -module CBRAINExtensions #:nodoc: - module ActiveRecordExtensions #:nodoc: - module RelationExtensions - # ActiveRecord::Relation Added Behavior; remove a where() condition from an existing relation. - module UndoWhere - - Revision_info=CbrainFileRevision[__FILE__] #:nodoc: - - # Returns a new Relation where some clauses have been - # removed; the arguments can be one or several attribute - # names (which can be qualified with table names). - # Any previous 'where' clauses that match one of these - # attributes will be removed. - # - # r = Author.joins(:book).where(:last => 'Austen').where('first like "J%"').where('books.title' like "Pride%") - # r.undo_where(:first, 'books.title') # => same as just: last = 'Austen' - # r.undo_where('title') # => does nothing (unless Author has also a :title !) - # r.undo_where('authors.last') # => same as undo_where(:last) - # - # Note that if a previous where() restriction was a long string - # with several subclauses, the entire string will be rejected - # as soon as somewhere inside we can detect that at least one - # subclause contained one of the attributes given in argument. - # - # r = Author.where([ "(id not in (?) or type = ?)", [2,3,4], 'AdminUser' ]) - # r.undo_where(:type) # => also rejects the restriction on :id ! - def undo_where(*args) - mymodel = self.model_name.classify.constantize - mytable = mymodel.table_name - without = clone # will create a new array for its where_values, but having the SAME elems! - where_vals = without.where_values.clone # this is what we need to prune - - to_reject = {} # "tab1.col1" => true, "tab1.col2" => true etc... - args.map do |colspec| # "col" or "table.col" - raise "Invalid column specification \"#{colspec}\"." unless - colspec.to_s =~ /^(\`?(\w+)\`?\.)?\`?(\w+)\`?$/ - tab = Regexp.last_match[2].presence || mytable - col = Regexp.last_match[3] - to_reject["#{tab}.#{col}"] = true - end - #puts_yellow "TO REJ=#{to_reject.inspect}" - - return without if to_reject.empty? && ! block_given? - - where_vals.reject! do |node| - if block_given? && yield(node) # custom rejection code ? - #puts_red "Rejected by block" - true - elsif to_reject.empty? # optimize case of no args with block_given - #puts_red "No args yet block" - false - elsif node.is_a?(Arel::Nodes::Equality) - tab = node.left.relation.name rescue '???' - col = node.left.name rescue '???' - #puts_red "EQ #{tab} #{col}" - to_reject["#{tab}.#{col}"] - elsif node.is_a?(String) # ((`table`.`col`) = 3) and col = 5 - #puts_red "STR #{node}" - node.scan(/([\`\'\"]*(\w+)[\`\'\"]*\.)?[\`\'\"]*(\w+)[\`\'\"]*\s*(=|<|>|\sis\s|\snot\s|\sin\s|\slike\s)/i).any? do |submatch| - tab = submatch[1] || mytable # note: numbering in submatch array is not like in Regexp.last_match ! - col = submatch[2] - #puts_red "--> MATCH #{tab}.#{col}" - to_reject["#{tab}.#{col}"] - end - else # unknown node type in Relation; TODO! - #puts_red "UNKNOWN: #{node.class}" - false - end - end - - without.where_values = where_vals - without - end - - - end - end - end -end From 1bb94e5967bfff6da2245cb38819fef3a52b322c Mon Sep 17 00:00:00 2001 From: Natacha Beck Date: Mon, 5 Oct 2015 15:28:44 -0400 Subject: [PATCH 58/85] Added tool config extra qsub option --- BrainPortal/app/controllers/tool_configs_controller.rb | 2 +- BrainPortal/app/models/cluster_task.rb | 7 +++++++ BrainPortal/app/models/tool_config.rb | 2 +- BrainPortal/app/views/tool_configs/_form_fields.html.erb | 7 +++++++ .../20150929193957_add_extra_qsub_args_to_tool_config.rb | 5 +++++ BrainPortal/lib/scir.rb | 2 +- BrainPortal/lib/scir_moab.rb | 7 ++++--- BrainPortal/lib/scir_pbs.rb | 7 ++++--- BrainPortal/lib/scir_sge.rb | 7 ++++--- BrainPortal/lib/scir_sharcnet.rb | 5 +++-- 10 files changed, 37 insertions(+), 14 deletions(-) create mode 100644 BrainPortal/db/migrate/20150929193957_add_extra_qsub_args_to_tool_config.rb diff --git a/BrainPortal/app/controllers/tool_configs_controller.rb b/BrainPortal/app/controllers/tool_configs_controller.rb index 0a68b6b62..b538e328d 100644 --- a/BrainPortal/app/controllers/tool_configs_controller.rb +++ b/BrainPortal/app/controllers/tool_configs_controller.rb @@ -142,7 +142,7 @@ def update #:nodoc: form_tool_config.bourreau_id = @tool_config.bourreau_id # Update everything else - [ :version_name, :description, :script_prologue, :group_id, :ncpus, :docker_image ].each do |att| + [ :version_name, :description, :script_prologue, :group_id, :ncpus, :docker_image, :extra_qsub_args ].each do |att| @tool_config[att] = form_tool_config[att] end diff --git a/BrainPortal/app/models/cluster_task.rb b/BrainPortal/app/models/cluster_task.rb index 13ebc1591..2353bbe3a 100644 --- a/BrainPortal/app/models/cluster_task.rb +++ b/BrainPortal/app/models/cluster_task.rb @@ -1493,6 +1493,13 @@ def submit_cluster_job job.name = self.tname_tid # "#{self.name}-#{self.id}" # some clusters want all names to be different! job.walltime = self.job_walltime_estimate + # Note: all extra_qsub_args defined in the tool_configs (bourreau, tool and bourreau/tool) + # are appended by level of priority. 'less' specific first, 'more' specific later. + # In this way if the same option is defined twice the more specific one will be the used. + job.tc_extra_qsub_args += "#{bourreau_glob_config.extra_qsub_args} " + job.tc_extra_qsub_args += "#{tool_glob_config.extra_qsub_args} " + job.tc_extra_qsub_args += "#{tool_config.extra_qsub_args} " + # Log version of Scir lib drm = scir_class.drm_system version = scir_class.version diff --git a/BrainPortal/app/models/tool_config.rb b/BrainPortal/app/models/tool_config.rb index 0c2ea1b68..8e18d9e39 100644 --- a/BrainPortal/app/models/tool_config.rb +++ b/BrainPortal/app/models/tool_config.rb @@ -56,7 +56,7 @@ class ToolConfig < ActiveRecord::Base cb_scope :global_for_bourreaux , where( { :tool_id => nil } ) cb_scope :specific_versions , where( "bourreau_id is not null and tool_id is not null" ) - attr_accessible :version_name, :description, :tool_id, :bourreau_id, :env_array, :script_prologue, :group_id, :ncpus, :docker_image + attr_accessible :version_name, :description, :tool_id, :bourreau_id, :env_array, :script_prologue, :group_id, :ncpus, :docker_image, :extra_qsub_args # CBRAIN extension force_text_attribute_encoding 'UTF-8', :description, :version_name diff --git a/BrainPortal/app/views/tool_configs/_form_fields.html.erb b/BrainPortal/app/views/tool_configs/_form_fields.html.erb index 692229cdd..30fa9444b 100644 --- a/BrainPortal/app/views/tool_configs/_form_fields.html.erb +++ b/BrainPortal/app/views/tool_configs/_form_fields.html.erb @@ -102,5 +102,12 @@

    + +

    <%= f.label :extra_qsub_args, "Extra 'qsub' options:" %>
    + <%= f.text_field :extra_qsub_args %>
    +

    Note: This option will be appended to the extra 'qsub' option defined at the bourreau level.
    +

    +
    +
    diff --git a/BrainPortal/db/migrate/20150929193957_add_extra_qsub_args_to_tool_config.rb b/BrainPortal/db/migrate/20150929193957_add_extra_qsub_args_to_tool_config.rb new file mode 100644 index 000000000..52d6cbd42 --- /dev/null +++ b/BrainPortal/db/migrate/20150929193957_add_extra_qsub_args_to_tool_config.rb @@ -0,0 +1,5 @@ +class AddExtraQsubArgsToToolConfig < ActiveRecord::Migration + def change + add_column :tool_configs, :extra_qsub_args, :string + end +end diff --git a/BrainPortal/lib/scir.rb b/BrainPortal/lib/scir.rb index 544ec463a..c7f2b30c9 100644 --- a/BrainPortal/lib/scir.rb +++ b/BrainPortal/lib/scir.rb @@ -202,7 +202,7 @@ def bash_this_and_capture_out_err(command) #:nodoc: class JobTemplate #:nodoc: # We only support a subset of DRMAA's job template - attr_accessor :name, :command, :arg, :wd, :stdin, :stdout, :stderr, :join, :queue, :walltime + attr_accessor :name, :command, :arg, :wd, :stdin, :stdout, :stderr, :join, :queue, :walltime, :extra_qsub_args def revision_info #:nodoc: Class.const_get(self.class.to_s.sub(/::JobTemplate/,"")).revision_info diff --git a/BrainPortal/lib/scir_moab.rb b/BrainPortal/lib/scir_moab.rb index e131efc24..33a016079 100644 --- a/BrainPortal/lib/scir_moab.rb +++ b/BrainPortal/lib/scir_moab.rb @@ -17,7 +17,7 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# along with this program. If not, see . # # This is a replacement for the drmaa.rb library; this particular subclass @@ -152,8 +152,9 @@ def qsub_command command += "-o #{shell_escape(self.stdout)} " if self.stdout command += "-e #{shell_escape(self.stderr)} " if self.stderr command += "-j oe " if self.join - command += " #{Scir.cbrain_config[:extra_qsub_args]} " unless Scir.cbrain_config[:extra_qsub_args].blank? - command += "-l walltime=#{self.walltime.to_i} " unless self.walltime.blank? + command += "#{Scir.cbrain_config[:extra_qsub_args]} " unless Scir.cbrain_config[:extra_qsub_args].blank? + command += "#{self.tc_extra_qsub_args} " unless self.tc_extra_qsub_args.blank? + command += "-l walltime=#{self.walltime.to_i} " unless self.walltime.blank? command += "#{shell_escape(self.arg[0])}" command += " 2>&1" diff --git a/BrainPortal/lib/scir_pbs.rb b/BrainPortal/lib/scir_pbs.rb index a1117acea..1e9bf90b3 100644 --- a/BrainPortal/lib/scir_pbs.rb +++ b/BrainPortal/lib/scir_pbs.rb @@ -17,7 +17,7 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# along with this program. If not, see . # # This is a replacement for the drmaa.rb library; this particular subclass @@ -131,8 +131,9 @@ def qsub_command command += "-e #{shell_escape(self.stderr)} " if self.stderr command += "-j oe " if self.join command += "-q #{shell_escape(self.queue)} " unless self.queue.blank? - command += " #{Scir.cbrain_config[:extra_qsub_args]} " unless Scir.cbrain_config[:extra_qsub_args].blank? - command += "-l walltime=#{self.walltime.to_i} " unless self.walltime.blank? + command += "#{Scir.cbrain_config[:extra_qsub_args]} " unless Scir.cbrain_config[:extra_qsub_args].blank? + command += "#{self.tc_extra_qsub_args} " unless self.tc_extra_qsub_args.blank? + command += "-l walltime=#{self.walltime.to_i} " unless self.walltime.blank? command += "#{shell_escape(self.arg[0])}" command += " 2>&1" diff --git a/BrainPortal/lib/scir_sge.rb b/BrainPortal/lib/scir_sge.rb index c5f60078d..c4595a16b 100644 --- a/BrainPortal/lib/scir_sge.rb +++ b/BrainPortal/lib/scir_sge.rb @@ -17,7 +17,7 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# along with this program. If not, see . # # This is a replacement for the drmaa.rb library; this particular subclass @@ -142,8 +142,9 @@ def qsub_command command += "-e #{shell_escape(self.stderr)} " if self.stderr command += "-j y " if self.join command += "-q #{shell_escape(self.queue)} " unless self.queue.blank? - command += " #{Scir.cbrain_config[:extra_qsub_args]} " unless Scir.cbrain_config[:extra_qsub_args].blank? - command += "-l h_rt=#{self.walltime.to_i} " unless self.walltime.blank? + command += "#{Scir.cbrain_config[:extra_qsub_args]} " unless Scir.cbrain_config[:extra_qsub_args].blank? + command += "#{self.tc_extra_qsub_args} " unless self.tc_extra_qsub_args.blank? + command += "-l h_rt=#{self.walltime.to_i} " unless self.walltime.blank? command += "#{shell_escape(self.arg[0])}" command += " 2>&1" diff --git a/BrainPortal/lib/scir_sharcnet.rb b/BrainPortal/lib/scir_sharcnet.rb index 09e351dcb..2fef15ae9 100644 --- a/BrainPortal/lib/scir_sharcnet.rb +++ b/BrainPortal/lib/scir_sharcnet.rb @@ -17,7 +17,7 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# along with this program. If not, see . # # This is a replacement for the drmaa.rb library; this particular subclass @@ -144,7 +144,8 @@ def qsub_command command += "-e #{shell_escape(stderrfile)} " if stderrfile && ! self.join && stderrfile != stdoutfile command += "-q #{shell_escape(self.queue)} " unless self.queue.blank? command += "-r #{(self.walltime.to_i/60)+1} " unless self.walltime.blank? # sqsub uses minutes - command += " #{Scir.cbrain_config[:extra_qsub_args]} " unless Scir.cbrain_config[:extra_qsub_args].blank? + command += "#{Scir.cbrain_config[:extra_qsub_args]} " unless Scir.cbrain_config[:extra_qsub_args].blank? + command += "#{self.tc_extra_qsub_args} " unless self.tc_extra_qsub_args.blank? command += "/bin/bash #{shell_escape(self.arg[0])}" command += " 2>&1" # they mix stdout and stderr !!! grrrrrr From 6974a62cc29e976e9c76c6cb170aaba248587266 Mon Sep 17 00:00:00 2001 From: Natacha Beck Date: Mon, 5 Oct 2015 16:31:51 -0400 Subject: [PATCH 59/85] Fixed some bugs --- BrainPortal/app/models/cluster_task.rb | 7 ++++--- BrainPortal/lib/scir.rb | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/BrainPortal/app/models/cluster_task.rb b/BrainPortal/app/models/cluster_task.rb index 2353bbe3a..3fd34e2a2 100644 --- a/BrainPortal/app/models/cluster_task.rb +++ b/BrainPortal/app/models/cluster_task.rb @@ -1496,9 +1496,10 @@ def submit_cluster_job # Note: all extra_qsub_args defined in the tool_configs (bourreau, tool and bourreau/tool) # are appended by level of priority. 'less' specific first, 'more' specific later. # In this way if the same option is defined twice the more specific one will be the used. - job.tc_extra_qsub_args += "#{bourreau_glob_config.extra_qsub_args} " - job.tc_extra_qsub_args += "#{tool_glob_config.extra_qsub_args} " - job.tc_extra_qsub_args += "#{tool_config.extra_qsub_args} " + job.tc_extra_qsub_args = "" + job.tc_extra_qsub_args += "#{bourreau_glob_config.extra_qsub_args} " if bourreau_glob_config + job.tc_extra_qsub_args += "#{tool_glob_config.extra_qsub_args} " if tool_glob_config + job.tc_extra_qsub_args += "#{tool_config.extra_qsub_args} " if tool_config # Log version of Scir lib drm = scir_class.drm_system diff --git a/BrainPortal/lib/scir.rb b/BrainPortal/lib/scir.rb index c7f2b30c9..b3768c52d 100644 --- a/BrainPortal/lib/scir.rb +++ b/BrainPortal/lib/scir.rb @@ -202,7 +202,7 @@ def bash_this_and_capture_out_err(command) #:nodoc: class JobTemplate #:nodoc: # We only support a subset of DRMAA's job template - attr_accessor :name, :command, :arg, :wd, :stdin, :stdout, :stderr, :join, :queue, :walltime, :extra_qsub_args + attr_accessor :name, :command, :arg, :wd, :stdin, :stdout, :stderr, :join, :queue, :walltime, :tc_extra_qsub_args def revision_info #:nodoc: Class.const_get(self.class.to_s.sub(/::JobTemplate/,"")).revision_info From 37c72dafcaf110e02195c848b9017bec55fbc943 Mon Sep 17 00:00:00 2001 From: Remi Bernard Date: Mon, 5 Oct 2015 17:32:06 -0400 Subject: [PATCH 60/85] Removal of leftover UndoWhere include, minor users table fix This changeset removes the previously removed UndoWhere module from the AR extensions initializer and fixes an incorrect call in the users table following a recent refactor of filter_values_for. (In other words, two simple bugfixes) --- BrainPortal/app/views/users/_users_table.html.erb | 12 ++++++------ .../initializers/core_extensions/active_record.rb | 8 +------- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/BrainPortal/app/views/users/_users_table.html.erb b/BrainPortal/app/views/users/_users_table.html.erb index 4c054bcfa..1d6bee79e 100644 --- a/BrainPortal/app/views/users/_users_table.html.erb +++ b/BrainPortal/app/views/users/_users_table.html.erb @@ -80,15 +80,15 @@ t.column("Login", :login, :sortable => true, - :filters => filter_values_for(@base_scope, :account_locked, - scope: @scope, - format: lambda do |value, label, count, scoped| + :filters => scoped_filters_for( + @base_scope, @scope, :account_locked, + format: lambda do |value, label, base, view| label = (!value || value == 0 ? 'Unlocked' : 'Locked') { :value => value, - :label => "#{label} (of #{count})", - :indicator => scoped, - :empty => scoped == 0 + :label => "#{label} (of #{base})", + :indicator => view, + :empty => view == 0 } end ) diff --git a/BrainPortal/config/initializers/core_extensions/active_record.rb b/BrainPortal/config/initializers/core_extensions/active_record.rb index 0af94f3f8..0e167a898 100644 --- a/BrainPortal/config/initializers/core_extensions/active_record.rb +++ b/BrainPortal/config/initializers/core_extensions/active_record.rb @@ -108,19 +108,13 @@ class Relation include CBRAINExtensions::ActiveRecordExtensions::RelationExtensions::RawData - ##################################################################### - # ActiveRecord::Relation Added Behavior For To Undo Conditions - ##################################################################### - - include CBRAINExtensions::ActiveRecordExtensions::RelationExtensions::UndoWhere - end # CBRAIN ActiveRecord::Associations::CollectionProxy extensions # delegating extended Relation methods. module Associations #:nodoc: class CollectionProxy #:nodoc: - delegate :raw_first_column, :raw_rows, :undo_where, :to => :scoped + delegate :raw_first_column, :raw_rows, :to => :scoped end end From c62f039c53cac1add07fad18b28fa9a099a616d7 Mon Sep 17 00:00:00 2001 From: Pierre Rioux Date: Tue, 6 Oct 2015 15:28:21 -0400 Subject: [PATCH 61/85] Fixed inconsistent codebase bugs. --- BrainPortal/app/models/user.rb | 10 +++++----- .../initializers/core_extensions/active_record.rb | 8 +------- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/BrainPortal/app/models/user.rb b/BrainPortal/app/models/user.rb index 69e29fbc3..33736cd7c 100644 --- a/BrainPortal/app/models/user.rb +++ b/BrainPortal/app/models/user.rb @@ -289,7 +289,7 @@ def has_role?(role) # Find the tools that this user has access to. def available_tools - cb_error "#available_tools called from User base class! Method must be implement in a subclass." + cb_error "#available_tools called from User base class! Method must be implemented in a subclass." end # Find the scientific tools that this user has access to. @@ -304,7 +304,7 @@ def available_conversion_tools # Returns the list of groups available to this user based on role. def available_groups - cb_error "#available_groups called from User base class! Method must be implement in a subclass." + cb_error "#available_groups called from User base class! Method must be implemented in a subclass." end # Returns the list of tags available to this user. @@ -314,17 +314,17 @@ def available_tags # Returns the list of tasks available to this user. def available_tasks - cb_error "#available_tasks called from User base class! Method must be implement in a subclass." + cb_error "#available_tasks called from User base class! Method must be implemented in a subclass." end # Return the list of users under this user's control based on role. def available_users - cb_error "#available_users called from User base class! Method must be implement in a subclass." + cb_error "#available_users called from User base class! Method must be implemented in a subclass." end # Return the list of sites accessible to the user def accessible_sites - cb_error "#accessible_sites called from User base class! Method must be implement in a subclass." + cb_error "#accessible_sites called from User base class! Method must be implemented in a subclass." end # Can this user be accessed by +user+? diff --git a/BrainPortal/config/initializers/core_extensions/active_record.rb b/BrainPortal/config/initializers/core_extensions/active_record.rb index 0af94f3f8..0e167a898 100644 --- a/BrainPortal/config/initializers/core_extensions/active_record.rb +++ b/BrainPortal/config/initializers/core_extensions/active_record.rb @@ -108,19 +108,13 @@ class Relation include CBRAINExtensions::ActiveRecordExtensions::RelationExtensions::RawData - ##################################################################### - # ActiveRecord::Relation Added Behavior For To Undo Conditions - ##################################################################### - - include CBRAINExtensions::ActiveRecordExtensions::RelationExtensions::UndoWhere - end # CBRAIN ActiveRecord::Associations::CollectionProxy extensions # delegating extended Relation methods. module Associations #:nodoc: class CollectionProxy #:nodoc: - delegate :raw_first_column, :raw_rows, :undo_where, :to => :scoped + delegate :raw_first_column, :raw_rows, :to => :scoped end end From 1ddadd6b8b0098ada75f973f681cdec40fb73054 Mon Sep 17 00:00:00 2001 From: Remi Bernard Date: Wed, 7 Oct 2015 12:10:45 -0400 Subject: [PATCH 62/85] Issue #228 - Improve 'search for anything' code Refactored duplicated & eval blocks by a lambda function, made the controller pass the result set hash instead of a bunch of instance variables. This changeset also adds the missing copyright block. --- .../app/controllers/portal_controller.rb | 18 +- BrainPortal/app/views/portal/search.html.erb | 167 +++++++++--------- 2 files changed, 82 insertions(+), 103 deletions(-) diff --git a/BrainPortal/app/controllers/portal_controller.rb b/BrainPortal/app/controllers/portal_controller.rb index 25d9d1e95..630b0120a 100644 --- a/BrainPortal/app/controllers/portal_controller.rb +++ b/BrainPortal/app/controllers/portal_controller.rb @@ -359,22 +359,10 @@ def report #:nodoc: # This action searches among all sorts of models for IDs or strings, # and reports links to the matches found. def search + @search = params[:search] + @limit = 20 # used by interface only - @search = params[:search] - @limit = 20 # used by interface only - - results = @search.present? ? ModelsReport.search_for_token(@search, current_user) : {} - - @users = results[:users] || [] - @tasks = results[:tasks] || [] - @groups = results[:groups] || [] - @files = results[:files] || [] - @rrs = results[:rrs] || [] - @dps = results[:dps] || [] - @sites = results[:sites] || [] - @tools = results[:tools] || [] - @tcs = results[:tcs] || [] - + @results = @search.present? ? ModelsReport.search_for_token(@search, current_user) : {} end private diff --git a/BrainPortal/app/views/portal/search.html.erb b/BrainPortal/app/views/portal/search.html.erb index cd91b67fc..a8bffbc0b 100644 --- a/BrainPortal/app/views/portal/search.html.erb +++ b/BrainPortal/app/views/portal/search.html.erb @@ -1,4 +1,27 @@ +<%- +# +# CBRAIN Project +# +# Copyright (C) 2008-2012 +# The Royal Institution for the Advancement of Learning +# McGill University +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +-%> + <% title 'Search' %> <%= form_tag(search_path, :method => :get) do %> @@ -6,115 +29,83 @@ This will search files, tasks, users, tools, projects, etc by name or description. <%= @limit %> results shown maximum. <% end %> -<% if [ @users, @tasks, @groups, @files, @rrs, @dps, @sites, @tools, @tcs ].any? { |var| var.present? } %> - <%#

    Results

    %> -

    -<% end %> - -<% [ :files, :tasks, :users, :groups, :rrs, :dps, :sites, :tools, :tcs ].each do |var| %> - <% - # Compute a @x_size variable for each result set, and truncate results to 20 entries max. - # Should be using reflexive interface instead of an eval, but oh well. - eval " - array = @#{var} - @#{var}_size = array.size - @#{var} = @#{var}[0,#{@limit}] if array.size > #{@limit} - " +<%# + Display a nice short table for the result set named +name+ with title +title+. + Each table cell is formatted by the given block (which is passed directly to + array_to_table). +%> +<% result_table = lambda do |name, title = nil, &block| %> + <% return if (result = @results[name]).blank? %> +

    <%= title || name.to_s.humanize %> (found: <%= result.size %>)

    + <%= + array_to_table(result[0, @limit], { + :rows => 5, + :fill_by_columns => true, + :table_class => "simple", + :td_class => "left_align", + }, &block) %> <% end %> -<% # this is used by array_to_table() below - tables_options = { - :rows => 5, - :fill_by_columns => true, - :table_class => "simple", - :td_class => "left_align", - } -%> - -<% if @files.present? %> -

    Files (found: <%= @files_size %>)

    - <%= array_to_table(@files, tables_options) do |file,r,c| %> - <%= link_to_userfile_if_accessible(file) %> (<%= file.user.login %>) - <% end %> +<% result_table.(:files) do |file, r, c| %> + <%= link_to_userfile_if_accessible(file) %> (<%= file.user.login %>) <% end %> -<% if @tasks.present? %> -

    Tasks (found: <%= @tasks_size %>)

    - <%= array_to_table(@tasks, tables_options) do |task,r,c| %> - <%= link_to_model_if_accessible(CbrainTask,task,:name_and_bourreau) %> (<%= task.user.login %>) - <%= link_to "(edit)", edit_task_path(task), :class => 'action_link' %>
    - <%= overlay_description(task.description, :header_width => 35) %> - <% end %> +<% result_table.(:tasks) do |task, r, c| %> + <%= link_to_model_if_accessible(CbrainTask,task,:name_and_bourreau) %> (<%= task.user.login %>) + <%= link_to "(edit)", edit_task_path(task), :class => 'action_link' %>
    + <%= overlay_description(task.description, :header_width => 35) %> <% end %> -<% if @users.present? %> -

    Users (found: <%= @users_size %>)

    - <%= array_to_table(@users, tables_options) do |user,r,c| %> - <%= user.full_name %> (<%= link_to_user_if_accessible(user) %>) - <%= link_to '(switch)', switch_user_path(user), :method => :post, :class => 'action_link' if current_user.has_role?(:admin_user) %> - <% end %> +<% result_table.(:users) do |user, r, c| %> + <%= user.full_name %> (<%= link_to_user_if_accessible(user) %>) + <%= link_to '(switch)', switch_user_path(user), :method => :post, :class => 'action_link' if current_user.has_role?(:admin_user) %> <% end %> -<% if @groups.present? %> -

    Projects (found: <%= @groups_size %>)

    - <%= array_to_table(@groups, tables_options) do |group,r,c| %> - <%= link_to_group_if_accessible(group) %> (<%= group.creator.try(:login) %>) - <%= link_to '(switch)', { :controller => :groups, :action => :switch, :id => group.id } , :method => :post, :class => 'action_link' %> - <% end %> +<% result_table.(:groups, 'Projects') do |group, r, c| %> + <%= link_to_group_if_accessible(group) %> (<%= group.creator.try(:login) %>) + <%= link_to '(switch)', { :controller => :groups, :action => :switch, :id => group.id } , :method => :post, :class => 'action_link' %> <% end %> -<% if @rrs.present? %> -

    Execution Servers (found: <%= @rrs_size %>)

    - <%= array_to_table(@rrs, tables_options) do |rr,r,c| %> - <%= link_to_bourreau_if_accessible(rr) %> - <% task_count = current_user.cbrain_tasks.real_tasks.where(:bourreau_id => rr.id).count %> - <% if task_count > 0 %> - (<%= index_count_filter(task_count, :tasks, - { :user_id => current_user.id, :bourreau_id => rr.id}) %> tasks) - <% end %> +<% result_table.(:rrs, 'Execution Servers') do |rr, r, c| %> + <%= link_to_bourreau_if_accessible(rr) %> + <% task_count = current_user.cbrain_tasks.real_tasks.where(:bourreau_id => rr.id).count %> + <% if task_count > 0 %> + (<%= index_count_filter( + task_count, :tasks, + { :user_id => current_user.id, :bourreau_id => rr.id } + ) %> tasks) <% end %> <% end %> -<% if @dps.present? %> -

    Data Providers (found: <%= @dps_size %>)

    - <%= array_to_table(@dps, tables_options) do |dp,r,c| %> - <%= link_to_data_provider_if_accessible(dp) %> - <%= link_to '(browse)', browse_data_provider_path(dp), :class => 'action_link' if dp.is_browsable? %> - <% file_count = current_user.userfiles.where(:data_provider_id => dp.id).count %> - <% if file_count > 0 %> - (<%= index_count_filter(file_count, :userfiles, - { :user_id => current_user.id, :data_provider_id => dp.id}) %> registered files) - <% end %> +<% result_table.(:dps, 'Data Providers') do |dp, r, c| %> + <%= link_to_data_provider_if_accessible(dp) %> + <%= link_to '(browse)', browse_data_provider_path(dp), :class => 'action_link' if dp.is_browsable? %> + <% file_count = current_user.userfiles.where(:data_provider_id => dp.id).count %> + <% if file_count > 0 %> + (<%= index_count_filter( + file_count, :userfiles, + { :user_id => current_user.id, :data_provider_id => dp.id } + ) %> registered files) <% end %> <% end %> -<% if @sites.present? %> -

    Sites (found: <%= @sites_size %>)

    - <%= array_to_table(@sites, tables_options) do |site,r,c| %> - <%= link_to_site_if_accessible(site) %> - <% end %> +<% result_table.(:sites) do |site, r, c| %> + <%= link_to_site_if_accessible(site) %> <% end %> -<% if @tools.present? %> -

    Tools (found: <%= @tools_size %>)

    - <%= array_to_table(@tools, tables_options) do |tool,r,c| %> - <% if current_user.has_role?(:admin_user) %> - <%= link_to tool.name, edit_tool_path(tool) %> - <% else %> - <%= tool.name %> - <% end %> +<% result_table.(:tools) do |tool, r, c| %> + <% if current_user.has_role?(:admin_user) %> + <%= link_to tool.name, edit_tool_path(tool) %> + <% else %> + <%= tool.name %> <% end %> <% end %> -<% if @tcs.present? %> -

    Tool Versions (found: <%= @tcs_size %>)

    - <%= array_to_table(@tcs, tables_options) do |tc,r,c| %> - <% if current_user.has_role?(:admin_user) %> - <%= tc.tool.name %>@<%= tc.bourreau.name%> : <%= link_to tc.version_name, edit_tool_config_path(tc) %> - <% else %> - <%= tc.tool.name %>@<%= tc.bourreau.name%> : <%= tc.version_name %> - <% end %> +<% result_table.(:tcs, 'Tool Versions') do |tc, r, c| %> + <% if current_user.has_role?(:admin_user) %> + <%= tc.tool.name %>@<%= tc.bourreau.name%> : <%= link_to tc.version_name, edit_tool_config_path(tc) %> + <% else %> + <%= tc.tool.name %>@<%= tc.bourreau.name%> : <%= tc.version_name %> <% end %> <% end %> - From bbac3085b4f84fb52c2bccaa25e2409d84c52000 Mon Sep 17 00:00:00 2001 From: Remi Bernard Date: Wed, 7 Oct 2015 17:12:58 -0400 Subject: [PATCH 63/85] Missing userfiles scoping method (view_scope) This short changeset fixes a small oversight; the view_scope method has been refactored with the Scope API port, and was yet still called at some places. This is fixed by matching (more-or-less) what is done in the tasks controller with a filtered_scope method which applies all known filters. --- BrainPortal/app/controllers/userfiles_controller.rb | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/BrainPortal/app/controllers/userfiles_controller.rb b/BrainPortal/app/controllers/userfiles_controller.rb index d73c7665b..7a53ca416 100644 --- a/BrainPortal/app/controllers/userfiles_controller.rb +++ b/BrainPortal/app/controllers/userfiles_controller.rb @@ -286,7 +286,7 @@ def show #:nodoc: # Rebuild the sorted Userfile scope @scope = scope_from_session('userfiles') - sorted_scope = view_scope + sorted_scope = filtered_scope # Fetch the neighbors of the shown userfile in the ordered scope's order neighbors = sorted_scope.where("userfiles.id != ?", @userfile.id).offset([0, @sort_index - 1].max).limit(2).all @@ -927,7 +927,7 @@ def manage_persistent persistent = Set.new(current_session[:persistent_userfiles]) if operation =~ /select/ - files = view_scope + files = filtered_scope .select(&:available?) .map(&:id) .map(&:to_s) @@ -1623,6 +1623,13 @@ def custom_scope(base = nil) .inject(base || base_scope) { |scope, filter| filter.filter_scope(scope) } end + # Combination of +base_scope+, +custom_scope+ and @scope object; returns a + # scoped list of userfiles fitlered/ordered by all three. + # Requires a valid @scope object. + def filtered_scope + @scope.apply(custom_scope(base_scope)) + end + # Userfiles-specific tag Scope filter; filter by a set of tags which must # all be on a given userfile for it to pass the filter. Note that this is # an all-or-nothing filter; to pass, an userfile needs *all* tags. From 3ce689f1568cdabe5d2f6d74e62a321ef2b5dad9 Mon Sep 17 00:00:00 2001 From: Ehsan Afkhami Date: Tue, 13 Oct 2015 14:36:58 -0400 Subject: [PATCH 64/85] fixes for issues 193 and 104 --- .../app/controllers/userfiles_controller.rb | 7 ++- .../app/views/layouts/application.html.erb | 1 + .../javascripts/custom-confirm-dialog.js | 54 +++++++++++++++++++ BrainPortal/public/stylesheets/cbrain.css | 6 +++ 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 BrainPortal/public/javascripts/custom-confirm-dialog.js diff --git a/BrainPortal/app/controllers/userfiles_controller.rb b/BrainPortal/app/controllers/userfiles_controller.rb index d73c7665b..fd9f18baa 100644 --- a/BrainPortal/app/controllers/userfiles_controller.rb +++ b/BrainPortal/app/controllers/userfiles_controller.rb @@ -647,7 +647,12 @@ def update_multiple #:nodoc: failed_list = {} CBRAIN.spawn_with_active_records_if(do_in_spawn,current_user,"Sending update to files") do access_requested = commit_name == :update_tags ? :read : :write - filelist = Userfile.find_all_accessible_by_user(current_user, :access_requested => access_requested ).where(:id => file_ids).all + # if the current user is admin or site manager they're allowed to update attributes of any file even if they're not the owner. Otherwise, the current user must be the owner to modify the attributes. + if current_user.has_role?(:site_manager) || current_user.has_role?(:admin_user) + filelist = Userfile.find_all_accessible_by_user(current_user, :access_requested => access_requested ).where(:id => file_ids).all + else + filelist = Userfile.find_all_accessible_by_user(current_user, :access_requested => access_requested ).where(:id => file_ids, :user_id => current_user.id).all + end failure_ids = file_ids - filelist.map {|u| u.id.to_s } failed_files = Userfile.where(:id => failure_ids).select([:id, :name, :type]).all failed_list["you don't have write access"] = failed_files if failed_files.present? diff --git a/BrainPortal/app/views/layouts/application.html.erb b/BrainPortal/app/views/layouts/application.html.erb index 857fc4f05..9f8768c77 100644 --- a/BrainPortal/app/views/layouts/application.html.erb +++ b/BrainPortal/app/views/layouts/application.html.erb @@ -54,6 +54,7 @@ <%= javascript_include_tag "cbrain" %> <%= javascript_include_tag "dynamic-table" %> <%= javascript_include_tag "rails" %> + <%= javascript_include_tag "custom-confirm-dialog" %> <%= yield :scripts %> diff --git a/BrainPortal/public/javascripts/custom-confirm-dialog.js b/BrainPortal/public/javascripts/custom-confirm-dialog.js new file mode 100644 index 000000000..59b26855c --- /dev/null +++ b/BrainPortal/public/javascripts/custom-confirm-dialog.js @@ -0,0 +1,54 @@ +// This call will override the rails allowAction method to allow us to present a custom dialog for +// a link with data-confirm attribute + +(function () { + +// The custom dialog box actually gets created in the function below +function myCustomConfirmBox(message, callback) { + + if ($('#dialog-confirm').length == 0){ + $(document.body).append('
    '); + } + + $("#dialog-confirm").html(message); + + // Define the Dialog and its properties. + $("#dialog-confirm").dialog({ + resizable: false, + modal: true, + title: "Confirmation", + height: 150, + width: 400, + buttons: { + "Yes": function () { + $(this).dialog('close'); + callback(); + }, + "No": function () { + $(this).dialog('close'); + } + } + }); +} + +// The function below overrides the allowAction to create a custom dialog box +$.rails.allowAction = function(element) { + var message = element.data('confirm'), callback; + if (!message) { return true; } // if there is no message, proceed with action + + if ($.rails.fire(element, 'confirm')) { + myCustomConfirmBox(message, function() { + callback = $.rails.fire(element,'confirm:complete', [false]); + if(callback) { + var oldAllowAction = $.rails.allowAction; + $.rails.allowAction = function() { return true; }; + element.trigger('click'); + $.rails.allowAction = oldAllowAction; + } + }); + } + return false; +} + +}()); + diff --git a/BrainPortal/public/stylesheets/cbrain.css b/BrainPortal/public/stylesheets/cbrain.css index 4fd1f116e..037c926f6 100644 --- a/BrainPortal/public/stylesheets/cbrain.css +++ b/BrainPortal/public/stylesheets/cbrain.css @@ -2329,4 +2329,10 @@ img { resize: both; } +/* % ######################################################### */ +/* % Jquery Confirmation UI styling */ +/* % ######################################################### */ + +#dialog-confirm { color: red; } + From 7db51bfeaa2c9be2429400178285f0fc6933da90 Mon Sep 17 00:00:00 2001 From: Ehsan Afkhami Date: Tue, 13 Oct 2015 16:34:21 -0400 Subject: [PATCH 65/85] changed dialog look --- .../app/views/layouts/application.html.erb | 2 +- .../javascripts/custom-confirm-dialog.js | 10 +++---- BrainPortal/public/stylesheets/cbrain.css | 26 ++++++++++++++++++- 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/BrainPortal/app/views/layouts/application.html.erb b/BrainPortal/app/views/layouts/application.html.erb index 9f8768c77..7bd6e0473 100644 --- a/BrainPortal/app/views/layouts/application.html.erb +++ b/BrainPortal/app/views/layouts/application.html.erb @@ -54,7 +54,7 @@ <%= javascript_include_tag "cbrain" %> <%= javascript_include_tag "dynamic-table" %> <%= javascript_include_tag "rails" %> - <%= javascript_include_tag "custom-confirm-dialog" %> + <%= javascript_include_tag "custom-confirm-dialog" %> <%= yield :scripts %> diff --git a/BrainPortal/public/javascripts/custom-confirm-dialog.js b/BrainPortal/public/javascripts/custom-confirm-dialog.js index 59b26855c..abef18616 100644 --- a/BrainPortal/public/javascripts/custom-confirm-dialog.js +++ b/BrainPortal/public/javascripts/custom-confirm-dialog.js @@ -6,18 +6,18 @@ // The custom dialog box actually gets created in the function below function myCustomConfirmBox(message, callback) { - if ($('#dialog-confirm').length == 0){ - $(document.body).append('
    '); + if ($('.cbrain-dialog-confirm').length == 0){ + $(document.body).append('
    '); } - $("#dialog-confirm").html(message); + $(".cbrain-dialog-confirm").html(message); + // Define the Dialog and its properties. - $("#dialog-confirm").dialog({ + $(".cbrain-dialog-confirm").dialog({ resizable: false, modal: true, title: "Confirmation", - height: 150, width: 400, buttons: { "Yes": function () { diff --git a/BrainPortal/public/stylesheets/cbrain.css b/BrainPortal/public/stylesheets/cbrain.css index 037c926f6..bc5fed8e9 100644 --- a/BrainPortal/public/stylesheets/cbrain.css +++ b/BrainPortal/public/stylesheets/cbrain.css @@ -2333,6 +2333,30 @@ img { /* % Jquery Confirmation UI styling */ /* % ######################################################### */ -#dialog-confirm { color: red; } +.cbrain-dialog-confirm { + background-color: pink !important; + font-weight: bold !important; + border: 3px solid red !important; +} + +.ui-dialog-titlebar-close { + visibility: hidden; +} + + +/* + + + +.ui-dialog-titlebar { + outline: red solid thick; + height: 20px; +} +.ui-dialog { + outline: #00FF00 dotted thick; +} +*/ + + From bd94292d632ba9a59c71077561bf60b2a56e6fe1 Mon Sep 17 00:00:00 2001 From: Ehsan Afkhami Date: Tue, 13 Oct 2015 16:39:20 -0400 Subject: [PATCH 66/85] changed dialog look 2 --- .../javascripts/custom-confirm-dialog.js | 1 + BrainPortal/public/stylesheets/cbrain.css | 19 ++----------------- 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/BrainPortal/public/javascripts/custom-confirm-dialog.js b/BrainPortal/public/javascripts/custom-confirm-dialog.js index abef18616..396bbf652 100644 --- a/BrainPortal/public/javascripts/custom-confirm-dialog.js +++ b/BrainPortal/public/javascripts/custom-confirm-dialog.js @@ -17,6 +17,7 @@ function myCustomConfirmBox(message, callback) { $(".cbrain-dialog-confirm").dialog({ resizable: false, modal: true, + dialogClass: "no-close", title: "Confirmation", width: 400, buttons: { diff --git a/BrainPortal/public/stylesheets/cbrain.css b/BrainPortal/public/stylesheets/cbrain.css index bc5fed8e9..e2e48af1f 100644 --- a/BrainPortal/public/stylesheets/cbrain.css +++ b/BrainPortal/public/stylesheets/cbrain.css @@ -2339,24 +2339,9 @@ img { border: 3px solid red !important; } -.ui-dialog-titlebar-close { - visibility: hidden; -} - - -/* - - - -.ui-dialog-titlebar { - outline: red solid thick; - height: 20px; -} -.ui-dialog { - outline: #00FF00 dotted thick; +.no-close .ui-dialog-titlebar-close { + display: none; } -*/ - From 3070eb6009cefc3ed7ef8f008623bf0ca7cf5778 Mon Sep 17 00:00:00 2001 From: Ehsan Afkhami Date: Wed, 21 Oct 2015 11:26:43 -0400 Subject: [PATCH 67/85] this is the fix for The site selection box is not sorted issue #241 --- BrainPortal/app/helpers/select_box_helper.rb | 32 +++++++++++++++++++- BrainPortal/app/views/groups/_new.html.erb | 4 +-- BrainPortal/app/views/groups/show.html.erb | 26 +++++++++------- BrainPortal/app/views/users/_new.html.erb | 4 +-- BrainPortal/app/views/users/show.html.erb | 16 +++++----- 5 files changed, 59 insertions(+), 23 deletions(-) diff --git a/BrainPortal/app/helpers/select_box_helper.rb b/BrainPortal/app/helpers/select_box_helper.rb index 02b455f3d..1d4a65898 100644 --- a/BrainPortal/app/helpers/select_box_helper.rb +++ b/BrainPortal/app/helpers/select_box_helper.rb @@ -60,6 +60,37 @@ def user_select(parameter_name = "user_id", options = {}, select_tag_options = { select_tag parameter_name, grouped_options, select_tag_options end + # Create a standard site select box for selecting a site id for a form. + # The +parameter_name+ argument will be the name of the parameter + # when the form is submitted and the +select_tag_options+ hash will be sent + # directly as options to the +select_tag+ helper method called to create the element. + # The +options+ hash can contain either or both of the following: + # [selector] used for default selection. This can be a Site object, a site id (String or Fixnum), + # or any model that has a site_id attribute. + # [sites] the array of Site objects used to build the select box. Defaults to +Site.order(:name).all+. + # When calling site_select, set the :prompt option in select_tag_options hash, to the text you want + # displayed when no option is selected + def site_select(parameter_name = "site", options = {}, select_tag_options = {} ) + options = { :selector => options } unless options.is_a?(Hash) + sites = options[:sites] || Site.order(:name).all + selector = options[:selector] + + if selector.respond_to?(:site_id) + selected = selector.site_id.to_s + elsif selector.is_a?(Site) + selected = selector.id.to_s + else + selected = selector.to_s + end + + site_options = sites.map{ |s| [s.name, s.id]} + + select_tag parameter_name, options_for_select(site_options, selected), select_tag_options + end + + + + # Create a standard groups select box for selecting a group id for a form. # The +parameter_name+ argument will be the name of the parameter # when the form is submitted and the +select_tag_options+ hash will be sent @@ -468,7 +499,6 @@ def task_type_select(parameter_name = "task_type", options = {}, select_tag_opti type_select(parameter_name, options.dup.merge({:types => task_types, :generate_descendants => generate_descendants, :include_top => include_top}), select_tag_options) end - # Create a standard types select box for selecting a types type for a form. # The +parameter_name+ argument will be the name of the parameter # when the form is submitted and the +select_tag_options+ hash will be sent diff --git a/BrainPortal/app/views/groups/_new.html.erb b/BrainPortal/app/views/groups/_new.html.erb index fed8a2eee..92d613939 100644 --- a/BrainPortal/app/views/groups/_new.html.erb +++ b/BrainPortal/app/views/groups/_new.html.erb @@ -18,7 +18,7 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# along with this program. If not, see . # -%> @@ -40,7 +40,7 @@ <% if current_user.has_role?(:admin_user) %>

    <%= f.label :site_id, "Site:" %> - <%= f.collection_select :site_id, Site.all, :id, :name, {:include_blank => true} %> + <%= site_select "group[site_id]",{}, :prompt => "(Select a site)" %>

    Make this a system group invisible to normal users: diff --git a/BrainPortal/app/views/groups/show.html.erb b/BrainPortal/app/views/groups/show.html.erb index b5b76d7dd..182d9e2c6 100644 --- a/BrainPortal/app/views/groups/show.html.erb +++ b/BrainPortal/app/views/groups/show.html.erb @@ -18,19 +18,19 @@ # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License -# along with this program. If not, see . +# along with this program. If not, see . # -%> <% title "Project Info" %> -<% if @group.is_a?(WorkGroup) %> +<% if @group.is_a?(WorkGroup) %>