From fb66131e296498eebd5bffdbfef5a3cd2cb1f135 Mon Sep 17 00:00:00 2001 From: zmagod Date: Tue, 6 Mar 2018 16:32:39 +0100 Subject: [PATCH 01/14] setup endpoint for repository_rows paging/search --- app/controllers/repository_rows_controller.rb | 25 ++++++- app/helpers/repository_datatable_helper.rb | 46 ++++++++++++ app/models/repository.rb | 2 + app/models/repository_list_value.rb | 6 +- .../views/datatables/search_repository.rb | 21 ++++++ app/services/repository_datatable_service.rb | 73 +++++++++++++++++++ app/views/repository_rows/index.json.jbuilder | 6 ++ config/environments/development.rb | 2 +- config/routes.rb | 2 +- ...180306074931_create_search_repositories.rb | 5 ++ db/schema.rb | 63 +++++++++++++++- db/views/search_repositories_v01.sql | 28 +++++++ spec/models/repository_list_value_spec.rb | 58 ++++++++++++++- spec/models/search_repository_spec.rb | 5 ++ 14 files changed, 335 insertions(+), 7 deletions(-) create mode 100644 app/helpers/repository_datatable_helper.rb create mode 100644 app/models/views/datatables/search_repository.rb create mode 100644 app/services/repository_datatable_service.rb create mode 100644 app/views/repository_rows/index.json.jbuilder create mode 100644 db/migrate/20180306074931_create_search_repositories.rb create mode 100644 db/views/search_repositories_v01.sql create mode 100644 spec/models/search_repository_spec.rb diff --git a/app/controllers/repository_rows_controller.rb b/app/controllers/repository_rows_controller.rb index cb34496f3..ddb8dbd37 100644 --- a/app/controllers/repository_rows_controller.rb +++ b/app/controllers/repository_rows_controller.rb @@ -5,11 +5,23 @@ class RepositoryRowsController < ApplicationController before_action :load_info_modal_vars, only: :show before_action :load_vars, only: %i(edit update) - before_action :load_repository, only: %i(create delete_records) + before_action :load_repository, only: %i(create delete_records index) + before_action :load_columns_mappings, only: :index before_action :check_create_permissions, only: :create before_action :check_edit_permissions, only: %i(edit update) before_action :check_destroy_permissions, only: :delete_records + def index + @draw = params[:draw].to_i + per_page = params[:length] == '-1' ? 100 : params[:length].to_i + page = (params[:start].to_i / per_page) + 1 + records = RepositoryDatatableService.new(@repository, + params, + @columns_mappings) + @repository_row_count = records.repository_rows.count + @repository_rows = records.repository_rows.page(page).per(per_page) + end + def create record = RepositoryRow.new(repository: @repository, created_by: current_user, @@ -238,6 +250,17 @@ class RepositoryRowsController < ApplicationController def load_repository @repository = Repository.find_by_id(params[:repository_id]) render_404 unless @repository + render_403 unless can_read_team?(@repository.team) + end + + def load_columns_mappings + # Make mappings of custom columns, so we have same id for every column + i = 5 + @columns_mappings = {} + @repository.repository_columns.order(:id).each do |column| + @columns_mappings[column.id] = i.to_s + i += 1 + end end def check_create_permissions diff --git a/app/helpers/repository_datatable_helper.rb b/app/helpers/repository_datatable_helper.rb new file mode 100644 index 000000000..c93bdf09d --- /dev/null +++ b/app/helpers/repository_datatable_helper.rb @@ -0,0 +1,46 @@ +module RepositoryDatatableHelper + include InputSanitizeHelper + def prepare_row_columns(repository_rows, repository, columns_mappings, team) + parsed_records = [] + repository_rows.each do |record| + row = { + 'DT_RowId': record.id, + '1': assigned_row(record), + '2': escape_input(record.name), + '3': I18n.l(record.created_at, format: :full), + '4': escape_input(record.created_by.full_name), + 'recordEditUrl': + Rails.application.routes.url_helpers + .edit_repository_repository_row_path(repository, + record.id), + 'recordUpdateUrl': + Rails.application.routes.url_helpers + .repository_repository_row_path(repository, record.id), + 'recordInfoUrl': + Rails.application.routes.url_helpers.repository_row_path(record.id) + } + + # Add custom columns + # byebug + record.repository_cells.each do |cell| + row[columns_mappings[cell.repository_column.id]] = + custom_auto_link( + display_tooltip(cell.value.data, + Constants::NAME_MAX_LENGTH), + simple_format: true, + team: team + ) + end + parsed_records << row + end + parsed_records + end + + def assigned_row(record) + # if @assigned_rows && @assigned_rows.include?(record) + # " " + # else + " " + # end + end +end diff --git a/app/models/repository.rb b/app/models/repository.rb index fce0a148d..ed5775492 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -12,6 +12,8 @@ class Repository < ApplicationRecord inverse_of: :repository, dependent: :destroy has_many :report_elements, inverse_of: :repository, dependent: :destroy has_many :repository_list_items, inverse_of: :repository, dependent: :destroy + has_many :repository_searchable_rows, + class_name: '::Views::Datatables::SearchRepository' auto_strip_attributes :name, nullify: false validates :name, diff --git a/app/models/repository_list_value.rb b/app/models/repository_list_value.rb index ae92b068b..c53856809 100644 --- a/app/models/repository_list_value.rb +++ b/app/models/repository_list_value.rb @@ -13,7 +13,11 @@ class RepositoryListValue < ApplicationRecord validates :repository_cell, presence: true def formatted - return '' unless repository_list_item + data.to_s + end + + def data + return nil unless repository_list_item repository_list_item.data end end diff --git a/app/models/views/datatables/search_repository.rb b/app/models/views/datatables/search_repository.rb new file mode 100644 index 000000000..2ff6a6f84 --- /dev/null +++ b/app/models/views/datatables/search_repository.rb @@ -0,0 +1,21 @@ +module Views + module Datatables + class SearchRepository < ApplicationRecord + belongs_to :repository + # def self.records(repository, search_value) + # # binding.pry + # # # where('repository_rows.repository_id', repository.id).to_a + # # where(repository_id: repository.id) + # # .where() + # end + + private + + # this isn't strictly necessary, but it will prevent + # rails from calling save, which would fail anyway. + def readonly? + true + end + end + end +end diff --git a/app/services/repository_datatable_service.rb b/app/services/repository_datatable_service.rb new file mode 100644 index 000000000..61fe08d12 --- /dev/null +++ b/app/services/repository_datatable_service.rb @@ -0,0 +1,73 @@ +class RepositoryDatatableService + + attr_reader :repository_rows + + def initialize(repository, params, mappings) + @mappings = mappings + @repository = repository + process_query(params) + end + + private + + def process_query(params) + contitions = build_conditions(params) + if contitions[:search_value].present? + @repository_rows = search(contitions[:search_value]) + else + @repository_rows = fetch_records + end + # byebug + end + + def fetch_records + RepositoryRow.preload(:repository_columns, + :created_by, + repository_cells: :value) + .joins(:created_by) + .where(repository: @repository) + end + + def search(value) + # binding.pry + filtered_rows = @repository.repository_searchable_rows.where( + 'name ILIKE :value + OR to_char(created_at, :time) ILIKE :value + OR user_full_name ILIKE :value + OR text_value ILIKE :value + OR date_value ILIKE :value + OR list_value ILIKE :value', + value: "%#{value}%", + time: "DD.MM.YYYY HH24:MI" + ).pluck(:id) + fetch_records.where(id: filtered_rows) + end + + def build_conditions(params) + search_value = params[:search][:value] + order_by_column = { column: params[:order][:column].to_i, + dir: params[:order][:dir] } + { search_value: search_value, order_by_column: order_by_column } + end + + def sortable_columns + sort_array = [ + 'assigned', + 'RepositoryRow.name', + 'RepositoryRow.created_at', + 'User.full_name' + ] + + sort_array.push(*repository_columns_sort_by) + @sortable_columns = sort_array + end + + def repository_columns_sort_by + array = [] + @repository.repository_columns.count.times do + array << 'RepositoryCell.value' + end + array + end + +end diff --git a/app/views/repository_rows/index.json.jbuilder b/app/views/repository_rows/index.json.jbuilder new file mode 100644 index 000000000..23af2e9f3 --- /dev/null +++ b/app/views/repository_rows/index.json.jbuilder @@ -0,0 +1,6 @@ +json.draw @draw +json.recordsTotal @repository_rows.total_count +json.recordsFiltered @repository_row_count +json.data do + json.array! prepare_row_columns(@repository_rows, @repository, @columns_mappings, @repository.team) +end diff --git a/config/environments/development.rb b/config/environments/development.rb index 5f6bbe557..580ca9c48 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -75,7 +75,7 @@ Rails.application.configure do config.assets.raise_runtime_errors = true # Only log info and higher on development - config.log_level = :info + config.log_level = :debug # Only allow Better Errors to work on trusted ip, use ifconfig to see which # one you use and put it into application.yml! diff --git a/config/routes.rb b/config/routes.rb index e28efbd58..e550e303d 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -449,7 +449,7 @@ Rails.application.routes.draw do resources :repositories do post 'repository_index', - to: 'repositories#repository_table_index', + to: 'repository_rows#index', # repository_rows#index repositories#repository_table_index as: 'table_index', defaults: { format: 'json' } # Save repository table state diff --git a/db/migrate/20180306074931_create_search_repositories.rb b/db/migrate/20180306074931_create_search_repositories.rb new file mode 100644 index 000000000..9311752f9 --- /dev/null +++ b/db/migrate/20180306074931_create_search_repositories.rb @@ -0,0 +1,5 @@ +class CreateSearchRepositories < ActiveRecord::Migration[5.1] + def change + create_view :search_repositories + end +end diff --git a/db/schema.rb b/db/schema.rb index 4fac94123..a3a437072 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180207095200) do +ActiveRecord::Schema.define(version: 20180306074931) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -964,4 +964,65 @@ ActiveRecord::Schema.define(version: 20180207095200) do JOIN user_teams ON ((teams.id = user_teams.team_id))); SQL + create_view "search_repositories", sql_definition: <<-SQL + SELECT DISTINCT repository_rows.id, + repository_rows.repository_id, + repository_rows.created_by_id, + repository_rows.last_modified_by_id, + repository_rows.name, + repository_rows.created_at, + repository_rows.updated_at, + users.full_name AS user_full_name, + "values".text_value, + "values".date_value, + "values".list_value + FROM ((repository_rows + JOIN ( SELECT users_1.id, + users_1.full_name, + users_1.initials, + users_1.email, + users_1.encrypted_password, + users_1.reset_password_token, + users_1.reset_password_sent_at, + users_1.remember_created_at, + users_1.sign_in_count, + users_1.current_sign_in_at, + users_1.last_sign_in_at, + users_1.current_sign_in_ip, + users_1.last_sign_in_ip, + users_1.created_at, + users_1.updated_at, + users_1.avatar_file_name, + users_1.avatar_content_type, + users_1.avatar_file_size, + users_1.avatar_updated_at, + users_1.confirmation_token, + users_1.confirmed_at, + users_1.confirmation_sent_at, + users_1.unconfirmed_email, + users_1.invitation_token, + users_1.invitation_created_at, + users_1.invitation_sent_at, + users_1.invitation_accepted_at, + users_1.invitation_limit, + users_1.invited_by_type, + users_1.invited_by_id, + users_1.invitations_count, + users_1.tutorial_status, + users_1.current_team_id, + users_1.authentication_token, + users_1.settings + FROM users users_1) users ON ((users.id = repository_rows.created_by_id))) + LEFT JOIN ( SELECT repository_cells.repository_row_id, + repository_text_values.data AS text_value, + to_char(repository_date_values.data, 'DD.MM.YYYY HH24:MI'::text) AS date_value, + ( SELECT repository_list_items.data + FROM repository_list_items + WHERE (repository_list_items.id = repository_list_values.repository_list_item_id)) AS list_value + FROM (((repository_cells + JOIN repository_text_values ON ((repository_text_values.id = repository_cells.value_id))) + FULL JOIN repository_date_values ON ((repository_date_values.id = repository_cells.value_id))) + FULL JOIN repository_list_values ON ((repository_list_values.id = repository_cells.value_id)))) "values" ON (("values".repository_row_id = repository_rows.id))); + SQL + end diff --git a/db/views/search_repositories_v01.sql b/db/views/search_repositories_v01.sql new file mode 100644 index 000000000..e9112c506 --- /dev/null +++ b/db/views/search_repositories_v01.sql @@ -0,0 +1,28 @@ +SELECT DISTINCT + repository_rows.*, + users.full_name AS user_full_name, + values.text_value AS text_value, + values.date_value AS date_value, + values.list_value AS list_value + FROM repository_rows + INNER JOIN ( + SELECT users.* + FROM users + ) AS users + ON users.id = repository_rows.created_by_id + LEFT OUTER JOIN ( + SELECT repository_cells.repository_row_id, + repository_text_values.data AS text_value, + to_char(repository_date_values.data, 'DD.MM.YYYY HH24:MI') AS date_value, + ( SELECT repository_list_items.data + FROM repository_list_items + WHERE repository_list_items.id = repository_list_values.repository_list_item_id ) AS list_value + FROM repository_cells + INNER JOIN repository_text_values + ON repository_text_values.id = repository_cells.value_id + FULL OUTER JOIN repository_date_values + ON repository_date_values.id = repository_cells.value_id + FUll OUTER JOIN repository_list_values + ON repository_list_values.id = repository_cells.value_id + ) AS values + ON values.repository_row_id = repository_rows.id diff --git a/spec/models/repository_list_value_spec.rb b/spec/models/repository_list_value_spec.rb index dcae9396c..4c9905bab 100644 --- a/spec/models/repository_list_value_spec.rb +++ b/spec/models/repository_list_value_spec.rb @@ -18,7 +18,7 @@ RSpec.describe RepositoryListValue, type: :model do it { should accept_nested_attributes_for(:repository_cell) } end - describe '#data' do + describe '#formatted' do let!(:repository) { create :repository } let!(:repository_column) { create :repository_column, name: 'My column' } let!(:repository_column) do @@ -32,7 +32,7 @@ RSpec.describe RepositoryListValue, type: :model do } end - it 'returns the data of a selected item' do + it 'returns the formatted data of a selected item' do list_item = create :repository_list_item, data: 'my item', repository: repository, @@ -71,4 +71,58 @@ RSpec.describe RepositoryListValue, type: :model do expect(repository_list_value.reload.formatted).to eq '' end end + + describe '#data' do + let!(:repository) { create :repository } + let!(:repository_column) { create :repository_column, name: 'My column' } + let!(:repository_column) do + create :repository_column, data_type: :RepositoryListValue + end + let!(:repository_row) { create :repository_row, name: 'My row' } + let!(:repository_list_value) do + create :repository_list_value, repository_cell_attributes: { + repository_column: repository_column, + repository_row: repository_row + } + end + + it 'returns the data of a selected item' do + list_item = create :repository_list_item, + data: 'my item', + repository: repository, + repository_column: repository_column + repository_list_value.repository_list_item = list_item + repository_list_value.save + expect(repository_list_value.reload.data).to eq 'my item' + end + + it 'retuns only the the item related to the list' do + repository_row_two = create :repository_row, name: 'New row' + repository_list_value_two = + create :repository_list_value, repository_cell_attributes: { + repository_column: repository_column, + repository_row: repository_row_two + } + list_item = create :repository_list_item, + data: 'new item', + repository: repository, + repository_column: repository_column + repository_list_value.repository_list_item = list_item + expect(repository_list_value.reload.data).to_not eq 'my item' + expect(repository_list_value.data).to be_nil + end + + it 'returns an empty string if no item selected' do + list_item = create :repository_list_item, + data: 'my item', + repository: repository, + repository_column: repository_column + expect(repository_list_value.reload.data).to be_nil + end + + it 'returns an empty string if item does not exists' do + repository_list_value.repository_list_item = nil + expect(repository_list_value.reload.data).to be_nil + end + end end diff --git a/spec/models/search_repository_spec.rb b/spec/models/search_repository_spec.rb new file mode 100644 index 000000000..052920181 --- /dev/null +++ b/spec/models/search_repository_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe SearchRepository, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From 26883af386af74a18dc5d9be113affd31e048750 Mon Sep 17 00:00:00 2001 From: zmagod Date: Thu, 8 Mar 2018 17:36:10 +0100 Subject: [PATCH 02/14] fix assigned ordering --- app/controllers/my_modules_controller.rb | 47 +++-- app/controllers/repository_rows_controller.rb | 4 +- app/helpers/repository_datatable_helper.rb | 21 +- app/models/repository.rb | 2 - app/models/repository_cell.rb | 4 + .../views/datatables/search_repository.rb | 21 -- app/services/repository_datatable_service.rb | 186 ++++++++++++++---- app/views/repository_rows/index.json.jbuilder | 6 +- ...180306074931_create_search_repositories.rb | 5 - db/schema.rb | 63 +----- db/views/search_repositories_v01.sql | 28 --- 11 files changed, 200 insertions(+), 187 deletions(-) delete mode 100644 app/models/views/datatables/search_repository.rb delete mode 100644 db/migrate/20180306074931_create_search_repositories.rb delete mode 100644 db/views/search_repositories_v01.sql diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb index 50b71be62..ecee8d96e 100644 --- a/app/controllers/my_modules_controller.rb +++ b/app/controllers/my_modules_controller.rb @@ -12,9 +12,11 @@ class MyModulesController < ApplicationController toggle_task_state samples_index archive complete_my_module repository repository_index assign_repository_records unassign_repository_records] - before_action :load_vars_nested, only: %I[new create] - before_action :load_repository, only: %I[assign_repository_records - unassign_repository_records] + before_action :load_vars_nested, only: %i(new create) + before_action :load_repository, only: %i(assign_repository_records + unassign_repository_records + repository_index) + before_action :load_columns_mappings, only: :repository_index before_action :check_manage_permissions, only: %i(update destroy description due_date) before_action :check_view_info_permissions, only: :show @@ -366,20 +368,18 @@ class MyModulesController < ApplicationController # AJAX actions def repository_index - @repository = Repository.find_by_id(params[:repository_id]) - if @repository.nil? || !can_read_team?(@repository.team) - render_403 - else - respond_to do |format| - format.html - format.json do - render json: ::RepositoryDatatable.new(view_context, - @repository, - @my_module, - current_user) - end - end - end + @draw = params[:draw].to_i + per_page = params[:length] == '-1' ? 100 : params[:length].to_i + page = (params[:start].to_i / per_page) + 1 + records = RepositoryDatatableService.new(@repository, + params, + @columns_mappings, + current_user, + @my_module) + @assigned_rows = records.assigned_rows + @repository_row_count = records.repository_rows.count + @repository_rows = records.repository_rows.page(page).per(per_page) + render 'repository_rows/index.json' end # Submit actions @@ -598,7 +598,18 @@ class MyModulesController < ApplicationController def load_repository @repository = Repository.find_by_id(params[:repository_id]) - render_404 unless @repository && can_read_team?(@repository.team) + render_404 unless @repository + render_403 unless can_read_team?(@repository.team) + end + + def load_columns_mappings + # Make mappings of custom columns, so we have same id for every column + i = 5 + @columns_mappings = {} + @repository.repository_columns.order(:id).each do |column| + @columns_mappings[column.id] = i.to_s + i += 1 + end end def check_manage_permissions diff --git a/app/controllers/repository_rows_controller.rb b/app/controllers/repository_rows_controller.rb index ddb8dbd37..3366acf13 100644 --- a/app/controllers/repository_rows_controller.rb +++ b/app/controllers/repository_rows_controller.rb @@ -17,7 +17,9 @@ class RepositoryRowsController < ApplicationController page = (params[:start].to_i / per_page) + 1 records = RepositoryDatatableService.new(@repository, params, - @columns_mappings) + @columns_mappings, + current_user) + @assigned_rows = records.assigned_rows @repository_row_count = records.repository_rows.count @repository_rows = records.repository_rows.page(page).per(per_page) end diff --git a/app/helpers/repository_datatable_helper.rb b/app/helpers/repository_datatable_helper.rb index c93bdf09d..7604e7123 100644 --- a/app/helpers/repository_datatable_helper.rb +++ b/app/helpers/repository_datatable_helper.rb @@ -1,11 +1,15 @@ module RepositoryDatatableHelper include InputSanitizeHelper - def prepare_row_columns(repository_rows, repository, columns_mappings, team) + def prepare_row_columns(repository_rows, + repository, + columns_mappings, + team, + assigned_rows) parsed_records = [] repository_rows.each do |record| row = { 'DT_RowId': record.id, - '1': assigned_row(record), + '1': assigned_row(record, assigned_rows), '2': escape_input(record.name), '3': I18n.l(record.created_at, format: :full), '4': escape_input(record.created_by.full_name), @@ -21,7 +25,6 @@ module RepositoryDatatableHelper } # Add custom columns - # byebug record.repository_cells.each do |cell| row[columns_mappings[cell.repository_column.id]] = custom_auto_link( @@ -36,11 +39,11 @@ module RepositoryDatatableHelper parsed_records end - def assigned_row(record) - # if @assigned_rows && @assigned_rows.include?(record) - # " " - # else - " " - # end + def assigned_row(record, assigned_rows) + if assigned_rows&.include?(record) + " " + else + " " + end end end diff --git a/app/models/repository.rb b/app/models/repository.rb index ed5775492..fce0a148d 100644 --- a/app/models/repository.rb +++ b/app/models/repository.rb @@ -12,8 +12,6 @@ class Repository < ApplicationRecord inverse_of: :repository, dependent: :destroy has_many :report_elements, inverse_of: :repository, dependent: :destroy has_many :repository_list_items, inverse_of: :repository, dependent: :destroy - has_many :repository_searchable_rows, - class_name: '::Views::Datatables::SearchRepository' auto_strip_attributes :name, nullify: false validates :name, diff --git a/app/models/repository_cell.rb b/app/models/repository_cell.rb index 44ed0f77b..13f07f371 100644 --- a/app/models/repository_cell.rb +++ b/app/models/repository_cell.rb @@ -8,6 +8,10 @@ class RepositoryCell < ActiveRecord::Base validates :repository_row, uniqueness: { scope: :repository_column } + belongs_to :repository_text_value, optional: true, foreign_key: :value_id + belongs_to :repository_date_value, optional: true, foreign_key: :value_id + belongs_to :repository_list_value, optional: true, foreign_key: :value_id + private def repository_column_data_type diff --git a/app/models/views/datatables/search_repository.rb b/app/models/views/datatables/search_repository.rb deleted file mode 100644 index 2ff6a6f84..000000000 --- a/app/models/views/datatables/search_repository.rb +++ /dev/null @@ -1,21 +0,0 @@ -module Views - module Datatables - class SearchRepository < ApplicationRecord - belongs_to :repository - # def self.records(repository, search_value) - # # binding.pry - # # # where('repository_rows.repository_id', repository.id).to_a - # # where(repository_id: repository.id) - # # .where() - # end - - private - - # this isn't strictly necessary, but it will prevent - # rails from calling save, which would fail anyway. - def readonly? - true - end - end - end -end diff --git a/app/services/repository_datatable_service.rb b/app/services/repository_datatable_service.rb index 61fe08d12..5f77a049c 100644 --- a/app/services/repository_datatable_service.rb +++ b/app/services/repository_datatable_service.rb @@ -1,73 +1,179 @@ class RepositoryDatatableService - attr_reader :repository_rows + attr_reader :repository_rows, :assigned_rows - def initialize(repository, params, mappings) + def initialize(repository, params, mappings, user, my_module = nil) @mappings = mappings @repository = repository - process_query(params) + @mappings = mappings + @user = user + @my_module = my_module + @params = params + process_query end private - def process_query(params) - contitions = build_conditions(params) - if contitions[:search_value].present? - @repository_rows = search(contitions[:search_value]) + def process_query + contitions = build_conditions(@params) + order_obj = contitions[:order_by_column] + search_value = contitions[:search_value] + if search_value.present? + @repository_rows = sort_rows(order_obj, search(search_value)) else - @repository_rows = fetch_records + @repository_rows = sort_rows(order_obj, fetch_records) end - # byebug end def fetch_records - RepositoryRow.preload(:repository_columns, - :created_by, - repository_cells: :value) - .joins(:created_by) - .where(repository: @repository) + repository_rows = RepositoryRow.preload(:repository_columns, + :created_by, + repository_cells: :value) + .joins(:created_by) + .where(repository: @repository) + if @my_module + @assigned_rows = @my_module.repository_rows + .preload( + :repository_columns, + :created_by, + repository_cells: :value + ) + .joins(:created_by) + .where(repository: @repository) + return @assigned_rows if @params[:assigned] == 'assigned' + else + @assigned_rows = repository_rows.joins( + 'INNER JOIN my_module_repository_rows ON + (repository_rows.id = my_module_repository_rows.repository_row_id)' + ) + end + repository_rows end def search(value) - # binding.pry - filtered_rows = @repository.repository_searchable_rows.where( - 'name ILIKE :value - OR to_char(created_at, :time) ILIKE :value - OR user_full_name ILIKE :value - OR text_value ILIKE :value - OR date_value ILIKE :value - OR list_value ILIKE :value', - value: "%#{value}%", - time: "DD.MM.YYYY HH24:MI" - ).pluck(:id) - fetch_records.where(id: filtered_rows) + includes_json = { + repository_cells: [:repository_text_value, + repository_list_value: :repository_list_item ] + } + RepositoryRow .left_outer_joins(:created_by) + .left_outer_joins(includes_json) + .where(repository: @repository) + .where_attributes_like( + ['repository_rows.name', + 'users.full_name', + 'repository_text_values.data', + 'repository_list_items.data'], + value + ) end def build_conditions(params) search_value = params[:search][:value] - order_by_column = { column: params[:order][:column].to_i, - dir: params[:order][:dir] } + order = params[:order].values.first + order_by_column = { column: order[:column].to_i, + dir: order[:dir] } { search_value: search_value, order_by_column: order_by_column } end def sortable_columns - sort_array = [ + array = [ 'assigned', - 'RepositoryRow.name', - 'RepositoryRow.created_at', - 'User.full_name' + 'repository_rows.name', + 'repository_rows.created_at', + 'users.full_name' ] - - sort_array.push(*repository_columns_sort_by) - @sortable_columns = sort_array - end - - def repository_columns_sort_by - array = [] @repository.repository_columns.count.times do - array << 'RepositoryCell.value' + array << 'repository_cell.value' end array end + def sort_rows(column_obj, records) + dir = %w[DESC ASC].find { |dir| dir == column_obj[:dir].upcase } || 'ASC' + column_index = column_obj[:column] + col_order = @repository.repository_table_states + .find_by_user_id(@user.id) + .state['ColReorder'] + column_id = col_order[column_index].to_i + + if sortable_columns[column_id - 1] == 'assigned' + return records if @my_module && @params[:assigned] == 'assigned' + if @my_module + # Depending on the sort, order nulls first or + # nulls last on repository_cells association + return records.joins( + "LEFT OUTER JOIN my_module_repository_rows ON + (repository_rows.id = my_module_repository_rows.repository_row_id + AND (my_module_repository_rows.my_module_id = #{@my_module.id} OR + my_module_repository_rows.id IS NULL))" + ).order( + "my_module_repository_rows.id NULLS #{sort_null_direction(dir)}" + ) + else + return sort_assigned_records(records, dir) + end + elsif sortable_columns[column_id - 1] == 'repository_cell.value' + id = @mappings.key(column_id.to_s) + type = RepositoryColumn.find_by_id(id) + return records unless type + return select_type(type.data_type, records, id, dir) + else + return records.order("#{sortable_columns[column_id - 1]} #{dir}") + end + end + + def sort_assigned_records(records, direction) + assigned = records.joins(:my_module_repository_rows).distinct.pluck(:id) + unassigned = records.where.not(id: assigned).pluck(:id) + if direction == 'ASC' + ids = assigned + unassigned + elsif direction == 'DESC' + ids = unassigned + assigned + end + + order_by_index = ActiveRecord::Base.send( + :sanitize_sql_array, + ["position((',' || repository_rows.id || ',') in ?)", + ids.join(',') + ','] + ) + records.order(order_by_index) + end + + def select_type(type, records, id, dir) + return filter_by_text_value( + records, id, dir) if type == 'RepositoryTextValue' + return filter_by_list_value( + records, id, dir) if type == 'RepositoryListValue' + end + + def sort_null_direction(val) + val == 'ASC' ? 'LAST' : 'FIRST' + end + + def filter_by_text_value(records, id, dir) + return records.joins( + "LEFT OUTER JOIN (SELECT repository_cells.repository_row_id, + repository_text_values.data AS value + FROM repository_cells + INNER JOIN repository_text_values + ON repository_text_values.id = repository_cells.value_id + WHERE repository_cells.repository_column_id = #{id}) AS values + ON values.repository_row_id = repository_rows.id" + ).order("values.value #{dir}") + end + + def filter_by_list_value(records, id, dir) + return records.joins( + "LEFT OUTER JOIN (SELECT repository_cells.repository_row_id, + repository_list_items.data AS value + FROM repository_cells + INNER JOIN repository_list_values + ON repository_list_values.id = repository_cells.value_id + INNER JOIN repository_list_items + ON repository_list_values.repository_list_item_id = + repository_list_items.id + WHERE repository_cells.repository_column_id = #{id}) AS values + ON values.repository_row_id = repository_rows.id" + ).order("values.value #{dir}") + end end diff --git a/app/views/repository_rows/index.json.jbuilder b/app/views/repository_rows/index.json.jbuilder index 23af2e9f3..c747bd696 100644 --- a/app/views/repository_rows/index.json.jbuilder +++ b/app/views/repository_rows/index.json.jbuilder @@ -2,5 +2,9 @@ json.draw @draw json.recordsTotal @repository_rows.total_count json.recordsFiltered @repository_row_count json.data do - json.array! prepare_row_columns(@repository_rows, @repository, @columns_mappings, @repository.team) + json.array! prepare_row_columns(@repository_rows, + @repository, + @columns_mappings, + @repository.team, + @assigned_rows) end diff --git a/db/migrate/20180306074931_create_search_repositories.rb b/db/migrate/20180306074931_create_search_repositories.rb deleted file mode 100644 index 9311752f9..000000000 --- a/db/migrate/20180306074931_create_search_repositories.rb +++ /dev/null @@ -1,5 +0,0 @@ -class CreateSearchRepositories < ActiveRecord::Migration[5.1] - def change - create_view :search_repositories - end -end diff --git a/db/schema.rb b/db/schema.rb index a3a437072..4fac94123 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20180306074931) do +ActiveRecord::Schema.define(version: 20180207095200) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -964,65 +964,4 @@ ActiveRecord::Schema.define(version: 20180306074931) do JOIN user_teams ON ((teams.id = user_teams.team_id))); SQL - create_view "search_repositories", sql_definition: <<-SQL - SELECT DISTINCT repository_rows.id, - repository_rows.repository_id, - repository_rows.created_by_id, - repository_rows.last_modified_by_id, - repository_rows.name, - repository_rows.created_at, - repository_rows.updated_at, - users.full_name AS user_full_name, - "values".text_value, - "values".date_value, - "values".list_value - FROM ((repository_rows - JOIN ( SELECT users_1.id, - users_1.full_name, - users_1.initials, - users_1.email, - users_1.encrypted_password, - users_1.reset_password_token, - users_1.reset_password_sent_at, - users_1.remember_created_at, - users_1.sign_in_count, - users_1.current_sign_in_at, - users_1.last_sign_in_at, - users_1.current_sign_in_ip, - users_1.last_sign_in_ip, - users_1.created_at, - users_1.updated_at, - users_1.avatar_file_name, - users_1.avatar_content_type, - users_1.avatar_file_size, - users_1.avatar_updated_at, - users_1.confirmation_token, - users_1.confirmed_at, - users_1.confirmation_sent_at, - users_1.unconfirmed_email, - users_1.invitation_token, - users_1.invitation_created_at, - users_1.invitation_sent_at, - users_1.invitation_accepted_at, - users_1.invitation_limit, - users_1.invited_by_type, - users_1.invited_by_id, - users_1.invitations_count, - users_1.tutorial_status, - users_1.current_team_id, - users_1.authentication_token, - users_1.settings - FROM users users_1) users ON ((users.id = repository_rows.created_by_id))) - LEFT JOIN ( SELECT repository_cells.repository_row_id, - repository_text_values.data AS text_value, - to_char(repository_date_values.data, 'DD.MM.YYYY HH24:MI'::text) AS date_value, - ( SELECT repository_list_items.data - FROM repository_list_items - WHERE (repository_list_items.id = repository_list_values.repository_list_item_id)) AS list_value - FROM (((repository_cells - JOIN repository_text_values ON ((repository_text_values.id = repository_cells.value_id))) - FULL JOIN repository_date_values ON ((repository_date_values.id = repository_cells.value_id))) - FULL JOIN repository_list_values ON ((repository_list_values.id = repository_cells.value_id)))) "values" ON (("values".repository_row_id = repository_rows.id))); - SQL - end diff --git a/db/views/search_repositories_v01.sql b/db/views/search_repositories_v01.sql deleted file mode 100644 index e9112c506..000000000 --- a/db/views/search_repositories_v01.sql +++ /dev/null @@ -1,28 +0,0 @@ -SELECT DISTINCT - repository_rows.*, - users.full_name AS user_full_name, - values.text_value AS text_value, - values.date_value AS date_value, - values.list_value AS list_value - FROM repository_rows - INNER JOIN ( - SELECT users.* - FROM users - ) AS users - ON users.id = repository_rows.created_by_id - LEFT OUTER JOIN ( - SELECT repository_cells.repository_row_id, - repository_text_values.data AS text_value, - to_char(repository_date_values.data, 'DD.MM.YYYY HH24:MI') AS date_value, - ( SELECT repository_list_items.data - FROM repository_list_items - WHERE repository_list_items.id = repository_list_values.repository_list_item_id ) AS list_value - FROM repository_cells - INNER JOIN repository_text_values - ON repository_text_values.id = repository_cells.value_id - FULL OUTER JOIN repository_date_values - ON repository_date_values.id = repository_cells.value_id - FUll OUTER JOIN repository_list_values - ON repository_list_values.id = repository_cells.value_id - ) AS values - ON values.repository_row_id = repository_rows.id From 4f951f6679fbd25a5de2103b6e16939bd8bb1401 Mon Sep 17 00:00:00 2001 From: zmagod Date: Fri, 9 Mar 2018 14:22:00 +0100 Subject: [PATCH 03/14] adds unit specs for datatable service [fixes SCI-2068] --- app/controllers/my_modules_controller.rb | 13 +- app/controllers/repository_rows_controller.rb | 13 +- app/datatables/repository_datatable.rb | 332 ------------------ app/models/repository_table_state.rb | 8 +- app/services/repository_datatable_service.rb | 17 +- config/environments/development.rb | 2 +- config/initializers/constants.rb | 25 ++ .../repository_rows_controller_spec.rb | 74 ++++ spec/models/search_repository_spec.rb | 5 - .../repository_datatable_service_spec.rb | 116 ++++++ .../schemas/repository_row_datatables.json | 25 ++ 11 files changed, 260 insertions(+), 370 deletions(-) delete mode 100644 app/datatables/repository_datatable.rb delete mode 100644 spec/models/search_repository_spec.rb create mode 100644 spec/services/repository_datatable_service_spec.rb create mode 100644 spec/support/api/schemas/repository_row_datatables.json diff --git a/app/controllers/my_modules_controller.rb b/app/controllers/my_modules_controller.rb index ecee8d96e..a485dba35 100644 --- a/app/controllers/my_modules_controller.rb +++ b/app/controllers/my_modules_controller.rb @@ -16,7 +16,6 @@ class MyModulesController < ApplicationController before_action :load_repository, only: %i(assign_repository_records unassign_repository_records repository_index) - before_action :load_columns_mappings, only: :repository_index before_action :check_manage_permissions, only: %i(update destroy description due_date) before_action :check_view_info_permissions, only: :show @@ -373,11 +372,11 @@ class MyModulesController < ApplicationController page = (params[:start].to_i / per_page) + 1 records = RepositoryDatatableService.new(@repository, params, - @columns_mappings, current_user, @my_module) @assigned_rows = records.assigned_rows @repository_row_count = records.repository_rows.count + @columns_mappings = records.mappings @repository_rows = records.repository_rows.page(page).per(per_page) render 'repository_rows/index.json' end @@ -602,16 +601,6 @@ class MyModulesController < ApplicationController render_403 unless can_read_team?(@repository.team) end - def load_columns_mappings - # Make mappings of custom columns, so we have same id for every column - i = 5 - @columns_mappings = {} - @repository.repository_columns.order(:id).each do |column| - @columns_mappings[column.id] = i.to_s - i += 1 - end - end - def check_manage_permissions render_403 unless can_manage_module?(@my_module) end diff --git a/app/controllers/repository_rows_controller.rb b/app/controllers/repository_rows_controller.rb index 3366acf13..4c7303a30 100644 --- a/app/controllers/repository_rows_controller.rb +++ b/app/controllers/repository_rows_controller.rb @@ -6,7 +6,6 @@ class RepositoryRowsController < ApplicationController before_action :load_info_modal_vars, only: :show before_action :load_vars, only: %i(edit update) before_action :load_repository, only: %i(create delete_records index) - before_action :load_columns_mappings, only: :index before_action :check_create_permissions, only: :create before_action :check_edit_permissions, only: %i(edit update) before_action :check_destroy_permissions, only: :delete_records @@ -17,10 +16,10 @@ class RepositoryRowsController < ApplicationController page = (params[:start].to_i / per_page) + 1 records = RepositoryDatatableService.new(@repository, params, - @columns_mappings, current_user) @assigned_rows = records.assigned_rows @repository_row_count = records.repository_rows.count + @columns_mappings = records.mappings @repository_rows = records.repository_rows.page(page).per(per_page) end @@ -255,16 +254,6 @@ class RepositoryRowsController < ApplicationController render_403 unless can_read_team?(@repository.team) end - def load_columns_mappings - # Make mappings of custom columns, so we have same id for every column - i = 5 - @columns_mappings = {} - @repository.repository_columns.order(:id).each do |column| - @columns_mappings[column.id] = i.to_s - i += 1 - end - end - def check_create_permissions render_403 unless can_manage_repository_rows?(@repository.team) end diff --git a/app/datatables/repository_datatable.rb b/app/datatables/repository_datatable.rb deleted file mode 100644 index 3cce51ca4..000000000 --- a/app/datatables/repository_datatable.rb +++ /dev/null @@ -1,332 +0,0 @@ -require 'active_record' - -class RepositoryDatatable < CustomDatatable - include ActionView::Helpers::TextHelper - include SamplesHelper - include InputSanitizeHelper - include Rails.application.routes.url_helpers - include ActionView::Helpers::UrlHelper - include ApplicationHelper - include ActiveRecord::Sanitization::ClassMethods - - ASSIGNED_SORT_COL = 'assigned'.freeze - - REPOSITORY_TABLE_DEFAULT_STATE = { - 'time' => 0, - 'start' => 0, - 'length' => 5, - 'order' => [[2, 'desc']], - 'search' => { 'search' => '', - 'smart' => true, - 'regex' => false, - 'caseInsensitive' => true }, - 'columns' => [], - 'assigned' => 'assigned', - 'ColReorder' => [*0..4] - } - 5.times do - REPOSITORY_TABLE_DEFAULT_STATE['columns'] << { - 'visible' => true, - 'search' => { 'search' => '', - 'smart' => true, - 'regex' => false, - 'caseInsensitive' => true } - } - end - REPOSITORY_TABLE_DEFAULT_STATE.freeze - - def initialize(view, - repository, - my_module = nil, - user = nil) - super(view) - @repository = repository - @team = repository.team - @my_module = my_module - @user = user - end - - # Define sortable columns, so 1st column will be sorted by attribute - # in sortable_columns[0] - def sortable_columns - sort_array = [ - ASSIGNED_SORT_COL, - 'RepositoryRow.name', - 'RepositoryRow.created_at', - 'User.full_name' - ] - - sort_array.push(*repository_columns_sort_by) - @sortable_columns = sort_array - end - - # Define attributes on which we perform search - def searchable_columns - search_array = [ - 'RepositoryRow.name', - 'RepositoryRow.created_at', - 'User.full_name' - ] - - # search_array.push(*repository_columns_sort_by) - @searchable_columns ||= filter_search_array search_array - end - - private - - # filters the search array by checking if the the column is visible - def filter_search_array(input_array) - param_index = 2 - filtered_array = [] - input_array.each do |col| - next if columns_params.to_a[param_index].nil? - params_col = - columns_params.to_a.find { |v| v[1]['data'] == param_index.to_s } - filtered_array.push(col) unless params_col[1]['searchable'] == 'false' - param_index += 1 - end - filtered_array - end - - # Get array of columns to sort by (for custom columns) - def repository_columns_sort_by - array = [] - @repository.repository_columns.count.times do - array << 'RepositoryCell.value' - end - array - end - - # Returns json of current repository rows (already paginated) - def data - records.map do |record| - row = { - 'DT_RowId': record.id, - '1': assigned_row(record), - '2': escape_input(record.name), - '3': I18n.l(record.created_at, format: :full), - '4': escape_input(record.created_by.full_name), - 'recordEditUrl': - Rails.application.routes.url_helpers - .edit_repository_repository_row_path(@repository, - record.id), - 'recordUpdateUrl': - Rails.application.routes.url_helpers - .repository_repository_row_path(@repository, record.id), - 'recordInfoUrl': - Rails.application.routes.url_helpers.repository_row_path(record.id) - } - - # Add custom columns - record.repository_cells.each do |cell| - row[@columns_mappings[cell.repository_column.id]] = - custom_auto_link( - display_tooltip(cell.value.data, - Constants::NAME_MAX_LENGTH), - simple_format: true, - team: @team - ) - end - row - end - end - - def assigned_row(record) - if @assigned_rows && @assigned_rows.include?(record) - " " - else - " " - end - end - - # Query database for records (this will be later paginated and filtered) - # after that "data" function will return json - def get_raw_records - repository_rows = RepositoryRow - .preload( - :repository_columns, - :created_by, - repository_cells: :value - ) - .joins(:created_by) - .where(repository: @repository) - - # Make mappings of custom columns, so we have same id for every column - i = 5 - @columns_mappings = {} - @repository.repository_columns.order(:id).each do |column| - @columns_mappings[column.id] = i.to_s - i += 1 - end - - if @my_module - @assigned_rows = @my_module.repository_rows - .preload( - :repository_columns, - :created_by, - repository_cells: :value - ) - .joins(:created_by) - .where(repository: @repository) - return @assigned_rows if dt_params[:assigned] == 'assigned' - else - @assigned_rows = repository_rows.joins( - 'INNER JOIN my_module_repository_rows ON - (repository_rows.id = my_module_repository_rows.repository_row_id)' - ) - end - - repository_rows - 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 = filter_records(records) if dt_params[:search].present? && - dt_params[:search][:value].present? - records = sort_records(records) if order_params.present? - records = paginate_records(records) unless dt_params[:length].present? && - dt_params[:length] == '-1' - escape_special_chars - records - end - - # Overriden to make it work for custom columns, because they are polymorphic - # NOTE: Function assumes the provided records/rows are only from the current - # repository! - def filter_records(repo_rows) - return repo_rows unless dt_params[:search].present? && - dt_params[:search][:value].present? - search_val = dt_params[:search][:value] - - filtered_rows = repo_rows.find_by_sql( - "SELECT DISTINCT repository_rows.* - FROM repository_rows - INNER JOIN ( - SELECT users.* - FROM users - ) AS users - ON users.id = repository_rows.created_by_id - LEFT OUTER JOIN ( - SELECT repository_cells.repository_row_id, - repository_text_values.data AS text_value, - to_char(repository_date_values.data, 'DD.MM.YYYY HH24:MI') - AS date_value - FROM repository_cells - INNER JOIN repository_text_values - ON repository_text_values.id = repository_cells.value_id - FULL OUTER JOIN repository_date_values - ON repository_date_values.id = repository_cells.value_id - ) AS values - ON values.repository_row_id = repository_rows.id - WHERE repository_rows.repository_id = #{@repository.id} - AND (repository_rows.name ILIKE '%#{search_val}%' - OR to_char(repository_rows.created_at, 'DD.MM.YYYY HH24:MI') - ILIKE '%#{search_val}%' - OR users.full_name ILIKE '%#{search_val}%' - OR text_value ILIKE '%#{search_val}%' - OR date_value ILIKE '%#{search_val}%')" - ) - repo_rows.where(id: filtered_rows) - end - - # Override default sort method if needed - def sort_records(records) - if sort_column(order_params) == ASSIGNED_SORT_COL - # If "assigned" column is sorted when viewing assigned items - return records if @my_module && dt_params[:assigned] == 'assigned' - # If "assigned" column is sorted - direction = sort_null_direction(order_params) - if @my_module - # Depending on the sort, order nulls first or - # nulls last on repository_cells association - records.joins( - "LEFT OUTER JOIN my_module_repository_rows ON - (repository_rows.id = my_module_repository_rows.repository_row_id - AND (my_module_repository_rows.my_module_id = #{@my_module.id} OR - my_module_repository_rows.id IS NULL))" - ).order("my_module_repository_rows.id NULLS #{direction}") - else - sort_assigned_records(records, order_params['dir']) - end - elsif sorting_by_custom_column - ci = sortable_displayed_columns[ - order_params['column'].to_i - 1 - ] - column_id = @columns_mappings.key((ci.to_i + 1).to_s) - dir = sort_direction(order_params) - - records.joins( - "LEFT OUTER JOIN (SELECT repository_cells.repository_row_id, - repository_text_values.data AS value FROM repository_cells - INNER JOIN repository_text_values - ON repository_text_values.id = repository_cells.value_id - WHERE repository_cells.repository_column_id = #{column_id}) AS values - ON values.repository_row_id = repository_rows.id" - ).order("values.value #{dir}") - 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 - sort_column(order_params) == 'repository_cells.value' - end - - # Escapes special characters in search query - def escape_special_chars - if dt_params[:search].present? - dt_params[:search][:value] = ActiveRecord::Base - .__send__(:sanitize_sql_like, - dt_params[:search][:value]) - end - end - - def new_sort_column(item) - coli = item[:column].to_i - 1 - model, column = sortable_columns[sortable_displayed_columns[coli].to_i] - .split('.') - - return model if model == ASSIGNED_SORT_COL - [model.constantize.table_name, column].join('.') - end - - def generate_sortable_displayed_columns - sort_order = RepositoryTableState.load_state(@user, @repository) - .first['ColReorder'] - sort_order.shift - sort_order.map! { |i| (i.to_i - 1).to_s } - - @sortable_displayed_columns = sort_order - end - - def sort_assigned_records(records, direction) - assigned = records.joins(:my_module_repository_rows).distinct.pluck(:id) - unassigned = records.where.not(id: assigned).pluck(:id) - if direction == 'asc' - ids = assigned + unassigned - elsif direction == 'desc' - ids = unassigned + assigned - end - - order_by_index = ActiveRecord::Base.send( - :sanitize_sql_array, - ["position((',' || repository_rows.id || ',') in ?)", - ids.join(',') + ','] - ) - records.order(order_by_index) - end -end diff --git a/app/models/repository_table_state.rb b/app/models/repository_table_state.rb index d88847ad7..dc2f3c6d2 100644 --- a/app/models/repository_table_state.rb +++ b/app/models/repository_table_state.rb @@ -43,7 +43,7 @@ class RepositoryTableState < ApplicationRecord else # add column index = repository_state['columns'].count - repository_state['columns'][index] = RepositoryDatatable:: + repository_state['columns'][index] = Constants:: REPOSITORY_TABLE_DEFAULT_STATE['columns'].first repository_state['ColReorder'].insert(2, index.to_s) end @@ -52,12 +52,12 @@ class RepositoryTableState < ApplicationRecord end def self.create_state(user, repository) - default_columns_num = RepositoryDatatable:: + default_columns_num = Constants:: REPOSITORY_TABLE_DEFAULT_STATE['columns'].count repository_state = - RepositoryDatatable::REPOSITORY_TABLE_DEFAULT_STATE.deep_dup + Constants::REPOSITORY_TABLE_DEFAULT_STATE.deep_dup repository.repository_columns.each_with_index do |_, index| - repository_state['columns'] << RepositoryDatatable:: + repository_state['columns'] << Constants:: REPOSITORY_TABLE_DEFAULT_STATE['columns'].first repository_state['ColReorder'] << (default_columns_num + index) end diff --git a/app/services/repository_datatable_service.rb b/app/services/repository_datatable_service.rb index 5f77a049c..60dcc3b42 100644 --- a/app/services/repository_datatable_service.rb +++ b/app/services/repository_datatable_service.rb @@ -1,19 +1,28 @@ class RepositoryDatatableService - attr_reader :repository_rows, :assigned_rows + attr_reader :repository_rows, :assigned_rows, :mappings - def initialize(repository, params, mappings, user, my_module = nil) - @mappings = mappings + def initialize(repository, params, user, my_module = nil) @repository = repository - @mappings = mappings @user = user @my_module = my_module @params = params + create_columns_mappings process_query end private + def create_columns_mappings + # Make mappings of custom columns, so we have same id for every column + i = 5 + @mappings = {} + @repository.repository_columns.order(:id).each do |column| + @mappings[column.id] = i.to_s + i += 1 + end + end + def process_query contitions = build_conditions(@params) order_obj = contitions[:order_by_column] diff --git a/config/environments/development.rb b/config/environments/development.rb index 580ca9c48..5f6bbe557 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -75,7 +75,7 @@ Rails.application.configure do config.assets.raise_runtime_errors = true # Only log info and higher on development - config.log_level = :debug + config.log_level = :info # Only allow Better Errors to work on trusted ip, use ifconfig to see which # one you use and put it into application.yml! diff --git a/config/initializers/constants.rb b/config/initializers/constants.rb index bdca92004..2bf846e03 100644 --- a/config/initializers/constants.rb +++ b/config/initializers/constants.rb @@ -859,6 +859,31 @@ class Constants ] }.freeze + # Repository default table state + REPOSITORY_TABLE_DEFAULT_STATE = { + 'time' => 0, + 'start' => 0, + 'length' => 5, + 'order' => [[2, 'desc']], + 'search' => { 'search' => '', + 'smart' => true, + 'regex' => false, + 'caseInsensitive' => true }, + 'columns' => [], + 'assigned' => 'assigned', + 'ColReorder' => [*0..4] + } + 5.times do + REPOSITORY_TABLE_DEFAULT_STATE['columns'] << { + 'visible' => true, + 'search' => { 'search' => '', + 'smart' => true, + 'regex' => false, + 'caseInsensitive' => true } + } + end + REPOSITORY_TABLE_DEFAULT_STATE.freeze + EXPORTABLE_ZIP_EXPIRATION_DAYS = 7 # Very basic regex to check for validity of emails diff --git a/spec/controllers/repository_rows_controller_spec.rb b/spec/controllers/repository_rows_controller_spec.rb index a7a0f2f52..0ee849e91 100644 --- a/spec/controllers/repository_rows_controller_spec.rb +++ b/spec/controllers/repository_rows_controller_spec.rb @@ -7,6 +7,13 @@ describe RepositoryRowsController, type: :controller do let!(:team) { create :team, created_by: user } let!(:user_team) { create :user_team, team: team, user: user } let!(:repository) { create :repository, team: team, created_by: user } + let!(:repository_state) do + RepositoryTableState.create( + repository: repository, + user: user, + state: Constants::REPOSITORY_TABLE_DEFAULT_STATE + ) + end let!(:repository_row) do create :repository_row, repository: repository, created_by: user, @@ -41,4 +48,71 @@ describe RepositoryRowsController, type: :controller do expect(response).to have_http_status(:success) end end + + context '#index' do + before do + repository.repository_rows.destroy_all + 110.times do |index| + create :repository_row, name: "row (#{index})", + repository: repository, + created_by: user, + last_modified_by: user + end + end + + describe 'json object' do + it 'returns a valid object' do + params = { order: { 0 => { column: '3', dir: 'asc' } }, + drow: '0', + search: { value: '' }, + length: '10', + start: '1', + repository_id: repository.id } + get :index, params: params, format: :json + + expect(response.status).to eq 200 + expect(response).to match_response_schema('repository_row_datatables') + end + end + + describe 'pagination' do + it 'returns first 10 records' do + params = { order: { 0 => { column: '3', dir: 'asc' } }, + drow: '0', + search: { value: '' }, + length: '10', + start: '1', + repository_id: repository.id } + get :index, params: params, format: :json + response_body = JSON.parse(response.body) + expect(response_body['data'].length).to eq 10 + expect(response_body['data'].first['2']).to eq 'row (0)' + end + + it 'returns next 10 records' do + params = { order: { 0 => { column: '3', dir: 'asc' } }, + drow: '0', + search: { value: '' }, + length: '10', + start: '11', + repository_id: repository.id } + get :index, params: params, format: :json + response_body = JSON.parse(response.body) + expect(response_body['data'].length).to eq 10 + expect(response_body['data'].first['2']).to eq 'row (10)' + end + + it 'returns first 25 records' do + params = { order: { 0 => { column: '2', dir: 'desc' } }, + drow: '0', + search: { value: '' }, + length: '25', + start: '1', + repository_id: repository.id } + get :index, params: params, format: :json + response_body = JSON.parse(response.body) + expect(response_body['data'].length).to eq 25 + end + end + end end diff --git a/spec/models/search_repository_spec.rb b/spec/models/search_repository_spec.rb deleted file mode 100644 index 052920181..000000000 --- a/spec/models/search_repository_spec.rb +++ /dev/null @@ -1,5 +0,0 @@ -require 'rails_helper' - -RSpec.describe SearchRepository, type: :model do - pending "add some examples to (or delete) #{__FILE__}" -end diff --git a/spec/services/repository_datatable_service_spec.rb b/spec/services/repository_datatable_service_spec.rb new file mode 100644 index 000000000..2e1369e9a --- /dev/null +++ b/spec/services/repository_datatable_service_spec.rb @@ -0,0 +1,116 @@ +require 'rails_helper' + +describe RepositoryDatatableService do + let!(:team) { create :team } + let!(:user) { create :user, email: 'user_one@asdf.com' } + let!(:repository) do + create :repository, name: 'my repo', created_by: user, team: team + end + let!(:repository_column) do + create :repository_column, name: 'My column', data_type: :RepositoryListValue + end + let!(:repository_state) do + RepositoryTableState.create( + repository: repository, + user: user, + state: Constants::REPOSITORY_TABLE_DEFAULT_STATE + ) + end + let!(:repository_row) do + create :repository_row, name: 'A row', + repository: repository, + created_by: user, + last_modified_by: user + end + let!(:repository_row_two) do + create :repository_row, name: 'B row', + repository: repository, + created_by: user, + last_modified_by: user + end + let!(:list_item) do + create :repository_list_item, data: 'bug', + repository: repository, + repository_column: repository_column, + created_by: user, + last_modified_by: user + end + let!(:repository_list_value) do + create :repository_list_value, repository_list_item: list_item, + created_by: user, + last_modified_by: user, + repository_cell_attributes: { + repository_column: repository_column, + repository_row: repository_row + } + end + + context 'object' do + let(:params) do + { order: { 0 => { column: '2', dir: 'asc' } }, search: { value: 'row' } } + end + let(:subject) { RepositoryDatatableService.new(repository, params, user) } + + describe '#build_conditions/1' do + it 'parsers the contitions' do + contitions = subject.send(:build_conditions, params) + expect(contitions[:search_value]).to eq 'row' + expect(contitions[:order_by_column]).to eq({ column: 2, dir: 'asc' }) + end + end + + describe '#sortable_columns' do + it 'returns an array of all columns that are sortable' do + columns = subject.send(:sortable_columns) + expect(columns.length).to eq 5 + end + end + + describe '#sort_null_direction' do + it 'returns LAST if value is ascending' do + result = subject.send(:sort_null_direction, 'ASC') + expect(result).to eq 'LAST' + end + + it 'returns FIRST if value is not ascending' do + result = subject.send(:sort_null_direction, 'DESC') + expect(result).to eq 'FIRST' + end + end + end + + describe 'ordering' do + it 'is ordered by row name asc' do + params = { order: { 0 => { column: '2', dir: 'asc' } }, + search: { value: '' } } + subject = RepositoryDatatableService.new(repository, params, user) + expect(subject.repository_rows.first.name).to eq 'A row' + expect(subject.repository_rows.last.name).to eq 'B row' + end + + it 'is ordered by row name desc' do + params = { order: { 0 => { column: '2', dir: 'desc' } }, + search: { value: '' } } + subject = RepositoryDatatableService.new(repository, params, user) + expect(subject.repository_rows.first.name).to eq 'B row' + expect(subject.repository_rows.last.name).to eq 'A row' + end + end + + describe 'search' do + before do + create :repository_row, name: 'test', + repository: repository, + created_by: user, + last_modified_by: user + end + + it 'returns only the searched entity' do + params = { order: { 0 => { column: '2', dir: 'desc' } }, + search: { value: 'test' } } + subject = RepositoryDatatableService.new(repository, params, user) + expect(subject.repository_rows.first.name).to eq 'test' + expect(subject.repository_rows.count).to eq 1 + end + end +end diff --git a/spec/support/api/schemas/repository_row_datatables.json b/spec/support/api/schemas/repository_row_datatables.json new file mode 100644 index 000000000..0a73d82f6 --- /dev/null +++ b/spec/support/api/schemas/repository_row_datatables.json @@ -0,0 +1,25 @@ +{ + "type": "object", + "required": ["draw", "recordsTotal", "recordsFiltered", "data"], + "properties": { + "draw": { "type": "integer" }, + "recordsTotal": { "type": "integer" }, + "recordsFiltered": { "type": "integer" }, + "data": { + "type": "array", + "items":{ + "required": ["DT_RowId", "1", "2", "3", "4", "recordEditUrl", "recordUpdateUrl", "recordInfoUrl"], + "properties": { + "DT_RowId": { "type": "integer" }, + "1": { "type": "string" }, + "2": { "type": "string" }, + "3": { "type": "string" }, + "4": { "type": "string" }, + "recordEditUrl": { "type": "string" }, + "recordUpdateUrl": { "type": "string" }, + "recordInfoUrl": { "type": "string" } + } + } + } + } +} From 0baa7a1f998dcdec1dbf7147653fa1774349fe7f Mon Sep 17 00:00:00 2001 From: zmagod Date: Fri, 9 Mar 2018 14:43:12 +0100 Subject: [PATCH 04/14] fix hound --- app/helpers/repository_datatable_helper.rb | 21 ++++--- app/services/repository_datatable_service.rb | 60 ++++++++++++------- spec/models/repository_list_value_spec.rb | 16 +++-- .../repository_datatable_service_spec.rb | 57 +++++++++++------- 4 files changed, 95 insertions(+), 59 deletions(-) diff --git a/app/helpers/repository_datatable_helper.rb b/app/helpers/repository_datatable_helper.rb index 7604e7123..f07546648 100644 --- a/app/helpers/repository_datatable_helper.rb +++ b/app/helpers/repository_datatable_helper.rb @@ -13,15 +13,18 @@ module RepositoryDatatableHelper '2': escape_input(record.name), '3': I18n.l(record.created_at, format: :full), '4': escape_input(record.created_by.full_name), - 'recordEditUrl': - Rails.application.routes.url_helpers - .edit_repository_repository_row_path(repository, - record.id), - 'recordUpdateUrl': - Rails.application.routes.url_helpers - .repository_repository_row_path(repository, record.id), - 'recordInfoUrl': - Rails.application.routes.url_helpers.repository_row_path(record.id) + 'recordEditUrl': Rails.application.routes.url_helpers + .edit_repository_repository_row_path( + repository, + record.id + ), + 'recordUpdateUrl': Rails.application.routes.url_helpers + .repository_repository_row_path( + repository, + record.id + ), + 'recordInfoUrl': Rails.application.routes.url_helpers + .repository_row_path(record.id) } # Add custom columns diff --git a/app/services/repository_datatable_service.rb b/app/services/repository_datatable_service.rb index 60dcc3b42..a3f3c0ceb 100644 --- a/app/services/repository_datatable_service.rb +++ b/app/services/repository_datatable_service.rb @@ -14,7 +14,8 @@ class RepositoryDatatableService private def create_columns_mappings - # Make mappings of custom columns, so we have same id for every column + # Make mappings of custom columns, so we have same id for every + # column i = 5 @mappings = {} @repository.repository_columns.order(:id).each do |column| @@ -24,9 +25,8 @@ class RepositoryDatatableService end def process_query - contitions = build_conditions(@params) - order_obj = contitions[:order_by_column] - search_value = contitions[:search_value] + order_obj = build_conditions(@params)[:order_by_column] + search_value = build_conditions(@params)[:search_value] if search_value.present? @repository_rows = sort_rows(order_obj, search(search_value)) else @@ -53,7 +53,8 @@ class RepositoryDatatableService else @assigned_rows = repository_rows.joins( 'INNER JOIN my_module_repository_rows ON - (repository_rows.id = my_module_repository_rows.repository_row_id)' + (repository_rows.id = + my_module_repository_rows.repository_row_id)' ) end repository_rows @@ -61,8 +62,10 @@ class RepositoryDatatableService def search(value) includes_json = { - repository_cells: [:repository_text_value, - repository_list_value: :repository_list_item ] + repository_cells: [ + :repository_text_value, + repository_list_value: :repository_list_item + ] } RepositoryRow .left_outer_joins(:created_by) .left_outer_joins(includes_json) @@ -98,7 +101,9 @@ class RepositoryDatatableService end def sort_rows(column_obj, records) - dir = %w[DESC ASC].find { |dir| dir == column_obj[:dir].upcase } || 'ASC' + dir = %w(DESC ASC).find do |dir| + dir == column_obj[:dir].upcase + end || 'ASC' column_index = column_obj[:column] col_order = @repository.repository_table_states .find_by_user_id(@user.id) @@ -106,17 +111,22 @@ class RepositoryDatatableService column_id = col_order[column_index].to_i if sortable_columns[column_id - 1] == 'assigned' - return records if @my_module && @params[:assigned] == 'assigned' + if @my_module && @params[:assigned] == 'assigned' + return records + end if @my_module # Depending on the sort, order nulls first or # nulls last on repository_cells association return records.joins( "LEFT OUTER JOIN my_module_repository_rows ON - (repository_rows.id = my_module_repository_rows.repository_row_id - AND (my_module_repository_rows.my_module_id = #{@my_module.id} OR - my_module_repository_rows.id IS NULL))" + (repository_rows.id = + my_module_repository_rows.repository_row_id + AND (my_module_repository_rows.my_module_id = + #{@my_module.id} + OR my_module_repository_rows.id IS NULL))" ).order( - "my_module_repository_rows.id NULLS #{sort_null_direction(dir)}" + "my_module_repository_rows.id NULLS + #{sort_null_direction(dir)}" ) else return sort_assigned_records(records, dir) @@ -127,12 +137,16 @@ class RepositoryDatatableService return records unless type return select_type(type.data_type, records, id, dir) else - return records.order("#{sortable_columns[column_id - 1]} #{dir}") + return records.order( + "#{sortable_columns[column_id - 1]} #{dir}" + ) end end def sort_assigned_records(records, direction) - assigned = records.joins(:my_module_repository_rows).distinct.pluck(:id) + assigned = records.joins(:my_module_repository_rows) + .distinct + .pluck(:id) unassigned = records.where.not(id: assigned).pluck(:id) if direction == 'ASC' ids = assigned + unassigned @@ -149,10 +163,14 @@ class RepositoryDatatableService end def select_type(type, records, id, dir) - return filter_by_text_value( - records, id, dir) if type == 'RepositoryTextValue' - return filter_by_list_value( - records, id, dir) if type == 'RepositoryListValue' + case type + when 'RepositoryTextValue' + filter_by_text_value(records, id, dir) + when 'RepositoryListValue' + filter_by_list_value(records, id, dir) + else + records + end end def sort_null_direction(val) @@ -160,7 +178,7 @@ class RepositoryDatatableService end def filter_by_text_value(records, id, dir) - return records.joins( + records.joins( "LEFT OUTER JOIN (SELECT repository_cells.repository_row_id, repository_text_values.data AS value FROM repository_cells @@ -172,7 +190,7 @@ class RepositoryDatatableService end def filter_by_list_value(records, id, dir) - return records.joins( + records.joins( "LEFT OUTER JOIN (SELECT repository_cells.repository_row_id, repository_list_items.data AS value FROM repository_cells diff --git a/spec/models/repository_list_value_spec.rb b/spec/models/repository_list_value_spec.rb index 4c9905bab..59dd9886c 100644 --- a/spec/models/repository_list_value_spec.rb +++ b/spec/models/repository_list_value_spec.rb @@ -98,11 +98,10 @@ RSpec.describe RepositoryListValue, type: :model do it 'retuns only the the item related to the list' do repository_row_two = create :repository_row, name: 'New row' - repository_list_value_two = - create :repository_list_value, repository_cell_attributes: { - repository_column: repository_column, - repository_row: repository_row_two - } + create :repository_list_value, repository_cell_attributes: { + repository_column: repository_column, + repository_row: repository_row_two + } list_item = create :repository_list_item, data: 'new item', repository: repository, @@ -113,10 +112,9 @@ RSpec.describe RepositoryListValue, type: :model do end it 'returns an empty string if no item selected' do - list_item = create :repository_list_item, - data: 'my item', - repository: repository, - repository_column: repository_column + create :repository_list_item, data: 'my item', + repository: repository, + repository_column: repository_column expect(repository_list_value.reload.data).to be_nil end diff --git a/spec/services/repository_datatable_service_spec.rb b/spec/services/repository_datatable_service_spec.rb index 2e1369e9a..e32b13f19 100644 --- a/spec/services/repository_datatable_service_spec.rb +++ b/spec/services/repository_datatable_service_spec.rb @@ -4,10 +4,13 @@ describe RepositoryDatatableService do let!(:team) { create :team } let!(:user) { create :user, email: 'user_one@asdf.com' } let!(:repository) do - create :repository, name: 'my repo', created_by: user, team: team + create :repository, name: 'my repo', + created_by: user, + team: team end let!(:repository_column) do - create :repository_column, name: 'My column', data_type: :RepositoryListValue + create :repository_column, name: 'My column', + data_type: :RepositoryListValue end let!(:repository_state) do RepositoryTableState.create( @@ -29,33 +32,41 @@ describe RepositoryDatatableService do last_modified_by: user end let!(:list_item) do - create :repository_list_item, data: 'bug', - repository: repository, - repository_column: repository_column, - created_by: user, - last_modified_by: user + create :repository_list_item, + data: 'bug', + repository: repository, + repository_column: repository_column, + created_by: user, + last_modified_by: user end let!(:repository_list_value) do - create :repository_list_value, repository_list_item: list_item, - created_by: user, - last_modified_by: user, - repository_cell_attributes: { - repository_column: repository_column, - repository_row: repository_row - } + create :repository_list_value, + repository_list_item: list_item, + created_by: user, + last_modified_by: user, + repository_cell_attributes: { + repository_column: repository_column, + repository_row: repository_row + } end context 'object' do let(:params) do - { order: { 0 => { column: '2', dir: 'asc' } }, search: { value: 'row' } } + { order: { 0 => { column: '2', dir: 'asc' } }, + search: { value: 'row' } } + end + + let(:subject) do + RepositoryDatatableService.new(repository, params, user) end - let(:subject) { RepositoryDatatableService.new(repository, params, user) } describe '#build_conditions/1' do it 'parsers the contitions' do contitions = subject.send(:build_conditions, params) expect(contitions[:search_value]).to eq 'row' - expect(contitions[:order_by_column]).to eq({ column: 2, dir: 'asc' }) + expect(contitions[:order_by_column]).to eq( + { column: 2, dir: 'asc' } + ) end end @@ -83,7 +94,9 @@ describe RepositoryDatatableService do it 'is ordered by row name asc' do params = { order: { 0 => { column: '2', dir: 'asc' } }, search: { value: '' } } - subject = RepositoryDatatableService.new(repository, params, user) + subject = RepositoryDatatableService.new(repository, + params, + user) expect(subject.repository_rows.first.name).to eq 'A row' expect(subject.repository_rows.last.name).to eq 'B row' end @@ -91,7 +104,9 @@ describe RepositoryDatatableService do it 'is ordered by row name desc' do params = { order: { 0 => { column: '2', dir: 'desc' } }, search: { value: '' } } - subject = RepositoryDatatableService.new(repository, params, user) + subject = RepositoryDatatableService.new(repository, + params, + user) expect(subject.repository_rows.first.name).to eq 'B row' expect(subject.repository_rows.last.name).to eq 'A row' end @@ -108,7 +123,9 @@ describe RepositoryDatatableService do it 'returns only the searched entity' do params = { order: { 0 => { column: '2', dir: 'desc' } }, search: { value: 'test' } } - subject = RepositoryDatatableService.new(repository, params, user) + subject = RepositoryDatatableService.new(repository, + params, + user) expect(subject.repository_rows.first.name).to eq 'test' expect(subject.repository_rows.count).to eq 1 end From 9de998e85c97a83af70a39c0bee8ac5ea742b95b Mon Sep 17 00:00:00 2001 From: mlorb Date: Wed, 14 Mar 2018 14:33:34 +0100 Subject: [PATCH 05/14] code left-side menu bar --- app/assets/stylesheets/partials/_sidebar.scss | 2 +- app/assets/stylesheets/themes/scinote.scss | 50 +++++++++++++++++++ app/helpers/left_menu_bar_helper.rb | 31 ++++++++++++ app/views/layouts/application.html.erb | 1 + app/views/shared/_left_menu_bar.html.erb | 48 ++++++++++++++++++ config/locales/en.yml | 9 ++++ 6 files changed, 140 insertions(+), 1 deletion(-) create mode 100644 app/helpers/left_menu_bar_helper.rb create mode 100644 app/views/shared/_left_menu_bar.html.erb diff --git a/app/assets/stylesheets/partials/_sidebar.scss b/app/assets/stylesheets/partials/_sidebar.scss index 7b14f5975..8457e1f5b 100644 --- a/app/assets/stylesheets/partials/_sidebar.scss +++ b/app/assets/stylesheets/partials/_sidebar.scss @@ -26,7 +26,6 @@ $toggle-btn-size: 50px; z-index: 1000; position: fixed; width: $wrapper-width; - left: $wrapper-width; height: 100%; margin-left: -$wrapper-width; -webkit-transition: all 0.5s ease; @@ -99,6 +98,7 @@ $toggle-btn-size: 50px; padding-left: 0; #sidebar-wrapper { + margin-left: 0; width: 0; #slide-panel { diff --git a/app/assets/stylesheets/themes/scinote.scss b/app/assets/stylesheets/themes/scinote.scss index 1d6252158..42eae4465 100644 --- a/app/assets/stylesheets/themes/scinote.scss +++ b/app/assets/stylesheets/themes/scinote.scss @@ -62,12 +62,62 @@ table { #content-wrapper { margin-top: 50px; + margin-left: 83px; &.alert-shown { margin-top: 102px; } } +.menu-bar { + background-color: $color-white; + box-shadow: inset -4px 0 0 0 $color-alto; + height: 100%; + left: 0; + overflow-x: hidden; + padding-bottom: 16px; + padding-top: 16px; + position: fixed; + width: 83px; + + ul.nav { + & > li { + padding-right: 4px; + + & > a { + color: $color-gray; + display: grid; + font-size: 12px; + margin-left: auto; + margin-right: auto; + padding: 10px; + text-align: center; + + & > span { + padding-top: 4px; + } + } + &.active { + background-color: $color-gainsboro; + margin-right: 4px; + padding-right: 0; + @include box-shadow(4px 0 0 $color-theme-primary); + + &> a { + color: $color-emperor; + } + } + } + } + + ul.nav-bottom { + bottom: 0; + padding-bottom: 16px; + position: fixed; + width: inherit; + } +} + .center-block-narrow { max-width: 400px; } diff --git a/app/helpers/left_menu_bar_helper.rb b/app/helpers/left_menu_bar_helper.rb new file mode 100644 index 000000000..5b7d91d78 --- /dev/null +++ b/app/helpers/left_menu_bar_helper.rb @@ -0,0 +1,31 @@ +module LeftMenuBarHelper + def projects_are_selected? + controller_name.in? %w(projects experiments my_modules) + end + + def repositories_are_selected? + controller_name == 'repositories' + end + + def templates_are_selected? + # TBD + controller_name == 'protocols' + end + + def reports_are_selected? + # TBD + controller_name == 'reports' + end + + def settings_are_selected? + controller_name.in? %(registrations preferences addons teams) + end + + def activities_are_selected? + controller_name == 'activities' + end + + def help_is_selected? + # TBD + end +end diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index f9c2ae09b..008f3885a 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -64,6 +64,7 @@ <% end %> + <%= render "shared/left_menu_bar" %>
"> <%= yield :content %>
diff --git a/app/views/shared/_left_menu_bar.html.erb b/app/views/shared/_left_menu_bar.html.erb new file mode 100644 index 000000000..805727fa2 --- /dev/null +++ b/app/views/shared/_left_menu_bar.html.erb @@ -0,0 +1,48 @@ + diff --git a/config/locales/en.yml b/config/locales/en.yml index 118a62d4f..b041b7a11 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -101,6 +101,15 @@ en: core_version: "sciNote core version" addon_versions: "Addon versions" + left_menu_bar: + projects: "Projects" + repositories: "Libraries" + templates: "Templates" + reports: "Reports" + settings: "Settings" + activities: "Activities" + help: "Help" + sidebar: title: "Navigation" no_module_group: "No workflow" From 9fd845c4786e23bfb63f9f4354711dd5c943e6b7 Mon Sep 17 00:00:00 2001 From: mlorb Date: Wed, 14 Mar 2018 17:55:04 +0100 Subject: [PATCH 06/14] fix failing cucumber tests --- app/views/layouts/application.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 008f3885a..4d1b16281 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -64,7 +64,7 @@ <% end %> - <%= render "shared/left_menu_bar" %> + <%= render "shared/left_menu_bar" if user_signed_in? %>
"> <%= yield :content %>
From d69754af04ca3ec3e257c0ac37609b6a7e959110 Mon Sep 17 00:00:00 2001 From: zmagod Date: Thu, 15 Mar 2018 15:43:16 +0100 Subject: [PATCH 07/14] implement search on list items dropdown, update, create actions [fixes SCI-2070] --- Gemfile | 2 +- Gemfile.lock | 4 +- app/assets/javascripts/application.js.erb | 5 +- ...tatable.js => repository_datatable.js.erb} | 130 +++++++++++++++++- app/assets/stylesheets/application.scss | 1 + .../repository_list_items_controller.rb | 26 ++++ app/controllers/repository_rows_controller.rb | 126 +++++++++++++---- app/models/repository_column.rb | 2 + .../repositories/_repository_table.html.erb | 2 +- config/routes.rb | 4 +- spec/models/asset_text_datum_spec.rb | 1 - .../javascripts/ajax-bootstrap-select.min.js | 21 +++ .../stylesheets/ajax-bootstrap-select.min.css | 16 +++ 13 files changed, 299 insertions(+), 41 deletions(-) rename app/assets/javascripts/repositories/{repository_datatable.js => repository_datatable.js.erb} (91%) create mode 100644 app/controllers/repository_list_items_controller.rb create mode 100644 vendor/assets/javascripts/ajax-bootstrap-select.min.js create mode 100644 vendor/assets/stylesheets/ajax-bootstrap-select.min.css diff --git a/Gemfile b/Gemfile index ff658fde3..5aa5b6bf2 100644 --- a/Gemfile +++ b/Gemfile @@ -26,7 +26,7 @@ gem 'momentjs-rails', '~> 2.17.1' # JS datetime picker gem 'bootstrap3-datetimepicker-rails', '~> 4.15.35' # Select elements for Bootstrap -gem 'bootstrap-select-rails', '~> 1.6.3' +gem 'bootstrap-select-rails', '~> 1.12.4' gem 'uglifier', '>= 1.3.0' # jQuery & plugins gem 'jquery-turbolinks' diff --git a/Gemfile.lock b/Gemfile.lock index 386d75f4b..bf469ae14 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -127,7 +127,7 @@ GEM bootstrap-sass (3.3.7) autoprefixer-rails (>= 5.2.1) sass (>= 3.3.4) - bootstrap-select-rails (1.6.3) + bootstrap-select-rails (1.12.4) bootstrap3-datetimepicker-rails (4.15.35) momentjs-rails (>= 2.8.1) bootstrap_form (2.7.0) @@ -518,7 +518,7 @@ DEPENDENCIES better_errors binding_of_caller bootstrap-sass (~> 3.3.5) - bootstrap-select-rails (~> 1.6.3) + bootstrap-select-rails (~> 1.12.4) bootstrap3-datetimepicker-rails (~> 4.15.35) bootstrap_form bullet diff --git a/app/assets/javascripts/application.js.erb b/app/assets/javascripts/application.js.erb index f5051cee4..6a2b7b168 100644 --- a/app/assets/javascripts/application.js.erb +++ b/app/assets/javascripts/application.js.erb @@ -31,11 +31,12 @@ //= require tinymce-jquery //= require jsPlumb-2.0.4-min //= require jsnetworkx +//= require dataTables.noSearchHidden +//= require bootstrap-select //= require_directory ./sitewide //= require jquery.dataTables.yadcf //= require datatables -//= require dataTables.noSearchHidden -//= require bootstrap-select +//= require ajax-bootstrap-select.min //= require underscore //= require i18n.js //= require i18n/translations diff --git a/app/assets/javascripts/repositories/repository_datatable.js b/app/assets/javascripts/repositories/repository_datatable.js.erb similarity index 91% rename from app/assets/javascripts/repositories/repository_datatable.js rename to app/assets/javascripts/repositories/repository_datatable.js.erb index f87c31015..98fa39f4e 100644 --- a/app/assets/javascripts/repositories/repository_datatable.js +++ b/app/assets/javascripts/repositories/repository_datatable.js.erb @@ -432,14 +432,20 @@ var RepositoryDatatable = (function(global) { } else if ($(th).attr('id') === 'row-name') { input = changeToInputField('repository_row', 'name', ''); tr.appendChild(createTdElement(input)); - } else if ($(th).hasClass('repository-column')) { + } else if ($(th).hasClass('repository-column') && + $(th).attr('data-type') === 'RepositoryTextValue') { input = changeToInputField('repository_cell', th.attr('id'), ''); tr.appendChild(createTdElement(input)); + } else if ($(th).hasClass('repository-column') && + $(th).attr('data-type') === 'RepositoryListValue') { + input = initialListItemsRequest($(th).attr('id')); + tr.appendChild(createTdElement(input)); } else { // Column we don't care for, just add empty td tr.appendChild(createTdElement('')); } }); + $('table' + TABLE_ID).prepend(tr); selectedRecord = tr; @@ -451,6 +457,8 @@ var RepositoryDatatable = (function(global) { }); // Adjust columns width in table header adjustTableHeader(); + // Init selectpicker + _initSelectPicker(); } global.onClickToggleAssignedRecords = function() { @@ -584,6 +592,7 @@ var RepositoryDatatable = (function(global) { // Take care of custom cells var cells = data.repository_row.repository_cells; + var list_columns = data.repository_row.repository_column_items; $(node).children('td').each(function(i) { var td = $(this); var rawIndex = TABLE.column.index('fromVisible', i); @@ -592,13 +601,17 @@ var RepositoryDatatable = (function(global) { // Check if cell on this record exists var cell = cells[$(colHeader).attr('id')]; if (cell) { - td.html(changeToInputField('repository_cell', + td.html(changeToFormField('repository_cell', $(colHeader).attr('id'), - cell.value)); + cell, + list_columns)); } else { - td.html(changeToInputField('repository_cell', - $(colHeader).attr('id'), '')); + td.html(changeToFormField('repository_cell', + $(colHeader).attr('id'), + '', + list_columns)); } + _initSelectPicker(); } }); @@ -647,13 +660,18 @@ var RepositoryDatatable = (function(global) { // Record name data.repository_row.name = $('td input[data-object = repository_row]').val(); - // Custom cells + // Custom cells text type $(node).find('td input[data-object = repository_cell]').each(function() { // Send data only and only if cell is not empty if ($(this).val().trim()) { data.repository_cells[$(this).attr('name')] = $(this).val(); } }); + // Custom cells list type + $(node).find('td[column_id]').each(function(index, el) { + var value = $(el).attr('list_item_id'); + data.repository_cells[$(el).attr('column_id')] = value; + }); var url; var type; @@ -845,6 +863,88 @@ var RepositoryDatatable = (function(global) { }); // Helper functions + function _listItemDropdown(options, current_value, column_id) { + var html = ''; + return html; + } + + function initialListItemsRequest(column_id) { + var massage_response = []; + $.ajax({ + url: '<%= Rails.application.routes.url_helpers.repository_list_items_path %>', + type: 'POST', + dataType: 'json', + async: false, + data: { + q: '', + column_id: column_id + } + }).done(function(data) { + $.each(data.list_items, function(index, el) { + massage_response.push([el.id, el.data]); + }) + }); + return _listItemDropdown(massage_response, '-1', column_id); + } + + function _initSelectPicker() { + $('.selectpicker') + .selectpicker({liveSearch: true}) + .ajaxSelectPicker({ + ajax: { + url: '<%= Rails.application.routes.url_helpers.repository_list_items_path %>', + type: 'POST', + dataType: 'json', + data: function () { + var params = { + q: '{{{q}}}', + column_id: $(this.valueOf().plugin.$element).attr('column_id') + }; + + return params; + } + }, + locale: { + emptyTitle: 'Nothing selected' + }, + preprocessData: function(data){ + var items = []; + if(data.hasOwnProperty('list_items')){ + items.push({ + 'value': '-1', + 'text': '', + 'disabled': false + }); + $.each(data.list_items, function(index, el) { + items.push( + { + 'value': el.id, + 'text': el.data, + 'disabled': false + } + ) + }); + } + return items; + }, + emptyRequest: true, + clearOnEmpty: false, + preserveSelected: false + }).on('change.bs.select', function(el) { + $(this).closest('td').attr('list_item_id', el.target.value); + $(this).closest('td').attr('column_id', $(this).attr('column_id')); + });; + } + function getColumnIndex(id) { if (id < 0) { return false; @@ -858,6 +958,24 @@ var RepositoryDatatable = (function(global) { object + "' name='" + name + "' value='" + value + "'>"; } + // Takes an object and creates custom html element + function changeToFormField(object, name, cell, list_columns) { + if (cell === '') { + var column = _.findWhere(list_columns, { column_id: parseInt(name) }); + if (column) { + return _listItemDropdown(column.list_items, '', parseInt(name)); + } else { + return changeToInputField(object, name, ''); + } + } else { + if (cell.type === 'RepositoryListValue') { + return _listItemDropdown(cell.list_items, cell.value, parseInt(name)); + } else { + return changeToInputField(object, name, cell.value); + } + } + } + // Return td element with content function createTdElement(content) { var td = document.createElement('td'); diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 023bd4d94..eeb5db641 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -16,6 +16,7 @@ @import "bootstrap-tagsinput"; @import "bootstrap-tagsinput-typeahead"; @import "handsontable.full.min"; +@import "ajax-bootstrap-select.min"; @import "extend/bootstrap"; @import "font-awesome"; @import "themes/scinote"; diff --git a/app/controllers/repository_list_items_controller.rb b/app/controllers/repository_list_items_controller.rb new file mode 100644 index 000000000..4e8eec34a --- /dev/null +++ b/app/controllers/repository_list_items_controller.rb @@ -0,0 +1,26 @@ +class RepositoryListItemsController < ApplicationController + before_action :load_vars, only: :search + + def search + column_list_items = @repository_column.repository_list_items + .where('data ILIKE ?', + "%#{search_params[:q]}%") + .limit(Constants::SEARCH_LIMIT) + .select(:id, :data) + + render json: { list_items: column_list_items }, status: :ok + end + + private + + def search_params + params.permit(:q, :column_id) + end + + def load_vars + @repository_column = RepositoryColumn.find_by_id(search_params[:column_id]) + repository = @repository_column.repository if @repository_column + render_404 and return unless @repository_column&.data_type == "RepositoryListValue" + render_403 unless can_manage_repository_rows?(repository.team) + end +end diff --git a/app/controllers/repository_rows_controller.rb b/app/controllers/repository_rows_controller.rb index 4c7303a30..2c0099ca5 100644 --- a/app/controllers/repository_rows_controller.rb +++ b/app/controllers/repository_rows_controller.rb @@ -38,15 +38,31 @@ class RepositoryRowsController < ApplicationController column = @repository.repository_columns.detect do |c| c.id == key.to_i end - cell_value = RepositoryTextValue.new( - data: value, - created_by: current_user, - last_modified_by: current_user, - repository_cell_attributes: { - repository_row: record, - repository_column: column - } - ) + if column.data_type == 'RepositoryListValue' + next if value == '-1' + # check if item existx else revert the transaction + list_item = RepositoryListItem.where(repository_column: column) + .find(value) + cell_value = RepositoryListValue.new( + repository_list_item_id: list_item.id, + created_by: current_user, + last_modified_by: current_user, + repository_cell_attributes: { + repository_row: record, + repository_column: column + } + ) + else + cell_value = RepositoryTextValue.new( + data: value, + created_by: current_user, + last_modified_by: current_user, + repository_cell_attributes: { + repository_row: record, + repository_column: column + } + ) + end if cell_value.save record_annotation_notification(record, cell_value.repository_cell) else @@ -91,7 +107,8 @@ class RepositoryRowsController < ApplicationController json = { repository_row: { name: escape_input(@record.name), - repository_cells: {} + repository_cells: {}, + repository_column_items: fetch_columns_list_items } } @@ -99,7 +116,9 @@ class RepositoryRowsController < ApplicationController @record.repository_cells.each do |cell| json[:repository_row][:repository_cells][cell.repository_column_id] = { repository_cell_id: cell.id, - value: escape_input(cell.value.data) + value: escape_input(cell.value.data), + type: cell.value_type, + list_items: fetch_list_items(cell) } end @@ -125,14 +144,26 @@ class RepositoryRowsController < ApplicationController end if existing # Cell exists and new value present, so update value - existing.value.data = value - if existing.value.save - record_annotation_notification(@record, existing) + if existing.value_type == 'RepositoryListValue' + item = RepositoryListItem.where( + repository_column: existing.repository_column + ).find(value) unless value == '-1' + if item + existing.repository_list_value + .update_attribute(:repository_list_item_id, item) + else + existing.delete + end else - errors[:repository_cells] << { - "#{existing.repository_column_id}": - existing.value.errors.messages - } + existing.value.data = value + if existing.value.save + record_annotation_notification(@record, existing) + else + errors[:repository_cells] << { + "#{existing.repository_column_id}": + existing.value.errors.messages + } + end end else # Looks like it is a new cell, so we need to create new value, cell @@ -140,15 +171,31 @@ class RepositoryRowsController < ApplicationController column = @repository.repository_columns.detect do |c| c.id == key.to_i end - cell_value = RepositoryTextValue.new( - data: value, - created_by: current_user, - last_modified_by: current_user, - repository_cell_attributes: { - repository_row: @record, - repository_column: column - } - ) + if column.data_type == 'RepositoryListValue' + next if value == '-1' + # check if item existx else revert the transaction + list_item = RepositoryListItem.where(repository_column: column) + .find(value) + cell_value = RepositoryListValue.new( + repository_list_item_id: list_item.id, + created_by: current_user, + last_modified_by: current_user, + repository_cell_attributes: { + repository_row: @record, + repository_column: column + } + ) + else + cell_value = RepositoryTextValue.new( + data: value, + created_by: current_user, + last_modified_by: current_user, + repository_cell_attributes: { + repository_row: @record, + repository_column: column + } + ) + end if cell_value.save record_annotation_notification(@record, cell_value.repository_cell) @@ -161,6 +208,7 @@ class RepositoryRowsController < ApplicationController end # Clean up empty cells, not present in updated record @record.repository_cells.each do |cell| + next if cell.value_type == 'RepositoryListValue' cell.value.destroy unless cell_params .key?(cell.repository_column_id.to_s) end @@ -293,4 +341,28 @@ class RepositoryRowsController < ApplicationController column: link_to(cell.repository_column.name, table_url)) ) end + + def fetch_list_items(cell) + return [] if cell.value_type != 'RepositoryListValue' + RepositoryListItem.where(repository: @repository) + .where(repository_column: cell.repository_column) + .limit(Constants::SEARCH_LIMIT) + .pluck(:id, :data) + end + + def fetch_columns_list_items + collection = [] + @repository.repository_columns + .list_type + .preload(:repository_list_items) + .each do |column| + collection << { + column_id: column.id, + list_items: column.repository_list_items + .limit(Constants::SEARCH_LIMIT) + .pluck(:id, :data) + } + end + collection + end end diff --git a/app/models/repository_column.rb b/app/models/repository_column.rb index ff3e8bb5f..40a54de05 100644 --- a/app/models/repository_column.rb +++ b/app/models/repository_column.rb @@ -21,6 +21,8 @@ class RepositoryColumn < ApplicationRecord after_create :update_repository_table_state + scope :list_type, -> { where(data_type: 'RepositoryListValue') } + def update_repository_table_state RepositoryTableState.update_state(self, nil, created_by) end diff --git a/app/views/repositories/_repository_table.html.erb b/app/views/repositories/_repository_table.html.erb index 0542b9469..b1b0ceb71 100644 --- a/app/views/repositories/_repository_table.html.erb +++ b/app/views/repositories/_repository_table.html.erb @@ -20,7 +20,7 @@ <%= t("repositories.table.added_on") %> <%= t("repositories.table.added_by") %> <% repository.repository_columns.order(:id).each do |column| %> - <%= 'data-deletable' if can_update_or_delete_repository_column?(column) %> <%= "data-edit-url='#{edit_repository_repository_column_path(repository, column)}'" %> diff --git a/config/routes.rb b/config/routes.rb index e550e303d..3b8e77004 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -472,7 +472,6 @@ Rails.application.routes.draw do as: 'columns_destroy_html' resources :repository_columns, only: %i(create edit update destroy) - resources :repository_rows, only: %i(create edit update) member do post 'parse_sheet' @@ -480,6 +479,9 @@ Rails.application.routes.draw do end end + post 'repository_list_items', to: 'repository_list_items#search', + defaults: { format: 'json' } + get 'repository_rows/:id', to: 'repository_rows#show', as: :repository_row, defaults: { format: 'json' } diff --git a/spec/models/asset_text_datum_spec.rb b/spec/models/asset_text_datum_spec.rb index e959296df..ee65213bc 100644 --- a/spec/models/asset_text_datum_spec.rb +++ b/spec/models/asset_text_datum_spec.rb @@ -26,7 +26,6 @@ describe AssetTextDatum, type: :model do it 'should have uniq asset' do create :asset_text_datum, asset: asset new_atd = build :asset_text_datum, asset: asset - # binding.pry expect(new_atd).to_not be_valid end end diff --git a/vendor/assets/javascripts/ajax-bootstrap-select.min.js b/vendor/assets/javascripts/ajax-bootstrap-select.min.js new file mode 100644 index 000000000..999fc5996 --- /dev/null +++ b/vendor/assets/javascripts/ajax-bootstrap-select.min.js @@ -0,0 +1,21 @@ +/*! + * Ajax Bootstrap Select + * + * Extends existing [Bootstrap Select] implementations by adding the ability to search via AJAX requests as you type. Originally for CROSCON. + * + * @version 1.4.3 + * @author Adam Heim - https://github.com/truckingsim + * @link https://github.com/truckingsim/Ajax-Bootstrap-Select + * @copyright 2017 Adam Heim + * @license Released under the MIT license. + * + * Contributors: + * Mark Carver - https://github.com/markcarver + * + * Last build: 2017-11-15 1:19:46 PM EST + */ +!function(a,b){var c=function(c,d){var e,f,g=this;d=d||{},this.$element=a(c),this.options=a.extend(!0,{},a.fn.ajaxSelectPicker.defaults,d),this.LOG_ERROR=1,this.LOG_WARNING=2,this.LOG_INFO=3,this.LOG_DEBUG=4,this.lastRequest=!1,this.previousQuery="",this.query="",this.request=!1;var h=[{from:"ajaxResultsPreHook",to:"preprocessData"},{from:"ajaxSearchUrl",to:{ajax:{url:"{{{value}}}"}}},{from:"ajaxOptions",to:"ajax"},{from:"debug",to:function(b){var c={};c.log=Boolean(g.options[b.from])?g.LOG_DEBUG:0,g.options=a.extend(!0,{},g.options,c),delete g.options[b.from],g.log(g.LOG_WARNING,'Deprecated option "'+b.from+'". Update code to use:',c)}},{from:"mixWithCurrents",to:"preserveSelected"},{from:"placeHolderOption",to:{locale:{emptyTitle:"{{{value}}}"}}}];h.length&&a.map(h,function(b){if(g.options[b.from])if(a.isPlainObject(b.to))g.replaceValue(b.to,"{{{value}}}",g.options[b.from]),g.options=a.extend(!0,{},g.options,b.to),g.log(g.LOG_WARNING,'Deprecated option "'+b.from+'". Update code to use:',b.to),delete g.options[b.from];else if(a.isFunction(b.to))b.to.apply(g,[b]);else{var c={};c[b.to]=g.options[b.from],g.options=a.extend(!0,{},g.options,c),g.log(g.LOG_WARNING,'Deprecated option "'+b.from+'". Update code to use:',c),delete g.options[b.from]}});var i=this.$element.data();i.searchUrl&&(g.log(g.LOG_WARNING,'Deprecated attribute name: "data-search-url". Update markup to use: \' data-abs-ajax-url="'+i.searchUrl+"\" '"),this.options.ajax.url=i.searchUrl);var j=function(a,b){return b.toLowerCase()},k=function(a,b,c){var d=[].concat(a),e=d.length,f=c||{};if(e){var g=d.shift();f[g]=k(d,b,f[g])}return e?f:b},l=Object.keys(i).filter(/./.test.bind(new RegExp("^abs[A-Z]")));if(l.length){var m={},n=["locale"];for(e=0,f=l.length;e1&&n.indexOf(p[0])!==-1){for(var q=[p.shift()],r="",s=0;s0)&&f.replaceValue(b[g],c,d,e):h===c&&(e.limit!==!1&&"number"==typeof e.limit&&e.limit--,b[g]=d))})},c.prototype.t=function(a,b){return b=b||this.options.langCode,this.locale[b]&&this.locale[b].hasOwnProperty(a)?this.locale[b][a]:(this.log(this.LOG_WARNING,"Unknown translation key:",a),a)},b.AjaxBootstrapSelect=b.AjaxBootstrapSelect||c;var d=function(b){var c=this;this.$status=a(b.options.templates.status).hide().appendTo(b.selectpicker.$menu);var d=b.t("statusInitialized");d&&d.length&&this.setStatus(d),this.cache={},this.plugin=b,this.selected=[],this.title=null,this.selectedTextFormat=b.selectpicker.options.selectedTextFormat;var e=[];b.$element.find("option").each(function(){var c=a(this),d=c.attr("value");e.push({value:d,text:c.text(),class:c.attr("class")||"",data:c.data()||{},preserved:b.options.preserveSelected,selected:!!c.attr("selected")})}),this.cacheSet("",e),b.options.preserveSelected&&(c.selected=e,b.$element.on("change.abs.preserveSelected",function(d){var e=b.$element.find(":selected");c.selected=[],b.selectpicker.multiple||(e=e.last()),e.each(function(){var b=a(this),d=b.attr("value");c.selected.push({value:d,text:b.text(),class:b.attr("class")||"",data:b.data()||{},preserved:!0,selected:!0})}),c.replaceOptions(c.cacheGet(c.plugin.query))}))};d.prototype.build=function(b){var c,d,e=b.length,f=a("