Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
50 changes: 50 additions & 0 deletions apps/organizations/migrations/0005_templatetab.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Generated by Django 6.0.5 on 2026-08-11 13:49

import apps.commons.mixins
import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("organizations", "0004_categoryfollow_categoryfollow_unique_category_follow"),
]

operations = [
migrations.CreateModel(
name="TemplateTab",
fields=[
(
"id",
models.BigAutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("uuid", models.UUIDField(auto_created=True)),
(
"type",
models.CharField(
choices=[("text", "Text"), ("blog", "Blog")],
default="text",
max_length=32,
),
),
("icon", models.CharField(blank=True, max_length=255, null=True)),
("show_preview", models.BooleanField(default=True)),
("title_item", models.TextField(blank=True, null=True)),
(
"template",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="tabs",
to="organizations.template",
),
),
],
bases=(apps.commons.mixins.OrganizationRelated, models.Model),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 6.0.5 on 2026-08-11 14:16

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("organizations", "0005_templatetab"),
]

operations = [
migrations.AddField(
model_name="templatetab",
name="content_item",
field=models.TextField(blank=True, null=True),
),
migrations.AlterField(
model_name="templatetab",
name="title_item",
field=models.TextField(blank=True, default="", max_length=255, null=True),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 6.0.5 on 2026-08-11 14:29

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("organizations", "0006_templatetab_content_item_and_more"),
]

operations = [
migrations.AddField(
model_name="templatetab",
name="content",
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name="templatetab",
name="title",
field=models.TextField(blank=True, default="", max_length=255, null=True),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 6.0.5 on 2026-08-12 10:14

from django.db import migrations


class Migration(migrations.Migration):

dependencies = [
("organizations", "0007_templatetab_content_templatetab_title"),
]

operations = [
migrations.RenameField(
model_name="templatetab",
old_name="content",
new_name="description",
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 6.0.5 on 2026-08-12 12:10

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("organizations", "0008_rename_content_templatetab_description"),
]

operations = [
migrations.AddConstraint(
model_name="templatetab",
constraint=models.UniqueConstraint(
fields=("uuid",), name="unique_template_tab"
),
),
]
19 changes: 19 additions & 0 deletions apps/organizations/migrations/0010_alter_templatetab_uuid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Generated by Django 6.0.5 on 2026-08-12 12:20

import uuid
from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("organizations", "0009_templatetab_unique_template_tab"),
]

operations = [
migrations.AlterField(
model_name="templatetab",
name="uuid",
field=models.UUIDField(auto_created=True, default=uuid.uuid4),
),
]
37 changes: 37 additions & 0 deletions apps/organizations/models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import uuid
from typing import TYPE_CHECKING, Any, Optional

from django.contrib.auth.models import Group, Permission
Expand All @@ -19,6 +20,7 @@
get_permissions_from_subscopes,
get_write_permissions_from_subscopes,
)
from apps.projects.models import ProjectTab, ProjectTabItem
from services.translator.mixins import HasAutoTranslatedFields

if TYPE_CHECKING:
Expand Down Expand Up @@ -480,6 +482,41 @@ def get_related_organizations(self) -> Organization:
return [self.organization]


class TemplateTab(OrganizationRelated, models.Model):
uuid = models.UUIDField(auto_created=True, default=uuid.uuid4)

# tabs
title = models.TextField(max_length=255, default="", blank=True, null=True)
description = models.TextField(blank=True, null=True)
template = models.ForeignKey(
Template, on_delete=models.CASCADE, related_name="tabs"
)
type = models.CharField(
max_length=32,
choices=ProjectTab.TabType.choices,
default=ProjectTab.TabType.TEXT,
)
icon = models.CharField(max_length=255, blank=True, null=True)
show_preview = models.BooleanField(default=True)

# content
title_item = models.TextField(max_length=255, default="", blank=True, null=True)
content_item = models.TextField(blank=True, null=True)

class Meta:
# tab need to be different uuid template (to have "unique tab")
constraints = [
models.UniqueConstraint(
fields=["uuid"],
name="unique_template_tab",
),
]

def get_related_organizations(self) -> Organization:
"""Return the organizations related to this model."""
return self.template.get_related_organizations()


class ProjectCategory(
HasAutoTranslatedFields, HasMultipleIDs, OrganizationRelated, models.Model
):
Expand Down
78 changes: 76 additions & 2 deletions apps/organizations/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
from typing import Any

from django.conf import settings
from django.db import transaction
from django.db.models import Q
from django.shortcuts import get_object_or_404
from rest_framework import serializers
from rest_framework.relations import SlugRelatedField
from services.keycloak.serializers import IdentityProviderSerializer
from services.translator.serializers import auto_translated

from apps.accounts.models import ProjectUser
from apps.commons.fields import (
Expand All @@ -27,8 +30,6 @@
TagClassificationMultipleIdRelatedField,
TagRelatedField,
)
from services.keycloak.serializers import IdentityProviderSerializer
from services.translator.serializers import auto_translated

from .exceptions import (
CategoryHierarchyLoopError,
Expand All @@ -44,6 +45,7 @@
Organization,
ProjectCategory,
Template,
TemplateTab,
TermsAndConditions,
)

Expand Down Expand Up @@ -449,12 +451,38 @@ def get_related_organizations(self) -> list[Organization]:
return [ProjectCategory.objects.get(id=self.validated_data["id"]).organization]


class TemplateTabSerializer(StringsImagesSerializer, serializers.ModelSerializer):
string_images_fields: list[str] = [
"content",
"description",
]

id = serializers.IntegerField(required=False)
uuid = serializers.UUIDField(read_only=True)

class Meta:
model = TemplateTab
fields = (
"id",
"uuid",
"title",
"description",
"template",
"type",
"icon",
"show_preview",
"title_item",
"content_item",
)


@auto_translated
class ProjectTemplateSerializer(
OrganizationRelatedSerializer,
serializers.ModelSerializer,
):
project_tags = TagRelatedField(many=True, read_only=True)
tabs = TemplateTabSerializer(many=True)

class Meta:
model = Template
Expand All @@ -474,6 +502,7 @@ class Meta:
"review_title",
"review_description",
"comment_content",
"tabs",
]
fields = read_only_fields

Expand Down Expand Up @@ -513,6 +542,7 @@ class TemplateSerializer(
queryset=ProjectCategory.objects.all(),
source="categories",
)
tabs = TemplateTabSerializer(many=True)

class Meta:
model = Template
Expand All @@ -533,6 +563,7 @@ class Meta:
"review_description",
"comment_content",
"categories_ids",
"tabs",
]

def get_related_organizations(self) -> list[Organization]:
Expand All @@ -548,6 +579,49 @@ def get_string_images_kwargs(
"template_id": instance.id,
}

@transaction.atomic
def update(self, instance, validated_data):
tabs_data = validated_data.pop("tabs", None)

# Update du Template
instance = super().update(instance, validated_data)

if tabs_data is None:
return instance

existing_tabs = {tab.id: tab for tab in instance.tabs.all()}
received_ids = set()

for tab_data in tabs_data:
tab_id = tab_data.pop("id", None)
tab_data["template"] = instance

tab = existing_tabs.get(tab_id, TemplateTab(**tab_data))

for field, value in tab_data.items():
setattr(tab, field, value)

tab.save()
received_ids.add(tab.id)

to_delete = [tab_id for tab_id in existing_tabs if tab_id not in received_ids]
TemplateTab.objects.filter(id__in=to_delete).delete()

return instance

@transaction.atomic
def create(self, validated_data):
tabs_data = validated_data.pop("tabs", [])

template = super().create(validated_data)

for tab_data in tabs_data:
tab_data["template"] = template
template = TemplateTab(**tab_data)
template.save()

return template


@auto_translated
class ProjectCategoryHierarchySerializer(
Expand Down
11 changes: 10 additions & 1 deletion apps/organizations/signals.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
from django.db.models.signals import post_save, pre_delete
from django.dispatch import receiver

from .models import TermsAndConditions
from apps.projects.models import Project
from apps.projects.utils import sync_project_tabs

from .models import TemplateTab, TermsAndConditions


@receiver(pre_delete, sender="organizations.Organization")
Expand All @@ -15,3 +18,9 @@ def create_terms_and_conditions(sender, instance, created, **kwargs):
"""Create the associated terms and conditions at user's creation."""
if created:
TermsAndConditions.objects.get_or_create(organization=instance)


@receiver(post_save, sender="organizations.TemplateTab")
def update_project_tab(sender, instance: TemplateTab, created=False, **kwargs):
template = instance.template
sync_project_tabs(Project.objects.filter(template=template), [instance])
Loading
Loading