Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
68.33% covered (warning)
68.33%
41 / 60
33.33% covered (danger)
33.33%
1 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
TsumegoCommentsController
68.33% covered (warning)
68.33%
41 / 60
33.33% covered (danger)
33.33%
1 / 3
16.57
0.00% covered (danger)
0.00%
0 / 1
 add
60.87% covered (warning)
60.87%
14 / 23
0.00% covered (danger)
0.00%
0 / 1
4.96
 delete
58.33% covered (warning)
58.33%
14 / 24
0.00% covered (danger)
0.00%
0 / 1
10.54
 _renderCommentsSection
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3/**
4 * Controller for managing tsumego comments (CRUD operations).
5 *
6 * Handles adding and deleting comments on tsumego problems.
7 * Comments can be standalone or associated with a TsumegoIssue.
8 *
9 * Uses idiomorph for React-like DOM diffing - htmx responses return the full
10 * comments section and idiomorph handles efficient DOM updates.
11 *
12 * This controller is htmx-only - all responses return HTML fragments for htmx.
13 */
14class TsumegoCommentsController extends AppController
15{
16    /**
17     * Add a new comment to a tsumego.
18     *
19     * Expects POST data with:
20     * - Comment.tsumego_id: int - The tsumego to comment on
21     * - Comment.message: string - The comment text
22     * - Comment.tsumego_issue_id: int|null - Optional issue to attach to
23     * - Comment.position: string|null - Optional board position
24     * - Comment.redirect: string - URL to redirect after success
25     *
26     * @return CakeResponse|null
27     */
28    public function add()
29    {
30        if (!$this->request->is('post'))
31            throw new MethodNotAllowedException();
32
33        if (!Auth::isLoggedIn())
34        {
35            $this->response->statusCode(401);
36            $this->response->body('You must be logged in to comment.');
37            return $this->response;
38        }
39
40        $TsumegoComment = ClassRegistry::init('TsumegoComment');
41        $tsumegoId = $this->request->data('Comment.tsumego_id');
42        $comment = [
43            'tsumego_id' => $tsumegoId,
44            'message' => $this->request->data('Comment.message'),
45            'tsumego_issue_id' => $this->request->data('Comment.tsumego_issue_id'),
46            'position' => $this->request->data('Comment.position'),
47            'user_id' => Auth::getUserID(),
48        ];
49
50        $TsumegoComment->create();
51        if (!$TsumegoComment->save($comment))
52        {
53            $this->response->statusCode(422);
54            $this->layout = false;
55            $this->autoRender = false;
56            $this->response->body('<div class="alert alert--error">Failed to add comment.</div>');
57            return $this->response;
58        }
59
60        // Return the full comments section (idiomorph handles the diff)
61        return $this->_renderCommentsSection($tsumegoId);
62    }
63
64    /**
65     * Delete a comment (soft delete).
66     *
67     * Only the comment author or an admin can delete a comment.
68     * If this was the last comment in an issue, the issue is also deleted.
69     *
70     * @param int $id Comment ID to delete
71     * @return CakeResponse|null
72     */
73    public function delete($id)
74    {
75        if (!$this->request->is('post'))
76            throw new MethodNotAllowedException();
77
78        $TsumegoComment = ClassRegistry::init('TsumegoComment');
79        $comment = $TsumegoComment->findById($id);
80
81        if (!$comment)
82        {
83            $this->response->statusCode(404);
84            $this->response->body('Comment not found.');
85            return $this->response;
86        }
87
88        // Only admin or comment author can delete
89        $isOwner = $comment['TsumegoComment']['user_id'] === Auth::getUserID();
90        if (!Auth::isAdmin() && !$isOwner)
91        {
92            $this->response->statusCode(403);
93            $this->response->body('You are not authorized to delete this comment.');
94            return $this->response;
95        }
96
97        // Remember the issue ID and tsumego ID before deleting
98        $issueId = $comment['TsumegoComment']['tsumego_issue_id'];
99        $tsumegoId = $comment['TsumegoComment']['tsumego_id'];
100
101        // Soft delete
102        $TsumegoComment->id = $id;
103        if (!$TsumegoComment->saveField('deleted', true))
104        {
105            $this->response->statusCode(500);
106            $this->response->body('Failed to delete comment.');
107            return $this->response;
108        }
109
110        // If comment was part of an issue, check if issue is now empty and delete it
111        if (!empty($issueId))
112        {
113            $TsumegoIssue = ClassRegistry::init('TsumegoIssue');
114            $TsumegoIssue->deleteIfEmpty($issueId);
115        }
116
117        // Return the full comments section (idiomorph handles the diff)
118        return $this->_renderCommentsSection($tsumegoId);
119    }
120
121    /**
122     * Render the morphable comments section content for htmx morph responses.
123     *
124     * Loads all comments data and renders just the inner content element.
125     *
126     * @param int $tsumegoId The tsumego ID
127     * @return CakeResponse
128     */
129    protected function _renderCommentsSection(int $tsumegoId): CakeResponse
130    {
131        $Tsumego = ClassRegistry::init('Tsumego');
132        $TsumegoIssue = ClassRegistry::init('TsumegoIssue');
133
134        $commentsData = $Tsumego->loadCommentsData($tsumegoId);
135        $counts = $TsumegoIssue->getCommentSectionCounts($tsumegoId);
136
137        $this->set('tsumegoId', $tsumegoId);
138        $this->set('issues', $commentsData['issues']);
139        $this->set('plainComments', $commentsData['plainComments']);
140        $this->set('totalCount', $counts['total']);
141        $this->set('commentCount', $counts['comments']);
142        $this->set('issueCount', $counts['issues']);
143        $this->set('openIssueCount', $counts['openIssues']);
144
145        $this->layout = false;
146        return $this->render('/Elements/TsumegoComments/section-content');
147    }
148}