Initial commit.

This commit is contained in:
Luka Murn 2016-02-12 16:52:43 +01:00
commit 055298fee8
655 changed files with 141708 additions and 0 deletions

2
.buildpacks Normal file
View file

@ -0,0 +1,2 @@
https://github.com/heroku/heroku-buildpack-ruby.git
https://github.com/weibeld/heroku-buildpack-graphviz.git

1
.gemrc Normal file
View file

@ -0,0 +1 @@
gem: --no-ri --no-rdoc

43
.gitignore vendored Normal file
View file

@ -0,0 +1,43 @@
# See https://help.github.com/articles/ignoring-files for more about ignoring files.
#
# If you find yourself ignoring temporary files generated by your text editor
# or operating system, you probably want to add a global ignore instead:
# git config --global core.excludesfile '~/.gitignore_global'
# Ignore bundler config.
/.bundle
# Ignore the default SQLite database.
/db/*.sqlite3
/db/*.sqlite3-journal
# Ignore PostgreSQL dump files
/db/*.dump
# Ignore all logfiles and tempfiles.
/log/*
!/log/.keep
/tmp
# Ignore gems etc. if built in bundle
/vendor/bundle
# Ignore any PDFs
*.pdf
# Ignore custom wmake user file
wmake.sh
# Windows image file caches
Thumbs.db
ehthumbs.db
# Ignore temporary files
public/system/*
tags
*.orig
*.swp
# Ignore application configuration
/config/application.yml

3
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,3 @@
# Contributing to sciNote
### TODO

25
Dockerfile Normal file
View file

@ -0,0 +1,25 @@
FROM rails:4.2.5
MAINTAINER BioSistemika <info@biosistemika.com>
# additional dependecies
RUN apt-get update -qq && apt-get install -y default-jre-headless unison sudo --no-install-recommends && rm -rf /var/lib/apt/lists/*
# heroku tools
RUN wget -O- https://toolbelt.heroku.com/install-ubuntu.sh | sh
# install gems
COPY Gemfile* /tmp/
WORKDIR /tmp
RUN bundle install
# create app directory
ENV APP_HOME /usr/src/app
RUN mkdir $APP_HOME
WORKDIR $APP_HOME
# container user
RUN groupadd scinote
RUN useradd -ms /bin/bash -g scinote scinote
USER scinote
CMD rails s -b 0.0.0.0

68
Gemfile Normal file
View file

@ -0,0 +1,68 @@
source 'https://rubygems.org'
ruby '2.2.4'
gem 'rails', '4.2.5'
gem 'figaro'
gem 'pg'
gem 'devise'
gem 'devise_invitable'
gem 'bootstrap-sass', '~> 3.3.5'
gem 'sass-rails', '~> 5.0'
gem 'bootstrap_form'
gem 'yomu'
# JS datetime library, requirement of datetime picker
gem 'momentjs-rails', '>= 2.9.0'
# JS datetime picker
gem 'bootstrap3-datetimepicker-rails', '~> 4.15.35'
# Select elements for Bootstrap
gem 'bootstrap-select-rails'
gem 'uglifier', '>= 1.3.0'
# jQuery & plugins
gem 'jquery-turbolinks'
gem 'jquery-rails'
gem 'jquery-ui-rails'
gem 'jquery-scrollto-rails'
gem 'hammerjs-rails'
gem 'introjs-rails' # Create quick tutorials
gem 'js_cookie_rails' # Simple JS API for cookies
gem 'spinjs-rails'
gem 'underscore-rails'
gem 'turbolinks'
gem 'sdoc', '~> 0.4.0', group: :doc
gem 'bcrypt', '~> 3.1.10'
gem 'logging', '~> 2.0.0'
gem 'aspector' # Aspect-oriented programming for Rails
gem 'rgl' # Graph framework for project diagram calculations
gem 'nested_form_fields'
gem 'ajax-datatables-rails'
gem 'commit_param_routing' # Enables different submit actions in the same form to route to different actions in controller
gem 'kaminari'
gem "i18n-js", ">= 3.0.0.rc11" # Localization in javascript files
gem 'roo', '~> 2.1.0' # Spreadsheet parser
gem 'wicked_pdf'
gem 'wkhtmltopdf-binary'
gem 'remotipart', '~> 1.2' # Async file uploads
gem 'redcarpet' # Markdown parser
gem 'faker' # Generate fake data
gem 'paperclip', '~> 4.3' # File attachment, image attachment library
gem 'aws-sdk', '~> 2.2.8'
gem 'aws-sdk-v1'
gem 'delayed_job_active_record'
gem 'devise-async'
group :development, :test do
gem 'byebug'
gem 'web-console', '~> 2.0'
end
group :production do
gem 'puma'
gem 'rails_12factor'
gem 'skylight'
end
# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

309
Gemfile.lock Normal file
View file

@ -0,0 +1,309 @@
GEM
remote: https://rubygems.org/
specs:
actionmailer (4.2.5)
actionpack (= 4.2.5)
actionview (= 4.2.5)
activejob (= 4.2.5)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 1.0, >= 1.0.5)
actionpack (4.2.5)
actionview (= 4.2.5)
activesupport (= 4.2.5)
rack (~> 1.6)
rack-test (~> 0.6.2)
rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (4.2.5)
activesupport (= 4.2.5)
builder (~> 3.1)
erubis (~> 2.7.0)
rails-dom-testing (~> 1.0, >= 1.0.5)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
activejob (4.2.5)
activesupport (= 4.2.5)
globalid (>= 0.3.0)
activemodel (4.2.5)
activesupport (= 4.2.5)
builder (~> 3.1)
activerecord (4.2.5)
activemodel (= 4.2.5)
activesupport (= 4.2.5)
arel (~> 6.0)
activesupport (4.2.5)
i18n (~> 0.7)
json (~> 1.7, >= 1.7.7)
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
ajax-datatables-rails (0.3.1)
railties (>= 3.1)
algorithms (0.6.1)
arel (6.0.3)
aspector (0.14.0)
autoprefixer-rails (6.1.2)
execjs
json
aws-sdk (2.2.8)
aws-sdk-resources (= 2.2.8)
aws-sdk-core (2.2.8)
jmespath (~> 1.0)
aws-sdk-resources (2.2.8)
aws-sdk-core (= 2.2.8)
aws-sdk-v1 (1.66.0)
json (~> 1.4)
nokogiri (>= 1.4.4)
bcrypt (3.1.10)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
bootstrap-select-rails (1.6.3)
bootstrap3-datetimepicker-rails (4.15.35)
momentjs-rails (>= 2.8.1)
bootstrap_form (2.3.0)
builder (3.2.2)
byebug (8.2.1)
climate_control (0.0.3)
activesupport (>= 3.0)
cocaine (0.5.8)
climate_control (>= 0.0.3, < 1.0)
coffee-rails (4.1.0)
coffee-script (>= 2.2.0)
railties (>= 4.0.0, < 5.0)
coffee-script (2.4.1)
coffee-script-source
execjs
coffee-script-source (1.10.0)
commit_param_routing (0.0.1)
concurrent-ruby (1.0.0)
debug_inspector (0.0.2)
delayed_job (4.1.1)
activesupport (>= 3.0, < 5.0)
delayed_job_active_record (4.1.0)
activerecord (>= 3.0, < 5)
delayed_job (>= 3.0, < 5)
devise (3.5.3)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 3.2.6, < 5)
responders
thread_safe (~> 0.1)
warden (~> 1.2.3)
devise-async (0.10.1)
devise (~> 3.2)
devise_invitable (1.5.5)
actionmailer (>= 3.2.6, < 5)
devise (>= 3.2.0)
erubis (2.7.0)
execjs (2.6.0)
faker (1.6.1)
i18n (~> 0.5)
figaro (1.1.1)
thor (~> 0.14)
globalid (0.3.6)
activesupport (>= 4.1.0)
hammerjs-rails (2.0.4)
i18n (0.7.0)
i18n-js (3.0.0.rc11)
i18n (~> 0.6)
introjs-rails (1.0.0)
sass-rails (>= 3.2)
thor (~> 0.14)
jmespath (1.1.3)
jquery-rails (4.0.5)
rails-dom-testing (~> 1.0)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
jquery-scrollto-rails (1.4.3)
railties (> 3.1, < 5.0)
jquery-turbolinks (2.1.0)
railties (>= 3.1.0)
turbolinks
jquery-ui-rails (5.0.5)
railties (>= 3.2.16)
js_cookie_rails (1.0.1)
railties (>= 3.1)
json (1.8.3)
kaminari (0.16.3)
actionpack (>= 3.0.0)
activesupport (>= 3.0.0)
little-plugger (1.1.4)
logging (2.0.0)
little-plugger (~> 1.1)
multi_json (~> 1.10)
loofah (2.0.3)
nokogiri (>= 1.5.9)
mail (2.6.3)
mime-types (>= 1.16, < 3)
mime-types (1.25.1)
mimemagic (0.3.0)
mini_portile2 (2.0.0)
minitest (5.8.3)
momentjs-rails (2.10.6)
railties (>= 3.1)
multi_json (1.11.2)
nested_form_fields (0.7.4)
coffee-rails (>= 3.2.1)
jquery-rails
rails (>= 3.2.0)
nokogiri (1.6.7.1)
mini_portile2 (~> 2.0.0.rc2)
orm_adapter (0.5.0)
paperclip (4.3.2)
activemodel (>= 3.2.0)
activesupport (>= 3.2.0)
cocaine (~> 0.5.5)
mime-types
mimemagic (= 0.3.0)
pg (0.18.4)
puma (2.15.3)
rack (1.6.4)
rack-test (0.6.3)
rack (>= 1.0)
rails (4.2.5)
actionmailer (= 4.2.5)
actionpack (= 4.2.5)
actionview (= 4.2.5)
activejob (= 4.2.5)
activemodel (= 4.2.5)
activerecord (= 4.2.5)
activesupport (= 4.2.5)
bundler (>= 1.3.0, < 2.0)
railties (= 4.2.5)
sprockets-rails
rails-deprecated_sanitizer (1.0.3)
activesupport (>= 4.2.0.alpha)
rails-dom-testing (1.0.7)
activesupport (>= 4.2.0.beta, < 5.0)
nokogiri (~> 1.6.0)
rails-deprecated_sanitizer (>= 1.0.1)
rails-html-sanitizer (1.0.2)
loofah (~> 2.0)
rails_12factor (0.0.3)
rails_serve_static_assets
rails_stdout_logging
rails_serve_static_assets (0.0.4)
rails_stdout_logging (0.0.4)
railties (4.2.5)
actionpack (= 4.2.5)
activesupport (= 4.2.5)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rake (10.4.2)
rdoc (4.2.0)
redcarpet (3.3.3)
remotipart (1.2.1)
responders (2.1.0)
railties (>= 4.2.0, < 5)
rgl (0.5.1)
algorithms (~> 0.6.1)
stream (~> 0.5.0)
roo (2.1.1)
nokogiri (~> 1)
rubyzip (~> 1.1, < 2.0.0)
rubyzip (1.1.7)
sass (3.4.20)
sass-rails (5.0.4)
railties (>= 4.0.0, < 5.0)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
sprockets-rails (>= 2.0, < 4.0)
tilt (>= 1.1, < 3)
sdoc (0.4.1)
json (~> 1.7, >= 1.7.7)
rdoc (~> 4.0)
skylight (0.10.0)
activesupport (>= 3.0.0)
spinjs-rails (1.4)
rails (>= 3.1)
sprockets (3.5.2)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (2.3.3)
actionpack (>= 3.0)
activesupport (>= 3.0)
sprockets (>= 2.8, < 4.0)
stream (0.5)
thor (0.19.1)
thread_safe (0.3.5)
tilt (2.0.1)
turbolinks (2.5.3)
coffee-rails
tzinfo (1.2.2)
thread_safe (~> 0.1)
uglifier (2.7.2)
execjs (>= 0.3.0)
json (>= 1.8.0)
underscore-rails (1.8.3)
warden (1.2.4)
rack (>= 1.0)
web-console (2.2.1)
activemodel (>= 4.0)
binding_of_caller (>= 0.7.2)
railties (>= 4.0)
sprockets-rails (>= 2.0, < 4.0)
wicked_pdf (1.0.3)
wkhtmltopdf-binary (0.9.9.3)
yomu (0.2.4)
json (~> 1.8)
mime-types (~> 1.23)
PLATFORMS
ruby
DEPENDENCIES
ajax-datatables-rails
aspector
aws-sdk (~> 2.2.8)
aws-sdk-v1
bcrypt (~> 3.1.10)
bootstrap-sass (~> 3.3.5)
bootstrap-select-rails
bootstrap3-datetimepicker-rails (~> 4.15.35)
bootstrap_form
byebug
commit_param_routing
delayed_job_active_record
devise
devise-async
devise_invitable
faker
figaro
hammerjs-rails
i18n-js (>= 3.0.0.rc11)
introjs-rails
jquery-rails
jquery-scrollto-rails
jquery-turbolinks
jquery-ui-rails
js_cookie_rails
kaminari
logging (~> 2.0.0)
momentjs-rails (>= 2.9.0)
nested_form_fields
paperclip (~> 4.3)
pg
puma
rails (= 4.2.5)
rails_12factor
redcarpet
remotipart (~> 1.2)
rgl
roo (~> 2.1.0)
sass-rails (~> 5.0)
sdoc (~> 0.4.0)
skylight
spinjs-rails
turbolinks
tzinfo-data
uglifier (>= 1.3.0)
underscore-rails
web-console (~> 2.0)
wicked_pdf
wkhtmltopdf-binary
yomu
BUNDLED WITH
1.11.2

2356
LICENSE-3RD-PARTY.txt Normal file

File diff suppressed because it is too large Load diff

377
LICENSE.txt Normal file
View file

@ -0,0 +1,377 @@
Copyright (c) 2016 BioSistemika USA, LLC <info@biosistemika.com>
sciNote is licensed under the following license:
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

50
Makefile Normal file
View file

@ -0,0 +1,50 @@
APP_HOME="/usr/src/app"
all: docker database
heroku:
@heroku buildpacks:remove https://github.com/ddollar/heroku-buildpack-multi.git
@heroku buildpacks:set https://github.com/ddollar/heroku-buildpack-multi.git
@echo "Set environment variables, DATABASE_URL, RAILS_SERVE_STATIC_FILES, RAKE_ENV, RAILS_ENV, SECRET_KEY_BASE, SKYLIGHT_AUTHENTICATION"
docker:
@docker-compose build
db-cli:
@$(MAKE) rails cmd="rails db"
database:
@$(MAKE) rails cmd="rake db:create db:setup db:migrate"
rails:
@docker-compose run web $(cmd)
run:
@docker-compose up
start:
@docker-compose start
stop:
@docker-compose stop
cli:
@$(MAKE) rails cmd="/bin/bash"
tests:
@$(MAKE) rails cmd="rake test"
console:
@$(MAKE) rails cmd="rails console"
log:
@docker-compose web log
status:
@docker-compose ps
export:
@git checkout-index -a -f --prefix=scinote/
@tar -zcvf scinote-$(shell git rev-parse --short HEAD).tar.gz scinote
@rm -rf scinote

2
Procfile Normal file
View file

@ -0,0 +1,2 @@
web: bundle exec puma -C config/puma.rb
worker: bundle exec rake jobs:work

170
README.md Normal file
View file

@ -0,0 +1,170 @@
# sciNote
![sciNote logo](http://scinote.net/wp-content/uploads/2015/10/logo_sciNote_final.png)
## About
sciNote is an open source electronic scientific notebook ([ELN](https://en.wikipedia.org/wiki/Electronic_lab_notebook)) that helps you manage your laboratory work and stores all your experimental data in one place. sciNote is specifically designed for life science students, researchers, lab technicians and laboratory managers.
## Build & run
sciNote is developed in [Ruby on Rails](http://rubyonrails.org/). It also makes use of [Docker](https://www.docker.com/) technology, so the easiest way to run it is inside Docker containers.
### Quick start
The following are minimal steps needed to start sciNote in development environment:
1. Clone this Git repository onto your development machine.
2. Create a file `config/application.yml`. Populate it with mandatory environmental variables (see [environmental variables](#user-content-environmental-variables)).
3. In sciNote folder, run the following command: `make docker`. This can take a while, since Docker must first pull an image from the Internet, and then also install all neccesary Gems required by sciNote.
4. Once the Docker image is created, run `make cli` command. Once inside the running Docker container, run the following command: `rake db:reset`. This should initialize the database and fill it with (very minimal) seed data.
5. Exit the Docker container by typing `exit`.
6. To start the server, run command `make run`. Wait until the server starts listening on port `3000`.
7. Open your favourite browser and navigate to [http://localhost:3000](http://localhost:3000/). Use the seeded administrator account from [seeds.rb](db/seeds.rb) to login, or sign up for a new account.
### Docker structure
The main sciNote application runs in a Docker container called `web`. The database runs in a separate container, called `db`. This database container makes use of a special, persistent container called `dbdata`.
### Commands
Call `make` commands to build Docker images and build Rails environment, including database.
Following commands are available:
| Command | Description |
|----------------|-------------------------------------------------------------------------------------------------|
| `make docker` | Downloads the Docker image and build Gems. This should be called whenever `Gemfile` is changed. |
| `make db-cli` | Runs a `/bin/bash` inside the `db` container. |
| `make run` | Runs the `db` container & starts the Rails server in `web` container. |
| `make start` | Runs the `db` container & starts the Rails server in `web` container in background. |
| `make stop` | Stops the `db` & `web` containers. |
| `make cli` | Runs a `/bin/bash` inside the `web` container. |
| `make tests` | Execute all Rails tests. |
| `make console` | Enters the Rails console in `web` container. |
| `make export` | Zips the head of this Git repository into a `.tar.gz` file. |
## Environmental variables
sciNote reads configuration parameters from system environment parameters. On production servers, this can be simply be system environmentam variables, while for development, a file `config/application.yml` can be created to specify those variables.
The following table describes all available environmental variables for sciNote server.
| Variable | Mandatory | Description |
|-------------------------|-----------|-------------|
| SECRET_KEY_BASE | Yes | Random hash for Rails encryption. Can be generated by running `rake secret`. |
| PAPERCLIP_STORAGE | Yes | Set to `'s3'` to store files on Amazon S3, or `'filesystem'` to store files on local server. If storing on S3, additional parameters need to be specified. |
| AWS_SECRET_ACCESS_KEY | No* | If storing files on Amazon S3, this must contain access key for accessing AWS S3 API. |
| AWS_ACCESS_KEY_ID | No* | If storing files on Amazon S3, this must contain access key ID for AWS S3. |
| S3_BUCKET | No* | If storing files on Amazon S3, this must contain S3 bucket on which files are stored. |
| AWS_REGION | No* | If storing files on Amazon S3, this must contain the AWS region. |
| PAPERCLIP_DIRECT_UPLOAD | No* | If storing files on Amazon S3, this must be set either to `1` (to upload files directly from client-side to S3, without passing through sciNote server) or to `0` (to upload files to S3 through sciNote server). |
| MAIL_FROM | Yes | The **from** address for emails sent from sciNote. |
| MAIL_REPLYTO | Yes | The **reply to** address for emails sent from sciNote. |
| SMTP_ADDRESS | Yes | The server address of the SMTP mailer used for delivering emails generated in sciNote. |
| SMTP_PORT | Yes | The port of the SMTP server. Defaults to `587`. |
| SMTP_DOMAIN | Yes | The server domain of the SMTP mailer used for delivering emails generated in sciNote. |
| SMTP_USERNAME | Yes | The username for SMTP mailer used for delivering emails generated in sciNote. |
| SMTP_PASSWORD | Yes | The password for SMTP mailer used for delivering emails generated in sciNote. |
| MAIL_SERVER_URL | Yes | The root URL address of the actual sciNote server. This is used in sent emails to redirect user to the correct sciNote server URL. |
| PAPERCLIP_HASH_SECRET | Yes | Random key for generating Paperclip hash key for URLs. Can be generated via following Ruby function: `SecureRandom.base64(128)`. Defaults to `localhost`. |
## Rake tasks
### Delayed jobs
sciNote uses [delayed jobs](https://github.com/tobi/delayed_job) library to do background processing, mostly for the following tasks:
* Sending emails,
* Extracting text from uploaded files (*full-text* search).
Best option to run delayed jobs is inside a worker process. To start a background worker process that will execute delayed jobs, run the following command:
```
rake jobs:work
```
To clear all currently queued jobs, you can use the following command:
```
rake jobs:clear
```
**Warning!** This is not advised to do on production environments.
### Adding users
To simplify adding of new users to the system, couple of special `rake` tasks have been created.
The first, `rake db:add_user` simply queries all the information for a specific user via STDIN, and then proceeds to create the user.
The second task, `rake db:load_users[file_path,create_orgs]` takes 2 parameters as an input:
* Path to `.yml` file containing list of users & organizations to be added. The YAML file needs to be structured properly - field names must match those in the database, users need to have a name `user_<id>`, and organizations name `org_<id>`. For an example load users file, see [db/load_users_template.yml](db/load_users_template.yml) file.
* A boolean ('true' or 'false') whether to create individual organizations for each user or not.
Both of those rake actions include all database operations inside a transaction, so as long as any error happens during the process, database will be unaffected.
### Generating fake data
For testing purposes, two special tasks that will populate the database with randomized, fake data, have been implemented.
The first, `rake db:fake:generate`, will add fake data to an existing database. Since the algorithm that generates randomized data relies heavily on querying existing entries in database, use of this task is **not advisable**.
It is **much better** to use `rake db:fake` task, that will drop the database first, recreate it, and populate it with fake data afterwards.
### Web statistics
To check current login statistics of registered users, use `rake web_stats:login` task.
### Clearing data
Execute `rake data:clean_temp_files` to remove all temporary files. Temporary files are used when importing samples.
Execute `rake data:clean_unconfirmed_users` to remove all users that registered, but never confirmed their email.
Calling `rake data:clean` will execute both above tasks.
## Mailer
sciNote needs a configured SMTP mail server to work properly. See [environmental variables](#user-content-environmental-variables) for configuration of the mailer.
## Deploy onto Heroku
Before deploying to Heroku, install heroku client as describe on offical website. To use existing heroku application, add new git remote repository.
```
git remote add heroku git@heroku.com:my-random-app-name.git
```
Or create new heroku application by executing following command.
```
heroku create
```
Before pushing to heroku master branch, some environmental variables should be set.
### Heroku environmental variables
For deployment of sciNote onto Heroku, additional environmental variables need to be specified.
| Variable | Mandatory | Description |
|--------------------------|-----------|--------------------------------------------------------------------------------------------|
| SKYLIGHT_AUTHENTICATION | No | The API key for [Skylight](https://www.skylight.io/) code profiler, if choosing to use it. |
| LANG | Yes | The default localization language (e.g. `en_US.UTF-8`). |
| RAILS_ENV | Yes | Rails environment: `production`, `test` or `development`. |
| RACK_ENV | Yes | Rack environment: `production`, `test` or `development`. |
| RAILS_SERVE_STATIC_FILES | Yes | Whether to serve static files. Must be set to `enabled`. |
| WEB_CONCURRENCY | Yes | The concurrency of the server. See Heroku specifications for details. |
| MAX_THREADS | Yes | The max. number of threads. See Heroku specifications for details. |
| PORT | Yes | The port on which the application should run. See Heroku specifications for details. |
| S3_HOST_NAME | No* | If storing files on Amazon S3, this must contain the S3 service host name. |
| RAILS_FORCE_SSL | Yes | If set to `1`, enforce SSL communication on all levels of application. |
| DATABASE_URL | Yes | Full URL for connecting to PostgreSQL database. |
## Testing
In current version, only *model* tests are implemented for sciNote. To execute them, call `rake test:models`.
## Contributing
For contributing, see [CONTRIBUTING.MD](CONTRIBUTING.MD).
## License
sciNote is developed and maintained by BioSistemika USA, LLC, under [Mozilla Public License Version 2.0](LICENSE.txt).
See [LICENSE-3RD-PARTY.txt](LICENSE-3RD-PARTY.txt) for licenses of included third-party libraries.

6
Rakefile Normal file
View file

@ -0,0 +1,6 @@
# Add your own tasks in files placed in lib/tasks ending in .rake,
# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
require File.expand_path('../config/application', __FILE__)
Rails.application.load_tasks

0
app/assets/images/.keep Normal file
View file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
app/assets/images/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 KiB

View file

@ -0,0 +1,139 @@
// jquery.turbolinks MUST IMMEDIATELY FOLLOW jquery inclusion
// turbolinks MUST BE THE LAST inclusion
//= require jquery
//= require jquery.turbolinks
//= require jquery_ujs
//= require jquery.remotipart
//= require jquery.mousewheel.min
//= require jquery.scrollTo
//= require jquery-ui/widget
//= require jquery-ui/mouse
//= require jquery-ui/draggable
//= require jquery-ui/droppable
//= require jquery.ui.touch-punch.min
//= require hammer
//= require introjs
//= require js.cookie
//= require spin
//= require jquery.spin
//= require bootstrap-sprockets
//= require moment
//= require bootstrap-datetimepicker
//= require bootstrap-colorselector
//= require nested_form_fields
//= require_directory ./sitewide
//= require bootstrap-select
//= require underscore
//= require i18n.js
//= require i18n/translations
//= require turbolinks
// Initialize links for submitting forms. This is useful for submitting
// forms with clicking on links outside form in cases when other than
// GET method is used.
function initFormSubmitLinks(el) {
el = el || $(document.body);
$(".form-submit-link", el).click(function () {
var val = true;
if ($(this).is("[data-confirm-form]")) {
val = confirm($(this).data("confirm-form"));
}
// Only submit form if confirmed
if (val) {
animateLoading();
$("#" + $(this).data("submit-form")).submit();
}
});
}
/* Enable loading bars */
Turbolinks.enableProgressBar();
$(document)
.bind("ajaxSend", function(){
animateLoading();
})
.bind("ajaxComplete", function(){
animateLoading(false);
});
/*
* Show/hide loading indicator on top of page.
*/
function animateLoading(start){
if (start === undefined)
start = true;
start = start !== false;
if (start) {
$("#loading-animation").addClass("animate");
}
else {
$("#loading-animation").removeClass("animate");
}
}
/*
* Show/hide spinner for a given element.
* Shows spinner if start is true or not given, hides it if false.
* Optional parameter options for spin.js options.
*/
function animateSpinner(el, start, options) {
if (start === undefined)
start = true;
if (start && options) {
$(el).spin(options);
}
else {
$(el).spin(start);
}
if (start) {
$(el).append('<div class="loading-overlay" />');
}
else {
$(".loading-overlay").remove();
}
}
/*
* Disable Firefox caching to get rid of issues with pressing
* browser back, like opening canvas in edit mode.
*/
$(window).unload(function () { $(window).unbind('unload'); });
$(document.body).ready(function () {
// Activity feed modal in main navigation menu
var activityModal = $('#activity-modal');
var activityModalBody = activityModal.find('.modal-body');
var initMoreBtn = function () {
activityModalBody.find('.btn-more-activities')
.on('ajax:success', function (e, data) {
$(data.html).insertBefore($(this).parents('li'));
$(this).attr('href', data.next_url);
if (data.activities_number < data.per_page) {
$(this).remove();
}
});
};
$("#notifications .alert").on("closed.bs.alert", function () {
$("#content-wrapper")
.addClass("alert-hidden")
.removeClass("alert-shown");
});
$('#main-menu .btn-activity')
.on('ajax:before', function () {
activityModal.modal('show');
})
.on('ajax:success', function (e, data) {
activityModalBody.html(data.html);
initMoreBtn();
});
activityModal.on('hidden.bs.modal', function () {
activityModalBody.html('');
});
});

View file

@ -0,0 +1,2 @@
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.

View file

@ -0,0 +1,164 @@
(function (exports) {
function generateThumbnail(origFile, type, max_width, max_height, cb) {
var img = new Image;
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
// todo allow for different x/y ratio
canvas.width = max_width;
canvas.height = max_height;
img.src = URL.createObjectURL(origFile);
img.onload = function () {
var size;
var offsetX = 0;
var offsetY = 0;
if (this.width > this.height) {
size = this.height;
offsetX = (this.width - this.height) / 2;
} else {
size = this.width;
offsetY = (this.height - this.width) / 2;
}
if(type === "image/jpeg") {
type = "image/jpg";
}
ctx.drawImage(this, offsetX, offsetY, size, size, 0, 0, canvas.width, canvas.height);
canvas.toBlob(function (blob) {
cb(blob);
}, type, 0.8)
};
}
function fetchUploadSignature(file, origId, signUrl, cb) {
var csrfParam = $("meta[name=csrf-param]").attr("content");
var csrfToken = $("meta[name=csrf-token]").attr("content");
var xhr = new XMLHttpRequest;
var data = [];
data.push("file_name=" + file.name);
data.push("file_size=" + file.size);
data.push(csrfParam + "=" + encodeURIComponent(csrfToken));
if (origId) {
data.push("asset_id=" + origId);
}
xhr.open("POST", signUrl);
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
xhr.send(data.join("&"));
xhr.onload = function () {
try {
var data = JSON.parse(xhr.responseText);
cb(data);
} catch (e) {
cb();
}
};
}
function uploadData(data, cb) {
var xhr = new XMLHttpRequest;
var fd = new FormData();
var fields = data.fields;
var url = data.url;
for (var k in fields) {
fd.append(k, fields[k]);
}
fd.append("file", data.file, data.fileName);
xhr.open("POST", url);
xhr.send(fd);
xhr.onload = function () {
cb();
};
xhr.onerror = function (error) {
cb(I18n.t("errors.upload"));
};
}
var styleOptionRe = /(\d+)x(\d+)/i;
function parseStyleOption(option) {
var m = option.match(styleOptionRe)
return {
width: m && m[1] || 150,
height: m && m[2] || 150
}
}
exports.directUpload = function (form, origId, signUrl, cb, cbErr, errKey) {
var file = $(form).find("input[type=file]").get(0).files[0];
if (!file) {
cbErr();
return;
}
fetchUploadSignature(file, origId, signUrl, function (data) {
function processPost(error) {
var postData = posts[postPosition];
if (error) {
var errObj = {};
errKey = errKey|| "asset.file";
errObj[errKey] = [error];
cbErr(errObj);
return;
}
if (!postData) {
cb(data.asset_id);
return;
}
postData.fileName = file.name;
postPosition += 1;
var styleSize;
if (postData.style_option) {
styleSize = parseStyleOption(postData.style_option);
generateThumbnail(file, postData.mime_type, styleSize.width,
styleSize.height, function (blob) {
postData.file = blob;
uploadData(postData, processPost);
});
} else {
postData.file = file;
uploadData(postData, processPost);
}
}
if (!data || data.status === 'error') {
cbErr(data && data.errors);
return;
}
var posts = data.posts;
var postPosition = 0;
processPost();
});
}
}(this));

View file

