mirror of
https://github.com/scinote-eln/scinote-web.git
synced 2024-09-20 06:35:56 +08:00
Initial commit.
This commit is contained in:
commit
055298fee8
2
.buildpacks
Normal file
2
.buildpacks
Normal file
|
@ -0,0 +1,2 @@
|
|||
https://github.com/heroku/heroku-buildpack-ruby.git
|
||||
https://github.com/weibeld/heroku-buildpack-graphviz.git
|
43
.gitignore
vendored
Normal file
43
.gitignore
vendored
Normal 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
3
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Contributing to sciNote
|
||||
|
||||
### TODO
|
25
Dockerfile
Normal file
25
Dockerfile
Normal 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
68
Gemfile
Normal 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
309
Gemfile.lock
Normal 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
2356
LICENSE-3RD-PARTY.txt
Normal file
File diff suppressed because it is too large
Load diff
377
LICENSE.txt
Normal file
377
LICENSE.txt
Normal 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
50
Makefile
Normal 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
2
Procfile
Normal file
|
@ -0,0 +1,2 @@
|
|||
web: bundle exec puma -C config/puma.rb
|
||||
worker: bundle exec rake jobs:work
|
170
README.md
Normal file
170
README.md
Normal 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
6
Rakefile
Normal 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
0
app/assets/images/.keep
Normal file
BIN
app/assets/images/icon/missing.png
Normal file
BIN
app/assets/images/icon/missing.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 KiB |
BIN
app/assets/images/icon_small/missing.png
Normal file
BIN
app/assets/images/icon_small/missing.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
app/assets/images/logo.png
Normal file
BIN
app/assets/images/logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
BIN
app/assets/images/medium/missing.png
Normal file
BIN
app/assets/images/medium/missing.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
BIN
app/assets/images/thumb/missing.png
Normal file
BIN
app/assets/images/thumb/missing.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.8 KiB |
139
app/assets/javascripts/application.js
Normal file
139
app/assets/javascripts/application.js
Normal 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('');
|
||||
});
|
||||
});
|
2
app/assets/javascripts/custom_fields.js
Normal file
2
app/assets/javascripts/custom_fields.js
Normal 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.
|
164
app/assets/javascripts/direct-upload.js
Normal file
164
app/assets/javascripts/direct-upload.js
Normal 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));
|
||||
|
215
app/assets/javascripts/my_modules.js
Normal file
215
app/assets/javascripts/my_modules.js
Normal 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();
|
16
app/assets/javascripts/my_modules/activities.js
Normal file
16
app/assets/javascripts/my_modules/activities.js
Normal 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);
|
||||
}
|
||||
});
|
275
app/assets/javascripts/my_modules/results.js
Normal file
275
app/assets/javascripts/my_modules/results.js
Normal 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();
|
||||
}
|
755
app/assets/javascripts/my_modules/steps.js
Normal file
755
app/assets/javascripts/my_modules/steps.js
Normal 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();
|
||||
}
|
4
app/assets/javascripts/navigation.js
Normal file
4
app/assets/javascripts/navigation.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
/* Loading overlay for search */
|
||||
$("#search-bar").submit(function (){
|
||||
animateSpinner(document.body);
|
||||
});
|
2
app/assets/javascripts/organizations.js
Normal file
2
app/assets/javascripts/organizations.js
Normal 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.
|
2
app/assets/javascripts/project_activities.js
Normal file
2
app/assets/javascripts/project_activities.js
Normal 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.
|
3030
app/assets/javascripts/projects/canvas.js
Normal file
3030
app/assets/javascripts/projects/canvas.js
Normal file
File diff suppressed because it is too large
Load diff
440
app/assets/javascripts/projects/index.js
Normal file
440
app/assets/javascripts/projects/index.js
Normal 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();
|
||||
}());
|
200
app/assets/javascripts/reports/index.js
Normal file
200
app/assets/javascripts/reports/index.js
Normal 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();
|
||||
});
|
||||
|
||||
}());
|
1158
app/assets/javascripts/reports/new_by_module.js
Normal file
1158
app/assets/javascripts/reports/new_by_module.js
Normal file
File diff suppressed because it is too large
Load diff
82
app/assets/javascripts/results/result_assets.js
Normal file
82
app/assets/javascripts/results/result_assets.js
Normal 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();
|
2
app/assets/javascripts/results/result_comments.js
Normal file
2
app/assets/javascripts/results/result_comments.js
Normal 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.
|
103
app/assets/javascripts/results/result_tables.js
Normal file
103
app/assets/javascripts/results/result_tables.js
Normal 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();
|
75
app/assets/javascripts/results/result_texts.js
Normal file
75
app/assets/javascripts/results/result_texts.js
Normal 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();
|
669
app/assets/javascripts/samples/sample_datatable.js
Normal file
669
app/assets/javascripts/samples/sample_datatable.js
Normal 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>");
|
||||
}
|
||||
|
2
app/assets/javascripts/samples/sample_groups.js
Normal file
2
app/assets/javascripts/samples/sample_groups.js
Normal 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.
|
2
app/assets/javascripts/samples/sample_types.js
Normal file
2
app/assets/javascripts/samples/sample_types.js
Normal 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.
|
170
app/assets/javascripts/samples/samples.js
Normal file
170
app/assets/javascripts/samples/samples.js
Normal 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();
|
41
app/assets/javascripts/samples/samples_importer.js
Normal file
41
app/assets/javascripts/samples/samples_importer.js
Normal 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);
|
||||
}
|
||||
});
|
221
app/assets/javascripts/sidebar.js
Normal file
221
app/assets/javascripts/sidebar.js
Normal 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();
|
||||
});
|
||||
}());
|
0
app/assets/javascripts/sitewide/.keep
Normal file
0
app/assets/javascripts/sitewide/.keep
Normal file
114
app/assets/javascripts/sitewide/form_errors.js
Normal file
114
app/assets/javascripts/sitewide/form_errors.js
Normal 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");
|
||||
}
|
39
app/assets/javascripts/sitewide/url_handling.js
Normal file
39
app/assets/javascripts/sitewide/url_handling.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
// Add parameter to provided specified URL
|
||||
function addParam(url, param, value) {
|
||||
var a = document.createElement('a'), regex = /(?:\?|&|&)+([^=]+)(?:=([^&]*))*/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;
|
||||
});
|
||||
});
|
2
app/assets/javascripts/step_comments.js
Normal file
2
app/assets/javascripts/step_comments.js
Normal 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.
|
2
app/assets/javascripts/user_my_modules.js
Normal file
2
app/assets/javascripts/user_my_modules.js
Normal 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.
|
114
app/assets/javascripts/users/registrations/edit.js
Normal file
114
app/assets/javascripts/users/registrations/edit.js
Normal 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();
|
||||
}
|
||||
|
||||
|
243
app/assets/javascripts/users/settings/organization.js
Normal file
243
app/assets/javascripts/users/settings/organization.js
Normal 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();
|
59
app/assets/javascripts/users/settings/organizations.js
Normal file
59
app/assets/javascripts/users/settings/organizations.js
Normal 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();
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
63
app/assets/javascripts/users/settings/preferences.js
Normal file
63
app/assets/javascripts/users/settings/preferences.js
Normal 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);
|
||||
});
|
17
app/assets/stylesheets/application.scss
Normal file
17
app/assets/stylesheets/application.scss
Normal 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";
|
27
app/assets/stylesheets/colors.scss
Normal file
27
app/assets/stylesheets/colors.scss
Normal 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;
|
3
app/assets/stylesheets/custom_fields.scss
Normal file
3
app/assets/stylesheets/custom_fields.scss
Normal 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/
|
13
app/assets/stylesheets/extend/bootstrap.scss
vendored
Normal file
13
app/assets/stylesheets/extend/bootstrap.scss
vendored
Normal 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;
|
||||
}
|
||||
|
73
app/assets/stylesheets/mixins.scss
Normal file
73
app/assets/stylesheets/mixins.scss
Normal 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);
|
||||
}
|
13
app/assets/stylesheets/my_modules.scss
Normal file
13
app/assets/stylesheets/my_modules.scss
Normal 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;
|
||||
}
|
3
app/assets/stylesheets/organizations.scss
Normal file
3
app/assets/stylesheets/organizations.scss
Normal 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/
|
154
app/assets/stylesheets/partials/_sidebar.scss
Normal file
154
app/assets/stylesheets/partials/_sidebar.scss
Normal 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;
|
||||
}
|
63
app/assets/stylesheets/partials/_tree_view.scss
Normal file
63
app/assets/stylesheets/partials/_tree_view.scss
Normal 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;
|
||||
}
|
3
app/assets/stylesheets/project_activities.scss
Normal file
3
app/assets/stylesheets/project_activities.scss
Normal 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/
|
408
app/assets/stylesheets/projects.scss
Normal file
408
app/assets/stylesheets/projects.scss
Normal 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;
|
||||
}
|
||||
}
|
518
app/assets/stylesheets/reports.scss
Normal file
518
app/assets/stylesheets/reports.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
15
app/assets/stylesheets/reports_pdf.scss
Normal file
15
app/assets/stylesheets/reports_pdf.scss
Normal 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;
|
||||
}
|
||||
}
|
163
app/assets/stylesheets/reports_print.scss
Normal file
163
app/assets/stylesheets/reports_print.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
3
app/assets/stylesheets/result_assets.scss
Normal file
3
app/assets/stylesheets/result_assets.scss
Normal 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/
|
3
app/assets/stylesheets/result_comments.scss
Normal file
3
app/assets/stylesheets/result_comments.scss
Normal 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/
|
3
app/assets/stylesheets/result_tables.scss
Normal file
3
app/assets/stylesheets/result_tables.scss
Normal 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/
|
3
app/assets/stylesheets/result_texts.scss
Normal file
3
app/assets/stylesheets/result_texts.scss
Normal 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/
|
3
app/assets/stylesheets/sample_groups.scss
Normal file
3
app/assets/stylesheets/sample_groups.scss
Normal 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/
|
3
app/assets/stylesheets/sample_types.scss
Normal file
3
app/assets/stylesheets/sample_types.scss
Normal 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/
|
19
app/assets/stylesheets/samples.scss
Normal file
19
app/assets/stylesheets/samples.scss
Normal 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;
|
||||
}
|
||||
}
|
8
app/assets/stylesheets/search.scss
Normal file
8
app/assets/stylesheets/search.scss
Normal 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;
|
||||
}
|
3
app/assets/stylesheets/step_comments.scss
Normal file
3
app/assets/stylesheets/step_comments.scss
Normal 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/
|
19
app/assets/stylesheets/steps.scss
Normal file
19
app/assets/stylesheets/steps.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
960
app/assets/stylesheets/themes/scinote.scss
Normal file
960
app/assets/stylesheets/themes/scinote.scss
Normal 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;
|
||||
}
|
3
app/assets/stylesheets/user_my_modules.scss
Normal file
3
app/assets/stylesheets/user_my_modules.scss
Normal 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/
|
40
app/controllers/activities_controller.rb
Normal file
40
app/controllers/activities_controller.rb
Normal 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
|
71
app/controllers/application_controller.rb
Normal file
71
app/controllers/application_controller.rb
Normal 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
|
125
app/controllers/assets_controller.rb
Normal file
125
app/controllers/assets_controller.rb
Normal 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
|
||||
|
277
app/controllers/canvas_controller.rb
Normal file
277
app/controllers/canvas_controller.rb
Normal 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
|
0
app/controllers/concerns/.keep
Normal file
0
app/controllers/concerns/.keep
Normal file
49
app/controllers/concerns/sample_actions.rb
Normal file
49
app/controllers/concerns/sample_actions.rb
Normal 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
|
47
app/controllers/custom_fields_controller.rb
Normal file
47
app/controllers/custom_fields_controller.rb
Normal 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
|
108
app/controllers/my_module_comments_controller.rb
Normal file
108
app/controllers/my_module_comments_controller.rb
Normal 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
|
159
app/controllers/my_module_tags_controller.rb
Normal file
159
app/controllers/my_module_tags_controller.rb
Normal 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
|
388
app/controllers/my_modules_controller.rb
Normal file
388
app/controllers/my_modules_controller.rb
Normal 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
|
303
app/controllers/organizations_controller.rb
Normal file
303
app/controllers/organizations_controller.rb
Normal 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
|
37
app/controllers/project_activities_controller.rb
Normal file
37
app/controllers/project_activities_controller.rb
Normal 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
|
106
app/controllers/project_comments_controller.rb
Normal file
106
app/controllers/project_comments_controller.rb
Normal 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
|
329
app/controllers/projects_controller.rb
Normal file
329
app/controllers/projects_controller.rb
Normal 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
|
607
app/controllers/reports_controller.rb
Normal file
607
app/controllers/reports_controller.rb
Normal 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
|
245
app/controllers/result_assets_controller.rb
Normal file
245
app/controllers/result_assets_controller.rb
Normal 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
|
123
app/controllers/result_comments_controller.rb
Normal file
123
app/controllers/result_comments_controller.rb
Normal 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
|
223
app/controllers/result_tables_controller.rb
Normal file
223
app/controllers/result_tables_controller.rb
Normal 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
|
||||
|
225
app/controllers/result_texts_controller.rb
Normal file
225
app/controllers/result_texts_controller.rb
Normal 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
|
||||
|
89
app/controllers/sample_groups_controller.rb
Normal file
89
app/controllers/sample_groups_controller.rb
Normal 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
|
28
app/controllers/sample_my_modules_controller.rb
Normal file
28
app/controllers/sample_my_modules_controller.rb
Normal 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
Loading…
Reference in a new issue