practical 0.1.0 → 3.0.0.pre.alpha1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -4
  3. data/app/components/practical/views/flash_messages_component.rb +0 -1
  4. data/app/components/practical/views/form/fallback_errors_section_component.rb +5 -3
  5. data/app/components/practical/views/form/option_label_component.rb +0 -1
  6. data/app/components/practical/views/navigation/breadcrumb_item_component.rb +0 -1
  7. data/app/components/practical/views/navigation/breadcrumbs_component.rb +2 -1
  8. data/app/{controllers/concerns/practical/auth/passkeys → concerns/practical/auth/passkeys/controllers}/emergency_registrations.rb +2 -2
  9. data/app/{controllers/concerns/practical/auth/passkeys → concerns/practical/auth/passkeys/controllers}/web_authn_debug_context.rb +1 -1
  10. data/app/concerns/practical/memberships/controllers/membership_invitations/register_with_passkey.rb +92 -0
  11. data/app/lib/practical/forms/datatables/base.rb +80 -0
  12. data/app/lib/practical/loaders/base.rb +44 -0
  13. data/app/lib/practical/relation_builders/base.rb +35 -0
  14. data/app/lib/practical/test/shared/attachment/models/attachment/base.rb +123 -0
  15. data/app/lib/practical/test/shared/attachment/models/attachment/for_organization.rb +39 -0
  16. data/app/lib/practical/test/shared/attachment/models/organization/has_attachments.rb +12 -0
  17. data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/base.rb +9 -6
  18. data/app/lib/practical/test/shared/auth/passkeys/controllers/emergency_registration/cross_pollination.rb +49 -0
  19. data/app/lib/practical/test/shared/auth/passkeys/controllers/passkey_management/base.rb +508 -0
  20. data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/base.rb +27 -9
  21. data/app/lib/practical/test/shared/auth/passkeys/controllers/reauthentication/cross_pollination.rb +19 -0
  22. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_destroy.rb +26 -8
  23. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/self_signup.rb +3 -2
  24. data/app/lib/practical/test/shared/auth/passkeys/controllers/registrations/update.rb +55 -19
  25. data/app/lib/practical/test/shared/auth/passkeys/controllers/sessions/cross_pollination.rb +29 -0
  26. data/app/lib/practical/test/shared/auth/passkeys/forms/emergency_registration.rb +0 -1
  27. data/app/lib/practical/test/shared/auth/passkeys/models/{passkey.rb → passkey/base.rb} +1 -1
  28. data/app/lib/practical/test/shared/auth/passkeys/models/passkey/emergency_registration.rb +23 -0
  29. data/app/lib/practical/test/shared/auth/passkeys/models/{resource_with_passkeys.rb → resource_with_passkeys/base.rb} +1 -1
  30. data/app/lib/practical/test/shared/auth/passkeys/models/resource_with_passkeys/emergency_registration.rb +41 -0
  31. data/app/lib/practical/test/shared/memberships/controllers/membership_invitations/base.rb +165 -0
  32. data/app/lib/practical/test/shared/memberships/controllers/membership_invitations/register_with_passkey.rb +417 -0
  33. data/app/lib/practical/test/shared/memberships/controllers/organization/membership.rb +400 -0
  34. data/app/lib/practical/test/shared/memberships/controllers/organization/membership_invitation.rb +148 -0
  35. data/app/lib/practical/test/shared/memberships/controllers/user/membership.rb +119 -0
  36. data/app/lib/practical/test/shared/memberships/controllers/user/membership_invitation.rb +57 -0
  37. data/app/lib/practical/test/shared/memberships/forms/create_new_user_with_membership_invitation.rb +197 -0
  38. data/app/lib/practical/test/shared/memberships/forms/organization/membership.rb +162 -0
  39. data/app/lib/practical/test/shared/memberships/forms/organization/new_membership_invitation.rb +195 -0
  40. data/app/lib/practical/test/shared/memberships/forms/user/membership.rb +87 -0
  41. data/app/lib/practical/test/shared/memberships/models/membership/base.rb +45 -0
  42. data/app/lib/practical/test/shared/memberships/models/membership_invitation/base.rb +85 -0
  43. data/app/lib/practical/test/shared/memberships/models/membership_invitation/sending.rb +76 -0
  44. data/app/lib/practical/test/shared/memberships/models/membership_invitation/use_for_and_notify.rb +55 -0
  45. data/app/lib/practical/test/shared/memberships/models/organization/base.rb +25 -0
  46. data/app/lib/practical/test/shared/memberships/models/user/base.rb +23 -0
  47. data/app/lib/practical/test/shared/memberships/policies/organization/base_resource.rb +29 -0
  48. data/app/lib/practical/test/shared/memberships/policies/organization/membership.rb +103 -0
  49. data/app/lib/practical/test/shared/memberships/policies/organization/membership_invitation.rb +94 -0
  50. data/app/lib/practical/test/shared/memberships/policies/organization/resource/inherits.rb +10 -0
  51. data/app/lib/practical/test/shared/memberships/policies/organization.rb +70 -0
  52. data/app/lib/practical/test/shared/memberships/policies/user/membership.rb +78 -0
  53. data/app/lib/practical/test/shared/memberships/policies/user/membership_invitation.rb +31 -0
  54. data/app/lib/practical/test/shared/models/normalized_email.rb +0 -1
  55. data/app/lib/practical/test/shared/policies/user/base.rb +14 -0
  56. data/app/lib/practical/views/error_handling.rb +2 -0
  57. data/app/lib/practical/views/error_response.rb +27 -0
  58. data/app/lib/practical/views/form_builders/base.rb +5 -4
  59. data/app/lib/practical/views/form_builders/collection_option.rb +5 -0
  60. data/app/lib/practical/views/icon_set.rb +12 -6
  61. data/config/locales/auth.en.yml +18 -0
  62. data/config/locales/memberships.en.yml +129 -0
  63. data/db/seeds/memberships/default.rb +68 -0
  64. data/db/seeds/moderators/default.rb +36 -0
  65. data/db/seeds/setup.rb +16 -0
  66. data/db/seeds/test/cases/membership_invitations.rb +31 -0
  67. data/db/seeds/users/default.rb +17 -15
  68. data/lib/generators/practical/test/shared_test/shared_test_generator.rb +2 -0
  69. data/lib/practical/framework/engine.rb +8 -0
  70. data/lib/practical/helpers/honeybadger_helper.rb +11 -0
  71. data/lib/practical/helpers/selector_helper.rb +8 -0
  72. data/lib/practical/version.rb +1 -1
  73. data/lib/practical/views/element_helper.rb +2 -0
  74. data/lib/practical/views/theme_helper.rb +13 -0
  75. data/lib/practical.rb +4 -1
  76. data/lib/tasks/practical/utility.rake +20 -0
  77. metadata +54 -11
  78. data/lib/tasks/practical/framework_tasks.rake +0 -6
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Policies::User::Membership
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ def assert_can_manage(membership:, user:)
8
+ assert_equal true, policy_for(membership: membership, user: user).apply(:manage?)
9
+ end
10
+
11
+ def assert_cannot_manage(membership:, user:)
12
+ assert_equal false, policy_for(membership: membership, user: user).apply(:manage?)
13
+ end
14
+
15
+ def assert_can_leave(membership:, user:)
16
+ assert_equal true, policy_for(membership: membership, user: user).apply(:leave?)
17
+ end
18
+
19
+ def assert_cannot_leave(membership:, user:)
20
+ assert_equal false, policy_for(membership: membership, user: user).apply(:leave?)
21
+ end
22
+
23
+ test "manage?: only true for the user's memberships" do
24
+ staff_membership = users.works_at_org_1_and_2.memberships.first
25
+
26
+ assert_can_manage(membership: staff_membership, user: users.works_at_org_1_and_2)
27
+ assert_cannot_manage(membership: staff_membership, user: users.organization_1_owner)
28
+ assert_cannot_manage(membership: staff_membership, user: users.organization_1_manager)
29
+ assert_cannot_manage(membership: staff_membership, user: users.organization_2_owner)
30
+ assert_cannot_manage(membership: staff_membership, user: users.organization_1_staff)
31
+ assert_cannot_manage(membership: staff_membership, user: users.retired_staff)
32
+ assert_cannot_manage(membership: staff_membership, user: users.archived_organization_1_manager)
33
+
34
+ organization_manager_membership = users.organization_1_manager.memberships.first
35
+
36
+ assert_can_manage(membership: organization_manager_membership, user: users.organization_1_manager)
37
+ assert_cannot_manage(membership: organization_manager_membership, user: users.organization_1_owner)
38
+ assert_cannot_manage(membership: organization_manager_membership, user: users.organization_2_owner)
39
+ assert_cannot_manage(membership: organization_manager_membership, user: users.works_at_org_1_and_2)
40
+ assert_cannot_manage(membership: organization_manager_membership, user: users.retired_staff)
41
+ assert_cannot_manage(membership: organization_manager_membership, user: users.archived_organization_1_manager)
42
+ end
43
+
44
+ test "leave?: only true if the membership_type is not organization_manager && there are multiple organization_managers for the organization" do
45
+ staff_membership = users.works_at_org_1_and_2.memberships.first
46
+
47
+ assert_can_leave(membership: staff_membership, user: users.works_at_org_1_and_2)
48
+ assert_cannot_leave(membership: staff_membership, user: users.organization_1_owner)
49
+ assert_cannot_leave(membership: staff_membership, user: users.organization_1_manager)
50
+ assert_cannot_leave(membership: staff_membership, user: users.organization_2_owner)
51
+ assert_cannot_leave(membership: staff_membership, user: users.organization_1_staff)
52
+ assert_cannot_leave(membership: staff_membership, user: users.retired_staff)
53
+ assert_cannot_leave(membership: staff_membership, user: users.archived_organization_1_manager)
54
+
55
+ organization_manager_membership = users.organization_1_manager.memberships.first
56
+
57
+ assert_can_leave(membership: organization_manager_membership, user: users.organization_1_manager)
58
+ assert_cannot_leave(membership: organization_manager_membership, user: users.organization_1_owner)
59
+ assert_cannot_leave(membership: organization_manager_membership, user: users.organization_2_owner)
60
+ assert_cannot_leave(membership: organization_manager_membership, user: users.works_at_org_1_and_2)
61
+ assert_cannot_leave(membership: organization_manager_membership, user: users.retired_staff)
62
+ assert_cannot_leave(membership: organization_manager_membership, user: users.archived_organization_1_manager)
63
+
64
+
65
+ only_manager_membership = users.organization_2_owner.memberships.first
66
+
67
+ assert_cannot_leave(membership: only_manager_membership, user: users.organization_1_manager)
68
+ assert_cannot_leave(membership: only_manager_membership, user: users.organization_1_owner)
69
+ assert_cannot_leave(membership: only_manager_membership, user: users.organization_2_owner)
70
+ assert_cannot_leave(membership: only_manager_membership, user: users.works_at_org_1_and_2)
71
+ assert_cannot_leave(membership: only_manager_membership, user: users.retired_staff)
72
+ assert_cannot_leave(membership: only_manager_membership, user: users.archived_organization_1_manager)
73
+
74
+ organization_1_staff_membership = users.organization_1_staff.memberships.first
75
+ assert_can_leave(membership: organization_1_staff_membership, user: users.organization_1_staff)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Memberships::Policies::User::MembershipInvitation
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ def assert_can_manage(invitation:, user:)
8
+ assert_equal true, policy_for(invitation: invitation, user: user).apply(:manage?)
9
+ end
10
+
11
+ def assert_cannot_manage(invitation:, user:)
12
+ assert_equal false, policy_for(invitation: invitation, user: user).apply(:manage?)
13
+ end
14
+
15
+ test "failing manage?: only true for the user's unused invitations" do
16
+ seed "cases/membership_invitations"
17
+ assert_can_manage(invitation: membership_invitations.one_invite_only, user: users.one_invite_only)
18
+ assert_cannot_manage(invitation: membership_invitations.one_invite_only, user: users.invite_organization_1_manager)
19
+ end
20
+
21
+ test "manage?: only true for the user's unused invitations" do
22
+ user = users.invited_user_1
23
+ membership_invitation = find_membership_invitation!(email: user.email)
24
+ assert_can_manage(invitation: membership_invitation, user: user)
25
+
26
+ user = users.invited_user_2
27
+ membership_invitation = find_membership_invitation!(email: user.email)
28
+ assert_cannot_manage(invitation: membership_invitation, user: user)
29
+ end
30
+ end
31
+ end
@@ -14,7 +14,6 @@ module Practical::Test::Shared::Models::NormalizedEmail
14
14
  assert_equal instance, model_class.find_by(email: original_email.upcase)
