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.newInput = input;}} + value={this.state.workspaceName} + onChange={this.handleNameChange} + /> + +
+ {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"),