@ -0,0 +1,215 @@
// Bind ajax for editing descriptions
function bindEditDescriptionAjax() {
var editDescriptionModal = $("#manage-module-description-modal");
var editDescriptionModalBody = editDescriptionModal.find(".modal-body");
var editDescriptionModalSubmitBtn = editDescriptionModal.find("[data-action='submit']");
$(".description-link")
.on("ajax:success", function (ev, data, status) {
var descriptionLink = $(".description-refresh");
// Set modal body & title
editDescriptionModalBody.html(data.html);
editDescriptionModal
.find("#manage-module-description-modal-label")
.text(data.title);
editDescriptionModalBody.find("form")
.on("ajax:success", function (ev2, data2, status2) {
// Update module's description in the tab
descriptionLink.html(data2.description_label);
// Close modal
editDescriptionModal.modal("hide");
})
.on("ajax:error", function (ev2, data2, status2) {
// Display errors if needed
$(this).render_form_errors("my_module", data.responseJSON);
});
// Show modal
editDescriptionModal.modal("show");
})
.on("ajax:error", function (ev, data, status) {
// TODO
});
editDescriptionModalSubmitBtn.on("click", function () {
// Submit the form inside the modal
editDescriptionModalBody.find("form").submit();
});
editDescriptionModal.on("hidden.bs.modal", function () {
editDescriptionModalBody.find("form").off("ajax:success ajax:error");
editDescriptionModalBody.html("");
});
}
// Bind ajax for editing due dates
function bindEditDueDateAjax() {
var editDueDateModal = null;
var editDueDateModalBody = null;
var editDueDateModalTitle = null;
var editDueDateModalSubmitBtn = null;
editDueDateModal = $("#manage-module-due-date-modal");
editDueDateModalBody = editDueDateModal.find(".modal-body");
editDueDateModalTitle = editDueDateModal.find("#manage-module-due-date-modal-label");
editDueDateModalSubmitBtn = editDueDateModal.find("[data-action='submit']");
$(".due-date-link")
.on("ajax:success", function (ev, data, status) {
var dueDateLink = $(".due-date-refresh");
// Load contents
editDueDateModalBody.html(data.html);
editDueDateModalTitle.text(data.title);
// Add listener to form inside modal
editDueDateModalBody.find("form")
.on("ajax:success", function (ev2, data2, status2) {
// Update module's due date
dueDateLink.html(data2.module_header_due_date_label);
// Close modal
editDueDateModal.modal("hide");
})
.on("ajax:error", function (ev2, data2, status2) {
// Display errors if needed
$(this).render_form_errors("my_module", data.responseJSON);
});
// Open modal
editDueDateModal.modal("show");
})
.on("ajax:error", function (ev, data, status) {
// TODO
});
editDueDateModalSubmitBtn.on("click", function () {
// Submit the form inside the modal
editDueDateModalBody.find("form").submit();
});
editDueDateModal.on("hidden.bs.modal", function () {
editDueDateModalBody.find("form").off("ajax:success ajax:error");
editDueDateModalBody.html("");
});
}
// Bind ajax for editing tags
function bindEditTagsAjax() {
var manageTagsModal = null;
var manageTagsModalBody = null;
// Initialize reloading of manage tags modal content after posting new
// tag.
function initAddTagForm() {
manageTagsModalBody.find(".add-tag-form")
.on("ajax:success", function (e, data) {
initTagsModalBody(data);
});
}
// Initialize edit tag & remove tag functionality from my_module links.
function initTagRowLinks() {
manageTagsModalBody.find(".edit-tag-link")
.on("click", function (e) {
var $this = $(this);
var li = $this.parents("li.list-group-item");
var editDiv = $(li.find("div.tag-edit"));
// Hide all other edit divs, show all show divs
manageTagsModalBody.find("div.tag-edit").hide();
manageTagsModalBody.find("div.tag-show").show();
editDiv.find("input[type=text]").val(li.data("name"));
editDiv.find('.edit-tag-color').colorselector('setColor', li.data("color"));
li.find("div.tag-show").hide();
editDiv.show();
});
manageTagsModalBody.find("div.tag-edit .dropdown-colorselector > .dropdown-menu li a")
.on("click", function (e) {
// Change background of the <li>
var $this = $(this);
var li = $this.parents("li.list-group-item");
li.css("background-color", $this.data("value"));
});
manageTagsModalBody.find(".remove-tag-link")
.on("ajax:success", function (e, data) {
initTagsModalBody(data);
});
manageTagsModalBody.find(".delete-tag-form")
.on("ajax:success", function (e, data) {
initTagsModalBody(data);
});
manageTagsModalBody.find(".edit-tag-form")
.on("ajax:success", function (e, data) {
initTagsModalBody(data);
})
.on("ajax:error", function (e, data) {
$(this).render_form_errors("tag", data.responseJSON);
});
manageTagsModalBody.find(".cancel-tag-link")
.on("click", function (e, data) {
var $this = $(this);
var li = $this.parents("li.list-group-item");
li.css("background-color", li.data("color"));
li.find(".edit-tag-form").clear_form_errors();
li.find("div.tag-edit").hide();
li.find("div.tag-show").show();
});
}
// Initialize ajax listeners and elements style on modal body. This
// function must be called when modal body is changed.
function initTagsModalBody(data) {
manageTagsModalBody.html(data.html);
manageTagsModalBody.find(".selectpicker").selectpicker();
initAddTagForm();
initTagRowLinks();
}
manageTagsModal = $("#manage-module-tags-modal");
manageTagsModalBody = manageTagsModal.find(".modal-body");
// Reload tags HTML element when modal is closed
manageTagsModal.on("hide.bs.modal", function () {
var tagsEl = $("#module-tags");
// Load HTML
$.ajax({
url: tagsEl.attr("data-module-tags-url"),
type: "GET",
dataType: "json",
success: function (data) {
tagsEl.find(".tags-refresh").html(data.html_module_header);
},
error: function (data) {
// TODO
}
});
});
// Remove modal content when modal window is closed.
manageTagsModal.on("hidden.bs.modal", function () {
manageTagsModalBody.html("");
});
// initialize my_module tab remote loading
$("a.edit-tags-link")
.on("ajax:before", function () {
manageTagsModal.modal('show');
})
.on("ajax:success", function (e, data) {
$("#manage-module-tags-modal-module").text(data.my_module.name);
initTagsModalBody(data);
});
}
bindEditDueDateAjax();
bindEditDescriptionAjax();
bindEditTagsAjax();

View file

@ -0,0 +1,16 @@
// Show more activities link.
$(".btn-more-activities")
.on("ajax:success", function (e, data) {
if (data.html) {
var list = $("#list-activities");
var moreBtn = $(".btn-more-activities");
// Remove button if all activities are shown
if (data.results_number < data.per_page) {
moreBtn.remove();
// Otherwise update reference
} else {
moreBtn.attr("href", data.more_url);
}
$(list).append(data.html);
}
});

View file

@ -0,0 +1,275 @@
function initHandsOnTables(root) {
root.find("div.hot_table").each(function() {
var $container = $(this).find(".step-result-hot-table");
var contents = $(this).find('.hot-contents');
$container.handsontable({
startRows: 5,
startCols: 5,
rowHeaders: true,
colHeaders: true,
cells: function (row, col, prop) {
var cellProperties = {};
if (col >= 0)
cellProperties.readOnly = true;
else
cellProperties.readOnly = false;
return cellProperties;
}
});
var hot = $container.handsontable('getInstance');
var data = JSON.parse(contents.attr("value"));
hot.loadData(data.data);
$(".result-panel-collapse-link")
.on("ajax:success", function() {
var collapseIcon = $(this).find(".collapse-result-icon");
// Toggle collapse button
collapseIcon.toggleClass("glyphicon-collapse-up");
collapseIcon.toggleClass("glyphicon-collapse-down");
root.find("div.step-result-hot-table").each(function() {
$(this).handsontable("render");
});
});
});
}
// Initialize comment form.
function initResultCommentForm($el) {
var $form = $el.find("ul form");
$(".help-block", $form).addClass("hide");
$form.on("ajax:send", function (data) {
$("#comment_message", $form).attr("readonly", true);
})
.on("ajax:success", function (e, data) {
if (data.html) {
var list = $form.parents("ul");
// Remove potential "no comments" element
list.parent().find(".content-comments")
.find("li.no-comments").remove();
list.parent().find(".content-comments")
.prepend("<li class='comment'>" + data.html + "</li>")
.scrollTop(0);
list.parents("ul").find("> li.comment:gt(8)").remove();
$("#comment_message", $form).val("");
$(".form-group", $form)
.removeClass("has-error");
$(".help-block", $form)
.html("")
.addClass("hide");
}
})
.on("ajax:error", function (ev, xhr) {
if (xhr.status === 400) {
var messageError = xhr.responseJSON.errors.message;
if (messageError) {
$(".form-group", $form)
.addClass("has-error");
$(".help-block", $form)
.html(messageError[0])
.removeClass("hide");
}
}
})
.on("ajax:complete", function () {
$("#comment_message", $form)
.attr("readonly", false)
.focus();
});
}
// Initialize show more comments link.
function initResultCommentsLink($el) {
$el.find(".btn-more-comments")
.on("ajax:success", function (e, data) {
if (data.html) {
var list = $(this).parents("ul");
var moreBtn = list.find(".btn-more-comments");
var listItem = moreBtn.parents('li');
$(data.html).insertBefore(listItem);
if (data.results_number < data.per_page) {
moreBtn.remove();
} else {
moreBtn.attr("href", data.more_url);
}
}
});
}
function initResultCommentTabAjax() {
$(".comment-tab-link")
.on("ajax:before", function (e) {
var $this = $(this);
var parentNode = $this.parents("li");
var targetId = $this.attr("aria-controls");
if (parentNode.hasClass("active")) {
// TODO move to fn
parentNode.removeClass("active");
$("#" + targetId).removeClass("active");
return false;
}
})
.on("ajax:success", function (e, data) {
if (data.html) {
var $this = $(this);
var targetId = $this.attr("aria-controls");
var target = $("#" + targetId);
var parentNode = $this.parents("ul").parent();
target.html(data.html);
initResultCommentForm(parentNode);
initResultCommentsLink(parentNode);
parentNode.find(".active").removeClass("active");
$this.parents("li").addClass("active");
target.addClass("active");
}
})
.on("ajax:error", function(e, xhr, status, error) {
// TODO
})
.on("ajax:complete", function () {
$(this).tab("show");
});
}
// Toggle editing buttons
function toggleResultEditButtons(show) {
if (show) {
$("#results-toolbar").show();
$(".edit-result-button").show();
} else {
$(".edit-result-button").hide();
$("#results-toolbar").hide();
}
}
// Expand all results
function expandAllResults() {
$('.result .panel-collapse').collapse('show');
$(document).find("div.step-result-hot-table").each(function() {
$(this).handsontable("render");
});
$(document).find("span.collapse-result-icon").each(function() {
$(this).addClass("glyphicon-collapse-up");
$(this).removeClass("glyphicon-collapse-down");
});
}
function expandResult(result) {
$('.panel-collapse', result).collapse('show');
$(result).find("span.collapse-result-icon").each(function() {
$(this).addClass("glyphicon-collapse-up");
$(this).removeClass("glyphicon-collapse-down");
});
}
initHandsOnTables($(document));
initResultCommentTabAjax();
expandAllResults();
initTutorial();
$(function () {
$("#results-collapse-btn").click(function () {
$('.result .panel-collapse').collapse('hide');
$(document).find("span.collapse-result-icon").each(function() {
$(this).addClass("glyphicon-collapse-down");
$(this).removeClass("glyphicon-collapse-up");
});
});
$("#results-expand-btn").click(expandAllResults);
});
// Initialize first-time tutorial
function initTutorial() {
var currentStep = Cookies.get('current_tutorial_step');
if (showTutorial() && (currentStep == '7' || currentStep == '8')) {
var moduleResultsTutorial = $("#results").attr("data-module-protocols-step-text");
Cookies.set('current_tutorial_step', '8');
introJs()
.setOptions({
steps: [{
element: document.getElementById("results-toolbar"),
intro: moduleResultsTutorial
}],
overlayOpacity: '0.1',
doneLabel: 'End tutorial',
showBullets: false,
showStepNumbers: false,
tooltipClass: 'custom disabled-next',
disableInteraction: true
})
.start();
// Destroy first-time tutorial cookies when skip tutorial
// or end tutorial is clicked
$(".introjs-skipbutton").each(function (){
$(this).click(function (){
Cookies.remove('tutorial_data');
Cookies.remove('current_tutorial_step');
});
});
}
}
function showTutorial() {
var tutorialData;
if (Cookies.get('tutorial_data'))
tutorialData = JSON.parse(Cookies.get('tutorial_data'));
else
return false;
var tutorialModuleId = tutorialData[0].qpcr_module;
var currentModuleId = $("#results").attr("data-module-id");
return tutorialModuleId == currentModuleId;
}
// S3 direct uploading
function startFileUpload(ev, btn) {
var form = btn.form;
var $form = $(form);
var assetInput = $form.find("input[name='result[asset_attributes][id]']").get(0);
var fileInput = $form.find("input[type=file]").get(0);
var origAssetId = assetInput ? assetInput.value : null;
var url = '/asset_signature.json';
$form.clear_form_errors();
animateSpinner($form);
directUpload(form, origAssetId, url, function (assetId) {
// edit mode - input field has to be removed
animateSpinner($form, false);
if (assetInput) {
assetInput.value = assetId;
$(fileInput).remove();
// create mode
} else {
fileInput.type = "hidden";
fileInput.name = "result[asset_attributes][id]";
fileInput.value = assetId;
}
btn.onclick = null;
$(btn).click();
}, function (errors) {
animateSpinner($form, false);
showResultFormErrors($form, errors);
});
ev.preventDefault();
}

View file

@ -0,0 +1,755 @@
// Sets callbacks for toggling checkboxes
function applyCheckboxCallBack() {
$("div.checkbox").on('click', function(e){
var checkboxitem = $(this).find("input");
var checked = checkboxitem.is(":checked");
$.ajax({
url: checkboxitem.data("link-url"),
type: "POST",
dataType: "json",
data: {checklistitem_id: checkboxitem.data("id"), checked: checked},
success: function (data) {
checkboxitem.prop("checked", checked);
},
error: function (data) {
checkboxitem.prop("checked", !checked);
}
});
});
}
// Sets callback for completing/uncompleting step
function applyStepCompletedCallBack() {
$("div.complete-step, div.uncomplete-step").on('click', function(e){
var button = $(this);
var step = $(this).parents(".step");
var completed = !step.hasClass("completed");
$.ajax({
url: button.data("link-url"),
type: "POST",
dataType: "json",
data: {completed: completed},
success: function (data) {
var button;
if (completed) {
step.addClass("completed").removeClass("not-completed");
button = step.find("div.complete-step");
button.addClass("uncomplete-step").removeClass("complete-step");
button.find(".btn").removeClass("btn-primary").addClass("btn-default");
}
else {
step.addClass("not-completed").removeClass("completed");
button = step.find("div.uncomplete-step");
button.addClass("complete-step").removeClass("uncomplete-step");
button.find(".btn").removeClass("btn-default").addClass("btn-primary");
}
button.find("button").html(data.new_title);
},
error: function (data) {
console.log ("error");
}
});
});
}
function applyCancelCallBack() {
//Click on cancel button
$("#cancel-edit").on("ajax:success", function(e, data) {
var $form = $(this).closest("form");
$form.after(data.html);
var $new_step = $(this).next();
$(this).remove();
initCallBacks();
initHandsOnTable($new_step);
toggleButtons(true);
});
$("#cancel-edit").on("ajax:error", function(e, xhr, status, error) {
// TODO: error handling
});
}
// Set callback for click on edit button
function applyEditCallBack() {
$(".edit-step").on("ajax:success", function(e, data) {
var $step = $(this).closest(".step");
var $edit_step = $step.after(data.html);
var $form = $step.next();
$step.remove();
formCallback($form);
initEditableHandsOnTable($form);
applyCancelCallBack();
formEditAjax($form);
toggleButtons(false);
initializeCheckboxSorting();
$("#new-step-checklists fieldset.nested_step_checklists ul").each(function () {
enableCheckboxSorting(this);
});
});
$(".edit-step").on("ajax:error", function(e, xhr, status, error) {
// TODO: render errors
});
}
function formCallback($form) {
$form
.on("fields_added.nested_form_fields", function(e, param) {
if (param.object_class == "table") {
initEditableHandsOnTable($form);
}
})
.on("fields_removed.nested_form_fields", function(e, param) {
if (param.object_class == "asset") {
// Clear file input
$(e.target).find("input[type='file']").val("");
}
});
// Add asset validation
$form.add_upload_file_size_check(function() {
tabsPropagateErrorClass($form);
});
// Add hidden fields for tables
$form.submit(function(){
$(this).find(".editable-table").each(function() {
var hot = $(this).find(".hot").handsontable('getInstance');
if (hot) {
var contents = $(this).find('.hot-contents');
var data = JSON.stringify({data: hot.getData()});
contents.attr("value", data);
}
});
return true;
});
}
// Init ajax success/error for edit form
function formEditAjax($form) {
var selectedTabIndex;
$form
.on("ajax:beforeSend", function () {
$(".nested_step_checklists ul").each(function () {
reorderCheckboxData(this);
});
})
.on("ajax:send", function(e, data) {
selectedTabIndex = $form.find("li.active").index() + 1;
});
$form.on("ajax:success", function(e, data) {
$(this).after(data.html);
var $new_step = $(this).next();
$(this).remove();
initCallBacks();
initHandsOnTable($new_step);
toggleButtons(true);
// Show the edited step
$new_step.find(".panel-collapse:first").addClass("collapse in");
//Rerender tables
$new_step.find("div.step-result-hot-table").each(function() {
$(this).handsontable("render");
});
});
$form.on("ajax:error", function(e, xhr, status, error) {
$(this).after(xhr.responseJSON.html);
var $form = $(this).next();
$(this).remove();
formCallback($form);
initEditableHandsOnTable($form);
applyCancelCallBack();
formEditAjax($form);
tabsPropagateErrorClass($form);
//Rerender tables
$new_step.find("div.step-result-hot-table").each(function() {
$(this).handsontable("render");
});
// Select the same tab pane as before
$form.find("ul.nav-tabs li.active").removeClass("active");
$form.find(".tab-content div.active").removeClass("active");
$form.find("ul.nav-tabs li:nth-child(" + selectedTabIndex + ")").addClass("active");
$form.find(".tab-content div:nth-child(" + selectedTabIndex + ")").addClass("active");
});
}
function formNewAjax($form) {
$form
.on("ajax:beforeSend", function () {
$(".nested_step_checklists ul").each(function () {
reorderCheckboxData(this);
});
})
.on("ajax:success", function(e, data) {
$(this).after(data.html);
var $new_step = $(this).next();
$(this).remove();
initCallBacks();
expandStep($new_step);
initHandsOnTable($new_step);
toggleButtons(true);
});
$form.on("ajax:error", function(e, xhr, status, error) {
$(this).after(xhr.responseJSON.html);
var $form = $(this).next();
$(this).remove();
formCallback($form);
formNewAjax($form);
applyCancelOnNew();
tabsPropagateErrorClass($form);
});
}
function toggleButtons(show) {
if (show) {
$("#new-step").show();
$(".edit-step-button").show();
} else {
$("#new-step").hide();
$(".edit-step-button").hide();
}
}
// Creates handsontable where needed
function initHandsOnTable(root) {
root.find("div.hot_table").each(function() {
var $container = $(this).find(".step-result-hot-table");
var contents = $(this).find('.hot-contents');
$container.handsontable({
startRows: 5,
startCols: 5,
rowHeaders: true,
colHeaders: true,
cells: function (row, col, prop) {
var cellProperties = {};
if (col >= 0)
cellProperties.readOnly = true;
else
cellProperties.readOnly = false;
return cellProperties;
}
});
var hot = $container.handsontable('getInstance');
if (contents.attr("value")) {
var data = JSON.parse(contents.attr("value"));
hot.loadData(data.data);
}
});
//Rerender tables after showing them in panel
$(".step-info-tab")
.on("shown.bs.tab", function() {
root.find("div.step-result-hot-table").each(function() {
$(this).handsontable("render");
});
});
$(".step-panel-collapse-link")
.on("ajax:success", function() {
var collapseIcon = $(this).find(".collapse-step-icon");
// Toggle collapse button
collapseIcon.toggleClass("glyphicon-collapse-up");
collapseIcon.toggleClass("glyphicon-collapse-down");
root.find("div.step-result-hot-table").each(function() {
$(this).handsontable("render");
});
});
}
// Init handsontable which can be edited
function initEditableHandsOnTable(root) {
root.find(".editable-table").each(function() {
var $container = $(this).find(".hot");
$container.handsontable({
startRows: 5,
startCols: 5,
rowHeaders: true,
colHeaders: true,
contextMenu: true
});
var hot = $(this).find(".hot").handsontable('getInstance');
var contents = $(this).find('.hot-contents');
if (contents.attr("value")) {
var data = JSON.parse(contents.attr("value"));
hot.loadData(data.data);
}
});
$(".new-step-tables-tab")
.on("shown.bs.tab", function() {
$(this).parents("form").find("div.hot").each(function() {
$(this).handsontable("render");
});
});
}
// Initialize comment form.
function initStepCommentForm($el) {
var $form = $el.find("ul form");
$(".help-block", $form).addClass("hide");
$form.on("ajax:send", function (data) {
$("#comment_message", $form).attr("readonly", true);
})
.on("ajax:success", function (e, data) {
if (data.html) {
var list = $form.parents("ul");
// Remove potential "no comments" element
list.parent().find(".content-comments")
.find("li.no-comments").remove();
list.parent().find(".content-comments")
.prepend("<li class='comment'>" + data.html + "</li>")
.scrollTop(0);
list.parents("ul").find("> li.comment:gt(8)").remove();
$("#comment_message", $form).val("");
$(".form-group", $form)
.removeClass("has-error");
$(".help-block", $form)
.html("")
.addClass("hide");
}
})
.on("ajax:error", function (ev, xhr) {
if (xhr.status === 400) {
var messageError = xhr.responseJSON.errors.message;
if (messageError) {
$(".form-group", $form)
.addClass("has-error");
$(".help-block", $form)
.html(messageError[0])
.removeClass("hide");
}
}
})
.on("ajax:complete", function () {
$("#comment_message", $form)
.attr("readonly", false)
.focus();
});
}
// Initialize show more comments link.
function initStepCommentsLink($el) {
$el.find(".btn-more-comments")
.on("ajax:success", function (e, data) {
if (data.html) {
var list = $(this).parents("ul");
var moreBtn = list.find(".btn-more-comments");
var listItem = moreBtn.parents('li');
$(data.html).insertBefore(listItem);
if (data.results_number < data.per_page) {
moreBtn.remove();
} else {
moreBtn.attr("href", data.more_url);
}
}
});
}
function initStepCommentTabAjax() {
$(".comment-tab-link")
.on("ajax:before", function (e) {
var $this = $(this);
var parentNode = $this.parents("li");
var targetId = $this.attr("aria-controls");
if (parentNode.hasClass("active")) {
// TODO move to fn
parentNode.removeClass("active");
$("#" + targetId).removeClass("active");
return false;
}
})
.on("ajax:success", function (e, data) {
if (data.html) {
var $this = $(this);
var targetId = $this.attr("aria-controls");
var target = $("#" + targetId);
var parentNode = $this.parents("ul").parent();
target.html(data.html);
initStepCommentForm(parentNode);
initStepCommentsLink(parentNode);
parentNode.find(".active").removeClass("active");
$this.parents("li").addClass("active");
target.addClass("active");
}
})
.on("ajax:error", function(e, xhr, status, error) {
// TODO
})
.on("ajax:complete", function () {
$(this).tab("show");
});
}
function applyCancelOnNew() {
$(".cancel-new").click(function() {
var $form = $(this).closest("form");
$form.parent().remove();
toggleButtons(true);
});
}
function initDeleteStep(){
$(".delete-step").on("confirm:complete", function (e, answer) {
if (answer) {
animateLoading();
}
});
}
function initCallBacks() {
applyCheckboxCallBack();
applyStepCompletedCallBack();
applyEditCallBack();
initStepCommentTabAjax();
initDeleteStep();
}
function reorderCheckboxData(el) {
var itemIds = [];
var checkboxes = $(el).find(".nested_fields:not(:hidden) .form-group");
checkboxes.each(function () {
var itemId = $(this).find("label").attr("for").match(/(\d+)_text/)[1];
itemIds.push(itemId);
});
itemIds.sort();
checkboxes.each(function (i) {
var $this = $(this);
var label = $this.find(".control-label");
var input = $this.find(".form-control");
var posInput = $this.parent().find(".checklist-item-pos");
var itemId = itemIds[i];
var forAttr = label.attr("for");
var idAttr = input.attr("id");
var nameAttr = input.attr("name");
var posIdAttr = posInput.attr("id");
var posNameAttr = posInput.attr("name");
forAttr = forAttr.replace(/\d+_text/, itemId + "_text");
nameAttr = nameAttr.replace(/\[\d+\]\[text\]/, "[" + itemId + "][text]");
posIdAttr = posIdAttr.replace(/\d+_position/, itemId + "_text");
posNameAttr = posNameAttr.replace(/\[\d+\]\[position\]/, "[" + itemId + "][position]")
label.attr("for", forAttr);
input.attr("name", nameAttr);
input.attr("id", forAttr);
posInput.attr("name", posNameAttr);
posInput.attr("id", posIdAttr);
posInput.val(itemId);
});
}
function enableCheckboxSorting(el) {
Sortable.create(el, {
draggable: 'fieldset',
filter: 'script',
handle: '.glyphicon-chevron-right',
onUpdate: function () {
reorderCheckboxData(el);
}
});
}
function initializeCheckboxSorting() {
var el = $("#new-step-checklists a[data-association-path=step_checklists]");
el.click(function () {
// calling code below must be defered because at this step HTML in not
// inserted into DOM.
setTimeout(function () {
var list = el.parent().find("fieldset.nested_step_checklists:last ul");
enableCheckboxSorting(list.get(0));
});
});
}
// New step AJAX
$("#new-step").on("ajax:success", function(e, data) {
var $form = $(data.html);
$("#steps").append($form);
// Scroll to bottom
$("html, body").animate({ scrollTop: $(document).height()-$(window).height() });
formCallback($form);
formNewAjax($form);
applyCancelOnNew();
toggleButtons(false);
initializeCheckboxSorting();
});
// Initialize edit description modal window
function initEditDescription() {
var editDescriptionModal = $("#manage-module-description-modal");
var editDescriptionModalBody = editDescriptionModal.find(".modal-body");
var editDescriptionModalSubmitBtn = editDescriptionModal.find("[data-action='submit']");
$(".description-link")
.on("ajax:success", function(ev, data, status) {
var descriptionLink = $(".description-refresh");
// Set modal body & title
editDescriptionModalBody.html(data.html);
editDescriptionModal
.find("#manage-module-description-modal-label")
.text(data.title);
editDescriptionModalBody.find("form")
.on("ajax:success", function(ev2, data2, status2) {
// Update module's description in the tab
descriptionLink.html(data2.description_label);
// Close modal
editDescriptionModal.modal("hide");
})
.on("ajax:error", function(ev2, data2, status2) {
// Display errors if needed
$(this).render_form_errors("my_module", data.responseJSON);
});
// Show modal
editDescriptionModal.modal("show");
})
.on("ajax:error", function(ev, data, status) {
// TODO
});
editDescriptionModalSubmitBtn.on("click", function() {
// Submit the form inside the modal
editDescriptionModalBody.find("form").submit();
});
editDescriptionModal.on("hidden.bs.modal", function() {
editDescriptionModalBody.find("form").off("ajax:success ajax:error");
editDescriptionModalBody.html("");
});
}
function bindEditDueDateAjax() {
var editDueDateModal = null;
var editDueDateModalBody = null;
var editDueDateModalTitle = null;
var editDueDateModalSubmitBtn = null;
editDueDateModal = $("#manage-module-due-date-modal");
editDueDateModalBody = editDueDateModal.find(".modal-body");
editDueDateModalTitle = editDueDateModal.find("#manage-module-due-date-modal-label");
editDueDateModalSubmitBtn = editDueDateModal.find("[data-action='submit']");
$(".due-date-link")
.on("ajax:success", function(ev, data, status) {
var dueDateLink = $(".due-date-refresh");
// Load contents
editDueDateModalBody.html(data.html);
editDueDateModalTitle.text(data.title);
// Add listener to form inside modal
editDueDateModalBody.find("form")
.on("ajax:success", function(ev2, data2, status2) {
// Update module's due date
dueDateLink.html(data2.module_header_due_date_label);
// Close modal
editDueDateModal.modal("hide");
})
.on("ajax:error", function(ev2, data2, status2) {
// Display errors if needed
$(this).render_form_errors("my_module", data.responseJSON);
});
// Open modal
editDueDateModal.modal("show");
})
.on("ajax:error", function(ev, data, status) {
// TODO
});
editDueDateModalSubmitBtn.on("click", function() {
// Submit the form inside the modal
editDueDateModalBody.find("form").submit();
});
editDueDateModal.on("hidden.bs.modal", function() {
editDueDateModalBody.find("form").off("ajax:success ajax:error");
editDueDateModalBody.html("");
});
}
// Expand all steps
function expandAllSteps () {
$('.step .panel-collapse').collapse('show');
$(document).find("div.step-result-hot-table").each(function() {
$(this).handsontable("render");
});
$(document).find("span.collapse-step-icon").each(function() {
$(this).addClass("glyphicon-collapse-up");
$(this).removeClass("glyphicon-collapse-down");
});
}
function expandStep(step) {
$('.panel-collapse', step).collapse('show');
$(step).find("span.collapse-step-icon").each(function() {
$(this).addClass("glyphicon-collapse-up");
$(this).removeClass("glyphicon-collapse-down");
});
}
// On init
initCallBacks();
initHandsOnTable($(document));
bindEditDueDateAjax();
initEditDescription();
expandAllSteps();
initTutorial();
$(function () {
$("#steps-collapse-btn").click(function () {
$('.step .panel-collapse').collapse('hide');
$(document).find("span.collapse-step-icon").each(function() {
$(this).addClass("glyphicon-collapse-down");
$(this).removeClass("glyphicon-collapse-up");
});
});
$("#steps-expand-btn").click(expandAllSteps);
});
function initTutorial() {
var currentStep = Cookies.get('current_tutorial_step');
if (showTutorial() && (currentStep == '6' || currentStep == '7')) {
var navTabs = $("#secondary-menu").find("ul.navbar-right");
var moduleProtocolsTutorial = $("#steps").attr("data-module-protocols-step-text");
navTabs.attr('data-step', '7');
navTabs.attr('data-intro', moduleProtocolsTutorial);
navTabs.attr('data-position', 'left');
Cookies.set('current_tutorial_step', '7');
introJs()
.setOptions({
overlayOpacity: '0.1',
doneLabel: 'End tutorial',
showBullets: false,
showStepNumbers: false,
tooltipClass: 'custom disabled-next'
})
.start();
// Destroy first-time tutorial cookies when skip tutorial
// or end tutorial is clicked
$(".introjs-skipbutton").each(function (){
$(this).click(function (){
Cookies.remove('tutorial_data');
Cookies.remove('current_tutorial_step');
});
});
}
}
function showTutorial() {
var tutorialData;
if (Cookies.get('tutorial_data'))
tutorialData = JSON.parse(Cookies.get('tutorial_data'));
else
return false;
var tutorialModuleId = tutorialData[0].qpcr_module;
var currentModuleId = $("#steps").attr("data-module-id");
return tutorialModuleId == currentModuleId;
}
// S3 direct uploading
function startFileUpload(ev, btn) {
var form = btn.form;
var $form = $(form);
var fileInputs = $form.find("input[type=file]");
var url = '/asset_signature.json';
var inputPos = 0;
var inputPointer = 0;
$form.clear_form_errors();
animateSpinner($form);
function processFile () {
var fileInput = fileInputs.get(inputPos);
inputPos += 1;
inputPointer += 1;
if (!fileInput) {
btn.onclick = null;
$(btn).click();
animateSpinner($form, false);
return;
}
directUpload(form, null, url, function (assetId) {
fileInput.type = "hidden";
fileInput.name = fileInput.name.replace("[file]", "[id]");
fileInput.value = assetId;
inputPointer -= 1;
processFile();
}, function (errors) {
var assetError;
animateSpinner($form, false);
for (var c in errors) {
if (/^asset\./.test(c)) {
assetError = errors[c];
break;
}
}
if (assetError) {
var el = $form.find("input[type=file]").get(inputPointer - 1);
var $el = $(el);
$form.clear_form_errors();
$el.closest(".form-group").addClass("has-error");
$el.parent().append("<span class='help-block'>" + assetError + "</span>");
}
});
}
processFile();
ev.preventDefault();
}

