Skip to content
Merged
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
16 changes: 15 additions & 1 deletion apps/organization/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from apps.organization.models import Organization
from apps.organization.serializers import OrganizationSerializer
from apps.organization.services import get_owner_name_query_set
from apps.organization_setting.models import OrganizationSetting
from apps.organization_setting.serializers import OrganizationSettingSerializer
from utils.views import OrganizationRetrieveAPIView


Expand Down Expand Up @@ -63,7 +65,19 @@ def get(self, request, slug_name):
else:
result = "The organization is valid."

setting = (
OrganizationSetting.objects.filter(organization=organization)
.prefetch_related("themes")
.first()
)

return Response(
{"result": result, "template": organization.template},
{
"result": result,
"template": organization.template,
"setting": OrganizationSettingSerializer(setting).data
if setting
else None,
},
status=status.HTTP_200_OK,
)
6 changes: 6 additions & 0 deletions apps/organization_setting/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.apps import AppConfig


class SettingsConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "apps.organization_setting"
6 changes: 6 additions & 0 deletions apps/organization_setting/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from django.db import models


class ThemeType(models.TextChoices):
LIGHT = "light"
DARK = "dark"
87 changes: 87 additions & 0 deletions apps/organization_setting/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Generated by Django 5.0.6 on 2026-06-01 02:26

import django.db.models.deletion
import uuid
from django.db import migrations, models


class Migration(migrations.Migration):
initial = True

dependencies = [
("organization", "0004_alter_organization_template"),
]

operations = [
migrations.CreateModel(
name="OrganizationSetting",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("brand_name", models.CharField(blank=True, max_length=255)),
("site_title", models.CharField(blank=True, max_length=255)),
("site_description", models.TextField(blank=True)),
("border_radius", models.JSONField(blank=True, default=dict)),
(
"organization",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
related_name="organization_settings",
to="organization.organization",
),
),
],
options={
"db_table": "organization_settings",
},
),
migrations.CreateModel(
name="OrganizationTheme",
fields=[
(
"id",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
("created_at", models.DateTimeField(auto_now_add=True)),
("updated_at", models.DateTimeField(auto_now=True)),
("theme_key", models.CharField(max_length=50)),
("favicon", models.CharField(blank=True, max_length=500)),
("logo", models.CharField(blank=True, max_length=500)),
("theme_colors", models.JSONField(blank=True, default=dict)),
(
"setting",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="themes",
to="organization_setting.organizationsetting",
),
),
],
options={
"db_table": "organization_themes",
},
),
migrations.AddConstraint(
model_name="organizationtheme",
constraint=models.UniqueConstraint(
fields=("setting", "theme_key"),
name="unique_organization_theme_key_per_setting",
),
),
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import uuid
from django.db import migrations


def backfill_organization_settings(apps, schema_editor):
from apps.organization_setting.services import (
DEFAULT_DARK_THEME_VALUES,
DEFAULT_LIGHT_THEME_VALUES,
DEFAULT_ORGANIZATION_SETTING_VALUES,
)
Comment thread
ngovinh2k2 marked this conversation as resolved.

Organization = apps.get_model("organization", "Organization")
OrganizationSetting = apps.get_model("organization_setting", "OrganizationSetting")
OrganizationTheme = apps.get_model("organization_setting", "OrganizationTheme")

organizations = list(Organization.objects.values_list("id", flat=True))
existing_setting_org_ids = set(
OrganizationSetting.objects.values_list("organization_id", flat=True)
)

missing_settings = [
OrganizationSetting(
id=uuid.uuid4(),
organization_id=organization_id,
**DEFAULT_ORGANIZATION_SETTING_VALUES,
)
for organization_id in organizations
if organization_id not in existing_setting_org_ids
]
if missing_settings:
OrganizationSetting.objects.bulk_create(missing_settings)

settings = list(OrganizationSetting.objects.values("id", "organization_id"))
existing_theme_keys = set(
OrganizationTheme.objects.values_list("setting_id", "theme_key")
)

themes_to_create = []
for setting in settings:
for theme_key, theme_defaults in [
("light", DEFAULT_LIGHT_THEME_VALUES),
("dark", DEFAULT_DARK_THEME_VALUES),
]:
theme_identity = (setting["id"], theme_key)
if theme_identity in existing_theme_keys:
continue

themes_to_create.append(
OrganizationTheme(
id=uuid.uuid4(),
setting_id=setting["id"],
theme_key=theme_key,
**theme_defaults,
)
)

if themes_to_create:
OrganizationTheme.objects.bulk_create(themes_to_create)


class Migration(migrations.Migration):
dependencies = [
("organization_setting", "0001_initial"),
]

operations = [
migrations.RunPython(
backfill_organization_settings,
migrations.RunPython.noop,
),
]
Empty file.
40 changes: 40 additions & 0 deletions apps/organization_setting/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from common.models.base_model import BaseModel
from django.db import models

from apps.organization.models import Organization


