class Author < ApplicationRecord
end
All of this, prepared for experienced programmers.
"abcd".size # returns 4
3.1415.to_s # returns "3.1415"
class Example
def self.hello
puts "I'm a class method"
end
def hello
puts "I'm an instance method"
end
end
Example.hello # prints "I'm a class method"
Example.hello() # same; parens are optional
obj = Example.new
obj.hello # prints "I'm an instance method"
class Example
def set_name(value)
@name=value
end
def name
return @name # the 'return' is optional
end
end
obj1 = Example.new
obj1.set_name("Saturn V rocket")
obj2 = Example.new()
obj2.set_name "Titanic"
puts obj2.name # prints 'Titanic'
They look like :symbolname
They basically are used as fast replacement for constant strings.
They are NOT strings, but can be transformed into strings.
myvariable = :hello
obj = SomeClass.new
obj.some_method "all_ok"
obj.other_method :all_ok
:rockets_are_cool.to_s # returns "rockets_are_cool"
mixed_hash = { "abcd" => 23, :defg => "aa", 34 => 41 }
symbol_hash = { :abcd => 23, :defg => "aa" }
symbol_hash = { abcd: 23, defg: "aa" } # alternative syntax when keys are all symbols
mixed_array = [ 'abcd', 99, Object.new, { "a" => 1 }, :yoshi ] # array of 5 elements
When a hash is the last argument of a method call,
obj.dostuff(23, "hello", { :name => "pierre", :age => 76 })
obj.dostuff(23, "hello", :name => "pierre", :age => 76 ) # 3 arguments, not 4
obj.dostuff 23, "hello", name: "pierre", age: 76
Their use is often esthetic.
class Something
def is_alive?
blah blah
end
def validate!(mode = :relax) # argument has default value
blah blah
end
end
a_something.is_alive?
a_something.validate! # inside, mode is :relax
a_something.validate! :strict # now it's :strict
x="hello"
x.sub("lo","p") # returns "help" and doesn't change x
x.sub!("lo","p") # returns "help" and change x to "help"
# Method with one argument, invoked in the traditional way
def standard(value)
do_something_with value
end
obj.standard("hello")
# Method with one argument, invoked as an assignment
def assignment=(value) # note the '=' is part of the method name!
do_something_with value
end
obj.assignment = "hello" # the method is "assignment="; spaces are allowed within!
class Something
def name=(arg)
@name=arg
end
def +(rightside)
[ @name , rightside.name ]
end
end
v1 = Something.new; v1.name = "Marilyn"
v2 = Something.new; v2.name = "Monroe"
v1 + v2 # returns [ "Marylin", "Monroe" ]
v1 + a_user # returns [ "Marylin", "Robert" ] assuming a_user has a name() method.
Many operators can be redefined that way:
+, -, *, |, &, +=, -=,
<<, >>, >>=, etc
obj.mymethod(arg1,arg2) do
codeblock1
codeblock2
end
obj.mymethod(arg1,arg2) { # same as do .. end
codeblock1
codeblock2
}
244.to_s(16) do
puts "This is never executed"
end # returns the string "f4" which is 244 in hexadecimal
244.to_s(16) { a = 2 + 3 ; system.reboot } # returns "f4" again
# Prints three lines:
# Z
# 4
# +
["Z", "4", "+"].each { |c| puts c } # each() is an Array method for iterating
# Prints three lines:
# Z0
# 41
# +2
["Z", "4", "+"].each_with_index do |c,i|
puts(c + i.to_s)
end
class Something
def invoke_and_save
@save = yield
end
def invoke_with_pi
yield(3.14)
end
def vowels
yield('a') ; yield('e') ; yield('i') ; yield('o') ; yield('u')
end
end
obj=Something.new
obj.invoke_and_save { "Hello " + 3.to_s } # internally, @save is now "Hello 3"
obj.invoke_with_pi { |value| puts "I got " + value.to_s } # prints "I got 3.14"
pi_plus_2 = obj.invoke_with_pi { |value| value + 2 } # stores 5.14 in pi_plus_2
obj.vowels { |v| puts v } # block is executed five times
class Something
def saveblock(&block) # special syntax to associate an argument to the block
@call_this_later = block
end
def invoke_saved_block(argument)
@call_this_later.call(argument)
end
end
obj = Something.new
obj.saveblock { |x| puts "The string has " + x.size + " characters" }
obj.invoke_saved_block "hello" # prints "The string has 5 characters"
obj.saveblock { |x| x.reverse }
obj.invoke_saved_block "hello" # returns (without printing it) "olleh"
class Something
def method_missing(name,*args) # * is the splat operator for varargs
puts "Eh bud, I got method #{name} but it doesn't exist."
end
end
obj=Something.new
obj.oh_no!('sorry man') # prints "Eh bud, I got method oh_no! but it does't exist."
This is often used to provide dynamic capabilities to classes.
Methods can be created the first time they are missing, and then the following times the method will exist and will respond quickly.
# Rails example:
User.find_by_login('prioux') # there is no such method, but it's created right away
User.find_by_login('mmonroe') # now the method does exist.
val = obj.method(arg1,arg2) { code block }
val = obj.method(arg1,arg2)
val = obj.method()
val = obj.method # Important: this still invokes the method!
val = method
method
# This is Ruby code:
This is ruby code
# It's executed as:
This(is(ruby(code())))
# A DSL I made up to describe a SQL table.
# It is Ruby code that assumes the methods
# "table", "column", "type" etc have been defined elsewhere.
table "users" do
column :id
column :login do
type :string
length 256
end
has_many do
other_table "files"
remote_key :user_id
end
end
mysql> select * from authors; +----+------------+-----------+ | id | first_name | last_name | +----+------------+-----------+ | 1 | Pierre | Rioux | | 2 | Jane | Austen | | 3 | Samir | Das | +----+------------+-----------+ 3 rows in set (0.00 sec) mysql> select * from books; +----+--------------------------------+-----------+--------------+ | id | title | author_id | published_at | +----+--------------------------------+-----------+--------------+ | 1 | How to cheat at origami | 1 | 2018-01-03 | | 2 | Pride And Potatoes | 2 | 1798-04-21 | | 3 | Some sunglasses for Kiki | 3 | 1995-05-10 | | 4 | Kiki vs the sunglasses tyrants | 3 | 2005-11-21 | | 5 | The sunglasses of God | 3 | 2015-12-04 | +----+--------------------------------+-----------+--------------+ 5 rows in set (0.00 sec)
class Author < ApplicationRecord
end
class Book < ApplicationRecord
end
These files are shown complete!
# Creating new records
n = Book.new( :title => 'My Stupid Cat', :author_id => 23 )
# Finder mehods
a = Author.find(2) # Jane Austen
b = Book.find(4) # "Kiki vs the sunglasses tyrants"
# Attribute methods
first_name = a.first_name # Get an attribute
b.title = 'Of mice and Zen' # Set an attribute
# Saving or destroying an object
n.save # Writes new row in database
n.destroy # Removes row
# Misc
b.valid?
b.changed? # True if modified in memory
n.new_record? # Returns true until saved
# Create querying objects with WHERE clauses
r1 = Author.where(:last_name => 'Das') # does NOT query yet
r2 = r1.where(:first_name => 'Beyoncé')
# SELECT `authors`.* FROM `authors` WHERE `authors`.`last_name` = 'Das'
a_list = r1.to_a # Array of Author objects
# SELECT `authors`.* FROM `authors`
# WHERE `authors`.`last_name` = 'Das'
# ORDER BY `authors`.`id` ASC LIMIT 1
a1 = r1.first
# SELECT `authors`.* FROM `authors`
# WHERE `authors`.`last_name` = 'Das'
# AND `authors`.`first_name` = 'Beyoncé'
# ORDER BY `authors`.`id` DESC LIMIT 1
a2 = r2.last # returns nil in this case
# SELECT COUNT(*) FROM `authors` WHERE `authors`.`last_name` = 'Das'
das_count = r1.count
class Author < ApplicationRecord
has_many :books
end
class Book < ApplicationRecord
belongs_to :author
end
a=Author.where(:last_name => 'Das').first
# Author Load (0.9ms) SELECT `authors`.* FROM `authors` WHERE
# `authors`.`last_name` = 'Das' ORDER BY `authors`.`id` ASC LIMIT 1
a.books.to_a
# Book Load (0.4ms) SELECT `books`.* FROM `books` WHERE `books`.`author_id` = 3
' => [
#<Book id: 3, title: "Some sunglasses for Kiki", author_id: 3 ... >,
#<Book id: 4, title: "Kiki vs the sunglasses tyrants", author_id: 3 ... >,
#<Book id: 5, title: "The sunglasses of God", author_id: 3 ... >
] '
b=Book.last
# Book Load (1.2ms) SELECT `books`.* FROM `books` ORDER BY `books`.`id` DESC LIMIT 1
b.author
# Author Load (0.4ms) SELECT `authors`.* FROM `authors` WHERE `authors`.`id` = 3 LIMIT 1
' => #<Author id: 3, first_name: "Samir", last_name: "Das"> '
class Book < ApplicationRecord
belongs_to :author
validate_presence_of :published_at
validates :last_name_has_uppercase?
after_create :new_book_added
def last_name_has_uppercase?
self.last_name =~ /^[A-Z]/
end
def new_book_added
# do something with self only when new book created
end
end
class Book < ApplicationRecord
end
class Novel < Book
end
class Biography < Book
end
mysql> select * from books; +----+--------------------------------------------+-----------+ | id | type | title | author_id | +----+--------------------------------------------+-----------+ | 1 | Book | How to cheat at origami | 1 | | 2 | Biography | Pride And Potatoes | 2 | | 3 | Novel | Some sunglasses for Kiki | 3 | | 4 | Novel | Kiki vs the sunglasses tyrants | 3 | | 5 | Novel | The sunglasses of God | 3 | +----+--------------------------------------------+-----------+
class AddSenderIdToMessage < ActiveRecord::Migration
def self.up
add_column :messages, :sender_id, :integer
end
def self.down
remove_column :messages, :sender_id
end
end
20120530164358_add_sender_id_to_message.rb
class BooksController < ApplicationController
def index # GET /books
end
def show # GET /books/2
id = params[:id]
end
def create # POST /books
end
def destroy # DELETE /book/id
id = params[:id]
end
def update # PUT /book/id
id = params[:id]
end
end
app/controllers/books_controller.rb
Rails.application.routes.draw do
# Routes built manually
match "/books" => "books#index"
match "/books/:id" => "books#show" # stores id in params[:id]
match "/books" => "books#create", :via => :post
# etc
# This creates about 7 standard CRUD routes for books
resources :books
end
config/routes.rb
class BooksController < ApplicationController
def show # GET /books/2
# Basic, common business logic
id = params[:id]
@book = Book.find(id) # This can be used in the view code.
# Rendering control, redirections, etc etc
render :action => :show # 'app/views/show.*' based on HTTP request
end
end
app/controllers/books_controller.rb
<html>
<head> </head> <!-- Not normally in view code! -->
<body>
Book title: <%= @book.title %>
Book author: <%= @book.author.first_name %> <%= @book.author.last_name %>
</body>
</html>
app/views/books/show.html.erb
Note: normally, view files render partial content inside other common view templates.
> ls -l users -rw-r--r-- 1 prioux staff 5755 Dec 12 2017 _users_table.html.erb -rw-r--r-- 1 prioux staff 1949 Feb 27 2017 change_password.html.erb -rw-r--r-- 1 prioux staff 886 May 13 2015 index.html.erb -rw-r--r-- 1 prioux staff 928 May 13 2015 index.js.erb -rw-r--r-- 1 prioux staff 3709 Jan 11 12:09 new.html.erb -rw-r--r-- 1 prioux staff 1371 Feb 27 2017 request_password.html.erb -rw-r--r-- 1 prioux staff 9499 Jun 20 10:11 show.html.erb
web interface | Ruby On Rails |
data | Userfile and DataProvider models |
processing | CbrainTask model |
neuroscience tools | Tool and ToolConfig models |
All the neuroscience code was partitioned into
distinct, separate GIT repos.
CBRAIN was extended such that when these other GIT repos are
extracted within a specific cbrain_plugins subdirectory,
they integrate automatically.
This became CBRAIN's plugins support, and so
CBRAIN is now a generic processing platform.
# Table: userfiles +----+--------------------------------------------------+ | id | type | name | +----+--------------------------------------------------+ | 12 | SingleFile | nihpd3.tar.gz | | 34 | TextFile | abc.txt | | 55 | FileCollection | dicom_files | | 98 | DicomCollection | more_dicoms | +----+--------------------------------------------------+
Browsable DataProviders are for more convenient data-exchange:
# Table: userfiles +----+--------------------------------------------------+-------------------+ | id | type | name | data_provider_id | +----+--------------------------------------------------+-------------------+ | 12 | SingleFile | nihpd3.tar.gz | 2 | | 34 | TextFile | abc.txt | 3 | | 55 | FileCollection | dicom_files | 3 | | 98 | DicomCollection | more_dicoms | 3 | +----+--------------------------------------------------+-------------------+
# Table: data_providers +----+--------------------------------------------------+-------------------+ | id | type | name | config_stuff_cols | +----+--------------------------------------------------+-------------------+ | 2 | EnCbrainSmartDataProvider | MainStore | paths, hosts, | | 3 | IncomingVaultSmartDataProvider | SFTP-1 | credentials etc | +----+--------------------------------------------------+-------------------+
At any one time:
Also:
# CBRAIN Ruby model for MP3 audio files.
class Mp3AudioFile < AudioFile # AudioFile is a SingleFile
has_viewer :name => 'MP3 Audio', :partial => :html5_mp3_audio, :if => :is_locally_synced?
def self.pretty_type
"MP3 Audio File"
end
end
cbrain_plugins/cbrain-plugins-base/userfiles/mp3_audio_file/mp3_audio_file.rb
<audio controls="controls" preload="none">
<source src="<%= content_userfile_path(@userfile) %>" type="audio/mp3" />
MP3 audio not supported in this browser.
</audio>
cbrain_plugins/cbrain-plugins-base/userfiles/mp3_audio_file/views/_mp3_audio_file.html.erb
# Table: cbrain_tasks +----+--------------------------------------------------+---------+------------+ | id | type | bourreau_id | user_id | params | +----+--------------------------------------------------+---------+------------+ | 38 | Civet | 3 | 5 | (yaml) | | 79 | Minc2Niftii | 4 | 3 | (yaml) | | 88 | ReconAll | 4 | 3 | (yaml) | +----+--------------------------------------------------+---------+------------+
File path in plugin | Role |
---|---|
.../portal/taskname.rb | Portal-side code |
.../views/_task_params.html.erb | Input form for task params |
.../views/_show_params.html.erb | View code for task info |
.../bourreau/taskname.rb | Bourreau-side code |
Note: these are the core files only, there are many more
class CbrainTask::Stupid < PortalTask
def before_form # verify stuff before rendering the form
ids = params[:interface_userfile_ids] # array of ids
raise "Need at least one file selected" if ids.size == 0
"" # Means all is OK
end
def after_form # verify or do stuff after form submitted
"" # I could do something here too
end
def final_task_list # framework expects an array of task objects to be saved
[ self ] # This is actually the default behavior in PortalTask
end
end
cbrain_plugins/cbrain-plugins-base/cbrain_task/stupid/portal/stupid.rb
This task has <%= (params[:interface_userfile_ids] || []).size %> files in its input list.
<p>
Name for planet: <%= form.params_text_field :name_of_planet %><br>
Destroy it? <%= form.params_check_box :destroy_planet %><br>
cbrain_plugins/cbrain-plugins-base/cbrain_task/stupid/views/_show_params.html.erb
This task has 2 files in its input list.
<p>
Name for planet: <input name="cbrain_task[params][name_of_planet]"
id="cbrain_task_BRA_params_KET__BRA_name_of_planet_KET_"
value=""
type="text"
>
<br>
Destroy it? <input name="cbrain_task[params][destroy_planet]"
value="0"
type="hidden"
>
<input name="cbrain_task[params][destroy_planet]"
id="cbrain_task_BRA_params_KET__BRA_destroy_planet_KET_"
value="1"
type="checkbox"
>
<br>
Note: manually reformatted for easier inspection
class TasksController < ApplicationController
# Rails will send all the HTML form inputs all
# structured nicely, e.g.
#
# params = { :cbrain_tasks => { :description => 'a test', # a normal attribute
# :params => { :planet_name => 'Mars'... },
# # other attribute here
# } }
def create # POST /tasks
post_params = params[:cbrain_tasks] # Provided by Rails
@task = CbrainTask.new(post_params) # Store everything in a new object
# @task.description == 'a test'
# @task.params[:planet_name] == 'Mars' etc
# Invoke task callback methods here for @task
# Create entries in DB with @task, etc
end
end
app/controllers/tasks_controller.rb
Note: Click image to open it in a new tab
class CbrainTask::Stupid < ClusterTask
def setup
# Set up all that's needed to run the process
end
def cluster_commands
# Return a bash script
end
def save_results
# Verify that processing is OK;
# if so, save the results as new CBRAIN files
end
end
cbrain_plugins/cbrain-plugins-base/cbrain_task/stupid/bourreau/stupid.rb
The task programmer does not have to set any of these things up.
def setup
my_params = self.params # a hash with arbitrary content, usually the scientific parameters
# Extract info about our file (we only use the first in this demo code)
file_id = params[:interface_userfile_ids].first # first of an array of IDs
f = Userfile.find(file_id)
# We need to make sure our input data is ready locally
f.sync_to_cache
# I could also use f.cache_full_path here to refer to sync'ed content
true # all good!
end
Excerpt: cbrain_plugins/cbrain-plugins-base/cbrain_task/stupid/bourreau/stupid.rb
def cluster_commands
my_params = self.params
# Extract info about what to do
pname = params[:planet_name]
killit = params[:destroy_planet] == '1'
file_id = params[:interface_userfile_ids].first # first of an array of IDs
fpath = Userfile.find(file_id).cache_full_path
# Build the script or command to run on the supercomputer
command = "planet_manager -p #{pname} -f #{fpath}" # not shown: helpers to avoid injection
command += " -k YES" if killit
return command
end
Excerpt: cbrain_plugins/cbrain-plugins-base/cbrain_task/stupid/bourreau/stupid.rbThis builds the simple bash script
planet_manager -p pluto -f /path/to/userfile/cacheor, if the checkbox destroy_planet was set,
planet_manager -p pluto -f /path/to/userfile/cache -k YES
def save_results
# Check that the task worked fine
return false unless File.exists? "report.txt"
new_cb_file = Textfile.new(:name => "PlanetReport", more_other_args_here)
new_cb_file.save!
# Next line: copies the content to the cache, then autosync to DataProvider
new_cb_file.cache_copy_from_localfile("report.txt")
true
end
Excerpt: cbrain_plugins/cbrain-plugins-base/cbrain_task/stupid/bourreau/stupid.rbIn red: states where the Ruby code of previous slides is run.
Note: Full explanations on the CBRAIN GitHub Wiki
But there is an API to control their progression
based on the states of other tasks.
There are two stop points in the state diagram:
A task can be prevented from entering these states until an arbitrary number of other tasks are in an arbitrary set of other states.
A task can even depend on another task being in a Failed state!
# Example of setting up the dependencies above
task2a .add_prerequisites_for_setup( task1, "Queued" )
task2b .add_prerequisites_for_setup( task1, "Queued" )
finaltask .add_prerequisites_for_post_processing( task2a, "Completed" )
finaltask .add_prerequisites_for_post_processing( task2b, "Completed" )
errortask .add_prerequisites_for_setup( finaltask, "Failed" )
However, any group task or group of tasks can be configured with an option to share the work directory of another task.
This, of course, can only work if all the tasks
are launched on the same supercomputer.
# Example of making sure task1, task2a and task2b all
# share the same work directory.
task2a.share_workdir_with( task1 )
task2b.share_workdir_with( task1 )
So tasks can fail at different stages for different reasons:
The CBRAIN interface allow users to retry tasks that have failed.
class CbrainTask::Something < ClusterTask
# Restartability Methods
def restart_at_setup
def restart_at_cluster
def restart_at_post_processing
# Error Recoverability Methods
def recover_from_setup_failure
def recover_from_cluster_failure
def recover_from_post_processing_failure
end
However, by following CBRAIN programming guidelines, a programmer can also avoid having to write these methods by making the other standard API methods restartable and recoverable naturally.
class CbrainTask::Something < ClusterTask
def setup
# symlink() raise an exception
# if target exists!
File.symlink cache_data, "my_input"
end
def restart_at_setup
File.rm "my_input"
end
end
class CbrainTask::Something < ClusterTask
include RestartableTask # adds trivial restart_at_setup()
def setup
# safe_symlink() is a CBRAIN helper method
safe_symlink cache_data, "my_input"
end
end
In red: states where the Ruby code in handlers is run.
In red: states where the Ruby code in handlers is run.
In that case, a task's work directory can be archived (turned into a .tar.gz), moved about, and even restarted on another supercomputer!
ToolConfigs are created by the administrator
using the interface. They provide configuration information
for a version of a scientific tool on a specific supercomputer.
This also means that the CBRAIN system itself, when deployed, is free of all the requirements of the tools it supports.
The first two are optional, the last one is mandatory.
They are applied in this order.
The user usually selects the third one, and the first two
are applied automatically as necessary.
This gives the opportunity for the system administrator
to provide default configurations for task types, for supercomputers,
or more specifically for a task on a supercomputer.
{
"tool-version": "5.0.0",
"name": "fsl_anat",
"command-line": "fsl_anat [INPUT_FILE] [INPUT_DIR] [OUTPUT_DIR] [CLOBBER_FLAG] [WEAKBIAS_FLAG]
[NO_REORIENT_FLAG] [NO_CROP_FLAG] [NO_BIAS_FLAG] [NO_REGISTRATION_FLAG] [NO_NONLINEAR_REG_FLAG]
[NO_SEG_FLAG] [NO_SUBCORTSEG_FLAG] [NO_SEARCH_FLAG] [NO_CLEANUP_FLAG] [BIAS_FIELD_SMOOTHING_VAL]
[IMAGE_TYPE] [BET_F_PARAM]",
"inputs": [
{
"command-line-flag": "-i",
"description": "Input image file (for single-image use), such as .nii.gz. Either this or an input
dir (-d) must be specified, but not both.",
"value-key": "[INPUT_FILE]",
"type": "File",
"list": false,
"optional": true,
"id": "infile",
"name": "Input file"
},
{
"command-line-flag": "-d",
"description": "Existing input directory (.anat extension) where this script will be run in place.
Either this or an input file (-i) must be specified, but not both.",
"value-key": "[INPUT_DIR]",
"type": "File",
"list": false,
(rest not shown...)
class SomeSuperSomething
# All CBRAIN ruby code has a line like this:
Revision_info = CbrainFileRevision[ __FILE__ ] # ignore the coloring error
end
# Create a new object
something = SomeSuperSomething.new
# Query the information from GIT
something.revision_info.basename # returns "some_super_something.rb"
something.revision_info.author # returns "Pierre Rioux"
something.revision_info.short_commit # returns "8f112ca9"
something.revision_info.date # returns "2018-08-01"
something.revision_info.time # returns "19:45:33 -0500"
user = User.where(:login => 'prioux').first
user.meta # returns a handler for metadata of obj
user.meta[:age] = 76 # setter: sets value associated with :age
user.meta[:age] # getter: returns value associated with :age
task = CbrainTask.first
task.meta[:xyz] = { :hello => 2, "goodbye" => "why?" } # save arbitrary data
This is implemented in a single table that does NOT use
any proper relational database joining, but is transparent
using the Ruby API:
mysql> select * from meta_data_store; +----+-------+---------------+-------------------------------------------+ | id | ar_id | ar_table_name | key | value | +----+-------+---------------+-------------------------------------------+ | 47 | 3 | users | "age" | "--- 76\n...\n" | | 92 | 2344 | cbrain_tasks | "xyz" | "---\n:hello: 2\ngoodbye: why?\n" | +----+-------------------------------------------------------------------+
user = User.where(:login => 'prioux').first
user.addlog("User complained about filesystem")
task = CbrainTask.first
task.addlog("Tried to transfer files, failed.")
Exactly as for the meta data store, the logs are all saved in a
single table the contains the table names along with the record ID.
Each log entry is timestamped and formatted.
E.g for the user above:
[2018-02-19 16:01:07 EST] NormalUser revision 4e779042 Pierre Rioux 2015-09-28 [2018-02-19 16:01:07 EST] create() UsersController rev. 46f29825 Account created by 'admin' [2018-08-01 19:28:38 EDT] User complained about filesystemThe logging API has a rich set of methods for automatically recording the revision info of classes and code.
Project management:
Marc-Étienne Rousseau
Shawn Brown