oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1package sheets
2
3import (
4 "net/http"
5 "strconv"
6
7 "amadan.net/oficina/sheets/internal/sheets/store"
8)
9
10type commentsView struct {
11 Sheet SheetSummary
12 Threads []CommentThread
13 MayComment bool
14 Notice string
15 NoticeTone string `json:"-"`
16}
17
18func commentNotice(code string) string {
19 switch code {
20 case "added":
21 return "Comment added."
22 case "resolved":
23 return "Thread resolved. It stays here, so the conversation is not lost."
24 case "empty":
25 return "A comment needs something in it."
26 case "no-cell":
27 return "That cell is not part of this sheet."
28 default:
29 return ""
30 }
31}
32
33// handleComments lists every thread on a sheet.
34//
35// Reading needs only a viewer's grant: a comment is about the sheet,
36// and somebody who can read the cells can read what was said about
37// them. Writing needs commenter, which is what that role is for.
38func (a *App) handleComments(w http.ResponseWriter, r *http.Request) {
39 s, ok := a.sheetFromURL(w, r)
40 if !ok {
41 return
42 }
43 viewer, ok := a.actor(r)
44 if !ok {
45 a.notFound(w, r)
46 return
47 }
48 h, err := a.openTabFor(s)
49 if err != nil {
50 a.fail(w, r, err)
51 return
52 }
53 defer h.Release()
54
55 threads, err := a.commentThreads(s.TabID, newPosIndex(h))
56 if err != nil {
57 a.fail(w, r, err)
58 return
59 }
60 a.render(w, r, "comments", onSheet(s, "Comments"), http.StatusOK, commentsView{
61 Sheet: s, Threads: threads,
62 MayComment: a.mayComment(s.ID, viewer),
63 Notice: commentNotice(r.URL.Query().Get("notice")),
64 NoticeTone: noticeTone(r.URL.Query().Get("notice")),
65 })
66}
67
68// handleCommentAdd writes a comment about a cell.
69func (a *App) handleCommentAdd(w http.ResponseWriter, r *http.Request) {
70 s, viewer, h, ok := a.commentTarget(w, r)
71 if !ok {
72 return
73 }
74 defer h.Release()
75
76 rowID, colID, ok := a.cellIDsFromForm(r, h)
77 if !ok {
78 a.commentRedirect(w, r, s, "no-cell")
79 return
80 }
81 if err := a.addComment(s.TabID, rowID, colID, r.FormValue("body"), viewer); err != nil {
82 // The body is not echoed. It is user content, and an error
83 // string is a log line waiting to happen (AGENTS.md).
84 a.commentRedirect(w, r, s, "empty")
85 return
86 }
87 a.commentRedirect(w, r, s, "added")
88}
89
90// handleCommentResolve settles a thread.
91func (a *App) handleCommentResolve(w http.ResponseWriter, r *http.Request) {
92 s, viewer, h, ok := a.commentTarget(w, r)
93 if !ok {
94 return
95 }
96 defer h.Release()
97
98 rowID, colID, ok := a.cellIDsFromForm(r, h)
99 if !ok {
100 a.commentRedirect(w, r, s, "no-cell")
101 return
102 }
103 if err := a.resolveThread(s.TabID, rowID, colID, viewer); err != nil {
104 a.fail(w, r, err)
105 return
106 }
107 a.commentRedirect(w, r, s, "resolved")
108}
109
110// commentTarget resolves the sheet, proves the viewer may comment, and
111// opens the tab. The caller must Release the handle.
112func (a *App) commentTarget(w http.ResponseWriter, r *http.Request) (SheetSummary, Member, *store.Handle, bool) {
113 s, ok := a.sheetFromURL(w, r)
114 if !ok {
115 return SheetSummary{}, Member{}, nil, false
116 }
117 viewer, ok := a.actor(r)
118 if !ok {
119 a.notFound(w, r)
120 return SheetSummary{}, Member{}, nil, false
121 }
122 if !a.mayComment(s.ID, viewer) {
123 // 404 like every other refusal: a viewer who cannot comment
124 // learns nothing about who can.
125 a.notFound(w, r)
126 return SheetSummary{}, Member{}, nil, false
127 }
128 h, err := a.openTabFor(s)
129 if err != nil {
130 a.fail(w, r, err)
131 return SheetSummary{}, Member{}, nil, false
132 }
133 return s, viewer, h, true
134}
135
136// cellIDsFromForm turns the r/c a form submitted into stable ids.
137//
138// EnsureRow/EnsureCol, so a comment can be left on a cell nobody has
139// typed into yet — which is a real thing to want ("this column should
140// hold X"). It is also why comments anchor on ids: the row exists from
141// the moment somebody talks about it.
142func (a *App) cellIDsFromForm(r *http.Request, h *store.Handle) (uint64, uint64, bool) {
143 row, err1 := strconv.Atoi(r.FormValue("r"))
144 col, err2 := strconv.Atoi(r.FormValue("c"))
145 if err1 != nil || err2 != nil || row < 0 || col < 0 ||
146 row >= maxWindowRows || col >= tableCols {
147 return 0, 0, false
148 }
149 rowID, err := h.EnsureRow(row)
150 if err != nil {
151 return 0, 0, false
152 }
153 colID, err := h.EnsureCol(col)
154 if err != nil {
155 return 0, 0, false
156 }
157 return rowID, colID, true
158}
159
160func (a *App) commentRedirect(w http.ResponseWriter, r *http.Request, s SheetSummary, notice string) {
161 if nativeNotice(w, r, notice) {
162 return
163 }
164 http.Redirect(w, r, "/sheets/"+strconv.FormatUint(s.ID, 10)+"/comments?notice="+notice,
165 http.StatusSeeOther)
166}
167