##// END OF EJS Templates
uploads: handle server errors nicer
marcink -
r3994:0ef8f167 default
parent child Browse files
Show More
@@ -1,927 +1,927 b''
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 var firefoxAnchorFix = function() {
19 var firefoxAnchorFix = function() {
20 // hack to make anchor links behave properly on firefox, in our inline
20 // hack to make anchor links behave properly on firefox, in our inline
21 // comments generation when comments are injected firefox is misbehaving
21 // comments generation when comments are injected firefox is misbehaving
22 // when jumping to anchor links
22 // when jumping to anchor links
23 if (location.href.indexOf('#') > -1) {
23 if (location.href.indexOf('#') > -1) {
24 location.href += '';
24 location.href += '';
25 }
25 }
26 };
26 };
27
27
28 var linkifyComments = function(comments) {
28 var linkifyComments = function(comments) {
29 var firstCommentId = null;
29 var firstCommentId = null;
30 if (comments) {
30 if (comments) {
31 firstCommentId = $(comments[0]).data('comment-id');
31 firstCommentId = $(comments[0]).data('comment-id');
32 }
32 }
33
33
34 if (firstCommentId){
34 if (firstCommentId){
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 }
36 }
37 };
37 };
38
38
39 var bindToggleButtons = function() {
39 var bindToggleButtons = function() {
40 $('.comment-toggle').on('click', function() {
40 $('.comment-toggle').on('click', function() {
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 });
42 });
43 };
43 };
44
44
45
45
46
46
47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
48 failHandler = failHandler || function() {};
48 failHandler = failHandler || function() {};
49 postData = toQueryString(postData);
49 postData = toQueryString(postData);
50 var request = $.ajax({
50 var request = $.ajax({
51 url: url,
51 url: url,
52 type: 'POST',
52 type: 'POST',
53 data: postData,
53 data: postData,
54 headers: {'X-PARTIAL-XHR': true}
54 headers: {'X-PARTIAL-XHR': true}
55 })
55 })
56 .done(function (data) {
56 .done(function (data) {
57 successHandler(data);
57 successHandler(data);
58 })
58 })
59 .fail(function (data, textStatus, errorThrown) {
59 .fail(function (data, textStatus, errorThrown) {
60 failHandler(data, textStatus, errorThrown)
60 failHandler(data, textStatus, errorThrown)
61 });
61 });
62 return request;
62 return request;
63 };
63 };
64
64
65
65
66
66
67
67
68 /* Comment form for main and inline comments */
68 /* Comment form for main and inline comments */
69 (function(mod) {
69 (function(mod) {
70
70
71 if (typeof exports == "object" && typeof module == "object") {
71 if (typeof exports == "object" && typeof module == "object") {
72 // CommonJS
72 // CommonJS
73 module.exports = mod();
73 module.exports = mod();
74 }
74 }
75 else {
75 else {
76 // Plain browser env
76 // Plain browser env
77 (this || window).CommentForm = mod();
77 (this || window).CommentForm = mod();
78 }
78 }
79
79
80 })(function() {
80 })(function() {
81 "use strict";
81 "use strict";
82
82
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
84 if (!(this instanceof CommentForm)) {
84 if (!(this instanceof CommentForm)) {
85 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
85 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
86 }
86 }
87
87
88 // bind the element instance to our Form
88 // bind the element instance to our Form
89 $(formElement).get(0).CommentForm = this;
89 $(formElement).get(0).CommentForm = this;
90
90
91 this.withLineNo = function(selector) {
91 this.withLineNo = function(selector) {
92 var lineNo = this.lineNo;
92 var lineNo = this.lineNo;
93 if (lineNo === undefined) {
93 if (lineNo === undefined) {
94 return selector
94 return selector
95 } else {
95 } else {
96 return selector + '_' + lineNo;
96 return selector + '_' + lineNo;
97 }
97 }
98 };
98 };
99
99
100 this.commitId = commitId;
100 this.commitId = commitId;
101 this.pullRequestId = pullRequestId;
101 this.pullRequestId = pullRequestId;
102 this.lineNo = lineNo;
102 this.lineNo = lineNo;
103 this.initAutocompleteActions = initAutocompleteActions;
103 this.initAutocompleteActions = initAutocompleteActions;
104
104
105 this.previewButton = this.withLineNo('#preview-btn');
105 this.previewButton = this.withLineNo('#preview-btn');
106 this.previewContainer = this.withLineNo('#preview-container');
106 this.previewContainer = this.withLineNo('#preview-container');
107
107
108 this.previewBoxSelector = this.withLineNo('#preview-box');
108 this.previewBoxSelector = this.withLineNo('#preview-box');
109
109
110 this.editButton = this.withLineNo('#edit-btn');
110 this.editButton = this.withLineNo('#edit-btn');
111 this.editContainer = this.withLineNo('#edit-container');
111 this.editContainer = this.withLineNo('#edit-container');
112 this.cancelButton = this.withLineNo('#cancel-btn');
112 this.cancelButton = this.withLineNo('#cancel-btn');
113 this.commentType = this.withLineNo('#comment_type');
113 this.commentType = this.withLineNo('#comment_type');
114
114
115 this.resolvesId = null;
115 this.resolvesId = null;
116 this.resolvesActionId = null;
116 this.resolvesActionId = null;
117
117
118 this.closesPr = '#close_pull_request';
118 this.closesPr = '#close_pull_request';
119
119
120 this.cmBox = this.withLineNo('#text');
120 this.cmBox = this.withLineNo('#text');
121 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
121 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
122
122
123 this.statusChange = this.withLineNo('#change_status');
123 this.statusChange = this.withLineNo('#change_status');
124
124
125 this.submitForm = formElement;
125 this.submitForm = formElement;
126 this.submitButton = $(this.submitForm).find('input[type="submit"]');
126 this.submitButton = $(this.submitForm).find('input[type="submit"]');
127 this.submitButtonText = this.submitButton.val();
127 this.submitButtonText = this.submitButton.val();
128
128
129 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
129 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
130 {'repo_name': templateContext.repo_name,
130 {'repo_name': templateContext.repo_name,
131 'commit_id': templateContext.commit_data.commit_id});
131 'commit_id': templateContext.commit_data.commit_id});
132
132
133 if (resolvesCommentId){
133 if (resolvesCommentId){
134 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
134 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
135 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
135 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
136 $(this.commentType).prop('disabled', true);
136 $(this.commentType).prop('disabled', true);
137 $(this.commentType).addClass('disabled');
137 $(this.commentType).addClass('disabled');
138
138
139 // disable select
139 // disable select
140 setTimeout(function() {
140 setTimeout(function() {
141 $(self.statusChange).select2('readonly', true);
141 $(self.statusChange).select2('readonly', true);
142 }, 10);
142 }, 10);
143
143
144 var resolvedInfo = (
144 var resolvedInfo = (
145 '<li class="resolve-action">' +
145 '<li class="resolve-action">' +
146 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
146 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
147 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
147 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
148 '</li>'
148 '</li>'
149 ).format(resolvesCommentId, _gettext('resolve comment'));
149 ).format(resolvesCommentId, _gettext('resolve comment'));
150 $(resolvedInfo).insertAfter($(this.commentType).parent());
150 $(resolvedInfo).insertAfter($(this.commentType).parent());
151 }
151 }
152
152
153 // based on commitId, or pullRequestId decide where do we submit
153 // based on commitId, or pullRequestId decide where do we submit
154 // out data
154 // out data
155 if (this.commitId){
155 if (this.commitId){
156 this.submitUrl = pyroutes.url('repo_commit_comment_create',
156 this.submitUrl = pyroutes.url('repo_commit_comment_create',
157 {'repo_name': templateContext.repo_name,
157 {'repo_name': templateContext.repo_name,
158 'commit_id': this.commitId});
158 'commit_id': this.commitId});
159 this.selfUrl = pyroutes.url('repo_commit',
159 this.selfUrl = pyroutes.url('repo_commit',
160 {'repo_name': templateContext.repo_name,
160 {'repo_name': templateContext.repo_name,
161 'commit_id': this.commitId});
161 'commit_id': this.commitId});
162
162
163 } else if (this.pullRequestId) {
163 } else if (this.pullRequestId) {
164 this.submitUrl = pyroutes.url('pullrequest_comment_create',
164 this.submitUrl = pyroutes.url('pullrequest_comment_create',
165 {'repo_name': templateContext.repo_name,
165 {'repo_name': templateContext.repo_name,
166 'pull_request_id': this.pullRequestId});
166 'pull_request_id': this.pullRequestId});
167 this.selfUrl = pyroutes.url('pullrequest_show',
167 this.selfUrl = pyroutes.url('pullrequest_show',
168 {'repo_name': templateContext.repo_name,
168 {'repo_name': templateContext.repo_name,
169 'pull_request_id': this.pullRequestId});
169 'pull_request_id': this.pullRequestId});
170
170
171 } else {
171 } else {
172 throw new Error(
172 throw new Error(
173 'CommentForm requires pullRequestId, or commitId to be specified.')
173 'CommentForm requires pullRequestId, or commitId to be specified.')
174 }
174 }
175
175
176 // FUNCTIONS and helpers
176 // FUNCTIONS and helpers
177 var self = this;
177 var self = this;
178
178
179 this.isInline = function(){
179 this.isInline = function(){
180 return this.lineNo && this.lineNo != 'general';
180 return this.lineNo && this.lineNo != 'general';
181 };
181 };
182
182
183 this.getCmInstance = function(){
183 this.getCmInstance = function(){
184 return this.cm
184 return this.cm
185 };
185 };
186
186
187 this.setPlaceholder = function(placeholder) {
187 this.setPlaceholder = function(placeholder) {
188 var cm = this.getCmInstance();
188 var cm = this.getCmInstance();
189 if (cm){
189 if (cm){
190 cm.setOption('placeholder', placeholder);
190 cm.setOption('placeholder', placeholder);
191 }
191 }
192 };
192 };
193
193
194 this.getCommentStatus = function() {
194 this.getCommentStatus = function() {
195 return $(this.submitForm).find(this.statusChange).val();
195 return $(this.submitForm).find(this.statusChange).val();
196 };
196 };
197 this.getCommentType = function() {
197 this.getCommentType = function() {
198 return $(this.submitForm).find(this.commentType).val();
198 return $(this.submitForm).find(this.commentType).val();
199 };
199 };
200
200
201 this.getResolvesId = function() {
201 this.getResolvesId = function() {
202 return $(this.submitForm).find(this.resolvesId).val() || null;
202 return $(this.submitForm).find(this.resolvesId).val() || null;
203 };
203 };
204
204
205 this.getClosePr = function() {
205 this.getClosePr = function() {
206 return $(this.submitForm).find(this.closesPr).val() || null;
206 return $(this.submitForm).find(this.closesPr).val() || null;
207 };
207 };
208
208
209 this.markCommentResolved = function(resolvedCommentId){
209 this.markCommentResolved = function(resolvedCommentId){
210 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
210 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
211 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
211 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
212 };
212 };
213
213
214 this.isAllowedToSubmit = function() {
214 this.isAllowedToSubmit = function() {
215 return !$(this.submitButton).prop('disabled');
215 return !$(this.submitButton).prop('disabled');
216 };
216 };
217
217
218 this.initStatusChangeSelector = function(){
218 this.initStatusChangeSelector = function(){
219 var formatChangeStatus = function(state, escapeMarkup) {
219 var formatChangeStatus = function(state, escapeMarkup) {
220 var originalOption = state.element;
220 var originalOption = state.element;
221 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
221 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
222 return tmpl
222 return tmpl
223 };
223 };
224 var formatResult = function(result, container, query, escapeMarkup) {
224 var formatResult = function(result, container, query, escapeMarkup) {
225 return formatChangeStatus(result, escapeMarkup);
225 return formatChangeStatus(result, escapeMarkup);
226 };
226 };
227
227
228 var formatSelection = function(data, container, escapeMarkup) {
228 var formatSelection = function(data, container, escapeMarkup) {
229 return formatChangeStatus(data, escapeMarkup);
229 return formatChangeStatus(data, escapeMarkup);
230 };
230 };
231
231
232 $(this.submitForm).find(this.statusChange).select2({
232 $(this.submitForm).find(this.statusChange).select2({
233 placeholder: _gettext('Status Review'),
233 placeholder: _gettext('Status Review'),
234 formatResult: formatResult,
234 formatResult: formatResult,
235 formatSelection: formatSelection,
235 formatSelection: formatSelection,
236 containerCssClass: "drop-menu status_box_menu",
236 containerCssClass: "drop-menu status_box_menu",
237 dropdownCssClass: "drop-menu-dropdown",
237 dropdownCssClass: "drop-menu-dropdown",
238 dropdownAutoWidth: true,
238 dropdownAutoWidth: true,
239 minimumResultsForSearch: -1
239 minimumResultsForSearch: -1
240 });
240 });
241 $(this.submitForm).find(this.statusChange).on('change', function() {
241 $(this.submitForm).find(this.statusChange).on('change', function() {
242 var status = self.getCommentStatus();
242 var status = self.getCommentStatus();
243
243
244 if (status && !self.isInline()) {
244 if (status && !self.isInline()) {
245 $(self.submitButton).prop('disabled', false);
245 $(self.submitButton).prop('disabled', false);
246 }
246 }
247
247
248 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
248 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
249 self.setPlaceholder(placeholderText)
249 self.setPlaceholder(placeholderText)
250 })
250 })
251 };
251 };
252
252
253 // reset the comment form into it's original state
253 // reset the comment form into it's original state
254 this.resetCommentFormState = function(content) {
254 this.resetCommentFormState = function(content) {
255 content = content || '';
255 content = content || '';
256
256
257 $(this.editContainer).show();
257 $(this.editContainer).show();
258 $(this.editButton).parent().addClass('active');
258 $(this.editButton).parent().addClass('active');
259
259
260 $(this.previewContainer).hide();
260 $(this.previewContainer).hide();
261 $(this.previewButton).parent().removeClass('active');
261 $(this.previewButton).parent().removeClass('active');
262
262
263 this.setActionButtonsDisabled(true);
263 this.setActionButtonsDisabled(true);
264 self.cm.setValue(content);
264 self.cm.setValue(content);
265 self.cm.setOption("readOnly", false);
265 self.cm.setOption("readOnly", false);
266
266
267 if (this.resolvesId) {
267 if (this.resolvesId) {
268 // destroy the resolve action
268 // destroy the resolve action
269 $(this.resolvesId).parent().remove();
269 $(this.resolvesId).parent().remove();
270 }
270 }
271 // reset closingPR flag
271 // reset closingPR flag
272 $('.close-pr-input').remove();
272 $('.close-pr-input').remove();
273
273
274 $(this.statusChange).select2('readonly', false);
274 $(this.statusChange).select2('readonly', false);
275 };
275 };
276
276
277 this.globalSubmitSuccessCallback = function(){
277 this.globalSubmitSuccessCallback = function(){
278 // default behaviour is to call GLOBAL hook, if it's registered.
278 // default behaviour is to call GLOBAL hook, if it's registered.
279 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
279 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
280 commentFormGlobalSubmitSuccessCallback()
280 commentFormGlobalSubmitSuccessCallback()
281 }
281 }
282 };
282 };
283
283
284 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
284 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
285 return _submitAjaxPOST(url, postData, successHandler, failHandler);
285 return _submitAjaxPOST(url, postData, successHandler, failHandler);
286 };
286 };
287
287
288 // overwrite a submitHandler, we need to do it for inline comments
288 // overwrite a submitHandler, we need to do it for inline comments
289 this.setHandleFormSubmit = function(callback) {
289 this.setHandleFormSubmit = function(callback) {
290 this.handleFormSubmit = callback;
290 this.handleFormSubmit = callback;
291 };
291 };
292
292
293 // overwrite a submitSuccessHandler
293 // overwrite a submitSuccessHandler
294 this.setGlobalSubmitSuccessCallback = function(callback) {
294 this.setGlobalSubmitSuccessCallback = function(callback) {
295 this.globalSubmitSuccessCallback = callback;
295 this.globalSubmitSuccessCallback = callback;
296 };
296 };
297
297
298 // default handler for for submit for main comments
298 // default handler for for submit for main comments
299 this.handleFormSubmit = function() {
299 this.handleFormSubmit = function() {
300 var text = self.cm.getValue();
300 var text = self.cm.getValue();
301 var status = self.getCommentStatus();
301 var status = self.getCommentStatus();
302 var commentType = self.getCommentType();
302 var commentType = self.getCommentType();
303 var resolvesCommentId = self.getResolvesId();
303 var resolvesCommentId = self.getResolvesId();
304 var closePullRequest = self.getClosePr();
304 var closePullRequest = self.getClosePr();
305
305
306 if (text === "" && !status) {
306 if (text === "" && !status) {
307 return;
307 return;
308 }
308 }
309
309
310 var excludeCancelBtn = false;
310 var excludeCancelBtn = false;
311 var submitEvent = true;
311 var submitEvent = true;
312 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
312 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
313 self.cm.setOption("readOnly", true);
313 self.cm.setOption("readOnly", true);
314
314
315 var postData = {
315 var postData = {
316 'text': text,
316 'text': text,
317 'changeset_status': status,
317 'changeset_status': status,
318 'comment_type': commentType,
318 'comment_type': commentType,
319 'csrf_token': CSRF_TOKEN
319 'csrf_token': CSRF_TOKEN
320 };
320 };
321
321
322 if (resolvesCommentId) {
322 if (resolvesCommentId) {
323 postData['resolves_comment_id'] = resolvesCommentId;
323 postData['resolves_comment_id'] = resolvesCommentId;
324 }
324 }
325
325
326 if (closePullRequest) {
326 if (closePullRequest) {
327 postData['close_pull_request'] = true;
327 postData['close_pull_request'] = true;
328 }
328 }
329
329
330 var submitSuccessCallback = function(o) {
330 var submitSuccessCallback = function(o) {
331 // reload page if we change status for single commit.
331 // reload page if we change status for single commit.
332 if (status && self.commitId) {
332 if (status && self.commitId) {
333 location.reload(true);
333 location.reload(true);
334 } else {
334 } else {
335 $('#injected_page_comments').append(o.rendered_text);
335 $('#injected_page_comments').append(o.rendered_text);
336 self.resetCommentFormState();
336 self.resetCommentFormState();
337 timeagoActivate();
337 timeagoActivate();
338
338
339 // mark visually which comment was resolved
339 // mark visually which comment was resolved
340 if (resolvesCommentId) {
340 if (resolvesCommentId) {
341 self.markCommentResolved(resolvesCommentId);
341 self.markCommentResolved(resolvesCommentId);
342 }
342 }
343 }
343 }
344
344
345 // run global callback on submit
345 // run global callback on submit
346 self.globalSubmitSuccessCallback();
346 self.globalSubmitSuccessCallback();
347
347
348 };
348 };
349 var submitFailCallback = function(data) {
349 var submitFailCallback = function(data) {
350 alert(
350 alert(
351 "Error while submitting comment.\n" +
351 "Error while submitting comment.\n" +
352 "Error code {0} ({1}).".format(data.status, data.statusText)
352 "Error code {0} ({1}).".format(data.status, data.statusText)
353 );
353 );
354 self.resetCommentFormState(text);
354 self.resetCommentFormState(text);
355 };
355 };
356 self.submitAjaxPOST(
356 self.submitAjaxPOST(
357 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
357 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
358 };
358 };
359
359
360 this.previewSuccessCallback = function(o) {
360 this.previewSuccessCallback = function(o) {
361 $(self.previewBoxSelector).html(o);
361 $(self.previewBoxSelector).html(o);
362 $(self.previewBoxSelector).removeClass('unloaded');
362 $(self.previewBoxSelector).removeClass('unloaded');
363
363
364 // swap buttons, making preview active
364 // swap buttons, making preview active
365 $(self.previewButton).parent().addClass('active');
365 $(self.previewButton).parent().addClass('active');
366 $(self.editButton).parent().removeClass('active');
366 $(self.editButton).parent().removeClass('active');
367
367
368 // unlock buttons
368 // unlock buttons
369 self.setActionButtonsDisabled(false);
369 self.setActionButtonsDisabled(false);
370 };
370 };
371
371
372 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
372 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
373 excludeCancelBtn = excludeCancelBtn || false;
373 excludeCancelBtn = excludeCancelBtn || false;
374 submitEvent = submitEvent || false;
374 submitEvent = submitEvent || false;
375
375
376 $(this.editButton).prop('disabled', state);
376 $(this.editButton).prop('disabled', state);
377 $(this.previewButton).prop('disabled', state);
377 $(this.previewButton).prop('disabled', state);
378
378
379 if (!excludeCancelBtn) {
379 if (!excludeCancelBtn) {
380 $(this.cancelButton).prop('disabled', state);
380 $(this.cancelButton).prop('disabled', state);
381 }
381 }
382
382
383 var submitState = state;
383 var submitState = state;
384 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
384 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
385 // if the value of commit review status is set, we allow
385 // if the value of commit review status is set, we allow
386 // submit button, but only on Main form, isInline means inline
386 // submit button, but only on Main form, isInline means inline
387 submitState = false
387 submitState = false
388 }
388 }
389
389
390 $(this.submitButton).prop('disabled', submitState);
390 $(this.submitButton).prop('disabled', submitState);
391 if (submitEvent) {
391 if (submitEvent) {
392 $(this.submitButton).val(_gettext('Submitting...'));
392 $(this.submitButton).val(_gettext('Submitting...'));
393 } else {
393 } else {
394 $(this.submitButton).val(this.submitButtonText);
394 $(this.submitButton).val(this.submitButtonText);
395 }
395 }
396
396
397 };
397 };
398
398
399 // lock preview/edit/submit buttons on load, but exclude cancel button
399 // lock preview/edit/submit buttons on load, but exclude cancel button
400 var excludeCancelBtn = true;
400 var excludeCancelBtn = true;
401 this.setActionButtonsDisabled(true, excludeCancelBtn);
401 this.setActionButtonsDisabled(true, excludeCancelBtn);
402
402
403 // anonymous users don't have access to initialized CM instance
403 // anonymous users don't have access to initialized CM instance
404 if (this.cm !== undefined){
404 if (this.cm !== undefined){
405 this.cm.on('change', function(cMirror) {
405 this.cm.on('change', function(cMirror) {
406 if (cMirror.getValue() === "") {
406 if (cMirror.getValue() === "") {
407 self.setActionButtonsDisabled(true, excludeCancelBtn)
407 self.setActionButtonsDisabled(true, excludeCancelBtn)
408 } else {
408 } else {
409 self.setActionButtonsDisabled(false, excludeCancelBtn)
409 self.setActionButtonsDisabled(false, excludeCancelBtn)
410 }
410 }
411 });
411 });
412 }
412 }
413
413
414 $(this.editButton).on('click', function(e) {
414 $(this.editButton).on('click', function(e) {
415 e.preventDefault();
415 e.preventDefault();
416
416
417 $(self.previewButton).parent().removeClass('active');
417 $(self.previewButton).parent().removeClass('active');
418 $(self.previewContainer).hide();
418 $(self.previewContainer).hide();
419
419
420 $(self.editButton).parent().addClass('active');
420 $(self.editButton).parent().addClass('active');
421 $(self.editContainer).show();
421 $(self.editContainer).show();
422
422
423 });
423 });
424
424
425 $(this.previewButton).on('click', function(e) {
425 $(this.previewButton).on('click', function(e) {
426 e.preventDefault();
426 e.preventDefault();
427 var text = self.cm.getValue();
427 var text = self.cm.getValue();
428
428
429 if (text === "") {
429 if (text === "") {
430 return;
430 return;
431 }
431 }
432
432
433 var postData = {
433 var postData = {
434 'text': text,
434 'text': text,
435 'renderer': templateContext.visual.default_renderer,
435 'renderer': templateContext.visual.default_renderer,
436 'csrf_token': CSRF_TOKEN
436 'csrf_token': CSRF_TOKEN
437 };
437 };
438
438
439 // lock ALL buttons on preview
439 // lock ALL buttons on preview
440 self.setActionButtonsDisabled(true);
440 self.setActionButtonsDisabled(true);
441
441
442 $(self.previewBoxSelector).addClass('unloaded');
442 $(self.previewBoxSelector).addClass('unloaded');
443 $(self.previewBoxSelector).html(_gettext('Loading ...'));
443 $(self.previewBoxSelector).html(_gettext('Loading ...'));
444
444
445 $(self.editContainer).hide();
445 $(self.editContainer).hide();
446 $(self.previewContainer).show();
446 $(self.previewContainer).show();
447
447
448 // by default we reset state of comment preserving the text
448 // by default we reset state of comment preserving the text
449 var previewFailCallback = function(data){
449 var previewFailCallback = function(data){
450 alert(
450 alert(
451 "Error while preview of comment.\n" +
451 "Error while preview of comment.\n" +
452 "Error code {0} ({1}).".format(data.status, data.statusText)
452 "Error code {0} ({1}).".format(data.status, data.statusText)
453 );
453 );
454 self.resetCommentFormState(text)
454 self.resetCommentFormState(text)
455 };
455 };
456 self.submitAjaxPOST(
456 self.submitAjaxPOST(
457 self.previewUrl, postData, self.previewSuccessCallback,
457 self.previewUrl, postData, self.previewSuccessCallback,
458 previewFailCallback);
458 previewFailCallback);
459
459
460 $(self.previewButton).parent().addClass('active');
460 $(self.previewButton).parent().addClass('active');
461 $(self.editButton).parent().removeClass('active');
461 $(self.editButton).parent().removeClass('active');
462 });
462 });
463
463
464 $(this.submitForm).submit(function(e) {
464 $(this.submitForm).submit(function(e) {
465 e.preventDefault();
465 e.preventDefault();
466 var allowedToSubmit = self.isAllowedToSubmit();
466 var allowedToSubmit = self.isAllowedToSubmit();
467 if (!allowedToSubmit){
467 if (!allowedToSubmit){
468 return false;
468 return false;
469 }
469 }
470 self.handleFormSubmit();
470 self.handleFormSubmit();
471 });
471 });
472
472
473 }
473 }
474
474
475 return CommentForm;
475 return CommentForm;
476 });
476 });
477
477
478 /* comments controller */
478 /* comments controller */
479 var CommentsController = function() {
479 var CommentsController = function() {
480 var mainComment = '#text';
480 var mainComment = '#text';
481 var self = this;
481 var self = this;
482
482
483 this.cancelComment = function(node) {
483 this.cancelComment = function(node) {
484 var $node = $(node);
484 var $node = $(node);
485 var $td = $node.closest('td');
485 var $td = $node.closest('td');
486 $node.closest('.comment-inline-form').remove();
486 $node.closest('.comment-inline-form').remove();
487 return false;
487 return false;
488 };
488 };
489
489
490 this.getLineNumber = function(node) {
490 this.getLineNumber = function(node) {
491 var $node = $(node);
491 var $node = $(node);
492 var lineNo = $node.closest('td').attr('data-line-no');
492 var lineNo = $node.closest('td').attr('data-line-no');
493 if (lineNo === undefined && $node.data('commentInline')){
493 if (lineNo === undefined && $node.data('commentInline')){
494 lineNo = $node.data('commentLineNo')
494 lineNo = $node.data('commentLineNo')
495 }
495 }
496
496
497 return lineNo
497 return lineNo
498 };
498 };
499
499
500 this.scrollToComment = function(node, offset, outdated) {
500 this.scrollToComment = function(node, offset, outdated) {
501 if (offset === undefined) {
501 if (offset === undefined) {
502 offset = 0;
502 offset = 0;
503 }
503 }
504 var outdated = outdated || false;
504 var outdated = outdated || false;
505 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
505 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
506
506
507 if (!node) {
507 if (!node) {
508 node = $('.comment-selected');
508 node = $('.comment-selected');
509 if (!node.length) {
509 if (!node.length) {
510 node = $('comment-current')
510 node = $('comment-current')
511 }
511 }
512 }
512 }
513 $wrapper = $(node).closest('div.comment');
513 $wrapper = $(node).closest('div.comment');
514 $comment = $(node).closest(klass);
514 $comment = $(node).closest(klass);
515 $comments = $(klass);
515 $comments = $(klass);
516
516
517 // show hidden comment when referenced.
517 // show hidden comment when referenced.
518 if (!$wrapper.is(':visible')){
518 if (!$wrapper.is(':visible')){
519 $wrapper.show();
519 $wrapper.show();
520 }
520 }
521
521
522 $('.comment-selected').removeClass('comment-selected');
522 $('.comment-selected').removeClass('comment-selected');
523
523
524 var nextIdx = $(klass).index($comment) + offset;
524 var nextIdx = $(klass).index($comment) + offset;
525 if (nextIdx >= $comments.length) {
525 if (nextIdx >= $comments.length) {
526 nextIdx = 0;
526 nextIdx = 0;
527 }
527 }
528 var $next = $(klass).eq(nextIdx);
528 var $next = $(klass).eq(nextIdx);
529
529
530 var $cb = $next.closest('.cb');
530 var $cb = $next.closest('.cb');
531 $cb.removeClass('cb-collapsed');
531 $cb.removeClass('cb-collapsed');
532
532
533 var $filediffCollapseState = $cb.closest('.filediff').prev();
533 var $filediffCollapseState = $cb.closest('.filediff').prev();
534 $filediffCollapseState.prop('checked', false);
534 $filediffCollapseState.prop('checked', false);
535 $next.addClass('comment-selected');
535 $next.addClass('comment-selected');
536 scrollToElement($next);
536 scrollToElement($next);
537 return false;
537 return false;
538 };
538 };
539
539
540 this.nextComment = function(node) {
540 this.nextComment = function(node) {
541 return self.scrollToComment(node, 1);
541 return self.scrollToComment(node, 1);
542 };
542 };
543
543
544 this.prevComment = function(node) {
544 this.prevComment = function(node) {
545 return self.scrollToComment(node, -1);
545 return self.scrollToComment(node, -1);
546 };
546 };
547
547
548 this.nextOutdatedComment = function(node) {
548 this.nextOutdatedComment = function(node) {
549 return self.scrollToComment(node, 1, true);
549 return self.scrollToComment(node, 1, true);
550 };
550 };
551
551
552 this.prevOutdatedComment = function(node) {
552 this.prevOutdatedComment = function(node) {
553 return self.scrollToComment(node, -1, true);
553 return self.scrollToComment(node, -1, true);
554 };
554 };
555
555
556 this.deleteComment = function(node) {
556 this.deleteComment = function(node) {
557 if (!confirm(_gettext('Delete this comment?'))) {
557 if (!confirm(_gettext('Delete this comment?'))) {
558 return false;
558 return false;
559 }
559 }
560 var $node = $(node);
560 var $node = $(node);
561 var $td = $node.closest('td');
561 var $td = $node.closest('td');
562 var $comment = $node.closest('.comment');
562 var $comment = $node.closest('.comment');
563 var comment_id = $comment.attr('data-comment-id');
563 var comment_id = $comment.attr('data-comment-id');
564 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
564 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
565 var postData = {
565 var postData = {
566 'csrf_token': CSRF_TOKEN
566 'csrf_token': CSRF_TOKEN
567 };
567 };
568
568
569 $comment.addClass('comment-deleting');
569 $comment.addClass('comment-deleting');
570 $comment.hide('fast');
570 $comment.hide('fast');
571
571
572 var success = function(response) {
572 var success = function(response) {
573 $comment.remove();
573 $comment.remove();
574 return false;
574 return false;
575 };
575 };
576 var failure = function(data, textStatus, xhr) {
576 var failure = function(data, textStatus, xhr) {
577 alert("error processing request: " + textStatus);
577 alert("error processing request: " + textStatus);
578 $comment.show('fast');
578 $comment.show('fast');
579 $comment.removeClass('comment-deleting');
579 $comment.removeClass('comment-deleting');
580 return false;
580 return false;
581 };
581 };
582 ajaxPOST(url, postData, success, failure);
582 ajaxPOST(url, postData, success, failure);
583 };
583 };
584
584
585 this.toggleWideMode = function (node) {
585 this.toggleWideMode = function (node) {
586 if ($('#content').hasClass('wrapper')) {
586 if ($('#content').hasClass('wrapper')) {
587 $('#content').removeClass("wrapper");
587 $('#content').removeClass("wrapper");
588 $('#content').addClass("wide-mode-wrapper");
588 $('#content').addClass("wide-mode-wrapper");
589 $(node).addClass('btn-success');
589 $(node).addClass('btn-success');
590 return true
590 return true
591 } else {
591 } else {
592 $('#content').removeClass("wide-mode-wrapper");
592 $('#content').removeClass("wide-mode-wrapper");
593 $('#content').addClass("wrapper");
593 $('#content').addClass("wrapper");
594 $(node).removeClass('btn-success');
594 $(node).removeClass('btn-success');
595 return false
595 return false
596 }
596 }
597
597
598 };
598 };
599
599
600 this.toggleComments = function(node, show) {
600 this.toggleComments = function(node, show) {
601 var $filediff = $(node).closest('.filediff');
601 var $filediff = $(node).closest('.filediff');
602 if (show === true) {
602 if (show === true) {
603 $filediff.removeClass('hide-comments');
603 $filediff.removeClass('hide-comments');
604 } else if (show === false) {
604 } else if (show === false) {
605 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
605 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
606 $filediff.addClass('hide-comments');
606 $filediff.addClass('hide-comments');
607 } else {
607 } else {
608 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
608 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
609 $filediff.toggleClass('hide-comments');
609 $filediff.toggleClass('hide-comments');
610 }
610 }
611 return false;
611 return false;
612 };
612 };
613
613
614 this.toggleLineComments = function(node) {
614 this.toggleLineComments = function(node) {
615 self.toggleComments(node, true);
615 self.toggleComments(node, true);
616 var $node = $(node);
616 var $node = $(node);
617 // mark outdated comments as visible before the toggle;
617 // mark outdated comments as visible before the toggle;
618 $(node.closest('tr')).find('.comment-outdated').show();
618 $(node.closest('tr')).find('.comment-outdated').show();
619 $node.closest('tr').toggleClass('hide-line-comments');
619 $node.closest('tr').toggleClass('hide-line-comments');
620 };
620 };
621
621
622 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
622 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
623 var pullRequestId = templateContext.pull_request_data.pull_request_id;
623 var pullRequestId = templateContext.pull_request_data.pull_request_id;
624 var commitId = templateContext.commit_data.commit_id;
624 var commitId = templateContext.commit_data.commit_id;
625
625
626 var commentForm = new CommentForm(
626 var commentForm = new CommentForm(
627 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
627 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
628 var cm = commentForm.getCmInstance();
628 var cm = commentForm.getCmInstance();
629
629
630 if (resolvesCommentId){
630 if (resolvesCommentId){
631 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
631 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
632 }
632 }
633
633
634 setTimeout(function() {
634 setTimeout(function() {
635 // callbacks
635 // callbacks
636 if (cm !== undefined) {
636 if (cm !== undefined) {
637 commentForm.setPlaceholder(placeholderText);
637 commentForm.setPlaceholder(placeholderText);
638 if (commentForm.isInline()) {
638 if (commentForm.isInline()) {
639 cm.focus();
639 cm.focus();
640 cm.refresh();
640 cm.refresh();
641 }
641 }
642 }
642 }
643 }, 10);
643 }, 10);
644
644
645 // trigger scrolldown to the resolve comment, since it might be away
645 // trigger scrolldown to the resolve comment, since it might be away
646 // from the clicked
646 // from the clicked
647 if (resolvesCommentId){
647 if (resolvesCommentId){
648 var actionNode = $(commentForm.resolvesActionId).offset();
648 var actionNode = $(commentForm.resolvesActionId).offset();
649
649
650 setTimeout(function() {
650 setTimeout(function() {
651 if (actionNode) {
651 if (actionNode) {
652 $('body, html').animate({scrollTop: actionNode.top}, 10);
652 $('body, html').animate({scrollTop: actionNode.top}, 10);
653 }
653 }
654 }, 100);
654 }, 100);
655 }
655 }
656
656
657 // add dropzone support
657 // add dropzone support
658 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
658 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
659 var renderer = templateContext.visual.default_renderer;
659 var renderer = templateContext.visual.default_renderer;
660 if (renderer == 'rst') {
660 if (renderer == 'rst') {
661 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
661 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
662 if (isRendered){
662 if (isRendered){
663 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
663 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
664 }
664 }
665 } else if (renderer == 'markdown') {
665 } else if (renderer == 'markdown') {
666 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
666 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
667 if (isRendered){
667 if (isRendered){
668 attachmentUrl = '!' + attachmentUrl;
668 attachmentUrl = '!' + attachmentUrl;
669 }
669 }
670 } else {
670 } else {
671 var attachmentUrl = '{}'.format(attachmentStoreUrl);
671 var attachmentUrl = '{}'.format(attachmentStoreUrl);
672 }
672 }
673 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
673 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
674
674
675 return false;
675 return false;
676 };
676 };
677
677
678 //see: https://www.dropzonejs.com/#configuration
678 //see: https://www.dropzonejs.com/#configuration
679 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
679 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
680 {'repo_name': templateContext.repo_name,
680 {'repo_name': templateContext.repo_name,
681 'commit_id': templateContext.commit_data.commit_id})
681 'commit_id': templateContext.commit_data.commit_id})
682
682
683 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
683 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
684 if (previewTmpl !== undefined){
684 if (previewTmpl !== undefined){
685 var selectLink = $(formElement).find('.pick-attachment').get(0);
685 var selectLink = $(formElement).find('.pick-attachment').get(0);
686 $(formElement).find('.comment-attachment-uploader').dropzone({
686 $(formElement).find('.comment-attachment-uploader').dropzone({
687 url: storeUrl,
687 url: storeUrl,
688 headers: {"X-CSRF-Token": CSRF_TOKEN},
688 headers: {"X-CSRF-Token": CSRF_TOKEN},
689 paramName: function () {
689 paramName: function () {
690 return "attachment"
690 return "attachment"
691 }, // The name that will be used to transfer the file
691 }, // The name that will be used to transfer the file
692 clickable: selectLink,
692 clickable: selectLink,
693 parallelUploads: 1,
693 parallelUploads: 1,
694 maxFiles: 10,
694 maxFiles: 10,
695 maxFilesize: templateContext.attachment_store.max_file_size_mb,
695 maxFilesize: templateContext.attachment_store.max_file_size_mb,
696 uploadMultiple: false,
696 uploadMultiple: false,
697 autoProcessQueue: true, // if false queue will not be processed automatically.
697 autoProcessQueue: true, // if false queue will not be processed automatically.
698 createImageThumbnails: false,
698 createImageThumbnails: false,
699 previewTemplate: previewTmpl.innerHTML,
699 previewTemplate: previewTmpl.innerHTML,
700
700
701 accept: function (file, done) {
701 accept: function (file, done) {
702 done();
702 done();
703 },
703 },
704 init: function () {
704 init: function () {
705
705
706 this.on("sending", function (file, xhr, formData) {
706 this.on("sending", function (file, xhr, formData) {
707 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
707 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
708 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
708 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
709 });
709 });
710
710
711 this.on("success", function (file, response) {
711 this.on("success", function (file, response) {
712 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
712 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
713 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
713 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
714
714
715 var isRendered = false;
715 var isRendered = false;
716 var ext = file.name.split('.').pop();
716 var ext = file.name.split('.').pop();
717 var imageExts = templateContext.attachment_store.image_ext;
717 var imageExts = templateContext.attachment_store.image_ext;
718 if (imageExts.indexOf(ext) !== -1){
718 if (imageExts.indexOf(ext) !== -1){
719 isRendered = true;
719 isRendered = true;
720 }
720 }
721
721
722 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
722 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
723 });
723 });
724
724
725 this.on("error", function (file, errorMessage, xhr) {
725 this.on("error", function (file, errorMessage, xhr) {
726 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
726 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
727
727
728 var error = null;
728 var error = null;
729
729
730 if (xhr !== undefined){
730 if (xhr !== undefined){
731 var httpStatus = xhr.status + " " + xhr.statusText;
731 var httpStatus = xhr.status + " " + xhr.statusText;
732 if (xhr.status >= 500) {
732 if (xhr !== undefined && xhr.status >= 500) {
733 error = httpStatus;
733 error = httpStatus;
734 }
734 }
735 }
735 }
736
736
737 if (error === null) {
737 if (error === null) {
738 error = errorMessage.error || errorMessage || httpStatus;
738 error = errorMessage.error || errorMessage || httpStatus;
739 }
739 }
740 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
740 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
741
741
742 });
742 });
743 }
743 }
744 });
744 });
745 }
745 }
746 return commentForm;
746 return commentForm;
747 };
747 };
748
748
749 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
749 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
750
750
751 var tmpl = $('#cb-comment-general-form-template').html();
751 var tmpl = $('#cb-comment-general-form-template').html();
752 tmpl = tmpl.format(null, 'general');
752 tmpl = tmpl.format(null, 'general');
753 var $form = $(tmpl);
753 var $form = $(tmpl);
754
754
755 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
755 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
756 var curForm = $formPlaceholder.find('form');
756 var curForm = $formPlaceholder.find('form');
757 if (curForm){
757 if (curForm){
758 curForm.remove();
758 curForm.remove();
759 }
759 }
760 $formPlaceholder.append($form);
760 $formPlaceholder.append($form);
761
761
762 var _form = $($form[0]);
762 var _form = $($form[0]);
763 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
763 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
764 var commentForm = this.createCommentForm(
764 var commentForm = this.createCommentForm(
765 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
765 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
766 commentForm.initStatusChangeSelector();
766 commentForm.initStatusChangeSelector();
767
767
768 return commentForm;
768 return commentForm;
769 };
769 };
770
770
771 this.createComment = function(node, resolutionComment) {
771 this.createComment = function(node, resolutionComment) {
772 var resolvesCommentId = resolutionComment || null;
772 var resolvesCommentId = resolutionComment || null;
773 var $node = $(node);
773 var $node = $(node);
774 var $td = $node.closest('td');
774 var $td = $node.closest('td');
775 var $form = $td.find('.comment-inline-form');
775 var $form = $td.find('.comment-inline-form');
776
776
777 if (!$form.length) {
777 if (!$form.length) {
778
778
779 var $filediff = $node.closest('.filediff');
779 var $filediff = $node.closest('.filediff');
780 $filediff.removeClass('hide-comments');
780 $filediff.removeClass('hide-comments');
781 var f_path = $filediff.attr('data-f-path');
781 var f_path = $filediff.attr('data-f-path');
782 var lineno = self.getLineNumber(node);
782 var lineno = self.getLineNumber(node);
783 // create a new HTML from template
783 // create a new HTML from template
784 var tmpl = $('#cb-comment-inline-form-template').html();
784 var tmpl = $('#cb-comment-inline-form-template').html();
785 tmpl = tmpl.format(escapeHtml(f_path), lineno);
785 tmpl = tmpl.format(escapeHtml(f_path), lineno);
786 $form = $(tmpl);
786 $form = $(tmpl);
787
787
788 var $comments = $td.find('.inline-comments');
788 var $comments = $td.find('.inline-comments');
789 if (!$comments.length) {
789 if (!$comments.length) {
790 $comments = $(
790 $comments = $(
791 $('#cb-comments-inline-container-template').html());
791 $('#cb-comments-inline-container-template').html());
792 $td.append($comments);
792 $td.append($comments);
793 }
793 }
794
794
795 $td.find('.cb-comment-add-button').before($form);
795 $td.find('.cb-comment-add-button').before($form);
796
796
797 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
797 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
798 var _form = $($form[0]).find('form');
798 var _form = $($form[0]).find('form');
799 var autocompleteActions = ['as_note', 'as_todo'];
799 var autocompleteActions = ['as_note', 'as_todo'];
800 var commentForm = this.createCommentForm(
800 var commentForm = this.createCommentForm(
801 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
801 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
802
802
803 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
803 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
804 form: _form,
804 form: _form,
805 parent: $td[0],
805 parent: $td[0],
806 lineno: lineno,
806 lineno: lineno,
807 f_path: f_path}
807 f_path: f_path}
808 );
808 );
809
809
810 // set a CUSTOM submit handler for inline comments.
810 // set a CUSTOM submit handler for inline comments.
811 commentForm.setHandleFormSubmit(function(o) {
811 commentForm.setHandleFormSubmit(function(o) {
812 var text = commentForm.cm.getValue();
812 var text = commentForm.cm.getValue();
813 var commentType = commentForm.getCommentType();
813 var commentType = commentForm.getCommentType();
814 var resolvesCommentId = commentForm.getResolvesId();
814 var resolvesCommentId = commentForm.getResolvesId();
815
815
816 if (text === "") {
816 if (text === "") {
817 return;
817 return;
818 }
818 }
819
819
820 if (lineno === undefined) {
820 if (lineno === undefined) {
821 alert('missing line !');
821 alert('missing line !');
822 return;
822 return;
823 }
823 }
824 if (f_path === undefined) {
824 if (f_path === undefined) {
825 alert('missing file path !');
825 alert('missing file path !');
826 return;
826 return;
827 }
827 }
828
828
829 var excludeCancelBtn = false;
829 var excludeCancelBtn = false;
830 var submitEvent = true;
830 var submitEvent = true;
831 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
831 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
832 commentForm.cm.setOption("readOnly", true);
832 commentForm.cm.setOption("readOnly", true);
833 var postData = {
833 var postData = {
834 'text': text,
834 'text': text,
835 'f_path': f_path,
835 'f_path': f_path,
836 'line': lineno,
836 'line': lineno,
837 'comment_type': commentType,
837 'comment_type': commentType,
838 'csrf_token': CSRF_TOKEN
838 'csrf_token': CSRF_TOKEN
839 };
839 };
840 if (resolvesCommentId){
840 if (resolvesCommentId){
841 postData['resolves_comment_id'] = resolvesCommentId;
841 postData['resolves_comment_id'] = resolvesCommentId;
842 }
842 }
843
843
844 var submitSuccessCallback = function(json_data) {
844 var submitSuccessCallback = function(json_data) {
845 $form.remove();
845 $form.remove();
846 try {
846 try {
847 var html = json_data.rendered_text;
847 var html = json_data.rendered_text;
848 var lineno = json_data.line_no;
848 var lineno = json_data.line_no;
849 var target_id = json_data.target_id;
849 var target_id = json_data.target_id;
850
850
851 $comments.find('.cb-comment-add-button').before(html);
851 $comments.find('.cb-comment-add-button').before(html);
852
852
853 //mark visually which comment was resolved
853 //mark visually which comment was resolved
854 if (resolvesCommentId) {
854 if (resolvesCommentId) {
855 commentForm.markCommentResolved(resolvesCommentId);
855 commentForm.markCommentResolved(resolvesCommentId);
856 }
856 }
857
857
858 // run global callback on submit
858 // run global callback on submit
859 commentForm.globalSubmitSuccessCallback();
859 commentForm.globalSubmitSuccessCallback();
860
860
861 } catch (e) {
861 } catch (e) {
862 console.error(e);
862 console.error(e);
863 }
863 }
864
864
865 // re trigger the linkification of next/prev navigation
865 // re trigger the linkification of next/prev navigation
866 linkifyComments($('.inline-comment-injected'));
866 linkifyComments($('.inline-comment-injected'));
867 timeagoActivate();
867 timeagoActivate();
868
868
869 if (window.updateSticky !== undefined) {
869 if (window.updateSticky !== undefined) {
870 // potentially our comments change the active window size, so we
870 // potentially our comments change the active window size, so we
871 // notify sticky elements
871 // notify sticky elements
872 updateSticky()
872 updateSticky()
873 }
873 }
874
874
875 commentForm.setActionButtonsDisabled(false);
875 commentForm.setActionButtonsDisabled(false);
876
876
877 };
877 };
878 var submitFailCallback = function(data){
878 var submitFailCallback = function(data){
879 alert(
879 alert(
880 "Error while submitting comment.\n" +
880 "Error while submitting comment.\n" +
881 "Error code {0} ({1}).".format(data.status, data.statusText)
881 "Error code {0} ({1}).".format(data.status, data.statusText)
882 );
882 );
883 commentForm.resetCommentFormState(text)
883 commentForm.resetCommentFormState(text)
884 };
884 };
885 commentForm.submitAjaxPOST(
885 commentForm.submitAjaxPOST(
886 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
886 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
887 });
887 });
888 }
888 }
889
889
890 $form.addClass('comment-inline-form-open');
890 $form.addClass('comment-inline-form-open');
891 };
891 };
892
892
893 this.createResolutionComment = function(commentId){
893 this.createResolutionComment = function(commentId){
894 // hide the trigger text
894 // hide the trigger text
895 $('#resolve-comment-{0}'.format(commentId)).hide();
895 $('#resolve-comment-{0}'.format(commentId)).hide();
896
896
897 var comment = $('#comment-'+commentId);
897 var comment = $('#comment-'+commentId);
898 var commentData = comment.data();
898 var commentData = comment.data();
899 if (commentData.commentInline) {
899 if (commentData.commentInline) {
900 this.createComment(comment, commentId)
900 this.createComment(comment, commentId)
901 } else {
901 } else {
902 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
902 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
903 }
903 }
904
904
905 return false;
905 return false;
906 };
906 };
907
907
908 this.submitResolution = function(commentId){
908 this.submitResolution = function(commentId){
909 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
909 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
910 var commentForm = form.get(0).CommentForm;
910 var commentForm = form.get(0).CommentForm;
911
911
912 var cm = commentForm.getCmInstance();
912 var cm = commentForm.getCmInstance();
913 var renderer = templateContext.visual.default_renderer;
913 var renderer = templateContext.visual.default_renderer;
914 if (renderer == 'rst'){
914 if (renderer == 'rst'){
915 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
915 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
916 } else if (renderer == 'markdown') {
916 } else if (renderer == 'markdown') {
917 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
917 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
918 } else {
918 } else {
919 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
919 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
920 }
920 }
921
921
922 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
922 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
923 form.submit();
923 form.submit();
924 return false;
924 return false;
925 };
925 };
926
926
927 };
927 };
@@ -1,211 +1,211 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2
2
3 <%def name="title()">
3 <%def name="title()">
4 ${_('{} Files Upload').format(c.repo_name)}
4 ${_('{} Files Upload').format(c.repo_name)}
5 %if c.rhodecode_name:
5 %if c.rhodecode_name:
6 &middot; ${h.branding(c.rhodecode_name)}
6 &middot; ${h.branding(c.rhodecode_name)}
7 %endif
7 %endif
8 </%def>
8 </%def>
9
9
10 <%def name="menu_bar_nav()">
10 <%def name="menu_bar_nav()">
11 ${self.menu_items(active='repositories')}
11 ${self.menu_items(active='repositories')}
12 </%def>
12 </%def>
13
13
14 <%def name="breadcrumbs_links()"></%def>
14 <%def name="breadcrumbs_links()"></%def>
15
15
16 <%def name="menu_bar_subnav()">
16 <%def name="menu_bar_subnav()">
17 ${self.repo_menu(active='files')}
17 ${self.repo_menu(active='files')}
18 </%def>
18 </%def>
19
19
20 <%def name="main()">
20 <%def name="main()">
21
21
22 <div class="box">
22 <div class="box">
23 ## Template for uploads
23 ## Template for uploads
24 <div style="display: none" id="tpl-dropzone">
24 <div style="display: none" id="tpl-dropzone">
25 <div class="dz-preview dz-file-preview">
25 <div class="dz-preview dz-file-preview">
26 <div class="dz-details">
26 <div class="dz-details">
27
27
28 <div class="dz-filename">
28 <div class="dz-filename">
29 <span data-dz-name></span>
29 <span data-dz-name></span>
30 </div>
30 </div>
31 <div class="dz-filename-size">
31 <div class="dz-filename-size">
32 <span class="dz-size" data-dz-size></span>
32 <span class="dz-size" data-dz-size></span>
33
33
34 </div>
34 </div>
35
35
36 <div class="dz-sending" style="display: none">${_('Uploading...')}</div>
36 <div class="dz-sending" style="display: none">${_('Uploading...')}</div>
37 <div class="dz-response" style="display: none">
37 <div class="dz-response" style="display: none">
38 ${_('Uploaded')} 100%
38 ${_('Uploaded')} 100%
39 </div>
39 </div>
40
40
41 </div>
41 </div>
42 <div class="dz-progress">
42 <div class="dz-progress">
43 <span class="dz-upload" data-dz-uploadprogress></span>
43 <span class="dz-upload" data-dz-uploadprogress></span>
44 </div>
44 </div>
45
45
46 <div class="dz-error-message">
46 <div class="dz-error-message">
47 </div>
47 </div>
48 </div>
48 </div>
49 </div>
49 </div>
50
50
51 <div class="edit-file-title">
51 <div class="edit-file-title">
52 <span class="title-heading">${_('Upload new file')} @ <code>${h.show_id(c.commit)}</code></span>
52 <span class="title-heading">${_('Upload new file')} @ <code>${h.show_id(c.commit)}</code></span>
53 % if c.commit.branch:
53 % if c.commit.branch:
54 <span class="tag branchtag">
54 <span class="tag branchtag">
55 <i class="icon-branch"></i> ${c.commit.branch}
55 <i class="icon-branch"></i> ${c.commit.branch}
56 </span>
56 </span>
57 % endif
57 % endif
58 </div>
58 </div>
59
59
60 <% form_url = h.route_path('repo_files_upload_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path) %>
60 <% form_url = h.route_path('repo_files_upload_file', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path) %>
61 ##${h.secure_form(form_url, id='eform', enctype="multipart/form-data", request=request)}
61 ##${h.secure_form(form_url, id='eform', enctype="multipart/form-data", request=request)}
62 <div class="edit-file-fieldset">
62 <div class="edit-file-fieldset">
63 <div class="path-items">
63 <div class="path-items">
64 <ul>
64 <ul>
65 <li class="breadcrumb-path">
65 <li class="breadcrumb-path">
66 <div>
66 <div>
67 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path='')}"><i class="icon-home"></i></a> /
67 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path='')}"><i class="icon-home"></i></a> /
68 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path)}">${c.f_path}</a> ${('/' if c.f_path else '')}
68 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path)}">${c.f_path}</a> ${('/' if c.f_path else '')}
69 </div>
69 </div>
70 </li>
70 </li>
71 <li class="location-path">
71 <li class="location-path">
72
72
73 </li>
73 </li>
74 </ul>
74 </ul>
75 </div>
75 </div>
76
76
77 </div>
77 </div>
78
78
79 <div class="upload-form table">
79 <div class="upload-form table">
80 <div>
80 <div>
81
81
82 <div class="dropzone-wrapper" id="file-uploader">
82 <div class="dropzone-wrapper" id="file-uploader">
83 <div class="dropzone-pure">
83 <div class="dropzone-pure">
84 <div class="dz-message">
84 <div class="dz-message">
85 <i class="icon-upload" style="font-size:36px"></i></br>
85 <i class="icon-upload" style="font-size:36px"></i></br>
86 ${_("Drag'n Drop files here or")} <span class="link">${_('Choose your files')}</span>.<br>
86 ${_("Drag'n Drop files here or")} <span class="link">${_('Choose your files')}</span>.<br>
87 </div>
87 </div>
88 </div>
88 </div>
89
89
90 </div>
90 </div>
91 </div>
91 </div>
92
92
93 </div>
93 </div>
94
94
95 <div class="upload-form edit-file-fieldset">
95 <div class="upload-form edit-file-fieldset">
96 <div class="fieldset">
96 <div class="fieldset">
97 <div class="message">
97 <div class="message">
98 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
98 <textarea id="commit" name="message" placeholder="${c.default_message}"></textarea>
99 </div>
99 </div>
100 </div>
100 </div>
101 <div class="pull-left">
101 <div class="pull-left">
102 ${h.submit('commit_btn',_('Commit changes'), class_="btn btn-small btn-success")}
102 ${h.submit('commit_btn',_('Commit changes'), class_="btn btn-small btn-success")}
103 </div>
103 </div>
104 </div>
104 </div>
105 ##${h.end_form()}
105 ##${h.end_form()}
106
106
107 <div class="file-upload-transaction-wrapper" style="display: none">
107 <div class="file-upload-transaction-wrapper" style="display: none">
108 <div class="file-upload-transaction">
108 <div class="file-upload-transaction">
109 <h3>${_('Commiting...')}</h3>
109 <h3>${_('Commiting...')}</h3>
110 <p>${_('Please wait while the files are being uploaded')}</p>
110 <p>${_('Please wait while the files are being uploaded')}</p>
111 <p class="error" style="display: none">
111 <p class="error" style="display: none">
112
112
113 </p>
113 </p>
114 <i class="icon-spin animate-spin"></i>
114 <i class="icon-spin animate-spin"></i>
115 <p></p>
115 <p></p>
116 </div>
116 </div>
117 </div>
117 </div>
118
118
119 </div>
119 </div>
120
120
121 <script type="text/javascript">
121 <script type="text/javascript">
122
122
123 $(document).ready(function () {
123 $(document).ready(function () {
124
124
125 //see: https://www.dropzonejs.com/#configuration
125 //see: https://www.dropzonejs.com/#configuration
126 myDropzone = new Dropzone("div#file-uploader", {
126 myDropzone = new Dropzone("div#file-uploader", {
127 url: "${form_url}",
127 url: "${form_url}",
128 headers: {"X-CSRF-Token": CSRF_TOKEN},
128 headers: {"X-CSRF-Token": CSRF_TOKEN},
129 paramName: function () {
129 paramName: function () {
130 return "files_upload"
130 return "files_upload"
131 }, // The name that will be used to transfer the file
131 }, // The name that will be used to transfer the file
132 parallelUploads: 20,
132 parallelUploads: 20,
133 maxFiles: 20,
133 maxFiles: 20,
134 uploadMultiple: true,
134 uploadMultiple: true,
135 //chunking: true, // use chunking transfer, not supported at the moment
135 //chunking: true, // use chunking transfer, not supported at the moment
136 //maxFilesize: 2, // in MBs
136 //maxFilesize: 2, // in MBs
137 autoProcessQueue: false, // if false queue will not be processed automatically.
137 autoProcessQueue: false, // if false queue will not be processed automatically.
138 createImageThumbnails: false,
138 createImageThumbnails: false,
139 previewTemplate: document.querySelector('#tpl-dropzone').innerHTML,
139 previewTemplate: document.querySelector('#tpl-dropzone').innerHTML,
140 accept: function (file, done) {
140 accept: function (file, done) {
141 done();
141 done();
142 },
142 },
143 init: function () {
143 init: function () {
144 this.on("addedfile", function (file) {
144 this.on("addedfile", function (file) {
145
145
146 });
146 });
147
147
148 this.on("sending", function (file, xhr, formData) {
148 this.on("sending", function (file, xhr, formData) {
149 formData.append("message", $('#commit').val());
149 formData.append("message", $('#commit').val());
150 $(file.previewElement).find('.dz-sending').show();
150 $(file.previewElement).find('.dz-sending').show();
151 });
151 });
152
152
153 this.on("success", function (file, response) {
153 this.on("success", function (file, response) {
154 $(file.previewElement).find('.dz-sending').hide();
154 $(file.previewElement).find('.dz-sending').hide();
155 $(file.previewElement).find('.dz-response').show();
155 $(file.previewElement).find('.dz-response').show();
156
156
157 if (response.error !== null) {
157 if (response.error !== null) {
158 $('.file-upload-transaction-wrapper .error').html('ERROR: {0}'.format(response.error));
158 $('.file-upload-transaction-wrapper .error').html('ERROR: {0}'.format(response.error));
159 $('.file-upload-transaction-wrapper .error').show();
159 $('.file-upload-transaction-wrapper .error').show();
160 $('.file-upload-transaction-wrapper i').hide()
160 $('.file-upload-transaction-wrapper i').hide()
161 }
161 }
162
162
163 var redirect_url = response.redirect_url || '/';
163 var redirect_url = response.redirect_url || '/';
164 window.location = redirect_url
164 window.location = redirect_url
165
165
166 });
166 });
167
167
168 this.on("error", function (file, errorMessage, xhr) {
168 this.on("error", function (file, errorMessage, xhr) {
169 var error = null;
169 var error = null;
170
170
171 if (xhr !== undefined){
171 if (xhr !== undefined){
172 var httpStatus = xhr.status + " " + xhr.statusText;
172 var httpStatus = xhr.status + " " + xhr.statusText;
173 if (xhr.status >= 500) {
173 if (xhr !== undefined && xhr.status >= 500) {
174 error = httpStatus;
174 error = httpStatus;
175 }
175 }
176 }
176 }
177
177
178 if (error === null) {
178 if (error === null) {
179 error = errorMessage.error || errorMessage || httpStatus;
179 error = errorMessage.error || errorMessage || httpStatus;
180 }
180 }
181
181
182 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
182 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
183 });
183 });
184 }
184 }
185 });
185 });
186
186
187 $('#commit_btn').on('click', function(e) {
187 $('#commit_btn').on('click', function(e) {
188 e.preventDefault();
188 e.preventDefault();
189 var button = $(this);
189 var button = $(this);
190 if (button.hasClass('clicked')) {
190 if (button.hasClass('clicked')) {
191 button.attr('disabled', true);
191 button.attr('disabled', true);
192 } else {
192 } else {
193 button.addClass('clicked');
193 button.addClass('clicked');
194 }
194 }
195
195
196 var files = myDropzone.getQueuedFiles();
196 var files = myDropzone.getQueuedFiles();
197 if (files.length === 0) {
197 if (files.length === 0) {
198 alert("Missing files");
198 alert("Missing files");
199 e.preventDefault();
199 e.preventDefault();
200 }
200 }
201
201
202 $('.upload-form').hide();
202 $('.upload-form').hide();
203 $('.file-upload-transaction-wrapper').show();
203 $('.file-upload-transaction-wrapper').show();
204 myDropzone.processQueue();
204 myDropzone.processQueue();
205
205
206 });
206 });
207
207
208 });
208 });
209
209
210 </script>
210 </script>
211 </%def>
211 </%def>
General Comments 0
You need to be logged in to leave comments. Login now