2017-06-23 21:19:08 +08:00
|
|
|
class Project < ApplicationRecord
|
2019-02-26 18:01:15 +08:00
|
|
|
include ArchivableModel
|
|
|
|
include SearchableModel
|
|
|
|
include SearchableByNameModel
|
2021-01-28 19:57:04 +08:00
|
|
|
include ViewableModel
|
2021-02-09 17:45:12 +08:00
|
|
|
include PermissionCheckableModel
|
2021-10-13 20:12:24 +08:00
|
|
|
include PermissionExtends
|
2021-10-01 18:20:24 +08:00
|
|
|
include Assignable
|
2016-02-12 23:52:43 +08:00
|
|
|
|
|
|
|
enum visibility: { hidden: 0, visible: 1 }
|
|
|
|
|
2016-09-21 21:35:23 +08:00
|
|
|
auto_strip_attributes :name, nullify: false
|
2016-02-12 23:52:43 +08:00
|
|
|
validates :name,
|
2016-10-05 23:45:20 +08:00
|
|
|
length: { minimum: Constants::NAME_MIN_LENGTH,
|
|
|
|
maximum: Constants::NAME_MAX_LENGTH },
|
2019-05-06 22:14:50 +08:00
|
|
|
uniqueness: { scope: :team_id, case_sensitive: false }
|
2016-02-12 23:52:43 +08:00
|
|
|
validates :visibility, presence: true
|
2017-01-24 23:34:21 +08:00
|
|
|
validates :team, presence: true
|
2020-12-02 20:13:58 +08:00
|
|
|
validate :project_folder_team, if: -> { project_folder.present? }
|
2021-06-27 19:22:19 +08:00
|
|
|
validate :selected_user_role_validation, if: :bulk_assignment?
|
|
|
|
|
2021-01-12 20:04:36 +08:00
|
|
|
before_validation :remove_project_folder, on: :update, if: :archived_changed?
|
2016-02-12 23:52:43 +08:00
|
|
|
|
2017-06-28 21:21:32 +08:00
|
|
|
belongs_to :created_by,
|
|
|
|
foreign_key: 'created_by_id',
|
|
|
|
class_name: 'User',
|
|
|
|
optional: true
|
|
|
|
belongs_to :last_modified_by,
|
|
|
|
foreign_key: 'last_modified_by_id',
|
|
|
|
class_name: 'User',
|
|
|
|
optional: true
|
|
|
|
belongs_to :archived_by,
|
|
|
|
foreign_key: 'archived_by_id',
|
|
|
|
class_name: 'User',
|
|
|
|
optional: true
|
|
|
|
belongs_to :restored_by,
|
|
|
|
foreign_key: 'restored_by_id',
|
|
|
|
class_name: 'User',
|
|
|
|
optional: true
|
2021-06-27 19:22:19 +08:00
|
|
|
belongs_to :group_user_role,
|
|
|
|
foreign_key: 'group_user_role_id',
|
|
|
|
class_name: 'UserRole',
|
|
|
|
optional: true
|
2019-07-26 18:40:36 +08:00
|
|
|
belongs_to :team, inverse_of: :projects, touch: true
|
2021-01-12 17:41:42 +08:00
|
|
|
belongs_to :project_folder, inverse_of: :projects, optional: true, touch: true
|
2016-02-12 23:52:43 +08:00
|
|
|
has_many :user_projects, inverse_of: :project
|
2021-02-04 21:08:20 +08:00
|
|
|
has_many :users, through: :user_assignments
|
2016-07-22 16:43:55 +08:00
|
|
|
has_many :experiments, inverse_of: :project
|
2018-06-01 19:49:02 +08:00
|
|
|
has_many :active_experiments, -> { where(archived: false) },
|
|
|
|
class_name: 'Experiment'
|
2017-03-08 20:18:20 +08:00
|
|
|
has_many :project_comments, foreign_key: :associated_id, dependent: :destroy
|
2016-02-12 23:52:43 +08:00
|
|
|
has_many :activities, inverse_of: :project
|
|
|
|
has_many :tags, inverse_of: :project
|
|
|
|
has_many :reports, inverse_of: :project, dependent: :destroy
|
|
|
|
has_many :report_elements, inverse_of: :project, dependent: :destroy
|
|
|
|
|
2021-02-09 17:45:12 +08:00
|
|
|
default_scope { includes(user_assignments: :user_role) }
|
|
|
|
|
2021-04-18 23:19:16 +08:00
|
|
|
accepts_nested_attributes_for :user_assignments,
|
|
|
|
allow_destroy: true,
|
|
|
|
reject_if: :all_blank
|
|
|
|
|
2018-10-26 22:46:01 +08:00
|
|
|
scope :visible_to, (lambda do |user, team|
|
|
|
|
unless user.is_admin_of_team?(team)
|
2021-06-06 16:29:01 +08:00
|
|
|
left_outer_joins(:user_assignments)
|
|
|
|
.where('visibility = 1 OR user_assignments.user_id = :id', id: user.id)
|
2020-12-30 03:52:35 +08:00
|
|
|
.group(:id)
|
2018-10-26 22:46:01 +08:00
|
|
|
end
|
|
|
|
end)
|
|
|
|
|
2019-01-25 17:50:08 +08:00
|
|
|
scope :templates, -> { where(template: true) }
|
|
|
|
|
2021-06-27 19:22:19 +08:00
|
|
|
after_create :auto_assign_project_members, if: :visible?
|
|
|
|
before_update :sync_project_assignments, if: :visibility_changed?
|
2021-04-18 18:23:29 +08:00
|
|
|
|
2017-01-11 21:56:47 +08:00
|
|
|
def self.search(
|
|
|
|
user,
|
|
|
|
include_archived,
|
|
|
|
query = nil,
|
|
|
|
page = 1,
|
2017-05-05 22:41:23 +08:00
|
|
|
current_team = nil,
|
|
|
|
options = {}
|
2017-01-11 21:56:47 +08:00
|
|
|
)
|
2016-07-21 19:11:15 +08:00
|
|
|
|
2021-10-22 17:43:20 +08:00
|
|
|
new_query = Project.viewable_by_user(user, current_team || user.teams)
|
|
|
|
.where_attributes_like('projects.name', query, options)
|
|
|
|
new_query = new_query.active unless include_archived
|
2016-02-12 23:52:43 +08:00
|
|
|
|
|
|
|
# Show all results if needed
|
2016-10-05 23:45:20 +08:00
|
|
|
if page == Constants::SEARCH_NO_LIMIT
|
2016-02-12 23:52:43 +08:00
|
|
|
new_query
|
|
|
|
else
|
2021-10-22 17:43:20 +08:00
|
|
|
new_query.limit(Constants::SEARCH_LIMIT).offset((page - 1) * Constants::SEARCH_LIMIT)
|
2016-02-12 23:52:43 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2019-02-26 18:01:15 +08:00
|
|
|
def self.viewable_by_user(user, teams)
|
|
|
|
# Admins see all projects in the team
|
|
|
|
# Member of the projects can view
|
|
|
|
# If project is visible everyone from the team can view it
|
2021-10-22 17:43:20 +08:00
|
|
|
projects = Project.where(team: teams)
|
|
|
|
.left_outer_joins(team: :user_teams)
|
|
|
|
.left_outer_joins(user_assignments: :user_role)
|
|
|
|
projects.where('projects.visibility = 1 OR (user_teams.user_id = ? AND user_teams.role = 2)', user)
|
|
|
|
.or(projects.with_granted_permissions(user, ProjectPermissions::READ))
|
|
|
|
.distinct
|
2019-02-26 18:01:15 +08:00
|
|
|
end
|
|
|
|
|
2021-02-09 17:45:12 +08:00
|
|
|
def permission_parent
|
|
|
|
nil
|
2019-02-26 18:01:15 +08:00
|
|
|
end
|
|
|
|
|
2021-01-28 19:57:04 +08:00
|
|
|
def default_view_state
|
|
|
|
{
|
|
|
|
experiments: {
|
|
|
|
active: { sort: 'new' },
|
2021-02-11 18:50:59 +08:00
|
|
|
archived: { sort: 'new' },
|
|
|
|
view_type: 'cards'
|
2021-01-28 19:57:04 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
|
|
|
def validate_view_state(view_state)
|
2021-02-11 18:50:59 +08:00
|
|
|
if %w(cards table).exclude?(view_state.state.dig('experiments', 'view_type')) ||
|
|
|
|
%w(new old atoz ztoa).exclude?(view_state.state.dig('experiments', 'active', 'sort')) ||
|
2021-01-28 19:57:04 +08:00
|
|
|
%w(new old atoz ztoa archived_new archived_old).exclude?(view_state.state.dig('experiments', 'archived', 'sort'))
|
|
|
|
view_state.errors.add(:state, :wrong_state)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2016-10-13 16:00:36 +08:00
|
|
|
def last_activities(count = Constants::ACTIVITY_AND_NOTIF_SEARCH_LIMIT)
|
2016-07-21 19:11:15 +08:00
|
|
|
activities.order(created_at: :desc).first(count)
|
2016-02-12 23:52:43 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
# Get project comments order by created_at time. Results are paginated
|
|
|
|
# using last comment id and per_page parameters.
|
2016-10-05 23:45:20 +08:00
|
|
|
def last_comments(last_id = 1, per_page = Constants::COMMENTS_SEARCH_LIMIT)
|
2016-10-13 17:05:11 +08:00
|
|
|
last_id = Constants::INFINITY if last_id <= 1
|
2017-03-08 20:18:20 +08:00
|
|
|
comments = ProjectComment.joins(:project)
|
|
|
|
.where(projects: { id: id })
|
|
|
|
.where('comments.id < ?', last_id)
|
|
|
|
.order(created_at: :desc)
|
|
|
|
.limit(per_page)
|
2020-11-30 22:55:20 +08:00
|
|
|
ProjectComment.from(comments, :comments).order(created_at: :asc)
|
2016-02-12 23:52:43 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def unassigned_users
|
2021-10-13 15:50:06 +08:00
|
|
|
User.joins(:user_teams)
|
|
|
|
.joins(
|
|
|
|
"LEFT OUTER JOIN user_assignments ON user_assignments.user_id = users.id "\
|
|
|
|
"AND user_assignments.assignable_id = #{id} "\
|
|
|
|
"AND user_assignments.assignable_type = 'Project'"
|
|
|
|
)
|
|
|
|
.where(user_teams: { team_id: team_id })
|
|
|
|
.where(user_assignments: { id: nil })
|
|
|
|
.where.not(confirmed_at: nil)
|
|
|
|
.distinct
|
2016-02-12 23:52:43 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def user_role(user)
|
2021-10-13 15:50:06 +08:00
|
|
|
user_assignments.includes(:user_role).references(:user_role).find_by(user: user)&.user_role&.name
|
2016-02-12 23:52:43 +08:00
|
|
|
end
|
|
|
|
|
2021-09-30 17:32:11 +08:00
|
|
|
def sorted_experiments(user, sort_by = :new, archived = false)
|
2016-12-23 22:40:01 +08:00
|
|
|
sort = case sort_by
|
|
|
|
when 'old' then { created_at: :asc }
|
|
|
|
when 'atoz' then { name: :asc }
|
|
|
|
when 'ztoa' then { name: :desc }
|
2020-12-31 23:00:26 +08:00
|
|
|
when 'archived_new' then { archived_on: :desc }
|
|
|
|
when 'archived_old' then { archived_on: :asc }
|
2016-12-23 22:40:01 +08:00
|
|
|
else { created_at: :desc }
|
|
|
|
end
|
2021-09-30 17:32:11 +08:00
|
|
|
experiments.readable_by_user(user).is_archived(archived).order(sort)
|
2020-12-17 20:07:30 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def archived_experiments
|
|
|
|
experiments.is_archived(true)
|
2016-08-08 20:19:35 +08:00
|
|
|
end
|
|
|
|
|
2016-07-25 17:25:10 +08:00
|
|
|
def project_my_modules
|
2016-07-26 14:44:09 +08:00
|
|
|
MyModule.where('"experiment_id" IN (?)', experiments.select(:id))
|
2016-02-12 23:52:43 +08:00
|
|
|
end
|
|
|
|
|
|
|
|
def space_taken
|
|
|
|
st = 0
|
2016-07-22 20:31:09 +08:00
|
|
|
project_my_modules.find_each do |my_module|
|
2016-02-12 23:52:43 +08:00
|
|
|
st += my_module.space_taken
|
|
|
|
end
|
|
|
|
st
|
|
|
|
end
|
|
|
|
|
2020-06-18 05:21:56 +08:00
|
|
|
def assigned_repositories_and_snapshots
|
2020-06-29 17:43:23 +08:00
|
|
|
live_repositories = Repository.assigned_to_project(self)
|
2021-06-08 20:21:34 +08:00
|
|
|
snapshots = RepositorySnapshot.assigned_to_project(self)
|
2020-06-18 05:21:56 +08:00
|
|
|
(live_repositories + snapshots).sort_by { |r| r.name.downcase }
|
|
|
|
end
|
|
|
|
|
2016-08-03 18:57:51 +08:00
|
|
|
def my_modules_ids
|
|
|
|
ids = active_experiments.map do |exp|
|
|
|
|
exp.my_modules.pluck(:id) if exp.my_modules
|
|
|
|
end
|
2021-07-23 17:56:28 +08:00
|
|
|
ids.delete_if { |i| i.flatten.blank? }
|
2016-08-03 18:57:51 +08:00
|
|
|
ids.join(', ')
|
|
|
|
end
|
2016-08-17 19:17:40 +08:00
|
|
|
|
|
|
|
def assigned_modules(user)
|
|
|
|
role = user_role(user)
|
|
|
|
if role.blank?
|
|
|
|
MyModule.none
|
|
|
|
elsif role == 'owner'
|
2016-08-18 16:55:29 +08:00
|
|
|
project_my_modules
|
|
|
|
.joins(:experiment)
|
|
|
|
.where('experiments.archived=false')
|
2016-08-18 17:53:57 +08:00
|
|
|
.where('my_modules.archived=false')
|
|
|
|
|
2016-08-17 19:17:40 +08:00
|
|
|
else
|
|
|
|
project_my_modules
|
|
|
|
.joins(:user_my_modules)
|
2016-08-18 16:55:29 +08:00
|
|
|
.joins(:experiment)
|
|
|
|
.where('experiments.archived=false AND user_my_modules.user_id IN (?)',
|
|
|
|
user.id)
|
2016-08-18 17:53:57 +08:00
|
|
|
.where('my_modules.archived=false')
|
2016-08-17 19:17:40 +08:00
|
|
|
.distinct
|
|
|
|
end
|
|
|
|
end
|
2017-05-10 23:28:37 +08:00
|
|
|
|
|
|
|
def notifications_count(user)
|
|
|
|
res = 0
|
|
|
|
assigned_modules(user).find_each do |t|
|
2019-04-12 15:17:34 +08:00
|
|
|
res += 1 if (t.is_overdue? || t.is_one_day_prior?) && !t.completed?
|
2017-05-10 23:28:37 +08:00
|
|
|
end
|
|
|
|
res
|
|
|
|
end
|
2018-10-15 04:52:54 +08:00
|
|
|
|
2020-11-20 19:29:40 +08:00
|
|
|
def comments
|
|
|
|
project_comments
|
|
|
|
end
|
|
|
|
|
2018-12-12 21:03:16 +08:00
|
|
|
def generate_teams_export_report_html(
|
|
|
|
user, team, html_title, obj_filenames = nil
|
|
|
|
)
|
2018-10-15 04:52:54 +08:00
|
|
|
ActionController::Renderer::RACK_KEY_TRANSLATION['warden'] ||= 'warden'
|
|
|
|
proxy = Warden::Proxy.new({}, Warden::Manager.new({}))
|
2018-10-30 21:04:01 +08:00
|
|
|
proxy.set_user(user, scope: :user, store: false)
|
2021-06-08 20:21:34 +08:00
|
|
|
ApplicationController.renderer.defaults[:http_host] = Rails.application.routes.default_url_options[:host]
|
2018-10-15 04:52:54 +08:00
|
|
|
renderer = ApplicationController.renderer.new(warden: proxy)
|
|
|
|
|
2018-10-15 11:54:19 +08:00
|
|
|
report = Report.generate_whole_project_report(self, user, team)
|
2018-10-15 04:52:54 +08:00
|
|
|
|
2018-10-15 11:54:19 +08:00
|
|
|
page_html_string =
|
2021-06-08 20:21:34 +08:00
|
|
|
renderer.render 'reports/export.html.erb',
|
|
|
|
locals: { report: report, export_all: true },
|
|
|
|
assigns: { settings: report.settings, obj_filenames: obj_filenames }
|
2018-10-15 04:52:54 +08:00
|
|
|
parsed_page_html = Nokogiri::HTML(page_html_string)
|
2018-12-12 18:15:16 +08:00
|
|
|
parsed_html = parsed_page_html.at_css('#report-content')
|
2018-10-15 04:52:54 +08:00
|
|
|
|
2019-01-31 14:39:17 +08:00
|
|
|
# Style tables (mimick frontend processing)
|
|
|
|
|
2018-12-12 18:15:16 +08:00
|
|
|
tables = parsed_html.css('.hot-table-contents')
|
2018-12-12 21:03:16 +08:00
|
|
|
.zip(parsed_html.css('.hot-table-container'))
|
2018-09-24 13:21:32 +08:00
|
|
|
tables.each do |table_input, table_container|
|
|
|
|
table_vals = JSON.parse(table_input['value'])
|
|
|
|
table_data = table_vals['data']
|
|
|
|
table_headers = table_vals['headers']
|
|
|
|
table_headers ||= ('A'..'Z').first(table_data[0].count)
|
|
|
|
|
2018-09-26 00:41:01 +08:00
|
|
|
table_el = table_container
|
|
|
|
.add_child('<table class="handsontable"></table>').first
|
|
|
|
|
|
|
|
# Add header row
|
2018-09-26 01:35:39 +08:00
|
|
|
header_cell = '<th>'\
|
|
|
|
'<div class="relative">'\
|
|
|
|
'<span>%s</span>'\
|
|
|
|
'</div>'\
|
|
|
|
'</th>'
|
2018-09-26 00:41:01 +08:00
|
|
|
header_el = table_el.add_child('<thead></thead>').first
|
|
|
|
row_el = header_el.add_child('<tr></tr>').first
|
2018-09-26 01:35:39 +08:00
|
|
|
row_el.add_child(format(header_cell, '')).first
|
2018-09-24 13:21:32 +08:00
|
|
|
table_headers.each do |col|
|
2018-09-26 01:35:39 +08:00
|
|
|
row_el.add_child(format(header_cell, col)).first
|
2018-09-24 13:21:32 +08:00
|
|
|
end
|
|
|
|
|
2018-09-26 00:41:01 +08:00
|
|
|
# Add body rows
|
2018-09-26 01:35:39 +08:00
|
|
|
body_cell = '<td>%s</td>'
|
2018-09-26 00:41:01 +08:00
|
|
|
body_el = table_el.add_child('<tbody></tbody>').first
|
2018-09-26 01:35:39 +08:00
|
|
|
table_data.each.with_index(1) do |row, idx|
|
2018-09-26 00:41:01 +08:00
|
|
|
row_el = body_el.add_child('<tr></tr>').first
|
2018-09-26 01:35:39 +08:00
|
|
|
row_el.add_child(format(header_cell, idx)).first
|
2018-09-24 13:21:32 +08:00
|
|
|
row.each do |col|
|
2018-09-26 01:35:39 +08:00
|
|
|
row_el.add_child(format(body_cell, col)).first
|
2018-09-24 13:21:32 +08:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2018-10-19 22:25:43 +08:00
|
|
|
ApplicationController.render(
|
2018-12-12 18:15:16 +08:00
|
|
|
layout: false,
|
|
|
|
locals: {
|
2018-12-12 21:03:16 +08:00
|
|
|
title: html_title,
|
2018-12-12 18:15:16 +08:00
|
|
|
content: parsed_html.children.map(&:to_s).join
|
|
|
|
},
|
|
|
|
template: 'team_zip_exports/report',
|
2018-10-15 04:52:54 +08:00
|
|
|
current_user: user,
|
2018-12-12 18:15:16 +08:00
|
|
|
current_team: team
|
2018-10-15 04:52:54 +08:00
|
|
|
)
|
2018-11-06 16:33:18 +08:00
|
|
|
ensure
|
|
|
|
report.destroy if report.present?
|
2018-10-15 04:52:54 +08:00
|
|
|
end
|
2020-12-02 20:13:58 +08:00
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def project_folder_team
|
|
|
|
return if project_folder.team_id == team_id
|
|
|
|
|
|
|
|
errors.add(:project_folder, I18n.t('activerecord.errors.models.project.attributes.project_folder.team'))
|
|
|
|
end
|
2021-01-12 20:04:36 +08:00
|
|
|
|
|
|
|
def remove_project_folder
|
2021-03-22 22:20:47 +08:00
|
|
|
self.project_folder = nil
|
2021-01-12 20:04:36 +08:00
|
|
|
end
|
2021-04-18 18:23:29 +08:00
|
|
|
|
2021-06-27 19:22:19 +08:00
|
|
|
def auto_assign_project_members
|
|
|
|
UserAssignments::GroupAssignmentJob.perform_now(
|
|
|
|
team,
|
|
|
|
self,
|
|
|
|
created_by
|
|
|
|
)
|
|
|
|
end
|
|
|
|
|
|
|
|
def bulk_assignment?
|
|
|
|
visible? && group_user_role.present?
|
|
|
|
end
|
|
|
|
|
|
|
|
def selected_user_role_validation
|
|
|
|
errors.add(:group_user_role_id, :inclusion) unless group_user_role.in?(UserRole.all)
|
|
|
|
end
|
|
|
|
|
|
|
|
def sync_project_assignments
|
|
|
|
if visible?
|
|
|
|
auto_assign_project_members
|
|
|
|
else
|
|
|
|
UserAssignments::GroupUnAssignmentJob.perform_now(self)
|
|
|
|
end
|
|
|
|
end
|
2016-02-12 23:52:43 +08:00
|
|
|
end
|