From 274a9fc6a8a1c1f04ed1c732714b1f90843cc328 Mon Sep 17 00:00:00 2001 From: "Courtney (CJ) Oka" Date: Fri, 5 May 2017 12:38:56 -0700 Subject: [PATCH] Asking the user for feedback using custom heuristic (#3139) * ask user for feedback * change toolbar for 20 seconds * feedback in toolbar * remove dead code * flake8 * flake8 * small changes * wording * change to cli config * feedback as command * utc * remove from shell config * flake8 --- .../azure-cli-shell/azclishell/__main__.py | 10 +++- .../azure-cli-shell/azclishell/app.py | 27 ++++++++-- .../azclishell/configuration.py | 19 ++++++- .../azclishell/frequency_heuristic.py | 53 +++++++++++++++++++ 4 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 src/command_modules/azure-cli-shell/azclishell/frequency_heuristic.py diff --git a/src/command_modules/azure-cli-shell/azclishell/__main__.py b/src/command_modules/azure-cli-shell/azclishell/__main__.py index 8ed8ffd5e8c..f0eded067da 100644 --- a/src/command_modules/azure-cli-shell/azclishell/__main__.py +++ b/src/command_modules/azure-cli-shell/azclishell/__main__.py @@ -16,6 +16,7 @@ from azclishell.az_completer import AzCompleter from azclishell.az_lexer import AzLexer from azclishell.color_styles import style_factory +from azclishell.frequency_heuristic import frequent_user from azure.cli.core.application import APPLICATION from azure.cli.core._session import ACCOUNT, CONFIG, SESSION @@ -52,12 +53,19 @@ def main(style=None): print("When in doubt, ask for 'help'") config.firsttime() + ask_feedback = False + if not config.has_feedback() and frequent_user: + print("\n\nAny comments or concerns? You can use the \'feedback\' command!" + + " We would greatly appreciate it.\n") + ask_feedback = True + shell_app = Shell( completer=AZCOMPLETER, lexer=AzLexer, history=FileHistory( os.path.join(shell_config_dir(), config.get_history())), app=APPLICATION, - styles=style_obj + styles=style_obj, + user_feedback=ask_feedback ) shell_app.run() diff --git a/src/command_modules/azure-cli-shell/azclishell/app.py b/src/command_modules/azure-cli-shell/azclishell/app.py index 756cc07f47f..09ae971d186 100644 --- a/src/command_modules/azure-cli-shell/azclishell/app.py +++ b/src/command_modules/azure-cli-shell/azclishell/app.py @@ -10,6 +10,7 @@ import os import subprocess import sys +import datetime import threading import jmespath @@ -27,6 +28,7 @@ import azclishell.configuration from azclishell.az_lexer import AzLexer, ExampleLexer, ToolbarLexer from azclishell.command_tree import in_tree +from azclishell.frequency_heuristic import DISPLAY_TIME from azclishell.gather_commands import add_random_new_lines from azclishell.key_bindings import registry, get_section, sub_section from azclishell.layout import create_layout, create_tutorial_layout, set_scope @@ -53,6 +55,7 @@ PROFILE = Profile() SELECT_SYMBOL = azclishell.configuration.SELECT_SYMBOL PART_SCREEN_EXAMPLE = .3 +START_TIME = datetime.datetime.utcnow() CLEAR_WORD = get_os_clear_screen_word() @@ -118,7 +121,8 @@ class Shell(object): # pylint: disable=too-many-arguments def __init__(self, completer=None, styles=None, lexer=None, history=InMemoryHistory(), - app=None, input_custom=sys.stdout, output_custom=None): + app=None, input_custom=sys.stdout, output_custom=None, + user_feedback=False): self.styles = styles if styles: self.lexer = lexer or AzLexer @@ -136,6 +140,7 @@ def __init__(self, completer=None, styles=None, self._env = os.environ self.last = None self.last_exit = 0 + self.user_feedback = user_feedback self.input = input_custom self.output = output_custom self.config_default = "" @@ -169,6 +174,7 @@ def on_input_timeout(self, cli): self.example_docs = u'{}'.format(example) self._update_default_info() + cli.buffers['description'].reset( initial_document=Document(self.description_docs, cursor_position=0)) cli.buffers['parameter'].reset( @@ -190,10 +196,17 @@ def _update_toolbar(self): for _ in range(cols): empty_space += " " - settings = self._toolbar_info() - settings, empty_space = space_toolbar(settings, cols, empty_space) + delta = datetime.datetime.utcnow() - START_TIME + if self.user_feedback and delta.seconds < DISPLAY_TIME: + toolbar = [ + ' Try out the \'feedback\' command', + 'If refreshed disappear in: {}'.format(str(DISPLAY_TIME - delta.seconds))] + else: + toolbar = self._toolbar_info() + + toolbar, empty_space = space_toolbar(toolbar, cols, empty_space) cli.buffers['bottom_toolbar'].reset( - initial_document=Document(u'{}{}{}'.format(NOTIFICATIONS, settings, empty_space))) + initial_document=Document(u'{}{}{}'.format(NOTIFICATIONS, toolbar, empty_space))) def _toolbar_info(self): sub_name = "" @@ -211,7 +224,6 @@ def _toolbar_info(self): "[F3]Keys", "[Ctrl+D]Quit", tool_val - # tool_val2 ] return settings_items @@ -534,10 +546,15 @@ def handle_scoping_input(self, continue_flag, cmd, text): def cli_execute(self, cmd): """ sends the command to the CLI to be executed """ + try: args = parse_quotes(cmd) azlogging.configure_logging(args) + if len(args) > 0 and args[0] == 'feedback': + SHELL_CONFIGURATION.set_feedback('yes') + self.user_feedback = False + azure_folder = get_config_dir() if not os.path.exists(azure_folder): os.makedirs(azure_folder) diff --git a/src/command_modules/azure-cli-shell/azclishell/configuration.py b/src/command_modules/azure-cli-shell/azclishell/configuration.py index d4253be53f0..9312e989cf3 100644 --- a/src/command_modules/azure-cli-shell/azclishell/configuration.py +++ b/src/command_modules/azure-cli-shell/azclishell/configuration.py @@ -25,7 +25,7 @@ SELECT_SYMBOL['query'] + "[path]": "query previous command using jmespath syntax", "[cmd] " + SELECT_SYMBOL['example'] + " [num]": "do a step by step tutorial of example", SELECT_SYMBOL['exit_code']: "get the exit code of the previous command", - SELECT_SYMBOL['scope'] + '[cmd]': "set a scope", + SELECT_SYMBOL['scope'] + '[cmd]': "set a scope, and scopes can be chained with spaces", SELECT_SYMBOL['scope'] + ' ' + SELECT_SYMBOL['unscope']: "go back a scope", "Ctrl+N": "Scroll down the documentation", "Ctrl+Y": "Scroll up the documentation" @@ -37,6 +37,7 @@ def help_text(values): + """ reformats the help text """ result = "" for key in values: result += key + ' '.join('' for x in range(GESTURE_LENGTH - len(key))) +\ @@ -63,6 +64,7 @@ def __init__(self): self.config.add_section('Layout') self.config.set('Help Files', 'command', 'help_dump.json') self.config.set('Help Files', 'history', 'history.txt') + self.config.set('Help Files', 'frequency', 'frequency.json') self.config.set('Layout', 'command_description', 'yes') self.config.set('Layout', 'param_description', 'yes') self.config.set('Layout', 'examples', 'yes') @@ -86,6 +88,10 @@ def get_help_files(self): """ returns where the command table is cached """ return self.config.get('Help Files', 'command') + def get_frequency(self): + """ returns the name of the frequency file """ + return self.config.get('Help Files', 'frequency') + def load(self, path): """ loads the configuration settings """ self.config.read(path) @@ -104,6 +110,14 @@ def get_style(self): """ gets the last style they used """ return self.config.get('DEFAULT', 'style') + def has_feedback(self): + """ returns whether user has given feedback """ + return az_config.getboolean('core', 'given feedback') + + def set_feedback(self, value): + """ sets the feedback in the config """ + set_global_config_value('core', 'given feedback', value) + def set_style(self, val): """ sets the style they used """ self.set_val('DEFAULT', 'style', val) @@ -140,3 +154,6 @@ def ask_user_for_telemetry(): CONFIGURATION = Configuration() + +if not az_config.has_option('core', 'given feedback'): + set_global_config_value('core', 'given feedback', 'no') diff --git a/src/command_modules/azure-cli-shell/azclishell/frequency_heuristic.py b/src/command_modules/azure-cli-shell/azclishell/frequency_heuristic.py new file mode 100644 index 00000000000..4aaeb978785 --- /dev/null +++ b/src/command_modules/azure-cli-shell/azclishell/frequency_heuristic.py @@ -0,0 +1,53 @@ +# -------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for license information. +# -------------------------------------------------------------------------------------------- + +import os +import datetime +import json + +from azure.cli.core._config import set_global_config_value + +from azclishell.configuration import CONFIGURATION, get_config_dir as shell_config + +SHELL_CONFIG = CONFIGURATION +DAYS_AGO = 28 +ACTIVE_STATUS = 5 +DISPLAY_TIME = 20 + + +def update_frequency(): + """ updates the frequency from files """ + with open(os.path.join(shell_config(), SHELL_CONFIG.get_frequency()), 'r') as freq: + try: + frequency = json.load(freq) + except ValueError: + frequency = {} + + with open(os.path.join(shell_config(), SHELL_CONFIG.get_frequency()), 'w') as freq: + now = datetime.datetime.utcnow() + val = frequency.get(now) + frequency[now] = val + 1 if val else 1 + json.dump(frequency, freq) + + return frequency + + +def frequency_measurement(): + """ measures how many times a user has used this program in the last calendar week """ + freq = update_frequency() + count = 0 + base = datetime.datetime.utcnow() + date_list = [base - datetime.timedelta(days=x) for x in range(1, DAYS_AGO)] + for day in date_list: + count += 1 if freq.get(day, 0) > 0 else 0 + return count + + +def frequency_heuristic(): + """ decides whether user meets requirements for frequency """ + return frequency_measurement() >= ACTIVE_STATUS + + +frequent_user = frequency_heuristic()