render.gno
6.61 Kb · 179 lines
1package contest
2
3import (
4 "strconv"
5 "strings"
6 "time"
7
8 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/core/v1"
9 "gno.land/p/g1lnkytfqcjwllws63gvf0mv9yt04aswy4y9amhm/remixlab/render/v1"
10)
11
12// Render serves the native pages: overview (entries, ballot and result), a
13// single entry, the review queue, members and rules.
14func Render(path string) string {
15 if members == nil {
16 return render.Heading(1, appTitle) + "This app is not initialized.\n"
17 }
18 segs, q := render.ParsePath(path)
19 switch {
20 case len(segs) == 0:
21 return renderHome()
22 case segs[0] == "entries" && len(segs) == 2:
23 id, err := strconv.ParseUint(segs[1], 10, 64)
24 if err != nil {
25 return notFound()
26 }
27 return renderEntry(id)
28 case segs[0] == "queue" && len(segs) == 1:
29 return renderQueue(render.Page(q, render.PageCount(entries.CountByStatus(core.SubmissionSubmitted), pageSize)))
30 case segs[0] == "members" && len(segs) == 1:
31 return renderMembers(render.Page(q, render.PageCount(members.Size(), pageSize)))
32 case segs[0] == "rules" && len(segs) == 1:
33 return renderRules()
34 }
35 return notFound()
36}
37
38func renderHome() string {
39 var sb strings.Builder
40 sb.WriteString(render.Heading(1, appTitle))
41 v, voting := Ballot()
42 now := time.Now().Unix()
43 switch {
44 case !voting:
45 sb.WriteString("**Taking entries.** Reviewers admit up to " + strconv.Itoa(maxEntries) + " entries; then voting opens on the admitted entries.\n\n")
46 sb.WriteString(render.TxLink("Submit an entry", pkgPath, "Submit") + " · " +
47 render.Link("Review queue", render.RealmURL(pkgPath, "queue")) + " (" + strconv.Itoa(entries.CountByStatus(core.SubmissionSubmitted)) + ") · " +
48 render.TxLink("Open voting", pkgPath, "OpenVoting") + "\n\n")
49 case v.Status == core.BallotOpen && now < v.Deadline:
50 sb.WriteString("**Voting is open** until " + render.Text(time.Unix(v.Deadline, 0).UTC().Format(time.RFC3339)) +
51 " · electorate " + strconv.Itoa(v.ElectorateSize) + " · ballots " + strconv.Itoa(v.Turnout) + "\n\n")
52 case v.Status == core.BallotOpen:
53 sb.WriteString("**Voting ended, awaiting finalization.** " + render.TxLink("Finalize result", pkgPath, "Finalize") + "\n\n")
54 default:
55 sb.WriteString("**Result: " + render.Text(resultLabel(v)) + "** · ballots " + strconv.Itoa(v.Turnout) + "\n\n")
56 }
57
58 var rows [][]string
59 for i, e := range Entries() {
60 idStr := strconv.FormatUint(e.ID, 10)
61 row := []string{
62 render.Link("#"+idStr, render.RealmURL(pkgPath, "entries/"+idStr)),
63 render.Text(e.Answers[titleIndex]),
64 }
65 if voting {
66 row = append(row, strconv.Itoa(v.Counts[i]))
67 if v.Status == core.BallotOpen && now < v.Deadline {
68 row = append(row, render.TxLink("vote", pkgPath, "Vote", "entryID", idStr))
69 } else {
70 row = append(row, "")
71 }
72 }
73 rows = append(rows, render.TrustedRow(row...))
74 }
75 headers := []string{"Entry", "Title"}
76 if voting {
77 headers = append(headers, "Votes", "")
78 }
79 if len(rows) == 0 {
80 sb.WriteString("No admitted entries yet.\n\n")
81 } else {
82 sb.WriteString(render.Table(headers, rows))
83 }
84
85 sb.WriteString("- " + render.Link("Members", render.RealmURL(pkgPath, "members")) + " (" + strconv.Itoa(members.Size()) + ") · " +
86 render.TxLink("Join (requires an invitation)", pkgPath, "Join") + "\n")
87 sb.WriteString("- " + render.Link("Rules", render.RealmURL(pkgPath, "rules")) + "\n\n")
88 sb.WriteString("_Recipe " + render.Text(recipeID+"@"+recipeVersion) + ". Membership is address-based; one address is not one person._\n")
89 return sb.String()
90}
91
92func renderQueue(page int) string {
93 list := entries.Submissions(int(core.SubmissionSubmitted), (page-1)*pageSize, pageSize)
94 if len(list) == 0 {
95 return render.Heading(1, "Review queue") + "Nothing to review.\n"
96 }
97 var rows [][]string
98 for _, e := range list {
99 idStr := strconv.FormatUint(e.ID, 10)
100 rows = append(rows, render.TrustedRow(
101 render.Link("#"+idStr, render.RealmURL(pkgPath, "entries/"+idStr)),
102 render.Text(e.Answers[titleIndex]),
103 reviewLinks(idStr),
104 ))
105 }
106 return render.Heading(1, "Review queue") +
107 render.Table([]string{"Entry", "Title", "Review"}, rows) +
108 render.Pager(pkgPath, "queue", page, render.PageCount(entries.CountByStatus(core.SubmissionSubmitted), pageSize))
109}
110
111func renderEntry(id uint64) string {
112 v, ok := entries.Submission(id)
113 if !ok {
114 return notFound()
115 }
116 idStr := strconv.FormatUint(id, 10)
117 if v.Status == core.SubmissionRejected {
118 return render.Heading(1, "Entry #"+idStr) + "This entry was rejected.\n\n" +
119 render.Link("Back to the contest", render.RealmURL(pkgPath, "")) + "\n"
120 }
121 status := "admitted"
122 if v.Status == core.SubmissionSubmitted {
123 status = "awaiting review"
124 }
125 var sb strings.Builder
126 sb.WriteString(render.Heading(1, v.Answers[titleIndex]))
127 sb.WriteString("Status: **" + status + "** · submitted by " + render.Code(v.Submitter.String()) + "\n\n")
128 var rows [][]string
129 for i, f := range entries.Fields() {
130 if i != titleIndex && v.Answers[i] != "" {
131 rows = append(rows, render.Row(f.Label, v.Answers[i]))
132 }
133 }
134 if len(rows) > 0 {
135 sb.WriteString(render.Table([]string{"Field", "Answer"}, rows))
136 }
137 if v.Status == core.SubmissionSubmitted && ballotID == 0 {
138 sb.WriteString("Review: " + reviewLinks(idStr) + "\n\n")
139 }
140 sb.WriteString(render.Link("Back to the contest", render.RealmURL(pkgPath, "")) + "\n")
141 return sb.String()
142}
143
144func reviewLinks(idStr string) string {
145 return render.TxLink("admit", pkgPath, "Accept", "id", idStr) + " · " +
146 render.TxLink("reject", pkgPath, "Reject", "id", idStr)
147}
148
149func renderMembers(page int) string {
150 var rows [][]string
151 for _, m := range members.List((page-1)*pageSize, pageSize) {
152 rows = append(rows, render.Row(m.Address.String(), m.Role.String()))
153 }
154 return render.Heading(1, "Members") +
155 render.Table([]string{"Address", "Role"}, rows) +
156 render.Pager(pkgPath, "members", page, render.PageCount(members.Size(), pageSize))
157}
158
159func renderRules() string {
160 var rows [][]string
161 for _, r := range policy.List() {
162 rows = append(rows, render.Row(r.Op, r.Rule.String(), r.Floor.String()))
163 }
164 rows = append(rows, render.Row("contest.vote", "electorate frozen when voting opens (members)", "-"))
165 return render.Heading(1, "Rules") +
166 render.Table([]string{"Operation", "Who may", "Most permissive allowed"}, rows) +
167 "Up to 50 pending or admitted submissions, at most 1 per address; at most 10 admitted entries. Review decisions are final; rejecting an entry frees its slot. A tie has no winner." + "\n"
168}
169
170func resultLabel(v core.BallotView) string {
171 if w, ok := Winner(); ok {
172 return "winner: " + v.Options[v.Winner] + " (entry " + strconv.FormatUint(w, 10) + ")"
173 }
174 return v.Outcome.String()
175}
176
177func notFound() string {
178 return render.Heading(1, "Not found") + render.Link("Back to the app", render.RealmURL(pkgPath, "")) + "\n"
179}