Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
9e99f3c
Add per-project hierarchical collaborative notes
BlaiseOfGlory Mar 31, 2026
06eebb4
Fix CodeFactor lint issues in collab notes views
BlaiseOfGlory Mar 31, 2026
75ed837
Add @dnd-kit dependencies for collab notes drag-and-drop
BlaiseOfGlory Apr 1, 2026
1ae04ae
Fix note creation, modal positioning, and drag indicators
BlaiseOfGlory Apr 1, 2026
3c5caaa
Fix timestamp defaults and improve drag indicators
BlaiseOfGlory Apr 1, 2026
a06f2a1
Fix timestamp DB defaults, thicker drag indicators, debug logging
BlaiseOfGlory Apr 1, 2026
378a609
Fix drag-over not firing in nested tree structure
BlaiseOfGlory Apr 1, 2026
7e9502b
Load collab notes CSS in project detail template
BlaiseOfGlory Apr 1, 2026
103d897
Fix drag-drop positioning and add inline drop indicators
BlaiseOfGlory Apr 1, 2026
dcdf1e0
Sort tree nodes by position in buildTree
BlaiseOfGlory Apr 1, 2026
0e13125
Fix loading spinner text spinning with the indicator
BlaiseOfGlory Apr 1, 2026
4cc3a86
Serve note images through authenticated Django view
BlaiseOfGlory Apr 1, 2026
2e678c3
Expand collab notes container to fill viewport height
BlaiseOfGlory Apr 1, 2026
62eec5e
Add drop zones at top and bottom of note tree
BlaiseOfGlory Apr 1, 2026
7d16dfa
Fix sentinel drop zones and remove debug logging
BlaiseOfGlory Apr 1, 2026
2ec095a
Use pointer position for folder before/inside/after zones
BlaiseOfGlory Apr 1, 2026
bc1daa0
Add package-lock.json and test data seed script
BlaiseOfGlory Apr 1, 2026
7c4d4c1
Address CodeFactor issues from PR #855
BlaiseOfGlory Apr 1, 2026
d125892
Merge upstream/master into feature/collab-notes-v2
BlaiseOfGlory Apr 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions ghostwriter/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
from ghostwriter.reporting.views2.report_finding_link import get_position
from ghostwriter.rolodex.models import (
Project,
ProjectCollabNote,
ProjectContact,
ProjectObjective,
ProjectSubTask,
Expand Down Expand Up @@ -1373,6 +1374,8 @@ class CheckEditPermissions(JwtRequiredMixin, HasuraActionView):
"report_finding_link": ReportFindingLink,
"report": Report,
"project": Project,
"project_collab_note": ProjectCollabNote,
"project_tree_sync": Project,
}

def post(self, request):
Expand Down
26 changes: 26 additions & 0 deletions ghostwriter/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,32 @@ class Meta:
operator = factory.SubFactory(UserFactory)


class ProjectCollabNoteFactory(factory.django.DjangoModelFactory):
class Meta:
model = "rolodex.ProjectCollabNote"

title = Faker("sentence", nb_words=3)
node_type = "note"
content = Faker("rich_text")
position = factory.Sequence(lambda n: n * 1000)
project = factory.SubFactory(ProjectFactory)


class ProjectCollabNoteFolderFactory(ProjectCollabNoteFactory):
node_type = "folder"
content = ""


class ProjectCollabNoteFieldFactory(factory.django.DjangoModelFactory):
class Meta:
model = "rolodex.ProjectCollabNoteField"

field_type = "rich_text"
content = Faker("rich_text")
position = factory.Sequence(lambda n: n)
note = factory.SubFactory(ProjectCollabNoteFactory)


class ClientInviteFactory(factory.django.DjangoModelFactory):
class Meta:
model = "rolodex.ClientInvite"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import django.db.models.deletion
import django.utils.timezone
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("rolodex", "0061_projectrole_position_ordering"),
]