View file

@ -0,0 +1,4 @@
/* Loading overlay for search */
$("#search-bar").submit(function (){
animateSpinner(document.body);
});

View file

@ -0,0 +1,2 @@
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.

View file

@ -0,0 +1,2 @@
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,440 @@
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.
// TODO
// - error handling of assigning user to project, check XHR data.errors
// - error handling of removing user from project, check XHR data.errors
// - refresh project users tab after manage user modal is closed
// - refactor view handling using library, ex. backbone.js
(function () {
var newProjectModal = null;
var newProjectModalForm = null;
var newProjectBtn = null;
var editProjectModal = null;
var editProjectModalTitle = null;
var editProjectModalForm = null;
var editProjectBtn = null;
var manageUsersModal = null;
var manageUsersModalBody = null;
/**
* Stupid Bootstrap btn-group bug hotfix
* @param btnGroup - The button group element.
*/
function activateBtnGroup(btnGroup) {
var btns = btnGroup.find(".btn");
btns.find("input[type='radio']")
.removeAttr("checked")
.prop("checked", false);
btns.filter(".active")
.find("input[type='radio']")
.attr("checked", "checked")
.prop("checked", true);
}
/**
* Initialize the JS for new project modal to work.
*/
function initNewProjectModal() {
newProjectModalForm.submit(function() {
// Stupid Bootstrap btn-group bug hotfix
activateBtnGroup(
newProjectModal
.find("form .btn-group[data-toggle='buttons']")
);
});
newProjectModal.on("hidden.bs.modal", function () {
// When closing the new project modal, clear its input vals
// and potential errors
newProjectModalForm.clear_form_errors();
// Clear input fields
newProjectModalForm.clear_form_fields();
var orgSelect = newProjectModalForm.find('select#project_organization_id');
orgSelect.val(0);
orgSelect.selectpicker('refresh');
var orgHidden = newProjectModalForm.find('input#project_visibility_hidden');
var orgVisible = newProjectModalForm.find('input#project_visibility_visible');
orgHidden.prop("checked", true);
orgHidden.attr("checked", "checked");
orgHidden.parent().addClass("active");
orgVisible.prop("checked", false);
orgVisible.parent().removeClass("active");
})
.on("show.bs.modal", function() {
var orgSelect = newProjectModalForm.find('select#project_organization_id');
orgSelect.selectpicker('refresh');
});
newProjectModalForm
.on("ajax:success", function(data, status, jqxhr) {
// Redirect to response page
$(location).attr("href", status.url);
})
.on("ajax:error", function(jqxhr, status, error) {
$(this).render_form_errors("project", status.responseJSON);
});
newProjectBtn.click(function(e) {
// Show the modal
newProjectModal.modal("show");
return false;
});
}
/**
* Initialize the JS for edit project modal to work.
*/
function initEditProjectModal() {
// Edit button click handler
editProjectBtn.click(function() {
// Stupid Bootstrap btn-group bug hotfix
activateBtnGroup(
editProjectModalBody
.find("form .btn-group[data-toggle='buttons']")
);
// Submit the modal body's form
editProjectModalBody.find("form").submit();
});
// On hide modal handler
editProjectModal.on("hidden.bs.modal", function() {
editProjectModalBody.html("");
});
$(".panel-project a[data-action='edit']")
.on("ajax:success", function(ev, data, status) {
// Update modal title
editProjectModalTitle.html(data.title);
// Set modal body
editProjectModalBody.html(data.html);
// Add modal body's submit handler
editProjectModal.find("form")
.on("ajax:success", function(ev2, data2, status2) {
// Project saved, replace changed project's title
var responseHtml = $(data2.html);
var id = responseHtml.attr("data-id");
var newTitle = responseHtml.find(".panel-title");
var existingTitle =
$(".panel-project[data-id='" + id + "'] .panel-title");
existingTitle.after(newTitle);
existingTitle.remove();
// Hide modal
editProjectModal.modal("hide");
})
.on("ajax:error", function(ev2, data2, status2) {
$(this).render_form_errors("project", data2.responseJSON);
});
// Show the modal
editProjectModal.modal("show");
})
.on("ajax:error", function(ev, data, status) {
// TODO
});
}
function initManageUsersModal() {
// Reload users tab HTML element when modal is closed
manageUsersModal.on("hide.bs.modal", function () {
var projectEl = $("#" + $(this).attr("data-project-id"));
// Load HTML to refresh users list
$.ajax({
url: projectEl.attr("data-project-users-tab-url"),
type: "GET",
dataType: "json",
success: function (data) {
projectEl.find("#users-" + projectEl.attr("id")).html(data.html);
initUsersEditLink(projectEl);
},
error: function (data) {
// TODO
}
});
});
// Remove modal content when modal window is closed.
manageUsersModal.on("hidden.bs.modal", function () {
manageUsersModalBody.html("");
});
}
// Initialize users editing modal remote loading.
function initUsersEditLink($el) {
$el.find(".manage-users-link")
.on("ajax:before", function () {
var projectId = $(this).closest(".panel-default").attr("id");
manageUsersModal.attr("data-project-id", projectId);
manageUsersModal.modal('show');
})
.on("ajax:success", function (e, data) {
$("#manage-users-modal-project").text(data.project.name);
initUsersModalBody(data);
});
}
// Initialize comment form.
function initCommentForm($el) {
var $form = $el.find("ul form");
$(".help-block", $form).addClass("hide");
$form.on("ajax:send", function (data) {
$("#comment_message", $form).attr("readonly", true);
})
.on("ajax:success", function (e, data) {
if (data.html) {
var list = $form.parents("ul");
// Remove potential "no comments" element
list.parent().find(".content-comments")
.find("li.no-comments").remove();
list.parent().find(".content-comments")
.prepend("<li class='comment'>" + data.html + "</li>")
.scrollTop(0);
list.parents("ul").find("> li.comment:gt(8)").remove();
$("#comment_message", $form).val("");
$(".form-group", $form)
.removeClass("has-error");
$(".help-block", $form)
.html("")
.addClass("hide");
}
})
.on("ajax:error", function (ev, xhr) {
if (xhr.status === 400) {
var messageError = xhr.responseJSON.errors.message;
if (messageError) {
$(".form-group", $form)
.addClass("has-error");
$(".help-block", $form)
.html(messageError[0])
.removeClass("hide");
}
}
})
.on("ajax:complete", function () {
$("#comment_message", $form)
.attr("readonly", false)
.focus();
});
}
// Initialize show more comments link.
function initCommentsLink($el) {
$el.find(".btn-more-comments")
.on("ajax:success", function (e, data) {
if (data.html) {
var list = $(this).parents("ul");
var moreBtn = list.find(".btn-more-comments");
var listItem = moreBtn.parents('li');
$(data.html).insertBefore(listItem);
if (data.results_number < data.per_page) {
moreBtn.remove();
} else {
moreBtn.attr("href", data.more_url);
}
}
});
}
// Initialize reloading manage user modal content after posting new
// user.
function initAddUserForm() {
manageUsersModalBody.find(".add-user-form")
.on("ajax:success", function (e, data) {
initUsersModalBody(data);
if (data.status === 'error') {
$(this).addClass("has-error");
$(this).append("<span class='help-block col-xs-8'>" + data.error + "</span>");
}
});
}
// Initialize remove user from project links.
function initRemoveUserLinks() {
manageUsersModalBody.find(".remove-user-link")
.on("ajax:success", function (e, data) {
initUsersModalBody(data);
});
}
//
function initUserRoleForms() {
manageUsersModalBody.find(".update-user-form select")
.on("change", function () {
$(this).parents("form").submit();
});
manageUsersModalBody.find(".update-user-form")
.on("ajax:success", function (e, data) {
initUsersModalBody(data);
})
.on("ajax:error", function (e, xhr, status, error) {
// TODO
});
}
// Initialize ajax listeners and elements style on modal body. This
// function must be called when modal body is changed.
function initUsersModalBody(data) {
manageUsersModalBody.html(data.html);
manageUsersModalBody.find(".selectpicker").selectpicker();
initAddUserForm();
initRemoveUserLinks();
initUserRoleForms();
}
function init() {
newProjectModal = $("#new-project-modal");
newProjectModalForm = newProjectModal.find("form");
newProjectBtn = $("#new-project-btn");
editProjectModal = $("#edit-project-modal");
editProjectModalTitle = editProjectModal.find("#edit-project-modal-label");
editProjectModalBody = editProjectModal.find(".modal-body");
editProjectBtn = editProjectModal.find(".btn[data-action='submit']");
manageUsersModal = $("#manage-users-modal");
manageUsersModalBody = manageUsersModal.find(".modal-body");
initNewProjectModal();
initEditProjectModal();
initManageUsersModal();
// initialize project tab remote loading
$(".panel-project .panel-footer [role=tab]")
.on("ajax:before", function (e) {
var $this = $(this);
var parentNode = $this.parents("li");
var targetId = $this.attr("aria-controls");
if (parentNode.hasClass("active")) {
// TODO move to fn
parentNode.removeClass("active");
$("#" + targetId).removeClass("active");
return false;
}
})
.on("ajax:success", function (e, data, status, xhr) {
var $this = $(this);
var targetId = $this.attr("aria-controls");
var target = $("#" + targetId);
var parentNode = $this.parents("ul").parent();
target.html(data.html);
initUsersEditLink(parentNode);
initCommentForm(parentNode);
initCommentsLink(parentNode);
// TODO move to fn
parentNode.find(".active").removeClass("active");
$this.parents("li").addClass("active");
target.addClass("active");
})
.on("ajax:error", function (e, xhr, status, error) {
// TODO
});
// Initialize first-time tutorial
if (Cookies.get('tutorial_data')) {
var tutorialData = JSON.parse(Cookies.get('tutorial_data'));
var goToStep = 1;
var demoProjectId = tutorialData[0].project;
if (Cookies.get('current_tutorial_step')) {
goToStep = parseInt(Cookies.get('current_tutorial_step'));
}
if (goToStep < 4) {
var projectOptionsTutorial = $("#projects-toolbar").attr("data-project-options-step-text");
var demoProject = $("#" + demoProjectId);
demoProject.attr('data-step', '3');
demoProject.attr('data-intro', projectOptionsTutorial);
demoProject.attr('data-position', 'left');
demoProject.attr('data-tooltipClass', 'custom disabled-next');
introJs()
.setOptions({
overlayOpacity: '0.2',
nextLabel: 'Next',
doneLabel: 'End tutorial',
skipLabel: 'End tutorial',
showBullets: false,
showStepNumbers: false,
tooltipClass: 'custom'
})
.goToStep(goToStep)
.onafterchange(function (tarEl) {
Cookies.set('current_tutorial_step', this._currentStep+1);
})
.start();
// Destroy first-time tutorial cookies when skip tutorial
// or end tutorial is clicked
$(".introjs-skipbutton").each(function (){
$(this).click(function (){
Cookies.remove('tutorial_data');
Cookies.remove('current_tutorial_step');
});
});
}
else if (goToStep > 11) {
var archiveProjectTutorial = $("#projects-toolbar").attr("data-archive-project-step-text");
Cookies.set('current_tutorial_step', '13');
introJs()
.setOptions({
steps: [{
element: document.getElementById(demoProjectId),
intro: archiveProjectTutorial,
position: "right"
}],
overlayOpacity: '0.2',
doneLabel: 'Start using sciNote',
showBullets: false,
showStepNumbers: false,
disableInteraction: false,
tooltipClass: 'custom disabled-next'
})
.oncomplete(function () {
Cookies.remove('tutorial_data');
Cookies.remove('current_tutorial_step');
})
.start();
}
}
}
init();
}());

View file

@ -0,0 +1,200 @@
(function () {
var newReportModal = null;
var newReportModalBody = null;
var newReportCreateButton = null;
var deleteReportsModal = null;
var deleteReportsInput = null;
var newReportButton = null;
var editReportButton = null;
var deleteReportsButton = null;
var checkAll = null;
var allChecks = null;
var allRows = null;
var checkedReports = [];
/**
* Initialize the new report modal.
*/
function initNewReportModal() {
// TEMPORARY DISABLED
/**
// Remove modal content when modal window is closed.
newReportModal.on("hidden.bs.modal", function () {
newReportModalBody.html("");
});
// Populate modal content when AJAX call is complete
newReportButton
.on("ajax:before", function () {
newReportModal.modal('show');
})
.on("ajax:success", function (e, data) {
newReportModalBody.html(data.html);
});
// Before redirecting, pass parameters
newReportCreateButton.click(function(event){
var url = $(this).closest("form").attr("action");
event.preventDefault();
// Copy the GET params
var val = newReportModalBody.find(".btn-primary.active > input[type='radio']").attr("value");
url += "/" + val;
$(location).attr("href", url);
return false;
});
*/
}
/**
* Initialize interaction between checkboxes, editing and deleting.
*/
function initCheckboxesAndEditing() {
checkAll.click(function() {
allChecks.prop("checked", this.checked);
checkedReports = [];
if (this.checked) {
_.each(allRows, function(row) {
checkedReports.push($(row).data("id"));
});
}
updateButtons();
});
allChecks.click(function() {
checkAll.prop("checked", false);
var id = $(this).closest(".report-row").data("id");
if (this.checked) {
if (_.indexOf(checkedReports, id) === -1) {
checkedReports.push(id);
}
} else {
var idx = _.indexOf(checkedReports, id);
if (idx !== -1) {
checkedReports.splice(idx, 1);
}
}
updateButtons();
});
}
/**
* Update edit & delete buttons depending on checking of reports.
*/
function updateButtons() {
if (checkedReports.length === 0) {
editReportButton.addClass("disabled");
deleteReportsButton.addClass("disabled");
} else if (checkedReports.length === 1) {
editReportButton.removeClass("disabled");
deleteReportsButton.removeClass("disabled");
} else {
editReportButton.addClass("disabled");
deleteReportsButton.removeClass("disabled");
}
}
/**
* Initialize the edit report functionality.
*/
function initEditReport() {
editReportButton.click(function(e) {
animateLoading();
if (checkedReports.length === 1) {
var id = checkedReports[0];
var row = $(".report-row[data-id='" + id + "']");
var url = row.data("edit-link");
$(location).attr("href", url);
}
return false;
});
}
/**
* Initialize the deleting of reports.
*/
function initDeleteReports() {
deleteReportsButton.click(function(e) {
if (checkedReports.length > 0) {
// Copy the checked IDs into the hidden input
deleteReportsInput.attr("value", "[" + checkedReports + "]");
// Show modal
deleteReportsModal.modal("show");
}
});
$("#confirm-delete-reports-btn").click(function(e) {
animateLoading();
});
}
/* Initilize first-time tutorial if needed */
function initTutorial() {
var currentStep = Cookies.get('current_tutorial_step');
if (showTutorial() && (currentStep == '10' || currentStep == '11')) {
Cookies.set('current_tutorial_step', '11');
introJs()
.setOptions({
overlayOpacity: '0.1',
doneLabel: 'End tutorial',
showBullets: false,
showStepNumbers: false,
tooltipClass: 'custom disabled-next'
})
.start();
// Destroy first-time tutorial cookies when skip tutorial
// or end tutorial is clicked
$(".introjs-skipbutton").each(function (){
$(this).click(function (){
Cookies.remove('tutorial_data');
Cookies.remove('current_tutorial_step');
});
});
}
}
function showTutorial() {
var tutorialData;
if (Cookies.get('tutorial_data'))
tutorialData = JSON.parse(Cookies.get('tutorial_data'));
else
return false;
var tutorialProjectId = tutorialData[0].project;
var currentProjectId = $(".report-table").attr("data-project-id");
return tutorialProjectId == currentProjectId;
}
$(document).ready(function() {
// Initialize selectors
newReportModal = $("#new-report-modal");
newReportModalBody = newReportModal.find(".modal-body");
newReportCreateButton = $("#create-new-report-btn");
deleteReportsModal = $("#delete-reports-modal");
deleteReportsInput = $("#report-ids");
newReportButton = $("#new-report-btn");
editReportButton = $("#edit-report-btn");
deleteReportsButton = $("#delete-reports-btn");
checkAll = $(".check-all-reports");
allChecks = $(".check-report");
allRows = $(".report-row");
initNewReportModal();
initCheckboxesAndEditing();
updateButtons();
initEditReport();
initDeleteReports();
initTutorial();
});
}());

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,82 @@
// New result asset behaviour
$("#new-result-asset").on("ajax:success", function(e, data) {
var $form = $(data.html);
$("#results").prepend($form);
$form.add_upload_file_size_check();
formAjaxResultAsset($form);
// Cancel button
$form.find(".cancel-new").click(function () {
$form.remove();
toggleResultEditButtons(true);
});
toggleResultEditButtons(false);
});
$("#new-result-asset").on("ajax:error", function(e, xhr, status, error) {
//TODO: Add error handling
});
// Edit result asset button behaviour
function applyEditResultAssetCallback() {
$(".edit-result-asset").on("ajax:success", function(e, data) {
var $result = $(this).closest(".result");
var $form = $(data.html);
var $prevResult = $result;
$result.after($form);
$result.remove();
$form.add_upload_file_size_check();
formAjaxResultAsset($form);
// Cancel button
$form.find(".cancel-edit").click(function () {
$form.after($prevResult);
$form.remove();
applyEditResultAssetCallback();
toggleResultEditButtons(true);
});
toggleResultEditButtons(false);
});
$(".edit-result-asset").on("ajax:error", function(e, xhr, status, error) {
//TODO: Add error handling
});
}
function showResultFormErrors($form, errors) {
$form.render_form_errors("result", errors);
if (errors["asset.file"]) {
var $el = $form.find("input[type=file]");
$el.closest(".form-group").addClass("has-error");
$el.parent().append("<span class='help-block'>" + errors["asset.file"] + "</span>");
}
}
// Apply ajax callback to form
function formAjaxResultAsset($form) {
$form.on("ajax:success", function(e, data) {
if (data.status === "ok") {
$form.after(data.html);
var newResult = $form.next();
initFormSubmitLinks(newResult);
$(this).remove();
applyEditResultAssetCallback();
toggleResultEditButtons(true);
initResultCommentTabAjax();
expandResult(newResult);
} else if (data.status === 'error') {
showResultFormErrors($form, data.errors);
}
});
}
applyEditResultAssetCallback();

View file

@ -0,0 +1,2 @@
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.

View file

@ -0,0 +1,103 @@
// Init handsontable which can be edited
function initEditableHandsOnTable(root) {
root.find(".editable-table").each(function() {
var $container = $(this).find(".hot");
$container.handsontable({
startRows: 5,
startCols: 5,
rowHeaders: true,
colHeaders: true,
contextMenu: true
});
var hot = $(this).find(".hot").handsontable('getInstance');
var contents = $(this).find('.hot-contents');
if (contents.attr("value")) {
var data = JSON.parse(contents.attr("value"));
hot.loadData(data.data);
}
});
}
function onSubmitExtractTable($form) {
$form.submit(function(){
var hot = $(".hot").handsontable('getInstance');
var contents = $('.hot-contents');
var data = JSON.stringify({data: hot.getData()});
contents.attr("value", data)
return true;
});
}
// Edit result table button behaviour
function applyEditResultTableCallback() {
$(".edit-result-table").on("ajax:success", function(e, data) {
var $result = $(this).closest(".result");
var $form = $(data.html);
var $prevResult = $result;
$result.after($form);
$result.remove();
formAjaxResultTable($form);
initEditableHandsOnTable($form);
onSubmitExtractTable($form);
// Cancel button
$form.find(".cancel-edit").click(function () {
$form.after($prevResult);
$form.remove();
applyEditResultTableCallback();
toggleResultEditButtons(true);
});
toggleResultEditButtons(false);
});
$(".edit-result-table").on("ajax:error", function(e, xhr, status, error) {
//TODO: Add error handling
});
}
// New result text behaviour
$("#new-result-table").on("ajax:success", function(e, data) {
var $form = $(data.html);
$("#results").prepend($form);
formAjaxResultTable($form);
initEditableHandsOnTable($form);
onSubmitExtractTable($form);
// Cancel button
$form.find(".cancel-new").click(function () {
$form.remove();
toggleResultEditButtons(true);
});
toggleResultEditButtons(false);
});
$("#new-result-table").on("ajax:error", function(e, xhr, status, error) {
//TODO: Add error handling
});
// Apply ajax callback to form
function formAjaxResultTable($form) {
$form.on("ajax:success", function(e, data) {
$form.after(data.html);
$result = $(this).next();
initFormSubmitLinks($result);
$(this).remove();
applyEditResultTableCallback();
initHandsOnTables($result);
toggleResultEditButtons(true);
initResultCommentTabAjax();
expandResult($result);
initHandsOnTables($result);
});
$form.on("ajax:error", function(e, xhr, status, error) {
var data = xhr.responseJSON;
$form.render_form_errors("result", data);
});
}
applyEditResultTableCallback();

View file

@ -0,0 +1,75 @@
// New result text behaviour
$("#new-result-text").on("ajax:success", function(e, data) {
var $form = $(data.html);
$("#results").prepend($form);
formAjaxResultText($form);
// Cancel button
$form.find(".cancel-new").click(function () {
$form.remove();
toggleResultEditButtons(true);
});
toggleResultEditButtons(false);
});
$("#new-result-text").on("ajax:error", function(e, xhr, status, error) {
//TODO: Add error handling
});
// Edit result text button behaviour
function applyEditResultTextCallback() {
$(".edit-result-text").on("ajax:success", function(e, data) {
var $result = $(this).closest(".result");
var $form = $(data.html);
var $prevResult = $result;
$result.after($form);
$result.remove();
formAjaxResultText($form);
// Cancel button
$form.find(".cancel-edit").click(function () {
$form.after($prevResult);
$form.remove();
applyEditResultTextCallback();
toggleResultEditButtons(true);
});
toggleResultEditButtons(false);
});
$(".edit-result-text").on("ajax:error", function(e, xhr, status, error) {
//TODO: Add error handling
});
}
// Apply ajax callback to form
function formAjaxResultText($form) {
$form.on("ajax:success", function(e, data) {
$form.after(data.html);
var newResult = $form.next();
initFormSubmitLinks(newResult);
$(this).remove();
applyEditResultTextCallback();
toggleResultEditButtons(true);
initResultCommentTabAjax();
expandResult(newResult);
});
$form.on("ajax:error", function(e, xhr, status, error) {
var data = xhr.responseJSON;
$form.render_form_errors("result", data);
if (data["result_text.text"]) {
var $el = $form.find("textarea[name=result\\[result_text_attributes\\]\\[text\\]]");
$el.closest(".form-group").addClass("has-error");
$el.parent().append("<span class='help-block'>" + data["result_text.text"] + "</span>");
}
});
}
applyEditResultTextCallback();

View file

