From 0d885aa2b8e04173ba13af5a67923d16cb0758dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8E=8B=E5=81=A5=E8=BE=89?= Date: Mon, 30 Sep 2019 09:40:14 +0800 Subject: [PATCH] dtable row share link (#4067) * rebase master * fix rebase error * return dtable columns --- frontend/config/webpack.config.dev.js | 5 + frontend/config/webpack.config.prod.js | 1 + frontend/src/shared-dtable-row.js | 26 +++ seahub/api2/endpoints/dtable.py | 193 ++++++++++++++++-- .../dtable/migrations/0004_dtablerowshares.py | 30 +++ seahub/dtable/models.py | 79 ++++++- seahub/dtable/urls.py | 3 +- seahub/dtable/views.py | 96 ++++++--- seahub/templates/dtable_file_view_react.html | 1 - .../shared_dtable_row_view_react.html | 35 ++++ seahub/urls.py | 4 +- tests/api/endpoints/test_dtable.py | 23 +++ 12 files changed, 455 insertions(+), 41 deletions(-) create mode 100644 frontend/src/shared-dtable-row.js create mode 100644 seahub/dtable/migrations/0004_dtablerowshares.py create mode 100644 seahub/templates/shared_dtable_row_view_react.html diff --git a/frontend/config/webpack.config.dev.js b/frontend/config/webpack.config.dev.js index ae11d723f0..ad445708d2 100644 --- a/frontend/config/webpack.config.dev.js +++ b/frontend/config/webpack.config.dev.js @@ -229,6 +229,11 @@ module.exports = { require.resolve('react-dev-utils/webpackHotDevClient'), paths.appSrc + "/dtable-form-view.js", ], + sharedDTableRow: [ + require.resolve('./polyfills'), + require.resolve('react-dev-utils/webpackHotDevClient'), + paths.appSrc + "/shared-dtable-row.js", + ], }, output: { diff --git a/frontend/config/webpack.config.prod.js b/frontend/config/webpack.config.prod.js index 5196e10bb4..e50be436cb 100644 --- a/frontend/config/webpack.config.prod.js +++ b/frontend/config/webpack.config.prod.js @@ -94,6 +94,7 @@ module.exports = { appDTable: [require.resolve('./polyfills'), paths.appSrc + "/app-dtable"], dtableAssetFileView: [require.resolve('./polyfills'), paths.appSrc + "/dtable-asset-file-view.js"], dtableFormView: [require.resolve('./polyfills'), paths.appSrc + "/dtable-form-view.js"], + sharedDTableRow: [require.resolve('./polyfills'), paths.appSrc + "/shared-dtable-row.js"], }, output: { diff --git a/frontend/src/shared-dtable-row.js b/frontend/src/shared-dtable-row.js new file mode 100644 index 0000000000..bdb501075b --- /dev/null +++ b/frontend/src/shared-dtable-row.js @@ -0,0 +1,26 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import CodeMirror from 'react-codemirror'; + +import 'codemirror/lib/codemirror.css'; +import './css/text-file-view.css'; + +const { rowContent, columns } = window.shared.pageOptions; + +class SharedDTableRowView extends React.Component { + render() { + return ( +
+ +
+ ); + } +} + +ReactDOM.render( + , + document.getElementById('wrapper') +); diff --git a/seahub/api2/endpoints/dtable.py b/seahub/api2/endpoints/dtable.py index 1c3d16a768..2512c6912f 100644 --- a/seahub/api2/endpoints/dtable.py +++ b/seahub/api2/endpoints/dtable.py @@ -18,7 +18,7 @@ from seaserv import seafile_api, ccnet_api 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, DTables +from seahub.dtable.models import Workspaces, DTables, DTableRowShares from seahub.base.templatetags.seahub_tags import email2nickname from seahub.group.utils import group_id_to_name from seahub.utils import is_valid_dirent_name, is_org_context, normalize_file_path, \ @@ -276,7 +276,7 @@ class DTableView(APIView): dtable = DTables.objects.get_dtable(workspace, old_table_name) if not dtable: - error_msg = 'dtable %s not found.' % old_table_name + error_msg = 'DTable %s not found.' % old_table_name return api_error(status.HTTP_404_NOT_FOUND, error_msg) old_table_file_name = old_table_name + FILE_TYPE @@ -346,7 +346,7 @@ class DTableView(APIView): dtable = DTables.objects.get_dtable(workspace, table_name) if not dtable: - error_msg = 'dtable %s not found.' % table_name + error_msg = 'DTable %s not found.' % table_name return api_error(status.HTTP_404_NOT_FOUND, error_msg) table_path = normalize_file_path(table_file_name) @@ -432,7 +432,7 @@ class DTableAssetUploadLinkView(APIView): dtable = DTables.objects.get_dtable(workspace, table_name) if not dtable: - error_msg = 'dtable %s not found.' % table_name + error_msg = 'DTable %s not found.' % table_name return api_error(status.HTTP_404_NOT_FOUND, error_msg) # permission check @@ -475,9 +475,7 @@ class DTableAccessTokenView(APIView): def get(self, request, workspace_id, name): """get dtable access token """ - table_name = name - table_file_name = table_name + FILE_TYPE # resource check workspace = Workspaces.objects.get_workspace_by_id(workspace_id) @@ -493,13 +491,7 @@ class DTableAccessTokenView(APIView): dtable = DTables.objects.get_dtable(workspace, table_name) if not dtable: - error_msg = 'dtable %s not found.' % table_name - return api_error(status.HTTP_404_NOT_FOUND, error_msg) - - table_path = normalize_file_path(table_file_name) - table_file_id = seafile_api.get_file_id_by_path(repo_id, table_path) - if not table_file_id: - error_msg = 'file %s not found.' % table_file_name + error_msg = 'DTable %s not found.' % table_name return api_error(status.HTTP_404_NOT_FOUND, error_msg) # permission check @@ -525,3 +517,178 @@ class DTableAccessTokenView(APIView): return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, error_msg) return Response({'access_token': access_token}) + + +class DTableRowSharesView(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, ) + throttle_classes = (UserRateThrottle, ) + + def get(self, request): + """get a dtable row share link + + Permission: + 1. owner + 2. group member + 3. shared user with `rw` or `admin` permission + """ + # argument check + workspace_id = request.GET.get('workspace_id', None) + if not workspace_id: + error_msg = 'workspace_id invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + table_name = request.GET.get('name', None) + if not table_name: + error_msg = 'name invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + table_id = request.GET.get('table_id', None) + if not table_id: + error_msg = 'table_id invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + row_id = request.GET.get('row_id', None) + if not row_id: + error_msg = 'row_id 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) + + dtable = DTables.objects.get_dtable(workspace, table_name) + if not dtable: + error_msg = 'DTable %s not found.' % table_name + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # permission check + username = request.user.username + if check_dtable_permission(username, workspace, dtable) not in WRITE_PERMISSION_TUPLE: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + dtable_uuid = dtable.uuid.hex + try: + row_share = DTableRowShares.objects.get_dtable_row_share( + username, workspace_id, dtable_uuid, table_id, row_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({"row_share": row_share}, status=status.HTTP_200_OK) + + def post(self, request): + """create a dtable row share link + + Permission: + 1. owner + 2. group member + 3. shared user with `rw` or `admin` permission + """ + # argument check + workspace_id = request.POST.get('workspace_id') + if not workspace_id: + error_msg = 'workspace_id 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_id = request.POST.get('table_id') + if not table_id: + error_msg = 'table_id invalid.' + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + + row_id = request.POST.get('row_id') + if not row_id: + error_msg = 'row_id 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) + + dtable = DTables.objects.get_dtable(workspace, table_name) + if not dtable: + error_msg = 'DTable %s not found.' % table_name + return api_error(status.HTTP_404_NOT_FOUND, error_msg) + + # permission check + username = request.user.username + if check_dtable_permission(username, workspace, dtable) not in WRITE_PERMISSION_TUPLE: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + dtable_uuid = dtable.uuid.hex + row_share = DTableRowShares.objects.get_dtable_row_share( + username, workspace_id, dtable_uuid, table_id, row_id + ) + if row_share: + error_msg = 'Row share link %s already exists.' % row_share['token'] + return api_error(status.HTTP_400_BAD_REQUEST, error_msg) + try: + row_share = DTableRowShares.objects.add_dtable_row_share( + username, workspace_id, dtable_uuid, table_id, row_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({"row_share": row_share}, status=status.HTTP_201_CREATED) + + +class DTableRowShareView(APIView): + + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, ) + throttle_classes = (UserRateThrottle, ) + + def delete(self, request, token): + """ Delete share link. + + Permission: + 1. dtable row share owner; + """ + # resource check + row_share = DTableRowShares.objects.get_dtable_row_share_by_token(token) + if not row_share: + return Response({'success': True}, status=status.HTTP_200_OK) + + # permission check + username = request.user.username + row_share_owner = row_share.username + if username != row_share_owner: + error_msg = 'Permission denied.' + return api_error(status.HTTP_403_FORBIDDEN, error_msg) + + try: + DTableRowShares.objects.delete_dtable_row_share(token) + 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) diff --git a/seahub/dtable/migrations/0004_dtablerowshares.py b/seahub/dtable/migrations/0004_dtablerowshares.py new file mode 100644 index 0000000000..95774501c4 --- /dev/null +++ b/seahub/dtable/migrations/0004_dtablerowshares.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.23 on 2019-09-17 10:47 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('dtable', '0003_auto'), + ] + + operations = [ + migrations.CreateModel( + name='DTableRowShares', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('username', models.CharField(db_index=True, max_length=255)), + ('workspace_id', models.IntegerField(db_index=True)), + ('dtable_uuid', models.CharField(db_index=True, max_length=36)), + ('table_id', models.CharField(max_length=36)), + ('row_id', models.CharField(db_index=True, max_length=36)), + ('token', models.CharField(max_length=100, unique=True)), + ], + options={ + 'db_table': 'dtable_row_shares', + }, + ), + ] diff --git a/seahub/dtable/models.py b/seahub/dtable/models.py index 16a03635b8..7fb87e629d 100644 --- a/seahub/dtable/models.py +++ b/seahub/dtable/models.py @@ -114,9 +114,9 @@ class DTablesManager(models.Manager): except self.model.DoesNotExist: return None - def get_dtable_by_uuid(self, uuid): + def get_dtable_by_uuid(self, dtable_uuid): try: - return super(DTablesManager, self).get(uuid=uuid) + return super(DTablesManager, self).get(uuid=dtable_uuid) except self.model.DoesNotExist: return None @@ -364,3 +364,78 @@ class DTableFormLinks(models.Model): 'form_id': self.form_id, 'token': self.token, } + + +class DTableRowSharesManager(models.Manager): + def add_dtable_row_share(self, username, workspace_id, dtable_uuid, table_id, row_id): + token = uuid.uuid4() + row_share_obj = self.model( + username=username, + workspace_id=workspace_id, + dtable_uuid=dtable_uuid, + table_id=table_id, + row_id=row_id, + token=token + ) + row_share_obj.save() + row_share = row_share_obj.to_dict() + row_share["row_share_link"] = "%s/dtable/row-share-links/%s" % (SERVICE_URL, token) + return row_share + + def get_dtable_row_share(self, username, workspace_id, dtable_uuid, table_id, row_id): + row_shares = super(DTableRowSharesManager, self).filter( + username=username, + workspace_id=workspace_id, + dtable_uuid=dtable_uuid, + table_id=table_id, + row_id=row_id + ) + if len(row_shares) > 0: + row_share_obj = row_shares[0] + row_share = row_share_obj.to_dict() + row_share["row_share_link"] = "%s/dtable/row-share-links/%s" % \ + (SERVICE_URL, row_share_obj.token) + return row_share + else: + return None + + def get_dtable_row_share_by_token(self, token): + try: + return super(DTableRowSharesManager, self).get(token=token) + except self.model.DoesNotExist: + return None + + def delete_dtable_row_share(self, token): + try: + row_share = super(DTableRowSharesManager, self).get(token=token) + row_share.delete() + return True + except self.model.DoesNotExist: + return False + + +class DTableRowShares(models.Model): + + username = models.CharField(max_length=255, db_index=True) + workspace_id = models.IntegerField(db_index=True) + dtable_uuid = models.CharField(max_length=36, db_index=True) + table_id = models.CharField(max_length=36) + row_id = models.CharField(max_length=36, db_index=True) + token = models.CharField(max_length=100, unique=True) + + objects = DTableRowSharesManager() + + class Meta: + db_table = 'dtable_row_shares' + + def to_dict(self): + + return { + 'id': self.pk, + 'username': self.username, + 'workspace_id': self.workspace_id, + 'dtable_uuid': self.dtable_uuid, + 'table_id': self.table_id, + 'row_id': self.row_id, + 'token': self.token, + } diff --git a/seahub/dtable/urls.py b/seahub/dtable/urls.py index 24e508a7f3..5b2749edc5 100644 --- a/seahub/dtable/urls.py +++ b/seahub/dtable/urls.py @@ -2,7 +2,7 @@ from django.conf.urls import url from .views import dtable_file_view, dtable_asset_access, dtable_asset_file_view, dtable_form_view, \ - dtable_share_link_view + dtable_share_link_view, dtable_row_share_link_view urlpatterns = [ url(r'^workspace/(?P\d+)/dtable/(?P.*)/$', dtable_file_view, name='dtable_file_view'), @@ -10,4 +10,5 @@ urlpatterns = [ url(r'^workspace/(?P\d+)/asset-file/(?P[-0-9a-f]{36})/(?P.*)$', dtable_asset_file_view, name='dtable_asset_file_view'), url(r'^dtable/forms/(?P[-0-9a-f]{36})$', dtable_form_view, name='dtable_form_view'), url(r'^dtable/links/(?P[-0-9a-f]+)/$', dtable_share_link_view, name='dtable_share_link_view'), + url(r'^dtable/row-share-links/(?P[-0-9a-f]{36})$', dtable_row_share_link_view, name='dtable_row_share_link_view'), ] diff --git a/seahub/dtable/views.py b/seahub/dtable/views.py index 4df8f9dbfa..42b6c2339e 100644 --- a/seahub/dtable/views.py +++ b/seahub/dtable/views.py @@ -10,13 +10,13 @@ from django.shortcuts import render from django.utils.translation import ugettext as _ from seaserv import seafile_api -from seahub.dtable.models import Workspaces, DTables, DTableFormLinks, DTableShareLinks +from seahub.dtable.models import Workspaces, DTables, DTableFormLinks, DTableShareLinks, \ + DTableRowShares from seahub.utils import normalize_file_path, render_error, render_permission_error, \ gen_file_get_url, get_file_type_and_ext, gen_inner_file_get_url from seahub.auth.decorators import login_required -from seahub.settings import SHARE_LINK_EXPIRE_DAYS_MIN, SHARE_LINK_EXPIRE_DAYS_MAX, \ - SHARE_LINK_EXPIRE_DAYS_DEFAULT, DTABLE_SERVER_URL, SEAFILE_COLLAB_SERVER, MEDIA_URL, \ - FILE_ENCODING_LIST, DTABLE_PRIVATE_KEY +from seahub.settings import DTABLE_SERVER_URL, SEAFILE_COLLAB_SERVER, MEDIA_URL, \ + DTABLE_PRIVATE_KEY, FILE_ENCODING_LIST from seahub.dtable.utils import check_dtable_permission from seahub.constants import PERMISSION_ADMIN, PERMISSION_READ_WRITE from seahub.views.file import get_file_content @@ -39,22 +39,16 @@ def dtable_file_view(request, workspace_id, name): # resource check workspace = Workspaces.objects.get_workspace_by_id(workspace_id) if not workspace: - raise Http404 + return render_error(request, 'Workspace does not exist.') repo_id = workspace.repo_id repo = seafile_api.get_repo(repo_id) if not repo: - raise Http404 + return render_error(request, 'Library does not exist.') dtable = DTables.objects.get_dtable(workspace, name) if not dtable: - return render_error(request, _('Table does not exist')) - - table_file_name = name + FILE_TYPE - table_path = normalize_file_path(table_file_name) - table_file_id = seafile_api.get_file_id_by_path(repo_id, table_path) - if not table_file_id: - return render_error(request, _('Table does not exist')) + return render_error(request, 'DTable does not exist') # permission check username = request.user.username @@ -62,13 +56,7 @@ def dtable_file_view(request, workspace_id, name): return render_permission_error(request, _('Permission denied.')) return_dict = { - 'share_link_expire_days_default': SHARE_LINK_EXPIRE_DAYS_DEFAULT, - 'share_link_expire_days_min': SHARE_LINK_EXPIRE_DAYS_MIN, - 'share_link_expire_days_max': SHARE_LINK_EXPIRE_DAYS_MAX, - 'repo': repo, 'filename': name, - 'path': table_path, - 'filetype': 'dtable', 'workspace_id': workspace_id, 'dtable_uuid': dtable.uuid.hex, 'media_url': MEDIA_URL, @@ -93,21 +81,21 @@ def dtable_asset_access(request, workspace_id, dtable_id, path): # resource check workspace = Workspaces.objects.get_workspace_by_id(workspace_id) if not workspace: - raise Http404 + return render_error(request, 'Workspace does not exist.') repo_id = workspace.repo_id repo = seafile_api.get_repo(repo_id) if not repo: - raise Http404 + return render_error(request, 'Library does not exist.') dtable = DTables.objects.get_dtable_by_uuid(dtable_id) if not dtable: - raise Http404 + return render_error(request, 'DTable does not exist.') asset_path = normalize_file_path(os.path.join('/asset', dtable_id, path)) asset_id = seafile_api.get_file_id_by_path(repo_id, asset_path) if not asset_id: - raise Http404 + return render_error(request, 'Asset file does not exist.') # permission check username = request.user.username @@ -238,6 +226,68 @@ def dtable_form_view(request, token): return render(request, 'dtable_form_view_react.html', return_dict) +@login_required +def dtable_row_share_link_view(request, token): + + # resource check + dtable_row_share = DTableRowShares.objects.get_dtable_row_share_by_token(token) + if not dtable_row_share: + return render_error(request, 'DTable row share link does not exist.') + + workspace_id = dtable_row_share.workspace_id + workspace = Workspaces.objects.get_workspace_by_id(workspace_id) + if not workspace: + return render_error(request, 'Workspace does not exist.') + + repo_id = workspace.repo_id + repo = seafile_api.get_repo(repo_id) + if not repo: + return render_error(request, 'Library does not exist.') + + dtable_uuid = dtable_row_share.dtable_uuid + dtable = DTables.objects.get_dtable_by_uuid(dtable_uuid) + if not dtable: + return render_error(request, 'DTable %s does not exist' % dtable_uuid) + + # generate json web token + username = request.user.username + payload = { + 'exp': int(time.time()) + 86400 * 3, + 'dtable_uuid': dtable.uuid.hex, + 'username': username, + } + + try: + access_token = jwt.encode( + payload, DTABLE_PRIVATE_KEY, algorithm='HS256' + ) + except Exception as e: + logger.error(e) + return render_error(request, _('Internal Server Error')) + + url_for_row = '%s/api/v1/dtables/%s/tables/%s/rows/%s/' % \ + (DTABLE_SERVER_URL.strip('/'), dtable_uuid, dtable_row_share.table_id, dtable_row_share.row_id) + req_for_row = requests.Request(url_for_row, headers={"Authorization": "Token %s" % access_token.decode()}) + + url_for_columns = '%s/api/v1/dtables/%s/tables/%s/columns/' % \ + (DTABLE_SERVER_URL.strip('/'), dtable_uuid, dtable_row_share.table_id) + req_for_columns = requests.Request(url_for_columns, headers={"Authorization": "Token %s" % access_token.decode()}) + + try: + row_content = requests.urlopen(req_for_row).read().decode() + columns = requests.urlopen(req_for_columns).read().decode() + except Exception as e: + logger.error(e) + return render_error(request, _('Internal Server Error')) + + return_dict = { + 'row_content': row_content, + 'columns': columns, + } + + return render(request, 'shared_dtable_row_view_react.html', return_dict) + + def dtable_share_link_view(request, token): dsl = DTableShareLinks.objects.filter(token=token).first() if not dsl: diff --git a/seahub/templates/dtable_file_view_react.html b/seahub/templates/dtable_file_view_react.html index 12ee95ca4c..ea5c337d1d 100644 --- a/seahub/templates/dtable_file_view_react.html +++ b/seahub/templates/dtable_file_view_react.html @@ -36,7 +36,6 @@ contactEmail: "{{request.user.username|email2contact_email|escapejs}}", fileName: '{{ filename|escapejs }}', - filePath: '{{ path|escapejs }}', workspaceID: '{{ workspace_id }}', dtableUuid: '{{ dtable_uuid }}', mediaUrl: '{{ media_url }}', diff --git a/seahub/templates/shared_dtable_row_view_react.html b/seahub/templates/shared_dtable_row_view_react.html new file mode 100644 index 0000000000..73c8951935 --- /dev/null +++ b/seahub/templates/shared_dtable_row_view_react.html @@ -0,0 +1,35 @@ +{% extends "base_for_react.html" %} +{% load seahub_tags i18n staticfiles %} +{% load render_bundle from webpack_loader %} + + + + +{% block sub_title %}{% endblock %}{{ site_title }} + + + + + + + + + + + + +
+ + {% block extra_script %} + + {% render_bundle 'sharedDTableRow' 'js' %} + {% endblock %} + + diff --git a/seahub/urls.py b/seahub/urls.py index 04ec05aaba..d2a7c0cff9 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -91,7 +91,7 @@ from seahub.api2.endpoints.starred_items import StarredItems from seahub.api2.endpoints.markdown_lint import MarkdownLintView from seahub.api2.endpoints.public_repos_search import PublishedRepoSearchView from seahub.api2.endpoints.dtable import WorkspacesView, DTableView, DTablesView, \ - DTableAssetUploadLinkView, DTableAccessTokenView + DTableAssetUploadLinkView, DTableAccessTokenView, DTableRowSharesView, DTableRowSharesView, DTableRowShareView from seahub.api2.endpoints.dtable_api_token import DTableAPITokensView, DTableAPITokenView, DTableAppAccessTokenView from seahub.api2.endpoints.dtable_forms import DTableFormLinksView, DTableFormLinkView from seahub.api2.endpoints.dtable_share import SharedDTablesView, DTableShareView @@ -396,6 +396,8 @@ urlpatterns = [ url(r'^api/v2.1/dtable/app-access-token/$', DTableAppAccessTokenView.as_view(), name='api-v2.1-dtable-app-access-token'), url(r'^api/v2.1/dtable-form-links/$', DTableFormLinksView.as_view(), name='api-v2.1-dtable-form-links'), url(r'^api/v2.1/dtable-form-links/(?P[-0-9a-f]{36})/$', DTableFormLinkView.as_view(), name='api-v2.1-dtable-form-link'), + url(r'^api/v2.1/dtable-row-shares/$', DTableRowSharesView.as_view(), name='api-v2.1-dtable-row-shares'), + url(r'^api/v2.1/dtable-row-shares/(?P[-0-9a-f]{36})/$', DTableRowShareView.as_view(), name='api-v2.1-dtable-row-share'), url(r'^api/v2.1/recent-added-files/$', RecentAddedFilesView.as_view(), name='api-v2.1-recent-added-files'), diff --git a/tests/api/endpoints/test_dtable.py b/tests/api/endpoints/test_dtable.py index b35a14ec46..3fda5dde06 100644 --- a/tests/api/endpoints/test_dtable.py +++ b/tests/api/endpoints/test_dtable.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- import json +import uuid from django.core.urlresolvers import reverse @@ -7,6 +8,7 @@ from seaserv import seafile_api from seahub.dtable.models import Workspaces from seahub.test_utils import BaseTestCase +from tests.common.utils import randstring class WorkspacesViewTest(BaseTestCase): @@ -191,3 +193,24 @@ class DTableTest(BaseTestCase): url4 = reverse('api-v2.1-dtable-access-token', args=[self.workspace.id, 'table11']) resp = self.client.get(url4, {}, 'application/x-www-form-urlencoded') self.assertEqual(404, resp.status_code) + + def test_create_dtable_row_share(self): + resp = self.client.post(self.url1, {'name': 'table12', 'owner': self.user.username}) + self.assertEqual(201, resp.status_code) + + json_resp = json.loads(resp.content) + assert json_resp["table"]["name"] == 'table12' + + url4 = reverse('api-v2.1-dtable-row-shares') + resp = self.client.post( + url4, + { + 'workspace_id': self.workspace.id, + 'name': 'table12', + 'table_id': randstring(4), + 'row_id': uuid.uuid4() + } + ) + self.assertEqual(201, resp.status_code) + json_resp = json.loads(resp.content) + self.assertIsNotNone(json_resp["row_share"])