diff --git a/frontend/src/components/dialog/list-repo-drafts-dialog.js b/frontend/src/components/dialog/list-repo-drafts-dialog.js new file mode 100644 index 0000000000..3d4bcb05e1 --- /dev/null +++ b/frontend/src/components/dialog/list-repo-drafts-dialog.js @@ -0,0 +1,80 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; +import { gettext, siteRoot } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import moment from 'moment'; +import { Utils } from '../../utils/utils'; +import Draft from '../../models/draft'; + +const propTypes = { + repoID: PropTypes.string.isRequired, + toggle: PropTypes.func.isRequired, +}; + +class ListRepoDraftsDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + drafts: [], + }; + } + + componentDidMount() { + seafileAPI.listRepoDrafts(this.props.repoID).then(res => { + let drafts = res.data.drafts.map(item => { + let draft = new Draft(item); + return draft; + }) + this.setState({ + drafts: drafts + }); + }) + } + + toggle = () => { + this.props.toggle(); + } + + render() { + let drafts = this.state.drafts; + return ( + + {gettext('Drafts')} + + + + + + + + + + + {this.state.drafts.map((draft) => { + let href = siteRoot + 'lib/' + draft.originRepoID + '/file' + Utils.encodePath(draft.draftFilePath); + return ( + + + + + + ); + })} + +
{gettext('Name')}{gettext('Owner')}{gettext('Last Update')}
+ {Utils.getFileName(draft.draftFilePath)} + {draft.ownerNickname}{moment(draft.createdStr).fromNow()}
+
+ + + +
+ ); + } +} + +ListRepoDraftsDialog.propTypes = propTypes; + +export default ListRepoDraftsDialog; diff --git a/frontend/src/components/dialog/list-repo-reviews-dialog.js b/frontend/src/components/dialog/list-repo-reviews-dialog.js new file mode 100644 index 0000000000..8638bc32a7 --- /dev/null +++ b/frontend/src/components/dialog/list-repo-reviews-dialog.js @@ -0,0 +1,81 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; +import { gettext, siteRoot } from '../../utils/constants'; +import { seafileAPI } from '../../utils/seafile-api'; +import moment from 'moment'; +import { Utils } from '../../utils/utils'; +import Review from '../../models/review'; + +const propTypes = { + repoID: PropTypes.string.isRequired, + toggle: PropTypes.func.isRequired, +}; + + +class ListRepoReviewsDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + reviews: [], + }; + } + + componentDidMount() { + seafileAPI.listRepoReviews(this.props.repoID).then(res => { + let reviews = res.data.reviews.map(item =>{ + let review = new Review(item); + return review; + }) + this.setState({ + reviews: reviews + }); + }); + } + + toggle = () => { + this.props.toggle(); + } + + render() { + let reviews = this.state.reviews; + return ( + + {gettext('Reviews')} + + + + + + + + + + + {this.state.reviews.map((review) => { + let href = siteRoot + 'drafts/review/' + review.id; + return ( + + + + + + ); + })} + +
{gettext('Name')}{gettext('Owner')}{gettext('Last Update')}
+ {Utils.getFileName(review.draftFilePath)} + {review.creatorName}{moment(review.createdStr).fromNow()}
+
+ + + +
+ ); + } +} + +ListRepoReviewsDialog.propTypes = propTypes; + +export default ListRepoReviewsDialog; diff --git a/frontend/src/components/dialog/list-taggedfiles-dialog.js b/frontend/src/components/dialog/list-taggedfiles-dialog.js index 07f11b5cce..2632625565 100644 --- a/frontend/src/components/dialog/list-taggedfiles-dialog.js +++ b/frontend/src/components/dialog/list-taggedfiles-dialog.js @@ -1,4 +1,4 @@ -import React, { Fragment } from 'react'; +import React from 'react'; import PropTypes from 'prop-types'; import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap'; import { gettext, siteRoot } from '../../utils/constants'; @@ -44,13 +44,10 @@ class ListTaggedFilesDialog extends React.Component { render() { let taggedFileList = this.state.taggedFileList; return ( - - - - {gettext('Tagged Files')} - + + {gettext('Tagged Files')} - +
@@ -78,7 +75,7 @@ class ListTaggedFilesDialog extends React.Component { - + ); } } diff --git a/frontend/src/components/dir-view/dir-panel.js b/frontend/src/components/dir-view/dir-panel.js index 548943106b..2566b46230 100644 --- a/frontend/src/components/dir-view/dir-panel.js +++ b/frontend/src/components/dir-view/dir-panel.js @@ -52,6 +52,8 @@ const propTypes = { onFileUploadSuccess: PropTypes.func.isRequired, usedRepoTags: PropTypes.array.isRequired, readmeMarkdown: PropTypes.object, + draftCounts: PropTypes.number, + reviewCounts: PropTypes.number, }; class DirPanel extends React.Component { @@ -129,7 +131,9 @@ class DirPanel extends React.Component { render() { const errMessage = (

{gettext('Folder does not exist.')}

); - + const showRepoInfoBar = this.props.path === '/' && ( + this.props.usedRepoTags.length != 0 || this.props.readmeMarkdown != null || + this.props.draftCounts != 0 || this.props.reviewCounts != 0); return (
@@ -191,12 +195,14 @@ class DirPanel extends React.Component { {!this.props.pathExist ? errMessage : - {this.props.path === '/' && !(this.props.usedRepoTags.length === 0 && this.props.readmeMarkdown === null) && ( + {showRepoInfoBar && ( )} { + this.setState({ + draftCounts: res.data.draft_counts, + reviewCounts: res.data.review_counts, + }) + }) seafileAPI.listRepoTags(repoID).then(res => { let usedRepoTags = []; res.data.repo_tags.forEach(item => { @@ -696,6 +704,8 @@ class DirView extends React.Component { onLibDecryptDialog={this.onLibDecryptDialog} usedRepoTags={this.state.usedRepoTags} readmeMarkdown={this.state.readmeMarkdown} + draftCounts={this.state.draftCounts} + reviewCounts={this.state.reviewCounts} /> ); } diff --git a/frontend/src/components/repo-info-bar.js b/frontend/src/components/repo-info-bar.js index 9838a5ab3e..0d37771945 100644 --- a/frontend/src/components/repo-info-bar.js +++ b/frontend/src/components/repo-info-bar.js @@ -3,7 +3,9 @@ import PropTypes from 'prop-types'; import ModalPortal from './modal-portal'; import { Modal } from 'reactstrap'; import ListTaggedFilesDialog from './dialog/list-taggedfiles-dialog'; -import { siteRoot } from '../utils/constants'; +import ListRepoDraftsDialog from './dialog/list-repo-drafts-dialog'; +import ListRepoReviewsDialog from './dialog/list-repo-reviews-dialog'; +import { siteRoot, gettext } from '../utils/constants'; import { Utils } from '../utils/utils'; import '../css/repo-info-bar.css'; @@ -13,6 +15,8 @@ const propTypes = { currentPath: PropTypes.string.isRequired, usedRepoTags: PropTypes.array.isRequired, readmeMarkdown: PropTypes.object, + draftCounts: PropTypes.number, + reviewCounts: PropTypes.number, }; class RepoInfoBar extends React.Component { @@ -22,6 +26,8 @@ class RepoInfoBar extends React.Component { this.state = { currentTag: null, isListTaggedFileShow: false, + showRepoDrafts: false, + showRepoReviews: false, }; } @@ -38,6 +44,18 @@ class RepoInfoBar extends React.Component { }); } + toggleDrafts = () => { + this.setState({ + showRepoDrafts: !this.state.showRepoDrafts + }); + } + + toggleReviews = () => { + this.setState({ + showRepoReviews: !this.state.showRepoReviews + }); + } + render() { let {repoID, currentPath, usedRepoTags, readmeMarkdown} = this.props; let href = readmeMarkdown !== null ? siteRoot + 'lib/' + repoID + '/file' + Utils.joinPath(currentPath, readmeMarkdown.name) : ''; @@ -59,24 +77,61 @@ class RepoInfoBar extends React.Component { })} )} - {readmeMarkdown !== null && ( - - )} +
+ {readmeMarkdown !== null && ( + + + {readmeMarkdown.name} + + )} + {this.props.draftCounts > 0 && + + + {gettext('draft')} + + {this.props.draftCounts > 1 ? this.props.draftCounts + ' files' : this.props.draftCounts + ' file'} + + + } + {this.props.reviewCounts > 0 && + + + {gettext('review')} + + {this.props.reviewCounts > 1 ? this.props.reviewCounts + ' files' : this.props.reviewCounts + ' file'} + + + } +
{this.state.isListTaggedFileShow && ( - - - + )} + + {this.state.showRepoDrafts && ( + + + + )} + + {this.state.showRepoReviews && ( + + + + )} +
); } diff --git a/frontend/src/css/repo-info-bar.css b/frontend/src/css/repo-info-bar.css index 1d29acb9c8..fa87f8eb3b 100644 --- a/frontend/src/css/repo-info-bar.css +++ b/frontend/src/css/repo-info-bar.css @@ -1,6 +1,5 @@ .repo-info-bar { - margin-bottom: 5px; - padding: 0 10px; + padding: 10px; border: 1px solid #e6e6dd; border-radius: 5px; background: #f8f8f8; @@ -8,7 +7,6 @@ .used-tag-list { list-style: none; - margin: 8px 0; } .used-tag-item { @@ -38,7 +36,8 @@ } .readme-file { - margin: 8px 15px; + margin: 0 15px; + display: inline-block; } .readme-file a { diff --git a/frontend/src/models/draft.js b/frontend/src/models/draft.js new file mode 100644 index 0000000000..106967d288 --- /dev/null +++ b/frontend/src/models/draft.js @@ -0,0 +1,15 @@ +import moment from 'moment'; + +class Draft { + + constructor(item) { + this.created = item.created_at; + this.createdStr = moment((new Date(item.created_at)).getTime()).format('YYYY-MM-DD HH:mm'); + this.id = item.id; + this.ownerNickname = item.owner_nickname; + this.originRepoID = item.origin_repo_id; + this.draftFilePath = item.draft_file_path; + } +} + +export default Draft; diff --git a/frontend/src/models/review.js b/frontend/src/models/review.js new file mode 100644 index 0000000000..a508966f6d --- /dev/null +++ b/frontend/src/models/review.js @@ -0,0 +1,14 @@ +import moment from 'moment'; + +class Review { + + constructor(item) { + this.created = item.created_at; + this.createdStr = moment((new Date(item.created_at)).getTime()).format('YYYY-MM-DD HH:mm'); + this.id = item.id; + this.creatorName = item.creator_name; + this.draftFilePath = item.draft_file_path; + } +} + +export default Review; diff --git a/frontend/src/pages/repo-wiki-mode/main-panel.js b/frontend/src/pages/repo-wiki-mode/main-panel.js index 30f4ccb4b8..b5521941a4 100644 --- a/frontend/src/pages/repo-wiki-mode/main-panel.js +++ b/frontend/src/pages/repo-wiki-mode/main-panel.js @@ -63,6 +63,8 @@ const propTypes = { reviewID: PropTypes.any, usedRepoTags: PropTypes.array.isRequired, readmeMarkdown: PropTypes.object, + draftCounts: PropTypes.number, + reviewCounts: PropTypes.number, }; class MainPanel extends Component { @@ -160,6 +162,9 @@ class MainPanel extends Component { render() { const ErrMessage = (

{gettext('Folder does not exist.')}

); + const showRepoInfoBar = this.props.path === '/' && ( + this.props.usedRepoTags.length != 0 || this.props.readmeMarkdown != null || + this.props.draftCounts != 0 || this.props.reviewCounts != 0); return (
@@ -240,12 +245,14 @@ class MainPanel extends Component { : - {this.props.path === '/' && !(this.props.usedRepoTags.length === 0 && this.props.readmeMarkdown === null) && ( + {showRepoInfoBar && ( )} { + this.setState({ + draftCounts: res.data.draft_counts, + reviewCounts: res.data.review_counts + }); + }); seafileAPI.listRepoTags(repoID).then(res => { let usedRepoTags = []; res.data.repo_tags.forEach(item => { @@ -1007,6 +1015,8 @@ class Wiki extends Component { goReviewPage={this.goReviewPage} usedRepoTags={this.state.usedRepoTags} readmeMarkdown={this.state.readmeMarkdown} + draftCounts={this.state.draftCounts} + reviewCounts={this.state.reviewCounts} />
); diff --git a/seahub/api2/endpoints/repo_draft_review_info.py b/seahub/api2/endpoints/repo_draft_review_info.py new file mode 100644 index 0000000000..b3fcc6f1f3 --- /dev/null +++ b/seahub/api2/endpoints/repo_draft_review_info.py @@ -0,0 +1,98 @@ +# 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.utils.translation import ugettext as _ + +from seahub.api2.authentication import TokenAuthentication +from seahub.api2.throttling import UserRateThrottle +from seahub.api2.utils import api_error +from seahub.views import check_folder_permission + +from seahub.drafts.models import Draft, DraftReview + +logger = logging.getLogger(__name__) + + +class RepoDraftReviewCounts(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, ) + throttle_classes = (UserRateThrottle, ) + + def get(self, request, repo_id, format=None): + repo = seafile_api.get_repo(repo_id) + if not repo: + error_msg = 'Library %s not found.' % repo_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # check perm + perm = check_folder_permission(request, repo_id, '/') + if not perm: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + result = {} + + # get draft counts + result['draft_counts'] = Draft.objects.get_draft_counts_by_repo_id(repo_id) + result['review_counts'] = DraftReview.objects.get_review_counts_by_repo_id(repo_id) + + return Response(result) + + +class RepoDraftInfo(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, ) + throttle_classes = (UserRateThrottle, ) + + def get(self, request, repo_id, format=None): + repo = seafile_api.get_repo(repo_id) + if not repo: + error_msg = 'Library %s not found.' % repo_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # check perm + perm = check_folder_permission(request, repo_id, '/') + if not perm: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + result = {} + + # list draft + drafts = Draft.objects.list_draft_by_repo_id(repo_id) + result['drafts'] = drafts + + return Response(result) + + +class RepoReviewInfo(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, ) + throttle_classes = (UserRateThrottle, ) + + def get(self, request, repo_id, format=None): + repo = seafile_api.get_repo(repo_id) + if not repo: + error_msg = 'Library %s not found.' % repo_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # check perm + perm = check_folder_permission(request, repo_id, '/') + if not perm: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + result = {} + + # list review + reviews = DraftReview.objects.list_review_by_repo_id(repo_id) + result['reviews'] = reviews + + return Response(result) diff --git a/seahub/drafts/models.py b/seahub/drafts/models.py index a94745acf1..af0c5750f8 100644 --- a/seahub/drafts/models.py +++ b/seahub/drafts/models.py @@ -28,6 +28,29 @@ class OriginalFileConflict(Exception): class DraftManager(models.Manager): + def get_draft_counts_by_repo_id(self, repo_id): + num = self.filter(origin_repo_id=repo_id).count() + + return num + + def list_draft_by_repo_id(self, repo_id): + """list draft by repo id + """ + drafts = [] + qs = self.filter(origin_repo_id=repo_id) + + for d in qs: + draft = {} + draft['id'] = d.id + draft['owner_nickname'] = email2nickname(d.username) + draft['origin_repo_id'] = d.origin_repo_id + draft['draft_file_path'] = d.draft_file_path + draft['created_at'] = datetime_to_isoformat_timestr(d.created_at) + + drafts.append(draft) + + return drafts + def list_draft_by_username(self, username, with_reviews=True): """list all user drafts If with_reviews is true, return the draft associated review @@ -246,6 +269,25 @@ class DraftReviewExist(Exception): class DraftReviewManager(models.Manager): + def get_review_counts_by_repo_id(self, repo_id, status='open'): + num = self.filter(origin_repo_id=repo_id, status=status).count() + + return num + + def list_review_by_repo_id(self, repo_id, status='open'): + reviews = [] + qs = self.filter(origin_repo_id=repo_id, status=status) + + for review in qs: + review_obj = {} + review_obj['id'] = review.id + review_obj['creator_name'] = email2nickname(review.creator) + review_obj['created_at'] = datetime_to_isoformat_timestr(review.created_at) + review_obj['draft_file_path'] = review.draft_file_path + reviews.append(review_obj) + + return reviews + def add(self, creator, draft): try: d_r = self.get(creator=creator, draft_id=draft) diff --git a/seahub/urls.py b/seahub/urls.py index 8f3a09ac39..b5967effd9 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -70,6 +70,8 @@ from seahub.api2.endpoints.wikis import WikisView, WikiView from seahub.api2.endpoints.drafts import DraftsView, DraftView from seahub.api2.endpoints.draft_reviews import DraftReviewsView, DraftReviewView from seahub.api2.endpoints.draft_review_reviewer import DraftReviewReviewerView +from seahub.api2.endpoints.repo_draft_review_info import RepoDraftInfo, \ + RepoReviewInfo, RepoDraftReviewCounts from seahub.api2.endpoints.file_review import FileReviewView from seahub.api2.endpoints.activities import ActivitiesView from seahub.api2.endpoints.wiki_pages import WikiPageView, WikiPagesView, WikiPagesDirView, WikiPageContentView @@ -373,6 +375,10 @@ urlpatterns = [ url(r'^api/v2.1/file-review/$', FileReviewView.as_view(), name='api-v2.1-file-review'), + url(r'^api/v2.1/repo/(?P[-0-9a-f]{36})/drafts/$', RepoDraftInfo.as_view(), name='api-v2.1-repo-drafts' ), + url(r'^api/v2.1/repo/(?P[-0-9a-f]{36})/reviews/$', RepoReviewInfo.as_view(), name='api-v2.1-repo-reviews' ), + url(r'^api/v2.1/repo/(?P[-0-9a-f]{36})/draft-review-counts/$', RepoDraftReviewCounts.as_view(), name='api-v2.1-repo-draft-review-counts' ), + ## user::activities url(r'^api/v2.1/activities/$', ActivitiesView.as_view(), name='api-v2.1-acitvity'),
{gettext('Name')}