1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-30 21:50:59 +00:00

Wiki publish (#6512)

* add wiki publish

publish wiki permission

optimize code

* optimize code

* optimize code

* update

* Update wiki2.py

* Update wiki2.py

* Update urls.py

* Update wiki_publish.html

---------

Co-authored-by: 孙永强 <11704063+s-yongqiang@user.noreply.gitee.com>
Co-authored-by: r350178982 <32759763+r350178982@users.noreply.github.com>
This commit is contained in:
awu0403 2024-09-12 14:32:00 +08:00 committed by GitHub
parent de576a7237
commit ac37e9dc31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 700 additions and 110 deletions

View File

@ -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 (
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('Publish Wiki')}</ModalHeader>
<ModalBody>
<p>{gettext('Customize URL')}</p>
<InputGroup>
<Input
onKeyDown={this.handleKeyDown}
innerRef={this.newInput}
placeholder="customize url"
value={this.state.url}
onChange={this.handleChange}
/>
<InputGroupAddon addonType="append">
<Button color="primary" onClick={this.copyLink} className="border-0">{gettext('Copy')}</Button>
</InputGroupAddon>
</InputGroup>
<span className='tip mb-1' style={{ fontSize: '14px' }}>
{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.')}
</span>
{this.state.errMessage && <Alert color="danger" className="mt-2">{this.state.errMessage}</Alert>}
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.deleteCustomUrl} disabled={this.props.customUrl === ''}>{gettext('Delete')}</Button>
<Button color="primary" onClick={this.handleSubmit} disabled={!this.state.isSubmitBtnActive}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}
PublishWikiDialog.propTypes = propTypes;
export default PublishWikiDialog;

View File

