Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

invites: Add option to receive direct notification on accepted invitations. #28819

Merged
merged 1 commit into from
Jul 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions api_docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ format used by the Zulip server that they are interacting with.

## Changes in Zulip 9.0

**Feature level 267**

* [`GET /invites`](/api/get-invites),[`POST /invites`](/api/send-invites): Added
`notify_referrer_on_join` parameter, indicating whether the referrer has opted
to receive a direct message from the notification bot whenever a user joins
via this invitation.

**Feature level 266**

* `PATCH /realm`, [`POST /register`](/api/register-queue),
Expand Down
2 changes: 1 addition & 1 deletion version.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
# Changes should be accompanied by documentation explaining what the
# new level means in api_docs/changelog.md, as well as "**Changes**"
# entries in the endpoint's documentation in `zulip.yaml`.
API_FEATURE_LEVEL = 266
API_FEATURE_LEVEL = 267

# Bump the minor PROVISION_VERSION to indicate that folks should provision
# only when going from an old version of the code to a newer version. Bump
Expand Down
5 changes: 5 additions & 0 deletions web/src/invite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ function reset_error_messages(): void {
function get_common_invitation_data(): {
csrfmiddlewaretoken: string;
invite_as: number;
notify_referrer_on_join: boolean;
stream_ids: string;
invite_expires_in_minutes: string;
invitee_emails: string;
Expand All @@ -56,6 +57,7 @@ function get_common_invitation_data(): {
$<HTMLSelectOneElement>("select:not([multiple])#invite_as").val()!,
10,
);
const notify_referrer_on_join = $("#receive-invite-acceptance-notification").is(":checked");
const raw_expires_in = $<HTMLSelectOneElement>("select:not([multiple])#expires_in").val()!;
// See settings_config.expires_in_values for why we do this conversion.
let expires_in: number | null;
Expand All @@ -82,6 +84,7 @@ function get_common_invitation_data(): {
const data = {
csrfmiddlewaretoken: csrf_token,
invite_as,
notify_referrer_on_join,
stream_ids: JSON.stringify(stream_ids),
invite_expires_in_minutes: JSON.stringify(expires_in),
invitee_emails: pills
Expand Down Expand Up @@ -459,9 +462,11 @@ function open_invite_user_modal(e: JQuery.ClickEvent<Document, undefined>): void
switch (key) {
case "invite-email-tab":
$("#invitee_emails_container").show();
$("#receive-invite-acceptance-notification-container").show();
break;
case "invite-link-tab":
$("#invitee_emails_container").hide();
$("#receive-invite-acceptance-notification-container").hide();
break;
}
toggle_invite_submit_button(key);
Expand Down
2 changes: 2 additions & 0 deletions web/src/settings_invites.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const invite_schema = z.intersection(
expiry_date: z.number().nullable(),
id: z.number(),
invited_as: z.number(),
notify_referrer_on_join: z.boolean(),
}),
z.discriminatedUnion("is_multiuse", [
z.object({
Expand All @@ -46,6 +47,7 @@ type Invite = z.output<typeof invite_schema> & {
disable_buttons?: boolean;
referrer_name?: string;
img_src?: string;
notify_referrer_on_join?: boolean;
};

const meta = {
Expand Down
7 changes: 7 additions & 0 deletions web/templates/invite_user_modal.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@
</div>
</div>
</div>
<div class="input-group new-style" id="receive-invite-acceptance-notification-container">
<label class="checkbox display-block">
<input type="checkbox" id="receive-invite-acceptance-notification" checked/>
<span></span>
{{t "Send me a direct message when my invitation is accepted" }}
</label>
</div>
<div class="input-group">
<label for="expires_in" class="modal-field-label">{{t "Invitation expires after" }}</label>
<select id="expires_in" class="invite-user-select modal_select bootstrap-focus-style">
Expand Down
1 change: 1 addition & 0 deletions zerver/actions/create_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ def process_new_human_user(
and prereg_user is not None
and prereg_user.referred_by is not None
and prereg_user.referred_by.is_active
and prereg_user.notify_referrer_on_join
):
# This is a cross-realm direct message.
with override_language(prereg_user.referred_by.default_language):
Expand Down
3 changes: 3 additions & 0 deletions zerver/actions/invites.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ def do_invite_users(
user_profile: UserProfile,
invitee_emails: Collection[str],
streams: Collection[Stream],
notify_referrer_on_join: bool = True,
*,
invite_expires_in_minutes: Optional[int],
include_realm_default_subscriptions: bool,
Expand Down Expand Up @@ -264,6 +265,7 @@ def do_invite_users(
invited_as=invite_as,
realm=realm,
include_realm_default_subscriptions=include_realm_default_subscriptions,
notify_referrer_on_join=notify_referrer_on_join,
)
prereg_user.save()
stream_ids = [stream.id for stream in streams]
Expand Down Expand Up @@ -318,6 +320,7 @@ def do_get_invites_controlled_by_user(user_profile: UserProfile) -> List[Dict[st
id=invitee.id,
invited_as=invitee.invited_as,
is_multiuse=False,
notify_referrer_on_join=invitee.notify_referrer_on_join,
)
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Generated by Django 5.0.6 on 2024-06-28 17:34

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("zerver", "0541_alter_realmauditlog_options"),
]

operations = [
migrations.AddField(
model_name="preregistrationuser",
name="notify_referrer_on_join",
field=models.BooleanField(default=True),
),
]
1 change: 1 addition & 0 deletions zerver/models/prereg_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class PreregistrationUser(models.Model):
full_name = models.CharField(max_length=UserProfile.MAX_NAME_LENGTH, null=True)
full_name_validated = models.BooleanField(default=False)
referred_by = models.ForeignKey(UserProfile, null=True, on_delete=CASCADE)
notify_referrer_on_join = models.BooleanField(default=True)
streams = models.ManyToManyField("zerver.Stream")
invited_at = models.DateTimeField(auto_now=True)
realm_creation = models.BooleanField(default=False)
Expand Down
23 changes: 23 additions & 0 deletions zerver/openapi/zulip.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12425,6 +12425,7 @@ paths:
invited: 1710606654,
invited_as: 200,
invited_by_user_id: 9,
notify_referrer_on_join: true,
is_multiuse: false,
},
{
Expand All @@ -12434,6 +12435,7 @@ paths:
invited_as: 400,
invited_by_user_id: 9,
is_multiuse: true,
notify_referrer_on_join: true,
link_url: "https://example.zulipchat.com/join/yddhtzk4jgl7rsmazc5fyyyy/",
},
],
Expand Down Expand Up @@ -12511,6 +12513,18 @@ paths:
type: boolean
default: false
example: false
notify_referrer_on_join:
description: |
A boolean indicating whether the referrer would like to receive a
direct message from [notification
bot](/help/configure-automated-notices) when a user account is created
using this invitation.

**Changes**: New in Zulip 9.0 (feature level 267). Previously,
referrers always received such direct messages.
type: boolean
example: false
default: true
required:
- invitee_emails
- stream_ids
Expand Down Expand Up @@ -20688,6 +20702,15 @@ components:
description: |
The email address the invitation was sent to. This will not be present when
`is_multiuse` is `true` (i.e. the invitation is a reusable invitation link).
notify_referrer_on_join:
type: boolean
description: |
A boolean indicating whether the referrer has opted to receive a direct
message from [notification bot](/help/configure-automated-notices) when a user
account is created using this invitation.

**Changes**: New in Zulip 9.0 (feature level 267). Previously,
referrers always received such direct messages.
link_url:
type: string
description: |
Expand Down
49 changes: 48 additions & 1 deletion zerver/tests/test_invite.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ def invite(
self,
invitee_emails: str,
stream_names: Sequence[str],
notify_referrer_on_join: bool = True,
invite_expires_in_minutes: Optional[int] = INVITATION_LINK_VALIDITY_MINUTES,
body: str = "",
invite_as: int = PreregistrationUser.INVITE_AS["MEMBER"],
Expand Down Expand Up @@ -206,6 +207,7 @@ def invite(
"include_realm_default_subscriptions": orjson.dumps(
include_realm_default_subscriptions
).decode(),
"notify_referrer_on_join": orjson.dumps(notify_referrer_on_join).decode(),
},
subdomain=realm.string_id if realm else "zulip",
)
Expand Down Expand Up @@ -1046,6 +1048,51 @@ def test_multi_user_invite(self) -> None:
]
)

def test_direct_notification_for_accepted_invitation(self) -> None:
"""
New test to check if notification-bot message is sent to the referrer
when notify_referrer_on_join is false.
"""

self.login("hamlet")
user_profile = self.example_user("hamlet")
invitee = self.nonreg_email("alice")
realm = get_realm("zulip")
self.assert_json_success(self.invite(invitee, ["Denmark"], False))
self.assertTrue(find_key_by_email(invitee))
self.submit_reg_form_for_user(invitee, "password")

assert user_profile.recipient_id is not None
invite_acceptance_notification_message = Message.objects.filter(
recipient_id=user_profile.recipient_id, realm=realm
).last()

self.assertIsNone(
invite_acceptance_notification_message,
"Unexpected message found from notification-bot for accepted invitations.",
)

self.login("hamlet")
new_invitee = self.nonreg_email("bob")
self.assert_json_success(self.invite(new_invitee, ["Denmark"], True))
self.assertTrue(find_key_by_email(new_invitee))
self.submit_reg_form_for_user(new_invitee, "new_password")

new_invitee_profile = self.nonreg_user("bob")
new_invite_acceptance_notification_message = Message.objects.filter(
recipient_id=user_profile.recipient_id, realm=realm
).last()

assert new_invite_acceptance_notification_message is not None
self.assertEqual(
new_invite_acceptance_notification_message.sender.email, "notification-bot@zulip.com"
)
self.assertTrue(
new_invite_acceptance_notification_message.content.startswith(
f"@_**{new_invitee_profile.full_name}|{new_invitee_profile.id}** accepted your",
)
)

def test_max_invites_model(self) -> None:
realm = get_realm("zulip")
self.assertEqual(realm.max_invites, settings.INVITES_DEFAULT_REALM_DAILY_MAX)
Expand Down Expand Up @@ -2302,7 +2349,7 @@ def test_resend_never_expiring_invitation(self) -> None:
self.login("iago")
invitee = "resend@zulip.com"

self.assert_json_success(self.invite(invitee, ["Denmark"], None))
self.assert_json_success(self.invite(invitee, ["Denmark"], True, None))
prereg_user = PreregistrationUser.objects.get(email=invitee)

# Verify and then clear from the outbox the original invite email
Expand Down
4 changes: 4 additions & 0 deletions zerver/views/invite.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ def invite_users_backend(
),
default=PreregistrationUser.INVITE_AS["MEMBER"],
),
notify_referrer_on_join: bool = REQ(
"notify_referrer_on_join", json_validator=check_bool, default=True
),
stream_ids: List[int] = REQ(json_validator=check_list(check_int)),
include_realm_default_subscriptions: bool = REQ(json_validator=check_bool, default=False),
) -> HttpResponse:
Expand Down Expand Up @@ -99,6 +102,7 @@ def invite_users_backend(
user_profile,
invitee_emails,
streams,
notify_referrer_on_join,
invite_expires_in_minutes=invite_expires_in_minutes,
include_realm_default_subscriptions=include_realm_default_subscriptions,
invite_as=invite_as,
Expand Down
Loading