mirror of
https://github.com/haiwen/seahub.git
synced 2025-09-04 00:20:07 +00:00
receive share from other service
This commit is contained in:
@@ -20,6 +20,7 @@ import ShareAdminShareLinks from './pages/share-admin/share-links';
|
|||||||
import ShareAdminUploadLinks from './pages/share-admin/upload-links';
|
import ShareAdminUploadLinks from './pages/share-admin/upload-links';
|
||||||
import SharedLibraries from './pages/shared-libs/shared-libs';
|
import SharedLibraries from './pages/shared-libs/shared-libs';
|
||||||
import ShareWithOCM from './pages/share-with-ocm/shared-with-ocm';
|
import ShareWithOCM from './pages/share-with-ocm/shared-with-ocm';
|
||||||
|
import OCMViaWebdav from './pages/ocm-via-webdav/ocm-via-webdav';
|
||||||
import OCMRepoDir from './pages/share-with-ocm/remote-dir-view';
|
import OCMRepoDir from './pages/share-with-ocm/remote-dir-view';
|
||||||
import MyLibraries from './pages/my-libs/my-libs';
|
import MyLibraries from './pages/my-libs/my-libs';
|
||||||
import MyLibDeleted from './pages/my-libs/my-libs-deleted';
|
import MyLibDeleted from './pages/my-libs/my-libs-deleted';
|
||||||
@@ -41,6 +42,7 @@ const StarredWrapper = MainContentWrapper(Starred);
|
|||||||
const LinkedDevicesWrapper = MainContentWrapper(LinkedDevices);
|
const LinkedDevicesWrapper = MainContentWrapper(LinkedDevices);
|
||||||
const SharedLibrariesWrapper = MainContentWrapper(SharedLibraries);
|
const SharedLibrariesWrapper = MainContentWrapper(SharedLibraries);
|
||||||
const SharedWithOCMWrapper = MainContentWrapper(ShareWithOCM);
|
const SharedWithOCMWrapper = MainContentWrapper(ShareWithOCM);
|
||||||
|
const OCMViaWebdavWrapper = MainContentWrapper(OCMViaWebdav);
|
||||||
const ShareAdminLibrariesWrapper = MainContentWrapper(ShareAdminLibraries);
|
const ShareAdminLibrariesWrapper = MainContentWrapper(ShareAdminLibraries);
|
||||||
const ShareAdminFoldersWrapper = MainContentWrapper(ShareAdminFolders);
|
const ShareAdminFoldersWrapper = MainContentWrapper(ShareAdminFolders);
|
||||||
const ShareAdminShareLinksWrapper = MainContentWrapper(ShareAdminShareLinks);
|
const ShareAdminShareLinksWrapper = MainContentWrapper(ShareAdminShareLinks);
|
||||||
@@ -261,6 +263,7 @@ class App extends Component {
|
|||||||
<ShareAdminUploadLinksWrapper path={siteRoot + 'share-admin-upload-links'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
|
<ShareAdminUploadLinksWrapper path={siteRoot + 'share-admin-upload-links'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
|
||||||
<SharedLibrariesWrapper path={siteRoot + 'shared-libs'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
|
<SharedLibrariesWrapper path={siteRoot + 'shared-libs'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
|
||||||
<SharedWithOCMWrapper path={siteRoot + 'shared-with-ocm'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
|
<SharedWithOCMWrapper path={siteRoot + 'shared-with-ocm'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
|
||||||
|
<OCMViaWebdavWrapper path={siteRoot + 'ocm-via-webdav'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
|
||||||
<MyLibraries path={siteRoot + 'my-libs'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
|
<MyLibraries path={siteRoot + 'my-libs'} onShowSidePanel={this.onShowSidePanel} onSearchedClick={this.onSearchedClick} />
|
||||||
<MyLibDeleted path={siteRoot + 'my-libs/deleted/'} onSearchedClick={this.onSearchedClick} />
|
<MyLibDeleted path={siteRoot + 'my-libs/deleted/'} onSearchedClick={this.onSearchedClick} />
|
||||||
<LibContentView path={siteRoot + 'library/:repoID/*'} pathPrefix={this.state.pathPrefix} onMenuClick={this.onShowSidePanel} onTabNavClick={this.tabItemClick}/>
|
<LibContentView path={siteRoot + 'library/:repoID/*'} pathPrefix={this.state.pathPrefix} onMenuClick={this.onShowSidePanel} onTabNavClick={this.tabItemClick}/>
|
||||||
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { Link } from '@reach/router';
|
import { Link } from '@reach/router';
|
||||||
import { Badge } from 'reactstrap';
|
import { Badge } from 'reactstrap';
|
||||||
import { gettext, siteRoot, canPublishRepo, canAddRepo, canGenerateShareLink, canGenerateUploadLink, canInvitePeople, dtableWebServer, enableOCM } from '../utils/constants';
|
import { gettext, siteRoot, canPublishRepo, canAddRepo, canGenerateShareLink, canGenerateUploadLink, canInvitePeople, dtableWebServer, enableOCM, enableOCMViaWebdav } from '../utils/constants';
|
||||||
import { seafileAPI } from '../utils/seafile-api';
|
import { seafileAPI } from '../utils/seafile-api';
|
||||||
import { Utils } from '../utils/utils';
|
import { Utils } from '../utils/utils';
|
||||||
import toaster from './toast';
|
import toaster from './toast';
|
||||||
@@ -224,6 +224,14 @@ class MainSideNav extends React.Component {
|
|||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
}
|
}
|
||||||
|
{enableOCMViaWebdav &&
|
||||||
|
<li className="nav-item">
|
||||||
|
<Link to={siteRoot + 'ocm-via-webdav/'} className={`nav-link ellipsis ${this.getActiveClass('ocm-via-webdav')}`} title={gettext('Shared from other servers')} onClick={(e) => this.tabItemClick(e, 'ocm-via-webdav')}>
|
||||||
|
<span className="sf3-font-share-from-other-servers sf3-font" aria-hidden="true"></span>
|
||||||
|
<span className="nav-text">{gettext('Shared from other servers')}</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
184
frontend/src/pages/ocm-via-webdav/ocm-via-webdav.js
Normal file
184
frontend/src/pages/ocm-via-webdav/ocm-via-webdav.js
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
import React, { Component, Fragment } from 'react';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { Link } from '@reach/router';
|
||||||
|
import { gettext, siteRoot } from '../../utils/constants';
|
||||||
|
import { seafileAPI } from '../../utils/seafile-api';
|
||||||
|
import { Utils } from '../../utils/utils';
|
||||||
|
import toaster from '../../components/toast';
|
||||||
|
import Loading from '../../components/loading';
|
||||||
|
import EmptyTip from '../../components/empty-tip';
|
||||||
|
|
||||||
|
class Content extends Component {
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { loading, errorMsg, items } = this.props;
|
||||||
|
|
||||||
|
const emptyTip = (
|
||||||
|
<EmptyTip>
|
||||||
|
<h2>{gettext('No libraries have been shared with you')}</h2>
|
||||||
|
<p>{gettext('No libraries have been shared with you from other servers.')}</p>
|
||||||
|
</EmptyTip>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return <Loading />;
|
||||||
|
} else if (errorMsg) {
|
||||||
|
return <p className="error text-center">{errorMsg}</p>;
|
||||||
|
} else {
|
||||||
|
const table = (
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th width="5%"></th>
|
||||||
|
<th width="30%">{gettext('Name')}</th>
|
||||||
|
<th width="35%">{gettext('Shared by')}</th>
|
||||||
|
<th width="20%">{gettext('Time')}</th>
|
||||||
|
<th width="5%">{/* operations */}</th>
|
||||||
|
<th width="5%">{/* operations */}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{items.map((item, index) => {
|
||||||
|
return <Item
|
||||||
|
key={index}
|
||||||
|
item={item}
|
||||||
|
leaveShare={this.props.leaveShare}
|
||||||
|
/>;
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
|
||||||
|
return items.length ? table : emptyTip;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Content.propTypes = {
|
||||||
|
loading: PropTypes.bool.isRequired,
|
||||||
|
errorMsg: PropTypes.string.isRequired,
|
||||||
|
items: PropTypes.array.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
class Item extends Component {
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
isOpIconShown: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseOver = () => {
|
||||||
|
this.setState({
|
||||||
|
isOpIconShown: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMouseOut = () => {
|
||||||
|
this.setState({
|
||||||
|
isOpIconShown: false
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadFile = () => {
|
||||||
|
let downloadUrl = siteRoot + 'ocm-via-webdav/download-received-file/?share_id=' + this.props.item.id;
|
||||||
|
window.location.href = downloadUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
leaveShare = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.props.leaveShare(this.props.item);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const item = this.props.item;
|
||||||
|
const { isOpIconShown } = this.state;
|
||||||
|
|
||||||
|
item.icon_url = Utils.getFileIconUrl(item.name);
|
||||||
|
return (
|
||||||
|
<tr onMouseOver={this.handleMouseOver} onMouseOut={this.handleMouseOut}>
|
||||||
|
<td><img src={item.icon_url} width="24" /></td>
|
||||||
|
<td>
|
||||||
|
{item.name}
|
||||||
|
</td>
|
||||||
|
<td>{item.shared_by}</td>
|
||||||
|
<td title={moment(item.last_modified).format('llll')}>{moment(item.ctime).fromNow()}</td>
|
||||||
|
<td>
|
||||||
|
<a href="#" className={`action-icon sf2-icon-download ${isOpIconShown ? '' : 'invisible'}`} title={gettext('Download')} onClick={this.downloadFile}></a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a href="#" className={`action-icon sf2-icon-x3 ${isOpIconShown ? '' : 'invisible'}`} title={gettext('Leave Share')} onClick={this.leaveShare}></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item.propTypes = {
|
||||||
|
item: PropTypes.object.isRequired
|
||||||
|
};
|
||||||
|
|
||||||
|
class OCMViaWebdav extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
loading: true,
|
||||||
|
errorMsg: '',
|
||||||
|
items: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
const url = seafileAPI.server + '/ocm-via-webdav/received-shares/';
|
||||||
|
seafileAPI.req.get(url).then((res) => {
|
||||||
|
this.setState({
|
||||||
|
loading: false,
|
||||||
|
items: res.data.received_share_list
|
||||||
|
});
|
||||||
|
}).catch(error => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
leaveShare = (item) => {
|
||||||
|
const { id, name } = item;
|
||||||
|
const url = seafileAPI.server + '/ocm-via-webdav/received-shares/' + id + '/';
|
||||||
|
seafileAPI.req.delete(url).then((res) => {
|
||||||
|
let items = this.state.items.filter(item => {
|
||||||
|
return item.id != id;
|
||||||
|
});
|
||||||
|
this.setState({items: items});
|
||||||
|
toaster.success(gettext('Successfully unshared {name}').replace('{name}', name));
|
||||||
|
}).catch(error => {
|
||||||
|
let errMessage = Utils.getErrorMsg(error);
|
||||||
|
toaster.danger(errMessage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Fragment>
|
||||||
|
<div className="main-panel-center">
|
||||||
|
<div className="cur-view-container">
|
||||||
|
<div className="cur-view-path">
|
||||||
|
<h3 className="sf-heading m-0">{gettext('Shared from other servers')}</h3>
|
||||||
|
</div>
|
||||||
|
<div className="cur-view-content">
|
||||||
|
<Content
|
||||||
|
loading={this.state.loading}
|
||||||
|
errorMsg={this.state.errorMsg}
|
||||||
|
items={this.state.items}
|
||||||
|
leaveShare={this.leaveShare}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default OCMViaWebdav;
|
@@ -74,6 +74,7 @@ export const maxUploadFileSize = window.app.pageOptions.maxUploadFileSize;
|
|||||||
export const maxNumberOfFilesForFileupload = window.app.pageOptions.maxNumberOfFilesForFileupload;
|
export const maxNumberOfFilesForFileupload = window.app.pageOptions.maxNumberOfFilesForFileupload;
|
||||||
export const enableOCM = window.app.pageOptions.enableOCM;
|
export const enableOCM = window.app.pageOptions.enableOCM;
|
||||||
export const ocmRemoteServers = window.app.pageOptions.ocmRemoteServers;
|
export const ocmRemoteServers = window.app.pageOptions.ocmRemoteServers;
|
||||||
|
export const enableOCMViaWebdav = window.app.pageOptions.enableOCMViaWebdav;
|
||||||
|
|
||||||
export const curNoteMsg = window.app.pageOptions.curNoteMsg;
|
export const curNoteMsg = window.app.pageOptions.curNoteMsg;
|
||||||
export const curNoteID = window.app.pageOptions.curNoteID;
|
export const curNoteID = window.app.pageOptions.curNoteID;
|
||||||
|
0
seahub/ocm_via_webdav/__init__.py
Normal file
0
seahub/ocm_via_webdav/__init__.py
Normal file
38
seahub/ocm_via_webdav/migrations/0001_initial.py
Normal file
38
seahub/ocm_via_webdav/migrations/0001_initial.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# Generated by Django 2.2.14 on 2021-09-16 16:55
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
import django.utils.timezone
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
initial = True
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ShareReceived',
|
||||||
|
fields=[
|
||||||
|
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('description', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
('name', models.CharField(db_index=True, max_length=255)),
|
||||||
|
('owner', models.CharField(db_index=True, max_length=255)),
|
||||||
|
('owner_display_name', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
('protocol_name', models.CharField(db_index=True, max_length=255)),
|
||||||
|
('shared_secret', models.CharField(db_index=True, max_length=255)),
|
||||||
|
('permissions', models.CharField(db_index=True, max_length=255)),
|
||||||
|
('provider_id', models.CharField(db_index=True, max_length=255)),
|
||||||
|
('resource_type', models.CharField(db_index=True, max_length=255)),
|
||||||
|
('share_type', models.CharField(db_index=True, max_length=255)),
|
||||||
|
('share_with', models.CharField(db_index=True, max_length=255)),
|
||||||
|
('shared_by', models.CharField(db_index=True, max_length=255)),
|
||||||
|
('shared_by_display_name', models.CharField(blank=True, max_length=255, null=True)),
|
||||||
|
('ctime', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'db_table': 'ocm_via_webdav_share_received',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
17
seahub/ocm_via_webdav/migrations/0002_auto_20210926_1503.py
Normal file
17
seahub/ocm_via_webdav/migrations/0002_auto_20210926_1503.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 2.2.14 on 2021-09-26 15:03
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ocm_via_webdav', '0001_initial'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RenameModel(
|
||||||
|
old_name='ShareReceived',
|
||||||
|
new_name='ReceivedShares',
|
||||||
|
),
|
||||||
|
]
|
32
seahub/ocm_via_webdav/migrations/0003_auto_20210926_1505.py
Normal file
32
seahub/ocm_via_webdav/migrations/0003_auto_20210926_1505.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# Generated by Django 2.2.14 on 2021-09-26 15:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('ocm_via_webdav', '0002_auto_20210926_1503'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='receivedshares',
|
||||||
|
name='name',
|
||||||
|
field=models.CharField(max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='receivedshares',
|
||||||
|
name='permissions',
|
||||||
|
field=models.CharField(max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='receivedshares',
|
||||||
|
name='protocol_name',
|
||||||
|
field=models.CharField(max_length=255),
|
||||||
|
),
|
||||||
|
migrations.AlterModelTable(
|
||||||
|
name='receivedshares',
|
||||||
|
table='ocm_via_webdav_received_shares',
|
||||||
|
),
|
||||||
|
]
|
0
seahub/ocm_via_webdav/migrations/__init__.py
Normal file
0
seahub/ocm_via_webdav/migrations/__init__.py
Normal file
25
seahub/ocm_via_webdav/models.py
Normal file
25
seahub/ocm_via_webdav/models.py
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
from django.db import models
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
|
|
||||||
|
class ReceivedShares(models.Model):
|
||||||
|
|
||||||
|
# https://cs3org.github.io/OCM-API/docs.html
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
db_table = 'ocm_via_webdav_received_shares'
|
||||||
|
|
||||||
|
description = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
name = models.CharField(max_length=255)
|
||||||
|
owner = models.CharField(max_length=255, db_index=True)
|
||||||
|
owner_display_name = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
protocol_name = models.CharField(max_length=255)
|
||||||
|
shared_secret = models.CharField(max_length=255, db_index=True)
|
||||||
|
permissions = models.CharField(max_length=255)
|
||||||
|
provider_id = models.CharField(max_length=255, db_index=True)
|
||||||
|
resource_type = models.CharField(max_length=255, db_index=True)
|
||||||
|
share_type = models.CharField(max_length=255, db_index=True)
|
||||||
|
share_with = models.CharField(max_length=255, db_index=True)
|
||||||
|
shared_by = models.CharField(max_length=255, db_index=True)
|
||||||
|
shared_by_display_name = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
ctime = models.DateTimeField(default=timezone.now)
|
373
seahub/ocm_via_webdav/ocm_api.py
Normal file
373
seahub/ocm_via_webdav/ocm_api.py
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
from constance import config
|
||||||
|
|
||||||
|
from django.http import HttpResponse
|
||||||
|
|
||||||
|
from rest_framework import status
|
||||||
|
from rest_framework.response import Response
|
||||||
|
from rest_framework.views import APIView
|
||||||
|
|
||||||
|
from seahub.api2.throttling import UserRateThrottle
|
||||||
|
from seahub.api2.utils import api_error
|
||||||
|
|
||||||
|
from seahub.base.templatetags.seahub_tags import email2nickname
|
||||||
|
|
||||||
|
from seahub.ocm_via_webdav.settings import ENABLE_OCM_VIA_WEBDAV, OCM_VIA_WEBDAV_ENDPOINT
|
||||||
|
from seahub.ocm_via_webdav.models import ReceivedShares
|
||||||
|
|
||||||
|
from seahub.ocm.settings import ENABLE_OCM, OCM_SEAFILE_PROTOCOL, \
|
||||||
|
OCM_RESOURCE_TYPE_LIBRARY, OCM_API_VERSION, OCM_SHARE_TYPES, \
|
||||||
|
OCM_ENDPOINT
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class OCMProviderView(APIView):
|
||||||
|
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""
|
||||||
|
Return ocm protocol info to remote server
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
|
||||||
|
if ENABLE_OCM:
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'enabled': True,
|
||||||
|
'apiVersion': OCM_API_VERSION,
|
||||||
|
'endPoint': config.SERVICE_URL + '/' + OCM_ENDPOINT,
|
||||||
|
'resourceTypes': {
|
||||||
|
'name': OCM_RESOURCE_TYPE_LIBRARY,
|
||||||
|
'shareTypes': OCM_SHARE_TYPES,
|
||||||
|
'protocols': {
|
||||||
|
OCM_SEAFILE_PROTOCOL: OCM_SEAFILE_PROTOCOL,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ENABLE_OCM_VIA_WEBDAV:
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'apiVersion': '1.0-proposal1',
|
||||||
|
'enabled': True,
|
||||||
|
'endPoint': config.SERVICE_URL + '/' + OCM_VIA_WEBDAV_ENDPOINT,
|
||||||
|
'resourceTypes': {
|
||||||
|
'name': 'file',
|
||||||
|
'protocols': {'webdav': 'TODO'},
|
||||||
|
'shareTypes': ['user'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Response(result)
|
||||||
|
|
||||||
|
|
||||||
|
class SharesView(APIView):
|
||||||
|
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""
|
||||||
|
Receive share from other service
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not ENABLE_OCM_VIA_WEBDAV:
|
||||||
|
error_msg = 'OCM via webdav feature is not enabled.'
|
||||||
|
return api_error(501, error_msg)
|
||||||
|
|
||||||
|
# {'description': '',
|
||||||
|
# 'name': 'file-3-in-nextcloud-folder.md',
|
||||||
|
# 'owner': 'lian@https://nextcloud.seafile.top/',
|
||||||
|
# 'ownerDisplayName': 'lian',
|
||||||
|
# 'protocol': {'name': 'webdav',
|
||||||
|
# 'options': {'permissions': '{http://open-cloud-mesh.org/ns}share-permissions',
|
||||||
|
# 'sharedSecret': 'HdjKpI4o6lamWwN'}},
|
||||||
|
# 'providerId': 9,
|
||||||
|
# 'resourceType': 'file',
|
||||||
|
# 'shareType': 'user',
|
||||||
|
# 'shareWith': 'lian@lian.com@https://demo.seafile.top', # or 'lian@https://demo.seafile.top',
|
||||||
|
# 'sharedBy': 'lian@https://nextcloud.seafile.top/',
|
||||||
|
# 'sharedByDisplayName': 'lian'}
|
||||||
|
|
||||||
|
protocol_dict = request.data.get('protocol', {})
|
||||||
|
protocol_name = protocol_dict.get('name')
|
||||||
|
shared_secret = protocol_dict.get('options').get('sharedSecret')
|
||||||
|
permissions = protocol_dict.get('options').get('permissions')
|
||||||
|
|
||||||
|
owner = request.data.get('owner')
|
||||||
|
owner_display_name = request.data.get('owner_display_name')
|
||||||
|
|
||||||
|
name = request.data.get('name')
|
||||||
|
description = request.data.get('description')
|
||||||
|
provider_id = request.data.get('providerId')
|
||||||
|
resource_type = request.data.get('resourceType')
|
||||||
|
share_type = request.data.get('shareType')
|
||||||
|
share_with = request.data.get('shareWith').split('http')[0].rstrip('@')
|
||||||
|
shared_by = request.data.get('sharedBy')
|
||||||
|
shared_by_display_name = request.data.get('sharedByDisplayName')
|
||||||
|
|
||||||
|
share = ReceivedShares(description=description,
|
||||||
|
name=name,
|
||||||
|
owner=owner,
|
||||||
|
owner_display_name=owner_display_name,
|
||||||
|
protocol_name=protocol_name,
|
||||||
|
shared_secret=shared_secret,
|
||||||
|
permissions=permissions,
|
||||||
|
provider_id=provider_id,
|
||||||
|
resource_type=resource_type,
|
||||||
|
share_type=share_type,
|
||||||
|
share_with=share_with,
|
||||||
|
shared_by=shared_by,
|
||||||
|
shared_by_display_name=shared_by_display_name)
|
||||||
|
share.save()
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"recipientDisplayName": email2nickname(share_with)
|
||||||
|
}
|
||||||
|
return Response(result, status=status.HTTP_201_CREATED)
|
||||||
|
|
||||||
|
|
||||||
|
class ReceivedSharesView(APIView):
|
||||||
|
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""
|
||||||
|
Get items shared from other service.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not ENABLE_OCM_VIA_WEBDAV:
|
||||||
|
error_msg = 'OCM via webdav feature is not enabled.'
|
||||||
|
return api_error(501, error_msg)
|
||||||
|
|
||||||
|
username = request.user.username
|
||||||
|
|
||||||
|
info_list = []
|
||||||
|
for share in ReceivedShares.objects.filter(share_with=username):
|
||||||
|
|
||||||
|
info = {}
|
||||||
|
info['id'] = share.id
|
||||||
|
info['name'] = share.name
|
||||||
|
info['ctime'] = share.ctime
|
||||||
|
info['shared_by'] = share.shared_by
|
||||||
|
|
||||||
|
info_list.append(info)
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'received_share_list': info_list
|
||||||
|
}
|
||||||
|
return Response(result)
|
||||||
|
|
||||||
|
|
||||||
|
class ReceivedShareView(APIView):
|
||||||
|
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def delete(self, request, share_id):
|
||||||
|
"""
|
||||||
|
Delete item shared from other service.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not ENABLE_OCM_VIA_WEBDAV:
|
||||||
|
error_msg = 'OCM via webdav feature is not enabled.'
|
||||||
|
return api_error(501, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
share = ReceivedShares.objects.get(id=share_id)
|
||||||
|
except ReceivedShares.DoesNotExist:
|
||||||
|
error_msg = "OCM share {} not found.".format(share_id)
|
||||||
|
return api_error(404, error_msg)
|
||||||
|
|
||||||
|
username = request.user.username
|
||||||
|
if share.share_with != username:
|
||||||
|
error_msg = 'Permission denied.'
|
||||||
|
return api_error(403, error_msg)
|
||||||
|
|
||||||
|
# get remote server endpoint
|
||||||
|
shared_by = share.shared_by
|
||||||
|
remote_domain = shared_by.split('@')[-1]
|
||||||
|
remote_domain = remote_domain.rstrip('/')
|
||||||
|
|
||||||
|
ocm_provider_url = remote_domain + '/ocm-provider/'
|
||||||
|
resp = requests.get(ocm_provider_url)
|
||||||
|
end_point = resp.json().get('endPoint')
|
||||||
|
|
||||||
|
if not end_point:
|
||||||
|
logger.error('Can not get endPoint from {}'.format(ocm_provider_url))
|
||||||
|
logger.error(resp.content)
|
||||||
|
|
||||||
|
end_point = end_point.rstrip('/')
|
||||||
|
|
||||||
|
# send SHARE_DECLINED notification
|
||||||
|
data = {
|
||||||
|
"notification": {
|
||||||
|
"message": "Recipient declined the share",
|
||||||
|
"sharedSecret": ""
|
||||||
|
},
|
||||||
|
"notificationType": "SHARE_DECLINED",
|
||||||
|
"providerId": "",
|
||||||
|
"resourceType": ""
|
||||||
|
}
|
||||||
|
|
||||||
|
data['notification']['sharedSecret'] = share.shared_secret
|
||||||
|
data['providerId'] = share.provider_id
|
||||||
|
data['resourceType'] = share.resource_type
|
||||||
|
|
||||||
|
notifications_url = end_point + '/notifications'
|
||||||
|
resp = requests.post(notifications_url, json=data)
|
||||||
|
if resp.status_code != 201:
|
||||||
|
logger.error('Error occurred when send notification to {}'.format(notifications_url))
|
||||||
|
logger.error(resp.content)
|
||||||
|
|
||||||
|
share.delete()
|
||||||
|
|
||||||
|
result = {
|
||||||
|
'success': True
|
||||||
|
}
|
||||||
|
return Response(result)
|
||||||
|
|
||||||
|
|
||||||
|
class DownloadReceivedFileView(APIView):
|
||||||
|
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def get(self, request):
|
||||||
|
"""
|
||||||
|
Download received file.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not ENABLE_OCM_VIA_WEBDAV:
|
||||||
|
error_msg = 'OCM via webdav feature is not enabled.'
|
||||||
|
return api_error(501, error_msg)
|
||||||
|
|
||||||
|
share_id = request.GET.get('share_id')
|
||||||
|
if not share_id:
|
||||||
|
error_msg = 'share_id invalid.'
|
||||||
|
return api_error(400, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
share_id = int(share_id)
|
||||||
|
except ValueError as e:
|
||||||
|
logger.error(e)
|
||||||
|
error_msg = 'share_id invalid.'
|
||||||
|
return api_error(400, error_msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
share = ReceivedShares.objects.get(id=share_id)
|
||||||
|
except ReceivedShares.DoesNotExist:
|
||||||
|
error_msg = "OCM share {} not found.".format(share_id)
|
||||||
|
return api_error(404, error_msg)
|
||||||
|
|
||||||
|
# get remote server endpoint
|
||||||
|
shared_by = share.shared_by
|
||||||
|
remote_domain = shared_by.split('@')[-1]
|
||||||
|
remote_domain = remote_domain.rstrip('/')
|
||||||
|
|
||||||
|
ocm_provider_url = remote_domain + '/ocm-provider/'
|
||||||
|
resp = requests.get(ocm_provider_url)
|
||||||
|
|
||||||
|
# {
|
||||||
|
# 'apiVersion': '1.0-proposal1',
|
||||||
|
# 'enabled': True,
|
||||||
|
# 'endPoint': 'https://nextcloud.seafile.top/index.php/ocm',
|
||||||
|
# 'resourceTypes': [
|
||||||
|
# {
|
||||||
|
# 'name': 'file',
|
||||||
|
# 'protocols': {'webdav': '/public.php/webdav/'},
|
||||||
|
# 'shareTypes': ['user', 'group']
|
||||||
|
# }
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
|
||||||
|
resource_types = resp.json().get('resourceTypes', [])
|
||||||
|
if not resource_types:
|
||||||
|
logger.error('Can not get resource_types from {}'.format(ocm_provider_url))
|
||||||
|
logger.error(resp.content)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(501, error_msg)
|
||||||
|
|
||||||
|
protocols = resource_types[0].get('protocols')
|
||||||
|
if not protocols:
|
||||||
|
logger.error('Can not get protocols from {}'.format(ocm_provider_url))
|
||||||
|
logger.error(resp.content)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(501, error_msg)
|
||||||
|
|
||||||
|
webdav_url = protocols.get('webdav')
|
||||||
|
if not webdav_url:
|
||||||
|
logger.error('Can not get webdav url from {}'.format(ocm_provider_url))
|
||||||
|
logger.error(resp.content)
|
||||||
|
error_msg = 'Internal Server Error'
|
||||||
|
return api_error(501, error_msg)
|
||||||
|
|
||||||
|
# download file via webdav
|
||||||
|
full_webdav_url = remote_domain + webdav_url
|
||||||
|
|
||||||
|
def format_string(string):
|
||||||
|
return string + (4 - len(string) % 4) * ':'
|
||||||
|
|
||||||
|
shared_secret = share.shared_secret
|
||||||
|
token = base64.b64encode('{}'.format(format_string(shared_secret)).encode('utf-8'))
|
||||||
|
headers = {"Authorization": "Basic {}".format(token.decode('utf-8'))}
|
||||||
|
download_file_resp = requests.get(full_webdav_url, headers=headers)
|
||||||
|
|
||||||
|
response = HttpResponse(download_file_resp.content, content_type="application/octet-stream")
|
||||||
|
response['Content-Disposition'] = 'attachment; filename={}'.format(share.name)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationsView(APIView):
|
||||||
|
|
||||||
|
throttle_classes = (UserRateThrottle,)
|
||||||
|
|
||||||
|
def post(self, request):
|
||||||
|
"""
|
||||||
|
Receive notification from remote server.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not ENABLE_OCM_VIA_WEBDAV:
|
||||||
|
error_msg = 'OCM via webdav feature is not enabled.'
|
||||||
|
return api_error(501, error_msg)
|
||||||
|
|
||||||
|
# {'notification': {'messgage': 'file is no longer shared with you',
|
||||||
|
# 'sharedSecret': 'QoVQuBhqphvVYvz'},
|
||||||
|
# 'notificationType': 'SHARE_UNSHARED',
|
||||||
|
# 'providerId': '13',
|
||||||
|
# 'resourceType': 'file'}
|
||||||
|
|
||||||
|
notification_type = request.data.get('notificationType')
|
||||||
|
notification_dict = request.data.get('notification')
|
||||||
|
shared_secret = notification_dict.get('sharedSecret')
|
||||||
|
provider_id = notification_dict.get('providerId')
|
||||||
|
|
||||||
|
error_result_not_found = {
|
||||||
|
"message": "RESOURCE_NOT_FOUND",
|
||||||
|
"validationErrors": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"message": "NOT_FOUND"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
if notification_type == 'SHARE_UNSHARED':
|
||||||
|
|
||||||
|
try:
|
||||||
|
share = ReceivedShares.objects.get(shared_secret=shared_secret)
|
||||||
|
except ReceivedShares.DoesNotExist:
|
||||||
|
error_msg = "OCM share with secret {} not found.".format(shared_secret)
|
||||||
|
error_result_not_found['validationErrors']['name'] = 'sharedSecret'
|
||||||
|
return Response(error_result_not_found, status=400)
|
||||||
|
|
||||||
|
if share.provider_id != provider_id:
|
||||||
|
error_msg = "OCM share with provider id {} not found.".format(provider_id)
|
||||||
|
error_result_not_found['validationErrors']['name'] = 'providerID'
|
||||||
|
return Response(error_result_not_found, status=400)
|
||||||
|
|
||||||
|
share.delete()
|
||||||
|
|
||||||
|
return Response({}, status=status.HTTP_201_CREATED)
|
4
seahub/ocm_via_webdav/settings.py
Normal file
4
seahub/ocm_via_webdav/settings.py
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
ENABLE_OCM_VIA_WEBDAV = getattr(settings, 'ENABLE_OCM_VIA_WEBDAV', False)
|
||||||
|
OCM_VIA_WEBDAV_ENDPOINT = getattr(settings, 'OCM_VIA_WEBDAV_ENDPOINT', 'ocm-via-webdav')
|
17
seahub/ocm_via_webdav/urls.py
Normal file
17
seahub/ocm_via_webdav/urls.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from django.urls import path
|
||||||
|
|
||||||
|
from seahub.views import react_fake_view
|
||||||
|
from seahub.ocm_via_webdav.ocm_api import SharesView, ReceivedSharesView, \
|
||||||
|
ReceivedShareView, DownloadReceivedFileView, NotificationsView
|
||||||
|
|
||||||
|
urlpatterns = [
|
||||||
|
|
||||||
|
path(r'', react_fake_view, name="ocm_via_webdav"),
|
||||||
|
|
||||||
|
path('shares', SharesView.as_view(), name='ocm-via-webdav-shares'),
|
||||||
|
path('notifications', NotificationsView.as_view(), name='ocm-via-webdav-notifications'),
|
||||||
|
|
||||||
|
path('received-shares/', ReceivedSharesView.as_view(), name='ocm-via-webdav-received-shares'),
|
||||||
|
path('received-shares/<int:share_id>/', ReceivedShareView.as_view(), name='ocm-via-webdav-received-share'),
|
||||||
|
path('download-received-file/', DownloadReceivedFileView.as_view(), name='ocm-via-webdav-download-received-file'),
|
||||||
|
]
|
@@ -263,7 +263,7 @@ INSTALLED_APPS = [
|
|||||||
'seahub.abuse_reports',
|
'seahub.abuse_reports',
|
||||||
'seahub.repo_auto_delete',
|
'seahub.repo_auto_delete',
|
||||||
'seahub.ocm',
|
'seahub.ocm',
|
||||||
|
'seahub.ocm_via_webdav',
|
||||||
'seahub.search',
|
'seahub.search',
|
||||||
'seahub.sysadmin_extra',
|
'seahub.sysadmin_extra',
|
||||||
'seahub.organizations',
|
'seahub.organizations',
|
||||||
|
@@ -106,6 +106,7 @@
|
|||||||
thumbnailSizeForOriginal: {{ thumbnail_size_for_original }},
|
thumbnailSizeForOriginal: {{ thumbnail_size_for_original }},
|
||||||
repoPasswordMinLength: {{repo_password_min_length}},
|
repoPasswordMinLength: {{repo_password_min_length}},
|
||||||
canAddPublicRepo: {% if can_add_public_repo %} true {% else %} false {% endif %},
|
canAddPublicRepo: {% if can_add_public_repo %} true {% else %} false {% endif %},
|
||||||
|
enableOCMViaWebdav: {% if enable_ocm_via_webdav %} true {% else %} false {% endif %},
|
||||||
enableOCM: {% if enable_ocm %} true {% else %} false {% endif %},
|
enableOCM: {% if enable_ocm %} true {% else %} false {% endif %},
|
||||||
ocmRemoteServers: (function () {
|
ocmRemoteServers: (function () {
|
||||||
var servers = [];
|
var servers = [];
|
||||||
|
@@ -106,6 +106,8 @@ from seahub.api2.endpoints.ocm_repos import OCMReposDirView, OCMReposDownloadLin
|
|||||||
OCMReposUploadLinkView
|
OCMReposUploadLinkView
|
||||||
from seahub.api2.endpoints.custom_share_permissions import CustomSharePermissionsView, CustomSharePermissionView
|
from seahub.api2.endpoints.custom_share_permissions import CustomSharePermissionsView, CustomSharePermissionView
|
||||||
|
|
||||||
|
from seahub.ocm_via_webdav.ocm_api import OCMProviderView
|
||||||
|
|
||||||
from seahub.api2.endpoints.repo_share_links import RepoShareLinks, RepoShareLink
|
from seahub.api2.endpoints.repo_share_links import RepoShareLinks, RepoShareLink
|
||||||
from seahub.api2.endpoints.repo_upload_links import RepoUploadLinks, RepoUploadLink
|
from seahub.api2.endpoints.repo_upload_links import RepoUploadLinks, RepoUploadLink
|
||||||
|
|
||||||
@@ -194,6 +196,7 @@ urlpatterns = [
|
|||||||
url(r'^shib-login/', shib_login, name="shib_login"),
|
url(r'^shib-login/', shib_login, name="shib_login"),
|
||||||
url(r'^oauth/', include('seahub.oauth.urls')),
|
url(r'^oauth/', include('seahub.oauth.urls')),
|
||||||
url(r'^thirdparty-editor/', include('seahub.thirdparty_editor.urls')),
|
url(r'^thirdparty-editor/', include('seahub.thirdparty_editor.urls')),
|
||||||
|
url(r'^ocm-via-webdav/', include('seahub.ocm_via_webdav.urls')),
|
||||||
|
|
||||||
url(r'^$', react_fake_view, name='libraries'),
|
url(r'^$', react_fake_view, name='libraries'),
|
||||||
url(r'^robots\.txt$', TemplateView.as_view(template_name='robots.txt', content_type='text/plain')),
|
url(r'^robots\.txt$', TemplateView.as_view(template_name='robots.txt', content_type='text/plain')),
|
||||||
@@ -474,7 +477,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
## user::ocm
|
## user::ocm
|
||||||
# ocm inter-server api, interact with other server
|
# ocm inter-server api, interact with other server
|
||||||
url(r'ocm-provider/$', OCMProtocolView.as_view(), name='api-v2.1-ocm-protocol'),
|
url(r'ocm-provider/$', OCMProviderView.as_view(), name='api-v2.1-ocm-protocol'),
|
||||||
url(r'' + OCM_ENDPOINT + 'shares/$', OCMSharesView.as_view(), name='api-v2.1-ocm-shares'),
|
url(r'' + OCM_ENDPOINT + 'shares/$', OCMSharesView.as_view(), name='api-v2.1-ocm-shares'),
|
||||||
url(r'' + OCM_ENDPOINT + 'notifications/$', OCMNotificationsView.as_view(), name='api-v2.1-ocm-notifications'),
|
url(r'' + OCM_ENDPOINT + 'notifications/$', OCMNotificationsView.as_view(), name='api-v2.1-ocm-notifications'),
|
||||||
|
|
||||||
|
@@ -61,6 +61,7 @@ from seahub.settings import AVATAR_FILE_STORAGE, \
|
|||||||
from seahub.wopi.settings import ENABLE_OFFICE_WEB_APP
|
from seahub.wopi.settings import ENABLE_OFFICE_WEB_APP
|
||||||
from seahub.onlyoffice.settings import ONLYOFFICE_DESKTOP_EDITORS_PORTAL_LOGIN
|
from seahub.onlyoffice.settings import ONLYOFFICE_DESKTOP_EDITORS_PORTAL_LOGIN
|
||||||
from seahub.ocm.settings import ENABLE_OCM, OCM_REMOTE_SERVERS
|
from seahub.ocm.settings import ENABLE_OCM, OCM_REMOTE_SERVERS
|
||||||
|
from seahub.ocm_via_webdav.settings import ENABLE_OCM_VIA_WEBDAV
|
||||||
from seahub.constants import HASH_URLS, PERMISSION_READ
|
from seahub.constants import HASH_URLS, PERMISSION_READ
|
||||||
from seahub.group.settings import GROUP_IMPORT_MEMBERS_EXTRA_MSG
|
from seahub.group.settings import GROUP_IMPORT_MEMBERS_EXTRA_MSG
|
||||||
|
|
||||||
@@ -1201,6 +1202,7 @@ def react_fake_view(request, **kwargs):
|
|||||||
'additional_share_dialog_note': ADDITIONAL_SHARE_DIALOG_NOTE,
|
'additional_share_dialog_note': ADDITIONAL_SHARE_DIALOG_NOTE,
|
||||||
'additional_app_bottom_links': ADDITIONAL_APP_BOTTOM_LINKS,
|
'additional_app_bottom_links': ADDITIONAL_APP_BOTTOM_LINKS,
|
||||||
'additional_about_dialog_links': ADDITIONAL_ABOUT_DIALOG_LINKS,
|
'additional_about_dialog_links': ADDITIONAL_ABOUT_DIALOG_LINKS,
|
||||||
|
'enable_ocm_via_webdav': ENABLE_OCM_VIA_WEBDAV,
|
||||||
'enable_ocm': ENABLE_OCM,
|
'enable_ocm': ENABLE_OCM,
|
||||||
'ocm_remote_servers': OCM_REMOTE_SERVERS,
|
'ocm_remote_servers': OCM_REMOTE_SERVERS,
|
||||||
'enable_share_to_department': settings.ENABLE_SHARE_TO_DEPARTMENT,
|
'enable_share_to_department': settings.ENABLE_SHARE_TO_DEPARTMENT,
|
||||||
|
Reference in New Issue
Block a user