Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5722ac8603 | ||
|
|
2088d0b02c | ||
|
|
8a7525096a | ||
|
|
1f8e62fdce |
@@ -219,7 +219,7 @@ def create_pages(workspace, project, user_id, pages_count):
|
||||
Faker.seed(0)
|
||||
|
||||
pages = []
|
||||
for _ in range(0, pages_count):
|
||||
for index in range(0, pages_count):
|
||||
text = fake.text(max_nb_chars=60000)
|
||||
pages.append(
|
||||
Page(
|
||||
@@ -231,13 +231,14 @@ def create_pages(workspace, project, user_id, pages_count):
|
||||
description_html=f"<p>{text}</p>",
|
||||
archived_at=None,
|
||||
is_locked=False,
|
||||
sort_order=index * 10000,
|
||||
)
|
||||
)
|
||||
# Bulk create pages
|
||||
pages = Page.objects.bulk_create(pages, ignore_conflicts=True)
|
||||
# Add Page to project
|
||||
ProjectPage.objects.bulk_create(
|
||||
[ProjectPage(page=page, project=project, workspace=workspace) for page in pages],
|
||||
[ProjectPage(page=page, project=project, workspace=workspace, sort_order=page.sort_order) for page in pages],
|
||||
batch_size=1000,
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# Generated by Django 4.2.22 on 2026-01-05 13:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
def update_projectpage_sort_order(apps, schema_editor):
|
||||
ProjectPage = apps.get_model("db", "ProjectPage")
|
||||
Project = apps.get_model("db", "Project")
|
||||
|
||||
for project in Project.objects.all():
|
||||
pages = list(
|
||||
ProjectPage.objects.filter(project=project).order_by("created_at")
|
||||
)
|
||||
|
||||
for index, page in enumerate(pages):
|
||||
page.sort_order = index * 10000
|
||||
|
||||
ProjectPage.objects.bulk_update(pages, ["sort_order"], batch_size=3000)
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0113_webhook_version'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='projectpage',
|
||||
name='sort_order',
|
||||
field=models.FloatField(default=65535),
|
||||
),
|
||||
migrations.RunPython(update_projectpage_sort_order, reverse_code=migrations.RunPython.noop),
|
||||
]
|
||||
@@ -4,10 +4,11 @@ from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
# Django imports
|
||||
from django.db import models
|
||||
from django.db import connection, models, transaction
|
||||
|
||||
# Module imports
|
||||
from plane.utils.html_processor import strip_tags
|
||||
from plane.utils.uuid import convert_uuid_to_integer
|
||||
|
||||
from .base import BaseModel
|
||||
|
||||
@@ -63,6 +64,22 @@ class Page(BaseModel):
|
||||
"""Return owner email and page name"""
|
||||
return f"{self.owned_by.email} <{self.name}>"
|
||||
|
||||
def _get_sort_order(self, project):
|
||||
"""Get the next sort order for the page within a specific project."""
|
||||
if self.access == Page.PRIVATE_ACCESS:
|
||||
largest = ProjectPage.objects.filter(
|
||||
page__access=Page.PRIVATE_ACCESS,
|
||||
page__owned_by=self.owned_by,
|
||||
project=project,
|
||||
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||
else:
|
||||
largest = ProjectPage.objects.filter(
|
||||
page__access=Page.PUBLIC_ACCESS,
|
||||
project=project,
|
||||
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||
|
||||
return (largest or self.DEFAULT_SORT_ORDER) + 10000
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Strip the html tags using html parser
|
||||
self.description_stripped = (
|
||||
@@ -70,7 +87,27 @@ class Page(BaseModel):
|
||||
if (self.description_html == "" or self.description_html is None)
|
||||
else strip_tags(self.description_html)
|
||||
)
|
||||
super(Page, self).save(*args, **kwargs)
|
||||
|
||||
if not self._state.adding:
|
||||
original = Page.objects.get(pk=self.pk)
|
||||
if original.access != self.access:
|
||||
with transaction.atomic():
|
||||
# Get the project pages for the page and update the sort order
|
||||
project_pages = list(ProjectPage.objects.filter(page=self).select_related("project"))
|
||||
|
||||
# Acquire advisory locks for all projects to prevent race conditions
|
||||
for project_page in project_pages:
|
||||
lock_key = convert_uuid_to_integer(project_page.project_id)
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT pg_advisory_xact_lock(%s)", [lock_key])
|
||||
|
||||
for project_page in project_pages:
|
||||
project_page.sort_order = self._get_sort_order(project_page.project)
|
||||
# Bulk update all project pages in a single query
|
||||
if project_pages:
|
||||
ProjectPage.objects.bulk_update(project_pages, ["sort_order"])
|
||||
|
||||
super(Page, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class PageLog(BaseModel):
|
||||
@@ -129,9 +166,12 @@ class PageLabel(BaseModel):
|
||||
|
||||
|
||||
class ProjectPage(BaseModel):
|
||||
DEFAULT_SORT_ORDER = 65535
|
||||
|
||||
project = models.ForeignKey("db.Project", on_delete=models.CASCADE, related_name="project_pages")
|
||||
page = models.ForeignKey("db.Page", on_delete=models.CASCADE, related_name="project_pages")
|
||||
workspace = models.ForeignKey("db.Workspace", on_delete=models.CASCADE, related_name="project_pages")
|
||||
sort_order = models.FloatField(default=DEFAULT_SORT_ORDER)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["project", "page", "deleted_at"]
|
||||
@@ -150,6 +190,42 @@ class ProjectPage(BaseModel):
|
||||
def __str__(self):
|
||||
return f"{self.project.name} {self.page.name}"
|
||||
|
||||
def _get_sort_order(self):
|
||||
"""Get the next sort order for the project page based on page access type."""
|
||||
if self.page.access == Page.PRIVATE_ACCESS:
|
||||
# For private pages, get max sort_order among pages owned by same user in same project
|
||||
largest = ProjectPage.objects.filter(
|
||||
page__access=Page.PRIVATE_ACCESS,
|
||||
page__owned_by=self.page.owned_by,
|
||||
project=self.project,
|
||||
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||
else:
|
||||
# For public pages, get max sort_order among all public pages in same project
|
||||
largest = ProjectPage.objects.filter(
|
||||
page__access=Page.PUBLIC_ACCESS,
|
||||
project=self.project,
|
||||
).aggregate(largest=models.Max("sort_order"))["largest"]
|
||||
|
||||
return (largest or self.DEFAULT_SORT_ORDER) + 10000
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
# Set sort_order for new project pages
|
||||
if self._state.adding:
|
||||
with transaction.atomic():
|
||||
# Create a lock for this specific project using a transaction-level advisory lock
|
||||
# This ensures only one transaction per project can execute this code at a time
|
||||
# The lock is automatically released when the transaction ends
|
||||
lock_key = convert_uuid_to_integer(self.project_id)
|
||||
|
||||
with connection.cursor() as cursor:
|
||||
# Get an exclusive transaction-level lock using the project ID as the lock key
|
||||
cursor.execute("SELECT pg_advisory_xact_lock(%s)", [lock_key])
|
||||
|
||||
self.sort_order = self._get_sort_order()
|
||||
super(ProjectPage, self).save(*args, **kwargs)
|
||||
else:
|
||||
super(ProjectPage, self).save(*args, **kwargs)
|
||||
|
||||
|
||||
class PageVersion(BaseModel):
|
||||
workspace = models.ForeignKey("db.Workspace", on_delete=models.CASCADE, related_name="page_versions")
|
||||
|
||||
Reference in New Issue
Block a user