From bbf5a5efed052e44353de420a004cfbfc7572bfc Mon Sep 17 00:00:00 2001 From: jagansivam28 Date: Fri, 1 May 2020 01:11:21 +0530 Subject: [PATCH] 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. --- static/js/settings_invites.js | 1 + static/templates/admin_invites_list.hbs | 2 + .../templates/settings/invites_list_admin.hbs | 5 + templates/zerver/app/settings_overlay.html | 4 +- zerver/lib/actions.py | 11 +- zerver/tests/test_signup.py | 111 ++++++++++++++++++ zerver/views/invite.py | 12 +- 7 files changed, 138 insertions(+), 8 deletions(-) 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 @@ + {{#if is_admin }} + {{/if}} 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})
  • {{t "Invitee" }}{{t "Invited by" }}{{t "Invited at" }} {{t "Invited as" }} {{t "Actions" }}