class TagAssociation(db.Model): __tablename__ = "tag_association" id = db.Column(db.Integer(), primary_key=True) rfw_id = db.Column(db.Integer, db.ForeignKey('rfw.id')) # can add more parent ids so different types can refer to same tags, ex: # proposal_id = db.Column(db.Integer, db.ForeignKey('proposal.id')) tag_id = db.Column(db.Integer, db.ForeignKey('tag.id'))
class User(db.Model): __tablename__ = "user" id = db.Column(db.Integer(), primary_key=True) email_address = db.Column(db.String(255), unique=True, nullable=True) account_address = db.Column(db.String(255), unique=True, nullable=True) display_name = db.Column(db.String(255), unique=False, nullable=True) title = db.Column(db.String(255), unique=False, nullable=True) social_medias = db.relationship(SocialMedia, backref="user", lazy=True) comments = db.relationship(Comment, backref="user", lazy=True) avatar = db.relationship(Avatar, uselist=False, back_populates="user") email_verification = db.relationship(EmailVerification, uselist=False, back_populates="user", lazy=True) # TODO - add create and validate methods def __init__(self, email_address=None, account_address=None, display_name=None, title=None): if not email_address and not account_address: raise ValueError("Either email_address or account_address is required to create a user") self.email_address = email_address self.account_address = account_address self.display_name = display_name self.title = title @staticmethod def create(email_address=None, account_address=None, display_name=None, title=None, _send_email=True): user = User( account_address=account_address, email_address=email_address, display_name=display_name, title=title ) db.session.add(user) db.session.flush() # Setup & send email verification ev = EmailVerification(user_id=user.id) db.session.add(ev) db.session.commit() if send_email: send_email(user.email_address, 'signup', { 'display_name': user.display_name, 'confirm_url': make_url(f'/email/verify?code={ev.code}') }) return user @staticmethod def get_by_identifier(email_address: str = None, account_address: str = None): if not email_address and not account_address: raise ValueError("Either email_address or account_address is required to get a user") return User.query.filter( (func.lower(User.account_address) == func.lower(account_address)) | (func.lower(User.email_address) == func.lower(email_address)) ).first()
class Milestone(db.Model): __tablename__ = "milestone" id = db.Column(db.Integer(), primary_key=True) date_created = db.Column(db.DateTime, nullable=False) title = db.Column(db.String(255), nullable=False) content = db.Column(db.Text, nullable=False) stage = db.Column(db.String(255), nullable=False) payout_percent = db.Column(db.String(255), nullable=False) immediate_payout = db.Column(db.Boolean) date_estimated = db.Column(db.DateTime, nullable=False) proposal_id = db.Column(db.Integer, db.ForeignKey("proposal.id"), nullable=False) def __init__( self, title: str, content: str, date_estimated: datetime, payout_percent: str, immediate_payout: bool, stage: str = NOT_REQUESTED, proposal_id=int ): self.title = title self.content = content self.stage = stage self.date_estimated = date_estimated self.payout_percent = payout_percent self.immediate_payout = immediate_payout self.proposal_id = proposal_id self.date_created = datetime.datetime.now()
class SocialMedia(db.Model): __tablename__ = "social_media" id = db.Column(db.Integer(), primary_key=True) # TODO replace this with something proper social_media_link = db.Column(db.String(255), unique=False, nullable=True) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) def __init__(self, social_media_link, user_id): self.social_media_link = social_media_link self.user_id = user_id
class Avatar(db.Model): __tablename__ = "avatar" id = db.Column(db.Integer(), primary_key=True) image_url = db.Column(db.String(255), unique=False, nullable=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) user = db.relationship("User", back_populates="avatar") def __init__(self, image_url, user_id): self.image_url = image_url self.user_id = user_id
class SocialMedia(db.Model): __tablename__ = "social_media" id = db.Column(db.Integer(), primary_key=True) service = db.Column(db.String(255), unique=False, nullable=False) username = db.Column(db.String(255), unique=False, nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) def __init__(self, service: str, username: str, user_id): self.service = service.upper()[:255] self.username = username.lower()[:255] self.user_id = user_id
class EmailVerification(db.Model): __tablename__ = "email_verification" user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) code = db.Column(db.String(255), unique=True, nullable=False) has_verified = db.Column(db.Boolean) user = db.relationship("User", back_populates="email_verification") def __init__(self, user_id: int): self.user_id = user_id self.code = gen_random_code(32) self.has_verified = False
class Task(db.Model): __tablename__ = 'task' id = db.Column(db.Integer(), primary_key=True) job_type = db.Column(db.Integer(), nullable=False) blob = db.Column(JsonEncodedDict, nullable=False) execute_after = db.Column(db.DateTime, nullable=False) completed = db.Column(db.Boolean, default=False) def __init__(self, job_type, blob, execute_after): assert job_type in list(JOBS.keys()), "Not a valid job" self.job_type = job_type self.blob = blob self.execute_after = execute_after
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.id = gen_random_id(ProposalUpdate) self.proposal_id = proposal_id self.title = title[:255] self.content = content self.date_created = datetime.datetime.now()
class Comment(db.Model): __tablename__ = "comment" id = db.Column(db.Integer(), primary_key=True) date_created = db.Column(db.DateTime) content = db.Column(db.Text, 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=False) def __init__(self, proposal_id, user_id, content): self.proposal_id = proposal_id self.user_id = user_id self.content = content self.date_created = datetime.datetime.now()
class EmailRecovery(db.Model): __tablename__ = "email_recovery" user_id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True) code = db.Column(db.String(255), unique=True, nullable=False) date_created = db.Column(db.DateTime) user = db.relationship("User", back_populates="email_recovery") def __init__(self, user_id: int): self.user_id = user_id self.code = gen_random_code(32) self.date_created = datetime.now() def is_expired(self): time_diff = datetime.now() - self.date_created return time_diff > RECOVERY_EXPIRATION
class Comment(db.Model): __tablename__ = "comment" id = db.Column(db.Integer(), primary_key=True) date_created = db.Column(db.DateTime) content = db.Column(db.Text, nullable=False) hidden = db.Column(db.Boolean, nullable=False, default=False, server_default=db.text("FALSE")) reported = db.Column(db.Boolean, nullable=True, default=False, server_default=db.text("FALSE")) parent_comment_id = db.Column(db.Integer, db.ForeignKey("comment.id"), nullable=True) proposal_id = db.Column(db.Integer, db.ForeignKey("proposal.id"), nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) user = db.relationship("User", back_populates="comments") author = db.relationship("User", back_populates="comments") replies = db.relationship("Comment") def __init__(self, proposal_id, user_id, parent_comment_id, content): self.id = gen_random_id(Comment) self.proposal_id = proposal_id self.user_id = user_id self.parent_comment_id = parent_comment_id self.content = content[:1000] self.date_created = datetime.datetime.now() @staticmethod def get_by_user(user): return Comment.query \ .options(raiseload(Comment.replies)) \ .filter(Comment.user_id == user.id) \ .order_by(Comment.date_created.desc()) \ .all() def report(self, reported: bool): self.reported = reported db.session.add(self) def hide(self, hidden: bool): self.hidden = hidden db.session.add(self)
class UserSettings(db.Model): __tablename__ = "user_settings" id = db.Column(db.Integer(), primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) _email_subscriptions = db.Column("email_subscriptions", db.Integer, default=0) # bitmask refund_address = db.Column(db.String(255), unique=False, nullable=True) tip_jar_address = db.Column(db.String(255), unique=False, nullable=True) tip_jar_view_key = db.Column(db.String(255), unique=False, nullable=True) user = db.relationship("User", back_populates="settings") @hybrid_property def email_subscriptions(self): return email_subscriptions_to_dict(self._email_subscriptions) @email_subscriptions.setter def email_subscriptions(self, subs): self._email_subscriptions = email_subscriptions_to_bits(subs) def __init__(self, user_id): self.email_subscriptions = get_default_email_subscriptions() self.user_id = user_id def unsubscribe_emails(self): es = self.email_subscriptions for k in es: es[k] = False self.email_subscriptions = es
class HistoryEvent(db.Model): __tablename__ = "history_event" id = db.Column(db.Integer(), primary_key=True) title = db.Column(db.String(255), nullable=False) content = db.Column(db.Text, nullable=False) date = db.Column(db.DateTime, nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True) proposal_id = db.Column(db.Integer, db.ForeignKey("proposal.id"), nullable=True) user = db.relationship("User", lazy=True) proposal = db.relationship("Proposal", lazy=True) def __init__( self, title: str, content: str, date: datetime = None, user_id: int = None, proposal_id: int = None, ): self.id = gen_random_id(HistoryEvent) self.title = title[:120] self.content = content[:1000] self.user_id = user_id self.proposal_id = proposal_id self.date = date or datetime.datetime.now()
class Tag(db.Model): __tablename__ = "tag" id = db.Column(db.Integer(), primary_key=True) text = db.Column(db.String(255), nullable=False) description = db.Column(db.Text, default='') color = db.Column(db.String(255), nullable=False) rfws = db.relationship('RFW', secondary='tag_association', back_populates="tags") def __init__(self, **kwargs): super().__init__(id=gen_random_id(Tag), **kwargs) @staticmethod def upsert(**kwargs): id = kwargs.get('id', False) if id: tag = Tag.query.get(id) if not tag: raise TagException(f'Attempted to update missing tag {id}') tag.update(**kwargs) return tag return Tag.create_tag(**kwargs) @staticmethod def create_tag(text: str, description: str, color: str): tag = Tag(text=text, description=description, color=color) db.session.add(tag) db.session.flush() return tag def update(self, **kwargs): for key, value in kwargs.items(): setattr(self, key, value) db.session.flush() def delete(self): db.session.delete(self) db.session.flush()
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[:255] 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 Avatar(db.Model): __tablename__ = "avatar" id = db.Column(db.Integer(), primary_key=True) _image_url = db.Column("image_url", db.String(255), unique=False, nullable=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id')) user = db.relationship("User", back_populates="avatar") @hybrid_property def image_url(self): return construct_avatar_url(self._image_url) @image_url.setter def image_url(self, image_url): self._image_url = extract_avatar_filename(image_url) def __init__(self, image_url, user_id): self.id = gen_random_id(Avatar) self.image_url = image_url self.user_id = user_id
class Proposal(db.Model): __tablename__ = "proposal" id = db.Column(db.Integer(), primary_key=True) date_created = db.Column(db.DateTime) title = db.Column(db.String(255), nullable=False) proposal_address = db.Column(db.String(255), unique=True, 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) team = db.relationship("User", secondary=proposal_team) comments = db.relationship(Comment, backref="proposal", lazy=True) updates = db.relationship(ProposalUpdate, backref="proposal", lazy=True) milestones = db.relationship("Milestone", backref="proposal", lazy=True) def __init__(self, stage: str, proposal_address: str, title: str, content: str, category: str): self.stage = stage self.proposal_address = proposal_address self.title = title self.content = content self.category = category self.date_created = datetime.datetime.now() @staticmethod def validate(stage: str, proposal_address: str, title: str, content: str, category: str): if stage not in PROPOSAL_STAGES: raise ValidationException("{} not in {}".format( stage, PROPOSAL_STAGES)) if category not in CATEGORIES: raise ValidationException("{} not in {}".format( category, CATEGORIES)) @staticmethod def create(**kwargs): Proposal.validate(**kwargs) return Proposal(**kwargs)
class AdminLog(db.Model): __tablename__ = "admin_log" id = db.Column(db.Integer(), primary_key=True) date_created = db.Column(db.DateTime, nullable=False) event = db.Column(db.String(255), nullable=False) message = db.Column(db.Text, nullable=False) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True) ip = db.Column(db.String(255), nullable=False) user = db.relationship("User") def __init__(self, **kwargs): super().__init__( id=gen_random_id(AdminLog), date_created=datetime.now(), **kwargs )
class RFWMilestone(db.Model): __tablename__ = 'rfw_milestone' id = db.Column(db.Integer(), primary_key=True) index = db.Column(db.Integer(), nullable=False) date_created = db.Column(db.DateTime) title = db.Column(db.String(255), nullable=False) content = db.Column(db.Text, nullable=False) effort_from = db.Column(db.BigInteger, nullable=False) effort_to = db.Column(db.BigInteger, nullable=False) bounty = db.Column(db.BigInteger, nullable=False) rfw_id = db.Column(db.Integer(), db.ForeignKey('rfw.id'), nullable=False) rfw = db.relationship('RFW', back_populates='milestones') claims = db.relationship("RFWMilestoneClaim", cascade="all,delete") @hybrid_property def authed_claim(self): authed = get_authed_user() if not authed: return None for c in self.claims: if c.worker.user_id == authed.id: return c return None @hybrid_property def is_authed_active(self): aw = self.rfw.authed_worker if aw and aw.status == RFWWorkerStatus.ACCEPTED: accepted = [ c.milestone_id for c in aw.claims if c.stage == RFWMilestoneClaimStage.ACCEPTED ] for ms in self.rfw.milestones: if ms.id not in accepted: # active ms for athed user if ms.id == self.id: return True else: return False return False def __init__(self, bounty=0, **kwargs): if 'index' not in kwargs: raise RFWException('Must set index on RFWMilestone') super().__init__(id=gen_random_id(RFWMilestone), date_created=datetime.now(), bounty=bounty, title=kwargs.pop('title', ''), content=kwargs.pop('content', ''), effort_from=kwargs.pop('effort_from', 0), effort_to=kwargs.pop('effort_to', 0), **kwargs) def update(self, **kwargs): if kwargs.pop('id', None): raise RFWException('Cannot update RFWMilestone IDs once created') for key, value in kwargs.items(): setattr(self, key, value) db.session.flush() def get_claim_by_id(self, id): claim = get(self.claims, 'id', int(id)) if claim is None: raise RFWException(f'Could not find RFWMilestone.claims[{id}]') return claim
CORE_DEV = "CORE_DEV" COMMUNITY = "COMMUNITY" DOCUMENTATION = "DOCUMENTATION" ACCESSIBILITY = "ACCESSIBILITY" CATEGORIES = [ DAPP, DEV_TOOL, CORE_DEV, COMMUNITY, DOCUMENTATION, ACCESSIBILITY ] class ValidationException(Exception): pass 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 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)
class RolesUsers(db.Model): __tablename__ = 'roles_users' id = db.Column(db.Integer(), primary_key=True) user_id = db.Column('user_id', db.Integer(), db.ForeignKey('user.id')) role_id = db.Column('role_id', db.Integer(), db.ForeignKey('role.id'))
class Role(db.Model, RoleMixin): __tablename__ = 'role' id = db.Column(db.Integer(), primary_key=True) name = db.Column(db.String(80), unique=True) description = db.Column(db.String(255))
class RFWMilestoneClaim(db.Model): __tablename__ = 'rfw_milestone_claim' id = db.Column(db.Integer(), primary_key=True) date_created = db.Column(db.DateTime) stage = db.Column(db.String(255), nullable=False) stage_message = db.Column(db.String, nullable=False) stage_change_date = db.Column(db.DateTime, nullable=False) stage_url = db.Column(db.String, nullable=False) worker_id = db.Column(db.Integer(), db.ForeignKey('rfw_worker.id')) milestone_id = db.Column(db.Integer(), db.ForeignKey('rfw_milestone.id')) worker = db.relationship("RFWWorker", back_populates="claims") milestone = db.relationship("RFWMilestone", back_populates="claims") @validates('stage') def validate_status(self, key, field): if not RFWMilestoneClaimStage.includes(field): raise RFWException( f'RFWMilestoneClaim stage must be in the RFWMilestoneClaimStageEnum, was [{field}]' ) return field def __init__(self, **kwargs): super().__init__(id=gen_random_id(RFWMilestoneClaim), date_created=datetime.now(), stage=RFWMilestoneClaimStage.REQUESTED, stage_message=kwargs.pop('stage_message', ''), stage_change_date=datetime.now(), stage_url=kwargs.pop('stage_url', ''), **kwargs) def set_requested(self, message='', url=''): if self.stage in [ RFWMilestoneClaimStage.ACCEPTED, RFWMilestoneClaimStage.REQUESTED ]: raise RFWException( f'Cannot request claim id {self.id} with status {self.stage}') self.stage = RFWMilestoneClaimStage.REQUESTED self.stage_message = message self.stage_change_date = datetime.now() self.stage_url = url db.session.flush() def set_accepted(self, message=''): if self.stage in [ RFWMilestoneClaimStage.ACCEPTED, RFWMilestoneClaimStage.REJECTED ]: raise RFWException( f'Cannot accept claim id {self.id} with status {self.stage}') self.stage = RFWMilestoneClaimStage.ACCEPTED self.stage_message = message self.stage_change_date = datetime.now() db.session.flush() def set_rejected(self, message: str): if self.stage in [ RFWMilestoneClaimStage.ACCEPTED, RFWMilestoneClaimStage.REJECTED ]: raise RFWException( f'Cannot reject claim id {self.id} with status {self.stage}') self.stage = RFWMilestoneClaimStage.REJECTED self.stage_message = message self.stage_change_date = datetime.now() self.stage_url = '' db.session.flush()
class RFP(db.Model): __tablename__ = "rfp" id = db.Column(db.Integer(), primary_key=True) date_created = db.Column(db.DateTime) title = db.Column(db.String(255), nullable=False) brief = db.Column(db.String(255), nullable=False) content = db.Column(db.Text, nullable=False) category = db.Column(db.String(255), nullable=False) status = db.Column(db.String(255), nullable=False) matching = db.Column(db.Boolean, default=False, nullable=False) _bounty = db.Column("bounty", db.String(255), nullable=True) date_closes = db.Column(db.DateTime, nullable=True) date_opened = db.Column(db.DateTime, nullable=True) date_closed = db.Column(db.DateTime, nullable=True) # Relationships proposals = db.relationship( "Proposal", backref="rfp", lazy=True, cascade="all, delete-orphan", ) accepted_proposals = db.relationship( "Proposal", lazy=True, primaryjoin="and_(Proposal.rfp_id==RFP.id, Proposal.status=='LIVE')", cascade="all, delete-orphan", ) @hybrid_property def bounty(self): return self._bounty @bounty.setter def bounty(self, bounty: str): if bounty and Decimal(bounty) > 0: self._bounty = bounty else: self._bounty = None def __init__( self, title: str, brief: str, content: str, category: str, bounty: str, date_closes: datetime, matching: bool = False, status: str = RFPStatus.DRAFT, ): assert RFPStatus.includes(status) assert Category.includes(category) self.id = gen_random_id(RFP) self.date_created = datetime.now() self.title = title[:255] self.brief = brief[:255] self.content = content self.category = category self.bounty = bounty self.date_closes = date_closes self.matching = matching self.status = status
class RFW(db.Model): __tablename__ = "rfw" id = db.Column(db.Integer(), primary_key=True) date_created = db.Column(db.DateTime) title = db.Column(db.String(255), nullable=False) brief = db.Column(db.String(255), nullable=False) content = db.Column(db.Text, nullable=False) status = db.Column(db.String(255), nullable=False) status_change_date = db.Column(db.DateTime, nullable=True) category = db.Column(db.String(255), nullable=False) # Relationships workers = db.relationship('RFWWorker', back_populates='rfw', cascade="all,delete") milestones = db.relationship('RFWMilestone', back_populates='rfw', order_by="RFWMilestone.index", cascade="all,delete,delete-orphan") tags = db.relationship('Tag', secondary='tag_association', back_populates="rfws") @hybrid_property def effort_from(self): return sum([ms.effort_from for ms in self.milestones]) @hybrid_property def effort_to(self): return sum([ms.effort_to for ms in self.milestones]) @hybrid_property def bounty(self): return sum([ms.bounty for ms in self.milestones]) @hybrid_property def authed_worker(self): authed = get_authed_user() if not authed: return None return get(self.workers, 'user_id', authed.id, None) @validates('status') def validate_status(self, key, field): if not RFWStatus.includes(field): raise RFWException( f'RFW status must be in the RFWStatusEnum, was [{field}]') return field @validates('category') def validate_category(self, key, field): if not Category.includes(field): raise RFWException( f'RFW category must be in the CategoryEnum, was [{field}]') return field def create(**kwargs): milestones = kwargs.pop('milestones', [{'index': 0}]) tags = kwargs.pop('tags', []) rfw = RFW(id=gen_random_id(RFW), date_created=datetime.now(), title=kwargs.pop('title', ''), brief=kwargs.pop('brief', ''), content=kwargs.pop('content', ''), status=kwargs.pop('status', RFWStatus.DRAFT), category=kwargs.pop('category', Category.COMMUNITY), **kwargs) db.session.add(rfw) db.session.flush() # milestones for ms in milestones: ms.pop('is_new', None) rfw.create_milestone(**ms) # tags for tag_id in tags: rfw.add_tag_by_id(tag_id) db.session.flush() return rfw def check_live(self): if self.status != RFWStatus.LIVE: raise RFWException( f'RFW must be {RFWStatus.LIVE}, was {self.status}') def delete(self): db.session.delete(self) db.session.flush() def update(self, milestones=[], delete_milestones=[], tags=[], **kwargs): for key, value in kwargs.items(): setattr(self, key, value) # ms sync for ms in milestones: if ms.pop('is_new', False): self.create_milestone(**ms) elif 'id' in ms: self.update_milestone_by_id(**ms) for ms_id in delete_milestones: self.delete_milestone_by_id(ms_id) self.check_milestone_integrity() # tags sync cur_tags = [x.id for x in self.tags] to_rem_tags = set(cur_tags) - set(tags) for tag_id in to_rem_tags: self.remove_tag_by_id(tag_id) for tag_id in tags: self.add_tag_by_id(tag_id) db.session.flush() def check_milestone_integrity(self): milestones = sorted(self.milestones, key=lambda x: x.index) for ind, ms in enumerate(milestones): if ind != ms.index: raise RFWException( f'RFW has bad milestone index for id {ms.id}. Got {ms.index}, expected {ind}' ) def get_milestone_by_id(self, id): ms = get(self.milestones, 'id', int(id)) if ms is None: raise RFWException(f'Could not find RFWMilestone with id {id}') return ms def update_milestone_by_id(self, id, **kwargs): ms = self.get_milestone_by_id(id) ms.update(**kwargs) db.session.flush() return ms def delete_milestone_by_id(self, id): ms = self.get_milestone_by_id(id) self.milestones.remove(ms) # delete-orphan, so ms is deleted as well db.session.flush() # re-order indexes milestones = sorted(self.milestones, key=lambda x: x.index) for ind, ms in enumerate(milestones): ms.index = ind db.session.flush() def create_milestone(self, **kwargs): self.milestones.append(RFWMilestone(**kwargs)) db.session.flush() def create_next_milestone(self, **kwargs): next_index = max([x.index for x in self.milestones]) + 1 next_milestone = RFWMilestone(index=next_index, **kwargs) self.milestones.append(next_milestone) db.session.flush() return next_milestone def create_worker_by_user_id_and_request(self, id, status_message): self.check_live() from grant.user.models import User user = User.query.get(int(id)) if not user: raise RFWException( f'Could not create a worker for RFW because user {id} not found' ) worker = get(self.workers, 'user_id', user.id) if not worker: worker = RFWWorker(user_id=user.id, rfw_id=self.id) self.workers.append(worker) worker.set_requested(message=status_message) db.session.flush() return worker def get_worker_by_id(self, id: int): worker = get(self.workers, 'id', int(id)) if not worker: raise RFWException( f'Could not find worker with id {id} for RFW with id {self.id}' ) return worker def accept_worker_by_id(self, id, message=''): worker = self.get_worker_by_id(id) worker.set_accepted(message) return worker def reject_worker_by_id(self, id, message=''): worker = self.get_worker_by_id(id) worker.set_rejected(message) return worker def get_existing_claim(self, worker_id, ms_id): worker = self.get_worker_by_id(worker_id) self.get_milestone_by_id( ms_id) # throws if non-child/non-existing milestone existing_claim = next( (x for x in worker.claims if x.milestone_id == ms_id), None) return existing_claim def request_milestone_claim(self, worker_id, ms_id, msg, url): self.check_live() worker_id = int(worker_id) ms_id = int(ms_id) claim = self.get_existing_claim(worker_id, ms_id) ms = self.get_milestone_by_id(ms_id) if not claim: worker = self.get_worker_by_id(worker_id) # init with message and url claim = RFWMilestoneClaim(stage_message=msg, stage_url=url) ms.claims.append(claim) worker.claims.append(claim) db.session.flush() else: claim.set_requested(msg, url) return claim def accept_milestone_claim(self, ms_id, claim_id, msg): ms = self.get_milestone_by_id(int(ms_id)) claim = ms.get_claim_by_id(int(claim_id)) claim.set_accepted(msg) def reject_milestone_claim(self, ms_id, claim_id, msg): ms = self.get_milestone_by_id(int(ms_id)) claim = ms.get_claim_by_id(int(claim_id)) claim.set_rejected(msg) def add_tag_by_id(self, id): tag = Tag.query.get(int(id)) if tag: self.add_tag(tag) def add_tag(self, tag: Tag): self.tags.append(tag) db.session.flush() def remove_tag_by_id(self, id): tag = Tag.query.get(int(id)) if tag: self.tags.remove(tag) db.session.flush() def set_status(self, status: RFWStatus): self.status = status self.status_change_date = datetime.now() db.session.flush() def publish(self): self.set_status(RFWStatus.LIVE) def close(self): self.set_status(RFWStatus.CLOSED)
class RFP(db.Model): __tablename__ = "rfp" id = db.Column(db.Integer(), primary_key=True) date_created = db.Column(db.DateTime) title = db.Column(db.String(255), nullable=False) brief = db.Column(db.String(255), nullable=False) content = db.Column(db.Text, nullable=False) category = db.Column(db.String(255), nullable=True) status = db.Column(db.String(255), nullable=False) matching = db.Column(db.Boolean, default=False, nullable=False) _bounty = db.Column("bounty", db.String(255), nullable=True) date_closes = db.Column(db.DateTime, nullable=True) date_opened = db.Column(db.DateTime, nullable=True) date_closed = db.Column(db.DateTime, nullable=True) version = db.Column(db.String(255), nullable=True) ccr = db.relationship("CCR", uselist=False, back_populates="rfp") # Relationships proposals = db.relationship( "Proposal", backref="rfp", lazy=True, cascade="all, delete-orphan", ) accepted_proposals = db.relationship( "Proposal", lazy=True, primaryjoin="and_(Proposal.rfp_id==RFP.id, Proposal.status=='LIVE')", cascade="all, delete-orphan", ) likes = db.relationship("User", secondary=rfp_liker, back_populates="liked_rfps") likes_count = column_property( select([func.count(rfp_liker.c.rfp_id) ]).where(rfp_liker.c.rfp_id == id).correlate_except(rfp_liker)) @hybrid_property def bounty(self): return self._bounty @bounty.setter def bounty(self, bounty: str): if bounty and Decimal(bounty) > 0: self._bounty = bounty else: self._bounty = None @hybrid_property def authed_liked(self): from grant.utils.auth import get_authed_user authed = get_authed_user() if not authed: return False res = (db.session.query(rfp_liker).filter_by(user_id=authed.id, rfp_id=self.id).count()) if res: return True return False def like(self, user, is_liked): if is_liked: self.likes.append(user) else: self.likes.remove(user) db.session.flush() def __init__( self, title: str, brief: str, content: str, bounty: str, date_closes: datetime, matching: bool = False, status: str = RFPStatus.DRAFT, ): assert RFPStatus.includes(status) self.id = gen_random_id(RFP) self.date_created = datetime.now() self.title = title[:255] self.brief = brief[:255] self.content = content self.bounty = bounty self.date_closes = date_closes self.matching = matching self.status = status self.version = '2'
from datetime import datetime from decimal import Decimal from grant.extensions import ma, db from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy import func, select from sqlalchemy.orm import column_property from grant.utils.enums import RFPStatus from grant.utils.misc import dt_to_unix, gen_random_id from grant.utils.enums import Category rfp_liker = db.Table( "rfp_liker", db.Model.metadata, db.Column("user_id", db.Integer, db.ForeignKey("user.id")), db.Column("rfp_id", db.Integer, db.ForeignKey("rfp.id")), ) class RFP(db.Model): __tablename__ = "rfp" id = db.Column(db.Integer(), primary_key=True) date_created = db.Column(db.DateTime) title = db.Column(db.String(255), nullable=False) brief = db.Column(db.String(255), nullable=False) content = db.Column(db.Text, nullable=False) category = db.Column(db.String(255), nullable=True) status = db.Column(db.String(255), nullable=False) matching = db.Column(db.Boolean, default=False, nullable=False) _bounty = db.Column("bounty", db.String(255), nullable=True)
class CCR(db.Model): __tablename__ = "ccr" id = db.Column(db.Integer(), primary_key=True) date_created = db.Column(db.DateTime) title = db.Column(db.String(255), nullable=True) brief = db.Column(db.String(255), nullable=True) content = db.Column(db.Text, nullable=True) status = db.Column(db.String(255), nullable=False) _target = db.Column("target", db.String(255), nullable=True) reject_reason = db.Column(db.String()) user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False) author = db.relationship("User", back_populates="ccrs") rfp_id = db.Column(db.Integer, db.ForeignKey("rfp.id"), nullable=True) rfp = db.relationship("RFP", back_populates="ccr") @staticmethod def get_by_user(user, statuses=[CCRStatus.LIVE]): status_filter = or_(CCR.status == v for v in statuses) return CCR.query \ .filter(CCR.user_id == user.id) \ .filter(status_filter) \ .all() @staticmethod def create(**kwargs): ccr = CCR( **kwargs ) db.session.add(ccr) db.session.flush() return ccr @hybrid_property def target(self): return self._target @target.setter def target(self, target: str): if target and Decimal(target) > 0: self._target = target else: self._target = None def __init__( self, user_id: int, title: str = '', brief: str = '', content: str = default_content(), target: str = '0', status: str = CCRStatus.DRAFT, ): assert CCRStatus.includes(status) self.id = gen_random_id(CCR) self.date_created = datetime.now() self.title = title[:255] self.brief = brief[:255] self.content = content self.target = target self.status = status self.user_id = user_id def update( self, title: str = '', brief: str = '', content: str = '', target: str = '0', ): self.title = title[:255] self.brief = brief[:255] self.content = content[:300000] self._target = target[:255] if target != '' and target else '0' # state: status (DRAFT || REJECTED) -> (PENDING || STAKING) def submit_for_approval(self): self.validate_publishable() allowed_statuses = [CCRStatus.DRAFT, CCRStatus.REJECTED] # specific validation if self.status not in allowed_statuses: raise ValidationException(f"CCR status must be draft or rejected to submit for approval") self.set_pending() def send_admin_email(self, type: str): from grant.user.models import User admins = User.get_admins() for a in admins: send_email(a.email_address, type, { 'user': a, 'ccr': self, 'ccr_url': make_admin_url(f'/ccrs/{self.id}'), }) # state: status DRAFT -> PENDING def set_pending(self): self.send_admin_email('admin_approval_ccr') self.status = CCRStatus.PENDING db.session.add(self) db.session.flush() def validate_publishable(self): # Require certain fields required_fields = ['title', 'content', 'brief', 'target'] for field in required_fields: if not hasattr(self, field): raise ValidationException("Proposal must have a {}".format(field)) # Stricter limits on certain fields if len(self.title) > 60: raise ValidationException("Proposal title cannot be longer than 60 characters") if len(self.brief) > 140: raise ValidationException("Brief cannot be longer than 140 characters") if len(self.content) > 250000: raise ValidationException("Content cannot be longer than 250,000 characters") # state: status PENDING -> (LIVE || REJECTED) def approve_pending(self, is_approve, reject_reason=None): from grant.rfp.models import RFP self.validate_publishable() # specific validation if not self.status == CCRStatus.PENDING: raise ValidationException(f"CCR must be pending to approve or reject") if is_approve: self.status = CCRStatus.LIVE rfp = RFP( title=self.title, brief=self.brief, content=self.content, bounty=self._target, date_closes=datetime.now() + timedelta(days=90), ) db.session.add(self) db.session.add(rfp) db.session.flush() self.rfp_id = rfp.id db.session.add(rfp) db.session.flush() # for emails db.session.commit() send_email(self.author.email_address, 'ccr_approved', { 'user': self.author, 'ccr': self, 'admin_note': f'Congratulations! Your Request has been accepted. There may be a delay between acceptance and final posting as required by the Zcash Foundation.' }) return rfp.id else: if not reject_reason: raise ValidationException("Please provide a reason for rejecting the ccr") self.status = CCRStatus.REJECTED self.reject_reason = reject_reason # for emails db.session.add(self) db.session.commit() send_email(self.author.email_address, 'ccr_rejected', { 'user': self.author, 'ccr': self, 'admin_note': reject_reason }) return None
class RFWWorker(db.Model): __tablename__ = 'rfw_worker' id = db.Column(db.Integer(), primary_key=True) date_created = db.Column(db.DateTime) status = db.Column(db.String(255), nullable=False) status_message = db.Column(db.String, nullable=False) status_change_date = db.Column(db.DateTime, nullable=True) # relations rfw_id = db.Column(db.Integer(), db.ForeignKey('rfw.id')) user_id = db.Column(db.Integer(), db.ForeignKey('user.id')) rfw = db.relationship('RFW', back_populates='workers') user = db.relationship('User', back_populates='rfws') claims = db.relationship("RFWMilestoneClaim") @staticmethod def get_work(user_id, is_self=False): if is_self: work = RFWWorker.query.filter_by(user_id=user_id) \ .order_by(RFWWorker.status_change_date.desc()) \ .all() work_dump = RFWWorkerSchema(many=True).dump(work) else: work = RFWWorker.query.filter_by(user_id=user_id, status=RFWWorkerStatus.ACCEPTED) \ .order_by(RFWWorker.status_change_date.desc()) \ .all() work_dump = RFWWorkerSchema(many=True, exclude=['status_message']).dump(work) for w in work_dump: w['claims'] = [ c for c in w['claims'] if c['stage'] == RFWMilestoneClaimStage.ACCEPTED ] return work_dump @hybrid_property def is_self(self): authed = get_authed_user() if authed: return authed.id == self.user.id return False @validates('status') def validate_status(self, key, field): if not RFWWorkerStatus.includes(field): raise RFWException( f'RFWWorker status must be in the RFWWorkerStatusEnum, was [{field}]' ) return field def __init__(self, **kwargs): super().__init__(id=gen_random_id(RFWWorker), date_created=datetime.now(), status=RFWWorkerStatus.REQUESTED, status_message=kwargs.pop('status_message', ''), **kwargs) def set_requested(self, message=''): if self.status == RFWWorkerStatus.ACCEPTED: raise RFWException( f'Cannot request worker when already accepted, worker id {worker.id}' ) if self.status == RFWWorkerStatus.REJECTED: pass self.status = RFWWorkerStatus.REQUESTED self.status_message = message self.status_change_date = datetime.now() db.session.flush() def set_accepted(self, message=''): self.status = RFWWorkerStatus.ACCEPTED self.status_message = message self.status_change_date = datetime.now() db.session.flush() def set_rejected(self, message: str): self.status = RFWWorkerStatus.REJECTED self.status_message = message self.status_change_date = datetime.now() db.session.flush()