loading
Generated 2025-06-22T21:12:17+01:00

All Files ( 88.4% covered at 49.23 hits/line )

57 files in total.
1431 relevant lines, 1265 lines covered and 166 lines missed. ( 88.4% )
391 total branches, 279 branches covered and 112 branches missed. ( 71.36% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/controllers/admin/admin_controller.rb 100.00 % 18 9 9 0 12.67 100.00 % 2 2 0
app/controllers/admin/bands_controller.rb 0.00 % 45 37 0 37 0.00 100.00 % 0 0 0
app/controllers/admin/impersonation_controller.rb 100.00 % 30 15 15 0 2.53 100.00 % 2 2 0
app/controllers/admin/users_controller.rb 100.00 % 63 29 29 0 5.48 90.00 % 10 9 1
app/controllers/application_controller.rb 85.33 % 169 75 64 11 107.29 71.88 % 32 23 9
app/controllers/bands_controller.rb 96.49 % 109 57 55 2 6.09 90.00 % 20 18 2
app/controllers/calendars_controller.rb 100.00 % 13 7 7 0 6.71 100.00 % 0 0 0
app/controllers/concerns/access_permissions.rb 100.00 % 30 19 19 0 68.00 100.00 % 4 4 0
app/controllers/device_access_controller.rb 92.86 % 32 14 13 1 10.64 100.00 % 2 2 0
app/controllers/events_controller.rb 98.00 % 97 50 49 1 23.90 91.67 % 12 11 1
app/controllers/ical_feeds_controller.rb 100.00 % 25 10 10 0 6.50 100.00 % 0 0 0
app/controllers/linked_devices_controller.rb 84.06 % 149 69 58 11 6.00 65.00 % 20 13 7
app/controllers/members_controller.rb 88.89 % 94 45 40 5 4.62 70.00 % 10 7 3
app/controllers/permissions_controller.rb 77.32 % 186 97 75 22 7.26 48.94 % 47 23 24
app/controllers/sessions_controller.rb 100.00 % 40 28 28 0 57.11 100.00 % 6 6 0
app/controllers/user_mails_controller.rb 100.00 % 38 19 19 0 5.53 100.00 % 6 6 0
app/controllers/users/registration_controller.rb 100.00 % 64 34 34 0 2.65 85.71 % 14 12 2
app/controllers/users_controller.rb 100.00 % 95 41 41 0 5.76 85.71 % 14 12 2
app/helpers/application_helper.rb 98.51 % 156 67 66 1 175.24 91.30 % 23 21 2
app/helpers/bands_helper.rb 100.00 % 2 1 1 0 1.00 100.00 % 0 0 0
app/helpers/events_helper.rb 100.00 % 51 32 32 0 54.75 100.00 % 25 25 0
app/helpers/linked_devices_helper.rb 60.87 % 45 23 14 9 3.17 46.15 % 13 6 7
app/helpers/members_helper.rb 100.00 % 2 1 1 0 1.00 100.00 % 0 0 0
app/helpers/permissions_helper.rb 47.83 % 182 69 33 36 23.87 24.24 % 33 8 25
app/helpers/sessions_helper.rb 100.00 % 2 1 1 0 1.00 100.00 % 0 0 0
app/helpers/users/registration_helper.rb 100.00 % 2 1 1 0 1.00 100.00 % 0 0 0
app/jobs/application_job.rb 100.00 % 7 1 1 0 1.00 100.00 % 0 0 0
app/jobs/send_mail_job.rb 66.67 % 9 6 4 2 0.67 100.00 % 0 0 0
app/lib/form_builders/nice_form_builder.rb 100.00 % 233 91 91 0 41.51 72.73 % 22 16 6
app/mailers/application_mailer.rb 100.00 % 3 2 2 0 7.00 100.00 % 0 0 0
app/mailers/test_mailer.rb 0.00 % 11 10 0 10 0.00 100.00 % 0 0 0
app/mailers/user_mailer.rb 87.50 % 42 16 14 2 2.81 50.00 % 2 1 1
app/models/application_record.rb 100.00 % 3 2 2 0 1.00 100.00 % 0 0 0
app/models/band.rb 100.00 % 39 22 22 0 11.05 50.00 % 2 1 1
app/models/band_member.rb 100.00 % 5 4 4 0 16.00 100.00 % 0 0 0
app/models/bands_audit.rb 100.00 % 6 4 4 0 7.00 100.00 % 0 0 0
app/models/concerns/auditable.rb 100.00 % 42 19 19 0 8.95 100.00 % 0 0 0
app/models/concerns/random_id.rb 100.00 % 16 9 9 0 819.78 50.00 % 2 1 1
app/models/confirmation_token.rb 100.00 % 15 8 8 0 9.88 100.00 % 0 0 0
app/models/current.rb 100.00 % 34 19 19 0 525.32 75.00 % 4 3 1
app/models/event.rb 97.30 % 73 37 36 1 42.08 70.00 % 10 7 3
app/models/event_band.rb 100.00 % 5 4 4 0 1.00 100.00 % 0 0 0
app/models/events_audit.rb 100.00 % 5 4 4 0 9.00 100.00 % 0 0 0
app/models/linked_device.rb 98.46 % 132 65 64 1 60.71 70.00 % 10 7 3
app/models/linked_device_linkable.rb 100.00 % 9 4 4 0 14.00 100.00 % 0 0 0
app/models/member.rb 71.88 % 71 32 23 9 20.53 0.00 % 8 0 8
app/models/member_skill.rb 100.00 % 6 4 4 0 16.00 100.00 % 0 0 0
app/models/members_audit.rb 100.00 % 5 4 4 0 5.00 100.00 % 0 0 0
app/models/permission.rb 89.58 % 86 48 43 5 67.58 88.89 % 18 16 2
app/models/skill.rb 100.00 % 6 4 4 0 1.00 100.00 % 0 0 0
app/models/user.rb 100.00 % 80 34 34 0 125.65 100.00 % 6 6 0
app/models/user_mail.rb 100.00 % 45 23 23 0 1.74 75.00 % 4 3 1
app/queries/band_permission_query.rb 100.00 % 134 17 17 0 70.76 100.00 % 0 0 0
app/queries/event_permission_query.rb 100.00 % 85 13 13 0 133.31 100.00 % 0 0 0
app/queries/member_permission_query.rb 100.00 % 157 18 18 0 64.72 100.00 % 0 0 0
app/services/device_access_service.rb 100.00 % 27 11 11 0 7.55 100.00 % 2 2 0
app/services/ical_generator_service.rb 100.00 % 81 46 46 0 11.89 100.00 % 6 6 0

Controllers ( 86.26% covered at 22.61 hits/line )

18 files in total.
655 relevant lines, 565 lines covered and 90 lines missed. ( 86.26% )
201 total branches, 150 branches covered and 51 branches missed. ( 74.63% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/controllers/admin/admin_controller.rb 100.00 % 18 9 9 0 12.67 100.00 % 2 2 0
app/controllers/admin/bands_controller.rb 0.00 % 45 37 0 37 0.00 100.00 % 0 0 0
app/controllers/admin/impersonation_controller.rb 100.00 % 30 15 15 0 2.53 100.00 % 2 2 0
app/controllers/admin/users_controller.rb 100.00 % 63 29 29 0 5.48 90.00 % 10 9 1
app/controllers/application_controller.rb 85.33 % 169 75 64 11 107.29 71.88 % 32 23 9
app/controllers/bands_controller.rb 96.49 % 109 57 55 2 6.09 90.00 % 20 18 2
app/controllers/calendars_controller.rb 100.00 % 13 7 7 0 6.71 100.00 % 0 0 0
app/controllers/concerns/access_permissions.rb 100.00 % 30 19 19 0 68.00 100.00 % 4 4 0
app/controllers/device_access_controller.rb 92.86 % 32 14 13 1 10.64 100.00 % 2 2 0
app/controllers/events_controller.rb 98.00 % 97 50 49 1 23.90 91.67 % 12 11 1
app/controllers/ical_feeds_controller.rb 100.00 % 25 10 10 0 6.50 100.00 % 0 0 0
app/controllers/linked_devices_controller.rb 84.06 % 149 69 58 11 6.00 65.00 % 20 13 7
app/controllers/members_controller.rb 88.89 % 94 45 40 5 4.62 70.00 % 10 7 3
app/controllers/permissions_controller.rb 77.32 % 186 97 75 22 7.26 48.94 % 47 23 24
app/controllers/sessions_controller.rb 100.00 % 40 28 28 0 57.11 100.00 % 6 6 0
app/controllers/user_mails_controller.rb 100.00 % 38 19 19 0 5.53 100.00 % 6 6 0
app/controllers/users/registration_controller.rb 100.00 % 64 34 34 0 2.65 85.71 % 14 12 2
app/controllers/users_controller.rb 100.00 % 95 41 41 0 5.76 85.71 % 14 12 2

Channels ( 100.0% covered at 0.0 hits/line )

0 files in total.
0 relevant lines, 0 lines covered and 0 lines missed. ( 100.0% )
0 total branches, 0 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches

Models ( 95.43% covered at 90.99 hits/line )

20 files in total.
350 relevant lines, 334 lines covered and 16 lines missed. ( 95.43% )
64 total branches, 44 branches covered and 20 branches missed. ( 68.75% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/models/application_record.rb 100.00 % 3 2 2 0 1.00 100.00 % 0 0 0
app/models/band.rb 100.00 % 39 22 22 0 11.05 50.00 % 2 1 1
app/models/band_member.rb 100.00 % 5 4 4 0 16.00 100.00 % 0 0 0
app/models/bands_audit.rb 100.00 % 6 4 4 0 7.00 100.00 % 0 0 0
app/models/concerns/auditable.rb 100.00 % 42 19 19 0 8.95 100.00 % 0 0 0
app/models/concerns/random_id.rb 100.00 % 16 9 9 0 819.78 50.00 % 2 1 1
app/models/confirmation_token.rb 100.00 % 15 8 8 0 9.88 100.00 % 0 0 0
app/models/current.rb 100.00 % 34 19 19 0 525.32 75.00 % 4 3 1
app/models/event.rb 97.30 % 73 37 36 1 42.08 70.00 % 10 7 3
app/models/event_band.rb 100.00 % 5 4 4 0 1.00 100.00 % 0 0 0
app/models/events_audit.rb 100.00 % 5 4 4 0 9.00 100.00 % 0 0 0
app/models/linked_device.rb 98.46 % 132 65 64 1 60.71 70.00 % 10 7 3
app/models/linked_device_linkable.rb 100.00 % 9 4 4 0 14.00 100.00 % 0 0 0
app/models/member.rb 71.88 % 71 32 23 9 20.53 0.00 % 8 0 8
app/models/member_skill.rb 100.00 % 6 4 4 0 16.00 100.00 % 0 0 0
app/models/members_audit.rb 100.00 % 5 4 4 0 5.00 100.00 % 0 0 0
app/models/permission.rb 89.58 % 86 48 43 5 67.58 88.89 % 18 16 2
app/models/skill.rb 100.00 % 6 4 4 0 1.00 100.00 % 0 0 0
app/models/user.rb 100.00 % 80 34 34 0 125.65 100.00 % 6 6 0
app/models/user_mail.rb 100.00 % 45 23 23 0 1.74 75.00 % 4 3 1

Mailers ( 57.14% covered at 2.11 hits/line )

3 files in total.
28 relevant lines, 16 lines covered and 12 lines missed. ( 57.14% )
2 total branches, 1 branches covered and 1 branches missed. ( 50.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/mailers/application_mailer.rb 100.00 % 3 2 2 0 7.00 100.00 % 0 0 0
app/mailers/test_mailer.rb 0.00 % 11 10 0 10 0.00 100.00 % 0 0 0
app/mailers/user_mailer.rb 87.50 % 42 16 14 2 2.81 50.00 % 2 1 1

Helpers ( 76.41% covered at 78.04 hits/line )

8 files in total.
195 relevant lines, 149 lines covered and 46 lines missed. ( 76.41% )
94 total branches, 60 branches covered and 34 branches missed. ( 63.83% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/helpers/application_helper.rb 98.51 % 156 67 66 1 175.24 91.30 % 23 21 2
app/helpers/bands_helper.rb 100.00 % 2 1 1 0 1.00 100.00 % 0 0 0
app/helpers/events_helper.rb 100.00 % 51 32 32 0 54.75 100.00 % 25 25 0
app/helpers/linked_devices_helper.rb 60.87 % 45 23 14 9 3.17 46.15 % 13 6 7
app/helpers/members_helper.rb 100.00 % 2 1 1 0 1.00 100.00 % 0 0 0
app/helpers/permissions_helper.rb 47.83 % 182 69 33 36 23.87 24.24 % 33 8 25
app/helpers/sessions_helper.rb 100.00 % 2 1 1 0 1.00 100.00 % 0 0 0
app/helpers/users/registration_helper.rb 100.00 % 2 1 1 0 1.00 100.00 % 0 0 0

Jobs ( 71.43% covered at 0.71 hits/line )

2 files in total.
7 relevant lines, 5 lines covered and 2 lines missed. ( 71.43% )
0 total branches, 0 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/jobs/application_job.rb 100.00 % 7 1 1 0 1.00 100.00 % 0 0 0
app/jobs/send_mail_job.rb 66.67 % 9 6 4 2 0.67 100.00 % 0 0 0

Libraries ( 100.0% covered at 41.51 hits/line )

1 files in total.
91 relevant lines, 91 lines covered and 0 lines missed. ( 100.0% )
22 total branches, 16 branches covered and 6 branches missed. ( 72.73% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/lib/form_builders/nice_form_builder.rb 100.00 % 233 91 91 0 41.51 72.73 % 22 16 6

Ungrouped ( 100.0% covered at 45.06 hits/line )

5 files in total.
105 relevant lines, 105 lines covered and 0 lines missed. ( 100.0% )
8 total branches, 8 branches covered and 0 branches missed. ( 100.0% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line Branch Coverage Branches Covered branches Missed branches
app/queries/band_permission_query.rb 100.00 % 134 17 17 0 70.76 100.00 % 0 0 0
app/queries/event_permission_query.rb 100.00 % 85 13 13 0 133.31 100.00 % 0 0 0
app/queries/member_permission_query.rb 100.00 % 157 18 18 0 64.72 100.00 % 0 0 0
app/services/device_access_service.rb 100.00 % 27 11 11 0 7.55 100.00 % 2 2 0
app/services/ical_generator_service.rb 100.00 % 81 46 46 0 11.89 100.00 % 6 6 0

app/controllers/admin/admin_controller.rb

100.0% lines covered

100.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. 13 module Admin
  2. 13 class AdminController < ApplicationController
  3. 13 before_action :require_admin!, :set_admin_border
  4. 13 private
  5. 13 def require_admin!
  6. 18 else: 16 then: 2 unless Current.user.admin? || Current.impersonating?
  7. 2 render plain: "Admins only",
  8. status: :forbidden
  9. end
  10. end
  11. 13 def set_admin_border
  12. 16 @admin_border = true
  13. end
  14. end
  15. end

app/controllers/admin/bands_controller.rb

0.0% lines covered

100.0% branches covered

37 relevant lines. 0 lines covered and 37 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. module Admin
  2. class BandsController < AdminController
  3. before_action :set_band, except: %i[
  4. index
  5. ]
  6. def index
  7. @bands = Band.all
  8. end
  9. def show
  10. end
  11. def edit
  12. end
  13. def update
  14. updated = @band.update(edit_band_params)
  15. if updated
  16. redirect_to edit_admin_band_path(@band), notice: "Band updated"
  17. else
  18. flash[:alert] = @band.errors.full_messages
  19. render :edit, status: :unprocessable_entity
  20. end
  21. end
  22. def destroy
  23. @band.destroy!
  24. redirect_to admin_bands_path, notice: "Band deleted."
  25. end
  26. private
  27. def set_band
  28. @band = Band.find(params[:id])
  29. redirect_to admin_bands_path, alert: "Band not found" unless @band
  30. end
  31. def edit_band_params
  32. params.require(:band).permit(
  33. :name
  34. )
  35. end
  36. end
  37. end

app/controllers/admin/impersonation_controller.rb

100.0% lines covered

100.0% branches covered

15 relevant lines. 15 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. 5 module Admin
  2. 5 class ImpersonationController < AdminController
  3. 5 def create
  4. 3 target_user = User.find(params[:user_id])
  5. 3 session[:impersonation] = {
  6. original_user_id: Current.user.id,
  7. target_user_id: target_user.id,
  8. started_at: Time.current.utc
  9. }
  10. 3 redirect_to(events_path, notice: "Impersonating '#{target_user.username}'")
  11. end
  12. 5 def destroy
  13. 2 else: 1 then: 1 unless session[:impersonation]
  14. 1 return redirect_to(admin_users_path,
  15. alert: "No active impersonation session")
  16. end
  17. 1 target_user = User.find(session[:impersonation]["target_user_id"])
  18. 1 original_user = User.find(session[:impersonation]["original_user_id"])
  19. 1 username = target_user.username
  20. 1 session[:user_id] = original_user.id
  21. 1 session.delete(:impersonation)
  22. 1 redirect_to(admin_users_path, notice: "Ended impersonation of #{username}")
  23. end
  24. end
  25. end

app/controllers/admin/users_controller.rb

100.0% lines covered

90.0% branches covered

29 relevant lines. 29 lines covered and 0 lines missed.
10 total branches, 9 branches covered and 1 branches missed.
    
  1. 10 module Admin
  2. 10 class UsersController < AdminController
  3. 10 before_action :set_user, except: %i[
  4. index
  5. ]
  6. 10 def index
  7. 4 then: 2 else: 2 @user_type = params[:user_type]&.to_sym
  8. 4 then: 1 else: 3 if @user_type && !User.user_types[@user_type]
  9. 1 flash[:alert] = "Invalid user type"
  10. 1 redirect_to admin_users_path
  11. end
  12. 4 @user_types = User.user_types.keys
  13. @users =
  14. 4 then: 2 if @user_type
  15. 2 User.where(user_type: @user_type)
  16. else: 2 else
  17. 2 User.all
  18. end
  19. end
  20. 10 def show
  21. end
  22. 10 def edit
  23. end
  24. 10 def update
  25. 3 updated = @user.update(edit_user_params)
  26. 2 then: 1 if updated
  27. 1 redirect_to edit_admin_user_path(@user), notice: "User updated"
  28. else: 1 else
  29. 1 flash[:alert] = @user.errors.full_messages
  30. 1 render :edit, status: :unprocessable_entity
  31. end
  32. end
  33. 10 def destroy
  34. 1 @user.destroy!
  35. 1 redirect_to admin_users_path, notice: "User deleted."
  36. end
  37. 10 private
  38. 10 def set_user
  39. 7 @user = User.find_by(username: params[:username])
  40. 7 else: 7 then: 0 redirect_to admin_users_path, alert: "User not found" unless @user
  41. end
  42. 10 def edit_user_params
  43. 3 params.require(:user).permit(
  44. :username,
  45. :name,
  46. :email,
  47. :user_type,
  48. :confirmed
  49. )
  50. end
  51. end
  52. end

app/controllers/application_controller.rb

85.33% lines covered

71.88% branches covered

75 relevant lines. 64 lines covered and 11 lines missed.
32 total branches, 23 branches covered and 9 branches missed.
    
  1. # frozen_string_literal: true
  2. 16 class ApplicationController < ActionController::Base
  3. 16 prepend_before_action :set_current_user
  4. 16 before_action :require_login
  5. 16 helper_method :show_tabs,
  6. :nav_tabs,
  7. :my_pending_invites,
  8. :body_class,
  9. :set_current_user_by_session
  10. 16 def nav_tabs
  11. 498 else: 486 then: 12 return unless show_tabs
  12. 486 @nav_tabs ||= build_nav_tabs
  13. end
  14. 16 def my_pending_invites
  15. 255 then: 243 else: 12 else: 101 then: 154 return unless Current.user&.member?
  16. 101 @my_pending_invites ||= Permission.where(
  17. user_id: Current.user.id,
  18. status: "pending"
  19. )
  20. end
  21. 16 def body_class
  22. 255 then: 7 else: 248 @admin_border ? "admin-background" : ""
  23. end
  24. 16 def show_tabs
  25. 741 @show_tabs ||= Current.user.present?
  26. end
  27. 16 private
  28. 16 def set_current_user
  29. 534 set_current_user_by_session(session)
  30. end
  31. 16 def set_current_user_by_session(session)
  32. 535 then: 2 if session[:impersonation]
  33. 2 else: 533 handle_impersonation(session)
  34. 533 then: 342 elsif session[:user_id]
  35. 342 set_regular_user(session[:user_id])
  36. else: 191 else
  37. 191 clear_current_user
  38. end
  39. end
  40. 16 def handle_impersonation(session)
  41. 2 impersonation = session[:impersonation]
  42. original_user_id =
  43. 2 impersonation["original_user_id"] ||
  44. impersonation[:original_user_id]
  45. target_user_id =
  46. 2 impersonation["target_user_id"] ||
  47. impersonation[:target_user_id]
  48. started_at =
  49. 2 impersonation["started_at"] ||
  50. impersonation[:started_at]
  51. 2 original_user = User.find_by(id: original_user_id)
  52. 2 target_user = User.find_by(id: target_user_id)
  53. 2 then: 2 if valid_impersonation?(original_user, target_user, started_at)
  54. 2 set_impersonated_user(original_user, target_user)
  55. else: 0 else
  56. end_impersonation(session)
  57. end
  58. end
  59. 16 def valid_impersonation?(original_user, target_user, started_at)
  60. failure_reasons = {
  61. 2 then: 2 else: 0 original_not_admin: !original_user&.admin?,
  62. no_target_user: !target_user,
  63. missing_time: !started_at,
  64. too_old: started_at && started_at.to_time < 12.hours.ago
  65. 8 }.select! { |k, v| !!v }.keys
  66. 2 then: 0 if failure_reasons.any?
  67. then: 0 else: 0 if request
  68. flash[:alert] = "Impersonation failed: #{failure_reasons.join(", ")}"
  69. end
  70. false
  71. else: 2 else
  72. 2 true
  73. end
  74. end
  75. 16 def set_impersonated_user(original_user, target_user)
  76. 2 Current.user = target_user
  77. 2 Current.impersonator = original_user
  78. end
  79. 16 def set_regular_user(user_id)
  80. 342 user = User.find_by(id: user_id)
  81. 342 then: 342 if user
  82. 342 Current.user = user
  83. 342 Current.impersonator = nil
  84. else: 0 else
  85. clear_current_user
  86. end
  87. end
  88. 16 def clear_current_user
  89. 191 Current.user = nil
  90. 191 Current.impersonator = nil
  91. end
  92. 16 def end_impersonation(session)
  93. session.delete(:impersonation)
  94. set_regular_user(session[:user_id])
  95. end
  96. 16 def require_login
  97. 518 then: 343 else: 175 then: 514 else: 4 return if Current.user&.confirmed? || allowed_path?
  98. 4 then: 0 if Current.user && !Current.user.confirmed?
  99. handle_unconfirmed_user
  100. else: 4 else
  101. 4 redirect_to_login
  102. end
  103. end
  104. 16 def allowed_path?
  105. [
  106. 175 login_path,
  107. logout_path,
  108. register_path,
  109. not_confirmed_path,
  110. resend_confirmation_path,
  111. confirm_registration_path,
  112. confirm_registration_submit_path
  113. ].include?(request.path) ||
  114. request.path.start_with?("/calendar/")
  115. end
  116. 16 def handle_unconfirmed_user
  117. else: 0 unless [
  118. not_confirmed_path,
  119. resend_confirmation_path
  120. then: 0 ].include?(request.path)
  121. flash[:alert] = "Your account is not confirmed"
  122. redirect_to not_confirmed_path
  123. end
  124. end
  125. 16 def redirect_to_login
  126. 4 flash[:alert] = "You must be logged in"
  127. 4 redirect_to login_url
  128. end
  129. 16 def build_nav_tabs
  130. shares_name =
  131. 243 then: 142 (Current.user.admin? || Current.user.organiser?) ?
  132. 142 else: 101 "Sharing" :
  133. 101 "Shares"
  134. [
  135. 243 {display_name: "Events", url: events_path},
  136. {display_name: "Bands", url: bands_path},
  137. {display_name: "Members", url: members_path},
  138. {
  139. display_name: shares_name,
  140. url: permissions_path
  141. }
  142. ]
  143. end
  144. end

app/controllers/bands_controller.rb

96.49% lines covered

90.0% branches covered

57 relevant lines. 55 lines covered and 2 lines missed.
20 total branches, 18 branches covered and 2 branches missed.
    
  1. 10 class BandsController < ApplicationController
  2. 10 include AccessPermissions
  3. 10 include EventsHelper
  4. 10 before_action :set_events, except: %i[index new create]
  5. 10 before_action :set_view, only: %i[show edit update confirm_destroy]
  6. 10 before_action :verify_organiser, only: %i[new create]
  7. 10 before_action :verify_organiser_or_admin, only: %i[confirm_destroy destroy]
  8. 10 def index
  9. 8 @bands = sort_bands(@bands, params[:sort])
  10. 8 bands_count = @bands.to_a.size
  11. 8 then: 1 if bands_count == 0
  12. 1 else: 7 redirect_to action: :new
  13. 7 then: 1 else: 6 elsif Current.user.member? && bands_count == 1
  14. 1 redirect_to @bands.first
  15. end
  16. end
  17. 10 def edit
  18. end
  19. 10 def show
  20. end
  21. 10 def confirm_destroy
  22. @view = "overview"
  23. end
  24. 10 def new
  25. 1 @band = Band.new
  26. end
  27. 10 def create
  28. 2 @band = Band.new(band_params)
  29. begin
  30. 2 Band.transaction do
  31. 2 @band.save!
  32. 2 Permission.create!(
  33. item_type: "Band",
  34. item_id: @band.id,
  35. user_id: Current.user.id,
  36. status: :owned,
  37. permission_type: :edit
  38. )
  39. end
  40. 1 redirect_to @band, notice: "Band was successfully created"
  41. rescue ActiveRecord::RecordInvalid
  42. 1 render :new, status: :unprocessable_entity
  43. end
  44. end
  45. 10 def update
  46. 2 then: 1 if @band.update(band_params)
  47. 1 redirect_to @band, notice: "Band was successfully updated."
  48. else: 1 else
  49. 1 render :edit
  50. end
  51. end
  52. 10 def destroy
  53. 2 then: 1 if @band.destroy
  54. 1 redirect_to bands_url, notice: "Band deleted!"
  55. else: 1 else
  56. 1 redirect_to @band, alert: @band.errors.full_messages.to_sentence
  57. end
  58. end
  59. 10 private
  60. 10 def set_events
  61. 7 @events = @band.events
  62. 7 .then { |rel| filter_by_period(rel, params[:period]) }
  63. 7 .then { |rel| sort_results(rel, params[:sort]) }
  64. end
  65. 10 def set_view
  66. 5 @views = %w[overview events shares]
  67. 5 @views_subtitles = [nil, "(#{@band.events.count})", nil]
  68. @view =
  69. 5 then: 0 @views.include?(params["view"]) ?
  70. else: 5 params["view"] :
  71. 5 "overview"
  72. end
  73. 10 def verify_organiser
  74. 4 else: 3 then: 1 redirect_to bands_url unless Current.user.organiser?
  75. end
  76. 10 def verify_organiser_or_admin
  77. 2 else: 2 then: 0 redirect_to bands_url unless Current.user.organiser? || Current.user.admin?
  78. end
  79. 10 def sort_bands(bands, sort_param)
  80. 8 then: 4 else: 4 sort_by, sort_order = sort_param&.split
  81. 8 then: 3 if sort_by.present? && Band.column_names.include?(sort_by)
  82. 3 then: 2 else: 1 bands.order(sort_by => ((sort_order == "DESC") ? :desc : :asc))
  83. else: 5 else
  84. 5 bands.order(name: :asc)
  85. end
  86. end
  87. 10 def band_params
  88. 4 params.require(:band).permit(:name, :description, member_ids: [])
  89. end
  90. end

app/controllers/calendars_controller.rb

100.0% lines covered

100.0% branches covered

7 relevant lines. 7 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 7 class CalendarsController < DeviceAccessController
  2. 7 layout "application"
  3. 7 def show
  4. 4 @events = accessible_events
  5. end
  6. 7 private
  7. 7 def allowed_device_types
  8. 8 [:web]
  9. end
  10. end

app/controllers/concerns/access_permissions.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
4 total branches, 4 branches covered and 0 branches missed.
    
  1. 16 module AccessPermissions
  2. 16 extend ActiveSupport::Concern
  3. 16 included do
  4. 37 before_action :get_resources
  5. 37 before_action :set_resource, except: %i[index new create]
  6. 37 before_action :deny_read_only, only: %i[edit update destroy]
  7. end
  8. 16 private
  9. 16 def get_resources
  10. 236 resource_class = controller_name.classify.constantize
  11. @resources =
  12. 236 then: 19 Current.user.admin? ?
  13. 19 else: 217 resource_class.all :
  14. 217 Current.user.send(controller_name)
  15. 236 instance_variable_set(:"@#{controller_name}", @resources)
  16. end
  17. 16 def set_resource
  18. 35 @resource = @resources.find(params[:id])
  19. 32 @read_only = @resource.permission_type != "edit"
  20. 32 instance_variable_set(:"@#{controller_name.singularize}", @resource)
  21. end
  22. 16 def deny_read_only
  23. 26 then: 5 else: 21 raise ActiveRecord::RecordNotFound if @read_only
  24. end
  25. end

app/controllers/device_access_controller.rb

92.86% lines covered

100.0% branches covered

14 relevant lines. 13 lines covered and 1 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. 11 class DeviceAccessController < ApplicationController
  2. 11 skip_before_action :require_login
  3. 11 before_action :authenticate_device
  4. 11 before_action :track_device_access
  5. 11 private
  6. 11 def authenticate_device
  7. 16 @linked_device = LinkedDevice.active.find_by(
  8. secret: params[:secret],
  9. device_type: allowed_device_types
  10. )
  11. 16 else: 9 then: 7 raise ActiveRecord::RecordNotFound unless @linked_device
  12. end
  13. 11 def track_device_access
  14. 9 @linked_device.touch_access!
  15. end
  16. 11 def accessible_events
  17. 9 @accessible_events ||= DeviceAccessService
  18. .new(@linked_device)
  19. .accessible_events
  20. .order(start_date: :asc)
  21. end
  22. 11 def allowed_device_types
  23. raise NotImplementedError, "Subclass must define allowed_device_types"
  24. end
  25. end

app/controllers/events_controller.rb

98.0% lines covered

91.67% branches covered

50 relevant lines. 49 lines covered and 1 lines missed.
12 total branches, 11 branches covered and 1 branches missed.
    
  1. 16 class EventsController < ApplicationController
  2. 16 include AccessPermissions
  3. 16 include EventsHelper
  4. 16 before_action :set_bands, only: %i[new edit create update]
  5. 16 before_action :set_view, only: %i[show edit update]
  6. 16 def index
  7. 177 @events = @events
  8. 177 .then { |rel| filter_by_period(rel, params[:period]) }
  9. 177 .then { |rel| filter_by_band(rel, params[:band_id]) }
  10. 177 .then { |rel| sort_results(rel, params[:sort]) }
  11. end
  12. 16 def new
  13. 3 @event = Event.new
  14. 3 param_band_id = Integer(params[:band_id], exception: false)
  15. 3 else: 1 then: 2 param_band_id = nil unless @bands.collect(&:id).include?(param_band_id)
  16. 3 then: 1 else: 2 @event.band_ids = [param_band_id] if param_band_id
  17. end
  18. 16 def show
  19. end
  20. 16 def edit
  21. end
  22. 16 def create
  23. 5 @event = Event.new(event_params)
  24. 4 then: 3 if @event.save
  25. 3 create_owner_permission(@event)
  26. 3 redirect_to @event, notice: "Event was successfully created"
  27. else: 1 else
  28. 1 render :new, status: :unprocessable_entity
  29. end
  30. end
  31. 16 def update
  32. 10 then: 8 if @event.update(event_params)
  33. 8 redirect_to @event, notice: "Event was successfully updated."
  34. else: 1 else
  35. 1 render :edit, status: :unprocessable_entity
  36. end
  37. end
  38. 16 def destroy
  39. 2 @event.destroy!
  40. 1 redirect_to events_url, notice: "Event was successfully destroyed."
  41. rescue ActiveRecord::RecordNotDestroyed
  42. 1 redirect_to @event, alert: @event.errors.full_messages.to_sentence
  43. end
  44. 16 private
  45. 16 def set_bands
  46. 18 @bands = Current.user.bands
  47. end
  48. 16 def set_view
  49. 13 @views = %w[overview bands shares]
  50. 13 @views_subtitles = [nil, "(#{@event.bands.count})", nil]
  51. @view =
  52. 13 then: 0 @views.include?(params["view"]) ?
  53. else: 13 params["view"] :
  54. 13 "overview"
  55. end
  56. 16 def event_params
  57. 15 permitted = params.require(:event).permit(
  58. :name,
  59. :description,
  60. :start_date,
  61. :end_date,
  62. band_ids: []
  63. )
  64. 15 permitted_band_ids = @bands.pluck(:id)
  65. 15 permitted.tap do |params|
  66. 15 params[:band_ids] = params[:band_ids].to_a.compact_blank.map(&:to_i)
  67. 15 invalid_bands = params[:band_ids] - permitted_band_ids
  68. 15 then: 1 else: 14 if invalid_bands.any?
  69. 1 raise ArgumentError, "Invalid band_ids: #{invalid_bands}"
  70. end
  71. end
  72. end
  73. 16 def create_owner_permission(event)
  74. 3 event.permissions.create!(
  75. user: Current.user,
  76. status: :owned,
  77. permission_type: :edit
  78. )
  79. end
  80. end

app/controllers/ical_feeds_controller.rb

100.0% lines covered

100.0% branches covered

10 relevant lines. 10 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 8 class IcalFeedsController < DeviceAccessController
  2. 8 def show
  3. 5 @events = accessible_events
  4. 5 calendar = IcalGeneratorService.new(
  5. events: @events,
  6. device: @linked_device
  7. ).generate
  8. 5 respond_to do |format|
  9. 5 format.ics do
  10. 5 send_data calendar.to_ical,
  11. type: "text/calendar",
  12. disposition: "attachment",
  13. filename: "libregig-calendar.ics"
  14. end
  15. end
  16. end
  17. 8 private
  18. 8 def allowed_device_types
  19. 8 [:web, :ical]
  20. end
  21. end

app/controllers/linked_devices_controller.rb

84.06% lines covered

65.0% branches covered

69 relevant lines. 58 lines covered and 11 lines missed.
20 total branches, 13 branches covered and 7 branches missed.
    
  1. 9 class LinkedDevicesController < ApplicationController
  2. 9 before_action :set_linked_device, only: [:show, :edit, :update, :destroy, :revoke]
  3. 9 before_action :set_linkables, only: [:new, :edit, :create, :update]
  4. 9 before_action :set_device_types
  5. 9 before_action :set_view, only: [:show]
  6. 9 def index
  7. 7 @linked_devices = Current.user.linked_devices
  8. .includes(:linked_device_linkables)
  9. 7 .then { |rel| filter_by_status(rel, params[:status]) }
  10. 7 .then { |rel| filter_by_type(rel, params[:device_type]) }
  11. 7 .then { |rel| sort_results(rel, params[:sort]) }
  12. end
  13. 9 def show
  14. end
  15. 9 def new
  16. 1 @linked_device = LinkedDevice.new
  17. # Set default linkable if provided in params
  18. 1 then: 0 else: 1 if params[:linkable_type].present? && params[:linkable_id].present?
  19. type = params[:linkable_type]
  20. id = params[:linkable_id]
  21. then: 0 else: 0 if %w[Event Band Member].include?(type)
  22. @linked_device.linkable_type = type
  23. @linked_device.linkable_id = id
  24. end
  25. end
  26. end
  27. 9 def edit
  28. end
  29. 9 def create
  30. 2 @linked_device = Current.user.linked_devices.new(linked_device_params)
  31. 2 then: 2 if @linked_device.save
  32. 2 redirect_to @linked_device, notice: "Device successfully linked."
  33. else: 0 else
  34. render :new, status: :unprocessable_entity
  35. end
  36. end
  37. 9 def update
  38. 2 then: 2 if @linked_device.update(linked_device_params)
  39. 2 redirect_to @linked_device, notice: "Device successfully updated."
  40. else: 0 else
  41. render :edit, status: :unprocessable_entity
  42. end
  43. end
  44. 9 def destroy
  45. 2 then: 1 else: 1 if @linked_device.accessed?
  46. 1 redirect_to(
  47. @linked_device,
  48. alert: "Cannot delete a device that has been accessed. Please revoke instead."
  49. )
  50. 1 return
  51. end
  52. 1 @linked_device.destroy!
  53. 1 redirect_to linked_devices_url, notice: "Device successfully removed."
  54. rescue ActiveRecord::RecordNotDestroyed
  55. redirect_to(
  56. @linked_device,
  57. alert: @linked_device.errors.full_messages.to_sentence
  58. )
  59. end
  60. 9 def revoke
  61. # For revocation, we don't need to validate access permissions
  62. 1 then: 1 else: 0 @linked_device.skip_access_validation! if Rails.env.test?
  63. 1 @linked_device.revoke!
  64. 1 redirect_to @linked_device, notice: "Device successfully revoked."
  65. end
  66. 9 private
  67. 9 def set_linked_device
  68. 9 @linked_device = Current.user.linked_devices.find(params[:id])
  69. end
  70. 9 def set_linkables
  71. 6 then: 0 if Current.user.admin?
  72. @events = Event.all
  73. @bands = Band.all
  74. @members = Member.all
  75. else: 6 else
  76. 6 @events = Current.user.events
  77. 6 @bands = Current.user.bands
  78. 6 @members = Current.user.members
  79. end
  80. end
  81. DEVICE_TYPE_NAMES = {
  82. 9 api: "API (For developers)",
  83. web: "Web page with private URL",
  84. ical: "iCal (Apple Calendar, Thunderbird)"
  85. }
  86. 9 def set_device_types
  87. @device_types =
  88. 19 LinkedDevice.device_types.keys.map do |key|
  89. 57 [DEVICE_TYPE_NAMES[key.to_sym], key]
  90. end
  91. end
  92. 9 def set_view
  93. 3 @views = %w[overview]
  94. 3 @view = "overview"
  95. end
  96. 9 def linked_device_params
  97. 4 params.require(:linked_device).permit(
  98. :name,
  99. :device_type,
  100. event_ids: [],
  101. band_ids: [],
  102. member_ids: []
  103. )
  104. end
  105. 9 def filter_by_status(relation, status)
  106. 7 then: 1 else: 6 return relation.active if status == "active"
  107. 6 then: 1 else: 5 return relation.revoked if status == "revoked"
  108. 5 relation
  109. end
  110. 9 def filter_by_type(relation, type)
  111. 7 then: 2 else: 5 LinkedDevice.device_types.key?(type) ? relation.where(device_type: type) : relation
  112. end
  113. 9 def sort_results(relation, sort)
  114. sort_mapping = {
  115. 7 "name" => {name: :asc},
  116. "name desc" => {name: :desc},
  117. "created" => {created_at: :asc},
  118. "created desc" => {created_at: :desc},
  119. "modified" => {updated_at: :asc},
  120. "modified desc" => {updated_at: :desc},
  121. "last_accessed" => {last_accessed_at: :asc},
  122. "last_accessed desc" => {last_accessed_at: :desc}
  123. }
  124. 7 relation.order(sort_mapping[sort] || {created_at: :desc})
  125. end
  126. end

app/controllers/members_controller.rb

88.89% lines covered

70.0% branches covered

45 relevant lines. 40 lines covered and 5 lines missed.
10 total branches, 7 branches covered and 3 branches missed.
    
  1. 11 class MembersController < ApplicationController
  2. 11 include AccessPermissions
  3. 11 include EventsHelper
  4. 11 before_action :set_member_events, only: :show
  5. 11 before_action :set_view, only: :show
  6. 11 def index
  7. # Get skills for the current set of members
  8. @skills =
  9. 1 Skill.joins(:members)
  10. .where(members: {id: @members.pluck(:id)})
  11. .distinct
  12. .order(:name)
  13. # Filter members by skill if skill_id parameter is present
  14. 1 then: 0 else: 1 if params[:skill_id].present?
  15. @skill = Skill.find(params[:skill_id])
  16. @members =
  17. @members
  18. .joins(:skills)
  19. .where(skills: {id: params[:skill_id]})
  20. end
  21. end
  22. 11 def edit
  23. end
  24. 11 def show
  25. end
  26. 11 def new
  27. 1 @member = Member.new
  28. end
  29. 11 def create
  30. 2 @member = Member.new(member_params)
  31. 2 else: 1 then: 1 return render :new, status: :unprocessable_entity unless @member.save
  32. 1 @permission = Permission.create(
  33. item_type: "Member",
  34. item_id: @member.id,
  35. user: Current.user,
  36. status: :owned,
  37. permission_type: "edit"
  38. )
  39. 1 else: 1 then: 0 return render :new, status: :unprocessable_entity unless @permission.save
  40. 1 redirect_to @member, notice: "Member was successfully created."
  41. end
  42. 11 def update
  43. 2 Member.transaction do
  44. 2 @member.assign_attributes(member_params)
  45. 2 then: 1 if @member.save
  46. 1 redirect_to @member, notice: "Member was successfully updated."
  47. else: 1 else
  48. 1 logger.warn "members/update error: #{@member.errors.full_messages}"
  49. 1 render :edit, status: :unprocessable_entity
  50. end
  51. end
  52. rescue ActiveRecord::RecordInvalid => e
  53. logger.warn "members/update transaction error: #{e.message}"
  54. render :edit, status: :unprocessable_entity
  55. end
  56. 11 def destroy
  57. 1 @member.destroy!
  58. 1 redirect_to members_url, notice: "Member was successfully destroyed."
  59. end
  60. 11 private
  61. 11 def member_params
  62. 4 params.require(:member).permit(
  63. :name, :description, :skills_list, band_ids: []
  64. )
  65. end
  66. 11 def set_view
  67. 1 @views = %w[overview events]
  68. 1 @views_subtitles = [nil, "(#{@events.count})"]
  69. @view =
  70. 1 then: 0 @views.include?(params["view"]) ?
  71. else: 1 params["view"] :
  72. 1 "overview"
  73. end
  74. 11 def set_member_events
  75. 1 @events = @member.events
  76. 1 .then { |rel| filter_by_period(rel, params[:period]) }
  77. 1 .then { |rel| sort_results(rel, params[:sort]) }
  78. end
  79. end

app/controllers/permissions_controller.rb

77.32% lines covered

48.94% branches covered

97 relevant lines. 75 lines covered and 22 lines missed.
47 total branches, 23 branches covered and 24 branches missed.
    
  1. 16 class PermissionsController < ApplicationController
  2. 16 include PermissionsHelper
  3. 16 before_action :set_permission, only: [:update, :destroy]
  4. 16 before_action :organiser_only, only: [:new, :create]
  5. 16 before_action :invitee_and_pending_only, only: [:update]
  6. 16 before_action :bestower_and_admin_only, only: [:destroy]
  7. 16 before_action :set_form_options, only: [:new, :create]
  8. 16 def index
  9. 8 then: 1 else: 7 @permissions = Current.user.admin? ? Permission.all : Permission.for_user(Current.user)
  10. 8 @permissions = sort_permissions(@permissions, params[:direct_sort])
  11. 8 @other_items = fetch_effective_permissions
  12. # Default to name sorting if not specified
  13. 8 effective_sort = params[:effective_sort].presence || "name asc"
  14. 8 @other_items = sort_effective_items(@other_items, effective_sort)
  15. 8 respond_to do |format|
  16. 8 format.html
  17. 8 format.json { render json: {permissions: @permissions, effective: @other_items} }
  18. end
  19. end
  20. 16 def new
  21. 1 @permission = Permission.new
  22. 1 then: 0 else: 1 if params[:item_type].present? && params[:item_id].present?
  23. @preselected_item = "#{params[:item_type].capitalize}_#{params[:item_id]}"
  24. end
  25. 1 @no_users_available = potential_users.empty?
  26. end
  27. 16 def create
  28. 7 params = permission_create_params
  29. 7 item_param = params.delete(:item)
  30. 7 @permission = Permission.new(params)
  31. 7 @permission.bestowing_user = Current.user
  32. 7 @permission.status = "pending"
  33. 7 @permission.item_type, @permission.item_id = item_param.split("_")
  34. 7 then: 6 else: 1 @permission.item_type&.capitalize!
  35. 7 else: 3 then: 2 return render :new, status: :unprocessable_entity unless @permission.save
  36. 3 redirect_to permissions_path, notice: "Invitation created"
  37. end
  38. 16 def update
  39. 6 status = permission_update_params[:status]
  40. 6 then: 5 if @permission.update(status: status)
  41. 5 then: 4 if status == "accepted"
  42. 4 redirect_to @permission.item_path, notice: "Invitation accepted"
  43. else: 1 else
  44. 1 redirect_to permissions_path, alert: "Invitation rejected"
  45. end
  46. else: 0 else
  47. render plain: "Not updated", status: :bad_request
  48. end
  49. end
  50. 16 def destroy
  51. 2 @permission.destroy!
  52. 2 redirect_to root_path, notice: "Invitation deleted"
  53. end
  54. 16 private
  55. 16 def sort_permissions(permissions, sort_param)
  56. 8 then: 8 else: 0 return permissions.order(created_at: :desc) if sort_param.blank?
  57. column, direction = sort_param.split
  58. then: 0 else: 0 then: 0 else: 0 direction_sym = (direction&.downcase == "desc") ? :desc : :asc
  59. case column
  60. when: 0 when "name"
  61. joins_sql = <<~SQL
  62. LEFT JOIN bands
  63. ON bands.id = permissions.item_id
  64. AND permissions.item_type = 'Band'
  65. LEFT JOIN events
  66. ON events.id = permissions.item_id
  67. AND permissions.item_type = 'Event'
  68. LEFT JOIN members
  69. ON members.id = permissions.item_id
  70. AND permissions.item_type = 'Member'
  71. SQL
  72. permissions.joins(joins_sql)
  73. .order(Arel.sql(
  74. "COALESCE(bands.name, events.name, members.name) #{direction_sym}"
  75. ))
  76. when: 0 when "type"
  77. permissions.order(item_type: direction_sym)
  78. when: 0 when "access"
  79. permissions.order(permission_type: direction_sym)
  80. when: 0 when "status"
  81. permissions.order(status: direction_sym)
  82. when: 0 when "bestower"
  83. permissions.joins(:bestowing_user).order(Arel.sql(
  84. "users.username #{direction_sym}"
  85. ))
  86. when: 0 when "created"
  87. permissions.order(created_at: direction_sym)
  88. when: 0 when "last_modified"
  89. permissions.order(updated_at: direction_sym)
  90. else: 0 else
  91. permissions.order(created_at: :desc)
  92. end
  93. end
  94. 16 def sort_effective_items(items, sort_param)
  95. 8 then: 0 else: 8 return items.sort_by { |item| item.name.downcase } if sort_param.blank?
  96. 8 column, direction = sort_param.split
  97. 8 then: 8 else: 0 desc = (direction&.downcase == "desc")
  98. 8 sorted_items = case column
  99. when: 8 when "name"
  100. 17 items.sort_by { |item| item.name.downcase }
  101. when: 0 when "type"
  102. items.sort_by { |item| item.class.name }
  103. when: 0 when "status"
  104. items.sort_by { |item| item.permission_type.to_s }
  105. when: 0 when "source"
  106. items.sort_by do |item|
  107. permission = find_effective_permission_source(Current.user, item)
  108. then: 0 if permission.nil?
  109. "AAA_Direct" # Sort direct permissions first
  110. else: 0 else
  111. "#{permission.item_type}_#{permission.item.name}"
  112. end
  113. end
  114. else: 0 else
  115. items.sort_by { |item| item.name.downcase }
  116. end
  117. 8 then: 0 else: 8 desc ? sorted_items.reverse : sorted_items
  118. end
  119. 16 def fetch_effective_permissions
  120. 8 events = Event.permitted_for(Current.user.id)
  121. 8 bands = Band.permitted_for(Current.user.id)
  122. 8 members = Member.permitted_for(Current.user.id)
  123. 8 events + bands + members
  124. end
  125. 16 def organiser_only
  126. 10 else: 8 then: 2 unless Current.user.organiser?
  127. 2 render plain: "Organisers only", status: :forbidden
  128. end
  129. end
  130. 16 def bestower_and_admin_only
  131. 3 else: 2 then: 1 unless Current.user.admin? || @permission.bestowing_user == Current.user
  132. 1 render plain: "Bestower only", status: :forbidden
  133. end
  134. end
  135. 16 def invitee_and_pending_only
  136. 10 then: 3 if @permission.user != Current.user
  137. 3 else: 7 render plain: "Invitee only", status: :forbidden
  138. 7 then: 1 else: 6 elsif !@permission.pending?
  139. 1 render plain: "Invite not pending", status: :bad_request
  140. end
  141. end
  142. 16 def set_form_options
  143. 8 @users = potential_users
  144. 8 @items = potential_items(Current.user)
  145. 8 @permission_types = %w[view edit]
  146. end
  147. 16 def set_permission
  148. 13 @permission = Permission.find(params[:id])
  149. end
  150. 16 def permission_create_params
  151. 7 params.require(:permission).permit(:user_id, :item, :permission_type)
  152. end
  153. 16 def permission_update_params
  154. 6 params.require(:permission).permit(:status)
  155. end
  156. end

app/controllers/sessions_controller.rb

100.0% lines covered

100.0% branches covered

28 relevant lines. 28 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. 16 class SessionsController < ApplicationController
  2. 16 def new
  3. 5 then: 3 else: 2 redirect_to events_path if Current.user
  4. 5 @user = User.new
  5. end
  6. 16 def create
  7. 166 username, password = login_params.values_at(:username, :password)
  8. 165 logger.debug("Login attempt from #{username}")
  9. 165 user = User.find_or_create_by(username: username)
  10. 165 then: 1 if Rails.env.development? && params.dig(:user, :debug_skip)
  11. 1 session[:user_id] = user.id
  12. 1 logger.debug("Skipped login: #{username}")
  13. 1 else: 164 redirect_to events_path, notice: "Skipped login."
  14. 164 then: 162 elsif user.authenticate(password)
  15. 162 session[:user_id] = user.id
  16. 162 logger.debug("Login success: #{username}")
  17. 162 redirect_to events_path, notice: "Logged in successfully."
  18. else: 2 else
  19. 2 @user = User.new
  20. 2 @user.username = username
  21. 2 logger.debug "Login failed: #{user.inspect}"
  22. 2 flash[:alert] = "Invalid username or password"
  23. 2 render :new, status: :unprocessable_entity
  24. end
  25. end
  26. 16 def destroy
  27. 1 session[:user_id] = nil
  28. 1 session[:impersonation] = nil
  29. 1 redirect_to login_path, notice: "Logged out successfully."
  30. end
  31. 16 private
  32. 16 def login_params
  33. 166 params.require(:user).permit(:username, :password)
  34. end
  35. end

app/controllers/user_mails_controller.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. 9 class UserMailsController < ApplicationController
  2. 9 before_action :set_mail, only: :show
  3. 9 before_action :can_view_mail, only: :show
  4. 9 def show
  5. end
  6. 9 def index
  7. @mails =
  8. 3 then: 1 if Current.user.admin?
  9. 1 UserMail.order(updated_at: :desc)
  10. else: 2 else
  11. 2 UserMail.where(user_id: Current.user.id).order(updated_at: :desc)
  12. end
  13. end
  14. 9 def create
  15. 2 @mail = UserMailer.send_confirmation_registration(Current.user)
  16. 2 then: 1 if @mail.persisted?
  17. 1 redirect_to user_mails_path, notice: "Confirmation sent successfully"
  18. else: 1 else
  19. 1 redirect_to user_mails_path, alert: "Failed to send confirmation"
  20. end
  21. end
  22. 9 private
  23. 9 def set_mail
  24. 5 @mail = UserMail.find(params[:id])
  25. end
  26. 9 def can_view_mail
  27. 5 then: 3 else: 2 return if @mail && (@mail.user_id == Current.user.id || Current.user.admin?)
  28. 2 redirect_to user_mails_path, alert: "Cannot view this mail"
  29. end
  30. end

app/controllers/users/registration_controller.rb

100.0% lines covered

85.71% branches covered

34 relevant lines. 34 lines covered and 0 lines missed.
14 total branches, 12 branches covered and 2 branches missed.
    
  1. 5 class Users::RegistrationController < ApplicationController
  2. 5 before_action :set_user, only: %i[
  3. edit
  4. update
  5. ]
  6. 5 def edit
  7. end
  8. 5 def update
  9. 1 else: 0 then: 1 UserMailer.send_confirmation_registration(@user) unless @user.confirmed?
  10. end
  11. 5 def new
  12. 4 @token = params[:token]
  13. 4 verifier = Rails.application.config.message_verifier
  14. begin
  15. 4 data = verifier.verify(@token)
  16. 3 @user = User.find(data["user_id"])
  17. 3 then: 1 else: 2 if @user.confirmed?
  18. 1 redirect_to user_path(@user),
  19. notice: "Your account is already confirmed"
  20. end
  21. 3 then: 1 else: 2 then: 1 else: 2 if data["expires_at"]&.< Time.current.to_i
  22. 1 flash[:alert] = "Your confirmation link has expired: #{data}."
  23. 1 redirect_to register_path
  24. end
  25. rescue ActiveSupport::MessageVerifier::InvalidSignature
  26. 1 flash[:alert] = "Invalid confirmation link."
  27. 1 redirect_to register_path
  28. end
  29. end
  30. 5 def create
  31. 3 token = params[:token]
  32. 3 verifier = Rails.application.config.message_verifier
  33. begin
  34. 3 data = verifier.verify(token)
  35. 2 then: 2 else: 0 then: 1 if data["expires_at"]&.>= Time.current.to_i
  36. 1 @user = User.find(data["user_id"])
  37. 1 @user.update(confirmed: true)
  38. 1 flash[:notice] = "Your registration has been confirmed."
  39. 1 redirect_to login_path
  40. else: 1 else
  41. 1 flash[:alert] = "Your confirmation link has expired: #{data}."
  42. 1 redirect_to register_path
  43. end
  44. rescue ActiveSupport::MessageVerifier::InvalidSignature
  45. 1 flash[:alert] = "Invalid confirmation link."
  46. 1 redirect_to register_path
  47. end
  48. end
  49. 5 private
  50. 5 def set_user
  51. 2 @user = User.find(session[:user_id])
  52. 2 then: 1 else: 1 redirect_to user_path(@user) if @user.confirmed?
  53. end
  54. end

app/controllers/users_controller.rb

100.0% lines covered

85.71% branches covered

41 relevant lines. 41 lines covered and 0 lines missed.
14 total branches, 12 branches covered and 2 branches missed.
    
  1. 10 class UsersController < ApplicationController
  2. 10 before_action :check_admin_user, only: %i[
  3. create
  4. new
  5. ]
  6. 10 before_action :check_not_logged_in, only: %i[
  7. create
  8. new
  9. ]
  10. 10 def new
  11. 1 @user = User.new
  12. end
  13. 10 def create
  14. 4 @user = User.new(create_user_params)
  15. 4 then: 1 else: 3 if @user.admin? && !@allow_admin_user
  16. 1 flash.now[:alert] = "No more admin users allowed"
  17. 1 return render :new, status: :unprocessable_entity
  18. end
  19. 3 then: 2 if @user.save
  20. 2 session[:user_id] = @user.id
  21. 2 redirect_to account_path, notice: "You registered successfully. Well done!"
  22. else: 1 else
  23. 1 render :new, status: :unprocessable_entity
  24. end
  25. end
  26. 10 def index
  27. 1 redirect_to user_path(Current.user)
  28. end
  29. 10 def show
  30. 4 @user = User.find_by(username: params[:id])
  31. 4 else: 4 then: 0 redirect_to user_path(Current.user) unless @user
  32. end
  33. 10 def edit
  34. 1 @user = Current.user
  35. 1 else: 0 then: 1 unless params[:id] == @user.username
  36. 1 redirect_to user_path(@user), alert: "Can't edit other users"
  37. end
  38. end
  39. 10 def update
  40. 7 then: 6 else: 1 if params[:user][:password].blank?
  41. 6 params[:user].delete(:password)
  42. 6 params[:user].delete(:password_confirmation)
  43. end
  44. 7 @user = Current.user
  45. 7 then: 5 if @user.update(update_user_params)
  46. 5 redirect_to user_url(@user), notice: "Updated successfully"
  47. else: 2 else
  48. 2 errors = @user.errors.full_messages.join(", ").downcase
  49. 2 render plain: "Could not update user: #{errors}",
  50. status: :forbidden
  51. end
  52. end
  53. 10 private
  54. 10 def create_user_params
  55. 4 params.require(:user).permit(
  56. :username,
  57. :name,
  58. :email,
  59. :password,
  60. :password_confirmation,
  61. :time_zone,
  62. :user_type
  63. )
  64. end
  65. 10 def update_user_params
  66. 7 params.require(:user).permit(
  67. :name,
  68. :email,
  69. :password,
  70. :password_confirmation,
  71. :time_zone
  72. )
  73. end
  74. 10 def check_admin_user
  75. 6 @allow_admin_user = User.admin.count.zero?
  76. end
  77. 10 def check_not_logged_in
  78. 6 else: 5 then: 1 redirect_to account_path unless Current.user.nil?
  79. end
  80. end

app/helpers/application_helper.rb

98.51% lines covered

91.3% branches covered

67 relevant lines. 66 lines covered and 1 lines missed.
23 total branches, 21 branches covered and 2 branches missed.
    
  1. 1 module ApplicationHelper
  2. 1 def page_heading(str)
  3. 203 content_tag(:h1, str, class: "text-gray-900 text-xl")
  4. end
  5. 1 def filter_link(name, path, active)
  6. 591 link_to_if(!active, name, path, class: "hover:underline") do
  7. 193 content_tag(:span, name, class: "font-bold")
  8. end
  9. end
  10. 1 def render_filter_group(
  11. filters,
  12. param_name,
  13. preserve_params = [],
  14. &path_generator
  15. )
  16. 204 path_generator ||= ->(options) { url_for(options) }
  17. 196 param_name_str = param_name.to_s
  18. # Build preserved params hash in one step
  19. 196 preserved_options = preserve_params.each_with_object({}) do |param, hash|
  20. 192 then: 6 else: 186 hash[param] = params[param] if params[param].present?
  21. end
  22. 196 content_tag(:p, class: "filter-group") do
  23. 196 filters.map do |filter|
  24. # Build options hash
  25. 589 options = preserved_options.merge(
  26. # Only add param if it has a value
  27. 589 then: 397 else: 192 filter[:value].present? ? {param_name => filter[:value]} : {},
  28. # Add any extra parameters
  29. filter[:extra] || {}
  30. )
  31. # Determine if this filter is selected
  32. 589 selected = params[param_name_str] == filter[:value] ||
  33. 397 (filter[:value].nil? && params[param_name_str].nil?)
  34. # Generate the link
  35. 589 filter_link(filter[:label], path_generator.call(options), selected)
  36. end.join.html_safe
  37. end
  38. end
  39. 1 def table_headers(sort_param_name, columns, resource = nil)
  40. 13 query_params = request.query_parameters.merge({}).except(sort_param_name)
  41. # Extract current sort information from params
  42. 13 current_sort_param = params[sort_param_name]
  43. 13 current_sort_column = nil
  44. 13 then: 0 else: 13 if current_sort_param.present?
  45. current_sort_column, _ = current_sort_param.split
  46. end
  47. 13 capture do
  48. 13 columns.each do |column|
  49. # Only set default if this column is actually the sorted one
  50. 66 then: 0 else: 66 default_sort = (column[:name] == current_sort_column) ? column[:default] : nil
  51. 66 concat(
  52. content_tag(:th,
  53. table_header_sort(
  54. resource || controller_name,
  55. column[:name],
  56. column[:display],
  57. default_sort,
  58. query_params,
  59. sort_param_name
  60. ),
  61. class: column[:class],
  62. 66 then: 19 else: 47 style: column[:wide] ? nil : "width: 1%")
  63. )
  64. end
  65. end
  66. end
  67. 1 def table_header_sort(
  68. resource,
  69. column,
  70. display_text,
  71. default_sort_column = nil,
  72. request_params = nil,
  73. sort_param_name = :sort,
  74. default_sort_direction = "asc"
  75. )
  76. 326 sort_param = sort_param_name.to_sym
  77. 326 then: 40 else: 286 current_sort_column, current_sort_direction = params[sort_param]&.split
  78. 326 current_sort_column ||= default_sort_column
  79. 326 current_sort_direction ||= default_sort_direction
  80. 326 then: 91 if current_sort_column == column
  81. 91 then: 84 else: 7 new_sort_direction = (current_sort_direction == "asc") ? "desc" : "asc"
  82. 91 sort_icon = sort_direction_icon(current_sort_direction)
  83. else: 235 else
  84. 235 new_sort_direction = default_sort_direction
  85. 235 sort_icon = ""
  86. end
  87. 326 query_params = request_params || request.query_parameters
  88. 326 new_params = query_params
  89. .except(:page, sort_param)
  90. .merge(sort_param => "#{column} #{new_sort_direction}")
  91. # Preserve period parameter if it exists
  92. 326 then: 55 else: 271 new_params[:period] = request.params[:period] if request.params[:period]
  93. 326 sort_url = url_for(new_params)
  94. 326 link_to "#{display_text}#{sort_icon}".html_safe, sort_url
  95. end
  96. 1 def flash_banner(key, msg = nil, &block)
  97. 187 content_tag :div, class: "#{key} banner" do
  98. 187 then: 9 else: 178 if block && msg.blank?
  99. 9 msg = capture(&block)
  100. end
  101. 187 concat(content_tag(:div, msg))
  102. 187 concat(hidden_checkbox_tag(key))
  103. 187 concat(close_button(key))
  104. end
  105. end
  106. 1 private
  107. 1 def sort_direction_icon(direction)
  108. 91 when: 84 case direction
  109. 84 when: 2 when "asc" then " <span>â–¼</span>"
  110. 2 else: 5 when "desc" then " <span>â–²</span>"
  111. 5 else ""
  112. end
  113. end
  114. 1 def hidden_checkbox_tag(key)
  115. 187 tag.input type: "checkbox", class: "hidden", id: "banneralert-#{key}"
  116. end
  117. 1 def close_button(key)
  118. 187 label_for = "banneralert-#{key}"
  119. 187 svg_xmlns = "http://www.w3.org/2000/svg"
  120. 187 close_path = "M14.348 14.849a1.2 1.2 0 0 1-1.697 0L10 11.819l-2.651 " \
  121. "3.029a1.2 1.2 0 1 1-1.697-1.697l2.758-3.15-2.759-3.152a1.2 " \
  122. "1.2 0 1 1 1.697-1.697L10 8.183l2.651-3.031a1.2 1.2 0 1 1 " \
  123. "1.697 1.697l-2.758 3.152 2.758 3.15a1.2 1.2 0 0 1 0 1.698z"
  124. 187 tag.label class: "close", style: "width:30px", for: label_for do
  125. 187 tag.svg class: "fill-current h-6 w-6", role: "button",
  126. xmlns: svg_xmlns, viewBox: "0 0 20 20" do
  127. 187 concat(tag.title("Close"))
  128. 187 concat(tag.path(d: close_path))
  129. end
  130. end
  131. end
  132. end

app/helpers/bands_helper.rb

100.0% lines covered

100.0% branches covered

1 relevant lines. 1 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module BandsHelper
  2. end

app/helpers/events_helper.rb

100.0% lines covered

100.0% branches covered

32 relevant lines. 32 lines covered and 0 lines missed.
25 total branches, 25 branches covered and 0 branches missed.
    
  1. 1 module EventsHelper
  2. 1 def filter_by_period(events, period)
  3. 185 when: 1 case period
  4. 1 when: 175 when "past" then events.past
  5. 175 else: 9 when "future", nil then events.future
  6. 9 else events
  7. end
  8. end
  9. 1 def filter_by_band(events, band_id)
  10. 177 else: 1 then: 176 return events unless band_id.present? && band_id.to_i.positive?
  11. 1 events.joins(:bands).where(bands: {id: band_id})
  12. end
  13. 1 def sort_results(events, sort_param)
  14. 185 column, direction = extract_sort_params(sort_param)
  15. 185 else: 184 then: 1 unless Event.attribute_names.include?(column.to_s)
  16. 1 return events.order(:start_date)
  17. end
  18. 184 events.order(column => direction)
  19. end
  20. 1 def extract_sort_params(sort_param)
  21. 185 col, dir = sort_param.to_s.split
  22. 185 then: 3 else: 182 dir = %w[desc DESC].include?(dir) ? :desc : :asc
  23. 185 [col.presence || :start_date, dir]
  24. end
  25. 1 def convert_seconds_to_duration(seconds)
  26. 11 then: 4 else: 7 return nil if seconds < 60
  27. 7 days = (seconds / 86400).floor
  28. 7 remaining = seconds % 86400
  29. 7 hours = (remaining / 3600).floor
  30. 7 remaining %= 3600
  31. 7 minutes = (remaining / 60).floor
  32. 7 parts = []
  33. 7 then: 5 else: 2 else: 4 then: 1 parts << "#{days} day#{"s" unless days == 1}" if days > 0
  34. 7 then: 2 else: 5 else: 1 then: 1 parts << "#{hours} hour#{"s" unless hours == 1}" if hours > 0
  35. 7 then: 4 else: 3 else: 1 then: 3 parts << "#{minutes} minute#{"s" unless minutes == 1}" if minutes > 0
  36. 7 when: 4 case parts.size
  37. 4 when 1 then parts.first
  38. else: 3 else
  39. 3 parts[0...-1].join(", ") + " and #{parts.last}"
  40. end
  41. end
  42. end

app/helpers/linked_devices_helper.rb

60.87% lines covered

46.15% branches covered

23 relevant lines. 14 lines covered and 9 lines missed.
13 total branches, 6 branches covered and 7 branches missed.
    
  1. 1 module LinkedDevicesHelper
  2. 1 def device_type_badge(device_type)
  3. 14 case device_type
  4. when: 11 when "api"
  5. 11 content_tag(:span, "API", class: "bg-purple-600 text-white px-2 py-1 rounded-full text-xs")
  6. when: 2 when "web"
  7. 2 content_tag(:span, "Web", class: "bg-blue-600 text-white px-2 py-1 rounded-full text-xs")
  8. else: 1 else
  9. 1 badge_class = "bg-gray-600 text-white px-2 py-1 rounded-full text-xs"
  10. 1 content_tag(:span, device_type.to_s.upcase, class: badge_class)
  11. end
  12. end
  13. 1 def resource_link(type, id)
  14. obj = type.constantize.find_by(id: id)
  15. else: 0 then: 0 return "Unknown #{type}" unless obj
  16. name = obj.name.presence || "NOT SET"
  17. case type
  18. when: 0 when "Event"
  19. link_to(name, event_path(obj), class: "text-purple-600")
  20. when: 0 when "Band"
  21. link_to(name, band_path(obj), class: "text-blue-600")
  22. when: 0 when "Member"
  23. link_to(name, member_path(obj), class: "text-green-600")
  24. else: 0 else
  25. "#{type}: #{name}"
  26. end
  27. end
  28. 1 def status_badge(device)
  29. 14 then: 1 if device.revoked?
  30. 1 content_tag(:span, "Revoked", class: "bg-red-600 text-white px-2 py-1 rounded-full text-xs")
  31. else: 13 else
  32. 13 content_tag(:span, "Active", class: "bg-green-600 text-white px-2 py-1 rounded-full text-xs")
  33. end
  34. end
  35. 1 def last_accessed_display(device)
  36. 11 else: 0 then: 11 return "Never" unless device.last_accessed_at
  37. time_ago_in_words(device.last_accessed_at) + " ago"
  38. end
  39. end

app/helpers/members_helper.rb

100.0% lines covered

100.0% branches covered

1 relevant lines. 1 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module MembersHelper
  2. end

app/helpers/permissions_helper.rb

47.83% lines covered

24.24% branches covered

69 relevant lines. 33 lines covered and 36 lines missed.
33 total branches, 8 branches covered and 25 branches missed.
    
  1. 1 module PermissionsHelper
  2. 1 include ActionView::Helpers::UrlHelper
  3. 1 def potential_users
  4. # Get all non-admin users (both members and organisers) sorted by username,
  5. # excluding current user
  6. users =
  7. 9 User
  8. .where
  9. .not(user_type: :admin)
  10. .where
  11. .not(id: Current.user.id)
  12. .order(:username)
  13. .pluck(:username, :id)
  14. 9 if users.any?
  15. then: 9 # Add a "Please select" option with empty value as the first option
  16. 9 [["Please select", ""]] + users
  17. else: 0 else
  18. []
  19. end
  20. end
  21. 1 def potential_items(owner)
  22. 83 invitable_klasses = [Event, Band, Member]
  23. 83 invitable_klasses.flat_map do |klass|
  24. 249 pluck_and_prefix(klass, owner)
  25. end
  26. end
  27. 1 def permission_status_color(status)
  28. case status.to_s
  29. when: 0 when "owned"
  30. "primary"
  31. when: 0 when "accepted"
  32. "success"
  33. when: 0 when "pending"
  34. "warning"
  35. when: 0 when "rejected"
  36. "danger"
  37. else: 0 else
  38. "secondary"
  39. end
  40. end
  41. 1 def get_access_level(user, item)
  42. 19 when: 8 case item
  43. 8 when: 6 when Member then get_ownership(user, item, :members, Member)
  44. 6 when: 5 when Band then get_ownership(user, item, :bands, Band)
  45. 5 else: 0 when Event then get_ownership(user, item, :events, Event)
  46. else raise "Invalid item type: #{item.class}"
  47. end
  48. end
  49. 1 def get_ownership(user, item, method, klass)
  50. 19 then: 18 if (direct_permission = user.send(method).find_by(id: item.id))
  51. 18 else: 1 direct_permission[:permission_type]
  52. 1 then: 0 else: 1 elsif klass.permitted_for(user.id).find_by(id: item.id)
  53. "view"
  54. end
  55. end
  56. 1 def find_effective_permission_source(user, item)
  57. # Direct permission check
  58. 9 direct_permission = Permission.where(
  59. user_id: user.id,
  60. item_type: item.class.to_s,
  61. item_id: item.id,
  62. status: ["owned", "accepted"]
  63. ).first
  64. 9 then: 9 else: 0 return nil if direct_permission
  65. else: 0 case item
  66. when: 0 when Band
  67. find_band_permission_source(user, item)
  68. when: 0 when Event
  69. find_event_permission_source(user, item)
  70. when: 0 when Member
  71. find_member_permission_source(user, item)
  72. end
  73. end
  74. 1 def find_band_permission_source(user, band)
  75. # Check permissions through members
  76. band.members.each do |member|
  77. permission = Permission.where(
  78. user_id: user.id,
  79. item_type: "Member",
  80. item_id: member.id,
  81. status: ["owned", "accepted"]
  82. ).first
  83. then: 0 else: 0 return permission if permission
  84. end
  85. # Check permissions through events
  86. band.events.each do |event|
  87. permission = Permission.where(
  88. user_id: user.id,
  89. item_type: "Event",
  90. item_id: event.id,
  91. status: ["owned", "accepted"]
  92. ).first
  93. then: 0 else: 0 return permission if permission
  94. end
  95. nil
  96. end
  97. 1 def find_event_permission_source(user, event)
  98. # Check permissions through bands
  99. event.bands.each do |band|
  100. permission = Permission.where(
  101. user_id: user.id,
  102. item_type: "Band",
  103. item_id: band.id,
  104. status: ["owned", "accepted"]
  105. ).first
  106. then: 0 else: 0 return permission if permission
  107. end
  108. # Check permissions through members in bands
  109. event.bands.each do |band|
  110. band.members.each do |member|
  111. permission = Permission.where(
  112. user_id: user.id,
  113. item_type: "Member",
  114. item_id: member.id,
  115. status: ["owned", "accepted"]
  116. ).first
  117. then: 0 else: 0 return permission if permission
  118. end
  119. end
  120. nil
  121. end
  122. 1 def find_member_permission_source(user, member)
  123. # Check permissions through bands
  124. member.bands.each do |band|
  125. permission = Permission.where(
  126. user_id: user.id,
  127. item_type: "Band",
  128. item_id: band.id,
  129. status: ["owned", "accepted"]
  130. ).first
  131. then: 0 else: 0 return permission if permission
  132. end
  133. # Check permissions through events via bands
  134. member.bands.each do |band|
  135. band.events.each do |event|
  136. permission = Permission.where(
  137. user_id: user.id,
  138. item_type: "Event",
  139. item_id: event.id,
  140. status: ["owned", "accepted"]
  141. ).first
  142. then: 0 else: 0 return permission if permission
  143. end
  144. end
  145. nil
  146. end
  147. 1 private
  148. 1 def pluck_and_prefix(klass, owner)
  149. 249 owned_permissions = owner.send(:"#{klass.to_s.downcase.pluralize}")
  150. 249 owned_item_ids = owned_permissions.pluck(:id)
  151. 249 items = klass.where(id: owned_item_ids).pluck(:id, :name)
  152. 249 items.map do |id, name|
  153. 102 ["#{name} (#{klass})", "#{klass}_#{id}"]
  154. end
  155. end
  156. end

app/helpers/sessions_helper.rb

100.0% lines covered

100.0% branches covered

1 relevant lines. 1 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module SessionsHelper
  2. end

app/helpers/users/registration_helper.rb

100.0% lines covered

100.0% branches covered

1 relevant lines. 1 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module Users::RegistrationHelper
  2. end

app/jobs/application_job.rb

100.0% lines covered

100.0% branches covered

1 relevant lines. 1 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 class ApplicationJob < ActiveJob::Base
  2. # Automatically retry jobs that encountered a deadlock
  3. # retry_on ActiveRecord::Deadlocked
  4. # Most jobs are safe to ignore if the underlying records are no longer available
  5. # discard_on ActiveJob::DeserializationError
  6. end

app/jobs/send_mail_job.rb

66.67% lines covered

100.0% branches covered

6 relevant lines. 4 lines covered and 2 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 class SendMailJob < ApplicationJob
  2. 1 queue_as :default
  3. 1 retry_on StandardError, wait: 1.minute, attempts: 5
  4. 1 def perform(user_mail_id)
  5. user_mail = UserMail.find(user_mail_id)
  6. user_mail.attempt_send
  7. end
  8. end

app/lib/form_builders/nice_form_builder.rb

100.0% lines covered

72.73% branches covered

91 relevant lines. 91 lines covered and 0 lines missed.
22 total branches, 16 branches covered and 6 branches missed.
    
  1. 14 module FormBuilders
  2. 14 class NiceFormBuilder < ActionView::Helpers::FormBuilder
  3. 14 class_attribute :text_field_helpers,
  4. default: field_helpers - %i[
  5. label
  6. check_box
  7. radio_button
  8. fields_for
  9. fields
  10. hidden_field
  11. file_field
  12. ]
  13. 14 TEXT_FIELD_STYLE = "text_field".freeze
  14. 14 TEXT_AREA_STYLE = "textarea_field".freeze
  15. 14 SELECT_FIELD_STYLE = "select_field".freeze
  16. 14 SUBMIT_BUTTON_STYLE = "primary_button".freeze
  17. 14 CHECKBOX_FIELD_STYLE = "checkbox_group".freeze
  18. 14 DATE_SELECT_STYLE = "date_select".freeze
  19. 14 TIME_SELECT_STYLE = "time_select".freeze
  20. 14 text_field_helpers.each do |field_method|
  21. 252 define_method(field_method) do |method, options = {}|
  22. 108 then: 54 if options.delete(:already_nice)
  23. 54 super(method, options)
  24. else: 54 else
  25. 54 text_like_field(field_method, method, options)
  26. end
  27. end
  28. end
  29. 14 def submit(value = nil, options = {})
  30. 29 custom_opts, opts = partition_custom_opts(options)
  31. 29 classes = apply_style_classes(SUBMIT_BUTTON_STYLE, custom_opts)
  32. 29 super(value, {class: classes}.merge(opts))
  33. end
  34. 14 def select(method, choices = nil, options = {}, html_options = {}, &block)
  35. 17 custom_opts, opts = partition_custom_opts(options)
  36. 17 classes = apply_style_classes(SELECT_FIELD_STYLE, custom_opts, method)
  37. 17 labels = labels(method, custom_opts[:label], options)
  38. 17 field = super(
  39. method,
  40. choices,
  41. opts,
  42. html_options.merge({class: classes}),
  43. &block
  44. )
  45. 17 @template.content_tag("div", labels + field, {class: "field"})
  46. end
  47. 14 def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
  48. 3 custom_opts, opts = partition_custom_opts(options)
  49. 3 classes = apply_style_classes(CHECKBOX_FIELD_STYLE, custom_opts, method)
  50. 3 labels = labels(method, custom_opts[:label], options)
  51. 3 field = super(method, opts.merge({class: classes}), checked_value, unchecked_value)
  52. 3 @template.content_tag("div", labels + field, {class: "field"})
  53. end
  54. 14 def collection_checkboxes(
  55. method,
  56. collection,
  57. value_method,
  58. text_method,
  59. options = {},
  60. html_options = {},
  61. &block
  62. )
  63. 5 custom_opts, opts = partition_custom_opts(options)
  64. 5 classes = apply_style_classes(
  65. CHECKBOX_FIELD_STYLE,
  66. custom_opts,
  67. method
  68. )
  69. 5 labels = labels(method, custom_opts[:label], options)
  70. 5 html_options = html_options.merge(class: classes)
  71. 5 check_boxes = super(
  72. method,
  73. collection,
  74. value_method,
  75. text_method,
  76. opts,
  77. html_options,
  78. &block
  79. )
  80. 5 @template.content_tag(
  81. "div",
  82. labels + check_boxes,
  83. {class: "field"}
  84. )
  85. end
  86. 14 def date_select(method, options = {}, html_options = {})
  87. 10 custom_opts, opts = partition_custom_opts(options)
  88. 10 classes = apply_style_classes(nil, custom_opts, method)
  89. 10 labels = labels(method, custom_opts[:label], options)
  90. 10 field = @template.content_tag(
  91. "div",
  92. super(
  93. method,
  94. opts,
  95. html_options.merge(class: classes)
  96. ),
  97. {class: "flex flex-row gap-4"}
  98. )
  99. 10 field_classes = ["field"]
  100. 10 then: 10 else: 0 field_classes << custom_opts[:field_class] if custom_opts[:field_class]
  101. 10 @template.content_tag(
  102. "div",
  103. labels + field,
  104. {class: field_classes.join(" ")}
  105. )
  106. end
  107. 14 def time_select(method, options = {}, html_options = {})
  108. 10 custom_opts, opts = partition_custom_opts(options)
  109. 10 classes = apply_style_classes(nil, custom_opts, method)
  110. 10 labels = labels(method, custom_opts[:label], options)
  111. 10 field = @template.content_tag(
  112. "div",
  113. super(
  114. method,
  115. opts,
  116. html_options.merge(class: classes)
  117. ),
  118. {class: "flex flex-row gap-4"}
  119. )
  120. 10 field_classes = ["field"]
  121. 10 then: 10 else: 0 field_classes << custom_opts[:field_class] if custom_opts[:field_class]
  122. 10 @template.content_tag(
  123. "div",
  124. labels + field,
  125. {class: field_classes.join(" ")}
  126. )
  127. end
  128. 14 private
  129. 14 def text_like_field(field_method, object_method, options = {})
  130. 54 custom_opts, opts = partition_custom_opts(options)
  131. 54 then: 0 else: 54 style = (field_method == :text_area) ? TEXT_AREA_STYLE : TEXT_FIELD_STYLE
  132. 54 classes = apply_style_classes(style, custom_opts, object_method)
  133. field_options = {
  134. 54 class: classes,
  135. then: 54 else: 0 title: errors_for(object_method)&.join(" ")
  136. }.compact.merge(opts).merge(already_nice: true)
  137. 54 field = send(field_method, object_method, field_options)
  138. 54 labels = labels(object_method, custom_opts[:label], options)
  139. 54 @template.content_tag("div", labels + field, {class: "field"})
  140. end
  141. 14 def labels(object_method, label_options, field_options)
  142. 99 label = nice_label(object_method, label_options, field_options)
  143. 99 error_label = error_label(object_method, field_options)
  144. 99 label + error_label
  145. end
  146. 14 def nice_label(object_method, label_options, field_options)
  147. text, label_opts =
  148. 103 then: 34 label_options.present? ?
  149. 34 else: 69 label_options.values_at(:text, :except) :
  150. 69 [nil, {}]
  151. 103 label_opts ||= {}
  152. 103 label_classes = label_opts[:class].to_s
  153. 103 then: 0 else: 103 label_classes += " disabled" if field_options[:disabled]
  154. 103 label(object_method, text, {
  155. class: label_classes.strip
  156. }.merge(label_opts.except(:class)))
  157. end
  158. 14 def error_label(object_method, options)
  159. 99 errors = errors_for(object_method)
  160. 99 then: 95 else: 4 return if errors.blank?
  161. 4 error_message = errors.collect(&:titleize).join(", ")
  162. 4 nice_label(
  163. object_method,
  164. {text: error_message, class: "error_message"},
  165. options
  166. )
  167. end
  168. 14 def border_color_classes(object_method)
  169. 128 else: 99 then: 29 return "" unless object_method
  170. 99 then: 4 else: 95 "has_error" if errors_for(object_method).present?
  171. end
  172. 14 def apply_style_classes(base_class, custom_opts, object_method = nil)
  173. [
  174. 128 base_class,
  175. border_color_classes(object_method),
  176. custom_opts[:class]
  177. ].compact.join(" ")
  178. end
  179. 14 CUSTOM_OPTS = [:label, :class, :field_class].freeze
  180. 14 def partition_custom_opts(opts)
  181. 216 opts.partition { |k, _| CUSTOM_OPTS.include?(k) }.map(&:to_h)
  182. end
  183. 14 def errors_for(object_method)
  184. 252 else: 252 then: 0 return unless @object.present? && object_method.present?
  185. 252 @object.errors[object_method]
  186. end
  187. end
  188. end

app/mailers/application_mailer.rb

100.0% lines covered

100.0% branches covered

2 relevant lines. 2 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 7 class ApplicationMailer < ActionMailer::Base
  2. 7 layout "mailer"
  3. end

app/mailers/test_mailer.rb

0.0% lines covered

100.0% branches covered

10 relevant lines. 0 lines covered and 10 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. class TestMailer < ApplicationMailer
  2. def test_email(to)
  3. Rails.logger.info "Preparing to send test email to #{to}."
  4. mail(
  5. to: to,
  6. subject: "Test Email",
  7. body: "This is a test email."
  8. )
  9. end
  10. end

app/mailers/user_mailer.rb

87.5% lines covered

50.0% branches covered

16 relevant lines. 14 lines covered and 2 lines missed.
2 total branches, 1 branches covered and 1 branches missed.
    
  1. # app/mailers/user_mailer.rb
  2. 7 class UserMailer < ApplicationMailer
  3. 7 CONFIRM_REGISTRATION_SUBJECT = "Thank you for joining Libregig"
  4. 7 def confirm_registration(user_mail)
  5. 1 @username = user_mail.params["username"]
  6. 1 @url = user_mail.params["url"]
  7. 1 mail(to: user_mail.recipient, subject: CONFIRM_REGISTRATION_SUBJECT)
  8. end
  9. 7 def self.send_confirmation_registration(user)
  10. 1 confirmation_token = user.confirmation_tokens.create!(token: SecureRandom.urlsafe_base64)
  11. 1 user_mail = UserMail.new(
  12. user: user,
  13. recipient: user.email,
  14. subject: CONFIRM_REGISTRATION_SUBJECT,
  15. template: "confirm_registration",
  16. params: {
  17. username: user.username,
  18. url: generate_confirmation_url(confirmation_token)
  19. }
  20. )
  21. 1 then: 1 if user_mail.save
  22. 1 SendMailJob.perform_later(user_mail.id)
  23. else: 0 else
  24. errors = user_mail.errors.full_messages.join(", ")
  25. Rails.logger.error("Failed to create UserMail: #{errors}")
  26. end
  27. 1 user_mail
  28. end
  29. 7 def self.generate_confirmation_url(confirmation_token)
  30. 2 Rails.application.routes.url_helpers.confirm_registration_url(
  31. token: confirmation_token.token,
  32. host: Rails.configuration.server_url
  33. )
  34. end
  35. end

app/models/application_record.rb

100.0% lines covered

100.0% branches covered

2 relevant lines. 2 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 class ApplicationRecord < ActiveRecord::Base
  2. 1 primary_abstract_class
  3. end

app/models/band.rb

100.0% lines covered

50.0% branches covered

22 relevant lines. 22 lines covered and 0 lines missed.
2 total branches, 1 branches covered and 1 branches missed.
    
  1. 1 class Band < ApplicationRecord
  2. 1 include RandomId
  3. 1 has_many :event_bands, dependent: :destroy
  4. 1 has_many :events, through: :event_bands
  5. 1 has_many :band_members, dependent: :restrict_with_error
  6. 1 has_many :members, through: :band_members
  7. 1 has_many :permission, as: :item, dependent: :destroy
  8. 1 has_many :linked_devices, as: :linkable, dependent: :nullify
  9. 1 has_many :bands_audits, dependent: :destroy
  10. 1 validates :description, presence: true
  11. 1 validates :name, presence: true
  12. 1 include Auditable
  13. 1 audit_log_columns :name, :description
  14. 1 attribute :permission_type, :string
  15. 1 scope :permitted_for, ->(user_id) {
  16. 149 select(
  17. "bands.*, #{BandPermissionQuery.with_permission_type_sql(user_id)}"
  18. )
  19. .where(BandPermissionQuery.permission_sql(user_id))
  20. }
  21. 1 def owner
  22. 73 then: 73 else: 0 permission.where(status: :owned).first&.user
  23. end
  24. 1 def url
  25. 1 Rails.application.routes.url_helpers.edit_band_path(self)
  26. end
  27. 1 def editable?
  28. 2 permission_type == "edit"
  29. end
  30. end

app/models/band_member.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 16 class BandMember < ApplicationRecord
  2. 16 include RandomId
  3. 16 belongs_to :member
  4. 16 belongs_to :band
  5. end

app/models/bands_audit.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 7 class BandsAudit < ApplicationRecord
  2. 7 self.table_name = "bands_audit"
  3. 7 belongs_to :band
  4. 7 belongs_to :user
  5. end

app/models/concerns/auditable.rb

100.0% lines covered

100.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 module Auditable
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 3 after_update_commit :log_changes
  5. end
  6. 1 def log_changes
  7. 21 changes_to_log.each do |column, values|
  8. 30 audit_class.create!(
  9. item_id => id,
  10. :column_changed => column,
  11. :old_value => values.first,
  12. :user_id => Current.user.id
  13. )
  14. end
  15. end
  16. 1 private
  17. 1 def changes_to_log
  18. 21 saved_changes.slice(*self.class.logged_columns)
  19. end
  20. 1 def audit_class
  21. 30 "#{self.class.name.pluralize}Audit".constantize
  22. end
  23. 1 def item_id
  24. 30 "#{self.class.name.downcase}_id"
  25. end
  26. 1 class_methods do
  27. 1 def audit_log_columns(*columns)
  28. 3 @logged_columns = columns.map(&:to_s)
  29. end
  30. 1 def logged_columns
  31. 21 @logged_columns || []
  32. end
  33. end
  34. end

app/models/concerns/random_id.rb

100.0% lines covered

50.0% branches covered

9 relevant lines. 9 lines covered and 0 lines missed.
2 total branches, 1 branches covered and 1 branches missed.
    
  1. 1 module RandomId
  2. 1 extend ActiveSupport::Concern
  3. 1 included do
  4. 53 before_create :randomise_id
  5. end
  6. 1 private
  7. 1 def randomise_id
  8. 2440 loop do
  9. 2440 self.id = SecureRandom.random_number(4294967295)
  10. 2440 else: 0 then: 2440 break unless self.class.where(id: id).exists?
  11. end
  12. end
  13. end

app/models/confirmation_token.rb

100.0% lines covered

100.0% branches covered

8 relevant lines. 8 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 10 class ConfirmationToken < ApplicationRecord
  2. 10 belongs_to :user
  3. 10 before_create :set_expires_at, unless: :expires_at?
  4. 10 validates :token, presence: true, uniqueness: true
  5. 14 scope :valid, -> { where("expires_at > ?", Time.current) }
  6. 10 private
  7. 10 def set_expires_at
  8. 5 self.expires_at = 24.hours.from_now
  9. end
  10. end

app/models/current.rb

100.0% lines covered

75.0% branches covered

19 relevant lines. 19 lines covered and 0 lines missed.
4 total branches, 3 branches covered and 1 branches missed.
    
  1. 1 class Current < ActiveSupport::CurrentAttributes
  2. 1 attribute :_user
  3. 1 attribute :_impersonator
  4. 1749 resets { Time.zone = nil }
  5. 1 def user=(user)
  6. 543 self._user = user
  7. 543 update_time_zone
  8. end
  9. 1 def user
  10. 4498 _user
  11. end
  12. 1 def impersonator=(impersonator)
  13. 535 self._impersonator = impersonator
  14. 535 update_time_zone
  15. end
  16. 1 def impersonator
  17. 1 _impersonator
  18. end
  19. 1 def impersonating?
  20. 489 _impersonator.present?
  21. end
  22. 1 private
  23. 1 def update_time_zone
  24. 1078 then: 696 else: 382 then: 0 else: 382 Time.zone = _user&.time_zone || _impersonator&.time_zone
  25. end
  26. end

app/models/event.rb

97.3% lines covered

70.0% branches covered

37 relevant lines. 36 lines covered and 1 lines missed.
10 total branches, 7 branches covered and 3 branches missed.
    
  1. 1 class Event < ApplicationRecord
  2. 1 include EventsHelper
  3. 1 include RandomId
  4. 1 has_many :event_bands, dependent: :destroy
  5. 1 has_many :bands, through: :event_bands
  6. 1 has_many :permissions, as: :item, dependent: :destroy
  7. 1 has_many :linked_devices, as: :linkable, dependent: :nullify
  8. 1 has_many :events_audits, dependent: :destroy
  9. 1 validate :end_date_nil_or_after_start
  10. 1 before_validation :set_defaults
  11. 1 attribute :permission_type, :string
  12. 3 scope :past, -> { where(<<~SQL) }
  13. start_date IS NULL OR
  14. COALESCE(end_date, start_date) <= CURRENT_TIMESTAMP
  15. SQL
  16. 177 scope :future, -> { where(<<~SQL) }
  17. start_date IS NULL OR
  18. COALESCE(end_date, start_date) >= CURRENT_TIMESTAMP
  19. SQL
  20. 1 scope :permitted_for, ->(user_id) {
  21. 321 select(
  22. "events.*, #{EventPermissionQuery.with_permission_type_sql(user_id)}"
  23. )
  24. .where(EventPermissionQuery.permission_sql(user_id))
  25. }
  26. 1 include Auditable
  27. 1 audit_log_columns :name, :description, :start_date, :end_date
  28. 1 def owner
  29. 76 then: 76 else: 0 permissions.where(status: :owned).first&.user
  30. end
  31. 1 def url
  32. 1 Rails.application.routes.url_helpers.edit_event_path(self)
  33. end
  34. 1 def external_url
  35. 1 Rails.application.routes.url_helpers.edit_event_url(self, host: Rails.application.config.server_url)
  36. end
  37. 1 def editable?
  38. 3 permission_type == "edit"
  39. end
  40. 1 def duration
  41. 3 then: 3 else: 0 if start_date.present? && end_date.present?
  42. 3 convert_seconds_to_duration(end_date - start_date)
  43. end
  44. end
  45. 1 private
  46. 1 def set_defaults
  47. 240 then: 233 else: 7 if start_date.present?
  48. 233 self.end_date ||= start_date
  49. 233 then: 1 else: 232 if end_date && end_date < start_date
  50. 1 self.end_date = start_date
  51. end
  52. end
  53. end
  54. 1 def end_date_nil_or_after_start
  55. 240 then: 0 else: 240 if end_date.present? && start_date.present? && end_date < start_date
  56. errors.add(:end_date, "Must be before start")
  57. end
  58. end
  59. end

app/models/event_band.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 class EventBand < ApplicationRecord
  2. 1 include RandomId
  3. 1 belongs_to :event
  4. 1 belongs_to :band
  5. end

app/models/events_audit.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 9 class EventsAudit < ApplicationRecord
  2. 9 self.table_name = "events_audit"
  3. 9 belongs_to :event
  4. 9 belongs_to :user
  5. end

app/models/linked_device.rb

98.46% lines covered

70.0% branches covered

65 relevant lines. 64 lines covered and 1 lines missed.
10 total branches, 7 branches covered and 3 branches missed.
    
  1. 16 class LinkedDevice < ApplicationRecord
  2. 16 include RandomId
  3. 16 belongs_to :user
  4. 16 has_many :linked_device_linkables, dependent: :destroy
  5. 206 has_many :event_linkables, -> { where(linkable_type: "Event") },
  6. class_name: "LinkedDeviceLinkable"
  7. 203 has_many :band_linkables, -> { where(linkable_type: "Band") },
  8. class_name: "LinkedDeviceLinkable"
  9. 201 has_many :member_linkables, -> { where(linkable_type: "Member") },
  10. class_name: "LinkedDeviceLinkable"
  11. 16 enum :device_type, {
  12. api: 0,
  13. web: 1,
  14. ical: 2
  15. }
  16. 16 validates :name, presence: true
  17. 16 validates :device_type, presence: true
  18. 16 validates :secret, presence: true, uniqueness: true
  19. 16 before_validation :generate_secret, on: :create
  20. 16 before_destroy :ensure_never_accessed
  21. 34 scope :active, -> { where(revoked_at: nil) }
  22. 18 scope :revoked, -> { where.not(revoked_at: nil) }
  23. # For checkboxes
  24. 16 attr_accessor :event_ids, :band_ids, :member_ids, :user_account
  25. # Skip validation flag - used in tests
  26. 16 attr_accessor :skip_access_validation
  27. 16 alias_method :skip_access_validation?, :skip_access_validation
  28. 16 def skip_access_validation!
  29. 3 @skip_access_validation = true
  30. end
  31. # Dynamic getters for linkable IDs
  32. 16 %w[event band member].each do |resource|
  33. 48 define_method(:"#{resource}_ids") do
  34. 295 instance_variable_get(:"@#{resource}_ids") ||
  35. 5 send(:"#{resource}_linkables").map { |l| l.linkable_id.to_s }
  36. end
  37. 48 define_method(:"#{resource}_ids=") do |ids|
  38. 11 instance_variable_set(:"@#{resource}_ids", Array(ids).compact_blank.map(&:to_s))
  39. end
  40. end
  41. # Process the IDs after save
  42. 16 after_save :process_linkables
  43. 16 def revoked?
  44. 22 revoked_at.present?
  45. end
  46. 16 def revoke!
  47. 2 update!(revoked_at: Time.current)
  48. end
  49. 16 def touch_access!
  50. 11 update!(last_accessed_at: Time.current)
  51. end
  52. 16 def accessed?
  53. 11 last_accessed_at.present?
  54. end
  55. 16 def has_specific_access?
  56. 13 linked_device_linkables.any?
  57. end
  58. 16 def access_type
  59. then: 0 else: 0 has_specific_access? ? "specific" : "full"
  60. end
  61. 16 def calendar_url
  62. 6 else: 4 then: 2 return nil unless web?
  63. 4 Rails.application.routes.url_helpers.calendar_url(
  64. secret,
  65. host: Rails.application.config.server_url
  66. )
  67. end
  68. 16 def ical_url
  69. 6 else: 5 then: 1 return nil unless ical? || web?
  70. 5 base_url = Rails.application.routes.url_helpers.ical_feed_url(
  71. secret,
  72. host: Rails.application.config.server_url
  73. )
  74. 5 "#{base_url}.ics"
  75. end
  76. 16 private
  77. 16 def generate_secret
  78. 79 self.secret ||= SecureRandom.hex(32)
  79. end
  80. 16 def ensure_never_accessed
  81. 3 then: 1 else: 2 if last_accessed_at.present?
  82. 1 errors.add(:base, "Cannot delete a device that has been accessed")
  83. 1 throw :abort
  84. end
  85. end
  86. 16 def process_linkables
  87. 97 else: 97 then: 0 return unless persisted?
  88. 97 %w[Event Band Member].each do |type|
  89. 291 process_linkable_type(type, send(:"#{type.downcase}_ids"))
  90. end
  91. end
  92. 16 def process_linkable_type(type, ids)
  93. 291 current = send(:"#{type.downcase}_linkables")
  94. 291 current_ids = current.pluck(:linkable_id).map(&:to_s)
  95. 291 new_linkables = ids - current_ids
  96. 291 new_linkables.each do |id|
  97. 10 linked_device_linkables.create!(
  98. linkable_type: type,
  99. linkable_id: id
  100. )
  101. end
  102. 291 old_linkables = current_ids - ids
  103. 291 current.where(linkable_id: old_linkables).destroy_all
  104. end
  105. end

app/models/linked_device_linkable.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 14 class LinkedDeviceLinkable < ApplicationRecord
  2. 14 belongs_to :linked_device
  3. 14 belongs_to :linkable, polymorphic: true
  4. 14 validates :linked_device_id, uniqueness: {
  5. scope: [:linkable_type, :linkable_id],
  6. message: "is already linked to this resource"
  7. }
  8. end

app/models/member.rb

71.88% lines covered

0.0% branches covered

32 relevant lines. 23 lines covered and 9 lines missed.
8 total branches, 0 branches covered and 8 branches missed.
    
  1. 1 class Member < ApplicationRecord
  2. 1 include RandomId
  3. 1 has_many :band_members, dependent: :destroy
  4. 1 has_many :bands, through: :band_members
  5. 1 has_many :member_skills, dependent: :destroy
  6. 1 has_many :skills, through: :member_skills
  7. 255 validates :skills, presence: true, if: -> { skills_list.present? }
  8. 1 has_many :permission, as: :item, dependent: :destroy
  9. 1 has_many :linked_devices, as: :linkable, dependent: :nullify
  10. 1 has_many :members_audits, dependent: :destroy
  11. 1 include Auditable
  12. 1 audit_log_columns :name, :description
  13. 1 attribute :permission_type, :string
  14. 1 scope :permitted_for, ->(user_id) {
  15. 125 select(
  16. "members.*, #{MemberPermissionQuery.with_permission_type_sql(user_id)}"
  17. )
  18. .where(MemberPermissionQuery.permission_sql(user_id))
  19. }
  20. 1 def owner
  21. then: 0 else: 0 Permission.where(
  22. status: :owned,
  23. item_type: "Member",
  24. item_id: id
  25. ).first&.user
  26. end
  27. 1 def skills_list
  28. 256 skills.pluck(:name).join(", ")
  29. end
  30. 1 def skills_list=(skills_string)
  31. then: 0 else: 0 return if skills_string.blank?
  32. skill_names =
  33. skills_string
  34. .downcase
  35. .split(",")
  36. .map(&:strip)
  37. .compact_blank
  38. .uniq
  39. then: 0 else: 0 return if skill_names.empty?
  40. transaction do
  41. member_skills.destroy_all
  42. skill_names.each do |name|
  43. skill = Skill.find_or_create_by!(name: name)
  44. else: 0 then: 0 member_skills.create!(skill: skill) unless member_skills.exists?(skill: skill)
  45. end
  46. end
  47. end
  48. 1 def editable?
  49. 2 permission_type == "edit"
  50. end
  51. 1 def events
  52. 1 Event.joins(:bands)
  53. .where(bands: {id: bands.pluck(:id)})
  54. .distinct
  55. end
  56. end

app/models/member_skill.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 16 class MemberSkill < ApplicationRecord
  2. 16 belongs_to :member
  3. 16 belongs_to :skill
  4. 16 validates :member_id, uniqueness: {scope: :skill_id}
  5. end

app/models/members_audit.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 5 class MembersAudit < ApplicationRecord
  2. 5 self.table_name = "members_audit"
  3. 5 belongs_to :member
  4. 5 belongs_to :user
  5. end

app/models/permission.rb

89.58% lines covered

88.89% branches covered

48 relevant lines. 43 lines covered and 5 lines missed.
18 total branches, 16 branches covered and 2 branches missed.
    
  1. 16 class Permission < ApplicationRecord
  2. 16 include RandomId
  3. 16 include PermissionsHelper
  4. 16 belongs_to :user, class_name: "User"
  5. 16 belongs_to :bestowing_user, class_name: "User", optional: true
  6. 16 belongs_to :item, polymorphic: true
  7. 16 scope :accepted, -> { where(status: :accepted) }
  8. 16 scope :accepted_or_owned, -> { where(status: %i[accepted owned]) }
  9. 16 scope :item_type, ->(type) { where(item_type: type) }
  10. 23 scope :for_user, ->(user) { where("bestowing_user_id = :user_id OR user_id = :user_id", user_id: user.id) }
  11. 16 def self.join_models(model)
  12. table_name = model.to_s.pluralize
  13. klass_name = model.to_s.classify
  14. joins("
  15. JOIN #{table_name}
  16. ON #{table_name}.id = permissions.item_id
  17. AND permissions.item_type = '#{klass_name}'
  18. ")
  19. end
  20. 16 scope :bands, -> { join_models(:band) }
  21. 16 scope :events, -> { join_models(:event) }
  22. 16 scope :members, -> { join_models(:member) }
  23. 16 scope :system_permissions, -> { where(bestowing_user: nil) }
  24. 16 validates :item_type, presence: true
  25. 16 validates :permission_type, presence: true, inclusion: {in: %w[view edit]}
  26. 16 validates :status, presence: true, inclusion: {in: %w[owned pending accepted rejected]}
  27. 16 validate :bestowing_user_must_be_organiser_or_nil, :user_must_be_member_or_organiser
  28. 16 validate :valid_item
  29. 16 validate :valid_user
  30. 16 enum :status, {
  31. owned: "owned",
  32. pending: "pending",
  33. accepted: "accepted",
  34. rejected: "rejected"
  35. }
  36. 16 def item_path
  37. 7 helper = Rails.application.routes.url_helpers
  38. 7 when: 2 case item_type
  39. 2 when: 3 when "Band" then helper.band_path(item)
  40. 3 when: 2 when "Event" then helper.event_path(item)
  41. 2 else: 0 when "Member" then helper.member_path(item)
  42. else raise "Invalid item type: #{item_type}"
  43. end
  44. end
  45. 16 private
  46. 16 def valid_item
  47. 634 then: 559 else: 75 return true if bestowing_user.nil?
  48. 75 item_ids = potential_items(bestowing_user).pluck(1)
  49. 75 item_str = "#{item_type}_#{item_id}"
  50. 75 else: 72 then: 3 unless item_ids.include?(item_str)
  51. 3 errors.add(:item, "is not a valid selection for #{bestowing_user}. Valid options: #{item_ids}")
  52. end
  53. end
  54. 16 def valid_user
  55. # TODO: make this fancier
  56. 634 else: 632 then: 0 unless User.find(user_id)
  57. errors.add(:user, "is not a valid selection")
  58. end
  59. end
  60. 16 def bestowing_user_must_be_organiser_or_nil
  61. 634 else: 633 then: 1 unless bestowing_user.nil? || bestowing_user.organiser?
  62. 1 errors.add(:bestowing_user, "must be an organiser")
  63. end
  64. end
  65. 16 def user_must_be_member_or_organiser
  66. 634 then: 632 else: 2 then: 209 else: 2 else: 631 then: 3 unless user&.organiser? || user&.member?
  67. 3 errors.add(:user, "must be a member or organiser")
  68. end
  69. end
  70. end

app/models/skill.rb

100.0% lines covered

100.0% branches covered

4 relevant lines. 4 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 1 class Skill < ApplicationRecord
  2. 1 has_many :member_skills, dependent: :destroy
  3. 1 has_many :members, through: :member_skills
  4. 1 validates :name, presence: true
  5. end

app/models/user.rb

100.0% lines covered

100.0% branches covered

34 relevant lines. 34 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. 1 class User < ApplicationRecord
  2. 1 include RandomId
  3. 1 include PermissionsHelper
  4. 1 enum :user_type, {admin: 0, member: 1, organiser: 2}
  5. 1 has_secure_password
  6. 1 has_many :permissions,
  7. dependent: :destroy,
  8. inverse_of: :user
  9. 1 def members
  10. 116 Member.permitted_for(id)
  11. end
  12. 1 def bands
  13. 141 Band.permitted_for(id)
  14. end
  15. 1 def events
  16. 305 Event.permitted_for(id)
  17. end
  18. 1 has_many :confirmation_tokens, dependent: :destroy
  19. 1 has_many :linked_devices, dependent: :destroy
  20. 643 then: 639 else: 3 before_save { email&.downcase! }
  21. 1 before_validation :set_default_time_zone
  22. 1 validates :username,
  23. presence: true,
  24. uniqueness: true
  25. 1 validates :email,
  26. presence: {message: "can't be blank"},
  27. uniqueness: true,
  28. format: {with: URI::MailTo::EMAIL_REGEXP, message: "is invalid", allow_blank: true}
  29. 1 validates :password,
  30. presence: true,
  31. length: {minimum: 6},
  32. if: :password_required?,
  33. allow_blank: true
  34. 1 validates :password_confirmation,
  35. presence: true,
  36. allow_blank: true
  37. 1 validates :time_zone,
  38. inclusion: {
  39. 151 in: ActiveSupport::TimeZone.all.map { |t| t.tzinfo.name },
  40. message: "%{value} is not a valid time zone"
  41. }, allow_blank: true
  42. 1 def confirmed?
  43. 362 confirmed || admin?
  44. end
  45. 1 def to_param
  46. 527 username
  47. end
  48. 1 def owned_links
  49. 2 then: 1 else: 1 return @owned_links if defined?(@owned_links)
  50. @owned_links =
  51. 1 bands.select(:name, :id) +
  52. events.select(:name, :id) +
  53. members.select(:name, :id)
  54. end
  55. 1 private
  56. 1 def password_required?
  57. 1335 new_record? || password.present?
  58. end
  59. 1 def set_default_time_zone
  60. 666 then: 8 else: 658 self.time_zone = "Etc/UTC" if time_zone.blank?
  61. end
  62. end

app/models/user_mail.rb

100.0% lines covered

75.0% branches covered

23 relevant lines. 23 lines covered and 0 lines missed.
4 total branches, 3 branches covered and 1 branches missed.
    
  1. 1 class UserMail < ApplicationRecord
  2. 1 belongs_to :user
  3. 1 enum :state, {pending: 0, sending: 1, sent: 2, failed: 3}
  4. 1 validates :subject, presence: true, length: {maximum: 120}
  5. 1 validates :recipient, presence: true, length: {maximum: 255}
  6. 1 validates :template, presence: true, length: {maximum: 50}
  7. 1 validates :params, presence: true
  8. 1 def html_content
  9. 4 content(:html)
  10. end
  11. 1 def text_content
  12. 4 content(:text)
  13. end
  14. 1 def attempt_send
  15. 3 else: 2 then: 1 return unless pending?
  16. 2 update!(state: :sending)
  17. begin
  18. 2 UserMailer.confirm_registration(self).deliver_now
  19. 1 update!(state: :sent)
  20. 1 Rails.logger.info("Mail sent successfully to #{recipient}.")
  21. rescue => e
  22. 1 then: 0 else: 1 raise e if Rails.env.development?
  23. 1 update!(state: :failed)
  24. 1 Rails.logger.error("Failed to send mail to #{recipient}: #{e}")
  25. end
  26. end
  27. 1 private
  28. 1 def content(format)
  29. 8 ApplicationController.render(
  30. layout: false,
  31. template: "user_mailer/#{template}",
  32. assigns: params,
  33. formats: [format]
  34. )
  35. end
  36. end

app/queries/band_permission_query.rb

100.0% lines covered

100.0% branches covered

17 relevant lines. 17 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 16 class BandPermissionQuery
  2. 16 class << self
  3. 16 def permission_sql(user_id)
  4. 149 <<-SQL
  5. EXISTS (
  6. SELECT 1 FROM permissions
  7. WHERE permissions.user_id = #{user_id}
  8. AND permissions.status IN ('owned', 'accepted')
  9. AND (
  10. /* Check three ways you might directly have permission */
  11. (#{direct_permission_sql})
  12. OR
  13. (#{through_member_sql})
  14. OR
  15. (#{through_event_sql})
  16. )
  17. )
  18. /* Or check indirect permissions through event relationships */
  19. OR bands.id IN (
  20. /* Can see bands at events you have have permission for other bands
  21. at */
  22. #{shares_event_with_permitted_band_sql(user_id)}
  23. )
  24. OR bands.id IN (
  25. /* Can see bands at events you have have permission for members in
  26. bands at */
  27. #{shares_event_with_band_with_permitted_member_sql(user_id)}
  28. )
  29. SQL
  30. end
  31. 16 def with_permission_type_sql(user_id)
  32. 149 <<~SQL
  33. CASE
  34. WHEN EXISTS (
  35. SELECT 1 FROM permissions
  36. WHERE permissions.user_id = #{user_id}
  37. AND permissions.status IN ('owned', 'accepted')
  38. AND permissions.item_type = 'Band'
  39. AND permissions.item_id = bands.id
  40. ) THEN (
  41. SELECT permission_type FROM permissions
  42. WHERE permissions.user_id = #{user_id}
  43. AND permissions.status IN ('owned', 'accepted')
  44. AND permissions.item_type = 'Band'
  45. AND permissions.item_id = bands.id
  46. LIMIT 1
  47. )
  48. ELSE 'view'
  49. END as permission_type
  50. SQL
  51. end
  52. 16 private
  53. 16 def direct_permission_sql
  54. 149 <<-SQL
  55. /* Simplest case: you have direct permission to the band */
  56. permissions.item_type = 'Band'
  57. AND permissions.item_id = bands.id
  58. SQL
  59. end
  60. 16 def through_member_sql
  61. 149 <<-SQL
  62. /* You have permission to a member who is in this band */
  63. permissions.item_type = 'Member'
  64. AND permissions.item_id IN (
  65. /* Find all members in this band */
  66. SELECT member_id
  67. FROM band_members
  68. WHERE band_id = bands.id
  69. )
  70. SQL
  71. end
  72. 16 def through_event_sql
  73. 149 <<-SQL
  74. /* You have permission to an event this band is playing at */
  75. permissions.item_type = 'Event'
  76. AND permissions.item_id IN (
  77. /* Find all events this band is playing at */
  78. SELECT event_id
  79. FROM event_bands
  80. WHERE band_id = bands.id
  81. )
  82. SQL
  83. end
  84. 16 def shares_event_with_permitted_band_sql(user_id)
  85. 149 <<~SQL
  86. /* Find bands that are playing at the same events as bands you can access */
  87. SELECT band.id
  88. FROM bands AS band
  89. /* First, find events this band is playing at */
  90. JOIN event_bands AS event_band
  91. ON event_band.band_id = band.id
  92. /* Then find other bands playing at those same events */
  93. JOIN event_bands AS other_event_band
  94. ON other_event_band.event_id = event_band.event_id
  95. /* Check if you have permission to any of those other bands */
  96. JOIN permissions AS p
  97. ON p.item_type = 'Band'
  98. AND p.item_id = other_event_band.band_id
  99. AND p.user_id = #{user_id}
  100. AND p.status IN ('owned', 'accepted')
  101. SQL
  102. end
  103. 16 def shares_event_with_band_with_permitted_member_sql(user_id)
  104. 149 <<-SQL
  105. /* Find bands that are playing at events where there's another band
  106. that has a member you have permission to */
  107. SELECT band.id
  108. FROM bands AS band
  109. /* First, find events this band is playing at */
  110. JOIN event_bands AS event_band
  111. ON event_band.band_id = band.id
  112. /* Then find other bands playing at those same events */
  113. JOIN event_bands AS other_event_band
  114. ON other_event_band.event_id = event_band.event_id
  115. /* Find members in those other bands */
  116. JOIN band_members AS bm
  117. ON bm.band_id = other_event_band.band_id
  118. /* Check if you have permission to any of those members */
  119. JOIN permissions AS p
  120. ON p.item_type = 'Member'
  121. AND p.item_id = bm.member_id
  122. AND p.user_id = #{user_id}
  123. AND p.status IN ('owned', 'accepted')
  124. SQL
  125. end
  126. end
  127. end

app/queries/event_permission_query.rb

100.0% lines covered

100.0% branches covered

13 relevant lines. 13 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 16 class EventPermissionQuery
  2. 16 class << self
  3. 16 def permission_sql(user_id)
  4. 321 <<-SQL
  5. EXISTS (
  6. SELECT 1 FROM permissions
  7. WHERE permissions.user_id = #{user_id}
  8. AND permissions.status IN ('owned', 'accepted')
  9. AND (
  10. /* Check three ways you might have permission */
  11. (#{direct_permission_sql})
  12. OR
  13. (#{through_band_sql})
  14. OR
  15. (#{through_member_sql})
  16. )
  17. )
  18. SQL
  19. end
  20. 16 def with_permission_type_sql(user_id)
  21. 321 <<~SQL
  22. CASE
  23. WHEN EXISTS (
  24. SELECT 1 FROM permissions
  25. WHERE permissions.user_id = #{user_id}
  26. AND permissions.status IN ('owned', 'accepted')
  27. AND permissions.item_type = 'Event'
  28. AND permissions.item_id = events.id
  29. ) THEN (
  30. SELECT permission_type FROM permissions
  31. WHERE permissions.user_id = #{user_id}
  32. AND permissions.status IN ('owned', 'accepted')
  33. AND permissions.item_type = 'Event'
  34. AND permissions.item_id = events.id
  35. LIMIT 1
  36. )
  37. ELSE 'view'
  38. END as permission_type
  39. SQL
  40. end
  41. 16 private
  42. 16 def direct_permission_sql
  43. 321 <<-SQL
  44. /* Simplest case: you have direct permission to the event */
  45. permissions.item_type = 'Event'
  46. AND permissions.item_id = events.id
  47. SQL
  48. end
  49. 16 def through_band_sql
  50. 321 <<-SQL
  51. /* You have permission to a band that's playing at this event */
  52. permissions.item_type = 'Band'
  53. AND permissions.item_id IN (
  54. /* Find all bands playing at this event */
  55. SELECT band_id
  56. FROM event_bands
  57. WHERE event_id = events.id
  58. )
  59. SQL
  60. end
  61. 16 def through_member_sql
  62. 321 <<-SQL
  63. /* You have permission to a member who's playing at this event */
  64. permissions.item_type = 'Member'
  65. AND permissions.item_id IN (
  66. /* Find all members playing at this event */
  67. SELECT members.id
  68. FROM members
  69. /* Connect members to their bands */
  70. JOIN band_members
  71. ON band_members.member_id = members.id
  72. /* Connect bands to the events they're playing */
  73. JOIN event_bands
  74. ON event_bands.band_id = band_members.band_id
  75. WHERE event_bands.event_id = events.id
  76. )
  77. SQL
  78. end
  79. end
  80. end

app/queries/member_permission_query.rb

100.0% lines covered

100.0% branches covered

18 relevant lines. 18 lines covered and 0 lines missed.
0 total branches, 0 branches covered and 0 branches missed.
    
  1. 16 class MemberPermissionQuery
  2. 16 class << self
  3. 16 def permission_sql(user_id)
  4. 125 <<~SQL
  5. EXISTS (
  6. SELECT 1 FROM permissions
  7. WHERE (
  8. permissions.user_id = #{user_id}
  9. AND permissions.status IN ('owned', 'accepted')
  10. AND (
  11. /* Check three ways you might directly have permission */
  12. (#{direct_permission_sql})
  13. OR
  14. (#{through_band_sql})
  15. OR
  16. (#{through_event_sql})
  17. )
  18. )
  19. /* Or check indirect permissions through various relationships */
  20. OR members.id IN (
  21. /* Can see members who are in bands with members you can access */
  22. #{shares_band_with_permitted_member_sql(user_id)}
  23. )
  24. OR members.id IN (
  25. /* Can see members who are playing at events with members you can access */
  26. #{shares_event_with_permitted_member_sql(user_id)}
  27. )
  28. OR members.id IN (
  29. /* Can see members who are in bands playing at events with bands you can access */
  30. #{shares_event_with_permitted_band_sql(user_id)}
  31. )
  32. )
  33. SQL
  34. end
  35. 16 def with_permission_type_sql(user_id)
  36. 125 <<~SQL
  37. CASE
  38. WHEN EXISTS (
  39. SELECT 1 FROM permissions
  40. WHERE permissions.user_id = #{user_id}
  41. AND permissions.status IN ('owned', 'accepted')
  42. AND permissions.item_type = 'Member'
  43. AND permissions.item_id = members.id
  44. ) THEN (
  45. SELECT permission_type FROM permissions
  46. WHERE permissions.user_id = #{user_id}
  47. AND permissions.status IN ('owned', 'accepted')
  48. AND permissions.item_type = 'Member'
  49. AND permissions.item_id = members.id
  50. LIMIT 1
  51. )
  52. ELSE 'view'
  53. END as permission_type
  54. SQL
  55. end
  56. 16 def direct_permission_sql
  57. 125 <<~SQL
  58. /* Simplest case: you have direct permission to see this member */
  59. permissions.item_type = 'Member'
  60. AND permissions.item_id = members.id
  61. SQL
  62. end
  63. 16 def through_band_sql
  64. 125 <<~SQL
  65. /* You have permission to a band this member plays in */
  66. permissions.item_type = 'Band'
  67. AND permissions.item_id IN (
  68. /* Find all bands this member is in */
  69. SELECT band_id FROM band_members
  70. WHERE member_id = members.id
  71. )
  72. SQL
  73. end
  74. 16 def through_event_sql
  75. 125 <<~SQL
  76. /* You have permission to an event where this member is playing */
  77. permissions.item_type = 'Event'
  78. AND permissions.item_id IN (
  79. /* Find all events where this member is playing */
  80. SELECT events.id FROM events
  81. /* Connect events to bands playing at them */
  82. JOIN event_bands ON event_bands.event_id = events.id
  83. /* Connect bands to their members */
  84. JOIN band_members ON band_members.band_id = event_bands.band_id
  85. WHERE band_members.member_id = members.id
  86. )
  87. SQL
  88. end
  89. 16 def shares_band_with_permitted_member_sql(user_id)
  90. 130 <<~SQL
  91. /* Find members who are in the same bands as members you can access */
  92. SELECT DISTINCT other_members.member_id AS id
  93. FROM band_members AS other_members
  94. /* Find bands where there's at least one member you have permission for */
  95. WHERE other_members.band_id IN (
  96. SELECT band_members.band_id
  97. FROM band_members
  98. JOIN permissions AS p
  99. ON p.item_type = 'Member'
  100. AND p.item_id = band_members.member_id
  101. WHERE p.user_id = #{user_id}
  102. AND p.status IN ('owned', 'accepted')
  103. )
  104. SQL
  105. end
  106. 16 def shares_event_with_permitted_member_sql(user_id)
  107. 125 <<~SQL
  108. /* Find members who are playing at the same events as members you can access */
  109. SELECT band_member.member_id AS id
  110. /* Start with a member in a band */
  111. FROM band_members AS band_member
  112. /* Find events where their band is playing */
  113. JOIN event_bands AS event_band
  114. ON event_band.band_id = band_member.band_id
  115. /* Find other bands at those same events */
  116. JOIN event_bands AS other_event_band
  117. ON other_event_band.event_id = event_band.event_id
  118. /* Find members in those other bands */
  119. JOIN band_members AS other_band_member
  120. ON other_band_member.band_id = other_event_band.band_id
  121. /* Check that you have permission to one of those other members */
  122. JOIN permissions AS p
  123. ON p.item_type = 'Member'
  124. AND p.item_id = other_band_member.member_id
  125. AND p.user_id = #{user_id}
  126. AND p.status IN ('owned', 'accepted')
  127. SQL
  128. end
  129. 16 def shares_event_with_permitted_band_sql(user_id)
  130. 125 <<~SQL
  131. /* Find members who are playing at events where you have permission to another band */
  132. SELECT band_member.member_id AS id
  133. /* Start with a member in a band */
  134. FROM band_members AS band_member
  135. /* Find events where their band is playing */
  136. JOIN event_bands AS event_band
  137. ON event_band.band_id = band_member.band_id
  138. /* Find other bands at those same events */
  139. JOIN event_bands AS other_event_band
  140. ON other_event_band.event_id = event_band.event_id
  141. /* Check that you have permission to one of those other bands */
  142. JOIN permissions AS p
  143. ON p.item_type = 'Band'
  144. AND p.item_id = other_event_band.band_id
  145. AND p.user_id = #{user_id}
  146. AND p.status IN ('owned', 'accepted')
  147. SQL
  148. end
  149. end
  150. end

app/services/device_access_service.rb

100.0% lines covered

100.0% branches covered

11 relevant lines. 11 lines covered and 0 lines missed.
2 total branches, 2 branches covered and 0 branches missed.
    
  1. 8 class DeviceAccessService
  2. 8 def initialize(linked_device)
  3. 13 @linked_device = linked_device
  4. end
  5. 8 def accessible_events
  6. 13 if @linked_device.has_specific_access?
  7. then: 5 # Get events directly linked to device
  8. 5 event_ids = @linked_device.linked_device_linkables
  9. .where(linkable_type: "Event")
  10. .pluck(:linkable_id)
  11. # Get events through linked bands
  12. 5 band_ids = @linked_device.linked_device_linkables
  13. .where(linkable_type: "Band")
  14. .pluck(:linkable_id)
  15. 5 event_ids_from_bands = EventBand.where(band_id: band_ids).pluck(:event_id)
  16. # Combine all accessible event IDs
  17. 5 all_event_ids = (event_ids + event_ids_from_bands).uniq
  18. 5 Event.where(id: all_event_ids)
  19. else
  20. else: 8 # Full access - get all events user can see
  21. 8 Event.permitted_for(@linked_device.user_id)
  22. end
  23. end
  24. end

app/services/ical_generator_service.rb

100.0% lines covered

100.0% branches covered

46 relevant lines. 46 lines covered and 0 lines missed.
6 total branches, 6 branches covered and 0 branches missed.
    
  1. 8 class IcalGeneratorService
  2. 8 def initialize(events:, device:)
  3. 12 @events = events
  4. 12 @device = device
  5. end
  6. 8 def generate
  7. 11 calendar = Icalendar::Calendar.new
  8. # Set calendar metadata
  9. 11 calendar.x_wr_calname = calendar_name
  10. 11 calendar.prodid = "-//LibreGig//Calendar//EN"
  11. 11 calendar.version = "2.0"
  12. 11 calendar.calscale = "GREGORIAN"
  13. # Add timezone component
  14. 11 add_timezone(calendar)
  15. # Add events
  16. 28 @events.each { |event| add_event(calendar, event) }
  17. 11 calendar
  18. end
  19. 8 private
  20. 8 def calendar_name
  21. 11 "LibreGig Calendar - #{@device.name}"
  22. end
  23. 8 def add_timezone(calendar)
  24. # Use Rails configured timezone
  25. 11 tz = Time.zone.tzinfo
  26. 11 timezone = tz.identifier
  27. 11 calendar.timezone do |t|
  28. 11 t.tzid = timezone
  29. # Add standard time
  30. 11 t.standard do |s|
  31. # Get the current offset in the format "+HH:MM" or "-HH:MM"
  32. 11 offset = Time.zone.utc_offset
  33. 11 hours = offset.abs / 3600
  34. 11 minutes = (offset.abs % 3600) / 60
  35. 11 then: 10 else: 1 formatted_offset = "%s%02d%02d" % [(offset >= 0) ? "+" : "-", hours, minutes]
  36. 11 s.tzoffsetfrom = formatted_offset
  37. 11 s.tzoffsetto = formatted_offset
  38. 11 s.dtstart = "19700101T000000"
  39. 11 s.tzname = tz.current_period.abbreviation
  40. end
  41. end
  42. end
  43. 8 def add_event(calendar, event)
  44. 17 calendar.event do |cal_event|
  45. 17 cal_event.uid = "event-#{event.id}@libregig.com"
  46. 17 cal_event.summary = event.name
  47. 17 cal_event.description = event_description(event)
  48. 17 then: 13 else: 4 if event.start_date
  49. 13 cal_event.dtstart = Icalendar::Values::DateTime.new(event.start_date)
  50. 13 cal_event.dtend = Icalendar::Values::DateTime.new(event.end_date || event.start_date)
  51. end
  52. 17 cal_event.dtstamp = Icalendar::Values::DateTime.new(event.updated_at)
  53. 17 cal_event.status = "CONFIRMED"
  54. end
  55. end
  56. 8 def event_description(event)
  57. 17 description = event.description.to_s
  58. 17 then: 2 else: 15 if event.bands.any?
  59. 2 band_names = event.bands.map(&:name).join(", ")
  60. 2 description += "\n\nBands: #{band_names}"
  61. end
  62. 17 description
  63. end
  64. end