示例#1
0
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, )))
示例#2
0
    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
示例#3
0
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
示例#4
0
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,)))
示例#5
0
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
示例#6
0
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
示例#7
0
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
示例#8
0
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
示例#9
0
    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
示例#10
0
    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
示例#11
0
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'])
示例#12
0
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']
            )
示例#13
0
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.")
示例#14
0
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.")