From 98087f105f797ec27c6ecb9e60a30eb82df00f80 Mon Sep 17 00:00:00 2001 From: Joonas Bergius Date: Fri, 11 Jan 2013 03:42:23 -0600 Subject: [PATCH] Add models for each table in the database, plus User. Relates to #446 Basically, I went ahead and reconstructed each table in the database as a mapping class. In addition, I recreated the User class that subclassed Participant class previously. There is still clean up that needs to be done here. Signed-off-by: Joonas Bergius --- gittip/models/__init__.py | 10 +++ gittip/models/absorption.py | 18 +++++ gittip/models/elsewhere.py | 30 +++++++++ gittip/models/exchange.py | 16 +++++ gittip/models/participant.py | 123 +++++++++++++++++++++++++++++++++++ gittip/models/payday.py | 36 ++++++++++ gittip/models/tip.py | 16 +++++ gittip/models/transfer.py | 16 +++++ gittip/models/user.py | 41 ++++++++++++ 9 files changed, 306 insertions(+) create mode 100644 gittip/models/__init__.py create mode 100644 gittip/models/absorption.py create mode 100644 gittip/models/elsewhere.py create mode 100644 gittip/models/exchange.py create mode 100644 gittip/models/participant.py create mode 100644 gittip/models/payday.py create mode 100644 gittip/models/tip.py create mode 100644 gittip/models/transfer.py create mode 100644 gittip/models/user.py diff --git a/gittip/models/__init__.py b/gittip/models/__init__.py new file mode 100644 index 0000000000..56b6e63123 --- /dev/null +++ b/gittip/models/__init__.py @@ -0,0 +1,10 @@ +from gittip.models.absorption import Absorption +from gittip.models.elsewhere import Elsewhere +from gittip.models.exchange import Exchange +from gittip.models.participant import Participant +from gittip.models.payday import Payday +from gittip.models.tip import Tip +from gittip.models.transfer import Transfer +from gittip.models.user import User + +all = [Elsewhere, Exchange, Participant, Payday, Tip, Transfer, User] \ No newline at end of file diff --git a/gittip/models/absorption.py b/gittip/models/absorption.py new file mode 100644 index 0000000000..3cb4093a18 --- /dev/null +++ b/gittip/models/absorption.py @@ -0,0 +1,18 @@ +from sqlalchemy.schema import Column, ForeignKey +from sqlalchemy.types import Integer, Text, TIMESTAMP + +from gittip.orm import Base + +class Absorption(Base): + __tablename__ = 'absorptions' + + id = Column(Integer, nullable=False, primary_key=True) + timestamp = Column(TIMESTAMP(timezone=True), nullable=False, + default="now()") + absorbed_was = Column(Text, nullable=False) + absorbed_by = Column(Text, ForeignKey("participants.id", + onupdate="CASCADE", + ondelete="RESTRICT"), nullable=False) + archived_as = Column(Text, ForeignKey("participants.id", + onupdate="RESTRICT", + ondelete="RESTRICT"), nullable=False) \ No newline at end of file diff --git a/gittip/models/elsewhere.py b/gittip/models/elsewhere.py new file mode 100644 index 0000000000..956e030e7e --- /dev/null +++ b/gittip/models/elsewhere.py @@ -0,0 +1,30 @@ +from sqlalchemy.dialects.postgresql.hstore import HSTORE +from sqlalchemy.schema import Column, UniqueConstraint, ForeignKey +from sqlalchemy.types import Integer, Text, Boolean + +from gittip.orm import Base + +class Elsewhere(Base): + __tablename__ = 'elsewhere' + __table_args__ = ( + UniqueConstraint('platform', 'participant_id', + name='elsewhere_platform_participant_id_key'), + UniqueConstraint('platform', 'user_id', + name='elsewhere_platform_user_id_key') + ) + + id = Column(Integer, nullable=False, primary_key=True) + platform = Column(Text, nullable=False) + user_id = Column(Text, nullable=False) + user_info = Column(HSTORE) + is_locked = Column(Boolean, default=False, nullable=False) + participant_id = Column(Text, ForeignKey("participants.id"), nullable=False) + + def resolve_unclaimed(self): + if self.platform == 'github': + out = '/on/github/%s/' % self.user_info['login'] + elif self.platform == 'twitter': + out = '/on/twitter/%s/' % self.user_info['screen_name'] + else: + out = None + return out \ No newline at end of file diff --git a/gittip/models/exchange.py b/gittip/models/exchange.py new file mode 100644 index 0000000000..a208c0d5ce --- /dev/null +++ b/gittip/models/exchange.py @@ -0,0 +1,16 @@ +from sqlalchemy.schema import Column, ForeignKey +from sqlalchemy.types import Integer, Numeric, Text, TIMESTAMP + +from gittip.orm import Base + +class Exchange(Base): + __tablename__ = 'exchanges' + + id = Column(Integer, nullable=False, primary_key=True) + timestamp = Column(TIMESTAMP(timezone=True), nullable=False, + default="now()") + amount = Column(Numeric(precision=35, scale=2), nullable=False) + fee = Column(Numeric(precision=35, scale=2), nullable=False) + participant_id = Column(Text, ForeignKey("participants.id", + onupdate="CASCADE", ondelete="RESTRICT"), + nullable=False) \ No newline at end of file diff --git a/gittip/models/participant.py b/gittip/models/participant.py new file mode 100644 index 0000000000..c5ce46323f --- /dev/null +++ b/gittip/models/participant.py @@ -0,0 +1,123 @@ +import datetime +from decimal import Decimal + +import pytz +from sqlalchemy import select, func +from sqlalchemy.exc import IntegrityError +from sqlalchemy.orm import relationship +from sqlalchemy.schema import Column, CheckConstraint, UniqueConstraint +from sqlalchemy.types import Text, TIMESTAMP, Boolean, Numeric +from aspen import Response + +import gittip +from gittip.orm import Base, db +from gittip.models import Elsewhere +# This is loaded for now to maintain functionality until the class is fully +# migrated over to doing everything using SQLAlchemy +from gittip.participant import Participant as ParticipantClass + +ASCII_ALLOWED_IN_PARTICIPANT_ID = set("0123456789" + "abcdefghijklmnopqrstuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + ".,-_;:@ ") + +class Participant(Base): + __tablename__ = "participants" + __table_args__ = ( + UniqueConstraint("session_token", + name="participants_session_token_key"), + ) + + id = Column(Text, nullable=False, primary_key=True) + statement = Column(Text, nullable=False) + stripe_customer_id = Column(Text) + last_bill_result = Column(Text) + session_token = Column(Text) + session_expires = Column(TIMESTAMP(timezone=True), default="now()") + ctime = Column(TIMESTAMP(timezone=True), nullable=False, default="now()") + claimed_time = Column(TIMESTAMP(timezone=True)) + is_admin = Column(Boolean, nullable=False, default=False) + balance = Column(Numeric(precision=35, scale=2), + CheckConstraint("balance >= 0", name="min_balance"), + default=0.0, nullable=False) + pending = Column(Numeric(precision=35, scale=2), default=None) + anonymous = Column(Boolean, default=False, nullable=False) + goal = Column(Numeric(precision=35, scale=2), default=None) + balanced_account_uri = Column(Text) + last_ach_result = Column(Text) + is_suspicious = Column(Boolean) + + ### Relations ### + accounts_elsewhere = relationship("Elsewhere", backref="participant", + lazy="dynamic") + exchanges = relationship("Exchange", backref="participant") + # TODO: Once tippee/tipper are renamed to tippee_id/tipper_idd, we can go + # ahead and drop the foreign_keys & rename backrefs to tipper/tippee + tipper_in = relationship("Tip", backref="tipper_participant", + foreign_keys="Tip.tipper", lazy="dynamic") + tippee_in = relationship("Tip", backref="tippee_participant", + foreign_keys="Tip.tippee", lazy="dynamic") + transferer = relationship("Transfer", backref="transferer", + foreign_keys="Transfer.tipper") + trasnferee = relationship("Transfer", backref="transferee", + foreign_keys="Transfer.tippee") + + def resolve_unclaimed(self): + if self.accounts_elsewhere: + return self.accounts_elsewhere[0].resolve_unclaimed() + else: + return None + + def set_as_claimed(self): + self.claimed_time = datetime.datetime.now(pytz.utc) + self.save() + + def change_id(self, desired_id): + ParticipantClass(self.id).change_id(desired_id) + + def get_accounts_elsewhere(self): + github_account = twitter_account = None + for account in self.accounts_elsewhere.all(): + if account.platform == "github": + github_account = account + elif account.platform == "twitter": + twitter_account = account + return (github_account, twitter_account) + + def get_giving_for_profile(self): + return ParticipantClass(self.id).get_giving_for_profile() + + def get_tip_to(self, tippee): + tip = self.tipper_in.filter_by(tippee=tippee).first() + + if tip: + amount = tip.amount + else: + amount = Decimal('0.0') + + return amount + + @property + def dollars_giving(self): + return sum(tip.amount for tip in self.tipper_in) + + @property + def dollars_receiving(self): + return sum(tip.amount for tip in self.tippee_in) + + def get_number_of_backers(self): + return ParticipantClass(self.id).get_number_of_backers() + + def get_chart_of_receiving(self): + # TODO: Move the query in to this class. + return ParticipantClass(self.id).get_chart_of_receiving() + + def get_giving_for_profile(self, db=None): + return ParticipantClass(self.id).get_giving_for_profile(db) + + def get_tips_and_total(self, for_payday=False, db=None): + return ParticipantClass(self.id).get_tips_and_total(for_payday, db) + + def take_over(self, account_elsewhere, have_confirmation=False): + ParticipantClass(self.id).take_over(account_elsewhere, + have_confirmation) \ No newline at end of file diff --git a/gittip/models/payday.py b/gittip/models/payday.py new file mode 100644 index 0000000000..cd9d3f5a18 --- /dev/null +++ b/gittip/models/payday.py @@ -0,0 +1,36 @@ +import datetime + +import pytz +from sqlalchemy.schema import Column, UniqueConstraint +from sqlalchemy.types import Integer, Numeric, Text, TIMESTAMP + +from gittip.orm import Base + +class Payday(Base): + __tablename__ = 'payday' + __table_args__ = ( + UniqueConstraint('ts_end', name='paydays_ts_end_key'), + ) + + # TODO: Move this to a different module? + EPOCH = datetime.datetime(1970, 1, 1, tzinfo=pytz.utc) + + id = Column(Integer, nullable=False, primary_key=True) + ts_start = Column(TIMESTAMP(timezone=True), nullable=False, + default="now()") + ts_end = Column(TIMESTAMP(timezone=True), nullable=False, + default=EPOCH) + nparticipants = Column(Integer, default=0) + ntippers = Column(Integer, default=0) + ntips = Column(Integer, default=0) + ntransfers = Column(Integer, default=0) + transfer_volume = Column(Numeric(precision=35, scale=2), default=0.0) + ncc_failing = Column(Integer, default=0) + ncc_missing = Column(Integer, default=0) + ncharges = Column(Integer, default=0) + charge_volume = Column(Numeric(precision=35, scale=2), default=0.0) + charge_fees_volume = Column(Numeric(precision=35, scale=2), default=0.0) + nachs = Column(Integer, default=0) + ach_volume = Column(Numeric(precision=35, scale=2), default=0.0) + ach_fees_volume = Column(Numeric(precision=35, scale=2), default=0.0) + nach_failing = Column(Integer, default=0) \ No newline at end of file diff --git a/gittip/models/tip.py b/gittip/models/tip.py new file mode 100644 index 0000000000..a94b37c065 --- /dev/null +++ b/gittip/models/tip.py @@ -0,0 +1,16 @@ +from sqlalchemy.schema import Column, ForeignKey +from sqlalchemy.types import Integer, Numeric, Text, TIMESTAMP + +from gittip.orm import Base + +class Tip(Base): + __tablename__ = 'tips' + + id = Column(Integer, nullable=False, primary_key=True) + ctime = Column(TIMESTAMP(timezone=True), nullable=False) + mtime = Column(TIMESTAMP(timezone=True), nullable=False, default="now()") + tipper = Column(Text, ForeignKey("participants.id", onupdate="CASCADE", + ondelete="RESTRICT"), nullable=False) + tippee = Column(Text, ForeignKey("participants.id", onupdate="CASCADE", + ondelete="RESTRICT"), nullable=False) + amount = Column(Numeric(precision=35, scale=2), nullable=False) \ No newline at end of file diff --git a/gittip/models/transfer.py b/gittip/models/transfer.py new file mode 100644 index 0000000000..90414e7012 --- /dev/null +++ b/gittip/models/transfer.py @@ -0,0 +1,16 @@ +from sqlalchemy.schema import Column, ForeignKey +from sqlalchemy.types import Integer, Numeric, Text, TIMESTAMP + +from gittip.orm import Base + +class Transfer(Base): + __tablename__ = 'transfers' + + id = Column(Integer, nullable=False, primary_key=True) + timestamp = Column(TIMESTAMP(timezone=True), nullable=False, + default="now()") + tipper = Column(Text, ForeignKey("participants.id", onupdate="CASCADE", + ondelete="RESTRICT"), nullable=False) + tippee = Column(Text, ForeignKey("participants.id", onupdate="CASCADE", + ondelete="RESTRICT"), nullable=False) + amount = Column(Numeric(precision=35, scale=2), nullable=False) \ No newline at end of file diff --git a/gittip/models/user.py b/gittip/models/user.py new file mode 100644 index 0000000000..3b0087df44 --- /dev/null +++ b/gittip/models/user.py @@ -0,0 +1,41 @@ +import uuid + +from gittip.models.participant import Participant + +class User(Participant): + """Represent a website user. + + Every current website user is also a participant, though if the user is + anonymous then the methods from Participant will fail with NoParticipantId. + """ + + @classmethod + def from_session_token(cls, token): + user = User.query.filter_by(session_token=token).first() + if user and not user.is_suspicious: + user = user + else: + user = User() + return user + + @classmethod + def from_id(cls, user_id): + user = User.query.filter_by(id=user_id).first() + if user and not user.is_suspicious: + user.session_token = uuid.uuid4().hex + user.save() + # attrs = participant.attrs_dict() + else: + user = User() + return user + + @property + def ADMIN(self): + return self.id is not None and self.is_admin + + @property + def ANON(self): + return self.id is None + + def __unicode__(self): + return '' % getattr(self, 'id', 'Anonymous') \ No newline at end of file