Merge pull request #5694 from smithellis/moz-loginless-form

Work to set up a loginless form for MA
This commit is contained in:
smith 2023-10-25 09:42:04 -04:00 коммит произвёл GitHub
Родитель 2a7081eb26 e22051c5e7
Коммит 20c6e1e0c3
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
6 изменённых файлов: 155 добавлений и 58 удалений

Просмотреть файл

@ -1,17 +1,50 @@
from django import forms
from django.conf import settings
from django.utils.translation import gettext_lazy as _lazy
from kitsune.customercare.zendesk import CATEGORY_CHOICES, OS_CHOICES, ZendeskClient
from kitsune.customercare.zendesk import ZendeskClient
PRODUCTS_WITH_OS = ["firefox-private-network-vpn"]
# See docs/zendesk.md for details about getting the valid choice values for each field:
CATEGORY_CHOICES = [
(None, _lazy("Select a reason for contacting")),
("payment", _lazy("Payments & Billing")),
("accounts", _lazy("Accounts & Login")),
("technical", _lazy("Technical")),
("feedback", _lazy("Provide Feedback/Request Features")),
("not_listed", _lazy("Not listed")),
]
CATEGORY_CHOICES_LOGINLESS = [
(None, _lazy("Select a reason for contacting")),
("fxa-reset-password", _lazy("I forgot my password")),
("fxa-emailverify-lockout", _lazy("I can't recover my account using email")),
("fxa-remove3rdprtylogin", _lazy("I'm having issues signing in with my Google or Apple ID")),
("fxa-2fa-lockout", _lazy("My security code isn't working or is lost")),
("other-account-issue", _lazy("I have another sign in issue")),
]
OS_CHOICES = [
(None, _lazy("Select platform")),
("win10", _lazy("Windows")),
("mac", _lazy("Mac OS")),
("linux", _lazy("Linux")),
("android", _lazy("Android")),
("ios", _lazy("iOS")),
("other", _lazy("Other")),
]
class ZendeskForm(forms.Form):
"""Form for submitting a ticket to Zendesk."""
required_css_class = "required"
product = forms.CharField(disabled=True, widget=forms.HiddenInput)
email = forms.EmailField(label=_lazy("Contact Email"), required=True, widget=forms.HiddenInput)
category = forms.ChoiceField(
label=_lazy("What do you need help with?"), choices=CATEGORY_CHOICES
label=_lazy("What do you need help with?"), required=True, choices=CATEGORY_CHOICES
)
os = forms.ChoiceField(
label=_lazy("What operating system does your device use?"),
@ -19,12 +52,21 @@ class ZendeskForm(forms.Form):
required=False,
)
subject = forms.CharField(label=_lazy("Subject"), required=False)
description = forms.CharField(label=_lazy("Your message"), widget=forms.Textarea())
description = forms.CharField(
label=_lazy("Tell us more"), widget=forms.Textarea(), required=False
)
country = forms.CharField(widget=forms.HiddenInput)
def __init__(self, *args, product, **kwargs):
def __init__(self, *args, product, user=None, **kwargs):
kwargs.update({"initial": {"product": product.slug}})
super().__init__(*args, **kwargs)
if product.slug in settings.LOGIN_EXCEPTIONS and not user.is_authenticated:
self.fields["email"].widget = forms.EmailInput()
self.fields["category"].choices = CATEGORY_CHOICES_LOGINLESS
else:
self.fields["email"].initial = user.email
self.label_suffix = ""
if product.slug not in PRODUCTS_WITH_OS:
del self.fields["os"]

Просмотреть файл

