def get_list_for(user): if user is None or (hasattr(user, 'ANON') and user.ANON): return list( dear_god_why.fetchall(""" SELECT max(name) AS name , slug , count(*) AS nmembers FROM current_communities GROUP BY slug ORDER BY nmembers DESC, slug """)) else: return list( dear_god_why.fetchall( """ SELECT max(name) AS name , slug , count(*) AS nmembers , bool_or(participant = %s) AS is_member FROM current_communities GROUP BY slug ORDER BY nmembers ASC, slug """, (user.username, )))
def get_giving_for_profile(self, db=None): """Given a participant id and a date, return a list and a Decimal. This function is used to populate a participant's page for their own viewing pleasure. A half-injected dependency, that's what db is. """ if db is None: from gittip import db TIPS = """\ SELECT * FROM ( SELECT DISTINCT ON (tippee) amount , tippee , t.ctime , p.claimed_time FROM tips t JOIN participants p ON p.id = t.tippee WHERE tipper = %s AND p.is_suspicious IS NOT true AND p.claimed_time IS NOT NULL ORDER BY tippee , t.mtime DESC ) AS foo ORDER BY amount DESC , tippee """ tips = list(db.fetchall(TIPS, (self.id,))) # Compute the total. # ================== # For payday we only want to process payments to tippees who have # themselves opted into Gittip. For the tipper's profile page we want # to show the total amount they've pledged (so they're not surprised # when someone *does* start accepting tips and all of a sudden they're # hit with bigger charges. to_total = tips total = sum([t['amount'] for t in to_total]) if not total: # If to_total is an empty list, total is int 0. We want a Decimal. total = Decimal('0.00') return tips, total
def get_giving_for_profile(tipper, db=None): """Given a participant id and a date, return a list and a Decimal. This function is used to populate a participant's page for their own viewing pleasure. A half-injected dependency, that's what db is. """ if db is None: from gittip import db TIPS = """\ SELECT * FROM ( SELECT DISTINCT ON (tippee) amount , tippee , t.ctime , p.claimed_time FROM tips t JOIN participants p ON p.id = t.tippee WHERE tipper = %s AND p.is_suspicious IS NOT true AND p.claimed_time IS NOT NULL ORDER BY tippee , t.mtime DESC ) AS foo ORDER BY amount DESC , tippee """ tips = list(db.fetchall(TIPS, (tipper, ))) # Compute the total. # ================== # For payday we only want to process payments to tippees who have # themselves opted into Gittip. For the tipper's profile page we want to # show the total amount they've pledged (so they're not surprised when # someone *does* start accepting tips and all of a sudden they're hit with # bigger charges. to_total = tips total = sum([t['amount'] for t in to_total]) if not total: # If to_total is an empty list then total is int 0. We want a Decimal. total = Decimal('0.00') return tips, total
def get_list_for(user): if user is None or (hasattr(user, 'ANON') and user.ANON): return list(dear_god_why.fetchall(""" SELECT max(name) AS name , slug , count(*) AS nmembers FROM current_communities GROUP BY slug ORDER BY nmembers DESC, slug """)) else: return list(dear_god_why.fetchall(""" SELECT max(name) AS name , slug , count(*) AS nmembers , bool_or(participant = %s) AS is_member FROM current_communities GROUP BY slug ORDER BY nmembers ASC, slug """, (user.username,)))
def initialize_payday(): # Start Payday. # ============= # We try to start a new Payday. If there is a Payday that hasn't finished # yet, then the UNIQUE constraint on ts_end will kick in and notify us # of that. In that case we load the existing Payday and work on it some # more. We use the start time of the current Payday to synchronize our # work. try: rec = db.fetchone("INSERT INTO paydays DEFAULT VALUES " "RETURNING ts_start") log("Starting a new payday.") except IntegrityError: # Collision, we have a Payday already. rec = db.fetchone(""" SELECT ts_start FROM paydays WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz """) log("Picking up with an existing payday.") assert rec is not None # Must either create or recycle a Payday. payday_start = rec['ts_start'] log("Payday started at %s." % payday_start) START_PENDING = """\ UPDATE participants SET pending=0.00 WHERE pending IS NULL """ db.execute(START_PENDING) log("Zeroed out the pending column.") PARTICIPANTS = """\ SELECT id, balance, balanced_account_uri FROM participants WHERE claimed_time IS NOT NULL """ participants = db.fetchall(PARTICIPANTS) log("Fetched participants.") return participants, payday_start
def upsert(network, user_id, username, user_info, claim=False): """Given str, unicode, unicode, and dict, return unicode and boolean. Network is the name of a social network that we support (ASCII blah). User_id is an immutable unique identifier for the given user on the given social network. Username is the user's login/user_id on the given social network. We will try to claim that for them here on Gittip. If their username is already taken on Gittip then we give them a random one; they can change it on their Gittip profile page. User_id and username may or may not be the same. User is a dictionary of profile info per the named network. All network dicts must have an id key that corresponds to the primary key in the underlying table in our own db. If claim is True, the return value is the participant_id. Otherwise it is a tuple: (participant_id, claimed [boolean], balance). """ typecheck( network, str , user_id, (int, unicode) , user_info, dict ) user_id = unicode(user_id) # Record the user info in our database. # ===================================== INSERT = """\ INSERT INTO social_network_users (network, user_id) VALUES (%s, %s) """ try: db.execute(INSERT, (network, user_id,)) except IntegrityError: pass # That login is already in our db. UPDATE = """\ UPDATE social_network_users SET user_info=%s WHERE user_id=%s RETURNING participant_id """ for k, v in user_info.items(): # I believe hstore can take any type of value, but psycopg2 can't. # https://postgres.heroku.com/blog/past/2012/3/14/introducing_keyvalue_data_storage_in_heroku_postgres/ # http://initd.org/psycopg/docs/extras.html#hstore-data-type user_info[k] = unicode(v) rec = db.fetchone(UPDATE, (user_info, user_id)) # Find a participant. # =================== if rec is not None and rec['participant_id'] is not None: # There is already a Gittip participant associated with this account. participant_id = rec['participant_id'] new_participant = False else: # This is the first time we've seen this user. Let's create a new # participant for them, claiming their user_id for them if possible. participant_id = claim_id(username) new_participant = True # Associate the social network user with the Gittip participant. # ================================================================ ASSOCIATE = """\ UPDATE social_network_users SET participant_id=%s WHERE network=%s AND user_id=%s AND ( (participant_id IS NULL) OR (participant_id=%s) ) RETURNING participant_id """ log(u"Associating %s (%s) on %s with %s on Gittip." % (username, user_id, network, participant_id)) rows = db.fetchall( ASSOCIATE , (participant_id, network, user_id, participant_id) ) nrows = len(list(rows)) assert nrows in (0, 1) if nrows == 0: # Against all odds, the account was otherwise associated with another # participant while we weren't looking. Maybe someone paid them money # at *just* the right moment. If we created a new participant then back # that out. if new_participant: db.execute( "DELETE FROM participants WHERE id=%s" , (participant_id,) ) rec = db.fetchone( "SELECT participant_id FROM social_network_users " "WHERE network=%s AND user_id=%s" , (network, user_id) ) if rec is not None: # Use the participant associated with this account. participant_id = rec['participant_id'] assert participant_id is not None else: # Okay, now this is just screwy. The participant disappeared right # at the last moment! Log it and fail. raise Exception("We're bailing on associating %s user %s (%s) with" " a Gittip participant." % (network, username, user_id)) # Record the participant as claimed if asked to. # ============================================== if claim: CLAIM = """\ UPDATE participants SET claimed_time=CURRENT_TIMESTAMP WHERE id=%s AND claimed_time IS NULL """ db.execute(CLAIM, (participant_id,)) out = participant_id else: rec = db.fetchone( "SELECT claimed_time, balance FROM participants " "WHERE id=%s" , (participant_id,) ) assert rec is not None out = (participant_id, rec['claimed_time'] is not None, rec['balance']) return out
def get_tips_and_total(tipper, for_payday=False, db=None): """Given a participant id and a date, return a list and a Decimal. This function is used to populate a participant's page for their own viewing pleasure, and also by the payday function. If for_payday is not False it must be a date object. A half-injected dependency, that's what db is. """ if db is None: from gittip import db if for_payday: # For payday we want the oldest relationship to be paid first. order_by = "ctime ASC" # This is where it gets crash-proof. # ================================== # We need to account for the fact that we may have crashed during # Payday and we're re-running that function. We only want to select # tips that existed before Payday started, but haven't been processed # as part of this Payday yet. # # It's a bug if the paydays subselect returns > 1 rows. # # XXX If we crash during Payday and we rerun it after a timezone # change, will we get burned? How? ts_filter = """\ AND mtime < %s AND ( SELECT id FROM transfers WHERE tipper=t.tipper AND tippee=t.tippee AND timestamp >= %s ) IS NULL """ args = (tipper, for_payday, for_payday) else: order_by = "amount DESC" ts_filter = "" args = (tipper,) TIPS = """\ SELECT * FROM ( SELECT DISTINCT ON (tippee) amount , tippee , t.ctime , p.claimed_time FROM tips t JOIN participants p ON p.id = t.tippee WHERE tipper = %%s AND p.is_suspicious IS NOT true %s ORDER BY tippee , t.mtime DESC ) AS foo ORDER BY %s , tippee """ % (ts_filter, order_by) # XXX, No injections here, right?! tips = list(db.fetchall(TIPS, args)) # Compute the total. # ================== # For payday we only want to process payments to tippees who have # themselves opted into Gittip. For the tipper's profile page we want to # show the total amount they've pledged (so they're not surprised when # someone *does* start accepting tips and all of a sudden they're hit with # bigger charges. if for_payday: to_total = [t for t in tips if t['claimed_time'] is not None] else: to_total = tips total = sum([t['amount'] for t in to_total]) if not total: # If to_total is an empty list then total is int 0. We want a Decimal. total = Decimal('0.00') return tips, total
def upsert(network, user_id, username, user_info, claim=False): """Given str, unicode, unicode, and dict, return unicode and boolean. Network is the name of a social network that we support (ASCII blah). User_id is an immutable unique identifier for the given user on the given social network. Username is the user's login/user_id on the given social network. We will try to claim that for them here on Gittip. If their username is already taken on Gittip then we give them a random one; they can change it on their Gittip profile page. User_id and username may or may not be the same. User is a dictionary of profile info per the named network. All network dicts must have an id key that corresponds to the primary key in the underlying table in our own db. If claim is True, the return value is the participant_id. Otherwise it is a tuple: (participant_id, claimed [boolean], balance). """ typecheck(network, str, user_id, (int, unicode), user_info, dict) user_id = unicode(user_id) # Record the user info in our database. # ===================================== INSERT = """\ INSERT INTO social_network_users (network, user_id) VALUES (%s, %s) """ try: db.execute(INSERT, ( network, user_id, )) except IntegrityError: pass # That login is already in our db. UPDATE = """\ UPDATE social_network_users SET user_info=%s WHERE user_id=%s RETURNING participant_id """ for k, v in user_info.items(): # I believe hstore can take any type of value, but psycopg2 can't. # https://postgres.heroku.com/blog/past/2012/3/14/introducing_keyvalue_data_storage_in_heroku_postgres/ # http://initd.org/psycopg/docs/extras.html#hstore-data-type user_info[k] = unicode(v) rec = db.fetchone(UPDATE, (user_info, user_id)) # Find a participant. # =================== if rec is not None and rec['participant_id'] is not None: # There is already a Gittip participant associated with this account. participant_id = rec['participant_id'] new_participant = False else: # This is the first time we've seen this user. Let's create a new # participant for them, claiming their user_id for them if possible. participant_id = claim_id(username) new_participant = True # Associate the social network user with the Gittip participant. # ================================================================ ASSOCIATE = """\ UPDATE social_network_users SET participant_id=%s WHERE network=%s AND user_id=%s AND ( (participant_id IS NULL) OR (participant_id=%s) ) RETURNING participant_id """ log(u"Associating %s (%s) on %s with %s on Gittip." % (username, user_id, network, participant_id)) rows = db.fetchall(ASSOCIATE, (participant_id, network, user_id, participant_id)) nrows = len(list(rows)) assert nrows in (0, 1) if nrows == 0: # Against all odds, the account was otherwise associated with another # participant while we weren't looking. Maybe someone paid them money # at *just* the right moment. If we created a new participant then back # that out. if new_participant: db.execute("DELETE FROM participants WHERE id=%s", (participant_id, )) rec = db.fetchone( "SELECT participant_id FROM social_network_users " "WHERE network=%s AND user_id=%s", (network, user_id)) if rec is not None: # Use the participant associated with this account. participant_id = rec['participant_id'] assert participant_id is not None else: # Okay, now this is just screwy. The participant disappeared right # at the last moment! Log it and fail. raise Exception("We're bailing on associating %s user %s (%s) with" " a Gittip participant." % (network, username, user_id)) # Record the participant as claimed if asked to. # ============================================== if claim: CLAIM = """\ UPDATE participants SET claimed_time=CURRENT_TIMESTAMP WHERE id=%s AND claimed_time IS NULL """ db.execute(CLAIM, (participant_id, )) out = participant_id else: rec = db.fetchone( "SELECT claimed_time, balance FROM participants " "WHERE id=%s", (participant_id, )) assert rec is not None out = (participant_id, rec['claimed_time'] is not None, rec['balance']) return out
def get_tips_and_total(self, for_payday=False, db=None): """Given a participant id and a date, return a list and a Decimal. This function is used by the payday function. If for_payday is not False it must be a date object. Originally we also used this function to populate the profile page, but our requirements there changed while, oddly, our requirements in payday *also* changed to match the old requirements of the profile page. So this function keeps the for_payday parameter after all. A half-injected dependency, that's what db is. """ if db is None: from gittip import db if for_payday: # For payday we want the oldest relationship to be paid first. order_by = "ctime ASC" # This is where it gets crash-proof. # ================================== # We need to account for the fact that we may have crashed during # Payday and we're re-running that function. We only want to select # tips that existed before Payday started, but haven't been # processed as part of this Payday yet. # # It's a bug if the paydays subselect returns > 1 rows. # # XXX If we crash during Payday and we rerun it after a timezone # change, will we get burned? How? ts_filter = """\ AND mtime < %s AND ( SELECT id FROM transfers WHERE tipper=t.tipper AND tippee=t.tippee AND timestamp >= %s ) IS NULL """ args = (self.username, for_payday, for_payday) else: order_by = "amount DESC" ts_filter = "" args = (self.username, ) TIPS = """\ SELECT * FROM ( SELECT DISTINCT ON (tippee) amount , tippee , t.ctime , p.claimed_time FROM tips t JOIN participants p ON p.username = t.tippee WHERE tipper = %%s AND p.is_suspicious IS NOT true %s ORDER BY tippee , t.mtime DESC ) AS foo ORDER BY %s , tippee """ % (ts_filter, order_by) # XXX, No injections here, right?! tips = list(db.fetchall(TIPS, args)) # Compute the total. # ================== # For payday we only want to process payments to tippees who have # themselves opted into Gittip. For the tipper's profile page we want # to show the total amount they've pledged (so they're not surprised # when someone *does* start accepting tips and all of a sudden they're # hit with bigger charges. if for_payday: to_total = [t for t in tips if t['claimed_time'] is not None] else: to_total = tips total = sum([t['amount'] for t in to_total]) if not total: # If to_total is an empty list, total is int 0. We want a Decimal. total = Decimal('0.00') return tips, total
def get_giving_for_profile(self, db=None): """Given a participant id and a date, return a list and a Decimal. This function is used to populate a participant's page for their own viewing pleasure. A half-injected dependency, that's what db is. """ if db is None: from gittip import db TIPS = """\ SELECT * FROM ( SELECT DISTINCT ON (tippee) amount , tippee , t.ctime , p.claimed_time , p.username_lower FROM tips t JOIN participants p ON p.username = t.tippee WHERE tipper = %s AND p.is_suspicious IS NOT true AND p.claimed_time IS NOT NULL ORDER BY tippee , t.mtime DESC ) AS foo ORDER BY amount DESC , username_lower """ tips = list(db.fetchall(TIPS, (self.username, ))) UNCLAIMED_TIPS = """\ SELECT * FROM ( SELECT DISTINCT ON (tippee) amount , tippee , t.ctime , p.claimed_time , e.platform , e.user_info FROM tips t JOIN participants p ON p.username = t.tippee JOIN elsewhere e ON e.participant = t.tippee WHERE tipper = %s AND p.is_suspicious IS NOT true AND p.claimed_time IS NULL ORDER BY tippee , t.mtime DESC ) AS foo ORDER BY amount DESC , lower(user_info->'screen_name') , lower(user_info->'username') , lower(user_info->'login') """ unclaimed_tips = list(db.fetchall(UNCLAIMED_TIPS, (self.username, ))) # Compute the total. # ================== # For payday we only want to process payments to tippees who have # themselves opted into Gittip. For the tipper's profile page we want # to show the total amount they've pledged (so they're not surprised # when someone *does* start accepting tips and all of a sudden they're # hit with bigger charges. total = sum([t['amount'] for t in tips]) if not total: # If tips is an empty list, total is int 0. We want a Decimal. total = Decimal('0.00') unclaimed_total = sum([t['amount'] for t in unclaimed_tips]) if not unclaimed_total: unclaimed_total = Decimal('0.00') return tips, total, unclaimed_tips, unclaimed_total
def upsert(network, user_id, username, user_info): """Given str, unicode, unicode, and dict, return unicode and boolean. Network is the name of a social network that we support (ASCII blah). User_id is an immutable unique identifier for the given user on the given social network. Username is the user's login/user_id on the given social network. It is only used here for logging. Specifically, we don't reserve their username for them on Gittip if they're new here. We give them a random participant_id here, and they'll have a chance to change it if/when they opt in. User_id and username may or may not be the same. User_info is a dictionary of profile info per the named network. All network dicts must have an id key that corresponds to the primary key in the underlying table in our own db. The return value is a tuple: (participant_id [unicode], is_claimed [boolean], is_locked [boolean], balance [Decimal]). """ typecheck(network, str, user_id, (int, unicode), username, unicode, user_info, dict) user_id = unicode(user_id) # Record the user info in our database. # ===================================== INSERT = """\ INSERT INTO social_network_users (network, user_id) VALUES (%s, %s) """ try: db.execute(INSERT, ( network, user_id, )) except IntegrityError: pass # That login is already in our db. UPDATE = """\ UPDATE social_network_users SET user_info=%s WHERE user_id=%s RETURNING participant_id """ for k, v in user_info.items(): # Cast everything to unicode. I believe hstore can take any type of # value, but psycopg2 can't. # https://postgres.heroku.com/blog/past/2012/3/14/introducing_keyvalue_data_storage_in_heroku_postgres/ # http://initd.org/psycopg/docs/extras.html#hstore-data-type user_info[k] = unicode(v) rec = db.fetchone(UPDATE, (user_info, user_id)) # Find a participant. # =================== if rec is not None and rec['participant_id'] is not None: # There is already a Gittip participant associated with this account. participant_id = rec['participant_id'] new_participant = False else: # This is the first time we've seen this user. Let's create a new # participant for them. participant_id = get_a_participant_id() new_participant = True # Associate the social network user with the Gittip participant. # ================================================================ ASSOCIATE = """\ UPDATE social_network_users SET participant_id=%s WHERE network=%s AND user_id=%s AND ( (participant_id IS NULL) OR (participant_id=%s) ) RETURNING participant_id, is_locked """ log(u"Associating %s (%s) on %s with %s on Gittip." % (username, user_id, network, participant_id)) rows = db.fetchall(ASSOCIATE, (participant_id, network, user_id, participant_id)) rows = list(rows) nrows = len(rows) assert nrows in (0, 1) if nrows == 1: is_locked = rows[0]['is_locked'] else: # Against all odds, the account was otherwise associated with another # participant while we weren't looking. Maybe someone paid them money # at *just* the right moment. If we created a new participant then back # that out. if new_participant: db.execute("DELETE FROM participants WHERE id=%s", (participant_id, )) rec = db.fetchone( "SELECT participant_id, is_locked " "FROM social_network_users " "WHERE network=%s AND user_id=%s", (network, user_id)) if rec is not None: # Use the participant associated with this account. participant_id = rec['participant_id'] is_locked = rec['is_locked'] assert participant_id is not None else: # Okay, now this is just screwy. The participant disappeared right # at the last moment! Log it and fail. raise Exception("We're bailing on associating %s user %s (%s) with" " a Gittip participant." % (network, username, user_id)) rec = db.fetchone( "SELECT claimed_time, balance FROM participants " "WHERE id=%s", (participant_id, )) assert rec is not None return (participant_id, rec['claimed_time'] is not None, is_locked, rec['balance'])
def upsert(network, user_id, username, user_info): """Given str, unicode, unicode, and dict, return unicode and boolean. Network is the name of a social network that we support (ASCII blah). User_id is an immutable unique identifier for the given user on the given social network. Username is the user's login/user_id on the given social network. It is only used here for logging. Specifically, we don't reserve their username for them on Gittip if they're new here. We give them a random participant_id here, and they'll have a chance to change it if/when they opt in. User_id and username may or may not be the same. User_info is a dictionary of profile info per the named network. All network dicts must have an id key that corresponds to the primary key in the underlying table in our own db. The return value is a tuple: (participant_id [unicode], is_claimed [boolean], is_locked [boolean], balance [Decimal]). """ typecheck( network, str , user_id, (int, unicode) , username, unicode , user_info, dict ) user_id = unicode(user_id) # Record the user info in our database. # ===================================== INSERT = """\ INSERT INTO social_network_users (network, user_id) VALUES (%s, %s) """ try: db.execute(INSERT, (network, user_id,)) except IntegrityError: pass # That login is already in our db. UPDATE = """\ UPDATE social_network_users SET user_info=%s WHERE user_id=%s RETURNING participant_id """ for k, v in user_info.items(): # Cast everything to unicode. I believe hstore can take any type of # value, but psycopg2 can't. # https://postgres.heroku.com/blog/past/2012/3/14/introducing_keyvalue_data_storage_in_heroku_postgres/ # http://initd.org/psycopg/docs/extras.html#hstore-data-type user_info[k] = unicode(v) rec = db.fetchone(UPDATE, (user_info, user_id)) # Find a participant. # =================== if rec is not None and rec['participant_id'] is not None: # There is already a Gittip participant associated with this account. participant_id = rec['participant_id'] new_participant = False else: # This is the first time we've seen this user. Let's create a new # participant for them. participant_id = get_a_participant_id() new_participant = True # Associate the social network user with the Gittip participant. # ================================================================ ASSOCIATE = """\ UPDATE social_network_users SET participant_id=%s WHERE network=%s AND user_id=%s AND ( (participant_id IS NULL) OR (participant_id=%s) ) RETURNING participant_id, is_locked """ log(u"Associating %s (%s) on %s with %s on Gittip." % (username, user_id, network, participant_id)) rows = db.fetchall( ASSOCIATE , (participant_id, network, user_id, participant_id) ) rows = list(rows) nrows = len(rows) assert nrows in (0, 1) if nrows == 1: is_locked = rows[0]['is_locked'] else: # Against all odds, the account was otherwise associated with another # participant while we weren't looking. Maybe someone paid them money # at *just* the right moment. If we created a new participant then back # that out. if new_participant: db.execute( "DELETE FROM participants WHERE id=%s" , (participant_id,) ) rec = db.fetchone( "SELECT participant_id, is_locked " "FROM social_network_users " "WHERE network=%s AND user_id=%s" , (network, user_id) ) if rec is not None: # Use the participant associated with this account. participant_id = rec['participant_id'] is_locked = rec['is_locked'] assert participant_id is not None else: # Okay, now this is just screwy. The participant disappeared right # at the last moment! Log it and fail. raise Exception("We're bailing on associating %s user %s (%s) with" " a Gittip participant." % (network, username, user_id)) rec = db.fetchone( "SELECT claimed_time, balance FROM participants " "WHERE id=%s" , (participant_id,) ) assert rec is not None return ( participant_id , rec['claimed_time'] is not None , is_locked , rec['balance'] )
def payday(): """This is the big one. Settling the graph of Gittip balances is an abstract event called Payday. On Payday, we want to use a participant's Gittip balance to settle their tips due (pulling in more money via credit card as needed), but we only want to use their balance at the start of Payday. Balance changes should be atomic globally per-Payday. This function runs every Friday. It is structured such that it can be run again safely if it crashes. """ log("Greetings, program! It's PAYDAY!!!!") # Start Payday. # ============= # We try to start a new Payday. If there is a Payday that hasn't finished # yet, then the UNIQUE constraint on ts_end will kick in and notify us # of that. In that case we load the existing Payday and work on it some # more. We use the start time of the current Payday to synchronize our # work. try: rec = db.fetchone("INSERT INTO paydays DEFAULT VALUES " "RETURNING ts_start") log("Starting a new payday.") except IntegrityError: # Collision, we have a Payday already. rec = db.fetchone("SELECT ts_start FROM paydays WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz") log("Picking up with an existing payday.") assert rec is not None # Must either create or recycle a Payday. payday_start = rec['ts_start'] log("Payday started at %s." % payday_start) START_PENDING = """\ UPDATE participants SET pending=0.00 WHERE pending IS NULL """ db.execute(START_PENDING) log("Zeroed out the pending column.") PARTICIPANTS = """\ SELECT id, balance, stripe_customer_id FROM participants WHERE claimed_time IS NOT NULL """ participants = db.fetchall(PARTICIPANTS) log("Fetched participants.") # Drop to core. # ============= # We are now locked for Payday. If the power goes out at this point then we # will need to start over and reacquire the lock. payday_loop(payday_start, participants) # Finish Payday. # ============== # Transfer pending into balance for all users, setting pending to NULL. # Close out the paydays entry as well. with db.get_connection() as conn: cursor = conn.cursor() cursor.execute("""\ UPDATE participants SET balance = (balance + pending) , pending = NULL """) cursor.execute("""\ UPDATE paydays SET ts_end=now() WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz RETURNING id """) assert_one_payday(cursor.fetchone()) conn.commit() log("Finished payday.")
def payday(): """This is the big one. Settling the graph of Gittip balances is an abstract event called Payday. On Payday, we want to use a participant's Gittip balance to settle their tips due (pulling in more money via credit card as needed), but we only want to use their balance at the start of Payday. Balance changes should be atomic globally per-Payday. This function runs every Friday. It is structured such that it can be run again safely if it crashes. """ log("Greetings, program! It's PAYDAY!!!!") # Start Payday. # ============= # We try to start a new Payday. If there is a Payday that hasn't finished # yet, then the UNIQUE constraint on ts_end will kick in and notify us # of that. In that case we load the existing Payday and work on it some # more. We use the start time of the current Payday to synchronize our # work. try: rec = db.fetchone("INSERT INTO paydays DEFAULT VALUES " "RETURNING ts_start") log("Starting a new payday.") except IntegrityError: # Collision, we have a Payday already. rec = db.fetchone( "SELECT ts_start FROM paydays WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz" ) log("Picking up with an existing payday.") assert rec is not None # Must either create or recycle a Payday. payday_start = rec['ts_start'] log("Payday started at %s." % payday_start) START_PENDING = """\ UPDATE participants SET pending=0.00 WHERE pending IS NULL """ db.execute(START_PENDING) log("Zeroed out the pending column.") PARTICIPANTS = """\ SELECT id, balance, payment_method_token AS pmt FROM participants """ participants = db.fetchall(PARTICIPANTS) log("Fetched participants.") # Drop to core. # ============= # We are now locked for Payday. If the power goes out at this point then we # will need to start over and reacquire the lock. payday_loop(payday_start, participants) # Finish Payday. # ============== # Transfer pending into balance for all users, setting pending to NULL. # Close out the paydays entry as well. with db.get_connection() as conn: cursor = conn.cursor() cursor.execute("""\ UPDATE participants SET balance = (balance + pending) , pending = NULL """) cursor.execute("""\ UPDATE paydays SET ts_end=now() WHERE ts_end='1970-01-01T00:00:00+00'::timestamptz RETURNING id """) assert_one_payday(cursor.fetchone()) conn.commit() log("Finished payday.")