Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.92% covered (warning)
83.92%
120 / 143
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
TsumegoCommentsController
83.92% covered (warning)
83.92%
120 / 143
0.00% covered (danger)
0.00%
0 / 3
26.40
0.00% covered (danger)
0.00%
0 / 1
 add
77.50% covered (warning)
77.50%
31 / 40
0.00% covered (danger)
0.00%
0 / 1
6.41
 delete
55.17% covered (warning)
55.17%
16 / 29
0.00% covered (danger)
0.00%
0 / 1
11.41
 index
98.65% covered (success)
98.65%
73 / 74
0.00% covered (danger)
0.00%
0 / 1
11
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 */
10class TsumegoCommentsController extends AppController
11{
12    /**
13     * Add a new comment to a tsumego.
14     *
15     * Expects POST data with:
16     * - Comment.tsumego_id: int - The tsumego to comment on
17     * - Comment.message: string - The comment text
18     * - Comment.tsumego_issue_id: int|null - Optional issue to attach to
19     * - Comment.position: string|null - Optional board position
20     * - Comment.redirect: string - URL to redirect after success
21     *
22     * @return CakeResponse|null
23     */
24    public function add()
25    {
26        if (!$this->request->is('post'))
27            throw new MethodNotAllowedException();
28
29        if (!Auth::isLoggedIn())
30        {
31            $this->response->statusCode(401);
32            $this->response->type('json');
33            $this->response->body(json_encode(['error' => 'You must be logged in to comment']));
34            return $this->response;
35        }
36
37        $input = json_decode($this->request->input(), true);
38
39        $TsumegoComment = ClassRegistry::init('TsumegoComment');
40        $comment = [
41            'tsumego_id' => $input['tsumego_id'],
42            'message' => $input['text'],
43            'tsumego_issue_id' => $input['issue_id'] ?? null,
44            'position' => $input['position'] ?? null,
45            'user_id' => Auth::getUserID(),
46        ];
47
48        $TsumegoComment->create();
49        if (!$TsumegoComment->save($comment))
50        {
51            $this->response->statusCode(422);
52            $this->response->type('json');
53            $this->response->body(json_encode(['error' => 'Failed to add comment']));
54            return $this->response;
55        }
56
57        // Get saved comment with user data
58        $savedComment = $TsumegoComment->find('first', [
59            'conditions' => ['TsumegoComment.id' => $TsumegoComment->id],
60            'contain' => ['User']
61        ]);
62
63        $this->response->type('json');
64        $this->response->body(json_encode([
65            'id' => $savedComment['TsumegoComment']['id'],
66            'text' => $savedComment['TsumegoComment']['message'],
67            'user_id' => $savedComment['TsumegoComment']['user_id'],
68            'user_name' => $savedComment['User']['name'] ?? null,
69            'user_picture' => $savedComment['User']['picture'] ?? null,
70            'user_rating' => $savedComment['User']['rating'] ?? null,
71            'user_external_id' => $savedComment['User']['external_id'] ?? null,
72            'isAdmin' => isset($savedComment['User']['isAdmin']) && $savedComment['User']['isAdmin'] ? true : false,
73            'created' => $savedComment['TsumegoComment']['created'],
74            'position' => $savedComment['TsumegoComment']['position'],
75        ]));
76        return $this->response;
77    }
78
79    /**
80     * Delete a comment (soft delete).
81     *
82     * Only the comment author or an admin can delete a comment.
83     * If this was the last comment in an issue, the issue is also deleted.
84     *
85     * @param int $id Comment ID to delete
86     * @return CakeResponse|null
87     */
88    public function delete($id)
89    {
90        if (!$this->request->is('post'))
91            throw new MethodNotAllowedException();
92
93        $TsumegoComment = ClassRegistry::init('TsumegoComment');
94        $comment = $TsumegoComment->findById($id);
95
96        if (!$comment)
97        {
98            $this->response->statusCode(404);
99            $this->response->type('json');
100            $this->response->body(json_encode(['error' => 'Comment not found']));
101            return $this->response;
102        }
103
104        // Only admin or comment author can delete
105        $isOwner = $comment['TsumegoComment']['user_id'] === Auth::getUserID();
106
107        if (!Auth::isAdmin() && !$isOwner)
108        {
109            $this->response->statusCode(403);
110            $this->response->type('json');
111            $this->response->body(json_encode(['error' => 'You are not authorized to delete this comment']));
112            return $this->response;
113        }
114
115        // Remember the issue ID before deleting
116        $issueId = $comment['TsumegoComment']['tsumego_issue_id'];
117
118        // Soft delete
119        $TsumegoComment->id = $id;
120        $saveResult = $TsumegoComment->saveField('deleted', true);
121
122        if (!$saveResult)
123        {
124            $this->response->statusCode(500);
125            $this->response->type('json');
126            $this->response->body(json_encode(['error' => 'Failed to delete comment']));
127            return $this->response;
128        }
129
130        // If comment was part of an issue, check if issue is now empty and delete it
131        if (!empty($issueId))
132        {
133            $TsumegoIssue = ClassRegistry::init('TsumegoIssue');
134            $TsumegoIssue->deleteIfEmpty($issueId);
135        }
136
137        $this->response->type('json');
138        $this->response->body(json_encode(['success' => true]));
139        return $this->response;
140    }
141
142    /**
143     * Get comments data for a tsumego
144     *
145     * Returns issues and standalone comments in the same format as initial SSR data.
146     *
147     * @param int $tsumegoId The tsumego ID
148     * @return CakeResponse
149     */
150    public function index($tsumegoId)
151    {
152        if (!$this->request->is('get'))
153            throw new MethodNotAllowedException();
154
155        $TsumegoIssue = ClassRegistry::init('TsumegoIssue');
156
157        // Load issues with comments
158        $issues = $TsumegoIssue->find('all', [
159            'conditions' => ['TsumegoIssue.tsumego_id' => $tsumegoId],
160            'contain' => [
161                'TsumegoComment' => [
162                    'conditions' => ['TsumegoComment.deleted' => 0],
163                    'User'
164                ],
165                'User'
166            ],
167            'order' => 'TsumegoIssue.created DESC'
168        ]);
169
170        // Load standalone comments
171        $TsumegoComment = ClassRegistry::init('TsumegoComment');
172        $plainComments = $TsumegoComment->find('all', [
173            'conditions' => [
174                'TsumegoComment.tsumego_id' => $tsumegoId,
175                'TsumegoComment.tsumego_issue_id' => null,
176                'TsumegoComment.deleted' => 0
177            ],
178            'contain' => ['User'],
179            'order' => 'TsumegoComment.created DESC'
180        ]);
181
182        $issuesJson = [];
183        foreach ($issues as $issue)
184        {
185            $comments = [];
186            foreach ($issue['TsumegoComment'] as $comment)
187                $comments[] = [
188                    'id' => $comment['id'],
189                    'text' => $comment['message'],
190                    'user_id' => $comment['user_id'],
191                    'user_name' => $comment['User']['name'] ?? null,
192                    'user_picture' => $comment['User']['picture'] ?? null,
193                    'user_rating' => $comment['User']['rating'] ?? null,
194                    'user_external_id' => $comment['User']['externalId'] ?? null,
195                    'isAdmin' => isset($comment['User']) && $comment['User']['isAdmin'] ? true : false,
196                    'created' => $comment['created'],
197                    'position' => $comment['position'],
198                ];
199
200
201            $issuesJson[] = [
202                'id' => $issue['TsumegoIssue']['id'],
203                'tsumego_issue_status_id' => $issue['TsumegoIssue']['tsumego_issue_status_id'],
204                'created' => $issue['TsumegoIssue']['created'],
205                'user_id' => $issue['TsumegoIssue']['user_id'],
206                'user_name' => $issue['User']['name'] ?? null,
207                'user_picture' => $issue['User']['picture'] ?? null,
208                'user_rating' => $issue['User']['rating'] ?? null,
209                'user_external_id' => $issue['User']['externalId'] ?? null,
210                'isAdmin' => isset($issue['User']) && $issue['User']['isAdmin'] ? true : false,
211                'comments' => $comments,
212            ];
213        }
214
215        $standaloneJson = [];
216        foreach ($plainComments as $comment)
217            $standaloneJson[] = [
218                'id' => $comment['TsumegoComment']['id'],
219                'text' => $comment['TsumegoComment']['message'],
220                'user_id' => $comment['TsumegoComment']['user_id'],
221                'user_name' => $comment['User']['name'] ?? null,
222                'user_picture' => $comment['User']['picture'] ?? null,
223                'user_rating' => $comment['User']['rating'] ?? null,
224                'user_external_id' => $comment['User']['externalId'] ?? null,
225                'isAdmin' => isset($comment['User']) && $comment['User']['isAdmin'] ? true : false,
226                'created' => $comment['TsumegoComment']['created'],
227                'position' => $comment['TsumegoComment']['position'],
228            ];
229
230        $counts = $TsumegoIssue->getCommentSectionCounts($tsumegoId);
231
232        $this->response->type('json');
233        $this->response->body(json_encode([
234            'issues' => $issuesJson,
235            'standalone' => $standaloneJson,
236            'counts' => $counts,
237        ]));
238        return $this->response;
239    }
240}