]> git.ipfire.org Git - dbl.git/commitdiff
reports: Create a separate table for comments
authorMichael Tremer <michael.tremer@ipfire.org>
Fri, 27 Feb 2026 17:28:16 +0000 (17:28 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Fri, 27 Feb 2026 17:28:16 +0000 (17:28 +0000)
That way, we may have multiple comments on a report.

Signed-off-by: Michael Tremer <michael.tremer@ipfire.org>
src/database.sql
src/dbl/api/reports.py
src/dbl/reports.py

index 3bab54248f0370ae4b1a61de725103cd8b03a6ab..60561161216dd6972897d049ef40e4f198dfe889 100644 (file)
@@ -2,7 +2,7 @@
 -- PostgreSQL database dump
 --
 
-\restrict ZeEQIdrFtt8lZBacF2YikE2NldALCViC5aH05xXVTjf5DSP1ITtfKT3w9cYoumo
+\restrict 97QQcysG1ebcqz7Y7sN4EJb44D05KafpruWuQjJfkHpdoO21URCTKpqX39K4fwP
 
 -- Dumped from database version 17.7 (Debian 17.7-0+deb13u1)
 -- Dumped by pg_dump version 17.7 (Debian 17.7-0+deb13u1)
@@ -269,6 +269,22 @@ CREATE SEQUENCE public.nameservers_id_seq
 ALTER SEQUENCE public.nameservers_id_seq OWNED BY public.nameservers.id;
 
 
+--
+-- Name: report_comments; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.report_comments (
+    id uuid DEFAULT gen_random_uuid() NOT NULL,
+    report_id uuid NOT NULL,
+    created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL,
+    created_by text,
+    deleted_at timestamp with time zone,
+    deleted_by text,
+    comment text DEFAULT ''::text NOT NULL,
+    points integer DEFAULT 0 NOT NULL
+);
+
+
 --
 -- Name: reports; Type: TABLE; Schema: public; Owner: -
 --
@@ -281,7 +297,6 @@ CREATE TABLE public.reports (
     reported_by text NOT NULL,
     closed_at timestamp with time zone,
     closed_by text,
-    comment text DEFAULT ''::text NOT NULL,
     block boolean DEFAULT true,
     accepted boolean DEFAULT false
 );
@@ -453,6 +468,14 @@ ALTER TABLE ONLY public.nameservers
     ADD CONSTRAINT nameservers_pkey PRIMARY KEY (id);
 
 
+--
+-- Name: report_comments report_comments_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.report_comments
+    ADD CONSTRAINT report_comments_pkey PRIMARY KEY (id);
+
+
 --
 -- Name: reports reports_pkey; Type: CONSTRAINT; Schema: public; Owner: -
 --
@@ -554,6 +577,13 @@ CREATE UNIQUE INDEX list_stats_unique ON public.list_stats USING btree (list_id,
 CREATE UNIQUE INDEX lists_unique ON public.lists USING btree (slug) WHERE (deleted_at IS NULL);
 
 
+--
+-- Name: report_comments_report_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX report_comments_report_id ON public.report_comments USING btree (report_id) WHERE (deleted_at IS NULL);
+
+
 --
 -- Name: reports_open; Type: INDEX; Schema: public; Owner: -
 --
@@ -615,6 +645,14 @@ ALTER TABLE ONLY public.list_stats
     ADD CONSTRAINT list_stats_list_id FOREIGN KEY (list_id) REFERENCES public.lists(id);
 
 
+--
+-- Name: report_comments report_comments_report_id; Type: FK CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.report_comments
+    ADD CONSTRAINT report_comments_report_id FOREIGN KEY (report_id) REFERENCES public.reports(id);
+
+
 --
 -- Name: reports reports_list_id; Type: FK CONSTRAINT; Schema: public; Owner: -
 --
@@ -643,5 +681,5 @@ ALTER TABLE ONLY public.sources
 -- PostgreSQL database dump complete
 --
 
-\unrestrict ZeEQIdrFtt8lZBacF2YikE2NldALCViC5aH05xXVTjf5DSP1ITtfKT3w9cYoumo
+\unrestrict 97QQcysG1ebcqz7Y7sN4EJb44D05KafpruWuQjJfkHpdoO21URCTKpqX39K4fwP
 
index cb13a11fbc0f18d367f92c87825c828ead62cc08..8f0e340e49de97faba4f1c90412f93c086692a89 100644 (file)
@@ -76,5 +76,15 @@ async def close_report(
        # Send 204
        return fastapi.Response(status_code=fastapi.status.HTTP_204_NO_CONTENT)
 
+"""
+       Comments
+"""
+
+@router.get("/{id}/comments")
+async def get_comments(
+               report: reports.Report = fastapi.Depends(get_report_from_path)
+) -> list[reports.ReportComment]:
+       return [comment async for comment in report.get_comments()]
+
 # Include our endpoints
 app.include_router(router)
index 534eccac2ff313b52248ba3b17aab8227fc82a9f..e4bf392ad0adc25211274cd87d626c3f0eb716e3 100644 (file)
@@ -64,7 +64,7 @@ class Reports(object):
 
                return await self.backend.db.fetch_one(stmt)
 
-       async def create(self, **kwargs):
+       async def create(self, comment=None, **kwargs):
                """
                        Creates a new report
                """
@@ -75,6 +75,12 @@ class Reports(object):
                # Manifest the object in the database immediately to assign the ID
                await self.backend.db.flush_and_refresh(report)
 
+               # Post the comment
+               if comment:
+                       await report.comment(
+                               comment=comment, reporter=report.reported_by, notify=False,
+                       )
+
                # Increment the counter of the list
                report.list.pending_reports += 1
 
@@ -223,9 +229,6 @@ class Report(sqlmodel.SQLModel, database.BackendMixin, table=True):
        # Closed By
        closed_by : str | None
 
-       # Comment
-       comment : str = ""
-
        # Block?
        block : bool = True
 
@@ -396,3 +399,100 @@ class Report(sqlmodel.SQLModel, database.BackendMixin, table=True):
        @property
        def url(self):
                return "https://www.ipfire.org/dbl/reports/%s" % self.id
+
+       # Comments
+
+       def get_comments(self):
+               """
+                       Returns all (non-deleted) comments to this report
+               """
+               return self.backend.db.fetch(
+                       sqlmodel
+                       .select(
+                               ReportComment,
+                       )
+                       .where(
+                               # Must match the report
+                               ReportComment.report == self,
+
+                               # Must not be deleted
+                               ReportComment.deleted_at == None,
+                       )
+                       .order_by(
+                               ReportComment.created_at,
+                       )
+               )
+
+       async def comment(self, comment, reporter, points=0, notify=True):
+               """
+                       Posts a new comment to this report
+               """
+               # Write the comment to the database
+               comment = await self.backend.db.insert(
+                       ReportComment,
+                       report     = self,
+                       created_by = reporter,
+                       comment    = comment,
+                       points     = points,
+               )
+
+               # Send a notification about this comment
+               if notify:
+                       pass # XXX TODO
+
+               return comment
+
+
+class ReportComment(sqlmodel.SQLModel, database.BackendMixin, table=True):
+       __tablename__ = "report_comments"
+
+       def __str__(self):
+               return "%s" % self.id
+
+       def __eq__(self, other):
+               if isinstance(other, self.__class__):
+                       return self.id == other.id
+
+               return NotImplemented
+
+       def __lt__(self, other):
+               if isinstance(other, self.__class__):
+                       return (self.report, self.created_at) < (other.report, other.created_at)
+
+               return NotImplemented
+
+       # ID
+       id: uuid.UUID = sqlmodel.Field(
+               primary_key=True, default=sqlmodel.func.gen_random_uuid(),
+       )
+
+       # Report ID
+       report_id: uuid.UUID = sqlmodel.Field(foreign_key="reports.id", exclude=True)
+
+       # report
+       report: "Report" = sqlmodel.Relationship(
+               sa_relationship_kwargs = {
+                       "lazy"      : "joined",
+                       "innerjoin" : True,
+               },
+       )
+
+       # Created At
+       created_at: datetime.datetime = sqlmodel.Field(
+               sa_column_kwargs = {"server_default" : sqlmodel.text("CURRENT_TIMESTAMP")}
+       )
+
+       # Created By
+       created_by: str
+
+       # Deleted At
+       deleted_at: datetime.datetime | None = sqlmodel.Field(exclude=True)
+
+       # Deleted By
+       deleted_by: str | None = sqlmodel.Field(exclude=True)
+
+       # Comment
+       comment: str = ""
+
+       # Points
+       points: int = 0