15
15
  assert_equal normalized_email, model_class.find_by(email: original_email.upcase).email
16
16
 
17
-
18
17
  instance.update!(email: "\n\n\t#{original_email.upcase}\n\n\t")
19
18
  assert_equal instance, model_class.find_by(email: "\n\n\t#{original_email.upcase}\n\n\t")
20
19
  assert_equal normalized_email, model_class.find_by(email: "\n\n\t#{original_email.upcase}\n\n\t").email
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Test::Shared::Policies::User::Base
4
+ extend ActiveSupport::Concern
5
+ included do
6
+ test "manage?: only true for the user" do
7
+ user = users.user_1
8
+ other_user = users.user_2
9
+
10
+ assert_equal true, policy_class.new(user, user: user).apply(:manage?)
11
+ assert_equal false, policy_class.new(user, user: other_user).apply(:manage?)
12
+ end
13
+ end
14
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Practical::Views::ErrorHandling
2
4
  def self.build_error_json(model:, helpers:)
3
5
  return model.errors.map do |error|
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Views::ErrorResponse
4
+ extend ActiveSupport::Concern
5
+
6
+ def render_json_error(format:, model:)
7
+ format.json do
8
+ errors = Practical::Views::ErrorHandling.build_error_json(model: model, helpers: helpers)
9
+ yield(errors) if block_given?
10
+ render json: errors, status: :bad_request
11
+ end
12
+ end
13
+
14
+ def render_html_error(action:, format:)
15
+ format.html do
16
+ yield if block_given?
17
+ render action, status: :bad_request
18
+ end
19
+ end
20
+
21
+ def default_respond_to_model_validation_error(html_action:, model:)
22
+ respond_to do |format|
23
+ render_json_error(format: format, model: model)
24
+ render_html_error(action: html_action, format: format)
25
+ end
26
+ end
27
+ end
@@ -94,10 +94,10 @@ class Practical::Views::FormBuilders::Base < ActionView::Helpers::FormBuilder
94
94
 
