-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Implement input box for email address confirmation * Fix typo in changes Update dialog to match new behavior Support Text entry when invalid email address is transcribed * Fix gui event registration * Make GUI title translatable Update email address formatting for TTS with unit test * Update test to handle dialog changes * Troubleshooting test failure * Update tests to handle email formatting * Fix gui input mocking * More test failure troubleshooting * Update to use methods added to ovos_utils * Update skill.json --------- Co-authored-by: NeonDaniel <[email protected]>
- Loading branch information
1 parent
718c456
commit 56edba1
Showing
11 changed files
with
176 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,6 +39,7 @@ | |
from neon_utils.skills.neon_skill import NeonSkill | ||
from neon_utils.user_utils import get_user_prefs | ||
from neon_utils.language_utils import get_supported_languages | ||
from neon_utils.parse_utils import validate_email | ||
from lingua_franca.parse import extract_langcode, get_full_lang_code | ||
from lingua_franca.format import pronounce_lang | ||
from lingua_franca.internal import UnsupportedLanguageError | ||
|
@@ -444,7 +445,8 @@ def handle_say_my_email(self, message: Message): | |
# TODO: Use get_response to ask for the user's email | ||
self.speak_dialog("email_not_known", private=True) | ||
else: | ||
self.speak_dialog("email_is", {"email": email_address}, | ||
self.speak_dialog("email_is", | ||
{"email": self._spoken_email(email_address)}, | ||
private=True) | ||
|
||
@intent_handler(IntentBuilder("SayMyLocation").require("tell_me_my") | ||
|
@@ -556,32 +558,85 @@ def handle_set_my_email(self, message: Message): | |
email_addr = "".join(email_words) | ||
LOG.info(email_addr) | ||
|
||
if '@' not in email_addr or '.' not in email_addr.split('@')[1]: | ||
if not validate_email(email_addr): | ||
self.speak_dialog("email_set_error", private=True) | ||
return | ||
email_addr = self.get_gui_input(self.translate("word_email_title"), | ||
"[email protected]") | ||
if not email_addr or not validate_email(email_addr): | ||
LOG.warning(f"Invalid email_addr entered: {email_addr}") | ||
return | ||
|
||
current_email = get_user_prefs(message)["user"]["email"] | ||
if current_email and email_addr == current_email: | ||
self.speak_dialog("email_already_set_same", | ||
{"email": current_email}, private=True) | ||
{"email": self._spoken_email(current_email)}, | ||
private=True) | ||
return | ||
if current_email: | ||
if self.ask_yesno("email_overwrite", {"old": current_email, | ||
"new": email_addr}) == "yes": | ||
if self.ask_yesno("email_overwrite", | ||
{"old": self._spoken_email(current_email), | ||
"new": self._spoken_email(email_addr)}) == "yes": | ||
self.update_profile({"user": {"email": email_addr}}) | ||
self.speak_dialog("email_set", {"email": email_addr}, | ||
self.speak_dialog("email_set", | ||
{"email": self._spoken_email(email_addr)}, | ||
private=True) | ||
else: | ||
self.speak_dialog("email_not_changed", | ||
{"email": current_email}, private=True) | ||
{"email": self._spoken_email(current_email)}, | ||
private=True) | ||
return | ||
if self.ask_yesno("email_confirmation", | ||
{"email": email_addr}) == "yes": | ||
{"email": self._spoken_email(email_addr)}) == "yes": | ||
self.update_profile({"user": {"email": email_addr}}) | ||
self.speak_dialog("email_set", {"email": email_addr}, | ||
self.speak_dialog("email_set", | ||
{"email": self._spoken_email(email_addr)}, | ||
private=True) | ||
else: | ||
self.speak_dialog("email_not_confirmed", private=True) | ||
email_addr = self.get_gui_input(self.translate("word_email_title"), | ||
"[email protected]") | ||
if email_addr: | ||
self.update_profile({"user": {"email": email_addr}}) | ||
self.speak_dialog("email_set", | ||
{"email": self._spoken_email(email_addr)}, | ||
private=True) | ||
else: | ||
LOG.info("User Cancelled email input") | ||
# TODO: Speak confirmation | ||
|
||
def get_gui_input(self, title=None, placeholder=None, | ||
confirm_text=None, exit_text=None) -> Optional[str]: | ||
gui_response = None | ||
response_event = Event() | ||
response_event.clear() | ||
self.gui.show_input_box(title, placeholder, confirm_text, exit_text, | ||
True, True) | ||
|
||
def _on_response(message): | ||
nonlocal gui_response | ||
gui_response = message.data.get("text") | ||
response_event.set() | ||
|
||
def _on_close(message): | ||
response_event.set() | ||
|
||
resp_message = self.gui.build_message_type('input.box.response') | ||
close_message = self.gui.build_message_type('input.box.close') | ||
self.add_event(resp_message, _on_response, once=True) | ||
self.add_event(close_message, _on_close, once=True) | ||
response_event.wait() | ||
self.gui.remove_input_box() | ||
self.remove_event(resp_message) | ||
self.remove_event(close_message) | ||
return gui_response | ||
|
||
# TODO: Update to import from ovos-utils | ||
def remove_input_box(self): | ||
LOG.info(f"GUI pages length {len(self.gui.pages)}") | ||
if len(self.gui.pages) > 1: | ||
self.gui.remove_page("SYSTEM_InputBox.qml") | ||
else: | ||
self.gui.release() | ||
|
||
@intent_handler(IntentBuilder("SetMyName").optionally("change") | ||
.require("my").require("name").require("rx_setting") | ||
|
@@ -962,6 +1017,13 @@ def _get_gender(self, request: str) -> Optional[str]: | |
LOG.info(f"no gender in request: {request}") | ||
return None | ||
|
||
def _spoken_email(self, email_addr: str): | ||
""" | ||
Get a pronouncable email address string | ||
""" | ||
return email_addr.replace('.', f' {self.translate("word_dot")} ')\ | ||
.replace('@', f' {self.translate("word_at")} ') | ||
|
||
@staticmethod | ||
def _get_name_parts(name: str, user_profile: dict) -> dict: | ||
""" | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
Okay, not changing anything. | ||
Please enter your email address on screen and press 'Confirm'. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
I did not hear a valid email address. Please, try again. | ||
I did not hear a valid email address. Please enter your email address on screen and press 'Confirm'. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
at |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
dot |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Email Address |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
neon-utils[network]~=1.3 | ||
ovos_utils~=0.0.28 | ||
ovos_utils~=0.0.28,>=0.0.31a1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -629,9 +629,8 @@ def test_handle_say_my_email(self): | |
test_message.context["user_profiles"][0]["user"]["email"] = \ | ||
"[email protected]" | ||
self.skill.handle_say_my_email(test_message) | ||
self.skill.speak_dialog.assert_called_with("email_is", | ||
{"email": "[email protected]"}, | ||
private=True) | ||
self.skill.speak_dialog.assert_called_with( | ||
"email_is", {"email": "test at neon dot ai"}, private=True) | ||
|
||
@mock.patch('neon_utils.net_utils.check_online') | ||
def test_handle_say_my_location(self, check_online): | ||
|
@@ -729,6 +728,8 @@ def test_handle_say_my_birthday(self): | |
self.skill.speak_dialog.assert_any_call("happy_birthday", private=True) | ||
|
||
def test_handle_set_my_email(self): | ||
real_get_gui_input = self.skill.get_gui_input | ||
self.skill.get_gui_input = Mock(return_value=None) | ||
real_ask_yesno = self.skill.ask_yesno | ||
self.skill.ask_yesno = Mock(return_value="no") | ||
test_profile = self.user_config | ||
|
@@ -740,7 +741,7 @@ def test_handle_set_my_email(self): | |
def _check_not_confirmed(msg): | ||
self.skill.handle_set_my_email(msg) | ||
self.skill.ask_yesno.assert_called_once_with( | ||
"email_confirmation", {"email": "test@neon.ai"}) | ||
"email_confirmation", {"email": "test at neon dot ai"}) | ||
self.skill.speak_dialog.assert_called_once_with( | ||
"email_not_confirmed", private=True) | ||
self.skill.ask_yesno.reset_mock() | ||
|
@@ -775,16 +776,16 @@ def _check_not_confirmed(msg): | |
self.skill.ask_yesno = Mock(return_value="yes") | ||
self.skill.handle_set_my_email(test_message) | ||
self.skill.ask_yesno.assert_called_with("email_confirmation", | ||
{"email": "test@neon.ai"}) | ||
{"email": "test at neon dot ai"}) | ||
self.skill.speak_dialog.assert_called_with("email_set", | ||
{"email": "test@neon.ai"}, | ||
{"email": "test at neon dot ai"}, | ||
private=True) | ||
self.assertEqual(test_message.context["user_profiles"][0] | ||
["user"]["email"], "[email protected]") | ||
# Set Email No Change | ||
self.skill.handle_set_my_email(test_message) | ||
self.skill.speak_dialog.assert_called_with("email_already_set_same", | ||
{"email": "test@neon.ai"}, | ||
{"email": "test at neon dot ai"}, | ||
private=True) | ||
self.assertEqual(test_message.context["user_profiles"][0] | ||
["user"]["email"], "[email protected]") | ||
|
@@ -794,24 +795,25 @@ def _check_not_confirmed(msg): | |
test_message.data["rx_setting"] = "demo at neon dot ai" | ||
self.skill.handle_set_my_email(test_message) | ||
self.skill.ask_yesno.assert_called_with("email_overwrite", | ||
{"old": "test@neon.ai", | ||
"new": "demo@neon.ai"}) | ||
{"old": "test at neon dot ai", | ||
"new": "demo at neon dot ai"}) | ||
self.skill.speak_dialog.assert_called_with("email_not_changed", | ||
{"email": "test@neon.ai"}, | ||
{"email": "test at neon dot ai"}, | ||
private=True) | ||
# Change Email Confirmed | ||
self.skill.ask_yesno = Mock(return_value="yes") | ||
self.skill.handle_set_my_email(test_message) | ||
self.skill.ask_yesno.assert_called_with("email_overwrite", | ||
{"old": "test@neon.ai", | ||
"new": "demo@neon.ai"}) | ||
{"old": "test at neon dot ai", | ||
"new": "demo at neon dot ai"}) | ||
self.skill.speak_dialog.assert_called_with("email_set", | ||
{"email": "demo@neon.ai"}, | ||
{"email": "demo at neon dot ai"}, | ||
private=True) | ||
self.assertEqual(test_message.context["user_profiles"][0] | ||
["user"]["email"], "[email protected]") | ||
|
||
self.skill.ask_yesno = real_ask_yesno | ||
self.skill.get_gui_input = real_get_gui_input | ||
|
||
def test_handle_set_my_name(self): | ||
test_profile = self.user_config | ||
|
@@ -1429,6 +1431,12 @@ def test_get_lang_name_and_code(self): | |
with self.assertRaises(UnsupportedLanguageError): | ||
self.skill._get_lang_code_and_name("nothing") | ||
|
||
def test_spoken_email(self): | ||
self.assertEqual(self.skill._spoken_email("[email protected]"), | ||
"test at neon dot ai") | ||
self.assertEqual(self.skill._spoken_email("[email protected]"), | ||
"my dot email at domain dot com") | ||
|
||
|
||
if __name__ == '__main__': | ||
unittest.main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import QtQuick.Layouts 1.12 | ||
import QtQuick 2.12 | ||
import QtQuick.Controls 2.12 | ||
import org.kde.kirigami 2.11 as Kirigami | ||
|
||
import Mycroft 1.0 as Mycroft | ||
|
||
Mycroft.Delegate { | ||
id: systemInputBoxFrame | ||
property var title: sessionData.title | ||
property var placeholderText: sessionData.placeholder | ||
property var confirmButtonText: sessionData.confirm_text | ||
property var exitButtonText: sessionData.exit_text | ||
property var skillIDHandler: sessionData.skill_id_handler | ||
property string responseEvent: "input.box.response" | ||
property string exitEvent: "input.box.close" | ||
|
||
ColumnLayout { | ||
width: parent.width | ||
spacing: Kirigami.Units.gridUnit | ||
|
||
Kirigami.Heading { | ||
level: 2 | ||
wrapMode: Text.WordWrap | ||
Layout.fillWidth: true | ||
horizontalAlignment: Text.AlignHCenter | ||
font.bold: true | ||
text: systemInputBoxFrame.title | ||
color: Kirigami.Theme.textColor | ||
} | ||
|
||
TextField { | ||
id: txtField | ||
Kirigami.Theme.colorSet: Kirigami.Theme.View | ||
Layout.fillWidth: true | ||
Layout.leftMargin: Mycroft.Units.gridUnit * 5 | ||
Layout.rightMargin: Mycroft.Units.gridUnit * 5 | ||
Layout.preferredHeight: Mycroft.Units.gridUnit * 4 | ||
placeholderText: systemInputBoxFrame.placeholderText | ||
|
||
onAccepted: { | ||
triggerGuiEvent(systemInputBoxFrame.responseEvent, {"text": txtField.text}) | ||
} | ||
} | ||
|
||
RowLayout { | ||
Layout.alignment: Qt.AlignCenter | ||
Button { | ||
Layout.fillWidth: true | ||
Layout.preferredHeight: Mycroft.Units.gridUnit * 5 | ||
text: systemInputBoxFrame.confirmButtonText | ||
onClicked: { | ||
console.log(systemInputBoxFrame.responseEvent) | ||
Mycroft.SoundEffects.playClickedSound(Qt.resolvedUrl("../snd/clicked.wav")) | ||
triggerGuiEvent(systemInputBoxFrame.responseEvent, {"text": txtField.text}) | ||
} | ||
} | ||
|
||
Button { | ||
Layout.fillWidth: true | ||
Layout.preferredHeight: Mycroft.Units.gridUnit * 5 | ||
text: systemInputBoxFrame.exitButtonText | ||
onClicked: { | ||
Mycroft.SoundEffects.playClickedSound(Qt.resolvedUrl("../snd/clicked.wav")) | ||
triggerGuiEvent(systemInputBoxFrame.exitEvent, {}) | ||
} | ||
} | ||
} | ||
Item { | ||
Layout.fillHeight: true | ||
} | ||
} | ||
} |