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

markdown: Add File Uploader and improve somewhat.
* Now markdown editor has file uploader. * The way to specify html elements for markdown changed; Set 'markdown' attribute on the elements instead of pass parameters to markdown.scala.html.
@22901be04357dcea25db4ce6df59c1edea462894
+++ app/controllers/AttachmentApp.java
... | ... | @@ -0,0 +1,228 @@ |
1 | +package controllers; | |
2 | + | |
3 | +import static play.libs.Json.toJson; | |
4 | + | |
5 | +import java.io.BufferedInputStream; | |
6 | +import java.io.File; | |
7 | +import java.io.FileInputStream; | |
8 | +import java.io.IOException; | |
9 | +import java.net.URLEncoder; | |
10 | +import java.security.DigestInputStream; | |
11 | +import java.security.MessageDigest; | |
12 | +import java.security.NoSuchAlgorithmException; | |
13 | +import java.util.ArrayList; | |
14 | +import java.util.Formatter; | |
15 | +import java.util.HashMap; | |
16 | +import java.util.List; | |
17 | +import java.util.Map; | |
18 | + | |
19 | +import javax.mail.internet.MimeUtility; | |
20 | + | |
21 | +import models.Attachment; | |
22 | +import models.enumeration.Operation; | |
23 | +import models.enumeration.Resource; | |
24 | + | |
25 | +import org.apache.tika.Tika; | |
26 | +import org.codehaus.jackson.JsonNode; | |
27 | + | |
28 | +import play.Logger; | |
29 | +import play.mvc.Controller; | |
30 | +import play.mvc.Http.MultipartFormData; | |
31 | +import play.mvc.Http.MultipartFormData.FilePart; | |
32 | +import play.mvc.Http.Request; | |
33 | +import play.mvc.Result; | |
34 | +import utils.AccessControl; | |
35 | +import utils.RequestUtil; | |
36 | + | |
37 | +public class AttachmentApp extends Controller { | |
38 | + | |
39 | + public static Result newFile() throws NoSuchAlgorithmException, IOException { | |
40 | + FilePart filePart = request().body().asMultipartFormData().getFile("filePath"); | |
41 | + | |
42 | + // Currently, anonymous cannot upload a file. | |
43 | + if (UserApp.currentUser() == UserApp.anonymous) { | |
44 | + return forbidden(); | |
45 | + } | |
46 | + | |
47 | + // Keep the file in the user's temporary area. | |
48 | + Attachment attach = new Attachment(); | |
49 | + attach.projectId = 0L; | |
50 | + attach.containerType = Resource.USER; | |
51 | + attach.containerId = UserApp.currentUser().id; | |
52 | + attach.mimeType = new Tika().detect(filePart.getFile()); | |
53 | + attach.name = filePart.getFilename(); | |
54 | + | |
55 | + // Store the file in the filesystem. | |
56 | + String hash = AttachmentApp.saveFile(request()); | |
57 | + attach.hash = hash; | |
58 | + | |
59 | + Attachment sameAttach = Attachment.findBy(attach); | |
60 | + | |
61 | + if (sameAttach == null) { | |
62 | + attach.save(); | |
63 | + } else { | |
64 | + attach = sameAttach; | |
65 | + } | |
66 | + | |
67 | + // The request has been fulfilled and resulted in a new resource being | |
68 | + // created. The newly created resource can be referenced by the URI(s) | |
69 | + // returned in the entity of the response, with the most specific URI | |
70 | + // for the resource given by a Location header field. | |
71 | + // -- RFC 2616, 10.2.2. 201 Created | |
72 | + String url = routes.AttachmentApp.getFile(attach.id, URLEncoder.encode(attach.name, "UTF-8")).url(); | |
73 | + response().setHeader("Location", url); | |
74 | + | |
75 | + // The response SHOULD include an entity containing a list of resource | |
76 | + // characteristics and location(s) from which the user or user agent can | |
77 | + // choose the one most appropriate. -- RFC 2616, 10.2.2. 201 Created | |
78 | + Map<String, String> file = new HashMap<String, String>(); | |
79 | + file.put("id", attach.id.toString()); | |
80 | + file.put("mimeType", attach.mimeType); | |
81 | + file.put("name", attach.name); | |
82 | + file.put("url", url); | |
83 | + JsonNode responseBody = toJson(file); | |
84 | + | |
85 | + // The entity format is specified by the media type given in the | |
86 | + // Content-Type header field. -- RFC 2616, 10.2.2. 201 Created | |
87 | + response().setHeader("Content-Type", "application/json"); | |
88 | + | |
89 | + if (sameAttach == null) { | |
90 | + // If an attachment has been created -- it does NOT mean that | |
91 | + // a file is created in the filesystem -- return 201 Created. | |
92 | + return created(responseBody); | |
93 | + } else { | |
94 | + // If the attachment already exists, return 200 OK. | |
95 | + // Why not 204? -- Because 204 doesn't allow that response has body, | |
96 | + // we cannot tell what is same with the file you try to add. | |
97 | + return ok(responseBody); | |
98 | + } | |
99 | + } | |
100 | + | |
101 | + public static Result getFile(Long id, String filename) | |
102 | + throws NoSuchAlgorithmException, IOException { | |
103 | + Attachment attachment = Attachment.findById(id); | |
104 | + | |
105 | + if (attachment == null) { | |
106 | + return notFound(); | |
107 | + } | |
108 | + | |
109 | + if (!AccessControl.isAllowed(UserApp.currentUser().id, attachment.projectId, attachment.containerType, Operation.READ, attachment.containerId)) { | |
110 | + return forbidden(); | |
111 | + } | |
112 | + | |
113 | + File file = new File("public/uploadFiles/" + attachment.hash); | |
114 | + | |
115 | + // RFC 2231; IE 8 or less, and Safari 5 or less are not supported. | |
116 | + filename = attachment.name.replaceAll("[:\\x5c\\/{?]", "_"); | |
117 | + filename = "filename*=UTF-8''" + URLEncoder.encode(filename, "UTF-8"); | |
118 | + | |
119 | + response().setHeader("Content-Length", Long.toString(file.length())); | |
120 | + response().setHeader("Content-Type", attachment.mimeType); | |
121 | + response().setHeader("Content-Disposition", "attachment; " + filename); | |
122 | + | |
123 | + return ok(file); | |
124 | + } | |
125 | + | |
126 | + public static Result deleteFile(Long id, String filename) throws NoSuchAlgorithmException, IOException { | |
127 | + // _method must be 'delete' | |
128 | + Map<String, String[]> data = request().body().asMultipartFormData().asFormUrlEncoded(); | |
129 | + if (!RequestUtil.getFirstValueFromQuery(data, "_method").toLowerCase().equals("delete")) { | |
130 | + return badRequest("_method must be 'delete'."); | |
131 | + } | |
132 | + | |
133 | + // Remove the attachment. | |
134 | + Attachment attach = Attachment.findById(id); | |
135 | + if (attach == null) { | |
136 | + return notFound(); | |
137 | + } | |
138 | + attach.delete(); | |
139 | + | |
140 | + // Delete the file matched with the attachment, | |
141 | + // if and only if no attachment refer the file. | |
142 | + if (Attachment.exists(attach.hash)) { | |
143 | + return ok("The attachment is removed successfully, but the origin file still exists because it is referred by somewhere."); | |
144 | + } | |
145 | + | |
146 | + boolean result = new File("public/uploadFiles/" + attach.hash).delete(); | |
147 | + | |
148 | + if (result) { | |
149 | + return ok("The both of attachment and origin file is removed successfully."); | |
150 | + } else { | |
151 | + return status(202, "The attachment is removed successfully, but the origin file still exists abnormally even if it is referred by nowhere."); | |
152 | + } | |
153 | + } | |
154 | + | |
155 | + /** | |
156 | + * From BoardApp | |
157 | + * | |
158 | + * @param request | |
159 | + * @return | |
160 | + * @throws NoSuchAlgorithmException | |
161 | + * @throws IOException | |
162 | + */ | |
163 | + static String saveFile(Request request) throws NoSuchAlgorithmException, IOException { | |
164 | + MultipartFormData body = request.body().asMultipartFormData(); | |
165 | + FilePart filePart = body.getFile("filePath"); | |
166 | + | |
167 | + if (filePart != null) { | |
168 | + MessageDigest algorithm = MessageDigest.getInstance("SHA1"); | |
169 | + DigestInputStream dis = new DigestInputStream(new BufferedInputStream(new FileInputStream(filePart.getFile())), algorithm); | |
170 | + while(dis.read() != -1); | |
171 | + Formatter formatter = new Formatter(); | |
172 | + for (byte b : algorithm.digest()) { | |
173 | + formatter.format("%02x", b); | |
174 | + } | |
175 | + File saveFile = new File("public/uploadFiles/" + formatter.toString()); | |
176 | + filePart.getFile().renameTo(saveFile); | |
177 | + String hash = formatter.toString(); | |
178 | + | |
179 | + formatter.close(); | |
180 | + dis.close(); | |
181 | + | |
182 | + return hash; | |
183 | + } | |
184 | + return null; | |
185 | + } | |
186 | + | |
187 | + public static Map<String, String> fileAsMap(Attachment attach) { | |
188 | + Map<String, String> file = new HashMap<String, String>(); | |
189 | + | |
190 | + file.put("id", attach.id.toString()); | |
191 | + file.put("mimeType", attach.mimeType); | |
192 | + file.put("name", attach.name); | |
193 | + file.put("url", routes.AttachmentApp.getFile(attach.id, attach.name).url()); | |
194 | + | |
195 | + return file; | |
196 | + } | |
197 | + | |
198 | + public static Result getFileList() { | |
199 | + Map<String, List<Map<String, String>>> files = new HashMap<String, List<Map<String, String>>>(); | |
200 | + | |
201 | + List<Map<String, String>> tempFiles = new ArrayList<Map<String, String>>(); | |
202 | + for (Attachment attach : Attachment.findTempFiles(UserApp.currentUser().id)) { | |
203 | + tempFiles.add(fileAsMap(attach)); | |
204 | + } | |
205 | + files.put("tempFiles", tempFiles); | |
206 | + | |
207 | + Map<String, String[]> query = request().queryString(); | |
208 | + String containerType = RequestUtil.getFirstValueFromQuery(query, "containerType"); | |
209 | + String containerId = RequestUtil.getFirstValueFromQuery(query, "containerId"); | |
210 | + | |
211 | + if (containerType != null && containerId != null) { | |
212 | + List<Map<String, String>> attachments = new ArrayList<Map<String, String>>(); | |
213 | + for (Attachment attach : Attachment.findByContainer(Resource.valueOf(containerType), Long.parseLong(containerId))) { | |
214 | + if (!AccessControl.isAllowed(UserApp.currentUser().id, attach.projectId, attach.containerType, Operation.READ, attach.containerId)) { | |
215 | + return forbidden(); | |
216 | + } | |
217 | + attachments.add(fileAsMap(attach)); | |
218 | + } | |
219 | + files.put("attachments", attachments); | |
220 | + } | |
221 | + | |
222 | + JsonNode responseBody = toJson(files); | |
223 | + | |
224 | + response().setHeader("Content-Type", "application/json"); | |
225 | + | |
226 | + return ok(responseBody); | |
227 | + } | |
228 | +}(파일 끝에 줄바꿈 문자 없음) |
--- app/controllers/IssueApp.java
+++ app/controllers/IssueApp.java
... | ... | @@ -10,8 +10,6 @@ |
10 | 10 |
import models.support.*; |
11 | 11 |
import play.data.*; |
12 | 12 |
import play.mvc.*; |
13 |
-import play.mvc.Http.*; |
|
14 |
-import play.mvc.Http.MultipartFormData.*; |
|
15 | 13 |
import utils.*; |
16 | 14 |
import views.html.issue.*; |
17 | 15 |
|
... | ... | @@ -35,7 +33,7 @@ |
35 | 33 |
Page<Issue> issues = Issue.find(project.name, issueParam.pageNum, |
36 | 34 |
StateType.getValue(stateType), issueParam.sortBy, |
37 | 35 |
Direction.getValue(issueParam.orderBy), issueParam.filter, issueParam.milestone, |
38 |
- issueParam.commentedCheck, issueParam.fileAttachedCheck); |
|
36 |
+ issueParam.commentedCheck); |
|
39 | 37 |
return ok(issueList.render("title.issueList", issues, issueParam, project)); |
40 | 38 |
} |
41 | 39 |
|
... | ... | @@ -57,9 +55,9 @@ |
57 | 55 |
return ok(newIssue.render("title.newIssue", new Form<Issue>(Issue.class), project)); |
58 | 56 |
} |
59 | 57 |
|
60 |
- public static Result saveIssue(String userName, String projectName) { |
|
58 |
+ public static Result saveIssue(String ownerName, String projectName) throws IOException { |
|
61 | 59 |
Form<Issue> issueForm = new Form<Issue>(Issue.class).bindFromRequest(); |
62 |
- Project project = ProjectApp.getProject(userName, projectName); |
|
60 |
+ Project project = ProjectApp.getProject(ownerName, projectName); |
|
63 | 61 |
if (issueForm.hasErrors()) { |
64 | 62 |
return badRequest(newIssue.render(issueForm.errors().toString(), issueForm, project)); |
65 | 63 |
} else { |
... | ... | @@ -69,9 +67,10 @@ |
69 | 67 |
newIssue.project = project; |
70 | 68 |
newIssue.state = IssueState.ENROLLED; |
71 | 69 |
newIssue.updateStateType(newIssue); |
72 |
- newIssue.filePath = saveFile(request()); |
|
73 |
- Issue.create(newIssue); |
|
70 |
+ Long issueId = Issue.create(newIssue); |
|
74 | 71 |
|
72 |
+ // Attach all of the files in the current user's temporary storage. |
|
73 |
+ Attachment.moveTempFiles(UserApp.currentUser().id, project.id, Resource.ISSUE_POST, issueId); |
|
75 | 74 |
} |
76 | 75 |
return redirect(routes.IssueApp.issues(project.owner, project.name, |
77 | 76 |
StateType.ALL.stateType())); |
... | ... | @@ -84,7 +83,7 @@ |
84 | 83 |
return ok(editIssue.render("title.editIssue", editForm, id, project)); |
85 | 84 |
} |
86 | 85 |
|
87 |
- public static Result updateIssue(String userName, String projectName, Long id) { |
|
86 |
+ public static Result updateIssue(String userName, String projectName, Long id) throws IOException { |
|
88 | 87 |
Form<Issue> issueForm = new Form<Issue>(Issue.class).bindFromRequest(); |
89 | 88 |
Project project = ProjectApp.getProject(userName, projectName); |
90 | 89 |
if (issueForm.hasErrors()) { |
... | ... | @@ -93,10 +92,12 @@ |
93 | 92 |
Issue issue = issueForm.get(); |
94 | 93 |
issue.id = id; |
95 | 94 |
issue.date = Issue.findById(id).date; |
96 |
- issue.filePath = saveFile(request()); |
|
97 | 95 |
issue.project = project; |
98 | 96 |
issue.updateState(issue); |
99 | 97 |
Issue.edit(issue); |
98 |
+ |
|
99 |
+ // Attach the files in the current user's temporary storage. |
|
100 |
+ Attachment.moveTempFiles(UserApp.currentUser().id, project.id, Resource.ISSUE_POST, id); |
|
100 | 101 |
} |
101 | 102 |
return redirect(routes.IssueApp.issues(project.owner, project.name, StateType.ALL.name())); |
102 | 103 |
} |
... | ... | @@ -109,7 +110,7 @@ |
109 | 110 |
StateType.ALL.stateType())); |
110 | 111 |
} |
111 | 112 |
|
112 |
- public static Result saveComment(String userName, String projectName, Long issueId) { |
|
113 |
+ public static Result saveComment(String userName, String projectName, Long issueId) throws IOException { |
|
113 | 114 |
Form<IssueComment> commentForm = new Form<IssueComment>(IssueComment.class) |
114 | 115 |
.bindFromRequest(); |
115 | 116 |
Project project = ProjectApp.getProject(userName, projectName); |
... | ... | @@ -121,9 +122,12 @@ |
121 | 122 |
comment.issue = Issue.findById(issueId); |
122 | 123 |
comment.authorId = UserApp.currentUser().id; |
123 | 124 |
comment.authorName = UserApp.currentUser().name; |
124 |
- comment.filePath = saveFile(request()); |
|
125 |
- IssueComment.create(comment); |
|
125 |
+ Long commentId = IssueComment.create(comment); |
|
126 | 126 |
Issue.updateNumOfComments(issueId); |
127 |
+ |
|
128 |
+ // Attach all of the files in the current user's temporary storage. |
|
129 |
+ Attachment.moveTempFiles(UserApp.currentUser().id, project.id, Resource.ISSUE_COMMENT, commentId); |
|
130 |
+ |
|
127 | 131 |
return redirect(routes.IssueApp.issue(project.owner, project.name, issueId)); |
128 | 132 |
} |
129 | 133 |
} |
... | ... | @@ -144,7 +148,7 @@ |
144 | 148 |
Page<Issue> issues = Issue.find(project.name, issueParam.pageNum, |
145 | 149 |
StateType.getValue(stateType), issueParam.sortBy, |
146 | 150 |
Direction.getValue(issueParam.orderBy), issueParam.filter, issueParam.milestone, |
147 |
- issueParam.commentedCheck, issueParam.fileAttachedCheck); |
|
151 |
+ issueParam.commentedCheck); |
|
148 | 152 |
Issue.excelSave(issues.getList(), project.name + "_" + stateType + "_filter_" |
149 | 153 |
+ issueParam.filter + "_milestone_" + issueParam.milestone); |
150 | 154 |
return ok(issueList.render("title.issueList", issues, issueParam, project)); |
... | ... | @@ -155,26 +159,7 @@ |
155 | 159 |
return TODO; |
156 | 160 |
} |
157 | 161 |
|
158 |
- /** |
|
159 |
- * From BoardApp |
|
160 |
- * |
|
161 |
- * @param request |
|
162 |
- * @return |
|
163 |
- */ |
|
164 |
- private static String saveFile(Request request) { |
|
165 |
- MultipartFormData body = request.body().asMultipartFormData(); |
|
166 |
- |
|
167 |
- FilePart filePart = body.getFile("filePath"); |
|
168 |
- |
|
169 |
- if (filePart != null) { |
|
170 |
- File saveFile = new File("public/uploadFiles/" + filePart.getFilename()); |
|
171 |
- filePart.getFile().renameTo(saveFile); |
|
172 |
- return filePart.getFilename(); |
|
173 |
- } |
|
174 |
- return null; |
|
175 |
- } |
|
176 |
- |
|
177 | 162 |
public static Result getIssueDatil(){ |
178 | 163 |
return TODO; |
179 | 164 |
} |
180 |
-} |
|
165 |
+}(파일 끝에 줄바꿈 문자 없음) |
+++ app/models/Attachment.java
... | ... | @@ -0,0 +1,89 @@ |
1 | +package models; | |
2 | + | |
3 | +import java.util.List; | |
4 | + | |
5 | +import javax.persistence.Entity; | |
6 | +import javax.persistence.EnumType; | |
7 | +import javax.persistence.Enumerated; | |
8 | +import javax.persistence.Id; | |
9 | + | |
10 | +import models.enumeration.Resource; | |
11 | + | |
12 | +import play.data.validation.*; | |
13 | + | |
14 | +import play.db.ebean.Model; | |
15 | + | |
16 | +@Entity | |
17 | +public class Attachment extends Model { | |
18 | + /** | |
19 | + * | |
20 | + */ | |
21 | + private static final long serialVersionUID = 7856282252495067924L; | |
22 | + private static Finder<Long, Attachment> find = new Finder<Long, Attachment>( | |
23 | + Long.class, Attachment.class); | |
24 | + @Id | |
25 | + public Long id; | |
26 | + | |
27 | + @Constraints.Required | |
28 | + public String name; | |
29 | + | |
30 | + @Constraints.Required | |
31 | + public String hash; | |
32 | + | |
33 | + public Long projectId; | |
34 | + | |
35 | + @Enumerated(EnumType.STRING) | |
36 | + public Resource containerType; | |
37 | + | |
38 | + public String mimeType; | |
39 | + | |
40 | + public Long containerId; | |
41 | + | |
42 | + public static List<Attachment> findTempFiles(Long userId) { | |
43 | + return find.where() | |
44 | + .eq("containerType", Resource.USER) | |
45 | + .eq("containerId", userId).findList(); | |
46 | + } | |
47 | + | |
48 | + public static Attachment findBy(Attachment attach) { | |
49 | + return find.where() | |
50 | + .eq("name", attach.name) | |
51 | + .eq("hash", attach.hash) | |
52 | + .eq("projectId", attach.projectId) | |
53 | + .eq("containerType",attach.containerType) | |
54 | + .eq("containerId", attach.containerId).findUnique(); | |
55 | + } | |
56 | + | |
57 | + public static Attachment findBy(String name, String hash, Long projectId, Resource containerType, Long containerId) { | |
58 | + return find.where() | |
59 | + .eq("name", name) | |
60 | + .eq("hash", hash) | |
61 | + .eq("projectId", projectId) | |
62 | + .eq("containerType",containerType) | |
63 | + .eq("containerId", containerId).findUnique(); | |
64 | + } | |
65 | + | |
66 | + public static boolean exists(String hash) { | |
67 | + return find.where().eq("hash", hash).findRowCount() > 0; | |
68 | + } | |
69 | + | |
70 | + public static Attachment findById(Long id) { | |
71 | + return find.byId(id); | |
72 | + } | |
73 | + public static List<Attachment> findByContainer(Resource containerType, Long containerId) { | |
74 | + return find.where() | |
75 | + .eq("containerType", containerType) | |
76 | + .eq("containerId", containerId).findList(); | |
77 | + } | |
78 | + | |
79 | + public static void moveTempFiles(Long userId, Long projectId, Resource containerType, Long containerId) { | |
80 | + // Move the attached files in the temporary area to the issue area. | |
81 | + List<Attachment> attachments = Attachment.findTempFiles(userId); | |
82 | + for(Attachment attachment : attachments) { | |
83 | + attachment.projectId = projectId; | |
84 | + attachment.containerType = containerType; | |
85 | + attachment.containerId = containerId; | |
86 | + attachment.save(); | |
87 | + } | |
88 | + } | |
89 | +}(파일 끝에 줄바꿈 문자 없음) |
--- app/models/Issue.java
+++ app/models/Issue.java
... | ... | @@ -38,7 +38,6 @@ |
38 | 38 |
* @param milestone 이슈가 등록된 마일스톤 |
39 | 39 |
* @param importance 이슈 상세정보의 중요도 |
40 | 40 |
* @param diagnosisResult 이슈 상세정보의 진단유형 |
41 |
- * @param filePath 이슈에 첨부된 파일 주소 |
|
42 | 41 |
* @param osType 이슈 상세정보의 OS 유형 |
43 | 42 |
* @param browserType 이슈 상세정보의 브라우저 유형 |
44 | 43 |
* @param dbmsType 이슈 상세정보의 DBMS 유형 |
... | ... | @@ -79,8 +78,6 @@ |
79 | 78 |
public StateType stateType; |
80 | 79 |
public String issueType; |
81 | 80 |
public String componentName; |
82 |
- // TODO 첨부 파일이 여러개인경우는? |
|
83 |
- public String filePath; |
|
84 | 81 |
public String osType; |
85 | 82 |
public String browserType; |
86 | 83 |
public String dbmsType; |
... | ... | @@ -355,10 +352,6 @@ |
355 | 352 |
* @param issue |
356 | 353 |
*/ |
357 | 354 |
public static void edit(Issue issue) { |
358 |
- Issue previousIssue = findById(issue.id); |
|
359 |
- if (issue.filePath == null) { |
|
360 |
- issue.filePath = previousIssue.filePath; |
|
361 |
- } |
|
362 | 355 |
issue.updateStateType(issue); |
363 | 356 |
issue.update(); |
364 | 357 |
} |
... | ... | @@ -392,7 +385,7 @@ |
392 | 385 |
*/ |
393 | 386 |
public static Page<Issue> findIssues(String projectName, StateType state) { |
394 | 387 |
return find(projectName, FIRST_PAGE_NUMBER, state, DEFAULT_SORTER, Direction.DESC, |
395 |
- "", null, false, false); |
|
388 |
+ "", null, false); |
|
396 | 389 |
} |
397 | 390 |
|
398 | 391 |
/** |
... | ... | @@ -402,13 +395,12 @@ |
402 | 395 |
* @param filter |
403 | 396 |
* @param state |
404 | 397 |
* @param commentedCheck |
405 |
- * @param fileAttachedCheck |
|
406 | 398 |
* @return |
407 | 399 |
*/ |
408 | 400 |
public static Page<Issue> findFilteredIssues(String projectName, String filter, |
409 |
- StateType state, boolean commentedCheck, boolean fileAttachedCheck) { |
|
401 |
+ StateType state, boolean commentedCheck) { |
|
410 | 402 |
return find(projectName, FIRST_PAGE_NUMBER, state, DEFAULT_SORTER, Direction.DESC, |
411 |
- filter, null, commentedCheck, fileAttachedCheck); |
|
403 |
+ filter, null, commentedCheck); |
|
412 | 404 |
} |
413 | 405 |
|
414 | 406 |
/** |
... | ... | @@ -420,20 +412,7 @@ |
420 | 412 |
*/ |
421 | 413 |
public static Page<Issue> findCommentedIssues(String projectName, String filter) { |
422 | 414 |
return find(projectName, FIRST_PAGE_NUMBER, StateType.ALL, DEFAULT_SORTER, |
423 |
- Direction.DESC, filter, null, true, false); |
|
424 |
- } |
|
425 |
- |
|
426 |
- /** |
|
427 |
- * 파일이 첨부된 이슈들만 찾아준다. |
|
428 |
- * |
|
429 |
- * @param projectName |
|
430 |
- * @param filter |
|
431 |
- * @return |
|
432 |
- */ |
|
433 |
- |
|
434 |
- public static Page<Issue> findFileAttachedIssues(String projectName, String filter) { |
|
435 |
- return find(projectName, FIRST_PAGE_NUMBER, StateType.ALL, DEFAULT_SORTER, |
|
436 |
- Direction.DESC, filter, null, false, true); |
|
415 |
+ Direction.DESC, filter, null, true); |
|
437 | 416 |
} |
438 | 417 |
|
439 | 418 |
/** |
... | ... | @@ -445,7 +424,7 @@ |
445 | 424 |
*/ |
446 | 425 |
public static Page<Issue> findIssuesByMilestoneId(String projectName, Long milestoneId) { |
447 | 426 |
return find(projectName, FIRST_PAGE_NUMBER, StateType.ALL, DEFAULT_SORTER, |
448 |
- Direction.DESC, "", milestoneId, false, false); |
|
427 |
+ Direction.DESC, "", milestoneId, false); |
|
449 | 428 |
} |
450 | 429 |
|
451 | 430 |
/** |
... | ... | @@ -459,13 +438,11 @@ |
459 | 438 |
* @param order Sort order(either asc or desc) |
460 | 439 |
* @param filter filter applied on the title column |
461 | 440 |
* @param commentedCheck filter applied on the commetedCheck column, 댓글이 존재하는 이슈만 필터링 |
462 |
- * @param fileAttachedCheck filter applied on the fileAttachedCheck column, 파일이 업로드된 이슈만 |
|
463 |
- * 필터링 |
|
464 | 441 |
* @return 위의 조건에 따라 필터링된 이슈들을 Page로 반환. |
465 | 442 |
*/ |
466 | 443 |
public static Page<Issue> find(String projectName, int pageNumber, StateType state, |
467 | 444 |
String sortBy, Direction order, String filter, Long milestoneId, |
468 |
- boolean commentedCheck, boolean fileAttachedCheck) { |
|
445 |
+ boolean commentedCheck) { |
|
469 | 446 |
OrderParams orderParams = new OrderParams().add(sortBy, order); |
470 | 447 |
SearchParams searchParams = new SearchParams().add("project.name", projectName, |
471 | 448 |
Matching.EQUALS); |
... | ... | @@ -479,9 +456,7 @@ |
479 | 456 |
if (commentedCheck) { |
480 | 457 |
searchParams.add("numOfComments", NUMBER_OF_ONE_MORE_COMMENTS, Matching.GE); |
481 | 458 |
} |
482 |
- if (fileAttachedCheck) { |
|
483 |
- searchParams.add("filePath", "", Matching.NOT_EQUALS); |
|
484 |
- } |
|
459 |
+ |
|
485 | 460 |
if (state == null) { |
486 | 461 |
state = StateType.ALL; |
487 | 462 |
} |
... | ... | @@ -566,7 +541,7 @@ |
566 | 541 |
sheet.setColumnView(i, 20); |
567 | 542 |
} |
568 | 543 |
for (int i = 1; i < resultList.size() + 1; i++) { |
569 |
- Issue issue = (Issue) resultList.get(i - 1); |
|
544 |
+ Issue issue = resultList.get(i - 1); |
|
570 | 545 |
int colcnt = 0; |
571 | 546 |
sheet.addCell(new Label(colcnt++, i, issue.id.toString(), cf2)); |
572 | 547 |
sheet.addCell(new Label(colcnt++, i, issue.state.toString(), cf2)); |
--- app/models/enumeration/Resource.java
+++ app/models/enumeration/Resource.java
... | ... | @@ -1,27 +1,28 @@ |
1 | 1 |
package models.enumeration; |
2 | 2 |
|
3 | 3 |
public enum Resource { |
4 |
- ISSUE_POST("issue_post"), |
|
5 |
- ISSUE_COMMENT("issue_comment"), |
|
4 |
+ ISSUE_POST("issue_post"), |
|
5 |
+ ISSUE_COMMENT("issue_comment"), |
|
6 | 6 |
ISSUE_ENVIRONMENT("issue_environment"), |
7 | 7 |
ISSUE_ASSIGNEE("issue_assignee"), |
8 |
- ISSUE_STATE("issue_state"), |
|
9 |
- ISSUE_IMPORTANCE("issue_importance"), |
|
10 |
- ISSUE_CATEGORY("issue_category"), |
|
11 |
- ISSUE_MILESTONE("issue_milestone"), |
|
12 |
- ISSUE_COMPONENT("issue_component"), |
|
13 |
- ISSUE_DIAGNOSISRESULT("issue_diagnosisResult"), |
|
14 |
- ISSUE_NOTICE("issue_notice"), |
|
8 |
+ ISSUE_STATE("issue_state"), |
|
9 |
+ ISSUE_IMPORTANCE("issue_importance"), |
|
10 |
+ ISSUE_CATEGORY("issue_category"), |
|
11 |
+ ISSUE_MILESTONE("issue_milestone"), |
|
12 |
+ ISSUE_COMPONENT("issue_component"), |
|
13 |
+ ISSUE_DIAGNOSISRESULT("issue_diagnosisResult"), |
|
14 |
+ ISSUE_NOTICE("issue_notice"), |
|
15 | 15 |
BOARD_POST("board_post"), |
16 |
- BOARD_COMMENT("board_comment"), |
|
17 |
- BOARD_CATEGORY("board_category"), |
|
18 |
- BOARD_NOTICE("board_notice"), |
|
19 |
- CODE("code"), |
|
20 |
- MILESTONE("milestone"), |
|
21 |
- WIKI_PAGE("wiki_page"), |
|
22 |
- PROJECT_SETTING("project_setting"), |
|
23 |
- SITE_SETTING("site_setting"); |
|
24 |
- |
|
16 |
+ BOARD_COMMENT("board_comment"), |
|
17 |
+ BOARD_CATEGORY("board_category"), |
|
18 |
+ BOARD_NOTICE("board_notice"), |
|
19 |
+ CODE("code"), |
|
20 |
+ MILESTONE("milestone"), |
|
21 |
+ WIKI_PAGE("wiki_page"), |
|
22 |
+ PROJECT_SETTING("project_setting"), |
|
23 |
+ SITE_SETTING("site_setting"), |
|
24 |
+ USER("user"); |
|
25 |
+ |
|
25 | 26 |
private String resource; |
26 | 27 |
|
27 | 28 |
Resource(String resource) { |
--- app/views/issue/editIssue.scala.html
+++ app/views/issue/editIssue.scala.html
... | ... | @@ -2,6 +2,7 @@ |
2 | 2 |
@import helper._ |
3 | 3 |
@import scala.collection.mutable.Map |
4 | 4 |
@implicitFieldConstructor = @{ FieldConstructor(twitterBootstrapInput.render) } |
5 |
+@import models.enumeration.Resource |
|
5 | 6 |
|
6 | 7 |
@isVisible(resource: models.enumeration.Resource)(content: => Html) = @{ |
7 | 8 |
roleCheck(session.get("userId"), project.id, resource, models.enumeration.Operation.EDIT){ |
... | ... | @@ -30,11 +31,11 @@ |
30 | 31 |
'_showConstraints -> false, |
31 | 32 |
'_label-> Messages("post.new.contents"), |
32 | 33 |
'rows -> 16, |
33 |
- 'class -> "input-xxlarge textbody") |
|
34 |
+ 'class -> "input-xxlarge textbody", |
|
35 |
+ 'resourceType -> Resource.ISSUE_POST, |
|
36 |
+ 'markdown -> true, |
|
37 |
+ 'resourceId -> issueId) |
|
34 | 38 |
|
35 |
- @inputFile( |
|
36 |
- issueForm("filePath"), |
|
37 |
- '_label -> Messages("post.new.filePath")) |
|
38 | 39 |
</fieldset> |
39 | 40 |
</br></br> |
40 | 41 |
|
... | ... | @@ -139,7 +140,7 @@ |
139 | 140 |
|
140 | 141 |
@board.postVaildate() |
141 | 142 |
|
142 |
-@views.html.markdown(Map("#body" -> "edit")) |
|
143 |
+@views.html.markdown() |
|
143 | 144 |
<script type="text/javascript"> |
144 | 145 |
nforge.require('shortcut.submit'); |
145 | 146 |
</script> |
--- app/views/issue/issue.scala.html
+++ app/views/issue/issue.scala.html
... | ... | @@ -1,6 +1,8 @@ |
1 | 1 |
@(title:String, issue:Issue, issueForm:Form[Issue], commentForm:Form[IssueComment],project:Project) |
2 | 2 |
@import helper._ |
3 | 3 |
@import scala.collection.mutable.Map |
4 |
+@import models.enumeration.Resource |
|
5 |
+ |
|
4 | 6 |
@implicitFieldConstructor = @{ FieldConstructor(twitterBootstrapInput.render) } |
5 | 7 |
|
6 | 8 |
@isVisible(resource: models.enumeration.Resource)(content: => Html) = @{ |
... | ... | @@ -43,13 +45,7 @@ |
43 | 45 |
</div> |
44 | 46 |
<div class="span11"> |
45 | 47 |
<div> |
46 |
- <div id="body">@issue.body</div> |
|
47 |
- @if(issue.filePath != null){ |
|
48 |
- <p> |
|
49 |
- <a href='@routes.Assets.at("uploadFiles/" + issue.filePath)'><i |
|
50 |
- class="icon-download"></i>@issue.filePath</a> |
|
51 |
- </p> |
|
52 |
- } |
|
48 |
+ <div id="body" markdown resourceType=@Resource.ISSUE_POST resourceId="@issue.id">@issue.body</div> |
|
53 | 49 |
</div> |
54 | 50 |
</div> |
55 | 51 |
<a class="btn pull-right" href=""><i class="icon-ok"></i>@Messages("button.autoNotification")</a> |
... | ... | @@ -167,13 +163,7 @@ |
167 | 163 |
</div> |
168 | 164 |
</div> |
169 | 165 |
} |
170 |
- <pre>@comment.contents</pre> |
|
171 |
- @if(comment.filePath != null){ |
|
172 |
- <p> |
|
173 |
- <a href='@routes.Assets.at("uploadFiles/" + comment.filePath)'><i |
|
174 |
- class="icon-download"> </i> @comment.filePath</a> |
|
175 |
- </p> |
|
176 |
- } |
|
166 |
+ <div markdown resourceType=@Resource.ISSUE_COMMENT resourceId=@comment.id>@comment.contents</div> |
|
177 | 167 |
</div> |
178 | 168 |
</div> |
179 | 169 |
} |
... | ... | @@ -184,14 +174,12 @@ |
184 | 174 |
'enctype -> "multipart/form-data", |
185 | 175 |
'class -> "form-horizontal"){ |
186 | 176 |
<div class="span12"> |
187 |
- @textarea(commentForm("contents"), |
|
188 |
- '_label-> Messages("post.new.contents") |
|
189 |
- ) |
|
177 |
+ @textarea(commentForm("contents"), |
|
178 |
+ '_label-> Messages("post.new.contents"), |
|
179 |
+ 'rows -> 5, |
|
180 |
+ 'markdown -> true, |
|
181 |
+ 'class -> "input-xxlarge textbody") |
|
190 | 182 |
</div> |
191 |
- <div class="span6"> |
|
192 |
- @inputFile(commentForm("filePath"), |
|
193 |
- '_label -> Messages("post.new.filePath")) |
|
194 |
- </div> |
|
195 | 183 |
<div class="span6"> |
196 | 184 |
<input class="btn pull-right" type="submit" value=@Messages( "button.comment.new") /> |
197 | 185 |
</div> |
... | ... | @@ -214,5 +202,5 @@ |
214 | 202 |
</div> |
215 | 203 |
</div> |
216 | 204 |
|
217 |
-@views.html.markdown(Map("#body" -> "render")) |
|
205 |
+@views.html.markdown() |
|
218 | 206 |
} |
--- app/views/issue/issueList.scala.html
+++ app/views/issue/issueList.scala.html
... | ... | @@ -58,7 +58,6 @@ |
58 | 58 |
<input type="hidden" name="stateType" value="@param.stateType"> |
59 | 59 |
<div class="control-group"> |
60 | 60 |
<label class="control-label" for="inlineCheckboxes"></label> |
61 |
- |
|
62 | 61 |
<div class="controls inline"> |
63 | 62 |
<fieldset> |
64 | 63 |
<!--FIXME view 전문가님이 아래의 if-else문 안의 중복된 코드를 업그레이드 해주시길 바랍니다. --> |
... | ... | @@ -73,7 +72,6 @@ |
73 | 72 |
</div> |
74 | 73 |
</form> |
75 | 74 |
</div> |
76 |
- |
|
77 | 75 |
<table class="table"> |
78 | 76 |
<thead> |
79 | 77 |
<tr> |
... | ... | @@ -85,12 +83,12 @@ |
85 | 83 |
</tr> |
86 | 84 |
</thead> |
87 | 85 |
<tbody> |
88 |
- |
|
89 | 86 |
@for(issue <- currentPage.getList()){ |
90 | 87 |
<tr> |
91 | 88 |
<td>@issue.id</td> |
92 | 89 |
<td>@Messages(issue.state.state)</td> |
93 |
- <td><a href="@routes.IssueApp.issue(project.owner, project.name, issue.id)">@issue.title @if(issue.comments.size > 0){[@issue.comments.size]} @if(issue.filePath != null){<i class="icon-file"></i>}</a></td> |
|
90 |
+ <td><a href="@routes.IssueApp.issue(project.owner, project.name, issue.id)">@issue.title</a></td> |
|
91 |
+ <td><a href="@routes.IssueApp.issue(project.owner, project.name, issue.id)"></a></td> |
|
94 | 92 |
<td> |
95 | 93 |
@if(issue.assigneeId == null){ |
96 | 94 |
<em>@Messages("issue.noAssignee")</em> |
--- app/views/issue/newIssue.scala.html
+++ app/views/issue/newIssue.scala.html
... | ... | @@ -21,29 +21,16 @@ |
21 | 21 |
@inputText( |
22 | 22 |
issueForm("title"), |
23 | 23 |
'_showConstraints -> false, |
24 |
- '_label-> Messages("post.new.title")) |
|
24 |
+ '_label-> Messages("post.new.title"), |
|
25 |
+ 'class -> "input-xxlarge") |
|
25 | 26 |
@textarea( |
26 | 27 |
issueForm("body"), |
27 | 28 |
'_showConstraints -> false, |
28 |
- '_label-> Messages("post.new.contents")) |
|
29 |
+ '_label-> Messages("post.new.contents"), |
|
30 |
+ 'rows -> 16, |
|
31 |
+ 'class -> "input-xxlarge textbody", |
|
32 |
+ 'markdown -> true) |
|
29 | 33 |
</fieldset> |
30 |
- |
|
31 |
- <i class = "icon-file"></i>@Messages("post.new.fileAttach") <a id="fileUpload" data-toggle="modal" href="#askFilePath" class="btn">@Messages("button.selectFile")</a> |
|
32 |
- <div class="modal hide" id="askFilePath"> |
|
33 |
- <div class="modal-header"> |
|
34 |
- <button type="button" class="close" data-dismiss="modal">@Messages("button.popup.exit")</button> |
|
35 |
- <h3>@Messages("post.popup.fileAttach.title")</h3> |
|
36 |
- </div> |
|
37 |
- <div class="modal-body"> |
|
38 |
- <p> @Messages("post.popup.fileAttach.contents")</p> |
|
39 |
- @inputFile(issueForm("filePath")) |
|
40 |
- </div> |
|
41 |
- <div class="modal-footer"> |
|
42 |
- <a href="#" class="btn" data-dismiss="modal">@Messages("button.cancel")</a> |
|
43 |
- <a href="#" class="btn btn-primary" data-dismiss="modal">@Messages("button.confirm")</a> |
|
44 |
- </div> |
|
45 |
- </div> |
|
46 |
- </br></br> |
|
47 | 34 |
|
48 | 35 |
<fieldset> |
49 | 36 |
<div class="well"> |
... | ... | @@ -145,7 +132,7 @@ |
145 | 132 |
</div> |
146 | 133 |
} |
147 | 134 |
|
148 |
-@views.html.markdown(Map("#body" -> "edit")) |
|
135 |
+@views.html.markdown() |
|
149 | 136 |
<script type="text/javascript"> |
150 | 137 |
nforge.require('shortcut.submit'); |
151 | 138 |
</script> |
--- app/views/markdown.scala.html
+++ app/views/markdown.scala.html
... | ... | @@ -1,4 +1,3 @@ |
1 |
-@(targets: Map[String, String]) |
|
2 | 1 |
@import utils.TemplateHelper._ |
3 | 2 |
<style> |
4 | 3 |
@@IMPORT url(@getCSSLink("hljsstyles/googlecode")); |
... | ... | @@ -6,9 +5,7 @@ |
6 | 5 |
<script src="@getJSLink("showdown")" type="text/javascript"></script> |
7 | 6 |
<script src="@getJSLink("hljs")" type="text/javascript"></script> |
8 | 7 |
<script src="@getJSLink("languages/allinone")" type="text/javascript"></script> |
8 |
+<script src="@getJSLink("jquery.form")" type="text/javascript"></script> |
|
9 | 9 |
<script type="text/javascript"> |
10 |
- @applyMarkdown(key: String) = { |
|
11 |
- nforge.require("markdown.@targets.get(key)", "@key"); |
|
12 |
- } |
|
13 |
- @targets.keys.map(applyMarkdown) |
|
10 |
+ nforge.require("markdown.enable", $("[markdown]"), "@routes.AttachmentApp.newFile"); |
|
14 | 11 |
</script> |
--- app/views/search/issueContentsSearch.scala.html
+++ app/views/search/issueContentsSearch.scala.html
... | ... | @@ -5,8 +5,8 @@ |
5 | 5 |
<tr> |
6 | 6 |
<td>@issue.id</td> |
7 | 7 |
<td>@Messages(issue.state.state)</td> |
8 |
- <td><a href="@routes.IssueApp.issue(project.owner, project.name, issue.id)">@issue.title @if(issue.comments.size > |
|
9 |
- 0){[@issue.comments.size]} @if(issue.filePath != null){<i class="icon-file"></i>}</a></td> |
|
8 |
+ <td><a href="@routes.IssueApp.issue(project.owner, project.name, issue.id)"> |
|
9 |
+ @issue.title @if(issue.comments.size > 0){[@issue.comments.size]}</a></td> |
|
10 | 10 |
<td> |
11 | 11 |
@if(issue.assigneeId == null){ |
12 | 12 |
<em>@Messages("issue.noAssignee")</em> |
--- conf/initial-data.yml
+++ conf/initial-data.yml
... | ... | @@ -103,7 +103,6 @@ |
103 | 103 |
milestoneId: 1 |
104 | 104 |
project: !!models.Project |
105 | 105 |
id: 1 |
106 |
- filePath: FakeFilePath/To/Check/FileIcon |
|
107 | 106 |
- !!models.Issue |
108 | 107 |
authorId: 3 |
109 | 108 |
assigneeId: 2 |
--- conf/routes
+++ conf/routes
... | ... | @@ -75,6 +75,12 @@ |
75 | 75 |
POST /:userName/:projectName/issues/:id/edit controllers.IssueApp.updateIssue(userName:String, projectName:String, id:Long) |
76 | 76 |
GET /issuedetail controllers.IssueApp.getIssueDatil() |
77 | 77 |
|
78 |
+# Attachments |
|
79 |
+GET /files controllers.AttachmentApp.getFileList() |
|
80 |
+POST /files controllers.AttachmentApp.newFile() |
|
81 |
+GET /files/:id/:filename controllers.AttachmentApp.getFile(id: Long, filename: String) |
|
82 |
+POST /files/:id/:filename controllers.AttachmentApp.deleteFile(id: Long, filename: String) |
|
83 |
+ |
|
78 | 84 |
# Git |
79 | 85 |
GET /:ownerName/:projectName/info/refs controllers.GitApp.advertise(ownerName:String, projectName:String) |
80 | 86 |
POST /:ownerName/:projectName/$service<git-upload-pack|git-receive-pack> controllers.GitApp.serviceRpc(ownerName:String, projectName:String, service:String) |
--- project/Build.scala
+++ project/Build.scala
... | ... | @@ -28,7 +28,8 @@ |
28 | 28 |
"commons-codec" % "commons-codec" % "1.2", |
29 | 29 |
// apache-mails |
30 | 30 |
"org.apache.commons" % "commons-email" % "1.2", |
31 |
- "commons-lang" % "commons-lang" % "2.6" |
|
31 |
+ "commons-lang" % "commons-lang" % "2.6", |
|
32 |
+ "org.apache.tika" % "tika-core" % "1.2" |
|
32 | 33 |
) |
33 | 34 |
|
34 | 35 |
val main = PlayProject(appName, appVersion, appDependencies, mainLang = JAVA).settings( |
+++ public/javascripts/jquery.form.js
... | ... | @@ -0,0 +1,1076 @@ |
1 | +/*! | |
2 | + * jQuery Form Plugin | |
3 | + * version: 3.09 (16-APR-2012) | |
4 | + * @requires jQuery v1.3.2 or later | |
5 | + * | |
6 | + * Examples and documentation at: http://malsup.com/jquery/form/ | |
7 | + * Project repository: https://github.com/malsup/form | |
8 | + * Dual licensed under the MIT and GPL licenses: | |
9 | + * http://malsup.github.com/mit-license.txt | |
10 | + * http://malsup.github.com/gpl-license-v2.txt | |
11 | + */ | |
12 | +/*global ActiveXObject alert */ | |
13 | +;(function($) { | |
14 | +"use strict"; | |
15 | + | |
16 | +/* | |
17 | + Usage Note: | |
18 | + ----------- | |
19 | + Do not use both ajaxSubmit and ajaxForm on the same form. These | |
20 | + functions are mutually exclusive. Use ajaxSubmit if you want | |
21 | + to bind your own submit handler to the form. For example, | |
22 | + | |
23 | + $(document).ready(function() { | |
24 | + $('#myForm').on('submit', function(e) { | |
25 | + e.preventDefault(); // <-- important | |
26 | + $(this).ajaxSubmit({ | |
27 | + target: '#output' | |
28 | + }); | |
29 | + }); | |
30 | + }); | |
31 | + | |
32 | + Use ajaxForm when you want the plugin to manage all the event binding | |
33 | + for you. For example, | |
34 | + | |
35 | + $(document).ready(function() { | |
36 | + $('#myForm').ajaxForm({ | |
37 | + target: '#output' | |
38 | + }); | |
39 | + }); | |
40 | + | |
41 | + You can also use ajaxForm with delegation (requires jQuery v1.7+), so the | |
42 | + form does not have to exist when you invoke ajaxForm: | |
43 | + | |
44 | + $('#myForm').ajaxForm({ | |
45 | + delegation: true, | |
46 | + target: '#output' | |
47 | + }); | |
48 | + | |
49 | + When using ajaxForm, the ajaxSubmit function will be invoked for you | |
50 | + at the appropriate time. | |
51 | +*/ | |
52 | + | |
53 | +/** | |
54 | + * Feature detection | |
55 | + */ | |
56 | +var feature = {}; | |
57 | +feature.fileapi = $("<input type='file'/>").get(0).files !== undefined; | |
58 | +feature.formdata = window.FormData !== undefined; | |
59 | + | |
60 | +/** | |
61 | + * ajaxSubmit() provides a mechanism for immediately submitting | |
62 | + * an HTML form using AJAX. | |
63 | + */ | |
64 | +$.fn.ajaxSubmit = function(options) { | |
65 | + /*jshint scripturl:true */ | |
66 | + | |
67 | + // fast fail if nothing selected (http://dev.jquery.com/ticket/2752) | |
68 | + if (!this.length) { | |
69 | + log('ajaxSubmit: skipping submit process - no element selected'); | |
70 | + return this; | |
71 | + } | |
72 | + | |
73 | + var method, action, url, $form = this; | |
74 | + | |
75 | + if (typeof options == 'function') { | |
76 | + options = { success: options }; | |
77 | + } | |
78 | + | |
79 | + method = this.attr('method'); | |
80 | + action = this.attr('action'); | |
81 | + url = (typeof action === 'string') ? $.trim(action) : ''; | |
82 | + url = url || window.location.href || ''; | |
83 | + if (url) { | |
84 | + // clean url (don't include hash vaue) | |
85 | + url = (url.match(/^([^#]+)/)||[])[1]; | |
86 | + } | |
87 | + | |
88 | + options = $.extend(true, { | |
89 | + url: url, | |
90 | + success: $.ajaxSettings.success, | |
91 | + type: method || 'GET', | |
92 | + iframeSrc: /^https/i.test(window.location.href || '') ? 'javascript:false' : 'about:blank' | |
93 | + }, options); | |
94 | + | |
95 | + // hook for manipulating the form data before it is extracted; | |
96 | + // convenient for use with rich editors like tinyMCE or FCKEditor | |
97 | + var veto = {}; | |
98 | + this.trigger('form-pre-serialize', [this, options, veto]); | |
99 | + if (veto.veto) { | |
100 | + log('ajaxSubmit: submit vetoed via form-pre-serialize trigger'); | |
101 | + return this; | |
102 | + } | |
103 | + | |
104 | + // provide opportunity to alter form data before it is serialized | |
105 | + if (options.beforeSerialize && options.beforeSerialize(this, options) === false) { | |
106 | + log('ajaxSubmit: submit aborted via beforeSerialize callback'); | |
107 | + return this; | |
108 | + } | |
109 | + | |
110 | + var traditional = options.traditional; | |
111 | + if ( traditional === undefined ) { | |
112 | + traditional = $.ajaxSettings.traditional; | |
113 | + } | |
114 | + | |
115 | + var elements = []; | |
116 | + var qx, a = this.formToArray(options.semantic, elements); | |
117 | + if (options.data) { | |
118 | + options.extraData = options.data; | |
119 | + qx = $.param(options.data, traditional); | |
120 | + } | |
121 | + | |
122 | + // give pre-submit callback an opportunity to abort the submit | |
123 | + if (options.beforeSubmit && options.beforeSubmit(a, this, options) === false) { | |
124 | + log('ajaxSubmit: submit aborted via beforeSubmit callback'); | |
125 | + return this; | |
126 | + } | |
127 | + | |
128 | + // fire vetoable 'validate' event | |
129 | + this.trigger('form-submit-validate', [a, this, options, veto]); | |
130 | + if (veto.veto) { | |
131 | + log('ajaxSubmit: submit vetoed via form-submit-validate trigger'); | |
132 | + return this; | |
133 | + } | |
134 | + | |
135 | + var q = $.param(a, traditional); | |
136 | + if (qx) { | |
137 | + q = ( q ? (q + '&' + qx) : qx ); | |
138 | + } | |
139 | + if (options.type.toUpperCase() == 'GET') { | |
140 | + options.url += (options.url.indexOf('?') >= 0 ? '&' : '?') + q; | |
141 | + options.data = null; // data is null for 'get' | |
142 | + } | |
143 | + else { | |
144 | + options.data = q; // data is the query string for 'post' | |
145 | + } | |
146 | + | |
147 | + var callbacks = []; | |
148 | + if (options.resetForm) { | |
149 | + callbacks.push(function() { $form.resetForm(); }); | |
150 | + } | |
151 | + if (options.clearForm) { | |
152 | + callbacks.push(function() { $form.clearForm(options.includeHidden); }); | |
153 | + } | |
154 | + | |
155 | + // perform a load on the target only if dataType is not provided | |
156 | + if (!options.dataType && options.target) { | |
157 | + var oldSuccess = options.success || function(){}; | |
158 | + callbacks.push(function(data) { | |
159 | + var fn = options.replaceTarget ? 'replaceWith' : 'html'; | |
160 | + $(options.target)[fn](data).each(oldSuccess, arguments); | |
161 | + }); | |
162 | + } | |
163 | + else if (options.success) { | |
164 | + callbacks.push(options.success); | |
165 | + } | |
166 | + | |
167 | + options.success = function(data, status, xhr) { // jQuery 1.4+ passes xhr as 3rd arg | |
168 | + var context = options.context || options; // jQuery 1.4+ supports scope context | |
169 | + for (var i=0, max=callbacks.length; i < max; i++) { | |
170 | + callbacks[i].apply(context, [data, status, xhr || $form, $form]); | |
171 | + } | |
172 | + }; | |
173 | + | |
174 | + // are there files to upload? | |
175 | + var fileInputs = $('input:file:enabled[value]', this); // [value] (issue #113) | |
176 | + var hasFileInputs = fileInputs.length > 0; | |
177 | + var mp = 'multipart/form-data'; | |
178 | + var multipart = ($form.attr('enctype') == mp || $form.attr('encoding') == mp); | |
179 | + | |
180 | + var fileAPI = feature.fileapi && feature.formdata; | |
181 | + log("fileAPI :" + fileAPI); | |
182 | + var shouldUseFrame = (hasFileInputs || multipart) && !fileAPI; | |
183 | + | |
184 | + // options.iframe allows user to force iframe mode | |
185 | + // 06-NOV-09: now defaulting to iframe mode if file input is detected | |
186 | + if (options.iframe !== false && (options.iframe || shouldUseFrame)) { | |
187 | + // hack to fix Safari hang (thanks to Tim Molendijk for this) | |
188 | + // see: http://groups.google.com/group/jquery-dev/browse_thread/thread/36395b7ab510dd5d | |
189 | + if (options.closeKeepAlive) { | |
190 | + $.get(options.closeKeepAlive, function() { | |
191 | + fileUploadIframe(a); | |
192 | + }); | |
193 | + } | |
194 | + else { | |
195 | + fileUploadIframe(a); | |
196 | + } | |
197 | + } | |
198 | + else if ((hasFileInputs || multipart) && fileAPI) { | |
199 | + fileUploadXhr(a); | |
200 | + } | |
201 | + else { | |
202 | + $.ajax(options); | |
203 | + } | |
204 | + | |
205 | + // clear element array | |
206 | + for (var k=0; k < elements.length; k++) | |
207 | + elements[k] = null; | |
208 | + | |
209 | + // fire 'notify' event | |
210 | + this.trigger('form-submit-notify', [this, options]); | |
211 | + return this; | |
212 | + | |
213 | + // XMLHttpRequest Level 2 file uploads (big hat tip to francois2metz) | |
214 | + function fileUploadXhr(a) { | |
215 | + var formdata = new FormData(); | |
216 | + | |
217 | + for (var i=0; i < a.length; i++) { | |
218 | + formdata.append(a[i].name, a[i].value); | |
219 | + } | |
220 | + | |
221 | + if (options.extraData) { | |
222 | + for (var p in options.extraData) | |
223 | + if (options.extraData.hasOwnProperty(p)) | |
224 | + formdata.append(p, options.extraData[p]); | |
225 | + } | |
226 | + | |
227 | + options.data = null; | |
228 | + | |
229 | + var s = $.extend(true, {}, $.ajaxSettings, options, { | |
230 | + contentType: false, | |
231 | + processData: false, | |
232 | + cache: false, | |
233 | + type: 'POST' | |
234 | + }); | |
235 | + | |
236 | + if (options.uploadProgress) { | |
237 | + // workaround because jqXHR does not expose upload property | |
238 | + s.xhr = function() { | |
239 | + var xhr = jQuery.ajaxSettings.xhr(); | |
240 | + if (xhr.upload) { | |
241 | + xhr.upload.onprogress = function(event) { | |
242 | + var percent = 0; | |
243 | + var position = event.loaded || event.position; /*event.position is deprecated*/ | |
244 | + var total = event.total; | |
245 | + if (event.lengthComputable) { | |
246 | + percent = Math.ceil(position / total * 100); | |
247 | + } | |
248 | + options.uploadProgress(event, position, total, percent); | |
249 | + }; | |
250 | + } | |
251 | + return xhr; | |
252 | + }; | |
253 | + } | |
254 | + | |
255 | + s.data = null; | |
256 | + var beforeSend = s.beforeSend; | |
257 | + s.beforeSend = function(xhr, o) { | |
258 | + o.data = formdata; | |
259 | + if(beforeSend) | |
260 | + beforeSend.call(o, xhr, options); | |
261 | + }; | |
262 | + $.ajax(s); | |
263 | + } | |
264 | + | |
265 | + // private function for handling file uploads (hat tip to YAHOO!) | |
266 | + function fileUploadIframe(a) { | |
267 | + var form = $form[0], el, i, s, g, id, $io, io, xhr, sub, n, timedOut, timeoutHandle; | |
268 | + var useProp = !!$.fn.prop; | |
269 | + | |
270 | + if ($(':input[name=submit],:input[id=submit]', form).length) { | |
271 | + // if there is an input with a name or id of 'submit' then we won't be | |
272 | + // able to invoke the submit fn on the form (at least not x-browser) | |
273 | + alert('Error: Form elements must not have name or id of "submit".'); | |
274 | + return; | |
275 | + } | |
276 | + | |
277 | + if (a) { | |
278 | + // ensure that every serialized input is still enabled | |
279 | + for (i=0; i < elements.length; i++) { | |
280 | + el = $(elements[i]); | |
281 | + if ( useProp ) | |
282 | + el.prop('disabled', false); | |
283 | + else | |
284 | + el.removeAttr('disabled'); | |
285 | + } | |
286 | + } | |
287 | + | |
288 | + s = $.extend(true, {}, $.ajaxSettings, options); | |
289 | + s.context = s.context || s; | |
290 | + id = 'jqFormIO' + (new Date().getTime()); | |
291 | + if (s.iframeTarget) { | |
292 | + $io = $(s.iframeTarget); | |
293 | + n = $io.attr('name'); | |
294 | + if (!n) | |
295 | + $io.attr('name', id); | |
296 | + else | |
297 | + id = n; | |
298 | + } | |
299 | + else { | |
300 | + $io = $('<iframe name="' + id + '" src="'+ s.iframeSrc +'" />'); | |
301 | + $io.css({ position: 'absolute', top: '-1000px', left: '-1000px' }); | |
302 | + } | |
303 | + io = $io[0]; | |
304 | + | |
305 | + | |
306 | + xhr = { // mock object | |
307 | + aborted: 0, | |
308 | + responseText: null, | |
309 | + responseXML: null, | |
310 | + status: 0, | |
311 | + statusText: 'n/a', | |
312 | + getAllResponseHeaders: function() {}, | |
313 | + getResponseHeader: function() {}, | |
314 | + setRequestHeader: function() {}, | |
315 | + abort: function(status) { | |
316 | + var e = (status === 'timeout' ? 'timeout' : 'aborted'); | |
317 | + log('aborting upload... ' + e); | |
318 | + this.aborted = 1; | |
319 | + $io.attr('src', s.iframeSrc); // abort op in progress | |
320 | + xhr.error = e; | |
321 | + if (s.error) | |
322 | + s.error.call(s.context, xhr, e, status); | |
323 | + if (g) | |
324 | + $.event.trigger("ajaxError", [xhr, s, e]); | |
325 | + if (s.complete) | |
326 | + s.complete.call(s.context, xhr, e); | |
327 | + } | |
328 | + }; | |
329 | + | |
330 | + g = s.global; | |
331 | + // trigger ajax global events so that activity/block indicators work like normal | |
332 | + if (g && 0 === $.active++) { | |
333 | + $.event.trigger("ajaxStart"); | |
334 | + } | |
335 | + if (g) { | |
336 | + $.event.trigger("ajaxSend", [xhr, s]); | |
337 | + } | |
338 | + | |
339 | + if (s.beforeSend && s.beforeSend.call(s.context, xhr, s) === false) { | |
340 | + if (s.global) { | |
341 | + $.active--; | |
342 | + } | |
343 | + return; | |
344 | + } | |
345 | + if (xhr.aborted) { | |
346 | + return; | |
347 | + } | |
348 | + | |
349 | + // add submitting element to data if we know it | |
350 | + sub = form.clk; | |
351 | + if (sub) { | |
352 | + n = sub.name; | |
353 | + if (n && !sub.disabled) { | |
354 | + s.extraData = s.extraData || {}; | |
355 | + s.extraData[n] = sub.value; | |
356 | + if (sub.type == "image") { | |
357 | + s.extraData[n+'.x'] = form.clk_x; | |
358 | + s.extraData[n+'.y'] = form.clk_y; | |
359 | + } | |
360 | + } | |
361 | + } | |
362 | + | |
363 | + var CLIENT_TIMEOUT_ABORT = 1; | |
364 | + var SERVER_ABORT = 2; | |
365 | + | |
366 | + function getDoc(frame) { | |
367 | + var doc = frame.contentWindow ? frame.contentWindow.document : frame.contentDocument ? frame.contentDocument : frame.document; | |
368 | + return doc; | |
369 | + } | |
370 | + | |
371 | + // Rails CSRF hack (thanks to Yvan Barthelemy) | |
372 | + var csrf_token = $('meta[name=csrf-token]').attr('content'); | |
373 | + var csrf_param = $('meta[name=csrf-param]').attr('content'); | |
374 | + if (csrf_param && csrf_token) { | |
375 | + s.extraData = s.extraData || {}; | |
376 | + s.extraData[csrf_param] = csrf_token; | |
377 | + } | |
378 | + | |
379 | + // take a breath so that pending repaints get some cpu time before the upload starts | |
380 | + function doSubmit() { | |
381 | + // make sure form attrs are set | |
382 | + var t = $form.attr('target'), a = $form.attr('action'); | |
383 | + | |
384 | + // update form attrs in IE friendly way | |
385 | + form.setAttribute('target',id); | |
386 | + if (!method) { | |
387 | + form.setAttribute('method', 'POST'); | |
388 | + } | |
389 | + if (a != s.url) { | |
390 | + form.setAttribute('action', s.url); | |
391 | + } | |
392 | + | |
393 | + // ie borks in some cases when setting encoding | |
394 | + if (! s.skipEncodingOverride && (!method || /post/i.test(method))) { | |
395 | + $form.attr({ | |
396 | + encoding: 'multipart/form-data', | |
397 | + enctype: 'multipart/form-data' | |
398 | + }); | |
399 | + } | |
400 | + | |
401 | + // support timout | |
402 | + if (s.timeout) { | |
403 | + timeoutHandle = setTimeout(function() { timedOut = true; cb(CLIENT_TIMEOUT_ABORT); }, s.timeout); | |
404 | + } | |
405 | + | |
406 | + // look for server aborts | |
407 | + function checkState() { | |
408 | + try { | |
409 | + var state = getDoc(io).readyState; | |
410 | + log('state = ' + state); | |
411 | + if (state && state.toLowerCase() == 'uninitialized') | |
412 | + setTimeout(checkState,50); | |
413 | + } | |
414 | + catch(e) { | |
415 | + log('Server abort: ' , e, ' (', e.name, ')'); | |
416 | + cb(SERVER_ABORT); | |
417 | + if (timeoutHandle) | |
418 | + clearTimeout(timeoutHandle); | |
419 | + timeoutHandle = undefined; | |
420 | + } | |
421 | + } | |
422 | + | |
423 | + // add "extra" data to form if provided in options | |
424 | + var extraInputs = []; | |
425 | + try { | |
426 | + if (s.extraData) { | |
427 | + for (var n in s.extraData) { | |
428 | + if (s.extraData.hasOwnProperty(n)) { | |
429 | + extraInputs.push( | |
430 | + $('<input type="hidden" name="'+n+'">').attr('value',s.extraData[n]) | |
431 | + .appendTo(form)[0]); | |
432 | + } | |
433 | + } | |
434 | + } | |
435 | + | |
436 | + if (!s.iframeTarget) { | |
437 | + // add iframe to doc and submit the form | |
438 | + $io.appendTo('body'); | |
439 | + if (io.attachEvent) | |
440 | + io.attachEvent('onload', cb); | |
441 | + else | |
442 | + io.addEventListener('load', cb, false); | |
443 | + } | |
444 | + setTimeout(checkState,15); | |
445 | + form.submit(); | |
446 | + } | |
447 | + finally { | |
448 | + // reset attrs and remove "extra" input elements | |
449 | + form.setAttribute('action',a); | |
450 | + if(t) { | |
451 | + form.setAttribute('target', t); | |
452 | + } else { | |
453 | + $form.removeAttr('target'); | |
454 | + } | |
455 | + $(extraInputs).remove(); | |
456 | + } | |
457 | + } | |
458 | + | |
459 | + if (s.forceSync) { | |
460 | + doSubmit(); | |
461 | + } | |
462 | + else { | |
463 | + setTimeout(doSubmit, 10); // this lets dom updates render | |
464 | + } | |
465 | + | |
466 | + var data, doc, domCheckCount = 50, callbackProcessed; | |
467 | + | |
468 | + function cb(e) { | |
469 | + if (xhr.aborted || callbackProcessed) { | |
470 | + return; | |
471 | + } | |
472 | + try { | |
473 | + doc = getDoc(io); | |
474 | + } | |
475 | + catch(ex) { | |
476 | + log('cannot access response document: ', ex); | |
477 | + e = SERVER_ABORT; | |
478 | + } | |
479 | + if (e === CLIENT_TIMEOUT_ABORT && xhr) { | |
480 | + xhr.abort('timeout'); | |
481 | + return; | |
482 | + } | |
483 | + else if (e == SERVER_ABORT && xhr) { | |
484 | + xhr.abort('server abort'); | |
485 | + return; | |
486 | + } | |
487 | + | |
488 | + if (!doc || doc.location.href == s.iframeSrc) { | |
489 | + // response not received yet | |
490 | + if (!timedOut) | |
491 | + return; | |
492 | + } | |
493 | + if (io.detachEvent) | |
494 | + io.detachEvent('onload', cb); | |
495 | + else | |
496 | + io.removeEventListener('load', cb, false); | |
497 | + | |
498 | + var status = 'success', errMsg; | |
499 | + try { | |
500 | + if (timedOut) { | |
501 | + throw 'timeout'; | |
502 | + } | |
503 | + | |
504 | + var isXml = s.dataType == 'xml' || doc.XMLDocument || $.isXMLDoc(doc); | |
505 | + log('isXml='+isXml); | |
506 | + if (!isXml && window.opera && (doc.body === null || !doc.body.innerHTML)) { | |
507 | + if (--domCheckCount) { | |
508 | + // in some browsers (Opera) the iframe DOM is not always traversable when | |
509 | + // the onload callback fires, so we loop a bit to accommodate | |
510 | + log('requeing onLoad callback, DOM not available'); | |
511 | + setTimeout(cb, 250); | |
512 | + return; | |
513 | + } | |
514 | + // let this fall through because server response could be an empty document | |
515 | + //log('Could not access iframe DOM after mutiple tries.'); | |
516 | + //throw 'DOMException: not available'; | |
517 | + } | |
518 | + | |
519 | + //log('response detected'); | |
520 | + var docRoot = doc.body ? doc.body : doc.documentElement; | |
521 | + xhr.responseText = docRoot ? docRoot.innerHTML : null; | |
522 | + xhr.responseXML = doc.XMLDocument ? doc.XMLDocument : doc; | |
523 | + if (isXml) | |
524 | + s.dataType = 'xml'; | |
525 | + xhr.getResponseHeader = function(header){ | |
526 | + var headers = {'content-type': s.dataType}; | |
527 | + return headers[header]; | |
528 | + }; | |
529 | + // support for XHR 'status' & 'statusText' emulation : | |
530 | + if (docRoot) { | |
531 | + xhr.status = Number( docRoot.getAttribute('status') ) || xhr.status; | |
532 | + xhr.statusText = docRoot.getAttribute('statusText') || xhr.statusText; | |
533 | + } | |
534 | + | |
535 | + var dt = (s.dataType || '').toLowerCase(); | |
536 | + var scr = /(json|script|text)/.test(dt); | |
537 | + if (scr || s.textarea) { | |
538 | + // see if user embedded response in textarea | |
539 | + var ta = doc.getElementsByTagName('textarea')[0]; | |
540 | + if (ta) { | |
541 | + xhr.responseText = ta.value; | |
542 | + // support for XHR 'status' & 'statusText' emulation : | |
543 | + xhr.status = Number( ta.getAttribute('status') ) || xhr.status; | |
544 | + xhr.statusText = ta.getAttribute('statusText') || xhr.statusText; | |
545 | + } | |
546 | + else if (scr) { | |
547 | + // account for browsers injecting pre around json response | |
548 | + var pre = doc.getElementsByTagName('pre')[0]; | |
549 | + var b = doc.getElementsByTagName('body')[0]; | |
550 | + if (pre) { | |
551 | + xhr.responseText = pre.textContent ? pre.textContent : pre.innerText; | |
552 | + } | |
553 | + else if (b) { | |
554 | + xhr.responseText = b.textContent ? b.textContent : b.innerText; | |
555 | + } | |
556 | + } | |
557 | + } | |
558 | + else if (dt == 'xml' && !xhr.responseXML && xhr.responseText) { | |
559 | + xhr.responseXML = toXml(xhr.responseText); | |
560 | + } | |
561 | + | |
562 | + try { | |
563 | + data = httpData(xhr, dt, s); | |
564 | + } | |
565 | + catch (e) { | |
566 | + status = 'parsererror'; | |
567 | + xhr.error = errMsg = (e || status); | |
568 | + } | |
569 | + } | |
570 | + catch (e) { | |
571 | + log('error caught: ',e); | |
572 | + status = 'error'; | |
573 | + xhr.error = errMsg = (e || status); | |
574 | + } | |
575 | + | |
576 | + if (xhr.aborted) { | |
577 | + log('upload aborted'); | |
578 | + status = null; | |
579 | + } | |
580 | + | |
581 | + if (xhr.status) { // we've set xhr.status | |
582 | + status = (xhr.status >= 200 && xhr.status < 300 || xhr.status === 304) ? 'success' : 'error'; | |
583 | + } | |
584 | + | |
585 | + // ordering of these callbacks/triggers is odd, but that's how $.ajax does it | |
586 | + if (status === 'success') { | |
587 | + if (s.success) | |
588 | + s.success.call(s.context, data, 'success', xhr); | |
589 | + if (g) | |
590 | + $.event.trigger("ajaxSuccess", [xhr, s]); | |
591 | + } | |
592 | + else if (status) { | |
593 | + if (errMsg === undefined) | |
594 | + errMsg = xhr.statusText; | |
595 | + if (s.error) | |
596 | + s.error.call(s.context, xhr, status, errMsg); | |
597 | + if (g) | |
598 | + $.event.trigger("ajaxError", [xhr, s, errMsg]); | |
599 | + } | |
600 | + | |
601 | + if (g) | |
602 | + $.event.trigger("ajaxComplete", [xhr, s]); | |
603 | + | |
604 | + if (g && ! --$.active) { | |
605 | + $.event.trigger("ajaxStop"); | |
606 | + } | |
607 | + | |
608 | + if (s.complete) | |
609 | + s.complete.call(s.context, xhr, status); | |
610 | + | |
611 | + callbackProcessed = true; | |
612 | + if (s.timeout) | |
613 | + clearTimeout(timeoutHandle); | |
614 | + | |
615 | + // clean up | |
616 | + setTimeout(function() { | |
617 | + if (!s.iframeTarget) | |
618 | + $io.remove(); | |
619 | + xhr.responseXML = null; | |
620 | + }, 100); | |
621 | + } | |
622 | + | |
623 | + var toXml = $.parseXML || function(s, doc) { // use parseXML if available (jQuery 1.5+) | |
624 | + if (window.ActiveXObject) { | |
625 | + doc = new ActiveXObject('Microsoft.XMLDOM'); | |
626 | + doc.async = 'false'; | |
627 | + doc.loadXML(s); | |
628 | + } | |
629 | + else { | |
630 | + doc = (new DOMParser()).parseFromString(s, 'text/xml'); | |
631 | + } | |
632 | + return (doc && doc.documentElement && doc.documentElement.nodeName != 'parsererror') ? doc : null; | |
633 | + }; | |
634 | + var parseJSON = $.parseJSON || function(s) { | |
635 | + /*jslint evil:true */ | |
636 | + return window['eval']('(' + s + ')'); | |
637 | + }; | |
638 | + | |
639 | + var httpData = function( xhr, type, s ) { // mostly lifted from jq1.4.4 | |
640 | + | |
641 | + var ct = xhr.getResponseHeader('content-type') || '', | |
642 | + xml = type === 'xml' || !type && ct.indexOf('xml') >= 0, | |
643 | + data = xml ? xhr.responseXML : xhr.responseText; | |
644 | + | |
645 | + if (xml && data.documentElement.nodeName === 'parsererror') { | |
646 | + if ($.error) | |
647 | + $.error('parsererror'); | |
648 | + } | |
649 | + if (s && s.dataFilter) { | |
650 | + data = s.dataFilter(data, type); | |
651 | + } | |
652 | + if (typeof data === 'string') { | |
653 | + if (type === 'json' || !type && ct.indexOf('json') >= 0) { | |
654 | + data = parseJSON(data); | |
655 | + } else if (type === "script" || !type && ct.indexOf("javascript") >= 0) { | |
656 | + $.globalEval(data); | |
657 | + } | |
658 | + } | |
659 | + return data; | |
660 | + }; | |
661 | + } | |
662 | +}; | |
663 | + | |
664 | +/** | |
665 | + * ajaxForm() provides a mechanism for fully automating form submission. | |
666 | + * | |
667 | + * The advantages of using this method instead of ajaxSubmit() are: | |
668 | + * | |
669 | + * 1: This method will include coordinates for <input type="image" /> elements (if the element | |
670 | + * is used to submit the form). | |
671 | + * 2. This method will include the submit element's name/value data (for the element that was | |
672 | + * used to submit the form). | |
673 | + * 3. This method binds the submit() method to the form for you. | |
674 | + * | |
675 | + * The options argument for ajaxForm works exactly as it does for ajaxSubmit. ajaxForm merely | |
676 | + * passes the options argument along after properly binding events for submit elements and | |
677 | + * the form itself. | |
678 | + */ | |
679 | +$.fn.ajaxForm = function(options) { | |
680 | + options = options || {}; | |
681 | + options.delegation = options.delegation && $.isFunction($.fn.on); | |
682 | + | |
683 | + // in jQuery 1.3+ we can fix mistakes with the ready state | |
684 | + if (!options.delegation && this.length === 0) { | |
685 | + var o = { s: this.selector, c: this.context }; | |
686 | + if (!$.isReady && o.s) { | |
687 | + log('DOM not ready, queuing ajaxForm'); | |
688 | + $(function() { | |
689 | + $(o.s,o.c).ajaxForm(options); | |
690 | + }); | |
691 | + return this; | |
692 | + } | |
693 | + // is your DOM ready? http://docs.jquery.com/Tutorials:Introducing_$(document).ready() | |
694 | + log('terminating; zero elements found by selector' + ($.isReady ? '' : ' (DOM not ready)')); | |
695 | + return this; | |
696 | + } | |
697 | + | |
698 | + if ( options.delegation ) { | |
699 | + $(document) | |
700 | + .off('submit.form-plugin', this.selector, doAjaxSubmit) | |
701 | + .off('click.form-plugin', this.selector, captureSubmittingElement) | |
702 | + .on('submit.form-plugin', this.selector, options, doAjaxSubmit) | |
703 | + .on('click.form-plugin', this.selector, options, captureSubmittingElement); | |
704 | + return this; | |
705 | + } | |
706 | + | |
707 | + return this.ajaxFormUnbind() | |
708 | + .bind('submit.form-plugin', options, doAjaxSubmit) | |
709 | + .bind('click.form-plugin', options, captureSubmittingElement); | |
710 | +}; | |
711 | + | |
712 | +// private event handlers | |
713 | +function doAjaxSubmit(e) { | |
714 | + /*jshint validthis:true */ | |
715 | + var options = e.data; | |
716 | + if (!e.isDefaultPrevented()) { // if event has been canceled, don't proceed | |
717 | + e.preventDefault(); | |
718 | + $(this).ajaxSubmit(options); | |
719 | + } | |
720 | +} | |
721 | + | |
722 | +function captureSubmittingElement(e) { | |
723 | + /*jshint validthis:true */ | |
724 | + var target = e.target; | |
725 | + var $el = $(target); | |
726 | + if (!($el.is(":submit,input:image"))) { | |
727 | + // is this a child element of the submit el? (ex: a span within a button) | |
728 | + var t = $el.closest(':submit'); | |
729 | + if (t.length === 0) { | |
730 | + return; | |
731 | + } | |
732 | + target = t[0]; | |
733 | + } | |
734 | + var form = this; | |
735 | + form.clk = target; | |
736 | + if (target.type == 'image') { | |
737 | + if (e.offsetX !== undefined) { | |
738 | + form.clk_x = e.offsetX; | |
739 | + form.clk_y = e.offsetY; | |
740 | + } else if (typeof $.fn.offset == 'function') { | |
741 | + var offset = $el.offset(); | |
742 | + form.clk_x = e.pageX - offset.left; | |
743 | + form.clk_y = e.pageY - offset.top; | |
744 | + } else { | |
745 | + form.clk_x = e.pageX - target.offsetLeft; | |
746 | + form.clk_y = e.pageY - target.offsetTop; | |
747 | + } | |
748 | + } | |
749 | + // clear form vars | |
750 | + setTimeout(function() { form.clk = form.clk_x = form.clk_y = null; }, 100); | |
751 | +} | |
752 | + | |
753 | + | |
754 | +// ajaxFormUnbind unbinds the event handlers that were bound by ajaxForm | |
755 | +$.fn.ajaxFormUnbind = function() { | |
756 | + return this.unbind('submit.form-plugin click.form-plugin'); | |
757 | +}; | |
758 | + | |
759 | +/** | |
760 | + * formToArray() gathers form element data into an array of objects that can | |
761 | + * be passed to any of the following ajax functions: $.get, $.post, or load. | |
762 | + * Each object in the array has both a 'name' and 'value' property. An example of | |
763 | + * an array for a simple login form might be: | |
764 | + * | |
765 | + * [ { name: 'username', value: 'jresig' }, { name: 'password', value: 'secret' } ] | |
766 | + * | |
767 | + * It is this array that is passed to pre-submit callback functions provided to the | |
768 | + * ajaxSubmit() and ajaxForm() methods. | |
769 | + */ | |
770 | +$.fn.formToArray = function(semantic, elements) { | |
771 | + var a = []; | |
772 | + if (this.length === 0) { | |
773 | + return a; | |
774 | + } | |
775 | + | |
776 | + var form = this[0]; | |
777 | + var els = semantic ? form.getElementsByTagName('*') : form.elements; | |
778 | + if (!els) { | |
779 | + return a; | |
780 | + } | |
781 | + | |
782 | + var i,j,n,v,el,max,jmax; | |
783 | + for(i=0, max=els.length; i < max; i++) { | |
784 | + el = els[i]; | |
785 | + n = el.name; | |
786 | + if (!n) { | |
787 | + continue; | |
788 | + } | |
789 | + | |
790 | + if (semantic && form.clk && el.type == "image") { | |
791 | + // handle image inputs on the fly when semantic == true | |
792 | + if(!el.disabled && form.clk == el) { | |
793 | + a.push({name: n, value: $(el).val(), type: el.type }); | |
794 | + a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y}); | |
795 | + } | |
796 | + continue; | |
797 | + } | |
798 | + | |
799 | + v = $.fieldValue(el, true); | |
800 | + if (v && v.constructor == Array) { | |
801 | + if (elements) | |
802 | + elements.push(el); | |
803 | + for(j=0, jmax=v.length; j < jmax; j++) { | |
804 | + a.push({name: n, value: v[j]}); | |
805 | + } | |
806 | + } | |
807 | + else if (feature.fileapi && el.type == 'file' && !el.disabled) { | |
808 | + if (elements) | |
809 | + elements.push(el); | |
810 | + var files = el.files; | |
811 | + if (files.length) { | |
812 | + for (j=0; j < files.length; j++) { | |
813 | + a.push({name: n, value: files[j], type: el.type}); | |
814 | + } | |
815 | + } | |
816 | + else { | |
817 | + // #180 | |
818 | + a.push({ name: n, value: '', type: el.type }); | |
819 | + } | |
820 | + } | |
821 | + else if (v !== null && typeof v != 'undefined') { | |
822 | + if (elements) | |
823 | + elements.push(el); | |
824 | + a.push({name: n, value: v, type: el.type, required: el.required}); | |
825 | + } | |
826 | + } | |
827 | + | |
828 | + if (!semantic && form.clk) { | |
829 | + // input type=='image' are not found in elements array! handle it here | |
830 | + var $input = $(form.clk), input = $input[0]; | |
831 | + n = input.name; | |
832 | + if (n && !input.disabled && input.type == 'image') { | |
833 | + a.push({name: n, value: $input.val()}); | |
834 | + a.push({name: n+'.x', value: form.clk_x}, {name: n+'.y', value: form.clk_y}); | |
835 | + } | |
836 | + } | |
837 | + return a; | |
838 | +}; | |
839 | + | |
840 | +/** | |
841 | + * Serializes form data into a 'submittable' string. This method will return a string | |
842 | + * in the format: name1=value1&name2=value2 | |
843 | + */ | |
844 | +$.fn.formSerialize = function(semantic) { | |
845 | + //hand off to jQuery.param for proper encoding | |
846 | + return $.param(this.formToArray(semantic)); | |
847 | +}; | |
848 | + | |
849 | +/** | |
850 | + * Serializes all field elements in the jQuery object into a query string. | |
851 | + * This method will return a string in the format: name1=value1&name2=value2 | |
852 | + */ | |
853 | +$.fn.fieldSerialize = function(successful) { | |
854 | + var a = []; | |
855 | + this.each(function() { | |
856 | + var n = this.name; | |
857 | + if (!n) { | |
858 | + return; | |
859 | + } | |
860 | + var v = $.fieldValue(this, successful); | |
861 | + if (v && v.constructor == Array) { | |
862 | + for (var i=0,max=v.length; i < max; i++) { | |
863 | + a.push({name: n, value: v[i]}); | |
864 | + } | |
865 | + } | |
866 | + else if (v !== null && typeof v != 'undefined') { | |
867 | + a.push({name: this.name, value: v}); | |
868 | + } | |
869 | + }); | |
870 | + //hand off to jQuery.param for proper encoding | |
871 | + return $.param(a); | |
872 | +}; | |
873 | + | |
874 | +/** | |
875 | + * Returns the value(s) of the element in the matched set. For example, consider the following form: | |
876 | + * | |
877 | + * <form><fieldset> | |
878 | + * <input name="A" type="text" /> | |
879 | + * <input name="A" type="text" /> | |
880 | + * <input name="B" type="checkbox" value="B1" /> | |
881 | + * <input name="B" type="checkbox" value="B2"/> | |
882 | + * <input name="C" type="radio" value="C1" /> | |
883 | + * <input name="C" type="radio" value="C2" /> | |
884 | + * </fieldset></form> | |
885 | + * | |
886 | + * var v = $(':text').fieldValue(); | |
887 | + * // if no values are entered into the text inputs | |
888 | + * v == ['',''] | |
889 | + * // if values entered into the text inputs are 'foo' and 'bar' | |
890 | + * v == ['foo','bar'] | |
891 | + * | |
892 | + * var v = $(':checkbox').fieldValue(); | |
893 | + * // if neither checkbox is checked | |
894 | + * v === undefined | |
895 | + * // if both checkboxes are checked | |
896 | + * v == ['B1', 'B2'] | |
897 | + * | |
898 | + * var v = $(':radio').fieldValue(); | |
899 | + * // if neither radio is checked | |
900 | + * v === undefined | |
901 | + * // if first radio is checked | |
902 | + * v == ['C1'] | |
903 | + * | |
904 | + * The successful argument controls whether or not the field element must be 'successful' | |
905 | + * (per http://www.w3.org/TR/html4/interact/forms.html#successful-controls). | |
906 | + * The default value of the successful argument is true. If this value is false the value(s) | |
907 | + * for each element is returned. | |
908 | + * | |
909 | + * Note: This method *always* returns an array. If no valid value can be determined the | |
910 | + * array will be empty, otherwise it will contain one or more values. | |
911 | + */ | |
912 | +$.fn.fieldValue = function(successful) { | |
913 | + for (var val=[], i=0, max=this.length; i < max; i++) { | |
914 | + var el = this[i]; | |
915 | + var v = $.fieldValue(el, successful); | |
916 | + if (v === null || typeof v == 'undefined' || (v.constructor == Array && !v.length)) { | |
917 | + continue; | |
918 | + } | |
919 | + if (v.constructor == Array) | |
920 | + $.merge(val, v); | |
921 | + else | |
922 | + val.push(v); | |
923 | + } | |
924 | + return val; | |
925 | +}; | |
926 | + | |
927 | +/** | |
928 | + * Returns the value of the field element. | |
929 | + */ | |
930 | +$.fieldValue = function(el, successful) { | |
931 | + var n = el.name, t = el.type, tag = el.tagName.toLowerCase(); | |
932 | + if (successful === undefined) { | |
933 | + successful = true; | |
934 | + } | |
935 | + | |
936 | + if (successful && (!n || el.disabled || t == 'reset' || t == 'button' || | |
937 | + (t == 'checkbox' || t == 'radio') && !el.checked || | |
938 | + (t == 'submit' || t == 'image') && el.form && el.form.clk != el || | |
939 | + tag == 'select' && el.selectedIndex == -1)) { | |
940 | + return null; | |
941 | + } | |
942 | + | |
943 | + if (tag == 'select') { | |
944 | + var index = el.selectedIndex; | |
945 | + if (index < 0) { | |
946 | + return null; | |
947 | + } | |
948 | + var a = [], ops = el.options; | |
949 | + var one = (t == 'select-one'); | |
950 | + var max = (one ? index+1 : ops.length); | |
951 | + for(var i=(one ? index : 0); i < max; i++) { | |
952 | + var op = ops[i]; | |
953 | + if (op.selected) { | |
954 | + var v = op.value; | |
955 | + if (!v) { // extra pain for IE... | |
956 | + v = (op.attributes && op.attributes['value'] && !(op.attributes['value'].specified)) ? op.text : op.value; | |
957 | + } | |
958 | + if (one) { | |
959 | + return v; | |
960 | + } | |
961 | + a.push(v); | |
962 | + } | |
963 | + } | |
964 | + return a; | |
965 | + } | |
966 | + return $(el).val(); | |
967 | +}; | |
968 | + | |
969 | +/** | |
970 | + * Clears the form data. Takes the following actions on the form's input fields: | |
971 | + * - input text fields will have their 'value' property set to the empty string | |
972 | + * - select elements will have their 'selectedIndex' property set to -1 | |
973 | + * - checkbox and radio inputs will have their 'checked' property set to false | |
974 | + * - inputs of type submit, button, reset, and hidden will *not* be effected | |
975 | + * - button elements will *not* be effected | |
976 | + */ | |
977 | +$.fn.clearForm = function(includeHidden) { | |
978 | + return this.each(function() { | |
979 | + $('input,select,textarea', this).clearFields(includeHidden); | |
980 | + }); | |
981 | +}; | |
982 | + | |
983 | +/** | |
984 | + * Clears the selected form elements. | |
985 | + */ | |
986 | +$.fn.clearFields = $.fn.clearInputs = function(includeHidden) { | |
987 | + var re = /^(?:color|date|datetime|email|month|number|password|range|search|tel|text|time|url|week)$/i; // 'hidden' is not in this list | |
988 | + return this.each(function() { | |
989 | + var t = this.type, tag = this.tagName.toLowerCase(); | |
990 | + if (re.test(t) || tag == 'textarea') { | |
991 | + this.value = ''; | |
992 | + } | |
993 | + else if (t == 'checkbox' || t == 'radio') { | |
994 | + this.checked = false; | |
995 | + } | |
996 | + else if (tag == 'select') { | |
997 | + this.selectedIndex = -1; | |
998 | + } | |
999 | + else if (includeHidden) { | |
1000 | + // includeHidden can be the valud true, or it can be a selector string | |
1001 | + // indicating a special test; for example: | |
1002 | + // $('#myForm').clearForm('.special:hidden') | |
1003 | + // the above would clean hidden inputs that have the class of 'special' | |
1004 | + if ( (includeHidden === true && /hidden/.test(t)) || | |
1005 | + (typeof includeHidden == 'string' && $(this).is(includeHidden)) ) | |
1006 | + this.value = ''; | |
1007 | + } | |
1008 | + }); | |
1009 | +}; | |
1010 | + | |
1011 | +/** | |
1012 | + * Resets the form data. Causes all form elements to be reset to their original value. | |
1013 | + */ | |
1014 | +$.fn.resetForm = function() { | |
1015 | + return this.each(function() { | |
1016 | + // guard against an input with the name of 'reset' | |
1017 | + // note that IE reports the reset function as an 'object' | |
1018 | + if (typeof this.reset == 'function' || (typeof this.reset == 'object' && !this.reset.nodeType)) { | |
1019 | + this.reset(); | |
1020 | + } | |
1021 | + }); | |
1022 | +}; | |
1023 | + | |
1024 | +/** | |
1025 | + * Enables or disables any matching elements. | |
1026 | + */ | |
1027 | +$.fn.enable = function(b) { | |
1028 | + if (b === undefined) { | |
1029 | + b = true; | |
1030 | + } | |
1031 | + return this.each(function() { | |
1032 | + this.disabled = !b; | |
1033 | + }); | |
1034 | +}; | |
1035 | + | |
1036 | +/** | |
1037 | + * Checks/unchecks any matching checkboxes or radio buttons and | |
1038 | + * selects/deselects and matching option elements. | |
1039 | + */ | |
1040 | +$.fn.selected = function(select) { | |
1041 | + if (select === undefined) { | |
1042 | + select = true; | |
1043 | + } | |
1044 | + return this.each(function() { | |
1045 | + var t = this.type; | |
1046 | + if (t == 'checkbox' || t == 'radio') { | |
1047 | + this.checked = select; | |
1048 | + } | |
1049 | + else if (this.tagName.toLowerCase() == 'option') { | |
1050 | + var $sel = $(this).parent('select'); | |
1051 | + if (select && $sel[0] && $sel[0].type == 'select-one') { | |
1052 | + // deselect all other options | |
1053 | + $sel.find('option').selected(false); | |
1054 | + } | |
1055 | + this.selected = select; | |
1056 | + } | |
1057 | + }); | |
1058 | +}; | |
1059 | + | |
1060 | +// expose debug var | |
1061 | +$.fn.ajaxSubmit.debug = false; | |
1062 | + | |
1063 | +// helper fn for console logging | |
1064 | +function log() { | |
1065 | + if (!$.fn.ajaxSubmit.debug) | |
1066 | + return; | |
1067 | + var msg = '[jquery.form] ' + Array.prototype.join.call(arguments,''); | |
1068 | + if (window.console && window.console.log) { | |
1069 | + window.console.log(msg); | |
1070 | + } | |
1071 | + else if (window.opera && window.opera.postError) { | |
1072 | + window.opera.postError(msg); | |
1073 | + } | |
1074 | +} | |
1075 | + | |
1076 | +})(jQuery); |
--- public/javascripts/modules/markdown.js
+++ public/javascripts/modules/markdown.js
... | ... | @@ -1,8 +1,8 @@ |
1 | 1 |
nforge.namespace('markdown'); |
2 | 2 |
|
3 |
-var markdownRender = function(text) { |
|
4 |
- text = text. |
|
5 |
- replace(/```(\w+)(?:\r\n|\r|\n)((\r|\n|.)*?)(\r|\n)```/gm, function(match, p1, p2) { |
|
3 |
+var renderMarkdown = function(text) { |
|
4 |
+ text = text |
|
5 |
+ .replace(/```(\w+)(?:\r\n|\r|\n)((\r|\n|.)*?)(\r|\n)```/gm, function(match, p1, p2) { |
|
6 | 6 |
try { |
7 | 7 |
return '<pre><code class="' + p1 + '">' + hljs(p2, p1).value + '</code></pre>'; |
8 | 8 |
} catch (e) { |
... | ... | @@ -13,51 +13,311 @@ |
13 | 13 |
return new Showdown.converter().makeHtml(text); |
14 | 14 |
}; |
15 | 15 |
|
16 |
-nforge.markdown.edit = function () { |
|
17 |
- var that; |
|
16 |
+var getFileList = function(target, urlToGetFileList, fn) { |
|
17 |
+ var form = $('<form>') |
|
18 |
+ .attr('method', 'get') |
|
19 |
+ .attr('action', urlToGetFileList); |
|
20 |
+ resourceType = target.attr('resourceType'); |
|
21 |
+ resourceId = target.attr('resourceId'); |
|
22 |
+ if (resourceType !== undefined) { |
|
23 |
+ form.append('<input type="hidden" name="containerType" value="' + resourceType + '">'); |
|
24 |
+ } |
|
25 |
+ if (resourceId !== undefined) { |
|
26 |
+ form.append('<input type="hidden" name="containerId" value="' + resourceId + '">'); |
|
27 |
+ } |
|
28 |
+ form.ajaxForm({ success: fn }); |
|
29 |
+ form.submit(); |
|
30 |
+}; |
|
18 | 31 |
|
19 |
- that = { |
|
20 |
- init : function (selector) { |
|
21 |
- var previewDiv, previewSwitch; |
|
32 |
+var editor = function (textarea) { |
|
33 |
+ var previewDiv, previewSwitch; |
|
22 | 34 |
|
23 |
- if (!selector) selector = '#body'; |
|
35 |
+ previewDiv = $('<div>'); |
|
36 |
+ previewDiv.attr('div', 'preview'); |
|
37 |
+ previewDiv.css('display', 'none'); |
|
24 | 38 |
|
25 |
- previewDiv = $('<div>'); |
|
26 |
- previewDiv.attr('div', 'preview'); |
|
39 |
+ previewSwitch = $('<div>'); |
|
40 |
+ previewSwitch.append($('<input type="radio" name="edit-mode" value="edit" checked>Edit</input>')); |
|
41 |
+ previewSwitch.append($('<input type="radio" name="edit-mode" value="preview">Preview</input>')); |
|
42 |
+ previewSwitch.change(function() { |
|
43 |
+ var val = $('input:radio[name=edit-mode]:checked').val(); |
|
44 |
+ if (val == 'preview') { |
|
45 |
+ previewDiv.html(renderMarkdown(textarea.val())); |
|
46 |
+ textarea.css('display', 'none'); |
|
47 |
+ previewDiv.css('display', ''); |
|
48 |
+ } else { |
|
49 |
+ textarea.css('display', ''); |
|
27 | 50 |
previewDiv.css('display', 'none'); |
51 |
+ } |
|
52 |
+ }); |
|
28 | 53 |
|
29 |
- previewSwitch = $('<div>'); |
|
30 |
- previewSwitch.append($('<input type="radio" name="edit-mode" value="edit" checked>Edit</input>')); |
|
31 |
- previewSwitch.append($('<input type="radio" name="edit-mode" value="preview">Preview</input>')); |
|
32 |
- previewSwitch.change(function() { |
|
33 |
- var val = $('input:radio[name=edit-mode]:checked').val(); |
|
34 |
- if (val == 'preview') { |
|
35 |
- previewDiv.html(markdownRender($(selector).val())); |
|
36 |
- $(selector).css('display', 'none'); |
|
37 |
- previewDiv.css('display', ''); |
|
38 |
- } else { |
|
39 |
- $(selector).css('display', ''); |
|
40 |
- previewDiv.css('display', 'none'); |
|
54 |
+ textarea.before(previewSwitch); |
|
55 |
+ textarea.before(previewDiv); |
|
56 |
+}; |
|
57 |
+ |
|
58 |
+var viewer = function (target) { |
|
59 |
+ target.html(renderMarkdown(target.text())); |
|
60 |
+}; |
|
61 |
+ |
|
62 |
+var fileUploader = function (textarea, action) { |
|
63 |
+ var setProgressBar = function(value) { |
|
64 |
+ progressbar.css("width", value + "%"); |
|
65 |
+ progressbar.text(value + "%"); |
|
66 |
+ }; |
|
67 |
+ |
|
68 |
+ var createFileItem = function(file, link) { |
|
69 |
+ var fileitem, icon, filelink, insertButton, deleteButton; |
|
70 |
+ |
|
71 |
+ fileitem = $('<li>'); |
|
72 |
+ fileitem.attr('tabindex', 0); |
|
73 |
+ |
|
74 |
+ icon = $('<i>'); |
|
75 |
+ icon.addClass('icon-upload'); |
|
76 |
+ |
|
77 |
+ filelink = $('<a>'); |
|
78 |
+ filelink.attr('href', file.url); |
|
79 |
+ filelink.text(file.name); |
|
80 |
+ |
|
81 |
+ insertButton = $('<input type="button">'); |
|
82 |
+ insertButton.attr('id', file.name); |
|
83 |
+ insertButton.attr('value', '본문에 삽입'); |
|
84 |
+ insertButton.addClass('insertInto label label-info'); |
|
85 |
+ insertButton.click(function() { insertLinkInto(textarea, link); }); |
|
86 |
+ |
|
87 |
+ deleteButton = $('<a>'); |
|
88 |
+ deleteButton.attr('name', 'submit'); |
|
89 |
+ deleteButton.addClass('fileDeleteBtn close'); |
|
90 |
+ deleteButton.text('x'); |
|
91 |
+ deleteButton.click(function() { |
|
92 |
+ var form = $('<form>') |
|
93 |
+ .attr('method', 'post') |
|
94 |
+ .attr('enctype', 'multipart/form-data') |
|
95 |
+ .attr('action', file.url); |
|
96 |
+ form.append('<input type="hidden" name="_method" value="delete">'); |
|
97 |
+ form.ajaxForm({ |
|
98 |
+ success: function() { |
|
99 |
+ fileitem.remove(); |
|
100 |
+ textarea.val(textarea.val().split(link).join('')); |
|
101 |
+ setProgressBar(0); |
|
102 |
+ notification.text(file.name + ' is deleted successfully.'); |
|
41 | 103 |
} |
42 | 104 |
}); |
105 |
+ form.submit(); |
|
106 |
+ }); |
|
43 | 107 |
|
44 |
- $(selector).before(previewSwitch); |
|
45 |
- $(selector).before(previewDiv); |
|
108 |
+ fileitem.append(icon); |
|
109 |
+ icon.after(filelink); |
|
110 |
+ filelink.after(insertButton); |
|
111 |
+ insertButton.after(deleteButton); |
|
112 |
+ |
|
113 |
+ return fileitem; |
|
114 |
+ }; |
|
115 |
+ |
|
116 |
+ var createFileList = function(title) { |
|
117 |
+ var filelist, div |
|
118 |
+ |
|
119 |
+ filelist = $('<ul>'); |
|
120 |
+ filelist.attr('id', 'filelist'); |
|
121 |
+ filelist.addClass('files'); |
|
122 |
+ |
|
123 |
+ div = $('<div>'); |
|
124 |
+ div.addClass('attachmentList'); |
|
125 |
+ div.append($('<strong>' + title + '</strong>')); |
|
126 |
+ div.append(filelist); |
|
127 |
+ |
|
128 |
+ return div; |
|
129 |
+ }; |
|
130 |
+ |
|
131 |
+ var _getFileNameOnly = function(filename) { |
|
132 |
+ var fakepath = 'fakepath'; |
|
133 |
+ var fakepathPostion = filename.indexOf(fakepath); |
|
134 |
+ if (fakepathPostion > -1) { |
|
135 |
+ filename = filename.substring(fakepath.length + fakepathPostion + 1); |
|
136 |
+ } |
|
137 |
+ return filename; |
|
138 |
+ }; |
|
139 |
+ |
|
140 |
+ var _replaceFileInputControl = function() { |
|
141 |
+ label.after(createAttachment()); |
|
142 |
+ }; |
|
143 |
+ |
|
144 |
+ var insertLinkInto = function(textarea, link) { |
|
145 |
+ var pos = textarea.prop('selectionStart'); |
|
146 |
+ var text = textarea.val(); |
|
147 |
+ textarea.val(text.substring(0, pos) + link + text.substring(pos)); |
|
148 |
+ }; |
|
149 |
+ |
|
150 |
+ var isImageType = function(mimeType) { |
|
151 |
+ if (mimeType && mimeType.substr(0, mimeType.indexOf('/')) == 'image') { |
|
152 |
+ return true; |
|
153 |
+ } else { |
|
154 |
+ return false; |
|
155 |
+ } |
|
156 |
+ }; |
|
157 |
+ |
|
158 |
+ var createFileLink = function(name, url, mimeType) { |
|
159 |
+ if (isImageType(mimeType)) { |
|
160 |
+ return '<img src="' + url + '">'; |
|
161 |
+ } else { |
|
162 |
+ return '[' + name + '](' + url + ')'; |
|
163 |
+ } |
|
164 |
+ }; |
|
165 |
+ |
|
166 |
+ var fileUploadOptions = { |
|
167 |
+ beforeSubmit: function() { |
|
168 |
+ var filename = _getFileNameOnly(attachment.val()); |
|
169 |
+ |
|
170 |
+ // show message box |
|
171 |
+ if (filename === "") { |
|
172 |
+ notification.text('Choose a file to be attached.'); |
|
173 |
+ return false; |
|
174 |
+ } |
|
175 |
+ |
|
176 |
+ return true; |
|
177 |
+ }, |
|
178 |
+ |
|
179 |
+ success: function(responseBody, statusText, xhr) { |
|
180 |
+ var file, link; |
|
181 |
+ file = responseBody; |
|
182 |
+ |
|
183 |
+ if (!(file instanceof Object) || !file.name || !file.url) { |
|
184 |
+ notification.text('Failed to upload - Server error.'); |
|
185 |
+ _replaceFileInputControl(); |
|
186 |
+ setProgressBar(0); |
|
187 |
+ return; |
|
188 |
+ } |
|
189 |
+ |
|
190 |
+ _replaceFileInputControl(); |
|
191 |
+ |
|
192 |
+ link = createFileLink(file.name, file.url, file.mimeType) |
|
193 |
+ if (isImageType(file.mimeType)) { |
|
194 |
+ insertLinkInto(textarea, link); |
|
195 |
+ } |
|
196 |
+ |
|
197 |
+ tempFileList.css('display', ''); |
|
198 |
+ tempFileList.append(createFileItem(file, link)); |
|
199 |
+ |
|
200 |
+ notification.text(file.name + ' is uploaded successfully.'); |
|
201 |
+ |
|
202 |
+ setProgressBar(100); |
|
203 |
+ }, |
|
204 |
+ |
|
205 |
+ error: function(responseBody, statusText, xhr) { |
|
206 |
+ notification.text('Failed to upload.'); |
|
207 |
+ _replaceFileInputControl(); |
|
208 |
+ setProgressBar(0); |
|
209 |
+ }, |
|
210 |
+ |
|
211 |
+ uploadProgress: function(event, position, total, percentComplete) { |
|
212 |
+ setProgressBar(percentComplete); |
|
213 |
+ } |
|
214 |
+ }; |
|
215 |
+ |
|
216 |
+ var createAttachment = function() { |
|
217 |
+ var attachment = $('<input type="file" name="filePath">'); |
|
218 |
+ |
|
219 |
+ attachment.click(function(event) { |
|
220 |
+ setProgressBar(0); |
|
221 |
+ }); |
|
222 |
+ |
|
223 |
+ attachment.change(function(event) { |
|
224 |
+ if (attachment.val() !== "") { |
|
225 |
+ var filename = _getFileNameOnly(attachment.val()); |
|
226 |
+ var form = $('<form>') |
|
227 |
+ .attr('method', 'post') |
|
228 |
+ .attr('enctype', 'multipart/form-data') |
|
229 |
+ .attr('action', action); |
|
230 |
+ form.append(attachment); |
|
231 |
+ form.ajaxForm(fileUploadOptions); |
|
232 |
+ form.submit(); |
|
233 |
+ notification.text(filename + ' is uploading...'); |
|
234 |
+ } |
|
235 |
+ }); |
|
236 |
+ |
|
237 |
+ return attachment; |
|
238 |
+ } |
|
239 |
+ |
|
240 |
+ if (!textarea || !action) { |
|
241 |
+ throw new Error('textarea and action is required.'); |
|
242 |
+ } |
|
243 |
+ |
|
244 |
+ var label = $('<label for="attachment">').text('Select file to upload'); |
|
245 |
+ var attachment = createAttachment(); |
|
246 |
+ var progressbar = $('<div class="bar">'); |
|
247 |
+ var progress = $('<div class="progress progress-warning">') |
|
248 |
+ .append(progressbar); |
|
249 |
+ var attachmentList = createFileList('Attachments'); |
|
250 |
+ var tempFileList = createFileList('Temporary files (attached if you save)'); |
|
251 |
+ var notification = $('<div>'); |
|
252 |
+ |
|
253 |
+ attachmentList.css('display', 'none'); |
|
254 |
+ tempFileList.css('display', 'none'); |
|
255 |
+ |
|
256 |
+ getFileList(textarea, action, function(responseBody, statusText, xhr) { |
|
257 |
+ var addFiles = function(files, targetList) { |
|
258 |
+ if (files) { |
|
259 |
+ for (var i = 0; i < files.length; i++) { |
|
260 |
+ var file = files[i]; |
|
261 |
+ var link = createFileLink(file.name, file.url, file.mimeType); |
|
262 |
+ targetList.css('display', ''); |
|
263 |
+ targetList.append(createFileItem(file, link)); |
|
264 |
+ } |
|
265 |
+ } |
|
266 |
+ }; |
|
267 |
+ |
|
268 |
+ addFiles(responseBody.attachments, attachmentList); |
|
269 |
+ addFiles(responseBody.tempFiles, tempFileList); |
|
270 |
+ }); |
|
271 |
+ |
|
272 |
+ textarea.after(label); |
|
273 |
+ label.after(notification); |
|
274 |
+ notification.after(attachment); |
|
275 |
+ attachment.after(progress); |
|
276 |
+ progress.after(attachmentList); |
|
277 |
+ attachmentList.after(tempFileList); |
|
278 |
+} |
|
279 |
+ |
|
280 |
+var fileDownloader = function (div, urlToGetFileList) { |
|
281 |
+ var createFileItem = function(file) { |
|
282 |
+ var link = $('<a>') |
|
283 |
+ .prop('href', file.url) |
|
284 |
+ .append($('<i>').addClass('icon-download')) |
|
285 |
+ .append($('<div>').text(file.name).html()); |
|
286 |
+ |
|
287 |
+ return $('<li>').append(link); |
|
288 |
+ } |
|
289 |
+ |
|
290 |
+ var filelist = $('<ul>'); |
|
291 |
+ var addFiles = function(responseBody, statusText, xhr) { |
|
292 |
+ var files = responseBody.attachments; |
|
293 |
+ for (var i = 0; i < files.length; i++) { |
|
294 |
+ filelist.css('display', ''); |
|
295 |
+ filelist.append(createFileItem(files[i])); |
|
296 |
+ } |
|
297 |
+ } |
|
298 |
+ |
|
299 |
+ getFileList(div, urlToGetFileList, addFiles); |
|
300 |
+ |
|
301 |
+ div.after(filelist); |
|
302 |
+} |
|
303 |
+ |
|
304 |
+nforge.markdown.enable = function() { |
|
305 |
+ var that = { |
|
306 |
+ init: function(targets, action) { |
|
307 |
+ for(var i = 0; i < targets.length; i++) { |
|
308 |
+ var target = targets[i]; |
|
309 |
+ var tagname = target.tagName.toLowerCase(); |
|
310 |
+ if (tagname == 'textarea' || tagname == 'input' |
|
311 |
+ || target.contentEditable == 'true') { |
|
312 |
+ editor($(target)); |
|
313 |
+ fileUploader($(target), action); |
|
314 |
+ } else { |
|
315 |
+ viewer($(target)); |
|
316 |
+ fileDownloader($(target), action); |
|
317 |
+ } |
|
318 |
+ } |
|
46 | 319 |
} |
47 | 320 |
}; |
48 | 321 |
|
49 | 322 |
return that; |
50 |
-}; |
|
51 |
- |
|
52 |
-nforge.markdown.render = function (selector) { |
|
53 |
- var that; |
|
54 |
- |
|
55 |
- that = { |
|
56 |
- init : function (selector) { |
|
57 |
- if (!selector) selector = '#body'; |
|
58 |
- $(selector).html(markdownRender($(selector).text())); |
|
59 |
- } |
|
60 |
- }; |
|
61 |
- |
|
62 |
- return that; |
|
63 |
-}; |
|
323 |
+} |
--- test/models/IssueTest.java
+++ test/models/IssueTest.java
... | ... | @@ -104,8 +104,7 @@ |
104 | 104 |
|
105 | 105 |
// Given |
106 | 106 |
// When |
107 |
- Page<Issue> issues = Issue.findFilteredIssues("nForge4java", "로그", StateType.OPEN, false, |
|
108 |
- true); |
|
107 |
+ Page<Issue> issues = Issue.findFilteredIssues("nForge4java", "로그", StateType.OPEN, false); |
|
109 | 108 |
// Then |
110 | 109 |
assertThat(issues.getTotalRowCount()).isEqualTo(1); |
111 | 110 |
|
... | ... | @@ -116,15 +115,6 @@ |
116 | 115 |
// Given |
117 | 116 |
// When |
118 | 117 |
Page<Issue> issues = Issue.findCommentedIssues("nForge4java", ""); |
119 |
- // Then |
|
120 |
- assertThat(issues.getTotalRowCount()).isEqualTo(1); |
|
121 |
- } |
|
122 |
- |
|
123 |
- @Test |
|
124 |
- public void findFileAttachedIssue() throws Exception { |
|
125 |
- // Given |
|
126 |
- // When |
|
127 |
- Page<Issue> issues = Issue.findFileAttachedIssues("nForge4java", ""); |
|
128 | 118 |
// Then |
129 | 119 |
assertThat(issues.getTotalRowCount()).isEqualTo(1); |
130 | 120 |
} |
Add a comment
Delete comment
Once you delete this comment, you won't be able to recover it. Are you sure you want to delete this comment?