95
95
  def radio_collection(field_method:, options:, collection_check_boxes_options: {})
96
96
  collection_radio_buttons(field_method,
97
- options,
98
- :value,
99
- :title,
100
- collection_check_boxes_options
97
+ options,
98
+ :value,
99
+ :title,
100
+ collection_check_boxes_options
101
101
  ) do |collection_builder|
102
102
  collection_builder.label(class: "wa-flank") do
103
103
  template.safe_join([
@@ -136,6 +136,7 @@ class Practical::Views::FormBuilders::Base < ActionView::Helpers::FormBuilder
136
136
  def fallback_error_section(blurb_key: :"practical_framework.forms.generic_error_blurb", id:, options: {})
137
137
  template.render Practical::Views::Form::FallbackErrorsSectionComponent.new(
138
138
  f: self,
139
+ id: id,
139
140
  blurb: template.translate(blurb_key, raise: true),
140
141
  options: options
141
142
  )
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Views::FormBuilders
4
+ CollectionOption = Struct.new(:value, :title, :description, :icon, keyword_init: true)
5
+ end
@@ -129,9 +129,12 @@ class Practical::Views::IconSet
129
129
  SpritesheetIconDefinition.new(method_name: :add_note_icon, icon_name: :"solid-note-circle-plus", library: :kit),
130
130
  SpritesheetIconDefinition.new(method_name: :edit_note_icon, icon_name: :"solid-note-pen", library: :kit),
131
131
  SpritesheetIconDefinition.new(method_name: :delete_note_icon, icon_name: :"solid-note-slash", library: :kit),
132
- SpritesheetIconDefinition.new(method_name: :archive_membership_icon, icon_name: :"solid-id-badge-slash", library: :kit),
133
- SpritesheetIconDefinition.new(method_name: :accepted_membership_invitation_icon, icon_name: :"solid-warehouse-circle-check", library: :kit),
134
- SpritesheetIconDefinition.new(method_name: :destroy_membership_invitation_icon, icon_name: :"solid-warehouse-slash", library: :kit),
132
+ SpritesheetIconDefinition.new(method_name: :archive_membership_icon, icon_name: :"solid-id-badge-slash",
133
+ library: :kit),
134
+ SpritesheetIconDefinition.new(method_name: :accepted_membership_invitation_icon,
135
+ icon_name: :"solid-warehouse-circle-check", library: :kit),
136
+ SpritesheetIconDefinition.new(method_name: :destroy_membership_invitation_icon,
137
+ icon_name: :"solid-warehouse-slash", library: :kit),
135
138
  SpritesheetIconDefinition.new(method_name: :delete_note_icon, icon_name: :"solid-note-slash", library: :kit),
136
139
 
137
140
  ])
