adds unit specs for datatable service [fixes SCI-2068]

This commit is contained in:
zmagod 2018-03-09 14:22:00 +01:00
parent 26883af386
commit 4f951f6679
11 changed files with 260 additions and 370 deletions

View file

@ -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

View file

@ -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

View file

@ -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)
"<span class='circle'>&nbsp;</span>"
else
"<span class='circle disabled'>&nbsp;</span>"
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

View file

@ -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

View file

@ -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]

View file

@ -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!

View file

@ -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

View file

@ -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

View file

@ -1,5 +0,0 @@
require 'rails_helper'
RSpec.describe SearchRepository, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
end

View file

@ -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

View file

@ -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" }
}
}
}
}
}