diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 459b74396c5..e208b84f56e 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -41,7 +41,7 @@ "react-select": "5.7.0", "react-transition-group": "4.4.5", "reactstrap": "8.9.0", - "seafile-js": "0.2.217", + "seafile-js": "0.2.218", "socket.io-client": "^2.2.0", "svg-sprite-loader": "^6.0.11", "svgo-loader": "^3.0.1", @@ -22233,9 +22233,9 @@ } }, "node_modules/seafile-js": { - "version": "0.2.217", - "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.217.tgz", - "integrity": "sha512-lsXtLZFmLa+8hdI2MTE59hypF2Rd8fHiaG3qZHJ/NTCLdlBplgn8hsHML0SfH04iuC6xsjLQLd6YJCt4fut2ww==", + "version": "0.2.218", + "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.218.tgz", + "integrity": "sha512-+3EFZIKxYUUU6+iImXFkc0k5wyRrPSdR9bQmB9NNTm1TTjFen5tmldt9nhZ1y/+7fyWjUS+k7JRBudF5YsOLOg==", "dependencies": { "axios": "^1.6.0", "form-data": "4.0.0" @@ -43133,9 +43133,9 @@ } }, "seafile-js": { - "version": "0.2.217", - "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.217.tgz", - "integrity": "sha512-lsXtLZFmLa+8hdI2MTE59hypF2Rd8fHiaG3qZHJ/NTCLdlBplgn8hsHML0SfH04iuC6xsjLQLd6YJCt4fut2ww==", + "version": "0.2.218", + "resolved": "https://registry.npmjs.org/seafile-js/-/seafile-js-0.2.218.tgz", + "integrity": "sha512-+3EFZIKxYUUU6+iImXFkc0k5wyRrPSdR9bQmB9NNTm1TTjFen5tmldt9nhZ1y/+7fyWjUS+k7JRBudF5YsOLOg==", "requires": { "axios": "^1.6.0", "form-data": "4.0.0" diff --git a/frontend/package.json b/frontend/package.json index 05bbf99adce..cbb4df2f653 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -36,7 +36,7 @@ "react-select": "5.7.0", "react-transition-group": "4.4.5", "reactstrap": "8.9.0", - "seafile-js": "0.2.217", + "seafile-js": "0.2.218", "socket.io-client": "^2.2.0", "svg-sprite-loader": "^6.0.11", "svgo-loader": "^3.0.1", diff --git a/frontend/src/pages/sdoc/sdoc-file-history/history-version.js b/frontend/src/pages/sdoc/sdoc-file-history/history-version.js index 741979f905f..bfaafa360af 100644 --- a/frontend/src/pages/sdoc/sdoc-file-history/history-version.js +++ b/frontend/src/pages/sdoc/sdoc-file-history/history-version.js @@ -2,6 +2,7 @@ import moment from 'moment'; import React from 'react'; import PropTypes from 'prop-types'; import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem} from 'reactstrap'; +import classnames from 'classnames'; import { gettext, filePath } from '../../../utils/constants'; import URLDecorator from '../../../utils/url-decorator'; import Rename from '../../../components/rename'; @@ -39,9 +40,9 @@ class HistoryVersion extends React.Component { onClick = () => { this.setState({ isShowOperationIcon: false }); - const { currentVersion, historyVersion } = this.props; + const { currentVersion, historyVersion, path } = this.props; if (currentVersion.commit_id === historyVersion.commit_id) return; - this.props.onSelectHistoryVersion(historyVersion); + this.props.onSelectHistoryVersion(path); }; onRestore = () => { @@ -73,19 +74,36 @@ class HistoryVersion extends React.Component { this.toggleRename(); }; + showDailyHistory = (event) => { + event.stopPropagation(); + event.nativeEvent.stopImmediatePropagation(); + const { path } = this.props; + this.props.showDailyHistory(path); + }; + render() { - const { currentVersion, historyVersion } = this.props; + const { currentVersion, historyVersion, path, showDaily } = this.props; if (!currentVersion || !historyVersion) return null; - const { ctime, commit_id, creator_name, obj_id, name} = historyVersion; + const { ctime, commit_id, creator_name, obj_id, name, count } = historyVersion; const isHighlightItem = commit_id === currentVersion.commit_id; const url = URLDecorator.getUrl({ type: 'download_historic_file', filePath: filePath, objID: obj_id }); return (
  • 0 ? 'daily-history-detail' : ''}`} onMouseEnter={this.onMouseEnter} onMouseLeave={this.onMouseLeave} onClick={this.onClick} > + {path[2] === 0 && ( +
    + {count > 1 && ( +
    + +
    + )} +
    + )} + {path[2] > 0 && (
    )}
    {this.state.isRenameShow ? @@ -109,7 +127,7 @@ class HistoryVersion extends React.Component { {/* {(this.props.index !== 0) && {gettext('Restore')}} */} {gettext('Download')} - {(this.props.index !== 0) && {gettext('Copy')}} + {(path[0] !== 0 && path[1] !== 0 && path[2] !== 0) && {gettext('Copy')}} {gettext('Rename')} @@ -120,13 +138,15 @@ class HistoryVersion extends React.Component { } HistoryVersion.propTypes = { - index: PropTypes.number, + showDaily: PropTypes.bool, + path: PropTypes.array, currentVersion: PropTypes.object.isRequired, historyVersion: PropTypes.object, onSelectHistoryVersion: PropTypes.func.isRequired, onRestore: PropTypes.func.isRequired, onCopy: PropTypes.func.isRequired, renameHistoryVersion: PropTypes.func.isRequired, + showDailyHistory: PropTypes.func.isRequired, }; export default HistoryVersion; diff --git a/frontend/src/css/sdoc-file-history.css b/frontend/src/pages/sdoc/sdoc-file-history/index.css similarity index 74% rename from frontend/src/css/sdoc-file-history.css rename to frontend/src/pages/sdoc/sdoc-file-history/index.css index d9838bb682d..8e3b501aea6 100644 --- a/frontend/src/css/sdoc-file-history.css +++ b/frontend/src/pages/sdoc/sdoc-file-history/index.css @@ -150,6 +150,10 @@ border-bottom: 1px solid #e5e5e5; } +.sdoc-file-history-versions .history-list-item.history-month-title { + padding: 5px 16px; +} + .sdoc-file-history-versions .history-list-item:last-child { border-bottom: none; } @@ -159,6 +163,25 @@ cursor: pointer; } +.sdoc-file-history-versions .history-list-item .history-operation { + flex-shrink: 0; +} + +.sdoc-file-history-versions .history-list-item .history-info { + overflow: hidden; +} + +.sdoc-file-history-versions .history-list-item .history-info .name { + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.sdoc-file-history-versions .history-list-item.item-active .history-info .name{ + color: #ff8000; +} + .sdoc-file-history-versions .history-list-item .history-operation:hover { cursor: pointer; } @@ -210,6 +233,57 @@ font-weight: bolder; } +.sdoc-file-history-versions .history-info .rename-container, +.sdoc-file-history-versions .history-info .rename-container input { + box-sizing: border-box; + width: 100%; + max-width: 100%; + min-width: 100%; + overflow: hidden; +} + +.sdoc-file-history-versions .history-info .rename-container input:focus { + box-shadow: none; +} + +.sdoc-file-history-versions .daily-history-detail-toggle-container { + width: 20px; + height: 100%; + display: flex; + justify-content: center; + flex-shrink: 0; +} + +.sdoc-file-history-versions .daily-history-detail-toggle-container .daily-history-detail-toggle { + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; +} + +.sdoc-file-history-versions .daily-history-detail-no-more { + width: 20px; + height: 20px; + margin-right: 14px; +} + +.sdoc-file-history-versions .daily-history-detail-toggle .dropdown-toggle { + font-size: 18px; + color: #888; + transform: rotate(-90deg); + transition: all .1s; +} + +.sdoc-file-history-versions .daily-history-detail-toggle.daily-history-detail-show .dropdown-toggle { + transform: unset; +} + +.sdoc-file-history-versions .daily-history-detail-toggle .dropdown-toggle:hover { + color: #333; + cursor: pointer; +} + @media (min-width:992px) { .history-side-panel { diff --git a/frontend/src/pages/sdoc/sdoc-file-history/index.js b/frontend/src/pages/sdoc/sdoc-file-history/index.js index 24414c704b0..b2dc3d60f39 100644 --- a/frontend/src/pages/sdoc/sdoc-file-history/index.js +++ b/frontend/src/pages/sdoc/sdoc-file-history/index.js @@ -12,7 +12,7 @@ import { Utils } from '../../../utils/utils'; import toaster from '../../../components/toast'; import '../../../css/layout.css'; -import '../../../css/sdoc-file-history.css'; +import './index.css'; const { serviceURL, avatarURL, siteRoot } = window.app.config; const { username, name } = window.app.pageOptions; @@ -48,15 +48,15 @@ class SdocFileHistory extends React.Component { }; } - onSelectHistoryVersion = (currentVersion, lastVersion) => { + onSelectHistoryVersion = (currentVersion, isShowChanges) => { this.setState({ isLoading: true, currentVersion }); seafileAPI.getFileRevision(historyRepoID, currentVersion.commit_id, currentVersion.path).then(res => { return seafileAPI.getFileContent(res.data); }).then(res => { const currentVersionContent = res.data; - if (lastVersion) { - seafileAPI.getFileRevision(historyRepoID, lastVersion.commit_id, lastVersion.path).then(res => { - return seafileAPI.getFileContent(res.data); + if (isShowChanges) { + seafileAPI.getNextFileRevision(historyRepoID, currentVersion.id, currentVersion.path).then(res => { + return res.data ? seafileAPI.getFileContent(res.data) : { data: '' }; }).then(res => { const lastVersionContent = res.data; this.setContent(currentVersionContent, lastVersionContent); @@ -79,28 +79,28 @@ class SdocFileHistory extends React.Component { this.setState({ currentVersionContent, lastVersionContent, isLoading: false, changes: [], currentDiffIndex: 0 }); }; - onShowChanges = (isShowChanges, lastVersion) => { - if (isShowChanges && lastVersion) { - const { currentVersionContent } = this.state; - this.setState({ isLoading: true }, () => { + onShowChanges = (isShowChanges) => { + if (isShowChanges) { + const { currentVersionContent, currentVersion } = this.state; + this.setState({ isLoading: true, isShowChanges }, () => { localStorage.setItem('seahub-sdoc-history-show-changes', isShowChanges + ''); - seafileAPI.getFileRevision(historyRepoID, lastVersion.commit_id, lastVersion.path).then(res => { - return seafileAPI.getFileContent(res.data); + seafileAPI.getNextFileRevision(historyRepoID, currentVersion.id, currentVersion.path).then(res => { + return res.data ? seafileAPI.getFileContent(res.data) : { data: '' }; }).then(res => { const lastVersionContent = res.data; this.setContent(currentVersionContent, lastVersionContent); - this.setState({ isShowChanges }); }).catch(error => { const errorMessage = Utils.getErrorMsg(error, true); toaster.danger(gettext(errorMessage)); this.setContent(currentVersionContent, ''); - this.setState({ isShowChanges }); }); }); return; } - this.setState({ isShowChanges }, () => { - localStorage.setItem('seahub-sdoc-history-show-changes', isShowChanges + ''); + this.setState({ isLoading: true, isShowChanges }, () => { + this.setState({ isLoading: false, lastVersionContent: '' }, () => { + localStorage.setItem('seahub-sdoc-history-show-changes', isShowChanges + ''); + }); }); }; @@ -175,10 +175,10 @@ class SdocFileHistory extends React.Component { >
    - + {gettext('Last modification')} - + {gettext('Next modification')} diff --git a/frontend/src/pages/sdoc/sdoc-file-history/side-panel.js b/frontend/src/pages/sdoc/sdoc-file-history/side-panel.js index 2a32af33902..a318b2a622d 100644 --- a/frontend/src/pages/sdoc/sdoc-file-history/side-panel.js +++ b/frontend/src/pages/sdoc/sdoc-file-history/side-panel.js @@ -1,4 +1,5 @@ -import React, { Component } from 'react'; +import React, { Component, Fragment } from 'react'; +import moment from 'moment'; import PropTypes from 'prop-types'; import classnames from 'classnames'; import Loading from '../../../components/loading'; @@ -10,6 +11,8 @@ import toaster from '../../../components/toast'; import HistoryVersion from './history-version'; import Switch from '../../../components/common/switch'; +moment.locale(window.app.config.lang); + const { docUuid } = window.fileHistory.pageOptions; class SidePanel extends Component { @@ -18,67 +21,82 @@ class SidePanel extends Component { super(props); this.state = { isLoading: true, - historyVersions: [], + historyGroups: [], errorMessage: '', - currentPage: 1, hasMore: false, fileOwner: '', isReloadingData: false, }; + this.currentPage = 1; } - componentDidMount() { - seafileAPI.listSdocHistory(docUuid, 1, PER_PAGE).then(res => { - let historyList = res.data; - if (historyList.length === 0) { - this.setState({isLoading: false}); - throw Error('there has an error in server'); - } - this.initResultState(res.data); - }); - } + // listSdocDailyHistoryDetail - refershFileList() { - seafileAPI.listSdocHistory(docUuid, 1, PER_PAGE).then(res => { - this.initResultState(res.data); - }); + componentDidMount() { + this.firstLoadSdocHistory(); } - initResultState(result) { - if (result.histories.length) { + firstLoadSdocHistory() { + this.currentPage = 1; + seafileAPI.listSdocHistory(docUuid, this.currentPage, PER_PAGE).then(res => { + const result = res.data; + const resultCount = result.histories.length; this.setState({ - historyVersions: result.histories, - currentPage: result.page, - hasMore: result.total_count > (PER_PAGE * this.state.currentPage), + historyGroups: this.formatHistories(result.histories), + hasMore: resultCount >= PER_PAGE, isLoading: false, fileOwner: result.histories[0].creator_email, }); - this.props.onSelectHistoryVersion(result.histories[0], result.histories[1]); - } + if (result.histories[0]) { + this.props.onSelectHistoryVersion(result.histories[0], result.histories[1]); + } + }).catch((error) => { + this.setState({isLoading: false}); + throw Error('there has an error in server'); + }); + } + + formatHistories(histories) { + const oldHistoryGroups = this.state.historyGroups; + if (!Array.isArray(histories) || histories.length === 0) return oldHistoryGroups; + const newHistoryGroups = oldHistoryGroups.slice(0); + histories.forEach(history => { + const { date } = history; + const momentDate = moment(date); + const month = momentDate.format('YYYY-MM'); + const monthItem = newHistoryGroups.find(item => item.month === month); + if (monthItem) { + monthItem.children.push({ day: momentDate.format('YYYY-MM-DD'), showDaily: false, children: [ history ] }); + } else { + newHistoryGroups.push({ + month, + children: [ + { day: momentDate.format('YYYY-MM-DD'), showDaily: false, children: [ history ] } + ] + }); + } + }); + return newHistoryGroups; } updateResultState(result) { - if (result.histories.length) { - this.setState({ - historyVersions: [...this.state.historyVersions, ...result.histories], - currentPage: result.page, - hasMore: result.total_count > (PER_PAGE * this.state.currentPage), - isLoading: false, - fileOwner: result.histories[0].creator_email - }); - } + const resultCount = result.histories.length; + this.setState({ + historyGroups: this.formatHistories(result.histories), + hasMore: resultCount >= PER_PAGE, + isLoading: false, + fileOwner: result.histories[0].creator_email, + }); } loadMore = () => { if (!this.state.isReloadingData) { - let currentPage = this.state.currentPage + 1; - this.setState({ - currentPage: currentPage, - isReloadingData: true, - }); - seafileAPI.listSdocHistory(docUuid, currentPage, PER_PAGE).then(res => { - this.updateResultState(res.data); - this.setState({isReloadingData: false}); + this.currentPage = this.currentPage + 1; + this.setState({ isReloadingData: true }, () => { + seafileAPI.listSdocHistory(docUuid, this.currentPage, PER_PAGE).then(res => { + this.updateResultState(res.data); + this.setState({isReloadingData: false}); + }); }); } }; @@ -86,7 +104,7 @@ class SidePanel extends Component { renameHistoryVersion = (objID, newName) => { seafileAPI.renameSdocHistory(docUuid, objID, newName).then((res) => { this.setState({ - historyVersions: this.state.historyVersions.map(item => { + historyGroups: this.state.historyGroups.map(item => { if (item.obj_id == objID) { item.name = newName; } @@ -113,8 +131,9 @@ class SidePanel extends Component { const { commit_id, path } = currentItem; editUtilities.revertFile(path, commit_id).then(res => { if (res.data.success) { - this.setState({isLoading: true}); - this.refershFileList(); + this.setState({isLoading: true}, () => { + this.firstLoadSdocHistory(); + }); } let message = gettext('Successfully restored.'); toaster.success(message); @@ -124,15 +143,11 @@ class SidePanel extends Component { }); }; - onSelectHistoryVersion = (historyVersion) => { + onSelectHistoryVersion = (path) => { const { isShowChanges } = this.props; - if (!isShowChanges) { - this.props.onSelectHistoryVersion(historyVersion); - return; - } - const { historyVersions } = this.state; - const historyVersionIndex = historyVersions.findIndex(item => item.commit_id === historyVersion.commit_id); - this.props.onSelectHistoryVersion(historyVersion, historyVersions[historyVersionIndex + 1]); + const { historyGroups } = this.state; + const historyVersion = historyGroups[path[0]].children[path[1]].children[path[2]]; + this.props.onSelectHistoryVersion(historyVersion, isShowChanges); }; copyHistoryFile = (historyVersion) => { @@ -148,9 +163,41 @@ class SidePanel extends Component { }); }; + showDailyHistory = (path, callback) => { + const { historyGroups } = this.state; + const newHistoryGroups = historyGroups.slice(0); + const dayHistoryGroup = newHistoryGroups[path[0]].children[path[1]]; + if (dayHistoryGroup.showDaily) { + dayHistoryGroup.showDaily = false; + this.setState({ historyGroups: newHistoryGroups }, () => { + callback && callback(); + }); + return; + } + if (dayHistoryGroup.children.length > 1) { + dayHistoryGroup.showDaily = true; + this.setState({ historyGroups: newHistoryGroups }, () => { + callback && callback(); + }); + return; + } + + seafileAPI.listSdocDailyHistoryDetail(docUuid, dayHistoryGroup.children[0].ctime).then(res => { + const histories = res.data.histories; + dayHistoryGroup.children.push(...histories); + dayHistoryGroup.showDaily = true; + this.setState({ historyGroups: newHistoryGroups }, () => { + callback && callback(); + }); + }).catch(error => { + const errorMessage = Utils.getErrorMsg(error, true); + toaster.danger(gettext(errorMessage)); + }); + }; + renderHistoryVersions = () => { - const { isLoading, historyVersions, errorMessage } = this.state; - if (historyVersions.length === 0) { + const { isLoading, historyGroups, errorMessage } = this.state; + if (historyGroups.length === 0) { if (isLoading) { return (
    @@ -174,18 +221,31 @@ class SidePanel extends Component { return ( <> - {historyVersions.map((historyVersion, index) => { + {historyGroups.map((monthHistoryGroup, historyGroupIndex) => { return ( - + +
    {monthHistoryGroup.month}
    + {monthHistoryGroup.children.map((dayHistoryGroup, dayHistoryGroupIndex) => { + const { children, showDaily } = dayHistoryGroup; + const displayHistories = showDaily ? children : children.slice(0, 1); + return displayHistories.map((history, index) => { + return ( + + ); + }); + }).flat()} +
    ); })} {isLoading && ( @@ -198,15 +258,12 @@ class SidePanel extends Component { }; onShowChanges = () => { - const { isShowChanges, currentVersion } = this.props; - const { historyVersions } = this.state; - const historyVersionIndex = historyVersions.findIndex(item => item.commit_id === currentVersion.commit_id); - const lastVersion = historyVersions[historyVersionIndex + 1]; - this.props.onShowChanges(!isShowChanges, lastVersion); + const { isShowChanges } = this.props; + this.props.onShowChanges(!isShowChanges); }; render() { - const { historyVersions } = this.state; + const { historyGroups } = this.state; return (
    @@ -216,7 +273,7 @@ class SidePanel extends Component {
    {this.renderHistoryVersions()} diff --git a/seahub/api2/urls.py b/seahub/api2/urls.py index 4f51a69b3bf..3bed6692387 100644 --- a/seahub/api2/urls.py +++ b/seahub/api2/urls.py @@ -61,6 +61,7 @@ re_path(r'^repos/(?P[-0-9-a-f]{36})/file/detail/$', FileDetailView.as_view()), re_path(r'^repos/(?P[-0-9-a-f]{36})/file/history/$', FileHistory.as_view()), re_path(r'^repos/(?P[-0-9-a-f]{36})/file/revision/$', FileRevision.as_view()), + re_path(r'^repos/(?P[-0-9-a-f]{36})/file/next-revision/(?P[0-9]+)/$', FileNextRevision.as_view(), name="file-next-revision"), re_path(r'^repos/(?P[-0-9-a-f]{36})/file/revert/$', FileRevert.as_view(), name='api2-file-revert'), re_path(r'^repos/(?P[-0-9-a-f]{36})/file/shared-link/$', FileSharedLinkView.as_view()), re_path(r'^repos/(?P[-0-9-a-f]{36})/dir/$', DirView.as_view(), name='DirView'), diff --git a/seahub/api2/views.py b/seahub/api2/views.py index 29d670003e5..06b3f80102b 100644 --- a/seahub/api2/views.py +++ b/seahub/api2/views.py @@ -71,7 +71,7 @@ gen_file_share_link, gen_dir_share_link, is_org_context, gen_shared_link, \ calculate_repos_last_modify, send_perm_audit_msg, \ gen_shared_upload_link, convert_cmmt_desc_link, is_valid_dirent_name, \ - normalize_file_path, get_no_duplicate_obj_name, normalize_dir_path + normalize_file_path, get_no_duplicate_obj_name, normalize_dir_path, get_next_file_history from seahub.tags.models import FileUUIDMap from seahub.seadoc.models import SeadocHistoryName, SeadocDraft, SeadocCommentReply @@ -3365,6 +3365,37 @@ def get(self, request, repo_id, format=None): return get_repo_file(request, repo_id, obj_id, file_name, 'download') + +class FileNextRevision(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle, ) + + def get(self, request, repo_id, current_revision_id, format=None): + path = request.GET.get('p', None) + if path is None: + return api_error(status.HTTP_400_BAD_REQUEST, 'Path is missing.') + + try: + next_file_history = get_next_file_history(repo_id, path, current_revision_id) + except Exception as e: + next_file_history = {} + + commit_id = next_file_history.get('commit_id', '') + if not commit_id: + # There is no need to get the content of the commit, just return '' + return Response('') + + path = next_file_history.get('path', '') + file_name = os.path.basename(path) + try: + obj_id = seafserv_threaded_rpc.get_file_id_by_commit_and_path( + repo_id, commit_id, path) + except: + return api_error(status.HTTP_404_NOT_FOUND, 'Revision not found.') + + return get_repo_file(request, repo_id, obj_id, file_name, 'download') + class FileHistory(APIView): authentication_classes = (TokenAuthentication,) permission_classes = (IsAuthenticated,) diff --git a/seahub/seadoc/apis.py b/seahub/seadoc/apis.py index 475308de157..d33c28ca142 100644 --- a/seahub/seadoc/apis.py +++ b/seahub/seadoc/apis.py @@ -7,6 +7,8 @@ import requests import posixpath from urllib.parse import unquote +import time +from datetime import datetime, timedelta from rest_framework.response import Response from rest_framework.views import APIView @@ -36,7 +38,7 @@ from seahub.utils import get_file_type_and_ext, normalize_file_path, \ normalize_dir_path, PREVIEW_FILEEXT, get_file_history, \ gen_inner_file_get_url, gen_inner_file_upload_url, \ - get_service_url, is_valid_username, is_pro_version + get_service_url, is_valid_username, is_pro_version, get_file_history_by_day, get_file_daily_history_detail from seahub.tags.models import FileUUIDMap from seahub.utils.error_msg import file_type_error_msg from seahub.utils.repo import parse_repo_perm @@ -58,6 +60,11 @@ logger = logging.getLogger(__name__) +try: + TO_TZ = time.strftime('%z')[:3] + ':' + time.strftime('%z')[3:] +except Exception as error: + TO_TZ = '+00:00' + class SeadocAccessToken(APIView): @@ -542,19 +549,22 @@ class SeadocHistory(APIView): def _get_new_file_history_info(self, ent, avatar_size, name_dict): info = {} - creator_name = ent.op_user + creator_name = ent.get('op_user') url, is_default, date_uploaded = api_avatar_url(creator_name, avatar_size) info['creator_avatar_url'] = url info['creator_email'] = creator_name info['creator_name'] = email2nickname(creator_name) info['creator_contact_email'] = email2contact_email(creator_name) - info['ctime'] = utc_datetime_to_isoformat_timestr(ent.timestamp) - info['size'] = ent.size - info['obj_id'] = ent.file_id - info['commit_id'] = ent.commit_id - info['old_path'] = ent.old_path if hasattr(ent, 'old_path') else '' - info['path'] = ent.path - info['name'] = name_dict.get(ent.file_id, '') + info['ctime'] = utc_datetime_to_isoformat_timestr(ent.get('timestamp')) + info['size'] = ent.get('size') + info['obj_id'] = ent.get('file_id') + info['commit_id'] = ent.get('commit_id') + info['old_path'] = ent.get('old_path', '') + info['path'] = ent.get('path') + info['name'] = name_dict.get(ent.get('file_id', ''), '') + info['count'] = ent.get('count', 1) + info['date'] = ent.get('date', '') + info['id'] = ent.get('id', '') return info def get(self, request, file_uuid): @@ -609,24 +619,24 @@ def get(self, request, file_uuid): start = (page - 1) * per_page count = per_page + to_tz = request.GET.get('to_tz', TO_TZ) + try: - file_revisions, total_count = get_file_history(repo_id, path, start, count, history_limit) + file_revisions = get_file_history_by_day(repo_id, path, start, count, to_tz, history_limit) except Exception as e: logger.error(e) error_msg = 'Internal Server Error' return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) name_dict = {} - obj_id_list = [commit.file_id for commit in file_revisions] + obj_id_list = [commit.get('file_id', '') for commit in file_revisions] if obj_id_list: name_queryset = SeadocHistoryName.objects.list_by_obj_ids( doc_uuid=file_uuid, obj_id_list=obj_id_list) name_dict = {item.obj_id: item.name for item in name_queryset} data = [self._get_new_file_history_info(ent, avatar_size, name_dict) for ent in file_revisions] result = { - "histories": data, - "page": page, - "total_count": total_count + "histories": data } return Response(result) @@ -678,6 +688,92 @@ def post(self, request, file_uuid): }) +class SeadocDailyHistoryDetail(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle, ) + + def _get_new_file_history_info(self, ent, avatar_size, name_dict): + info = {} + creator_name = ent.op_user + url, is_default, date_uploaded = api_avatar_url(creator_name, avatar_size) + info['creator_avatar_url'] = url + info['creator_email'] = creator_name + info['creator_name'] = email2nickname(creator_name) + info['creator_contact_email'] = email2contact_email(creator_name) + info['ctime'] = utc_datetime_to_isoformat_timestr(ent.timestamp) + info['size'] = ent.size + info['obj_id'] = ent.file_id + info['commit_id'] = ent.commit_id + info['old_path'] = ent.old_path if hasattr(ent, 'old_path') else '' + info['path'] = ent.path + info['name'] = name_dict.get(ent.file_id, '') + info['count'] = 1 + info['id'] = ent.id + return info + + def get(self, request, file_uuid): + uuid_map = FileUUIDMap.objects.get_fileuuidmap_by_uuid(file_uuid) + if not uuid_map: + error_msg = 'seadoc uuid %s not found.' % file_uuid + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + repo_id = uuid_map.repo_id + username = request.user.username + path = posixpath.join(uuid_map.parent_path, uuid_map.filename) + + # permission check + if not check_folder_permission(request, repo_id, path): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + # resource check + 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) + + try: + avatar_size = int(request.GET.get('avatar_size', AVATAR_DEFAULT_SIZE)) + except ValueError: + avatar_size = AVATAR_DEFAULT_SIZE + + op_date = request.GET.get('op_date', None) + if not op_date: + error_msg = 'op_date invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + try: + # op_date eg: 2023-05-16T07:30:49+08:00 + op_date_format = datetime.strptime(op_date, '%Y-%m-%dT%H:%M:%S%z') + to_tz = op_date[-6:] + start_time = op_date_format.replace(hour=0, minute=0, second=0, tzinfo=None) + end_time = start_time + timedelta(days=1) + except Exception as e: + logger.error(e) + error_msg = 'op_date invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + try: + file_revisions = get_file_daily_history_detail(repo_id, path, start_time, end_time, to_tz) + except Exception as e: + logger.error(e) + error_msg = 'Internal Server Error' + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + name_dict = {} + obj_id_list = [commit.file_id for commit in file_revisions] + if obj_id_list: + name_queryset = SeadocHistoryName.objects.list_by_obj_ids( + doc_uuid=file_uuid, obj_id_list=obj_id_list) + name_dict = {item.obj_id: item.name for item in name_queryset} + data = [self._get_new_file_history_info(ent, avatar_size, name_dict) for ent in file_revisions] + result = { + "histories": data[1:] + } + return Response(result) + + class SeadocDrafts(APIView): authentication_classes = (TokenAuthentication, SessionAuthentication) diff --git a/seahub/seadoc/urls.py b/seahub/seadoc/urls.py index 487490438d9..a169234c031 100644 --- a/seahub/seadoc/urls.py +++ b/seahub/seadoc/urls.py @@ -4,7 +4,8 @@ SeadocCommentsView, SeadocCommentView, SeadocStartRevise, SeadocPublishRevision, SeadocRevisionsCount, SeadocRevisions, \ SeadocCommentRepliesView, SeadocCommentReplyView, SeadocFileView, SeadocFileUUIDView, SeadocDirView, SdocRevisionBaseVersionContent, SeadocRevisionView, \ SdocRepoTagsView, SdocRepoTagView, SdocRepoFileTagsView, SdocRepoFileTagView, SeadocNotificationsView, SeadocNotificationView, \ - SeadocFilesInfoView, DeleteSeadocOtherRevision, SeadocPublishedRevisionContent, SdocParticipantsView, SdocParticipantView, SdocRelatedUsers,SeadocEditorCallBack + SeadocFilesInfoView, DeleteSeadocOtherRevision, SeadocPublishedRevisionContent, SdocParticipantsView, SdocParticipantView, SdocRelatedUsers, SeadocEditorCallBack, \ + SeadocDailyHistoryDetail # api/v2.1/seadoc/ urlpatterns = [ @@ -18,6 +19,7 @@ re_path(r'^query-copy-move-progress/(?P[-0-9a-f]{36})/$', SeadocQueryCopyMoveProgressView.as_view(), name='seadoc_query_copy_move_progress'), re_path(r'^copy-history-file/(?P[-0-9a-f]{36})/$', SeadocCopyHistoryFile.as_view(), name='seadoc_copy_history_file'), re_path(r'^history/(?P[-0-9a-f]{36})/$', SeadocHistory.as_view(), name='seadoc_history'), + re_path(r'^daily-history-detail/(?P[-0-9a-f]{36})/$', SeadocDailyHistoryDetail.as_view(), name='seadoc_daily_history_detail'), re_path(r'^drafts/$', SeadocDrafts.as_view(), name='seadoc_drafts'), re_path(r'^mark-as-draft/(?P[-0-9a-f]{36})/$', SeadocMaskAsDraft.as_view(), name='seadoc_mark_as_draft'), re_path(r'^comments/(?P[-0-9a-f]{36})/$', SeadocCommentsView.as_view(), name='seadoc_comments'), diff --git a/seahub/utils/__init__.py b/seahub/utils/__init__.py index d9933cf6e80..b694926e977 100644 --- a/seahub/utils/__init__.py +++ b/seahub/utils/__init__.py @@ -631,6 +631,27 @@ def get_file_history(repo_id, path, start, count, history_limit=-1): res = seafevents_api.get_file_history(session, repo_id, path, start, count, history_limit) return res + def get_file_history_by_day(repo_id, path, start, count, to_tz, history_limit): + """Return file histories + """ + with _get_seafevents_session() as session: + res = seafevents_api.get_file_history_by_day(session, repo_id, path, start, count, to_tz, history_limit) + return res + + def get_file_daily_history_detail(repo_id, path, start_time, end_time, to_tz): + """Return file histories detail + """ + with _get_seafevents_session() as session: + res = seafevents_api.get_file_daily_history_detail(session, repo_id, path, start_time, end_time, to_tz) + return res + + def get_next_file_history(repo_id, path, current_revision_id): + """Return next file history + """ + with _get_seafevents_session() as session: + res = seafevents_api.get_next_file_history(session, repo_id, path, current_revision_id) + return res + def get_log_events_by_time(log_type, tstart, tend): """Return log events list by start/end timestamp. (If no logs, return 'None') """ @@ -815,6 +836,12 @@ def get_user_activities(): pass def get_file_history(): pass + def get_file_history_by_day(): + pass + def get_file_daily_history_detail(): + pass + def get_next_file_history(): + pass def generate_file_audit_event_type(): pass def get_file_audit_events_by_path():