rcomments 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rcomments-0.1.2/LICENSE +21 -0
- rcomments-0.1.2/MANIFEST.in +6 -0
- rcomments-0.1.2/PKG-INFO +471 -0
- rcomments-0.1.2/README.md +408 -0
- rcomments-0.1.2/pyproject.toml +61 -0
- rcomments-0.1.2/rcomments/__init__.py +28 -0
- rcomments-0.1.2/rcomments/cli.py +134 -0
- rcomments-0.1.2/rcomments/config.py +80 -0
- rcomments-0.1.2/rcomments/models.py +139 -0
- rcomments-0.1.2/rcomments/routes.py +360 -0
- rcomments-0.1.2/rcomments/utils.py +131 -0
- rcomments-0.1.2/rcomments.egg-info/PKG-INFO +471 -0
- rcomments-0.1.2/rcomments.egg-info/SOURCES.txt +20 -0
- rcomments-0.1.2/rcomments.egg-info/dependency_links.txt +1 -0
- rcomments-0.1.2/rcomments.egg-info/entry_points.txt +2 -0
- rcomments-0.1.2/rcomments.egg-info/requires.txt +17 -0
- rcomments-0.1.2/rcomments.egg-info/top_level.txt +1 -0
- rcomments-0.1.2/setup.cfg +4 -0
- rcomments-0.1.2/tests/__init__.py +1 -0
- rcomments-0.1.2/tests/conftest.py +120 -0
- rcomments-0.1.2/tests/test_models.py +190 -0
- rcomments-0.1.2/tests/test_routes.py +274 -0
rcomments-0.1.2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 RComments Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
rcomments-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: rcomments
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: A minimal Flask + PostgreSQL commenting system with hierarchical threading, soft deletion, rate limiting, and XSS protection
|
|
5
|
+
Author-email: Rahul Naik <rnnutube@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2024 RComments Contributors
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com
|
|
29
|
+
Project-URL: Repository, https://github.com
|
|
30
|
+
Project-URL: Issues, https://github.com
|
|
31
|
+
Keywords: flask,postgresql,comments,forum,discussion
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
38
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
39
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
40
|
+
Classifier: Framework :: Flask
|
|
41
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
42
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
43
|
+
Requires-Python: >=3.8
|
|
44
|
+
Description-Content-Type: text/markdown
|
|
45
|
+
License-File: LICENSE
|
|
46
|
+
Requires-Dist: Flask>=2.0.0
|
|
47
|
+
Requires-Dist: Flask-SQLAlchemy>=3.0.0
|
|
48
|
+
Requires-Dist: Flask-Migrate>=4.0.0
|
|
49
|
+
Requires-Dist: psycopg2-binary>=2.9.0
|
|
50
|
+
Requires-Dist: python-dotenv>=0.21.0
|
|
51
|
+
Requires-Dist: email-validator>=2.0.0
|
|
52
|
+
Requires-Dist: bleach>=6.0.0
|
|
53
|
+
Requires-Dist: limits>=3.0.0
|
|
54
|
+
Requires-Dist: flask-limiter>=3.0.0
|
|
55
|
+
Provides-Extra: dev
|
|
56
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
57
|
+
Requires-Dist: pytest-flask>=1.2.0; extra == "dev"
|
|
58
|
+
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
59
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
|
60
|
+
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
|
61
|
+
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
62
|
+
Dynamic: license-file
|
|
63
|
+
|
|
64
|
+
# RComments
|
|
65
|
+
|
|
66
|
+
A minimal, secure, and feature-rich commenting system for Flask + PostgreSQL applications.
|
|
67
|
+
|
|
68
|
+
## Features
|
|
69
|
+
|
|
70
|
+
- **Hierarchical Threading**: Support for nested replies with unlimited depth
|
|
71
|
+
- **Flexible Identity**: Registered usernames or anonymous commenting
|
|
72
|
+
- **Metadata Tracking**: Automatic UTC timestamps and IP logging for moderation
|
|
73
|
+
- **XSS Protection**: HTML sanitization to prevent injection attacks
|
|
74
|
+
- **Rate Limiting**: Configurable limits to prevent spam
|
|
75
|
+
- **Soft Deletion**: Comments can be marked as deleted without permanent removal
|
|
76
|
+
- **Automatic Cleanup**: Permanently removes old comments (30 days or 150 comments limit)
|
|
77
|
+
- **Easy Integration**: Simple Flask blueprint registration
|
|
78
|
+
|
|
79
|
+
## Installation
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install rcomments
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
## Quick Start
|
|
88
|
+
|
|
89
|
+
### 1. Configuration
|
|
90
|
+
|
|
91
|
+
Create a `.env` file in your project root:
|
|
92
|
+
|
|
93
|
+
```env
|
|
94
|
+
DATABASE_URL=postgresql://username:password@localhost/yourdb
|
|
95
|
+
SECRET_KEY=your-flask-secret-key
|
|
96
|
+
RATE_LIMIT=10 per minute
|
|
97
|
+
MAX_COMMENT_AGE_DAYS=30
|
|
98
|
+
MAX_TOTAL_COMMENTS=150
|
|
99
|
+
ALLOW_ANONYMOUS=true
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 2. Flask Application Setup
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from flask import Flask
|
|
106
|
+
from flask_sqlalchemy import SQLAlchemy
|
|
107
|
+
from flask_limiter import Limiter
|
|
108
|
+
from rcomments import init_config, init_routes, db as rcomments_db
|
|
109
|
+
|
|
110
|
+
app = Flask(__name__)
|
|
111
|
+
app.config.from_prefixed_env() # Loads from .env
|
|
112
|
+
|
|
113
|
+
# Initialize RComments
|
|
114
|
+
init_config()
|
|
115
|
+
|
|
116
|
+
# Setup database (use your existing SQLAlchemy instance or create a new one)
|
|
117
|
+
db = SQLAlchemy(app)
|
|
118
|
+
rcomments_db.init_app(app)
|
|
119
|
+
|
|
120
|
+
# Setup rate limiter
|
|
121
|
+
limiter = Limiter(
|
|
122
|
+
app,
|
|
123
|
+
key_func=lambda: request.headers.get("X-Forwarded-For", request.remote_addr)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Initialize routes
|
|
127
|
+
init_routes(app, limiter)
|
|
128
|
+
|
|
129
|
+
if __name__ == "__main__":
|
|
130
|
+
app.run(debug=True)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 3. Database Migration
|
|
134
|
+
|
|
135
|
+
Initialize the database:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
# Using Flask-Migrate (recommended)
|
|
139
|
+
flask db init
|
|
140
|
+
flask db migrate -m "Initial RComments tables"
|
|
141
|
+
flask db upgrade
|
|
142
|
+
|
|
143
|
+
# Or using the CLI
|
|
144
|
+
rcomments-cli init-db
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## API Endpoints
|
|
148
|
+
|
|
149
|
+
All endpoints are prefixed with `/api/comments`.
|
|
150
|
+
|
|
151
|
+
### GET `/api/comments`
|
|
152
|
+
|
|
153
|
+
Fetch all top-level comments with nested replies.
|
|
154
|
+
|
|
155
|
+
**Query Parameters:**
|
|
156
|
+
- `sort` (optional): `asc` or `desc` (default: `desc`)
|
|
157
|
+
- `limit` (optional): Maximum number of top-level comments
|
|
158
|
+
|
|
159
|
+
**Response:**
|
|
160
|
+
```json
|
|
161
|
+
{
|
|
162
|
+
"success": true,
|
|
163
|
+
"data": [
|
|
164
|
+
{
|
|
165
|
+
"id": 1,
|
|
166
|
+
"content": "This is a comment",
|
|
167
|
+
"author_name": "John",
|
|
168
|
+
"is_anonymous": false,
|
|
169
|
+
"status": "ACTIVE",
|
|
170
|
+
"created_at": "2024-01-15T10:30:00Z",
|
|
171
|
+
"parent_id": null,
|
|
172
|
+
"replies": []
|
|
173
|
+
}
|
|
174
|
+
],
|
|
175
|
+
"count": 1
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### POST `/api/comments`
|
|
180
|
+
|
|
181
|
+
Create a new comment.
|
|
182
|
+
|
|
183
|
+
**Request Body:**
|
|
184
|
+
```json
|
|
185
|
+
{
|
|
186
|
+
"content": "Your comment text",
|
|
187
|
+
"author_name": "Your Name",
|
|
188
|
+
"author_email": "email@example.com", // optional
|
|
189
|
+
"parent_id": 1, // optional, for replies
|
|
190
|
+
"is_anonymous": false // optional, default: false
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**Response:**
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"success": true,
|
|
198
|
+
"data": {
|
|
199
|
+
"id": 2,
|
|
200
|
+
"content": "Your comment text",
|
|
201
|
+
"author_name": "Your Name",
|
|
202
|
+
"is_anonymous": false,
|
|
203
|
+
"status": "ACTIVE",
|
|
204
|
+
"created_at": "2024-01-15T10:35:00Z",
|
|
205
|
+
"parent_id": 1
|
|
206
|
+
},
|
|
207
|
+
"message": "Comment created successfully"
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### GET `/api/comments/<id>`
|
|
212
|
+
|
|
213
|
+
Fetch a single comment with its replies.
|
|
214
|
+
|
|
215
|
+
### DELETE `/api/comments/<id>`
|
|
216
|
+
|
|
217
|
+
Soft delete a comment (marks as `[Comment removed]`).
|
|
218
|
+
|
|
219
|
+
### GET `/api/comments/<id>/replies`
|
|
220
|
+
|
|
221
|
+
Fetch direct replies for a comment.
|
|
222
|
+
|
|
223
|
+
### GET `/api/comments/stats`
|
|
224
|
+
|
|
225
|
+
Get comment statistics.
|
|
226
|
+
|
|
227
|
+
### POST `/api/comments/cleanup`
|
|
228
|
+
|
|
229
|
+
Trigger cleanup of old comments (admin only - secure this in production!).
|
|
230
|
+
|
|
231
|
+
**Query Parameters:**
|
|
232
|
+
- `dry_run=true` - Preview what would be deleted
|
|
233
|
+
|
|
234
|
+
## Security Features
|
|
235
|
+
|
|
236
|
+
### XSS Protection
|
|
237
|
+
|
|
238
|
+
All comment content is automatically sanitized using `bleach`. Only basic formatting tags are allowed:
|
|
239
|
+
- `<b>`, `<i>`, `<u>`, `<em>`, `<strong>`, `<br>`, `<p>`
|
|
240
|
+
|
|
241
|
+
### Rate Limiting
|
|
242
|
+
|
|
243
|
+
Configurable rate limits prevent automated spamming. Default: 10 comments per minute per IP.
|
|
244
|
+
|
|
245
|
+
### IP Logging
|
|
246
|
+
|
|
247
|
+
IP addresses are hashed (SHA-256) for privacy while maintaining moderation capabilities.
|
|
248
|
+
|
|
249
|
+
### Soft Deletion
|
|
250
|
+
|
|
251
|
+
Comments can be soft-deleted via the DELETE endpoint, which marks them with status `DELETED` and replaces content with `[Comment removed]`. This preserves thread structure while hiding content.
|
|
252
|
+
|
|
253
|
+
### Automatic Cleanup
|
|
254
|
+
|
|
255
|
+
The cleanup process permanently removes comments from the database based on two limits:
|
|
256
|
+
1. **Age**: Comments older than 30 days (configurable) are **permanently deleted**
|
|
257
|
+
2. **Count**: If total active comments exceed 150 (configurable), oldest comments are **permanently deleted**
|
|
258
|
+
|
|
259
|
+
**Note**: Cleanup removes both top-level comments and replies. The `dry_run` mode allows previewing what would be deleted without making changes.
|
|
260
|
+
|
|
261
|
+
## Integration with React/TypeScript
|
|
262
|
+
|
|
263
|
+
Example React component:
|
|
264
|
+
|
|
265
|
+
```tsx
|
|
266
|
+
import React, { useState, useEffect } from 'react';
|
|
267
|
+
import axios from 'axios';
|
|
268
|
+
|
|
269
|
+
interface Comment {
|
|
270
|
+
id: number;
|
|
271
|
+
content: string;
|
|
272
|
+
author_name: string;
|
|
273
|
+
is_anonymous: boolean;
|
|
274
|
+
created_at: string;
|
|
275
|
+
parent_id: number | null;
|
|
276
|
+
replies: Comment[];
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const CommentSection: React.FC = () => {
|
|
280
|
+
const [comments, setComments] = useState<Comment[]>([]);
|
|
281
|
+
const [newComment, setNewComment] = useState({
|
|
282
|
+
content: '',
|
|
283
|
+
author_name: '',
|
|
284
|
+
author_email: '',
|
|
285
|
+
is_anonymous: false,
|
|
286
|
+
parent_id: null as number | null
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
fetchComments();
|
|
291
|
+
}, []);
|
|
292
|
+
|
|
293
|
+
const fetchComments = async () => {
|
|
294
|
+
const response = await axios.get('/api/comments');
|
|
295
|
+
setComments(response.data.data);
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
const submitComment = async (e: React.FormEvent) => {
|
|
299
|
+
e.preventDefault();
|
|
300
|
+
await axios.post('/api/comments', newComment);
|
|
301
|
+
setNewComment({ content: '', author_name: '', author_email: '', is_anonymous: false, parent_id: null });
|
|
302
|
+
fetchComments();
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const deleteComment = async (id: number) => {
|
|
306
|
+
await axios.delete(`/api/comments/${id}`);
|
|
307
|
+
fetchComments();
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<div className="comment-section">
|
|
312
|
+
<h2>Comments</h2>
|
|
313
|
+
|
|
314
|
+
<form onSubmit={submitComment}>
|
|
315
|
+
<textarea
|
|
316
|
+
value={newComment.content}
|
|
317
|
+
onChange={(e) => setNewComment({...newComment, content: e.target.value})}
|
|
318
|
+
placeholder="Your comment..."
|
|
319
|
+
required
|
|
320
|
+
/>
|
|
321
|
+
<input
|
|
322
|
+
type="text"
|
|
323
|
+
value={newComment.author_name}
|
|
324
|
+
onChange={(e) => setNewComment({...newComment, author_name: e.target.value})}
|
|
325
|
+
placeholder="Your name"
|
|
326
|
+
required
|
|
327
|
+
/>
|
|
328
|
+
<input
|
|
329
|
+
type="email"
|
|
330
|
+
value={newComment.author_email}
|
|
331
|
+
onChange={(e) => setNewComment({...newComment, author_email: e.target.value})}
|
|
332
|
+
placeholder="Email (optional)"
|
|
333
|
+
/>
|
|
334
|
+
<label>
|
|
335
|
+
<input
|
|
336
|
+
type="checkbox"
|
|
337
|
+
checked={newComment.is_anonymous}
|
|
338
|
+
onChange={(e) => setNewComment({...newComment, is_anonymous: e.target.checked})}
|
|
339
|
+
/>
|
|
340
|
+
Post anonymously
|
|
341
|
+
</label>
|
|
342
|
+
<button type="submit">Post Comment</button>
|
|
343
|
+
</form>
|
|
344
|
+
|
|
345
|
+
<div className="comments-list">
|
|
346
|
+
{comments.map(comment => (
|
|
347
|
+
<CommentItem
|
|
348
|
+
key={comment.id}
|
|
349
|
+
comment={comment}
|
|
350
|
+
onReply={(parentId) => setNewComment({
|
|
351
|
+
...newComment,
|
|
352
|
+
parent_id: parentId,
|
|
353
|
+
content: '',
|
|
354
|
+
})}
|
|
355
|
+
onDelete={deleteComment}
|
|
356
|
+
/>
|
|
357
|
+
))}
|
|
358
|
+
</div>
|
|
359
|
+
</div>
|
|
360
|
+
);
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
const CommentItem: React.FC<{ comment: Comment; onReply: (id: number) => void; onDelete: (id: number) => void }> = ({
|
|
364
|
+
comment,
|
|
365
|
+
onReply,
|
|
366
|
+
onDelete
|
|
367
|
+
}) => {
|
|
368
|
+
const [showReplyForm, setShowReplyForm] = useState(false);
|
|
369
|
+
|
|
370
|
+
return (
|
|
371
|
+
<div className="comment" style={{ marginLeft: comment.parent_id ? '20px' : '0' }}>
|
|
372
|
+
<div className="comment-header">
|
|
373
|
+
<strong>{comment.author_name}</strong>
|
|
374
|
+
{comment.is_anonymous && <span> (Anonymous)</span>}
|
|
375
|
+
<small>{new Date(comment.created_at).toLocaleString()}</small>
|
|
376
|
+
</div>
|
|
377
|
+
<div className="comment-content" dangerouslySetInnerHTML={{ __html: comment.content }} />
|
|
378
|
+
<div className="comment-actions">
|
|
379
|
+
<button onClick={() => setShowReplyForm(!showReplyForm)}>Reply</button>
|
|
380
|
+
<button onClick={() => onDelete(comment.id)}>Delete</button>
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
{showReplyForm && (
|
|
384
|
+
<form onSubmit={(e) => { e.preventDefault(); onReply(comment.id); }}>
|
|
385
|
+
<textarea placeholder="Your reply..." required />
|
|
386
|
+
<button type="submit">Post Reply</button>
|
|
387
|
+
</form>
|
|
388
|
+
)}
|
|
389
|
+
|
|
390
|
+
{comment.replies && comment.replies.length > 0 && (
|
|
391
|
+
<div className="replies">
|
|
392
|
+
{comment.replies.map(reply => (
|
|
393
|
+
<CommentItem
|
|
394
|
+
key={reply.id}
|
|
395
|
+
comment={reply}
|
|
396
|
+
onReply={onReply}
|
|
397
|
+
onDelete={onDelete}
|
|
398
|
+
/>
|
|
399
|
+
))}
|
|
400
|
+
</div>
|
|
401
|
+
)}
|
|
402
|
+
</div>
|
|
403
|
+
);
|
|
404
|
+
};
|
|
405
|
+
|
|
406
|
+
export default CommentSection;
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
## Command Line Interface
|
|
410
|
+
|
|
411
|
+
RComments includes a CLI for database management:
|
|
412
|
+
|
|
413
|
+
```bash
|
|
414
|
+
# Initialize database and migrations
|
|
415
|
+
rcomments-cli init-db
|
|
416
|
+
|
|
417
|
+
# Create and apply migrations
|
|
418
|
+
rcomments-cli migrate-db
|
|
419
|
+
|
|
420
|
+
# Clean up old comments
|
|
421
|
+
rcomments-cli cleanup [--dry-run]
|
|
422
|
+
|
|
423
|
+
# Show statistics
|
|
424
|
+
rcomments-cli stats
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
## Configuration Options
|
|
428
|
+
|
|
429
|
+
| Variable | Default | Description |
|
|
430
|
+
|----------|---------|-------------|
|
|
431
|
+
| `DATABASE_URL` | `postgresql://localhost/rcomments` | PostgreSQL connection string |
|
|
432
|
+
| `SECRET_KEY` | `dev-secret-key-change-in-production` | Flask secret key |
|
|
433
|
+
| `RATE_LIMIT` | `10 per minute` | Rate limit format |
|
|
434
|
+
| `MAX_COMMENT_AGE_DAYS` | `30` | Maximum age of comments in days |
|
|
435
|
+
| `MAX_TOTAL_COMMENTS` | `150` | Maximum total active comments |
|
|
436
|
+
| `ALLOW_ANONYMOUS` | `true` | Allow anonymous comments |
|
|
437
|
+
| `REQUIRE_EMAIL_ANONYMOUS` | `false` | Require email for anonymous users |
|
|
438
|
+
|
|
439
|
+
## Development
|
|
440
|
+
|
|
441
|
+
### Setup
|
|
442
|
+
|
|
443
|
+
```bash
|
|
444
|
+
|
|
445
|
+
cd rcomments
|
|
446
|
+
python -m venv venv
|
|
447
|
+
source venv/bin/activate # On Windows: venv\Scripts\activate
|
|
448
|
+
pip install -e ".[dev]"
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Running Tests
|
|
452
|
+
|
|
453
|
+
```bash
|
|
454
|
+
pytest tests/ -v
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
### Code Formatting
|
|
458
|
+
|
|
459
|
+
```bash
|
|
460
|
+
black rcomments/
|
|
461
|
+
flake8 rcomments/
|
|
462
|
+
mypy rcomments/
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
## License
|
|
466
|
+
|
|
467
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
|