@@ -143,14 +146,17 @@ class Practical::Views::IconSet
143
146
  end
144
147
 
145
148
  def self.emergency_passkey_registration_icon
146
- duotone_icon(name: :"light-emergency-on", options: {style: "--secondary-color: var(--wa-color-danger-fill-loud); --secondary-opacity: 0.7;"})
149
+ duotone_icon(name: :"light-emergency-on",
150
+ options: {style: "--secondary-color: var(--wa-color-danger-fill-loud); --secondary-opacity: 0.7;"})
147
151
  end
148
152
 
149
153
  def self.signin_icon
150
- duotone_icon(name: :"person-to-portal", options: {style: "--secondary-color: var(--wa-color-brand-fill-normal); --secondary-opacity: 0.9;"})
154
+ duotone_icon(name: :"person-to-portal",
155
+ options: {style: "--secondary-color: var(--wa-color-brand-fill-normal); --secondary-opacity: 0.9;"})
151
156
  end
152
157
 
153
158
  def self.signout_icon
154
- duotone_icon(name: :"person-from-portal", options: {style: "--secondary-color: var(--wa-color-brand-fill-normal); --secondary-opacity: 0.9;"})
159
+ duotone_icon(name: :"person-from-portal",
160
+ options: {style: "--secondary-color: var(--wa-color-brand-fill-normal); --secondary-opacity: 0.9;"})
155
161
  end