@ -1,30 +1,9 @@
from django.conf import settings
from django.utils.translation import gettext_lazy as _lazy
from zenpy import Zenpy
from zenpy.lib.api_objects import Identity as ZendeskIdentity
from zenpy.lib.api_objects import Ticket
from zenpy.lib.api_objects import User as ZendeskUser
# See docs/zendesk.md for details about getting the valid choice values for each field:
CATEGORY_CHOICES = [
(None, _lazy("Select a topic")),
("payment", _lazy("Payments & Billing")),
("accounts", _lazy("Accounts & Login")),
("technical", _lazy("Technical")),
("feedback", _lazy("Provide Feedback/Request Features")),
("not_listed", _lazy("Not listed")),
]
OS_CHOICES = [
(None, _lazy("Select platform")),
("win10", _lazy("Windows")),
("mac", _lazy("Mac OS")),
("linux", _lazy("Linux")),
("android", _lazy("Android")),
("ios", _lazy("iOS")),
("other", _lazy("Other")),
]
class ZendeskClient(object):
"""Client to connect to Zendesk API."""
@ -38,27 +17,42 @@ class ZendeskClient(object):
}
self.client = Zenpy(**creds)
def _user_to_zendesk_user(self, user, include_email=True):
fxa_uid = user.profile.fxa_uid
id_str = user.profile.zendesk_id
def _user_to_zendesk_user(self, user, ticket_fields=None):
if not user.is_authenticated:
name = "Anonymous User"
locale = "en-US"
id = None
external_id = None
user_fields = None
else:
fxa_uid = user.profile.fxa_uid
id_str = user.profile.zendesk_id
id = int(id_str) if id_str else None
name = user.profile.display_name
locale = user.profile.locale
user_fields = {"user_id": fxa_uid}
external_id = fxa_uid
return ZendeskUser(
id=int(id_str) if id_str else None,
id=id,
verified=True,
email=user.email if include_email else "",
name=user.profile.display_name,
locale=user.profile.locale,
user_fields={"user_id": fxa_uid},
external_id=fxa_uid,
email=ticket_fields.get("email") or user.email,
name=name,
locale=locale,
user_fields=user_fields,
external_id=external_id,
)
def create_user(self, user):
def create_user(self, user, ticket_fields=None):
"""Given a Django user, create a user in Zendesk."""
zendesk_user = self._user_to_zendesk_user(user)
zendesk_user = self._user_to_zendesk_user(user, ticket_fields=ticket_fields)
# call create_or_update to avoid duplicating users FxA previously created
zendesk_user = self.client.users.create_or_update(zendesk_user)
user.profile.zendesk_id = str(zendesk_user.id)
user.profile.save(update_fields=["zendesk_id"])
# We can't save anything to AnonymousUser Profile
# as it has none
if user.is_authenticated:
user.profile.zendesk_id = str(zendesk_user.id)
user.profile.save(update_fields=["zendesk_id"])
return zendesk_user
@ -84,20 +78,41 @@ class ZendeskClient(object):
def create_ticket(self, user, ticket_fields):
"""Create a ticket in Zendesk."""
custom_fields = [
{"id": settings.ZENDESK_PRODUCT_FIELD_ID, "value": ticket_fields.get("product")},
{"id": settings.ZENDESK_OS_FIELD_ID, "value": ticket_fields.get("os")},
{"id": settings.ZENDESK_COUNTRY_FIELD_ID, "value": ticket_fields.get("country")},
]
# If this is the normal, athenticated form we want to use the category field
if user.is_authenticated:
custom_fields.append(
{"id": settings.ZENDESK_CATEGORY_FIELD_ID, "value": ticket_fields.get("category")},
)
# If this is the loginless form we want to use the contact label field (tag)
# and fix the category field to be "accounts"
else:
custom_fields.extend(
[
{
"id": settings.ZENDESK_CONTACT_LABEL_ID,
"value": ticket_fields.get("category"),
},
{"id": settings.ZENDESK_CATEGORY_FIELD_ID, "value": "accounts"},
]
)
ticket = Ticket(
subject=ticket_fields.get("subject"),
comment={"body": ticket_fields.get("description")},
ticket_form_id=settings.ZENDESK_TICKET_FORM_ID,
custom_fields=[
{"id": settings.ZENDESK_PRODUCT_FIELD_ID, "value": ticket_fields.get("product")},
{"id": settings.ZENDESK_CATEGORY_FIELD_ID, "value": ticket_fields.get("category")},
{"id": settings.ZENDESK_OS_FIELD_ID, "value": ticket_fields.get("os")},
{"id": settings.ZENDESK_COUNTRY_FIELD_ID, "value": ticket_fields.get("country")},
],
custom_fields=custom_fields,
)
if user.profile.zendesk_id:
# TODO: is this necessary if we're updating users as soon as they're updated locally?
ticket.requester_id = self.update_user(user).id
if user.is_authenticated:
if user.profile.zendesk_id:
# TODO: is this necessary if we're
# updating users as soon as they're updated locally?
ticket.requester_id = self.update_user(user).id
else:
ticket.requester_id = self.create_user(user).id
else:
ticket.requester_id = self.create_user(user).id
ticket.requester_id = self.create_user(user, ticket_fields=ticket_fields).id
return self.client.tickets.create(ticket)

Просмотреть файл