@ -7,6 +7,9 @@ import ModalPortal from '../modal-portal';
import DeleteWikiDialog from '../dialog/delete-wiki-dialog'; import DeleteWikiDialog from '../dialog/delete-wiki-dialog';
import RenameWikiDialog from '../dialog/rename-wiki-dialog'; import RenameWikiDialog from '../dialog/rename-wiki-dialog';
import ShareWikiDialog from '../dialog/share-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 = { const propTypes = {
wiki: PropTypes.object.isRequired, wiki: PropTypes.object.isRequired,
@ -26,6 +29,8 @@ class WikiCardItem extends Component {
isShowRenameDialog: false, isShowRenameDialog: false,
isItemMenuShow: false, isItemMenuShow: false,
isShowShareDialog: false, isShowShareDialog: false,
isShowPublishDialog: false,
customUrl: '',
}; };
} }
@ -48,6 +53,10 @@ class WikiCardItem extends Component {
}); });
}; };
onPublishToggle = (e) => {
this.getPublishWikiLink();
};
onDeleteCancel = () => { onDeleteCancel = () => {
this.setState({ this.setState({
isShowDeleteDialog: !this.state.isShowDeleteDialog, isShowDeleteDialog: !this.state.isShowDeleteDialog,
@ -77,6 +86,39 @@ class WikiCardItem extends Component {
this.setState({ isShowRenameDialog: false }); 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) => { clickWikiCard = (link) => {
window.open(link); window.open(link);
}; };
@ -130,6 +172,7 @@ class WikiCardItem extends Component {
let showDelete = false; let showDelete = false;
let showLeaveShare = false; let showLeaveShare = false;
let showDropdownMenu = false; let showDropdownMenu = false;
let showPublish = false;
if (isDepartment) { if (isDepartment) {
if (isAdmin) { if (isAdmin) {
@ -137,6 +180,7 @@ class WikiCardItem extends Component {
showDelete = true; showDelete = true;
showShare = true; showShare = true;
showRename = true; showRename = true;
showPublish = true;
} else { } else {
showLeaveShare = true; showLeaveShare = true;
} }
@ -146,6 +190,7 @@ class WikiCardItem extends Component {
showShare = true; showShare = true;
showDelete = true; showDelete = true;
showRename = true; showRename = true;
showPublish = true;
} else { } else {
showLeaveShare = true; showLeaveShare = true;
} }
@ -180,6 +225,8 @@ class WikiCardItem extends Component {
<DropdownMenu right={true} className="dtable-dropdown-menu"> <DropdownMenu right={true} className="dtable-dropdown-menu">
{showRename && {showRename &&
<DropdownItem onClick={this.onRenameToggle}>{gettext('Rename')}</DropdownItem>} <DropdownItem onClick={this.onRenameToggle}>{gettext('Rename')}</DropdownItem>}
{showPublish &&
<DropdownItem onClick={this.onPublishToggle}>{gettext('Publish')}</DropdownItem>}
{showShare && {showShare &&
<DropdownItem onClick={this.onShareToggle}>{gettext('Share')}</DropdownItem> <DropdownItem onClick={this.onShareToggle}>{gettext('Share')}</DropdownItem>
} }
@ -200,7 +247,12 @@ class WikiCardItem extends Component {
<div className="wiki-item-owner"> <div className="wiki-item-owner">
{isShowAvatar && (isDepartment ? this.renderDept() : this.renderAvatar())} {isShowAvatar && (isDepartment ? this.renderDept() : this.renderAvatar())}
</div> </div>
<div className="wiki-item-updated-time">{moment(wiki.updated_at).fromNow()}</div> <div className="wiki-item-updated-time">
{moment(wiki.updated_at).fromNow()}
{wiki.is_published &&
<span style={{ marginLeft: '25%' }}>published</span>
}
</div>
</div> </div>
{this.state.isShowDeleteDialog && {this.state.isShowDeleteDialog &&
<ModalPortal> <ModalPortal>
@ -264,6 +316,16 @@ class WikiCardItem extends Component {
/> />
</ModalPortal> </ModalPortal>
} }
{this.state.isShowPublishDialog &&
<ModalPortal>
<PublishWikiDialog
toggleCancel={this.onPublishToggle}
onPublish={this.publishWiki}
wiki={wiki}
customUrl={this.state.customUrl}
/>
</ModalPortal>
}
</> </>
); );
} }

View File

@ -5,7 +5,7 @@ import { Modal } from 'reactstrap';
import { Utils } from '../../utils/utils'; import { Utils } from '../../utils/utils';
import wikiAPI from '../../utils/wiki-api'; import wikiAPI from '../../utils/wiki-api';
import SDocServerApi from '../../utils/sdoc-server-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 WikiConfig from './models/wiki-config';
import toaster from '../../components/toast'; import toaster from '../../components/toast';
import SidePanel from './side-panel'; import SidePanel from './side-panel';
@ -53,11 +53,21 @@ class Wiki extends Component {
} }
handlePath = () => { handlePath = () => {
const custom_url = window.location.pathname.substring(1);
if (custom_url.includes('wiki/publish')) {
return custom_url;
}
return isWiki2 ? 'wikis/' : 'published/'; return isWiki2 ? 'wikis/' : 'published/';
}; };
getWikiConfig = () => { 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 { wiki_config, repo_id, id: wikiRepoId } = res.data.wiki;
const config = new WikiConfig(wiki_config || {}); const config = new WikiConfig(wiki_config || {});
this.setState({ this.setState({
@ -137,7 +147,13 @@ class Wiki extends Component {
this.setState({ this.setState({
isDataLoading: true, 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; const { permission, seadoc_access_token, assets_url } = res.data;
this.setState({ this.setState({
permission, permission,
@ -154,7 +170,10 @@ class Wiki extends Component {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
params.set('page_id', pageId); 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); window.history.pushState({ url: fileUrl, path: filePath }, filePath, fileUrl);
}; };

View File

@ -1,7 +1,7 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { SdocWikiEditor } from '@seafile/sdoc-editor'; 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 Loading from '../../components/loading';
import { Utils } from '../../utils/utils'; import { Utils } from '../../utils/utils';
import Account from '../../components/common/account'; import Account from '../../components/common/account';
@ -69,7 +69,7 @@ class MainPanel extends Component {
currentPageId={this.props.currentPageId} currentPageId={this.props.currentPageId}
currentPageConfig={currentPageConfig} currentPageConfig={currentPageConfig}
/> />
{username && <Account />} {username && wikiPermission !== 'public' && <Account />}
</div> </div>
<div className="main-panel-center"> <div className="main-panel-center">
<div className={`cur-view-content ${isViewingFile ? 'o-hidden' : ''}`}> <div className={`cur-view-content ${isViewingFile ? 'o-hidden' : ''}`}>

View File

@ -2,7 +2,7 @@ import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import deepCopy from 'deep-copy'; import deepCopy from 'deep-copy';
import { UncontrolledTooltip } from 'reactstrap'; 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 toaster from '../../components/toast';
import Loading from '../../components/loading'; import Loading from '../../components/loading';
import WikiNav from './wiki-nav/index'; import WikiNav from './wiki-nav/index';
@ -160,12 +160,16 @@ class SidePanel extends Component {
<div className={`wiki2-side-panel${this.props.closeSideBar ? '' : ' left-zero'}`}> <div className={`wiki2-side-panel${this.props.closeSideBar ? '' : ' left-zero'}`}>
<div className="wiki2-side-panel-top"> <div className="wiki2-side-panel-top">
<h4 className="text-truncate ml-0 mb-0" title={repoName}>{repoName}</h4> <h4 className="text-truncate ml-0 mb-0" title={repoName}>{repoName}</h4>
<div id='wiki-add-new-page' className='add-new-page' onClick={this.handleAddNewPage.bind(true)}> {wikiPermission !== 'public' &&
<i className='sf3-font sf3-font-new-page'></i> <div>
</div> <div id='wiki-add-new-page' className='add-new-page' onClick={this.handleAddNewPage.bind(true)}>
<UncontrolledTooltip className='wiki-new-page-tooltip' target="wiki-add-new-page"> <i className='sf3-font sf3-font-new-page'></i>
{gettext('New page')} </div>
</UncontrolledTooltip> <UncontrolledTooltip className='wiki-new-page-tooltip' target="wiki-add-new-page">
{gettext('New page')}
</UncontrolledTooltip>
</div>
}
</div> </div>
<div className="wiki2-side-nav"> <div className="wiki2-side-nav">
{isLoading ? <Loading/> : this.renderWikiNav()} {isLoading ? <Loading/> : this.renderWikiNav()}
@ -178,6 +182,9 @@ class SidePanel extends Component {
getWikiConfig={this.props.getWikiConfig} getWikiConfig={this.props.getWikiConfig}
/> />
)} )}
{wikiPermission !== 'public' &&
<WikiExternalOperations onAddWikiPage={this.handleAddNewPage.bind(false)} />
}
</div> </div>
); );
} }

View File

@ -5,7 +5,7 @@ import NameEditPopover from '../../common/name-edit-popover';
import NavItemIcon from '../../common/nav-item-icon'; import NavItemIcon from '../../common/nav-item-icon';
import PageDropdownMenu from './page-dropdownmenu'; import PageDropdownMenu from './page-dropdownmenu';
import DeleteDialog from '../../common/delete-dialog'; 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 AddNewPageDialog from '../add-new-page-dialog';
import Icon from '../../../../components/icon'; import Icon from '../../../../components/icon';
import DraggedPageItem from './dragged-page-item'; import DraggedPageItem from './dragged-page-item';
@ -225,7 +225,7 @@ class PageItem extends Component {
</div> </div>
</div> </div>
<div className="d-flex"> <div className="d-flex">
{isEditMode && {isEditMode && wikiPermission !== 'public' &&
<> <>
<div className="more-wiki-page-operation" onClick={this.toggleDropdown}> <div className="more-wiki-page-operation" onClick={this.toggleDropdown}>
<Icon symbol={'more-level'} /> <Icon symbol={'more-level'} />

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { DropTarget, DragLayer } from 'react-dnd'; import { DropTarget, DragLayer } from 'react-dnd';
import html5DragDropContext from './html5DragDropContext'; import html5DragDropContext from './html5DragDropContext';
import DraggedPageItem from './pages/dragged-page-item'; 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'; import '../css/wiki-nav.css';
@ -108,10 +108,12 @@ class WikiNav extends Component {
{navigation.map((item, index) => { {navigation.map((item, index) => {
return this.renderPage(item, index, pages.length, isOnlyOnePage, id_page_map, layerDragProps); return this.renderPage(item, index, pages.length, isOnlyOnePage, id_page_map, layerDragProps);
})} })}
<div className='wiki2-trash' onClick={this.props.toggelTrashDialog}> {wikiPermission !== 'public' &&
<span className="sf3-font-trash sf3-font mr-2"></span> <div className='wiki2-trash' onClick={this.props.toggelTrashDialog}>
<span>{gettext('Trash')}</span> <span className="sf3-font-recycle1 sf3-font mr-2"></span>
</div> <span>{gettext('Trash')}</span>
</div>
}
</div> </div>
); );
}); });

View File

@ -2,7 +2,7 @@ import React, { useCallback, useRef, useState } from 'react';
import { UncontrolledPopover } from 'reactstrap'; import { UncontrolledPopover } from 'reactstrap';
import classNames from 'classnames'; import classNames from 'classnames';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { gettext } from '../../../utils/constants'; import { gettext, wikiPermission } from '../../../utils/constants';
import { WIKI_COVER_LIST } from '../constant'; import { WIKI_COVER_LIST } from '../constant';
import './page-cover.css'; import './page-cover.css';
@ -52,29 +52,33 @@ function PageCover({ currentPageConfig, onUpdatePage }) {
<div id="wiki-page-cover" className='wiki-page-cover' onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}> <div id="wiki-page-cover" className='wiki-page-cover' onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<img className='wiki-page-cover__img' alt={gettext('Cover')} src={getCoverImgUrl(currentPageConfig.cover_img_url)} /> <img className='wiki-page-cover__img' alt={gettext('Cover')} src={getCoverImgUrl(currentPageConfig.cover_img_url)} />
<div className={classNames('wiki-page-cover__controller', { show: isShowCoverController })}> <div className={classNames('wiki-page-cover__controller', { show: isShowCoverController })}>
<div className='wiki-cover-controller-btn' id='wiki-change-cover-btn'>{gettext('Change cover')}</div> {wikiPermission !== 'public' &&
<div className='wiki-cover-controller-btn' id='wiki-change-cover-btn'>{gettext('Change cover')}</div>
}
</div> </div>
</div> </div>
<UncontrolledPopover {wikiPermission !== 'public' &&
ref={popoverRef} <UncontrolledPopover
flip ref={popoverRef}
target="wiki-change-cover-btn" flip
placement="bottom" target="wiki-change-cover-btn"
hideArrow={true} placement="bottom"
popperClassName='wiki-page-cover-popover' hideArrow={true}
innerClassName='wiki-page-cover-panel wiki-page-panel' popperClassName='wiki-page-cover-popover'
trigger="legacy" innerClassName='wiki-page-cover-panel wiki-page-panel'
> trigger="legacy"
<div className='wiki-page-cover-panel__header popover-header'> >
<span>{gettext('Gallery')}</span> <div className='wiki-page-cover-panel__header popover-header'>
<span onClick={removeCoverImage} className='wiki-remove-icon-btn'>{gettext('Remove')}</span> <span>{gettext('Gallery')}</span>
</div> <span onClick={removeCoverImage} className='wiki-remove-icon-btn'>{gettext('Remove')}</span>
<div className='wiki-page-cover-panel__body popover-body'> </div>
{WIKI_COVER_LIST.map(imgName => ( <div className='wiki-page-cover-panel__body popover-body'>
<img key={imgName} onClick={updateCoverImage.bind(null, imgName)} className='wiki-cover-gallery-img' alt={gettext('Cover')} src={getCoverImgUrl(`${imgName}`)} /> {WIKI_COVER_LIST.map(imgName => (
))} <img key={imgName} onClick={updateCoverImage.bind(null, imgName)} className='wiki-cover-gallery-img' alt={gettext('Cover')} src={getCoverImgUrl(`${imgName}`)} />
</div> ))}
</UncontrolledPopover> </div>
</UncontrolledPopover>
}
</> </>
); );
} }

View File

@ -3,7 +3,7 @@ import { UncontrolledPopover } from 'reactstrap';
import classNames from 'classnames'; import classNames from 'classnames';
import Picker from '@emoji-mart/react'; import Picker from '@emoji-mart/react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { gettext } from '../../../utils/constants'; import { gettext, wikiPermission } from '../../../utils/constants';
import { data } from './../utils/emoji-utils'; import { data } from './../utils/emoji-utils';
const PageIcon = ({ currentPageConfig, onUpdatePage }) => { const PageIcon = ({ currentPageConfig, onUpdatePage }) => {
@ -27,32 +27,34 @@ const PageIcon = ({ currentPageConfig, onUpdatePage }) => {
<span>{currentPageConfig.icon}</span> <span>{currentPageConfig.icon}</span>
</div> </div>
</div> </div>
<UncontrolledPopover {wikiPermission !== 'public' &&
ref={popoverRef} <UncontrolledPopover
flip ref={popoverRef}
target="wiki-page-icon-container" flip
placement="bottom" target="wiki-page-icon-container"
hideArrow={true} placement="bottom"
popperClassName='wiki-page-icon-popover' hideArrow={true}
innerClassName='wiki-page-icon-panel wiki-page-panel' popperClassName='wiki-page-icon-popover'
trigger="legacy" innerClassName='wiki-page-icon-panel wiki-page-panel'
> trigger="legacy"
<div className='wiki-page-icon-panel__header popover-header'> >
<span>{gettext('Emojis')}</span> <div className='wiki-page-icon-panel__header popover-header'>
<span onClick={handleIconRemove} className='wiki-remove-icon-btn'>{gettext('Remove')}</span> <span>{gettext('Emojis')}</span>
</div> <span onClick={handleIconRemove} className='wiki-remove-icon-btn'>{gettext('Remove')}</span>
<div className='wiki-page-icon-panel__body popover-body'> </div>
<Picker <div className='wiki-page-icon-panel__body popover-body'>
data={data} <Picker
onEmojiSelect={(emoji) => handleSetIcon(emoji.native)} data={data}
previewPosition="none" onEmojiSelect={(emoji) => handleSetIcon(emoji.native)}
skinTonePosition="none" previewPosition="none"
locale={window.seafile.lang.slice(0, 2)} skinTonePosition="none"
maxFrequentRows={2} locale={window.seafile.lang.slice(0, 2)}
theme="light" maxFrequentRows={2}
/> theme="light"
</div> />
</UncontrolledPopover> </div>
</UncontrolledPopover>
}
</> </>
); );
}; };

View File

@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { throttle } from '../utils'; import { throttle } from '../utils';
import { wikiPermission } from '../../../utils/constants';
function PageTitleEditor({ isUpdateBySide, currentPageConfig, onUpdatePage }) { function PageTitleEditor({ isUpdateBySide, currentPageConfig, onUpdatePage }) {
@ -87,18 +88,28 @@ function PageTitleEditor({ isUpdateBySide, currentPageConfig, onUpdatePage }) {
return ( return (
<div <div>
className='wiki-sdoc-title' {wikiPermission === 'public' ?
contentEditable <div
suppressContentEditableWarning className='wiki-sdoc-title'
ref={contentEditableRef} >
onInput={handleInput} {pageName}
onKeyDown={onKeyDown} </div> :
onCompositionStart={onCompositionStart} <div
onCompositionEnd={onCompositionEnd} className='wiki-sdoc-title'
> contentEditable
{pageName} suppressContentEditableWarning
ref={contentEditableRef}
onInput={handleInput}
onKeyDown={onKeyDown}
onCompositionStart={onCompositionStart}
onCompositionEnd={onCompositionEnd}
>
{pageName}
</div>
}
</div> </div>
); );
} }

View File

@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import { gettext } from '../../../utils/constants'; import { gettext, wikiPermission } from '../../../utils/constants';
import { WIKI_COVER_LIST } from '../constant'; import { WIKI_COVER_LIST } from '../constant';
import PageIcon from './page-icon'; import PageIcon from './page-icon';
import { generateARandomEmoji, generateEmojiIcon } from '../utils/emoji-utils'; import { generateARandomEmoji, generateEmojiIcon } from '../utils/emoji-utils';
@ -56,13 +56,13 @@ const PageTitle = ({ isUpdateBySide, currentPageConfig, onUpdatePage }) => {
<PageIcon currentPageConfig={currentPageConfig} onUpdatePage={onUpdatePage} /> <PageIcon currentPageConfig={currentPageConfig} onUpdatePage={onUpdatePage} />
)} )}
<div className={classnames('wiki-page-controller', { 'show': isShowController })}> <div className={classnames('wiki-page-controller', { 'show': isShowController })}>
{!currentPageConfig.icon && ( {!currentPageConfig.icon && wikiPermission !== 'public' && (
<div className='wiki-page-controller-item' onClick={handleAddIcon}> <div className='wiki-page-controller-item' onClick={handleAddIcon}>
<i className='sf3-font sf3-font-icon'></i> <i className='sf3-font sf3-font-icon'></i>
<span className='text'>{gettext('Add icon')}</span> <span className='text'>{gettext('Add icon')}</span>
</div> </div>
)} )}
{!currentPageConfig.cover_img_url && ( {!currentPageConfig.cover_img_url && wikiPermission !== 'public' && (
<div className='wiki-page-controller-item' onClick={handleAddCover}> <div className='wiki-page-controller-item' onClick={handleAddCover}>
<i className='sf3-font sf3-font-image'></i> <i className='sf3-font sf3-font-image'></i>
<span className='text'>{gettext('Add cover')}</span> <span className='text'>{gettext('Add cover')}</span>

View File

@ -115,6 +115,7 @@ export const wikiId = window.wiki ? window.wiki.config.wikiId : '';
export const repoID = window.wiki ? window.wiki.config.repoId : ''; export const repoID = window.wiki ? window.wiki.config.repoId : '';
export const initialPath = window.wiki ? window.wiki.config.initial_path : ''; export const initialPath = window.wiki ? window.wiki.config.initial_path : '';
export const permission = window.wiki ? window.wiki.config.permission === 'True' : ''; 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 isDir = window.wiki ? window.wiki.config.isDir : '';
export const serviceUrl = window.wiki ? window.wiki.config.serviceUrl : ''; export const serviceUrl = window.wiki ? window.wiki.config.serviceUrl : '';
export const isPublicWiki = window.wiki ? window.wiki.config.isPublicWiki === 'True' : ''; export const isPublicWiki = window.wiki ? window.wiki.config.isPublicWiki === 'True' : '';

View File

@ -176,6 +176,11 @@ class WikiAPI {
return this.req.get(url); 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) { createWiki2Page(wikiId, pageName, currentId) {
const url = this.server + '/api/v2.1/wiki2/' + wikiId + '/pages/'; const url = this.server + '/api/v2.1/wiki2/' + wikiId + '/pages/';
let form = new FormData(); let form = new FormData();
@ -211,6 +216,11 @@ class WikiAPI {
return this.req.get(url); 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) { renameWiki2(wikiId, wikiName) {
const url = this.server + '/api/v2.1/wiki2/' + wikiId + '/'; const url = this.server + '/api/v2.1/wiki2/' + wikiId + '/';
let params = { 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(); let wikiAPI = new WikiAPI();

View File

@ -8,6 +8,7 @@ import posixpath
import time import time
import datetime import datetime
import uuid import uuid
import re
import urllib.request, urllib.error, urllib.parse import urllib.request, urllib.error, urllib.parse
from copy import deepcopy from copy import deepcopy
from constance import config 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.api2.utils import api_error, to_python_boolean, is_wiki_repo
from seahub.utils.db_api import SeafileDB from seahub.utils.db_api import SeafileDB
from seahub.wiki2.models import Wiki2 as Wiki 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, \ 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, \ 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, \ 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, \ 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 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 import check_folder_permission
from seahub.views.file import send_file_access_msg
from seahub.base.templatetags.seahub_tags import email2nickname 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.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 from seahub.utils.repo import parse_repo_perm, get_repo_owner
@ -61,7 +61,8 @@ HTTP_520_OPERATION_FAILED = 520
logger = logging.getLogger(__name__) 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_ids = [gw.group_id for gw in group_wikis]
group_id_wikis_map = {key: [] for key in group_ids} group_id_wikis_map = {key: [] for key in group_ids}
@ -77,7 +78,8 @@ def _merge_wiki_in_groups(group_wikis):
repo_info = { repo_info = {
"type": "group", "type": "group",
"permission": gw.permission, "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) wiki_info.update(repo_info)
group_id = gw.group_id 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) user_groups = ccnet_api.get_org_groups_by_user(org_id, username, return_ancestors=True)
else: else:
user_groups = ccnet_api.get_groups(username, return_ancestors=True) user_groups = ccnet_api.get_groups(username, return_ancestors=True)
owned_wikis = [r for r in owned if is_wiki_repo(r)] 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 = [] wiki_list = []
for r in owned_wikis: for r in owned_wikis:
r.owner = username r.owner = username
@ -116,12 +124,12 @@ class Wikis2View(APIView):
repo_info = { repo_info = {
"type": "mine", "type": "mine",
"permission": 'rw', "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_info.update(repo_info)
wiki_list.append(wiki_info) wiki_list.append(wiki_info)
shared_wikis = [r for r in shared if is_wiki_repo(r)]
for r in shared_wikis: for r in shared_wikis:
owner = r.user owner = r.user
r.owner = owner r.owner = owner
@ -135,12 +143,12 @@ class Wikis2View(APIView):
repo_info = { repo_info = {
"type": "shared", "type": "shared",
"permission": r.permission, "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_info.update(repo_info)
wiki_list.append(wiki_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])) group_id_in_wikis = list(set([r.group_id for r in group_wikis]))
try: try:
group_ids_admins_map = {} group_ids_admins_map = {}
@ -157,7 +165,7 @@ class Wikis2View(APIView):
r.owner = r.user r.owner = r.user
group_wiki_list = [] 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: for group_obj in user_wiki_groups:
group_wiki = { group_wiki = {
'group_name': group_obj.group_name, 'group_name': group_obj.group_name,
@ -354,6 +362,8 @@ class Wiki2View(APIView):
else: else:
seafile_api.remove_repo(wiki.repo_id) seafile_api.remove_repo(wiki.repo_id)
Wiki2Publish.objects.filter(repo_id=wiki.repo_id).delete()
return Response() return Response()
@ -426,6 +436,35 @@ class Wiki2ConfigView(APIView):
return Response({'wiki': wiki}) 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): class Wiki2PagesView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication) authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated, ) permission_classes = (IsAuthenticated, )
@ -613,8 +652,6 @@ class Wiki2PageView(APIView):
throttle_classes = (UserRateThrottle, ) throttle_classes = (UserRateThrottle, )
def get(self, request, wiki_id, page_id): def get(self, request, wiki_id, page_id):
wiki = Wiki.objects.get(wiki_id=wiki_id) wiki = Wiki.objects.get(wiki_id=wiki_id)
if not wiki: if not wiki:
error_msg = "Wiki not found." error_msg = "Wiki not found."
@ -658,9 +695,6 @@ class Wiki2PageView(APIView):
if not file_id: if not file_id:
return api_error(status.HTTP_404_NOT_FOUND, "File not found") 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) filename = os.path.basename(path)
try: try:
dirent = seafile_api.get_dirent_by_path(repo.repo_id, path) dirent = seafile_api.get_dirent_by_path(repo.repo_id, path)
@ -767,6 +801,68 @@ class Wiki2PageView(APIView):
return Response({'success': True}) 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): class Wiki2DuplicatePageView(APIView):
authentication_classes = (TokenAuthentication, SessionAuthentication) authentication_classes = (TokenAuthentication, SessionAuthentication)
permission_classes = (IsAuthenticated, ) permission_classes = (IsAuthenticated, )
@ -1073,4 +1169,102 @@ class WikiPageTrashView(APIView):
return Response({'success': True}) 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})

View File

@ -61,6 +61,7 @@ from seahub.base.accounts import User
from seahub.avatar.settings import AVATAR_DEFAULT_SIZE from seahub.avatar.settings import AVATAR_DEFAULT_SIZE
from seahub.repo_tags.models import RepoTags from seahub.repo_tags.models import RepoTags
from seahub.file_tags.models import FileTags from seahub.file_tags.models import FileTags
from seahub.wiki2.models import Wiki2Publish
if HAS_FILE_SEARCH or HAS_FILE_SEASEARCH: if HAS_FILE_SEARCH or HAS_FILE_SEASEARCH:
from seahub.search.utils import search_files, ai_search_files, format_repos 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 repo_id = uuid_map.repo_id
username = request.user.username username = request.user.username
wiki_publish = Wiki2Publish.objects.filter(repo_id=repo_id).first()
# permission check # permission check
file_path = posixpath.join(uuid_map.parent_path, uuid_map.filename) if not wiki_publish:
if not can_access_seadoc_asset(request, repo_id, file_path, file_uuid): file_path = posixpath.join(uuid_map.parent_path, uuid_map.filename)
error_msg = 'Permission denied.' if not can_access_seadoc_asset(request, repo_id, file_path, file_uuid):
return api_error(status.HTTP_403_FORBIDDEN, error_msg) error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
# main # main
parent_path = gen_seadoc_image_parent_path(file_uuid, repo_id, username) parent_path = gen_seadoc_image_parent_path(file_uuid, repo_id, username)

View File

@ -0,0 +1,35 @@
{% extends "base_for_react.html" %}
{% load i18n %}
{% load render_bundle from webpack_loader %}
{% load seahub_tags %}
{% block extra_ogp_tags %}
<meta property="og:type" content="website" />
<meta property="og:site_name" content="{{ site_name }}">
<meta property="og:url" content="{{ service_url }}{{ request.path }}" />
<meta property="og:title" content="{{ repo_name }}" />
<meta property="og:image" content="{{ service_url }}{{ MEDIA_URL }}img/file/{{ filename|file_icon_filter }}" />
<meta property="og:description" content="{{ filename }}" />
{% endblock %}
{% block extra_style %}
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}css/seafile-editor-font/seafile-editor-font.css" />
<link rel="stylesheet" type="text/css" href="{{ MEDIA_URL }}sdoc-editor/sdoc-editor-font.css" />
{% render_bundle 'wiki2' 'css' %}
{% endblock %}
{% block wiki_title %}{{repo_name}}{% endblock %}
{% block extra_script %}
<script type="text/javascript">
window.wiki = {
config: {
wikiId: "{{ wiki.id }}",
repoName: "{{ wiki.name }}",
initial_path: "{{ file_path|escapejs }}",
isWiki2: true,
seadocServerUrl: "{{ seadoc_server_url }}",
permission: "{{ permission }}",
}
};
</script>
{% render_bundle 'wiki2' 'js' %}
{% endblock %}

View File

@ -206,10 +206,9 @@ from seahub.api2.endpoints.repo_related_users import RepoRelatedUsersView
from seahub.api2.endpoints.repo_auto_delete import RepoAutoDeleteView from seahub.api2.endpoints.repo_auto_delete import RepoAutoDeleteView
from seahub.seadoc.views import sdoc_revision, sdoc_revisions, sdoc_to_docx from seahub.seadoc.views import sdoc_revision, sdoc_revisions, sdoc_to_docx
from seahub.ocm.settings import OCM_ENDPOINT from seahub.ocm.settings import OCM_ENDPOINT
from seahub.wiki2.views import wiki_view, wiki_publish_view
from seahub.wiki2.views import wiki_view
from seahub.api2.endpoints.wiki2 import Wikis2View, Wiki2View, Wiki2ConfigView, Wiki2PagesView, Wiki2PageView, \ 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.subscription import SubscriptionView, SubscriptionPlansView, SubscriptionLogsView
from seahub.api2.endpoints.metadata_manage import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \ from seahub.api2.endpoints.metadata_manage import MetadataRecords, MetadataManage, MetadataColumns, MetadataRecordInfo, \
MetadataViews, MetadataViewsMoveView, MetadataViewsDetailView, MetadataViewsDuplicateView 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/wikis2/$', Wikis2View.as_view(), name='api-v2.1-wikis2'),
re_path(r'^api/v2.1/wiki2/(?P<wiki_id>[-0-9a-f]{36})/$', Wiki2View.as_view(), name='api-v2.1-wiki2'), re_path(r'^api/v2.1/wiki2/(?P<wiki_id>[-0-9a-f]{36})/$', Wiki2View.as_view(), name='api-v2.1-wiki2'),
re_path(r'^api/v2.1/wiki2/(?P<wiki_id>[-0-9a-f]{36})/config/$', Wiki2ConfigView.as_view(), name='api-v2.1-wiki2-config'), re_path(r'^api/v2.1/wiki2/(?P<wiki_id>[-0-9a-f]{36})/config/$', Wiki2ConfigView.as_view(), name='api-v2.1-wiki2-config'),
re_path(r'^api/v2.1/wiki2/(?P<wiki_id>[-0-9a-f]{36})/publish/config/$', Wiki2PublishConfigView.as_view(), name='api-v2.1-wiki2-publish-config'),
re_path(r'^api/v2.1/wiki2/(?P<wiki_id>[-0-9a-f]{36})/pages/$', Wiki2PagesView.as_view(), name='api-v2.1-wiki2-pages'), re_path(r'^api/v2.1/wiki2/(?P<wiki_id>[-0-9a-f]{36})/pages/$', Wiki2PagesView.as_view(), name='api-v2.1-wiki2-pages'),
re_path(r'^api/v2.1/wiki2/(?P<wiki_id>[-0-9a-f]{36})/page/(?P<page_id>[-0-9a-zA-Z]{4})/$', Wiki2PageView.as_view(), name='api-v2.1-wiki2-page'), re_path(r'^api/v2.1/wiki2/(?P<wiki_id>[-0-9a-f]{36})/page/(?P<page_id>[-0-9a-zA-Z]{4})/$', Wiki2PageView.as_view(), name='api-v2.1-wiki2-page'),
re_path(r'^api/v2.1/wiki2/(?P<wiki_id>[-0-9a-f]{36})/publish/page/(?P<page_id>[-0-9a-zA-Z]{4})/$', Wiki2PublishPageView.as_view(), name='api-v2.1-wiki2-page'),
re_path(r'^api/v2.1/wiki2/(?P<wiki_id>[-0-9a-f]{36})/duplicate-page/$', Wiki2DuplicatePageView.as_view(), name='api-v2.1-wiki2-duplicate-page'), re_path(r'^api/v2.1/wiki2/(?P<wiki_id>[-0-9a-f]{36})/duplicate-page/$', Wiki2DuplicatePageView.as_view(), name='api-v2.1-wiki2-duplicate-page'),
re_path(r'^api/v2.1/wiki2/(?P<wiki_id>[-0-9a-f]{36})/trash/', WikiPageTrashView.as_view(), name='api-v2.1-wiki2-trash'), re_path(r'^api/v2.1/wiki2/(?P<wiki_id>[-0-9a-f]{36})/trash/', WikiPageTrashView.as_view(), name='api-v2.1-wiki2-trash'),
re_path(r'^api/v2.1/wiki2/(?P<wiki_id>[-0-9a-f]{36})/publish/$', Wiki2PublishView.as_view(), name='api-v2.1-wiki2-publish'),
## user::drafts ## user::drafts
re_path(r'^api/v2.1/drafts/$', DraftsView.as_view(), name='api-v2.1-drafts'), re_path(r'^api/v2.1/drafts/$', DraftsView.as_view(), name='api-v2.1-drafts'),
re_path(r'^api/v2.1/drafts/(?P<pk>\d+)/$', DraftView.as_view(), name='api-v2.1-draft'), re_path(r'^api/v2.1/drafts/(?P<pk>\d+)/$', DraftView.as_view(), name='api-v2.1-draft'),
@ -737,6 +738,7 @@ urlpatterns = [
re_path(r'^api/v2.1/admin/invitations/(?P<token>[a-f0-9]{32})/$', AdminInvitation.as_view(), name='api-v2.1-admin-invitation'), re_path(r'^api/v2.1/admin/invitations/(?P<token>[a-f0-9]{32})/$', AdminInvitation.as_view(), name='api-v2.1-admin-invitation'),
re_path(r'^wikis/(?P<wiki_id>[^/]+)/$', wiki_view, name='wiki'), re_path(r'^wikis/(?P<wiki_id>[^/]+)/$', wiki_view, name='wiki'),
re_path(r'^wiki/publish/(?P<publish_url>[-0-9a-zA-Z]+)/$', wiki_publish_view, name='wiki-publish'),
path('avatar/', include('seahub.avatar.urls')), path('avatar/', include('seahub.avatar.urls')),
path('group/', include('seahub.group.urls')), path('group/', include('seahub.group.urls')),

View File

@ -33,7 +33,6 @@ class Wiki2(object):
self.name = wiki.repo_name self.name = wiki.repo_name
self.updated_at = timestamp_to_isoformat_timestr(wiki.last_modify) self.updated_at = timestamp_to_isoformat_timestr(wiki.last_modify)
self.repo_id = wiki.repo_id self.repo_id = wiki.repo_id
def to_dict(self): def to_dict(self):
return { return {
@ -71,6 +70,25 @@ class WikiPageTrash(models.Model):
'size': self.size '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 ###### signal handlers
from django.dispatch import receiver from django.dispatch import receiver
from seahub.signals import repo_deleted from seahub.signals import repo_deleted

View File

@ -11,6 +11,7 @@ from django.http import Http404
from django.shortcuts import render from django.shortcuts import render
from seahub.wiki2.models import Wiki2 as Wiki 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 import get_file_type_and_ext, render_permission_error
from seahub.utils.file_types import SEADOC from seahub.utils.file_types import SEADOC
from seahub.auth.decorators import login_required from seahub.auth.decorators import login_required
@ -81,3 +82,66 @@ def wiki_view(request, wiki_id):
"permission": permission, "permission": permission,
"enable_user_clean_trash": config.ENABLE_USER_CLEAN_TRASH "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'
})

View File

@ -1557,3 +1557,16 @@ CREATE TABLE `sdoc_operation_log` (
KEY `sdoc_operation_log_doc_uuid` (`doc_uuid`), KEY `sdoc_operation_log_doc_uuid` (`doc_uuid`),
KEY `sdoc_idx_operation_log_doc_uuid_op_id` (`doc_uuid`,`op_id`) KEY `sdoc_idx_operation_log_doc_uuid_op_id` (`doc_uuid`,`op_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; ) 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;