bullet_train 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +8 -0
- data/app/assets/config/bullet_train_manifest.js +0 -0
- data/app/controllers/account/invitations_controller.rb +146 -0
- data/app/controllers/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments_controller.rb +83 -0
- data/app/controllers/account/memberships_controller.rb +132 -0
- data/app/controllers/account/onboarding/user_details_controller.rb +63 -0
- data/app/controllers/account/onboarding/user_email_controller.rb +65 -0
- data/app/controllers/account/teams_controller.rb +122 -0
- data/app/controllers/account/users_controller.rb +59 -0
- data/app/helpers/account/invitations_helper.rb +2 -0
- data/app/helpers/account/memberships_helper.rb +9 -0
- data/app/helpers/account/teams_helper.rb +80 -0
- data/app/helpers/account/users_helper.rb +81 -0
- data/app/helpers/invitation_only_helper.rb +10 -0
- data/app/helpers/invitations_helper.rb +17 -0
- data/app/models/invitation.rb +73 -0
- data/app/models/membership.rb +164 -0
- data/app/models/memberships/reassignments/assignment.rb +12 -0
- data/app/models/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignment.rb +38 -0
- data/app/models/memberships/reassignments.rb +5 -0
- data/app/models/team.rb +81 -0
- data/app/models/user.rb +191 -0
- data/app/views/account/invitations/_breadcrumbs.html.erb +9 -0
- data/app/views/account/invitations/_form.html.erb +49 -0
- data/app/views/account/invitations/_invitation.json.jbuilder +7 -0
- data/app/views/account/invitations/index.json.jbuilder +1 -0
- data/app/views/account/invitations/new.html.erb +12 -0
- data/app/views/account/invitations/show.html.erb +30 -0
- data/app/views/account/invitations/show.json.jbuilder +1 -0
- data/app/views/account/memberships/_breadcrumbs.html.erb +8 -0
- data/app/views/account/memberships/_form.html.erb +45 -0
- data/app/views/account/memberships/_index.html.erb +66 -0
- data/app/views/account/memberships/_menu_item.html.erb +8 -0
- data/app/views/account/memberships/_tombstones.html.erb +59 -0
- data/app/views/account/memberships/edit.html.erb +22 -0
- data/app/views/account/memberships/index.html.erb +7 -0
- data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/_breadcrumbs.html.erb +10 -0
- data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/_form.html.erb +24 -0
- data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/_scaffolding_completely_concrete_tangible_things_reassignment.json.jbuilder +8 -0
- data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/index.json.jbuilder +1 -0
- data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/new.html.erb +11 -0
- data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/show.json.jbuilder +1 -0
- data/app/views/account/memberships/show.html.erb +60 -0
- data/app/views/account/onboarding/user_details/edit.html.erb +72 -0
- data/app/views/account/onboarding/user_email/edit.html.erb +33 -0
- data/app/views/account/teams/_breadcrumbs.html.erb +11 -0
- data/app/views/account/teams/_form.html.erb +22 -0
- data/app/views/account/teams/_index.html.erb +33 -0
- data/app/views/account/teams/_menu_item.html.erb +8 -0
- data/app/views/account/teams/_team.json.jbuilder +9 -0
- data/app/views/account/teams/edit.html.erb +12 -0
- data/app/views/account/teams/index.html.erb +6 -0
- data/app/views/account/teams/index.json.jbuilder +1 -0
- data/app/views/account/teams/new.html.erb +88 -0
- data/app/views/account/teams/show.html.erb +25 -0
- data/app/views/account/teams/show.json.jbuilder +1 -0
- data/app/views/account/users/_breadcrumbs.html.erb +4 -0
- data/app/views/account/users/_form.html.erb +39 -0
- data/app/views/account/users/edit.html.erb +50 -0
- data/app/views/account/users/show.html.erb +17 -0
- data/config/locales/en/invitations.en.yml +69 -0
- data/config/locales/en/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments.en.yml +42 -0
- data/config/locales/en/memberships.en.yml +99 -0
- data/config/locales/en/onboarding/user_details.en.yml +11 -0
- data/config/locales/en/onboarding/user_email.en.yml +13 -0
- data/config/locales/en/teams.en.yml +85 -0
- data/config/locales/en/users.en.yml +110 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20161115160419_devise_create_users.rb +41 -0
- data/db/migrate/20161116003852_add_api_key_to_user.rb +6 -0
- data/db/migrate/20161117154605_create_teams.rb +10 -0
- data/db/migrate/20161117154709_create_memberships.rb +11 -0
- data/db/migrate/20161203193930_add_current_team_to_user.rb +5 -0
- data/db/migrate/20161204234150_create_invitations.rb +11 -0
- data/db/migrate/20161205154821_add_team_to_invitation.rb +5 -0
- data/db/migrate/20161205164613_add_admin_to_invitation.rb +5 -0
- data/db/migrate/20170908205756_add_names_to_user.rb +6 -0
- data/db/migrate/20170915215309_add_team_to_thing.rb +5 -0
- data/db/migrate/20171105001408_remove_api_key_from_user.rb +6 -0
- data/db/migrate/20180326124105_add_timezone_to_user.rb +5 -0
- data/db/migrate/20180902142350_create_membership_roles.rb +10 -0
- data/db/migrate/20180902143758_remove_admin_from_membership.rb +5 -0
- data/db/migrate/20180902154611_create_invitation_roles.rb +10 -0
- data/db/migrate/20180902154652_migrate_admin_flag_on_invitations.rb +14 -0
- data/db/migrate/20180902195848_remove_admin_from_invitation.rb +5 -0
- data/db/migrate/20180903101707_add_last_seen_at_to_users.rb +5 -0
- data/db/migrate/20190321203224_add_profile_photo_id_to_user.rb +5 -0
- data/db/migrate/20190519230202_add_ability_cache_to_user.rb +5 -0
- data/db/migrate/20190628194704_add_last_notification_email_sent_at_to_user.rb +5 -0
- data/db/migrate/20200211034208_add_invitation_to_membership.rb +5 -0
- data/db/migrate/20200211044616_drop_invitation_roles_table.rb +10 -0
- data/db/migrate/20200213052748_add_former_user_fields_to_membership.rb +8 -0
- data/db/migrate/20200213235037_add_former_user_to_user.rb +7 -0
- data/db/migrate/20200219013834_add_added_by_to_membership.rb +5 -0
- data/db/migrate/20200219015116_rename_from_user_to_from_membership.rb +5 -0
- data/db/migrate/20200726222314_add_being_destroyed_to_team.rb +5 -0
- data/db/migrate/20200727171308_add_devise_two_factor_to_users.rb +9 -0
- data/db/migrate/20200727175949_add_devise_two_factor_backupable_to_users.rb +5 -0
- data/db/migrate/20210304133200_add_time_zone_to_team.rb +5 -0
- data/db/migrate/20210816072419_add_locale_to_users.rb +5 -0
- data/db/migrate/20210816072508_add_locale_to_teams.rb +5 -0
- data/db/migrate/20211020200855_add_doorkeeper_application_to_memberships.rb +5 -0
- data/db/migrate/20211027002944_add_doorkeeper_application_to_users.rb +5 -0
- data/lib/bullet_train/engine.rb +4 -0
- data/lib/bullet_train/version.rb +3 -0
- data/lib/bullet_train.rb +6 -0
- data/lib/tasks/bullet_train_tasks.rake +4 -0
- metadata +168 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: bcd7fed56a4e0b49d9ca96b95e4a804f6d8d48ef0443f45a28e9a9f28b811549
|
4
|
+
data.tar.gz: 1bbc923e87403a718037831d9a7c6c7beeafb8192169b7897ce56ec0845c27dc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 53052501e61f60b496c5e82cd6f0c0cca4a1310a6093146c07f719556cd2005a260042de0dd05a289e0230e41e83d3b02195430134c1706d91a5c105bde35f09
|
7
|
+
data.tar.gz: 9b34d71773c57d358ca7322ecdaf9948a676a42eca564245dc891f9d4a9bc2f94b731350e59d5d952aeedf7b45521bf2a153159c6ced6ccad8d1c1aff18cb97f
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2022 Andrew Culver
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# BulletTrain
|
2
|
+
Short description and motivation.
|
3
|
+
|
4
|
+
## Usage
|
5
|
+
How to use my plugin.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
Add this line to your application's Gemfile:
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
gem "bullet_train"
|
12
|
+
```
|
13
|
+
|
14
|
+
And then execute:
|
15
|
+
```bash
|
16
|
+
$ bundle
|
17
|
+
```
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
```bash
|
21
|
+
$ gem install bullet_train
|
22
|
+
```
|
23
|
+
|
24
|
+
## Contributing
|
25
|
+
Contribution directions go here.
|
26
|
+
|
27
|
+
## License
|
28
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
File without changes
|
@@ -0,0 +1,146 @@
|
|
1
|
+
class Account::InvitationsController < Account::ApplicationController
|
2
|
+
# the 'accept' action isn't covered by cancancan, because simply having the
|
3
|
+
# link is authorization enough to claim membership on a team. see `#accept`
|
4
|
+
# for more information.
|
5
|
+
account_load_and_authorize_resource :invitation, :team, except: [:show]
|
6
|
+
|
7
|
+
# we skip the onboarding requirement for users claiming and invitation.
|
8
|
+
# this way the invitation gets accepted after they complete the devise
|
9
|
+
# workflow, but before they're prompted to complete their onboarding.
|
10
|
+
skip_before_action :ensure_onboarding_is_complete_and_set_next_step, only: :accept
|
11
|
+
skip_before_action :authenticate_user!, only: :accept
|
12
|
+
|
13
|
+
# GET /invitations
|
14
|
+
# GET /invitations.json
|
15
|
+
def index
|
16
|
+
redirect_to [:account, @team, :memberships]
|
17
|
+
end
|
18
|
+
|
19
|
+
# GET /invitations/1
|
20
|
+
# GET /invitations/1.json
|
21
|
+
def show
|
22
|
+
# it's important that we only allow invitations to be shown via their uuid,
|
23
|
+
# otherwise team members can just step the id in the url to claim an
|
24
|
+
# invitation that would escalate their privileges.
|
25
|
+
@invitation = Invitation.find_by(uuid: params[:id])
|
26
|
+
unless @invitation
|
27
|
+
raise I18n.t("global.notifications.not_found")
|
28
|
+
end
|
29
|
+
@team = @invitation.team
|
30
|
+
|
31
|
+
# backfill these objects for the locale magic, since we didn't use `account_load_and_authorize_resource`.
|
32
|
+
@child_object = @invitation
|
33
|
+
@parent_object = @team
|
34
|
+
|
35
|
+
render layout: "devise"
|
36
|
+
end
|
37
|
+
|
38
|
+
# POST /invitations/1/accept
|
39
|
+
# POST /invitations/1/accept.json
|
40
|
+
def accept
|
41
|
+
# unless the user is signed in.
|
42
|
+
if !current_user.present?
|
43
|
+
|
44
|
+
# keep track of the uuid of the invitation so we can reload it
|
45
|
+
# after they sign up. at this point we don't even know if it's
|
46
|
+
# valid, but that's fine.
|
47
|
+
session[:invitation_uuid] = params[:id]
|
48
|
+
|
49
|
+
# also, we'll queue devise up to return to the invitation url after a sign in.
|
50
|
+
session["user_return_to"] = request.path
|
51
|
+
|
52
|
+
# assume the user needs to create an account.
|
53
|
+
# this is not the default for devise, but a sensible default here.
|
54
|
+
redirect_to new_user_registration_path
|
55
|
+
|
56
|
+
else
|
57
|
+
|
58
|
+
@invitation = Invitation.find_by(uuid: params[:id])
|
59
|
+
|
60
|
+
if @invitation
|
61
|
+
@team = @invitation.team
|
62
|
+
if @invitation.is_for?(current_user) || request.post?
|
63
|
+
@invitation.accept_for(current_user)
|
64
|
+
redirect_to account_dashboard_path, notice: I18n.t("invitations.notifications.welcome", team_name: @team.name)
|
65
|
+
else
|
66
|
+
redirect_to account_invitation_path(@invitation.uuid)
|
67
|
+
end
|
68
|
+
else
|
69
|
+
redirect_to account_dashboard_path
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# GET /invitations/new
|
76
|
+
def new
|
77
|
+
@invitation.build_membership
|
78
|
+
@cancel_path = only_allow_path(params[:cancel_path])
|
79
|
+
end
|
80
|
+
|
81
|
+
# POST /invitations
|
82
|
+
# POST /invitations.json
|
83
|
+
def create
|
84
|
+
@invitation.membership.team = current_team
|
85
|
+
# this allows notifications to be sent to a user before they've accepted their invitation.
|
86
|
+
@invitation.membership.user_email = @invitation.email
|
87
|
+
@invitation.from_membership = current_membership
|
88
|
+
respond_to do |format|
|
89
|
+
if @invitation.save
|
90
|
+
format.html { redirect_to account_team_invitations_path(@team), notice: I18n.t("invitations.notifications.created") }
|
91
|
+
format.json { render :show, status: :created, location: [:account, @team, @invitation] }
|
92
|
+
else
|
93
|
+
format.html { render :new, status: :unprocessable_entity }
|
94
|
+
format.json { render json: @invitation.errors, status: :unprocessable_entity }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# DELETE /invitations/1
|
100
|
+
# DELETE /invitations/1.json
|
101
|
+
def destroy
|
102
|
+
@invitation.destroy
|
103
|
+
respond_to do |format|
|
104
|
+
format.html { redirect_to account_team_invitations_path(@team), notice: I18n.t("invitations.notifications.destroyed") }
|
105
|
+
format.json { head :no_content }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
private
|
110
|
+
|
111
|
+
def manageable_role_keys
|
112
|
+
helpers.current_membership.manageable_roles.map(&:key)
|
113
|
+
end
|
114
|
+
|
115
|
+
# NOTE this method is only designed to work in the context of creating a invitation.
|
116
|
+
# we don't provide any support for updating invitations.
|
117
|
+
def invitation_params
|
118
|
+
# we use strong params first.
|
119
|
+
strong_params = params.require(:invitation).permit(
|
120
|
+
:email,
|
121
|
+
# 🚅 super scaffolding will insert new fields above this line.
|
122
|
+
# 🚅 super scaffolding will insert new arrays above this line.
|
123
|
+
membership_attributes: [
|
124
|
+
:user_first_name,
|
125
|
+
:user_last_name,
|
126
|
+
role_ids: []
|
127
|
+
],
|
128
|
+
)
|
129
|
+
|
130
|
+
# after that, we have to be more careful how we assign the roles.
|
131
|
+
# we can't let users assign roles to an invitation that they don't have permission
|
132
|
+
# to assign, but they do have permission to assign some to other team members.
|
133
|
+
if params[:invitation] && params[:invitation][:role_ids].present?
|
134
|
+
|
135
|
+
# ensure the list of role keys from the form only includes keys that they're allowed to assign.
|
136
|
+
assignable_role_keys_from_the_form = params[:invitation][:role_ids].map(&:to_i) & manageable_role_keys
|
137
|
+
|
138
|
+
strong_params[:role_ids] = assignable_role_keys_from_the_form
|
139
|
+
|
140
|
+
end
|
141
|
+
|
142
|
+
# 🚅 super scaffolding will insert processing for new fields above this line.
|
143
|
+
|
144
|
+
strong_params
|
145
|
+
end
|
146
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
class Account::Memberships::Reassignments::ScaffoldingCompletelyConcreteTangibleThingsReassignmentsController < Account::ApplicationController
|
2
|
+
account_load_and_authorize_resource :scaffolding_completely_concrete_tangible_things_reassignment, through: :membership, through_association: :reassignments_scaffolding_completely_concrete_tangible_things_reassignments
|
3
|
+
|
4
|
+
# GET /account/memberships/:membership_id/reassignments/scaffolding_completely_concrete_tangible_things_reassignments
|
5
|
+
# GET /account/memberships/:membership_id/reassignments/scaffolding_completely_concrete_tangible_things_reassignments.json
|
6
|
+
def index
|
7
|
+
# if you only want these objects shown on their parent's show page, uncomment this:
|
8
|
+
redirect_to [:account, @membership]
|
9
|
+
end
|
10
|
+
|
11
|
+
# GET /account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/:id
|
12
|
+
# GET /account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/:id.json
|
13
|
+
def show
|
14
|
+
end
|
15
|
+
|
16
|
+
# GET /account/memberships/:membership_id/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/new
|
17
|
+
def new
|
18
|
+
end
|
19
|
+
|
20
|
+
# GET /account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/:id/edit
|
21
|
+
def edit
|
22
|
+
end
|
23
|
+
|
24
|
+
# POST /account/memberships/:membership_id/reassignments/scaffolding_completely_concrete_tangible_things_reassignments
|
25
|
+
# POST /account/memberships/:membership_id/reassignments/scaffolding_completely_concrete_tangible_things_reassignments.json
|
26
|
+
def create
|
27
|
+
respond_to do |format|
|
28
|
+
if @scaffolding_completely_concrete_tangible_things_reassignment.save
|
29
|
+
format.html { redirect_to [:account, @membership, :reassignments_scaffolding_completely_concrete_tangible_things_reassignments], notice: I18n.t("memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments.notifications.created") }
|
30
|
+
format.json { render :show, status: :created, location: [:account, @scaffolding_completely_concrete_tangible_things_reassignment] }
|
31
|
+
else
|
32
|
+
format.html { render :new, status: :unprocessable_entity }
|
33
|
+
format.json { render json: @scaffolding_completely_concrete_tangible_things_reassignment.errors, status: :unprocessable_entity }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# PATCH/PUT /account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/:id
|
39
|
+
# PATCH/PUT /account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/:id.json
|
40
|
+
def update
|
41
|
+
respond_to do |format|
|
42
|
+
if @scaffolding_completely_concrete_tangible_things_reassignment.update(scaffolding_completely_concrete_tangible_things_reassignment_params)
|
43
|
+
format.html { redirect_to [:account, @scaffolding_completely_concrete_tangible_things_reassignment], notice: I18n.t("memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments.notifications.updated") }
|
44
|
+
format.json { render :show, status: :ok, location: [:account, @scaffolding_completely_concrete_tangible_things_reassignment] }
|
45
|
+
else
|
46
|
+
format.html { render :edit, status: :unprocessable_entity }
|
47
|
+
format.json { render json: @scaffolding_completely_concrete_tangible_things_reassignment.errors, status: :unprocessable_entity }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# DELETE /account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/:id
|
53
|
+
# DELETE /account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/:id.json
|
54
|
+
def destroy
|
55
|
+
@scaffolding_completely_concrete_tangible_things_reassignment.destroy
|
56
|
+
respond_to do |format|
|
57
|
+
format.html { redirect_to [:account, @membership, :reassignments_scaffolding_completely_concrete_tangible_things_reassignments], notice: I18n.t("memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments.notifications.destroyed") }
|
58
|
+
format.json { head :no_content }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# Never trust parameters from the scary internet, only allow the white list through.
|
65
|
+
def scaffolding_completely_concrete_tangible_things_reassignment_params
|
66
|
+
strong_params = params.require(:memberships_reassignments_scaffolding_completely_concrete_tangible_things_reassignment).permit(
|
67
|
+
# 🚅 super scaffolding will insert new fields above this line.
|
68
|
+
# 🚅 super scaffolding will insert new arrays above this line.
|
69
|
+
membership_ids: [],
|
70
|
+
)
|
71
|
+
|
72
|
+
strong_params[:membership_ids] = create_models_if_new(strong_params[:membership_ids]) do |email|
|
73
|
+
# stub out a new membership and invitation with no special permissions.
|
74
|
+
membership = current_team.memberships.create(user_email: email, added_by: current_membership)
|
75
|
+
current_team.invitations.create(email: email, from_membership: current_membership, membership: membership)
|
76
|
+
membership
|
77
|
+
end
|
78
|
+
|
79
|
+
# 🚅 super scaffolding will insert processing for new fields above this line.
|
80
|
+
|
81
|
+
strong_params
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
class Account::MembershipsController < Account::ApplicationController
|
2
|
+
account_load_and_authorize_resource :membership, :team, member_actions: [:demote, :promote, :reinvite], collection_actions: [:search]
|
3
|
+
|
4
|
+
def index
|
5
|
+
unless @memberships.count > 0
|
6
|
+
redirect_to account_team_invitations_path(@team), notice: I18n.t("memberships.notifications.no_members")
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def search
|
11
|
+
# TODO This is a particularly crazy example where we're doing the search logic ourselves in SQL.
|
12
|
+
# In the future, I could see us replacing this with a recommended example using Elasticsearch and the `searchkick` Ruby Gem.
|
13
|
+
limit = params[:limit] || 100
|
14
|
+
page = [params[:page].to_i, 1].max # Ensure we never have a negative or zero page value
|
15
|
+
search_term = "%#{params[:search]&.upcase}%"
|
16
|
+
offset = (page - 1) * limit
|
17
|
+
# Currently we're only searching on user.first_name, user.last_name, memberships.user_first_name and memberships.user_last_name. Should we also search on the email address?
|
18
|
+
# This query could use impromement. Currently if you search for "Ad Pal" you wouldn't find a user "Adam Pallozzi"
|
19
|
+
query = "UPPER(first_name) LIKE :search_term OR UPPER(last_name) LIKE :search_term OR UPPER(user_first_name) LIKE :search_term OR UPPER(user_last_name) LIKE :search_term"
|
20
|
+
# We're using left outer join here because we may get memberships that don't belong to a membership yet
|
21
|
+
memberships = @team.memberships.accessible_by(current_ability, :show).left_outer_joins(:user).where(query, search_term: search_term)
|
22
|
+
total_results = memberships.size
|
23
|
+
# the Areal.sql(LOWER(COALESCE...)) part means that if the user record doesn't exist or if there are records that start with a lower case letter, we will still sort everything correctly using the user.first_name instead.
|
24
|
+
memberships_array = memberships.limit(limit).offset(offset).order(Arel.sql("LOWER(COALESCE(first_name, user_first_name) )")).map { |membership| {id: membership.id, text: membership.label_string.to_s} }
|
25
|
+
results = {results: memberships_array, pagination: {more: (total_results > page * limit)}}
|
26
|
+
render json: results.to_json
|
27
|
+
end
|
28
|
+
|
29
|
+
def show
|
30
|
+
end
|
31
|
+
|
32
|
+
def edit
|
33
|
+
end
|
34
|
+
|
35
|
+
# PATCH/PUT /account/memberships/:id
|
36
|
+
# PATCH/PUT /account/memberships/:id.json
|
37
|
+
def update
|
38
|
+
respond_to do |format|
|
39
|
+
if @membership.update(membership_params)
|
40
|
+
format.html { redirect_to [:account, @membership], notice: I18n.t("memberships.notifications.updated") }
|
41
|
+
format.json { render :show, status: :ok, location: [:account, @membership] }
|
42
|
+
else
|
43
|
+
format.html { render :edit, status: :unprocessable_entity }
|
44
|
+
format.json { render json: @membership.errors, status: :unprocessable_entity }
|
45
|
+
end
|
46
|
+
rescue RemovingLastTeamAdminException => _
|
47
|
+
format.html { redirect_to [:account, @team, :memberships], alert: I18n.t("memberships.notifications.cant_demote") }
|
48
|
+
format.json { render json: {exception: I18n.t("memberships.notifications.cant_demote")}, status: :unprocessable_entity }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def demote
|
53
|
+
@membership.roles.delete Role.admin
|
54
|
+
redirect_to account_team_memberships_path(@team)
|
55
|
+
rescue RemovingLastTeamAdminException => _
|
56
|
+
redirect_to account_team_memberships_path(@team), alert: I18n.t("memberships.notifications.cant_demote")
|
57
|
+
end
|
58
|
+
|
59
|
+
def promote
|
60
|
+
@membership.roles << Role.admin unless @membership.roles.include?(Role.admin)
|
61
|
+
redirect_to account_team_memberships_path(@team)
|
62
|
+
end
|
63
|
+
|
64
|
+
def destroy
|
65
|
+
# Instead of destroying the membership, we nullify the user_id and use the membership record as a 'Tombstone' for referencing past associations (eg message at-mentions and Scaffolding::CompletelyConcrete::TangibleThings::Assignment)
|
66
|
+
|
67
|
+
user_was = @membership.user
|
68
|
+
@membership.nullify_user
|
69
|
+
|
70
|
+
if user_was == current_user
|
71
|
+
# if a user removes themselves from a team, we'll have to send them to their dashboard.
|
72
|
+
redirect_to account_dashboard_path, notice: I18n.t("memberships.notifications.you_removed_yourself", team_name: @team.name)
|
73
|
+
else
|
74
|
+
redirect_to [:account, @team, :memberships], notice: I18n.t("memberships.notifications.destroyed")
|
75
|
+
end
|
76
|
+
rescue RemovingLastTeamAdminException
|
77
|
+
redirect_to account_team_memberships_path(@team), alert: I18n.t("memberships.notifications.cant_remove")
|
78
|
+
end
|
79
|
+
|
80
|
+
def reinvite
|
81
|
+
@invitation = Invitation.new(membership: @membership, team: @team, email: @membership.user_email, from_membership: current_membership)
|
82
|
+
if @invitation.save
|
83
|
+
redirect_to [:account, @team, :memberships], notice: I18n.t("account.memberships.notifications.reinvited")
|
84
|
+
else
|
85
|
+
redirect_to [:account, @team, :memberships], notice: "There was an error creating the invitation (#{@invitation.errors.full_messages.to_sentence})"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
|
91
|
+
def manageable_role_keys
|
92
|
+
helpers.current_membership.manageable_roles.map(&:key)
|
93
|
+
end
|
94
|
+
|
95
|
+
# NOTE this method is only designed to work in the context of updating a membership.
|
96
|
+
# we don't provide any support for creating memberships other than by an invitation.
|
97
|
+
def membership_params
|
98
|
+
# we use strong params first.
|
99
|
+
strong_params = params.require(:membership).permit(
|
100
|
+
:user_first_name,
|
101
|
+
:user_last_name,
|
102
|
+
:user_profile_photo_id,
|
103
|
+
# 🚅 super scaffolding will insert new fields above this line.
|
104
|
+
# 🚅 super scaffolding will insert new arrays above this line.
|
105
|
+
)
|
106
|
+
|
107
|
+
# after that, we have to be more careful how we update the roles.
|
108
|
+
# we can't let users remove roles from a membership that they don't have permission
|
109
|
+
# to remove, but we want to allow them to add or remove other roles they do have
|
110
|
+
# permission to assign to other team members.
|
111
|
+
if params[:membership] && params[:membership][:role_ids].present?
|
112
|
+
|
113
|
+
# first, start with the list of role keys already assigned to this membership.
|
114
|
+
existing_role_keys = @membership.role_ids
|
115
|
+
|
116
|
+
# generate a list of role keys we can't allow the current user to remove from this membership.
|
117
|
+
existing_role_keys_that_are_unmanageable = existing_role_keys - manageable_role_keys
|
118
|
+
|
119
|
+
# now let's ensure the list of role keys from the form only includes keys that they're allowed to assign.
|
120
|
+
assignable_role_keys_from_the_form = params[:membership][:role_ids].map(&:to_s) & manageable_role_keys
|
121
|
+
|
122
|
+
# any role keys that are manageable by the current user have to then come from the form data,
|
123
|
+
# otherwise we can assume they were removed by being unchecked.
|
124
|
+
strong_params[:role_ids] = existing_role_keys_that_are_unmanageable + assignable_role_keys_from_the_form
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
# 🚅 super scaffolding will insert processing for new fields above this line.
|
129
|
+
|
130
|
+
strong_params
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
class Account::Onboarding::UserDetailsController < Account::ApplicationController
|
2
|
+
layout "devise"
|
3
|
+
load_and_authorize_resource class: "User"
|
4
|
+
|
5
|
+
# this is because cancancan doesn't let us set the instance variable name above.
|
6
|
+
before_action do
|
7
|
+
@user = @user_detail
|
8
|
+
end
|
9
|
+
|
10
|
+
# GET /users/1/edit
|
11
|
+
def edit
|
12
|
+
flash[:notice] = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
# PATCH/PUT /users/1
|
16
|
+
# PATCH/PUT /users/1.json
|
17
|
+
def update
|
18
|
+
respond_to do |format|
|
19
|
+
if @user.update(user_params)
|
20
|
+
# if you update your own user account, devise will normally kick you out, so we do this instead.
|
21
|
+
bypass_sign_in current_user.reload
|
22
|
+
|
23
|
+
if @user.details_provided?
|
24
|
+
format.html { redirect_to account_team_path(@user.teams.first), notice: "" }
|
25
|
+
else
|
26
|
+
format.html {
|
27
|
+
flash[:error] = I18n.t("global.notifications.all_fields_required")
|
28
|
+
redirect_to edit_account_onboarding_user_detail_path(@user)
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
format.json { render :show, status: :ok, location: [:account, @user] }
|
33
|
+
else
|
34
|
+
format.html { render :edit, status: :unprocessable_entity }
|
35
|
+
format.json { render json: @user.errors, status: :unprocessable_entity }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
# Never trust parameters from the scary internet, only allow the white list through.
|
43
|
+
def user_params
|
44
|
+
permitted_attributes = [
|
45
|
+
:first_name,
|
46
|
+
:last_name,
|
47
|
+
:time_zone,
|
48
|
+
# 🚅 super scaffolding will insert new fields above this line.
|
49
|
+
]
|
50
|
+
|
51
|
+
permitted_hash = {
|
52
|
+
# 🚅 super scaffolding will insert new arrays above this line.
|
53
|
+
}
|
54
|
+
|
55
|
+
if can? :edit, @user.current_team
|
56
|
+
permitted_hash[:current_team_attributes] = [:id, :name]
|
57
|
+
end
|
58
|
+
|
59
|
+
params.require(:user).permit(permitted_attributes, permitted_hash)
|
60
|
+
|
61
|
+
# 🚅 super scaffolding will insert processing for new fields above this line.
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
class Account::Onboarding::UserEmailController < Account::ApplicationController
|
2
|
+
layout "devise"
|
3
|
+
load_and_authorize_resource class: "User"
|
4
|
+
|
5
|
+
# this is because cancancan doesn't let us set the instance variable name above.
|
6
|
+
before_action do
|
7
|
+
@user = @user_email
|
8
|
+
end
|
9
|
+
|
10
|
+
# GET /users/1/edit
|
11
|
+
def edit
|
12
|
+
flash[:notice] = nil
|
13
|
+
if @user.email_is_oauth_placeholder?
|
14
|
+
@user.email = nil
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# PATCH/PUT /users/1
|
19
|
+
# PATCH/PUT /users/1.json
|
20
|
+
def update
|
21
|
+
respond_to do |format|
|
22
|
+
if @user.update(user_params)
|
23
|
+
# if you update your own user account, devise will normally kick you out, so we do this instead.
|
24
|
+
bypass_sign_in current_user.reload
|
25
|
+
|
26
|
+
if !@user.email_is_oauth_placeholder?
|
27
|
+
@user.send_welcome_email
|
28
|
+
format.html { redirect_to account_team_path(@user.teams.first), notice: "" }
|
29
|
+
else
|
30
|
+
format.html {
|
31
|
+
flash[:error] = I18n.t("global.notifications.all_fields_required")
|
32
|
+
redirect_to edit_account_onboarding_user_detail_path(@user)
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
format.json { render :show, status: :ok, location: [:account, @user] }
|
37
|
+
else
|
38
|
+
|
39
|
+
# this is just checking whether the error on the email field is taking the email
|
40
|
+
# address is already taken.
|
41
|
+
@email_taken = begin
|
42
|
+
@user.errors.details[:email].select { |error| error[:error] == :taken }.any?
|
43
|
+
rescue
|
44
|
+
false
|
45
|
+
end
|
46
|
+
|
47
|
+
format.html { render :edit, status: :unprocessable_entity }
|
48
|
+
format.json { render json: @user.errors, status: :unprocessable_entity }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
# Never trust parameters from the scary internet, only allow the white list through.
|
56
|
+
def user_params
|
57
|
+
params.require(:user).permit(
|
58
|
+
:email,
|
59
|
+
# 🚅 super scaffolding will insert new fields above this line.
|
60
|
+
# 🚅 super scaffolding will insert new arrays above this line.
|
61
|
+
)
|
62
|
+
|
63
|
+
# 🚅 super scaffolding will insert processing for new fields above this line.
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
class Account::TeamsController < Account::ApplicationController
|
2
|
+
load_and_authorize_resource :team, class: "Team", prepend: true
|
3
|
+
|
4
|
+
prepend_before_action do
|
5
|
+
if params["action"] == "new"
|
6
|
+
current_user.current_team = nil
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
before_action :enforce_invitation_only, only: [:create]
|
11
|
+
|
12
|
+
before_action do
|
13
|
+
# for magic locales.
|
14
|
+
@child_object = @team
|
15
|
+
end
|
16
|
+
|
17
|
+
# GET /teams
|
18
|
+
# GET /teams.json
|
19
|
+
def index
|
20
|
+
# if a user doesn't have multiple teams, we try to simplify the team ui/ux
|
21
|
+
# as much as possible. links to this page should go to the current team
|
22
|
+
# dashboard. however, some other links to this page are actually in branch
|
23
|
+
# logic and will not display at all. instead, users will be linked to the
|
24
|
+
# "new team" page. (see the main account sidebar menu for an example of
|
25
|
+
# this.)
|
26
|
+
unless current_user.multiple_teams?
|
27
|
+
redirect_to account_team_path(current_team)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# POST /teams/1/switch
|
32
|
+
def switch_to
|
33
|
+
current_user.current_team = @team
|
34
|
+
current_user.save
|
35
|
+
redirect_to account_dashboard_path
|
36
|
+
end
|
37
|
+
|
38
|
+
# GET /teams/1
|
39
|
+
# GET /teams/1.json
|
40
|
+
def show
|
41
|
+
# I don't think this is the best place to close the loop on the onboarding process, but practically speaking it's
|
42
|
+
# the easiest place to implement this at the moment, because all the onboarding steps redirect here on success.
|
43
|
+
if session[:after_onboarding_url].present?
|
44
|
+
redirect_to session.delete(:after_onboarding_url)
|
45
|
+
end
|
46
|
+
|
47
|
+
current_user.current_team = @team
|
48
|
+
current_user.save
|
49
|
+
end
|
50
|
+
|
51
|
+
# GET /teams/new
|
52
|
+
def new
|
53
|
+
render :new, layout: "devise"
|
54
|
+
end
|
55
|
+
|
56
|
+
# GET /teams/1/edit
|
57
|
+
def edit
|
58
|
+
end
|
59
|
+
|
60
|
+
# POST /teams
|
61
|
+
# POST /teams.json
|
62
|
+
def create
|
63
|
+
@team = Team.new(team_params)
|
64
|
+
|
65
|
+
respond_to do |format|
|
66
|
+
if @team.save
|
67
|
+
|
68
|
+
# also make the creator of the team the default admin.
|
69
|
+
@team.memberships.create(user: current_user, roles: [Role.admin])
|
70
|
+
|
71
|
+
current_user.current_team = @team
|
72
|
+
current_user.former_user = false
|
73
|
+
current_user.save
|
74
|
+
|
75
|
+
format.html { redirect_to [:account, @team], notice: I18n.t("teams.notifications.created") }
|
76
|
+
format.json { render :show, status: :created, location: [:account, @team] }
|
77
|
+
else
|
78
|
+
format.html { render :new, layout: "devise" }
|
79
|
+
format.json { render json: @team.errors, status: :unprocessable_entity }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# PATCH/PUT /teams/1
|
85
|
+
# PATCH/PUT /teams/1.json
|
86
|
+
def update
|
87
|
+
respond_to do |format|
|
88
|
+
if @team.update(team_params)
|
89
|
+
format.html { redirect_to [:account, @team], notice: I18n.t("teams.notifications.updated") }
|
90
|
+
format.json { render :show, status: :ok, location: [:account, @team] }
|
91
|
+
else
|
92
|
+
format.html { render :edit, status: :unprocessable_entity }
|
93
|
+
format.json { render json: @team.errors, status: :unprocessable_entity }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# # DELETE /teams/1
|
99
|
+
# # DELETE /teams/1.json
|
100
|
+
# def destroy
|
101
|
+
# @team.destroy
|
102
|
+
# respond_to do |format|
|
103
|
+
# format.html { redirect_to account_teams_url, notice: 'Team was successfully destroyed.' }
|
104
|
+
# format.json { head :no_content }
|
105
|
+
# end
|
106
|
+
# end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
# Never trust parameters from the scary internet, only allow the white list through.
|
111
|
+
def team_params
|
112
|
+
params.require(:team).permit(
|
113
|
+
:name,
|
114
|
+
:time_zone,
|
115
|
+
:locale,
|
116
|
+
# 🚅 super scaffolding will insert new fields above this line.
|
117
|
+
# 🚅 super scaffolding will insert new arrays above this line.
|
118
|
+
)
|
119
|
+
|
120
|
+
# 🚅 super scaffolding will insert processing for new fields above this line.
|
121
|
+
end
|
122
|
+
end
|