1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-08-01 23:38:37 +00:00

modify code

This commit is contained in:
王健辉 2019-06-24 20:31:00 +08:00
parent 7374b1a4b9
commit a888efb78a
5 changed files with 212 additions and 275 deletions

View File

@ -1,12 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
import Select from 'react-select';
import makeAnimated from 'react-select/lib/animated';
import { seafileAPI } from '../../utils/seafile-api.js';
import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Alert } from 'reactstrap';
import { gettext } from '../../utils/constants';
const propTypes = {
createTable: PropTypes.func.isRequired,
onAddTable: PropTypes.func.isRequired,
createDTable: PropTypes.func.isRequired,
onAddDTable: PropTypes.func.isRequired,
};
class CreateTableDialog extends React.Component {
@ -16,6 +19,8 @@ class CreateTableDialog extends React.Component {
tableName: '',
errMessage: '',
isSubmitBtnActive: false,
selectedOption: null,
options:[],
};
this.newInput = React.createRef();
}
@ -23,6 +28,26 @@ class CreateTableDialog extends React.Component {
componentDidMount() {
this.newInput.focus();
this.newInput.setSelectionRange(0,0);
let options = [];
seafileAPI.getAccountInfo().then((res) => {
let obj = {};
console.log(res);
obj.value = 'Personal';
obj.email = res.data.email;
obj.label = 'Personal';
options.push(obj);
seafileAPI.listGroups().then((res) => {
for (let i = 0 ; i < res.data.length; i++) {
let obj = {};
obj.value = res.data[i].name;
obj.email = res.data[i].id + '@seafile_group';
obj.label = res.data[i].name;
options.push(obj);
}
this.setState({options: options});
});
});
}
handleChange = (e) => {
@ -37,6 +62,10 @@ class CreateTableDialog extends React.Component {
}) ;
}
handleSelectChange = (option) => {
this.setState({selectedOption: option});
}
handleSubmit = () => {
if (!this.state.isSubmitBtnActive) {
return;
@ -45,7 +74,8 @@ class CreateTableDialog extends React.Component {
let isValid = this.validateInputParams();
if (isValid) {
let newName = this.state.tableName.trim();
this.props.createTable(newName);
let owner = this.state.selectedOption;
this.props.createDTable(newName, owner.email);
}
}
@ -57,7 +87,7 @@ class CreateTableDialog extends React.Component {
}
toggle = () => {
this.props.onAddTable();
this.props.onAddDTable();
}
validateInputParams = () => {
@ -94,6 +124,17 @@ class CreateTableDialog extends React.Component {
</FormGroup>
</Form>
{this.state.errMessage && <Alert color="danger" className="mt-2">{this.state.errMessage}</Alert>}
<Label>{gettext('Belong to')}</Label>
<Select
isClearable
isMulti={false}
maxMenuHeight={200}
hideSelectedOptions={true}
components={makeAnimated()}
placeholder=''
options={this.state.options}
onChange={this.handleSelectChange}
/>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>

View File

@ -1,85 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import Select from 'react-select';
import makeAnimated from 'react-select/lib/animated';
import { Button, Modal, ModalHeader, ModalBody, ModalFooter } from 'reactstrap';
import { gettext } from '../../utils/constants';
import { seafileAPI } from '../../utils/seafile-api.js';
const propTypes = {
createWorkspace: PropTypes.func.isRequired,
onAddWorkspace: PropTypes.func.isRequired,
};
const NoOptionsMessage = (props) => {
return (
<div {...props.innerProps} style={{margin: '6px 10px', textAlign: 'center', color: 'hsl(0,0%,50%)'}}>{gettext('Group not found')}</div>
);
};
class CreateWorkspaceDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedOption: null,
options:[],
};
}
componentDidMount() {
seafileAPI.listGroups().then((res) => {
let options = [];
for (let i = 0 ; i < res.data.length; i++) {
let obj = {};
obj.value = res.data[i].name;
obj.email = res.data[i].id + '@seafile_group';
obj.label = res.data[i].name;
options.push(obj);
}
this.setState({options: options});
});
}
handleSelectChange = (option) => {
this.setState({selectedOption: option});
}
submit = () => {
let owner = this.state.selectedOption;
this.props.createWorkspace(owner.email);
}
toggle = () => {
this.props.onAddWorkspace();
}
render() {
return (
<Modal isOpen={true} toggle={this.toggle}>
<ModalHeader toggle={this.toggle}>{gettext('New DTable')}</ModalHeader>
<ModalBody>
<Select
isClearable
isMulti={false}
maxMenuHeight={200}
hideSelectedOptions={true}
components={makeAnimated()}
placeholder={gettext('Select a owner')}
options={this.state.options}
onChange={this.handleSelectChange}
components={{ NoOptionsMessage }}
/>
</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={this.toggle}>{gettext('Cancel')}</Button>
<Button color="primary" onClick={this.submit}>{gettext('Submit')}</Button>
</ModalFooter>
</Modal>
);
}
}
CreateWorkspaceDialog.propTypes = propTypes;
export default CreateWorkspaceDialog;

View File

@ -7,7 +7,6 @@ import { seafileAPI } from '../../utils/seafile-api';
import { gettext, siteRoot } from '../../utils/constants';
import CommonToolbar from '../../components/toolbar/common-toolbar';
import Loading from '../../components/loading';
import CreateWorkspaceDialog from '../../components/dialog/create-workspace-dialog';
import CreateTableDialog from '../../components/dialog/create-table-dialog';
import DeleteTableDialog from '../../components/dialog/delete-table-dialog';
import Rename from '../../components/rename';
@ -156,7 +155,6 @@ class Workspace extends Component {
this.state = {
tableList: this.props.workspace.table_list,
errorMsg: '',
isShowAddTableDialog: false,
isItemFreezed: false,
};
}
@ -169,25 +167,6 @@ class Workspace extends Component {
this.setState({isItemFreezed: false});
}
onAddTable = () => {
this.setState({
isShowAddTableDialog: !this.state.isShowAddTableDialog,
});
}
createTable = (tableName) => {
let workspaceID = this.props.workspace.id;
seafileAPI.createTable(workspaceID, tableName).then((res) => {
this.state.tableList.push(res.data.table);
this.setState({tableList: this.state.tableList});
}).catch((error) => {
if(error.response) {
this.setState({errorMsg: gettext('Error')});
}
});
this.onAddTable();
}
renameTable = (oldTableName, newTableName) => {
let workspaceID = this.props.workspace.id;
seafileAPI.renameTable(workspaceID, oldTableName, newTableName).then((res) => {
@ -233,18 +212,6 @@ class Workspace extends Component {
return(
<div className="workspace my-2">
<div>{workspace.owner_name}</div>
<div className="d-flex add-table-btn-container">
<div className="add-table-btn cursor-pointer" onClick={this.onAddTable}>
<i className="fa fa-plus"></i>
</div>
{this.state.isShowAddTableDialog &&
<CreateTableDialog
onAddTable={this.onAddTable}
createTable={this.createTable}
/>
}
<span>{gettext('Add a DTable')}</span>
</div>
<table width="100%" className="table-vcenter">
<colgroup>
<col width="4%"/><col width="31%"/><col width="30%"/><col width="27%"/><col width="8%"/>
@ -286,29 +253,28 @@ class DTable extends Component {
loading: true,
errorMsg: '',
workspaceList: [],
isShowAddWorkspaceDialog: false,
isShowAddDTableDialog: false,
};
}
onAddWorkspace = () => {
onAddDTable = () => {
this.setState({
isShowAddWorkspaceDialog: !this.state.isShowAddWorkspaceDialog,
isShowAddDTableDialog: !this.state.isShowAddDTableDialog,
});
}
createWorkspace = (owner) => {
seafileAPI.createWorkspace(owner).then((res) => {
this.state.workspaceList.push(res.data.workspace);
this.setState({workspaceList: this.state.workspaceList});
createDTable = (tableName, owner) => {
seafileAPI.createTable(tableName, owner).then(() => {
this.listWorkspaces();
}).catch((error) => {
if(error.response) {
this.setState({errorMsg: gettext('Error')});
}
});
this.onAddWorkspace();
this.onAddDTable();
}
componentDidMount() {
listWorkspaces = () => {
seafileAPI.listWorkspaces().then((res) => {
this.setState({
loading: false,
@ -329,6 +295,10 @@ class DTable extends Component {
});
}
componentDidMount() {
this.listWorkspaces();
}
render() {
return (
<Fragment>
@ -338,10 +308,10 @@ class DTable extends Component {
<div className="operation">
<Fragment>
<MediaQuery query="(min-width: 768px)">
<Button className="btn btn-secondary operation-item" onClick={this.onAddWorkspace}>{gettext('New DTable')}</Button>
<Button className="btn btn-secondary operation-item" onClick={this.onAddDTable}>{gettext('New DTable')}</Button>
</MediaQuery>
<MediaQuery query="(max-width: 767.8px)">
<Button className="btn btn-secondary operation-item my-1" onClick={this.onAddWorkspace}>{gettext('New DTable')}</Button>
<Button className="btn btn-secondary operation-item my-1" onClick={this.onAddDTable}>{gettext('New DTable')}</Button>
</MediaQuery>
</Fragment>
</div>
@ -365,16 +335,14 @@ class DTable extends Component {
<Workspace
key={index}
workspace={workspace}
renameWorkspace={this.renameWorkspace}
deleteWorkspace={this.deleteWorkspace}
/>
);
})}
<div className="my-2">
{this.state.isShowAddWorkspaceDialog &&
<CreateWorkspaceDialog
createWorkspace={this.createWorkspace}
onAddWorkspace={this.onAddWorkspace}
{this.state.isShowAddDTableDialog &&
<CreateTableDialog
createDTable={this.createDTable}
onAddDTable={this.onAddDTable}
/>
}
</div>

View File

@ -23,7 +23,7 @@ from seahub.dtable.models import Workspaces
from seahub.tags.models import FileUUIDMap
from seahub.base.templatetags.seahub_tags import email2nickname
from seahub.utils.timeutils import timestamp_to_isoformat_timestr
from seahub.group.utils import group_id_to_name
from seahub.group.utils import group_id_to_name, is_group_member
from seahub.utils import is_valid_dirent_name, is_org_context, normalize_file_path, \
check_filename_with_rename, render_error, render_permission_error, gen_file_upload_url, \
gen_file_get_url, get_file_type_and_ext, IMAGE
@ -59,14 +59,14 @@ class WorkspacesView(APIView):
else:
groups = ccnet_api.get_groups(username, return_ancestors=True)
owners = list()
owners.append(username)
owner_list = list()
owner_list.append(username)
for group in groups:
group_user = '%s@seafile_group' % group.id
owners.append(group_user)
owner_list.append(group_user)
workspace_list = list()
for owner in owners:
for owner in owner_list:
try:
workspace = Workspaces.objects.get_workspace_by_owner(owner)
except Exception as e:
@ -111,94 +111,133 @@ class WorkspacesView(APIView):
logger.warning('Library %s not found.' % repo_id)
continue
if '@seafile_group' in owner:
group_id = int(owner.split('@')[0])
owner_name = group_id_to_name(group_id)
else:
owner_name = email2nickname(owner)
table_objs = seafile_api.list_dir_by_path(repo_id, '/')
table_list = list()
for table_obj in table_objs:
if table_obj.obj_name[-7:] != FILE_TYPE:
continue
table = dict()
table["name"] = table_obj.obj_name[:-7]
table["mtime"] = timestamp_to_isoformat_timestr(table_obj.mtime)
table["modifier"] = email2nickname(table_obj.modifier) if table_obj.modifier else email2nickname(owner)
table_file = seafile_api.get_dirent_by_path(repo_id, '/'+table_obj.obj_name)
table["name"] = table_file.obj_name[:-7]
table["mtime"] = timestamp_to_isoformat_timestr(table_file.mtime)
table["modifier"] = email2nickname(table_file.modifier)
table_list.append(table)
res = workspace.to_dict()
if '@seafile_group' in owner:
group_id = int(owner.split('@')[0])
owner_name = group_id_to_name(group_id)
else:
owner_name = email2nickname(owner)
res["owner_name"] = owner_name
res["table_list"] = table_list
res["updated_at"] = workspace.updated_at
workspace_list.append(res)
resp = dict()
resp["workspace_list"] = workspace_list
return Response({"workspace_list": workspace_list}, status=status.HTTP_200_OK)
return Response(resp, status=status.HTTP_200_OK)
class DTablesView(APIView):
def post(self, request):
"""create a workspace
"""create a table file
"""
# argument check
owner = request.POST.get('owner')
if not owner:
table_owner = request.POST.get('owner')
if not table_owner:
error_msg = 'owner invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
table_name = request.POST.get('name')
if not table_name:
error_msg = 'name invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
table_file_name = table_name + FILE_TYPE
if not is_valid_dirent_name(table_file_name):
error_msg = 'name invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# resource check
workspace = Workspaces.objects.get_workspace_by_owner(owner)
if workspace:
return Response({'success': True}, status=status.HTTP_200_OK)
workspace = Workspaces.objects.get_workspace_by_owner(table_owner)
if not workspace:
if not request.user.permissions.can_add_repo():
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
org_id = -1
if is_org_context(request):
org_id = request.user.org.org_id
try:
if org_id > 0:
repo_id = seafile_api.create_org_repo(
_("My Workspace"),
_("My Workspace"),
"dtable@seafile",
org_id
)
else:
repo_id = seafile_api.create_repo(
_("My Workspace"),
_("My Workspace"),
"dtable@seafile"
)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error.'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
try:
workspace = Workspaces.objects.create_workspace(table_owner, repo_id)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error.'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
repo_id = workspace.repo_id
repo = seafile_api.get_repo(repo_id)
if not repo:
error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# permission check
if not request.user.permissions.can_add_repo():
error_msg = 'You do not have permission to create workspace.'
username = request.user.username
if '@seafile_group' in table_owner:
group_id = int(table_owner.split('@')[0])
if not is_group_member(group_id, username):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
else:
if username != table_owner:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
# repo status check
repo_status = repo.status
if repo_status != 0:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
org_id = -1
if is_org_context(request):
org_id = request.user.org.org_id
# create new empty table
table_file_name = check_filename_with_rename(repo_id, '/', table_file_name)
try:
if org_id > 0:
repo_id = seafile_api.create_org_repo(
_("My Workspace"),
_("My Workspace"),
"dtable@seafile",
org_id
)
else:
repo_id = seafile_api.create_repo(
_("My Workspace"),
_("My Workspace"),
"dtable@seafile"
)
except Exception as e:
seafile_api.post_empty_file(repo_id, '/', table_file_name, username)
except SearpcError, e:
logger.error(e)
error_msg = 'Internal Server Error.'
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
try:
workspace = Workspaces.objects.create_workspace(owner, repo_id)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error.'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
table_path = normalize_file_path(table_file_name)
table_obj = seafile_api.get_dirent_by_path(repo_id, table_path)
table = dict()
table["name"] = table_obj.obj_name[:-7]
table["mtime"] = timestamp_to_isoformat_timestr(table_obj.mtime)
table["modifier"] = email2nickname(table_obj.modifier)
if '@seafile_group' in owner:
group_id = int(owner.split('@')[0])
owner_name = group_id_to_name(group_id)
else:
owner_name = email2nickname(owner)
res = workspace.to_dict()
res["owner_name"] = owner_name
res["table_list"] = []
res["updated_at"] = workspace.updated_at
return Response({"workspace": res}, status=status.HTTP_201_CREATED)
return Response({"table": table}, status=status.HTTP_201_CREATED)
class DTableView(APIView):
@ -243,9 +282,15 @@ class DTableView(APIView):
# permission check
username = request.user.username
owner = workspace.owner
if username != owner:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
if '@seafile_group' in owner:
group_id = int(owner.split('@')[0])
if not is_group_member(group_id, username):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
else:
if username != owner:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
# send stats message
send_file_access_msg(request, repo, table_path, 'api')
@ -254,64 +299,6 @@ class DTableView(APIView):
use_onetime = False if reuse == '1' else True
return get_repo_file(request, repo_id, table_file_id, table_file_name, op, use_onetime)
def post(self, request, workspace_id):
"""create a table file
"""
# argument check
table_name = request.POST.get('name')
if not table_name:
error_msg = 'name invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
table_file_name = table_name + FILE_TYPE
if not is_valid_dirent_name(table_file_name):
error_msg = 'name invalid.'
return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
# resource check
workspace = Workspaces.objects.get_workspace_by_id(workspace_id)
if not workspace:
error_msg = 'Workspace %s not found.' % workspace_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
repo_id = workspace.repo_id
repo = seafile_api.get_repo(repo_id)
if not repo:
error_msg = 'Library %s not found.' % repo_id
return api_error(status.HTTP_404_NOT_FOUND, error_msg)
# permission check
username = request.user.username
owner = workspace.owner
if username != owner:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
# repo status check
repo_status = repo.status
if repo_status != 0:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
# create new empty table
table_file_name = check_filename_with_rename(repo_id, '/', table_file_name)
try:
seafile_api.post_empty_file(repo_id, '/', table_file_name, owner)
except SearpcError, e:
logger.error(e)
error_msg = 'Internal Server Error'
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
table_path = normalize_file_path(table_file_name)
table_obj = seafile_api.get_dirent_by_path(repo_id, table_path)
table = dict()
table["name"] = table_obj.obj_name[:-7]
table["mtime"] = timestamp_to_isoformat_timestr(table_obj.mtime)
table["modifier"] = email2nickname(table_obj.modifier) if table_obj.modifier else email2nickname(owner)
return Response({"table": table}, status=status.HTTP_201_CREATED)
def put(self, request, workspace_id):
"""rename a table
"""
@ -357,9 +344,15 @@ class DTableView(APIView):
# permission check
username = request.user.username
owner = workspace.owner
if username != owner:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
if '@seafile_group' in owner:
group_id = int(owner.split('@')[0])
if not is_group_member(group_id, username):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
else:
if username != owner:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
# repo status check
repo_status = repo.status
@ -370,7 +363,7 @@ class DTableView(APIView):
# rename table
new_table_file_name = check_filename_with_rename(repo_id, '/', new_table_file_name)
try:
seafile_api.rename_file(repo_id, '/', old_table_file_name, new_table_file_name, owner)
seafile_api.rename_file(repo_id, '/', old_table_file_name, new_table_file_name, username)
except SearpcError as e:
logger.error(e)
error_msg = 'Internal Server Error'
@ -381,7 +374,7 @@ class DTableView(APIView):
table = dict()
table["name"] = table_obj.obj_name[:-7]
table["mtime"] = timestamp_to_isoformat_timestr(table_obj.mtime)
table["modifier"] = email2nickname(table_obj.modifier) if table_obj.modifier else email2nickname(owner)
table["modifier"] = email2nickname(table_obj.modifier)
return Response({"table": table}, status=status.HTTP_200_OK)
@ -415,9 +408,15 @@ class DTableView(APIView):
# permission check
username = request.user.username
owner = workspace.owner
if username != owner:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
if '@seafile_group' in owner:
group_id = int(owner.split('@')[0])
if not is_group_member(group_id, username):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
else:
if username != owner:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
# repo status check
repo_status = repo.status
@ -477,13 +476,19 @@ class DTableUpdateLinkView(APIView):
# permission check
username = request.user.username
owner = workspace.owner
if username != owner:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
if '@seafile_group' in owner:
group_id = int(owner.split('@')[0])
if not is_group_member(group_id, username):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
else:
if username != owner:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
try:
token = seafile_api.get_fileserver_access_token(repo_id, 'dummy', 'update',
owner, use_onetime=False)
username, use_onetime=False)
except Exception as e:
logger.error(e)
error_msg = 'Internal Server Error'
@ -575,8 +580,15 @@ def dtable_file_view(request, workspace_id, name):
# permission check
username = request.user.username
owner = workspace.owner
if username != owner:
return render_permission_error(request, _(u'Unable to view file'))
if '@seafile_group' in owner:
group_id = int(owner.split('@')[0])
if not is_group_member(group_id, username):
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
else:
if username != owner:
error_msg = 'Permission denied.'
return api_error(status.HTTP_403_FORBIDDEN, error_msg)
return_dict = {
'share_link_expire_days_default': SHARE_LINK_EXPIRE_DAYS_DEFAULT,

View File

@ -88,7 +88,7 @@ from seahub.api2.endpoints.related_files import RelatedFilesView, RelatedFileVie
from seahub.api2.endpoints.webdav_secret import WebdavSecretView
from seahub.api2.endpoints.starred_items import StarredItems
from seahub.api2.endpoints.markdown_lint import MarkdownLintView
from seahub.api2.endpoints.dtable import WorkspacesView, DTableView, \
from seahub.api2.endpoints.dtable import WorkspacesView, DTableView, DTablesView, \
DTableUpdateLinkView, DTableAssetUploadLinkView, dtable_file_view, dtable_asset_access
# Admin
@ -359,6 +359,7 @@ urlpatterns = [
# user: workspaces
url(r'^api/v2.1/workspaces/$', WorkspacesView.as_view(), name='api-v2.1-workspaces'),
url(r'^api/v2.1/dtables/$', DTablesView.as_view(), name='api-v2.1-dtables'),
url(r'^api/v2.1/workspace/(?P<workspace_id>\d+)/dtable/$', DTableView.as_view(), name='api-v2.1-workspace-dtable'),
url(r'^api/v2.1/workspace/(?P<workspace_id>\d+)/dtable-update-link/$', DTableUpdateLinkView.as_view(), name='api-v2.1-workspace-dtable-update-link'),
url(r'^api/v2.1/workspace/(?P<workspace_id>\d+)/dtable-asset-upload-link/$', DTableAssetUploadLinkView.as_view(), name='api-v2.1-workspace-dtable-asset-upload-link'),