import datetime from typing import List from sqlalchemy import func from sqlalchemy.schema import Sequence from grant.comment.models import Comment from grant.extensions import ma, db from grant.utils.misc import dt_to_unix from grant.utils.exceptions import ValidationException from grant.blockchain import blockchainGet # Proposal states DRAFT = 'DRAFT' PENDING = 'PENDING' LIVE = 'LIVE' DELETED = 'DELETED' STATUSES = [DRAFT, PENDING, LIVE, DELETED] # Funding stages FUNDING_REQUIRED = 'FUNDING_REQUIRED' COMPLETED = 'COMPLETED' PROPOSAL_STAGES = [FUNDING_REQUIRED, COMPLETED] # Proposal categories DAPP = "DAPP" DEV_TOOL = "DEV_TOOL" CORE_DEV = "CORE_DEV" COMMUNITY = "COMMUNITY" DOCUMENTATION = "DOCUMENTATION" ACCESSIBILITY = "ACCESSIBILITY" CATEGORIES = [DAPP, DEV_TOOL, CORE_DEV, COMMUNITY, DOCUMENTATION, ACCESSIBILITY] # Contribution states # PENDING = 'PENDING' CONFIRMED = 'CONFIRMED' proposal_team = db.Table( 'proposal_team', db.Model.metadata, db.Column('user_id', db.Integer, db.ForeignKey('user.id')), db.Column('proposal_id', db.Integer, db.ForeignKey('proposal.id')) ) class ProposalTeamInvite(db.Model): __tablename__ = "proposal_team_invite" id = db.Column(db.Integer(), primary_key=True) date_created = db.Column(db.DateTime) proposal_id = db.Column(db.Integer, db.ForeignKey("proposal.id"), nullable=False) address = db.Column(db.String(255), nullable=False) accepted = db.Column(db.Boolean) def __init__(self, proposal_id: int, address: str, accepted: bool = None): self.proposal_id = proposal_id self.address = address self.accepted = accepted self.date_created = datetime.datetime.now() @staticmethod def get_pending_for_user(user): return ProposalTeamInvite.query.filter( ProposalTeamInvite.accepted == None, (func.lower(user.email_address) == func.lower(ProposalTeamInvite.address)) ).all() class ProposalUpdate(db.Model): __tablename__ = "proposal_update" id = db.Column(db.Integer(), primary_key=True) date_created = db.Column(db.DateTime) proposal_id = db.Column(db.Integer, db.ForeignKey("proposal.id"), nullable=False) title = db.Column(db.String(255), nullable=False) content = db.Column(db.Text, nullable=False) def __init__(self, proposal_id: int, title: str, content: str): self.proposal_id = proposal_id self.title = title self.content = content self.date_created = datetime.datetime.now() class ProposalContribution(db.Model): __tablename__ = "proposal_contribution" # Manual sequence due to migration # TODO: Remove manual sequence next time we wipe db sequence = Sequence('proposal_contribution_sequence') id = db.Column(db.Integer(), sequence, server_default=sequence.next_value(), primary_key=True) date_created = db.Column(db.DateTime, nullable=False) proposal_id = db.Column(db.Integer, db.ForeignKey("proposal.id"), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True) status = db.Column(db.String(255), nullable=False) amount = db.Column(db.String(255), nullable=False) tx_id = db.Column(db.String(255)) def __init__( self, proposal_id: int, user_id: int, amount: str ): self.proposal_id = proposal_id self.user_id = user_id self.amount = amount self.date_created = datetime.datetime.now() self.status = PENDING @staticmethod def getByUserAndProposal(user_id: int, proposal_id: int): return ProposalContribution.query \ .filter_by(user_id=user_id, proposal_id=proposal_id) \ .first() def confirm(self, tx_id: str, amount: str): self.status = CONFIRMED self.tx_id = tx_id self.amount = amount class Proposal(db.Model): __tablename__ = "proposal" id = db.Column(db.Integer(), primary_key=True) date_created = db.Column(db.DateTime) # Content info status = db.Column(db.String(255), nullable=False) title = db.Column(db.String(255), nullable=False) brief = db.Column(db.String(255), nullable=False) stage = db.Column(db.String(255), nullable=False) content = db.Column(db.Text, nullable=False) category = db.Column(db.String(255), nullable=False) # Payment info target = db.Column(db.String(255), nullable=False) payout_address = db.Column(db.String(255), nullable=False) deadline_duration = db.Column(db.Integer(), nullable=False) # Relations team = db.relationship("User", secondary=proposal_team) comments = db.relationship(Comment, backref="proposal", lazy=True, cascade="all, delete-orphan") updates = db.relationship(ProposalUpdate, backref="proposal", lazy=True, cascade="all, delete-orphan") contributions = db.relationship(ProposalContribution, backref="proposal", lazy=True, cascade="all, delete-orphan") milestones = db.relationship("Milestone", backref="proposal", lazy=True, cascade="all, delete-orphan") invites = db.relationship(ProposalTeamInvite, backref="proposal", lazy=True, cascade="all, delete-orphan") def __init__( self, status: str = 'DRAFT', title: str = '', brief: str = '', content: str = '', stage: str = '', target: str = '0', payout_address: str = '', deadline_duration: int = 5184000, # 60 days category: str = '' ): self.date_created = datetime.datetime.now() self.status = status self.title = title self.brief = brief self.content = content self.category = category self.target = target self.payout_address = payout_address self.deadline_duration = deadline_duration self.stage = stage @staticmethod def validate(proposal): title = proposal.get('title') stage = proposal.get('stage') category = proposal.get('category') if title and len(title) > 60: raise ValidationException("Proposal title cannot be longer than 60 characters") if stage and stage not in PROPOSAL_STAGES: raise ValidationException("Proposal stage {} not in {}".format(stage, PROPOSAL_STAGES)) if category and category not in CATEGORIES: raise ValidationException("Category {} not in {}".format(category, CATEGORIES)) @staticmethod def create(**kwargs): Proposal.validate(kwargs) return Proposal( **kwargs ) @staticmethod def get_by_user(user): return Proposal.query \ .join(proposal_team) \ .filter(proposal_team.c.user_id == user.id) \ .all() @staticmethod def get_by_user_contribution(user): return Proposal.query \ .join(ProposalContribution) \ .filter(ProposalContribution.user_id == user.id) \ .order_by(ProposalContribution.date_created.desc()) \ .all() def update( self, title: str = '', brief: str = '', category: str = '', content: str = '', target: str = '0', payout_address: str = '', deadline_duration: int = 5184000 # 60 days ): self.title = title self.brief = brief self.category = category self.content = content self.target = target self.payout_address = payout_address self.deadline_duration = deadline_duration Proposal.validate(vars(self)) def publish(self): # Require certain fields # TODO: I'm an idiot, make this a loop. if not self.title: raise ValidationException("Proposal must have a title") if not self.content: raise ValidationException("Proposal must have content") if not self.brief: raise ValidationException("Proposal must have a brief") if not self.category: raise ValidationException("Proposal must have a category") if not self.target: raise ValidationException("Proposal must have a target amount") if not self.payout_address: raise ValidationException("Proposal must have a payout address") # Then run through regular validation Proposal.validate(vars(self)) self.status = 'LIVE' class ProposalSchema(ma.Schema): class Meta: model = Proposal # Fields to expose fields = ( "stage", "date_created", "title", "brief", "proposal_id", "target", "funded", "content", "comments", "updates", "contributions", "milestones", "category", "team", "payout_address", "deadline_duration", "invites" ) date_created = ma.Method("get_date_created") proposal_id = ma.Method("get_proposal_id") funded = ma.Method("get_funded") comments = ma.Nested("CommentSchema", many=True) updates = ma.Nested("ProposalUpdateSchema", many=True) contributions = ma.Nested("ProposalContributionSchema", many=True, exclude=['proposal']) team = ma.Nested("UserSchema", many=True) milestones = ma.Nested("MilestoneSchema", many=True) invites = ma.Nested("ProposalTeamInviteSchema", many=True) def get_proposal_id(self, obj): return obj.id def get_date_created(self, obj): return dt_to_unix(obj.date_created) def get_funded(self, obj): # TODO: Add up all contributions and return that return "0" proposal_schema = ProposalSchema() proposals_schema = ProposalSchema(many=True) class ProposalUpdateSchema(ma.Schema): class Meta: model = ProposalUpdate # Fields to expose fields = ( "update_id", "date_created", "proposal_id", "title", "content" ) date_created = ma.Method("get_date_created") proposal_id = ma.Method("get_proposal_id") update_id = ma.Method("get_update_id") def get_update_id(self, obj): return obj.id def get_proposal_id(self, obj): return obj.proposal_id def get_date_created(self, obj): return dt_to_unix(obj.date_created) proposal_update_schema = ProposalUpdateSchema() proposals_update_schema = ProposalUpdateSchema(many=True) class ProposalTeamInviteSchema(ma.Schema): class Meta: model = ProposalTeamInvite fields = ( "id", "date_created", "address", "accepted" ) date_created = ma.Method("get_date_created") def get_date_created(self, obj): return dt_to_unix(obj.date_created) proposal_team_invite_schema = ProposalTeamInviteSchema() proposal_team_invites_schema = ProposalTeamInviteSchema(many=True) # TODO: Find a way to extend ProposalTeamInviteSchema instead of redefining class InviteWithProposalSchema(ma.Schema): class Meta: model = ProposalTeamInvite fields = ( "id", "date_created", "address", "accepted", "proposal" ) date_created = ma.Method("get_date_created") proposal = ma.Nested("ProposalSchema") def get_date_created(self, obj): return dt_to_unix(obj.date_created) invite_with_proposal_schema = InviteWithProposalSchema() invites_with_proposal_schema = InviteWithProposalSchema(many=True) class ProposalContributionSchema(ma.Schema): class Meta: model = ProposalContribution # Fields to expose fields = ( "id", "proposal", "user", "tx_id", "amount", "date_created", "addresses", ) proposal = ma.Nested("ProposalSchema") user = ma.Nested("UserSchema") date_created = ma.Method("get_date_created") addresses = ma.Method("get_addresses") def get_date_created(self, obj): return dt_to_unix(obj.date_created) def get_addresses(self, obj): return blockchainGet('/contribution/addresses', { 'contributionId': obj.id }) proposal_contribution_schema = ProposalContributionSchema() proposals_contribution_schema = ProposalContributionSchema(many=True) class UserProposalSchema(ma.Schema): class Meta: model = Proposal # Fields to expose fields = ( "proposal_id", "title", "brief", "date_created", "team", ) date_created = ma.Method("get_date_created") proposal_id = ma.Method("get_proposal_id") team = ma.Nested("UserSchema", many=True) def get_proposal_id(self, obj): return obj.id def get_date_created(self, obj): return dt_to_unix(obj.date_created) * 1000 user_proposal_schema = UserProposalSchema() user_proposals_schema = UserProposalSchema(many=True)