@ -0,0 +1,669 @@
var rowsSelected = [];
// Tells whether we're currently viewing or editing table
var currentMode = "viewMode";
// Tells what action will execute by pressing on save button (update/create)
var saveAction = "update";
var selectedSample;
table = $("#samples").DataTable({
order: [[2, "desc"]],
dom: "RB<'row'<'col-sm-9 toolbar'l><'col-sm-3'f>>tpi",
stateSave: true,
buttons: [{
extend: "colvis",
text: function () {
return '<span class="glyphicon glyphicon-option-horizontal"></span> ' +
'<span class="hidden-xs">' +
I18n.t('samples.column_visibility') +
'</span>';
},
columns: ":gt(2)"
}],
processing: true,
serverSide: true,
ajax: {
url: $("#samples").data("source"),
global: false,
type: "POST"
},
colReorder: {
fixedColumnsLeft: 1000000 // Disable reordering
},
columnDefs: [{
targets: 0,
searchable: false,
orderable: false,
className: "dt-body-center",
sWidth: "1%",
render: function (data, type, full, meta){
return "<input type='checkbox'>";
}
}, {
targets: 1,
searchable: false,
orderable: true,
sWidth: "1%"
}],
rowCallback: function(row, data, dataIndex){
// Get row ID
var rowId = data["DT_RowId"];
// If row ID is in the list of selected row IDs
if($.inArray(rowId, rowsSelected) !== -1){
$(row).find('input[type="checkbox"]').prop('checked', true);
$(row).addClass('selected');
}
},
columns: (function() {
var numOfColumns = $("#samples").data("num-columns");
var columns = [];
for (var i = 0; i < numOfColumns; i++) {
var visible = (i <= 6);
columns.push({
data: i + "",
defaultContent: "",
visible: visible
});
}
return columns;
})(),
fnDrawCallback: function(settings, json) {
animateSpinner(this, false);
changeToViewMode();
},
stateLoadParams: function(settings, data) {
// Check if URL parameters contain the column to show, if so, display it
// no matter what
if (getParam("new_col") !== null &&
data.columns.length === $("#samples").data("num-columns") - 1) {
// # of columns grew to +1, we need to add new column to data!
var i = data.columns.length + "";
data.columns.push({
data: i,
defaultContent: "",
visible: true
});
}
},
stateSaveCallback: function (settings, data) {
// Set a cookie with the table state using the organization id
localStorage.setItem('datatables_state/' + $("#samples").attr("data-organization-id"), JSON.stringify(data));
},
stateLoadCallback: function (settings) {
// Load the table state for the current organization
var state = localStorage.getItem('datatables_state/' + $("#samples").attr("data-organization-id"));
if (state !== null) {
return JSON.parse(state);
}
return null;
},
preDrawCallback: function(settings) {
animateSpinner(this);
}
});
table.buttons().container().appendTo('#datatables-buttons');
// Append button to inner toolbar in table
$("div.toolbarButtons").appendTo("div.toolbar");
$("div.toolbarButtons").show();
$(".delete_samples_submit").click(function () {
animateLoading();
});
$("#assignSamples, #unassignSamples").click(function () {
animateLoading();
});
// Updates "Select all" control in a data table
function updateDataTableSelectAllCtrl(table){
var $table = table.table().node();
var $chkbox_all = $('tbody input[type="checkbox"]', $table);
var $chkbox_checked = $('tbody input[type="checkbox"]:checked', $table);
var chkbox_select_all = $('thead input[name="select_all"]', $table).get(0);
// If none of the checkboxes are checked
if($chkbox_checked.length === 0){
chkbox_select_all.checked = false;
if('indeterminate' in chkbox_select_all){
chkbox_select_all.indeterminate = false;
}
// If all of the checkboxes are checked
} else if ($chkbox_checked.length === $chkbox_all.length){
chkbox_select_all.checked = true;
if('indeterminate' in chkbox_select_all){
chkbox_select_all.indeterminate = false;
}
// If some of the checkboxes are checked
} else {
chkbox_select_all.checked = true;
if('indeterminate' in chkbox_select_all){
chkbox_select_all.indeterminate = true;
}
}
}
// Handle click on table cells with checkboxes
$('#samples').on('click', 'tbody td, thead th:first-child', function(e){
$(this).parent().find('input[type="checkbox"]').trigger('click');
});
// Handle clicks on checkbox
$("#samples tbody").on("click", "input[type='checkbox']", function(e){
if (currentMode != "viewMode")
return false;
// Get row ID
var $row = $(this).closest("tr");
var data = table.row($row).data();
var rowId = data["DT_RowId"];
// Determine whether row ID is in the list of selected row IDs
var index = $.inArray(rowId, rowsSelected);
// If checkbox is checked and row ID is not in list of selected row IDs
if(this.checked && index === -1){
rowsSelected.push(rowId);
// Otherwise, if checkbox is not checked and row ID is in list of selected row IDs
} else if (!this.checked && index !== -1){
rowsSelected.splice(index, 1);
}
if(this.checked){
$row.addClass('selected');
} else {
$row.removeClass('selected');
}
updateDataTableSelectAllCtrl(table);
e.stopPropagation();
updateButtons();
});
// Handle click on "Select all" control
$('#samples thead input[name="select_all"]').on('click', function(e){
if(this.checked){
$('#samples tbody input[type="checkbox"]:not(:checked)').trigger('click');
} else {
$('#samples tbody input[type="checkbox"]:checked').trigger('click');
}
// Prevent click event from propagating to parent
e.stopPropagation();
});
// Append selected samples to form
$("form#form-samples").submit(function(e){
var form = this;
if (currentMode == "viewMode")
appendSamplesIdToForm(form);
});
// Append selected samples and headers form
$("form#form-export").submit(function(e){
var form = this;
if (currentMode == "viewMode") {
// Remove all hidden fields
$("#form-export").find("input[name=sample_ids\\[\\]]").remove();
$("#form-export").find("input[name=header_ids\\[\\]]").remove();
// Append samples
appendSamplesIdToForm(form);
// Append visible column information
$("table#samples thead tr").children("th").each(function(i) {
var th = $(this);
var val;
if ($(th).attr("id") == "sample-name")
val = -1;
else if ($(th).attr("id") == "sample-type")
val = -2;
else if ($(th).attr("id") == "sample-group")
val = -3;
else if ($(th).attr("id") == "added-by")
val = -4;
else if ($(th).attr("id") == "added-on")
val = -5;
else if ($(th).hasClass("custom-field"))
val = th.attr("id");
if (val)
$(form).append(
$('<input>')
.attr('type', 'hidden')
.attr('name', 'header_ids[]')
.val(val)
);
});
}
});
function appendSamplesIdToForm(form) {
$.each(rowsSelected, function(index, rowId){
$(form).append(
$('<input>')
.attr('type', 'hidden')
.attr('name', 'sample_ids[]')
.val(rowId)
);
});
}
// Handle table draw event
table.on('draw', function(){
updateDataTableSelectAllCtrl(table);
});
// Edit sample
function onClickEdit() {
if (rowsSelected.length != 1) return;
var row = table.row("#" + rowsSelected[0]);
var node = row.node();
var rowData = row.data();
$(node).find("td input").trigger("click");
selectedSample = node;
clearAllErrors();
changeToEditMode();
saveAction = "update";
$.ajax({
url: rowData["sampleInfoUrl"],
type: "GET",
dataType: "json",
success: function (data) {
// Show save and cancel buttons in first two columns
$(node).children("td").eq(0).html($("#saveSample").clone());
$(node).children("td").eq(1).html($("#cancelSave").clone());
// Sample name column
var colIndex = getColumnIndex("#sample-name");
if (colIndex) {
$(node).children("td").eq(colIndex).html(changeToInputField("sample", "name", data["sample"]["name"]));
}
// Sample type column
var colIndex = getColumnIndex("#sample-type");
if (colIndex) {
var selectType = createSampleTypeSelect(data["sample_types"], data["sample"]["sample_type"]);
$(node).children("td").eq(colIndex).html(selectType);
$("select[name=sample_type_id]").selectpicker();
}
// Sample group column
var colIndex = getColumnIndex("#sample-group");
if (colIndex) {
var selectGroup = createSampleGroupSelect(data["sample_groups"], data["sample"]["sample_group"]);
$(node).children("td").eq(colIndex).html(selectGroup);
$("select[name=sample_group_id]").selectpicker();
}
// Take care of custom fields
var cfields = data["sample"]["custom_fields"];
$(node).children("td").each(function(i) {
var td = $(this);
var rawIndex = table.column.index("fromVisible", i);
var colHeader = table.column(rawIndex).header();
if ($(colHeader).hasClass("custom-field")) {
// Check if custom field on this sample exists
var cf = cfields[$(colHeader).attr("id")];
if (cf)
td.html(changeToInputField("sample_custom_fields", cf["sample_custom_field_id"], cf["value"]));
else
td.html(changeToInputField("custom_fields", $(colHeader).attr("id"), ""));
}
});
},
error: function (e, data, status, xhr) {
if (e.status == 403) {
showAlertMessage(I18n.t("samples.js.permission_error"));
changeToViewMode();
}
}
});
}
// Save sample
function onClickSave() {
if (saveAction == "update") {
var row = table.row(selectedSample);
var node = row.node();
var rowData = row.data();
} else if (saveAction == "create")
var node = selectedSample;
// First fetch all the data in input fields
data = {
sample_id: $(selectedSample).attr("id"),
sample: {},
custom_fields: {}, // These fields are not currently bound to this sample
sample_custom_fields: {} // These fields are already in database (linked to this sample)
};
// Direct sample attributes
// Sample name
$(node).find("td input[data-object = sample]").each(function() {
data["sample"][$(this).attr("name")] = $(this).val();
});
// Sample type
$(node).find("td select[name = sample_type_id]").each(function() {
data["sample"]["sample_type_id"] = $(this).val();
});
// Sample group
$(node).find("td select[name = sample_group_id]").each(function() {
data["sample"]["sample_group_id"] = $(this).val();
});
// Custom fields (new fields)
$(node).find("td input[data-object = custom_fields]").each(function () {
// Send data only and only if string is not empty
if ($(this).val().trim()) {
data["custom_fields"][$(this).attr("name")] = $(this).val();
}
});
// Sample custom fields (existent fields)
$(node).find("td input[data-object = sample_custom_fields]").each(function () {
data["sample_custom_fields"][$(this).attr("name")] = $(this).val();
});
var url = (saveAction == "update" ? rowData["sampleUpdateUrl"] : $("table#samples").data("create-sample"))
var type = (saveAction == "update" ? "PUT" : "POST")
$.ajax({
url: url,
type: type,
dataType: "json",
data: data,
success: function (data) {
onClickCancel();
},
error: function (e, eData, status, xhr) {
var data = e.responseJSON;
clearAllErrors();
if (e.status == 404) {
showAlertMessage(I18n.t("samples.js.not_found_error"));
changeToViewMode();
}
else if (e.status == 403) {
showAlertMessage(I18n.t("samples.js.permission_error"));
changeToViewMode();
}
else if (e.status == 400) {
if (data["init_fields"]) {
var init_fields = data["init_fields"];
// Validate sample name
if (init_fields["name"]) {
var input = $(selectedSample).find("input[name=name]");
if (input) {
input.closest(".form-group").addClass("has-error");
input.parent().append("<span class='help-block'>" + init_fields["name"] + "<br /></span>");
}
}
};
// Validate custom fields
$.each(data["custom_fields"] || [], function(key, val) {
$.each(val, function(key, val) {
var input = $(selectedSample).find("input[name=" + key + "]");
if (input) {
input.closest(".form-group").addClass("has-error");
input.parent().append("<span class='help-block'>" + val["value"][0] + "<br /></span>");
}
});
});
// Validate sample custom fields
$.each(data["sample_custom_fields"] || [], function(key, val) {
$.each(val, function(key, val) {
var input = $(selectedSample).find("input[name=" + key + "]");
if (input) {
input.closest(".form-group").addClass("has-error");
input.parent().append("<span class='help-block'>" + val["value"][0] + "<br /></span>");
}
});
});
}
}
});
}
// Enable/disable edit button
function updateButtons() {
if (rowsSelected.length == 1) {
$("#editSample").prop("disabled", false);
$("#deleteSamplesButton").prop("disabled", false);
$("#exportSamplesButton").removeAttr("disabled");
$("#exportSamplesButton").on("click", function() { $('#form-export').submit(); });
$("#assignSamples").prop("disabled", false);
$("#unassignSamples").prop("disabled", false);
}
else if (rowsSelected.length === 0) {
$("#editSample").prop("disabled", true);
$("#deleteSamplesButton").prop("disabled", true);
$("#exportSamplesButton").attr("disabled", "disabled");
$("#exportSamplesButton").off("click");
$("#assignSamples").prop("disabled", true);
$("#unassignSamples").prop("disabled", true);
}
else {
$("#editSample").prop("disabled", true);
$("#deleteSamplesButton").prop("disabled", false);
$("#exportSamplesButton").removeAttr("disabled");
$("#exportSamplesButton").on("click", function() { $('#form-export').submit(); });
$("#assignSamples").prop("disabled", false);
$("#unassignSamples").prop("disabled", false);
}
}
// Clear all has-error tags
function clearAllErrors() {
// Remove any validation errors
$(selectedSample).find(".has-error").each(function() {
$(this).removeClass("has-error");
$(this).find("span").remove();
});
// Remove any alerts
$("#alert-container").find("div").remove();
}
// Restore previous table
function onClickCancel() {
table.ajax.reload();
changeToViewMode();
updateButtons();
}
function onClickAddSample() {
changeToEditMode();
saveAction = "create";
$.ajax({
url: $("table#samples").data("new-sample"),
type: "GET",
dataType: "json",
success: function (data) {
var tr = document.createElement("tr")
$("table#samples thead tr").children("th").each(function(i) {
var th = $(this);
if ($(th).attr("id") == "checkbox") {
var td = createTdElement("");
$(td).html($("#saveSample").clone());
tr.appendChild(td);
}
else if ($(th).attr("id") == "assigned") {
var td = createTdElement("");
$(td).html($("#cancelSave").clone());
tr.appendChild(td);
}
else if ($(th).attr("id") == "sample-name") {
var input = changeToInputField("sample", "name", "");
tr.appendChild(createTdElement(input));
}
else if ($(th).attr("id") == "sample-type") {
var colIndex = getColumnIndex("#sample-type")
if (colIndex) {
var selectType = createSampleTypeSelect(data["sample_types"], -1);
var td = createTdElement("");
td.appendChild(selectType[0]);
tr.appendChild(td);
}
}
else if ($(th).attr("id") == "sample-group") {
var colIndex = getColumnIndex("#sample-group")
if (colIndex) {
var selectGroup = createSampleGroupSelect(data["sample_groups"], -1);
var td = createTdElement("");
td.appendChild(selectGroup[0]);
tr.appendChild(td);
}
}
else if ($(th).hasClass("custom-field")) {
var input = changeToInputField("custom_fields", th.attr("id"), "");
tr.appendChild(createTdElement(input));
}
else {
// Column we don't care for, just add empty td
tr.appendChild(createTdElement(""));
}
});
$("table#samples").prepend(tr);
selectedSample = tr;
// Init dropdown with icons
$("select[name=sample_group_id]").selectpicker();
$("select[name=sample_type_id]").selectpicker();
},
error: function (e, eData, status, xhr) {
if (e.status == 403)
showAlertMessage(I18n.t("samples.js.permission_error"));
changeToViewMode();
}
});
}
// Handle enter key
$(document).off("keypress").keypress(function(event) {
var keycode = (event.keyCode ? event.keyCode : event.which);
if(currentMode == "editMode" && keycode == '13'){
$("#saveSample").click();
return false;
}
});
// Helper functions
function getColumnIndex(id) {
if (id < 0) return false;
return table.column(id).index("visible");
}
// Takes object and surrounds it with input
function changeToInputField(object, name, value) {
return "<div class='form-group'><input class='form-control' data-object='" + object + "' name='" + name + "' value='" + value + "'></input></div>";
}
// Return td element with content
function createTdElement(content) {
var td = document.createElement("td");
td.innerHTML = content;
return td;
}
/**
* Creates select dropdown for sample type
* @param data List of sample types
* @param selected Selected sample type id
*/
function createSampleTypeSelect(data, selected) {
var $selectType = $("<select></select>").attr("name", "sample_type_id").addClass("show-tick");
var $option = $("<option></option>").attr("value", -1).text(I18n.t("samples.table.no_type"))
$selectType.append($option);
$.each(data, function(i, val) {
var $option = $("<option></option>").attr("value", val["id"]).text(val["name"]);
$selectType.append($option);
});
$selectType.val(selected);
return $selectType;
}
/**
* Creates select dropdown for sample group
* @param data List of sample groups
* @param selected Selected sample group id
*/
function createSampleGroupSelect(data, selected) {
var $selectGroup = $("<select></select>").attr("name", "sample_group_id").addClass("show-tick");
var $span = $("<span></span>").addClass("glyphicon glyphicon-asterisk");
var $option = $("<option></option>").attr("value", -1).text(I18n.t("samples.table.no_group"))
.attr("data-icon", "glyphicon glyphicon-asterisk");
$selectGroup.append($option);
$.each(data, function(i, val) {
var $span = $("<span></span>").addClass("glyphicon glyphicon-asterisk").css("color", val["color"]);
var $option = $("<option></option>").attr("value", val["id"]).text(val["name"])
.attr("data-content", $span.prop("outerHTML") + " " + val["name"]);
$selectGroup.append($option);
});
$selectGroup.val(selected);
return $selectGroup;
}
function changeToViewMode() {
currentMode = "viewMode";
// $("#saveCancel").hide();
$(".editAdd").removeClass("disabled");
$("#addNewColumn").removeClass("disabled");
$("#exportSamples").removeClass("disabled");
// Table specific stuff
table.button(0).enable(true);
}
function changeToEditMode() {
currentMode = "editMode";
// $("#saveCancel").show();
$(".editAdd").addClass("disabled");
$("#addNewColumn").addClass("disabled");
$("#exportSamples").addClass("disabled");
// Table specific stuff
table.button(0).enable(false);
}
// Shows alert and changes
function showAlertMessage(msg) {
$("#alert-container").append("<div class='alert alert-danger'> <strong>Error!</strong> " + msg + "</div>");
}

View file

@ -0,0 +1,2 @@
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.

View file

@ -0,0 +1,2 @@
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.

View file

@ -0,0 +1,170 @@
//= require datatables
// Create custom field ajax
$("#modal-create-custom-field").on("show.bs.modal", function(event) {
// Clear input when modal is opened
input = $(this).find("input#name-input");
input.val("");
input.closest(".form-group").removeClass("has-error");
input.closest(".form-group").find(".help-block").remove();
});
$("#modal-create-custom-field").on("shown.bs.modal", function(event) {
$(this).find("input#name-input").focus();
});
$("form#new_custom_field").on("ajax:success", function(ev, data, status) {
$("#modal-create-custom-field").modal("hide");
// Reload page with URL parameter of newly created field
window.location.href = addParam(window.location.href, "new_col");
});
$("form#new_custom_field").on("ajax:error", function(e, data, status, xhr) {
input = $(this).find("#name-input");
input.closest(".form-group").find(".help-block").remove();
input.closest(".form-group").addClass("has-error");
$.each(data.responseJSON, function(i, val) {
input.parent().append("<span class='help-block'>" + val[0].charAt(0).toUpperCase() + val[0].slice(1) +"<br /></span>");
});
});
// Create sample type ajax
$("#modal-create-sample-type").on("show.bs.modal", function(event) {
// Clear input when modal is opened
input = $(this).find("input#name-input");
input.val("");
input.closest(".form-group").removeClass("has-error");
input.closest(".form-group").find(".help-block").remove();
});
$("#modal-create-sample-type").on("shown.bs.modal", function(event) {
$(this).find("input#name-input").focus();
});
$("form#new_sample_type").on("ajax:success", function(ev, data, status) {
$("#modal-create-sample-type").modal("hide");
});
$("form#new_sample_type").on("ajax:error", function(e, data, status, xhr) {
input = $(this).find("#name-input");
input.closest(".form-group").find(".help-block").remove();
input.closest(".form-group").addClass("has-error");
$.each(data.responseJSON, function(i, val) {
input.parent().append("<span class='help-block'>" + val[0].charAt(0).toUpperCase() + val[0].slice(1) +"<br /></span>");
});
});
// Create sample group ajax
$("#modal-create-sample-group").on("show.bs.modal", function(event) {
// Clear input when modal is opened
input = $(this).find("input#name-input");
input.val("");
input.closest(".form-group").removeClass("has-error");
input.closest(".form-group").find(".help-block").remove();
});
$("#modal-create-sample-group").on("shown.bs.modal", function(event) {
$(this).find("input#name-input").focus();
});
$("form#new_sample_group").on("ajax:success", function(ev, data, status) {
$("#modal-create-sample-group").modal("hide");
});
$("form#new_sample_group").on("ajax:error", function(e, data, status, xhr) {
input = $(this).find("#name-input");
input.closest(".form-group").find(".help-block").remove();
input.closest(".form-group").addClass("has-error");
$.each(data.responseJSON, function(i, val) {
input.parent().append("<span class='help-block'>" + val[0].charAt(0).toUpperCase() + val[0].slice(1) +"<br /></span>");
});
});
// Create import samples ajax
$("#modal-import-samples").on("show.bs.modal", function(event) {
formGroup = $(this).find(".form-group");
formGroup.removeClass("has-error");
formGroup.find(".help-block").remove();
});
$("form#form-samples-file")
.on("ajax:success", function(ev, data, status) {
$("#modal-parse-samples").html(data.html);
$("#modal-import-samples").modal("hide");
$("#modal-parse-samples").modal("show");
})
.on("ajax:error", function(ev, data, status) {
$(this).find(".form-group").addClass("has-error");
$(this).find(".form-group").find(".help-block").remove();
$(this).find(".form-group").append("<span class='help-block'>" + data.responseJSON.message + "</span>");
});
function initTutorial() {
var currentStep = parseInt(Cookies.get('current_tutorial_step'));
if (currentStep == 8)
currentStep++;
if (showTutorial() && currentStep == 9 || currentStep == 10) {
var samplesTutorial =$("#samples-toolbar").attr("data-samples-step-text");
var breadcrumbsTutorial = $("#samples-toolbar").attr("data-breadcrumbs-step-text");
introJs()
.setOptions({
steps: [
{
element: document.getElementById("samples-toolbar"),
intro: samplesTutorial,
tooltipClass: 'custom'
},
{
element: document.getElementById("secondary-menu"),
intro: breadcrumbsTutorial,
tooltipClass: 'custom disabled-next'
}
],
overlayOpacity: '0.1',
nextLabel: 'Next',
doneLabel: 'End tutorial',
skipLabel: 'End tutorial',
showBullets: false,
showStepNumbers: false,
disableInteraction: true
})
.goToStep(currentStep - 8)
.onafterchange(function (tarEl) {
Cookies.set('current_tutorial_step', this._currentStep + 9);
// Disable interaction only for first step (dirty hack)
if (this._currentStep)
$('.introjs-disableInteraction').remove();
})
.start();
// Destroy first-time tutorial cookies when skip tutorial
// or end tutorial is clicked
$(".introjs-skipbutton").each(function (){
$(this).click(function (){
Cookies.remove('tutorial_data');
Cookies.remove('current_tutorial_step');
});
});
}
}
function showTutorial() {
var tutorialData;
if (Cookies.get('tutorial_data'))
tutorialData = JSON.parse(Cookies.get('tutorial_data'));
else
return false;
var tutorialModuleId = tutorialData[0].qpcr_module;
var currentModuleId = $("#samples-toolbar").attr("data-module-id");
return tutorialModuleId == currentModuleId;
}
// Initialize first-time tutorial
initTutorial();

View file

@ -0,0 +1,41 @@
var previousIndex;
var disabledOptions;
$("select").focus(function() {
previousIndex = $(this)[0].selectedIndex;
}).change(function () {
var currSelect = $(this);
var currIndex = $(currSelect)[0].selectedIndex;
$("select").each(function() {
if (currSelect !== $(this) && currIndex > 0) {
$(this).children().eq(currIndex).attr("disabled", "disabled");
}
$(this).children().eq(previousIndex).removeAttr("disabled");
});
previousIndex = currIndex;
});
// Create import samples ajax
$("form#form-import")
.submit(function(e) {
disabledOptions = $("option[disabled='disabled']");
disabledOptions.removeAttr("disabled");
})
.on("ajax:success", function(ev, data, status) {
// Simply reload page to show flash and updated samples list
location.reload();
})
.on("ajax:error", function(ev, data, status) {
if (_.isUndefined(data.responseJSON.html)) {
// Simply reload page to show flash
location.reload();
} else {
// Re-disable options
disabledOptions.attr("disabled", "disabled");
// Populate the errors container
$("#import-errors-container").html(data.responseJSON.html);
}
});

View file

@ -0,0 +1,221 @@
/**
* The functions here are global because they need to be
* accesed from outside (in reports view).
*/
var STORAGE_TREE_KEY = "scinote-sidebar-tree-collapsed-ids";
var STORAGE_TOGGLE_KEY = "scinote-sidebar-toggled";
var SCREEN_SIZE_LARGE = 928;
/**
* Get all collapsed sidebar elements.
* @return An array of sidebar element IDs.
*/
function sessionGetCollapsedSidebarElements() {
var val = sessionStorage.getItem(STORAGE_TREE_KEY);
if (val === null) {
val = "[]";
sessionStorage.setItem(STORAGE_TREE_KEY, val);
}
return JSON.parse(val);
}
/**
* Collapse a specified element in the sidebar.
* @param id - The collapsed element's ID.
*/
function sessionCollapseSidebarElement(id) {
var ids = sessionGetCollapsedSidebarElements();
if (_.indexOf(ids, id) === -1) {
ids.push(id);
sessionStorage.setItem(STORAGE_TREE_KEY, JSON.stringify(ids));
}
}
/**
* Expand a specified element in the sidebar.
* @param id - The expanded element's ID.
*/
function sessionExpandSidebarElement(id) {
var ids = sessionGetCollapsedSidebarElements();
var index = _.indexOf(ids, id);
if (index !== -1) {
ids.splice(index, 1);
sessionStorage.setItem(STORAGE_TREE_KEY, JSON.stringify(ids));
}
}
/**
* Get the session stored toggled boolean or null value if
* sidebar toggle state was not changed by user. It allow for
* automatic toggling for small devices.
*
* @return True if sidebar is toggled; false otherwise.
*/
function sessionIsSidebarToggled() {
var val = sessionStorage.getItem(STORAGE_TOGGLE_KEY);
if (val === null) {
return null;
}
return val === "toggled";
}
/**
* Store the sidebar toggled boolean to session storage.
*/
function sessionToggleSidebar() {
if (sessionIsSidebarToggled()) {
sessionStorage.setItem(STORAGE_TOGGLE_KEY, "not_toggled");
} else {
sessionStorage.setItem(STORAGE_TOGGLE_KEY, "toggled");
}
}
/**
* Setup the sidebar collapsing & expanding functionality.
*/
function setupSidebarTree() {
function toggleLi(el, collapse, animate) {
var children = el
.find(" > ul > li");
if (collapse) {
if (animate) {
children.hide("fast");
} else {
children.hide();
}
el
.find(" > span i")
.attr("title", "Expand this branch")
.removeClass("expanded");
} else {
if (animate) {
children.show("fast");
} else {
children.show();
}
el
.find(" > span i")
.attr("title", "Collapse this branch")
.addClass("expanded");
}
}
// Add triangle icons and titles to every parent node
$(".tree li:has(ul)")
.addClass("parent_li")
.find(" > span i")
.attr("title", "Collapse this branch");
$(".tree li.parent_li ")
.find("> span i")
.addClass("glyphicon glyphicon-triangle-right expanded");
// Add IDs to all parent <lis>
var i = 0;
_.each($(".tree li.parent_li"), function(el) {
$(el).attr("data-toggle-id", i++);
});
// Collapse session-stored elements
var collapsedIds = sessionGetCollapsedSidebarElements();
_.each(collapsedIds, function(id) {
var li = $(".tree li.parent_li[data-toggle-id='" + id + "']");
if (li.find("li.active").length === 0) {
// Only collapse element if it's descendants don't contain the currently
// active element
toggleLi(li,
true,
false);
} else {
// Else, set the element as expanded
sessionExpandSidebarElement(id);
}
});
// Add onclick callback to every triangle icon
$(".tree li.parent_li ")
.find("> span i")
.on("click", function (e) {
var el = $(this)
.parent("span")
.parent("li.parent_li");
if (el.find(" > ul > li").is(":visible")) {
toggleLi(el, true, true);
sessionCollapseSidebarElement(el.data("toggle-id"));
} else {
toggleLi(el, false, true);
sessionExpandSidebarElement(el.data("toggle-id"));
}
e.stopPropagation();
return false;
});
}
/**
* Initialize the show/hide toggling of sidebar.
*/
function initializeSidebarToggle() {
var wrapper = $("#wrapper");
var toggled = sessionIsSidebarToggled();
if (toggled || toggled === null && $(window).width() < SCREEN_SIZE_LARGE) {
wrapper.addClass("no-animation");
wrapper.addClass("toggled");
// Cause reflow of the wrapper element
wrapper[0].offsetHeight;
wrapper.removeClass("no-animation");
$(".navbar-secondary").addClass("navbar-without-sidebar");
}
$("#toggle-sidebar-link").on("click", function() {
$("#wrapper").toggleClass("toggled");
sessionToggleSidebar();
$(".navbar-secondary").toggleClass("navbar-without-sidebar", sessionIsSidebarToggled());
return false;
});
}
// Resize the sidebar to accomodate to the page size
function resizeSidebarContents() {
var wrapper = $("#wrapper");
var tree = $("#sidebar-wrapper .tree");
var toggled = sessionIsSidebarToggled();
if (tree.length && tree.length == 1) {
tree.css(
"height",
($(window).height() - tree.position().top - 50) + "px"
);
}
// Automatic toggling of sidebar for smaller devices
if (toggled === null) {
if ($(window).width() < SCREEN_SIZE_LARGE) {
wrapper.addClass("toggled");
} else {
wrapper.removeClass("toggled");
}
}
}
(function () {
// Initialize click listeners
setupSidebarTree();
initializeSidebarToggle();
// Actually display wrapper, which is, up to now,
// hidden
$("#wrapper").show();
// Resize the sidebar automatically
resizeSidebarContents();
// Bind onto window resize function
$(window).resize(function() {
resizeSidebarContents();
});
}());

View file

View file

@ -0,0 +1,114 @@
// Define AJAX methods for handling errors on forms
$.fn.render_form_errors = function(model_name, errors, clear) {
if (clear || clear === undefined) {
this.clear_form_errors();
}
$(this).render_form_errors_no_clear(model_name, errors, false);
};
$.fn.render_form_errors_input_group = function(model_name, errors) {
this.clear_form_errors();
$(this).render_form_errors_no_clear(model_name, errors, true);
};
$.fn.render_form_errors_no_clear = function(model_name, errors, input_group) {
var form = $(this);
$.each(errors, function(field, messages) {
input = $(_.filter(form.find('input, select, textarea'), function(el) {
var name = $(el).attr('name');
if (name) {
return name.match(new RegExp(model_name + '\\[' + field + '\\(?'));
}
return false;
}));
input.closest('.form-group').addClass('has-error');
var error_text = '<span class="help-block">';
error_text += (_.map(messages, function(m) {
return m.charAt(0).toUpperCase() + m.slice(1);
})).join('<br />');
error_text += '</span>';
if (input_group) {
input.closest('.form-group').append(error_text);
} else {
input.parent().append(error_text);
}
});
};
$.fn.clear_form_errors = function() {
$(this).find('.form-group').removeClass('has-error');
$(this).find('span.help-block').remove();
};
$.fn.clear_form_fields = function() {
$(this).find("input")
.not("button")
.not('input[type="submit"], input[type="reset"], input[type="hidden"]')
.not('input[type="radio"]') // Leave out radios as this messes up Bootstrap btn-groups
.val('')
.removeAttr('checked')
.removeAttr('selected');
};
// Add JavaScript client-side upload file size checking
// Callback function can be provided to be called
// any time at least one file size is too large
$.fn.add_upload_file_size_check = function(callback) {
var form = $(this);
if (form.length && form.length > 0) {
form.submit(function (ev) {
var fileInputs = $(this).find("input[type='file']");
if (fileInputs.length && fileInputs.length > 0) {
var cntr = 0;
_.each(fileInputs, function(fileInput) {
if (typeof (fileInput.files) != "undefined") {
var size = parseInt(fileInput.files[0].size);
if (size > 52428800) {
cntr++;
var input = $(fileInput);
var existingError = input.parent().find("[data-error='file-size']");
if (!(existingError.length && existingError.length > 0)) {
input.closest('.form-group').addClass('has-error');
input.parent().append(
"<span class='help-block' data-error='file-size'>Must be less than 50 MB</span>"
);
}
}
}
});
if (cntr > 0) {
// Don't submit form
ev.preventDefault();
ev.stopPropagation();
if (callback) {
callback();
}
return false;
}
}
});
}
};
// If any of tabs has errors, add has-error class to
// parent tab navigation link
function tabsPropagateErrorClass(parent) {
var contents = parent.find("div.tab-pane");
_.each(contents, function(tab) {
var $tab = $(tab);
var errorFields = $tab.find(".has-error");
if (errorFields.length > 0) {
var id = $tab.attr("id");
var navLink = parent.find("a[href='#" + id + "'][data-toggle='tab']");
if (navLink.parent().length > 0) {
navLink.parent().addClass("has-error");
}
}
});
$(".nav-tabs .has-error:first > a", parent).tab("show");
}

View file

@ -0,0 +1,39 @@
// Add parameter to provided specified URL
function addParam(url, param, value) {
var a = document.createElement('a'), regex = /(?:\?|&amp;|&)+([^=]+)(?:=([^&]*))*/gi;
var params = {}, match, str = []; a.href = url;
while (match = regex.exec(a.search)) {
if (encodeURIComponent(param) != match[1]) {
str.push(match[1] + (match[2] ? "=" + match[2] : ""));
}
}
str.push(encodeURIComponent(param) + (value ? "=" + encodeURIComponent(value) : ""));
a.search = str.join("&");
return a.href;
}
// Get URL parameter value
function getParam(param, asArray) {
return document.location.search.substring(1).split('&').reduce(function(p,c) {
var parts = c.split('=', 2).map(function(param) { return decodeURIComponent(param); });
if(parts.length == 0 || parts[0] != param) return (p instanceof Array) && !asArray ? null : p;
return asArray ? p.concat(parts.concat(true)[1]) : parts.concat(true)[1];
}, []);
}
// bootstrap-select should handle detection automatically but when
// rails version it does not detect selects with selectpicker class.
$(document).ready(function () {
$(".selectpicker").selectpicker();
initFormSubmitLinks();
$("#hide-alert").click(function(ev) {
$(this).closest("div.alert").addClass("alert-hidden");
$("#content-wrapper").addClass("alert-hidden");
$("#content-wrapper").removeClass("alert-shown");
ev.preventDefault();
ev.stopPropagation();
return false;
});
});

View file

@ -0,0 +1,2 @@
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.

View file

@ -0,0 +1,2 @@
// Place all the behaviors and hooks related to the matching controller here.
// All this logic will automatically be available in application.js.

View file

@ -0,0 +1,114 @@
/**
* Toggle the view/edit form visibility.
* @param form - The jQuery form selector.
* @param edit - True to set form to edit mode;
* false to set form to view mode.
*/
function toggleFormVisibility(form, edit) {
if (edit) {
form.find("[data-part='view']").hide();
form.find("[data-part='edit']").show();
form.find("[data-part='edit'] input:not([type='file']):not([type='submit']):first").focus();
} else {
form.find("[data-part='view']").show();
form.find("[data-part='edit'] input").blur();
form.find("[data-part='edit']").hide();
// Clear all errors on the parent form
form.clear_form_errors();
// Clear any neccesary fields
form.find("input[data-role='clear']").val("");
// Copy field data
var val = form.find("input[data-role='src']").val();
form.find("input[data-role='edit']").val(val);
}
}
var forms = $("form[data-for]");
// Add "edit form" listeners
forms
.find("[data-action='edit']").click(function() {
var form = $(this).closest("form");
// First, hide all form edits
_.each(forms, function(form) {
toggleFormVisibility($(form), false);
});
// Then, edit the current form
toggleFormVisibility(form, true);
});
// Add "cancel form" listeners
forms
.find("[data-action='cancel']").click(function() {
var form = $(this).closest("form");
// Hide the edit portion of the form
toggleFormVisibility(form, false);
});
// Add form submit listeners
forms
.on("ajax:success", function(ev, data, status) {
// Simply reload the page
location.reload();
})
.on("ajax:error", function(ev, data, status) {
// Render form errors
$(this).render_form_errors("user", data.responseJSON);
});
// Add upload file size checking
$("form[data-for='avatar']").add_upload_file_size_check();
// S3 direct uploading
function startFileUpload(ev, btn) {
var form = btn.form;
var $form = $(form);
var fileInput = $form.find("input[type=file]").get(0);
var url = "/avatar_signature.json";
$form.clear_form_errors();
animateSpinner($form);
directUpload(form, null, url, function (assetId) {
var file = fileInput.files[0];
fileInput.type = "hidden";
fileInput.name = fileInput.name.replace("[avatar]", "[avatar_file_name]");
fileInput.value = file.name;
$("#user_change_avatar").remove();
btn.onclick = null;
$(btn).click();
animateSpinner($form, false);
}, function (errors) {
$form.render_form_errors("user", errors);
var avatarError;
animateSpinner($form, false);
for (var c in errors) {
if (/^avatar/.test(c)) {
avatarError = errors[c];
break;
}
}
if (avatarError) {
var $el = $form.find("input[type=file]");
$form.clear_form_errors();
$el.closest(".form-group").addClass("has-error");
$el.parent().append("<span class='help-block'>" + avatarError + "</span>");
}
}, "avatar");
ev.preventDefault();
}

