diff --git a/src/back/routes/proposal.ts b/src/back/routes/proposal.ts index 8e81dff4a..412731cef 100644 --- a/src/back/routes/proposal.ts +++ b/src/back/routes/proposal.ts @@ -83,11 +83,27 @@ import { SNAPSHOT_DURATION } from '../../entities/Snapshot/constants' import { isSameAddress } from '../../entities/Snapshot/utils' import { validateUniqueAddresses } from '../../entities/Transparency/utils' import UpdateModel from '../../entities/Updates/model' +import { + FinancialRecord, + FinancialUpdateSectionSchema, + GeneralUpdateSectionSchema, + UpdateGeneralSection, +} from '../../entities/Updates/types' +import { + getCurrentUpdate, + getFundsReleasedSinceLatestUpdate, + getLatestUpdate, + getNextPendingUpdate, + getPendingUpdates, + getPublicUpdates, + getReleases, +} from '../../entities/Updates/utils' import BidService from '../../services/BidService' import { DiscourseService } from '../../services/DiscourseService' import { ErrorService } from '../../services/ErrorService' import { ProjectService } from '../../services/ProjectService' import { ProposalInCreation, ProposalService } from '../../services/ProposalService' +import { VestingService } from '../../services/VestingService' import { getProfile } from '../../utils/Catalyst' import Time from '../../utils/date/Time' import { ErrorCategory } from '../../utils/errorCategories' @@ -95,6 +111,7 @@ import { isProdEnv } from '../../utils/governanceEnvs' import logger from '../../utils/logger' import { DclNotificationService } from '../services/dcl-notification' import { NotificationService } from '../services/notification' +import { UpdateService } from '../services/update' import { validateAddress, validateProposalId } from '../utils/validations' export default routes((route) => { @@ -119,6 +136,8 @@ export default routes((route) => { route.patch('/proposals/:proposal', withAuth, handleAPI(updateProposalStatus)) route.delete('/proposals/:proposal', withAuth, handleAPI(removeProposal)) route.get('/proposals/:proposal/comments', handleAPI(getProposalComments)) + route.get('/proposals/:proposal/updates', handleAPI(getProposalUpdates)) + route.post('/proposals/:proposal/update', withAuth, handleAPI(createProposalUpdate)) route.get('/proposals/linked-wearables/image', handleAPI(checkImage)) }) @@ -683,3 +702,84 @@ async function checkImage(req: Request) { }) }) } + +async function getProposalUpdates(req: Request<{ proposal_id: string }>) { + const proposal_id = req.params.proposal_id + + if (!proposal_id) { + throw new RequestError(`Proposal not found: "${proposal_id}"`, RequestError.NotFound) + } + + const updates = await UpdateService.getAllByProposalId(proposal_id) + const publicUpdates = getPublicUpdates(updates) + const nextUpdate = getNextPendingUpdate(updates) + const currentUpdate = getCurrentUpdate(updates) + const pendingUpdates = getPendingUpdates(updates) + + return { + publicUpdates, + pendingUpdates, + nextUpdate, + currentUpdate, + } +} + +function parseFinancialRecords(financial_records: unknown) { + const parsedResult = FinancialUpdateSectionSchema.safeParse({ financial_records }) + if (!parsedResult.success) { + ErrorService.report('Submission of invalid financial records', { + error: parsedResult.error, + category: ErrorCategory.Financial, + }) + throw new RequestError(`Invalid financial records`, RequestError.BadRequest) + } + return parsedResult.data.financial_records +} + +async function validateFinancialRecords( + proposal: ProposalAttributes, + financial_records: unknown +): Promise { + const [vestingData, updates] = await Promise.all([ + VestingService.getVestingInfo(proposal.vesting_addresses), + UpdateService.getAllByProposalId(proposal.id), + ]) + + const releases = vestingData ? getReleases(vestingData) : undefined + const publicUpdates = getPublicUpdates(updates) + const latestUpdate = getLatestUpdate(publicUpdates || []) + const { releasedFunds } = getFundsReleasedSinceLatestUpdate(latestUpdate, releases) + return releasedFunds > 0 ? parseFinancialRecords(financial_records) : null +} + +async function createProposalUpdate(req: WithAuth>) { + const { author, financial_records, ...body } = req.body + const { health, introduction, highlights, blockers, next_steps, additional_notes } = validate( + schema.compile(GeneralUpdateSectionSchema), + body + ) + try { + const proposal = await getProposal(req) + const financialRecords = await validateFinancialRecords(proposal, financial_records) + return await UpdateService.create( + { + proposal_id: req.params.proposal, + author, + health, + introduction, + highlights, + blockers, + next_steps, + additional_notes, + financial_records: financialRecords, + }, + req.auth! + ) + } catch (error) { + ErrorService.report('Error creating update', { + error, + category: ErrorCategory.Update, + }) + throw new RequestError(`Something went wrong: ${error}`, RequestError.InternalServerError) + } +} diff --git a/src/back/routes/update.ts b/src/back/routes/update.ts index f9443fa91..5f2282ccf 100644 --- a/src/back/routes/update.ts +++ b/src/back/routes/update.ts @@ -9,44 +9,29 @@ import { Request } from 'express' import ProposalModel from '../../entities/Proposal/model' import { ProposalAttributes } from '../../entities/Proposal/types' import { - FinancialRecord, FinancialUpdateSectionSchema, GeneralUpdateSectionSchema, UpdateGeneralSection, } from '../../entities/Updates/types' -import { - getCurrentUpdate, - getFundsReleasedSinceLatestUpdate, - getLatestUpdate, - getNextPendingUpdate, - getPendingUpdates, - getPublicUpdates, - getReleases, - isBetweenLateThresholdDate, -} from '../../entities/Updates/utils' +import { isBetweenLateThresholdDate } from '../../entities/Updates/utils' import { DiscourseService } from '../../services/DiscourseService' import { ErrorService } from '../../services/ErrorService' import { FinancialService } from '../../services/FinancialService' -import { VestingService } from '../../services/VestingService' import Time from '../../utils/date/Time' import { ErrorCategory } from '../../utils/errorCategories' import { CoauthorService } from '../services/coauthor' import { UpdateService } from '../services/update' -import { getProposal } from './proposal' - export default routes((route) => { const withAuth = auth() - route.get('/proposals/:proposal/updates', handleAPI(getProposalUpdates)) - route.get('/proposals/:update/update', handleAPI(getProposalUpdate)) - route.get('/proposals/:update_id/update/comments', handleAPI(getProposalUpdateComments)) - route.post('/proposals/:proposal/update', withAuth, handleAPI(createProposalUpdate)) - route.patch('/proposals/:proposal/update', withAuth, handleAPI(updateProposalUpdate)) - route.delete('/proposals/:update_id/update', withAuth, handleAPI(deleteProposalUpdate)) + route.get('/updates/:update_id', handleAPI(getProposalUpdate)) + route.patch('/updates/:update_id', withAuth, handleAPI(updateProposalUpdate)) + route.delete('/updates/:update_id', withAuth, handleAPI(deleteProposalUpdate)) + route.get('/updates/:update_id/comments', handleAPI(getProposalUpdateComments)) }) -async function getProposalUpdate(req: Request<{ update: string }>) { - const id = req.params.update +async function getProposalUpdate(req: Request<{ update_id: string }>) { + const id = req.params.update_id if (!id) { throw new RequestError(`Missing id`, RequestError.NotFound) @@ -61,27 +46,6 @@ async function getProposalUpdate(req: Request<{ update: string }>) { return update } -async function getProposalUpdates(req: Request<{ proposal: string }>) { - const proposal_id = req.params.proposal - - if (!proposal_id) { - throw new RequestError(`Proposal not found: "${proposal_id}"`, RequestError.NotFound) - } - - const updates = await UpdateService.getAllByProposalId(proposal_id) - const publicUpdates = getPublicUpdates(updates) - const nextUpdate = getNextPendingUpdate(updates) - const currentUpdate = getCurrentUpdate(updates) - const pendingUpdates = getPendingUpdates(updates) - - return { - publicUpdates, - pendingUpdates, - nextUpdate, - currentUpdate, - } -} - async function getProposalUpdateComments(req: Request<{ update_id: string }>) { const update = await UpdateService.getById(req.params.update_id) if (!update) { @@ -110,80 +74,19 @@ async function getProposalUpdateComments(req: Request<{ update_id: string }>) { } const generalSectionValidator = schema.compile(GeneralUpdateSectionSchema) - -function parseFinancialRecords(financial_records: unknown) { - const parsedResult = FinancialUpdateSectionSchema.safeParse({ financial_records }) - if (!parsedResult.success) { - ErrorService.report('Submission of invalid financial records', { - error: parsedResult.error, - category: ErrorCategory.Financial, - }) - throw new RequestError(`Invalid financial records`, RequestError.BadRequest) - } - return parsedResult.data.financial_records -} - -async function validateFinancialRecords( - proposal: ProposalAttributes, - financial_records: unknown -): Promise { - const [vestingData, updates] = await Promise.all([ - VestingService.getVestingInfo(proposal.vesting_addresses), - UpdateService.getAllByProposalId(proposal.id), - ]) - - const releases = vestingData ? getReleases(vestingData) : undefined - const publicUpdates = getPublicUpdates(updates) - const latestUpdate = getLatestUpdate(publicUpdates || []) - const { releasedFunds } = getFundsReleasedSinceLatestUpdate(latestUpdate, releases) - return releasedFunds > 0 ? parseFinancialRecords(financial_records) : null -} - -async function createProposalUpdate(req: WithAuth>) { +async function updateProposalUpdate(req: WithAuth>) { + const id = req.params.update_id const { author, financial_records, ...body } = req.body const { health, introduction, highlights, blockers, next_steps, additional_notes } = validate( generalSectionValidator, body ) - try { - const proposal = await getProposal(req) - const financialRecords = await validateFinancialRecords(proposal, financial_records) - return await UpdateService.create( - { - proposal_id: req.params.proposal, - author, - health, - introduction, - highlights, - blockers, - next_steps, - additional_notes, - financial_records: financialRecords, - }, - req.auth! - ) - } catch (error) { - ErrorService.report('Error creating update', { - error, - category: ErrorCategory.Update, - }) - throw new RequestError(`Something wnt wrong: ${error}`, RequestError.InternalServerError) - } -} - -async function updateProposalUpdate(req: WithAuth>) { - const { id, author, financial_records, ...body } = req.body - const { health, introduction, highlights, blockers, next_steps, additional_notes } = validate( - generalSectionValidator, - body - ) const parsedResult = FinancialUpdateSectionSchema.safeParse({ financial_records }) if (!parsedResult.success) { throw new RequestError(`Invalid financial records`, RequestError.BadRequest, parsedResult.error) } const parsedRecords = parsedResult.data.financial_records const update = await UpdateService.getById(id) - const proposalId = req.params.proposal if (!update) { throw new RequestError(`Update not found: "${id}"`, RequestError.NotFound) @@ -191,9 +94,9 @@ async function updateProposalUpdate(req: WithAuth> const user = req.auth - const proposal = await ProposalModel.findOne({ id: req.params.proposal }) + const proposal = await ProposalModel.findOne({ id: update.proposal_id }) const isAuthorOrCoauthor = - user && (proposal?.user === user || (await CoauthorService.isCoauthor(proposalId, user))) && author === user + user && (proposal?.user === user || (await CoauthorService.isCoauthor(update.proposal_id, user))) && author === user if (!proposal || !isAuthorOrCoauthor) { throw new RequestError(`Unauthorized`, RequestError.Forbidden) diff --git a/src/clients/Governance.ts b/src/clients/Governance.ts index 7e0bd7f6d..b8e63ba55 100644 --- a/src/clients/Governance.ts +++ b/src/clients/Governance.ts @@ -250,7 +250,7 @@ export class Governance extends API { } async getProposalUpdate(update_id: string) { - return await this.fetchApiResponse(`/proposals/${update_id}/update`) + return await this.fetchApiResponse(`/updates/${update_id}`) } async getProposalUpdates(proposal_id: string) { @@ -269,10 +269,10 @@ export class Governance extends API { } async updateProposalUpdate( - proposal_id: string, + update_id: string, update: UpdateSubmissionDetails & UpdateGeneralSection & UpdateFinancialSection ) { - return await this.fetchApiResponse(`/proposals/${proposal_id}/update`, { + return await this.fetchApiResponse(`/updates/${update_id}`, { method: 'PATCH', sign: true, json: update, @@ -280,7 +280,7 @@ export class Governance extends API { } async deleteProposalUpdate(update_id: UpdateAttributes['id']) { - return await this.fetchApiResponse(`/proposals/${update_id}/update`, { + return await this.fetchApiResponse(`/updates/${update_id}`, { method: 'DELETE', sign: true, }) @@ -559,7 +559,7 @@ export class Governance extends API { } async getUpdateComments(update_id: string) { - return await this.fetchApiResponse(`/proposals/${update_id}/update/comments`) + return await this.fetchApiResponse(`/updates/${update_id}/comments`) } async airdropBadge(badgeSpecCid: string, recipients: string[]) { diff --git a/src/components/Proposal/Update/ProposalUpdate.css b/src/components/Proposal/Update/ProposalUpdate.css index 5ddae7caf..5a4aaa033 100644 --- a/src/components/Proposal/Update/ProposalUpdate.css +++ b/src/components/Proposal/Update/ProposalUpdate.css @@ -65,6 +65,7 @@ } .ProposalUpdate__Description--expanded { + word-break: break-word; text-overflow: ellipsis; overflow: hidden; display: -webkit-box !important; diff --git a/src/pages/submit/update.tsx b/src/pages/submit/update.tsx index 445f00655..6f9d1af94 100644 --- a/src/pages/submit/update.tsx +++ b/src/pages/submit/update.tsx @@ -156,7 +156,7 @@ export default function Update({ isEdit }: Props) { try { if (updateId) { - await Governance.get().updateProposalUpdate(proposalId, { id: updateId, ...newUpdate }) + await Governance.get().updateProposalUpdate(updateId, newUpdate) if (isEdit) { setIsEditModalOpen(false) } diff --git a/static/api.yaml b/static/api.yaml index 7e1a633a5..2ccc5f2e8 100644 --- a/static/api.yaml +++ b/static/api.yaml @@ -213,7 +213,7 @@ paths: responses: '200': $ref: '#/components/responses/200' - /proposals/{update}/update: + /updates/{update}: get: tags: - Grant Updates @@ -224,6 +224,17 @@ paths: responses: '200': $ref: '#/components/responses/200' + /updates/{update}/comments: + get: + tags: + - Grant Updates + summary: Get the update comments from the given update ID + parameters: + - name: update + $ref: '#/components/parameters/updateId' + responses: + '200': + $ref: '#/components/responses/200' /committee: get: