mirror of https://github.com/zulip/zulip.git
invitation: Make Member to see invitations sent by him/her.
Member of the org can able see list of invitations sent by him/her. given permission for the member to revoke and resend the invitations sent by him/her and added tests for test member can revoke and resend the invitations only sent by him/her. Fixes #14007.
This commit is contained in:
parent
fc107d2c24
commit
bbf5a5efed
|
@ -50,6 +50,7 @@ function populate_invites(invites_data) {
|
||||||
name: 'admin_invites_list',
|
name: 'admin_invites_list',
|
||||||
modifier: function (item) {
|
modifier: function (item) {
|
||||||
item.invited_absolute_time = timerender.absolute_time(item.invited * 1000);
|
item.invited_absolute_time = timerender.absolute_time(item.invited * 1000);
|
||||||
|
item.is_admin = page_params.is_admin;
|
||||||
return render_admin_invites_list({ invite: item });
|
return render_admin_invites_list({ invite: item });
|
||||||
},
|
},
|
||||||
filter: {
|
filter: {
|
||||||
|
|
|
@ -7,9 +7,11 @@
|
||||||
<span class="email">{{email}}</span>
|
<span class="email">{{email}}</span>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
</td>
|
</td>
|
||||||
|
{{#if is_admin}}
|
||||||
<td>
|
<td>
|
||||||
<span class="referred_by">{{ref}}</span>
|
<span class="referred_by">{{ref}}</span>
|
||||||
</td>
|
</td>
|
||||||
|
{{/if}}
|
||||||
<td>
|
<td>
|
||||||
<span class="invited_at">{{invited_absolute_time}}</span>
|
<span class="invited_at">{{invited_absolute_time}}</span>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
<div id="admin-invites-list" class="settings-section" data-name="invites-list-admin">
|
<div id="admin-invites-list" class="settings-section" data-name="invites-list-admin">
|
||||||
|
{{#unless is_admin }}
|
||||||
|
<div class="tip">{{t "Members can only view or manage invitations that you yourself sent." }}</div>
|
||||||
|
{{/unless}}
|
||||||
<a class="invite-user-link" href="#invite"><i class="fa fa-plus-circle" aria-hidden="true"></i>{{t "Invite more users" }}</a>
|
<a class="invite-user-link" href="#invite"><i class="fa fa-plus-circle" aria-hidden="true"></i>{{t "Invite more users" }}</a>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="inline-block">{{t "Invites" }}</h3>
|
<h3 class="inline-block">{{t "Invites" }}</h3>
|
||||||
|
@ -11,7 +14,9 @@
|
||||||
<table class="table table-condensed table-striped">
|
<table class="table table-condensed table-striped">
|
||||||
<thead>
|
<thead>
|
||||||
<th class="active" data-sort="invitee">{{t "Invitee" }}</th>
|
<th class="active" data-sort="invitee">{{t "Invitee" }}</th>
|
||||||
|
{{#if is_admin }}
|
||||||
<th data-sort="alphabetic" data-sort-prop="ref">{{t "Invited by" }}</th>
|
<th data-sort="alphabetic" data-sort-prop="ref">{{t "Invited by" }}</th>
|
||||||
|
{{/if}}
|
||||||
<th data-sort="numeric" data-sort-prop="invited">{{t "Invited at" }}</th>
|
<th data-sort="numeric" data-sort-prop="invited">{{t "Invited at" }}</th>
|
||||||
<th data-sort="numeric" data-sort-prop="invited_as">{{t "Invited as" }}</th>
|
<th data-sort="numeric" data-sort-prop="invited_as">{{t "Invited as" }}</th>
|
||||||
<th class="actions">{{t "Actions" }}</th>
|
<th class="actions">{{t "Actions" }}</th>
|
||||||
|
|
|
@ -140,11 +140,13 @@
|
||||||
<div class="text">{{ _('Custom profile fields') }}</div>
|
<div class="text">{{ _('Custom profile fields') }}</div>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_admin %}
|
{% if not guest %}
|
||||||
<li tabindex="0" data-section="invites-list-admin">
|
<li tabindex="0" data-section="invites-list-admin">
|
||||||
<i class="icon fa fa-user-plus" aria-hidden="true"></i>
|
<i class="icon fa fa-user-plus" aria-hidden="true"></i>
|
||||||
<div class="text">{{ _('Invitations') }}</div>
|
<div class="text">{{ _('Invitations') }}</div>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if is_admin %}
|
||||||
<li tabindex="0" data-section="data-exports-admin">
|
<li tabindex="0" data-section="data-exports-admin">
|
||||||
<i class="icon fa fa-database" aria-hidden="true"></i>
|
<i class="icon fa fa-database" aria-hidden="true"></i>
|
||||||
<div class="text">{{ _('Data exports') }}</div>
|
<div class="text">{{ _('Data exports') }}</div>
|
||||||
|
|
|
@ -5099,11 +5099,14 @@ def do_invite_users(user_profile: UserProfile,
|
||||||
def do_get_user_invites(user_profile: UserProfile) -> List[Dict[str, Any]]:
|
def do_get_user_invites(user_profile: UserProfile) -> List[Dict[str, Any]]:
|
||||||
days_to_activate = settings.INVITATION_LINK_VALIDITY_DAYS
|
days_to_activate = settings.INVITATION_LINK_VALIDITY_DAYS
|
||||||
active_value = getattr(confirmation_settings, 'STATUS_ACTIVE', 1)
|
active_value = getattr(confirmation_settings, 'STATUS_ACTIVE', 1)
|
||||||
|
|
||||||
lowest_datetime = timezone_now() - datetime.timedelta(days=days_to_activate)
|
lowest_datetime = timezone_now() - datetime.timedelta(days=days_to_activate)
|
||||||
prereg_users = PreregistrationUser.objects.exclude(status=active_value).filter(
|
base_query = PreregistrationUser.objects.exclude(status=active_value).filter(
|
||||||
invited_at__gte=lowest_datetime,
|
invited_at__gte=lowest_datetime)
|
||||||
referred_by__realm=user_profile.realm)
|
|
||||||
|
if user_profile.is_realm_admin:
|
||||||
|
prereg_users = base_query.filter(referred_by__realm=user_profile.realm)
|
||||||
|
else:
|
||||||
|
prereg_users = base_query.filter(referred_by=user_profile)
|
||||||
|
|
||||||
invites = []
|
invites = []
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,7 @@ from zerver.lib.send_email import send_future_email, FromAddress, \
|
||||||
deliver_email
|
deliver_email
|
||||||
from zerver.lib.initial_password import initial_password
|
from zerver.lib.initial_password import initial_password
|
||||||
from zerver.lib.actions import (
|
from zerver.lib.actions import (
|
||||||
|
do_get_user_invites,
|
||||||
do_deactivate_realm,
|
do_deactivate_realm,
|
||||||
do_deactivate_user,
|
do_deactivate_user,
|
||||||
do_set_realm_property,
|
do_set_realm_property,
|
||||||
|
@ -1469,6 +1470,26 @@ so we didn't send them an invitation. We did send invitations to everyone else!"
|
||||||
"or been deactivated."], result)
|
"or been deactivated."], result)
|
||||||
|
|
||||||
class InvitationsTestCase(InviteUserBase):
|
class InvitationsTestCase(InviteUserBase):
|
||||||
|
def test_do_get_user_invites(self) -> None:
|
||||||
|
self.login('iago')
|
||||||
|
user_profile = self.example_user("iago")
|
||||||
|
hamlet = self.example_user('hamlet')
|
||||||
|
othello = self.example_user('othello')
|
||||||
|
prereg_user_one = PreregistrationUser(email="TestOne@zulip.com", referred_by=user_profile)
|
||||||
|
prereg_user_one.save()
|
||||||
|
prereg_user_two = PreregistrationUser(email="TestTwo@zulip.com", referred_by=user_profile)
|
||||||
|
prereg_user_two.save()
|
||||||
|
prereg_user_three = PreregistrationUser(email="TestThree@zulip.com", referred_by=hamlet)
|
||||||
|
prereg_user_three.save()
|
||||||
|
prereg_user_four = PreregistrationUser(email="TestFour@zulip.com", referred_by=othello)
|
||||||
|
prereg_user_four.save()
|
||||||
|
prereg_user_other_realm = PreregistrationUser(
|
||||||
|
email="TestOne@zulip.com", referred_by=self.mit_user("sipbtest"))
|
||||||
|
prereg_user_other_realm.save()
|
||||||
|
self.assertEqual(len(do_get_user_invites(user_profile)), 4)
|
||||||
|
self.assertEqual(len(do_get_user_invites(hamlet)), 1)
|
||||||
|
self.assertEqual(len(do_get_user_invites(othello)), 1)
|
||||||
|
|
||||||
def test_successful_get_open_invitations(self) -> None:
|
def test_successful_get_open_invitations(self) -> None:
|
||||||
"""
|
"""
|
||||||
A GET call to /json/invites returns all unexpired invitations.
|
A GET call to /json/invites returns all unexpired invitations.
|
||||||
|
@ -1537,6 +1558,46 @@ class InvitationsTestCase(InviteUserBase):
|
||||||
lambda: ScheduledEmail.objects.get(address__iexact=invitee,
|
lambda: ScheduledEmail.objects.get(address__iexact=invitee,
|
||||||
type=ScheduledEmail.INVITATION_REMINDER))
|
type=ScheduledEmail.INVITATION_REMINDER))
|
||||||
|
|
||||||
|
def test_successful_member_delete_invitation(self) -> None:
|
||||||
|
"""
|
||||||
|
A DELETE call from member account to /json/invites/<ID> should delete the invite and
|
||||||
|
any scheduled invitation reminder emails.
|
||||||
|
"""
|
||||||
|
user_profile = self.example_user('hamlet')
|
||||||
|
self.login_user(user_profile)
|
||||||
|
invitee = "DeleteMe@zulip.com"
|
||||||
|
self.assert_json_success(self.invite(invitee, ['Denmark']))
|
||||||
|
|
||||||
|
# Verify that the scheduled email exists.
|
||||||
|
prereg_user = PreregistrationUser.objects.get(email=invitee,
|
||||||
|
referred_by=user_profile)
|
||||||
|
ScheduledEmail.objects.get(address__iexact=invitee,
|
||||||
|
type=ScheduledEmail.INVITATION_REMINDER)
|
||||||
|
|
||||||
|
# Verify another non-admin can't delete
|
||||||
|
result = self.api_delete(self.example_user("othello"),
|
||||||
|
'/api/v1/invites/' + str(prereg_user.id))
|
||||||
|
self.assert_json_error(result, "Must be an organization administrator")
|
||||||
|
|
||||||
|
# Verify that the scheduled email still exists.
|
||||||
|
prereg_user = PreregistrationUser.objects.get(email=invitee,
|
||||||
|
referred_by=user_profile)
|
||||||
|
ScheduledEmail.objects.get(address__iexact=invitee,
|
||||||
|
type=ScheduledEmail.INVITATION_REMINDER)
|
||||||
|
|
||||||
|
# Verify deletion works.
|
||||||
|
result = self.api_delete(user_profile,
|
||||||
|
'/api/v1/invites/' + str(prereg_user.id))
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
|
||||||
|
result = self.api_delete(user_profile,
|
||||||
|
'/api/v1/invites/' + str(prereg_user.id))
|
||||||
|
self.assert_json_error(result, "No such invitation")
|
||||||
|
|
||||||
|
self.assertRaises(ScheduledEmail.DoesNotExist,
|
||||||
|
lambda: ScheduledEmail.objects.get(address__iexact=invitee,
|
||||||
|
type=ScheduledEmail.INVITATION_REMINDER))
|
||||||
|
|
||||||
def test_delete_multiuse_invite(self) -> None:
|
def test_delete_multiuse_invite(self) -> None:
|
||||||
"""
|
"""
|
||||||
A DELETE call to /json/invites/multiuse<ID> should delete the
|
A DELETE call to /json/invites/multiuse<ID> should delete the
|
||||||
|
@ -1599,6 +1660,56 @@ class InvitationsTestCase(InviteUserBase):
|
||||||
|
|
||||||
self.check_sent_emails([invitee], custom_from_name="Zulip")
|
self.check_sent_emails([invitee], custom_from_name="Zulip")
|
||||||
|
|
||||||
|
def test_successful_member_resend_invitation(self) -> None:
|
||||||
|
"""A POST call from member a account to /json/invites/<ID>/resend
|
||||||
|
should send an invitation reminder email and delete any
|
||||||
|
scheduled invitation reminder email if they send the invite.
|
||||||
|
"""
|
||||||
|
self.login('hamlet')
|
||||||
|
user_profile = self.example_user('hamlet')
|
||||||
|
invitee = "resend_me@zulip.com"
|
||||||
|
self.assert_json_success(self.invite(invitee, ['Denmark']))
|
||||||
|
# Verify hamlet has only one invitation (Member can resend invitations only sent by him).
|
||||||
|
invitation = PreregistrationUser.objects.filter(referred_by=user_profile)
|
||||||
|
self.assertEqual(len(invitation), 1)
|
||||||
|
prereg_user = PreregistrationUser.objects.get(email=invitee)
|
||||||
|
|
||||||
|
# Verify and then clear from the outbox the original invite email
|
||||||
|
self.check_sent_emails([invitee], custom_from_name="Zulip")
|
||||||
|
from django.core.mail import outbox
|
||||||
|
outbox.pop()
|
||||||
|
|
||||||
|
# Verify that the scheduled email exists.
|
||||||
|
scheduledemail_filter = ScheduledEmail.objects.filter(
|
||||||
|
address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER)
|
||||||
|
self.assertEqual(scheduledemail_filter.count(), 1)
|
||||||
|
original_timestamp = scheduledemail_filter.values_list('scheduled_timestamp', flat=True)
|
||||||
|
|
||||||
|
# Resend invite
|
||||||
|
result = self.client_post('/json/invites/' + str(prereg_user.id) + '/resend')
|
||||||
|
self.assertEqual(ScheduledEmail.objects.filter(
|
||||||
|
address__iexact=invitee, type=ScheduledEmail.INVITATION_REMINDER).count(), 1)
|
||||||
|
|
||||||
|
# Check that we have exactly one scheduled email, and that it is different
|
||||||
|
self.assertEqual(scheduledemail_filter.count(), 1)
|
||||||
|
self.assertNotEqual(original_timestamp,
|
||||||
|
scheduledemail_filter.values_list('scheduled_timestamp', flat=True))
|
||||||
|
|
||||||
|
self.assertEqual(result.status_code, 200)
|
||||||
|
error_result = self.client_post('/json/invites/' + str(9999) + '/resend')
|
||||||
|
self.assert_json_error(error_result, "No such invitation")
|
||||||
|
|
||||||
|
self.check_sent_emails([invitee], custom_from_name="Zulip")
|
||||||
|
|
||||||
|
self.logout()
|
||||||
|
self.login("othello")
|
||||||
|
invitee = "TestOne@zulip.com"
|
||||||
|
prereg_user_one = PreregistrationUser(email=invitee, referred_by=user_profile)
|
||||||
|
prereg_user_one.save()
|
||||||
|
prereg_user = PreregistrationUser.objects.get(email=invitee)
|
||||||
|
error_result = self.client_post('/json/invites/' + str(prereg_user.id) + '/resend')
|
||||||
|
self.assert_json_error(error_result, "Must be an organization administrator")
|
||||||
|
|
||||||
def test_accessing_invites_in_another_realm(self) -> None:
|
def test_accessing_invites_in_another_realm(self) -> None:
|
||||||
inviter = UserProfile.objects.exclude(realm=get_realm('zulip')).first()
|
inviter = UserProfile.objects.exclude(realm=get_realm('zulip')).first()
|
||||||
prereg_user = PreregistrationUser.objects.create(
|
prereg_user = PreregistrationUser.objects.create(
|
||||||
|
|
|
@ -59,12 +59,12 @@ def get_invitee_emails_set(invitee_emails_raw: str) -> Set[str]:
|
||||||
invitee_emails.add(email.strip())
|
invitee_emails.add(email.strip())
|
||||||
return invitee_emails
|
return invitee_emails
|
||||||
|
|
||||||
@require_realm_admin
|
@require_member_or_admin
|
||||||
def get_user_invites(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
|
def get_user_invites(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
|
||||||
all_users = do_get_user_invites(user_profile)
|
all_users = do_get_user_invites(user_profile)
|
||||||
return json_success({'invites': all_users})
|
return json_success({'invites': all_users})
|
||||||
|
|
||||||
@require_realm_admin
|
@require_member_or_admin
|
||||||
@has_request_variables
|
@has_request_variables
|
||||||
def revoke_user_invite(request: HttpRequest, user_profile: UserProfile,
|
def revoke_user_invite(request: HttpRequest, user_profile: UserProfile,
|
||||||
prereg_id: int) -> HttpResponse:
|
prereg_id: int) -> HttpResponse:
|
||||||
|
@ -76,6 +76,9 @@ def revoke_user_invite(request: HttpRequest, user_profile: UserProfile,
|
||||||
if prereg_user.referred_by.realm != user_profile.realm:
|
if prereg_user.referred_by.realm != user_profile.realm:
|
||||||
raise JsonableError(_("No such invitation"))
|
raise JsonableError(_("No such invitation"))
|
||||||
|
|
||||||
|
if prereg_user.referred_by_id != user_profile.id and not user_profile.is_realm_admin:
|
||||||
|
raise JsonableError(_("Must be an organization administrator"))
|
||||||
|
|
||||||
do_revoke_user_invite(prereg_user)
|
do_revoke_user_invite(prereg_user)
|
||||||
return json_success()
|
return json_success()
|
||||||
|
|
||||||
|
@ -95,7 +98,7 @@ def revoke_multiuse_invite(request: HttpRequest, user_profile: UserProfile,
|
||||||
do_revoke_multi_use_invite(invite)
|
do_revoke_multi_use_invite(invite)
|
||||||
return json_success()
|
return json_success()
|
||||||
|
|
||||||
@require_realm_admin
|
@require_member_or_admin
|
||||||
@has_request_variables
|
@has_request_variables
|
||||||
def resend_user_invite_email(request: HttpRequest, user_profile: UserProfile,
|
def resend_user_invite_email(request: HttpRequest, user_profile: UserProfile,
|
||||||
prereg_id: int) -> HttpResponse:
|
prereg_id: int) -> HttpResponse:
|
||||||
|
@ -109,6 +112,9 @@ def resend_user_invite_email(request: HttpRequest, user_profile: UserProfile,
|
||||||
if prereg_user.referred_by is None or prereg_user.referred_by.realm != user_profile.realm:
|
if prereg_user.referred_by is None or prereg_user.referred_by.realm != user_profile.realm:
|
||||||
raise JsonableError(_("No such invitation"))
|
raise JsonableError(_("No such invitation"))
|
||||||
|
|
||||||
|
if prereg_user.referred_by_id != user_profile.id and not user_profile.is_realm_admin:
|
||||||
|
raise JsonableError(_("Must be an organization administrator"))
|
||||||
|
|
||||||
timestamp = do_resend_user_invite_email(prereg_user)
|
timestamp = do_resend_user_invite_email(prereg_user)
|
||||||
return json_success({'timestamp': timestamp})
|
return json_success({'timestamp': timestamp})
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue