-
# frozen_string_literal: true
-
-
2
class Admin::BaseController < ApplicationController
-
2
before_action :admin_authorize
-
-
2
layout 'admin'
-
-
2
private
-
-
2
def admin_authorize
-
208
authorize! current_user, with: AdminPolicy
-
end
-
end
-
# frozen_string_literal: true
-
-
2
class Admin::CategoriesController < Admin::BaseController
-
2
before_action :set_category, only: %i[update destroy]
-
-
2
def index
-
7
@categories = Category.select(:id, :name).order(:name)
-
end
-
-
2
def create
-
3
category = Category.new(category_params)
-
-
3
if category.save
-
2
render json: { category: { id: category.id, name: category.name } }
-
else
-
1
render json: { message: category.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
2
def update
-
4
return head :ok if @category.update(category_params)
-
-
1
render json: { message: @category.errors.full_messages }, status: :unprocessable_entity
-
end
-
-
2
def destroy
-
2
@category.destroy ? head(:ok) : head(:bad_request)
-
end
-
-
2
private
-
-
2
def category_params
-
7
params.require(:category).permit(:name)
-
end
-
-
2
def set_category
-
6
@category = Category.find(params[:id])
-
end
-
end
-
# frozen_string_literal: true
-
-
2
class Admin::ChartsController < Admin::BaseController
-
2
include Admin::Concerns::ChartsFilterDate
-
-
2
before_action :load_plan_subscriptions, only: %i[subscription_ratio subscription_by_date]
-
-
2
before_action :load_landing_page_feedback, only: :landing_page_feedback
-
-
2
before_action :load_ahoy_event_social, only: %i[social_share_ratio social_share_by_date]
-
-
2
before_action :load_ahoy_visit_referrer, only: %i[referrers_ratio referrers_by_date]
-
-
2
before_action :load_ahoy_event_time_spent, only: :average_time_spent_per_page
-
-
2
before_action :load_ahoy_visit_per_page, only: :number_of_visits_per_page
-
-
2
before_action :load_newsletter_subscription_by_date, only: :newsletter_subscription_by_date
-
-
2
before_action :load_unsubscription_by_newsletter, only: :unsubscription_by_newsletter
-
-
2
before_action :load_unsubscription_reason, only: :unsubscription_reason
-
-
# These charts are filtered by 'time' column
-
2
before_action :filter_date_by_time_column,
-
only: %i[
-
social_share_ratio
-
social_share_by_date
-
average_time_spent_per_page
-
number_of_visits_per_page
-
]
-
-
# These charts are filtered by 'created_at' column
-
2
before_action :filter_date_by_created_at,
-
only: %i[
-
landing_page_feedback
-
newsletter_subscription_by_date
-
unsubscription_by_newsletter
-
unsubscription_reason
-
]
-
-
2
before_action :filter_date_by_updated_at, only: %i[subscription_ratio subscription_by_date]
-
-
# These charts are filtered by 'started_at' column
-
2
before_action :filter_date_by_started_at, only: %i[referrers_ratio referrers_by_date]
-
-
2
def subscription_ratio
-
6
render json: @records.size
-
end
-
-
2
def subscription_by_date
-
3
render json: @records.group_by_day(:updated_at).size.chart_json
-
end
-
-
2
def landing_page_feedback
-
4
render json: { data: [
-
@records[0].group(:smiley).size,
-
@records[1].group(:channel).size,
-
@records[2].group(:interest).size
-
] }
-
end
-
-
2
def social_share_ratio
-
2
render json: @records.type_size
-
end
-
-
2
def social_share_by_date
-
2
render json: @records.type_time_size.chart_json
-
end
-
-
2
def referrers_ratio
-
12
render json: @records.size
-
end
-
-
2
def referrers_by_date
-
4
render json: @records.group_by_day(:started_at).size.chart_json
-
end
-
-
2
def average_time_spent_per_page
-
4
render json: @records
-
.group("properties ->> 'pathname'")
-
.average("cast(properties ->> 'time_spent' as integer)")
-
end
-
-
2
def number_of_visits_per_page
-
4
render json: @records.group("properties ->> 'action'").size
-
end
-
-
2
def newsletter_subscription_by_date
-
8
render json: [
-
{ name: 'Subscription', data: @records[0].group_by_day(:created_at).size },
-
{ name: 'Unsubscription', data: @records[1].group_by_day(:created_at).size }
-
]
-
end
-
-
2
def unsubscription_by_newsletter
-
5
@records.map! do |record|
-
3
["#{record['title']}, #{record['created_at'].utc.strftime('%d-%m-%Y')}", record['feedback_count']]
-
end
-
-
5
render json: @records
-
end
-
-
2
def unsubscription_reason
-
4
render json: NewsletterFeedback.count_reason(@records)
-
end
-
-
2
private
-
-
2
def chart_params
-
125
params.require(:chart).permit(:date)
-
end
-
-
2
def load_plan_subscriptions
-
13
@records = License.group(:plan)
-
end
-
-
2
def load_ahoy_event_social
-
8
@records = Ahoy::Event.social
-
end
-
-
2
def load_landing_page_feedback
-
@records = [
-
6
LandingFeedback.select(:smiley, :created_at),
-
LandingFeedback.select(:channel, :created_at),
-
LandingFeedback.select(:interest, :created_at)
-
]
-
end
-
-
2
def load_ahoy_visit_referrer
-
20
@records = Ahoy::Visit.group(:referrer)
-
end
-
-
2
def load_ahoy_event_time_spent
-
6
@records = Ahoy::Event.where(name: 'Time Spent')
-
end
-
-
2
def load_ahoy_visit_per_page
-
6
@records = Ahoy::Event.action
-
end
-
-
2
def load_newsletter_subscription_by_date
-
@records = [
-
10
NewsletterSubscription.select(:created_at),
-
NewsletterFeedback.select(:created_at)
-
]
-
end
-
-
2
def load_unsubscription_by_newsletter
-
7
@records = Newsletter.unsubscription_by_newsletter
-
end
-
-
2
def load_unsubscription_reason
-
6
@records = NewsletterFeedback.select(:reasons)
-
end
-
end
-
# frozen_string_literal: true
-
-
2
module Admin::Concerns::ChartsFilterDate
-
2
extend ActiveSupport::Concern
-
-
2
def filter_date_by_time_column
-
20
filter_date('time')
-
end
-
-
2
def filter_date_by_created_at
-
29
filter_date('created_at')
-
end
-
-
2
def filter_date_by_updated_at
-
13
filter_date('updated_at')
-
end
-
-
2
def filter_date_by_started_at
-
20
filter_date('started_at')
-
end
-
-
2
private
-
-
2
def date_params
-
43
chart_params[:date].split(',')
-
end
-
-
2
def start_date
-
27
Time.zone.parse(date_params[0]).beginning_of_day
-
rescue StandardError
-
11
raise ActionController::BadRequest
-
end
-
-
2
def end_date
-
16
Time.zone.parse(date_params[1]).end_of_day
-
rescue StandardError
-
1
raise ActionController::BadRequest
-
end
-
-
2
def filter_date(column)
-
82
return if chart_params[:date].blank?
-
-
24
case action_name
-
when 'landing_page_feedback', 'newsletter_subscription_by_date'
-
# Array of ActiveRecord::Relation objects
-
11
@records.map! { |record| record.where(column + ' BETWEEN ? AND ?', start_date, end_date) }
-
when 'unsubscription_by_newsletter'
-
# Class of Newsletter, cannot use where
-
4
@records.filter! { |record| record[:created_at].between?(start_date, end_date) }
-
else
-
18
@records = @records.where(column + ' BETWEEN ? AND ?', start_date, end_date)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
2
class Admin::DashboardController < Admin::BaseController
-
# rubocop:disable Metrics/MethodLength
-
2
def index
-
2
@counts = Project.pluck(
-
Arel.sql('COUNT(1),'\
-
'(SELECT COUNT(1) FROM projects WHERE visibility = true),'\
-
'(SELECT COUNT(1) FROM projects WHERE visibility = false),'\
-
'(SELECT COUNT(1) FROM users),'\
-
"(SELECT COUNT(1) FROM licenses WHERE plan = 'free'),"\
-
"(SELECT COUNT(1) FROM licenses WHERE plan = 'pro'),"\
-
"(SELECT COUNT(1) FROM licenses WHERE plan = 'ultimate'),"\
-
'(SELECT COUNT(1) FROM categories),'\
-
'(SELECT COUNT(1) FROM skills)')
-
).first
-
end
-
# rubocop:enable Metrics/MethodLength
-
end
-
# frozen_string_literal: true
-
-
# Controller for metrics
-
2
class Admin::MetricsController < Admin::BaseController
-
2
before_action :admin_authorize
-
-
2
layout 'metrics_page'
-
-
2
def index
-
@data = {
-
6
subscribed_count: NewsletterSubscription.subscribed_count,
-
today_count: Ahoy::Visit.today_count,
-
total_count: Ahoy::Visit.count
-
}.freeze
-
end
-
-
2
def traffic
-
@data = {
-
12
device_type: Ahoy::Visit.group(:device_type).size.chart_json,
-
browser: Ahoy::Visit.group(:browser).size.chart_json,
-
country: Ahoy::Visit.group(:country).size.chart_json
-
}.freeze
-
end
-
end
-
# frozen_string_literal: true
-
-
# Controller for newsletter
-
2
class Admin::NewslettersController < Admin::BaseController
-
2
skip_before_action :authenticate_user!, only: %i[subscribe unsubscribe post_unsubscribe]
-
-
2
skip_before_action :admin_authorize, only: %i[subscribe unsubscribe post_unsubscribe]
-
-
2
layout 'metrics_page', except: :unsubscribe
-
-
2
def index
-
4
@newsletters = Newsletter.select(:id, :title, :created_at)
-
end
-
-
2
def create
-
5
newsletter = Newsletter.new(newsletter_params)
-
-
4
if newsletter.save
-
2
flash[:toast_success] = 'Newsletter sent'
-
2
render js: "window.location = '#{newsletter_path(newsletter)}'"
-
else
-
2
render json: { message: 'Send Failed' }, status: :unprocessable_entity
-
end
-
end
-
-
2
def show
-
8
@newsletter = Newsletter.find(params[:id])
-
-
6
return unless request.xhr?
-
-
2
render json: { html: view_to_html_string('admin/newsletters/_modal') }
-
end
-
-
2
def subscribers
-
10
@subscribers = NewsletterSubscription.where(subscribed: true)
-
end
-
-
2
def subscribe
-
7
if NewsletterSubscription.subscribe(params[:email])
-
4
render json: { message: 'Thanks for subscribing' }
-
else
-
3
render json: { message: 'Subscription Failed' }, status: :unprocessable_entity
-
end
-
end
-
-
2
def unsubscribe; end
-
-
2
def post_unsubscribe
-
8
feedback = NewsletterFeedback.create(unsubscribe_params)
-
-
8
if feedback.errors.any?
-
2
render json: { message: feedback.errors.full_messages }, status: :unprocessable_entity
-
else
-
# Prevent Oracle attack on newsletter subscribers list by returning as
-
# long as the params syntax are correct
-
6
render json: { message: 'Newsletter Unsubscribed! Hope to see you again' }
-
end
-
end
-
-
2
private
-
-
2
def newsletter_params
-
5
params.require(:newsletter).permit(:title, :content)
-
end
-
-
2
def unsubscribe_params
-
8
p = params.require(:newsletter_unsubscription).permit(:email, reasons: [])
-
-
8
{ newsletter_subscription: NewsletterSubscription.find_by(email: p[:email]), reasons: p[:reasons] }
-
end
-
end
-
# frozen_string_literal: true
-
-
2
class Admin::SkillsController < Admin::BaseController
-
2
before_action :set_skill, only: %i[update destroy]
-
-
2
def index
-
10
@skills = Skill.with_category
-
10
@categories = Category.select(:id, :name)
-
end
-
-
2
def create
-
4
skill = Skill.new(skill_params)
-
-
4
if skill.save
-
2
render json: { skill: { id: skill.id, name: skill.name, category_name: skill.category.name } }
-
else
-
2
render json: { message: skill.errors.full_messages }, status: :unprocessable_entity
-
end
-
end
-
-
2
def update
-
5
return head :ok if @skill.update(skill_params)
-
-
1
render json: { message: @skill.errors.full_messages }, status: :unprocessable_entity
-
end
-
-
2
def destroy
-
2
@skill.destroy ? head(:ok) : head(:bad_request)
-
end
-
-
2
private
-
-
2
def skill_params
-
9
params.require(:skill).permit(:name, :category_id)
-
end
-
-
2
def set_skill
-
7
@skill = Skill.find(params[:id])
-
end
-
end
-
# frozen_string_literal: true
-
-
# Default application controller
-
4
class ApplicationController < ActionController::Base
-
4
include Pagy::Backend
-
-
# Ahoy gem, used in PagesController only
-
4
skip_before_action :track_ahoy_visit
-
-
4
before_action :authenticate_user!
-
904
before_action :unread_notification_count, if: proc { !request.xhr? && user_signed_in? }
-
-
4
protect_from_forgery with: :exception, prepend: true
-
-
4
rescue_from ActionController::ParameterMissing do
-
20
head :bad_request
-
end
-
-
# 1. RecordNotFound exception is raised when using *find* method
-
# 2. Exception object contains the following information
-
# ex.policy #=> policy class, e.g. UserPolicy
-
# ex.rule #=> applied rule, e.g. :show?
-
# 3. Pagy VariableError or OverflowError
-
4
rescue_from ActiveRecord::RecordNotFound,
-
ActionPolicy::Unauthorized,
-
Pagy::VariableError do
-
92
render_404
-
end
-
-
4
rescue_from ActiveRecord::StatementInvalid do
-
1
render json: { message: 'Invalid Statement' }, status: :unprocessable_entity
-
end
-
-
4
def view_to_html_string(partial, locals = {})
-
55
render_to_string(partial, locals: locals, layout: false, formats: [:html])
-
end
-
-
4
private
-
-
4
def unread_notification_count
-
594
@unread_count = current_user.notifications.unread.size
-
end
-
-
4
def render_404(msg = 'Not found')
-
97
respond_to do |format|
-
193
format.html { render 'errors/error_404', layout: 'errors', status: 404 }
-
98
format.json { render json: { message: msg }, status: :not_found }
-
97
format.any { head :not_found }
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
2
class CategoriesController < ApplicationController
-
2
skip_before_action :authenticate_user!
-
-
2
def index
-
5
return render_404 unless request.xhr?
-
-
4
render json: { categories: Category.order(:name).pluck(:name) }
-
end
-
end
-
# frozen_string_literal: true
-
-
# Error controller
-
2
class ErrorsController < ApplicationController
-
2
skip_before_action :authenticate_user!
-
-
2
layout 'errors'
-
-
2
def error_403
-
3
render status: 403
-
end
-
-
2
def error_404
-
3
render status: 404
-
end
-
-
2
def error_422
-
3
render status: 422
-
end
-
-
2
def error_500
-
3
render status: 500
-
end
-
end
-
# frozen_string_literal: true
-
-
# Controller for notifications
-
2
class NotificationsController < ApplicationController
-
2
before_action :set_notification, only: %i[destroy read unread]
-
-
2
def index
-
3
return render_404 unless request.xhr?
-
-
2
_, notifications = pagy(current_user.notifications.load_index, items: 5)
-
-
2
render json: NotificationBlueprint.render(notifications)
-
end
-
-
2
def destroy
-
1
@notification.delete ? head(:ok) : head(:bad_request)
-
end
-
-
2
def read
-
1
@notification.update_columns(read: true) # rubocop:disable Rails/SkipsModelValidations
-
1
head :ok
-
end
-
-
2
def unread
-
1
@notification.update_columns(read: false) # rubocop:disable Rails/SkipsModelValidations
-
1
head :ok
-
end
-
-
2
def read_all
-
1
current_user.notifications.unread.update_all(read: true) # rubocop:disable Rails/SkipsModelValidations
-
1
head :ok
-
end
-
-
2
private
-
-
2
def set_notification
-
5
@notification = Notification.find(params[:id])
-
5
authorize! @notification, with: NotificationPolicy
-
end
-
end
-
# frozen_string_literal: true
-
-
# Controller for landing pages
-
2
class PagesController < ApplicationController
-
2
skip_before_action :authenticate_user!
-
-
2
before_action :track_ahoy_visit
-
-
25
after_action :track_action, except: :track_time, unless: -> { request.xhr? }
-
-
2
layout 'landing_page'
-
-
2
def track_social
-
9
return head :bad_request unless valid_social_type?
-
-
8
ahoy.track 'Click Social', type: params[:type]
-
8
head :ok
-
end
-
-
# Function to track time spent in a page
-
2
def track_time
-
20
return head :bad_request unless valid_request?
-
-
12
ahoy.track 'Time Spent',
-
time_spent: params[:time].to_f.round / 1000,
-
pathname: params[:pathname]
-
12
head :ok
-
end
-
-
2
def submit_feedback
-
5
if LandingFeedback.new(feedback_params).save
-
3
render json: { message: 'Thank you for your feedback' }
-
else
-
1
render json: { message: 'Submission Failed' }, status: :unprocessable_entity
-
end
-
end
-
-
2
private
-
-
# Ahoy Gem function to track actions
-
2
def track_action
-
10
ahoy.track('Ran action', request.path_parameters)
-
end
-
-
2
def valid_request?
-
20
params.require(%i[time pathname]) && valid_pathname?(params[:pathname])
-
end
-
-
2
def valid_pathname?(pathname)
-
17
pathname.in?(%w[/ /pricing /about /love /features /feedback /newsletter])
-
end
-
-
2
def valid_social_type?
-
9
params[:type].in?(%w[Facebook Twitter Email])
-
end
-
-
2
def feedback_params
-
5
params.require(:landing_feedback).permit(%i[smiley channel interest])
-
end
-
end
-
# frozen_string_literal: true
-
-
2
class Projects::Appeals::ApplicationsController < Projects::Appeals::BaseController
-
2
before_action :set_application, only: %i[accept destroy]
-
-
2
def index
-
3
project = Project.find(params[:project_id])
-
3
authorize! project, with: Appeal::ApplicationPolicy
-
-
2
render json: { applications: Appeal.application.list_in_project(project) }
-
end
-
-
2
def create
-
4
@application = Appeal.new(user: current_user, project_id: params[:project_id], type: 'application')
-
-
4
authorize! @application, with: Appeal::ApplicationPolicy
-
3
return application_fail unless @application.save
-
-
3
flash[:toast_success] = 'Application sent'
-
3
render js: 'location.reload();'
-
end
-
-
2
def accept
-
2
@application.project.unassigned_team.users << @application.user
-
-
2
msg = @application.project.errors.full_messages
-
2
return application_fail(msg) unless msg.blank? && @application.delete
-
-
1
@application.send_resolve_notification('accept')
-
1
head :ok
-
end
-
-
2
def destroy
-
1
return application_fail unless @application.delete
-
-
1
@application.send_resolve_notification('decline', current_user == @application.user)
-
-
# TODO: should return JSON only, otherwise the Table will redirect
-
1
flash[:toast_success] = 'Application deleted'
-
1
render js: 'location.reload();'
-
end
-
-
2
private
-
-
2
def application_fail(msg = @application.errors.full_messages)
-
1
render json: { message: msg }, status: :unprocessable_entity
-
end
-
-
2
def set_application
-
6
@application = Appeal.application.find(params[:id])
-
6
authorize! @application, with: Appeal::ApplicationPolicy
-
end
-
end
-
# frozen_string_literal: true
-
-
2
class Projects::Appeals::BaseController < ApplicationController
-
2
rescue_from ActionPolicy::Unauthorized do
-
11
head :forbidden
-
end
-
end
-
# frozen_string_literal: true
-
-
2
class Projects::Appeals::InvitationsController < Projects::Appeals::BaseController
-
2
before_action :set_invitation, only: %i[accept destroy]
-
-
2
def index
-
8
project = Project.find(params[:project_id])
-
8
authorize! project, with: Appeal::InvitationPolicy
-
-
7
render json: { invitations: Appeal.invitation.list_in_project(project) }
-
end
-
-
2
def create
-
5
@invitation = Appeal.new(user: User.find_by(email: params[:email]),
-
project_id: params[:project_id],
-
type: 'invitation')
-
-
5
authorize! @invitation, with: Appeal::InvitationPolicy
-
4
return invitation_fail unless @invitation.save
-
-
2
render json: { invitation: @invitation }
-
end
-
-
2
def accept
-
3
@invitation.project.unassigned_team.users << @invitation.user
-
-
3
msg = @invitation.project.errors.full_messages
-
3
return invitation_fail(msg) unless msg.blank? && @invitation.delete
-
-
2
@invitation.send_resolve_notification('accept')
-
2
render js: 'location.reload();'
-
end
-
-
2
def destroy
-
4
return invitation_fail unless @invitation.delete
-
-
4
@invitation.send_resolve_notification('decline', current_user == @invitation.user)
-
-
# TODO: should return JSON only, otherwise the Table will redirect
-
4
flash[:toast_success] = 'Invitation deleted'
-
4
render js: 'location.reload();'
-
end
-
-
2
private
-
-
2
def invitation_fail(msg = @invitation.errors.full_messages)
-
3
render json: { message: msg }, status: :unprocessable_entity
-
end
-
-
2
def set_invitation
-
11
@invitation = Appeal.invitation.find(params[:id])
-
11
authorize! @invitation, with: Appeal::InvitationPolicy
-
end
-
end
-
# frozen_string_literal: true
-
-
2
class Projects::TasksController < ApplicationController
-
2
before_action :set_task,
-
only: %i[edit update set_percentage assign_self destroy]
-
2
before_action :set_project,
-
except: %i[update destroy assign_self set_percentage]
-
2
before_action :set_skills, only: %i[new edit]
-
-
2
layout 'project'
-
-
# GET /tasks/new
-
2
def new
-
12
@task = Task.new(project: @project)
-
12
authorize! @project, to: :create_task?
-
end
-
-
# GET /tasks/1/edit
-
2
def edit; end
-
-
# POST /tasks
-
2
def create
-
12
authorize! @project, to: :create_task?
-
10
@task = @project.tasks.build(create_params)
-
-
9
if @task.save
-
8
@task.skill_ids = params.dig(:task, :skill_ids)
-
8
@task.user_ids = params.dig(:task, :user_ids)
-
8
task_success('Task created')
-
else
-
1
task_fail
-
end
-
end
-
-
# PATCH/PUT /tasks/1
-
2
def update
-
9
@task.update(edit_params) ? task_success('Task updated') : task_fail
-
end
-
-
2
def assign_self
-
3
return task_fail('Task is already Assigned') if @task.users.present?
-
-
2
@task.users << current_user
-
2
@task.send_picked_up_notification
-
2
head :ok
-
end
-
-
# DELETE /tasks/1
-
2
def destroy
-
5
@task.destroy
-
5
head :ok
-
end
-
-
2
def data
-
30
extend AvatarHelper
-
-
30
return render_404 unless request.xhr? && valid_data_type?
-
-
27
render json: {
-
data: TaskBlueprint.render_as_json(
-
authorized_scope(@project.tasks, as: @type).includes(users: [{ avatar_attachment: :blob }])
-
)
-
}
-
end
-
-
2
def set_percentage
-
8
@task.update(edit_params) ? head(:ok) : task_fail
-
end
-
-
2
private
-
-
2
def set_project
-
63
@project = Project.find(params[:project_id])
-
end
-
-
2
def set_skills
-
46
@skills = @project.category.skills.map { |s| [s.name, s.id] }
-
-
21
team = current_user.teams.find_by(project: @project)
-
21
@assignees = authorized_scope(
-
@project.users,
-
as: :assignee,
-
scope_options: { team_id: team&.id, project: @project }
-
)
-
end
-
-
2
def set_task
-
42
@task = Task.find(params[:id])
-
42
authorize! @task
-
end
-
-
2
def create_params
-
10
params.require(:task)
-
.except(:skill_ids, :user_ids)
-
.permit(:name, :description, :priority)
-
.merge(user: current_user)
-
end
-
-
2
def edit_params
-
17
params.require(:task).permit(:description, :name,
-
:percentage, :priority,
-
skill_ids: [], user_ids: [])
-
end
-
-
2
def task_fail(message = @task.errors.full_messages)
-
4
render json: { message: message }, status: :unprocessable_entity
-
end
-
-
2
def task_success(message)
-
15
flash[:toast_success] = message
-
15
render js: "window.location = '#{project_path(@task.project)}'"
-
end
-
-
2
def valid_data_type?
-
30
authorize! @project, to: :count?
-
-
29
@type = params[:type]&.to_sym
-
29
%i[assigned unassigned active completed].include?(@type)
-
end
-
end
-
# frozen_string_literal: true
-
-
2
class Projects::Teams::BaseController < ApplicationController
-
2
layout 'project'
-
-
2
private
-
-
2
def set_team
-
29
@team = Team.find(params[:id])
-
29
authorize! @team
-
end
-
-
2
def set_skills
-
14
@skills = Skill.where(category: @project.category)
-
11
.collect { |s| [s.name, s.id] }
-
end
-
-
2
def set_project
-
83
@project = Project.find(params[:project_id])
-
83
authorize! @project, to: :manage?
-
end
-
-
2
def team_success(message)
-
10
flash[:toast_success] = message
-
10
render js: "window.location = '#{project_manage_index_path(@project)}'"
-
end
-
-
2
def team_fail(message = @team.errors.full_messages)
-
4
render json: { message: message }, status: :unprocessable_entity
-
end
-
end
-
# frozen_string_literal: true
-
-
2
class Projects::Teams::ManageController < Projects::Teams::BaseController
-
2
before_action :set_project
-
-
2
def index; end
-
-
2
def manage_data
-
11
return render_404 unless request.xhr?
-
-
11
@data = User.teams_data(@project)
-
11
compute = CompatibilityCompute.new(@project.teams,
-
@project.unassigned_team)
-
-
11
render json: {
-
17
compatibility: @data.to_h { |u| [u.id, compute.call(u)] },
-
data: @data.group_by(&:team_id),
-
teams: @project.teams&.select(:id, :name, :team_size)
-
}
-
end
-
-
2
def suggest
-
2
return team_fail('Invalid Mode') unless %w[balance cohesion efficient].include?(params[:mode])
-
-
2
suggestion = Suggest.new(User.teams_data(@project),
-
@project.teams.where.not(name: 'Unassigned'),
-
@project.unassigned_team,
-
params[:mode]).call
-
2
render json: { data: suggestion }
-
end
-
-
# save_data
-
2
def create
-
2
data = Oj.load(params[:data])
-
2
teams = @project.teams
-
2
users = @project.users
-
2
data.each do |id, members|
-
14
new_team = teams.find { |x| x.id.to_s == id }
-
4
next if new_team.nil?
-
-
4
authorize! new_team, to: :manage?
-
-
4
saving_loop(new_team, members, teams, users)
-
end
-
2
render json: { data: data }
-
end
-
-
2
def recompute_data
-
2
return team_fail('Invalid Mode') unless %w[balance cohesion efficient].include?(params[:mode])
-
-
2
i = Oj.load(params[:data])
-
2
teams = @project.teams.find(i.keys)
-
2
d = recompute_loop(i, teams, params[:mode])
-
-
2
render json: { compatibility: d }
-
end
-
-
2
def remove_user
-
3
user = User.find(params[:user_id])
-
3
@team = user.teams.find_by(project: @project)
-
3
return team_fail('Cannot remove owner') if user == @project.user
-
-
2
@team.users.delete(user)
-
2
head :ok
-
end
-
-
2
private
-
-
2
def change_team(member, new_team, old_team)
-
2
authorize! new_team, to: :manage?
-
2
new_team.users << member
-
2
old_team.users.delete(member)
-
end
-
-
2
def recompute_loop(input, teams, mode)
-
2
data = {}
-
6
compute = Recompute.new(teams, teams.find { |x| x.name == 'Unassigned' }, mode)
-
-
2
input.each do |team_id, members|
-
4
next if members.blank?
-
-
4
data.merge!(
-
6
compute.call(teams.find { |x| x.id.to_s == team_id }, input).to_h
-
)
-
end
-
2
data
-
end
-
-
2
def saving_loop(new_team, members, teams, users)
-
4
members.each do |m|
-
4
next unless m['team_id'] != new_team.id
-
-
13
target = users.find { |x| x.id == m['id'] }
-
6
old_team = teams.find { |x| x.id == m['team_id'] }
-
2
change_team(target, new_team, old_team)
-
2
m['team_id'] = new_team.id
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
2
class Projects::Teams::TeamsController < Projects::Teams::BaseController
-
2
before_action :set_team, only: %i[edit show update destroy]
-
2
before_action :set_project, except: %i[show]
-
2
before_action :set_skills, only: %i[new edit]
-
2
skip_before_action :authenticate_user!, only: %i[show]
-
-
# GET /teams/new
-
2
def new; end
-
-
# GET /teams/1/edit
-
2
def edit; end
-
-
2
def show
-
7
extend AvatarHelper
-
-
7
return render_404 unless request.xhr?
-
-
7
render json: { member: @team.users, skill: @team.skills&.select(:id, :name),
-
team: @team, images: get_avatars(@team.users.pluck('id')) }
-
end
-
-
# POST /teams
-
2
def create
-
7
@team = @project.teams.build(create_params)
-
-
7
if @team.save
-
5
@team.skill_ids = params[:team][:skill_ids]
-
5
team_success('Team was successfully created')
-
else
-
2
team_fail
-
end
-
end
-
-
# PATCH/PUT /teams/1
-
2
def update
-
6
@team.update(edit_params) ? team_success('Team Saved') : team_fail
-
end
-
-
# DELETE /teams/1
-
2
def destroy
-
3
@project.unassigned_team.users << @team.users
-
3
@team.destroy
-
3
head :ok
-
end
-
-
2
private
-
-
2
def create_params
-
7
params.require(:team).except(:skill_ids)
-
.permit(:team_size, :project_id, :name)
-
end
-
-
2
def edit_params
-
6
params.require(:team).permit(:team_size, :name, skill_ids: [])
-
end
-
end
-
# frozen_string_literal: true
-
-
# Controller for projects
-
2
class ProjectsController < ApplicationController
-
2
skip_before_action :authenticate_user!, only: %i[explore show]
-
-
2
before_action :set_project, except: %i[index explore new create]
-
-
2
layout 'project'
-
-
# GET /projects
-
2
def index
-
22
render_projects(params[:joined].present? ? :joined : :own)
-
end
-
-
2
def explore
-
4
render_projects(:explore)
-
end
-
-
# GET /projects/1
-
2
def show; end
-
-
# GET /projects/new
-
2
def new; end
-
-
# POST /projects
-
2
def create
-
7
@project = current_user.projects.build(project_params)
-
7
@project.save ? project_success('Project Created') : project_fail
-
end
-
-
# PATCH/PUT /projects/1
-
2
def update
-
6
return project_fail unless @project.update(project_params)
-
-
5
project_success('Project Updated')
-
end
-
-
2
def change_status
-
7
msg = case params[:status]
-
when 'completed'
-
2
'Project Archived'
-
when 'active'
-
3
@project.appeals.application.destroy_all
-
3
@project.completed? ? 'Project Activated' : 'Project Closed'
-
when 'open'
-
2
'Project Opened'
-
end
-
-
7
@project.update(status: params[:status]) ? project_success(msg) : project_fail
-
end
-
-
2
def count
-
3
return head :bad_request unless %w[task application].include?(params[:type])
-
-
2
render json: {
-
count: case params[:type]
-
1
when 'task' then @project.tasks.where('percentage < 100').size
-
1
when 'application' then @project.appeals.size
-
end
-
}
-
end
-
-
2
private
-
-
2
def set_project
-
89
@project = Project.includes(:teams, :tasks)
-
.find(params[:id])
-
89
authorize! @project, with: ProjectPolicy
-
end
-
-
2
def project_params
-
13
params.require(:project).permit(%i[name description visibility category_id avatar])
-
end
-
-
2
def render_projects(policy_scope)
-
26
@pagy, projects = pagy(authorized_scope(Project.search(params), as: policy_scope))
-
25
@html = view_to_html_string('projects/_projects', projects: projects)
-
-
25
respond_to do |format|
-
25
format.html
-
31
format.json { render json: { html: @html, total: @pagy.count } }
-
end
-
end
-
-
2
def project_success(message)
-
17
@project.appeals.delete_all if @project.completed?
-
17
flash[:toast_success] = message
-
17
render js: "window.location = '#{project_path(@project)}'"
-
end
-
-
2
def project_fail(message = @project.errors.full_messages)
-
3
render json: { message: message }, status: :unprocessable_entity
-
end
-
end
-
# frozen_string_literal: true
-
-
2
class SkillsController < ApplicationController
-
2
skip_before_action :authenticate_user!
-
-
2
def index
-
5
return render_404 unless request.xhr?
-
-
4
render json: {
-
skills: Skill.with_category.where('categories.name ~* ?', params[:category]).to_json
-
}
-
end
-
end
-
# frozen_string_literal: true
-
-
# controller for OAuth Devise
-
2
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
-
2
def omniauth_flow
-
18
user_signed_in? ? connect_flow : sign_in_flow
-
-
18
return unless @user&.persisted?
-
-
12
user_signed_in? ? connect_success_action : sign_in_success_action
-
end
-
-
2
Devise.omniauth_providers.each do |provider|
-
6
alias_method provider, :omniauth_flow
-
end
-
-
# More info at:
-
# https://github.com/plataformatec/devise#omniauth
-
-
# GET|POST /resource/auth/twitter
-
# def passthru
-
# super
-
# end
-
-
# GET|POST /users/auth/twitter/callback
-
# def failure
-
# super
-
# end
-
-
# protected
-
-
# The path used when OmniAuth fails
-
# def after_omniauth_failure_path_for(scope)
-
# super(scope)
-
# end
-
-
2
private
-
-
2
def sign_in_flow
-
12
identity = User.sign_in_omniauth(request.env['omniauth.auth'])
-
-
12
errors = identity.errors.full_messages + identity.user.errors.full_messages
-
-
12
if errors.blank?
-
9
@user = identity.user
-
else
-
3
flash[:toast_error] = errors
-
3
redirect_to new_user_registration_url
-
end
-
end
-
-
2
def connect_flow
-
6
identity = current_user.connect_omniauth(request.env['omniauth.auth'])
-
-
6
if identity.errors.blank?
-
3
@user = identity.user
-
else
-
3
flash[:toast_error] = identity.errors.full_messages
-
3
redirect_to settings_account_path
-
end
-
end
-
-
2
def connect_success_action
-
3
flash[:toast_success] = 'Account Connected'
-
3
redirect_to settings_account_path
-
end
-
-
2
def sign_in_success_action
-
9
sign_in @user
-
9
redirect_to after_sign_in_path_for(@user)
-
end
-
end
-
# frozen_string_literal: true
-
-
# Controller for setting and changing password
-
2
class Users::PasswordsController < Devise::PasswordsController
-
# GET /resource/password/new
-
# def new
-
# super
-
# end
-
-
# POST /resource/password
-
2
def create
-
4
self.resource = resource_class.send_reset_password_instructions(resource_params)
-
4
yield resource if block_given?
-
-
4
return head :ok if successfully_sent?(resource)
-
-
2
render json: { message: resource.errors.full_messages }, status: :bad_request
-
end
-
-
# GET /resource/password/edit?reset_password_token=abcdef
-
# def edit
-
# super
-
# end
-
-
# PUT /resource/password
-
2
def update
-
3
self.resource = resource_class.reset_password_by_token(resource_params)
-
3
yield resource if block_given?
-
-
3
resource.errors.empty? ? update_success : update_fail
-
end
-
-
# protected
-
-
# def after_resetting_password_path_for(resource)
-
# super(resource)
-
# end#
-
-
# The path used after sending reset password instructions
-
# def after_sending_reset_password_instructions_path_for(resource_name)
-
# super(resource_name)
-
# end
-
-
2
private
-
-
2
def update_fail
-
2
set_minimum_password_length
-
2
render json: { message: resource.errors.full_messages }, status: :bad_request
-
end
-
-
2
def update_success
-
1
resource.unlock_access! if unlockable?(resource)
-
1
if Devise.sign_in_after_reset_password
-
1
resource.after_database_authentication
-
1
sign_in(resource_name, resource)
-
end
-
1
render js: "window.location='#{root_path}'"
-
end
-
end
-
# frozen_string_literal: true
-
-
# Controller for registration and update password
-
2
class Users::RegistrationsController < Devise::RegistrationsController
-
# before_action :configure_sign_up_params, only: [:create]
-
# before_action :configure_account_update_params, only: [:update]
-
-
# GET /resource/sign_up
-
# def new
-
# super
-
# end
-
-
# POST /resource
-
2
def create
-
6
build_resource(sign_up_params)
-
-
6
resource.save
-
6
yield resource if block_given?
-
6
resource.persisted? ? register_success : register_fail
-
end
-
-
# GET /resource/edit
-
# def edit
-
# super
-
# end
-
-
# PUT /resource
-
2
def update
-
7
if form_filled?
-
6
resource_updated = oauth_update_resource
-
6
yield resource if block_given?
-
6
resource_updated ? update_success : register_fail
-
else
-
1
render json: { message: 'Please fill in the form' }, status: :bad_request
-
end
-
end
-
-
# DELETE /resource
-
# def destroy
-
# super
-
# end
-
-
# GET /resource/cancel
-
# Forces the session data which is usually expired after sign
-
# in to be expired now. This is useful if the user wants to
-
# cancel oauth signing in/up in the middle of the process,
-
# removing all OAuth session data.
-
# def cancel
-
# super
-
# end
-
-
# protected
-
-
# If you have extra params to permit, append them to the sanitizer.
-
# def configure_sign_up_params
-
# devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute])
-
# end
-
-
# If you have extra params to permit, append them to the sanitizer.
-
# def configure_account_update_params
-
# devise_parameter_sanitizer.permit(:account_update, keys: [:attribute])
-
# end
-
-
# The path used after sign up.
-
# def after_sign_up_path_for(resource)
-
# super(resource)
-
# end
-
-
# The path used after sign up for inactive accounts.
-
# def after_inactive_sign_up_path_for(resource)
-
# super(resource)
-
# root_path
-
# end
-
-
2
private
-
-
2
def form_filled?
-
7
params[:user][:password].present? && params[:user][:password_confirmation].present?
-
end
-
-
2
def register_success
-
2
sign_up(resource_name, resource)
-
2
render js: "window.location='#{root_path}'"
-
end
-
-
2
def update_success
-
2
bypass_sign_in resource, scope: resource_name if sign_in_after_change_password?
-
-
2
flash[:toast_success] = 'Password Changed'
-
2
render js: "window.location='#{settings_account_path}'"
-
end
-
-
2
def register_fail
-
8
clean_up_passwords resource
-
8
set_minimum_password_length
-
8
render json: { message: resource.errors.full_messages }, status: :bad_request
-
end
-
-
2
def oauth_update_resource
-
6
if resource.password_automatically_set?
-
3
resource.update(account_update_params)
-
else
-
3
update_resource(resource, account_update_params)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# Controller for handling user sessions
-
2
class Users::SessionsController < Devise::SessionsController
-
# before_action :configure_sign_in_params, only: [:create]
-
-
# GET /resource/sign_in
-
# def new
-
# super
-
# end
-
-
# POST /resource/sign_in
-
2
def create
-
5
self.resource = warden.authenticate(auth_options)
-
-
5
if resource
-
2
sign_in(resource_name, resource)
-
2
yield resource if block_given?
-
2
render js: "window.location='#{after_sign_in_path_for(resource)}'"
-
else
-
3
set_flash_message(:errors, :invalid)
-
3
render json: { message: flash[:errors] }, status: :unauthorized
-
end
-
end
-
-
# DELETE /resource/sign_out
-
# def destroy
-
# super
-
# end
-
-
# protected
-
-
# If you have extra params to permit, append them to the sanitizer.
-
# def configure_sign_in_params
-
# devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
-
# end
-
end
-
# frozen_string_literal: true
-
-
2
class Users::Settings::AccountsController < Users::Settings::BaseController
-
2
def show
-
23
render 'users/settings/account'
-
end
-
-
2
def disconnect_omniauth
-
8
identity = current_user.identities.find_by!(provider: params[:provider])
-
-
7
if disconnect_provider_allowed?
-
4
identity.delete
-
4
flash[:toast_success] = 'Account Disconnected'
-
else
-
3
flash[:toast_error] = 'Please set up a password before disabling all Social Accounts'
-
end
-
-
7
redirect_to settings_account_path
-
end
-
-
2
def reset_password
-
1
current_user.send_reset_password_instructions
-
1
flash[:toast_success] = 'Check your email for reset instruction'
-
1
redirect_to settings_account_path
-
end
-
-
2
private
-
-
2
def disconnect_provider_allowed?
-
7
!current_user.password_automatically_set? || current_user.identities.size > 1
-
end
-
end
-
# frozen_string_literal: true
-
-
2
class Users::Settings::BaseController < ApplicationController
-
2
layout 'user'
-
end
-
# frozen_string_literal: true
-
-
1
class Users::Settings::BillingsController < Users::Settings::BaseController
-
1
def show
-
3
render 'users/settings/billing'
-
end
-
-
1
def update
-
2
current_user.license.update(plan: params[:plan])
-
1
flash[:toast_success] = 'Billing plan updated'
-
rescue ArgumentError
-
1
flash[:toast_error] = 'Invalid argument'
-
ensure
-
2
redirect_to settings_billing_path
-
end
-
end
-
# frozen_string_literal: true
-
-
2
class Users::Settings::EmailsController < Users::Settings::BaseController
-
2
def show
-
6
render 'users/settings/emails'
-
end
-
-
2
def subscribe
-
2
NewsletterSubscription.subscribe(current_user.email)
-
-
2
flash[:toast_success] = 'Newsletter Subscribed'
-
2
redirect_to settings_emails_path
-
end
-
-
2
def unsubscribe
-
2
NewsletterSubscription.find_by(email: current_user.email)&.unsubscribe
-
-
2
flash[:toast_success] = 'Newsletter Unsubscribed'
-
2
redirect_to settings_emails_path
-
end
-
end
-
# frozen_string_literal: true
-
-
# Controller for setting user personality
-
2
class Users::Settings::PersonalitiesController < Users::Settings::BaseController
-
2
def show
-
36
@personality = current_user.personality || Personality.new
-
36
render 'users/settings/personalities'
-
end
-
-
2
def update
-
# Invalid input will by rescued by StatementInvalid in ApplcationController
-
18
current_user.update(personality: Personality.find_by(personality_params))
-
-
17
flash[:toast_success] = 'Personality Updated'
-
17
render js: "window.location = '#{settings_personality_path}'"
-
end
-
-
2
private
-
-
2
def personality_params
-
18
params.require(:personality).permit(:mind, :energy, :nature, :tactic)
-
end
-
end
-
# frozen_string_literal: true
-
-
2
class Users::Settings::ProfilesController < Users::Settings::BaseController
-
2
def show
-
6
render 'users/settings/profile'
-
end
-
-
2
def update
-
6
if current_user.update(profile_params)
-
2
render json: { message: 'Profile updated' }
-
else
-
4
render json: { message: current_user.errors.full_messages }, status: :bad_request
-
end
-
end
-
-
2
def remove_avatar
-
2
return head :not_found unless current_user.avatar.attached?
-
-
1
current_user.avatar.purge_later
-
-
1
redirect_to settings_profile_path, status: :found
-
end
-
-
2
private
-
-
2
def profile_params
-
6
params.require(:user).permit(:name, :avatar, :birthdate)
-
end
-
end
-
# frozen_string_literal: true
-
-
2
class Users::Settings::SkillsController < Users::Settings::BaseController
-
2
def show
-
8
@skills = current_user.skills.with_category('category_name ASC, name ASC')
-
8
render 'users/settings/skills'
-
end
-
-
2
def create
-
# insert_all does not trigger validations, handle the errors on client-side
-
3
records = UserSkill.insert_all(
-
3
skill_params[:skill_ids].map { |id| { user_id: current_user.id, skill_id: id } }
-
)
-
-
3
if skill_params[:skill_ids].length == records.length
-
2
head :ok
-
else
-
1
head :unprocessable_entity
-
end
-
end
-
-
2
def destroy
-
2
user_skill = current_user.user_skills.find_by!(skill_id: params[:skill_id])
-
1
user_skill.delete ? head(:ok) : head(:bad_request)
-
end
-
-
2
private
-
-
2
def skill_params
-
6
params.require(:skill).permit(skill_ids: [])
-
end
-
end
-
# frozen_string_literal: true
-
-
# Controller for users
-
2
class UsersController < ApplicationController
-
2
skip_before_action :authenticate_user!
-
-
2
before_action :set_user, only: %i[show timeline]
-
-
2
layout 'user'
-
-
2
def show
-
25
render_projects(params[:joined].present? ? :profile_joined : :profile_owned)
-
25
prepare_skills
-
25
prepare_counts
-
end
-
-
2
def timeline
-
5
return render_404 unless request.xhr?
-
-
5
month = find_next_activity(params[:month].to_i)
-
5
month.present? ? render_timeline(month) : head(:no_content)
-
end
-
-
2
private
-
-
2
def set_user
-
31
@user = User.find(params[:id])
-
end
-
-
2
def find_next_activity(month)
-
5
data = @user.activities.where('created_at <= ?',
-
DateTime.current.beginning_of_month << month)
-
-
5
data.order('created_at DESC').first.created_at.to_datetime if data.exists?
-
end
-
-
2
def render_timeline(mth)
-
3
tasks, events = authorized_scope(@user.activities.from_month(mth))
-
3
html = view_to_html_string('users/_timeline',
-
events: events,
-
tasks: tasks,
-
header: mth.strftime('%B %Y'))
-
-
3
render json: { html: html, m: ((Time.current - mth) / 1.month).floor }
-
end
-
-
2
def render_projects(policy_scope)
-
25
pagy, projects = pagy(authorized_scope(
-
Project.search(params),
-
as: policy_scope,
-
scope_options: { profile_owner: @user }
-
))
-
25
html = view_to_html_string('projects/_projects', projects: projects)
-
-
25
respond_to do |format|
-
25
format.html
-
31
format.json { render json: { html: html, total: pagy.count } }
-
end
-
end
-
-
2
def prepare_skills
-
25
@skills = @user.user_skills.joins(skill: :category)
-
.select('user_skills.created_at')
-
.select('skills.name AS name')
-
.select('categories.name AS category_name')
-
25
@chart = @user.skills.joins(:category)
-
.select('categories.name')
-
.group('categories.name').count
-
end
-
-
2
def prepare_counts
-
25
joined_ids = @user.teams.present? ? "(#{@user.teams.pluck(:project_id).join(',')})" : '(0)'
-
-
25
@counts = @user.projects.pluck(
-
Arel.sql(
-
"(SELECT COUNT(1) FROM projects WHERE id IN #{joined_ids} AND NOT user_id = #{@user.id}),"\
-
'COUNT(1),'\
-
"(SELECT COUNT(1) FROM projects WHERE id IN #{joined_ids} AND status = 'completed')"
-
)
-
).first
-
end
-
end
-
# frozen_string_literal: true
-
-
# Base helper module
-
4
module ApplicationHelper
-
4
def current_path?(path)
-
1426
request.path == path
-
end
-
-
4
def current_controller?(*args)
-
448
args.any? do |v|
-
449
v.to_s.downcase == controller_name || v.to_s.downcase == controller_path
-
end
-
end
-
-
4
def current_action?(*args)
-
268
args.any? { |v| v.to_s.downcase == action_name }
-
end
-
end
-
# frozen_string_literal: true
-
-
# Helper for showing avatar images
-
4
module AvatarHelper
-
4
include Rails.application.routes.url_helpers
-
4
include GravatarImageTag
-
-
4
def user_avatar(user)
-
434
if user.avatar.attached?
-
6
rails_representation_url(user.avatar.variant(resize: '100x100!'))
-
else
-
428
gravatar_image_url(user.email, size: 100, default: :retro, secure: true)
-
end
-
end
-
-
4
def project_icon(project)
-
145
return source_identicon(project) unless project.avatar.attached?
-
-
2
content_tag(:figure, class: 'image') do
-
2
image_tag rails_representation_url(project.avatar.variant(resize: '100x100!')), alt: 'Project avatar'
-
end
-
end
-
-
4
def get_avatars(ids)
-
9
arr = {}
-
9
User.find(ids).each do |u|
-
3
arr[u.id] = u.avatar.attached? ? url_for(user_avatar(u)) : user_avatar(u)
-
end
-
9
arr
-
end
-
-
4
private
-
-
4
def source_identicon(project)
-
143
content_tag(:div, class: "identicon bg#{(project.id % 7) + 1}") do
-
143
project.name.first.upcase
-
end
-
end
-
-
4
def default_url_options
-
71
{ only_path: true }
-
end
-
end
-
# frozen_string_literal: true
-
-
# Helper for devise authentication
-
4
module DeviseHelper
-
PROVIDERS = {
-
4
google_oauth2: {
-
label: 'Google',
-
icon: 'flat-color-icons:google'
-
},
-
facebook: {
-
label: 'Facebook',
-
icon: 'fe:facebook'
-
},
-
twitter: {
-
label: 'Twitter',
-
icon: 'fe:twitter'
-
}
-
}.freeze
-
-
4
def provider_label(provider)
-
210
PROVIDERS[provider][:label]
-
end
-
-
4
def provider_icon(provider)
-
141
PROVIDERS[provider][:icon]
-
end
-
end
-
# frozen_string_literal: true
-
-
# Helper for creating navigation links
-
4
module NavHelper
-
4
def nav_link_to(name = nil, options = {}, &block)
-
1865
classes = [options.delete(:class)&.split(' ')]
-
1865
classes << 'is-active' if active_nav_link?(options)
-
-
1865
if block_given?
-
986
link_to(options[:path], class: classes) { capture(&block) + name }
-
else
-
1372
link_to name, options[:path], class: classes
-
end
-
end
-
-
4
def active_nav_link?(options = {})
-
# Path params is ignored if controller or action is provided
-
1865
if only_path?(options)
-
1424
current_path?(options[:path])
-
else
-
441
current_controller_and_action?(options)
-
end
-
end
-
-
4
private
-
-
4
def only_path?(options)
-
1865
!options[:controller] && !options[:action]
-
end
-
-
4
def current_controller_and_action?(options)
-
441
c = options.delete(:controller)
-
441
a = options.delete(:action)
-
-
441
if c && a
-
# When given both options, make sure BOTH are true
-
437
current_controller?(*c) && current_action?(*a)
-
else
-
# Otherwise check EITHER option
-
4
current_controller?(*c) || current_action?(*a)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
4
class ApplicationMailer < ActionMailer::Base
-
4
default from: 'Diversify <no-reply@sheffield.ac.uk>'
-
4
layout 'mailer'
-
end
-
# frozen_string_literal: true
-
-
# Mailer class for Newsletter
-
4
class NewsletterMailer < ApplicationMailer
-
4
def send_newsletter(emails, newsletter)
-
4
@content = newsletter.content
-
-
4
mail(to: 'no-reply@sheffield.ac.uk',
-
bcc: emails,
-
subject: newsletter.title,
-
content_type: 'text/html')
-
end
-
-
4
def send_welcome(email)
-
3
mail(to: 'no-reply@sheffield.ac.uk',
-
bcc: email,
-
subject: 'Welcome to Diversify',
-
content_type: 'text/html')
-
end
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: activities
-
#
-
# id :bigint not null, primary key
-
# key :string default("")
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# project_id :bigint
-
# user_id :bigint not null
-
#
-
# Indexes
-
#
-
# index_activities_on_project_id (project_id)
-
# index_activities_on_user_id (user_id)
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (project_id => projects.id)
-
# fk_rails_... (user_id => users.id)
-
#
-
4
class Activity < ApplicationRecord
-
4
belongs_to :user
-
4
belongs_to :project, optional: true
-
-
4
validates :key, presence: true
-
-
9
scope :from_month, ->(mth) { where(created_at: mth.all_month) }
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: ahoy_events
-
#
-
# id :bigint not null, primary key
-
# name :string
-
# properties :jsonb
-
# time :datetime
-
# user_id :bigint
-
# visit_id :bigint
-
#
-
# Indexes
-
#
-
# index_ahoy_events_on_name_and_time (name,time)
-
# index_ahoy_events_on_properties (properties) USING gin
-
# index_ahoy_events_on_user_id (user_id)
-
# index_ahoy_events_on_visit_id (visit_id)
-
#
-
-
4
class Ahoy::Event < ApplicationRecord
-
4
include Ahoy::QueryMethods
-
-
4
self.table_name = 'ahoy_events'
-
-
4
belongs_to :visit
-
4
belongs_to :user, optional: true
-
-
11
scope :action, -> { where(name: 'Ran action') }
-
15
scope :social, -> { where(name: 'Click Social') }
-
7
scope :type_size, -> { group("properties ->> 'type'").size }
-
7
scope :type_time_size, -> { group("properties ->> 'type'").group_by_day(:time).size }
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: ahoy_visits
-
#
-
# id :bigint not null, primary key
-
# app_version :string
-
# browser :string
-
# city :string
-
# country :string
-
# device_type :string
-
# ip :string
-
# landing_page :text
-
# latitude :float
-
# longitude :float
-
# os :string
-
# os_version :string
-
# platform :string
-
# referrer :text
-
# referring_domain :string
-
# region :string
-
# started_at :datetime
-
# user_agent :text
-
# utm_campaign :string
-
# utm_content :string
-
# utm_medium :string
-
# utm_source :string
-
# utm_term :string
-
# visit_token :string
-
# visitor_token :string
-
# user_id :bigint
-
#
-
# Indexes
-
#
-
# index_ahoy_visits_on_user_id (user_id)
-
# index_ahoy_visits_on_visit_token (visit_token) UNIQUE
-
#
-
-
4
class Ahoy::Visit < ApplicationRecord
-
4
self.table_name = 'ahoy_visits'
-
-
4
has_many :events, class_name: 'Ahoy::Event', dependent: :destroy
-
4
belongs_to :user, optional: true
-
-
16
scope :today_count, -> { where(started_at: Time.zone.today.all_day).size }
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: appeals
-
#
-
# id :bigint not null, primary key
-
# type :enum default("invitation"), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# project_id :bigint not null
-
# user_id :bigint not null
-
#
-
# Indexes
-
#
-
# index_appeals_on_project_id (project_id)
-
# index_appeals_on_user_id (user_id)
-
# index_appeals_on_user_id_and_project_id (user_id,project_id) UNIQUE
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (project_id => projects.id)
-
# fk_rails_... (user_id => users.id)
-
#
-
4
class Appeal < ApplicationRecord
-
4
self.inheritance_column = nil
-
-
4
enum type: { invitation: 'invitation', application: 'application' }
-
-
4
belongs_to :project
-
4
belongs_to :user
-
-
4
validate :not_owner, on: :create
-
4
validate :in_project, on: :create
-
-
4
validates :user_id,
-
presence: true,
-
uniqueness: { scope: :project_id,
-
message: 'has already been invited/applied' }
-
-
4
scope :list_in_project, lambda { |project|
-
10
joins(:user)
-
.select('appeals.id, users.id AS user_id, users.email AS user_email')
-
.where(project: project)
-
}
-
-
4
after_create_commit :send_notification
-
-
# resolution: accept or decline
-
4
def send_resolve_notification(resolution, is_cancel = false)
-
11
Notification.delete_by(send_notification_params)
-
11
return if is_cancel
-
-
7
SendNotificationJob.perform_later(
-
7
invitation? ? [project.user] : [user],
-
{ key: "#{type}/#{resolution}",
-
7
notifiable: invitation? ? user : project,
-
notifier: project }
-
)
-
-
7
join_activity if resolution == 'accept'
-
end
-
-
4
private
-
-
4
def join_activity
-
4
Activity.find_or_create_by(key: 'project/join', user: user, project: project)
-
end
-
-
4
def send_notification
-
35
SendNotificationJob.perform_later(
-
35
invitation? ? [project.user] : [user],
-
send_notification_params
-
)
-
end
-
-
4
def send_notification_params
-
{
-
92
user: invitation? ? user : project.user,
-
key: "#{type}/send",
-
46
notifiable: invitation? ? project : user,
-
notifier: project
-
}
-
end
-
-
4
def not_owner
-
44
errors[:base] << 'Owner cannot be added to project' if user == project&.user
-
end
-
-
4
def in_project
-
44
errors[:base] << 'User already in project' if user&.in_project?(project)
-
end
-
end
-
# frozen_string_literal: true
-
-
4
class ApplicationRecord < ActiveRecord::Base
-
4
self.abstract_class = true
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: categories
-
#
-
# id :bigint not null, primary key
-
# name :string default(""), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
# Indexes
-
#
-
# index_categories_on_name (name) UNIQUE
-
#
-
-
4
class Category < ApplicationRecord
-
4
has_many :skills, dependent: :nullify
-
4
has_many :projects, dependent: :nullify
-
-
4
validates :name, presence: true, uniqueness: { case_sensitive: false }
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: collaborations
-
#
-
# id :bigint not null, primary key
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# team_id :bigint not null
-
# user_id :bigint not null
-
#
-
# Indexes
-
#
-
# index_collaborations_on_team_id (team_id)
-
# index_collaborations_on_user_id (user_id)
-
# index_collaborations_on_user_id_and_team_id (user_id,team_id) UNIQUE
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (team_id => teams.id)
-
# fk_rails_... (user_id => users.id)
-
#
-
-
4
class Collaboration < ApplicationRecord
-
4
belongs_to :user
-
4
belongs_to :team
-
-
4
validates :user_id, presence: true, uniqueness: { scope: :team_id }
-
4
validates :team_id, presence: true
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: identities
-
#
-
# id :bigint not null, primary key
-
# provider :string
-
# uid :string
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# user_id :bigint
-
#
-
# Indexes
-
#
-
# index_identities_on_provider_and_uid (provider,uid) UNIQUE
-
# index_identities_on_provider_and_user_id (provider,user_id) UNIQUE
-
# index_identities_on_user_id (user_id)
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (user_id => users.id)
-
#
-
-
# Identity model, for OAuth
-
3
class Identity < ApplicationRecord
-
3
belongs_to :user
-
-
3
validates :provider, presence: true
-
3
validates :uid,
-
presence: true,
-
uniqueness: { case_sensitive: false,
-
scope: :provider,
-
message: 'Account has been taken' }
-
3
validates :user_id,
-
presence: true,
-
uniqueness: { scope: :provider, message: 'Account has been taken' }
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: landing_feedbacks
-
#
-
# id :bigint not null, primary key
-
# channel :string default(""), not null
-
# interest :boolean default(TRUE), not null
-
# smiley :string default(""), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
-
4
class LandingFeedback < ApplicationRecord
-
4
CHANNEL = [
-
'Social Media',
-
'Search Engine',
-
'Newspaper',
-
'Recommended by others'
-
].freeze
-
-
4
validates :channel, presence: true, inclusion: { in: CHANNEL }
-
4
validates :interest, inclusion: { in: [true, false] }
-
4
validates :smiley, presence: true
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: licenses
-
#
-
# id :bigint not null, primary key
-
# plan :enum default("free"), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# user_id :bigint
-
#
-
# Indexes
-
#
-
# index_licenses_on_user_id (user_id) UNIQUE
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (user_id => users.id)
-
#
-
-
4
class License < ApplicationRecord
-
4
MEMBER_LIMIT = { free: 10, pro: 30, ultimate: 1 / 0.0 }.freeze
-
-
4
enum plan: { free: 'free', pro: 'pro', ultimate: 'ultimate' }
-
-
4
belongs_to :user
-
-
4
validates :plan, presence: true
-
4
validates :user_id, presence: true, uniqueness: true
-
-
4
def member_limit
-
748
MEMBER_LIMIT[plan.to_sym]
-
end
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: newsletters
-
#
-
# id :bigint not null, primary key
-
# title :string not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
-
4
class Newsletter < ApplicationRecord
-
# Equivalent to: has one :rich_text_content
-
4
has_rich_text :content
-
-
4
validates :title, presence: true
-
4
validates :content, presence: true
-
-
4
scope :unsubscription_by_newsletter, lambda {
-
11
find_by_sql(
-
"SELECT newsletters.title,
-
newsletters.created_at, COUNT(newsletter_feedbacks)
-
as feedback_count FROM newsletters JOIN newsletter_feedbacks
-
ON newsletter_feedbacks.created_at BETWEEN newsletters.created_at
-
AND newsletters.created_at + interval '7 days' GROUP BY newsletters.id"
-
)
-
}
-
-
4
after_commit :send_newsletter, on: :create
-
-
4
private
-
-
4
def send_newsletter
-
28
NewsletterSubscription.all_subscribed_emails.each_slice(50) do |emails|
-
1
NewsletterMailer.send_newsletter(emails, self).deliver_later
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: newsletter_feedbacks
-
#
-
# id :bigint not null, primary key
-
# email :string default(""), not null
-
# reasons :string default([]), not null, is an Array
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# newsletter_subscription_id :bigint not null
-
#
-
# Indexes
-
#
-
# index_newsletter_feedbacks_on_newsletter_subscription_id (newsletter_subscription_id)
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (newsletter_subscription_id => newsletter_subscriptions.id)
-
#
-
-
4
class NewsletterFeedback < ApplicationRecord
-
4
REASONS = { no_longer: 'I no longer want to receive these emails',
-
too_frequent: 'The emails are too frequent',
-
never_signed: 'I never signed up for the newsletter',
-
inappropriate: 'The emails are inappropriate',
-
not_interested: 'I am not interested anymore' }.freeze
-
-
4
belongs_to :newsletter_subscription, optional: true
-
-
4
validates :reasons,
-
presence: true,
-
array_inclusion: { in: REASONS.keys.map(&:to_s) << 'admin' }
-
-
4
before_save :validate_subscription_status
-
-
4
after_commit :change_subscribed_to_false
-
-
4
def self.count_reason(feedbacks)
-
5
feedbacks
-
2
.reduce([]) { |arr, fb| arr.concat(fb.reasons) }
-
.group_by(&:itself)
-
.transform_values(&:count)
-
.transform_keys do |key|
-
2
REASONS.key?(key.to_sym) ? REASONS[key.to_sym] : key
-
end
-
end
-
-
4
private
-
-
# Disallow submitting multiple feedback after unsubscription
-
4
def validate_subscription_status
-
21
throw :abort unless newsletter_subscription&.subscribed?
-
end
-
-
4
def change_subscribed_to_false
-
19
newsletter_subscription&.unsubscribe
-
end
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: newsletter_subscriptions
-
#
-
# id :bigint not null, primary key
-
# email :string not null
-
# subscribed :boolean default(TRUE), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
# Indexes
-
#
-
# index_newsletter_subscriptions_on_email (email) UNIQUE
-
#
-
-
4
class NewsletterSubscription < ApplicationRecord
-
4
has_many :newsletter_feedbacks, dependent: :nullify
-
-
4
validates :email,
-
presence: true,
-
uniqueness: true,
-
format: { with: URI::MailTo::EMAIL_REGEXP }
-
-
42
scope :all_subscribed_emails, -> { where(subscribed: true).pluck(:email) }
-
6
scope :previously_subscribed, -> { where(subscribed: false) }
-
11
scope :subscribed_count, -> { all_subscribed_emails.size }
-
-
4
after_commit :send_welcome, on: :create
-
-
4
def self.subscribe(email)
-
12
record = where(email: email).first_or_initialize
-
-
12
record.update(subscribed: true) if record.new_record? || (record.persisted? && !record.subscribed?)
-
end
-
-
4
def unsubscribe
-
23
return true unless subscribed?
-
-
22
update_columns(subscribed: false) # rubocop:disable Rails/SkipsModelValidations
-
end
-
-
4
private
-
-
4
def send_welcome
-
173
NewsletterMailer.send_welcome(email).deliver_later
-
end
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: notifications
-
#
-
# id :bigint not null, primary key
-
# key :string default("")
-
# notifiable_type :string not null
-
# notifier_type :string not null
-
# read :boolean default(FALSE), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# notifiable_id :bigint not null
-
# notifier_id :bigint not null
-
# user_id :bigint not null
-
#
-
# Indexes
-
#
-
# index_notifications_on_notifiable_type_and_notifiable_id (notifiable_type,notifiable_id)
-
# index_notifications_on_notifier_type_and_notifier_id (notifier_type,notifier_id)
-
# index_notifications_on_user_id (user_id)
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (user_id => users.id)
-
#
-
4
class Notification < ApplicationRecord
-
4
belongs_to :user
-
4
belongs_to :notifiable, polymorphic: true
-
4
belongs_to :notifier, polymorphic: true
-
-
599
scope :unread, -> { where(read: false) }
-
-
# TODO: tasks may not have avatar attachment, so the eager load wont work
-
6
scope :load_index, -> { includes(:notifier, notifiable: [{ avatar_attachment: :blob }]) }
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: personalities
-
#
-
# id :bigint not null, primary key
-
# compatibilities :integer default([]), is an Array
-
# energy :enum
-
# mind :enum
-
# nature :enum
-
# tactic :enum
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
#
-
# Indexes
-
#
-
# index_personalities_on_mind_and_energy_and_nature_and_tactic (mind,energy,nature,tactic) UNIQUE
-
#
-
-
4
class Personality < ApplicationRecord
-
TYPES = {
-
4
INTJ: 'Architect',
-
INTP: 'Logician',
-
ENTJ: 'Commander',
-
ENTP: 'Debater',
-
INFJ: 'Advocate',
-
INFP: 'Mediator',
-
ENFJ: 'Protagonist',
-
ENFP: 'Campaigner',
-
ISTJ: 'Logistician',
-
ISFJ: 'Defender',
-
ESTJ: 'Executive',
-
ESFJ: 'Consul',
-
ISTP: 'Virtuoso',
-
ISFP: 'Adventurer',
-
ESTP: 'Entrepreneur',
-
ESFP: 'Entertainer'
-
}.freeze
-
-
4
has_many :users, dependent: :nullify
-
-
4
validates :mind, presence: true, inclusion: { in: %w[I E] }
-
4
validates :energy, presence: true, inclusion: { in: %w[S N] }
-
4
validates :nature, presence: true, inclusion: { in: %w[T F] }
-
4
validates :tactic, presence: true, inclusion: { in: %w[J P] }
-
-
4
def to_type
-
35
TYPES[trait.to_sym]
-
end
-
-
4
def trait
-
36
mind + energy + nature + tactic
-
end
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: projects
-
#
-
# id :bigint not null, primary key
-
# description :text default(""), not null
-
# name :string(100) default(""), not null
-
# status :enum default("active"), not null
-
# visibility :boolean default(TRUE), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# category_id :bigint
-
# user_id :bigint not null
-
#
-
# Indexes
-
#
-
# index_projects_on_category_id (category_id)
-
# index_projects_on_user_id (user_id)
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (category_id => categories.id)
-
# fk_rails_... (user_id => users.id)
-
#
-
-
4
class Project < ApplicationRecord
-
4
include ActionPolicy::Behaviour
-
-
# To prevent ORDER BY injection
-
SORT_BY = {
-
4
created_asc: 'projects.created_at ASC',
-
created_desc: 'projects.created_at DESC',
-
name: 'projects.name ASC'
-
}.freeze
-
-
4
enum status: { open: 'open', active: 'active', completed: 'completed' }
-
-
4
belongs_to :user
-
4
belongs_to :category
-
-
4
has_one_attached :avatar
-
-
4
has_many :appeals, dependent: :destroy
-
4
has_many :notifications, as: :notifiable, dependent: :destroy
-
4
has_many :notifications, as: :notifier, dependent: :destroy
-
4
has_many :activities, dependent: :destroy
-
4
has_many :tasks, dependent: :destroy
-
4
has_many :teams, dependent: :destroy
-
4
has_many :users, through: :teams
-
-
4
validates :name, presence: true, length: { maximum: 100 }
-
4
validates :status, presence: true
-
-
4
validates :avatar, content_type: %w[image/png image/jpg image/jpeg],
-
size: { less_than: 200.kilobytes }
-
-
4
scope :search, lambda { |params|
-
52
with_attached_avatar
-
.left_outer_joins(:category)
-
.joins(:user)
-
.select('projects.*')
-
.select('categories.name AS category_name')
-
.select('users.name AS user_name, users.email')
-
.where('projects.status::text ~ ?', params[:status] || '')
-
.where('categories.name ~ ?', params[:category] || '')
-
.where('projects.name ~* :query OR projects.description ~* :query', query: params[:query] || '')
-
.order(SORT_BY[params[:sort]&.to_sym] || SORT_BY[:created_desc])
-
}
-
-
4
before_validation :validate_status_update,
-
on: :update,
-
if: :will_save_change_to_status?
-
-
4
before_save :validate_visibility_change, if: :will_save_change_to_visibility?
-
-
4
before_commit :create_unassigned_team, on: :create
-
4
after_create_commit :create_activity
-
4
after_update_commit :complete_activity, if: :saved_change_to_status?
-
-
4
def applicable?
-
68
open? && visibility
-
end
-
-
4
def unassigned_team
-
230
teams.find_by(name: 'Unassigned')
-
end
-
-
4
def check_users_limit
-
680
return if users.size < user.license.member_limit
-
-
3
errors.add(:base, 'Project is already full')
-
3
throw :abort
-
end
-
-
4
private
-
-
4
def validate_status_update
-
7
if status_was != 'active' && status != 'active'
-
1
errors.add(:base, 'Invalid status change')
-
1
throw :abort
-
end
-
-
6
check_users_limit if status == 'open'
-
end
-
-
4
def validate_visibility_change
-
28
return if allowed_to?(:change_visibility?, self, context: { user: user })
-
-
1
errors.add(:base, 'Private project not available on free license')
-
1
throw :abort
-
end
-
-
4
def create_unassigned_team
-
427
teams.create(name: 'Unassigned', team_size: 999).users << user
-
end
-
-
4
def create_activity
-
427
activities.create(key: 'project/create', user: user)
-
end
-
-
4
def complete_activity
-
5
activities.find_or_create_by(key: 'project/complete', user: user) if completed?
-
end
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: skills
-
#
-
# id :bigint not null, primary key
-
# name :string default(""), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# category_id :bigint
-
#
-
# Indexes
-
#
-
# index_skills_on_category_id (category_id)
-
# index_skills_on_name (name) UNIQUE
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (category_id => categories.id)
-
#
-
-
4
class Skill < ApplicationRecord
-
4
belongs_to :category
-
-
4
has_many :task_skills, dependent: :destroy
-
4
has_many :tasks, through: :task_skills
-
4
has_many :team_skills, dependent: :destroy
-
4
has_many :teams, through: :team_skills
-
4
has_many :user_skills, dependent: :destroy
-
4
has_many :users, through: :user_skills
-
-
4
validates :name, presence: true, uniqueness: { case_sensitive: false }
-
-
4
scope :with_category, lambda { |*order|
-
23
joins(:category)
-
.select(:id, :name)
-
.select('categories.name AS category_name')
-
.order(order.presence || 'skills.name ASC')
-
}
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: tasks
-
#
-
# id :bigint not null, primary key
-
# description :text default(""), not null
-
# name :string default(""), not null
-
# percentage :integer default(0), not null
-
# priority :enum default("medium"), not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# project_id :bigint
-
# user_id :bigint
-
#
-
# Indexes
-
#
-
# index_tasks_on_project_id (project_id)
-
# index_tasks_on_user_id (user_id)
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (project_id => projects.id)
-
# fk_rails_... (user_id => users.id)
-
#
-
-
4
class Task < ApplicationRecord
-
4
enum priority: { high: 'high', medium: 'medium', low: 'low' }
-
-
4
belongs_to :project
-
4
belongs_to :user
-
-
4
has_many :task_users, dependent: :destroy
-
4
has_many :users, through: :task_users,
-
before_add: :check_in_project,
-
after_add: :send_assigned_notification,
-
after_remove: :send_removed_notification
-
-
4
has_many :task_skills, dependent: :destroy
-
4
has_many :skills, through: :task_skills
-
-
4
validates :name, presence: true
-
4
validates :priority, presence: true
-
4
validates :percentage, presence: true, numericality: { only_integer: true },
-
inclusion: {
-
in: 0..100,
-
message: 'Percentage should be between 0 and 100'
-
}
-
-
4
after_update_commit :send_update_notification
-
4
after_destroy_commit :destroy_notifications
-
-
4
def completed?
-
6
percentage == 100
-
end
-
-
4
def send_picked_up_notification
-
2
SendNotificationJob.perform_later([user], notification_params('task/picked'))
-
end
-
-
4
private
-
-
4
def send_update_notification
-
13
if saved_change_to_percentage? && completed?
-
5
SendNotificationJob.perform_later([user], notification_params('task/completed'))
-
5
users.each do |user|
-
1
Activity.find_or_create_by(key: "task/#{id}", user: user, project: project)
-
end
-
else
-
8
SendNotificationJob.perform_later(users.to_a, notification_params('task/update'))
-
end
-
end
-
-
4
def send_assigned_notification(user)
-
21
SendNotificationJob.perform_later([user], notification_params('task/assigned'))
-
end
-
-
4
def send_removed_notification(user)
-
1
SendNotificationJob.perform_later([user], notification_params('task/removed'))
-
end
-
-
4
def destroy_notifications
-
5
Notification.delete_by(notifier: self)
-
end
-
-
4
def notification_params(key)
-
37
{ key: key, notifiable: project, notifier: self }
-
end
-
-
4
def check_in_project(record)
-
22
return if record.in_project?(project)
-
-
1
errors[:base] << 'User is not in project'
-
1
throw(:abort)
-
end
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: task_skills
-
#
-
# id :bigint not null, primary key
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# skill_id :bigint not null
-
# task_id :bigint not null
-
#
-
# Indexes
-
#
-
# index_task_skills_on_skill_id (skill_id)
-
# index_task_skills_on_skill_id_and_task_id (skill_id,task_id) UNIQUE
-
# index_task_skills_on_task_id (task_id)
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (skill_id => skills.id)
-
# fk_rails_... (task_id => tasks.id)
-
#
-
4
class TaskSkill < ApplicationRecord
-
4
belongs_to :task
-
4
belongs_to :skill
-
-
4
validates :skill_id, presence: true, uniqueness: { scope: :task_id }
-
4
validates :task_id, presence: true
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: task_users
-
#
-
# id :bigint not null, primary key
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# task_id :bigint not null
-
# user_id :bigint not null
-
#
-
# Indexes
-
#
-
# index_task_users_on_task_id (task_id)
-
# index_task_users_on_user_id (user_id)
-
# index_task_users_on_user_id_and_task_id (user_id,task_id) UNIQUE
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (task_id => tasks.id)
-
# fk_rails_... (user_id => users.id)
-
#
-
4
class TaskUser < ApplicationRecord
-
4
belongs_to :task
-
4
belongs_to :user
-
-
4
validates :user_id, presence: true, uniqueness: { scope: :task_id }
-
4
validates :task_id, presence: true
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: teams
-
#
-
# id :bigint not null, primary key
-
# name :string default(""), not null
-
# team_size :integer not null
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# project_id :bigint not null
-
#
-
# Indexes
-
#
-
# index_teams_on_name_and_project_id (name,project_id) UNIQUE
-
# index_teams_on_project_id (project_id)
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (project_id => projects.id)
-
#
-
-
4
class Team < ApplicationRecord
-
4
belongs_to :project
-
-
# Join table
-
4
has_many :collaborations, dependent: :destroy
-
4
has_many :users, through: :collaborations, before_add: :check_users_limit,
-
after_add: :send_notification,
-
after_remove: :unassign_tasks
-
4
has_many :team_skills, dependent: :destroy
-
4
has_many :skills, through: :team_skills
-
-
4
validates :name,
-
presence: true,
-
uniqueness: { scope: :project_id, message: 'already exist' }
-
4
validates :team_size,
-
presence: true,
-
numericality: { greater_than_or_equal_to: 1 }
-
-
4
after_destroy_commit :destroy_notifications
-
-
4
private
-
-
4
def check_users_limit(_)
-
678
return project.check_users_limit unless users.size > team_size
-
-
1
errors[:base] << 'Team Size is smaller than total members'
-
end
-
-
4
def send_notification(user)
-
676
return if name == 'Unassigned'
-
-
30
SendNotificationJob.perform_later(
-
[user],
-
{ key: 'team', notifiable: project, notifier: self }
-
)
-
end
-
-
4
def unassign_tasks(user)
-
4
return if user.in_project?(project)
-
-
2
tasks = user.tasks.where(project: project)
-
-
2
tasks.each do |task|
-
task.users.delete(user)
-
end
-
end
-
-
4
def destroy_notifications
-
3
Notification.delete_by(notifier: self)
-
end
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: team_skills
-
#
-
# id :bigint not null, primary key
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# skill_id :bigint not null
-
# team_id :bigint not null
-
#
-
# Indexes
-
#
-
# index_team_skills_on_skill_id (skill_id)
-
# index_team_skills_on_skill_id_and_team_id (skill_id,team_id) UNIQUE
-
# index_team_skills_on_team_id (team_id)
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (skill_id => skills.id)
-
# fk_rails_... (team_id => teams.id)
-
#
-
4
class TeamSkill < ApplicationRecord
-
4
belongs_to :team
-
4
belongs_to :skill
-
-
4
validates :skill_id, presence: true, uniqueness: { scope: :team_id }
-
4
validates :team_id, presence: true
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: users
-
#
-
# id :bigint not null, primary key
-
# admin :boolean default(FALSE), not null
-
# birthdate :date
-
# email :string(254) default(""), not null
-
# encrypted_password :string default(""), not null
-
# name :string(255) default(""), not null
-
# password_automatically_set :boolean default(FALSE), not null
-
# remember_created_at :datetime
-
# reset_password_sent_at :datetime
-
# reset_password_token :string
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# personality_id :bigint
-
#
-
# Indexes
-
#
-
# index_users_on_email (email) UNIQUE
-
# index_users_on_personality_id (personality_id)
-
# index_users_on_reset_password_token (reset_password_token) UNIQUE
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (personality_id => personalities.id)
-
#
-
-
4
class User < ApplicationRecord
-
4
devise :database_authenticatable, :registerable,
-
:recoverable, :rememberable, :validatable,
-
:omniauthable, omniauth_providers: Devise.omniauth_providers
-
-
4
belongs_to :personality, optional: true
-
-
4
has_one_attached :avatar
-
4
has_one :license, dependent: :destroy
-
-
4
has_many :activities, dependent: :destroy
-
4
has_many :identities, dependent: :destroy
-
4
has_many :notifications, dependent: :destroy
-
4
has_many :projects, dependent: :destroy
-
4
has_many :appeals, dependent: :destroy
-
-
# Join table
-
4
has_many :collaborations, dependent: :destroy
-
4
has_many :teams, through: :collaborations
-
4
has_many :task_users, dependent: :destroy
-
4
has_many :tasks, through: :task_users
-
4
has_many :user_skills, dependent: :destroy
-
4
has_many :skills, through: :user_skills
-
-
4
validates :email, presence: true, uniqueness: true, length: { maximum: 254 },
-
format: { with: URI::MailTo::EMAIL_REGEXP }
-
-
4
validates :name, length: { maximum: 255 }
-
4
validates :name, presence: true, on: :update
-
-
4
validates :avatar, content_type: %w[image/png image/jpg image/jpeg],
-
size: { less_than: 200.kilobytes }
-
-
4
validate :provided_birthdate, on: :update
-
-
4
before_create :build_license, :build_activities
-
-
4
after_update_commit :disable_password_automatically_set,
-
if: [:saved_change_to_encrypted_password?,
-
3
proc { |user| user.password_automatically_set }]
-
-
4
def self.sign_in_omniauth(auth)
-
12
Identity.where(provider: auth.provider, uid: auth.uid).first_or_create(
-
user: create(
-
email: auth.info.email,
-
name: auth.info.name,
-
password: Devise.friendly_token[0, 20],
-
password_automatically_set: true
-
)
-
)
-
end
-
-
4
def connect_omniauth(auth)
-
6
identities.create(provider: auth.provider, uid: auth.uid)
-
end
-
-
4
def oauth_provider_connected?(provider = nil)
-
69
identities.exists?(provider: provider)
-
end
-
-
4
def newsletter_subscribed?
-
6
NewsletterSubscription.find_by(email: email)&.subscribed?
-
end
-
-
4
def empty_compatibility_data?
-
30
personality_id.blank? && user_skills.exists?
-
end
-
-
# TODO: use has_many :members, through: :teams
-
4
def in_project?(project)
-
237
teams&.exists?(project: project) || project&.user == self
-
end
-
-
4
def notifications
-
597
super.order(id: :desc, created_at: :desc)
-
end
-
-
4
def compatible_with?(target_user)
-
17
personality.compatibilities[target_user.personality_id - 1]
-
end
-
-
4
def self.teams_data(project)
-
33
includes(:user_skills)
-
.joins('LEFT OUTER JOIN task_users ON task_users.user_id = users.id')
-
.joins('LEFT OUTER JOIN tasks ON tasks.id = task_users.task_id AND tasks.percentage != 100')
-
.joins(:teams).where(teams: { project: project })
-
.select('users.id, users.name, users.email, users.personality_id')
-
.select('teams.id as team_id, COUNT(tasks.id) as count')
-
.group('users.id, teams.id')
-
end
-
-
4
private
-
-
4
def build_activities
-
1171
activities.build(key: 'user/create')
-
end
-
-
4
def disable_password_automatically_set
-
1
update_columns(password_automatically_set: false) # rubocop:disable Rails/SkipsModelValidations
-
end
-
-
4
def provided_birthdate
-
124
return if birthdate_before_type_cast.blank?
-
-
5
dob = Date.new(
-
birthdate_before_type_cast[1], # year
-
birthdate_before_type_cast[2], # month
-
birthdate_before_type_cast[3] # day of month
-
)
-
-
4
return errors.add(:birthdate, 'must be before the account creation date') if dob >= created_at.to_date
-
-
3
age = ((Time.current - dob.to_time) / 1.year.seconds).floor
-
3
return if age.between?(6, 80)
-
-
2
errors.add(:age, 'must be between 6 and 80 years old')
-
rescue StandardError
-
1
errors.add(:date, 'is invalid')
-
end
-
end
-
# frozen_string_literal: true
-
-
# == Schema Information
-
#
-
# Table name: user_skills
-
#
-
# id :bigint not null, primary key
-
# created_at :datetime not null
-
# updated_at :datetime not null
-
# skill_id :bigint not null
-
# user_id :bigint not null
-
#
-
# Indexes
-
#
-
# index_user_skills_on_skill_id (skill_id)
-
# index_user_skills_on_skill_id_and_user_id (skill_id,user_id) UNIQUE
-
# index_user_skills_on_user_id (user_id)
-
#
-
# Foreign Keys
-
#
-
# fk_rails_... (skill_id => skills.id)
-
# fk_rails_... (user_id => users.id)
-
#
-
3
class UserSkill < ApplicationRecord
-
3
belongs_to :user
-
3
belongs_to :skill
-
-
3
validates :skill_id, presence: true, uniqueness: { scope: :user_id }
-
3
validates :user_id, presence: true
-
end
-
# frozen_string_literal: true
-
-
# Class for Activity Policies
-
3
class ActivityPolicy < ApplicationPolicy
-
3
relation_scope do |scope|
-
5
task = scope.joins(:project)
-
.where("activities.key LIKE '%task%'")
-
.select('projects.name AS name, projects.id AS project_id')
-
.select('COUNT(projects.id) as count')
-
.group('projects.id')
-
-
5
project = scope.joins(:project)
-
.where("activities.key NOT LIKE '%task%'")
-
.select('activities.id, activities.created_at, key')
-
.select('projects.name AS name, projects.id AS project_id')
-
.order('activities.created_at DESC')
-
-
5
user = scope.where("activities.key LIKE '%user%'")
-
.select('activities.id, activities.created_at, activities.key')
-
-
5
return task, (project + user)
-
end
-
end
-
# frozen_string_literal: true
-
-
# Policy for Metrics controller
-
3
class AdminPolicy < ApplicationPolicy
-
3
default_rule :manage?
-
3
alias_rule :index?, :create?, :new?, to: :manage?
-
-
3
def manage?
-
210
user.admin?
-
end
-
end
-
# frozen_string_literal: true
-
-
3
class Appeal::ApplicationPolicy < Appeal::BasePolicy
-
3
def create?
-
9
record.project.applicable? && !project_owner?
-
end
-
-
3
def accept?
-
7
!owner? && project_owner? || user.admin?
-
end
-
end
-
# frozen_string_literal: true
-
-
3
class Appeal::BasePolicy < ApplicationPolicy
-
3
def index?
-
# Record is a project
-
14
owner? || user.admin?
-
end
-
-
3
def destroy?
-
12
owner? || project_owner? || user.admin?
-
end
-
end
-
# frozen_string_literal: true
-
-
3
class Appeal::InvitationPolicy < Appeal::BasePolicy
-
3
def create?
-
8
project_owner? || user.admin?
-
end
-
-
3
def accept?
-
9
owner? && !project_owner? || user.admin?
-
end
-
end
-
# frozen_string_literal: true
-
-
# Base class for application policies
-
4
class ApplicationPolicy < ActionPolicy::Base
-
4
authorize :user, allow_nil: true
-
-
4
def owner?
-
383
record.user_id == user&.id
-
end
-
-
4
def project_owner?
-
84
record.project.user_id == user&.id
-
end
-
end
-
# frozen_string_literal: true
-
-
# Class for Notification policies
-
2
class NotificationPolicy < ApplicationPolicy
-
2
default_rule :manage?
-
-
2
def manage?
-
7
owner?
-
end
-
end
-
# frozen_string_literal: true
-
-
# Class for Project policies
-
4
class ProjectPolicy < ApplicationPolicy
-
4
alias_rule :update?, to: :manage?
-
-
4
relation_scope(:own) do |scope|
-
22
scope.where(user: user)
-
end
-
-
4
relation_scope(:joined) do |scope|
-
2
scope.where(id: user.teams.pluck(:project_id)).where.not(user: user)
-
end
-
-
4
relation_scope(:explore) do |scope|
-
6
next scope if user&.admin
-
-
5
scope.where(visibility: true).or(scope.where(user: user)).distinct
-
end
-
-
4
relation_scope(:profile_owned) do |scope, profile_owner: nil|
-
26
data = scope.joins(teams: :collaborations).where(user: profile_owner).distinct
-
26
next data if user&.admin || user&.id == profile_owner.id
-
-
4
data.where("visibility = 't' OR collaborations.user_id = ?", user&.id)
-
end
-
-
4
relation_scope(:profile_joined) do |scope, profile_owner: nil|
-
7
data = scope.joins(teams: :collaborations)
-
.where(id: profile_owner.teams.pluck(:project_id))
-
.where.not(user: profile_owner).distinct
-
-
7
next data if user&.admin || user&.id == profile_owner.id
-
-
2
data.where("visibility = 't' OR collaborations.user_id = ?", user&.id)
-
end
-
-
4
def show?
-
75
record.visibility || manage? || user&.in_project?(record) ||
-
record.appeals.invitation.where(user: user).exists?
-
end
-
-
4
def manage?
-
251
(owner? || user&.admin?) && !record.completed?
-
end
-
-
4
def change_status?
-
12
owner? || user&.admin?
-
end
-
-
4
def count?
-
37
user&.in_project?(record) || user&.admin?
-
end
-
-
4
def change_visibility?
-
94
user&.admin? || (!user.license.free? && (owner? || record.new_record?))
-
end
-
-
4
def create_task?
-
79
manage? || (!record.unassigned_team.users.include?(user) &&
-
9
record.teams.any? { |team| team.users.include?(user) })
-
end
-
end
-
# frozen_string_literal: true
-
-
# Class for Task policies
-
3
class TaskPolicy < ApplicationPolicy
-
3
scope_matcher :user, User
-
-
3
default_rule :manage?
-
-
3
relation_scope(:assigned) do |scope|
-
13
ids = scope.left_outer_joins(:task_users).where(task_users: { user_id: user&.id }).pluck('task_users.task_id').uniq
-
13
scope.where(id: ids)
-
end
-
-
3
relation_scope(:unassigned) do |scope|
-
5
scope.left_outer_joins(:task_users).where(task_users: { user_id: nil })
-
end
-
-
3
relation_scope(:active) do |scope|
-
10
scope.where('percentage != 100')
-
end
-
-
3
relation_scope(:completed) do |scope|
-
3
scope.where('percentage = 100')
-
end
-
-
3
def manage?
-
49
owner? || user&.admin? || project_owner?
-
end
-
-
3
def set_percentage?
-
15
manage? || record.users.include?(user)
-
end
-
-
3
def assign_self?
-
6
project_owner? || record.project.users.include?(user)
-
end
-
end
-
# frozen_string_literal: true
-
-
# Class for Team policies
-
3
class TeamPolicy < ApplicationPolicy
-
3
alias_rule :edit?, :update?, to: :access_team?
-
3
default_rule :manage?
-
-
3
def show?
-
12
user&.admin? || record.project.visibility? || user&.in_project?(record.project)
-
end
-
-
3
def access_team?
-
19
record.name != 'Unassigned' && manage?
-
end
-
-
3
def manage?
-
31
user&.admin? || project_owner?
-
end
-
end
-
# frozen_string_literal: true
-
-
# Class for User policies
-
3
class UserPolicy < ApplicationPolicy
-
3
relation_scope(:assignee) do |scope, team_id: nil, project: nil|
-
45
next scope.map { |s| [s.name, s.id] } if user&.admin? || project.user_id == user&.id
-
-
9
scope.includes(:teams).where(teams: { id: team_id }).map { |s| [s.name, s.id] }
-
end
-
end
-
# frozen_string_literal: true
-
-
3
class NotificationBlueprint < Blueprinter::Base
-
3
extend ActionView::Helpers::DateHelper
-
-
3
fields :id, :key, :read
-
-
3
field :time_ago do |notification|
-
9
"#{time_ago_in_words(notification.created_at)} ago"
-
end
-
-
3
association :notifiable, blueprint: lambda { |notifiable|
-
9
case notifiable.class.name
-
when 'User'
-
8
UserBlueprint
-
when 'Project'
-
1
ProjectBlueprint
-
end
-
}, view: :notifiable
-
-
3
association :notifier, blueprint: lambda { |notifier|
-
9
case notifier.class.name
-
when 'Project'
-
7
ProjectBlueprint
-
when 'Team', 'Task'
-
2
ProjectObjectBlueprint
-
end
-
}, view: :notifier
-
end
-
# frozen_string_literal: true
-
-
1
class ProjectBlueprint < Blueprinter::Base
-
1
extend ActionView::Context
-
1
extend ActionView::Helpers::TagHelper
-
1
extend ActionView::Helpers::AssetTagHelper
-
1
extend AvatarHelper
-
-
1
field :name
-
-
1
view :notifiable do
-
1
field :icon do |project|
-
3
"<div class='project-avatar'>#{project_icon(project)}</div>"
-
end
-
end
-
-
1
view :notifier do
-
1
field :link do |project|
-
8
"/projects/#{project.id}"
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
1
class ProjectObjectBlueprint < Blueprinter::Base
-
1
field :name
-
-
1
view :notifier do
-
5
field(:link) { |object| "/projects/#{object.project.id}" }
-
end
-
end
-
# frozen_string_literal: true
-
-
3
class TaskBlueprint < Blueprinter::Base
-
3
extend AvatarHelper
-
-
3
fields :id, :description, :name, :percentage, :priority
-
-
3
field :owner_id do |task|
-
28
task.user.id
-
end
-
-
3
field :owner_name do |task|
-
28
task.user.name
-
end
-
-
3
field :skills do |task|
-
28
task.skills.pluck(:name)
-
end
-
-
3
association :users, name: :assignees, view: :assignee, blueprint: UserBlueprint
-
end
-
# frozen_string_literal: true
-
-
3
class UserBlueprint < Blueprinter::Base
-
3
extend AvatarHelper
-
-
3
field :name
-
-
3
view :notifiable do
-
3
field :icon do |user|
-
"<figure class='image is-48x48 user-avatar-container mr-4'>"\
-
10
"<img src=#{user_avatar(user)}>"\
-
'</figure>'
-
end
-
end
-
-
3
view :assignee do
-
3
field :id
-
-
3
field :icon do |user|
-
7
user_avatar(user)
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
3
class CompatibilityCompute < ComputeService
-
3
def initialize(teams, unassigned)
-
16
@teams = teams
-
16
@unassigned = unassigned
-
end
-
-
3
def call(target)
-
22
if @unassigned.id == target['team_id']
-
20
best_team?(target, @teams)
-
else
-
2
team = @teams.where(id: target['team_id']).first
-
2
team_compatibility(target, team, team.users).round(2).to_s
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
3
class ComputeService
-
WEIGHTAGE = {
-
3
"balance": [1.0, 1.0],
-
"cohesion": [1.5, 0.5],
-
"efficient": [0.5, 1.5]
-
}.freeze
-
-
3
def best_team?(target, teams, u_list = nil)
-
30
user = u_list.blank? ? target : User.find(target['id'])
-
30
return '' if user.empty_compatibility_data?
-
-
28
results = compare_team(user, teams, u_list)
-
-
54
best = results.max_by { |x| x[1] }
-
28
best.blank? || best[1] <= 1.0 ? '' : "(#{best[1].round(2)}) Team #{best[0]}"
-
end
-
-
3
def compare_team(target, teams, u_list = nil, ignore_empty = false, weightage = [1.0, 1.0])
-
45
results = {}
-
45
teams.each do |team|
-
71
next if valid_team_compare(team, u_list, ignore_empty)
-
-
39
users = prepare_team_compare(team, u_list)
-
-
39
results[team.name] = team_compatibility(target, team, users, weightage) if users.size < team.team_size
-
end
-
45
results
-
end
-
-
3
def team_compatibility(target, team, users, weightage = [1.0, 1.0])
-
58
1.0 * team_personality_score(target, users, weightage[0]) *
-
teamskill_score(team.team_skills.pluck(:skill_id),
-
target.user_skills.pluck(:skill_id), weightage[1])
-
end
-
-
3
def teamskill_score(t_skill, u_skill, weightage = 1.0)
-
85
return 1.0 if t_skill.empty? || u_skill.empty?
-
-
68
1.0 + (weightage *
-
(t_skill.size - (t_skill - u_skill).size) / (t_skill.size * 5.0))
-
end
-
-
3
def team_personality_score(target, users, weightage = 1.0)
-
62
return 1.0 if target.personality_id.blank?
-
-
44
user_score = 0
-
44
counter = 0
-
44
users.each do |usr|
-
31
next if usr.personality_id.blank? || target == usr
-
-
17
user_score += target.compatible_with?(usr)
-
17
counter += 1.0
-
end
-
44
counter.zero? ? 1.0 : 1.0 + (weightage * (user_score / (counter * 10.0)))
-
end
-
-
3
private
-
-
3
def valid_team_compare(team, u_list, ignore_empty)
-
71
return true if team.name == 'Unassigned'
-
-
39
return false if u_list.nil?
-
-
18
!u_list.key?(team.id.to_s) || (ignore_empty && u_list[team.id.to_s].empty?)
-
end
-
-
3
def prepare_team_compare(team, list)
-
39
return team.users if list.blank?
-
-
18
list[team.id.to_s].empty? ? [] : User.find(list[team.id.to_s].pluck('id'))
-
end
-
end
-
# frozen_string_literal: true
-
-
2
class Recompute < ComputeService
-
2
def initialize(teams, unassigned, mode = 'balance')
-
14
@teams = teams
-
14
@unassigned = unassigned
-
14
@weightage = WEIGHTAGE[mode.to_sym]
-
end
-
-
2
def call(target_team, u_list)
-
16
members = u_list[target_team.id.to_s]
-
-
16
if @unassigned == target_team
-
12
members.map { |u| [u['id'], best_team?(u, @teams, u_list)] }
-
else
-
10
users = User.includes(:user_skills).find(members.pluck('id'))
-
-
10
users.map do |u|
-
13
[u.id, team_compatibility(u, target_team, users, @weightage).round(2).to_s]
-
end
-
end
-
end
-
end
-
# frozen_string_literal: true
-
-
2
class Suggest < ComputeService
-
2
def initialize(users, teams, unassigned, mode = 'balance')
-
17
@users = split_users(users)
-
17
@teams = teams
-
17
@unassigned = unassigned
-
17
@mode = mode
-
17
@weightage = WEIGHTAGE[mode.to_sym]
-
end
-
-
2
def call
-
17
suggestion, leftover = allocate_first_users
-
-
17
is_coh = @mode == 'cohesion'
-
17
[leftover, @users[is_coh ? 2 : 1], @users[is_coh ? 1 : 2]].each do |data|
-
51
suggestion = match(data, suggestion)
-
end
-
-
17
leftover = @users.flatten - suggestion.values.flatten
-
17
suggestion[@unassigned.id.to_s] = leftover
-
17
suggestion
-
end
-
-
2
private
-
-
2
def split_users(users)
-
17
[users.where.not(user_skills: { user_id: nil })
-
.where.not(users: { personality_id: nil }).group('user_skills.id'),
-
users.where.not(user_skills: { user_id: nil })
-
.where(personality_id: nil).group('user_skills.id'),
-
users.where(user_skills: { user_id: nil })
-
.where.not(users: { personality_id: nil })
-
.group('user_skills.id'),
-
users.where(user_skills: { user_id: nil }, users: { personality_id: nil })
-
.group('user_skills.id')]
-
end
-
-
2
def allocate_first_users
-
17
return [@teams.map { |t| [t.id, []] }.to_h, []] if @users[0].blank?
-
-
17
top_mem = first_users_loop
-
-
34
[top_mem.map { |k, v| v.nil? ? [k, []] : [k, [v]] }.to_h, @users[0] - top_mem.values]
-
end
-
-
2
def first_users_loop
-
17
skill_comp = prepare_skill_comp
-
17
top_mem, conflict = get_top_mem(skill_comp)
-
-
17
until conflict.blank?
-
3
winner = find_winner(top_mem, skill_comp, conflict)
-
9
skill_comp.each { |id, u| u.delete(conflict) if id != winner }
-
3
top_mem, conflict = get_top_mem(skill_comp)
-
end
-
17
top_mem
-
end
-
-
2
def prepare_skill_comp
-
17
skill_comp = @teams.map do |t|
-
17
t_s = t.team_skills.pluck(:skill_id)
-
17
data = prepare_skill_comp_loop(t_s)
-
40
[t.id.to_s, data.sort_by { |_, v| -v }.to_h]
-
end
-
17
skill_comp.to_h
-
end
-
-
2
def prepare_skill_comp_loop(t_s)
-
17
@users[0].map do |u|
-
23
[u, teamskill_score(t_s, u.user_skills.pluck(:skill_id)) * @weightage[1]]
-
end
-
end
-
-
2
def get_top_mem(skill_comp)
-
43
top_mem = skill_comp.collect { |k, v| [k, v.first&.[](0)] }.to_h
-
43
conflicts = top_mem.values.select { |e| top_mem.values.count(e) > 1 }.uniq
-
20
[top_mem, conflicts[0]]
-
end
-
-
2
def find_winner(top_mem, skill_comp, contested_user_id)
-
9
conflict_ids = top_mem.select { |_, v| v == contested_user_id }.to_h
-
9
conflict_teams = skill_comp.select { |k, _| conflict_ids.key?(k) }
-
3
resolve(conflict_teams, contested_user_id)
-
end
-
-
# if more than one team has similar top users, resolve by comparing total score
-
# skill_comps that are competing are parsed in
-
# returns the lowest team with lowest skill score
-
2
def resolve(conflict_teams, contested_user_id)
-
3
selected = { id: 0, score: Float::MAX }
-
3
conflict_teams.each do |team_id, users_scores|
-
6
score = users_scores.values.sum - users_scores[contested_user_id]
-
-
6
selected = { id: team_id, score: score } if score < selected[:score]
-
end
-
3
selected[:id]
-
end
-
-
2
def match(data, suggestion)
-
51
data.each do |u|
-
23
compare_team(u, @teams, suggestion, @mode == 'cohesion', @weightage).sort_by { |_, v| -v }.each do |k, v|
-
10
team = @teams.where(name: k).first
-
10
target = suggestion[team.id.to_s]
-
10
next if target.size == team.team_size || v <= 1.0
-
-
3
target.push(u)
-
3
break
-
end
-
end
-
51
suggestion
-
end
-
end
-
# frozen_string_literal: true
-
-
# Validator for inclusion of array values
-
4
class ArrayInclusionValidator < ActiveModel::EachValidator
-
4
def validate_each(record, attribute, value)
-
60
return if value.present? && value.all? { |val| options[:in].include?(val) }
-
-
9
record.errors[attribute] << (options[:message] || 'is not included in the list')
-
end
-
end