diff --git a/frontend/src/components/dialog/publish-wiki-dialog.js b/frontend/src/components/dialog/publish-wiki-dialog.js new file mode 100644 index 0000000000..08fb8d3c99 --- /dev/null +++ b/frontend/src/components/dialog/publish-wiki-dialog.js @@ -0,0 +1,126 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import copy from 'copy-to-clipboard'; +import { gettext, serviceURL } from '../../utils/constants'; +import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Alert, InputGroup, InputGroupAddon } from 'reactstrap'; +import toaster from '../toast'; +import wikiAPI from '../../utils/wiki-api'; + +const propTypes = { + wiki: PropTypes.object, + onPublish: PropTypes.func.isRequired, + toggleCancel: PropTypes.func.isRequired, +}; + +class PublishWikiDialog extends React.Component { + + constructor(props) { + super(props); + this.state = { + url: serviceURL + '/wiki/publish/' + this.props.customUrl, + errMessage: '', + isSubmitBtnActive: false, + }; + this.newInput = React.createRef(); + } + + handleChange = (e) => { + this.setState({ + isSubmitBtnActive: !!e.target.value.trim(), + url: e.target.value + }); + }; + + handleSubmit = () => { + let { isValid, errMessage } = this.validateInput(); + if (!isValid) { + this.setState({ + errMessage: errMessage, + url: serviceURL + '/wiki/publish/', + }); + } else { + this.props.onPublish(this.state.url.trim()); + } + }; + + deleteCustomUrl = () => { + let wiki_id = this.props.wiki.id; + wikiAPI.deletePublishWikiLink(wiki_id).then((res) => { + this.setState({ url: serviceURL + '/wiki/publish/' }); + toaster.success(gettext('Successfully.')); + }).catch((error) => { + if (error.response) { + let errorMsg = error.response.data.error_msg; + toaster.danger(errorMsg); + } + }); + }; + + + handleKeyDown = (e) => { + if (e.key === 'Enter') { + this.handleSubmit(); + } + }; + + toggle = () => { + this.props.toggleCancel(); + }; + + validateInput = () => { + let url = this.state.url.trim(); + let isValid = true; + let errMessage = ''; + if (!url) { + isValid = false; + errMessage = gettext('url is required.'); + return { isValid, errMessage }; + } + if (!(url.includes(serviceURL + '/wiki/publish/'))) { + isValid = false; + errMessage = gettext('url need include specific prefix.'); + return { isValid, errMessage }; + } + return { isValid, errMessage }; + }; + + copyLink = () => { + copy(this.state.url); + toaster.success(gettext('URL is copied to the clipboard')); + }; + + render() { + return ( + + {gettext('Publish Wiki')} + +

{gettext('Customize URL')}

+ + + + + + + + {gettext('The custom part of the URL must be between 5 and 30 characters long and may only contain letters (a-z), numbers, and hyphens.')} + + {this.state.errMessage && {this.state.errMessage}} +
+ + + + +
+ ); + } +} + +PublishWikiDialog.propTypes = propTypes; + +export default PublishWikiDialog; diff --git a/frontend/src/components/wiki-card-view/wiki-card-item.js b/frontend/src/components/wiki-card-view/wiki-card-item.js index 12524e65fe..96a1307374 100644 --- a/frontend/src/components/wiki-card-view/wiki-card-item.js +++ b/frontend/src/components/wiki-card-view/wiki-card-item.js @@ -7,6 +7,9 @@ import ModalPortal from '../modal-portal'; import DeleteWikiDialog from '../dialog/delete-wiki-dialog'; import RenameWikiDialog from '../dialog/rename-wiki-dialog'; import ShareWikiDialog from '../dialog/share-wiki-dialog'; +import PublishWikiDialog from '../dialog/publish-wiki-dialog'; +import wikiAPI from '../../utils/wiki-api'; +import toaster from '../toast'; const propTypes = { wiki: PropTypes.object.isRequired, @@ -26,6 +29,8 @@ class WikiCardItem extends Component { isShowRenameDialog: false, isItemMenuShow: false, isShowShareDialog: false, + isShowPublishDialog: false, + customUrl: '', }; } @@ -48,6 +53,10 @@ class WikiCardItem extends Component { }); }; + onPublishToggle = (e) => { + this.getPublishWikiLink(); + }; + onDeleteCancel = () => { this.setState({ isShowDeleteDialog: !this.state.isShowDeleteDialog, @@ -77,6 +86,39 @@ class WikiCardItem extends Component { this.setState({ isShowRenameDialog: false }); }; + publishWiki = (url) => { + const urlIndex = url.indexOf('/publish/'); + const publish_url = url.substring(urlIndex + '/publish/'.length); + wikiAPI.publishWiki(this.props.wiki.id, publish_url).then((res) => { + const { publish_url } = res.data; + this.setState({ customUrl: publish_url }); + toaster.success(gettext('Successfully.')); + }).catch((error) => { + if (error.response) { + let errorMsg = error.response.data.error_msg; + toaster.danger(errorMsg); + } + }); + }; + + getPublishWikiLink = () => { + wikiAPI.getPublishWikiLink(this.props.wiki.id).then((res) => { + const { publish_url } = res.data; + this.setState({ + customUrl: publish_url, + isShowPublishDialog: !this.state.isShowPublishDialog, + }); + }).catch((error) => { + this.setState({ + isShowPublishDialog: !this.state.isShowPublishDialog, + }); + if (error.response) { + let errorMsg = error.response.data.error_msg; + toaster.danger(errorMsg); + } + }); + }; + clickWikiCard = (link) => { window.open(link); }; @@ -130,6 +172,7 @@ class WikiCardItem extends Component { let showDelete = false; let showLeaveShare = false; let showDropdownMenu = false; + let showPublish = false; if (isDepartment) { if (isAdmin) { @@ -137,6 +180,7 @@ class WikiCardItem extends Component { showDelete = true; showShare = true; showRename = true; + showPublish = true; } else { showLeaveShare = true; } @@ -146,6 +190,7 @@ class WikiCardItem extends Component { showShare = true; showDelete = true; showRename = true; + showPublish = true; } else { showLeaveShare = true; } @@ -180,6 +225,8 @@ class WikiCardItem extends Component { {showRename && {gettext('Rename')}} + {showPublish && + {gettext('Publish')}} {showShare && {gettext('Share')} } @@ -200,7 +247,12 @@ class WikiCardItem extends Component {
{isShowAvatar && (isDepartment ? this.renderDept() : this.renderAvatar())}
-
{moment(wiki.updated_at).fromNow()}
+
+ {moment(wiki.updated_at).fromNow()} + {wiki.is_published && + published + } +
{this.state.isShowDeleteDialog && @@ -264,6 +316,16 @@ class WikiCardItem extends Component { /> } + {this.state.isShowPublishDialog && + + + + } ); } diff --git a/frontend/src/pages/wiki2/index.js b/frontend/src/pages/wiki2/index.js index e43760c616..86130edaf7 100644 --- a/frontend/src/pages/wiki2/index.js +++ b/frontend/src/pages/wiki2/index.js @@ -5,7 +5,7 @@ import { Modal } from 'reactstrap'; import { Utils } from '../../utils/utils'; import wikiAPI from '../../utils/wiki-api'; import SDocServerApi from '../../utils/sdoc-server-api'; -import { wikiId, siteRoot, lang, isWiki2, seadocServerUrl, gettext } from '../../utils/constants'; +import { wikiId, siteRoot, lang, isWiki2, seadocServerUrl, gettext, wikiPermission } from '../../utils/constants'; import WikiConfig from './models/wiki-config'; import toaster from '../../components/toast'; import SidePanel from './side-panel'; @@ -53,11 +53,21 @@ class Wiki extends Component { } handlePath = () => { + const custom_url = window.location.pathname.substring(1); + if (custom_url.includes('wiki/publish')) { + return custom_url; + } + return isWiki2 ? 'wikis/' : 'published/'; }; - getWikiConfig = () => { - wikiAPI.getWiki2Config(wikiId).then(res => { + let wikiAPIConfig; + if (wikiPermission === 'public') { + wikiAPIConfig = wikiAPI.getWiki2PublishConfig(wikiId); + } else { + wikiAPIConfig = wikiAPI.getWiki2Config(wikiId); + } + wikiAPIConfig.then(res => { const { wiki_config, repo_id, id: wikiRepoId } = res.data.wiki; const config = new WikiConfig(wiki_config || {}); this.setState({ @@ -137,7 +147,13 @@ class Wiki extends Component { this.setState({ isDataLoading: true, }); - wikiAPI.getWiki2Page(wikiId, pageId).then(res => { + let getWikiPage; + if (wikiPermission === 'public') { + getWikiPage = wikiAPI.getWiki2PublishPage(wikiId, pageId); + } else { + getWikiPage = wikiAPI.getWiki2Page(wikiId, pageId); + } + getWikiPage.then(res => { const { permission, seadoc_access_token, assets_url } = res.data; this.setState({ permission, @@ -154,7 +170,10 @@ class Wiki extends Component { const params = new URLSearchParams(window.location.search); params.set('page_id', pageId); - const fileUrl = `${siteRoot}${this.handlePath()}${wikiId}/?${params.toString()}`; + let fileUrl = `${siteRoot}${this.handlePath()}${wikiId}/?${params.toString()}`; + if (this.handlePath().includes('wiki/publish')) { + fileUrl = `${siteRoot}${this.handlePath()}?${params.toString()}`; + } window.history.pushState({ url: fileUrl, path: filePath }, filePath, fileUrl); }; diff --git a/frontend/src/pages/wiki2/main-panel.js b/frontend/src/pages/wiki2/main-panel.js index d178fe9fb9..e37b6e3ad9 100644 --- a/frontend/src/pages/wiki2/main-panel.js +++ b/frontend/src/pages/wiki2/main-panel.js @@ -1,7 +1,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { SdocWikiEditor } from '@seafile/sdoc-editor'; -import { gettext, username } from '../../utils/constants'; +import { gettext, username, wikiPermission } from '../../utils/constants'; import Loading from '../../components/loading'; import { Utils } from '../../utils/utils'; import Account from '../../components/common/account'; @@ -69,7 +69,7 @@ class MainPanel extends Component { currentPageId={this.props.currentPageId} currentPageConfig={currentPageConfig} /> - {username && } + {username && wikiPermission !== 'public' && }
diff --git a/frontend/src/pages/wiki2/side-panel.js b/frontend/src/pages/wiki2/side-panel.js index 0b1d63ac92..33e545e5c6 100644 --- a/frontend/src/pages/wiki2/side-panel.js +++ b/frontend/src/pages/wiki2/side-panel.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import deepCopy from 'deep-copy'; import { UncontrolledTooltip } from 'reactstrap'; -import { gettext, isWiki2, wikiId } from '../../utils/constants'; +import { gettext, isWiki2, wikiId, wikiPermission } from '../../utils/constants'; import toaster from '../../components/toast'; import Loading from '../../components/loading'; import WikiNav from './wiki-nav/index'; @@ -160,12 +160,16 @@ class SidePanel extends Component {

{repoName}

-
- -
- - {gettext('New page')} - + {wikiPermission !== 'public' && +
+
+ +
+ + {gettext('New page')} + +
+ }
{isLoading ? : this.renderWikiNav()} @@ -178,6 +182,9 @@ class SidePanel extends Component { getWikiConfig={this.props.getWikiConfig} /> )} + {wikiPermission !== 'public' && + + }
); } diff --git a/frontend/src/pages/wiki2/wiki-nav/pages/page-item.js b/frontend/src/pages/wiki2/wiki-nav/pages/page-item.js index 851bd162eb..faa90da159 100644 --- a/frontend/src/pages/wiki2/wiki-nav/pages/page-item.js +++ b/frontend/src/pages/wiki2/wiki-nav/pages/page-item.js @@ -5,7 +5,7 @@ import NameEditPopover from '../../common/name-edit-popover'; import NavItemIcon from '../../common/nav-item-icon'; import PageDropdownMenu from './page-dropdownmenu'; import DeleteDialog from '../../common/delete-dialog'; -import { gettext } from '../../../../utils/constants'; +import { gettext, wikiPermission } from '../../../../utils/constants'; import AddNewPageDialog from '../add-new-page-dialog'; import Icon from '../../../../components/icon'; import DraggedPageItem from './dragged-page-item'; @@ -225,7 +225,7 @@ class PageItem extends Component {
- {isEditMode && + {isEditMode && wikiPermission !== 'public' && <>
diff --git a/frontend/src/pages/wiki2/wiki-nav/wiki-nav.js b/frontend/src/pages/wiki2/wiki-nav/wiki-nav.js index 7987a962c4..f6f4508dce 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, gettext } from '../../../utils/constants'; +import { repoID, gettext, wikiPermission } from '../../../utils/constants'; import '../css/wiki-nav.css'; @@ -108,10 +108,12 @@ class WikiNav extends Component { {navigation.map((item, index) => { return this.renderPage(item, index, pages.length, isOnlyOnePage, id_page_map, layerDragProps); })} -
- - {gettext('Trash')} -
+ {wikiPermission !== 'public' && +
+ + {gettext('Trash')} +
+ }
); }); diff --git a/frontend/src/pages/wiki2/wiki-right-header/page-cover.js b/frontend/src/pages/wiki2/wiki-right-header/page-cover.js index 4f593bbddb..4caf260bd3 100644 --- a/frontend/src/pages/wiki2/wiki-right-header/page-cover.js +++ b/frontend/src/pages/wiki2/wiki-right-header/page-cover.js @@ -2,7 +2,7 @@ import React, { useCallback, useRef, useState } from 'react'; import { UncontrolledPopover } from 'reactstrap'; import classNames from 'classnames'; import PropTypes from 'prop-types'; -import { gettext } from '../../../utils/constants'; +import { gettext, wikiPermission } from '../../../utils/constants'; import { WIKI_COVER_LIST } from '../constant'; import './page-cover.css'; @@ -52,29 +52,33 @@ function PageCover({ currentPageConfig, onUpdatePage }) {
{gettext('Cover')}
-
{gettext('Change cover')}
+ {wikiPermission !== 'public' && +
{gettext('Change cover')}
+ }
- -
- {gettext('Gallery')} - {gettext('Remove')} -
-
- {WIKI_COVER_LIST.map(imgName => ( - {gettext('Cover')} - ))} -
-
+ {wikiPermission !== 'public' && + +
+ {gettext('Gallery')} + {gettext('Remove')} +
+
+ {WIKI_COVER_LIST.map(imgName => ( + {gettext('Cover')} + ))} +
+
+ } ); } diff --git a/frontend/src/pages/wiki2/wiki-right-header/page-icon.js b/frontend/src/pages/wiki2/wiki-right-header/page-icon.js index 2edfcd1062..abfa233305 100644 --- a/frontend/src/pages/wiki2/wiki-right-header/page-icon.js +++ b/frontend/src/pages/wiki2/wiki-right-header/page-icon.js @@ -3,7 +3,7 @@ import { UncontrolledPopover } from 'reactstrap'; import classNames from 'classnames'; import Picker from '@emoji-mart/react'; import PropTypes from 'prop-types'; -import { gettext } from '../../../utils/constants'; +import { gettext, wikiPermission } from '../../../utils/constants'; import { data } from './../utils/emoji-utils'; const PageIcon = ({ currentPageConfig, onUpdatePage }) => { @@ -27,32 +27,34 @@ const PageIcon = ({ currentPageConfig, onUpdatePage }) => { {currentPageConfig.icon}
- -
- {gettext('Emojis')} - {gettext('Remove')} -
-
- handleSetIcon(emoji.native)} - previewPosition="none" - skinTonePosition="none" - locale={window.seafile.lang.slice(0, 2)} - maxFrequentRows={2} - theme="light" - /> -
-
+ {wikiPermission !== 'public' && + +
+ {gettext('Emojis')} + {gettext('Remove')} +
+
+ handleSetIcon(emoji.native)} + previewPosition="none" + skinTonePosition="none" + locale={window.seafile.lang.slice(0, 2)} + maxFrequentRows={2} + theme="light" + /> +
+
+ } ); }; diff --git a/frontend/src/pages/wiki2/wiki-right-header/page-title-editor.js b/frontend/src/pages/wiki2/wiki-right-header/page-title-editor.js index d731c648bd..dd5a7e9a2d 100644 --- a/frontend/src/pages/wiki2/wiki-right-header/page-title-editor.js +++ b/frontend/src/pages/wiki2/wiki-right-header/page-title-editor.js @@ -1,6 +1,7 @@ import React, { useCallback, useEffect, useRef, useState } from 'react'; import PropTypes from 'prop-types'; import { throttle } from '../utils'; +import { wikiPermission } from '../../../utils/constants'; function PageTitleEditor({ isUpdateBySide, currentPageConfig, onUpdatePage }) { @@ -87,18 +88,28 @@ function PageTitleEditor({ isUpdateBySide, currentPageConfig, onUpdatePage }) { return ( -
- {pageName} +
+ {wikiPermission === 'public' ? +
+ {pageName} +
: +
+ {pageName} +
+ }
+ ); } diff --git a/frontend/src/pages/wiki2/wiki-right-header/page-title.js b/frontend/src/pages/wiki2/wiki-right-header/page-title.js index 5534fe912f..c7ec676b41 100644 --- a/frontend/src/pages/wiki2/wiki-right-header/page-title.js +++ b/frontend/src/pages/wiki2/wiki-right-header/page-title.js @@ -1,7 +1,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { gettext } from '../../../utils/constants'; +import { gettext, wikiPermission } from '../../../utils/constants'; import { WIKI_COVER_LIST } from '../constant'; import PageIcon from './page-icon'; import { generateARandomEmoji, generateEmojiIcon } from '../utils/emoji-utils'; @@ -56,13 +56,13 @@ const PageTitle = ({ isUpdateBySide, currentPageConfig, onUpdatePage }) => { )}
- {!currentPageConfig.icon && ( + {!currentPageConfig.icon && wikiPermission !== 'public' && (
{gettext('Add icon')}
)} - {!currentPageConfig.cover_img_url && ( + {!currentPageConfig.cover_img_url && wikiPermission !== 'public' && (
{gettext('Add cover')} diff --git a/frontend/src/utils/constants.js b/frontend/src/utils/constants.js index 0c1295af9f..ca3bd1d643 100644 --- a/frontend/src/utils/constants.js +++ b/frontend/src/utils/constants.js @@ -115,6 +115,7 @@ export const wikiId = window.wiki ? window.wiki.config.wikiId : ''; export const repoID = window.wiki ? window.wiki.config.repoId : ''; export const initialPath = window.wiki ? window.wiki.config.initial_path : ''; export const permission = window.wiki ? window.wiki.config.permission === 'True' : ''; +export const wikiPermission = window.wiki ? window.wiki.config.permission : ''; export const isDir = window.wiki ? window.wiki.config.isDir : ''; export const serviceUrl = window.wiki ? window.wiki.config.serviceUrl : ''; export const isPublicWiki = window.wiki ? window.wiki.config.isPublicWiki === 'True' : ''; diff --git a/frontend/src/utils/wiki-api.js b/frontend/src/utils/wiki-api.js index 15ca3b5e07..c93e02d521 100644 --- a/frontend/src/utils/wiki-api.js +++ b/frontend/src/utils/wiki-api.js @@ -176,6 +176,11 @@ class WikiAPI { return this.req.get(url); } + getWiki2PublishConfig(wikiId) { + const url = this.server + '/api/v2.1/wiki2/' + wikiId + '/publish/config/'; + return this.req.get(url); + } + createWiki2Page(wikiId, pageName, currentId) { const url = this.server + '/api/v2.1/wiki2/' + wikiId + '/pages/'; let form = new FormData(); @@ -211,6 +216,11 @@ class WikiAPI { return this.req.get(url); } + getWiki2PublishPage(wikiId, pageId) { + const url = this.server + '/api/v2.1/wiki2/' + wikiId + '/publish/page/' + pageId + '/'; + return this.req.get(url); + } + renameWiki2(wikiId, wikiName) { const url = this.server + '/api/v2.1/wiki2/' + wikiId + '/'; let params = { @@ -253,6 +263,23 @@ class WikiAPI { }); } + publishWiki(wikiId, publish_url) { + const url = this.server + '/api/v2.1/wiki2/' + wikiId + '/publish/'; + let form = new FormData(); + form.append('publish_url', publish_url); + return this._sendPostRequest(url, form); + } + + getPublishWikiLink(wikiId) { + const url = this.server + '/api/v2.1/wiki2/' + wikiId + '/publish/'; + return this.req.get(url); + } + + deletePublishWikiLink(wikiId, customUrl) { + const url = this.server + '/api/v2.1/wiki2/' + wikiId + '/publish/'; + return this.req.delete(url); + } + } let wikiAPI = new WikiAPI(); diff --git a/seahub/api2/endpoints/wiki2.py b/seahub/api2/endpoints/wiki2.py index cf4279cfb0..174e5951fa 100644 --- a/seahub/api2/endpoints/wiki2.py +++ b/seahub/api2/endpoints/wiki2.py @@ -8,6 +8,7 @@ import posixpath import time import datetime import uuid +import re import urllib.request, urllib.error, urllib.parse from copy import deepcopy from constance import config @@ -26,7 +27,7 @@ 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.models import WikiPageTrash, Wiki2Publish 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, \ @@ -36,7 +37,6 @@ from seahub.wiki2.utils import is_valid_wiki_name, can_edit_wiki, get_wiki_dirs_ 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 from seahub.views import check_folder_permission -from seahub.views.file import send_file_access_msg from seahub.base.templatetags.seahub_tags import email2nickname from seahub.utils.file_op import check_file_lock, ONLINE_OFFICE_LOCK_OWNER, if_locked_by_online_office from seahub.utils.repo import parse_repo_perm, get_repo_owner @@ -61,7 +61,8 @@ HTTP_520_OPERATION_FAILED = 520 logger = logging.getLogger(__name__) -def _merge_wiki_in_groups(group_wikis): + +def _merge_wiki_in_groups(group_wikis, publish_wiki_ids): group_ids = [gw.group_id for gw in group_wikis] group_id_wikis_map = {key: [] for key in group_ids} @@ -77,7 +78,8 @@ def _merge_wiki_in_groups(group_wikis): repo_info = { "type": "group", "permission": gw.permission, - "owner_nickname": owner_nickname + "owner_nickname": owner_nickname, + "is_published": True if wiki.repo_id in publish_wiki_ids else False } wiki_info.update(repo_info) group_id = gw.group_id @@ -105,8 +107,14 @@ class Wikis2View(APIView): user_groups = ccnet_api.get_org_groups_by_user(org_id, username, return_ancestors=True) else: user_groups = ccnet_api.get_groups(username, return_ancestors=True) - owned_wikis = [r for r in owned if is_wiki_repo(r)] + shared_wikis = [r for r in shared if is_wiki_repo(r)] + group_wikis = [r for r in groups if is_wiki_repo(r)] + wiki_ids = [w.repo_id for w in owned_wikis + shared_wikis + group_wikis] + publish_wiki_ids = [] + published_wikis = Wiki2Publish.objects.filter(repo_id__in=wiki_ids) + for w in published_wikis: + publish_wiki_ids.append(w.repo_id) wiki_list = [] for r in owned_wikis: r.owner = username @@ -116,12 +124,12 @@ class Wikis2View(APIView): repo_info = { "type": "mine", "permission": 'rw', - "owner_nickname": email2nickname(username) + "owner_nickname": email2nickname(username), + "is_published": True if wiki_info['repo_id'] in publish_wiki_ids else False } wiki_info.update(repo_info) wiki_list.append(wiki_info) - - shared_wikis = [r for r in shared if is_wiki_repo(r)] + for r in shared_wikis: owner = r.user r.owner = owner @@ -135,12 +143,12 @@ class Wikis2View(APIView): repo_info = { "type": "shared", "permission": r.permission, - "owner_nickname": owner_nickname + "owner_nickname": owner_nickname, + "is_published": True if wiki_info['repo_id'] in publish_wiki_ids else False } wiki_info.update(repo_info) wiki_list.append(wiki_info) - group_wikis = [r for r in groups if is_wiki_repo(r)] group_id_in_wikis = list(set([r.group_id for r in group_wikis])) try: group_ids_admins_map = {} @@ -157,7 +165,7 @@ class Wikis2View(APIView): r.owner = r.user group_wiki_list = [] - group_id_wikis_map = _merge_wiki_in_groups(group_wikis) + group_id_wikis_map = _merge_wiki_in_groups(group_wikis, publish_wiki_ids) for group_obj in user_wiki_groups: group_wiki = { 'group_name': group_obj.group_name, @@ -354,6 +362,8 @@ class Wiki2View(APIView): else: seafile_api.remove_repo(wiki.repo_id) + Wiki2Publish.objects.filter(repo_id=wiki.repo_id).delete() + return Response() @@ -426,6 +436,35 @@ class Wiki2ConfigView(APIView): return Response({'wiki': wiki}) + +class Wiki2PublishConfigView(APIView): + 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) + if not Wiki2Publish.objects.filter(repo_id=wiki.repo_id).exists(): + error_msg = "Wiki not found." + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + try: + repo = seafile_api.get_repo(wiki.repo_id) + if not repo: + error_msg = "Wiki library not found." + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + except SearpcError: + error_msg = _("Internal Server Error") + return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) + + wiki = wiki.to_dict() + wiki_config = get_wiki_config(repo.repo_id, '') + + wiki['wiki_config'] = wiki_config + + return Response({'wiki': wiki}) + + class Wiki2PagesView(APIView): authentication_classes = (TokenAuthentication, SessionAuthentication) permission_classes = (IsAuthenticated, ) @@ -613,8 +652,6 @@ class Wiki2PageView(APIView): throttle_classes = (UserRateThrottle, ) def get(self, request, wiki_id, page_id): - - wiki = Wiki.objects.get(wiki_id=wiki_id) if not wiki: error_msg = "Wiki not found." @@ -658,9 +695,6 @@ class Wiki2PageView(APIView): if not file_id: return api_error(status.HTTP_404_NOT_FOUND, "File not found") - # send stats message - send_file_access_msg(request, repo, path, 'api') - filename = os.path.basename(path) try: dirent = seafile_api.get_dirent_by_path(repo.repo_id, path) @@ -767,6 +801,68 @@ class Wiki2PageView(APIView): return Response({'success': True}) +class Wiki2PublishPageView(APIView): + throttle_classes = (UserRateThrottle,) + + def get(self, request, wiki_id, page_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) + + repo_id = wiki.repo_id + if not Wiki2Publish.objects.filter(repo_id=repo_id).exists(): + error_msg = "Wiki not found." + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + 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) + + wiki_config = get_wiki_config(repo_id, "") + pages = wiki_config.get('pages', []) + page_info = next(filter(lambda t: t['id'] == page_id, pages), {}) + path = page_info.get('path') + doc_uuid = page_info.get('docUuid') + + if not page_info: + error_msg = 'page %s not found.' % page_id + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + try: + file_id = seafile_api.get_file_id_by_path(repo.repo_id, path) + except SearpcError as e: + logger.error(e) + return api_error(HTTP_520_OPERATION_FAILED, + "Failed to get file id by path.") + if not file_id: + return api_error(status.HTTP_404_NOT_FOUND, "File not found") + + filename = os.path.basename(path) + try: + dirent = seafile_api.get_dirent_by_path(repo.repo_id, path) + if dirent: + latest_contributor, last_modified = dirent.modifier, dirent.mtime + else: + latest_contributor, last_modified = None, 0 + except SearpcError as e: + logger.error(e) + latest_contributor, last_modified = None, 0 + + assets_url = '/api/v2.1/seadoc/download-image/' + doc_uuid + seadoc_access_token = gen_seadoc_access_token(doc_uuid, filename, request.user.username, permission='r', + default_title='') + + return Response({ + "latest_contributor": email2nickname(latest_contributor), + "last_modified": last_modified, + "permission": 'r', + "seadoc_access_token": seadoc_access_token, + "assets_url": assets_url, + }) + class Wiki2DuplicatePageView(APIView): authentication_classes = (TokenAuthentication, SessionAuthentication) permission_classes = (IsAuthenticated, ) @@ -1073,4 +1169,102 @@ class WikiPageTrashView(APIView): return Response({'success': True}) +class Wiki2PublishView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated,) + throttle_classes = (UserRateThrottle,) + def _check_custom_url(self, publish_url): + return True if re.search(r'^[-0-9a-zA-Z]+$', publish_url) else False + + 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) + username = request.user.username + repo_owner = get_repo_owner(request, wiki_id) + wiki.owner = repo_owner + if not check_wiki_admin_permission(wiki, username): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + try: + publish_config = Wiki2Publish.objects.get(repo_id=wiki.repo_id) + publish_url = publish_config.publish_url + creator = publish_config.username + created_at = publish_config.created_at + visit_count = publish_config.visit_count + except Wiki2Publish.DoesNotExist: + publish_url = '' + creator = '' + created_at = '' + visit_count = 0 + publish_info = { + 'publish_url': publish_url, + 'creator': creator, + 'created_at': created_at, + 'visit_count': visit_count + } + return Response(publish_info) + + def post(self, request, wiki_id): + publish_url = request.data.get('publish_url', None) + if not publish_url: + error_msg = 'wiki custom url invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + publish_url = publish_url.strip() + if not self._check_custom_url(publish_url): + error_msg = _('URL is invalid') + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + if len(publish_url) < 5 or len(publish_url) > 30: + error_msg = _('The custom part of URL should have 5-30 characters.') + 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) + + # check permission + 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) + wiki_pub = Wiki2Publish.objects.filter(publish_url=publish_url).first() + if wiki_pub: + if wiki_pub.repo_id != wiki_id: + error_msg = _('This custom domain is already in use and cannot be used for your wiki') + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + return Response({"publish_url": publish_url}) + + wiki_publish = Wiki2Publish.objects.filter(repo_id=wiki.repo_id).first() + if not wiki_publish: + wiki_publish = Wiki2Publish(repo_id=wiki.repo_id, username=username, publish_url=publish_url) + else: + wiki_publish.publish_url = publish_url + wiki_publish.save() + + return Response({"publish_url": publish_url}) + + def delete(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) + + 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) + + publish_config = Wiki2Publish.objects.filter(repo_id=wiki.repo_id).first() + if publish_config: + publish_config.delete() + return Response({'success': True}) diff --git a/seahub/seadoc/apis.py b/seahub/seadoc/apis.py index 04b81cc495..bd0e2dbed3 100644 --- a/seahub/seadoc/apis.py +++ b/seahub/seadoc/apis.py @@ -61,6 +61,7 @@ from seahub.base.accounts import User from seahub.avatar.settings import AVATAR_DEFAULT_SIZE from seahub.repo_tags.models import RepoTags from seahub.file_tags.models import FileTags +from seahub.wiki2.models import Wiki2Publish if HAS_FILE_SEARCH or HAS_FILE_SEASEARCH: from seahub.search.utils import search_files, ai_search_files, format_repos @@ -410,11 +411,13 @@ class SeadocDownloadImage(APIView): repo_id = uuid_map.repo_id username = request.user.username + wiki_publish = Wiki2Publish.objects.filter(repo_id=repo_id).first() # permission check - file_path = posixpath.join(uuid_map.parent_path, uuid_map.filename) - if not can_access_seadoc_asset(request, repo_id, file_path, file_uuid): - error_msg = 'Permission denied.' - return api_error(status.HTTP_403_FORBIDDEN, error_msg) + if not wiki_publish: + file_path = posixpath.join(uuid_map.parent_path, uuid_map.filename) + if not can_access_seadoc_asset(request, repo_id, file_path, file_uuid): + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) # main parent_path = gen_seadoc_image_parent_path(file_uuid, repo_id, username) diff --git a/seahub/templates/wiki/wiki_publish.html b/seahub/templates/wiki/wiki_publish.html new file mode 100644 index 0000000000..5ad0103854 --- /dev/null +++ b/seahub/templates/wiki/wiki_publish.html @@ -0,0 +1,35 @@ +{% extends "base_for_react.html" %} +{% load i18n %} +{% load render_bundle from webpack_loader %} +{% load seahub_tags %} +{% block extra_ogp_tags %} + + + + + + +{% endblock %} +{% block extra_style %} + + +{% render_bundle 'wiki2' 'css' %} +{% endblock %} + +{% block wiki_title %}{{repo_name}}{% endblock %} + +{% block extra_script %} + +{% render_bundle 'wiki2' 'js' %} +{% endblock %} diff --git a/seahub/urls.py b/seahub/urls.py index 775ef18cd2..fbd39ff45b 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -206,10 +206,9 @@ from seahub.api2.endpoints.repo_related_users import RepoRelatedUsersView from seahub.api2.endpoints.repo_auto_delete import RepoAutoDeleteView from seahub.seadoc.views import sdoc_revision, sdoc_revisions, sdoc_to_docx from seahub.ocm.settings import OCM_ENDPOINT - -from seahub.wiki2.views import wiki_view +from seahub.wiki2.views import wiki_view, wiki_publish_view from seahub.api2.endpoints.wiki2 import Wikis2View, Wiki2View, Wiki2ConfigView, Wiki2PagesView, Wiki2PageView, \ - Wiki2DuplicatePageView, WikiPageTrashView + Wiki2DuplicatePageView, WikiPageTrashView, Wiki2PublishView, Wiki2PublishConfigView, Wiki2PublishPageView from seahub.api2.endpoints.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView from seahub.api2.endpoints.metadata_manage import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \ MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView @@ -546,11 +545,13 @@ urlpatterns = [ re_path(r'^api/v2.1/wikis2/$', Wikis2View.as_view(), name='api-v2.1-wikis2'), re_path(r'^api/v2.1/wiki2/(?P[-0-9a-f]{36})/$', Wiki2View.as_view(), name='api-v2.1-wiki2'), re_path(r'^api/v2.1/wiki2/(?P[-0-9a-f]{36})/config/$', Wiki2ConfigView.as_view(), name='api-v2.1-wiki2-config'), + re_path(r'^api/v2.1/wiki2/(?P[-0-9a-f]{36})/publish/config/$', Wiki2PublishConfigView.as_view(), name='api-v2.1-wiki2-publish-config'), 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})/publish/page/(?P[-0-9a-zA-Z]{4})/$', Wiki2PublishPageView.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'), - + re_path(r'^api/v2.1/wiki2/(?P[-0-9a-f]{36})/publish/$', Wiki2PublishView.as_view(), name='api-v2.1-wiki2-publish'), ## user::drafts re_path(r'^api/v2.1/drafts/$', DraftsView.as_view(), name='api-v2.1-drafts'), re_path(r'^api/v2.1/drafts/(?P\d+)/$', DraftView.as_view(), name='api-v2.1-draft'), @@ -737,6 +738,7 @@ urlpatterns = [ re_path(r'^api/v2.1/admin/invitations/(?P[a-f0-9]{32})/$', AdminInvitation.as_view(), name='api-v2.1-admin-invitation'), re_path(r'^wikis/(?P[^/]+)/$', wiki_view, name='wiki'), + re_path(r'^wiki/publish/(?P[-0-9a-zA-Z]+)/$', wiki_publish_view, name='wiki-publish'), path('avatar/', include('seahub.avatar.urls')), path('group/', include('seahub.group.urls')), diff --git a/seahub/wiki2/models.py b/seahub/wiki2/models.py index b47757091d..e731500783 100644 --- a/seahub/wiki2/models.py +++ b/seahub/wiki2/models.py @@ -33,7 +33,6 @@ class Wiki2(object): self.name = wiki.repo_name self.updated_at = timestamp_to_isoformat_timestr(wiki.last_modify) self.repo_id = wiki.repo_id - def to_dict(self): return { @@ -71,6 +70,25 @@ class WikiPageTrash(models.Model): 'size': self.size } +class Wiki2Publish(models.Model): + repo_id = models.CharField(max_length=36, unique=True, db_index=True) + publish_url = models.CharField(max_length=40, null=True, unique=True) + username = models.CharField(max_length=255) + created_at = models.DateTimeField(auto_now_add=True) + visit_count = models.IntegerField(default=0) + + class Meta: + db_table = 'wiki_wiki2_publish' + + def to_dict(self): + return { + 'repo_id': self.repo_id, + 'publish_url': self.publish_url, + 'username': self.username, + 'created_at': self.created_at, + 'visit_count': self.visit_count + } + ###### signal handlers from django.dispatch import receiver from seahub.signals import repo_deleted diff --git a/seahub/wiki2/views.py b/seahub/wiki2/views.py index 4ba5cef141..763230c766 100644 --- a/seahub/wiki2/views.py +++ b/seahub/wiki2/views.py @@ -11,6 +11,7 @@ from django.http import Http404 from django.shortcuts import render from seahub.wiki2.models import Wiki2 as Wiki +from seahub.wiki2.models import Wiki2Publish from seahub.utils import get_file_type_and_ext, render_permission_error from seahub.utils.file_types import SEADOC from seahub.auth.decorators import login_required @@ -81,3 +82,66 @@ def wiki_view(request, wiki_id): "permission": permission, "enable_user_clean_trash": config.ENABLE_USER_CLEAN_TRASH }) + + +def wiki_publish_view(request, publish_url): + """ view wiki page. for wiki2 + 1 permission + All user + """ + # get wiki_publish object or 404 + wiki_publish = Wiki2Publish.objects.filter(publish_url=publish_url).first() + if not wiki_publish: + raise Http404 + + wiki_id = wiki_publish.repo_id + wiki = Wiki.objects.get(wiki_id=wiki_id) + if not wiki: + raise Http404 + + repo_owner = get_repo_owner(request, wiki_id) + wiki.owner = repo_owner + + page_id = request.GET.get('page_id') + file_path = '' + + if page_id: + wiki_config = get_wiki_config(wiki.repo_id, request.user.username) + pages = wiki_config.get('pages', []) + page_info = next(filter(lambda t: t['id'] == page_id, pages), {}) + file_path = page_info.get('path', '') + + is_page = False + if file_path: + is_page = True + + 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) + if is_page and file_type == SEADOC: + try: + dirent = seafile_api.get_dirent_by_path(wiki.repo_id, file_path) + if dirent: + latest_contributor, last_modified = dirent.modifier, dirent.mtime + except Exception as e: + logger.warning(e) + + last_modified = datetime.fromtimestamp(last_modified) + # update visit_count + try: + current_count = wiki_publish.visit_count + wiki_publish.visit_count = current_count + 1 + wiki_publish.save() + except Exception as e: + logger.warning(e) + + return render(request, "wiki/wiki_publish.html", { + "wiki": wiki, + "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": 'public' + }) diff --git a/sql/mysql.sql b/sql/mysql.sql index 98b7a9788c..c5bdece528 100644 --- a/sql/mysql.sql +++ b/sql/mysql.sql @@ -1557,3 +1557,16 @@ CREATE TABLE `sdoc_operation_log` ( KEY `sdoc_operation_log_doc_uuid` (`doc_uuid`), KEY `sdoc_idx_operation_log_doc_uuid_op_id` (`doc_uuid`,`op_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; + +CREATE TABLE IF NOT EXISTS `wiki_wiki2_publish` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `repo_id` varchar(36) NOT NULL, + `publish_url` varchar(40) DEFAULT NULL, + `username` varchar(255) NOT NULL, + `created_at` timestamp NOT NULL DEFAULT current_timestamp(), + `visit_count` int(11) NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + UNIQUE KEY `repo_id` (`repo_id`), + UNIQUE KEY `publish_url` (`publish_url`), + KEY `ix_wiki2_publish_repo_id` (`repo_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;