# -*- coding: utf-8 -*- import os import random import re import sys from select import select import conf from db import User, Session from helpers import search_user, search_product, safe_str, guess_data_type, argmax from text_interface import context_commands, local_help_commands, help_commands, \ exit_commands class ExitMenu(Exception): pass class Menu(object): def __init__(self, name, items=None, prompt='> ', return_index=True, exit_msg=None, exit_confirm_msg=None, exit_disallowed_msg=None, help_text=None, uses_db=False): self.name = name self.items = items if items is not None else [] self.prompt = prompt self.return_index = return_index self.exit_msg = exit_msg self.exit_confirm_msg = exit_confirm_msg self.exit_disallowed_msg = exit_disallowed_msg self.help_text = help_text self.context = None self.header_format = '[%s]' self.uses_db = uses_db self.session = None def exit_menu(self): if self.exit_disallowed_msg is not None: print self.exit_disallowed_msg return if self.exit_confirm_msg is not None: if not self.confirm(self.exit_confirm_msg, default=True): return raise ExitMenu() def at_exit(self): if self.exit_msg: print self.exit_msg def set_context(self, string, display=True): self.context = string if self.context is not None and display: print self.context def add_to_context(self, string): self.context += string def printc(self, string): print string if self.context is None: self.context = string else: self.context += '\n' + string def show_context(self): print self.header_format % self.name if self.context is not None: print self.context def item_is_submenu(self, i): return isinstance(self.items[i], Menu) def item_name(self, i): if self.item_is_submenu(i): return self.items[i].name elif isinstance(self.items[i], tuple): return self.items[i][1] else: return self.items[i] def item_value(self, i): if isinstance(self.items[i], tuple): return self.items[i][0] if self.return_index: return i return self.items[i] def input_str(self, prompt=None, regex=None, length_range=(None, None), empty_string_is_none=False, timeout=None): if prompt is None: prompt = self.prompt if regex is not None: while True: result = self.input_str(prompt, length_range=length_range, empty_string_is_none=empty_string_is_none) if result is None or re.match(regex + '$', result): return result else: print 'Value must match regular expression "%s"' % regex if length_range != (None, None): while True: result = self.input_str(prompt, empty_string_is_none=empty_string_is_none) if result is None: length = 0 else: length = len(result) if ((length_range[0] and length < length_range[0]) or (length_range[1] and length > length_range[1])): if length_range[0] and length_range[1]: print 'Value must have length in range [%d,%d]' % length_range elif length_range[0]: print 'Value must have length at least %d' % length_range[0] else: print 'Value must have length at most %d' % length_range[1] else: return result while True: try: # result = None # It is replaced either way if timeout: # assuming line buffering sys.stdout.write(safe_str(prompt)) sys.stdout.flush() rlist, _, _ = select([sys.stdin], [], [], timeout) if not rlist: # timeout occurred, simulate empty line result = '' else: result = unicode(raw_input(), conf.input_encoding).strip() else: result = unicode(raw_input(safe_str(prompt)), conf.input_encoding).strip() except EOFError: print 'quit' self.exit_menu() continue if result in exit_commands: self.exit_menu() continue if result in help_commands: self.general_help() continue if result in local_help_commands: self.local_help() continue if result in context_commands: self.show_context() continue if self.special_input_options(result): continue if empty_string_is_none and result == '': return None return result def special_input_options(self, result): """ Handles special, magic input for input_str Override this in subclasses to implement magic menu choices. Return True if str was some valid magic menu choice, False otherwise. """ return False def special_input_choice(self, in_str): """ Handle choices which are not simply menu items. Override this in subclasses to implement magic menu choices. Return True if str was some valid magic menu choice, False otherwise. """ return False def input_choice(self, number_of_choices, prompt=None): if prompt is None: prompt = self.prompt while True: result = self.input_str(prompt) if result == '': print 'Please enter something' # 'c' in main menu to change colours elif result == 'c': os.system('echo -e "\033[' + str(random.randint(40, 49)) + ';' + str(random.randint(30, 37)) + ';5m"') os.system('clear') self.show_context() # 'cs' in main menu to change colours back to default elif result == 'cs': os.system('echo -e "\033[0m"') os.system('clear') self.show_context() else: if result.isdigit(): choice = int(result) if choice == 0 and 10 <= number_of_choices: return 10 if 0 < choice <= number_of_choices: return choice if not self.special_input_choice(result): self.invalid_menu_choice(result) def invalid_menu_choice(self, in_str): print 'Please enter a valid choice.' def input_int(self, prompt=None, allowed_range=(None, None), null_allowed=False, default=None): if prompt is None: prompt = self.prompt if default is not None: prompt += ("[%s] " % default) while True: result = self.input_str(prompt) if result == '': if default is not None: return default elif null_allowed: return False try: value = int(result) if ((allowed_range[0] and value < allowed_range[0]) or (allowed_range[1] and value > allowed_range[1])): if allowed_range[0] and allowed_range[1]: print 'Value must be in range [%d,%d]' % allowed_range elif allowed_range[0]: print 'Value must be at least %d' % allowed_range[0] else: print 'Value must be at most %d' % allowed_range[1] else: return value except ValueError: print "Please enter an integer" def input_user(self, prompt=None): user = None while user is None: user = self.retrieve_user(self.input_str(prompt)) return user def retrieve_user(self, search_str): return self.search_ui(search_user, search_str, 'user') def input_product(self, prompt=None): product = None while product is None: product = self.retrieve_product(self.input_str(prompt)) return product def retrieve_product(self, search_str): return self.search_ui(search_product, search_str, 'product') def input_thing(self, prompt=None, permitted_things=('user', 'product'), add_nonexisting=(), empty_input_permitted=False, find_hidden_products=True): result = None while result is None: search_str = self.input_str(prompt) if search_str == '' and empty_input_permitted: return None result = self.search_for_thing(search_str, permitted_things, add_nonexisting, find_hidden_products) return result def input_multiple(self, prompt=None, permitted_things=('user', 'product'), add_nonexisting=(), empty_input_permitted=False, find_hidden_products=True): result = None num = 0 while result is None: search_str = self.input_str(prompt) search_lst = search_str.split(" ") if search_str == '' and empty_input_permitted: return None else: result = self.search_for_thing(search_str, permitted_things, add_nonexisting, find_hidden_products) num = 1 if (result is None) and (len(search_lst) > 1): print 'Interpreting input as " "' try: num = int(search_lst[0]) result = self.search_for_thing(" ".join(search_lst[1:]), permitted_things, add_nonexisting, find_hidden_products) # Her kan det legges inn en except ValueError, # men da blir det fort mye plaging av brukeren except Exception as e: print(e) return result, num def search_for_thing(self, search_str, permitted_things=('user', 'product'), add_non_existing=(), find_hidden_products=True): search_fun = {'user': search_user, 'product': search_product} results = {} result_values = {} for thing in permitted_things: results[thing] = search_fun[thing](search_str, self.session, find_hidden_products) result_values[thing] = self.search_result_value(results[thing]) selected_thing = argmax(result_values) if not results[selected_thing]: thing_for_type = {'card': 'user', 'username': 'user', 'bar_code': 'product', 'rfid': 'rfid'} type_guess = guess_data_type(search_str) if type_guess is not None and thing_for_type[type_guess] in add_non_existing: return self.search_add(search_str) # print 'No match found for "%s".' % search_str return None return self.search_ui2(search_str, results[selected_thing], selected_thing) @staticmethod def search_result_value(result): if result is None: return 0 if not isinstance(result, list): return 3 if len(result) == 0: return 0 if len(result) == 1: return 2 return 1 def search_add(self, string): type_guess = guess_data_type(string) if type_guess == 'username': print '"%s" looks like a username, but no such user exists.' % string if self.confirm('Create user %s?' % string): user = User(string, None) self.session.add(user) return user return None if type_guess == 'card': selector = Selector('"%s" looks like a card number, but no user with that card number exists.' % string, [('create', 'Create user with card number %s' % string), ('set', 'Set card number of an existing user to %s' % string)]) selection = selector.execute() if selection == 'create': username = self.input_str('Username for new user (should be same as PVV username)> ', User.name_re, (1, 10)) user = User(username, string) self.session.add(user) return user if selection == 'set': user = self.input_user('User to set card number for> ') old_card = user.card user.card = string print 'Card number of %s set to %s (was %s)' % (user.name, string, old_card) return user return None if type_guess == 'bar_code': print '"%s" looks like the bar code for a product, but no such product exists.' % string return None def search_ui(self, search_fun, search_str, thing): result = search_fun(search_str, self.session) return self.search_ui2(search_str, result, thing) def search_ui2(self, search_str, result, thing): if not isinstance(result, list): return result if len(result) == 0: print 'No %ss matching "%s"' % (thing, search_str) return None if len(result) == 1: msg = 'One %s matching "%s": %s. Use this?' % \ (thing, search_str, unicode(result[0])) if self.confirm(msg, default=True): return result[0] return None limit = 9 if len(result) > limit: select_header = '%d %ss matching "%s"; showing first %d' % \ (len(result), thing, search_str, limit) select_items = result[:limit] else: select_header = '%d %ss matching "%s"' % \ (len(result), thing, search_str) select_items = result selector = Selector(select_header, items=select_items, return_index=False) return selector.execute() def confirm(self, prompt, default=None, timeout=None): return ConfirmMenu(prompt, default, timeout).execute() def print_header(self): print "" print self.header_format % self.name def pause(self): self.input_str('.') def general_help(self): print ''' DIBBLER HELP The following commands are recognized (almost) everywhere: help, ? -- display this help what, ?? -- redisplay the current context help!, ??? -- display context-specific help (if any) faq -- display frequently asked questions (with answers) exit, quit, etc. -- exit from the current menu When prompted for a user, you can type (parts of) the user name or card number. When prompted for a product, you can type (parts of) the product name or barcode. About payment and "credit": When paying for something, use either Dibbler or the good old money box -- never both at the same time. Dibbler keeps track of a "credit" for each user, which is the amount of money PVVVV owes the user. This value decreases with the appropriate amount when you register a purchase, and you may increase it by putting money in the box and using the "Adjust credit" menu. ''' def local_help(self): if self.help_text is None: print 'no help here' else: print '' print 'Help for %s:' % (self.header_format % self.name) print self.help_text def execute(self, **kwargs): self.set_context(None) try: if self.uses_db and not self.session: self.session = Session() return self._execute(**kwargs) except ExitMenu: self.at_exit() return None finally: if self.session is not None: self.session.close() self.session = None def _execute(self, **kwargs): line_format = '%' + str(len(str(len(self.items)))) + 'd ) %s' while True: self.print_header() self.set_context(None) if len(self.items) == 0: self.printc('(empty menu)') self.pause() return None for i in range(len(self.items)): self.printc(line_format % (i + 1, self.item_name(i))) item_i = self.input_choice(len(self.items), prompt=self.prompt) - 1 if self.item_is_submenu(item_i): self.items[item_i].execute() else: return self.item_value(item_i) class MessageMenu(Menu): def __init__(self, name, message, pause_after_message=True): Menu.__init__(self, name) self.message = message.strip() self.pause_after_message = pause_after_message def _execute(self): self.print_header() print '' print self.message if self.pause_after_message: self.pause() class ConfirmMenu(Menu): def __init__(self, prompt='confirm?', default=None, timeout=0): Menu.__init__(self, 'question', prompt=prompt, exit_disallowed_msg='Please answer yes or no') self.default = default self.timeout = timeout def _execute(self): options = {True: '[y]/n', False: 'y/[n]', None: 'y/n'}[self.default] while True: result = self.input_str('%s (%s) ' % (self.prompt, options), timeout=self.timeout) result = result.lower().strip() if result in ['y', 'yes']: return True elif result in ['n', 'no']: return False elif self.default is not None and result == '': return self.default else: print 'Please answer yes or no' class Selector(Menu): def __init__(self, name, items=None, prompt='select> ', return_index=True, exit_msg=None, exit_confirm_msg=None, help_text=None): if items is None: items = [] Menu.__init__(self, name, items, prompt, return_index, exit_msg) self.header_format = '%s' def print_header(self): print self.header_format % self.name def local_help(self): if self.help_text is None: print 'This is a selection menu. Enter one of the listed numbers, or' print '\'exit\' to go out and do something else.' else: print '' print 'Help for selector (%s):' % self.name print self.help_text