From 055298fee87cc260de4f3c5b1761aa5fac56ffc6 Mon Sep 17 00:00:00 2001 From: Luka Murn Date: Fri, 12 Feb 2016 16:52:43 +0100 Subject: [PATCH] Initial commit. --- .buildpacks | 2 + .gemrc | 1 + .gitignore | 43 + CONTRIBUTING.md | 3 + Dockerfile | 25 + Gemfile | 68 + Gemfile.lock | 309 + LICENSE-3RD-PARTY.txt | 2356 + LICENSE.txt | 377 + Makefile | 50 + Procfile | 2 + README.md | 170 + Rakefile | 6 + app/assets/images/.keep | 0 app/assets/images/icon/missing.png | Bin 0 -> 1859 bytes app/assets/images/icon_small/missing.png | Bin 0 -> 1278 bytes app/assets/images/logo.png | Bin 0 -> 4652 bytes app/assets/images/medium/missing.png | Bin 0 -> 33100 bytes app/assets/images/thumb/missing.png | Bin 0 -> 6943 bytes app/assets/javascripts/application.js | 139 + app/assets/javascripts/custom_fields.js | 2 + app/assets/javascripts/direct-upload.js | 164 + app/assets/javascripts/my_modules.js | 215 + .../javascripts/my_modules/activities.js | 16 + app/assets/javascripts/my_modules/results.js | 275 + app/assets/javascripts/my_modules/steps.js | 755 + app/assets/javascripts/navigation.js | 4 + app/assets/javascripts/organizations.js | 2 + app/assets/javascripts/project_activities.js | 2 + app/assets/javascripts/projects/canvas.js | 3030 + app/assets/javascripts/projects/index.js | 440 + app/assets/javascripts/reports/index.js | 200 + .../javascripts/reports/new_by_module.js | 1158 + .../javascripts/results/result_assets.js | 82 + .../javascripts/results/result_comments.js | 2 + .../javascripts/results/result_tables.js | 103 + .../javascripts/results/result_texts.js | 75 + .../javascripts/samples/sample_datatable.js | 669 + .../javascripts/samples/sample_groups.js | 2 + .../javascripts/samples/sample_types.js | 2 + app/assets/javascripts/samples/samples.js | 170 + .../javascripts/samples/samples_importer.js | 41 + app/assets/javascripts/sidebar.js | 221 + app/assets/javascripts/sitewide/.keep | 0 .../javascripts/sitewide/form_errors.js | 114 + .../javascripts/sitewide/url_handling.js | 39 + app/assets/javascripts/step_comments.js | 2 + app/assets/javascripts/user_my_modules.js | 2 + .../javascripts/users/registrations/edit.js | 114 + .../users/settings/organization.js | 243 + .../users/settings/organizations.js | 59 + .../settings/organizations/add_user_modal.js | 167 + .../javascripts/users/settings/preferences.js | 63 + app/assets/stylesheets/application.scss | 17 + app/assets/stylesheets/colors.scss | 27 + app/assets/stylesheets/custom_fields.scss | 3 + app/assets/stylesheets/extend/bootstrap.scss | 13 + app/assets/stylesheets/mixins.scss | 73 + app/assets/stylesheets/my_modules.scss | 13 + app/assets/stylesheets/organizations.scss | 3 + app/assets/stylesheets/partials/_sidebar.scss | 154 + .../stylesheets/partials/_tree_view.scss | 63 + .../stylesheets/project_activities.scss | 3 + app/assets/stylesheets/projects.scss | 408 + app/assets/stylesheets/reports.scss | 518 + app/assets/stylesheets/reports_pdf.scss | 15 + app/assets/stylesheets/reports_print.scss | 163 + app/assets/stylesheets/result_assets.scss | 3 + app/assets/stylesheets/result_comments.scss | 3 + app/assets/stylesheets/result_tables.scss | 3 + app/assets/stylesheets/result_texts.scss | 3 + app/assets/stylesheets/sample_groups.scss | 3 + app/assets/stylesheets/sample_types.scss | 3 + app/assets/stylesheets/samples.scss | 19 + app/assets/stylesheets/search.scss | 8 + app/assets/stylesheets/step_comments.scss | 3 + app/assets/stylesheets/steps.scss | 19 + app/assets/stylesheets/themes/scinote.scss | 960 + app/assets/stylesheets/user_my_modules.scss | 3 + app/controllers/activities_controller.rb | 40 + app/controllers/application_controller.rb | 71 + app/controllers/assets_controller.rb | 125 + app/controllers/canvas_controller.rb | 277 + app/controllers/concerns/.keep | 0 app/controllers/concerns/sample_actions.rb | 49 + app/controllers/custom_fields_controller.rb | 47 + .../my_module_comments_controller.rb | 108 + app/controllers/my_module_tags_controller.rb | 159 + app/controllers/my_modules_controller.rb | 388 + app/controllers/organizations_controller.rb | 303 + .../project_activities_controller.rb | 37 + .../project_comments_controller.rb | 106 + app/controllers/projects_controller.rb | 329 + app/controllers/reports_controller.rb | 607 + app/controllers/result_assets_controller.rb | 245 + app/controllers/result_comments_controller.rb | 123 + app/controllers/result_tables_controller.rb | 223 + app/controllers/result_texts_controller.rb | 225 + app/controllers/sample_groups_controller.rb | 89 + .../sample_my_modules_controller.rb | 28 + app/controllers/sample_types_controller.rb | 94 + app/controllers/samples_controller.rb | 290 + app/controllers/search_controller.rb | 181 + app/controllers/step_comments_controller.rb | 125 + app/controllers/steps_controller.rb | 560 + app/controllers/tags_controller.rb | 154 + app/controllers/user_my_modules_controller.rb | 206 + app/controllers/user_projects_controller.rb | 282 + .../users/confirmations_controller.rb | 28 + .../users/invitations_controller.rb | 30 + .../users/omniauth_callbacks_controller.rb | 28 + app/controllers/users/passwords_controller.rb | 32 + .../users/registrations_controller.rb | 239 + app/controllers/users/sessions_controller.rb | 26 + app/controllers/users/settings_controller.rb | 443 + app/controllers/users/unlocks_controller.rb | 28 + .../organization_users_datatable.rb | 70 + app/datatables/sample_datatable.rb | 231 + app/helpers/application_helper.rb | 16 + app/helpers/bootstrap_form_helper.rb | 241 + app/helpers/custom_fields_helper.rb | 2 + app/helpers/database_helper.rb | 57 + app/helpers/my_modules_helper.rb | 30 + app/helpers/organizations_helper.rb | 2 + app/helpers/permission_helper.rb | 608 + app/helpers/project_activities_helper.rb | 2 + app/helpers/projects_helper.rb | 14 + app/helpers/reports_helper.rb | 75 + app/helpers/result_assets_helper.rb | 2 + app/helpers/result_comments_helper.rb | 2 + app/helpers/result_tables_helper.rb | 2 + app/helpers/result_texts_helper.rb | 2 + app/helpers/results_helper.rb | 61 + app/helpers/sample_groups_helper.rb | 2 + app/helpers/sample_types_helper.rb | 2 + app/helpers/samples_helper.rb | 37 + app/helpers/search_helper.rb | 2 + app/helpers/secondary_navigation_helper.rb | 50 + app/helpers/sidebar_helper.rb | 36 + app/helpers/step_comments_helper.rb | 2 + app/helpers/steps_helper.rb | 2 + app/helpers/user_my_modules_helper.rb | 2 + app/mailers/.keep | 0 app/models/.keep | 0 app/models/activity.rb | 38 + app/models/asset.rb | 239 + app/models/asset_text_datum.rb | 64 + app/models/checklist.rb | 28 + app/models/checklist_item.rb | 19 + app/models/comment.rb | 92 + app/models/concerns/.keep | 0 app/models/concerns/archivable_model.rb | 81 + app/models/concerns/searchable_model.rb | 32 + app/models/connection.rb | 4 + app/models/custom_field.rb | 11 + app/models/log.rb | 6 + app/models/my_module.rb | 413 + app/models/my_module_comment.rb | 7 + app/models/my_module_group.rb | 34 + app/models/my_module_tag.rb | 8 + app/models/organization.rb | 294 + app/models/project.rb | 547 + app/models/project_comment.rb | 7 + app/models/report.rb | 106 + app/models/report_element.rb | 114 + app/models/result.rb | 138 + app/models/result_asset.rb | 11 + app/models/result_comment.rb | 9 + app/models/result_table.rb | 8 + app/models/result_text.rb | 7 + app/models/sample.rb | 62 + app/models/sample_comment.rb | 7 + app/models/sample_custom_field.rb | 9 + app/models/sample_group.rb | 14 + app/models/sample_my_module.rb | 14 + app/models/sample_type.rb | 12 + app/models/step.rb | 144 + app/models/step_asset.rb | 6 + app/models/step_comment.rb | 7 + app/models/step_table.rb | 6 + app/models/table.rb | 31 + app/models/tag.rb | 36 + app/models/temp_file.rb | 6 + app/models/user.rb | 211 + app/models/user_my_module.rb | 7 + app/models/user_organization.rb | 28 + app/models/user_project.rb | 28 + app/utilities/first_time_data_generator.rb | 593 + app/utilities/users_generator.rb | 88 + app/views/activities/_activity.html.erb | 11 + app/views/activities/_index.html.erb | 13 + app/views/activities/_list.html.erb | 23 + app/views/activities/index.html.erb | 2 + app/views/canvas/_edit.html.erb | 88 + app/views/canvas/_full_zoom.html.erb | 16 + app/views/canvas/_medium_zoom.html.erb | 7 + app/views/canvas/_small_zoom.html.erb | 7 + app/views/canvas/_tags.html.erb | 20 + app/views/canvas/edit/_my_module.html.erb | 65 + .../canvas/edit/modal/_delete_module.html.erb | 18 + .../edit/modal/_delete_module_group.html.erb | 18 + .../canvas/edit/modal/_edit_module.html.erb | 28 + .../edit/modal/_edit_module_group.html.erb | 28 + .../canvas/edit/modal/_new_module.html.erb | 28 + .../canvas/full_zoom/_my_module.html.erb | 110 + .../canvas/medium_zoom/_my_module.html.erb | 30 + .../canvas/small_zoom/_my_module.html.erb | 14 + app/views/custom_fields/_new_modal.html.erb | 19 + app/views/devise/confirmations/new.html.erb | 26 + .../mailer/confirmation_instructions.html.erb | 5 + .../reset_password_instructions.html.erb | 8 + .../mailer/unlock_instructions.html.erb | 7 + app/views/devise/passwords/edit.html.erb | 35 + app/views/devise/passwords/new.html.erb | 25 + app/views/devise/sessions/new.html.erb | 34 + app/views/devise/shared/_links.html.erb | 25 + app/views/devise/unlocks/new.html.erb | 25 + app/views/layouts/application.html.erb | 70 + app/views/layouts/fluid.html.erb | 12 + app/views/layouts/main.html.erb | 6 + .../my_module_comments/_comment.html.erb | 3 + app/views/my_module_comments/_index.html.erb | 26 + app/views/my_module_comments/_list.html.erb | 12 + app/views/my_module_comments/new.html.erb | 7 + app/views/my_module_tags/_index_edit.html.erb | 89 + app/views/my_module_tags/new.html.erb | 10 + app/views/my_modules/_activities.html.erb | 15 + app/views/my_modules/_description.html.erb | 3 + .../my_modules/_description_label.html.erb | 5 + app/views/my_modules/_due_date.html.erb | 3 + app/views/my_modules/_due_date_label.html.erb | 8 + app/views/my_modules/_module_header.html.erb | 98 + .../_module_header_due_date_label.html.erb | 5 + app/views/my_modules/_result.html.erb | 61 + .../_result_user_generated.html.erb | 7 + app/views/my_modules/_show.html.erb | 15 + app/views/my_modules/_step.html.erb | 126 + app/views/my_modules/_tags.html.erb | 8 + app/views/my_modules/activities.html.erb | 27 + .../my_modules/activities/_activity.html.erb | 7 + .../activities/_list_activities.html.erb | 6 + app/views/my_modules/archive.html.erb | 19 + app/views/my_modules/archive/_result.html.erb | 52 + app/views/my_modules/edit.html.erb | 9 + .../modals/_manage_description_modal.html.erb | 15 + .../modals/_manage_due_date_modal.html.erb | 15 + .../modals/_manage_module_tags_modal.html.erb | 14 + .../modals/_manage_users_modal.html.erb | 14 + app/views/my_modules/results.html.erb | 55 + app/views/my_modules/samples.html.erb | 8 + app/views/my_modules/show.html.erb | 11 + app/views/my_modules/steps.html.erb | 42 + app/views/organizations/_parse_error.html.erb | 7 + .../organizations/_parse_errors.html.erb | 26 + app/views/project_activities/_index.html.erb | 13 + app/views/project_activities/index.html.erb | 3 + app/views/project_comments/_comment.html.erb | 3 + app/views/project_comments/_index.html.erb | 25 + app/views/project_comments/_list.html.erb | 16 + app/views/project_comments/new.html.erb | 7 + app/views/projects/_edit.html.erb | 9 + app/views/projects/_new.html.erb | 16 + app/views/projects/_notifications.html.erb | 30 + app/views/projects/archive.html.erb | 72 + .../projects/archive/_org_projects.html.erb | 18 + app/views/projects/archive/_project.html.erb | 37 + app/views/projects/canvas.html.erb | 37 + app/views/projects/index.html.erb | 141 + .../projects/index/_org_projects.html.erb | 18 + app/views/projects/index/_project.html.erb | 109 + app/views/projects/module_archive.html.erb | 28 + .../module_archive/_my_module.html.erb | 39 + app/views/projects/samples.html.erb | 8 + app/views/projects/show.html.erb | 3 + app/views/reports/_new.html.erb | 15 + .../_module_element_controls.html.erb | 18 + .../_my_module_activity_element.html.erb | 47 + .../elements/_my_module_element.html.erb | 59 + .../_my_module_samples_element.html.erb | 34 + .../reports/elements/_new_element.html.erb | 25 + .../elements/_project_header_element.html.erb | 21 + .../elements/_result_asset_element.html.erb | 40 + .../_result_comments_element.html.erb | 46 + .../elements/_result_table_element.html.erb | 30 + .../elements/_result_text_element.html.erb | 33 + .../elements/_step_asset_element.html.erb | 34 + .../elements/_step_checklist_element.html.erb | 34 + .../elements/_step_comments_element.html.erb | 46 + .../reports/elements/_step_element.html.erb | 40 + .../elements/_step_table_element.html.erb | 24 + app/views/reports/index.html.erb | 110 + .../reports/new/_report_navigation.html.erb | 64 + .../reports/new/_report_sidebar.html.erb | 22 + .../new/modal/_module_contents.html.erb | 58 + .../new/modal/_module_contents_inner.html.erb | 52 + .../new/modal/_project_contents.html.erb | 109 + .../modal/_project_contents_inner.html.erb | 46 + .../new/modal/_result_contents.html.erb | 4 + .../new/modal/_result_contents_inner.html.erb | 7 + app/views/reports/new/modal/_save.html.erb | 6 + .../reports/new/modal/_step_contents.html.erb | 19 + .../new/modal/_step_contents_inner.html.erb | 38 + app/views/reports/new_by_module.html.erb | 76 + app/views/reports/report.pdf.erb | 13 + app/views/result_assets/_edit.html.erb | 17 + app/views/result_assets/_new.html.erb | 16 + app/views/result_comments/_comment.html.erb | 2 + app/views/result_comments/_index.html.erb | 26 + app/views/result_comments/_list.html.erb | 12 + app/views/result_comments/new.html.erb | 7 + app/views/result_tables/_download.txt.erb | 3 + app/views/result_tables/_edit.html.erb | 17 + app/views/result_tables/_new.html.erb | 16 + app/views/result_texts/_edit.html.erb | 13 + app/views/result_texts/_new.html.erb | 12 + app/views/results/_result_asset.html.erb | 8 + app/views/results/_result_table.html.erb | 5 + app/views/results/_result_text.html.erb | 5 + app/views/sample_groups/edit.html.erb | 9 + app/views/sample_my_modules/_index.html.erb | 13 + app/views/sample_types/edit.html.erb | 8 + .../_create_sample_group_modal.html.erb | 20 + .../_create_sample_type_modal.html.erb | 19 + .../samples/_delete_samples_modal.html.erb | 18 + .../samples/_import_samples_modal.html.erb | 20 + .../samples/_parse_samples_modal.html.erb | 54 + app/views/search/index.html.erb | 211 + app/views/search/new.html.erb | 11 + app/views/search/results/_assets.html.erb | 55 + app/views/search/results/_comments.html.erb | 58 + app/views/search/results/_contents.erb | 7 + app/views/search/results/_modules.html.erb | 35 + app/views/search/results/_projects.html.erb | 20 + app/views/search/results/_reports.html.erb | 52 + app/views/search/results/_results.html.erb | 40 + app/views/search/results/_samples.html.erb | 68 + app/views/search/results/_steps.html.erb | 31 + app/views/search/results/_tags.html.erb | 34 + app/views/search/results/_workflows.html.erb | 25 + .../results/partials/_asset_text.html.erb | 25 + .../results/partials/_content_text.html.erb | 16 + .../results/partials/_my_module_text.html.erb | 21 + .../partials/_organization_text.html.erb | 7 + .../results/partials/_project_text.html.erb | 21 + .../results/partials/_report_text.html.erb | 10 + .../results/partials/_result_text.html.erb | 21 + .../results/partials/_step_text.html.erb | 10 + .../results/partials/_tag_text.html.erb | 10 + app/views/shared/_navigation.html.erb | 90 + app/views/shared/_sample.html.erb | 23 + app/views/shared/_samples.html.erb | 153 + .../shared/_secondary_navigation.html.erb | 175 + app/views/shared/_sidebar.html.erb | 96 + app/views/step_comments/_comment.html.erb | 2 + app/views/step_comments/_index.html.erb | 26 + app/views/step_comments/_list.html.erb | 12 + app/views/step_comments/new.html.erb | 7 + app/views/steps/_edit.html.erb | 16 + app/views/steps/_empty_step.html.erb | 59 + app/views/steps/_form_assets.html.erb | 22 + app/views/steps/_form_checklists.html.erb | 41 + app/views/steps/_form_tables.html.erb | 15 + app/views/steps/_new.html.erb | 16 + app/views/user_my_modules/_index.html.erb | 33 + .../user_my_modules/_index_edit.html.erb | 54 + app/views/user_my_modules/new.html.erb | 13 + app/views/user_projects/_index.html.erb | 28 + app/views/user_projects/_index_edit.html.erb | 79 + app/views/user_projects/edit.html.erb | 26 + app/views/user_projects/new.html.erb | 28 + app/views/users/invitations/edit.html.erb | 67 + app/views/users/invitations/new.html.erb | 12 + .../mailer/invitation_instructions.html.erb | 11 + app/views/users/registrations/edit.html.erb | 175 + app/views/users/registrations/new.html.erb | 76 + app/views/users/settings/_navigation.html.erb | 8 + .../users/settings/new_organization.html.erb | 32 + .../users/settings/organization.html.erb | 118 + .../users/settings/organizations.html.erb | 93 + .../organizations/_add_user_modal.html.erb | 92 + .../organizations/_breadcrumbs.html.erb | 21 + .../organizations/_description_label.html.erb | 5 + .../organizations/_description_modal.html.erb | 17 + .../_description_modal_body.html.erb | 3 + .../organizations/_destroy_modal.html.erb | 19 + .../_destroy_user_organization_modal.html.erb | 16 + ...troy_user_organization_modal_body.html.erb | 3 + .../_existing_users_search_results.html.erb | 25 + .../_leave_user_organization_modal.html.erb | 16 + ...eave_user_organization_modal_body.html.erb | 4 + .../organizations/_name_modal.html.erb | 17 + .../organizations/_name_modal_body.html.erb | 3 + .../organizations/_user_dropdown.html.erb | 62 + app/views/users/settings/preferences.html.erb | 39 + bin/bundle | 3 + bin/delayed_job | 5 + bin/rails | 4 + bin/rake | 4 + bin/setup | 29 + config.ru | 4 + config/application.rb | 36 + config/boot.rb | 3 + config/database.yml | 85 + config/environment.rb | 6 + config/environments/development.rb | 64 + config/environments/production.rb | 105 + config/environments/test.rb | 71 + config/initializers/assets.rb | 47 + config/initializers/aws.rb | 8 + config/initializers/backtrace_silencers.rb | 7 + config/initializers/constants.rb | 50 + config/initializers/cookies_serializer.rb | 3 + config/initializers/devise.rb | 313 + config/initializers/devise_async.rb | 4 + .../initializers/filter_parameter_logging.rb | 4 + config/initializers/inflections.rb | 16 + config/initializers/mime_types.rb | 4 + config/initializers/paperclip.rb | 53 + config/initializers/session_store.rb | 3 + config/initializers/wicked_pdf.rb | 25 + config/initializers/wrap_parameters.rb | 14 + config/locales/devise.en.yml | 60 + config/locales/devise_invitable.en.yml | 31 + config/locales/en.yml | 1161 + config/puma.rb | 15 + config/routes.rb | 184 + config/secrets.yml | 48 + config/skylight.yml | 3 + db/load_users_template.yml | 25 + .../20150713060702_devise_create_users.rb | 46 + db/migrate/20150713061603_add_user_columns.rb | 9 + .../20150713063224_create_organizations.rb | 11 + ...0150713070738_create_user_organizations.rb | 13 + db/migrate/20150713071921_create_projects.rb | 13 + .../20150713072417_create_user_projects.rb | 17 + .../20150714125221_add_archive_to_projects.rb | 6 + db/migrate/20150715122019_create_logs.rb | 9 + .../20150715124934_create_my_modules.rb | 22 + .../20150715131400_create_project_comments.rb | 10 + ...0150715132459_create_my_module_comments.rb | 10 + db/migrate/20150715132920_create_tags.rb | 9 + ...150715133511_create_my_modules_and_tags.rb | 8 + .../20150715133709_create_my_module_groups.rb | 9 + .../20150715134133_create_connections.rb | 10 + db/migrate/20150715135452_create_steps.rb | 21 + db/migrate/20150715141810_create_assets.rb | 10 + .../20150715142704_create_step_assets.rb | 11 + .../20150715142929_create_result_assets.rb | 10 + db/migrate/20150715143134_create_results.rb | 16 + .../20150716060140_create_result_comments.rb | 10 + db/migrate/20150716061004_create_comments.rb | 13 + .../20150716061555_create_step_comments.rb | 11 + db/migrate/20150716061937_create_tables.rb | 10 + .../20150716062013_create_checklists.rb | 11 + .../20150716062110_create_checklist_items.rb | 12 + db/migrate/20150716062801_create_samples.rb | 17 + .../20150716064453_create_activities.rb | 18 + ...20150716120130_create_sample_my_modules.rb | 11 + .../20150716120659_create_sample_comments.rb | 11 + .../20150717084645_create_result_texts.rb | 10 + .../20150717085043_create_step_tables.rb | 11 + .../20150717085133_create_result_tables.rb | 11 + .../20150722095027_create_user_my_modules.rb | 14 + db/migrate/20150722112911_add_foreign_keys.rb | 10 + ...0150723134648_add_confirmable_to_devise.rb | 20 + ...0090021_add_my_module_groups_to_project.rb | 18 + .../20150804055341_add_color_to_tags.rb | 5 + .../20150820120553_create_sample_groups.rb | 13 + .../20150820123018_create_sample_types.rb | 12 + ...20124022_add_default_columns_to_samples.rb | 11 + .../20150827130647_create_custom_fields.rb | 17 + ...50827130822_create_sample_custom_fields.rb | 17 + .../20150911125914_add_project_to_tags.rb | 33 + db/migrate/20150915074650_create_reports.rb | 20 + .../20150923065605_create_temp_files.rb | 10 + ...0150923110208_add_archive_to_my_modules.rb | 7 + ...20150923154140_add_index_to_sample_name.rb | 5 + .../20150924115001_create_report_elements.rb | 44 + .../20150924181017_add_archive_to_results.rb | 6 + ...20151005122041_add_created_by_to_assets.rb | 41 + .../20151021082639_add_pg_trgm_support.rb | 16 + ...20151021085335_add_search_query_indexes.rb | 52 + ...0_remove_unique_organization_name_index.rb | 11 + ...1028091615_add_counter_cache_to_samples.rb | 20 + ...20151103155048_add_btree_gist_extension.rb | 16 + ...1135802_reset_assigned_samples_counters.rb | 11 + ...83839_add_project_reference_to_activity.rb | 34 + .../20151119141714_create_asset_text_data.rb | 11 + ...d_text_search_vector_to_asset_text_data.rb | 12 + ...100514_add_workflow_order_to_my_modules.rb | 9 + .../20151207151820_add_timezone_to_user.rb | 5 + ...800_add_organization_management_support.rb | 36 + ...151215103642_add_foreign_keys_to_tables.rb | 34 + ...134147_add_text_search_vector_to_tables.rb | 12 + ...e_text_search_vector_for_table_contents.rb | 14 + .../20160114155705_create_delayed_jobs.rb | 23 + ...0118114850_remove_private_organizations.rb | 13 + ...19101947_add_position_to_checklist_item.rb | 18 + ...125200130_devise_invitable_add_to_users.rb | 23 + ...20160125205500_add_empty_field_to_asset.rb | 11 + ...85344_add_tutorial_status_field_to_user.rb | 14 + ...5192344_migrate_organizations_structure.rb | 44 + db/schema.rb | 677 + db/seeds.rb | 12 + docker-compose.yml | 20 + lib/assets/.keep | 0 lib/tasks/.keep | 0 lib/tasks/data.rake | 59 + lib/tasks/db_fake_data.rake | 1116 + lib/tasks/db_users.rake | 138 + lib/tasks/i18n_missing_keys.rake | 54 + lib/tasks/paperclip.rake | 31 + lib/tasks/web_stats.rake | 65 + log/.keep | 0 public/403.html | 72 + public/404.html | 72 + public/422.html | 67 + public/500.html | 66 + public/favicon-16.png | Bin 0 -> 3088 bytes public/favicon-32.png | Bin 0 -> 3357 bytes public/favicon-48.png | Bin 0 -> 3438 bytes public/favicon.ico | Bin 0 -> 7406 bytes public/images/.keep | 0 public/images/favicon-16.png | Bin 0 -> 3127 bytes public/images/favicon-32.png | Bin 0 -> 3711 bytes public/images/favicon-48.png | Bin 0 -> 4344 bytes public/images/favicon.ico | Bin 0 -> 1150 bytes public/images/icon/missing.png | Bin 0 -> 1859 bytes public/images/icon_small/missing.png | Bin 0 -> 1278 bytes public/images/logo.png | Bin 0 -> 4652 bytes public/images/medium/missing.png | Bin 0 -> 33100 bytes public/images/thumb/missing.png | Bin 0 -> 6943 bytes public/robots.txt | 5 + test/controllers/.keep | 0 .../controllers/activities_controller_test.rb | 5 + .../custom_fields_controller_test.rb | 7 + .../controllers/my_modules_controller_test.rb | 7 + .../organizations_controller_test.rb | 7 + .../project_activities_controller_test.rb | 7 + test/controllers/projects_controller_test.rb | 7 + .../result_assets_controller_test.rb | 7 + .../result_comments_controller_test.rb | 7 + .../result_tables_controller_test.rb | 7 + .../result_texts_controller_test.rb | 7 + .../sample_groups_controller_test.rb | 7 + .../sample_types_controller_test.rb | 7 + test/controllers/samples_controller_test.rb | 7 + test/controllers/search_controller_test.rb | 7 + .../step_comments_controller_test.rb | 7 + test/controllers/steps_controller_test.rb | 7 + .../user_my_modules_controller_test.rb | 7 + test/fixtures/.keep | 0 test/fixtures/activities.yml | 95 + test/fixtures/asset_text_datum.yml | 15 + test/fixtures/assets.yml | 48 + test/fixtures/checklist_items.yml | 56 + test/fixtures/checklists.yml | 17 + test/fixtures/comments.yml | 89 + test/fixtures/connections.yml | 19 + test/fixtures/custom_fields.yml | 16 + test/fixtures/logs.yml | 8 + test/fixtures/my_module_comments.yml | 23 + test/fixtures/my_module_groups.yml | 14 + test/fixtures/my_modules.yml | 140 + test/fixtures/organizations.yml | 21 + test/fixtures/project_comments.yml | 15 + test/fixtures/projects.yml | 175 + test/fixtures/report_elements.yml | 105 + test/fixtures/reports.yml | 6 + test/fixtures/result_assets.yml | 7 + test/fixtures/result_comments.yml | 25 + test/fixtures/result_tables.yml | 7 + test/fixtures/result_texts.yml | 7 + test/fixtures/results.yml | 66 + test/fixtures/sample_comments.yml | 11 + test/fixtures/sample_custom_fields.yml | 21 + test/fixtures/sample_groups.yml | 14 + test/fixtures/sample_my_modules.yml | 17 + test/fixtures/sample_types.yml | 9 + test/fixtures/samples.yml | 31 + test/fixtures/step_assets.yml | 15 + test/fixtures/step_comments.yml | 17 + test/fixtures/step_tables.yml | 7 + test/fixtures/steps.yml | 48 + test/fixtures/tables.yml | 14 + test/fixtures/tags.yml | 34 + test/fixtures/temp_files.yml | 5 + test/fixtures/user_my_modules.yml | 48 + test/fixtures/user_organizations.yml | 99 + test/fixtures/user_projects.yml | 120 + test/fixtures/users.yml | 41 + test/helpers/.keep | 0 test/helpers/archivable_model_test_helper.rb | 76 + test/helpers/fake_test_helper.rb | 62 + test/helpers/searchable_model_test_helper.rb | 33 + test/integration/.keep | 0 test/integration/canvas_update_test.rb | 245 + test/mailers/.keep | 0 test/models/.keep | 0 test/models/activity_test.rb | 41 + test/models/asset_test.rb | 90 + test/models/asset_text_datum_test.rb | 26 + test/models/checklist_item_test.rb | 56 + test/models/checklist_test.rb | 44 + test/models/comment_test.rb | 73 + test/models/connection_test.rb | 5 + test/models/custom_field_test.rb | 37 + test/models/log_test.rb | 28 + test/models/my_module_comment_test.rb | 39 + test/models/my_module_group_test.rb | 30 + test/models/my_module_tag_test.rb | 42 + test/models/my_module_test.rb | 161 + test/models/organization_test.rb | 42 + test/models/project_comment_test.rb | 39 + test/models/project_test.rb | 79 + test/models/report_element_test.rb | 103 + test/models/report_test.rb | 110 + test/models/result_asset_test.rb | 32 + test/models/result_comment_test.rb | 38 + test/models/result_table_test.rb | 48 + test/models/result_test.rb | 160 + test/models/result_text_test.rb | 39 + test/models/sample_comment_test.rb | 40 + test/models/sample_custom_field_test.rb | 37 + test/models/sample_group_test.rb | 40 + test/models/sample_my_module_test.rb | 43 + test/models/sample_test.rb | 55 + test/models/sample_type_test.rb | 30 + test/models/step_asset_test.rb | 35 + test/models/step_comment_test.rb | 34 + test/models/step_table_test.rb | 32 + test/models/step_test.rb | 122 + test/models/table_test.rb | 37 + test/models/tag_test.rb | 66 + test/models/temp_file_test.rb | 13 + test/models/user_my_module_test.rb | 32 + test/models/user_organization_test.rb | 67 + test/models/user_project_test.rb | 88 + test/models/user_test.rb | 168 + test/test_helper.rb | 27 + vendor/assets/javascripts/.keep | 0 vendor/assets/javascripts/Sortable.min.js | 2 + .../javascripts/bootstrap-colorselector.js | 137 + .../assets/javascripts/canvas-to-blob.min.js | 1 + vendor/assets/javascripts/datatables.js | 97864 ++++++++++++++++ vendor/assets/javascripts/eventPause-min.js | 8 + .../javascripts/handsontable.full.min.js | 15 + .../javascripts/jquery.mousewheel.min.js | 8 + .../javascripts/jquery.ui.touch-punch.min.js | 11 + .../assets/javascripts/jsPlumb-2.0.4-min.js | 6 + vendor/assets/javascripts/jsnetworkx.js | 8 + vendor/assets/stylesheets/.keep | 0 .../stylesheets/bootstrap-colorselector.scss | 108 + vendor/assets/stylesheets/datatables.css | 521 + .../stylesheets/handsontable.full.min.scss | 15 + 655 files changed, 141708 insertions(+) create mode 100644 .buildpacks create mode 100644 .gemrc create mode 100644 .gitignore create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 LICENSE-3RD-PARTY.txt create mode 100644 LICENSE.txt create mode 100644 Makefile create mode 100644 Procfile create mode 100644 README.md create mode 100644 Rakefile create mode 100644 app/assets/images/.keep create mode 100644 app/assets/images/icon/missing.png create mode 100644 app/assets/images/icon_small/missing.png create mode 100644 app/assets/images/logo.png create mode 100644 app/assets/images/medium/missing.png create mode 100644 app/assets/images/thumb/missing.png create mode 100644 app/assets/javascripts/application.js create mode 100644 app/assets/javascripts/custom_fields.js create mode 100644 app/assets/javascripts/direct-upload.js create mode 100644 app/assets/javascripts/my_modules.js create mode 100644 app/assets/javascripts/my_modules/activities.js create mode 100644 app/assets/javascripts/my_modules/results.js create mode 100644 app/assets/javascripts/my_modules/steps.js create mode 100644 app/assets/javascripts/navigation.js create mode 100644 app/assets/javascripts/organizations.js create mode 100644 app/assets/javascripts/project_activities.js create mode 100644 app/assets/javascripts/projects/canvas.js create mode 100644 app/assets/javascripts/projects/index.js create mode 100644 app/assets/javascripts/reports/index.js create mode 100644 app/assets/javascripts/reports/new_by_module.js create mode 100644 app/assets/javascripts/results/result_assets.js create mode 100644 app/assets/javascripts/results/result_comments.js create mode 100644 app/assets/javascripts/results/result_tables.js create mode 100644 app/assets/javascripts/results/result_texts.js create mode 100644 app/assets/javascripts/samples/sample_datatable.js create mode 100644 app/assets/javascripts/samples/sample_groups.js create mode 100644 app/assets/javascripts/samples/sample_types.js create mode 100644 app/assets/javascripts/samples/samples.js create mode 100644 app/assets/javascripts/samples/samples_importer.js create mode 100644 app/assets/javascripts/sidebar.js create mode 100644 app/assets/javascripts/sitewide/.keep create mode 100644 app/assets/javascripts/sitewide/form_errors.js create mode 100644 app/assets/javascripts/sitewide/url_handling.js create mode 100644 app/assets/javascripts/step_comments.js create mode 100644 app/assets/javascripts/user_my_modules.js create mode 100644 app/assets/javascripts/users/registrations/edit.js create mode 100644 app/assets/javascripts/users/settings/organization.js create mode 100644 app/assets/javascripts/users/settings/organizations.js create mode 100644 app/assets/javascripts/users/settings/organizations/add_user_modal.js create mode 100644 app/assets/javascripts/users/settings/preferences.js create mode 100644 app/assets/stylesheets/application.scss create mode 100644 app/assets/stylesheets/colors.scss create mode 100644 app/assets/stylesheets/custom_fields.scss create mode 100644 app/assets/stylesheets/extend/bootstrap.scss create mode 100644 app/assets/stylesheets/mixins.scss create mode 100644 app/assets/stylesheets/my_modules.scss create mode 100644 app/assets/stylesheets/organizations.scss create mode 100644 app/assets/stylesheets/partials/_sidebar.scss create mode 100644 app/assets/stylesheets/partials/_tree_view.scss create mode 100644 app/assets/stylesheets/project_activities.scss create mode 100644 app/assets/stylesheets/projects.scss create mode 100644 app/assets/stylesheets/reports.scss create mode 100644 app/assets/stylesheets/reports_pdf.scss create mode 100644 app/assets/stylesheets/reports_print.scss create mode 100644 app/assets/stylesheets/result_assets.scss create mode 100644 app/assets/stylesheets/result_comments.scss create mode 100644 app/assets/stylesheets/result_tables.scss create mode 100644 app/assets/stylesheets/result_texts.scss create mode 100644 app/assets/stylesheets/sample_groups.scss create mode 100644 app/assets/stylesheets/sample_types.scss create mode 100644 app/assets/stylesheets/samples.scss create mode 100644 app/assets/stylesheets/search.scss create mode 100644 app/assets/stylesheets/step_comments.scss create mode 100644 app/assets/stylesheets/steps.scss create mode 100644 app/assets/stylesheets/themes/scinote.scss create mode 100644 app/assets/stylesheets/user_my_modules.scss create mode 100644 app/controllers/activities_controller.rb create mode 100644 app/controllers/application_controller.rb create mode 100644 app/controllers/assets_controller.rb create mode 100644 app/controllers/canvas_controller.rb create mode 100644 app/controllers/concerns/.keep create mode 100644 app/controllers/concerns/sample_actions.rb create mode 100644 app/controllers/custom_fields_controller.rb create mode 100644 app/controllers/my_module_comments_controller.rb create mode 100644 app/controllers/my_module_tags_controller.rb create mode 100644 app/controllers/my_modules_controller.rb create mode 100644 app/controllers/organizations_controller.rb create mode 100644 app/controllers/project_activities_controller.rb create mode 100644 app/controllers/project_comments_controller.rb create mode 100644 app/controllers/projects_controller.rb create mode 100644 app/controllers/reports_controller.rb create mode 100644 app/controllers/result_assets_controller.rb create mode 100644 app/controllers/result_comments_controller.rb create mode 100644 app/controllers/result_tables_controller.rb create mode 100644 app/controllers/result_texts_controller.rb create mode 100644 app/controllers/sample_groups_controller.rb create mode 100644 app/controllers/sample_my_modules_controller.rb create mode 100644 app/controllers/sample_types_controller.rb create mode 100644 app/controllers/samples_controller.rb create mode 100644 app/controllers/search_controller.rb create mode 100644 app/controllers/step_comments_controller.rb create mode 100644 app/controllers/steps_controller.rb create mode 100644 app/controllers/tags_controller.rb create mode 100644 app/controllers/user_my_modules_controller.rb create mode 100644 app/controllers/user_projects_controller.rb create mode 100644 app/controllers/users/confirmations_controller.rb create mode 100644 app/controllers/users/invitations_controller.rb create mode 100644 app/controllers/users/omniauth_callbacks_controller.rb create mode 100644 app/controllers/users/passwords_controller.rb create mode 100644 app/controllers/users/registrations_controller.rb create mode 100644 app/controllers/users/sessions_controller.rb create mode 100644 app/controllers/users/settings_controller.rb create mode 100644 app/controllers/users/unlocks_controller.rb create mode 100644 app/datatables/organization_users_datatable.rb create mode 100644 app/datatables/sample_datatable.rb create mode 100644 app/helpers/application_helper.rb create mode 100644 app/helpers/bootstrap_form_helper.rb create mode 100644 app/helpers/custom_fields_helper.rb create mode 100644 app/helpers/database_helper.rb create mode 100644 app/helpers/my_modules_helper.rb create mode 100644 app/helpers/organizations_helper.rb create mode 100644 app/helpers/permission_helper.rb create mode 100644 app/helpers/project_activities_helper.rb create mode 100644 app/helpers/projects_helper.rb create mode 100644 app/helpers/reports_helper.rb create mode 100644 app/helpers/result_assets_helper.rb create mode 100644 app/helpers/result_comments_helper.rb create mode 100644 app/helpers/result_tables_helper.rb create mode 100644 app/helpers/result_texts_helper.rb create mode 100644 app/helpers/results_helper.rb create mode 100644 app/helpers/sample_groups_helper.rb create mode 100644 app/helpers/sample_types_helper.rb create mode 100644 app/helpers/samples_helper.rb create mode 100644 app/helpers/search_helper.rb create mode 100644 app/helpers/secondary_navigation_helper.rb create mode 100644 app/helpers/sidebar_helper.rb create mode 100644 app/helpers/step_comments_helper.rb create mode 100644 app/helpers/steps_helper.rb create mode 100644 app/helpers/user_my_modules_helper.rb create mode 100644 app/mailers/.keep create mode 100644 app/models/.keep create mode 100644 app/models/activity.rb create mode 100644 app/models/asset.rb create mode 100644 app/models/asset_text_datum.rb create mode 100644 app/models/checklist.rb create mode 100644 app/models/checklist_item.rb create mode 100644 app/models/comment.rb create mode 100644 app/models/concerns/.keep create mode 100644 app/models/concerns/archivable_model.rb create mode 100644 app/models/concerns/searchable_model.rb create mode 100644 app/models/connection.rb create mode 100644 app/models/custom_field.rb create mode 100644 app/models/log.rb create mode 100644 app/models/my_module.rb create mode 100644 app/models/my_module_comment.rb create mode 100644 app/models/my_module_group.rb create mode 100644 app/models/my_module_tag.rb create mode 100644 app/models/organization.rb create mode 100644 app/models/project.rb create mode 100644 app/models/project_comment.rb create mode 100644 app/models/report.rb create mode 100644 app/models/report_element.rb create mode 100644 app/models/result.rb create mode 100644 app/models/result_asset.rb create mode 100644 app/models/result_comment.rb create mode 100644 app/models/result_table.rb create mode 100644 app/models/result_text.rb create mode 100644 app/models/sample.rb create mode 100644 app/models/sample_comment.rb create mode 100644 app/models/sample_custom_field.rb create mode 100644 app/models/sample_group.rb create mode 100644 app/models/sample_my_module.rb create mode 100644 app/models/sample_type.rb create mode 100644 app/models/step.rb create mode 100644 app/models/step_asset.rb create mode 100644 app/models/step_comment.rb create mode 100644 app/models/step_table.rb create mode 100644 app/models/table.rb create mode 100644 app/models/tag.rb create mode 100644 app/models/temp_file.rb create mode 100644 app/models/user.rb create mode 100644 app/models/user_my_module.rb create mode 100644 app/models/user_organization.rb create mode 100644 app/models/user_project.rb create mode 100644 app/utilities/first_time_data_generator.rb create mode 100644 app/utilities/users_generator.rb create mode 100644 app/views/activities/_activity.html.erb create mode 100644 app/views/activities/_index.html.erb create mode 100644 app/views/activities/_list.html.erb create mode 100644 app/views/activities/index.html.erb create mode 100644 app/views/canvas/_edit.html.erb create mode 100644 app/views/canvas/_full_zoom.html.erb create mode 100644 app/views/canvas/_medium_zoom.html.erb create mode 100644 app/views/canvas/_small_zoom.html.erb create mode 100644 app/views/canvas/_tags.html.erb create mode 100644 app/views/canvas/edit/_my_module.html.erb create mode 100644 app/views/canvas/edit/modal/_delete_module.html.erb create mode 100644 app/views/canvas/edit/modal/_delete_module_group.html.erb create mode 100644 app/views/canvas/edit/modal/_edit_module.html.erb create mode 100644 app/views/canvas/edit/modal/_edit_module_group.html.erb create mode 100644 app/views/canvas/edit/modal/_new_module.html.erb create mode 100644 app/views/canvas/full_zoom/_my_module.html.erb create mode 100644 app/views/canvas/medium_zoom/_my_module.html.erb create mode 100644 app/views/canvas/small_zoom/_my_module.html.erb create mode 100644 app/views/custom_fields/_new_modal.html.erb create mode 100644 app/views/devise/confirmations/new.html.erb create mode 100644 app/views/devise/mailer/confirmation_instructions.html.erb create mode 100644 app/views/devise/mailer/reset_password_instructions.html.erb create mode 100644 app/views/devise/mailer/unlock_instructions.html.erb create mode 100644 app/views/devise/passwords/edit.html.erb create mode 100644 app/views/devise/passwords/new.html.erb create mode 100644 app/views/devise/sessions/new.html.erb create mode 100644 app/views/devise/shared/_links.html.erb create mode 100644 app/views/devise/unlocks/new.html.erb create mode 100644 app/views/layouts/application.html.erb create mode 100644 app/views/layouts/fluid.html.erb create mode 100644 app/views/layouts/main.html.erb create mode 100644 app/views/my_module_comments/_comment.html.erb create mode 100644 app/views/my_module_comments/_index.html.erb create mode 100644 app/views/my_module_comments/_list.html.erb create mode 100644 app/views/my_module_comments/new.html.erb create mode 100644 app/views/my_module_tags/_index_edit.html.erb create mode 100644 app/views/my_module_tags/new.html.erb create mode 100644 app/views/my_modules/_activities.html.erb create mode 100644 app/views/my_modules/_description.html.erb create mode 100644 app/views/my_modules/_description_label.html.erb create mode 100644 app/views/my_modules/_due_date.html.erb create mode 100644 app/views/my_modules/_due_date_label.html.erb create mode 100644 app/views/my_modules/_module_header.html.erb create mode 100644 app/views/my_modules/_module_header_due_date_label.html.erb create mode 100644 app/views/my_modules/_result.html.erb create mode 100644 app/views/my_modules/_result_user_generated.html.erb create mode 100644 app/views/my_modules/_show.html.erb create mode 100644 app/views/my_modules/_step.html.erb create mode 100644 app/views/my_modules/_tags.html.erb create mode 100644 app/views/my_modules/activities.html.erb create mode 100644 app/views/my_modules/activities/_activity.html.erb create mode 100644 app/views/my_modules/activities/_list_activities.html.erb create mode 100644 app/views/my_modules/archive.html.erb create mode 100644 app/views/my_modules/archive/_result.html.erb create mode 100644 app/views/my_modules/edit.html.erb create mode 100644 app/views/my_modules/modals/_manage_description_modal.html.erb create mode 100644 app/views/my_modules/modals/_manage_due_date_modal.html.erb create mode 100644 app/views/my_modules/modals/_manage_module_tags_modal.html.erb create mode 100644 app/views/my_modules/modals/_manage_users_modal.html.erb create mode 100644 app/views/my_modules/results.html.erb create mode 100644 app/views/my_modules/samples.html.erb create mode 100644 app/views/my_modules/show.html.erb create mode 100644 app/views/my_modules/steps.html.erb create mode 100644 app/views/organizations/_parse_error.html.erb create mode 100644 app/views/organizations/_parse_errors.html.erb create mode 100644 app/views/project_activities/_index.html.erb create mode 100644 app/views/project_activities/index.html.erb create mode 100644 app/views/project_comments/_comment.html.erb create mode 100644 app/views/project_comments/_index.html.erb create mode 100644 app/views/project_comments/_list.html.erb create mode 100644 app/views/project_comments/new.html.erb create mode 100644 app/views/projects/_edit.html.erb create mode 100644 app/views/projects/_new.html.erb create mode 100644 app/views/projects/_notifications.html.erb create mode 100644 app/views/projects/archive.html.erb create mode 100644 app/views/projects/archive/_org_projects.html.erb create mode 100644 app/views/projects/archive/_project.html.erb create mode 100644 app/views/projects/canvas.html.erb create mode 100644 app/views/projects/index.html.erb create mode 100644 app/views/projects/index/_org_projects.html.erb create mode 100644 app/views/projects/index/_project.html.erb create mode 100644 app/views/projects/module_archive.html.erb create mode 100644 app/views/projects/module_archive/_my_module.html.erb create mode 100644 app/views/projects/samples.html.erb create mode 100644 app/views/projects/show.html.erb create mode 100644 app/views/reports/_new.html.erb create mode 100644 app/views/reports/elements/_module_element_controls.html.erb create mode 100644 app/views/reports/elements/_my_module_activity_element.html.erb create mode 100644 app/views/reports/elements/_my_module_element.html.erb create mode 100644 app/views/reports/elements/_my_module_samples_element.html.erb create mode 100644 app/views/reports/elements/_new_element.html.erb create mode 100644 app/views/reports/elements/_project_header_element.html.erb create mode 100644 app/views/reports/elements/_result_asset_element.html.erb create mode 100644 app/views/reports/elements/_result_comments_element.html.erb create mode 100644 app/views/reports/elements/_result_table_element.html.erb create mode 100644 app/views/reports/elements/_result_text_element.html.erb create mode 100644 app/views/reports/elements/_step_asset_element.html.erb create mode 100644 app/views/reports/elements/_step_checklist_element.html.erb create mode 100644 app/views/reports/elements/_step_comments_element.html.erb create mode 100644 app/views/reports/elements/_step_element.html.erb create mode 100644 app/views/reports/elements/_step_table_element.html.erb create mode 100644 app/views/reports/index.html.erb create mode 100644 app/views/reports/new/_report_navigation.html.erb create mode 100644 app/views/reports/new/_report_sidebar.html.erb create mode 100644 app/views/reports/new/modal/_module_contents.html.erb create mode 100644 app/views/reports/new/modal/_module_contents_inner.html.erb create mode 100644 app/views/reports/new/modal/_project_contents.html.erb create mode 100644 app/views/reports/new/modal/_project_contents_inner.html.erb create mode 100644 app/views/reports/new/modal/_result_contents.html.erb create mode 100644 app/views/reports/new/modal/_result_contents_inner.html.erb create mode 100644 app/views/reports/new/modal/_save.html.erb create mode 100644 app/views/reports/new/modal/_step_contents.html.erb create mode 100644 app/views/reports/new/modal/_step_contents_inner.html.erb create mode 100644 app/views/reports/new_by_module.html.erb create mode 100644 app/views/reports/report.pdf.erb create mode 100644 app/views/result_assets/_edit.html.erb create mode 100644 app/views/result_assets/_new.html.erb create mode 100644 app/views/result_comments/_comment.html.erb create mode 100644 app/views/result_comments/_index.html.erb create mode 100644 app/views/result_comments/_list.html.erb create mode 100644 app/views/result_comments/new.html.erb create mode 100644 app/views/result_tables/_download.txt.erb create mode 100644 app/views/result_tables/_edit.html.erb create mode 100644 app/views/result_tables/_new.html.erb create mode 100644 app/views/result_texts/_edit.html.erb create mode 100644 app/views/result_texts/_new.html.erb create mode 100644 app/views/results/_result_asset.html.erb create mode 100644 app/views/results/_result_table.html.erb create mode 100644 app/views/results/_result_text.html.erb create mode 100644 app/views/sample_groups/edit.html.erb create mode 100644 app/views/sample_my_modules/_index.html.erb create mode 100644 app/views/sample_types/edit.html.erb create mode 100644 app/views/samples/_create_sample_group_modal.html.erb create mode 100644 app/views/samples/_create_sample_type_modal.html.erb create mode 100644 app/views/samples/_delete_samples_modal.html.erb create mode 100644 app/views/samples/_import_samples_modal.html.erb create mode 100644 app/views/samples/_parse_samples_modal.html.erb create mode 100644 app/views/search/index.html.erb create mode 100644 app/views/search/new.html.erb create mode 100644 app/views/search/results/_assets.html.erb create mode 100644 app/views/search/results/_comments.html.erb create mode 100644 app/views/search/results/_contents.erb create mode 100644 app/views/search/results/_modules.html.erb create mode 100644 app/views/search/results/_projects.html.erb create mode 100644 app/views/search/results/_reports.html.erb create mode 100644 app/views/search/results/_results.html.erb create mode 100644 app/views/search/results/_samples.html.erb create mode 100644 app/views/search/results/_steps.html.erb create mode 100644 app/views/search/results/_tags.html.erb create mode 100644 app/views/search/results/_workflows.html.erb create mode 100644 app/views/search/results/partials/_asset_text.html.erb create mode 100644 app/views/search/results/partials/_content_text.html.erb create mode 100644 app/views/search/results/partials/_my_module_text.html.erb create mode 100644 app/views/search/results/partials/_organization_text.html.erb create mode 100644 app/views/search/results/partials/_project_text.html.erb create mode 100644 app/views/search/results/partials/_report_text.html.erb create mode 100644 app/views/search/results/partials/_result_text.html.erb create mode 100644 app/views/search/results/partials/_step_text.html.erb create mode 100644 app/views/search/results/partials/_tag_text.html.erb create mode 100644 app/views/shared/_navigation.html.erb create mode 100644 app/views/shared/_sample.html.erb create mode 100644 app/views/shared/_samples.html.erb create mode 100644 app/views/shared/_secondary_navigation.html.erb create mode 100644 app/views/shared/_sidebar.html.erb create mode 100644 app/views/step_comments/_comment.html.erb create mode 100644 app/views/step_comments/_index.html.erb create mode 100644 app/views/step_comments/_list.html.erb create mode 100644 app/views/step_comments/new.html.erb create mode 100644 app/views/steps/_edit.html.erb create mode 100644 app/views/steps/_empty_step.html.erb create mode 100644 app/views/steps/_form_assets.html.erb create mode 100644 app/views/steps/_form_checklists.html.erb create mode 100644 app/views/steps/_form_tables.html.erb create mode 100644 app/views/steps/_new.html.erb create mode 100644 app/views/user_my_modules/_index.html.erb create mode 100644 app/views/user_my_modules/_index_edit.html.erb create mode 100644 app/views/user_my_modules/new.html.erb create mode 100644 app/views/user_projects/_index.html.erb create mode 100644 app/views/user_projects/_index_edit.html.erb create mode 100644 app/views/user_projects/edit.html.erb create mode 100644 app/views/user_projects/new.html.erb create mode 100644 app/views/users/invitations/edit.html.erb create mode 100644 app/views/users/invitations/new.html.erb create mode 100644 app/views/users/mailer/invitation_instructions.html.erb create mode 100644 app/views/users/registrations/edit.html.erb create mode 100644 app/views/users/registrations/new.html.erb create mode 100644 app/views/users/settings/_navigation.html.erb create mode 100644 app/views/users/settings/new_organization.html.erb create mode 100644 app/views/users/settings/organization.html.erb create mode 100644 app/views/users/settings/organizations.html.erb create mode 100644 app/views/users/settings/organizations/_add_user_modal.html.erb create mode 100644 app/views/users/settings/organizations/_breadcrumbs.html.erb create mode 100644 app/views/users/settings/organizations/_description_label.html.erb create mode 100644 app/views/users/settings/organizations/_description_modal.html.erb create mode 100644 app/views/users/settings/organizations/_description_modal_body.html.erb create mode 100644 app/views/users/settings/organizations/_destroy_modal.html.erb create mode 100644 app/views/users/settings/organizations/_destroy_user_organization_modal.html.erb create mode 100644 app/views/users/settings/organizations/_destroy_user_organization_modal_body.html.erb create mode 100644 app/views/users/settings/organizations/_existing_users_search_results.html.erb create mode 100644 app/views/users/settings/organizations/_leave_user_organization_modal.html.erb create mode 100644 app/views/users/settings/organizations/_leave_user_organization_modal_body.html.erb create mode 100644 app/views/users/settings/organizations/_name_modal.html.erb create mode 100644 app/views/users/settings/organizations/_name_modal_body.html.erb create mode 100644 app/views/users/settings/organizations/_user_dropdown.html.erb create mode 100644 app/views/users/settings/preferences.html.erb create mode 100644 bin/bundle create mode 100644 bin/delayed_job create mode 100644 bin/rails create mode 100644 bin/rake create mode 100644 bin/setup create mode 100644 config.ru create mode 100644 config/application.rb create mode 100644 config/boot.rb create mode 100644 config/database.yml create mode 100644 config/environment.rb create mode 100644 config/environments/development.rb create mode 100644 config/environments/production.rb create mode 100644 config/environments/test.rb create mode 100644 config/initializers/assets.rb create mode 100644 config/initializers/aws.rb create mode 100644 config/initializers/backtrace_silencers.rb create mode 100644 config/initializers/constants.rb create mode 100644 config/initializers/cookies_serializer.rb create mode 100644 config/initializers/devise.rb create mode 100644 config/initializers/devise_async.rb create mode 100644 config/initializers/filter_parameter_logging.rb create mode 100644 config/initializers/inflections.rb create mode 100644 config/initializers/mime_types.rb create mode 100644 config/initializers/paperclip.rb create mode 100644 config/initializers/session_store.rb create mode 100644 config/initializers/wicked_pdf.rb create mode 100644 config/initializers/wrap_parameters.rb create mode 100644 config/locales/devise.en.yml create mode 100644 config/locales/devise_invitable.en.yml create mode 100644 config/locales/en.yml create mode 100644 config/puma.rb create mode 100644 config/routes.rb create mode 100644 config/secrets.yml create mode 100644 config/skylight.yml create mode 100644 db/load_users_template.yml create mode 100644 db/migrate/20150713060702_devise_create_users.rb create mode 100644 db/migrate/20150713061603_add_user_columns.rb create mode 100644 db/migrate/20150713063224_create_organizations.rb create mode 100644 db/migrate/20150713070738_create_user_organizations.rb create mode 100644 db/migrate/20150713071921_create_projects.rb create mode 100644 db/migrate/20150713072417_create_user_projects.rb create mode 100644 db/migrate/20150714125221_add_archive_to_projects.rb create mode 100644 db/migrate/20150715122019_create_logs.rb create mode 100644 db/migrate/20150715124934_create_my_modules.rb create mode 100644 db/migrate/20150715131400_create_project_comments.rb create mode 100644 db/migrate/20150715132459_create_my_module_comments.rb create mode 100644 db/migrate/20150715132920_create_tags.rb create mode 100644 db/migrate/20150715133511_create_my_modules_and_tags.rb create mode 100644 db/migrate/20150715133709_create_my_module_groups.rb create mode 100644 db/migrate/20150715134133_create_connections.rb create mode 100644 db/migrate/20150715135452_create_steps.rb create mode 100644 db/migrate/20150715141810_create_assets.rb create mode 100644 db/migrate/20150715142704_create_step_assets.rb create mode 100644 db/migrate/20150715142929_create_result_assets.rb create mode 100644 db/migrate/20150715143134_create_results.rb create mode 100644 db/migrate/20150716060140_create_result_comments.rb create mode 100644 db/migrate/20150716061004_create_comments.rb create mode 100644 db/migrate/20150716061555_create_step_comments.rb create mode 100644 db/migrate/20150716061937_create_tables.rb create mode 100644 db/migrate/20150716062013_create_checklists.rb create mode 100644 db/migrate/20150716062110_create_checklist_items.rb create mode 100644 db/migrate/20150716062801_create_samples.rb create mode 100644 db/migrate/20150716064453_create_activities.rb create mode 100644 db/migrate/20150716120130_create_sample_my_modules.rb create mode 100644 db/migrate/20150716120659_create_sample_comments.rb create mode 100644 db/migrate/20150717084645_create_result_texts.rb create mode 100644 db/migrate/20150717085043_create_step_tables.rb create mode 100644 db/migrate/20150717085133_create_result_tables.rb create mode 100644 db/migrate/20150722095027_create_user_my_modules.rb create mode 100644 db/migrate/20150722112911_add_foreign_keys.rb create mode 100644 db/migrate/20150723134648_add_confirmable_to_devise.rb create mode 100644 db/migrate/20150730090021_add_my_module_groups_to_project.rb create mode 100644 db/migrate/20150804055341_add_color_to_tags.rb create mode 100644 db/migrate/20150820120553_create_sample_groups.rb create mode 100644 db/migrate/20150820123018_create_sample_types.rb create mode 100644 db/migrate/20150820124022_add_default_columns_to_samples.rb create mode 100644 db/migrate/20150827130647_create_custom_fields.rb create mode 100644 db/migrate/20150827130822_create_sample_custom_fields.rb create mode 100644 db/migrate/20150911125914_add_project_to_tags.rb create mode 100644 db/migrate/20150915074650_create_reports.rb create mode 100644 db/migrate/20150923065605_create_temp_files.rb create mode 100644 db/migrate/20150923110208_add_archive_to_my_modules.rb create mode 100644 db/migrate/20150923154140_add_index_to_sample_name.rb create mode 100644 db/migrate/20150924115001_create_report_elements.rb create mode 100644 db/migrate/20150924181017_add_archive_to_results.rb create mode 100644 db/migrate/20151005122041_add_created_by_to_assets.rb create mode 100644 db/migrate/20151021082639_add_pg_trgm_support.rb create mode 100644 db/migrate/20151021085335_add_search_query_indexes.rb create mode 100644 db/migrate/20151022123530_remove_unique_organization_name_index.rb create mode 100644 db/migrate/20151028091615_add_counter_cache_to_samples.rb create mode 100644 db/migrate/20151103155048_add_btree_gist_extension.rb create mode 100644 db/migrate/20151111135802_reset_assigned_samples_counters.rb create mode 100644 db/migrate/20151117083839_add_project_reference_to_activity.rb create mode 100644 db/migrate/20151119141714_create_asset_text_data.rb create mode 100644 db/migrate/20151130160157_add_text_search_vector_to_asset_text_data.rb create mode 100644 db/migrate/20151203100514_add_workflow_order_to_my_modules.rb create mode 100644 db/migrate/20151207151820_add_timezone_to_user.rb create mode 100644 db/migrate/20151214110800_add_organization_management_support.rb create mode 100644 db/migrate/20151215103642_add_foreign_keys_to_tables.rb create mode 100644 db/migrate/20151215134147_add_text_search_vector_to_tables.rb create mode 100644 db/migrate/20151216095259_generate_text_search_vector_for_table_contents.rb create mode 100644 db/migrate/20160114155705_create_delayed_jobs.rb create mode 100644 db/migrate/20160118114850_remove_private_organizations.rb create mode 100644 db/migrate/20160119101947_add_position_to_checklist_item.rb create mode 100644 db/migrate/20160125200130_devise_invitable_add_to_users.rb create mode 100644 db/migrate/20160125205500_add_empty_field_to_asset.rb create mode 100644 db/migrate/20160201085344_add_tutorial_status_field_to_user.rb create mode 100644 db/migrate/20160205192344_migrate_organizations_structure.rb create mode 100644 db/schema.rb create mode 100644 db/seeds.rb create mode 100644 docker-compose.yml create mode 100644 lib/assets/.keep create mode 100644 lib/tasks/.keep create mode 100644 lib/tasks/data.rake create mode 100644 lib/tasks/db_fake_data.rake create mode 100644 lib/tasks/db_users.rake create mode 100644 lib/tasks/i18n_missing_keys.rake create mode 100644 lib/tasks/paperclip.rake create mode 100644 lib/tasks/web_stats.rake create mode 100644 log/.keep create mode 100644 public/403.html create mode 100644 public/404.html create mode 100644 public/422.html create mode 100644 public/500.html create mode 100644 public/favicon-16.png create mode 100644 public/favicon-32.png create mode 100644 public/favicon-48.png create mode 100644 public/favicon.ico create mode 100644 public/images/.keep create mode 100644 public/images/favicon-16.png create mode 100644 public/images/favicon-32.png create mode 100644 public/images/favicon-48.png create mode 100644 public/images/favicon.ico create mode 100644 public/images/icon/missing.png create mode 100644 public/images/icon_small/missing.png create mode 100644 public/images/logo.png create mode 100644 public/images/medium/missing.png create mode 100644 public/images/thumb/missing.png create mode 100644 public/robots.txt create mode 100644 test/controllers/.keep create mode 100644 test/controllers/activities_controller_test.rb create mode 100644 test/controllers/custom_fields_controller_test.rb create mode 100644 test/controllers/my_modules_controller_test.rb create mode 100644 test/controllers/organizations_controller_test.rb create mode 100644 test/controllers/project_activities_controller_test.rb create mode 100644 test/controllers/projects_controller_test.rb create mode 100644 test/controllers/result_assets_controller_test.rb create mode 100644 test/controllers/result_comments_controller_test.rb create mode 100644 test/controllers/result_tables_controller_test.rb create mode 100644 test/controllers/result_texts_controller_test.rb create mode 100644 test/controllers/sample_groups_controller_test.rb create mode 100644 test/controllers/sample_types_controller_test.rb create mode 100644 test/controllers/samples_controller_test.rb create mode 100644 test/controllers/search_controller_test.rb create mode 100644 test/controllers/step_comments_controller_test.rb create mode 100644 test/controllers/steps_controller_test.rb create mode 100644 test/controllers/user_my_modules_controller_test.rb create mode 100644 test/fixtures/.keep create mode 100644 test/fixtures/activities.yml create mode 100644 test/fixtures/asset_text_datum.yml create mode 100644 test/fixtures/assets.yml create mode 100644 test/fixtures/checklist_items.yml create mode 100644 test/fixtures/checklists.yml create mode 100644 test/fixtures/comments.yml create mode 100644 test/fixtures/connections.yml create mode 100644 test/fixtures/custom_fields.yml create mode 100644 test/fixtures/logs.yml create mode 100644 test/fixtures/my_module_comments.yml create mode 100644 test/fixtures/my_module_groups.yml create mode 100644 test/fixtures/my_modules.yml create mode 100644 test/fixtures/organizations.yml create mode 100644 test/fixtures/project_comments.yml create mode 100644 test/fixtures/projects.yml create mode 100644 test/fixtures/report_elements.yml create mode 100644 test/fixtures/reports.yml create mode 100644 test/fixtures/result_assets.yml create mode 100644 test/fixtures/result_comments.yml create mode 100644 test/fixtures/result_tables.yml create mode 100644 test/fixtures/result_texts.yml create mode 100644 test/fixtures/results.yml create mode 100644 test/fixtures/sample_comments.yml create mode 100644 test/fixtures/sample_custom_fields.yml create mode 100644 test/fixtures/sample_groups.yml create mode 100644 test/fixtures/sample_my_modules.yml create mode 100644 test/fixtures/sample_types.yml create mode 100644 test/fixtures/samples.yml create mode 100644 test/fixtures/step_assets.yml create mode 100644 test/fixtures/step_comments.yml create mode 100644 test/fixtures/step_tables.yml create mode 100644 test/fixtures/steps.yml create mode 100644 test/fixtures/tables.yml create mode 100644 test/fixtures/tags.yml create mode 100644 test/fixtures/temp_files.yml create mode 100644 test/fixtures/user_my_modules.yml create mode 100644 test/fixtures/user_organizations.yml create mode 100644 test/fixtures/user_projects.yml create mode 100644 test/fixtures/users.yml create mode 100644 test/helpers/.keep create mode 100644 test/helpers/archivable_model_test_helper.rb create mode 100644 test/helpers/fake_test_helper.rb create mode 100644 test/helpers/searchable_model_test_helper.rb create mode 100644 test/integration/.keep create mode 100644 test/integration/canvas_update_test.rb create mode 100644 test/mailers/.keep create mode 100644 test/models/.keep create mode 100644 test/models/activity_test.rb create mode 100644 test/models/asset_test.rb create mode 100644 test/models/asset_text_datum_test.rb create mode 100644 test/models/checklist_item_test.rb create mode 100644 test/models/checklist_test.rb create mode 100644 test/models/comment_test.rb create mode 100644 test/models/connection_test.rb create mode 100644 test/models/custom_field_test.rb create mode 100644 test/models/log_test.rb create mode 100644 test/models/my_module_comment_test.rb create mode 100644 test/models/my_module_group_test.rb create mode 100644 test/models/my_module_tag_test.rb create mode 100644 test/models/my_module_test.rb create mode 100644 test/models/organization_test.rb create mode 100644 test/models/project_comment_test.rb create mode 100644 test/models/project_test.rb create mode 100644 test/models/report_element_test.rb create mode 100644 test/models/report_test.rb create mode 100644 test/models/result_asset_test.rb create mode 100644 test/models/result_comment_test.rb create mode 100644 test/models/result_table_test.rb create mode 100644 test/models/result_test.rb create mode 100644 test/models/result_text_test.rb create mode 100644 test/models/sample_comment_test.rb create mode 100644 test/models/sample_custom_field_test.rb create mode 100644 test/models/sample_group_test.rb create mode 100644 test/models/sample_my_module_test.rb create mode 100644 test/models/sample_test.rb create mode 100644 test/models/sample_type_test.rb create mode 100644 test/models/step_asset_test.rb create mode 100644 test/models/step_comment_test.rb create mode 100644 test/models/step_table_test.rb create mode 100644 test/models/step_test.rb create mode 100644 test/models/table_test.rb create mode 100644 test/models/tag_test.rb create mode 100644 test/models/temp_file_test.rb create mode 100644 test/models/user_my_module_test.rb create mode 100644 test/models/user_organization_test.rb create mode 100644 test/models/user_project_test.rb create mode 100644 test/models/user_test.rb create mode 100644 test/test_helper.rb create mode 100644 vendor/assets/javascripts/.keep create mode 100644 vendor/assets/javascripts/Sortable.min.js create mode 100644 vendor/assets/javascripts/bootstrap-colorselector.js create mode 100644 vendor/assets/javascripts/canvas-to-blob.min.js create mode 100644 vendor/assets/javascripts/datatables.js create mode 100644 vendor/assets/javascripts/eventPause-min.js create mode 100644 vendor/assets/javascripts/handsontable.full.min.js create mode 100644 vendor/assets/javascripts/jquery.mousewheel.min.js create mode 100644 vendor/assets/javascripts/jquery.ui.touch-punch.min.js create mode 100644 vendor/assets/javascripts/jsPlumb-2.0.4-min.js create mode 100644 vendor/assets/javascripts/jsnetworkx.js create mode 100644 vendor/assets/stylesheets/.keep create mode 100644 vendor/assets/stylesheets/bootstrap-colorselector.scss create mode 100644 vendor/assets/stylesheets/datatables.css create mode 100644 vendor/assets/stylesheets/handsontable.full.min.scss diff --git a/.buildpacks b/.buildpacks new file mode 100644 index 000000000..44003558f --- /dev/null +++ b/.buildpacks @@ -0,0 +1,2 @@ +https://github.com/heroku/heroku-buildpack-ruby.git +https://github.com/weibeld/heroku-buildpack-graphviz.git diff --git a/.gemrc b/.gemrc new file mode 100644 index 000000000..6153a6e0f --- /dev/null +++ b/.gemrc @@ -0,0 +1 @@ +gem: --no-ri --no-rdoc diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..d7e291225 --- /dev/null +++ b/.gitignore @@ -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 \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..b06e493a5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +# Contributing to sciNote + +### TODO \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..e5f0c982b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,25 @@ +FROM rails:4.2.5 +MAINTAINER BioSistemika + +# 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 diff --git a/Gemfile b/Gemfile new file mode 100644 index 000000000..81323214d --- /dev/null +++ b/Gemfile @@ -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] diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 000000000..258dde4cb --- /dev/null +++ b/Gemfile.lock @@ -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 diff --git a/LICENSE-3RD-PARTY.txt b/LICENSE-3RD-PARTY.txt new file mode 100644 index 000000000..af8f15b91 --- /dev/null +++ b/LICENSE-3RD-PARTY.txt @@ -0,0 +1,2356 @@ +----------------------------------------------------------------------------- + Ruby +----------------------------------------------------------------------------- + +Ruby is copyrighted free software by Yukihiro Matsumoto . +You can redistribute it and/or modify it under either the terms of the +2-clause BSDL (see the file BSDL), or the conditions below: + + 1. You may make and give away verbatim copies of the source form of the + software without restriction, provided that you duplicate all of the + original copyright notices and associated disclaimers. + + 2. You may modify your copy of the software in any way, provided that + you do at least ONE of the following: + + a) place your modifications in the Public Domain or otherwise + make them Freely Available, such as by posting said + modifications to Usenet or an equivalent medium, or by allowing + the author to include your modifications in the software. + + b) use the modified software only within your corporation or + organization. + + c) give non-standard binaries non-standard names, with + instructions on where to get the original software distribution. + + d) make other distribution arrangements with the author. + + 3. You may distribute the software in object code or binary form, + provided that you do at least ONE of the following: + + a) distribute the binaries and library files of the software, + together with instructions (in the manual page or equivalent) + on where to get the original distribution. + + b) accompany the distribution with the machine-readable source of + the software. + + c) give non-standard binaries non-standard names, with + instructions on where to get the original software distribution. + + d) make other distribution arrangements with the author. + + 4. You may modify and include the part of the software into any other + software (possibly commercial). But some files in the distribution + are not written by the author, so that they are not under these terms. + + For the list of those files and their copying conditions, see the + file LEGAL. + + 5. The scripts and library files supplied as input to or produced as + output from the software do not automatically fall under the + copyright of the software, but belong to whomever generated them, + and may be sold commercially, and may be aggregated with this + software. + + 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. + +----------------------------------------------------------------------------- + Ruby on Rails +----------------------------------------------------------------------------- + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Figaro Ruby Gem +----------------------------------------------------------------------------- + +Copyright (c) 2012 Steve Richert + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + PG Ruby Gem +----------------------------------------------------------------------------- + +Ruby is copyrighted free software by Yukihiro Matsumoto . +You can redistribute it and/or modify it under either the terms of the +2-clause BSDL (see the file BSDL), or the conditions below: + + 1. You may make and give away verbatim copies of the source form of the + software without restriction, provided that you duplicate all of the + original copyright notices and associated disclaimers. + + 2. You may modify your copy of the software in any way, provided that + you do at least ONE of the following: + + a) place your modifications in the Public Domain or otherwise + make them Freely Available, such as by posting said + modifications to Usenet or an equivalent medium, or by allowing + the author to include your modifications in the software. + + b) use the modified software only within your corporation or + organization. + + c) give non-standard binaries non-standard names, with + instructions on where to get the original software distribution. + + d) make other distribution arrangements with the author. + + 3. You may distribute the software in object code or binary form, + provided that you do at least ONE of the following: + + a) distribute the binaries and library files of the software, + together with instructions (in the manual page or equivalent) + on where to get the original distribution. + + b) accompany the distribution with the machine-readable source of + the software. + + c) give non-standard binaries non-standard names, with + instructions on where to get the original software distribution. + + d) make other distribution arrangements with the author. + + 4. You may modify and include the part of the software into any other + software (possibly commercial). But some files in the distribution + are not written by the author, so that they are not under these terms. + + For the list of those files and their copying conditions, see the + file LEGAL. + + 5. The scripts and library files supplied as input to or produced as + output from the software do not automatically fall under the + copyright of the software, but belong to whomever generated them, + and may be sold commercially, and may be aggregated with this + software. + + 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. + +----------------------------------------------------------------------------- + Devise gem +----------------------------------------------------------------------------- + +Copyright 2009-2016 Plataformatec. http://plataformatec.com.br + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Devise invitable gem +----------------------------------------------------------------------------- + +Copyright (c) 2009 Sergio Cambra + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Bootstrap Sass Gem +----------------------------------------------------------------------------- + +The MIT License (MIT) + +Copyright (c) 2013-2016 Twitter, Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----------------------------------------------------------------------------- + Bootstrap +----------------------------------------------------------------------------- + +The MIT License (MIT) + +Copyright (c) 2011-2016 Twitter, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----------------------------------------------------------------------------- + Sass Rails Gem +----------------------------------------------------------------------------- + +Copyright (c) 2011-2016 Christopher Eppstein + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Sass +----------------------------------------------------------------------------- + +Copyright (c) 2006-2015 Hampton Catlin, Natalie Weizenbaum, and Chris Eppstein + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Rails Bootstrap Forms Gem +----------------------------------------------------------------------------- + +Copyright 2012-2014 Stephen Potenza + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Yomu Gem +----------------------------------------------------------------------------- + +Copyright (c) 2012 Erol Fornoles + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Apache Tika +----------------------------------------------------------------------------- + +Apache Tika is used by Yomu Gem and is licelsed by Apache License (http://www.apache.org/licenses/). + +----------------------------------------------------------------------------- + Moment-JS Rails Gem +----------------------------------------------------------------------------- + +Copyright 2011 Derek Prior + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + MomentJS +----------------------------------------------------------------------------- + +Copyright (c) 2011-2016 Tim Wood, Iskren Chernev, Moment.js contributors + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Bootstrap 3 Datetime Picker Rails Gem +----------------------------------------------------------------------------- + +Copyright (c) 2014 Trevor Strieber + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Bootstrap 3 Datetime Picker +----------------------------------------------------------------------------- + +The MIT License (MIT) + +Copyright (c) 2015 Jonathan Peterson (@Eonasdan) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +----------------------------------------------------------------------------- + Bootstrap Select Rails Gem +----------------------------------------------------------------------------- + +Copyright (c) 2013 Maciej Krajowski-Kukiel + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Bootstrap Select +----------------------------------------------------------------------------- + +The MIT License (MIT) + +Copyright (c) 2013-2015 bootstrap-select + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +----------------------------------------------------------------------------- + Uglifier Gem +----------------------------------------------------------------------------- + +Copyright (c) 2011 Ville Lautanala + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + UglifyJS +----------------------------------------------------------------------------- + +UglifyJS is released under the BSD license: + +Copyright 2012-2013 (c) Mihai Bazon + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: + + * Redistributions of source code must retain the above + copyright notice, this list of conditions and the following + disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials + provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER “AS IS” AND ANY +EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, +OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + +----------------------------------------------------------------------------- + jQuery Turbolinks Gem +----------------------------------------------------------------------------- + +The MIT License + +Copyright (c) 2012 Sasha Koss + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + jQuery Rails Gem +----------------------------------------------------------------------------- + +The MIT License + +Copyright (c) 2010-2016 Andre Arko + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----------------------------------------------------------------------------- + jQuery +----------------------------------------------------------------------------- + +Copyright jQuery Foundation and other contributors, https://jquery.org/ + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/jquery/jquery + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +All files located in the node_modules and external directories are +externally maintained libraries used by this software which have their +own licenses; we recommend you read them, as their terms may differ from +the terms above. + +----------------------------------------------------------------------------- + jQuery UI Rails Gem & jQuery UI +----------------------------------------------------------------------------- + +jQuery UI as well as this gem are licensed under the MIT license (see +below). + +Copyright jQuery Foundation and other contributors, https://jquery.org/ + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/jquery/jquery-ui + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +Copyright and related rights for sample code are waived via CC0. Sample +code is defined as all source code contained within the demos directory. + +CC0: http://creativecommons.org/publicdomain/zero/1.0/ + +==== + +All files located in the node_modules and external directories are +externally maintained libraries used by this software which have their +own licenses; we recommend you read them, as their terms may differ from +the terms above. + +----------------------------------------------------------------------------- + jQuery scrollto Rails Gem +----------------------------------------------------------------------------- + +This gem packages the scrollTo project for Rails. + +The scrollTo license can be found at https://raw.githubusercontent.com/JohnColvin/jquery-scrollto-rails/master/LICENSE and is copied below: +Apprise was created by Ariel Flesler. The project page is http://archive.plugins.jquery.com/project/ScrollTo. The javascript files claim as of version 1.4.1 that the project is dual licensed under MIT and GPL. + +Please respect the scrollTo licensing. + +The gem code is released under MIT license. + +The MIT License (MIT) + +Copyright (c) 2014 Ariel Flesler (scrollTo author), John Colvin (gem author) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----------------------------------------------------------------------------- + HammerJS Rails Gem +----------------------------------------------------------------------------- + +HammerJS is licensed under MIT license. + +The MIT License (MIT) + +Copyright (C) 2011-2014 by Jorik Tangelder (Eight Media) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + IntroJS Rails Gem +----------------------------------------------------------------------------- + +Copyright (c) 2012 Pablo Fernandez + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + IntroJS +----------------------------------------------------------------------------- + +Copyright (C) 2012 Afshin Mehrabani (afshin.meh@gmail.com) +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + JS Cookie Rails Gem +----------------------------------------------------------------------------- + +The MIT License (MIT) + +Copyright (c) 2015 Alessandro Lepore + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----------------------------------------------------------------------------- + JS Cookie +----------------------------------------------------------------------------- + +Copyright 2014 Klaus Hartl + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Spin JS Rails Gem +----------------------------------------------------------------------------- + +Copyright (c) 2013 Dmytrii Nagirniak + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----------------------------------------------------------------------------- + SpinJS +----------------------------------------------------------------------------- + +Copyright (c) 2011-2015 Felix Gnass [fgnass at gmail dot com] + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Underscore Rails +----------------------------------------------------------------------------- + +The MIT License (MIT) + +Copyright (c) 2013 Robin Wenglewski, Travis Jeffery + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Underscore JS +----------------------------------------------------------------------------- + +Copyright (c) 2009-2016 Jeremy Ashkenas, DocumentCloud and Investigative +Reporters & Editors + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Turbolinks Gem +----------------------------------------------------------------------------- + +Copyright 2012-2016 David Heinemeier Hansson + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + SDoc Gem +----------------------------------------------------------------------------- + +Copyright (c) 2014 Zachary Scott, Vladimir Kolesnikov, and Nathan Broadbent + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + + + +Darkfish RDoc HTML Generator + +Copyright (c) 2007, 2008, Michael Granger. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the author/s, nor the names of the project's + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + +RDoc is copyrighted free software. + +You can redistribute it and/or modify it under either the terms of the GPL +version 2 (see the file GPL), or the conditions below: + + 1. You may make and give away verbatim copies of the source form of the + software without restriction, provided that you duplicate all of the + original copyright notices and associated disclaimers. + + 2. You may modify your copy of the software in any way, provided that + you do at least ONE of the following: + + a) place your modifications in the Public Domain or otherwise + make them Freely Available, such as by posting said + modifications to Usenet or an equivalent medium, or by allowing + the author to include your modifications in the software. + + b) use the modified software only within your corporation or + organization. + + c) give non-standard binaries non-standard names, with + instructions on where to get the original software distribution. + + d) make other distribution arrangements with the author. + + 3. You may distribute the software in object code or binary form, + provided that you do at least ONE of the following: + + a) distribute the binaries and library files of the software, + together with instructions (in the manual page or equivalent) + on where to get the original distribution. + + b) accompany the distribution with the machine-readable source of + the software. + + c) give non-standard binaries non-standard names, with + instructions on where to get the original software distribution. + + d) make other distribution arrangements with the author. + + 4. You may modify and include the part of the software into any other + software (possibly commercial). But some files in the distribution + are not written by the author, so that they are not under these terms. + + For the list of those files and their copying conditions, see the + file LEGAL. + + 5. The scripts and library files supplied as input to or produced as + output from the software do not automatically fall under the + copyright of the software, but belong to whomever generated them, + and may be sold commercially, and may be aggregated with this + software. + + 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR + IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. + +----------------------------------------------------------------------------- + BCrypt Gem +----------------------------------------------------------------------------- + +(The MIT License) + +Copyright 2007-2011: + +* Coda Hale + +C implementation of the BCrypt algorithm by Solar Designer and placed in the +public domain. +jBCrypt is Copyright (c) 2006 Damien Miller . + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Logging Gem +----------------------------------------------------------------------------- + +The MIT License + +Copyright (c) 2015 Tim Pease + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Aspector Gem +----------------------------------------------------------------------------- + +Copyright (c) 2011 Guoliang Cao, Maciej Mensfeld + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + RGL Gem & RGL +----------------------------------------------------------------------------- + +RGL is Copyright (c) 2002,2004,2005,2008,2013,2015 by Horst Duchene. It is free software, and may be redistributed under the terms specified in the README file of the Ruby distribution. + +----------------------------------------------------------------------------- + Nested Form Fields Gem +----------------------------------------------------------------------------- + +Copyright (c) 2012 Nico Ritsche + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + AJAX Datatables Rails Gem +----------------------------------------------------------------------------- + +Copyright (c) 2012 Joel Quenneville + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Datatables JS +----------------------------------------------------------------------------- + +MIT license +Copyright (C) 2008-2016, SpryMedia Ltd. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Commit param routing gem +----------------------------------------------------------------------------- + +Copyright (c) 2013 Senthil V S + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Kaminari Gem +----------------------------------------------------------------------------- + +Copyright (c) 2011 Akira Matsuda + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + I18n-JS Gem +----------------------------------------------------------------------------- + +(The MIT License) + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Roo Gem +----------------------------------------------------------------------------- + +Copyright (c) 2008-2014 Thomas Preymesser, Ben Woosley + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Wicked PDF Gem +----------------------------------------------------------------------------- + +Copyright (c) 2008 Miles Z. Sterrett + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + wkhtmltopdf +----------------------------------------------------------------------------- + + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. + +----------------------------------------------------------------------------- + Remotipart Gem +----------------------------------------------------------------------------- + +Copyright (c) 2009 Greg Leppert + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + RedCarpet Gem +----------------------------------------------------------------------------- + +Copyright (c) 2009, Natacha Porté +Copyright (c) 2015, Vicent Marti + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----------------------------------------------------------------------------- + Markdown +----------------------------------------------------------------------------- + +Copyright © 2004, John Gruber +http://daringfireball.net/ +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +Neither the name “Markdown” nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +This software is provided by the copyright holders and contributors “as is” and any express or implied warranties, including, but not limited to, the implied warranties of merchantability and fitness for a particular purpose are disclaimed. In no event shall the copyright owner or contributors be liable for any direct, indirect, incidental, special, exemplary, or consequential damages (including, but not limited to, procurement of substitute goods or services; loss of use, data, or profits; or business interruption) however caused and on any theory of liability, whether in contract, strict liability, or tort (including negligence or otherwise) arising in any way out of the use of this software, even if advised of the possibility of such damage. + +----------------------------------------------------------------------------- + Faker Gem +----------------------------------------------------------------------------- + +Copyright (c) 2007-2010 Benjamin Curtis + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Paperclip Gem +----------------------------------------------------------------------------- + +Copyright (c) 2008-2016 Jon Yurek and thoughtbot, inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----------------------------------------------------------------------------- + AWS-SDK Gem +----------------------------------------------------------------------------- + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +----------------------------------------------------------------------------- + AWS-SDK V1 Gem +----------------------------------------------------------------------------- + +Copyright 2011-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"). You +may not use this file except in compliance with the License. A copy of +the License is located at + + http://aws.amazon.com/apache2.0/ + +or in the "license" file accompanying this file. This file is +distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +ANY KIND, either express or implied. See the License for the specific +language governing permissions and limitations under the License. + +----------------------------------------------------------------------------- + Delayed Job ActiveRecord Gem +----------------------------------------------------------------------------- + +Copyright (c) 2005 Tobias Lütke + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOa AND NONINFRINGEMENT. IN NO EVENT SaALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Delayed Job +----------------------------------------------------------------------------- + +Copyright (c) 2005 Tobias Luetke + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOa AND +NONINFRINGEMENT. IN NO EVENT SaALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Devise Async Gem +----------------------------------------------------------------------------- + +Copyright (c) 2012 Marcelo Silveira + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Byebug Gem +----------------------------------------------------------------------------- + + +Copyright (c) David Rodríguez +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions +are met: +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +SUCH DAMAGE. + +----------------------------------------------------------------------------- + Web Console Gem +----------------------------------------------------------------------------- + +Copyright 2014-2016 Charlie Somerville, Genadi Samokovarov, Guillermo Iguaran and Ryan Dao + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Puma Ruby Server +----------------------------------------------------------------------------- + +Some code copyright (c) 2005, Zed Shaw +Copyright (c) 2011, Evan Phoenix +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. +* Neither the name of the Evan Phoenix nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----------------------------------------------------------------------------- + Rails 12factor Gem +----------------------------------------------------------------------------- + +The MIT License (MIT) + +Copyright (c) 2013,2014 Heroku + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----------------------------------------------------------------------------- + TZInfo::Data Gem +----------------------------------------------------------------------------- + +Copyright (c) 2005-2016 Philip Ross + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----------------------------------------------------------------------------- + SkyLight Gem +----------------------------------------------------------------------------- + +All other components of this product, except where otherwise noted, are Copyright (c) 2013-2014 Tilde, Inc. All rights reserved. + +Certain inventions disclosed in this file may be claimed within patents owned or patent applications filed by Tilde, Inc. or third parties. + +Subject to the terms of this notice, Tilde grants you a nonexclusive, nontransferable license, without the right to sublicense, to (a) install and execute one copy of these files on any number of workstations owned or controlled by you and (b) distribute verbatim copies of these files to third parties. As a condition to the foregoing grant, you must provide this notice along with each copy you distribute and you must not remove, alter, or obscure this notice. All other use, reproduction, modification, distribution, or other exploitation of these files is strictly prohibited, except as may be set forth in a separate written license agreement between you and Tilde. The terms of any such license agreement will control over this notice. The license stated above will be automatically terminated and revoked if you exceed its scope or violate any of the terms of this notice. + +This License does not grant permission to use the trade names, trademarks, service marks, or product names of Tilde, except as required for reasonable and customary use in describing the origin of this file and reproducing the content of this notice. You may not mark or brand this file with any trade name, trademarks, service marks, or product names other than the original brand (if any) provided by Tilde. + +Unless otherwise expressly agreed by Tilde in a separate written license agreement, these files are provided AS IS, WITHOUT WARRANTY OF ANY KIND, including without any implied warranties of MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, TITLE, or NON-INFRINGEMENT. As a condition to your use of these files, you are solely responsible for such use. Tilde will have no liability to you for direct, indirect, consequential, incidental, special, or punitive damages or for lost profits or data. + +Other Licenses +ActiveSupport + +Copyright (c) 2005-2014 David Heinemeier Hansson + +Released under the MIT License. + +Original source at https://github.com/rails/rails/tree/master/activesupport. + +HighLine + +Copyright (c) 2014 James Edward Gray II, Gregory Brown, et al. + +Distributed under the user's choice of the GPL Version 2 or the Ruby software license. + +Original source at https://github.com/JEG2/highline. + +Thor + +Copyright (c) 2008 Yehuda Katz, Eric Hodel, et al. + +Released under the MIT License. + +Original source at https://github.com/erikhuda/thor. + +ThreadSafe + +Copyright (c) 2014 Charles Oliver Nutter, thedarkone, et al. + +Distributed under Apache License, Version 2.0, January 2004 + +Original source at https://github.com/ruby-concurrency/thread_safe. + +----------------------------------------------------------------------------- + i18n_missing_keys Rake Task License +----------------------------------------------------------------------------- + +Copyright (c) 2010 Jakob Skjerning + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----------------------------------------------------------------------------- + bootstrap-colorselector.js License +----------------------------------------------------------------------------- + +Copyright (c) 2013 Flaute + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +----------------------------------------------------------------------------- + canvas-to-blob.js +----------------------------------------------------------------------------- + +The JavaScript Canvas to Blob script is released under the MIT license. + +----------------------------------------------------------------------------- + eventPause.js License +----------------------------------------------------------------------------- + +eventPause.js v 1.0.0 +Author: sudhanshu yadav +s-yadav.github.com +Copyright (c) 2013 Sudhanshu Yadav. +Dual licensed under the MIT and GPL licenses + +----------------------------------------------------------------------------- + Handsontable JS +----------------------------------------------------------------------------- + +(The MIT License) + +Copyright (c) 2012-2014 Marcin Warpechowski +Copyright (c) 2015 Handsoncode sp. z o.o. + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + jQuery mousewheel JS +----------------------------------------------------------------------------- + +Copyright jQuery Foundation and other contributors +https://jquery.org/ + +This software consists of voluntary contributions made by many +individuals. For exact contribution history, see the revision history +available at https://github.com/jquery/jquery-mousewheel + +The following license applies to all parts of this software except as +documented below: + +==== + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +==== + +All files located in the node_modules and external directories are +externally maintained libraries used by this software which have their +own licenses; we recommend you read them, as their terms may differ from +the terms above. + +----------------------------------------------------------------------------- + jQuery UI Touch Punch +----------------------------------------------------------------------------- + +Project page: http://touchpunch.furf.com/ + +Copyright (c) 2012 David Furfero + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + JSNetworkX +----------------------------------------------------------------------------- + +JSNetworkX is distributed with the BSD license + +Copyright (C) 2012 Felix Kling + + +NetworkX code is distributed with the BSD license + + +Copyright (C) 2004-2011, NetworkX Developers +Aric Hagberg +Dan Schult +Pieter Swart +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +* Neither the name of the NetworkX Developers nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +----------------------------------------------------------------------------- + jsPlumb Community Edition License +----------------------------------------------------------------------------- + +Copyright (c) 2010 - 2014 jsPlumb, http://jsplumbtoolkit.com/ + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Sortable.min.js +----------------------------------------------------------------------------- + +Copyright 2013-2016 Lebedev Konstantin ibnRubaXa@gmail.com http://rubaxa.github.io/Sortable/ + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +----------------------------------------------------------------------------- + Simple Sidebar HTML Template +----------------------------------------------------------------------------- + +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. + +----------------------------------------------------------------------------- + Creative Loading Effects +----------------------------------------------------------------------------- + +By Codrops +http://tympanus.net/codrops/2013/09/18/creative-loading-effects/ + +Licensing & Terms of Use + +The resources on Codrops can be used freely in personal and commercial projects. Please note, that most of the tutorials and resources are experimental and not ready for production, but made for inspiration and demonstration purpose only. + +The resources on Codrops can be used in websites, web apps and web templates intended for sale. You don’t have to link back to us if it vitiates your work but we appreciate any credit. + +You are not allowed to take our work “as-is” and sell it, redistribute or re-publish it (with the exception of forking our GitHub repos), or sell “pluginized” versions of it. + +If you plan to create free WordPress, jQuery, Joomla, etc. plugins out of our scripts, please credit us in a fair way and link to the respective article on Codrops. + +Please, respect the licenses of the resources (audio, video or images) that we often use in our demos. We always indicate the license in the article and link to the owner/creator in both, article and demo. + +If you write about some of our work we would like you to add a link back to us. You are free to copy excerpts but please do not copy entire articles (e.g. RSS feed scraping), we put our heart into this work. Don’t re-publish our demos and our ZIP files, and don’t link directly to any ZIP file, link to the article instead. + +Please contact us, if you’d like to translate articles and re-publish them. + +Summarized, use it freely, integrate it, make it your own, but don’t copy and paste our work and sell it or claim that it’s yours, stay fair. \ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 000000000..5e806a327 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,377 @@ +Copyright (c) 2016 BioSistemika USA, LLC + +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. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..237cfb19f --- /dev/null +++ b/Makefile @@ -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 + diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..f787d4ed7 --- /dev/null +++ b/Procfile @@ -0,0 +1,2 @@ +web: bundle exec puma -C config/puma.rb +worker: bundle exec rake jobs:work diff --git a/README.md b/README.md new file mode 100644 index 000000000..fd02d1575 --- /dev/null +++ b/README.md @@ -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_`, and organizations name `org_`. 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. \ No newline at end of file diff --git a/Rakefile b/Rakefile new file mode 100644 index 000000000..ba6b733dd --- /dev/null +++ b/Rakefile @@ -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 diff --git a/app/assets/images/.keep b/app/assets/images/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/assets/images/icon/missing.png b/app/assets/images/icon/missing.png new file mode 100644 index 0000000000000000000000000000000000000000..b13b08127eeba4ca630ebfa06ea6d235358a5fc1 GIT binary patch literal 1859 zcmV-J2fX-+P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qRNAp5A000KENklJ63*7t1Fuho$!Qja}Pb@dbvaH)wdoeKpA33#JH9~1tD z@DQ)q2pFE_Pp$KvGu4Zl#)~iUJwu{X1`|X6SU4nwLw?WZ;{5#5!keIXO9}^Lp#U2M z8v%2#Sf^|rQR|qa!UXz4r?GHA5<>AvSQ3O;`)q4l$4?CgsH3xcd0`JTNfCQB}=YZK=4JiisE^1`0Oko6G#hF+xTS zqmh=13}G~rh^6DP!5Xhx!T0>DS6Nv~NunH$Ft+NO)S5nU1q{1*;azv45t`xyF%&>< zjG9X(Y*rgbs#UY0E#UR$l5vF{4zl@%6Q62Y1ry_|E7)#e*SA4JMpFzHLq@`&=2P

_{9a~SnQ1j@C1<*XkW4HTG*4T&zW%eUW(!Q~z%*^@n66zl zXY5UhXk>C?s;Q}!>*a^k*wi{cI*x5l4EkBx)iq7dk4I!}(RvQJ$tuF=$igMX9QV0B zQoH_#Y1aZ&;W?Uhc6RslJQ*9C*l@X$QdBK5#EEG}v?`cX zmT%n^ppF?B`v8{Eb~dxOy?waN%eE@#rIz0%J4`_t3Pp$f)+q6MRj z>Nv(6SHWR`nPU~4&t~9BE3E9Rt1sWaefx5C_3@A2lX5bX&YYc|+zn=POp2=t#vE6{ zx~Pi)zdx?TB}q$V`t#x^;U;dTP7wD7bqw;|obs?ojU7cf{V_Ma0vt)BD(NiHO z$BUWtZn03_+dZk2w+neKlF8%oUYwtwpPgB)Gx*uu+QP9SQ&1KKd{am#OZhx{IW8ZZRw_7pwNwaccXXiOALE3;&(V>wlgi=Y zyFCmSY0*tPiu|HMKfuazqU)GAW?)3mz(^YhFs2!sElV4{pf3cNPA2>M2DM?TQBi8z&!vwBD%XQi@~sEKJ@waBq}a|?iJe2MS#sl;}bEk&a^bU7sS?CA*VpLoq> zi|Hi04&V~O?B3+epU2cR#zJaaJJU39+&#d?oFQ`|7%&=pI4T6NC`5|c47?I*3_GQO zRiw@?EUER(q<}F5W*UXZ))fsL7cPOZakgh=*);r&L#i4Z9nU5cs#ivZ0oWkRm)WIP z9|g>eli$QN`L6@oY1qVfT!@6cKD*6+$2|m>g}E2MguIv|Tr_W}{!LvmtExuLM0YW^ zrrBhNe4eE@Zw3d34TkzB5~r4yj^QEG%EAJ^B~*mGUgwGl>A=*ta3>hD4rl&7o%+^r!j#rl!dVBl0j~_&X z{e!OMMetOX4m@Liu&WlQh6cl&eOAlTMQxdy=012p*{wG4RF)1r{qrQ(`T;d#vFO06 xcW82Af@}SNvYE|Vmd^3y`2PyF+^`%g{{X!@mDwIAlDGf>002ovPDHLkV1mb%b6x-d literal 0 HcmV?d00001 diff --git a/app/assets/images/icon_small/missing.png b/app/assets/images/icon_small/missing.png new file mode 100644 index 0000000000000000000000000000000000000000..d931b189f7130ffa91d6ef5ae29c3a7ed93fe890 GIT binary patch literal 1278 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qRNAp5A000DSNklXn*5Jsc@|z^ zib74LP*#gtLEc#32nNSPp~+|4znV(1q!v*F1Lz7|x=VvRX!x~5fESp;%(A9b3|-FU zrlubMFg)h!$^1e|RgQEG1+W5`aOlZCdN&X|UzZEBmaf)Hh9(y#C#St!s-?x{N=dJn zI?7Wzl7Ta_vE9}ruJH%`kV|ni2gGXWTCHqW%jVqN6Cc#o&a<7yQMn4bO6W)?FQ%|h zSiQ%!y>+mR*BAsNwX#vSN|Duw59ihJ$d61s4mv_J6&Y*o#^M+!u3;H#!3(@jv5c5Z zW<>!@_Ob3N5D30Jj9G@B&!)2i=_nX?_j3ppPzTkJvR07xc6Kpmc*IZnn&3ScnivfP zpYQHc<0NsMBL~g1sD_&83Z%TfwH+7>dFOvyqk+)o#%4)T(2MW~m*f!HXc9E`7aNGi zKyW9OBg-_>f{;q`2m1%2C>CWU7E3HFu8fU^DGp|1FJe5$@m$hS6>N;9Y1k*AB^q}Z z&2`ao)ih43mGkCF=knsOH#aQ9Jer<4JdB;5wQg^3KYaMN-ENykNfgsJo%Y+;*H`DQ zvqqzFRKjv;pGX4lMHAPwohv|Bj$HKC);6_iJ01J<>GPM*U;cUjUJ%53wfg6E$41xD zCq1<2L6fYh>!K4#>*Tn7@#^i(wHFP&d-wN$A3s`U3(toKhi^JSC?Cxea5Nwf8T%o#}&%AjvL1n3nw07V%=`@Ij0d%*vWz$x1l3DEc4>*6m%xR z`nXU@*O3^?!$jROEls6C=Mi-Gg0=4u&)sAjSE?oSiPdT}KsR-LWHjjIPEXIAH=E#| z)@w-5J?Jt$==iqv-F#dE8PUxo2S+2<9#)FUvLpqA6aDB}RW3GbRm#QHN-u7hBiB>+ zo8a8n)HrJPE$1e$eH&xUhtLDc6a@zEsop22y!GVEvyMfn(R6u7|^<83t-% z{Ly-JUF2BGl~T^9zy*5H_7y0wiRZgJGmqzdpl@h)cK)}$Jxug+q2329`SL|%C4y_r zH}g9c3Qw#>B78jN0(-94qiep=56~|gKLhG_y}G>O8}|T3BT+ypu;+St>Hg2ky?VMD oae+P8OAAZBaSssC4v>le4>GHSI@GwNp8x;=07*qoM6N<$g1xR&JOBUy literal 0 HcmV?d00001 diff --git a/app/assets/images/logo.png b/app/assets/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b368cd36d776fc7eba12a74c515b31b7a86fa503 GIT binary patch literal 4652 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̙!xU^+mZKmSf9vp|a9#_)!a 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%0ASfTQ12%X&;-)cyhz0RQR8vr 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 P0DNfyGXHywb?E;AnHO#^ literal 0 HcmV?d00001 diff --git a/app/assets/images/thumb/missing.png b/app/assets/images/thumb/missing.png new file mode 100644 index 0000000000000000000000000000000000000000..cc0b6b9e67571ebaa005e9e25d2d888bf3015cd3 GIT binary patch literal 6943 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 literal 0 HcmV?d00001 diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js new file mode 100644 index 000000000..f1d6c2db1 --- /dev/null +++ b/app/assets/javascripts/application.js @@ -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('

'); + } + 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(''); + }); +}); diff --git a/app/assets/javascripts/custom_fields.js b/app/assets/javascripts/custom_fields.js new file mode 100644 index 000000000..dee720fac --- /dev/null +++ b/app/assets/javascripts/custom_fields.js @@ -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. diff --git a/app/assets/javascripts/direct-upload.js b/app/assets/javascripts/direct-upload.js new file mode 100644 index 000000000..1034a4ef8 --- /dev/null +++ b/app/assets/javascripts/direct-upload.js @@ -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)); + diff --git a/app/assets/javascripts/my_modules.js b/app/assets/javascripts/my_modules.js new file mode 100644 index 000000000..22bcf7957 --- /dev/null +++ b/app/assets/javascripts/my_modules.js @@ -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
  • + 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(); diff --git a/app/assets/javascripts/my_modules/activities.js b/app/assets/javascripts/my_modules/activities.js new file mode 100644 index 000000000..d43b1e1ed --- /dev/null +++ b/app/assets/javascripts/my_modules/activities.js @@ -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); + } + }); diff --git a/app/assets/javascripts/my_modules/results.js b/app/assets/javascripts/my_modules/results.js new file mode 100644 index 000000000..7e67618a4 --- /dev/null +++ b/app/assets/javascripts/my_modules/results.js @@ -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("
  • " + data.html + "
  • ") + .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(); +} diff --git a/app/assets/javascripts/my_modules/steps.js b/app/assets/javascripts/my_modules/steps.js new file mode 100644 index 000000000..68479312f --- /dev/null +++ b/app/assets/javascripts/my_modules/steps.js @@ -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("
  • " + data.html + "
  • ") + .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("" + assetError + ""); + } + }); + } + + processFile(); + ev.preventDefault(); +} diff --git a/app/assets/javascripts/navigation.js b/app/assets/javascripts/navigation.js new file mode 100644 index 000000000..ae028b1b2 --- /dev/null +++ b/app/assets/javascripts/navigation.js @@ -0,0 +1,4 @@ +/* Loading overlay for search */ +$("#search-bar").submit(function (){ + animateSpinner(document.body); +}); \ No newline at end of file diff --git a/app/assets/javascripts/organizations.js b/app/assets/javascripts/organizations.js new file mode 100644 index 000000000..dee720fac --- /dev/null +++ b/app/assets/javascripts/organizations.js @@ -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. diff --git a/app/assets/javascripts/project_activities.js b/app/assets/javascripts/project_activities.js new file mode 100644 index 000000000..dee720fac --- /dev/null +++ b/app/assets/javascripts/project_activities.js @@ -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. diff --git a/app/assets/javascripts/projects/canvas.js b/app/assets/javascripts/projects/canvas.js new file mode 100644 index 000000000..15f948a05 --- /dev/null +++ b/app/assets/javascripts/projects/canvas.js @@ -0,0 +1,3030 @@ +//************************************ +// CONSTANTS +//************************************ + +var NAME_VALID = 0; +var NAME_LENGTH_ERROR = -1; +var NAME_INVALID_CHARACTERS_ERROR = -2; +var NAME_WHITESPACES_ERROR = -3; + +var DRAG_INVALID = 0; +var DRAG_MOUSE = 1; +var DRAG_TOUCH = 2; + +// This JS code also contains some .css styling instructions. +// Those defaults are used in code where there is a "CSS_STYLE" comment. +var DEFAULT_ENDPOINT_STYLE = "Blank"; +var DEFAULT_CONNECTION_HOVER_STYLE = +{ + lineWidth: 5 +}; +var DEFAULT_CONNECTION_OVERLAY_STYLE = +[ "Arrow", { + location: 1, + id: "arrow", + length: 12, + width: 10, + foldback: 1 +} ]; +var DEFAULT_CONNECTION_LABEL_STYLE = +{ + label: "x", + id: "label", + cssClass: "connLabel" +}; +var DEFAULT_ANCHOR_STYLE = "Continuous"; +var DEFAULT_CONNECTOR_STYLE = +[ "Straight", { + gap: 2 +} ]; +var DEFAULT_CONNECTOR_STYLE_2 = +{ + strokeStyle: "#FFFFFF", + lineWidth: 1.5, + outlineColor: "transparent", + outlineWidth: 0 +}; + +// Zoom-level specific variables +var GRID_DIST_EDIT_X = 300; +var GRID_DIST_EDIT_Y = 135; +var EDIT_ENDPOINT_STYLE = +[ "Dot", { + radius: 4, + cssClass: "ep-normal", + hoverClass: "ep-hover" +} ]; +var EDIT_CONNECTOR_STYLE_2 = +{ + strokeStyle: "#FFFFFF", + lineWidth: 3, + outlineColor: "transparent", + outlineWidth: 0 +}; +var GRID_DIST_FULL_X = 340; +var GRID_DIST_FULL_Y = 201; +var GRID_DIST_MEDIUM_X = 250; +var GRID_DIST_MEDIUM_Y = 88; +var GRID_DIST_SMALL_X = 100; +var GRID_DIST_SMALL_Y = 100; +var SUBMIT_FORM_NAME_SEPARATOR = "|"; + +//************************************ +// GLOBAL VARIABLES +//************************************ + +// Current GUI mode +var currentMode = "full_zoom"; + +// JSNetworkX graph structure, used for graph analysis +var graph; + +// Instance of jsPlumb, a library for canvas manipulation +var instance; + +// ID "generator" for new modules +var newModuleIndex = 0; + +// Global variables for module dragging +var leftInitial = 0, topInitial = 0, collided = false; + +// Global variables for canvas dragging +var x_start = 0, y_start = 0; +var drag_type = DRAG_INVALID; +var draggable = null; + +// Draggable position (initial values specified here) +var draggableLeft = 0.5; +var draggableTop = 0.5; + +var ignoreUnsavedWorkAlert; + +// Global variable for hammer js +var hammertime; + +// Cookie data for tutorial +var tutorialData; + +/* + * As a guideline, all module elements should contain + * the following attributes: + * + * id - ID of the module. + * data-module-name - Name of the module. + * data-module-id - ID of the module. + * data-module-group - ID of the group the module belongs to (if it exists). + * data-module-x - X position of the module (integer). + * data-module-y - Y position of the module (integer). + * data-module-conns - List of module IDs this module is connected + * to (outbound connections). + */ + +//************************************ +// DEFAULT INITIALIZATION CODE +//************************************ +jsPlumb.ready(function () { + bindModeChange(); + bindAjax(); + bindWindowResizeEvent(); + initializeGraph(".diagram .module-large"); + initializeFullZoom(); + initializeTutorial(); +}); + +//************************************ +// INDIVIDUAL ACTION INIT & DESTROY +//************************************ + +function initializeEdit() { + newModuleIndex = 0; + ignoreUnsavedWorkAlert = false; + + // Read permissions from the data attributes of the form + var canEditModules = _.isEqual($("#update-canvas").data("can-edit-modules"), "yes"); + var canEditModuleGroups = _.isEqual($("#update-canvas").data("can-edit-module-groups"), "yes"); + var canCreateModules = _.isEqual($("#update-canvas").data("can-create-modules"), "yes"); + var canCloneModules = _.isEqual($("#update-canvas").data("can-clone-modules"), "yes"); + var canDeleteModules = _.isEqual($("#update-canvas").data("can-delete-modules"), "yes"); + var canDragModules = _.isEqual($("#update-canvas").data("can-reposition-modules"), "yes"); + var canEditConnections = _.isEqual($("#update-canvas").data("can-edit-connections"), "yes"); + + $("#canvas-container").addClass("canvas-container-edit-mode"); + + // Hide sidebar & also its toggle button + $("#wrapper").addClass("hidden2"); + $("#wrapper").find(".sidebar-header-toggle").hide(); + $(".navbar-secondary").addClass("navbar-without-sidebar"); + + // Also, hide zoom levels button group + $("#diagram-buttons").hide(); + + // Resize container + resizeContainer(); + + positionModules(".diagram .module", GRID_DIST_EDIT_X, GRID_DIST_EDIT_Y); + initJsPlumb( + "#diagram-container", + "#diagram", + "div.module", + { + scrollEnabled: true, + gridDistX: GRID_DIST_EDIT_X, + gridDistY: GRID_DIST_EDIT_Y, + endpointStyle: EDIT_ENDPOINT_STYLE, + connectorStyle2: EDIT_CONNECTOR_STYLE_2, + zoomEnabled: true, + modulesDraggable: canDragModules, + connectionsEditable: canEditConnections + } + ); + bindEditModeDropdownHandlers(); + if (canCreateModules) { + bindNewModuleAction(GRID_DIST_EDIT_X, GRID_DIST_EDIT_Y); + } + bindEditFormSubmission(GRID_DIST_EDIT_X, GRID_DIST_EDIT_Y); + + if (canEditModules) { + initEditModules(); + $(".edit-module").on("click touchstart", editModuleHandler); + } + + if (canEditModuleGroups) { + initEditModuleGroups(); + $(".edit-module-group").on("click touchstart", editModuleGroupHandler); + } + + if (canCloneModules) { + bindCloneModuleAction( + $(".module-options a.clone-module"), + ".diagram .module", + GRID_DIST_EDIT_X, + GRID_DIST_EDIT_Y); + bindCloneModuleGroupAction( + $(".module-options a.clone-module-group"), + ".diagram .module", + GRID_DIST_EDIT_X, + GRID_DIST_EDIT_Y); + } + if (canDeleteModules) { + bindDeleteModuleAction(); + bindDeleteModuleGroupAction(); + } + + bindEditModeCloseWindow(); + bindTouchDropdowns($(".dropdown-toggle")); + + // Restore draggable position + restoreDraggablePosition($("#diagram"), $("#canvas-container")); + + $("#canvas-container").submit(function (){ + animateSpinner( + this, + true, + { color: 'white', shadow: true } + ); + }); + + // Add edit canvas tutorial step and show it + if (showTutorial() && Cookies.get('current_tutorial_step') == '4') { + var editWorkflowTutorial = $("#canvas-container").attr("data-edit-workflow-step-text"); + Cookies.set('current_tutorial_step', '5'); + $(".introjs-overlay").remove(); + $(".introjs-helperLayer").remove(); + $(".introjs-tooltipReferenceLayer").remove(); + + introJs() + .setOptions({ + steps: [{ + intro: editWorkflowTutorial + }], + overlayOpacity: '0.1', + doneLabel: 'End tutorial', + showBullets: false, + showStepNumbers: false, + tooltipClass: 'custom disabled-next' + }) + .start(); + + $(".introjs-overlay").addClass("introjs-no-overlay"); + var positionLeft = $(".introjs-tooltipReferenceLayer").position().left / 4; + $(".introjs-tooltipReferenceLayer") + .addClass("bring-to-front") + .css({ left: positionLeft + 'px' }); + + // 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 destroyEdit() { + // Read permissions from the data attributes of the form + var canCreateModules = _.isEqual($("#update-canvas").data("can-create-modules"), "yes"); + var canCloneModules = _.isEqual($("#update-canvas").data("can-clone-modules"), "yes"); + var canDeleteModules = _.isEqual($("#update-canvas").data("can-delete-modules"), "yes"); + + instance.cleanupListeners(); + $(".dropdown").off("show.bs.dropdown hide.bs.dropdown"); + $("#diagram-container").off("mousewheel mousedown mouseup mousemove"); + hammertime.off('pinch'); + $("form#update-canvas").off("submit"); + if (canDeleteModules) { + $(".delete-container a").off("click"); + $("#modal-delete-module").off("show.bs.modal hide.bs.modal"); + $("#modal-delete-module").find("button[data-action='confirm']").off("click"); + + $(".buttons-container a.delete-module").off("click touchstart"); + $(".buttons-container a.delete-module-group").off("click touchstart"); + $("#modal-delete-module-group").off("show.bs.modal hide.bs.modal"); + $("#modal-delete-module-group").find("button[data-action='confirm']").off("click"); + } + if (canCreateModules) { + $("#modal-new-module").off("show.bs.modal shown.bs.modal hide.bs.modal"); + $("#modal-new-module").find("button[data-action='confirm']").off("click"); + $("#canvas-new-module").draggable("destroy"); + $("#canvas-new-module").off("click"); + } + if (canCloneModules) { + $(".buttons-container a.clone-module").off("click touchstart"); + $(".buttons-container a.clone-module-group").off("click touchstart"); + } + + $("#update-canvas .cancel-edit-canvas").off("click"); + $(window).off("beforeunload"); + $(document).off("page:before-change"); + $(".dropdown-toggle").off("touchstart"); + + // Remember the draggable position + rememberDraggablePosition($("#diagram"), $("#canvas-container")); +} + +function initializeFullZoom() { + // Resize container + resizeContainer(); + + positionModules(".diagram .module-large", GRID_DIST_FULL_X, GRID_DIST_FULL_Y); + initJsPlumb( + "#diagram-container", + "#diagram", + "div.module-large", + { + scrollEnabled: true, + gridDistX: GRID_DIST_FULL_X, + gridDistY: GRID_DIST_FULL_Y + }); + bindEditDueDateAjax(); + bindEditTagsAjax($("div.module-large")); + bindFullZoomAjaxTabs(); + initModulesHover($("div.module-large"), $("#slide-panel")); + initSidebarClicks($("div.module-large"), $("#slide-panel"), $("#diagram"), $("#canvas-container"), 20); + + // Restore draggable position + restoreDraggablePosition($("#diagram"), $("#canvas-container")); +} + +function destroyFullZoom() { + instance.cleanupListeners(); + $("#diagram-container").off("mousedown mouseup mousemove"); + $(".module-large .buttons-container [role=tab]").off("ajax:before ajax:success ajax:error"); + $("div.module-large").off("mouseenter mouseleave"); + $("div.module-large a.due-date-link").off("ajax:success ajax:error"); + $("#manage-module-description-modal [data-action='submit']").off("click"); + $("#manage-module-due-date-modal [data-action='submit']").off("click"); + $("div.module-large a.edit-tags-link").off("ajax:before ajax:success"); + $("li[data-module-group]").off("mouseenter mouseleave"); + $("li[data-module-group] > span > a.canvas-center-on").off("click"); + $("li[data-module-id]").off("mouseenter mouseleave"); + $("li[data-module-id] > span > a.canvas-center-on").off("click"); + + // Remember the draggable position + rememberDraggablePosition($("#diagram"), $("#canvas-container")); +} + +function initializeMediumZoom() { + // Resize container + resizeContainer(); + + positionModules(".diagram .module-medium", GRID_DIST_MEDIUM_X, GRID_DIST_MEDIUM_Y); + initJsPlumb("#diagram-container", "#diagram", "div.module-medium", { scrollEnabled: true, gridDistX: GRID_DIST_MEDIUM_X, gridDistY: GRID_DIST_MEDIUM_Y }); + bindEditTagsAjax($("div.module-medium")); + initModulesHover($("div.module-medium"), $("#slide-panel")); + initSidebarClicks($("div.module-medium"), $("#slide-panel"), $("#diagram"), $("#canvas-container"), 20); + + // Restore draggable position + restoreDraggablePosition($("#diagram"), $("#canvas-container")); +} + +function destroyMediumZoom() { + instance.cleanupListeners(); + $("#diagram-container").off("mousedown mouseup mousemove"); + $("div.module-medium").off("mouseenter mouseleave"); + $("div.module-medium a.edit-tags-link").off("ajax:before ajax:success"); + $("li[data-module-group]").off("mouseenter mouseleave"); + $("li[data-module-group] > span > a.canvas-center-on").off("click"); + $("li[data-module-id]").off("mouseenter mouseleave"); + $("li[data-module-id] > span > a.canvas-center-on").off("click"); + + // Remember the draggable position + rememberDraggablePosition($("#diagram"), $("#canvas-container")); +} + +function initializeSmallZoom() { + // Resize container + resizeContainer(); + + positionModules(".diagram .module-small", GRID_DIST_SMALL_X, GRID_DIST_SMALL_Y); + initJsPlumb("#diagram-container", "#diagram", "div.module-small", { scrollEnabled: true, gridDistX: GRID_DIST_SMALL_X, gridDistY: GRID_DIST_SMALL_Y }); + initModulesHover($("div.module-small"), $("#slide-panel")); + initSidebarClicks($("div.module-small"), $("#slide-panel"), $("#diagram"), $("#canvas-container"), 20); + + // Restore draggable position + restoreDraggablePosition($("#diagram"), $("#canvas-container")); +} + +function destroySmallZoom() { + instance.cleanupListeners(); + $("#diagram-container").off("mousedown mouseup mousemove"); + $("div.module-small").off("mouseenter mouseleave"); + $("li[data-module-group]").off("mouseenter mouseleave"); + $("li[data-module-group] > span > a.canvas-center-on").off("click"); + $("li[data-module-id]").off("mouseenter mouseleave"); + $("li[data-module-id] > span > a.canvas-center-on").off("click"); + + // Remember the draggable position + rememberDraggablePosition($("#diagram"), $("#canvas-container")); +} + +//************************************ +// FUNCTIONS +//************************************ + +/** + * Enable/disable canvas events (related to dragging, zooming, ...). + * @param activate - True to activate events; false + * to deactivate them. + */ +function toggleCanvasEvents(activate) { + var cmd = "pause"; + if (activate) { + cmd = "active"; + } + $("#diagram-container").eventPause(cmd, + "mousedown mouseup mouseout mousewheel touchstart touchend touchcancel touchmove"); + hammertime.get('pinch').set({ enable: activate }); +} + +/** + * Validate the module/module group name. + * @param The value to be validated. + * @return 0 if valid; -1 if length too small/large; -2 if + * it contains invalid characters. + */ +function validateName(val) { + var result = NAME_VALID; + if (_.isUndefined(val) || + val.length < 2 || + val.length > 50) { + result = NAME_LENGTH_ERROR; + } else if (val.indexOf(SUBMIT_FORM_NAME_SEPARATOR) != -1) { + result = NAME_INVALID_CHARACTERS_ERROR; + } else if (/^\s+$/.test(val)){ + result = NAME_WHITESPACES_ERROR; + } + return result; +} + +/** + * Gets or sets the left CSS position of the element. + * @param el - The element. + * @param newVal - The new left CSS value, if setting value. + * @return The new float value of the element's left CSS position. + */ +function elLeft(el, newVal) { + if (_.isUndefined(newVal)) { + return parseFloat($(el).css("left").replace("px", "")); + } else { + $(el).css("left", newVal + "px"); + return newVal; + } +} + +/** + * Gets or sets the top CSS position of the element. + * @param el - The element. + * @param newVal - The new top CSS value, if setting value. + * @return The new float value of the element's top CSS position. + */ +function elTop(el, newVal) { + if (_.isUndefined(newVal)) { + return parseFloat($(el).css("top").replace("px", "")); + } else { + $(el).css("top", newVal + "px"); + return newVal; + } +} + +/** + * Animate the reposition of the specified element. + * @param el - The element to be repositioned. + * @param left - The new left CSS property. + * @param top - The new top CSS property. + */ +function animateReposition(el, left, top) { + var leftMove, topMove, leftDir, topDir; + if (_.isUndefined($(el).css("left"))) { + leftMove = left; + } else { + leftMove = (-parseInt($(el).css("left").replace("px", ""), 10) + left); + } + if (_.isUndefined($(el).css("top"))) { + topMove = top; + } else { + topMove = (-parseInt($(el).css("top").replace("px", ""), 10) + top); + } + leftDir = leftMove >=0 ? "+=" : "-="; + topDir = topMove >=0 ? "+=" : "-="; + el.animate({ + left: leftDir + Math.abs(leftMove) + "px", + top: topDir + Math.abs(topMove) + "px" + }, 300); +} + +/** + * Bind the change of the canvas mode. + */ +function bindModeChange() { + var buttons = $('#diagram-buttons').find("a[type='button']"); + + buttons.on('click', function() { + var action = $(this).data("action"); + + // Ignore clicks on the currently active button + if (_.isEqual(action, currentMode)) { + return false; + } + + // Else, call destroy action function + switch (action) { + case "edit": + destroyEdit(); + break; + case "full_zoom": + destroyFullZoom(); + break; + case "medium_zoom": + destroyMediumZoom(); + break; + case "small_zoom": + destroySmallZoom(); + break; + } + }); +} + +function bindTouchDropdowns(selector) { + selector.on("touchstart", function(event) { + event.stopPropagation(); + }); +} + +function bindEditModeCloseWindow() { + var alertText = $("#update-canvas").attr("data-unsaved-work-text"); + + $("#update-canvas .cancel-edit-canvas").click(function(ev) { + ignoreUnsavedWorkAlert = true; + }); + $(window).on("beforeunload", function(ev) { + if (ignoreUnsavedWorkAlert) { + // Remove unload listeners + $(window).off("beforeunload"); + $(document).off("page:before-change"); + + ev.returnValue = undefined; + return undefined; + } else { + return alertText; + } + }); + $(document).on("page:before-change", function(ev) { + var exit; + if (ignoreUnsavedWorkAlert) { + exit = true; + } else { + exit = confirm(alertText); + } + + if (exit) { + // Remove unload listeners + $(window).off("beforeunload"); + $(document).off("page:before-change"); + } + + return exit; + }); +} + +function bindEditModeDropdownHandlers(node) { + // When "module clone/delete" dropdowns are opened, + // module needs to increase z-index in order for the dropdown + // menu to be above connections etc. + $(".dropdown", node).on("show.bs.dropdown", function(event) { + $(this).parents(".module").css("z-index", "30"); + }); + $(".dropdown", node).on("hide.bs.dropdown", function(event) { + $(this).parents(".module").css("z-index", "20"); + }); +} + +function resizeContainer() { + // Resize diagram container + var cont = $("#diagram-container"); + + if (cont.length > 0) { + cont.css( + "height", + ($(window).height() - cont.offset().top - 15) + "px" + ); + } +} + +function bindWindowResizeEvent() { + $(window).resize(function() { + resizeContainer(); + }); +} + +function bindFullZoomAjaxTabs() { + var manageUsersModal = null; + var manageUsersModalBody = null; + var editDescriptionModal = null; + var editDescriptionModalBody = null; + + // Initialize edit description modal window + function initEditDescription($el) { + $el.find(".description-link") + .on("ajax:success", function(ev, data, status) { + var descriptionLink = $(this); + var descriptionTab = descriptionLink.closest(".tab-pane"); + + // 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 + descriptionTab.find(".description-label") + .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); + }); + + // Disable canvas dragging events + toggleCanvasEvents(false); + + // Show modal + editDescriptionModal.modal("show"); + }) + .on("ajax:error", function(ev, data, status) { + // TODO + }); + } + + // Initialize users editing modal remote loading. + function initUsersEditLink($el) { + $el.find(".manage-users-link") + .on("ajax:before", function () { + var moduleId = $(this).closest(".panel-default").attr("id"); + manageUsersModal.attr("data-module-id", moduleId); + manageUsersModal.modal('show'); + }) + .on("ajax:success", function (e, data) { + $("#manage-module-users-modal-module").text(data.my_module.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("
  • " + data.html + "
  • ") + .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); + }); + } + + // Initialize remove user from my_module links. + function initRemoveUserLinks() { + manageUsersModalBody.find(".remove-user-link") + .on("ajax:success", function (e, data) { + initUsersModalBody(data); + }); + } + + // 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(); + } + + manageUsersModal = $("#manage-module-users-modal"); + manageUsersModalBody = manageUsersModal.find(".modal-body"); + editDescriptionModal = $("#manage-module-description-modal"); + editDescriptionModalBody = editDescriptionModal.find(".modal-body"); + + // Reload users tab HTML element when modal is closed + manageUsersModal.on("hide.bs.modal", function () { + var moduleEl = $("#" + $(this).attr("data-module-id")); + + // Load HTML to refresh users list + $.ajax({ + url: moduleEl.attr("data-module-users-tab-url"), + type: "GET", + dataType: "json", + success: function (data) { + moduleEl.find("#" + moduleEl.attr("id") + "_users").html(data.html); + initUsersEditLink(moduleEl); + }, + error: function (data) { + // TODO + } + }); + }); + + // Remove users modal content when modal window is closed. + manageUsersModal.on("hidden.bs.modal", function () { + manageUsersModalBody.html(""); + }); + + // When clicking on description modal "Update" button, + // submit its inner-lying form + editDescriptionModal.find("[data-action='submit']").click(function() { + editDescriptionModalBody.find("form").submit(); + }); + + // Remove description modal content when window is closed + editDescriptionModal.on("hidden.bs.modal", function() { + $(this).find("form").off("ajax:success ajax:error"); + editDescriptionModalBody.html(""); + + // Re-activate canvas dragging events + toggleCanvasEvents(true); + }); + + // initialize my_module tab remote loading + var elements = $(".module-large .buttons-container [role=tab]"); + elements.on("ajax:before", function (e) { + var $this = $(this); + var parentNode = $this.parents("li"); + var targetId = $this.attr("aria-controls"); + + if (parentNode.hasClass("active")) { + parentNode.removeClass("active"); + $("#" + targetId).removeClass("active"); + $this.parents(".module-large").addClass("expanded"); + return false; + } + }) + .on("ajax:success", function (e, data, status, xhr) { + + // Hide all potentially shown tabs + elements.parents("li").removeClass("active"); + $(".tab-content").children().removeClass("active"); + $(".module-large").removeClass("expanded"); + + var $this = $(this); + var targetId = $this.attr("aria-controls"); + var target = $("#" + targetId); + var targetContents = target.attr("data-contents"); + var parentNode = $this.parents("ul").parent(); + + target.html(data.html); + if (targetContents === "info") { + initEditDescription(parentNode); + } else if (targetContents === "users") { + initUsersEditLink(parentNode); + } else if (targetContents === "comments") { + initCommentForm(parentNode); + initCommentsLink(parentNode); + } + + $this.parents("ul").parent().find(".active").removeClass("active"); + $this.parents("li").addClass("active"); + target.addClass("active"); + $this.parents(".module-large").addClass("expanded"); + }) + .on("ajax:error", function (e, xhr, status, error) { + // TODO + }); +} + +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']"); + + $("div.module-large .panel-body .due-date-link") + .on("ajax:success", function(ev, data, status) { + var dueDateLink = $(this); + if (!dueDateLink.hasClass("due-date-refresh")) { + dueDateLink = dueDateLink.parent().next().find(".due-date-refresh"); + } + var moduleEl = dueDateLink.closest("div.module-large"); + + // 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.due_date_label); + + // Update module's classes if needed + moduleEl + .removeClass("alert-red") + .removeClass("alert-yellow"); + _.each(data2.alerts, function(alert) { + moduleEl.addClass(alert); + }); + + // Close modal + editDueDateModal.modal("hide"); + }) + .on("ajax:error", function(ev2, data2, status2) { + // Display errors if needed + $(this).render_form_errors("my_module", data.responseJSON); + }); + + // Disable canvas dragging events + toggleCanvasEvents(false); + + // 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(""); + + // Re-activate canvas dragging events + toggleCanvasEvents(true); + }); +} + +function bindEditTagsAjax(elements) { + 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
  • + 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 moduleEl = $("#" + $(this).attr("data-module-id")); + + // Load HTML + $.ajax({ + url: moduleEl.attr("data-module-tags-url"), + type: "GET", + dataType: "json", + success: function (data) { + moduleEl.find(".edit-tags-link").html(data.html_canvas); + }, + 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 + $(elements).find("a.edit-tags-link") + .on("ajax:before", function () { + var moduleId = $(this).closest(".panel-default").attr("id"); + manageTagsModal.attr("data-module-id", moduleId); + manageTagsModal.modal('show'); + }) + .on("ajax:success", function (e, data) { + $("#manage-module-tags-modal-module").text(data.my_module.name); + initTagsModalBody(data); + }); +} + +/** + * Bind change of GUI buttons to Ajax success callback. + */ +function bindAjax() { + $('#diagram-buttons .ajax').on('ajax:success', function(evt, data) { + // Set toggled button state + $("#diagram-buttons a").removeClass("active"); + $("#diagram-buttons a").removeAttr("aria-pressed"); + $("#diagram-buttons a").removeData("toggle"); + $(evt.target).addClass("active"); + $(evt.target).attr("aria-pressed", true); + $(evt.target).data("toggle", "button"); + + // Fill contents of container with AJAX content + var target = $('#canvas-container'); + $(target).html(data); + + // Re-run canvas GUI initialization code + var action = $(evt.target).data("action"); + switch (action) { + case "edit": + initializeEdit(); + break; + case "full_zoom": + initializeFullZoom(); + break; + case "medium_zoom": + initializeMediumZoom(); + break; + case "small_zoom": + initializeSmallZoom(); + break; + } + + currentMode = action; + }); + $('#diagram-buttons .ajax').on('ajax:error', function(evt, data) { + // Redirect to provided URL + var json = $.parseJSON(data.responseText); + $(location).attr('href', json.redirect_url); + }); +} + +/** + * Add a new node to the graph. + * @param moduleId - The ID of the module to add. + * @param module - The module jQuery element. + */ +function addNode(moduleId, module) { + var connsAttr = module.attr("data-module-conns"); + var conns = _.isUndefined(connsAttr) ? [] : connsAttr.split(", "); + graph.addNode( + moduleId, + { + name: module.data["module-name"], + x: module.data["module-x"], + y: module.data["module-y"], + conns: conns + } + ); +} + +/** + * Initialize the global graph variable from modules. + * @param modulesSel - The jQuery selector text of module elements. + */ +function initializeGraph(modulesSel) { + var modules = $(modulesSel); + + graph = new jsnx.DiGraph(); + + var module, moduleId; + _.each(modules, function(m) { + module = $(m); + moduleId = module.attr("id"); + if (!graph.hasNode(moduleId)) { + addNode(moduleId, module); + } + var outs = module.attr("data-module-conns").split(", "); + _.each(outs, function(targetId) { + if (targetId === "") { + return; + } + if (!graph.hasNode(targetId)) { + addNode(targetId, $(".diagram .module[id=" + targetId + "]")); + } + + graph.addEdge(module.attr("id"), targetId); + }); + }); +} + +/** + * Get the connected components of a specified graph and module. Alas, this + * function doesn't exist in jsnetworkx. + * @param graph - The graph instance. + * @param moduleId - We're only interested in the connected component in which + * the specified module is located. + * @return A list of node IDs representing a connected component. + */ +function connectedComponents(graph, moduleId) { + function getNeighbors(graph, node, visited) { + visited.push(node); + var neighbours = _.union(graph.predecessors(node), graph.successors(node)); + var unvisitedNeighbors = _.filter(neighbours, function(n) { + return !_.contains(visited, n); + }); + var result = _.flatten(_.map(unvisitedNeighbors, function(neighbour) { + nodes = getNeighbors(graph, neighbour, visited); + _.each(nodes, function(n) { + if (!_.contains(visited, n)) { + visited.push(n); + } + }); + return nodes; + })); + result.push(node); + return result; + } + + return _.uniq(getNeighbors(graph, moduleId, [])); +} + +/** + * Create a virtual new module (without links & functionality). + * @param event - The event, can be null. + */ +function createVirtualModule(event) { + // Generate new module div + var newModule = document.createElement("div"); + $(newModule) + .addClass("panel panel-default module new") + .css("z-index", "900") + .attr("data-module-name", "") + .attr("data-module-group-name","") + .attr("data-module-x", "") + .attr("data-module-y", "") + .attr("data-module-conns", "") + .appendTo(draggable); + + var panelHeading = document.createElement("div"); + $(panelHeading) + .addClass("panel-heading") + .appendTo($(newModule)); + + var panelTitle = document.createElement("div"); + $(panelTitle) + .addClass("panel-title") + .html("") + .appendTo($(panelHeading)); + + if (_.isEqual($("#update-canvas").data("can-edit-connections"), "yes")) { + var panelBody = document.createElement("div"); + $(panelBody) + .addClass("panel-body ep") + .appendTo($(newModule)); + } + + var overlayContainer = document.createElement("div"); + $(overlayContainer) + .addClass("overlay") + .appendTo($(newModule)); + + return $(newModule); +} + +/** + * Update a previously created virtual module with HTML elements. + * @param module - The jQuery module selector. + * @param id - The new module id. + * @param name - The module name. + * @param gridDistX - The grid distance in X direction. + * @param gridDistY - The grid distance in Y direction. + * @return The updated module. + */ +function updateModuleHtml(module, id, name, gridDistX, gridDistY) { + // Update some stuff inside the module + module + .attr("id", id) + .attr("data-module-id", id) + .attr("data-module-name", name) + .css("z-index", ""); + + var panelHeading = module.find(".panel-heading"); + + module.find(".panel-title").html(name); + + module.find(".ep").html($("#drag-connections-placeholder").text().trim()); + + // Add dropdown + var dropdown = document.createElement("div"); + $(dropdown) + .addClass("dropdown pull-right module-options") + .appendTo($(panelHeading)); + + var dropdownToggle = document.createElement("a"); + $(dropdownToggle) + .addClass("dropdown-toggle") + .attr("id", id + "_options") + .attr("data-toggle", "dropdown") + .attr("aria-haspopup", "true") + .attr("aria-expanded", "true") + .appendTo(dropdown); + + var toggleIcon = document.createElement("span"); + $(toggleIcon) + .addClass("glyphicon") + .addClass("glyphicon-triangle-bottom") + .attr("aria-hidden", "true") + .appendTo(dropdownToggle); + + var dropdownMenu = document.createElement("ul"); + $(dropdownMenu) + .addClass("dropdown-menu") + .addClass("no-scale") + .attr("aria-labelledby", id + "_options") + .appendTo(dropdown); + + var dropdownMenuHeader = document.createElement("li"); + $(dropdownMenuHeader) + .addClass("dropdown-header") + .html($("#dropdown-header-placeholder").text().trim()) + .appendTo(dropdownMenu); + + // Add edit links if neccesary + if (_.isEqual($("#update-canvas").data("can-edit-modules"), "yes")) { + var editModuleItem = document.createElement("li"); + $(editModuleItem).appendTo(dropdownMenu); + + var editModuleLink = document.createElement("a"); + $(editModuleLink) + .attr("href", "") + .attr("data-module-id", id) + .addClass("edit-module") + .html($("#edit-link-placeholder").text().trim()) + .appendTo(editModuleItem); + + // Add click handler for the edit module + $(editModuleLink).on("click touchstart", editModuleHandler); + } + if (_.isEqual($("#update-canvas").data("can-edit-module-groups"), "yes")) { + var editModuleGroupItem = document.createElement("li"); + $(editModuleGroupItem).appendTo(dropdownMenu); + $(editModuleGroupItem).hide(); + + var editModuleGroupLink = document.createElement("a"); + $(editModuleGroupLink) + .attr("href", "") + .attr("data-module-id", id) + .addClass("edit-module-group") + .html($("#edit-group-link-placeholder").text().trim()) + .appendTo(editModuleGroupItem); + + // Add click handler for the edit module group + $(editModuleGroupLink).on("click touchstart", editModuleGroupHandler); + } + + // Add clone links if neccesary + if (_.isEqual($("#update-canvas").data("can-clone-modules"), "yes")) { + var cloneModuleItem = document.createElement("li"); + $(cloneModuleItem).appendTo(dropdownMenu); + + var cloneModuleLink = document.createElement("a"); + $(cloneModuleLink) + .attr("href", "") + .attr("data-module-id", id) + .addClass("clone-module") + .html($("#clone-link-placeholder").text().trim()) + .appendTo(cloneModuleItem); + + // Add clone click handler for the new module + bindCloneModuleAction($(cloneModuleLink), ".diagram .module", gridDistX, gridDistY); + + var cloneModuleGroupItem = document.createElement("li"); + $(cloneModuleGroupItem).appendTo(dropdownMenu); + $(cloneModuleGroupItem).hide(); + + var cloneModuleGroupLink = document.createElement("a"); + $(cloneModuleGroupLink) + .attr("href", "") + .attr("data-module-id", id) + .addClass("clone-module-group") + .html($("#clone-group-link-placeholder").text().trim()) + .appendTo(cloneModuleGroupItem); + + // Add clone click handler for the new module + bindCloneModuleGroupAction($(cloneModuleGroupLink), ".diagram .module", gridDistX, gridDistY); + + bindEditModeDropdownHandlers(module); + } + + // Add delete links if neccesary + if (_.isEqual($("#update-canvas").data("can-delete-modules"), "yes")) { + var deleteModuleItem = document.createElement("li"); + $(deleteModuleItem).appendTo(dropdownMenu); + + var deleteModuleLink = document.createElement("a"); + $(deleteModuleLink) + .attr("href", "") + .attr("data-module-id", id) + .addClass("delete-module") + .html($("#delete-link-placeholder").text().trim()) + .appendTo(deleteModuleItem); + + // Add delete click handler for the new module + $(deleteModuleLink).on("click touchstart", deleteModuleHandler); + + var deleteModuleGroupItem = document.createElement("li"); + $(deleteModuleGroupItem).appendTo(dropdownMenu); + $(deleteModuleGroupItem).hide(); + + var deleteModuleGroupLink = document.createElement("a"); + $(deleteModuleGroupLink) + .attr("href", "") + .attr("data-module-id", id) + .addClass("delete-module-group") + .html($("#delete-group-link-placeholder").text().trim()) + .appendTo(deleteModuleGroupItem); + + // Add delete click handler for the new module + $(deleteModuleGroupLink).on("click touchstart", deleteModuleGroupHandler); + } + + // Set it up for jsPlumb, depending on permissions + if (_.isEqual($("#update-canvas").data("can-reposition-modules"), "yes")) { + addDraggablesToInstance(module, gridDistX, gridDistY); + } + if (_.isEqual($("#update-canvas").data("can-edit-connections"), "yes")) { + setElementsAsDropTargets(module); + setElementsAsDragSources(module, null, null, EDIT_CONNECTOR_STYLE_2); + } + + // Add dropdown touch support + bindTouchDropdowns($(dropdownToggle)); + + // Re-zoom dropdown menu, so the new module's no-scale dropdown gets + // rescaled + $(dropdownMenu).css("transform", "scale(" + (1.0 / instance.getZoom()) + ")"); + $(dropdownMenu).css("transform-origin", "0 0"); + + // Add IDs to the form + var formAddInput = $('#update-canvas form input#add'); + var formAddNameInput = $('#update-canvas form input#add-names'); + var inputVal = formAddInput.attr("value"); + var inputNameVal = formAddNameInput.attr("value"); + if (_.isUndefined(inputVal) || inputVal === "") { + formAddInput.attr("value", id); + formAddNameInput.attr("value", name); + } else { + formAddInput.attr("value", inputVal + "," + id); + formAddNameInput.attr("value", inputNameVal + SUBMIT_FORM_NAME_SEPARATOR + name); + } + + return module; +} + +/** + * Bind the new module button action. + * @param gridDistX - The canvas grid distance in X direction. + * @param gridDistY - The canvas grid distance in Y direction. + */ +function bindNewModuleAction(gridDistX, gridDistY) { + function handleDragStart(event, ui) { + collided = false; + } + + function handleDrag(event, ui) { + // Custom grid implementation is needed + // (so the new module snaps on the same grid offset + // as the other modules) + var l = ui.position.left; + var t = ui.position.top; + var gdx = gridDistX; + var gdy = gridDistY; + var z = instance.getZoom(); + + ui.position.left = Math.floor(l / (gdx * z)) * gdx; + ui.position.top = Math.floor(t / (gdy * z)) * gdy; + + // Check if collision occured + var modules = $(".module"); + var module; + for (var i = 0; i < modules.length; i++) { + module = $(modules[i]); + if (module.hasClass("new")) { + continue; + } + + if (_.isEqual(ui.helper.position(), module.position())) { + // Collision! + collided = true; + break; + } else { + collided = false; + } + } + + if (collided) { + ui.helper.addClass("collided"); + } else { + ui.helper.removeClass("collided"); + } + } + + function handleDragStop(event, ui) { + if (!collided) { + // Disable scroll on canvas temporarily, as it can be + // dragged from modal area + toggleCanvasEvents(false); + + // Copy the ui.helper, since it's gonna vanish soon! + var clone = ui.helper.clone(true, true); + clone.appendTo(draggable); + clone.css("z-index", "20"); + + // Open modal window + $("#modal-new-module").modal({ + "backdrop": "static" + }); + } + + collided = false; + } + + function handleNewNameConfirm() { + var input = $("#new-module-name-input"); + var error = false; + var message; + + // Validate module name + var res = validateName(input.val()); + if (res === NAME_LENGTH_ERROR) { + error = true; + message = modal.find(".module-name-length-error").html(); + } else if (res === NAME_INVALID_CHARACTERS_ERROR) { + error = true; + message = modal.find(".module-name-invalid-error").html(); + } else if (res === NAME_WHITESPACES_ERROR) { + error = true; + message = modal.find(".module-name-whitespaces-error").html(); + } + + if (error) { + // Style the form so it displays error + input.parent().addClass("has-error"); + input.parent().find("span.help-block").remove(); + var errorSpan = document.createElement("span"); + $(errorSpan) + .addClass("help-block") + .html(message) + .appendTo(input.parent()); + + return false; + } else { + // Set the "clicked" property to true + modal.data("submit", "true"); + return true; + } + } + + var newModuleBtn = $("#canvas-new-module"); + var modal = $("#modal-new-module"); + + newModuleBtn.draggable({ + cursor: "move", + helper: createVirtualModule, + start: handleDragStart, + drag: handleDrag, + stop: handleDragStop + }); + + // Prevent "new module" button from submitting form + newModuleBtn.click(function(event) { + event.preventDefault(); + event.stopPropagation(); + return false; + }); + + // Bind the confirm button on modal + modal.find("button[data-action='confirm']").on("click", function(event) { + if (!handleNewNameConfirm()) { + // Prevent modal from closing if errorous form + event.preventDefault(); + event.stopPropagation(); + return false; + } + }); + + // Also, bind on modal window open & close + modal.on("show.bs.modal", function(event) { + // Clear input + $(this).removeData("submit"); + $(this).find("#new-module-name-input").val(""); + + // Remove potential error classes from form + $(this).find("#new-module-name-input").parent().removeClass("has-error"); + $(this).find("span.help-block").remove(); + + // Bind onto input keypress (to prevent form from being submitted) + $(this).find("#new-module-name-input").keydown(function(ev) { + if (ev.keyCode == 13) { + if (handleNewNameConfirm()) { + // Close modal + modal.modal("hide"); + } + + // In any case, prevent form submission + ev.preventDefault(); + ev.stopPropagation(); + return false; + } + }); + }); + + modal.on("shown.bs.modal", function(event) { + // Focus the text element + $(this).find("#new-module-name-input").focus(); + }); + + modal.on("hide.bs.modal", function (event) { + var newModule = $(".module.new"); + + $(this).find("#new-module-name-input").off("keydown"); + + if (_.isEqual($(event.target).data("submit"), "true")) { + // If modal was successfully submitted, generate the module + var id = "n" + newModuleIndex++; + graph.addNode(id); + var name = $(this).find("#new-module-name-input").val(); + updateModuleHtml(newModule, id, name, gridDistX, gridDistY); + newModule.removeClass("new"); + } else { + // Else, remove the element + newModule.remove(); + } + + // In any case, enable scrolling on edit screen again + toggleCanvasEvents(true); + }); +} + +function initEditModules() { + function handleRenameConfirm(modal) { + var input = modal.find("#edit-module-name-input"); + + var moduleId = modal.attr("data-module-id"); + var moduleEl = $("#" + moduleId); + + var error = false; + var message; + var newName; + + // Validate module name + newName = input.val(); + var res = validateName(newName); + if (res === NAME_LENGTH_ERROR) { + error = true; + message = modal.find(".module-name-length-error").html(); + } else if (res === NAME_INVALID_CHARACTERS_ERROR) { + error = true; + message = modal.find(".module-name-invalid-error").html(); + } else if (res === NAME_WHITESPACES_ERROR) { + error = true; + message = modal.find(".module-name-whitespaces-error").html(); + } + + if (error) { + // Style the form so it displays error + input.parent().addClass("has-error"); + input.parent().find("span.help-block").remove(); + var errorSpan = document.createElement("span"); + $(errorSpan) + .addClass("help-block") + .html(message) + .appendTo(input.parent()); + } else { + // Update the module's name in GUI + moduleEl.attr("data-module-name", newName); + moduleEl.find(".panel-heading .panel-title").html(newName); + + // Add this information to form + var formAddInput = $('#update-canvas form input#add'); + var formAddNameInput = $('#update-canvas form input#add-names'); + var formRenameInput = $("#update-canvas form input#rename"); + var addedIds = formAddInput.attr("value").split(","); + var existingIndex = _.indexOf(addedIds, moduleEl.attr("id")); + if (existingIndex === -1) { + // Actually rename an existing module + var renameVal = JSON.parse(formRenameInput.attr("value")); + renameVal[moduleEl.attr("id")] = newName; + formRenameInput.attr("value", JSON.stringify(renameVal)); + } else { + // Just rename the add-name entry + var addedNames = formAddNameInput.attr("value").split(SUBMIT_FORM_NAME_SEPARATOR); + addedNames[existingIndex] = newName; + formAddNameInput.attr("value", addedNames.join(SUBMIT_FORM_NAME_SEPARATOR)); + } + + // Hide modal + modal.modal("hide"); + } + } + + $("#modal-edit-module") + .on("show.bs.modal", function (event) { + var modal = $(this); + var moduleId = modal.attr("data-module-id"); + var moduleEl = $("#" + moduleId); + var input = modal.find("#edit-module-name-input"); + + // Set the input to the current module's name + input.attr("value", moduleEl.attr("data-module-name")); + input.val(moduleEl.attr("data-module-name")); + + // Bind on enter button + input.keydown(function(ev) { + if (ev.keyCode == 13) { + // "Submit" modal + handleRenameConfirm(modal); + + // In any case, prevent form submission + ev.preventDefault(); + ev.stopPropagation(); + return false; + } + }); + }) + .on("shown.bs.modal", function(event) { + // Focus the text element + $(this).find("#edit-module-name-input").focus(); + }) + .on("hide.bs.modal", function (event) { + // Remove potential error classes + $(this).find("#edit-module-name-input").parent().removeClass("has-error"); + $(this).find("span.help-block").remove(); + + $(this).find("#edit-module-name-input").off("keydown"); + + // When hiding modal, re-enable events + toggleCanvasEvents(true); + }); + + // Bind the confirm button on modal + $("#modal-edit-module").find("button[data-action='confirm']").on("click", function(event) { + var modal = $(this).closest(".modal"); + handleRenameConfirm(modal); + }); +} + +/** + * Handler when editing a specific module. + */ +editModuleHandler = function(ev) { + var modal = $("#modal-edit-module"); + var moduleEl = $(this).closest(".module"); + + // Set modal's module id + modal.attr("data-module-id", moduleEl.attr("id")); + + // Disable dragging & zooming events on canvas temporarily + toggleCanvasEvents(false); + + // Show modal + modal.modal("show"); + + ev.preventDefault(); + ev.stopPropagation(); + return false; +}; + +/** + * Initialize editing of module groups. + */ +function initEditModuleGroups() { + function handleRenameConfirm(modal) { + var input = modal.find("#edit-module-group-name-input"); + + var moduleId = modal.attr("data-module-id"); + var moduleEl = $("#" + moduleId); + + var error = false; + var message; + var newModuleGroupName; + + // Validate module name + newModuleGroupName = input.val(); + var res = validateName(newModuleGroupName); + if (res === NAME_LENGTH_ERROR) { + error = true; + message = modal.find(".module-name-length-error").html(); + } else if (res === NAME_INVALID_CHARACTERS_ERROR) { + error = true; + message = modal.find(".module-name-invalid-error").html(); + } else if (res === NAME_WHITESPACES_ERROR) { + error = true; + message = modal.find(".module-name-whitespaces-error").html(); + } + + if (error) { + // Style the form so it displays error + input.parent().addClass("has-error"); + input.parent().find("span.help-block").remove(); + var errorSpan = document.createElement("span"); + $(errorSpan) + .addClass("help-block") + .html(message) + .appendTo(input.parent()); + } else { + // Update the module group name for all modules + // currently in the module group + var ids = connectedComponents(graph, moduleEl.attr("id")); + _.each(ids, function(id) { + $("#" + id).attr("data-module-group-name", newModuleGroupName); + }); + + // Hide modal + modal.modal("hide"); + } + } + + $("#modal-edit-module-group") + .on("show.bs.modal", function (event) { + var modal = $(this); + var moduleId = modal.attr("data-module-id"); + var moduleEl = $("#" + moduleId); + var input = modal.find("#edit-module-group-name-input"); + + // Set the input to the current module's name + input + .attr("value", moduleEl.attr("data-module-group-name")); + input.val(moduleEl.attr("data-module-group-name")); + + // Bind on enter button + input.keydown(function(ev) { + if (ev.keyCode == 13) { + // "Submit" modal + handleRenameConfirm(modal); + + // In any case, prevent form submission + ev.preventDefault(); + ev.stopPropagation(); + return false; + } + }); + }) + .on("shown.bs.modal", function (event) { + $(this).find("#edit-module-group-name-input").focus(); + }) + .on("hide.bs.modal", function (event) { + // Remove potential error classes + $(this).find("#edit-module-group-name-input").parent().removeClass("has-error"); + $(this).find("span.help-block").remove(); + + $(this).find("#edit-module-group-name-input").off("keydown"); + + // When hiding modal, re-enable events + toggleCanvasEvents(true); + }); + + // Bind the confirm button on modal + $("#modal-edit-module-group").find("button[data-action='confirm']").on("click", function(event) { + var modal = $(this).closest(".modal"); + handleRenameConfirm(modal); + }); +} + +/** + * Handler when editing a module group. + */ +editModuleGroupHandler = function(ev) { + var modal = $("#modal-edit-module-group"); + var moduleEl = $(this).closest(".module"); + + // Set modal's module id + modal.attr("data-module-id", moduleEl.attr("id")); + + // Disable dragging & zooming events on canvas temporarily + toggleCanvasEvents(false); + + // Show modal + modal.modal("show"); + + ev.preventDefault(); + ev.stopPropagation(); + return false; +}; + +/** + * Bind the delete module buttons actions. + */ +function bindDeleteModuleAction() { + // First, bind the delete module handler onto all "delete module" links + $(".module-options a.delete-module").on("click touchstart", deleteModuleHandler); + + // Then, bind on modal events + var modal = $("#modal-delete-module"); + + // Bind the confirm button on modal + modal.find("button[data-action='confirm']").on("click", function(event) { + // Set the "clicked" property to true + modal.data("submit", "true"); + }); + + // Also, bind on modal window open & close + modal.on("show.bs.modal", function(event) { + // Remove submit flag + $(this).removeData("submit"); + + // Disable dragging & zooming events on canvas temporarily + toggleCanvasEvents(false); + }); + + modal.on("hide.bs.modal", function (event) { + if (_.isEqual($(event.target).data("submit"), "true")) { + // If modal was successfully submitted, delete the module + var id = $(event.target).data("module-id"); + + deleteModule(id.toString(), true); + } + + // In any case, re-enable events on canvas + toggleCanvasEvents(true); + }); +} + +function deleteModule(id, linkConnections) { + var ins = graph.inEdges(id); + var outs = graph.outEdges(id); + var tempModuleEl; + + // Remove id from the graph structure, along with all connections + if (graph.hasNode(id)) { + graph.removeNode(id); + } + + // Remove the module
    , along with all connections + instance.remove($("#" + id)); + + // Connect the sources to destinations + if (linkConnections) { + _.each(ins, function(inEdge) { + _.each(outs, function(outEdge) { + // Only connect 2 nodes if + // such a connection doesn't exist already + if (!graph.hasEdge(inEdge[0], outEdge[1])) { + graph.addEdge(inEdge[0], outEdge[1]); + instance.connect({ + source: $("#" + inEdge[0]), + target: $("#" + outEdge[1]) + }); + } + }); + }); + + //Hide module group options for unconnected modules + if (outs.length === 0) { // If node is sink + _.each (ins, function(inEdge) { + if (graph.degree(inEdge[0]) === 0) { + tempModuleEl = $("#" + inEdge[0]); + tempModuleEl.find(".edit-module-group").parents("li").hide(); + tempModuleEl.find(".clone-module-group").parents("li").hide(); + tempModuleEl.find(".delete-module-group").parents("li").hide(); + } + }); + } + if (ins.length === 0) { // If node is source + _.each (outs, function(outEdge) { + if (graph.degree(outEdge[1]) === 0) { + tempModuleEl = $("#" + outEdge[1]); + tempModuleEl.find(".edit-module-group").parents("li").hide(); + tempModuleEl.find(".clone-module-group").parents("li").hide(); + tempModuleEl.find(".delete-module-group").parents("li").hide(); + } + }); + } + } + + // Add ID to the form + var formAddInput = $('#update-canvas form input#add'); + var formAddNamesInput = $('#update-canvas form input#add-names'); + var formClonedInput = $('#update-canvas form input#cloned'); + var formRemoveInput = $('#update-canvas form input#remove'); + var inputVal, newVal; + var vals, idx; + var addToRemoveList = true; + + // If the module we are deleting was added via JS + // (and hasn't been saved yet), we don't need to "add" it + // neither "remove" it, it simply ceases to exist + inputVal = formAddInput.attr("value"); + if (!_.isUndefined(inputVal) && inputVal !== "") { + vals = inputVal.split(","); + if (_.contains(vals, id)) { + addToRemoveList = false; + idx = vals.indexOf(id); + vals.splice(idx, 1); + formAddInput.attr("value", vals.join()); + vals = formAddNamesInput.attr("value").split(SUBMIT_FORM_NAME_SEPARATOR); + vals.splice(idx, 1); + formAddNamesInput.attr("value", vals.join(SUBMIT_FORM_NAME_SEPARATOR)); + } + } + + // Okay, the module was not created, but it might be cloned, + // so we need to check that as well + if (!addToRemoveList) { + inputVal = formClonedInput.attr("value"); + if (!_.isUndefined(inputVal) && inputVal !== "") { + vals = _.map(inputVal.split(";"), function(val) { + return val.split(",")[1]; + }); + if (_.contains(vals, id)) { + addToRemoveList = false; + + // Remove the cloned module from the cloned list + newVal = ""; + _.each(inputVal.split(";"), function(val) { + if (!_.isEqual(val.split(",")[1], id)) { + newVal = (newVal === "" ? "" : (newVal + ";")) + val; + } + }); + formClonedInput.attr("value", newVal); + } + } + } + + if (addToRemoveList) { + inputVal = formRemoveInput.attr("value"); + if (_.isUndefined(inputVal) || inputVal === "") { + formRemoveInput.attr("value", id); + } else { + formRemoveInput.attr("value", inputVal + "," + id); + } + } +} + +/** + * Handler function when deleting a single module. + */ +deleteModuleHandler =function() { + var id = $(this).data("module-id"); + var modal = $("#modal-delete-module"); + + var name = $(".module#" + id).data("module-name"); + var template = modal.find("#message-template").text().trim(); + + // Set the modal message + modal.find("#delete-message").text(template.replace("%{module}", name)); + + // Send module id to modal + modal.data("module-id", id); + + // Display delete modal + modal.modal({ + "backdrop": "static" + }); + + return false; +}; + +/** + * Bind the delete module group buttons actions. + */ +function bindDeleteModuleGroupAction() { + // First, bind the delete module group handler onto all + // "delete module group" links + $(".module-options a.delete-module-group").on("click touchstart", deleteModuleGroupHandler); + + // Then, bind on modal events + var modal = $("#modal-delete-module-group"); + + // Bind the confirm button on modal + modal.find("button[data-action='confirm']").on("click", function(event) { + // Set the "clicked" property to true + modal.data("submit", "true"); + }); + + // Also, bind on modal window open & close + modal.on("show.bs.modal", function(event) { + // Remove submit flag + $(this).removeData("submit"); + + // Disable dragging & zooming events on canvas temporarily + toggleCanvasEvents(false); + }); + + modal.on("hide.bs.modal", function (event) { + if (_.isEqual($(event.target).data("submit"), "true")) { + // If modal was successfully submitted, delete the module + var id = $(event.target).data("module-id"); + + // Find all modules in the connected component + var modules = connectedComponents(graph, id.toString()); + + // Delete all modules of the module group + _.each(modules, function(moduleId) { + deleteModule(moduleId, false); + }); + } + + // In any case, re-enable events on canvas + toggleCanvasEvents(true); + }); +} + +/** + * Handler function when deleting module group. + */ +deleteModuleGroupHandler = function() { + var id = $(this).data("module-id"); + var modal = $("#modal-delete-module-group"); + + var name = $(".module#" + id).data("module-name"); + var template = modal.find("#message-template").text().trim(); + + // Set the modal message + modal.find("#delete-message").text(template.replace("%{module}", name)); + + // Send module id to modal + modal.data("module-id", id); + + // Display delete modal + modal.modal({ + "backdrop": "static" + }); + + return false; +}; + +/** + * Bind the clone module action. + * @param element - jQUery selector for the element on which the click action will run. + * @param modulesSel - The selector string for all modules. + * @param gridDistX - The grid distance in X direction. + * @param gridDistY - The grid distance in Y direction. + */ +function bindCloneModuleAction(element, modulesSel, gridDistX, gridDistY) { + element.on("click touchstart", function(event) { + cloneModuleHandler($(this).data("module-id"), modulesSel, gridDistX, gridDistY); + event.preventDefault(); + event.stopPropagation(); + return false; + }); +} + +/** + * Handler function when cloning a single module. + * @param moduleId - The ID of the original module. + * @param modulesSel - The selector string for all modules. + * @param gridDistX - The grid distance in X direction. + * @param gridDistY - The grid distance in Y direction. + */ +cloneModuleHandler = function(moduleId, modulesSel, gridDistX, gridDistY) { + var modules = $(modulesSel); + var module = modules.filter("#" + moduleId); + + // Figure out the free position for the cloned module + var top = elTop(module); + var left = elLeft(module); + var modulesInRow = []; + _.each(modules, function(m) { + if (elTop(m) === top) { + modulesInRow.push(m); + } + }); + var i, free; + while (true) { + left += gridDistX; + free = true; + for (i = 0; i < modulesInRow.length; i++) { + if (elLeft(modulesInRow[i]) === left) { + free = false; + break; + } + } + + if (free) { + break; + } + } + + cloneModule(module, gridDistX, gridDistY, left, top); + + // Hide all open dropdowns + $(".module-options").removeClass("open"); +}; + +/** + * Clone the original module. + * @param originalModule - The jQuery original module selector. + * @param gridDistX - The grid distance in X direction. + * @param gridDistY - The grid distance in Y direction. + * @param left - The left position of the new module. + * @param top - The top position of the new module. + * @return The new module. + */ +function cloneModule(originalModule, gridDistX, gridDistY, left, top) { + var moduleId = originalModule.data("module-id"); + + // Create new module element + var id = "n" + newModuleIndex++; + graph.addNode(id); + var newModule = createVirtualModule(); + elLeft(newModule, left); + elTop(newModule, top); + updateModuleHtml(newModule, id, originalModule.data("module-name"), gridDistX, gridDistY); + newModule.removeClass("new"); + + // Add the cloned module id into the hidden input field + var formAddInput = $('#update-canvas form input#add'); + var formAddNamesInput = $('#update-canvas form input#add-names'); + var formClonedInput = $('#update-canvas form input#cloned'); + var inputVal, inputNameVal; + + // If we cloned a module with virtual id, there are 2 possibilities: + // 1. Original module is newly created, which means that our cloned + // module can also simply be treated as a new module; + // 2. Original module is a cloned module, which means we want to extend + // original module's original module into this new module + var originalId = moduleId; + var originalWasCloned = false; + var fillClonedInput = true; + + if (_.isEqual(moduleId.toString().charAt(0), "n")) { + // Find the original module's "original module", and retrieve its id + // If such ID cannot be found, original module was not cloned + fillClonedInput = false; + inputVal = formClonedInput.attr("value"); + _.each(inputVal.split(";"), function(val) { + var val2 = val.split(","); + if (_.isEqual(val2[1], moduleId)) { + originalId = val2[0]; + fillClonedInput = true; + } + }); + } + + if (fillClonedInput) { + inputVal = formClonedInput.attr("value"); + if (_.isUndefined(inputVal) || inputVal === "") { + formClonedInput.attr("value", originalId + "," + id); + } else { + formClonedInput.attr("value", inputVal + ";" + originalId + "," + id); + } + } + + instance.repaintEverything(); + + return newModule; +} + +/** + * Bind the clone module group action. + * @param element - jQUery selector for the element on which the click action will run. + * @param modulesSel - The selector string for all modules. + * @param gridDistX - The grid distance in X direction. + * @param gridDistY - The grid distance in Y direction. + */ +function bindCloneModuleGroupAction(element, modulesSel, gridDistX, gridDistY) { + element.on("click touchstart", function(event) { + cloneModuleGroupHandler($(this).data("module-id"), modulesSel, gridDistX, gridDistY); + event.preventDefault(); + event.stopPropagation(); + return false; + }); +} + +/** + * Handler function when cloning a module group. + * @param moduleId - The ID of the original module. + * @param modulesSel - The selector string for all modules. + * @param gridDistX - The grid distance in X direction. + * @param gridDistY - The grid distance in Y direction. + */ +cloneModuleGroupHandler = function(moduleId, modulesSel, gridDistX, gridDistY) { + var modules = $(modulesSel); + + // Retrieve all modules in this module group + var components = connectedComponents(graph, moduleId.toString()); + var group = _.map(components, function(id) { return $("#" + id); }); + + // Calculate the size of the rectangle containing the whole workflow + var width, height; + var minX = Number.MAX_VALUE, maxX = -Number.MAX_VALUE; + var minY = Number.MAX_VALUE, maxY = -Number.MAX_VALUE; + + _.each(group, function(m) { + var l = elLeft(m); + var t = elTop(m); + if (l < minX) { minX = l; } + if (l > maxX) { maxX = l; } + if (t < minY) { minY = t; } + if (t > maxY) { maxY = t; } + }); + width = maxX - minX + gridDistX; + height = maxY - minY + gridDistY; + + // Find the appropriate "free space" + var left = minX != Number.MAX_VALUE ? minX : 0; + var top = maxY != -Number.MAX_VALUE ? maxY : 0; + var offset = height; + var moduleContained; + + while (true) { + moduleContained = false; + for (var i = 0; i < modules.length; i++) { + var module = $(modules[i]); + + // Skip modules from the module group + if (_.contains(components, module.data("module-id").toString())) { + continue; + } + + var ml = elLeft(module); + var mt = elTop(module); + + if (ml >= left && + ml <= left + width - gridDistX && + mt >= top + offset && + mt <= top + offset + height - gridDistY) { + moduleContained = true; + break; + } + } + + // If no module contained, exit + if (!moduleContained) { + break; + } + + offset += gridDistY; + } + + // Alright, clone all modules from the group and + // move them by the vertical offset + clones = {}; + _.each(group, function(m) { + var nm = cloneModule(m, gridDistX, gridDistY, elLeft(m), elTop(m) + height + offset - gridDistY); + + //Show module group options + nm.find(".edit-module-group").parents("li").show(); + nm.find(".clone-module-group").parents("li").show(); + nm.find(".delete-module-group").parents("li").show(); + + clones[m.attr("id")] = nm.attr("id"); + }); + + // Also, copy the outbound connections + _.each(_.keys(clones), function(originalId) { + var clonedId = clones[originalId]; + + _.each(graph.successors(originalId), function(outNode) { + graph.addEdge(clonedId, clones[outNode]); + instance.connect({ + source: $("#" + clonedId), + target: $("#" + clones[outNode]) + }); + }); + }); + + // Hide all open dropdowns + $(".module-options").removeClass("open"); + + // Repainting is needed twice (weird, huh) + instance.repaintEverything(); + instance.repaintEverything(); +}; + +/** + * Before submission, graph & module group info needs to be + * copied into hidden input fields via form + * submission callback. + * @param gridDistX - The canvas grid distance in X direction. + * @param gridDistY - The canvas grid distance in Y direction. + */ +function bindEditFormSubmission(gridDistX, gridDistY) { + $('#update-canvas form').submit(function(){ + var modules = $(".diagram .module"); + var connectionsDiv = $('#update-canvas form input#connections'); + var positionsDiv = $('#update-canvas form input#positions'); + var moduleNamesDiv = $('#update-canvas form input#module-groups'); + + // Connections are easy, just copy graph data + connectionsDiv.attr("value", graph.edges().toString()); + + // Positions are a bit more tricky, but still pretty straightforward + var moduleGroupNames = {}; + var positionsVal = ""; + var module, id, x, y; + _.each(modules, function(m) { + module = $(m); + id = module.attr("id"); + x = elLeft(module) / gridDistX; + y = elTop(module) / gridDistY; + positionsVal += id + "," + x + "," + y + ";"; + moduleGroupNames[id] = module.attr("data-module-group-name"); + }); + positionsDiv.attr("value", positionsVal); + moduleNamesDiv.attr("value", JSON.stringify(moduleGroupNames)); + + ignoreUnsavedWorkAlert = true; + return true; + }); +} + +/** + * Position the modules onto the canvas. + * @param modulesSel - The jQuery selector text of module elements. + * @param gridDistX - The X canvas grid distance. + * @param gridDistY - The Y canvas grid distance. + */ +function positionModules(modulesSel, gridDistX, gridDistY) { + var modules = $(modulesSel); + + var module, x, y; + _.each(modules, function(m) { + module = $(m); + x = module.data("module-x"); + y = module.data("module-y"); + elLeft(module, x * gridDistX); + elTop(module, y * gridDistY); + }); +} + +/** + * Add draggable element/s to the jsPlumb instance. + * @param elements - The elements selector. + * @param gridDistX - The grid distance in X direction. + * @param gridDistY - The grid distance in Y direction. + */ +function addDraggablesToInstance(elements, gridDistX, gridDistY) { + function handleDragStart(event, ui) { + var draggedModule = $(event.el); + + leftInitial = elLeft(draggedModule); + topInitial = elTop(draggedModule); + collided = false; + + draggedModule + .css("z-index", "25") + .addClass("dragged"); + } + + function handleDrag(event, ui) { + var draggedModule = $(event.el); + var modules = $(".module"); + + // Check if collision occured + var module; + for (var i = 0; i < modules.length; i++) { + module = $(modules[i]); + if (_.isEqual(module, draggedModule)) { + continue; + } + + if (_.isEqual(draggedModule.position(), module.position())) { + // Collision! + collided = true; + break; + } else { + collided = false; + } + } + + if (collided) { + draggedModule.addClass("collided"); + } else { + draggedModule.removeClass("collided"); + } + } + + function handleDragStop(event, ui) { + var draggedModule = $(event.el); + + draggedModule + .css("z-index", "20") + .removeClass("dragged"); + + // Reposition element to back where it was + if (collided) { + draggedModule.removeClass("collided"); + elLeft(draggedModule, leftInitial); + elTop(draggedModule, topInitial); + instance.repaintEverything(); + } + + collided = false; + } + + instance.draggable(elements, { + snapThreshold: Math.max(gridDistX, gridDistY), + grid: [gridDistX, gridDistY], + start: handleDragStart, + drag: handleDrag, + stop: handleDragStop + }); +} + +/** + * Set the specified elements as drop targets in jsPlumb instance. + * @param elements - The elements to be used as drop targets. + */ +function setElementsAsDropTargets(elements) { + instance.makeTarget(elements, { + dropOptions: { hoverClass: "dragHover" }, + anchor: "AutoDefault", + allowLoopback: false + }); +} + +/** + * Set the specified elements as drag sources in jsPlumb instance. + * @param elements - The elements to be used as drag sources. + * @param anchorStyle - Anchor style. + * @param connectorStyle - Connector style. + * @param connectorStyle2 - Connector style 2. + */ +function setElementsAsDragSources(elements, anchorStyle, connectorStyle, connectorStyle2) { + // CSS_STYLE + var newAnchorStyle = anchorStyle || DEFAULT_ANCHOR_STYLE; + var newConnectorStyle = connectorStyle || DEFAULT_CONNECTOR_STYLE; + var newConnectorStyle2 = connectorStyle2 || DEFAULT_CONNECTOR_STYLE_2; + + instance.makeSource(elements, { + filter: ".ep", + anchor: newAnchorStyle, + connector: newConnectorStyle, + allowLoopback: false, + connectorStyle: newConnectorStyle2, + }); +} + +/** + * Zooms the specified instance element, while retaining + * the non-zoomable children. + * @param zoomableElement - The element to be zoomed. + * @param zoom - The zoom level (value between 0 and 1). + * @param origin - The zoom origin, can be null. + */ +function zoomInstance(zoomableElement, zoom, origin) { + zoomableElement.css("transform", "scale(" + zoom + ")"); + + if (!_.isUndefined(origin)) { + zoomableElement.css("transform-origin", origin); + } + + instance.setZoom(zoom); + + // Make sure the no-scale elements are kept on original scale: + var noscale = zoomableElement.find(".no-scale"); + noscale.css("transform", "scale(" + (1.0 / zoom) + ")"); + noscale.css("transform-origin", "0 0"); +} + +/** + * Calculates the draggable element's size. + * @param draggable - The draggable element. + * @return - An object {width: , height: }. + */ +function calculateDraggableSize(draggable) { + // Since draggable's width & height is usually 0, + // we need to calculate its actual width & height + // by extracting positions of its children (e.g. modules) + var minX = Number.MAX_VALUE, maxX = -Number.MAX_VALUE; + var minY = Number.MAX_VALUE, maxY = -Number.MAX_VALUE; + var child, left, top, right, bottom; + _.each(draggable.children(), function(c) { + child = $(c); + left = elLeft(child); + top = elTop(child); + right = left + child.width(); + bottom = top + child.height(); + if (left < minX) { minX = left; } + if (right > maxX) { maxX = right; } + if (top < minY) { minY = top; } + if (bottom > maxY) { maxY = bottom; } + }); + if (minX === Number.MAX_VALUE) { minX = -1; } + if (maxX === -Number.MAX_VALUE) { maxX = 1; } + if (minY === Number.MAX_VALUE) { minY = -1; } + if (maxY === -Number.MAX_VALUE) { maxY = 1; } + + return { width: maxX - minX, height: maxY - minY }; +} + +/** + * Remembers the draggable element's position. + * @param draggable - The draggable element. + * @param parent - The parent element to which the draggable element + * is relative to. + */ +function rememberDraggablePosition(draggable, parent) { + // Calculate the center of the draggable's parent X & Y + var centerX = parent.width() / 2 - elLeft(draggable); + var centerY = parent.height() / 2 - elTop(draggable); + + var draggableSize = calculateDraggableSize(draggable); + + draggableLeft = centerX / draggableSize.width; + draggableTop = centerY / draggableSize.height; +} + +/** + * Restores the draggable element's position. + * @param draggable - The draggable element. + */ +function restoreDraggablePosition(draggable, parent) { + // Calculate the center of the draggable's parent X & Y + var centerX = parent.width() / 2; + var centerY = parent.height() / 2; + + var draggableSize = calculateDraggableSize(draggable); + + var left = draggableLeft * draggableSize.width; + var top = draggableTop * draggableSize.height; + + elLeft(draggable, centerX - left); + elTop(draggable, centerY - top); +} + +/** + * Initialize the modules group hover animation. + * @param modules - The modules jQuery selector. + * @param sidebar - The sidebar jQuery selector. + */ +function initModulesHover(modules, sidebar) { + function handlerIn() { + var groupId = $(this).data("module-group"); + var groupModules = modules.filter("[data-module-group='" + groupId + "']"); + var groupMenu = sidebar.find("li[data-module-group='" + groupId + "']"); + groupModules.addClass("group-hover"); + groupMenu.addClass("group-hover"); + + var moduleId = $(this).data("module-id"); + if (!_.isUndefined(moduleId)) { + var currentModule = modules.filter("[data-module-id='" + moduleId + "']"); + var currentMenu = sidebar.find("li[data-module-id='" + moduleId + "']"); + currentModule.addClass("module-hover"); + currentMenu.addClass("module-hover"); + } + } + function handlerOut() { + var groupId = $(this).data("module-group"); + var groupModules = modules.filter("[data-module-group='" + groupId + "']"); + var groupMenu = $("li[data-module-group='" + groupId + "']"); + groupModules.removeClass("group-hover"); + groupMenu.removeClass("group-hover"); + + var moduleId = $(this).data("module-id"); + if (!_.isUndefined(moduleId)) { + var currentModule = modules.filter("[data-module-id='" + moduleId + "']"); + var currentMenu = sidebar.find("li[data-module-id='" + moduleId + "']"); + currentModule.removeClass("module-hover"); + currentMenu.removeClass("module-hover"); + } + } + modules.hover(handlerIn, handlerOut); + sidebar.find("li[data-module-id]").hover(handlerIn, handlerOut); + sidebar.find("li[data-module-group]").hover(handlerIn, handlerOut); +} + +/** + * Calculates the specified module group size. + * @param modules - The modules belonging to this module group. + * @return - An object {width: , height: }. + */ +function calculateModuleGroupSize(modules) { + var minX = Number.MAX_VALUE, maxX = -Number.MAX_VALUE; + var minY = Number.MAX_VALUE, maxY = -Number.MAX_VALUE; + var module, left, top, right, bottom; + _.each(modules, function(m) { + module = $(m); + left = elLeft(module); + top = elTop(module); + right = left + module.width(); + bottom = top + module.height(); + if (left < minX) { minX = left; } + if (right > maxX) { maxX = right; } + if (top < minY) { minY = top; } + if (bottom > maxY) { maxY = bottom; } + }); + if (minX === Number.MAX_VALUE) { minX = -1; } + if (maxX === -Number.MAX_VALUE) { maxX = 1; } + if (minY === Number.MAX_VALUE) { minY = -1; } + if (maxY === -Number.MAX_VALUE) { maxY = 1; } + + return { + left: minX, + top: minY, + width: maxX - minX, + height: maxY - minY + }; +} + +/** + * Initialize the modules & groups click action on sidebar + * so the modules/groups are then centered in canvas. + * @param modules - The modules jQuery selector. + * @param sidebar - The sidebar jQuery selector. + * @param draggable - The canvas draggable jQuery selector. + * @param parent - The parent of the draggable element. + * @param modulePadding - The top-left padding form module display. + */ +function initSidebarClicks(modules, sidebar, draggable, parent, modulePadding) { + function moduleHandler(event) { + var moduleId = $(this).closest("li").data("module-id"); + var module = modules.filter("[data-module-id='" + moduleId + "']"); + var centerX = parent.width() / 2; + var centerY = parent.height() / 2; + var left = centerX - elLeft(module) - (module.width() / 2); + var top = centerY - elTop(module) - (module.height() / 2); + + event.preventDefault(); + event.stopPropagation(); + animateReposition(draggable, left, top); + return false; + } + function moduleGroupHandler(event) { + var groupId = $(this).closest("li").data("module-group"); + var groupModules = modules.filter("[data-module-group='" + groupId + "']"); + var groupSize = calculateModuleGroupSize(groupModules); + var centerX = parent.width() / 2; + var centerY = parent.height() / 2; + var left, top; + if (groupSize.width > parent.width() || groupSize.height > parent.height()) { + left = -groupSize.left + modulePadding; + top = -groupSize.top + modulePadding; + } else { + left = centerX - groupSize.left - (groupSize.width / 2); + top = centerY - groupSize.top - (groupSize.height / 2); + } + + event.preventDefault(); + event.stopPropagation(); + animateReposition(draggable, left, top); + return false; + } + sidebar.find("li[data-module-id] > span > a.canvas-center-on").click(moduleHandler); + sidebar.find("li[data-module-group] > span > a.canvas-center-on").click(moduleGroupHandler); +} + +/** + * Initialize the jsPlumb by creating a new jsPlumb instance + * (overrides the currently set global variable 'instance'). + * @param containerSel - The jQuery selector text of the canvas container. + * @param containerChildSel - The jQuery selector text of the canvas container child. + * @param modulesSel - The jQuery selector text of module elements. + * @param params - Various parameters: + * @param params.gridDistX - Grid distance between modules in X direction. + * @param params.gridDistY - Grid distance between modules in Y direction. + * @param params.modulesDraggable - True if modules are draggable. + * @param params.connectionsEditable - True if connections can be created/edited/removed. + * @param params.zoomEnabled - True if zooming in/out is enabled. + * @param params.zoomMin - The minimum zoom level (0. or greater, 1.0 is no zoom). + * @param params.zoomMax - The maximum zoom level (0. or greater, 1.0 is no zoom). + * @param params.zoomDist - The distance to travel towards mouse position on zoom. + * @param params.scrollEnabled - True if dragging/scrolling of content is enabled. + * @param params.endpointStyle - jsPlumb endpoint style. + * @param params.connectionHoverStyle - jsPlumb style. + * @param params.connectionOverlayStyle - jsPlumb style. + * @param params.connectionLabelStyle - jsPlumb style. + * @param params.anchorStyle - jsPlumb anchor style. + * @param params.connectorStyle - jsPlumb endpoint style. + * @param params.connectorStyle2 - jsPlumb endpoint style. + */ +function initJsPlumb(containerSel, containerChildSel, modulesSel, params) { + // Functions used for scrolling + function mouseUp(event) { + container.off("mousemove touchmove"); + } + function mouseDown(event) { + var source = $(event.target); + if (!$(modulesSel).is(source) && + $(modulesSel).has(source).length === 0 && + source.is($(container))) { + // Only do drag & drop if it doesn't + // origin from module element + drag_type = DRAG_INVALID; + if (event.type == "mousedown" && (event.which || event.button) == 1) { + drag_type = DRAG_MOUSE; + } else if (event.type == "touchstart" && event.originalEvent.touches.length == 1) { + drag_type = DRAG_TOUCH; + } + if (drag_type > 0) { + event.preventDefault(); + event.stopPropagation(); + x_start = calcOffsetX(event); + y_start = calcOffsetY(event); + container.on("mousemove touchmove", moveDiagram); + } + } + function moveDiagram(event, x_offset, y_offset) { + // This function is invoked on mousemove + var x_pos = 0, y_pos = 0, x_el = 0, y_el = 0; + event.preventDefault(); + event.stopPropagation(); + x_el = draggable.offset().left - draggable.parent().offset().left; + y_el = draggable.offset().top - draggable.parent().offset().top; + // Scale offset for X + // (otherwise, this function only works on scale = 1.0) + x_so = draggable.parent().width() / 2 * (1 - instance.getZoom()); + x_pos = x_el - x_so + (calcOffsetX(event) - x_start); + y_pos = y_el + (calcOffsetY(event) - y_start); + x_start = calcOffsetX(event); + y_start = calcOffsetY(event); + if (draggable !== null) { + elLeft(draggable, x_pos); + elTop(draggable, y_pos); + } + } + } + // This needs to be here due to Firefox + function calcOffsetX(e) { + return (drag_type == DRAG_TOUCH ? + (e.originalEvent.touches[0].offsetX || e.originalEvent.touches[0].pageX) : + (e.offsetX || e.pageX) + ) - $(e.target).offset().left; + } + function calcOffsetY(e) { + return (drag_type == DRAG_TOUCH ? + (e.originalEvent.touches[0].offsetY || e.originalEvent.touches[0].pageY) : + (e.offsetY || e.pageY) + ) - $(e.target).offset().top; + } + + // Default parameter values + var params2 = params ? params : {}; + var gridDistX = params2.gridDistX || 350; + var gridDistY = params2.gridDistY || 350; + var modulesDraggable = params2.modulesDraggable || false; + var connectionsEditable = params2.connectionsEditable || false; + var zoomEnabled = params2.zoomEnabled || false; + var zoomMin = params2.zoomMin || 0.3; + var zoomMax = params2.zoomMax || 1.0; + var zoomDist = params2.zoomDist || 150; + var scrollEnabled = params2.scrollEnabled || true; + + // CSS_STYLE + var endpointStyle = params2.endpointStyle || DEFAULT_ENDPOINT_STYLE; + var connectionHoverStyle = params2.connectionHoverStyle || DEFAULT_CONNECTION_HOVER_STYLE; + var connectionOverlayStyle = params2.connectionOverlayStyle || DEFAULT_CONNECTION_OVERLAY_STYLE; + var connectionLabelStyle = params2.connectionLabelStyle || DEFAULT_CONNECTION_LABEL_STYLE; + var anchorStyle = params2.anchorStyle || null; + var connectorStyle = params2.connectorStyle || null; + var connectorStyle2 = params2.connectorStyle2 || null; + + // End of parameters block + + var container = $(containerSel); + var containerChild = $(containerChildSel); + + // Script for multitouch events + hammertime = new Hammer(document.getElementById("canvas-container")); + hammertime.get('pinch').set({ enable: true }); + + + function hammerZoom (event) { + zoom = instance.getZoom() * event.scale; + if (zoom < zoomMin) + zoom = zoomMin; + else if (zoom > zoomMax) + zoom = zoomMax; + zoomInstance(containerChild, zoom); + } + + // Setup some styling defaults for jsPlumb + instance = jsPlumb.getInstance({ + Endpoint: endpointStyle, + ConnectionOverlays: [ connectionOverlayStyle ], + Container: containerChild + }); + + window.jsp = instance; + var modules = $(modulesSel); + var jsp_modules = jsPlumb.getSelector(modulesSel); + draggable = $(containerChildSel); + + if (modulesDraggable) { + // Initialize draggable elements + addDraggablesToInstance(jsp_modules, gridDistX, gridDistY); + } + + if (connectionsEditable) { + // Prevent a connection to be made if it already exists between 2 modules + instance.bind("beforeDrop", function(info) { + var newConnection = info.connection; + var allConnections = instance.getAllConnections(); + var conn; + for (var i = 0; i < allConnections.length; i++) { + conn = allConnections[i]; + if (_.isEqual(conn.source, newConnection.source) && + _.isEqual(conn.target, newConnection.target)) { + return false; + } + } + + // Now, check if we created a cycle + var srcNode = $(newConnection.source).attr("id"); + var targetNode = $(newConnection.target).attr("id"); + graph.addEdge(srcNode, targetNode); + if (!jsnx.isDirectedAcyclicGraph(graph)) { + graph.removeEdge(srcNode, targetNode); + return false; + } + + var srcModuleEl = $("#" + srcNode); + var targetModuleEl = $("#" + targetNode); + + //Modules should belong to module group now + //Show module group options for target and source + + srcModuleEl.find(".edit-module-group").parents("li").show(); + srcModuleEl.find(".clone-module-group").parents("li").show(); + srcModuleEl.find(".delete-module-group").parents("li").show(); + + targetModuleEl.find(".edit-module-group").parents("li").show(); + targetModuleEl.find(".clone-module-group").parents("li").show(); + targetModuleEl.find(".delete-module-group").parents("li").show(); + return true; + }); + + // Bind a connection listener. Note that the parameter passed to this function contains more than + // just the new connection - see the documentation for a full list of what is included in 'info'. + // this listener sets the connection's internal + // id as the label overlay's text. + instance.bind("connection", function (info) { + }); + + // Bind a click listener to each connection + instance.bind("click", function (c) { + // Remove the edge from our graph data structure + graph.removeEdge(c.sourceId, c.targetId); + + // Remove edge from GUI + instance.detach(c); + + // Hide module group options if source or target module + // is not part of a module group anymore + + var srcModuleEl = $("#" + c.sourceId); + var targetModuleEl = $("#" + c.targetId); + //First source + if (graph.degree(c.sourceId) === 0) { + srcModuleEl.find(".edit-module-group").parents("li").hide(); + srcModuleEl.find(".clone-module-group").parents("li").hide(); + srcModuleEl.find(".delete-module-group").parents("li").hide(); + } + if (graph.degree(c.targetId) === 0) { + targetModuleEl.find(".edit-module-group").parents("li").hide(); + targetModuleEl.find(".clone-module-group").parents("li").hide(); + targetModuleEl.find(".delete-module-group").parents("li").hide(); + } + }); + } + + // Suspend drawing and initialize + if (modules.length > 0) { + instance.batch(function () { + // Set elements as connection sources + setElementsAsDragSources(jsp_modules, anchorStyle, connectorStyle, connectorStyle2); + + // Initialise all elements as connection targets + setElementsAsDropTargets(jsp_modules); + + // Initialize module connections + var module, outs; + _.each(modules, function(m) { + module = $(m); + outs = graph.successors(module.attr("id")); + _.each(outs, function(out) { + instance.connect({source: module.attr("id"), target: out }); + }); + }); + }); + } + + // Enable/disable connection endpoints + _.each(instance.getAllConnections(), function(conn) { + conn.endpoints[0].setEnabled(connectionsEditable); + conn.endpoints[1].setEnabled(connectionsEditable); + }); + + // Update style on existing connections + if (connectionsEditable) { + _.each(instance.getAllConnections(), function(conn) { + conn.setHoverPaintStyle(connectionHoverStyle); + conn.setLabel(connectionLabelStyle); + }); + } + + // Make sure the new connections will have same style + if (connectionsEditable) { + instance.importDefaults({ + HoverPaintStyle: connectionHoverStyle, + ConnectionOverlays: [ + connectionOverlayStyle, + [ "Label", connectionLabelStyle ] + ] + }); + } else { + instance.importDefaults({ + HoverPaintStyle: connectionHoverStyle, + ConnectionOverlays: [ connectionOverlayStyle ] + }); + } + + // Enable / disable instance configs + if (modules.length > 0) { + instance.setDraggable(jsp_modules, modulesDraggable); + instance.setSourceEnabled(jsp_modules, connectionsEditable); + instance.setTargetEnabled(jsp_modules, connectionsEditable); + } + + // Bind the mouse scroll event to scaling + if (zoomEnabled) { + container.mousewheel(function(event) { + zoom = instance.getZoom() + (event.deltaY / 20); + if (zoom < zoomMin || zoom > zoomMax) { + return; + } + zoomInstance(containerChild, zoom); + }); + + hammertime.on('pinch', hammerZoom); + } + + if (scrollEnabled) { + // Make the jsPlumb container movable/draggable for "google maps" effect + container.on("mousedown touchstart", mouseDown); + container.on("mouseup mouseout touchend", mouseUp); + } + + // If this is not triggered, dropdown in edit mode + // don't work on Firefox prior to zooming the canvas + zoomInstance(containerChild, 1.0); + + jsPlumb.fire("jsPlumbLoaded", instance); +} + +// Initialize first-time tutorial +function initializeTutorial() { + if (showTutorial()) { + var currentStep = Cookies.get('current_tutorial_step'); + if (currentStep == '5' || currentStep == '6') { + var sidebarTutorial = $("#canvas-container").attr("data-sidebar-step-text"); + Cookies.set('current_tutorial_step', '6'); + + introJs() + .setOptions({ + steps: [{ + element: document.querySelector("li.leaf[data-module-id='" + tutorialData[0].qpcr_module + "']"), + intro: sidebarTutorial, + position: 'right' + }], + overlayOpacity: '0.2', + doneLabel: 'End tutorial', + showBullets: false, + showStepNumbers: false, + tooltipClass: 'custom disabled-next' + }) + .start(); + } + else if (currentStep == '3' || currentStep == '4') { + Cookies.set('current_tutorial_step', '4'); + introJs() + .setOptions({ + overlayOpacity: '0.2', + doneLabel: 'End tutorial', + showBullets: false, + showStepNumbers: false, + tooltipClass: 'custom disabled-next' + }) + .start(); + + $(".introjs-overlay").addClass("introjs-no-overlay"); + var top = $('#canvas-container').position().top + $('#canvas-container').height()/3; + $(".introjs-tooltipReferenceLayer").css({ + top: top + 'px' + }); + } + + // 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() { + if (Cookies.get('tutorial_data')) + tutorialData = JSON.parse(Cookies.get('tutorial_data')); + else + return false; + var tutorialProjectId = tutorialData[0].project; + var currentProjectId = $("#canvas-container").attr("data-project-id"); + return tutorialProjectId == currentProjectId; +} diff --git a/app/assets/javascripts/projects/index.js b/app/assets/javascripts/projects/index.js new file mode 100644 index 000000000..f6f06f2d7 --- /dev/null +++ b/app/assets/javascripts/projects/index.js @@ -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("
  • " + data.html + "
  • ") + .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("" + data.error + ""); + } + }); + } + + // 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(); +}()); \ No newline at end of file diff --git a/app/assets/javascripts/reports/index.js b/app/assets/javascripts/reports/index.js new file mode 100644 index 000000000..3c63ef38a --- /dev/null +++ b/app/assets/javascripts/reports/index.js @@ -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(); + }); + +}()); diff --git a/app/assets/javascripts/reports/new_by_module.js b/app/assets/javascripts/reports/new_by_module.js new file mode 100644 index 000000000..ba125b9d5 --- /dev/null +++ b/app/assets/javascripts/reports/new_by_module.js @@ -0,0 +1,1158 @@ +// Custom jQuery function that finds elements including +// the parent element +$.fn.findWithSelf = function(selector) { + return this.filter(selector).add(this.find(selector)); +}; + +var REPORT_CONTENT = "#report-content"; +var SIDEBAR_PARENT_TREE = "#report-sidebar-tree"; +var ADD_CONTENTS_FORM_ID = "#add-contents-form"; +var SAVE_REPORT_FORM_ID = "#save-report-form"; + +var hotTableContainers = null; +var addContentsModal = null; +var addContentsModalBody = null; +var saveReportModal = null; +var saveReportModalBody = null; + +var ignoreUnsavedWorkAlert; + +/** + * INITIALIZATION FUNCTIONS + */ + +/** + * Initialize the hands on table on the given + * element with the specified data. + * @param el - The jQuery element/s selector. + */ +function initializeHandsonTable(el) { + var input = el.siblings("input.hot-table-contents"); + var inputObj = JSON.parse(input.attr("value")); + var data = inputObj.data; + + // Special handling if this is a samples table + if (input.hasClass("hot-samples")) { + var headers = inputObj.headers; + var parentEl = el.closest(".report-module-samples-element"); + var order = parentEl.attr("data-order") === "asc"; + + el.handsontable({ + disableVisualSelection: true, + rowHeaders: true, + colHeaders: headers, + columnSorting: true, + editor: false, + copyPaste: false + }); + el.handsontable("getInstance").loadData(data); + el.handsontable("getInstance").sort(3, order); + + // "Hack" to disable user sorting rows by clicking on + // header elements + el.handsontable("getInstance") + .addHook("afterRender", function() { + el.find(".colHeader.columnSorting") + .removeClass("columnSorting"); + }); + } else { + el.handsontable({ + disableVisualSelection: true, + rowHeaders: true, + colHeaders: true, + editor: false, + copyPaste: false + }); + el.handsontable("getInstance").loadData(data); + } +} + +/** +* Initialize the controls for the specified report element. +* @param el - The element in the report. +*/ +function initializeElementControls(el) { + var controls = el.find(".report-element-header:first .controls"); + controls.find("[data-action='sort-asc']").click(function(e) { + var el = $(this).closest(".report-element"); + if (el.hasClass("report-comments-element")) { + sortCommentsElement(el, true); + } else if (el.hasClass("report-module-activity-element")) { + sortModuleActivityElement(el, true); + } else if (el.hasClass("report-module-samples-element")) { + sortModuleSamplesElement(el, true); + } else { + sortElementChildren(el, true, false); + } + e.preventDefault(); + e.stopPropagation(); + return false; + }); + controls.find("[data-action='sort-desc']").click(function(e) { + var el = $(this).closest(".report-element"); + if (el.hasClass("report-comments-element")) { + sortCommentsElement(el, false); + } else if (el.hasClass("report-module-activity-element")) { + sortModuleActivityElement(el, false); + } else if (el.hasClass("report-module-samples-element")) { + sortModuleSamplesElement(el, false); + } else { + sortElementChildren(el, false, false); + } + e.preventDefault(); + e.stopPropagation(); + return false; + }); + controls.find("[data-action='move-up']").click(function(e) { + var el = $(this).closest(".report-element"); + moveElement(el, true); + e.preventDefault(); + e.stopPropagation(); + return false; + }); + controls.find("[data-action='move-down']").click(function(e) { + var el = $(this).closest(".report-element"); + moveElement(el, false); + e.preventDefault(); + e.stopPropagation(); + return false; + }); + controls.find("[data-action='remove']").click(function(e) { + var el = $(this).closest(".report-element"); + if (el.hasClass("report-result-comments-element")) { + removeResultCommentsElement(el); + } else { + removeElement(el); + } + e.preventDefault(); + e.stopPropagation(); + return false; + }); +} + +/** + * Initialize all the neccesary report elements stuff for all + * descendants of the provided parent DOM element. + * @param parentElement - The parent DOM element. + */ +function initializeReportElements(parentElement) { + // Initialize handsontable containers + _.each(parentElement.findWithSelf(".hot-table-container"), function(el) { + initializeHandsonTable($(el)); + }); + + // Add event listeners element to controls + _.each(parentElement.findWithSelf(".report-element"), function(el) { + initializeElementControls($(el)); + updateElementControls($(el)); + }); + + // Initialize new element click actions + _.each(parentElement.findWithSelf(".new-element"), function(el) { + initializeNewElement($(el)); + }); +} + +/** + * Initialize the new element click action. + * @param newEl - The "new element" element in the report. + */ +function initializeNewElement(newEl) { + newEl.click(function(e) { + var el = $(this); + var dh = $("#data-holder"); + var parent = el.closest(".report-element"); + + var parentElementId; + var url; + var modalTitle; + + if (!parent.length) { + // No parent, this means adding is done on report level + parentElementId = "null"; + url = dh.data("add-project-contents-url"); + modalTitle = dh.data("project-modal-title"); + } else { + parentElementId = parent.data("id"); + modalTitle = parent.data("modal-title"); + + if (parent.data("type") == "my_module") { + // Adding module contents + url = dh.data("add-module-contents-url"); + } else if (parent.data("type") == "step") { + // Adding step contents + url = dh.data("add-step-contents-url"); + } else if (_.contains( + ["result_asset", "result_table", "result_text"], + parent.data("type"))) { + // Adding result comments + url = dh.data("add-result-contents-url"); + } + } + + // Send AJAX request to retrieve the modal contents + $.ajax({ + url: url, + type: "GET", + dataType: "json", + data: { + id: parentElementId + }, + success: function(data, status, jqxhr) { + // Open modal, set its title + addContentsModal.find(".modal-title").text(modalTitle); + + // Display module contents + addContentsModalBody.html(data.html); + + // Bind to the ajax events of the modal form in its body + $(ADD_CONTENTS_FORM_ID) + .on("ajax:beforeSend", function(){ + animateSpinner(this); + }) + .on("ajax:success", function(e, xhr, opts, data) { + if (data.status == 204) { + // No content was added, simply hide modal + } else if (data.status == 200) { + // Add elements + addElements(el, data.responseJSON.elements); + + // Update sidebar + initializeSidebarNavigation(); + } + }) + .on("ajax:error", function(e, xhr, settings, error) { + // TODO + }) + .on("ajax:complete", function(){ + animateSpinner(this, false); + addContentsModal.modal("hide"); + }); + + // Execute any JS code the response might contain + _.each(addContentsModalBody.find("script"), function (script) { + eval(script.text); + }); + + // Finally, show the modal + addContentsModal.modal("show"); + }, + error: function(jqxhr, status, error) { + // TODO + } + }); + + // Prevent page reload + e.preventDefault(); + e.stopPropagation(); + return false; + }); +} + +/** + * Initialize the modal window for adding content. + */ +function initializeAddContentsModal() { + addContentsModal = $("#add-contents-modal"); + addContentsModalBody = addContentsModal.find(".modal-body"); + + // Remove "add contents" modal content when modal + // window is closed + addContentsModal.on("hidden.bs.modal", function () { + addContentsModalBody.html(""); + }); + + // Bind click action of "add" button in modal footer to + // submit the form in the modal body + addContentsModal.find(".modal-footer button[data-action='add']") + .click(function(e) { + $(ADD_CONTENTS_FORM_ID).submit(); + }); +} + +/** + * Initialize the save report modal window etc. + * for saving the report. + */ +function initializeSaveReport() { + var dh = $("#data-holder"); + var saveReportLink = $("#save-report-link"); + + saveReportModal = $("#save-report-modal"); + saveReportModalBody = saveReportModal.find(".modal-body"); + + var modalContentsUrl = dh.data("save-report-url"); + + var reportId = dh.data("report-id"); + + // Remove "save report" modal content when modal + // window is closed + saveReportModal.on("hidden.bs.modal", function () { + saveReportModalBody.html(""); + }); + + saveReportLink.click(function(e) { + // Send AJAX request to retrieve the modal contents + $.ajax({ + url: modalContentsUrl, + type: "POST", + global: false, + dataType: "json", + data: { + id: reportId, + contents: JSON.stringify(constructReportContentsJson()) + }, + success: function(data, status, jqxhr) { + // Display module contents + saveReportModalBody.html(data.html); + + // Bind to the ajax events of the modal form in its body + $(SAVE_REPORT_FORM_ID) + .on("ajax:beforeSend", function() { + animateSpinner(this); + }) + .on("ajax:success", function(e, xhr, opts, data) { + if (data.status == 200) { + // Redirect back to index + ignoreUnsavedWorkAlert = true; + $(location).attr("href", xhr.url); + } + }) + .on("ajax:error", function(e, xhr, settings, error) { + // Display errors + if (xhr.status == 422) { + $(this).render_form_errors("report", xhr.responseJSON); + } else { + // TODO + } + }) + .on("ajax:complete", function () { + animateSpinner(this, false); + }); + + // Finally, show the modal + saveReportModal.modal("show"); + }, + error: function(jqxhr, status, error) { + // TODO + } + }); + }); + + // Bind click action of "save" button in modal footer to + // submit the form in the modal body + saveReportModal.find(".modal-footer button[data-action='save']") + .click(function(e) { + $(SAVE_REPORT_FORM_ID).submit(); + }); +} + +/** + * Initialize global report sorting action. + */ +function initializeGlobalReportSort() { + $("#sort-report .dropdown-menu a[data-sort]").click(function(ev) { + var dh = $("#data-holder"); + var $this = $(this); + var asc = true; + + if ($(ev.target).data("sort") == "desc") { + asc = false; + } + + if (confirm(dh.data("global-sort-text"))) { + sortWholeReport(asc); + } + }); +} + +/** + * Initialize the print popup functionality. + */ +function initializePrintPopup() { + $("#print-report").click(function() { + var html = $(REPORT_CONTENT).html(); + var dh = $("#data-holder"); + + var print_window = window.open( + "", + "_blank", + "width=" + screen.width + + ",height=" + screen.height + + ",fullscreen=yes" + + ",scrollbars=yes" + ); + + print_window.document.open(); + print_window.document.write( + "" + + "" + + "" + + dh.data("print-title") + + "" + + "" + + "" + + "" + + "" + + "" + ); + print_window.document.close(); + }); +} + +/** + * Initialize the save to PDF functionality. + */ +function initializeSaveToPdf() { + var saveToPdfForm = $(".get-report-pdf-form"); + var hiddenInput = saveToPdfForm.find("input[type='hidden']"); + var saveToPdfBtn = saveToPdfForm.find("#get-report-pdf"); + + saveToPdfBtn.click(function(e) { + var content = $(REPORT_CONTENT); + + // Fill hidden input element + hiddenInput.attr("value", content.html()); + + // Fire form submission + saveToPdfForm.submit(); + + // Clear form + hiddenInput.attr("value", ""); + + // Prevent page reload + e.preventDefault(); + e.stopPropagation(); + return false; + }); +} + +function initializeUnsavedWorkDialog() { + var dh = $("#data-holder"); + var alertText = dh.attr("data-unsaved-work-text"); + + ignoreUnsavedWorkAlert = false; + + $(window) + .on("beforeunload", function(ev) { + if (ignoreUnsavedWorkAlert) { + // Remove unload listeners + $(window).off("beforeunload"); + $(document).off("page:before-change"); + + ev.returnValue = undefined; + return undefined; + } else { + return alertText; + } + }); + $(document).on("page:before-change", function(ev) { + var exit; + if (ignoreUnsavedWorkAlert) { + exit = true; + } else { + exit = confirm(alertText); + } + + if (exit) { + // Remove unload listeners + $(window).off("beforeunload"); + $(document).off("page:before-change"); + } + + return exit; + }); +} + +/** + * SIDEBAR CODE + */ + + /** + * Get the sidebar
  • element for the specified report element. + * @param reportEl - The .report-element in the report. + * @return The corresponding sidebar
  • . + */ +function getSidebarEl(reportEl) { + var type = reportEl.data("type"); + var id = reportEl.data("id"); + return $(SIDEBAR_PARENT_TREE).find( + "li" + + "[data-type='" + type + "']" + + "[data-id='" + id + "']" + ); +} + +/** + * Get the report element for the specified + * sidebar element. + * @param sidebarEl - The
  • sidebar element. + * @return The corresponding report element. + */ +function getReportEl(sidebarEl) { + var type = sidebarEl.data("type"); + var id = sidebarEl.data("id"); + return $(REPORT_CONTENT).find( + "div.report-element" + + "[data-type='" + type + "']" + + "[data-id='" + id + "']" + ); +} + +/** + * Initialize the sidebar navigation pane. + */ +function initializeSidebarNavigation() { + var reportContent = $(REPORT_CONTENT); + var treeParent = $(SIDEBAR_PARENT_TREE); + + // Remove existing contents (also remove click listeners) + treeParent.find(".report-nav-link").off("click"); + treeParent.children().remove(); + + // Re-populate the sidebar + _.each(reportContent.children(".report-element"), function(child) { + var li = initSidebarElement($(child)); + li.appendTo(treeParent); + }); + + // Add click listener on all links + treeParent.find(".report-nav-link").click(function(e) { + var el = $(this).closest("li"); + scrollToElement(el); + + e.preventDefault(); + e.stopPropagation(); + return false; + }); + + // Call to sidebar function to re-initialize tree functionality + setupSidebarTree(); +} + +/** + * Recursive call to initialize sidebar elements. + * @param reportEl - The report element for which to + * generate the sidebar. + * @return A
  • jQuery element containing sidebar entry. + */ +function initSidebarElement(reportEl) { + var elChildrenContainer = reportEl.children(".report-element-children"); + var type = reportEl.data("type"); + var name = reportEl.data("name"); + var id = reportEl.data("id"); + var iconClass = "glyphicon " + reportEl.data("icon-class"); + + // Generate list element + var newLi = $(document.createElement("li")); + newLi + .attr("data-type", type) + .attr("data-id", id); + + var newSpan = $(document.createElement("span")); + newSpan.appendTo(newLi); + var newI = $(document.createElement("i")); + newI.appendTo(newSpan); + var newHref = $(document.createElement("a")); + newHref + .attr("href", "") + .addClass("report-nav-link") + .text(name) + .appendTo(newSpan); + var newIcon = $(document.createElement("span")); + newIcon.addClass(iconClass).prependTo(newHref); + + if (elChildrenContainer.length && elChildrenContainer.length > 0) { + var elChildren = elChildrenContainer.children(".report-element"); + if (elChildren.length && elChildren.length > 0) { + var newUl = $(document.createElement("ul")); + newUl.appendTo(newLi); + + _.each(elChildren, function(child) { + var li = initSidebarElement($(child)); + li.appendTo(newUl); + }); + } + } + + return newLi; +} + +/** + * Scroll to the specified element in the report. + * @param sidebarEl - The sidebar element. + */ +function scrollToElement(sidebarEl) { + var el = getReportEl(sidebarEl); + + if (el.length && el.length == 1) { + var content = $("body"); + content.scrollTo( + el, + { + axis: 'y', + duration: 500, + offset: -150 + } + ); + } +} + +/** + * INDIVIDUAL ELEMENTS SORTING/MODIFYING FUNCTIONS + */ + + /** + * Update the enabled/disabled state of element controls. + * @param el - The element in the report. + */ +function updateElementControls(el) { + var controls = el.find(".report-element-header:first .controls"); + + var isFirst = !el.prevAll(".report-element:not(.report-project-header-element)").length; + var isLast = !el.nextAll(".report-element").length; + + controls.find("[data-action='move-up']") + .css("display", isFirst ? "none" : ""); + controls.find("[data-action='move-down']") + .css("display", isLast ? "none" : ""); +} + +/** + * Sort the whole report with all of its elements. + * @param asc - True to sort in ascending order, false to sort + * in descending order. + */ +function sortWholeReport(asc) { + animateLoading(); + var reportContent = $(REPORT_CONTENT); + var moduleElements = reportContent.children(".report-module-element"); + var newEls = reportContent.children(".new-element"); + + if ( + moduleElements.length === 0 || // Nothing to sort + moduleElements.length != newEls.length - 1 // This should never happen + ) { + return; + } + + moduleElements = _.sortBy(moduleElements, function(el) { + if (!asc) + { + return -$(el).data("ts"); + } + return $(el).data("ts"); + }); + + newEls.detach(); + moduleElements = $(moduleElements); + moduleElements.detach(); + + // Re-insert the children into DOM + reportContent.append(newEls[0]); + for (var i = 0; i < moduleElements.length; i++) { + reportContent.append(moduleElements[i]); + reportContent.append(newEls[i + 1]); + } + + // Finally, fix their controls + _.each(moduleElements, function(el) { + updateElementControls($(el)); + sortElementChildren($(el), asc, true); + }); + + // Reinitialize sidebar + initializeSidebarNavigation(); + animateLoading(false); +} + +/** + * Sort the element's children. + * @param el - The element in the report. + * @param asc - True to sort in ascending order, false to sort + * in descending order. + * @param recursive - True to recursively sort the element's + * children; false otherwise. + */ +function sortElementChildren(el, asc, recursive) { + var childrenEl = el.find(".report-element-children:first"); + var newEls = childrenEl.children(".new-element"); + var children = childrenEl.children(".report-element"); + + if ( + children.length === 0 || // No children, keep things + children.length != newEls.length - 1 // This should never happen + ) { + return; + } + + children = _.sortBy(children, function(child) { + if (!asc) + { + return -$(child).data("ts"); + } + return $(child).data("ts"); + }); + + newEls.detach(); + children = $(children); + children.detach(); + + // Re-insert the children into DOM + childrenEl.append(newEls[0]); + for (var i = 0; i < children.length; i++) { + childrenEl.append(children[i]); + childrenEl.append(newEls[i + 1]); + } + + // Finally, fix their controls + _.each(children, function(child) { + updateElementControls($(child)); + if (recursive) { + sortElementChildren($(child), asc, true); + } + }); + + // Update sidebar + var prevEl = null; + _.each(children, function(child) { + var sidebarEl = getSidebarEl($(child)); + if (sidebarEl.length && sidebarEl.length == 1) { + var sidebarParent = sidebarEl.closest("ul"); + sidebarEl.detach(); + if (prevEl === null) { + sidebarParent.prepend(sidebarEl); + } else { + prevEl.after(sidebarEl); + } + prevEl = sidebarEl; + } + }); +} + +/** + * Sort the comments element (special handling needs to be + * done in this case). + * @param el - The comments element in the report. + * @param asc - True to sort in ascending order, false to sort + * in descending order. + */ +function sortCommentsElement(el, asc) { + var commentsList = el.find(".comments-list:first"); + var comments = commentsList.children(".comment"); + + comments = _.sortBy(comments, function(comment) { + if (!asc) + { + return -$(comment).data("ts"); + } + return $(comment).data("ts"); + }); + + comments = $(comments); + comments.detach(); + _.each(comments, function(comment) { + commentsList.append(comment); + }); + + // Update data attribute on sorting on the element + el.attr("data-order", asc ? "asc" : "desc"); +} + +/** + * Sort the module activity element (special handling needs + * to be done in this case). + * @param el - The module activity element in the report. + * @param asc - True to sort in ascending order, false to sort + * in descending order. + */ +function sortModuleActivityElement(el, asc) { + var activityList = el.find(".activity-list:first"); + var activities = activityList.children(".activity"); + + activities = _.sortBy(activities, function(activity) { + if (!asc) + { + return -$(activity).data("ts"); + } + return $(activity).data("ts"); + }); + + activities = $(activities); + activities.detach(); + _.each(activities, function(activity) { + activityList.append(activity); + }); + + // Update data attribute on sorting on the element + el.attr("data-order", asc ? "asc" : "desc"); +} + +/** + * Sort the module samples element (special handling needs + * to be done in this case). + * @param el - The module samples element in the report. + * @param asc - True to sort in ascending order, false to sort + * in descending order. + */ +function sortModuleSamplesElement(el, asc) { + var hotEl = el.find(".report-element-body .hot-table-container"); + var hotInstance = hotEl.handsontable("getInstance"); + + hotInstance.sort(3, asc); + + // Update data attribute on sorting on the element + el.attr("data-order", asc ? "asc" : "desc"); +} + +/** + * Move the specified element up or down. + * @param el - The element in the report. + * @param up - True to move element up; false to move it down. + */ +function moveElement(el, up) { + var prevNewEl = el.prev(); + var nextNewEl = el.next(); + if ( + !prevNewEl.length || + !prevNewEl.hasClass("new-element") || + !nextNewEl.length || + !nextNewEl.hasClass("new-element")) { + return; + } + + var sidebarEl; + if (up) { + var prevEl = prevNewEl.prev(); + if (!prevEl.length || !prevEl.hasClass("report-element")) { + return; + } + + // Move sidebar element up + sidebarEl = getSidebarEl(el); + var sidebarPrev = sidebarEl.prev(); + sidebarEl.detach(); + sidebarPrev.before(sidebarEl); + + el.detach(); + nextNewEl.detach(); + prevEl.before(el); + prevEl.before(nextNewEl); + updateElementControls(prevEl); + + + } else { + var nextEl = nextNewEl.next(); + if (!nextEl.length || !nextEl.hasClass("report-element")) { + return; + } + + // Move sidebar element up + sidebarEl = getSidebarEl(el); + var sidebarNext = sidebarEl.next(); + sidebarEl.detach(); + sidebarNext.after(sidebarEl); + + prevNewEl.detach(); + el.detach(); + nextEl.after(el); + nextEl.after(prevNewEl); + updateElementControls(nextEl); + } + + updateElementControls(el); +} + +/** + * Remove the specified element (and all its children) + * from the report. + * @param el - The element in the report. + */ +function removeElement(el) { + var prevNewEl = el.prev(); + var nextNewEl = el.next(); + + if ( + !prevNewEl.length || + !prevNewEl.hasClass("new-element") || + !nextNewEl.length || + !nextNewEl.hasClass("new-element")) { + return; + } + + // TODO Remove event listeners + + // Remove sidebar entry + var sidebarEl = getSidebarEl(el); + sidebarEl.remove(); + + prevNewEl.remove(); + el.remove(); + + // Fix controls of previous / next element + var prevEl = prevNewEl.prev(); + var nextEl = nextNewEl.next(); + + if (prevEl.length && prevEl.hasClass("report-element")) { + updateElementControls(prevEl); + } + if (nextEl.length && nextEl.hasClass("report-element")) { + updateElementControls(nextEl); + } +} + +/** + * Remove the specified comment element from the report. + * @param el - The comments element in the report. + */ +function removeResultCommentsElement(el) { + var parent = el.closest(".report-element-children"); + + // TODO Remove event listeners + + // Remove sidebar entry + var sidebarEl = getSidebarEl(el); + sidebarEl.remove(); + + // Remove element, show the new element container + el.remove(); + parent.children(".new-element").removeClass("hidden"); +} + +/** + * ADDING CONTENT FUNCTIONS + */ + + /** + * Inject new elements into the place where the "old", + * new element
    was previously located. Also take + * care of everything else. + * @param newElToBeReplaced - The "new element" to be replaced. + * @param elements - A JSON array of elements to be injected. + */ +function addElements(newElToBeReplaced, elements) { + var parent; + + // Remove event listener on the new element to be replaced + newElToBeReplaced.off("click"); + + parent = newElToBeReplaced.parent(); + newElToBeReplaced.addClass("original-new-el"); + var prevEl = newElToBeReplaced; + var lastChild, secLastChild; + var newElements = []; + for (var i = 0; i < elements.length; i++) { + var newEl = addElement(elements[i], prevEl); + prevEl = newEl; + newElements.push(newEl); + } + + if (parent.length && parent.length > 0) { + // Remove a potential last new child element remaining + lastChild = parent.children().last(); + if (lastChild.length && lastChild.length > 0) { + secLastChild = lastChild.prev(); + if (secLastChild.length && secLastChild.length > 0) { + if (lastChild.hasClass("new-element") && + secLastChild.hasClass("new-element")) { + // TODO Remove event listeners on existing element + + lastChild.remove(); + } + } + } + } + + // Remove the temporary class from all + // added new elements + $(".new-element.added-new-element") + .removeClass("added-new-element"); + + // Remove the "replaced" element + newElToBeReplaced.remove(); + + // Initialize everything on all elements + _.each(newElements, function(element) { + initializeReportElements($(element)); + }); +} + +/** + * Add a single element to the DOM after the + * previous element. This function is recursive. + * @param jsonEl - The JSON element to be inserted. + * @param prevEl - The jQuery element preceeding + * the newly injected element. + * @returns The newly created jQuery element. + */ +function addElement(jsonEl, prevEl) { + var el = $(jsonEl.html); + + // If such an element already exists in the report, remove it + // (this only applies to report elements) + if (el.hasClass("report-element")) { + var existing = $(REPORT_CONTENT) + .find( + ".report-element" + + "[data-type='" + el.attr("data-type") + "']" + + "[data-id='" + el.attr("data-id") + "']" + ); + if (existing.length && existing.length > 0) { + // TODO Remove event listeners on existing element + + // First, remove the "new element" before the existing one + var prevNewEl = existing.prev(); + if (prevNewEl.hasClass("new-element") && + !prevNewEl.hasClass("original-new-el") && + !prevNewEl.hasClass("added-new-element")) { + // TODO Remove event listeners on existing element + + prevNewEl.remove(); + } + existing.remove(); + } + } else if (el.hasClass("new-element")) { + el.addClass("added-new-element"); + } + + // Add the new element after the previous one + prevEl.after(el); + + // If element has children, recursively add them to the element + var children = jsonEl.children; + var childrenContainer = el.find(".report-element-children"); + var child, prevChild; + var lastChild, secLastChild; + if (!_.isUndefined(children) && children.length > 0) { + for (var i = 0; i < children.length; i++) { + if (i === 0) { + // Make a "dummy" child so following children + // can be added after it recursively + var tmpDiv = $(""); + childrenContainer.append(tmpDiv); + child = addElement(children[i], tmpDiv); + tmpDiv.remove(); + } else { + child = addElement(children[i], prevChild); + } + prevChild = child; + } + + // Remove a potential last new child element remaining + lastChild = childrenContainer.children().last(); + if (lastChild.length && lastChild.length > 0) { + secLastChild = lastChild.prev(); + if (secLastChild.length && secLastChild.length > 0) { + if (lastChild.hasClass("new-element") && + secLastChild.hasClass("new-element")) { + // TODO Remove event listeners on existing element + + lastChild.remove(); + } + } + } + } + + return el; +} + +/** + * Construct the report contents JSON. This is used + * when saving/updating the report. + * @return The JSON representation of report contents. + */ +function constructReportContentsJson() { + var res = []; + var rootEls = $(REPORT_CONTENT).children(".report-element"); + + _.each(rootEls, function(el) { + res.push(constructElementContentsJson($(el))); + }); + + return res; +} + +/** + * Recursive function to retrieve element JSON contents. + * @return The JSON representation of the element. + */ +function constructElementContentsJson(el) { + var jsonEl = {}; + jsonEl["type_of"] = el.data("type"); + jsonEl["id"] = el.data("id"); + jsonEl["sort_order"] = null; + if (!_.isUndefined(el.data("order"))) { + jsonEl["sort_order"] = el.data("order"); + } + + var jsonChildren = []; + var childrenContainer = el.children(".report-element-children"); + if (childrenContainer.length && childrenContainer.length == 1) { + var children = childrenContainer.children(".report-element"); + _.each(children, function(child) { + jsonChildren.push(constructElementContentsJson($(child))); + }); + } + jsonEl["children"] = jsonChildren; + + return jsonEl; +} + +/* Initialize the first-time demo tutorial if needed. */ +function initializeTutorial() { + if (showTutorial()) { + Cookies.set('current_tutorial_step', '12'); + introJs() + .setOptions({ + overlayOpacity: '0.1', + doneLabel: 'End tutorial', + showBullets: false, + showStepNumbers: false, + tooltipClass: 'custom disabled-next' + }) + .start(); + + $(".introjs-showElement").addClass("send-to-back"); + $(".introjs-tooltipReferenceLayer").addClass("send-to-back"); + // 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 currentStep = Cookies.get('current_tutorial_step'); + if (currentStep != '11' && currentStep != '12') + return false; + var tutorialProjectId = tutorialData[0].project; + var currentProjectId = $("#data-holder").attr("data-project-id"); + return tutorialProjectId == currentProjectId; +} + +/** + * ACTUAL CODE + */ +initializeReportElements($(REPORT_CONTENT)); + +initializeGlobalReportSort(); +initializePrintPopup(); +initializeSaveToPdf(); +initializeSaveReport(); +initializeAddContentsModal(); +initializeSidebarNavigation(); +initializeUnsavedWorkDialog(); +initializeTutorial(); diff --git a/app/assets/javascripts/results/result_assets.js b/app/assets/javascripts/results/result_assets.js new file mode 100644 index 000000000..3003da064 --- /dev/null +++ b/app/assets/javascripts/results/result_assets.js @@ -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("" + errors["asset.file"] + ""); + } +} + +// 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(); diff --git a/app/assets/javascripts/results/result_comments.js b/app/assets/javascripts/results/result_comments.js new file mode 100644 index 000000000..dee720fac --- /dev/null +++ b/app/assets/javascripts/results/result_comments.js @@ -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. diff --git a/app/assets/javascripts/results/result_tables.js b/app/assets/javascripts/results/result_tables.js new file mode 100644 index 000000000..2437aea17 --- /dev/null +++ b/app/assets/javascripts/results/result_tables.js @@ -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(); diff --git a/app/assets/javascripts/results/result_texts.js b/app/assets/javascripts/results/result_texts.js new file mode 100644 index 000000000..e9171cfad --- /dev/null +++ b/app/assets/javascripts/results/result_texts.js @@ -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("" + data["result_text.text"] + ""); + } + }); +} + + +applyEditResultTextCallback(); diff --git a/app/assets/javascripts/samples/sample_datatable.js b/app/assets/javascripts/samples/sample_datatable.js new file mode 100644 index 000000000..631ff2c11 --- /dev/null +++ b/app/assets/javascripts/samples/sample_datatable.js @@ -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 ' ' + + ''; + }, + 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 ""; + } + }, { + 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( + $('') + .attr('type', 'hidden') + .attr('name', 'header_ids[]') + .val(val) + ); + }); + + } +}); + +function appendSamplesIdToForm(form) { + $.each(rowsSelected, function(index, rowId){ + $(form).append( + $('') + .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("" + init_fields["name"] + "
    "); + } + } + }; + + // 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("" + val["value"][0] + "
    "); + } + }); + }); + + // 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("" + val["value"][0] + "
    "); + } + }); + }); + } + } + }); +} + +// 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 "
    "; +} + +// 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 = $("").attr("name", "sample_type_id").addClass("show-tick"); + + var $option = $("").attr("value", -1).text(I18n.t("samples.table.no_type")) + $selectType.append($option); + + $.each(data, function(i, val) { + var $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 = $("").attr("name", "sample_group_id").addClass("show-tick"); + + var $span = $("").addClass("glyphicon glyphicon-asterisk"); + var $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 = $("").addClass("glyphicon glyphicon-asterisk").css("color", val["color"]); + var $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("
    Error! " + msg + "
    "); +} + diff --git a/app/assets/javascripts/samples/sample_groups.js b/app/assets/javascripts/samples/sample_groups.js new file mode 100644 index 000000000..dee720fac --- /dev/null +++ b/app/assets/javascripts/samples/sample_groups.js @@ -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. diff --git a/app/assets/javascripts/samples/sample_types.js b/app/assets/javascripts/samples/sample_types.js new file mode 100644 index 000000000..dee720fac --- /dev/null +++ b/app/assets/javascripts/samples/sample_types.js @@ -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. diff --git a/app/assets/javascripts/samples/samples.js b/app/assets/javascripts/samples/samples.js new file mode 100644 index 000000000..a9c2be6e2 --- /dev/null +++ b/app/assets/javascripts/samples/samples.js @@ -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("" + val[0].charAt(0).toUpperCase() + val[0].slice(1) +"
    "); + }); +}); + +// 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("" + val[0].charAt(0).toUpperCase() + val[0].slice(1) +"
    "); + }); +}); + +// 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("" + val[0].charAt(0).toUpperCase() + val[0].slice(1) +"
    "); + }); +}); + + +// 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("" + data.responseJSON.message + ""); +}); + + +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(); \ No newline at end of file diff --git a/app/assets/javascripts/samples/samples_importer.js b/app/assets/javascripts/samples/samples_importer.js new file mode 100644 index 000000000..10e8e4d55 --- /dev/null +++ b/app/assets/javascripts/samples/samples_importer.js @@ -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); + } +}); \ No newline at end of file diff --git a/app/assets/javascripts/sidebar.js b/app/assets/javascripts/sidebar.js new file mode 100644 index 000000000..57b3dc5d3 --- /dev/null +++ b/app/assets/javascripts/sidebar.js @@ -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 + 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(); + }); +}()); diff --git a/app/assets/javascripts/sitewide/.keep b/app/assets/javascripts/sitewide/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/assets/javascripts/sitewide/form_errors.js b/app/assets/javascripts/sitewide/form_errors.js new file mode 100644 index 000000000..20706a792 --- /dev/null +++ b/app/assets/javascripts/sitewide/form_errors.js @@ -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 = ''; + error_text += (_.map(messages, function(m) { + return m.charAt(0).toUpperCase() + m.slice(1); + })).join('
    '); + error_text += '
    '; + 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( + "Must be less than 50 MB" + ); + } + } + } + }); + + 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"); +} diff --git a/app/assets/javascripts/sitewide/url_handling.js b/app/assets/javascripts/sitewide/url_handling.js new file mode 100644 index 000000000..beb604dd7 --- /dev/null +++ b/app/assets/javascripts/sitewide/url_handling.js @@ -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; + }); +}); \ No newline at end of file diff --git a/app/assets/javascripts/step_comments.js b/app/assets/javascripts/step_comments.js new file mode 100644 index 000000000..dee720fac --- /dev/null +++ b/app/assets/javascripts/step_comments.js @@ -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. diff --git a/app/assets/javascripts/user_my_modules.js b/app/assets/javascripts/user_my_modules.js new file mode 100644 index 000000000..dee720fac --- /dev/null +++ b/app/assets/javascripts/user_my_modules.js @@ -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. diff --git a/app/assets/javascripts/users/registrations/edit.js b/app/assets/javascripts/users/registrations/edit.js new file mode 100644 index 000000000..0b77b6636 --- /dev/null +++ b/app/assets/javascripts/users/registrations/edit.js @@ -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("" + avatarError + ""); + } + }, "avatar"); + + ev.preventDefault(); +} + + diff --git a/app/assets/javascripts/users/settings/organization.js b/app/assets/javascripts/users/settings/organization.js new file mode 100644 index 000000000..58efe2f47 --- /dev/null +++ b/app/assets/javascripts/users/settings/organization.js @@ -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(); \ No newline at end of file diff --git a/app/assets/javascripts/users/settings/organizations.js b/app/assets/javascripts/users/settings/organizations.js new file mode 100644 index 000000000..573e7b626 --- /dev/null +++ b/app/assets/javascripts/users/settings/organizations.js @@ -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(); \ No newline at end of file diff --git a/app/assets/javascripts/users/settings/organizations/add_user_modal.js b/app/assets/javascripts/users/settings/organizations/add_user_modal.js new file mode 100644 index 000000000..9c36ec7d7 --- /dev/null +++ b/app/assets/javascripts/users/settings/organizations/add_user_modal.js @@ -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(); + } +}); + diff --git a/app/assets/javascripts/users/settings/preferences.js b/app/assets/javascripts/users/settings/preferences.js new file mode 100644 index 000000000..5f028beca --- /dev/null +++ b/app/assets/javascripts/users/settings/preferences.js @@ -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); +}); \ No newline at end of file diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss new file mode 100644 index 000000000..616c0e1af --- /dev/null +++ b/app/assets/stylesheets/application.scss @@ -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"; diff --git a/app/assets/stylesheets/colors.scss b/app/assets/stylesheets/colors.scss new file mode 100644 index 000000000..1f6e5546b --- /dev/null +++ b/app/assets/stylesheets/colors.scss @@ -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; diff --git a/app/assets/stylesheets/custom_fields.scss b/app/assets/stylesheets/custom_fields.scss new file mode 100644 index 000000000..dfffcf497 --- /dev/null +++ b/app/assets/stylesheets/custom_fields.scss @@ -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/ diff --git a/app/assets/stylesheets/extend/bootstrap.scss b/app/assets/stylesheets/extend/bootstrap.scss new file mode 100644 index 000000000..5f76666f4 --- /dev/null +++ b/app/assets/stylesheets/extend/bootstrap.scss @@ -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; +} + diff --git a/app/assets/stylesheets/mixins.scss b/app/assets/stylesheets/mixins.scss new file mode 100644 index 000000000..a9de05aa9 --- /dev/null +++ b/app/assets/stylesheets/mixins.scss @@ -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); +} diff --git a/app/assets/stylesheets/my_modules.scss b/app/assets/stylesheets/my_modules.scss new file mode 100644 index 000000000..9a5b3f79e --- /dev/null +++ b/app/assets/stylesheets/my_modules.scss @@ -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; +} \ No newline at end of file diff --git a/app/assets/stylesheets/organizations.scss b/app/assets/stylesheets/organizations.scss new file mode 100644 index 000000000..2cd042feb --- /dev/null +++ b/app/assets/stylesheets/organizations.scss @@ -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/ diff --git a/app/assets/stylesheets/partials/_sidebar.scss b/app/assets/stylesheets/partials/_sidebar.scss new file mode 100644 index 000000000..202e9a2d7 --- /dev/null +++ b/app/assets/stylesheets/partials/_sidebar.scss @@ -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; +} diff --git a/app/assets/stylesheets/partials/_tree_view.scss b/app/assets/stylesheets/partials/_tree_view.scss new file mode 100644 index 000000000..9d13c9c78 --- /dev/null +++ b/app/assets/stylesheets/partials/_tree_view.scss @@ -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; +} diff --git a/app/assets/stylesheets/project_activities.scss b/app/assets/stylesheets/project_activities.scss new file mode 100644 index 000000000..e2256c239 --- /dev/null +++ b/app/assets/stylesheets/project_activities.scss @@ -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/ diff --git a/app/assets/stylesheets/projects.scss b/app/assets/stylesheets/projects.scss new file mode 100644 index 000000000..ddbb4f5a7 --- /dev/null +++ b/app/assets/stylesheets/projects.scss @@ -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; + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/reports.scss b/app/assets/stylesheets/reports.scss new file mode 100644 index 000000000..8303b0ebd --- /dev/null +++ b/app/assets/stylesheets/reports.scss @@ -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; + } + } +} diff --git a/app/assets/stylesheets/reports_pdf.scss b/app/assets/stylesheets/reports_pdf.scss new file mode 100644 index 000000000..71f26e61f --- /dev/null +++ b/app/assets/stylesheets/reports_pdf.scss @@ -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; + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/reports_print.scss b/app/assets/stylesheets/reports_print.scss new file mode 100644 index 000000000..e7cfeac45 --- /dev/null +++ b/app/assets/stylesheets/reports_print.scss @@ -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; + } + } + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/result_assets.scss b/app/assets/stylesheets/result_assets.scss new file mode 100644 index 000000000..e385f36db --- /dev/null +++ b/app/assets/stylesheets/result_assets.scss @@ -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/ diff --git a/app/assets/stylesheets/result_comments.scss b/app/assets/stylesheets/result_comments.scss new file mode 100644 index 000000000..18daaa0e6 --- /dev/null +++ b/app/assets/stylesheets/result_comments.scss @@ -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/ diff --git a/app/assets/stylesheets/result_tables.scss b/app/assets/stylesheets/result_tables.scss new file mode 100644 index 000000000..933c58364 --- /dev/null +++ b/app/assets/stylesheets/result_tables.scss @@ -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/ diff --git a/app/assets/stylesheets/result_texts.scss b/app/assets/stylesheets/result_texts.scss new file mode 100644 index 000000000..4ea45dffd --- /dev/null +++ b/app/assets/stylesheets/result_texts.scss @@ -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/ diff --git a/app/assets/stylesheets/sample_groups.scss b/app/assets/stylesheets/sample_groups.scss new file mode 100644 index 000000000..dd56c5546 --- /dev/null +++ b/app/assets/stylesheets/sample_groups.scss @@ -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/ diff --git a/app/assets/stylesheets/sample_types.scss b/app/assets/stylesheets/sample_types.scss new file mode 100644 index 000000000..f561d92c5 --- /dev/null +++ b/app/assets/stylesheets/sample_types.scss @@ -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/ diff --git a/app/assets/stylesheets/samples.scss b/app/assets/stylesheets/samples.scss new file mode 100644 index 000000000..8d13e9743 --- /dev/null +++ b/app/assets/stylesheets/samples.scss @@ -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; + } +} \ No newline at end of file diff --git a/app/assets/stylesheets/search.scss b/app/assets/stylesheets/search.scss new file mode 100644 index 000000000..7ef5abb96 --- /dev/null +++ b/app/assets/stylesheets/search.scss @@ -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; +} diff --git a/app/assets/stylesheets/step_comments.scss b/app/assets/stylesheets/step_comments.scss new file mode 100644 index 000000000..37a85def0 --- /dev/null +++ b/app/assets/stylesheets/step_comments.scss @@ -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/ diff --git a/app/assets/stylesheets/steps.scss b/app/assets/stylesheets/steps.scss new file mode 100644 index 000000000..85680557f --- /dev/null +++ b/app/assets/stylesheets/steps.scss @@ -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; + } + } + } +} diff --git a/app/assets/stylesheets/themes/scinote.scss b/app/assets/stylesheets/themes/scinote.scss new file mode 100644 index 000000000..4598d9322 --- /dev/null +++ b/app/assets/stylesheets/themes/scinote.scss @@ -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; +} diff --git a/app/assets/stylesheets/user_my_modules.scss b/app/assets/stylesheets/user_my_modules.scss new file mode 100644 index 000000000..1cc5aacd3 --- /dev/null +++ b/app/assets/stylesheets/user_my_modules.scss @@ -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/ diff --git a/app/controllers/activities_controller.rb b/app/controllers/activities_controller.rb new file mode 100644 index 000000000..d27a3cdd1 --- /dev/null +++ b/app/controllers/activities_controller.rb @@ -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 diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb new file mode 100644 index 000000000..df27645d2 --- /dev/null +++ b/app/controllers/application_controller.rb @@ -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 diff --git a/app/controllers/assets_controller.rb b/app/controllers/assets_controller.rb new file mode 100644 index 000000000..f63aa6fbb --- /dev/null +++ b/app/controllers/assets_controller.rb @@ -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 + diff --git a/app/controllers/canvas_controller.rb b/app/controllers/canvas_controller.rb new file mode 100644 index 000000000..37f7f8ae0 --- /dev/null +++ b/app/controllers/canvas_controller.rb @@ -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 diff --git a/app/controllers/concerns/.keep b/app/controllers/concerns/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/controllers/concerns/sample_actions.rb b/app/controllers/concerns/sample_actions.rb new file mode 100644 index 000000000..52ee3bb11 --- /dev/null +++ b/app/controllers/concerns/sample_actions.rb @@ -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 diff --git a/app/controllers/custom_fields_controller.rb b/app/controllers/custom_fields_controller.rb new file mode 100644 index 000000000..ad8ee810e --- /dev/null +++ b/app/controllers/custom_fields_controller.rb @@ -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 diff --git a/app/controllers/my_module_comments_controller.rb b/app/controllers/my_module_comments_controller.rb new file mode 100644 index 000000000..ae755fa3d --- /dev/null +++ b/app/controllers/my_module_comments_controller.rb @@ -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 diff --git a/app/controllers/my_module_tags_controller.rb b/app/controllers/my_module_tags_controller.rb new file mode 100644 index 000000000..b683034f8 --- /dev/null +++ b/app/controllers/my_module_tags_controller.rb @@ -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 diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb new file mode 100644 index 000000000..781571bb4 --- /dev/null +++ b/app/controllers/my_modules_controller.rb @@ -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 diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb new file mode 100644 index 000000000..e6ddb7a90 --- /dev/null +++ b/app/controllers/organizations_controller.rb @@ -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 diff --git a/app/controllers/project_activities_controller.rb b/app/controllers/project_activities_controller.rb new file mode 100644 index 000000000..e76cc5be1 --- /dev/null +++ b/app/controllers/project_activities_controller.rb @@ -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 diff --git a/app/controllers/project_comments_controller.rb b/app/controllers/project_comments_controller.rb new file mode 100644 index 000000000..365359008 --- /dev/null +++ b/app/controllers/project_comments_controller.rb @@ -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 diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb new file mode 100644 index 000000000..9b86b4509 --- /dev/null +++ b/app/controllers/projects_controller.rb @@ -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 diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb new file mode 100644 index 000000000..71e230d4c --- /dev/null +++ b/app/controllers/reports_controller.rb @@ -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 = "

    No content

    " + 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 \ No newline at end of file diff --git a/app/controllers/result_assets_controller.rb b/app/controllers/result_assets_controller.rb new file mode 100644 index 000000000..1e1f965d4 --- /dev/null +++ b/app/controllers/result_assets_controller.rb @@ -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 diff --git a/app/controllers/result_comments_controller.rb b/app/controllers/result_comments_controller.rb new file mode 100644 index 000000000..8285a0b60 --- /dev/null +++ b/app/controllers/result_comments_controller.rb @@ -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 diff --git a/app/controllers/result_tables_controller.rb b/app/controllers/result_tables_controller.rb new file mode 100644 index 000000000..711f5baa0 --- /dev/null +++ b/app/controllers/result_tables_controller.rb @@ -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 + diff --git a/app/controllers/result_texts_controller.rb b/app/controllers/result_texts_controller.rb new file mode 100644 index 000000000..c5bf778eb --- /dev/null +++ b/app/controllers/result_texts_controller.rb @@ -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 + diff --git a/app/controllers/sample_groups_controller.rb b/app/controllers/sample_groups_controller.rb new file mode 100644 index 000000000..d3a5632d8 --- /dev/null +++ b/app/controllers/sample_groups_controller.rb @@ -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 diff --git a/app/controllers/sample_my_modules_controller.rb b/app/controllers/sample_my_modules_controller.rb new file mode 100644 index 000000000..73ea1c15d --- /dev/null +++ b/app/controllers/sample_my_modules_controller.rb @@ -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 diff --git a/app/controllers/sample_types_controller.rb b/app/controllers/sample_types_controller.rb new file mode 100644 index 000000000..6c6e7d6c8 --- /dev/null +++ b/app/controllers/sample_types_controller.rb @@ -0,0 +1,94 @@ +class SampleTypesController < 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_type = SampleType.new + session[:return_to] ||= request.referer + end + + def create + @sample_type = SampleType.new(sample_type_params) + @sample_type.organization = @organization + @sample_type.created_by = current_user + @sample_type.last_modified_by = current_user + + respond_to do |format| + if @sample_type.save + flash[:success] = t( + "sample_types.create.success_flash", + sample_type: @sample_type.name, + organization: @organization.name + ) + format.json { + render json: { + id: @sample_type.id + }, + status: :ok + } + else + format.json { + render json: @sample_type.errors, + status: :unprocessable_entity + } + end + end + end + + def edit + + end + + def update + @sample_type.last_modified_by = current_user + if @sample_type.update_attributes(sample_type_params) + flash[:success] = t( + "sample_types.update.success_flash", + sample_type: @sample_type.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_type = SampleType.find_by_id(params[:id]) + @organization = @sample_type.organization + + unless @sample_type + 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_type_params + params.require(:sample_type).permit(:name) + end +end diff --git a/app/controllers/samples_controller.rb b/app/controllers/samples_controller.rb new file mode 100644 index 000000000..e20b081b8 --- /dev/null +++ b/app/controllers/samples_controller.rb @@ -0,0 +1,290 @@ +class SamplesController < ApplicationController + before_action :load_vars, only: [:edit, :update, :destroy] + before_action :load_vars_nested, only: [:new, :create] + + before_action :check_edit_permissions, only: [:edit] + before_action :check_destroy_permissions, only: [:destroy] + + def new + respond_to do |format| + format.html + if can_create_samples(@organization) + format.json { + render json: { + sample_groups: @organization.sample_groups.as_json(only: [:id, :name, :color]), + sample_types: @organization.sample_types.as_json(only: [:id, :name]) + } + } + else + format.json { render json: {}, status: :unauthorized } + end + end + end + + def create + sample = Sample.new( + user: current_user, + organization: @organization + ) + sample.last_modified_by = current_user + errors = { + init_fields: [], + custom_fields: [] + }; + + respond_to do |format| + if can_create_samples(@organization) + if params[:sample] + # Sample name + if params[:sample][:name] + sample.name = params[:sample][:name] + end + + # Sample type + if params[:sample][:sample_type_id] != "-1" + sample_type = SampleType.find_by_id(params[:sample][:sample_type_id]) + + if sample_type + sample.sample_type_id = params[:sample][:sample_type_id] + end + end + + # Sample group + if params[:sample][:sample_group_id] != "-1" + sample_group = SampleGroup.find_by_id(params[:sample][:sample_group_id]) + + if sample_group + sample.sample_group_id = params[:sample][:sample_group_id] + end + end + end + + if !sample.save + errors[:init_fields] = sample.errors.messages + else + # Sample was saved, we can add all newly added sample fields + params[:custom_fields].to_a.each do |id, val| + scf = SampleCustomField.new( + custom_field_id: id, + sample_id: sample.id, + value: val + ) + + if !scf.save + errors[:custom_fields] << { + "#{id}": scf.errors.messages + } + end + end + end + + errors.delete_if { |k, v| v.blank? } + if errors.empty? + format.json { render json: {}, status: :ok } + else + format.json { render json: errors, status: :bad_request } + end + else + format.json { render json: {}, status: :unauthorized } + end + end + end + + def edit + json = { + sample: { + name: @sample.name, + sample_type: @sample.sample_type.nil? ? "" : @sample.sample_type.id, + sample_group: @sample.sample_group.nil? ? "" : @sample.sample_group.id, + custom_fields: {} + }, + sample_groups: @organization.sample_groups.as_json(only: [:id, :name, :color]), + sample_types: @organization.sample_types.as_json(only: [:id, :name]) + } + + # Add custom fields ids as key (easier lookup on js side) + @sample.sample_custom_fields.each do |scf| + json[:sample][:custom_fields][scf.custom_field_id] = { + sample_custom_field_id: scf.id, + value: scf.value + } + end + + respond_to do |format| + format.html + format.json { + render json: json + } + end + end + + def update + sample = Sample.find_by_id(params[:sample_id]) + sample.last_modified_by = current_user + errors = { + init_fields: [], + sample_custom_fields: [], + custom_fields: [] + }; + + respond_to do |format| + if sample + if can_edit_sample(sample) + if params[:sample] + if params[:sample][:name] + sample.name = params[:sample][:name] + end + + # Check if user selected empty sample type + if params[:sample][:sample_type_id] == "-1" + sample.sample_type_id = nil + elsif params[:sample][:sample_type_id] + sample_type = SampleType.find_by_id(params[:sample][:sample_type_id]) + + if sample_type + sample.sample_type_id = params[:sample][:sample_type_id] + end + end + + # Check if user selected empty sample type + if params[:sample][:sample_group_id] == "-1" + sample.sample_group_id = nil + elsif params[:sample][:sample_group_id] + sample_group = SampleGroup.find_by_id(params[:sample][:sample_group_id]) + + if sample_group + sample.sample_group_id = params[:sample][:sample_group_id] + end + end + end + + # Add all newly added sample fields + params[:custom_fields].to_a.each do |id, val| + # Check if client is lying (SCF shouldn't exist) + scf = SampleCustomField.where("custom_field_id = ? AND sample_id = ?", id, sample.id).take + + if scf + # Well, client was naughty, no XMAS for him this year, update + # existing SCF instead of creating new one + scf.value = val + + if !scf.save + # This client needs some lessons + errors[:custom_fields] << { + "#{id}": scf.errors.messages + } + end + else + # SCF doesn't exist, create it + scf = SampleCustomField.new( + custom_field_id: id, + sample_id: sample.id, + value: val + ) + + if !scf.save + errors[:custom_fields] << { + "#{id}": scf.errors.messages + } + end + end + end + + scf_to_delete = [] + # Update all existing custom values + params[:sample_custom_fields].to_a.each do |id, val| + scf = SampleCustomField.find_by_id(id) + + if scf + # SCF exists, but value is empty, add scf to queue to be deleted + # (if everything is correct) + if val.empty? + scf_to_delete << scf + else + # SCF exists, update away + scf.value = val + + if !scf.save + errors[:sample_custom_fields] << { + "#{id}": scf.errors.messages + } + end + end + else + # SCF doesn't exist, we can't do much but yield error + errors[:sample_custom_fields] << { + "#{id}": I18n.t("samples.edit.scf_does_not_exist") + } + end + end + + if !sample.save + errors[:init_fields] = sample.errors.messages + end + + errors.delete_if { |k, v| v.blank? } + if errors.empty? + # Now we can destroy empty scfs + scf_to_delete.map(&:destroy) + + format.json { render json: {}, status: :ok } + else + format.json { render json: errors, status: :bad_request } + end + else + format.json { render json: {}, status: :unauthorized } + end + else + format.json { render json: {}, status: :not_found } + end + end + end + + def destroy + end + + private + + def load_vars + @sample = Sample.find_by_id(params[:id]) + @organization = @sample.organization + + unless @sample + 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_samples(@organization) + render_403 + end + end + + def check_edit_permissions + unless can_edit_sample(@sample) + render_403 + end + end + + def check_destroy_permissions + unless can_delete_samples(@organization) + render_403 + end + end + + def sample_params + params.require(:sample).permit( + :name, + :sample_type_id, + :sample_group_id + ) + end +end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb new file mode 100644 index 000000000..881a5ed5a --- /dev/null +++ b/app/controllers/search_controller.rb @@ -0,0 +1,181 @@ +class SearchController < ApplicationController + before_filter :load_vars, only: :index + + MIN_QUERY_CHARS = 3 + + def index + if not @search_query + redirect_to new_search_path + end + + count_search_results + + search_projects if @search_category == :projects + search_modules if @search_category == :modules + search_workflows if @search_category == :workflows + search_tags if @search_category == :tags + search_assets if @search_category == :assets + search_steps if @search_category == :steps + search_results if @search_category == :results + search_samples if @search_category == :samples + search_reports if @search_category == :reports + search_comments if @search_category == :comments + search_contents if @search_category == :contents + + @search_pages = (@search_count.to_f / SEARCH_LIMIT.to_f).ceil + @start_page = @search_page - 2 + @start_page = 1 if @start_page < 1 + @end_page = @start_page + 4 + + if @end_page > @search_pages + @end_page = @search_pages + @start_page = @end_page - 4 + @start_page = 1 if @start_page < 1 + end + end + + def new + end + + private + + def load_vars + @search_query = params[:q] || '' + @search_category = params[:category] || '' + @search_category = @search_category.to_sym + @search_page = params[:page].to_i || 1 + + if @search_query.length < MIN_QUERY_CHARS + flash[:error] = t'search.index.error.query_length', n: MIN_QUERY_CHARS + redirect_to new_search_path + end + + if @search_page < 1 + @search_page = 1 + end + end + + protected + + def search_by_name(model) + model.search(current_user, true, @search_query, @search_page) + end + + def count_by_name(model) + search_by_name(model).limit(nil).offset(nil).size + end + + def count_search_results + @project_search_count = count_by_name Project + @module_search_count = count_by_name MyModule + @workflow_search_count = count_by_name MyModuleGroup + @tag_search_count = count_by_name Tag + @asset_search_count = count_by_name Asset + @step_search_count = count_by_name Step + @result_search_count = count_by_name Result + @sample_search_count = count_by_name Sample + @report_search_count = count_by_name Report + @comment_search_count = count_by_name Comment + @contents_search_count = count_by_name AssetTextDatum + + @search_results_count = @project_search_count + @search_results_count += @module_search_count + @search_results_count += @workflow_search_count + @search_results_count += @tag_search_count + @search_results_count += @asset_search_count + @search_results_count += @step_search_count + @search_results_count += @result_search_count + @search_results_count += @sample_search_count + @search_results_count += @report_search_count + @search_results_count += @comment_search_count + @search_results_count += @contents_search_count + end + + def search_projects + @project_results = [] + if @project_search_count > 0 then + @project_results = search_by_name Project + end + @search_count = @project_search_count + end + + def search_modules + @module_results = [] + if @module_search_count > 0 then + @module_results = search_by_name MyModule + end + @search_count = @module_search_count + end + + def search_workflows + @workflow_results = [] + if @workflow_search_count > 0 then + @workflow_results = search_by_name MyModuleGroup + end + @search_count = @workflow_search_count + end + + def search_tags + @tag_results = [] + if @tag_search_count > 0 then + @tag_results = search_by_name Tag + end + @search_count = @tag_search_count + end + + def search_assets + @asset_results = [] + if @asset_search_count > 0 then + @asset_results = search_by_name Asset + end + @search_count = @asset_search_count + end + + def search_steps + @step_results = [] + if @step_search_count > 0 then + @step_results = search_by_name Step + end + @search_count = @step_search_count + end + + def search_results + @result_results = [] + if @result_search_count > 0 then + @result_results = search_by_name Result + end + @search_count = @result_search_count + end + + def search_samples + @sample_results = [] + if @sample_search_count > 0 then + @sample_results = search_by_name Sample + end + @search_count = @sample_search_count + end + + def search_reports + @report_results = [] + if @report_search_count > 0 then + @report_results = search_by_name Report + end + @search_count = @report_search_count + end + + def search_comments + @comment_results = [] + if @comment_search_count > 0 then + @comment_results = search_by_name Comment + end + @search_count = @comment_search_count + end + + def search_contents + @contents_results = [] + if @contents_search_count > 0 then + @contents_results = search_by_name AssetTextDatum + end + @search_count = @contents_search_count + end +end diff --git a/app/controllers/step_comments_controller.rb b/app/controllers/step_comments_controller.rb new file mode 100644 index 000000000..94ccf8581 --- /dev/null +++ b/app/controllers/step_comments_controller.rb @@ -0,0 +1,125 @@ +class StepCommentsController < ApplicationController + before_action :load_vars + + before_action :check_view_permissions, only: [ :index ] + before_action :check_add_permissions, only: [ :new, :create ] + + def index + @comments = @step.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(step_step_comments_path(@step, + 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? && @step.comments << @comment) + + # Generate activity + Activity.create( + type_of: :add_comment_to_step, + user: current_user, + project: @step.my_module.project, + my_module: @step.my_module, + message: t( + "activities.add_comment_to_step", + user: current_user.full_name, + step: @step.position + 1, + step_name: @step.name + ) + ) + + format.html { + flash[:success] = t( + "step_comments.create.success_flash", + step: @step.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 + @step = Step.find_by_id(params[:step_id]) + @my_module = @step.my_module + + unless @step + render_404 + end + end + + def check_view_permissions + unless can_view_step_comments(@my_module) + render_403 + end + end + + def check_add_permissions + unless can_add_step_comment_in_module(@my_module) + render_403 + end + end + + def comment_params + params.require(:comment).permit(:message) + end + +end diff --git a/app/controllers/steps_controller.rb b/app/controllers/steps_controller.rb new file mode 100644 index 000000000..0bb137a1c --- /dev/null +++ b/app/controllers/steps_controller.rb @@ -0,0 +1,560 @@ +class StepsController < ApplicationController + before_action :load_vars, only: [:edit, :update, :destroy, :show] + before_action :load_vars_nested, only: [:new, :create] + before_action :load_paperclip_vars + before_action :convert_table_contents_to_utf8, only: [:create, :update] + + before_action :check_view_permissions, only: [:show] + before_action :check_create_permissions, only: [:new, :create] + before_action :check_edit_permissions, only: [:edit, :update] + before_action :check_destroy_permissions, only: [:destroy] + + def new + @step = Step.new + + respond_to do |format| + format.json { + render json: { + html: render_to_string({ + partial: "new.html.erb", + locals: { + direct_upload: @direct_upload + } + }) + } + } + end + end + + def create + if @direct_upload + step_data = step_params.except(:assets_attributes) + step_assets = step_params.slice(:assets_attributes) + @step = Step.new(step_data) + + if step_assets.size > 0 + step_assets[:assets_attributes].each do |i, data| + asset = Asset.find_by_id(data[:id]) + asset.created_by = current_user + asset.last_modified_by = current_user + @step.assets << asset + end + end + else + @step = Step.new(step_params) + end + + @step.completed = false + @step.position = @my_module.number_of_steps + @step.my_module = @my_module + @step.user = current_user + @step.last_modified_by = current_user + + # Update default checked state + @step.checklists.each do |checklist| + checklist.checklist_items.each do |checklist_item| + checklist_item.checked = false + end + end + + respond_to do |format| + if @step.save + # Post process all assets + @step.assets.each do |asset| + asset.post_process_file(@my_module.project.organization) + end + + # Generate activity + Activity.create( + type_of: :create_step, + user: current_user, + project: @my_module.project, + my_module: @my_module, + message: t( + "activities.create_step", + user: current_user.full_name, + step: @step.position + 1, + step_name: @step.name + ) + ) + + flash_success = t("my_modules.steps.create.success_flash", module: @my_module.name) + format.html { + flash[:success] = flash_success + redirect_to steps_my_module_path(@my_module) + } + format.json { + render json: { + html: render_to_string({ + partial: "my_modules/step.html.erb", locals: {step: @step} + })}, status: :ok + } + else + format.json { + render json: { + html: render_to_string({ + partial: "new.html.erb", + locals: { + direct_upload: @direct_upload + } + }) + }, status: :bad_request + } + end + end + end + + def show + respond_to do |format| + format.json { + render json: { + html: render_to_string({ + partial: "my_modules/step.html.erb", locals: {step: @step} + })}, status: :ok + } + 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 + respond_to do |format| + previous_size = @step.space_taken + + step_params_all = step_params + + # process only destroy update on step references. This prevents + # skipping deleting reference in case update validation fails. + # NOTE - step_params_all variable is updated + destroy_attributes(step_params_all) + + if @direct_upload + step_data = step_params_all.except(:assets_attributes) + step_assets = step_params_all.slice(:assets_attributes) + step_params_all = step_data + + if step_assets.include? :assets_attributes + step_assets[:assets_attributes].each do |i, data| + asset_id = data[:id] + asset = Asset.find_by_id(asset_id) + + unless @step.assets.include? asset or not asset + asset.last_modified_by = current_user + @step.assets << asset + end + end + end + end + + @step.assign_attributes(step_params_all) + @step.last_modified_by = current_user + + if @step.save + @step.reload + + # Release organization's space taken + org = @step.my_module.project.organization + org.release_space(previous_size) + org.save + + # Post process step assets + @step.assets.each do |asset| + asset.post_process_file(org) + end + + # Generate activity + Activity.create( + type_of: :edit_step, + user: current_user, + project: @step.my_module.project, + my_module: @step.my_module, + message: t( + "activities.edit_step", + user: current_user.full_name, + step: @step.position + 1, + step_name: @step.name + ) + ) + + flash_success = t( + "my_modules.steps.update.success_flash", + step: (@step.position + 1).to_s) + format.html { + flash[:success] = flash_success + redirect_to steps_my_module_path(@step.my_module) + } + format.json { + render json: { + html: render_to_string({ + partial: "my_modules/step.html.erb", locals: {step: @step} + })}, status: :ok + } + else + format.json { + render json: { + html: render_to_string({ + partial: "edit.html.erb", + locals: { + direct_upload: @direct_upload + } + })}, status: :bad_request + } + end + end + end + + def destroy + # Update position on other steps of this module + my_module = @step.my_module + my_module.steps.where("position > ?", @step.position).each do |step| + step.position = step.position - 1 + step.save + end + + # Calculate space taken by this step + org = @step.my_module.project.organization + previous_size = @step.space_taken + + # Destroy the step + @step.destroy(current_user) + + # Release space taken by the step + org.release_space(previous_size) + org.save + + flash[:success] = t( + "my_modules.steps.destroy.success_flash", + step: (@step.position + 1).to_s) + redirect_to steps_my_module_path(@step.my_module) + end + + # Responds to checkbox toggling in steps view + def checklistitem_state + chkItem = ChecklistItem.find_by_id(params["checklistitem_id"]) + + respond_to do |format| + if chkItem + checked = params[:checked] == "true" + my_module = chkItem.checklist.step.my_module + + authorized = ((checked and can_check_checkbox(my_module)) or (!checked and can_uncheck_checkbox(my_module))) + + if authorized + changed = chkItem.checked != checked + chkItem.checked = checked + + if chkItem.save + format.json { + render json: {}, status: :accepted + } + + # Create activity + if changed + str = checked ? "activities.check_step_checklist_item" : + "activities.uncheck_step_checklist_item" + completed_items = chkItem.checklist.checklist_items.where(checked: true).count + all_items = chkItem.checklist.checklist_items.count + message = t( + str, + user: current_user.full_name, + checkbox: chkItem.text, + step: chkItem.checklist.step.position + 1, + step_name: chkItem.checklist.step.name, + completed: completed_items, + all: all_items + ) + + Activity.create( + user: current_user, + project: my_module.project, + my_module: my_module, + message: message, + type_of: checked ? :check_step_checklist_item : :uncheck_step_checklist_item + ) + end + else + format.json { + render json: {}, status: :unprocessable_entity + } + end + else + format.json { + render json: {}, status: :unauthorized + } + end + else + format.json { + render json: {}, status: :not_found + } + end + end + end + + # Complete/uncomplete step + def toggle_step_state + step = Step.find_by_id(params[:id]) + + respond_to do |format| + if step + completed = params[:completed] == "true" + my_module = step.my_module + + authorized = ((completed and can_complete_step_in_module(my_module)) or (!completed and can_uncomplete_step_in_module(my_module))) + + if authorized + changed = step.completed != completed + step.completed = completed + + # Update completed_on + if changed + step.completed_on = completed ? Time.current : nil + end + + if step.save + # Create activity + if changed + completed_steps = my_module.steps.where(completed: true).count + all_steps = my_module.steps.count + str = completed ? "activities.complete_step" : + "activities.uncomplete_step" + + message = t( + str, + user: current_user.full_name, + step: step.position + 1, + step_name: step.name, + completed: completed_steps, + all: all_steps + ) + + Activity.create( + user: current_user, + project: my_module.project, + my_module: my_module, + message: message, + type_of: completed ? :complete_step : :uncomplete_step + ) + end + + # Create localized title for complete/uncomplete button + localized_title = !completed ? + t("my_modules.steps.options.complete_title") : + t("my_modules.steps.options.uncomplete_title") + + format.json { + render json: {new_title: localized_title}, status: :accepted + } + else + format.json { + render json: {}, status: :unprocessable_entity + } + end + else + format.json { + render json: {}, status: :unauthorized + } + end + else + format.json { + render json: {}, status: :not_found + } + end + end + end + + def move_up + step = Step.find_by_id(params[:id]) + + if step + if can_reorder_step_in_module(step.my_module) + if step.position > 0 + step_down = step.my_module.steps.where(position: step.position - 1).first + step.position -= 1 + step.save + + if step_down + step_down.position += 1 + step_down.save + end + end + else + render_403 and return + end + else + render_404 and return + end + redirect_to steps_my_module_path(step.my_module) + end + + def move_down + step = Step.find_by_id(params[:id]) + + if step + if can_reorder_step_in_module(step.my_module) + if step.position < step.my_module.steps.count - 1 + step_down = step.my_module.steps.where(position: step.position + 1).first + step.position += 1 + step.save + + if step_down + step_down.position -= 1 + step_down.save + end + end + else + render_403 and return + end + else + render_404 and return + end + redirect_to steps_my_module_path(step.my_module) + end + + private + + # This function is used for partial update of step references and + # it's useful when you want to execute destroy action on attribute + # collection separately from normal update action, for example if + # you don't want that update validation interupt destroy action. + # In case of step model you can delete checkboxes, assets or tables. + def destroy_attributes(params) + update_params = {} + extract_destroy_params(params, update_params) + @step.update_attributes(update_params) unless update_params.empty? + end + + # Checks if hash contains destroy parameter '_destroy' and returns + # boolean value. + def has_destroy_params(params) + for key, values in params do + if values.respond_to?(:each) + for pos, attrs in params[key] do + return true if attrs[:_destroy] == "1" + end + end + end + + false + end + + # Extracts part of hash that contains destroy parameters. It deletes + # values that contains destroy parameters from original variable and + # puts them into update_params variable. + def extract_destroy_params(params, update_params) + for key, values in params do + if values.respond_to?(:each) + update_params[key] = {} unless update_params[key] + attr_params = update_params[key] + + for pos, attrs in params[key] do + if attrs[:_destroy] == "1" + attr_params[pos] = {id: attrs[:id], _destroy: "1"} + params[key].delete(pos) + else + if has_destroy_params(params[key][pos]) + attr_params[pos] = {id: attrs[:id]} + extract_destroy_params(params[key][pos], attr_params[pos]) + end + end + end + end + end + end + + def load_paperclip_vars + @direct_upload = ENV['PAPERCLIP_DIRECT_UPLOAD'] + end + + def load_vars + @step = Step.find_by_id(params[:id]) + @my_module = @step.my_module + @project = @my_module.project + + unless @my_module + render_404 + end + end + + def load_vars_nested + @my_module = MyModule.find_by_id(params[:my_module_id]) + @project = @my_module.project + + unless @my_module + render_404 + end + end + + def convert_table_contents_to_utf8 + if params.include? :step and + params[:step].include? :tables_attributes then + params[:step][:tables_attributes].each do |k,v| + params[:step][:tables_attributes][k][:contents] = + v[:contents].encode(Encoding::UTF_8).force_encoding(Encoding::UTF_8) + end + end + end + + def check_view_permissions + unless can_view_steps_in_module(@my_module) + render_403 + end + end + + def check_create_permissions + unless can_create_step_in_module(@my_module) + render_403 + end + end + + def check_edit_permissions + unless can_edit_step_in_module(@my_module) + render_403 + end + end + + def check_destroy_permissions + unless can_delete_step_in_module(@my_module) + render_403 + end + end + + def step_params + params.require(:step).permit( + :name, + :description, + checklists_attributes: [ + :id, + :name, + :_destroy, + checklist_items_attributes: [ + :id, + :text, + :position, + :_destroy + ] + ], + assets_attributes: [ + :id, + :file, + :_destroy + ], + tables_attributes: [ + :id, + :contents, + :_destroy + ] + ) + end +end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb new file mode 100644 index 000000000..d8ce8fe68 --- /dev/null +++ b/app/controllers/tags_controller.rb @@ -0,0 +1,154 @@ +class TagsController < ApplicationController + before_action :load_vars, only: [:new, :create, :update, :destroy] + before_action :load_vars_nested, only: [:update, :destroy] + before_action :check_create_permissions, only: [:new, :create] + before_action :check_update_permissions, only: [:update] + before_action :check_destroy_permissions, only: [:destroy] + + def new + @tag = Tag.new + session[:return_to] ||= request.referer + end + + def create + @tag = Tag.new(tag_params) + @tag.created_by = current_user + @tag.last_modified_by = current_user + + if @tag.name.blank? + @tag.name = t("tags.create.new_name") + end + + if @tag.color.blank? + @tag.color = TAG_COLORS[0] + end + + if @tag.save + if params.include? "my_module_id" + # Assign the tag to the specified module + new_mmt = MyModuleTag.new( + my_module_id: params[:my_module_id], + tag_id: @tag.id) + new_mmt.save + end + + flash_success = t( + "tags.create.success_flash", + tag: @tag.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(params[:my_module_id], @tag, format: :json), :status => 303 + } + end + else + flash_error = t("tags.create.error_flash") + respond_to do |format| + format.html { + flash[:error] = flash_error + render :new + } + format.json { + # TODO + redirect_to my_module_tags_edit_path(params[:my_module_id], @tag, format: :json), :status => 303 + } + end + end + end + + def update + @tag.last_modified_by = current_user + if @tag.update_attributes(tag_params) + respond_to do |format| + format.html + format.json { + redirect_to my_module_tags_edit_path(params[:my_module_id], @tag, format: :json), :status => 303 + } + end + else + respond_to do |format| + format.html + format.json { + render json: @tag.errors, status: :unprocessable_entity + } + end + end + end + + def destroy + if @tag.destroy + flash_success = t( + "tags.destroy.success_flash", + tag: @tag.name) + + respond_to do |format| + format.html { + flash[:success] = flash_success + redirect_to root_path + } + format.json { + redirect_to my_module_tags_edit_path(params[:my_module_id], @tag, format: :json), :status => 303 + } + end + else + flash_error = t( + "tags.destroy.error_flash", + tag: @tag.name) + + respond_to do |format| + format.html { + flash[:error] = flash_error + redirect_to root_path + } + format.json { + # TODO + redirect_to my_module_tags_edit_path(format: :json), :status => 303 + } + end + end + end + + private + + def load_vars + @project = Project.find_by_id(params[:project_id]) + + unless @project + render_404 + end + end + + def load_vars_nested + @tag = Tag.find_by_id(params[:id]) + + unless @tag + render_404 + end + end + + # Currently unimplemented + def check_create_permissions + unless can_create_new_tag(@project) + render_403 + end + end + + def check_update_permissions + unless can_edit_tag(@project) + render_403 + end + end + + def check_destroy_permissions + unless can_delete_tag(@project) + render_403 + end + end + + def tag_params + params.require(:tag).permit(:name, :color, :project_id) + end +end diff --git a/app/controllers/user_my_modules_controller.rb b/app/controllers/user_my_modules_controller.rb new file mode 100644 index 000000000..f050f227a --- /dev/null +++ b/app/controllers/user_my_modules_controller.rb @@ -0,0 +1,206 @@ +class UserMyModulesController < ApplicationController + before_action :load_vars + before_action :check_view_permissions, only: [ :index ] + before_action :check_edit_permissions, only: [ :index_edit ] + before_action :check_create_permissions, only: [:new, :create] + before_action :check_delete_permisisons, only: [:destroy] + + def index + @user_my_modules = @my_module.user_my_modules + + respond_to do |format| + format.json { + render :json => { + :html => render_to_string({ + :partial => "index.html.erb" + }) + } + } + end + end + + def index_edit + @user_my_modules = @my_module.user_my_modules + @unassigned_users = @my_module.unassigned_users + @new_um = UserMyModule.new(my_module: @my_module) + + respond_to do |format| + format.json { + render :json => { + :my_module => @my_module, + :html => render_to_string({ + :partial => "index_edit.html.erb" + }) + } + } + end + end + + def new + @um = UserMyModule.new( + my_module: @my_module + ) + init_gui + session[:return_to] ||= request.referer + end + + def create + @um = UserMyModule.new(um_params.merge(my_module: @my_module)) + @um.assigned_by = current_user + if @um.save + flash_success = t( + "user_my_modules.create.success_flash", + user: @um.user.full_name, + module: @um.my_module.name) + + # Create activity + message = t( + "activities.assign_user_to_module", + assigned_user: @um.user.full_name, + module: @my_module.name, + assigned_by_user: current_user.full_name + ) + Activity.create( + user: current_user, + project: @um.my_module.project, + my_module: @um.my_module, + message: message, + type_of: :assign_user_to_module + ) + respond_to do |format| + format.html { + flash[:success] = flash_success + redirect_to session.delete(:return_to) + } + format.json { + redirect_to :action => :index_edit, :format => :json + } + end + else + flash_error = t("user_my_modules.create.error_flash", + user: @um.user.full_name, + module: @um.my_module.name) + + respond_to do |format| + format.html { + flash[:error] = flash_error + init_gui + render :new + } + format.json { + render :json => { + :errors => [ + flash_error] + } + } + end + end + end + + def destroy + session[:return_to] ||= request.referer + + if @um.destroy + flash_success = t( + "user_my_modules.destroy.success_flash", + user: @um.user.full_name, + module: @um.my_module.name) + + # Create activity + message = t( + "activities.unassign_user_from_module", + unassigned_user: @um.user.full_name, + module: @my_module.name, + unassigned_by_user: current_user.full_name + ) + + Activity.create( + user: current_user, + project: @um.my_module.project, + my_module: @um.my_module, + message: message, + type_of: :unassign_user_from_module + ) + + respond_to do |format| + format.html { + flash[:success] = flash_success + redirect_to session.delete(:return_to), :status => 303 + } + format.json { + redirect_to my_module_users_edit_path(format: :json), :status => 303 + } + end + else + flash_error = t("user_my_modules.destroy.error_flash", + user: @um.user.full_name, + module: @um.my_module.name) + + respond_to do |format| + format.html { + flash[:error] = flash_error + init_gui + render :new + } + format.json { + render :json => { + :errors => [ + flash_error + ] + } + } + end + end + end + + private + + def load_vars + @my_module = MyModule.find_by_id(params[:my_module_id]) + + if @my_module + @project = @my_module.project + else + render_404 + end + + if action_name == "destroy" + @um = UserMyModule.find_by_id(params[:id]) + unless @um + render_404 + end + end + end + + def check_view_permissions + unless can_view_module_users(@my_module) + render_403 + end + end + + def check_edit_permissions + unless can_edit_users_on_module(@my_module) + render_403 + end + end + + def check_create_permissions + unless can_add_user_to_module(@my_module) + render_403 + end + end + + def check_delete_permisisons + unless can_remove_user_from_module(@my_module) + render_403 + end + end + + def init_gui + @users = @my_module.unassigned_users + end + + def um_params + params.require(:user_my_module).permit(:user_id, :my_module_id) + end +end diff --git a/app/controllers/user_projects_controller.rb b/app/controllers/user_projects_controller.rb new file mode 100644 index 000000000..7f9c2174e --- /dev/null +++ b/app/controllers/user_projects_controller.rb @@ -0,0 +1,282 @@ +class UserProjectsController < ApplicationController + before_action :load_vars + before_action :check_view_tab_permissions, only: [ :index ] + before_action :check_view_permissions, only: [ :index_edit ] + before_action :check_create_permissions, only: [:new, :create] + # TODO check update permissions + before_action :check_update_permisisons, only: [:update] + before_action :check_delete_permisisons, only: [:destroy] + + def index + @users = @project.user_projects + + respond_to do |format| + #format.html + format.json { + render :json => { + :html => render_to_string({ + :partial => "index.html.erb" + }) + } + } + end + end + + def index_edit + @users = @project.user_projects + @unassigned_users = @project.unassigned_users + @up = UserProject.new(project: @project) + + respond_to do |format| + format.json { + render :json => { + :project => @project, + :html => render_to_string({ + :partial => "index_edit.html.erb" + }) + } + } + end + end + + def new + @up = UserProject.new( + project: @project + ) + init_gui + end + + def create + @up = UserProject.new(up_params.merge(project: @project)) + @up.assigned_by = current_user + + if @up.save + flash_success = t('user_projects.create.success_flash', + user: @up.user.full_name, + project: @up.project.name) + + # Generate activity + Activity.create( + type_of: :assign_user_to_project, + user: current_user, + project: @project, + message: t( + "activities.assign_user_to_project", + assigned_user: @up.user.full_name, + role: @up.role_str, + project: @project.name, + assigned_by_user: current_user.full_name + ) + ) + + respond_to do |format| + format.html { + flash[:success] = flash_success + redirect_to projects_path + } + format.json { + redirect_to :action => :index_edit, :format => :json + } + end + else + flash_error = t('user_projects.create.error_flash', + user: @up.user.full_name, + project: @up.project.name) + error = t('user_projects.create.can_add_user_to_project') + error = t('user_projects.create.select_user_role') unless @up.role + + respond_to do |format| + format.html { + flash[:error] = flash_error + init_gui + render :new + } + format.json { + render :json => { + status: 'error', + error: error, + :errors => [ + flash_error + ] + } + } + end + end + end + + def edit + @up = UserProject.find(params[:id]) + end + + def update + @up = UserProject.find(params[:id]) + + unless @up + render_404 + end + + @up.role = up_params[:role] + + if @up.save + flash_success = t( + "user_projects.update.success_flash", + user: @up.user.full_name, + project: @up.project.name) + + # Generate activity + Activity.create( + type_of: :change_user_role_on_project, + user: current_user, + project: @project, + message: t( + "activities.change_user_role_on_project", + actor: current_user.full_name, + user: @up.user.full_name, + project: @project.name, + role: @up.role_str + ) + ) + + respond_to do |format| + format.html { + flash[:success] = flash_success + redirect_to projects_path + } + format.json { + redirect_to :action => :index_edit, :format => :json + } + end + else + flash_error = t('user_projects.update.error_flash', + user: @up.user.full_name, + project: @up.project.name) + + respond_to do |format| + format.html { + flash[:error] = flash_error + init_gui + render :new + } + format.json { + render :json => { + status: 'error', + :errors => [ + flash_error + ] + } + } + end + end + end + + def destroy + if @up.destroy + flash_success = t( + 'user_projects.destroy.success_flash', + user: @up.user.full_name, + project: @up.project.name) + + # Generate activity + Activity.create( + type_of: :unassign_user_from_project, + user: current_user, + project: @project, + message: t( + "activities.unassign_user_from_project", + unassigned_user: @up.user.full_name, + project: @project.name, + unassigned_by_user: current_user.full_name + ) + ) + + respond_to do |format| + format.html { + flash[:success] = flash_success + redirect_to projects_path, :status => 303 + } + format.json { + redirect_to project_users_edit_path(format: :json), :status => 303 + } + end + else + flash_error = t('user_projects.destroy.error_flash', + user: @up.user.full_name, + project: @up.project.name) + + respond_to do |format| + format.html { + flash[:error] = flash_error + init_gui + # TODO handle response for html format in case of error + render :new + } + format.json { + render :json => { + :errors => [ + flash_error + ] + } + } + end + end + end + + private + + def load_vars + @project = Project.find_by_id(params[:project_id]) + unless @project + render_404 + end + + if action_name == "destroy" + @up = UserProject.find(params[:id]) + unless @up + render_404 + end + end + end + + def check_view_tab_permissions + unless can_view_project_users(@project) + render_403 + end + end + + def check_view_permissions + unless can_edit_users_on_project(@project) + render_403 + end + end + + def check_create_permissions + unless can_add_user_to_project(@project) + render_403 + end + end + + def check_update_permisisons + # TODO improve permissions for changing your role on project + unless params[:id] != current_user.id + render_403 + end + end + + def check_delete_permisisons + # TODO improve permissions for remove yourself from project + unless params[:id] != current_user.id + render_403 + end + unless can_remove_user_from_project(@project) + render_403 + end + end + + def init_gui + @users = @project.unassigned_users + end + + def up_params + params.require(:user_project).permit(:user_id, :project_id, :role) + end +end diff --git a/app/controllers/users/confirmations_controller.rb b/app/controllers/users/confirmations_controller.rb new file mode 100644 index 000000000..5358353f8 --- /dev/null +++ b/app/controllers/users/confirmations_controller.rb @@ -0,0 +1,28 @@ +class Users::ConfirmationsController < Devise::ConfirmationsController + # GET /resource/confirmation/new + # def new + # super + # end + + # POST /resource/confirmation + # def create + # super + # end + + # GET /resource/confirmation?confirmation_token=abcdef + # def show + # super + # end + + protected + + # The path used after resending confirmation instructions. + def after_resending_confirmation_instructions_path_for(resource_name) + new_user_session_path + end + + # The path used after confirmation. + def after_confirmation_path_for(resource_name, resource) + new_user_session_path + end +end diff --git a/app/controllers/users/invitations_controller.rb b/app/controllers/users/invitations_controller.rb new file mode 100644 index 000000000..ff1e7b417 --- /dev/null +++ b/app/controllers/users/invitations_controller.rb @@ -0,0 +1,30 @@ +class Users::InvitationsController < Devise::InvitationsController + + def update + @org = Organization.new + @org.name = params[:organization][:name] + + super do |user| + if user.errors.empty? + @org.created_by = user + @org.save + + UserOrganization.create( + user: user, + organization: @org, + role: 'admin' + ) + end + end + end + + def accept_resource + resource = super + + if not @org.valid? + resource.errors.add(:base, @org.errors.to_a.first) + end + + resource + end +end diff --git a/app/controllers/users/omniauth_callbacks_controller.rb b/app/controllers/users/omniauth_callbacks_controller.rb new file mode 100644 index 000000000..1907e5b1b --- /dev/null +++ b/app/controllers/users/omniauth_callbacks_controller.rb @@ -0,0 +1,28 @@ +class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController + # You should configure your model like this: + # devise :omniauthable, omniauth_providers: [:twitter] + + # You should also create an action method in this controller like this: + # def twitter + # end + + # More info at: + # https://github.com/plataformatec/devise#omniauth + + # GET|POST /resource/auth/twitter + # def passthru + # super + # end + + # GET|POST /users/auth/twitter/callback + # def failure + # super + # end + + # protected + + # The path used when OmniAuth fails + # def after_omniauth_failure_path_for(scope) + # super(scope) + # end +end diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb new file mode 100644 index 000000000..53cc34e39 --- /dev/null +++ b/app/controllers/users/passwords_controller.rb @@ -0,0 +1,32 @@ +class Users::PasswordsController < Devise::PasswordsController + # GET /resource/password/new + # def new + # super + # end + + # POST /resource/password + # def create + # super + # end + + # GET /resource/password/edit?reset_password_token=abcdef + # def edit + # super + # end + + # PUT /resource/password + # def update + # super + # end + + # protected + + # def after_resetting_password_path_for(resource) + # super(resource) + # end + + # The path used after sending reset password instructions + # def after_sending_reset_password_instructions_path_for(resource_name) + # super(resource_name) + # end +end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb new file mode 100644 index 000000000..399c36ef3 --- /dev/null +++ b/app/controllers/users/registrations_controller.rb @@ -0,0 +1,239 @@ +class Users::RegistrationsController < Devise::RegistrationsController + + before_action :load_paperclip_vars + + def avatar + style = params[:style] || "icon_small" + redirect_to current_user.avatar.url(style.to_sym), status: 307 + end + + def signature + respond_to do |format| + format.json { + + # Changed avatar values are only used for pre-generating S3 key + # and user object is not persisted with this values. + current_user.empty_avatar params[:file_name], params[:file_size] + + unless current_user.valid? + render json: { + status: 'error', + errors: current_user.errors + } + else + render json: { + posts: generate_upload_posts + } + end + } + end + end + + def update_resource(resource, params) + @user_avatar_url = avatar_path(:thumb) + + if @direct_upload + if params.include? :avatar_file_name + file_name = params[:avatar_file_name] + file_ext = file_name.split(".").last + params[:avatar_content_type] = Rack::Mime.mime_type(".#{file_ext}") + resource.avatar.destroy + end + end + + if params.include? :change_password + # Special handling if changing password + params.delete(:change_password) + if ( + resource.valid_password?(params[:current_password]) and + params.include? :password and + params.include? :password_confirmation and + params[:password].blank? + ) then + # If new password is blank and we're in process of changing + # password, add error to the resource and return false + resource.errors.add(:password, :blank) + false + else + resource.update_with_password(params) + end + elsif params.include? :change_avatar + params.delete(:change_avatar) + unless params.include? :avatar + resource.errors.add(:avatar, :blank) + false + else + resource.update_without_password(params) + end + elsif params.include? :email or params.include? :password + # For changing email or password, validate current_password + resource.update_with_password(params) + + else + # For changing some attributes, no current_password validation + # is required + resource.update_without_password(params) + end + end + + # Override default registrations_controller.rb implementation + # to support JSON + def update + change_password = account_update_params.include? :change_password + respond_to do |format| + self.resource = resource_class.to_adapter.get!(send(:"current_#{resource_name}").to_key) + prev_unconfirmed_email = resource.unconfirmed_email if resource.respond_to?(:unconfirmed_email) + + resource_updated = update_resource(resource, account_update_params) + yield resource if block_given? + if resource_updated + # Set "needs confirmation" flash if neccesary + if is_flashing_format? + flash_key = update_needs_confirmation?(resource, prev_unconfirmed_email) ? + :update_needs_confirmation : :updated + set_flash_message :notice, flash_key + end + + # Set "password successfully updated" flash if neccesary + if change_password + set_flash_message :notice, :password_changed + end + + format.html { + sign_in resource_name, resource, bypass: true + respond_with resource, location: edit_user_registration_path + } + format.json { + flash.keep + sign_in resource_name, resource, bypass: true + render json: { status: :ok } + } + else + clean_up_passwords resource + format.html { + respond_with resource, location: edit_user_registration_path + } + format.json { + render json: self.resource.errors, + status: :unprocessable_entity + } + end + end + end + + def create + + # Create new organization for the new user + @org = Organization.new + @org.name = params[:organization][:name] + + build_resource(sign_up_params) + + valid_org = @org.valid? + valid_resource = resource.valid? + + if valid_org and valid_resource + + # this must be called after @org variable is defined. Otherwise this + # variable won't be accessable in view. + super do |resource| + + if resource.valid? and resource.persisted? + @org.created_by = resource #set created_by for oraganization + @org.save + + # Add this user to the organization as owner + UserOrganization.create( + user: resource, + organization: @org, + role: :admin + ) + end + end + + else + render :new + end + end + + protected + + def load_paperclip_vars + @direct_upload = ENV['PAPERCLIP_DIRECT_UPLOAD'] + end + + # Called upon creating User (before .save). Permits parameters and extracts + # initials from :full_name (takes at most 4 chars). If :full_name is empty, it + # uses "PLCH" as a placeholder (user won't get error complaining about + # initials being empty. + def sign_up_params + tmp = params.require(:user).permit(:full_name, :initials, :email, :password, :password_confirmation) + initials = tmp[:full_name].titleize.scan(/[A-Z]+/).join() + initials = initials.strip.empty? ? "PLCH" : initials[0..3] + tmp.merge(:initials => initials) + end + + def account_update_params + params.require(:user).permit( + :full_name, + :initials, + :avatar, + :avatar_file_name, + :email, + :password, + :password_confirmation, + :current_password, + :change_password, + :change_avatar + ) + end + + def generate_upload_posts + posts = [] + file_size = current_user.avatar_file_size + content_type = current_user.avatar_content_type + s3_post = S3_BUCKET.presigned_post( + key: current_user.avatar.path[1..-1], + success_action_status: '201', + acl: 'private', + storage_class: "STANDARD", + content_length_range: file_size..file_size, + content_type: content_type + ) + posts.push({ + url: s3_post.url, + fields: s3_post.fields + }) + + current_user.avatar.options[:styles].each do |style, option| + s3_post = S3_BUCKET.presigned_post( + key: current_user.avatar.path(style)[1..-1], + success_action_status: '201', + acl: 'public-read', + storage_class: "REDUCED_REDUNDANCY", + content_length_range: 1..(1024*1024*50), + content_type: content_type + ) + posts.push({ + url: s3_post.url, + fields: s3_post.fields, + style_option: option, + mime_type: content_type + }) + end + + posts + end + + private + + # Redirect to login page after signing up + def after_sign_up_path_for(resource) + new_user_session_path + end + + # Redirect to login page after signing up + def after_inactive_sign_up_path_for(resource) + new_user_session_path + end +end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb new file mode 100644 index 000000000..a9bda1e3f --- /dev/null +++ b/app/controllers/users/sessions_controller.rb @@ -0,0 +1,26 @@ +class Users::SessionsController < Devise::SessionsController +# before_filter :configure_sign_in_params, only: [:create] + + # GET /resource/sign_in + # def new + # super + # end + + # POST /resource/sign_in + # def create + # super + # end + + # DELETE /resource/sign_out + # def destroy + # super + # end + + protected + + # If you have extra params to permit, append them to the sanitizer. + def configure_sign_in_params + devise_parameter_sanitizer.for(:sign_in) << :attribute + end + +end diff --git a/app/controllers/users/settings_controller.rb b/app/controllers/users/settings_controller.rb new file mode 100644 index 000000000..4d8cd2a84 --- /dev/null +++ b/app/controllers/users/settings_controller.rb @@ -0,0 +1,443 @@ +class Users::SettingsController < ApplicationController + include UsersGenerator + + before_action :load_user, only: [ + :preferences, + :update_preferences, + :organizations, + :organization, + :create_organization, + :organization_users_datatable + ] + + before_action :check_organization_permission, only: [ + :organization, + :update_organization, + :destroy_organization, + :organization_name, + :organization_description, + :search_organization_users, + :organization_users_datatable, + :create_user_and_user_organization + ] + + before_action :check_create_user_organization_permission, only: [ + :create_user_organization + ] + + before_action :check_user_organization_permission, only: [ + :update_user_organization, + :leave_user_organization_html, + :destroy_user_organization_html, + :destroy_user_organization + ] + + def preferences + end + + def update_preferences + respond_to do |format| + if @user.update(update_preferences_params) + flash[:notice] = t("users.settings.preferences.update_flash") + format.json { + flash.keep + render json: { status: :ok } + } + else + format.json { + render json: @user.errors, + status: :unprocessable_entity + } + end + end + end + + def organizations + @user_orgs = + @user + .user_organizations + .includes(organization: :users) + .order(created_at: :asc) + @member_of = @user_orgs.count + end + + def organization + @user_org = UserOrganization.find_by(user: @user, organization: @org) + end + + def update_organization + respond_to do |format| + if @org.update(update_organization_params) + @org.update(last_modified_by: current_user) + format.json { + render json: { + status: :ok, + description_label: render_to_string( + partial: "users/settings/organizations/description_label.html.erb", + locals: { org: @org } + ) + } + } + else + format.json { + render json: @org.errors, + status: :unprocessable_entity + } + end + end + end + + def organization_name + respond_to do |format| + format.json { + render json: { + html: render_to_string({ + partial: "users/settings/organizations/name_modal_body.html.erb", + locals: { org: @org } + }) + } + } + end + end + + def organization_description + respond_to do |format| + format.json { + render json: { + html: render_to_string({ + partial: "users/settings/organizations/description_modal_body.html.erb", + locals: { org: @org } + }) + } + } + end + end + + def search_organization_users + respond_to do |format| + format.json { + if params.include? :existing_query and + (query = params[:existing_query].strip()).present? + if query.length < 3 + render json: { + "existing_query": [ + I18n.t("users.settings.organizations.edit.modal_add_user.existing_query_too_short") + ]}, + status: :unprocessable_entity + else + # Okay, query exists and is non-blank, find users + nr_of_results = User.search(true, query, @org).count + users = User.search(true, query, @org).limit(5) + + render json: { + html: render_to_string({ + partial: "users/settings/organizations/existing_users_search_results.html.erb", + locals: { + users: users, + nr_of_results: nr_of_results, + org: @org, + query: query + } + }) + } + end + else + render json: { + "existing_query": [ + I18n.t("users.settings.organizations.edit.modal_add_user.existing_query_blank") + ]}, + status: :unprocessable_entity + end + } + end + end + + def organization_users_datatable + respond_to do |format| + format.json { + render json: ::OrganizationUsersDatatable.new(view_context, @org, @user) + } + end + end + + def new_organization + @new_org = Organization.new + end + + def create_organization + @new_org = Organization.new(create_organization_params) + @new_org.created_by = @user + + if @new_org.save + # Okay, organization is created, now + # add the current user as admin + UserOrganization.create( + user: @user, + organization: @new_org, + role: 2 + ) + + # Redirect to new organization page + redirect_to action: :organization, organization_id: @new_org.id + else + render :new_organization + end + end + + def destroy_organization + @org.destroy + + flash[:notice] = I18n.t( + "users.settings.organizations.edit.modal_destroy_organization.flash_success", + org: @org.name + ) + + # Redirect back to all organizations page + redirect_to action: :organizations + end + + def create_user_organization + @new_user_org = UserOrganization.new(create_user_organization_params) + + if @new_user_org.save + flash[:notice] = I18n.t( + "users.settings.organizations.edit.modal_add_user.existing_flash_success", + user: @new_user_org.user.full_name, + role: @new_user_org.role_str + ) + else + flash[:alert] = + I18n.t("users.settings.organizations.edit.modal_add_user.existing_flash_error") + end + + # Either way, redirect back to organization page + redirect_to action: :organization, + organization_id: @new_user_org.organization_id + end + + def create_user_and_user_organization + respond_to do |format| + # User & organization + # parameters are already taken care of, + # so only role needs to be verified + if !params.include? :role or + !UserOrganization.roles.keys.include? params[:role] + format.json { + render json: "Invalid role provided", + status: :unprocessable_entity + } + else + password = generate_user_password + user_params = create_user_params + full_name = user_params[:full_name] + email = user_params[:email] + + # Validate the user data + errors = validate_user(full_name, email, password) + + if errors.count == 0 + @user = User.invite!( + full_name: full_name, + email: email, + initials: full_name.split(" ").map{|w| w[0].upcase}.join[0..3], + skip_invitation: true + ) + + # Sending email invitation is done in background job to prevent + # issues with email delivery. Also invite method must be call + # with :skip_invitation attribute set to true - see above. + @user.delay.deliver_invitation + + # Also generate user organization relation + @user_org = UserOrganization.new( + user: @user, + organization: @org, + role: params[:role] + ) + @user_org.save + + # Flash message + flash[:notice] = t( + "users.settings.organizations.edit.modal_add_user.new_flash_success", + user: @user.full_name, + role: @user_org.role_str, + email: @user.email + ) + flash.keep + + # Return success! + format.json { + render json: { + status: :ok + } + } + else + format.json { + render json: errors, + status: :unprocessable_entity + } + end + end + end + end + + def update_user_organization + respond_to do |format| + if @user_org.update(update_user_organization_params) + format.json { + render json: { + status: :ok + } + } + else + format.json { + render json: @user_org.errors, + status: :unprocessable_entity + } + end + end + end + + def leave_user_organization_html + respond_to do |format| + format.json { + render json: { + html: render_to_string({ + partial: "users/settings/organizations/leave_user_organization_modal_body.html.erb", + locals: { user_organization: @user_org } + }), + heading: I18n.t( + "users.settings.organizations.index.leave_uo_heading", + org: @user_org.organization.name + ) + } + } + end + end + + def destroy_user_organization_html + respond_to do |format| + format.json { + render json: { + html: render_to_string({ + partial: "users/settings/organizations/destroy_user_organization_modal_body.html.erb", + locals: { user_organization: @user_org } + }), + heading: I18n.t( + "users.settings.organizations.edit.destroy_uo_heading", + user: @user_org.user.full_name, + org: @user_org.organization.name + ) + } + } + end + end + + def destroy_user_organization + respond_to do |format| + # If user is last administrator of organization, + # he/she cannot be deleted from it. + invalid = + @user_org.admin? && + @user_org + .organization + .user_organizations + .where(role: 2) + .count <= 1 + + if !invalid && @user_org.destroy + if params[:leave] then + flash[:notice] = I18n.t( + "users.settings.organizations.index.leave_flash", + org: @user_org.organization.name + ) + flash.keep(:notice) + end + + format.json { + render json: { + status: :ok + } + } + else + format.json { + render json: @user_org.errors, + status: :unprocessable_entity + } + end + end + end + + private + + def load_user + @user = current_user + end + + def check_organization_permission + @org = Organization.find_by_id(params[:organization_id]) + unless is_admin_of_organization(@org) + render_403 + end + end + + def check_create_user_organization_permission + @org = Organization.find_by_id(params[:user_organization][:organization_id]) + unless is_admin_of_organization(@org) + render_403 + end + end + + def check_user_organization_permission + @user_org = UserOrganization.find_by_id(params[:user_organization_id]) + @org = @user_org.organization + # Don't allow the user to modify UserOrganization-s if he's not admin, + # unless he/she is modifying his/her UserOrganization + if current_user != @user_org.user and + !is_admin_of_organization(@user_org.organization) + render_403 + end + end + + def update_preferences_params + params.require(:user).permit( + :time_zone + ) + end + + def create_organization_params + params.require(:organization).permit( + :name, + :description + ) + end + + def update_organization_params + params.require(:organization).permit( + :name, + :description + ) + end + + def create_user_params + params.require(:user).permit( + :full_name, + :email + ) + end + + def create_user_organization_params + params.require(:user_organization).permit( + :user_id, + :organization_id, + :role + ) + end + + def update_user_organization_params + params.require(:user_organization).permit( + :role + ) + end + +end diff --git a/app/controllers/users/unlocks_controller.rb b/app/controllers/users/unlocks_controller.rb new file mode 100644 index 000000000..8b9ef8612 --- /dev/null +++ b/app/controllers/users/unlocks_controller.rb @@ -0,0 +1,28 @@ +class Users::UnlocksController < Devise::UnlocksController + # GET /resource/unlock/new + # def new + # super + # end + + # POST /resource/unlock + # def create + # super + # end + + # GET /resource/unlock?unlock_token=abcdef + # def show + # super + # end + + # protected + + # The path used after sending unlock password instructions + # def after_sending_unlock_instructions_path_for(resource) + # super(resource) + # end + + # The path used after unlocking the resource + # def after_unlock_path_for(resource) + # super(resource) + # end +end diff --git a/app/datatables/organization_users_datatable.rb b/app/datatables/organization_users_datatable.rb new file mode 100644 index 000000000..5bb1a1f31 --- /dev/null +++ b/app/datatables/organization_users_datatable.rb @@ -0,0 +1,70 @@ +class OrganizationUsersDatatable < AjaxDatatablesRails::Base + def_delegator :@view, :link_to + def_delegator :@view, :update_user_organization_path + def_delegator :@view, :destroy_user_organization_html_path + + def initialize(view, org, user) + super(view) + @org = org + @user = user + end + + def sortable_columns + @sortable_columns ||= [ + "User.full_name", + "User.email", + "UserOrganization.created_at", + "User.confirmed_at", + "UserOrganization.role" + ] + end + + def searchable_columns + @searchable_columns ||= [ + "User.full_name", + "User.email", + "UserOrganization.created_at" + ] + end + + private + + # Returns json of current samples (already paginated) + def data + records.map do |record| + { + "DT_RowId": record.id, + "0": record.user.full_name, + "1": record.user.email, + "2": I18n.l(record.created_at, format: :full), + "3": record.user.active_status_str, + "4": record.role_str, + "5": ApplicationController.new.render_to_string( + partial: "users/settings/organizations/user_dropdown.html.erb", + locals: { + user_organization: record, + update_role_path: update_user_organization_path(record, format: :json), + destroy_uo_link: link_to( + I18n.t("users.settings.organizations.edit.user_dropdown.remove_label"), + destroy_user_organization_html_path(record, format: :json), + remote: true, + data: { action: "destroy-user-organization" } + ), + user: @user + } + ) + } + end + end + + # Query database for records (this will be later paginated and filtered) + # after that "data" function will return json + def get_raw_records + UserOrganization + .includes(:user) + .references(:user) + .where(organization: @org) + .distinct + end + +end diff --git a/app/datatables/sample_datatable.rb b/app/datatables/sample_datatable.rb new file mode 100644 index 000000000..22d7b9266 --- /dev/null +++ b/app/datatables/sample_datatable.rb @@ -0,0 +1,231 @@ +class SampleDatatable < AjaxDatatablesRails::Base + include SamplesHelper + + ASSIGNED_SORT_COL = "assigned" + + def initialize(view, organization, project = nil, my_module = nil) + super(view) + @organization = organization + @project = project + @my_module = my_module + end + + # Define sortable columns, so 1st column will be sorted by attribute in sortable_columns[0] + def sortable_columns + sort_array = [ + ASSIGNED_SORT_COL, + "Sample.name", + "SampleType.name", + "SampleGroup.name", + "Sample.created_at", + "User.full_name", + ] + sort_array.push(*custom_fields_sort_by) + + @sortable_columns ||= sort_array + end + + # Define attributes on which we perform search + def searchable_columns + search_array = [ + "Sample.name", + "Sample.created_at", + "SampleType.name", + "SampleGroup.name", + "Sample.created_at", + "User.full_name" + ] + search_array.push(*custom_fields_sort_by) + + @searchable_columns ||= search_array + end + + private + + # Get array of columns to sort by (for custom fields) + def custom_fields_sort_by + num_cf = CustomField.where(organization_id: @organization).count + array = [] + + for _ in 0..num_cf + array << "SampleCustomField.value" + end + array + end + + # Returns json of current samples (already paginated) + def data + records.map do |record| + sample = { + "DT_RowId": record.id, + "1": assigned_cell(record), + "2": record.name, + "3": record.sample_type.nil? ? I18n.t("samples.table.no_type") : record.sample_type.name, + "4": record.sample_group.nil? ? + " " + I18n.t("samples.table.no_group") : + " " + record.sample_group.name, + "5": I18n.l(record.created_at, format: :full), + "6": record.user.full_name, + "sampleInfoUrl": Rails.application.routes.url_helpers.edit_sample_path(record.id), + "sampleUpdateUrl": Rails.application.routes.url_helpers.sample_path(record.id) + } + + # Add custom attributes + record.sample_custom_fields.each do |scf| + sample[@cf_mappings[scf.custom_field_id]] = scf.value + end + sample + end + end + + def assigned_cell(record) + @assigned_samples.include?(record) ? + " " : + " " + end + + # Query database for records (this will be later paginated and filtered) + # after that "data" function will return json + def get_raw_records + samples = Sample + .includes( + :sample_type, + :sample_group, + :user, + :sample_custom_fields + ) + .references( + :sample_type, + :sample_group, + :user, + :sample_custom_fields + ) + .where( + organization: @organization + ) + + if @my_module + @assigned_samples = @my_module.samples + samples = samples + .joins( + "LEFT OUTER JOIN sample_my_modules ON + (samples.id = sample_my_modules.sample_id AND + (sample_my_modules.my_module_id = #{@my_module.id.to_s} OR + sample_my_modules.id IS NULL))" + ) + .references(:sample_my_modules) + elsif @project + @assigned_samples = @project.assigned_samples + ids = @project.my_modules.select(:id) + samples = samples + .joins( + "LEFT OUTER JOIN sample_my_modules ON + (samples.id = sample_my_modules.sample_id AND + (sample_my_modules.my_module_id IN (#{ids.to_sql}) OR + sample_my_modules.id IS NULL))" + ) + .references(:sample_my_modules) + end + + # Make mappings of custom fields, so we have same id for every column + i = 7 + @cf_mappings = {} + all_custom_fields.each do |cf| + @cf_mappings[cf.id] = i.to_s + i += 1 + end + + samples + end + + # Override default behaviour + # Don't filter and paginate records when sorting by custom column - everything + # is done in sort_records method - you might ask why, well if you want the + # number of samples/all samples it's dependant upon sort_record query + def fetch_records + records = get_raw_records + records = sort_records(records) if params[:order].present? + records = filter_records(records) if params[:search].present? && (not (sorting_by_custom_column)) + records = paginate_records(records) if (not (params[:length].present? && params[:length] == '-1')) && (not (sorting_by_custom_column)) + records + end + + # Override default sort method if needed + def sort_records(records) + if params[:order].present? and params[:order].length == 1 + if sort_column(params[:order].values[0]) == ASSIGNED_SORT_COL + # If "assigned" column is sorted + if @my_module then + # Depending on the sort, order nulls first or + # nulls last on sample_my_modules association + records.order("sample_my_modules.id NULLS #{sort_null_direction(params[:order].values[0])}") + elsif @project + # For project, simply sort on the count of assigned modules; + # if sample is assigned to 0 modules, it's not assigned to project; + # if it's assigned to > 0 modules, it's definately assigned to the + # @project since we filtered the samples table to only include + # the ones on this project + records.order("samples.nr_of_modules_assigned_to #{inverse_sort_direction(params[:order].values[0])}") + end + elsif sorting_by_custom_column + # Check if have to filter samples first + if params[:search].present? and params[:search][:value].present? + # Couldn't force ActiveRecord to yield the same query as below because + # Rails apparently forgets to join stuff in subqueries - + # #justrailsthings + conditions = build_conditions_for(params[:search][:value]) + filter_query = 'SELECT "samples"."id" FROM "samples" + LEFT OUTER JOIN "sample_custom_fields" ON "sample_custom_fields"."sample_id" = "samples"."id" + LEFT OUTER JOIN "sample_types" ON "sample_types"."id" = "samples"."sample_type_id" + LEFT OUTER JOIN "sample_groups" ON "sample_groups"."id" = "samples"."sample_group_id" + LEFT OUTER JOIN "users" ON "users"."id" = "samples"."user_id" + WHERE "samples"."organization_id" = ' + @organization.id.to_s + ' AND ' + conditions.to_sql + + records = records.where("samples.id IN (#{filter_query})") + end + + cf_id = all_custom_fields[params[:order].values[0]["column"].to_i - 7].id + dir = sort_direction(params[:order].values[0]) + + # Because samples can have multiple sample custom fields, we first group + # them by samples.id and inside that group we sort them by cf_id. Because + # we sort them ASC, sorted columns will be on top. Distinct then only + # takes the first row and cuts the rest of every group and voila we have + # 1 row for every sample, which are not sorted yet ... + records = records.select("DISTINCT ON (samples.id) *") + .order("samples.id, CASE WHEN sample_custom_fields.custom_field_id = #{cf_id} THEN 1 ELSE 2 END ASC") + + # ... this little gem (pun intended) then takes the records query, sorts it again + # and paginates it. sq.t0_* are determined empirically and are crucial - + # imagine A -> B -> C transitive relation but where A and C are the + # same. Useless right? But not when you acknowledge that find_by_sql + # method does some funky stuff when your query spans multiple queries - + # Sample object might have id from SampleType, name from + # User ... chaos ensues basically. If something changes in db this might + # change. + Sample.find_by_sql("SELECT sq.t0_r0 as id, sq.t0_r1 as name, sq.t0_r4 as created_at, sq.t0_r5, sq.t0_r2 as user_id, sq.custom_field_id FROM (#{records.to_sql}) + as sq ORDER BY CASE WHEN sq.custom_field_id = #{cf_id} THEN 1 ELSE 2 END #{dir}, sq.value #{dir} + LIMIT #{per_page} OFFSET #{offset}") + else + super(records) + end + else + super(records) + end + end + + def sort_null_direction(item) + val = sort_direction(item) + val == "ASC" ? "LAST" : "FIRST" + end + + def inverse_sort_direction(item) + val = sort_direction(item) + val == "ASC" ? "DESC" : "ASC" + end + + def sorting_by_custom_column + params[:order].values[0]["column"].to_i > 6 + end + +end \ No newline at end of file diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb new file mode 100644 index 000000000..b7f0f5300 --- /dev/null +++ b/app/helpers/application_helper.rb @@ -0,0 +1,16 @@ +module ApplicationHelper + + def is_module_page? + controller_name == "my_modules" + end + + def is_project_page? + controller_name == "projects" or + (controller_name == "reports" and action_name == "index") + end + + def is_project_activities_page? + controller_name == "project_activities" + end + +end diff --git a/app/helpers/bootstrap_form_helper.rb b/app/helpers/bootstrap_form_helper.rb new file mode 100644 index 000000000..c6134ddaf --- /dev/null +++ b/app/helpers/bootstrap_form_helper.rb @@ -0,0 +1,241 @@ +module BootstrapFormHelper + + # Extend Bootstrap form builder + class BootstrapForm::FormBuilder + + # Returns Bootstrap date-time picker of the "datetime" type tailored for accessing a specified datetime attribute (identified by +name+) on an object + # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. The supported options are shown in the following examples. + # + # ==== Examples + # Specify custom label (otherwise, a humanized version of +name+ is used) + # # => datetime_picker(:post, :published, label: "Published date") + # + # Specify custom CSS style on the element + # # => datetime_picker(:post, :published, style: "background-color: #000; margin-top: 20px;") + # + # Show a "clear" button on the bottom of the date picker. + # # => datetime_picker(:post, :published, clear: true) + # + # Show a "today" button on the bottom of the date picker. + # # => datetime_picker(:post, :published, today: true) + def datetime_picker(name, options = {}) + id = "#{@object_name}_#{name.to_s}" + input_name = "#{@object_name}[#{name.to_s}]" + timestamp = @object[name] ? "#{@object[name].to_i}000" : "" + js_locale = I18n.locale.to_s + js_format = I18n.t("time.formats.full_js") + + label = name.to_s.humanize + if options[:label] then + label = options[:label] + end + + styleStr = "" + if options[:style] then + styleStr = "style='#{options[:style]}'" + end + + jsOpts = "" + if options[:today] then + jsOpts << "showTodayButton: true, " + end + + res = "" + res << "
    " + if options[:clear] then + res << "
    " + end + res << "" + if options[:clear] then + res << "
    " + end + res << "
    " + res.html_safe + end + + # Returns Bootstrap button group for choosing a specified enum attribute (identified by +name+) on an object + # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a + # hash with +options+. The supported options are shown in the following examples. + # + # ==== Examples + # Specify custom label (otherwise, a humanized version of +name+ is used) + # # => enum_btn_group(:car, :type, label: "Car type") + # + # Specify custom button names for enum values (a hash) + # # => enum_btn_groups(:car, :type, btn_names: { diesel: "Diesel car", electric: "Electric car" }) + # + # Specify custom CSS style on the element + # # => enum_btn_group(:car, :type, style: "background-color: #000; margin-top: 20px;") + # + # Specify custom CSS classes on the element + # # => enum_btn_group(:car, :type, class: "class1 class2") + def enum_btn_group(name, options = {}) + id = "#{@object_name}_#{name.to_s}" + input_name = "#{@object_name}[#{name.to_s}]" + + enum_vals = @object.class.send(name.to_s.pluralize) + btn_names = Hash[enum_vals.keys.collect { |k| [k, k] }] + if options[:btn_names] then + btn_names = options[:btn_names] + end + btn_names = HashWithIndifferentAccess.new(btn_names) + + label = name.to_s.humanize + if options[:label] then + label = options[:label] + end + + style_str = "" + if options[:style] then + style_str = " style='#{options[:style]}'" + end + + class_str = "" + if options[:class] then + class_str = " #{options[:class]}" + end + + res = "" + res << "
    " + res << "" + res << "
    " + res << "
    " + enum_vals.keys.each do |val| + active = @object.send("#{val}?") + active_str = active ? " active" : "" + checked_str = active ? " checked='checked'" : "" + + res << "" + end + res << "
    " + res << "
    " + res.html_safe + end + + # Returns color picker as a dropdown (" + colors.each do |color| + res << "" + end + res << "" + res << "" + res.html_safe + end + + # Returns color picker as a button group of color buttons, tailored for accessing a specified color + # attribute (identified by +name+) on an object assigned to the template (identified by +object+). + # List of colors must be provided (identified by +colors+). Colors must be a list of hashed hex values + # (e.g. '#ff0000'). Additional options on the input tag can be passed as a hash with +options+. + # The supported options are shown in the following examples. + # + # ==== Examples + # Specify custom label (otherwise, a humanized version of +name+ is used) + # # => color_picker_btn_group(:tag, :color, colors: ["#ff0000", "#00ff00"], label: "Choose color") + # + # Specify size (available options: :large, :normal, :small, :extra_small) + # # => color_picker_btn_group(:tag, :color, colors: ["#ff0000", "#00ff00"], size: :small) + # + # Set the picker as vertical + # # => color_picker_btn_group(:tag, :color, colors: ["#ff0000", "#00ff00"], vertical: true) + # + # Specify custom CSS style on the element + # # => color_picker_btn_group(:tag, :color, colors: ["#ff0000", "#00ff00"], style: "background-color: #000; margin-top: 20px;") + # + # Specify custom CSS class on the element + # # => color_picker_btn_group(:tag, :color, colors: ["#ff0000", "#00ff00"], class: "custom") + def color_picker_btn_group(name, colors, options = {}) + id = "#{@object_name}_#{name.to_s}" + input_name = "#{@object_name}[#{name.to_s}]" + + icon_str = '' + icon_str_hidden = '' + + label = name.to_s.humanize + if options[:label] then + label = options[:label] + end + + group_str = "btn-group" + if options[:vertical] and options[:vertical] == true then + group_str << "-vertical" + end + if options[:size] then + if options[:size] == :large + group_str << " btn-group-lg" + elsif options[:size] == :small + group_str << " btn-group-sm" + elsif options[:size] == :extra_small + group_str << " btn-group-xs" + end + end + + style_str = "" + if options[:style] then + style_str = "style='#{options[:style]}'" + end + + class_str = "" + if options[:class] then + class_str = "#{options[:class]}" + end + + res = "" + res << "
    " + res << "" + res << "
    " + colors.each_with_index do |color, i| + active = i == 0 ? " active" : "" + checked = i == 0 ? " checked='checked'" : "" + contents = i == 0 ? icon_str : icon_str_hidden + + res << "" + end + res << "
    " + res << "" + res << "
    " + res.html_safe + end + end +end \ No newline at end of file diff --git a/app/helpers/custom_fields_helper.rb b/app/helpers/custom_fields_helper.rb new file mode 100644 index 000000000..564a41c60 --- /dev/null +++ b/app/helpers/custom_fields_helper.rb @@ -0,0 +1,2 @@ +module CustomFieldsHelper +end diff --git a/app/helpers/database_helper.rb b/app/helpers/database_helper.rb new file mode 100644 index 000000000..358239079 --- /dev/null +++ b/app/helpers/database_helper.rb @@ -0,0 +1,57 @@ +module DatabaseHelper + + # Check if database adapter equals to the specified name + def db_adapter_is?(adapter_name) + ActiveRecord::Base.connection.adapter_name == adapter_name + end + + # Create PostgreSQL extension. PostgreSQL only! + def create_extension(ext_name) + ActiveRecord::Base.connection.execute( + "CREATE EXTENSION #{ext_name};" + ) + end + + # Drop PostgreSQL extension. PostgreSQL only! + def drop_extension(ext_name) + ActiveRecord::Base.connection.execute( + "DROP EXTENSION #{ext_name};" + ) + end + + # Create gist trigram index. PostgreSQL only! + def add_gist_index(table, column) + ActiveRecord::Base.connection.execute( + "CREATE INDEX index_#{table}_on_#{column} ON " + + "#{table} USING gist (#{column} gist_trgm_ops);" + ) + end + + # Get size of whole table & its indexes + # (in bytes). PostgreSQL only! + def get_table_size(table) + ActiveRecord::Base.connection.execute( + "SELECT pg_total_relation_size('#{table}');" + ).getvalue(0, 0).to_i + end + + # Get octet length (in bytes) of given column + # of specified SINGLE ActiveRecord. PostgreSQL only! + def get_octet_length_record(object, column) + get_octet_length( + object.class.to_s.tableize, + column, + object.id + ) + end + + # Get octet length (in bytes) of given column + # in table for specific id. PostgreSQL only! + def get_octet_length(table, column, id) + ActiveRecord::Base.connection.execute( + "SELECT octet_length(cast(t.#{column} as text)) FROM #{table} " + + "AS t WHERE t.id = #{id};" + ).getvalue(0, 0).to_i + end + +end \ No newline at end of file diff --git a/app/helpers/my_modules_helper.rb b/app/helpers/my_modules_helper.rb new file mode 100644 index 000000000..417e91433 --- /dev/null +++ b/app/helpers/my_modules_helper.rb @@ -0,0 +1,30 @@ +module MyModulesHelper + def ordered_step_of(my_module) + my_module.steps.order(:position) + end + + def ordered_checklist_items(checklist) + checklist.checklist_items.order(:position) + end + + def ordered_assets(step) + step.assets.order(:file_updated_at) + end + + def number_of_samples(my_module) + my_module.samples.count + end + + def ordered_result_of(my_module) + my_module.results.where(archived: false).order(created_at: :desc) + end + + def is_steps_page? + action_name == "steps" + end + + def is_results_page? + action_name == "results" + end + +end diff --git a/app/helpers/organizations_helper.rb b/app/helpers/organizations_helper.rb new file mode 100644 index 000000000..24cc9a80e --- /dev/null +++ b/app/helpers/organizations_helper.rb @@ -0,0 +1,2 @@ +module OrganizationsHelper +end diff --git a/app/helpers/permission_helper.rb b/app/helpers/permission_helper.rb new file mode 100644 index 000000000..175803917 --- /dev/null +++ b/app/helpers/permission_helper.rb @@ -0,0 +1,608 @@ +require "aspector" + +module PermissionHelper + + ####################################################### + # SOME REFLECTION MAGIC + ####################################################### + aspector do + # ---- ORGANIZATION ROLES DEFINITIONS ---- + around [ + :is_member_of_organization, + :is_admin_of_organization, + :is_normal_user_of_organization, + :is_normal_user_or_admin_of_organization, + :is_guest_of_organization + ] do |proxy, *args, &block| + if args[0] + @user_organization = current_user.user_organizations.where(organization: args[0]).take + @user_organization ? proxy.call(*args, &block) : false + else + false + end + end + + # ---- PROJECT ROLES DEFINITIONS ---- + around [ + :is_member_of_project, + :is_owner_of_project, + :is_user_of_project, + :is_user_or_higher_of_project, + :is_technician_of_project, + :is_technician_or_higher_of_project, + :is_viewer_of_project + ] do |proxy, *args, &block| + if args[0] + @user_project = current_user.user_projects.where(project: args[0]).take + @user_project ? proxy.call(*args, &block) : false + else + false + end + end + + # ---- Almost everything is disabled for archived projects ---- + around [ + :can_view_project, + :can_view_project_activities, + :can_view_project_users, + :can_view_project_notifications, + :can_view_project_comments, + :can_edit_project, + :can_archive_project, + :can_add_user_to_project, + :can_remove_user_from_project, + :can_edit_users_on_project, + :can_add_comment_to_project, + :can_restore_archived_modules, + :can_view_project_samples, + :can_view_project_archive, + :can_create_new_tag, + :can_edit_tag, + :can_delete_tag, + :can_edit_canvas, + :can_reposition_modules, + :can_edit_connections, + :can_create_modules, + :can_edit_modules, + :can_edit_module_groups, + :can_clone_modules, + :can_archive_modules, + :can_view_reports, + :can_create_new_report, + :can_delete_reports + ] do |proxy, *args, &block| + if args[0] + project = args[0] + project.active? ? proxy.call(*args, &block) : false + else + false + end + end + + # ---- Almost everything is disabled for archived modules ---- + around [ + :can_view_module, + # TODO: Because module restoring is made via updating module attributes, + # (and that action checks if module is editable) this needs to be + # commented out or that functionality will not work any more. + #:can_edit_module, + :can_archive_module, + :can_edit_tags_for_module, + :can_add_tag_to_module, + :can_remove_tag_from_module, + :can_view_module_info, + :can_view_module_users, + :can_edit_users_on_module, + :can_add_user_to_module, + :can_remove_user_from_module, + :can_view_module_activities, + :can_view_module_comments, + :can_add_comment_to_module, + :can_view_module_samples, + :can_view_module_archive, + :can_view_steps_in_module, + :can_create_step_in_module, + :can_edit_step_in_module, + :can_delete_step_in_module, + :can_download_step_assets, + :can_view_step_comments, + :can_add_step_comment_in_module, + :can_complete_step_in_module, + :can_uncomplete_step_in_module, + :can_duplicate_step_in_module, + :can_reorder_step_in_module, + :can_check_checkbox, + :can_uncheck_checkbox, + :can_view_results_in_module, + :can_download_result_assets, + :can_view_result_comments, + :can_add_result_comment_in_module, + :can_create_result_text_in_module, + :can_edit_result_text_in_module, + :can_archive_result_text_in_module, + :can_create_result_table_in_module, + :can_edit_result_table_in_module, + :can_archive_result_table_in_module, + :can_create_result_asset_in_module, + :can_edit_result_asset_in_module, + :can_archive_result_asset_in_module, + :can_add_samples_to_module, + :can_delete_samples_from_module + ] do |proxy, *args, &block| + if args[0] + my_module = args[0] + if my_module.active? and my_module.project.active? + proxy.call(*args, &block) + else + false + end + else + false + end + end + end + + private + + ####################################################### + # ROLES + ####################################################### + # The following code should stay private, and for each + # permission that's needed throughout application, a + # public method should be made. That way, we can have + # all permissions gathered here in one place. + + # ---- ORGANIZATION ROLES ---- + def is_member_of_organization(organization) + # This is already checked by aspector, so just return true + true + end + + def is_admin_of_organization(organization) + @user_organization.admin? + end + + def is_normal_user_of_organization(organization) + @user_organization.normal_user? + end + + def is_normal_user_or_admin_of_organization(organization) + @user_organization.normal_user? or @user_organization.admin? + end + + def is_guest_of_organization(organization) + @user_organization.guest? + end + + # ---- PROJECT ROLES ---- + def is_member_of_project(project) + # This is already checked by aspector, so just return true + true + end + + def is_creator_of_project(project) + project.created_by == current_user + end + + def is_owner_of_project(project) + @user_project.owner? + end + + def is_user_of_project(project) + @user_project.normal_user? + end + + def is_user_or_higher_of_project(project) + @user_project.normal_user? or @user_project.owner? + end + + def is_technician_of_project(project) + @user_project.technician? + end + + def is_technician_or_higher_of_project(project) + @user_project.technician? or + @user_project.normal_user? or + @user_project.owner? + end + + def is_viewer_of_project(project) + @user_project.viewer? + end + + public + + ####################################################### + # PERMISSIONS + ####################################################### + # The following list can be expanded for new permissions, + # and only the following list should be public. Also, + # in a lot of cases, the following methods should be added + # to "is project archived" or "is module archived" checks + # at the beginning of this file (via aspector). + + # ---- PROJECT PERMISSIONS ---- + + def can_view_projects(organization) + is_member_of_organization(organization) + end + + def can_create_project(organization) + is_normal_user_or_admin_of_organization(organization) + end + + # User can view project if he's assigned onto it, or if + # a project is public/visible, and user is a member of that organization + def can_view_project(project) + is_member_of_project(project) or + (project.visible? and is_member_of_organization(project.organization)) + end + + def can_view_project_activities(project) + is_member_of_project(project) + end + + def can_view_project_users(project) + can_view_project(project) + end + + def can_view_project_notifications(project) + can_view_project(project) + end + + def can_view_project_comments(project) + can_view_project(project) + end + + def can_edit_project(project) + is_owner_of_project(project) + end + + def can_archive_project(project) + is_owner_of_project(project) + end + + def can_restore_project(project) + project.archived? and is_owner_of_project(project) + end + + def can_add_user_to_project(project) + is_owner_of_project(project) + end + + def can_remove_user_from_project(project) + is_owner_of_project(project) + end + + def can_edit_users_on_project(project) + is_owner_of_project(project) + end + + def can_add_comment_to_project(project) + is_technician_or_higher_of_project(project) + end + + def can_restore_archived_modules(project) + is_user_or_higher_of_project(project) + end + + def can_view_project_samples(project) + can_view_samples(project.organization) + end + + def can_view_project_archive(project) + is_user_or_higher_of_project(project) + end + + def can_create_new_tag(project) + is_user_or_higher_of_project(project) + end + + def can_edit_tag(project) + is_user_or_higher_of_project(project) + end + + def can_delete_tag(project) + is_user_or_higher_of_project(project) + end + + # ---- WORKFLOW PERMISSIONS ---- + + def can_edit_canvas(project) + is_user_or_higher_of_project(project) + end + + def can_reposition_modules(project) + is_user_or_higher_of_project(project) + end + + def can_edit_connections(project) + is_user_or_higher_of_project(project) + end + + # ---- MODULE PERMISSIONS ---- + + def can_create_modules(project) + is_user_or_higher_of_project(project) + end + + def can_edit_modules(project) + is_user_or_higher_of_project(project) + end + + def can_edit_module_groups(project) + is_user_or_higher_of_project(project) + end + + def can_clone_modules(project) + is_user_or_higher_of_project(project) + end + + def can_archive_modules(project) + is_user_or_higher_of_project(project) + end + + def can_view_module(my_module) + can_view_project(my_module.project) + end + + def can_edit_module(my_module) + is_user_or_higher_of_project(my_module.project) + end + + def can_archive_module(my_module) + is_user_or_higher_of_project(my_module.project) + end + + def can_restore_module(my_module) + my_module.archived? and is_user_or_higher_of_project(my_module.project) + end + + def can_edit_tags_for_module(my_module) + is_user_or_higher_of_project(my_module.project) + end + + def can_add_tag_to_module(my_module) + is_user_or_higher_of_project(my_module.project) + end + + def can_remove_tag_from_module(my_module) + is_user_or_higher_of_project(my_module.project) + end + + def can_view_module_info(my_module) + can_view_project(my_module.project) + end + + def can_view_module_users(my_module) + can_view_project(my_module.project) + end + + def can_edit_users_on_module(my_module) + is_owner_of_project(my_module.project) + end + + def can_add_user_to_module(my_module) + is_owner_of_project(my_module.project) + end + + def can_remove_user_from_module(my_module) + is_owner_of_project(my_module.project) + end + + def can_view_module_activities(my_module) + is_member_of_project(my_module.project) + end + + def can_view_module_comments(my_module) + can_view_project(my_module.project) + end + + def can_add_comment_to_module(my_module) + is_technician_or_higher_of_project(my_module.project) + end + + def can_view_module_samples(my_module) + can_view_module(my_module) and + can_view_samples(my_module.project.organization) + end + + def can_view_module_archive(my_module) + is_user_or_higher_of_project(my_module.project) + end + + # ---- STEPS PERMISSIONS ---- + + def can_view_steps_in_module(my_module) + can_view_module(my_module) + end + + def can_create_step_in_module(my_module) + is_user_or_higher_of_project(my_module.project) + end + + # Could possibly be divided into: + # - edit step name/description + # - adding checklists + # - adding assets + # - adding tables + # but right now we have 1 page to rule them all. + def can_edit_step_in_module(my_module) + is_user_or_higher_of_project(my_module.project) + end + + def can_delete_step_in_module(my_module) + is_owner_of_project(my_module.project) + end + + def can_download_step_assets(my_module) + is_member_of_project(my_module.project) + end + + def can_view_step_comments(my_module) + can_view_project(my_module.project) + end + + def can_add_step_comment_in_module(my_module) + is_technician_or_higher_of_project(my_module.project) + end + + def can_complete_step_in_module(my_module) + is_technician_or_higher_of_project(my_module.project) + end + + def can_uncomplete_step_in_module(my_module) + is_user_or_higher_of_project(my_module.project) + end + + def can_duplicate_step_in_module(my_module) + is_user_or_higher_of_project(my_module.project) + end + + def can_reorder_step_in_module(my_module) + is_user_or_higher_of_project(my_module.project) + end + + def can_check_checkbox(my_module) + is_technician_or_higher_of_project(my_module.project) + end + + def can_uncheck_checkbox(my_module) + is_user_or_higher_of_project(my_module.project) + end + + # ---- RESULTS PERMISSIONS ---- + + def can_view_results_in_module(my_module) + can_view_project(my_module.project) + end + + def can_download_result_assets(my_module) + is_member_of_project(my_module.project) + end + + def can_view_result_comments(my_module) + can_view_project(my_module.project) + end + + def can_add_result_comment_in_module(my_module) + is_technician_or_higher_of_project(my_module.project) + end + + # ---- RESULT TEXT PERMISSIONS ---- + + def can_create_result_text_in_module(my_module) + is_user_or_higher_of_project(my_module.project) + end + + def can_edit_result_text_in_module(my_module) + is_user_or_higher_of_project(my_module.project) + end + + def can_archive_result_text_in_module(my_module) + is_owner_of_project(my_module.project) + end + + # ---- RESULT TABLE PERMISSIONS ---- + + def can_create_result_table_in_module(my_module) + is_user_or_higher_of_project(my_module.project) + end + + def can_edit_result_table_in_module(my_module) + is_user_or_higher_of_project(my_module.project) + end + + def can_archive_result_table_in_module(my_module) + is_owner_of_project(my_module.project) + end + + # ---- RESULT ASSET PERMISSIONS ---- + + def can_create_result_asset_in_module(my_module) + is_user_or_higher_of_project(my_module.project) + end + + def can_edit_result_asset_in_module(my_module) + is_user_or_higher_of_project(my_module.project) + end + + def can_archive_result_asset_in_module(my_module) + is_owner_of_project(my_module.project) + end + + # ---- REPORTS PERMISSIONS ---- + + def can_view_reports(project) + can_view_project(project) + end + + def can_create_new_report(project) + is_technician_or_higher_of_project(project) + end + + def can_delete_reports(project) + is_technician_or_higher_of_project(project) + end + + # ---- SAMPLE PERMISSIONS ---- + + def can_create_samples(organization) + is_normal_user_or_admin_of_organization(organization) + end + + def can_view_samples(organization) + is_member_of_organization(organization) + end + + # Only person who created the sample + # or organization admin can edit it + def can_edit_sample(sample) + is_admin_of_organization(sample.organization) or + sample.user == current_user + end + + # Only person who created sample can delete it + def can_delete_sample(sample) + sample.user == current_user + end + + def can_delete_samples(organization) + is_normal_user_or_admin_of_organization(organization) + end + + def can_add_samples_to_module(my_module) + is_technician_or_higher_of_project(my_module.project) + end + + def can_delete_samples_from_module(my_module) + is_technician_or_higher_of_project(my_module.project) + end + + # ---- SAMPLE TYPES PERMISSIONS ---- + + def can_create_sample_type_in_organization(organization) + is_normal_user_or_admin_of_organization(organization) + end + + def can_edit_sample_type_in_organization(organization) + is_normal_user_or_admin_of_organization(organization) + end + + # ---- SAMPLE GROUPS PERMISSIONS ---- + + def can_create_sample_group_in_organization(organization) + is_normal_user_or_admin_of_organization(organization) + end + + def can_edit_sample_group_in_organization(organization) + is_normal_user_or_admin_of_organization(organization) + end + + # ---- CUSTOM FIELDS PERMISSIONS ---- + + def can_create_custom_field_in_organization(organization) + is_normal_user_or_admin_of_organization(organization) + end + +end diff --git a/app/helpers/project_activities_helper.rb b/app/helpers/project_activities_helper.rb new file mode 100644 index 000000000..ad28ff76b --- /dev/null +++ b/app/helpers/project_activities_helper.rb @@ -0,0 +1,2 @@ +module ProjectActivitiesHelper +end diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb new file mode 100644 index 000000000..5edf289da --- /dev/null +++ b/app/helpers/projects_helper.rb @@ -0,0 +1,14 @@ +module ProjectsHelper + + def user_project_role_to_s(user_project) + t("user_projects.enums.role." + user_project.role) + end + + def construct_module_connections(my_module) + conns = Array.new + my_module.outputs.each do |output| + conns.push(output.to.id) + end + conns.to_s[1..-2] + end +end diff --git a/app/helpers/reports_helper.rb b/app/helpers/reports_helper.rb new file mode 100644 index 000000000..6453094ba --- /dev/null +++ b/app/helpers/reports_helper.rb @@ -0,0 +1,75 @@ +module ReportsHelper + +def render_new_element(hide) + render partial: "reports/elements/new_element.html.erb", + locals: { hide: hide } +end + +def render_report_element(element) + children_html = "".html_safe + + # First, recursively render element's children + if element.comments? or element.project_header? + # Render no children + elsif element.result? + # Special handling for result comments + if element.has_children? + children_html.safe_concat render_new_element(true) + element.children.each do |child| + children_html.safe_concat render_report_element(child) + end + else + children_html.safe_concat render_new_element(false) + end + else + if element.has_children? + element.children.each do |child| + children_html.safe_concat render_new_element(false) + children_html.safe_concat render_report_element(child) + end + end + children_html.safe_concat render_new_element(false) + end + + view = "reports/elements/#{element.type_of}_element.html.erb" + locals = { children: children_html } + if element.project_header? + locals[:project] = element.element_reference + elsif element.my_module? + locals[:my_module] = element.element_reference + elsif element.step? + locals[:step] = element.element_reference + elsif element.result_asset? + locals[:result] = element.element_reference + elsif element.result_table? + locals[:result] = element.element_reference + elsif element.result_text? + locals[:result] = element.element_reference + elsif element.my_module_activity? + locals[:my_module] = element.element_reference + locals[:order] = element.sort_order + elsif element.my_module_samples? + locals[:my_module] = element.element_reference + locals[:order] = element.sort_order + elsif element.step_checklist? + locals[:checklist] = element.element_reference + elsif element.step_asset? + locals[:asset] = element.element_reference + elsif element.step_table? + locals[:table] = element.element_reference + elsif element.step_comments? + locals[:step] = element.element_reference + locals[:order] = element.sort_order + elsif element.result_comments? + locals[:result] = element.element_reference + locals[:order] = element.sort_order + elsif element.project_activity? + # TODO + elsif element.project_samples? + # TODO + end + + return (render partial: view, locals: locals).html_safe +end + +end diff --git a/app/helpers/result_assets_helper.rb b/app/helpers/result_assets_helper.rb new file mode 100644 index 000000000..764b18a2f --- /dev/null +++ b/app/helpers/result_assets_helper.rb @@ -0,0 +1,2 @@ +module ResultAssetsHelper +end diff --git a/app/helpers/result_comments_helper.rb b/app/helpers/result_comments_helper.rb new file mode 100644 index 000000000..be6c63b9c --- /dev/null +++ b/app/helpers/result_comments_helper.rb @@ -0,0 +1,2 @@ +module ResultCommentsHelper +end diff --git a/app/helpers/result_tables_helper.rb b/app/helpers/result_tables_helper.rb new file mode 100644 index 000000000..afd9a0a92 --- /dev/null +++ b/app/helpers/result_tables_helper.rb @@ -0,0 +1,2 @@ +module ResultTablesHelper +end diff --git a/app/helpers/result_texts_helper.rb b/app/helpers/result_texts_helper.rb new file mode 100644 index 000000000..3eac04d7a --- /dev/null +++ b/app/helpers/result_texts_helper.rb @@ -0,0 +1,2 @@ +module ResultTextsHelper +end diff --git a/app/helpers/results_helper.rb b/app/helpers/results_helper.rb new file mode 100644 index 000000000..9a718ae4c --- /dev/null +++ b/app/helpers/results_helper.rb @@ -0,0 +1,61 @@ +module ResultsHelper + def published_text_for_result(result) + if result.is_text + t("my_modules.results.published_text", timestamp: l(result.created_at, format: :full)) + elsif result.is_table + t("my_modules.results.published_table", timestamp: l(result.created_at, format: :full)) + elsif result.is_asset + t("my_modules.results.published_asset", timestamp: l(result.created_at, format: :full)) + end + end + + def edit_result_link(result) + if result.is_text + edit_result_text_path(result.result_text, format: :json) + elsif result.is_table + edit_result_table_path(result.result_table, format: :json) + elsif result.is_asset + edit_result_asset_path(result.result_asset, format: :json) + end + end + + def can_edit_result(result) + if result.is_text + can_edit_result_text_in_module(result.my_module) + elsif result.is_table + can_edit_result_table_in_module(result.my_module) + elsif result.is_asset + can_edit_result_asset_in_module(result.my_module) + end + end + + def can_archive_result(result) + if result.is_text + can_archive_result_text_in_module(result.my_module) + elsif result.is_table + can_archive_result_table_in_module(result.my_module) + elsif result.is_asset + can_archive_result_asset_in_module(result.my_module) + end + end + + def result_path_of_type(result) + if result.is_asset + result_asset_path(result.result_asset) + elsif result.is_text + result_text_path(result.result_text) + elsif result.is_table + result_table_path(result.result_table) + end + end + + def edit_result_button_class(result) + if result.is_asset + "edit-result-asset" + elsif result.is_text + "edit-result-text" + elsif result.is_table + "edit-result-table" + end + end +end diff --git a/app/helpers/sample_groups_helper.rb b/app/helpers/sample_groups_helper.rb new file mode 100644 index 000000000..98cd4b710 --- /dev/null +++ b/app/helpers/sample_groups_helper.rb @@ -0,0 +1,2 @@ +module SampleGroupsHelper +end diff --git a/app/helpers/sample_types_helper.rb b/app/helpers/sample_types_helper.rb new file mode 100644 index 000000000..630ecb7a9 --- /dev/null +++ b/app/helpers/sample_types_helper.rb @@ -0,0 +1,2 @@ +module SampleTypesHelper +end diff --git a/app/helpers/samples_helper.rb b/app/helpers/samples_helper.rb new file mode 100644 index 000000000..30e041437 --- /dev/null +++ b/app/helpers/samples_helper.rb @@ -0,0 +1,37 @@ +module SamplesHelper + + def can_add_samples + is_module_page? and can_add_samples_to_module(@my_module) + end + + def can_remove_samples + is_module_page? and can_delete_samples_from_module(@my_module) + end + + def can_add_sample_related_things_to_organization + can_create_custom_field_in_organization(@organization) \ + or can_create_sample_type_in_organization(@organization) \ + or can_create_sample_group_in_organization(@organization) + end + + def all_custom_fields + CustomField.where(organization_id: @organization).order(:created_at) + end + + def num_of_columns + # Magic numbers, woohoo: + # - 1 for checkbox column, + # - 6 corresponds to initial number of basic sample columns (without + # custom) + 1 + 6 + all_custom_fields.count + end + + def form_submit_link + if is_module_page? + assign_samples_my_module_path(@my_module) + elsif is_project_page? + delete_samples_project_path(@project) + end + end + +end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb new file mode 100644 index 000000000..b3ce20acb --- /dev/null +++ b/app/helpers/search_helper.rb @@ -0,0 +1,2 @@ +module SearchHelper +end diff --git a/app/helpers/secondary_navigation_helper.rb b/app/helpers/secondary_navigation_helper.rb new file mode 100644 index 000000000..170d992c3 --- /dev/null +++ b/app/helpers/secondary_navigation_helper.rb @@ -0,0 +1,50 @@ +module SecondaryNavigationHelper + + def is_project_info? + action_name == "show" + end + + def is_project_canvas? + action_name == "canvas" + end + + def is_project_samples? + action_name == "samples" + end + + def is_project_activities? + controller_name == "project_activities" and action_name == "index" + end + + def is_project_reports? + controller_name == "reports" and action_name == "index" + end + + def is_project_archive? + action_name == "module_archive" + end + + def is_module_info? + action_name == "show" + end + + def is_module_steps? + action_name == "steps" + end + + def is_module_results? + action_name == "results" + end + + def is_module_activities? + action_name == "activities" + end + + def is_module_samples? + action_name == "samples" + end + + def is_module_archive? + action_name == "archive" + end +end diff --git a/app/helpers/sidebar_helper.rb b/app/helpers/sidebar_helper.rb new file mode 100644 index 000000000..0e55c5db5 --- /dev/null +++ b/app/helpers/sidebar_helper.rb @@ -0,0 +1,36 @@ +module SidebarHelper + + def currently_active?(my_module) + @my_module.present? and @my_module.id == my_module.id + end + + def is_canvas? + action_name == "canvas" + end + + def project_action_to_link_to(project) + case action_name + when "samples" + return samples_project_path(project) + when "archive" + return module_archive_project_url(project) + else + return canvas_project_path(project) + end + end + + def module_action_to_link_to(my_module) + case action_name + when "results" + return results_my_module_url(my_module) + when "activities" + return activities_my_module_url(my_module) + when "samples" + return samples_my_module_url(my_module) + when "archive", "module_archive" + return archive_my_module_url(my_module) + else + return my_module_steps_path(my_module) + end + end +end diff --git a/app/helpers/step_comments_helper.rb b/app/helpers/step_comments_helper.rb new file mode 100644 index 000000000..d3373a6f7 --- /dev/null +++ b/app/helpers/step_comments_helper.rb @@ -0,0 +1,2 @@ +module StepCommentsHelper +end diff --git a/app/helpers/steps_helper.rb b/app/helpers/steps_helper.rb new file mode 100644 index 000000000..3d39e1bc7 --- /dev/null +++ b/app/helpers/steps_helper.rb @@ -0,0 +1,2 @@ +module StepsHelper +end diff --git a/app/helpers/user_my_modules_helper.rb b/app/helpers/user_my_modules_helper.rb new file mode 100644 index 000000000..1f744cdbd --- /dev/null +++ b/app/helpers/user_my_modules_helper.rb @@ -0,0 +1,2 @@ +module UserMyModulesHelper +end diff --git a/app/mailers/.keep b/app/mailers/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/models/.keep b/app/models/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/models/activity.rb b/app/models/activity.rb new file mode 100644 index 000000000..780e8e216 --- /dev/null +++ b/app/models/activity.rb @@ -0,0 +1,38 @@ +class Activity < ActiveRecord::Base + enum type_of: [ + :create_project, + :rename_project, + :change_project_visibility, + :archive_project, + :restore_project, + :assign_user_to_project, + :change_user_role_on_project, + :unassign_user_from_project, + :create_module, + :clone_module, + :archive_module, + :restore_module, + :change_module_description, + :assign_user_to_module, + :unassign_user_from_module, + :create_step, + :destroy_step, + :add_comment_to_step, + :complete_step, + :uncomplete_step, + :check_step_checklist_item, + :uncheck_step_checklist_item, + :edit_step, + :add_result, + :add_comment_to_result, + :archive_result, + :edit_result + ] + + validates :type_of, presence: true + validates :project, :user, presence: true + + belongs_to :project, inverse_of: :activities + belongs_to :my_module, inverse_of: :activities + belongs_to :user, inverse_of: :activities +end diff --git a/app/models/asset.rb b/app/models/asset.rb new file mode 100644 index 000000000..37eeda2c8 --- /dev/null +++ b/app/models/asset.rb @@ -0,0 +1,239 @@ +class Asset < ActiveRecord::Base + include SearchableModel + include DatabaseHelper + + require 'tempfile' + + # Paperclip validation + has_attached_file :file, { + styles: { + medium: '300x300>' + } + } + + validates_attachment :file, presence: true, size: { less_than: 50.megabytes } + validates :estimated_size, presence: true + validates :file_present, inclusion: { in: [true, false] } + + # Should be checked for any security leaks + do_not_validate_attachment_file_type :file + + before_file_post_process :allow_styles_on_images + + # Asset validation + # This could cause some problems if you create empty asset and want to + # assign it to result + validate :step_or_result + + belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User' + belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User' + has_one :step_asset, + inverse_of: :asset, + dependent: :destroy + has_one :step, through: :step_asset, + dependent: :nullify + + has_one :result_asset, + inverse_of: :asset, + dependent: :destroy + has_one :result, through: :result_asset, + dependent: :nullify + has_many :report_elements, inverse_of: :asset, dependent: :destroy + has_one :asset_text_datum, inverse_of: :asset, dependent: :destroy + + def file_empty(name, size) + file_ext = name.split(".").last + self.file_file_name = name + self.file_content_type = Rack::Mime.mime_type(".#{file_ext}") + self.file_file_size = size + self.file_updated_at = DateTime.now + self.file_present = false + end + + def self.new_empty(name, size) + asset = self.new + asset.file_empty name, size + asset + end + + def self.search( + user, + include_archived, + query = nil, + page = 1 + ) + step_ids = + Step + .search(user, include_archived, nil, SHOW_ALL_RESULTS) + .joins(:step_assets) + .select("step_assets.id") + .distinct + + result_ids = + Result + .search(user, include_archived, nil, SHOW_ALL_RESULTS) + .joins(:result_asset) + .select("result_assets.id") + .distinct + + new_query = Asset + .distinct + .joins("LEFT OUTER JOIN step_assets ON step_assets.asset_id = assets.id") + .joins("LEFT OUTER JOIN result_assets ON result_assets.asset_id = assets.id") + .where( + "step_assets.id IN (?) OR result_assets.id IN (?)", + step_ids, + result_ids + ) + .where_attributes_like(:file_file_name, query) + + # Show all results if needed + if page == SHOW_ALL_RESULTS + new_query + else + new_query + .limit(SEARCH_LIMIT) + .offset((page - 1) * SEARCH_LIMIT) + end + end + + def is_image? + !(self.file.content_type =~ /^image/).nil? + end + # TODO: get the current_user + # before_save do + # if current_user + # self.created_by ||= current_user + # self.last_modified_by = current_user if self.changed? + # end + # end + + def is_stored_on_s3? + file.options[:storage].to_sym == :s3 + end + + def post_process_file(org = nil) + # Update self.empty + self.update(file_present: true) + + # Extract asset text if it's of correct type + if TEXT_EXTRACT_FILE_TYPES.any? { |v| file_content_type.start_with? v } + Rails.logger.info "Asset #{id}: Creating extract text job" + # The extract_asset_text also includes + # estimated size calculation + delay(queue: :assets).extract_asset_text(org) + else + # Update asset's estimated size immediately + update_estimated_size(org) + end + end + + def extract_asset_text(org = nil) + if file.blank? + return + end + + begin + file_path = file.path + + if file.is_stored_on_s3? + fa = file.fetch + file_path = fa.path + end + + y = Yomu.new file_path + text_data = y.text + + if asset_text_datum.present? + # Update existing text datum if it exists + asset_text_datum.update(data: text_data) + else + # Create new text datum + AssetTextDatum.create(data: text_data, asset: self) + end + + Rails.logger.info "Asset #{id}: Asset file successfully extracted" + + # Finally, update asset's estimated size to include + # the data vector + update_estimated_size(org) + rescue Exception => e + Rails.logger.fatal "Asset #{id}: Error extracting contents from asset file #{file.path}: " + e.message + ensure + File.delete file_path if fa + end + end + + # If organization is provided, its space_taken + # is updated as well + def update_estimated_size(org = nil) + if file_file_size.blank? + return + end + + es = file_file_size + if asset_text_datum.present? and asset_text_datum.persisted? then + asset_text_datum.reload + es += get_octet_length_record(asset_text_datum, :data) + es += get_octet_length_record(asset_text_datum, :data_vector) + end + es = es * ASSET_ESTIMATED_SIZE_FACTOR + update(estimated_size: es) + Rails.logger.info "Asset #{id}: Estimated size successfully calculated" + + # Finally, update organization's space + if org.present? + org.take_space(es) + org.save + end + end + + def presigned_url + if file.is_stored_on_s3? + signer = Aws::S3::Presigner.new(client: S3_BUCKET.client) + + signer.presigned_url(:get_object, + bucket: S3_BUCKET.name, + key: file.path[1..-1], + expires_in: 30, + # this response header forces object download + response_content_disposition: 'attachment; filename=' + file_file_name) + end + end + + protected + + # Checks if attachments is an image (in post processing imagemagick will + # generate styles) + def allow_styles_on_images + if !(file.content_type =~ %r{^(image|(x-)?application)/(x-png|pjpeg|jpeg|jpg|png|gif)$}) + return false + end + end + + private + + def file_changed? + previous_changes.present? and + ( + ( + previous_changes.key? "file_file_name" and + previous_changes["file_file_name"].first != + previous_changes["file_file_name"].last + ) or ( + previous_changes.key? "file_file_size" and + previous_changes["file_file_size"].first != + previous_changes["file_file_size"].last + ) + ) + end + + def step_or_result + # We must allow both step and result to be blank because of GUI + # (eventhough it's not really a "valid" asset) + if step.present? && result.present? + errors.add(:base, "Asset can only be result or step, not both.") + end + end + +end diff --git a/app/models/asset_text_datum.rb b/app/models/asset_text_datum.rb new file mode 100644 index 000000000..ad131ba11 --- /dev/null +++ b/app/models/asset_text_datum.rb @@ -0,0 +1,64 @@ +class AssetTextDatum < ActiveRecord::Base + include SearchableModel + + validates :data, presence: true + validates :asset, presence: true, uniqueness: true + belongs_to :asset + + after_save :update_ts_index + + def self.search(user, include_archived, query = nil, page = 1) + + module_ids = + MyModule + .search(user, include_archived, nil, SHOW_ALL_RESULTS) + .select("id") + + # Trim whitespace and replace it with OR character. Make prefixed + # wildcard search term and escape special characters. + # For example, search term 'demo project' is transformed to + # 'demo:*|project:*' which makes word inclusive search with postfix + # wildcard. + s_query = query.strip + .split(/\s+/) + .map {|t| t + ":*" } + .join("|") + .gsub('\'', '"') + # make prefixed wildcard search term + query = query.gsub(":*", "") + ":*" + + ids = AssetTextDatum + .select(:id) + .distinct + .joins(:asset) + .joins("LEFT JOIN result_assets ON assets.id = result_assets.asset_id") + .joins("LEFT JOIN results ON result_assets.result_id = results.id") + .joins("LEFT JOIN step_assets ON assets.id = step_assets.asset_id") + .joins("LEFT JOIN steps ON step_assets.step_id = steps.id") + .joins("INNER JOIN my_modules ON results.my_module_id = my_modules.id OR steps.my_module_id = my_modules.id") + .where("my_modules.id IN (?)", module_ids) + .where("data_vector @@ to_tsquery(?)", s_query) + + # Limit results if needed + if page != SHOW_ALL_RESULTS + ids = ids + .limit(SEARCH_LIMIT) + .offset((page - 1) * SEARCH_LIMIT) + end + + AssetTextDatum + .select("*") + .select("ts_headline(data, to_tsquery('" + s_query + "'), + 'StartSel=, StopSel=') headline") + .where("id IN (?)", ids) + end + + def update_ts_index + if data_changed? + sql = "UPDATE asset_text_data " + + "SET data_vector = to_tsvector(data) " + + "WHERE id = " + Integer(id).to_s + AssetTextDatum.connection.execute(sql) + end + end +end diff --git a/app/models/checklist.rb b/app/models/checklist.rb new file mode 100644 index 000000000..67ddd1466 --- /dev/null +++ b/app/models/checklist.rb @@ -0,0 +1,28 @@ +class Checklist < ActiveRecord::Base + validates :name, + presence: true, + length: { maximum: 50 } + validates :step, presence: true + + belongs_to :step, inverse_of: :checklists + belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User' + belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User' + has_many :checklist_items, + inverse_of: :checklist, + dependent: :destroy + has_many :report_elements, + inverse_of: :checklist, + dependent: :destroy + + accepts_nested_attributes_for :checklist_items, + reject_if: :all_blank, + allow_destroy: true + + # TODO: get the current_user + # before_save do + # if current_user + # self.created_by ||= current_user + # self.last_modified_by = current_user if self.changed? + # end + # end +end diff --git a/app/models/checklist_item.rb b/app/models/checklist_item.rb new file mode 100644 index 000000000..36973b3a4 --- /dev/null +++ b/app/models/checklist_item.rb @@ -0,0 +1,19 @@ +class ChecklistItem < ActiveRecord::Base + validates :text, + presence: true, + length: { maximum: 1000 } + validates :checklist, presence: true + validates :checked, inclusion: { in: [true, false] } + + belongs_to :checklist, inverse_of: :checklist_items + belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User' + belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User' + + # TODO: get the current_user + # before_save do + # if current_user + # self.created_by ||= current_user + # self.last_modified_by = current_user if self.changed? + # end + # end +end diff --git a/app/models/comment.rb b/app/models/comment.rb new file mode 100644 index 000000000..0473f62aa --- /dev/null +++ b/app/models/comment.rb @@ -0,0 +1,92 @@ +class Comment < ActiveRecord::Base + include SearchableModel + + validates :message, + presence: true, + length: { maximum: 1000 } + validates :user, presence: true + + validate :belongs_to_only_one_object + + belongs_to :user, inverse_of: :comments + belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User' + + has_one :step_comment, inverse_of: :comment + has_one :my_module_comment, inverse_of: :comment + has_one :result_comment, inverse_of: :comment + has_one :sample_comment, inverse_of: :comment + has_one :project_comment, inverse_of: :comment + + def self.search( + user, + include_archived, + query = nil, + page = 1 + ) + project_ids = + Project + .search(user, include_archived, nil, SHOW_ALL_RESULTS) + .select("id") + my_module_ids = + MyModule + .search(user, include_archived, nil, SHOW_ALL_RESULTS) + .select("id") + step_ids = + Step + .search(user, include_archived, nil, SHOW_ALL_RESULTS) + .select("id") + result_ids = + Result + .search(user, include_archived, nil, SHOW_ALL_RESULTS) + .select("id") + + new_query = Comment + .distinct + .joins(:user) + .joins("LEFT JOIN project_comments ON project_comments.comment_id = comments.id") + .joins("LEFT JOIN my_module_comments ON my_module_comments.comment_id = comments.id") + .joins("LEFT JOIN step_comments ON step_comments.comment_id = comments.id") + .joins("LEFT JOIN result_comments ON result_comments.comment_id = comments.id") + .where( + "project_comments.project_id IN (?) OR " + + "my_module_comments.my_module_id IN (?) OR " + + "step_comments.step_id IN (?) OR " + + "result_comments.result_id IN (?)", + project_ids, + my_module_ids, + step_ids, + result_ids + ) + .where_attributes_like( + [ :message, "users.full_name" ], + query + ) + + # Show all results if needed + if page == SHOW_ALL_RESULTS + new_query + else + new_query + .limit(SEARCH_LIMIT) + .offset((page - 1) * SEARCH_LIMIT) + end + end + + private + + def belongs_to_only_one_object + # We must allow all elements to be blank because of GUI + # (eventhough it's not really a "valid" comment) + cntr = 0 + cntr += 1 if step_comment.present? + cntr += 1 if my_module_comment.present? + cntr += 1 if result_comment.present? + cntr += 1 if sample_comment.present? + cntr += 1 if project_comment.present? + + if cntr > 1 + errors.add(:base, "Comment can only belong to 1 'parent' object.") + end + end + +end diff --git a/app/models/concerns/.keep b/app/models/concerns/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/app/models/concerns/archivable_model.rb b/app/models/concerns/archivable_model.rb new file mode 100644 index 000000000..4de805c95 --- /dev/null +++ b/app/models/concerns/archivable_model.rb @@ -0,0 +1,81 @@ +module ArchivableModel + extend ActiveSupport::Concern + + included do + validates :archived, inclusion: { in: [true, false] } + before_save :set_archive_timestamp + before_save :set_restore_timestamp + end + + # Not archived + def active? + not archived? + end + + # Helper for archiving project. Timestamp of archiving is handler by + # before_save callback. + def archive + self.archived = true + save + end + + # Helper for archiving project. Timestamp of archiving is handler by + # before_save callback. + # Sets the archived_by value to the current user. + def archive (current_user) + self.archived = true + self.archived_by = current_user + save + end + + # Same as archive but raises exception if archive fails. + def archive! + archive || raise(RecordNotSaved) + end + + # Same as archive but raises exception if archive fails. + # Sets the archived_by value to the current user. + def archive!(current_user) + archive(current_user) || raise(RecordNotSaved) + end + + # Helper for restoring project from archive. + def restore + self.archived = false + save + end + + # Helper for restoring project from archive. + # Sets the restored_by value to the current user. + def restore (current_user) + self.archived = false + self.restored_by = current_user + save + end + + # Same as restore but raises exception if restore fails. + def restore! + restore || raise(RecordNotSaved) + end + + + # Same as restore but raises exception if restore fails. + # Sets the restored_by value to the current user. + def restore!(current_user) + restore(current_user) || raise(RecordNotSaved) + end + + protected + + def set_archive_timestamp + if self.archived_changed?(from: false, to: true) + self.archived_on = Time.current.to_formatted_s + end + end + + def set_restore_timestamp + if self.archived_changed?(from: true, to: false) + self.restored_on = Time.current.to_formatted_s + end + end +end diff --git a/app/models/concerns/searchable_model.rb b/app/models/concerns/searchable_model.rb new file mode 100644 index 000000000..c4a32f501 --- /dev/null +++ b/app/models/concerns/searchable_model.rb @@ -0,0 +1,32 @@ +module SearchableModel + extend ActiveSupport::Concern + + included do + + # Helper function for relations that + # adds OR ILIKE where clause for all specified attributes + # for the given search query + scope :where_attributes_like, ->(attributes, query) do + attrs = [] + if attributes.blank? or query.blank? + # Do nothing in this case + elsif attributes.is_a? Symbol + attrs = [attributes.to_s] + elsif attributes.is_a? String + attrs = [attributes] + elsif attributes.is_a? Array + attrs = attributes.collect { |a| a.to_s } + else + raise ArgumentError, ":attributes must be an array, symbol or string" + end + + if (attrs.length > 0) + where_str = + (attrs.map.with_index { |a,i| "#{a} ILIKE :t#{i} OR " }).join[0..-5] + vals = (attrs.map.with_index { |a,i| [ "t#{i}".to_sym, "%#{query}%" ] }).to_h + + return where(where_str, vals) + end + end + end +end \ No newline at end of file diff --git a/app/models/connection.rb b/app/models/connection.rb new file mode 100644 index 000000000..e2c68652b --- /dev/null +++ b/app/models/connection.rb @@ -0,0 +1,4 @@ +class Connection < ActiveRecord::Base + belongs_to :to, :class_name => 'MyModule', :foreign_key => 'input_id', inverse_of: :inputs + belongs_to :from, :class_name => 'MyModule', :foreign_key => 'output_id', inverse_of: :outputs +end diff --git a/app/models/custom_field.rb b/app/models/custom_field.rb new file mode 100644 index 000000000..da5ccb234 --- /dev/null +++ b/app/models/custom_field.rb @@ -0,0 +1,11 @@ +class CustomField < ActiveRecord::Base + validates :name, + presence: true, + length: { maximum: 50 } + validates :user, :organization, presence: true + + belongs_to :user, inverse_of: :custom_fields + belongs_to :organization, inverse_of: :custom_fields + belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User' + has_many :sample_custom_fields, inverse_of: :custom_field +end diff --git a/app/models/log.rb b/app/models/log.rb new file mode 100644 index 000000000..a17fdebf9 --- /dev/null +++ b/app/models/log.rb @@ -0,0 +1,6 @@ +class Log < ActiveRecord::Base + validates :message, presence: true + validates :organization, presence: true + + belongs_to :organization, inverse_of: :logs +end diff --git a/app/models/my_module.rb b/app/models/my_module.rb new file mode 100644 index 000000000..bc40c9c39 --- /dev/null +++ b/app/models/my_module.rb @@ -0,0 +1,413 @@ +class MyModule < ActiveRecord::Base + include ArchivableModel, SearchableModel + + + validates :name, + presence: true, + length: { minimum: 2, maximum: 50 } + validates :x, :y, :workflow_order, presence: true + validates :project, presence: true + validates :my_module_group, presence: true, if: "!my_module_group_id.nil?" + + belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User' + belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User' + belongs_to :archived_by, foreign_key: 'archived_by_id', class_name: 'User' + belongs_to :restored_by, foreign_key: 'restored_by_id', class_name: 'User' + belongs_to :project, inverse_of: :my_modules + belongs_to :my_module_group, inverse_of: :my_modules + has_many :steps, inverse_of: :my_module, :dependent => :destroy + has_many :results, inverse_of: :my_module, :dependent => :destroy + has_many :my_module_tags, inverse_of: :my_module, :dependent => :destroy + has_many :tags, through: :my_module_tags + has_many :my_module_comments, inverse_of: :my_module, :dependent => :destroy + has_many :comments, through: :my_module_comments + has_many :inputs, :class_name => 'Connection', :foreign_key => "input_id", inverse_of: :to, :dependent => :destroy + has_many :outputs, :class_name => 'Connection', :foreign_key => "output_id", inverse_of: :from, :dependent => :destroy + has_many :my_modules, through: :outputs, source: :to + has_many :my_module_antecessors, through: :inputs, source: :from, class_name: 'MyModule' + has_many :sample_my_modules, inverse_of: :my_module, :dependent => :destroy + has_many :samples, through: :sample_my_modules + has_many :user_my_modules, inverse_of: :my_module, :dependent => :destroy + has_many :users, through: :user_my_modules + has_many :activities, inverse_of: :my_module + has_many :report_elements, inverse_of: :my_module, :dependent => :destroy + + def self.search(user, include_archived, query = nil, page = 1) + project_ids = + Project + .search(user, include_archived, nil, SHOW_ALL_RESULTS) + .select("id") + + if include_archived + new_query = MyModule + .distinct + .where("my_modules.project_id IN (?)", project_ids) + .where_attributes_like(:name, query) + else + new_query = MyModule + .distinct + .where("my_modules.project_id IN (?)", project_ids) + .where("my_modules.archived = ?", false) + .where_attributes_like(:name, query) + end + + # Show all results if needed + if page == SHOW_ALL_RESULTS + new_query + else + new_query + .limit(SEARCH_LIMIT) + .offset((page - 1) * SEARCH_LIMIT) + end + end + + # Deep-clone given array of assets + def self.deep_clone_assets(assets_to_clone, org) + assets_to_clone.each do |src_id, dest_id| + src = Asset.find_by_id(src_id) + dest = Asset.find_by_id(dest_id) + if src.present? and dest.present? then + # Clone file + dest.file = src.file + dest.save + + # Clone extracted text data if it exists + if (atd = src.asset_text_datum).present? then + atd2 = AssetTextDatum.new( + data: atd.data, + asset: dest + ) + atd2.save + end + + # Update estimated size of cloned asset + dest.update(estimated_size: src.estimated_size) + + # Update organization's space taken + org.reload + org.take_space(dest.estimated_size) + org.save + end + end + end + + # Removes assigned samples from module and connections with other + # modules. + def archive (current_user) + self.x = 0 + self.y = 0 + # Remove association with module group. + self.my_module_group = nil + + MyModule.transaction do + archived = super + # Unassociate all samples from module. + archived = SampleMyModule.destroy_all(:my_module => self) if archived + # Remove all connection between modules. + archived = Connection.delete_all(:input_id => id) if archived + archived = Connection.delete_all(:output_id => id) if archived + unless archived + raise ActiveRecord::Rollback + end + end + archived + end + + # Similar as super restore, but also calculate new module position + def restore(current_user) + restored = false + + # Calculate new module position + new_pos = get_new_position + self.x = new_pos[:x] + self.y = new_pos[:y] + + MyModule.transaction do + restored = super + + unless restored + raise ActiveRecord::Rollback + end + end + restored + end + + def unassigned_users + User.find_by_sql( + "SELECT DISTINCT users.id, users.full_name FROM users " + + "INNER JOIN user_projects ON users.id = user_projects.user_id " + + "WHERE user_projects.project_id = #{project_id.to_s}" + + " AND users.id NOT IN " + + "(SELECT DISTINCT user_id FROM user_my_modules WHERE user_my_modules.my_module_id = #{id.to_s})" + ) + end + + def unassigned_samples + Sample.where(organization_id: project.organization).where.not(id: samples) + end + + def unassigned_tags + Tag.find_by_sql( + "SELECT DISTINCT tags.id, tags.name, tags.color FROM tags " + + "WHERE tags.project_id = #{project_id.to_s} AND tags.id NOT IN " + + "(SELECT DISTINCT tag_id FROM my_module_tags WHERE my_module_tags.my_module_id = #{id.to_s})" + ) + end + + def number_of_steps + steps.count + end + + def last_activities(count = 20) + Activity.where(my_module_id: id).order(:created_at).last(count) + end + + # Get module comments ordered by created_at time. Results are paginated + # using last comment id and per_page parameters. + def last_comments(last_id = 1, per_page = 20) + last_id = 9999999999999 if last_id <= 1 + Comment.joins(:my_module_comment) + .where(my_module_comments: {my_module_id: id}) + .where('comments.id < ?', last_id) + .order(created_at: :desc) + .limit(per_page) + end + + def last_activities(last_id = 1, count = 20) + last_id = 9999999999999 if last_id <= 1 + Activity.joins(:my_module) + .where(my_module_id: id) + .where('activities.id < ?', last_id) + .order(created_at: :desc) + .limit(count) + .uniq + end + + def first_n_samples(count = 20) + samples.order(name: :asc).limit(count) + end + + def completed_steps + steps.select { |step| step.completed } + end + + def number_of_samples + samples.count + end + + def is_overdue?(datetime = DateTime.current) + due_date.present? and datetime.utc > due_date.utc + end + + def overdue_for_days(datetime = DateTime.current) + if due_date.blank? or due_date.utc > datetime.utc + return 0 + else + return ((datetime.utc.to_i - due_date.utc.to_i) / (60*60*24).to_f).ceil + end + end + + def is_one_day_prior?(datetime = DateTime.current) + is_due_in?(datetime, 1.day) + end + + def is_due_in?(datetime, diff) + due_date.present? and datetime.utc < due_date.utc and datetime.utc > (due_date.utc - diff) + end + + def space_taken + st = 0 + steps.find_each do |step| + st += step.space_taken + end + results + .includes(:result_asset) + .find_each do |result| + st += result.space_taken + end + st + end + + def archived_results + results + .select('results.*') + .select('ra.id AS result_asset_id') + .select('rt.id AS result_table_id') + .select('rx.id AS result_text_id') + .joins('LEFT JOIN result_assets AS ra ON ra.result_id = results.id') + .joins('LEFT JOIN result_tables AS rt ON rt.result_id = results.id') + .joins('LEFT JOIN result_texts AS rx ON rx.result_id = results.id') + .where(:archived => true) + end + + # Treat this module as root, get all modules of that subtree + def get_downstream_modules + final = [] + modules = [self] + while !modules.empty? + my_module = modules.shift + if !final.include?(my_module) + final << my_module + end + modules.push(*my_module.my_modules.flatten) + end + final + end + + # Treat this module as inversed root, get all modules of that inversed subtree + def get_upstream_modules + final = [] + modules = [self] + while !modules.empty? + my_module = modules.shift + if !final.include?(my_module) + final << my_module + end + modules.push(*my_module.my_module_antecessors.flatten) + end + final + end + + + # Generate the samples belonging to this module + # in JSON form, suitable for display in handsontable.js + def samples_json_hot(order) + data = [] + samples.order(created_at: order).each do |sample| + sample_json = [] + sample_json << sample.name + if sample.sample_type.present? + sample_json << sample.sample_type.name + else + sample_json << I18n.t("samples.table.no_type") + end + if sample.sample_group.present? + sample_json << sample.sample_group.name + else + sample_json << I18n.t("samples.table.no_group") + end + sample_json << I18n.l(sample.created_at, format: :full) + sample_json << sample.user.full_name + data << sample_json + end + + # Prepare column headers + headers = [ + I18n.t("samples.table.sample_name"), + I18n.t("samples.table.sample_type"), + I18n.t("samples.table.sample_group"), + I18n.t("samples.table.added_on"), + I18n.t("samples.table.added_by") + ] + { data: data, headers: headers } + end + + def deep_clone(current_user) + assets_to_clone = [] + + # Copy the module + clone = MyModule.new( + name: self.name, + project: self.project, + description: self.description, + x: self.x, + y: self.y) + clone.save + + # Copy steps + self.steps.each do |step| + step2 = Step.new( + name: step.name, + description: step.description, + position: step.position, + completed: false, + user: current_user, + my_module: clone) + step2.save + + # Copy checklists + step.checklists.each do |checklist| + checklist2 = Checklist.new( + name: checklist.name, + step: step2 + ) + checklist2.created_by = current_user + checklist2.last_modified_by = current_user + checklist2.save + + checklist.checklist_items.each do |item| + item2 = ChecklistItem.new( + text: item.text, + checked: false, + checklist: checklist2) + item2.created_by = current_user + item2.last_modified_by = current_user + item2.save + end + + step2.checklists << checklist2 + end + + # "Shallow" Copy assets + step.assets.each do |asset| + asset2 = Asset.new_empty( + asset.file_file_name, + asset.file_file_size + ) + asset2.created_by = current_user + asset2.last_modified_by = current_user + asset2.save + + step2.assets << asset2 + assets_to_clone << [asset.id, asset2.id] + end + + # Copy tables + step.tables.each do |table| + table2 = Table.new( + contents: table.contents) + table2.created_by = current_user + table2.last_modified_by = current_user + step2.tables << table2 + end + end + + # Call clone module helper + MyModule.delay(queue: :assets).deep_clone_assets( + assets_to_clone, + self.project.organization + ) + + clone.reload + + return clone + end + + # Writes to user log. + def log(message) + final = "[%s] %s" % [name, message] + project.log(final) + end + + private + + # Find an empty position for the restored module. It's + # basically a first empty row with x=0. + def get_new_position + if project.blank? + return { x: 0, y: 0 } + end + + new_y = 0 + positions = project.active_modules.collect{ |m| [m.x, m.y] } + (0..10000).each do |n| + unless positions.include? [0, n] + new_y = n + break + end + end + + return { x: 0, y: new_y } + end + +end diff --git a/app/models/my_module_comment.rb b/app/models/my_module_comment.rb new file mode 100644 index 000000000..7f0eaea11 --- /dev/null +++ b/app/models/my_module_comment.rb @@ -0,0 +1,7 @@ +class MyModuleComment < ActiveRecord::Base + validates :comment, :my_module, presence: true + validates :my_module_id, uniqueness: { scope: :comment_id } + + belongs_to :comment, inverse_of: :my_module_comment + belongs_to :my_module, inverse_of: :my_module_comments +end diff --git a/app/models/my_module_group.rb b/app/models/my_module_group.rb new file mode 100644 index 000000000..c22c984d7 --- /dev/null +++ b/app/models/my_module_group.rb @@ -0,0 +1,34 @@ +class MyModuleGroup < ActiveRecord::Base + include SearchableModel + + validates :name, + presence: true, + length: { maximum: 50 } + validates :project, presence: true + + belongs_to :project, inverse_of: :my_module_groups + belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User' + has_many :my_modules, inverse_of: :my_module_group, + dependent: :nullify + + def self.search(user, include_archived, query = nil, page = 1) + project_ids = + Project + .search(user, include_archived, nil, SHOW_ALL_RESULTS) + .select("id") + + new_query = MyModuleGroup + .distinct + .where("my_module_groups.project_id IN (?)", project_ids) + .where_attributes_like(:name, query) + + # Show all results if needed + if page == SHOW_ALL_RESULTS + new_query + else + new_query + .limit(SEARCH_LIMIT) + .offset((page - 1) * SEARCH_LIMIT) + end + end +end diff --git a/app/models/my_module_tag.rb b/app/models/my_module_tag.rb new file mode 100644 index 000000000..f64bedbcc --- /dev/null +++ b/app/models/my_module_tag.rb @@ -0,0 +1,8 @@ +class MyModuleTag < ActiveRecord::Base + validates :my_module, :tag, presence: true + validates :tag_id, uniqueness: { scope: :my_module_id } + + belongs_to :my_module, inverse_of: :my_module_tags + belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User' + belongs_to :tag, inverse_of: :my_module_tags +end diff --git a/app/models/organization.rb b/app/models/organization.rb new file mode 100644 index 000000000..3a1a21037 --- /dev/null +++ b/app/models/organization.rb @@ -0,0 +1,294 @@ +class Organization < ActiveRecord::Base + # Not really MVC-compliant, but we just use it for logger + # output in space_taken related functions + include ActionView::Helpers::NumberHelper + + validates :name, + presence: true, + length: { minimum: 4, maximum: 100 } + validates :space_taken, + presence: true + + belongs_to :created_by, :foreign_key => 'created_by_id', :class_name => 'User' + belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User' + has_many :user_organizations, inverse_of: :organization, dependent: :destroy + has_many :users, through: :user_organizations + has_many :samples, inverse_of: :organization + has_many :sample_groups, inverse_of: :organization + has_many :sample_types, inverse_of: :organization + has_many :logs, inverse_of: :organization + has_many :projects, inverse_of: :organization + has_many :custom_fields, inverse_of: :organization + + # Based on file's extension opens file (used for importing) + def self.open_spreadsheet(file) + filename = file.original_filename + file_path = file.path + + if file.class == Paperclip::Attachment and file.is_stored_on_s3? + fa = file.fetch + file_path = fa.path + end + + case File.extname(filename) + when ".csv" then + Roo::CSV.new(file_path, extension: :csv) + when ".tdv" then + Roo::CSV.new(file_path, nil, :ignore, csv_options: {col_sep: "\t"}) + when ".txt" then + # This assumption is based purely on biologist's habits + Roo::CSV.new(file_path, csv_options: {col_sep: "\t"}) + when ".xls" then + Roo::Excel.new(file_path) + when ".xlsx" then + Roo::Excelx.new(file_path) + else + raise TypeError + end + end + + # Writes to user log. + def log(message) + final = "[%s] %s" % [Time.current.to_s, message] + logs.create(message: final) + end + + # Imports samples into db + # -1 == sample_name, + # -2 == sample_type, + # -3 == sample_group + # TODO: use constants + def import_samples(sheet, mappings, user) + errors = [] + nr_of_added = 0 + total_nr = 0 + + # First let's query for all custom_fields we're refering to + custom_fields = [] + sname_index = -1 + stype_index = -1 + sgroup_index = -1 + mappings.each.with_index do |(k, v), i| + if v == "-1" + # Fill blank space, so our indices stay the same + custom_fields << nil + sname_index = i + elsif v == "-2" + custom_fields << nil + stype_index = i + elsif v == "-3" + custom_fields << nil + sgroup_index = i + else + cf = CustomField.find_by_id(v) + + # Even if doesn't exist we add nil value in order not to destroy our + # indices + custom_fields << cf + end + end + + # Now we can iterate through sample data and save stuff into db + (2..sheet.last_row).each do |i| + error = [] + total_nr += 1 + + sample = Sample.new( + name: sheet.row(i)[sname_index], + organization_id: id, + user: user + ) + + if sample.save + sheet.row(i).each.with_index do |value, index| + # We need to have sample saved before messing with custom fields (they + # need sample id) + if index == stype_index + stype = SampleType.where(name: value, organization_id: id).take(); + + if stype + sample.sample_type = stype + else + sample.create_sample_type( + name: value, + organization_id: id + ) + end + sample.save + elsif index == sgroup_index + sgroup = SampleGroup.where(name: value, organization_id: id).take(); + + if sgroup + sample.sample_group = sgroup + else + sample.create_sample_group( + name: value, + organization_id: id + ) + end + sample.save + elsif value and mappings[index.to_s].strip.present? and index != sname_index + if custom_fields[index] + # we're working with CustomField + scf = SampleCustomField.new( + sample_id: sample.id, + custom_field_id: custom_fields[index].id, + value: value + ) + + if !scf.save + error << scf.errors.messages + end + else + # This custom_field does not exist + error << {"#{mappings[index]}": "Does not exists"} + end + end + end + else + error << sample.errors.messages + end + if error.present? + errors << { "#{i}": error} + else + nr_of_added += 1 + end + end + + if errors.count > 0 then + return { + status: :error, + errors: errors, + nr_of_added: nr_of_added, + total_nr: total_nr + } + else + return { + status: :ok, + nr_of_added: nr_of_added, + total_nr: total_nr + } + end + end + + def to_csv(samples, headers) + require "csv" + + # Parse headers (magic numbers should be refactored - see + # sample-datatable.js) + header_names = [] + headers.each do |header| + if header == "-1" + header_names << I18n.t("samples.table.sample_name") + elsif header == "-2" + header_names << I18n.t("samples.table.sample_type") + elsif header == "-3" + header_names << I18n.t("samples.table.sample_group") + elsif header == "-4" + header_names << I18n.t("samples.table.added_by") + elsif header == "-5" + header_names << I18n.t("samples.table.added_on") + else + cf = CustomField.find_by_id(header) + + if cf + header_names << cf.name + else + header_names << nil + end + end + end + + CSV.generate do |csv| + csv << header_names + samples.each do |sample| + sample_row = [] + headers.each do |header| + if header == "-1" + sample_row << sample.name + elsif header == "-2" + sample_row << (sample.sample_type.nil? ? I18n.t("samples.table.no_type") : sample.sample_type.name) + elsif header == "-3" + sample_row << (sample.sample_group.nil? ? I18n.t("samples.table.no_group") : sample.sample_group.name) + elsif header == "-4" + sample_row << sample.user.full_name + elsif header == "-5" + sample_row << I18n.l(sample.created_at, format: :full) + else + scf = SampleCustomField.where( + custom_field_id: header, + sample_id: sample.id + ).take + + if scf + sample_row << scf.value + else + sample_row << nil + end + end + end + csv << sample_row + end + end + end + + # Get all header fields for samples (used in importing for mappings - dropdowns) + def get_available_sample_fields + fields = {}; + + # First and foremost add sample name + fields["-1"] = I18n.t("samples.table.sample_name") + fields["-2"] = I18n.t("samples.table.sample_type") + fields["-3"] = I18n.t("samples.table.sample_group") + + # Add all other custom fields + CustomField.where(organization_id: id).order(:created_at).each do |cf| + fields[cf.id] = cf.name + end + + fields + end + + # (re)calculate the space taken by this organization + def calculate_space_taken + st = 0 + projects.includes( + my_modules: { steps: :assets, results: { result_asset: :asset } } + ).find_each do |project| + project.my_modules.find_each do |my_module| + my_module.steps.find_each do |step| + step.assets.find_each { |asset| st += asset.estimated_size } + end + my_module.results.find_each do |result| + if result.is_asset then + st += result.asset.estimated_size + end + end + end + end + self.space_taken = [st, MINIMAL_ORGANIZATION_SPACE_TAKEN].max + Rails::logger.info "Organization #{self.id}: " + + "space (re)calculated to: " + + "#{self.space_taken}B (#{number_to_human_size(self.space_taken)})" + end + + # Take specified amount of bytes + def take_space(space) + orig_space = self.space_taken + self.space_taken += space + Rails::logger.info "Organization #{self.id}: " + + "space taken: " + + "#{orig_space}B + #{space}B = " + + "#{self.space_taken}B (#{number_to_human_size(self.space_taken)})" + end + + # Release specified amount of bytes + def release_space(space) + orig_space = self.space_taken + self.space_taken = [space_taken - space, MINIMAL_ORGANIZATION_SPACE_TAKEN].max + Rails::logger.info "Organization #{self.id}: " + + "space released: " + + "#{orig_space}B - #{space}B = " + + "#{self.space_taken}B (#{number_to_human_size(self.space_taken)})" + end +end diff --git a/app/models/project.rb b/app/models/project.rb new file mode 100644 index 000000000..e4e0dc787 --- /dev/null +++ b/app/models/project.rb @@ -0,0 +1,547 @@ +class Project < ActiveRecord::Base + include ArchivableModel, SearchableModel + + enum visibility: { hidden: 0, visible: 1 } + + validates :name, + presence: true, + length: { minimum: 4, maximum: 30 }, + uniqueness: { scope: :organization, case_sensitive: false } + validates :visibility, presence: true + validates :organization, presence: true + + belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User' + belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User' + belongs_to :archived_by, foreign_key: 'archived_by_id', class_name: 'User' + belongs_to :restored_by, foreign_key: 'restored_by_id', class_name: 'User' + has_many :user_projects, inverse_of: :project + has_many :users, through: :user_projects + has_many :my_modules, inverse_of: :project + has_many :project_comments, inverse_of: :project + has_many :comments, through: :project_comments + has_many :activities, inverse_of: :project + has_many :my_module_groups, inverse_of: :project + has_many :tags, inverse_of: :project + has_many :reports, inverse_of: :project, dependent: :destroy + has_many :report_elements, inverse_of: :project, dependent: :destroy + belongs_to :organization, inverse_of: :projects + + def self.search(user, include_archived, query = nil, page = 1) + org_ids = + Organization + .joins(:user_organizations) + .where("user_organizations.user_id = ?", user.id) + .select("id") + .distinct + + if include_archived + new_query = Project + .distinct + .joins(:user_projects) + .where("projects.organization_id IN (?)", org_ids) + .where("projects.visibility = 1 OR user_projects.user_id = ?", user.id) + .where_attributes_like(:name, query) + + else + new_query = Project + .distinct + .joins(:user_projects) + .where("projects.organization_id IN (?)", org_ids) + .where("projects.visibility = 1 OR user_projects.user_id = ?", user.id) + .where_attributes_like(:name, query) + .where("projects.archived = ?", false) + end + + # Show all results if needed + if page == SHOW_ALL_RESULTS + new_query + else + new_query + .limit(SEARCH_LIMIT) + .offset((page - 1) * SEARCH_LIMIT) + end + end + + def last_activities(count = 20) + activities.order(:created_at).last(count) + end + + # Get project comments order by created_at time. Results are paginated + # using last comment id and per_page parameters. + def last_comments(last_id = 1, per_page = 20) + last_id = 9999999999999 if last_id <= 1 + Comment.joins(:project_comment) + .where(project_comments: {project_id: id}) + .where('comments.id < ?', last_id) + .order(created_at: :desc) + .limit(per_page) + end + + def active_modules + self.my_modules.where(:archived => false) + end + + def archived_modules + self.my_modules.where(:archived => true) + end + + def unassigned_users + User.find_by_sql( + "SELECT DISTINCT users.id, users.full_name FROM users " + + "INNER JOIN user_organizations ON users.id = user_organizations.user_id " + + "WHERE user_organizations.organization_id = " + organization_id.to_s + + " AND users.id NOT IN " + + "(SELECT DISTINCT user_id FROM user_projects WHERE user_projects.project_id = " + id.to_s + ")" + ) + end + + def assigned_modules(user) + role = self.user_role(user) + if role.blank? + return MyModule.none + elsif role == "owner" + return self.my_modules.where(archived: false) + else + return self.my_modules.where(archived: false) + .joins(:user_my_modules) + .where("user_my_modules.user_id IN (?)", user.id) + .distinct + end + end + + def user_role(user) + unless self.users.include? user + return nil + end + + return (self.user_projects.select { |up| up.user == user }).first.role + end + + def modules_without_group + MyModule.where(project_id: id).where(my_module_group: nil) + .where(archived: false) + end + + def active_module_groups + self.my_module_groups.joins(:my_modules) + .where('my_modules.archived = ?', false) + .distinct + end + + def assigned_samples + Sample.joins(:my_modules).where(my_modules: {id: my_modules} ) + end + + def unassigned_samples(assigned_samples) + Sample.where(organization_id: organization).where.not(id: assigned_samples) + end + + def space_taken + st = 0 + my_modules.find_each do |my_module| + st += my_module.space_taken + end + st + end + + def update_canvas( + to_archive, + to_add, + to_rename, + to_clone, + connections, + positions, + current_user, + module_groups + ) + cloned_modules = [] + begin + Project.transaction do + # First, add new modules + new_ids, cloned_pairs, originals = add_modules( + to_add, to_clone, current_user) + cloned_modules = cloned_pairs.collect { |mn, _| mn } + + # Rename modules + rename_modules(to_rename) + + # Add activities that modules were created + originals.each do |m| + Activity.create( + type_of: :create_module, + user: current_user, + project: self, + my_module: m, + message: I18n.t( + "activities.create_module", + user: current_user.full_name, + module: m.name + ) + ) + end + + # Add activities that modules were cloned + cloned_pairs.each do |mn, mo| + Activity.create( + type_of: :clone_module, + project: mn.project, + my_module: mn, + user: current_user, + message: I18n.t( + "activities.clone_module", + user: current_user.full_name, + module_new: mn.name, + module_original: mo.name + ) + ) + end + + # Then, archive modules that need to be archived + archive_modules(to_archive, current_user) + + # Update connections, positions & module group variables + # with actual IDs retrieved from the new modules creation + updated_connections = [] + connections.each do |a,b| + updated_connections << [new_ids.fetch(a, a), new_ids.fetch(b, b)] + end + updated_positions = Hash.new + positions.each do |id, pos| + updated_positions[new_ids.fetch(id, id)] = pos + end + updated_module_groups = {} + module_groups.each do |id, name| + updated_module_groups[new_ids.fetch(id, id)] = name + end + + # Update connections + update_module_connections(updated_connections) + + # Update module positions (no validation needed here) + update_module_positions(updated_positions) + + # Normalize module positions + normalize_module_positions + + # Finally, update module groups + update_module_groups(updated_module_groups, current_user) + end + rescue ActiveRecord::ActiveRecordError, ArgumentError, ActiveRecord::RecordNotSaved + return false + end + + return true + end + + # Writes to user log. + def log(message) + final = "[%s] %s" % [name, message] + organization.log(final) + end + + private + + # Archive all modules. Receives an array of module integer IDs. + def archive_modules(module_ids) + module_ids.each do |m_id| + my_module = my_modules.find_by_id(m_id) + unless my_module.blank? + my_module.archive! + end + end + my_modules.reload + end + + # Archive all modules. Receives an array of module integer IDs and current user. + def archive_modules(module_ids, current_user) + module_ids.each do |m_id| + my_module = my_modules.find_by_id(m_id) + unless my_module.blank? + my_module.archive!(current_user) + end + end + my_modules.reload + end + + # Add modules, and returns a map of "virtual" IDs with + # actual IDs of saved modules. + # to_add is an array of hashes, each containing 'name', + # 'x', 'y' and 'id'. + # to_clone is a hash, storing new cloned modules as keys, + # and original modules as values. + def add_modules(to_add, to_clone, current_user) + originals = [] + cloned_pairs = {} + ids_map = Hash.new + to_add.each do |m| + original = MyModule.find_by_id(to_clone.fetch(m[:id], nil)) + if original.present? then + my_module = original.deep_clone(current_user) + cloned_pairs[my_module] = original + else + my_module = MyModule.new( + project: self) + originals << my_module + end + + my_module.name = m[:name] + my_module.x = m[:x] + my_module.y = m[:y] + my_module.created_by = current_user + my_module.last_modified_by = current_user + my_module.save! + + ids_map[m[:id]] = my_module.id.to_s + end + my_modules.reload + return ids_map, cloned_pairs, originals + end + + # Rename modules; this method accepts a map where keys + # represent IDs of modules, and values new names for + # such modules. If a module with given ID doesn't exist, + # it's obviously not updated. + def rename_modules(to_rename) + to_rename.each do |id, new_name| + my_module = MyModule.find_by_id(id) + if my_module.present? + my_module.name = new_name + my_module.save! + end + end + end + + # Update connections for all modules in this project. + # Input is an array of arrays, where first element represents + # source node, and second element represents target node. + # Example input: [ [1, 2], [2, 3], [4, 5], [2, 5] ] + def update_module_connections(connections) + require 'rgl/base' + require 'rgl/adjacency' + require 'rgl/topsort' + + dg = RGL::DirectedAdjacencyGraph.new + connections.each do |a,b| + # Check if both vertices exist + if (my_modules.find_all {|m| [a.to_i, b.to_i].include? m.id }).count == 2 + dg.add_edge(a, b) + end + end + + # Check if cycles exist! + topsort = dg.topsort_iterator.to_a + if topsort.length == 0 and dg.edges.size > 1 + raise ArgumentError, "Cycles exist." + end + + # First, delete existing connections + # but keep a copy of previous state + previous_sources = {} + previous_sources.default = [] + my_modules.each do |m| + previous_sources[m.id] = [] + m.inputs.each do |c| + previous_sources[m.id] << c.from + end + end + my_modules.each do |m| + unless m.outputs.destroy_all + raise ActiveRecord::ActiveRecordError + end + end + + + # Add new connections + filtered_edges = dg.edges.collect { |e| [e.source, e.target] } + filtered_edges.each do |a, b| + Connection.create!(:input_id => b, :output_id => a) + end + + # Unassign samples from former downstream modules + # for all destroyed connections + unassign_samples_from_old_downstream_modules(previous_sources) + + visited = [] + # Assign samples to all new downstream modules + filtered_edges.each do |a, b| + source = my_modules.find(a.to_i) + target = my_modules.find(b.to_i) + # Do this only for new edges + if previous_sources[target.id].exclude?(source) + # Go as high upstream as new edges take us + # and then assign samples to all downsteam samples + assign_samples_to_new_downstream_modules(previous_sources, visited, source) + end + end + + # Save topological order of modules (for modules without workflow, + # leave them unordered) + my_modules.each do |m| + if topsort.include? m.id.to_s + m.workflow_order = topsort.find_index(m.id.to_s) + else + m.workflow_order = -1 + end + m.save! + end + + # Make sure to reload my modules, which now have updated connections and samples + my_modules.reload + true + end + + # When connections are deleted, unassign samples that + # are not inherited anymore + def unassign_samples_from_old_downstream_modules(sources) + my_modules.each do |my_module| + sources[my_module.id].each do |s| + # Only do this for newly deleted connections + if s.outputs.map{|i| i.to}.exclude? my_module + my_module.get_downstream_modules.each do |dm| + # Get unique samples for all upstream modules + um = dm.get_upstream_modules + um.shift # remove current module + ums = um.map{|m| m.samples}.flatten.uniq + s.samples.each do |sample| + dm.samples.delete(sample) if ums.exclude? sample + end + end + end + end + end + end + + # Assign samples to new connections recursively + def assign_samples_to_new_downstream_modules(sources, visited, my_module) + # If samples are already assigned for this module, stop going upstream + if visited.include? (my_module) + return + end + visited << my_module + # Edge case, when module is source or it doesn't have any new input connections + if my_module.inputs.blank? or ( + my_module.inputs.map{|c| c.from} - + sources[my_module.id] + ).empty? + my_module.get_downstream_modules.each do |dm| + new_samples = my_module.samples.select { |el| dm.samples.exclude?(el) } + dm.samples.push(*new_samples) + end + else + my_module.inputs.each do |input| + # Go upstream for new in connections + if sources[my_module.id].exclude?(input.from) + assign_samples_to_new_downstream_modules(input.from) + end + end + end + end + + # Updates positions of modules. + # Input is a map where keys are module IDs, and values are + # hashes like { x: , y: }. + def update_module_positions(positions) + positions.each do |id, pos| + unless MyModule.update(id, x: pos[:x], y: pos[:y]) + raise ActiveRecord::ActiveRecordError + end + end + my_modules.reload + end + + # Normalize module positions in this project. + def normalize_module_positions + # This method normalizes module positions so x-s and y-s + # are all positive + x_diff = (my_modules.collect { |m| m.x }).min + y_diff = (my_modules.collect { |m| m.y }).min + + my_modules.each do |m| + unless + m.update_attribute(:x, m.x - x_diff) and + m.update_attribute(:y, m.y - y_diff) + raise ActiveRecord::ActiveRecordError + end + end + end + + # Recalculate module groups in this project. Input is + # a hash of module ids and their corresponding module names. + def update_module_groups(module_groups, current_user) + require 'rgl/base' + require 'rgl/adjacency' + require 'rgl/connected_components' + + dg = RGL::DirectedAdjacencyGraph[] + group_ids = Set.new + my_modules.where(archived: :false).each do |m| + unless m.my_module_group.blank? + group_ids << m.my_module_group.id + end + unless dg.has_vertex? m.id + dg.add_vertex m.id + end + m.outputs.each do |o| + dg.add_edge m.id, o.to.id + end + end + workflows = [] + dg.to_undirected.each_connected_component { |w| workflows << w } + + # Retrieve maximum allowed module group name + max_length = (MyModuleGroup.validators_on(:name).select { |v| v.class == ActiveModel::Validations::LengthValidator }).first.options[:maximum] + # For each workflow, generate new names + new_index = 1 + wf_names = [] + suffix = I18n.t("my_module_groups.new.suffix") + cut_index = -(suffix.length + 1) + workflows.each do |w| + modules = MyModule.find(w) + + # Get an array of module names + names = [] + modules.each do |m| + names << module_groups.fetch(m.id.to_s, "") + end + names = names.uniq + name = (names.select { |v| v != "" }).join(", ") + + if w.length <= 1 + name = nil + elsif name.blank? + name = I18n.t("my_module_groups.new.name", index: new_index) + new_index += 1 + while MyModuleGroup.find_by(name: name).present? + name = I18n.t("my_module_groups.new.name", index: new_index) + new_index += 1 + end + elsif name.length > max_length + # If length is too long, shorten it + name = name[0..(max_length + cut_index)] + suffix + end + + wf_names << name + end + + # Remove any existing module groups from modules + unless MyModuleGroup.destroy_all(:id => group_ids.to_a) + raise ActiveRecord::ActiveRecordError + end + + # Second, create new groups + workflows.each_with_index do |w, i| + # Single modules are not considered part of any workflow + if w.length > 1 + group = MyModuleGroup.new( + name: wf_names[i], + project: self, + my_modules: MyModule.find(w)) + group.created_by = current_user + group.save! + end + end + + my_module_groups.reload + true + end +end diff --git a/app/models/project_comment.rb b/app/models/project_comment.rb new file mode 100644 index 000000000..151564a63 --- /dev/null +++ b/app/models/project_comment.rb @@ -0,0 +1,7 @@ +class ProjectComment < ActiveRecord::Base + validates :comment, :project, presence: true + validates :project_id, uniqueness: { scope: :comment_id } + + belongs_to :comment, inverse_of: :project_comment + belongs_to :project, inverse_of: :project_comments +end diff --git a/app/models/report.rb b/app/models/report.rb new file mode 100644 index 000000000..245b3bb1a --- /dev/null +++ b/app/models/report.rb @@ -0,0 +1,106 @@ +class Report < ActiveRecord::Base + include SearchableModel + + enum grouped_by: { by_module: 0, by_timestamp: 1 } + + validates :name, + presence: true, + length: { minimum: 2, maximum: 30 }, + uniqueness: { scope: [:user, :project], case_sensitive: false } + validates :description, length: { maximum: 1000 } + validates :grouped_by, presence: true + validates :project, presence: true + validates :user, presence: true + + belongs_to :project, inverse_of: :reports + belongs_to :user, inverse_of: :reports + belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User' + + # Report either has many report elements (if grouped by timestamp), + # or many module elements (if grouped by module) + has_many :report_elements, inverse_of: :report, dependent: :destroy + + def self.search( + user, + include_archived, + query = nil, + attributes = :name, + page = 1 + ) + + project_ids = + Project + .search(user, include_archived, nil, SHOW_ALL_RESULTS) + .select("id") + + new_query = Report + .distinct + .joins("LEFT OUTER JOIN users ON users.id = reports.user_id") + .joins("LEFT OUTER JOIN users AS users_last_modified_by ON users.id = reports.last_modified_by_id") + .where("reports.project_id IN (?)", project_ids) + .where("reports.user_id = (?)", user.id) + .where_attributes_like( + [ + :name, + :description, + "users.full_name", + "users_last_modified_by.full_name" + ], + query + ) + + # Show all results if needed + if page == SHOW_ALL_RESULTS + new_query + else + new_query + .limit(SEARCH_LIMIT) + .offset((page - 1) * SEARCH_LIMIT) + end + end + + def root_elements + (report_elements.order(:position)).select { |el| el.parent.blank? } + end + + # Save the JSON represented contents to this report + # (this action will overwrite any existing report elements) + def save_with_contents(json_contents) + begin + Report.transaction do + #First, save the report itself + save! + + # Secondly, delete existing report elements + report_elements.destroy_all + + # Lastly, iterate through contents + json_contents.each_with_index do |json_el, i| + save_json_element(json_el, i, nil) + end + end + rescue ActiveRecord::ActiveRecordError, ArgumentError + return false + end + return true + end + + private + + # Recursively save a single JSON element + def save_json_element(json_element, index, parent) + el = ReportElement.new + el.position = index + el.report = self + el.parent = parent + el.type_of = json_element["type_of"] + el.sort_order = json_element["sort_order"] + el.set_element_reference(json_element["id"]) + el.save! + if json_element["children"].present? + json_element["children"].each_with_index do |child, i| + save_json_element(child, i, el) + end + end + end +end diff --git a/app/models/report_element.rb b/app/models/report_element.rb new file mode 100644 index 000000000..5fac89144 --- /dev/null +++ b/app/models/report_element.rb @@ -0,0 +1,114 @@ +class ReportElement < ActiveRecord::Base + enum type_of: { + project_header: 0, + my_module: 1, + step: 2, + result_asset: 3, + result_table: 4, + result_text: 5, + my_module_activity: 6, + my_module_samples: 7, + step_checklist: 8, + step_asset: 9, + step_table: 10, + step_comments: 11, + result_comments: 12, + project_activity: 13, # TODO + project_samples: 14 # TODO + } + + # This is only used by certain elements + enum sort_order: { + asc: 0, + desc: 1 + } + + validates :position, presence: true + validates :report, presence: true + validates :type_of, presence: true + validate :has_one_of_referenced_elements + + belongs_to :report, inverse_of: :report_elements + + # Hierarchical structure representation + has_many :children, -> { order(:position) }, class_name: "ReportElement", foreign_key: "parent_id", dependent: :destroy + belongs_to :parent, class_name: "ReportElement" + + # References to various report entities + belongs_to :project, inverse_of: :report_elements + belongs_to :my_module, inverse_of: :report_elements + belongs_to :step, inverse_of: :report_elements + belongs_to :result, inverse_of: :report_elements + belongs_to :checklist, inverse_of: :report_elements + belongs_to :asset, inverse_of: :report_elements + belongs_to :table, inverse_of: :report_elements + + def has_children? + children.length > 0 + end + + def result? + result_asset? or result_table? or result_text? + end + + def comments? + step_comments? or result_comments? + end + + # Get the referenced element (previously, element's type_of must be set) + def element_reference + if project_header? or project_activity? or project_samples? + return project + elsif my_module? or my_module_activity? or my_module_samples? + return my_module + elsif step? or step_comments? + return step + elsif result_asset? or result_table? or result_text? or result_comments? + return result + elsif step_checklist? + return checklist + elsif step_asset? + return asset + elsif step_table? + return table + end + end + + + # Set the element reference (previously, element's type_of must be set) + def set_element_reference(ref_id) + if project_header? or project_activity? or project_samples? + self.project_id = ref_id + elsif my_module? or my_module_activity? or my_module_samples? + self.my_module_id = ref_id + elsif step? or step_comments? + self.step_id = ref_id + elsif result_asset? or result_table? or result_text? or result_comments? + self.result_id = ref_id + elsif step_checklist? + self.checklist_id = ref_id + elsif step_asset? + self.asset_id = ref_id + elsif step_table? + self.table_id = ref_id + end + end + + private + + def has_one_of_referenced_elements + num_of_refs = [ + project, + my_module, + step, + result, + checklist, + asset, + table + ].count { |r| r.present? } + if num_of_refs != 1 + errors.add(:base, "Report element must have exactly one element reference.") + end + end + +end \ No newline at end of file diff --git a/app/models/result.rb b/app/models/result.rb new file mode 100644 index 000000000..f45d05d28 --- /dev/null +++ b/app/models/result.rb @@ -0,0 +1,138 @@ +class Result < ActiveRecord::Base + include ArchivableModel, SearchableModel + + validates :user, :my_module, presence: true + validates :name, + length: { maximum: 50 } + validate :text_or_asset_or_table + + belongs_to :user, inverse_of: :results + belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User' + belongs_to :archived_by, foreign_key: 'archived_by_id', class_name: 'User' + belongs_to :restored_by, foreign_key: 'restored_by_id', class_name: 'User' + belongs_to :my_module, inverse_of: :results + has_one :result_asset, + inverse_of: :result, + dependent: :destroy + has_one :asset, through: :result_asset + has_one :result_table, + inverse_of: :result, + dependent: :destroy + has_one :table, through: :result_table + has_one :result_text, + inverse_of: :result, + dependent: :destroy + has_many :result_comments, + inverse_of: :result, + dependent: :destroy + has_many :comments, through: :result_comments + has_many :report_elements, inverse_of: :result, dependent: :destroy + + accepts_nested_attributes_for :result_text + accepts_nested_attributes_for :asset + accepts_nested_attributes_for :table + + def self.search(user, include_archived, query = nil, page = 1) + module_ids = + MyModule + .search(user, include_archived, nil, SHOW_ALL_RESULTS) + .select("id") + + if query + if include_archived + new_query = Result + .distinct + .joins("LEFT JOIN result_texts ON results.id = result_texts.result_id") + .joins("LEFT JOIN result_tables ON results.id = result_tables.result_id") + .joins("LEFT JOIN tables ON result_tables.table_id = tables.id") + .where("results.my_module_id IN (?)", module_ids) + .where( + "results.name ILIKE ? " + + "OR result_texts.text ILIKE ? " + + "OR tables.data_vector @@ plainto_tsquery(?) ", + "%" + query + "%", + "%" + query + "%", + query + ) + else + new_query = Result + .distinct + .joins("LEFT JOIN result_texts ON results.id = result_texts.result_id") + .joins("LEFT JOIN result_tables ON results.id = result_tables.result_id") + .joins("LEFT JOIN tables ON result_tables.table_id = tables.id") + .where("results.my_module_id IN (?)", module_ids) + .where("results.archived = ?", false) + .where( + "results.name ILIKE ? " + + "OR result_texts.text ILIKE ? " + + "OR tables.data_vector @@ plainto_tsquery(?) ", + "%" + query + "%", + "%" + query + "%", + query + ) + end + + else + if include_archived + new_query = Result + .distinct + .where("results.my_module_id IN (?)", module_ids) + else + new_query = Result + .distinct + .where("results.my_module_id IN (?)", module_ids) + .where("results.archived = ?", false) + end + end + + # Show all results if needed + if page == SHOW_ALL_RESULTS + new_query + else + new_query + .limit(SEARCH_LIMIT) + .offset((page - 1) * SEARCH_LIMIT) + end + end + + def space_taken + is_asset ? result_asset.space_taken : 0 + end + + def last_comments(last_id = 1, per_page = 20) + last_id = 9999999999999 if last_id <= 1 + Comment.joins(:result_comment) + .where(result_comments: {result_id: id}) + .where('comments.id < ?', last_id) + .order(created_at: :desc) + .limit(per_page) + end + + def is_text + self.result_text.present? + end + + def is_table + self.table.present? + end + + def is_asset + self.asset.present? + end + + private + def text_or_asset_or_table + num_of_assigns = 0 + num_of_assigns += result_text.blank? ? 0 : 1 + num_of_assigns += asset.blank? ? 0 : 1 + num_of_assigns += table.blank? ? 0 : 1 + + # Theoretically, we should make sure == 1, not > 1, + # but due to GUI problems this is how it is + if num_of_assigns > 1 + errors.add(:base, "Result can only be instance of text/asset/table.") + elsif num_of_assigns < 1 + errors.add(:base, "Result should be instance of text/asset/table.") + end + end +end diff --git a/app/models/result_asset.rb b/app/models/result_asset.rb new file mode 100644 index 000000000..d683a9dca --- /dev/null +++ b/app/models/result_asset.rb @@ -0,0 +1,11 @@ +class ResultAsset < ActiveRecord::Base + validates :result, :asset, presence: true + + belongs_to :result, inverse_of: :result_asset + belongs_to :asset, inverse_of: :result_asset, + dependent: :destroy + + def space_taken + asset.present? ? asset.estimated_size : 0 + end +end diff --git a/app/models/result_comment.rb b/app/models/result_comment.rb new file mode 100644 index 000000000..35821d417 --- /dev/null +++ b/app/models/result_comment.rb @@ -0,0 +1,9 @@ +class ResultComment < ActiveRecord::Base + validates :result, :comment, presence: true + validates :result_id, uniqueness: { scope: :comment_id } + + belongs_to :result, inverse_of: :result_comments + belongs_to :comment, + inverse_of: :result_comment, + dependent: :destroy +end diff --git a/app/models/result_table.rb b/app/models/result_table.rb new file mode 100644 index 000000000..c512e6fac --- /dev/null +++ b/app/models/result_table.rb @@ -0,0 +1,8 @@ +class ResultTable < ActiveRecord::Base + validates :result, :table, presence: true + + belongs_to :result, inverse_of: :result_table + belongs_to :table, + inverse_of: :result_table, + dependent: :destroy +end diff --git a/app/models/result_text.rb b/app/models/result_text.rb new file mode 100644 index 000000000..a5f778c88 --- /dev/null +++ b/app/models/result_text.rb @@ -0,0 +1,7 @@ +class ResultText < ActiveRecord::Base + validates :text, presence: true + validates :result, presence: true + + belongs_to :result, inverse_of: :result_text + +end diff --git a/app/models/sample.rb b/app/models/sample.rb new file mode 100644 index 000000000..b9d41023d --- /dev/null +++ b/app/models/sample.rb @@ -0,0 +1,62 @@ +class Sample < ActiveRecord::Base + include SearchableModel + + validates :name, + presence: true, + length: { maximum: 50 } + validates :user, :organization, presence: true + + belongs_to :user, inverse_of: :samples + belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User' + belongs_to :organization, inverse_of: :samples + belongs_to :sample_group, inverse_of: :samples + belongs_to :sample_type, inverse_of: :samples + has_many :sample_my_modules, inverse_of: :sample, dependent: :destroy + has_many :my_modules, through: :sample_my_modules + has_many :sample_comments, inverse_of: :sample, dependent: :destroy + has_many :comments, through: :sample_comments, dependent: :destroy + has_many :sample_custom_fields, inverse_of: :sample, dependent: :destroy + has_many :custom_fields, through: :sample_custom_fields + + def self.search( + user, + include_archived, + query = nil, + page = 1 + ) + org_ids = + Organization + .joins(:user_organizations) + .where("user_organizations.user_id = ?", user.id) + .select("id") + .distinct + + new_query = Sample + .distinct + .joins(:user) + .joins("LEFT OUTER JOIN sample_types ON samples.sample_type_id = sample_types.id") + .joins("LEFT OUTER JOIN sample_groups ON samples.sample_group_id = sample_groups.id") + .joins("LEFT OUTER JOIN sample_custom_fields ON samples.id = sample_custom_fields.sample_id") + .where("samples.organization_id IN (?)", org_ids) + .where_attributes_like( + [ + "samples.name", + "sample_types.name", + "sample_groups.name", + "users.full_name", + "sample_custom_fields.value" + ], + query + ) + + # Show all results if needed + if page == SHOW_ALL_RESULTS + new_query + else + new_query + .limit(SEARCH_LIMIT) + .offset((page - 1) * SEARCH_LIMIT) + end + end + +end diff --git a/app/models/sample_comment.rb b/app/models/sample_comment.rb new file mode 100644 index 000000000..489a61c6a --- /dev/null +++ b/app/models/sample_comment.rb @@ -0,0 +1,7 @@ +class SampleComment < ActiveRecord::Base + validates :comment, :sample, presence: true + validates :sample_id, uniqueness: { scope: :comment_id } + + belongs_to :comment, inverse_of: :sample_comment + belongs_to :sample, inverse_of: :sample_comments +end diff --git a/app/models/sample_custom_field.rb b/app/models/sample_custom_field.rb new file mode 100644 index 000000000..630ba645b --- /dev/null +++ b/app/models/sample_custom_field.rb @@ -0,0 +1,9 @@ +class SampleCustomField < ActiveRecord::Base + validates :value, + presence: true, + length: { maximum: 100 } + validates :custom_field, :sample, presence: true + + belongs_to :custom_field, inverse_of: :sample_custom_fields + belongs_to :sample, inverse_of: :sample_custom_fields +end diff --git a/app/models/sample_group.rb b/app/models/sample_group.rb new file mode 100644 index 000000000..82dead63a --- /dev/null +++ b/app/models/sample_group.rb @@ -0,0 +1,14 @@ +class SampleGroup < ActiveRecord::Base + validates :name, + presence: true, + length: { maximum: 50 } + validates :color, + presence: true, + length: { maximum: 7 } + validates :organization, presence: true + + belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User' + belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User' + belongs_to :organization, inverse_of: :sample_groups + has_many :samples, inverse_of: :sample_groups +end diff --git a/app/models/sample_my_module.rb b/app/models/sample_my_module.rb new file mode 100644 index 000000000..fb83af565 --- /dev/null +++ b/app/models/sample_my_module.rb @@ -0,0 +1,14 @@ +class SampleMyModule < ActiveRecord::Base + validates :sample, :my_module, presence: true + + # One sample can only be assigned once to a specific module + validates_uniqueness_of :sample_id, :scope => :my_module_id + + belongs_to :assigned_by, foreign_key: 'assigned_by_id', class_name: 'User' + belongs_to :sample, + inverse_of: :sample_my_modules, + counter_cache: :nr_of_modules_assigned_to + belongs_to :my_module, + inverse_of: :sample_my_modules, + counter_cache: :nr_of_assigned_samples +end diff --git a/app/models/sample_type.rb b/app/models/sample_type.rb new file mode 100644 index 000000000..8e4f83c45 --- /dev/null +++ b/app/models/sample_type.rb @@ -0,0 +1,12 @@ +class SampleType < ActiveRecord::Base + validates :name, + presence: true, + length: { maximum: 50 } + validates :organization, presence: true + + belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User' + belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User' + belongs_to :organization, inverse_of: :sample_types + has_many :samples, inverse_of: :sample_types + +end diff --git a/app/models/step.rb b/app/models/step.rb new file mode 100644 index 000000000..8ac6734e5 --- /dev/null +++ b/app/models/step.rb @@ -0,0 +1,144 @@ +class Step < ActiveRecord::Base + include SearchableModel + + validates :name, presence: true, + length: { maximum: 50 } + validates :description, + length: { maximum: 1000} + validates :position, presence: true + validates :completed, inclusion: { in: [true, false] } + validates :user, :my_module, presence: true + validates :completed_on, presence: true, if: "completed?" + + belongs_to :user, inverse_of: :steps + belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User' + belongs_to :my_module, inverse_of: :steps + has_many :checklists, inverse_of: :step, + dependent: :destroy + has_many :step_comments, inverse_of: :step, + dependent: :destroy + has_many :comments, through: :step_comments + has_many :step_assets, inverse_of: :step, + dependent: :destroy + has_many :assets, through: :step_assets + has_many :step_tables, inverse_of: :step, + dependent: :destroy + has_many :tables, through: :step_tables + has_many :report_elements, inverse_of: :step, + dependent: :destroy + + accepts_nested_attributes_for :checklists, + reject_if: :all_blank, + allow_destroy: true + accepts_nested_attributes_for :assets, + reject_if: :all_blank, + allow_destroy: true + accepts_nested_attributes_for :tables, + reject_if: :all_blank, + allow_destroy: true + + after_destroy :cascade_after_destroy + before_save :set_last_modified_by + + def self.search(user, include_archived, query = nil, page = 1) + module_ids = + MyModule + .search(user, include_archived, nil, SHOW_ALL_RESULTS) + .select("id") + + new_query = Step + .distinct + .where("steps.my_module_id IN (?)", module_ids) + .where_attributes_like(:name, query) + + # Show all results if needed + if page == SHOW_ALL_RESULTS + new_query + else + new_query + .limit(SEARCH_LIMIT) + .offset((page - 1) * SEARCH_LIMIT) + end + end + + def destroy(current_user) + @current_user = current_user + + # Store IDs of comments, assets & tables so they + # can be destroyed in after_destroy + @c_ids = self.comments.collect { |c| c.id } + @a_ids = self.assets.collect { |a| a.id } + @t_ids = self.tables.collect { |t| t.id } + + super() + end + + def last_comments(last_id = 1, per_page = 20) + last_id = 9999999999999 if last_id <= 1 + Comment.joins(:step_comment) + .where(step_comments: {step_id: id}) + .where('comments.id < ?', last_id) + .order(created_at: :desc) + .limit(per_page) + end + + def save(current_user=nil) + @current_user = current_user + super() + end + + def space_taken + st = 0 + assets.each do |asset| + st += asset.estimated_size + end + st + end + + protected + + def cascade_after_destroy + Comment.destroy(@c_ids) + @c_ids = nil + Asset.destroy(@a_ids) + @a_ids = nil + Table.destroy(@t_ids) + @t_ids = nil + + # Generate "delete" activity + Activity.create( + type_of: :destroy_step, + project: my_module.project, + my_module: my_module, + user: @current_user, + message: I18n.t( + "activities.destroy_step", + user: @current_user.full_name, + step: position + 1, + step_name: name + ) + ) + end + + def set_last_modified_by + if @current_user + self.tables.each do |t| + t.created_by ||= @current_user + t.last_modified_by = @current_user if t.changed? + end + self.assets.each do |a| + a.created_by ||= @current_user + a.last_modified_by = @current_user if a.changed? + end + self.checklists.each do |checklist| + checklist.created_by ||= @current_user + checklist.last_modified_by = @current_user if checklist.changed? + checklist.checklist_items.each do |checklist_item| + checklist_item.created_by ||= @current_user + checklist_item.last_modified_by = @current_user if checklist_item.changed? + end + end + end + end +end + diff --git a/app/models/step_asset.rb b/app/models/step_asset.rb new file mode 100644 index 000000000..084b87610 --- /dev/null +++ b/app/models/step_asset.rb @@ -0,0 +1,6 @@ +class StepAsset < ActiveRecord::Base + validates :step, :asset, presence: true + + belongs_to :step, inverse_of: :step_assets + belongs_to :asset, inverse_of: :step_asset +end diff --git a/app/models/step_comment.rb b/app/models/step_comment.rb new file mode 100644 index 000000000..c3154a47c --- /dev/null +++ b/app/models/step_comment.rb @@ -0,0 +1,7 @@ +class StepComment < ActiveRecord::Base + validates :comment, :step, presence: true + validates :step_id, uniqueness: { scope: :comment_id } + + belongs_to :comment, inverse_of: :step_comment + belongs_to :step, inverse_of: :step_comments +end diff --git a/app/models/step_table.rb b/app/models/step_table.rb new file mode 100644 index 000000000..ee1e1eeed --- /dev/null +++ b/app/models/step_table.rb @@ -0,0 +1,6 @@ +class StepTable < ActiveRecord::Base + validates :step, :table, presence: true + + belongs_to :step, inverse_of: :step_tables + belongs_to :table, inverse_of: :step_table +end diff --git a/app/models/table.rb b/app/models/table.rb new file mode 100644 index 000000000..f6643fbd0 --- /dev/null +++ b/app/models/table.rb @@ -0,0 +1,31 @@ +class Table < ActiveRecord::Base + validates :contents, + presence: true, + length: { maximum: 20971520 } + + belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User' + belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User' + has_one :step_table, inverse_of: :table + has_one :step, through: :step_table + + has_one :result_table, inverse_of: :table + has_one :result, through: :result_table + has_many :report_elements, inverse_of: :table, dependent: :destroy + + after_save :update_ts_index + + def contents_utf_8 + contents.present? ? contents.force_encoding(Encoding::UTF_8) : nil + end + + def update_ts_index + if contents_changed? + sql = "UPDATE tables " + + "SET data_vector = " + + "to_tsvector(substring(encode(contents::bytea, 'escape'), 9)) " + + "WHERE id = " + Integer(id).to_s + Table.connection.execute(sql) + end + end +end + diff --git a/app/models/tag.rb b/app/models/tag.rb new file mode 100644 index 000000000..c359d55fb --- /dev/null +++ b/app/models/tag.rb @@ -0,0 +1,36 @@ +class Tag < ActiveRecord::Base + include SearchableModel + + validates :name, + presence: true, + length: { maximum: 50 } + validates :color, presence: true + validates :project, presence: true + + belongs_to :created_by, foreign_key: 'created_by_id', class_name: 'User' + belongs_to :last_modified_by, foreign_key: 'last_modified_by_id', class_name: 'User' + belongs_to :project + has_many :my_module_tags, inverse_of: :tag, :dependent => :destroy + has_many :my_modules, through: :my_module_tags + + def self.search(user, include_archived, query = nil, page = 1) + project_ids = + Project + .search(user, include_archived, nil, SHOW_ALL_RESULTS) + .select("id") + + new_query = Tag + .distinct + .where("tags.project_id IN (?)", project_ids) + .where_attributes_like(:name, query) + + # Show all results if needed + if page == SHOW_ALL_RESULTS + new_query + else + new_query + .limit(SEARCH_LIMIT) + .offset((page - 1) * SEARCH_LIMIT) + end + end +end diff --git a/app/models/temp_file.rb b/app/models/temp_file.rb new file mode 100644 index 000000000..90fbbcc3d --- /dev/null +++ b/app/models/temp_file.rb @@ -0,0 +1,6 @@ +class TempFile < ActiveRecord::Base + validates :session_id, presence: true + + has_attached_file :file + do_not_validate_attachment_file_type :file +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 000000000..3bb2eddee --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,211 @@ +class User < ActiveRecord::Base + include SearchableModel + + devise :invitable, :confirmable, :database_authenticatable, :registerable, :async, + :recoverable, :rememberable, :trackable, :validatable, stretches: 10 + has_attached_file :avatar, :styles => { + :medium => "300x300>", + :thumb => "100x100>", + :icon => "40x40>", + :icon_small => "30x30>" + }, + :default_url => "/images/:style/missing.png" + + enum tutorial_status: { + no_tutorial_done: 0, + intro_tutorial_done: 1 + } + + validates :full_name, presence: true, length: { maximum: 50 } + validates :initials, presence: true, length: { minimum: 1, maximum: 4 } + validates_attachment :avatar, + :content_type => { :content_type => ["image/jpeg", "image/png"] }, + :size => { :less_than => 200.kilobytes } + validates :time_zone, presence: true + validate :time_zone_check + + # Relations + has_many :user_organizations, inverse_of: :user + has_many :organizations, through: :user_organizations + has_many :user_projects, inverse_of: :user + has_many :projects, through: :user_projects + has_many :user_my_modules, inverse_of: :user + has_many :my_modules, through: :user_my_modules + has_many :comments, inverse_of: :user + has_many :activities, inverse_of: :user + has_many :results, inverse_of: :user + has_many :samples, inverse_of: :user + has_many :steps, inverse_of: :user + has_many :custom_fields, inverse_of: :user + has_many :reports, inverse_of: :user + has_many :created_assets, class_name: 'Asset', foreign_key: 'created_by_id' + has_many :modified_assets, class_name: 'Asset', foreign_key: 'last_modified_by_id' + has_many :created_checklists, class_name: 'Checklist', foreign_key: 'created_by_id' + has_many :modified_checklists, class_name: 'Checklist', foreign_key: 'last_modified_by_id' + has_many :created_checklist_items, class_name: 'ChecklistItem', foreign_key: 'created_by_id' + has_many :modified_checklist_items, class_name: 'ChecklistItem', foreign_key: 'last_modified_by_id' + has_many :modified_comments, class_name: 'Comment', foreign_key: 'last_modified_by_id' + has_many :modified_custom_fields, class_name: 'CustomField', foreign_key: 'last_modified_by_id' + has_many :created_my_module_groups, class_name: 'MyModuleGroup', foreign_key: 'created_by_id' + has_many :created_my_module_tags, class_name: 'MyModuleTag', foreign_key: 'created_by_id' + has_many :created_my_modules, class_name: 'MyModule', foreign_key: 'created_by_id' + has_many :modified_my_modules, class_name: 'MyModule', foreign_key: 'last_modified_by_id' + has_many :archived_my_modules, class_name: 'MyModule', foreign_key: 'archived_by_id' + has_many :restored_my_modules, class_name: 'MyModule', foreign_key: 'restored_by_id' + has_many :created_organizations, class_name: 'Organization', foreign_key: 'created_by_id' + has_many :modified_organizations, class_name: 'Organization', foreign_key: 'last_modified_by_id' + has_many :created_projects, class_name: 'Project', foreign_key: 'created_by_id' + has_many :modified_projects, class_name: 'Project', foreign_key: 'last_modified_by_id' + has_many :archived_projects, class_name: 'Project', foreign_key: 'archived_by_id' + has_many :restored_projects, class_name: 'Project', foreign_key: 'restored_by_id' + has_many :modified_reports, class_name: 'Report', foreign_key: 'last_modified_by_id' + has_many :modified_results, class_name: 'Result', foreign_key: 'modified_by_id' + has_many :archived_results, class_name: 'Result', foreign_key: 'archived_by_id' + has_many :restored_results, class_name: 'Result', foreign_key: 'restored_by_id' + has_many :created_sample_groups, class_name: 'SampleGroup', foreign_key: 'created_by_id' + has_many :modified_sample_groups, class_name: 'SampleGroup', foreign_key: 'last_modified_by_id' + has_many :assigned_sample_my_modules, class_name: 'SampleMyModule', foreign_key: 'assigned_by_id' + has_many :created_sample_types, class_name: 'SampleType', foreign_key: 'created_by_id' + has_many :modified_sample_types, class_name: 'SampleType', foreign_key: 'last_modified_by_id' + has_many :modified_samples, class_name: 'Sample', foreign_key: 'last_modified_by_id' + has_many :modified_steps, class_name: 'Step', foreign_key: 'modified_by_id' + has_many :created_tables, class_name: 'Table', foreign_key: 'created_by_id' + has_many :modified_tables, class_name: 'Table', foreign_key: 'last_modified_by_id' + has_many :created_tags, class_name: 'Tag', foreign_key: 'created_by_id' + has_many :modified_tags, class_name: 'Tag', foreign_key: 'last_modified_by_id' + has_many :assigned_user_my_modules, class_name: 'UserMyModule', foreign_key: 'assigned_by_id' + has_many :assigned_user_organizations, class_name: 'UserOrganization', foreign_key: 'assigned_by_id' + has_many :assigned_user_projects, class_name: 'UserProject', foreign_key: 'assigned_by_id' + + # Prevents repetition of errors after validation (e.g. if file_size + # exceeds limits, 2 same errors will be shown) + after_validation :clean_paperclip_errors + + def name + full_name + end + + def name=(name) + full_name = name + end + + # Search all active users for username & email. Can + # also specify which organization to ignore. + def self.search( + active_only, + query = nil, + organization_to_ignore = nil + ) + result = User.all + + if active_only + result = result.where.not(confirmed_at: nil) + end + + if organization_to_ignore.present? + ignored_ids = + UserOrganization + .select(:user_id) + .where(organization_id: organization_to_ignore.id) + result = + result + .where("users.id NOT IN (?)", ignored_ids) + end + + result + .where_attributes_like([:full_name, :email], query) + .distinct + end + + def empty_avatar(name, size) + file_ext = name.split(".").last + self.avatar_file_name = name + self.avatar_content_type = Rack::Mime.mime_type(".#{file_ext}") + self.avatar_file_size = size.to_i + end + + def clean_paperclip_errors + errors.delete(:avatar) + end + + # Whether user is active (= confirmed) or not + def active? + confirmed_at.present? + end + + def active_status_str + if active? + I18n.t("users.enums.status.active") + else + I18n.t("users.enums.status.pending") + end + end + + def projects_by_orgs(org_id = 0, sort_by = nil, archived = false) + archived = archived ? true : false + query = Project.all.joins(:user_projects); + sql = "projects.organization_id IN " + + "(SELECT DISTINCT organization_id FROM user_organizations WHERE user_organizations.user_id = ?) " + + "AND (projects.visibility=1 OR user_projects.user_id=?) " + + "AND projects.archived = ? "; + + case sort_by + when "old" + sort = {created_at: :asc} + when "atoz" + sort = {name: :asc} + when "ztoa" + sort = {name: :desc} + else + sort = {created_at: :desc} + end + + if org_id > 0 + result = query + .where("projects.organization_id = ?", org_id) + .where(sql, id, id, archived) + .order(sort) + .distinct + .group_by { |project| project.organization } + else + result = query + .where(sql, id, id, archived) + .order(sort) + .distinct + .group_by { |project| project.organization } + end + result || [] + end + + # Finds all activities of user that is assigned to project. If user + # is not an owner of the project, user must be also assigned to + # module. + def last_activities(last_activity_id = nil, per_page = 10) + # TODO replace with some kind of Infinity value + last_activity_id = 999999999999999999999999 if last_activity_id < 1 + Activity + .joins(project: :user_projects) + .joins("LEFT OUTER JOIN my_modules ON activities.my_module_id = my_modules.id") + .joins("LEFT OUTER JOIN user_my_modules ON my_modules.id = user_my_modules.my_module_id") + .where('activities.id < ?', last_activity_id) + .where(user_projects: { user_id: self }) + .where( + 'activities.my_module_id IS NULL OR ' + + 'user_projects.role = 0 OR ' + + 'user_my_modules.user_id = ?', + id + ) + .order(created_at: :desc) + .limit(per_page) + .uniq + end + + protected + + def time_zone_check + if time_zone.nil? or ActiveSupport::TimeZone.new(time_zone).nil? + errors.add(:time_zone) + end + end +end + diff --git a/app/models/user_my_module.rb b/app/models/user_my_module.rb new file mode 100644 index 000000000..cdacef178 --- /dev/null +++ b/app/models/user_my_module.rb @@ -0,0 +1,7 @@ +class UserMyModule < ActiveRecord::Base + validates :user, :my_module, presence: true + + belongs_to :user, inverse_of: :user_my_modules + belongs_to :assigned_by, foreign_key: 'assigned_by_id', class_name: 'User' + belongs_to :my_module, inverse_of: :user_my_modules +end diff --git a/app/models/user_organization.rb b/app/models/user_organization.rb new file mode 100644 index 000000000..f5edaa5d9 --- /dev/null +++ b/app/models/user_organization.rb @@ -0,0 +1,28 @@ +class UserOrganization < ActiveRecord::Base + enum role: { guest: 0, normal_user: 1, admin: 2 } + + validates :role, presence: true + validates :user, presence: true + validates :organization, presence: true + + belongs_to :user, inverse_of: :user_organizations + belongs_to :assigned_by, foreign_key: 'assigned_by_id', class_name: 'User' + belongs_to :organization, inverse_of: :user_organizations + + before_destroy :destroy_associations + + def role_str + I18n.t("user_organizations.enums.role.#{role.to_s}") + end + + def destroy_associations + # Destroy the user from all organization's projects + organization.projects.each do |project| + up2 = (project.user_projects.select { |up| up.user == self.user }).first + if up2.present? + up2.destroy + end + end + end + +end diff --git a/app/models/user_project.rb b/app/models/user_project.rb new file mode 100644 index 000000000..e839a92c9 --- /dev/null +++ b/app/models/user_project.rb @@ -0,0 +1,28 @@ +class UserProject < ActiveRecord::Base + enum role: { owner: 0, normal_user: 1, technician: 2, viewer: 3 } + + validates :role, presence: true + validates :user, presence: true + validates :project, presence: true + + belongs_to :user, inverse_of: :user_projects + belongs_to :assigned_by, foreign_key: 'assigned_by_id', class_name: 'User' + belongs_to :project, inverse_of: :user_projects + + before_destroy :destroy_associations + + def role_str + I18n.t("user_projects.enums.role.#{role.to_s}") + end + + def destroy_associations + # Destroy the user from all project's modules + project.my_modules.each do |my_module| + um2 = (my_module.user_my_modules.select { |um| um.user == self.user }).first + if um2.present? + um2.destroy + end + end + end + +end diff --git a/app/utilities/first_time_data_generator.rb b/app/utilities/first_time_data_generator.rb new file mode 100644 index 000000000..0e4ec5586 --- /dev/null +++ b/app/utilities/first_time_data_generator.rb @@ -0,0 +1,593 @@ +module FirstTimeDataGenerator + + # Create data for tutorial for new users + def seed_demo_data user + @user = user + + # First organization that this user created + # should contain the "intro" project + org = user + .organizations + .where(created_by: user) + .order(created_at: :asc) + .first + + # If private private organization does not exist, + # there was something wrong with user creation. + # Do nothing + return unless org + + # Create sample types + SampleType.create( + name: "Potato leaves", + organization: org + ) + + SampleType.create( + name: "Tea leaves", + organization: org + ) + + SampleType.create( + name: "Potato bug", + organization: org + ) + + SampleGroup.create( + name: "Fodder", + organization: org, + color: "#159B5E" + ) + + SampleGroup.create( + name: "Nutrient", + organization: org, + color: "#6C159E" + ) + + SampleGroup.create( + name: "Seed", + organization: org, + color: "#FF4500" + ) + + samples = [] + # Generate random sample names start + # and put it on the beginning of 5 samples + sample_name = (0...3).map{65.+(rand(26)).chr}.join << '/' + for i in 1..5 + samples << Sample.create( + name: sample_name + i.to_s, + organization: org, + user: user, + sample_type: rand < 0.8 ? pluck_random(org.sample_types) : nil, + sample_group: rand < 0.8 ? pluck_random(org.sample_groups) : nil + ) + end + + sample_name = (0...3).map{65.+(rand(26)).chr}.join << '/' + for i in 1..5 + samples << Sample.create( + name: sample_name + i.to_s, + organization: org, + user: user, + sample_type: rand < 0.8 ? pluck_random(org.sample_types) : nil, + sample_group: rand < 0.8 ? pluck_random(org.sample_groups) : nil + ) + end + + project = Project.create( + visibility: 1, + name: "Demo project - qPCR", + due_date: nil, + organization: org, + created_by: user, + created_at: Time.now - 1.week, + last_modified_by: user, + archived: false, + archived_by: nil, + archived_on: nil, + restored_by: nil, + restored_on: nil + ) + + # Automatically assign project author onto project + UserProject.create( + user: user, + project: project, + role: 0, + created_at: Time.now - 1.week + ) + + # Activity for creating project + Activity.create( + type_of: :create_project, + user: user, + project: project, + message: I18n.t( + "activities.create_project", + user: user.full_name, + project: project.name + ), + created_at: project.created_at + ) + + # Add a comment + project.comments << Comment.create( + user: user, + message: "I've created a demo project", + created_at: Time.now - 1.week + ) + + # Create a module group + my_module_group = MyModuleGroup.create( + name: "Potato qPCR workflow", + project: project + ) + + # Create project modules + my_modules = [] + my_module_names = [ + "Experiment design", + "Sampling biological material", + "RNA isolation", + "RNA quality & quantity - BIOANALYSER", + "Reverse transcription", + "qPCR", + "Data quality control", + "Data analysis - ddCq" + ] + + qpcr_module_description = "PCR is a method where an enzyme + (thermostable DNA polymerase, originally isolated in 1960s + from bacterium Thermus aquaticus, growing in hot lakes of + Yellowstone park, USA) amplifies a short specific part of + the template DNA (amplicon) in cycles. In every cycle the + number of short specific sections of DNA is doubled, leading + to an exponential amplification of targets. More on how + conventional PCR works can be found here." + + my_module_names.each_with_index do |name, i| + my_module = MyModule.create( + name: name, + created_by: user, + created_at: Time.now - rand(6).days, + due_date: Time.now + (2 * i + 1).weeks, + description: i == 5 ? qpcr_module_description : nil, + x: i < 4 ? i % 4 : 7 - i, + y: i/4, + project: project, + workflow_order: i, + my_module_group: my_module_group + ) + + my_modules << my_module + + # Add connections between current and previous module + if i > 0 + Connection.create( + input_id: my_module.id, + output_id: my_modules[i-1].id + ) + end + + # Create module activity + Activity.create( + type_of: :create_module, + user: user, + project: project, + my_module: my_module, + message: I18n.t( + "activities.create_module", + user: user.full_name, + module: my_module.name + ), + created_at: my_module.created_at + ) + + UserMyModule.create( + user: user, + my_module: my_module, + assigned_by: user, + created_at: my_module.created_at + 2.minutes + ) + Activity.create( + type_of: :assign_user_to_module, + user: user, + project: project, + my_module: my_module, + message: I18n.t( + "activities.assign_user_to_module", + assigned_user: user.full_name, + module: my_module.name, + assigned_by_user: user.full_name + ), + created_at: my_module.created_at + 2.minutes + ) + end + + # Create an archived module + archived_module = MyModule.create( + name: "Data analysis - Pfaffl method", + created_by: user, + created_at: Time.now - rand(4..6).days, + due_date: Time.now + 1.week, + description: nil, + x: -1, + y: -1, + project: project, + workflow_order: -1, + my_module_group: nil, + archived: true, + archived_on: Time.now - rand(3).days, + archived_by: user + ) + + # Activity for creating archived module + Activity.create( + type_of: :create_module, + user: user, + project: project, + my_module: archived_module, + message: I18n.t( + "activities.create_module", + user: user.full_name, + module: archived_module.name + ), + created_at: archived_module.created_at + ) + + # Activity for archiving archived module + Activity.create( + type_of: :archive_module, + user: user, + project: project, + my_module: archived_module, + message: I18n.t( + "activities.archive_module", + user: user.full_name, + module: archived_module.name + ), + created_at: archived_module.archived_on + ) + + # Assign new user to archived module + UserMyModule.create( + user: user, + my_module: archived_module, + assigned_by: user, + created_at: archived_module.created_at + 2.minutes + ) + Activity.create( + type_of: :assign_user_to_module, + user: user, + project: project, + my_module: archived_module, + message: I18n.t( + "activities.assign_user_to_module", + assigned_user: user.full_name, + module: archived_module.name, + assigned_by_user: user.full_name + ), + created_at: archived_module.created_at + 2.minutes + ) + + # Assign 4 samples to modules + samples_to_assign = [] + taken_sample_ids = [] + for _ in 1..4 + begin + sample = samples.sample + end while sample.id.in? taken_sample_ids + taken_sample_ids << sample.id + samples_to_assign << sample + end + + + my_modules[1].get_downstream_modules.each do |mm| + samples_to_assign.each do |s| + SampleMyModule.create( + sample: s, + my_module: mm + ) + end + end + + # Add comments to modules + my_modules[0].comments << Comment.create( + user: user, + message: "We should have a meeting to discuss sampling parametrs soon.", + created_at: my_modules[0].created_at + 1.day + ) + my_modules[0].comments << Comment.create( + user: user, + message: "I agree." + ) + + my_modules[1].comments << Comment.create( + user: user, + message: "The samples have arrived.", + created_at: my_modules[0].created_at + 2.days + ) + + my_modules[2].comments << Comment.create( + user: user, + message: "Due date has been postponed for a day.", + created_at: my_modules[0].created_at + 1.days + ) + + my_modules[4].comments << Comment.create( + user: user, + message: "Please show Steve the RT procedure.", + created_at: my_modules[0].created_at + 2.days + ) + + my_modules[5].comments << Comment.create( + user: user, + message: "The results must be very definitive.", + created_at: my_modules[0].created_at + 3.days + ) + + my_modules[7].comments << Comment.create( + user: user, + message: "The due date here is flexible.", + created_at: my_modules[0].created_at + 3.days + ) + + + # Create module steps + # Module 1 + module_step_names = [ + "Gene expression" + ] + module_step_descriptions = [ + "Compare response of PVYNTN, cab4 and PR1 genes in mock/virus inoculated potatoes & in time" + ] + generate_module_steps(my_modules[0], module_step_names, module_step_descriptions) + + # Module 2 + module_step_names = [ + "Inoculation of potatoes", + "Collection of potatoes", + "Store samples" + ] + module_step_descriptions = [ + "50% of samples should be mock inoculated while other 50% with PVY NTN virus.", + "50% of PVYNTN inoculated potatos and 50% of Mock inoculated potatos collect 1 day post inocullation while other halph of samples collect 6 days post inoculation.", + "Collect samples in 2ml tubes and put them in liquid nitrogen and store at 80°C." + ] + generate_module_steps(my_modules[1], module_step_names, module_step_descriptions) + + # Module 3 + module_step_names = [ + "Homogenization of the material", + "Isolation of RNA with RNeasy plant mini kit" + ] + module_step_descriptions = [ + " Use tissue lyser: 1 min on step 3.", + nil + ] + generate_module_steps(my_modules[2], module_step_names, module_step_descriptions) + + # Module 4 + module_step_names = [ + "Use Nano chip for testing RNA integrity" + ] + module_step_descriptions = [ + nil + ] + generate_module_steps(my_modules[3], module_step_names, module_step_descriptions) + + # Module 5 + module_step_names = [ + "RNA denaturation", + "Prepare mastermix for RT", + "RT reaction" + ] + module_step_descriptions = [ + "1 ug of RNA denature at 80°C for 5 min --> ice", + "High Capacity cDNA Reverse Transcription Kit (Applied Biosystems)", + "25°C for 10 min 37°C for 2 h" + ] + generate_module_steps(my_modules[4], module_step_names, module_step_descriptions) + + # Module 6 + module_step_names = [ + "Sample preparation", + "Reaction setup", + "Setup of the 96 plate" + ] + module_step_descriptions = [ + nil, + nil, + "Template of the 96-well plate" + ] + generate_module_steps(my_modules[5], module_step_names, module_step_descriptions) + + # Module 7 + module_step_names = [ + "Check negative controls NTC", + "Eliminate results that have positive NTCs" + ] + module_step_descriptions = [ + "They have to be negative when using TaqMan assays. If they are positive when using SYBR assays check also melitng curve where signal comes from. - if it is primer dimer result is negative - If it is specific signal it is positive", + "And repeat procedure" + ] + generate_module_steps(my_modules[6], module_step_names, module_step_descriptions) + + # Module 8 + module_step_names = [ + "Template for ddCq analysis" + ] + module_step_descriptions = [ + nil + ] + generate_module_steps(my_modules[7], module_step_names, module_step_descriptions) + + + # Add a text result + temp_result = Result.new( + name: "Number of samples", + my_module: my_modules[1], + user: user, + created_at: my_modules[1].created_at + rand(20).hours, + ) + temp_text = "There are many biological replicates we harvested for each type of sample (code-names):\n\n" + samples_to_assign.each do |s| + temp_text << "* #{s.name}\n\n" + end + temp_result.result_text = ResultText.new( + text: temp_text + ) + + temp_result.save + + # Create result activity + Activity.create( + type_of: :add_result, + project: project, + my_module: my_modules[1], + user: user, + created_at: temp_result.created_at, + message: I18n.t( + "activities.add_text_result", + user: user.full_name, + result: temp_result.name + ) + ) + + # Add a hard-coded table result + temp_result = Result.new( + name: "Sample distribution on the plate", + my_module: my_modules[5], + user: user, + created_at: my_modules[5].created_at + rand(20).hours, + ) + temp_result.table = Table.new( + created_by: user, + contents: { data: [ + ["#{samples_to_assign[0].name} (100x)","","#{samples_to_assign[0].name} (100x)","","#{samples_to_assign[0].name} (100x)","","#{samples_to_assign[0].name} (100x)","","#{samples_to_assign[0].name} (100x)","","#{samples_to_assign[0].name} (100x)","","#{samples_to_assign[0].name} (100x)","","#{samples_to_assign[0].name} (100x)","","","","Mix smpl (10x)","","Mix smpl (10x)",""], + ["#{samples_to_assign[0].name} (1000x)","","#{samples_to_assign[0].name} (1000x)","","#{samples_to_assign[0].name} (1000x)","","#{samples_to_assign[0].name} (1000x)","","#{samples_to_assign[0].name} (1000x)","","#{samples_to_assign[0].name} (1000x)","","#{samples_to_assign[0].name} (1000x)","","#{samples_to_assign[0].name} (1000x)","","","","Mix smpl (100x)","","Mix smpl (100x)",""], + ["#{samples_to_assign[1].name} (100x)","","#{samples_to_assign[1].name} (100x)","","#{samples_to_assign[1].name} (100x)","","#{samples_to_assign[1].name} (100x)","","#{samples_to_assign[1].name} (100x)","","#{samples_to_assign[1].name} (100x)","","#{samples_to_assign[1].name} (100x)","","#{samples_to_assign[1].name} (100x)","","","","Mix smpl (1000x)","","Mix smpl (1000x)",""], + ["#{samples_to_assign[1].name} (1000x)","","#{samples_to_assign[1].name} (1000x)","","#{samples_to_assign[1].name} (1000x)","","#{samples_to_assign[1].name} (1000x)","","#{samples_to_assign[1].name} (1000x)","","#{samples_to_assign[1].name} (1000x)","","#{samples_to_assign[1].name} (1000x)","","#{samples_to_assign[1].name} (1000x)","","","","Mix smpl (10000x)","","Mix smpl (10000x)",""], + ["#{samples_to_assign[2].name} (100x)","","#{samples_to_assign[2].name} (100x)","","#{samples_to_assign[2].name} (100x)","","#{samples_to_assign[2].name} (100x)","","#{samples_to_assign[2].name} (100x)","","#{samples_to_assign[2].name} (100x)","","#{samples_to_assign[2].name} (100x)","","#{samples_to_assign[2].name} (100x)","","","","NTC1","NTC2","#{samples_to_assign[2].name} (100x)",""], + ["#{samples_to_assign[2].name} (1000x)","","#{samples_to_assign[2].name} (1000x)","","#{samples_to_assign[2].name} (1000x)","","#{samples_to_assign[2].name} (1000x)","","#{samples_to_assign[2].name} (1000x)","","#{samples_to_assign[2].name} (1000x)","","#{samples_to_assign[2].name} (1000x)","","#{samples_to_assign[2].name} (1000x)","","","","NTC1","NTC2","#{samples_to_assign[2].name} (1000x)",""], + ["#{samples_to_assign[3].name} (100x)","","#{samples_to_assign[3].name} (100x)","","#{samples_to_assign[3].name} (100x)","","#{samples_to_assign[3].name} (100x)","","#{samples_to_assign[3].name} (100x)","","#{samples_to_assign[3].name} (100x)","","#{samples_to_assign[3].name} (100x)","","#{samples_to_assign[3].name} (100x)","","","","NTC1","NTC2","#{samples_to_assign[3].name} (100x)",""], + ["#{samples_to_assign[3].name} (1000x)","","#{samples_to_assign[3].name} (1000x)","","#{samples_to_assign[3].name} (1000x)","","#{samples_to_assign[3].name} (1000x)","","#{samples_to_assign[3].name} (1000x)","","#{samples_to_assign[3].name} (1000x)","","#{samples_to_assign[3].name} (1000x)","","#{samples_to_assign[3].name} (1000x)","","","","NTC1","NTC2","#{samples_to_assign[3].name} (1000x)",""] + ]}.to_json + ) + + temp_result.save + + # Create result activity + Activity.create( + type_of: :add_result, + project: project, + my_module: my_modules[5], + user: user, + created_at: temp_result.created_at, + message: I18n.t( + "activities.add_table_result", + user: user.full_name, + result: temp_result.name + ) + ) + + # Lastly, create cookie with according ids + # so tutorial steps can be properly positioned + return JSON.generate([ + organization: org.id, + project: project.id, + qpcr_module: my_modules[5].id + ]) + end + + # WARNING: This only works on PostgreSQL + def pluck_random(scope) + scope.order("RANDOM()").first + end + + # Create steps for given module + def generate_module_steps(my_module, step_names, step_descriptions) + step_names.each_with_index do |name, i| + created_at = my_module.created_at + rand(1..5).days + completed = rand <= 0.3 + completed_on = completed ? + created_at + rand(10).hours : nil + + step = Step.create( + created_at: created_at, + name: name, + description: step_descriptions[i], + position: i, + completed: completed, + user: @user, + my_module: my_module, + completed_on: completed_on + ) + + # Create activity + Activity.create( + type_of: :create_step, + project: my_module.project, + my_module: my_module, + user: step.user, + created_at: created_at, + message: I18n.t( + "activities.create_step", + user: step.user.full_name, + step: i, + step_name: step.name + ) + ) + if completed then + Activity.create( + type_of: :complete_step, + project: my_module.project, + my_module: my_module, + user: step.user, + created_at: completed_on, + message: I18n.t( + "activities.complete_step", + user: step.user.full_name, + step: i+1, + step_name: step.name, + completed: my_module.completed_steps.count, + all: i+1 + ) + ) + + # Also add random comments to completed steps + if rand < 0.3 + polite_comment = "This looks well." + elsif rand < 0.4 + polite_comment = "Seems satisfactory." + elsif rand < 0.4 + polite_comment = "Try a bit harder next time." + end + if polite_comment + commented_on = completed_on + rand(500).minutes + step.comments << Comment.create( + user: @user, + message: polite_comment, + created_at: commented_on + ) + Activity.create( + type_of: :add_comment_to_step, + project: my_module.project, + my_module: my_module, + user: @user, + created_at: commented_on, + message: I18n.t( + "activities.add_comment_to_step", + user: @user.full_name, + step: i, + step_name: step.name + ) + ) + end + end + end + end + +end \ No newline at end of file diff --git a/app/utilities/users_generator.rb b/app/utilities/users_generator.rb new file mode 100644 index 000000000..aa950bbb3 --- /dev/null +++ b/app/utilities/users_generator.rb @@ -0,0 +1,88 @@ +module UsersGenerator + + # Simply validate the user with the given data, + # and return an array of errors (which is 0-length + # if user is valid) + def validate_user( + full_name, + email, + password + ) + nu = User.new({ + full_name: full_name, + initials: get_user_initials(full_name), + email: email, + password: password + }) + nu.validate + nu.errors + end + + # If confirmed == true, the user is automatically confirmed; + # otherwise, sciNote sends the "confirmation" email to the user + # If private_org_name == nil, private organization is not created. + def create_user( + full_name, + email, + password, + confirmed, + private_org_name, + org_ids) + nu = User.new({ + full_name: full_name, + initials: get_user_initials(full_name), + email: email, + password: password, + password_confirmation: password + }) + if confirmed then + nu.confirmed_at = Time.now + end + nu.save + + # TODO: If user is not confirmed, maybe additional email + # needs to be sent with his/her password & email? + + # Create user's own organization of needed + if private_org_name.present? then + create_private_user_organization(nu, private_org_name) + end + + # Assign user to additional organizations + org_ids.each do |org_id| + org = Organization.find_by_id(org_id) + if org.present? + UserOrganization.create({ user: nu, organization: org, role: :admin }) + end + end + + nu.reload + return nu + end + + def create_private_user_organization(user, private_org_name) + no = Organization.create({ name: private_org_name, created_by: user }) + UserOrganization.create({ user: user, organization: no, role: :admin }) + end + + def print_user(user, password) + puts "USER ##{user.id}" + puts " Full name: #{user.full_name}" + puts " Initials: #{user.initials}" + puts " Email: #{user.email}" + puts " Password: #{password}" + puts " Confirmed at: #{user.confirmed_at}" + orgs = user.organizations.collect{ |org| org.name }.join(", ") + puts " Member of organizations: #{orgs}" + end + + def generate_user_password + require 'securerandom' + SecureRandom.hex(5) + end + + def get_user_initials(full_name) + full_name.split(" ").collect{ |n| n.capitalize[0] }.join[0..3] + end + +end \ No newline at end of file diff --git a/app/views/activities/_activity.html.erb b/app/views/activities/_activity.html.erb new file mode 100644 index 000000000..552767b3b --- /dev/null +++ b/app/views/activities/_activity.html.erb @@ -0,0 +1,11 @@ +
  • + + <%= l activity.created_at, format: '%H:%M' %> + + + <%= activity.message.html_safe %> + <% if activity.my_module %> + [project: <%= activity.my_module.project.name %>, module: <%= activity.my_module.name %>] + <% end %> + +
  • \ No newline at end of file diff --git a/app/views/activities/_index.html.erb b/app/views/activities/_index.html.erb new file mode 100644 index 000000000..a167190e7 --- /dev/null +++ b/app/views/activities/_index.html.erb @@ -0,0 +1,13 @@ +
      + <% if @activities.length == 0 then %> +
    • <%= t'activities.index.no_activities' %>
    • + <% else %> + <%= render 'activities/list.html.erb', activities: @activities, hide_today: hide_today, day: @day %> + <% end %> + <% if @last_activity_id < 1 and @activities.length == @per_page %> +
    • + + <%= t'activities.index.more_activities' %> +
    • + <% end %> +
    diff --git a/app/views/activities/_list.html.erb b/app/views/activities/_list.html.erb new file mode 100644 index 000000000..287fad4fb --- /dev/null +++ b/app/views/activities/_list.html.erb @@ -0,0 +1,23 @@ +<% + current_day = DateTime.current.strftime('%j').to_i +%> +<% if !hide_today and activities.count > 0 and activities[0].created_at.strftime('%j').to_i == current_day %> +
  • + + <%=t "activities.index.today" %> + +
  • +<% end %> +<% activities.each do |activity| %> + <% activity_day = activity.created_at.strftime('%j').to_i %> + + <% if activity_day < current_day and activity_day < day %> + <% day = activity.created_at.strftime('%j').to_i %> +
  • + + <%= activity.created_at.strftime('%d.%m.%Y') %> + +
  • + <% end %> + <%= render 'activities/activity.html.erb', activity: activity %> +<% end %> diff --git a/app/views/activities/index.html.erb b/app/views/activities/index.html.erb new file mode 100644 index 000000000..70399ed5a --- /dev/null +++ b/app/views/activities/index.html.erb @@ -0,0 +1,2 @@ +

    Activities#index

    +

    Find me in app/views/activities/index.html.erb

    diff --git a/app/views/canvas/_edit.html.erb b/app/views/canvas/_edit.html.erb new file mode 100644 index 000000000..9b9770e1d --- /dev/null +++ b/app/views/canvas/_edit.html.erb @@ -0,0 +1,88 @@ +
    " + data-can-edit-modules="<%= can_edit_modules(@project) ? "yes" : "no" %>" + data-can-edit-module-groups="<%= can_edit_module_groups(@project) ? "yes" : "no" %>" + data-can-clone-modules="<%= can_clone_modules(@project) ? "yes" : "no" %>" + data-can-delete-modules="<%= can_archive_modules(@project) ? "yes" : "no" %>" + data-can-reposition-modules="<%= can_reposition_modules(@project) ? "yes" : "no" %>" + data-can-edit-connections="<%= can_edit_connections(@project) ? "yes" : "no" %>" + data-unsaved-work-text="<%=t "projects.canvas.edit.unsaved_work" %>" +> + <%= bootstrap_form_tag url: canvas_project_url, method: "post" do |f| %> +
    + <%= f.submit class: "btn btn-primary", id: "canvas-save" do %> + <%= t("projects.canvas.edit.save_short") %> + + <% end %> + <%= link_to canvas_project_path(@project), type: "button", class: "btn btn-default cancel-edit-canvas" do %> + +   + <% end %> +
    + <% if can_create_modules(@project) %> + <%=link_to "", type: "button", class: "btn btn-default", id: "canvas-new-module" do %> + + + <%= t("projects.canvas.edit.new_module") %> + + + + + <%= t("projects.canvas.edit.new_module") %> + + <% end %> + <% end %> + <%= hidden_field_tag 'connections', '' %> + <%= hidden_field_tag 'positions', '' %> + <%= hidden_field_tag 'add', '' %> + <%= hidden_field_tag 'add-names', '' %> + <%= hidden_field_tag 'rename', '{}' %> + <%= hidden_field_tag 'cloned', '' %> + <%= hidden_field_tag 'remove', '' %> + <%= hidden_field_tag 'module-groups', '{}' %> + <% end %> + + + + + + + + +
    +
    +
    + <% my_modules.each do |my_module| %> + <%= render partial: "canvas/edit/my_module", locals: {project: @project, my_module: my_module} %> + <% end %> +
    + +<% if can_create_modules(@project) %> + <%= render partial: "canvas/edit/modal/new_module", locals: {project: @project} %> +<% end %> +<% if can_edit_modules(@project) %> + <%= render partial: "canvas/edit/modal/edit_module", locals: {project: @project } %> +<% end %> +<% if can_edit_module_groups(@project) %> + <%= render partial: "canvas/edit/modal/edit_module_group", locals: {project: @project } %> +<% end %> +<% if can_archive_modules(@project) %> + <%= render partial: "canvas/edit/modal/delete_module", locals: {project: @project} %> + <%= render partial: "canvas/edit/modal/delete_module_group", locals: {project: @project} %> +<% end %> diff --git a/app/views/canvas/_full_zoom.html.erb b/app/views/canvas/_full_zoom.html.erb new file mode 100644 index 000000000..4f27f2625 --- /dev/null +++ b/app/views/canvas/_full_zoom.html.erb @@ -0,0 +1,16 @@ +
    +
    + <% my_modules.each do |my_module| %> + <%= render partial: "canvas/full_zoom/my_module", locals: {project: project, my_module: my_module} %> + <% end %> +
    +
    + + +<%= render partial: "my_modules/modals/manage_description_modal" %> + + +<%= render partial: "my_modules/modals/manage_due_date_modal" %> + + +<%= render partial: "my_modules/modals/manage_users_modal" %> \ No newline at end of file diff --git a/app/views/canvas/_medium_zoom.html.erb b/app/views/canvas/_medium_zoom.html.erb new file mode 100644 index 000000000..5c86efdca --- /dev/null +++ b/app/views/canvas/_medium_zoom.html.erb @@ -0,0 +1,7 @@ +
    +
    + <% my_modules.each do |my_module| %> + <%= render partial: "canvas/medium_zoom/my_module", locals: {project: project, my_module: my_module} %> + <% end %> +
    +
    \ No newline at end of file diff --git a/app/views/canvas/_small_zoom.html.erb b/app/views/canvas/_small_zoom.html.erb new file mode 100644 index 000000000..44eaf23d3 --- /dev/null +++ b/app/views/canvas/_small_zoom.html.erb @@ -0,0 +1,7 @@ +
    +
    + <% my_modules.each do |my_module| %> + <%= render partial: "canvas/small_zoom/my_module", locals: {project: project, my_module: my_module} %> + <% end %> +
    +
    \ No newline at end of file diff --git a/app/views/canvas/_tags.html.erb b/app/views/canvas/_tags.html.erb new file mode 100644 index 000000000..dfc6d1ade --- /dev/null +++ b/app/views/canvas/_tags.html.erb @@ -0,0 +1,20 @@ +
    + <% tags2 = my_module.tags[0..3] %> + <% tags2.each do |tag| %> +
    " title="<%= tag.name %>"> + +
    + <% end %> + <% if tags2.count == 0 %> +
    + <% end %> + <% if my_module.tags.count > 0 %> + + <%= my_module.tags.count %> + + <% else %> + "> + + + + <% end %> +
    \ No newline at end of file diff --git a/app/views/canvas/edit/_my_module.html.erb b/app/views/canvas/edit/_my_module.html.erb new file mode 100644 index 000000000..535e311c2 --- /dev/null +++ b/app/views/canvas/edit/_my_module.html.erb @@ -0,0 +1,65 @@ +
    + data-module-group-name="<%= my_module.my_module_group.name %>" + <% else %> + data-module-group-name="" + <% end %> + data-module-x="<%= my_module.x %>" + data-module-y="<%= my_module.y %>" + data-module-conns="<%= construct_module_connections(my_module) %>"> + +
    + +

    <%= my_module.name %>

    + + + +
    + + <% if can_edit_connections(my_module.project) %> +
    + <%=t "projects.canvas.edit.drag_connections" %> +
    + <% end %> + +
    + +
    diff --git a/app/views/canvas/edit/modal/_delete_module.html.erb b/app/views/canvas/edit/modal/_delete_module.html.erb new file mode 100644 index 000000000..85c2f50f8 --- /dev/null +++ b/app/views/canvas/edit/modal/_delete_module.html.erb @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/app/views/canvas/edit/modal/_delete_module_group.html.erb b/app/views/canvas/edit/modal/_delete_module_group.html.erb new file mode 100644 index 000000000..35051ecb5 --- /dev/null +++ b/app/views/canvas/edit/modal/_delete_module_group.html.erb @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/app/views/canvas/edit/modal/_edit_module.html.erb b/app/views/canvas/edit/modal/_edit_module.html.erb new file mode 100644 index 000000000..89dc2295a --- /dev/null +++ b/app/views/canvas/edit/modal/_edit_module.html.erb @@ -0,0 +1,28 @@ + \ No newline at end of file diff --git a/app/views/canvas/edit/modal/_edit_module_group.html.erb b/app/views/canvas/edit/modal/_edit_module_group.html.erb new file mode 100644 index 000000000..dce4f55af --- /dev/null +++ b/app/views/canvas/edit/modal/_edit_module_group.html.erb @@ -0,0 +1,28 @@ + \ No newline at end of file diff --git a/app/views/canvas/edit/modal/_new_module.html.erb b/app/views/canvas/edit/modal/_new_module.html.erb new file mode 100644 index 000000000..9c70387b4 --- /dev/null +++ b/app/views/canvas/edit/modal/_new_module.html.erb @@ -0,0 +1,28 @@ + diff --git a/app/views/canvas/full_zoom/_my_module.html.erb b/app/views/canvas/full_zoom/_my_module.html.erb new file mode 100644 index 000000000..7511ddc2a --- /dev/null +++ b/app/views/canvas/full_zoom/_my_module.html.erb @@ -0,0 +1,110 @@ +
    <%= " alert-yellow" if my_module.is_one_day_prior? %>" + id="<%= my_module.id %>" + data-module-id="<%= my_module.id %>" + data-module-name="<%= my_module.name %>" + <% if my_module.my_module_group.present? %> + data-module-group="<%= my_module.my_module_group.id %>" + <% end %> + data-module-x="<%= my_module.x %>" + data-module-y="<%= my_module.y %>" + data-module-conns="<%= construct_module_connections(my_module) %>" + data-module-tags-url="<%= my_module_my_module_tags_url(my_module, format: :json) %>" + data-module-users-tab-url="<%= my_module_user_my_modules_url(my_module_id: my_module.id, format: :json) %>"> + + <% if can_edit_tags_for_module(my_module) %> + + <% else %> + + <% end %> + <%= render partial: "canvas/tags.html.erb", locals: { my_module: my_module } %> + <% if can_edit_tags_for_module(my_module) %> + + <% else %> + + <% end %> + +
    +

    + <%= link_to_if can_view_module(my_module), my_module.name, steps_my_module_path(my_module) %> +

    +
    + +
    +
    +
    + <%= link_to_if can_edit_module(my_module), t("projects.canvas.full_zoom.due_date"), due_date_my_module_path(my_module, format: :json), remote: true, class: "due-date-link" %> +
    +
    + <% if can_edit_module(my_module) %> + <%= link_to due_date_my_module_path(my_module, format: :json), remote: true, class: "due-date-link due-date-refresh" do %> + <%= render partial: "my_modules/due_date_label.html.erb", locals: { my_module: my_module } %> + <% end %> + <% else %> + <%= render partial: "my_modules/due_date_label.html.erb", locals: { my_module: my_module } %> + <% end %> +
    +
    +
    + + +
    diff --git a/app/views/canvas/medium_zoom/_my_module.html.erb b/app/views/canvas/medium_zoom/_my_module.html.erb new file mode 100644 index 000000000..9e65cf004 --- /dev/null +++ b/app/views/canvas/medium_zoom/_my_module.html.erb @@ -0,0 +1,30 @@ +
    <%= " alert-yellow" if my_module.is_one_day_prior? %>" + id="<%= my_module.id %>" + data-module-id="<%= my_module.id %>" + data-module-name="<%= my_module.name %>" + <% if my_module.my_module_group.present? %> + data-module-group="<%= my_module.my_module_group.id %>" + <% end %> + data-module-x="<%= my_module.x %>" + data-module-y="<%= my_module.y %>" + data-module-conns="<%= construct_module_connections(my_module) %>" + data-module-tags-url="<%= my_module_my_module_tags_url(my_module, format: :json) %>"> + + <% if can_edit_tags_for_module(my_module) %> + + <% else %> + + <% end %> + <%= render partial: "canvas/tags.html.erb", locals: { my_module: my_module } %> + <% if can_edit_tags_for_module(my_module) %> + + <% else %> + + <% end %> + +
    +

    + <%= link_to_if can_view_module(my_module), my_module.name, steps_my_module_path(my_module) %> +

    +
    +
    diff --git a/app/views/canvas/small_zoom/_my_module.html.erb b/app/views/canvas/small_zoom/_my_module.html.erb new file mode 100644 index 000000000..c279b39f2 --- /dev/null +++ b/app/views/canvas/small_zoom/_my_module.html.erb @@ -0,0 +1,14 @@ +
    <%= " alert-yellow" if my_module.is_one_day_prior? %>" + id="<%= my_module.id %>" + data-module-id="<%= my_module.id %>" + data-module-name="<%= my_module.name %>" + <% if my_module.my_module_group.present? %> + data-module-group="<%= my_module.my_module_group.id %>" + <% end %> + data-module-x="<%= my_module.x %>" + data-module-y="<%= my_module.y %>" + data-module-conns="<%= construct_module_connections(my_module) %>"> + + <%= link_to_if can_view_module(my_module), my_module.name[0], steps_my_module_path(my_module) %> + +
    diff --git a/app/views/custom_fields/_new_modal.html.erb b/app/views/custom_fields/_new_modal.html.erb new file mode 100644 index 000000000..868edc1a9 --- /dev/null +++ b/app/views/custom_fields/_new_modal.html.erb @@ -0,0 +1,19 @@ + diff --git a/app/views/devise/confirmations/new.html.erb b/app/views/devise/confirmations/new.html.erb new file mode 100644 index 000000000..543416673 --- /dev/null +++ b/app/views/devise/confirmations/new.html.erb @@ -0,0 +1,26 @@ +<% provide(:head_title, t("devise.confirmations.new.head_title")) %> + +
    + +

    <%=t "devise.confirmations.new.title" %>

    + + <%= form_for(resource, as: resource_name, url: confirmation_path(resource_name), html: { method: :post }) do |f| %> + + <% if not resource.errors.empty? %> +
    + <%= devise_error_messages! %> +
    + <% end %> + +
    + <%= f.label :email %> + <%= f.email_field :email, autofocus: true, class: "form-control", value: (resource.pending_reconfirmation? ? resource.unconfirmed_email : resource.email) %> +
    + +
    + <%= f.submit t("devise.confirmations.new.submit"), class: "btn btn-primary" %> +
    + <% end %> + + <%= render "devise/shared/links" %> +
    diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb new file mode 100644 index 000000000..dc55f64f6 --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -0,0 +1,5 @@ +

    Welcome <%= @email %>!

    + +

    You can confirm your account email through the link below:

    + +

    <%= link_to 'Confirm my account', confirmation_url(@resource, confirmation_token: @token) %>

    diff --git a/app/views/devise/mailer/reset_password_instructions.html.erb b/app/views/devise/mailer/reset_password_instructions.html.erb new file mode 100644 index 000000000..f667dc12f --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.html.erb @@ -0,0 +1,8 @@ +

    Hello <%= @resource.email %>!

    + +

    Someone has requested a link to change your password. You can do this through the link below.

    + +

    <%= link_to 'Change my password', edit_password_url(@resource, reset_password_token: @token) %>

    + +

    If you didn't request this, please ignore this email.

    +

    Your password won't change until you access the link above and create a new one.

    diff --git a/app/views/devise/mailer/unlock_instructions.html.erb b/app/views/devise/mailer/unlock_instructions.html.erb new file mode 100644 index 000000000..41e148bf2 --- /dev/null +++ b/app/views/devise/mailer/unlock_instructions.html.erb @@ -0,0 +1,7 @@ +

    Hello <%= @resource.email %>!

    + +

    Your account has been locked due to an excessive number of unsuccessful sign in attempts.

    + +

    Click the link below to unlock your account:

    + +

    <%= link_to 'Unlock my account', unlock_url(@resource, unlock_token: @token) %>

    diff --git a/app/views/devise/passwords/edit.html.erb b/app/views/devise/passwords/edit.html.erb new file mode 100644 index 000000000..3b36aca76 --- /dev/null +++ b/app/views/devise/passwords/edit.html.erb @@ -0,0 +1,35 @@ +<% provide(:head_title, t("devise.passwords.edit.head_title")) %> + +
    +

    <%=t "devise.passwords.edit.title" %>

    + + <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :put }) do |f| %> + + <% if not resource.errors.empty? %> +
    + <%= devise_error_messages! %> +
    + <% end %> + + <%= f.hidden_field :reset_password_token %> + +
    + <%= f.label :password, t("devise.passwords.edit.password") %> + <% if @minimum_password_length %> + <%= t("devise.passwords.edit.password_length", min_length: @minimum_password_length) %> + <% end %>
    + <%= f.password_field :password, autofocus: true, autocomplete: "off", class: "form-control" %> +
    + +
    + <%= f.label :password_confirmation, t("devise.passwords.edit.password_confirm") %> + <%= f.password_field :password_confirmation, autocomplete: "off", class: "form-control" %> +
    + +
    + <%= f.submit t("devise.passwords.edit.submit"), class: "btn btn-primary" %> +
    + <% end %> + + <%= render "devise/shared/links" %> +
    diff --git a/app/views/devise/passwords/new.html.erb b/app/views/devise/passwords/new.html.erb new file mode 100644 index 000000000..cfd228269 --- /dev/null +++ b/app/views/devise/passwords/new.html.erb @@ -0,0 +1,25 @@ +<% provide(:head_title, t("devise.passwords.new.head_title")) %> + +
    +

    <%=t "devise.passwords.new.title" %>

    + + <%= form_for(resource, as: resource_name, url: password_path(resource_name), html: { method: :post }) do |f| %> + + <% if not resource.errors.empty? %> +
    + <%= devise_error_messages! %> +
    + <% end %> + +
    + <%= f.label :email %>
    + <%= f.email_field :email, autofocus: true, class: "form-control" %> +
    + +
    + <%= f.submit t("devise.passwords.new.submit"), class: "btn btn-primary" %> +
    + <% end %> + + <%= render "devise/shared/links" %> +
    diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb new file mode 100644 index 000000000..f0f7f0c0e --- /dev/null +++ b/app/views/devise/sessions/new.html.erb @@ -0,0 +1,34 @@ +<% provide(:head_title, t("devise.sessions.new.head_title")) %> + +
    +

    <%=t "devise.sessions.new.title" %>

    + + <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> +
    + + <%= :email %> + + <%= f.email_field :email, autofocus: true, class: "form-control", placeholder: t("devise.sessions.new.email_placeholder") %> +
    + +
    + + <%= :password %> + + <%= f.password_field :password, autocomplete: "off", class: "form-control", placeholder: t("devise.sessions.new.password_placeholder") %> +
    + + <% if devise_mapping.rememberable? -%> +
    + <%= f.check_box :remember_me %> + <%= f.label :remember_me %> +
    + <% end -%> + +
    + <%= f.submit t("devise.sessions.new.submit"), class: "btn btn-default" %> +
    + <% end %> + + <%= render "devise/shared/links" %> +
    diff --git a/app/views/devise/shared/_links.html.erb b/app/views/devise/shared/_links.html.erb new file mode 100644 index 000000000..130dad09f --- /dev/null +++ b/app/views/devise/shared/_links.html.erb @@ -0,0 +1,25 @@ +<%- if controller_name != 'sessions' %> + <%= link_to t("devise.links.login"), new_session_path(resource_name) %>
    +<% end -%> + +<%- if devise_mapping.registerable? && controller_name != 'registrations' %> + <%= link_to t("devise.links.signup"), new_registration_path(resource_name) %>
    +<% end -%> + +<%- if devise_mapping.recoverable? && controller_name != 'passwords' && controller_name != 'registrations' %> + <%= link_to t("devise.links.forgot"), new_password_path(resource_name) %>
    +<% end -%> + +<%- if devise_mapping.confirmable? && controller_name != 'confirmations' %> + <%= link_to t("devise.links.not_receive_confirmation"), new_confirmation_path(resource_name) %>
    +<% end -%> + +<%- if devise_mapping.lockable? && resource_class.unlock_strategy_enabled?(:email) && controller_name != 'unlocks' %> + <%= link_to t("devise.links.not_receive_unlock"), new_unlock_path(resource_name) %>
    +<% end -%> + +<%- if devise_mapping.omniauthable? %> + <%- resource_class.omniauth_providers.each do |provider| %> + <%= link_to t("devise.links.sign_in_provider", provider: "#{provider.to_s.titleize}"), omniauth_authorize_path(resource_name, provider) %>
    + <% end -%> +<% end -%> diff --git a/app/views/devise/unlocks/new.html.erb b/app/views/devise/unlocks/new.html.erb new file mode 100644 index 000000000..1418ab7f8 --- /dev/null +++ b/app/views/devise/unlocks/new.html.erb @@ -0,0 +1,25 @@ +<% provide(:head_title, t("devise.unlocks.new.head_title")) %> + +
    +

    <%=t "devise.unlocks.new.title" %>

    + + <%= form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post }) do |f| %> + + <% if not resource.errors.empty? %> +
    + <%= devise_error_messages! %> +
    + <% end %> + +
    + <%= f.label :email %> + <%= f.email_field :email, autofocus: true, class: "form-control" %> +
    + +
    + <%= f.submit t("devise.unlocks.new.submit"), class: "btn btn-primary" %> +
    + <% end %> + + <%= render "devise/shared/links" %> +
    diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb new file mode 100644 index 000000000..a52646702 --- /dev/null +++ b/app/views/layouts/application.html.erb @@ -0,0 +1,70 @@ + + + + <%=t "head.title", title: (yield :head_title) %> + + <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> + <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> + + <%= favicon_link_tag "favicon.ico" %> + <%= favicon_link_tag "favicon-16.png", type: "image/png", size: "16x16" %> + <%= favicon_link_tag "favicon-32.png", type: "image/png", size: "32x32" %> + <%= favicon_link_tag "favicon-48.png", type: "image/png", size: "48x48" %> + + <%= csrf_meta_tags %> + + + + + + + <%= render "shared/navigation" %> + +
    + <% if flash[:success] %> +
    +
    + + + <%= flash[:success].html_safe %> +
    +
    + <% end %> + <% if notice %> +
    +
    + + + <%= notice %> +
    +
    + <% end %> + <% if alert or flash[:error]%> +
    +
    + + + <%= alert || flash[:error].html_safe %> +
    +
    + <% end %> +
    + +
    "> + <%= yield :content %> +
    + + diff --git a/app/views/layouts/fluid.html.erb b/app/views/layouts/fluid.html.erb new file mode 100644 index 000000000..fc2c73d27 --- /dev/null +++ b/app/views/layouts/fluid.html.erb @@ -0,0 +1,12 @@ +<% content_for :content do %> +
    + + <%= yield :secondary_navigation %> +
    + <%= yield %> +
    +
    +<% end %> +<%= render template: 'layouts/application' %> diff --git a/app/views/layouts/main.html.erb b/app/views/layouts/main.html.erb new file mode 100644 index 000000000..51e0d0a53 --- /dev/null +++ b/app/views/layouts/main.html.erb @@ -0,0 +1,6 @@ +<% content_for :content do %> +
    + <%= yield %> +
    +<% end %> +<%= render template: 'layouts/application' %> diff --git a/app/views/my_module_comments/_comment.html.erb b/app/views/my_module_comments/_comment.html.erb new file mode 100644 index 000000000..95d970ced --- /dev/null +++ b/app/views/my_module_comments/_comment.html.erb @@ -0,0 +1,3 @@ +<%= l comment.created_at, format: '%H:%M' %> +<%= comment.user.full_name %>: +

    <%= comment.message %>

    \ No newline at end of file diff --git a/app/views/my_module_comments/_index.html.erb b/app/views/my_module_comments/_index.html.erb new file mode 100644 index 000000000..516dd1ba5 --- /dev/null +++ b/app/views/my_module_comments/_index.html.erb @@ -0,0 +1,26 @@ +
    <%= t('projects.canvas.popups.comments_tab') %>
    +
    +
      + <% if @comments.size == 0 then %> +
    • <%= t 'projects.canvas.popups.no_comments' %>
    • + <% else %> + <%= render 'my_module_comments/list.html.erb', comments: @comments %> + <% end %> + <% if @comments.length == @per_page %> +
    • + + <%=t "projects.canvas.popups.more_comments" %> + +
    • + <% end %> +
    +<% if can_add_comment_to_module(@my_module) %> +
      +
    • +
      + <%= bootstrap_form_for :comment, url: { format: :json }, method: :post, remote: true do |f| %> + <%= f.text_field :message, hide_label: true, placeholder: t("projects.canvas.popups.comment_placeholder"), append: f.submit("+"), help: '.' %> + <% end %> +
    • +
    +<% end %> diff --git a/app/views/my_module_comments/_list.html.erb b/app/views/my_module_comments/_list.html.erb new file mode 100644 index 000000000..f4224e036 --- /dev/null +++ b/app/views/my_module_comments/_list.html.erb @@ -0,0 +1,12 @@ +<% day = 366 %> +<% current_day = DateTime.current.strftime('%j').to_i %> + +<% comments.each do |comment| %> +
  • + <% comment_day = comment.created_at.strftime('%j').to_i %> + <% if comment_day < current_day and comment_day < day %> + <% day = comment.created_at.strftime('%j').to_i %> +

    <%= comment.created_at.strftime('%d.%m.%Y') %>

    + <% end %> + <%= render 'project_comments/comment.html.erb', comment: comment %>
  • +<% end %> diff --git a/app/views/my_module_comments/new.html.erb b/app/views/my_module_comments/new.html.erb new file mode 100644 index 000000000..b3f4c0de4 --- /dev/null +++ b/app/views/my_module_comments/new.html.erb @@ -0,0 +1,7 @@ +<% provide(:head_title, t("my_module_comments.new.head_title", project: @my_module.project.name, module: @my_module.name)) %> +

    <%=t "my_module_comments.new.title", module: @my_module.name %>

    + +<%= bootstrap_form_for [@my_module, @comment], url: my_module_my_module_comments_path do |f| %> + <%= f.text_area :message, style: "margin-top: 10px;" %>
    + <%= f.submit t("my_module_comments.new.create"), style: "margin-top: 10px;" %> +<% end %> diff --git a/app/views/my_module_tags/_index_edit.html.erb b/app/views/my_module_tags/_index_edit.html.erb new file mode 100644 index 000000000..7167c7acd --- /dev/null +++ b/app/views/my_module_tags/_index_edit.html.erb @@ -0,0 +1,89 @@ +
    <%=t "projects.canvas.modal_manage_tags.subtitle", module: @my_module.name %>
    +<% if @my_module_tags.size == 0 then %> +
    <%= t 'projects.canvas.modal_manage_tags.no_tags' %>
    +<% else %> +
      + <% @my_module_tags.each_with_index do |mmt, i| tag = mmt.tag %> +
    • + +
      +
      +

      <%= tag.name %>

      +
      +
      + <% if can_edit_tag(@my_module.project) then %> + <%= link_to "", remote: true, class: 'btn btn-link edit-tag-link', title: t("projects.canvas.modal_manage_tags.edit_tag") do %> + + <% end %> + <% end %> + <% if can_remove_tag_from_module(@my_module) then %> + <%= link_to my_module_my_module_tag_path(@my_module, mmt, format: :json), method: :delete, remote: true, class: 'btn btn-link remove-tag-link', title: t("projects.canvas.modal_manage_tags.remove_tag", module: @my_module.name) do %> + + <% end %> + <% end %> + <% if can_delete_tag(@my_module.project) then %> + <%= bootstrap_form_for tag, remote: true, url: project_tag_path(@my_module.project, tag, format: :json), method: :delete, html: { class: "delete-tag-form"} do |f| %> + <%= hidden_field_tag :my_module_id, @my_module.id %> + <%= f.button class: 'btn btn-link delete-tag-link', title: t("projects.canvas.modal_manage_tags.delete_tag") do %> + + <% end %> + <% end %> + <% end %> +
      +
      + + <% if can_edit_tag(@my_module.project) %> + + <% end %> + +
    • + <% end %> +
    +<% end %> + +
    +
    + <% if can_add_tag_to_module(@my_module) then %> + <%= bootstrap_form_for [@my_module, @new_mmt], remote: true, format: :json, html: { class: 'add-tag-form' } do |f| %> +
    +
    + <%= collection_select(:my_module_tag, :tag_id, @unassigned_tags, :id, :name, {}, { class: 'selectpicker' }) %> + <%= f.button class: 'btn btn-primary' do %> + + + <% end %> +
    +
    + <% end %> + <% end %> + <% if can_create_new_tag(@my_module.project) then %> +
    + <%= bootstrap_form_for [@my_module.project, @new_tag], remote: true, format: :json, html: { class: 'add-tag-form' } do |f| %> + <%= hidden_field_tag :my_module_id, @my_module.id %> + <%= f.hidden_field :project_id, :value => @my_module.project.id %> + <%= f.hidden_field :name, :value => t("tags.create.new_name") %> + <%= f.hidden_field :color, :value => TAG_COLORS[0] %> + <%= f.button class: "btn btn-primary" do %> + + + <% end %> + <% end %> +
    + <% end %> +
    diff --git a/app/views/my_module_tags/new.html.erb b/app/views/my_module_tags/new.html.erb new file mode 100644 index 000000000..abd566b26 --- /dev/null +++ b/app/views/my_module_tags/new.html.erb @@ -0,0 +1,10 @@ +<% provide(:head_title, t("my_module_tags.new.head_title", project: @my_module.project.name, module: @my_module.name)) %> +

    <%=t "my_module_tags.new.title", module: @my_module.name %>

    + +<%= bootstrap_form_for [@my_module, @mt], url: my_module_my_module_tags_path do |f| %> + <%= collection_select(:my_module_tag, :tag_id, @tags, :id, :name ) %> +
    + <%= f.submit t("my_module_tags.new.create"), style: "margin-top: 10px;" %> +<% end %> +
    +<%= link_to t("my_module_tags.new.new_tag"), new_tag_path, type: "button", class: "btn btn-default" %> diff --git a/app/views/my_modules/_activities.html.erb b/app/views/my_modules/_activities.html.erb new file mode 100644 index 000000000..4edc2bc3f --- /dev/null +++ b/app/views/my_modules/_activities.html.erb @@ -0,0 +1,15 @@ +
    <%= t("projects.canvas.popups.activities_tab") %>
    +
    +
      + <% if @activities.size == 0 then %> +
    • <%= t 'projects.canvas.popups.no_activities' %>
    • + <% else %> + <% @activities.each do |activity| %> +
    • <%=l activity.created_at, format: :full %> +
      <%= activity.message.html_safe %> +
    • + <% end %> + <% end %> +
      +
    • <%= link_to t("projects.canvas.popups.more_activities"), activities_my_module_path(@my_module) %>
    • +
    diff --git a/app/views/my_modules/_description.html.erb b/app/views/my_modules/_description.html.erb new file mode 100644 index 000000000..8fa2e9a52 --- /dev/null +++ b/app/views/my_modules/_description.html.erb @@ -0,0 +1,3 @@ +<%= bootstrap_form_for @my_module, url: my_module_path(@my_module, format: :json), remote: :true do |f| %> + <%= f.text_area :description, label: t("my_modules.description.label") %> +<% end %> \ No newline at end of file diff --git a/app/views/my_modules/_description_label.html.erb b/app/views/my_modules/_description_label.html.erb new file mode 100644 index 000000000..46f1027da --- /dev/null +++ b/app/views/my_modules/_description_label.html.erb @@ -0,0 +1,5 @@ +<% if @my_module.description.blank? %> + <%=t "projects.canvas.popups.no_description" %> +<% else %> + <%= @my_module.description %> +<% end %> \ No newline at end of file diff --git a/app/views/my_modules/_due_date.html.erb b/app/views/my_modules/_due_date.html.erb new file mode 100644 index 000000000..04377861f --- /dev/null +++ b/app/views/my_modules/_due_date.html.erb @@ -0,0 +1,3 @@ +<%= bootstrap_form_for @my_module, url: my_module_path(@my_module, format: :json), remote: :true do |f| %> + <%= f.datetime_picker :due_date, label: t("my_modules.due_date.label"), clear: true %> +<% end %> \ No newline at end of file diff --git a/app/views/my_modules/_due_date_label.html.erb b/app/views/my_modules/_due_date_label.html.erb new file mode 100644 index 000000000..53740adad --- /dev/null +++ b/app/views/my_modules/_due_date_label.html.erb @@ -0,0 +1,8 @@ +<% if my_module.due_date then %> + <%=l my_module.due_date, format: :full_date %> + <% if my_module.is_overdue? or my_module.is_one_day_prior? %> + + <% end %> +<% else %> + <%=t "projects.canvas.full_zoom.no_due_date" %> +<% end %> diff --git a/app/views/my_modules/_module_header.html.erb b/app/views/my_modules/_module_header.html.erb new file mode 100644 index 000000000..91b9a1348 --- /dev/null +++ b/app/views/my_modules/_module_header.html.erb @@ -0,0 +1,98 @@ +
    +
    +
    + +
    +
    + + <%= l(@my_module.created_at, format: :full) %> +
    +
    + +
    +
    + <% if can_edit_module(@my_module) then %> + <%= link_to due_date_my_module_path(@my_module, format: :json), remote: true, class: "due-date-link", style: "color: inherit" do %> + + <% end %> + <% else %> + + <% end %> +
    +
    + + <% if can_edit_module(@my_module) then %> + <%= link_to due_date_my_module_path(@my_module, format: :json), remote: true, class: "due-date-link due-date-refresh", style: "color: inherit" do %> + <%= render partial: "module_header_due_date_label.html.erb", + locals: { my_module: @my_module } %> + <% end %> + <% else %> + <%= render partial: "module_header_due_date_label.html.erb", + locals: { my_module: @my_module } %> + <% end %> +
    +
    + +
    +
    + <% if can_edit_tags_for_module(@my_module) %> + + <% end %> + + <% if can_edit_tags_for_module(@my_module) %> + + <% end %> +
    +
    + + <% if can_edit_module(@my_module) then %> + <%= link_to my_module_tags_edit_url(@my_module, format: :json), remote: true, class: "edit-tags-link tags-refresh", style: "color: inherit" do %> + <%= render partial: "my_modules/tags", locals: { my_module: @my_module } %> + <% end %> + <% else %> + <%= render partial: "my_modules/tags", locals: { my_module: @my_module } %> + <% end %> +
    +
    +
    + +
    +
    + <% if can_edit_module(@my_module) %> + <%= link_to description_my_module_path(@my_module, format: :json), remote: true, class: "description-link", style: "color: inherit" do %> + + <% end %> + <% else %> + + <% end %> +
    +
    + <% if can_edit_module(@my_module) %> + <%= link_to description_my_module_path(@my_module, format: :json), remote: true, class: "description-label description-link description-refresh", style: "color: inherit" do %> + <% if @my_module.description.present? and not @my_module.description.empty? %> + <%= @my_module.description %> + <% else %> + <%=t "my_modules.module_header.no_description" %> + <% end %> + <% end %> + <% else %> + <% if @my_module.description.present? and not @my_module.description.empty? %> + <%= @my_module.description %> + <% else %> + <%=t "my_modules.module_header.no_description" %> + <% end %> + <% end %> +
    +
    + + +<%= render partial: "my_modules/modals/manage_description_modal" %> + + +<%= render partial: "my_modules/modals/manage_due_date_modal" %> + + + +<%= render partial: "my_modules/modals/manage_module_tags_modal", locals: { my_module: @my_module } %> + +<%= javascript_include_tag("my_modules") %> \ No newline at end of file diff --git a/app/views/my_modules/_module_header_due_date_label.html.erb b/app/views/my_modules/_module_header_due_date_label.html.erb new file mode 100644 index 000000000..c193aae34 --- /dev/null +++ b/app/views/my_modules/_module_header_due_date_label.html.erb @@ -0,0 +1,5 @@ +<% if @my_module.due_date.blank? %> + <%=t "projects.canvas.full_zoom.no_due_date" %> +<% else %> + <%= l(@my_module.due_date, format: :full) %> +<% end %> \ No newline at end of file diff --git a/app/views/my_modules/_result.html.erb b/app/views/my_modules/_result.html.erb new file mode 100644 index 000000000..6e7e68c7d --- /dev/null +++ b/app/views/my_modules/_result.html.erb @@ -0,0 +1,61 @@ +<% markdown = markdown ||= nil %> +
    +
    + + <% if result.is_text %> + + <% elsif result.is_table %> + + <% elsif result.is_asset %> + + <% end %> + +
    +
    +
    +
    + <% if can_edit_result(result) %> + + + + <% end %> + <% if can_archive_result(result) and not result.archived %> + + + + <%= form_for :result, url: result_path_of_type(result), method: :patch, html: {id: 'result-archive-form-' + result.id.to_s } do |f| %> + <%= f.hidden_field :archived, value: true %> + <% end %> + <% end %> +
    + + + <%= result.name %> | + <%= raw t'my_modules.results.published_on', timestamp: l(result.created_at, format: :full), user: result.user.full_name %> + +
    +
    +
    + +
    +
    + <%= render partial: 'my_modules/result_user_generated.html.erb', locals: { result: result, markdown: markdown } %> +
    +
    +
    +
    +
    +
    +
    diff --git a/app/views/my_modules/_result_user_generated.html.erb b/app/views/my_modules/_result_user_generated.html.erb new file mode 100644 index 000000000..1ddd67755 --- /dev/null +++ b/app/views/my_modules/_result_user_generated.html.erb @@ -0,0 +1,7 @@ +<% if result.is_text %> + <%= render partial: "results/result_text.html.erb", locals: {result: result, markdown: markdown} %> +<% elsif result.is_table %> + <%= render partial: "results/result_table.html.erb", locals: {result: result} %> +<% elsif result.is_asset %> + <%= render partial: "results/result_asset.html.erb", locals: {result: result} %> +<% end %> diff --git a/app/views/my_modules/_show.html.erb b/app/views/my_modules/_show.html.erb new file mode 100644 index 000000000..7212250fd --- /dev/null +++ b/app/views/my_modules/_show.html.erb @@ -0,0 +1,15 @@ +
    <%=t "projects.canvas.popups.info_tab" %>
    +
    +
      +
    • + + <%= render partial: "description_label.html.erb" %> + +
    • + <% if can_edit_module(@my_module) %> +
      +
    • + <%= link_to t("projects.canvas.popups.full_info"), description_my_module_path(@my_module, format: :json), class: "description-link", remote: true %> +
    • + <% end %> +
    \ No newline at end of file diff --git a/app/views/my_modules/_step.html.erb b/app/views/my_modules/_step.html.erb new file mode 100644 index 000000000..862f07df7 --- /dev/null +++ b/app/views/my_modules/_step.html.erb @@ -0,0 +1,126 @@ +
    "> +
    + <%= step.position + 1 %> +
    +
    +
    +
    + <% if can_reorder_step_in_module(@my_module) %> + + + + + <% end %> + <% if can_edit_step_in_module(@my_module) %> + + + + <% end %> + <% if can_delete_step_in_module(@my_module) %> + <%= link_to(step_path(step), title: t("my_modules.steps.options.delete_title"), method: "delete", class: "btn btn-link delete-step", + data: {confirm: t("my_modules.steps.destroy.confirm", step: step.name)}) do %> + + <% end %> + <% end %> +
    + + + <%= step.name %> | + <%= raw t'my_modules.steps.published_on', timestamp: l(step.created_at, format: :full), user: step.user.full_name %> + +
    +
    +
    + +
    +
    + <%= step.description %> +
    +
    + <% unless step.tables.blank? then %> +
    + <%= t'my_modules.steps.tables' %> + <% step.tables.each do |table| %> +
    + <%= hidden_field(table, :contents, value: table.contents_utf_8, class: "hot-contents") %> +
    +
    + <% end %> +
    + <% end %> + <% assets = ordered_assets(step) %> + <% unless assets.blank? then %> +
    + <%= t'my_modules.steps.files' %> +
      + <% assets.each do |asset| %> +
    • + <% if can_download_step_assets(@my_module) %> + <%= link_to download_asset_path(asset), data: {no_turbolink: true} do %> + <%= image_tag preview_asset_path(asset) if asset.is_image? %> + <%= raw '
      ' if asset.is_image? %> + <%= asset.file_file_name %> + <% end %> + <% else %> + <%= image_tag preview_asset_path(asset) if asset.is_image? %> + <%= raw '
      ' if asset.is_image? %> + <%= asset.file_file_name %> + <% end %> +
    • + <% end %> +
    +
    + <% end %> + + <% unless step.checklists.blank? then %> +
    + <% step.checklists.each do |checklist| %> + <%= checklist.name %> + <% if checklist.checklist_items.empty? %> +
    + <%= t("my_modules.steps.empty_checklist") %> +
    + <% else %> + <% ordered_checklist_items(checklist).each do |checklist_item| %> +
    + +
    + <% end %> + <% end %> + <% end %> +
    + <% end %> +
    + + <% if !step.completed? and can_complete_step_in_module(@my_module) %> +
    + +
    + <% elsif step.completed? and can_uncomplete_step_in_module(@my_module) %> +
    + +
    + <% end %> +
    +
    +
    +
    +
    +
    +
    diff --git a/app/views/my_modules/_tags.html.erb b/app/views/my_modules/_tags.html.erb new file mode 100644 index 000000000..8799679bc --- /dev/null +++ b/app/views/my_modules/_tags.html.erb @@ -0,0 +1,8 @@ +<% tags = my_module.tags %> + <% tags.each do |tag| %> + + <%= tag.name %> + <% end %> +<% if tags.count == 0 %> + <%=t "my_modules.module_header.no_tags" %> +<% end %> diff --git a/app/views/my_modules/activities.html.erb b/app/views/my_modules/activities.html.erb new file mode 100644 index 000000000..28a9f3b32 --- /dev/null +++ b/app/views/my_modules/activities.html.erb @@ -0,0 +1,27 @@ +<% provide(:head_title, t("my_modules.activities.head_title", project: @my_module.project.name, module: @my_module.name)) %> +<%= render partial: "shared/sidebar" %> +<%= render partial: "shared/secondary_navigation" %> + +
    + <% if @activities.length == 0 %> +
    + <%= t 'my_modules.activities.no_activities' %> +
    + <% else %> +
    + <%= render partial: "my_modules/activities/list_activities.html.erb" %> + + + <% if @activities.length == @per_page %> + + <% end %> +
    +
    +<% end %> +
    + +<%= javascript_include_tag("my_modules/activities") %> + diff --git a/app/views/my_modules/activities/_activity.html.erb b/app/views/my_modules/activities/_activity.html.erb new file mode 100644 index 000000000..f5d261aaa --- /dev/null +++ b/app/views/my_modules/activities/_activity.html.erb @@ -0,0 +1,7 @@ +
  • + + <%= l activity.created_at, format: :full %> + + <%= activity.message.html_safe %> + +
  • \ No newline at end of file diff --git a/app/views/my_modules/activities/_list_activities.html.erb b/app/views/my_modules/activities/_list_activities.html.erb new file mode 100644 index 000000000..a14ccd8ab --- /dev/null +++ b/app/views/my_modules/activities/_list_activities.html.erb @@ -0,0 +1,6 @@ + +
      + <% @activities.each do |act| %> + <%= render partial: "my_modules/activities/activity.html.erb", locals: { activity: act } %> + <% end %> +
    diff --git a/app/views/my_modules/archive.html.erb b/app/views/my_modules/archive.html.erb new file mode 100644 index 000000000..d79c8b9a5 --- /dev/null +++ b/app/views/my_modules/archive.html.erb @@ -0,0 +1,19 @@ +<% provide(:head_title, t("my_modules.module_archive.head_title", project: @my_module.project.name, module: @my_module.name)) %> +<%= render partial: "shared/sidebar" %> +<%= render partial: "shared/secondary_navigation" %> + +
    +
    + <% i = 0 %> + <% @archived_results.each do |result| %> + + <%= render partial: "my_modules/archive/result.html.erb", locals: { result: result } %> + + <% i = i + 1 %> + <% end %> + + <% if i == 0 %> + <%=t "my_modules.module_archive.no_archived_results" %> + <% end %> +
    +
    \ No newline at end of file diff --git a/app/views/my_modules/archive/_result.html.erb b/app/views/my_modules/archive/_result.html.erb new file mode 100644 index 000000000..c96415cf4 --- /dev/null +++ b/app/views/my_modules/archive/_result.html.erb @@ -0,0 +1,52 @@ +
    + + + +
    +

    + <% if result.is_asset %> + <% if result.asset.is_image? %> + + <% else %> + + <% end %> + <% elsif result.is_text %> + + <% elsif result.is_table %> + + <% end %> + <%= result.name %> +

    +
    + +
    +
    +
    + <%=t "my_modules.module_archive.archived_on" %> +
    +
    + "> + <%=l result.archived_on, format: :full_date %> + +
    +
    +
    + +
    diff --git a/app/views/my_modules/edit.html.erb b/app/views/my_modules/edit.html.erb new file mode 100644 index 000000000..ceb72b470 --- /dev/null +++ b/app/views/my_modules/edit.html.erb @@ -0,0 +1,9 @@ +<% provide(:head_title, t("my_modules.edit.head_title", project: @my_module.project.name, module: @my_module.name)) %> +

    <%=t "my_modules.edit.title", module: @my_module.name %>

    + +<%= bootstrap_form_for @my_module, url: my_module_path do |f| %> + <%= f.text_field :name, label: t("my_modules.edit.name") %> + <%= f.text_area :description, style: "margin-top: 10px;" %> + <%= f.datetime_picker :due_date, label: t("my_modules.edit.due_date"), clear: true %> + <%= f.submit t("my_modules.edit.save"), style: "margin-top: 10px;" %> +<% end %> \ No newline at end of file diff --git a/app/views/my_modules/modals/_manage_description_modal.html.erb b/app/views/my_modules/modals/_manage_description_modal.html.erb new file mode 100644 index 000000000..ce86a812b --- /dev/null +++ b/app/views/my_modules/modals/_manage_description_modal.html.erb @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/app/views/my_modules/modals/_manage_due_date_modal.html.erb b/app/views/my_modules/modals/_manage_due_date_modal.html.erb new file mode 100644 index 000000000..b71ae1a2d --- /dev/null +++ b/app/views/my_modules/modals/_manage_due_date_modal.html.erb @@ -0,0 +1,15 @@ + \ No newline at end of file diff --git a/app/views/my_modules/modals/_manage_module_tags_modal.html.erb b/app/views/my_modules/modals/_manage_module_tags_modal.html.erb new file mode 100644 index 000000000..1c3e2edb1 --- /dev/null +++ b/app/views/my_modules/modals/_manage_module_tags_modal.html.erb @@ -0,0 +1,14 @@ + \ No newline at end of file diff --git a/app/views/my_modules/modals/_manage_users_modal.html.erb b/app/views/my_modules/modals/_manage_users_modal.html.erb new file mode 100644 index 000000000..1596fc080 --- /dev/null +++ b/app/views/my_modules/modals/_manage_users_modal.html.erb @@ -0,0 +1,14 @@ + diff --git a/app/views/my_modules/results.html.erb b/app/views/my_modules/results.html.erb new file mode 100644 index 000000000..70cec1f1b --- /dev/null +++ b/app/views/my_modules/results.html.erb @@ -0,0 +1,55 @@ +<% provide(:head_title, t("my_modules.results.head_title", project: @project.name, module: @my_module.name)) %> +<%= render partial: "shared/sidebar" %> +<%= render partial: "shared/secondary_navigation" %> + +
    +
    + + +
    + + <% if can_create_result_text_in_module(@my_module) or + can_create_result_table_in_module(@my_module) or + can_create_result_asset_in_module(@my_module) %> + + <% end %> + <% if can_create_result_text_in_module(@my_module) %> + + + + + <% end %> + <% if can_create_result_table_in_module(@my_module) %> + + + + + <% end %> + <% if can_create_result_asset_in_module(@my_module) %> + + + + + <% end %> +
    + +
    + +
    +<% ordered_result_of(@my_module).each do |result| %> + <%= render partial: "result", locals: {result: result, markdown: @markdown, direct_upload: @direct_upload} %> +<% end %> +
    + +<%= javascript_include_tag "handsontable.full.min" %> +<%= javascript_include_tag("canvas-to-blob.min") %> +<%= javascript_include_tag("direct-upload") %> +<%= javascript_include_tag "my_modules/results" %> +<%= javascript_include_tag "results/result_texts" %> +<%= javascript_include_tag "results/result_tables" %> +<%= javascript_include_tag "results/result_assets" %> diff --git a/app/views/my_modules/samples.html.erb b/app/views/my_modules/samples.html.erb new file mode 100644 index 000000000..c25473cdf --- /dev/null +++ b/app/views/my_modules/samples.html.erb @@ -0,0 +1,8 @@ +<% provide(:head_title, t("my_modules.samples.head_title", project: @project.name, module: @my_module.name)) %> +<%= render partial: "shared/sidebar" %> +<%= render partial: "shared/secondary_navigation" %> + +
    + <%= render partial: "shared/samples" %> +
    + diff --git a/app/views/my_modules/show.html.erb b/app/views/my_modules/show.html.erb new file mode 100644 index 000000000..eb0499654 --- /dev/null +++ b/app/views/my_modules/show.html.erb @@ -0,0 +1,11 @@ +<% provide(:head_title, t("my_modules.show.head_title", project: @my_module.project.name, module: @my_module.name)) %> +<%= render partial: "shared/sidebar" %> +<%= render partial: "shared/secondary_navigation" %> + +<% unless @my_module.archived %> +<%= form_for @my_module, method: :patch, format: :html do |f| %> + <%= f.hidden_field :archived, value: true %> + <%= button_tag t('my_modules.show.archive_action'), class: 'btn btn-danger', + data: { confirm: t('my_modules.show.archive_confirm_text') } %> +<% end %> +<% end %> diff --git a/app/views/my_modules/steps.html.erb b/app/views/my_modules/steps.html.erb new file mode 100644 index 000000000..8e088332d --- /dev/null +++ b/app/views/my_modules/steps.html.erb @@ -0,0 +1,42 @@ +<% provide(:head_title, t("my_modules.steps.head_title", project: @project.name, module: @my_module.name)) %> +<%= render partial: "shared/sidebar" %> +<%= render partial: "shared/secondary_navigation" %> + +<%= render partial: "module_header" %> + +
    + <% if can_create_step_in_module(@my_module) %> + + <%= t("my_modules.steps.new_step") %> + + <% end %> + + +
    + +

    <%=t "my_modules.steps.subtitle" %>

    + +
    +<% step_num = 0 %> +<% ordered_step_of(@my_module).each do |step| step_num += 1 %> + <%= render partial: "step", locals: {step: step} %> +<% end %> +
    + + +<%= render partial: "my_modules/modals/manage_description_modal" %> + + +<%= render partial: "my_modules/modals/manage_due_date_modal" %> + +<%= javascript_include_tag "handsontable.full.min" %> +<%= javascript_include_tag "Sortable.min" %> +<%= javascript_include_tag("canvas-to-blob.min") %> +<%= javascript_include_tag("direct-upload") %> +<%= javascript_include_tag("my_modules/steps") %> + diff --git a/app/views/organizations/_parse_error.html.erb b/app/views/organizations/_parse_error.html.erb new file mode 100644 index 000000000..a968f05e7 --- /dev/null +++ b/app/views/organizations/_parse_error.html.erb @@ -0,0 +1,7 @@ +<% if error.present? %> + +<% end %> \ No newline at end of file diff --git a/app/views/organizations/_parse_errors.html.erb b/app/views/organizations/_parse_errors.html.erb new file mode 100644 index 000000000..8712c94b1 --- /dev/null +++ b/app/views/organizations/_parse_errors.html.erb @@ -0,0 +1,26 @@ +<% if errors.present? %> + +<% end %> \ No newline at end of file diff --git a/app/views/project_activities/_index.html.erb b/app/views/project_activities/_index.html.erb new file mode 100644 index 000000000..b21c483d2 --- /dev/null +++ b/app/views/project_activities/_index.html.erb @@ -0,0 +1,13 @@ +
    <%= t("projects.index.activity_tab") %>
    +
    +
      + <% if @activities.size == 0 then %> +
    • <%= t 'projects.index.no_activities' %>
    • + <% else %> + <% @activities.each do |activity| %> +
    • <%= l activity.created_at, format: :full %> +
      <%= activity.message.html_safe %> +
    • + <% end %> + <% end %> +
    diff --git a/app/views/project_activities/index.html.erb b/app/views/project_activities/index.html.erb new file mode 100644 index 000000000..ae2e0b0d6 --- /dev/null +++ b/app/views/project_activities/index.html.erb @@ -0,0 +1,3 @@ +<% provide(:head_title, t("projects.activities.head_title", project: @project.name)) %> +<%= render partial: "shared/sidebar" %> +<%= render partial: "shared/secondary_navigation" %> diff --git a/app/views/project_comments/_comment.html.erb b/app/views/project_comments/_comment.html.erb new file mode 100644 index 000000000..adf691da0 --- /dev/null +++ b/app/views/project_comments/_comment.html.erb @@ -0,0 +1,3 @@ +<%= l comment.created_at, format: '%H:%M' %> +<%= comment.user.full_name %>: +

    <%= comment.message %>

    diff --git a/app/views/project_comments/_index.html.erb b/app/views/project_comments/_index.html.erb new file mode 100644 index 000000000..51894d7e6 --- /dev/null +++ b/app/views/project_comments/_index.html.erb @@ -0,0 +1,25 @@ +
    <%= t('projects.index.comment_tab') %>
    +
    +
      + <% if @comments.size == 0 then %> +
    • <%= t 'projects.index.no_comments' %>
    • + <% else %> + <%= render 'project_comments/list.html.erb', comments: @comments %> + <% end %> + <% if @comments.length == @per_page %> +
    • + + <%= t'projects.index.more_comments' %> +
    • + <% end %> +
    +<% if can_add_comment_to_project(@project) %> +
      +
    • +
      + <%= bootstrap_form_for :comment, url: {format: :json}, method: :post, remote: true do |f| %> + <%= f.text_field :message, hide_label: true, placeholder: t('projects.index.comment_placeholder'), append: f.submit('+'), help: '.' %> + <% end %> +
    • +
    +<% end %> diff --git a/app/views/project_comments/_list.html.erb b/app/views/project_comments/_list.html.erb new file mode 100644 index 000000000..68e17ebd8 --- /dev/null +++ b/app/views/project_comments/_list.html.erb @@ -0,0 +1,16 @@ +<% + day = 366 + current_day = DateTime.current.strftime('%j').to_i +%> +<% comments.each do |comment| %> +
  • + <% + comment_day = comment.created_at.strftime('%j').to_i + + if comment_day < current_day and comment_day < day then + day = comment.created_at.strftime('%j').to_i + %> +

    <%= comment.created_at.strftime('%d.%m.%Y') %>

    + <% end %> + <%= render 'project_comments/comment.html.erb', comment: comment %>
  • +<% end %> diff --git a/app/views/project_comments/new.html.erb b/app/views/project_comments/new.html.erb new file mode 100644 index 000000000..603235b61 --- /dev/null +++ b/app/views/project_comments/new.html.erb @@ -0,0 +1,7 @@ +<% provide(:head_title, t("project_comments.new.head_title", project: @project.name)) %> +

    <%=t "project_comments.new.title", project: @project.name %>

    + +<%= bootstrap_form_for [@project, @comment], url: project_project_comments_path do |f| %> + <%= f.text_area :message, style: "margin-top: 10px;" %>
    + <%= f.submit t("project_comments.new.create"), style: "margin-top: 10px;" %> +<% end %> diff --git a/app/views/projects/_edit.html.erb b/app/views/projects/_edit.html.erb new file mode 100644 index 000000000..9960ba8a6 --- /dev/null +++ b/app/views/projects/_edit.html.erb @@ -0,0 +1,9 @@ +<%= bootstrap_form_for @project, url: project_path(@project ,format: :json), method: :put, remote: true do |f| %> +
    +
    + <%= f.text_field :name, label: t("projects.index.modal_new_project.name"), autofocus: true, placeholder: t("projects.index.modal_new_project.name_placeholder") %> +
    +
    + + <%= f.enum_btn_group :visibility, label: t("projects.index.modal_new_project.visibility"), btn_names: { hidden: t("projects.index.modal_new_project.visibility_hidden"), visible: t("projects.index.modal_new_project.visibility_visible") } %> +<% end %> \ No newline at end of file diff --git a/app/views/projects/_new.html.erb b/app/views/projects/_new.html.erb new file mode 100644 index 000000000..664886836 --- /dev/null +++ b/app/views/projects/_new.html.erb @@ -0,0 +1,16 @@ +
    +
    + <%= form.text_field :name, label: t("projects.index.modal_new_project.name"), autofocus: true, placeholder: t("projects.index.modal_new_project.name_placeholder") %> +
    +
    + +
    +
    +
    + <%= form.label t("projects.index.modal_new_project.organization") %> + <%= collection_select(:project, :organization_id, @organizations, :id, :name, {}, { :class => "form-control"} ) %> +
    +
    +
    + +<%= form.enum_btn_group :visibility, label: t("projects.index.modal_new_project.visibility"), btn_names: { hidden: t("projects.index.modal_new_project.visibility_hidden"), visible: t("projects.index.modal_new_project.visibility_visible") } %> \ No newline at end of file diff --git a/app/views/projects/_notifications.html.erb b/app/views/projects/_notifications.html.erb new file mode 100644 index 000000000..c4610c45c --- /dev/null +++ b/app/views/projects/_notifications.html.erb @@ -0,0 +1,30 @@ +
    <%= t("projects.index.notifications_tab") %>
    +
    +
      + <% nr_of_notifications = 0 %> + <% @modules.each do |mod| %> + <% if mod.is_overdue? %> + <% nr_of_notifications += 1 %> + <% days = t("projects.index.module_overdue_days", count: mod.overdue_for_days) %> +
    • +
      + <%= l(mod.due_date, format: :full) %> + +
      + <%=t "projects.index.module_overdue_html", module: mod.name, days: days %> +
    • + <% elsif mod.is_one_day_prior? %> + <% nr_of_notifications += 1 %> +
    • +
      + <%= l(mod.due_date, format: :full) %> + +
      + <%=t "projects.index.module_one_day_due_html", module: mod.name %> +
    • + <% end %> + <% end %> + <% if nr_of_notifications == 0 %> +
    • <%= t 'projects.index.no_notifications' %>
    • + <% end %> +
    diff --git a/app/views/projects/archive.html.erb b/app/views/projects/archive.html.erb new file mode 100644 index 000000000..7ab34a617 --- /dev/null +++ b/app/views/projects/archive.html.erb @@ -0,0 +1,72 @@ +<% provide(:head_title, t("projects.archive.head_title")) %> + +<% if @projects_by_orgs.length > 0 %> +
    + +
    + +
    + + + + + + + +
    +
    +
    + + <% @projects_by_orgs.each do |org, projects| %> + <%= render partial: "projects/archive/org_projects", locals: {org: org, projects: projects} %> + <% end %> +<% else %> + +
    +
    +
    + +
    +
    +

    <%=t "projects.index.no_archived_projects" %>

    +
    +
    +
    +
    + <%= link_to t("projects.index.back_to_projects_index"), projects_path %> +
    +
    +<% end %> diff --git a/app/views/projects/archive/_org_projects.html.erb b/app/views/projects/archive/_org_projects.html.erb new file mode 100644 index 000000000..05da02bb8 --- /dev/null +++ b/app/views/projects/archive/_org_projects.html.erb @@ -0,0 +1,18 @@ + + +
    +<% projects.each_index do |i| project = projects[i] %> +
    + <%= render partial: "projects/archive/project", locals: {project: project} %> +
    + <% if (i+1) % 4 == 0 %> +
    + <% end %> + <% if (i+1) % 3 == 0 %> +
    + <% end %> + <% if (i+1) % 2 == 0 %> +
    + <% end %> +<% end %> +
    diff --git a/app/views/projects/archive/_project.html.erb b/app/views/projects/archive/_project.html.erb new file mode 100644 index 000000000..4b9ddfd74 --- /dev/null +++ b/app/views/projects/archive/_project.html.erb @@ -0,0 +1,37 @@ +
    +
    + + + +

    + <% if project.hidden? then %> + + <% else %> + + <% end %> + <%= project.name %> +

    +
    + +
    +
    +
    <%= t('projects.index.start_date') %>
    +
    + <%=l project.created_at, format: :full_date %> +
    +
    +
    +
    diff --git a/app/views/projects/canvas.html.erb b/app/views/projects/canvas.html.erb new file mode 100644 index 000000000..890d70354 --- /dev/null +++ b/app/views/projects/canvas.html.erb @@ -0,0 +1,37 @@ +<% provide(:head_title, t("projects.canvas.head_title", project: @project.name)) %> +<%= render partial: "shared/sidebar" %> +<%= render partial: "shared/secondary_navigation" %> + +
    + <% if can_edit_canvas(@project) %> + <%=link_to t("projects.canvas.canvas_edit"), canvas_edit_project_url(@project), remote: true, type: "button", class: "ajax btn btn-default", "data-action" => "edit" %> + <% end %> +
    + <%=link_to canvas_full_zoom_project_path(@project), remote: true, type: "button", class: "ajax btn btn-primary active", "data-action" => "full_zoom", "data-toggle" => "button", "aria-pressed" => true do %> + + <% end %> + <%=link_to canvas_medium_zoom_project_path(@project), remote: true, type: "button", class: "ajax btn btn-primary", "data-action" => "medium_zoom" do %> + + <% end %> + <%=link_to canvas_small_zoom_project_path(@project), remote: true, type: "button", class: "ajax btn btn-primary", "data-action" => "small_zoom" do %> + + <% end %> +
    +
    +
    + <%= render partial: 'canvas/full_zoom', locals: { project: @project, my_modules: @project.active_modules } %> +
    + + +<%= render partial: "my_modules/modals/manage_module_tags_modal", locals: { my_module: nil } %> + + +<%= javascript_include_tag("jsPlumb-2.0.4-min") %> +<%= javascript_include_tag("jsnetworkx") %> +<%= javascript_include_tag("eventPause-min") %> + +<%= javascript_include_tag("projects/canvas") %> diff --git a/app/views/projects/index.html.erb b/app/views/projects/index.html.erb new file mode 100644 index 000000000..a8fc3b8c5 --- /dev/null +++ b/app/views/projects/index.html.erb @@ -0,0 +1,141 @@ +<% provide(:head_title, t("projects.index.head_title")) %> + + + + + + + + + + +
    + +
    + +
    + + + + + + + <% if @organizations.length > 0 %> + + + + + <% end %> + + + + + + + +
    +
    +
    + +<% if @organizations.length == 0 %> + +
    +

    <%= t("projects.index.no_orgs.title") %>

    +

    <%= t("projects.index.no_orgs.text") %>

    +

    + <%= link_to t("projects.index.no_orgs.btn"), organizations_path, class: "btn btn-primary" %> +

    +
    +<% end %> + +<% if @projects_by_orgs.length > 0 %> + <% @projects_by_orgs.each do |org, projects| %> + <%= render partial: "projects/index/org_projects", locals: {org: org, projects: projects} %> + <% end %> +<% end %> + +<%= javascript_include_tag "projects/index", "data-turbolinks-track" => true %> diff --git a/app/views/projects/index/_org_projects.html.erb b/app/views/projects/index/_org_projects.html.erb new file mode 100644 index 000000000..e5ddb0496 --- /dev/null +++ b/app/views/projects/index/_org_projects.html.erb @@ -0,0 +1,18 @@ + + +
    +<% projects.each_index do |i| project = projects[i] %> +
    + <%= render partial: "projects/index/project", locals: {project: project} %> +
    + <% if (i+1) % 4 == 0 %> +
    + <% end %> + <% if (i+1) % 3 == 0 %> +
    + <% end %> + <% if (i+1) % 2 == 0 %> +
    + <% end %> +<% end %> +
    diff --git a/app/views/projects/index/_project.html.erb b/app/views/projects/index/_project.html.erb new file mode 100644 index 000000000..f87ed13fe --- /dev/null +++ b/app/views/projects/index/_project.html.erb @@ -0,0 +1,109 @@ +
    +
    + + <% if can_edit_project(project) or can_archive_project(project) %> + + <% end %> + +

    + <% if project.hidden? then %> + + <% else %> + + <% end %> + <% if can_view_project(project) then %> + <%= link_to project.name, canvas_project_path(project) %> + <% else %> + <%= project.name %> + <% end %> +

    +
    + +
    +
    +
    <%= t('projects.index.start_date') %>
    +
    + <%=l project.created_at, format: :full_date %> +
    +
    +
    + + +
    diff --git a/app/views/projects/module_archive.html.erb b/app/views/projects/module_archive.html.erb new file mode 100644 index 000000000..2c7f4a79d --- /dev/null +++ b/app/views/projects/module_archive.html.erb @@ -0,0 +1,28 @@ +<% provide(:head_title, t("projects.module_archive.head_title", project: @project.name)) %> +<%= render partial: "shared/sidebar" %> +<%= render partial: "shared/secondary_navigation" %> + +<% if @project.archived_modules.count > 0 %> +
    + <% @project.archived_modules.each_with_index do |my_module, i| %> +
    + <%= render partial: "projects/module_archive/my_module.html.erb", locals: { my_module: my_module} %> +
    + <% if (i+1) % 6 == 0 %> +
    + <% end %> + <% if (i+1) % 4 == 0 %> +
    + <% end %> + <% if (i+1) % 3 == 0 %> +
    + <% end %> + <% end %> +
    +<% else %> +
    +
    + <%=t "projects.module_archive.no_archived_modules" %> +
    +
    +<% end %> \ No newline at end of file diff --git a/app/views/projects/module_archive/_my_module.html.erb b/app/views/projects/module_archive/_my_module.html.erb new file mode 100644 index 000000000..d7c81ec5e --- /dev/null +++ b/app/views/projects/module_archive/_my_module.html.erb @@ -0,0 +1,39 @@ +
    + +
    + + + +

    <%= my_module.name %>

    + +
    + +
    +
    +
    + <%=t "projects.module_archive.archived_on" %> +
    +
    + "> + <%=l my_module.archived_on, format: :full_date %> + +
    +
    +
    + +
    \ No newline at end of file diff --git a/app/views/projects/samples.html.erb b/app/views/projects/samples.html.erb new file mode 100644 index 000000000..aea19b9fd --- /dev/null +++ b/app/views/projects/samples.html.erb @@ -0,0 +1,8 @@ +<% provide(:head_title, t("projects.samples.head_title", project: @project.name)) %> +<%= render partial: "shared/sidebar" %> +<%= render partial: "shared/secondary_navigation" %> + +
    + <%= render partial: "shared/samples" %> +
    + diff --git a/app/views/projects/show.html.erb b/app/views/projects/show.html.erb new file mode 100644 index 000000000..588de6ab7 --- /dev/null +++ b/app/views/projects/show.html.erb @@ -0,0 +1,3 @@ +<% provide(:head_title, t("projects.show.head_title", project: @project.name)) %> +<%= render partial: "shared/sidebar" %> +<%= render partial: "shared/secondary_navigation" %> diff --git a/app/views/reports/_new.html.erb b/app/views/reports/_new.html.erb new file mode 100644 index 000000000..35ad509b4 --- /dev/null +++ b/app/views/reports/_new.html.erb @@ -0,0 +1,15 @@ +
    + <%= label_tag :grouped_by, t("projects.reports.index.modal_new.grouped_by") %>
    +
    + + +
    +
    \ No newline at end of file diff --git a/app/views/reports/elements/_module_element_controls.html.erb b/app/views/reports/elements/_module_element_controls.html.erb new file mode 100644 index 000000000..251d712db --- /dev/null +++ b/app/views/reports/elements/_module_element_controls.html.erb @@ -0,0 +1,18 @@ +<% if !defined? show_sort then show_sort = false end %> +<% if !defined? show_move_up then show_move_up = true end %> +<% if !defined? show_move_down then show_move_down = true end %> +<% if !defined? show_remove then show_remove = true end %> + +<%if show_sort %> + "> + "> +<% end %> +<% if show_move_up %> + "> +<% end %> +<% if show_move_down %> + "> +<% end %> +<% if show_remove %> + "> +<% end %> \ No newline at end of file diff --git a/app/views/reports/elements/_my_module_activity_element.html.erb b/app/views/reports/elements/_my_module_activity_element.html.erb new file mode 100644 index 000000000..4b68c3fa0 --- /dev/null +++ b/app/views/reports/elements/_my_module_activity_element.html.erb @@ -0,0 +1,47 @@ +<% if my_module.blank? and @my_module.present? then my_module = @my_module end %> +<% if order.blank? and @order.present? then order = @order end %> +<% timestamp = Time.current + 1.year - 2.days %> +<% activities = my_module.activities.order(created_at: order) %> +
    " data-name="<%=t "projects.reports.elements.module_activity.sidebar_name" %>" data-icon-class="glyphicon glyphicon-equalizer"> +
    +
    +
    + +
    +
    + <%=t "projects.reports.elements.module_activity.name", my_module: my_module.name %> +
    +
    + <%= render partial: "reports/elements/module_element_controls.html.erb", locals: { show_sort: true } %> +
    +
    +
    +
    +
    +
    + <% if activities.count > 0 %> + +
      + <% activities.each do |activity| %> + <% activity_ts = activity.created_at %> +
    • + + <%=l activity_ts, format: :full %> + + +   + <%= activity.message.html_safe %> + +
    • + <% end %> +
    + <% else %> + <%=t "projects.reports.elements.module_activity.no_activity" %> + <% end %> +
    +
    +
    +
    + <%= children if (defined? children and children.present?) %> +
    +
    diff --git a/app/views/reports/elements/_my_module_element.html.erb b/app/views/reports/elements/_my_module_element.html.erb new file mode 100644 index 000000000..cbb89a4f4 --- /dev/null +++ b/app/views/reports/elements/_my_module_element.html.erb @@ -0,0 +1,59 @@ +<% if my_module.blank? and @my_module.present? then my_module = @my_module end %> +<% timestamp = my_module.created_at %> +<% name = my_module.name %> +
    " data-name="<%= name %>" data-icon-class="glyphicon-credit-card"> +
    +
    +
    + <%=t "projects.reports.elements.module.user_time", timestamp: l(timestamp, format: :full) %> +
    +
    + <%= render partial: "reports/elements/module_element_controls.html.erb", locals: { show_sort: true } %> +
    +
    +
    +
    +
    +
    +

    + + <%= name %>

    +
    +
    + <% if my_module.due_date.present? %> + <%=t "projects.reports.elements.module.due_date", due_date: l(my_module.due_date, format: :full) %> + <% else %> + <%=t "projects.reports.elements.module.no_due_date" %> + <% end %> +
    +
    +
    +
    + <% if my_module.description.present? %> + <%= my_module.description %> + <% else %> + <%=t "projects.reports.elements.module.no_description" %> + <% end %> +
    +
    +
    +
    + <%=t "projects.reports.elements.module.tags_header" %> +
    + <% if my_module.tags.count > 0 %> + <% my_module.tags.each do |tag| %> +
    + <%= tag.name %> +
    + <% end %> + <% else %> +
    + <%=t "projects.reports.elements.module.no_tags" %> +
    + <% end %> +
    +
    +
    + <%= children if (defined? children and children.present?) %> +
    +
    \ No newline at end of file diff --git a/app/views/reports/elements/_my_module_samples_element.html.erb b/app/views/reports/elements/_my_module_samples_element.html.erb new file mode 100644 index 000000000..bec61ae75 --- /dev/null +++ b/app/views/reports/elements/_my_module_samples_element.html.erb @@ -0,0 +1,34 @@ +<% if my_module.blank? and @my_module.present? then my_module = @my_module end %> +<% if order.blank? and @order.present? then order = @order end %> +<% timestamp = Time.current + 1.year - 1.days %> +<% samples_json = my_module.samples_json_hot(order) %> +
    " data-name="<%=t "projects.reports.elements.module_samples.sidebar_name" %>" data-icon-class="glyphicon-tint"> +
    +
    +
    + +
    +
    + <%=t "projects.reports.elements.module_samples.name", my_module: my_module.name %> +
    +
    + <%= render partial: "reports/elements/module_element_controls.html.erb", locals: { show_sort: true } %> +
    +
    +
    +
    + <% if samples_json[:data].count > 0 %> + +
    + <% else %> +
    +
    + <%=t "projects.reports.elements.module_samples.no_samples" %> +
    +
    + <% end %> +
    +
    + <%= children if (defined? children and children.present?) %> +
    +
    diff --git a/app/views/reports/elements/_new_element.html.erb b/app/views/reports/elements/_new_element.html.erb new file mode 100644 index 000000000..be803e124 --- /dev/null +++ b/app/views/reports/elements/_new_element.html.erb @@ -0,0 +1,25 @@ +<% if !defined? hide then hide = false end %> +<% if !defined? initial then initial = false end %> +
    <%= "initial" if initial %>" data-ts="ignore" data-type="new" title="<%=t "projects.reports.elements.new_element.title" %>" + <% if initial %> + data-step="12" + data-position="left" + data-intro="<%= t('tutorial.new_report_html', private_org: @project.organization.name) %>" + <% end %>> + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    \ No newline at end of file diff --git a/app/views/reports/elements/_project_header_element.html.erb b/app/views/reports/elements/_project_header_element.html.erb new file mode 100644 index 000000000..c39d2a483 --- /dev/null +++ b/app/views/reports/elements/_project_header_element.html.erb @@ -0,0 +1,21 @@ +<% if project.blank? and @project.present? then project = @project end %> +<% name = t("projects.reports.elements.project_header.title", project: project.name) %> +
    +
    +
    +
    + <%=t "projects.reports.elements.project_header.user_time", timestamp: l(project.created_at, format: :full) %> +
    +
    +
    +
    +
    +
    +

    <%= name %>

    +
    +
    +
    +
    + <%= children if (defined? children and children.present?) %> +
    +
    \ No newline at end of file diff --git a/app/views/reports/elements/_result_asset_element.html.erb b/app/views/reports/elements/_result_asset_element.html.erb new file mode 100644 index 000000000..3938051d0 --- /dev/null +++ b/app/views/reports/elements/_result_asset_element.html.erb @@ -0,0 +1,40 @@ +<% if result.blank? and @result.present? then result = @result end %> +<% asset = result.asset %> +<% is_image = result.asset.is_image? %> +<% comments = result.comments %> +<% timestamp = asset.updated_at %> +<% name = result.name %> +<% icon_class = is_image ? "glyphicon-picture" : "glyphicon-file" %> +
    " data-name="<%= name %>" data-icon-class="<%= icon_class %>"> +
    +
    +
    + +
    +
    + <%= name %> +
    +
    + <%=t "projects.reports.elements.result_asset.file_name", file: asset.file_file_name %> +
    +
    + <%=t "projects.reports.elements.result_asset.user_time", user: result.user.full_name, timestamp: l(timestamp, format: :full) %> +
    +
    + <%= render partial: "reports/elements/module_element_controls.html.erb" %> +
    +
    +
    +
    + <% if is_image %> +
    +
    + <%= image_tag preview_asset_path asset %> +
    +
    + <% end %> +
    +
    + <%= children if (defined? children and children.present?) %> +
    +
    diff --git a/app/views/reports/elements/_result_comments_element.html.erb b/app/views/reports/elements/_result_comments_element.html.erb new file mode 100644 index 000000000..cce2b6e39 --- /dev/null +++ b/app/views/reports/elements/_result_comments_element.html.erb @@ -0,0 +1,46 @@ +<% if result.blank? and @result.present? then result = @result end %> +<% if order.blank? and @order.present? then order = @order end %> +<% comments = result.comments.order(created_at: order) %> +<% timestamp = Time.current + 1.year %> +
    " data-type="result_comments" data-id="<%= result.id %>" data-name="<%=t "projects.reports.elements.result_comments.sidebar_name" %>" data-icon-class="glyphicon-comment"> +
    +
    +
    + +
    +
    + <%=t "projects.reports.elements.result_comments.name", result: result.name %> +
    +
    + <%= render partial: "reports/elements/module_element_controls.html.erb", locals: { show_sort: true, show_move_up: false, show_move_down: false } %> +
    +
    +
    +
    +
    +
    + <% if comments.count == 0 %> + <%=t "projects.reports.elements.result_comments.no_comments" %> + <% else %> +
      + <% comments.each do |comment| %> + <% comment_ts = comment.created_at %> +
    • + + <%=t "projects.reports.elements.result_comments.comment_prefix", user: comment.user.full_name, date: l(comment_ts, format: :full_date), time: l(comment_ts, format: :time) %> + + +   + <%= comment.message %> + +
    • + <% end %> +
    + <% end %> +
    +
    +
    +
    + <%= children if (defined? children and children.present?) %> +
    +
    diff --git a/app/views/reports/elements/_result_table_element.html.erb b/app/views/reports/elements/_result_table_element.html.erb new file mode 100644 index 000000000..1a06b1b05 --- /dev/null +++ b/app/views/reports/elements/_result_table_element.html.erb @@ -0,0 +1,30 @@ +<% if result.blank? and @result.present? then result = @result end %> +<% table = result.table %> +<% comments = result.comments %> +<% timestamp = table.updated_at %> +<% name = result.name %> +
    " data-name="<%= name %>" data-icon-class="glyphicon-th"> +
    +
    +
    + +
    +
    + <%= name %> +
    +
    + <%=t "projects.reports.elements.result_table.user_time", user: result.user.full_name , timestamp: l(timestamp, format: :full) %> +
    +
    + <%= render partial: "reports/elements/module_element_controls.html.erb" %> +
    +
    +
    +
    + +
    +
    +
    + <%= children if (defined? children and children.present?) %> +
    +
    \ No newline at end of file diff --git a/app/views/reports/elements/_result_text_element.html.erb b/app/views/reports/elements/_result_text_element.html.erb new file mode 100644 index 000000000..cbd7892c7 --- /dev/null +++ b/app/views/reports/elements/_result_text_element.html.erb @@ -0,0 +1,33 @@ +<% if result.blank? and @result.present? then result = @result end %> +<% result_text = result.result_text %> +<% comments = result.comments %> +<% timestamp = result.updated_at %> +<% name = result.name %> +
    " data-name="<%= name %>" data-icon-class="glyphicon-asterisk"> +
    +
    +
    + +
    +
    + <%= name %> +
    +
    + <%=t "projects.reports.elements.result_text.user_time", user: result.user.full_name, timestamp: l(timestamp, format: :full) %> +
    +
    + <%= render partial: "reports/elements/module_element_controls.html.erb" %> +
    +
    +
    +
    +
    +
    + <%= result_text.text %> +
    +
    +
    +
    + <%= children if (defined? children and children.present?) %> +
    +
    \ No newline at end of file diff --git a/app/views/reports/elements/_step_asset_element.html.erb b/app/views/reports/elements/_step_asset_element.html.erb new file mode 100644 index 000000000..2de266cd9 --- /dev/null +++ b/app/views/reports/elements/_step_asset_element.html.erb @@ -0,0 +1,34 @@ +<% if asset.blank? and @asset.present? then asset = @asset end %> +<% is_image = asset.is_image? %> +<% timestamp = asset.updated_at %> +<% icon_class = is_image ? "glyphicon-picture" : "glyphicon-file" %> +
    " data-icon-class="<%= icon_class %>"> +
    +
    +
    + +
    +
    + <%=t "projects.reports.elements.step_asset.file_name", file: asset.file_file_name %> +
    +
    + <%=t "projects.reports.elements.step_asset.user_time", timestamp: l(timestamp, format: :full) %> +
    +
    + <%= render partial: "reports/elements/module_element_controls.html.erb" %> +
    +
    +
    +
    + <% if is_image %> +
    +
    + <%= image_tag preview_asset_path asset %> +
    +
    + <% end %> +
    +
    + <%= children if (defined? children and children.present?) %> +
    +
    diff --git a/app/views/reports/elements/_step_checklist_element.html.erb b/app/views/reports/elements/_step_checklist_element.html.erb new file mode 100644 index 000000000..987e7b9ed --- /dev/null +++ b/app/views/reports/elements/_step_checklist_element.html.erb @@ -0,0 +1,34 @@ +<% if checklist.blank? and @checklist.present? then checklist = @checklist end %> +<% items = checklist.checklist_items %> +<% timestamp = checklist.updated_at %> +
    +
    +
    +
    + +
    +
    + <%=t "projects.reports.elements.step_checklist.checklist_name", name: checklist.name %> +
    +
    + <%=t "projects.reports.elements.step_checklist.user_time", timestamp: l(timestamp, format: :full) %> +
    +
    + <%= render partial: "reports/elements/module_element_controls.html.erb" %> +
    +
    +
    +
    +
      + <% items.each do |item| %> +
    • + /> + "><%= item.text %> +
    • + <% end %> +
    +
    +
    + <%= children if (defined? children and children.present?) %> +
    +
    \ No newline at end of file diff --git a/app/views/reports/elements/_step_comments_element.html.erb b/app/views/reports/elements/_step_comments_element.html.erb new file mode 100644 index 000000000..8cf39420b --- /dev/null +++ b/app/views/reports/elements/_step_comments_element.html.erb @@ -0,0 +1,46 @@ +<% if step.blank? and @step.present? then step = @step end %> +<% if order.blank? and @order.present? then order = @order end %> +<% comments = step.comments.order(created_at: order) %> +<% timestamp = Time.current + 1.year %> +
    " data-icon-class="glyphicon-comment"> +
    +
    +
    + +
    +
    + <%=t "projects.reports.elements.step_comments.name", step: step.name %> +
    +
    + <%= render partial: "reports/elements/module_element_controls.html.erb", locals: { show_sort: true } %> +
    +
    +
    +
    +
    +
    + <% if comments.count == 0 %> + <%=t "projects.reports.elements.step_comments.no_comments" %> + <% else %> +
      + <% comments.each do |comment| %> + <% comment_ts = comment.created_at %> +
    • + + <%=t "projects.reports.elements.step_comments.comment_prefix", user: comment.user.full_name, date: l(comment_ts, format: :full_date), time: l(comment_ts, format: :time) %> + + +   + <%= comment.message %> + +
    • + <% end %> +
    + <% end %> +
    +
    +
    +
    + <%= children if (defined? children and children.present?) %> +
    +
    diff --git a/app/views/reports/elements/_step_element.html.erb b/app/views/reports/elements/_step_element.html.erb new file mode 100644 index 000000000..204f9643a --- /dev/null +++ b/app/views/reports/elements/_step_element.html.erb @@ -0,0 +1,40 @@ +<% if step.blank? and @step.present? then step = @step end %> +<% timestamp = step.completed_on %> +<% tables = step.tables %> +<% assets = step.assets %> +<% checklists = step.checklists %> +<% comments = step.comments %> +
    " data-name="<%=t "projects.reports.elements.step.sidebar_name", pos: (step.position + 1), name: step.name %>" data-icon-class="glyphicon-circle-arrow-right"> +
    +
    +
    + <%=t "projects.reports.elements.step.user_time", user: step.user.full_name , timestamp: l(timestamp, format: :full) %> +
    +
    + <%= render partial: "reports/elements/module_element_controls.html.erb", locals: { show_sort: true } %> +
    +
    +
    +
    +
    +
    +
    + + <%=t "projects.reports.elements.step.step_pos", pos: (step.position + 1) %> <%= step.name %> +
    +
    +
    +
    +
    + <% if step.description.present? %> + <%= step.description %> + <% else %> + <%=t "projects.reports.elements.step.no_description" %> + <% end %> +
    +
    +
    +
    + <%= children if (defined? children and children.present?) %> +
    +
    \ No newline at end of file diff --git a/app/views/reports/elements/_step_table_element.html.erb b/app/views/reports/elements/_step_table_element.html.erb new file mode 100644 index 000000000..6cb900454 --- /dev/null +++ b/app/views/reports/elements/_step_table_element.html.erb @@ -0,0 +1,24 @@ +<% if table.blank? and @table.present? then table = @table end %> +<% timestamp = table.updated_at %> +
    " data-icon-class="glyphicon-th"> +
    +
    +
    + +
    +
    + <%=t "projects.reports.elements.step_table.user_time", timestamp: l(timestamp, format: :full) %> +
    +
    + <%= render partial: "reports/elements/module_element_controls.html.erb" %> +
    +
    +
    +
    + +
    +
    +
    + <%= children if (defined? children and children.present?) %> +
    +
    \ No newline at end of file diff --git a/app/views/reports/index.html.erb b/app/views/reports/index.html.erb new file mode 100644 index 000000000..edb2e6a7f --- /dev/null +++ b/app/views/reports/index.html.erb @@ -0,0 +1,110 @@ +<% provide(:head_title, t("projects.reports.index.head_title", project: @project.name)) %> +<%= render partial: "shared/sidebar" %> +<%= render partial: "shared/secondary_navigation" %> + +
    +
    + <% if can_create_new_report(@project) %> + <%= link_to new_by_module_project_reports_path(@project), class: "btn btn-primary", id: "new-report-btn", + "data-step" => "10", "data-intro" => t("tutorial.reports_index_html") do %> + <%# TEMPORARY HIDDEN %> + <%#= link_to new_project_report_path(@project, format: :json), remote: true, class: "btn btn-primary pull-left", id: "new-report-btn" do %> + + + <% end %> + + <%= link_to "", remote: true, class: "btn btn-default", id: "edit-report-btn" do %> + + + <% end %> + <% if can_delete_reports(@project) %> + <%= link_to "", remote: true, class: "btn btn-default", id: "delete-reports-btn" do %> + + + <% end %> + <% end %> + + <% end %> +
    + + + + + + + + + + + + + + + + <% if @project.reports.count > 0 %> + <% @project.reports.each do |report| %> + + + + + + + + + + <% end %> + <% else %> + + <% end %> + +
    <%=t "projects.reports.index.thead_name" %><%=t "projects.reports.index.thead_grouped_by" %><%=t "projects.reports.index.thead_created_by" %><%=t "projects.reports.index.thead_last_modified_by" %><%=t "projects.reports.index.thead_created_at" %><%=t "projects.reports.index.thead_updated_at" %>
    <%= report.name %><%=t report.by_module? ? "projects.reports.index.table_grouped_by_module" : "projects.reports.index.table_grouped_by_timestamp" %><%= report.user.full_name %><%= report.last_modified_by ? report.last_modified_by.full_name : report.user.full_name %><%=l report.created_at, format: :full %><%=l report.updated_at, format: :full %>
    <%=t "projects.reports.index.no_reports" %>
    + +
    + + + + + + + + +<%= javascript_include_tag("reports/index") %> diff --git a/app/views/reports/new/_report_navigation.html.erb b/app/views/reports/new/_report_navigation.html.erb new file mode 100644 index 000000000..30c5107ef --- /dev/null +++ b/app/views/reports/new/_report_navigation.html.erb @@ -0,0 +1,64 @@ +<% content_for :secondary_navigation do %> + + +<% end %> diff --git a/app/views/reports/new/_report_sidebar.html.erb b/app/views/reports/new/_report_sidebar.html.erb new file mode 100644 index 000000000..e0735f247 --- /dev/null +++ b/app/views/reports/new/_report_sidebar.html.erb @@ -0,0 +1,22 @@ +<% content_for :sidebar do %> +
    + + + + +
    +
      +
    +
    +
    +<% end %> + +<%= javascript_include_tag("sidebar") %> diff --git a/app/views/reports/new/modal/_module_contents.html.erb b/app/views/reports/new/modal/_module_contents.html.erb new file mode 100644 index 000000000..feeda0bbe --- /dev/null +++ b/app/views/reports/new/modal/_module_contents.html.erb @@ -0,0 +1,58 @@ +<%= bootstrap_form_tag remote: true, url: module_contents_project_reports_path(project, format: :json), method: :post, html: { id: "add-contents-form" } do |f| %> + <%= hidden_field_tag :id, my_module.id %> +
    + + + + +
    +
    + <%= render partial: "reports/new/modal/module_contents_inner.html.erb", locals: { form: f, my_module: my_module } %> +
    +
    + <%= render partial: "reports/new/modal/step_contents_inner.html.erb", locals: { form: f } %> +
    +
    + <%= render partial: "reports/new/modal/result_contents_inner.html.erb", locals: { form: f } %> +
    +
    +
    +<% end %> + + \ No newline at end of file diff --git a/app/views/reports/new/modal/_module_contents_inner.html.erb b/app/views/reports/new/modal/_module_contents_inner.html.erb new file mode 100644 index 000000000..0484c22e8 --- /dev/null +++ b/app/views/reports/new/modal/_module_contents_inner.html.erb @@ -0,0 +1,52 @@ +<% my_module_undefined = !defined? my_module or my_module.blank? %> + +
    + + <%= t("projects.reports.elements.modals.module_contents_inner.instructions") %> + +
    + +<%= form.check_box :module_all, label: t("general.check_all"), class: "module-check-all" %> + +<%= form.label t("projects.reports.elements.modals.module_contents_inner.header") %> + +<% if my_module_undefined or my_module.steps.count > 0 %> + <%= form.check_box :module_steps, label: t("projects.reports.elements.modals.module_contents_inner.steps") %> +<% else %> +
    + + <%= t("projects.reports.elements.modals.module_contents_inner.no_steps") %> + +
    +<% end %> + +<% if my_module_undefined or my_module.results.count > 0 %> + <%= form.check_box :module_results, label: t("projects.reports.elements.modals.module_contents_inner.results"), class: "results-all" %> +
      + <% if my_module_undefined or (my_module.results.select { |r| r.is_asset }).count > 0 %> +
    • + <%= form.check_box :module_result_assets, label: t("projects.reports.elements.modals.module_contents_inner.result_assets"), class: "result-cb" %> +
    • + <% end %> + <% if my_module_undefined or (my_module.results.select { |r| r.is_table }).count > 0 %> +
    • + <%= form.check_box :module_result_tables, label: t("projects.reports.elements.modals.module_contents_inner.result_tables"), class: "result-cb" %> +
    • + <% end %> + <% if my_module_undefined or (my_module.results.select { |r| r.is_text }).count > 0 %> +
    • + <%= form.check_box :module_result_texts, label: t("projects.reports.elements.modals.module_contents_inner.result_texts"), class: "result-cb" %> +
    • + <% end %> +
    +<% else %> +
    + + <%= t("projects.reports.elements.modals.module_contents_inner.no_results") %> + +
    +<% end %> + +<%= form.check_box :module_activity, label: t("projects.reports.elements.modals.module_contents_inner.activity") %> + +<%= form.check_box :module_samples, label: t("projects.reports.elements.modals.module_contents_inner.samples") %> \ No newline at end of file diff --git a/app/views/reports/new/modal/_project_contents.html.erb b/app/views/reports/new/modal/_project_contents.html.erb new file mode 100644 index 000000000..d05b53af9 --- /dev/null +++ b/app/views/reports/new/modal/_project_contents.html.erb @@ -0,0 +1,109 @@ +<%= bootstrap_form_tag remote: true, url: project_contents_project_reports_path(project, format: :json), method: :post, html: { id: "add-contents-form" } do |f| %> + <%= hidden_field_tag :id, project.id %> +
    + + + + +
    +
    +
    <%= t("projects.reports.elements.modals.project_contents.project_tab") %>
    + <%= render partial: "reports/new/modal/project_contents_inner.html.erb", locals: { form: f, project: project } %> +
    + <% if project.my_modules.count > 0 %> +
    +
    <%= t("projects.reports.elements.modals.project_contents.modules_tab") %>
    + <%= render partial: "reports/new/modal/module_contents_inner.html.erb", locals: { form: f } %> +
    +
    +
    <%= t("projects.reports.elements.modals.project_contents.steps_tab") %>
    + <%= render partial: "reports/new/modal/step_contents_inner.html.erb", locals: { form: f } %> +
    +
    +
    <%= t("projects.reports.elements.modals.project_contents.results_tab") %>
    + <%= render partial: "reports/new/modal/result_contents_inner.html.erb", locals: { form: f } %> +
    + <% end %> +
    +
    +<% end %> + + diff --git a/app/views/reports/new/modal/_project_contents_inner.html.erb b/app/views/reports/new/modal/_project_contents_inner.html.erb new file mode 100644 index 000000000..7edbc46d1 --- /dev/null +++ b/app/views/reports/new/modal/_project_contents_inner.html.erb @@ -0,0 +1,46 @@ +<% modules_without_group = project.modules_without_group %> + +
    + + <%= t("projects.reports.elements.modals.project_contents_inner.instructions") %> + +
    + +<% if project.my_modules.count > 0 %> + <%= form.check_box :project, label: project.name, class: "project-all-cb" %> +
      + <% project.my_module_groups.each do |my_module_group| %> +
    • + <%= form.check_box "module_group_#{my_module_group.id}", label: my_module_group.name, class: "project-all-cb" %> + <% if my_module_group.my_modules.present? then %> +
        + <% my_module_group.my_modules.each do |my_module| %> +
      • + <%= form.check_box "modules[#{my_module.id}]", label: my_module.name %> +
      • + <% end %> +
      + <% end %> +
    • + <% end %> + + <% if modules_without_group.present? and modules_without_group.count > 0 %> +
    • + <%= form.check_box :no_module_group, label: t("projects.reports.elements.modals.project_contents_inner.no_module_group"), class: "project-all-cb" %> +
        + <% modules_without_group.each do |my_module| %> +
      • + <%= form.check_box "modules[#{my_module.id}]", label: my_module.name %> +
      • + <% end %> +
      +
    • + <% end %> +
    +<% else %> +
    + + <%= t("projects.reports.elements.modals.project_contents_inner.no_modules") %> + +
    +<% end %> \ No newline at end of file diff --git a/app/views/reports/new/modal/_result_contents.html.erb b/app/views/reports/new/modal/_result_contents.html.erb new file mode 100644 index 000000000..98833b0af --- /dev/null +++ b/app/views/reports/new/modal/_result_contents.html.erb @@ -0,0 +1,4 @@ +<%= bootstrap_form_tag remote: true, url: result_contents_project_reports_path(project, format: :json), method: :post, html: { id: "add-contents-form" } do |f| %> + <%= hidden_field_tag :id, result.id %> + <%= render partial: "reports/new/modal/result_contents_inner.html.erb", locals: { form: f } %> +<% end %> \ No newline at end of file diff --git a/app/views/reports/new/modal/_result_contents_inner.html.erb b/app/views/reports/new/modal/_result_contents_inner.html.erb new file mode 100644 index 000000000..41cbcad2b --- /dev/null +++ b/app/views/reports/new/modal/_result_contents_inner.html.erb @@ -0,0 +1,7 @@ +
    + + <%= t("projects.reports.elements.modals.result_contents_inner.instructions") %> + +
    + +<%= form.check_box :result_comments, label: t("projects.reports.elements.modals.result_contents_inner.comments") %> \ No newline at end of file diff --git a/app/views/reports/new/modal/_save.html.erb b/app/views/reports/new/modal/_save.html.erb new file mode 100644 index 000000000..a64f3b4be --- /dev/null +++ b/app/views/reports/new/modal/_save.html.erb @@ -0,0 +1,6 @@ +<%= bootstrap_form_for [@project, @report], remote: true, url: @url, method: @method, html: { id: "save-report-form" } do |f| %> + <%= f.hidden_field :grouped_by, value: :by_module %> + <%= f.text_field :name, label: t("projects.reports.elements.modals.save_report.name"), placeholder: t("projects.reports.elements.modals.save_report.name_placeholder") %> + <%= f.text_area :description, label: t("projects.reports.elements.modals.save_report.description"), placeholder: t("projects.reports.elements.modals.save_report.description_placeholder") %> + <%= hidden_field_tag :report_contents, @report_contents %> +<% end %> \ No newline at end of file diff --git a/app/views/reports/new/modal/_step_contents.html.erb b/app/views/reports/new/modal/_step_contents.html.erb new file mode 100644 index 000000000..c9f3d621e --- /dev/null +++ b/app/views/reports/new/modal/_step_contents.html.erb @@ -0,0 +1,19 @@ +<%= bootstrap_form_tag remote: true, url: step_contents_project_reports_path(project, format: :json), method: :post, html: { id: "add-contents-form" } do |f| %> + <%= hidden_field_tag :id, step.id %> + + <%= render partial: "reports/new/modal/step_contents_inner.html.erb", locals: { form: f, step: step } %> +<% end %> + + \ No newline at end of file diff --git a/app/views/reports/new/modal/_step_contents_inner.html.erb b/app/views/reports/new/modal/_step_contents_inner.html.erb new file mode 100644 index 000000000..0ccee30a0 --- /dev/null +++ b/app/views/reports/new/modal/_step_contents_inner.html.erb @@ -0,0 +1,38 @@ +<% step_undefined = !defined? step or step.blank? %> + +
    + + <%= t("projects.reports.elements.modals.step_contents_inner.instructions") %> + +
    + +<%= form.check_box :step_all, label: t("general.check_all"), class: "step-check-all" %> +<%= form.label t("projects.reports.elements.modals.step_contents_inner.header") %> +<% if step_undefined or step.checklists.count > 0 %> + <%= form.check_box :step_checklists, label: t("projects.reports.elements.modals.step_contents_inner.checklists") %> +<% else %> +
    + + <%= t("projects.reports.elements.modals.step_contents_inner.no_checklists") %> + +
    +<% end %> +<% if step_undefined or step.assets.count > 0 %> + <%= form.check_box :step_assets, label: t("projects.reports.elements.modals.step_contents_inner.assets") %> +<% else %> +
    + + <%= t("projects.reports.elements.modals.step_contents_inner.no_assets") %> + +
    +<% end %> +<% if step_undefined or step.tables.count > 0 %> + <%= form.check_box :step_tables, label: t("projects.reports.elements.modals.step_contents_inner.tables") %> +<% else %> +
    + + <%= t("projects.reports.elements.modals.step_contents_inner.no_tables") %> + +
    +<% end %> +<%= form.check_box :step_comments, label: t("projects.reports.elements.modals.step_contents_inner.comments") %> \ No newline at end of file diff --git a/app/views/reports/new_by_module.html.erb b/app/views/reports/new_by_module.html.erb new file mode 100644 index 000000000..7d4d1903e --- /dev/null +++ b/app/views/reports/new_by_module.html.erb @@ -0,0 +1,76 @@ +<% provide(:head_title, t("projects.reports.new.head_title", project: @project.name)) %> +<% provide(:body_class, "report-body") %> +<% provide(:sidebar_wrapper_class, "report-sidebar-wrapper") %> +<% provide(:container_class, "report-container") %> + +<%= render partial: "reports/new/report_sidebar" %> +<%= render partial: "reports/new/report_navigation" %> + + + + +
    + +<% if @report.present? %> + <% @report.root_elements.each do |el| %> + <%= render_report_element(el) %> + <%= render_new_element(false) %> + <% end %> +<% else %> + <%= render partial: "reports/elements/project_header_element", locals: { project: @project } %> + <%= render partial: "reports/elements/new_element", locals: { initial: true } %> +<% end %> +
    + + + + + + + +<%= javascript_include_tag "handsontable.full.min" %> +<%= javascript_include_tag("reports/new_by_module") %> diff --git a/app/views/reports/report.pdf.erb b/app/views/reports/report.pdf.erb new file mode 100644 index 000000000..e8a87c85c --- /dev/null +++ b/app/views/reports/report.pdf.erb @@ -0,0 +1,13 @@ + + + + + <%= wicked_pdf_stylesheet_link_tag "application" %> + <%= wicked_pdf_stylesheet_link_tag "reports_pdf" %> + + + + + \ No newline at end of file diff --git a/app/views/result_assets/_edit.html.erb b/app/views/result_assets/_edit.html.erb new file mode 100644 index 000000000..fa2bf8fe4 --- /dev/null +++ b/app/views/result_assets/_edit.html.erb @@ -0,0 +1,17 @@ +
    + <%= bootstrap_form_for(@result, url: result_asset_path(format: :json), remote: true, multipart: true, data: { type: :json }) do |f| %> + <%= f.text_field :name, style: "margin-top: 10px;" %>
    + <%= f.fields_for :asset do |ff| %> + <%= ff.file_field :file %> + <% end %> +
    + <% if direct_upload %> + <%= f.submit t("result_assets.edit.update"), class: 'btn btn-primary', onclick: 'startFileUpload(event, this);' %> + <% else %> + <%= f.submit t("result_assets.edit.update"), class: 'btn btn-primary' %> + <% end %> + + <% end %> +
    diff --git a/app/views/result_assets/_new.html.erb b/app/views/result_assets/_new.html.erb new file mode 100644 index 000000000..c114af584 --- /dev/null +++ b/app/views/result_assets/_new.html.erb @@ -0,0 +1,16 @@ +
    + <%= bootstrap_form_for(@result, url: my_module_result_assets_path(format: :json), remote: true, multipart: true, data: { type: :json }) do |f| %> + <%= f.text_field :name, style: "margin-top: 10px;" %>
    + <%= f.fields_for :asset do |ff| %> + <%= ff.file_field :file %> + <% end %> + <% if direct_upload %> + <%= f.submit t("result_assets.new.create"), class: 'btn btn-primary', onclick: 'startFileUpload(event, this);' %> + <% else %> + <%= f.submit t("result_assets.new.create"), class: 'btn btn-primary' %> + <% end %> + + <% end %> +
    diff --git a/app/views/result_comments/_comment.html.erb b/app/views/result_comments/_comment.html.erb new file mode 100644 index 000000000..74715974a --- /dev/null +++ b/app/views/result_comments/_comment.html.erb @@ -0,0 +1,2 @@ +<%=t "my_modules.results.comment_title", user: comment.user.full_name, time: l(comment.created_at, format: :time) %> +

    <%= comment.message %>

    diff --git a/app/views/result_comments/_index.html.erb b/app/views/result_comments/_index.html.erb new file mode 100644 index 000000000..d840a32f7 --- /dev/null +++ b/app/views/result_comments/_index.html.erb @@ -0,0 +1,26 @@ +
    <%= t('my_modules.results.comments_tab') %>
    +
    +
      + <% if @comments.size == 0 then %> +
    • <%= t 'general.no_comments' %>
    • + <% else %> + <%= render 'result_comments/list.html.erb', comments: @comments %> + <% end %> + <% if @comments.length == @per_page %> +
    • + + <%=t "general.more_comments" %> + +
    • + <% end %> +
    +<% if can_add_result_comment_in_module(@my_module) then %> +
      +
    • +
      + <%= bootstrap_form_for :comment, url: { format: :json }, method: :post, remote: true do |f| %> + <%= f.text_field :message, hide_label: true, placeholder: t("general.comment_placeholder"), append: f.submit("+"), help: '.' %> + <% end %> +
    • +
    +<% end %> diff --git a/app/views/result_comments/_list.html.erb b/app/views/result_comments/_list.html.erb new file mode 100644 index 000000000..1d2941643 --- /dev/null +++ b/app/views/result_comments/_list.html.erb @@ -0,0 +1,12 @@ +<% day = 366 %> +<% current_day = DateTime.current.strftime('%j').to_i %> + +<% comments.each do |comment| %> +
  • + <% comment_day = comment.created_at.strftime('%j').to_i %> + <% if comment_day < current_day and comment_day < day %> + <% day = comment.created_at.strftime('%j').to_i %> +

    <%= comment.created_at.strftime('%d.%m.%Y') %>

    + <% end %> + <%= render 'result_comments/comment.html.erb', comment: comment %>
  • +<% end %> diff --git a/app/views/result_comments/new.html.erb b/app/views/result_comments/new.html.erb new file mode 100644 index 000000000..2cd513ed9 --- /dev/null +++ b/app/views/result_comments/new.html.erb @@ -0,0 +1,7 @@ +<% provide(:head_title, t("result_comments.new.head_title", project: @result.my_module.project.name, module: @result.my_module.name)) %> +

    <%=t "result_comments.new.title" %>

    + +<%= bootstrap_form_for [@result, @comment], url: result_result_comments_path do |f| %> + <%= f.text_area :message, style: "margin-top: 10px;" %>
    + <%= f.submit t("result_comments.new.create"), style: "margin-top: 10px;" %> +<% end %> diff --git a/app/views/result_tables/_download.txt.erb b/app/views/result_tables/_download.txt.erb new file mode 100644 index 000000000..df3c92e0f --- /dev/null +++ b/app/views/result_tables/_download.txt.erb @@ -0,0 +1,3 @@ +<% @table_data.each do |row| %> +<%= row.join("\t") %> +<% end %> diff --git a/app/views/result_tables/_edit.html.erb b/app/views/result_tables/_edit.html.erb new file mode 100644 index 000000000..d76d2f72d --- /dev/null +++ b/app/views/result_tables/_edit.html.erb @@ -0,0 +1,17 @@ +
    + <%= bootstrap_form_for(@result, url: result_table_path(format: :json), remote: true) do |f| %> + <%= f.text_field :name, style: "margin-top: 10px;" %>
    +
    + <%= f.fields_for :table do |ff| %> + <%= ff.hidden_field(:contents, value: ff.object.contents_utf_8, class: "hot-contents" ) %> +
    +
    + <% end %> +
    +
    + <%= f.submit t("result_tables.edit.update"), class: 'btn btn-primary' %> + + <% end %> +
    diff --git a/app/views/result_tables/_new.html.erb b/app/views/result_tables/_new.html.erb new file mode 100644 index 000000000..02db4c5c3 --- /dev/null +++ b/app/views/result_tables/_new.html.erb @@ -0,0 +1,16 @@ +
    + <%= bootstrap_form_for(@result, url: my_module_result_tables_path(format: :json), remote: true) do |f| %> + <%= f.text_field :name, style: "margin-top: 10px;" %>
    +
    + <%= f.fields_for :table do |ff| %> + <%= ff.hidden_field(:contents, value: ff.object.contents, class: "hot-contents" ) %> +
    +
    + <% end %> +
    + <%= f.submit t("result_tables.new.create"), class: 'btn btn-primary' %> + + <% end %> +
    diff --git a/app/views/result_texts/_edit.html.erb b/app/views/result_texts/_edit.html.erb new file mode 100644 index 000000000..86bbb77be --- /dev/null +++ b/app/views/result_texts/_edit.html.erb @@ -0,0 +1,13 @@ +
    + <%= bootstrap_form_for(@result, url: result_text_path(format: :json), remote: :true) do |f| %> + <%= f.text_field :name, style: "margin-top: 10px;" %>
    + <%= f.fields_for :result_text do |ff| %> + <%= ff.text_area :text, style: "margin-top: 10px;" %>
    + <% end %> +
    + <%= f.submit t("result_texts.edit.update"), class: 'btn btn-primary' %> + + <% end %> +
    diff --git a/app/views/result_texts/_new.html.erb b/app/views/result_texts/_new.html.erb new file mode 100644 index 000000000..20394e581 --- /dev/null +++ b/app/views/result_texts/_new.html.erb @@ -0,0 +1,12 @@ +
    + <%= bootstrap_form_for(@result, url: my_module_result_texts_path(format: :json), remote: true) do |f| %> + <%= f.text_field :name, style: "margin-top: 10px;" %>
    + <%= f.fields_for :result_text do |ff| %> + <%= ff.text_area :text, style: "margin-top: 10px;" %>
    + <% end %> + <%= f.submit t("result_texts.new.create"), class: 'btn btn-primary' %> + + <% end %> +
    diff --git a/app/views/results/_result_asset.html.erb b/app/views/results/_result_asset.html.erb new file mode 100644 index 000000000..a6e83a5af --- /dev/null +++ b/app/views/results/_result_asset.html.erb @@ -0,0 +1,8 @@ +<% if can_download_result_assets(result.my_module) %> + <%= link_to image_tag(preview_asset_path result.asset), + download_asset_path(result.asset), data: {no_turbolink: true} if result.asset.is_image? %> +

    <%= link_to result.asset.file_file_name, download_asset_path(result.asset), data: {no_turbolink: true} %>

    +<% else %> + <%= image_tag(preview_asset_path result.asset) if result.asset.is_image? %> +

    <%= result.asset.file_file_name %>

    +<% end %> diff --git a/app/views/results/_result_table.html.erb b/app/views/results/_result_table.html.erb new file mode 100644 index 000000000..c43f70358 --- /dev/null +++ b/app/views/results/_result_table.html.erb @@ -0,0 +1,5 @@ +
    + <%= hidden_field(result.table, :contents, value: result.table.contents_utf_8, class: "hot-contents" ) %> +
    +
    +
    diff --git a/app/views/results/_result_text.html.erb b/app/views/results/_result_text.html.erb new file mode 100644 index 000000000..43bdfb385 --- /dev/null +++ b/app/views/results/_result_text.html.erb @@ -0,0 +1,5 @@ +<% if markdown.present? %> + <%= markdown.render(result.result_text.text).html_safe %> +<% else %> + <%= result.result_text.text %> +<% end %> diff --git a/app/views/sample_groups/edit.html.erb b/app/views/sample_groups/edit.html.erb new file mode 100644 index 000000000..31eddf650 --- /dev/null +++ b/app/views/sample_groups/edit.html.erb @@ -0,0 +1,9 @@ +<% provide(:head_title, t("sample_groups.edit.head_title", organization: @organization.name)) %> +

    <%=t "sample_groups.edit.title", sample_group: @sample_group.name, organization: @organization.name %>

    + +<%= bootstrap_form_for [@sample_group], url: sample_group_path do |f| %> + <%= f.text_area :name, style: "margin-top: 10px;" %> + <%= f.text_field :color %> +
    + <%= f.submit t("sample_groups.edit.create"), style: "margin-top: 10px;" %> +<% end %> diff --git a/app/views/sample_my_modules/_index.html.erb b/app/views/sample_my_modules/_index.html.erb new file mode 100644 index 000000000..3bf2a0704 --- /dev/null +++ b/app/views/sample_my_modules/_index.html.erb @@ -0,0 +1,13 @@ +
    <%= t('projects.canvas.popups.samples_tab') %>
    +
    +
      + <% if @number_of_samples == 0 then %> +
    • <%= t 'projects.canvas.popups.no_samples' %>
    • + <% else %> + <% @samples.each do |sample| %> +
    • <%= sample.name %>
    • + <% end %> + <% end %> +
      +
    • <%= link_to t("projects.canvas.popups.manage_samples"), samples_my_module_url(id: @my_module.id) %>
    • +
    diff --git a/app/views/sample_types/edit.html.erb b/app/views/sample_types/edit.html.erb new file mode 100644 index 000000000..e8c09d356 --- /dev/null +++ b/app/views/sample_types/edit.html.erb @@ -0,0 +1,8 @@ +<% provide(:head_title, t("sample_types.edit.head_title", organization: @organization.name)) %> +

    <%=t "sample_types.edit.title", sample_type: @sample_type.name, organization: @organization.name %>

    + +<%= bootstrap_form_for [@sample_type], url: sample_type_path do |f| %> + <%= f.text_area :name, style: "margin-top: 10px;" %> +
    + <%= f.submit t("sample_types.edit.create"), style: "margin-top: 10px;" %> +<% end %> diff --git a/app/views/samples/_create_sample_group_modal.html.erb b/app/views/samples/_create_sample_group_modal.html.erb new file mode 100644 index 000000000..42c860f3e --- /dev/null +++ b/app/views/samples/_create_sample_group_modal.html.erb @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/app/views/samples/_create_sample_type_modal.html.erb b/app/views/samples/_create_sample_type_modal.html.erb new file mode 100644 index 000000000..caa303041 --- /dev/null +++ b/app/views/samples/_create_sample_type_modal.html.erb @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/app/views/samples/_delete_samples_modal.html.erb b/app/views/samples/_delete_samples_modal.html.erb new file mode 100644 index 000000000..ee9292c58 --- /dev/null +++ b/app/views/samples/_delete_samples_modal.html.erb @@ -0,0 +1,18 @@ + \ No newline at end of file diff --git a/app/views/samples/_import_samples_modal.html.erb b/app/views/samples/_import_samples_modal.html.erb new file mode 100644 index 000000000..7f0fe86b4 --- /dev/null +++ b/app/views/samples/_import_samples_modal.html.erb @@ -0,0 +1,20 @@ + diff --git a/app/views/samples/_parse_samples_modal.html.erb b/app/views/samples/_parse_samples_modal.html.erb new file mode 100644 index 000000000..11b61a548 --- /dev/null +++ b/app/views/samples/_parse_samples_modal.html.erb @@ -0,0 +1,54 @@ + + +<%= javascript_include_tag("samples/samples_importer") %> diff --git a/app/views/search/index.html.erb b/app/views/search/index.html.erb new file mode 100644 index 000000000..3c5d28fae --- /dev/null +++ b/app/views/search/index.html.erb @@ -0,0 +1,211 @@ +<% provide(:head_title, t("search.index.head_title")) %> + +

    <%= t('search.index.results_title_html', query: @search_query) %>

    + +<%= form_tag search_path, method: :get do %> + <%= hidden_field_tag :q, @search_query %> + <%= hidden_field_tag :category, @search_category %> + +
    +
    + +
    + + <% if not @search_category.empty? %> +
    + +
    + <% if @search_results_count == 0 %> +

    <%= t'search.index.error.no_results', q: @search_query %>

    + <% end %> + +
    + + <% if @search_category == :projects and @project_search_count > 0 %> + <%= render 'search/results/projects', search_query: @search_query, results: @project_results %> + <% end %> + <% if @search_category == :modules and @module_search_count > 0 %> + <%= render 'search/results/modules', search_query: @search_query, results: @module_results %> + <% end %> + <% if @search_category == :workflows and @workflow_search_count > 0 %> + <%= render 'search/results/workflows', search_query: @search_query, results: @workflow_results %> + <% end %> + <% if @search_category == :tags and @tag_search_count > 0 %> + <%= render 'search/results/tags', search_query: @search_query, results: @tag_results %> + <% end %> + <% if @search_category == :assets and @asset_search_count > 0 %> + <%= render 'search/results/assets', search_query: @search_query, results: @asset_results %> + <% end %> + <% if @search_category == :steps and @step_search_count > 0 %> + <%= render 'search/results/steps', search_query: @search_query, results: @step_results %> + <% end %> + <% if @search_category == :results and @result_search_count > 0 %> + <%= render 'search/results/results', search_query: @search_query, results: @result_results %> + <% end %> + <% if @search_category == :samples and @sample_search_count > 0 %> + <%= render 'search/results/samples', search_query: @search_query, results: @sample_results %> + <% end %> + <% if @search_category == :reports and @report_search_count > 0 %> + <%= render 'search/results/reports', search_query: @search_query, results: @report_results %> + <% end %> + <% if @search_category == :comments and @comment_search_count > 0 %> + <%= render 'search/results/comments', search_query: @search_query, results: @comment_results %> + <% end %> + <% if @search_category == :contents and @contents_search_count > 0 %> + <%= render 'search/results/contents', search_query: @search_query, results: @contents_results %> + <% end %> + +
    +
    + <% end %> +
    + +<% end %> + +<% if @search_pages > 1 %> + +<% end %> diff --git a/app/views/search/new.html.erb b/app/views/search/new.html.erb new file mode 100644 index 000000000..7172eb574 --- /dev/null +++ b/app/views/search/new.html.erb @@ -0,0 +1,11 @@ +<% provide(:head_title, t("search.index.head_title")) %> + + diff --git a/app/views/search/results/_assets.html.erb b/app/views/search/results/_assets.html.erb new file mode 100644 index 000000000..c5fc67232 --- /dev/null +++ b/app/views/search/results/_assets.html.erb @@ -0,0 +1,55 @@ +<% @asset_results.each do |asset| + is_result = nil + if asset.result.present? + is_result = true + my_module = asset.result.my_module + elsif asset.step.present? + is_result = false + my_module = asset.step.my_module + end +%> +
    + <% if asset.is_image? %> + + <% else %> + + <% end %> + <%= render partial: "search/results/partials/asset_text.html.erb", locals: { asset: asset, query: search_query, is_result: is_result } %> +
    + +

    + + <%=t "search.index.created_at" %> + <%=l asset.created_at, format: :full %> + +
    + <% if is_result != nil %> + + <% if is_result %> + <%=t "search.index.result" %> + <%= render partial: "search/results/partials/result_text.html.erb", locals: { result: asset.result } %> + <% else %> + <%=t "search.index.step" %> + <%= render partial: "search/results/partials/step_text.html.erb", locals: { step: asset.step } %> + <% end %> + +
    + + <%=t "search.index.module" %> + <%= render partial: "search/results/partials/my_module_text.html.erb", locals: { my_module: my_module } %> + +
    + + <%=t "search.index.project" %> + <%= render partial: "search/results/partials/project_text.html.erb", locals: { project: my_module.project } %> + +
    + + <%=t "search.index.organization" %> + <%= render partial: "search/results/partials/organization_text.html.erb", locals: { organization: my_module.project.organization } %> + + <% end %> +

    + +
    +<% end %> diff --git a/app/views/search/results/_comments.html.erb b/app/views/search/results/_comments.html.erb new file mode 100644 index 000000000..84e035c5c --- /dev/null +++ b/app/views/search/results/_comments.html.erb @@ -0,0 +1,58 @@ +<% results.each do |comment| %> + +

    + + <% if comment.project_comment.present? %> + <%=t "search.index.comments.project" %> + <% elsif comment.my_module_comment.present? %> + <%=t "search.index.comments.my_module" %> + <% elsif comment.step_comment.present? %> + <%=t "search.index.comments.step" %> + <% elsif comment.result_comment.present? %> + <%=t "search.index.comments.result" %> + <% end %> +

    +
    +

    + <%= highlight comment.message, @search_query %> +

    +
    +

    + + <%=t "search.index.created_by" %> + <%= highlight comment.user.full_name, @search_query %> + +
    + + <%=t "search.index.created_at" %> + <%=l comment.created_at, format: :full %> + +
    +

    + +

    + <% if comment.project_comment.present? %> + + <%=t "search.index.project" %> + <%= render partial: "search/results/partials/project_text.html.erb", locals: { project: comment.project_comment.project } %> + + <% elsif comment.my_module_comment.present? %> + + <%=t "search.index.module" %> + <%= render partial: "search/results/partials/my_module_text.html.erb", locals: { my_module: comment.my_module_comment.my_module } %> + + <% elsif comment.step_comment.present? %> + + <%=t "search.index.step" %> + <%= render partial: "search/results/partials/step_text.html.erb", locals: { step: comment.step_comment.step } %> + + <% elsif comment.result_comment.present? %> + + <%=t "search.index.result" %> + <%= render partial: "search/results/partials/result_text.html.erb", locals: { result: comment.result_comment.result } %> + + <% end %> +

    + +
    +<% end %> \ No newline at end of file diff --git a/app/views/search/results/_contents.erb b/app/views/search/results/_contents.erb new file mode 100644 index 000000000..41dc52436 --- /dev/null +++ b/app/views/search/results/_contents.erb @@ -0,0 +1,7 @@ +<% @contents_results.each do |content| %> +
    + + <%= render partial: "search/results/partials/content_text.html.erb", locals: { content: content, query: search_query } %> +
    +
    +<% end %> diff --git a/app/views/search/results/_modules.html.erb b/app/views/search/results/_modules.html.erb new file mode 100644 index 000000000..85f5890f8 --- /dev/null +++ b/app/views/search/results/_modules.html.erb @@ -0,0 +1,35 @@ +<% results.each do |mod| %> +
    + + <%= render partial: "search/results/partials/my_module_text.html.erb", locals: { my_module: mod, query: search_query } %> +
    + + <% if mod.description and mod.description.include? @search_query %> +

    + + <%=t "search.index.description" %> + <%= highlight mod.description, @search_query %> + +

    + <% end %> + +

    + + <%=t "search.index.created_at" %> + <%=l mod.created_at, format: :full %> + +
    + + <%=t "search.index.project" %> + <%= render partial: "search/results/partials/project_text.html.erb", locals: { project: mod.project } %> + +
    + + <%=t "search.index.organization" %> + <%= render partial: "search/results/partials/organization_text.html.erb", locals: { organization: mod.project.organization } %> + +

    + +
    +<% end %> + diff --git a/app/views/search/results/_projects.html.erb b/app/views/search/results/_projects.html.erb new file mode 100644 index 000000000..83a3f02f9 --- /dev/null +++ b/app/views/search/results/_projects.html.erb @@ -0,0 +1,20 @@ +<% results.each do |project| %> +
    + + <%= render partial: "search/results/partials/project_text.html.erb", locals: { project: project, query: search_query } %> +
    + +

    + + <%=t "search.index.created_at" %> + <%=l project.created_at, format: :full %> + +
    + + <%=t "search.index.organization" %> + <%= render partial: "search/results/partials/organization_text.html.erb", locals: { organization: project.organization } %> + +

    + +
    +<% end %> diff --git a/app/views/search/results/_reports.html.erb b/app/views/search/results/_reports.html.erb new file mode 100644 index 000000000..71fe4024d --- /dev/null +++ b/app/views/search/results/_reports.html.erb @@ -0,0 +1,52 @@ +<% results.each do |report| %> +
    + + <%= render partial: "search/results/partials/report_text.html.erb", locals: { report: report, query: search_query } %> +
    + +

    + + <%=t "search.index.description" %> + <% if report.description.present? %> + <%= highlight report.description, @search_query %> + <% else %> + <%=t "search.index.no_description" %> + <% end %> + +
    + + <%=t "search.index.created_by" %> + <%= highlight report.user.full_name, @search_query %> + +
    + + <%=t "search.index.created_at" %> + <%=l report.created_at, format: :full %> + +
    + + <%=t "search.index.last_modified_by" %> + <%= highlight report.last_modified_by.full_name, @search_query %> + +
    + + <%=t "search.index.last_modified_at" %> + <%=l report.updated_at, format: :full %> + +

    + +

    + + <%=t "search.index.project" %> + <%= render partial: "search/results/partials/project_text.html.erb", locals: { project: report.project } %> + +
    + + <%=t "search.index.organization" %> + <%= render partial: "search/results/partials/organization_text.html.erb", locals: { organization: report.project.organization } %> + +

    + +
    +<% end %> + diff --git a/app/views/search/results/_results.html.erb b/app/views/search/results/_results.html.erb new file mode 100644 index 000000000..8da032072 --- /dev/null +++ b/app/views/search/results/_results.html.erb @@ -0,0 +1,40 @@ +<% results.each do |result| %> +
    + <% if result.is_text %> + + <% elsif result.is_table %> + + <% else %> + <% if result.asset.is_image? %> + + <% else %> + + <% end %> + <% end %> + <%= render partial: "search/results/partials/result_text.html.erb", locals: { result: result, query: search_query } %> +
    + +

    + + <%=t "search.index.created_at" %> + <%=l result.created_at, format: :full %> + +
    + + <%=t "search.index.module" %> + <%= render partial: "search/results/partials/my_module_text.html.erb", locals: { my_module: result.my_module } %> + +
    + + <%=t "search.index.project" %> + <%= render partial: "search/results/partials/project_text.html.erb", locals: { project: result.my_module.project } %> + +
    + + <%=t "search.index.organization" %> + <%= render partial: "search/results/partials/organization_text.html.erb", locals: { organization: result.my_module.project.organization } %> + +

    + +
    +<% end %> diff --git a/app/views/search/results/_samples.html.erb b/app/views/search/results/_samples.html.erb new file mode 100644 index 000000000..bcfcb6870 --- /dev/null +++ b/app/views/search/results/_samples.html.erb @@ -0,0 +1,68 @@ +<% results.each do |sample| %> +
    + + <%=t "search.index.samples.sample" %> + <%= highlight sample.name, search_query %> +
    + +

    + + <%=t "search.index.samples.sample_type" %> + <% if sample.sample_type.present? %> + <%= highlight sample.sample_type.name, search_query %> + <% else %> + <%=t "search.index.samples.no_sample_type" %> + <% end %> + +
    + + <%=t "search.index.samples.sample_group" %> + "> + <% if sample.sample_group.present? %> + <%= highlight sample.sample_group.name, search_query %> + <% else %> + <%=t "search.index.samples.no_sample_group" %> + <% end %> + +
    + + <%=t "search.index.samples.added_on" %> + <%=l sample.created_at, format: :full %> + +
    + + <%=t "search.index.samples.added_by" %> + <%= highlight sample.user.full_name, search_query %> + + <% sample.sample_custom_fields.each do |sample_custom_field| %> +
    + + <%=t "search.index.samples.custom_field", cf: sample_custom_field.custom_field.name %> + <%= highlight sample_custom_field.value, search_query %> + + <% end %> +

    + +

    + + <%=t "search.index.modules" %> + <% if sample.my_modules.count > 0 %> + <% sample.my_modules.each_with_index do |mod, i| %> + <%= render partial: "search/results/partials/my_module_text.html.erb", locals: { my_module: mod } %> + <% if i != sample.my_modules.count - 1 %> + ,  + <% end %> + <% end %> + <% else %> + <%=t "search.index.sample_no_modules" %> + <% end %> + +
    + + <%=t "search.index.organization" %> + <%= render partial: "search/results/partials/organization_text.html.erb", locals: { organization: sample.organization } %> + +

    + +
    +<% end %> diff --git a/app/views/search/results/_steps.html.erb b/app/views/search/results/_steps.html.erb new file mode 100644 index 000000000..1813d69ea --- /dev/null +++ b/app/views/search/results/_steps.html.erb @@ -0,0 +1,31 @@ +<% results.each do |step| %> +
    + + <%= render partial: "search/results/partials/step_text.html.erb", locals: { step: step, query: search_query } %> +
    + +

    + + <%=t "search.index.created_at" %> + <%=l step.created_at, format: :full %> + +
    + + <%=t "search.index.module" %> + <%= render partial: "search/results/partials/my_module_text.html.erb", locals: { my_module: step.my_module } %> + +
    + + <%=t "search.index.project" %> + <%= render partial: "search/results/partials/project_text.html.erb", locals: { project: step.my_module.project } %> + +
    + + <%=t "search.index.organization" %> + <%= render partial: "search/results/partials/organization_text.html.erb", locals: { organization: step.my_module.project.organization } %> + +

    + +
    +<% end %> + diff --git a/app/views/search/results/_tags.html.erb b/app/views/search/results/_tags.html.erb new file mode 100644 index 000000000..e7323eff8 --- /dev/null +++ b/app/views/search/results/_tags.html.erb @@ -0,0 +1,34 @@ +<% results.each do |tag| %> +
    + + <%= render partial: "search/results/partials/tag_text.html.erb", locals: { tag: tag, query: search_query } %> +
    + +

    + + <%=t "search.index.created_at" %> + <%=l tag.created_at, format: :full %> + +
    + + <%=t "search.index.modules" %> + <% if tag.my_modules.count > 0 %> + <% tag.my_modules.each_with_index do |mod, i| %> + <%= render partial: "search/results/partials/my_module_text.html.erb", locals: { my_module: mod } %> + <% if i != tag.my_modules.count - 1 %> + ,  + <% end %> + <% end %> + <% else %> + <%=t "search.index.tag_no_modules" %> + <% end %> + +
    + + <%=t "search.index.project" %> + <%= render partial: "search/results/partials/project_text.html.erb", locals: { project: tag.project } %> + +

    + +
    +<% end %> \ No newline at end of file diff --git a/app/views/search/results/_workflows.html.erb b/app/views/search/results/_workflows.html.erb new file mode 100644 index 000000000..72e35aa92 --- /dev/null +++ b/app/views/search/results/_workflows.html.erb @@ -0,0 +1,25 @@ +<% results.each do |workflow| %> +
    + + <%= highlight workflow.name, search_query %> +
    + +

    + + <%=t "search.index.created_at" %> + <%=l workflow.created_at, format: :full %> + +
    + + <%=t "search.index.project" %> + <%= render partial: "search/results/partials/project_text.html.erb", locals: { project: workflow.project } %> + +
    + + <%=t "search.index.organization" %> + <%= render partial: "search/results/partials/organization_text.html.erb", locals: { organization: workflow.project.organization } %> + +

    + +
    +<% end %> diff --git a/app/views/search/results/partials/_asset_text.html.erb b/app/views/search/results/partials/_asset_text.html.erb new file mode 100644 index 000000000..2cd9f267d --- /dev/null +++ b/app/views/search/results/partials/_asset_text.html.erb @@ -0,0 +1,25 @@ +<% is_result ||= nil %> +<% query ||= nil %> +<% text = query.present? ? highlight(asset.file_file_name, query) : asset.file_file_name %> + +<% if is_result.blank? %> + <%= text %> +<% elsif is_result %> + + <% if can_download_result_assets(asset.result.my_module) %> + + <%= text %> + + <% else %> + <%= text %> + <% end %> +<% else %> + + <% if can_download_step_assets(asset.step.my_module) %> + + <%= text %> + + <% else %> + <%= text %> + <% end %> +<% end %> diff --git a/app/views/search/results/partials/_content_text.html.erb b/app/views/search/results/partials/_content_text.html.erb new file mode 100644 index 000000000..7fd03729f --- /dev/null +++ b/app/views/search/results/partials/_content_text.html.erb @@ -0,0 +1,16 @@ +<% if content.asset.step and can_download_step_assets(content.asset.step.my_module) %> + + <%= content.asset.file_file_name %> + +<% elsif content.asset.result and can_download_result_assets(content.asset.result.my_module) %> + + <%= content.asset.file_file_name %> + +<% else %> +<%= t'search.index.file', {filename: content.asset.file_file_name} %> +<% end %> +

    + +
    +

    <%= raw content.headline %>

    +
    diff --git a/app/views/search/results/partials/_my_module_text.html.erb b/app/views/search/results/partials/_my_module_text.html.erb new file mode 100644 index 000000000..e7e3a1fbb --- /dev/null +++ b/app/views/search/results/partials/_my_module_text.html.erb @@ -0,0 +1,21 @@ +<% query ||= nil %> +<% text = query.present? ? highlight(my_module.name, query) : my_module.name %> + +<% if my_module.archived? %> + <%=t "search.index.archived" %> + <% if can_view_project_archive(my_module.project) and can_restore_module(my_module) %> + + <%= text %> + + <% else %> + <%= text %> + <% end %> +<% else %> + <% if can_view_module(my_module) %> + + <%= text %> + + <% else %> + <%= text %> + <% end %> +<% end %> diff --git a/app/views/search/results/partials/_organization_text.html.erb b/app/views/search/results/partials/_organization_text.html.erb new file mode 100644 index 000000000..9669942e9 --- /dev/null +++ b/app/views/search/results/partials/_organization_text.html.erb @@ -0,0 +1,7 @@ +<% if can_view_projects(organization) %> + + <%= organization.name %> + +<% else %> + <%= organization.name %> +<% end %> \ No newline at end of file diff --git a/app/views/search/results/partials/_project_text.html.erb b/app/views/search/results/partials/_project_text.html.erb new file mode 100644 index 000000000..79417c19a --- /dev/null +++ b/app/views/search/results/partials/_project_text.html.erb @@ -0,0 +1,21 @@ +<% query ||= nil %> +<% text = query.present? ? highlight(project.name, query) : project.name %> + +<% if project.archived? %> + <%=t "search.index.archived" %> + <% if can_view_projects(project.organization) and can_restore_project(project) %> + + <%= text %> + + <% else %> + <%= text %> + <% end %> +<% else %> + <% if can_view_project(project) %> + + <%= text %> + + <% else %> + <%= text %> + <% end %> +<% end %> \ No newline at end of file diff --git a/app/views/search/results/partials/_report_text.html.erb b/app/views/search/results/partials/_report_text.html.erb new file mode 100644 index 000000000..ae4531b6f --- /dev/null +++ b/app/views/search/results/partials/_report_text.html.erb @@ -0,0 +1,10 @@ +<% query ||= nil %> +<% text = query.present? ? highlight(report.name, query) : report.name %> + +<% if can_view_reports(report.project) %> + + <%= text %> + +<% else %> + <%= text %> +<% end %> \ No newline at end of file diff --git a/app/views/search/results/partials/_result_text.html.erb b/app/views/search/results/partials/_result_text.html.erb new file mode 100644 index 000000000..0d5009999 --- /dev/null +++ b/app/views/search/results/partials/_result_text.html.erb @@ -0,0 +1,21 @@ +<% query ||= nil %> +<% text = query.present? ? highlight(result.name, query) : result.name %> + +<% if result.archived? %> + <%=t "search.index.archived" %> + <% if can_view_module_archive(result.my_module) %> + + <%= text %> + + <% else %> + <%= text %> + <% end %> +<% else %> + <% if can_view_results_in_module(result.my_module) %> + + <%= text %> + + <% else %> + <%= text %> + <% end %> +<% end %> \ No newline at end of file diff --git a/app/views/search/results/partials/_step_text.html.erb b/app/views/search/results/partials/_step_text.html.erb new file mode 100644 index 000000000..04c130e2b --- /dev/null +++ b/app/views/search/results/partials/_step_text.html.erb @@ -0,0 +1,10 @@ +<% query ||= nil %> +<% text = query.present? ? highlight(step.name, query) : step.name %> + +<% if can_view_steps_in_module(step.my_module) %> + + <%= text %> + +<% else %> + <%= text %> +<% end %> \ No newline at end of file diff --git a/app/views/search/results/partials/_tag_text.html.erb b/app/views/search/results/partials/_tag_text.html.erb new file mode 100644 index 000000000..2163821b6 --- /dev/null +++ b/app/views/search/results/partials/_tag_text.html.erb @@ -0,0 +1,10 @@ +<% query ||= nil %> +<% text = query.present? ? highlight(tag.name, query) : tag.name %> + +<% if can_view_project(tag.project) %> + + <%= text %> + +<% else %> + <%= text %> +<% end %> diff --git a/app/views/shared/_navigation.html.erb b/app/views/shared/_navigation.html.erb new file mode 100644 index 000000000..8b056a12d --- /dev/null +++ b/app/views/shared/_navigation.html.erb @@ -0,0 +1,90 @@ + +
    + + +<%= javascript_include_tag("navigation") %> diff --git a/app/views/shared/_sample.html.erb b/app/views/shared/_sample.html.erb new file mode 100644 index 000000000..d7c68e359 --- /dev/null +++ b/app/views/shared/_sample.html.erb @@ -0,0 +1,23 @@ + + +<% if assigned %> + + + <% else %> + + + <% end %> + + <%= sample.name %> + <% if sample.sample_type %> + <%= sample.sample_type.name %> + <% else %> + <%= t("samples.table.no_type")%> + <% end %> + <% if sample.sample_group %> + <%= sample.sample_group.name %> + <% else %> + <%= t("samples.table.no_group")%> + <% end %> + <%= l(sample.created_at, format: :full) %> + <%= sample.user.full_name %> diff --git a/app/views/shared/_samples.html.erb b/app/views/shared/_samples.html.erb new file mode 100644 index 000000000..206a52cbf --- /dev/null +++ b/app/views/shared/_samples.html.erb @@ -0,0 +1,153 @@ +<%= render partial: "custom_fields/new_modal" %> +<%= render partial: "samples/import_samples_modal" %> +<%= render partial: "samples/delete_samples_modal" %> +<%= render partial: "samples/create_sample_group_modal" %> +<%= render partial: "samples/create_sample_type_modal" %> + + + +
    + +<% if can_view_samples(@organization) %> + <%= bootstrap_form_tag(url: export_samples_organization_path(@organization, format: :csv), html: {id: "form-export", class: "hidden"}) do |f| %> + <% end %> +<% end %> + +<%= form_for :sample, url: form_submit_link, html: {id: "form-samples"} do |f|%> + +
    + data-module-id="<%= @my_module.id %>" + data-samples-step-text="<%=t 'tutorial.samples_html' %>" + data-breadcrumbs-step-text="<%=t 'tutorial.breadcrumbs_html' %>" + <% end %>> + + <% if can_create_samples(@organization) %> + + + <% end %> + + <% if can_view_samples(@organization) %> + + + + + <% end %> + +
    +
    + + +
    + + + + + + +
    + + + + + + + + + + + <% all_custom_fields.each do |cf| %> + + <% end %> + + + +
    <%= t("samples.table.assigned") %><%= t("samples.table.sample_name") %><%= t("samples.table.sample_type") %><%= t("samples.table.sample_group") %><%= t("samples.table.added_on") %><%= t("samples.table.added_by") %><%= cf.name %>
    +
    +<% end %> +<%= stylesheet_link_tag 'datatables' %> +<%= javascript_include_tag("samples/samples") %> +<%= javascript_include_tag("samples/sample_datatable") %> diff --git a/app/views/shared/_secondary_navigation.html.erb b/app/views/shared/_secondary_navigation.html.erb new file mode 100644 index 000000000..9f708712c --- /dev/null +++ b/app/views/shared/_secondary_navigation.html.erb @@ -0,0 +1,175 @@ +<% content_for :secondary_navigation do %> + +<% end %> diff --git a/app/views/shared/_sidebar.html.erb b/app/views/shared/_sidebar.html.erb new file mode 100644 index 000000000..7a84edf6c --- /dev/null +++ b/app/views/shared/_sidebar.html.erb @@ -0,0 +1,96 @@ +<% content_for :sidebar do %> +
    + + + + +
    +
      +
    • "> + + + <% if is_module_page? %> + <%= link_to @project.name, project_action_to_link_to(@project), title: @project.name %> + <% else %> + <%= @project.name %> + <% end %> + + <% if @project.active_modules.present? then %> +
        + <% @project.active_module_groups.each do |my_module_group| %> +
      • + + + + <%= my_module_group.name %> + + <% if is_canvas? %> + + <% end %> + + <% if my_module_group.my_modules.present? then %> +
          + <% my_module_group.my_modules.sort_by{|m| m.workflow_order}.each do |my_module| %> +
        • " data-module-id="<%= my_module.id %>"> + + <% if currently_active? my_module %> + <%= my_module.name %> + <% else %> + <% if can_view_module(my_module) then %> + <%= link_to my_module.name, module_action_to_link_to(my_module) %> + <% else %> + <%= my_module.name %> + <% end %> + <% end %> + <% if is_canvas? %> + + <% end %> + +
        • + <% end %> +
        + <% end %> +
      • + <% end %> + <% modules_without_group = @project.modules_without_group %> + <% if modules_without_group.present? then %> +
      • + + + <%= t("sidebar.no_module_group") %> + +
          + <% modules_without_group.each do |my_module| %> +
        • " data-module-id="<%= my_module.id %>"> + + <% if currently_active? my_module %> + <%= my_module.name %> + <% else %> + <%= link_to my_module.name, module_action_to_link_to(my_module) %> + <% end %> + <% if is_canvas? %> + + <% end %> + +
        • + <% end %> +
        +
      • + <% end %> +
      + <% end %> +
    • +
    +
    +
    +<% end %> + +<%= javascript_include_tag("sidebar") %> diff --git a/app/views/step_comments/_comment.html.erb b/app/views/step_comments/_comment.html.erb new file mode 100644 index 000000000..39b701b05 --- /dev/null +++ b/app/views/step_comments/_comment.html.erb @@ -0,0 +1,2 @@ +<%=t "my_modules.steps.comment_title", user: comment.user.full_name, time: l(comment.created_at, format: :time) %> +

    <%= comment.message %>

    diff --git a/app/views/step_comments/_index.html.erb b/app/views/step_comments/_index.html.erb new file mode 100644 index 000000000..ac5009a52 --- /dev/null +++ b/app/views/step_comments/_index.html.erb @@ -0,0 +1,26 @@ +
    <%= t('my_modules.steps.comments_tab') %>
    +
    +
      + <% if @comments.size == 0 then %> +
    • <%= t 'general.no_comments' %>
    • + <% else %> + <%= render 'step_comments/list.html.erb', comments: @comments %> + <% end %> + <% if @comments.length == @per_page %> +
    • + + <%=t "general.more_comments" %> + +
    • + <% end %> +
    +<% if can_add_step_comment_in_module(@my_module) %> +
      +
    • +
      + <%= bootstrap_form_for :comment, url: { format: :json }, method: :post, remote: true do |f| %> + <%= f.text_field :message, hide_label: true, placeholder: t("general.comment_placeholder"), append: f.submit("+"), help: '.' %> + <% end %> +
    • +
    +<% end %> diff --git a/app/views/step_comments/_list.html.erb b/app/views/step_comments/_list.html.erb new file mode 100644 index 000000000..19683d30d --- /dev/null +++ b/app/views/step_comments/_list.html.erb @@ -0,0 +1,12 @@ +<% day = 366 %> +<% current_day = DateTime.current.strftime('%j').to_i %> + +<% comments.each do |comment| %> +
  • + <% comment_day = comment.created_at.strftime('%j').to_i %> + <% if comment_day < current_day and comment_day < day %> + <% day = comment.created_at.strftime('%j').to_i %> +

    <%= comment.created_at.strftime('%d.%m.%Y') %>

    + <% end %> + <%= render 'step_comments/comment.html.erb', comment: comment %>
  • +<% end %> diff --git a/app/views/step_comments/new.html.erb b/app/views/step_comments/new.html.erb new file mode 100644 index 000000000..2c2996326 --- /dev/null +++ b/app/views/step_comments/new.html.erb @@ -0,0 +1,7 @@ +<% provide(:head_title, t("step_comments.new.head_title", project: @step.my_module.project.name, module: @step.my_module.name)) %> +

    <%=t "step_comments.new.title", step: (@step.position + 1).to_s %>

    + +<%= bootstrap_form_for [@step, @comment], url: step_step_comments_path do |f| %> + <%= f.text_area :message, style: "margin-top: 10px;" %>
    + <%= f.submit t("step_comments.new.create"), class: 'btn btn-primary' %> +<% end %> diff --git a/app/views/steps/_edit.html.erb b/app/views/steps/_edit.html.erb new file mode 100644 index 000000000..5a97a9686 --- /dev/null +++ b/app/views/steps/_edit.html.erb @@ -0,0 +1,16 @@ +
    + <%= bootstrap_form_for(@step, url: step_path(id: @step.id, format: :json), remote: true, authenticity_token: true, multipart: true, data: { type: :json }) do |f| %> +

    <%=t "my_modules.steps.edit.edit_step_title" %>

    +
    + <%= render partial: "empty_step.html.erb", locals: {step: @step, f: f} %> +
    + <% if direct_upload %> + <%= f.submit t("my_modules.steps.edit.edit_step"), class: 'btn btn-primary', onclick: 'startFileUpload(event, this);' %> + <% else %> + <%= f.submit t("my_modules.steps.edit.edit_step"), class: 'btn btn-primary' %> + <% end %> + + <%= t("general.cancel")%> + + <% end %> +
    diff --git a/app/views/steps/_empty_step.html.erb b/app/views/steps/_empty_step.html.erb new file mode 100644 index 000000000..0c8055902 --- /dev/null +++ b/app/views/steps/_empty_step.html.erb @@ -0,0 +1,59 @@ + +
    +
    + <%= f.text_field :name, label: t("my_modules.steps.new.name"), autofocus: true, placeholder: t("my_modules.steps.new.name_placeholder") %> + + <%= f.text_area :description, label: t("my_modules.steps.new.description"), autofocus: true, placeholder: t("my_modules.steps.new.description_placeholder") %> +
    +
    + <%= f.nested_fields_for :checklists do |ff| %> + <%= render "form_checklists.html.erb", ff: ff %> + <% end %> + <%= f.add_nested_fields_link :checklists do %> + + <%=t "my_modules.steps.new.add_checklist" %> + <% end %> +
    +
    + <%= f.nested_fields_for :assets do |ff| %> + <%= render "form_assets.html.erb", ff: ff %> + <% end %> + <%= f.add_nested_fields_link :assets do %> + + <%=t "my_modules.steps.new.add_asset" %> + <% end %> +
    +
    + <%= f.nested_fields_for :tables do |ff| %> + <%= render "form_tables.html.erb", ff: ff %> + <% end %> + <%= f.add_nested_fields_link :tables, id: "add-table" do %> + + <%=t "my_modules.steps.new.add_table" %> + <% end %> +
    +
    diff --git a/app/views/steps/_form_assets.html.erb b/app/views/steps/_form_assets.html.erb new file mode 100644 index 000000000..21a09b370 --- /dev/null +++ b/app/views/steps/_form_assets.html.erb @@ -0,0 +1,22 @@ +
    +
    + + <%=t "my_modules.steps.new.asset_panel_title" %> +
    + <%= ff.remove_nested_fields_link do %> + + <% end %> +
    +
    +
    + <% if ff.object.file.exists? %> + <% if !(ff.object.file.content_type =~ /^image/).nil? %> + <%= image_tag ff.object.file.url(:medium) %> + <% else %> + <%= ff.object.file_file_name %> + <% end %> + <% else %> + <%= ff.file_field :file %> + <% end %> +
    +
    \ No newline at end of file diff --git a/app/views/steps/_form_checklists.html.erb b/app/views/steps/_form_checklists.html.erb new file mode 100644 index 000000000..4f10c443b --- /dev/null +++ b/app/views/steps/_form_checklists.html.erb @@ -0,0 +1,41 @@ +
    +
    + + <%=t "my_modules.steps.new.checklist_panel_title" %> +
    + <%= ff.remove_nested_fields_link do %> + + <% end %> +
    +
    +
    + <%= ff.text_field :name, label: t("my_modules.steps.new.checklist_name"), autofocus: true, placeholder: t("my_modules.steps.new.checklist_name_placeholder") %> + <%= ff.label t("my_modules.steps.new.checklist_items") %> +
      + <%= ff.nested_fields_for :checklist_items, ordered_checklist_items(ff.object) do |chkItems| %> +
    • +
      + +
      +
      + <%= chkItems.text_field :text, autofocus: true, placeholder: t("my_modules.steps.new.checklist_item_placeholder"), hide_label: true, class: "form-control" %> + <%= chkItems.hidden_field :position, class: "checklist-item-pos" %> +
      +
      + +
      +
      +
      +
    • + <% end %> +
    + <%= ff.add_nested_fields_link :checklist_items do %> + + <%=t "my_modules.steps.new.checklist_add_item" %> + <% end %> +
    +
    diff --git a/app/views/steps/_form_tables.html.erb b/app/views/steps/_form_tables.html.erb new file mode 100644 index 000000000..69b5bc3f5 --- /dev/null +++ b/app/views/steps/_form_tables.html.erb @@ -0,0 +1,15 @@ +
    +
    + + <%=t "my_modules.steps.new.table_panel_title" %> +
    + <%= ff.remove_nested_fields_link do %> + + <% end %> +
    +
    +
    + <%= ff.hidden_field(:contents, value: ff.object.contents_utf_8, class: "hot-contents" ) %> +
    +
    +
    \ No newline at end of file diff --git a/app/views/steps/_new.html.erb b/app/views/steps/_new.html.erb new file mode 100644 index 000000000..4334da273 --- /dev/null +++ b/app/views/steps/_new.html.erb @@ -0,0 +1,16 @@ +
    + <%= bootstrap_form_for(@step, url: my_module_steps_path(@my_module.id, format: :json), remote: true, authenticity_token: true, multipart: true, data: { type: :json }) do |f| %> +

    <%=t "my_modules.steps.new.add_step_title" %>

    +
    + <%= render partial: "empty_step.html.erb", locals: {step: @step, f: f} %> +
    + <% if direct_upload %> + <%= f.submit t("my_modules.steps.new.add_step"), id: "create-step", class: 'btn btn-primary', onclick: 'startFileUpload(event, this);' %> + <% else %> + <%= f.submit t("my_modules.steps.new.add_step"), id: "create-step", class: 'btn btn-primary' %> + <% end %> + + <% end %> +
    diff --git a/app/views/user_my_modules/_index.html.erb b/app/views/user_my_modules/_index.html.erb new file mode 100644 index 000000000..b6fb9ba89 --- /dev/null +++ b/app/views/user_my_modules/_index.html.erb @@ -0,0 +1,33 @@ +
    <%=t "projects.canvas.popups.users_tab" %>
    +
    +
      + <% if @user_my_modules.size == 0 then %> +
    • <%= t "projects.canvas.popups.no_users" %>
    • + <% else %> + <% @user_my_modules.each_with_index do |user_my_module, i| %> + <% user = user_my_module.user %> +
    • + <% if i > 0 %>
      <% end %> +
      +
      + <%= image_tag avatar_path(:icon_small), class: "img-circle pull-left" %> +
      +
      + <%= user.full_name %>
      + "> + + <%=t "projects.canvas.popups.module_user_join", date: l(user_my_module.created_at, format: :full_date) %> + + +
      +
      +
    • + <% end %> + <% end %> + <% if can_edit_users_on_module(@my_module) then %> +
    • +
      + <%= link_to t('projects.canvas.popups.manage_users'), my_module_users_edit_path(@my_module, format: :json), remote: true, class: "manage-users-link" %> +
    • + <% end %> +
    diff --git a/app/views/user_my_modules/_index_edit.html.erb b/app/views/user_my_modules/_index_edit.html.erb new file mode 100644 index 000000000..c9b180eca --- /dev/null +++ b/app/views/user_my_modules/_index_edit.html.erb @@ -0,0 +1,54 @@ +
      + +<% if @user_my_modules.size == 0 then %> +
    • <%= t 'projects.canvas.full_zoom.modal_manage_users.no_users' %>
    • +<% else %> + <% @user_my_modules.each_with_index do |umm, i| user = umm.user %> +
    • + <% if i > 0 %>
      <% end %> +
      + +
      + <%= image_tag avatar_path(:icon_small), class: 'img-circle pull-left' %> +
      + +
      + <%= user.full_name %> +
      "> + <%=t "projects.canvas.full_zoom.modal_manage_users.user_join", date: l(umm.created_at, format: :full_date) %> + +
      + + <% if can_remove_user_from_module(@my_module) then %> +
      + <%= link_to my_module_user_my_module_path(@my_module, umm, format: :json), method: :delete, remote: true, class: 'btn btn-link remove-user-link' do %> + + <% end %> +
      + <% end %> + +
      +
    • + <% end %> +<% end %> + +<% if can_add_user_to_module(@my_module) and @unassigned_users.count > 0 %> +
    • +
      +
      + <%= bootstrap_form_for [@my_module, @new_um], remote: true, format: :json, html: { class: 'add-user-form' } do |f| %> +
      + <%= collection_select(:user_my_module, :user_id, @unassigned_users, :id, :full_name, {}, { class: 'selectpicker' }) %> +
      +
      + <%= f.button class: 'btn btn-primary' do %> + + + <% end %> +
      + <% end %> +
      +
    • +<% end %> + +
    diff --git a/app/views/user_my_modules/new.html.erb b/app/views/user_my_modules/new.html.erb new file mode 100644 index 000000000..a48dc070e --- /dev/null +++ b/app/views/user_my_modules/new.html.erb @@ -0,0 +1,13 @@ +<% provide(:head_title, t("user_my_modules.new.head_title", project: @my_module.project.name, module: @my_module.name)) %> +

    <%=t "user_my_modules.new.title", module: @my_module.name %>

    + +<%= bootstrap_form_for [@my_module, @um] do |f| %> + <% if @users.empty? then %> +

    <%= t("user_my_modules.new.no_users_available") %>

    + <%= link_to t("user_my_modules.new.back_button"), :back %> + <% else %> + <%= collection_select(:user_my_module, :user_id, @users, :id, :full_name ) %> +
    + <%= f.submit t("user_my_modules.new.assign_user"), style: "margin-top: 10px;" %> + <% end %> +<% end %> diff --git a/app/views/user_projects/_index.html.erb b/app/views/user_projects/_index.html.erb new file mode 100644 index 000000000..4e64b8370 --- /dev/null +++ b/app/views/user_projects/_index.html.erb @@ -0,0 +1,28 @@ +
    <%= t("projects.index.users_tab") %>
    +
    +
      + <% if @users.size == 0 then %> +
    • <%= t 'projects.index.no_users' %>
    • + <% else %> + <% @users.each_index do |i| user = @users[i].user %> +
    • + <% if i > 0 %>
      <% end %> +
      +
      + <%= image_tag avatar_path(:icon_small), class: 'img-circle pull-left' %> +
      +
      + <%= user.full_name %> +
      <%= t('user_projects.enums.role.'<<@users[i].role.to_s) %> +
      +
      +
    • + <% end %> + <% end %> + <% if can_edit_users_on_project(@project) %> +
    • +
      + <%= link_to t("projects.index.manage_users"), project_users_edit_path(@project, format: :json), class: "manage-users-link", remote: true %> +
    • + <% end %> +
    diff --git a/app/views/user_projects/_index_edit.html.erb b/app/views/user_projects/_index_edit.html.erb new file mode 100644 index 000000000..c951faabe --- /dev/null +++ b/app/views/user_projects/_index_edit.html.erb @@ -0,0 +1,79 @@ +
      + +<% if @users.size == 0 then %> +
    • <%= t 'projects.index.modal_manage_users.no_users' %>
    • +<% else %> + <% @users.each_index do |i| user = @users[i].user %> +
    • + <% if i > 0 %>
      <% end %> +
      + +
      + <%= image_tag avatar_path(:icon_small), class: 'img-circle pull-left' %> +
      + +
      + <%= user.full_name %> +
      <%= t('user_projects.enums.role.'<<@users[i].role) %> +
      + + <% unless user.id == current_user.id %> +
       
      +
      + <%= form_for @up, url: project_user_project_path(@project, @users[i].id, method: :put, format: :json), format: :json, method: 'put', remote: true, html: { class: 'update-user-form' } do |f| %> + <% # TODO replace with form helper %> + + <% # TODO replace hardcoded select html with rails helper %> + + <% end %> +
      +
      + <%= link_to project_user_project_path(@project, @users[i], format: :json), method: :delete, remote: true, class: 'btn btn-link remove-user-link' do %> + + <% end %> +
      + + <% end %> +
      +
    • + <% end %> +<% end %> + +<% if @unassigned_users.count > 0 %> +
    • +
      +
      + <%= bootstrap_form_for [@project, @up], remote: true, format: :json, html: { class: 'add-user-form' } do |f| %> + <%= hidden_field_tag :project_id, @project.id %> +
      + <%= collection_select(:user_project, :user_id, @unassigned_users, :id, :full_name, {}, { class: 'selectpicker' }) %> +
      +
       
      +
      + <% # TODO replace hardcoded select html with rails helper %> + +
      +
      + <%= f.button class: 'btn btn-primary' do %> + + + <% end %> +
      + <% end %> +
      +
    • +<% end %> + +
    diff --git a/app/views/user_projects/edit.html.erb b/app/views/user_projects/edit.html.erb new file mode 100644 index 000000000..c34ac200b --- /dev/null +++ b/app/views/user_projects/edit.html.erb @@ -0,0 +1,26 @@ +<% provide(:head_title, t("user_projects.edit.head_title", project: @project.name)) %> +

    <%=t "user_projects.edit.title", user: @up.user.full_name, project: @project.name %>

    + +<%= bootstrap_form_for [@project, @up] do |f| %> + <%= f.label t("user_projects.new.role"), style: "margin-top: 10px;" %>
    +
    + + + + +
    +
    + <%= f.submit t("user_projects.edit.update"), style: "margin-top: 10px;" %> +<% end %> \ No newline at end of file diff --git a/app/views/user_projects/new.html.erb b/app/views/user_projects/new.html.erb new file mode 100644 index 000000000..8e4a2f7b0 --- /dev/null +++ b/app/views/user_projects/new.html.erb @@ -0,0 +1,28 @@ +<% provide(:head_title, t("user_projects.new.head_title", project: @project.name)) %> +

    <%=t "user_projects.new.title", project: @project.name %>

    + +<%= bootstrap_form_for [@project, @up] do |f| %> + <%= collection_select(:user_project, :user_id, @users, :id, :full_name ) %> +
    + <%= f.label t("user_projects.new.role"), style: "margin-top: 10px;" %>
    +
    + + + + +
    +
    + <%= f.submit t("user_projects.new.create"), style: "margin-top: 10px;" %> +<% end %> diff --git a/app/views/users/invitations/edit.html.erb b/app/views/users/invitations/edit.html.erb new file mode 100644 index 000000000..193561fad --- /dev/null +++ b/app/views/users/invitations/edit.html.erb @@ -0,0 +1,67 @@ +<% provide(:head_title, t("users.invitations.edit.head_title")) %> + +
    + +

    <%= t 'devise.invitations.edit.header' %>

    + +<%= form_for (resource or :user), :as => resource_name, :url => invitation_path(resource_name), :html => { :method => :put } do |f| %> + + <%= f.hidden_field :invitation_token %> + +
    + <%= f.label :password %> + <% if @minimum_password_length %> + (<%= @minimum_password_length %> characters minimum) + <% end %> + <%= f.password_field :password, autofocus: true, autocomplete: "off", class: "form-control" %> +
    + +
    + <%= f.label :password_confirmation %> + <%= f.password_field :password_confirmation, autocomplete: "off", class: "form-control" %> +
    + +
    + <%= label :organization, :name, t('users.invitations.edit.name_label') %> + <% if @org %> + <%= text_field :organization, :name, class: "form-control", value: @org.name %> + <% else %> + <%= text_field :organization, :name, class: "form-control" %> + <% end %> + <%= t 'users.invitations.edit.name_help' %> +
    + +
    + <%= f.submit "Sign Up", class: "btn btn-primary" %> +
    + +<% end %> +
    + +<% if resource and not resource.errors.empty? %> + +<% end %> + +<% if @org and not @org.errors.empty? %> + +<% end %> diff --git a/app/views/users/invitations/new.html.erb b/app/views/users/invitations/new.html.erb new file mode 100644 index 000000000..b5acf475a --- /dev/null +++ b/app/views/users/invitations/new.html.erb @@ -0,0 +1,12 @@ +

    <%= t "devise.invitations.new.header" %>

    + +<%= form_for resource, :as => resource_name, :url => invitation_path(resource_name), :html => {:method => :post} do |f| %> + <%= devise_error_messages! %> + +<% resource.class.invite_key_fields.each do |field| -%> +

    <%= f.label field %>
    + <%= f.text_field field %>

    +<% end -%> + +

    <%= f.submit t("devise.invitations.new.submit_button") %>

    +<% end %> diff --git a/app/views/users/mailer/invitation_instructions.html.erb b/app/views/users/mailer/invitation_instructions.html.erb new file mode 100644 index 000000000..e73e32fb9 --- /dev/null +++ b/app/views/users/mailer/invitation_instructions.html.erb @@ -0,0 +1,11 @@ +

    <%= t("devise.mailer.invitation_instructions.hello", email: @resource.email) %>

    + +

    <%= t("devise.mailer.invitation_instructions.someone_invited_you", url: root_url) %>

    + +

    <%= link_to t("devise.mailer.invitation_instructions.accept"), accept_invitation_url(@resource, :invitation_token => @token) %>

    + +<% if @resource.invitation_due_at %> +

    <%= t("devise.mailer.invitation_instructions.accept_until", due_date: l(@resource.invitation_due_at, format: :'devise.mailer.invitation_instructions.accept_until_format')) %>

    +<% end %> + +

    <%= t("devise.mailer.invitation_instructions.ignore").html_safe %>

    diff --git a/app/views/users/registrations/edit.html.erb b/app/views/users/registrations/edit.html.erb new file mode 100644 index 000000000..8f138cce3 --- /dev/null +++ b/app/views/users/registrations/edit.html.erb @@ -0,0 +1,175 @@ +<% provide(:head_title, t("users.registrations.edit.head_title")) %> + +
    +

    <%=t "users.registrations.edit.title" %>

    + + <% if not resource.errors.empty? %> +
    + <%= devise_error_messages! %> +
    + <% end %> + + <%= form_for(resource, as: resource_name, url: registration_path(resource_name), html: { method: :put, "data-for" => "avatar" }) do |f| %> + <%= hidden_field_tag "user[change_avatar]", "true" %> +
    +
    + <%= f.label t("users.registrations.edit.avatar_label") %>
    + <% @user_avatar_url ||= avatar_path(:thumb) %> + <%= image_tag @user_avatar_url %>

    + <%=t "users.registrations.edit.avatar_btn" %> +
    +
    +
    +
    +

    <%=t "users.registrations.edit.avatar_title" %>

    +
    + <%= f.label :avatar, t("users.registrations.edit.avatar_edit_label") %> + <%= f.file_field :avatar, class: "btn btn-default" %> +
    +
    + <%=t "general.cancel" %> + <% if @direct_upload %> + <%= f.submit t("users.registrations.edit.avatar_submit"), class: "btn btn-primary", onclick: 'startFileUpload(event, this);' %> + <% else %> + <%= f.submit t("users.registrations.edit.avatar_submit"), class: "btn btn-primary" %> + <% end %> +
    +
    +
    + <% end %> + + <%= form_for(resource, as: resource_name, url: registration_path(resource_name, format: :json), remote: true, html: { method: :put, "data-for" => "full_name" }) do |f| %> +
    +
    + <%= f.label t("users.registrations.edit.name_label") %> + +
    +
    +
    +
    +

    <%=t "users.registrations.edit.name_title" %>

    +
    + <%= f.label :full_name, t("users.registrations.edit.name_label") %> + <%= f.text_field :full_name, class: "form-control", "data-role" => "edit" %> +
    +
    + <%=t "general.cancel" %> + <%= f.submit t("general.update"), class: "btn btn-primary" %> +
    +
    +
    + <% end %> + + <%= form_for(resource, as: resource_name, url: registration_path(resource_name, format: :json), remote: true, html: { method: :put, "data-for" => "initials" }) do |f| %> +
    +
    + <%= f.label t("users.registrations.edit.initials_label") %> + +
    +
    +
    +
    +

    <%=t "users.registrations.edit.initials_title" %>

    +
    + <%= f.label :initials, t("users.registrations.edit.initials_label") %> + <%= f.text_field :initials, class: "form-control", "data-role" => "edit" %> +
    +
    + <%=t "general.cancel" %> + <%= f.submit t("general.update"), class: "btn btn-primary" %> +
    +
    +
    + <% end %> + + <%= form_for(resource, as: resource_name, url: registration_path(resource_name, format: :json), remote: true, html: { method: :put, "data-for" => "email" }) do |f| %> +
    +
    + <%= f.label t("users.registrations.edit.email_label") %> + + <% if devise_mapping.confirmable? && resource.pending_reconfirmation? %> + + <% end %> +
    +
    +
    +
    +

    <%=t "users.registrations.edit.email_title" %>

    +
    + <%= f.label :email, t("users.registrations.edit.new_email_label") %> + <%= f.email_field :email, class: "form-control", "data-role" => "edit" %> +
    +
    + <%= f.label :current_password, t("users.registrations.edit.current_password_label") %> <%=t "users.registrations.edit.password_explanation" %> + <%= f.password_field :current_password, autocomplete: "off", class: "form-control", "data-role" => "clear" %> +
    +
    + <%=t "general.cancel" %> + <%= f.submit t("general.update"), class: "btn btn-primary" %> +
    +
    +
    + <% end %> + + <%= form_for(resource, as: resource_name, url: registration_path(resource_name, format: :json), remote: true, html: { method: :put, "data-for" => "password" }) do |f| %> + <%= hidden_field_tag "user[change_password]", "true" %> +
    +
    + <%= f.label t("users.registrations.edit.password_label") %> + +
    +
    +
    +
    +

    <%=t "users.registrations.edit.password_title" %>

    +
    + <%= f.label :current_password, t("users.registrations.edit.current_password_label") %> <%=t "users.registrations.edit.password_explanation" %> + <%= f.password_field :current_password, autocomplete: "off", class: "form-control", "data-role" => "clear" %> +
    + +
    + <%= f.label :password, t("users.registrations.edit.new_password_label") %> + <%= f.password_field :password, autocomplete: "off", class: "form-control", "data-role" => "clear" %> +
    + +
    + <%= f.label :password_confirmation, t("users.registrations.edit.new_password_2_label") %> + <%= f.password_field :password_confirmation, autocomplete: "off", class: "form-control", "data-role" => "clear" %> +
    + +
    + <%=t "general.cancel" %> + <%= f.submit t("general.update"), class: "btn btn-primary" %> +
    +
    +
    + <% end %> + +
    + +<%= javascript_include_tag("canvas-to-blob.min") %> +<%= javascript_include_tag("direct-upload") %> +<%= javascript_include_tag "users/registrations/edit" %> diff --git a/app/views/users/registrations/new.html.erb b/app/views/users/registrations/new.html.erb new file mode 100644 index 000000000..feed19d84 --- /dev/null +++ b/app/views/users/registrations/new.html.erb @@ -0,0 +1,76 @@ +<% provide(:head_title, t("users.registrations.new.head_title")) %> + +
    +

    Sign up

    + + <%= form_for(:user, as: resource_name, url: registration_path(resource_name)) do |f| %> + +
    + <%= f.label :full_name %> + <%= f.text_field :full_name, autofocus: true, class: "form-control" %> +
    + +
    + <%= f.label :email %> + <%= f.email_field :email, class: "form-control" %> +
    + +
    + <%= f.label :password %> + <% if @minimum_password_length %> + (<%= @minimum_password_length %> characters minimum) + <% end %> + <%= f.password_field :password, autocomplete: "off", class: "form-control" %> +
    + +
    + <%= f.label :password_confirmation %> + <%= f.password_field :password_confirmation, autocomplete: "off", class: "form-control" %> +
    + +
    + <%= label :organization, :name %> + <% if @org %> + <%= text_field :organization, :name, class: "form-control", value: @org.name %> + <% else %> + <%= text_field :organization, :name, class: "form-control" %> + <% end %> + <%= t'users.registrations.new.name_help' %> +
    + +
    + <%= f.submit "Sign up", class: "btn btn-primary" %> +
    + <% end %> + + <%= render "devise/shared/links" %> +
    + +<% if resource and not resource.errors.empty? %> + +<% end %> + +<% if @org and not @org.errors.empty? %> + +<% end %> + diff --git a/app/views/users/settings/_navigation.html.erb b/app/views/users/settings/_navigation.html.erb new file mode 100644 index 000000000..00e2c963a --- /dev/null +++ b/app/views/users/settings/_navigation.html.erb @@ -0,0 +1,8 @@ + \ No newline at end of file diff --git a/app/views/users/settings/new_organization.html.erb b/app/views/users/settings/new_organization.html.erb new file mode 100644 index 000000000..f849a22e3 --- /dev/null +++ b/app/views/users/settings/new_organization.html.erb @@ -0,0 +1,32 @@ +<% provide(:head_title, t("users.settings.organizations.head_title")) %> + +<%= render partial: "users/settings/navigation.html.erb" %> +
    +
    +
    + + <%= render partial: "users/settings/organizations/breadcrumbs.html.erb" %> + + <%= bootstrap_form_for @new_org, url: create_organization_path do |f| %> +
    + <%= f.text_field :name, label: t("users.settings.organizations.new.name_label"), autofocus: true, placeholder: t("users.settings.organizations.new.name_placeholder") %> + + <%= t("users.settings.organizations.new.name_sublabel") %> + +
    + +
    + <%= f.text_area :description, label: t("users.settings.organizations.new.description_label") %> + + <%= t("users.settings.organizations.new.description_sublabel") %> + +
    + + <%= link_to t("general.cancel"), organizations_path, class: "btn btn-default" %> + <%= f.submit t("users.settings.organizations.new.create"), class: "btn btn-primary" %> + + <% end %> + + +
    +
    \ No newline at end of file diff --git a/app/views/users/settings/organization.html.erb b/app/views/users/settings/organization.html.erb new file mode 100644 index 000000000..8e99a59cc --- /dev/null +++ b/app/views/users/settings/organization.html.erb @@ -0,0 +1,118 @@ +<% provide(:head_title, t("users.settings.organizations.head_title")) %> + +<%= render partial: "users/settings/navigation.html.erb" %> +
    +
    +
    + + <%= render partial: "users/settings/organizations/breadcrumbs.html.erb" %> + + + <%= link_to organization_name_path(@org, format: :json), remote: true, class: "name-link name-refresh", style: "color: inherit" do %> +

    <%= @org.name %>

    + <% end %> +
    + + + +
    +
    +
    + +
    +
    + + <%= l(@org.created_at, format: :full) %> +
    +
    + +
    +
    + +
    +
    + + <%= l(@user_org.created_at, format: :full) %> +
    +
    + +
    +
    + +
    +
    + + <%= "#{number_to_human_size(@org.space_taken)}" %> +
    +
    +
    + +
    +
    + <%= link_to organization_description_path(@org, format: :json), remote: true, class: "description-link", style: "color: inherit" do %> + + <% end %> +
    +
    + <%= link_to organization_description_path(@org, format: :json), remote: true, class: "description-label description-link description-refresh", style: "color: inherit" do %> + <%= render partial: "users/settings/organizations/description_label.html.erb", locals: { org: @org } %> + <% end %> +
    +
    + + + +
    +
    + <%= t("users.settings.organizations.edit.manage_users") %> +
    +
    + <%= link_to "#", class: "btn btn-primary", data: { toggle: "modal", target: "#add-user-modal" } do %> + + <%= t("users.settings.organizations.edit.add_user") %> + <% end %> +
    + + + + + + + + + + + + +
    <%= t("users.settings.organizations.edit.thead_user_name") %><%= t("users.settings.organizations.edit.thead_email") %><%= t("users.settings.organizations.edit.thead_joined_on") %><%= t("users.settings.organizations.edit.thead_status") %><%= t("users.settings.organizations.edit.thead_role") %>
    +
    +
    +
    + + + +
    +
    + <%= t("users.settings.organizations.edit.delete_organization_heading") %> +
    +
    + <% if @org.projects.count > 0 %> + <%= t("users.settings.organizations.edit.cannot_delete_message_projects") %> + <% else %> + <%= t("users.settings.organizations.edit.can_delete_message") %> + <%= t("users.settings.organizations.edit.delete_text") %> + <% end %> +
    +
    + + +
    +
    + +<%= render partial: "users/settings/organizations/name_modal.html.erb" %> +<%= render partial: "users/settings/organizations/description_modal.html.erb" %> +<%= render partial: "users/settings/organizations/add_user_modal.html.erb", locals: { org: @org } %> +<%= render partial: "users/settings/organizations/destroy_modal.html.erb", locals: { org: @org } %> +<%= render partial: "users/settings/organizations/destroy_user_organization_modal.html.erb" %> +<%= stylesheet_link_tag 'datatables' %> +<%= javascript_include_tag "users/settings/organization" %> \ No newline at end of file diff --git a/app/views/users/settings/organizations.html.erb b/app/views/users/settings/organizations.html.erb new file mode 100644 index 000000000..c72366f55 --- /dev/null +++ b/app/views/users/settings/organizations.html.erb @@ -0,0 +1,93 @@ +<% provide(:head_title, t("users.settings.organizations.head_title")) %> + +<%= render partial: "users/settings/navigation.html.erb" %> +
    +
    +
    + + <%= render partial: "users/settings/organizations/breadcrumbs.html.erb" %> + +
    + <% if @member_of > 0 %> + <%= t("users.settings.organizations.index.member_of", count: @member_of) %> + <% else %> + <%= t("users.settings.organizations.index.no_organizations") %> + <% end %> + <%= link_to new_organization_path, class: "btn btn-default", style: "margin-left: 30px;" do %> + + + <% end %> +
    + + <% if @member_of > 0 %> + + + + + + + + + + + + + <% @user_orgs.each do |user_org| %> + <% org = user_org.organization %> + + + + + + + + + <% end %> + +
    <%=t "users.settings.organizations.index.thead_name" %><%=t "users.settings.organizations.index.thead_role" %><%=t "users.settings.organizations.index.thead_members" %> 
    + <% if user_org.admin? %> + <%= link_to org.name, organization_path(org) %> + <% else %> + <%= org.name %> + <% end %> + <%= user_org.role_str %> + <% if user_org.guest? %> + <%= t("users.settings.organizations.index.na") %> + <% else %> + <%= org.users.count %> + <% end %> + + + <% if user_org.admin? && org.user_organizations.where(role: 2).count <= 1 %> +
    + + +
    + <% else %> + <%= link_to leave_user_organization_html_path(user_org, format: :json), remote: true, class: "btn btn-default btn-xs", type: "button", data: { action: "leave-user-organization" } do %> + + + <% end %> + <% end %> +
    + <% else %> +
    + <% end %> +
    +
    + +<%= render partial: "users/settings/organizations/leave_user_organization_modal.html.erb" %> +<%= javascript_include_tag "users/settings/organizations" %> \ No newline at end of file diff --git a/app/views/users/settings/organizations/_add_user_modal.html.erb b/app/views/users/settings/organizations/_add_user_modal.html.erb new file mode 100644 index 000000000..0b41bf8d3 --- /dev/null +++ b/app/views/users/settings/organizations/_add_user_modal.html.erb @@ -0,0 +1,92 @@ + \ No newline at end of file diff --git a/app/views/users/settings/organizations/_breadcrumbs.html.erb b/app/views/users/settings/organizations/_breadcrumbs.html.erb new file mode 100644 index 000000000..37385710c --- /dev/null +++ b/app/views/users/settings/organizations/_breadcrumbs.html.erb @@ -0,0 +1,21 @@ + \ No newline at end of file diff --git a/app/views/users/settings/organizations/_description_label.html.erb b/app/views/users/settings/organizations/_description_label.html.erb new file mode 100644 index 000000000..d7d0be67d --- /dev/null +++ b/app/views/users/settings/organizations/_description_label.html.erb @@ -0,0 +1,5 @@ +<% if org.description.present? and not org.description.empty? %> + <%= org.description %> +<% else %> + <%= t("users.settings.organizations.edit.header_no_description") %> +<% end %> \ No newline at end of file diff --git a/app/views/users/settings/organizations/_description_modal.html.erb b/app/views/users/settings/organizations/_description_modal.html.erb new file mode 100644 index 000000000..5017ad7be --- /dev/null +++ b/app/views/users/settings/organizations/_description_modal.html.erb @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/app/views/users/settings/organizations/_description_modal_body.html.erb b/app/views/users/settings/organizations/_description_modal_body.html.erb new file mode 100644 index 000000000..9740a7d5b --- /dev/null +++ b/app/views/users/settings/organizations/_description_modal_body.html.erb @@ -0,0 +1,3 @@ +<%= bootstrap_form_for org, url: update_organization_path(org, format: :json), remote: :true, method: :put do |f| %> + <%= f.text_area :description, label: t("users.settings.organizations.edit.description_label") %> +<% end %> \ No newline at end of file diff --git a/app/views/users/settings/organizations/_destroy_modal.html.erb b/app/views/users/settings/organizations/_destroy_modal.html.erb new file mode 100644 index 000000000..6354e9d2b --- /dev/null +++ b/app/views/users/settings/organizations/_destroy_modal.html.erb @@ -0,0 +1,19 @@ + \ No newline at end of file diff --git a/app/views/users/settings/organizations/_destroy_user_organization_modal.html.erb b/app/views/users/settings/organizations/_destroy_user_organization_modal.html.erb new file mode 100644 index 000000000..1366c1b0d --- /dev/null +++ b/app/views/users/settings/organizations/_destroy_user_organization_modal.html.erb @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/app/views/users/settings/organizations/_destroy_user_organization_modal_body.html.erb b/app/views/users/settings/organizations/_destroy_user_organization_modal_body.html.erb new file mode 100644 index 000000000..66af6fe70 --- /dev/null +++ b/app/views/users/settings/organizations/_destroy_user_organization_modal_body.html.erb @@ -0,0 +1,3 @@ +<%= bootstrap_form_for user_organization, url: destroy_user_organization_path(user_organization, format: :json), remote: :true, method: :delete, data: { id: "destroy-user-organization-form" } do |f| %> + <%= t("users.settings.organizations.edit.destroy_uo_message", user: user_organization.user.full_name, org: user_organization.organization.name) %> +<% end %> \ No newline at end of file diff --git a/app/views/users/settings/organizations/_existing_users_search_results.html.erb b/app/views/users/settings/organizations/_existing_users_search_results.html.erb new file mode 100644 index 000000000..c02915e6f --- /dev/null +++ b/app/views/users/settings/organizations/_existing_users_search_results.html.erb @@ -0,0 +1,25 @@ +<% if users.count > 0 %> + <%= bootstrap_form_for UserOrganization.new, url: create_user_organization_path, data: { id: "create-user-organization-form" } do %> + + + +
    + + <% users.each do |user| %> + + <% end %> +
    + <% if nr_of_results > users.count %> +
    + <%= t("users.settings.organizations.edit.modal_add_user.existing_users_smalltext", nr: users.count) %> +
    + <% end %> + <% end %> +<% else %> + <%= t("users.settings.organizations.edit.modal_add_user.no_existing_users") %> +<% end %> \ No newline at end of file diff --git a/app/views/users/settings/organizations/_leave_user_organization_modal.html.erb b/app/views/users/settings/organizations/_leave_user_organization_modal.html.erb new file mode 100644 index 000000000..16c61621b --- /dev/null +++ b/app/views/users/settings/organizations/_leave_user_organization_modal.html.erb @@ -0,0 +1,16 @@ + \ No newline at end of file diff --git a/app/views/users/settings/organizations/_leave_user_organization_modal_body.html.erb b/app/views/users/settings/organizations/_leave_user_organization_modal_body.html.erb new file mode 100644 index 000000000..5f053deba --- /dev/null +++ b/app/views/users/settings/organizations/_leave_user_organization_modal_body.html.erb @@ -0,0 +1,4 @@ +<%= bootstrap_form_for user_organization, url: destroy_user_organization_path(user_organization, format: :json), remote: :true, method: :delete, data: { id: "leave-user-organization-form" } do |f| %> + <%= hidden_field_tag :leave, true %> + <%= t("users.settings.organizations.index.leave_uo_message", org: user_organization.organization.name) %> +<% end %> \ No newline at end of file diff --git a/app/views/users/settings/organizations/_name_modal.html.erb b/app/views/users/settings/organizations/_name_modal.html.erb new file mode 100644 index 000000000..e6cecbbf4 --- /dev/null +++ b/app/views/users/settings/organizations/_name_modal.html.erb @@ -0,0 +1,17 @@ + \ No newline at end of file diff --git a/app/views/users/settings/organizations/_name_modal_body.html.erb b/app/views/users/settings/organizations/_name_modal_body.html.erb new file mode 100644 index 000000000..c9d8170d8 --- /dev/null +++ b/app/views/users/settings/organizations/_name_modal_body.html.erb @@ -0,0 +1,3 @@ +<%= bootstrap_form_for org, url: update_organization_path(org, format: :json), remote: :true, method: :put do |f| %> + <%= f.text_field :name, label: t("users.settings.organizations.edit.name_label") %> +<% end %> \ No newline at end of file diff --git a/app/views/users/settings/organizations/_user_dropdown.html.erb b/app/views/users/settings/organizations/_user_dropdown.html.erb new file mode 100644 index 000000000..64ed63387 --- /dev/null +++ b/app/views/users/settings/organizations/_user_dropdown.html.erb @@ -0,0 +1,62 @@ +<% if user_organization.user == user %> +
    + + <%= t("users.settings.organizations.edit.user_dropdown.btn_label") %>  + +
    +<% else %> + <% id = "user-#{user_organization.user.id}-dropdown" %> + +<% end %> \ No newline at end of file diff --git a/app/views/users/settings/preferences.html.erb b/app/views/users/settings/preferences.html.erb new file mode 100644 index 000000000..2a8c731ef --- /dev/null +++ b/app/views/users/settings/preferences.html.erb @@ -0,0 +1,39 @@ +<% provide(:head_title, t("users.settings.preferences.head_title")) %> + +<%= render partial: "users/settings/navigation.html.erb" %> +
    +
    + + <%= form_for(@user, url: update_preferences_path(format: :json), remote: true, html: { method: :put, "data-for" => "time_zone" }) do |f| %> +
    +
    + <%= f.label t("users.settings.preferences.edit.time_zone_label") %> + + <%= t("users.settings.preferences.edit.time_zone_sublabel") %> +
    +
    +
    +
    +

    <%=t "settings.preferences.edit.time_zone_title" %>

    +
    + <%= f.select :time_zone, ActiveSupport::TimeZone.all.collect {|tz| [tz.formatted_offset + " " + tz.name, tz.name] }, {}, {class: "form-control selectpicker", "data-role" => "clear"} %> + <%= t("users.settings.preferences.edit.time_zone_sublabel") %> +
    +
    + <%=t "general.cancel" %> + <%= f.submit t("general.update"), class: "btn btn-primary" %> +
    +
    +
    + <% end %> + +
    +
    +
    + +<%= javascript_include_tag "users/settings/preferences" %> \ No newline at end of file diff --git a/bin/bundle b/bin/bundle new file mode 100644 index 000000000..e3c2f622b --- /dev/null +++ b/bin/bundle @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby.exe +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +load Gem.bin_path('bundler', 'bundle') diff --git a/bin/delayed_job b/bin/delayed_job new file mode 100644 index 000000000..edf195985 --- /dev/null +++ b/bin/delayed_job @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby + +require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment')) +require 'delayed/command' +Delayed::Command.new(ARGV).daemonize diff --git a/bin/rails b/bin/rails new file mode 100644 index 000000000..c9a0f3891 --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby.exe +APP_PATH = File.expand_path('../../config/application', __FILE__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/bin/rake b/bin/rake new file mode 100644 index 000000000..f6ed5a2a6 --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby.exe +require_relative '../config/boot' +require 'rake' +Rake.application.run diff --git a/bin/setup b/bin/setup new file mode 100644 index 000000000..2d041ee3d --- /dev/null +++ b/bin/setup @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby.exe +require 'pathname' + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) + +Dir.chdir APP_ROOT do + # This script is a starting point to setup your application. + # Add necessary setup steps to this file: + + puts "== Installing dependencies ==" + system "gem install bundler --conservative" + system "bundle check || bundle install" + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # system "cp config/database.yml.sample config/database.yml" + # end + + puts "\n== Preparing database ==" + system "bin/rake db:setup" + + puts "\n== Removing old logs and tempfiles ==" + system "rm -f log/*" + system "rm -rf tmp/cache" + + puts "\n== Restarting application server ==" + system "touch tmp/restart.txt" +end diff --git a/config.ru b/config.ru new file mode 100644 index 000000000..bd83b2541 --- /dev/null +++ b/config.ru @@ -0,0 +1,4 @@ +# This file is used by Rack-based servers to start the application. + +require ::File.expand_path('../config/environment', __FILE__) +run Rails.application diff --git a/config/application.rb b/config/application.rb new file mode 100644 index 000000000..605efe45d --- /dev/null +++ b/config/application.rb @@ -0,0 +1,36 @@ +require File.expand_path('../boot', __FILE__) + +require 'rails/all' + +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + +module Scinote + class Application < Rails::Application + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. + + # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. + # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. + # config.time_zone = 'Central Time (US & Canada)' + + # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. + # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] + # config.i18n.default_locale = :de + + config.active_job.queue_adapter = :delayed_job + + # Do not swallow errors in after_commit/after_rollback callbacks. + config.active_record.raise_in_transactional_callbacks = true + + # Logging + config.log_formatter = proc do |severity, datetime, progname, msg| + "[#{datetime}] #{severity}: #{msg}\n" + end + + # Paperclip spoof checking + Paperclip.options[:content_type_mappings] = {:csv => "text/plain"} + end +end diff --git a/config/boot.rb b/config/boot.rb new file mode 100644 index 000000000..6b750f00b --- /dev/null +++ b/config/boot.rb @@ -0,0 +1,3 @@ +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) + +require 'bundler/setup' # Set up gems listed in the Gemfile. diff --git a/config/database.yml b/config/database.yml new file mode 100644 index 000000000..f028fdda5 --- /dev/null +++ b/config/database.yml @@ -0,0 +1,85 @@ +# PostgreSQL. Versions 8.2 and up are supported. +# +# Install the pg driver: +# gem install pg +# On OS X with Homebrew: +# gem install pg -- --with-pg-config=/usr/local/bin/pg_config +# On OS X with MacPorts: +# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config +# On Windows: +# gem install pg +# Choose the win32 build. +# Install PostgreSQL and put its /bin directory on your path. +# +# Configure Using Gemfile +# gem 'pg' +# +default: &default + adapter: postgresql + encoding: unicode + database: postgres + pool: 5 + username: postgres + password: mysecretpassword + host: db + # For details on connection pooling, see rails configuration guide + # http://guides.rubyonrails.org/configuring.html#database-pooling + +development: + <<: *default + database: scinote_development + + # The specified database role being used to connect to postgres. + # To create additional roles in postgres see `$ createuser --help`. + # When left blank, postgres will use the default role. This is + # the same name as the operating system user that initialized the database. + # username: postgres + + # The password associated with the postgres role (username). + # password: mysecretpassword + + # Connect on a TCP socket. Omitted by default since the client uses a + # domain socket that doesn't need configuration. Windows does not have + # domain sockets, so uncomment these lines. + + # The TCP port the server listens on. Defaults to 5432. + # If your server runs on a different port number, change accordingly. + #port: 5432 + + # Schema search path. The server defaults to $user,public + #schema_search_path: myapp,sharedapp,public + + # Minimum log levels, in increasing order: + # debug5, debug4, debug3, debug2, debug1, + # log, notice, warning, error, fatal, and panic + # Defaults to warning. + #min_messages: notice + +# Warning: The database defined as "test" will be erased and +# re-generated from your development database when you run "rake". +# Do not set this db to the same as development or production. +test: + <<: *default + database: scinote_test + +# As with config/secrets.yml, you never want to store sensitive information, +# like your database password, in your source code. If your source code is +# ever seen by anyone, they now have access to your database. +# +# Instead, provide the password as a unix environment variable when you boot +# the app. Read http://guides.rubyonrails.org/configuring.html#configuring-a-database +# for a full rundown on how to provide these environment variables in a +# production deployment. +# +# On Heroku and other platform providers, you may have a full connection URL +# available as an environment variable. For example: +# +# DATABASE_URL="postgres://myuser:mypass@localhost/somedatabase" +# +# You can use this database configuration with: +# +# production: +# url: <%= ENV['DATABASE_URL'] %> +# +production: + <<: *default diff --git a/config/environment.rb b/config/environment.rb new file mode 100644 index 000000000..e70d286d0 --- /dev/null +++ b/config/environment.rb @@ -0,0 +1,6 @@ +# Load the Rails application. +require File.expand_path('../application', __FILE__) + +# Initialize the Rails application. +Rails.application.initialize! + diff --git a/config/environments/development.rb b/config/environments/development.rb new file mode 100644 index 000000000..c88d50f43 --- /dev/null +++ b/config/environments/development.rb @@ -0,0 +1,64 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false + + # Do not eager load code on boot. + config.eager_load = false + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Don't care if the mailer can't send. + config.action_mailer.default_url_options = { + host: Rails.application.secrets.mail_server_url + } + config.action_mailer.default_options = { + from: Rails.application.secrets.mailer_from, + reply_to: Rails.application.secrets.mailer_reply_to + } + config.action_mailer.raise_delivery_errors = true + config.action_mailer.delivery_method = :smtp + + config.action_mailer.smtp_settings = { + address: Rails.application.secrets.mailer_address, + port: Rails.application.secrets.mailer_port, + domain: Rails.application.secrets.mailer_domain, + authentication: "plain", + enable_starttls_auto: true, + user_name: Rails.application.secrets.mailer_user_name, + password: Rails.application.secrets.mailer_password + } + #config.action_mailer.perform_deliveries = false + + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log + + # Raise an error on page load if there are pending migrations. + config.active_record.migration_error = :page_load + + # Debug mode disables concatenation and preprocessing of assets. + # This option may cause significant delays in view rendering with a large + # number of complex assets. + config.assets.debug = true + + # Asset digests allow you to set far-future HTTP expiration dates on all assets, + # yet still be able to expire them through the digest params. + config.assets.digest = true + + # Adds additional error checking when serving assets at runtime. + # Checks for improperly declared sprockets dependencies. + # Raises helpful error messages. + config.assets.raise_runtime_errors = true + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true + + # Enable first-time tutorial for users signing in the sciNote for + # the first time. + config.x.enable_tutorial = ENV["ENABLE_TUTORIAL"] || false +end diff --git a/config/environments/production.rb b/config/environments/production.rb new file mode 100644 index 000000000..31e442aa1 --- /dev/null +++ b/config/environments/production.rb @@ -0,0 +1,105 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Code is not reloaded between requests. + config.cache_classes = true + + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true + + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true + + # Don't care if the mailer can't send. + config.action_mailer.default_url_options = { + host: Rails.application.secrets.mail_server_url + } + config.action_mailer.default_options = { + from: Rails.application.secrets.mailer_from, + reply_to: Rails.application.secrets.mailer_reply_to + } + config.action_mailer.raise_delivery_errors = true + config.action_mailer.delivery_method = :smtp + + config.action_mailer.smtp_settings = { + address: Rails.application.secrets.mailer_address, + port: Rails.application.secrets.mailer_port, + domain: Rails.application.secrets.mailer_domain, + authentication: "plain", + enable_starttls_auto: true, + user_name: Rails.application.secrets.mailer_user_name, + password: Rails.application.secrets.mailer_password + } + #config.action_mailer.perform_deliveries = false + + # Enable Rack::Cache to put a simple HTTP cache in front of your application + # Add `rack-cache` to your Gemfile before enabling this. + # For large-scale production use, consider using a caching reverse proxy like + # NGINX, varnish or squid. + # config.action_dispatch.rack_cache = true + + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? + + # Compress JavaScripts and CSS. + config.assets.js_compressor = :uglifier + # config.assets.css_compressor = :sass + + # Do not fallback to assets pipeline if a precompiled asset is missed. + config.assets.compile = false + + # Asset digests allow you to set far-future HTTP expiration dates on all assets, + # yet still be able to expire them through the digest params. + config.assets.digest = true + + # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb + + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = ENV['RAILS_FORCE_SSL'].present? + + # Use the lowest log level to ensure availability of diagnostic information + # when problems arise. + config.log_level = :debug + + # Prepend all log lines with the following tags. + # config.log_tags = [ :subdomain, :uuid ] + + # Use a different logger for distributed setups. + # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + + # Use a different cache store in production. + # config.cache_store = :mem_cache_store + + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.action_controller.asset_host = 'http://assets.example.com' + + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false + + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true + + # Send deprecation notices to registered listeners. + config.active_support.deprecation = :notify + + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new + + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false + + # Enable first-time tutorial for users signing in the sciNote for + # the first time. + config.x.enable_tutorial = ENV["ENABLE_TUTORIAL"] || true +end diff --git a/config/environments/test.rb b/config/environments/test.rb new file mode 100644 index 000000000..c3a380d8c --- /dev/null +++ b/config/environments/test.rb @@ -0,0 +1,71 @@ +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. + + # Enable this to be able to output stuff to STDOUT during tests + # via Rails::logger.info "..." + config.logger = Logger.new(STDOUT) + config.log_level = :info + + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! + config.cache_classes = true + + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false + + # Configure static file server for tests with Cache-Control for performance. + config.serve_static_files = true + config.static_cache_control = 'public, max-age=3600' + + # Show full error reports and disable caching. + config.consider_all_requests_local = true + config.action_controller.perform_caching = false + + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = false + + # Disable request forgery protection in test environment. + config.action_controller.allow_forgery_protection = false + + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Don't care if the mailer can't send. + config.action_mailer.default_url_options = { host: Rails.application.secrets.mail_server_url } + config.action_mailer.default_options = { from: Rails.application.secrets.mail_from } + config.action_mailer.raise_delivery_errors = false + config.action_mailer.delivery_method = :smtp + + config.action_mailer.smtp_settings = { + address: Rails.application.secrets.mailer_address, + port: Rails.application.secrets.mailer_port, + domain: Rails.application.secrets.mailer_domain, + authentication: "plain", + enable_starttls_auto: true, + user_name: Rails.application.secrets.mailer_user_name, + password: Rails.application.secrets.mailer_password + } + #config.action_mailer.perform_deliveries = false + + # Randomize the order test cases are executed. + config.active_support.test_order = :random + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + + config.logger = Logger.new(STDOUT) + config.log_level = :error + + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true + + # Enable first-time tutorial for users signing in the sciNote for + # the first time. + config.x.enable_tutorial = false +end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100644 index 000000000..886ffb976 --- /dev/null +++ b/config/initializers/assets.rb @@ -0,0 +1,47 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '1.0' + +# Add additional assets to the asset load path +# Rails.application.config.assets.paths << Emoji.images_path + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. +Rails.application.config.assets.precompile += %w( underscore.js ) +Rails.application.config.assets.precompile += %w( jsPlumb-2.0.4-min.js ) +Rails.application.config.assets.precompile += %w( jsnetworkx.js ) +Rails.application.config.assets.precompile += %w( handsontable.full.min.js ) +Rails.application.config.assets.precompile += %w( users/settings/preferences.js ) +Rails.application.config.assets.precompile += %w( users/settings/organizations.js ) +Rails.application.config.assets.precompile += %w( users/settings/organizations/add_user_modal.js ) +Rails.application.config.assets.precompile += %w( users/settings/organization.js ) +Rails.application.config.assets.precompile += %w( my_modules/activities.js ) +Rails.application.config.assets.precompile += %w( my_modules/steps.js ) +Rails.application.config.assets.precompile += %w( my_modules/results.js ) +Rails.application.config.assets.precompile += %w( results/result_tables.js ) +Rails.application.config.assets.precompile += %w( results/result_assets.js ) +Rails.application.config.assets.precompile += %w( results/result_texts.js ) +Rails.application.config.assets.precompile += %w( users/registrations/edit.js ) +Rails.application.config.assets.precompile += %w( jquery-ui/draggable.js ) +Rails.application.config.assets.precompile += %w( jquery-ui/droppable.js ) +Rails.application.config.assets.precompile += %w( jquery.ui.touch-punch.min.js ) +Rails.application.config.assets.precompile += %w( bootstrap-colorselector.js ) +Rails.application.config.assets.precompile += %w( eventPause-min.js ) +Rails.application.config.assets.precompile += %w( sidebar.js ) +Rails.application.config.assets.precompile += %w( samples/samples.js ) +Rails.application.config.assets.precompile += %w( samples/sample_datatable.js ) +Rails.application.config.assets.precompile += %w( projects/index.js ) +Rails.application.config.assets.precompile += %w( samples/samples_importer.js ) +Rails.application.config.assets.precompile += %w( projects/canvas.js ) +Rails.application.config.assets.precompile += %w( reports/index.js ) +Rails.application.config.assets.precompile += %w( reports/new_by_module.js ) +Rails.application.config.assets.precompile += %w( datatables.js ) +Rails.application.config.assets.precompile += %w( search/index.js ) +Rails.application.config.assets.precompile += %w( navigation.js ) +Rails.application.config.assets.precompile += %w( datatables.css ) +Rails.application.config.assets.precompile += %w( my_modules.js ) +Rails.application.config.assets.precompile += %w( direct-upload.js ) +Rails.application.config.assets.precompile += %w( canvas-to-blob.min.js ) +Rails.application.config.assets.precompile += %w( Sortable.min.js ) +Rails.application.config.assets.precompile += %w( reports_pdf.css ) diff --git a/config/initializers/aws.rb b/config/initializers/aws.rb new file mode 100644 index 000000000..a93f57858 --- /dev/null +++ b/config/initializers/aws.rb @@ -0,0 +1,8 @@ +if ENV['AWS_ACCESS_KEY_ID'] then + Aws.config.update({ + region: ENV['AWS_REGION'], + credentials: Aws::Credentials.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY']), + }) + + S3_BUCKET = Aws::S3::Resource.new.bucket(ENV['S3_BUCKET']) +end \ No newline at end of file diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb new file mode 100644 index 000000000..59385cdf3 --- /dev/null +++ b/config/initializers/backtrace_silencers.rb @@ -0,0 +1,7 @@ +# Be sure to restart your server when you modify this file. + +# You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. +# Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } + +# You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. +# Rails.backtrace_cleaner.remove_silencers! diff --git a/config/initializers/constants.rb b/config/initializers/constants.rb new file mode 100644 index 000000000..06ffb2294 --- /dev/null +++ b/config/initializers/constants.rb @@ -0,0 +1,50 @@ +# Application version +APP_VERSION = "1.0.0" + +TAG_COLORS = [ + "#6C159E", + "#159B5E", + "#FF4500", + "#008B8B", + "#757575", + "#32CD32", + "#FFD700", + "#48D1CC", + "#15369E", + "#FF69B4", + "#CD5C5C", + "#C9C9C9", + "#6495ED", + "#DC143C", + "#FF8C00", + "#C71585", + "#000000" +] + +SEARCH_LIMIT = 20 + +SHOW_ALL_RESULTS = -1 + +TEXT_EXTRACT_FILE_TYPES = [ + "application/pdf", + "application/rtf", + "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "application/vnd.openxmlformats-officedocument.presentationml.presentation", + "application/vnd.oasis.opendocument.presentation", + "application/vnd.oasis.opendocument.spreadsheet", + "application/vnd.oasis.opendocument.text", + "application/vnd.ms-excel", + "application/vnd.ms-powerpoint", + "application/vnd.ms-word", + "text/plain" +] + +# 1 MB of space is minimal for organizations +MINIMAL_ORGANIZATION_SPACE_TAKEN = 1024*1024 + +# additional space of each file is added to its estimated +# size to account for DB indexes size etc. +ASSET_ESTIMATED_SIZE_FACTOR = 1.1 + +DEFAULT_PRIVATE_ORG_NAME = "My projects" diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb new file mode 100644 index 000000000..7f70458de --- /dev/null +++ b/config/initializers/cookies_serializer.rb @@ -0,0 +1,3 @@ +# Be sure to restart your server when you modify this file. + +Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb new file mode 100644 index 000000000..5f027dbaf --- /dev/null +++ b/config/initializers/devise.rb @@ -0,0 +1,313 @@ +# Use this hook to configure devise mailer, warden hooks and so forth. +# Many of these configuration options can be set straight in your model. +Devise.setup do |config| + # The secret key used by Devise. Devise uses this key to generate + # random tokens. Changing this key will render invalid all existing + # confirmation, reset password and unlock tokens in the database. + # Devise will use the `secret_key_base` on Rails 4+ applications as its `secret_key` + # by default. You can change it below and use your own secret key. + # config.secret_key = '541693b548daa586c6f7d4d4ac38de22e7d46b64ff85492ca05bb176b3b18249d6dbcf74b419a7d680146c6f6173b9c780d77e10580d98b0dc7f8f0c18efae9a' + + # ==> Mailer Configuration + # Configure the e-mail address which will be shown in Devise::Mailer, + # note that it will be overwritten if you use your own mailer class + # with default "from" parameter. + config.mailer_sender = Rails.application.secrets.mailer_user_name + + # Configure the class responsible to send e-mails. + config.mailer = 'Devise::Mailer' + + # ==> ORM configuration + # Load and configure the ORM. Supports :active_record (default) and + # :mongoid (bson_ext recommended) by default. Other ORMs may be + # available as additional gems. + require 'devise/orm/active_record' + + # ==> Configuration for any authentication mechanism + # Configure which keys are used when authenticating a user. The default is + # just :email. You can configure it to use [:username, :subdomain], so for + # authenticating a user, both parameters are required. Remember that those + # parameters are used only when authenticating and not when retrieving from + # session. If you need permissions, you should implement that in a before filter. + # You can also supply a hash where the value is a boolean determining whether + # or not authentication should be aborted when the value is not present. + # config.authentication_keys = [:email] + + # Configure parameters from the request object used for authentication. Each entry + # given should be a request method and it will automatically be passed to the + # find_for_authentication method and considered in your model lookup. For instance, + # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. + # The same considerations mentioned for authentication_keys also apply to request_keys. + # config.request_keys = [] + + # Configure which authentication keys should be case-insensitive. + # These keys will be downcased upon creating or modifying a user and when used + # to authenticate or find a user. Default is :email. + config.case_insensitive_keys = [:email] + + # Configure which authentication keys should have whitespace stripped. + # These keys will have whitespace before and after removed upon creating or + # modifying a user and when used to authenticate or find a user. Default is :email. + config.strip_whitespace_keys = [:email] + + # Tell if authentication through request.params is enabled. True by default. + # It can be set to an array that will enable params authentication only for the + # given strategies, for example, `config.params_authenticatable = [:database]` will + # enable it only for database (email + password) authentication. + # config.params_authenticatable = true + + # Tell if authentication through HTTP Auth is enabled. False by default. + # It can be set to an array that will enable http authentication only for the + # given strategies, for example, `config.http_authenticatable = [:database]` will + # enable it only for database authentication. The supported strategies are: + # :database = Support basic authentication with authentication key + password + # config.http_authenticatable = false + + # If 401 status code should be returned for AJAX requests. True by default. + # config.http_authenticatable_on_xhr = true + + # The realm used in Http Basic Authentication. 'Application' by default. + # config.http_authentication_realm = 'Application' + + # It will change confirmation, password recovery and other workflows + # to behave the same regardless if the e-mail provided was right or wrong. + # Does not affect registerable. + # config.paranoid = true + + # By default Devise will store the user in session. You can skip storage for + # particular strategies by setting this option. + # Notice that if you are skipping storage for all authentication paths, you + # may want to disable generating routes to Devise's sessions controller by + # passing skip: :sessions to `devise_for` in your config/routes.rb + config.skip_session_storage = [:http_auth] + + # By default, Devise cleans up the CSRF token on authentication to + # avoid CSRF token fixation attacks. This means that, when using AJAX + # requests for sign in and sign up, you need to get a new CSRF token + # from the server. You can disable this option at your own risk. + # config.clean_up_csrf_token_on_authentication = true + + # ==> Configuration for :database_authenticatable + # For bcrypt, this is the cost for hashing the password and defaults to 10. If + # using other encryptors, it sets how many times you want the password re-encrypted. + # + # Limiting the stretches to just one in testing will increase the performance of + # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use + # a value less than 10 in other environments. Note that, for bcrypt (the default + # encryptor), the cost increases exponentially with the number of stretches (e.g. + # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). + config.stretches = Rails.env.test? ? 1 : 10 + + # Setup a pepper to generate the encrypted password. + # config.pepper = '92163bf66a1f40744272870543efb00fbad5ba9f6fd24ab4153366e557e275661687e9a47d0281f91b55f048a05aba6a186a86eb62fc1be88e34485b43e762b5' + + # ==> Configuration for :invitable + # The period the generated invitation token is valid, after + # this period, the invited resource won't be able to accept the invitation. + # When invite_for is 0 (the default), the invitation won't expire. + config.invite_for = 3.days + + # Number of invitations users can send. + # - If invitation_limit is nil, there is no limit for invitations, users can + # send unlimited invitations, invitation_limit column is not used. + # - If invitation_limit is 0, users can't send invitations by default. + # - If invitation_limit n > 0, users can send n invitations. + # You can change invitation_limit column for some users so they can send more + # or less invitations, even with global invitation_limit = 0 + # Default: nil + config.invitation_limit = 100 + + # The key to be used to check existing users when sending an invitation + # and the regexp used to test it when validate_on_invite is not set. + # config.invite_key = {:email => /\A[^@]+@[^@]+\z/} + # config.invite_key = {:email => /\A[^@]+@[^@]+\z/, :username => nil} + + # Flag that force a record to be valid before being actually invited + # Default: false + # config.validate_on_invite = true + + # Resend invitation if user with invited status is invited again + # Default: true + # config.resend_invitation = false + + # The class name of the inviting model. If this is nil, + # the #invited_by association is declared to be polymorphic. + # Default: nil + # config.invited_by_class_name = 'User' + + # The foreign key to the inviting model (if invited_by_class_name is set) + # Default: :invited_by_id + # config.invited_by_foreign_key = :invited_by_id + + # The column name used for counter_cache column. If this is nil, + # the #invited_by association is declared without counter_cache. + # Default: nil + # config.invited_by_counter_cache = :invitations_count + + # Auto-login after the user accepts the invite. If this is false, + # the user will need to manually log in after accepting the invite. + # Default: false + config.allow_insecure_sign_in_after_accept = false + + # ==> Configuration for :confirmable + # A period that the user is allowed to access the website even without + # confirming their account. For instance, if set to 2.days, the user will be + # able to access the website for two days without confirming their account, + # access will be blocked just in the third day. Default is 0.days, meaning + # the user cannot access the website without confirming their account. + # config.allow_unconfirmed_access_for = 2.days + + # A period that the user is allowed to confirm their account before their + # token becomes invalid. For example, if set to 3.days, the user can confirm + # their account within 3 days after the mail was sent, but on the fourth day + # their account can't be confirmed with the token any more. + # Default is nil, meaning there is no restriction on how long a user can take + # before confirming their account. + # config.confirm_within = 3.days + + # If true, requires any email changes to be confirmed (exactly the same way as + # initial account confirmation) to be applied. Requires additional unconfirmed_email + # db field (see migrations). Until confirmed, new email is stored in + # unconfirmed_email column, and copied to email column on successful confirmation. + config.reconfirmable = true + + # Defines which key will be used when confirming an account + # config.confirmation_keys = [:email] + + # ==> Configuration for :rememberable + # The time the user will be remembered without asking for credentials again. + # config.remember_for = 2.weeks + + # Invalidates all the remember me tokens when the user signs out. + config.expire_all_remember_me_on_sign_out = true + + # If true, extends the user's remember period when remembered via cookie. + # config.extend_remember_period = false + + # Options to be passed to the created cookie. For instance, you can set + # secure: true in order to force SSL only cookies. + # config.rememberable_options = {} + + # ==> Configuration for :validatable + # Range for password length. + config.password_length = 8..72 + + # Email regex used to validate email formats. It simply asserts that + # one (and only one) @ exists in the given string. This is mainly + # to give user feedback and not to assert the e-mail validity. + # config.email_regexp = /\A[^@]+@[^@]+\z/ + + # ==> Configuration for :timeoutable + # The time you want to timeout the user session without activity. After this + # time the user will be asked for credentials again. Default is 30 minutes. + # config.timeout_in = 30.minutes + + # If true, expires auth token on session timeout. + # config.expire_auth_token_on_timeout = false + + # ==> Configuration for :lockable + # Defines which strategy will be used to lock an account. + # :failed_attempts = Locks an account after a number of failed attempts to sign in. + # :none = No lock strategy. You should handle locking by yourself. + # config.lock_strategy = :failed_attempts + + # Defines which key will be used when locking and unlocking an account + # config.unlock_keys = [:email] + + # Defines which strategy will be used to unlock an account. + # :email = Sends an unlock link to the user email + # :time = Re-enables login after a certain amount of time (see :unlock_in below) + # :both = Enables both strategies + # :none = No unlock strategy. You should handle unlocking by yourself. + # config.unlock_strategy = :both + + # Number of authentication tries before locking an account if lock_strategy + # is failed attempts. + # config.maximum_attempts = 20 + + # Time interval to unlock the account if :time is enabled as unlock_strategy. + # config.unlock_in = 1.hour + + # Warn on the last attempt before the account is locked. + # config.last_attempt_warning = true + + # ==> Configuration for :recoverable + # + # Defines which key will be used when recovering the password for an account + # config.reset_password_keys = [:email] + + # Time interval you can reset your password with a reset password key. + # Don't put a too small interval or your users won't have the time to + # change their passwords. + config.reset_password_within = 6.hours + + # When set to false, does not sign a user in automatically after their password is + # reset. Defaults to true, so a user is signed in automatically after a reset. + # config.sign_in_after_reset_password = true + + # ==> Configuration for :encryptable + # Allow you to use another encryption algorithm besides bcrypt (default). You can use + # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1, + # :authlogic_sha512 (then you should set stretches above to 20 for default behavior) + # and :restful_authentication_sha1 (then you should set stretches to 10, and copy + # REST_AUTH_SITE_KEY to pepper). + # + # Require the `devise-encryptable` gem when using anything other than bcrypt + # config.encryptor = :sha512 + + # ==> Scopes configuration + # Turn scoped views on. Before rendering "sessions/new", it will first check for + # "users/sessions/new". It's turned off by default because it's slower if you + # are using only default views. + # config.scoped_views = false + + # Configure the default scope given to Warden. By default it's the first + # devise role declared in your routes (usually :user). + # config.default_scope = :user + + # Set this configuration to false if you want /users/sign_out to sign out + # only the current scope. By default, Devise signs out all scopes. + # config.sign_out_all_scopes = true + + # ==> Navigation configuration + # Lists the formats that should be treated as navigational. Formats like + # :html, should redirect to the sign in page when the user does not have + # access, but formats like :xml or :json, should return 401. + # + # If you have any extra navigational formats, like :iphone or :mobile, you + # should add them to the navigational formats lists. + # + # The "*/*" below is required to match Internet Explorer requests. + # config.navigational_formats = ['*/*', :html] + + # The default HTTP method used to sign out a resource. Default is :delete. + config.sign_out_via = :delete + + # ==> OmniAuth + # Add a new OmniAuth provider. Check the wiki for more information on setting + # up on your models and hooks. + # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' + + # ==> Warden configuration + # If you want to use other strategies, that are not supported by Devise, or + # change the failure app, you can configure them inside the config.warden block. + # + # config.warden do |manager| + # manager.intercept_401 = false + # manager.default_strategies(scope: :user).unshift :some_external_strategy + # end + + # ==> Mountable engine configurations + # When using Devise inside an engine, let's call it `MyEngine`, and this engine + # is mountable, there are some extra configurations to be taken into account. + # The following options are available, assuming the engine is mounted as: + # + # mount MyEngine, at: '/my_engine' + # + # The router that invoked `devise_for`, in the example above, would be: + # config.router_name = :my_engine + # + # When using OmniAuth, Devise cannot automatically set OmniAuth path, + # so you need to do it manually. For the users scope, it would be: + # config.omniauth_path_prefix = '/my_engine/users/auth' +end diff --git a/config/initializers/devise_async.rb b/config/initializers/devise_async.rb new file mode 100644 index 000000000..9cd6ee8ad --- /dev/null +++ b/config/initializers/devise_async.rb @@ -0,0 +1,4 @@ +Devise::Async.enabled = true +Devise::Async.backend = :delayed_job +Devise::Async.queue = :devise_email +Devise::Async.priority = 10 diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 000000000..4a994e1e7 --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [:password] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb new file mode 100644 index 000000000..ac033bf9d --- /dev/null +++ b/config/initializers/inflections.rb @@ -0,0 +1,16 @@ +# Be sure to restart your server when you modify this file. + +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.plural /^(ox)$/i, '\1en' +# inflect.singular /^(ox)en/i, '\1' +# inflect.irregular 'person', 'people' +# inflect.uncountable %w( fish sheep ) +# end + +# These inflection rules are supported but not enabled by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| +# inflect.acronym 'RESTful' +# end diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb new file mode 100644 index 000000000..dc1899682 --- /dev/null +++ b/config/initializers/mime_types.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Add new mime types for use in respond_to blocks: +# Mime::Type.register "text/richtext", :rtf diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb new file mode 100644 index 000000000..a3b20037d --- /dev/null +++ b/config/initializers/paperclip.rb @@ -0,0 +1,53 @@ + +if ENV['PAPERCLIP_HASH_SECRET'].nil? + puts "WARNING! Environment variable PAPERCLIP_HASH_SECRET must be set." + exit 1 +end + +Paperclip::Attachment.default_options.merge!({ + hash_data: ':class/:attachment/:id/:style', + hash_secret: ENV['PAPERCLIP_HASH_SECRET'], + preserve_files: false, + url: '/system/:class/:attachment/:id_partition/:hash/:style/:filename' +}) + +if ENV['PAPERCLIP_STORAGE'] == "s3" + + if ENV['S3_BUCKET'].nil? or ENV['AWS_REGION'].nil? or + ENV['AWS_ACCESS_KEY_ID'].nil? or ENV['AWS_SECRET_ACCESS_KEY'].nil? + puts "WARNING! Environment variables S3_BUCKET, AWS_REGION, " + + "AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY must be set." + exit 1 + end + Paperclip::Attachment.default_options.merge!({ + url: ':s3_domain_url', + path: '/:class/:attachment/:id_partition/:hash/:style/:filename', + storage: :s3, + s3_host_name: "s3.#{ENV['AWS_REGION']}.amazonaws.com", + s3_protocol: 'https', + s3_credentials: { + bucket: ENV['S3_BUCKET'], + access_key_id: ENV['AWS_ACCESS_KEY_ID'], + secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'] + }, + s3_permissions: { + original: :private + }, + s3_storage_class: { + medium: :reduced_redundancy, + thumb: :reduced_redundancy, + icon: :reduced_redundancy, + icon_small: :reduced_redundancy + } + }) +end + +Paperclip::Attachment.class_eval do + def is_stored_on_s3? + options[:storage].to_sym == :s3 + end + + def fetch + Paperclip.io_adapters.for self + end +end diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb new file mode 100644 index 000000000..b64e189d7 --- /dev/null +++ b/config/initializers/session_store.rb @@ -0,0 +1,3 @@ +# Be sure to restart your server when you modify this file. + +Rails.application.config.session_store :cookie_store, key: '_scinote_session' diff --git a/config/initializers/wicked_pdf.rb b/config/initializers/wicked_pdf.rb new file mode 100644 index 000000000..1d5abe397 --- /dev/null +++ b/config/initializers/wicked_pdf.rb @@ -0,0 +1,25 @@ +WickedPdf.config = { + #:wkhtmltopdf => '/usr/local/bin/wkhtmltopdf', + #:layout => "pdf.html", + #:exe_path => '/usr/local/bin/wkhtmltopdf' +} + + +# WickedPdfHelper patch that fixes issue with including application.css +# in environments like Heroku where assets.compile option is disabled and +# it is not acceptable to enable it. +if Rails.env.production? and Rails.configuration.assets.compile == false + + WickedPdfHelper::Assets.module_eval do + + def read_asset(source) + manifest = Rails.application.assets_manifest + path = File.join(manifest.dir, manifest.assets[source]) + File.read(path) + end + + def asset_exists?(source) + Rails.application.assets_manifest.assets.key?(source) + end + end +end diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb new file mode 100644 index 000000000..33725e95f --- /dev/null +++ b/config/initializers/wrap_parameters.rb @@ -0,0 +1,14 @@ +# Be sure to restart your server when you modify this file. + +# This file contains settings for ActionController::ParamsWrapper which +# is enabled by default. + +# Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. +ActiveSupport.on_load(:action_controller) do + wrap_parameters format: [:json] if respond_to?(:wrap_parameters) +end + +# To enable root element in JSON for ActiveRecord objects. +# ActiveSupport.on_load(:active_record) do +# self.include_root_in_json = true +# end diff --git a/config/locales/devise.en.yml b/config/locales/devise.en.yml new file mode 100644 index 000000000..838c7195f --- /dev/null +++ b/config/locales/devise.en.yml @@ -0,0 +1,60 @@ +# Additional translations at https://github.com/plataformatec/devise/wiki/I18n + +en: + devise: + confirmations: + confirmed: "Your email address has been successfully confirmed." + send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." + failure: + already_authenticated: "You are already logged in." + inactive: "Your account is not activated yet." + invalid: "Invalid %{authentication_keys} or password." + locked: "Your account is locked." + last_attempt: "You have one more attempt before your account is locked." + not_found_in_database: "Invalid %{authentication_keys} or password." + timeout: "Your session expired. Please log in again to continue." + unauthenticated: "You need to log in or sign up before continuing." + unconfirmed: "You have to confirm your email address before continuing." + mailer: + confirmation_instructions: + subject: "Confirmation instructions" + reset_password_instructions: + subject: "Reset password instructions" + unlock_instructions: + subject: "Unlock instructions" + omniauth_callbacks: + failure: "Could not authenticate you from %{kind} because \"%{reason}\"." + success: "Successfully authenticated from %{kind} account." + passwords: + no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." + send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." + send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." + updated: "Your password has been changed successfully. You are now logged in." + updated_not_active: "Your password has been changed successfully." + registrations: + destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." + signed_up: "Welcome! You have signed up successfully." + signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." + signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." + signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." + update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." + updated: "Your account has been updated successfully." + sessions: + signed_in: "Logged in successfully." + signed_out: "Logged out successfully." + already_signed_out: "Logged out successfully." + unlocks: + send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." + send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." + unlocked: "Your account has been unlocked successfully. Please log in to continue." + errors: + messages: + already_confirmed: "was already confirmed, please try logging in" + confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" + expired: "has expired, please request a new one" + not_found: "not found" + not_locked: "was not locked" + not_saved: + one: "1 error prohibited this %{resource} from being saved:" + other: "%{count} errors prohibited this %{resource} from being saved:" diff --git a/config/locales/devise_invitable.en.yml b/config/locales/devise_invitable.en.yml new file mode 100644 index 000000000..87bb46ec0 --- /dev/null +++ b/config/locales/devise_invitable.en.yml @@ -0,0 +1,31 @@ +en: + devise: + failure: + invited: "You have a pending invitation, accept it to finish creating your account." + invitations: + send_instructions: "An invitation email has been sent to %{email}." + invitation_token_invalid: "The invitation token provided is not valid!" + updated: "Your password was set successfully. You are now signed in." + updated_not_active: "Your password was set successfully." + no_invitations_remaining: "No invitations remaining" + invitation_removed: "Your invitation was removed." + new: + header: "Send invitation" + submit_button: "Send an invitation" + edit: + header: "Set your password" + submit_button: "Set my password" + mailer: + invitation_instructions: + subject: "Invitation instructions" + hello: "Hello %{email}" + someone_invited_you: "Someone has invited you to %{url}, you can accept it through the link below." + accept: "Accept invitation" + accept_until: "This invitation will be due in %{due_date}." + ignore: "If you don't want to accept the invitation, please ignore this email.
    Your account won't be created until you access the link above and set your password." + time: + formats: + devise: + mailer: + invitation_instructions: + accept_until_format: "%B %d, %Y %I:%M %p" diff --git a/config/locales/en.yml b/config/locales/en.yml new file mode 100644 index 000000000..b9c42525f --- /dev/null +++ b/config/locales/en.yml @@ -0,0 +1,1161 @@ +en: + + devise: + confirmations: + new: + head_title: "Resend confirmation instructions" + title: "Resend confirmation instructions" + submit: "Resend confirmation instructions" + passwords: + edit: + head_title: "Change password" + title: "Change your password" + password: "New password" + password_length: "(%{min_length} characters minimum)" + password_confirm: "Confirm new password" + submit: "Change my password" + new: + head_title: "Forgot password" + title: "Forgot your password?" + submit: "Send me reset password instructions" + registrations: + password_changed: "Password successfully updated." + sessions: + new: + head_title: "Log in" + title: "Log in" + email_placeholder: "username@email.com" + password_placeholder: "pass****" + submit: "Log in" + create: + org_name: "%{user}'s projects" + unlocks: + new: + head_title: "Resend unlock instructions" + title: "Resend unlock instructions" + submit: "Resend unlock instructions" + links: + login: "Log in" + signup: "Sign up" + forgot: "Forgot your password?" + not_receive_confirmation: "Didn't receive confirmation instructions?" + not_receive_unlock: "Didn't receive unlock instructions?" + sign_in_provider: "Sign in with %{provider}" + + helpers: + label: + organization: + name: "Organization name" + user: + full_name: "Full name" + initials: "Initials" + avatar: "Avatar" + + head: + title: "sciNote | %{title}" + + nav: + search: "Search" + user_greeting: "Hi, %{full_name}" + advanced_search: "Advanced search" + title: "sciNote" + user: + profile: "My profile" + settings: "Settings" + logout: "Log out" + activities: + none: "No activities!" + label: + projects: "Projects" + calendar: "Calendar" + activities: "Activities" + + sidebar: + title: "Navigation" + no_module_group: "No workflow" + + nav2: + projects: + canvas: "Overview" + samples: "Samples" + activities: "Activity" + reports: "Reports" + modules: + steps: "Protocols" + results: "Results" + activities: "Activity" + samples: "Samples" + archive: "Archive" + + search: + index: + head_title: "Search" + page_title: "Search" + results_title_html: "Search Results for '%{query}'" + archived: "Archived" + created_by: "Created by: " + created_at: "Created at: " + last_modified_by: "Last modified by: " + last_modified_at: "Last modified at: " + description: "Description: " + no_description: "No description" + organization: "Organization: " + project: "Project: " + modules: "Modules: " + tag_no_modules: "not added to any modules" + sample_no_modules: "not assigned to any modules" + module: "Module: " + step: "Step: " + result: "Result: " + file: "File: %{filename}" + error: + no_results: "No results for %{q}." + query_length: "Search query is too short. The query should contain at least %{n} characters." + max_length: "Search query is too long. The query is limited to %{n} characters." + samples: + sample: "Sample: " + sample_type: "Sample type: " + no_sample_type: "No sample type" + sample_group: "Sample group: " + no_sample_group: "No sample group" + added_on: "Added on: " + added_by: "Added by: " + custom_field: "%{cf}: " + comments: + project: "Project comment" + my_module: "Module comment" + step: "Step comment" + result: "Result comment" + + projects: + index: + head_title: "Home" + new: "New project" + visibility_private: "Project is private. Only invited users can see it." + visibility_public: "Project is public. Everybody from the organization can see it." + user_project: "%{user} joined on %{timestamp}." + no_activities: "No activities!" + no_comments: "No comments!" + no_users: "No users!" + manage_users: "Manage users" + no_notifications: "No notifications!" + module_overdue_html: "Module %{module} is overdue (%{days})." + module_overdue_days: + one: "1 day" + other: "%{count} days" + module_one_day_due_html: "Module %{module} is due in less than 1 day." + new_comment: "New comment" + user_role: "Role: " + user_full_name: "User: " + edit_user: "Edit role" + delete_user: "Remove user from project" + delete_user_confirm: "Are you sure you wish to remove user %{user} from project %{project}?" + organization_filter: "Show projects from" + all_filter: "All" + sort: "Sort by" + sort_new: "Newest first" + sort_old: "Oldest first" + sort_atoz: "From A to Z" + sort_ztoa: "From Z to A" + start_date: "Start date" + activity_tab: "Activity" + users_tab: "Users" + notifications_tab: "Notifications" + comment_tab: "Comments" + new_comment: "New comment" + content_loading: "Loading..." + edit_option: "Edit" + archive_option: "Archive" + archive_confirm: "Are you sure to archive project?" + restore_option: "Restore" + options_header: "Options" + comment_placeholder: "Your Message" + more_comments: "More Comments" + no_archived_projects: "No archived projects!" + back_to_projects_index: "Back to home" + modal_new_project: + modal_title: "Create new project" + create: "Create project" + name: "Project name" + name_placeholder: "My project" + organization: "Organization" + visibility: "Visibility" + visibility_hidden: "Private" + visibility_visible: "Public" + modal_edit_project: + modal_title: "Edit project %{project}" + submit: "Update project" + modal_manage_users: + modal_title: "Manage users for" + no_users: "No users!" + create: "Add" + select_user_role: "Select Role" + no_orgs: + title: "It's empty here!" + text: "It seems you're not a member of any organization. See organization management to sort it out." + btn: "Manage organizations" + create: + success_flash: "Project %{name} successfully created." + update: + success_flash: "Project %{name} successfully updated." + error_flash: "Project %{name} not updated." + archive: + head_title: "Projects Archive" + success_flash: "Project %{name} successfully archived." + error_flash: "Project %{name} not archived." + restore: + success_flash: "Project %{name} successfully restored." + error_flash: "Project %{name} not restored." + show: + head_title: "%{project}" + canvas: + head_title: "%{project} | Overview" + canvas_edit: "Edit workflow" + modal_manage_tags: + head_title: "Manage tags for" + subtitle: "Showing tags of module %{module}" + no_tags: "No tags!" + edit_tag: "Edit tag." + remove_tag: "Remove tag from module %{module}." + delete_tag: "Permanently delete tag from all modules." + save_tag: "Save tag." + cancel_tag: "Cancel changes to the tag." + create: "Add tag" + create_new: "Create new tag" + edit: + new_module: "New module" + new_module_hover: "Drag me onto canvas" + save: "Save workflow" + save_short: "Save" + cancel: "Cancel" + unsaved_work: "Are you sure you want to leave this page? All unsaved data will be lost." + drag_connections: "Drag connection/s from here" + options_header: "Options" + edit_module: "Rename module" + edit_module_group: "Rename workflow" + clone_module: "Clone module" + clone_module_group: "Clone workflow" + delete_module: "Archive module" + delete_module_group: "Archive workflow" + modal_new_module: + title: "Add new module" + name: "Module name" + name_placeholder: "My module" + confirm: "Add module" + error_length: "Name must contain from 2 to 50 characters." + error_invalid: "Name contains invalid characters ('|')." + error_whitespaces: "Name cannot be blank." + modal_edit_module: + title: "Rename module" + name: "Module name" + confirm: "Rename module" + modal_edit_module_group: + title: "Rename workflow" + name: "Workflow name" + confirm: "Rename workflow" + modal_delete_module: + title: "Archive module" + confirm: "Archive module" + message: "Are you sure you wish to archive module %{module}? Module's samples and position will be removed." + modal_delete_module_group: + title: "Archive workflow" + confirm: "Archive workflow" + message: "Are you sure you wish to archive the workflow module %{module} belongs to? All workflow modules' samples and positions will be removed." + popups: + info_tab: "Module info" + no_description: "This module has no description." + full_info: "Edit description" + users_tab: "Users" + no_users: "This module has no assigned users." + manage_users: "Manage users" + module_user_join: "Joined on %{date}." + module_user_join_full: "%{user} joined on %{date} at %{time}." + activities_tab: "Activity" + no_activities: "No activities!" + more_activities: "All activities" + comments_tab: "Comments" + no_comments: "No comments!" + more_comments: "More Comments" + comment_placeholder: "Your Message" + new_comment: "New comment" + samples_tab: "Samples" + no_samples: "No samples!" + manage_samples: "Manage samples" + full_zoom: + due_date: "Due date" + no_due_date: "not set" + modal_manage_users: + modal_title: "Manage users for" + no_users: "No users" + user_join: "Joined on %{date}." + user_join_full: "%{user} joined on %{date} at %{time}." + create: "Add" + update: + success_flash: "Project successfully updated." + samples: + head_title: "%{project} | Samples" + activities: + head_title: "%{project} | Activity" + reports: + print_title: "%{project} | Report" + index: + head_title: "%{project} | Reports" + new: "New report" + edit: "Edit report" + delete: "Delete report/s" + thead_name: "Report name" + thead_created_by: "Created by" + thead_last_modified_by: "Last modified by" + thead_grouped_by: "Grouped by" + thead_created_at: "Created at" + thead_updated_at: "Last updated at" + no_reports: "No reports!" + table_grouped_by_module: "By modules" + table_grouped_by_timestamp: "By timestamp" + modal_new: + head_title: "Create new report" + grouped_by: "How would you like your report structured?" + grouped_by_module: "Group elements by module" + grouped_by_timestamp: "Sort elements by timestamp" + create: "Create" + modal_delete: + head_title: "Delete report/s" + message: "Are you sure to delete selected report/s?" + delete: "Delete" + new_by_module: + new: + head_title: "%{project} | New report" + nav_title: "Report for: " + nav_print: "Print" + nav_pdf: "Export to PDF" + nav_save: "Save report" + nav_close: "Close" + nav_sort_by: "Sort report by" + nav_sort_asc: "Oldest on top" + nav_sort_desc: "Newest on top" + nav_sort: "Sort" + sidebar_title: "Navigation" + global_sort: "Sorting whole report will undo any custom sorting you might have done. Proceed?" + unsaved_work: "Are you sure you want to leave this page? All unsaved data will be lost." + elements: + modals: + project_contents: + head_title: "Add contents to report" + project_tab: "Choose modules" + modules_tab: "Modules content" + steps_tab: "Protocols content" + results_tab: "Results content" + project_contents_inner: + instructions: "Select projects/workflows/modules to include in the report" + no_modules: "The project contains no modules" + no_module_group: "No workflow" + module_contents: + head_title: "Add contents to module %{module}" + module_tab: "Module content" + steps_tab: "Protocols content" + results_tab: "Results content" + module_contents_inner: + instructions: "Choose what information from module/s to include in the report" + header: "Elements" + steps: "Completed protocol steps" + no_steps: "Module contains no protocols" + results: "Results" + result_assets: "Files" + result_tables: "Tables" + result_texts: "Texts" + no_results: "Module contains no results" + activity: "Activity" + samples: "Samples" + step_contents: + head_title: "Add contents to step %{step}" + step_tab: "Step content" + results_tab: "Results content" + step_contents_inner: + instructions: "Choose what information from module protocol step/s to include in the report" + header: "Elements" + tables: "Tables" + no_tables: "Step contains no tables" + assets: "Files" + no_assets: "Step contains no uploaded files" + checklists: "Checklists" + no_checklists: "Step contains no checklists" + comments: "Comments" + result_contents: + head_title: "Add contents to result %{result}" + result_contents_inner: + instructions: "Include result/s comments in the report?" + comments: "Comments" + add: "Add to report" + save_report: + head_title: "Save report" + name: "Report name" + name_placeholder: "My report" + description: "Report description" + description_placeholder: "My report description..." + save: "Save" + all: + sort_asc: "Sort element contents by oldest on top" + sort_desc: "Sort element contents by newest on top" + move_up: "Move element up" + move_down: "Move element down" + remove: "Remove element from the report" + new_element: + title: "Add new element/s here" + project_header: + user_time: "Project created on %{timestamp}." + title: "Report for project %{project}" + module: + user_time: "Module created on %{timestamp}." + due_date: "Due date: %{due_date}" + no_due_date: "No due date" + no_description: "No description" + tags_header: "Module tags:" + no_tags: "No tags" + module_activity: + name: "Activity of module %{my_module}" + sidebar_name: "Activity" + no_activity: "No activities" + module_samples: + name: "Samples of module %{my_module}" + sidebar_name: "Samples" + no_samples: "No samples" + result_asset: + file_name: "[ %{file} ]" + user_time: "Uploaded by %{user} on %{timestamp}." + result_table: + user_time: "Created by %{user} on %{timestamp}." + result_text: + user_time: "Created by %{user} on %{timestamp}." + step: + sidebar_name: "Step %{pos}: %{name}" + user_time: "Completed by %{user} on %{timestamp}." + step_pos: "Step %{pos}:" + no_description: "No description" + step_table: + sidebar_name: "Table" # TODO + user_time: "Table created on %{timestamp}." + step_asset: + sidebar_name: "File %{file}" + file_name: "[ %{file} ]" + user_time: "File uploaded on %{timestamp}." + step_checklist: + checklist_name: "[ %{name} ]" + user_time: "Checklist created on %{timestamp}." + result_comments: + sidebar_name: "Comments" + name: "Comments for result %{result}" + no_comments: "No comments" + comment_prefix: "%{user} on %{date} at %{time}:" + step_comments: + sidebar_name: "Comments" + name: "Comments for step %{step}" + no_comments: "No comments" + comment_prefix: "%{user} on %{date} at %{time}:" + module_archive: + head_title: "%{project} | Archived modules" + no_archived_modules: "No archived modules!" + restore_option: "Restore" + archived_on: "Archived on" + archived_on_title: "Module archived on %{date} at %{time}." + + user_organizations: + enums: + role: + guest: "Guest" + normal_user: "Normal user" + admin: "Administrator" + + user_projects: + enums: + role: + owner: "Owner" + normal_user: "User" + technician: "Technician" + viewer: "Viewer" + new: + head_title: "%{project} | Add user" + title: "Add user to project %{project}" + create: "Add" + role: "Role" + create: + success_flash: "User %{user} successfully added to project %{project}." + error_flash: "User %{user} not added to project %{project}." + select_user_role: "Please select a user role." + add_user_generic_error: "An error occured. " + edit: + head_title: "%{project} | Edit user role" + title: "Edit role for user %{user} on project %{project}" + update: "Update role" + update_role: "Change Role" + update: + success_flash: "Successfully changed project role for user %{user} on project %{project}." + error_flash: "Project role not changed for user %{user} on project %{project}." + destroy: + success_flash: "Successfully removed user %{user} from project %{project}." + error_flash: "User %{user} not removed from project %{project}." + + my_modules: + show: + head_title: "%{project} | %{module}" + archive_action: "Archive" + archive_confirm_text: "All samples will be unassigned from the + module when archiving. Are you sure to archive module?" + edit: + head_title: "%{project} | %{module} | Edit" + title: "Edit module %{module}" + name: "Module name" + due_date: "Due date" + save: "Save module" + description: + title: "Edit module %{module} description" + label: "Description" + due_date: + title: "Edit module %{module} due date" + label: "Due date" + update: + success_flash: "Successfully modified module %{module}." + destroy: + success_flash: "Successfully removed module %{module} from project %{project}." + samples: + head_title: "%{project} | %{module} | Samples" + module_archive: + head_title: "%{project} | %{module} | Archive" + archived_on: "Archived on" + archived_on_title: "Result archived on %{date} at %{time}." + option_download: "Download" + no_archived_results: "No archived results!" + archive_timelabel: "archived on %{date}" + module_header: + start_date: "Start date:" + due_date: "Due date:" + tags: "Tags:" + no_tags: "No tags" + no_description: "No description" + steps: + head_title: "%{project} | %{module} | Protocols" + subtitle: "Protocol steps" + add_due_date: "Add a due date" + go_to_results: "Go to results view" + expand_label: "Expand all" + collapse_label: "Collapse all" + new_step: "Add new" + published_on: "Published on %{timestamp} by %{user}" + empty_step_name: "Add title" + empty_checklist: "No items" + save_step: "Save step" + tables: "Tables" + Files: "Files" + info_tab: "Info" + comments_tab: "Comments" + comment_title: "%{user} at %{time}:" + options_label: "Options" + new: + head_title: "%{project} | %{module} | Add new step" + title: "Add new step to module %{module}" + add_step_title: "Add new step" + tab_checklists: "Checklists" + tab_assets: "Files" + tab_tables: "Tables" + add_step: "Add" + name: "Step name" + name_placeholder: "mRNA sequencing" + description: "Description" + description_placeholder: "Write what should be done here ..." + checklist_panel_title: "Checklist" + checklist_name: "Checklist name" + checklist_name_placeholder: "Checklist name" + checklist_items: "Items" + checklist_item_placeholder: "Task" + checklist_add_item: "Add item" + add_checklist: "Add checklist" + table_panel_title: "Table" + table_placeholder: "Table contents" + add_table: "Add table" + asset_panel_title: "File" + add_asset: "Add file" + create: + success_flash: "Step successfully added to module %{module}." + edit: + head_title: "%{project} | %{module} | Edit step" + title: "Edit step %{step} from module %{module}" + edit_step_title: "Edit step" + edit_step: "Save" + update: + success_flash: "Step %{step} successfully updated." + destroy: + confirm: "Are you sure you want to delete step %{step}?" + success_flash: "Step %{step} successfully deleted." + options: + up_arrow_title: "Move step up" + down_arrow_title: "Move step down" + comment_title: "Comments" + no_comments: "No comments" + new_comments: "New comment" + edit_title: "Edit step" + delete_title: "Delete step" + duplicate_title: "Duplicate step" + complete_title: "Complete step" + uncomplete_title: "Uncomplete step" + results: + head_title: "%{project} | %{module} | Results" + add_label: "Add new result:" + new_text_result: "Text" + new_table_result: "Table" + new_asset_result: "File" + published_on: "Published on %{timestamp} by %{user}" + published_table: "entered a table on %{timestamp}." + published_text: "entered a text on %{timestamp}." + published_asset: "uploaded a file on %{timestamp}." + expand_label: "Expand all" + collapse_label: "Collapse all" + empty_name: "Add title" + archive_confirm: "Are you sure to archive result?" + info_tab: "Info" + comments_tab: "Comments" + comment_title: "%{user} at %{time}:" + options_label: "Options" + options: + comment_title: "Comments" + no_comments: "No comments" + new_comment: "New comment" + edit_title: "Edit result" + archive_title: "Archive result" + activities: + head_title: "%{project} | %{module} | Activity" + no_activities: "There are no activities for this module." + more_activities: "Load older activities" + + my_module_tags: + new: + head_title: "%{project} | %{module} | Add tag" + title: "Add tag to module %{module}" + create: "Add tag" + new_tag: "Create new tag" + create: + success_flash: "Successfully added tag %{tag} to module %{module}." + error_flash: "Could not create new tag for module %{module}." + destroy: + success_flash: "Successfully removed tag %{tag} from module %{module}." + error_flash: "Tag %{tag} could not be removed from %{module}." + + my_module_comments: + new: + head_title: "%{project} | %{module} | Add comment" + title: "Add comment to module %{module}" + create: "Add comment" + create: + success_flash: "Successfully added comment to module %{module}." + + my_module_groups: + new: + name: "Workflow %{index}" + suffix: "..." + + tags: + new: + head_title: "Create tag" + title: "Create a new tag" + name: "Tag name" + name_placeholder: "My tag" + color: "Tag color" + create: "Create tag" + create: + new_name: "New tag" + success_flash: "Successfully created tag %{tag}." + error_flash: "Could not create a new tag." + destroy: + success_flash: "Successfully removed tag %{tag}." + error_flash: "Could not remove tag %{tag}." + + project_comments: + new: + head_title: "%{project} | Add comment" + title: "Add comment to project %{project}" + create: "Add comment" + create: + success_flash: "Successfully added comment to project %{project}." + + result_texts: + new: + head_title: "%{project} | %{module} | Add text result" + title: "Add result to module %{module}" + create: "Add text result" + edit: + head_title: "%{project} | %{module} | Edit text result" + title: "Edit result from module %{module}" + update: "Update text result" + create: + success_flash: "Successfully added text result to module %{module}" + update: + success_flash: "Successfully updated text result in module %{module}" + archive: + success_flash: "Successfully archived text result in module %{module}" + destroy: + success_flash: "Text result successfully deleted." + + result_assets: + new: + head_title: "%{project} | %{module} | Add file result" + title: "Add result to module %{module}" + create: "Add file result" + edit: + head_title: "%{project} | %{module} | Edit file result" + title: "Edit result from module %{module}" + update: "Update file result" + create: + success_flash: "Successfully added file result to module %{module}" + update: + success_flash: "Successfully updated file result in module %{module}" + archive: + success_flash: "Successfully archived file result in module %{module}" + destroy: + success_flash: "File result successfully deleted." + + result_tables: + new: + head_title: "%{project} | %{module} | Add table result" + title: "Add result to module %{module}" + create: "Add table result" + edit: + head_title: "%{project} | %{module} | Edit table result" + title: "Edit result from module %{module}" + update: "Update table result" + create: + success_flash: "Successfully added table result to module %{module}" + update: + success_flash: "Successfully updated table result in module %{module}" + archive: + success_flash: "Successfully archived table result in module %{module}" + destroy: + success_flash: "Table result successfully deleted." + + samples: + actions: "Actions" + add_new_sample: "Add new sample" + import: "Import" + export: "Export" + assign_samples_to_module: "Assign" + unassign_samples_to_module: "Unassign" + delete_samples: "Delete" + edit_sample: "Edit" + save_sample: "Save" + cancel_save: "Cancel" + column_visibility: "Visible columns" + add_new_sample_type: "Add sample type" + add_new_sample_group: "Add sample group" + add_new_column: "Add column" + modal_import: + title: "Import samples" + notice: "You may upload .csv file (comma separated) or tab separated file (.txt or .tdv) or Excel file (.xls, .xlsx). First row should include header names, followed by rows with sample data." + upload: "Upload file" + modal_delete: + title: "Delete samples" + notice: "Are you sure you want to delete the selected samples?" + other_samples: "Only samples created by you will be deleted." + delete: "Delete samples" + modal_add_custom_field: + title_html: "Add new column to organization %{organization}" + create: "Add new column" + modal_add_new_sample_group: + title_html: "Add new sample group to organization %{organization}" + create: "Add new sample group" + modal_add_new_sample_type: + title_html: "Add new sample type to organization %{organization}" + create: "Add new sample type" + table: + assigned: "Assigned" + sample_name: "Sample name" + sample_type: "Sample type" + sample_group: "Sample group" + added_on: "Added on" + added_by: "Added by" + no_group: "No sample group" + no_type: "No sample type" + new: + head_title: "%{organization} | Add new sample" + title: "Add new sample to organization %{organization}" + create: "Add new sample" + sample_type: "Sample type" + sample_group: "Sample group" + no_group: "No sample group" + no_type: "No sample type" + edit_sample_type: "Edit sample type" + edit_sample_group: "Edit sample group" + edit: + head_title: "%{organization} | Edit sample" + title: "Edit sample %{sample} from organization %{organization}" + create: "Edit sample" + scf_does_not_exist: "This field does not exists." + create: + success_flash: "Successfully added sample to organization %{organization}" + update: + success_flash: "Successfully updated sample %{sample} to organization %{organization}" + destroy: + success_flash: "%{sample_number} samples successfully deleted." + contains_other_samples_flash: "%{sample_number} samples successfully deleted. %{other_samples_number} of the selected samples were created by other users and were not deleted." + no_sample_selected_flash: "There were no selected samples." + no_deleted_samples_flash: "No samples were deleted. %{other_samples_number} of the selected samples were created by other users and were not deleted." + js: + permission_error: "You don't have permission to edit this sample." + not_found_error: "This sample does not exist." + sample_types: + edit: + head_title: "%{organization} | Edit sample type" + title: "Edit sample type %{sample_type} from organization %{organization}" + create: "Edit sample type" + create: + success_flash: "Successfully added sample type %{sample_type} to organization %{organization}." + update: + success_flash: "Successfully updated sample type %{sample_type} to organization %{organization}." + destroy: + success_flash: "Sample type %{sample_type} successfully deleted." + + custom_fields: + new: + title_html: "Add new column to organization %{organization}" + create: "Add new column" + create: + success_flash: "Successfully added column %{custom_field} to organization %{organization}." + + organizations: + parse_sheet: + head_title: "%{organization} | Import samples" + title: "Import samples to organization %{organization}" + help_text: "Match the columns of your uploaded file with already existing columns in database." + scinote_columns_html: "sciNote columns:" + file_columns: "Imported columns:" + example_value: "Imported file content:" + import_samples: "Import samples" + do_not_include_column: "Do not include this column" + errors: + invalid_file: "The file you provided is invalid." + invalid_extension: "The file has invalid extension." + empty_file: "You've selected empty file. There's not much to import." + temp_file_failure: "We couldn't create temporary file. Please contact administrator." + no_file_selected: "You didn't select any file." + errors_list_title: "Samples were not imported because one or more errors was found:" + file_size_exceeded: "Must be less than 50MB" + list_row: "Row %{row}" + list_error: "%{key}: %{val}" + import_samples: + head_title: "%{organization} | Import samples" + title: "Import samples to organization %{organization}" + sample: + one: "1 sample" + other: "%{count} samples" + success_flash: "%{nr} of %{samples} successfully imported." + partial_success_flash: "%{nr} of %{samples} successfully imported. Other rows contained errors." + errors: + temp_file_not_found: "This file could not be found. Your session might expire." + session_expired: "Your session expired. Please retry importing samples again." + no_data_to_parse: "There's nothing to be parsed." + no_sample_name: "Sample name is required!" + duplicated_values: "Two or more columns have the same mapping." + + sample_groups: + color_label: "Sample group color" + edit: + head_title: "%{organization} | Edit sample group" + title: "Edit sample group %{sample_group} from organization %{organization}" + create: "Edit sample group" + create: + success_flash: "Successfully added sample group %{sample_group} to organization %{organization}." + update: + success_flash: "Successfully updated sample group %{sample_group} to organization %{organization}." + destroy: + success_flash: "Sample group %{sample_group} successfully deleted." + + activities: + index: + today: "Today" + more_activities: "More Activities" + no_activities: "No activities!" + modal: + modal_title: "Activities" + create_project: "%{user} created project %{project}." + rename_project: "%{user} renamed project %{project_old} to %{project_new}." + change_project_visibility: "%{user} changed project %{project}'s visibility to %{visibility}." + archive_project: "%{user} moved project %{project} to archive." + restore_project: "%{user} restored project %{project} from archive." + assign_user_to_project: "%{assigned_user} was added as %{role} to project %{project} by %{assigned_by_user}." + change_user_role_on_project: "%{actor} changed %{user}'s role on project %{project} to %{role}." + unassign_user_from_project: "%{unassigned_user} was removed from project %{project} by %{unassigned_by_user}." + create_module: "%{user} created module %{module}." + assign_user_to_module: "%{assigned_user} was added to module %{module} by %{assigned_by_user}." + unassign_user_from_module: "%{unassigned_user} was removed from module %{module} by %{unassigned_by_user}." + clone_module: "%{user} cloned %{module_new} from %{module_original}." + archive_module: "%{user} moved module %{module} to archive." + restore_module: "%{user} restored module %{module} from archive." + change_module_description: "%{user} changed module %{module}'s description." + create_step: "%{user} created Step %{step} %{step_name}." + destroy_step: "%{user} deleted Step %{step} %{step_name}." + add_comment_to_step: "%{user} commented on Step %{step} %{step_name}." + complete_step: "%{user} completed Step %{step} %{step_name} (%{completed}/%{all} completed)." + uncomplete_step: "%{user} uncompleted Step %{step} %{step_name} (%{completed}/%{all} completed)." + check_step_checklist_item: "%{user} completed task %{checkbox} (%{completed}/%{all} completed) in Step %{step} %{step_name}." + uncheck_step_checklist_item: "%{user} uncompleted task %{checkbox} (%{completed}/%{all} completed) in Step %{step} %{step_name}." + edit_step: "%{user} edited Step %{step} %{step_name}." + add_asset_result: "%{user} added file result %{result}." + add_text_result: "%{user} added text result %{result}." + add_table_result: "%{user} added table result %{result}." + add_comment_to_result: "%{user} commented on result %{result}." + archive_asset_result: "%{user} archived file result %{result}." + archive_text_result: "%{user} archived text result %{result}." + archive_table_result: "%{user} archived table result %{result}." + edit_asset_result: "%{user} edited file result %{result}." + edit_text_result: "%{user} edited text result %{result}." + edit_table_result: "%{user} edited table result %{result}." + + user_my_modules: + new: + head_title: "%{project} | %{module} | Add user" + title: "Add user to module %{module}" + create: "Add user to module" + no_users_available: "All users of the current project all already added to this module." + assign_user: "Add user" + back_button: "Back to module" + create: + success_flash: "Successfully added user %{user} to module %{module}." + error_flash: "User %{user} could not be added to module %{module}." + destroy: + success_flash: "Successfully removed user %{user} from module %{module}." + error_flash: "User %{user} could not be removed from module %{module}." + + step_comments: + new: + head_title: "%{project} | %{module} | Add comment to step" + title: "Add comment to step %{step}" + create: "Add comment" + create: + success_flash: "Successfully added comment to step %{step}." + + result_comments: + new: + head_title: "%{project} | %{module} | Add comment to result" + title: "Add comment to result" + create: "Add comment" + create: + success_flash: "Successfully added comment to result." + + users: + enums: + status: + active: "Active" + pending: "Pending" + invitations: + edit: + head_title: "Accept invitation" + name_label: "Organization name" + name_help: "Organization name is required in order to create your own organization." + registrations: + edit: + head_title: "My profile" + title: "My profile" + avatar_label: "Avatar" + avatar_btn: "Change avatar" + avatar_title: "Change avatar" + avatar_edit_label: "Upload new avatar file" + avatar_submit: "Upload" + name_label: "Full name" + name_title: "Change name" + initials_label: "Initials" + initials_title: "Change initials" + email_label: "Email" + email_title: "Change email" + new_email_label: "New email" + current_password_label: "Current password" + password_explanation: "(we need your current password to confirm your changes)" + waiting_for_confirm: "Currently waiting confirmation for: %{email}" + password_label: "Password" + password_title: "Change password" + new_password_label: "New password" + new_password_2_label: "New password confirmation" + new: + head_title: "Sign up" + name_help: "Organization name is required in order to create your own organization." + settings: + navigation: + preferences: "My preferences" + organizations: "My organizations" + preferences: + head_title: "Settings | My preferences" + edit: + time_zone_label: "Time zone" + time_zone_sublabel: "Time zone setting affects all time & date fields throughout application." + time_zone_title: "Time zone" + update_flash: "Preferences successfully updated." + organizations: + head_title: "Settings | My organizations" + breadcrumbs: + all: "All organizations" + new_organization: "New organization" + index: + member_of: + one: "You are member of %{count} organization." + other: "You are member of %{count} organizations." + no_organizations: "You are not a member of any organization." + new_organization: "New organization" + thead_name: "Organization" + thead_role: "Role" + thead_created_at: "Created at" + thead_joined_on: "Joined on" + thead_members: "Members" + na: "n/a" + leave: "Leave organization" + leave_uo_heading: "Leave organization %{org}" + leave_uo_message: "Are you sure you wish to leave organization %{org}? This action is irreversible." + leave_uo_confirm: "Leave" + leave_flash: "Successfuly left organization %{org}." + new: + name_label: "Organization name" + name_placeholder: "My organization" + name_sublabel: "Pick a name that would best describe your organization (e.g. 'University of ..., Department of ...')." + description_label: "Description" + description_sublabel: "Describe your organization." + create: "Create organization" + edit: + header_created_at: "Created at:" + header_joined_on: "Joined on:" + header_space_taken: "Space taken:" + header_no_description: "No description" + name_title: "Edit organization name" + name_label: "Name" + description_title: "Edit organization description" + description_label: "Description" + manage_users: "Manage users" + add_user: "Invite user" + thead_user_name: "Member name" + thead_email: "Member email" + thead_joined_on: "Joined on" + thead_status: "Status" + thead_role: "Role" + user_dropdown: + btn_label: "Edit" + role_label: "User role" + remove_label: "Remove" + destroy_uo_heading: "Remove user %{user} from organization %{org}" + destroy_uo_message: "Are you sure you wish to remove user %{user} from organization %{org}?" + destroy_uo_confirm: "Remove user" + delete_organization_heading: "Delete organization" + can_delete_message: "This organization can be deleted because it doesn't have any projects." + delete_text: "Delete organization." + cannot_delete_message_projects: "Cannot delete this organization. Only empty organizations (without any projects) can be deleted." + modal_add_user: + title: "Invite user to organization" + existing_heading: "Invite existing sciNote user" + existing_label: "Find existing user by name or email:" + existing_placeholder: "Name or email" + existing_query_too_short: "Query is too short (minimum is 3 characters)" + existing_query_blank: "Query can't be blank" + existing_results_title: "Choose user" + no_existing_users: "No existing users found." + existing_users_smalltext: "Only showing top %{nr} matched results." + existing_flash_success: "User %{user} successfully invited to organization as %{role}." + existing_flash_error: "Error inviting user to organization." + new_heading: "Invite new user" + new_label_name: "Type in the new user's full name:" + new_placeholder_name: "Full name" + new_label_email: "Type in the new user's email:" + new_placeholder_email: "Email" + new_flash_success: "User %{user} successfully invited to organization as %{role}. Confirmation email was sent to %{email}." + invite: "Invite user" + invite_guest: "as Guest" + invite_user: "as Normal user" + invite_admin: "as Administrator" + modal_destroy_organization: + title: "Delete organization" + message: "Are you sure you wish to delete organization %{org}? All of the users will be removed from the organization as well. This action is irreversible." + confirm: "Delete organization" + flash_success: "Organization %{org} was successfully deleted." + forbidden: + title: "Access to this page is denied (403)." + notice: "You do not have permission to view this project page using the + credentials that you supplied." + help: "Ask project owner to grant you permission to access this project." + go_back: "You can go back to" + homepage: "home page" + try_searching: "or try searching." + + not_found: + title: "This page could not be found (404)." + notice: "You may have mistyped the address or the page may have moved." + help: "If you are the application owner check the logs for more information." + go_back: "You can go back to" + homepage: "home page" + try_searching: "or try searching." + + general: + update: "Update" + edit: "Edit" + cancel: "Cancel" + close: "Close" + check_all: "Check all" + no_comments: "No comments!" + more_comments: "More comments" + comment_placeholder: "Your Message" + module: + one: "module" + other: "modules" + public: "public" + private: "private" + search: "Search" + + time: + formats: + full: "%d.%m.%Y %H:%M" + # This format is used for JS datetimepicker, + # only in different notation; + # it should be the same as the above full format + full_js: "D.M.YYYY HH:mm" + full_date: "%d.%m.%Y" + time: "%H:%M" + short: "%H" + + tutorial: + next: "Next" + skip_tutorial: "Skip tutorial" + end_tutorial: "End tutorial" + finish_tutorial: "Start using sciNote" + tutorial_welcome_title_html: "Welcome to sciNote" + welcome_html: "Take a quick tour to see how SciNote works." + create_project_html: "sciNote organizes your work in projects. Click here to create a project and it will appear under the Organization you've chosen. Choose public if you want everyone from the organization to view the project or choose private so only the people who you invite to the project can see it." + project_options_html: "In the bottom row of the project box you can view and add comments , invite collaborators , and see the latest activity and notifications . You can also edit the project's name and visibility or archive it. You can recall the project from the archive at any time. Click on the project name Demo project - qPCR to continue." + canvas_overview_html: "A module is a basic unit of your experiment. Within a module, you can add protocols, results and add samples. You can connect modules into workflows to assure traceability of your work. You can grab the module or entire workflow and move it in all directions on the canvas. Click Edit workflow to add modules or modify your existing workflow." + edit_workflow_html: "To add a new module, simply drag and drop the New module button to an unoccupied place on the canvas. Modules can be a part of a workflow or detached as a single unit. Create more modules and connect them into workflows by clicking Drag connections from here and drag the arrow to the next module. To remove the connection, click on the X in the middle of the arrow. Click to rename module, rename workflow, or archive the module. Click Save workflow to save the changes you've made." + sidebar_html: "Browse the contents in the folder structure or navigate to a specific module on the canvas by clicking the navigate icon . Click on the module name qPCR to see its contents." + module_protocols_html: "Add steps to a protocol, create checklists or upload existing protocol files. After finishing one step, click Complete step to proceed to the next step. Exact time and the person who completed the step are recorded and can be seen at any time in Activity tab. Click on the Results tab to upload experiment results." + module_results_html: "You can upload any type of a file, insert table or write comments. Click on the Samples tab to import, export or assign samples to the module." + samples_html: "You can easily import sample tables from Excel or tab-delimited files. If you assign samples to one module in a workflow, these samples will be automatically assigned to downstream modules." + breadcrumbs_html: "You can always use these breadcrumbs to navigate back. Click on the Reports tab to go to Demo project - qPCR reports page." + reports_index_html: "You can automatically generate reports for meetings, projects, patents or theses. Click New report to create a new report." + new_report_html: "To add elements to the report, click on the —+— sign on the sheet. Select the report contents in the tabs and click Add to report. You can save the report as a PDF on your computer or save it within sciNote to view and modify later. Click on %{private_org} in breadcrumbs to return to the Dashboard." + archive_project_html: "Archive demo project by clicking on the down arrow . You can always access the archive by clicking the button on the top right side of the dashboard and restore any item you have archived." + + # This section contains general words that can be used in any parts of + # application. + + errors: + upload: "Upload error. Try again or contact the administrator." + + Add: "Add" + Asset: "File" + AssetTextData: "File Contents" + Assets: "Files" + Download: "Download" + Module: "Module" + Modules: "Modules" + Project: "Project" + Projects: "Projects" + Result: "Result" + Results: "Results" + Sample: "Sample" + Samples: "Samples" + Reports: "Reports" + Comments: "Comments" + Step: "Step" + Steps: "Steps" + Tag: "Tag" + Tags: "Tags" + Workflow: "Workflow" + Workflows: "Workflows" + More: "More" diff --git a/config/puma.rb b/config/puma.rb new file mode 100644 index 000000000..afdb99a38 --- /dev/null +++ b/config/puma.rb @@ -0,0 +1,15 @@ +workers Integer(ENV['WEB_CONCURRENCY'] || 2) +threads_count = Integer(ENV['MAX_THREADS'] || 5) +threads threads_count, threads_count + +preload_app! + +rackup DefaultRackup +port ENV['PORT'] || 3000 +environment ENV['RACK_ENV'] || 'development' + +on_worker_boot do + # Worker specific setup for Rails 4.1+ + # See: https://devcenter.heroku.com/articles/deploying-rails-applications-with-the-puma-web-server#on-worker-boot + ActiveRecord::Base.establish_connection +end diff --git a/config/routes.rb b/config/routes.rb new file mode 100644 index 000000000..802238c62 --- /dev/null +++ b/config/routes.rb @@ -0,0 +1,184 @@ +Rails.application.routes.draw do + devise_for :users, controllers: { registrations: "users/registrations", + sessions: "users/sessions", invitations: "users/invitations", + confirmations: "users/confirmations" } + + root 'projects#index' + + resources :activities, only: [:index] + + get "forbidden", :to => "application#forbidden", as: "forbidden" + get "not_found", :to => "application#not_found", as: "not_found" + + # Settings + get "users/settings/preferences", to: "users/settings#preferences", as: "preferences" + put "users/settings/preferences", to: "users/settings#update_preferences", as: "update_preferences" + get "users/settings/organizations", to: "users/settings#organizations", as: "organizations" + get "users/settings/organizations/new", to: "users/settings#new_organization", as: "new_organization" + post "users/settings/organizations/new", to: "users/settings#create_organization", as: "create_organization" + get "users/settings/organizations/:organization_id", to: "users/settings#organization", as: "organization" + put "users/settings/organizations/:organization_id", to: "users/settings#update_organization", as: "update_organization" + get "users/settings/organizations/:organization_id/name", to: "users/settings#organization_name", as: "organization_name" + get "users/settings/organizations/:organization_id/description", to: "users/settings#organization_description", as: "organization_description" + get "users/settings/organizations/:organization_id/search", to: "users/settings#search_organization_users", as: "search_organization_users" + post "users/settings/organizations/:organization_id/users_datatable", to: "users/settings#organization_users_datatable", as: "organization_users_datatable" + delete "users/settings/organizations/:organization_id", to: "users/settings#destroy_organization", as: "destroy_organization" + post "users/settings/user_organizations/new", to: "users/settings#create_user_organization", as: "create_user_organization" + post "users/settings/users_organizations/new_user", to: "users/settings#create_user_and_user_organization", as: "create_user_and_user_organization" + put "users/settings/user_organizations/:user_organization_id", to: "users/settings#update_user_organization", as: "update_user_organization" + get "users/settings/user_organizations/:user_organization_id/leave_html", to: "users/settings#leave_user_organization_html", as: "leave_user_organization_html" + get "users/settings/user_organizations/:user_organization_id/destroy_html", to: "users/settings#destroy_user_organization_html", as: "destroy_user_organization_html" + delete "users/settings/user_organizations/:user_organization_id", to: "users/settings#destroy_user_organization", as: "destroy_user_organization" + + resources :organizations, only: [] do + resources :samples, only: [:new, :create] + resources :sample_types, only: [:new, :create] + resources :sample_groups, only: [:new, :create] + resources :custom_fields, only: [:create] + member do + post 'parse_sheet' + post 'import_samples' + post 'export_samples' + end + match '*path', :to => 'organizations#routing_error', via: [:get, :post, :put, :patch] + end + + get 'projects/archive', to: 'projects#archive', as: 'projects_archive' + + resources :projects, except: [:new, :destroy] do + resources :user_projects, path: "/users", only: [:new, :create, :index, :edit, :update, :destroy] + resources :project_comments, path: "/comments", only: [:new, :create, :index] + # Activities popup (JSON) for individual project in projects index, + # as well as all activities page for single project (HTML) + resources :project_activities, path: "/activities", only: [:index] + resources :tags, only: [:create, :update, :destroy] + resources :reports, path: "/reports", only: [:index, :new, :create, :edit, :update] do + collection do + # The posts following here should in theory be gets, + # but are posts because of parameters payload + post 'generate', to: 'reports#generate' + get 'new/by_module', to: 'reports#new_by_module' + get 'new/by_module/project_contents_modal', + to: 'reports#project_contents_modal', + as: :project_contents_modal + post 'new/by_module/project_contents', + to: 'reports#project_contents', + as: :project_contents + get 'new/by_module/module_contents_modal', + to: 'reports#module_contents_modal', + as: :module_contents_modal + post 'new/by_module/module_contents', + to: 'reports#module_contents', + as: :module_contents + get 'new/by_module/step_contents_modal', + to: 'reports#step_contents_modal', + as: :step_contents_modal + post 'new/by_module/step_contents', + to: 'reports#step_contents', + as: :step_contents + get 'new/by_module/result_contents_modal', + to: 'reports#result_contents_modal', + as: :result_contents_modal + post 'new/by_module/result_contents', + to: 'reports#result_contents', + as: :result_contents + get 'new/by_timestamp', to: 'reports#new_by_timestamp' + post '_save', to: 'reports#save_modal', as: :save_modal + post 'destroy', as: :destroy # Destroy multiple entries at once + end + end + member do + get 'canvas' # Overview/structure for single project + get 'canvas/edit', to: 'canvas#edit' # AJAX-loaded canvas edit mode (from canvas) + get 'canvas/full_zoom', to: 'canvas#full_zoom' # AJAX-loaded canvas zoom + get 'canvas/medium_zoom', to: 'canvas#medium_zoom' # AJAX-loaded canvas zoom + get 'canvas/small_zoom', to: 'canvas#small_zoom' # AJAX-loaded canvas zoom + post 'canvas', to: 'canvas#update' # Save updated canvas action + get 'notifications' # Notifications popup for individual project in projects index + get 'samples' # Samples for single project + get 'module_archive' # Module archive for single project + post 'samples_index' # Renders sample datatable for single project (ajax action) + post :delete_samples, constraints: CommitParamRouting.new(MyModulesController::DELETE_SAMPLES), action: :delete_samples + end + + # This route is defined outside of member block to preserve original :project_id parameter in URL. + get 'users/edit', to: 'user_projects#index_edit' + end + + # Show action is a popup (JSON) for individual module in full-zoom canvas, + # as well as "module info" page for single module (HTML) + resources :my_modules, path: "/modules", only: [:show, :edit, :update, :destroy] do + resources :my_module_tags, path: "/tags", only: [:index, :create, :update, :destroy] + resources :user_my_modules, path: "/users", only: [:index, :new, :create, :destroy] + resources :my_module_comments, path: "/comments", only: [:index, :new, :create] + resources :sample_my_modules, path: "/samples_index", only: [:index] + resources :steps, only: [:new, :create] + resources :result_texts, only: [:new, :create] + resources :result_assets, only: [:new, :create] + resources :result_tables, only: [:new, :create] + member do + # AJAX popup accessed from full-zoom canvas for single module, + # as well as full activities view (HTML) for single module + get 'description' + get 'activities' + get 'activities_tab' # Activities in tab view for single module + get 'due_date' + get 'steps' # Steps view for single module + get 'results' # Results view for single module + get 'samples' # Samples view for single module + get 'archive' # Archive view for single module + post 'samples_index' # Renders sample datatable for single module (ajax action) + post :assign_samples, constraints: CommitParamRouting.new(MyModulesController::ASSIGN_SAMPLES), action: :assign_samples + post :assign_samples, constraints: CommitParamRouting.new(MyModulesController::UNASSIGN_SAMPLES), action: :unassign_samples + post :assign_samples, constraints: CommitParamRouting.new(MyModulesController::DELETE_SAMPLES), action: :delete_samples + end + + # Those routes are defined outside of member block to preserve original id parameters in URL. + get 'tags/edit', to: 'my_module_tags#index_edit' + get 'users/edit', to: 'user_my_modules#index_edit' + end + + resources :steps, only: [:edit, :update, :destroy, :show] do + resources :step_comments, path: "/comments", only: [:new, :create, :index] + member do + post 'checklistitem_state' + post 'toggle_step_state' + get 'move_down' + get 'move_up' + end + end + + resources :results, only: [:update] do + resources :result_comments, path: "/comments", only: [:new, :create, :index] + end + + resources :samples, only: [:edit, :update, :destroy] + resources :sample_types, only: [:edit, :update] + resources :sample_groups, only: [:edit, :update] + resources :result_texts, only: [:edit, :update, :destroy] + get 'result_texts/:id/download' => 'result_texts#download', + as: :result_text_download + resources :result_assets, only: [:edit, :update, :destroy] + get 'result_assets/:id/download' => 'result_assets#download', + as: :result_asset_download + resources :result_tables, only: [:edit, :update, :destroy] + get 'result_tables/:id/download' => 'result_tables#download', + as: :result_table_download + + get 'search' => 'search#index' + get 'search/new' => 'search#new', as: :new_search + + resources :assets, only: [:show] do + member do + get :preview + get :download + end + end + + post 'asset_signature' => 'assets#signature' + + devise_scope :user do + get 'avatar/:style' => 'users/registrations#avatar', as: 'avatar' + post 'avatar_signature' => 'users/registrations#signature' + end +end diff --git a/config/secrets.yml b/config/secrets.yml new file mode 100644 index 000000000..831480ff7 --- /dev/null +++ b/config/secrets.yml @@ -0,0 +1,48 @@ +# Be sure to restart your server when you modify this file. + +common: &common + +# ====================================================================== +# Server address is used for mailer (in "confirm new email/..." emails, +# sciNote needs to know onto which URL to redirect the user for +# confirmation page) +# ====================================================================== + + mail_server_url: <%= ENV["MAIL_SERVER_URL"] || "localhost" %> + +# ====================================================================== +# Mailer configuration to define from which SMTP server to send +# e-mails. +# ====================================================================== + + mailer_from: <%= ENV["MAIL_FROM"] %> + mailer_reply_to: <%= ENV["MAIL_REPLYTO"] %> + mailer_address: <%= ENV["SMTP_ADDRESS"] %> + mailer_port: <%= ENV["SMTP_PORT"] || "587" %> + mailer_domain: <%= ENV["SMTP_DOMAIN"] %> + mailer_user_name: <%= ENV["SMTP_USERNAME"] %> + mailer_password: <%= ENV["SMTP_PASSWORD"] %> + +# ====================================================================== +# Your secret key is used for verifying the integrity of signed cookies. +# If you change this key, all old signed cookies will become invalid! + +# Make sure the secret is at least 30 characters and all random, +# no regular words or you'll be exposed to dictionary attacks. +# You can use `rake secret` to generate a secure secret key. +# ====================================================================== + + secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> + +# ====================================================================== +# Write any potential environment-specific secrets here. +# ====================================================================== + +development: + <<: *common + +test: + <<: *common + +production: + <<: *common \ No newline at end of file diff --git a/config/skylight.yml b/config/skylight.yml new file mode 100644 index 000000000..0bf0b90e6 --- /dev/null +++ b/config/skylight.yml @@ -0,0 +1,3 @@ +--- +# The authentication token for the application. +authentication: <%= ENV["SKYLIGHT_AUTHENTICATION"] ?> diff --git a/db/load_users_template.yml b/db/load_users_template.yml new file mode 100644 index 000000000..300351041 --- /dev/null +++ b/db/load_users_template.yml @@ -0,0 +1,25 @@ +org_1: + name: Org1 + +org_2: + name: Org2 + +user_1: + full_name: Usr1 + email: usr1@gmail.com + organizations: org_1, org_2 + password: secretPassword + +user_2: + full_name: Usr2 + email: usr2@gmail.com + organizations: org_1 + +user_3: + full_name: Usr3 + email: usr3@gmail.com + organizations: org_2 + +user_4: + full_name: Usr4 + email: usr4@gmail.com \ No newline at end of file diff --git a/db/migrate/20150713060702_devise_create_users.rb b/db/migrate/20150713060702_devise_create_users.rb new file mode 100644 index 000000000..15f651d46 --- /dev/null +++ b/db/migrate/20150713060702_devise_create_users.rb @@ -0,0 +1,46 @@ +class DeviseCreateUsers < ActiveRecord::Migration + def change + create_table(:users) do |t| + ## General user data + t.string :full_name, null: false + t.string :initials, null: false + + ## Database authenticatable + t.string :email, null: false, default: "" + t.string :encrypted_password, null: false, default: "" + + ## Recoverable + t.string :reset_password_token + t.datetime :reset_password_sent_at + + ## Rememberable + t.datetime :remember_created_at + + ## Trackable + t.integer :sign_in_count, default: 0, null: false + t.datetime :current_sign_in_at + t.datetime :last_sign_in_at + t.string :current_sign_in_ip + t.string :last_sign_in_ip + + ## Confirmable + # t.string :confirmation_token + # t.datetime :confirmed_at + # t.datetime :confirmation_sent_at + # t.string :unconfirmed_email # Only if using reconfirmable + + ## Lockable + # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts + # t.string :unlock_token # Only if unlock strategy is :email or :both + # t.datetime :locked_at + + + t.timestamps null: false + end + + add_index :users, :email, unique: true + add_index :users, :reset_password_token, unique: true + # add_index :users, :confirmation_token, unique: true + # add_index :users, :unlock_token, unique: true + end +end diff --git a/db/migrate/20150713061603_add_user_columns.rb b/db/migrate/20150713061603_add_user_columns.rb new file mode 100644 index 000000000..54153ea3d --- /dev/null +++ b/db/migrate/20150713061603_add_user_columns.rb @@ -0,0 +1,9 @@ +class AddUserColumns < ActiveRecord::Migration + def up + add_attachment :users, :avatar + end + + def down + remove_attachment :users, :avatar + end +end diff --git a/db/migrate/20150713063224_create_organizations.rb b/db/migrate/20150713063224_create_organizations.rb new file mode 100644 index 000000000..7e03258c7 --- /dev/null +++ b/db/migrate/20150713063224_create_organizations.rb @@ -0,0 +1,11 @@ +class CreateOrganizations < ActiveRecord::Migration + def change + create_table :organizations do |t| + ## General info + t.string :name, null: false + + t.timestamps null: false + end + add_index :organizations, :name, unique: true + end +end diff --git a/db/migrate/20150713070738_create_user_organizations.rb b/db/migrate/20150713070738_create_user_organizations.rb new file mode 100644 index 000000000..5b929adbe --- /dev/null +++ b/db/migrate/20150713070738_create_user_organizations.rb @@ -0,0 +1,13 @@ +class CreateUserOrganizations < ActiveRecord::Migration + def change + create_table :user_organizations do |t| + t.column :role, :integer, null: false, default: 1 + t.integer :user_id, null: false + t.integer :organization_id, null: false + + t.timestamps null: false + end + add_foreign_key :user_organizations, :users + add_foreign_key :user_organizations, :organizations + end +end diff --git a/db/migrate/20150713071921_create_projects.rb b/db/migrate/20150713071921_create_projects.rb new file mode 100644 index 000000000..e651efc5a --- /dev/null +++ b/db/migrate/20150713071921_create_projects.rb @@ -0,0 +1,13 @@ +class CreateProjects < ActiveRecord::Migration + def change + create_table :projects do |t| + t.string :name, null: false + t.column :visibility, :integer, null: false, default: 0 + t.datetime :due_date + t.integer :organization_id, null: false + + t.timestamps null: false + end + add_foreign_key :projects, :organizations + end +end diff --git a/db/migrate/20150713072417_create_user_projects.rb b/db/migrate/20150713072417_create_user_projects.rb new file mode 100644 index 000000000..c7ecfa696 --- /dev/null +++ b/db/migrate/20150713072417_create_user_projects.rb @@ -0,0 +1,17 @@ +class CreateUserProjects < ActiveRecord::Migration + def change + create_table :user_projects do |t| + t.column :role, :integer, default: 0 + + # Comment in spite of SQLite + # t.integer :permissions, array: true, default: [] + + t.integer :user_id, null: false + t.integer :project_id, null: false + + t.timestamps null: false + end + add_foreign_key :user_projects, :users + add_foreign_key :user_projects, :projects + end +end diff --git a/db/migrate/20150714125221_add_archive_to_projects.rb b/db/migrate/20150714125221_add_archive_to_projects.rb new file mode 100644 index 000000000..f8e4e9c52 --- /dev/null +++ b/db/migrate/20150714125221_add_archive_to_projects.rb @@ -0,0 +1,6 @@ +class AddArchiveToProjects < ActiveRecord::Migration + def change + add_column :projects, :archived, :boolean, { default: false, null: false } + add_column :projects, :archived_on, :datetime + end +end diff --git a/db/migrate/20150715122019_create_logs.rb b/db/migrate/20150715122019_create_logs.rb new file mode 100644 index 000000000..3ce29d334 --- /dev/null +++ b/db/migrate/20150715122019_create_logs.rb @@ -0,0 +1,9 @@ +class CreateLogs < ActiveRecord::Migration + def change + create_table :logs do |t| + t.integer :organization_id, null: false + t.string :message, null:false + end + add_foreign_key :logs, :organizations + end +end diff --git a/db/migrate/20150715124934_create_my_modules.rb b/db/migrate/20150715124934_create_my_modules.rb new file mode 100644 index 000000000..fb231234d --- /dev/null +++ b/db/migrate/20150715124934_create_my_modules.rb @@ -0,0 +1,22 @@ +class CreateMyModules < ActiveRecord::Migration + def change + create_table :my_modules do |t| + t.string :name, null: false + t.datetime :due_date + t.string :description + + # Positions + t.integer :x, null: false, default: 0 + t.integer :y, null: false, default: 0 + + # Foreign keys + t.integer :project_id, null: false + t.integer :my_module_group_id + + t.timestamps null: false + end + add_foreign_key :my_modules, :projects + add_index :my_modules, :project_id + add_index :my_modules, :my_module_group_id + end +end diff --git a/db/migrate/20150715131400_create_project_comments.rb b/db/migrate/20150715131400_create_project_comments.rb new file mode 100644 index 000000000..fc4cf9e26 --- /dev/null +++ b/db/migrate/20150715131400_create_project_comments.rb @@ -0,0 +1,10 @@ +class CreateProjectComments < ActiveRecord::Migration + def change + create_table :project_comments do |t| + t.integer :project_id, null: false + t.integer :comment_id, null: false + end + add_foreign_key :project_comments, :projects + add_index :project_comments, [:project_id, :comment_id] + end +end diff --git a/db/migrate/20150715132459_create_my_module_comments.rb b/db/migrate/20150715132459_create_my_module_comments.rb new file mode 100644 index 000000000..fe6f34946 --- /dev/null +++ b/db/migrate/20150715132459_create_my_module_comments.rb @@ -0,0 +1,10 @@ +class CreateMyModuleComments < ActiveRecord::Migration + def change + create_table :my_module_comments do |t| + t.integer :my_module_id, null: false + t.integer :comment_id, null: false + end + add_foreign_key :my_module_comments, :my_modules + add_index :my_module_comments, [:my_module_id, :comment_id] + end +end diff --git a/db/migrate/20150715132920_create_tags.rb b/db/migrate/20150715132920_create_tags.rb new file mode 100644 index 000000000..df4785d76 --- /dev/null +++ b/db/migrate/20150715132920_create_tags.rb @@ -0,0 +1,9 @@ +class CreateTags < ActiveRecord::Migration + def change + create_table :tags do |t| + t.string :name, null:false + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20150715133511_create_my_modules_and_tags.rb b/db/migrate/20150715133511_create_my_modules_and_tags.rb new file mode 100644 index 000000000..dd916ca4d --- /dev/null +++ b/db/migrate/20150715133511_create_my_modules_and_tags.rb @@ -0,0 +1,8 @@ +class CreateMyModulesAndTags < ActiveRecord::Migration + def change + create_table :my_module_tags do |t| + t.belongs_to :my_module, index: true + t.belongs_to :tag, index: true + end + end +end diff --git a/db/migrate/20150715133709_create_my_module_groups.rb b/db/migrate/20150715133709_create_my_module_groups.rb new file mode 100644 index 000000000..37ad4c2af --- /dev/null +++ b/db/migrate/20150715133709_create_my_module_groups.rb @@ -0,0 +1,9 @@ +class CreateMyModuleGroups < ActiveRecord::Migration + def change + create_table :my_module_groups do |t| + t.string :name, null: false + + t.timestamps null: false + end + end +end diff --git a/db/migrate/20150715134133_create_connections.rb b/db/migrate/20150715134133_create_connections.rb new file mode 100644 index 000000000..819cdc410 --- /dev/null +++ b/db/migrate/20150715134133_create_connections.rb @@ -0,0 +1,10 @@ +class CreateConnections < ActiveRecord::Migration + def change + create_table :connections do |t| + t.integer :input_id, null: false + t.integer :output_id, null: false + end + add_foreign_key :connections, :my_modules, column: :input_id + add_foreign_key :connections, :my_modules, column: :output_id + end +end diff --git a/db/migrate/20150715135452_create_steps.rb b/db/migrate/20150715135452_create_steps.rb new file mode 100644 index 000000000..312cc5e9e --- /dev/null +++ b/db/migrate/20150715135452_create_steps.rb @@ -0,0 +1,21 @@ +class CreateSteps < ActiveRecord::Migration + def change + create_table :steps do |t| + t.string :name + t.string :description + t.integer :position, null: false + t.boolean :completed, null: false + t.datetime :completed_on + t.integer :user_id, null: false + t.integer :my_module_id, null: false + + t.timestamps null: false + end + add_foreign_key :steps, :users + add_foreign_key :steps, :my_modules + add_index :steps, :my_module_id + add_index :steps, :user_id + add_index :steps, :created_at + add_index :steps, :position + end +end diff --git a/db/migrate/20150715141810_create_assets.rb b/db/migrate/20150715141810_create_assets.rb new file mode 100644 index 000000000..63e0f5604 --- /dev/null +++ b/db/migrate/20150715141810_create_assets.rb @@ -0,0 +1,10 @@ +class CreateAssets < ActiveRecord::Migration + def change + create_table :assets do |t| + + t.timestamps null: false + end + add_attachment :assets, :file + add_index :assets, :created_at + end +end diff --git a/db/migrate/20150715142704_create_step_assets.rb b/db/migrate/20150715142704_create_step_assets.rb new file mode 100644 index 000000000..7e711cb45 --- /dev/null +++ b/db/migrate/20150715142704_create_step_assets.rb @@ -0,0 +1,11 @@ +class CreateStepAssets < ActiveRecord::Migration + def change + create_table :step_assets do |t| + t.integer :step_id, null: false + t.integer :asset_id, null: false + end + add_foreign_key :step_assets, :steps + add_foreign_key :step_assets, :assets + add_index :step_assets, [:step_id, :asset_id] + end +end diff --git a/db/migrate/20150715142929_create_result_assets.rb b/db/migrate/20150715142929_create_result_assets.rb new file mode 100644 index 000000000..661073237 --- /dev/null +++ b/db/migrate/20150715142929_create_result_assets.rb @@ -0,0 +1,10 @@ +class CreateResultAssets < ActiveRecord::Migration + def change + create_table :result_assets do |t| + t.integer :result_id, null: false + t.integer :asset_id, null: false + end + add_foreign_key :result_assets, :assets + add_index :result_assets, [:result_id, :asset_id] + end +end diff --git a/db/migrate/20150715143134_create_results.rb b/db/migrate/20150715143134_create_results.rb new file mode 100644 index 000000000..4f7bce5d5 --- /dev/null +++ b/db/migrate/20150715143134_create_results.rb @@ -0,0 +1,16 @@ +class CreateResults < ActiveRecord::Migration + def change + create_table :results do |t| + t.string :name + t.integer :my_module_id, null: false + t.integer :user_id, null: false + + t.timestamps null: false + end + add_foreign_key :results, :users + add_foreign_key :results, :my_modules + add_index :results, :my_module_id + add_index :results, :user_id + add_index :results, :created_at + end +end diff --git a/db/migrate/20150716060140_create_result_comments.rb b/db/migrate/20150716060140_create_result_comments.rb new file mode 100644 index 000000000..4aae1aadc --- /dev/null +++ b/db/migrate/20150716060140_create_result_comments.rb @@ -0,0 +1,10 @@ +class CreateResultComments < ActiveRecord::Migration + def change + create_table :result_comments do |t| + t.integer :result_id, null: false + t.integer :comment_id, null: false + end + add_foreign_key :result_comments, :results + add_index :result_comments, [:result_id, :comment_id] + end +end diff --git a/db/migrate/20150716061004_create_comments.rb b/db/migrate/20150716061004_create_comments.rb new file mode 100644 index 000000000..79d333cf0 --- /dev/null +++ b/db/migrate/20150716061004_create_comments.rb @@ -0,0 +1,13 @@ +class CreateComments < ActiveRecord::Migration + def change + create_table :comments do |t| + t.string :message, null: false + t.integer :user_id, null: false + + t.timestamps null: false + end + add_foreign_key :comments, :users + add_index :comments, :user_id + add_index :comments, :created_at + end +end diff --git a/db/migrate/20150716061555_create_step_comments.rb b/db/migrate/20150716061555_create_step_comments.rb new file mode 100644 index 000000000..824ef7f20 --- /dev/null +++ b/db/migrate/20150716061555_create_step_comments.rb @@ -0,0 +1,11 @@ +class CreateStepComments < ActiveRecord::Migration + def change + create_table :step_comments do |t| + t.integer :step_id, null: false + t.integer :comment_id, null: false + end + add_foreign_key :step_comments, :steps + add_foreign_key :step_comments, :comments + add_index :step_comments, [:step_id, :comment_id] + end +end diff --git a/db/migrate/20150716061937_create_tables.rb b/db/migrate/20150716061937_create_tables.rb new file mode 100644 index 000000000..86a96f6bd --- /dev/null +++ b/db/migrate/20150716061937_create_tables.rb @@ -0,0 +1,10 @@ +class CreateTables < ActiveRecord::Migration + def change + create_table :tables do |t| + t.binary :contents, null: false, limit: 20.megabyte + + t.timestamps null: false + end + add_index :tables, :created_at + end +end diff --git a/db/migrate/20150716062013_create_checklists.rb b/db/migrate/20150716062013_create_checklists.rb new file mode 100644 index 000000000..60c3180f8 --- /dev/null +++ b/db/migrate/20150716062013_create_checklists.rb @@ -0,0 +1,11 @@ +class CreateChecklists < ActiveRecord::Migration + def change + create_table :checklists do |t| + t.string :name, null: false + t.integer :step_id, null: false + + t.timestamps null: false + end + add_foreign_key :checklists, :steps + end +end diff --git a/db/migrate/20150716062110_create_checklist_items.rb b/db/migrate/20150716062110_create_checklist_items.rb new file mode 100644 index 000000000..8a10eaae4 --- /dev/null +++ b/db/migrate/20150716062110_create_checklist_items.rb @@ -0,0 +1,12 @@ +class CreateChecklistItems < ActiveRecord::Migration + def change + create_table :checklist_items do |t| + t.string :text, null: false + t.boolean :checked, null: false, default: false + t.integer :checklist_id, null: false + + t.timestamps null: false + end + add_index :checklist_items, :checklist_id + end +end diff --git a/db/migrate/20150716062801_create_samples.rb b/db/migrate/20150716062801_create_samples.rb new file mode 100644 index 000000000..5eda9b741 --- /dev/null +++ b/db/migrate/20150716062801_create_samples.rb @@ -0,0 +1,17 @@ +class CreateSamples < ActiveRecord::Migration + def change + create_table :samples do |t| + t.string :name, null: false + + # Foreign keys + t.integer :user_id, null: false + t.integer :organization_id, null: false + + t.timestamps null: false + end + add_foreign_key :samples, :users + add_foreign_key :samples, :organizations + add_index :samples, :user_id + add_index :samples, :organization_id + end +end diff --git a/db/migrate/20150716064453_create_activities.rb b/db/migrate/20150716064453_create_activities.rb new file mode 100644 index 000000000..5573a349e --- /dev/null +++ b/db/migrate/20150716064453_create_activities.rb @@ -0,0 +1,18 @@ +class CreateActivities < ActiveRecord::Migration + def change + create_table :activities do |t| + t.integer :my_module_id, null: false + t.integer :user_id + t.integer :type_of, null: false + t.string :message, null: false + + t.timestamps null: false + end + add_foreign_key :activities, :my_modules + add_foreign_key :activities, :users + add_index :activities, :my_module_id + add_index :activities, :user_id + add_index :activities, :type_of + add_index :activities, :created_at + end +end diff --git a/db/migrate/20150716120130_create_sample_my_modules.rb b/db/migrate/20150716120130_create_sample_my_modules.rb new file mode 100644 index 000000000..3ed3555f7 --- /dev/null +++ b/db/migrate/20150716120130_create_sample_my_modules.rb @@ -0,0 +1,11 @@ +class CreateSampleMyModules < ActiveRecord::Migration + def change + create_table :sample_my_modules do |t| + t.integer :sample_id, null: false + t.integer :my_module_id, null: false + end + add_foreign_key :sample_my_modules, :samples + add_foreign_key :sample_my_modules, :my_modules + add_index :sample_my_modules, [:sample_id, :my_module_id] + end +end diff --git a/db/migrate/20150716120659_create_sample_comments.rb b/db/migrate/20150716120659_create_sample_comments.rb new file mode 100644 index 000000000..161413ff4 --- /dev/null +++ b/db/migrate/20150716120659_create_sample_comments.rb @@ -0,0 +1,11 @@ +class CreateSampleComments < ActiveRecord::Migration + def change + create_table :sample_comments do |t| + t.integer :sample_id, null: false + t.integer :comment_id, null: false + end + add_foreign_key :sample_comments, :samples + add_foreign_key :sample_comments, :comments + add_index :sample_comments, [:sample_id, :comment_id] + end +end diff --git a/db/migrate/20150717084645_create_result_texts.rb b/db/migrate/20150717084645_create_result_texts.rb new file mode 100644 index 000000000..7b566bf72 --- /dev/null +++ b/db/migrate/20150717084645_create_result_texts.rb @@ -0,0 +1,10 @@ +class CreateResultTexts < ActiveRecord::Migration + def change + create_table :result_texts do |t| + t.string :text, null: false + t.integer :result_id, null: false + end + add_foreign_key :result_texts, :results + add_index :result_texts, :result_id + end +end diff --git a/db/migrate/20150717085043_create_step_tables.rb b/db/migrate/20150717085043_create_step_tables.rb new file mode 100644 index 000000000..716f22a2c --- /dev/null +++ b/db/migrate/20150717085043_create_step_tables.rb @@ -0,0 +1,11 @@ +class CreateStepTables < ActiveRecord::Migration + def change + create_table :step_tables do |t| + t.integer :step_id, null: false + t.integer :table_id, null: false + end + add_foreign_key :step_tables, :steps + add_foreign_key :step_tables, :tables + add_index :step_tables, [:step_id, :table_id], unique: true + end +end diff --git a/db/migrate/20150717085133_create_result_tables.rb b/db/migrate/20150717085133_create_result_tables.rb new file mode 100644 index 000000000..ab35abd78 --- /dev/null +++ b/db/migrate/20150717085133_create_result_tables.rb @@ -0,0 +1,11 @@ +class CreateResultTables < ActiveRecord::Migration + def change + create_table :result_tables do |t| + t.integer :result_id, null: false + t.integer :table_id, null: false + end + add_foreign_key :result_tables, :results + add_foreign_key :result_tables, :tables + add_index :result_tables, [:result_id, :table_id] + end +end diff --git a/db/migrate/20150722095027_create_user_my_modules.rb b/db/migrate/20150722095027_create_user_my_modules.rb new file mode 100644 index 000000000..e82912680 --- /dev/null +++ b/db/migrate/20150722095027_create_user_my_modules.rb @@ -0,0 +1,14 @@ +class CreateUserMyModules < ActiveRecord::Migration + def change + create_table :user_my_modules do |t| + t.integer :user_id, null: false + t.integer :my_module_id, null: false + + t.timestamps null: false + end + add_foreign_key :user_my_modules, :users + add_foreign_key :user_my_modules, :my_modules + add_index :user_my_modules, :user_id + add_index :user_my_modules, :my_module_id + end +end diff --git a/db/migrate/20150722112911_add_foreign_keys.rb b/db/migrate/20150722112911_add_foreign_keys.rb new file mode 100644 index 000000000..02d96a385 --- /dev/null +++ b/db/migrate/20150722112911_add_foreign_keys.rb @@ -0,0 +1,10 @@ +class AddForeignKeys < ActiveRecord::Migration + def change + add_foreign_key :my_modules, :my_module_groups + add_foreign_key :project_comments, :comments + add_foreign_key :my_module_comments, :comments + add_foreign_key :result_assets, :results + add_foreign_key :result_comments, :comments + add_foreign_key :checklist_items, :checklists + end +end diff --git a/db/migrate/20150723134648_add_confirmable_to_devise.rb b/db/migrate/20150723134648_add_confirmable_to_devise.rb new file mode 100644 index 000000000..b1f0d28b0 --- /dev/null +++ b/db/migrate/20150723134648_add_confirmable_to_devise.rb @@ -0,0 +1,20 @@ +class AddConfirmableToDevise < ActiveRecord::Migration + def up + add_column :users, :confirmation_token, :string + add_column :users, :confirmed_at, :datetime + add_column :users, :confirmation_sent_at, :datetime + add_column :users, :unconfirmed_email, :string + add_index :users, :confirmation_token, unique: true + + # SQlite All existing user accounts should be able to log in after this. + execute("UPDATE users SET confirmed_at = date('now')") + + # For postgres + #execute("UPDATE users SET confirmed_at = NOW()") + end + + def down + remove_columns :users, :confirmation_token, :confirmed_at, :confirmation_sent_at + remove_columns :users, :unconfirmed_email + end +end diff --git a/db/migrate/20150730090021_add_my_module_groups_to_project.rb b/db/migrate/20150730090021_add_my_module_groups_to_project.rb new file mode 100644 index 000000000..7352a61df --- /dev/null +++ b/db/migrate/20150730090021_add_my_module_groups_to_project.rb @@ -0,0 +1,18 @@ +class AddMyModuleGroupsToProject < ActiveRecord::Migration + def change + add_column :my_module_groups, :project_id, :integer + + # Update current module groups + MyModuleGroup.all.each do |my_module_group| + if my_module_group.my_modules.present? then + my_module_group.project_id = my_module_group.my_modules.first.project.id + my_module_group.save + end + end + + # Now make column non-nullable + change_column :my_module_groups, :project_id, :integer, :null => false + add_foreign_key :my_module_groups, :projects + add_index :my_module_groups, :project_id + end +end diff --git a/db/migrate/20150804055341_add_color_to_tags.rb b/db/migrate/20150804055341_add_color_to_tags.rb new file mode 100644 index 000000000..03ad53cd3 --- /dev/null +++ b/db/migrate/20150804055341_add_color_to_tags.rb @@ -0,0 +1,5 @@ +class AddColorToTags < ActiveRecord::Migration + def change + add_column :tags, :color, :string, { default: "#ff0000", null: false } + end +end diff --git a/db/migrate/20150820120553_create_sample_groups.rb b/db/migrate/20150820120553_create_sample_groups.rb new file mode 100644 index 000000000..733ecd882 --- /dev/null +++ b/db/migrate/20150820120553_create_sample_groups.rb @@ -0,0 +1,13 @@ +class CreateSampleGroups < ActiveRecord::Migration + def change + create_table :sample_groups do |t| + t.string :name, null: false + t.string :color, null: false, default: "#ff0000" + + t.integer :organization_id, null: false + t.timestamps null: false + end + add_foreign_key :sample_groups, :organizations + add_index :sample_groups, :organization_id + end +end diff --git a/db/migrate/20150820123018_create_sample_types.rb b/db/migrate/20150820123018_create_sample_types.rb new file mode 100644 index 000000000..1a434c80e --- /dev/null +++ b/db/migrate/20150820123018_create_sample_types.rb @@ -0,0 +1,12 @@ +class CreateSampleTypes < ActiveRecord::Migration + def change + create_table :sample_types do |t| + t.string :name, null: false + + t.integer :organization_id, null: false + t.timestamps null: false + end + add_foreign_key :sample_types, :organizations + add_index :sample_types, :organization_id + end +end diff --git a/db/migrate/20150820124022_add_default_columns_to_samples.rb b/db/migrate/20150820124022_add_default_columns_to_samples.rb new file mode 100644 index 000000000..41d361460 --- /dev/null +++ b/db/migrate/20150820124022_add_default_columns_to_samples.rb @@ -0,0 +1,11 @@ +class AddDefaultColumnsToSamples < ActiveRecord::Migration + def change + add_column :samples, :sample_group_id, :integer + add_column :samples, :sample_type_id, :integer + + add_foreign_key :samples, :sample_groups + add_foreign_key :samples, :sample_types + add_index :samples, :sample_group_id + add_index :samples, :sample_type_id + end +end diff --git a/db/migrate/20150827130647_create_custom_fields.rb b/db/migrate/20150827130647_create_custom_fields.rb new file mode 100644 index 000000000..04c340df5 --- /dev/null +++ b/db/migrate/20150827130647_create_custom_fields.rb @@ -0,0 +1,17 @@ +class CreateCustomFields < ActiveRecord::Migration + def change + create_table :custom_fields do |t| + t.string :name, null: false + + t.integer :user_id, null: false + t.integer :organization_id, null: false + + t.timestamps null: false + end + add_foreign_key :custom_fields, :users + add_foreign_key :custom_fields, :organizations + + add_index :custom_fields, :user_id + add_index :custom_fields, :organization_id + end +end diff --git a/db/migrate/20150827130822_create_sample_custom_fields.rb b/db/migrate/20150827130822_create_sample_custom_fields.rb new file mode 100644 index 000000000..5cb52dec2 --- /dev/null +++ b/db/migrate/20150827130822_create_sample_custom_fields.rb @@ -0,0 +1,17 @@ +class CreateSampleCustomFields < ActiveRecord::Migration + def change + create_table :sample_custom_fields do |t| + t.string :value, null: false + + t.integer :custom_field_id, null: false + t.integer :sample_id, null: :false + + t.timestamps null: false + end + add_foreign_key :sample_custom_fields, :custom_fields + add_foreign_key :sample_custom_fields, :samples + + add_index :sample_custom_fields, :custom_field_id + add_index :sample_custom_fields, :sample_id + end +end diff --git a/db/migrate/20150911125914_add_project_to_tags.rb b/db/migrate/20150911125914_add_project_to_tags.rb new file mode 100644 index 000000000..2766b2ead --- /dev/null +++ b/db/migrate/20150911125914_add_project_to_tags.rb @@ -0,0 +1,33 @@ +class AddProjectToTags < ActiveRecord::Migration + def change + # Add project ID reference, make it nullable at first + add_column :tags, :project_id, :integer, { null: true } + add_foreign_key :tags, :projects + + # Clone tags for each project, just to make sure not a single tag has + # null project_id + all_tags = Tag.all + all_tags.each do |tag| + Project.all.each do |project| + new_tag = Tag.create( + name: tag.name, + color: tag.color, + project: project + ) + + # Okay, add all my_module-tag references + tag.my_module_tags.each do |mmt| + MyModuleTag.create( + my_module: mmt.my_module, + tag: new_tag) + end + end + end + + # Okay, clear all tags that still have nil project reference + Tag.where(project_id: nil).destroy_all + + # Make project ID not null + change_column_null :tags, :project_id, false + end +end diff --git a/db/migrate/20150915074650_create_reports.rb b/db/migrate/20150915074650_create_reports.rb new file mode 100644 index 000000000..b0ab25881 --- /dev/null +++ b/db/migrate/20150915074650_create_reports.rb @@ -0,0 +1,20 @@ +class CreateReports < ActiveRecord::Migration + def change + create_table :reports do |t| + t.string :name, null: false + t.string :description + t.integer :grouped_by, null: false, default: 0 + + t.integer :project_id, null: false + t.integer :user_id, null: false + + t.timestamps null: false + end + + add_foreign_key :reports, :projects + add_index :reports, :project_id + + add_foreign_key :reports, :users + add_index :reports, :user_id + end +end diff --git a/db/migrate/20150923065605_create_temp_files.rb b/db/migrate/20150923065605_create_temp_files.rb new file mode 100644 index 000000000..771fe259f --- /dev/null +++ b/db/migrate/20150923065605_create_temp_files.rb @@ -0,0 +1,10 @@ +class CreateTempFiles < ActiveRecord::Migration + def change + create_table :temp_files do |t| + t.string :session_id, null: false + + t.timestamps null: false + end + add_attachment :temp_files, :file + end +end diff --git a/db/migrate/20150923110208_add_archive_to_my_modules.rb b/db/migrate/20150923110208_add_archive_to_my_modules.rb new file mode 100644 index 000000000..115c6ba8f --- /dev/null +++ b/db/migrate/20150923110208_add_archive_to_my_modules.rb @@ -0,0 +1,7 @@ +class AddArchiveToMyModules < ActiveRecord::Migration + def change + add_column :my_modules, :archived, :boolean, { default: false, null: false } + add_column :my_modules, :archived_on, :datetime + end +end + diff --git a/db/migrate/20150923154140_add_index_to_sample_name.rb b/db/migrate/20150923154140_add_index_to_sample_name.rb new file mode 100644 index 000000000..1d8965d07 --- /dev/null +++ b/db/migrate/20150923154140_add_index_to_sample_name.rb @@ -0,0 +1,5 @@ +class AddIndexToSampleName < ActiveRecord::Migration + def change + add_index :samples, :name + end +end diff --git a/db/migrate/20150924115001_create_report_elements.rb b/db/migrate/20150924115001_create_report_elements.rb new file mode 100644 index 000000000..6f0cd909a --- /dev/null +++ b/db/migrate/20150924115001_create_report_elements.rb @@ -0,0 +1,44 @@ +class CreateReportElements < ActiveRecord::Migration + def change + create_table :report_elements do |t| + t.integer :position, null: false + t.integer :type_of, null: false + t.integer :sort_order, { default: 0 } # Can be null + + # Each element belongs to report + t.integer :report_id + + # Each element can have parent element + t.references :parent, index: true + + # References to various report entities + t.integer :project_id + t.integer :my_module_id + t.integer :step_id + t.integer :result_id + t.integer :checklist_id + t.integer :asset_id + t.integer :table_id + + t.timestamps null: false + end + + add_foreign_key :report_elements, :reports + add_index :report_elements, :report_id + + add_foreign_key :report_elements, :projects + add_index :report_elements, :project_id + add_foreign_key :report_elements, :my_modules + add_index :report_elements, :my_module_id + add_foreign_key :report_elements, :steps + add_index :report_elements, :step_id + add_foreign_key :report_elements, :results + add_index :report_elements, :result_id + add_foreign_key :report_elements, :checklists + add_index :report_elements, :checklist_id + add_foreign_key :report_elements, :assets + add_index :report_elements, :asset_id + add_foreign_key :report_elements, :tables + add_index :report_elements, :table_id + end +end diff --git a/db/migrate/20150924181017_add_archive_to_results.rb b/db/migrate/20150924181017_add_archive_to_results.rb new file mode 100644 index 000000000..06ef93e38 --- /dev/null +++ b/db/migrate/20150924181017_add_archive_to_results.rb @@ -0,0 +1,6 @@ +class AddArchiveToResults < ActiveRecord::Migration + def change + add_column :results, :archived, :boolean, { default: false, null: false } + add_column :results, :archived_on, :datetime + end +end diff --git a/db/migrate/20151005122041_add_created_by_to_assets.rb b/db/migrate/20151005122041_add_created_by_to_assets.rb new file mode 100644 index 000000000..900e7d6db --- /dev/null +++ b/db/migrate/20151005122041_add_created_by_to_assets.rb @@ -0,0 +1,41 @@ +class AddCreatedByToAssets < ActiveRecord::Migration + def change + tables = [:assets, :checklists, :checklist_items, :my_module_groups, + :my_module_tags, :my_modules, :organizations, :projects, + :sample_groups, :sample_types, :tables, :tags] + + tables.each do |table_name| + add_column table_name, :created_by_id, :integer + add_index table_name, :created_by_id + end + + tables = [:assets, :checklists, :checklist_items, :comments, + :custom_fields, :my_modules, :organizations, :projects, + :reports, :results, :sample_groups, :sample_types, :samples, + :steps, :tables, :tags] + + tables.each do |table_name| + add_column table_name, :last_modified_by_id, :integer + add_index table_name, :last_modified_by_id + end + + tables = [:my_modules, :projects, :results] + + tables.each do |table_name| + add_column table_name, :archived_by_id, :integer + add_index table_name, :archived_by_id + add_column table_name, :restored_by_id, :integer + add_index table_name, :restored_by_id + add_column table_name, :restored_on, :datetime + end + + tables = [:sample_my_modules, :user_my_modules, + :user_organizations, :user_projects] + tables.each do |table_name| + add_column table_name, :assigned_by_id, :integer + add_index table_name, :assigned_by_id + end + + add_column :sample_my_modules, :assigned_on, :datetime + end +end diff --git a/db/migrate/20151021082639_add_pg_trgm_support.rb b/db/migrate/20151021082639_add_pg_trgm_support.rb new file mode 100644 index 000000000..73bebf20c --- /dev/null +++ b/db/migrate/20151021082639_add_pg_trgm_support.rb @@ -0,0 +1,16 @@ +require File.expand_path('app/helpers/database_helper') +include DatabaseHelper + +class AddPgTrgmSupport < ActiveRecord::Migration + def up + if db_adapter_is? "PostgreSQL" then + create_extension :pg_trgm + end + end + + def down + if db_adapter_is? "PostgreSQL" then + drop_extension :pg_trgm + end + end +end diff --git a/db/migrate/20151021085335_add_search_query_indexes.rb b/db/migrate/20151021085335_add_search_query_indexes.rb new file mode 100644 index 000000000..07c43a2e6 --- /dev/null +++ b/db/migrate/20151021085335_add_search_query_indexes.rb @@ -0,0 +1,52 @@ +require File.expand_path('app/helpers/database_helper') +include DatabaseHelper + +class AddSearchQueryIndexes < ActiveRecord::Migration + def up + add_index :projects, :organization_id + add_index :user_organizations, :user_id + add_index :user_organizations, :organization_id + add_index :user_projects, :user_id + add_index :user_projects, :project_id + add_index :tags, :project_id + + # Add GIST trigram indexes onto columns that check for + # ILIKE %pattern% during search + if db_adapter_is? "PostgreSQL" then + add_gist_index :projects, :name + add_gist_index :my_modules, :name + add_gist_index :my_module_groups, :name + add_gist_index :tags, :name + add_gist_index :steps, :name + add_gist_index :results, :name + add_gist_index :assets, :file_file_name + + # There's already semi-useless BTree index on samples + remove_index :samples, :name + add_gist_index :samples, :name + end + end + + def down + remove_index :projects, :organization_id + remove_index :user_organizations, :user_id + remove_index :user_organizations, :organization_id + remove_index :user_projects, :user_id + remove_index :user_projects, :project_id + remove_index :tags, :project_id + + if db_adapter_is? "PostgreSQL" then + remove_index :projects, :name + remove_index :my_modules, :name + remove_index :my_module_groups, :name + remove_index :tags, :name + remove_index :steps, :name + remove_index :results, :name + remove_index :assets, :file_file_name + + # Re-add semi-useless BTree index on samples + remove_index :samples, :name + add_index :samples, :name + end + end +end diff --git a/db/migrate/20151022123530_remove_unique_organization_name_index.rb b/db/migrate/20151022123530_remove_unique_organization_name_index.rb new file mode 100644 index 000000000..96e84e3f4 --- /dev/null +++ b/db/migrate/20151022123530_remove_unique_organization_name_index.rb @@ -0,0 +1,11 @@ +class RemoveUniqueOrganizationNameIndex < ActiveRecord::Migration + def up + remove_index :organizations, :name + add_index :organizations, :name + end + + def down + remove_index :organizations, :name + add_index :organizations, :name, unique: true + end +end diff --git a/db/migrate/20151028091615_add_counter_cache_to_samples.rb b/db/migrate/20151028091615_add_counter_cache_to_samples.rb new file mode 100644 index 000000000..209ed057c --- /dev/null +++ b/db/migrate/20151028091615_add_counter_cache_to_samples.rb @@ -0,0 +1,20 @@ +class AddCounterCacheToSamples < ActiveRecord::Migration + def up + add_column :samples, :nr_of_modules_assigned_to, :integer, :default => 0 + add_column :my_modules, :nr_of_assigned_samples, :integer, :default => 0 + + # Okay, now initialize the values + Sample.find_each do |sample| + Sample.reset_counters(sample.id, :sample_my_modules) + end + + MyModule.find_each do |my_module| + MyModule.reset_counters(my_module.id, :sample_my_modules) + end + end + + def down + remove_column :samples, :nr_of_modules_assigned_to + remove_column :my_modules, :nr_of_assigned_samples + end +end diff --git a/db/migrate/20151103155048_add_btree_gist_extension.rb b/db/migrate/20151103155048_add_btree_gist_extension.rb new file mode 100644 index 000000000..dd082e86b --- /dev/null +++ b/db/migrate/20151103155048_add_btree_gist_extension.rb @@ -0,0 +1,16 @@ +require File.expand_path('app/helpers/database_helper') +include DatabaseHelper + +class AddBtreeGistExtension < ActiveRecord::Migration + def up + if db_adapter_is? "PostgreSQL" then + create_extension :btree_gist + end + end + + def down + if db_adapter_is? "PostgreSQL" then + drop_extension :btree_gist + end + end +end diff --git a/db/migrate/20151111135802_reset_assigned_samples_counters.rb b/db/migrate/20151111135802_reset_assigned_samples_counters.rb new file mode 100644 index 000000000..2447f26a9 --- /dev/null +++ b/db/migrate/20151111135802_reset_assigned_samples_counters.rb @@ -0,0 +1,11 @@ +class ResetAssignedSamplesCounters < ActiveRecord::Migration + def change + Sample.find_each do |sample| + Sample.reset_counters(sample.id, :sample_my_modules) + end + + MyModule.find_each do |my_module| + MyModule.reset_counters(my_module.id, :sample_my_modules) + end + end +end diff --git a/db/migrate/20151117083839_add_project_reference_to_activity.rb b/db/migrate/20151117083839_add_project_reference_to_activity.rb new file mode 100644 index 000000000..1da4062be --- /dev/null +++ b/db/migrate/20151117083839_add_project_reference_to_activity.rb @@ -0,0 +1,34 @@ +class AddProjectReferenceToActivity < ActiveRecord::Migration + def up + # Make my module reference nullable + change_column_null :activities, :my_module_id, true + + # Add reference to project + add_reference :activities, :project, index: true + add_foreign_key :activities, :projects + + # Update existing entries so they all have project reference + Activity.all.each do |activity| + if activity.present? and + activity.my_module.present? and + activity.my_module.project.present? then + activity.project = activity.my_module.project + activity.save + end + end + + # Make project reference non-nullable + change_column_null :activities, :project_id, false + end + + def down + # Unfortunately, all activities that are bound to project + # need to be deleted since they're not "compatible" in the previous + # version of the DB + Activity.destroy_all(my_module: nil) + + remove_foreign_key :activities, :projects + remove_reference :activities, :project, index: true + change_column_null :activities, :my_module_id, false + end +end diff --git a/db/migrate/20151119141714_create_asset_text_data.rb b/db/migrate/20151119141714_create_asset_text_data.rb new file mode 100644 index 000000000..9ea3b592b --- /dev/null +++ b/db/migrate/20151119141714_create_asset_text_data.rb @@ -0,0 +1,11 @@ +class CreateAssetTextData < ActiveRecord::Migration + def change + create_table :asset_text_data do |t| + t.text :data, null: false + t.integer :asset_id, null: false + t.timestamps null: false + end + add_foreign_key :asset_text_data, :assets + add_index :asset_text_data, :asset_id, unique: true + end +end diff --git a/db/migrate/20151130160157_add_text_search_vector_to_asset_text_data.rb b/db/migrate/20151130160157_add_text_search_vector_to_asset_text_data.rb new file mode 100644 index 000000000..7a9a24f3a --- /dev/null +++ b/db/migrate/20151130160157_add_text_search_vector_to_asset_text_data.rb @@ -0,0 +1,12 @@ +require File.expand_path('app/helpers/database_helper') +include DatabaseHelper + +class AddTextSearchVectorToAssetTextData < ActiveRecord::Migration + def change + add_column :asset_text_data, :data_vector, :tsvector + + if db_adapter_is? "PostgreSQL" then + add_index :asset_text_data, :data_vector, using: "gin" + end + end +end diff --git a/db/migrate/20151203100514_add_workflow_order_to_my_modules.rb b/db/migrate/20151203100514_add_workflow_order_to_my_modules.rb new file mode 100644 index 000000000..701a24710 --- /dev/null +++ b/db/migrate/20151203100514_add_workflow_order_to_my_modules.rb @@ -0,0 +1,9 @@ +class AddWorkflowOrderToMyModules < ActiveRecord::Migration + def up + add_column :my_modules, :workflow_order, :integer, { null: false, default: -1} + end + + def down + remove_column :my_modules, :workflow_order + end +end diff --git a/db/migrate/20151207151820_add_timezone_to_user.rb b/db/migrate/20151207151820_add_timezone_to_user.rb new file mode 100644 index 000000000..abc92216a --- /dev/null +++ b/db/migrate/20151207151820_add_timezone_to_user.rb @@ -0,0 +1,5 @@ +class AddTimezoneToUser < ActiveRecord::Migration + def change + add_column :users, :time_zone, :string, :default => "UTC" + end +end diff --git a/db/migrate/20151214110800_add_organization_management_support.rb b/db/migrate/20151214110800_add_organization_management_support.rb new file mode 100644 index 000000000..360d162a1 --- /dev/null +++ b/db/migrate/20151214110800_add_organization_management_support.rb @@ -0,0 +1,36 @@ +require File.expand_path('app/helpers/database_helper') +include DatabaseHelper + +class AddOrganizationManagementSupport < ActiveRecord::Migration + def up + # Add nullable description to organization + add_column :organizations, :description, :string + + # Add estimated file size to asset (in B) + add_column :assets, :estimated_size, :integer, + default: 0 + change_column_null :assets, :estimated_size, false + + # Add space taken to organization (in B!) + add_column :organizations, :space_taken, :integer, + limit: 5, default: MINIMAL_ORGANIZATION_SPACE_TAKEN + change_column_null :organizations, :space_taken, false + + # Add reference to private user + add_column :organizations, :private_user_id, :integer + add_index :organizations, :private_user_id + add_foreign_key :organizations, :users, column: :private_user_id + end + + def down + remove_column :organizations, :description + + remove_column :assets, :estimated_size + + remove_column :organizations, :space_taken + + remove_foreign_key :organizations, column: :private_user_id + remove_index :organizations, :private_user_id + remove_column :organizations, :private_user_id + end +end diff --git a/db/migrate/20151215103642_add_foreign_keys_to_tables.rb b/db/migrate/20151215103642_add_foreign_keys_to_tables.rb new file mode 100644 index 000000000..c22d7922c --- /dev/null +++ b/db/migrate/20151215103642_add_foreign_keys_to_tables.rb @@ -0,0 +1,34 @@ +class AddForeignKeysToTables < ActiveRecord::Migration + def change + tables = [:assets, :checklists, :checklist_items, :my_module_groups, + :my_module_tags, :my_modules, :organizations, :projects, + :sample_groups, :sample_types, :tables, :tags] + + tables.each do |table_name| + add_foreign_key table_name, :users, column: :created_by_id + end + + tables = [:assets, :checklists, :checklist_items, :comments, + :custom_fields, :my_modules, :organizations, :projects, + :reports, :results, :sample_groups, :sample_types, :samples, + :steps, :tables, :tags] + + tables.each do |table_name| + add_foreign_key table_name, :users, column: :last_modified_by_id + end + + tables = [:my_modules, :projects, :results] + + tables.each do |table_name| + add_foreign_key table_name, :users, column: :archived_by_id + add_foreign_key table_name, :users, column: :restored_by_id + end + + tables = [:sample_my_modules, :user_my_modules, + :user_organizations, :user_projects] + tables.each do |table_name| + add_foreign_key table_name, :users, column: :assigned_by_id + end + end + +end diff --git a/db/migrate/20151215134147_add_text_search_vector_to_tables.rb b/db/migrate/20151215134147_add_text_search_vector_to_tables.rb new file mode 100644 index 000000000..39d91b6b7 --- /dev/null +++ b/db/migrate/20151215134147_add_text_search_vector_to_tables.rb @@ -0,0 +1,12 @@ +require File.expand_path('app/helpers/database_helper') +include DatabaseHelper + +class AddTextSearchVectorToTables < ActiveRecord::Migration + def change + add_column :tables, :data_vector, :tsvector + + if db_adapter_is? "PostgreSQL" then + add_index :tables, :data_vector, using: "gin" + end + end +end diff --git a/db/migrate/20151216095259_generate_text_search_vector_for_table_contents.rb b/db/migrate/20151216095259_generate_text_search_vector_for_table_contents.rb new file mode 100644 index 000000000..d733008c0 --- /dev/null +++ b/db/migrate/20151216095259_generate_text_search_vector_for_table_contents.rb @@ -0,0 +1,14 @@ +require File.expand_path('app/helpers/database_helper') +include DatabaseHelper + +class GenerateTextSearchVectorForTableContents < ActiveRecord::Migration + def up + if db_adapter_is? "PostgreSQL" then + execute <<-SQL + UPDATE tables + SET data_vector = + to_tsvector(substring(encode(contents::bytea, 'escape'), 9)) + SQL + end + end +end diff --git a/db/migrate/20160114155705_create_delayed_jobs.rb b/db/migrate/20160114155705_create_delayed_jobs.rb new file mode 100644 index 000000000..471f95e2d --- /dev/null +++ b/db/migrate/20160114155705_create_delayed_jobs.rb @@ -0,0 +1,23 @@ +class CreateDelayedJobs < ActiveRecord::Migration + def self.up + create_table :delayed_jobs, force: true do |table| + table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue + table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually. + table.text :handler, null: false # YAML-encoded string of the object that will do work + table.text :last_error # reason for last failure (See Note below) + table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future. + table.datetime :locked_at # Set when a client is working on this object + table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead) + table.string :locked_by # Who is working on this object (if locked) + table.string :queue # The name of the queue this job is in + table.timestamps null: true + end + + add_index :delayed_jobs, [:priority, :run_at], name: "delayed_jobs_priority" + add_index :delayed_jobs, [:queue], :name => "delayed_jobs_queue" + end + + def self.down + drop_table :delayed_jobs + end +end diff --git a/db/migrate/20160118114850_remove_private_organizations.rb b/db/migrate/20160118114850_remove_private_organizations.rb new file mode 100644 index 000000000..90761d6cf --- /dev/null +++ b/db/migrate/20160118114850_remove_private_organizations.rb @@ -0,0 +1,13 @@ +class RemovePrivateOrganizations < ActiveRecord::Migration + def up + remove_foreign_key :organizations, column: :private_user_id + remove_index :organizations, :private_user_id + remove_column :organizations, :private_user_id + end + + def down + add_column :organizations, :private_user_id, :integer + add_index :organizations, :private_user_id + add_foreign_key :organizations, :users, column: :private_user_id + end +end diff --git a/db/migrate/20160119101947_add_position_to_checklist_item.rb b/db/migrate/20160119101947_add_position_to_checklist_item.rb new file mode 100644 index 000000000..63a7a2a14 --- /dev/null +++ b/db/migrate/20160119101947_add_position_to_checklist_item.rb @@ -0,0 +1,18 @@ +class AddPositionToChecklistItem < ActiveRecord::Migration + def change + add_column :checklist_items, :position, :integer, { default: 0, null: false } + + Checklist.transaction do + Checklist.all.each do |checklist| + pos = 0 + + checklist.checklist_items.each do |item| + item.position = pos + pos += 1 + end + + checklist.save! + end + end + end +end diff --git a/db/migrate/20160125200130_devise_invitable_add_to_users.rb b/db/migrate/20160125200130_devise_invitable_add_to_users.rb new file mode 100644 index 000000000..788529ab3 --- /dev/null +++ b/db/migrate/20160125200130_devise_invitable_add_to_users.rb @@ -0,0 +1,23 @@ +class DeviseInvitableAddToUsers < ActiveRecord::Migration + def up + change_table :users do |t| + t.string :invitation_token + t.datetime :invitation_created_at + t.datetime :invitation_sent_at + t.datetime :invitation_accepted_at + t.integer :invitation_limit + t.references :invited_by, polymorphic: true + t.integer :invitations_count, default: 0 + t.index :invitations_count + t.index :invitation_token, unique: true # for invitable + t.index :invited_by_id + end + end + + def down + change_table :users do |t| + t.remove_references :invited_by, polymorphic: true + t.remove :invitations_count, :invitation_limit, :invitation_sent_at, :invitation_accepted_at, :invitation_token, :invitation_created_at + end + end +end diff --git a/db/migrate/20160125205500_add_empty_field_to_asset.rb b/db/migrate/20160125205500_add_empty_field_to_asset.rb new file mode 100644 index 000000000..0a6a96dec --- /dev/null +++ b/db/migrate/20160125205500_add_empty_field_to_asset.rb @@ -0,0 +1,11 @@ +class AddEmptyFieldToAsset < ActiveRecord::Migration + def up + add_column :assets, :file_present, :boolean, default: false + Asset.update_all(file_present: true) + change_column_null :assets, :file_present, false + end + + def down + remove_column :assets, :file_present + end +end \ No newline at end of file diff --git a/db/migrate/20160201085344_add_tutorial_status_field_to_user.rb b/db/migrate/20160201085344_add_tutorial_status_field_to_user.rb new file mode 100644 index 000000000..5fd99a9fb --- /dev/null +++ b/db/migrate/20160201085344_add_tutorial_status_field_to_user.rb @@ -0,0 +1,14 @@ +class AddTutorialStatusFieldToUser < ActiveRecord::Migration + def up + add_column :users, :tutorial_status, :integer, default: 0 + + # We assume all present users already ran the intro tutorial + User.update_all(tutorial_status: 1) + + change_column_null :users, :tutorial_status, false + end + + def down + remove_column :users, :tutorial_status + end +end \ No newline at end of file diff --git a/db/migrate/20160205192344_migrate_organizations_structure.rb b/db/migrate/20160205192344_migrate_organizations_structure.rb new file mode 100644 index 000000000..2d5ff79c8 --- /dev/null +++ b/db/migrate/20160205192344_migrate_organizations_structure.rb @@ -0,0 +1,44 @@ +class MigrateOrganizationsStructure < ActiveRecord::Migration + def up + # Update estimated size of all assets + Asset.includes(:asset_text_datum).find_each do |asset| + asset.update_estimated_size + asset.update(file_present: true) + end + + # Calculate organizations' taken space + Organization.find_each do |org| + org.calculate_space_taken + org.save + end + + # Finally, the trickiest task: Re-define organizations + demo_org = Organization.find_by(name: "Demo organization") + if demo_org + demo_org.user_organizations.each do |uo| + uo.destroy + end + end + Organization.find_each do |org| + user = org.users.first + org.update(created_by: user) + end + + UserOrganization.find_each do |uo| + uo.update(role: 2) + end + end + + def down + # We cannot re-assign users to demo organization or re-update + # their previous user-organization roles + + # But we can remove created_by field from organizations + Organization.find_each do |org| + org.update(created_by: nil) + end + + # Resetting calculated assets & organizations' space + # to 0 doesn't make much sense even when downgrading migration + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 000000000..95c3ff86a --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,677 @@ +# encoding: UTF-8 +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# Note that this schema.rb definition is the authoritative source for your +# database schema. If you need to create the application database on another +# system, you should be using db:schema:load, not running all the migrations +# from scratch. The latter is a flawed and unsustainable approach (the more migrations +# you'll amass, the slower it'll run and the greater likelihood for issues). +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 20160205192344) do + + # These are extensions that must be enabled in order to support this database + enable_extension "plpgsql" + enable_extension "pg_trgm" + enable_extension "btree_gist" + + create_table "activities", force: :cascade do |t| + t.integer "my_module_id" + t.integer "user_id" + t.integer "type_of", null: false + t.string "message", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "project_id", null: false + end + + add_index "activities", ["created_at"], name: "index_activities_on_created_at", using: :btree + add_index "activities", ["my_module_id"], name: "index_activities_on_my_module_id", using: :btree + add_index "activities", ["project_id"], name: "index_activities_on_project_id", using: :btree + add_index "activities", ["type_of"], name: "index_activities_on_type_of", using: :btree + add_index "activities", ["user_id"], name: "index_activities_on_user_id", using: :btree + + create_table "asset_text_data", force: :cascade do |t| + t.text "data", null: false + t.integer "asset_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.tsvector "data_vector" + end + + add_index "asset_text_data", ["asset_id"], name: "index_asset_text_data_on_asset_id", unique: true, using: :btree + add_index "asset_text_data", ["data_vector"], name: "index_asset_text_data_on_data_vector", using: :gin + + create_table "assets", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "file_file_name" + t.string "file_content_type" + t.integer "file_file_size" + t.datetime "file_updated_at" + t.integer "created_by_id" + t.integer "last_modified_by_id" + t.integer "estimated_size", default: 0, null: false + t.boolean "file_present", default: false, null: false + end + + add_index "assets", ["created_at"], name: "index_assets_on_created_at", using: :btree + add_index "assets", ["created_by_id"], name: "index_assets_on_created_by_id", using: :btree + add_index "assets", ["file_file_name"], name: "index_assets_on_file_file_name", using: :gist + add_index "assets", ["last_modified_by_id"], name: "index_assets_on_last_modified_by_id", using: :btree + + create_table "checklist_items", force: :cascade do |t| + t.string "text", null: false + t.boolean "checked", default: false, null: false + t.integer "checklist_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "created_by_id" + t.integer "last_modified_by_id" + t.integer "position", default: 0, null: false + end + + add_index "checklist_items", ["checklist_id"], name: "index_checklist_items_on_checklist_id", using: :btree + add_index "checklist_items", ["created_by_id"], name: "index_checklist_items_on_created_by_id", using: :btree + add_index "checklist_items", ["last_modified_by_id"], name: "index_checklist_items_on_last_modified_by_id", using: :btree + + create_table "checklists", force: :cascade do |t| + t.string "name", null: false + t.integer "step_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "created_by_id" + t.integer "last_modified_by_id" + end + + add_index "checklists", ["created_by_id"], name: "index_checklists_on_created_by_id", using: :btree + add_index "checklists", ["last_modified_by_id"], name: "index_checklists_on_last_modified_by_id", using: :btree + + create_table "comments", force: :cascade do |t| + t.string "message", null: false + t.integer "user_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "last_modified_by_id" + end + + add_index "comments", ["created_at"], name: "index_comments_on_created_at", using: :btree + add_index "comments", ["last_modified_by_id"], name: "index_comments_on_last_modified_by_id", using: :btree + add_index "comments", ["user_id"], name: "index_comments_on_user_id", using: :btree + + create_table "connections", force: :cascade do |t| + t.integer "input_id", null: false + t.integer "output_id", null: false + end + + create_table "custom_fields", force: :cascade do |t| + t.string "name", null: false + t.integer "user_id", null: false + t.integer "organization_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "last_modified_by_id" + end + + add_index "custom_fields", ["last_modified_by_id"], name: "index_custom_fields_on_last_modified_by_id", using: :btree + add_index "custom_fields", ["organization_id"], name: "index_custom_fields_on_organization_id", using: :btree + add_index "custom_fields", ["user_id"], name: "index_custom_fields_on_user_id", using: :btree + + create_table "delayed_jobs", force: :cascade do |t| + t.integer "priority", default: 0, null: false + t.integer "attempts", default: 0, null: false + t.text "handler", null: false + t.text "last_error" + t.datetime "run_at" + t.datetime "locked_at" + t.datetime "failed_at" + t.string "locked_by" + t.string "queue" + t.datetime "created_at" + t.datetime "updated_at" + end + + add_index "delayed_jobs", ["priority", "run_at"], name: "delayed_jobs_priority", using: :btree + add_index "delayed_jobs", ["queue"], name: "delayed_jobs_queue", using: :btree + + create_table "logs", force: :cascade do |t| + t.integer "organization_id", null: false + t.string "message", null: false + end + + create_table "my_module_comments", force: :cascade do |t| + t.integer "my_module_id", null: false + t.integer "comment_id", null: false + end + + add_index "my_module_comments", ["my_module_id", "comment_id"], name: "index_my_module_comments_on_my_module_id_and_comment_id", using: :btree + + create_table "my_module_groups", force: :cascade do |t| + t.string "name", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "project_id", null: false + t.integer "created_by_id" + end + + add_index "my_module_groups", ["created_by_id"], name: "index_my_module_groups_on_created_by_id", using: :btree + add_index "my_module_groups", ["name"], name: "index_my_module_groups_on_name", using: :gist + add_index "my_module_groups", ["project_id"], name: "index_my_module_groups_on_project_id", using: :btree + + create_table "my_module_tags", force: :cascade do |t| + t.integer "my_module_id" + t.integer "tag_id" + t.integer "created_by_id" + end + + add_index "my_module_tags", ["created_by_id"], name: "index_my_module_tags_on_created_by_id", using: :btree + add_index "my_module_tags", ["my_module_id"], name: "index_my_module_tags_on_my_module_id", using: :btree + add_index "my_module_tags", ["tag_id"], name: "index_my_module_tags_on_tag_id", using: :btree + + create_table "my_modules", force: :cascade do |t| + t.string "name", null: false + t.datetime "due_date" + t.string "description" + t.integer "x", default: 0, null: false + t.integer "y", default: 0, null: false + t.integer "project_id", null: false + t.integer "my_module_group_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "archived", default: false, null: false + t.datetime "archived_on" + t.integer "created_by_id" + t.integer "last_modified_by_id" + t.integer "archived_by_id" + t.integer "restored_by_id" + t.datetime "restored_on" + t.integer "nr_of_assigned_samples", default: 0 + t.integer "workflow_order", default: -1, null: false + end + + add_index "my_modules", ["archived_by_id"], name: "index_my_modules_on_archived_by_id", using: :btree + add_index "my_modules", ["created_by_id"], name: "index_my_modules_on_created_by_id", using: :btree + add_index "my_modules", ["last_modified_by_id"], name: "index_my_modules_on_last_modified_by_id", using: :btree + add_index "my_modules", ["my_module_group_id"], name: "index_my_modules_on_my_module_group_id", using: :btree + add_index "my_modules", ["name"], name: "index_my_modules_on_name", using: :gist + add_index "my_modules", ["project_id"], name: "index_my_modules_on_project_id", using: :btree + add_index "my_modules", ["restored_by_id"], name: "index_my_modules_on_restored_by_id", using: :btree + + create_table "organizations", force: :cascade do |t| + t.string "name", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "created_by_id" + t.integer "last_modified_by_id" + t.string "description" + t.integer "space_taken", limit: 8, default: 1048576, null: false + end + + add_index "organizations", ["created_by_id"], name: "index_organizations_on_created_by_id", using: :btree + add_index "organizations", ["last_modified_by_id"], name: "index_organizations_on_last_modified_by_id", using: :btree + add_index "organizations", ["name"], name: "index_organizations_on_name", using: :btree + + create_table "project_comments", force: :cascade do |t| + t.integer "project_id", null: false + t.integer "comment_id", null: false + end + + add_index "project_comments", ["project_id", "comment_id"], name: "index_project_comments_on_project_id_and_comment_id", using: :btree + + create_table "projects", force: :cascade do |t| + t.string "name", null: false + t.integer "visibility", default: 0, null: false + t.datetime "due_date" + t.integer "organization_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "archived", default: false, null: false + t.datetime "archived_on" + t.integer "created_by_id" + t.integer "last_modified_by_id" + t.integer "archived_by_id" + t.integer "restored_by_id" + t.datetime "restored_on" + end + + add_index "projects", ["archived_by_id"], name: "index_projects_on_archived_by_id", using: :btree + add_index "projects", ["created_by_id"], name: "index_projects_on_created_by_id", using: :btree + add_index "projects", ["last_modified_by_id"], name: "index_projects_on_last_modified_by_id", using: :btree + add_index "projects", ["name"], name: "index_projects_on_name", using: :gist + add_index "projects", ["organization_id"], name: "index_projects_on_organization_id", using: :btree + add_index "projects", ["restored_by_id"], name: "index_projects_on_restored_by_id", using: :btree + + create_table "report_elements", force: :cascade do |t| + t.integer "position", null: false + t.integer "type_of", null: false + t.integer "sort_order", default: 0 + t.integer "report_id" + t.integer "parent_id" + t.integer "project_id" + t.integer "my_module_id" + t.integer "step_id" + t.integer "result_id" + t.integer "checklist_id" + t.integer "asset_id" + t.integer "table_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "report_elements", ["asset_id"], name: "index_report_elements_on_asset_id", using: :btree + add_index "report_elements", ["checklist_id"], name: "index_report_elements_on_checklist_id", using: :btree + add_index "report_elements", ["my_module_id"], name: "index_report_elements_on_my_module_id", using: :btree + add_index "report_elements", ["parent_id"], name: "index_report_elements_on_parent_id", using: :btree + add_index "report_elements", ["project_id"], name: "index_report_elements_on_project_id", using: :btree + add_index "report_elements", ["report_id"], name: "index_report_elements_on_report_id", using: :btree + add_index "report_elements", ["result_id"], name: "index_report_elements_on_result_id", using: :btree + add_index "report_elements", ["step_id"], name: "index_report_elements_on_step_id", using: :btree + add_index "report_elements", ["table_id"], name: "index_report_elements_on_table_id", using: :btree + + create_table "reports", force: :cascade do |t| + t.string "name", null: false + t.string "description" + t.integer "grouped_by", default: 0, null: false + t.integer "project_id", null: false + t.integer "user_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "last_modified_by_id" + end + + add_index "reports", ["last_modified_by_id"], name: "index_reports_on_last_modified_by_id", using: :btree + add_index "reports", ["project_id"], name: "index_reports_on_project_id", using: :btree + add_index "reports", ["user_id"], name: "index_reports_on_user_id", using: :btree + + create_table "result_assets", force: :cascade do |t| + t.integer "result_id", null: false + t.integer "asset_id", null: false + end + + add_index "result_assets", ["result_id", "asset_id"], name: "index_result_assets_on_result_id_and_asset_id", using: :btree + + create_table "result_comments", force: :cascade do |t| + t.integer "result_id", null: false + t.integer "comment_id", null: false + end + + add_index "result_comments", ["result_id", "comment_id"], name: "index_result_comments_on_result_id_and_comment_id", using: :btree + + create_table "result_tables", force: :cascade do |t| + t.integer "result_id", null: false + t.integer "table_id", null: false + end + + add_index "result_tables", ["result_id", "table_id"], name: "index_result_tables_on_result_id_and_table_id", using: :btree + + create_table "result_texts", force: :cascade do |t| + t.string "text", null: false + t.integer "result_id", null: false + end + + add_index "result_texts", ["result_id"], name: "index_result_texts_on_result_id", using: :btree + + create_table "results", force: :cascade do |t| + t.string "name" + t.integer "my_module_id", null: false + t.integer "user_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.boolean "archived", default: false, null: false + t.datetime "archived_on" + t.integer "last_modified_by_id" + t.integer "archived_by_id" + t.integer "restored_by_id" + t.datetime "restored_on" + end + + add_index "results", ["archived_by_id"], name: "index_results_on_archived_by_id", using: :btree + add_index "results", ["created_at"], name: "index_results_on_created_at", using: :btree + add_index "results", ["last_modified_by_id"], name: "index_results_on_last_modified_by_id", using: :btree + add_index "results", ["my_module_id"], name: "index_results_on_my_module_id", using: :btree + add_index "results", ["name"], name: "index_results_on_name", using: :gist + add_index "results", ["restored_by_id"], name: "index_results_on_restored_by_id", using: :btree + add_index "results", ["user_id"], name: "index_results_on_user_id", using: :btree + + create_table "sample_comments", force: :cascade do |t| + t.integer "sample_id", null: false + t.integer "comment_id", null: false + end + + add_index "sample_comments", ["sample_id", "comment_id"], name: "index_sample_comments_on_sample_id_and_comment_id", using: :btree + + create_table "sample_custom_fields", force: :cascade do |t| + t.string "value", null: false + t.integer "custom_field_id", null: false + t.integer "sample_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + end + + add_index "sample_custom_fields", ["custom_field_id"], name: "index_sample_custom_fields_on_custom_field_id", using: :btree + add_index "sample_custom_fields", ["sample_id"], name: "index_sample_custom_fields_on_sample_id", using: :btree + + create_table "sample_groups", force: :cascade do |t| + t.string "name", null: false + t.string "color", default: "#ff0000", null: false + t.integer "organization_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "created_by_id" + t.integer "last_modified_by_id" + end + + add_index "sample_groups", ["created_by_id"], name: "index_sample_groups_on_created_by_id", using: :btree + add_index "sample_groups", ["last_modified_by_id"], name: "index_sample_groups_on_last_modified_by_id", using: :btree + add_index "sample_groups", ["organization_id"], name: "index_sample_groups_on_organization_id", using: :btree + + create_table "sample_my_modules", force: :cascade do |t| + t.integer "sample_id", null: false + t.integer "my_module_id", null: false + t.integer "assigned_by_id" + t.datetime "assigned_on" + end + + add_index "sample_my_modules", ["assigned_by_id"], name: "index_sample_my_modules_on_assigned_by_id", using: :btree + add_index "sample_my_modules", ["sample_id", "my_module_id"], name: "index_sample_my_modules_on_sample_id_and_my_module_id", using: :btree + + create_table "sample_types", force: :cascade do |t| + t.string "name", null: false + t.integer "organization_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "created_by_id" + t.integer "last_modified_by_id" + end + + add_index "sample_types", ["created_by_id"], name: "index_sample_types_on_created_by_id", using: :btree + add_index "sample_types", ["last_modified_by_id"], name: "index_sample_types_on_last_modified_by_id", using: :btree + add_index "sample_types", ["organization_id"], name: "index_sample_types_on_organization_id", using: :btree + + create_table "samples", force: :cascade do |t| + t.string "name", null: false + t.integer "user_id", null: false + t.integer "organization_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "sample_group_id" + t.integer "sample_type_id" + t.integer "last_modified_by_id" + t.integer "nr_of_modules_assigned_to", default: 0 + end + + add_index "samples", ["last_modified_by_id"], name: "index_samples_on_last_modified_by_id", using: :btree + add_index "samples", ["name"], name: "index_samples_on_name", using: :gist + add_index "samples", ["organization_id"], name: "index_samples_on_organization_id", using: :btree + add_index "samples", ["sample_group_id"], name: "index_samples_on_sample_group_id", using: :btree + add_index "samples", ["sample_type_id"], name: "index_samples_on_sample_type_id", using: :btree + add_index "samples", ["user_id"], name: "index_samples_on_user_id", using: :btree + + create_table "step_assets", force: :cascade do |t| + t.integer "step_id", null: false + t.integer "asset_id", null: false + end + + add_index "step_assets", ["step_id", "asset_id"], name: "index_step_assets_on_step_id_and_asset_id", using: :btree + + create_table "step_comments", force: :cascade do |t| + t.integer "step_id", null: false + t.integer "comment_id", null: false + end + + add_index "step_comments", ["step_id", "comment_id"], name: "index_step_comments_on_step_id_and_comment_id", using: :btree + + create_table "step_tables", force: :cascade do |t| + t.integer "step_id", null: false + t.integer "table_id", null: false + end + + add_index "step_tables", ["step_id", "table_id"], name: "index_step_tables_on_step_id_and_table_id", unique: true, using: :btree + + create_table "steps", force: :cascade do |t| + t.string "name" + t.string "description" + t.integer "position", null: false + t.boolean "completed", null: false + t.datetime "completed_on" + t.integer "user_id", null: false + t.integer "my_module_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "last_modified_by_id" + end + + add_index "steps", ["created_at"], name: "index_steps_on_created_at", using: :btree + add_index "steps", ["last_modified_by_id"], name: "index_steps_on_last_modified_by_id", using: :btree + add_index "steps", ["my_module_id"], name: "index_steps_on_my_module_id", using: :btree + add_index "steps", ["name"], name: "index_steps_on_name", using: :gist + add_index "steps", ["position"], name: "index_steps_on_position", using: :btree + add_index "steps", ["user_id"], name: "index_steps_on_user_id", using: :btree + + create_table "tables", force: :cascade do |t| + t.binary "contents", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "created_by_id" + t.integer "last_modified_by_id" + t.tsvector "data_vector" + end + + add_index "tables", ["created_at"], name: "index_tables_on_created_at", using: :btree + add_index "tables", ["created_by_id"], name: "index_tables_on_created_by_id", using: :btree + add_index "tables", ["data_vector"], name: "index_tables_on_data_vector", using: :gin + add_index "tables", ["last_modified_by_id"], name: "index_tables_on_last_modified_by_id", using: :btree + + create_table "tags", force: :cascade do |t| + t.string "name", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "color", default: "#ff0000", null: false + t.integer "project_id", null: false + t.integer "created_by_id" + t.integer "last_modified_by_id" + end + + add_index "tags", ["created_by_id"], name: "index_tags_on_created_by_id", using: :btree + add_index "tags", ["last_modified_by_id"], name: "index_tags_on_last_modified_by_id", using: :btree + add_index "tags", ["name"], name: "index_tags_on_name", using: :gist + add_index "tags", ["project_id"], name: "index_tags_on_project_id", using: :btree + + create_table "temp_files", force: :cascade do |t| + t.string "session_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "file_file_name" + t.string "file_content_type" + t.integer "file_file_size" + t.datetime "file_updated_at" + end + + create_table "user_my_modules", force: :cascade do |t| + t.integer "user_id", null: false + t.integer "my_module_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "assigned_by_id" + end + + add_index "user_my_modules", ["assigned_by_id"], name: "index_user_my_modules_on_assigned_by_id", using: :btree + add_index "user_my_modules", ["my_module_id"], name: "index_user_my_modules_on_my_module_id", using: :btree + add_index "user_my_modules", ["user_id"], name: "index_user_my_modules_on_user_id", using: :btree + + create_table "user_organizations", force: :cascade do |t| + t.integer "role", default: 1, null: false + t.integer "user_id", null: false + t.integer "organization_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "assigned_by_id" + end + + add_index "user_organizations", ["assigned_by_id"], name: "index_user_organizations_on_assigned_by_id", using: :btree + add_index "user_organizations", ["organization_id"], name: "index_user_organizations_on_organization_id", using: :btree + add_index "user_organizations", ["user_id"], name: "index_user_organizations_on_user_id", using: :btree + + create_table "user_projects", force: :cascade do |t| + t.integer "role", default: 0 + t.integer "user_id", null: false + t.integer "project_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "assigned_by_id" + end + + add_index "user_projects", ["assigned_by_id"], name: "index_user_projects_on_assigned_by_id", using: :btree + add_index "user_projects", ["project_id"], name: "index_user_projects_on_project_id", using: :btree + add_index "user_projects", ["user_id"], name: "index_user_projects_on_user_id", using: :btree + + create_table "users", force: :cascade do |t| + t.string "full_name", null: false + t.string "initials", null: false + t.string "email", default: "", null: false + t.string "encrypted_password", default: "", null: false + t.string "reset_password_token" + t.datetime "reset_password_sent_at" + t.datetime "remember_created_at" + t.integer "sign_in_count", default: 0, null: false + t.datetime "current_sign_in_at" + t.datetime "last_sign_in_at" + t.string "current_sign_in_ip" + t.string "last_sign_in_ip" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "avatar_file_name" + t.string "avatar_content_type" + t.integer "avatar_file_size" + t.datetime "avatar_updated_at" + t.string "confirmation_token" + t.datetime "confirmed_at" + t.datetime "confirmation_sent_at" + t.string "unconfirmed_email" + t.string "time_zone", default: "UTC" + t.string "invitation_token" + t.datetime "invitation_created_at" + t.datetime "invitation_sent_at" + t.datetime "invitation_accepted_at" + t.integer "invitation_limit" + t.integer "invited_by_id" + t.string "invited_by_type" + t.integer "invitations_count", default: 0 + t.integer "tutorial_status", default: 0, null: false + end + + add_index "users", ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true, using: :btree + add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree + add_index "users", ["invitation_token"], name: "index_users_on_invitation_token", unique: true, using: :btree + add_index "users", ["invitations_count"], name: "index_users_on_invitations_count", using: :btree + add_index "users", ["invited_by_id"], name: "index_users_on_invited_by_id", using: :btree + add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree + + add_foreign_key "activities", "my_modules" + add_foreign_key "activities", "projects" + add_foreign_key "activities", "users" + add_foreign_key "asset_text_data", "assets" + add_foreign_key "assets", "users", column: "created_by_id" + add_foreign_key "assets", "users", column: "last_modified_by_id" + add_foreign_key "checklist_items", "checklists" + add_foreign_key "checklist_items", "users", column: "created_by_id" + add_foreign_key "checklist_items", "users", column: "last_modified_by_id" + add_foreign_key "checklists", "steps" + add_foreign_key "checklists", "users", column: "created_by_id" + add_foreign_key "checklists", "users", column: "last_modified_by_id" + add_foreign_key "comments", "users" + add_foreign_key "comments", "users", column: "last_modified_by_id" + add_foreign_key "connections", "my_modules", column: "input_id" + add_foreign_key "connections", "my_modules", column: "output_id" + add_foreign_key "custom_fields", "organizations" + add_foreign_key "custom_fields", "users" + add_foreign_key "custom_fields", "users", column: "last_modified_by_id" + add_foreign_key "logs", "organizations" + add_foreign_key "my_module_comments", "comments" + add_foreign_key "my_module_comments", "my_modules" + add_foreign_key "my_module_groups", "projects" + add_foreign_key "my_module_groups", "users", column: "created_by_id" + add_foreign_key "my_module_tags", "users", column: "created_by_id" + add_foreign_key "my_modules", "my_module_groups" + add_foreign_key "my_modules", "projects" + add_foreign_key "my_modules", "users", column: "archived_by_id" + add_foreign_key "my_modules", "users", column: "created_by_id" + add_foreign_key "my_modules", "users", column: "last_modified_by_id" + add_foreign_key "my_modules", "users", column: "restored_by_id" + add_foreign_key "organizations", "users", column: "created_by_id" + add_foreign_key "organizations", "users", column: "last_modified_by_id" + add_foreign_key "project_comments", "comments" + add_foreign_key "project_comments", "projects" + add_foreign_key "projects", "organizations" + add_foreign_key "projects", "users", column: "archived_by_id" + add_foreign_key "projects", "users", column: "created_by_id" + add_foreign_key "projects", "users", column: "last_modified_by_id" + add_foreign_key "projects", "users", column: "restored_by_id" + add_foreign_key "report_elements", "assets" + add_foreign_key "report_elements", "checklists" + add_foreign_key "report_elements", "my_modules" + add_foreign_key "report_elements", "projects" + add_foreign_key "report_elements", "reports" + add_foreign_key "report_elements", "results" + add_foreign_key "report_elements", "steps" + add_foreign_key "report_elements", "tables" + add_foreign_key "reports", "projects" + add_foreign_key "reports", "users" + add_foreign_key "reports", "users", column: "last_modified_by_id" + add_foreign_key "result_assets", "assets" + add_foreign_key "result_assets", "results" + add_foreign_key "result_comments", "comments" + add_foreign_key "result_comments", "results" + add_foreign_key "result_tables", "results" + add_foreign_key "result_tables", "tables" + add_foreign_key "result_texts", "results" + add_foreign_key "results", "my_modules" + add_foreign_key "results", "users" + add_foreign_key "results", "users", column: "archived_by_id" + add_foreign_key "results", "users", column: "last_modified_by_id" + add_foreign_key "results", "users", column: "restored_by_id" + add_foreign_key "sample_comments", "comments" + add_foreign_key "sample_comments", "samples" + add_foreign_key "sample_custom_fields", "custom_fields" + add_foreign_key "sample_custom_fields", "samples" + add_foreign_key "sample_groups", "organizations" + add_foreign_key "sample_groups", "users", column: "created_by_id" + add_foreign_key "sample_groups", "users", column: "last_modified_by_id" + add_foreign_key "sample_my_modules", "my_modules" + add_foreign_key "sample_my_modules", "samples" + add_foreign_key "sample_my_modules", "users", column: "assigned_by_id" + add_foreign_key "sample_types", "organizations" + add_foreign_key "sample_types", "users", column: "created_by_id" + add_foreign_key "sample_types", "users", column: "last_modified_by_id" + add_foreign_key "samples", "organizations" + add_foreign_key "samples", "sample_groups" + add_foreign_key "samples", "sample_types" + add_foreign_key "samples", "users" + add_foreign_key "samples", "users", column: "last_modified_by_id" + add_foreign_key "step_assets", "assets" + add_foreign_key "step_assets", "steps" + add_foreign_key "step_comments", "comments" + add_foreign_key "step_comments", "steps" + add_foreign_key "step_tables", "steps" + add_foreign_key "step_tables", "tables" + add_foreign_key "steps", "my_modules" + add_foreign_key "steps", "users" + add_foreign_key "steps", "users", column: "last_modified_by_id" + add_foreign_key "tables", "users", column: "created_by_id" + add_foreign_key "tables", "users", column: "last_modified_by_id" + add_foreign_key "tags", "projects" + add_foreign_key "tags", "users", column: "created_by_id" + add_foreign_key "tags", "users", column: "last_modified_by_id" + add_foreign_key "user_my_modules", "my_modules" + add_foreign_key "user_my_modules", "users" + add_foreign_key "user_my_modules", "users", column: "assigned_by_id" + add_foreign_key "user_organizations", "organizations" + add_foreign_key "user_organizations", "users" + add_foreign_key "user_organizations", "users", column: "assigned_by_id" + add_foreign_key "user_projects", "projects" + add_foreign_key "user_projects", "users" + add_foreign_key "user_projects", "users", column: "assigned_by_id" +end diff --git a/db/seeds.rb b/db/seeds.rb new file mode 100644 index 000000000..81739c4f6 --- /dev/null +++ b/db/seeds.rb @@ -0,0 +1,12 @@ +include UsersGenerator + +# Create admin user +admin_password = "inHisHouseAtRlyehDeadCthulhuWaitsDreaming" +create_user( + "Admin", + "admin@scinote.net", + admin_password, + true, + DEFAULT_PRIVATE_ORG_NAME, + [] +) \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..709414b8e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,20 @@ + +dbdata: + image: busybox + volumes: + - /var/lib/postgresql + command: "true" + +db: + image: postgres:9.4 + volumes_from: + - dbdata + +web: + build: . + ports: + - "3000:3000" + volumes: + - .:/usr/src/app + links: + - db diff --git a/lib/assets/.keep b/lib/assets/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/lib/tasks/.keep b/lib/tasks/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/lib/tasks/data.rake b/lib/tasks/data.rake new file mode 100644 index 000000000..36e93a049 --- /dev/null +++ b/lib/tasks/data.rake @@ -0,0 +1,59 @@ +namespace :data do + Rails.logger = Logger.new(STDOUT) + + def confirm(message) + puts "\n#{message}? (Y/n)" + res = $stdin.gets.to_s.downcase + unless res.in?(["y", "n", "\n"]) then + puts "Invalid answer, enter option Y or n." + confirm(message) + end + res.in?(["y", "\n"]) + end + + desc "Remove expired temporary files" + task clean_temp_files: :environment do + if not confirm "Remove expired temporary files" + next + end + Rails.logger.info "Cleaning temporary files older than 3 days" + TempFile.where("created_at < ?", 3.days.ago).each do |tmp_file| + + TempFile.transaction do + begin + tmp_file.destroy! + rescue Exception => e + Rails.logger.error "Failed to destroy temporary file #{tmp_file.id}: #{e}" + raise ActiveRecord::Rollback + else + Rails.logger.info "Temporary file ##{tmp_file.id} removed" + end + end + end + end + + desc "Remove unconfirmed user accounts" + task clean_unconfirmed_users: :environment do + if not confirm "Remove unconfirmed user accounts" + next + end + Rails.logger.info "Cleaning unconfirmed users older than 3 days" + User.where("confirmed_at = ? and created_at < ?", nil, 3.days.ago).each do |user| + + User.transaction do + begin + user.destroy! + rescue Exception => e + Rails.logger.error "Failed to destroy unconfirmed user #{user.id}: #{e}" + raise ActiveRecord::Rollback + else + Rails.logger.info "Unconfirmed user ##{user.id} removed" + end + end + end + end + + desc "Remove temporary and obsolete data" + task clean: [:environment, :clean_temp_files, :clean_unconfirmed_users] + +end diff --git a/lib/tasks/db_fake_data.rake b/lib/tasks/db_fake_data.rake new file mode 100644 index 000000000..5217501e5 --- /dev/null +++ b/lib/tasks/db_fake_data.rake @@ -0,0 +1,1116 @@ +require "#{Rails.root}/app/utilities/users_generator" +require "#{Rails.root}/test/helpers/fake_test_helper" +include UsersGenerator +include FakeTestHelper + +namespace :db do + + NR_ORGANIZATIONS = 4 + NR_USERS = 100 + NR_SAMPLE_TYPES = 20 + NR_SAMPLE_GROUPS = 20 + NR_CUSTOM_FIELDS = 20 + NR_SAMPLES = 100 + NR_PROJECTS = 5 + NR_MODULE_GROUPS = 4 + NR_MODULES = 12 + NR_STEPS = 6 + NR_RESULTS = 8 + NR_REPORTS = 4 + NR_COMMENTS = 10 + RATIO_USER_ORGANIZATIONS = 0.5 + NR_MAX_USER_ORGANIZATIONS = 20 + RATIO_CUSTOM_FIELDS = 0.7 + RATIO_SAMPLE_CUSTOM_FIELDS = 0.6 + NR_MAX_USER_PROJECTS = 30 + RATIO_USER_PROJECTS = 0.5 + RATIO_COMMENTS = 0.7 + NR_MAX_USER_MODULES = 30 + RATIO_USER_MODULES = 0.5 + NR_MAX_SAMPLE_MODULES = 100 + RATIO_SAMPLE_MODULES = 0.7 + RATIO_MODULE_MODULE_GROUPS = 0.8 + RATIO_EDGES = 0.7 + RATIO_STEP_COMPLETED = 0.5 + NR_MAX_STEP_ATTACHMENTS = 5 + RATIO_STEP_ATTACHMENTS = 0.2 + NR_MAX_CHECKLIST_ITEMS = 20 + RATIO_CHECKLIST_ITEM = 0.2 + RATIO_CHECKLIST_ITEM_CHECKED = 0.5 + RATIO_RESULT_ARCHIVED = 0.2 + RATIO_REPORT_ELEMENTS = 0.75 + + THRESHOLD_ARCHIVED = 0.2 + THRESHOLD_RESTORED = 0.9 + + MIN_FILE_SIZE = 0.01 + MAX_FILE_SIZE = 0.3 + + desc "Drops the database, sets it up and inserts fake data for " + + "the current RAILS_ENV. WARNING: THIS WILL ERASE ALL " + + "CURRENT DATA IN THE DATABASE." + task :fake => :environment do + Rake::Task["db:drop"].reenable + Rake::Task["db:drop"].invoke + Rake::Task["db:setup"].reenable + Rake::Task["db:setup"].invoke + Rake::Task["db:fake:generate"].reenable + Rake::Task["db:fake:generate"].invoke + end + + namespace :fake do + desc "Generates fake data & inserts it into database for the " + + "current RAILS_ENV." + task :generate => :environment do + require 'rgl/base' + require 'rgl/adjacency' + require 'rgl/topsort' + + puts "Verbose? (Y/n)" + res = $stdin.gets.to_s.downcase.strip + unless res.in?(["y", "n"]) then + puts "Invalid parameter, exiting" + return + end + verbose = res == "y" + + puts "Simple seeding? (Y/n)" + res = $stdin.gets.to_s.downcase.strip + unless res.in?(["y", "n"]) then + puts "Invalid parameter, exiting" + return + end + simple = res == "y" + + if simple + puts "Choose the size of generated dataset(T - tiny, " + + "S - small, M - medium, L - large, H -huge)" + res = $stdin.gets.to_s.downcase.strip + unless res.in?(["t", "s", "m", "l", "h"]) then + puts "Invalid parameter, exiting" + return + end + + case res + when "t" + factor = 0.5 + when "s" + factor = 1 + when "m" + factor = 5 + when "l" + factor = 20 + when "h" + factor = 100 + end + + nr_org = NR_ORGANIZATIONS * factor + nr_users = NR_USERS * factor + nr_sample_types = NR_SAMPLE_TYPES * factor + nr_sample_groups = NR_SAMPLE_GROUPS * factor + nr_custom_fields = NR_CUSTOM_FIELDS * factor + nr_samples = NR_SAMPLES * factor + nr_projects = NR_PROJECTS * factor + nr_module_groups = NR_MODULE_GROUPS * factor + nr_modules = NR_MODULES * factor + nr_steps = NR_STEPS * factor + nr_results = NR_RESULTS * factor + nr_reports = NR_REPORTS * factor + nr_comments = NR_COMMENTS * factor + else + puts "Type in the number of seeded organizations" + nr_org = $stdin.gets.to_i + puts "Type in the number of seeded users" + nr_users = $stdin.gets.to_i + puts "Type in the number of seeded sample types " + + "for each organization" + nr_sample_types = $stdin.gets.to_i + puts "Type in the number of seeded sample groups for " + + "each organization" + nr_sample_groups = $stdin.gets.to_i + puts "Type in the max. number of seeded custom fields " + + "for each organization" + nr_custom_fields = $stdin.gets.to_i + puts "Type in the number of seeded samples for each organization" + nr_samples = $stdin.gets.to_i + puts "Type in the number of seeded projects for each organization" + nr_projects = $stdin.gets.to_i + puts "Type in the number of seeded workflows for each project" + nr_module_groups = $stdin.gets.to_i + puts "Type in the number of seeded modules for each project" + nr_modules = $stdin.gets.to_i + puts "Type in the number of seeded steps for each module" + nr_steps = $stdin.gets.to_i + puts "Type in the number of seeded results for each module" + nr_results = $stdin.gets.to_i + puts "Type in the number of seeded reports for each project" + nr_reports = $stdin.gets.to_i + puts "Type in the max. number of seeded comments for each " + + "commentable item" + nr_comments = $stdin.gets.to_i + end + + begin + ActiveRecord::Base.transaction do + + puts "Generating fake organizations..." + taken_org_names = [] + for _ in 1..nr_org + begin + name = Faker::University.name + end while name.in? taken_org_names + taken_org_names << name + + Organization.create( + name: name, + description: rand >= 0.7 ? Faker::Lorem.sentence : nil + ) + end + + all_organizations = Organization.all + + puts "Generating fake users..." + taken_emails = [] + for _ in 1..nr_users + begin + if rand >= 0.8 + name = generate_got_name + else + name = Faker::Name.name + end + email_name = name.downcase.remove(".").split(" ").join(".") + password = Faker::Internet.password(10, 20) + email = Faker::Internet.free_email(email_name) + end while email.in? taken_emails + taken_emails << email + + user = create_user( + name, + email, + password, + true, + nil, + [] + ) + user.update( + confirmed_at: Faker::Date.backward(30), + ) + if verbose then + puts " Generated user #{name} (email: #{email}, " + + "password: #{password})" + end + + # Randomly assign user to organizations + taken_org_ids = [] + for _ in 1..[NR_MAX_USER_ORGANIZATIONS, all_organizations.count].min + if rand <= RATIO_USER_ORGANIZATIONS then + begin + org = pluck_random(all_organizations) + end while org.id.in? taken_org_ids + taken_org_ids << org.id + + UserOrganization.create( + user: user, + organization: org, + role: rand(0..2) + ) + end + end + end + + puts "Generating fake sample types..." + all_organizations.find_each do |org| + for _ in 1..nr_sample_types + SampleType.create( + name: Faker::Commerce.department(4), + organization: org + ) + end + end + + puts "Generating fake sample groups..." + all_organizations.find_each do |org| + for _ in 1..nr_sample_groups + SampleGroup.create( + name: Faker::Commerce.color, + organization: org, + color: generate_color + ) + end + end + + puts "Generating fake custom fields..." + all_organizations.find_each do |org| + for _ in 1..nr_custom_fields + if rand <= RATIO_CUSTOM_FIELDS then + CustomField.create( + name: Faker::Team.state, + organization: org, + user: pluck_random(org.users) + ) + end + end + end + + puts "Generating fake samples..." + all_organizations.find_each do |org| + for _ in 1..nr_samples + sample = Sample.create( + name: Faker::Book.title, + organization: org, + user: pluck_random(org.users), + sample_type: pluck_random(org.sample_types), + sample_group: pluck_random(org.sample_groups) + ) + + # Add some custom fields to sample + org.custom_fields.find_each do |cf| + if rand <= RATIO_SAMPLE_CUSTOM_FIELDS then + SampleCustomField.create( + sample: sample, + custom_field: cf, + value: Faker::Team.state + ) + end + end + end + end + + puts "Generating fake projects..." + all_organizations.find_each do |org| + taken_project_names = [] + for _ in 1..nr_projects + begin + name = Faker::Company.name[0..29] + end while name.in? taken_project_names + taken_project_names << name + + author = pluck_random(org.users) + created_at = Faker::Time.backward(500) + last_modified_by = pluck_random(org.users) + archived_by = pluck_random(org.users) + archived_on = Faker::Time.between(created_at, DateTime.now) + restored_by = pluck_random(org.users) + restored_on = Faker::Time.between(archived_on, DateTime.now) + status = random_status + + project = Project.create( + visibility: rand(0..1), + name: name, + due_date: nil, + organization: org, + created_by: author, + created_at: created_at, + last_modified_by: last_modified_by, + archived: status == :archived, + archived_by: status.in?([:archived, :restored]) ? + archived_by : nil, + archived_on: status.in?([:archived, :restored]) ? + archived_on : nil, + restored_by: status == :restored ? restored_by : nil, + restored_on: status == :restored ? restored_on : nil + ) + # Automatically assign project author onto project + UserProject.create( + user: author, + project: project, + role: 0, + created_at: created_at + ) + + # Activities + Activity.create( + type_of: :create_project, + user: author, + project: project, + message: I18n.t( + "activities.create_project", + user: author.full_name, + project: project.name + ), + created_at: created_at + ) + if status.in?([:archived, :restored]) then + Activity.create( + type_of: :archive_project, + user: archived_by, + project: project, + message: I18n.t( + "activities.archive_project", + user: archived_by.full_name, + project: project.name + ), + created_at: archived_on + ) + end + if status == :restored then + Activity.create( + type_of: :restore_project, + user: restored_by, + project: project, + message: I18n.t( + "activities.restore_project", + user: restored_by.full_name, + project: project.name + ), + created_at: restored_on + ) + end + + # Assign users onto the project + taken_user_ids = [] + for _ in 2..[NR_MAX_USER_PROJECTS, org.users.count].min + if rand <= RATIO_USER_PROJECTS + begin + user = pluck_random(org.users) + end while user.id.in? taken_user_ids + taken_user_ids << user.id + + assigned_on = Faker::Time.backward(500) + assigned_by = pluck_random(project.users) + up = UserProject.create( + user: user, + project: project, + role: rand(0..3), + created_at: assigned_on, + assigned_by: assigned_by + ) + Activity.create( + type_of: :assign_user_to_project, + user: assigned_by, + project: project, + message: I18n.t( + "activities.assign_user_to_project", + assigned_user: user.full_name, + role: up.role_str, + project: project.name, + assigned_by_user: assigned_by.full_name + ), + created_at: assigned_on + ) + end + end + + # Add some comments + for _ in 1..nr_comments + if rand <= RATIO_COMMENTS + project.comments << Comment.create( + user: pluck_random(project.users), + message: Faker::Hipster.sentence, + created_at: Faker::Time.backward(500) + ) + end + end + end + end + + puts "Generating fake workflows..." + Project.find_each do |project| + for _ in 1..nr_module_groups + MyModuleGroup.create( + name: Faker::Hacker.noun, + project: project + ) + end + end + + puts "Generating fake modules..." + total_projects = Project.count + Project.find_each.with_index do |project, i| + if verbose then + puts " Generating modules for project #{project.name} " + + "(#{i + 1} of #{total_projects})..." + end + taken_pos = [] + for _ in 1..nr_modules + begin + x = rand(0..nr_modules) + y = rand(0..nr_modules) + end while [x, y].in? taken_pos + taken_pos << [x, y] + + status = random_status + author = pluck_random(org.users) + created_at = Faker::Time.backward(500) + archived_by = pluck_random(org.users) + archived_on = Faker::Time.between(created_at, DateTime.now) + restored_by = pluck_random(org.users) + restored_on = Faker::Time.between(archived_on, DateTime.now) + + my_module = MyModule.create( + name: Faker::Hacker.verb, + created_by: author, + created_at: created_at, + due_date: rand <= 0.5 ? + Faker::Time.forward(500) : nil, + description: rand <= 0.5 ? + Faker::Hacker.say_something_smart : nil, + x: x, + y: y, + project: project, + my_module_group: status == :archived ? + nil : + (rand <= RATIO_MODULE_MODULE_GROUPS ? + pluck_random(project.my_module_groups) : nil + ), + archived: status == :archived, + archived_on: status.in?([:archived, :restored]) ? + archived_on : nil, + archived_by: status.in?([:archived, :restored]) ? + archived_by : nil, + restored_on: status == :restored ? restored_on : nil, + restored_by: status == :restored ? restored_by : nil + ) + + # Activities + Activity.create( + type_of: :create_module, + user: author, + project: my_module.project, + my_module: my_module, + message: I18n.t( + "activities.create_module", + user: author.full_name, + module: my_module.name + ), + created_at: created_at + ) + if status.in?([:archived, :restored]) then + Activity.create( + type_of: :archive_module, + user: archived_by, + project: my_module.project, + my_module: my_module, + message: I18n.t( + "activities.archive_module", + user: archived_by.full_name, + module: my_module.name + ), + created_at: archived_on + ) + end + if status == :restored then + Activity.create( + type_of: :restore_module, + user: restored_by, + project: my_module.project, + my_module: my_module, + message: I18n.t( + "activities.restore_module", + user: restored_by.full_name, + module: my_module.name + ), + created_at: restored_on + ) + end + + if verbose then + puts " Generated module #{my_module.name}" + end + + # Assign some users onto module + taken_user_ids = [] + for _ in 1..[NR_MAX_USER_MODULES, project.users.count].min + if rand <= RATIO_USER_MODULES then + begin + user = pluck_random(project.users) + end while user.id.in? taken_user_ids + taken_user_ids << user.id + + assigned_on = Faker::Time.backward(500) + assigned_by = pluck_random(my_module.project.users) + UserMyModule.create( + user: user, + my_module: my_module, + assigned_by: pluck_random(project.users), + created_at: assigned_on + ) + Activity.create( + type_of: :assign_user_to_module, + user: assigned_by, + project: my_module.project, + my_module: my_module, + message: I18n.t( + "activities.assign_user_to_module", + assigned_user: user.full_name, + module: my_module.name, + assigned_by_user: assigned_by.full_name + ), + created_at: assigned_on + ) + end + end + + # Assign some samples onto module + taken_sample_ids = [] + for _ in 1..[ + NR_MAX_SAMPLE_MODULES, + project.organization.samples.count + ].min + if rand <= RATIO_SAMPLE_MODULES then + begin + sample = pluck_random(project.organization.samples) + end while sample.id.in? taken_sample_ids + taken_sample_ids << sample.id + + SampleMyModule.create( + sample: sample, + my_module: my_module + ) + end + end + + # Add some comments + for _ in 1..nr_comments + if rand <= RATIO_COMMENTS + my_module.comments << Comment.create( + user: pluck_random(my_module.project.users), + message: Faker::Hipster.sentence, + created_at: Faker::Time.backward(500) + ) + end + end + end + + # Generate some connections between modules + project.my_module_groups.find_each do |my_module_group| + if my_module_group.my_modules.empty? or + my_module_group.my_modules.count == 1 + # If any module group doesn't contain + # any modules (or has only 1 module), remove it + my_module_group.destroy + else + # Make connections between project modules, + # keeping in mind not to generate cycles + n = my_module_group.my_modules.count + max_edges = (n - 1) * n / 2 + + dg = RGL::DirectedAdjacencyGraph.new + for _ in 1..max_edges + if rand <= RATIO_EDGES + begin + m1 = pluck_random(my_module_group.my_modules) + m2 = pluck_random(my_module_group.my_modules) + end while ( + m1 == m2 or + dg.has_edge?(m1.id, m2.id) + ) + + # Only add edge if it won't make graph cyclic + dg.add_edge(m1.id, m2.id) + if dg.acyclic? + Connection.create( + input_id: m1.id, + output_id: m2.id + ) + else + dg.remove_edge(m1.id, m2.id) + end + end + end + + # Set order number for each module in group + topsort = dg.topsort_iterator.to_a + my_module_group.my_modules.each do |mm| + if topsort.include? mm.id + mm.workflow_order = topsort.find_index(mm.id) + mm.save! + end + end + end + end + end + + puts "Generating fake module steps..." + Project.find_each do |project| + project.my_modules.find_each do |my_module| + for i in 1..nr_steps + created_at = Faker::Time.backward(500) + completed = rand <= RATIO_STEP_COMPLETED + completed_on = completed ? + Faker::Time.between(created_at, DateTime.now) : nil + + step = Step.create( + created_at: created_at, + name: Faker::Hacker.ingverb, + description: Faker::Hacker.say_something_smart, + position: i - 1, + completed: completed, + user: pluck_random(my_module.users), + my_module: my_module, + completed_on: completed_on + ) + Activity.create( + type_of: :create_step, + project: project, + my_module: my_module, + user: step.user, + created_at: created_at, + message: I18n.t( + "activities.create_step", + user: step.user.full_name, + step: i, + step_name: step.name + ) + ) + if completed then + Activity.create( + type_of: :complete_step, + project: project, + my_module: my_module, + user: step.user, + created_at: completed_on, + message: I18n.t( + "activities.complete_step", + user: step.user.full_name, + step: i, + step_name: step.name, + completed: i, + all: i + ) + ) + end + + # Add checklists + for _ in 1..NR_MAX_STEP_ATTACHMENTS + if rand <= RATIO_STEP_ATTACHMENTS then + checklist = Checklist.create( + name: Faker::Hacker.noun, + step: step, + created_by: step.user, + ) + + # Add checklist items + for j in 1..NR_MAX_CHECKLIST_ITEMS + if rand <= RATIO_CHECKLIST_ITEM then + checked = rand <= RATIO_CHECKLIST_ITEM_CHECKED + checked_on = Faker::Time.backward(500) + ci = ChecklistItem.create( + created_at: checked_on, + text: Faker::Hipster.sentence, + checklist: checklist, + checked: checked, + created_by: step.user + ) + if checked then + Activity.create( + type_of: :check_step_checklist_item, + project: project, + my_module: my_module, + user: step.user, + created_at: checked_on, + message: I18n.t( + "activities.check_step_checklist_item", + user: step.user.full_name, + checkbox: ci.text, + completed: j, + all: j, + step: i, + step_name: step.name + ) + ) + end + end + end + end + end + + # Add assets + for _ in 1..NR_MAX_STEP_ATTACHMENTS + if rand <= RATIO_STEP_ATTACHMENTS then + asset = Asset.create( + file: generate_file(rand(MIN_FILE_SIZE..MAX_FILE_SIZE)), + created_by: step.user + ) + StepAsset.create( + step: step, + asset: asset + ) + end + end + + # Add tables + for _ in 1..NR_MAX_STEP_ATTACHMENTS + if rand <= RATIO_STEP_ATTACHMENTS then + table = Table.create( + contents: + generate_table_contents(rand(30), rand(150)), + created_by: step.user + ) + StepTable.create( + step: step, + table: table + ) + end + end + + # Add some comments + for _ in 1..nr_comments + if rand <= RATIO_COMMENTS + user = pluck_random(project.users) + created_at = Faker::Time.backward(500) + step.comments << Comment.create( + user: user, + message: Faker::Hipster.sentence, + created_at: created_at + ) + Activity.create( + type_of: :add_comment_to_step, + project: project, + my_module: my_module, + user: user, + created_at: created_at, + message: I18n.t( + "activities.add_comment_to_step", + user: user.full_name, + step: i, + step_name: step.name + ) + ) + end + end + + end + end + end + + puts "Generating fake module results..." + Project.find_each do |project| + project.my_modules.find_each do |my_module| + for _ in 1..nr_results + user = pluck_random(my_module.users) + created_at = Faker::Time.backward(500) + archived_on = Faker::Time.between(created_at, DateTime.now) + restored_on = Faker::Time.between(archived_on, DateTime.now) + status = random_status + + result = Result.new( + name: Faker::Hacker.abbreviation[0..49], + my_module: my_module, + user: user, + created_at: created_at, + archived: status == :archived, + archived_by: status.in?([:archived, :restored]) ? + user : nil, + archived_on: status.in?([:archived, :restored]) ? + archived_on : nil, + restored_by: status == :restored ? user : nil, + restored_on: status == :restored ? restored_on : nil + ) + + type = [:text, :asset, :table][rand(0..2)] + case type + when :text + result.result_text = ResultText.new( + text: Faker::Hipster.paragraph, + ) + str = "activities.add_text_result" + str2 = "activities.archive_text_result" + when :asset + result.asset = Asset.new( + file: generate_file(rand(MIN_FILE_SIZE..MAX_FILE_SIZE)), + created_by: result.user + ) + str = "activities.add_asset_result" + str2 = "activities.archive_asset_result" + when :table + result.table = Table.new( + contents: generate_table_contents(rand(30), rand(150)), + created_by: result.user + ) + str = "activities.add_table_result" + str2 = "activities.archive_table_result" + end + + result.save + + # Add activities + Activity.create( + type_of: :add_result, + project: project, + my_module: my_module, + user: user, + created_at: created_at, + message: I18n.t( + str, + user: user.full_name, + result: result.name + ) + ) + if status.in?([:archived, :restored]) then + Activity.create( + type_of: :archive_result, + user: user, + project: project, + my_module: my_module, + message: I18n.t( + str2, + user: user.full_name, + result: result.name + ), + created_at: archived_on + ) + end + # Currently, there is no way to restore archived results + + # Add some comments + for _ in 1..nr_comments + if rand <= RATIO_COMMENTS + comment_user = pluck_random(result.my_module.project.users) + comment_created_at = Faker::Time.backward(500) + result.comments << Comment.create( + user: comment_user, + message: Faker::Hipster.sentence, + created_at: comment_created_at + ) + Activity.create( + type_of: :add_comment_to_result, + project: project, + my_module: my_module, + user: comment_user, + created_at: comment_created_at, + message: I18n.t( + "activities.add_comment_to_result", + user: user.full_name, + result: result.name + ) + ) + end + end + end + end + end + + puts "Generating fake reports..." + Project.find_each do |project| + for _ in 1..nr_reports + taken_project_names = [] + begin + name = Faker::Company.bs + user = pluck_random(project.users) + end while [user, name].in? taken_project_names + taken_project_names << [user, name] + + report = Report.create( + name: name, + grouped_by: 0, + description: Faker::Hipster.sentence, + project: project, + user: user + ) + + # Generate the oh-so-many report elements + ReportElement.create( + sort_order: 0, + position: 0, + report: report, + type_of: :project_header + ) + project.my_modules.each do |my_module| + if rand <= RATIO_REPORT_ELEMENTS then + re_my_module = ReportElement.create( + sort_order: rand <= 0.5 ? 0 : 1, + position: 0, + report: report, + type_of: :my_module, + my_module: my_module + ) + + my_module.completed_steps.each do |step| + if rand <= RATIO_REPORT_ELEMENTS then + re_step = ReportElement.create( + sort_order: rand <= 0.5 ? 0 : 1, + position: 0, + report: report, + type_of: :step, + step: step, + parent: re_my_module + ) + + step.checklists.each do |checklist| + if rand <= RATIO_REPORT_ELEMENTS then + ReportElement.create( + sort_order: rand <= 0.5 ? 0 : 1, + position: 0, + report: report, + type_of: :step_checklist, + checklist: checklist, + parent: re_step + ) + end + end + step.assets.each do |asset| + if rand <= RATIO_REPORT_ELEMENTS then + ReportElement.create( + sort_order: rand <= 0.5 ? 0 : 1, + position: 0, + report: report, + type_of: :step_asset, + asset: asset, + parent: re_step + ) + end + end + step.tables.each do |table| + if rand <= RATIO_REPORT_ELEMENTS then + ReportElement.create( + sort_order: rand <= 0.5 ? 0 : 1, + position: 0, + report: report, + type_of: :step_table, + table: table, + parent: re_step + ) + end + end + if rand <= RATIO_REPORT_ELEMENTS then + ReportElement.create( + sort_order: rand <= 0.5 ? 0 : 1, + position: 0, + report: report, + type_of: :step_comments, + step: step, + parent: re_step + ) + end + end + end + + my_module.results.each do |result| + if rand <= RATIO_REPORT_ELEMENTS then + if result.is_asset + type_of = :result_asset + elsif result.is_table + type_of = :result_table + else + type_of = :result_text + end + re_result = ReportElement.create( + sort_order: rand <= 0.5 ? 0 : 1, + position: 0, + report: report, + type_of: type_of, + result: result, + parent: re_my_module + ) + + if rand <= RATIO_REPORT_ELEMENTS then + ReportElement.create( + sort_order: rand <= 0.5 ? 0 : 1, + position: 0, + report: report, + type_of: :result_comments, + result: result, + parent: re_result + ) + end + end + end + + if rand <= RATIO_REPORT_ELEMENTS then + ReportElement.create( + sort_order: rand <= 0.5 ? 0 : 1, + position: 0, + report: report, + type_of: :my_module_activity, + my_module: my_module, + parent: re_my_module + ) + end + + if rand <= RATIO_REPORT_ELEMENTS then + ReportElement.create( + sort_order: rand <= 0.5 ? 0 : 1, + position: 0, + report: report, + type_of: :my_module_samples, + my_module: my_module, + parent: re_my_module + ) + end + end + end + + # Shuffle the report + shuffle_report_elements( + report.report_elements.where(parent: nil) + ) + end + end + + end + + # Now, at the end, add additional "private" organization + # to each user + User.find_each do |user| + create_private_user_organization(user, DEFAULT_PRIVATE_ORG_NAME) + end + + # Calculate space taken by each organization; this must + # be done in a separate transaction because the estimated + # asset sizes are calculated in after_commit, which is done + # after the first transaction is completed + ActiveRecord::Base.transaction do + puts "Calculating organization sizes..." + Organization.find_each do |org| + org.calculate_space_taken + org.save + end + end + rescue ActiveRecord::ActiveRecordError, + ArgumentError, ActiveRecord::RecordNotSaved => e + puts "Error seeding fake data, transaction reverted" + puts "Output: #{e.inspect}" + end + end + end + + def shuffle_report_elements(report_elements) + if report_elements.blank? or report_elements.count == 0 + return + end + + header = report_elements.find_by(type_of: :project_header) + if header.present? + header.position = 0 + header.save + i = 1 + else + i = 0 + end + + ids_map = (i..(report_elements.count - 1)).to_a.shuffle + for i in i..(report_elements.count - 1) + re = report_elements[i] + re.position = ids_map[i] + re.save + end + + # Recursively shuffle children + report_elements.each do |re2| + if re2.children.count > 0 + shuffle_report_elements(re2.children) + end + end + end + + # WARNING: This only works on PostgreSQL + def pluck_random(scope) + scope.order("RANDOM()").first + end + + # Randomly determine whether project/module/result is active (0), + # archived (1), or already restored (2) + def random_status + val = rand + status = :active + if val > THRESHOLD_ARCHIVED + if val > THRESHOLD_RESTORED + status = :archived + end + else + status = :restored + end + status + end +end \ No newline at end of file diff --git a/lib/tasks/db_users.rake b/lib/tasks/db_users.rake new file mode 100644 index 000000000..1013c2860 --- /dev/null +++ b/lib/tasks/db_users.rake @@ -0,0 +1,138 @@ +require "#{Rails.root}/app/utilities/users_generator" +include UsersGenerator + +namespace :db do + + desc "Load users into database from the provided YAML file" + task :load_users, [ :file_path, :create_orgs ] => :environment do |task, args| + if args.blank? or args.empty? or args[:file_path].blank? + puts "No file provided" + return + end + + create_orgs = false + if args[:create_orgs].present? and args[:create_orgs].downcase == "true" + create_orgs = true + end + + # Parse file + yaml = YAML.load(File.read("#{args[:file_path]}")) + + begin + ActiveRecord::Base.transaction do + # Parse user & organization hashes from YAML + orgs = yaml.select{ |k, v| /org_[0-9]+/ =~ k } + users = yaml.select{ |k, v| /user_[0-9]+/ =~ k } + + # Create organizations + orgs.each do |k, org_hash| + org = Organization.order(created_at: :desc).where(name: org_hash["name"]).first + if org.blank? + org = Organization.create({ + name: org_hash["name"][0..99] + }) + end + org_hash["id"] = org.id + end + + # Create users + puts "Created users" + users.each do |k, user_hash| + password = user_hash["password"] + if password.blank? + password = generate_user_password + end + + user_orgs = user_hash["organizations"] + if user_orgs.blank? + user_orgs = "" + end + + org_ids = + user_orgs + .split(",") + .collect{ |o| o.strip } + .uniq + .select{ |o| orgs.include? o } + .collect{ |o| orgs[o]["id"] } + + user = create_user( + user_hash["full_name"], + user_hash["email"], + password, + true, + create_orgs ? DEFAULT_PRIVATE_ORG_NAME : nil, + org_ids + ) + + if user.id.present? then + puts "" + print_user(user, password) + end + end + + puts "" + end + rescue ActiveRecord::ActiveRecordError, ArgumentError + puts "Error creating all users, transaction reverted" + end + end + + desc "Add a single user to the database" + task :add_user => :environment do + puts "Type in user's full name (e.g. 'Steve Johnson')" + full_name = $stdin.gets.to_s.strip + puts "Type in user's email (e.g. 'steve.johnson@gmail.com')" + email = $stdin.gets.to_s.strip + puts "Type in user's password (e.g. 'password'), or leave blank to let Rails generate password" + password = $stdin.gets.to_s.strip + if password.empty? + password = generate_user_password + end + puts "Do you want Rails to create default user's organization? (T/F)" + create_org = $stdin.gets.to_s.strip == "T" + puts "Type names of any additional organizations you want the user to be admin of (delimited with ','), or leave blank" + org_names = $stdin.gets.to_s.strip + if org_names.empty? + org_names = [] + else + org_names = org_names.split(",").collect { |n| n.strip } + end + + begin + ActiveRecord::Base.transaction do + # Add/fetch organizations if needed + org_ids = [] + org_names.each do |org_name| + org = Organization.order(created_at: :desc).where(name: org_name).first + if org.blank? then + org = Organization.create({ name: org_name[0..99] }) + end + + org_ids << org.id + end + + user = create_user( + full_name, + email, + password, + true, + create_org ? DEFAULT_PRIVATE_ORG_NAME : nil, + org_ids + ) + + if user.id.present? then + puts "Successfully created user" + puts "" + print_user(user, password) + else + puts "Error creating new user" + end + + puts "" + end + rescue ActiveRecord::ActiveRecordError, ArgumentError, ActiveRecord::RecordNotSaved + puts "Error creating user, transaction reverted" + end + end +end \ No newline at end of file diff --git a/lib/tasks/i18n_missing_keys.rake b/lib/tasks/i18n_missing_keys.rake new file mode 100644 index 000000000..daad76b6e --- /dev/null +++ b/lib/tasks/i18n_missing_keys.rake @@ -0,0 +1,54 @@ +namespace :i18n do + + desc "Find and list translation keys that do not exist in all locales" + task :missing_keys => :environment do + + def collect_keys(scope, translations) + full_keys = [] + translations.to_a.each do |key, translations| + new_scope = scope.dup << key + if translations.is_a?(Hash) + full_keys += collect_keys(new_scope, translations) + else + full_keys << new_scope.join('.') + end + end + return full_keys + end + + # Make sure we've loaded the translations + I18n.backend.send(:init_translations) + puts "#{I18n.available_locales.size} #{I18n.available_locales.size == 1 ? 'locale' : 'locales'} available: #{I18n.available_locales.to_sentence}" + + # Get all keys from all locales + all_keys = I18n.backend.send(:translations).collect do |check_locale, translations| + collect_keys([], translations).sort + end.flatten.uniq + puts "#{all_keys.size} #{all_keys.size == 1 ? 'unique key' : 'unique keys'} found." + + missing_keys = {} + all_keys.each do |key| + + I18n.available_locales.each do |locale| + I18n.locale = locale + begin + result = I18n.translate(key, :raise => true) + rescue I18n::MissingInterpolationArgument + # noop + rescue I18n::MissingTranslationData + if missing_keys[key] + missing_keys[key] << locale + else + missing_keys[key] = [locale] + end + end + end + end + + puts "#{missing_keys.size} #{missing_keys.size == 1 ? 'key is missing' : 'keys are missing'} from one or more locales:" + missing_keys.keys.sort.each do |key| + puts "'#{key}': Missing from #{missing_keys[key].join(', ')}" + end + + end +end \ No newline at end of file diff --git a/lib/tasks/paperclip.rake b/lib/tasks/paperclip.rake new file mode 100644 index 000000000..53d6af864 --- /dev/null +++ b/lib/tasks/paperclip.rake @@ -0,0 +1,31 @@ +namespace :paperclip do + + def trim_url(url) + url.split("?").first + end + + def print_model_files(model, file_attr) + + model.all.each do |a| + file = a.send file_attr + styles = file.options[:styles] + url = file.url + + puts trim_url(url) + + if styles + styles.each do |style, option| + url = file.url style + puts trim_url(url) + end + end + end + end + + desc "List all paperclip files" + task files: :environment do + print_model_files Asset, :file + print_model_files User, :avatar + end + +end diff --git a/lib/tasks/web_stats.rake b/lib/tasks/web_stats.rake new file mode 100644 index 000000000..cc30274cf --- /dev/null +++ b/lib/tasks/web_stats.rake @@ -0,0 +1,65 @@ +namespace :web_stats do + + desc "Report login statistics from the application" + task :login => :environment do + def print_splitter + puts "+--------------------------------------+-----------------+-----------------------+" + end + + def print_header + print_splitter + puts "| Username | Times signed in | Last signed in on |" + print_splitter + end + + def print_line(user) + last_signed_in = "never" + if user.last_sign_in_at.present? + last_signed_in = I18n.l(user.last_sign_in_at, format: :full) + end + puts "| #{user.email.ljust(36)} " \ + "| #{user.sign_in_count.to_s.ljust(15)} " \ + "| #{last_signed_in.ljust(21)} |" + end + + # Actual task code + print_header + User.all.each{ |u| print_line(u) } + print_splitter + + # Calculate total & avg + total = 0 + User.all.each{ |u| total += u.sign_in_count } + avg = total.to_f / User.count.to_f + + puts " Total times signed in: #{total} Avg. times signed in: #{avg.round(4)}" + puts "" + end + + desc "Report information on last login" + task :last_login => :environment do + ll_user = User.where.not(current_sign_in_at: nil).order(current_sign_in_at: :desc).first + ll_user2 = User.where.not(last_sign_in_at: nil).order(last_sign_in_at: :desc).first + + ll = ll_user.present? ? ll_user.current_sign_in_at : nil + ll2 = ll_user2.present? ? ll_user2.last_sign_in_at : nil + if ll.present? and ll2.present? + ll_real = ll > ll2 ? ll : ll2 + elsif ll.present? + ll_real = ll + elsif ll2.present + ll_real = ll2 + else + ll_real = nil + end + + puts "Current time: #{Time.now.to_s}" + if ll_real.present? + diff = ((Time.now - ll_real) / 1.hour).round + puts "Last login at: #{ll_real.to_s}" + puts " (#{diff} hours ago)" + else + puts "Woops, seems nobody has logged in yet!" + end + end +end \ No newline at end of file diff --git a/log/.keep b/log/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/public/403.html b/public/403.html new file mode 100644 index 000000000..d41fdd74c --- /dev/null +++ b/public/403.html @@ -0,0 +1,72 @@ + + + + <%= t ("forbidden.title") %> + + + + + + +
    +
    +

    <%= t("forbidden.title") %>

    +

    <%= t("forbidden.notice") %>

    +
    +

    + <%= t("forbidden.help") %>
    + <%= t("forbidden.go_back") %> + <%= link_to t("forbidden.homepage"), root_path %>, + <%= link_to t("forbidden.try_searching"), new_search_path %> +

    +
    + + diff --git a/public/404.html b/public/404.html new file mode 100644 index 000000000..c80c0cd65 --- /dev/null +++ b/public/404.html @@ -0,0 +1,72 @@ + + + + <%= t("not_found.title") %> + + + + + + +
    +
    +

    <%= t("not_found.title") %>

    +

    <%= t("not_found.notice") %>

    +
    +

    + <%= t("not_found.help") %>
    + <%= t("not_found.go_back") %> + <%= link_to t("not_found.homepage"), root_path %>, + <%= link_to t("not_found.try_searching"), new_search_path %> +

    +
    + + diff --git a/public/422.html b/public/422.html new file mode 100644 index 000000000..a21f82b3b --- /dev/null +++ b/public/422.html @@ -0,0 +1,67 @@ + + + + The change you wanted was rejected (422) + + + + + + +
    +
    +

    The change you wanted was rejected.

    +

    Maybe you tried to change something you didn't have access to.

    +
    +

    If you are the application owner check the logs for more information.

    +
    + + diff --git a/public/500.html b/public/500.html new file mode 100644 index 000000000..061abc587 --- /dev/null +++ b/public/500.html @@ -0,0 +1,66 @@ + + + + We're sorry, but something went wrong (500) + + + + + + +
    +
    +

    We're sorry, but something went wrong.

    +
    +

    If you are the application owner check the logs for more information.

    +
    + + diff --git a/public/favicon-16.png b/public/favicon-16.png new file mode 100644 index 0000000000000000000000000000000000000000..68247c8d5afe39d0fdad4abe0bb0e309792c2233 GIT binary patch literal 3088 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 literal 0 HcmV?d00001 diff --git a/public/favicon-48.png b/public/favicon-48.png new file mode 100644 index 0000000000000000000000000000000000000000..9a33ef56377987091a31a7ca62f8ca41cee7f2cb GIT binary patch literal 3438 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 QVE_OC07*qoM6N<$f=3l}WdHyG literal 0 HcmV?d00001 diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..8f221810ba38457a7621cd93f23ed0272ad34d62 GIT binary patch literal 7406 zcmeHL$!-%t5Uugp@k-(w7o_-@V@@m~$l*^oav=+FXHIbB7YK{QXZRDi#zEjuNNyt$ zrFqrUOZBXhr4RvAN~OB0UiDJ8d#Z*gqL_Af335PRH;L{O(QIb)0nrcGrqfJ+-XnT@ zmuLqVgwQiE$JNpDbdSzHJfO4Fhmc_C%TN2X{P-C13G_#Fa`=K?pB&Sx<0IJj==}2m zoqu^sFOLrErfz+02HFg?8TeBfn2u^`lH13m77b@Epb0)2g$vZ;^}QFnb0w0 zQ)m&|E#SrK#i8BL=9Z*>M^GfAGQ9AX4Pzgn@xgTU0UBQh32knHW_-+%MZB{V2LAy5 zjysggkFyj8IIk~7T6}$e#+;zw8&ZMSi8TKhZ)g}r!S4yt@F4#ccn-_0P{2r=AbUx!~~MXYccukhkek1)h9>UkW3> zPs--UTmeoh8@VN|+La(NM!!%_kpTIjxNpnD{xYl5L{urSaUlGR)Kzw{qMR$WVt<9? ziQ&gz#4;hGH;_#8l@@{AQkV($o0QxaVmDU>$=3z!bLHj2=qn>Hj7*K>v@o7Sv`ma# z;GAsNR4sQ9Hv=DloBCAlLuH?;dsOtNx=%&VYWq{}RolK432mv(K%0Sl2F67aZQiKp zg~fMSrS9?AEdswA+kv#n@j8ARBPGunbsgEKb=N+u;V*xhce7dGw=qI;#UTB1MLoWG zzbR(2{gteL`+jUqlh2#qW~Z}3#x3gR&_C2PBLDe>ey6#@*jZ2m%3{yfkX3%7tOTs~ z8-PDbCtn=u23Pp~TxR5t0hImv6;Nv!vN}T7QjtnfPcXk%sbi(mgyIxcgywB^nKm#A zl}qLbtP=17d;w0m2%=bBQ{OGibqZdw>sqC`PT}|Hc%P1U>HKcp)+1b}wA5yx&A@+_ zfu5u6!ggveyI|cl3%w807d7XoCtf}9vlzNd-8%XP&lhJ;{SIa`^Xoay%zVX5o!p&o rS2lNi`R%UI^Wg&4{0q%tvJd4Rdcp4@_rDSKe-ga|FSgMigE{s$0MvJ@ literal 0 HcmV?d00001 diff --git a/public/images/.keep b/public/images/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/public/images/favicon-16.png b/public/images/favicon-16.png new file mode 100644 index 0000000000000000000000000000000000000000..0ebf0087e33d4ace2a3d2024fc9b19eef58670a7 GIT binary patch literal 3127 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 Rgctw-002ovPDHLkV1lV!_ox5> literal 0 HcmV?d00001 diff --git a/public/images/favicon-32.png b/public/images/favicon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..868ea6dc161328d49fccecfa96b94316dd90c102 GIT binary patch literal 3711 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 literal 0 HcmV?d00001 diff --git a/public/images/favicon-48.png b/public/images/favicon-48.png new file mode 100644 index 0000000000000000000000000000000000000000..0a9b0b8ba5e02cf12c7106259339a192c7a751c1 GIT binary patch literal 4344 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 literal 0 HcmV?d00001 diff --git a/public/images/icon/missing.png b/public/images/icon/missing.png new file mode 100644 index 0000000000000000000000000000000000000000..b13b08127eeba4ca630ebfa06ea6d235358a5fc1 GIT binary patch literal 1859 zcmV-J2fX-+P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qRNAp5A000KENklJ63*7t1Fuho$!Qja}Pb@dbvaH)wdoeKpA33#JH9~1tD z@DQ)q2pFE_Pp$KvGu4Zl#)~iUJwu{X1`|X6SU4nwLw?WZ;{5#5!keIXO9}^Lp#U2M z8v%2#Sf^|rQR|qa!UXz4r?GHA5<>AvSQ3O;`)q4l$4?CgsH3xcd0`JTNfCQB}=YZK=4JiisE^1`0Oko6G#hF+xTS zqmh=13}G~rh^6DP!5Xhx!T0>DS6Nv~NunH$Ft+NO)S5nU1q{1*;azv45t`xyF%&>< zjG9X(Y*rgbs#UY0E#UR$l5vF{4zl@%6Q62Y1ry_|E7)#e*SA4JMpFzHLq@`&=2P

    _{9a~SnQ1j@C1<*XkW4HTG*4T&zW%eUW(!Q~z%*^@n66zl zXY5UhXk>C?s;Q}!>*a^k*wi{cI*x5l4EkBx)iq7dk4I!}(RvQJ$tuF=$igMX9QV0B zQoH_#Y1aZ&;W?Uhc6RslJQ*9C*l@X$QdBK5#EEG}v?`cX zmT%n^ppF?B`v8{Eb~dxOy?waN%eE@#rIz0%J4`_t3Pp$f)+q6MRj z>Nv(6SHWR`nPU~4&t~9BE3E9Rt1sWaefx5C_3@A2lX5bX&YYc|+zn=POp2=t#vE6{ zx~Pi)zdx?TB}q$V`t#x^;U;dTP7wD7bqw;|obs?ojU7cf{V_Ma0vt)BD(NiHO z$BUWtZn03_+dZk2w+neKlF8%oUYwtwpPgB)Gx*uu+QP9SQ&1KKd{am#OZhx{IW8ZZRw_7pwNwaccXXiOALE3;&(V>wlgi=Y zyFCmSY0*tPiu|HMKfuazqU)GAW?)3mz(^YhFs2!sElV4{pf3cNPA2>M2DM?TQBi8z&!vwBD%XQi@~sEKJ@waBq}a|?iJe2MS#sl;}bEk&a^bU7sS?CA*VpLoq> zi|Hi04&V~O?B3+epU2cR#zJaaJJU39+&#d?oFQ`|7%&=pI4T6NC`5|c47?I*3_GQO zRiw@?EUER(q<}F5W*UXZ))fsL7cPOZakgh=*);r&L#i4Z9nU5cs#ivZ0oWkRm)WIP z9|g>eli$QN`L6@oY1qVfT!@6cKD*6+$2|m>g}E2MguIv|Tr_W}{!LvmtExuLM0YW^ zrrBhNe4eE@Zw3d34TkzB5~r4yj^QEG%EAJ^B~*mGUgwGl>A=*ta3>hD4rl&7o%+^r!j#rl!dVBl0j~_&X z{e!OMMetOX4m@Liu&WlQh6cl&eOAlTMQxdy=012p*{wG4RF)1r{qrQ(`T;d#vFO06 xcW82Af@}SNvYE|Vmd^3y`2PyF+^`%g{{X!@mDwIAlDGf>002ovPDHLkV1mb%b6x-d literal 0 HcmV?d00001 diff --git a/public/images/icon_small/missing.png b/public/images/icon_small/missing.png new file mode 100644 index 0000000000000000000000000000000000000000..d931b189f7130ffa91d6ef5ae29c3a7ed93fe890 GIT binary patch literal 1278 zcmVPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02*{fSaefwW^{L9 za%BKeVQFr3E>1;MAa*k@H7+qRNAp5A000DSNklXn*5Jsc@|z^ zib74LP*#gtLEc#32nNSPp~+|4znV(1q!v*F1Lz7|x=VvRX!x~5fESp;%(A9b3|-FU zrlubMFg)h!$^1e|RgQEG1+W5`aOlZCdN&X|UzZEBmaf)Hh9(y#C#St!s-?x{N=dJn zI?7Wzl7Ta_vE9}ruJH%`kV|ni2gGXWTCHqW%jVqN6Cc#o&a<7yQMn4bO6W)?FQ%|h zSiQ%!y>+mR*BAsNwX#vSN|Duw59ihJ$d61s4mv_J6&Y*o#^M+!u3;H#!3(@jv5c5Z zW<>!@_Ob3N5D30Jj9G@B&!)2i=_nX?_j3ppPzTkJvR07xc6Kpmc*IZnn&3ScnivfP zpYQHc<0NsMBL~g1sD_&83Z%TfwH+7>dFOvyqk+)o#%4)T(2MW~m*f!HXc9E`7aNGi zKyW9OBg-_>f{;q`2m1%2C>CWU7E3HFu8fU^DGp|1FJe5$@m$hS6>N;9Y1k*AB^q}Z z&2`ao)ih43mGkCF=knsOH#aQ9Jer<4JdB;5wQg^3KYaMN-ENykNfgsJo%Y+;*H`DQ zvqqzFRKjv;pGX4lMHAPwohv|Bj$HKC);6_iJ01J<>GPM*U;cUjUJ%53wfg6E$41xD zCq1<2L6fYh>!K4#>*Tn7@#^i(wHFP&d-wN$A3s`U3(toKhi^JSC?Cxea5Nwf8T%o#}&%AjvL1n3nw07V%=`@Ij0d%*vWz$x1l3DEc4>*6m%xR z`nXU@*O3^?!$jROEls6C=Mi-Gg0=4u&)sAjSE?oSiPdT}KsR-LWHjjIPEXIAH=E#| z)@w-5J?Jt$==iqv-F#dE8PUxo2S+2<9#)FUvLpqA6aDB}RW3GbRm#QHN-u7hBiB>+ zo8a8n)HrJPE$1e$eH&xUhtLDc6a@zEsop22y!GVEvyMfn(R6u7|^<83t-% z{Ly-JUF2BGl~T^9zy*5H_7y0wiRZgJGmqzdpl@h)cK)}$Jxug+q2329`SL|%C4y_r zH}g9c3Qw#>B78jN0(-94qiep=56~|gKLhG_y}G>O8}|T3BT+ypu;+St>Hg2ky?VMD oae+P8OAAZBaSssC4v>le4>GHSI@GwNp8x;=07*qoM6N<$g1xR&JOBUy literal 0 HcmV?d00001 diff --git a/public/images/logo.png b/public/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..b368cd36d776fc7eba12a74c515b31b7a86fa503 GIT binary patch literal 4652 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̙!xU^+mZKmSf9vp|a9#_)!a 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%0ASfTQ12%X&;-)cyhz0RQR8vr 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 P0DNfyGXHywb?E;AnHO#^ literal 0 HcmV?d00001 diff --git a/public/images/thumb/missing.png b/public/images/thumb/missing.png new file mode 100644 index 0000000000000000000000000000000000000000..cc0b6b9e67571ebaa005e9e25d2d888bf3015cd3 GIT binary patch literal 6943 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 literal 0 HcmV?d00001 diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 000000000..3c9c7c01f --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,5 @@ +# See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file +# +# To ban all spiders from the entire site uncomment the next two lines: +# User-agent: * +# Disallow: / diff --git a/test/controllers/.keep b/test/controllers/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/test/controllers/activities_controller_test.rb b/test/controllers/activities_controller_test.rb new file mode 100644 index 000000000..f9fedcf2f --- /dev/null +++ b/test/controllers/activities_controller_test.rb @@ -0,0 +1,5 @@ +require 'test_helper' + +class ActivitiesControllerTest < ActionController::TestCase + +end diff --git a/test/controllers/custom_fields_controller_test.rb b/test/controllers/custom_fields_controller_test.rb new file mode 100644 index 000000000..02fbebb47 --- /dev/null +++ b/test/controllers/custom_fields_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class CustomFieldsControllerTest < ActionController::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/my_modules_controller_test.rb b/test/controllers/my_modules_controller_test.rb new file mode 100644 index 000000000..78a2877fc --- /dev/null +++ b/test/controllers/my_modules_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class MyModulesControllerTest < ActionController::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/organizations_controller_test.rb b/test/controllers/organizations_controller_test.rb new file mode 100644 index 000000000..555dcfe1e --- /dev/null +++ b/test/controllers/organizations_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class OrganizationsControllerTest < ActionController::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/project_activities_controller_test.rb b/test/controllers/project_activities_controller_test.rb new file mode 100644 index 000000000..955cf95e9 --- /dev/null +++ b/test/controllers/project_activities_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class ProjectActivitiesControllerTest < ActionController::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/projects_controller_test.rb b/test/controllers/projects_controller_test.rb new file mode 100644 index 000000000..c0981663a --- /dev/null +++ b/test/controllers/projects_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class ProjectsControllerTest < ActionController::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/result_assets_controller_test.rb b/test/controllers/result_assets_controller_test.rb new file mode 100644 index 000000000..96b48c7ef --- /dev/null +++ b/test/controllers/result_assets_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class ResultAssetsControllerTest < ActionController::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/result_comments_controller_test.rb b/test/controllers/result_comments_controller_test.rb new file mode 100644 index 000000000..9727d52f1 --- /dev/null +++ b/test/controllers/result_comments_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class ResultCommentsControllerTest < ActionController::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/result_tables_controller_test.rb b/test/controllers/result_tables_controller_test.rb new file mode 100644 index 000000000..4ea0e9658 --- /dev/null +++ b/test/controllers/result_tables_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class ResultTablesControllerTest < ActionController::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/result_texts_controller_test.rb b/test/controllers/result_texts_controller_test.rb new file mode 100644 index 000000000..ea6a42887 --- /dev/null +++ b/test/controllers/result_texts_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class ResultTextsControllerTest < ActionController::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/sample_groups_controller_test.rb b/test/controllers/sample_groups_controller_test.rb new file mode 100644 index 000000000..a68347ffa --- /dev/null +++ b/test/controllers/sample_groups_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class SampleGroupsControllerTest < ActionController::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/sample_types_controller_test.rb b/test/controllers/sample_types_controller_test.rb new file mode 100644 index 000000000..bc20963ba --- /dev/null +++ b/test/controllers/sample_types_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class SampleTypesControllerTest < ActionController::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/samples_controller_test.rb b/test/controllers/samples_controller_test.rb new file mode 100644 index 000000000..ce7d277e8 --- /dev/null +++ b/test/controllers/samples_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class SamplesControllerTest < ActionController::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/search_controller_test.rb b/test/controllers/search_controller_test.rb new file mode 100644 index 000000000..bfbf22d8b --- /dev/null +++ b/test/controllers/search_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class SearchControllerTest < ActionController::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/step_comments_controller_test.rb b/test/controllers/step_comments_controller_test.rb new file mode 100644 index 000000000..f942eb6ae --- /dev/null +++ b/test/controllers/step_comments_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class StepCommentsControllerTest < ActionController::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/steps_controller_test.rb b/test/controllers/steps_controller_test.rb new file mode 100644 index 000000000..21422c062 --- /dev/null +++ b/test/controllers/steps_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class StepsControllerTest < ActionController::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/user_my_modules_controller_test.rb b/test/controllers/user_my_modules_controller_test.rb new file mode 100644 index 000000000..2d84d2c89 --- /dev/null +++ b/test/controllers/user_my_modules_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class UserMyModulesControllerTest < ActionController::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/fixtures/.keep b/test/fixtures/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/test/fixtures/activities.yml b/test/fixtures/activities.yml new file mode 100644 index 000000000..813ced53f --- /dev/null +++ b/test/fixtures/activities.yml @@ -0,0 +1,95 @@ +one: + project: interfaces + my_module: qpcr + user: steve + type_of: assignment + message: User Steve assigned module lalala to user Jlaw. + created_at: 2015-11-01 10:01:01 + +two: + project: interfaces + my_module: qpcr + user: steve + type_of: result + message: User Steve added result to step 2 of module qPCR. + created_at: 2015-11-01 10:02:01 + +three: + project: interfaces + my_module: qpcr + user: steve + type_of: result + message: User Steve added result to step 3 of module qPCR. + created_at: 2015-11-01 10:03:01 + +four: + project: interfaces + my_module: rna_test + user: steve + type_of: assignment + message: User Steve assigned module lalala to user Jlaw. + created_at: 2015-11-01 10:04:01 + +five: + project: interfaces + my_module: rna_test + user: steve + type_of: result + message: User Steve added result to step 2 of module RNA test. + created_at: 2015-11-01 10:05:01 + +six: + project: interfaces + my_module: rna_test + user: steve + type_of: result + message: User Steve added result to step 3 of module RNA test. + created_at: 2015-11-01 10:06:01 + +seven: + project: interfaces + my_module: sample_prep + user: steve + type_of: assignment + message: User Steve assigned module lalala to user Jlaw. + created_at: 2015-11-01 10:07:01 + +eight: + project: interfaces + my_module: sample_prep + user: steve + type_of: result + message: User Steve added result to step 2 of module Sample preparation. + created_at: 2015-11-01 10:08:01 + +nine: + project: interfaces + my_module: sample_prep + user: steve + type_of: result + message: User Steve added result to step 3 of module Sample preparation. + created_at: 2015-11-01 10:09:01 + +ten: + project: interfaces + my_module: list_of_samples + user: steve + type_of: assignment + message: User Steve assigned module lalala to user Jlaw. + created_at: 2015-11-01 10:10:01 + +eleven: + project: interfaces + my_module: list_of_samples + user: steve + type_of: result + message: User Steve added result to step 2 of module List of samples. + created_at: 2015-11-01 10:11:01 + +twelve: + project: interfaces + my_module: list_of_samples + user: steve + type_of: result + message: User Steve added result to step 3 of module List of samples. + created_at: 2015-11-01 10:12:01 diff --git a/test/fixtures/asset_text_datum.yml b/test/fixtures/asset_text_datum.yml new file mode 100644 index 000000000..d05255e55 --- /dev/null +++ b/test/fixtures/asset_text_datum.yml @@ -0,0 +1,15 @@ +one: + asset: one + data: This is text content of asset file #1. + +two: + asset: two + data: This is text content of asset file #2. + +invalid_asset_id: + asset_id: 12321321 + data: This is text content of invalid asset. + +invalid_asset_value: + asset: nil + data: This is text content of nil asset. diff --git a/test/fixtures/assets.yml b/test/fixtures/assets.yml new file mode 100644 index 000000000..1baa3edc6 --- /dev/null +++ b/test/fixtures/assets.yml @@ -0,0 +1,48 @@ +one: + file_file_name: file1.pdf + file_content_type: application/pdf + file_file_size: 15 + estimated_size: <%= (15 * ASSET_ESTIMATED_SIZE_FACTOR).to_i %> + created_by: steve + last_modified_by: steve + +two: + file_file_name: file2.zip + file_content_type: application/zip + file_file_size: 32 + estimated_size: <%= (32 * ASSET_ESTIMATED_SIZE_FACTOR).to_i %> + created_by: mark + last_modified_by: mark + + +three: + file_file_name: file3.jpg + file_content_type: image/jpg + file_file_size: 64 + estimated_size: <%= (64 * ASSET_ESTIMATED_SIZE_FACTOR).to_i %> + created_by: mark + last_modified_by: jlaw + +four: + file_file_name: file4.png + file_content_type: image/png + file_file_size: 128 + estimated_size: <%= (128 * ASSET_ESTIMATED_SIZE_FACTOR).to_i %> + created_by: steve + last_modified_by: jlaw + +test: + file_file_name: file7.png + file_content_type: image/png + file_file_size: 578 + estimated_size: <%= (578 * ASSET_ESTIMATED_SIZE_FACTOR).to_i %> + created_by: steve + last_modified_by: jlaw + +test_result: + file_file_name: file8.png + file_content_type: image/png + file_file_size: 631 + estimated_size: <%= (631 * ASSET_ESTIMATED_SIZE_FACTOR).to_i %> + created_by: steve + last_modified_by: steve diff --git a/test/fixtures/checklist_items.yml b/test/fixtures/checklist_items.yml new file mode 100644 index 000000000..04f45d970 --- /dev/null +++ b/test/fixtures/checklist_items.yml @@ -0,0 +1,56 @@ +one: + text: "Checklist Item 1" + checked: true + checklist: one + created_by: steve + last_modified_by: steve + + +two: + text: "Checklist Item 2" + checked: false + checklist: one + created_by: mark + last_modified_by: mark + +three: + text: "Checklist Item 3" + checked: false + checklist: one + created_by: steve + last_modified_by: mark + +four: + text: "Checklist Item 4" + checked: true + checklist: two + created_by: jlaw + last_modified_by: jlaw + +five: + text: "Checklist Item 5" + checked: false + checklist: two + created_by: steve + last_modified_by: nora + +six: + text: "Checklist Item 6" + checked: true + checklist: two + created_by: mark + last_modified_by: mark + +seven: + text: "Checklist Item 7" + checked: false + checklist: three + created_by: mark + last_modified_by: steve + +eight: + text: "Checklist Item 8" + checked: false + checklist: three + created_by: nora + last_modified_by: steve \ No newline at end of file diff --git a/test/fixtures/checklists.yml b/test/fixtures/checklists.yml new file mode 100644 index 000000000..383e88025 --- /dev/null +++ b/test/fixtures/checklists.yml @@ -0,0 +1,17 @@ +one: + name: "Checklist 1" + step: step1 + created_by: steve + last_modified_by: steve + +two: + name: "Checklist 2" + step: step2 + created_by: steve + last_modified_by: jlaw + +three: + name: "Checklist 3" + step: step3 + created_by: nora + last_modified_by: jlaw \ No newline at end of file diff --git a/test/fixtures/comments.yml b/test/fixtures/comments.yml new file mode 100644 index 000000000..97ccffcd9 --- /dev/null +++ b/test/fixtures/comments.yml @@ -0,0 +1,89 @@ +one: + message: "Good job, JLaw, very comprehensive results!" + user: steve + +two: + message: "This doesn't look so good, please repeat the experiment." + user: steve + +three: + message: "Consider it done!" + user: nora + +four: + message: "I still have to write abstract." + user: mark + +five: + message: "Ok, I will do it tomorrow." + user: mark + +six: + message: "Ok" + user: jlaw + +seven: + message: "Nah, I don't feel like it." + user: jlaw + +eight: + message: "I don't have time now, maybe tomorrow." + user: jlaw + +nine: + message: "This is project comment." + user: mark + +ten: + message: "This is module comment." + user: mark + +eleven: + message: "This is module comment 2." + user: nora + +twelve: + message: "This is project comment 2." + user: nora + +thirteen: + message: "Random comment 1" + user: john + +fourteen: + message: "Random comment 2" + user: john + +fifteen: + message: "Random comment 3" + user: john + +sixteen: + message: "Congrats!" + user: jlaw + +seventeen: + message: "Congrats, aswell!" + user: nora + +test: + message: "This is test message" + user: nora + +<% 25.times do |n| %> +test_step_comment_<%= n %>: + message: "This is test message #<%= n %>" + user: steve + created_at: "2015-11-06 11:<%= n.to_s.rjust(2, '0') %>:00" +<% end %> + +<% 25.times do |n| %> +test_result_comment_<%= n %>: + message: "This is test message #<%= n %>" + user: steve + created_at: "2015-11-06 11:<%= n.to_s.rjust(2, '0') %>:00" +<% end %> + +unassociated: + message: This message is not associated with any entity + user: nora diff --git a/test/fixtures/connections.yml b/test/fixtures/connections.yml new file mode 100644 index 000000000..3e3617449 --- /dev/null +++ b/test/fixtures/connections.yml @@ -0,0 +1,19 @@ +one: + from: list_of_samples + to: sample_prep + +two: + from: sample_prep + to: qpcr + +three: + from: qpcr + to: quantification + +four: + from: list_of_samples + to: rna_test + +five: + from: rna_test + to: quantification diff --git a/test/fixtures/custom_fields.yml b/test/fixtures/custom_fields.yml new file mode 100644 index 000000000..639e9aad0 --- /dev/null +++ b/test/fixtures/custom_fields.yml @@ -0,0 +1,16 @@ +volume: + name: "Volume" + user: steve + organization: biosistemika + +location: + name: "Location" + user: jlaw + organization: biosistemika + +description: + name: "Description" + user: nora + organization: nib + + diff --git a/test/fixtures/logs.yml b/test/fixtures/logs.yml new file mode 100644 index 000000000..404eba3c4 --- /dev/null +++ b/test/fixtures/logs.yml @@ -0,0 +1,8 @@ +one: + organization: biosistemika + message: This is a test log numero uno + +two: + organization: biosistemika + message: This is a test log numero due + diff --git a/test/fixtures/my_module_comments.yml b/test/fixtures/my_module_comments.yml new file mode 100644 index 000000000..7dc5b90a9 --- /dev/null +++ b/test/fixtures/my_module_comments.yml @@ -0,0 +1,23 @@ +one: + my_module: list_of_samples + comment: thirteen + +two: + my_module: sample_prep + comment: fourteen + +three: + my_module: sample_prep + comment: fifteen + +four: + my_module: qpcr + comment: sixteen + +five: + my_module: quantification + comment: seventeen + +test: + my_module: qpcr + comment: eleven diff --git a/test/fixtures/my_module_groups.yml b/test/fixtures/my_module_groups.yml new file mode 100644 index 000000000..29b073675 --- /dev/null +++ b/test/fixtures/my_module_groups.yml @@ -0,0 +1,14 @@ +wf1: + name: Workflow 1 + project: phd + created_by: mark + +ge: + name: Gene Expression + project: interfaces + created_by: steve + +wf2: + name: Workflow 2 + project: interfaces + created_by: jlaw diff --git a/test/fixtures/my_modules.yml b/test/fixtures/my_modules.yml new file mode 100644 index 000000000..dae4ffadc --- /dev/null +++ b/test/fixtures/my_modules.yml @@ -0,0 +1,140 @@ +list_of_samples: + name: List of samples + due_date: 2015-11-10 12:00:00 + description: Listing samples... + x: 0 + y: 0 + workflow_order: 0 + project: interfaces + my_module_group: ge + tags: urgent, nice + created_by: steve + last_modified_by: steve + archived: false + archived_by: + restored_by: + archived_on: + restored_on: + + +sample_prep: + name: Sample preparation + due_date: 12-04-2015 12:45:00 + description: nil + x: 1 + y: 0 + workflow_order: 2 + project: interfaces + my_module_group: ge + created_by: mark + last_modified_by: mark + archived: false + archived_by: steve + restored_by: steve + archived_on: 2012-09-10 16:23:35 + restored_on: 2012-10-05 06:03:33 + +qpcr: + name: qPCR + due_date: 12-04-2015 15:10:00 + description: nil + x: 2 + y: 0 + workflow_order: 3 + project: interfaces + my_module_group: ge + tags: nice + created_by: jlaw + last_modified_by: jlaw + archived: false + archived_by: + restored_by: + archived_on: + restored_on: + +quantification: + name: Quantification + due_date: "04-13-2015 09:45:00" + description: nil + x: 3 + y: 0 + workflow_order: 4 + project: interfaces + my_module_group: ge + tags: todo + created_by: mark + last_modified_by: mark + archived: false + archived_by: + restored_by: + archived_on: + restored_on: + +rna_test: + name: RNA quality test + due_date: 12-04-2015 16:15:00 + description: nil + x: 1 + y: 1 + workflow_order: 1 + project: interfaces + my_module_group: ge + tags: urgent, todo + created_by: steve + last_modified_by: steve + archived: false + archived_by: mark + restored_by: mark + archived_on: 2012-02-09 16:23:35 + restored_on: 2012-12-09 12:33:48 + +custom: + name: Unknown module + due_date: + description: Hodor hodor hodor. + x: 0 + y: 0 + workflow_order: -1 + project: phd + my_module_group: wf1 + created_by: steve + last_modified_by: steve + archived: false + archived_by: + restored_by: + archived_on: + restored_on: + +no_group: + name: No group module + due_date: + description: Module without group + x: 0 + y: 3 + workflow_order: -1 + project: interfaces + my_module_group: wf2 + created_by: nora + last_modified_by: nora + archived: false + archived_by: + restored_by: + archived_on: + restored_on: + +archived: + name: Archived module + due_date: + description: Module in archive + x: 0 + y: 0 + workflow_order: -1 + project: interfaces + my_module_group: + created_by: steve + last_modified_by: steve + archived: true + archived_by: jlaw + restored_by: + archived_on: 2015-11-16 10:25:34 + restored_on: diff --git a/test/fixtures/organizations.yml b/test/fixtures/organizations.yml new file mode 100644 index 000000000..6b01252fe --- /dev/null +++ b/test/fixtures/organizations.yml @@ -0,0 +1,21 @@ +biosistemika: + name: BioSistemika + created_by: steve + description: "The best company in the world!" + +nib: + name: nib + created_by: steve + +phylos: + name: Phylos Bioscience + created_by: steve + +test: + name: Test Organization + description: "Testing organization, you know" + +steve_org: + name: Steve's Organization + description: "Private organization" + created_by: steve \ No newline at end of file diff --git a/test/fixtures/project_comments.yml b/test/fixtures/project_comments.yml new file mode 100644 index 000000000..b3a30a41e --- /dev/null +++ b/test/fixtures/project_comments.yml @@ -0,0 +1,15 @@ +one: + project: interfaces + comment: seven + +two: + project: interfaces + comment: eight + +three: + project: interfaces + comment: nine + +test: + project: interfaces + comment: six diff --git a/test/fixtures/projects.yml b/test/fixtures/projects.yml new file mode 100644 index 000000000..d4ea69cbe --- /dev/null +++ b/test/fixtures/projects.yml @@ -0,0 +1,175 @@ +interfaces: + name: INTERFACES + visibility: 1 + due_date: 2015-11-02 15:30:22 + organization: biosistemika + archived: false + archived_on: + created_at: 2015-11-01 11:37:26 + created_by: steve + last_modified_by: steve + archived_by: + restored_by: + restored_on: + +eurostars: + name: EUROSTARS + visibility: 1 + due_date: 2015-11-02 15:30:22 + organization: biosistemika + archived: false + archived_on: + created_at: 2015-11-01 12:53:07 + created_by: steve + last_modified_by: steve + archived_by: + restored_by: + restored_on: + +krop: + name: KROP 2012 + visibility: 1 + due_date: 2012-05-07 13:30:25 + organization: biosistemika + archived: true + archived_on: 2012-05-09 16:23:33 + created_by: mark + last_modified_by: mark + archived_by: mark + restored_by: + restored_on: + +phd: + name: PhD Thesis + visibility: 0 + due_date: + organization: nib + archived: false + archived_on: + created_by: mark + last_modified_by: steve + archived_by: + restored_by: + restored_on: + +decathlon: + name: DECATHLON + visibility: 1 + due_date: + organization: phylos + archived: false + archived_on: + created_by: jlaw + last_modified_by: jlaw + archived_by: + restored_by: + restored_on: + +valor: + name: VALOR 2010 + visibility: 1 + due_date: 2010-11-13 14:11:23 + organization: nib + archived: false + archived_on: 2012-05-09 16:33:33 + created_by: nora + last_modified_by: nora + archived_by: mark + restored_by: mark + restored_on: 2014-09-15 11:43:22 + +secret: + name: My Secret Life + visibility: 0 + due_date: 2030-06-04 13:00:00 + organization: nib + archived: false + archived_on: + created_by: mark + last_modified_by: steve + archived_by: + restored_by: + restored_on: + +a_project: + name: A Project + visibility: 1 + due_date: 2018-12-02 15:30:22 + organization: biosistemika + archived: false + archived_on: + created_at: 2015-11-01 14:28:15 + created_by: steve + last_modified_by: steve + archived_by: + restored_by: + restored_on: + +z_project: + name: Z Project + visibility: 1 + due_date: 2018-12-02 15:30:22 + organization: biosistemika + archived: false + archived_on: + created_at: 2015-11-01 16:15:37 + created_by: steve + last_modified_by: steve + archived_by: + restored_by: + restored_on: + +a_archived_project: + name: A Archived Project + visibility: 1 + due_date: 2018-12-02 15:30:22 + organization: biosistemika + archived: true + archived_on: 2015-11-05 08:14:56 + created_at: 2015-11-01 14:28:15 + created_by: steve + last_modified_by: steve + archived_by: + restored_by: + restored_on: + +z_archived_project: + name: Z Archived Project + visibility: 1 + due_date: 2018-12-02 15:30:22 + organization: biosistemika + archived: true + archived_on: 2015-11-05 08:12:56 + created_at: 2015-11-01 16:15:37 + created_by: steve + last_modified_by: steve + archived_by: + restored_by: + restored_on: + +test1: + name: Project 1 + visibility: 1 + due_date: <%= DateTime.now %> + organization: biosistemika + archived: false + created_by: steve + +test2: + name: Project 2 + visibility: 0 + organization: biosistemika + archived: true + archived_on: <%= DateTime.now %> + +test3: + name: Project 3 + visibility: 1 + organization: nib + archived: false + +dummy: + name: Dummy project + visibility: 1 + organization: nib + archived: false diff --git a/test/fixtures/report_elements.yml b/test/fixtures/report_elements.yml new file mode 100644 index 000000000..9d8b9af96 --- /dev/null +++ b/test/fixtures/report_elements.yml @@ -0,0 +1,105 @@ +re_1: + report: one + position: 0 + type_of: 0 + project: interfaces + +re_2: + report: one + position: 1 + type_of: 1 + my_module: list_of_samples + +re_2_1: + report: one + position: 0 + type_of: 2 + step: step1 + parent: re_2 + +re_2_1_1: + type_of: 8 + position: 0 + report: one + checklist: one + parent: re_2_1 + +re_2_1_2: + report: one + position: 1 + type_of: 10 + table: two + parent: re_2_1 + +re_2_1_3: + report: one + position: 2 + type_of: 11 + sort_order: asc + step: step1 + parent: re_2_1 + +re_2_2: + report: one + position: 1 + type_of: 4 + result: four + parent: re_2 + +re_2_2_1: + report: one + position: 0 + type_of: 12 + sort_order: desc + result: four + parent: re_2_2 + +re_2_3: + report: one + position: 2 + type_of: 6 + sort_order: asc + my_module: list_of_samples + +re_2_4: + report: one + position: 3 + type_of: 7 + sort_order: desc + my_module: list_of_samples + +re_3: + report: one + position: 2 + type_of: 1 + sort_order: asc + my_module: sample_prep + +re_4: + report: one + position: 3 + type_of: 1 + sort_order: asc + my_module: qpcr + +re_5: + report: one + position: 4 + type_of: 1 + sort_order: asc + my_module: quantification + +re_6: + report: one + position: 5 + type_of: 1 + sort_order: asc + my_module: rna_test + +re_7: + report: one + position: 6 + type_of: 1 + sort_order: asc + my_module: no_group + diff --git a/test/fixtures/reports.yml b/test/fixtures/reports.yml new file mode 100644 index 000000000..ddf437fcc --- /dev/null +++ b/test/fixtures/reports.yml @@ -0,0 +1,6 @@ +one: + name: "Report 1" + description: "test description" + grouped_by: 0 + project: interfaces + user: steve \ No newline at end of file diff --git a/test/fixtures/result_assets.yml b/test/fixtures/result_assets.yml new file mode 100644 index 000000000..fb2c8c120 --- /dev/null +++ b/test/fixtures/result_assets.yml @@ -0,0 +1,7 @@ +one: + result: two + asset: one + +test: + result: test_result + asset: test_result diff --git a/test/fixtures/result_comments.yml b/test/fixtures/result_comments.yml new file mode 100644 index 000000000..c645b5439 --- /dev/null +++ b/test/fixtures/result_comments.yml @@ -0,0 +1,25 @@ +one: + result: one + comment: three + +two: + result: one + comment: four + +three: + result: two + comment: five + +four: + result: three + comment: six + +test: + result: test + comment: test + +<% 25.times do |n| %> +test_<%= n %>: + result: test2 + comment: test_result_comment_<%= n %> +<% end %> diff --git a/test/fixtures/result_tables.yml b/test/fixtures/result_tables.yml new file mode 100644 index 000000000..aae32a67a --- /dev/null +++ b/test/fixtures/result_tables.yml @@ -0,0 +1,7 @@ +one: + result: three + table: one + +test: + result: test + table: test diff --git a/test/fixtures/result_texts.yml b/test/fixtures/result_texts.yml new file mode 100644 index 000000000..1d8f40247 --- /dev/null +++ b/test/fixtures/result_texts.yml @@ -0,0 +1,7 @@ +one: + text: This is an example result text. + result: one + +test: + text: This is a test result text. + result: test diff --git a/test/fixtures/results.yml b/test/fixtures/results.yml new file mode 100644 index 000000000..04da5fc00 --- /dev/null +++ b/test/fixtures/results.yml @@ -0,0 +1,66 @@ +one: + name: Result text nr. 1 + my_module: rna_test + user: steve + last_modified_by: steve + #to be added after merge + #archived: false + archived_by: + #archived_on: + restored_by: + restored_on: + +two: + name: My image Result + my_module: rna_test + user: steve + last_modified_by: jlaw + #to be added after merge + #archived: false + archived_by: + #archived_on: + restored_by: + restored_on: + +three: + name: My table result + my_module: rna_test + user: steve + last_modified_by: jlaw + #to be added after merge + #archived: false + archived_by: steve + #archived_on: 2015-10-01 14:11:23 + restored_by: jlaw + restored_on: 2015-10-05 07:01:13 + +four: + name: My table result + my_module: list_of_samples + user: steve + last_modified_by: jlaw + #to be added after merge + archived: true + archived_by: steve + archived_on: 2015-10-01 14:11:23 + #restored_by: jlaw + #restored_on: 2015-10-05 07:01:13 + +test: + name: Result test + user: steve + my_module: list_of_samples + +test2: + name: Another result + user: jlaw + my_module: rna_test + +test_result: + name: Result test + user: steve + my_module: list_of_samples + +no_items: + user: steve + my_module: sample_prep diff --git a/test/fixtures/sample_comments.yml b/test/fixtures/sample_comments.yml new file mode 100644 index 000000000..36d8e4bf7 --- /dev/null +++ b/test/fixtures/sample_comments.yml @@ -0,0 +1,11 @@ +one: + sample: sample1 + comment: ten + +two: + sample: sample1 + comment: eleven + +three: + sample: sample1 + comment: twelve \ No newline at end of file diff --git a/test/fixtures/sample_custom_fields.yml b/test/fixtures/sample_custom_fields.yml new file mode 100644 index 000000000..64e259a8e --- /dev/null +++ b/test/fixtures/sample_custom_fields.yml @@ -0,0 +1,21 @@ +one: + sample: sample1 + custom_field: volume + value: "10ml" + +two: + sample: sample1 + custom_field: location + value: "New York" + +four: + sample: sample2 + custom_field: volume + value: "15ml" + +five: + sample: sample2 + custom_field: location + value: "Paris" + + diff --git a/test/fixtures/sample_groups.yml b/test/fixtures/sample_groups.yml new file mode 100644 index 000000000..29e07da32 --- /dev/null +++ b/test/fixtures/sample_groups.yml @@ -0,0 +1,14 @@ +blood: + name: Blood + color: "#ff0000" + organization: biosistemika + created_by: steve + last_modified_by: mark + + +noble: + name: Blue blood + color: "#0000ff" + organization: biosistemika + created_by: steve + last_modified_by: steve diff --git a/test/fixtures/sample_my_modules.yml b/test/fixtures/sample_my_modules.yml new file mode 100644 index 000000000..14bb8b405 --- /dev/null +++ b/test/fixtures/sample_my_modules.yml @@ -0,0 +1,17 @@ +one: + sample: sample1 + my_module: sample_prep + assigned_by: steve + assigned_on: 2015-09-10 10:11:57 + +two: + sample: sample2 + my_module: sample_prep + assigned_by: jlaw + assigned_on: 2015-09-11 12:43:22 + +three: + sample: sample3 + my_module: sample_prep + assigned_by: nora + assigned_on: 2015-09-15 16:23:35 \ No newline at end of file diff --git a/test/fixtures/sample_types.yml b/test/fixtures/sample_types.yml new file mode 100644 index 000000000..b48fbbb1c --- /dev/null +++ b/test/fixtures/sample_types.yml @@ -0,0 +1,9 @@ +skin: + name: Skin + organization: biosistemika + created_by: steve + +urine: + name: Urine + organization: biosistemika + created_by: steve diff --git a/test/fixtures/samples.yml b/test/fixtures/samples.yml new file mode 100644 index 000000000..af9711f96 --- /dev/null +++ b/test/fixtures/samples.yml @@ -0,0 +1,31 @@ +sample1: + name: Cow DNA + user: steve + organization: biosistemika + sample_type: urine + sample_group: blood + last_modified_by: steve + +sample2: + name: Bat DNA + user: steve + organization: biosistemika + sample_type: skin + sample_group: noble + last_modified_by: steve + +sample3: + name: Cat DNA + user: steve + organization: biosistemika + last_modified_by: jlaw + +test: + name: Test Sample + user: jlaw + organization: biosistemika + +test2: + name: Another test sample + user: jlaw + organization: biosistemika diff --git a/test/fixtures/step_assets.yml b/test/fixtures/step_assets.yml new file mode 100644 index 000000000..8b953af21 --- /dev/null +++ b/test/fixtures/step_assets.yml @@ -0,0 +1,15 @@ +one: + step: step1 + asset: three + +two: + step: step2 + asset: four + +three: + step: step3 + asset: two + +test: + step: test + asset: test diff --git a/test/fixtures/step_comments.yml b/test/fixtures/step_comments.yml new file mode 100644 index 000000000..9dc392704 --- /dev/null +++ b/test/fixtures/step_comments.yml @@ -0,0 +1,17 @@ +one: + step: step1 + comment: one + +two: + step: step1 + comment: two + +test: + step: test + comment: test + +<% 25.times do |n| %> +test_<%= n %>: + step: test2 + comment: test_step_comment_<%= n %> +<% end %> diff --git a/test/fixtures/step_tables.yml b/test/fixtures/step_tables.yml new file mode 100644 index 000000000..294ebc407 --- /dev/null +++ b/test/fixtures/step_tables.yml @@ -0,0 +1,7 @@ +one: + step: step1 + table: two + +test: + step: test + table: test diff --git a/test/fixtures/steps.yml b/test/fixtures/steps.yml new file mode 100644 index 000000000..a669dbe6c --- /dev/null +++ b/test/fixtures/steps.yml @@ -0,0 +1,48 @@ +step1: + name: mRNA sequencing - preparation (1) + description: lsdkfjdfkltfgjsdkljrsdlkjrsdlrj + position: 0 + completed: false + user: steve + my_module: rna_test + last_modified_by: steve + +step2: + name: mRNA sequencing - preparation (2) + description: s098sdofsdufisdfusdifusdfusdfsdo + position: 1 + completed: false + user: steve + my_module: rna_test + last_modified_by: jlaw + +step3: + name: mRNA sequencing - preparation (3) + description: lsdkfjdfkltfgjsdkljrsdlkjrsdlrj + position: 2 + completed: false + user: steve + my_module: rna_test + last_modified_by: steve + +test: + name: Step test + position: 3 + completed: false + user: jlaw + my_module: sample_prep + +test2: + name: test + description: testing description + completed: false + user: steve + my_module: list_of_samples + position: 5 + +empty: + name: Step with no tables + position: 4 + completed: false + user: jlaw + my_module: sample_prep diff --git a/test/fixtures/tables.yml b/test/fixtures/tables.yml new file mode 100644 index 000000000..df16b745c --- /dev/null +++ b/test/fixtures/tables.yml @@ -0,0 +1,14 @@ +one: + contents: '{"data":[["1",null,null,null,"213"],["",null,null,null,"213"],["","8","4","4",null],["3","7","4","4","12312"],["5","6","4",null,"3"],["5","6",null,"5","1"],[null,"1","1","1",null],[null,null,null,null,null]]}' + created_by: steve + last_modified_by: steve + +two: + contents: '{"data":[["as",null,null,null,null],[null,null,"as",null,null],[null,null,null,null,"as"],[null,null,null,"as","as"],[null,null,null,null,null]]}' + created_by: steve + last_modified_by: steve + +test: + contents: Test + created_by: steve + last_modified_by: steve diff --git a/test/fixtures/tags.yml b/test/fixtures/tags.yml new file mode 100644 index 000000000..26149b6b6 --- /dev/null +++ b/test/fixtures/tags.yml @@ -0,0 +1,34 @@ +urgent: + name: urgent + color: "#ff0000" + project: interfaces + created_by: steve + last_modified_by: jlaw + +todo: + name: todo + color: "#00ff00" + project: interfaces + created_by: steve + last_modified_by: steve + +classified: + name: classified + color: "#0000ff" + project: interfaces + created_by: steve + last_modified_by: jlaw + +nice: + name: nice-to-have + color: "#f0000f" + project: interfaces + created_by: steve + last_modified_by: mark + +old: + name: old + color: "#000ff0" + project: a_archived_project + created_by: steve + last_modified_by: mark diff --git a/test/fixtures/temp_files.yml b/test/fixtures/temp_files.yml new file mode 100644 index 000000000..ade750ced --- /dev/null +++ b/test/fixtures/temp_files.yml @@ -0,0 +1,5 @@ +one: + session_id: 0 + file_file_name: my-avatar.jpg + file_content_type: text/plain + file_file_size: 153 diff --git a/test/fixtures/user_my_modules.yml b/test/fixtures/user_my_modules.yml new file mode 100644 index 000000000..d621b5746 --- /dev/null +++ b/test/fixtures/user_my_modules.yml @@ -0,0 +1,48 @@ +one: + user: steve + my_module: sample_prep + assigned_by: steve + +two: + user: steve + my_module: rna_test + assigned_by: jlaw + +three: + user: jlaw + my_module: rna_test + assigned_by: jlaw + +four: + user: steve + my_module: qpcr + assigned_by: steve + +five: + user: steve + my_module: list_of_samples + assigned_by: steve + +non_existing_user: + user_id: 99999 + my_module: rna_test + assigned_by: jlaw + +non_existing_module: + user_id: jlaw + my_module_id: 99999 + assigned_by: jlaw + +without_user: + user_id: 0 + my_module: rna_test + assigned_by: jlaw + +without_module: + user_id: jlaw + my_module: 0 + +archived: + user: steve + my_module: archived + diff --git a/test/fixtures/user_organizations.yml b/test/fixtures/user_organizations.yml new file mode 100644 index 000000000..eea958c54 --- /dev/null +++ b/test/fixtures/user_organizations.yml @@ -0,0 +1,99 @@ +one: + user: steve + organization: biosistemika + role: 1 + assigned_by: steve + + +two: + user: steve + organization: nib + role: 1 + assigned_by: steve + + +seven: + user: steve + organization: phylos + role: 1 + assigned_by: steve + + +three: + user: mark + organization: biosistemika + role: 1 + assigned_by: mark + + +four: + user: mark + organization: nib + role: 2 + assigned_by: mark + + +five: + user: jlaw + organization: biosistemika + role: 0 + assigned_by: steve + + +six: + user: nora + organization: nib + role: 1 + assigned_by: nora + + +eight: + user: nora + organization: phylos + role: 2 + assigned_by: nora + + +nine: + user: default + organization: biosistemika + role: 2 + assigned_by: default + + +ten: + user: default + organization: nib + role: 2 + assigned_by: default + +without_role: + user: nora + organization: nib + role: -1 + assigned_by: steve + +without_user: + user: 0 + organization: nib + role: 1 + assigned_by: steve + +without_organization: + user: nora + organization: 0 + role: 1 + assigned_by: steve + +with_invalid_user: + user_id: 9999999 + organization: nib + role: 1 + assigned_by: steve + +with_invalid_organization: + user: nora + organization_id: 999999 + role: 1 + assigned_by: steve + diff --git a/test/fixtures/user_projects.yml b/test/fixtures/user_projects.yml new file mode 100644 index 000000000..e5ddc2c40 --- /dev/null +++ b/test/fixtures/user_projects.yml @@ -0,0 +1,120 @@ +one: + user: steve + project: interfaces + role: 0 + assigned_by: steve + +two: + user: steve + project: decathlon + role: 1 + assigned_by: steve + +three: + user: steve + project: valor + role: 1 + assigned_by: mark + +four: + user: mark + project: eurostars + role: 3 + assigned_by: mark + +five: + user: mark + project: phd + role: 0 + assigned_by: nora + +six: + user: mark + project: valor + role: 2 + assigned_by: mark + +seven: + user: jlaw + project: eurostars + role: 3 + assigned_by: mark + +eight: + user: nora + project: decathlon + role: 2 + assigned_by: nora + +nine: + user: nora + project: secret + role: 0 + assigned_by: nora + +ten: + user: nora + project: valor + role: 1 + assigned_by: default + +eleven: + user: jlaw + project: interfaces + role: 1 + assigned_by: steve + +twelve: + user: steve + project: a_project + role: 0 + assigned_by: steve + +thirteen: + user: steve + project: z_project + role: 0 + assigned_by: steve + +fourteen: + user: steve + project: a_archived_project + role: 0 + assigned_by: steve + +fifteen: + user: steve + project: z_archived_project + role: 0 + assigned_by: steve + +without_role: + user: nora + project: decathlon + role: -1 + assigned_by: steve + +without_user: + user: 0 + project: decathlon + role: 1 + assigned_by: steve + +without_project: + user: nora + project: 0 + role: 1 + assigned_by: steve + +with_invalid_user: + user_id: 9999999 + project: decathlon + role: 1 + assigned_by: steve + +with_invalid_project: + user: nora + project_id: 999999 + role: 1 + assigned_by: steve + diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml new file mode 100644 index 000000000..28a506ee6 --- /dev/null +++ b/test/fixtures/users.yml @@ -0,0 +1,41 @@ +steve: + full_name: Steve Johnson + initials: SJ + email: steve.johnson@gmail.com + encrypted_password: <%= Devise::Encryptor.digest(User, 'hidden_password') %> + confirmed_at: <%= Time.now %> + +john: + full_name: John Doe + initials: JD + email: john.doe@gmail.com + encrypted_password: <%= Devise::Encryptor.digest(User, '12345678') %> + confirmed_at: <%= Time.now %> + +mark: + full_name: Mark Bond + initials: MB + email: mark.bond@gmail.com + encrypted_password: <%= Devise::Encryptor.digest(User, 'secret_password') %> + confirmed_at: <%= Time.now %> + +jlaw: + full_name: Jennifer Lawrence + initials: JL + email: jennifer.lawrence@gmail.com + encrypted_password: <%= Devise::Encryptor.digest(User, 'jlaw_pass') %> + confirmed_at: <%= Time.now %> + +nora: + full_name: Nora Jones + initials: NJ + email: nora.jones@gmail.com + encrypted_password: <%= Devise::Encryptor.digest(User, 'nora_pass') %> + confirmed_at: <%= Time.now %> + +default: + full_name: Default User + initials: DU + email: my.platr@gmail.com + encrypted_password: <%= Devise::Encryptor.digest(User, '12345678') %> + confirmed_at: <%= Time.now %> diff --git a/test/helpers/.keep b/test/helpers/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/test/helpers/archivable_model_test_helper.rb b/test/helpers/archivable_model_test_helper.rb new file mode 100644 index 000000000..5db097a25 --- /dev/null +++ b/test/helpers/archivable_model_test_helper.rb @@ -0,0 +1,76 @@ +module ArchivableModelTestHelper + + def assert_archived_present(obj) + assert_not obj.archived.nil?, "Archived attribute must be present." + end + + def assert_active_is_inverse_of_archived(obj) + assert_archived_present(obj) + assert obj.archived? == !obj.active?, "Active attribute isn't inverse of archived attribute." + end + + # A new object should be provided (one that + # has never been archived/restored before, but is + # otherwise valid). + def archive_and_restore_action_test(obj, user) + # Check initial values + assert obj.archived? == false, "New (never archived) #{obj.class}'s archived attribute is not set to 'false' by default." + assert obj.archived_on.blank?, "New (never archived) #{obj.class}'s archived_on attribute is not blank." + assert obj.archived_by.blank?, "New (never archived) #{obj.class}'s archived_by attribute is not blank." + assert_restored_on_blank(obj) + assert_restored_by_blank(obj) + + # Now, archive the project + ts = Time.now + sleep 1 + if user.present? + obj.archive(user) + assert obj.archived_by.present?, "#{obj.class}'s archive action didn't set the archived_by attribute." + else + obj.archive + end + assert obj.archived? == true, "#{obj.class}'s archive action didn't archive it." + assert ( + obj.archived_on.present? and + obj.archived_on > ts and + (obj.archived_on - ts) < 5.seconds + ), "#{obj.class}'s archive action didn't set archived_on timestamp properly." + + # Make sure restored values are still blank + assert_restored_on_blank(obj) + assert_restored_by_blank(obj) + + archived_on_ts = obj.archived_on + archived_by_user = obj.archived_by + + # Alright, restore the object now + ts = Time.now + sleep 1 + if user.present? + obj.restore(user) + assert obj.restored_by.present?, "#{obj.class}'s restore action didn't set the restored_by attribute." + else + obj.restore + end + assert obj.archived? == false, "#{obj.class}'s restore action didn't restore it." + assert ( + obj.restored_on.present? and + obj.restored_on > ts and + (obj.restored_on - ts) < 5.seconds + ), "#{obj.class}'s restore action didn't set restored_on timestamp properly." + + assert archived_on_ts == obj.archived_on, "#{obj.class}'s restore action changed its archived_on timestamp." + assert archived_by_user == obj.archived_by, "#{obj.class}'s restore action' changed its archived_by attribute." + end + + private + + def assert_restored_on_blank(obj) + assert obj.restored_on.blank?, "New (never restored) #{obj.class}'s restored_on attribute is not blank." + end + + def assert_restored_by_blank(obj) + assert obj.restored_by.blank?, "New (never restored) #{obj.class}'s restored_by attribute is not blank." + end + +end \ No newline at end of file diff --git a/test/helpers/fake_test_helper.rb b/test/helpers/fake_test_helper.rb new file mode 100644 index 000000000..986341409 --- /dev/null +++ b/test/helpers/fake_test_helper.rb @@ -0,0 +1,62 @@ +module FakeTestHelper + + # Generates random CSV file - used for testing purposes + def generate_csvfile + file = Tempfile.open("test", Rails.root.join("tmp")) + begin + (1..10).each do |i| + (1..10).each do |k| + file.write(rand(50..100).to_s + ",") + end + file.write("\n") + end + ensure + file.close + end + file.open + end + + # Generates File of size size_in_mb and returns fd + def generate_file(size_in_mb) + require 'securerandom' + one_megabyte = 2 ** 20 + randString = SecureRandom.random_bytes(size_in_mb * one_megabyte) + + file = Tempfile.open("asset_test", Rails.root.join("tmp")) + file.binmode + file.write(randString) + file.close + file.open + end + + def generate_table_contents(rows, cols) + res = [] + if rows > 0 + res << (1..cols).map{ Faker::Team.state }.to_a + for _ in 2..rows + res << (1..cols).map{ rand * 1000 }.to_a + end + end + return { data: res }.to_json + end + + def generate_color + res = "#" + for _ in 1..6 + res << "0123456789ABCDEF"[rand(0..15)] + end + res + end + + # Game of Thrones name generator v0.2 in all its glory! + # Generate your favourite Westerosi characters with a single + # line of code! + def generate_got_name + GAME_OF_THRONES_NAMES.sample + end + + private + + GAME_OF_THRONES_NAMES = ["Tyrion Lannister", "Cersei Lannister", "Daenerys Targaryen", "Arya Stark", "Jon Snow", "Sansa Stark", "Jorah Mormont", "Jaime Lannister", "Sandor Clegan", "Tywin lannister", "Theon Greyjoy", "Samwell Tarly", "Joffrey Baratheon", "Catelyn Stark", "Bran Stark", "Petyr Baelish", "Varys", "Robb Stark", "Brienne of Tarth", "Bronn", "Shae", "Gendry", "Ygritte", "Margaery Tyrell", "Stannis Baratheon", "Missandei", "Davos Seaworth", "Melisandre", "Gilly", "Tormund Giantsbane", "Jeor Mormont", "Talisa Stark", "Eddard Stark", "Khal Drogo", "Ramsay Bolton", "Robert Baratheon", "Daario Naharis", "Viserys Targaryen", "Olenna Tyrell", "Maester Luwin", "Mance Rayder", "Oberyn Martell", "Ellaria Sand", "Gregor Clegane", "Walder Frey", "Robin Arryn", "Lysa Arryn", "Tommen Baratheon", "Myrcella Baratheon", "Renly Baratheon", "Salladhor Saan", "Roose Bolton", "Ramsay Bolton", "Balon Greyjoy", "Yara Greyjoy", "Kevan Lannister", "Lancel Lannister", "Polliver", "Amory Lorch", "Doran Martell", "Trystane Martell", "Areo Hotah", "Nymeria Sand", "Obara Sand", "Tyene Sand", "Rickon Stark", "Hodor", "Meera Reed", "Jojen Reed", "Osha", "Rodrik Cassel", "Jory Cassel", "Old Nan", "Jon Umber", "Barristan Selmy", "Grey Worm", "Edmure Tully", "Brynden Tully", "Loras Tyrell", "Mace Tyrell", "Maester Aemon", "Alliser Thorne", "Janos Slynt", "Grenn", "Pyp", "Yoren", "Qhorin Halfhand", "Benjen Stark", "Illyrio Mopatis", "Podrick Payne", "Jaqen H'ghar", "Beric Dondarrion", "Thoros of Myr", "Syrio Forel", "Grand Maester Pycelle", "Qyburn", "Meryn Trant", "The High Septon", "Dontos Hollard", "Ilyn Payne", "Craster", "Grey Wind", "Ghost", "Lady", "Nymeria", "Summer", "Shaggydog", "Balerion", "Meraxes", "Vhagar", "Drogon", "Rhaegal", "Viserion"] + +end \ No newline at end of file diff --git a/test/helpers/searchable_model_test_helper.rb b/test/helpers/searchable_model_test_helper.rb new file mode 100644 index 000000000..544c46ab9 --- /dev/null +++ b/test/helpers/searchable_model_test_helper.rb @@ -0,0 +1,33 @@ +module SearchableModelTestHelper + + def attributes_like_test(clazz, attributes, query) + if attributes.blank? or query.blank? + attrs = [] + elsif attributes.is_a? Symbol + attrs = [attributes.to_s] + elsif attributes.is_a? String + attrs = [attributes] + elsif attributes.is_a? Array + attrs = attributes.collect { |a| a.to_s } + else + raise ArgumentError, ":attributes must be an array, symbol or string" + end + + results = clazz.all.where_attributes_like(attrs, query) + unless results.blank? + equery = "#{query.downcase}" + results.each do |result| + cntr = 0 + attrs.each do |attr| + val = eval("result.#{attr}").downcase + if (val =~ /.*#{equery}.*/) then + cntr += 1 + end + end + + assert cntr > 0, "Not all attributes are matching" + end + end + end + +end \ No newline at end of file diff --git a/test/integration/.keep b/test/integration/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/test/integration/canvas_update_test.rb b/test/integration/canvas_update_test.rb new file mode 100644 index 000000000..2aff5d835 --- /dev/null +++ b/test/integration/canvas_update_test.rb @@ -0,0 +1,245 @@ +require 'test_helper' + +class CanvasUpdateTest < ActionDispatch::IntegrationTest + def setup + # Preload user + @user = users(:steve) + @password = "hidden_password" + + # Preload project + @project = projects(:interfaces) + + # Initialize empty params + @connections = ( + @project.my_modules + .select { |m| m.active? } + .collect { |m| m.outputs.collect { |c| "#{c.from.id},#{c.to.id}" } } + ).flatten.join(",") + @positions = ( + @project.my_modules + .select { |m| m.active? } + .collect { |m| "#{m.id},#{m.x},#{m.y}" } + ).join(";") + @add = "" + @add_names = "" + @rename = "{}" + @cloned = "" + @remove = "" + @module_groups = "{}" + + # Sign in as user first + sign_in @user, @password + end + + test "should pass without arguments" do + error = false + begin + post_via_redirect canvas_project_url(@project) + rescue Exception + error = true + end + assert_not error + end + + test "should pass with valid arguments" do + post canvas_project_url(@project), + connections: @connections, + positions: @positions, + add: @add, + "add-names" => @add_names, + rename: @rename, + cloned: @cloned, + remove: @remove, + "module-groups" => @module_groups + + assert_redirected_to canvas_project_url(@project) + end + + test "should not pass with invalid project id" do + post_via_redirect canvas_project_url(-5) + assert_redirected_to_404 + end + + test "should not pass with invalid connections" do + m1 = my_modules(:qpcr).id + m2 = my_modules(:no_group).id + + invalid_connections = [ + "1,2,3", # Not dividable by 2 + "kkj44gk", # Invalid string + 2015, # Number, not dividable by 2 + "#{m1},#{m2},#{m2},#{m1}" # Cycle + ] + + invalid_connections.each do |conn| + post_via_redirect canvas_project_url(@project), + connections: conn, + positions: @positions, + add: @add, + "add-names" => @add_names, + rename: @rename, + cloned: @cloned, + remove: @remove, + "module-groups" => @module_groups + + assert_redirected_to_403 + end + end + + test "should not pass with invalid positions" do + invalid_positions = [ + "fkgdfgfd", + "dsfldkfsd;ldfkdsl;asdsa", # Subtsrings not divided by commas + "a,b,c,d;1,2,3,4", # Substrings have lenghts of 4 + "m1,2,a;m2,b,3", # Position is not an integer + "m1,1,2;m2,1,2;m3,2,2" # Multiple modules cannot have same position + ] + + invalid_positions.each do |pos| + post_via_redirect canvas_project_url(@project), + connections: @connections, + positions: pos, + add: @add, + "add-names" => @add_names, + rename: @rename, + cloned: @cloned, + remove: @remove, + "module-groups" => @module_groups + + assert_redirected_to_403 + end + end + + test "should not pass with invalid add strings" do + invalid_positions = [ + "", # No positions provided + "m1,0,1;m2,4,5", # Invalid module names (too short) + "m1,0,1;m2,4,5" # Names.length != Ids.length + ] + invalid_adds = [ + "m1,m2", # No positions provided + "m1,m2", # Invalid module names (too short) + "m1,m2" # Names.length != Ids.length + ] + invalid_names = [ + "module1,module2", # No positions provided + "a,b", # Invalid module names (too short) + "module1,module2,module3" # Names.length != Ids.length + ] + + invalid_adds.zip(invalid_names).each_with_index do |val, i| + pos = @positions + ";" + invalid_positions[i] + post_via_redirect canvas_project_url(@project), + connections: @connections, + positions: pos, + add: val[0], + "add-names" => val[1], + rename: @rename, + cloned: @cloned, + remove: @remove, + "module-groups" => @module_groups + + assert_redirected_to_403 + end + end + + test "should not pass with invalid rename strings" do + invalid_renames = [ + "asdkjkasd asd", + "'m1':'abule'", + "{15:'aa', 'ac': 23}", + "[]", + ] + + invalid_renames.each do |val| + post_via_redirect canvas_project_url(@project), + connections: @connections, + positions: @positions, + add: @adds, + "add-names" => @add_names, + rename: val, + cloned: @cloned, + remove: @remove, + "module-groups" => @module_groups + + assert_redirected_to_403 + end + end + + test "should not pass with invalid clone strings" do + positions = "m1,0,1;m2,4,5" + adds = "m1,m2" + names = "module1|module2" + + invalid_clones = [ + "kgjfdklg;123;aa2", # Invalid strings + "5k6,m1;zulu,m2", # Invalid source module + "133,m3;233,m4" # Cloned IDs not present in add string + ] + + invalid_clones.each do |val| + post_via_redirect canvas_project_url(@project), + connections: @connections, + positions: positions, + add: adds, + "add-names" => names, + rename: @rename, + cloned: val, + remove: @remove, + "module-groups" => @module_groups + + assert_redirected_to_403 + end + end + + test "should not pass with invalid remove strings" do + invalid_removes = [ + "a,b,c" # Non-integers + ] + + invalid_removes.each do |remove| + post_via_redirect canvas_project_url(@project), + connections: @connections, + positions: @positions, + add: @add, + "add-names" => @add_names, + rename: @rename, + cloned: @cloned, + remove: remove, + "module-groups" => @module_groups + + assert_redirected_to_403 + end + end + + test "should not pass with invalid module group strings" do + invalid_module_groups = [ + "asdkjkasd asd", + "'m1':'abule'", + "{15:'aa', 'ac': 23}", + "[]", + ] + + invalid_module_groups.each do |val| + post_via_redirect canvas_project_url(@project), + connections: @connections, + positions: @positions, + add: @adds, + "add-names" => @add_names, + rename: @rename, + cloned: @cloned, + remove: @remove, + "module-groups" => val + + assert_redirected_to_403 + end + end + + private + + # Alas, Devise test helpers don't work in integration tests, + # so this is a "manual" login + def sign_in(user, password) + post_via_redirect user_session_url, "user[email]" => user.email, "user[password]" => password + end +end diff --git a/test/mailers/.keep b/test/mailers/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/test/models/.keep b/test/models/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/test/models/activity_test.rb b/test/models/activity_test.rb new file mode 100644 index 000000000..7ac583b10 --- /dev/null +++ b/test/models/activity_test.rb @@ -0,0 +1,41 @@ +require 'test_helper' + +class ActivityTest < ActiveSupport::TestCase + test "should validate with correct data" do + activity = Activity.new( + type_of: 0, + project: projects(:interfaces), + my_module: my_modules(:sample_prep), + user: users(:steve) + ) + assert activity.valid? + end + + test "should not validate without type_of" do + activity = Activity.new( + project: projects(:interfaces), + my_module: my_modules(:sample_prep), + user: users(:steve) + ) + assert_not activity.valid? + end + + test "should not validate with non existent project" do + activity = Activity.new( + type_of: 0, + project_id: 1212, + user: users(:steve) + ) + assert_not activity.valid? + end + + test "should not validate with non existent user" do + activity = Activity.new( + type_of: 0, + project: projects(:interfaces), + my_module: my_modules(:sample_prep), + user_id: 123213123 + ) + assert_not activity.valid? + end +end diff --git a/test/models/asset_test.rb b/test/models/asset_test.rb new file mode 100644 index 000000000..c8987b627 --- /dev/null +++ b/test/models/asset_test.rb @@ -0,0 +1,90 @@ +require 'test_helper' +require 'helpers/searchable_model_test_helper' +require 'helpers/fake_test_helper' + +class AssetTest < ActiveSupport::TestCase + include SearchableModelTestHelper + include FakeTestHelper + + def setup + @user = users(:nora) + @step = Step.create( + name: "Step test", + position: 0, + completed: 0, + user: @user, + my_module: my_modules(:sample_prep)) + @result = Result.create( + name: "Result test", + user: @user, + my_module: my_modules(:list_of_samples), + asset: assets(:one) + ) + + @comment = Comment.create( + message: "random comment", + user: @user) + @asset = Asset.new(file: generate_csvfile) + end + + def teardown + @asset.file = nil + if @asset.persisted? then + @asset.save + @asset.destroy + end + end + + test "should not validate with step and result present" do + @asset.step = @step + @asset.result = @result + assert_not @asset.valid? + end + + test "should not validate without step and result present" do + skip # Omit due to GUI problems (see asset.rb) + assert @asset.result.blank? + assert @asset.step.blank? + assert_not @asset.valid? + end + + test "should not validate without estimated_size present" do + @asset.step = @step + @asset.estimated_size = nil + assert @asset.invalid? + end + + test "estimated size defaults to 0" do + asset = Asset.new + assert 0, asset.estimated_size + end + + test "should validate with only step present" do + assert @asset.result.blank? + @asset.step = @step + assert @asset.valid? + end + + test "should validate with only result present" do + assert @asset.step.blank? + @asset.result = @result + assert @asset.valid? + end + + + test "should not allow files larger than 20MB" do + asset = Asset.new(file: generate_file(21)) + asset.step = @step + assert_not asset.valid? + end + + test "should allow files < 20MB" do + asset = Asset.new(file: generate_file(19)) + asset.step = @step + assert asset.valid? + end + + test "where_attributes_like should work" do + attributes_like_test(Asset, :file_file_name, "file") + end +end diff --git a/test/models/asset_text_datum_test.rb b/test/models/asset_text_datum_test.rb new file mode 100644 index 000000000..01a02202d --- /dev/null +++ b/test/models/asset_text_datum_test.rb @@ -0,0 +1,26 @@ +require 'test_helper' + +class AssetTextDatumTest < ActiveSupport::TestCase + def setup + @asset_data = asset_text_datum(:one) + end + + test "should validate with valid data" do +skip + assert @asset_data.valid? + end + + test "should check if data is present" do +skip + @asset_data.data = "" + assert_not @assert_data.valid? + @asset_data.data = nil + assert_not @assert_data.valid? + end + + test "should check if associated asset is valid" do +skip + assert_not asset_text_datum(:invalid_asset_id) + assert_not asset_text_datum(:invalid_asset_value) + end +end diff --git a/test/models/checklist_item_test.rb b/test/models/checklist_item_test.rb new file mode 100644 index 000000000..e147a2a23 --- /dev/null +++ b/test/models/checklist_item_test.rb @@ -0,0 +1,56 @@ +require 'test_helper' + +class ChecklistItemTest < ActiveSupport::TestCase + + test "should validate with correct data" do + chkItem = ChecklistItem.new( + text: "test", + checked: false, + checklist: checklists(:one) + ) + assert chkItem.valid? + end + + test "should not validate without text" do + chkItem = ChecklistItem.new( + text: " ", checked: false, + checklist: checklists(:one)) + assert_not chkItem.valid?, "Checklist item was created without text." + end + + test "should not validate with text too long" do + chkItem = ChecklistItem.new( + text: "#" * 1001, + checked: false, + checklist: checklists(:one) + ) + assert_not chkItem.valid?, "Checklist item was created with text being too long." + end + + test "should not validate without checked value" do + chkItem = ChecklistItem.new( + text: "text", checked: nil, + checklist: checklists(:one)) + assert_not chkItem.valid?, "Checklist item was created without checked value." + end + + test "should not validate with non existent checklist" do + chkItem = ChecklistItem.new( + text: "text", checked: false, + checklist_id: 1231234121) + assert_not chkItem.valid?, "Checklist item was created with checklist which doesn't exist." + end + + test "should have association checklist <-> checklist item" do + checklist = Checklist.create( + name: "Checklist 17", + step: steps(:step1)) + item = ChecklistItem.create( + text: "text", checked: false, + checklist: checklist) + + checklist.checklist_items << item + assert_equal item, Checklist.find(checklist.id).checklist_items.first, "No association checklist -> checklist item." + assert_equal checklist, ChecklistItem.find(item.id).checklist, "No association checklist item -> checklist." + end +end diff --git a/test/models/checklist_test.rb b/test/models/checklist_test.rb new file mode 100644 index 000000000..a2c741e31 --- /dev/null +++ b/test/models/checklist_test.rb @@ -0,0 +1,44 @@ +require 'test_helper' + +class ChecklistTest < ActiveSupport::TestCase + test "should validate with correct data" do + checklist = Checklist.new( + name: "test", + step: steps(:step1) + ) + assert checklist + end + + test "should not validate without name" do + checklist = Checklist.new(step: steps(:step1)) + assert_not checklist.valid? + end + + test "should not validate with name too long" do + checklist = Checklist.new( + name: "#" * 51, + step: steps(:step1) + ) + assert_not checklist.valid? + end + + test "should not validate with non existent step" do + checklist = Checklist.new( + name: "test", + step_id: 123123) + assert_not checklist.valid? + end + + test "should have association step -> checklist" do + checklist = Checklist.new( + name: "test", + step: steps(:step1)) + step = Step.create(name: "Step test", position: 0, completed: 0, user: users(:steve), my_module: my_modules(:sample_prep)) + + assert_empty step.checklists + step.checklists << checklist + + assert_equal checklist, Step.find(step.id).checklists.first + end + +end diff --git a/test/models/comment_test.rb b/test/models/comment_test.rb new file mode 100644 index 000000000..37d444a86 --- /dev/null +++ b/test/models/comment_test.rb @@ -0,0 +1,73 @@ +require 'test_helper' + +class CommentTest < ActiveSupport::TestCase + + def setup + @valid = Comment.new( + message: "test", + user: users(:steve), + ) + ResultComment.new( + result: results(:one), + comment: @valid + ) + end + + test "should validate" do + assert @valid.valid? + end + + test "should not validate with empty or nil message" do + comment_empty = Comment.new(message: " ", user: users(:steve)) + comment_nil = Comment.new(message: nil, user: users(:steve)) + + assert_not comment_empty.valid?, "Comment was created with empty message." + assert_not comment_nil.valid?, "Comment was created with nil message." + end + + test "should not validate with message too long" do + comment = Comment.new( + message: "#" * 1001, + user: users(:steve) + ) + assert_not comment.valid?, "Comment was created with message being too long." + end + + test "should not validate with empty user_id" do + comment = Comment.new(message: "Message") + assert_not comment.valid?, "Comment was created with empty user_id." + end + + test "should not validate with non existent user" do + comment = Comment.new(message: "comment", user_id: 123123) + assert_not comment.valid?, "Comment was created with user who doesn't exist." + end + + test "should not validate with more than one assigned object" do + StepComment.new( + step: steps(:step1), + comment: @valid + ) + ResultComment.new( + result: results(:one), + comment: @valid + ) + assert_not @valid.valid?, "Comment was valid with more than one assigned object." + end + + test "should not validate with no assigned object" do + skip # Omit due to GUI problems (see comment.rb) + @valid.step_comment = nil + @valid.my_module_comment = nil + @valid.result_comment = nil + @valid.sample_comment = nil + @valid.project_comment = nil + assert_not @valid.valid?, "Comment was valid despite having no assigned object." + end + + test "should have association comment -> user" do + user = users(:jlaw) + comment = Comment.create(message: "comment", user: user) + assert_equal user, Comment.find(comment.id).user + end +end diff --git a/test/models/connection_test.rb b/test/models/connection_test.rb new file mode 100644 index 000000000..e2c816630 --- /dev/null +++ b/test/models/connection_test.rb @@ -0,0 +1,5 @@ +require 'test_helper' + +class ConnectionTest < ActiveSupport::TestCase + # Nothing to test 8) +end diff --git a/test/models/custom_field_test.rb b/test/models/custom_field_test.rb new file mode 100644 index 000000000..5d48b69ad --- /dev/null +++ b/test/models/custom_field_test.rb @@ -0,0 +1,37 @@ +require 'test_helper' + +class CustomFieldTest < ActiveSupport::TestCase + def setup + @custom_field = custom_fields(:volume) + end + + test "should validate with correct data" do + assert @custom_field.valid? + end + + test "should not validate without name" do + @custom_field.name = "" + assert_not @custom_field.valid? + @custom_field.name = nil + assert_not @custom_field.valid? + end + + test "should not validate with too long name" do + @custom_field.name = "n" * 51 + assert_not @custom_field.valid? + end + + test "should not validate with non existent user" do + @custom_field.user_id = 11231231 + assert_not @custom_field.valid? + @custom_field.user = nil + assert_not @custom_field.valid? + end + + test "should not validate with non existent organization" do + @custom_field.organization_id = 1231231 + assert_not @custom_field.valid? + @custom_field.organization = nil + assert_not @custom_field.valid? + end +end diff --git a/test/models/log_test.rb b/test/models/log_test.rb new file mode 100644 index 000000000..6318ab311 --- /dev/null +++ b/test/models/log_test.rb @@ -0,0 +1,28 @@ +require 'test_helper' + +class LogTest < ActiveSupport::TestCase + + def setup + @log = logs(:one) + end + + test "should validate log with valid data" do + assert @log.valid? + end + + test "should have non-blank message" do + @log.message = "" + assert @log.invalid?, "Log with blank message returns valid? = true" + @log.message = nil + assert @log.invalid?, "Log with nil message returns valid? = true" + end + + test "should have organization" do + @log.organization_id = 12321321 + assert @log.invalid?, "Log without organization returns valid? = true" + @log.organization = nil + assert @log.invalid?, "Log without organization returns valid? = true" + @log.organization = Organization.new + assert @log.valid?, "Log with organization returns valid? = false" + end +end diff --git a/test/models/my_module_comment_test.rb b/test/models/my_module_comment_test.rb new file mode 100644 index 000000000..0877dafb2 --- /dev/null +++ b/test/models/my_module_comment_test.rb @@ -0,0 +1,39 @@ +require 'test_helper' + +class MyModuleCommentTest < ActiveSupport::TestCase + def setup + @module_comment = my_module_comments(:test) + @my_module = @module_comment.my_module + @comment = @module_comment.comment + end + + test "should validate with correct data" do + assert @module_comment.valid? + end + + test "should not validate with non existent comment id" do + @module_comment.comment_id = 2343434 + assert_not @module_comment.valid? + @module_comment.comment = nil + assert_not @module_comment.valid? + end + + test "should not validate with non existent module id" do + @module_comment.my_module_id = 1223232323 + assert_not @module_comment.valid? + @module_comment.my_module = nil + assert_not @module_comment.valid? + end + + test "should check module/comment uniqueness" do + module_comment = MyModuleComment.new( + my_module: @my_module, comment: @comment) + assert_not module_comment.save + end + + test "should have association my_module -> comment" do + @my_module.comments << comments(:unassociated) + assert_equal @comment, MyModule.find(@my_module.id).comments.first, + "There is no association between my_module -> comment." + end +end diff --git a/test/models/my_module_group_test.rb b/test/models/my_module_group_test.rb new file mode 100644 index 000000000..91cdab33c --- /dev/null +++ b/test/models/my_module_group_test.rb @@ -0,0 +1,30 @@ +require 'test_helper' +require 'helpers/searchable_model_test_helper' + +class MyModuleGroupTest < ActiveSupport::TestCase + include SearchableModelTestHelper + + def setup + @module_group = my_module_groups(:wf1) + end + + test "should validate with valid data" do + assert @module_group.valid? + end + + test "should not validate with name" do + @module_group.name = "" + assert_not @module_group.valid? + @module_group.name = nil + assert_not @module_group.valid? + end + + test "should not validate too long name" do + @module_group.name = "n" * 51 + assert_not @module_group.valid? + end + + test "where_attributes_like should work" do + attributes_like_test(MyModuleGroup, :name, "expression") + end +end diff --git a/test/models/my_module_tag_test.rb b/test/models/my_module_tag_test.rb new file mode 100644 index 000000000..57653cce9 --- /dev/null +++ b/test/models/my_module_tag_test.rb @@ -0,0 +1,42 @@ +require 'test_helper' + +class MyModuleTagTest < ActiveSupport::TestCase + def setup + @my_module = my_modules(:qpcr) + @tag = tags(:todo) + @module_tag = MyModuleTag.new( + my_module: @my_module, tag: @tag) + assert @module_tag.save + end + + test "should validate with correct data" do + assert @module_tag.valid? + end + + test "should not validate with non existent tag id" do + @module_tag.tag_id = 2343434 + assert_not @module_tag.valid? + @module_tag.tag = nil + assert_not @module_tag.valid? + end + + test "should not validate with non existent module id" do + @module_tag.my_module_id = 1223232323 + assert_not @module_tag.valid? + @module_tag.my_module = nil + assert_not @module_tag.valid? + end + + test "should check module/tag uniqueness" do + module_tag = MyModuleTag.new( + my_module: @my_module, tag: @tag) + assert_not module_tag.save + end + + test "should have association my_module -> tag" do + tag = tags(:urgent) + @my_module.tags << tag + assert_equal tag, MyModule.find(@my_module.id).tags.last, + "There is no association between my_module -> tag." + end +end diff --git a/test/models/my_module_test.rb b/test/models/my_module_test.rb new file mode 100644 index 000000000..b8cef493c --- /dev/null +++ b/test/models/my_module_test.rb @@ -0,0 +1,161 @@ +require 'test_helper' +require 'helpers/archivable_model_test_helper' +require 'helpers/searchable_model_test_helper' + +class MyModuleTest < ActiveSupport::TestCase + include ArchivableModelTestHelper + include SearchableModelTestHelper + + def setup + @my_module = my_modules(:list_of_samples) + end + + test "should validate valid module object" do + assert @my_module.valid? + end + + test "should not validate without name" do + @my_module.name = "" + assert_not @my_module.valid? + @my_module.name = nil + assert_not @my_module.valid? + end + + test "should not validate too short name" do + @my_module.name = "n" + assert_not @my_module.valid? + end + + test "should not validate too long name" do + @my_module.name = "n" * 51 + assert_not @my_module.valid? + end + + test "should not validate with non existing project" do + @my_module.project_id = 123123 + assert_not @my_module.valid? + @my_module.project = nil + assert_not @my_module.valid? + end + + test "should not validate with non existing module group, when group is set" do + @my_module.my_module_group_id = 23123 + assert_not @my_module.valid? + end + + test "should default to 0, when x and y not set" do + assert_equal 0, @my_module.x + assert_equal 0, @my_module.y + end + + test "should default to 0, when workflow_order not set" do + assert_equal 0, @my_module.workflow_order + end + + test "should have archived set" do + assert_archived_present(@my_module) + assert_active_is_inverse_of_archived(@my_module) + end + + test "archiving should work" do + user = users(:steve) + archive_and_restore_action_test(@my_module, user) + end + + test "where_attributes_like should work" do + attributes_like_test(MyModule, [:name, :description], "sample") + end + + test "should get unassigned users" do + unassigned_users = @my_module.unassigned_users + assert_equal 1, unassigned_users.size + @my_module.users << unassigned_users.first + assert @my_module.save + unassigned_users = @my_module.unassigned_users + assert_equal 0, unassigned_users.size + end + + test "should get unassigned samples" do + unassigned_samples = @my_module.unassigned_samples + assert_equal 5, unassigned_samples.size + @my_module.samples << unassigned_samples.first + assert @my_module.save + unassigned_samples = @my_module.unassigned_samples + assert_equal 4, unassigned_samples.size + end + + test "should get unassigned tags" do + unassigned_tags = @my_module.unassigned_tags + assert_equal 2, unassigned_tags.size + @my_module.tags << unassigned_tags.first + assert @my_module.save + unassigned_tags = @my_module.unassigned_tags + assert_equal 1, unassigned_tags.size + end + + test "should count steps of module" do + assert_equal 1, @my_module.number_of_steps + end + + test "should get last comments" do + skip + end + + test "should get last activities" do + skip + end + + test "should get specified number of samples" do + skip + end + + test "should get completed steps" do + skip + end + + test "should check if project is overdue" do + assert @my_module.is_overdue? + @my_module.due_date = "2025-12-04 12:00:00" + assert_not @my_module.is_overdue? + end + + test "should check if overdue in days" do + days_diff = 12 + @my_module.due_date = DateTime.now - days_diff + assert_equal days_diff, @my_module.overdue_for_days + end + + test "should check if is due date one day prior" do + @my_module.due_date = DateTime.now + 1.hour + assert @my_module.is_one_day_prior? + end + + test "should check if due date is due in specified days" do + @my_module.due_date = DateTime.now + 1.hour + assert @my_module.is_due_in?(DateTime.now, 2.hours) + end + + test "should get archived results" do + archived_results = @my_module.archived_results + assert_equal 1, archived_results.size + end + + test "should get downstream modules" do + skip + end + + test "should get samples in JSON format" do + skip + end + + test "should deep clone module" do + skip + end + + test "should save log message" do + message = "This is test message for my module" + @my_module.log(message) + log_message = Log.last.message + assert_equal log_message[59..-1], message + end +end diff --git a/test/models/organization_test.rb b/test/models/organization_test.rb new file mode 100644 index 000000000..a203ae0f7 --- /dev/null +++ b/test/models/organization_test.rb @@ -0,0 +1,42 @@ +require 'test_helper' + +class OrganizationTest < ActiveSupport::TestCase + def setup + @org = organizations(:test) + end + + test "should validate organization default values" do + assert @org.valid? + end + + test "should have non-blank name" do + @org.name = "" + assert @org.invalid?, "Organization with blank name returns valid? = true" + end + + test "should have short name" do + @org.name = "k" * 101 + assert @org.invalid?, "Organization with name too long returns valid? = true" + end + + test "should have space_taken present" do + @org.space_taken = nil + assert @org.invalid?, "Organization without space_taken returns valid? = true" + end + + test "space_taken_defaults_to_value" do + org = Organization.new + assert_equal MINIMAL_ORGANIZATION_SPACE_TAKEN, org.space_taken + end + + test "should save log message" do + message = "This is test message" + @org.log(message) + log_message = Log.last.message[28..-1] + assert_equal log_message, message + end + + test "should open spreadsheet file" do + skip + end +end diff --git a/test/models/project_comment_test.rb b/test/models/project_comment_test.rb new file mode 100644 index 000000000..4cf35e5d2 --- /dev/null +++ b/test/models/project_comment_test.rb @@ -0,0 +1,39 @@ +require 'test_helper' + +class ProjectCommentTest < ActiveSupport::TestCase + def setup + @project_comment = project_comments(:test) + @project = @project_comment.project + @comment = @project_comment.comment + end + + test "should validate with correct data" do + assert @project_comment.valid? + end + + test "should not validate with non existent comment id" do + @project_comment.comment_id = 2343434 + assert_not @project_comment.valid? + @project_comment.comment = nil + assert_not @project_comment.valid? + end + + test "should not validate with non existent project id" do + @project_comment.project_id = 1223232323 + assert_not @project_comment.valid? + @project_comment.project = nil + assert_not @project_comment.valid? + end + + test "should validate for project/comment uniqueness" do + project_comment = ProjectComment.new( + project: @project, comment: @comment) + assert_not project_comment.save + end + + test "should have association project -> comment" do + project = projects(:dummy) + project.comments << @comment + assert_equal @comment, Project.find(project.id).comments.first, "There is no association between project -> comment." + end +end diff --git a/test/models/project_test.rb b/test/models/project_test.rb new file mode 100644 index 000000000..8bbf46c3f --- /dev/null +++ b/test/models/project_test.rb @@ -0,0 +1,79 @@ +require 'test_helper' +require 'helpers/archivable_model_test_helper' +require 'helpers/searchable_model_test_helper' + +class ProjectTest < ActiveSupport::TestCase + include ArchivableModelTestHelper + include SearchableModelTestHelper + + def setup + @project = projects(:test1) + @project2 = projects(:test2) + @project3 = projects(:test3) + end + + test "should have non-blank name" do + @project.name = "" + assert @project.invalid?, "Project with blank name returns valid? = true" + end + + test "should have short name" do + @project.name = "k" * 31 + assert @project.invalid?, "Project with name too long returns valid? = true" + end + + test "should have long enough name" do + @project.name = "k" * 3 + assert @project.invalid?, "Project with name too short returns valid? = true" + end + + test "should have organization-wide unique name" do + @project.name = @project2.name + assert @project.invalid?, "Project with non-unique organization-wide name returns valid? = true" + end + + test "should not have non-organization-wide unique name" do + @project.name = @project3.name + assert @project.valid?, "Project with non-unique name in different organizations returns valid? = false" + end + + test "should have default visibility & archived" do + project = Project.new( + name: "sample project", + organization_id: organizations(:biosistemika).id) + assert project.hidden?, "Project by default doesn't have visibility = hidden set" + assert_not project.archived?, "Project has default archived = true" + end + + test "should belong to organization" do + @project.organization = nil + assert_not @project.valid?, "Project without organization returns valid? = true" + @project.organization_id = 12321321 + assert_not @project.valid?, "Project with organization returls valid? = false" + end + + test "should have archived set" do + project = Project.new( + name: "test project", + visibility: 1, + organization_id: organizations(:biosistemika).id + ) + assert_archived_present(project) + assert_active_is_inverse_of_archived(project) + end + + test "archiving should work" do + user = users(:steve) + project = Project.new( + name: "test project", + visibility: 1, + organization_id: organizations(:biosistemika).id, + ) + project.save + archive_and_restore_action_test(project, user) + end + + test "where_attributes_like should work" do + attributes_like_test(Project, :name, "star") + end +end diff --git a/test/models/report_element_test.rb b/test/models/report_element_test.rb new file mode 100644 index 000000000..10224189b --- /dev/null +++ b/test/models/report_element_test.rb @@ -0,0 +1,103 @@ +require 'test_helper' + +class ReportElementTest < ActiveSupport::TestCase + + test "should validate with valid data" do + @re = generate_new_el(true) + assert @re.valid?, "Valid report element is not valid" + end + + test "should not validate with invalid position" do + @re = generate_new_el(true) + @re.position = nil + assert_not @re.valid?, "Report element without position was valid" + end + + test "should not validate without report" do + @re = generate_new_el(true) + @re.report = nil + assert_not @re.valid?, "Report element without report was valid" + + @re.report_id = -1 + assert_not @re.valid?, "Report element with invalid report reference was valid" + end + + test "should not validate without type_of" do + @re = generate_new_el(true) + @re.type_of = nil + assert_not @re.valid?, "Report element without type_of was valid" + end + + test "test element references" do + @re = generate_new_el(true) + @re.project = nil + assert_not @re.valid?, "Report without any element reference was valid" + + @re.project = projects(:interfaces) + @re.my_module = my_modules(:list_of_samples) + assert_not @re.valid?, "Report with >1 element references was valid" + + # Test all types of elements + re_vals_list = [ + { type_of: 0, id: projects(:interfaces).id }, + { type_of: 1, id: my_modules(:list_of_samples).id }, + { type_of: 2, id: steps(:step1).id }, + { type_of: 3, id: results(:two).id, result: true }, + { type_of: 4, id: results(:four).id, result: true }, + { type_of: 5, id: results(:one).id, result: true }, + { type_of: 6, id: my_modules(:list_of_samples).id }, + { type_of: 7, id: my_modules(:list_of_samples).id }, + { type_of: 8, id: checklists(:one).id }, + { type_of: 9, id: assets(:one).id }, + { type_of: 10, id: tables(:one).id }, + { type_of: 11, id: steps(:step1).id, comments: true }, + { type_of: 12, id: results(:one).id, comments: true } + ] + + re_vals_list.each do |re_vals| + re = generate_new_el(false) + re.type_of = re_vals[:type_of] + re.set_element_reference(re_vals[:id]) + assert re.valid? + assert_equal re_vals[:id], re.element_reference.id + assert re.result? if re_vals.include? :result + assert re.comments? if re_vals.include? :comments + assert_element_reference_present re + end + end + + private + + def generate_new_el(include_reference) + re = ReportElement.new( + position: 0, + type_of: 0, + sort_order: nil, + report: reports(:one), + project: projects(:interfaces) + ) + unless include_reference then + re.project = nil + end + re + end + + def assert_element_reference_present(re) + if re.project_header? or re.project_activity? or re.project_samples? + assert re.project.present? + elsif re.my_module? or re.my_module_activity? or re.my_module_samples? + assert re.my_module.present? + elsif re.step? or re.step_comments? + assert re.step.present? + elsif re.result_asset? or re.result_table? or re.result_text? or re.result_comments? + assert re.result.present? + elsif re.step_checklist? + assert re.checklist.present? + elsif re.step_asset? + assert re.asset.present? + elsif re.step_table? + assert re.table.present? + end + end + +end diff --git a/test/models/report_test.rb b/test/models/report_test.rb new file mode 100644 index 000000000..c572d3791 --- /dev/null +++ b/test/models/report_test.rb @@ -0,0 +1,110 @@ +require 'test_helper' + +class ReportTest < ActiveSupport::TestCase + + def setup + @report = reports(:one) + @json_el = { + "type_of" => "result_comments", + "sort_order" => "desc", + "id" => results(:four).id, + } + end + + test "should validate with valid data" do + assert @report.valid?, "Report with valid data was invalid" + end + + test "should not validate with invalid name" do + @report.name = nil + assert_not @report.valid?, "Report with nil name was valid" + @report.name = "a" + assert_not @report.valid?, "Report with name too short was valid" + @report.name = "a" * 31 + assert_not @report.valid?, "Report with name too long was valid" + + # Check if uniqueness constraint works + @report2 = Report.new( + name: @report.name_was, + grouped_by: 0, + project: projects(:interfaces), + user: users(:steve) + ) + assert_not @report.valid?, "Report with same name for specific user was valid" + end + + test "should not validate with invalid description" do + @report.description = "a" * 1001 + assert_not @report.valid?, "Report with description too long was valid" + end + + test "should not validate with invalid grouped_by" do + @report.grouped_by = nil + assert_not @report.valid?, "Report with nil grouped_by was valid" + end + + test "should not validate without project" do + @report.project = nil + assert_not @report.valid?, "Report without project reference was valid" + end + + test "should not validate without user" do + @report.user = nil + assert_not @report.valid?, "Report without user reference was valid" + end + + test "test root_elements function" do + elements = @report.root_elements + pos = -10000 + elements.each do |element| + assert element.position >= pos, "Function root_elements doesn't sort elements properly" + pos = element.position + + assert element.parent.blank?, "Function root_elements doesn't return elements without parents" + end + end + + test "test save_with_contents function" do + # We shall only test if saving fails for sinigle json element variants + # (saving of report itself was handled in previous tests). + @report2 = new_valid_report + @json_el2 = @json_el.deep_dup + @json_el2.delete("type_of") + assert_not @report2.save_with_contents([@json_el2]), "Report with invalid json_element (without type_of) was saved" + + @report2 = new_valid_report + @json_el2 = @json_el.deep_dup + @json_el2["type_of"] = "tralala" + assert_not @report2.save_with_contents([@json_el2]), "Report with invalid json_element (invalid type_of) was saved" + + @report2 = new_valid_report + @json_el2 = @json_el.deep_dup + @json_el2["sort_order"] = "tralala" + assert_not @report2.save_with_contents([@json_el2]), "Report with invalid json_element (invalid sort_order) was saved" + + @report2 = new_valid_report + @json_el2 = @json_el.deep_dup + @json_el2.delete("id") + assert_not @report2.save_with_contents([@json_el2]), "Report with invalid json_element (without id) was saved" + + @report2 = new_valid_report + @json_el2 = @json_el.deep_dup + @json_el2["id"] = -1 + assert_not @report2.save_with_contents([@json_el2]), "Report with invalid json_element (invalid id) was saved" + + @report2 = new_valid_report + @json_el2 = @json_el.deep_dup + assert @report2.save_with_contents([@json_el2]), "Report with valid json_element was not saved" + end + + private + + def new_valid_report + Report.new( + name: "report 2", + grouped_by: 0, + project: projects(:interfaces), + user: users(:steve) + ) + end +end diff --git a/test/models/result_asset_test.rb b/test/models/result_asset_test.rb new file mode 100644 index 000000000..bff094c92 --- /dev/null +++ b/test/models/result_asset_test.rb @@ -0,0 +1,32 @@ +require 'test_helper' +require 'helpers/fake_test_helper' + +class ResultAssetTest < ActiveSupport::TestCase + include FakeTestHelper + + def setup + @result_asset = result_assets(:test) + end + + test "should not validate with non existent asset_id" do + @result_asset.asset_id = 1231295 + assert_not @result_asset.valid? + @result_asset.asset = nil + assert_not @result_asset.valid? + end + + test "should not validate with non existent result_id" do + @result_asset.result_id = 123123 + assert_not @result_asset.valid? + @result_asset.result = nil + assert_not @result_asset.valid? + end + + test "should have association result -> asset" do + result = results(:two) + asset = Asset.new(file: generate_csvfile) + result.asset = asset + assert result.save + assert_equal asset, Result.find(result.id).asset, "There is no association between result -> asset." + end +end diff --git a/test/models/result_comment_test.rb b/test/models/result_comment_test.rb new file mode 100644 index 000000000..44fd2fc2e --- /dev/null +++ b/test/models/result_comment_test.rb @@ -0,0 +1,38 @@ +require 'test_helper' + +class ResultCommentTest < ActiveSupport::TestCase + def setup + @result_comment = result_comments(:test) + end + + test "should validate with correct data" do + assert @result_comment.valid? + end + + test "should not validate with non existent comment id" do + @result_comment.comment_id = 2343434 + assert_not @result_comment.valid? + @result_comment.comment = nil + assert_not @result_comment.valid? + end + + test "should not validate with non existent result id" do + @result_comment.result_id = 1223232323 + assert_not @result_comment.valid? + @result_comment.result = nil + assert_not @result_comment.valid? + end + + test "should validate uniqueness" do + result_comment = ResultComment.new( + result: @result_comment.result, comment: @result_comment.comment) + assert_not result_comment.save + end + + test "should destroy dependent comments" do + result_comment = result_comments(:one) + assert Comment.find(result_comment.comment_id) + assert result_comment.destroy + assert_not Comment.find_by_id(result_comment.comment_id) + end +end diff --git a/test/models/result_table_test.rb b/test/models/result_table_test.rb new file mode 100644 index 000000000..67d92dfec --- /dev/null +++ b/test/models/result_table_test.rb @@ -0,0 +1,48 @@ +require 'test_helper' + +class ResultTableTest < ActiveSupport::TestCase + def setup + @result_table = result_tables(:test) + end + + test "should validate with correct data" do + assert @result_table.valid? + end + + test "should not validate with non existent result_id" do + @result_table.result_id = 123123 + assert_not @result_table.valid? + @result_table.result = nil + assert_not @result_table.valid? + end + + test "should not validate with non existent table_id" do + @result_table.table_id = 12321321 + assert_not @result_table.valid? + @result_table.table = nil + assert_not @result_table.valid? + end + + test "should have association result -> table" do + result = Result.new( + name: "Result test", + user: users(:steve), + my_module: my_modules(:list_of_samples)) + table = tables(:test) + + assert_nil result.asset + assert_nil result.table + assert_nil result.result_text + + result.table = table + result.save + assert_equal table, Result.find(result.id).table + end + + test "should destroy dependent tables" do + result_table = result_tables(:one) + assert Table.find(result_table.table_id) + assert result_table.destroy + assert_not Table.find_by_id(result_table.table_id) + end +end diff --git a/test/models/result_test.rb b/test/models/result_test.rb new file mode 100644 index 000000000..4b9cf455f --- /dev/null +++ b/test/models/result_test.rb @@ -0,0 +1,160 @@ +require 'test_helper' +require 'helpers/archivable_model_test_helper' +require 'helpers/searchable_model_test_helper' + +class ResultTest < ActiveSupport::TestCase + include ArchivableModelTestHelper + include SearchableModelTestHelper + + def setup + @result = results(:test_result) + end + + test "should be valid with correct data" do + assert @result.valid?, @result.errors.messages + end + + test "should validate with blank text" do + @result.name = "" + assert @result.valid? + @result.name = " " + assert @result.valid? + @result.name = nil + assert @result.valid? + end + + test "should validate name length" do + @result.name *= 50 + assert_not @result.valid? + end + + test "should not validate with non existent user" do + @result.user_id = 12321321 + assert_not @result.valid? + @result.user = nil + assert_not @result.valid? + end + + test "should not validate with non existent my_module" do + @result.my_module_id = 1231232 + assert_not @result.valid? + @result.my_module = nil + assert_not @result.valid? + end + + test "should not validate with having no text, asset nor table" do + result = Result.new( + user: users(:steve), + my_module: my_modules(:sample_prep)) + assert_not result.valid?, "Result should not be valid without text, asset nor table set." + end + + test "should not validate with being text, asset, table at the same time" do + result = results(:no_items) + result.result_text = result_texts(:one) + assert result.valid?, "Result should be valid with only result_text." + + result.asset = assets(:one) + assert_not result.valid? "Result should not be valid with both result_text and asset." + + result.asset = nil + result.table = Table.new(contents: "test") + assert_not result.valid?, "Result should not be valid with all types assigned." + end + + test "should validate with only asset present" do + result = results(:no_items) + result.asset = assets(:one) + assert result.valid? + end + + test "should have archived set" do + result = results(:no_items) + result.asset = assets(:one) + assert_archived_present(result) + assert_active_is_inverse_of_archived(result) + end + + test "archiving should work" do + result = results(:no_items) + result.asset = assets(:one) + result.save + archive_and_restore_action_test(result, result.user) + end + + test "where_attributes_like should work" do + attributes_like_test(Result, :name, "text nr. 1") + end + + test "should test for asset type of result" do + result = results(:no_items) + assert_not result.is_asset + result.asset = assets(:one) + assert result.is_asset + end + + test "should test for table type of result" do + result = results(:no_items) + assert_not result.is_table + result.table = tables(:test) + assert result.is_table + end + + test "should test for text type of result" do + result = results(:no_items) + assert_not result.is_text + result.result_text = result_texts(:one) + assert result.is_text + end + + test "should get last comments" do + last_comments = results(:test2).last_comments + first_comment = comments(:test_result_comment_24) + last_comment = comments(:test_result_comment_5) + assert_equal 20, last_comments.size + assert_equal first_comment, last_comments.first + assert_equal last_comment, last_comments.last + end + + # Not possible to test with fixtures and random id values + test "should get last comments before specific comment" do + skip + end + + test "should get last comments of specified length" do + last_comments = results(:test2).last_comments(0, 5) + first_comment = comments(:test_result_comment_24) + last_comment = comments(:test_result_comment_20) + assert_equal 5, last_comments.size + assert_equal first_comment, last_comments.first + assert_equal last_comment, last_comments.last + end + + test "should search for results of user" do + search_results = Result.search(users(:steve), false) + assert_equal 7, search_results.size + end + + test "should search archived results of user" do + search_results = Result.search(users(:steve), true) + assert_equal 8, search_results.size + end + + test "should search results by name" do + search_results = Result.search(users(:steve), false, "table") + assert_equal 1, search_results.size + end + + test "should search archived results by name" do + search_results = Result.search(users(:steve), true, "table") + assert_equal 2, search_results.size + end + + test "should have association result -> comment" do + num_of_comments = @result.comments.size + comment = comments(:one) + @result.comments << comment + assert_equal comment, Result.find(@result.id).comments.last, "There is no association between result -> comment." + assert_equal num_of_comments + 1, @result.comments.size + end +end diff --git a/test/models/result_text_test.rb b/test/models/result_text_test.rb new file mode 100644 index 000000000..21c653385 --- /dev/null +++ b/test/models/result_text_test.rb @@ -0,0 +1,39 @@ +require 'test_helper' + +class ResultTextTest < ActiveSupport::TestCase + def setup + @result_text = result_texts(:test) + end + + test "should validate with correct data" do + assert @result_text.valid? + end + + test "should not validate without text" do + @result_text.text = "" + assert_not @result_text.valid? + @result_text.text = nil + assert_not @result_text.valid? + end + + test "should not validate with non existent result" do + @result_text.result_id = 1232132 + assert_not @result_text.valid? + @result_text.result = nil + assert_not @result_text.valid? + end + + test "should have association result -> result_text" do + result = Result.new( + name: "Result test", + user: users(:steve), + my_module: my_modules(:list_of_samples)) + result_text = ResultText.new( + text: "test") + + assert_nil result.result_text + result.result_text = result_text + result.save + assert_equal result_text, Result.find(result.id).result_text + end +end diff --git a/test/models/sample_comment_test.rb b/test/models/sample_comment_test.rb new file mode 100644 index 000000000..c1d1b4c36 --- /dev/null +++ b/test/models/sample_comment_test.rb @@ -0,0 +1,40 @@ +require 'test_helper' + +class SampleCommentTest < ActiveSupport::TestCase + def setup + @sample_comment = sample_comments(:one) + @user = users(:nora) + @sample = samples(:sample1) + end + + test "should validate with correct data" do + assert @sample_comment.valid? + end + + test "should not validate with non existent comment id" do + @sample_comment.comment_id = 2343434 + assert_not @sample_comment.valid? + @sample_comment.comment = nil + assert_not @sample_comment.valid? + end + + test "should not validate with non existent sample id" do + @sample_comment.sample_id = 1223232323 + assert_not @sample_comment.valid? + @sample_comment.sample = nil + assert_not @sample_comment.valid? + end + + test "should allow only unique associations" do + sample_comment = SampleComment.new + sample_comment.sample = @sample_comment.sample + sample_comment.comment = @sample_comment.comment + assert_not sample_comment.save + end + + test "should have association sample -> comment" do + comment = comments(:unassociated) + @sample.comments << comment + assert_equal comment, Sample.find(@sample.id).comments.last, "There is no association between sample -> comment." + end +end diff --git a/test/models/sample_custom_field_test.rb b/test/models/sample_custom_field_test.rb new file mode 100644 index 000000000..35e831ea9 --- /dev/null +++ b/test/models/sample_custom_field_test.rb @@ -0,0 +1,37 @@ +require 'test_helper' + +class SampleCustomFieldTest < ActiveSupport::TestCase + def setup + @sample_custom_field = sample_custom_fields(:one) + end + + test "should validate with correct data" do + assert @sample_custom_field.valid? + end + + test "should not validate without value" do + @sample_custom_field.value = "" + assert_not @sample_custom_field.valid? + @sample_custom_field.value = nil + assert_not @sample_custom_field.valid? + end + + test "should validate to long value length" do + @sample_custom_field.value *= 100 + assert_not @sample_custom_field.valid? + end + + test "should not validate with non existent custom field" do + @sample_custom_field.custom_field_id = 123421321 + assert_not @sample_custom_field.valid? + @sample_custom_field.custom_field = nil + assert_not @sample_custom_field.valid? + end + + test "should not validate with non existent sample" do + @sample_custom_field.sample_id = 12313213 + assert_not @sample_custom_field.valid? + @sample_custom_field.sample = nil + assert_not @sample_custom_field.valid? + end +end diff --git a/test/models/sample_group_test.rb b/test/models/sample_group_test.rb new file mode 100644 index 000000000..1bfabb304 --- /dev/null +++ b/test/models/sample_group_test.rb @@ -0,0 +1,40 @@ +require 'test_helper' + +class SampleGroupTest < ActiveSupport::TestCase + def setup + @sample_group = sample_groups(:blood) + end + + test "should validate with correct data" do + assert @sample_group.valid? + end + + test "should not validate without name" do + @sample_group.name = "" + assert_not @sample_group.valid? + @sample_group.name = nil + assert_not @sample_group.valid? + end + + test "should validate too long name value" do + @sample_group.name *= 50 + assert_not @sample_group.valid? + end + + test "should validate without color because of default value" do + @sample_group.color = "" + assert_not @sample_group.valid? + @sample_group.color = nil + assert_not @sample_group.valid? + end + + test "should validate too long color value" do + @sample_group.color *= 7 + assert_not @sample_group.valid? + end + + test "should not validate without organization" do + @sample_group.organization = nil + assert_not @sample_group.valid? + end +end diff --git a/test/models/sample_my_module_test.rb b/test/models/sample_my_module_test.rb new file mode 100644 index 000000000..821afa26c --- /dev/null +++ b/test/models/sample_my_module_test.rb @@ -0,0 +1,43 @@ +require 'test_helper' + +class SampleMyModuleTest < ActiveSupport::TestCase + def setup + @sample_module = sample_my_modules(:one) + end + + test "should validate with correct data" do + assert @sample_module.valid? + end + + test "should not validate with non existent sample" do + @sample_module.sample_id = 123123213 + assert_not @sample_module.valid? + @sample_module.sample = nil + assert_not @sample_module.valid? + end + + test "should not validate with non existent my_module" do + @sample_module.my_module_id = 12312312 + assert_not @sample_module.valid? + @sample_module.my_module = nil + assert_not @sample_module.valid? + end + + test "should have association my_module <-> sample" do + sample = Sample.create( + name: "test sample", + user: users(:jlaw), + organization: organizations(:biosistemika)) + my_module = MyModule.create( + name: "test module", + project: projects(:interfaces), + my_module_group: my_module_groups(:wf1) + ) + + assert_empty sample.my_modules + assert_empty my_module.samples + + my_module.samples << sample + assert_equal sample, MyModule.find(my_module.id).samples.first + end +end diff --git a/test/models/sample_test.rb b/test/models/sample_test.rb new file mode 100644 index 000000000..fe7bd8f59 --- /dev/null +++ b/test/models/sample_test.rb @@ -0,0 +1,55 @@ +require 'test_helper' +require 'helpers/searchable_model_test_helper' + +class SampleTest < ActiveSupport::TestCase + include SearchableModelTestHelper + + def setup + @sample = samples(:sample1) + @user = users(:jlaw) + end + + test "should validate with correct data" do + assert @sample.valid? + end + + test "should not validate without name" do + @sample.name = nil + assert_not @sample.valid? + @sample.name = "" + assert_not @sample.valid? + end + + test "should not validate with to long name" do + @sample.name *= 50 + assert_not @sample.valid? + end + + test "should not validate with non existent user" do + @sample.user_id = 1232132 + assert_not @sample.valid? + @sample.user = nil + assert_not @sample.valid? + end + + test "should not validate with non existent organization" do + @sample.organization_id = 1231232 + assert_not @sample.valid? + @sample.organization = nil + assert_not @sample.valid? + end + + test "where_attributes_like should work" do + attributes_like_test(Sample, :name, "dna") + end + + test "should get user's samples" do + samples = Sample.search(@user, false) + assert_equal 5, samples.size + end + + test "should search user's samples by name" do + samples = Sample.search(@user, false, "test") + assert_equal 2, samples.size + end +end diff --git a/test/models/sample_type_test.rb b/test/models/sample_type_test.rb new file mode 100644 index 000000000..2a31bddd2 --- /dev/null +++ b/test/models/sample_type_test.rb @@ -0,0 +1,30 @@ +require 'test_helper' + +class SampleTypeTest < ActiveSupport::TestCase + def setup + @sample_type = sample_types(:skin) + end + + test "should validate with correct data" do + assert @sample_type.valid? + end + + test "should not validate without name" do + @sample_type.name = "" + assert_not @sample_type.valid? + @sample_type.name = nil + assert_not @sample_type.valid? + end + + test "should validate too long name value" do + @sample_type.name *= 50 + assert_not @sample_type.valid? + end + + test "should not validate without organization" do + @sample_type.organization_id = 12321321 + assert_not @sample_type.valid? + @sample_type.organization = nil + assert_not @sample_type.valid? + end +end diff --git a/test/models/step_asset_test.rb b/test/models/step_asset_test.rb new file mode 100644 index 000000000..b10581540 --- /dev/null +++ b/test/models/step_asset_test.rb @@ -0,0 +1,35 @@ +require 'helpers/fake_test_helper' + +class StepAssetTest < ActiveSupport::TestCase + include FakeTestHelper + + def setup + @step = steps(:empty) + @step_asset = step_assets(:test) + end + + test "should not validate with non existent asset_id" do + @step_asset.asset_id = 1231295 + assert_not @step_asset.valid? + @step_asset.asset = nil + assert_not @step_asset.valid? + end + + test "should not validate with non existent step_id" do + @step_asset.step_id = 1232132 + assert_not @step_asset.valid? + @step_asset.step = nil + assert_not @step_asset.valid? + end + + test "should have association step -> asset" do + assert_empty @step.assets + + asset = Asset.new(file: generate_csvfile) + asset.step = @step + asset.save + + @step.assets << asset + assert_equal asset, Step.find(@step.id).assets.first, "There is no association between step -> asset." + end +end diff --git a/test/models/step_comment_test.rb b/test/models/step_comment_test.rb new file mode 100644 index 000000000..f7781ae0c --- /dev/null +++ b/test/models/step_comment_test.rb @@ -0,0 +1,34 @@ +require 'test_helper' + +class StepCommentTest < ActiveSupport::TestCase + + def setup + @step = steps(:empty) + @step_comment = step_comments(:test) + @comment = comments(:test) + end + + test "should validate with correct data" do + assert @step_comment.valid? + end + + test "should not validate with non existent comment id" do + @step_comment.comment_id = 2343434 + assert_not @step_comment.valid? + @step_comment.comment = nil + assert_not @step_comment.valid? + end + + test "should not validate with non existent step id" do + @step_comment.step_id = 1223232323 + assert_not @step_comment.valid? + @step_comment.step = nil + assert_not @step_comment.valid? + end + + test "should have association steps -> comment" do + assert_empty @step.comments + @step.comments << @comment + assert_equal @comment, Step.find(@step.id).comments.first, "There is no association between step -> comment." + end +end diff --git a/test/models/step_table_test.rb b/test/models/step_table_test.rb new file mode 100644 index 000000000..652df3495 --- /dev/null +++ b/test/models/step_table_test.rb @@ -0,0 +1,32 @@ +require 'test_helper' + +class StepTableTest < ActiveSupport::TestCase + def setup + @user = users(:jlaw) + @step = steps(:test) + @step_table = step_tables(:test) + @table = tables(:test) + end + + test "should validate with correct data" do + assert @step_table.valid? + end + + test "should not validate with non existent step_id" do + @step_table.step_id = 123123 + assert_not @step_table.valid? + end + + test "should not validate with non existent table_id" do + @step_table.table_id = 12321321 + assert_not @step_table.valid? + end + + test "should have association step -> table" do + step = steps(:empty) + assert_empty step.tables + + step.tables << @table + assert_equal @table, Step.find(step.id).tables.first + end +end diff --git a/test/models/step_test.rb b/test/models/step_test.rb new file mode 100644 index 000000000..7cba43500 --- /dev/null +++ b/test/models/step_test.rb @@ -0,0 +1,122 @@ +require 'test_helper' +require 'helpers/searchable_model_test_helper' + +class StepTest < ActiveSupport::TestCase + include SearchableModelTestHelper + + def setup + @step = steps(:test2) + end + + test "should not validate without name" do + assert @step.valid? + @step.name = "" + assert_not @step.valid? + end + + test "should not validate with to long name" do + assert @step.valid? + @step.name *= 50 + assert_not @step.valid? + end + + test "should validate without description" do + assert @step.valid? + @step.description = "" + assert @step.valid? + end + + test "should not validate with to long description" do + assert @step.valid? + @step.name *= 1000 + assert_not @step.valid? + end + + test "should not validate without position" do + assert @step.valid? + @step.position = nil + assert_not @step.valid? + end + + test "should not validate without completed" do + assert @step.valid? + @step.completed = nil + assert_not @step.valid? + end + + test "should not validate with completed=true and completed not present" do + assert @step.valid? + @step.completed = true + assert_not @step.valid? + end + + test "should validate with completed=true and completed present" do + assert @step.valid? + @step.completed = true + @step.completed_on = "2015-07-21" + assert @step.valid? + end + + test "should validate with non existent user" do + assert @step.valid? + @step.user_id = 123123 + assert_not @step.valid? + @step.user = nil + assert_not @step.valid? + end + + test "should not validate with non existing my_module" do + assert @step.valid? + @step.my_module_id = 12312321 + assert_not @step.valid? + @step.my_module = nil + assert_not @step.valid? + end + + test "where_attributes_like should work" do + attributes_like_test(Result, :name, "mrna") + end + + +# Testing last_comments method + + test "should get last comments" do + last_comments = steps(:test2).last_comments + first_comment = comments(:test_step_comment_24) + last_comment = comments(:test_step_comment_5) + assert_equal 20, last_comments.size + assert_equal first_comment, last_comments.first + assert_equal last_comment, last_comments.last + end + + # Not possible to test with fixtures and random id values + test "should get last comments before specific comment" do + end + + test "should get last comments of specified length" do + last_comments = steps(:test2).last_comments(0, 5) + first_comment = comments(:test_step_comment_24) + last_comment = comments(:test_step_comment_20) + assert_equal 5, last_comments.size + assert_equal first_comment, last_comments.first + assert_equal last_comment, last_comments.last + end + + +# Testing destroy_activity callback + + test "should create new activity for step_remove" do + last_activity = Activity.last + user = users(:jlaw) + assert @step.destroy(user) + created_activity = Activity.last + assert_not_equal last_activity, created_activity + assert_equal "destroy_step", created_activity.type_of + assert_equal user, created_activity.user + end + + +# Testing save method + + # TODO check last_modified_by for step tables, assets and checklists +end diff --git a/test/models/table_test.rb b/test/models/table_test.rb new file mode 100644 index 000000000..9814873c7 --- /dev/null +++ b/test/models/table_test.rb @@ -0,0 +1,37 @@ +require 'test_helper' + +class TableTest < ActiveSupport::TestCase + + def setup + @table = tables(:one) + end + + test "should validate with correct data" do + assert @table.valid? + end + + test "should not validate without content" do + @table.contents = nil + assert_not @table.save, "Table was created without content." + end + + test "should not allow tables larger than 20MB" do + content = generate_string(21) + table = Table.new(contents: content) + assert_not table.valid? + end + + test "should allow tables <= 20MB" do + content = generate_string(20) + table = Table.new(contents: content) + assert table.valid? + end + + private + # Generates string of size size_in_mb + def generate_string(size_in_mb) + require 'securerandom' + one_megabyte = 2 ** 20 + SecureRandom.random_bytes(size_in_mb * one_megabyte) + end +end diff --git a/test/models/tag_test.rb b/test/models/tag_test.rb new file mode 100644 index 000000000..daf733307 --- /dev/null +++ b/test/models/tag_test.rb @@ -0,0 +1,66 @@ +require 'test_helper' +require 'helpers/searchable_model_test_helper' + +class TagTest < ActiveSupport::TestCase + include SearchableModelTestHelper + + def setup + @tag = tags(:urgent) + @user = users(:steve) + end + + test "should not validate without name" do + assert @tag.valid? + @tag.name = nil + assert_not @tag.valid? + end + + test "should not have name too long" do + assert @tag.valid? + @tag.name *= 50 + assert_not @tag.valid? + end + + test "should not validate without color" do + assert @tag.valid? + @tag.color = nil + assert_not @tag.valid? + end + + test "should not validate without project" do + assert @tag.valid? + @tag.project_id = 0 + assert_not @tag.valid? + @tag.project = nil + assert_not @tag.valid? + end + + test "where_attributes_like should work" do + attributes_like_test(Tag, :name, "to") + end + + test "should get user's tags" do + tags = Tag.search(@user, false) + assert_equal 4, tags.size + end + + test "should get user's tags including archived" do + tags = Tag.search(@user, true) + assert_equal 5, tags.size + end + + test "should search user's tags by name" do + tags = Tag.search(@user, false, "do") + assert_equal 1, tags.size + end + + test "should search user's tags by color" do + tags = Tag.search(@user, false, "classified") + assert_equal 1, tags.size + end + + test "should search user's tags by color including archived" do + tags = Tag.search(@user, true, "old") + assert_equal 1, tags.size + end +end diff --git a/test/models/temp_file_test.rb b/test/models/temp_file_test.rb new file mode 100644 index 000000000..30ae60398 --- /dev/null +++ b/test/models/temp_file_test.rb @@ -0,0 +1,13 @@ +require 'test_helper' + +class TempFileTest < ActiveSupport::TestCase + + def setup + @temp_file = temp_files(:one) + end + + test "should not save temp file without session" do + @temp_file.session_id = nil + assert_not @temp_file.save, "Saved temp file without session" + end +end diff --git a/test/models/user_my_module_test.rb b/test/models/user_my_module_test.rb new file mode 100644 index 000000000..593708e45 --- /dev/null +++ b/test/models/user_my_module_test.rb @@ -0,0 +1,32 @@ +require 'test_helper' + +class UserMyModuleTest < ActiveSupport::TestCase + + test "should validate with correct data" do + assert user_my_modules(:one).valid? + assert user_my_modules(:two).valid? + assert user_my_modules(:three).valid? + end + + test "should not save user module without user" do + user_my_modules(:without_user).user = nil + + assert_not user_my_modules(:without_user).save, + "Saved user module without user" + end + + test "should not validate with non existing user" do + assert_not user_my_modules(:non_existing_user).valid? + end + + test "should not save user module without module" do + user_my_modules(:without_module).my_module = nil + + assert_not user_my_modules(:without_module).save, + "Saved user module without module" + end + + test "should not validate with non existing my_module" do + assert_not user_my_modules(:non_existing_module).valid? + end +end diff --git a/test/models/user_organization_test.rb b/test/models/user_organization_test.rb new file mode 100644 index 000000000..8e951463e --- /dev/null +++ b/test/models/user_organization_test.rb @@ -0,0 +1,67 @@ +require 'test_helper' + +class UserOrganizationTest < ActiveSupport::TestCase + def setup + @user_org = user_organizations(:one) + end + +# Test role attribute + test "should not save user organization without role" do + assert_not user_organizations(:without_role).save, + "Saved user organization without role" + end + + test "should have default role" do + assert @user_org.normal_user?, + "User organization does not have default normal_user role" + end + + test "should set valid role values" do + assert_nothing_raised(ArgumentError, + "User organization role was set with invalid role value") { + @user_org.role = 0 + @user_org.role = 1 + @user_org.role = 2 + @user_org.role = "guest" + @user_org.role = "normal_user" + @user_org.role = "admin" + } + end + + test "should not have undefined role" do + assert_raises(ArgumentError, + "User organization role can not be set to undefined numeric role value") { + @user_org.role = 5 + } + assert_raises(ArgumentError, + "User organization role can not be set to undefined role value") { + @user_org.role = "gatekeeper" + } + end + +# Test user attribute + test "should not save user organization without user" do + assert_not user_organizations(:without_user).save, + "Saved user organization without user" + end + + test "should not associate unexisting user" do + assert_raises(ActiveRecord::RecordInvalid, + "User organization saved unexisting user association") { + user_organizations(:with_invalid_user).save! + } + end + +# Test organization attribute + test "should not save user organization without organization" do + assert_not user_organizations(:without_organization).save, + "Saved user organization without organization" + end + + test "should not associate unexisting organization" do + assert_raises(ActiveRecord::RecordInvalid, + "User organization saved unexisting organization association") { + user_organizations(:with_invalid_organization).save! + } + end +end diff --git a/test/models/user_project_test.rb b/test/models/user_project_test.rb new file mode 100644 index 000000000..3a735d620 --- /dev/null +++ b/test/models/user_project_test.rb @@ -0,0 +1,88 @@ +require 'test_helper' + +class UserProjectTest < ActiveSupport::TestCase + def setup + @user_proj = user_projects(:one) + end + +# Test role attribute + test "should not save user project without role" do + assert_not user_projects(:without_role).save, + "Saved user project without role" + end + + test "should have default role" do + assert @user_proj.owner?, "User project does not have default owner role" + end + + test "should set valid role values" do + assert_nothing_raised(ArgumentError, + "User project role was set with invalid role value") { + @user_proj.role = 0 + @user_proj.role = 1 + @user_proj.role = 2 + @user_proj.role = 3 + @user_proj.role = "owner" + @user_proj.role = "normal_user" + @user_proj.role = "technician" + @user_proj.role = "viewer" + } + end + + test "should not have undefined role" do + assert_raises(ArgumentError, + "User project role can not be set to undefined numeric role value") { + @user_proj.role = 5 + } + assert_raises(ArgumentError, + "User project role can not be set to undefined role value") { + @user_proj.role = "gatekeeper" + } + end + +# Test user attribute + test "should not save user project without user" do + assert_not user_projects(:without_user).save, + "Saved user project without user" + end + + test "should not associate unexisting user" do + assert_raises(ActiveRecord::RecordInvalid, + "User project saved unexisting user association") { + user_projects(:with_invalid_user).save! + } + end + +# Test project attribute + test "should not save user project without project" do + assert_not user_projects(:without_project).save, + "Saved user project without project" + end + + test "should not associate unexisting project" do + assert_raises(ActiveRecord::RecordInvalid, + "User project saved unexisting project association") { + user_projects(:with_invalid_project).save! + } + end + +# Test destroy_associations method + test "should unassign user from all projects' modules" do + user_project = @user_proj + user = user_project.user + + # Test associations before destroy + assert_equal 1, my_modules(:sample_prep) + .user_my_modules.select { |um| um.user == user }.count + assert_equal 1, my_modules(:rna_test) + .user_my_modules.select { |um| um.user == user }.count + + assert user_project.destroy + + # Test associations after destroy + projects(:interfaces).my_modules.each do |my_module| + assert_equal 0, my_module.user_my_modules + .select { |um| um.user == user }.count + end + end +end diff --git a/test/models/user_test.rb b/test/models/user_test.rb new file mode 100644 index 000000000..dbbc9cee0 --- /dev/null +++ b/test/models/user_test.rb @@ -0,0 +1,168 @@ +require 'test_helper' + +class UserTest < ActiveSupport::TestCase + def setup + @user = users(:john) + @user2 = users(:steve) + @org = organizations(:biosistemika) + end + +# Test full_name attribute + test "should have non-blank full_name" do + @user.full_name = "" + assert @user.invalid?, "User with blank full_name is not valid" + end + + test "should have short full_name" do + @user.full_name = "k" * 51 + assert @user.invalid?, "User with name too long is not valid" + end + +# Test initials attribute + test "should have non-blank initials" do + @user.initials = "" + assert @user.invalid?, "User with blank initials is not valid" + end + + test "should have short initials" do + @user.initials = "k" * 5 + assert @user.invalid?, "User with initials too long is not valid" + end + +# Test password attribute + test "should have non-blank password" do + @user.password = "" + assert @user.invalid?, "User with blank email is not valid" + end + + test "should have password with at least 8 characters" do + @user.password = "1234567" + assert @user.invalid?, "User with too short password is not valid" + @user.password = "12345678" + assert_not @user.invalid?, "User with password longer than 7 characters is valid" + end + +# Test email attribute + test "should have non-blank email" do + @user.email = "" + assert @user.invalid?, "User with blank email is not valid" + end + + test "should have unique email" do + @user.email = @user2.email + assert @user.invalid?, "User with non-unique email in not valid" + end + +# Test methods + test "should get projects for organization" do + org_projects = @user2.projects_by_orgs(@org.id) + assert_equal 1, org_projects.size, "Projects are grouped into one organization" + assert_equal 4, org_projects[@org].size, "Organization group has many projects" + end + + test "should get archived projects for organization" do + org_projects = @user2.projects_by_orgs(@org.id, nil, true) + assert_equal 1, org_projects.size, "Projects are grouped into one organization" + assert_equal 2, org_projects[@org].size, "Organization group has many projects" + end + + test "should sort projects by create timestamp ascending" do + org_projects = @user2.projects_by_orgs(@org.id, "old") + first_project = projects(:interfaces) + last_project = projects(:z_project) + assert_equal first_project, org_projects[@org].first + assert_equal last_project, org_projects[@org].last + end + + test "should sort projects by create timestamp descending" do + org_projects = @user2.projects_by_orgs(@org.id) + first_project = projects(:z_project) + last_project = projects(:interfaces) + assert_equal first_project, org_projects[@org].first + assert_equal last_project, org_projects[@org].last + end + + test "should sort projects by project name ascending" do + org_projects = @user2.projects_by_orgs(@org.id, "atoz") + first_project = projects(:a_project) + last_project = projects(:z_project) + assert_equal first_project, org_projects[@org].first + assert_equal last_project, org_projects[@org].last + end + + test "should sort projects by project name descending" do + org_projects = @user2.projects_by_orgs(@org.id, "ztoa") + first_project = projects(:z_project) + last_project = projects(:a_project) + assert_equal first_project, org_projects[@org].first + assert_equal last_project, org_projects[@org].last + end + + test "should sort archived projects by create timestamp ascending" do + org_projects = @user2.projects_by_orgs(@org.id, "old", true) + first_project = projects(:a_archived_project) + last_project = projects(:z_archived_project) + assert_equal first_project, org_projects[@org].first + assert_equal last_project, org_projects[@org].last + end + + test "should sort archived projects by create timestamp descending" do + org_projects = @user2.projects_by_orgs(@org.id, nil, true) + first_project = projects(:z_archived_project) + last_project = projects(:a_archived_project) + assert_equal first_project, org_projects[@org].first + assert_equal last_project, org_projects[@org].last + end + + test "should sort archived projects by project name ascending" do + org_projects = @user2.projects_by_orgs(@org.id, "atoz", true) + first_project = projects(:a_archived_project) + last_project = projects(:z_archived_project) + assert_equal first_project, org_projects[@org].first + assert_equal last_project, org_projects[@org].last + end + + test "should sort archived projects by project name descending" do + org_projects = @user2.projects_by_orgs(@org.id, "ztoa", true) + first_project = projects(:z_archived_project) + last_project = projects(:a_archived_project) + assert_equal first_project, org_projects[@org].first + assert_equal last_project, org_projects[@org].last + end + + test "should get last activities" do + last_activities = @user2.last_activities(0) + first_activity = activities(:twelve) + last_activity = activities(:three) + assert_equal 10, last_activities.size + assert_equal first_activity, last_activities.first + assert_equal last_activity, last_activities.last + end + + test "should get specified number of last activities" do + last_activities = @user2.last_activities(0, 4) + first_activity = activities(:twelve) + last_activity = activities(:nine) + assert_equal 4, last_activities.size + assert_equal first_activity, last_activities.first + assert_equal last_activity, last_activities.last + end + + test "should allow to change time zone" do + assert @user.valid? + @user.time_zone = "Ljubljana" + assert @user.valid? + end + + test "should validate time zone value" do + assert @user.valid? + @user.time_zone = "Very Strange Place on Earth" + assert_not @user.valid? + end + + test "should check if time zone value is set" do + assert @user.valid? + @user.time_zone = nil + assert_not @user.valid? + end +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 000000000..65ddfe659 --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,27 @@ +ENV['RAILS_ENV'] ||= 'test' +require File.expand_path('../../config/environment', __FILE__) +require 'rails/test_help' + +class ActiveSupport::TestCase + # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. + fixtures :all + + def assert_redirected_to_403 + assert_select "div.dialog div h1", + { count: 1, text: I18n.t("forbidden.title") }, + "Not redirected to 403" + end + + def assert_redirected_to_404 + assert_select "div.dialog div h1", + { count: 1, text: I18n.t("not_found.title") }, + "Not redirected to 404" + end +end + +class ActionController::TestCase + # Include Devise test helpers + # (must not include them in ActiveSupport, + # causes 'env' not found errors) + include Devise::TestHelpers +end \ No newline at end of file diff --git a/vendor/assets/javascripts/.keep b/vendor/assets/javascripts/.keep new file mode 100644 index 000000000..e69de29bb diff --git a/vendor/assets/javascripts/Sortable.min.js b/vendor/assets/javascripts/Sortable.min.js new file mode 100644 index 000000000..bef7fa3a1 --- /dev/null +++ b/vendor/assets/javascripts/Sortable.min.js @@ -0,0 +1,2 @@ +/*! Sortable 1.3.0 - MIT | git://github.com/rubaxa/Sortable.git */ +!function(a){"use strict";"function"==typeof define&&define.amd?define(a):"undefined"!=typeof module&&"undefined"!=typeof module.exports?module.exports=a():"undefined"!=typeof Package?Sortable=a():window.Sortable=a()}(function(){"use strict";function a(a,b){if(!a||!a.nodeType||1!==a.nodeType)throw"Sortable: `el` must be HTMLElement, and not "+{}.toString.call(a);this.el=a,this.options=b=r({},b),a[L]=this;var c={group:Math.random(),sort:!0,disabled:!1,store:null,handle:null,scroll:!0,scrollSensitivity:30,scrollSpeed:10,draggable:/[uo]l/i.test(a.nodeName)?"li":">*",ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",ignore:"a, img",filter:null,animation:0,setData:function(a,b){a.setData("Text",b.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1};for(var d in c)!(d in b)&&(b[d]=c[d]);V(b);for(var f in this)"_"===f.charAt(0)&&(this[f]=this[f].bind(this));this.nativeDraggable=b.forceFallback?!1:P,e(a,"mousedown",this._onTapStart),e(a,"touchstart",this._onTapStart),this.nativeDraggable&&(e(a,"dragover",this),e(a,"dragenter",this)),T.push(this._onDragOver),b.store&&this.sort(b.store.get(this))}function b(a){v&&v.state!==a&&(h(v,"display",a?"none":""),!a&&v.state&&w.insertBefore(v,s),v.state=a)}function c(a,b,c){if(a){c=c||N,b=b.split(".");var d=b.shift().toUpperCase(),e=new RegExp("\\s("+b.join("|")+")(?=\\s)","g");do if(">*"===d&&a.parentNode===c||(""===d||a.nodeName.toUpperCase()==d)&&(!b.length||((" "+a.className+" ").match(e)||[]).length==b.length))return a;while(a!==c&&(a=a.parentNode))}return null}function d(a){a.dataTransfer&&(a.dataTransfer.dropEffect="move"),a.preventDefault()}function e(a,b,c){a.addEventListener(b,c,!1)}function f(a,b,c){a.removeEventListener(b,c,!1)}function g(a,b,c){if(a)if(a.classList)a.classList[c?"add":"remove"](b);else{var d=(" "+a.className+" ").replace(K," ").replace(" "+b+" "," ");a.className=(d+(c?" "+b:"")).replace(K," ")}}function h(a,b,c){var d=a&&a.style;if(d){if(void 0===c)return N.defaultView&&N.defaultView.getComputedStyle?c=N.defaultView.getComputedStyle(a,""):a.currentStyle&&(c=a.currentStyle),void 0===b?c:c[b];b in d||(b="-webkit-"+b),d[b]=c+("string"==typeof c?"":"px")}}function i(a,b,c){if(a){var d=a.getElementsByTagName(b),e=0,f=d.length;if(c)for(;f>e;e++)c(d[e],e);return d}return[]}function j(a,b,c,d,e,f,g){var h=N.createEvent("Event"),i=(a||b[L]).options,j="on"+c.charAt(0).toUpperCase()+c.substr(1);h.initEvent(c,!0,!0),h.to=b,h.from=e||b,h.item=d||b,h.clone=v,h.oldIndex=f,h.newIndex=g,b.dispatchEvent(h),i[j]&&i[j].call(a,h)}function k(a,b,c,d,e,f){var g,h,i=a[L],j=i.options.onMove;return g=N.createEvent("Event"),g.initEvent("move",!0,!0),g.to=b,g.from=a,g.dragged=c,g.draggedRect=d,g.related=e||b,g.relatedRect=f||b.getBoundingClientRect(),a.dispatchEvent(g),j&&(h=j.call(i,g)),h}function l(a){a.draggable=!1}function m(){R=!1}function n(a,b){var c=a.lastElementChild,d=c.getBoundingClientRect();return(b.clientY-(d.top+d.height)>5||b.clientX-(d.right+d.width)>5)&&c}function o(a){for(var b=a.tagName+a.className+a.src+a.href+a.textContent,c=b.length,d=0;c--;)d+=b.charCodeAt(c);return d.toString(36)}function p(a){var b=0;if(!a||!a.parentNode)return-1;for(;a&&(a=a.previousElementSibling);)"TEMPLATE"!==a.nodeName.toUpperCase()&&b++;return b}function q(a,b){var c,d;return function(){void 0===c&&(c=arguments,d=this,setTimeout(function(){1===c.length?a.call(d,c[0]):a.apply(d,c),c=void 0},b))}}function r(a,b){if(a&&b)for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c]);return a}var s,t,u,v,w,x,y,z,A,B,C,D,E,F,G,H,I,J={},K=/\s+/g,L="Sortable"+(new Date).getTime(),M=window,N=M.document,O=M.parseInt,P=!!("draggable"in N.createElement("div")),Q=function(a){return a=N.createElement("x"),a.style.cssText="pointer-events:auto","auto"===a.style.pointerEvents}(),R=!1,S=Math.abs,T=([].slice,[]),U=q(function(a,b,c){if(c&&b.scroll){var d,e,f,g,h=b.scrollSensitivity,i=b.scrollSpeed,j=a.clientX,k=a.clientY,l=window.innerWidth,m=window.innerHeight;if(z!==c&&(y=b.scroll,z=c,y===!0)){y=c;do if(y.offsetWidth=l-j)-(h>=j),g=(h>=m-k)-(h>=k),(f||g)&&(d=M)),(J.vx!==f||J.vy!==g||J.el!==d)&&(J.el=d,J.vx=f,J.vy=g,clearInterval(J.pid),d&&(J.pid=setInterval(function(){d===M?M.scrollTo(M.pageXOffset+f*i,M.pageYOffset+g*i):(g&&(d.scrollTop+=g*i),f&&(d.scrollLeft+=f*i))},24)))}},30),V=function(a){var b=a.group;b&&"object"==typeof b||(b=a.group={name:b}),["pull","put"].forEach(function(a){a in b||(b[a]=!0)}),a.groups=" "+b.name+(b.put.join?" "+b.put.join(" "):"")+" "};return a.prototype={constructor:a,_onTapStart:function(a){var b=this,d=this.el,e=this.options,f=a.type,g=a.touches&&a.touches[0],h=(g||a).target,i=h,k=e.filter;if(!("mousedown"===f&&0!==a.button||e.disabled)&&(h=c(h,e.draggable,d))){if(D=p(h),"function"==typeof k){if(k.call(this,a,h,this))return j(b,i,"filter",h,d,D),void a.preventDefault()}else if(k&&(k=k.split(",").some(function(a){return a=c(i,a.trim(),d),a?(j(b,a,"filter",h,d,D),!0):void 0})))return void a.preventDefault();(!e.handle||c(i,e.handle,d))&&this._prepareDragStart(a,g,h)}},_prepareDragStart:function(a,b,c){var d,f=this,h=f.el,j=f.options,k=h.ownerDocument;c&&!s&&c.parentNode===h&&(G=a,w=h,s=c,t=s.parentNode,x=s.nextSibling,F=j.group,d=function(){f._disableDelayedDrag(),s.draggable=!0,g(s,f.options.chosenClass,!0),f._triggerDragStart(b)},j.ignore.split(",").forEach(function(a){i(s,a.trim(),l)}),e(k,"mouseup",f._onDrop),e(k,"touchend",f._onDrop),e(k,"touchcancel",f._onDrop),j.delay?(e(k,"mouseup",f._disableDelayedDrag),e(k,"touchend",f._disableDelayedDrag),e(k,"touchcancel",f._disableDelayedDrag),e(k,"mousemove",f._disableDelayedDrag),e(k,"touchmove",f._disableDelayedDrag),f._dragStartTimer=setTimeout(d,j.delay)):d())},_disableDelayedDrag:function(){var a=this.el.ownerDocument;clearTimeout(this._dragStartTimer),f(a,"mouseup",this._disableDelayedDrag),f(a,"touchend",this._disableDelayedDrag),f(a,"touchcancel",this._disableDelayedDrag),f(a,"mousemove",this._disableDelayedDrag),f(a,"touchmove",this._disableDelayedDrag)},_triggerDragStart:function(a){a?(G={target:s,clientX:a.clientX,clientY:a.clientY},this._onDragStart(G,"touch")):this.nativeDraggable?(e(s,"dragend",this),e(w,"dragstart",this._onDragStart)):this._onDragStart(G,!0);try{N.selection?N.selection.empty():window.getSelection().removeAllRanges()}catch(b){}},_dragStarted:function(){w&&s&&(g(s,this.options.ghostClass,!0),a.active=this,j(this,w,"start",s,w,D))},_emulateDragOver:function(){if(H){if(this._lastX===H.clientX&&this._lastY===H.clientY)return;this._lastX=H.clientX,this._lastY=H.clientY,Q||h(u,"display","none");var a=N.elementFromPoint(H.clientX,H.clientY),b=a,c=" "+this.options.group.name,d=T.length;if(b)do{if(b[L]&&b[L].options.groups.indexOf(c)>-1){for(;d--;)T[d]({clientX:H.clientX,clientY:H.clientY,target:a,rootEl:b});break}a=b}while(b=b.parentNode);Q||h(u,"display","")}},_onTouchMove:function(b){if(G){a.active||this._dragStarted(),this._appendGhost();var c=b.touches?b.touches[0]:b,d=c.clientX-G.clientX,e=c.clientY-G.clientY,f=b.touches?"translate3d("+d+"px,"+e+"px,0)":"translate("+d+"px,"+e+"px)";I=!0,H=c,h(u,"webkitTransform",f),h(u,"mozTransform",f),h(u,"msTransform",f),h(u,"transform",f),b.preventDefault()}},_appendGhost:function(){if(!u){var a,b=s.getBoundingClientRect(),c=h(s);u=s.cloneNode(!0),g(u,this.options.ghostClass,!1),g(u,this.options.fallbackClass,!0),h(u,"top",b.top-O(c.marginTop,10)),h(u,"left",b.left-O(c.marginLeft,10)),h(u,"width",b.width),h(u,"height",b.height),h(u,"opacity","0.8"),h(u,"position","fixed"),h(u,"zIndex","100000"),h(u,"pointerEvents","none"),this.options.fallbackOnBody&&N.body.appendChild(u)||w.appendChild(u),a=u.getBoundingClientRect(),h(u,"width",2*b.width-a.width),h(u,"height",2*b.height-a.height)}},_onDragStart:function(a,b){var c=a.dataTransfer,d=this.options;this._offUpEvents(),"clone"==F.pull&&(v=s.cloneNode(!0),h(v,"display","none"),w.insertBefore(v,s)),b?("touch"===b?(e(N,"touchmove",this._onTouchMove),e(N,"touchend",this._onDrop),e(N,"touchcancel",this._onDrop)):(e(N,"mousemove",this._onTouchMove),e(N,"mouseup",this._onDrop)),this._loopId=setInterval(this._emulateDragOver,50)):(c&&(c.effectAllowed="move",d.setData&&d.setData.call(this,c,s)),e(N,"drop",this),setTimeout(this._dragStarted,0))},_onDragOver:function(a){var d,e,f,g=this.el,i=this.options,j=i.group,l=j.put,o=F===j,p=i.sort;if(void 0!==a.preventDefault&&(a.preventDefault(),!i.dragoverBubble&&a.stopPropagation()),I=!0,F&&!i.disabled&&(o?p||(f=!w.contains(s)):F.pull&&l&&(F.name===j.name||l.indexOf&&~l.indexOf(F.name)))&&(void 0===a.rootEl||a.rootEl===this.el)){if(U(a,i,this.el),R)return;if(d=c(a.target,i.draggable,g),e=s.getBoundingClientRect(),f)return b(!0),void(v||x?w.insertBefore(s,v||x):p||w.appendChild(s));if(0===g.children.length||g.children[0]===u||g===a.target&&(d=n(g,a))){if(d){if(d.animated)return;r=d.getBoundingClientRect()}b(o),k(w,g,s,e,d,r)!==!1&&(s.contains(g)||(g.appendChild(s),t=g),this._animate(e,s),d&&this._animate(r,d))}else if(d&&!d.animated&&d!==s&&void 0!==d.parentNode[L]){A!==d&&(A=d,B=h(d),C=h(d.parentNode));var q,r=d.getBoundingClientRect(),y=r.right-r.left,z=r.bottom-r.top,D=/left|right|inline/.test(B.cssFloat+B.display)||"flex"==C.display&&0===C["flex-direction"].indexOf("row"),E=d.offsetWidth>s.offsetWidth,G=d.offsetHeight>s.offsetHeight,H=(D?(a.clientX-r.left)/y:(a.clientY-r.top)/z)>.5,J=d.nextElementSibling,K=k(w,g,s,e,d,r);if(K!==!1){if(R=!0,setTimeout(m,30),b(o),1===K||-1===K)q=1===K;else if(D){var M=s.offsetTop,N=d.offsetTop;q=M===N?d.previousElementSibling===s&&!E||H&&E:N>M}else q=J!==s&&!G||H&&G;s.contains(g)||(q&&!J?g.appendChild(s):d.parentNode.insertBefore(s,q?J:d)),t=s.parentNode,this._animate(e,s),this._animate(r,d)}}}},_animate:function(a,b){var c=this.options.animation;if(c){var d=b.getBoundingClientRect();h(b,"transition","none"),h(b,"transform","translate3d("+(a.left-d.left)+"px,"+(a.top-d.top)+"px,0)"),b.offsetWidth,h(b,"transition","all "+c+"ms"),h(b,"transform","translate3d(0,0,0)"),clearTimeout(b.animated),b.animated=setTimeout(function(){h(b,"transition",""),h(b,"transform",""),b.animated=!1},c)}},_offUpEvents:function(){var a=this.el.ownerDocument;f(N,"touchmove",this._onTouchMove),f(a,"mouseup",this._onDrop),f(a,"touchend",this._onDrop),f(a,"touchcancel",this._onDrop)},_onDrop:function(b){var c=this.el,d=this.options;clearInterval(this._loopId),clearInterval(J.pid),clearTimeout(this._dragStartTimer),f(N,"mousemove",this._onTouchMove),this.nativeDraggable&&(f(N,"drop",this),f(c,"dragstart",this._onDragStart)),this._offUpEvents(),b&&(I&&(b.preventDefault(),!d.dropBubble&&b.stopPropagation()),u&&u.parentNode.removeChild(u),s&&(this.nativeDraggable&&f(s,"dragend",this),l(s),g(s,this.options.ghostClass,!1),g(s,this.options.chosenClass,!1),w!==t?(E=p(s),E>=0&&(j(null,t,"sort",s,w,D,E),j(this,w,"sort",s,w,D,E),j(null,t,"add",s,w,D,E),j(this,w,"remove",s,w,D,E))):(v&&v.parentNode.removeChild(v),s.nextSibling!==x&&(E=p(s),E>=0&&(j(this,w,"update",s,w,D,E),j(this,w,"sort",s,w,D,E)))),a.active&&((null===E||-1===E)&&(E=D),j(this,w,"end",s,w,D,E),this.save())),w=s=t=u=x=v=y=z=G=H=I=E=A=B=F=a.active=null)},handleEvent:function(a){var b=a.type;"dragover"===b||"dragenter"===b?s&&(this._onDragOver(a),d(a)):("drop"===b||"dragend"===b)&&this._onDrop(a)},toArray:function(){for(var a,b=[],d=this.el.children,e=0,f=d.length,g=this.options;f>e;e++)a=d[e],c(a,g.draggable,this.el)&&b.push(a.getAttribute(g.dataIdAttr)||o(a));return b},sort:function(a){var b={},d=this.el;this.toArray().forEach(function(a,e){var f=d.children[e];c(f,this.options.draggable,d)&&(b[a]=f)},this),a.forEach(function(a){b[a]&&(d.removeChild(b[a]),d.appendChild(b[a]))})},save:function(){var a=this.options.store;a&&a.set(this)},closest:function(a,b){return c(a,b||this.options.draggable,this.el)},option:function(a,b){var c=this.options;return void 0===b?c[a]:(c[a]=b,void("group"===a&&V(c)))},destroy:function(){var a=this.el;a[L]=null,f(a,"mousedown",this._onTapStart),f(a,"touchstart",this._onTapStart),this.nativeDraggable&&(f(a,"dragover",this),f(a,"dragenter",this)),Array.prototype.forEach.call(a.querySelectorAll("[draggable]"),function(a){a.removeAttribute("draggable")}),T.splice(T.indexOf(this._onDragOver),1),this._onDrop(),this.el=a=null}},a.utils={on:e,off:f,css:h,find:i,is:function(a,b){return!!c(a,b,a)},extend:r,throttle:q,closest:c,toggleClass:g,index:p},a.create=function(b,c){return new a(b,c)},a.version="1.3.0",a}); \ No newline at end of file diff --git a/vendor/assets/javascripts/bootstrap-colorselector.js b/vendor/assets/javascripts/bootstrap-colorselector.js new file mode 100644 index 000000000..d0ed0f7f2 --- /dev/null +++ b/vendor/assets/javascripts/bootstrap-colorselector.js @@ -0,0 +1,137 @@ +/* + * A colorselector for Twitter Bootstrap which lets you select a color from a predefined set of colors only. + * https://github.com/flaute/bootstrap-colorselector + * + * Copyright (C) 2014 Flaute + * + * Licensed under the MIT license + */ + +(function($) { + "use strict"; + + var ColorSelector = function(select, options) { + this.options = options; + this.$select = $(select); + this._init(); + }; + + ColorSelector.prototype = { + + constructor : ColorSelector, + + _init : function() { + + var callback = this.options.callback; + + var selectValue = this.$select.val(); + var selectColor = this.$select.find("option:selected").data("color"); + + var $markupUl = $("