This commit is contained in:
robertem 2015-10-03 18:41:29 +00:00
parent e53b680dd2
commit c8a6f6c209
2 changed files with 1298 additions and 1198 deletions

13
db.py
View File

@ -92,15 +92,16 @@ class Transaction(Base):
user = relationship(User, backref=backref('transactions', order_by=time)) user = relationship(User, backref=backref('transactions', order_by=time))
def __init__(self, user, amount=0, description=None, purchase=None): def __init__(self, user, amount=0, description=None, purchase=None, penalty_ratio=1):
self.user = user self.user = user
self.amount = amount self.amount = amount
self.description = description self.description = description
self.purchase = purchase self.purchase = purchase
self.penalty_ratio = penalty_ratio
def perform_transaction(self): def perform_transaction(self):
self.time = datetime.datetime.now() self.time = datetime.datetime.now()
self.user.credit -= self.amount self.user.credit -= self.amount * self.penalty_ratio
if self.purchase: if self.purchase:
for entry in self.purchase.entries: for entry in self.purchase.entries:
entry.product.stock -= entry.amount entry.product.stock -= entry.amount
@ -111,12 +112,12 @@ class Purchase(Base):
id = Column(Integer, primary_key=True) id = Column(Integer, primary_key=True)
time = Column(DateTime) time = Column(DateTime)
# user_name = Column(Integer, ForeignKey('users.name')) # user_name = Column(Integer, ForeignKey('users.name'))
price = Column(Integer) price = Column(Integer)
# performed = Column(Boolean) # performed = Column(Boolean)
# user = relationship(User, backref=backref('purchases', order_by=id)) # user = relationship(User, backref=backref('purchases', order_by=id))
# users = relationship(User, secondary=purchase_user, backref='purhcases' # users = relationship(User, secondary=purchase_user, backref='purhcases'
transactions = relationship(Transaction, order_by=Transaction.user_name, backref='purchase') transactions = relationship(Transaction, order_by=Transaction.user_name, backref='purchase')
entries = relationship(PurchaseEntry, backref=backref("purchase")) entries = relationship(PurchaseEntry, backref=backref("purchase"))

View File

