From c91d632e97d8d3fd4f5d96a56cd78b9c20108b2e Mon Sep 17 00:00:00 2001 From: Dheeraj Kumar Ketireddy Date: Fri, 13 Jun 2025 18:51:52 +0530 Subject: [PATCH] [SILO-316] Migration: Added cover_image_asset to Template model (#3404) * Added cover_image_asset to Template model * use the right property * Test case for Template Model * Added is_mentionable boolean field to Application --- .../0004_application_is_mentionable.py | 18 ++ .../plane/authentication/models/oauth.py | 2 + .../0039_template_cover_image_asset.py | 20 +++ apiserver/plane/ee/models/template.py | 15 ++ apiserver/plane/tests/conftest.py | 45 ++++- .../plane/tests/unit/models/test_template.py | 169 ++++++++++++++++++ 6 files changed, 268 insertions(+), 1 deletion(-) create mode 100644 apiserver/plane/authentication/migrations/0004_application_is_mentionable.py create mode 100644 apiserver/plane/ee/migrations/0039_template_cover_image_asset.py create mode 100644 apiserver/plane/tests/unit/models/test_template.py diff --git a/apiserver/plane/authentication/migrations/0004_application_is_mentionable.py b/apiserver/plane/authentication/migrations/0004_application_is_mentionable.py new file mode 100644 index 0000000000..475771a9df --- /dev/null +++ b/apiserver/plane/authentication/migrations/0004_application_is_mentionable.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.21 on 2025-06-13 11:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0003_application_website_applicationcategory_is_active'), + ] + + operations = [ + migrations.AddField( + model_name='application', + name='is_mentionable', + field=models.BooleanField(default=False), + ), + ] diff --git a/apiserver/plane/authentication/models/oauth.py b/apiserver/plane/authentication/models/oauth.py index 1df2f30b5b..22abdde70e 100644 --- a/apiserver/plane/authentication/models/oauth.py +++ b/apiserver/plane/authentication/models/oauth.py @@ -66,6 +66,8 @@ class Application(AbstractApplication, UserAuditModel, SoftDeleteModel): video_url = models.URLField(max_length=800, null=True, blank=True) website = models.URLField(max_length=800, null=True, blank=True) + is_mentionable = models.BooleanField(default=False) + objects = ApplicationManager() class Meta(AbstractApplication.Meta): diff --git a/apiserver/plane/ee/migrations/0039_template_cover_image_asset.py b/apiserver/plane/ee/migrations/0039_template_cover_image_asset.py new file mode 100644 index 0000000000..09abb2d139 --- /dev/null +++ b/apiserver/plane/ee/migrations/0039_template_cover_image_asset.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.21 on 2025-06-13 08:02 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('db', '0097_project_external_id_project_external_source'), + ('ee', '0038_importreport_completed_batch_count_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='template', + name='cover_image_asset', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cover_image_templates', to='db.fileasset'), + ), + ] diff --git a/apiserver/plane/ee/models/template.py b/apiserver/plane/ee/models/template.py index b09c09c1cc..b49a5a86ff 100644 --- a/apiserver/plane/ee/models/template.py +++ b/apiserver/plane/ee/models/template.py @@ -47,6 +47,13 @@ class Template(WorkspaceBaseModel): through="ee.TemplateAttachment", blank=True, ) + cover_image_asset = models.ForeignKey( + "db.FileAsset", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="cover_image_templates", + ) short_description = models.TextField(blank=True, null=True) privacy_policy_url = models.URLField(max_length=800, null=True, blank=True) terms_of_service_url = models.URLField(max_length=800, null=True, blank=True) @@ -71,6 +78,14 @@ class Template(WorkspaceBaseModel): ) super(Template, self).save(*args, **kwargs) + @property + def cover_image_url(self): + # Return cover image url + if self.cover_image_asset: + return self.cover_image_asset.asset_url + + return None + class TemplateAttachment(BaseModel): template = models.ForeignKey( diff --git a/apiserver/plane/tests/conftest.py b/apiserver/plane/tests/conftest.py index 0dc2013b45..612a5e4f32 100644 --- a/apiserver/plane/tests/conftest.py +++ b/apiserver/plane/tests/conftest.py @@ -3,8 +3,9 @@ from django.conf import settings from rest_framework.test import APIClient from pytest_django.fixtures import django_db_setup from unittest.mock import patch, MagicMock +from uuid import uuid4 -from plane.db.models import User +from plane.db.models import User, Workspace, FileAsset from plane.db.models.api import APIToken @@ -44,6 +45,48 @@ def create_user(db, user_data): return user +@pytest.fixture +def workspace(db, create_user): + """Create and return a workspace instance""" + workspace = Workspace.objects.create( + name="Test Workspace", slug="test-workspace", id=uuid4(), owner=create_user + ) + return workspace + + +@pytest.fixture +def file_asset(db, workspace): + """Create and return a basic FileAsset instance for testing""" + file_asset = FileAsset.objects.create( + workspace=workspace, + asset="test_file.txt", + attributes={ + "name": "test_file.txt", + "size": 1024, + "mime_type": "text/plain", + }, + size=1024, + ) + return file_asset + + +@pytest.fixture +def cover_image_asset(db, workspace): + """Create and return a FileAsset instance specifically for cover images""" + file_asset = FileAsset.objects.create( + workspace=workspace, + asset="cover_image.jpg", + attributes={ + "name": "cover_image.jpg", + "size": 2048, + "mime_type": "image/jpeg", + }, + size=2048, + entity_type=FileAsset.EntityTypeContext.PROJECT_COVER, + ) + return file_asset + + @pytest.fixture def api_token(db, create_user): """Create and return an API token for testing the external API""" diff --git a/apiserver/plane/tests/unit/models/test_template.py b/apiserver/plane/tests/unit/models/test_template.py new file mode 100644 index 0000000000..019738b4b6 --- /dev/null +++ b/apiserver/plane/tests/unit/models/test_template.py @@ -0,0 +1,169 @@ +import pytest +from django.core.exceptions import ValidationError + +from plane.ee.models.template import Template + + +@pytest.mark.unit +class TestTemplateModel: + """Test the Template model""" + + @pytest.mark.django_db + def test_template_creation(self, workspace): + """Test creating a template with basic fields""" + # Arrange + template_data = { + "name": "Test Template", + "workspace": workspace, + "template_type": Template.TemplateType.WORKITEM, + "description": {"content": "Test description"}, + "description_html": "

Test description

", + "company_name": "Test Company", + "short_description": "Short test description", + } + + # Act + template = Template.objects.create(**template_data) + + # Assert + # Basic fields + assert template.id is not None + assert template.name == template_data["name"] + assert template.template_type == template_data["template_type"] + assert template.description == template_data["description"] + assert template.description_html == template_data["description_html"] + assert template.company_name == template_data["company_name"] + assert template.short_description == template_data["short_description"] + + # Default values + assert template.is_published is False + assert template.is_verified is False + assert template.supported_languages == {} + assert template.support == {} + assert template.resources == {} + assert template.keywords == [] + + @pytest.mark.django_db + def test_template_description_stripping(self, workspace): + """Test that HTML tags are stripped from description_html""" + # Arrange + html_content = "

Test description with HTML

" + expected_stripped = "Test description with HTML" + + # Act + template = Template.objects.create( + name="Test Template", + workspace=workspace, + description_html=html_content, + ) + + # Assert + assert template.description_stripped == expected_stripped + + @pytest.mark.django_db + def test_template_empty_description(self, workspace): + """Test template with empty description""" + # Arrange + empty_html = "" + + # Act + template = Template.objects.create( + name="Test Template", + workspace=workspace, + description_html=empty_html, + ) + + # Assert + assert template.description_stripped is None + + @pytest.mark.django_db + def test_template_type_choices(self, workspace): + """Test template type choices""" + # Arrange + valid_types = Template.TemplateType.values + invalid_type = "invalid_type" + + # Act & Assert for valid types + for template_type in valid_types: + template = Template.objects.create( + name=f"Test {template_type}", + workspace=workspace, + template_type=template_type, + ) + assert template.template_type == template_type + + # Act & Assert for invalid type + template = Template( + name="Invalid Template", + workspace=workspace, + template_type=invalid_type, + ) + with pytest.raises(ValidationError): + template.full_clean() + + @pytest.mark.django_db + def test_template_url_fields(self, workspace): + """Test template URL fields""" + # Arrange + url_data = { + "privacy_policy_url": "https://example.com/privacy", + "terms_of_service_url": "https://example.com/terms", + "support_url": "https://example.com/support", + "video_url": "https://example.com/video", + "website": "https://example.com", + } + + # Act + template = Template.objects.create( + name="Test Template", + workspace=workspace, + **url_data, + ) + + # Assert + for field, expected_url in url_data.items(): + assert getattr(template, field) == expected_url + + @pytest.mark.django_db + def test_template_contact_email(self, workspace): + """Test template contact email field""" + # Arrange + valid_email = "test@example.com" + invalid_email = "invalid-email" + + # Act & Assert for valid email + template = Template.objects.create( + name="Test Template", + workspace=workspace, + contact_email=valid_email, + ) + assert template.contact_email == valid_email + + # Act & Assert for invalid email + template = Template( + name="Invalid Template", + workspace=workspace, + contact_email=invalid_email, + ) + with pytest.raises(ValidationError): + template.full_clean() + + @pytest.mark.django_db + def test_template_cover_image_url(self, workspace, cover_image_asset): + """Test template cover image URL property""" + # Arrange + template_name = "Test Template" + + # Act - Test without cover image + template = Template.objects.create( + name=template_name, + workspace=workspace, + ) + assert template.cover_image_url is None + + # Act - Test with cover image + template.cover_image_asset = cover_image_asset + template.save() + + # Assert + assert template.cover_image_url == cover_image_asset.asset_url