1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-07-16 16:21:48 +00:00

Merge pull request #1952 from haiwen/invite-multi

[invite people] enable to invite multiple guests at one time
This commit is contained in:
xiez 2017-12-25 13:32:42 +08:00 committed by GitHub
commit baeb8ccacf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 272 additions and 19 deletions

View File

@ -72,3 +72,67 @@ class InvitationsView(APIView):
i.send_to(email=accepter) i.send_to(email=accepter)
return Response(i.to_dict(), status=201) 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)

View File

@ -2514,7 +2514,7 @@
<form id="invitation-form" action="" method="post">{% csrf_token %} <form id="invitation-form" action="" method="post">{% csrf_token %}
<h3 id="dialogTitle">{% trans "Invite People" %}</h3> <h3 id="dialogTitle">{% trans "Invite People" %}</h3>
<label for="accepter">{% trans "Email" %}</label><br/> <label for="accepter">{% trans "Email" %}</label><br/>
<input id="accepter" type="text" name="accepter" value="" class="input" /><br /> <input id="accepter" type="text" name="accepter" value="" class="input" placeholder="{% trans "Emails, separated by ','"%}" title="{% trans "Emails, separated by ','"%}" /><br />
<p class="error hide"></p> <p class="error hide"></p>
<input type="submit" value="{% trans "Submit" %}" class="submit vam" /> <input type="submit" value="{% trans "Submit" %}" class="submit vam" />
<span class="loading-icon vam" style="margin-left:5px;display:none;"></span> <span class="loading-icon vam" style="margin-left:5px;display:none;"></span>

View File

@ -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.query_zip_progress import QueryZipProgressView
from seahub.api2.endpoints.copy_move_task import CopyMoveTaskView from seahub.api2.endpoints.copy_move_task import CopyMoveTaskView
from seahub.api2.endpoints.query_copy_move_progress import QueryCopyMoveProgressView 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.invitation import InvitationView
from seahub.api2.endpoints.notifications import NotificationsView, NotificationView from seahub.api2.endpoints.notifications import NotificationsView, NotificationView
from seahub.api2.endpoints.user_enabled_modules import UserEnabledModulesView from seahub.api2.endpoints.user_enabled_modules import UserEnabledModulesView
@ -274,6 +274,7 @@ urlpatterns = patterns(
## user::invitations ## user::invitations
url(r'^api/v2.1/invitations/$', InvitationsView.as_view()), 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<token>[a-f0-9]{32})/$', InvitationView.as_view()), url(r'^api/v2.1/invitations/(?P<token>[a-f0-9]{32})/$', InvitationView.as_view()),
## user::avatar ## user::avatar

View File

@ -34,7 +34,7 @@ define([
beforeSend: Common.prepareCSRFToken, beforeSend: Common.prepareCSRFToken,
success: function() { success: function() {
_this.remove(); _this.remove();
Common.feedback(gettext("Successfully deleted 1 item"), 'success'); Common.feedback(gettext("Successfully deleted 1 item."), 'success');
}, },
error: function(xhr) { error: function(xhr) {
Common.ajaxErrorHandler(xhr); Common.ajaxErrorHandler(xhr);

View File

@ -46,38 +46,80 @@ define([
$('#simplemodal-container').css({'height':'auto'}); $('#simplemodal-container').css({'height':'auto'});
$form.submit(function() { $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 $error = $('.error', $form);
var $submitBtn = $('[type="submit"]', $form); var $submitBtn = $('[type="submit"]', $form);
var $loading = $('.loading-icon', $form); var $loading = $('.loading-icon', $form);
if (!accepter) {
if (!accepters) {
$error.html(gettext("It is required.")).show(); $error.html(gettext("It is required.")).show();
return false; 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(); $error.hide();
Common.disableButton($submitBtn); Common.disableButton($submitBtn);
$loading.show(); $loading.show();
_this.collection.create({ $.ajax({
'type': 'guest', url: Common.getUrl({'name': 'invitations_batch'}),
'accepter': accepter type: 'POST',
}, { cache: false,
wait: true, data: {
prepend: true, 'type': 'guest',
success: function() { 'accepter': accepter_list
if (_this.collection.length == 1) { },
_this.reset(); 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(); $.modal.close();
}, },
error: function(collection, response, options) { error: function(xhr) {
var err_msg; var err_msg;
if (response.responseText) { if (xhr.responseText) {
err_msg = response.responseJSON.error_msg||response.responseJSON.detail; err_msg = xhr.responseJSON.error_msg||xhr.responseJSON.detail;
} else { } else {
err_msg = gettext('Please check the network.'); err_msg = gettext('Please check the network.');
} }
$error.html(err_msg).show(); $error.html(err_msg).show();
Common.enableButton($submitBtn); Common.enableButton($submitBtn);
}, },
complete: function() { complete: function() {

View File

@ -170,6 +170,7 @@ define([
case 'events': return siteRoot + 'api2/events/'; case 'events': return siteRoot + 'api2/events/';
case 'devices': return siteRoot + 'api2/devices/'; case 'devices': return siteRoot + 'api2/devices/';
case 'invitations': return siteRoot + 'api/v2.1/invitations/'; 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 'invitation': return siteRoot + 'api/v2.1/invitations/' + options.token + '/';
case 'search_user': return siteRoot + 'api2/search-user/'; case 'search_user': return siteRoot + 'api2/search-user/';
case 'user_profile': return siteRoot + 'profile/' + options.username + '/'; case 'user_profile': return siteRoot + 'profile/' + options.username + '/';
@ -374,14 +375,25 @@ define([
}, },
feedback: function(con, type, time) { feedback: function(con, type, time) {
var _this = this;
var time = time || 5000; var time = time || 5000;
var $el; var $el;
var hide_pos_top, var hide_pos_top,
show_pos_top = '15px'; show_pos_top = '15px';
var $con, str = '';
if (typeof con == 'string') { // most of the time
$con = $('<li class="' + type + '">' + this.HTMLescape(con) + '</li>');
} else { // [{'msg':'', 'type':''}]
$(con).each(function(index, item) {
str += '<li class="' + item.type + '">' + _this.HTMLescape(item.msg) + '</li>';
});
$con = $(str);
}
if ($('.messages').length > 0) { if ($('.messages').length > 0) {
$el = $('.messages').html('<li class="' + type + '">' + this.HTMLescape(con) + '</li>'); $el = $('.messages').html($con);
} else { } else {
$el = $('<ul class="messages"><li class="' + type + '">' + this.HTMLescape(con) + '</li></ul>'); $el = $('<ul class="messages"></ul>').html($con);
$('#main').append($el); $('#main').append($el);
} }

View File

@ -113,3 +113,137 @@ class InvitationsTest(BaseTestCase):
self.assertEqual(200, resp.status_code) self.assertEqual(200, resp.status_code)
json_resp = json.loads(resp.content) json_resp = json.loads(resp.content)
assert len(json_resp) == 2 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']