156
162
  end
@@ -31,6 +31,24 @@ en:
31
31
  title: Create an account
32
32
  submit_button_title: "Sign up"
33
33
 
34
+ destroy:
35
+ form:
36
+ title: "Delete my account"
37
+ submit_button_title: "Verify & destroy my account"
38
+ contact_support_message:
39
+ html: "<p>Want to delete your account? Please contact customer support</p>"
40
+
41
+ passkeys:
42
+ section_title: "Passkeys"
43
+ current_passkeys_title: "My passkeys"
44
+ create:
45
+ form:
46
+ title: "Add passkey"
47
+ submit_button_title: "Add passkey"
48
+ destroy:
49
+ form:
50
+ submit_button_title: "Remove"
51
+
34
52
  app_navigation:
35
53
  user_settings:
36
54
  home_title: "Home"
@@ -0,0 +1,129 @@
1
+ en:
2
+ app_navigation:
3
+ user_settings:
4
+ organizations_title: "Change Organization"
5
+ memberships_title: "Memberships"
6
+ organization_settings:
7
+ memberships_title: "Memberships"
8
+ organizations:
9
+ title: "Available Organizations"
10
+
11
+ organization_memberships:
12
+ title: "Organization Membership"
13
+ invitation_form:
14
+ title: "Invite someone to your organization"
15
+ submit_button_title: "Send invitation"
16
+ active:
17
+ title: "Active members"
18
+ pending_reacceptance:
19
+ title: "Waiting for response"
20
+ pending_invitations:
21
+ title: "Sent invitations"
22
+ archived:
23
+ title: "Archived members"
24
+
25
+ edit:
26
+ page_title: "Membership for %{name}"
27
+ link_title: "Edit"
28
+ form:
29
+ submit_button_title: "Update membership"
30
+
31
+ archive:
32
+ button_title: "Archive"
33
+
34
+ activate:
35
+ button_title: "Reactivate"
36
+
37
+ invitation:
38
+ resend:
39
+ button_title: "Resend"
40
+ revoke:
41
+ button_title: "Revoke"
42
+
43
+ invitation_sent_message: "Invitation sent to ‟%{email}”!"
44
+ invitation_revoked_message: "Invitation to ‟%{email}” has been revoked"
45
+ cannot_be_resent_message: "We were not able to resend this invitation; please contact support."
46
+ already_member_message: "Good news, everyone! They’re already a member!"
47
+ awaiting_reacceptance_message: "We’ve sent them an invitation to rejoin, please ask them to check their ‟Memberships” tab."
48
+ updated_message: "Updated membership!"
49
+
50
+ user_memberships:
51
+ title: "My Memberships"
52
+ active:
53
+ title: "Active memberships"
54
+ pending_reacceptance:
55
+ title: "Memberships awaiting my response"
56
+ pending_invitations:
57
+ title: "Membership invitations"
58
+ archived:
59
+ title: "Archived memberships"
60
+
61
+ archive:
62
+ button_title: "Leave"
63
+
64
+ invitation:
65
+ accept:
66
+ button_title: "Accept"
67
+ hide:
68
+ button_title: "Hide"
69
+ updated_message: "Updated membership!"
70
+ archived_message: "This membership has been archived."
71
+
72
+ invitation_hidden_message: "Hidden the invitation for %{organization_name}."
73
+
74
+ membership_invitations:
75
+ accept_form:
76
+ title: "Accept this invitation?"
77
+ description:
78
+ html: "<p>You have been invited to join a new organization.</p>"
79
+ details:
80
+ caption:
81
+ html: "Invitation details"
82
+ accept_as_current_user:
83
+ title: "Accept this invitation as %{name}"
84
+ description:
85
+ html: "<p>You’ll share the following information with %{organization_name}:</p>"
86
+ accept_button_title: "Accept invitation"
87
+ sign_out_button_title: "Actually, sign out"
88
+ new_user_from_invitation_form:
89
+ signin_button_title: "Wait! I have an account; let me sign in."
90
+ submit_button_title: "Sign up & Accept invitation"
91
+ accepted_message: "You’re now a member of %{organization_name}"
92
+ registered_message: "Your account has been created; you can login now!"
93
+
94
+ errors:
95
+ attributes:
96
+ sender:
97
+ cannot_manage_organization: "We were not able to send this; please contact support."
98
+ membership_type:
99
+ blank: "Please select a role."
100
+ resource:
101
+ cannot_access_organization: "We were not able to save this; please contact support."
102
+ activemodel:
103
+ errors:
104
+ models:
105
+ organization/new_membership_invitation_form:
106
+ attributes:
107
+ base:
108
+ cannot_be_resent: "We were not able to resend this invitation; please contact support."
109
+
110
+ organization/membership_form:
111
+ attributes:
112
+ state:
113
+ cannot_be_archived: "This member cannot be archived."
114
+ membership_type:
115
+ cannot_be_downgraded: "There must be at least one organization manager."
116
+
117
+ user/membership_form:
118
+ attributes:
119
+ state:
120
+ cannot_be_archived: "This membership cannot be archived."
121
+ cannot_be_changed: "This membership cannot be changed."
122
+ inclusion: "This membership can only be archived."
123
+ activerecord:
124
+ errors:
125
+ models:
126
+ membership:
127
+ attributes:
128
+ user:
129
+ taken: "This user is already part of this organization."
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ organization_1 = organizations.create(:organization_1,
4
+ name: Faker::Company.name
5
+ )
6
+
7
+ organization_2 = organizations.create(:organization_2,
8
+ name: Faker::Company.name
9
+ )
10
+
11
+ organization_3 = organizations.create(:organization_3,
12
+ name: Faker::Company.name
13
+ )
14
+
15
+ organization_1_owner = users.create_labeled(:organization_1_owner)
16
+ organization_2_owner = users.create_labeled(:organization_2_owner)
17
+ organization_3_owner = users.create_labeled(:organization_3_owner)
18
+
19
+ organization_1.memberships.create!(user: organization_1_owner, state: :active, membership_type: :organization_manager)
20
+ organization_2.memberships.create!(user: organization_2_owner, state: :active, membership_type: :organization_manager)
21
+ organization_3.memberships.create!(user: organization_3_owner, state: :active, membership_type: :organization_manager)
22
+
23
+ organization_1_manager = users.create_labeled(:organization_1_manager)
24
+
25
+ organization_1.memberships.create!(user: organization_1_manager,
26
+ state: :active,
27
+ membership_type: :organization_manager
28
+ )
29
+
30
+ works_at_org_1_and_2 = users.create_labeled(:works_at_org_1_and_2)
31
+
32
+ organization_1.memberships.create!(user: works_at_org_1_and_2, state: :active, membership_type: :staff)
33
+ organization_2.memberships.create!(user: works_at_org_1_and_2, state: :active, membership_type: :staff)
34
+
35
+ organization_1_staff = users.create_labeled(:organization_1_staff)
36
+ organization_2.memberships.create!(user: organization_1_staff, state: :pending_reacceptance, membership_type: :staff)
37
+
38
+ retired_staff = users.create_labeled(:retired_staff)
39
+ organization_1.memberships.create!(user: retired_staff, state: :archived_by_organization, membership_type: :staff)
40
+
41
+ archived_organization_1_manager = users.create_labeled(:archived_organization_1_manager)
42
+ organization_1.memberships.create!(user: archived_organization_1_manager,
43
+ state: :archived_by_organization,
44
+ membership_type: :organization_manager
45
+ )
46
+
47
+ invited_user_1 = users.create_labeled(:invited_user_1)
48
+ invited_user_2 = users.create_labeled(:invited_user_2)
49
+
50
+ organization_3.membership_invitations.create!(
51
+ email: Faker::Internet.email,
52
+ sender: organization_3_owner,
53
+ membership_type: :staff
54
+ )
55
+
56
+ organization_3.membership_invitations.create!(
57
+ email: invited_user_1.email,
58
+ sender: organization_3_owner,
59
+ membership_type: :staff
60
+ )
61
+
62
+ organization_3.membership_invitations.create!(
63
+ user: invited_user_2,
64
+ email: invited_user_2.email,
65
+ sender: organization_3_owner,
66
+ membership: organization_3.memberships.create!(user: invited_user_2, state: :active, membership_type: :staff),
67
+ membership_type: :staff
68
+ )
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ moderator_1 = moderators.create_labeled(:moderator_1)
4
+
5
+ moderator_passkeys.create(:moderator_1_passkey,
6
+ unique_by: :moderator,
7
+ moderator: moderator_1,
8
+ label: "Passkey 1",
9
+ external_id: SecureRandom.hex,
10
+ public_key: SecureRandom.hex
11
+ )
12
+
13
+ moderator_1.emergency_passkey_registrations.create!
14
+
15
+ moderator_2 = moderators.create_labeled(:moderator_2)
16
+
17
+ moderator_passkeys.create(:moderator_2_passkey,
18
+ unique_by: :moderator,
19
+ moderator: moderator_2,
20
+ label: "Passkey 2",
21
+ external_id: SecureRandom.hex,
22
+ public_key: SecureRandom.hex
23
+ )
24
+
25
+ moderator_passkeys.create(:moderator_1_passkey_2,
26
+ unique_by: :moderator,
27
+ moderator: moderator_1,
28
+ label: "Passkey 3",
29
+ external_id: SecureRandom.hex,
30
+ public_key: SecureRandom.hex
31
+ )
32
+
33
+ moderator_1.emergency_passkey_registrations.create!(
34
+ used_at: 1.week.ago,
35
+ passkey: moderator_passkeys.moderator_1_passkey_2
36
+ )
data/db/seeds/setup.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  users.defaults(name: -> { Faker::Name.name },
2
4
  email: -> { Faker::Internet.email },
3
5
  webauthn_id: -> { SecureRandom.uuid }
@@ -10,4 +12,18 @@ end
10
12
 
11
13
  def users.labeled_email(label)
12
14
  "#{label}@example.com"
15
+ end
16
+
17
+
18
+ moderators.defaults(email: -> { Faker::Internet.email },
19
+ webauthn_id: -> { SecureRandom.uuid }
20
+ )
21
+
22
+
23
+ def moderators.create_labeled(label, email: labeled_email(label), **)
24
+ create(label, unique_by: :email, email: email, **)
25
+ end
26
+
27
+ def moderators.labeled_email(label)
28
+ "#{label}@example.com"
13
29
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ users.create_labeled(:one_invite_only) => one_invite_only_user
4
+ users.create_labeled(:only_used_invites)
5
+ users.create_labeled(:only_hidden_invites)
6
+ users.create_labeled(:multiple_open_invites)
7
+ users.create_labeled(:one_invite_of_each_type)
8
+
9
+ def create_dummy_organization(label:)
10
+ organizations.create(label, name: Faker::Company.name).tap do |organization|
11
+ organization.memberships.organization_manager.active.create!(user: users.create(:"#{label}_manager"))
12
+ end
13
+ end
14
+
15
+ create_dummy_organization(label: :invite_organization_1)
16
+ create_dummy_organization(label: :invite_organization_2) => invite_organization_2
17
+ create_dummy_organization(label: :invite_organization_3)
18
+
19
+ def membership_invitations.create_labeled(label, email:, organization:, sender:)
20
+ create(label, email: email,
21
+ membership_type: :staff,
22
+ organization: organization,
23
+ sender: sender
24
+ )
25
+ end
26
+
27
+
28
+ membership_invitations.create_labeled(:one_invite_only, email: one_invite_only_user.email,
29
+ organization: invite_organization_2,
30
+ sender: invite_organization_2.users.first
31
+ )
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  user_1 = users.create_labeled(:user_1)
2
4
 
3
5
  passkeys.create(:user_1_passkey,
4
- unique_by: :user,
5
- user: user_1,
6
- label: "Passkey 1",
7
- external_id: SecureRandom.hex,
8
- public_key: SecureRandom.hex
6
+ unique_by: :user,
7
+ user: user_1,
8
+ label: "Passkey 1",
9
+ external_id: SecureRandom.hex,
10
+ public_key: SecureRandom.hex
9
11
  )
10
12
 
11
13
  user_1.emergency_passkey_registrations.create!
@@ -13,19 +15,19 @@ user_1.emergency_passkey_registrations.create!
13
15
  user_2 = users.create_labeled(:user_2)
14
16
 
15
17
  passkeys.create(:user_2_passkey,
16
- unique_by: :user,
17
- user: user_2,
18
- label: "Passkey 2",
19
- external_id: SecureRandom.hex,
20
- public_key: SecureRandom.hex
18
+ unique_by: :user,
19
+ user: user_2,
20
+ label: "Passkey 2",
21
+ external_id: SecureRandom.hex,
22
+ public_key: SecureRandom.hex
21
23
  )
22
24
 
23
25
  passkeys.create(:user_1_passkey_2,
24
- unique_by: :user,
25
- user: user_1,
26
- label: "Passkey 3",
27
- external_id: SecureRandom.hex,
28
- public_key: SecureRandom.hex
26
+ unique_by: :user,
27
+ user: user_1,
28
+ label: "Passkey 3",
29
+ external_id: SecureRandom.hex,
30
+ public_key: SecureRandom.hex
29
31
  )
30
32
 
31
33
  user_1.emergency_passkey_registrations.create!(
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Practical::Test::SharedTestGenerator < Rails::Generators::NamedBase
2
4
  source_root File.expand_path("templates", __dir__)
3
5
 
@@ -3,6 +3,11 @@
3
3
  module Practical
4
4
  module Framework
5
5
  class Engine < ::Rails::Engine
6
+ rake_tasks do
7
+ load 'tasks/practical/coverage.rake'
8
+ load 'tasks/practical/utility.rake'
9
+ end
10
+
6
11
  config.app_generators do |g|
7
12
  g.apply_rubocop_autocorrect_after_generate!
8
13
  g.test_framework :test_unit, fixture: false, fixture_replacement: :oaken
@@ -24,10 +29,13 @@ module Practical
24
29
  initializer 'practical-framework.view_helpers' do
25
30
  ActiveSupport.on_load(:action_view) do
26
31
  include Practical::Framework::Engine::PracticalFrameworkFormBuilderActiveSupportExtension
32
+ include Practical::Views::ThemeHelper
27
33
  include Practical::Helpers::FormWithHelper
28
34
  include Practical::Helpers::IconHelper
29
35
  include Practical::Helpers::TextHelper
30
36
  include Practical::Helpers::TranslationHelper
37
+ include Practical::Helpers::SelectorHelper
38
+ include Practical::Helpers::HoneybadgerHelpers
31
39
  end
32
40
  end
33
41
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Helpers::HoneybadgerHelpers
4
+ def honeybadger_js_configuration
5
+ {
6
+ "apiKey" => AppSettings.honeybadger_js_api_key,
7
+ "environment" => AppSettings.app_env,
8
+ "revision" => AppSettings.app_revision
9
+ }
10
+ end
11
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Practical::Helpers::SelectorHelper
4
+ def id_selector(text)
5
+ text = text.to_param if text.is_a?(GlobalID)
6
+ "##{text}"
7
+ end
8
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Practical
4
- VERSION = "0.1.0"
4
+ VERSION = "3.0.0-alpha1"
5
5
  end