zcash-grant-system/backend/grant/ccr/models.py

231 lines
7.0 KiB
Python
Raw Normal View History

CCRs (#86) * CCRs API / Models boilerplate * start on frontend * backendy things * Create CCR redux module, integrate API endpoints, create types * Fix/Cleanup API * Wire up CreateRequestDraftList * bounty->target * Add 'Create Request Flow' MVP * cleanup * Tweak filenames * Simplify migrations * fix migrations * CCR Staking MVP * tslint * Get Pending Requests into Profile * Remove staking requirement * more staking related removals * MVP Admin integration * Make RFP when CCR is accepted * Add pagination to CCRs in Admin Improve styles for Proposals * Hookup notifications Adjust copy * Simplify ccr->rfp relationship Add admin approval email Fixup copy * Show Message on RFP Detail Make Header CTAs change based on draft status Adjust proposal card style * Bugfix: Show header for non signed in users * Add 'create a request' to intro * Profile Created CCRs RFP CCR attribution * ignore * CCR Price in USD (#85) * init profile tipjar backend * init profile tipjar frontend * fix lint * implement tip jar block * fix wrapping, hide tip block on self * init backend proposal tipjar * init frontend proposal tipjar * add hide title, fix bug * uncomment rate limit * rename vars, use null check * allow address and view key to be unset * add api tests * fix tsc errors * fix lint * fix CopyInput styling * fix migrations * hide tipping in proposal if address not set * add tip address to create flow * redesign campaign block * fix typo * init backend changes * init admin changes * init frontend changes * fix backend tests * update campaign block * be - init rfp usd changes * admin - init rfp usd changes * fe - fully adapt api util functions to usd * fe - init rfp usd changes * adapt profile created to usd * misc usd changes * add tip jar to dedicated card * fix tipjar bug * use zf light logo * switch to zf grants logo * hide profile tip jar if address not set * add comment, run prettier * conditionally add info icon and tooltip to funding line * admin - disallow decimals in RFPs * fe - cover usd string edge case * add Usd as rfp bounty type * fix migration order * fix email bug * adapt CCRs to USD * implement CCR preview * fix tsc * Copy Updates and UX Tweaks (#87) * Add default structure to proposal content * Landing page copy * Hide contributors tab for v2 proposals * Minor UX tweaks for Liking/Following/Tipping * Copy for Tipping Tooltip, proposal explainer for review, and milestone day estimate notice. * Fix header styles bug and remove commented out styles. * Revert "like" / "unfollow" hyphenication * Comment out unused tests related to staking Increase PROPOSAL_TARGET_MAX in .env.example * Comment out ccr approval email send until ready * Adjust styles, copy. * fix proposal prune test (#88) * fix USD display in preview, fix non-unique key (#90) * Pre-stepper explainer for CCRs. * Tweak styles * Default content for CCRs * fix tsc * CCR approval and rejection emails * add back admin_approval_ccr email templates * Link ccr author name to profile in RFPs * copy tweaks * copy tweak * hookup mangle user command * Fix/add endif in jinja * fix tests * review * fix review
2019-12-05 17:01:02 -08:00
from datetime import datetime, timedelta
from decimal import Decimal
from sqlalchemy import or_
from sqlalchemy.ext.hybrid import hybrid_property
from grant.email.send import send_email
from grant.extensions import ma, db
from grant.utils.enums import CCRStatus
from grant.utils.exceptions import ValidationException
from grant.utils.misc import make_admin_url, gen_random_id, dt_to_unix
def default_content():
return """# Overview
What you think should be accomplished
# Approach
How you expect a proposing team to accomplish your request
# Deliverable
The end result of a proposal the fulfills this request
"""
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 CCRSchema(ma.Schema):
class Meta:
model = CCR
# Fields to expose
fields = (
"author",
"id",
"title",
"brief",
"ccr_id",
"content",
"status",
"target",
"date_created",
"reject_reason",
"rfp"
)
rfp = ma.Nested("RFPSchema")
date_created = ma.Method("get_date_created")
author = ma.Nested("UserSchema")
ccr_id = ma.Method("get_ccr_id")
def get_date_created(self, obj):
return dt_to_unix(obj.date_created)
def get_ccr_id(self, obj):
return obj.id
ccr_schema = CCRSchema()
ccrs_schema = CCRSchema(many=True)