Skip to content

Commit 0b3afba

Browse files
committed
CAS-647: Add Quorum Voting
1 parent 7c7962b commit 0b3afba

File tree

16 files changed

+185
-52
lines changed

16 files changed

+185
-52
lines changed

backend/main/models/proposal.go

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ type Proposal struct {
4242
Voucher *shared.Voucher `json:"voucher,omitempty"`
4343
Achievements_done bool `json:"achievementsDone"`
4444
TallyMethod string `json:"voteType" validate:"required"`
45+
Quorum *float64 `json:"quorum,omitempty"`
4546
}
4647

4748
type UpdateProposalRequestPayload struct {
@@ -146,9 +147,10 @@ func (p *Proposal) CreateProposal(db *s.Database) error {
146147
cid,
147148
composite_signatures,
148149
voucher,
149-
tally_method
150+
tally_method,
151+
quorum
150152
)
151-
VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16)
153+
VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
152154
RETURNING id, created_at
153155
`,
154156
p.Community_id,
@@ -167,6 +169,7 @@ func (p *Proposal) CreateProposal(db *s.Database) error {
167169
p.Composite_signatures,
168170
p.Voucher,
169171
p.TallyMethod,
172+
p.Quorum,
170173
).Scan(&p.ID, &p.Created_at)
171174

172175
return err
@@ -210,17 +213,19 @@ func (p *Proposal) UpdateDraftProposal(db *s.Database) error {
210213
strategy = COALESCE($3, strategy),
211214
min_balance = COALESCE($4, min_balance),
212215
max_weight = COALESCE($5, max_weight),
213-
start_time = COALESCE($6, start_time),
214-
end_time = COALESCE($7, end_time),
215-
body = COALESCE($8, body),
216-
block_height = COALESCE($9, block_height),
217-
cid = COALESCE($10, cid)
218-
WHERE id = $11
216+
quorum = COALESCE($6, quorum),
217+
start_time = COALESCE($7, start_time),
218+
end_time = COALESCE($8, end_time),
219+
body = COALESCE($9, body),
220+
block_height = COALESCE($10, block_height),
221+
cid = COALESCE($11, cid)
222+
WHERE id = $12
219223
`, p.Name,
220224
p.Choices,
221225
p.Strategy,
222226
p.Min_balance,
223227
p.Max_weight,
228+
p.Quorum,
224229
p.Start_time,
225230
p.End_time,
226231
p.Body,

backend/main/server/controllers.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,6 @@ func (a *App) createProposal(w http.ResponseWriter, r *http.Request) {
398398
var p models.Proposal
399399
p.Community_id = communityId
400400

401-
402401
if err := validatePayload(r.Body, &p); err != nil {
403402
log.Error().Err(err).Msg("Error validating payload")
404403
respondWithError(w, errIncompleteRequest)

backend/main/server/helpers.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -606,7 +606,7 @@ func (h *Helpers) createProposal(p models.Proposal) (models.Proposal, errorRespo
606606

607607
canUserCreateProposal := community.CanUserCreateProposal(h.A.DB, h.A.FlowAdapter, p.Creator_addr)
608608

609-
if err := handlePermissionErrorr(canUserCreateProposal); err != nilErr {
609+
if err := handlePermissionError(canUserCreateProposal); err != nilErr {
610610
return models.Proposal{}, err
611611
}
612612

@@ -652,7 +652,7 @@ func (h *Helpers) createDraftProposal(c models.Community, p models.Proposal) (mo
652652
p.Creator_addr,
653653
)
654654

655-
if err := handlePermissionErrorr(canUserCreateProposal); err != nilErr {
655+
if err := handlePermissionError(canUserCreateProposal); err != nilErr {
656656
return models.Proposal{}, err
657657
}
658658

@@ -729,7 +729,7 @@ func (h *Helpers) updateDraftProposal(p models.Proposal) (models.Proposal, error
729729
p.Creator_addr,
730730
)
731731

732-
if err := handlePermissionErrorr(canUserCreateProposal); err != nilErr {
732+
if err := handlePermissionError(canUserCreateProposal); err != nilErr {
733733
return models.Proposal{}, err
734734
}
735735

@@ -742,7 +742,7 @@ func (h *Helpers) updateDraftProposal(p models.Proposal) (models.Proposal, error
742742
return p, nilErr
743743
}
744744

745-
func handlePermissionErrorr(result models.CanUserCreateProposalResponse) errorResponse {
745+
func handlePermissionError(result models.CanUserCreateProposalResponse) errorResponse {
746746
// If user doesn't have permission, populate errorResponse
747747
// with reason and error.
748748
if !result.HasPermission {
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE proposals DROP COLUMN IF EXISTS quorum;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE proposals ADD COLUMN quorum float;

frontend/packages/client/src/api/proposals.js

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
API_BASE_URL,
33
COMMUNITIES_URL,
4-
IPFS_GETWAY,
4+
IPFS_GATEWAY,
55
PROPOSALS_URL,
66
} from './constants';
77
import { checkResponse } from 'utils';
@@ -20,10 +20,25 @@ export const fetchProposal = async ({ proposalId }) => {
2020
const response = await fetch(url);
2121
const proposal = await checkResponse(response);
2222

23-
const sortedProposalChoices =
23+
let sortedProposalChoices =
2424
proposal.choices?.sort((a, b) => (a.choiceText > b.choiceText ? 1 : -1)) ??
2525
[];
2626

27+
if (proposal.voteType === 'basic') {
28+
// Ensure choices are ordered as For/Against/Abstain for basic voting
29+
const choices = [...sortedProposalChoices];
30+
sortedProposalChoices.forEach((choice) => {
31+
let index = 0;
32+
if (choice.choiceText === 'Against') {
33+
index = 1;
34+
} else if (choice.choiceText === 'Abstain') {
35+
index = 2;
36+
}
37+
choices[index] = choice;
38+
});
39+
sortedProposalChoices = choices;
40+
}
41+
2742
const proposalData = {
2843
...proposal,
2944
choices: sortedProposalChoices.map((choice) => ({
@@ -32,7 +47,7 @@ export const fetchProposal = async ({ proposalId }) => {
3247
choiceImgUrl: choice.choiceImgUrl,
3348
})),
3449
ipfs: proposal.cid,
35-
ipfsUrl: `${IPFS_GETWAY}/${proposal.cid}`,
50+
ipfsUrl: `${IPFS_GATEWAY}/${proposal.cid}`,
3651
totalVotes: proposal.total_votes,
3752
// this is coming as a string from db but there could be multiple based on design
3853
strategy: proposal.strategy || '-',
@@ -48,6 +63,15 @@ export const createProposalApiReq = async ({
4863
timestamp,
4964
} = {}) => {
5065
const { communityId, ...proposalData } = proposalPayload;
66+
67+
if (proposalData.voteType === 'basic') {
68+
proposalData.choices = [
69+
{ choiceText: 'For', choiceImgUrl: null },
70+
{ choiceText: 'Against', choiceImgUrl: null },
71+
{ choiceText: 'Abstain', choiceImgUrl: null },
72+
];
73+
}
74+
5175
const url = `${COMMUNITIES_URL}/${communityId}/proposals`;
5276
const fetchOptions = {
5377
method: 'POST',

frontend/packages/client/src/components/Proposal/VoteHeader.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ const Wrapper = ({ children }) => (
99
</h3>
1010
);
1111

12-
export default function VoteHeader({ status }) {
12+
export default function VoteHeader({ status, voteType = 'single-choice' }) {
1313
// Status: user-voted, invite-to-vote, is-closed
1414
const message = {
1515
'user-voted': (
@@ -20,7 +20,12 @@ export default function VoteHeader({ status }) {
2020
You successfully voted on this proposal!
2121
</div>
2222
),
23-
'invite-to-vote': <>Rank your vote &#10024;</>,
23+
'invite-to-vote': (
24+
<>
25+
{voteType === 'single-choice' ? 'Cast your vote' : 'Rank your vote'}{' '}
26+
&#10024;
27+
</>
28+
),
2429
'is-closed': <>Voting has ended on this proposal.</>,
2530
};
2631

frontend/packages/client/src/components/ProposalCreate/FormConfig.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,21 @@ const StepTwoSchema = yup.object().shape({
4646
})
4747
),
4848
})
49-
.when('voteType', (voteType, schema) =>
50-
voteType === 'single-choice'
49+
.when('voteType', (voteType, schema) => {
50+
if (voteType === 'basic') return;
51+
return voteType === 'single-choice'
5152
? schema.min(2, 'Please add a choice, minimum amount is two')
52-
: schema.min(3, 'Please add a choice, minimum amount is three')
53-
)
53+
: schema.min(3, 'Please add a choice, minimum amount is three');
54+
})
5455
.unique('value', 'Invalid duplicated option'),
56+
quorum: yup
57+
.string()
58+
.trim()
59+
.matches(/\s+$|^$|(^[0-9]+$)/, 'Quorum threshold must be a valid number')
60+
.when('voteType', (voteType, schema) => {
61+
if (voteType !== 'basic') return;
62+
return schema.required('Quorum threshold is required');
63+
}),
5564
maxWeight: yup
5665
.string()
5766
.trim()

frontend/packages/client/src/components/ProposalCreate/Preview.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const Preview = ({ stepsData }) => {
3333
}))
3434
: null,
3535
};
36+
console.log(proposal.choices);
3637
return (
3738
<div>
3839
<h1 className="title mt-5 is-3">{name}</h1>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
export default function BasicVoteExample() {
2+
return (
3+
<>
4+
{['For', 'Against', 'Abstain'].map((text, index) => (
5+
<div
6+
className="is-flex is-align-items-center is-justify-content-left mb-1"
7+
style={{ whiteSpace: 'nowrap', width: 75 }}
8+
key={index}
9+
>
10+
<div
11+
className="rounded-full has-background-grey has-text-white mr-2 is-flex is-align-items-center is-justify-content-center"
12+
style={{ width: 12, height: 12 }}
13+
>
14+
{index === 0 ? (
15+
<span style={{ fontSize: 7, paddingTop: 1 }}>&#x2713;</span>
16+
) : null}
17+
</div>
18+
<span className="smaller-text">{text}</span>
19+
</div>
20+
))}
21+
</>
22+
);
23+
}

0 commit comments

Comments
 (0)