Merge pull request #4701 from okriuchykhin/ok_SCI_7516

Add versions and access columns to protocols table, update protocol model [SCI-7516]
This commit is contained in:
aignatov-bio 2022-12-21 13:03:38 +01:00 committed by GitHub
commit 7437af540b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 276 additions and 142 deletions

View file

@ -55,21 +55,24 @@ function initProtocolsTable() {
</div>`;
}
}, {
targets: [ 1, 2, 3, 4, 5 ],
targets: [1, 2, 3, 4, 5, 6, 7, 8, 9],
searchable: true,
orderable: true
}],
columns: [
{ data: "0" },
{ data: "1" },
{ data: "2" },
{ data: '0' },
{ data: '1' },
{ data: '2' },
{ data: '3' },
{ data: '4' },
{
data: "3",
visible: repositoryType != "archive"
data: '5',
visible: repositoryType !== 'archived'
},
{ data: "4" },
{ data: "5" },
{ data: "6" }
{ data: '6' },
{ data: '7' },
{ data: '8' },
{ data: '9' }
],
oLanguage: {
sSearch: I18n.t('general.filter')
@ -232,7 +235,7 @@ function initKeywordFiltering() {
function initProtocolPreviewModal() {
// Only do this if the repository is public/private
if (repositoryType !== "archive") {
if (repositoryType !== 'archived') {
// If you are in protocol repository
var protocolsEl = protocolsTableEl;
// If you are in search results
@ -271,7 +274,7 @@ function initProtocolPreviewModal() {
function initLinkedChildrenModal() {
// Only do this if the repository is public/private
if (repositoryType !== "archive") {
if (repositoryType !== "archived") {
protocolsTableEl.on("click", "a[data-action='load-linked-children']", function(e) {
var link = $(this);
$.ajax({

View file

@ -60,6 +60,51 @@
border: 0;
height: calc(100vh - 167px);
}
.protocol-users-link {
align-items: center;
color: $color-silver-chalice;
display: flex;
&:hover {
text-decoration: none;
}
}
.users-access-cell {
a:hover {
text-decoration: none;
}
.value {
display: flex;
flex-wrap: wrap;
}
.global-avatar-container {
height: 2em;
line-height: 2em;
margin-right: .25em;
width: 2em;
}
.more-users {
background: $color-volcano;
border-radius: 50%;
color: $color-white;
height: 2em;
line-height: 2em;
margin-right: .25em;
text-align: center;
text-decoration: none;
width: 2em;
}
.new-user {
background: $color-concrete;
text-align: center;
}
}
}
.tab-pane.protocolsio {

View file

@ -233,7 +233,7 @@ class ProtocolsController < ApplicationController
def create
@protocol = Protocol.new(
team: @current_team,
protocol_type: Protocol.protocol_types[@type == :public ? :in_repository_public : :in_repository_private],
protocol_type: Protocol.protocol_types[:in_repository_draft],
added_by: current_user,
name: t('protocols.index.default_name')
)
@ -1105,7 +1105,7 @@ class ProtocolsController < ApplicationController
def load_team_and_type
@current_team = current_team
# :public, :private or :archive
@type = (params[:type] || 'public').to_sym
@type = (params[:type] || 'active').to_sym
end
def check_view_all_permissions

View file

@ -23,12 +23,15 @@ class ProtocolsDatatable < CustomDatatable
def sortable_columns
@sortable_columns ||= [
"Protocol.name",
"protocol_keywords_str",
"Protocol.nr_of_linked_children",
"full_username_str",
'Protocol.name',
'Protocol.id',
'nr_of_versions',
'protocol_keywords_str',
'Protocol.nr_of_linked_children',
'nr_of_assigned_users',
'full_username_str',
timestamp_db_column,
"Protocol.updated_at"
'Protocol.updated_at'
]
end
@ -77,57 +80,55 @@ class ProtocolsDatatable < CustomDatatable
# Returns json of current protocols (already paginated)
def data
result_data = []
records.each do |record|
protocol = Protocol.find(record.id)
result_data << {
'DT_RowId': record.id,
'DT_RowAttr': {
'data-permissions-url': permissions_protocol_path(protocol)
},
'1': if protocol.in_repository_archived?
escape_input(record.name)
else
name_html(record)
end,
'2': keywords_html(record),
'3': modules_html(record),
'4': escape_input(record.full_username_str),
'5': timestamp_column_html(record),
'6': I18n.l(record.updated_at, format: :full)
records.map do |record|
{
DT_RowId: record.id,
DT_CanClone: can_clone_protocol_in_repository?(record),
DT_CloneUrl: if can_clone_protocol_in_repository?(record)
clone_protocol_path(record, team: @team, type: @type)
end,
'1': record.archived? ? escape_input(record.name) : name_html(record),
'2': record.code,
'3': versions_html(record),
'4': keywords_html(record),
'5': modules_html(record),
'6': access_html(record),
'7': escape_input(record.full_username_str),
'8': timestamp_column_html(record),
'9': I18n.l(record.updated_at, format: :full)
}
end
result_data
end
def get_raw_records_base
records =
Protocol
.where(team: @team)
.joins('LEFT OUTER JOIN "protocol_protocol_keywords" ON "protocol_protocol_keywords"."protocol_id" = "protocols"."id"')
.joins('LEFT OUTER JOIN "protocol_keywords" ON "protocol_protocol_keywords"."protocol_keyword_id" = "protocol_keywords"."id"')
.where('protocols.protocol_type = ? OR protocols.protocol_type = ? AND protocols.parent_id IS NULL',
Protocol.protocol_types[:in_repository_published_original],
Protocol.protocol_types[:in_repository_draft])
.joins('LEFT OUTER JOIN "user_assignments" "all_user_assignments" '\
'ON "all_user_assignments"."assignable_type" = \'Protocol\' '\
'AND "all_user_assignments"."assignable_id" = "protocols"."id"')
.joins("LEFT OUTER JOIN protocols protocol_versions "\
"ON protocol_versions.protocol_type = #{Protocol.protocol_types[:in_repository_published_version]} "\
"AND protocol_versions.parent_id = protocols.id")
.joins("LEFT OUTER JOIN protocols protocol_drafts "\
"ON protocol_drafts.protocol_type = #{Protocol.protocol_types[:in_repository_draft]} "\
"AND protocol_drafts.parent_id = protocols.id")
.joins('LEFT OUTER JOIN "protocol_protocol_keywords" '\
'ON "protocol_protocol_keywords"."protocol_id" = "protocols"."id"')
.joins('LEFT OUTER JOIN "protocol_keywords" '\
'ON "protocol_protocol_keywords"."protocol_keyword_id" = "protocol_keywords"."id"')
.with_granted_permissions(@user, ProtocolPermissions::READ)
.preload(user_assignments: %i(user user_role))
if @type == :public
records =
records
.joins('LEFT OUTER JOIN users ON users.id = protocols.added_by_id')
.where('protocols.protocol_type = ?',
Protocol.protocol_types[:in_repository_public])
elsif @type == :private
records =
records
.joins('LEFT OUTER JOIN users ON users.id = protocols.added_by_id')
.where('protocols.protocol_type = ?',
Protocol.protocol_types[:in_repository_private])
.where(added_by: @user)
else
records =
records
.joins('LEFT OUTER JOIN users ON users.id = protocols.archived_by_id')
.where('protocols.protocol_type = ?',
Protocol.protocol_types[:in_repository_archived])
.where(added_by: @user)
end
records =
if @type == :archived
records.joins('LEFT OUTER JOIN "users" ON "users"."id" = "protocols"."archived_by_id"').archived
else
records.joins('LEFT OUTER JOIN "users" ON "users"."id" = "protocols"."added_by_id"').active
end
records.group('"protocols"."id"')
end
@ -135,30 +136,23 @@ class ProtocolsDatatable < CustomDatatable
# Query database for records (this will be later paginated and filtered)
# after that "data" function will return json
def get_raw_records
get_raw_records_base
.select(
'"protocols"."id"',
'"protocols"."name"',
'"protocols"."protocol_type"',
'string_agg("protocol_keywords"."name", \', \') AS "protocol_keywords_str"',
'"protocols"."nr_of_linked_children"',
'max("users"."full_name") AS "full_username_str"', # "Hack" to get single username
'"protocols"."created_at"',
'"protocols"."updated_at"',
'"protocols"."published_on"',
'"protocols"."archived_on"'
get_raw_records_base.select(
'"protocols".*',
'STRING_AGG("protocol_keywords"."name", \', \') AS "protocol_keywords_str"',
'COUNT("protocol_versions"."id") + 1 AS "nr_of_versions"',
'COUNT("protocol_drafts"."id") AS "nr_of_drafts"',
'COUNT("user_assignments"."id") AS "nr_of_assigned_users"',
'MAX("users"."full_name") AS "full_username_str"' # "Hack" to get single username
)
end
# Various helper methods
def timestamp_db_column
if @type == :public
"Protocol.published_on"
elsif @type == :private
"Protocol.created_at"
if @type == :archived
'Protocol.archived_on'
else
"Protocol.archived_on"
'Protocol.published_on'
end
end
@ -188,13 +182,20 @@ class ProtocolsDatatable < CustomDatatable
"</a>"
end
def versions_html(record)
@view.controller
.render_to_string(partial: 'protocols/index/protocol_versions.html.erb', locals: { protocol: record })
end
def access_html(record)
@view.controller.render_to_string(partial: 'protocols/index/protocol_access.html.erb', locals: { protocol: record })
end
def timestamp_column_html(record)
if @type == :public
I18n.l(record.published_on, format: :full)
elsif @type == :private
I18n.l(record.created_at, format: :full)
else
if @type == :archived
I18n.l(record.archived_on, format: :full)
else
I18n.l(record.published_on || record.created_at, format: :full)
end
end
@ -206,27 +207,27 @@ class ProtocolsDatatable < CustomDatatable
# using HAVING keyword (which is the correct way to filter aggregated columns).
# Another OR is then appended to the WHERE clause, checking if protocol is inside
# this list of IDs.
def build_conditions_for(query)
# Inner query to retrieve list of protocol IDs where concatenated
# protocol keywords string, or user's full_name contains searched query
search_val = dt_params[:search][:value]
records_having = get_raw_records_base.having(
::Arel::Nodes::NamedFunction.new(
'CAST',
[::Arel::Nodes::SqlLiteral.new("string_agg(\"protocol_keywords\".\"name\", ' ') AS #{typecast}")]
).matches("%#{sanitize_sql_like(search_val)}%").to_sql +
" OR " +
::Arel::Nodes::NamedFunction.new(
'CAST',
[::Arel::Nodes::SqlLiteral.new("max(\"users\".\"full_name\") AS #{typecast}")]
).matches("%#{sanitize_sql_like(search_val)}%").to_sql
).select(:id)
# def build_conditions_for(query)
# # Inner query to retrieve list of protocol IDs where concatenated
# # protocol keywords string, or user's full_name contains searched query
# search_val = dt_params[:search][:value]
# records_having = get_raw_records_base.having(
# ::Arel::Nodes::NamedFunction.new(
# 'CAST',
# [::Arel::Nodes::SqlLiteral.new("string_agg(\"protocol_keywords\".\"name\", ' ') AS #{typecast}")]
# ).matches("%#{sanitize_sql_like(search_val)}%").to_sql +
# " OR " +
# ::Arel::Nodes::NamedFunction.new(
# 'CAST',
# [::Arel::Nodes::SqlLiteral.new("max(\"users\".\"full_name\") AS #{typecast}")]
# ).matches("%#{sanitize_sql_like(search_val)}%").to_sql
# ).select(:id)
# Call parent function
criteria = super(query)
# # Call parent function
# criteria = super(query)
# Aight, now append another or
criteria = criteria.or(Protocol.arel_table[:id].in(records_having.arel))
criteria
end
# # Aight, now append another or
# criteria = criteria.or(Protocol.arel_table[:id].in(records_having.arel))
# criteria
# end
end

View file

@ -1,7 +1,10 @@
# frozen_string_literal: true
class Protocol < ApplicationRecord
ID_PREFIX = 'PT'
include ArchivableModel
include PrefixedIdModel
include SearchableModel
include RenamingUtil
include SearchableByNameModel
@ -18,8 +21,9 @@ class Protocol < ApplicationRecord
enum protocol_type: {
unlinked: 0,
linked: 1,
in_repository_published: 2,
in_repository_draft: 3
in_repository_published_original: 2,
in_repository_draft: 3,
in_repository_published_version: 4
}
auto_strip_attributes :name, :description, nullify: false
@ -28,9 +32,9 @@ class Protocol < ApplicationRecord
validates :description, length: { maximum: Constants::RICH_TEXT_MAX_LENGTH }
validates :team, presence: true
validates :protocol_type, presence: true
validate :prevent_update, on: :update, if: :in_repository_published?
# Only one draft can exist for each protocol
validates :previous_version_id, uniqueness: true, if: -> { in_repository_draft? && previous_version_id.present? }
validate :prevent_update,
on: :update,
if: -> { in_repository_published? && !protocol_type_changed?(from: 'in_repository_draft') }
with_options if: :in_module? do
validates :my_module, presence: true
@ -38,20 +42,27 @@ class Protocol < ApplicationRecord
validates :archived_on, absence: true
end
with_options if: :linked? do
validate :parent_type_constrain
validate :linked_parent_type_constrain
validates :added_by, presence: true
validates :parent, presence: true
validates :parent_updated_at, presence: true
end
with_options if: :in_repository? do
validates :name, presence: true
validate :versions_same_name_constrain
validates :added_by, presence: true
validates :my_module, absence: true
validates :parent, absence: true
validates :parent_updated_at, absence: true
end
with_options if: -> { in_repository? && active? && !previous_version } do |protocol|
with_options if: :in_repository_published_version? do
validates :parent, presence: true
validate :versions_same_name_constrain
end
with_options if: :in_repository_draft? do
# Only one draft can exist for each protocol
validates :parent_id, uniqueness: true, if: -> { parent_id.present? }
validate :versions_same_name_constrain
end
with_options if: -> { in_repository? && active? && !parent } do |protocol|
# Active protocol must have unique name inside its team
protocol
.validates_uniqueness_of :name, case_sensitive: false,
@ -59,7 +70,7 @@ class Protocol < ApplicationRecord
conditions: lambda {
active.where(
protocol_type: [
Protocol.protocol_types[:in_repository_published],
Protocol.protocol_types[:in_repository_published_original],
Protocol.protocol_types[:in_repository_draft]
]
)
@ -73,7 +84,7 @@ class Protocol < ApplicationRecord
conditions: lambda {
archived.where(
protocol_type: [
Protocol.protocol_types[:in_repository_published],
Protocol.protocol_types[:in_repository_published_original],
Protocol.protocol_types[:in_repository_draft]
]
)
@ -285,13 +296,17 @@ class Protocol < ApplicationRecord
end
def in_repository_active?
in_repository && active?
in_repository? && active?
end
def in_repository?
in_repository_published? || in_repository_draft?
end
def in_repository_published?
in_repository_published_original? || in_repository_published_version?
end
def in_module?
unlinked? || linked?
end
@ -725,14 +740,26 @@ class Protocol < ApplicationRecord
errors.add(:base, I18n.t('activerecord.errors.models.protocol.unchangable'))
end
def parent_type_constrain
def linked_parent_type_constrain
unless parent.in_repository_published?
errors.add(:base, I18n.t('activerecord.errors.models.protocol.wrong_parent_type'))
end
end
def version_parent_type_constrain
unless parent.in_repository_published_original?
errors.add(:base, I18n.t('activerecord.errors.models.protocol.wrong_parent_type'))
end
end
def draft_parent_type_constrain
unless parent.in_repository_published_original?
errors.add(:base, I18n.t('activerecord.errors.models.protocol.wrong_parent_type'))
end
end
def versions_same_name_constrain
if previous_version.present? && !previous_version.name.eql?(name)
if parent.present? && !parent.name.eql?(name)
errors.add(:base, I18n.t('activerecord.errors.models.protocol.wrong_version_name'))
end
end

View file

@ -70,20 +70,34 @@ Canaid::Permissions.register_for(ProjectFolder) do
end
Canaid::Permissions.register_for(Protocol) do
%i(read_protocol_in_repository
manage_protocol_in_repository
manage_protocol_users)
.each do |perm|
can perm do |_, protocol|
protocol.active?
end
end
# protocol in repository: read, export, read step, read/download step asset
can :read_protocol_in_repository do |user, protocol|
protocol.in_repository_active? && protocol.permission_granted?(user, ProtocolPermissions::READ)
protocol.permission_granted?(user, ProtocolPermissions::READ)
end
# protocol in repository: update, create/update/delete/reorder step,
# toggle private/public visibility, archive
can :manage_protocol_in_repository do |user, protocol|
protocol.in_repository_active? && protocol.permission_granted?(user, ProtocolPermissions::MANAGE)
protocol.permission_granted?(user, ProtocolPermissions::MANAGE)
end
can :manage_protocol_users do |user, protocol|
protocol.permission_granted?(user, ProtocolPermissions::USERS_MANAGE) ||
protocol.team.permission_granted?(user, TeamPermissions::MANAGE)
end
# protocol in repository: restore
can :restore_protocol_in_repository do |user, protocol|
protocol.in_repository_archived? && protocol.permission_granted?(user, ProtocolPermissions::MANAGE)
protocol.archived? && protocol.permission_granted?(user, ProtocolPermissions::MANAGE)
end
# protocol in repository: copy

View file

@ -0,0 +1,14 @@
<div class="users-access-cell">
<% if can_manage_protocol_users?(protocol) %>
<%= link_to edit_access_permissions_protocol_path(protocol), class: 'protocol-users-link', data: { action: 'remote-modal' } do %>
<%= render partial: 'protocols/index/users_list.html.erb', locals: { protocol: protocol } %>
<span class="new-user global-avatar-container">
<i class="fas fa-plus"></i>
</span>
<% end %>
<% else %>
<%= link_to access_permissions_protocol_path(protocol), class: 'protocol-users-link', data: { action: 'remote-modal' } do %>
<%= render partial: 'protocols/index/users_list.html.erb', locals: { protocol: protocol } %>
<% end %>
<% end %>
</div>

View file

@ -0,0 +1,10 @@
<%= link_to versions_modal_protocol_path(protocol), remote: true do %>
<% if protocol.in_repository_published_original? %>
<%= protocol.nr_of_versions %>
<% if protocol.nr_of_drafts.positive? %>
/ <%= t("protocols.index.table.draft") %>
<% end %>
<% elsif protocol.in_repository_draft? %>
<%= t("protocols.index.table.draft") %>
<% end %>
<% end %>

View file

@ -9,20 +9,20 @@
<span class="sci-checkbox-label"></span>
</div>
</th>
<th id="protocol-name"><%= t("protocols.index.thead_name") %></th>
<th id="protocol-keywords"><%= t("protocols.index.thead_keywords") %></th>
<th id="protocol-nr-of-linked-children"><%= t("protocols.index.thead_nr_of_linked_children") %></th>
<% if @type == :public %>
<th id="protocol-published-by"><%= t("protocols.index.thead_published_by") %></th>
<th id="protocol-published-on"><%= t("protocols.index.thead_published_on") %></th>
<% elsif @type == :private %>
<th id="protocol-added-by"><%= t("protocols.index.thead_added_by") %></th>
<th id="protocol-created-at"><%= t("protocols.index.thead_created_at") %></th>
<th id="protocol-name"><%= t("protocols.index.thead.name") %></th>
<th id="protocol-id"><%= t("protocols.index.thead.id") %></th>
<th id="protocol-versions"><%= t("protocols.index.thead.versions") %></th>
<th id="protocol-keywords"><%= t("protocols.index.thead.keywords") %></th>
<th id="protocol-nr-of-linked-children"><%= t("protocols.index.thead.nr_of_linked_children") %></th>
<th id="protocol-access"><%= t("protocols.index.thead.access") %></th>
<% if @type == :archived %>
<th id="protocol-archived-by"><%= t("protocols.index.thead.archived_by") %></th>
<th id="protocol-archived-on"><%= t("protocols.index.thead.archived_on") %></th>
<% else %>
<th id="protocol-archived-by"><%= t("protocols.index.thead_archived_by") %></th>
<th id="protocol-archived-on"><%= t("protocols.index.thead_archived_on") %></th>
<th id="protocol-published-by"><%= t("protocols.index.thead.published_by") %></th>
<th id="protocol-published-on"><%= t("protocols.index.thead.published_on") %></th>
<% end %>
<th id="protocol-updated-at"><%= t("protocols.index.thead_updated_at") %></th>
<th id="protocol-updated-at"><%= t("protocols.index.thead.updated_at") %></th>
</tr>
</thead>
<tbody></tbody>

View file

@ -0,0 +1,12 @@
<% protocol.user_assignments[0..3].each do |user_assigment| %>
<span class="global-avatar-container">
<%= image_tag(avatar_path(user_assigment.user, :icon_small), title: user_name_with_role(user_assigment)) %>
</span>
<% end %>
<% more_users = protocol.user_assignments[4..-1].to_a %>
<% if more_users.any? %>
<span class="more-users" title="<%= user_names_with_roles(more_users) %>">
+<%= more_users.size %>
</span>
<% end %>

View file

@ -2581,16 +2581,22 @@ en:
make_private: "Move to My Protocols"
publish: "Move to Team protocols"
archive_action: "Archive"
thead_name: "Name"
thead_keywords: "Keywords"
thead_nr_of_linked_children: "No. of linked tasks"
thead_published_by: "Published by"
thead_added_by: "Added by"
thead_archived_by: "Archived by"
thead_published_on: "Published at"
thead_created_at: "Created at"
thead_archived_on: "Archived at"
thead_updated_at: "Last modified at"
thead:
name: "Name"
id: "ID"
keywords: "Keywords"
nr_of_linked_children: "No. of linked tasks"
versions: "Versions"
access: "Access"
published_by: "Published by"
added_by: "Added by"
archived_by: "Archived by"
published_on: "Published on"
created_at: "Created at"
archived_on: "Archived at"
updated_at: "Modified on"
table:
draft: "Draft"
preview:
title: "%{protocol} preview"
linked_children:

View file

@ -286,6 +286,7 @@ Rails.application.routes.draw do
resources :my_modules, only: %i(show update edit)
end
end
resources :protocols, only: %i(show update edit)
end
resources :projects, except: [:destroy] do
@ -551,6 +552,7 @@ Rails.application.routes.draw do
get 'linked_children', to: 'protocols#linked_children'
post 'linked_children_datatable',
to: 'protocols#linked_children_datatable'
get 'versions_modal', to: 'protocols#versions_modal'
get 'preview', to: 'protocols#preview'
patch 'description', to: 'protocols#update_description'
patch 'name', to: 'protocols#update_name'