loading
Generated 2020-05-26T16:46:14+01:00

All Files ( 99.93% covered at 28.61 hits/line )

85 files in total.
1395 relevant lines, 1394 lines covered and 1 lines missed. ( 99.93% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/controllers/admin/base_controller.rb 100.00 % 13 6 6 0 36.33
app/controllers/admin/categories_controller.rb 100.00 % 39 19 19 0 2.84
app/controllers/admin/charts_controller.rb 100.00 % 156 62 62 0 5.63
app/controllers/admin/concerns/charts_filter_date.rb 100.00 % 54 25 25 0 13.64
app/controllers/admin/dashboard_controller.rb 100.00 % 19 3 3 0 2.00
app/controllers/admin/metrics_controller.rb 100.00 % 24 7 7 0 4.00
app/controllers/admin/newsletters_controller.rb 100.00 % 71 34 34 0 3.88
app/controllers/admin/skills_controller.rb 100.00 % 40 20 20 0 3.70
app/controllers/application_controller.rb 100.00 % 51 22 22 0 99.95
app/controllers/categories_controller.rb 100.00 % 11 5 5 0 3.00
app/controllers/errors_controller.rb 100.00 % 24 11 11 0 2.36
app/controllers/notifications_controller.rb 100.00 % 40 21 21 0 2.00
app/controllers/pages_controller.rb 100.00 % 60 28 28 0 6.79
app/controllers/projects/appeals/applications_controller.rb 100.00 % 53 29 29 0 2.34
app/controllers/projects/appeals/base_controller.rb 100.00 % 7 3 3 0 5.00
app/controllers/projects/appeals/invitations_controller.rb 100.00 % 54 28 28 0 3.96
app/controllers/projects/tasks_controller.rb 100.00 % 120 56 56 0 11.27
app/controllers/projects/teams/base_controller.rb 100.00 % 31 17 17 0 17.00
app/controllers/projects/teams/manage_controller.rb 100.00 % 99 55 55 0 3.87
app/controllers/projects/teams/teams_controller.rb 100.00 % 58 28 28 0 3.68
app/controllers/projects_controller.rb 100.00 % 95 46 46 0 10.76
app/controllers/skills_controller.rb 100.00 % 13 5 5 0 3.00
app/controllers/users/omniauth_callbacks_controller.rb 100.00 % 72 27 27 0 6.15
app/controllers/users/passwords_controller.rb 100.00 % 59 20 20 0 2.20
app/controllers/users/registrations_controller.rb 100.00 % 106 30 30 0 4.03
app/controllers/users/sessions_controller.rb 100.00 % 37 9 9 0 2.89
app/controllers/users/settings/accounts_controller.rb 100.00 % 32 17 17 0 4.59
app/controllers/users/settings/base_controller.rb 100.00 % 5 2 2 0 2.00
app/controllers/users/settings/billings_controller.rb 100.00 % 16 8 8 0 1.50
app/controllers/users/settings/emails_controller.rb 100.00 % 21 11 11 0 2.36
app/controllers/users/settings/personalities_controller.rb 100.00 % 23 11 11 0 13.82
app/controllers/users/settings/profiles_controller.rb 100.00 % 29 14 14 0 2.86
app/controllers/users/settings/skills_controller.rb 100.00 % 32 16 16 0 3.06
app/controllers/users_controller.rb 100.00 % 82 34 34 0 11.68
app/helpers/application_helper.rb 100.00 % 18 8 8 0 325.88
app/helpers/avatar_helper.rb 100.00 % 43 22 22 0 65.45
app/helpers/devise_helper.rb 100.00 % 27 6 6 0 61.17
app/helpers/nav_helper.rb 100.00 % 43 20 20 0 766.80
app/mailers/application_mailer.rb 100.00 % 6 3 3 0 4.00
app/mailers/newsletter_mailer.rb 100.00 % 20 6 6 0 3.83
app/models/activity.rb 100.00 % 31 5 5 0 5.00
app/models/ahoy/event.rb 100.00 % 34 9 9 0 6.67
app/models/ahoy/visit.rb 100.00 % 47 5 5 0 6.40
app/models/appeal.rb 100.00 % 93 31 31 0 13.81
app/models/application_record.rb 100.00 % 5 2 2 0 4.00
app/models/category.rb 100.00 % 22 4 4 0 4.00
app/models/collaboration.rb 100.00 % 31 5 5 0 4.00
app/models/identity.rb 100.00 % 38 5 5 0 3.00
app/models/landing_feedback.rb 100.00 % 26 5 5 0 4.00
app/models/license.rb 100.00 % 35 8 8 0 97.00
app/models/newsletter.rb 100.00 % 39 11 11 0 6.55
app/models/newsletter_feedback.rb 100.00 % 60 15 15 0 5.93
app/models/newsletter_subscription.rb 100.00 % 49 16 16 0 20.81
app/models/notification.rb 100.00 % 37 6 6 0 103.50
app/models/personality.rb 100.00 % 55 11 11 0 9.73
app/models/project.rb 100.00 % 126 48 48 0 43.08
app/models/skill.rb 100.00 % 41 11 11 0 5.73
app/models/task.rb 100.00 % 97 36 36 0 6.00
app/models/task_skill.rb 100.00 % 30 5 5 0 4.00
app/models/task_user.rb 100.00 % 30 5 5 0 4.00
app/models/team.rb 95.65 % 74 23 22 1 63.13
app/models/team_skill.rb 100.00 % 30 5 5 0 4.00
app/models/user.rb 100.00 % 151 55 55 0 44.87
app/models/user_skill.rb 100.00 % 30 5 5 0 3.00
app/policies/activity_policy.rb 100.00 % 23 6 6 0 4.33
app/policies/admin_policy.rb 100.00 % 11 5 5 0 44.40
app/policies/appeal/application_policy.rb 100.00 % 11 5 5 0 5.00
app/policies/appeal/base_policy.rb 100.00 % 12 5 5 0 7.00
app/policies/appeal/invitation_policy.rb 100.00 % 11 5 5 0 5.20
app/policies/application_policy.rb 100.00 % 14 6 6 0 80.50
app/policies/notification_policy.rb 100.00 % 10 4 4 0 3.25
app/policies/project_policy.rb 100.00 % 63 30 30 0 23.87
app/policies/task_policy.rb 100.00 % 37 18 18 0 8.00
app/policies/team_policy.rb 100.00 % 19 9 9 0 8.89
app/policies/user_policy.rb 100.00 % 10 4 4 0 15.00
app/serializers/notification_blueprint.rb 100.00 % 29 13 13 0 4.85
app/serializers/project_blueprint.rb 100.00 % 22 12 12 0 1.75
app/serializers/project_object_blueprint.rb 100.00 % 9 4 4 0 2.00
app/serializers/task_blueprint.rb 100.00 % 21 10 10 0 10.50
app/serializers/user_blueprint.rb 100.00 % 23 10 10 0 4.10
app/services/compatibility_compute.rb 100.00 % 17 9 9 0 9.67
app/services/compute_service.rb 100.00 % 74 37 37 0 31.97
app/services/recompute.rb 100.00 % 23 12 12 0 10.42
app/services/suggest.rb 100.00 % 112 62 62 0 14.34
app/validators/array_inclusion_validator.rb 100.00 % 10 4 4 0 19.25

Controllers ( 100.0% covered at 9.23 hits/line )

34 files in total.
729 relevant lines, 729 lines covered and 0 lines missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/controllers/admin/base_controller.rb 100.00 % 13 6 6 0 36.33
app/controllers/admin/categories_controller.rb 100.00 % 39 19 19 0 2.84
app/controllers/admin/charts_controller.rb 100.00 % 156 62 62 0 5.63
app/controllers/admin/concerns/charts_filter_date.rb 100.00 % 54 25 25 0 13.64
app/controllers/admin/dashboard_controller.rb 100.00 % 19 3 3 0 2.00
app/controllers/admin/metrics_controller.rb 100.00 % 24 7 7 0 4.00
app/controllers/admin/newsletters_controller.rb 100.00 % 71 34 34 0 3.88
app/controllers/admin/skills_controller.rb 100.00 % 40 20 20 0 3.70
app/controllers/application_controller.rb 100.00 % 51 22 22 0 99.95
app/controllers/categories_controller.rb 100.00 % 11 5 5 0 3.00
app/controllers/errors_controller.rb 100.00 % 24 11 11 0 2.36
app/controllers/notifications_controller.rb 100.00 % 40 21 21 0 2.00
app/controllers/pages_controller.rb 100.00 % 60 28 28 0 6.79
app/controllers/projects/appeals/applications_controller.rb 100.00 % 53 29 29 0 2.34
app/controllers/projects/appeals/base_controller.rb 100.00 % 7 3 3 0 5.00
app/controllers/projects/appeals/invitations_controller.rb 100.00 % 54 28 28 0 3.96
app/controllers/projects/tasks_controller.rb 100.00 % 120 56 56 0 11.27
app/controllers/projects/teams/base_controller.rb 100.00 % 31 17 17 0 17.00
app/controllers/projects/teams/manage_controller.rb 100.00 % 99 55 55 0 3.87
app/controllers/projects/teams/teams_controller.rb 100.00 % 58 28 28 0 3.68
app/controllers/projects_controller.rb 100.00 % 95 46 46 0 10.76
app/controllers/skills_controller.rb 100.00 % 13 5 5 0 3.00
app/controllers/users/omniauth_callbacks_controller.rb 100.00 % 72 27 27 0 6.15
app/controllers/users/passwords_controller.rb 100.00 % 59 20 20 0 2.20
app/controllers/users/registrations_controller.rb 100.00 % 106 30 30 0 4.03
app/controllers/users/sessions_controller.rb 100.00 % 37 9 9 0 2.89
app/controllers/users/settings/accounts_controller.rb 100.00 % 32 17 17 0 4.59
app/controllers/users/settings/base_controller.rb 100.00 % 5 2 2 0 2.00
app/controllers/users/settings/billings_controller.rb 100.00 % 16 8 8 0 1.50
app/controllers/users/settings/emails_controller.rb 100.00 % 21 11 11 0 2.36
app/controllers/users/settings/personalities_controller.rb 100.00 % 23 11 11 0 13.82
app/controllers/users/settings/profiles_controller.rb 100.00 % 29 14 14 0 2.86
app/controllers/users/settings/skills_controller.rb 100.00 % 32 16 16 0 3.06
app/controllers/users_controller.rb 100.00 % 82 34 34 0 11.68

Models ( 99.7% covered at 27.08 hits/line )

24 files in total.
331 relevant lines, 330 lines covered and 1 lines missed. ( 99.7% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/models/activity.rb 100.00 % 31 5 5 0 5.00
app/models/ahoy/event.rb 100.00 % 34 9 9 0 6.67
app/models/ahoy/visit.rb 100.00 % 47 5 5 0 6.40
app/models/appeal.rb 100.00 % 93 31 31 0 13.81
app/models/application_record.rb 100.00 % 5 2 2 0 4.00
app/models/category.rb 100.00 % 22 4 4 0 4.00
app/models/collaboration.rb 100.00 % 31 5 5 0 4.00
app/models/identity.rb 100.00 % 38 5 5 0 3.00
app/models/landing_feedback.rb 100.00 % 26 5 5 0 4.00
app/models/license.rb 100.00 % 35 8 8 0 97.00
app/models/newsletter.rb 100.00 % 39 11 11 0 6.55
app/models/newsletter_feedback.rb 100.00 % 60 15 15 0 5.93
app/models/newsletter_subscription.rb 100.00 % 49 16 16 0 20.81
app/models/notification.rb 100.00 % 37 6 6 0 103.50
app/models/personality.rb 100.00 % 55 11 11 0 9.73
app/models/project.rb 100.00 % 126 48 48 0 43.08
app/models/skill.rb 100.00 % 41 11 11 0 5.73
app/models/task.rb 100.00 % 97 36 36 0 6.00
app/models/task_skill.rb 100.00 % 30 5 5 0 4.00
app/models/task_user.rb 100.00 % 30 5 5 0 4.00
app/models/team.rb 95.65 % 74 23 22 1 63.13
app/models/team_skill.rb 100.00 % 30 5 5 0 4.00
app/models/user.rb 100.00 % 151 55 55 0 44.87
app/models/user_skill.rb 100.00 % 30 5 5 0 3.00

Mailers ( 100.0% covered at 3.89 hits/line )

2 files in total.
9 relevant lines, 9 lines covered and 0 lines missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/mailers/application_mailer.rb 100.00 % 6 3 3 0 4.00
app/mailers/newsletter_mailer.rb 100.00 % 20 6 6 0 3.83

Helpers ( 100.0% covered at 352.68 hits/line )

4 files in total.
56 relevant lines, 56 lines covered and 0 lines missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/helpers/application_helper.rb 100.00 % 18 8 8 0 325.88
app/helpers/avatar_helper.rb 100.00 % 43 22 22 0 65.45
app/helpers/devise_helper.rb 100.00 % 27 6 6 0 61.17
app/helpers/nav_helper.rb 100.00 % 43 20 20 0 766.80

Policies ( 100.0% covered at 18.87 hits/line )

11 files in total.
97 relevant lines, 97 lines covered and 0 lines missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/policies/activity_policy.rb 100.00 % 23 6 6 0 4.33
app/policies/admin_policy.rb 100.00 % 11 5 5 0 44.40
app/policies/appeal/application_policy.rb 100.00 % 11 5 5 0 5.00
app/policies/appeal/base_policy.rb 100.00 % 12 5 5 0 7.00
app/policies/appeal/invitation_policy.rb 100.00 % 11 5 5 0 5.20
app/policies/application_policy.rb 100.00 % 14 6 6 0 80.50
app/policies/notification_policy.rb 100.00 % 10 4 4 0 3.25
app/policies/project_policy.rb 100.00 % 63 30 30 0 23.87
app/policies/task_policy.rb 100.00 % 37 18 18 0 8.00
app/policies/team_policy.rb 100.00 % 19 9 9 0 8.89
app/policies/user_policy.rb 100.00 % 10 4 4 0 15.00

Serializers ( 100.0% covered at 4.86 hits/line )

5 files in total.
49 relevant lines, 49 lines covered and 0 lines missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/serializers/notification_blueprint.rb 100.00 % 29 13 13 0 4.85
app/serializers/project_blueprint.rb 100.00 % 22 12 12 0 1.75
app/serializers/project_object_blueprint.rb 100.00 % 9 4 4 0 2.00
app/serializers/task_blueprint.rb 100.00 % 21 10 10 0 10.50
app/serializers/user_blueprint.rb 100.00 % 23 10 10 0 4.10

Validators ( 100.0% covered at 19.25 hits/line )

1 files in total.
4 relevant lines, 4 lines covered and 0 lines missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/validators/array_inclusion_validator.rb 100.00 % 10 4 4 0 19.25

Ungrouped ( 100.0% covered at 19.03 hits/line )

4 files in total.
120 relevant lines, 120 lines covered and 0 lines missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app/services/compatibility_compute.rb 100.00 % 17 9 9 0 9.67
app/services/compute_service.rb 100.00 % 74 37 37 0 31.97
app/services/recompute.rb 100.00 % 23 12 12 0 10.42
app/services/suggest.rb 100.00 % 112 62 62 0 14.34

app/controllers/admin/base_controller.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Admin::BaseController < ApplicationController
  3. 2 before_action :admin_authorize
  4. 2 layout 'admin'
  5. 2 private
  6. 2 def admin_authorize
  7. 208 authorize! current_user, with: AdminPolicy
  8. end
  9. end

app/controllers/admin/categories_controller.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Admin::CategoriesController < Admin::BaseController
  3. 2 before_action :set_category, only: %i[update destroy]
  4. 2 def index
  5. 7 @categories = Category.select(:id, :name).order(:name)
  6. end
  7. 2 def create
  8. 3 category = Category.new(category_params)
  9. 3 if category.save
  10. 2 render json: { category: { id: category.id, name: category.name } }
  11. else
  12. 1 render json: { message: category.errors.full_messages }, status: :unprocessable_entity
  13. end
  14. end
  15. 2 def update
  16. 4 return head :ok if @category.update(category_params)
  17. 1 render json: { message: @category.errors.full_messages }, status: :unprocessable_entity
  18. end
  19. 2 def destroy
  20. 2 @category.destroy ? head(:ok) : head(:bad_request)
  21. end
  22. 2 private
  23. 2 def category_params
  24. 7 params.require(:category).permit(:name)
  25. end
  26. 2 def set_category
  27. 6 @category = Category.find(params[:id])
  28. end
  29. end

app/controllers/admin/charts_controller.rb

100.0% lines covered

62 relevant lines. 62 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Admin::ChartsController < Admin::BaseController
  3. 2 include Admin::Concerns::ChartsFilterDate
  4. 2 before_action :load_plan_subscriptions, only: %i[subscription_ratio subscription_by_date]
  5. 2 before_action :load_landing_page_feedback, only: :landing_page_feedback
  6. 2 before_action :load_ahoy_event_social, only: %i[social_share_ratio social_share_by_date]
  7. 2 before_action :load_ahoy_visit_referrer, only: %i[referrers_ratio referrers_by_date]
  8. 2 before_action :load_ahoy_event_time_spent, only: :average_time_spent_per_page
  9. 2 before_action :load_ahoy_visit_per_page, only: :number_of_visits_per_page
  10. 2 before_action :load_newsletter_subscription_by_date, only: :newsletter_subscription_by_date
  11. 2 before_action :load_unsubscription_by_newsletter, only: :unsubscription_by_newsletter
  12. 2 before_action :load_unsubscription_reason, only: :unsubscription_reason
  13. # These charts are filtered by 'time' column
  14. 2 before_action :filter_date_by_time_column,
  15. only: %i[
  16. social_share_ratio
  17. social_share_by_date
  18. average_time_spent_per_page
  19. number_of_visits_per_page
  20. ]
  21. # These charts are filtered by 'created_at' column
  22. 2 before_action :filter_date_by_created_at,
  23. only: %i[
  24. landing_page_feedback
  25. newsletter_subscription_by_date
  26. unsubscription_by_newsletter
  27. unsubscription_reason
  28. ]
  29. 2 before_action :filter_date_by_updated_at, only: %i[subscription_ratio subscription_by_date]
  30. # These charts are filtered by 'started_at' column
  31. 2 before_action :filter_date_by_started_at, only: %i[referrers_ratio referrers_by_date]
  32. 2 def subscription_ratio
  33. 6 render json: @records.size
  34. end
  35. 2 def subscription_by_date
  36. 3 render json: @records.group_by_day(:updated_at).size.chart_json
  37. end
  38. 2 def landing_page_feedback
  39. 4 render json: { data: [
  40. @records[0].group(:smiley).size,
  41. @records[1].group(:channel).size,
  42. @records[2].group(:interest).size
  43. ] }
  44. end
  45. 2 def social_share_ratio
  46. 2 render json: @records.type_size
  47. end
  48. 2 def social_share_by_date
  49. 2 render json: @records.type_time_size.chart_json
  50. end
  51. 2 def referrers_ratio
  52. 12 render json: @records.size
  53. end
  54. 2 def referrers_by_date
  55. 4 render json: @records.group_by_day(:started_at).size.chart_json
  56. end
  57. 2 def average_time_spent_per_page
  58. 4 render json: @records
  59. .group("properties ->> 'pathname'")
  60. .average("cast(properties ->> 'time_spent' as integer)")
  61. end
  62. 2 def number_of_visits_per_page
  63. 4 render json: @records.group("properties ->> 'action'").size
  64. end
  65. 2 def newsletter_subscription_by_date
  66. 8 render json: [
  67. { name: 'Subscription', data: @records[0].group_by_day(:created_at).size },
  68. { name: 'Unsubscription', data: @records[1].group_by_day(:created_at).size }
  69. ]
  70. end
  71. 2 def unsubscription_by_newsletter
  72. 5 @records.map! do |record|
  73. 3 ["#{record['title']}, #{record['created_at'].utc.strftime('%d-%m-%Y')}", record['feedback_count']]
  74. end
  75. 5 render json: @records
  76. end
  77. 2 def unsubscription_reason
  78. 4 render json: NewsletterFeedback.count_reason(@records)
  79. end
  80. 2 private
  81. 2 def chart_params
  82. 125 params.require(:chart).permit(:date)
  83. end
  84. 2 def load_plan_subscriptions
  85. 13 @records = License.group(:plan)
  86. end
  87. 2 def load_ahoy_event_social
  88. 8 @records = Ahoy::Event.social
  89. end
  90. 2 def load_landing_page_feedback
  91. @records = [
  92. 6 LandingFeedback.select(:smiley, :created_at),
  93. LandingFeedback.select(:channel, :created_at),
  94. LandingFeedback.select(:interest, :created_at)
  95. ]
  96. end
  97. 2 def load_ahoy_visit_referrer
  98. 20 @records = Ahoy::Visit.group(:referrer)
  99. end
  100. 2 def load_ahoy_event_time_spent
  101. 6 @records = Ahoy::Event.where(name: 'Time Spent')
  102. end
  103. 2 def load_ahoy_visit_per_page
  104. 6 @records = Ahoy::Event.action
  105. end
  106. 2 def load_newsletter_subscription_by_date
  107. @records = [
  108. 10 NewsletterSubscription.select(:created_at),
  109. NewsletterFeedback.select(:created_at)
  110. ]
  111. end
  112. 2 def load_unsubscription_by_newsletter
  113. 7 @records = Newsletter.unsubscription_by_newsletter
  114. end
  115. 2 def load_unsubscription_reason
  116. 6 @records = NewsletterFeedback.select(:reasons)
  117. end
  118. end

app/controllers/admin/concerns/charts_filter_date.rb

100.0% lines covered

25 relevant lines. 25 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 module Admin::Concerns::ChartsFilterDate
  3. 2 extend ActiveSupport::Concern
  4. 2 def filter_date_by_time_column
  5. 20 filter_date('time')
  6. end
  7. 2 def filter_date_by_created_at
  8. 29 filter_date('created_at')
  9. end
  10. 2 def filter_date_by_updated_at
  11. 13 filter_date('updated_at')
  12. end
  13. 2 def filter_date_by_started_at
  14. 20 filter_date('started_at')
  15. end
  16. 2 private
  17. 2 def date_params
  18. 43 chart_params[:date].split(',')
  19. end
  20. 2 def start_date
  21. 27 Time.zone.parse(date_params[0]).beginning_of_day
  22. rescue StandardError
  23. 11 raise ActionController::BadRequest
  24. end
  25. 2 def end_date
  26. 16 Time.zone.parse(date_params[1]).end_of_day
  27. rescue StandardError
  28. 1 raise ActionController::BadRequest
  29. end
  30. 2 def filter_date(column)
  31. 82 return if chart_params[:date].blank?
  32. 24 case action_name
  33. when 'landing_page_feedback', 'newsletter_subscription_by_date'
  34. # Array of ActiveRecord::Relation objects
  35. 11 @records.map! { |record| record.where(column + ' BETWEEN ? AND ?', start_date, end_date) }
  36. when 'unsubscription_by_newsletter'
  37. # Class of Newsletter, cannot use where
  38. 4 @records.filter! { |record| record[:created_at].between?(start_date, end_date) }
  39. else
  40. 18 @records = @records.where(column + ' BETWEEN ? AND ?', start_date, end_date)
  41. end
  42. end
  43. end

app/controllers/admin/dashboard_controller.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Admin::DashboardController < Admin::BaseController
  3. # rubocop:disable Metrics/MethodLength
  4. 2 def index
  5. 2 @counts = Project.pluck(
  6. Arel.sql('COUNT(1),'\
  7. '(SELECT COUNT(1) FROM projects WHERE visibility = true),'\
  8. '(SELECT COUNT(1) FROM projects WHERE visibility = false),'\
  9. '(SELECT COUNT(1) FROM users),'\
  10. "(SELECT COUNT(1) FROM licenses WHERE plan = 'free'),"\
  11. "(SELECT COUNT(1) FROM licenses WHERE plan = 'pro'),"\
  12. "(SELECT COUNT(1) FROM licenses WHERE plan = 'ultimate'),"\
  13. '(SELECT COUNT(1) FROM categories),'\
  14. '(SELECT COUNT(1) FROM skills)')
  15. ).first
  16. end
  17. # rubocop:enable Metrics/MethodLength
  18. end

app/controllers/admin/metrics_controller.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Controller for metrics
  3. 2 class Admin::MetricsController < Admin::BaseController
  4. 2 before_action :admin_authorize
  5. 2 layout 'metrics_page'
  6. 2 def index
  7. @data = {
  8. 6 subscribed_count: NewsletterSubscription.subscribed_count,
  9. today_count: Ahoy::Visit.today_count,
  10. total_count: Ahoy::Visit.count
  11. }.freeze
  12. end
  13. 2 def traffic
  14. @data = {
  15. 12 device_type: Ahoy::Visit.group(:device_type).size.chart_json,
  16. browser: Ahoy::Visit.group(:browser).size.chart_json,
  17. country: Ahoy::Visit.group(:country).size.chart_json
  18. }.freeze
  19. end
  20. end

app/controllers/admin/newsletters_controller.rb

100.0% lines covered

34 relevant lines. 34 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Controller for newsletter
  3. 2 class Admin::NewslettersController < Admin::BaseController
  4. 2 skip_before_action :authenticate_user!, only: %i[subscribe unsubscribe post_unsubscribe]
  5. 2 skip_before_action :admin_authorize, only: %i[subscribe unsubscribe post_unsubscribe]
  6. 2 layout 'metrics_page', except: :unsubscribe
  7. 2 def index
  8. 4 @newsletters = Newsletter.select(:id, :title, :created_at)
  9. end
  10. 2 def create
  11. 5 newsletter = Newsletter.new(newsletter_params)
  12. 4 if newsletter.save
  13. 2 flash[:toast_success] = 'Newsletter sent'
  14. 2 render js: "window.location = '#{newsletter_path(newsletter)}'"
  15. else
  16. 2 render json: { message: 'Send Failed' }, status: :unprocessable_entity
  17. end
  18. end
  19. 2 def show
  20. 8 @newsletter = Newsletter.find(params[:id])
  21. 6 return unless request.xhr?
  22. 2 render json: { html: view_to_html_string('admin/newsletters/_modal') }
  23. end
  24. 2 def subscribers
  25. 10 @subscribers = NewsletterSubscription.where(subscribed: true)
  26. end
  27. 2 def subscribe
  28. 7 if NewsletterSubscription.subscribe(params[:email])
  29. 4 render json: { message: 'Thanks for subscribing' }
  30. else
  31. 3 render json: { message: 'Subscription Failed' }, status: :unprocessable_entity
  32. end
  33. end
  34. 2 def unsubscribe; end
  35. 2 def post_unsubscribe
  36. 8 feedback = NewsletterFeedback.create(unsubscribe_params)
  37. 8 if feedback.errors.any?
  38. 2 render json: { message: feedback.errors.full_messages }, status: :unprocessable_entity
  39. else
  40. # Prevent Oracle attack on newsletter subscribers list by returning as
  41. # long as the params syntax are correct
  42. 6 render json: { message: 'Newsletter Unsubscribed! Hope to see you again' }
  43. end
  44. end
  45. 2 private
  46. 2 def newsletter_params
  47. 5 params.require(:newsletter).permit(:title, :content)
  48. end
  49. 2 def unsubscribe_params
  50. 8 p = params.require(:newsletter_unsubscription).permit(:email, reasons: [])
  51. 8 { newsletter_subscription: NewsletterSubscription.find_by(email: p[:email]), reasons: p[:reasons] }
  52. end
  53. end

app/controllers/admin/skills_controller.rb

100.0% lines covered

20 relevant lines. 20 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Admin::SkillsController < Admin::BaseController
  3. 2 before_action :set_skill, only: %i[update destroy]
  4. 2 def index
  5. 10 @skills = Skill.with_category
  6. 10 @categories = Category.select(:id, :name)
  7. end
  8. 2 def create
  9. 4 skill = Skill.new(skill_params)
  10. 4 if skill.save
  11. 2 render json: { skill: { id: skill.id, name: skill.name, category_name: skill.category.name } }
  12. else
  13. 2 render json: { message: skill.errors.full_messages }, status: :unprocessable_entity
  14. end
  15. end
  16. 2 def update
  17. 5 return head :ok if @skill.update(skill_params)
  18. 1 render json: { message: @skill.errors.full_messages }, status: :unprocessable_entity
  19. end
  20. 2 def destroy
  21. 2 @skill.destroy ? head(:ok) : head(:bad_request)
  22. end
  23. 2 private
  24. 2 def skill_params
  25. 9 params.require(:skill).permit(:name, :category_id)
  26. end
  27. 2 def set_skill
  28. 7 @skill = Skill.find(params[:id])
  29. end
  30. end

app/controllers/application_controller.rb

100.0% lines covered

22 relevant lines. 22 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Default application controller
  3. 4 class ApplicationController < ActionController::Base
  4. 4 include Pagy::Backend
  5. # Ahoy gem, used in PagesController only
  6. 4 skip_before_action :track_ahoy_visit
  7. 4 before_action :authenticate_user!
  8. 904 before_action :unread_notification_count, if: proc { !request.xhr? && user_signed_in? }
  9. 4 protect_from_forgery with: :exception, prepend: true
  10. 4 rescue_from ActionController::ParameterMissing do
  11. 20 head :bad_request
  12. end
  13. # 1. RecordNotFound exception is raised when using *find* method
  14. # 2. Exception object contains the following information
  15. # ex.policy #=> policy class, e.g. UserPolicy
  16. # ex.rule #=> applied rule, e.g. :show?
  17. # 3. Pagy VariableError or OverflowError
  18. 4 rescue_from ActiveRecord::RecordNotFound,
  19. ActionPolicy::Unauthorized,
  20. Pagy::VariableError do
  21. 92 render_404
  22. end
  23. 4 rescue_from ActiveRecord::StatementInvalid do
  24. 1 render json: { message: 'Invalid Statement' }, status: :unprocessable_entity
  25. end
  26. 4 def view_to_html_string(partial, locals = {})
  27. 55 render_to_string(partial, locals: locals, layout: false, formats: [:html])
  28. end
  29. 4 private
  30. 4 def unread_notification_count
  31. 594 @unread_count = current_user.notifications.unread.size
  32. end
  33. 4 def render_404(msg = 'Not found')
  34. 97 respond_to do |format|
  35. 193 format.html { render 'errors/error_404', layout: 'errors', status: 404 }
  36. 98 format.json { render json: { message: msg }, status: :not_found }
  37. 97 format.any { head :not_found }
  38. end
  39. end
  40. end

app/controllers/categories_controller.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class CategoriesController < ApplicationController
  3. 2 skip_before_action :authenticate_user!
  4. 2 def index
  5. 5 return render_404 unless request.xhr?
  6. 4 render json: { categories: Category.order(:name).pluck(:name) }
  7. end
  8. end

app/controllers/errors_controller.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Error controller
  3. 2 class ErrorsController < ApplicationController
  4. 2 skip_before_action :authenticate_user!
  5. 2 layout 'errors'
  6. 2 def error_403
  7. 3 render status: 403
  8. end
  9. 2 def error_404
  10. 3 render status: 404
  11. end
  12. 2 def error_422
  13. 3 render status: 422
  14. end
  15. 2 def error_500
  16. 3 render status: 500
  17. end
  18. end

app/controllers/notifications_controller.rb

100.0% lines covered

21 relevant lines. 21 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Controller for notifications
  3. 2 class NotificationsController < ApplicationController
  4. 2 before_action :set_notification, only: %i[destroy read unread]
  5. 2 def index
  6. 3 return render_404 unless request.xhr?
  7. 2 _, notifications = pagy(current_user.notifications.load_index, items: 5)
  8. 2 render json: NotificationBlueprint.render(notifications)
  9. end
  10. 2 def destroy
  11. 1 @notification.delete ? head(:ok) : head(:bad_request)
  12. end
  13. 2 def read
  14. 1 @notification.update_columns(read: true) # rubocop:disable Rails/SkipsModelValidations
  15. 1 head :ok
  16. end
  17. 2 def unread
  18. 1 @notification.update_columns(read: false) # rubocop:disable Rails/SkipsModelValidations
  19. 1 head :ok
  20. end
  21. 2 def read_all
  22. 1 current_user.notifications.unread.update_all(read: true) # rubocop:disable Rails/SkipsModelValidations
  23. 1 head :ok
  24. end
  25. 2 private
  26. 2 def set_notification
  27. 5 @notification = Notification.find(params[:id])
  28. 5 authorize! @notification, with: NotificationPolicy
  29. end
  30. end

app/controllers/pages_controller.rb

100.0% lines covered

28 relevant lines. 28 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Controller for landing pages
  3. 2 class PagesController < ApplicationController
  4. 2 skip_before_action :authenticate_user!
  5. 2 before_action :track_ahoy_visit
  6. 25 after_action :track_action, except: :track_time, unless: -> { request.xhr? }
  7. 2 layout 'landing_page'
  8. 2 def track_social
  9. 9 return head :bad_request unless valid_social_type?
  10. 8 ahoy.track 'Click Social', type: params[:type]
  11. 8 head :ok
  12. end
  13. # Function to track time spent in a page
  14. 2 def track_time
  15. 20 return head :bad_request unless valid_request?
  16. 12 ahoy.track 'Time Spent',
  17. time_spent: params[:time].to_f.round / 1000,
  18. pathname: params[:pathname]
  19. 12 head :ok
  20. end
  21. 2 def submit_feedback
  22. 5 if LandingFeedback.new(feedback_params).save
  23. 3 render json: { message: 'Thank you for your feedback' }
  24. else
  25. 1 render json: { message: 'Submission Failed' }, status: :unprocessable_entity
  26. end
  27. end
  28. 2 private
  29. # Ahoy Gem function to track actions
  30. 2 def track_action
  31. 10 ahoy.track('Ran action', request.path_parameters)
  32. end
  33. 2 def valid_request?
  34. 20 params.require(%i[time pathname]) && valid_pathname?(params[:pathname])
  35. end
  36. 2 def valid_pathname?(pathname)
  37. 17 pathname.in?(%w[/ /pricing /about /love /features /feedback /newsletter])
  38. end
  39. 2 def valid_social_type?
  40. 9 params[:type].in?(%w[Facebook Twitter Email])
  41. end
  42. 2 def feedback_params
  43. 5 params.require(:landing_feedback).permit(%i[smiley channel interest])
  44. end
  45. end

app/controllers/projects/appeals/applications_controller.rb

100.0% lines covered

29 relevant lines. 29 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Projects::Appeals::ApplicationsController < Projects::Appeals::BaseController
  3. 2 before_action :set_application, only: %i[accept destroy]
  4. 2 def index
  5. 3 project = Project.find(params[:project_id])
  6. 3 authorize! project, with: Appeal::ApplicationPolicy
  7. 2 render json: { applications: Appeal.application.list_in_project(project) }
  8. end
  9. 2 def create
  10. 4 @application = Appeal.new(user: current_user, project_id: params[:project_id], type: 'application')
  11. 4 authorize! @application, with: Appeal::ApplicationPolicy
  12. 3 return application_fail unless @application.save
  13. 3 flash[:toast_success] = 'Application sent'
  14. 3 render js: 'location.reload();'
  15. end
  16. 2 def accept
  17. 2 @application.project.unassigned_team.users << @application.user
  18. 2 msg = @application.project.errors.full_messages
  19. 2 return application_fail(msg) unless msg.blank? && @application.delete
  20. 1 @application.send_resolve_notification('accept')
  21. 1 head :ok
  22. end
  23. 2 def destroy
  24. 1 return application_fail unless @application.delete
  25. 1 @application.send_resolve_notification('decline', current_user == @application.user)
  26. # TODO: should return JSON only, otherwise the Table will redirect
  27. 1 flash[:toast_success] = 'Application deleted'
  28. 1 render js: 'location.reload();'
  29. end
  30. 2 private
  31. 2 def application_fail(msg = @application.errors.full_messages)
  32. 1 render json: { message: msg }, status: :unprocessable_entity
  33. end
  34. 2 def set_application
  35. 6 @application = Appeal.application.find(params[:id])
  36. 6 authorize! @application, with: Appeal::ApplicationPolicy
  37. end
  38. end

app/controllers/projects/appeals/base_controller.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Projects::Appeals::BaseController < ApplicationController
  3. 2 rescue_from ActionPolicy::Unauthorized do
  4. 11 head :forbidden
  5. end
  6. end

app/controllers/projects/appeals/invitations_controller.rb

100.0% lines covered

28 relevant lines. 28 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Projects::Appeals::InvitationsController < Projects::Appeals::BaseController
  3. 2 before_action :set_invitation, only: %i[accept destroy]
  4. 2 def index
  5. 8 project = Project.find(params[:project_id])
  6. 8 authorize! project, with: Appeal::InvitationPolicy
  7. 7 render json: { invitations: Appeal.invitation.list_in_project(project) }
  8. end
  9. 2 def create
  10. 5 @invitation = Appeal.new(user: User.find_by(email: params[:email]),
  11. project_id: params[:project_id],
  12. type: 'invitation')
  13. 5 authorize! @invitation, with: Appeal::InvitationPolicy
  14. 4 return invitation_fail unless @invitation.save
  15. 2 render json: { invitation: @invitation }
  16. end
  17. 2 def accept
  18. 3 @invitation.project.unassigned_team.users << @invitation.user
  19. 3 msg = @invitation.project.errors.full_messages
  20. 3 return invitation_fail(msg) unless msg.blank? && @invitation.delete
  21. 2 @invitation.send_resolve_notification('accept')
  22. 2 render js: 'location.reload();'
  23. end
  24. 2 def destroy
  25. 4 return invitation_fail unless @invitation.delete
  26. 4 @invitation.send_resolve_notification('decline', current_user == @invitation.user)
  27. # TODO: should return JSON only, otherwise the Table will redirect
  28. 4 flash[:toast_success] = 'Invitation deleted'
  29. 4 render js: 'location.reload();'
  30. end
  31. 2 private
  32. 2 def invitation_fail(msg = @invitation.errors.full_messages)
  33. 3 render json: { message: msg }, status: :unprocessable_entity
  34. end
  35. 2 def set_invitation
  36. 11 @invitation = Appeal.invitation.find(params[:id])
  37. 11 authorize! @invitation, with: Appeal::InvitationPolicy
  38. end
  39. end

app/controllers/projects/tasks_controller.rb

100.0% lines covered

56 relevant lines. 56 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Projects::TasksController < ApplicationController
  3. 2 before_action :set_task,
  4. only: %i[edit update set_percentage assign_self destroy]
  5. 2 before_action :set_project,
  6. except: %i[update destroy assign_self set_percentage]
  7. 2 before_action :set_skills, only: %i[new edit]
  8. 2 layout 'project'
  9. # GET /tasks/new
  10. 2 def new
  11. 12 @task = Task.new(project: @project)
  12. 12 authorize! @project, to: :create_task?
  13. end
  14. # GET /tasks/1/edit
  15. 2 def edit; end
  16. # POST /tasks
  17. 2 def create
  18. 12 authorize! @project, to: :create_task?
  19. 10 @task = @project.tasks.build(create_params)
  20. 9 if @task.save
  21. 8 @task.skill_ids = params.dig(:task, :skill_ids)
  22. 8 @task.user_ids = params.dig(:task, :user_ids)
  23. 8 task_success('Task created')
  24. else
  25. 1 task_fail
  26. end
  27. end
  28. # PATCH/PUT /tasks/1
  29. 2 def update
  30. 9 @task.update(edit_params) ? task_success('Task updated') : task_fail
  31. end
  32. 2 def assign_self
  33. 3 return task_fail('Task is already Assigned') if @task.users.present?
  34. 2 @task.users << current_user
  35. 2 @task.send_picked_up_notification
  36. 2 head :ok
  37. end
  38. # DELETE /tasks/1
  39. 2 def destroy
  40. 5 @task.destroy
  41. 5 head :ok
  42. end
  43. 2 def data
  44. 30 extend AvatarHelper
  45. 30 return render_404 unless request.xhr? && valid_data_type?
  46. 27 render json: {
  47. data: TaskBlueprint.render_as_json(
  48. authorized_scope(@project.tasks, as: @type).includes(users: [{ avatar_attachment: :blob }])
  49. )
  50. }
  51. end
  52. 2 def set_percentage
  53. 8 @task.update(edit_params) ? head(:ok) : task_fail
  54. end
  55. 2 private
  56. 2 def set_project
  57. 63 @project = Project.find(params[:project_id])
  58. end
  59. 2 def set_skills
  60. 46 @skills = @project.category.skills.map { |s| [s.name, s.id] }
  61. 21 team = current_user.teams.find_by(project: @project)
  62. 21 @assignees = authorized_scope(
  63. @project.users,
  64. as: :assignee,
  65. scope_options: { team_id: team&.id, project: @project }
  66. )
  67. end
  68. 2 def set_task
  69. 42 @task = Task.find(params[:id])
  70. 42 authorize! @task
  71. end
  72. 2 def create_params
  73. 10 params.require(:task)
  74. .except(:skill_ids, :user_ids)
  75. .permit(:name, :description, :priority)
  76. .merge(user: current_user)
  77. end
  78. 2 def edit_params
  79. 17 params.require(:task).permit(:description, :name,
  80. :percentage, :priority,
  81. skill_ids: [], user_ids: [])
  82. end
  83. 2 def task_fail(message = @task.errors.full_messages)
  84. 4 render json: { message: message }, status: :unprocessable_entity
  85. end
  86. 2 def task_success(message)
  87. 15 flash[:toast_success] = message
  88. 15 render js: "window.location = '#{project_path(@task.project)}'"
  89. end
  90. 2 def valid_data_type?
  91. 30 authorize! @project, to: :count?
  92. 29 @type = params[:type]&.to_sym
  93. 29 %i[assigned unassigned active completed].include?(@type)
  94. end
  95. end

app/controllers/projects/teams/base_controller.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Projects::Teams::BaseController < ApplicationController
  3. 2 layout 'project'
  4. 2 private
  5. 2 def set_team
  6. 29 @team = Team.find(params[:id])
  7. 29 authorize! @team
  8. end
  9. 2 def set_skills
  10. 14 @skills = Skill.where(category: @project.category)
  11. 11 .collect { |s| [s.name, s.id] }
  12. end
  13. 2 def set_project
  14. 83 @project = Project.find(params[:project_id])
  15. 83 authorize! @project, to: :manage?
  16. end
  17. 2 def team_success(message)
  18. 10 flash[:toast_success] = message
  19. 10 render js: "window.location = '#{project_manage_index_path(@project)}'"
  20. end
  21. 2 def team_fail(message = @team.errors.full_messages)
  22. 4 render json: { message: message }, status: :unprocessable_entity
  23. end
  24. end

app/controllers/projects/teams/manage_controller.rb

100.0% lines covered

55 relevant lines. 55 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Projects::Teams::ManageController < Projects::Teams::BaseController
  3. 2 before_action :set_project
  4. 2 def index; end
  5. 2 def manage_data
  6. 11 return render_404 unless request.xhr?
  7. 11 @data = User.teams_data(@project)
  8. 11 compute = CompatibilityCompute.new(@project.teams,
  9. @project.unassigned_team)
  10. 11 render json: {
  11. 17 compatibility: @data.to_h { |u| [u.id, compute.call(u)] },
  12. data: @data.group_by(&:team_id),
  13. teams: @project.teams&.select(:id, :name, :team_size)
  14. }
  15. end
  16. 2 def suggest
  17. 2 return team_fail('Invalid Mode') unless %w[balance cohesion efficient].include?(params[:mode])
  18. 2 suggestion = Suggest.new(User.teams_data(@project),
  19. @project.teams.where.not(name: 'Unassigned'),
  20. @project.unassigned_team,
  21. params[:mode]).call
  22. 2 render json: { data: suggestion }
  23. end
  24. # save_data
  25. 2 def create
  26. 2 data = Oj.load(params[:data])
  27. 2 teams = @project.teams
  28. 2 users = @project.users
  29. 2 data.each do |id, members|
  30. 14 new_team = teams.find { |x| x.id.to_s == id }
  31. 4 next if new_team.nil?
  32. 4 authorize! new_team, to: :manage?
  33. 4 saving_loop(new_team, members, teams, users)
  34. end
  35. 2 render json: { data: data }
  36. end
  37. 2 def recompute_data
  38. 2 return team_fail('Invalid Mode') unless %w[balance cohesion efficient].include?(params[:mode])
  39. 2 i = Oj.load(params[:data])
  40. 2 teams = @project.teams.find(i.keys)
  41. 2 d = recompute_loop(i, teams, params[:mode])
  42. 2 render json: { compatibility: d }
  43. end
  44. 2 def remove_user
  45. 3 user = User.find(params[:user_id])
  46. 3 @team = user.teams.find_by(project: @project)
  47. 3 return team_fail('Cannot remove owner') if user == @project.user
  48. 2 @team.users.delete(user)
  49. 2 head :ok
  50. end
  51. 2 private
  52. 2 def change_team(member, new_team, old_team)
  53. 2 authorize! new_team, to: :manage?
  54. 2 new_team.users << member
  55. 2 old_team.users.delete(member)
  56. end
  57. 2 def recompute_loop(input, teams, mode)
  58. 2 data = {}
  59. 6 compute = Recompute.new(teams, teams.find { |x| x.name == 'Unassigned' }, mode)
  60. 2 input.each do |team_id, members|
  61. 4 next if members.blank?
  62. 4 data.merge!(
  63. 6 compute.call(teams.find { |x| x.id.to_s == team_id }, input).to_h
  64. )
  65. end
  66. 2 data
  67. end
  68. 2 def saving_loop(new_team, members, teams, users)
  69. 4 members.each do |m|
  70. 4 next unless m['team_id'] != new_team.id
  71. 13 target = users.find { |x| x.id == m['id'] }
  72. 6 old_team = teams.find { |x| x.id == m['team_id'] }
  73. 2 change_team(target, new_team, old_team)
  74. 2 m['team_id'] = new_team.id
  75. end
  76. end
  77. end

app/controllers/projects/teams/teams_controller.rb

100.0% lines covered

28 relevant lines. 28 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Projects::Teams::TeamsController < Projects::Teams::BaseController
  3. 2 before_action :set_team, only: %i[edit show update destroy]
  4. 2 before_action :set_project, except: %i[show]
  5. 2 before_action :set_skills, only: %i[new edit]
  6. 2 skip_before_action :authenticate_user!, only: %i[show]
  7. # GET /teams/new
  8. 2 def new; end
  9. # GET /teams/1/edit
  10. 2 def edit; end
  11. 2 def show
  12. 7 extend AvatarHelper
  13. 7 return render_404 unless request.xhr?
  14. 7 render json: { member: @team.users, skill: @team.skills&.select(:id, :name),
  15. team: @team, images: get_avatars(@team.users.pluck('id')) }
  16. end
  17. # POST /teams
  18. 2 def create
  19. 7 @team = @project.teams.build(create_params)
  20. 7 if @team.save
  21. 5 @team.skill_ids = params[:team][:skill_ids]
  22. 5 team_success('Team was successfully created')
  23. else
  24. 2 team_fail
  25. end
  26. end
  27. # PATCH/PUT /teams/1
  28. 2 def update
  29. 6 @team.update(edit_params) ? team_success('Team Saved') : team_fail
  30. end
  31. # DELETE /teams/1
  32. 2 def destroy
  33. 3 @project.unassigned_team.users << @team.users
  34. 3 @team.destroy
  35. 3 head :ok
  36. end
  37. 2 private
  38. 2 def create_params
  39. 7 params.require(:team).except(:skill_ids)
  40. .permit(:team_size, :project_id, :name)
  41. end
  42. 2 def edit_params
  43. 6 params.require(:team).permit(:team_size, :name, skill_ids: [])
  44. end
  45. end

app/controllers/projects_controller.rb

100.0% lines covered

46 relevant lines. 46 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Controller for projects
  3. 2 class ProjectsController < ApplicationController
  4. 2 skip_before_action :authenticate_user!, only: %i[explore show]
  5. 2 before_action :set_project, except: %i[index explore new create]
  6. 2 layout 'project'
  7. # GET /projects
  8. 2 def index
  9. 22 render_projects(params[:joined].present? ? :joined : :own)
  10. end
  11. 2 def explore
  12. 4 render_projects(:explore)
  13. end
  14. # GET /projects/1
  15. 2 def show; end
  16. # GET /projects/new
  17. 2 def new; end
  18. # POST /projects
  19. 2 def create
  20. 7 @project = current_user.projects.build(project_params)
  21. 7 @project.save ? project_success('Project Created') : project_fail
  22. end
  23. # PATCH/PUT /projects/1
  24. 2 def update
  25. 6 return project_fail unless @project.update(project_params)
  26. 5 project_success('Project Updated')
  27. end
  28. 2 def change_status
  29. 7 msg = case params[:status]
  30. when 'completed'
  31. 2 'Project Archived'
  32. when 'active'
  33. 3 @project.appeals.application.destroy_all
  34. 3 @project.completed? ? 'Project Activated' : 'Project Closed'
  35. when 'open'
  36. 2 'Project Opened'
  37. end
  38. 7 @project.update(status: params[:status]) ? project_success(msg) : project_fail
  39. end
  40. 2 def count
  41. 3 return head :bad_request unless %w[task application].include?(params[:type])
  42. 2 render json: {
  43. count: case params[:type]
  44. 1 when 'task' then @project.tasks.where('percentage < 100').size
  45. 1 when 'application' then @project.appeals.size
  46. end
  47. }
  48. end
  49. 2 private
  50. 2 def set_project
  51. 89 @project = Project.includes(:teams, :tasks)
  52. .find(params[:id])
  53. 89 authorize! @project, with: ProjectPolicy
  54. end
  55. 2 def project_params
  56. 13 params.require(:project).permit(%i[name description visibility category_id avatar])
  57. end
  58. 2 def render_projects(policy_scope)
  59. 26 @pagy, projects = pagy(authorized_scope(Project.search(params), as: policy_scope))
  60. 25 @html = view_to_html_string('projects/_projects', projects: projects)
  61. 25 respond_to do |format|
  62. 25 format.html
  63. 31 format.json { render json: { html: @html, total: @pagy.count } }
  64. end
  65. end
  66. 2 def project_success(message)
  67. 17 @project.appeals.delete_all if @project.completed?
  68. 17 flash[:toast_success] = message
  69. 17 render js: "window.location = '#{project_path(@project)}'"
  70. end
  71. 2 def project_fail(message = @project.errors.full_messages)
  72. 3 render json: { message: message }, status: :unprocessable_entity
  73. end
  74. end

app/controllers/skills_controller.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class SkillsController < ApplicationController
  3. 2 skip_before_action :authenticate_user!
  4. 2 def index
  5. 5 return render_404 unless request.xhr?
  6. 4 render json: {
  7. skills: Skill.with_category.where('categories.name ~* ?', params[:category]).to_json
  8. }
  9. end
  10. end

app/controllers/users/omniauth_callbacks_controller.rb

100.0% lines covered

27 relevant lines. 27 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # controller for OAuth Devise
  3. 2 class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  4. 2 def omniauth_flow
  5. 18 user_signed_in? ? connect_flow : sign_in_flow
  6. 18 return unless @user&.persisted?
  7. 12 user_signed_in? ? connect_success_action : sign_in_success_action
  8. end
  9. 2 Devise.omniauth_providers.each do |provider|
  10. 6 alias_method provider, :omniauth_flow
  11. end
  12. # More info at:
  13. # https://github.com/plataformatec/devise#omniauth
  14. # GET|POST /resource/auth/twitter
  15. # def passthru
  16. # super
  17. # end
  18. # GET|POST /users/auth/twitter/callback
  19. # def failure
  20. # super
  21. # end
  22. # protected
  23. # The path used when OmniAuth fails
  24. # def after_omniauth_failure_path_for(scope)
  25. # super(scope)
  26. # end
  27. 2 private
  28. 2 def sign_in_flow
  29. 12 identity = User.sign_in_omniauth(request.env['omniauth.auth'])
  30. 12 errors = identity.errors.full_messages + identity.user.errors.full_messages
  31. 12 if errors.blank?
  32. 9 @user = identity.user
  33. else
  34. 3 flash[:toast_error] = errors
  35. 3 redirect_to new_user_registration_url
  36. end
  37. end
  38. 2 def connect_flow
  39. 6 identity = current_user.connect_omniauth(request.env['omniauth.auth'])
  40. 6 if identity.errors.blank?
  41. 3 @user = identity.user
  42. else
  43. 3 flash[:toast_error] = identity.errors.full_messages
  44. 3 redirect_to settings_account_path
  45. end
  46. end
  47. 2 def connect_success_action
  48. 3 flash[:toast_success] = 'Account Connected'
  49. 3 redirect_to settings_account_path
  50. end
  51. 2 def sign_in_success_action
  52. 9 sign_in @user
  53. 9 redirect_to after_sign_in_path_for(@user)
  54. end
  55. end

app/controllers/users/passwords_controller.rb

100.0% lines covered

20 relevant lines. 20 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Controller for setting and changing password
  3. 2 class Users::PasswordsController < Devise::PasswordsController
  4. # GET /resource/password/new
  5. # def new
  6. # super
  7. # end
  8. # POST /resource/password
  9. 2 def create
  10. 4 self.resource = resource_class.send_reset_password_instructions(resource_params)
  11. 4 yield resource if block_given?
  12. 4 return head :ok if successfully_sent?(resource)
  13. 2 render json: { message: resource.errors.full_messages }, status: :bad_request
  14. end
  15. # GET /resource/password/edit?reset_password_token=abcdef
  16. # def edit
  17. # super
  18. # end
  19. # PUT /resource/password
  20. 2 def update
  21. 3 self.resource = resource_class.reset_password_by_token(resource_params)
  22. 3 yield resource if block_given?
  23. 3 resource.errors.empty? ? update_success : update_fail
  24. end
  25. # protected
  26. # def after_resetting_password_path_for(resource)
  27. # super(resource)
  28. # end#
  29. # The path used after sending reset password instructions
  30. # def after_sending_reset_password_instructions_path_for(resource_name)
  31. # super(resource_name)
  32. # end
  33. 2 private
  34. 2 def update_fail
  35. 2 set_minimum_password_length
  36. 2 render json: { message: resource.errors.full_messages }, status: :bad_request
  37. end
  38. 2 def update_success
  39. 1 resource.unlock_access! if unlockable?(resource)
  40. 1 if Devise.sign_in_after_reset_password
  41. 1 resource.after_database_authentication
  42. 1 sign_in(resource_name, resource)
  43. end
  44. 1 render js: "window.location='#{root_path}'"
  45. end
  46. end

app/controllers/users/registrations_controller.rb

100.0% lines covered

30 relevant lines. 30 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Controller for registration and update password
  3. 2 class Users::RegistrationsController < Devise::RegistrationsController
  4. # before_action :configure_sign_up_params, only: [:create]
  5. # before_action :configure_account_update_params, only: [:update]
  6. # GET /resource/sign_up
  7. # def new
  8. # super
  9. # end
  10. # POST /resource
  11. 2 def create
  12. 6 build_resource(sign_up_params)
  13. 6 resource.save
  14. 6 yield resource if block_given?
  15. 6 resource.persisted? ? register_success : register_fail
  16. end
  17. # GET /resource/edit
  18. # def edit
  19. # super
  20. # end
  21. # PUT /resource
  22. 2 def update
  23. 7 if form_filled?
  24. 6 resource_updated = oauth_update_resource
  25. 6 yield resource if block_given?
  26. 6 resource_updated ? update_success : register_fail
  27. else
  28. 1 render json: { message: 'Please fill in the form' }, status: :bad_request
  29. end
  30. end
  31. # DELETE /resource
  32. # def destroy
  33. # super
  34. # end
  35. # GET /resource/cancel
  36. # Forces the session data which is usually expired after sign
  37. # in to be expired now. This is useful if the user wants to
  38. # cancel oauth signing in/up in the middle of the process,
  39. # removing all OAuth session data.
  40. # def cancel
  41. # super
  42. # end
  43. # protected
  44. # If you have extra params to permit, append them to the sanitizer.
  45. # def configure_sign_up_params
  46. # devise_parameter_sanitizer.permit(:sign_up, keys: [:attribute])
  47. # end
  48. # If you have extra params to permit, append them to the sanitizer.
  49. # def configure_account_update_params
  50. # devise_parameter_sanitizer.permit(:account_update, keys: [:attribute])
  51. # end
  52. # The path used after sign up.
  53. # def after_sign_up_path_for(resource)
  54. # super(resource)
  55. # end
  56. # The path used after sign up for inactive accounts.
  57. # def after_inactive_sign_up_path_for(resource)
  58. # super(resource)
  59. # root_path
  60. # end
  61. 2 private
  62. 2 def form_filled?
  63. 7 params[:user][:password].present? && params[:user][:password_confirmation].present?
  64. end
  65. 2 def register_success
  66. 2 sign_up(resource_name, resource)
  67. 2 render js: "window.location='#{root_path}'"
  68. end
  69. 2 def update_success
  70. 2 bypass_sign_in resource, scope: resource_name if sign_in_after_change_password?
  71. 2 flash[:toast_success] = 'Password Changed'
  72. 2 render js: "window.location='#{settings_account_path}'"
  73. end
  74. 2 def register_fail
  75. 8 clean_up_passwords resource
  76. 8 set_minimum_password_length
  77. 8 render json: { message: resource.errors.full_messages }, status: :bad_request
  78. end
  79. 2 def oauth_update_resource
  80. 6 if resource.password_automatically_set?
  81. 3 resource.update(account_update_params)
  82. else
  83. 3 update_resource(resource, account_update_params)
  84. end
  85. end
  86. end

app/controllers/users/sessions_controller.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Controller for handling user sessions
  3. 2 class Users::SessionsController < Devise::SessionsController
  4. # before_action :configure_sign_in_params, only: [:create]
  5. # GET /resource/sign_in
  6. # def new
  7. # super
  8. # end
  9. # POST /resource/sign_in
  10. 2 def create
  11. 5 self.resource = warden.authenticate(auth_options)
  12. 5 if resource
  13. 2 sign_in(resource_name, resource)
  14. 2 yield resource if block_given?
  15. 2 render js: "window.location='#{after_sign_in_path_for(resource)}'"
  16. else
  17. 3 set_flash_message(:errors, :invalid)
  18. 3 render json: { message: flash[:errors] }, status: :unauthorized
  19. end
  20. end
  21. # DELETE /resource/sign_out
  22. # def destroy
  23. # super
  24. # end
  25. # protected
  26. # If you have extra params to permit, append them to the sanitizer.
  27. # def configure_sign_in_params
  28. # devise_parameter_sanitizer.permit(:sign_in, keys: [:attribute])
  29. # end
  30. end

app/controllers/users/settings/accounts_controller.rb

100.0% lines covered

17 relevant lines. 17 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Users::Settings::AccountsController < Users::Settings::BaseController
  3. 2 def show
  4. 23 render 'users/settings/account'
  5. end
  6. 2 def disconnect_omniauth
  7. 8 identity = current_user.identities.find_by!(provider: params[:provider])
  8. 7 if disconnect_provider_allowed?
  9. 4 identity.delete
  10. 4 flash[:toast_success] = 'Account Disconnected'
  11. else
  12. 3 flash[:toast_error] = 'Please set up a password before disabling all Social Accounts'
  13. end
  14. 7 redirect_to settings_account_path
  15. end
  16. 2 def reset_password
  17. 1 current_user.send_reset_password_instructions
  18. 1 flash[:toast_success] = 'Check your email for reset instruction'
  19. 1 redirect_to settings_account_path
  20. end
  21. 2 private
  22. 2 def disconnect_provider_allowed?
  23. 7 !current_user.password_automatically_set? || current_user.identities.size > 1
  24. end
  25. end

app/controllers/users/settings/base_controller.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Users::Settings::BaseController < ApplicationController
  3. 2 layout 'user'
  4. end

app/controllers/users/settings/billings_controller.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class Users::Settings::BillingsController < Users::Settings::BaseController
  3. 1 def show
  4. 3 render 'users/settings/billing'
  5. end
  6. 1 def update
  7. 2 current_user.license.update(plan: params[:plan])
  8. 1 flash[:toast_success] = 'Billing plan updated'
  9. rescue ArgumentError
  10. 1 flash[:toast_error] = 'Invalid argument'
  11. ensure
  12. 2 redirect_to settings_billing_path
  13. end
  14. end

app/controllers/users/settings/emails_controller.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Users::Settings::EmailsController < Users::Settings::BaseController
  3. 2 def show
  4. 6 render 'users/settings/emails'
  5. end
  6. 2 def subscribe
  7. 2 NewsletterSubscription.subscribe(current_user.email)
  8. 2 flash[:toast_success] = 'Newsletter Subscribed'
  9. 2 redirect_to settings_emails_path
  10. end
  11. 2 def unsubscribe
  12. 2 NewsletterSubscription.find_by(email: current_user.email)&.unsubscribe
  13. 2 flash[:toast_success] = 'Newsletter Unsubscribed'
  14. 2 redirect_to settings_emails_path
  15. end
  16. end

app/controllers/users/settings/personalities_controller.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Controller for setting user personality
  3. 2 class Users::Settings::PersonalitiesController < Users::Settings::BaseController
  4. 2 def show
  5. 36 @personality = current_user.personality || Personality.new
  6. 36 render 'users/settings/personalities'
  7. end
  8. 2 def update
  9. # Invalid input will by rescued by StatementInvalid in ApplcationController
  10. 18 current_user.update(personality: Personality.find_by(personality_params))
  11. 17 flash[:toast_success] = 'Personality Updated'
  12. 17 render js: "window.location = '#{settings_personality_path}'"
  13. end
  14. 2 private
  15. 2 def personality_params
  16. 18 params.require(:personality).permit(:mind, :energy, :nature, :tactic)
  17. end
  18. end

app/controllers/users/settings/profiles_controller.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Users::Settings::ProfilesController < Users::Settings::BaseController
  3. 2 def show
  4. 6 render 'users/settings/profile'
  5. end
  6. 2 def update
  7. 6 if current_user.update(profile_params)
  8. 2 render json: { message: 'Profile updated' }
  9. else
  10. 4 render json: { message: current_user.errors.full_messages }, status: :bad_request
  11. end
  12. end
  13. 2 def remove_avatar
  14. 2 return head :not_found unless current_user.avatar.attached?
  15. 1 current_user.avatar.purge_later
  16. 1 redirect_to settings_profile_path, status: :found
  17. end
  18. 2 private
  19. 2 def profile_params
  20. 6 params.require(:user).permit(:name, :avatar, :birthdate)
  21. end
  22. end

app/controllers/users/settings/skills_controller.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Users::Settings::SkillsController < Users::Settings::BaseController
  3. 2 def show
  4. 8 @skills = current_user.skills.with_category('category_name ASC, name ASC')
  5. 8 render 'users/settings/skills'
  6. end
  7. 2 def create
  8. # insert_all does not trigger validations, handle the errors on client-side
  9. 3 records = UserSkill.insert_all(
  10. 3 skill_params[:skill_ids].map { |id| { user_id: current_user.id, skill_id: id } }
  11. )
  12. 3 if skill_params[:skill_ids].length == records.length
  13. 2 head :ok
  14. else
  15. 1 head :unprocessable_entity
  16. end
  17. end
  18. 2 def destroy
  19. 2 user_skill = current_user.user_skills.find_by!(skill_id: params[:skill_id])
  20. 1 user_skill.delete ? head(:ok) : head(:bad_request)
  21. end
  22. 2 private
  23. 2 def skill_params
  24. 6 params.require(:skill).permit(skill_ids: [])
  25. end
  26. end

app/controllers/users_controller.rb

100.0% lines covered

34 relevant lines. 34 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Controller for users
  3. 2 class UsersController < ApplicationController
  4. 2 skip_before_action :authenticate_user!
  5. 2 before_action :set_user, only: %i[show timeline]
  6. 2 layout 'user'
  7. 2 def show
  8. 25 render_projects(params[:joined].present? ? :profile_joined : :profile_owned)
  9. 25 prepare_skills
  10. 25 prepare_counts
  11. end
  12. 2 def timeline
  13. 5 return render_404 unless request.xhr?
  14. 5 month = find_next_activity(params[:month].to_i)
  15. 5 month.present? ? render_timeline(month) : head(:no_content)
  16. end
  17. 2 private
  18. 2 def set_user
  19. 31 @user = User.find(params[:id])
  20. end
  21. 2 def find_next_activity(month)
  22. 5 data = @user.activities.where('created_at <= ?',
  23. DateTime.current.beginning_of_month << month)
  24. 5 data.order('created_at DESC').first.created_at.to_datetime if data.exists?
  25. end
  26. 2 def render_timeline(mth)
  27. 3 tasks, events = authorized_scope(@user.activities.from_month(mth))
  28. 3 html = view_to_html_string('users/_timeline',
  29. events: events,
  30. tasks: tasks,
  31. header: mth.strftime('%B %Y'))
  32. 3 render json: { html: html, m: ((Time.current - mth) / 1.month).floor }
  33. end
  34. 2 def render_projects(policy_scope)
  35. 25 pagy, projects = pagy(authorized_scope(
  36. Project.search(params),
  37. as: policy_scope,
  38. scope_options: { profile_owner: @user }
  39. ))
  40. 25 html = view_to_html_string('projects/_projects', projects: projects)
  41. 25 respond_to do |format|
  42. 25 format.html
  43. 31 format.json { render json: { html: html, total: pagy.count } }
  44. end
  45. end
  46. 2 def prepare_skills
  47. 25 @skills = @user.user_skills.joins(skill: :category)
  48. .select('user_skills.created_at')
  49. .select('skills.name AS name')
  50. .select('categories.name AS category_name')
  51. 25 @chart = @user.skills.joins(:category)
  52. .select('categories.name')
  53. .group('categories.name').count
  54. end
  55. 2 def prepare_counts
  56. 25 joined_ids = @user.teams.present? ? "(#{@user.teams.pluck(:project_id).join(',')})" : '(0)'
  57. 25 @counts = @user.projects.pluck(
  58. Arel.sql(
  59. "(SELECT COUNT(1) FROM projects WHERE id IN #{joined_ids} AND NOT user_id = #{@user.id}),"\
  60. 'COUNT(1),'\
  61. "(SELECT COUNT(1) FROM projects WHERE id IN #{joined_ids} AND status = 'completed')"
  62. )
  63. ).first
  64. end
  65. end

app/helpers/application_helper.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Base helper module
  3. 4 module ApplicationHelper
  4. 4 def current_path?(path)
  5. 1426 request.path == path
  6. end
  7. 4 def current_controller?(*args)
  8. 448 args.any? do |v|
  9. 449 v.to_s.downcase == controller_name || v.to_s.downcase == controller_path
  10. end
  11. end
  12. 4 def current_action?(*args)
  13. 268 args.any? { |v| v.to_s.downcase == action_name }
  14. end
  15. end

app/helpers/avatar_helper.rb

100.0% lines covered

22 relevant lines. 22 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Helper for showing avatar images
  3. 4 module AvatarHelper
  4. 4 include Rails.application.routes.url_helpers
  5. 4 include GravatarImageTag
  6. 4 def user_avatar(user)
  7. 434 if user.avatar.attached?
  8. 6 rails_representation_url(user.avatar.variant(resize: '100x100!'))
  9. else
  10. 428 gravatar_image_url(user.email, size: 100, default: :retro, secure: true)
  11. end
  12. end
  13. 4 def project_icon(project)
  14. 145 return source_identicon(project) unless project.avatar.attached?
  15. 2 content_tag(:figure, class: 'image') do
  16. 2 image_tag rails_representation_url(project.avatar.variant(resize: '100x100!')), alt: 'Project avatar'
  17. end
  18. end
  19. 4 def get_avatars(ids)
  20. 9 arr = {}
  21. 9 User.find(ids).each do |u|
  22. 3 arr[u.id] = u.avatar.attached? ? url_for(user_avatar(u)) : user_avatar(u)
  23. end
  24. 9 arr
  25. end
  26. 4 private
  27. 4 def source_identicon(project)
  28. 143 content_tag(:div, class: "identicon bg#{(project.id % 7) + 1}") do
  29. 143 project.name.first.upcase
  30. end
  31. end
  32. 4 def default_url_options
  33. 71 { only_path: true }
  34. end
  35. end

app/helpers/devise_helper.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Helper for devise authentication
  3. 4 module DeviseHelper
  4. PROVIDERS = {
  5. 4 google_oauth2: {
  6. label: 'Google',
  7. icon: 'flat-color-icons:google'
  8. },
  9. facebook: {
  10. label: 'Facebook',
  11. icon: 'fe:facebook'
  12. },
  13. twitter: {
  14. label: 'Twitter',
  15. icon: 'fe:twitter'
  16. }
  17. }.freeze
  18. 4 def provider_label(provider)
  19. 210 PROVIDERS[provider][:label]
  20. end
  21. 4 def provider_icon(provider)
  22. 141 PROVIDERS[provider][:icon]
  23. end
  24. end

app/helpers/nav_helper.rb

100.0% lines covered

20 relevant lines. 20 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Helper for creating navigation links
  3. 4 module NavHelper
  4. 4 def nav_link_to(name = nil, options = {}, &block)
  5. 1865 classes = [options.delete(:class)&.split(' ')]
  6. 1865 classes << 'is-active' if active_nav_link?(options)
  7. 1865 if block_given?
  8. 986 link_to(options[:path], class: classes) { capture(&block) + name }
  9. else
  10. 1372 link_to name, options[:path], class: classes
  11. end
  12. end
  13. 4 def active_nav_link?(options = {})
  14. # Path params is ignored if controller or action is provided
  15. 1865 if only_path?(options)
  16. 1424 current_path?(options[:path])
  17. else
  18. 441 current_controller_and_action?(options)
  19. end
  20. end
  21. 4 private
  22. 4 def only_path?(options)
  23. 1865 !options[:controller] && !options[:action]
  24. end
  25. 4 def current_controller_and_action?(options)
  26. 441 c = options.delete(:controller)
  27. 441 a = options.delete(:action)
  28. 441 if c && a
  29. # When given both options, make sure BOTH are true
  30. 437 current_controller?(*c) && current_action?(*a)
  31. else
  32. # Otherwise check EITHER option
  33. 4 current_controller?(*c) || current_action?(*a)
  34. end
  35. end
  36. end

app/mailers/application_mailer.rb

100.0% lines covered

3 relevant lines. 3 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 class ApplicationMailer < ActionMailer::Base
  3. 4 default from: 'Diversify <no-reply@sheffield.ac.uk>'
  4. 4 layout 'mailer'
  5. end

app/mailers/newsletter_mailer.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Mailer class for Newsletter
  3. 4 class NewsletterMailer < ApplicationMailer
  4. 4 def send_newsletter(emails, newsletter)
  5. 4 @content = newsletter.content
  6. 4 mail(to: 'no-reply@sheffield.ac.uk',
  7. bcc: emails,
  8. subject: newsletter.title,
  9. content_type: 'text/html')
  10. end
  11. 4 def send_welcome(email)
  12. 3 mail(to: 'no-reply@sheffield.ac.uk',
  13. bcc: email,
  14. subject: 'Welcome to Diversify',
  15. content_type: 'text/html')
  16. end
  17. end

app/models/activity.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: activities
  5. #
  6. # id :bigint not null, primary key
  7. # key :string default("")
  8. # created_at :datetime not null
  9. # updated_at :datetime not null
  10. # project_id :bigint
  11. # user_id :bigint not null
  12. #
  13. # Indexes
  14. #
  15. # index_activities_on_project_id (project_id)
  16. # index_activities_on_user_id (user_id)
  17. #
  18. # Foreign Keys
  19. #
  20. # fk_rails_... (project_id => projects.id)
  21. # fk_rails_... (user_id => users.id)
  22. #
  23. 4 class Activity < ApplicationRecord
  24. 4 belongs_to :user
  25. 4 belongs_to :project, optional: true
  26. 4 validates :key, presence: true
  27. 9 scope :from_month, ->(mth) { where(created_at: mth.all_month) }
  28. end

app/models/ahoy/event.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: ahoy_events
  5. #
  6. # id :bigint not null, primary key
  7. # name :string
  8. # properties :jsonb
  9. # time :datetime
  10. # user_id :bigint
  11. # visit_id :bigint
  12. #
  13. # Indexes
  14. #
  15. # index_ahoy_events_on_name_and_time (name,time)
  16. # index_ahoy_events_on_properties (properties) USING gin
  17. # index_ahoy_events_on_user_id (user_id)
  18. # index_ahoy_events_on_visit_id (visit_id)
  19. #
  20. 4 class Ahoy::Event < ApplicationRecord
  21. 4 include Ahoy::QueryMethods
  22. 4 self.table_name = 'ahoy_events'
  23. 4 belongs_to :visit
  24. 4 belongs_to :user, optional: true
  25. 11 scope :action, -> { where(name: 'Ran action') }
  26. 15 scope :social, -> { where(name: 'Click Social') }
  27. 7 scope :type_size, -> { group("properties ->> 'type'").size }
  28. 7 scope :type_time_size, -> { group("properties ->> 'type'").group_by_day(:time).size }
  29. end

app/models/ahoy/visit.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: ahoy_visits
  5. #
  6. # id :bigint not null, primary key
  7. # app_version :string
  8. # browser :string
  9. # city :string
  10. # country :string
  11. # device_type :string
  12. # ip :string
  13. # landing_page :text
  14. # latitude :float
  15. # longitude :float
  16. # os :string
  17. # os_version :string
  18. # platform :string
  19. # referrer :text
  20. # referring_domain :string
  21. # region :string
  22. # started_at :datetime
  23. # user_agent :text
  24. # utm_campaign :string
  25. # utm_content :string
  26. # utm_medium :string
  27. # utm_source :string
  28. # utm_term :string
  29. # visit_token :string
  30. # visitor_token :string
  31. # user_id :bigint
  32. #
  33. # Indexes
  34. #
  35. # index_ahoy_visits_on_user_id (user_id)
  36. # index_ahoy_visits_on_visit_token (visit_token) UNIQUE
  37. #
  38. 4 class Ahoy::Visit < ApplicationRecord
  39. 4 self.table_name = 'ahoy_visits'
  40. 4 has_many :events, class_name: 'Ahoy::Event', dependent: :destroy
  41. 4 belongs_to :user, optional: true
  42. 16 scope :today_count, -> { where(started_at: Time.zone.today.all_day).size }
  43. end

app/models/appeal.rb

100.0% lines covered

31 relevant lines. 31 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: appeals
  5. #
  6. # id :bigint not null, primary key
  7. # type :enum default("invitation"), not null
  8. # created_at :datetime not null
  9. # updated_at :datetime not null
  10. # project_id :bigint not null
  11. # user_id :bigint not null
  12. #
  13. # Indexes
  14. #
  15. # index_appeals_on_project_id (project_id)
  16. # index_appeals_on_user_id (user_id)
  17. # index_appeals_on_user_id_and_project_id (user_id,project_id) UNIQUE
  18. #
  19. # Foreign Keys
  20. #
  21. # fk_rails_... (project_id => projects.id)
  22. # fk_rails_... (user_id => users.id)
  23. #
  24. 4 class Appeal < ApplicationRecord
  25. 4 self.inheritance_column = nil
  26. 4 enum type: { invitation: 'invitation', application: 'application' }
  27. 4 belongs_to :project
  28. 4 belongs_to :user
  29. 4 validate :not_owner, on: :create
  30. 4 validate :in_project, on: :create
  31. 4 validates :user_id,
  32. presence: true,
  33. uniqueness: { scope: :project_id,
  34. message: 'has already been invited/applied' }
  35. 4 scope :list_in_project, lambda { |project|
  36. 10 joins(:user)
  37. .select('appeals.id, users.id AS user_id, users.email AS user_email')
  38. .where(project: project)
  39. }
  40. 4 after_create_commit :send_notification
  41. # resolution: accept or decline
  42. 4 def send_resolve_notification(resolution, is_cancel = false)
  43. 11 Notification.delete_by(send_notification_params)
  44. 11 return if is_cancel
  45. 7 SendNotificationJob.perform_later(
  46. 7 invitation? ? [project.user] : [user],
  47. { key: "#{type}/#{resolution}",
  48. 7 notifiable: invitation? ? user : project,
  49. notifier: project }
  50. )
  51. 7 join_activity if resolution == 'accept'
  52. end
  53. 4 private
  54. 4 def join_activity
  55. 4 Activity.find_or_create_by(key: 'project/join', user: user, project: project)
  56. end
  57. 4 def send_notification
  58. 35 SendNotificationJob.perform_later(
  59. 35 invitation? ? [project.user] : [user],
  60. send_notification_params
  61. )
  62. end
  63. 4 def send_notification_params
  64. {
  65. 92 user: invitation? ? user : project.user,
  66. key: "#{type}/send",
  67. 46 notifiable: invitation? ? project : user,
  68. notifier: project
  69. }
  70. end
  71. 4 def not_owner
  72. 44 errors[:base] << 'Owner cannot be added to project' if user == project&.user
  73. end
  74. 4 def in_project
  75. 44 errors[:base] << 'User already in project' if user&.in_project?(project)
  76. end
  77. end

app/models/application_record.rb

100.0% lines covered

2 relevant lines. 2 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 4 class ApplicationRecord < ActiveRecord::Base
  3. 4 self.abstract_class = true
  4. end

app/models/category.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: categories
  5. #
  6. # id :bigint not null, primary key
  7. # name :string default(""), not null
  8. # created_at :datetime not null
  9. # updated_at :datetime not null
  10. #
  11. # Indexes
  12. #
  13. # index_categories_on_name (name) UNIQUE
  14. #
  15. 4 class Category < ApplicationRecord
  16. 4 has_many :skills, dependent: :nullify
  17. 4 has_many :projects, dependent: :nullify
  18. 4 validates :name, presence: true, uniqueness: { case_sensitive: false }
  19. end

app/models/collaboration.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: collaborations
  5. #
  6. # id :bigint not null, primary key
  7. # created_at :datetime not null
  8. # updated_at :datetime not null
  9. # team_id :bigint not null
  10. # user_id :bigint not null
  11. #
  12. # Indexes
  13. #
  14. # index_collaborations_on_team_id (team_id)
  15. # index_collaborations_on_user_id (user_id)
  16. # index_collaborations_on_user_id_and_team_id (user_id,team_id) UNIQUE
  17. #
  18. # Foreign Keys
  19. #
  20. # fk_rails_... (team_id => teams.id)
  21. # fk_rails_... (user_id => users.id)
  22. #
  23. 4 class Collaboration < ApplicationRecord
  24. 4 belongs_to :user
  25. 4 belongs_to :team
  26. 4 validates :user_id, presence: true, uniqueness: { scope: :team_id }
  27. 4 validates :team_id, presence: true
  28. end

app/models/identity.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: identities
  5. #
  6. # id :bigint not null, primary key
  7. # provider :string
  8. # uid :string
  9. # created_at :datetime not null
  10. # updated_at :datetime not null
  11. # user_id :bigint
  12. #
  13. # Indexes
  14. #
  15. # index_identities_on_provider_and_uid (provider,uid) UNIQUE
  16. # index_identities_on_provider_and_user_id (provider,user_id) UNIQUE
  17. # index_identities_on_user_id (user_id)
  18. #
  19. # Foreign Keys
  20. #
  21. # fk_rails_... (user_id => users.id)
  22. #
  23. # Identity model, for OAuth
  24. 3 class Identity < ApplicationRecord
  25. 3 belongs_to :user
  26. 3 validates :provider, presence: true
  27. 3 validates :uid,
  28. presence: true,
  29. uniqueness: { case_sensitive: false,
  30. scope: :provider,
  31. message: 'Account has been taken' }
  32. 3 validates :user_id,
  33. presence: true,
  34. uniqueness: { scope: :provider, message: 'Account has been taken' }
  35. end

app/models/landing_feedback.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: landing_feedbacks
  5. #
  6. # id :bigint not null, primary key
  7. # channel :string default(""), not null
  8. # interest :boolean default(TRUE), not null
  9. # smiley :string default(""), not null
  10. # created_at :datetime not null
  11. # updated_at :datetime not null
  12. #
  13. 4 class LandingFeedback < ApplicationRecord
  14. 4 CHANNEL = [
  15. 'Social Media',
  16. 'Search Engine',
  17. 'Newspaper',
  18. 'Recommended by others'
  19. ].freeze
  20. 4 validates :channel, presence: true, inclusion: { in: CHANNEL }
  21. 4 validates :interest, inclusion: { in: [true, false] }
  22. 4 validates :smiley, presence: true
  23. end

app/models/license.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: licenses
  5. #
  6. # id :bigint not null, primary key
  7. # plan :enum default("free"), not null
  8. # created_at :datetime not null
  9. # updated_at :datetime not null
  10. # user_id :bigint
  11. #
  12. # Indexes
  13. #
  14. # index_licenses_on_user_id (user_id) UNIQUE
  15. #
  16. # Foreign Keys
  17. #
  18. # fk_rails_... (user_id => users.id)
  19. #
  20. 4 class License < ApplicationRecord
  21. 4 MEMBER_LIMIT = { free: 10, pro: 30, ultimate: 1 / 0.0 }.freeze
  22. 4 enum plan: { free: 'free', pro: 'pro', ultimate: 'ultimate' }
  23. 4 belongs_to :user
  24. 4 validates :plan, presence: true
  25. 4 validates :user_id, presence: true, uniqueness: true
  26. 4 def member_limit
  27. 748 MEMBER_LIMIT[plan.to_sym]
  28. end
  29. end

app/models/newsletter.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: newsletters
  5. #
  6. # id :bigint not null, primary key
  7. # title :string not null
  8. # created_at :datetime not null
  9. # updated_at :datetime not null
  10. #
  11. 4 class Newsletter < ApplicationRecord
  12. # Equivalent to: has one :rich_text_content
  13. 4 has_rich_text :content
  14. 4 validates :title, presence: true
  15. 4 validates :content, presence: true
  16. 4 scope :unsubscription_by_newsletter, lambda {
  17. 11 find_by_sql(
  18. "SELECT newsletters.title,
  19. newsletters.created_at, COUNT(newsletter_feedbacks)
  20. as feedback_count FROM newsletters JOIN newsletter_feedbacks
  21. ON newsletter_feedbacks.created_at BETWEEN newsletters.created_at
  22. AND newsletters.created_at + interval '7 days' GROUP BY newsletters.id"
  23. )
  24. }
  25. 4 after_commit :send_newsletter, on: :create
  26. 4 private
  27. 4 def send_newsletter
  28. 28 NewsletterSubscription.all_subscribed_emails.each_slice(50) do |emails|
  29. 1 NewsletterMailer.send_newsletter(emails, self).deliver_later
  30. end
  31. end
  32. end

app/models/newsletter_feedback.rb

100.0% lines covered

15 relevant lines. 15 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: newsletter_feedbacks
  5. #
  6. # id :bigint not null, primary key
  7. # email :string default(""), not null
  8. # reasons :string default([]), not null, is an Array
  9. # created_at :datetime not null
  10. # updated_at :datetime not null
  11. # newsletter_subscription_id :bigint not null
  12. #
  13. # Indexes
  14. #
  15. # index_newsletter_feedbacks_on_newsletter_subscription_id (newsletter_subscription_id)
  16. #
  17. # Foreign Keys
  18. #
  19. # fk_rails_... (newsletter_subscription_id => newsletter_subscriptions.id)
  20. #
  21. 4 class NewsletterFeedback < ApplicationRecord
  22. 4 REASONS = { no_longer: 'I no longer want to receive these emails',
  23. too_frequent: 'The emails are too frequent',
  24. never_signed: 'I never signed up for the newsletter',
  25. inappropriate: 'The emails are inappropriate',
  26. not_interested: 'I am not interested anymore' }.freeze
  27. 4 belongs_to :newsletter_subscription, optional: true
  28. 4 validates :reasons,
  29. presence: true,
  30. array_inclusion: { in: REASONS.keys.map(&:to_s) << 'admin' }
  31. 4 before_save :validate_subscription_status
  32. 4 after_commit :change_subscribed_to_false
  33. 4 def self.count_reason(feedbacks)
  34. 5 feedbacks
  35. 2 .reduce([]) { |arr, fb| arr.concat(fb.reasons) }
  36. .group_by(&:itself)
  37. .transform_values(&:count)
  38. .transform_keys do |key|
  39. 2 REASONS.key?(key.to_sym) ? REASONS[key.to_sym] : key
  40. end
  41. end
  42. 4 private
  43. # Disallow submitting multiple feedback after unsubscription
  44. 4 def validate_subscription_status
  45. 21 throw :abort unless newsletter_subscription&.subscribed?
  46. end
  47. 4 def change_subscribed_to_false
  48. 19 newsletter_subscription&.unsubscribe
  49. end
  50. end

app/models/newsletter_subscription.rb

100.0% lines covered

16 relevant lines. 16 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: newsletter_subscriptions
  5. #
  6. # id :bigint not null, primary key
  7. # email :string not null
  8. # subscribed :boolean default(TRUE), not null
  9. # created_at :datetime not null
  10. # updated_at :datetime not null
  11. #
  12. # Indexes
  13. #
  14. # index_newsletter_subscriptions_on_email (email) UNIQUE
  15. #
  16. 4 class NewsletterSubscription < ApplicationRecord
  17. 4 has_many :newsletter_feedbacks, dependent: :nullify
  18. 4 validates :email,
  19. presence: true,
  20. uniqueness: true,
  21. format: { with: URI::MailTo::EMAIL_REGEXP }
  22. 42 scope :all_subscribed_emails, -> { where(subscribed: true).pluck(:email) }
  23. 6 scope :previously_subscribed, -> { where(subscribed: false) }
  24. 11 scope :subscribed_count, -> { all_subscribed_emails.size }
  25. 4 after_commit :send_welcome, on: :create
  26. 4 def self.subscribe(email)
  27. 12 record = where(email: email).first_or_initialize
  28. 12 record.update(subscribed: true) if record.new_record? || (record.persisted? && !record.subscribed?)
  29. end
  30. 4 def unsubscribe
  31. 23 return true unless subscribed?
  32. 22 update_columns(subscribed: false) # rubocop:disable Rails/SkipsModelValidations
  33. end
  34. 4 private
  35. 4 def send_welcome
  36. 173 NewsletterMailer.send_welcome(email).deliver_later
  37. end
  38. end

app/models/notification.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: notifications
  5. #
  6. # id :bigint not null, primary key
  7. # key :string default("")
  8. # notifiable_type :string not null
  9. # notifier_type :string not null
  10. # read :boolean default(FALSE), not null
  11. # created_at :datetime not null
  12. # updated_at :datetime not null
  13. # notifiable_id :bigint not null
  14. # notifier_id :bigint not null
  15. # user_id :bigint not null
  16. #
  17. # Indexes
  18. #
  19. # index_notifications_on_notifiable_type_and_notifiable_id (notifiable_type,notifiable_id)
  20. # index_notifications_on_notifier_type_and_notifier_id (notifier_type,notifier_id)
  21. # index_notifications_on_user_id (user_id)
  22. #
  23. # Foreign Keys
  24. #
  25. # fk_rails_... (user_id => users.id)
  26. #
  27. 4 class Notification < ApplicationRecord
  28. 4 belongs_to :user
  29. 4 belongs_to :notifiable, polymorphic: true
  30. 4 belongs_to :notifier, polymorphic: true
  31. 599 scope :unread, -> { where(read: false) }
  32. # TODO: tasks may not have avatar attachment, so the eager load wont work
  33. 6 scope :load_index, -> { includes(:notifier, notifiable: [{ avatar_attachment: :blob }]) }
  34. end

app/models/personality.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: personalities
  5. #
  6. # id :bigint not null, primary key
  7. # compatibilities :integer default([]), is an Array
  8. # energy :enum
  9. # mind :enum
  10. # nature :enum
  11. # tactic :enum
  12. # created_at :datetime not null
  13. # updated_at :datetime not null
  14. #
  15. # Indexes
  16. #
  17. # index_personalities_on_mind_and_energy_and_nature_and_tactic (mind,energy,nature,tactic) UNIQUE
  18. #
  19. 4 class Personality < ApplicationRecord
  20. TYPES = {
  21. 4 INTJ: 'Architect',
  22. INTP: 'Logician',
  23. ENTJ: 'Commander',
  24. ENTP: 'Debater',
  25. INFJ: 'Advocate',
  26. INFP: 'Mediator',
  27. ENFJ: 'Protagonist',
  28. ENFP: 'Campaigner',
  29. ISTJ: 'Logistician',
  30. ISFJ: 'Defender',
  31. ESTJ: 'Executive',
  32. ESFJ: 'Consul',
  33. ISTP: 'Virtuoso',
  34. ISFP: 'Adventurer',
  35. ESTP: 'Entrepreneur',
  36. ESFP: 'Entertainer'
  37. }.freeze
  38. 4 has_many :users, dependent: :nullify
  39. 4 validates :mind, presence: true, inclusion: { in: %w[I E] }
  40. 4 validates :energy, presence: true, inclusion: { in: %w[S N] }
  41. 4 validates :nature, presence: true, inclusion: { in: %w[T F] }
  42. 4 validates :tactic, presence: true, inclusion: { in: %w[J P] }
  43. 4 def to_type
  44. 35 TYPES[trait.to_sym]
  45. end
  46. 4 def trait
  47. 36 mind + energy + nature + tactic
  48. end
  49. end

app/models/project.rb

100.0% lines covered

48 relevant lines. 48 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: projects
  5. #
  6. # id :bigint not null, primary key
  7. # description :text default(""), not null
  8. # name :string(100) default(""), not null
  9. # status :enum default("active"), not null
  10. # visibility :boolean default(TRUE), not null
  11. # created_at :datetime not null
  12. # updated_at :datetime not null
  13. # category_id :bigint
  14. # user_id :bigint not null
  15. #
  16. # Indexes
  17. #
  18. # index_projects_on_category_id (category_id)
  19. # index_projects_on_user_id (user_id)
  20. #
  21. # Foreign Keys
  22. #
  23. # fk_rails_... (category_id => categories.id)
  24. # fk_rails_... (user_id => users.id)
  25. #
  26. 4 class Project < ApplicationRecord
  27. 4 include ActionPolicy::Behaviour
  28. # To prevent ORDER BY injection
  29. SORT_BY = {
  30. 4 created_asc: 'projects.created_at ASC',
  31. created_desc: 'projects.created_at DESC',
  32. name: 'projects.name ASC'
  33. }.freeze
  34. 4 enum status: { open: 'open', active: 'active', completed: 'completed' }
  35. 4 belongs_to :user
  36. 4 belongs_to :category
  37. 4 has_one_attached :avatar
  38. 4 has_many :appeals, dependent: :destroy
  39. 4 has_many :notifications, as: :notifiable, dependent: :destroy
  40. 4 has_many :notifications, as: :notifier, dependent: :destroy
  41. 4 has_many :activities, dependent: :destroy
  42. 4 has_many :tasks, dependent: :destroy
  43. 4 has_many :teams, dependent: :destroy
  44. 4 has_many :users, through: :teams
  45. 4 validates :name, presence: true, length: { maximum: 100 }
  46. 4 validates :status, presence: true
  47. 4 validates :avatar, content_type: %w[image/png image/jpg image/jpeg],
  48. size: { less_than: 200.kilobytes }
  49. 4 scope :search, lambda { |params|
  50. 52 with_attached_avatar
  51. .left_outer_joins(:category)
  52. .joins(:user)
  53. .select('projects.*')
  54. .select('categories.name AS category_name')
  55. .select('users.name AS user_name, users.email')
  56. .where('projects.status::text ~ ?', params[:status] || '')
  57. .where('categories.name ~ ?', params[:category] || '')
  58. .where('projects.name ~* :query OR projects.description ~* :query', query: params[:query] || '')
  59. .order(SORT_BY[params[:sort]&.to_sym] || SORT_BY[:created_desc])
  60. }
  61. 4 before_validation :validate_status_update,
  62. on: :update,
  63. if: :will_save_change_to_status?
  64. 4 before_save :validate_visibility_change, if: :will_save_change_to_visibility?
  65. 4 before_commit :create_unassigned_team, on: :create
  66. 4 after_create_commit :create_activity
  67. 4 after_update_commit :complete_activity, if: :saved_change_to_status?
  68. 4 def applicable?
  69. 68 open? && visibility
  70. end
  71. 4 def unassigned_team
  72. 230 teams.find_by(name: 'Unassigned')
  73. end
  74. 4 def check_users_limit
  75. 680 return if users.size < user.license.member_limit
  76. 3 errors.add(:base, 'Project is already full')
  77. 3 throw :abort
  78. end
  79. 4 private
  80. 4 def validate_status_update
  81. 7 if status_was != 'active' && status != 'active'
  82. 1 errors.add(:base, 'Invalid status change')
  83. 1 throw :abort
  84. end
  85. 6 check_users_limit if status == 'open'
  86. end
  87. 4 def validate_visibility_change
  88. 28 return if allowed_to?(:change_visibility?, self, context: { user: user })
  89. 1 errors.add(:base, 'Private project not available on free license')
  90. 1 throw :abort
  91. end
  92. 4 def create_unassigned_team
  93. 427 teams.create(name: 'Unassigned', team_size: 999).users << user
  94. end
  95. 4 def create_activity
  96. 427 activities.create(key: 'project/create', user: user)
  97. end
  98. 4 def complete_activity
  99. 5 activities.find_or_create_by(key: 'project/complete', user: user) if completed?
  100. end
  101. end

app/models/skill.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: skills
  5. #
  6. # id :bigint not null, primary key
  7. # name :string default(""), not null
  8. # created_at :datetime not null
  9. # updated_at :datetime not null
  10. # category_id :bigint
  11. #
  12. # Indexes
  13. #
  14. # index_skills_on_category_id (category_id)
  15. # index_skills_on_name (name) UNIQUE
  16. #
  17. # Foreign Keys
  18. #
  19. # fk_rails_... (category_id => categories.id)
  20. #
  21. 4 class Skill < ApplicationRecord
  22. 4 belongs_to :category
  23. 4 has_many :task_skills, dependent: :destroy
  24. 4 has_many :tasks, through: :task_skills
  25. 4 has_many :team_skills, dependent: :destroy
  26. 4 has_many :teams, through: :team_skills
  27. 4 has_many :user_skills, dependent: :destroy
  28. 4 has_many :users, through: :user_skills
  29. 4 validates :name, presence: true, uniqueness: { case_sensitive: false }
  30. 4 scope :with_category, lambda { |*order|
  31. 23 joins(:category)
  32. .select(:id, :name)
  33. .select('categories.name AS category_name')
  34. .order(order.presence || 'skills.name ASC')
  35. }
  36. end

app/models/task.rb

100.0% lines covered

36 relevant lines. 36 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: tasks
  5. #
  6. # id :bigint not null, primary key
  7. # description :text default(""), not null
  8. # name :string default(""), not null
  9. # percentage :integer default(0), not null
  10. # priority :enum default("medium"), not null
  11. # created_at :datetime not null
  12. # updated_at :datetime not null
  13. # project_id :bigint
  14. # user_id :bigint
  15. #
  16. # Indexes
  17. #
  18. # index_tasks_on_project_id (project_id)
  19. # index_tasks_on_user_id (user_id)
  20. #
  21. # Foreign Keys
  22. #
  23. # fk_rails_... (project_id => projects.id)
  24. # fk_rails_... (user_id => users.id)
  25. #
  26. 4 class Task < ApplicationRecord
  27. 4 enum priority: { high: 'high', medium: 'medium', low: 'low' }
  28. 4 belongs_to :project
  29. 4 belongs_to :user
  30. 4 has_many :task_users, dependent: :destroy
  31. 4 has_many :users, through: :task_users,
  32. before_add: :check_in_project,
  33. after_add: :send_assigned_notification,
  34. after_remove: :send_removed_notification
  35. 4 has_many :task_skills, dependent: :destroy
  36. 4 has_many :skills, through: :task_skills
  37. 4 validates :name, presence: true
  38. 4 validates :priority, presence: true
  39. 4 validates :percentage, presence: true, numericality: { only_integer: true },
  40. inclusion: {
  41. in: 0..100,
  42. message: 'Percentage should be between 0 and 100'
  43. }
  44. 4 after_update_commit :send_update_notification
  45. 4 after_destroy_commit :destroy_notifications
  46. 4 def completed?
  47. 6 percentage == 100
  48. end
  49. 4 def send_picked_up_notification
  50. 2 SendNotificationJob.perform_later([user], notification_params('task/picked'))
  51. end
  52. 4 private
  53. 4 def send_update_notification
  54. 13 if saved_change_to_percentage? && completed?
  55. 5 SendNotificationJob.perform_later([user], notification_params('task/completed'))
  56. 5 users.each do |user|
  57. 1 Activity.find_or_create_by(key: "task/#{id}", user: user, project: project)
  58. end
  59. else
  60. 8 SendNotificationJob.perform_later(users.to_a, notification_params('task/update'))
  61. end
  62. end
  63. 4 def send_assigned_notification(user)
  64. 21 SendNotificationJob.perform_later([user], notification_params('task/assigned'))
  65. end
  66. 4 def send_removed_notification(user)
  67. 1 SendNotificationJob.perform_later([user], notification_params('task/removed'))
  68. end
  69. 4 def destroy_notifications
  70. 5 Notification.delete_by(notifier: self)
  71. end
  72. 4 def notification_params(key)
  73. 37 { key: key, notifiable: project, notifier: self }
  74. end
  75. 4 def check_in_project(record)
  76. 22 return if record.in_project?(project)
  77. 1 errors[:base] << 'User is not in project'
  78. 1 throw(:abort)
  79. end
  80. end

app/models/task_skill.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: task_skills
  5. #
  6. # id :bigint not null, primary key
  7. # created_at :datetime not null
  8. # updated_at :datetime not null
  9. # skill_id :bigint not null
  10. # task_id :bigint not null
  11. #
  12. # Indexes
  13. #
  14. # index_task_skills_on_skill_id (skill_id)
  15. # index_task_skills_on_skill_id_and_task_id (skill_id,task_id) UNIQUE
  16. # index_task_skills_on_task_id (task_id)
  17. #
  18. # Foreign Keys
  19. #
  20. # fk_rails_... (skill_id => skills.id)
  21. # fk_rails_... (task_id => tasks.id)
  22. #
  23. 4 class TaskSkill < ApplicationRecord
  24. 4 belongs_to :task
  25. 4 belongs_to :skill
  26. 4 validates :skill_id, presence: true, uniqueness: { scope: :task_id }
  27. 4 validates :task_id, presence: true
  28. end

app/models/task_user.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: task_users
  5. #
  6. # id :bigint not null, primary key
  7. # created_at :datetime not null
  8. # updated_at :datetime not null
  9. # task_id :bigint not null
  10. # user_id :bigint not null
  11. #
  12. # Indexes
  13. #
  14. # index_task_users_on_task_id (task_id)
  15. # index_task_users_on_user_id (user_id)
  16. # index_task_users_on_user_id_and_task_id (user_id,task_id) UNIQUE
  17. #
  18. # Foreign Keys
  19. #
  20. # fk_rails_... (task_id => tasks.id)
  21. # fk_rails_... (user_id => users.id)
  22. #
  23. 4 class TaskUser < ApplicationRecord
  24. 4 belongs_to :task
  25. 4 belongs_to :user
  26. 4 validates :user_id, presence: true, uniqueness: { scope: :task_id }
  27. 4 validates :task_id, presence: true
  28. end

app/models/team.rb

95.65% lines covered

23 relevant lines. 22 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: teams
  5. #
  6. # id :bigint not null, primary key
  7. # name :string default(""), not null
  8. # team_size :integer not null
  9. # created_at :datetime not null
  10. # updated_at :datetime not null
  11. # project_id :bigint not null
  12. #
  13. # Indexes
  14. #
  15. # index_teams_on_name_and_project_id (name,project_id) UNIQUE
  16. # index_teams_on_project_id (project_id)
  17. #
  18. # Foreign Keys
  19. #
  20. # fk_rails_... (project_id => projects.id)
  21. #
  22. 4 class Team < ApplicationRecord
  23. 4 belongs_to :project
  24. # Join table
  25. 4 has_many :collaborations, dependent: :destroy
  26. 4 has_many :users, through: :collaborations, before_add: :check_users_limit,
  27. after_add: :send_notification,
  28. after_remove: :unassign_tasks
  29. 4 has_many :team_skills, dependent: :destroy
  30. 4 has_many :skills, through: :team_skills
  31. 4 validates :name,
  32. presence: true,
  33. uniqueness: { scope: :project_id, message: 'already exist' }
  34. 4 validates :team_size,
  35. presence: true,
  36. numericality: { greater_than_or_equal_to: 1 }
  37. 4 after_destroy_commit :destroy_notifications
  38. 4 private
  39. 4 def check_users_limit(_)
  40. 678 return project.check_users_limit unless users.size > team_size
  41. 1 errors[:base] << 'Team Size is smaller than total members'
  42. end
  43. 4 def send_notification(user)
  44. 676 return if name == 'Unassigned'
  45. 30 SendNotificationJob.perform_later(
  46. [user],
  47. { key: 'team', notifiable: project, notifier: self }
  48. )
  49. end
  50. 4 def unassign_tasks(user)
  51. 4 return if user.in_project?(project)
  52. 2 tasks = user.tasks.where(project: project)
  53. 2 tasks.each do |task|
  54. task.users.delete(user)
  55. end
  56. end
  57. 4 def destroy_notifications
  58. 3 Notification.delete_by(notifier: self)
  59. end
  60. end

app/models/team_skill.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: team_skills
  5. #
  6. # id :bigint not null, primary key
  7. # created_at :datetime not null
  8. # updated_at :datetime not null
  9. # skill_id :bigint not null
  10. # team_id :bigint not null
  11. #
  12. # Indexes
  13. #
  14. # index_team_skills_on_skill_id (skill_id)
  15. # index_team_skills_on_skill_id_and_team_id (skill_id,team_id) UNIQUE
  16. # index_team_skills_on_team_id (team_id)
  17. #
  18. # Foreign Keys
  19. #
  20. # fk_rails_... (skill_id => skills.id)
  21. # fk_rails_... (team_id => teams.id)
  22. #
  23. 4 class TeamSkill < ApplicationRecord
  24. 4 belongs_to :team
  25. 4 belongs_to :skill
  26. 4 validates :skill_id, presence: true, uniqueness: { scope: :team_id }
  27. 4 validates :team_id, presence: true
  28. end

app/models/user.rb

100.0% lines covered

55 relevant lines. 55 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: users
  5. #
  6. # id :bigint not null, primary key
  7. # admin :boolean default(FALSE), not null
  8. # birthdate :date
  9. # email :string(254) default(""), not null
  10. # encrypted_password :string default(""), not null
  11. # name :string(255) default(""), not null
  12. # password_automatically_set :boolean default(FALSE), not null
  13. # remember_created_at :datetime
  14. # reset_password_sent_at :datetime
  15. # reset_password_token :string
  16. # created_at :datetime not null
  17. # updated_at :datetime not null
  18. # personality_id :bigint
  19. #
  20. # Indexes
  21. #
  22. # index_users_on_email (email) UNIQUE
  23. # index_users_on_personality_id (personality_id)
  24. # index_users_on_reset_password_token (reset_password_token) UNIQUE
  25. #
  26. # Foreign Keys
  27. #
  28. # fk_rails_... (personality_id => personalities.id)
  29. #
  30. 4 class User < ApplicationRecord
  31. 4 devise :database_authenticatable, :registerable,
  32. :recoverable, :rememberable, :validatable,
  33. :omniauthable, omniauth_providers: Devise.omniauth_providers
  34. 4 belongs_to :personality, optional: true
  35. 4 has_one_attached :avatar
  36. 4 has_one :license, dependent: :destroy
  37. 4 has_many :activities, dependent: :destroy
  38. 4 has_many :identities, dependent: :destroy
  39. 4 has_many :notifications, dependent: :destroy
  40. 4 has_many :projects, dependent: :destroy
  41. 4 has_many :appeals, dependent: :destroy
  42. # Join table
  43. 4 has_many :collaborations, dependent: :destroy
  44. 4 has_many :teams, through: :collaborations
  45. 4 has_many :task_users, dependent: :destroy
  46. 4 has_many :tasks, through: :task_users
  47. 4 has_many :user_skills, dependent: :destroy
  48. 4 has_many :skills, through: :user_skills
  49. 4 validates :email, presence: true, uniqueness: true, length: { maximum: 254 },
  50. format: { with: URI::MailTo::EMAIL_REGEXP }
  51. 4 validates :name, length: { maximum: 255 }
  52. 4 validates :name, presence: true, on: :update
  53. 4 validates :avatar, content_type: %w[image/png image/jpg image/jpeg],
  54. size: { less_than: 200.kilobytes }
  55. 4 validate :provided_birthdate, on: :update
  56. 4 before_create :build_license, :build_activities
  57. 4 after_update_commit :disable_password_automatically_set,
  58. if: [:saved_change_to_encrypted_password?,
  59. 3 proc { |user| user.password_automatically_set }]
  60. 4 def self.sign_in_omniauth(auth)
  61. 12 Identity.where(provider: auth.provider, uid: auth.uid).first_or_create(
  62. user: create(
  63. email: auth.info.email,
  64. name: auth.info.name,
  65. password: Devise.friendly_token[0, 20],
  66. password_automatically_set: true
  67. )
  68. )
  69. end
  70. 4 def connect_omniauth(auth)
  71. 6 identities.create(provider: auth.provider, uid: auth.uid)
  72. end
  73. 4 def oauth_provider_connected?(provider = nil)
  74. 69 identities.exists?(provider: provider)
  75. end
  76. 4 def newsletter_subscribed?
  77. 6 NewsletterSubscription.find_by(email: email)&.subscribed?
  78. end
  79. 4 def empty_compatibility_data?
  80. 30 personality_id.blank? && user_skills.exists?
  81. end
  82. # TODO: use has_many :members, through: :teams
  83. 4 def in_project?(project)
  84. 237 teams&.exists?(project: project) || project&.user == self
  85. end
  86. 4 def notifications
  87. 597 super.order(id: :desc, created_at: :desc)
  88. end
  89. 4 def compatible_with?(target_user)
  90. 17 personality.compatibilities[target_user.personality_id - 1]
  91. end
  92. 4 def self.teams_data(project)
  93. 33 includes(:user_skills)
  94. .joins('LEFT OUTER JOIN task_users ON task_users.user_id = users.id')
  95. .joins('LEFT OUTER JOIN tasks ON tasks.id = task_users.task_id AND tasks.percentage != 100')
  96. .joins(:teams).where(teams: { project: project })
  97. .select('users.id, users.name, users.email, users.personality_id')
  98. .select('teams.id as team_id, COUNT(tasks.id) as count')
  99. .group('users.id, teams.id')
  100. end
  101. 4 private
  102. 4 def build_activities
  103. 1171 activities.build(key: 'user/create')
  104. end
  105. 4 def disable_password_automatically_set
  106. 1 update_columns(password_automatically_set: false) # rubocop:disable Rails/SkipsModelValidations
  107. end
  108. 4 def provided_birthdate
  109. 124 return if birthdate_before_type_cast.blank?
  110. 5 dob = Date.new(
  111. birthdate_before_type_cast[1], # year
  112. birthdate_before_type_cast[2], # month
  113. birthdate_before_type_cast[3] # day of month
  114. )
  115. 4 return errors.add(:birthdate, 'must be before the account creation date') if dob >= created_at.to_date
  116. 3 age = ((Time.current - dob.to_time) / 1.year.seconds).floor
  117. 3 return if age.between?(6, 80)
  118. 2 errors.add(:age, 'must be between 6 and 80 years old')
  119. rescue StandardError
  120. 1 errors.add(:date, 'is invalid')
  121. end
  122. end

app/models/user_skill.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # == Schema Information
  3. #
  4. # Table name: user_skills
  5. #
  6. # id :bigint not null, primary key
  7. # created_at :datetime not null
  8. # updated_at :datetime not null
  9. # skill_id :bigint not null
  10. # user_id :bigint not null
  11. #
  12. # Indexes
  13. #
  14. # index_user_skills_on_skill_id (skill_id)
  15. # index_user_skills_on_skill_id_and_user_id (skill_id,user_id) UNIQUE
  16. # index_user_skills_on_user_id (user_id)
  17. #
  18. # Foreign Keys
  19. #
  20. # fk_rails_... (skill_id => skills.id)
  21. # fk_rails_... (user_id => users.id)
  22. #
  23. 3 class UserSkill < ApplicationRecord
  24. 3 belongs_to :user
  25. 3 belongs_to :skill
  26. 3 validates :skill_id, presence: true, uniqueness: { scope: :user_id }
  27. 3 validates :user_id, presence: true
  28. end

app/policies/activity_policy.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Class for Activity Policies
  3. 3 class ActivityPolicy < ApplicationPolicy
  4. 3 relation_scope do |scope|
  5. 5 task = scope.joins(:project)
  6. .where("activities.key LIKE '%task%'")
  7. .select('projects.name AS name, projects.id AS project_id')
  8. .select('COUNT(projects.id) as count')
  9. .group('projects.id')
  10. 5 project = scope.joins(:project)
  11. .where("activities.key NOT LIKE '%task%'")
  12. .select('activities.id, activities.created_at, key')
  13. .select('projects.name AS name, projects.id AS project_id')
  14. .order('activities.created_at DESC')
  15. 5 user = scope.where("activities.key LIKE '%user%'")
  16. .select('activities.id, activities.created_at, activities.key')
  17. 5 return task, (project + user)
  18. end
  19. end

app/policies/admin_policy.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Policy for Metrics controller
  3. 3 class AdminPolicy < ApplicationPolicy
  4. 3 default_rule :manage?
  5. 3 alias_rule :index?, :create?, :new?, to: :manage?
  6. 3 def manage?
  7. 210 user.admin?
  8. end
  9. end

app/policies/appeal/application_policy.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 class Appeal::ApplicationPolicy < Appeal::BasePolicy
  3. 3 def create?
  4. 9 record.project.applicable? && !project_owner?
  5. end
  6. 3 def accept?
  7. 7 !owner? && project_owner? || user.admin?
  8. end
  9. end

app/policies/appeal/base_policy.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 class Appeal::BasePolicy < ApplicationPolicy
  3. 3 def index?
  4. # Record is a project
  5. 14 owner? || user.admin?
  6. end
  7. 3 def destroy?
  8. 12 owner? || project_owner? || user.admin?
  9. end
  10. end

app/policies/appeal/invitation_policy.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 class Appeal::InvitationPolicy < Appeal::BasePolicy
  3. 3 def create?
  4. 8 project_owner? || user.admin?
  5. end
  6. 3 def accept?
  7. 9 owner? && !project_owner? || user.admin?
  8. end
  9. end

app/policies/application_policy.rb

100.0% lines covered

6 relevant lines. 6 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Base class for application policies
  3. 4 class ApplicationPolicy < ActionPolicy::Base
  4. 4 authorize :user, allow_nil: true
  5. 4 def owner?
  6. 383 record.user_id == user&.id
  7. end
  8. 4 def project_owner?
  9. 84 record.project.user_id == user&.id
  10. end
  11. end

app/policies/notification_policy.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Class for Notification policies
  3. 2 class NotificationPolicy < ApplicationPolicy
  4. 2 default_rule :manage?
  5. 2 def manage?
  6. 7 owner?
  7. end
  8. end

app/policies/project_policy.rb

100.0% lines covered

30 relevant lines. 30 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Class for Project policies
  3. 4 class ProjectPolicy < ApplicationPolicy
  4. 4 alias_rule :update?, to: :manage?
  5. 4 relation_scope(:own) do |scope|
  6. 22 scope.where(user: user)
  7. end
  8. 4 relation_scope(:joined) do |scope|
  9. 2 scope.where(id: user.teams.pluck(:project_id)).where.not(user: user)
  10. end
  11. 4 relation_scope(:explore) do |scope|
  12. 6 next scope if user&.admin
  13. 5 scope.where(visibility: true).or(scope.where(user: user)).distinct
  14. end
  15. 4 relation_scope(:profile_owned) do |scope, profile_owner: nil|
  16. 26 data = scope.joins(teams: :collaborations).where(user: profile_owner).distinct
  17. 26 next data if user&.admin || user&.id == profile_owner.id
  18. 4 data.where("visibility = 't' OR collaborations.user_id = ?", user&.id)
  19. end
  20. 4 relation_scope(:profile_joined) do |scope, profile_owner: nil|
  21. 7 data = scope.joins(teams: :collaborations)
  22. .where(id: profile_owner.teams.pluck(:project_id))
  23. .where.not(user: profile_owner).distinct
  24. 7 next data if user&.admin || user&.id == profile_owner.id
  25. 2 data.where("visibility = 't' OR collaborations.user_id = ?", user&.id)
  26. end
  27. 4 def show?
  28. 75 record.visibility || manage? || user&.in_project?(record) ||
  29. record.appeals.invitation.where(user: user).exists?
  30. end
  31. 4 def manage?
  32. 251 (owner? || user&.admin?) && !record.completed?
  33. end
  34. 4 def change_status?
  35. 12 owner? || user&.admin?
  36. end
  37. 4 def count?
  38. 37 user&.in_project?(record) || user&.admin?
  39. end
  40. 4 def change_visibility?
  41. 94 user&.admin? || (!user.license.free? && (owner? || record.new_record?))
  42. end
  43. 4 def create_task?
  44. 79 manage? || (!record.unassigned_team.users.include?(user) &&
  45. 9 record.teams.any? { |team| team.users.include?(user) })
  46. end
  47. end

app/policies/task_policy.rb

100.0% lines covered

18 relevant lines. 18 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Class for Task policies
  3. 3 class TaskPolicy < ApplicationPolicy
  4. 3 scope_matcher :user, User
  5. 3 default_rule :manage?
  6. 3 relation_scope(:assigned) do |scope|
  7. 13 ids = scope.left_outer_joins(:task_users).where(task_users: { user_id: user&.id }).pluck('task_users.task_id').uniq
  8. 13 scope.where(id: ids)
  9. end
  10. 3 relation_scope(:unassigned) do |scope|
  11. 5 scope.left_outer_joins(:task_users).where(task_users: { user_id: nil })
  12. end
  13. 3 relation_scope(:active) do |scope|
  14. 10 scope.where('percentage != 100')
  15. end
  16. 3 relation_scope(:completed) do |scope|
  17. 3 scope.where('percentage = 100')
  18. end
  19. 3 def manage?
  20. 49 owner? || user&.admin? || project_owner?
  21. end
  22. 3 def set_percentage?
  23. 15 manage? || record.users.include?(user)
  24. end
  25. 3 def assign_self?
  26. 6 project_owner? || record.project.users.include?(user)
  27. end
  28. end

app/policies/team_policy.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Class for Team policies
  3. 3 class TeamPolicy < ApplicationPolicy
  4. 3 alias_rule :edit?, :update?, to: :access_team?
  5. 3 default_rule :manage?
  6. 3 def show?
  7. 12 user&.admin? || record.project.visibility? || user&.in_project?(record.project)
  8. end
  9. 3 def access_team?
  10. 19 record.name != 'Unassigned' && manage?
  11. end
  12. 3 def manage?
  13. 31 user&.admin? || project_owner?
  14. end
  15. end

app/policies/user_policy.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Class for User policies
  3. 3 class UserPolicy < ApplicationPolicy
  4. 3 relation_scope(:assignee) do |scope, team_id: nil, project: nil|
  5. 45 next scope.map { |s| [s.name, s.id] } if user&.admin? || project.user_id == user&.id
  6. 9 scope.includes(:teams).where(teams: { id: team_id }).map { |s| [s.name, s.id] }
  7. end
  8. end

app/serializers/notification_blueprint.rb

100.0% lines covered

13 relevant lines. 13 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 class NotificationBlueprint < Blueprinter::Base
  3. 3 extend ActionView::Helpers::DateHelper
  4. 3 fields :id, :key, :read
  5. 3 field :time_ago do |notification|
  6. 9 "#{time_ago_in_words(notification.created_at)} ago"
  7. end
  8. 3 association :notifiable, blueprint: lambda { |notifiable|
  9. 9 case notifiable.class.name
  10. when 'User'
  11. 8 UserBlueprint
  12. when 'Project'
  13. 1 ProjectBlueprint
  14. end
  15. }, view: :notifiable
  16. 3 association :notifier, blueprint: lambda { |notifier|
  17. 9 case notifier.class.name
  18. when 'Project'
  19. 7 ProjectBlueprint
  20. when 'Team', 'Task'
  21. 2 ProjectObjectBlueprint
  22. end
  23. }, view: :notifier
  24. end

app/serializers/project_blueprint.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class ProjectBlueprint < Blueprinter::Base
  3. 1 extend ActionView::Context
  4. 1 extend ActionView::Helpers::TagHelper
  5. 1 extend ActionView::Helpers::AssetTagHelper
  6. 1 extend AvatarHelper
  7. 1 field :name
  8. 1 view :notifiable do
  9. 1 field :icon do |project|
  10. 3 "<div class='project-avatar'>#{project_icon(project)}</div>"
  11. end
  12. end
  13. 1 view :notifier do
  14. 1 field :link do |project|
  15. 8 "/projects/#{project.id}"
  16. end
  17. end
  18. end

app/serializers/project_object_blueprint.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 class ProjectObjectBlueprint < Blueprinter::Base
  3. 1 field :name
  4. 1 view :notifier do
  5. 5 field(:link) { |object| "/projects/#{object.project.id}" }
  6. end
  7. end

app/serializers/task_blueprint.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 class TaskBlueprint < Blueprinter::Base
  3. 3 extend AvatarHelper
  4. 3 fields :id, :description, :name, :percentage, :priority
  5. 3 field :owner_id do |task|
  6. 28 task.user.id
  7. end
  8. 3 field :owner_name do |task|
  9. 28 task.user.name
  10. end
  11. 3 field :skills do |task|
  12. 28 task.skills.pluck(:name)
  13. end
  14. 3 association :users, name: :assignees, view: :assignee, blueprint: UserBlueprint
  15. end

app/serializers/user_blueprint.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 class UserBlueprint < Blueprinter::Base
  3. 3 extend AvatarHelper
  4. 3 field :name
  5. 3 view :notifiable do
  6. 3 field :icon do |user|
  7. "<figure class='image is-48x48 user-avatar-container mr-4'>"\
  8. 10 "<img src=#{user_avatar(user)}>"\
  9. '</figure>'
  10. end
  11. end
  12. 3 view :assignee do
  13. 3 field :id
  14. 3 field :icon do |user|
  15. 7 user_avatar(user)
  16. end
  17. end
  18. end

app/services/compatibility_compute.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 class CompatibilityCompute < ComputeService
  3. 3 def initialize(teams, unassigned)
  4. 16 @teams = teams
  5. 16 @unassigned = unassigned
  6. end
  7. 3 def call(target)
  8. 22 if @unassigned.id == target['team_id']
  9. 20 best_team?(target, @teams)
  10. else
  11. 2 team = @teams.where(id: target['team_id']).first
  12. 2 team_compatibility(target, team, team.users).round(2).to_s
  13. end
  14. end
  15. end

app/services/compute_service.rb

100.0% lines covered

37 relevant lines. 37 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 3 class ComputeService
  3. WEIGHTAGE = {
  4. 3 "balance": [1.0, 1.0],
  5. "cohesion": [1.5, 0.5],
  6. "efficient": [0.5, 1.5]
  7. }.freeze
  8. 3 def best_team?(target, teams, u_list = nil)
  9. 30 user = u_list.blank? ? target : User.find(target['id'])
  10. 30 return '' if user.empty_compatibility_data?
  11. 28 results = compare_team(user, teams, u_list)
  12. 54 best = results.max_by { |x| x[1] }
  13. 28 best.blank? || best[1] <= 1.0 ? '' : "(#{best[1].round(2)}) Team #{best[0]}"
  14. end
  15. 3 def compare_team(target, teams, u_list = nil, ignore_empty = false, weightage = [1.0, 1.0])
  16. 45 results = {}
  17. 45 teams.each do |team|
  18. 71 next if valid_team_compare(team, u_list, ignore_empty)
  19. 39 users = prepare_team_compare(team, u_list)
  20. 39 results[team.name] = team_compatibility(target, team, users, weightage) if users.size < team.team_size
  21. end
  22. 45 results
  23. end
  24. 3 def team_compatibility(target, team, users, weightage = [1.0, 1.0])
  25. 58 1.0 * team_personality_score(target, users, weightage[0]) *
  26. teamskill_score(team.team_skills.pluck(:skill_id),
  27. target.user_skills.pluck(:skill_id), weightage[1])
  28. end
  29. 3 def teamskill_score(t_skill, u_skill, weightage = 1.0)
  30. 85 return 1.0 if t_skill.empty? || u_skill.empty?
  31. 68 1.0 + (weightage *
  32. (t_skill.size - (t_skill - u_skill).size) / (t_skill.size * 5.0))
  33. end
  34. 3 def team_personality_score(target, users, weightage = 1.0)
  35. 62 return 1.0 if target.personality_id.blank?
  36. 44 user_score = 0
  37. 44 counter = 0
  38. 44 users.each do |usr|
  39. 31 next if usr.personality_id.blank? || target == usr
  40. 17 user_score += target.compatible_with?(usr)
  41. 17 counter += 1.0
  42. end
  43. 44 counter.zero? ? 1.0 : 1.0 + (weightage * (user_score / (counter * 10.0)))
  44. end
  45. 3 private
  46. 3 def valid_team_compare(team, u_list, ignore_empty)
  47. 71 return true if team.name == 'Unassigned'
  48. 39 return false if u_list.nil?
  49. 18 !u_list.key?(team.id.to_s) || (ignore_empty && u_list[team.id.to_s].empty?)
  50. end
  51. 3 def prepare_team_compare(team, list)
  52. 39 return team.users if list.blank?
  53. 18 list[team.id.to_s].empty? ? [] : User.find(list[team.id.to_s].pluck('id'))
  54. end
  55. end

app/services/recompute.rb

100.0% lines covered

12 relevant lines. 12 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Recompute < ComputeService
  3. 2 def initialize(teams, unassigned, mode = 'balance')
  4. 14 @teams = teams
  5. 14 @unassigned = unassigned
  6. 14 @weightage = WEIGHTAGE[mode.to_sym]
  7. end
  8. 2 def call(target_team, u_list)
  9. 16 members = u_list[target_team.id.to_s]
  10. 16 if @unassigned == target_team
  11. 12 members.map { |u| [u['id'], best_team?(u, @teams, u_list)] }
  12. else
  13. 10 users = User.includes(:user_skills).find(members.pluck('id'))
  14. 10 users.map do |u|
  15. 13 [u.id, team_compatibility(u, target_team, users, @weightage).round(2).to_s]
  16. end
  17. end
  18. end
  19. end

app/services/suggest.rb

100.0% lines covered

62 relevant lines. 62 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 2 class Suggest < ComputeService
  3. 2 def initialize(users, teams, unassigned, mode = 'balance')
  4. 17 @users = split_users(users)
  5. 17 @teams = teams
  6. 17 @unassigned = unassigned
  7. 17 @mode = mode
  8. 17 @weightage = WEIGHTAGE[mode.to_sym]
  9. end
  10. 2 def call
  11. 17 suggestion, leftover = allocate_first_users
  12. 17 is_coh = @mode == 'cohesion'
  13. 17 [leftover, @users[is_coh ? 2 : 1], @users[is_coh ? 1 : 2]].each do |data|
  14. 51 suggestion = match(data, suggestion)
  15. end
  16. 17 leftover = @users.flatten - suggestion.values.flatten
  17. 17 suggestion[@unassigned.id.to_s] = leftover
  18. 17 suggestion
  19. end
  20. 2 private
  21. 2 def split_users(users)
  22. 17 [users.where.not(user_skills: { user_id: nil })
  23. .where.not(users: { personality_id: nil }).group('user_skills.id'),
  24. users.where.not(user_skills: { user_id: nil })
  25. .where(personality_id: nil).group('user_skills.id'),
  26. users.where(user_skills: { user_id: nil })
  27. .where.not(users: { personality_id: nil })
  28. .group('user_skills.id'),
  29. users.where(user_skills: { user_id: nil }, users: { personality_id: nil })
  30. .group('user_skills.id')]
  31. end
  32. 2 def allocate_first_users
  33. 17 return [@teams.map { |t| [t.id, []] }.to_h, []] if @users[0].blank?
  34. 17 top_mem = first_users_loop
  35. 34 [top_mem.map { |k, v| v.nil? ? [k, []] : [k, [v]] }.to_h, @users[0] - top_mem.values]
  36. end
  37. 2 def first_users_loop
  38. 17 skill_comp = prepare_skill_comp
  39. 17 top_mem, conflict = get_top_mem(skill_comp)
  40. 17 until conflict.blank?
  41. 3 winner = find_winner(top_mem, skill_comp, conflict)
  42. 9 skill_comp.each { |id, u| u.delete(conflict) if id != winner }
  43. 3 top_mem, conflict = get_top_mem(skill_comp)
  44. end
  45. 17 top_mem
  46. end
  47. 2 def prepare_skill_comp
  48. 17 skill_comp = @teams.map do |t|
  49. 17 t_s = t.team_skills.pluck(:skill_id)
  50. 17 data = prepare_skill_comp_loop(t_s)
  51. 40 [t.id.to_s, data.sort_by { |_, v| -v }.to_h]
  52. end
  53. 17 skill_comp.to_h
  54. end
  55. 2 def prepare_skill_comp_loop(t_s)
  56. 17 @users[0].map do |u|
  57. 23 [u, teamskill_score(t_s, u.user_skills.pluck(:skill_id)) * @weightage[1]]
  58. end
  59. end
  60. 2 def get_top_mem(skill_comp)
  61. 43 top_mem = skill_comp.collect { |k, v| [k, v.first&.[](0)] }.to_h
  62. 43 conflicts = top_mem.values.select { |e| top_mem.values.count(e) > 1 }.uniq
  63. 20 [top_mem, conflicts[0]]
  64. end
  65. 2 def find_winner(top_mem, skill_comp, contested_user_id)
  66. 9 conflict_ids = top_mem.select { |_, v| v == contested_user_id }.to_h
  67. 9 conflict_teams = skill_comp.select { |k, _| conflict_ids.key?(k) }
  68. 3 resolve(conflict_teams, contested_user_id)
  69. end
  70. # if more than one team has similar top users, resolve by comparing total score
  71. # skill_comps that are competing are parsed in
  72. # returns the lowest team with lowest skill score
  73. 2 def resolve(conflict_teams, contested_user_id)
  74. 3 selected = { id: 0, score: Float::MAX }
  75. 3 conflict_teams.each do |team_id, users_scores|
  76. 6 score = users_scores.values.sum - users_scores[contested_user_id]
  77. 6 selected = { id: team_id, score: score } if score < selected[:score]
  78. end
  79. 3 selected[:id]
  80. end
  81. 2 def match(data, suggestion)
  82. 51 data.each do |u|
  83. 23 compare_team(u, @teams, suggestion, @mode == 'cohesion', @weightage).sort_by { |_, v| -v }.each do |k, v|
  84. 10 team = @teams.where(name: k).first
  85. 10 target = suggestion[team.id.to_s]
  86. 10 next if target.size == team.team_size || v <= 1.0
  87. 3 target.push(u)
  88. 3 break
  89. end
  90. end
  91. 51 suggestion
  92. end
  93. end

app/validators/array_inclusion_validator.rb

100.0% lines covered

4 relevant lines. 4 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. # Validator for inclusion of array values
  3. 4 class ArrayInclusionValidator < ActiveModel::EachValidator
  4. 4 def validate_each(record, attribute, value)
  5. 60 return if value.present? && value.all? { |val| options[:in].include?(val) }
  6. 9 record.errors[attribute] << (options[:message] || 'is not included in the list')
  7. end
  8. end