From 12ad58961c688c56ce0d6d3b729a244845efce45 Mon Sep 17 00:00:00 2001 From: Christoffer Viken Date: Mon, 20 Feb 2017 12:19:03 +0000 Subject: [PATCH] --- text_based.py | 1502 ++------------------------------- text_interface/__init__.py | 8 + text_interface/addstock.py | 124 +++ text_interface/buymenu.py | 201 +++++ text_interface/editing.py | 187 ++++ text_interface/faq.py | 99 +++ text_interface/helpermenus.py | 509 +++++++++++ text_interface/mainmenu.py | 49 ++ text_interface/miscmenus.py | 212 +++++ text_interface/stats.py | 95 +++ 10 files changed, 1538 insertions(+), 1448 deletions(-) create mode 100644 text_interface/__init__.py create mode 100644 text_interface/addstock.py create mode 100644 text_interface/buymenu.py create mode 100644 text_interface/editing.py create mode 100644 text_interface/faq.py create mode 100644 text_interface/helpermenus.py create mode 100644 text_interface/mainmenu.py create mode 100644 text_interface/miscmenus.py create mode 100644 text_interface/stats.py diff --git a/text_based.py b/text_based.py index 57ff710..6735493 100755 --- a/text_based.py +++ b/text_based.py @@ -2,1463 +2,69 @@ # -*- coding: utf-8 -*- import random -import re import sys import traceback -from select import select -import sqlalchemy from helpers import * -from sqlalchemy import desc -from sqlalchemy.sql import func -from statistikkHelpers import statisticsTextOnly +from text_interface.addstock import AddStockMenu +from text_interface.buymenu import BuyMenu +from text_interface.editing import * +from text_interface.faq import FAQMenu +from text_interface.helpermenus import Menu +from text_interface.mainmenu import MainMenu +from text_interface.miscmenus import ProductSearchMenu, TransferMenu, AdjustCreditMenu, UserListMenu, ShowUserMenu, \ + ProductListMenu +from text_interface.stats import * random.seed() -exit_commands = ['exit', 'abort', 'quit', 'bye', 'eat flaming death', 'q'] -help_commands = ['help', '?'] -context_commands = ['what', '??'] -local_help_commands = ['help!', '???'] -faq_commands = ['faq'] -restart_commands = ['restart'] -class ExitMenu(Exception): - pass +if __name__ == '__main__': + if not conf.stop_allowed: + signal.signal(signal.SIGQUIT, signal.SIG_IGN) + if not conf.stop_allowed: + signal.signal(signal.SIGTSTP, signal.SIG_IGN) -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() - print 'hei hello' - continue - if result in faq_commands: - FAQMenu().execute() - continue - if result in restart_commands: - if self.confirm('Restart Dibbler?'): - restart() - continue - if empty_string_is_none and result == '': - return None - return result - - 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' i hovedmenyen for å endre farger - 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' i hovedmenyen for å sette standardfarger - 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 choice > 0 and 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_nonexisting=(), 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_nonexisting: - 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) - - def search_result_value(self, 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: + main = MainMenu('Dibbler main menu', + items=[BuyMenu(), + ProductListMenu(), + ShowUserMenu(), + UserListMenu(), + AdjustCreditMenu(), + TransferMenu(), + AddStockMenu(), + Menu('Add/edit', + items=[AddUserMenu(), + EditUserMenu(), + AddProductMenu(), + EditProductMenu(), + AdjustStockMenu(), + CleanupStockMenu(), ]), + ProductSearchMenu(), + Menu('Statistics', + items=[ProductPopularityMenu(), + ProductRevenueMenu(), + BalanceMenu(), + LoggedStatisticsMenu()]), + FAQMenu() + ], + exit_msg='happy happy joy joy', + exit_confirm_msg='Really quit Dibbler?') + if not conf.quit_allowed: + main.exit_disallowed_msg = 'You can check out any time you like, but you can never leave.' + while True: + # noinspection PyBroadException + try: + main.execute() + except KeyboardInterrupt: 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 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.' + print 'Interrupted.' + except: + print 'Something went wrong.' + print '%s: %s' % (sys.exc_info()[0], sys.exc_info()[1]) + if conf.show_tracebacks: + traceback.print_tb(sys.exc_info()[2]) else: - print '' - print 'Help for selector (%s):' % self.name - print self.help_text - - -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 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 FAQMenu(Menu): - def __init__(self): - Menu.__init__(self, 'Frequently Asked Questions') - self.items = [MessageMenu('What is the meaning with this program?', - ''' - We want to avoid keeping lots of cash in PVVVV\'s money box and to - make it easy to pay for stuff without using money. (Without using - money each time, that is. You do of course have to pay for the things - you buy eventually). - - Dibbler stores a "credit" amount for each user. When you register a - purchase in Dibbler, this amount is decreased. To increase your - credit, add money to the money box and use "Adjust credit" to tell - Dibbler about it. - '''), - MessageMenu('Can I still pay for stuff using cash?', - 'Yes. You can safely ignore this program completely.'), - MessageMenu('How do I exit from a submenu/dialog/thing?', - 'Type "exit" or C-d.'), - MessageMenu('What does "." mean?', - ''' - The "." character, known as "full stop" or "period", is most often - used to indicate the end of a sentence. - - It is also used by Dibbler to indicate that the program wants you to - read some text before continuing. Whenever some output ends with a - line containing only a period, you should read the lines above and - then press enter to continue. - '''), - MessageMenu('Why is the user interface so terribly unintuitive?', - ''' - Answer #1: It is not. - - Answer #2: We are trying to compete with PVV\'s microwave oven in - userfriendliness. - - Answer #3: YOU are unintuitive. - '''), - MessageMenu('Why is there no help command?', - 'There is. Have you tried typing "help"?'), - MessageMenu('Where are the easter eggs? I tried saying "moo", but nothing happened.', - 'Don\'t say "moo".'), - MessageMenu('Why does the program speak English when all the users are Norwegians?', - u'Godt spørsmål. Det virket sikkert som en god idé der og da.'), - MessageMenu('I found a bug; is there a reward?', - ''' - No. - - But if you are certain that it is a bug, not a feature, then you - should fix it (or better: force someone else to do it). - - Follow this procedure: - - 1. Check out the Dibbler code from https://dev.pvv.ntnu.no/svn/dibbler - - 2. Fix the bug. - - 3. Check that the program still runs (and, preferably, that the bug is - in fact fixed). - - 4. Commit. - - 5. Update the running copy from svn: - - $ su - - # su -l -s /bin/bash pvvvv - $ cd dibbler - $ svn up - - 6. Type "restart" in Dibbler to replace the running process by a new - one using the updated files. - '''), - MessageMenu('My question isn\'t listed here; what do I do?', - ''' - DON\'T PANIC. - - Follow this procedure: - - 1. Ask someone (or read the source code) and get an answer. - - 2. Check out the Dibbler code from https://dev.pvv.ntnu.no/svn/dibbler - - 3. Add your question (with answer) to the FAQ and commit. - - 4. Update the running copy from svn: - - $ su - - # su -l -s /bin/bash pvvvv - $ cd dibbler - $ svn up - - 5. Type "restart" in Dibbler to replace the running process by a new - one using the updated files. - ''')] - - -class TransferMenu(Menu): - def __init__(self): - Menu.__init__(self, 'Transfer credit between users', - uses_db=True) - - def _execute(self): - self.print_header() - amount = self.input_int('Transfer amount> ', (1, 100000)) - self.set_context('Transfering %d kr' % amount, display=False) - user1 = self.input_user('From user> ') - self.add_to_context(' from ' + user1.name) - user2 = self.input_user('To user> ') - self.add_to_context(' to ' + user2.name) - - t1 = Transaction(user1, amount, - 'transfer to ' + user2.name) - t2 = Transaction(user2, -amount, - 'transfer from ' + user1.name) - t1.perform_transaction() - t2.perform_transaction() - self.session.add(t1) - self.session.add(t2) - try: - self.session.commit() - print 'Transfered %d kr from %s to %s' % (amount, user1, user2) - print 'User %s\'s credit is now %d kr' % (user1, user1.credit) - print 'User %s\'s credit is now %d kr' % (user2, user2.credit) - except sqlalchemy.exc.SQLAlchemyError, e: - print 'Could not perform transfer: %s' % e - # self.pause() - - -class AddUserMenu(Menu): - def __init__(self): - Menu.__init__(self, 'Add user', uses_db=True) - - def _execute(self): - self.print_header() - username = self.input_str('Username (should be same as PVV username)> ', User.name_re, (1, 10)) - cardnum = self.input_str('Card number (optional)> ', User.card_re, (0, 10)) - cardnum = cardnum.lower() - rfid = self.input_str('RFID (optional)> ', User.rfid_re, (0, 10)) - user = User(username, cardnum, rfid) - self.session.add(user) - try: - self.session.commit() - print 'User %s stored' % username - except sqlalchemy.exc.IntegrityError, e: - print 'Could not store user %s: %s' % (username, e) - self.pause() - - -class EditUserMenu(Menu): - def __init__(self): - Menu.__init__(self, 'Edit user', uses_db=True) - self.help_text = ''' -The only editable part of a user is its card number and rfid. - -First select an existing user, then enter a new card number for that -user, then rfid (write an empty line to remove the card number or rfid). -''' - - def _execute(self): - self.print_header() - user = self.input_user('User> ') - self.printc('Editing user %s' % user.name) - card_str = '"%s"' % user.card - if user.card is None: - card_str = 'empty' - user.card = self.input_str('Card number (currently %s)> ' % card_str, - User.card_re, (0, 10), - empty_string_is_none=True) - if user.card: - user.card = user.card.lower() - - rfid_str = '"%s"' % user.rfid - if user.rfid is None: - rfid_str = 'empty' - user.rfid = self.input_str('RFID (currently %s)> ' % rfid_str, - User.rfid_re, (0, 10), - empty_string_is_none=True) - try: - self.session.commit() - print 'User %s stored' % user.name - except sqlalchemy.exc.SQLAlchemyError, e: - print 'Could not store user %s: %s' % (user.name, e) - self.pause() - - -class AddProductMenu(Menu): - def __init__(self): - Menu.__init__(self, 'Add product', uses_db=True) - - def _execute(self): - self.print_header() - bar_code = self.input_str('Bar code> ', Product.bar_code_re, (8, 13)) - name = self.input_str('Name> ', Product.name_re, (1, Product.name_length)) - price = self.input_int('Price> ', (1, 100000)) - product = Product(bar_code, name, price) - self.session.add(product) - try: - self.session.commit() - print 'Product %s stored' % name - except sqlalchemy.exc.SQLAlchemyError, e: - print 'Could not store product %s: %s' % (name, e) - self.pause() - - -class EditProductMenu(Menu): - def __init__(self): - Menu.__init__(self, 'Edit product', uses_db=True) - - def _execute(self): - self.print_header() - product = self.input_product('Product> ') - self.printc('Editing product %s' % product.name) - while True: - selector = Selector('Do what with %s?' % product.name, - items=[('name', 'Edit name'), - ('price', 'Edit price'), - ('barcode', 'Edit barcode'), - ('hidden', 'Edit hidden status'), - ('store', 'Store')]) - what = selector.execute() - if what == 'name': - product.name = self.input_str('Name[%s]> ' % product.name, Product.name_re, (1, product.name_length)) - elif what == 'price': - product.price = self.input_int('Price[%s]> ' % product.price, (1, 100000)) - elif what == 'barcode': - product.bar_code = self.input_str('Bar code[%s]> ' % product.bar_code, Product.bar_code_re, (8, 13)) - elif what == 'hidden': - product.hidden = self.confirm('Hidden[%s]' % ("Y" if product.hidden else "N"), False) - elif what == 'store': - try: - self.session.commit() - print 'Product %s stored' % product.name - except sqlalchemy.exc.SQLAlchemyError, e: - print 'Could not store product %s: %s' % (product.name, e) - self.pause() - return - elif what is None: - print 'Edit aborted' - return - else: - print 'What what?' - - -class ShowUserMenu(Menu): - def __init__(self): - Menu.__init__(self, 'Show user', uses_db=True) - - def _execute(self): - self.print_header() - user = self.input_user('User name, card number or RFID> ') - print 'User name: %s' % user.name - print 'Card number: %s' % user.card - print 'RFID: %s' % user.rfid - print 'Credit: %s kr' % user.credit - selector = Selector('What do you want to know about %s?' % user.name, - items=[('transactions', 'Recent transactions (List of last ' + str( - conf.user_recent_transaction_limit) + ')'), - ('products', 'Which products %s has bought, and how many' % user.name), - ('transactions-all', 'Everything (List of all transactions)')]) - what = selector.execute() - if what == 'transactions': - self.print_transactions(user, conf.user_recent_transaction_limit) - elif what == 'products': - self.print_purchased_products(user) - elif what == 'transactions-all': - self.print_all_transactions(user) - else: - print 'What what?' - - def print_all_transactions(self, user): - num_trans = len(user.transactions) - string = '%s\'s transactions (%d):\n' % (user.name, num_trans) - for t in user.transactions[::-1]: - string += ' * %s: %s %d kr, ' % \ - (t.time.strftime('%Y-%m-%d %H:%M'), - {True: 'in', False: 'out'}[t.amount < 0], - abs(t.amount)) - if t.purchase: - string += 'purchase (' - string += ', '.join(map(lambda e: e.product.name, - t.purchase.entries)) - string += ')' - if t.penalty > 1: - string += ' * %dx penalty applied' % t.penalty - else: - string += t.description - string += '\n' - less(string) - - def print_transactions(self, user, limit=10): - num_trans = len(user.transactions) - if num_trans <= limit: - string = '%s\'s transactions (%d):\n' % (user.name, num_trans) - else: - string = '%s\'s transactions (%d, showing only last %d):\n' % (user.name, num_trans, limit) - for t in user.transactions[-1:-limit - 1:-1]: - string += ' * %s: %s %d kr, ' % \ - (t.time.strftime('%Y-%m-%d %H:%M'), - {True: 'in', False: 'out'}[t.amount < 0], - abs(t.amount)) - if t.purchase: - string += 'purchase (' - string += ', '.join(map(lambda e: e.product.name, - t.purchase.entries)) - string += ')' - if t.penalty > 1: - string += ' * %dx penalty applied' % t.penalty - else: - string += t.description - string += '\n' - less(string) - - def print_purchased_products(self, user): - products = [] - for ref in user.products: - product = ref.product - count = ref.count - products.append((product, count)) - num_products = len(products) - if num_products == 0: - print 'No products purchased yet' - else: - text = '' - text += 'Products purchased:\n' - for product, count in products: - text += u'{0:<47} {1:>3}\n'.format(product.name, count) - less(text) - - -class UserListMenu(Menu): - def __init__(self): - Menu.__init__(self, 'User list', uses_db=True) - - def _execute(self): - self.print_header() - user_list = self.session.query(User).all() - total_credit = self.session.query(sqlalchemy.func.sum(User.credit)).first()[0] - - line_format = '%-12s | %6s\n' - hline = '---------------------\n' - text = '' - text += line_format % ('username', 'credit') - text += hline - for user in user_list: - text += line_format % (user.name, user.credit) - text += hline - text += line_format % ('total credit', total_credit) - less(text) - - -class BuyMenu(Menu): - def __init__(self, session=None): - Menu.__init__(self, 'Buy', uses_db=True) - if session: - self.session = session - self.superfast_mode = False - self.help_text = ''' -Each purchase may contain one or more products and one or more buyers. - -Enter products (by name or bar code) and buyers (by name or bar code) -in any order. The information gathered so far is displayed after each -addition, and you can type 'what' at any time to redisplay it. - -When finished, write an empty line to confirm the purchase. -''' - - def credit_check(self, user): - """ - - :param user: - :type user: User - :rtype: boolean - """ - assert isinstance(user, User) - - return user.credit > conf.low_credit_warning_limit - - def low_credit_warning(self, user, timeout=False): - assert isinstance(user, User) - - print "***********************************************************************" - print "***********************************************************************" - print "" - print "$$\ $$\ $$$$$$\ $$$$$$$\ $$\ $$\ $$$$$$\ $$\ $$\ $$$$$$\\" - print "$$ | $\ $$ |$$ __$$\ $$ __$$\ $$$\ $$ |\_$$ _|$$$\ $$ |$$ __$$\\" - print "$$ |$$$\ $$ |$$ / $$ |$$ | $$ |$$$$\ $$ | $$ | $$$$\ $$ |$$ / \__|" - print "$$ $$ $$\$$ |$$$$$$$$ |$$$$$$$ |$$ $$\$$ | $$ | $$ $$\$$ |$$ |$$$$\\" - print "$$$$ _$$$$ |$$ __$$ |$$ __$$< $$ \$$$$ | $$ | $$ \$$$$ |$$ |\_$$ |" - print "$$$ / \$$$ |$$ | $$ |$$ | $$ |$$ |\$$$ | $$ | $$ |\$$$ |$$ | $$ |" - print "$$ / \$$ |$$ | $$ |$$ | $$ |$$ | \$$ |$$$$$$\ $$ | \$$ |\$$$$$$ |" - print "\__/ \__|\__| \__|\__| \__|\__| \__|\______|\__| \__| \______/" - print "" - print "***********************************************************************" - print "***********************************************************************" - print "" - print "USER %s HAS LOWER CREDIT THAN %d." % (user.name, conf.low_credit_warning_limit) - print "THIS PURCHASE WILL CHARGE YOUR CREDIT TWICE AS MUCH." - print "CONSIDER PUTTING MONEY IN THE BOX TO AVOID THIS." - print "" - print "Do you want to continue with this purchase?" - - if timeout: - print"THIS PURCHASE WILL AUTOMATICALLY BE PERFORMED IN 3 MINUTES!" - return self.confirm(prompt=">", default=True, timeout=180) - else: - return self.confirm(prompt=">", default=True) - - def add_thing_to_purchase(self, thing): - if isinstance(thing, User): - if thing.is_anonymous(): - print '--------------------------------------------' - print'You are now purchasing as the user anonym.' - print'You have to put money in the anonym-jar.' - print '--------------------------------------------' - - if not self.credit_check(thing): - if self.low_credit_warning(user=thing, timeout=self.superfast_mode): - Transaction(thing, purchase=self.purchase, penalty=2) - else: - return False - else: - Transaction(thing, purchase=self.purchase) - elif isinstance(thing, Product): - PurchaseEntry(self.purchase, thing, 1) - return True - - def _execute(self, initial_contents=None): - self.print_header() - self.purchase = Purchase() - self.exit_confirm_msg = None - self.superfast_mode = False - - if initial_contents is None: - initial_contents = [] - - for thing in initial_contents: - self.add_thing_to_purchase(thing) - - isproduct = lambda t: isinstance(t, Product) - if len(initial_contents) > 0 and all(map(isproduct, initial_contents)): - self.superfast_mode = True - print '***********************************************' - print '****** Buy menu is in SUPERFASTmode[tm]! ******' - print '*** The purchase will be stored immediately ***' - print '*** when you enter a user. ***' - print '***********************************************' - - while True: - self.print_purchase() - self.printc({(False, False): 'Enter user or product identification', - (False, True): 'Enter user identification or more products', - (True, False): 'Enter product identification or more users', - (True, True): 'Enter more products or users, or an empty line to confirm' - }[(len(self.purchase.transactions) > 0, - len(self.purchase.entries) > 0)]) - - # Read in a 'thing' (product or user): - thing = self.input_thing(add_nonexisting=('user',), - empty_input_permitted=True, - find_hidden_products=False) - - # Possibly exit from the menu: - if thing is None: - if not self.complete_input(): - if self.confirm('Not enough information entered. Abort purchase?', - default=True): - return False - continue - break - else: - # once we get something in the - # purchase, we want to protect the - # user from accidentally killing it - self.exit_confirm_msg = 'Abort purchase?' - - # Add the thing to our purchase object: - if not self.add_thing_to_purchase(thing): - continue - - # In superfast mode, we complete the purchase once we get a user: - if self.superfast_mode and isinstance(thing, User): - break - - self.purchase.perform_purchase() - self.session.add(self.purchase) - try: - self.session.commit() - except sqlalchemy.exc.SQLAlchemyError, e: - print 'Could not store purchase: %s' % e - else: - print 'Purchase stored.' - self.print_purchase() - for t in self.purchase.transactions: - if not t.user.is_anonymous(): - print 'User %s\'s credit is now %d kr' % (t.user.name, t.user.credit) - if t.user.credit < conf.low_credit_warning_limit: - print 'USER %s HAS LOWER CREDIT THAN %d, AND SHOULD CONSIDER PUTTING SOME MONEY IN THE BOX.'\ - % (t.user.name, conf.low_credit_warning_limit) - # skriver til log - # print Product.price - # with open("dibbler-out.txt", "a") as f: - # f.write("purchase|"+ time() +"|"+self.purchase.entries[0].product.name+"|"+t.user.name+"|+str(Product.price)+|"+'-1'+"|\n") - - return True - - def complete_input(self): - return self.purchase.is_complete() - - def format_purchase(self): - self.purchase.set_price() - transactions = self.purchase.transactions - entries = self.purchase.entries - if len(transactions) == 0 and len(entries) == 0: - return None - string = 'Purchase:' - string += '\n buyers: ' - if len(transactions) == 0: - string += '(empty)' - else: - string += ', '.join( - map(lambda t: t.user.name + ("*" if t.user.credit < conf.user_recent_transaction_limit else ""), - transactions)) - string += '\n products: ' - if len(entries) == 0: - string += '(empty)' - else: - string += ', '.join(map(lambda e: '%s (%d kr)' % (e.product.name, e.product.price), - entries)) - if len(transactions) > 1: - string += '\n price per person: %d kr' % self.purchase.price_per_transaction() - if any(t.penalty > 1 for t in transactions): - string += ' *(%d kr)' % (self.purchase.price_per_transaction() * 2) - - string += '\n total price: %d kr' % self.purchase.price - - if any(t.penalty > 1 for t in transactions): - string += '\n *total with penalty: %d kr' % sum( - self.purchase.price_per_transaction() * t.penalty for t in transactions) - - return string - - def print_purchase(self): - info = self.format_purchase() - if info is not None: - self.set_context(info) - - -class AdjustStockMenu(Menu): - def __init__(self): - Menu.__init__(self, 'Adjust stock', uses_db=True) - - def _execute(self): - self.print_header() - product = self.input_product('Product> ') - - print 'The stock of this product is: %d ' % (product.stock) - print 'Write the number of products you have added to the stock' - print 'Alternatively, correct the stock for any mistakes' - add_stock = self.input_int('Added stock> ', (-1000, 1000)) - print 'You added %d to the stock of %s' % (add_stock, product) - - product.stock += add_stock - - print 'The stock is now %d' % (product.stock) - - try: - self.session.commit() - print 'Stock is now stored' - self.pause() - except sqlalchemy.exc.SQLAlchemyError, e: - print 'Could not store stock: %s' % (e) - self.pause() - return - print 'The stock is now %d' % (product.stock) - - -class CleanupStockMenu(Menu): - def __init__(self): - Menu.__init__(self, 'Stock Cleanup', uses_db=True) - - def _execute(self): - self.print_header() - - products = self.session.query(Product).filter(Product.stock != 0).all() - - print "Every product in stock will be printed." - print "Entering no value will keep current stock or set it to 0 if it is negative." - print "Entering a value will set current stock to that value." - print "Press enter to begin." - - self.pause() - - changed_products = [] - - for product in products: - oldstock = product.stock - product.stock = self.input_int(product.name, (0, 10000), default=max(0, oldstock)) - self.session.add(product) - if oldstock != product.stock: - changed_products.append((product, oldstock)) - - try: - self.session.commit() - print 'New stocks are now stored.' - self.pause() - except sqlalchemy.exc.SQLAlchemyError, e: - print 'Could not store stock: %s' % (e) - self.pause() - return - - for p in changed_products: - print p[0].name, ".", p[1], "->", p[0].stock - - -class AdjustCreditMenu(Menu): # reimplements ChargeMenu; these should be combined to one - def __init__(self): - Menu.__init__(self, 'Adjust credit', uses_db=True) - - def _execute(self): - self.print_header() - user = self.input_user('User> ') - print 'User %s\'s credit is %d kr' % (user.name, user.credit) - self.set_context('Adjusting credit for user %s' % user.name, display=False) - print '(Note on sign convention: Enter a positive amount here if you have' - print 'added money to the PVVVV money box, a negative amount if you have' - print 'taken money from it)' - amount = self.input_int('Add amount> ', (-100000, 100000)) - print '(The "log message" will show up in the transaction history in the' - print '"Show user" menu. It is not necessary to enter a message, but it' - print 'might be useful to help you remember why you adjusted the credit)' - description = self.input_str('Log message> ', length_range=(0, 50)) - if description == '': - description = 'manually adjusted credit' - transaction = Transaction(user, -amount, description) - transaction.perform_transaction() - self.session.add(transaction) - try: - self.session.commit() - print 'User %s\'s credit is now %d kr' % (user.name, user.credit) - except sqlalchemy.exc.SQLAlchemyError, e: - print 'Could not store transaction: %s' % e - # self.pause() - - -class ProductListMenu(Menu): - def __init__(self): - Menu.__init__(self, 'Product list', uses_db=True) - - def _execute(self): - self.print_header() - text = '' - product_list = self.session.query(Product).filter(Product.hidden == False).order_by(Product.stock.desc()) - total_value = 0 - for p in product_list: - total_value += p.price * p.stock - line_format = '%-15s | %5s | %-' + str(Product.name_length) + 's | %5s \n' - text += line_format % ('bar code', 'price', 'name', 'stock') - text += 78 * '-' + '\n' - for p in product_list: - text += line_format % (p.bar_code, p.price, p.name, p.stock) - text += 78 * '-' + '\n' - text += line_format % ('Total value', total_value, '', '',) - less(text) - - -class ProductSearchMenu(Menu): - def __init__(self): - Menu.__init__(self, 'Product search', uses_db=True) - - def _execute(self): - self.print_header() - self.set_context('Enter (part of) product name or bar code') - product = self.input_product() - print 'Result: %s, price: %d kr, bar code: %s, stock: %d, hidden: %s' % (product.name, product.price, - product.bar_code, product.stock, - ("Y" if product.hidden else "N")) - # self.pause() - - -class ProductPopularityMenu(Menu): - def __init__(self): - Menu.__init__(self, 'Products by popularity', uses_db=True) - - def _execute(self): - self.print_header() - text = '' - sub = \ - self.session.query(PurchaseEntry.product_id, - func.count('*').label('purchase_count')) \ - .group_by(PurchaseEntry.product_id) \ - .subquery() - product_list = \ - self.session.query(Product, sub.c.purchase_count) \ - .outerjoin((sub, Product.product_id == sub.c.product_id)) \ - .order_by(desc(sub.c.purchase_count)) \ - .filter(sub.c.purchase_count is not None) \ - .all() - line_format = '%10s | %-' + str(Product.name_length) + 's\n' - text += line_format % ('items sold', 'product') - text += '-' * 58 + '\n' - for product, number in product_list: - text += line_format % (number, product.name) - less(text) - - -class ProductRevenueMenu(Menu): - def __init__(self): - Menu.__init__(self, 'Products by revenue', uses_db=True) - - def _execute(self): - self.print_header() - text = '' - sub = \ - self.session.query(PurchaseEntry.product_id, - func.count('*').label('purchase_count')) \ - .group_by(PurchaseEntry.product_id) \ - .subquery() - product_list = \ - self.session.query(Product, sub.c.purchase_count) \ - .outerjoin((sub, Product.product_id == sub.c.product_id)) \ - .order_by(desc(sub.c.purchase_count * Product.price)) \ - .filter(sub.c.purchase_count is not None) \ - .all() - line_format = '%7s | %10s | %5s | %-' + str(Product.name_length) + 's\n' - text += line_format % ('revenue', 'items sold', 'price', 'product') - text += '-' * (31 + Product.name_length) + '\n' - for product, number in product_list: - text += line_format % (number * product.price, number, product.price, product.name) - less(text) - - -class BalanceMenu(Menu): - def __init__(self): - Menu.__init__(self, 'Total balance of PVVVV', uses_db=True) - - def _execute(self): - self.print_header() - text = '' - total_value = 0 - product_list = self.session.query(Product).all() - for p in product_list: - total_value += p.stock * p.price - - total_credit = self.session.query(sqlalchemy.func.sum(User.credit)).first()[0] - total_balance = total_value - total_credit - - line_format = '%15s | %5d \n' - text += line_format % ('Total value', total_value) - text += 24 * '-' + '\n' - text += line_format % ('Total credit', total_credit) - text += 24 * '-' + '\n' - text += line_format % ('Total balance', total_balance) - less(text) - - -class LoggedStatisticsMenu(Menu): - def __init__(self): - Menu.__init__(self, 'Statistics from log', uses_db=True) - - def _execute(self): - statisticsTextOnly() - - -def restart(): - # Does not work if the script is not executable, or if it was - # started by searching $PATH. - os.execv(sys.argv[0], sys.argv) - - -if not conf.stop_allowed: - signal.signal(signal.SIGQUIT, signal.SIG_IGN) - -if not conf.stop_allowed: - signal.signal(signal.SIGTSTP, signal.SIG_IGN) - - -class MainMenu(Menu): - def special_input_choice(self, in_str): - buy_menu = BuyMenu(Session()) - thing = buy_menu.search_for_thing(in_str) - if thing: - buy_menu.execute(initialContents=[thing]) - print self.show_context() - return True - return False - - def invalid_menu_choice(self, in_str): - print self.show_context() - - -class AddStockMenu(Menu): - def __init__(self): - Menu.__init__(self, 'Add stock and adjust credit', uses_db=True) - self.help_text = ''' -Enter what you have bought for PVVVV here, along with your user name and how -much money you're due in credits for the purchase when prompted. - ''' - - def _execute(self): - questions = { - (False, False): 'Enter user id or a string of the form " "', - (False, True): 'Enter user id or more strings of the form " "', - (True, False): 'Enter a string of the form " "', - (True, True): 'Enter more strings of the form " ", or an empty line to confirm' - } - - self.user = None - self.products = {} - self.price = 0 - - while True: - self.print_info() - self.printc(questions[bool(self.user), bool(len(self.products))]) - thing_price = 0 - - # Read in a 'thing' (product or user): - line = self.input_multiple(add_nonexisting=('user', 'product'), empty_input_permitted=True, - find_hidden_products=False) - - if line: - (thing, amount) = line - - if isinstance(thing, Product): - self.printc("%d of %s registered" % (amount, thing.name)) - thing_price = self.input_int('What did you pay a piece? ', (1, 100000), - default=thing.price) * amount - self.price += thing_price - - # once we get something in the - # purchase, we want to protect the - # user from accidentally killing it - self.exit_confirm_msg = 'Abort transaction?' - else: - thing = None - - if not self.complete_input(): - if self.confirm('Not enough information entered. Abort transaction?', default=True): - return False - continue - - break - - # Add the thing to the pending adjustments: - self.add_thing_to_pending(thing, amount, thing_price) - - if self.confirm('Do you want to change the credited amount?', default=False): - self.price = self.input_int('Total amount> ', (1, 100000), default=self.price) - - self.perform_transaction() - - def complete_input(self): - return (bool(self.user) and len(self.products) and self.price) - - def print_info(self): - print(6 + Product.name_length) * '-' - if self.price: - print("Amount to be credited: %" + str(Product.name_length - 17) + "i") % (self.price) - if self.user: - print("User to credit: %" + str(Product.name_length - 10) + "s") % (self.user.name) - print('\n%-' + str(Product.name_length - 1) + 's Amount') % ("Product") - print(6 + Product.name_length) * '-' - if len(self.products): - # print "Products added:" - # print (6+Product.name_length)*'-' - for product in self.products.keys(): - print('%' + str(-Product.name_length) + 's %5i') % (product.name, self.products[product][0]) - print(6 + Product.name_length) * '-' - - def add_thing_to_pending(self, thing, amount, price): - if isinstance(thing, User): - if self.user: - print "Only one user may be credited for a purchase, transfer credit manually afterwards" - return - else: - self.user = thing - elif thing in self.products.keys(): - print 'Already added this product, adding amounts' - self.products[thing][0] += amount - self.products[thing][1] += price - else: - self.products[thing] = [amount, price] - - def perform_transaction(self): - # self.user.credit += self.price - description = self.input_str('Log message> ', length_range=(0, 50)) - if description == '': - description = 'Purchased products for PVVVV, adjusted credit ' + str(self.price) - transaction = Transaction(self.user, -self.price, description) - transaction.perform_transaction() - self.session.add(transaction) - for product in self.products: - value = max(product.stock, 0) * product.price + self.products[product][1] - old_price = product.price - old_hidden = product.hidden - product.price = int(ceil(float(value) / (max(product.stock, 0) + self.products[product][0]))) - product.stock += self.products[product][0] - product.hidden = False - print "New stock for %s: %d" % (product.name, product.stock), \ - ("- New price: " + str(product.price) if old_price != product.price else ""), \ - ("- Removed hidden status" if old_hidden != product.hidden else "") - try: - self.session.commit() - print "Success! Transaction performed:" - # self.print_info() - print "User %s's credit is now %i" % (self.user.name, self.user.credit) - except sqlalchemy.exc.SQLAlchemyError, e: - print 'Could not perform transaction: %s' % e - - -main = MainMenu('Dibbler main menu', - items=[BuyMenu(), - ProductListMenu(), - ShowUserMenu(), - UserListMenu(), - AdjustCreditMenu(), - TransferMenu(), - AddStockMenu(), - Menu('Add/edit', - items=[AddUserMenu(), - EditUserMenu(), - AddProductMenu(), - EditProductMenu(), - AdjustStockMenu(), - CleanupStockMenu(), ]), - ProductSearchMenu(), - Menu('Statistics', - items=[ProductPopularityMenu(), - ProductRevenueMenu(), - BalanceMenu(), - LoggedStatisticsMenu()]), - FAQMenu() - ], - exit_msg='happy happy joy joy', - exit_confirm_msg='Really quit Dibbler?') -if not conf.quit_allowed: - main.exit_disallowed_msg = 'You can check out any time you like, but you can never leave.' -while True: - try: - main.execute() - except KeyboardInterrupt: - print '' - print 'Interrupted.' - except: - print 'Something went wrong.' - print '%s: %s' % sys.exc_info()[0], sys.exc_info()[1] - if conf.show_tracebacks: - traceback.print_tb(sys.exc_info()[2]) - else: - break - print 'Restarting main menu.' + break + print 'Restarting main menu.' diff --git a/text_interface/__init__.py b/text_interface/__init__.py new file mode 100644 index 0000000..1a76240 --- /dev/null +++ b/text_interface/__init__.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +exit_commands = ['exit', 'abort', 'quit', 'bye', 'eat flaming death', 'q'] +help_commands = ['help', '?'] +context_commands = ['what', '??'] +local_help_commands = ['help!', '???'] +faq_commands = ['faq'] +restart_commands = ['restart'] diff --git a/text_interface/addstock.py b/text_interface/addstock.py new file mode 100644 index 0000000..a5e4fe2 --- /dev/null +++ b/text_interface/addstock.py @@ -0,0 +1,124 @@ +from math import ceil + +import sqlalchemy +from db import Product, User, Transaction +from text_interface.helpermenus import Menu + + +class AddStockMenu(Menu): + def __init__(self): + Menu.__init__(self, 'Add stock and adjust credit', uses_db=True) + self.help_text = ''' +Enter what you have bought for PVVVV here, along with your user name and how +much money you're due in credits for the purchase when prompted.\n''' + self.user = None + + def _execute(self): + questions = { + (False, False): 'Enter user id or a string of the form " "', + (False, True): 'Enter user id or more strings of the form " "', + (True, False): 'Enter a string of the form " "', + (True, True): 'Enter more strings of the form " ", or an empty line to confirm' + } + + self.user = None + self.products = {} + self.price = 0 + + while True: + self.print_info() + self.printc(questions[bool(self.user), bool(len(self.products))]) + thing_price = 0 + + # Read in a 'thing' (product or user): + line = self.input_multiple(add_nonexisting=('user', 'product'), empty_input_permitted=True, + find_hidden_products=False) + + if line: + (thing, amount) = line + + if isinstance(thing, Product): + self.printc("%d of %s registered" % (amount, thing.name)) + thing_price = self.input_int('What did you pay a piece? ', (1, 100000), + default=thing.price) * amount + self.price += thing_price + + # once we get something in the + # purchase, we want to protect the + # user from accidentally killing it + self.exit_confirm_msg = 'Abort transaction?' + else: + # thing = None + + if not self.complete_input(): + if self.confirm('Not enough information entered. Abort transaction?', default=True): + return False + continue + + break + + # Add the thing to the pending adjustments: + self.add_thing_to_pending(thing, amount, thing_price) + + if self.confirm('Do you want to change the credited amount?', default=False): + self.price = self.input_int('Total amount> ', (1, 100000), default=self.price) + + self.perform_transaction() + + def complete_input(self): + return bool(self.user) and len(self.products) and self.price + + def print_info(self): + print(6 + Product.name_length) * '-' + if self.price: + print("Amount to be credited: %" + str(Product.name_length - 17) + "i") % self.price + if self.user: + print("User to credit: %" + str(Product.name_length - 10) + "s") % self.user.name + print('\n%-' + str(Product.name_length - 1) + 's Amount') % "Product" + print(6 + Product.name_length) * '-' + if len(self.products): + # print "Products added:" + # print (6+Product.name_length)*'-' + for product in self.products.keys(): + print('%' + str(-Product.name_length) + 's %5i') % (product.name, self.products[product][0]) + print(6 + Product.name_length) * '-' + + def add_thing_to_pending(self, thing, amount, price): + if isinstance(thing, User): + if self.user: + print "Only one user may be credited for a purchase, transfer credit manually afterwards" + return + else: + self.user = thing + elif thing in self.products.keys(): + print 'Already added this product, adding amounts' + self.products[thing][0] += amount + self.products[thing][1] += price + else: + self.products[thing] = [amount, price] + + def perform_transaction(self): + # self.user.credit += self.price + description = self.input_str('Log message> ', length_range=(0, 50)) + if description == '': + description = 'Purchased products for PVVVV, adjusted credit ' + str(self.price) + transaction = Transaction(self.user, -self.price, description) + transaction.perform_transaction() + self.session.add(transaction) + for product in self.products: + value = max(product.stock, 0) * product.price + self.products[product][1] + old_price = product.price + old_hidden = product.hidden + product.price = int(ceil(float(value) / (max(product.stock, 0) + self.products[product][0]))) + product.stock += self.products[product][0] + product.hidden = False + print "New stock for %s: %d" % (product.name, product.stock), \ + ("- New price: " + str(product.price) if old_price != product.price else ""), \ + ("- Removed hidden status" if old_hidden != product.hidden else "") + try: + self.session.commit() + print "Success! Transaction performed:" + # self.print_info() + print "User %s's credit is now %i" % (self.user.name, self.user.credit) + except sqlalchemy.exc.SQLAlchemyError, e: + print 'Could not perform transaction: %s' % e diff --git a/text_interface/buymenu.py b/text_interface/buymenu.py new file mode 100644 index 0000000..8183857 --- /dev/null +++ b/text_interface/buymenu.py @@ -0,0 +1,201 @@ +import conf +import sqlalchemy +from db import User, Purchase, PurchaseEntry, Transaction, Product +from text_interface.helpermenus import Menu + + +class BuyMenu(Menu): + def __init__(self, session=None): + Menu.__init__(self, 'Buy', uses_db=True) + if session: + self.session = session + self.superfast_mode = False + self.help_text = ''' +Each purchase may contain one or more products and one or more buyers. + +Enter products (by name or bar code) and buyers (by name or bar code) +in any order. The information gathered so far is displayed after each +addition, and you can type 'what' at any time to redisplay it. + +When finished, write an empty line to confirm the purchase.\n''' + + @staticmethod + def credit_check(user): + """ + + :param user: + :type user: User + :rtype: boolean + """ + assert isinstance(user, User) + + return user.credit > conf.low_credit_warning_limit + + def low_credit_warning(self, user, timeout=False): + assert isinstance(user, User) + + print "***********************************************************************" + print "***********************************************************************" + print "" + print "$$\ $$\ $$$$$$\ $$$$$$$\ $$\ $$\ $$$$$$\ $$\ $$\ $$$$$$\\" + print "$$ | $\ $$ |$$ __$$\ $$ __$$\ $$$\ $$ |\_$$ _|$$$\ $$ |$$ __$$\\" + print "$$ |$$$\ $$ |$$ / $$ |$$ | $$ |$$$$\ $$ | $$ | $$$$\ $$ |$$ / \__|" + print "$$ $$ $$\$$ |$$$$$$$$ |$$$$$$$ |$$ $$\$$ | $$ | $$ $$\$$ |$$ |$$$$\\" + print "$$$$ _$$$$ |$$ __$$ |$$ __$$< $$ \$$$$ | $$ | $$ \$$$$ |$$ |\_$$ |" + print "$$$ / \$$$ |$$ | $$ |$$ | $$ |$$ |\$$$ | $$ | $$ |\$$$ |$$ | $$ |" + print "$$ / \$$ |$$ | $$ |$$ | $$ |$$ | \$$ |$$$$$$\ $$ | \$$ |\$$$$$$ |" + print "\__/ \__|\__| \__|\__| \__|\__| \__|\______|\__| \__| \______/" + print "" + print "***********************************************************************" + print "***********************************************************************" + print "" + print "USER %s HAS LOWER CREDIT THAN %d." % (user.name, conf.low_credit_warning_limit) + print "THIS PURCHASE WILL CHARGE YOUR CREDIT TWICE AS MUCH." + print "CONSIDER PUTTING MONEY IN THE BOX TO AVOID THIS." + print "" + print "Do you want to continue with this purchase?" + + if timeout: + print"THIS PURCHASE WILL AUTOMATICALLY BE PERFORMED IN 3 MINUTES!" + return self.confirm(prompt=">", default=True, timeout=180) + else: + return self.confirm(prompt=">", default=True) + + def add_thing_to_purchase(self, thing, amount=1): + if isinstance(thing, User): + if thing.is_anonymous(): + print '--------------------------------------------' + print'You are now purchasing as the user anonym.' + print'You have to put money in the anonym-jar.' + print '--------------------------------------------' + + if not self.credit_check(thing): + if self.low_credit_warning(user=thing, timeout=self.superfast_mode): + Transaction(thing, purchase=self.purchase, penalty=2) + else: + return False + else: + Transaction(thing, purchase=self.purchase) + elif isinstance(thing, Product): + PurchaseEntry(self.purchase, thing, amount) + return True + + def _execute(self, initial_contents=None): + self.print_header() + self.purchase = Purchase() + self.exit_confirm_msg = None + self.superfast_mode = False + + if initial_contents is None: + initial_contents = [] + + for thing, num in initial_contents: + self.add_thing_to_purchase(thing, num) + + def is_product(candidate): + return isinstance(candidate, Product) + + if len(initial_contents) > 0 and all(map(is_product, initial_contents)): + self.superfast_mode = True + print '***********************************************' + print '****** Buy menu is in SUPERFASTmode[tm]! ******' + print '*** The purchase will be stored immediately ***' + print '*** when you enter a user. ***' + print '***********************************************' + + while True: + self.print_purchase() + self.printc({(False, False): 'Enter user or product identification', + (False, True): 'Enter user identification or more products', + (True, False): 'Enter product identification or more users', + (True, True): 'Enter more products or users, or an empty line to confirm' + }[(len(self.purchase.transactions) > 0, + len(self.purchase.entries) > 0)]) + + # Read in a 'thing' (product or user): + line = self.input_multiple(add_nonexisting=('user', 'product'), empty_input_permitted=True, + find_hidden_products=False) + + thing, num = line + + # Possibly exit from the menu: + if thing is None: + if not self.complete_input(): + if self.confirm('Not enough information entered. Abort purchase?', + default=True): + return False + continue + break + else: + # once we get something in the + # purchase, we want to protect the + # user from accidentally killing it + self.exit_confirm_msg = 'Abort purchase?' + + # Add the thing to our purchase object: + if not self.add_thing_to_purchase(thing, amount=num): + continue + + # In super-fast mode, we complete the purchase once we get a user: + if self.superfast_mode and isinstance(thing, User): + break + + self.purchase.perform_purchase() + self.session.add(self.purchase) + try: + self.session.commit() + except sqlalchemy.exc.SQLAlchemyError, e: + print 'Could not store purchase: %s' % e + else: + print 'Purchase stored.' + self.print_purchase() + for t in self.purchase.transactions: + if not t.user.is_anonymous(): + print 'User %s\'s credit is now %d kr' % (t.user.name, t.user.credit) + if t.user.credit < conf.low_credit_warning_limit: + print 'USER %s HAS LOWER CREDIT THAN %d, AND SHOULD CONSIDER PUTTING SOME MONEY IN THE BOX.' \ + % (t.user.name, conf.low_credit_warning_limit) + + return True + + def complete_input(self): + return self.purchase.is_complete() + + def format_purchase(self): + self.purchase.set_price() + transactions = self.purchase.transactions + entries = self.purchase.entries + if len(transactions) == 0 and len(entries) == 0: + return None + string = 'Purchase:' + string += '\n buyers: ' + if len(transactions) == 0: + string += '(empty)' + else: + string += ', '.join( + map(lambda t: t.user.name + ("*" if t.user.credit < conf.user_recent_transaction_limit else ""), + transactions)) + string += '\n products: ' + if len(entries) == 0: + string += '(empty)' + else: + string += "\n " + string += '\n '.join(map(lambda e: '%dx %s (%d kr)' % (e.amount, e.product.name, e.product.price), + entries)) + if len(transactions) > 1: + string += '\n price per person: %d kr' % self.purchase.price_per_transaction() + if any(t.penalty > 1 for t in transactions): + string += ' *(%d kr)' % (self.purchase.price_per_transaction() * 2) + + string += '\n total price: %d kr' % self.purchase.price + + if any(t.penalty > 1 for t in transactions): + string += '\n *total with penalty: %d kr' % sum( + self.purchase.price_per_transaction() * t.penalty for t in transactions) + + return string + + def print_purchase(self): + info = self.format_purchase() + if info is not None: + self.set_context(info) diff --git a/text_interface/editing.py b/text_interface/editing.py new file mode 100644 index 0000000..9780bc2 --- /dev/null +++ b/text_interface/editing.py @@ -0,0 +1,187 @@ +import sqlalchemy +from db import User, Product +from text_interface.helpermenus import Menu, Selector + +__all__ = ["AddUserMenu", "AddProductMenu", "EditProductMenu", "AdjustStockMenu", "CleanupStockMenu", "EditUserMenu"] + + +class AddUserMenu(Menu): + def __init__(self): + Menu.__init__(self, 'Add user', uses_db=True) + + def _execute(self): + self.print_header() + username = self.input_str('Username (should be same as PVV username)> ', User.name_re, (1, 10)) + cardnum = self.input_str('Card number (optional)> ', User.card_re, (0, 10)) + cardnum = cardnum.lower() + rfid = self.input_str('RFID (optional)> ', User.rfid_re, (0, 10)) + user = User(username, cardnum, rfid) + self.session.add(user) + try: + self.session.commit() + print 'User %s stored' % username + except sqlalchemy.exc.IntegrityError, e: + print 'Could not store user %s: %s' % (username, e) + self.pause() + + +class EditUserMenu(Menu): + def __init__(self): + Menu.__init__(self, 'Edit user', uses_db=True) + self.help_text = ''' +The only editable part of a user is its card number and rfid. + +First select an existing user, then enter a new card number for that +user, then rfid (write an empty line to remove the card number or rfid). +''' + + def _execute(self): + self.print_header() + user = self.input_user('User> ') + self.printc('Editing user %s' % user.name) + card_str = '"%s"' % user.card + if user.card is None: + card_str = 'empty' + user.card = self.input_str('Card number (currently %s)> ' % card_str, + User.card_re, (0, 10), + empty_string_is_none=True) + if user.card: + user.card = user.card.lower() + + rfid_str = '"%s"' % user.rfid + if user.rfid is None: + rfid_str = 'empty' + user.rfid = self.input_str('RFID (currently %s)> ' % rfid_str, + User.rfid_re, (0, 10), + empty_string_is_none=True) + try: + self.session.commit() + print 'User %s stored' % user.name + except sqlalchemy.exc.SQLAlchemyError, e: + print 'Could not store user %s: %s' % (user.name, e) + self.pause() + + +class AddProductMenu(Menu): + def __init__(self): + Menu.__init__(self, 'Add product', uses_db=True) + + def _execute(self): + self.print_header() + bar_code = self.input_str('Bar code> ', Product.bar_code_re, (8, 13)) + name = self.input_str('Name> ', Product.name_re, (1, Product.name_length)) + price = self.input_int('Price> ', (1, 100000)) + product = Product(bar_code, name, price) + self.session.add(product) + try: + self.session.commit() + print 'Product %s stored' % name + except sqlalchemy.exc.SQLAlchemyError, e: + print 'Could not store product %s: %s' % (name, e) + self.pause() + + +class EditProductMenu(Menu): + def __init__(self): + Menu.__init__(self, 'Edit product', uses_db=True) + + def _execute(self): + self.print_header() + product = self.input_product('Product> ') + self.printc('Editing product %s' % product.name) + while True: + selector = Selector('Do what with %s?' % product.name, + items=[('name', 'Edit name'), + ('price', 'Edit price'), + ('barcode', 'Edit barcode'), + ('hidden', 'Edit hidden status'), + ('store', 'Store')]) + what = selector.execute() + if what == 'name': + product.name = self.input_str('Name[%s]> ' % product.name, Product.name_re, (1, product.name_length)) + elif what == 'price': + product.price = self.input_int('Price[%s]> ' % product.price, (1, 100000)) + elif what == 'barcode': + product.bar_code = self.input_str('Bar code[%s]> ' % product.bar_code, Product.bar_code_re, (8, 13)) + elif what == 'hidden': + product.hidden = self.confirm('Hidden[%s]' % ("Y" if product.hidden else "N"), False) + elif what == 'store': + try: + self.session.commit() + print 'Product %s stored' % product.name + except sqlalchemy.exc.SQLAlchemyError, e: + print 'Could not store product %s: %s' % (product.name, e) + self.pause() + return + elif what is None: + print 'Edit aborted' + return + else: + print 'What what?' + + +class AdjustStockMenu(Menu): + def __init__(self): + Menu.__init__(self, 'Adjust stock', uses_db=True) + + def _execute(self): + self.print_header() + product = self.input_product('Product> ') + + print 'The stock of this product is: %d ' % product.stock + print 'Write the number of products you have added to the stock' + print 'Alternatively, correct the stock for any mistakes' + add_stock = self.input_int('Added stock> ', (-1000, 1000)) + print 'You added %d to the stock of %s' % (add_stock, product) + + product.stock += add_stock + + print 'The stock is now %d' % product.stock + + try: + self.session.commit() + print 'Stock is now stored' + self.pause() + except sqlalchemy.exc.SQLAlchemyError, e: + print 'Could not store stock: %s' % e + self.pause() + return + print 'The stock is now %d' % product.stock + + +class CleanupStockMenu(Menu): + def __init__(self): + Menu.__init__(self, 'Stock Cleanup', uses_db=True) + + def _execute(self): + self.print_header() + + products = self.session.query(Product).filter(Product.stock != 0).all() + + print "Every product in stock will be printed." + print "Entering no value will keep current stock or set it to 0 if it is negative." + print "Entering a value will set current stock to that value." + print "Press enter to begin." + + self.pause() + + changed_products = [] + + for product in products: + oldstock = product.stock + product.stock = self.input_int(product.name, (0, 10000), default=max(0, oldstock)) + self.session.add(product) + if oldstock != product.stock: + changed_products.append((product, oldstock)) + + try: + self.session.commit() + print 'New stocks are now stored.' + self.pause() + except sqlalchemy.exc.SQLAlchemyError, e: + print 'Could not store stock: %s' % e + self.pause() + return + + for p in changed_products: + print p[0].name, ".", p[1], "->", p[0].stock diff --git a/text_interface/faq.py b/text_interface/faq.py new file mode 100644 index 0000000..1604bb3 --- /dev/null +++ b/text_interface/faq.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- + +from .helpermenus import MessageMenu, Menu + + +class FAQMenu(Menu): + def __init__(self): + Menu.__init__(self, 'Frequently Asked Questions') + self.items = [MessageMenu('What is the meaning with this program?', + ''' + We want to avoid keeping lots of cash in PVVVV\'s money box and to + make it easy to pay for stuff without using money. (Without using + money each time, that is. You do of course have to pay for the things + you buy eventually). + + Dibbler stores a "credit" amount for each user. When you register a + purchase in Dibbler, this amount is decreased. To increase your + credit, add money to the money box and use "Adjust credit" to tell + Dibbler about it. + '''), + MessageMenu('Can I still pay for stuff using cash?', + 'Yes. You can safely ignore this program completely.'), + MessageMenu('How do I exit from a submenu/dialog/thing?', + 'Type "exit" or C-d.'), + MessageMenu('What does "." mean?', + ''' + The "." character, known as "full stop" or "period", is most often + used to indicate the end of a sentence. + + It is also used by Dibbler to indicate that the program wants you to + read some text before continuing. Whenever some output ends with a + line containing only a period, you should read the lines above and + then press enter to continue. + '''), + MessageMenu('Why is the user interface so terribly unintuitive?', + ''' + Answer #1: It is not. + + Answer #2: We are trying to compete with PVV\'s microwave oven in + userfriendliness. + + Answer #3: YOU are unintuitive. + '''), + MessageMenu('Why is there no help command?', + 'There is. Have you tried typing "help"?'), + MessageMenu('Where are the easter eggs? I tried saying "moo", but nothing happened.', + 'Don\'t say "moo".'), + MessageMenu('Why does the program speak English when all the users are Norwegians?', + u'Godt spørsmål. Det virket sikkert som en god idé der og da.'), + MessageMenu('I found a bug; is there a reward?', + ''' + No. + + But if you are certain that it is a bug, not a feature, then you + should fix it (or better: force someone else to do it). + + Follow this procedure: + + 1. Check out the Dibbler code from https://dev.pvv.ntnu.no/svn/dibbler + + 2. Fix the bug. + + 3. Check that the program still runs (and, preferably, that the bug is + in fact fixed). + + 4. Commit. + + 5. Update the running copy from svn: + + $ su - + # su -l -s /bin/bash pvvvv + $ cd dibbler + $ svn up + + 6. Type "restart" in Dibbler to replace the running process by a new + one using the updated files. + '''), + MessageMenu('My question isn\'t listed here; what do I do?', + ''' + DON\'T PANIC. + + Follow this procedure: + + 1. Ask someone (or read the source code) and get an answer. + + 2. Check out the Dibbler code from https://dev.pvv.ntnu.no/svn/dibbler + + 3. Add your question (with answer) to the FAQ and commit. + + 4. Update the running copy from svn: + + $ su - + # su -l -s /bin/bash pvvvv + $ cd dibbler + $ svn up + + 5. Type "restart" in Dibbler to replace the running process by a new + one using the updated files. + ''')] diff --git a/text_interface/helpermenus.py b/text_interface/helpermenus.py new file mode 100644 index 0000000..432334c --- /dev/null +++ b/text_interface/helpermenus.py @@ -0,0 +1,509 @@ +# -*- 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 diff --git a/text_interface/mainmenu.py b/text_interface/mainmenu.py new file mode 100644 index 0000000..a00051a --- /dev/null +++ b/text_interface/mainmenu.py @@ -0,0 +1,49 @@ + +# -*- coding: utf-8 -*- +import os + +import sys + +from db import Session +from text_interface import faq_commands, restart_commands +from text_interface.buymenu import BuyMenu +from text_interface.faq import FAQMenu +from text_interface.helpermenus import Menu + + +def restart(): + # Does not work if the script is not executable, or if it was + # started by searching $PATH. + os.execv(sys.argv[0], sys.argv) + + +class MainMenu(Menu): + def special_input_choice(self, in_str): + mv = in_str.split() + if len(mv) == 2 and mv[0].isdigit(): + num = int(mv[0]) + item_name = mv[1] + else: + num = 1 + item_name = in_str + buy_menu = BuyMenu(Session()) + thing = buy_menu.search_for_thing(item_name, find_hidden_products=False) + if thing: + buy_menu.execute(initial_contents=[(thing, num)]) + self.show_context() + return True + return False + + def special_input_options(self, result): + if result in faq_commands: + FAQMenu().execute() + return True + if result in restart_commands: + if self.confirm('Restart Dibbler?'): + restart() + pass + return True + return False + + def invalid_menu_choice(self, in_str): + print self.show_context() diff --git a/text_interface/miscmenus.py b/text_interface/miscmenus.py new file mode 100644 index 0000000..70ed2b0 --- /dev/null +++ b/text_interface/miscmenus.py @@ -0,0 +1,212 @@ +import conf +import sqlalchemy +from db import Transaction, Product, User +from helpers import less +from text_interface.helpermenus import Menu, Selector + + +class TransferMenu(Menu): + def __init__(self): + Menu.__init__(self, 'Transfer credit between users', + uses_db=True) + + def _execute(self): + self.print_header() + amount = self.input_int('Transfer amount> ', (1, 100000)) + self.set_context('Transfering %d kr' % amount, display=False) + user1 = self.input_user('From user> ') + self.add_to_context(' from ' + user1.name) + user2 = self.input_user('To user> ') + self.add_to_context(' to ' + user2.name) + + t1 = Transaction(user1, amount, + 'transfer to ' + user2.name) + t2 = Transaction(user2, -amount, + 'transfer from ' + user1.name) + t1.perform_transaction() + t2.perform_transaction() + self.session.add(t1) + self.session.add(t2) + try: + self.session.commit() + print 'Transfered %d kr from %s to %s' % (amount, user1, user2) + print 'User %s\'s credit is now %d kr' % (user1, user1.credit) + print 'User %s\'s credit is now %d kr' % (user2, user2.credit) + except sqlalchemy.exc.SQLAlchemyError, e: + print 'Could not perform transfer: %s' % e + # self.pause() + + +class ShowUserMenu(Menu): + def __init__(self): + Menu.__init__(self, 'Show user', uses_db=True) + + def _execute(self): + self.print_header() + user = self.input_user('User name, card number or RFID> ') + print 'User name: %s' % user.name + print 'Card number: %s' % user.card + print 'RFID: %s' % user.rfid + print 'Credit: %s kr' % user.credit + selector = Selector('What do you want to know about %s?' % user.name, + items=[('transactions', 'Recent transactions (List of last ' + str( + conf.user_recent_transaction_limit) + ')'), + ('products', 'Which products %s has bought, and how many' % user.name), + ('transactions-all', 'Everything (List of all transactions)')]) + what = selector.execute() + if what == 'transactions': + self.print_transactions(user, conf.user_recent_transaction_limit) + elif what == 'products': + self.print_purchased_products(user) + elif what == 'transactions-all': + self.print_all_transactions(user) + else: + print 'What what?' + + @staticmethod + def print_all_transactions(user): + num_trans = len(user.transactions) + string = '%s\'s transactions (%d):\n' % (user.name, num_trans) + for t in user.transactions[::-1]: + string += ' * %s: %s %d kr, ' % \ + (t.time.strftime('%Y-%m-%d %H:%M'), + {True: 'in', False: 'out'}[t.amount < 0], + abs(t.amount)) + if t.purchase: + string += 'purchase (' + string += ', '.join(map(lambda e: e.product.name, + t.purchase.entries)) + string += ')' + if t.penalty > 1: + string += ' * %dx penalty applied' % t.penalty + else: + string += t.description + string += '\n' + less(string) + + @staticmethod + def print_transactions(user, limit=10): + num_trans = len(user.transactions) + if num_trans <= limit: + string = '%s\'s transactions (%d):\n' % (user.name, num_trans) + else: + string = '%s\'s transactions (%d, showing only last %d):\n' % (user.name, num_trans, limit) + for t in user.transactions[-1:-limit - 1:-1]: + string += ' * %s: %s %d kr, ' % \ + (t.time.strftime('%Y-%m-%d %H:%M'), + {True: 'in', False: 'out'}[t.amount < 0], + abs(t.amount)) + if t.purchase: + string += 'purchase (' + string += ', '.join(map(lambda e: e.product.name, + t.purchase.entries)) + string += ')' + if t.penalty > 1: + string += ' * %dx penalty applied' % t.penalty + else: + string += t.description + string += '\n' + less(string) + + @staticmethod + def print_purchased_products(user): + products = [] + for ref in user.products: + product = ref.product + count = ref.count + products.append((product, count)) + num_products = len(products) + if num_products == 0: + print 'No products purchased yet' + else: + text = '' + text += 'Products purchased:\n' + for product, count in products: + text += u'{0:<47} {1:>3}\n'.format(product.name, count) + less(text) + + +class UserListMenu(Menu): + def __init__(self): + Menu.__init__(self, 'User list', uses_db=True) + + def _execute(self): + self.print_header() + user_list = self.session.query(User).all() + total_credit = self.session.query(sqlalchemy.func.sum(User.credit)).first()[0] + + line_format = '%-12s | %6s\n' + hline = '---------------------\n' + text = '' + text += line_format % ('username', 'credit') + text += hline + for user in user_list: + text += line_format % (user.name, user.credit) + text += hline + text += line_format % ('total credit', total_credit) + less(text) + + +class AdjustCreditMenu(Menu): # reimplements ChargeMenu; these should be combined to one + def __init__(self): + Menu.__init__(self, 'Adjust credit', uses_db=True) + + def _execute(self): + self.print_header() + user = self.input_user('User> ') + print 'User %s\'s credit is %d kr' % (user.name, user.credit) + self.set_context('Adjusting credit for user %s' % user.name, display=False) + print '(Note on sign convention: Enter a positive amount here if you have' + print 'added money to the PVVVV money box, a negative amount if you have' + print 'taken money from it)' + amount = self.input_int('Add amount> ', (-100000, 100000)) + print '(The "log message" will show up in the transaction history in the' + print '"Show user" menu. It is not necessary to enter a message, but it' + print 'might be useful to help you remember why you adjusted the credit)' + description = self.input_str('Log message> ', length_range=(0, 50)) + if description == '': + description = 'manually adjusted credit' + transaction = Transaction(user, -amount, description) + transaction.perform_transaction() + self.session.add(transaction) + try: + self.session.commit() + print 'User %s\'s credit is now %d kr' % (user.name, user.credit) + except sqlalchemy.exc.SQLAlchemyError, e: + print 'Could not store transaction: %s' % e + # self.pause() + + +class ProductListMenu(Menu): + def __init__(self): + Menu.__init__(self, 'Product list', uses_db=True) + + def _execute(self): + self.print_header() + text = '' + product_list = self.session.query(Product).filter(Product.hidden.is_(False)).order_by(Product.stock.desc()) + total_value = 0 + for p in product_list: + total_value += p.price * p.stock + line_format = '%-15s | %5s | %-' + str(Product.name_length) + 's | %5s \n' + text += line_format % ('bar code', 'price', 'name', 'stock') + text += 78 * '-' + '\n' + for p in product_list: + text += line_format % (p.bar_code, p.price, p.name, p.stock) + text += 78 * '-' + '\n' + text += line_format % ('Total value', total_value, '', '',) + less(text) + + +class ProductSearchMenu(Menu): + def __init__(self): + Menu.__init__(self, 'Product search', uses_db=True) + + def _execute(self): + self.print_header() + self.set_context('Enter (part of) product name or bar code') + product = self.input_product() + print 'Result: %s, price: %d kr, bar code: %s, stock: %d, hidden: %s' % (product.name, product.price, + product.bar_code, product.stock, + ("Y" if product.hidden else "N")) + # self.pause() diff --git a/text_interface/stats.py b/text_interface/stats.py new file mode 100644 index 0000000..a71a2a1 --- /dev/null +++ b/text_interface/stats.py @@ -0,0 +1,95 @@ +import sqlalchemy +from db import PurchaseEntry, Product, User +from helpers import less +from sqlalchemy import desc +from sqlalchemy import func +from statistikkHelpers import statisticsTextOnly +from text_interface.helpermenus import Menu + +__all__ = ["ProductPopularityMenu", "ProductRevenueMenu", "BalanceMenu", "LoggedStatisticsMenu"] + + +class ProductPopularityMenu(Menu): + def __init__(self): + Menu.__init__(self, 'Products by popularity', uses_db=True) + + def _execute(self): + self.print_header() + text = '' + sub = \ + self.session.query(PurchaseEntry.product_id, + func.count('*').label('purchase_count')) \ + .group_by(PurchaseEntry.product_id) \ + .subquery() + product_list = \ + self.session.query(Product, sub.c.purchase_count) \ + .outerjoin((sub, Product.product_id == sub.c.product_id)) \ + .order_by(desc(sub.c.purchase_count)) \ + .filter(sub.c.purchase_count is not None) \ + .all() + line_format = '%10s | %-' + str(Product.name_length) + 's\n' + text += line_format % ('items sold', 'product') + text += '-' * 58 + '\n' + for product, number in product_list: + text += line_format % (number, product.name) + less(text) + + +class ProductRevenueMenu(Menu): + def __init__(self): + Menu.__init__(self, 'Products by revenue', uses_db=True) + + def _execute(self): + self.print_header() + text = '' + sub = \ + self.session.query(PurchaseEntry.product_id, + func.count('*').label('purchase_count')) \ + .group_by(PurchaseEntry.product_id) \ + .subquery() + product_list = \ + self.session.query(Product, sub.c.purchase_count) \ + .outerjoin((sub, Product.product_id == sub.c.product_id)) \ + .order_by(desc(sub.c.purchase_count * Product.price)) \ + .filter(sub.c.purchase_count is not None) \ + .all() + line_format = '%7s | %10s | %5s | %-' + str(Product.name_length) + 's\n' + text += line_format % ('revenue', 'items sold', 'price', 'product') + text += '-' * (31 + Product.name_length) + '\n' + for product, number in product_list: + text += line_format % (number * product.price, number, product.price, product.name) + less(text) + + +class BalanceMenu(Menu): + def __init__(self): + Menu.__init__(self, 'Total balance of PVVVV', uses_db=True) + + def _execute(self): + self.print_header() + text = '' + total_value = 0 + product_list = self.session.query(Product).all() + for p in product_list: + total_value += p.stock * p.price + + total_credit = self.session.query(sqlalchemy.func.sum(User.credit)).first()[0] + total_balance = total_value - total_credit + + line_format = '%15s | %5d \n' + text += line_format % ('Total value', total_value) + text += 24 * '-' + '\n' + text += line_format % ('Total credit', total_credit) + text += 24 * '-' + '\n' + text += line_format % ('Total balance', total_balance) + less(text) + + +class LoggedStatisticsMenu(Menu): + def __init__(self): + Menu.__init__(self, 'Statistics from log', uses_db=True) + + def _execute(self): + statisticsTextOnly() + +