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 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 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 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 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 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 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 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 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 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 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 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'
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 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 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) author = db.relationship("User", back_populates="comments") replies = db.relationship("Comment") likes = db.relationship( "User", secondary=comment_liker, back_populates="liked_comments" ) likes_count = column_property( select([func.count(comment_liker.c.comment_id)]) .where(comment_liker.c.comment_id == id) .correlate_except(comment_liker) ) 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[:5000] 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) @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(comment_liker) .filter_by(user_id=authed.id, comment_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()
class User(db.Model, UserMixin): __tablename__ = "user" id = db.Column(db.Integer(), primary_key=True) email_address = db.Column(db.String(255), unique=True, nullable=False) password = db.Column(db.String(255), unique=False, nullable=False) display_name = db.Column(db.String(255), unique=False, nullable=True) title = db.Column(db.String(255), unique=False, nullable=True) active = db.Column(db.Boolean, default=True) is_admin = db.Column(db.Boolean, default=False, nullable=False, server_default=db.text("FALSE")) totp_secret = db.Column(db.String(255), nullable=True) backup_codes = db.Column(db.String(), nullable=True) # moderation silenced = db.Column(db.Boolean, default=False) banned = db.Column(db.Boolean, default=False) banned_reason = db.Column(db.String(), nullable=True) # relations social_medias = db.relationship(SocialMedia, backref="user", lazy=True, cascade="all, delete-orphan") comments = db.relationship(Comment, backref="user", lazy=True) ccrs = db.relationship(CCR, back_populates="author", lazy=True, cascade="all, delete-orphan") avatar = db.relationship(Avatar, uselist=False, back_populates="user", cascade="all, delete-orphan") settings = db.relationship(UserSettings, uselist=False, back_populates="user", lazy=True, cascade="all, delete-orphan") email_verification = db.relationship(EmailVerification, uselist=False, back_populates="user", lazy=True, cascade="all, delete-orphan") email_recovery = db.relationship(EmailRecovery, uselist=False, back_populates="user", lazy=True, cascade="all, delete-orphan") roles = db.relationship('Role', secondary='roles_users', backref=db.backref('users', lazy='dynamic')) arbiter_proposals = db.relationship("ProposalArbiter", lazy=True, back_populates="user") followed_proposals = db.relationship("Proposal", secondary="proposal_follower", back_populates="followers") liked_proposals = db.relationship("Proposal", secondary="proposal_liker", back_populates="likes") liked_comments = db.relationship("Comment", secondary="comment_liker", back_populates="likes") liked_rfps = db.relationship("RFP", secondary="rfp_liker", back_populates="likes") def __init__( self, email_address, password, active, roles, display_name=None, title=None, ): self.id = gen_random_id(User) self.email_address = email_address self.display_name = display_name[:255] self.title = title[:255] self.password = password @staticmethod def validate(user): em = user.get('email_address') if not em: raise ValidationException('Must have email address') if not is_email(em): raise ValidationException('Email address looks invalid') t = user.get('title') if t and len(t) > 255: raise ValidationException('Title is too long') dn = user.get('display_name') if dn and len(dn) > 255: raise ValidationException('Display name is too long') @staticmethod def create(email_address=None, password=None, display_name=None, title=None, _send_email=True): user = security.datastore.create_user(email_address=email_address, password=hash_password(password), display_name=display_name, title=title) User.validate(vars(user)) security.datastore.commit() # user settings us = UserSettings(user_id=user.id) db.session.add(us) # Setup & send email verification ev = EmailVerification(user_id=user.id) db.session.add(ev) db.session.commit() if _send_email: user.send_verification_email() return user @staticmethod def get_by_id(user_id: int): return security.datastore.get_user(user_id) @staticmethod def get_by_email(email_address: str): return security.datastore.get_user(email_address) @staticmethod def get_admins(): return User.query.filter(User.is_admin == True).all() def check_password(self, password: str): return verify_and_update_password(password, self) def set_password(self, password: str): self.password = hash_password(password) db.session.commit() send_email( self.email_address, 'change_password', { 'display_name': self.display_name, 'recover_url': make_url('/auth/recover'), 'contact_url': make_url('/contact') }) def set_email(self, email: str): # Update email address old_email = self.email_address self.email_address = email # Delete old verification(s?) old_evs = EmailVerification.query.filter_by(user_id=self.id).all() for old_ev in old_evs: db.session.delete(old_ev) # Generate a new one ev = EmailVerification(user_id=self.id) db.session.add(ev) # Save changes & send notification & verification emails db.session.commit() send_email(old_email, 'change_email_old', { 'display_name': self.display_name, 'contact_url': make_url('/contact') }) send_email( self.email_address, 'change_email', { 'display_name': self.display_name, 'confirm_url': make_url(f'/email/verify?code={ev.code}') }) def login(self): login_user(self) def send_verification_email(self): send_email( self.email_address, 'signup', { 'display_name': self.display_name, 'confirm_url': make_url(f'/email/verify?code={self.email_verification.code}') }) def send_recovery_email(self): existing = self.email_recovery if existing: db.session.delete(existing) er = EmailRecovery(user_id=self.id) db.session.add(er) db.session.commit() send_email( self.email_address, 'recover', { 'display_name': self.display_name, 'recover_url': make_url(f'/email/recover?code={er.code}'), }) def set_banned(self, is_ban: bool, reason: str = None): self.banned = is_ban self.banned_reason = reason db.session.add(self) db.session.flush() def set_silenced(self, is_silence: bool): self.silenced = is_silence db.session.add(self) db.session.flush() def set_admin(self, is_admin: bool): self.is_admin = is_admin db.session.add(self) db.session.flush() def set_2fa(self, codes, secret): self.totp_secret = secret self.backup_codes = totp_2fa.serialize_backup_codes(codes) db.session.add(self) db.session.flush() def set_serialized_backup_codes(self, codes): self.backup_codes = codes db.session.add(self) db.session.flush() def has_2fa(self): return self.totp_secret is not None def get_backup_code_count(self): if not self.backup_codes: return 0 return len(totp_2fa.deserialize_backup_codes(self.backup_codes))
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
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()
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 Proposal(db.Model): __tablename__ = "proposal" id = db.Column(db.Integer(), primary_key=True) date_created = db.Column(db.DateTime) rfp_id = db.Column(db.Integer(), db.ForeignKey('rfp.id'), nullable=True) # 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) date_approved = db.Column(db.DateTime) date_published = db.Column(db.DateTime) reject_reason = db.Column(db.String()) private = db.Column(db.Boolean, default=False, nullable=False) # Payment info target = db.Column(db.String(255), 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" ) milestones = db.relationship( "Milestone", backref="proposal", order_by="asc(Milestone.index)", lazy=True, cascade="all, delete-orphan" ) invites = db.relationship( ProposalTeamInvite, backref="proposal", lazy=True, cascade="all, delete-orphan" ) followers = db.relationship( "User", secondary=proposal_follower, back_populates="followed_proposals" ) followers_count = column_property( select([func.count(proposal_follower.c.proposal_id)]) .where(proposal_follower.c.proposal_id == id) .correlate_except(proposal_follower) ) comments_count = column_property( select([func.count(Comment.proposal_id)]) .where(Comment.proposal_id == id) .where(Comment.hidden != True) .correlate_except(Comment) ) def __init__( self, status: str = ProposalStatus.DRAFT, title: str = '', brief: str = '', content: str = '', stage: str = ProposalStage.PREVIEW, target: str = '0', category: str = '' ): self.id = gen_random_id(Proposal) 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.stage = stage @staticmethod def simple_validate(proposal): # Validate fields to be database save-able. # Stricter validation is done in validate_publishable. stage = proposal.get('stage') category = proposal.get('category') if stage and not ProposalStage.includes(stage): raise ValidationException("Proposal stage {} is not a valid stage".format(stage)) if category and not Category.includes(category): raise ValidationException("Category {} not a valid category".format(category)) def validate_publishable_milestones(self): payout_total = 0.0 for i, milestone in enumerate(self.milestones): if milestone.immediate_payout and i != 0: raise ValidationException("Only the first milestone can have an immediate payout") if len(milestone.title) > 60: raise ValidationException("Milestone title cannot be longer than 60 chars") if len(milestone.content) > 200: raise ValidationException("Milestone content cannot be longer than 200 chars") try: p = float(milestone.payout_amount) if not p.is_integer(): raise ValidationException("Milestone payout must be whole numbers, no decimals") if p <= 0: raise ValidationException("Milestone payout must be greater than zero") except ValueError: raise ValidationException("Milestone payout percent must be a number") payout_total += p if payout_total != float(self.target): raise ValidationException("Payout of milestones must add up to proposal target") def validate_publishable(self): self.validate_publishable_milestones() # Require certain fields required_fields = ['title', 'content', 'brief', 'category', '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") # Then run through regular validation Proposal.simple_validate(vars(self)) # only do this when user submits for approval, there is a chance the dates will # be passed by the time admin approval / user publishing occurs def validate_milestone_dates(self): present = datetime.datetime.today().replace(day=1, hour=0, minute=0, second=0, microsecond=0) for milestone in self.milestones: if present > milestone.date_estimated: raise ValidationException("Milestone date estimate must be in the future ") @staticmethod def create(**kwargs): Proposal.simple_validate(kwargs) proposal = Proposal( **kwargs ) db.session.add(proposal) db.session.flush() return proposal @staticmethod def get_by_user(user, statuses=[ProposalStatus.LIVE]): from grant.utils.auth import get_authed_user authed = get_authed_user() status_filter = or_(Proposal.status == v for v in statuses) res = Proposal.query \ .join(proposal_team) \ .filter(proposal_team.c.user_id == user.id) \ .filter(status_filter) \ .all() # only team members get to see private proposals without_priv = [] for p in res: if p.private: if authed and authed.id in [t.id for t in p.team]: without_priv.append(p) else: without_priv.append(p) return without_priv def update( self, title: str = '', brief: str = '', category: str = '', content: str = '', target: str = '0', ): self.title = title[:255] self.brief = brief[:255] self.category = category self.content = content[:300000] self.target = target[:255] if target != '' else '0' Proposal.simple_validate(vars(self)) def send_admin_email(self, type: str): send_admin_email(type, { 'proposal': self, 'proposal_url': make_admin_url(f'/proposals/{self.id}'), }) def send_follower_email(self, type: str, email_args={}, url_suffix=''): for u in self.followers: send_email(u.email_address, type, { 'user': u, 'proposal': self, 'proposal_url': make_url(f'/proposals/{self.id}{url_suffix}'), **email_args }) # state: status (DRAFT || REJECTED) -> PENDING def submit_for_approval(self): self.validate_publishable() self.validate_milestone_dates() allowed_statuses = [ProposalStatus.DRAFT, ProposalStatus.REJECTED] # specific validation if self.status not in allowed_statuses: raise ValidationException(f"Proposal status must be draft or rejected to submit for approval") self.send_admin_email('admin_approval') self.status = ProposalStatus.PENDING db.session.add(self) db.session.flush() # state: status PENDING -> (APPROVED || REJECTED) def approve_pending(self, is_approve, reject_reason=None): self.validate_publishable() # specific validation if not self.status == ProposalStatus.PENDING: raise ValidationException(f"Proposal must be pending to approve or reject") if is_approve: self.status = ProposalStatus.APPROVED self.date_approved = datetime.datetime.now() for t in self.team: send_email(t.email_address, 'proposal_approved', { 'user': t, 'proposal': self, 'proposal_url': make_url(f'/proposals/{self.id}'), 'admin_note': 'Congratulations! Your proposal has been approved.' }) else: if not reject_reason: raise ValidationException("Please provide a reason for rejecting the proposal") self.status = ProposalStatus.REJECTED self.reject_reason = reject_reason for t in self.team: send_email(t.email_address, 'proposal_rejected', { 'user': t, 'proposal': self, 'proposal_url': make_url(f'/proposals/{self.id}'), 'admin_note': reject_reason }) # state: status APPROVE -> LIVE, stage PREVIEW -> WIP def publish(self): self.validate_publishable() # specific validation if not self.status == ProposalStatus.APPROVED: raise ValidationException(f"Proposal status must be approved") self.date_published = datetime.datetime.now() self.status = ProposalStatus.LIVE self.stage = ProposalStage.WIP def cancel(self): if self.status != ProposalStatus.LIVE: raise ValidationException("Cannot cancel a proposal until it's live") self.stage = ProposalStage.CANCELED db.session.add(self) db.session.flush() # Send emails to team & contributors for u in self.team: send_email(u.email_address, 'proposal_canceled', { 'proposal': self, 'support_url': make_url('/contact'), }) def follow(self, user, is_follow): if is_follow: self.followers.append(user) else: self.followers.remove(user) db.session.flush() @hybrid_property def is_failed(self): if not self.status == ProposalStatus.LIVE or not self.date_published: return False if self.stage == ProposalStage.FAILED or self.stage == ProposalStage.CANCELED: return True return False @hybrid_property def current_milestone(self): if self.milestones: for ms in self.milestones: if ms.stage != MilestoneStage.PAID: return ms return self.milestones[-1] # return last one if all PAID return None @hybrid_property def authed_follows(self): from grant.utils.auth import get_authed_user authed = get_authed_user() if not authed: return False res = db.session.query(proposal_follower) \ .filter_by(user_id=authed.id, proposal_id=self.id) \ .count() if res: return True return False