View file

@ -0,0 +1,243 @@
//= require datatables
//= require users/settings/organizations/add_user_modal
var usersDatatable = null;
// Initialize edit name modal window
function initEditName() {
var editNameModal = $("#organization-name-modal");
var editNameModalBody = editNameModal.find(".modal-body");
var editNameModalSubmitBtn = editNameModal.find("[data-action='submit']");
$(".name-link")
.on("ajax:success", function(ev, data, status) {
var nameLink = $(".name-refresh");
// Set modal body
editNameModalBody.html(data.html);
editNameModalBody.find("form")
.on("ajax:success", function(ev2, data2, status2) {
// Reload page
location.reload();
})
.on("ajax:error", function(ev2, data2, status2) {
// Display errors if needed
editNameModalBody
.find("form")
.render_form_errors("organization", data2.responseJSON);
});
// Show modal
editNameModal.modal("show");
})
.on("ajax:error", function(ev, data, status) {
// TODO
});
editNameModalSubmitBtn.on("click", function() {
// Submit the form inside the modal
editNameModalBody.find("form").submit();
});
editNameModal.on("hidden.bs.modal", function() {
editNameModalBody.find("form").off("ajax:success ajax:error");
editNameModalBody.html("");
});
}
// Initialize edit description modal window
function initEditDescription() {
var editDescriptionModal = $("#organization-description-modal");
var editDescriptionModalBody = editDescriptionModal.find(".modal-body");
var editDescriptionModalSubmitBtn = editDescriptionModal.find("[data-action='submit']");
$(".description-link")
.on("ajax:success", function(ev, data, status) {
var descriptionLink = $(".description-refresh");
// Set modal body
editDescriptionModalBody.html(data.html);
editDescriptionModalBody.find("form")
.on("ajax:success", function(ev2, data2, status2) {
// Update module's description in the tab
descriptionLink.html(data2.description_label);
// Close modal
editDescriptionModal.modal("hide");
})
.on("ajax:error", function(ev2, data2, status2) {
// Display errors if needed
editDescriptionModalBody
.find("form")
.render_form_errors("organization", data2.responseJSON);
});
// Show modal
editDescriptionModal.modal("show");
})
.on("ajax:error", function(ev, data, status) {
// TODO
});
editDescriptionModalSubmitBtn.on("click", function() {
// Submit the form inside the modal
editDescriptionModalBody.find("form").submit();
});
editDescriptionModal.on("hidden.bs.modal", function() {
editDescriptionModalBody.find("form").off("ajax:success ajax:error");
editDescriptionModalBody.html("");
});
}
// Initialize users DataTable
function initUsersTable() {
usersDatatable = $("#users-table").DataTable({
order: [[1, "asc"]],
dom: "RBfltpi",
stateSave: true,
buttons: [],
processing: true,
serverSide: true,
ajax: {
url: $("#users-table").data("source"),
type: "POST"
},
colReorder: {
fixedColumnsLeft: 1000000 // Disable reordering
},
columnDefs: [{
targets: [ 0, 1, 2 ],
searchable: true,
orderable: true
}, {
targets: [ 3, 4 ],
searchable: true,
orderable: true,
sWidth: "1%"
}, {
targets: 5,
searchable: false,
orderable: false,
sWidth: "1%"
}],
columns: [
{ data: "0" },
{ data: "1" },
{ data: "2" },
{ data: "3" },
{ data: "4" },
{ data: "5" }
]
});
}
function initUpdateRoles() {
// Bind on click event of various "set role" links in user
// dropdowns.
$(".users-datatable")
.on("click", "[data-action='submit-role']", function() {
var link = $(this);
var form = link
.closest(".dropdown-menu")
.find("form[data-id='update-role-form']");
var hiddenField = form.find("input[data-field='role']");
// Update the hidden field of the parent form
hiddenField.attr("value", link.attr("data-value"));
// Submit the parent form
form.submit();
});
$(document)
.on(
"ajax:success",
"[data-id='update-role-form']",
function (e, data, status, xhr) {
// Reload the whole table
usersDatatable.ajax.reload();
}
)
.on(
"ajax:error",
"[data-id='update-role-form']",
function (e, data, status, xhr) {
// TODO
}
);
}
function initRemoveUsers() {
// Bind the "remove user" button in users dropdown
$(document)
.on(
"ajax:success",
"[data-action='destroy-user-organization']",
function (e, data, status, xhr) {
// Populate the modal heading & body
var modal = $("#destroy-user-organization-modal");
var modalHeading = modal.find(".modal-header").find(".modal-title");
var modalBody = modal.find(".modal-body");
modalHeading.text(data.heading);
modalBody.html(data.html);
// Show the modal
modal.modal("show");
}
)
.on(
"ajax:error",
"[data-action='destroy-user-organization']",
function (e, data, status, xhr) {
// TODO
}
);
// Also, bind the click action on the modal
$("#destroy-user-organization-modal")
.on("click", "[data-action='submit']", function() {
var btn = $(this);
var form = btn
.closest(".modal")
.find(".modal-body")
.find("form[data-id='destroy-user-organization-form']");
// Simply submit the form!
form.submit();
});
// Lastly, bind on the ajax form
$(document)
.on(
"ajax:success",
"[data-id='destroy-user-organization-form']",
function (e, data, status, xhr) {
// Hide modal & clear its contents
var modal = $("#destroy-user-organization-modal");
var modalHeading = modal.find(".modal-header").find(".modal-title");
var modalBody = modal.find(".modal-body");
modalHeading.text("");
modalBody.html("");
// Hide the modal
modal.modal("hide");
// Reload the whole table
usersDatatable.ajax.reload();
}
)
.on(
"ajax:error",
"[data-id='destroy-user-organization-form']",
function (e, data, status, xhr) {
// TODO
}
);
}
initEditName();
initEditDescription();
initUsersTable();
initUpdateRoles();
initRemoveUsers();

View file

@ -0,0 +1,59 @@
function initLeaveOrganizations() {
// Bind the "leave organization" buttons in organizations table
$(document)
.on(
"ajax:success",
"[data-action='leave-user-organization']",
function (e, data, status, xhr) {
// Populate the modal heading & body
var modal = $("#modal-leave-user-organization");
var modalHeading = modal.find(".modal-header").find(".modal-title");
var modalBody = modal.find(".modal-body");
modalHeading.text(data.heading);
modalBody.html(data.html);
// Show the modal
modal.modal("show");
}
)
.on(
"ajax:error",
"[data-action='destroy-user-organization']",
function (e, data, status, xhr) {
// TODO
}
);
// Also, bind the click action on the modal
$("#modal-leave-user-organization")
.on("click", "[data-action='submit']", function() {
var btn = $(this);
var form = btn
.closest(".modal")
.find(".modal-body")
.find("form[data-id='leave-user-organization-form']");
// Simply submit the form!
form.submit();
});
// Lastly, bind on the ajax form
$(document)
.on(
"ajax:success",
"[data-id='leave-user-organization-form']",
function (e, data, status, xhr) {
// Simply reload the page
location.reload();
}
)
.on(
"ajax:error",
"[data-id='destroy-user-organization-form']",
function (e, data, status, xhr) {
// TODO
}
);
}
initLeaveOrganizations();

View file

@ -0,0 +1,167 @@
/* Global selectors */
var modal = $("#add-user-modal");
var modalContent = modal.find(".modal-content");
var invitingExisting = true;
var inviteButton = $("[data-id='invite-btn']");
var inviteLinks = $("[data-action='invite']");
var inviteExistingCollapsible = $("#invite-existing");
var inviteExistingForm = $("[data-id='invite-existing-form']");
var inviteExistingQuery = $("#existing_query");
var inviteExistingResults = $("#invite-existing-results");
var inviteNewCollapsible = $("#invite-new");
var inviteNewForm = $("[data-id='invite-new-form']");
var inviteNewRoleInput = $("[data-id='new-user-role-input']");
var inviteNewNameInput = $("[data-id='invite-new-name-input']");
var inviteNewEmailInput = $("[data-id='invite-new-email-input']");
function disableInviteBtn() {
inviteButton.attr("disabled", "disabled");
}
function enableInviteBtn() {
inviteButton.removeAttr("disabled");
}
/**
* General modal configuration & toggling.
*/
modal
.on("shown.bs.modal", function() {
// Focus the invite existing input
inviteExistingQuery.focus();
invitingExisting = true;
})
.on("hidden.bs.modal", function() {
// Disable invite button,
// reset forms, reset rendered content
disableInviteBtn();
inviteExistingForm.clear_form_fields();
inviteExistingForm.clear_form_errors();
inviteExistingResults.html("");
inviteNewForm.clear_form_fields();
inviteNewForm.clear_form_errors();
});
inviteExistingCollapsible
.on("hidden.bs.collapse", function() {
// Reset form & rendered content
inviteExistingForm.clear_form_fields();
inviteExistingForm.clear_form_errors();
inviteExistingResults.html("");
})
.on("hide.bs.collapse", function() {
// Disable invite button
disableInviteBtn();
})
.on("shown.bs.collapse", function() {
// Focus input when collapsible is shown
inviteExistingQuery.focus();
});
inviteNewCollapsible
.on("hidden.bs.collapse", function() {
// Reset form
inviteNewForm.clear_form_fields();
inviteNewForm.clear_form_errors();
})
.on("hide.bs.collapse", function() {
// Disable invite button
disableInviteBtn();
})
.on("shown.bs.collapse", function() {
// Focus input when collapsible is shown
inviteNewNameInput.focus();
invitingExisting = false;
});
// Invite links simply submit either of the forms
inviteLinks.on("click", function() {
var $this = $(this);
if (invitingExisting) {
var form =
inviteExistingResults
.find("form[data-id='create-user-organization-form']");
// Set the role value in the form
form
.find("[data-id='existing-user-role-input']")
.attr("value", $this.attr("data-value"));
// Submit the form inside "invite existing"
animateSpinner(modalContent);
form.submit();
} else {
// Set the role value in the form
inviteNewRoleInput
.attr("value", $this.attr("data-value"));
// Submit the form inside "invite new"
animateSpinner(modalContent);
inviteNewForm.submit();
}
});
/**
* Invite existing user functionality.
*/
// Invite existing form submission
modal
.on("ajax:success", inviteExistingForm.selector, function(ev, data, status) {
// Clear form errors
inviteExistingForm.clear_form_errors();
// Alright, render the html
inviteExistingResults.html(data.html);
// Disable invite button
disableInviteBtn();
})
.on("ajax:error", inviteExistingForm.selector, function(ev, data, status) {
// Display form errors
inviteExistingForm.render_form_errors_input_group("", data.responseJSON);
});
// Update values & enable "invite" button
// when user clicks on existing user
inviteExistingResults
.on("change", "[data-action='select-existing-user']", function() {
var $this = $(this);
// Set the hidden input user ID
$("[data-id='existing-user-id-input']")
.attr("value", $this.attr("data-user-id"));
// Enable button
enableInviteBtn();
});
/**
* Invite new user functionality.
*/
inviteNewForm
.on("ajax:success", function(ev, data, status) {
// Reload the page
location.reload();
})
.on("ajax:error", function(ev, data, status) {
// Render form errors
animateSpinner(modalContent, false);
$(this).render_form_errors("user", data.responseJSON);
});
// Enable/disable invite button depending whether
// any of the new user inputs are empty
inviteNewForm
.on("input", "input[data-role='input']", function() {
if (
_.isEmpty(inviteNewNameInput.val()) ||
_.isEmpty(inviteNewEmailInput.val())
) {
disableInviteBtn();
} else {
enableInviteBtn();
}
});

View file

@ -0,0 +1,63 @@
/**
* Toggle the view/edit form visibility.
* @param form - The jQuery form selector.
* @param edit - True to set form to edit mode;
* false to set form to view mode.
*/
function toggleFormVisibility(form, edit) {
if (edit) {
form.find("[data-part='view']").hide();
form.find("[data-part='edit']").show();
form.find("[data-part='edit'] input:not([type='file']):not([type='submit']):first").focus();
} else {
form.find("[data-part='view']").show();
form.find("[data-part='edit'] input").blur();
form.find("[data-part='edit']").hide();
// Clear all errors on the parent form
form.clear_form_errors();
// Clear any neccesary fields
form.find("input[data-role='clear']").val("");
// Copy field data
var val = form.find("input[data-role='src']").val();
form.find("input[data-role='edit']").val(val);
}
}
var forms = $("form[data-for]");
// Add "edit form" listeners
forms
.find("[data-action='edit']").click(function() {
var form = $(this).closest("form");
// First, hide all form edits
_.each(forms, function(form) {
toggleFormVisibility($(form), false);
});
// Then, edit the current form
toggleFormVisibility(form, true);
});
// Add "cancel form" listeners
forms
.find("[data-action='cancel']").click(function() {
var form = $(this).closest("form");
// Hide the edit portion of the form
toggleFormVisibility(form, false);
});
// Add form submit listeners
forms
.on("ajax:success", function(ev, data, status) {
// Simply reload the page
location.reload();
})
.on("ajax:error", function(ev, data, status) {
// Render form errors
$(this).render_form_errors("user", data.responseJSON);
});

View file

@ -0,0 +1,17 @@
/*
*= require_self
*= require_tree .
*= require jquery-ui/draggable
*= require rails_bootstrap_forms
*= require bootstrap-select
*= require colors
*= require introjs
*= stub reports_pdf
*/
@import "bootstrap-sprockets";
@import "bootstrap";
@import "bootstrap-datetimepicker";
@import "bootstrap-colorselector";
@import "handsontable.full.min";
@import "extend/bootstrap";
@import "themes/scinote";

View file

@ -0,0 +1,27 @@
// Theme colors
$color-theme-primary: #37a0d9;
$color-theme-secondary: #8fd13f;
$color-theme-dark: #6d6e71;
// Grayscales
$color-white: #fff;
$color-alabaster: #fcfcfc;
$color-concrete: #f2f2f2;
$color-gallery: #EEE;
$color-alto: #d2d2d2;
$color-silver: #c5c5c5;
$color-silver-chalice: #a0a0a0;
$color-gray: #909088;
$color-dove-gray: #666666;
$color-emperor: #555;
$color-mine-shaft: #333;
$color-black: #000;
// Misc.
$color-mystic: #eaeff2;
$color-candlelight: #ffda23;
// Reds
$color-mojo: #cf4b48;
$color-apple-blossom: #a94442;
$color-milano-red: #a70b05;

View file

@ -0,0 +1,3 @@
// Place all the styles related to the CustomFields controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View file

@ -0,0 +1,13 @@
/* Extending Bootstrap */
/* navbar avatar image */
.navbar-nav .avatar {
border-radius: 30px;
height: 30px;
margin-top: -14px;
position: relative;
width: 30px;
top: 5px;
}

View file

@ -0,0 +1,73 @@
@mixin box-shadow($shadows...) {
-moz-box-shadow: $shadows;
-webkit-box-shadow: $shadows;
box-shadow: $shadows;
-o-box-shadow: $shadows;
}
@mixin glyphicon-flip-horizontal() {
-moz-transform: scaleX(-1);
-o-transform: scaleX(-1);
-webkit-transform: scaleX(-1);
transform: scaleX(-1);
filter: FlipH;
-ms-filter: "FlipH";
}
@mixin rotate($degrees) {
-webkit-transform: rotate($degrees);
-moz-transform: rotate($degrees);
-ms-transform: rotate($degrees);
-o-transform: rotate($degrees);
transform: rotate($degrees);
}
@mixin rotate-important($degrees) {
-webkit-transform: rotate($degrees) !important;
-moz-transform: rotate($degrees) !important;
-ms-transform: rotate($degrees) !important;
-o-transform: rotate($degrees) !important;
transform: rotate($degrees) !important;
}
@mixin no-animation() {
-webkit-transition: none !important;
-moz-transition: none !important;
-ms-transition: none !important;
-o-transition: none !important;
transition: none !important;
}
@mixin transition($trans) {
-webkit-transition: $trans;
-moz-transition: $trans;
-ms-transition: $trans;
-o-transition: $trans;
transition: $trans;
}
@mixin rotate-animation-important($duration, $degrees) {
-webkit-transition-duration: $duration !important;
-moz-transition-duration: $duration !important;
-ms-transition-duration: $duration !important;
-o-transition-duration: $duration !important;
transition-duration: $duration !important;
-webkit-transition-property: -webkit-transform !important;
-moz-transition-property: -moz-transform !important;
-ms-transition-property: -ms-transform !important;
-o-transition-property: -o-transform !important;
transition-property: transform !important;
@include rotate-important($degrees);
}
@mixin rotate-animation($duration, $degrees) {
-webkit-transition-duration: $duration;
-moz-transition-duration: $duration;
-ms-transition-duration: $duration;
-o-transition-duration: $duration;
transition-duration: $duration;
-webkit-transition-property: -webkit-transform;
-moz-transition-property: -moz-transform;
-ms-transition-property: -ms-transform;
-o-transition-property: -o-transform;
transition-property: transform;
@include rotate($degrees);
}

View file

@ -0,0 +1,13 @@
// Place all the styles related to the MyModules controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
.description-label {
word-wrap: break-word;
}
/* Results index page */
#results {
margin-top: 20px;
}

View file

@ -0,0 +1,3 @@
// Place all the styles related to the Organizations controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View file

@ -0,0 +1,154 @@
/*!
* Start Bootstrap - Simple Sidebar HTML Template (http://startbootstrap.com)
* Code licensed under the Apache License v2.0.
* For details, see http://www.apache.org/licenses/LICENSE-2.0.
*/
@import "colors";
@import "mixins";
$wrapper-width: 280px;
$toggle-btn-size: 50px;
@mixin sidebar-shown {
// This rule is always overriden (show()) in JS
// after document is loaded
display: none;
padding-left: $wrapper-width;
padding-right: 0;
-webkit-transition: all 0.5s ease;
-moz-transition: all 0.5s ease;
-o-transition: all 0.5s ease;
transition: all 0.5s ease;
#sidebar-wrapper {
background-color: $color-alto;
z-index: 1000;
position: fixed;
width: $wrapper-width;
left: $wrapper-width;
height: 100%;
margin-left: -$wrapper-width;
-webkit-transition: all 0.5s ease;
-moz-transition: all 0.5s ease;
-o-transition: all 0.5s ease;
transition: all 0.5s ease;
#slide-panel {
height: 100%;
.sidebar-header {
height: $toggle-btn-size;
background: $color-theme-primary;
border-bottom: 2px solid darken($color-theme-primary, 10%);
.sidebar-header-title {
width: inherit;
color: $color-white;
display: inline-block;
margin-left: 15px;
margin-top: 6px;
text-transform: uppercase;
max-width: ($wrapper-width - $toggle-btn-size);
overflow: hidden;
text-overflow: ellipsis;
opacity: 1;
// Animations
@include transition(opacity 0.5s ease);
}
}
.sidebar-header-toggle {
height: $toggle-btn-size;
width: $toggle-btn-size;
margin-left: ($wrapper-width - $toggle-btn-size);
margin-top: -$toggle-btn-size;
font-size: 20pt;
background: $color-theme-primary;
border-left: 2px solid darken($color-theme-primary, 10%);
border-bottom: 2px solid darken($color-theme-primary, 10%);
// Animations
@include transition(margin-left 0.5s ease);
span {
margin: 10px;
color: $color-white;
// Animations
@include rotate-animation(0.5s, 180deg);
@include transition(color 0.5s ease);
}
}
.tree {
margin-bottom: 0;
padding-top: 15px;
opacity: 1;
// Animations
@include transition(opacity 0.5s ease);
}
}
}
}
@mixin sidebar-hidden {
padding-left: 0;
#sidebar-wrapper {
width: 0;
#slide-panel {
.sidebar-header .sidebar-header-title {
width: 0;
opacity: 0;
@include transition(width 0.5s ease);
@include transition(opacity 0.5s ease);
}
.sidebar-header-toggle {
margin-left: 0;
background: none;
border: none;
@include transition(margin-left 0.5s ease);
span {
color: darken($color-theme-primary, 10%);
@include rotate-animation(0.5s, 0deg);
@include transition(color 0.5s ease);
}
}
.tree {
opacity: 0;
@include transition(opacity 0.5s ease);
}
}
}
}
#wrapper {
@include sidebar-shown;
}
#wrapper.no-animation * {
@include no-animation;
}
#wrapper.toggled {
@include sidebar-hidden;
}
#wrapper.hidden2 {
@include sidebar-hidden;
}
.sidebar-no-module-group {
color: $color-silver-chalice;
}

View file

@ -0,0 +1,63 @@
@import "colors";
@import "mixins";
.tree {
height: 100%;
overflow-y: auto;
padding-bottom: 30px;
}
.tree > ul {
margin-bottom: 0;
}
.tree ul {
padding-left: 0;
}
.tree li {
list-style-type: none;
margin: 0;
padding: 5px 5px 5px 15px;
position: relative;
&.active > span {
background-color: $color-white;
border: 1px solid $color-white;
border-radius: 4px;
font-weight: bold;
}
&.active:not span.tree-link:hover {
text-decoration: underline;
}
&.leaf {
padding-left: 30px;
}
& i.glyphicon {
font-size: 9pt;
&.expanded {
@include rotate(45deg);
}
}
/* Links are recolored */
a {
color: $color-emperor;
&:hover {
color: $color-theme-primary;
}
}
span {
display:inline-block;
padding:3px 8px;
}
}
.tree li.parent_li>span {
display: block;
}
.tree li:last-child::before {
height:30px;
}

View file

@ -0,0 +1,3 @@
// Place all the styles related to the project_activities controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View file

@ -0,0 +1,408 @@
@import "colors";
@import "mixins";
// Some color definitions
$color-group-hover: $color-theme-primary;
$color-module-hover: $color-theme-secondary;
/* Canvas index page */
#canvas-container:not(.canvas-container-edit-mode) {
margin-top: 20px;
}
/**********************************
* jsPlumb CANVAS RELATED STYLING *
*********************************/
#diagram-buttons {
margin-bottom: 10px;
}
#update-canvas {
#canvas-new-module {
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
}
.btn-group > .btn:first-child {
border-bottom-left-radius: 0;
}
.btn-group > .btn:last-child {
border-bottom-right-radius: 0;
}
}
#canvas-new-module {
margin-left: 10px;
.hbtn-default {
opacity: 1;
width: 0;
float: none;
}
.hbtn-hover {
opacity: 0;
width: 0;
height: 0;
float: left;
}
&:hover {
.hbtn-default {
opacity: 0;
height: 0;
float: left;
}
.hbtn-hover {
opacity: 1;
float: none;
}
}
}
#diagram-container {
/* for IE10+ touch devices */
touch-action: none;
height: 650px;
background: $color-dove-gray;
@include box-shadow(0px 0px 2px 1px $color-dove-gray);
overflow: hidden;
cursor: move;
}
.diagram {
position: relative;
display: block;
.window:hover {
@include box-shadow(2px 2px 19px $color-emperor);
}
.hover {
border: 1px dotted red;
}
._jsPlumb_connector {
z-index: 4;
}
._jsPlumb_endpoint_anchor {
}
._jsPlumb_endpoint, ._jsPlumb_endpoint_full {
z-index: 21;
cursor: pointer;
}
._jsPlumb_overlay, .endpointTargetLabel, .endpointSourceLabel {
z-index: 21;
background-color: $color-white;
cursor: pointer;
}
.connLabel {
background-color: $color-white;
color: $color-dove-gray;
padding: 0px 7px 2px 7px;
font: 20px arial;
font-weight: bold;
border-radius: 50%;
z-index: 5;
cursor: pointer;
&:hover {
color: $color-theme-primary;
padding: 2px 9px 4px 9px;
}
}
}
.window._jsPlumb_connected {
border: 2px solid green;
}
.jsplumb-drag .title {
background-color: $color-theme-primary !important;
color: $color-white !important;
}
path, ._jsPlumb_endpoint {
cursor: pointer;
}
.ep-normal svg * {
fill: $color-white;
}
.ep-hover svg * {
fill: $color-theme-primary;
}
/* EDIT MODE MODULE */
.module.new {
opacity: 0.7;
}
.module.dragged > .panel-heading {
background-color: $color-theme-primary;
color: $color-white;
}
.module.collided {
.overlay {
display: block;
z-index: 21;
background-color: $color-milano-red;
border: 1px solid $color-milano-red;
@include box-shadow(0 0 0 1pt $color-milano-red);
border-radius: 4px;
position: absolute;
top: 0;
height: 100%;
width: 100%;
opacity: 0.7;
}
}
.module {
width: 250px;
cursor: pointer;
position: absolute;
display: block;
.panel-heading {
height: 40px;
.dropdown {
bottom: 18px;
left: 0;
}
}
.panel-body {
height: 45px;
}
.ep {
font-style: italic;
}
.dropdown {
.dropdown-toggle {
color: $color-silver-chalice;
}
.dropdown-menu {
z-index: 30;
}
}
.overlay {
display: none;
}
}
/* FULL-ZOOM MODULE */
.module-large {
width: 290px;
cursor: pointer;
position: absolute;
display: block;
z-index: 5;
.panel-body .due-date-link {
color: $color-emperor;
}
&.expanded {
z-index: 30;
}
&.group-hover {
@include box-shadow(0px 0px 13px 7px $color-group-hover);
}
&.module-hover {
@include box-shadow(0px 0px 13px 7px $color-module-hover);
}
&.alert-yellow .panel-body {
color: $color-candlelight;
font-weight: bold;
.due-date-link {
color: $color-candlelight;
}
}
&.alert-red .panel-body {
color: $color-milano-red;
font-weight: bold;
.due-date-link {
color: $color-milano-red;
}
}
}
/* MEDIUM-ZOOM MODULE */
.module-medium {
width: 200px;
cursor: pointer;
position: absolute;
display: block;
z-index: 5;
&.group-hover {
@include box-shadow(0px 0px 13px 7px $color-group-hover);
}
&.module-hover {
@include box-shadow(0px 0px 13px 7px $color-module-hover);
}
&.alert-yellow {
border-color: $color-candlelight;
border-width: 4px;
border-radius: 8px;
}
&.alert-red {
border-color: $color-milano-red;
border-width: 4px;
border-radius: 8px;
}
}
.module-large .tags-container,
.module-medium .tags-container {
padding-top: 2px;
div {
font-size: 22pt;
width: 4px;
height: 0px;
display: inline-block;
& .glyphicon {
position: inherit;
}
&.last {
margin-right: 15px;
color: $color-silver-chalice;
}
}
& span.badge {
margin-left: -8px;
margin-top: -10px;
margin-right: 4px;
}
}
/* SMALL-ZOOM MODULE */
.module-small {
width: 50px;
height: 50px;
border-radius: 50%;
border: 6px solid $color-white;
@include box-shadow(inset 5px 5px 45px -6px $color-dove-gray);
background-color: $color-alto;
cursor: pointer;
position: absolute;
display: block;
text-align: center;
z-index: 5;
color: black;
span {
font-weight: bold;
font-size: 16px;
text-transform: uppercase;
display: block;
margin-top: 20%;
a {
color: $color-mine-shaft;
}
}
&.group-hover {
@include box-shadow(0px 0px 13px 7px $color-group-hover);
}
&.module-hover {
@include box-shadow(0px 0px 13px 7px $color-module-hover);
}
&.alert-yellow {
border-color: $color-candlelight;
}
&.alert-red {
border-color: $color-milano-red;
}
}
/* Sidebar hovered style */
li.group-hover {
background-color: $color-silver;
border-radius: 4px;
}
li.module-hover {
a {
color: $color-theme-primary;
text-decoration: underline;
}
}
/* Edit module tags modal window */
#manage-module-tags-modal {
.modal-body ul.list-group > li {
padding-top: 2px;
padding-bottom: 2px;
& > div.tag-show {
color: $color-white;
form {
display: inline-block;
.btn-link {
margin-top: 4px;
}
}
}
& > div.tag-edit {
.form-group {
margin-bottom: 2px;
margin-top: 3px;
}
.dropdown-colorselector {
display: inline-block;
.btn-colorselector {
height: 30px;
width: 30px;
margin-top: 5px;
font-family: 'Glyphicons Halflings';
color: $color-white;
font-size: 12pt;
&:before {
content: "\e221";
margin-left: 6px;
}
}
}
}
.glyphicon {
color: $color-white;
font-size: 12pt;
}
a.btn-link {
padding-top: 10px;
}
}
.well {
margin-bottom: 0;
& .bootstrap-select {
width: 150px !important;
}
}
.create-new-tag-btn {
margin-right: 15px;
}
}

View file

