From 6d0680f4b4db064c9d0710addf00a15fc575de7c Mon Sep 17 00:00:00 2001
From: awu0403 <76416779+awu0403@users.noreply.github.com>
Date: Fri, 16 Aug 2024 10:17:54 +0800
Subject: [PATCH] Wiki page trash (#6423)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* add wiki page trash
* update css
* update ui
* redesign
* update ui
* optimize code
* Update views.py
* Update models.py
* update notes
* Update mysql.sql
* change wiki trash UI
* redesign2
* update
* update
---------
Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
Co-authored-by: r350178982 <32759763+r350178982@users.noreply.github.com>
Co-authored-by: Michael An <2331806369@qq.com>
---
.../src/components/dialog/wiki-clean-trash.js | 86 ++++++
frontend/src/css/wiki-trash-dialog.css | 45 +++
frontend/src/pages/wiki2/css/wiki-nav.css | 14 +
frontend/src/pages/wiki2/index.js | 1 +
.../src/pages/wiki2/models/wiki-config.js | 10 -
frontend/src/pages/wiki2/side-panel.js | 25 +-
frontend/src/pages/wiki2/wiki-nav/wiki-nav.js | 7 +-
frontend/src/pages/wiki2/wiki-trash-dialog.js | 271 ++++++++++++++++++
frontend/src/utils/wiki-api.js | 27 ++
seahub/api2/endpoints/wiki2.py | 262 +++++++++++++----
seahub/templates/wiki/wiki_edit.html | 2 +
seahub/urls.py | 4 +-
seahub/wiki2/models.py | 26 ++
seahub/wiki2/utils.py | 54 ++++
seahub/wiki2/views.py | 24 +-
sql/mysql.sql | 15 +
16 files changed, 800 insertions(+), 73 deletions(-)
create mode 100644 frontend/src/components/dialog/wiki-clean-trash.js
create mode 100644 frontend/src/css/wiki-trash-dialog.css
create mode 100644 frontend/src/pages/wiki2/wiki-trash-dialog.js
diff --git a/frontend/src/components/dialog/wiki-clean-trash.js b/frontend/src/components/dialog/wiki-clean-trash.js
new file mode 100644
index 0000000000..1fa250f9f6
--- /dev/null
+++ b/frontend/src/components/dialog/wiki-clean-trash.js
@@ -0,0 +1,86 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
+import CreatableSelect from 'react-select/creatable';
+import { gettext } from '../../utils/constants';
+import { Utils } from '../../utils/utils';
+import toaster from '../toast';
+import wikiAPI from '../../utils/wiki-api';
+
+const propTypes = {
+ wikiId: PropTypes.string.isRequired,
+ refreshTrash: PropTypes.func.isRequired,
+ toggleDialog: PropTypes.func.isRequired
+};
+
+class WikiCleanTrash extends React.Component {
+ constructor(props) {
+ super(props);
+ this.options = [
+ { label: gettext('3 days ago'), value: 3 },
+ { label: gettext('1 week ago'), value: 7 },
+ { label: gettext('1 month ago'), value: 30 },
+ { label: gettext('all'), value: 0 }
+ ];
+ this.state = {
+ inputValue: this.options[0],
+ submitBtnDisabled: false
+ };
+ }
+
+ handleInputChange = (value) => {
+ this.setState({
+ inputValue: value
+ });
+ };
+
+ formSubmit = () => {
+ const inputValue = this.state.inputValue;
+ const { wikiId } = this.props;
+
+ this.setState({
+ submitBtnDisabled: true
+ });
+
+ wikiAPI.cleanWikiTrash(wikiId, inputValue.value).then((res) => {
+ toaster.success(gettext('Clean succeeded.'));
+ this.props.refreshTrash();
+ this.props.toggleDialog();
+ }).catch((error) => {
+ let errorMsg = Utils.getErrorMsg(error);
+ this.setState({
+ formErrorMsg: errorMsg,
+ submitBtnDisabled: false
+ });
+ });
+ };
+
+ render() {
+ const { formErrorMsg } = this.state;
+ return (
+
+ {gettext('Clean')}
+
+
+ {gettext('Clear files in trash and history:')}
+
+ {formErrorMsg && {formErrorMsg}
}
+
+
+
+
+
+
+ );
+ }
+}
+
+WikiCleanTrash.propTypes = propTypes;
+
+export default WikiCleanTrash;
diff --git a/frontend/src/css/wiki-trash-dialog.css b/frontend/src/css/wiki-trash-dialog.css
new file mode 100644
index 0000000000..a255e13524
--- /dev/null
+++ b/frontend/src/css/wiki-trash-dialog.css
@@ -0,0 +1,45 @@
+.trash-dialog {
+ max-width: 1100px;
+}
+
+.trash-dialog .modal-header {
+ align-items: center;
+ display: flex;
+}
+
+.trash-dialog .modal-header .but-contral {
+ margin-left: auto;
+}
+
+.trash-dialog .modal-header .clean {
+ height: 30px;
+ line-height: 28px;
+ padding: 0 0.5rem;
+}
+
+.trash-dialog .modal-header .trash-dialog-close-icon {
+ color: #000;
+ opacity: 0.5;
+ font-weight: 700;
+ cursor: pointer;
+}
+
+.trash-dialog .modal-header .trash-dialog-close-icon:hover {
+ opacity: 0.75;
+}
+
+.trash-dialog .modal-body {
+ height: 500px;
+ overflow-y: auto;
+}
+
+.trash-dialog .modal-body .more {
+ background: #efefef;
+ border: 0;
+ color: #777;
+}
+
+.trash-dialog .modal-body .more:hover {
+ color: #000;
+ background: #dfdfdf;
+}
diff --git a/frontend/src/pages/wiki2/css/wiki-nav.css b/frontend/src/pages/wiki2/css/wiki-nav.css
index 959dd1fb89..070a8922ac 100644
--- a/frontend/src/pages/wiki2/css/wiki-nav.css
+++ b/frontend/src/pages/wiki2/css/wiki-nav.css
@@ -279,6 +279,7 @@
color: #212529;
}
+.wiki-nav .wiki2-trash .sf3-font,
.wiki-nav .wiki-page-item .sf3-font.sf3-font-enlarge,
.wiki-nav .wiki-page-item .seafile-multicolor-icon-more-level {
color: #666;
@@ -289,3 +290,16 @@
height: 1em;
font-size: 16px;
}
+
+.wiki-nav .wiki2-trash {
+ height: 32px;
+ display: flex;
+ align-items: center;
+ padding-left: 10px;
+ margin-top: 16px;
+ cursor: pointer;
+}
+
+.wiki-nav .wiki2-trash:hover {
+ background-color: #EFEFED;
+}
diff --git a/frontend/src/pages/wiki2/index.js b/frontend/src/pages/wiki2/index.js
index f008d43b3c..a2d03dd3fd 100644
--- a/frontend/src/pages/wiki2/index.js
+++ b/frontend/src/pages/wiki2/index.js
@@ -239,6 +239,7 @@ class Wiki extends Component {
onCloseSide={this.onCloseSide}
config={this.state.config}
updateWikiConfig={this.updateWikiConfig}
+ getWikiConfig={this.getWikiConfig}
setCurrentPage={this.setCurrentPage}
currentPageId={this.state.currentPageId}
onUpdatePage={this.onUpdatePage}
diff --git a/frontend/src/pages/wiki2/models/wiki-config.js b/frontend/src/pages/wiki2/models/wiki-config.js
index ba90a0c470..a39f7db7b0 100644
--- a/frontend/src/pages/wiki2/models/wiki-config.js
+++ b/frontend/src/pages/wiki2/models/wiki-config.js
@@ -23,15 +23,5 @@ export default class WikiConfig {
}
}
traversePage({ children: this.navigation });
- for (let key in page_id_map) {
- if (page_id_map[key] === false) {
- const page = this.pages.find(item => item.id === key);
- this.navigation.push({
- id: page.id,
- type: 'page',
- children: page.children || [],
- });
- }
- }
}
}
diff --git a/frontend/src/pages/wiki2/side-panel.js b/frontend/src/pages/wiki2/side-panel.js
index fcb4ec7d49..0b1d63ac92 100644
--- a/frontend/src/pages/wiki2/side-panel.js
+++ b/frontend/src/pages/wiki2/side-panel.js
@@ -12,6 +12,7 @@ import { isObjectNotEmpty } from './utils';
import wikiAPI from '../../utils/wiki-api';
import { Utils } from '../../utils/utils';
import WikiExternalOperations from './wiki-external-operations';
+import WikiTrashDialog from './wiki-trash-dialog';
import './side-panel.css';
@@ -22,13 +23,19 @@ const propTypes = {
isLoading: PropTypes.bool.isRequired,
config: PropTypes.object.isRequired,
updateWikiConfig: PropTypes.func.isRequired,
+ getWikiConfig: PropTypes.func.isRequired,
setCurrentPage: PropTypes.func.isRequired,
currentPageId: PropTypes.string,
onUpdatePage: PropTypes.func.isRequired,
};
class SidePanel extends Component {
-
+ constructor(props) {
+ super(props);
+ this.state = {
+ isShowTrashDialog: false,
+ };
+ }
confirmDeletePage = (pageId) => {
const config = deepCopy(this.props.config);
const { pages } = config;
@@ -93,6 +100,10 @@ class SidePanel extends Component {
this.props.updateWikiConfig(config);
};
+ toggelTrashDialog = () => {
+ this.setState({ 'isShowTrashDialog': !this.state.isShowTrashDialog });
+ };
+
renderWikiNav = () => {
const { config, onUpdatePage } = this.props;
const { pages, navigation } = config;
@@ -112,6 +123,7 @@ class SidePanel extends Component {
duplicatePage={this.duplicatePage}
currentPageId={this.props.currentPageId}
addPageInside={this.addPageInside}
+ toggelTrashDialog={this.toggelTrashDialog}
/>
}
@@ -156,9 +168,16 @@ class SidePanel extends Component {
- {isLoading ? : this.renderWikiNav()}
+ {isLoading ? : this.renderWikiNav()}
-
+
+ {this.state.isShowTrashDialog && (
+
+ )}
);
}
diff --git a/frontend/src/pages/wiki2/wiki-nav/wiki-nav.js b/frontend/src/pages/wiki2/wiki-nav/wiki-nav.js
index 5f2424a123..c25f000e49 100644
--- a/frontend/src/pages/wiki2/wiki-nav/wiki-nav.js
+++ b/frontend/src/pages/wiki2/wiki-nav/wiki-nav.js
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { DropTarget, DragLayer } from 'react-dnd';
import html5DragDropContext from './html5DragDropContext';
import DraggedPageItem from './pages/dragged-page-item';
-import { repoID } from '../../../utils/constants';
+import { repoID, gettext } from '../../../utils/constants';
import '../css/wiki-nav.css';
@@ -21,6 +21,7 @@ class WikiNav extends Component {
currentPageId: PropTypes.string,
addPageInside: PropTypes.func,
updateWikiConfig: PropTypes.func.isRequired,
+ toggelTrashDialog: PropTypes.func.isRequired,
};
constructor(props) {
@@ -107,6 +108,10 @@ class WikiNav extends Component {
{navigation.map((item, index) => {
return this.renderPage(item, index, pages.length, isOnlyOnePage, id_page_map, layerDragProps);
})}
+
+
+ {gettext('Trash')}
+
);
});
diff --git a/frontend/src/pages/wiki2/wiki-trash-dialog.js b/frontend/src/pages/wiki2/wiki-trash-dialog.js
new file mode 100644
index 0000000000..7c72e9285d
--- /dev/null
+++ b/frontend/src/pages/wiki2/wiki-trash-dialog.js
@@ -0,0 +1,271 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Modal, ModalHeader, ModalBody } from 'reactstrap';
+import moment from 'moment';
+import { Utils } from '../../utils/utils';
+import { gettext, wikiId } from '../../utils/constants';
+import wikiAPI from '../../utils/wiki-api';
+import ModalPortal from '../../components/modal-portal';
+import toaster from '../../components/toast';
+import Paginator from '../../components/paginator';
+import WikiCleanTrash from '../../components/dialog/wiki-clean-trash';
+import NavItemIcon from './common/nav-item-icon';
+
+import '../../css/toolbar.css';
+import '../../css/search.css';
+import '../../css/wiki-trash-dialog.css';
+
+const propTypes = {
+ showTrashDialog: PropTypes.bool.isRequired,
+ toggleTrashDialog: PropTypes.func.isRequired,
+ getWikiConfig: PropTypes.func.isRequired
+};
+
+class WikiTrashDialog extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ isLoading: true,
+ errorMsg: '',
+ items: [],
+ isCleanTrashDialogOpen: false,
+ currentPage: 1,
+ perPage: 100,
+ hasNextPage: false
+ };
+ }
+
+ componentDidMount() {
+ this.getItems();
+ }
+
+ getItems = (page) => {
+ wikiAPI.getWikiTrash(wikiId, page, this.state.perPage).then((res) => {
+ const { items, total_count } = res.data;
+ if (!page) {
+ page = 1;
+ }
+ this.setState({
+ currentPage: page,
+ hasNextPage: total_count - page * this.state.perPage > 0,
+ isLoading: false,
+ items: items,
+ });
+ });
+ };
+
+ resetPerPage = (perPage) => {
+ this.setState({
+ perPage: perPage
+ }, () => {
+ this.getItems(1);
+ });
+ };
+ cleanTrash = () => {
+ this.toggleCleanTrashDialog();
+ };
+
+ toggleCleanTrashDialog = () => {
+ this.setState({
+ isCleanTrashDialogOpen: !this.state.isCleanTrashDialogOpen
+ });
+ };
+
+ refreshTrash = () => {
+ this.setState({
+ isLoading: true,
+ errorMsg: '',
+ items: []
+ });
+ this.getItems();
+ };
+
+ render() {
+ const { showTrashDialog, toggleTrashDialog } = this.props;
+ const { isCleanTrashDialogOpen } = this.state;
+ const { isAdmin, enableUserCleanTrash, repoName } = window.wiki.config;
+ let title = gettext('{placeholder} Wiki Trash');
+ title = title.replace('{placeholder}', '' + Utils.HTMLescape(repoName) + '');
+ return (
+
+
+
+ {(isAdmin && enableUserCleanTrash) &&
+
+ }
+
+
+ >
+ }
+ >
+
+
+
+
+ {isCleanTrashDialogOpen &&
+
+
+
+ }
+
+
+ );
+ }
+}
+
+class Content extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.theadData = [
+ { width: '3%', text: gettext('Name') },
+ { width: '20%', text: '' },
+ { width: '30%', text: gettext('Size') },
+ { width: '37%', text: gettext('Delete Time') },
+ { width: '10%', text: '' }
+ ];
+ }
+
+ getPreviousPage = () => {
+ this.props.getListByPage(this.props.currentPage - 1);
+ };
+
+ getNextPage = () => {
+ this.props.getListByPage(this.props.currentPage + 1);
+ };
+
+ render() {
+ const { items } = this.props.data;
+ const { curPerPage, currentPage, hasNextPage } = this.props;
+ return (
+
+
+
+
+ {this.theadData.map((item, index) => {
+ return {item.text} | ;
+ })}
+
+
+
+ {items.map((item, index) => {
+ return (
+
+ );
+ })}
+
+
+
+
+ );
+ }
+}
+
+Content.propTypes = {
+ data: PropTypes.object.isRequired,
+ getListByPage: PropTypes.func.isRequired,
+ resetPerPage: PropTypes.func.isRequired,
+ currentPage: PropTypes.number.isRequired,
+ curPerPage: PropTypes.number.isRequired,
+ hasNextPage: PropTypes.bool.isRequired,
+ getWikiConfig: PropTypes.func.isRequired
+
+};
+
+class Item extends React.Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ restored: false,
+ isIconShown: false,
+ getWikiConfig: PropTypes.func.isRequired
+ };
+ }
+
+ handleMouseOver = () => {
+ this.setState({ isIconShown: true });
+ };
+
+ handleMouseOut = () => {
+ this.setState({ isIconShown: false });
+ };
+
+ restoreItem = (e) => {
+ e.preventDefault();
+ const item = this.props.item;
+ wikiAPI.revertTrashPage(wikiId, item.page_id).then(res => {
+ this.setState({
+ restored: true
+ });
+ this.props.getWikiConfig();
+ toaster.success(gettext('Successfully restored 1 item.'));
+ }).catch((error) => {
+ let errorMsg = '';
+ if (error.response) {
+ errorMsg = error.response.data.error_msg || gettext('Error');
+ } else {
+ errorMsg = gettext('Please check the network.');
+ }
+ toaster.danger(errorMsg);
+ });
+ };
+
+
+ render() {
+ const item = this.props.item;
+ const { restored, isIconShown } = this.state;
+ if (restored) {
+ return null;
+ }
+ const { isAdmin } = window.wiki.config;
+ return (
+
+ |
+ {item.name} |
+ {Utils.bytesToSize(item.size)} |
+ {moment(item.deleted_time).format('YYYY-MM-DD')} |
+
+ {isAdmin &&
+ {gettext('Restore')}
+ }
+ |
+
+ );
+ }
+}
+
+Item.propTypes = {
+ item: PropTypes.object.isRequired
+};
+
+
+WikiTrashDialog.propTypes = propTypes;
+
+export default WikiTrashDialog;
diff --git a/frontend/src/utils/wiki-api.js b/frontend/src/utils/wiki-api.js
index a8ceccaf34..15ca3b5e07 100644
--- a/frontend/src/utils/wiki-api.js
+++ b/frontend/src/utils/wiki-api.js
@@ -226,6 +226,33 @@ class WikiAPI {
return this._sendPostRequest(url, form);
}
+ getWikiTrash(wikiId, page, per_page) {
+ const url = this.server + '/api/v2.1/wiki2/' + wikiId + '/trash/';
+ let params = {
+ page: page || 1,
+ per_page: per_page
+ };
+ return this.req.get(url, { params: params });
+ }
+
+ revertTrashPage(wikiId, page_id) {
+ const url = this.server + '/api/v2.1/wiki2/' + wikiId + '/trash/';
+ let params = {
+ page_id: page_id
+ };
+ return this.req.put(url, params);
+ }
+
+ cleanWikiTrash(wikiId, days) {
+ const url = this.server + '/api/v2.1/wiki2/' + wikiId + '/trash/';
+ let params = {
+ keep_days: days
+ };
+ return this.req.delete(url, {
+ data: params
+ });
+ }
+
}
let wikiAPI = new WikiAPI();
diff --git a/seahub/api2/endpoints/wiki2.py b/seahub/api2/endpoints/wiki2.py
index d222c73dfe..cf4279cfb0 100644
--- a/seahub/api2/endpoints/wiki2.py
+++ b/seahub/api2/endpoints/wiki2.py
@@ -6,9 +6,11 @@ import logging
import requests
import posixpath
import time
+import datetime
import uuid
import urllib.request, urllib.error, urllib.parse
from copy import deepcopy
+from constance import config
from rest_framework import status
from rest_framework.authentication import SessionAuthentication
@@ -24,11 +26,12 @@ from seahub.api2.throttling import UserRateThrottle
from seahub.api2.utils import api_error, to_python_boolean, is_wiki_repo
from seahub.utils.db_api import SeafileDB
from seahub.wiki2.models import Wiki2 as Wiki
+from seahub.wiki2.models import WikiPageTrash
from seahub.wiki2.utils import is_valid_wiki_name, can_edit_wiki, get_wiki_dirs_by_path, \
get_wiki_config, WIKI_PAGES_DIR, WIKI_CONFIG_PATH, WIKI_CONFIG_FILE_NAME, is_group_wiki, \
check_wiki_admin_permission, check_wiki_permission, get_all_wiki_ids, get_and_gen_page_nav_by_id, \
get_current_level_page_ids, save_wiki_config, gen_unique_id, gen_new_page_nav_by_id, pop_nav, \
- delete_page, move_nav
+ delete_page, move_nav, revert_nav, get_sub_ids_by_page_id, get_parent_id_stack
from seahub.utils import is_org_context, get_user_repos, gen_inner_file_get_url, gen_file_upload_url, \
normalize_dir_path, is_pro_version, check_filename_with_rename, is_valid_dirent_name, get_no_duplicate_obj_name
@@ -41,7 +44,7 @@ from seahub.seadoc.utils import get_seadoc_file_uuid, gen_seadoc_access_token, c
from seahub.settings import SEADOC_SERVER_URL, ENABLE_STORAGE_CLASSES, STORAGE_CLASS_MAPPING_POLICY, \
ENCRYPTED_LIBRARY_VERSION
from seahub.seadoc.sdoc_server_api import SdocServerAPI
-from seahub.utils.timeutils import timestamp_to_isoformat_timestr, datetime_to_isoformat_timestr
+from seahub.utils.timeutils import timestamp_to_isoformat_timestr
from seahub.utils.ccnet_db import CcnetDB
from seahub.tags.models import FileUUIDMap
from seahub.seadoc.models import SeadocHistoryName, SeadocDraft, SeadocCommentReply
@@ -51,6 +54,7 @@ from seahub.group.utils import group_id_to_name, is_group_admin
from seahub.utils.rpc import SeafileAPI
from seahub.constants import PERMISSION_READ_WRITE
from seaserv import ccnet_api
+from seahub.signals import clean_up_repo_trash
HTTP_520_OPERATION_FAILED = 520
@@ -163,7 +167,6 @@ class Wikis2View(APIView):
'wiki_info': group_id_wikis_map[group_obj.id]
}
group_wiki_list.append(group_wiki)
-
wiki_list = sorted(wiki_list, key=lambda x: x.get('updated_at'), reverse=True)
return Response({'wikis': wiki_list, 'group_wikis': group_wiki_list})
@@ -249,7 +252,7 @@ class Wikis2View(APIView):
logger.error(e)
msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, msg)
-
+
repo = seafile_api.get_repo(repo_id)
wiki = Wiki(repo, wiki_owner)
wiki_info = wiki.to_dict()
@@ -258,7 +261,7 @@ class Wikis2View(APIView):
else:
group_id = int(wiki.owner.split('@')[0])
wiki_info['owner_nickname'] = group_id_to_name(group_id)
-
+
return Response(wiki_info)
@@ -287,7 +290,7 @@ class Wiki2View(APIView):
repo_id = wiki.repo_id
repo = seafile_api.get_repo(repo_id)
-
+
if not repo:
error_msg = "Wiki library not found."
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
@@ -323,7 +326,7 @@ class Wiki2View(APIView):
"""Delete a wiki.
"""
username = request.user.username
-
+
wiki = Wiki.objects.get(wiki_id=wiki_id)
if not wiki:
error_msg = 'Wiki not found.'
@@ -335,7 +338,7 @@ class Wiki2View(APIView):
if not check_wiki_admin_permission(wiki, username):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
-
+
org_id = -1
if is_org_context(request):
org_id = request.user.org.org_id
@@ -394,7 +397,7 @@ class Wiki2ConfigView(APIView):
def get(self, request, wiki_id):
-
+
wiki = Wiki.objects.get(wiki_id=wiki_id)
if not wiki:
error_msg = "Wiki not found."
@@ -452,7 +455,7 @@ class Wiki2PagesView(APIView):
error_msg = 'page_name invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
-
+
wiki = Wiki.objects.get(wiki_id=wiki_id)
if not wiki:
error_msg = "Wiki not found."
@@ -545,7 +548,7 @@ class Wiki2PagesView(APIView):
return Response({'file_info': file_info})
def put(self, request, wiki_id):
-
+
wiki = Wiki.objects.get(wiki_id=wiki_id)
if not wiki:
error_msg = "Wiki not found."
@@ -611,7 +614,7 @@ class Wiki2PageView(APIView):
def get(self, request, wiki_id, page_id):
-
+
wiki = Wiki.objects.get(wiki_id=wiki_id)
if not wiki:
error_msg = "Wiki not found."
@@ -681,7 +684,7 @@ class Wiki2PageView(APIView):
})
def delete(self, request, wiki_id, page_id):
-
+
wiki = Wiki.objects.get(wiki_id=wiki_id)
if not wiki:
error_msg = "Wiki not found."
@@ -704,14 +707,13 @@ class Wiki2PageView(APIView):
wiki_config = get_wiki_config(repo_id, username)
pages = wiki_config.get('pages', [])
page_info = next(filter(lambda t: t['id'] == page_id, pages), {})
- path = page_info.get('path')
-
if not page_info:
error_msg = 'page %s not found.' % page_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# check file lock
try:
+ path = page_info.get('path')
is_locked, locked_by_me = check_file_lock(repo_id, path, username)
except Exception as e:
logger.error(e)
@@ -731,47 +733,30 @@ class Wiki2PageView(APIView):
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# update navigation and page
- pop_nav(navigation, page_id)
- id_set = get_all_wiki_ids(navigation)
- new_pages, old_pages = delete_page(pages, id_set)
- for old_page in old_pages:
- sdoc_dir_path = os.path.dirname(old_page['path'])
- parent_dir = os.path.dirname(sdoc_dir_path)
- dir_name = os.path.basename(sdoc_dir_path)
- old_page['sdoc_dir_path'] = sdoc_dir_path
- old_page['parent_dir'] = parent_dir
- old_page['dir_name'] = dir_name
-
+ stack_ids = get_parent_id_stack(navigation, page_id)
+ parent_page_id = stack_ids.pop() if stack_ids else None
+ subpages = pop_nav(navigation, page_id)
# delete the folder where the sdoc is located
try:
- for old_page in old_pages:
- seafile_api.del_file(repo_id, old_page['parent_dir'], json.dumps([old_page['dir_name']]), username)
- except SearpcError as e:
+ file_id = seafile_api.get_file_id_by_path(repo_id, page_info['path'])
+ page_size = seafile_api.get_file_size(repo.store_id, repo.version, file_id)
+ doc_uuid = os.path.basename(os.path.dirname(page_info['path']))
+ WikiPageTrash.objects.create(repo_id=repo_id,
+ doc_uuid=doc_uuid,
+ page_id=page_info['id'],
+ parent_page_id=parent_page_id,
+ subpages=json.dumps(subpages),
+ name=page_info['name'],
+ delete_time=datetime.datetime.utcnow(),
+ size=page_size)
+ except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
- try: # rm sdoc fileuuid
- for old_page in old_pages:
- file_name = os.path.basename(old_page['path'])
- file_uuid = get_seadoc_file_uuid(repo, old_page['path'])
- FileComment.objects.filter(uuid=file_uuid).delete()
- FileUUIDMap.objects.delete_fileuuidmap_by_path(repo_id, old_page['sdoc_dir_path'], file_name, is_dir=False)
- SeadocHistoryName.objects.filter(doc_uuid=file_uuid).delete()
- SeadocDraft.objects.filter(doc_uuid=file_uuid).delete()
- SeadocCommentReply.objects.filter(doc_uuid=file_uuid).delete()
- except Exception as e:
- logger.error(e)
-
# update wiki_config
try:
wiki_config['navigation'] = navigation
- wiki_config['pages'] = new_pages
- # TODO: add trash.
- if 'trash_pages' in wiki_config:
- wiki_config['trash_pages'].extend(old_pages)
- else:
- wiki_config['trash_pages'] = old_pages
wiki_config = json.dumps(wiki_config)
save_wiki_config(wiki, request.user.username, wiki_config)
except Exception as e:
@@ -794,7 +779,7 @@ class Wiki2DuplicatePageView(APIView):
error_msg = 'page_id invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
-
+
wiki = Wiki.objects.get(wiki_id=wiki_id)
if not wiki:
error_msg = "Wiki not found."
@@ -908,3 +893,184 @@ class Wiki2DuplicatePageView(APIView):
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
return Response({'wiki_config': wiki_config})
+
+
+class WikiPageTrashView(APIView):
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated,)
+ throttle_classes = (UserRateThrottle,)
+
+ def get(self, request, wiki_id):
+ wiki = Wiki.objects.get(wiki_id=wiki_id)
+ if not wiki:
+ error_msg = "Wiki not found."
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ # check argument
+ try:
+ current_page = int(request.GET.get('page', '1'))
+ per_page = int(request.GET.get('per_page', '100'))
+ except ValueError:
+ current_page = 1
+ per_page = 100
+ start = (current_page - 1) * per_page
+ end = per_page + start
+
+ # check permission
+ repo_owner = get_repo_owner(request, wiki_id)
+ wiki.owner = repo_owner
+ username = request.user.username
+ if not check_wiki_permission(wiki, username):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ # check resource
+ repo_id = wiki.repo_id
+ 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)
+
+ trash_pages = WikiPageTrash.objects.filter(repo_id=repo_id).order_by('-delete_time')
+ total_count = trash_pages.count()
+ trash_pages = trash_pages[start: end]
+ items = []
+ for item in trash_pages:
+ items.append(item.to_dict())
+
+ return Response({'items': items, 'total_count': total_count})
+
+ def put(self, request, wiki_id):
+ """revert page"""
+ page_id = request.data.get('page_id', None)
+ if not page_id:
+ error_msg = "Page not found."
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ wiki = Wiki.objects.get(wiki_id=wiki_id)
+ if not wiki:
+ error_msg = "Wiki not found."
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ repo_owner = get_repo_owner(request, wiki_id)
+ wiki.owner = repo_owner
+ username = request.user.username
+ if not check_wiki_admin_permission(wiki, username):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ repo_id = wiki.repo_id
+ 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)
+
+ # update wiki config
+ wiki_config = get_wiki_config(repo_id, username)
+ navigation = wiki_config.get('navigation', [])
+ try:
+ page = WikiPageTrash.objects.get(page_id=page_id)
+ subpages = json.loads(page.subpages)
+ parent_page_id = page.parent_page_id
+ revert_nav(navigation, parent_page_id, subpages)
+ page.delete()
+ wiki_config = json.dumps(wiki_config)
+ save_wiki_config(wiki, username, wiki_config)
+ except Exception as e:
+ logger.exception(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response({'success': True})
+
+ def delete(self, request, wiki_id):
+ """Clean Wiki Trash
+ Permission checking:
+ 1. wiki owner can perform this action.
+ 2. is group admin."""
+
+ # argument check
+ try:
+ keep_days = int(request.data.get('keep_days', 0))
+ except ValueError:
+ error_msg = 'keep_days invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ wiki = Wiki.objects.get(wiki_id=wiki_id)
+ if not wiki:
+ error_msg = "Wiki not found."
+ return api_error(status.HTTP_404_NOT_FOUND, error_msg)
+
+ # resource check
+ repo_id = wiki.repo_id
+ 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)
+
+ # permission check
+ username = request.user.username
+ repo_owner = get_repo_owner(request, repo_id)
+ wiki.owner = repo_owner
+ if not config.ENABLE_USER_CLEAN_TRASH:
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ if not check_wiki_admin_permission(wiki, username):
+ error_msg = 'Permission denied.'
+ return api_error(status.HTTP_403_FORBIDDEN, error_msg)
+
+ wiki_config = get_wiki_config(repo_id, username)
+ _timestamp = datetime.datetime.now() - datetime.timedelta(days=keep_days)
+ del_pages = WikiPageTrash.objects.filter(repo_id=repo_id, delete_time__lt=_timestamp)
+
+ navigation = wiki_config.get('navigation', [])
+ pages = wiki_config.get('pages', [])
+ id_list = []
+ for del_page in del_pages:
+ get_sub_ids_by_page_id([(json.loads(del_page.subpages))], id_list)
+ id_set = set(id_list)
+ clean_pages, not_del_pages = delete_page(pages, id_set)
+ try:
+ file_uuids = []
+ for del_page in clean_pages:
+ # rm dir
+ sdoc_dir_path = os.path.dirname(del_page['path'])
+ parent_dir = os.path.dirname(sdoc_dir_path)
+ dir_name = os.path.basename(sdoc_dir_path)
+ seafile_api.del_file(repo_id, parent_dir,
+ json.dumps([dir_name]), username)
+
+ # rm sdoc fileuuid
+ file_uuid = get_seadoc_file_uuid(repo, del_page['path'])
+ file_uuids.append(file_uuid)
+ FileComment.objects.filter(uuid__in=file_uuids).delete()
+ FileUUIDMap.objects.filter(uuid__in=file_uuids).delete()
+ SeadocHistoryName.objects.filter(doc_uuid__in=file_uuids).delete()
+ SeadocDraft.objects.filter(doc_uuid__in=file_uuids).delete()
+ SeadocCommentReply.objects.filter(doc_uuid__in=file_uuids).delete()
+ except Exception as e:
+ logger.error(e)
+
+ try:
+ seafile_api.clean_up_repo_history(repo_id, 0)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ # update wiki_config
+ try:
+ del_pages.delete()
+ wiki_config['navigation'] = navigation
+ wiki_config['pages'] = not_del_pages
+ wiki_config = json.dumps(wiki_config)
+ save_wiki_config(wiki, username, wiki_config)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response({'success': True})
+
+
diff --git a/seahub/templates/wiki/wiki_edit.html b/seahub/templates/wiki/wiki_edit.html
index 630b1787bd..71bb737121 100644
--- a/seahub/templates/wiki/wiki_edit.html
+++ b/seahub/templates/wiki/wiki_edit.html
@@ -24,11 +24,13 @@
config: {
wikiId: "{{ wiki.id }}",
repoName: "{{ wiki.name }}",
+ isAdmin: {% if is_admin %} true {% else %} false {% endif %},
initial_path: "{{ file_path|escapejs }}",
isWiki2: true,
seadocServerUrl: "{{ seadoc_server_url }}",
seadocAccessToken: "{{ seadoc_access_token }}",
permission: "{{ permission }}",
+ enableUserCleanTrash: {% if enable_user_clean_trash %} true {% else %} false {% endif %}
}
};
diff --git a/seahub/urls.py b/seahub/urls.py
index 2f2dbd15dc..33040c43df 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -205,7 +205,8 @@ from seahub.ocm.settings import OCM_ENDPOINT
from seahub.ai.apis import Search
from seahub.wiki2.views import wiki_view
-from seahub.api2.endpoints.wiki2 import Wikis2View, Wiki2View, Wiki2ConfigView, Wiki2PagesView, Wiki2PageView, Wiki2DuplicatePageView
+from seahub.api2.endpoints.wiki2 import Wikis2View, Wiki2View, Wiki2ConfigView, Wiki2PagesView, Wiki2PageView, \
+ Wiki2DuplicatePageView, WikiPageTrashView
from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView
from seahub.api2.endpoints.metadata_manage import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \
MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView
@@ -541,6 +542,7 @@ urlpatterns = [
re_path(r'^api/v2.1/wiki2/(?P[-0-9a-f]{36})/pages/$', Wiki2PagesView.as_view(), name='api-v2.1-wiki2-pages'),
re_path(r'^api/v2.1/wiki2/(?P[-0-9a-f]{36})/page/(?P[-0-9a-zA-Z]{4})/$', Wiki2PageView.as_view(), name='api-v2.1-wiki2-page'),
re_path(r'^api/v2.1/wiki2/(?P[-0-9a-f]{36})/duplicate-page/$', Wiki2DuplicatePageView.as_view(), name='api-v2.1-wiki2-duplicate-page'),
+ re_path(r'^api/v2.1/wiki2/(?P[-0-9a-f]{36})/trash/', WikiPageTrashView.as_view(), name='api-v2.1-wiki2-trash'),
## user::drafts
re_path(r'^api/v2.1/drafts/$', DraftsView.as_view(), name='api-v2.1-drafts'),
diff --git a/seahub/wiki2/models.py b/seahub/wiki2/models.py
index 26a9716d9f..b47757091d 100644
--- a/seahub/wiki2/models.py
+++ b/seahub/wiki2/models.py
@@ -45,6 +45,32 @@ class Wiki2(object):
}
+class WikiPageTrash(models.Model):
+ repo_id = models.CharField(max_length=36, db_index=True)
+ doc_uuid = models.TextField()
+ page_id = models.CharField(max_length=4)
+ parent_page_id = models.CharField(max_length=4)
+ subpages = models.TextField()
+ name = models.CharField(max_length=255)
+ delete_time = models.DateTimeField(auto_now_add=True, blank=False, null=False)
+ size = models.BigIntegerField(blank=False, null=False)
+
+ class Meta:
+ db_table = 'WikiPageTrash'
+
+ def to_dict(self):
+ return {
+ 'id': self.pk,
+ 'repo_id': self.repo_id,
+ 'doc_uuid': self.doc_uuid,
+ 'page_id': self.page_id,
+ 'parent_page_id': self.parent_page_id,
+ 'subpages': self.subpages,
+ 'name': self.name,
+ 'delete_time': self.delete_time,
+ 'size': self.size
+ }
+
###### signal handlers
from django.dispatch import receiver
from seahub.signals import repo_deleted
diff --git a/seahub/wiki2/utils.py b/seahub/wiki2/utils.py
index d76da35d3d..ac3dc3ab4b 100644
--- a/seahub/wiki2/utils.py
+++ b/seahub/wiki2/utils.py
@@ -13,6 +13,8 @@ from seaserv import seafile_api
from seahub.constants import PERMISSION_READ_WRITE
from seahub.utils import gen_inner_file_get_url, gen_file_upload_url
from seahub.group.utils import is_group_admin, is_group_member
+from seahub.wiki2.models import WikiPageTrash
+
logger = logging.getLogger(__name__)
@@ -229,6 +231,9 @@ def delete_page(pages, id_set):
new_pages.append(page)
else:
old_pages.append(page)
+ for page in pages:
+ if page['id'] in id_set:
+ pages.remove(page)
return new_pages, old_pages
@@ -264,3 +269,52 @@ def move_nav(navigation, target_id, moved_nav, move_position):
if 'children' in nav:
move_nav(nav['children'], target_id, moved_nav, move_position)
+
+def revert_nav(navigation, parent_page_id, subpages):
+
+ # connect the subpages to the parent_page
+ # if not parent_page_id marked as flag, connect the subpages to the root
+ def recurse(navigation, parent_page_id, subpages):
+ for nav in navigation:
+ if nav['id'] == parent_page_id:
+ if nav['children']:
+ nav['children'].append(subpages)
+ else:
+ nav['children'] = [subpages]
+ return nav
+ if 'children' in nav and nav['children']:
+ result = recurse(nav['children'], parent_page_id, subpages)
+ if result:
+ return result
+ flag = recurse(navigation, parent_page_id, subpages)
+ if not flag:
+ navigation.append(subpages)
+
+
+def get_sub_ids_by_page_id(subpages, ids):
+ for subpage in subpages:
+ ids.append(subpage['id'])
+ if 'children' in subpage:
+ get_sub_ids_by_page_id(subpage['children'], ids)
+
+
+def get_parent_id_stack(navigation, page_id):
+ '''
+ DFS (Depth First Search)
+ '''
+ id_list = []
+
+ def return_parent_page_id(navigation, page_id, id_list):
+ for nav in navigation:
+ id_list.append(nav['id'])
+ if nav['id'] == page_id:
+ id_list.pop()
+ return True
+ if 'children' in nav and nav['children']:
+ result = return_parent_page_id(nav['children'], page_id, id_list)
+ if result:
+ return True
+ id_list.pop()
+ return_parent_page_id(navigation, page_id, id_list)
+
+ return id_list
diff --git a/seahub/wiki2/views.py b/seahub/wiki2/views.py
index 7b98e55712..4ba5cef141 100644
--- a/seahub/wiki2/views.py
+++ b/seahub/wiki2/views.py
@@ -4,6 +4,7 @@ import logging
import posixpath
from datetime import datetime
+from constance import config
from seaserv import seafile_api
from django.http import Http404
@@ -15,7 +16,7 @@ from seahub.utils.file_types import SEADOC
from seahub.auth.decorators import login_required
from seahub.wiki2.utils import check_wiki_permission, get_wiki_config
-from seahub.utils.repo import get_repo_owner
+from seahub.utils.repo import get_repo_owner, is_repo_admin, is_repo_owner, is_group_repo_staff
from seahub.settings import SEADOC_SERVER_URL
# Get an instance of a logger
@@ -31,7 +32,7 @@ def wiki_view(request, wiki_id):
if not wiki:
raise Http404
-
+ username = request.user.username
repo_owner = get_repo_owner(request, wiki_id)
wiki.owner = repo_owner
@@ -39,7 +40,7 @@ def wiki_view(request, wiki_id):
file_path = ''
if page_id:
- wiki_config = get_wiki_config(wiki.repo_id, request.user.username)
+ wiki_config = get_wiki_config(wiki.repo_id, username)
pages = wiki_config.get('pages', [])
page_info = next(filter(lambda t: t['id'] == page_id, pages), {})
file_path = page_info.get('path', '')
@@ -49,31 +50,34 @@ def wiki_view(request, wiki_id):
is_page = True
# perm check
- req_user = request.user.username
- permission = check_wiki_permission(wiki, req_user)
- if not check_wiki_permission(wiki, req_user):
+ permission = check_wiki_permission(wiki, username)
+ if not check_wiki_permission(wiki, username):
return render_permission_error(request, 'Permission denied.')
latest_contributor = ''
last_modified = 0
file_type, ext = get_file_type_and_ext(posixpath.basename(file_path))
- repo = seafile_api.get_repo(wiki.repo_id)
+ repo_id = wiki.repo_id
+ repo = seafile_api.get_repo(repo_id)
if is_page and file_type == SEADOC:
try:
- dirent = seafile_api.get_dirent_by_path(wiki.repo_id, file_path)
+ dirent = seafile_api.get_dirent_by_path(repo_id, file_path)
if dirent:
latest_contributor, last_modified = dirent.modifier, dirent.mtime
except Exception as e:
logger.warning(e)
+
+ is_admin = is_repo_admin(username, repo_id)
last_modified = datetime.fromtimestamp(last_modified)
-
return render(request, "wiki/wiki_edit.html", {
"wiki": wiki,
+ "is_admin": is_admin,
"file_path": file_path,
"repo_name": repo.name if repo else '',
"modifier": latest_contributor,
"modify_time": last_modified,
"seadoc_server_url": SEADOC_SERVER_URL,
- "permission": permission
+ "permission": permission,
+ "enable_user_clean_trash": config.ENABLE_USER_CLEAN_TRASH
})
diff --git a/sql/mysql.sql b/sql/mysql.sql
index 0178ccf6c4..9fb97b13f5 100644
--- a/sql/mysql.sql
+++ b/sql/mysql.sql
@@ -1494,3 +1494,18 @@ CREATE TABLE IF NOT EXISTS `FileTrash` (
PRIMARY KEY (`id`),
KEY `ix_FileTrash_repo_id` (`repo_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+
+CREATE TABLE `WikiPageTrash` (
+ `id` int(11) NOT NULL AUTO_INCREMENT,
+ `repo_id` varchar(36) NOT NULL,
+ `doc_uuid` text NOT NULL,
+ `page_id` varchar(4) NOT NULL,
+ `parent_page_id` varchar(4) default NULL,
+ `subpages` longtext,
+ `name` varchar(255) NOT NULL,
+ `delete_time` datetime NOT NULL,
+ `size` bigint(20) NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `ix_WikiPageTrash_repo_id` (`repo_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
+