зеркало из https://github.com/mozilla/kitsune.git
Merge pull request #5694 from smithellis/moz-loginless-form
Work to set up a loginless form for MA
This commit is contained in:
Коммит
20c6e1e0c3
|
@ -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);
|
||||
}
|
Загрузка…
Ссылка в новой задаче