diff --git a/frontend/src/app.js b/frontend/src/app.js
index f34b85d0bf..684f90ae24 100644
--- a/frontend/src/app.js
+++ b/frontend/src/app.js
@@ -12,6 +12,7 @@ import DraftsView from './pages/drafts/drafts-view';
import DraftContent from './pages/drafts/draft-content';
import FilesActivities from './pages/dashboard/files-activities';
import Starred from './pages/starred/starred';
+import DTable from './pages/dtable/dtable';
import LinkedDevices from './pages/linked-devices/linked-devices';
import editUtilties from './utils/editor-utilties';
import ShareAdminLibraries from './pages/share-admin/libraries';
@@ -36,6 +37,7 @@ import './css/search.css';
const FilesActivitiesWrapper = MainContentWrapper(FilesActivities);
const DraftsViewWrapper = MainContentWrapper(DraftsView);
const StarredWrapper = MainContentWrapper(Starred);
+const DTableWrapper = MainContentWrapper(DTable);
const LinkedDevicesWrapper = MainContentWrapper(LinkedDevices);
const SharedLibrariesWrapper = MainContentWrapper(SharedLibraries);
const ShareAdminLibrariesWrapper = MainContentWrapper(ShareAdminLibraries);
@@ -233,6 +235,7 @@ class App extends Component {
/>
+
diff --git a/frontend/src/components/dialog/create-workspace-dialog.js b/frontend/src/components/dialog/create-workspace-dialog.js
new file mode 100644
index 0000000000..4f0bb69f3e
--- /dev/null
+++ b/frontend/src/components/dialog/create-workspace-dialog.js
@@ -0,0 +1,102 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Button, Modal, ModalHeader, Input, ModalBody, ModalFooter, Form, FormGroup, Label, Alert } from 'reactstrap';
+import { gettext } from '../../utils/constants';
+
+
+const propTypes = {
+ createWorkSpace: PropTypes.func.isRequired,
+ onCreateToggle: PropTypes.func.isRequired,
+};
+
+class CreateWorkSpaceDialog extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ workspaceName: '',
+ errMessage: '',
+ isSubmitBtnActive: false,
+ };
+ this.newInput = React.createRef();
+ }
+
+ handleNameChange = (e) => {
+ if (!e.target.value.trim()) {
+ this.setState({isSubmitBtnActive: false});
+ } else {
+ this.setState({isSubmitBtnActive: true});
+ }
+
+ this.setState({workspaceName: e.target.value});
+ }
+
+ handleSubmit = () => {
+ let isValid= this.validateInputParams();
+ if (isValid) {
+ let workspaceName = this.state.workspaceName.trim();
+ this.props.createWorkSpace(workspaceName);
+ }
+ }
+
+ handleKeyPress = (e) => {
+ if (e.key === 'Enter') {
+ this.handleSubmit();
+ e.preventDefault();
+ }
+ }
+
+ toggle = () => {
+ this.props.onCreateToggle();
+ }
+
+ componentDidMount() {
+ this.newInput.focus();
+ }
+
+ validateInputParams() {
+ let errMessage = '';
+ let workspaceName = this.state.workspaceName.trim();
+ if (!workspaceName.length) {
+ errMessage = gettext('Name is required');
+ this.setState({errMessage: errMessage});
+ return false;
+ }
+ if (workspaceName.indexOf('/') > -1) {
+ errMessage = gettext('Name should not include \'/\'.');
+ this.setState({errMessage: errMessage});
+ return false;
+ }
+ return true;
+ }
+
+ render() {
+ return (
+
+ {gettext('New Library')}
+
+
+ {this.state.errMessage && {this.state.errMessage}}
+
+
+
+
+
+
+ );
+ }
+}
+
+CreateWorkSpaceDialog.propTypes = propTypes;
+
+export default CreateWorkSpaceDialog;
diff --git a/frontend/src/components/main-side-nav.js b/frontend/src/components/main-side-nav.js
index 1c6cd0ac78..b31c632fe1 100644
--- a/frontend/src/components/main-side-nav.js
+++ b/frontend/src/components/main-side-nav.js
@@ -182,6 +182,16 @@ class MainSideNav extends React.Component {
+
{gettext('Database')}
+
+ -
+ this.tabItemClick('dtable')}>
+
+ {gettext('DTable')}
+
+
+
+
{gettext('Tools')}
-
diff --git a/frontend/src/pages/dtable/dtable.js b/frontend/src/pages/dtable/dtable.js
new file mode 100644
index 0000000000..6deb987e4c
--- /dev/null
+++ b/frontend/src/pages/dtable/dtable.js
@@ -0,0 +1,251 @@
+import React, { Component, Fragment } from 'react';
+import PropTypes from 'prop-types';
+import { Button, ModalHeader, ModalBody, ModalFooter, Alert, Dropdown, DropdownToggle, DropdownMenu, DropdownItem } from 'reactstrap';
+import moment from 'moment';
+import { seafileAPI } from '../../utils/seafile-api';
+import { Utils } from '../../utils/utils';
+import { gettext, siteRoot } from '../../utils/constants';
+import Loading from '../../components/loading';
+import ModalPortal from '../../components/modal-portal';
+import CreateWorkSpaceDialog from '../../components/dialog/create-workspace-dialog';
+
+moment.locale(window.app.config.lang);
+
+
+const itemPropTypes = {
+ item: PropTypes.object.isRequired,
+ renameWorkSpace: PropTypes.func.isRequired,
+ deleteWorkSpace: PropTypes.func.isRequired,
+};
+
+class Item extends Component {
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ dropdownOpen: false,
+ newName: '',
+ };
+ }
+
+ onRenameWorkSpace(workspace) {
+ let name = this.state.newName;
+ this.props.renameWorkSpace(workspace, name);
+ }
+
+ onDeleteWorkSpace(workspace) {
+ this.props.deleteWorkSpace(workspace);
+ }
+
+ dropdownToggle = () => {
+ this.setState({ dropdownOpen: !this.state.dropdownOpen });
+ }
+
+ render() {
+ let item = this.props.item;
+
+ return(
+
+
+
+ {item.name}
+
+
+
+
+ {gettext('Rename')}
+ {gettext('Delete')}
+
+
+ |
+
+ {item.table_list.map((table, index) => {
+ let tableHref = siteRoot + 'lib/' + item.repo_id + '/file' + Utils.encodePath(Utils.joinPath('/', table.name));
+ return (
+
+ |
+ {table.name} |
+ {table.modifier} |
+ {moment(table.mtime).fromNow()} |
+ |
+
+ );
+ })}
+
+ |
+ {gettext('Add a table')} |
+
+
+ );
+ }
+}
+
+Item.propTypes = itemPropTypes;
+
+
+const contentPropTypes = {
+ items: PropTypes.array.isRequired,
+ renameWorkSpace: PropTypes.func.isRequired,
+ deleteWorkSpace: PropTypes.func.isRequired,
+};
+
+class Content extends Component {
+
+ render() {
+ let items = this.props.items;
+
+ return (
+
+
+
+
+
+
+
+
+
+ {items.map((item, index) => {
+ return (
+
+ );
+ })}
+
+
+ );
+ }
+}
+
+Content.propTypes = contentPropTypes;
+
+
+class DTable extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ loading: true,
+ errorMsg: '',
+ items: [],
+ isShowCreateDialog: false,
+ };
+ }
+
+ onCreateToggle = () => {
+ this.setState({
+ isShowCreateDialog: !this.state.isShowCreateDialog,
+ });
+ }
+
+ createWorkSpace = (name) => {
+ seafileAPI.addWorkSpace(name).then((res) => {
+ this.state.items.push(res.data.workspace);
+ this.setState({items: this.state.items});
+ }).catch((error) => {
+ if(error.response) {
+ this.setState({errorMsg: gettext('Error')});
+ }
+ });
+ this.onCreateToggle();
+ }
+
+ renameWorkSpace = (workspace, name) => {
+ seafileAPI.renameWorkSpace(workspace.id, name).then((res) => {
+ let items = this.state.items.map((item) => {
+ if (item.id === workspace.id) {
+ item = res.data;
+ }
+ return item;
+ });
+ this.setState({items: items});
+ }).catch((error) => {
+ if(error.response) {
+ this.setState({errorMsg: gettext('Error')});
+ }
+ });
+ }
+
+ deleteWorkSpace = (workspace) => {
+ seafileAPI.deleteWorkSpace(workspace.id).then(() => {
+ let items = this.state.items.filter(item => {
+ return item.id !== workspace.id;
+ });
+ this.setState({items: items});
+ }).catch((error) => {
+ if(error.response) {
+ this.setState({errorMsg: gettext('Error')});
+ }
+ });
+ }
+
+ componentDidMount() {
+ seafileAPI.listWorkSpaces().then((res) => {
+ this.setState({
+ loading: false,
+ items: res.data.workspace_list,
+ });
+ }).catch((error) => {
+ if (error.response) {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Error')
+ });
+ } else {
+ this.setState({
+ loading: false,
+ errorMsg: gettext('Please check the network.')
+ });
+ }
+ });
+ }
+
+ render() {
+ return (
+
+
+
+
{gettext('DTable')}
+
+
+ {this.state.loading &&
}
+ {(!this.state.loading && this.state.errorMsg) &&
+
{this.state.errorMsg}
+ }
+ {!this.state.loading &&
+
+
+
+
+ {this.state.isShowCreateDialog &&
+
+ }
+
+
+
+ }
+
+
+
+ );
+ }
+}
+
+export default DTable;
diff --git a/seahub/api2/endpoints/dtable.py b/seahub/api2/endpoints/dtable.py
new file mode 100644
index 0000000000..3f167fc82d
--- /dev/null
+++ b/seahub/api2/endpoints/dtable.py
@@ -0,0 +1,394 @@
+# -*- coding: utf-8 -*-
+
+import logging
+
+from rest_framework.views import APIView
+from rest_framework.authentication import SessionAuthentication
+from rest_framework.permissions import IsAuthenticated
+from rest_framework import status
+from rest_framework.response import Response
+
+from pysearpc import SearpcError
+from seaserv import seafile_api, edit_repo
+from seahub.api2.authentication import TokenAuthentication
+from seahub.api2.throttling import UserRateThrottle
+from seahub.api2.utils import api_error
+from seahub.dtable.models import WorkSpaces
+from seahub.base.templatetags.seahub_tags import email2nickname
+from seahub.utils.timeutils import timestamp_to_isoformat_timestr
+from seahub.utils import is_valid_dirent_name, is_org_context, normalize_file_path, check_filename_with_rename
+from seahub.settings import MAX_UPLOAD_FILE_NAME_LEN
+
+
+logger = logging.getLogger(__name__)
+
+
+class WorkSpacesView(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated, )
+ throttle_classes = (UserRateThrottle, )
+
+ def get(self, request):
+ """get all workspaces
+ """
+ owner = request.user.username
+ try:
+ workspaces = WorkSpaces.objects.get_workspaces_by_owner(owner)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error.'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ workspace_list = []
+ for workspace in workspaces:
+ repo_id = workspace.repo_id
+ repo = seafile_api.get_repo(repo_id)
+ if not repo:
+ logger.warning('Library %s not found.' % repo_id)
+ continue
+
+ table_objs = seafile_api.list_dir_by_path(repo_id, '/')
+ table_list = list()
+ for table_obj in table_objs:
+ table = dict()
+ table["name"] = table_obj.obj_name
+ table["mtime"] = timestamp_to_isoformat_timestr(table_obj.mtime)
+ table["modifier"] = email2nickname(table_obj.modifier) if table_obj.modifier else email2nickname(owner)
+ table_list.append(table)
+
+ res = workspace.to_dict()
+ res["table_list"] = table_list
+ res["updated_at"] = workspace.updated_at
+ workspace_list.append(res)
+
+ return Response({"workspace_list": workspace_list}, status=status.HTTP_200_OK)
+
+ def post(self, request):
+ """create a workspace
+ """
+ # argument check
+ name = request.POST.get('name')
+ if not name:
+ error_msg = 'name invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if not is_valid_dirent_name(name):
+ error_msg = 'name invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ # permission check
+ if not request.user.permissions.can_add_repo():
+ error_msg = 'You do not have permission to create workspace.'
+ 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(name, '', "dtable@seafile", org_id)
+ else:
+ repo_id = seafile_api.create_repo(name, '', "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)
+
+ owner = request.user.username
+ try:
+ workspace = WorkSpaces.objects.create_workspace(name, 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)
+
+ return Response({"workspace": workspace.to_dict()}, status=status.HTTP_201_CREATED)
+
+
+class WorkSpaceView(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated, )
+ throttle_classes = (UserRateThrottle, )
+
+ def put(self, request, workspace_id):
+ """rename a workspace
+ """
+ # argument check
+ workspace_name = request.data.get('name')
+ if not workspace_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)
+
+ try:
+ edit_repo(repo_id, workspace_name, '', username)
+ 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.name = workspace_name
+ workspace.save()
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error.'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response({"workspace": workspace.to_dict()}, status=status.HTTP_200_OK)
+
+ def delete(self, request, workspace_id):
+ """delete a workspace
+ """
+ # 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)
+
+ # remove repo
+ try:
+ seafile_api.remove_repo(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)
+
+ try:
+ WorkSpaces.objects.delete_workspace(workspace_id)
+ except Exception as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error.'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response({"success": "true"}, status=status.HTTP_200_OK)
+
+
+class DTableView(APIView):
+
+ authentication_classes = (TokenAuthentication, SessionAuthentication)
+ permission_classes = (IsAuthenticated, )
+ throttle_classes = (UserRateThrottle, )
+
+ 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)
+
+ if not is_valid_dirent_name(table_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_name = check_filename_with_rename(repo_id, '/', table_name)
+
+ try:
+ seafile_api.post_empty_file(repo_id, '/', table_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_name)
+ table_obj = seafile_api.get_dirent_by_path(repo_id, table_path)
+ table = dict()
+ table["name"] = table_obj.obj_name
+ 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
+ """
+ # argument check
+ old_table_name = request.data.get('old_name')
+ if not old_table_name:
+ error_msg = 'old_name invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ new_table_name = request.data.get('new_name')
+ if not new_table_name:
+ error_msg = 'new_name invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if not is_valid_dirent_name(new_table_name):
+ error_msg = 'new_name invalid.'
+ return api_error(status.HTTP_400_BAD_REQUEST, error_msg)
+
+ if len(new_table_name) > MAX_UPLOAD_FILE_NAME_LEN:
+ error_msg = 'new_name is too long.'
+ 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)
+
+ old_table_path = normalize_file_path(old_table_name)
+ table_file_id = seafile_api.get_file_id_by_path(repo_id, old_table_path)
+ if not table_file_id:
+ error_msg = 'table %s not found.' % old_table_name
+ 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)
+
+ # rename table
+ new_table_name = check_filename_with_rename(repo_id, '/', new_table_name)
+ try:
+ seafile_api.rename_file(repo_id, '/', old_table_name, new_table_name, owner)
+ except SearpcError as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ new_table_path = normalize_file_path(new_table_name)
+ table_obj = seafile_api.get_dirent_by_path(repo_id, new_table_path)
+ table = dict()
+ table["name"] = table_obj.obj_name
+ 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_200_OK)
+
+ def delete(self, request, workspace_id):
+ """delete a table
+ """
+ # argument check
+ table_name = request.data.get('name')
+ if not table_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)
+
+ table_path = normalize_file_path(table_name)
+ table_file_id = seafile_api.get_file_id_by_path(repo_id, table_path)
+ if not table_file_id:
+ return Response({'success': True}, status=status.HTTP_200_OK)
+
+ # 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)
+
+ # delete table
+ try:
+ seafile_api.del_file(repo_id, '/', table_name, owner)
+ except SearpcError as e:
+ logger.error(e)
+ error_msg = 'Internal Server Error'
+ return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg)
+
+ return Response({'success': True}, status=status.HTTP_200_OK)
diff --git a/seahub/dtable/__init__.py b/seahub/dtable/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/seahub/dtable/apps.py b/seahub/dtable/apps.py
new file mode 100644
index 0000000000..37a046bc54
--- /dev/null
+++ b/seahub/dtable/apps.py
@@ -0,0 +1,8 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.apps import AppConfig
+
+
+class DtableConfig(AppConfig):
+ name = 'dtable'
diff --git a/seahub/dtable/migrations/0001_initial.py b/seahub/dtable/migrations/0001_initial.py
new file mode 100644
index 0000000000..2a5124b48e
--- /dev/null
+++ b/seahub/dtable/migrations/0001_initial.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.15 on 2019-05-24 03:56
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+import seahub.base.fields
+
+
+class Migration(migrations.Migration):
+
+ initial = True
+
+ dependencies = [
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='WorkSpaces',
+ fields=[
+ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+ ('name', seahub.base.fields.LowerCaseCharField(max_length=255)),
+ ('owner', models.CharField(max_length=255)),
+ ('repo_id', models.CharField(db_index=True, max_length=36)),
+ ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
+ ],
+ options={
+ 'db_table': 'workspaces',
+ },
+ ),
+ migrations.AlterUniqueTogether(
+ name='workspaces',
+ unique_together=set([('owner', 'repo_id')]),
+ ),
+ ]
diff --git a/seahub/dtable/migrations/__init__.py b/seahub/dtable/migrations/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/seahub/dtable/models.py b/seahub/dtable/models.py
new file mode 100644
index 0000000000..2a0ce1fef9
--- /dev/null
+++ b/seahub/dtable/models.py
@@ -0,0 +1,72 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+from django.db import models
+from seaserv import seafile_api
+
+from seahub.base.fields import LowerCaseCharField
+from seahub.utils.timeutils import timestamp_to_isoformat_timestr, datetime_to_isoformat_timestr
+
+
+class WorkSpacesManager(models.Manager):
+
+ def get_workspaces_by_owner(self, owner):
+ try:
+ return super(WorkSpacesManager, self).filter(owner=owner)
+ except self.model.DoesNotExist:
+ return None
+
+ def get_workspace_by_id(self, workspace_id):
+ try:
+ return super(WorkSpacesManager, self).get(pk=workspace_id)
+ except self.model.DoesNotExist:
+ return None
+
+ def create_workspace(self, name, owner, repo_id):
+ try:
+ return super(WorkSpacesManager, self).get(name=name, owner=owner, repo_id=repo_id)
+ except self.model.DoesNotExist:
+ workspace = self.model(name=name, owner=owner, repo_id=repo_id)
+ workspace.save()
+ return workspace
+
+ def delete_workspace(self, workspace_id):
+ try:
+ workspace = super(WorkSpacesManager, self).get(pk=workspace_id)
+ workspace.delete()
+ return True
+ except self.model.DoesNotExist:
+ return False
+
+
+class WorkSpaces(models.Model):
+
+ name = LowerCaseCharField(max_length=255)
+ owner = models.CharField(max_length=255)
+ repo_id = models.CharField(max_length=36, db_index=True)
+ created_at = models.DateTimeField(auto_now_add=True, db_index=True)
+
+ objects = WorkSpacesManager()
+
+ class Meta:
+ unique_together = (('owner', 'repo_id'),)
+ db_table = 'workspaces'
+
+ @property
+ def updated_at(self):
+ assert len(self.repo_id) == 36
+
+ repo = seafile_api.get_repo(self.repo_id)
+ if not repo:
+ return ''
+
+ return timestamp_to_isoformat_timestr(repo.last_modify)
+
+ def to_dict(self):
+
+ return {
+ 'id': self.pk,
+ 'name': self.name,
+ 'owner': self.owner,
+ 'repo_id': self.repo_id,
+ 'created_at': datetime_to_isoformat_timestr(self.created_at),
+ }
diff --git a/seahub/settings.py b/seahub/settings.py
index 3713a3433a..3ad42873f0 100644
--- a/seahub/settings.py
+++ b/seahub/settings.py
@@ -258,6 +258,7 @@ INSTALLED_APPS = (
'seahub.file_tags',
'seahub.related_files',
'seahub.work_weixin',
+ 'seahub.dtable',
)
# Enable or disable view File Scan
diff --git a/seahub/urls.py b/seahub/urls.py
index e9fd491d5a..17538c48b2 100644
--- a/seahub/urls.py
+++ b/seahub/urls.py
@@ -84,6 +84,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, WorkSpaceView, DTableView
# Admin
from seahub.api2.endpoints.admin.revision_tag import AdminTaggedItemsView
@@ -220,6 +221,7 @@ urlpatterns = [
url(r'^my-libs/deleted/$', react_fake_view, name="my_libs_deleted"),
url(r'^org/$', react_fake_view, name="org"),
url(r'^invitations/$', react_fake_view, name="invitations"),
+ url(r'^dtable/$', react_fake_view, name='dtable'),
### Ajax ###
url(r'^ajax/repo/(?P[-0-9a-f]{36})/dirents/$', get_dirents, name="get_dirents"),
@@ -342,6 +344,11 @@ urlpatterns = [
# user: markdown-lint
url(r'^api/v2.1/markdown-lint/$', MarkdownLintView.as_view(), name='api-v2.1-markdown-lint'),
+ # user: workspaces
+ url(r'^api/v2.1/workspaces/$', WorkSpacesView.as_view(), name='api-v2.1-workspaces'),
+ url(r'^api/v2.1/workspace/(?P\d+)/$', WorkSpaceView.as_view(), name='api-v2.1-workspace'),
+ url(r'^api/v2.1/workspace/(?P\d+)/table/$', DTableView.as_view(), name='api-v2.1-workspace-table'),
+
# Deprecated
url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/tags/$', FileTagsView.as_view(), name="api-v2.1-filetags-view"),
url(r'^api/v2.1/repos/(?P[-0-9a-f]{36})/tags/(?P.*?)/$', FileTagView.as_view(), name="api-v2.1-filetag-view"),