From 21d22a7dfb89e3003e55790bef4a071552d4d88e Mon Sep 17 00:00:00 2001 From: Juanfran Date: Fri, 26 Feb 2016 08:20:43 +0100 Subject: [PATCH] edit project restrictions (max public/private projects) --- .../modules/admin/project-profile.coffee | 29 ++- app/coffee/modules/auth.coffee | 11 + app/coffee/modules/resources.coffee | 1 + app/locales/taiga/locale-en.json | 6 +- .../services/current-user.service.coffee | 48 ++++ .../services/current-user.service.spec.coffee | 242 ++++++++++++++++++ app/partials/admin/admin-project-profile.jade | 7 + .../admin/admin-project-restrictions.jade | 11 + app/styles/components/buttons.scss | 12 + .../modules/admin/admin-project-profile.scss | 43 ++++ gulpfile.js | 3 +- test-utils.js | 2 + 12 files changed, 409 insertions(+), 6 deletions(-) create mode 100644 app/partials/admin/admin-project-restrictions.jade diff --git a/app/coffee/modules/admin/project-profile.coffee b/app/coffee/modules/admin/project-profile.coffee index 9e81d553..e28b1ef7 100644 --- a/app/coffee/modules/admin/project-profile.coffee +++ b/app/coffee/modules/admin/project-profile.coffee @@ -51,11 +51,13 @@ class ProjectProfileController extends mixOf(taiga.Controller, taiga.PageMixin) "$tgLocation", "$tgNavUrls", "tgAppMetaService", - "$translate" + "$translate", + "$tgAuth", + "tgCurrentUserService" ] constructor: (@scope, @rootscope, @repo, @confirm, @rs, @params, @q, @location, @navUrls, - @appMetaService, @translate) -> + @appMetaService, @translate, @tgAuth, @currentUserService) -> @scope.project = {} promise = @.loadInitialData() @@ -67,6 +69,11 @@ class ProjectProfileController extends mixOf(taiga.Controller, taiga.PageMixin) description = @scope.project.description @appMetaService.setAll(title, description) + @scope.canBePrivateProject = @.currentUserService.canBePrivateProject(@scope.project.id) + @scope.canBePublicProject = @.currentUserService.canBePublicProject(@scope.project.id) + + @scope.isPrivateProject = @scope.project.is_private + promise.then null, @.onInitialDataError.bind(@) @scope.$on "project:loaded", => @@ -94,8 +101,10 @@ class ProjectProfileController extends mixOf(taiga.Controller, taiga.PageMixin) return project loadInitialData: -> - promise = @.loadProject() - return promise + return @q.all([ + @.loadProject(), + @tgAuth.refresh() + ]) openDeleteLightbox: -> @rootscope.$broadcast("deletelightbox:new", @scope.project) @@ -510,3 +519,15 @@ ProjectLogoModelDirective = ($parse) -> return {link:link} module.directive('tgProjectLogoModel', ['$parse', ProjectLogoModelDirective]) + + +AdminProjectRestrictionsDirective = () -> + return { + scope: { + "canBePrivateProject": "=", + "canBePublicProject": "=" + }, + templateUrl: "admin/admin-project-restrictions.html" + } + +module.directive('tgAdminProjectRestrictions', [AdminProjectRestrictionsDirective]) diff --git a/app/coffee/modules/auth.coffee b/app/coffee/modules/auth.coffee index 452bd217..b6448ca9 100644 --- a/app/coffee/modules/auth.coffee +++ b/app/coffee/modules/auth.coffee @@ -135,6 +135,17 @@ class AuthService extends taiga.Service return false ## Http interface + refresh: () -> + url = @urls.resolve("user-me") + + return @http.get(url).then (data, status) => + user = data.data + user.token = @.getUser().auth_token + + user = @model.make_model("users", user) + + @.setUser(user) + return user login: (data, type) -> url = @urls.resolve("auth") diff --git a/app/coffee/modules/resources.coffee b/app/coffee/modules/resources.coffee index fda8262b..a4a45eb9 100644 --- a/app/coffee/modules/resources.coffee +++ b/app/coffee/modules/resources.coffee @@ -45,6 +45,7 @@ urls = { "user-voted": "/users/%s/voted" "user-watched": "/users/%s/watched" "user-contacts": "/users/%s/contacts" + "user-me": "/users/me" # User - Notification "permissions": "/permissions" diff --git a/app/locales/taiga/locale-en.json b/app/locales/taiga/locale-en.json index 138fdb09..28c7e652 100644 --- a/app/locales/taiga/locale-en.json +++ b/app/locales/taiga/locale-en.json @@ -463,7 +463,11 @@ "DELETE": "Delete this project", "LOGO_HELP": "The image will be scaled to 80x80px.", "CHANGE_LOGO": "Change logo", - "ACTION_USE_DEFAULT_LOGO": "Use default image" + "ACTION_USE_DEFAULT_LOGO": "Use default image", + "MAX_PRIVATE_PROJECTS": "You've reached the maximum number of private projects", + "MAX_PRIVATE_PROJECTS_MEMBERS": "The project exceeds the maximun members number in private projects", + "MAX_PUBLIC_PROJECTS": "You've reached the maximum number of public projects", + "MAX_PUBLIC_PROJECTS_MEMBERS": "The project exceeds the maximun members number in public projects" }, "REPORTS": { "TITLE": "Reports", diff --git a/app/modules/services/current-user.service.coffee b/app/modules/services/current-user.service.coffee index 568c53f3..44fc08ce 100644 --- a/app/modules/services/current-user.service.coffee +++ b/app/modules/services/current-user.service.coffee @@ -118,4 +118,52 @@ class CurrentUserService return @.projects + canBePrivateProject: (projectId) -> + project = @.projects.get('all').find (project) -> project.get('id') == projectId + + return {valid: true} if project.get('is_private') + + result = @.canCreatePrivateProjects() + + return result if !result.valid + + user = @.getUser() + + if user.get('max_members_private_projects') != null && project.get('members').size >= user.get('max_members_private_projects') + return {valid: false, reason: 'max_members_private_projects', type: 'private_project'} + + return {valid: true} + + canBePublicProject: (projectId) -> + project = @.projects.get('all').find (project) -> project.get('id') == projectId + + return {valid: true} if !project.get('is_private') + + result = @.canCreatePublicProjects() + + return result if !result.valid + + user = @.getUser() + + if user.get('max_members_public_projects') != null && project.get('members').size >= user.get('max_members_public_projects') + return {valid: false, reason: 'max_members_public_projects', type: 'public_project'} + + return {valid: true} + + canCreatePrivateProjects: () -> + user = @.getUser() + + if user.get('max_private_projects') != null && user.get('max_private_projects') <= user.get('total_private_projects') + return {valid: false, reason: 'max_private_projects', type: 'private_project'} + + return {valid: true} + + canCreatePublicProjects: () -> + user = @.getUser() + + if user.get('max_public_projects') != null && user.get('max_public_projects') <= user.get('total_public_projects') + return {valid: false, reason: 'max_public_projects', type: 'public_project'} + + return {valid: true} + angular.module("taigaCommon").service("tgCurrentUserService", CurrentUserService) diff --git a/app/modules/services/current-user.service.spec.coffee b/app/modules/services/current-user.service.spec.coffee index a3fe43b7..32b51b0b 100644 --- a/app/modules/services/current-user.service.spec.coffee +++ b/app/modules/services/current-user.service.spec.coffee @@ -203,3 +203,245 @@ describe "tgCurrentUserService", -> expect(config).to.be.eql(joyride) done() + + it "the user can't create private projects if they reach the maximum number of private projects", () -> + user = Immutable.fromJS({ + id: 1, + name: "fake1", + max_private_projects: 1, + total_private_projects: 1 + }) + + currentUserService._user = user + + result = currentUserService.canCreatePrivateProjects(0) + + expect(result).to.be.eql({ + valid: false, + reason: 'max_private_projects', + type: 'private_project' + }) + + it "the user can create private projects", () -> + user = Immutable.fromJS({ + id: 1, + name: "fake1", + max_private_projects: 10, + total_private_projects: 1, + max_members_private_projects: 20 + }) + + currentUserService._user = user + + result = currentUserService.canCreatePrivateProjects(10) + + expect(result).to.be.eql({ + valid: true + }) + + it "the user can't convert a private project to a public project if they reach the maximum number of members", () -> + user = Immutable.fromJS({ + id: 1, + name: "fake1", + max_private_projects: 10, + total_private_projects: 1, + max_members_public_projects: 2 + }) + + currentUserService._user = user + + projects = Immutable.fromJS({ + all: [ + {id: 1, name: "fake1"}, + {id: 2, name: "fake2", members: [1, 2, 3, 4, 5], is_private: true}, + {id: 3, name: "fake3"}, + {id: 4, name: "fake4"} + ] + }) + + currentUserService._projects = projects + + result = currentUserService.canBePublicProject(2) + + expect(result).to.be.eql({ + valid: false, + reason: 'max_members_public_projects', + type: 'public_project' + }) + + it "the user can convert private projects to a public project", () -> + user = Immutable.fromJS({ + id: 1, + name: "fake1", + max_private_projects: 10, + total_private_projects: 1, + max_members_public_projects: 20 + }) + + currentUserService._user = user + + projects = Immutable.fromJS({ + all: [ + {id: 1, name: "fake1"}, + {id: 2, name: "fake2", members: [1, 2, 3, 4, 5]}, + {id: 3, name: "fake3"}, + {id: 4, name: "fake4"} + ] + }) + + currentUserService._projects = projects + + result = currentUserService.canBePublicProject(2) + + expect(result).to.be.eql({ + valid: true + }) + + it "the user can convert public projects to a public project if it is already public", () -> + user = Immutable.fromJS({ + id: 1, + name: "fake1", + max_private_projects: 10, + total_private_projects: 100, + max_members_public_projects: 2 + }) + + currentUserService._user = user + + projects = Immutable.fromJS({ + all: [ + {id: 1, name: "fake1"}, + {id: 2, name: "fake2", members: [1, 2, 3, 4, 5], is_private: false}, + {id: 3, name: "fake3"}, + {id: 4, name: "fake4"} + ] + }) + + currentUserService._projects = projects + + result = currentUserService.canBePublicProject(2) + + expect(result).to.be.eql({ + valid: true + }) + + it "the user can't create public projects if they reach the maximum number of private projects", () -> + user = Immutable.fromJS({ + id: 1, + name: "fake1", + max_public_projects: 1, + total_public_projects: 1 + }) + + currentUserService._user = user + + result = currentUserService.canCreatePublicProjects(0) + + expect(result).to.be.eql({ + valid: false, + reason: 'max_public_projects', + type: 'public_project' + }) + + it "the user can create public projects", () -> + user = Immutable.fromJS({ + id: 1, + name: "fake1", + max_public_projects: 10, + total_public_projects: 1, + max_members_public_projects: 20 + }) + + currentUserService._user = user + + result = currentUserService.canCreatePublicProjects(10) + + expect(result).to.be.eql({ + valid: true + }) + + it "the user can't convert a public projects to a private project if they reach the maximum number of members", () -> + user = Immutable.fromJS({ + id: 1, + name: "fake1", + max_public_projects: 10, + total_public_projects: 1, + max_members_private_projects: 2 + }) + + currentUserService._user = user + + projects = Immutable.fromJS({ + all: [ + {id: 1, name: "fake1"}, + {id: 2, name: "fake2", members: [1, 2, 3, 4, 5]}, + {id: 3, name: "fake3"}, + {id: 4, name: "fake4"} + ] + }) + + currentUserService._projects = projects + + result = currentUserService.canBePrivateProject(2) + + expect(result).to.be.eql({ + valid: false, + reason: 'max_members_private_projects', + type: 'private_project' + }) + + it "the user can convert public projects to a private project", () -> + user = Immutable.fromJS({ + id: 1, + name: "fake1", + max_public_projects: 10, + total_public_projects: 1, + max_members_private_projects: 20 + }) + + currentUserService._user = user + + projects = Immutable.fromJS({ + all: [ + {id: 1, name: "fake1"}, + {id: 2, name: "fake2", members: [1, 2, 3, 4, 5]}, + {id: 3, name: "fake3"}, + {id: 4, name: "fake4"} + ] + }) + + currentUserService._projects = projects + + result = currentUserService.canBePrivateProject(2) + + expect(result).to.be.eql({ + valid: true + }) + + it "the user can convert private project to a private project if it is already private", () -> + user = Immutable.fromJS({ + id: 1, + name: "fake1", + max_public_projects: 10, + total_public_projects: 1, + max_members_private_projects: 20 + }) + + currentUserService._user = user + + projects = Immutable.fromJS({ + all: [ + {id: 1, name: "fake1"}, + {id: 2, name: "fake2", members: [1, 2, 3, 4, 5]}, + {id: 3, name: "fake3"}, + {id: 4, name: "fake4"} + ] + }) + + currentUserService._projects = projects + + result = currentUserService.canBePrivateProject(2) + + expect(result).to.be.eql({ + valid: true + }) diff --git a/app/partials/admin/admin-project-profile.jade b/app/partials/admin/admin-project-profile.jade index a527c4bb..bb5e3b2e 100644 --- a/app/partials/admin/admin-project-profile.jade +++ b/app/partials/admin/admin-project-profile.jade @@ -126,10 +126,16 @@ div.wrapper( placeholder="{{ 'ADMIN.PROJECT_PROFILE.RECRUITING_PLACEHOLDER' | translate }}" ) + tg-admin-project-restrictions( + can-be-private-project="canBePrivateProject" + can-be-public-project="canBePublicProject" + ) + fieldset .project-privacy-settings div.privacy-option input.privacy-project( + ng-disabled="!canBePublicProject.valid" type="radio" id="private-project" name="privacy-project" @@ -140,6 +146,7 @@ div.wrapper( div.privacy-option input.privacy-project( + ng-disabled="!canBePrivateProject.valid" type="radio" id="public-project" name="privacy-project" diff --git a/app/partials/admin/admin-project-restrictions.jade b/app/partials/admin/admin-project-restrictions.jade new file mode 100644 index 00000000..12a481bf --- /dev/null +++ b/app/partials/admin/admin-project-restrictions.jade @@ -0,0 +1,11 @@ +fieldset(ng-if="!canBePrivateProject.valid") + p + span(ng-if="canBePrivateProject.reason == 'max_private_projects'") {{ 'ADMIN.PROJECT_PROFILE.MAX_PRIVATE_PROJECTS' | translate }} + + span(ng-if="canBePrivateProject.reason == 'max_members_private_projects'") {{ 'ADMIN.PROJECT_PROFILE.MAX_PRIVATE_PROJECTS_MEMBERS' | translate }} + +fieldset(ng-if="!canBePublicProject.valid") + p + span(ng-if="canBePublicProject.reason == 'max_public_projects'") {{ 'ADMIN.PROJECT_PROFILE.MAX_PUBLIC_PROJECTS' | translate }} + + span(ng-if="canBePublicProject.reason == 'max_members_public_projects'") {{ 'ADMIN.PROJECT_PROFILE.MAX_PUBLIC_PROJECTS_MEMBERS' | translate }} diff --git a/app/styles/components/buttons.scss b/app/styles/components/buttons.scss index 7c57383b..77c4b389 100755 --- a/app/styles/components/buttons.scss +++ b/app/styles/components/buttons.scss @@ -31,6 +31,18 @@ .icon { color: $white; } + &.disabled, + &[disabled] { + background: lighten($whitish, 10%); + box-shadow: none; + color: $gray-light; + cursor: not-allowed; + opacity: .65; + &:hover { + background: lighten($whitish, 10%); + color: $gray-light; + } + } } .trans-button { diff --git a/app/styles/modules/admin/admin-project-profile.scss b/app/styles/modules/admin/admin-project-profile.scss index 5e4e0483..50562431 100644 --- a/app/styles/modules/admin/admin-project-profile.scss +++ b/app/styles/modules/admin/admin-project-profile.scss @@ -103,4 +103,47 @@ display: block; } } + .privacy-project[disabled] { + + label { + background: $whitish; + box-shadow: none; + color: $gray-light; + cursor: not-allowed; + opacity: .65; + &:hover { + background: $whitish; + color: $gray-light; + } + } + } +} + +tg-admin-project-restrictions { + p { + @extend %xsmall; + margin-bottom: 0; + } + span { + display: block; + } + a { + color: $primary; + } + fieldset { + text-align: center; + } + span:first-child { + &::before { + border: 1px solid $red-light; + border-radius: 6px; + color: $red-light; + content: '!'; + display: inline-block; + height: 12px; + line-height: 12px; + margin-right: .5rem; + text-align: center; + width: 12px; + } + } } diff --git a/gulpfile.js b/gulpfile.js index 1ea5bd33..92d40f2c 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -70,6 +70,7 @@ paths.css_vendor = [ paths.vendor + "intro.js/introjs.css" ]; paths.locales = paths.app + "locales/**/*.json"; +paths.modulesLocales = paths.app + "modules/**/locales/*.json"; paths.sass = [ paths.app + "**/*.scss", @@ -566,7 +567,7 @@ gulp.task("watch", function() { gulp.watch(paths.svg, ["copy-svg"]); gulp.watch(paths.coffee, ["app-watch"]); gulp.watch(paths.libs, ["jslibs-watch"]); - gulp.watch(paths.locales, ["locales"]); + gulp.watch([paths.locales, paths.modulesLocales], ["locales"]); gulp.watch(paths.images, ["copy-images"]); gulp.watch(paths.fonts, ["copy-fonts"]); }); diff --git a/test-utils.js b/test-utils.js index 4b33b6ad..751db5a0 100644 --- a/test-utils.js +++ b/test-utils.js @@ -30,4 +30,6 @@ var original = searchOriginal(this); original._rejectfn.apply(this, arguments); }; + + window.addDecorator = function() {}; }());