operations = [
migrations.CreateModel(
name="ProjectCollabNote",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"title",
models.CharField(
help_text="Title of the note or folder",
max_length=255,
verbose_name="Title",
),
),
(
"node_type",
models.CharField(
choices=[("folder", "Folder"), ("note", "Note")],
default="note",
help_text="Whether this is a folder or a note",
max_length=10,
verbose_name="Type",
),
),
(
"content",
models.TextField(
blank=True,
default="",
help_text="Rich text content (for notes only, empty for folders)",
verbose_name="Content",
),
),
(
"position",
models.PositiveIntegerField(
default=0,
help_text="Order within parent (lower values first)",
verbose_name="Position",
),
),
(
"created_at",
models.DateTimeField(default=django.utils.timezone.now),
),
(
"updated_at",
models.DateTimeField(default=django.utils.timezone.now),
),
(
"parent",
models.ForeignKey(
blank=True,
help_text="Parent folder (null for root-level items)",
null=True,
on_delete=django.db.models.deletion.CASCADE,
related_name="children",
to="rolodex.projectcollabnote",
),
),
(
"project",
models.ForeignKey(
help_text="The project this note belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="collab_notes",
to="rolodex.project",
),
),
],
options={
"verbose_name": "Project collaborative note",
"verbose_name_plural": "Project collaborative notes",
"ordering": ["position", "title"],
},
),
migrations.AddConstraint(
model_name="projectcollabnote",
constraint=models.CheckConstraint(
check=models.Q(("node_type", "note"), ("content", ""), _connector="OR"),
name="folder_has_no_content",
),
),
migrations.CreateModel(
name="ProjectCollabNoteField",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
(
"field_type",
models.CharField(
choices=[("rich_text", "Rich Text"), ("image", "Image")],
default="rich_text",
help_text="Type of content in this field",
max_length=10,
verbose_name="Field Type",
),
),
(
"content",
models.TextField(
blank=True,
default="",
help_text="HTML content for rich text fields",
verbose_name="Content",
),
),
(
"image_width",
models.IntegerField(
blank=True,
editable=False,
null=True,
verbose_name="Image Width",
),
),
(
"image_height",
models.IntegerField(
blank=True,
editable=False,
null=True,
verbose_name="Image Height",
),
),
(
"image",
models.ImageField(
blank=True,
help_text="Image file for image fields",
height_field="image_height",
null=True,
upload_to="collab_note_images/%Y/%m/%d/",
verbose_name="Image",
width_field="image_width",
),
),
(
"position",
models.PositiveIntegerField(
default=0,
help_text="Order within note (lower values first)",
verbose_name="Position",
),
),
(
"created_at",
models.DateTimeField(default=django.utils.timezone.now),
),
(
"updated_at",
models.DateTimeField(default=django.utils.timezone.now),
),
(
"note",
models.ForeignKey(
help_text="The note this field belongs to",
on_delete=django.db.models.deletion.CASCADE,
related_name="fields",
to="rolodex.projectcollabnote",
),
),
],
options={
"verbose_name": "Project collaborative note field",
"verbose_name_plural": "Project collaborative note fields",
"ordering": ["note", "position"],
},
),
]
39 changes: 39 additions & 0 deletions ghostwriter/rolodex/migrations/0063_migrate_collab_notes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Data migration to move content from Project.collab_note to ProjectCollabNote."""

from django.db import migrations


def migrate_collab_notes(apps, schema_editor):
Project = apps.get_model("rolodex", "Project")
ProjectCollabNote = apps.get_model("rolodex", "ProjectCollabNote")
ProjectCollabNoteField = apps.get_model("rolodex", "ProjectCollabNoteField")

for project in Project.objects.exclude(collab_note=""):
note = ProjectCollabNote.objects.create(
project=project,
title="Notes",
node_type="note",
content=project.collab_note,
position=0,
)
ProjectCollabNoteField.objects.create(
note=note,
field_type="rich_text",
content=project.collab_note,
position=0,
)


def reverse_migrate(apps, schema_editor):
ProjectCollabNote = apps.get_model("rolodex", "ProjectCollabNote")
ProjectCollabNote.objects.all().delete()


class Migration(migrations.Migration):
dependencies = [
("rolodex", "0062_projectcollabnote_projectcollabnotefield"),
]

operations = [
migrations.RunPython(migrate_collab_notes, reverse_migrate),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Generated by Django 4.2.16 on 2026-03-31 21:26

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("rolodex", "0063_migrate_collab_notes"),
]

operations = [
migrations.AlterField(
model_name="projectcollabnote",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
migrations.AlterField(
model_name="projectcollabnotefield",
name="id",
field=models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"""Set database-level defaults for collab note timestamp columns.

Django 4.2's ``default=timezone.now`` only applies at the ORM level.
Hasura and the collab server insert rows directly, bypassing the ORM,
so database-level defaults are required.
"""

from django.db import migrations


class Migration(migrations.Migration):
dependencies = [
("rolodex", "0064_alter_projectcollabnote_id_and_more"),
]

operations = [
migrations.RunSQL(
sql="ALTER TABLE rolodex_projectcollabnote ALTER COLUMN created_at SET DEFAULT NOW();",
reverse_sql="ALTER TABLE rolodex_projectcollabnote ALTER COLUMN created_at DROP DEFAULT;",
),
migrations.RunSQL(
sql="ALTER TABLE rolodex_projectcollabnote ALTER COLUMN updated_at SET DEFAULT NOW();",
reverse_sql="ALTER TABLE rolodex_projectcollabnote ALTER COLUMN updated_at DROP DEFAULT;",
),
migrations.RunSQL(
sql="ALTER TABLE rolodex_projectcollabnotefield ALTER COLUMN created_at SET DEFAULT NOW();",
reverse_sql="ALTER TABLE rolodex_projectcollabnotefield ALTER COLUMN created_at DROP DEFAULT;",
),
migrations.RunSQL(
sql="ALTER TABLE rolodex_projectcollabnotefield ALTER COLUMN updated_at SET DEFAULT NOW();",
reverse_sql="ALTER TABLE rolodex_projectcollabnotefield ALTER COLUMN updated_at DROP DEFAULT;",
),
]
Loading