@ -5,9 +5,11 @@ import sqlalchemy
from sqlalchemy.sql import func from sqlalchemy.sql import func
from sqlalchemy import desc from sqlalchemy import desc
import re, sys, os, traceback, signal, readline import re, sys, os, traceback, signal, readline
from select import select
from helpers import * from helpers import *
import random import random
from statistikkHelpers import statisticsTextOnly from statistikkHelpers import statisticsTextOnly
from math import ceil
random.seed() random.seed()
exit_commands = ['exit', 'abort', 'quit', 'bye', 'eat flaming death', 'q'] exit_commands = ['exit', 'abort', 'quit', 'bye', 'eat flaming death', 'q']
@ -93,7 +95,9 @@ class Menu():
return self.items[i] return self.items[i]
def input_str(self, prompt=None, regex=None, length_range=(None,None), def input_str(self, prompt=None, regex=None, length_range=(None,None),
empty_string_is_none=False): empty_string_is_none=False, timeout=0):
if prompt == None:
prompt = self.prompt
if regex != None: if regex != None:
while True: while True:
result = self.input_str(prompt, length_range=length_range, result = self.input_str(prompt, length_range=length_range,
@ -119,8 +123,14 @@ class Menu():
print 'Value must have length at most %d' % length_range[1] print 'Value must have length at most %d' % length_range[1]
else: else:
return result return result
if prompt == None: if timeout != 0:
prompt = self.prompt print prompt,
rlist, _, _ = select([sys.stdin], [], [], timeout)
if rlist:
s = sys.stdin.readline()
return s
else:
return ''
while True: while True:
try: try:
result = unicode(raw_input(safe_str(prompt)), result = unicode(raw_input(safe_str(prompt)),
@ -274,7 +284,6 @@ class Menu():
return (result,num) return (result,num)
def search_for_thing(self, search_str, permitted_things=('user','product'), def search_for_thing(self, search_str, permitted_things=('user','product'),
add_nonexisting=()): add_nonexisting=()):
search_fun = {'user': search_user, search_fun = {'user': search_user,
@ -349,7 +358,7 @@ class Menu():
print 'No %ss matching "%s"' % (thing, search_str) print 'No %ss matching "%s"' % (thing, search_str)
return None return None
if len(result) == 1: if len(result) == 1:
msg = 'One %s matching "%s": %s. Use this?' %\ msg = 'One %s matching "%s": %s. Use this?' % \
(thing, search_str, unicode(result[0])) (thing, search_str, unicode(result[0]))
if self.confirm(msg, default=True): if self.confirm(msg, default=True):
return result[0] return result[0]
@ -369,8 +378,8 @@ class Menu():
def confirm(self, prompt, default=None): def confirm(self, prompt, default=None, timeout=0):
return ConfirmMenu(prompt, default).execute() return ConfirmMenu(prompt, default, timeout).execute()
def print_header(self): def print_header(self):
print print
@ -465,15 +474,16 @@ class Selector(Menu):
class ConfirmMenu(Menu): class ConfirmMenu(Menu):
def __init__(self, prompt='confirm?', default=None): def __init__(self, prompt='confirm?', default=None, timeout=0):
Menu.__init__(self, 'question', prompt=prompt, Menu.__init__(self, 'question', prompt=prompt,
exit_disallowed_msg='Please answer yes or no') exit_disallowed_msg='Please answer yes or no')
self.default=default self.default=default
self.timeout=0
def _execute(self): def _execute(self):
options = {True: '[y]/n', False: 'y/[n]', None: 'y/n'}[self.default] options = {True: '[y]/n', False: 'y/[n]', None: 'y/n'}[self.default]
while True: while True:
result = self.input_str('%s (%s) ' % (self.prompt, options)) result = self.input_str('%s (%s) ' % (self.prompt, options), timeout=self.timeout)
result = result.lower() result = result.lower()
if result in ['y','yes']: if result in ['y','yes']:
return True return True
@ -503,39 +513,39 @@ class FAQMenu(Menu):
Menu.__init__(self, 'Frequently Asked Questions') Menu.__init__(self, 'Frequently Asked Questions')
self.items = [MessageMenu('What is the meaning with this program?', 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 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 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 money each time, that is. You do of course have to pay for the things
you buy eventually). you buy eventually).
Dibbler stores a "credit" amount for each user. When you register a Dibbler stores a "credit" amount for each user. When you register a
purchase in Dibbler, this amount is decreased. To increase your purchase in Dibbler, this amount is decreased. To increase your
credit, add money to the money box and use "Adjust credit" to tell credit, add money to the money box and use "Adjust credit" to tell
Dibbler about it. Dibbler about it.
'''), '''),
MessageMenu('Can I still pay for stuff using cash?', MessageMenu('Can I still pay for stuff using cash?',
'Yes. You can safely ignore this program completely.'), 'Yes. You can safely ignore this program completely.'),
MessageMenu('How do I exit from a submenu/dialog/thing?', MessageMenu('How do I exit from a submenu/dialog/thing?',
'Type "exit" or C-d.'), 'Type "exit" or C-d.'),
MessageMenu('What does "." mean?', MessageMenu('What does "." mean?',
''' '''
The "." character, known as "full stop" or "period", is most often The "." character, known as "full stop" or "period", is most often
used to indicate the end of a sentence. used to indicate the end of a sentence.
It is also used by Dibbler to indicate that the program wants you to 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 read some text before continuing. Whenever some output ends with a
line containing only a period, you should read the lines above and line containing only a period, you should read the lines above and
then press enter to continue. then press enter to continue.
'''), '''),
MessageMenu('Why is the user interface so terribly unintuitive?', MessageMenu('Why is the user interface so terribly unintuitive?',
''' '''
Answer #1: It is not. Answer #1: It is not.
Answer #2: We are trying to compete with PVV\'s microwave oven in Answer #2: We are trying to compete with PVV\'s microwave oven in
userfriendliness. userfriendliness.
Answer #3: YOU are unintuitive. Answer #3: YOU are unintuitive.
'''), '''),
MessageMenu('Why is there no help command?', MessageMenu('Why is there no help command?',
'There is. Have you tried typing "help"?'), 'There is. Have you tried typing "help"?'),
MessageMenu('Where are the easter eggs? I tried saying "moo", but nothing happened.', MessageMenu('Where are the easter eggs? I tried saying "moo", but nothing happened.',
@ -544,54 +554,54 @@ Answer #3: YOU are unintuitive.
u'Godt spørsmål. Det virket sikkert som en god idé der og da.'), u'Godt spørsmål. Det virket sikkert som en god idé der og da.'),
MessageMenu('I found a bug; is there a reward?', MessageMenu('I found a bug; is there a reward?',
''' '''
No. No.
But if you are certain that it is a bug, not a feature, then you 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). should fix it (or better: force someone else to do it).
Follow this procedure: Follow this procedure:
1. Check out the Dibbler code from https://dev.pvv.ntnu.no/svn/dibbler 1. Check out the Dibbler code from https://dev.pvv.ntnu.no/svn/dibbler
2. Fix the bug. 2. Fix the bug.
3. Check that the program still runs (and, preferably, that the bug is 3. Check that the program still runs (and, preferably, that the bug is
in fact fixed). in fact fixed).
4. Commit. 4. Commit.
5. Update the running copy from svn: 5. Update the running copy from svn:
$ su - $ su -
# su -l -s /bin/bash pvvvv # su -l -s /bin/bash pvvvv
$ cd dibbler $ cd dibbler
$ svn up $ svn up
6. Type "restart" in Dibbler to replace the running process by a new 6. Type "restart" in Dibbler to replace the running process by a new
one using the updated files. one using the updated files.
'''), '''),
MessageMenu('My question isn\'t listed here; what do I do?', MessageMenu('My question isn\'t listed here; what do I do?',
''' '''
DON\'T PANIC. DON\'T PANIC.
Follow this procedure: Follow this procedure:
1. Ask someone (or read the source code) and get an answer. 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 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. 3. Add your question (with answer) to the FAQ and commit.
4. Update the running copy from svn: 4. Update the running copy from svn:
$ su - $ su -
# su -l -s /bin/bash pvvvv # su -l -s /bin/bash pvvvv
$ cd dibbler $ cd dibbler
$ svn up $ svn up
5. Type "restart" in Dibbler to replace the running process by a new 5. Type "restart" in Dibbler to replace the running process by a new
one using the updated files. one using the updated files.
''')] ''')]
@ -857,6 +867,47 @@ addition, and you can type 'what' at any time to redisplay it.
When finished, write an empty line to confirm the purchase. 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 > 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, 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=">")
def add_thing_to_purchase(self, thing): def add_thing_to_purchase(self, thing):
if isinstance(thing, User): if isinstance(thing, User):
@ -865,6 +916,13 @@ When finished, write an empty line to confirm the purchase.
print 'You are now purchasing as the user anonym.' print 'You are now purchasing as the user anonym.'
print 'You have to put money in the anonym-jar.' print 'You have to put money in the anonym-jar.'
print '--------------------------------------------' print '--------------------------------------------'
if not self.credit_check(thing):
if self.low_credit_warning(thing, self.superfast_mode ):
Transaction(thing, purchase=self.purchase, penalty_ratio=2)
else:
return False
else:
Transaction(thing, purchase=self.purchase) Transaction(thing, purchase=self.purchase)
elif isinstance(thing, Product): elif isinstance(thing, Product):
PurchaseEntry(self.purchase, thing, 1) PurchaseEntry(self.purchase, thing, 1)
@ -1006,6 +1064,46 @@ class AdjustStockMenu(Menu):
print 'The stock is now %d' % (product.stock) 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 class AdjustCreditMenu(Menu): # reimplements ChargeMenu; these should be combined to one
def __init__(self): def __init__(self):
Menu.__init__(self, 'Adjust credit', uses_db=True) Menu.__init__(self, 'Adjust credit', uses_db=True)
@ -1078,14 +1176,14 @@ class ProductPopularityMenu(Menu):
text = '' text = ''
sub = \ sub = \
self.session.query(PurchaseEntry.product_bar_code, self.session.query(PurchaseEntry.product_bar_code,
func.count('*').label('purchase_count'))\ func.count('*').label('purchase_count')) \
.group_by(PurchaseEntry.product_bar_code)\ .group_by(PurchaseEntry.product_bar_code) \
.subquery() .subquery()
product_list = \ product_list = \
self.session.query(Product, sub.c.purchase_count)\ self.session.query(Product, sub.c.purchase_count) \
.outerjoin((sub, Product.bar_code==sub.c.product_bar_code))\ .outerjoin((sub, Product.bar_code==sub.c.product_bar_code)) \
.order_by(desc(sub.c.purchase_count))\ .order_by(desc(sub.c.purchase_count)) \
.filter(sub.c.purchase_count != None)\ .filter(sub.c.purchase_count != None) \
.all() .all()
line_format = '%10s | %-'+str(Product.name_length)+'s\n' line_format = '%10s | %-'+str(Product.name_length)+'s\n'
text += line_format % ('items sold', 'product') text += line_format % ('items sold', 'product')
@ -1103,14 +1201,14 @@ class ProductRevenueMenu(Menu):
text = '' text = ''
sub = \ sub = \
self.session.query(PurchaseEntry.product_bar_code, self.session.query(PurchaseEntry.product_bar_code,
func.count('*').label('purchase_count'))\ func.count('*').label('purchase_count')) \
.group_by(PurchaseEntry.product_bar_code)\ .group_by(PurchaseEntry.product_bar_code) \
.subquery() .subquery()
product_list = \ product_list = \
self.session.query(Product, sub.c.purchase_count)\ self.session.query(Product, sub.c.purchase_count) \
.outerjoin((sub, Product.bar_code==sub.c.product_bar_code))\ .outerjoin((sub, Product.bar_code==sub.c.product_bar_code)) \
.order_by(desc(sub.c.purchase_count*Product.price))\ .order_by(desc(sub.c.purchase_count*Product.price)) \
.filter(sub.c.purchase_count != None)\ .filter(sub.c.purchase_count != None) \
.all() .all()
line_format = '%7s | %10s | %5s | %-'+str(Product.name_length)+'s\n' line_format = '%7s | %10s | %5s | %-'+str(Product.name_length)+'s\n'
text += line_format % ('revenue', 'items sold', 'price', 'product') text += line_format % ('revenue', 'items sold', 'price', 'product')
@ -1244,8 +1342,8 @@ much money you're due in credits for the purchase when prompted.
print ('\n%-'+str(Product.name_length-1)+'s Amount') % ("Product") print ('\n%-'+str(Product.name_length-1)+'s Amount') % ("Product")
print (6+Product.name_length)*'-' print (6+Product.name_length)*'-'
if len(self.products): if len(self.products):
# print "Products added:" # print "Products added:"
# print (6+Product.name_length)*'-' # print (6+Product.name_length)*'-'
for product in self.products.keys(): for product in self.products.keys():
print ('%'+str(-Product.name_length)+'s %5i') % (product.name, self.products[product]) print ('%'+str(-Product.name_length)+'s %5i') % (product.name, self.products[product])
print (6+Product.name_length)*'-' print (6+Product.name_length)*'-'
@ -1265,7 +1363,7 @@ much money you're due in credits for the purchase when prompted.
self.products[thing] = amount self.products[thing] = amount
def perform_transaction(self): def perform_transaction(self):
# self.user.credit += self.price # self.user.credit += self.price
description = self.input_str('Log message> ', length_range=(0,50)) description = self.input_str('Log message> ', length_range=(0,50))
if description == '': if description == '':
description = 'Purchased products for PVVVV, adjusted credit '+str(self.price) description = 'Purchased products for PVVVV, adjusted credit '+str(self.price)
@ -1277,7 +1375,7 @@ much money you're due in credits for the purchase when prompted.
try: try:
self.session.commit() self.session.commit()
print "Success! Transaction performed:" print "Success! Transaction performed:"
# self.print_info() # self.print_info()
print "User %s's credit is now %i" % (self.user.name, self.user.credit) print "User %s's credit is now %i" % (self.user.name, self.user.credit)
except sqlalchemy.exc.SQLAlchemyError, e: except sqlalchemy.exc.SQLAlchemyError, e:
print 'Could not perform transaction: %s' % e print 'Could not perform transaction: %s' % e
@ -1298,7 +1396,8 @@ main = MainMenu('Dibbler main menu',
EditUserMenu(), EditUserMenu(),
AddProductMenu(), AddProductMenu(),
EditProductMenu(), EditProductMenu(),
AdjustStockMenu(),]), AdjustStockMenu(),
CleanupStockMenu(),]),
ProductSearchMenu(), ProductSearchMenu(),
Menu('Statistics', Menu('Statistics',
items=[ProductPopularityMenu(), items=[ProductPopularityMenu(),