diff --git a/corporate/lib/support.py b/corporate/lib/support.py new file mode 100644 index 0000000000..cbffbc389a --- /dev/null +++ b/corporate/lib/support.py @@ -0,0 +1,15 @@ +from urllib.parse import urlencode, urljoin, urlunsplit + +from django.conf import settings +from django.urls import reverse + +from zerver.models import Realm, get_realm + + +def get_support_url(realm: Realm) -> str: + support_realm_uri = get_realm(settings.STAFF_SUBDOMAIN).uri + support_url = urljoin( + support_realm_uri, + urlunsplit(("", "", reverse("support"), urlencode({"q": realm.string_id}), "")), + ) + return support_url diff --git a/corporate/tests/test_stripe.py b/corporate/tests/test_stripe.py index 16eb1cb703..dd56bed63b 100644 --- a/corporate/tests/test_stripe.py +++ b/corporate/tests/test_stripe.py @@ -1487,6 +1487,41 @@ class StripeTest(StripeTestCase): self.assert_json_success(response) + def test_support_request(self) -> None: + user = self.example_user("hamlet") + self.assertIsNone(get_customer_by_realm(user.realm)) + + self.login_user(user) + + result = self.client_get("/support/") + self.assertEqual(result.status_code, 200) + self.assert_in_success_response(["Contact support"], result) + + data = { + "request_subject": "Not getting messages.", + "request_message": "Running into this weird issue.", + } + result = self.client_post("/support/", data) + self.assert_in_success_response(["Thanks for getting in touch with us!"], result) + + from django.core.mail import outbox + + self.assert_length(outbox, 1) + + for message in outbox: + self.assert_length(message.to, 1) + self.assertEqual(message.to[0], "desdemona+admin@zulip.com") + self.assertEqual(message.subject, "Support request for zulip") + self.assertEqual(message.reply_to, ["hamlet@zulip.com"]) + self.assertEqual(self.email_envelope_from(message), settings.NOREPLY_EMAIL_ADDRESS) + self.assertIn("Zulip Support None: user = self.example_user("hamlet") self.assertIsNone(get_customer_by_realm(user.realm)) diff --git a/corporate/urls.py b/corporate/urls.py index cd52ad888a..4fae5b72d1 100644 --- a/corporate/urls.py +++ b/corporate/urls.py @@ -5,6 +5,7 @@ from django.urls import path from django.views.generic import TemplateView from corporate.views.billing_page import billing_home, replace_payment_source, update_plan +from corporate.views.support import support_request from corporate.views.upgrade import initial_upgrade, sponsorship, upgrade from zerver.lib.rest import rest_path @@ -16,6 +17,7 @@ i18n_urlpatterns: Any = [ # Billing path("billing/", billing_home), path("upgrade/", initial_upgrade, name="initial_upgrade"), + path("support/", support_request), ] v1_api_and_json_patterns = [ diff --git a/corporate/views/support.py b/corporate/views/support.py new file mode 100644 index 0000000000..b168868117 --- /dev/null +++ b/corporate/views/support.py @@ -0,0 +1,58 @@ +from django import forms +from django.http import HttpRequest, HttpResponse +from django.shortcuts import render + +from corporate.lib.support import get_support_url +from zerver.decorator import zulip_login_required +from zerver.lib.request import has_request_variables +from zerver.lib.send_email import FromAddress, send_email + + +class SupportRequestForm(forms.Form): + # We use the same subject length requirement as GitHub's + # contact support form. + MAX_SUBJECT_LENGTH = 50 + request_subject = forms.CharField(max_length=MAX_SUBJECT_LENGTH) + request_message = forms.CharField(widget=forms.Textarea) + + +@zulip_login_required +@has_request_variables +def support_request(request: HttpRequest) -> HttpResponse: + user = request.user + assert user.is_authenticated + + context = { + "email": user.delivery_email, + "realm_name": user.realm.name, + "MAX_SUBJECT_LENGTH": SupportRequestForm.MAX_SUBJECT_LENGTH, + } + + if request.POST: + post_data = request.POST.copy() + form = SupportRequestForm(post_data) + + if form.is_valid(): + email_context = { + "requested_by": user.full_name, + "realm_string_id": user.realm.string_id, + "request_subject": form.cleaned_data["request_subject"], + "request_message": form.cleaned_data["request_message"], + "support_url": get_support_url(user.realm), + "user_role": user.get_role_name(), + } + + send_email( + "zerver/emails/support_request", + to_emails=[FromAddress.SUPPORT], + from_name="Zulip Support", + from_address=FromAddress.tokenized_no_reply_address(), + reply_to_email=user.delivery_email, + context=email_context, + ) + + response = render(request, "corporate/support_request_thanks.html", context=context) + return response + + response = render(request, "corporate/support_request.html", context=context) + return response diff --git a/corporate/views/upgrade.py b/corporate/views/upgrade.py index 33d53105a3..df603304ad 100644 --- a/corporate/views/upgrade.py +++ b/corporate/views/upgrade.py @@ -1,7 +1,6 @@ import logging from decimal import Decimal from typing import Any, Dict, Optional -from urllib.parse import urlencode, urljoin, urlunsplit from django import forms from django.conf import settings @@ -24,6 +23,7 @@ from corporate.lib.stripe import ( update_sponsorship_status, validate_licenses, ) +from corporate.lib.support import get_support_url from corporate.models import ( CustomerPlan, ZulipSponsorshipRequest, @@ -37,7 +37,7 @@ from zerver.lib.request import REQ, has_request_variables from zerver.lib.response import json_success from zerver.lib.send_email import FromAddress, send_email from zerver.lib.validator import check_bool, check_int, check_string_in -from zerver.models import Realm, UserProfile, get_org_type_display_name, get_realm +from zerver.models import Realm, UserProfile, get_org_type_display_name billing_logger = logging.getLogger("corporate.stripe") @@ -221,12 +221,7 @@ def sponsorship( requested_by = user.full_name user_role = user.get_role_name() - - support_realm_uri = get_realm(settings.STAFF_SUBDOMAIN).uri - support_url = urljoin( - support_realm_uri, - urlunsplit(("", "", reverse("support"), urlencode({"q": realm.string_id}), "")), - ) + support_url = get_support_url(realm) post_data = request.POST.copy() # We need to do this because the field name in the template diff --git a/static/styles/portico/portico.css b/static/styles/portico/portico.css index 65b33d3e6f..0d4b35b924 100644 --- a/static/styles/portico/portico.css +++ b/static/styles/portico/portico.css @@ -286,6 +286,10 @@ html { text-align: center; } +.thanks-page div { + text-align: center; +} + .lead { font-weight: bold; } diff --git a/static/styles/portico/portico_signin.css b/static/styles/portico/portico_signin.css index 5abcc2f284..c063c11329 100644 --- a/static/styles/portico/portico_signin.css +++ b/static/styles/portico/portico_signin.css @@ -417,6 +417,7 @@ html { input[type="text"], input[type="email"], input[type="password"], + textarea, select { padding: 10px 32px 10px 12px; margin: 25px 0 5px; @@ -815,6 +816,16 @@ button#register_auth_button_gitlab { margin-left: 2px; } } + + &.support-form-field { + width: 450px; + + input, + textarea { + width: 400px; + font-size: 1rem; + } + } } & [for="realm_in_root_domain"] { @@ -831,6 +842,12 @@ button#register_auth_button_gitlab { border-radius: 4px; } + .support-submit-button { + margin-top: 20px; + margin-bottom: 5px; + width: 450px; + } + .register-button .loader { display: none; vertical-align: top; @@ -868,7 +885,9 @@ button#register_auth_button_gitlab { } } - #id_email { + #id_email, + #support_from, + #support_realm { font-weight: normal; margin: 2px; padding-top: 25px; diff --git a/templates/corporate/support_request.html b/templates/corporate/support_request.html new file mode 100644 index 0000000000..3795fa3ac0 --- /dev/null +++ b/templates/corporate/support_request.html @@ -0,0 +1,43 @@ +{% extends "zerver/portico_signup.html" %} + +{% block portico_content %} +
+
+
+

{{ _('Contact support') }}

+
+ +
+ {{ csrf_input }} + +
+
+ +
{{ email }}
+
+
+ +
{{ realm_name }}
+
+
+ + +
+
+ + +
+ +
+ +
+
+
+
+
+ +{% endblock %} + diff --git a/templates/corporate/support_request_thanks.html b/templates/corporate/support_request_thanks.html new file mode 100644 index 0000000000..a3b4f61380 --- /dev/null +++ b/templates/corporate/support_request_thanks.html @@ -0,0 +1,15 @@ +{% extends "zerver/portico.html" %} + +{% block portico_content %} +
+
+

Thanks for contacting us!

+

We will be in touch with you soon.

+

+ You can find answers to frequently asked questions in the + Zulip help center. +

+
+
+ +{% endblock %} diff --git a/templates/zerver/emails/support_request.html b/templates/zerver/emails/support_request.html new file mode 100644 index 0000000000..66ee14de5e --- /dev/null +++ b/templates/zerver/emails/support_request.html @@ -0,0 +1,21 @@ +{% extends "zerver/emails/email_base_messages.html" %} + +{% block content %} +Support URL: {{ support_url }} + +

+ +Subject: {{ request_subject }} + +

+ +Message: +
+{{ request_message }} + +

+ +Requested by: {{ requested_by }} ({{ user_role }}) + +{% endblock %} + diff --git a/templates/zerver/emails/support_request.subject.txt b/templates/zerver/emails/support_request.subject.txt new file mode 100644 index 0000000000..beee73540e --- /dev/null +++ b/templates/zerver/emails/support_request.subject.txt @@ -0,0 +1 @@ +Support request for {{ realm_string_id }} diff --git a/templates/zerver/emails/support_request.txt b/templates/zerver/emails/support_request.txt new file mode 100644 index 0000000000..4e90e165ac --- /dev/null +++ b/templates/zerver/emails/support_request.txt @@ -0,0 +1,8 @@ +Support URL: {{ support_url }} + +Subject: {{ request_subject }} + +Message: +{{ request_message }} + +Requested by: {{ requested_by }} ({{ user_role }}) diff --git a/tools/linter_lib/custom_check.py b/tools/linter_lib/custom_check.py index 383e5a4640..df1cebff99 100644 --- a/tools/linter_lib/custom_check.py +++ b/tools/linter_lib/custom_check.py @@ -547,6 +547,9 @@ html_rules: List["Rule"] = [ "templates/zerver/email.html", "zerver/tests/fixtures/email", "templates/zerver/for-companies.html", + "templates/corporate/support_request.html", + "templates/corporate/support_request_thanks.html", + "templates/zerver/emails/support_request.html", }, "exclude_pattern": "email subject", "description": "avoid subject in templates",