diff --git a/seahub/api2/endpoints/invitations.py b/seahub/api2/endpoints/invitations.py index 91d7cb0c77..ba917b31a6 100644 --- a/seahub/api2/endpoints/invitations.py +++ b/seahub/api2/endpoints/invitations.py @@ -72,3 +72,67 @@ class InvitationsView(APIView): i.send_to(email=accepter) return Response(i.to_dict(), status=201) + + +class InvitationsBatchView(APIView): + authentication_classes = (TokenAuthentication, SessionAuthentication) + permission_classes = (IsAuthenticated, CanInviteGuest) + throttle_classes = (UserRateThrottle,) + + def post(self, request): + + itype = request.data.get('type', '').lower() + if not itype or itype != 'guest': + return api_error(status.HTTP_400_BAD_REQUEST, 'type invalid.') + + accepters = request.data.getlist('accepter', None) + if not accepters: + return api_error(status.HTTP_400_BAD_REQUEST, 'accepters invalid.') + + result = {} + result['failed'] = [] + result['success'] = [] + + for accepter in accepters: + + if not accepter.strip(): + continue + + accepter = accepter.lower() + + if not is_valid_email(accepter): + result['failed'].append({ + 'email': accepter, + 'error_msg': _('Email %s invalid.') % accepter + }) + continue + + if block_accepter(accepter): + result['failed'].append({ + 'email': accepter, + 'error_msg': _('The email address is not allowed to be invited as a guest.') + }) + continue + + if Invitation.objects.filter(inviter=request.user.username, + accepter=accepter).count() > 0: + result['failed'].append({ + 'email': accepter, + 'error_msg': _('%s is already invited.') % accepter + }) + continue + + try: + User.objects.get(accepter) + result['failed'].append({ + 'email': accepter, + 'error_msg': _('User %s already exists.') % accepter + }) + continue + except User.DoesNotExist: + i = Invitation.objects.add(inviter=request.user.username, + accepter=accepter) + i.send_to(email=accepter) + result['success'].append(i.to_dict()) + + return Response(result) diff --git a/seahub/templates/js/templates.html b/seahub/templates/js/templates.html index 69cd3dca9b..cca557039d 100644 --- a/seahub/templates/js/templates.html +++ b/seahub/templates/js/templates.html @@ -2514,7 +2514,7 @@
{% csrf_token %}

{% trans "Invite People" %}


-
+

