Compare commits

...

1 Commits

Author SHA1 Message Date
pablohashescobar 1053ff4684 feat: add tour_completed_features to Profile model and implement validation in ProfileSerializer
- Introduced a new JSONField `tour_completed_features` in the Profile model with a default value from `get_default_tour_completed_features`.
- Added validation logic in ProfileSerializer to clean and structure `tour_completed_features` input, ensuring only valid keys are retained.
- Updated ExporterHistory model to change `url` field from URLField to TextField for better flexibility.
- Created a new migration to apply these changes to the database.
2025-10-08 17:21:45 +05:30
4 changed files with 74 additions and 2 deletions
+32 -1
View File
@@ -4,7 +4,7 @@ from rest_framework import serializers
# Module import
from plane.db.models import Account, Profile, User, Workspace, WorkspaceMemberInvite
from plane.utils.url import contains_url
from plane.db.models.user import get_default_tour_completed_features
from .base import BaseSerializer
@@ -194,6 +194,37 @@ class ResetPasswordSerializer(serializers.Serializer):
class ProfileSerializer(BaseSerializer):
def validate_tour_completed_features(self, value):
"""
Clean tour_completed_features by removing invalid keys and keeping only
the keys present in get_default_tour_completed_features function.
"""
if not isinstance(value, dict):
return get_default_tour_completed_features()
expected_structure = get_default_tour_completed_features()
# Use dict comprehension for cleaner, more efficient code
cleaned_data = {}
# Process valid top-level keys
for key in expected_structure:
if key in value:
val = value[key]
if isinstance(val, dict) and key in ['workspace_features', 'project_features']:
# Clean nested structure using intersection
expected_nested = expected_structure[key]
cleaned_data[key] = {k: v for k, v in val.items() if k in expected_nested}
else:
cleaned_data[key] = val
else:
# Add missing key with default
default_value = expected_structure[key]
cleaned_data[key] = default_value.copy() if isinstance(default_value, dict) else default_value
return cleaned_data
class Meta:
model = Profile
fields = "__all__"
@@ -0,0 +1,24 @@
# Generated by Django 4.2.24 on 2025-10-08 10:46
from django.db import migrations, models
import plane.db.models.user
class Migration(migrations.Migration):
dependencies = [
('db', '0107_migrate_filters_to_rich_filters'),
]
operations = [
migrations.AddField(
model_name='profile',
name='tour_completed_features',
field=models.JSONField(default=plane.db.models.user.get_default_tour_completed_features),
),
migrations.AlterField(
model_name='exporterhistory',
name='url',
field=models.TextField(blank=True, null=True),
),
]
+1 -1
View File
@@ -42,7 +42,7 @@ class ExporterHistory(BaseModel):
)
reason = models.TextField(blank=True)
key = models.TextField(blank=True)
url = models.URLField(max_length=800, blank=True, null=True)
url = models.TextField(blank=True, null=True)
token = models.CharField(max_length=255, default=generate_token, unique=True)
initiated_by = models.ForeignKey(
settings.AUTH_USER_MODEL,
+17
View File
@@ -34,6 +34,20 @@ def get_mobile_default_onboarding():
"workspace_join": False,
}
def get_default_tour_completed_features():
return {
"workspace_features": {
"projects": False
},
"project_features": {
"workitems": False,
"cycles": False,
"modules": False,
"views": False,
"pages": False,
},
}
class User(AbstractBaseUser, PermissionsMixin):
id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True)
@@ -214,6 +228,9 @@ class Profile(TimeAuditModel):
goals = models.JSONField(default=dict)
background_color = models.CharField(max_length=255, default=get_random_color)
# product tour features
tour_completed_features = models.JSONField(default=get_default_tour_completed_features)
# marketing
has_marketing_email_consent = models.BooleanField(default=False)