@ -0,0 +1,518 @@
@import "colors";
@import "mixins";
/* Index page */
.report-table {
margin-top: 20px;
}
/* New page navbar */
.navbar-report {
border-left: none;
border-top: none;
border-right: none;
border-bottom: 4px solid $color-silver;
background: $color-concrete !important;
margin-bottom: 0;
min-width: 320px;
padding: 0 15px;
z-index: 500;
position: fixed;
width: 100%;
div.row {
margin-right: 0;
}
#report-menu {
form {
display: inline-block;
}
}
& > div.row {
margin-right: 0;
}
}
#sort-report {
display: inline-block;
}
.get-report-pdf-form {
display: inline-block;
}
/* New page sidebar */
.report-sidebar-wrapper {
background-color: $color-white !important;
}
// Some additional styling on the treeview
.report-tree {
li {
padding: 0 0 0 15px;
a.report-nav-link:visited {
text-decoration: none;
}
a.report-nav-link:hover {
text-decoration: none;
}
}
}
.report-sidebar-panel-description {
margin: 10px 10px 0 10px;
}
.report-item-elements {
margin-top: 10px !important;
margin-left: 15px !important;
li {
margin: 5px 5px 5px 15px;
}
ul {
padding-left: 15px !important;
}
}
/**
* Global fix for handsontable
*/
.hot-table-container {
.ht_master .wtHolder {
height: auto !important;
width: auto !important;
}
.ht_clone_top,.ht_clone_left,.ht_clone_corner {
display: none !important;
}
}
/* New page content */
.report-body {
background: $color-dove-gray;
}
.report-container {
overflow-x: auto;
overflow-y: auto;
padding-top: 30px;
padding-bottom: 30px;
}
#report-content {
color: $color-black;
background: $color-white;
@include box-shadow(0px 0px 58px -10px $color-black);
max-width: 800px;
min-width: 230px;
min-height: 1200px;
margin-left: auto;
margin-right: auto;
padding: 45px;
}
@media (max-width: 720px) {
#report-content {
padding: 25px;
}
}
ul.project-contents-list {
padding-left: 15px !important;
}
/** "New element" floating element */
.new-element {
display: block;
position: relative;
opacity: 0.05;
&.initial {
/** Special "visual" display of initial new element block */
opacity: 0.7;
padding: 15px;
border-radius: 5px;
border: 4px $color-theme-primary solid;
.plus-icon {
bottom: 16px !important;
}
}
.line {
display: block;
float: left;
width: 50%;
.filler-wrapper {
display: block;
.filler {
display: block;
height: 4px;
background-color: $color-theme-primary;
border-radius: 1px;
margin-top: 8px;
margin-bottom: 8px;
}
}
}
.left-line .filler-wrapper {
padding: 0 20px 0 0;
}
.right-line .filler-wrapper {
padding: 0 0 0 20px;
}
.plus-icon {
color: $color-theme-primary;
display: block;
text-align: center;
width: 40px;
position: absolute;
bottom: 2px;
left: 50%;
margin: 0 0 0 -20px;
}
.clear {
clear: left;
}
}
.new-element:hover {
opacity: 1.0;
.filler {
background-color: $color-theme-primary;
.plus-icon span {
font-weight: bold;
}
}
}
/* GLOBAL REPORT ELEMENT STYLE */
.report-element {
width: 100%;
margin-bottom: 15px;
.report-element-header {
border-bottom: 2px solid $color-black;
.user-time {
color: $color-emperor;
margin-left: 15px;
}
.controls {
margin-right: 15px;
font-size: 12pt;
opacity: 0.05;
}
}
.report-element-body {
padding-top: 10px;
padding-left: 15px;
padding-right: 15px;
}
.report-element-children {
padding-left: 45px;
padding-top: 15px;
}
&:hover {
background-color: $color-mystic;
@include box-shadow(0 0 2px 15px $color-mystic);
& > .report-element-header {
.controls {
opacity: 1.0;
}
}
}
}
/* Project header element style */
.report-project-header-element {
margin-bottom: 60px;
.report-element-header {
border-bottom: none;
}
.report-element-body {
.project-name {
border-bottom: 4px solid $color-black;
}
}
&:hover > .report-element-body .project-name {
color: $color-theme-primary;
}
}
/* Module element style */
.report-module-element {
margin-top: 15px;
margin-bottom: 15px;
.report-element-body {
.module-name {
margin-left: 15px;
}
.module-tags {
margin-left: 0;
margin-top: 10px;
.module-no-tag {
margin-left: 5px;
}
.module-tag {
margin-left: 5px;
border-radius: 4px;
padding: 2px 4px;
color: $color-white;
}
}
}
&:hover > .report-element-body .module-name {
color: $color-theme-primary;
}
}
/* Result element style (generic) */
.report-result-element {
margin-bottom: 5px;
.report-element-header {
border-bottom: none;
height: 0;
.result-icon {
margin-left: 15px;
}
.result-name {
margin-left: 5px;
}
}
&:hover > .report-element-header {
color: $color-theme-primary;
}
}
/* Result asset element style */
.report-result-asset-element {
.report-element-header {
.file-name {
margin-left: 15px;
}
}
}
/* Result asset element style */
.report-result-table-element {
.report-element-body {
padding-top: 30px;
}
}
/* Result text element style */
.report-result-text-element {
.report-element-body {
.text-container {
border-radius: 4px;
padding: 5px;
background-color: $color-concrete;
}
}
}
/** Step element style */
.report-step-element {
&:hover > .report-element-body .step-name {
color: $color-theme-primary;
}
}
/* Step attachment style (table, asset or checklist) */
.report-step-attachment-element {
.report-element-header {
border-bottom: none;
.attachment-icon {
color: $color-emperor;
}
}
.report-element-children {
height: 0;
}
&:hover > .report-element-header {
.attachment-icon {
color: $color-theme-primary;
}
}
}
/** Step table element style */
.report-step-table-element {
.report-element-header {
.user-time {
margin-left: 5px;
}
}
&:hover > .report-element-header .user-time {
color: $color-theme-primary;
}
}
/** Step asset element style */
.report-step-asset-element {
.report-element-header {
.file-name {
margin-left: 5px;
}
}
&:hover > .report-element-header .file-name {
color: $color-theme-primary;
}
}
/** Step checklist element style */
.report-step-checklist-element {
.report-element-header {
.checklist-name {
margin-left: 5px;
}
}
.report-element-body {
padding-top: 0;
& > ul > li > span.checked {
/* Currently nothing */
}
}
&:hover > .report-element-header .checklist-name {
color: $color-theme-primary;
}
}
/** Comments element style (generic) */
.report-comments-element {
.report-element-header {
border-bottom: none;
.comments-icon {
color: $color-emperor;
}
.comments-name {
margin-left: 5px;
color: $color-emperor;
}
}
.report-element-body {
.comments-container {
border-radius: 4px;
padding: 5px;
background-color: $color-alabaster;
.comment {
margin: 3px 2px;
.comment-prefix {
color: $color-emperor;
}
}
}
}
&:hover > .report-element-header {
.comments-icon,.comments-name {
color: $color-theme-primary;
}
}
}
/** Result comments element style */
.report-result-comments-element {
}
/** Step comments element style */
.report-step-comments-element {
}
/** Module samples element */
.report-module-samples-element {
margin-bottom: 0;
.report-element-header {
border-bottom: none;
.samples-name {
margin-left: 5px;
}
}
&:hover > .report-element-header {
.samples-icon,.samples-name {
color: $color-theme-primary;
}
}
}
/** Module activity element */
.report-module-activity-element {
margin-bottom: 0;
.report-element-header {
border-bottom: none;
.activity-name {
margin-left: 5px;
}
}
.report-element-body {
.activity-container {
border-radius: 4px;
padding: 5px;
background-color: $color-alabaster;
.activity {
margin: 3px 2px;
.activity-prefix {
color: $color-emperor;
}
}
}
}
&:hover > .report-element-header {
.activity-icon,.activity-name {
color: $color-theme-primary;
}
}
}

View file

@ -0,0 +1,15 @@
/**
* Additional rules when generating PDF from the reports.
*/
// Hide all glyphicons
.glyphicon {
display: none;
}
.print-report-body {
.print-report {
overflow-y: hidden !important;
overflow-x: hidden !important;
}
}

View file

@ -0,0 +1,163 @@
@import "colors";
@import "mixins";
/** Custom CSS for report print (& PDF) */
body.print-report-body {
background-color: $color-white;
}
div.print-report {
background-color: $color-white;
padding: 30px;
.new-element {
height: 0;
display: none;
}
.report-element {
color: $color-black !important;
.controls {
display: none;
}
&:hover {
background-color: $color-white;
@include box-shadow(none);
}
.hot-table-container {
.ht_master .wtHolder {
overflow: hidden !important;
.wtHider {
height: auto !important;
}
}
}
}
.report-project-header-element {
& > .report-element-body .project-name {
color: $color-black;
}
&:hover > .report-element-body .project-name {
color: $color-black;
}
}
.report-module-element:hover {
& > .report-element-body .module-name {
color: $color-black;
}
&:hover > .report-element-body .module-name {
color: $color-black;
}
}
.report-result-element {
& > .report-element-header {
color: $color-black;
}
&:hover > .report-element-header {
color: $color-black;
}
}
.report-step-element {
& > .report-element-body .step-name {
color: $color-black;
}
&:hover > .report-element-body .step-name {
color: $color-black;
}
}
.report-step-attachment-element {
& > .report-element-header .attachment-icon {
color: $color-black;
}
&:hover > .report-element-header .attachment-icon {
color: $color-black;
}
}
.report-step-table-element {
& > .report-element-header .user-time {
color: $color-black;
}
&:hover > .report-element-header .user-time {
color: $color-black;
}
}
.report-step-asset-element {
& > .report-element-header .file-name {
color: $color-black;
}
&:hover > .report-element-header .file-name {
color: $color-black;
}
}
.report-step-checklist-element {
& > .report-element-header .checklist-name {
color: $color-black;
}
&:hover > .report-element-header .checklist-name {
color: $color-black;
}
}
.report-comments-element {
& > .report-element-header {
.comments-icon,.comments-name {
color: $color-black !important;
}
}
&:hover > .report-element-header {
.comments-icon,.comments-name {
color: $color-black !important;
}
}
}
.report-module-samples-element {
& > .report-element-header {
.samples-icon,.samples-name {
color: $color-black !important;
}
}
&:hover > .report-element-header {
.samples-icon,.samples-name {
color: $color-black !important;
}
}
}
.report-module-activity-element {
& > .report-element-header {
.activity-icon,.activity-name {
color: $color-black !important;
}
}
&:hover > .report-element-header {
.activity-icon,.activity-name {
color: $color-black !important;
}
}
}
}

View file

@ -0,0 +1,3 @@
// Place all the styles related to the ResultAssets controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View file

@ -0,0 +1,3 @@
// Place all the styles related to the ResultComments controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View file

@ -0,0 +1,3 @@
// Place all the styles related to the ResultTables controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View file

@ -0,0 +1,3 @@
// Place all the styles related to the ResultTexts controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View file

@ -0,0 +1,3 @@
// Place all the styles related to the SampleGroups controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View file

@ -0,0 +1,3 @@
// Place all the styles related to the SampleTypes controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View file

@ -0,0 +1,19 @@
.samples-table {
margin-top: 20px;
}
#samples_filter,
#samples_paginate,
#datatables-buttons {
float: right;
text-align: inherit;
}
#import-errors-container {
padding-top: 15px;
.alert {
position: inherit !important;
}
}

View file

@ -0,0 +1,8 @@
// Place all the styles related to the search controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
#search-content {
background-color: #fff;
padding-top: 20px;
}

View file

@ -0,0 +1,3 @@
// Place all the styles related to the StepComments controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View file

@ -0,0 +1,19 @@
// Place all the styles related to the Steps controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/
#new_step,
.panel-step-attachment {
ul {
list-style: none;
li {
margin-bottom: 10px;
& > div > span.pull-left {
margin-top: 8px;
font-size: 1.2em;
}
}
}
}

View file

@ -0,0 +1,960 @@
@import "colors";
@import "mixins";
/** Layout **/
body,
#activity-modal,
#main-nav,
#notifications,
#notifications .alert {
min-width: 320px;
}
#alert-container {
margin-bottom: 20px;
}
#main-nav {
margin-bottom: 0;
}
#project-archive-btn {
margin-left: 15px;
}
#projects-toolbar {
margin: 15px 0;
}
#projects-toolbar .form-group {
width: 100%;
}
.form-inline {
.form-group .dropdown {
display: inline-block;
}
}
#fluid-content {
padding-left: 15px;
padding-right: 15px;
padding-top: 80px;
}
.spacer {
margin-left: 0.5em;
margin-right: 0.5em;
}
#content-wrapper {
margin-top: 50px;
&.alert-shown {
margin-top: 102px;
}
}
.center-block-narrow {
max-width: 400px;
}
#search-menu {
padding-right: 0;
.nav {
position: relative;
z-index: 1000;
}
}
#search-content {
padding-left: 0;
}
#search-container {
padding-left: 45px;
}
.vertical-spacer-one-half {
display: inline-block;
width: 1.5em;
}
// Global invisible setter (hide element, but keep its size)
.invisible {
visibility: hidden !important;
}
/** Skin **/
@import url(https://fonts.googleapis.com/css?family=Open+Sans:400,600,700,400italic&subset=latin,latin-ext);
body {
background-color: $color-concrete;
color: $color-emperor;
font-family: "Open Sans",Arial,Helvetica,sans-serif;
font-size: 13px;
}
a {
color: $color-theme-primary;
}
.jumbotron {
background-color: inherit;
}
.alert {
border-radius: 0;
margin-bottom: 0;
opacity: 1;
width: 100%;
&.alert-hidden {
display: none;
}
a#hide-alert {
margin-left: 15px;
}
&.alert-floating {
position: fixed;
top: 50px;
z-index: 1000;
}
}
.badge {
background-color: $color-theme-primary;
font-size: 11px;
border-radius: 5px;
}
.badge-indicator,
.btn .badge-indicator {
margin-left: -8px;
top: 3px;
}
.handle-move {
cursor: move;
cursor: -webkit-grabbing;
}
.bg-primary {
background-color: $color-theme-primary;
}
/* this rule is strict because the order of css files is not correct */
.bootstrap-select:not([class*="col-"]):not([class*="form-control"]):not(.input-group-btn) {
width: 100% !important;
}
.btn {
border-radius: 1.5em;
}
.btn-primary {
background-color: $color-theme-secondary;
border-color: darken($color-theme-secondary, 5%);
&.active,
&.focus,
&.active.focus {
background-color: darken($color-theme-secondary, 20%);
border-color: darken($color-theme-secondary, 25%);
&:hover {
background-color: darken($color-theme-secondary, 25%);
border-color: darken($color-theme-secondary, 30%);
}
}
&:active,
&:focus,
&:active:focus,
&:active:hover,
&:focus:hover,
&:active:focus:hover {
background-color: darken($color-theme-secondary, 20%);
border-color: darken($color-theme-secondary, 25%);
}
&:hover {
background-color: darken($color-theme-secondary, 5%);
border-color: darken($color-theme-secondary, 10%);
}
}
mark,.mark {
background-color: $color-candlelight;
}
.label-default {
background-color: $color-alto;
}
.label-primary {
background-color: $color-theme-primary;
}
.circle {
@extend .badge;
background-color: $color-theme-primary;
border-radius: 1em;
&.disabled {
background-color: $color-silver-chalice;
}
}
.navbar {
border-radius: 0;
}
.navbar-default {
background-color: $color-white;
border-color: $color-alto;
}
.navbar-default .navbar-brand {
background-color: $color-theme-primary;
font-size: 23px;
& > img {
margin-top: -4px;
&.with-version {
margin-top: -10px;
}
}
& > span.version {
font-size: 0.6em;
color: $color-white;
float: right;
}
&:hover,
&:focus,
&:focus:active,
&:focus:visited {
background-color: $color-theme-primary;
}
}
.nav-tabs {
margin-bottom: 15px;
& > li.has-error {
& > a {
color: $color-apple-blossom;
&:hover {
color: $color-mojo;
}
}
}
}
.nav-tabs-less {
margin-bottom: 0;
}
.nav-pills {
& > li {
a {
color: $color-theme-primary;
}
&:not(.active):hover a {
background-color: $color-alto;
}
&.active a {
color: $color-white;
background-color: $color-theme-primary;
}
}
}
.breadcrumb {
background-color: transparent;
padding: 15px;
margin-bottom: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.nav-tabs-less > li.active > a {
&,&:hover,&:focus {
color: $color-theme-secondary;
background-color: transparent;
border-color: transparent;
}
}
#secondary-navigation {
white-space: nowrap;
overflow: hidden
}
.navbar-secondary {
background: $color-concrete !important;
margin-left: -280px;
padding-left: 295px;
padding-right: 15px;
margin-bottom: 0;
border-color: transparent;
z-index: 500;
position: fixed;
width: 100%;
.container-fluid {
border-left: 0;
border-top: 0;
border-right: 0;
border-bottom: 4px solid $color-silver;
padding-right: 0;
}
ul.nav {
margin-right: 0;
& > li {
text-transform: uppercase;
& > a {
color: $color-gray;
span {
//width: 14px;
}
}
&.active {
@include box-shadow(0 4px 0 $color-theme-primary);
&> a {
font-weight: bold;
color: $color-emperor;
}
}
}
}
}
.navbar-secondary {
-webkit-transition: all 0.5s ease;
-moz-transition: all 0.5s ease;
-o-transition: all 0.5s ease;
transition: all 0.5s ease;
}
.navbar-without-sidebar{
padding-left: 15px;
margin-left: 0px;
-webkit-transition: all 0.5s ease;
-moz-transition: all 0.5s ease;
-o-transition: all 0.5s ease;
transition: all 0.5s ease;
}
/** Chat bubble */
.chat-bubble {
background-color: $color-white;
border-radius: 1em;
padding: 10px;
}
/** Search */
.nav-search {
li.disabled {
opacity: 0.8;
.badge {
background-color: $color-emperor;
opacity: 0.8;
}
}
}
/** Settings */
.nav-settings {
margin-top: 15px;
margin-bottom: 0;
}
.tab-pane-settings {
background-color: $color-white;
padding: 15px;
border-left: 1px solid $color-alto;
border-right: 1px solid $color-alto;
border-bottom: 1px solid $color-alto;
margin-bottom: 50px;
}
.breadcrumb-organizations {
background-color: $color-concrete;
margin-bottom: 15px;
}
/** Add users modal */
.btn-group-existing-users {
width: 100%;
label.btn {
text-align: center;
&.btn-title {
color: $color-white;
cursor: inherit;
background-color: $color-theme-primary;
&:focus, &:active, &:hover {
box-shadow: none;
background-color: $color-theme-primary;
border-color: #adadad;
}
}
}
}
.existing-users-smalltext {
width: 100%;
text-align: center;
}
/** Users datatable */
.panel-organization-users .panel-body {
padding-bottom: 0;
}
.users-datatable {
margin-bottom: 20px;
#users-table_filter {
float: right;
margin-top: 19px;
}
#users-table_paginate {
float: right;
}
.dropdown-organizations-user {
.dropdown-menu li.user-organization-role {
& > :first-child {
padding-left: 10px;
}
&:not(.disabled) span.glyphicon {
color: transparent !important;
}
}
}
}
@media(max-width:768px) {
.navbar-secondary ul.breadcrumb {
margin-left: 15px;
}
}
ul.no-style {
list-style: none;
margin: 0;
padding: 0;
}
ul.double-line > li {
margin-bottom: 1em;
}
.page-header {
border-color: $color-alto;
}
.pagination > .active > a,
.pagination > .active > a:hover,
.pagination > .active > a:focus,
.pagination > .active > span,
.pagination > .active > span:hover,
.pagination > .active > span:focus {
background-color: $color-theme-primary;
}
.pagination > li > a,
.pagination > li > span {
color: $color-theme-primary;
}
.panel-default > .panel-heading {
background-color: $color-mystic;
&>.panel-title {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.panel-project {
.panel-heading {
background-color: $color-theme-primary;
color: $color-white;
}
}
.panel-archive {
.panel-heading {
background-color: darken($color-mystic, 5%);
color: lighten($color-mine-shaft, 15%);
}
}
.panel-options {
position: relative;
bottom: 6px;
}
.panel-footer {
padding: 0 15px;
}
.panel-footer-scinote {
background: linear-gradient(to bottom, $color-concrete, $color-white 10px);
padding: 0;
hr {
margin-top: 10px;
margin-bottom: 10px;
}
.btn-link {
color: $color-silver-chalice;
}
.btn-link:hover {
color: darken($color-silver-chalice, 15%);
}
.tab-content ul {
margin-bottom: 15px;
}
.tab-content li {
padding-left: 15px;
padding-right: 15px;
}
.content-module-info {
max-height: 250px;
overflow: auto;
}
.content-comments {
max-height: 250px;
overflow: auto;
}
.content-activities {
max-height: 250px;
overflow: auto;
}
.content-users {
max-height: 250px;
overflow: auto;
}
.content-notifications {
max-height: 250px;
overflow: auto;
li.notification.alert-red > .date-time {
font-weight: bold;
color: $color-milano-red;
}
li.notification.alert-yellow > .date-time {
font-weight: bold;
color: $color-candlelight;
}
}
}
/* Accordion panel */
.panel-accordion {
border: 0;
border-radius: 0;
margin-bottom: 0;
&> .panel-heading {
background-color: $color-mystic;
border-bottom: 1px solid $color-alto;
.panel-title > a {
&:hover, &:focus {
text-decoration: none;
}
& > span {
@include rotate(90deg);
}
}
}
& .panel-body {
background-color: $color-white;
padding: 0;
}
}
.form-control.bootstrap-select {
background-color: inherit;
@include box-shadow(inherit);
}
.panel-heading .dropdown {
bottom: 8px;
left: 8px;
}
#activity-modal {
.modal-body {
background-color: $color-concrete;
color: $color-mine-shaft;
}
}
/** Activity list resembling Bootstrap wells */
ul.content-activities {
li.activity-item {
border-radius: .25em;
background-color: $color-white;
border: 1px solid $color-concrete;
.activity-item-date {
display: table-cell;
vertical-align: middle;
border-top-left-radius: .25em;
border-bottom-left-radius: .25em;
border: 3px solid $color-alto;
background-color: $color-alto;
padding-left: 10px;
padding-right: 10px;
vertical-align: top;
}
.activity-item-text {
display: table-cell;
padding: 3px 10px;
text-align: justify;
}
}
li.activity-date-item {
font-size: 1.4em;
& > span {
padding-left: 2em;
padding-right: 2em;
}
}
}
ul.content-module-activities {
li.activity-item {
margin-bottom: 15px;
.activity-item-date {
font-size: 1.2em;
background-color: $color-theme-primary;
border-color: $color-theme-primary;
color: $color-white;
padding-top: 5px;
padding-bottom: 5px;
}
.activity-item-text {
padding-top: 5px;
padding-bottom: 5px;
}
}
}
.step {
.panel-heading a[data-toggle] {
color: inherit;
}
&.not-completed {
.badge-num > span.badge {
background-color: $color-silver;
}
}
}
.well {
background-color: $color-white;
}
.well-sm {
border-radius: 0;
}
/* Steps and Results */
#steps {
background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAABCAYAAACsXeyTAAAAIUlEQVQImWNgoD5gZGBgMILSjKRo/P//vwiSGQwMDAwMAEnaA0jgHoquAAAAAElFTkSuQmCC");
background-repeat: repeat-y;
background-position: -3px 0;
}
.badge-icon {
font-size: 1.4em;
float: left;
padding: 6px 10px;
& + .well-sm {
margin-left: 38px;
}
}
.step,
.result {
.panel {
margin-left: 38px;
}
.badge-num {
position: absolute;
& > .badge {
border-radius: 2em;
float: left;
font-size: 23.4px;
padding: 6px 11px;
position: relative;
top: 2px;
}
.size-digit-2 {
font-size: 18px;
padding: 8px;
}
.size-digit-3 {
font-size: 14px;
padding: 10px 6px;
}
& > .badge.icon {
font-size: 16.5px;
padding: 9px;
}
}
.panel-heading a[data-toggle] {
color: inherit;
}
.content-comments {
max-height: 250px;
overflow: auto;
}
}
.hot_table {
margin-bottom: 25px;
}
.step-result-hot-table {
max-height: 400px;
overflow: hidden;
width: 100%;
}
.btn-greyed {
background-color: $color-silver-chalice;
border-color: $color-silver-chalice;
color: $color-white;
&:hover,
&:focus {
background-color: darken($color-silver-chalice, 15%);
border-color: darken($color-silver-chalice, 15%);
color: $color-white;
}
}
/* Data table */
table.dataTable {
width: 100% !important;
background-color: $color-alabaster;
thead {
background-color: $color-gray;
}
thead > tr > th {
border-bottom-width: 0;
border-left: 2px solid $color-alabaster;
color: $color-white;
font-weight: normal;
}
thead > tr > th:first-child {
border-left: none;
}
thead > tr > th,
thead > tr > td {
padding: 6px;
}
tbody > tr.selected,
tbody > tr > .selected {
background-color: $color-alto !important;
color: $color-emperor !important;
}
.sorting_desc,
.sorting_asc {
background-color: $color-theme-primary;
}
}
/* Helpers */
.line-wrap {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
&.short {
position: relative;
top: 6px;
max-width: 78%;
}
}
/* Sample group color picker */
.btn-group-sample-group-color {
.btn-group > .btn {
border-radius: 0 !important;
}
}
#samples_length {
display: inline-block;
}
.toolbarButtons {
display: inline-block;
padding-left: 20px;
}
/* Pills with arrow */
.nav-stacked-arrow > li > a {
border-radius: 2px;
}
.nav-stacked-arrow > li.active > a:after,
.nav-stacked-arrow > li.active > a:hover:after,
.nav-stacked-arrow > li.active > a:focus:after {
content: '';
position: absolute;
left: 100%;
top: 50%;
margin-top: -19px;
border-top: 19px solid transparent;
border-left: 13px solid #37A0D9;
border-bottom: 19px solid transparent;
}
.nav-stacked-arrow > li.active > a:hover:after {
border-left: 13px solid #337ab7;
}
/* Overlay to disable interaction while loading ajax */
.loading-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000000000;
cursor: wait;
}
html.turbolinks-progress-bar::before {
background-color: $color-mojo !important;
}
/* Loading animation for ajax events, inspired by Codrops */
#loading-animation {
position: fixed;
top: 0;
left: 0;
z-index: -1;
width: 100%;
height: 3px;
background: $color-mojo;
-webkit-transform: translate3d(-100%, 0, 0);
transform: translate3d(-100%, 0, 0);
pointer-events: none;
}
#loading-animation.animate {
z-index: 10000000;
opacity: 0;
-webkit-transition: -webkit-transform 5s ease-in, opacity 1s 5s;
transition: transform 5s ease-in, opacity 1s 5s;
-webkit-transform: translate3d(0%, 0, 0);
transform: translate3d(0%, 0, 0);
}
/* Custom settings for intro-js */
.custom .introjs-button {
font-weight: bold;
text-transform: uppercase;
}
.custom .introjs-prevbutton {
display: none ;
}
.custom .introjs-skipbutton {
border-radius: 0;
color: $color-theme-primary;
background-color: $color-white;
background-image: none;
border: none;
}
.disabled-next .introjs-nextbutton {
display: none;
}
.introjs-overlay {
z-index: 0 !important;
}
.introjs-helperLayer {
z-index: 0 !important;
}
.introjs-no-overlay {
z-index: -1 !important;
}
.introjs-showElement.send-to-back {
z-index: 1 !important;
}
.introjs-tooltipReferenceLayer:not(.bring-to-front) {
z-index: 999999 !important;
}

View file

@ -0,0 +1,3 @@
// Place all the styles related to the UserMyModules controller here.
// They will automatically be included in application.css.
// You can use Sass (SCSS) here: http://sass-lang.com/

View file

@ -0,0 +1,40 @@
class ActivitiesController < ApplicationController
before_filter :load_vars
def index
@per_page = 10
@activities = current_user.last_activities(@last_activity_id,
@per_page)
# Whether to hide date labels
@hide_today = params.include? :from
@day = @last_activity.present? ?
@last_activity.created_at.strftime("%j").to_i :
366
more_url = url_for(activities_url(format: :json,
from: @activities.last.id))
respond_to do |format|
format.json {
render :json => {
per_page: @per_page,
activities_number: @activities.length,
next_url: more_url,
html: render_to_string({
partial: 'index.html.erb',
locals: {
more_activities_url: more_url,
hide_today: @hide_today,
day: @day
}
})
}
}
end
end
def load_vars
@last_activity_id = params[:from].to_i || 0
@last_activity = Activity.find_by_id(@last_activity_id)
end
end

View file

@ -0,0 +1,71 @@
class ApplicationController < ActionController::Base
include PermissionHelper
include FirstTimeDataGenerator
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :exception
before_action :authenticate_user!
before_action :generate_intro_tutorial, if: :is_current_page_root?
around_action :set_time_zone, if: :current_user
layout "main"
def forbidden
render_403
end
def not_found
render_404
end
def is_current_page_root?
controller_name == "projects" && action_name == "index"
end
protected
def log(message)
if @my_module
@my_module.log(message)
elsif @project
@project.log(message)
elsif @organization
@organization.log(message)
else
logger.error(message)
end
end
def render_403
render :file => 'public/403.html', :status => :forbidden, :layout => false
end
def render_404
render :file => 'public/404.html', :status => :not_found, :layout => false
end
private
def generate_intro_tutorial
if Rails.configuration.x.enable_tutorial &&
current_user.no_tutorial_done? &&
current_user.organizations.where(created_by: current_user).count > 0 then
demo_cookie = seed_demo_data current_user
cookies[:tutorial_data] = {
value: demo_cookie,
expires: 1.week.from_now
}
current_user.update(tutorial_status: 1)
end
end
# With this Devise callback user is redirected directly to sign in page instead
# of to root path. Therefore notification for sign out is displayed.
def after_sign_out_path_for(resource_or_scope)
new_user_session_path
end
def set_time_zone(&block)
Time.use_zone(current_user.time_zone, &block)
end
end

View file

