diff --git a/data/model/organization.py b/data/model/organization.py index e83c6907e..a63b020cc 100644 --- a/data/model/organization.py +++ b/data/model/organization.py @@ -35,23 +35,30 @@ def get_organization(name): def convert_user_to_organization(user_obj, admin_user): - # Change the user to an organization. - user_obj.organization = True + if user_obj.robot: + raise DataModelException('Cannot convert a robot into an organization') - # disable this account for login. - user_obj.password_hash = None - user_obj.save() + with db_transaction(): + # Change the user to an organization and disable this account for login. + user_obj.organization = True + user_obj.password_hash = None + user_obj.save() - # Clear any federated auth pointing to this user - FederatedLogin.delete().where(FederatedLogin.user == user_obj).execute() + # Clear any federated auth pointing to this user. + FederatedLogin.delete().where(FederatedLogin.user == user_obj).execute() - # Create a team for the owners - owners_team = team.create_team('owners', user_obj, 'admin') + # Delete any user-specific permissions on repositories. + (RepositoryPermission.delete() + .where(RepositoryPermission.user == user_obj) + .execute()) - # Add the user who will admin the org to the owners team - team.add_user_to_team(admin_user, owners_team) + # Create a team for the owners + owners_team = team.create_team('owners', user_obj, 'admin') - return user_obj + # Add the user who will admin the org to the owners team + team.add_user_to_team(admin_user, owners_team) + + return user_obj def get_user_organizations(username): diff --git a/static/css/directives/ui/convert-user-to-org.css b/static/css/directives/ui/convert-user-to-org.css index c0cf59336..eefa6c44a 100644 --- a/static/css/directives/ui/convert-user-to-org.css +++ b/static/css/directives/ui/convert-user-to-org.css @@ -1,9 +1,14 @@ +.convert-user-to-org-element { + padding: 10px; +} + .convert-user-to-org .convert-form h3 { margin-bottom: 20px; } .convert-user-to-org #convertForm { max-width: 700px; + margin-top: 20px; } .convert-user-to-org #convertForm .form-group { @@ -12,24 +17,53 @@ .convert-user-to-org #convertForm input { margin-bottom: 10px; - margin-left: 20px; } .convert-user-to-org #convertForm .existing-data { + display: block; font-size: 16px; font-weight: bold; } +.convert-user-to-org #convertForm .existing-data .avatar { + margin-right: 4px; +} + +.convert-user-to-org #convertForm .existing-data .username { + vertical-align: middle; +} + + .convert-user-to-org #convertForm .description { margin-top: 10px; display: block; color: #888; font-size: 12px; - margin-left: 20px; } -.convert-user-to-org #convertForm .existing-data { - display: block; - padding-left: 20px; - margin-top: 10px; +.convert-user-to-org .org-list { + list-style: none; } + +.convert-user-to-org .org-list li { + margin-top: 4px; +} + +.convert-user-to-org .org-list li a { + vertical-align: middle; + margin-left: 6px; +} + +.convert-user-to-org .fa-arrow-circle-right { + margin-left: 6px; +} + +.convert-user-to-org .form-group-content { + padding-left: 10px; + padding-top: 10px; +} + + +.convert-user-to-org .form-group-content .co-table { + margin: 0px; +} \ No newline at end of file diff --git a/static/css/directives/ui/plans-table.css b/static/css/directives/ui/plans-table.css new file mode 100644 index 000000000..434c3bb68 --- /dev/null +++ b/static/css/directives/ui/plans-table.css @@ -0,0 +1,30 @@ +.plans-table-element table { + margin: 20px; + border: 1px solid #eee; +} + +.plans-table-element thead td { + padding-top: 10px !important; +} + +.plans-table-element td { + vertical-align: middle !important; +} + +.plans-table-element .plan-price { + font-size: 16px; +} + +.plans-table ul { + margin-top: 10px; + padding: 0px; +} + +.plans-table ul li { + padding: 4px; + margin: 0px; +} + +.plans-table ul li .plan-info { + padding: 4px; +} \ No newline at end of file diff --git a/static/css/quay.css b/static/css/quay.css index e2938b286..ffd627e63 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -2819,34 +2819,6 @@ p.editable:hover i { margin-bottom: 20px; } -.plans-table-element table { - margin: 20px; - border: 1px solid #eee; -} - -.plans-table-element td { - vertical-align: middle !important; -} - -.plans-table-element .plan-price { - font-size: 16px; -} - -.plans-table ul { - margin-top: 10px; - padding: 0px; -} - -.plans-table ul li { - padding: 4px; - margin: 0px; -} - -.plans-table ul li .plan-info { - padding: 4px; -} - - .repo-breadcrumb-element .crumb { cursor: pointer; } diff --git a/static/directives/convert-user-to-org.html b/static/directives/convert-user-to-org.html index 4ba2d356a..774260822 100644 --- a/static/directives/convert-user-to-org.html +++ b/static/directives/convert-user-to-org.html @@ -1,51 +1,63 @@ <div class="convert-user-to-org-element"> <!-- Step 0 --> - <div class="panel" ng-show="convertStep == 0"> - <div class="panel-body" ng-show="user.organizations.length > 0"> - <div class="co-alert co-alert-info"> - Cannot convert this account into an organization, as it is a member of {{user.organizations.length}} other - organization{{user.organizations.length > 1 ? 's' : ''}}. Please leave - {{user.organizations.length > 1 ? 'those organizations' : 'that organization'}} first. - </div> + <div ng-show="convertStep == 0"> + <div ng-show="user.organizations.length > 0"> + Cannot convert this account into an organization, as it is a member of {{user.organizations.length}} other + organization{{user.organizations.length > 1 ? 's' : ''}}. + <br><br> + Please leave the following organizations first: + <ul class="org-list"> + <li ng-repeat="org in user.organizations"> + <span class="avatar" size="avatarSize || 16" data="org.avatar"></span> + <a href="/organization/{{ org.name }}">{{ org.name }}</a> + </li> + </ul> </div> - <div class="panel-body" ng-show="user.organizations.length == 0"> - <div class="co-alert co-alert-warning"> - Note: Converting a user account into an organization <b>cannot be undone</b> - </div> - - <button class="btn btn-primary" ng-click="showConvertForm()">Start conversion process</button> + <div ng-show="user.organizations.length == 0"> + <button class="btn btn-primary" ng-click="showConvertForm()">Start conversion process <i class="fa fa-arrow-circle-right" aria-hidden="true"></i></button> </div> </div> <!-- Step 1 --> <div class="convert-form" ng-show="convertStep == 1"> + Fill out the form below to convert your current user account into an organization. Your existing repositories will be maintained under the + namespace. All <strong>direct</strong> permissions delegated to {{ user.username }} will be deleted. + <form method="post" name="convertForm" id="convertForm" ng-submit="convertToOrg()"> <div class="form-group"> <label for="orgName">Organization Name</label> - <div class="existing-data"> - <span class="avatar" size="24" data="user.avatar"></span> - {{ user.username }}</div> - <span class="description">This will continue to be the namespace for your repositories</span> + <div class="form-group-content"> + <div class="existing-data"> + <span class="avatar" size="24" data="user.avatar"></span> + <span class="username">{{ user.username }}</span> + </div> + <span class="description">This will continue to be the namespace for your repositories</span> + </div> </div> <div class="form-group"> <label for="orgName">Admin User</label> - <input id="adminUsername" name="adminUsername" type="text" class="form-control" placeholder="Admin Username" - ng-model="org.adminUser" required autofocus> - <input id="adminPassword" name="adminPassword" type="password" class="form-control" placeholder="Admin Password" - ng-model="org.adminPassword" required> - <span class="description"> - The username and password for the account that will become an administrator of the organization. - Note that this account <b>must be a separate registered account</b> from the account that you are - trying to convert, and <b>must already exist</b>. - </span> + <div class="form-group-content"> + <input id="adminUsername" name="adminUsername" type="text" class="form-control" placeholder="Admin Username" + ng-model="org.adminUser" required autofocus> + <input id="adminPassword" name="adminPassword" type="password" class="form-control" placeholder="Admin Password" + ng-model="org.adminPassword" required> + <span class="description"> + The username and password for the account that will become an administrator of the organization. + Note that this account <b>must be a separate registered account</b> from the account that you are + trying to convert, and <b>must already exist</b>. + </span> + </div> </div> <!-- Plans Table --> <div class="form-group plan-group" quay-require="['BILLING']"> <label>Organization Plan</label> - <div class="plans-table" plans="orgPlans" current-plan="org.plan"></div> + <div class="form-group-content"> + <div class="plans-table" plans="orgPlans" current-plan="org.plan"></div> + <span class="description">The billing plan for the new organization. If private repositories are unneeded, select "Open Source".</span> + </div> </div> <div class="button-bar"> @@ -60,7 +72,7 @@ <!-- Modal message dialog --> <div class="modal fade" id="cannotconvertModal"> - <div class="modal-dialog"> + <div class="modal-dialog co-dialog"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> @@ -78,8 +90,8 @@ <!-- Modal message dialog --> - <div class="modal fade" id="reallyconvertModal"> - <div class="modal-dialog"> + <div class="modal co-modal fade" id="reallyconvertModal"> + <div class="modal-dialog co-dialog"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> diff --git a/static/js/services/api-service.js b/static/js/services/api-service.js index 311879661..2c48cfb71 100644 --- a/static/js/services/api-service.js +++ b/static/js/services/api-service.js @@ -316,7 +316,7 @@ angular.module('quay').factory('ApiService', ['Restangular', '$q', 'UtilService' bootbox.dialog({ "message": message, - "title": defaultMessage, + "title": defaultMessage || 'Request Failure', "buttons": { "close": { "label": "Close", diff --git a/test/test_api_usage.py b/test/test_api_usage.py index 66e24df22..7f6e69c7c 100644 --- a/test/test_api_usage.py +++ b/test/test_api_usage.py @@ -417,6 +417,15 @@ class TestConvertToOrganization(ApiTestCase): def test_convert(self): self.login(READ_ACCESS_USER) + + # Add at least one permission for the read-user. + read_user = model.user.get_user(READ_ACCESS_USER) + simple_repo = model.repository.get_repository(ADMIN_ACCESS_USER, 'simple') + read_role = database.Role.get(name='read') + + database.RepositoryPermission.create(user=read_user, repository=simple_repo, role=read_role) + + # Convert the read user into an organization. json = self.postJsonResponse(ConvertToOrganization, data={'adminUser': ADMIN_ACCESS_USER, 'adminPassword': 'password', @@ -436,6 +445,11 @@ class TestConvertToOrganization(ApiTestCase): self.assertEquals(READ_ACCESS_USER, json['name']) self.assertEquals(True, json['is_admin']) + # Verify the now-org has no permissions. + count = (database.RepositoryPermission.select() + .where(database.RepositoryPermission.user == organization) + .count()) + self.assertEquals(0, count) def test_convert_via_email(self): self.login(READ_ACCESS_USER)