mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-16 07:08:55 +00:00
Review comment (#2459)
This commit is contained in:
95
frontend/package-lock.json
generated
95
frontend/package-lock.json
generated
@@ -98,6 +98,30 @@
|
||||
"react-popper": "^0.8.3",
|
||||
"react-transition-group": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"unified": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/unified/-/unified-6.2.0.tgz",
|
||||
"integrity": "sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA==",
|
||||
"requires": {
|
||||
"bail": "^1.0.0",
|
||||
"extend": "^3.0.0",
|
||||
"is-plain-obj": "^1.1.0",
|
||||
"trough": "^1.0.0",
|
||||
"vfile": "^2.0.0",
|
||||
"x-is-string": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"vfile": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz",
|
||||
"integrity": "sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w==",
|
||||
"requires": {
|
||||
"is-buffer": "^1.1.4",
|
||||
"replace-ext": "1.0.0",
|
||||
"unist-util-stringify-position": "^1.0.0",
|
||||
"vfile-message": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -1896,7 +1920,7 @@
|
||||
},
|
||||
"browserify-rsa": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
|
||||
"resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
|
||||
"integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@@ -1947,7 +1971,7 @@
|
||||
},
|
||||
"buffer": {
|
||||
"version": "4.9.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
|
||||
"resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
|
||||
"integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@@ -3838,7 +3862,7 @@
|
||||
},
|
||||
"events": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz",
|
||||
"resolved": "http://registry.npmjs.org/events/-/events-1.1.1.tgz",
|
||||
"integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=",
|
||||
"dev": true
|
||||
},
|
||||
@@ -9775,6 +9799,32 @@
|
||||
"remark-parse": "^5.0.0",
|
||||
"remark-stringify": "^5.0.0",
|
||||
"unified": "^6.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"unified": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/unified/-/unified-6.2.0.tgz",
|
||||
"integrity": "sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA==",
|
||||
"requires": {
|
||||
"bail": "^1.0.0",
|
||||
"extend": "^3.0.0",
|
||||
"is-plain-obj": "^1.1.0",
|
||||
"trough": "^1.0.0",
|
||||
"vfile": "^2.0.0",
|
||||
"x-is-string": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"vfile": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz",
|
||||
"integrity": "sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w==",
|
||||
"requires": {
|
||||
"is-buffer": "^1.1.4",
|
||||
"replace-ext": "1.0.0",
|
||||
"unist-util-stringify-position": "^1.0.0",
|
||||
"vfile-message": "^1.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"remark-breaks": {
|
||||
@@ -10175,9 +10225,9 @@
|
||||
}
|
||||
},
|
||||
"seafile-js": {
|
||||
"version": "0.2.26",
|
||||
"resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.26.tgz",
|
||||
"integrity": "sha512-iKP2nLBCDE2G4MoiVdtQ4JlKY3vUByb7dwbwOV/pC8OdSxYcpu1zmCFl45TLnhz8B6wOlc7EC00ByZAvyTaZOQ==",
|
||||
"version": "0.2.28",
|
||||
"resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.28.tgz",
|
||||
"integrity": "sha512-+QA16BLpNVrvYIHfvrU/0D0x90bb1gCzW31U2BeFdL1CT3vgdLXaVc+j0XAtDahBYTyKtu7YKGPR6UO0ZdlXdw==",
|
||||
"requires": {
|
||||
"axios": "^0.18.0",
|
||||
"form-data": "^2.3.2"
|
||||
@@ -11183,7 +11233,7 @@
|
||||
},
|
||||
"yargs": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz",
|
||||
"integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@@ -11210,15 +11260,15 @@
|
||||
}
|
||||
},
|
||||
"unified": {
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/unified/-/unified-6.2.0.tgz",
|
||||
"integrity": "sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA==",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unified/-/unified-7.0.0.tgz",
|
||||
"integrity": "sha512-j+Sm7upmmt3RXPBeA+KFGYBlHBxClnby2DtxezFKwMfhWTAklY4WbEdhwRo6c6GpuHdi04YDsyPKY/kh5a/xnQ==",
|
||||
"requires": {
|
||||
"bail": "^1.0.0",
|
||||
"extend": "^3.0.0",
|
||||
"is-plain-obj": "^1.1.0",
|
||||
"trough": "^1.0.0",
|
||||
"vfile": "^2.0.0",
|
||||
"vfile": "^3.0.0",
|
||||
"x-is-string": "^0.1.0"
|
||||
}
|
||||
},
|
||||
@@ -11499,14 +11549,21 @@
|
||||
}
|
||||
},
|
||||
"vfile": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz",
|
||||
"integrity": "sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w==",
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/vfile/-/vfile-3.0.0.tgz",
|
||||
"integrity": "sha512-X2DiPHL9Nxgfyu5DNVgtTkZtD4d4Zzf7rVBVI+uXP2pWWIQG8Ri+xAP9KdH/sB6SS0a1niWp5bRF88n4ciwhoA==",
|
||||
"requires": {
|
||||
"is-buffer": "^1.1.4",
|
||||
"is-buffer": "^2.0.0",
|
||||
"replace-ext": "1.0.0",
|
||||
"unist-util-stringify-position": "^1.0.0",
|
||||
"vfile-message": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"is-buffer": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.3.tgz",
|
||||
"integrity": "sha512-U15Q7MXTuZlrbymiz95PJpZxu8IlipAp4dtS3wOdgPXx3mqBnslrWU14kxfHB+Py/+2PVKSr37dMAgM2A4uArw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"vfile-location": {
|
||||
@@ -11671,7 +11728,7 @@
|
||||
},
|
||||
"load-json-file": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz",
|
||||
"integrity": "sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@@ -11794,13 +11851,13 @@
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-1.1.1.tgz",
|
||||
"resolved": "http://registry.npmjs.org/ansi-regex/-/ansi-regex-1.1.1.tgz",
|
||||
"integrity": "sha1-QchHGUZGN15qGl0Qw8oFTvn8mA0=",
|
||||
"dev": true
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-2.0.1.tgz",
|
||||
"resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-2.0.1.tgz",
|
||||
"integrity": "sha1-32LBqpTtLxFOHQ8h/R1QSCt5pg4=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@@ -11961,7 +12018,7 @@
|
||||
},
|
||||
"yargs": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz",
|
||||
"integrity": "sha1-eC7CHvQDNF+DCoCMo9UTr1YGUgg=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
|
@@ -25,11 +25,13 @@
|
||||
"react-dom": "^16.5.2",
|
||||
"react-moment": "^0.7.9",
|
||||
"reactstrap": "^6.4.0",
|
||||
"seafile-js": "^0.2.26",
|
||||
"seafile-js": "^0.2.28",
|
||||
"seafile-ui": "^0.1.10",
|
||||
"sw-precache-webpack-plugin": "0.11.4",
|
||||
"unified": "^7.0.0",
|
||||
"url-loader": "0.6.2",
|
||||
"url-parse": "^1.4.3",
|
||||
"vfile": "^3.0.0",
|
||||
"whatwg-fetch": "2.0.3"
|
||||
},
|
||||
"scripts": {
|
||||
|
198
frontend/src/components/review-list-view/review-comments.js
Normal file
198
frontend/src/components/review-list-view/review-comments.js
Normal file
@@ -0,0 +1,198 @@
|
||||
import React from 'react';
|
||||
import { processor } from "../../utils/seafile-markdown2html";
|
||||
import { Button, Input, Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
|
||||
import { seafileAPI } from '../../utils/seafile-api';
|
||||
import { draftID, reviewID, gettext } from '../../utils/constants';
|
||||
import moment from 'moment';
|
||||
import Loading from '../../components/loading.js';
|
||||
|
||||
import '../../css/review-comments.css';
|
||||
|
||||
class ReviewComments extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
commentsList: [],
|
||||
userAvatar: `${window.location.host}media/avatars/default.png`,
|
||||
}
|
||||
this.accountInfo = {};
|
||||
}
|
||||
|
||||
listComments = () => {
|
||||
seafileAPI.listReviewComments(reviewID).then((response) => {
|
||||
this.setState({
|
||||
commentsList: response.data.comments
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getUserAvatar = () => {
|
||||
seafileAPI.getAccountInfo().then((res) => {
|
||||
this.accountInfo = res.data;
|
||||
this.setState({
|
||||
userAvatar: res.data.avatar_url,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
handleCommentChange = (event) => {
|
||||
this.setState({
|
||||
comment: event.target.value,
|
||||
});
|
||||
}
|
||||
|
||||
submitComment = () => {
|
||||
let comment = this.refs.commentTextarea.value;
|
||||
if (comment.trim().length > 0) {
|
||||
seafileAPI.addReviewComment(reviewID, comment.trim()).then((res) => {
|
||||
this.listComments();
|
||||
this.props.getCommentsNumber();
|
||||
});
|
||||
this.refs.commentTextarea.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
resolveComment = (event) => {
|
||||
seafileAPI.updateReviewComment(reviewID, event.target.id, 'true').then((res) => {
|
||||
this.listComments();
|
||||
});
|
||||
}
|
||||
|
||||
deleteComment = (event) => {
|
||||
seafileAPI.deleteReviewComment(reviewID, event.target.id).then((res) => {
|
||||
this.props.getCommentsNumber();
|
||||
this.listComments();
|
||||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.getUserAvatar();
|
||||
this.listComments();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="seafile-comment">
|
||||
<div className="seafile-comment-title">
|
||||
<div onClick={this.props.toggleCommentList} className={'seafile-comment-title-close'}>
|
||||
<i className={'fa fa-times-circle'}/>
|
||||
</div>
|
||||
<div className={'seafile-comment-title-text'}>{gettext('Comments')}</div>
|
||||
</div>
|
||||
{ this.props.commentsNumber == 0 &&
|
||||
<div className={"seafile-comment-list"}>
|
||||
<div className="comment-vacant">{gettext('No comment yet.')}</div>
|
||||
</div>
|
||||
}
|
||||
{ (this.state.commentsList.length == 0 && this.props.commentsNumber > 0) &&
|
||||
<div><Loading/></div>
|
||||
}
|
||||
<ul className={"seafile-comment-list"}>
|
||||
{ (this.state.commentsList.length > 0 && this.props.commentsNumber > 0) &&
|
||||
this.state.commentsList.map((item, index = 0, arr) => {
|
||||
if (item.resolved) {
|
||||
return
|
||||
} else {
|
||||
let oldTime = (new Date(item.created_at)).getTime();
|
||||
let time = moment(oldTime).format("YYYY-MM-DD HH:mm");
|
||||
return (
|
||||
<CommentItem id={item.id} time={time} headUrl={item.avatar_url}
|
||||
editorUtilities={this.props.editorUtilities}
|
||||
comment={item.comment} name={item.user_name}
|
||||
user_email={item.user_email} key={index}
|
||||
deleteComment={this.deleteComment}
|
||||
resolveComment={this.resolveComment}
|
||||
commentsList={this.state.commentsList}
|
||||
accountInfo={this.accountInfo}
|
||||
/>
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
</ul>
|
||||
<div className="seafile-comment-footer">
|
||||
<div className="user-header">
|
||||
<img className="avatar" src={this.state.userAvatar}/>
|
||||
</div>
|
||||
<div className="seafile-add-comment">
|
||||
<textarea className="add-comment-input" ref="commentTextarea"
|
||||
placeholder={gettext('Add a comment.')}
|
||||
clos="100" rows="3" warp="virtual"></textarea>
|
||||
<Button className="submit-comment" color="success"
|
||||
size="sm" onClick={this.submitComment}>
|
||||
{gettext('Submit')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class CommentItem extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
dropdownOpen: false,
|
||||
html: '',
|
||||
}
|
||||
}
|
||||
|
||||
toggleDropDownMenu = () => {
|
||||
this.setState({
|
||||
dropdownOpen: !this.state.dropdownOpen,
|
||||
})
|
||||
}
|
||||
|
||||
convertComment = (mdFile) => {
|
||||
processor.process(mdFile).then(
|
||||
(result) => {
|
||||
let html = String(result);
|
||||
this.setState({
|
||||
html: html
|
||||
})
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.convertComment(this.props.comment);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
this.convertComment(nextProps.comment);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<li className="seafile-comment-item" id={this.props.id}>
|
||||
<div className="seafile-comment-info">
|
||||
<img className="avatar reviewer-head" src={this.props.headUrl} />
|
||||
<div className="reviewer-info">
|
||||
<div className="reviewer-name">{this.props.name}</div>
|
||||
<div className="review-time">{this.props.time}</div>
|
||||
</div>
|
||||
<Dropdown isOpen={this.state.dropdownOpen} size="sm"
|
||||
className="seafile-comment-dropdown" toggle={this.toggleDropDownMenu}>
|
||||
<DropdownToggle className="seafile-comment-dropdown-btn">
|
||||
<i className="fas fa-ellipsis-v"></i>
|
||||
</DropdownToggle>
|
||||
<DropdownMenu>
|
||||
{
|
||||
(this.props.user_email === this.props.accountInfo.email) &&
|
||||
<DropdownItem onClick={this.props.deleteComment}
|
||||
className="delete-comment" id={this.props.id}>{gettext('Delete')}</DropdownItem>
|
||||
}
|
||||
<DropdownItem onClick={this.props.resolveComment}
|
||||
className="seafile-comment-resolved" id={this.props.id}>{gettext('Mark as resolved')}</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div className="seafile-comment-content" dangerouslySetInnerHTML={{ __html: this.state.html }}></div>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default ReviewComments;
|
35
frontend/src/css/draft-review.css
Normal file
35
frontend/src/css/draft-review.css
Normal file
@@ -0,0 +1,35 @@
|
||||
.header .button-group {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.header .common-list-btn {
|
||||
margin-right: .25em;
|
||||
}
|
||||
|
||||
.header .common-list-btn .common-list-btn-number {
|
||||
margin-left: .5em;
|
||||
}
|
||||
|
||||
.main .cur-view-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
background-color: #fafaf9;
|
||||
}
|
||||
|
||||
.main .cur-view-container .cur-view-content {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.main .cur-view-container .cur-view-content-commenton {
|
||||
overflow: auto;
|
||||
margin-right: 30em;
|
||||
}
|
||||
|
||||
.main .cur-view-container .seafile-comment {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
height: calc(100% - 4.5em);
|
||||
}
|
123
frontend/src/css/review-comments.css
Normal file
123
frontend/src/css/review-comments.css
Normal file
@@ -0,0 +1,123 @@
|
||||
.seafile-comment {
|
||||
border-left: 1px solid #e6e6dd;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 0 0 auto;
|
||||
min-height: 18.5em;
|
||||
width: 30em;
|
||||
}
|
||||
.seafile-comment-title {
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
min-height: 3em;
|
||||
line-height: 3em;
|
||||
padding: 0 1em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
background-color: #fafaf9;
|
||||
}
|
||||
.seafile-comment-title .seafile-comment-title-text {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
.seafile-comment-title .seafile-comment-title-close {
|
||||
color: #b9b9b9;
|
||||
}
|
||||
.seafile-comment-title .seafile-comment-title-close:hover {
|
||||
color: #888;
|
||||
}
|
||||
.seafile-comment-list {
|
||||
height: calc(100% - 40px);
|
||||
overflow-y: auto;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.seafile-comment .loading-icon {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.seafile-comment-list .comment-vacant {
|
||||
padding: 1em;
|
||||
text-align: center;
|
||||
}
|
||||
.seafile-comment-item {
|
||||
overflow-y: auto;
|
||||
padding: 15px 10px;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.seafile-comment-item .seafile-comment-info {
|
||||
padding-bottom: 0.5em;
|
||||
height: 3em;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.seafile-comment-item .seafile-comment-info .reviewer-head {
|
||||
margin-top: .3em;
|
||||
}
|
||||
.seafile-comment-item .seafile-comment-info .reviewer-info {
|
||||
padding-left: 10px;
|
||||
}
|
||||
.seafile-comment-item .seafile-comment-info .review-time {
|
||||
font-size: .6em;
|
||||
color: #777;
|
||||
}
|
||||
.seafile-comment-item .seafile-comment-info .seafile-comment-dropdown {
|
||||
margin-left: auto;
|
||||
}
|
||||
.seafile-comment-item .seafile-comment-info .seafile-comment-dropdown button,
|
||||
.seafile-comment-item .seafile-comment-info .seafile-comment-dropdown button:hover,
|
||||
.seafile-comment-item .seafile-comment-info .seafile-comment-dropdown button:focus
|
||||
{
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
background-color: #fff;
|
||||
height: 100%;
|
||||
}
|
||||
.seafile-comment-item .seafile-comment-info.seafile-comment-dropdown .seafile-comment-dropdown-btn {
|
||||
color: #999;
|
||||
}
|
||||
.seafile-comment-item .seafile-comment-info .seafile-comment-dropdown:hover .seafile-comment-dropdown-btn {
|
||||
color: #555;
|
||||
}
|
||||
.seafile-comment-item .seafile-comment-content {
|
||||
margin-left: 42px;
|
||||
}
|
||||
.seafile-comment-item .seafile-comment-content ol,
|
||||
.seafile-comment-item .seafile-comment-content ul,
|
||||
.seafile-comment-item .seafile-comment-content li {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.seafile-comment-item .seafile-comment-content table,
|
||||
.seafile-comment-item .seafile-comment-content th,
|
||||
.seafile-comment-item .seafile-comment-content td {
|
||||
border: 1px solid #333;
|
||||
}
|
||||
.seafile-comment-footer {
|
||||
background-color: #fafaf9;
|
||||
padding: 10px;
|
||||
border-top: 1px solid #e5e5e5;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
min-height: 150px;
|
||||
}
|
||||
.seafile-comment-footer .seafile-add-comment {
|
||||
margin-left: 10px;
|
||||
overflow: hidden;
|
||||
width: 25em;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.seafile-comment-footer .seafile-add-comment .add-comment-input {
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
border: 1px solid #e6e6dd;
|
||||
padding: 5px;
|
||||
width: 25em;
|
||||
min-height: 90px;
|
||||
}
|
||||
.seafile-comment-footer .seafile-add-comment .submit-comment {
|
||||
margin-top: 5px;
|
||||
width: 60px;
|
||||
height: 28px;
|
||||
min-height: 28px;
|
||||
}
|
@@ -3,12 +3,13 @@ import ReactDOM from 'react-dom';
|
||||
/* eslint-disable */
|
||||
import Prism from 'prismjs';
|
||||
/* eslint-enable */
|
||||
import { siteRoot, gettext, reviewID, draftOriginFilePath, draftFilePath, draftOriginRepoID, draftFileName, opStatus, publishFileVersion, originFileVersion } from './utils/constants';
|
||||
import { siteRoot, gettext, reviewID, draftOriginFilePath, draftFilePath, draftOriginRepoID, draftFileName, opStatus, publishFileVersion, originFileVersion } from './utils/constants';
|
||||
import { seafileAPI } from './utils/seafile-api';
|
||||
import axios from 'axios';
|
||||
import DiffViewer from '@seafile/seafile-editor/dist/viewer/diff-viewer';
|
||||
import Loading from './components/loading';
|
||||
import Toast from './components/toast';
|
||||
import ReviewComments from './components/review-list-view/review-comments';
|
||||
|
||||
import 'seafile-ui';
|
||||
import './assets/css/fa-solid.css';
|
||||
@@ -17,6 +18,7 @@ import './assets/css/fontawesome.css';
|
||||
import './css/layout.css';
|
||||
import './css/initial-style.css';
|
||||
import './css/toolbar.css';
|
||||
import './css/draft-review.css';
|
||||
|
||||
require('@seafile/seafile-editor/dist/editor/code-hight-package');
|
||||
|
||||
@@ -28,6 +30,8 @@ class DraftReview extends React.Component {
|
||||
draftOriginContent: '',
|
||||
reviewStatus: opStatus,
|
||||
isLoading: true,
|
||||
commentsNumber: null,
|
||||
isShowComments: false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -82,6 +86,25 @@ class DraftReview extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
toggleCommentList = () => {
|
||||
this.setState({
|
||||
isShowComments: !this.state.isShowComments
|
||||
});
|
||||
}
|
||||
|
||||
getCommentsNumber = () => {
|
||||
seafileAPI.listReviewComments(reviewID).then((res) => {
|
||||
let number = res.data.total_count;
|
||||
this.setState({
|
||||
commentsNumber: number,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.getCommentsNumber();
|
||||
}
|
||||
|
||||
render() {
|
||||
return(
|
||||
<div className="wrapper">
|
||||
@@ -95,25 +118,35 @@ class DraftReview extends React.Component {
|
||||
<span className="file-copywriting">{gettext('review')}</span>
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
this.state.reviewStatus === 'open' &&
|
||||
<div className="cur-file-operation">
|
||||
<button className="btn btn-secondary file-operation-btn" title={gettext('Close Review')} onClick={this.onCloseReview}>{gettext('Close')}</button>
|
||||
<button className="btn btn-success file-operation-btn" title={gettext('Publish Review')} onClick={this.onPublishReview}>{gettext('Publish')}</button>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
this.state.reviewStatus === 'finished' &&
|
||||
<div className="review-state review-state-finished">{gettext('Finished')}</div>
|
||||
}
|
||||
{
|
||||
this.state.reviewStatus === 'closed' &&
|
||||
<div className="review-state review-state-closed">{gettext('Closed')}</div>
|
||||
}
|
||||
<div className="button-group">
|
||||
<button className="btn btn-icon btn-secondary btn-active common-list-btn"
|
||||
id="commentsNumber" type="button" data-active="false"
|
||||
onMouseDown={this.toggleCommentList}>
|
||||
<i className="fa fa-comments"></i>
|
||||
{ this.state.commentsNumber > 0 &&
|
||||
<span> {this.state.commentsNumber}</span>
|
||||
}
|
||||
</button>
|
||||
{
|
||||
this.state.reviewStatus === 'open' &&
|
||||
<div className="cur-file-operation">
|
||||
<button className="btn btn-secondary file-operation-btn" title={gettext('Close Review')} onClick={this.onCloseReview}>{gettext("Close")}</button>
|
||||
<button className="btn btn-success file-operation-btn" title={gettext('Publish Review')} onClick={this.onPublishReview}>{gettext("Publish")}</button>
|
||||
</div>
|
||||
}
|
||||
{
|
||||
this.state.reviewStatus === 'finished' &&
|
||||
<div className="review-state review-state-finished">{gettext('Finished')}</div>
|
||||
}
|
||||
{
|
||||
this.state.reviewStatus === 'closed' &&
|
||||
<div className="review-state review-state-closed">{gettext('Closed')}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div id="main" className="main">
|
||||
<div className="cur-view-container content-container">
|
||||
<div className="cur-view-content">
|
||||
<div className={!this.state.isShowComments ? "cur-view-content" : "cur-view-content cur-view-content-commenton"}>
|
||||
<div className="markdown-viewer-render-content article">
|
||||
{this.state.isLoading ?
|
||||
<Loading /> :
|
||||
@@ -121,6 +154,13 @@ class DraftReview extends React.Component {
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{ this.state.isShowComments &&
|
||||
<ReviewComments
|
||||
toggleCommentList={this.toggleCommentList}
|
||||
commentsNumber={this.state.commentsNumber}
|
||||
getCommentsNumber={this.getCommentsNumber}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -131,4 +171,4 @@ class DraftReview extends React.Component {
|
||||
ReactDOM.render (
|
||||
<DraftReview />,
|
||||
document.getElementById('wrapper')
|
||||
);
|
||||
);
|
54
frontend/src/utils/seafile-markdown2html.js
Normal file
54
frontend/src/utils/seafile-markdown2html.js
Normal file
@@ -0,0 +1,54 @@
|
||||
var unified = require('unified');
|
||||
var markdown = require('remark-parse');
|
||||
var slug = require('remark-slug');
|
||||
var breaks = require('remark-breaks');
|
||||
var remark2rehype = require('remark-rehype');
|
||||
var format = require('rehype-format');
|
||||
var raw = require('rehype-raw');
|
||||
var xtend = require('xtend');
|
||||
var toHTML = require('hast-util-to-html');
|
||||
var sanitize = require('hast-util-sanitize');
|
||||
var gh = require('hast-util-sanitize/lib/github');
|
||||
var deepmerge = require('deepmerge').default;
|
||||
|
||||
function stringify(config) {
|
||||
var settings = xtend(config, this.data('settings'));
|
||||
var schema = deepmerge(gh, {
|
||||
"attributes":{
|
||||
"input": [
|
||||
"type",
|
||||
],
|
||||
"li": [
|
||||
"className"
|
||||
],
|
||||
"code":[
|
||||
"className",
|
||||
],
|
||||
},
|
||||
"tagNames": [
|
||||
"input",
|
||||
"code"
|
||||
]
|
||||
});
|
||||
this.Compiler = compiler;
|
||||
|
||||
function compiler(tree) {
|
||||
var hast = sanitize(tree, schema);
|
||||
return toHTML(hast, settings);
|
||||
}
|
||||
}
|
||||
|
||||
var processor = unified()
|
||||
.use(markdown, {commonmark: true})
|
||||
.use(breaks)
|
||||
.use(slug)
|
||||
.use(remark2rehype, {allowDangerousHTML: true})
|
||||
.use(raw)
|
||||
.use(format)
|
||||
.use(stringify);
|
||||
|
||||
var processorGetAST = unified()
|
||||
.use(markdown, {commonmark: true})
|
||||
.use(slug)
|
||||
|
||||
export { processor, processorGetAST };
|
211
seahub/api2/endpoints/review_comments.py
Normal file
211
seahub/api2/endpoints/review_comments.py
Normal file
@@ -0,0 +1,211 @@
|
||||
# Copyright (c) 2012-2016 Seafile Ltd.
|
||||
import logging
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from seaserv import seafile_api
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from seahub.api2.authentication import TokenAuthentication
|
||||
from seahub.api2.throttling import UserRateThrottle
|
||||
from seahub.api2.utils import api_error, user_to_dict, to_python_boolean
|
||||
from seahub.avatar.settings import AVATAR_DEFAULT_SIZE
|
||||
from seahub.utils.repo import get_repo_owner
|
||||
from seahub.api2.endpoints.utils import generate_links_header_for_paginator
|
||||
from seahub.views import check_folder_permission
|
||||
from seahub.drafts.models import DraftReview, ReviewComment
|
||||
from seahub.drafts.signals import comment_review_successful
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReviewCommentsView(APIView):
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAuthenticated, )
|
||||
throttle_classes = (UserRateThrottle, )
|
||||
|
||||
def get(self, request, review_id, format=None):
|
||||
"""List all comments of a review.
|
||||
"""
|
||||
# resource check
|
||||
try:
|
||||
r = DraftReview.objects.get(pk=review_id)
|
||||
except DraftReview.DoesNotExist:
|
||||
return api_error(status.HTTP_404_NOT_FOUND,
|
||||
'Review %s not found' % review_id)
|
||||
|
||||
resolved = request.GET.get('resolved', None)
|
||||
if resolved not in ('true', 'false', None):
|
||||
error_msg = 'resolved invalid.'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
# permission check
|
||||
if check_folder_permission(request, r.origin_repo_id, '/') is None:
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
try:
|
||||
avatar_size = int(request.GET.get('avatar_size',
|
||||
AVATAR_DEFAULT_SIZE))
|
||||
page = int(request.GET.get('page', '1'))
|
||||
per_page = int(request.GET.get('per_page', '25'))
|
||||
except ValueError:
|
||||
avatar_size = AVATAR_DEFAULT_SIZE
|
||||
page = 1
|
||||
per_page = 25
|
||||
|
||||
start = (page - 1) * per_page
|
||||
end = page * per_page
|
||||
|
||||
total_count = ReviewComment.objects.filter(review_id=r).count()
|
||||
comments = []
|
||||
|
||||
if resolved is None:
|
||||
file_comments = ReviewComment.objects.filter(review_id=r)
|
||||
else:
|
||||
comment_resolved = to_python_boolean(resolved)
|
||||
file_comments = ReviewComment.objects.filter(review_id=r, resolved=comment_resolved)[start: end]
|
||||
|
||||
for file_comment in file_comments:
|
||||
comment = file_comment.to_dict()
|
||||
comment.update(user_to_dict(file_comment.author, request=request, avatar_size=avatar_size))
|
||||
comments.append(comment)
|
||||
|
||||
result = {'comments': comments, 'total_count': total_count}
|
||||
resp = Response(result)
|
||||
base_url = reverse('api2-review-comments', args=[review_id])
|
||||
links_header = generate_links_header_for_paginator(base_url, page,
|
||||
per_page, total_count)
|
||||
resp['Links'] = links_header
|
||||
return resp
|
||||
|
||||
def post(self, request, review_id, format=None):
|
||||
"""Post a comments of a review.
|
||||
"""
|
||||
|
||||
# resource check
|
||||
try:
|
||||
r = DraftReview.objects.get(pk=review_id)
|
||||
except DraftReview.DoesNotExist:
|
||||
return api_error(status.HTTP_404_NOT_FOUND,
|
||||
'Review %s not found' % review_id)
|
||||
|
||||
# permission check
|
||||
if check_folder_permission(request, r.origin_repo_id, '/') is None:
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
# argument check
|
||||
comment = request.data.get('comment', '')
|
||||
if not comment:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'Comment can not be empty.')
|
||||
|
||||
try:
|
||||
avatar_size = int(request.GET.get('avatar_size',
|
||||
AVATAR_DEFAULT_SIZE))
|
||||
except ValueError:
|
||||
avatar_size = AVATAR_DEFAULT_SIZE
|
||||
|
||||
detail = request.data.get('detail', '')
|
||||
username = request.user.username
|
||||
|
||||
review_comment = ReviewComment.objects.add(comment, detail, username, r)
|
||||
|
||||
# Send notification to review creator
|
||||
comment_review_successful.send(sender=None, review=r, comment=comment, author=username)
|
||||
|
||||
comment = review_comment.to_dict()
|
||||
comment.update(user_to_dict(username, request=request, avatar_size=avatar_size))
|
||||
return Response(comment)
|
||||
|
||||
|
||||
class ReviewCommentView(APIView):
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAuthenticated, )
|
||||
throttle_classes = (UserRateThrottle, )
|
||||
|
||||
def delete(self, request, review_id, comment_id, format=None):
|
||||
"""Delete a comment, only comment author or review creator can perform this op.
|
||||
"""
|
||||
# resource check
|
||||
try:
|
||||
r = DraftReview.objects.get(pk=review_id)
|
||||
except DraftReview.DoesNotExist:
|
||||
return api_error(status.HTTP_404_NOT_FOUND,
|
||||
'Review %s not found' % review_id)
|
||||
|
||||
try:
|
||||
review_comment = ReviewComment.objects.get(pk=comment_id)
|
||||
except ReviewComment.DoesNotExist:
|
||||
return api_error(status.HTTP_404_NOT_FOUND,
|
||||
'Review comment %s not found' % comment_id)
|
||||
|
||||
username = request.user.username
|
||||
if username != (review_comment.author and r.creator):
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
review_comment.delete()
|
||||
|
||||
return Response(status.HTTP_200_OK)
|
||||
|
||||
def put(self, request, review_id, comment_id, format=None):
|
||||
"""Update a comment, only comment author or review creator can perform
|
||||
this op
|
||||
1.Change resolved of comment
|
||||
2.Add comment_detail
|
||||
"""
|
||||
|
||||
# argument check
|
||||
resolved = request.data.get('resolved')
|
||||
if resolved not in ('true', 'false', None):
|
||||
error_msg = 'resolved invalid.'
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
|
||||
|
||||
detail = request.data.get('detail')
|
||||
|
||||
# resource check
|
||||
try:
|
||||
r = DraftReview.objects.get(pk=review_id)
|
||||
except DraftReview.DoesNotExist:
|
||||
return api_error(status.HTTP_404_NOT_FOUND,
|
||||
'Review %s not found' % review_id)
|
||||
|
||||
try:
|
||||
review_comment = ReviewComment.objects.get(pk=comment_id)
|
||||
except ReviewComment.DoesNotExist:
|
||||
error_msg = 'Review comment %s not found.' % comment_id
|
||||
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
|
||||
|
||||
# permission check
|
||||
username = request.user.username
|
||||
if username != (review_comment.author and r.creator):
|
||||
error_msg = 'Permission denied.'
|
||||
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
|
||||
|
||||
if resolved is not None:
|
||||
comment_resolved = to_python_boolean(resolved)
|
||||
try:
|
||||
review_comment.resolved = comment_resolved
|
||||
review_comment.save()
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal error.')
|
||||
|
||||
if detail is not None:
|
||||
try:
|
||||
review_comment.detail = detail
|
||||
review_comment.save()
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal error.')
|
||||
|
||||
try:
|
||||
avatar_size = int(request.GET.get('avatar_size', AVATAR_DEFAULT_SIZE))
|
||||
except ValueError:
|
||||
avatar_size = AVATAR_DEFAULT_SIZE
|
||||
|
||||
comment = review_comment.to_dict()
|
||||
comment.update(user_to_dict(username, request=request, avatar_size=avatar_size))
|
||||
|
||||
return Response(comment)
|
@@ -17,6 +17,7 @@ from .endpoints.group_discussions import GroupDiscussions
|
||||
from .endpoints.group_discussion import GroupDiscussion
|
||||
from .endpoints.send_share_link_email import SendShareLinkView
|
||||
from .endpoints.send_upload_link_email import SendUploadLinkView
|
||||
from .endpoints.review_comments import ReviewCommentsView, ReviewCommentView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^ping/$', Ping.as_view()),
|
||||
@@ -36,6 +37,8 @@ urlpatterns = [
|
||||
url(r'^regdevice/$', RegDevice.as_view(), name="regdevice"),
|
||||
url(r'^search/$', Search.as_view(), name='api_search'),
|
||||
url(r'^search-user/$', SearchUser.as_view(), name='search-user'),
|
||||
url(r'^review/(?P<review_id>\d+)/comments/$', ReviewCommentsView.as_view(), name='api2-review-comments'),
|
||||
url(r'^review/(?P<review_id>\d+)/comment/(?P<comment_id>\d+)/$', ReviewCommentView.as_view(), name='api2-review-comment'),
|
||||
url(r'^repos/$', Repos.as_view(), name="api2-repos"),
|
||||
url(r'^repos/public/$', PubRepos.as_view(), name="api2-pub-repos"),
|
||||
url(r'^repos/(?P<repo_id>[-0-9a-f]{36})/$', Repo.as_view(), name="api2-repo"),
|
||||
|
34
seahub/drafts/migrations/0003_reviewcomment.py
Normal file
34
seahub/drafts/migrations/0003_reviewcomment.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.15 on 2018-10-16 07:04
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import seahub.base.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('drafts', '0002_auto_20181011_1207'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ReviewComment',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True, db_index=True)),
|
||||
('author', seahub.base.fields.LowerCaseCharField(db_index=True, max_length=255)),
|
||||
('resolved', models.BooleanField(db_index=True, default=False)),
|
||||
('comment', models.TextField()),
|
||||
('detail', models.TextField()),
|
||||
('review_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='drafts.DraftReview')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-created_at', '-updated_at'],
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
@@ -201,13 +201,34 @@ class DraftReview(TimestampedModel):
|
||||
}
|
||||
|
||||
|
||||
###### signal handlers
|
||||
from django.dispatch import receiver
|
||||
from seahub.signals import repo_deleted
|
||||
class ReviewCommentManager(models.Manager):
|
||||
def add(self, comment, detail, author, review_id):
|
||||
review_comment = self.model(author=author, comment=comment,
|
||||
detail=detail, review_id=review_id)
|
||||
review_comment.save(using=self._db)
|
||||
|
||||
@receiver(repo_deleted)
|
||||
def remove_drafts(sender, **kwargs):
|
||||
repo_owner = kwargs['repo_owner']
|
||||
repo_id = kwargs['repo_id']
|
||||
return review_comment
|
||||
|
||||
Draft.objects.filter(username=repo_owner, draft_repo_id=repo_id).delete()
|
||||
|
||||
class ReviewComment(TimestampedModel):
|
||||
"""
|
||||
Model used to record file comments.
|
||||
"""
|
||||
author = LowerCaseCharField(max_length=255, db_index=True)
|
||||
resolved = models.BooleanField(default=False, db_index=True)
|
||||
review_id = models.ForeignKey('DraftReview', on_delete=models.CASCADE)
|
||||
comment = models.TextField()
|
||||
detail = models.TextField()
|
||||
|
||||
objects = ReviewCommentManager()
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
'id': self.pk,
|
||||
'review_id': self.review_id_id,
|
||||
'comment': self.comment,
|
||||
'created_at': datetime_to_isoformat_timestr(self.created_at),
|
||||
'updated_at': datetime_to_isoformat_timestr(self.updated_at),
|
||||
'resolved': self.resolved,
|
||||
'detail': self.detail,
|
||||
}
|
||||
|
4
seahub/drafts/signals.py
Normal file
4
seahub/drafts/signals.py
Normal file
@@ -0,0 +1,4 @@
|
||||
# Copyright (c) 2012-2018 Seafile Ltd.
|
||||
import django.dispatch
|
||||
|
||||
comment_review_successful = django.dispatch.Signal(providing_args=["review", "comment", "author"])
|
@@ -53,6 +53,7 @@ MSG_TYPE_REPO_SHARE = 'repo_share'
|
||||
MSG_TYPE_REPO_SHARE_TO_GROUP = 'repo_share_to_group'
|
||||
MSG_TYPE_USER_MESSAGE = 'user_message'
|
||||
MSG_TYPE_FILE_COMMENT = 'file_comment'
|
||||
MSG_TYPE_REVIEW_COMMENT = 'review_comment'
|
||||
MSG_TYPE_GUEST_INVITATION_ACCEPTED = 'guest_invitation_accepted'
|
||||
|
||||
USER_NOTIFICATION_COUNT_CACHE_PREFIX = 'USER_NOTIFICATION_COUNT_'
|
||||
@@ -92,6 +93,11 @@ def file_comment_msg_to_json(repo_id, file_path, author, comment):
|
||||
'author': author,
|
||||
'comment': comment})
|
||||
|
||||
def review_comment_msg_to_json(review_id, author, comment):
|
||||
return json.dumps({'review_id': review_id,
|
||||
'author': author,
|
||||
'comment': comment})
|
||||
|
||||
def guest_invitation_accepted_msg_to_json(invitation_id):
|
||||
return json.dumps({'invitation_id': invitation_id})
|
||||
|
||||
@@ -287,6 +293,11 @@ class UserNotificationManager(models.Manager):
|
||||
"""
|
||||
return self._add_user_notification(to_user, MSG_TYPE_FILE_COMMENT, detail)
|
||||
|
||||
def add_review_comment_msg(self, to_user, detail):
|
||||
"""Notify ``to_user`` that review creator
|
||||
"""
|
||||
return self._add_user_notification(to_user, MSG_TYPE_REVIEW_COMMENT, detail)
|
||||
|
||||
def add_guest_invitation_accepted_msg(self, to_user, detail):
|
||||
"""Nofity ``to_user`` that a guest has accpeted an invitation.
|
||||
"""
|
||||
@@ -385,6 +396,9 @@ class UserNotification(models.Model):
|
||||
def is_file_comment_msg(self):
|
||||
return self.msg_type == MSG_TYPE_FILE_COMMENT
|
||||
|
||||
def is_review_comment_msg(self):
|
||||
return self.msg_type == MSG_TYPE_REVIEW_COMMENT
|
||||
|
||||
def is_guest_invitation_accepted_msg(self):
|
||||
return self.msg_type == MSG_TYPE_GUEST_INVITATION_ACCEPTED
|
||||
|
||||
@@ -729,6 +743,23 @@ class UserNotification(models.Model):
|
||||
}
|
||||
return msg
|
||||
|
||||
def format_review_comment_msg(self):
|
||||
try:
|
||||
d = json.loads(self.detail)
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return _(u"Internal error")
|
||||
|
||||
review_id = d['review_id']
|
||||
author = d['author']
|
||||
|
||||
msg = _("Review <a href='%(file_url)s'>%(review_id)s</a> has a new comment from user %(author)s") % {
|
||||
'review_id': review_id,
|
||||
'file_url': reverse('drafts:review', args=[review_id]),
|
||||
'author': escape(email2nickname(author)),
|
||||
}
|
||||
return msg
|
||||
|
||||
def format_guest_invitation_accepted_msg(self):
|
||||
try:
|
||||
d = json.loads(self.detail)
|
||||
@@ -763,6 +794,7 @@ from seahub.group.signals import grpmsg_added, group_join_request, add_user_to_g
|
||||
from seahub.share.signals import share_repo_to_user_successful, \
|
||||
share_repo_to_group_successful
|
||||
from seahub.invitations.signals import accept_guest_invitation_successful
|
||||
from seahub.drafts.signals import comment_review_successful
|
||||
|
||||
@receiver(upload_file_successful)
|
||||
def add_upload_file_msg_cb(sender, **kwargs):
|
||||
@@ -867,6 +899,16 @@ def comment_file_successful_cb(sender, **kwargs):
|
||||
detail = file_comment_msg_to_json(repo.id, file_path, author, comment)
|
||||
UserNotification.objects.add_file_comment_msg(u, detail)
|
||||
|
||||
@receiver(comment_review_successful)
|
||||
def comment_review_successful_cb(sender, **kwargs):
|
||||
review = kwargs['review']
|
||||
comment = kwargs['comment']
|
||||
author = kwargs['author']
|
||||
|
||||
detail = review_comment_msg_to_json(review.id, author, comment)
|
||||
UserNotification.objects.add_review_comment_msg(review.creator, detail)
|
||||
|
||||
|
||||
@receiver(accept_guest_invitation_successful)
|
||||
def accept_guest_invitation_successful_cb(sender, **kwargs):
|
||||
inv_obj = kwargs['invitation_obj']
|
||||
|
@@ -31,9 +31,13 @@
|
||||
|
||||
{% elif notice.is_group_join_request %}
|
||||
<p class="brief">{{ notice.format_group_join_request|safe }}</p>
|
||||
|
||||
{% elif notice.is_file_comment_msg %}
|
||||
<p class="brief">{{ notice.format_file_comment_msg|safe }}</p>
|
||||
|
||||
{% elif notice.is_review_comment_msg %}
|
||||
<p class="brief">{{ notice.format_review_comment_msg|safe }}</p>
|
||||
|
||||
{% elif notice.is_guest_invitation_accepted_msg %}
|
||||
<p class="brief">{{ notice.format_guest_invitation_accepted_msg|safe }}</p>
|
||||
|
||||
|
@@ -185,4 +185,12 @@ def add_notice_from_info(notices):
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
elif notice.is_review_comment_msg():
|
||||
try:
|
||||
d = json.loads(notice.detail)
|
||||
notice.msg_from = d['author']
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
|
||||
|
||||
return notices
|
||||
|
@@ -37,6 +37,9 @@
|
||||
{% elif notice.is_file_comment_msg %}
|
||||
<p class="brief">{{ notice.format_file_comment_msg|safe }}</p>
|
||||
|
||||
{% elif notice.is_review_comment_msg %}
|
||||
<p class="brief">{{ notice.format_review_comment_msg|safe }}</p>
|
||||
|
||||
{% elif notice.is_guest_invitation_accepted_msg %}
|
||||
<p class="brief">{{ notice.format_guest_invitation_accepted_msg|safe }}</p>
|
||||
|
||||
|
Reference in New Issue
Block a user