@ -90,12 +90,20 @@ we can compute the edit-title URL.
{% endif %}
<div class="info card shade-bg highlight mb">
{% if is_loginless %}
{% trans %}
Can't sign in to your account and need help?
You've found the right place. Complete the form below
to contact our support staff.
{% endtrans %}
{% else %}
{% trans %}
Be descriptive.
Saying “playing video on YouTube is always choppy”
will help us understand the issue better than saying
“something is wrong” or “the app is broken”.
{% endtrans %}
{% endif %}
</div>
{% for field in form.hidden_fields() %}
@ -115,7 +123,6 @@ we can compute the edit-title URL.
</p>
</li>
{% endif %}
{% if field.name == 'ff_version' or field.name == 'os' %}
{% set li_class='details' %}
{% endif %}
@ -123,11 +130,21 @@ we can compute the edit-title URL.
<li class="{{ li_class }} {% if field.errors %}has-error invalid{% endif %} cf">
{{ field.label_tag()|safe }}
{% if field.name == 'content' %}
{{ content_editor(field) }}
{% elif field.name == 'troubleshooting' %}
{{ troubleshooting_instructions(field) }}
{% elif field.name == 'description' and is_loginless %}
<label for="{{ field.id_for_label }}">
<span class="mzp-c-fieldnote">
{{ _(
"Include details such as your account email or specifics about"
" your sign-in issue to help us get you back into your account quicker."
) }}
</span>
</label>
{{ field }}
{{ field.errors }}
{% else %}
{{ field|safe }}
{% endif %}

Просмотреть файл

@ -6,6 +6,7 @@ from datetime import date, datetime, timedelta
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.views import redirect_to_login
from django.contrib.contenttypes.models import ContentType
from django.contrib.sites.models import Site
from django.core.exceptions import PermissionDenied
@ -462,7 +463,7 @@ def edit_details(request, question_id):
return redirect(reverse("questions.details", kwargs={"question_id": question_id}))
def aaq(request, product_key=None, category_key=None, step=1):
def aaq(request, product_key=None, category_key=None, step=1, is_loginless=False):
"""Ask a new question."""
template = "questions/new_question.html"
@ -509,6 +510,7 @@ def aaq(request, product_key=None, category_key=None, step=1):
"current_product": product_config,
"current_step": step,
"host": Site.objects.get_current().domain,
"is_loginless": is_loginless,
}
if step > 1:
@ -535,21 +537,24 @@ def aaq(request, product_key=None, category_key=None, step=1):
return HttpResponseRedirect(path)
if has_subscriptions:
zendesk_form = ZendeskForm(data=request.POST or None, product=product)
zendesk_form = ZendeskForm(
data=request.POST or None,
product=product,
user=request.user,
)
context["form"] = zendesk_form
if zendesk_form.is_valid():
try:
zendesk_form.send(request.user)
email = zendesk_form.cleaned_data["email"]
messages.add_message(
request,
messages.SUCCESS,
_(
"Done! Your message was sent to Mozilla Support, "
"thank you for reaching out. "
"We'll contact you via email as soon as possible."
),
"Done! Thank you for reaching out Mozilla Support."
" We've sent a confirmation email to {email}"
).format(email=email),
)
url = reverse("products.product", args=[product.slug])
@ -618,11 +623,19 @@ def aaq_step2(request, product_key):
return aaq(request, product_key=product_key, step=2)
@login_required
def aaq_step3(request, product_key, category_key=None):
"""Step 3: Show full question form."""
# Since removing the @login_required decorator for MA form
# need to catch unauthenticated, non-MA users here """
is_loginless = product_key in settings.LOGIN_EXCEPTIONS
if not is_loginless and not request.user.is_authenticated:
return redirect_to_login(next=request.path, login_url=reverse("users.login"))
return aaq(
request,
is_loginless=is_loginless,
product_key=product_key,
category_key=category_key,
step=3,

Просмотреть файл

@ -1131,9 +1131,13 @@ ZENDESK_USER_EMAIL = config("ZENDESK_USER_EMAIL", default="")
ZENDESK_TICKET_FORM_ID = config("ZENDESK_TICKET_FORM_ID", default="360000417171", cast=int)
ZENDESK_PRODUCT_FIELD_ID = config("ZENDESK_PRODUCT_FIELD_ID", default="360047198211", cast=int)
ZENDESK_CATEGORY_FIELD_ID = config("ZENDESK_CATEGORY_FIELD_ID", default="360047206172", cast=int)
ZENDESK_CONTACT_LABEL_ID = config("ZENDESK_CONTACT_LABEL_ID", default="1900002215047", cast=int)
ZENDESK_OS_FIELD_ID = config("ZENDESK_OS_FIELD_ID", default="360018604871", cast=int)
ZENDESK_COUNTRY_FIELD_ID = config("ZENDESK_COUNTRY_FIELD_ID", default="360026463511", cast=int)
# Products that allow un-authenticated users to submit support requests
LOGIN_EXCEPTIONS = ["mozilla-account"]
# Django CSP configuration
CSP_INCLUDE_NONCE_IN = ["script-src"]

Просмотреть файл

@ -407,3 +407,9 @@ textarea {
margin-top: 52px;
border-bottom: 1px solid var(--color-heading);
}
label.required::after {
content: "\A Required";
white-space: pre;
color: var(--color-marketing-red-01);
}