@ -0,0 +1,125 @@
class AssetsController < ApplicationController
before_action :load_vars, except: [:signature]
before_action :check_read_permission, except: [:signature]
def signature
respond_to do |format|
format.json {
if params[:asset_id]
asset = Asset.find_by_id params[:asset_id]
asset.file.destroy
asset.file_empty params[:file_name], params[:file_size]
else
asset = Asset.new_empty params[:file_name], params[:file_size]
end
if not asset.valid?
errors = Hash[asset.errors.map{|k,v| ["asset.#{k}",v]}]
render json: {
status: 'error',
errors: errors
}
else
asset.save!
posts = generate_upload_posts asset
render json: {
asset_id: asset.id,
posts: posts
}
end
}
end
end
def preview
if @asset.is_image?
url = @asset.file.url :medium
redirect_to url, status: 307
else
render_400
end
end
def download
if @asset.file.is_stored_on_s3?
redirect_to @asset.presigned_url, status: 307
else
send_file @asset.file.path, filename: @asset.file_file_name,
type: @asset.file_content_type
end
end
private
def load_vars
@asset = Asset.find_by_id(params[:id])
unless @asset
render_404
end
step_assoc = @asset.step
result_assoc = @asset.result
@assoc = step_assoc if not step_assoc.nil?
@assoc = result_assoc if not result_assoc.nil?
@my_module = @assoc.my_module
@project = @my_module.project
end
def check_read_permission
if @assoc.class == Step
unless can_download_step_assets(@my_module)
render_403
end
elsif @assoc.class == Result
unless can_download_result_assets(@my_module)
render_403
end
end
end
def generate_upload_posts(asset)
posts = []
s3_post = S3_BUCKET.presigned_post(
key: asset.file.path[1..-1],
success_action_status: '201',
acl: 'private',
storage_class: "STANDARD",
content_length_range: 1..(1024*1024*50),
content_type: asset.file_content_type
)
posts.push({
url: s3_post.url,
fields: s3_post.fields
})
if (asset.file_content_type =~ /^image\//) == 0
asset.file.options[:styles].each do |style, option|
s3_post = S3_BUCKET.presigned_post(
key: asset.file.path(style)[1..-1],
success_action_status: '201',
acl: 'public-read',
storage_class: "REDUCED_REDUNDANCY",
content_length_range: 1..(1024*1024*50),
content_type: asset.file_content_type
)
posts.push({
url: s3_post.url,
fields: s3_post.fields,
style_option: option,
mime_type: asset.file_content_type
})
end
end
posts
end
end

View file

@ -0,0 +1,277 @@
class CanvasController < ApplicationController
before_action :load_vars
before_action :check_view_canvas, only: [:edit, :full_zoom, :medium_zoom, :small_zoom]
before_action :check_edit_canvas, only: [:edit, :update]
def edit
render partial: 'canvas/edit',
locals: { project: @project, my_modules: @my_modules },
:content_type => 'text/html'
end
def full_zoom
render partial: 'canvas/full_zoom',
locals: { project: @project, my_modules: @my_modules },
:content_type => 'text/html'
end
def medium_zoom
render partial: 'canvas/medium_zoom',
locals: { project: @project, my_modules: @my_modules },
:content_type => 'text/html'
end
def small_zoom
render partial: 'canvas/small_zoom',
locals: { project: @project, my_modules: @my_modules },
:content_type => 'text/html'
end
def update
error = false
# Make sure that remove parameter is valid
to_archive = []
if can_archive_modules(@project) and
update_params[:remove].present? then
to_archive = update_params[:remove].split(",")
unless to_archive.all? { |id| is_int? id }
error = true
else
to_archive.collect! { |id| id.to_i }
end
end
if error then
render_403 and return
end
# Make sure connections parameter is valid
connections = []
if can_edit_connections(@project) and
update_params[:connections].present? then
conns = update_params[:connections].split(",")
unless conns.length % 2 == 0 and
conns.all? { |c| c.is_a? String } then
error = true
else
conns.each_slice(2).each do |c|
connections << [c[0], c[1]]
end
end
end
if error then
render_403 and return
end
# Make sure positions parameter is valid
positions = Hash.new
if can_reposition_modules(@project) and
update_params[:positions].present? then
poss = update_params[:positions].split(";")
(poss.collect { |pos| pos.split(",") }).each do |pos|
unless (pos.length == 3 and
pos[0].is_a? String and
is_int? pos[1] and
is_int? pos[2])
error = true
break
end
x = pos[1].to_i
y = pos[2].to_i
# Multiple modules cannot have same position
if positions.any? { |k,v| v[:x] == x and v[:y] == y} then
error = true
break
end
positions[pos[0]] = { x: x, y: y }
end
end
if error then
render_403 and return
end
# Make sure that to_add is an array of strings,
# as well as that positions for newly added modules exist
to_add = []
if can_create_modules(@project) and
update_params[:add].present? and
update_params["add-names"].present? then
ids = update_params[:add].split(",")
names = update_params["add-names"].split("|")
unless ids.length == names.length and
ids.all? { |id| id.is_a? String and positions.include? id } and
names.all? { |name| name.is_a? String }
error = true
else
ids.each_with_index do |id, i|
to_add << {
id: id,
name: names[i],
x: positions[id][:x],
y: positions[id][:y]
}
end
end
end
if error then
render_403 and return
end
# Make sure rename parameter is valid
to_rename = Hash.new
if can_edit_modules(@project) and
update_params[:rename].present? then
begin
to_rename = JSON.parse(update_params[:rename])
# Okay, JSON parsed!
unless (
to_rename.is_a? Hash and
to_rename.keys.all? { |k| k.is_a? String } and
to_rename.values.all? { |k| k.is_a? String }
)
error = true
end
rescue
error = true
end
end
if error then
render_403 and return
end
# Make sure that to_clone is an array of pairs,
# as well as that all IDs exist
to_clone = Hash.new
if can_clone_modules(@project) and
update_params[:cloned].present? then
clones = update_params[:cloned].split(";")
(clones.collect { |v| v.split(",") }).each do |val|
unless (val.length == 2 and
is_int? val[0] and
val[1].is_a? String and
to_add.any? { |m| m[:id] == val[1] })
error = true
break
else
to_clone[val[1]] = val[0]
end
end
end
if error then
render_403 and return
end
module_groups = Hash.new
if can_edit_module_groups(@project) and
update_params["module-groups"].present? then
begin
module_groups = JSON.parse(update_params["module-groups"])
# Okay, JSON parsed!
unless (
module_groups.is_a? Hash and
module_groups.keys.all? { |k| k.is_a? String } and
module_groups.values.all? { |k| k.is_a? String }
)
error = true
end
rescue
error = true
end
end
if error then
render_403 and return
end
# Call the "master" function to do all the updating for us
unless @project.update_canvas(
to_archive,
to_add,
to_rename,
to_clone,
connections,
positions,
current_user,
module_groups
)
render_403 and return
end
# Save activities that modules were archived
to_archive.each do |module_id|
my_module = MyModule.find_by_id(module_id)
unless my_module.blank?
Activity.create(
type_of: :archive_module,
project: my_module.project,
my_module: my_module,
user: current_user,
message: t(
'activities.archive_module',
user: current_user.full_name,
module: my_module.name
)
)
end
end
flash[:success] = t(
"projects.canvas.update.success_flash")
redirect_to canvas_project_path(@project)
end
private
def update_params
params.permit(
:id,
:connections,
:positions,
:add,
"add-names",
:rename,
:cloned,
:remove,
"module-groups"
)
end
def load_vars
@project = Project.find_by_id(params[:id])
unless @project
respond_to do |format|
format.html { render_404 and return }
format.any(:xml, :json, :js) { render(json: { redirect_url: not_found_url }, status: :not_found) and return }
end
end
@my_modules = @project.active_modules
end
def check_edit_canvas
unless can_edit_canvas(@project)
render_403 and return
end
end
def check_view_canvas
unless can_view_project(@project)
render_403 and return
end
end
# Check if given value is "integer" string (e.g. "15")
def is_int?(val)
/\A[-+]?\d+\z/ === val
end
end

View file

View file

@ -0,0 +1,49 @@
module SampleActions
extend ActiveSupport::Concern
include PermissionHelper
def delete_samples
check_destroy_samples_permissions
if params[:sample_ids].present?
counter_user = 0
counter_other_users = 0
params[:sample_ids].each do |id|
sample = Sample.find_by_id(id)
if sample and can_delete_sample(sample)
sample.destroy
counter_user += 1
else
counter_other_users += 1
end
end
if counter_user > 0
if counter_other_users > 0
flash[:success] = t("samples.destroy.contains_other_samples_flash",
sample_number: counter_user, other_samples_number: counter_other_users)
else
flash[:success] = t("samples.destroy.success_flash",
sample_number: counter_user)
end
else
flash[:notice] = t("samples.destroy.no_deleted_samples_flash",
other_samples_number: counter_other_users)
end
else
flash[:notice] = t("samples.destroy.no_sample_selected_flash")
end
if params[:controller] == "my_modules"
redirect_to samples_my_module_path(@my_module)
elsif params[:controller] == "projects"
redirect_to samples_project_path(@project)
end
end
def check_destroy_samples_permissions
unless can_delete_samples(@project.organization)
render_403
end
end
end

View file

@ -0,0 +1,47 @@
class CustomFieldsController < ApplicationController
before_action :load_vars_nested, only: [:create]
before_action :check_create_permissions, only: [:create]
def create
@custom_field = CustomField.new(custom_field_params)
@custom_field.organization = @organization
@custom_field.user = current_user
respond_to do |format|
if @custom_field.save
flash[:success] = t(
"custom_fields.create.success_flash",
custom_field: @custom_field.name,
organization: @organization.name
)
format.json {
render json: {
id: @custom_field.id
},
status: :ok }
else
format.json { render json: @custom_field.errors, status: :unprocessable_entity }
end
end
end
private
def load_vars_nested
@organization = Organization.find_by_id(params[:organization_id])
unless @organization
render_404
end
end
def check_create_permissions
unless can_create_custom_field_in_organization(@organization)
render_403
end
end
def custom_field_params
params.require(:custom_field).permit(:name)
end
end

View file

@ -0,0 +1,108 @@
class MyModuleCommentsController < ApplicationController
before_action :load_vars
before_action :check_view_permissions, only: [ :index ]
before_action :check_add_permissions, only: [ :new, :create ]
def index
@comments = @my_module.last_comments(@last_comment_id, @per_page)
respond_to do |format|
format.json {
# 'index' partial includes header and form for adding new
# messages. 'list' partial is used for showing more
# comments.
partial = "index.html.erb"
partial = "list.html.erb" if @last_comment_id > 0
more_url = ""
if @comments.count > 0
more_url = url_for(my_module_my_module_comments_url(@my_module,
format: :json,
from: @comments.last.id))
end
render :json => {
per_page: @per_page,
results_number: @comments.length,
more_url: more_url,
html: render_to_string({
partial: partial,
locals: {
comments: @comments,
more_comments_url: more_url
}
})
}
}
end
end
def new
@comment = Comment.new(
user: current_user
)
end
def create
@comment = Comment.new(
message: comment_params[:message],
user: current_user)
respond_to do |format|
if (@comment.valid? && @my_module.comments << @comment)
format.html {
flash[:success] = t(
"my_module_comments.create.success_flash",
module: @my_module.name)
redirect_to session.delete(:return_to)
}
format.json {
render json: {
html: render_to_string({
partial: "comment.html.erb",
locals: {
comment: @comment
}
})
},
status: :created
}
else
response.status = 400
format.html { render :new }
format.json {
render json: {
errors: @comment.errors.to_hash(true)
}
}
end
end
end
private
def load_vars
@last_comment_id = params[:from].to_i
@per_page = 10
@my_module = MyModule.find_by_id(params[:my_module_id])
unless @my_module
render_404
end
end
def check_view_permissions
unless can_view_module_comments(@my_module)
render_403
end
end
def check_add_permissions
unless can_add_comment_to_module(@my_module)
render_403
end
end
def comment_params
params.require(:comment).permit(:message)
end
end

View file

@ -0,0 +1,159 @@
class MyModuleTagsController < ApplicationController
before_action :load_vars
before_action :check_view_permissions, only: [:index_edit, :index]
before_action :check_create_permissions, only: [:new, :create]
before_action :check_destroy_permissions, only: [:destroy]
def index_edit
@my_module_tags = @my_module.my_module_tags
@unassigned_tags = @my_module.unassigned_tags
@new_mmt = MyModuleTag.new(my_module: @my_module)
@new_tag = Tag.new(project: @my_module.project)
respond_to do |format|
format.json {
render :json => {
:my_module => @my_module,
:html => render_to_string({
:partial => "index_edit.html.erb"
})
}
}
end
end
def index
respond_to do |format|
format.json {
render json: {
html_canvas: render_to_string(
partial: "canvas/tags.html.erb",
locals: { my_module: @my_module }
),
html_module_header: render_to_string(
partial: "my_modules/tags.html.erb",
locals: { my_module: @my_module }
)
}
}
end
end
def new
session[:return_to] ||= request.referer
@mt = MyModuleTag.new(my_module: @my_module)
init_gui
end
def create
@mt = MyModuleTag.new(mt_params.merge(my_module: @my_module))
@mt.created_by = current_user
if @mt.save
flash_success = t(
"my_module_tags.create.success_flash",
tag: @mt.tag.name,
module: @mt.my_module.name)
respond_to do |format|
format.html {
flash[:success] = flash_success
redirect_to session.delete(:return_to)
}
format.json {
redirect_to my_module_tags_edit_path(format: :json), :status => 303
}
end
else
flash_error = t(
"my_module_tags.create.error_flash",
module: @mt.my_module.name)
respond_to do |format|
format.html {
flash[:error] = flash_error
init_gui
render :new
}
format.json {
# TODO
redirect_to my_module_tags_edit_path(format: :json), :status => 303
}
end
end
end
def destroy
session[:return_to] ||= request.referer
@mt = MyModuleTag.find_by_id(params[:id])
if @mt.present? and @mt.destroy
flash_success = t(
"my_module_tags.destroy.success_flash",
tag: @mt.tag.name,
module: @mt.my_module.name)
respond_to do |format|
format.html {
flash[:success] = flash_success
redirect_to session.delete(:return_to)
}
format.json {
redirect_to my_module_tags_edit_path(format: :json), :status => 303
}
end
else
flash_success = t(
"my_module_tags.destroy.error_flash",
tag: @mt.tag.name,
module: @mt.my_module.name)
respond_to do |format|
format.html {
flash[:error] = flash_error
redirect_to session.delete(:return_to)
}
format.json {
# TODO
redirect_to my_module_tags_edit_path(format: :json), :status => 303
}
end
end
end
private
def load_vars
@my_module = MyModule.find_by_id(params[:my_module_id])
unless @my_module
render_404
end
end
def check_view_permissions
unless can_edit_tags_for_module(@my_module)
render_403
end
end
def check_create_permissions
unless can_add_tag_to_module(@my_module)
render_403
end
end
def check_destroy_permissions
unless can_remove_tag_from_module(@my_module)
render_403
end
end
def init_gui
@tags = @my_module.unassigned_tags
end
def mt_params
params.require(:my_module_tag).permit(:my_module_id, :tag_id)
end
end

View file

@ -0,0 +1,388 @@
class MyModulesController < ApplicationController
include SampleActions
before_action :load_vars, only: [
:show, :edit, :update, :destroy,
:description, :due_date, :steps, :results,
:samples, :activities, :activities_tab,
:assign_samples, :unassign_samples,
:delete_samples,
:samples_index, :archive]
before_action :load_markdown, only: [ :results ]
before_action :load_vars_nested, only: [:new, :create]
before_action :check_edit_permissions, only: [
:edit, :update, :description, :due_date
]
before_action :check_destroy_permissions, only: [:destroy]
before_action :check_view_info_permissions, only: [:show]
before_action :check_view_activities_permissions, only: [:activities, :activities_tab]
before_action :check_view_steps_permissions, only: [:steps]
before_action :check_view_results_permissions, only: [:results]
before_action :check_view_samples_permissions, only: [:samples, :samples_index]
before_action :check_view_archive_permissions, only: [:archive]
before_action :check_assign_samples_permissions, only: [:assign_samples]
before_action :check_unassign_samples_permissions, only: [:unassign_samples]
layout "fluid"
# Define submit actions constants (used in routing)
ASSIGN_SAMPLES = 'Assign'
UNASSIGN_SAMPLES = 'Unassign'
# Action defined in SampleActions
DELETE_SAMPLES = 'Delete'
def show
respond_to do |format|
format.html
format.json {
render :json => {
:html => render_to_string({
:partial => "show.html.erb"
})
}
}
end
end
# Description modal window in full-zoom canvas
def description
respond_to do |format|
format.html
format.json {
render json: {
html: render_to_string({
partial: "description.html.erb"
}),
title: t("my_modules.description.title", module: @my_module.name)
}
}
end
end
def activities
@last_activity_id = params[:from].to_i || 0
@per_page = 10
@activities = @my_module.last_activities(@last_activity_id, @per_page)
@more_activities_url = ""
if @activities.count > 0
@more_activities_url = url_for(
controller: 'my_modules',
action: 'activities',
format: :json,
from: @activities.last.id)
end
respond_to do |format|
format.html
format.json {
# 'activites' partial includes header and form for adding older
# activities. 'list' partial is used for showing more activities.
partial = "activities.html.erb"
if @activities.last.id > 0
partial = "my_modules/activities/list_activities.html.erb"
end
render :json => {
:per_page => @per_page,
:results_number => @activities.length,
:more_url => @more_activities_url,
:html => render_to_string({
:partial => partial
})
}
}
end
end
# Different controller for showing activities inside tab
def activities_tab
@activities = @my_module.last_activities(1, @per_page)
respond_to do |format|
format.html
format.json {
render :json => {
:html => render_to_string({
:partial => "activities.html.erb"
})
}
}
end
end
# Due date modal window in full-zoom canvas
def due_date
respond_to do |format|
format.html
format.json {
render json: {
html: render_to_string({
partial: "due_date.html.erb"
}),
title: t("my_modules.due_date.title", module: @my_module.name)
}
}
end
end
def edit
session[:return_to] ||= request.referer
end
def update
@my_module.assign_attributes(my_module_params)
@my_module.last_modified_by = current_user
description_changed = @my_module.description_changed?
if @my_module.archived_changed?(from: false, to: true)
saved = @my_module.archive(current_user)
if saved
# Currently not in use
Activity.create(
type_of: :archive_module,
project: @my_module.project,
my_module: @my_module,
user: current_user,
message: t(
'activities.archive_module',
user: current_user.full_name,
module: @my_module.name
)
)
end
elsif @my_module.archived_changed?(from: true, to: false)
saved = @my_module.restore(current_user)
if saved
Activity.create(
type_of: :restore_module,
project: @my_module.project,
my_module: @my_module,
user: current_user,
message: t(
'activities.restore_module',
user: current_user.full_name,
module: @my_module.name
)
)
end
else
saved = @my_module.save
if saved and description_changed then
Activity.create(
type_of: :change_module_description,
project: @my_module.project,
my_module: @my_module,
user: current_user,
message: t(
"activities.change_module_description",
user: current_user.full_name,
module: @my_module.name
)
)
end
end
respond_to do |format|
if saved
format.html {
flash[:success] = t("my_modules.update.success_flash",
module: @my_module.name)
redirect_to(:back)
}
format.json {
alerts = []
alerts << "alert-red" if @my_module.is_overdue?
alerts << "alert-yellow" if @my_module.is_one_day_prior?
render json: {
status: :ok,
due_date_label: render_to_string(
partial: "my_modules/due_date_label.html.erb",
locals: { my_module: @my_module }
),
module_header_due_date_label: render_to_string(
partial: "my_modules/module_header_due_date_label.html.erb",
locals: { my_module: @my_module }
),
description_label: render_to_string(
partial: "my_modules/description_label.html.erb",
locals: { my_module: @my_module }
),
alerts: alerts
}
}
else
format.html {
render :edit
}
format.json {
render json: @project.errors,
status: :unprocessable_entity
}
end
end
end
def steps
end
def results
end
def samples
@samples_index_link = samples_index_my_module_path(@my_module, format: :json)
@organization = @my_module.project.organization
end
def archive
@archived_results = @my_module.archived_results
end
# Submit actions
def assign_samples
if params[:sample_ids].present?
samples = []
params[:sample_ids].each do |id|
sample = Sample.find_by_id(id)
sample.last_modified_by = current_user
sample.save
if sample
samples << sample
end
end
@my_module.get_downstream_modules.each do |my_module|
new_samples = samples.select { |el| my_module.samples.exclude?(el) }
my_module.samples.push(*new_samples)
end
end
redirect_to samples_my_module_path(@my_module)
end
def unassign_samples
if params[:sample_ids].present?
samples = []
params[:sample_ids].each do |id|
sample = Sample.find_by_id(id)
sample.last_modified_by = current_user
sample.save
if sample
samples << sample
end
end
@my_module.get_downstream_modules.each do |my_module|
my_module.samples.delete(samples & my_module.samples)
end
end
redirect_to samples_my_module_path(@my_module)
end
# AJAX actions
def samples_index
@organization = @my_module.project.organization
respond_to do |format|
format.html
format.json {
render json: ::SampleDatatable.new(view_context, @organization, nil, @my_module)
}
end
end
private
def load_vars
@direct_upload = ENV['PAPERCLIP_DIRECT_UPLOAD']
@my_module = MyModule.find_by_id(params[:id])
if @my_module
@project = @my_module.project
else
render_404
end
end
# Initialize markdown parser
def load_markdown
@markdown = Redcarpet::Markdown.new(
Redcarpet::Render::HTML.new(
filter_html: true,
no_images: true
)
)
end
def check_edit_permissions
unless can_edit_module(@my_module)
render_403
end
end
def check_destroy_permissions
unless can_archive_module(@my_module)
render_403
end
end
def check_view_info_permissions
unless can_view_module_info(@my_module)
render_403
end
end
def check_view_activities_permissions
unless can_view_module_activities(@my_module)
render_403
end
end
def check_view_steps_permissions
unless can_view_steps_in_module(@my_module)
render_403
end
end
def check_view_results_permissions
unless can_view_results_in_module(@my_module)
render_403
end
end
def check_view_samples_permissions
unless can_view_module_samples(@my_module)
render_403
end
end
def check_view_archive_permissions
unless can_view_module_archive(@my_module)
render_403
end
end
def check_assign_samples_permissions
unless can_add_samples_to_module(@my_module)
render_403
end
end
def check_unassign_samples_permissions
unless can_delete_samples_from_module(@my_module)
render_403
end
end
def my_module_params
params.require(:my_module).permit(:name, :description, :due_date,
:archived)
end
end

View file

@ -0,0 +1,303 @@
class OrganizationsController < ApplicationController
before_action :load_vars, only: [:parse_sheet, :import_samples, :export_samples]
before_action :check_create_sample_permissions, only: [:parse_sheet, :import_samples]
before_action :check_view_samples_permission, only: [:export_samples]
FILE_SIZE_LIMIT = 50 * 1024 * 1024
def parse_sheet
session[:return_to] ||= request.referer
respond_to do |format|
if params[:file]
begin
if params[:file].size > FILE_SIZE_LIMIT
error = t("organizations.parse_sheet.errors.file_size_exceeded")
format.html {
flash[:alert] = error
redirect_to session.delete(:return_to)
}
format.json {
render json: {message: error},
status: :unprocessable_entity
}
else
sheet = Organization.open_spreadsheet(params[:file])
# Check if we actually have any rows (last_row > 1)
if sheet.last_row.between?(0, 1)
flash[:notice] = t(
"organizations.parse_sheet.errors.empty_file")
redirect_to session.delete(:return_to) and return
end
# Get data (it will trigger any errors as well)
@header = sheet.row(1)
@rows = [];
@rows << Hash[[@header, sheet.row(2)].transpose]
# Fill in fields for dropdown
@available_fields = @organization.get_available_sample_fields
# Save file for next step (importing)
@temp_file = TempFile.new(
session_id: session.id,
file: params[:file]
)
if @temp_file.save
# format.html
format.json {
render :json => {
:html => render_to_string({
:partial => "samples/parse_samples_modal.html.erb"
})
}
}
else
error = t("organizations.parse_sheet.errors.temp_file_failure")
format.html {
flash[:alert] = error
redirect_to session.delete(:return_to)
}
format.json {
render json: {message: error},
status: :unprocessable_entity
}
end
end
rescue ArgumentError, CSV::MalformedCSVError
error = t("organizations.parse_sheet.errors.invalid_file")
format.html {
flash[:alert] = error
redirect_to session.delete(:return_to)
}
format.json {
render json: {message: error},
status: :unprocessable_entity
}
rescue TypeError
error = t("organizations.parse_sheet.errors.invalid_extension")
format.html {
flash[:alert] = error
redirect_to session.delete(:return_to)
}
format.json {
render json: {message: error},
status: :unprocessable_entity
}
end
else
error = t("organizations.parse_sheet.errors.no_file_selected")
format.html {
flash[:alert] = error
session[:return_to] ||= request.referer
redirect_to session.delete(:return_to)
}
format.json {
render json: {message: error},
status: :unprocessable_entity
}
end
end
end
def import_samples
session[:return_to] ||= request.referer
respond_to do |format|
if params[:file_id]
@temp_file = TempFile.find_by_id(params[:file_id])
if @temp_file
# Check if session_id is equal to prevent file stealing
if @temp_file.session_id == session.id
# Check if mappings exists or else we don't have anything to parse
if params[:mappings]
@sheet = Organization.open_spreadsheet(@temp_file.file)
# Check for duplicated values
h1 = params[:mappings].clone.delete_if { |k, v| v.empty? }
if h1.length == h1.invert.length
# Check if there exist mapping for sample name (it's mandatory)
if params[:mappings].has_value?("-1")
result = @organization.import_samples(@sheet, params[:mappings], current_user)
nr_of_added = result[:nr_of_added]
total_nr = result[:total_nr]
if result[:status] == :ok
# If no errors are present, redirect back
# to samples table
flash[:success] = t(
"organizations.import_samples.success_flash",
nr: nr_of_added,
samples: t(
"organizations.import_samples.sample",
count: total_nr
)
)
@temp_file.destroy
format.html {
redirect_to session.delete(:return_to)
}
format.json {
flash.keep(:success)
render json: { status: :ok }
}
else
# Otherwise, also redirect back,
# but display different message
flash[:alert] = t(
"organizations.import_samples.partial_success_flash",
nr: nr_of_added,
samples: t(
"organizations.import_samples.sample",
count: total_nr
)
)
@temp_file.destroy
format.html {
redirect_to session.delete(:return_to)
}
format.json {
flash.keep(:alert)
render json: { status: :unprocessable_entity }
}
end
else
# This is currently the only AJAX error response
flash_alert = t(
"organizations.import_samples.errors.no_sample_name")
format.html {
flash[:alert] = flash_alert
redirect_to session.delete(:return_to)
}
format.json {
render json: {
html: render_to_string({
partial: "parse_error.html.erb",
locals: { error: flash_alert }
})
},
status: :unprocessable_entity
}
end
else
# This code should never execute unless user tampers with
# JS (selects same column in more than one dropdown)
flash_alert = t(
"organizations.import_samples.errors.duplicated_values")
format.html {
flash[:alert] = flash_alert
redirect_to session.delete(:return_to)
}
format.json {
render json: {
html: render_to_string({
partial: "parse_error.html.erb",
locals: { error: flash_alert }
})
},
status: :unprocessable_entity
}
end
else
@temp_file.destroy
flash[:alert] = t(
"organizations.import_samples.errors.no_data_to_parse")
format.html {
redirect_to session.delete(:return_to)
}
format.json {
flash.keep(:alert)
render json: { status: :unprocessable_entity }
}
end
else
@temp_file.destroy
flash[:alert] = t(
"organizations.import_samples.errors.session_expired")
format.html {
redirect_to session.delete(:return_to)
}
format.json {
flash.keep(:alert)
render json: { status: :unprocessable_entity }
}
end
else
# No temp file to begin with, so no need to destroy it
flash[:alert] = t(
"organizations.import_samples.errors.temp_file_not_found")
format.html {
redirect_to session.delete(:return_to)
}
format.json {
flash.keep(:alert)
render json: { status: :unprocessable_entity }
}
end
else
flash[:alert] = t(
"organizations.import_samples.errors.temp_file_not_found")
format.html {
redirect_to session.delete(:return_to)
}
format.json {
flash.keep(:alert)
render json: { status: :unprocessable_entity }
}
end
end
end
def export_samples
require "csv"
respond_to do |format|
if params[:sample_ids].present? and params[:header_ids].present?
samples = []
params[:sample_ids].each do |id|
sample = Sample.find_by_id(id)
if sample
samples << sample
end
end
format.csv { send_data @organization.to_csv(samples, params[:header_ids]) }
else
format.csv { render nothing: true }
end
end
end
def load_vars
@organization = Organization.find_by_id(params[:id])
unless @organization
render_404
end
end
def check_create_sample_permissions
unless can_create_samples(@organization)
render_403
end
end
def check_view_samples_permission
unless can_view_samples(@organization)
render_403
end
end
def routing_error(error = 'Routing error', status = :not_found, exception=nil)
redirect_to root_path
end
end

View file

@ -0,0 +1,37 @@
class ProjectActivitiesController < ApplicationController
before_action :load_vars, only: [ :index ]
before_action :check_view_permissions, only: [ :index ]
def index
@activities = @project.last_activities
respond_to do |format|
format.html {
render :index, layout: "fluid"
}
format.json {
render :json => {
:html => render_to_string({
:partial => "index.html.erb"
})
}
}
end
end
private
def load_vars
@project = Project.find_by_id(params[:project_id])
unless @project
render_404
end
end
def check_view_permissions
unless can_view_project_activities(@project)
render_403
end
end
end

View file

@ -0,0 +1,106 @@
class ProjectCommentsController < ApplicationController
before_action :load_vars
before_action :check_view_permissions, only: [ :index ]
before_action :check_add_permissions, only: [ :new, :create ]
def index
@comments = @project.last_comments(@last_comment_id, @per_page)
respond_to do |format|
format.json {
# 'index' partial includes header and form for adding new
# messages. 'list' partial is used for showing more
# comments.
partial = "index.html.erb"
partial = "list.html.erb" if @last_comment_id > 0
more_url = ""
if @comments.count > 0
more_url = url_for(project_project_comments_url(format: :json,
from: @comments.last.id))
end
render :json => {
:per_page => @per_page,
:results_number => @comments.length,
:more_url => more_url,
:html => render_to_string({
:partial => partial,
:locals => {
:comments => @comments,
:more_comments_url => more_url
}
})
}
}
end
end
def new
@comment = Comment.new(
user: current_user
)
end
def create
@comment = Comment.new(
message: comment_params[:message],
user: current_user)
respond_to do |format|
if (@comment.valid? && @project.comments << @comment)
format.html {
flash[:success] = t(
"project_comments.create.success_flash",
project: @project.name)
redirect_to projects_path
}
format.json {
render json: {
html: render_to_string({
partial: 'comment.html.erb',
locals: {
comment: @comment
}
})
}, status: :created
}
else
response.status = 400
format.html { render :new }
format.json {
render json: {
errors: @comment.errors.to_hash(true)
}
}
end
end
end
private
def load_vars
@last_comment_id = params[:from].to_i
@per_page = 10
@project = Project.find_by_id(params[:project_id])
unless @project
render_404
end
end
def check_view_permissions
unless can_view_project_comments(@project)
render_403
end
end
def check_add_permissions
unless can_add_comment_to_project(@project)
render_403
end
end
def comment_params
params.require(:comment).permit(:message)
end
end

View file

@ -0,0 +1,329 @@
class ProjectsController < ApplicationController
include SampleActions
before_action :load_vars, only: [:show, :edit, :update, :canvas,
:notifications, :reports,
:samples, :module_archive,
:delete_samples, :samples_index]
before_action :check_view_permissions, only: [:show, :canvas, :reports,
:samples, :module_archive,
:samples_index]
before_action :check_view_notifications_permissions, only: [ :notifications ]
before_action :check_edit_permissions, only: [ :edit ]
before_action :check_module_archive_permissions, only: [:module_archive]
before_action :check_canvas_permissions, only: [:workflow]
filter_by_archived = false
# except parameter could be used but it is not working.
layout :choose_layout
# Action defined in SampleActions
DELETE_SAMPLES = I18n.t("samples.delete_samples")
def index
@current_organization_id = params[:organization].to_i
@current_sort = params[:sort].to_s
@projects_by_orgs = current_user.projects_by_orgs(
@current_organization_id, @current_sort, @filter_by_archived)
@organizations = current_user.organizations
# New project for create new project modal
@project = Project.new
end
def archive
@filter_by_archived = true
index
end
def new
@project = Project.new
@organizations = current_user.organizations
end
def create
@project = Project.new(project_params)
@project.created_by = current_user
@project.last_modified_by = current_user
if @project.save
# Create user-project association
up = UserProject.new(
role: :owner,
user: current_user,
project: @project
)
up.save
# Create "project created" activity
Activity.create(
type_of: :create_project,
user: current_user,
project: @project,
message: t(
"activities.create_project",
user: current_user.full_name,
project: @project.name
)
)
flash[:success] = t("projects.create.success_flash", name: @project.name)
respond_to do |format|
format.json {
render json: { url: projects_path }, status: :ok
}
end
else
respond_to do |format|
format.json {
render json: @project.errors, status: :unprocessable_entity
}
end
end
end
def edit
respond_to do |format|
format.json {
render json: {
html: render_to_string({
partial: "edit.html.erb",
locals: { project: @project }
}),
title: t("projects.index.modal_edit_project.modal_title", project: @project.name)
}
}
end
end
def update
return_error = false
flash_error = t('projects.update.error_flash', name: @project.name)
# Check archive permissions if archiving/restoring
if project_params.include? :archive
if (project_params[:archive] and !can_archive_project(@project)) or
(!project_params[:archive] and !can_restore_project(@project))
return_error = true
is_archive = URI(request.referer).path == projects_archive_path ? "restore" : "archive"
flash_error = t("projects.#{is_archive}.error_flash", name: @project.name)
end
end
message_renamed = nil
message_visibility = nil
if project_params.include? :name and
project_params[:name] != @project.name then
message_renamed = t(
"activities.rename_project",
user: current_user.full_name,
project_old: @project.name,
project_new: project_params[:name]
)
end
if project_params.include? :visibility and
project_params[:visibility] != @project.visibility then
message_visibility = t(
"activities.change_project_visibility",
user: current_user.full_name,
project: @project.name,
visibility: project_params[:visibility] == "visible" ?
t("general.public") :
t("general.private")
)
end
@project.last_modified_by = current_user
if @project.update(project_params)
# Add activities if needed
if message_renamed.present?
Activity.create(
type_of: :rename_project,
user: current_user,
project: @project,
message: message_renamed
)
end
if message_visibility.present?
Activity.create(
type_of: :change_project_visibility,
user: current_user,
project: @project,
message: message_visibility
)
end
flash_success = t('projects.update.success_flash', name: @project.name)
respond_to do |format|
format.html {
# Redirect URL for archive view is different as for other views.
if URI(request.referer).path == projects_archive_path
# The project should be restored
unless @project.archived
@project.restore(current_user)
# "Restore project" activity
Activity.create(
type_of: :restore_project,
user: current_user,
project: @project,
message: t(
"activities.restore_project",
user: current_user.full_name,
project: @project.name
)
)
flash_success = t('projects.restore.success_flash',
name: @project.name)
end
redirect_to projects_archive_path
else
# The project should be archived
if @project.archived
@project.archive(current_user)
# "Archive project" activity
Activity.create(
type_of: :archive_project,
user: current_user,
project: @project,
message: t(
"activities.archive_project",
user: current_user.full_name,
project: @project.name
)
)
flash_success = t('projects.archive.success_flash', name: @project.name)
end
redirect_to projects_path
end
flash[:success] = flash_success
}
format.json {
render json: {
status: :ok,
html: render_to_string({
partial: "projects/index/project.html.erb",
locals: { project: @project }
})
}
}
end
else
return_error = true
end
if return_error then
respond_to do |format|
format.html {
flash[:error] = flash_error
# Redirect URL for archive view is different as for other views.
if URI(request.referer).path == projects_archive_path
redirect_to projects_archive_path
else
redirect_to projects_path
end
}
format.json {
render json: @project.errors,
status: :unprocessable_entity
}
end
end
end
def show
# This is the "info" view
end
def canvas
# This is the "structure/overview/canvas" view
end
def notifications
@modules = @project
.assigned_modules(current_user)
.order(due_date: :desc)
respond_to do |format|
#format.html
format.json {
render :json => {
:html => render_to_string({
:partial => "notifications.html.erb"
})
}
}
end
end
def samples
@samples_index_link = samples_index_project_path(@project, format: :json)
@organization = @project.organization
end
def module_archive
end
def samples_index
@organization = @project.organization
respond_to do |format|
format.html
format.json {
render json: ::SampleDatatable.new(view_context, @organization, @project, nil)
}
end
end
private
def project_params
params.require(:project).permit(:name, :organization_id, :visibility, :archived)
end
def load_vars
@project = Project.find_by_id(params[:id])
unless @project
render_404
end
end
def check_view_permissions
unless can_view_project(@project)
render_403
end
end
def check_view_notifications_permissions
unless can_view_project_notifications(@project)
render_403
end
end
def check_edit_permissions
unless can_edit_project(@project)
render_403
end
end
def check_canvas_permissions
@project = Project.find_by_id(wf_params[:id])
unless can_edit_canvas(@project)
render_403
end
end
def check_module_archive_permissions
unless can_restore_archived_modules(@project)
render_403
end
end
def choose_layout
action_name.in?(['index', 'archive']) ? 'main' : 'fluid'
end
end

View file

@ -0,0 +1,607 @@
class ReportsController < ApplicationController
# Ignore CSRF protection just for PDF generation (because it's
# used via target='_blank')
protect_from_forgery with: :exception, :except => :generate
before_action :load_vars, only: [
:edit,
:update
]
before_action :load_vars_nested, only: [
:index,
:new,
:new_by_module,
:new_by_timestamp,
:create,
:edit,
:update,
:generate,
:destroy,
:save_modal,
:project_contents_modal,
:module_contents_modal,
:step_contents_modal,
:result_contents_modal,
:project_contents,
:module_contents,
:step_contents,
:result_contents
]
before_action :check_view_permissions, only: [:index]
before_action :check_create_permissions, only: [
:new,
:new_by_module,
:new_by_timestamp,
:create,
:edit,
:update,
:generate,
:save_modal,
:project_contents_modal,
:module_contents_modal,
:step_contents_modal,
:result_contents_modal,
:project_contents,
:module_contents,
:step_contents,
:result_contents
]
before_action :check_destroy_permissions, only: [:destroy]
layout "fluid"
# Index showing all reports of a single project
def index
end
# Modal for creating a new report/saving an existing report
def new
respond_to do |format|
format.html
format.json {
render :json => {
:html => render_to_string({
:partial => "new.html.erb"
})
}
}
end
end
# Report grouped by modules
def new_by_module
@report = nil
end
# Report grouped by timestamp
def new_by_timestamp
# TODO
end
# Creating new report from the _save modal of the new page
def create
continue = true
begin
report_contents = JSON.parse(params.delete(:report_contents))
rescue
continue = false
end
@report = Report.new(report_params)
@report.project = @project
@report.user = current_user
@report.last_modified_by = current_user
if continue and @report.save_with_contents(report_contents)
respond_to do |format|
format.json {
render json: { url: project_reports_path(@project) }, status: :ok
}
end
else
respond_to do |format|
format.json {
render json: @report.errors, status: :unprocessable_entity
}
end
end
end
def edit
if @report.by_module?
render "reports/new_by_module.html.erb"
else
# TODO
render_403
end
end
# Updating existing report from the _save modal of the new page
def update
continue = true
begin
report_contents = JSON.parse(params.delete(:report_contents))
rescue
continue = false
end
@report.last_modified_by = current_user
@report.assign_attributes(report_params)
if continue and @report.save_with_contents(report_contents)
respond_to do |format|
format.json {
render json: { url: project_reports_path(@project) }, status: :ok
}
end
else
respond_to do |format|
format.json {
render json: @report.errors, status: :unprocessable_entity
}
end
end
end
# Destroy multiple entries action
def destroy
unless params.include? :report_ids
render_404
end
begin
report_ids = JSON.parse(params[:report_ids])
rescue
render_404
end
report_ids.each do |report_id|
report = Report.find_by_id(report_id)
if report.present?
report.destroy
end
end
redirect_to project_reports_path(@project)
end
# Generation action
# Currently, only .PDF is supported
def generate
respond_to do |format|
format.pdf {
@html = params[:html]
if @html.blank? then
@html = "<h1>No content</h1>"
end
render pdf: "report",
header: { right: '[page] of [topage]' },
template: "reports/report.pdf.erb"
}
end
end
# Modal for saving the existsing/new report
def save_modal
# Assume user is updating existing report
@report = Report.find_by_id(params[:id])
@method = :put
# Case when saving a new report
if @report.blank?
@report = Report.new
@method = :post
@url = project_reports_path(@project, format: :json)
else
@url = project_report_path(@project, @report, format: :json)
end
if !params.include? :contents
render_403 and return
end
@report_contents = params[:contents]
respond_to do |format|
format.json {
render json: {
html: render_to_string({
partial: "reports/new/modal/save.html.erb"
})
}
}
end
end
# Modal for adding contents into project element
def project_contents_modal
respond_to do |format|
format.json {
render json: {
html: render_to_string({
partial: "reports/new/modal/project_contents.html.erb",
locals: { project: @project }
})
}
}
end
end
# Modal for adding contents into module element
def module_contents_modal
my_module = MyModule.find_by_id(params[:id])
respond_to do |format|
if my_module.blank?
format.json {
render json: {}, status: :not_found
}
else
format.json {
render json: {
html: render_to_string({
partial: "reports/new/modal/module_contents.html.erb",
locals: { project: @project, my_module: my_module }
})
}
}
end
end
end
# Modal for adding contents into step element
def step_contents_modal
step = Step.find_by_id(params[:id])
respond_to do |format|
if step.blank?
format.json {
render json: {}, status: :not_found
}
else
format.json {
render json: {
html: render_to_string({
partial: "reports/new/modal/step_contents.html.erb",
locals: { project: @project, step: step }
})
}
}
end
end
end
# Modal for adding contents into result element
def result_contents_modal
result = Result.find_by_id(params[:id])
respond_to do |format|
if result.blank?
format.json {
render json: {}, status: :not_found
}
else
format.json {
render json: {
html: render_to_string({
partial: "reports/new/modal/result_contents.html.erb",
locals: { project: @project, result: result }
})
}
}
end
end
end
def project_contents
respond_to do |format|
elements = generate_project_contents_json
if elements_empty? elements
format.json { render json: {}, status: :no_content }
else
format.json {
render json: {
status: :ok,
elements: elements
}
}
end
end
end
def module_contents
my_module = MyModule.find_by_id(params[:id])
respond_to do |format|
if my_module.blank?
format.json { render json: {}, status: :not_found }
else
elements = generate_module_contents_json(my_module)
if elements_empty? elements
format.json { render json: {}, status: :no_content }
else
format.json {
render json: {
status: :ok,
elements: elements
}
}
end
end
end
end
def step_contents
step = Step.find_by_id(params[:id])
respond_to do |format|
if step.blank?
format.json { render json: {}, status: :not_found }
else
elements = generate_step_contents_json(step)
if elements_empty? elements
format.json { render json: {}, status: :no_content }
else
format.json {
render json: {
status: :ok,
elements: elements
}
}
end
end
end
end
def result_contents
result = Result.find_by_id(params[:id])
respond_to do |format|
if result.blank?
format.json { render json: {}, status: :not_found }
else
elements = generate_result_contents_json(result)
if elements_empty? elements
format.json { render json: {}, status: :no_content }
else
format.json {
render json: {
status: :ok,
elements: elements
}
}
end
end
end
end
private
def in_params?(val)
params.include? val and params[val] == "1"
end
def generate_new_el(hide)
el = {}
el[:html] = render_to_string({
partial: "reports/elements/new_element.html.erb",
locals: { hide: hide }
})
el[:children] = []
el[:new_element] = true
el
end
def generate_el(partial, locals)
el = {}
el[:html] = render_to_string({
partial: partial,
locals: locals
})
el[:children] = []
el[:new_element] = false
el
end
def generate_project_contents_json
res = []
if params.include? :modules then
modules =
(params[:modules].select { |m, p| p == "1" })
.keys
.collect { |id| id.to_i }
modules.each do |module_id|
my_module = MyModule.find_by_id(module_id)
if my_module.present?
res << generate_new_el(false)
el = generate_el(
"reports/elements/my_module_element.html.erb",
{ my_module: my_module }
)
el[:children] = generate_module_contents_json(my_module)
res << el
end
end
end
res << generate_new_el(false)
res
end
def generate_module_contents_json(my_module)
res = []
if in_params? :module_steps then
my_module.completed_steps.each do |step|
res << generate_new_el(false)
el = generate_el(
"reports/elements/step_element.html.erb",
{ step: step }
)
el[:children] = generate_step_contents_json(step)
res << el
end
end
if in_params? :module_result_assets then
(my_module.results.select { |r| r.is_asset }).each do |result_asset|
res << generate_new_el(false)
el = generate_el(
"reports/elements/result_asset_element.html.erb",
{ result: result_asset }
)
el[:children] = generate_result_contents_json(result_asset)
res << el
end
end
if in_params? :module_result_tables then
(my_module.results.select { |r| r.is_table }).each do |result_table|
res << generate_new_el(false)
el = generate_el(
"reports/elements/result_table_element.html.erb",
{ result: result_table }
)
el[:children] = generate_result_contents_json(result_table)
res << el
end
end
if in_params? :module_result_texts then
(my_module.results.select { |r| r.is_text }).each do |result_text|
res << generate_new_el(false)
el = generate_el(
"reports/elements/result_text_element.html.erb",
{ result: result_text }
)
el[:children] = generate_result_contents_json(result_text)
res << el
end
end
if in_params? :module_activity then
res << generate_new_el(false)
res << generate_el(
"reports/elements/my_module_activity_element.html.erb",
{ my_module: my_module, order: :asc }
)
end
if in_params? :module_samples then
res << generate_new_el(false)
res << generate_el(
"reports/elements/my_module_samples_element.html.erb",
{ my_module: my_module, order: :asc }
)
end
res << generate_new_el(false)
res
end
def generate_step_contents_json(step)
res = []
if in_params? :step_checklists then
step.checklists.each do |checklist|
res << generate_new_el(false)
res << generate_el(
"reports/elements/step_checklist_element.html.erb",
{ checklist: checklist }
)
end
end
if in_params? :step_assets then
step.assets.each do |asset|
res << generate_new_el(false)
res << generate_el(
"reports/elements/step_asset_element.html.erb",
{ asset: asset }
)
end
end
if in_params? :step_tables then
step.tables.each do |table|
res << generate_new_el(false)
res << generate_el(
"reports/elements/step_table_element.html.erb",
{ table: table }
)
end
end
if in_params? :step_comments then
res << generate_new_el(false)
res << generate_el(
"reports/elements/step_comments_element.html.erb",
{ step: step, order: :asc }
)
end
res << generate_new_el(false)
res
end
def generate_result_contents_json(result)
res = []
if in_params? :result_comments then
res << generate_new_el(true)
res << generate_el(
"reports/elements/result_comments_element.html.erb",
{ result: result, order: :asc }
)
else
res << generate_new_el(false)
end
res
end
def elements_empty?(elements)
if elements.blank?
return true
elsif elements.count == 0 then
return true
elsif elements.count == 1
el = elements[0]
if el.include? :new_element and el[:new_element]
return true
else
return false
end
end
return false
end
def load_vars
@report = Report.find_by_id(params[:id])
unless @report
render_404
end
end
def load_vars_nested
@project = Project.find_by_id(params[:project_id])
unless @project
render_404
end
end
def check_view_permissions
unless can_view_reports(@project)
render_403
end
end
def check_create_permissions
unless can_create_new_report(@project)
render_403
end
end
def check_destroy_permissions
unless can_delete_reports(@project)
render_403
end
end
def report_params
params.require(:report).permit(:name, :description, :grouped_by, :report_contents)
end
end

View file

@ -0,0 +1,245 @@
class ResultAssetsController < ApplicationController
include ResultsHelper
before_action :load_vars, only: [:edit, :update, :download]
before_action :load_vars_nested, only: [:new, :create]
before_action :load_paperclip_vars
before_action :check_create_permissions, only: [:new, :create]
before_action :check_edit_permissions, only: [:edit, :update]
before_action :check_archive_permissions, only: [:update]
def new
@asset = Asset.new
@result = Result.new(
user: current_user,
my_module: @my_module,
asset: @asset
)
respond_to do |format|
format.json {
render json: {
html: render_to_string({
partial: "new.html.erb",
locals: {
direct_upload: @direct_upload
}
})
}, status: :ok
}
end
end
def create
asset_attrs = result_params[:asset_attributes]
if asset_attrs and asset_attrs[:id]
@asset = Asset.find_by_id asset_attrs[:id]
else
@asset = Asset.new(result_params[:asset_attributes])
end
@asset.created_by = current_user
@asset.last_modified_by = current_user
@result = Result.new(
user: current_user,
my_module: @my_module,
name: result_params[:name],
asset: @asset
)
@result.last_modified_by = current_user
respond_to do |format|
if (@result.save and @asset.save) then
# Post process file here
@asset.post_process_file(@my_module.project.organization)
# Generate activity
Activity.create(
type_of: :add_result,
user: current_user,
project: @my_module.project,
my_module: @my_module,
message: t(
"activities.add_asset_result",
user: current_user.full_name,
result: @result.name,
)
)
format.html {
flash[:success] = t(
"result_assets.create.success_flash",
module: @my_module.name)
redirect_to results_my_module_path(@my_module)
}
format.json {
render json: {
status: 'ok',
html: render_to_string({
partial: "my_modules/result.html.erb", locals: {result: @result}
})
}, status: :ok
}
else
# This response is sent as 200 OK due to IE security error when
# accessing iframe content.
format.json {
render json: {status: 'error', errors: @result.errors}
}
end
end
end
def edit
respond_to do |format|
format.json {
render json: {
html: render_to_string({
partial: "edit.html.erb",
locals: {
direct_upload: @direct_upload
}
})
}, status: :ok
}
end
end
def update
update_params = result_params
previous_size = @result.space_taken
@result.asset.last_modified_by = current_user
@result.last_modified_by = current_user
@result.assign_attributes(update_params)
success_flash = t("result_assets.update.success_flash",
module: @my_module.name)
if @result.archived_changed?(from: false, to: true)
saved = @result.archive(current_user)
success_flash = t("result_assets.archive.success_flash",
module: @my_module.name)
if saved
Activity.create(
type_of: :archive_result,
project: @my_module.project,
my_module: @my_module,
user: current_user,
message: t(
'activities.archive_asset_result',
user: current_user.full_name,
result: @result.name
)
)
end
elsif @result.archived_changed?(from: true, to: false)
render_403
else
# Asset (file) and/or name has been changed
saved = @result.save
if saved then
@result.reload
# Release organization's space taken due to
# previous asset being removed
org = @result.my_module.project.organization
org.release_space(previous_size)
org.save
# Post process new file if neccesary
if @result.asset.present?
@result.asset.post_process_file(org)
end
Activity.create(
type_of: :edit_result,
user: current_user,
project: @my_module.project,
my_module: @my_module,
message: t(
"activities.edit_asset_result",
user: current_user.full_name,
result: @result.name
)
)
end
end
respond_to do |format|
if saved
format.html {
flash[:success] = success_flash
redirect_to results_my_module_path(@my_module)
}
format.json {
render json: {
status: 'ok',
html: render_to_string({
partial: "my_modules/result.html.erb", locals: {result: @result}
})
}, status: :ok
}
else
format.json {
render json: @result.errors, status: :bad_request
}
end
end
end
private
def load_paperclip_vars
@direct_upload = ENV['PAPERCLIP_DIRECT_UPLOAD']
end
def load_vars
@result_asset = ResultAsset.find_by_id(params[:id])
if @result_asset
@result = @result_asset.result
@my_module = @result.my_module
else
render_404
end
end
def load_vars_nested
@my_module = MyModule.find_by_id(params[:my_module_id])
unless @my_module
render_404
end
end
def check_create_permissions
unless can_create_result_asset_in_module(@my_module)
render_403
end
end
def check_edit_permissions
unless can_edit_result_asset_in_module(@my_module)
render_403
end
end
def check_archive_permissions
if result_params[:archived].to_s != '' and
not can_archive_result(@result)
render_403
end
end
def result_params
params.require(:result).permit(
:name, :archived,
asset_attributes: [
:id,
:file
]
)
end
end

View file

@ -0,0 +1,123 @@
class ResultCommentsController < ApplicationController
before_action :load_vars
before_action :check_view_permissions, only: [ :index ]
before_action :check_add_permissions, only: [ :new, :create ]
def index
@comments = @result.last_comments(@last_comment_id, @per_page)
respond_to do |format|
format.json {
# 'index' partial includes header and form for adding new
# messages. 'list' partial is used for showing more
# comments.
partial = "index.html.erb"
partial = "list.html.erb" if @last_comment_id > 0
more_url = ""
if @comments.count > 0
more_url = url_for(result_result_comments_path(@result,
format: :json,
from: @comments.last.id))
end
render :json => {
per_page: @per_page,
results_number: @comments.length,
more_url: more_url,
html: render_to_string({
partial: partial,
locals: {
comments: @comments,
more_comments_url: more_url
}
})
}
}
end
end
def new
@comment = Comment.new(
user: current_user
)
end
def create
@comment = Comment.new(
message: comment_params[:message],
user: current_user)
respond_to do |format|
if (@comment.valid? && @result.comments << @comment)
# Generate activity
Activity.create(
type_of: :add_comment_to_result,
user: current_user,
project: @result.my_module.project,
my_module: @result.my_module,
message: t(
"activities.add_comment_to_result",
user: current_user.full_name,
result: @result.name
)
)
format.html {
flash[:success] = t(
"result_comments.create.success_flash")
redirect_to session.delete(:return_to)
}
format.json {
render json: {
html: render_to_string({
partial: "comment.html.erb",
locals: {
comment: @comment
}
})
},
status: :created
}
else
response.status = 400
format.html { render :new }
format.json {
render json: {
errors: @comment.errors.to_hash(true)
}
}
end
end
end
private
def load_vars
@last_comment_id = params[:from].to_i
@per_page = 10
@result = Result.find_by_id(params[:result_id])
@my_module = @result.my_module
unless @result
render_404
end
end
def check_view_permissions
unless can_view_result_comments(@my_module)
render_403
end
end
def check_add_permissions
unless can_add_result_comment_in_module(@my_module)
render_403
end
end
def comment_params
params.require(:comment).permit(:message)
end
end

View file

@ -0,0 +1,223 @@
class ResultTablesController < ApplicationController
include ResultsHelper
before_action :load_vars, only: [:edit, :update, :download]
before_action :load_vars_nested, only: [:new, :create]
before_action :convert_contents_to_utf8, only: [:create, :update]
before_action :check_create_permissions, only: [:new, :create]
before_action :check_edit_permissions, only: [:edit, :update]
before_action :check_archive_permissions, only: [:update]
def new
@table = Table.new
@result = Result.new(
user: current_user,
my_module: @my_module,
table: @table
)
respond_to do |format|
format.json {
render json: {
html: render_to_string({
partial: "new.html.erb"
})
}, status: :ok
}
end
end
def create
@table = Table.new(result_params[:table_attributes])
@table.created_by = current_user
@table.last_modified_by = current_user
@result = Result.new(
user: current_user,
my_module: @my_module,
name: result_params[:name],
table: @table
)
@result.last_modified_by = current_user
respond_to do |format|
if (@result.save and @table.save) then
# Generate activity
Activity.create(
type_of: :add_result,
user: current_user,
project: @my_module.project,
my_module: @my_module,
message: t(
"activities.add_table_result",
user: current_user.full_name,
result: @result.name
)
)
format.html {
flash[:success] = t(
"result_tables.create.success_flash",
module: @my_module.name)
redirect_to results_my_module_path(@my_module)
}
format.json {
render json: {
html: render_to_string({
partial: "my_modules/result.html.erb", locals: {result: @result}
})
}, status: :ok
}
else
format.json {
render json: @result.errors, status: :bad_request
}
end
end
end
def edit
respond_to do |format|
format.json {
render json: {
html: render_to_string({
partial: "edit.html.erb"
})
}, status: :ok
}
end
end
def update
update_params = result_params
@result.last_modified_by = current_user
@result.table.last_modified_by = current_user
@result.assign_attributes(update_params)
flash_success = t("result_tables.update.success_flash",
module: @my_module.name)
if @result.archived_changed?(from: false, to: true)
saved = @result.archive(current_user)
flash_success = t("result_tables.archive.success_flash",
module: @my_module.name)
if saved
Activity.create(
type_of: :archive_result,
project: @my_module.project,
my_module: @my_module,
user: current_user,
message: t(
'activities.archive_table_result',
user: current_user.full_name,
result: @result.name
)
)
end
elsif @result.archived_changed?(from: true, to: false)
render_403
else
saved = @result.save
if saved then
Activity.create(
type_of: :edit_result,
user: current_user,
project: @my_module.project,
my_module: @my_module,
message: t(
"activities.edit_table_result",
user: current_user.full_name,
result: @result.name
)
)
end
end
respond_to do |format|
if saved
format.html {
flash[:success] = flash_success
redirect_to results_my_module_path(@my_module)
}
format.json {
render json: {
html: render_to_string({
partial: "my_modules/result.html.erb", locals: {result: @result}
})
}, status: :ok
}
else
format.json {
render json: @result.errors, status: :bad_request
}
end
end
end
def download
_ = JSON.parse @result_table.table.contents
@table_data = _["data"] || []
data = render_to_string partial: 'download.txt.erb'
send_data data, filename: @result_table.result.name + '.txt',
type: 'plain/text'
end
private
def load_vars
@result_table = ResultTable.find_by_id(params[:id])
if @result_table
@result = @result_table.result
@my_module = @result.my_module
else
render_404
end
end
def load_vars_nested
@my_module = MyModule.find_by_id(params[:my_module_id])
unless @my_module
render_404
end
end
def convert_contents_to_utf8
if params.include? :result and
params[:result].include? :table_attributes and
params[:result][:table_attributes].include? :contents then
params[:result][:table_attributes][:contents] =
params[:result][:table_attributes][:contents].encode(Encoding::UTF_8).force_encoding(Encoding::UTF_8)
end
end
def check_create_permissions
unless can_create_result_table_in_module(@my_module)
render_403
end
end
def check_edit_permissions
unless can_edit_result_table_in_module(@my_module)
render_403
end
end
def check_archive_permissions
if result_params[:archived].to_s != '' and
not can_archive_result(@result)
render_403
end
end
def result_params
params.require(:result).permit(
:name, :archived,
table_attributes: [
:id,
:contents
]
)
end
end

View file

@ -0,0 +1,225 @@
class ResultTextsController < ApplicationController
include ResultsHelper
before_action :load_vars, only: [:edit, :update, :download]
before_action :load_vars_nested, only: [:new, :create]
before_action :load_markdown, only: [ :create, :update ]
before_action :check_create_permissions, only: [:new, :create]
before_action :check_edit_permissions, only: [:edit, :update]
before_action :check_archive_permissions, only: [:update]
def new
@result = Result.new(
user: current_user,
my_module: @my_module
)
@result.build_result_text
respond_to do |format|
format.json {
render json: {
html: render_to_string({
partial: "new.html.erb"
})
}, status: :ok
}
end
end
def create
@result_text = ResultText.new(result_params[:result_text_attributes])
@result = Result.new(
user: current_user,
my_module: @my_module,
name: result_params[:name],
result_text: @result_text
)
@result.last_modified_by = current_user
respond_to do |format|
if (@result.save and @result_text.save) then
# Generate activity
Activity.create(
type_of: :add_result,
user: current_user,
project: @my_module.project,
my_module: @my_module,
message: t(
"activities.add_text_result",
user: current_user.full_name,
result: @result.name
)
)
format.html {
flash[:success] = t(
"result_texts.create.success_flash",
module: @my_module.name)
redirect_to results_my_module_path(@my_module)
}
format.json {
render json: {
html: render_to_string({
partial: "my_modules/result.html.erb",
locals: {
result: @result,
markdown: @markdown
}
})
}, status: :ok
}
else
format.json {
render json: @result.errors, status: :bad_request
}
end
end
end
def edit
respond_to do |format|
format.json {
render json: {
html: render_to_string({
partial: "edit.html.erb"
})
}, status: :ok
}
end
end
def update
update_params = result_params
@result.last_modified_by = current_user
@result.assign_attributes(update_params)
success_flash = t("result_texts.update.success_flash",
module: @my_module.name)
if @result.archived_changed?(from: false, to: true)
saved = @result.archive(current_user)
success_flash = t("result_texts.archive.success_flash",
module: @my_module.name)
if saved
Activity.create(
type_of: :archive_result,
project: @my_module.project,
my_module: @my_module,
user: current_user,
message: t(
'activities.archive_text_result',
user: current_user.full_name,
result: @result.name
)
)
end
elsif @result.archived_changed?(from: true, to: false)
render_403
else
saved = @result.save
if saved then
Activity.create(
type_of: :edit_result,
user: current_user,
project: @my_module.project,
my_module: @my_module,
message: t(
"activities.edit_text_result",
user: current_user.full_name,
result: @result.name
)
)
end
end
respond_to do |format|
if saved
format.html {
flash[:success] = success_flash
redirect_to results_my_module_path(@my_module)
}
format.json {
render json: {
html: render_to_string({
partial: "my_modules/result.html.erb",
locals: {
result: @result,
markdown: @markdown
}
})
}, status: :ok
}
else
format.json {
render json: @result.errors, status: :bad_request
}
end
end
end
def download
send_data @result_text.text, filename: @result_text.result.name + '.txt',
type: 'plain/text'
end
private
def load_vars
@result_text = ResultText.find_by_id(params[:id])
if @result_text
@result = @result_text.result
@my_module = @result.my_module
else
render_404
end
end
def load_vars_nested
@my_module = MyModule.find_by_id(params[:my_module_id])
unless @my_module
render_404
end
end
# Initialize markdown parser
def load_markdown
@markdown = Redcarpet::Markdown.new(
Redcarpet::Render::HTML.new(
filter_html: true,
no_images: true
)
)
end
def check_create_permissions
unless can_create_result_text_in_module(@my_module)
render_403
end
end
def check_edit_permissions
unless can_edit_result_text_in_module(@my_module)
render_403
end
end
def check_archive_permissions
if result_params[:archived].to_s != '' and
not can_archive_result(@result)
render_403
end
end
def result_params
params.require(:result).permit(
:name, :archived,
result_text_attributes: [
:id,
:text
]
)
end
end

View file

@ -0,0 +1,89 @@
class SampleGroupsController < ApplicationController
before_action :load_vars, only: [:edit, :update]
before_action :load_vars_nested, only: [:new, :create]
before_action :check_create_permissions, only: [:new, :create]
before_action :check_edit_permissions, only: [:edit, :update]
def new
@sample_group = SampleGroup.new
session[:return_to] ||= request.referer
end
def create
@sample_group = SampleGroup.new(sample_group_params)
@sample_group.organization = @organization
@sample_group.created_by = current_user
@sample_group.last_modified_by = current_user
respond_to do |format|
if @sample_group.save
format.json {
render json: {
id: @sample_group.id
},
status: :ok
}
else
format.json {
render json: @sample_group.errors,
status: :unprocessable_entity
}
end
end
end
def edit
end
def update
@sample_group.last_modified_by = current_user
if @sample_group.update_attributes(sample_group_params)
flash[:success] = t(
"sample_groups.update.success_flash",
sample_group: @sample_group.name,
organization: @organization.name)
redirect_to (session.delete(:return_to) || root_path)
else
render :edit
end
end
def destroy
end
private
def load_vars
@sample_group = SampleGroup.find_by_id(params[:id])
@organization = @sample_group.organization
unless @sample_group
render_404
end
end
def load_vars_nested
@organization = Organization.find_by_id(params[:organization_id])
unless @organization
render_404
end
end
def check_create_permissions
unless can_create_sample_type_in_organization(@organization)
render_403
end
end
def check_edit_permissions
unless can_edit_sample_type_in_organization(@organization)
render_403
end
end
def sample_group_params
params.require(:sample_group).permit(:name, :color)
end
end

View file

@ -0,0 +1,28 @@
class SampleMyModulesController < ApplicationController
before_action :load_vars
def index
@number_of_samples = @my_module.number_of_samples
@samples = @my_module.first_n_samples(5)
respond_to do |format|
format.json {
render :json => {
:html => render_to_string({
:partial => "index.html.erb"
})
}
}
end
end
private
def load_vars
@my_module = MyModule.find_by_id(params[:my_module_id])
unless @my_module
render_404
end
end
end

Some files were not shown because too many files have changed in this diff Show more