diff --git a/static/js/settings_invites.js b/static/js/settings_invites.js
index ecae2edecd..85f0fd6515 100644
--- a/static/js/settings_invites.js
+++ b/static/js/settings_invites.js
@@ -50,6 +50,7 @@ function populate_invites(invites_data) {
name: 'admin_invites_list',
modifier: function (item) {
item.invited_absolute_time = timerender.absolute_time(item.invited * 1000);
+ item.is_admin = page_params.is_admin;
return render_admin_invites_list({ invite: item });
},
filter: {
diff --git a/static/templates/admin_invites_list.hbs b/static/templates/admin_invites_list.hbs
index 0b04f6ef87..b2306dc803 100644
--- a/static/templates/admin_invites_list.hbs
+++ b/static/templates/admin_invites_list.hbs
@@ -7,9 +7,11 @@
{{email}}
{{/if}}
+ {{#if is_admin}}
{{ref}}
|
+ {{/if}}
{{invited_absolute_time}}
|
diff --git a/static/templates/settings/invites_list_admin.hbs b/static/templates/settings/invites_list_admin.hbs
index 624e59eb75..d8905d64ad 100644
--- a/static/templates/settings/invites_list_admin.hbs
+++ b/static/templates/settings/invites_list_admin.hbs
@@ -1,4 +1,7 @@
+ {{#unless is_admin }}
+
{{t "Members can only view or manage invitations that you yourself sent." }}
+ {{/unless}}
{{t "Invite more users" }}
{{t "Invites" }}
@@ -11,7 +14,9 @@
| {{t "Invitee" }} |
+ {{#if is_admin }}
{{t "Invited by" }} |
+ {{/if}}
{{t "Invited at" }} |
{{t "Invited as" }} |
{{t "Actions" }} |
diff --git a/templates/zerver/app/settings_overlay.html b/templates/zerver/app/settings_overlay.html
index 3d3bd03e28..e4c3263155 100644
--- a/templates/zerver/app/settings_overlay.html
+++ b/templates/zerver/app/settings_overlay.html
@@ -140,11 +140,13 @@
{{ _('Custom profile fields') }}
{% endif %}
- {% if is_admin %}
+ {% if not guest %}
{{ _('Invitations') }}
+ {% endif %}
+ {% if is_admin %}
{{ _('Data exports') }}
diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py
index d31a8b984c..4d9dd2b3fe 100644
--- a/zerver/lib/actions.py
+++ b/zerver/lib/actions.py
@@ -5099,11 +5099,14 @@ def do_invite_users(user_profile: UserProfile,
def do_get_user_invites(user_profile: UserProfile) -> List[Dict[str, Any]]:
days_to_activate = settings.INVITATION_LINK_VALIDITY_DAYS
active_value = getattr(confirmation_settings, 'STATUS_ACTIVE', 1)
-
lowest_datetime = timezone_now() - datetime.timedelta(days=days_to_activate)
- prereg_users = PreregistrationUser.objects.exclude(status=active_value).filter(
- invited_at__gte=lowest_datetime,
- referred_by__realm=user_profile.realm)
+ base_query = PreregistrationUser.objects.exclude(status=active_value).filter(
+ invited_at__gte=lowest_datetime)
+
+ 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 = []
diff --git a/zerver/tests/test_signup.py b/zerver/tests/test_signup.py
index 6315af6ac9..98dcbab2b7 100644
--- a/zerver/tests/test_signup.py
+++ b/zerver/tests/test_signup.py
@@ -46,6 +46,7 @@ from zerver.lib.send_email import send_future_email, FromAddress, \
deliver_email
from zerver.lib.initial_password import initial_password
from zerver.lib.actions import (
+ do_get_user_invites,
do_deactivate_realm,
do_deactivate_user,
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)
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:
"""
A GET call to /json/invites returns all unexpired invitations.
@@ -1537,6 +1558,46 @@ class InvitationsTestCase(InviteUserBase):
lambda: ScheduledEmail.objects.get(address__iexact=invitee,
type=ScheduledEmail.INVITATION_REMINDER))
+ def test_successful_member_delete_invitation(self) -> None:
+ """
+ A DELETE call from member account to /json/invites/ 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:
"""
A DELETE call to /json/invites/multiuse should delete the
@@ -1599,6 +1660,56 @@ class InvitationsTestCase(InviteUserBase):
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//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:
inviter = UserProfile.objects.exclude(realm=get_realm('zulip')).first()
prereg_user = PreregistrationUser.objects.create(
diff --git a/zerver/views/invite.py b/zerver/views/invite.py
index b062ba1fa9..25e4c5db8d 100644
--- a/zerver/views/invite.py
+++ b/zerver/views/invite.py
@@ -59,12 +59,12 @@ def get_invitee_emails_set(invitee_emails_raw: str) -> Set[str]:
invitee_emails.add(email.strip())
return invitee_emails
-@require_realm_admin
+@require_member_or_admin
def get_user_invites(request: HttpRequest, user_profile: UserProfile) -> HttpResponse:
all_users = do_get_user_invites(user_profile)
return json_success({'invites': all_users})
-@require_realm_admin
+@require_member_or_admin
@has_request_variables
def revoke_user_invite(request: HttpRequest, user_profile: UserProfile,
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:
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)
return json_success()
@@ -95,7 +98,7 @@ def revoke_multiuse_invite(request: HttpRequest, user_profile: UserProfile,
do_revoke_multi_use_invite(invite)
return json_success()
-@require_realm_admin
+@require_member_or_admin
@has_request_variables
def resend_user_invite_email(request: HttpRequest, user_profile: UserProfile,
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:
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)
return json_success({'timestamp': timestamp})