github.com/decred/politeia@v1.4.0/politeiad/backendv2/tstorebe/plugins/pi/proposalstatus.go (about) 1 // Copyright (c) 2021 The Decred developers 2 // Use of this source code is governed by an ISC 3 // license that can be found in the LICENSE file. 4 5 package pi 6 7 import ( 8 "encoding/hex" 9 10 backend "github.com/decred/politeia/politeiad/backendv2" 11 "github.com/decred/politeia/politeiad/plugins/pi" 12 "github.com/decred/politeia/politeiad/plugins/ticketvote" 13 "github.com/pkg/errors" 14 ) 15 16 // getPoposalStatus determines the proposal status at runtime, it uses the 17 // in-memory cache to avoid retrieving the record, it's vote summary or 18 // it's billing status changes when possible. 19 func (p *piPlugin) getProposalStatus(token []byte) (pi.PropStatusT, error) { 20 var ( 21 propStatus pi.PropStatusT 22 err error 23 tokenStr = hex.EncodeToString(token) 24 25 // The following fields are required to determine the proposal status and 26 // MUST be populated. 27 recordState backend.StateT 28 recordStatus backend.StatusT 29 voteStatus ticketvote.VoteStatusT 30 31 // The following fields are required to determine the proposal status and 32 // will only be populated for certain types of proposals or during certain 33 // stages of the proposal lifecycle. 34 voteMetadata *ticketvote.VoteMetadata 35 billingStatuses []pi.BillingStatusChange 36 billingStatusesCount int 37 38 // Declarations to prevent goto errors 39 voteSummary *ticketvote.SummaryReply 40 ) 41 42 // Check if the proposal status has been cached 43 e := p.statuses.get(tokenStr) 44 if e != nil { 45 propStatus = e.propStatus 46 recordState = e.recordState 47 recordStatus = e.recordStatus 48 voteStatus = e.voteStatus 49 voteMetadata = e.voteMetadata 50 billingStatusesCount = e.billingStatusesCount 51 } 52 53 // Check if we need to get any additional data 54 if statusIsFinal(propStatus) { 55 // The status is final and cannot be changed. 56 // No need to get any additional data. 57 return propStatus, nil 58 } 59 60 // Get the record if required 61 if statusRequiresRecord(propStatus) { 62 r, err := p.record(backend.RecordRequest{ 63 Token: token, 64 Filenames: []string{ticketvote.FileNameVoteMetadata}, 65 }) 66 if err != nil { 67 return "", err 68 } 69 70 // Update the record data fields required to 71 // determine the proposal status. 72 recordState = r.RecordMetadata.State 73 recordStatus = r.RecordMetadata.Status 74 75 // Pull the vote metadata out of the record files 76 voteMetadata, err = voteMetadataDecode(r.Files) 77 if err != nil { 78 return "", err 79 } 80 81 // If the proposal is unvetted, no other data is 82 // required in order to determine the status. 83 if recordState == backend.StateUnvetted { 84 goto determineStatus 85 } 86 } 87 88 // If cached vote status is not final, fetch the latest vote status 89 if !voteStatusIsFinal(voteStatus) { 90 voteSummary, err = p.voteSummary(token) 91 if err != nil { 92 return "", err 93 } 94 voteStatus = voteSummary.Status 95 } 96 97 // Get the billing statuses if required 98 if statusRequiresBillingStatuses(voteStatus) { 99 // If the maximum allowed number of billing status changes 100 // have already been made for this proposal and those results 101 // have been cached, then we don't need to retrieve anything 102 // else. The proposal status cannot be changed any further. 103 if uint32(billingStatusesCount) >= p.billingStatusChangesMax { 104 return propStatus, nil 105 } 106 billingStatuses, err = p.billingStatusChanges(token) 107 if err != nil { 108 return "", err 109 } 110 } 111 112 determineStatus: 113 // Determine the proposal status 114 propStatus, err = proposalStatus(recordState, recordStatus, voteStatus, 115 voteMetadata, billingStatuses) 116 if err != nil { 117 return "", nil 118 } 119 120 // Cache the results 121 p.statuses.set(tokenStr, statusEntry{ 122 propStatus: propStatus, 123 recordState: recordState, 124 recordStatus: recordStatus, 125 voteStatus: voteStatus, 126 voteMetadata: voteMetadata, 127 billingStatusesCount: len(billingStatuses), 128 }) 129 130 return propStatus, nil 131 } 132 133 // statusIsFinal returns whether the proposal status is a final status and 134 // cannot be changed any further. 135 func statusIsFinal(s pi.PropStatusT) bool { 136 switch s { 137 case pi.PropStatusUnvettedAbandoned, pi.PropStatusUnvettedCensored, 138 pi.PropStatusAbandoned, pi.PropStatusCensored, pi.PropStatusApproved, 139 pi.PropStatusRejected: 140 return true 141 default: 142 return false 143 } 144 } 145 146 // statusRequiresRecord returns whether the proposal status requires the record 147 // to be retrieved from the backend. This is necessary when the proposal is in 148 // a part of the proposal lifecycle that still allows changes to the underlying 149 // record data. For example, an unvetted proposal may still have it's record 150 // metadata or vote metadata altered, but a proposal with the status of active 151 // cannot. 152 func statusRequiresRecord(s pi.PropStatusT) bool { 153 if statusIsFinal(s) { 154 // The status is final and cannot be changed 155 // any further, which means the record data 156 // is not required. 157 return false 158 } 159 160 switch s { 161 case pi.PropStatusVoteStarted, pi.PropStatusActive, 162 pi.PropStatusCompleted, pi.PropStatusClosed: 163 // The record cannot be changed any further for 164 // these statuses. 165 return false 166 167 case pi.PropStatusUnvetted, pi.PropStatusUnderReview, 168 pi.PropStatusVoteAuthorized: 169 // The record can still change for these statuses. 170 return true 171 172 default: 173 // Defaulting to true is the conservative default 174 // since it will force the record to be retrieved 175 // for unhandled cases. 176 return true 177 } 178 } 179 180 // statusRequiresBillingStatuses returns whether the proposal requires the 181 // billing status changes to be retrieved. This is necessary when the proposal 182 // is in a stage where it's billing status can still change. 183 func statusRequiresBillingStatuses(vs ticketvote.VoteStatusT) bool { 184 switch vs { 185 case ticketvote.VoteStatusUnauthorized, 186 ticketvote.VoteStatusAuthorized, 187 ticketvote.VoteStatusStarted, 188 ticketvote.VoteStatusFinished, 189 ticketvote.VoteStatusRejected, 190 ticketvote.VoteStatusIneligible: 191 // These vote statuses cannot have billing status 192 // changes, so there is not need to retrieve them. 193 return false 194 195 case ticketvote.VoteStatusApproved: 196 // Approved proposals can have billing status 197 // changes. Retrieve them. 198 return true 199 200 default: 201 // Force the billing statuses to be retrieved for any 202 // unhandled cases. 203 return true 204 } 205 } 206 207 // voteStatusIsFinal returns whether the given vote status is final and 208 // cannot be changed any further. 209 func voteStatusIsFinal(vs ticketvote.VoteStatusT) bool { 210 switch vs { 211 case ticketvote.VoteStatusIneligible, 212 ticketvote.VoteStatusFinished, 213 ticketvote.VoteStatusRejected, 214 ticketvote.VoteStatusApproved: 215 return true 216 217 default: 218 return false 219 } 220 } 221 222 // proposalStatusApproved returns the proposal status of an approved proposal. 223 func proposalStatusApproved(voteMD *ticketvote.VoteMetadata, bscs []pi.BillingStatusChange) (pi.PropStatusT, error) { 224 // If the proposal in an RFP then we don't need to 225 // check the billing status changes. RFP proposals 226 // do not bill against the treasury. This does not 227 // apply to RFP submission proposals. 228 if isRFP(voteMD) { 229 return pi.PropStatusApproved, nil 230 } 231 232 // Use the billing status to determine the proposal status. 233 bs := proposalBillingStatus(ticketvote.VoteStatusApproved, bscs) 234 switch bs { 235 case pi.BillingStatusClosed: 236 return pi.PropStatusClosed, nil 237 case pi.BillingStatusCompleted: 238 return pi.PropStatusCompleted, nil 239 case pi.BillingStatusActive: 240 return pi.PropStatusActive, nil 241 } 242 243 // Shouldn't happen return an error 244 return pi.PropStatusInvalid, 245 errors.Errorf( 246 "couldn't determine proposal status of an approved propsoal: "+ 247 "billingStatus: %v", bs) 248 } 249 250 // proposalStatus combines record metadata and plugin metadata in order to 251 // create a unified map of the various paths a proposal can take throughout 252 // the proposal process. 253 func proposalStatus(state backend.StateT, status backend.StatusT, voteStatus ticketvote.VoteStatusT, voteMD *ticketvote.VoteMetadata, bscs []pi.BillingStatusChange) (pi.PropStatusT, error) { 254 switch state { 255 case backend.StateUnvetted: 256 switch status { 257 case backend.StatusUnreviewed: 258 return pi.PropStatusUnvetted, nil 259 case backend.StatusArchived: 260 return pi.PropStatusUnvettedAbandoned, nil 261 case backend.StatusCensored: 262 return pi.PropStatusUnvettedCensored, nil 263 } 264 case backend.StateVetted: 265 switch status { 266 case backend.StatusArchived: 267 return pi.PropStatusAbandoned, nil 268 case backend.StatusCensored: 269 return pi.PropStatusCensored, nil 270 case backend.StatusPublic: 271 switch voteStatus { 272 case ticketvote.VoteStatusUnauthorized: 273 return pi.PropStatusUnderReview, nil 274 case ticketvote.VoteStatusAuthorized: 275 return pi.PropStatusVoteAuthorized, nil 276 case ticketvote.VoteStatusStarted: 277 return pi.PropStatusVoteStarted, nil 278 case ticketvote.VoteStatusRejected: 279 return pi.PropStatusRejected, nil 280 case ticketvote.VoteStatusApproved: 281 return proposalStatusApproved(voteMD, bscs) 282 } 283 } 284 } 285 // Shouldn't happen return an error 286 return pi.PropStatusInvalid, 287 errors.Errorf( 288 "couldn't determine proposal status: proposal state: %v, "+ 289 "proposal status %v, vote status: %v", state, status, voteStatus) 290 }