+
{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 2a32af3390..e139ecac26 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,81 @@ class SidePanel extends Component {
super(props);
this.state = {
isLoading: true,
- historyVersions: [],
+ historyGroups: [],
errorMessage: '',
- currentPage: 1,
hasMore: false,
fileOwner: '',
isReloadingData: false,
};
+ this.currentPage = 1;
}
+ // listSdocDailyHistoryDetail
+
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);
- });
+ this.firstLoadSdocHistory();
}
- refershFileList() {
- seafileAPI.listSdocHistory(docUuid, 1, PER_PAGE).then(res => {
- this.initResultState(res.data);
- });
- }
-
- 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 month = moment(date).format('YYYY-MM');
+ const monthItem = newHistoryGroups.find(item => item.month === month);
+ if (monthItem) {
+ monthItem.children.push({ day: moment(date).format('YYYY-MM-DD'), showDaily: false, children: [ history ] });
+ } else {
+ newHistoryGroups.push({
+ month,
+ children: [
+ { day: moment(date).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 +103,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 +130,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 +142,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 +162,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 +220,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 +257,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 +272,7 @@ class SidePanel extends Component {
{this.renderHistoryVersions()}
diff --git a/seahub/api2/urls.py b/seahub/api2/urls.py
index 4f51a69b3b..3bed669238 100644
--- a/seahub/api2/urls.py
+++ b/seahub/api2/urls.py
@@ -61,6 +61,7 @@ urlpatterns = [
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 29d670003e..ab50e38e62 100644
--- a/seahub/api2/views.py
+++ b/seahub/api2/views.py
@@ -71,7 +71,7 @@ from seahub.utils import gen_file_get_url, gen_token, gen_file_upload_url, \
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,39 @@ class FileRevision(APIView):
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:
+ print('**********')
+ print(e)
+ next_file_history = {}
+
+ commit_id = next_file_history.get('commit_id', '')
+ path = next_file_history.get('path', '')
+ file_name = os.path.basename(path)
+
+ if not commit_id:
+ return Response('')
+
+ 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 475308de15..f2c88b8c73 100644
--- a/seahub/seadoc/apis.py
+++ b/seahub/seadoc/apis.py
@@ -7,6 +7,8 @@ import logging
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.file_op import if_locked_by_online_office
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 @@ from seahub.file_tags.models import FileTags
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 @@ class SeadocHistory(APIView):
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,94 @@ class SeadocHistory(APIView):
})
+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)
+
+ to_tz = request.GET.get('to_tz', TO_TZ)
+
+ 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 487490438d..a169234c03 100644
--- a/seahub/seadoc/urls.py
+++ b/seahub/seadoc/urls.py
@@ -4,7 +4,8 @@ from .apis import SeadocAccessToken, SeadocUploadLink, SeadocDownloadLink, Seado
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 @@ urlpatterns = [
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 d9933cf6e8..b694926e97 100644
--- a/seahub/utils/__init__.py
+++ b/seahub/utils/__init__.py
@@ -631,6 +631,27 @@ if EVENTS_CONFIG_FILE:
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 @@ else:
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():