class OrganizationSetting(BaseModel):
organization = models.OneToOneField(
Organization,
on_delete=models.CASCADE,
related_name="organization_settings",
)
brand_name = models.CharField(max_length=255, blank=True)
site_title = models.CharField(max_length=255, blank=True)
site_description = models.TextField(blank=True)
border_radius = models.JSONField(default=dict, blank=True)

class Meta:
db_table = "organization_settings"


class OrganizationTheme(BaseModel):
setting = models.ForeignKey(
OrganizationSetting,
on_delete=models.CASCADE,
related_name="themes",
)
theme_key = models.CharField(max_length=50)
favicon = models.CharField(max_length=500, blank=True)
logo = models.CharField(max_length=500, blank=True)
theme_colors = models.JSONField(default=dict, blank=True)

class Meta:
db_table = "organization_themes"
constraints = [
models.UniqueConstraint(
fields=["setting", "theme_key"],
name="unique_organization_theme_key_per_setting",
)
]
137 changes: 137 additions & 0 deletions apps/organization_setting/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
from common.apps.upload_file.service import get_presigned_url
from django.conf import settings
from django.db import transaction
from rest_framework import serializers

from apps.custom_page.serializers import CustomPageSerializer
from apps.organization_setting.models import OrganizationSetting, OrganizationTheme


class OrganizationThemeSerializer(serializers.ModelSerializer):
url_logo = serializers.SerializerMethodField()
url_favicon = serializers.SerializerMethodField()

class Meta:
model = OrganizationTheme
fields = [
"id",
"theme_key",
"favicon",
"logo",
"theme_colors",
"url_logo",
"url_favicon",
]
extra_kwargs = {
"id": {"read_only": True},
"favicon": {"write_only": True},
"logo": {"write_only": True},
"url_logo": {"read_only": True},
"url_favicon": {"read_only": True},
}

def get_url_logo(self, instance):
host = settings.HOST.rstrip("/")
if not instance.logo:
default_logo = (
"logo_white.png" if instance.theme_key == "dark" else "logo_black.png"
)
return f"{host}/static/images/branding/{default_logo}"
return get_presigned_url(
settings.AWS_S3.get("AWS_STORAGE_BUCKET_NAME"),
f"uploads/{instance.logo}",
)

def get_url_favicon(self, instance):
host = settings.HOST.rstrip("/")
if not instance.favicon:
default_favicon = (
"favicon_white.png"
if instance.theme_key == "dark"
else "favicon_black.png"
)
return f"{host}/static/images/branding/{default_favicon}"
return get_presigned_url(
settings.AWS_S3.get("AWS_STORAGE_BUCKET_NAME"),
f"uploads/{instance.favicon}",
)


class OrganizationSettingSerializer(serializers.ModelSerializer):
themes = OrganizationThemeSerializer(many=True, required=False)

class Meta:
model = OrganizationSetting
fields = [
"id",
"site_title",
"site_description",
"border_radius",
"themes",
"brand_name",
]
read_only_fields = ["id"]


class UpdateOrganizationSettingSerializer(serializers.Serializer):
id = serializers.UUIDField(read_only=True)
site_title = serializers.CharField(required=False, allow_blank=True)
site_description = serializers.CharField(required=False, allow_blank=True)
border_radius = serializers.JSONField(required=False)
themes = OrganizationThemeSerializer(many=True, required=False)
custom_pages = CustomPageSerializer(many=True, required=False)
brand_name = serializers.CharField(required=False, allow_blank=True)
created_at = serializers.DateTimeField(read_only=True)
updated_at = serializers.DateTimeField(read_only=True)

def _save_instance(self, instance, data):
for attr, value in data.items():
setattr(instance, attr, value)
instance.save()

def _get_instance(self, queryset, data, fallback_field, default=None):
data = data.copy()
object_id = data.pop("id", None)
fallback_value = data.get(fallback_field)
instance = queryset.filter(id=object_id).first() if object_id else None
Comment thread
ngovinh2k2 marked this conversation as resolved.
if instance is None and fallback_value:
instance = queryset.filter(**{fallback_field: fallback_value}).first()
return instance or default(data)
Comment thread
ngovinh2k2 marked this conversation as resolved.

def _update_themes(self, instance, themes_data):
for theme_data in themes_data or []:
theme = self._get_instance(
instance.themes,
theme_data,
"theme_key",
lambda data: instance.themes.create(
theme_key=data.get("theme_key") or "light"
),
)
self._save_instance(theme, theme_data)

def to_representation(self, instance):
data = OrganizationSettingSerializer(instance, context=self.context).data
data["custom_pages"] = CustomPageSerializer(
instance.organization.organization_custom_page.all(),
many=True,
context=self.context,
).data
return data

def update(self, instance, validated_data):
with transaction.atomic():
custom_pages_data = validated_data.pop("custom_pages", [])
themes_data = validated_data.pop("themes", [])

self._save_instance(instance, validated_data)
self._update_themes(instance, themes_data)

pages = instance.organization.organization_custom_page
for page_data in custom_pages_data:
page = self._get_instance(pages, page_data, "page_type")
if page is None:
continue
self._save_instance(page, page_data)

return instance
Loading
Loading