[Notice] Announcing the End of Demo Server [Read me]

Merge remote-tracking branch 'yobi/feature/organization' into feature/organization-on-master
@1300d451b7406f6115869479029704d6766c0a29
--- app/assets/stylesheets/less/_page.less
+++ app/assets/stylesheets/less/_page.less
... | ... | @@ -1817,7 +1817,7 @@ |
1817 | 1817 |
} |
1818 | 1818 |
} |
1819 | 1819 |
.cu-label { |
1820 |
- width: 80px; |
|
1820 |
+ width: 160px; |
|
1821 | 1821 |
padding-right: 45px; |
1822 | 1822 |
.inline-block; |
1823 | 1823 |
vertical-align: top; |
--- app/controllers/ImportApp.java
+++ app/controllers/ImportApp.java
... | ... | @@ -1,23 +1,32 @@ |
1 | 1 |
package controllers; |
2 | 2 |
|
3 |
+import models.Organization; |
|
4 |
+import models.OrganizationUser; |
|
3 | 5 |
import models.Project; |
4 | 6 |
import models.ProjectUser; |
7 |
+import models.User; |
|
5 | 8 |
import models.enumeration.RoleType; |
6 | 9 |
import play.data.Form; |
7 | 10 |
import play.db.ebean.Transactional; |
8 | 11 |
import play.mvc.Controller; |
9 | 12 |
import play.mvc.Result; |
13 |
+import play.mvc.With; |
|
10 | 14 |
import playRepository.GitRepository; |
11 | 15 |
import utils.AccessControl; |
12 |
-import utils.Constants; |
|
16 |
+import utils.ErrorViews; |
|
13 | 17 |
import utils.FileUtil; |
18 |
+import utils.ValidationResult; |
|
19 |
+import views.html.project.create; |
|
14 | 20 |
import views.html.project.importing; |
15 | 21 |
|
16 | 22 |
import org.apache.commons.lang3.StringUtils; |
17 | 23 |
import org.eclipse.jgit.api.errors.*; |
18 |
-import java.io.IOException; |
|
19 | 24 |
|
25 |
+import actions.AnonymousCheckAction; |
|
26 |
+ |
|
27 |
+import java.io.IOException; |
|
20 | 28 |
import java.io.File; |
29 |
+import java.util.List; |
|
21 | 30 |
|
22 | 31 |
import static play.data.Form.form; |
23 | 32 |
|
... | ... | @@ -28,13 +37,10 @@ |
28 | 37 |
* |
29 | 38 |
* @return |
30 | 39 |
*/ |
40 |
+ @With(AnonymousCheckAction.class) |
|
31 | 41 |
public static Result importForm() { |
32 |
- if (UserApp.currentUser().isAnonymous()) { |
|
33 |
- flash(Constants.WARNING, "user.login.alert"); |
|
34 |
- return redirect(routes.UserApp.loginForm()); |
|
35 |
- } else { |
|
36 |
- return ok(importing.render("title.newProject", form(Project.class))); |
|
37 |
- } |
|
42 |
+ List<OrganizationUser> orgUserList = OrganizationUser.findByAdmin(UserApp.currentUser().id); |
|
43 |
+ return ok(importing.render("title.newProject", form(Project.class), orgUserList)); |
|
38 | 44 |
} |
39 | 45 |
|
40 | 46 |
/** |
... | ... | @@ -48,34 +54,30 @@ |
48 | 54 |
if( !AccessControl.isGlobalResourceCreatable(UserApp.currentUser()) ){ |
49 | 55 |
return forbidden("'" + UserApp.currentUser().name + "' has no permission"); |
50 | 56 |
} |
51 |
- |
|
52 | 57 |
Form<Project> filledNewProjectForm = form(Project.class).bindFromRequest(); |
58 |
+ String owner = filledNewProjectForm.field("owner").value(); |
|
59 |
+ Organization organization = Organization.findByName(owner); |
|
60 |
+ User user = User.findByLoginId(owner); |
|
53 | 61 |
|
54 |
- String gitUrl = StringUtils.trim(filledNewProjectForm.data().get("url")); |
|
55 |
- if(StringUtils.isBlank(gitUrl)) { |
|
56 |
- flash(Constants.WARNING, "project.import.error.empty.url"); |
|
57 |
- return badRequest(importing.render("title.newProject", filledNewProjectForm)); |
|
62 |
+ ValidationResult result = validateForm(filledNewProjectForm, organization, user); |
|
63 |
+ if (result.hasError()) { |
|
64 |
+ return result.getResult(); |
|
58 | 65 |
} |
59 | 66 |
|
60 |
- if (Project.exists(UserApp.currentUser().loginId, filledNewProjectForm.field("name").value())) { |
|
61 |
- flash(Constants.WARNING, "project.name.duplicate"); |
|
62 |
- filledNewProjectForm.reject("name"); |
|
63 |
- return badRequest(importing.render("title.newProject", filledNewProjectForm)); |
|
64 |
- } |
|
65 |
- |
|
66 |
- if (filledNewProjectForm.hasErrors()) { |
|
67 |
- filledNewProjectForm.reject("name"); |
|
68 |
- flash(Constants.WARNING, "project.name.alert"); |
|
69 |
- return badRequest(importing.render("title.newProject", filledNewProjectForm)); |
|
70 |
- } |
|
71 |
- |
|
67 |
+ String gitUrl = filledNewProjectForm.data().get("url"); |
|
72 | 68 |
Project project = filledNewProjectForm.get(); |
73 |
- project.owner = UserApp.currentUser().loginId; |
|
69 |
+ |
|
70 |
+ if (Organization.isNameExist(owner)) { |
|
71 |
+ project.organization = organization; |
|
72 |
+ } |
|
74 | 73 |
String errorMessageKey = null; |
75 | 74 |
try { |
76 | 75 |
GitRepository.cloneRepository(gitUrl, project); |
77 | 76 |
Long projectId = Project.create(project); |
78 |
- ProjectUser.assignRole(UserApp.currentUser().id, projectId, RoleType.MANAGER); |
|
77 |
+ |
|
78 |
+ if (User.isLoginIdExist(owner)) { |
|
79 |
+ ProjectUser.assignRole(UserApp.currentUser().id, projectId, RoleType.MANAGER); |
|
80 |
+ } |
|
79 | 81 |
} catch (InvalidRemoteException e) { |
80 | 82 |
// It is not an url. |
81 | 83 |
errorMessageKey = "project.import.error.wrong.url"; |
... | ... | @@ -87,12 +89,62 @@ |
87 | 89 |
} |
88 | 90 |
|
89 | 91 |
if (errorMessageKey != null) { |
90 |
- flash(Constants.WARNING, errorMessageKey); |
|
92 |
+ List<OrganizationUser> orgUserList = OrganizationUser.findByAdmin(UserApp.currentUser().id); |
|
93 |
+ filledNewProjectForm.reject("url", errorMessageKey); |
|
91 | 94 |
FileUtil.rm_rf(new File(GitRepository.getGitDirectory(project))); |
92 |
- return badRequest(importing.render("title.newProject", filledNewProjectForm)); |
|
95 |
+ return badRequest(importing.render("title.newProject", filledNewProjectForm, orgUserList)); |
|
93 | 96 |
} else { |
94 | 97 |
return redirect(routes.ProjectApp.project(project.owner, project.name)); |
95 | 98 |
} |
96 | 99 |
} |
97 | 100 |
|
101 |
+ private static ValidationResult validateForm(Form<Project> newProjectForm, Organization organization, User user) { |
|
102 |
+ boolean hasError = false; |
|
103 |
+ Result result = null; |
|
104 |
+ |
|
105 |
+ List<OrganizationUser> orgUserList = OrganizationUser.findByAdmin(UserApp.currentUser().id); |
|
106 |
+ |
|
107 |
+ String owner = newProjectForm.field("owner").value(); |
|
108 |
+ String name = newProjectForm.field("name").value(); |
|
109 |
+ boolean ownerIsUser = User.isLoginIdExist(owner); |
|
110 |
+ boolean ownerIsOrganization = Organization.isNameExist(owner); |
|
111 |
+ |
|
112 |
+ if (!ownerIsUser && !ownerIsOrganization) { |
|
113 |
+ newProjectForm.reject("owner", "project.owner.invalidate"); |
|
114 |
+ hasError = true; |
|
115 |
+ result = badRequest(create.render("title.newProject", newProjectForm, orgUserList)); |
|
116 |
+ } |
|
117 |
+ |
|
118 |
+ if (ownerIsUser && UserApp.currentUser().id != user.id) { |
|
119 |
+ newProjectForm.reject("owner", "project.owner.invalidate"); |
|
120 |
+ hasError = true; |
|
121 |
+ result = badRequest(create.render("title.newProject", newProjectForm, orgUserList)); |
|
122 |
+ } |
|
123 |
+ |
|
124 |
+ if (ownerIsOrganization && !OrganizationUser.isAdmin(organization.id, UserApp.currentUser().id)) { |
|
125 |
+ hasError = true; |
|
126 |
+ result = forbidden(ErrorViews.Forbidden.render("'" + UserApp.currentUser().name + "' has no permission")); |
|
127 |
+ } |
|
128 |
+ |
|
129 |
+ if (Project.exists(owner, name)) { |
|
130 |
+ newProjectForm.reject("name", "project.name.duplicate"); |
|
131 |
+ hasError = true; |
|
132 |
+ result = badRequest(importing.render("title.newProject", newProjectForm, orgUserList)); |
|
133 |
+ } |
|
134 |
+ |
|
135 |
+ String gitUrl = StringUtils.trim(newProjectForm.data().get("url")); |
|
136 |
+ if (StringUtils.isBlank(gitUrl)) { |
|
137 |
+ newProjectForm.reject("url", "project.import.error.empty.url"); |
|
138 |
+ hasError = true; |
|
139 |
+ result = badRequest(importing.render("title.newProject", newProjectForm, orgUserList)); |
|
140 |
+ } |
|
141 |
+ |
|
142 |
+ if (newProjectForm.hasErrors()) { |
|
143 |
+ newProjectForm.reject("name", "project.name.alert"); |
|
144 |
+ hasError = true; |
|
145 |
+ result = badRequest(importing.render("title.newProject", newProjectForm, orgUserList)); |
|
146 |
+ } |
|
147 |
+ |
|
148 |
+ return new ValidationResult(result, hasError); |
|
149 |
+ } |
|
98 | 150 |
} |
+++ app/controllers/OrganizationApp.java
... | ... | @@ -0,0 +1,392 @@ |
1 | +/** | |
2 | + * Yobi, Project Hosting SW | |
3 | + * | |
4 | + * Copyright 2013 NAVER Corp. | |
5 | + * http://yobi.io | |
6 | + * | |
7 | + * @Author Keesun Baik | |
8 | + * | |
9 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
10 | + * you may not use this file except in compliance with the License. | |
11 | + * You may obtain a copy of the License at | |
12 | + * | |
13 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
14 | + * | |
15 | + * Unless required by applicable law or agreed to in writing, software | |
16 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
17 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
18 | + * See the License for the specific language governing permissions and | |
19 | + * limitations under the License. | |
20 | + */ | |
21 | +package controllers; | |
22 | + | |
23 | +import actions.AnonymousCheckAction; | |
24 | +import models.Organization; | |
25 | +import models.User; | |
26 | +import models.enumeration.Operation; | |
27 | +import org.codehaus.jackson.node.ObjectNode; | |
28 | +import play.data.Form; | |
29 | +import play.data.validation.Validation; | |
30 | +import play.db.ebean.Transactional; | |
31 | +import play.libs.Json; | |
32 | +import play.mvc.Controller; | |
33 | +import play.mvc.Http; | |
34 | +import play.mvc.Result; | |
35 | +import play.mvc.With; | |
36 | +import utils.AccessControl; | |
37 | +import utils.Constants; | |
38 | +import utils.ErrorViews; | |
39 | +import models.*; | |
40 | +import models.enumeration.RoleType; | |
41 | +import views.html.organization.create; | |
42 | +import views.html.organization.view; | |
43 | +import views.html.organization.setting; | |
44 | + | |
45 | +import javax.validation.ConstraintViolation; | |
46 | +import java.io.IOException; | |
47 | +import java.security.NoSuchAlgorithmException; | |
48 | +import java.util.Date; | |
49 | +import java.util.Set; | |
50 | + | |
51 | +import static play.data.Form.form; | |
52 | +import static utils.LogoUtil.*; | |
53 | + | |
54 | +/** | |
55 | + * @author Keeun Baik | |
56 | + */ | |
57 | +public class OrganizationApp extends Controller { | |
58 | + | |
59 | + @With(AnonymousCheckAction.class) | |
60 | + public static Result newForm() { | |
61 | + return ok(create.render("title.newOrganization", new Form<>(Organization.class))); | |
62 | + } | |
63 | + | |
64 | + @With(AnonymousCheckAction.class) | |
65 | + public static Result newOrganization() throws Exception { | |
66 | + Form<Organization> newOrgForm = form(Organization.class).bindFromRequest(); | |
67 | + validate(newOrgForm); | |
68 | + if (newOrgForm.hasErrors()) { | |
69 | + flash(Constants.WARNING, newOrgForm.error("name").message()); | |
70 | + return badRequest(create.render("title.newOrganization", newOrgForm)); | |
71 | + } else { | |
72 | + Organization org = newOrgForm.get(); | |
73 | + org.created = new Date(); | |
74 | + org.save(); | |
75 | + | |
76 | + UserApp.currentUser().createOrganization(org); | |
77 | + return redirect(routes.OrganizationApp.organization(org.name)); | |
78 | + } | |
79 | + } | |
80 | + | |
81 | + public static Result organization(String name) { | |
82 | + Organization org = Organization.findByName(name); | |
83 | + if(org == null) { | |
84 | + return notFound(ErrorViews.NotFound.render("error.notfound.organization")); | |
85 | + } | |
86 | + return ok(view.render(org)); | |
87 | + } | |
88 | + | |
89 | + private static void validate(Form<Organization> newOrgForm) { | |
90 | + // 조직 이름 패턴을 검사한다. | |
91 | + Set<ConstraintViolation<Organization>> results = Validation.getValidator().validate(newOrgForm.get()); | |
92 | + if(!results.isEmpty()) { | |
93 | + newOrgForm.reject("name", "organization.name.alert"); | |
94 | + } | |
95 | + | |
96 | + String name = newOrgForm.field("name").value(); | |
97 | + // 중복된 loginId로 가입할 수 없다. | |
98 | + if (User.isLoginIdExist(name)) { | |
99 | + newOrgForm.reject("name", "organization.name.duplicate"); | |
100 | + } | |
101 | + | |
102 | + // 같은 이름의 조직을 만들 수 없다. | |
103 | + if (Organization.isNameExist(name)) { | |
104 | + newOrgForm.reject("name", "organization.name.duplicate"); | |
105 | + } | |
106 | + } | |
107 | + | |
108 | + /** | |
109 | + * 그룹에 멤버를 추가한다. | |
110 | + * | |
111 | + * @param organizationName | |
112 | + * @return | |
113 | + */ | |
114 | + @Transactional | |
115 | + public static Result addMember(String organizationName) { | |
116 | + Form<User> addMemberForm = form(User.class).bindFromRequest(); | |
117 | + Result result = validateForAddMember(addMemberForm, organizationName); | |
118 | + if (result != null) { | |
119 | + return result; | |
120 | + } | |
121 | + | |
122 | + User user = User.findByLoginId(addMemberForm.get().loginId); | |
123 | + Organization organization = Organization.findByOrganizationName(organizationName); | |
124 | + OrganizationUser.assignRole(user.id, organization.id, RoleType.ORG_MEMBER.roleType()); | |
125 | + | |
126 | + return redirect(routes.OrganizationApp.members(organizationName)); | |
127 | + } | |
128 | + | |
129 | + /** | |
130 | + * {@link #addMember(String)}를 위해 사용되는 변수의 유효성 검사를 한다. | |
131 | + * | |
132 | + * @param addMemberForm | |
133 | + * @param organizationName | |
134 | + * @return | |
135 | + */ | |
136 | + private static Result validateForAddMember(Form<User> addMemberForm, String organizationName) { | |
137 | + String userLoginId = addMemberForm.get().loginId; | |
138 | + User userToBeAdded = User.findByLoginId(userLoginId); | |
139 | + | |
140 | + if (addMemberForm.hasErrors() || userToBeAdded.isAnonymous()) { | |
141 | + flash(Constants.WARNING, "organization.member.unknownUser"); | |
142 | + return redirect(routes.OrganizationApp.members(organizationName)); | |
143 | + } | |
144 | + | |
145 | + Organization organization = Organization.findByOrganizationName(organizationName); | |
146 | + if (organization == null) { | |
147 | + flash(Constants.WARNING, "organization.member.unknownOrganization"); | |
148 | + return redirect(routes.OrganizationApp.members(organizationName)); | |
149 | + } | |
150 | + | |
151 | + User currentUser = UserApp.currentUser(); | |
152 | + if (!OrganizationUser.isAdmin(organization.id, currentUser.id)) { | |
153 | + flash(Constants.WARNING, "organization.member.needManagerRole"); | |
154 | + return redirect(routes.OrganizationApp.members(organizationName)); | |
155 | + } | |
156 | + | |
157 | + if (OrganizationUser.exist(organization.id, userToBeAdded.id)) { | |
158 | + flash(Constants.WARNING, "organization.member.alreadyMember"); | |
159 | + return redirect(routes.OrganizationApp.members(organizationName)); | |
160 | + } | |
161 | + | |
162 | + return null; | |
163 | + } | |
164 | + | |
165 | + /** | |
166 | + * 그룹에서 멤버를 삭제한다. | |
167 | + * | |
168 | + * @param organizationName | |
169 | + * @param userId | |
170 | + * @return | |
171 | + */ | |
172 | + @Transactional | |
173 | + public static Result deleteMember(String organizationName, Long userId) { | |
174 | + Result result = validateForDeleteMember(organizationName, userId); | |
175 | + if (result != null) { | |
176 | + return result; | |
177 | + } | |
178 | + | |
179 | + Organization organization = Organization.findByOrganizationName(organizationName); | |
180 | + OrganizationUser.delete(organization.id, userId); | |
181 | + | |
182 | + if (UserApp.currentUser().id.equals(userId)) { | |
183 | + return okWithLocation(routes.OrganizationApp.organization(organizationName).url()); | |
184 | + } else { | |
185 | + return okWithLocation(routes.OrganizationApp.members(organizationName).url()); | |
186 | + } | |
187 | + } | |
188 | + | |
189 | + /** | |
190 | + * {@link #deleteMember(String, Long)}를 위해 사용되는 변수의 유효성 검사를 한다. | |
191 | + * | |
192 | + * @param organizationName | |
193 | + * @param userId | |
194 | + * @return | |
195 | + */ | |
196 | + private static Result validateForDeleteMember(String organizationName, Long userId) { | |
197 | + Organization organization = Organization.findByOrganizationName(organizationName); | |
198 | + if (organization == null) { | |
199 | + return notFound(ErrorViews.NotFound.render("organization.member.unknownOrganization", organization)); | |
200 | + } | |
201 | + | |
202 | + if (!OrganizationUser.exist(organization.id, userId)) { | |
203 | + flash(Constants.WARNING, "organization.member.isNotAMember"); | |
204 | + return okWithLocation(routes.OrganizationApp.members(organizationName).url()); | |
205 | + } | |
206 | + | |
207 | + User currentUser = UserApp.currentUser(); | |
208 | + if (!AccessControl.isAllowed(currentUser, organization.asResource(), Operation.UPDATE) | |
209 | + && !currentUser.id.equals(userId)) { | |
210 | + flash(Constants.WARNING, "organization.member.needManagerRole"); | |
211 | + return okWithLocation(routes.OrganizationApp.members(organizationName).url()); | |
212 | + } | |
213 | + | |
214 | + if (OrganizationUser.isAdmin(organization.id, userId) && organization.getAdmins().size() == 1) { | |
215 | + flash(Constants.WARNING, "organization.member.atLeastOneAdmin"); | |
216 | + return okWithLocation(routes.OrganizationApp.members(organizationName).url()); | |
217 | + } | |
218 | + | |
219 | + return null; | |
220 | + } | |
221 | + | |
222 | + /** | |
223 | + * 그룹 멤버의 권한을 수정한다. | |
224 | + * | |
225 | + * @param organizationName | |
226 | + * @param userId | |
227 | + * @return | |
228 | + */ | |
229 | + @Transactional | |
230 | + public static Result editMember(String organizationName, Long userId) { | |
231 | + Form<Role> roleForm = form(Role.class).bindFromRequest(); | |
232 | + Result result = validateForEditMember(roleForm, organizationName, userId); | |
233 | + if (result != null) { | |
234 | + return result; | |
235 | + } | |
236 | + | |
237 | + Organization organization = Organization.findByOrganizationName(organizationName); | |
238 | + OrganizationUser.assignRole(userId, organization.id, roleForm.get().id); | |
239 | + | |
240 | + return status(Http.Status.NO_CONTENT); | |
241 | + } | |
242 | + | |
243 | + /** | |
244 | + * {@link #editMember(String, Long)}를 위해 사용되는 변수의 유효성 검사를 한다. | |
245 | + * | |
246 | + * @param roleForm | |
247 | + * @param organizationName | |
248 | + * @param userId | |
249 | + * @return | |
250 | + */ | |
251 | + private static Result validateForEditMember(Form<Role> roleForm, String organizationName, Long userId) { | |
252 | + if (roleForm.hasErrors()) { | |
253 | + flash(Constants.WARNING, "organization.member.unknownRole"); | |
254 | + return okWithLocation(routes.OrganizationApp.members(organizationName).url()); | |
255 | + } | |
256 | + | |
257 | + Organization organization = Organization.findByOrganizationName(organizationName); | |
258 | + if (organization == null) { | |
259 | + return notFound(ErrorViews.NotFound.render("organization.member.unknownOrganization", organization)); | |
260 | + } | |
261 | + | |
262 | + if (!OrganizationUser.exist(organization.id, userId)) { | |
263 | + flash(Constants.WARNING, "organization.member.isNotAMember"); | |
264 | + return okWithLocation(routes.OrganizationApp.members(organizationName).url()); | |
265 | + } | |
266 | + | |
267 | + User currentUser = UserApp.currentUser(); | |
268 | + if (!AccessControl.isAllowed(currentUser, organization.asResource(), Operation.UPDATE)) { | |
269 | + flash(Constants.WARNING, "organization.member.needManagerRole"); | |
270 | + return okWithLocation(routes.OrganizationApp.members(organizationName).url()); | |
271 | + } | |
272 | + if (OrganizationUser.isAdmin(organization.id, userId) && organization.getAdmins().size() == 1) { | |
273 | + flash(Constants.WARNING, "organization.member.atLeastOneAdmin"); | |
274 | + return okWithLocation(routes.OrganizationApp.members(organizationName).url()); | |
275 | + } | |
276 | + | |
277 | + return null; | |
278 | + } | |
279 | + | |
280 | + /** | |
281 | + * 그룹 페이지 안에있는 멤버 관리 페이지로 이동한다. | |
282 | + * | |
283 | + * @param organizationName | |
284 | + * @return | |
285 | + */ | |
286 | + public static Result members(String organizationName) { | |
287 | + Result result = validateForSetting(organizationName); | |
288 | + if (result != null) { | |
289 | + return result; | |
290 | + } | |
291 | + | |
292 | + Organization organization = Organization.findByOrganizationName(organizationName); | |
293 | + | |
294 | + return ok(views.html.organization.members.render(organization, Role.findOrganizationRoles())); | |
295 | + } | |
296 | + | |
297 | + /** | |
298 | + * 그룹 페이지 안에있는 그룹 관리 페이지로 이동한다. | |
299 | + * | |
300 | + * @param organizationName | |
301 | + * @return | |
302 | + */ | |
303 | + public static Result settingForm(String organizationName) { | |
304 | + Result result = validateForSetting(organizationName); | |
305 | + if (result != null) { | |
306 | + return result; | |
307 | + } | |
308 | + | |
309 | + Organization organization = Organization.findByOrganizationName(organizationName); | |
310 | + | |
311 | + return ok(views.html.organization.setting.render(organization)); | |
312 | + } | |
313 | + | |
314 | + /** | |
315 | + * {@link #members(String)}를 위해 사용되는 변수의 유효성 검사를 한다. | |
316 | + * | |
317 | + * @param organizationName | |
318 | + * @return | |
319 | + */ | |
320 | + private static Result validateForSetting(String organizationName) { | |
321 | + Organization organization = Organization.findByOrganizationName(organizationName); | |
322 | + if (organization == null) { | |
323 | + return notFound(ErrorViews.NotFound.render("organization.member.unknownOrganization", organization)); | |
324 | + } | |
325 | + | |
326 | + User currentUser = UserApp.currentUser(); | |
327 | + if (!OrganizationUser.isAdmin(organization.id, currentUser.id)) { | |
328 | + return forbidden(ErrorViews.Forbidden.render("error.forbidden", organization)); | |
329 | + } | |
330 | + | |
331 | + return null; | |
332 | + } | |
333 | + | |
334 | + /** | |
335 | + * {@code location}을 JSON 형태로 저장하여 ok와 함께 리턴한다. | |
336 | + * | |
337 | + * Ajax 요청에 대해 redirect를 리턴하면 정상 작동하지 않음으로 ok에 redirect loation을 포함하여 리턴한다. | |
338 | + * 클라이언트에서 {@code location}을 확인하여 redirect 시킨다. | |
339 | + * | |
340 | + * @param location | |
341 | + * @return | |
342 | + */ | |
343 | + private static Result okWithLocation(String location) { | |
344 | + ObjectNode result = Json.newObject(); | |
345 | + result.put("location", location); | |
346 | + | |
347 | + return ok(result); | |
348 | + } | |
349 | + | |
350 | + private static Result validateForupdateOrganizationInfo(String organizationName) { | |
351 | + Result result = validateForSetting(organizationName); | |
352 | + | |
353 | + if (result == null) { | |
354 | + Form<Organization> organizationForm = form(Organization.class).bindFromRequest(); | |
355 | + if (organizationForm.hasErrors()) { | |
356 | + Organization organization = Organization.findByOrganizationName(organizationName); | |
357 | + return badRequest(setting.render(organization)); | |
358 | + } | |
359 | + } | |
360 | + | |
361 | + return result; | |
362 | + } | |
363 | + | |
364 | + public static Result updateOrganizationInfo(String organizationName) throws IOException, NoSuchAlgorithmException { | |
365 | + Result result = validateForupdateOrganizationInfo(organizationName); | |
366 | + if (result != null) { | |
367 | + return result; | |
368 | + } | |
369 | + | |
370 | + Form<Organization> organizationForm = form(Organization.class).bindFromRequest(); | |
371 | + Organization organization = organizationForm.get(); | |
372 | + Http.MultipartFormData body = request().body().asMultipartFormData(); | |
373 | + Http.MultipartFormData.FilePart filePart = body.getFile("logoPath"); | |
374 | + | |
375 | + if (!isEmptyFilePart(filePart)) { | |
376 | + if(!isImageFile(filePart.getFilename())) { | |
377 | + flash(Constants.WARNING, "project.logo.alert"); | |
378 | + organizationForm.reject("logoPath"); | |
379 | + } else if (filePart.getFile().length() > LOGO_FILE_LIMIT_SIZE) { | |
380 | + flash(Constants.WARNING, "project.logo.fileSizeAlert"); | |
381 | + organizationForm.reject("logoPath"); | |
382 | + } else { | |
383 | + Attachment.deleteAll(organization.asResource()); | |
384 | + new Attachment().store(filePart.getFile(), filePart.getFilename(), organization.asResource()); | |
385 | + } | |
386 | + } | |
387 | + | |
388 | + organization.update(); | |
389 | + | |
390 | + return redirect(routes.OrganizationApp.settingForm(organizationName)); | |
391 | + } | |
392 | +} |
--- app/controllers/ProjectApp.java
+++ app/controllers/ProjectApp.java
... | ... | @@ -8,6 +8,7 @@ |
8 | 8 |
import com.avaje.ebean.Page; |
9 | 9 |
|
10 | 10 |
import controllers.annotation.IsAllowed; |
11 |
+import info.schleichardt.play2.mailplugin.Mailer; |
|
11 | 12 |
import models.*; |
12 | 13 |
import models.Project.State; |
13 | 14 |
import models.enumeration.Operation; |
... | ... | @@ -16,16 +17,21 @@ |
16 | 17 |
import models.enumeration.RoleType; |
17 | 18 |
|
18 | 19 |
import org.apache.commons.collections.CollectionUtils; |
20 |
+import org.apache.commons.lang.exception.ExceptionUtils; |
|
19 | 21 |
import org.apache.commons.lang3.StringUtils; |
22 |
+import org.apache.commons.mail.HtmlEmail; |
|
20 | 23 |
import org.codehaus.jackson.JsonNode; |
21 | 24 |
import org.codehaus.jackson.node.ObjectNode; |
22 | 25 |
import org.eclipse.jgit.api.errors.GitAPIException; |
23 | 26 |
import org.eclipse.jgit.api.errors.NoHeadException; |
27 |
+import org.jsoup.Jsoup; |
|
24 | 28 |
import org.tmatesoft.svn.core.SVNException; |
25 | 29 |
|
30 |
+import play.Logger; |
|
26 | 31 |
import play.data.Form; |
27 | 32 |
import play.data.validation.ValidationError; |
28 | 33 |
import play.db.ebean.Transactional; |
34 |
+import play.i18n.Messages; |
|
29 | 35 |
import play.libs.Json; |
30 | 36 |
import play.mvc.Controller; |
31 | 37 |
import play.mvc.Http; |
... | ... | @@ -43,6 +49,7 @@ |
43 | 49 |
import views.html.project.delete; |
44 | 50 |
import views.html.project.home; |
45 | 51 |
import views.html.project.setting; |
52 |
+import views.html.project.transfer; |
|
46 | 53 |
|
47 | 54 |
import javax.servlet.ServletException; |
48 | 55 |
|
... | ... | @@ -52,6 +59,7 @@ |
52 | 59 |
|
53 | 60 |
import static play.data.Form.form; |
54 | 61 |
import static play.libs.Json.toJson; |
62 |
+import static utils.LogoUtil.*; |
|
55 | 63 |
|
56 | 64 |
|
57 | 65 |
/** |
... | ... | @@ -61,11 +69,6 @@ |
61 | 69 |
public class ProjectApp extends Controller { |
62 | 70 |
|
63 | 71 |
private static final int ISSUE_MENTION_SHOW_LIMIT = 1000; |
64 |
- |
|
65 |
- private static final int LOGO_FILE_LIMIT_SIZE = 1024*1000*5; //5M |
|
66 |
- |
|
67 |
- /** 프로젝트 로고로 사용할 수 있는 이미지 확장자 */ |
|
68 |
- public static final String[] LOGO_TYPE = {"jpg", "jpeg", "png", "gif", "bmp"}; |
|
69 | 72 |
|
70 | 73 |
/** 자동완성에서 보여줄 최대 프로젝트 개수 */ |
71 | 74 |
private static final int MAX_FETCH_PROJECTS = 1000; |
... | ... | @@ -85,8 +88,6 @@ |
85 | 88 |
private static final String HTML = "text/html"; |
86 | 89 |
|
87 | 90 |
private static final String JSON = "application/json"; |
88 |
- |
|
89 |
- |
|
90 | 91 |
|
91 | 92 |
/** |
92 | 93 |
* getProject |
... | ... | @@ -202,7 +203,8 @@ |
202 | 203 |
flash(Constants.WARNING, "user.login.alert"); |
203 | 204 |
return redirect(routes.UserApp.loginForm()); |
204 | 205 |
} else { |
205 |
- return ok(create.render("title.newProject", form(Project.class))); |
|
206 |
+ List<OrganizationUser> orgUserList = OrganizationUser.findByAdmin(UserApp.currentUser().id); |
|
207 |
+ return ok(create.render("title.newProject", form(Project.class), orgUserList)); |
|
206 | 208 |
} |
207 | 209 |
} |
208 | 210 |
|
... | ... | @@ -241,25 +243,65 @@ |
241 | 243 |
} |
242 | 244 |
Form<Project> filledNewProjectForm = form(Project.class).bindFromRequest(); |
243 | 245 |
|
244 |
- if (Project.exists(UserApp.currentUser().loginId, filledNewProjectForm.field("name").value())) { |
|
245 |
- flash(Constants.WARNING, "project.name.duplicate"); |
|
246 |
- filledNewProjectForm.reject("name"); |
|
247 |
- return badRequest(create.render("title.newProject", filledNewProjectForm)); |
|
248 |
- } else if (filledNewProjectForm.hasErrors()) { |
|
249 |
- ValidationError error = filledNewProjectForm.error("name"); |
|
250 |
- flash(Constants.WARNING, RestrictedValidator.message.equals(error.message()) ? |
|
251 |
- "project.name.reserved.alert" : "project.name.alert"); |
|
252 |
- filledNewProjectForm.reject("name"); |
|
253 |
- return badRequest(create.render("title.newProject", filledNewProjectForm)); |
|
254 |
- } else { |
|
255 |
- Project project = filledNewProjectForm.get(); |
|
256 |
- project.owner = UserApp.currentUser().loginId; |
|
257 |
- ProjectUser.assignRole(UserApp.currentUser().id, Project.create(project), RoleType.MANAGER); |
|
246 |
+ String owner = filledNewProjectForm.field("owner").value(); |
|
247 |
+ Organization organization = Organization.findByName(owner); |
|
248 |
+ User user = User.findByLoginId(owner); |
|
258 | 249 |
|
259 |
- RepositoryService.createRepository(project); |
|
260 |
- |
|
261 |
- return redirect(routes.ProjectApp.project(project.owner, project.name)); |
|
250 |
+ ValidationResult validation = validateForm(filledNewProjectForm, organization, user); |
|
251 |
+ if (validation.hasError()) { |
|
252 |
+ return validation.getResult(); |
|
262 | 253 |
} |
254 |
+ |
|
255 |
+ Project project = filledNewProjectForm.get(); |
|
256 |
+ if (Organization.isNameExist(owner)) { |
|
257 |
+ project.organization = organization; |
|
258 |
+ } |
|
259 |
+ ProjectUser.assignRole(UserApp.currentUser().id, Project.create(project), RoleType.MANAGER); |
|
260 |
+ RepositoryService.createRepository(project); |
|
261 |
+ return redirect(routes.ProjectApp.project(project.owner, project.name)); |
|
262 |
+ } |
|
263 |
+ |
|
264 |
+ private static ValidationResult validateForm(Form<Project> newProjectForm, Organization organization, User user) { |
|
265 |
+ Result result = null; |
|
266 |
+ boolean hasError = false; |
|
267 |
+ List<OrganizationUser> orgUserList = OrganizationUser.findByAdmin(UserApp.currentUser().id); |
|
268 |
+ |
|
269 |
+ String owner = newProjectForm.field("owner").value(); |
|
270 |
+ String name = newProjectForm.field("name").value(); |
|
271 |
+ boolean ownerIsUser = User.isLoginIdExist(owner); |
|
272 |
+ boolean ownerIsOrganization = Organization.isNameExist(owner); |
|
273 |
+ |
|
274 |
+ if (!ownerIsUser && !ownerIsOrganization) { |
|
275 |
+ newProjectForm.reject("owner", "project.owner.invalidate"); |
|
276 |
+ hasError = true; |
|
277 |
+ result = badRequest(create.render("title.newProject", newProjectForm, orgUserList)); |
|
278 |
+ } |
|
279 |
+ |
|
280 |
+ if (ownerIsUser && UserApp.currentUser().id != user.id) { |
|
281 |
+ newProjectForm.reject("owner", "project.owner.invalidate"); |
|
282 |
+ hasError = true; |
|
283 |
+ result = badRequest(create.render("title.newProject", newProjectForm, orgUserList)); |
|
284 |
+ } |
|
285 |
+ |
|
286 |
+ if (ownerIsOrganization && !OrganizationUser.isAdmin(organization.id, UserApp.currentUser().id)) { |
|
287 |
+ hasError = true; |
|
288 |
+ result = forbidden(ErrorViews.Forbidden.render("'" + UserApp.currentUser().name + "' has no permission")); |
|
289 |
+ } |
|
290 |
+ |
|
291 |
+ if (Project.exists(owner, name)) { |
|
292 |
+ newProjectForm.reject("name", "project.name.duplicate"); |
|
293 |
+ hasError = true; |
|
294 |
+ result = badRequest(create.render("title.newProject", newProjectForm, orgUserList)); |
|
295 |
+ } |
|
296 |
+ |
|
297 |
+ if (newProjectForm.hasErrors()) { |
|
298 |
+ ValidationError error = newProjectForm.error("name"); |
|
299 |
+ newProjectForm.reject("name", RestrictedValidator.message.equals(error.message()) ? |
|
300 |
+ "project.name.reserved.alert" : "project.name.alert"); |
|
301 |
+ hasError = true; |
|
302 |
+ result = badRequest(create.render("title.newProject", newProjectForm, orgUserList)); |
|
303 |
+ } |
|
304 |
+ return new ValidationResult(result, hasError); |
|
263 | 305 |
} |
264 | 306 |
|
265 | 307 |
/** |
... | ... | @@ -330,37 +372,14 @@ |
330 | 372 |
repository.setDefaultBranch(defaultBranch); |
331 | 373 |
} |
332 | 374 |
|
333 |
- if (!repository.renameTo(updatedProject.name)) { |
|
334 |
- throw new FileOperationException("fail repository rename to " + project.owner + "/" + updatedProject.name); |
|
375 |
+ if (!project.name.equals(updatedProject.name)) { |
|
376 |
+ if (!repository.renameTo(updatedProject.name)) { |
|
377 |
+ throw new FileOperationException("fail repository rename to " + project.owner + "/" + updatedProject.name); |
|
378 |
+ } |
|
335 | 379 |
} |
336 |
- |
|
380 |
+ |
|
337 | 381 |
updatedProject.update(); |
338 | 382 |
return redirect(routes.ProjectApp.settingForm(loginId, updatedProject.name)); |
339 |
- } |
|
340 |
- |
|
341 |
- /** |
|
342 |
- * {@code filePart} 정보가 비어있는지 확인한다.<p /> |
|
343 |
- * @param filePart |
|
344 |
- * @return {@code filePart}가 null이면 true, {@code filename}이 null이면 true, {@code fileLength}가 0 이하이면 true |
|
345 |
- */ |
|
346 |
- private static boolean isEmptyFilePart(FilePart filePart) { |
|
347 |
- return filePart == null || filePart.getFilename() == null || filePart.getFilename().length() <= 0; |
|
348 |
- } |
|
349 |
- |
|
350 |
- /** |
|
351 |
- * {@code filename}의 확장자를 체크하여 이미지인지 확인한다.<p /> |
|
352 |
- * |
|
353 |
- * 이미지 확장자는 {@link controllers.ProjectApp#LOGO_TYPE} 에 정의한다. |
|
354 |
- * @param filename the filename |
|
355 |
- * @return true, if is image file |
|
356 |
- */ |
|
357 |
- public static boolean isImageFile(String filename) { |
|
358 |
- boolean isImageFile = false; |
|
359 |
- for(String suffix : LOGO_TYPE) { |
|
360 |
- if(filename.toLowerCase().endsWith(suffix)) |
|
361 |
- isImageFile = true; |
|
362 |
- } |
|
363 |
- return isImageFile; |
|
364 | 383 |
} |
365 | 384 |
|
366 | 385 |
/** |
... | ... | @@ -602,6 +621,141 @@ |
602 | 621 |
Collections.reverse(userList); |
603 | 622 |
} |
604 | 623 |
|
624 |
+ @IsAllowed(Operation.DELETE) |
|
625 |
+ public static Result transferForm(String loginId, String projectName) { |
|
626 |
+ Project project = Project.findByOwnerAndProjectName(loginId, projectName); |
|
627 |
+ Form<Project> projectForm = form(Project.class).fill(project); |
|
628 |
+ return ok(transfer.render("title.projectTransfer", projectForm, project)); |
|
629 |
+ } |
|
630 |
+ |
|
631 |
+ @Transactional |
|
632 |
+ @IsAllowed(Operation.DELETE) |
|
633 |
+ public static Result transferProject(String loginId, String projectName) throws Exception { |
|
634 |
+ Project project = Project.findByOwnerAndProjectName(loginId, projectName); |
|
635 |
+ String destination = request().getQueryString("owner"); |
|
636 |
+ |
|
637 |
+ User destOwner = User.findByLoginId(destination); |
|
638 |
+ Organization destOrg = Organization.findByName(destination); |
|
639 |
+ if(destOwner.isAnonymous() && destOrg == null) { |
|
640 |
+ return badRequest(ErrorViews.BadRequest.render()); |
|
641 |
+ } |
|
642 |
+ |
|
643 |
+ ProjectTransfer pt = null; |
|
644 |
+ // make a request to move to an user |
|
645 |
+ if(!destOwner.isAnonymous()) { |
|
646 |
+ pt = ProjectTransfer.requestNewTransfer(project, UserApp.currentUser(), destOwner.loginId); |
|
647 |
+ } |
|
648 |
+ // make a request to move to an group |
|
649 |
+ if(destOrg != null) { |
|
650 |
+ pt = ProjectTransfer.requestNewTransfer(project, UserApp.currentUser(), destOrg.name); |
|
651 |
+ } |
|
652 |
+ sendTransferRequestMail(pt); |
|
653 |
+ |
|
654 |
+ // if the request is sent by XHR, response with 204 204 No Content and Location header. |
|
655 |
+ String url = routes.ProjectApp.project(loginId, projectName).url(); |
|
656 |
+ if(HttpUtil.isRequestedWithXHR(request())){ |
|
657 |
+ response().setHeader("Location", url); |
|
658 |
+ return status(204); |
|
659 |
+ } |
|
660 |
+ |
|
661 |
+ return redirect(url); |
|
662 |
+ } |
|
663 |
+ |
|
664 |
+ @Transactional |
|
665 |
+ @With(AnonymousCheckAction.class) |
|
666 |
+ public static synchronized Result acceptTransfer(Long id, String confirmKey) throws IOException, ServletException { |
|
667 |
+ ProjectTransfer pt = ProjectTransfer.findValidOne(id); |
|
668 |
+ if(pt == null) { |
|
669 |
+ return notFound(ErrorViews.NotFound.render()); |
|
670 |
+ } |
|
671 |
+ if(confirmKey == null || !pt.confirmKey.equals(confirmKey)) { |
|
672 |
+ return badRequest(ErrorViews.BadRequest.render()); |
|
673 |
+ } |
|
674 |
+ |
|
675 |
+ if(!AccessControl.isAllowed(UserApp.currentUser(), pt.asResource(), Operation.ACCEPT)) { |
|
676 |
+ return forbidden(ErrorViews.Forbidden.render()); |
|
677 |
+ } |
|
678 |
+ |
|
679 |
+ Project project = pt.project; |
|
680 |
+ |
|
681 |
+ // Change the project's name and move the repository. |
|
682 |
+ String newProjectName = Project.newProjectName(pt.destination, project.name); |
|
683 |
+ PlayRepository repository = RepositoryService.getRepository(project); |
|
684 |
+ repository.move(pt.sender.loginId, project.name, pt.destination, newProjectName); |
|
685 |
+ |
|
686 |
+ User newOwnerUser = User.findByLoginId(pt.destination); |
|
687 |
+ Organization newOwnerOrg = Organization.findByName(pt.destination); |
|
688 |
+ |
|
689 |
+ // Change the project's information. |
|
690 |
+ project.owner = pt.destination; |
|
691 |
+ project.name = newProjectName; |
|
692 |
+ if(newOwnerOrg != null) { |
|
693 |
+ project.organization = newOwnerOrg; |
|
694 |
+ } |
|
695 |
+ project.update(); |
|
696 |
+ |
|
697 |
+ // Change roles. |
|
698 |
+ if(!newOwnerUser.isAnonymous()) { |
|
699 |
+ ProjectUser.assignRole(newOwnerUser.id, project.id, RoleType.MANAGER); |
|
700 |
+ } |
|
701 |
+ if(ProjectUser.isManager(pt.sender.id, project.id)) { |
|
702 |
+ ProjectUser.assignRole(pt.sender.id, project.id, RoleType.MEMBER); |
|
703 |
+ } |
|
704 |
+ |
|
705 |
+ // Change the tranfer's status to be accepted. |
|
706 |
+ pt.newProjectName = newProjectName; |
|
707 |
+ pt.accepted = true; |
|
708 |
+ pt.update(); |
|
709 |
+ |
|
710 |
+ // If the opposite request is exists, delete it. |
|
711 |
+ ProjectTransfer.deleteExisting(project, pt.sender, pt.destination); |
|
712 |
+ |
|
713 |
+ return redirect(routes.ProjectApp.project(project.owner, project.name)); |
|
714 |
+ } |
|
715 |
+ |
|
716 |
+ private static void sendTransferRequestMail(ProjectTransfer pt) { |
|
717 |
+ HtmlEmail email = new HtmlEmail(); |
|
718 |
+ try { |
|
719 |
+ String acceptUrl = pt.getAcceptUrl(); |
|
720 |
+ String message = Messages.get("transfer.message.hello", pt.destination) + "\n\n" |
|
721 |
+ + Messages.get("transfer.message.detail", pt.project.name, pt.newProjectName, pt.project.owner, pt.destination) + "\n" |
|
722 |
+ + Messages.get("transfer.message.link") + "\n\n" |
|
723 |
+ + acceptUrl + "\n\n" |
|
724 |
+ + Messages.get("transfer.message.deadline") + "\n\n" |
|
725 |
+ + Messages.get("transfer.message.thank"); |
|
726 |
+ |
|
727 |
+ email.setFrom(Config.getEmailFromSmtp(), pt.sender.name); |
|
728 |
+ email.addTo(Config.getEmailFromSmtp(), "Yobi"); |
|
729 |
+ |
|
730 |
+ User to = User.findByLoginId(pt.destination); |
|
731 |
+ if(!to.isAnonymous()) { |
|
732 |
+ email.addBcc(to.email, to.name); |
|
733 |
+ } |
|
734 |
+ |
|
735 |
+ Organization org = Organization.findByName(pt.destination); |
|
736 |
+ if(org != null) { |
|
737 |
+ List<OrganizationUser> admins = OrganizationUser.findAdminsOf(org); |
|
738 |
+ for(OrganizationUser admin : admins) { |
|
739 |
+ email.addBcc(admin.user.email, admin.user.name); |
|
740 |
+ } |
|
741 |
+ } |
|
742 |
+ |
|
743 |
+ email.setSubject(String.format("[%s] @%s wants to transfer project", pt.project.name, pt.sender.loginId)); |
|
744 |
+ email.setHtmlMsg(Markdown.render(message)); |
|
745 |
+ email.setTextMsg(message); |
|
746 |
+ email.setCharset("utf-8"); |
|
747 |
+ email.addHeader("References", "<" + acceptUrl + "@" + Config.getHostname() + ">"); |
|
748 |
+ email.setSentDate(pt.requested); |
|
749 |
+ Mailer.send(email); |
|
750 |
+ String escapedTitle = email.getSubject().replace("\"", "\\\""); |
|
751 |
+ String logEntry = String.format("\"%s\" %s", escapedTitle, email.getBccAddresses()); |
|
752 |
+ play.Logger.of("mail").info(logEntry); |
|
753 |
+ } catch (Exception e) { |
|
754 |
+ Logger.warn("Failed to send a notification: " |
|
755 |
+ + email + "\n" + ExceptionUtils.getStackTrace(e)); |
|
756 |
+ } |
|
757 |
+ } |
|
758 |
+ |
|
605 | 759 |
private static void addCodeCommenters(String commitId, Long projectId, List<User> userList) { |
606 | 760 |
Project project = Project.find.byId(projectId); |
607 | 761 |
|
--- app/controllers/UserApp.java
+++ app/controllers/UserApp.java
... | ... | @@ -432,7 +432,10 @@ |
432 | 432 |
} |
433 | 433 |
|
434 | 434 |
/** |
435 |
- * 사용자 정보 조회 |
|
435 |
+ * 사용자 또는 그룹 정보 조회 |
|
436 |
+ * |
|
437 |
+ * {@code loginId}에 해당하는 그룹이 있을 때는 그룹을 보여주고 해당하는 |
|
438 |
+ * 그룹이 없을 경우에는 {@code loginId}에 해당하는 사용자 페이지를 보여준다. |
|
436 | 439 |
* |
437 | 440 |
* when: 사용자 로그인 아이디나 아바타를 클릭할 때 사용한다. |
438 | 441 |
* |
... | ... | @@ -444,6 +447,11 @@ |
444 | 447 |
* @return |
445 | 448 |
*/ |
446 | 449 |
public static Result userInfo(String loginId, String groups, int daysAgo, String selected) { |
450 |
+ Organization org = Organization.findByName(loginId); |
|
451 |
+ if(org != null) { |
|
452 |
+ return redirect(routes.OrganizationApp.organization(org.name)); |
|
453 |
+ } |
|
454 |
+ |
|
447 | 455 |
if (daysAgo == UNDEFINED) { |
448 | 456 |
Cookie cookie = request().cookie(DAYS_AGO_COOKIE); |
449 | 457 |
if (cookie != null) { |
+++ app/models/Organization.java
... | ... | @@ -0,0 +1,135 @@ |
1 | +/** | |
2 | + * Yobi, Project Hosting SW | |
3 | + * | |
4 | + * Copyright 2013 NAVER Corp. | |
5 | + * http://yobi.io | |
6 | + * | |
7 | + * @Author Keesun Baik, Wansoon Park, ChangSung Kim | |
8 | + * | |
9 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
10 | + * you may not use this file except in compliance with the License. | |
11 | + * You may obtain a copy of the License at | |
12 | + * | |
13 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
14 | + * | |
15 | + * Unless required by applicable law or agreed to in writing, software | |
16 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
17 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
18 | + * See the License for the specific language governing permissions and | |
19 | + * limitations under the License. | |
20 | + */ | |
21 | +package models; | |
22 | + | |
23 | +import models.enumeration.ProjectScope; | |
24 | +import models.enumeration.ResourceType; | |
25 | +import models.resource.GlobalResource; | |
26 | +import models.resource.Resource; | |
27 | +import play.data.format.Formats; | |
28 | +import play.data.validation.Constraints; | |
29 | +import play.db.ebean.Model; | |
30 | +import utils.ReservedWordsValidator; | |
31 | + | |
32 | +import javax.persistence.*; | |
33 | +import java.util.*; | |
34 | + | |
35 | +@Entity | |
36 | +public class Organization extends Model { | |
37 | + | |
38 | + private static final long serialVersionUID = -1L; | |
39 | + | |
40 | + public static final Finder<Long, Organization> find = new Finder<>(Long.class, Organization.class); | |
41 | + | |
42 | + @Id | |
43 | + public Long id; | |
44 | + | |
45 | + @Constraints.Pattern(value = "^" + User.LOGIN_ID_PATTERN + "$", message = "user.wrongloginId.alert") | |
46 | + @Constraints.Required | |
47 | + @Constraints.ValidateWith(ReservedWordsValidator.class) | |
48 | + public String name; | |
49 | + | |
50 | + @Formats.DateTime(pattern = "yyyy-MM-dd") | |
51 | + public Date created; | |
52 | + | |
53 | + @OneToMany(mappedBy = "organization", cascade = CascadeType.ALL) | |
54 | + public List<Project> projects; | |
55 | + | |
56 | + @OneToMany(mappedBy = "organization", cascade = CascadeType.ALL) | |
57 | + public List<OrganizationUser> users; | |
58 | + | |
59 | + public String descr; | |
60 | + | |
61 | + public void add(OrganizationUser ou) { | |
62 | + this.users.add(ou); | |
63 | + } | |
64 | + | |
65 | + public static Organization findByName(String name) { | |
66 | + return find.where().eq("name", name).findUnique(); | |
67 | + } | |
68 | + | |
69 | + public static boolean isNameExist(String name) { | |
70 | + int findRowCount = find.where().ieq("name", name).findRowCount(); | |
71 | + return (findRowCount != 0); | |
72 | + } | |
73 | + | |
74 | + public List<Project> getVisiableProjects(User user) { | |
75 | + List<Project> result = new ArrayList<>(); | |
76 | + if(OrganizationUser.isAdmin(this.id, user.id)) { | |
77 | + // 모든 프로젝트 | |
78 | + result.addAll(this.projects); | |
79 | + } else if(OrganizationUser.isMember(this.id, user.id)) { | |
80 | + // private 프로젝트를 제외한 모든 프로젝트와 자신이 멤버로 속한 프로젝트 | |
81 | + for(Project p : this.projects) { | |
82 | + if(p.projectScope != ProjectScope.PRIVATE || ProjectUser.isMember(user.id, p.id)) { | |
83 | + result.add(p); | |
84 | + } | |
85 | + } | |
86 | + } else { | |
87 | + // public 프로젝트와 자신이 멤버로 속한 프로젝트 | |
88 | + for(Project p : this.projects) { | |
89 | + if(p.projectScope == ProjectScope.PUBLIC || ProjectUser.isMember(user.id, p.id)) { | |
90 | + result.add(p); | |
91 | + } | |
92 | + } | |
93 | + } | |
94 | + | |
95 | + // 정렬 | |
96 | + Collections.sort(result, new Comparator<Project>() { | |
97 | + @Override | |
98 | + public int compare(Project p1, Project p2) { | |
99 | + return p1.name.compareTo(p2.name); | |
100 | + } | |
101 | + }); | |
102 | + | |
103 | + return result; | |
104 | + } | |
105 | + | |
106 | + public static Organization findByOrganizationName(String organizationName) { | |
107 | + return find.where().ieq("name", organizationName).findUnique(); | |
108 | + } | |
109 | + | |
110 | + /** | |
111 | + * As resource. | |
112 | + * | |
113 | + * @return the resource | |
114 | + */ | |
115 | + public Resource asResource() { | |
116 | + return new GlobalResource() { | |
117 | + | |
118 | + @Override | |
119 | + public String getId() { | |
120 | + return id.toString(); | |
121 | + } | |
122 | + | |
123 | + @Override | |
124 | + public ResourceType getType() { | |
125 | + return ResourceType.ORGANIZATION; | |
126 | + } | |
127 | + | |
128 | + }; | |
129 | + } | |
130 | + | |
131 | + public List<OrganizationUser> getAdmins() { | |
132 | + return OrganizationUser.findAdminsOf(this); | |
133 | + } | |
134 | +} | |
135 | + |
+++ app/models/OrganizationUser.java
... | ... | @@ -0,0 +1,108 @@ |
1 | +package models; | |
2 | + | |
3 | +import java.util.List; | |
4 | + | |
5 | +import javax.persistence.Entity; | |
6 | +import javax.persistence.Id; | |
7 | +import javax.persistence.ManyToOne; | |
8 | + | |
9 | +import models.enumeration.RoleType; | |
10 | +import play.db.ebean.Model; | |
11 | + | |
12 | +/** | |
13 | + * @author Keeun Baik | |
14 | + */ | |
15 | +@Entity | |
16 | +public class OrganizationUser extends Model { | |
17 | + | |
18 | + private static final long serialVersionUID = -1L; | |
19 | + | |
20 | + public static final Finder<Long, OrganizationUser> find = new Finder<>(Long.class, OrganizationUser.class); | |
21 | + | |
22 | + @Id | |
23 | + public Long id; | |
24 | + | |
25 | + @ManyToOne | |
26 | + public User user; | |
27 | + | |
28 | + @ManyToOne | |
29 | + public Organization organization; | |
30 | + | |
31 | + @ManyToOne | |
32 | + public Role role; | |
33 | + | |
34 | + public static List<OrganizationUser> findAdminsOf(Organization organization) { | |
35 | + return find.where() | |
36 | + .eq("organization", organization) | |
37 | + .eq("role", Role.findByName("org_admin")) | |
38 | + .findList(); | |
39 | + } | |
40 | + public static List<OrganizationUser> findByAdmin(Long userId) { | |
41 | + return find.where().eq("role", Role.findByRoleType(RoleType.ORG_ADMIN)) | |
42 | + .eq("user.id", userId) | |
43 | + .findList(); | |
44 | + } | |
45 | + public static boolean isAdmin(Long organizationId, Long userId) { | |
46 | + return contains(organizationId, userId, RoleType.ORG_ADMIN); | |
47 | + } | |
48 | + | |
49 | + public static boolean isMember(Long organizationId, Long userId) { | |
50 | + return contains(organizationId, userId, RoleType.ORG_MEMBER); | |
51 | + } | |
52 | + | |
53 | + private static boolean contains(Long organizationId, Long userId, RoleType roleType) { | |
54 | + int rowCount = find.where().eq("organization.id", organizationId) | |
55 | + .eq("user.id", userId) | |
56 | + .eq("role.id", Role.findByRoleType(roleType).id) | |
57 | + .findRowCount(); | |
58 | + return rowCount > 0; | |
59 | + } | |
60 | + | |
61 | + public static void assignRole(Long userId, Long organizationId, Long roleId) { | |
62 | + OrganizationUser organizationUser = OrganizationUser.findByOrganizationIdAndUserId(organizationId, userId); | |
63 | + | |
64 | + if (organizationUser == null) { | |
65 | + OrganizationUser.create(userId, organizationId, roleId); | |
66 | + } else { | |
67 | + Role role = Role.findById(roleId); | |
68 | + | |
69 | + if (role != null) { | |
70 | + organizationUser.role = role; | |
71 | + organizationUser.update(); | |
72 | + } | |
73 | + } | |
74 | + } | |
75 | + | |
76 | + public static OrganizationUser findByOrganizationIdAndUserId(Long organizationId, Long userId) { | |
77 | + return find.where().eq("user.id", userId) | |
78 | + .eq("organization.id", organizationId) | |
79 | + .findUnique(); | |
80 | + } | |
81 | + | |
82 | + public static void create(Long userId, Long organizationId, Long roleId) { | |
83 | + OrganizationUser organizationUser = new OrganizationUser(); | |
84 | + organizationUser.user = User.find.byId(userId); | |
85 | + organizationUser.organization = Organization.find.byId(organizationId); | |
86 | + organizationUser.role = Role.findById(roleId); | |
87 | + organizationUser.save(); | |
88 | + } | |
89 | + | |
90 | + public static void delete(Long organizationId, Long userId) { | |
91 | + OrganizationUser organizationUser = OrganizationUser.findByOrganizationIdAndUserId(organizationId, userId); | |
92 | + | |
93 | + if (organizationUser != null) { | |
94 | + organizationUser.delete(); | |
95 | + } | |
96 | + } | |
97 | + | |
98 | + public static boolean exist(Long organizationId, Long userId) { | |
99 | + return findByOrganizationIdAndUserId(organizationId, userId) != null; | |
100 | + } | |
101 | + | |
102 | + public static List<OrganizationUser> findByUser(User user, int size) { | |
103 | + return find.where().eq("user", user) | |
104 | + .order().asc("organization.name") | |
105 | + .setMaxRows(size) | |
106 | + .findList(); | |
107 | + } | |
108 | +} |
--- app/models/Project.java
+++ app/models/Project.java
... | ... | @@ -3,7 +3,7 @@ |
3 | 3 |
import com.avaje.ebean.Ebean; |
4 | 4 |
import com.avaje.ebean.ExpressionList; |
5 | 5 |
import com.avaje.ebean.Page; |
6 |
-import controllers.routes; |
|
6 |
+import models.enumeration.ProjectScope; |
|
7 | 7 |
import models.enumeration.RequestState; |
8 | 8 |
import models.enumeration.ResourceType; |
9 | 9 |
import models.enumeration.RoleType; |
... | ... | @@ -119,6 +119,12 @@ |
119 | 119 |
public Integer defaultReviewerCount = 1; |
120 | 120 |
|
121 | 121 |
public boolean isUsingReviewerCount; |
122 |
+ |
|
123 |
+ @ManyToOne |
|
124 |
+ public Organization organization; |
|
125 |
+ |
|
126 |
+ @Enumerated(EnumType.STRING) |
|
127 |
+ public ProjectScope projectScope; |
|
122 | 128 |
|
123 | 129 |
/** |
124 | 130 |
* 신규 프로젝트를 생성한다. |
... | ... | @@ -733,6 +739,7 @@ |
733 | 739 |
@Override |
734 | 740 |
public void delete() { |
735 | 741 |
deleteProjectVisitations(); |
742 |
+ deleteProjectTransfer(); |
|
736 | 743 |
deleteFork(); |
737 | 744 |
deletePullRequests(); |
738 | 745 |
|
... | ... | @@ -770,8 +777,15 @@ |
770 | 777 |
|
771 | 778 |
private void deleteProjectVisitations() { |
772 | 779 |
List<ProjectVisitation> pvs = ProjectVisitation.findByProject(this); |
773 |
- for(ProjectVisitation pv : pvs) { |
|
780 |
+ for (ProjectVisitation pv : pvs) { |
|
774 | 781 |
pv.delete(); |
782 |
+ } |
|
783 |
+ } |
|
784 |
+ |
|
785 |
+ private void deleteProjectTransfer() { |
|
786 |
+ List<ProjectTransfer> pts = ProjectTransfer.findByProject(this); |
|
787 |
+ for(ProjectTransfer pt : pts) { |
|
788 |
+ pt.delete(); |
|
775 | 789 |
} |
776 | 790 |
} |
777 | 791 |
|
... | ... | @@ -806,7 +820,7 @@ |
806 | 820 |
* @param projectName |
807 | 821 |
* @return |
808 | 822 |
*/ |
809 |
- private static String newProjectName(String loginId, String projectName) { |
|
823 |
+ public static String newProjectName(String loginId, String projectName) { |
|
810 | 824 |
Project project = Project.findByOwnerAndProjectName(loginId, projectName); |
811 | 825 |
if(project == null) { |
812 | 826 |
return projectName; |
... | ... | @@ -924,4 +938,8 @@ |
924 | 938 |
return Watch.countBy(resource.getType(), resource.getId()); |
925 | 939 |
} |
926 | 940 |
|
941 |
+ public boolean hasGroup() { |
|
942 |
+ return this.organization != null; |
|
943 |
+ } |
|
944 |
+ |
|
927 | 945 |
} |
+++ app/models/ProjectTransfer.java
... | ... | @@ -0,0 +1,158 @@ |
1 | +/** | |
2 | + * Yobi, Project Hosting SW | |
3 | + * | |
4 | + * Copyright 2013 NAVER Corp. | |
5 | + * http://yobi.io | |
6 | + * | |
7 | + * @Author Keeun Baik | |
8 | + * | |
9 | + * Licensed under the Apache License, Version 2.0 (the "License"); | |
10 | + * you may not use this file except in compliance with the License. | |
11 | + * You may obtain a copy of the License at | |
12 | + * | |
13 | + * http://www.apache.org/licenses/LICENSE-2.0 | |
14 | + * | |
15 | + * Unless required by applicable law or agreed to in writing, software | |
16 | + * distributed under the License is distributed on an "AS IS" BASIS, | |
17 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
18 | + * See the License for the specific language governing permissions and | |
19 | + * limitations under the License. | |
20 | + */ | |
21 | +package models; | |
22 | + | |
23 | +import controllers.routes; | |
24 | +import models.enumeration.ResourceType; | |
25 | +import models.resource.Resource; | |
26 | +import models.resource.ResourceConvertible; | |
27 | +import org.apache.commons.lang3.RandomStringUtils; | |
28 | +import org.joda.time.DateTime; | |
29 | +import play.db.ebean.Model; | |
30 | +import utils.Url; | |
31 | + | |
32 | +import javax.persistence.*; | |
33 | +import java.util.Date; | |
34 | +import java.util.List; | |
35 | + | |
36 | +/** | |
37 | + * 프로젝트 이관 정보 | |
38 | + * | |
39 | + * 이관 요청을 할 때 새로운 프로젝트 이관 정보를 생성하고, 이관이 완려되면 {@code accepted}가 true로 바뀐다. | |
40 | + * 새로운 이관 요청을 만들때 받을 사용자의 프로젝트 이름을 확인하고 중복되는 프로젝트가 있을 경우에는 프로젝트 fork와 동일한 정책으로 프로젝트 이름을 변경한다. | |
41 | + * 동일한 프로젝트를 동일한 사용자에서 동일한 사용자로 여러번 이관 요청을 할 경우에는 이전 요청의 날짜와 확인키를 변경한다. | |
42 | + * 요청이 수락되면 이전 이관 요청 중에서 동일한 프로젝트를 반대로 주고받은 요청을 삭제한다. | |
43 | + */ | |
44 | +@Entity | |
45 | +public class ProjectTransfer extends Model implements ResourceConvertible { | |
46 | + | |
47 | + private static final long serialVersionUID = 1L; | |
48 | + | |
49 | + public static Finder<Long, ProjectTransfer> find = new Finder<>(Long.class, ProjectTransfer.class); | |
50 | + | |
51 | + @Id | |
52 | + public Long id; | |
53 | + | |
54 | + // who requested this transfer. | |
55 | + @ManyToOne | |
56 | + public User sender; | |
57 | + | |
58 | + /** | |
59 | + * Destination can be either an user or an organization. | |
60 | + * If you want to transfer to an user, then destination have to be set with the user's loginId. | |
61 | + * If you want to transfer to an organization, then destination have to be set with the organization's name. | |
62 | + */ | |
63 | + public String destination; | |
64 | + | |
65 | + @ManyToOne | |
66 | + public Project project; | |
67 | + | |
68 | + @Temporal(TemporalType.TIMESTAMP) | |
69 | + public Date requested; | |
70 | + | |
71 | + public String confirmKey; | |
72 | + | |
73 | + public boolean accepted; | |
74 | + | |
75 | + public String newProjectName; | |
76 | + | |
77 | + public static ProjectTransfer requestNewTransfer(Project project, User sender, String destination) { | |
78 | + ProjectTransfer pt = find.where() | |
79 | + .eq("project", project) | |
80 | + .eq("sender", sender) | |
81 | + .eq("destination", destination) | |
82 | + .findUnique(); | |
83 | + | |
84 | + if(pt != null) { | |
85 | + pt.requested = new Date(); | |
86 | + pt.confirmKey = RandomStringUtils.randomAlphanumeric(50); | |
87 | + pt.update(); | |
88 | + } else { | |
89 | + pt = new ProjectTransfer(); | |
90 | + pt.project = project; | |
91 | + pt.sender = sender; | |
92 | + pt.destination = destination; | |
93 | + pt.requested = new Date(); | |
94 | + pt.confirmKey = RandomStringUtils.randomAlphanumeric(50); | |
95 | + pt.newProjectName = Project.newProjectName(destination, project.name); | |
96 | + pt.save(); | |
97 | + } | |
98 | + | |
99 | + return pt; | |
100 | + } | |
101 | + | |
102 | + public String getAcceptUrl() { | |
103 | + return Url.create(routes.ProjectApp.acceptTransfer(id, confirmKey).url()); | |
104 | + } | |
105 | + | |
106 | + /** | |
107 | + * 현재 시간 기준으로 하루 안에 생성되었고 수락하지 않은 {@code ProjectTransfer} 요청을 찾습니다. | |
108 | + * @param id | |
109 | + * @return | |
110 | + */ | |
111 | + public static ProjectTransfer findValidOne(Long id) { | |
112 | + Date now = new Date(); | |
113 | + DateTime oneDayBefore = new DateTime(now).minusDays(1); | |
114 | + | |
115 | + return find.where() | |
116 | + .eq("id", id) | |
117 | + .eq("accepted", false) | |
118 | + .between("requested", oneDayBefore, now) | |
119 | + .findUnique(); | |
120 | + } | |
121 | + | |
122 | + public static void deleteExisting(Project project, User sender, String destination) { | |
123 | + ProjectTransfer pt = find.where() | |
124 | + .eq("project", project) | |
125 | + .eq("sender", sender) | |
126 | + .eq("destination", destination) | |
127 | + .findUnique(); | |
128 | + | |
129 | + if(pt != null) { | |
130 | + pt.delete(); | |
131 | + } | |
132 | + } | |
133 | + | |
134 | + public Resource asResource() { | |
135 | + return new Resource() { | |
136 | + @Override | |
137 | + public String getId() { | |
138 | + return id.toString(); | |
139 | + } | |
140 | + | |
141 | + @Override | |
142 | + public Project getProject() { | |
143 | + return project; | |
144 | + } | |
145 | + | |
146 | + @Override | |
147 | + public ResourceType getType() { | |
148 | + return ResourceType.PROJECT_TRANSFER; | |
149 | + } | |
150 | + }; | |
151 | + } | |
152 | + | |
153 | + public static List<ProjectTransfer> findByProject(Project project) { | |
154 | + return find.where() | |
155 | + .eq("project", project) | |
156 | + .findList(); | |
157 | + } | |
158 | +} |
--- app/models/Role.java
+++ app/models/Role.java
... | ... | @@ -65,4 +65,19 @@ |
65 | 65 |
.in("id", projectRoleIds) |
66 | 66 |
.findList(); |
67 | 67 |
} |
68 |
+ |
|
69 |
+ /** |
|
70 |
+ * 그룹과 관련된 롤들의 목록을 반환합니다. |
|
71 |
+ * |
|
72 |
+ * @return |
|
73 |
+ */ |
|
74 |
+ public static List<Role> findOrganizationRoles() { |
|
75 |
+ List<Long> organizationRoleIds = new ArrayList<>(); |
|
76 |
+ organizationRoleIds.add(RoleType.ORG_ADMIN.roleType()); |
|
77 |
+ organizationRoleIds.add(RoleType.ORG_MEMBER.roleType()); |
|
78 |
+ |
|
79 |
+ return find.where() |
|
80 |
+ .in("id", organizationRoleIds) |
|
81 |
+ .findList(); |
|
82 |
+ } |
|
68 | 83 |
} |
--- app/models/User.java
+++ app/models/User.java
... | ... | @@ -152,6 +152,9 @@ |
152 | 152 |
*/ |
153 | 153 |
public String lang; |
154 | 154 |
|
155 |
+ @OneToMany(mappedBy = "user", cascade = CascadeType.ALL) |
|
156 |
+ public List<OrganizationUser> organizationUsers; |
|
157 |
+ |
|
155 | 158 |
public User() { |
156 | 159 |
} |
157 | 160 |
|
... | ... | @@ -604,4 +607,31 @@ |
604 | 607 |
|
605 | 608 |
return this.recentlyVisitedProjects.findRecentlyVisitedProjects(size); |
606 | 609 |
} |
610 |
+ |
|
611 |
+ public List<Organization> getOrganizations(int size) { |
|
612 |
+ if(size < 1) { |
|
613 |
+ throw new IllegalArgumentException("the size should be bigger then 0"); |
|
614 |
+ } |
|
615 |
+ List<Organization> orgs = new ArrayList<>(); |
|
616 |
+ for(OrganizationUser ou : OrganizationUser.findByUser(this, size)) { |
|
617 |
+ orgs.add(ou.organization); |
|
618 |
+ } |
|
619 |
+ return orgs; |
|
620 |
+ } |
|
621 |
+ |
|
622 |
+ public void createOrganization(Organization organization) { |
|
623 |
+ OrganizationUser ou = new OrganizationUser(); |
|
624 |
+ ou.user = this; |
|
625 |
+ ou.organization = organization; |
|
626 |
+ ou.role = Role.findByRoleType(RoleType.ORG_ADMIN); |
|
627 |
+ ou.save(); |
|
628 |
+ |
|
629 |
+ this.add(ou); |
|
630 |
+ organization.add(ou); |
|
631 |
+ this.update(); |
|
632 |
+ } |
|
633 |
+ |
|
634 |
+ private void add(OrganizationUser ou) { |
|
635 |
+ this.organizationUsers.add(ou); |
|
636 |
+ } |
|
607 | 637 |
} |
+++ app/models/enumeration/ProjectScope.java
... | ... | @@ -0,0 +1,9 @@ |
1 | +package models.enumeration; | |
2 | + | |
3 | +/** | |
4 | + * @author Keeun Baik | |
5 | + */ | |
6 | +public enum ProjectScope { | |
7 | + | |
8 | + PRIVATE, PROTECTED, PUBLIC; | |
9 | +} |
--- app/models/enumeration/ResourceType.java
+++ app/models/enumeration/ResourceType.java
... | ... | @@ -30,6 +30,8 @@ |
30 | 30 |
COMMIT("commit"), |
31 | 31 |
COMMENT_THREAD("comment_thread"), |
32 | 32 |
REVIEW_COMMENT("review_comment"), |
33 |
+ ORGANIZATION("organization"), |
|
34 |
+ PROJECT_TRANSFER("project_transfer"), |
|
33 | 35 |
NOT_A_RESOURCE(""); |
34 | 36 |
|
35 | 37 |
private String resource; |
--- app/models/enumeration/RoleType.java
+++ app/models/enumeration/RoleType.java
... | ... | @@ -1,7 +1,7 @@ |
1 | 1 |
package models.enumeration; |
2 | 2 |
|
3 | 3 |
public enum RoleType { |
4 |
- MANAGER(1l), MEMBER(2l), SITEMANAGER(3l), ANONYMOUS(4l), GUEST(5l); |
|
4 |
+ MANAGER(1l), MEMBER(2l), SITEMANAGER(3l), ANONYMOUS(4l), GUEST(5l), ORG_ADMIN(6l), ORG_MEMBER(7l); |
|
5 | 5 |
|
6 | 6 |
private Long roleType; |
7 | 7 |
|
--- app/models/resource/Resource.java
+++ app/models/resource/Resource.java
... | ... | @@ -1,7 +1,6 @@ |
1 | 1 |
package models.resource; |
2 | 2 |
|
3 | 3 |
import actions.support.PathParser; |
4 |
-import controllers.routes; |
|
5 | 4 |
import models.*; |
6 | 5 |
import models.enumeration.ResourceType; |
7 | 6 |
import play.db.ebean.Model; |
... | ... | @@ -53,6 +52,9 @@ |
53 | 52 |
break; |
54 | 53 |
case REVIEW_COMMENT: |
55 | 54 |
finder = ReviewComment.find; |
55 |
+ break; |
|
56 |
+ case ORGANIZATION: |
|
57 |
+ finder = Organization.find; |
|
56 | 58 |
break; |
57 | 59 |
case COMMIT: |
58 | 60 |
try { |
... | ... | @@ -124,6 +126,9 @@ |
124 | 126 |
return User.find.byId(longId).avatarAsResource(); |
125 | 127 |
case REVIEW_COMMENT: |
126 | 128 |
return ReviewComment.find.byId(longId).asResource(); |
129 |
+ case ORGANIZATION: |
|
130 |
+ resource = Organization.find.byId(longId).asResource(); |
|
131 |
+ break; |
|
127 | 132 |
default: |
128 | 133 |
throw new IllegalArgumentException(getInvalidResourceTypeMessage(resourceType)); |
129 | 134 |
} |
--- app/playRepository/GitRepository.java
+++ app/playRepository/GitRepository.java
... | ... | @@ -9,7 +9,6 @@ |
9 | 9 |
import models.enumeration.ResourceType; |
10 | 10 |
import models.resource.Resource; |
11 | 11 |
import models.support.ModelLock; |
12 |
- |
|
13 | 12 |
import org.apache.commons.collections.IteratorUtils; |
14 | 13 |
import org.apache.commons.lang3.ArrayUtils; |
15 | 14 |
import org.apache.commons.lang3.StringUtils; |
... | ... | @@ -40,7 +39,6 @@ |
40 | 39 |
import org.eclipse.jgit.util.FileUtils; |
41 | 40 |
import org.eclipse.jgit.util.io.NullOutputStream; |
42 | 41 |
import org.tmatesoft.svn.core.SVNException; |
43 |
- |
|
44 | 42 |
import play.Logger; |
45 | 43 |
import play.libs.Json; |
46 | 44 |
import utils.FileUtil; |
... | ... | @@ -1928,16 +1926,7 @@ |
1928 | 1926 |
*/ |
1929 | 1927 |
@Override |
1930 | 1928 |
public boolean renameTo(String projectName) { |
1931 |
- |
|
1932 |
- repository.close(); |
|
1933 |
- WindowCacheConfig config = new WindowCacheConfig(); |
|
1934 |
- config.install(); |
|
1935 |
- File src = new File(getGitDirectory(this.ownerName, this.projectName)); |
|
1936 |
- File dest = new File(getGitDirectory(this.ownerName, projectName)); |
|
1937 |
- |
|
1938 |
- src.setWritable(true); |
|
1939 |
- |
|
1940 |
- return src.renameTo(dest); |
|
1929 |
+ return move(this.ownerName, this.projectName, this.ownerName, projectName); |
|
1941 | 1930 |
} |
1942 | 1931 |
|
1943 | 1932 |
@Override |
... | ... | @@ -2015,4 +2004,26 @@ |
2015 | 2004 |
RevWalk revWalk = new RevWalk(repository); |
2016 | 2005 |
return revWalk.parseCommit(objectId); |
2017 | 2006 |
} |
2007 |
+ |
|
2008 |
+ public boolean move(String srcProjectOwner, String srcProjectName, String desrProjectOwner, String destProjectName) { |
|
2009 |
+ repository.close(); |
|
2010 |
+ WindowCacheConfig config = new WindowCacheConfig(); |
|
2011 |
+ config.install(); |
|
2012 |
+ |
|
2013 |
+ File srcGitDirectory = new File(getGitDirectory(srcProjectOwner, srcProjectName)); |
|
2014 |
+ File destGitDirectory = new File(getGitDirectory(desrProjectOwner, destProjectName)); |
|
2015 |
+ File srcGitDirectoryForMerging = new File(getDirectoryForMerging(srcProjectOwner, srcProjectName)); |
|
2016 |
+ File destGitDirectoryForMerging = new File(getDirectoryForMerging(desrProjectOwner, destProjectName)); |
|
2017 |
+ srcGitDirectory.setWritable(true); |
|
2018 |
+ srcGitDirectoryForMerging.setWritable(true); |
|
2019 |
+ |
|
2020 |
+ try { |
|
2021 |
+ org.apache.commons.io.FileUtils.moveDirectory(srcGitDirectory, destGitDirectory); |
|
2022 |
+ org.apache.commons.io.FileUtils.moveDirectory(srcGitDirectoryForMerging, destGitDirectoryForMerging); |
|
2023 |
+ return true; |
|
2024 |
+ } catch (IOException e) { |
|
2025 |
+ play.Logger.error("Move Failed", e); |
|
2026 |
+ return false; |
|
2027 |
+ } |
|
2028 |
+ } |
|
2018 | 2029 |
} |
--- app/playRepository/PlayRepository.java
+++ app/playRepository/PlayRepository.java
... | ... | @@ -156,4 +156,15 @@ |
156 | 156 |
* @return 저장소가 비어있을시 true / 비어있지 않을시 false |
157 | 157 |
*/ |
158 | 158 |
boolean isEmpty(); |
159 |
+ |
|
160 |
+ /** |
|
161 |
+ * 저장소를 옮긴다. |
|
162 |
+ * |
|
163 |
+ * @param srcProjectOwner |
|
164 |
+ * @param srcProjectName |
|
165 |
+ * @param desrProjectOwner |
|
166 |
+ * @param destProjectName |
|
167 |
+ * @return |
|
168 |
+ */ |
|
169 |
+ boolean move(String srcProjectOwner, String srcProjectName, String desrProjectOwner, String destProjectName); |
|
159 | 170 |
} |
--- app/playRepository/SVNRepository.java
+++ app/playRepository/SVNRepository.java
... | ... | @@ -1,36 +1,35 @@ |
1 | 1 |
package playRepository; |
2 | 2 |
|
3 |
-import java.io.*; |
|
4 |
-import java.util.*; |
|
5 |
- |
|
6 |
-import javax.servlet.*; |
|
7 |
- |
|
3 |
+import controllers.ProjectApp; |
|
8 | 4 |
import controllers.UserApp; |
9 | 5 |
import controllers.routes; |
10 | 6 |
import models.Project; |
11 | 7 |
import models.User; |
12 | 8 |
import models.enumeration.ResourceType; |
13 | 9 |
import models.resource.Resource; |
14 |
- |
|
10 |
+import org.apache.commons.io.FileUtils; |
|
15 | 11 |
import org.apache.tika.Tika; |
16 | 12 |
import org.codehaus.jackson.node.ObjectNode; |
17 | 13 |
import org.eclipse.jgit.api.errors.GitAPIException; |
18 |
-import org.eclipse.jgit.api.errors.NoHeadException; |
|
19 | 14 |
import org.eclipse.jgit.diff.RawText; |
20 |
-import org.eclipse.jgit.errors.AmbiguousObjectException; |
|
21 |
-import org.tigris.subversion.javahl.*; |
|
15 |
+import org.joda.time.format.DateTimeFormatter; |
|
16 |
+import org.joda.time.format.ISODateTimeFormat; |
|
17 |
+import org.tigris.subversion.javahl.ClientException; |
|
22 | 18 |
import org.tmatesoft.svn.core.*; |
23 | 19 |
import org.tmatesoft.svn.core.io.SVNRepositoryFactory; |
24 | 20 |
import org.tmatesoft.svn.core.wc.SVNClientManager; |
25 | 21 |
import org.tmatesoft.svn.core.wc.SVNDiffClient; |
26 | 22 |
import org.tmatesoft.svn.core.wc.SVNRevision; |
27 |
- |
|
28 |
-import controllers.ProjectApp; |
|
29 | 23 |
import play.libs.Json; |
30 | 24 |
import utils.FileUtil; |
31 | 25 |
import utils.GravatarUtil; |
32 | 26 |
|
33 |
-import org.joda.time.format.*; |
|
27 |
+import java.io.ByteArrayOutputStream; |
|
28 |
+import java.io.File; |
|
29 |
+import java.io.IOException; |
|
30 |
+import java.util.ArrayList; |
|
31 |
+import java.util.Collection; |
|
32 |
+import java.util.List; |
|
34 | 33 |
|
35 | 34 |
public class SVNRepository implements PlayRepository { |
36 | 35 |
|
... | ... | @@ -387,13 +386,7 @@ |
387 | 386 |
*/ |
388 | 387 |
@Override |
389 | 388 |
public boolean renameTo(String projectName) { |
390 |
- |
|
391 |
- File src = new File(getRepoPrefix() + this.ownerName + "/" + this.projectName); |
|
392 |
- File dest = new File(getRepoPrefix() + this.ownerName + "/" + projectName); |
|
393 |
- src.setWritable(true); |
|
394 |
- |
|
395 |
- return src.renameTo(dest); |
|
396 |
- |
|
389 |
+ return move(this.ownerName, this.projectName, this.ownerName, projectName); |
|
397 | 390 |
} |
398 | 391 |
|
399 | 392 |
@Override |
... | ... | @@ -437,4 +430,18 @@ |
437 | 430 |
} |
438 | 431 |
} |
439 | 432 |
} |
433 |
+ |
|
434 |
+ public boolean move(String srcProjectOwner, String srcProjectName, String desrProjectOwner, String destProjectName) { |
|
435 |
+ File src = new File(getRepoPrefix() + srcProjectOwner + "/" + srcProjectName); |
|
436 |
+ File dest = new File(getRepoPrefix() + desrProjectOwner + "/" + destProjectName); |
|
437 |
+ src.setWritable(true); |
|
438 |
+ |
|
439 |
+ try { |
|
440 |
+ FileUtils.moveDirectory(src, dest); |
|
441 |
+ return true; |
|
442 |
+ } catch (IOException e) { |
|
443 |
+ play.Logger.error("Move Failed", e); |
|
444 |
+ return false; |
|
445 |
+ } |
|
446 |
+ } |
|
440 | 447 |
} |
--- app/utils/AccessControl.java
+++ app/utils/AccessControl.java
... | ... | @@ -3,6 +3,7 @@ |
3 | 3 |
import models.Project; |
4 | 4 |
import models.ProjectUser; |
5 | 5 |
import models.User; |
6 |
+import models.*; |
|
6 | 7 |
import models.enumeration.Operation; |
7 | 8 |
import models.enumeration.ResourceType; |
8 | 9 |
import models.resource.GlobalResource; |
... | ... | @@ -39,6 +40,11 @@ |
39 | 40 |
if (user.isSiteManager()) { |
40 | 41 |
return true; |
41 | 42 |
} |
43 |
+ |
|
44 |
+ // project가 조직에 속하고 사용자가 Admin 이면 true |
|
45 |
+ /*if (project.organization != null && project.organization.isAdmin(user)) { |
|
46 |
+ return true; |
|
47 |
+ }*/ |
|
42 | 48 |
|
43 | 49 |
if (ProjectUser.isMember(user.id, project.id)) { |
44 | 50 |
// Project members can create anything. |
... | ... | @@ -135,7 +141,19 @@ |
135 | 141 |
case USER_AVATAR: |
136 | 142 |
return user.id.toString().equals(resource.getId()); |
137 | 143 |
case PROJECT: |
138 |
- return ProjectUser.isManager(user.id, Long.valueOf(resource.getId())); |
|
144 |
+ // allow to managers of the project. |
|
145 |
+ boolean isManager = ProjectUser.isManager(user.id, Long.valueOf(resource.getId())); |
|
146 |
+ if(isManager) { |
|
147 |
+ return true; |
|
148 |
+ } |
|
149 |
+ // allow to admins of the group of the project. |
|
150 |
+ Project project = Project.find.byId(Long.valueOf(resource.getId())); |
|
151 |
+ if(project.hasGroup()) { |
|
152 |
+ return OrganizationUser.isAdmin(project.organization.id, user.id); |
|
153 |
+ } |
|
154 |
+ return false; |
|
155 |
+ case ORGANIZATION: |
|
156 |
+ return OrganizationUser.isAdmin(Long.valueOf(resource.getId()), user.id); |
|
139 | 157 |
default: |
140 | 158 |
// undefined |
141 | 159 |
return false; |
... | ... | @@ -155,10 +173,30 @@ |
155 | 173 |
* @return true if the user has the permission |
156 | 174 |
*/ |
157 | 175 |
private static boolean isProjectResourceAllowed(User user, Project project, Resource resource, Operation operation) { |
176 |
+ Organization org = project.organization; |
|
177 |
+ if(org != null && OrganizationUser.isAdmin(org.id, user.id)) { |
|
178 |
+ return true; |
|
179 |
+ } |
|
180 |
+ |
|
158 | 181 |
if (user.isSiteManager() |
159 | 182 |
|| ProjectUser.isManager(user.id, project.id) |
160 | 183 |
|| isAllowedIfAuthor(user, resource)) { |
161 | 184 |
return true; |
185 |
+ } |
|
186 |
+ |
|
187 |
+ // If the resource is a project_transfer, only new owner can accept the request. |
|
188 |
+ if(resource.getType() == ResourceType.PROJECT_TRANSFER) { |
|
189 |
+ switch (operation) { |
|
190 |
+ case ACCEPT: |
|
191 |
+ ProjectTransfer pt = ProjectTransfer.find.byId(Long.parseLong(resource.getId())); |
|
192 |
+ User to = User.findByLoginId(pt.destination); |
|
193 |
+ if(!to.isAnonymous()) { |
|
194 |
+ return user.loginId.equals(pt.destination); |
|
195 |
+ } else { |
|
196 |
+ Organization receivingOrg = Organization.findByName(pt.destination); |
|
197 |
+ return receivingOrg != null && OrganizationUser.isAdmin(receivingOrg.id, user.id); |
|
198 |
+ } |
|
199 |
+ } |
|
162 | 200 |
} |
163 | 201 |
|
164 | 202 |
// Some resource's permission depends on their container. |
... | ... | @@ -167,7 +205,7 @@ |
167 | 205 |
case ISSUE_ASSIGNEE: |
168 | 206 |
case ISSUE_MILESTONE: |
169 | 207 |
case ATTACHMENT: |
170 |
- switch(operation) { |
|
208 |
+ switch (operation) { |
|
171 | 209 |
case READ: |
172 | 210 |
return isAllowed(user, resource.getContainer(), Operation.READ); |
173 | 211 |
case UPDATE: |
--- app/utils/ErrorViews.java
+++ app/utils/ErrorViews.java
... | ... | @@ -1,5 +1,6 @@ |
1 | 1 |
package utils; |
2 | 2 |
import controllers.UserApp; |
3 |
+import models.Organization; |
|
3 | 4 |
import models.Project; |
4 | 5 |
import models.User; |
5 | 6 |
import play.api.templates.Html; |
... | ... | @@ -22,11 +23,16 @@ |
22 | 23 |
} |
23 | 24 |
|
24 | 25 |
public Html render(String messageKey, String returnUrl) { |
25 |
- if(UserApp.currentUser() == User.anonymous){ |
|
26 |
+ if (UserApp.currentUser() == User.anonymous) { |
|
26 | 27 |
return views.html.user.login.render("error.fobidden", null, returnUrl); |
27 | 28 |
} else { |
28 | 29 |
return views.html.error.forbidden_default.render(messageKey); |
29 | 30 |
} |
31 |
+ } |
|
32 |
+ |
|
33 |
+ @Override |
|
34 |
+ public Html render(String messageKey, Organization organization) { |
|
35 |
+ return views.html.error.forbidden_organization.render(messageKey, organization); |
|
30 | 36 |
} |
31 | 37 |
|
32 | 38 |
@Deprecated |
... | ... | @@ -49,6 +55,12 @@ |
49 | 55 |
@Override |
50 | 56 |
public Html render(String messageKey, Project project) { |
51 | 57 |
return render(messageKey, project, null); |
58 |
+ } |
|
59 |
+ |
|
60 |
+ @Override |
|
61 |
+ public Html render(String messageKey, Organization organization) { |
|
62 |
+ // TODO : make notfound view for organization |
|
63 |
+ return views.html.error.notfound_default.render(messageKey); |
|
52 | 64 |
} |
53 | 65 |
|
54 | 66 |
@Override |
... | ... | @@ -78,6 +90,11 @@ |
78 | 90 |
} |
79 | 91 |
|
80 | 92 |
@Override |
93 |
+ public Html render(String messageKey, Organization organization) { |
|
94 |
+ throw new UnsupportedOperationException(); |
|
95 |
+ } |
|
96 |
+ |
|
97 |
+ @Override |
|
81 | 98 |
public Html render(String messageKey, Project project, String target) { |
82 | 99 |
throw new UnsupportedOperationException(); |
83 | 100 |
} |
... | ... | @@ -91,6 +108,12 @@ |
91 | 108 |
@Override |
92 | 109 |
public Html render(String messageKey, Project project) { |
93 | 110 |
return views.html.error.badrequest.render(messageKey, project); |
111 |
+ } |
|
112 |
+ |
|
113 |
+ @Override |
|
114 |
+ public Html render(String messageKey, Organization organization) { |
|
115 |
+ // TODO : make badrequest view for organization |
|
116 |
+ return views.html.error.badrequest_default.render(messageKey); |
|
94 | 117 |
} |
95 | 118 |
|
96 | 119 |
@Deprecated |
... | ... | @@ -139,6 +162,16 @@ |
139 | 162 |
|
140 | 163 |
/** |
141 | 164 |
* 오류페이지 HTML을 레더링 한다. |
165 |
+ * 메세지는 파라미터로 전달되는 messageKey를 사용하고 레이아웃은 그룹레벨이 된다. |
|
166 |
+ * |
|
167 |
+ * @param messageKey 메세지키 |
|
168 |
+ * @param organization 그룹 정보 |
|
169 |
+ * @return |
|
170 |
+ */ |
|
171 |
+ public abstract Html render(String messageKey, Organization organization); |
|
172 |
+ |
|
173 |
+ /** |
|
174 |
+ * 오류페이지 HTML을 레더링 한다. |
|
142 | 175 |
* 메세지와 레이아웃은 세부타겟정보에 따라 이슈/게시판/프로젝트로 나뉘어 진다. |
143 | 176 |
* |
144 | 177 |
* @param messageKey 메세지키 |
+++ app/utils/LogoUtil.java
... | ... | @@ -0,0 +1,35 @@ |
1 | +package utils; | |
2 | + | |
3 | +import play.mvc.Http; | |
4 | + | |
5 | +public class LogoUtil { | |
6 | + public static final int LOGO_FILE_LIMIT_SIZE = 1024*1000*5; //5M | |
7 | + | |
8 | + /** 프로젝트 로고로 사용할 수 있는 이미지 확장자 */ | |
9 | + public static final String[] LOGO_TYPE = {"jpg", "jpeg", "png", "gif", "bmp"}; | |
10 | + | |
11 | + /** | |
12 | + * {@code filePart} 정보가 비어있는지 확인한다.<p /> | |
13 | + * @param filePart | |
14 | + * @return {@code filePart}가 null이면 true, {@code filename}이 null이면 true, {@code fileLength}가 0 이하이면 true | |
15 | + */ | |
16 | + public static boolean isEmptyFilePart(Http.MultipartFormData.FilePart filePart) { | |
17 | + return filePart == null || filePart.getFilename() == null || filePart.getFilename().length() <= 0; | |
18 | + } | |
19 | + | |
20 | + /** | |
21 | + * {@code filename}의 확장자를 체크하여 이미지인지 확인한다.<p /> | |
22 | + * | |
23 | + * 이미지 확장자는 {@link #LOGO_TYPE} 에 정의한다. | |
24 | + * @param filename the filename | |
25 | + * @return true, if is image file | |
26 | + */ | |
27 | + public static boolean isImageFile(String filename) { | |
28 | + boolean isImageFile = false; | |
29 | + for(String suffix : LOGO_TYPE) { | |
30 | + if(filename.toLowerCase().endsWith(suffix)) | |
31 | + isImageFile = true; | |
32 | + } | |
33 | + return isImageFile; | |
34 | + } | |
35 | +} |
--- app/utils/TemplateHelper.scala
+++ app/utils/TemplateHelper.scala
... | ... | @@ -5,20 +5,18 @@ |
5 | 5 |
import play.i18n.Messages |
6 | 6 |
import controllers.routes |
7 | 7 |
import controllers.UserApp |
8 |
-import java.security.MessageDigest |
|
9 | 8 |
import views.html._ |
10 | 9 |
import java.net.URI |
11 | 10 |
import playRepository.DiffLine |
12 | 11 |
import playRepository.DiffLineType |
13 | 12 |
import models.CodeRange.Side |
14 | 13 |
import scala.collection.JavaConversions._ |
15 |
-import org.apache.commons.lang3.StringEscapeUtils.escapeHtml4 |
|
16 | 14 |
import views.html.partial_diff_comment_on_line |
17 | 15 |
import views.html.partial_diff_line |
18 | 16 |
import views.html.git.partial_pull_request_event |
17 |
+import models.Organization |
|
19 | 18 |
import models.PullRequestEvent |
20 | 19 |
import models.PullRequest |
21 |
-import models.TimelineItem |
|
22 | 20 |
import models.Project |
23 | 21 |
import models.Issue |
24 | 22 |
import java.net.URLEncoder |
... | ... | @@ -27,7 +25,6 @@ |
27 | 25 |
import play.api.i18n.Lang |
28 | 26 |
import models.CodeCommentThread |
29 | 27 |
import models.CommentThread |
30 |
-import javax.swing.text.html.HTML |
|
31 | 28 |
|
32 | 29 |
object TemplateHelper { |
33 | 30 |
|
... | ... | @@ -212,10 +209,17 @@ |
212 | 209 |
} |
213 | 210 |
|
214 | 211 |
def countOpenIssuesBy(project:Project, cond:java.util.Map[String,String]) = { |
215 |
- cond += ("state"->models.enumeration.State.OPEN.toString) |
|
212 |
+ cond += ("state" -> models.enumeration.State.OPEN.toString) |
|
216 | 213 |
Issue.countIssuesBy(project.id, cond) |
217 | 214 |
} |
218 | 215 |
|
216 |
+ def urlToOrganizationLogo(organization: Organization) = { |
|
217 |
+ models.Attachment.findByContainer(organization.asResource) match { |
|
218 |
+ case files if files.size > 0 => routes.AttachmentApp.getFile(files.head.id) |
|
219 |
+ case _ => routes.Assets.at("images/bg-default-project.jpg") |
|
220 |
+ } |
|
221 |
+ } |
|
222 |
+ |
|
219 | 223 |
object DiffRenderer { |
220 | 224 |
|
221 | 225 |
def removedWord(word: String) = "<span class='remove'>" + word + "</span>" |
+++ app/utils/ValidationResult.java
... | ... | @@ -0,0 +1,20 @@ |
1 | +package utils; | |
2 | + | |
3 | +import play.mvc.Result; | |
4 | + | |
5 | +public class ValidationResult { | |
6 | + private Result result; | |
7 | + private boolean hasError; | |
8 | + | |
9 | + public ValidationResult(Result result, boolean hasError) { | |
10 | + this.result = result; | |
11 | + this.hasError = hasError; | |
12 | + } | |
13 | + | |
14 | + public boolean hasError(){ | |
15 | + return hasError; | |
16 | + } | |
17 | + public Result getResult() { | |
18 | + return result; | |
19 | + } | |
20 | +} |
+++ app/views/error/forbidden_organization.scala.html
... | ... | @@ -0,0 +1,13 @@ |
1 | +@(messageKey:String = "error.forbidden", organization: Organization) | |
2 | + | |
3 | +@siteLayout(organization.name, utils.MenuType.NONE) { | |
4 | + <div class="site-breadcrumb-outer"> | |
5 | + <div class="site-breadcrumb-inner"> | |
6 | + <div class="error-wrap"> | |
7 | + <i class="ico ico-err2"></i> | |
8 | + <p>@Messages(messageKey)</p> | |
9 | + </div> | |
10 | + </div> | |
11 | + </div> | |
12 | +} | |
13 | + |
+++ app/views/organization/create.scala.html
... | ... | @@ -0,0 +1,49 @@ |
1 | +@(title:String, form: Form[Organization]) | |
2 | + | |
3 | +@siteLayout("app.name", utils.MenuType.NONE) { | |
4 | +<div class="page-wrap-outer"> | |
5 | + <div class="project-page-wrap"> | |
6 | + <div class="form-wrap new-project"> | |
7 | + <form action="@routes.OrganizationApp.newOrganization()" method="post" name="new-org" class="frm-wrap"> | |
8 | + <legend> | |
9 | + @Messages("title.newOrganization") | |
10 | + </legend> | |
11 | + <dl> | |
12 | + <dt> | |
13 | + <div class="n-alert" data-errType="name"> | |
14 | + <div class="orange-txt"> | |
15 | + @if(flash.get("warning") != null) { <span class="warning">@Messages(flash.get("warning"))</span> } | |
16 | + <span class="msg wrongName" style="display: none;">@Messages("project.wrongName")</span> | |
17 | + </div> | |
18 | + </div> | |
19 | + <label for="name">@Messages("organization.name.placeholder")</label> | |
20 | + </dt> | |
21 | + <dd> | |
22 | + <input id="name" type="text" name="name" class="text" placeholder="" maxlength="250" value="@form.field("name").value()" /> | |
23 | + </dd> | |
24 | + | |
25 | + <dt> | |
26 | + <label for="descr">@Messages("organization.description.placeholder")</label> | |
27 | + </dt> | |
28 | + <dd> | |
29 | + <textarea id="descr" name="descr" class="text textarea.span4" style="resize: vertical;" >@form.field("descr").value()</textarea> | |
30 | + </dd> | |
31 | + </dl> | |
32 | + <div class="actions"> | |
33 | + <button class="ybtn ybtn-success"> | |
34 | + <i class="yobicon-friends"></i> @Messages("organization.create") | |
35 | + </button> | |
36 | + <a href="/" class="ybtn">@Messages("button.cancel")</a> | |
37 | + </div> | |
38 | + </form> | |
39 | + </div> | |
40 | + </div> | |
41 | +</div> | |
42 | +<script type="text/javascript"> | |
43 | + $(document).ready(function() { | |
44 | + $yobi.loadModule("organization.New", { | |
45 | + "sFormName" : "new-org" | |
46 | + }); | |
47 | + }); | |
48 | +</script> | |
49 | +} |
+++ app/views/organization/header.scala.html
... | ... | @@ -0,0 +1,19 @@ |
1 | +@(org:Organization) | |
2 | + | |
3 | +@import utils.TemplateHelper._ | |
4 | +@import utils.JodaDateUtil | |
5 | + | |
6 | +<div class="project-header-outer" style="background-image:url('@urlToOrganizationLogo(org)')"> | |
7 | + <div class="project-header-inner"> | |
8 | + <div class="project-header-wrap"> | |
9 | + <div class="project-header-avatar"> | |
10 | + <img src="@urlToOrganizationLogo(org)" /> | |
11 | + </div> | |
12 | + <div class="project-breadcrumb-wrap"> | |
13 | + <div class="project-breadcrumb"> | |
14 | + <span class="project-author"><a href="@routes.OrganizationApp.organization(org.name)">@org.name</a></span> | |
15 | + </div> | |
16 | + </div> | |
17 | + </div> | |
18 | + </div> | |
19 | +</div> |
+++ app/views/organization/members.scala.html
... | ... | @@ -0,0 +1,78 @@ |
1 | +@(organization:Organization, roles: List[Role]) | |
2 | + | |
3 | +@memberRole(userRole: String, loginId: String, userId: Long) = { | |
4 | +@for(role <- roles){ | |
5 | + <li data-value="@role.id" @if(role.name.equals(userRole)){ data-selected="true" class="active" }><a href="javascript:void(0)" data-action="apply" data-href="@routes.OrganizationApp.editMember(organization.name, userId)" data-loginId="@loginId">@Messages("user.role." + role.name)</a></li> | |
6 | +} | |
7 | +} | |
8 | + | |
9 | +@siteLayout(organization.name, utils.MenuType.NONE) { | |
10 | +@header(organization) | |
11 | +@menu(organization) | |
12 | + | |
13 | +<div class="page-wrap-outer"> | |
14 | + <div class="project-page-wrap"> | |
15 | + @partial_settingmenu(organization) | |
16 | + | |
17 | + <div class="inner-bubble"> | |
18 | + <form class="nm" action="@routes.OrganizationApp.addMember(organization.name)" method="post" id="addNewMember"> | |
19 | + <input type="text" class="text uname" id="loginId" name="loginId" | |
20 | + data-provider="typeahead" autocomplete="off" | |
21 | + placeholder="@Messages("project.members.addMember")" | |
22 | + pattern="^[a-zA-Z0-9-]+([_.][a-zA-Z0-9-]+)*$" title="@Messages("user.wrongloginId.alert")" /><!-- | |
23 | + --><button type="submit" class="ybtn ybtn-success"><i class="yobicon-addfriend"></i> @Messages("button.add")</button> | |
24 | + </form> | |
25 | + </div> | |
26 | + | |
27 | + <ul class="members project row-fluid"> | |
28 | + @for(member <- organization.users){ | |
29 | + @if(member.user != null){ | |
30 | + <li class="member span6"> | |
31 | + <a href="@routes.UserApp.userInfo(member.user.loginId)" class="avatar-wrap mlarge pull-left mr10"> | |
32 | + <img src="@User.findByLoginId(member.user.loginId).avatarUrl" width="64" height="64"> | |
33 | + </a> | |
34 | + <div class="member-name">@member.user.name</div> | |
35 | + <div class="member-id">@{"@"}@member.user.loginId</div> | |
36 | + <div class="member-setting"> | |
37 | + <div class="btn-group" data-name="roleof-@member.user.loginId"> | |
38 | + <button class="btn dropdown-toggle large" data-toggle="dropdown"> | |
39 | + <span class="d-label">@Messages("user.role." + member.role.name)</span> | |
40 | + <span class="d-caret"><span class="caret"></span></span> | |
41 | + </button> | |
42 | + <ul class="dropdown-menu">@memberRole(member.role.name, member.user.loginId, member.user.id)</ul> | |
43 | + </div> | |
44 | + <a href="javascript:void(0)" data-action="delete" data-href="@routes.OrganizationApp.deleteMember(organization.name, member.user.id)" class="ybtn ybtn-danger ybtn-small"> | |
45 | + @Messages("button.delete") | |
46 | + </a> | |
47 | + </div> | |
48 | + </li> | |
49 | + } | |
50 | + } | |
51 | + </ul> | |
52 | + | |
53 | + @** Confirm to delete member **@ | |
54 | + <div id="alertDeletion" class="modal hide"> | |
55 | + <div class="modal-header"> | |
56 | + <button type="button" class="close" data-dismiss="modal">×</button> | |
57 | + <h3>@Messages("organization.member.delete")</h3> | |
58 | + </div> | |
59 | + <div class="modal-body"> | |
60 | + <p>@Messages("organization.member.deleteConfirm")</p> | |
61 | + </div> | |
62 | + <div class="modal-footer"> | |
63 | + <button type="button" class="ybtn ybtn-info ybtn-mini" id="deleteBtn">@Messages("button.yes")</button> | |
64 | + <button type="button" class="ybtn ybtn-mini" data-dismiss="modal">@Messages("button.no")</button> | |
65 | + </div> | |
66 | + </div> | |
67 | + </div> | |
68 | +</div> | |
69 | + | |
70 | +<link rel="stylesheet" type="text/css" media="screen" href="/assets/javascripts/lib/mentionjs/mention.css"> | |
71 | +<script type="text/javascript"> | |
72 | +$(document).ready(function(){ | |
73 | + $yobi.loadModule("organization.Member", { | |
74 | + "sActionURL": "@routes.UserApp.users()" | |
75 | + }); | |
76 | +}); | |
77 | +</script> | |
78 | +} |
+++ app/views/organization/setting.scala.html
... | ... | @@ -0,0 +1,55 @@ |
1 | +@(organization:Organization) | |
2 | + | |
3 | +@import utils.TemplateHelper._ | |
4 | + | |
5 | +@siteLayout(organization.name, utils.MenuType.NONE) { | |
6 | +@header(organization) | |
7 | +@menu(organization) | |
8 | + | |
9 | +<div class="page-wrap-outer"> | |
10 | + <div class="project-page-wrap"> | |
11 | + @partial_settingmenu(organization) | |
12 | + <form id="saveSetting" method="post" action="@routes.OrganizationApp.updateOrganizationInfo(organization.name)" enctype="multipart/form-data" class="nm"> | |
13 | + <input type="hidden" name="id" value="@organization.id"> | |
14 | + <input type="hidden" name="name" value="@organization.name"> | |
15 | + <div class="bubble-wrap gray"> | |
16 | + <div class="box-wrap top clearfix frm-wrap" style="padding-top:20px;"> | |
17 | + <div class="setting-box left"> | |
18 | + <div class="logo-wrap" style="background-image:url('@urlToOrganizationLogo(organization)')"></div> | |
19 | + <div class="logo-desc"> | |
20 | + <ul class="unstyled descs"> | |
21 | + <li><strong>@Messages("organization.logo")</strong></li> | |
22 | + <li>@Messages("organization.logo.type") <span class="point">bmp, jpg, gif, png</span></li> | |
23 | + <li>@Messages("organization.logo.maxFileSize") <span class="point">5MB</span></li> | |
24 | + <li> | |
25 | + <div class="btn-wrap"> | |
26 | + <div class="nbtn medium white fake-file-wrap"> | |
27 | + <i class="yobicon-upload"></i> @Messages("button.upload")<input id="logoPath" type="file" class="file" name="logoPath" accept="image/*"> | |
28 | + </div> | |
29 | + </div> | |
30 | + </li> | |
31 | + </ul> | |
32 | + </div> | |
33 | + </div> | |
34 | + <dl class="setting-box right"> | |
35 | + <dt> | |
36 | + <label for="project-desc">@Messages("organization.description.placeholder")</label> | |
37 | + </dt> | |
38 | + <dd> | |
39 | + <textarea id="project-desc" name="descr" maxlength="250" class="textarea">@organization.descr</textarea> | |
40 | + </dd> | |
41 | + </dl> | |
42 | + </div> | |
43 | + </div> | |
44 | + <div class="box-wrap bottom"> | |
45 | + <button id="save" type="submit" class="ybtn ybtn-success">@Messages("button.save")</button> | |
46 | + </div> | |
47 | + </form> | |
48 | + </div> | |
49 | +</div> | |
50 | +<script language="javascript"> | |
51 | + $(function() { | |
52 | + $yobi.loadModule("organization.Setting") | |
53 | + }) | |
54 | +</script> | |
55 | +} |
+++ app/views/organization/view.scala.html
... | ... | @@ -0,0 +1,105 @@ |
1 | +@(org:Organization) | |
2 | + | |
3 | +@import utils.TemplateHelper._ | |
4 | +@import utils.JodaDateUtil | |
5 | + | |
6 | +@siteLayout(org.name, utils.MenuType.NONE) { | |
7 | +@header(org) | |
8 | +@menu(org) | |
9 | + | |
10 | +<div class="page-wrap-outer"> | |
11 | + <div class="project-page-wrap"> | |
12 | + <div class="project-home-header row-fluid"> | |