mirror of
https://github.com/haiwen/seahub.git
synced 2025-07-08 12:44:03 +00:00
file participants api-v2.1
This commit is contained in:
parent
0b7a3be295
commit
ae543df9b7
0
seahub/file_participants/__init__.py
Normal file
0
seahub/file_participants/__init__.py
Normal file
64
seahub/file_participants/models.py
Normal file
64
seahub/file_participants/models.py
Normal file
@ -0,0 +1,64 @@
|
||||
# Copyright (c) 2012-2019 Seafile Ltd.
|
||||
import os
|
||||
|
||||
import logging
|
||||
from django.db import models
|
||||
from seahub.tags.models import FileUUIDMap
|
||||
from seahub.base.fields import LowerCaseCharField
|
||||
|
||||
# Get an instance of a logger
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FileParticipantManager(models.Manager):
|
||||
|
||||
def _get_file_uuid_map(self, repo_id, file_path):
|
||||
parent_path = os.path.dirname(file_path)
|
||||
item_name = os.path.basename(file_path)
|
||||
|
||||
file_uuid_map = FileUUIDMap.objects.get_or_create_fileuuidmap(
|
||||
repo_id, parent_path, item_name, False)
|
||||
|
||||
return file_uuid_map
|
||||
|
||||
def add_by_file_path_and_username(self, repo_id, file_path, username):
|
||||
uuid = self._get_file_uuid_map(repo_id, file_path)
|
||||
if self.filter(uuid=uuid, username=username).exists():
|
||||
return self.filter(uuid=uuid, username=username)[0]
|
||||
|
||||
obj = self.model(uuid=uuid, username=username)
|
||||
obj.save(using=self._db)
|
||||
|
||||
return obj
|
||||
|
||||
def get_by_file_path_and_username(self, repo_id, file_path, username):
|
||||
uuid = self._get_file_uuid_map(repo_id, file_path)
|
||||
try:
|
||||
obj = self.get(uuid=uuid, username=username)
|
||||
return obj
|
||||
except self.model.DoesNotExist:
|
||||
return None
|
||||
|
||||
def delete_by_file_path_and_username(self, repo_id, file_path, username):
|
||||
uuid = self._get_file_uuid_map(repo_id, file_path)
|
||||
self.filter(uuid=uuid, username=username).delete()
|
||||
|
||||
def get_by_file_path(self, repo_id, file_path):
|
||||
uuid = self._get_file_uuid_map(repo_id, file_path)
|
||||
objs = self.filter(uuid=uuid)
|
||||
|
||||
return objs
|
||||
|
||||
|
||||
class FileParticipant(models.Model):
|
||||
"""
|
||||
Model used to record file participants.
|
||||
"""
|
||||
uuid = models.ForeignKey(FileUUIDMap, on_delete=models.CASCADE)
|
||||
username = LowerCaseCharField(max_length=255)
|
||||
|
||||
objects = FileParticipantManager()
|
||||
|
||||
class Meta:
|
||||
"""Meta data"""
|
||||
unique_together = ('uuid', 'username')
|
12
seahub/file_participants/utils.py
Normal file
12
seahub/file_participants/utils.py
Normal file
@ -0,0 +1,12 @@
|
||||
from .models import FileParticipant
|
||||
|
||||
|
||||
def list_file_participants_username(repo_id, path):
|
||||
""" return participants username list
|
||||
"""
|
||||
username_list = []
|
||||
file_participant_queryset = FileParticipant.objects.get_by_file_path(repo_id, path)
|
||||
for participant in file_participant_queryset:
|
||||
username_list.append(participant.username)
|
||||
|
||||
return username_list
|
144
seahub/file_participants/views.py
Normal file
144
seahub/file_participants/views.py
Normal file
@ -0,0 +1,144 @@
|
||||
# Copyright (c) 2012-2019 Seafile Ltd.
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
|
||||
from rest_framework import status
|
||||
from rest_framework.authentication import SessionAuthentication
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
from seaserv import seafile_api
|
||||
|
||||
from seahub.api2.authentication import TokenAuthentication
|
||||
from seahub.api2.permissions import IsRepoAccessible
|
||||
from seahub.api2.throttling import UserRateThrottle
|
||||
from seahub.api2.utils import api_error, get_user_common_info
|
||||
from .models import FileParticipant
|
||||
from seahub.utils import normalize_file_path, is_valid_username
|
||||
from seahub.views import check_folder_permission
|
||||
from pysearpc import SearpcError
|
||||
from seahub.base.accounts import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FileParticipantsView(APIView):
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAuthenticated, IsRepoAccessible)
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
|
||||
def get(self, request, repo_id, format=None):
|
||||
"""List all participants of a file.
|
||||
"""
|
||||
# argument check
|
||||
path = request.GET.get('path', '/').rstrip('/')
|
||||
if not path:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'Invalid path.')
|
||||
path = normalize_file_path(path)
|
||||
|
||||
# resource check
|
||||
try:
|
||||
file_id = seafile_api.get_file_id_by_path(repo_id, path)
|
||||
except SearpcError as e:
|
||||
logger.error(e)
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal error.')
|
||||
if not file_id:
|
||||
return api_error(status.HTTP_404_NOT_FOUND, 'File not found.')
|
||||
|
||||
# permission check
|
||||
if not check_folder_permission(request, repo_id, '/'):
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
# main
|
||||
participant_list = []
|
||||
participant_queryset = FileParticipant.objects.get_by_file_path(repo_id, path)
|
||||
|
||||
for participant in participant_queryset:
|
||||
participant_info = get_user_common_info(participant.username)
|
||||
participant_list.append(participant_info)
|
||||
|
||||
return Response({'participant_list': participant_list})
|
||||
|
||||
def post(self, request, repo_id, format=None):
|
||||
"""Post a participant of a file.
|
||||
"""
|
||||
# argument check
|
||||
path = request.GET.get('path', '/').rstrip('/')
|
||||
if not path:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'Invalid path.')
|
||||
path = normalize_file_path(path)
|
||||
|
||||
email = request.data.get('email', '').lower()
|
||||
if not email:
|
||||
email = request.user.username
|
||||
|
||||
if not is_valid_username(email):
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'Invalid email.')
|
||||
|
||||
# resource check
|
||||
try:
|
||||
file_id = seafile_api.get_file_id_by_path(repo_id, path)
|
||||
except SearpcError as e:
|
||||
logger.error(e)
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal error.')
|
||||
if not file_id:
|
||||
return api_error(status.HTTP_404_NOT_FOUND, 'File not found.')
|
||||
|
||||
try:
|
||||
user = User.objects.get(email=email)
|
||||
except User.DoesNotExist:
|
||||
return api_error(status.HTTP_404_NOT_FOUND, 'User %s not found.' % email)
|
||||
|
||||
# permission check
|
||||
if not check_folder_permission(request, repo_id, '/'):
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
request.user = user
|
||||
if not check_folder_permission(request, repo_id, '/'):
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'User %s permission denied.' % email)
|
||||
|
||||
# main
|
||||
if FileParticipant.objects.get_by_file_path_and_username(repo_id, path, email):
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'Participant %s already exists.' % email)
|
||||
|
||||
FileParticipant.objects.add_by_file_path_and_username(repo_id, path, email)
|
||||
participant = get_user_common_info(email)
|
||||
|
||||
return Response(participant, status=201)
|
||||
|
||||
|
||||
class FileParticipantView(APIView):
|
||||
authentication_classes = (TokenAuthentication, SessionAuthentication)
|
||||
permission_classes = (IsAuthenticated, IsRepoAccessible)
|
||||
throttle_classes = (UserRateThrottle,)
|
||||
|
||||
def delete(self, request, repo_id, format=None):
|
||||
"""Delete a participant
|
||||
"""
|
||||
# argument check
|
||||
path = request.GET.get('path', '/').rstrip('/')
|
||||
if not path:
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'Invalid path.')
|
||||
path = normalize_file_path(path)
|
||||
|
||||
email = request.data.get('email', '').lower()
|
||||
if not email or not is_valid_username(email):
|
||||
return api_error(status.HTTP_400_BAD_REQUEST, 'Invalid email.')
|
||||
|
||||
# resource check
|
||||
try:
|
||||
file_id = seafile_api.get_file_id_by_path(repo_id, path)
|
||||
except SearpcError as e:
|
||||
logger.error(e)
|
||||
return api_error(status.HTTP_500_INTERNAL_SERVER_ERROR, 'Internal error.')
|
||||
if not file_id:
|
||||
return api_error(status.HTTP_404_NOT_FOUND, 'File not found.')
|
||||
|
||||
# permission check
|
||||
if not check_folder_permission(request, repo_id, '/'):
|
||||
return api_error(status.HTTP_403_FORBIDDEN, 'Permission denied.')
|
||||
|
||||
# main
|
||||
FileParticipant.objects.delete_by_file_path_and_username(repo_id, path, email)
|
||||
|
||||
return Response({'success': True})
|
@ -25,6 +25,7 @@ from seahub.utils import normalize_cache_key
|
||||
from seahub.utils.timeutils import datetime_to_isoformat_timestr
|
||||
from seahub.constants import HASH_URLS
|
||||
from seahub.drafts.models import DraftReviewer
|
||||
from seahub.file_participants.utils import list_file_participants_username
|
||||
|
||||
# Get an instance of a logger
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -979,14 +980,15 @@ def add_user_to_group_cb(sender, **kwargs):
|
||||
|
||||
@receiver(comment_file_successful)
|
||||
def comment_file_successful_cb(sender, **kwargs):
|
||||
""" send notification to file participants
|
||||
"""
|
||||
repo = kwargs['repo']
|
||||
repo_owner = kwargs['repo_owner']
|
||||
file_path = kwargs['file_path']
|
||||
comment = kwargs['comment']
|
||||
author = kwargs['author']
|
||||
|
||||
notify_users = get_repo_shared_users(repo.id, repo_owner)
|
||||
notify_users.append(repo_owner)
|
||||
notify_users = list_file_participants_username(repo.id, file_path)
|
||||
notify_users = [x for x in notify_users if x != author]
|
||||
for u in notify_users:
|
||||
detail = file_comment_msg_to_json(repo.id, file_path, author, comment)
|
||||
|
@ -259,6 +259,7 @@ INSTALLED_APPS = (
|
||||
'seahub.related_files',
|
||||
'seahub.work_weixin',
|
||||
'seahub.dtable',
|
||||
'seahub.file_participants',
|
||||
)
|
||||
|
||||
# Enable or disable view File Scan
|
||||
|
@ -143,6 +143,7 @@ from seahub.api2.endpoints.admin.file_scan_records import AdminFileScanRecords
|
||||
from seahub.api2.endpoints.admin.notifications import AdminNotificationsView
|
||||
from seahub.api2.endpoints.admin.work_weixin import AdminWorkWeixinDepartments, \
|
||||
AdminWorkWeixinDepartmentMembers, AdminWorkWeixinUsersBatch
|
||||
from seahub.file_participants.views import FileParticipantsView, FileParticipantView
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^accounts/', include('seahub.base.registration_urls')),
|
||||
@ -353,6 +354,8 @@ urlpatterns = [
|
||||
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/file-tags/$', RepoFileTagsView.as_view(), name='api-v2.1-file-tags'),
|
||||
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/file-tags/(?P<file_tag_id>\d+)/$', RepoFileTagView.as_view(), name='api-v2.1-file-tag'),
|
||||
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/tagged-files/(?P<repo_tag_id>\d+)/$', TaggedFilesView.as_view(), name='api-v2.1-tagged-files'),
|
||||
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/file/participants/$', FileParticipantsView.as_view(), name='api-v2.1-file-participants'),
|
||||
url(r'^api/v2.1/repos/(?P<repo_id>[-0-9a-f]{36})/file/participant/$', FileParticipantView.as_view(), name='api-v2.1-file-participant'),
|
||||
|
||||
# user::related-files
|
||||
url(r'^api/v2.1/related-files/$', RelatedFilesView.as_view(), name='api-v2.1-related-files'),
|
||||
|
@ -7,6 +7,7 @@ from seaserv import seafile_api, ccnet_api
|
||||
from seahub.base.models import FileComment
|
||||
from seahub.notifications.models import UserNotification
|
||||
from seahub.test_utils import BaseTestCase
|
||||
from seahub.file_participants.models import FileParticipant
|
||||
|
||||
class FileCommentsTest(BaseTestCase):
|
||||
def setUp(self):
|
||||
@ -86,12 +87,13 @@ class FileCommentsTest(BaseTestCase):
|
||||
})
|
||||
self.assertEqual(403, resp.status_code)
|
||||
|
||||
def test_can_notify_others(self):
|
||||
def test_can_notify_participant(self):
|
||||
assert len(UserNotification.objects.all()) == 0
|
||||
|
||||
username = self.user.username
|
||||
seafile_api.share_repo(self.repo.id, username,
|
||||
self.admin.username, 'rw')
|
||||
# share repo and add participant
|
||||
seafile_api.share_repo(self.repo.id, self.user.username, self.admin.username, 'rw')
|
||||
FileParticipant.objects.add_by_file_path_and_username(
|
||||
repo_id=self.repo.id, file_path=self.file, username=self.admin.username)
|
||||
|
||||
resp = self.client.post(self.endpoint, {
|
||||
'comment': 'new comment'
|
||||
@ -100,28 +102,3 @@ class FileCommentsTest(BaseTestCase):
|
||||
|
||||
assert len(UserNotification.objects.all()) == 1
|
||||
assert UserNotification.objects.all()[0].to_user == self.admin.username
|
||||
|
||||
def test_can_notify_others_including_group(self):
|
||||
self.logout()
|
||||
self.login_as(self.tmp_user)
|
||||
|
||||
assert len(UserNotification.objects.all()) == 0
|
||||
|
||||
# share repo to tmp_user
|
||||
username = self.user.username
|
||||
seafile_api.share_repo(self.repo.id, username,
|
||||
self.tmp_user.username, 'rw')
|
||||
|
||||
# share repo to group(owner, admin)
|
||||
ccnet_api.group_add_member(self.group.id, username,
|
||||
self.admin.username)
|
||||
seafile_api.set_group_repo(self.repo.id, self.group.id,
|
||||
username, 'rw')
|
||||
|
||||
# tmp_user comment a file
|
||||
resp = self.client.post(self.endpoint, {
|
||||
'comment': 'new comment'
|
||||
})
|
||||
self.assertEqual(201, resp.status_code)
|
||||
|
||||
assert len(UserNotification.objects.all()) == 2
|
||||
|
0
tests/seahub/file_participant/__init__.py
Normal file
0
tests/seahub/file_participant/__init__.py
Normal file
48
tests/seahub/file_participant/test_file_participant.py
Normal file
48
tests/seahub/file_participant/test_file_participant.py
Normal file
@ -0,0 +1,48 @@
|
||||
import json
|
||||
|
||||
from seaserv import seafile_api
|
||||
from django.core.urlresolvers import reverse
|
||||
from seahub.file_participants.models import FileParticipant
|
||||
from seahub.test_utils import BaseTestCase
|
||||
from tests.common.utils import randstring
|
||||
|
||||
|
||||
class FileParticipantTest(BaseTestCase):
|
||||
def setUp(self):
|
||||
self.tmp_user = self.create_user()
|
||||
|
||||
self.login_as(self.user)
|
||||
self.url = reverse('api-v2.1-file-participant', args=[self.repo.id]) + '?path=' + self.file
|
||||
# share repo and add participant
|
||||
seafile_api.share_repo(self.repo.id, self.user.username, self.tmp_user.username, 'rw')
|
||||
FileParticipant.objects.add_by_file_path_and_username(
|
||||
repo_id=self.repo.id, file_path=self.file, username=self.tmp_user.username)
|
||||
|
||||
def tearDown(self):
|
||||
self.remove_repo()
|
||||
self.remove_user(self.tmp_user.email)
|
||||
|
||||
def test_can_delete(self):
|
||||
data = 'email=' + self.tmp_user.username
|
||||
resp = self.client.delete(self.url, data, 'application/x-www-form-urlencoded')
|
||||
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
json_resp = json.loads(resp.content)
|
||||
assert json_resp['success']
|
||||
|
||||
def test_can_not_delete_by_not_exists_path(self):
|
||||
invalid_path = randstring(5)
|
||||
data = 'email=' + self.tmp_user.username
|
||||
resp = self.client.delete(self.url + invalid_path, data, 'application/x-www-form-urlencoded')
|
||||
|
||||
self.assertEqual(404, resp.status_code)
|
||||
|
||||
def test_can_not_delete_by_invalid_user_permission(self):
|
||||
self.logout()
|
||||
self.login_as(self.admin)
|
||||
|
||||
data = 'email=' + self.tmp_user.username
|
||||
resp = self.client.delete(self.url, data, 'application/x-www-form-urlencoded')
|
||||
|
||||
self.assertEqual(403, resp.status_code)
|
88
tests/seahub/file_participant/test_file_participants.py
Normal file
88
tests/seahub/file_participant/test_file_participants.py
Normal file
@ -0,0 +1,88 @@
|
||||
import json
|
||||
|
||||
from seaserv import seafile_api
|
||||
from django.core.urlresolvers import reverse
|
||||
from seahub.file_participants.models import FileParticipant
|
||||
from seahub.test_utils import BaseTestCase
|
||||
from tests.common.utils import randstring
|
||||
|
||||
|
||||
class FileParticipantsTest(BaseTestCase):
|
||||
def setUp(self):
|
||||
self.tmp_user = self.create_user()
|
||||
|
||||
self.login_as(self.user)
|
||||
self.url = reverse('api-v2.1-file-participants', args=[self.repo.id]) + '?path=' + self.file
|
||||
# share repo and add participant
|
||||
seafile_api.share_repo(self.repo.id, self.user.username, self.tmp_user.username, 'rw')
|
||||
FileParticipant.objects.add_by_file_path_and_username(
|
||||
repo_id=self.repo.id, file_path=self.file, username=self.user.username)
|
||||
|
||||
def tearDown(self):
|
||||
self.remove_repo()
|
||||
self.remove_user(self.tmp_user.email)
|
||||
|
||||
def test_can_list(self):
|
||||
resp = self.client.get(self.url)
|
||||
self.assertEqual(200, resp.status_code)
|
||||
|
||||
json_resp = json.loads(resp.content)
|
||||
assert json_resp['participant_list']
|
||||
assert json_resp['participant_list'][0]
|
||||
assert json_resp['participant_list'][0]['email'] == self.user.username
|
||||
assert json_resp['participant_list'][0]['avatar_url']
|
||||
assert json_resp['participant_list'][0]['contact_email']
|
||||
assert json_resp['participant_list'][0]['name']
|
||||
|
||||
def test_can_not_list_by_not_exists_path(self):
|
||||
invalid_path = randstring(5)
|
||||
resp = self.client.get(self.url + invalid_path)
|
||||
self.assertEqual(404, resp.status_code)
|
||||
|
||||
def test_can_not_list_by_invalid_user_permission(self):
|
||||
self.logout()
|
||||
self.login_as(self.admin)
|
||||
|
||||
resp = self.client.get(self.url)
|
||||
self.assertEqual(403, resp.status_code)
|
||||
|
||||
def test_can_post(self):
|
||||
resp = self.client.post(self.url, {
|
||||
'email': self.tmp_user.username
|
||||
})
|
||||
self.assertEqual(201, resp.status_code)
|
||||
|
||||
json_resp = json.loads(resp.content)
|
||||
assert json_resp['email'] == self.tmp_user.username
|
||||
assert json_resp['avatar_url']
|
||||
assert json_resp['contact_email']
|
||||
assert json_resp['name']
|
||||
|
||||
def test_can_not_post_by_not_exists_path(self):
|
||||
invalid_path = randstring(5)
|
||||
resp = self.client.post(self.url + invalid_path, {
|
||||
'email': self.tmp_user.username
|
||||
})
|
||||
self.assertEqual(404, resp.status_code)
|
||||
|
||||
def test_can_not_post_by_invalid_user_permission(self):
|
||||
self.logout()
|
||||
self.login_as(self.admin)
|
||||
|
||||
resp = self.client.post(self.url, {
|
||||
'email': self.tmp_user.username
|
||||
})
|
||||
self.assertEqual(403, resp.status_code)
|
||||
|
||||
def test_can_not_post_by_not_exists_user(self):
|
||||
invalid_email = randstring(5) + '@seafile.com'
|
||||
resp = self.client.post(self.url, {
|
||||
'email': invalid_email
|
||||
})
|
||||
self.assertEqual(404, resp.status_code)
|
||||
|
||||
def test_can_not_post_by_invalid_email_permission(self):
|
||||
resp = self.client.post(self.url, {
|
||||
'email': self.admin.username
|
||||
})
|
||||
self.assertEqual(403, resp.status_code)
|
Loading…
Reference in New Issue
Block a user