diff --git a/seahub/urls.py b/seahub/urls.py index 2c441c378c..3c1aad739c 100644 --- a/seahub/urls.py +++ b/seahub/urls.py @@ -45,7 +45,7 @@ from seahub.api2.endpoints.share_link_zip_task import ShareLinkZipTaskView from seahub.api2.endpoints.query_zip_progress import QueryZipProgressView from seahub.api2.endpoints.copy_move_task import CopyMoveTaskView from seahub.api2.endpoints.query_copy_move_progress import QueryCopyMoveProgressView -from seahub.api2.endpoints.invitations import InvitationsView +from seahub.api2.endpoints.invitations import InvitationsView, InvitationsBatchView from seahub.api2.endpoints.invitation import InvitationView from seahub.api2.endpoints.notifications import NotificationsView, NotificationView from seahub.api2.endpoints.user_enabled_modules import UserEnabledModulesView @@ -274,6 +274,7 @@ urlpatterns = patterns( ## user::invitations url(r'^api/v2.1/invitations/$', InvitationsView.as_view()), + url(r'^api/v2.1/invitations/batch/$', InvitationsBatchView.as_view()), url(r'^api/v2.1/invitations/(?P[a-f0-9]{32})/$', InvitationView.as_view()), ## user::avatar diff --git a/static/scripts/app/views/invitation.js b/static/scripts/app/views/invitation.js index c8ad13da02..a882a9b167 100644 --- a/static/scripts/app/views/invitation.js +++ b/static/scripts/app/views/invitation.js @@ -34,7 +34,7 @@ define([ beforeSend: Common.prepareCSRFToken, success: function() { _this.remove(); - Common.feedback(gettext("Successfully deleted 1 item"), 'success'); + Common.feedback(gettext("Successfully deleted 1 item."), 'success'); }, error: function(xhr) { Common.ajaxErrorHandler(xhr); diff --git a/static/scripts/app/views/invitations.js b/static/scripts/app/views/invitations.js index e610dcf385..cb4d40b8f3 100644 --- a/static/scripts/app/views/invitations.js +++ b/static/scripts/app/views/invitations.js @@ -46,38 +46,80 @@ define([ $('#simplemodal-container').css({'height':'auto'}); $form.submit(function() { - var accepter = $.trim($('input[name="accepter"]', $form).val()); + var accepters = $.trim($('input[name="accepter"]', $form).val()); + var accepter_list = []; + var email; + var $error = $('.error', $form); var $submitBtn = $('[type="submit"]', $form); var $loading = $('.loading-icon', $form); - if (!accepter) { + + if (!accepters) { $error.html(gettext("It is required.")).show(); return false; }; + accepters = accepters.split(','); + for (var i = 0, len = accepters.length; i < len; i++) { + email = $.trim(accepters[i]); + if (email) { + accepter_list.push(email); + } + } + if (!accepter_list.length) { + return false; + } $error.hide(); Common.disableButton($submitBtn); $loading.show(); - _this.collection.create({ - 'type': 'guest', - 'accepter': accepter - }, { - wait: true, - prepend: true, - success: function() { - if (_this.collection.length == 1) { - _this.reset(); + $.ajax({ + url: Common.getUrl({'name': 'invitations_batch'}), + type: 'POST', + cache: false, + data: { + 'type': 'guest', + 'accepter': accepter_list + }, + traditional: true, + beforeSend: Common.prepareCSRFToken, + success: function(data) { + var msgs = []; + if (data.success.length) { + var msg; + _this.collection.add(data.success, {prepend: true}); + if (_this.collection.length == data.success.length) { + _this.reset(); + } + if (data.success.length == 1) { + msg = gettext('Successfully invited %(email).') + .replace('%(email)', data.success[0].accepter); + } else { + msg = gettext('Successfully invited %(email) and %(num) other people.') + .replace('%(email)', data.success[0].accepter) + .replace('%(num)', data.success.length - 1); + } + msgs.push({'msg': msg, 'type': 'success'}); + } + if (data.failed.length) { + $(data.failed).each(function(index, item) { + var err_msg = item.email + ': ' + item.error_msg; + msgs.push({'msg': err_msg, 'type': 'error'}); + }); + } + if (msgs.length) { + Common.feedback(msgs); } $.modal.close(); }, - error: function(collection, response, options) { + error: function(xhr) { var err_msg; - if (response.responseText) { - err_msg = response.responseJSON.error_msg||response.responseJSON.detail; + if (xhr.responseText) { + err_msg = xhr.responseJSON.error_msg||xhr.responseJSON.detail; } else { err_msg = gettext('Please check the network.'); } $error.html(err_msg).show(); + Common.enableButton($submitBtn); }, complete: function() { diff --git a/static/scripts/common.js b/static/scripts/common.js index 9302227a85..c8bc9c411e 100644 --- a/static/scripts/common.js +++ b/static/scripts/common.js @@ -170,6 +170,7 @@ define([ case 'events': return siteRoot + 'api2/events/'; case 'devices': return siteRoot + 'api2/devices/'; case 'invitations': return siteRoot + 'api/v2.1/invitations/'; + case 'invitations_batch': return siteRoot + 'api/v2.1/invitations/batch/'; case 'invitation': return siteRoot + 'api/v2.1/invitations/' + options.token + '/'; case 'search_user': return siteRoot + 'api2/search-user/'; case 'user_profile': return siteRoot + 'profile/' + options.username + '/'; @@ -374,14 +375,25 @@ define([ }, feedback: function(con, type, time) { + var _this = this; var time = time || 5000; var $el; var hide_pos_top, show_pos_top = '15px'; + + var $con, str = ''; + if (typeof con == 'string') { // most of the time + $con = $('
  • ' + this.HTMLescape(con) + '
  • '); + } else { // [{'msg':'', 'type':''}] + $(con).each(function(index, item) { + str += '
  • ' + _this.HTMLescape(item.msg) + '
  • '; + }); + $con = $(str); + } if ($('.messages').length > 0) { - $el = $('.messages').html('
  • ' + this.HTMLescape(con) + '
  • '); + $el = $('.messages').html($con); } else { - $el = $(''); + $el = $('').html($con); $('#main').append($el); } diff --git a/tests/api/endpoints/test_invitations.py b/tests/api/endpoints/test_invitations.py index 1675c261c0..00a6a44335 100644 --- a/tests/api/endpoints/test_invitations.py +++ b/tests/api/endpoints/test_invitations.py @@ -113,3 +113,137 @@ class InvitationsTest(BaseTestCase): self.assertEqual(200, resp.status_code) json_resp = json.loads(resp.content) assert len(json_resp) == 2 + + +class BatchInvitationsTest(BaseTestCase): + def setUp(self): + self.login_as(self.user) + self.endpoint = '/api/v2.1/invitations/batch/' + self.username = self.user.username + + @patch.object(CanInviteGuest, 'has_permission') + @patch.object(UserPermissions, 'can_invite_guest') + def test_can_add_with_batch(self, mock_can_invite_guest, mock_has_permission): + + mock_can_invite_guest.return_val = True + mock_has_permission.return_val = True + + assert len(Invitation.objects.all()) == 0 + resp = self.client.post(self.endpoint, { + 'type': 'guest', + 'accepter': ['some_random_user@1.com', 'some_random_user@2.com'], + }) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert self.username == json_resp['success'][0]['inviter'] + assert 'some_random_user@1.com' == json_resp['success'][0]['accepter'] + assert 'some_random_user@2.com' == json_resp['success'][1]['accepter'] + assert json_resp['success'][0]['expire_time'] is not None + + assert len(Invitation.objects.all()) == 2 + + @patch.object(CanInviteGuest, 'has_permission') + @patch.object(UserPermissions, 'can_invite_guest') + def test_can_not_add_same_email_with_batch(self, mock_can_invite_guest, mock_has_permission): + + mock_can_invite_guest.return_val = True + mock_has_permission.return_val = True + + assert len(Invitation.objects.all()) == 0 + resp = self.client.post(self.endpoint, { + 'type': 'guest', + 'accepter': ['some_random_user@1.com', 'some_random_user@2.com'], + }) + self.assertEqual(200, resp.status_code) + + json_resp = json.loads(resp.content) + assert self.username == json_resp['success'][0]['inviter'] + assert 'some_random_user@1.com' == json_resp['success'][0]['accepter'] + assert 'some_random_user@2.com' == json_resp['success'][1]['accepter'] + assert json_resp['success'][0]['expire_time'] is not None + + resp = self.client.post(self.endpoint, { + 'type': 'guest', + 'accepter': ['some_random_user@1.com', 'some_random_user@2.com'], + }) + json_resp = json.loads(resp.content) + assert 'some_random_user@1.com' == json_resp['failed'][0]['email'] + assert 'some_random_user@2.com' == json_resp['failed'][1]['email'] + assert 'already invited' in json_resp['failed'][0]['error_msg'] + assert 'already invited' in json_resp['failed'][1]['error_msg'] + + @override_settings(INVITATION_ACCEPTER_BLACKLIST=["*@2-1.com", "*@1-1.com", r".*@(foo|bar).com"]) + @patch.object(CanInviteGuest, 'has_permission') + @patch.object(UserPermissions, 'can_invite_guest') + def test_can_not_add_blocked_email(self, mock_can_invite_guest, mock_has_permission): + + mock_can_invite_guest.return_val = True + mock_has_permission.return_val = True + + assert len(Invitation.objects.all()) == 0 + resp = self.client.post(self.endpoint, { + 'type': 'guest', + 'accepter': ['some_random_user@1-1.com', 'some_random_user@2-1.com'], + }) + assert len(Invitation.objects.all()) == 0 + json_resp = json.loads(resp.content) + assert 'some_random_user@1-1.com' == json_resp['failed'][0]['email'] + assert 'some_random_user@2-1.com' == json_resp['failed'][1]['email'] + assert 'The email address is not allowed to be invited as a guest.' == json_resp['failed'][0]['error_msg'] + assert 'The email address is not allowed to be invited as a guest.' == json_resp['failed'][1]['error_msg'] + + @patch.object(CanInviteGuest, 'has_permission') + @patch.object(UserPermissions, 'can_invite_guest') + def test_can_send_mail(self, mock_can_invite_guest, mock_has_permission): + + mock_can_invite_guest.return_val = True + mock_has_permission.return_val = True + + self.assertEqual(len(Email.objects.all()), 0) + + resp = self.client.post(self.endpoint, { + 'type': 'guest', + 'accepter': ['some_random_user@1.com', 'some_random_user@2.com'], + }) + self.assertEqual(200, resp.status_code) + json_resp = json.loads(resp.content) + + self.assertEqual(len(Email.objects.all()), 2) + self.assertRegexpMatches(Email.objects.all()[0].html_message, + json_resp['success'][0]['token']) + self.assertRegexpMatches(Email.objects.all()[1].html_message, + json_resp['success'][1]['token']) + assert Email.objects.all()[0].status == 0 + assert Email.objects.all()[1].status == 0 + + def test_without_permission(self): + self.logout() + resp = self.client.post(self.endpoint, { + 'type': 'guest', + 'accepter': ['some_random_user@1-1.com', 'some_random_user@2-1.com'], + }) + json_resp = json.loads(resp.content) + + assert len(Invitation.objects.all()) == 0 + self.assertEqual(403, resp.status_code) + assert 'Authentication credentials were not provided.' == json_resp['detail'] + + @patch.object(CanInviteGuest, 'has_permission') + @patch.object(UserPermissions, 'can_invite_guest') + def test_with_invalid_email(self, mock_can_invite_guest, mock_has_permission): + + mock_can_invite_guest.return_val = True + mock_has_permission.return_val = True + + resp = self.client.post(self.endpoint, { + 'type': 'guest', + 'accepter': ['some_random _user@1-1.com', 's ome_random_user@2-1.com'], + }) + json_resp = json.loads(resp.content) + + assert len(Invitation.objects.all()) == 0 + assert 'some_random _user@1-1.com' == json_resp['failed'][0]['email'] + assert 's ome_random_user@2-1.com' == json_resp['failed'][1]['email'] + assert 'invalid.' in json_resp['failed'][0]['error_msg'] + assert 'invalid.' in json_resp['failed'][0]['error_msg']