diff --git a/apiserver/plane/api/serializers/issue.py b/apiserver/plane/api/serializers/issue.py index 020917ee5..280dc0208 100644 --- a/apiserver/plane/api/serializers/issue.py +++ b/apiserver/plane/api/serializers/issue.py @@ -11,6 +11,7 @@ from rest_framework import serializers # Module imports from plane.db.models import ( Issue, + IssueType, IssueActivity, IssueAssignee, IssueAttachment, @@ -131,7 +132,16 @@ class IssueSerializer(BaseSerializer): workspace_id = self.context["workspace_id"] default_assignee_id = self.context["default_assignee_id"] - issue = Issue.objects.create(**validated_data, project_id=project_id) + # Get the issue type from the project + issue_type = ( + IssueType.objects.filter(project_id=project_id) + .order_by("created_at") + .first() + ) + + issue = Issue.objects.create( + **validated_data, project_id=project_id, type=issue_type + ) # Issue Audit Users created_by_id = issue.created_by_id @@ -312,10 +322,14 @@ class IssueLinkSerializer(BaseSerializer): return IssueLink.objects.create(**validated_data) def update(self, instance, validated_data): - if IssueLink.objects.filter( - url=validated_data.get("url"), - issue_id=instance.issue_id, - ).exclude(pk=instance.id).exists(): + if ( + IssueLink.objects.filter( + url=validated_data.get("url"), + issue_id=instance.issue_id, + ) + .exclude(pk=instance.id) + .exists() + ): raise serializers.ValidationError( {"error": "URL already exists for this Issue"} ) diff --git a/apiserver/plane/api/views/inbox.py b/apiserver/plane/api/views/inbox.py index 43c9d5652..513dca52b 100644 --- a/apiserver/plane/api/views/inbox.py +++ b/apiserver/plane/api/views/inbox.py @@ -16,6 +16,7 @@ from plane.app.permissions import ProjectLitePermission from plane.bgtasks.issue_activites_task import issue_activity from plane.db.models import ( Inbox, + IssueType, InboxIssue, Issue, Project, @@ -141,6 +142,12 @@ class InboxIssueAPIEndpoint(BaseAPIView): color="#ff7700", is_triage=True, ) + # Get the issue type + issue_type = ( + IssueType.objects.filter(project_id=project_id) + .order_by("created_at") + .first() + ) # create an issue issue = Issue.objects.create( @@ -152,6 +159,7 @@ class InboxIssueAPIEndpoint(BaseAPIView): priority=request.data.get("issue", {}).get("priority", "none"), project_id=project_id, state=state, + type=issue_type, ) # create an inbox issue diff --git a/apiserver/plane/api/views/project.py b/apiserver/plane/api/views/project.py index 408e14fed..1e0234d39 100644 --- a/apiserver/plane/api/views/project.py +++ b/apiserver/plane/api/views/project.py @@ -26,6 +26,7 @@ from plane.db.models import ( ProjectMember, State, Workspace, + IssueType, ) from plane.bgtasks.webhook_task import model_activity from .base import BaseAPIView @@ -240,6 +241,14 @@ class ProjectAPIEndpoint(BaseAPIView): .filter(pk=serializer.data["id"]) .first() ) + + # Create the Issue Types + IssueType.objects.create( + name="Task", + description="A task that needs to be done", + project_id=project.id, + ) + # Model activity model_activity.delay( model_name="project", diff --git a/apiserver/plane/app/serializers/__init__.py b/apiserver/plane/app/serializers/__init__.py index 0511f315e..04717f756 100644 --- a/apiserver/plane/app/serializers/__init__.py +++ b/apiserver/plane/app/serializers/__init__.py @@ -91,6 +91,7 @@ from .page import ( PageLogSerializer, SubPageSerializer, PageDetailSerializer, + PageVersionSerializer, ) from .estimate import ( diff --git a/apiserver/plane/app/serializers/issue.py b/apiserver/plane/app/serializers/issue.py index 318f136da..c1440be51 100644 --- a/apiserver/plane/app/serializers/issue.py +++ b/apiserver/plane/app/serializers/issue.py @@ -33,6 +33,7 @@ from plane.db.models import ( IssueVote, IssueRelation, State, + IssueType, ) @@ -135,7 +136,16 @@ class IssueCreateSerializer(BaseSerializer): workspace_id = self.context["workspace_id"] default_assignee_id = self.context["default_assignee_id"] - issue = Issue.objects.create(**validated_data, project_id=project_id) + # Get Issue Type + issue_type = ( + IssueType.objects.filter(project_id=project_id) + .order_by("created_at") + .first() + ) + # Create Issue + issue = Issue.objects.create( + **validated_data, project_id=project_id, type=issue_type + ) # Issue Audit Users created_by_id = issue.created_by_id diff --git a/apiserver/plane/app/serializers/page.py b/apiserver/plane/app/serializers/page.py index 6e38537f0..c754bd431 100644 --- a/apiserver/plane/app/serializers/page.py +++ b/apiserver/plane/app/serializers/page.py @@ -10,6 +10,7 @@ from plane.db.models import ( Label, ProjectPage, Project, + PageVersion, ) @@ -161,3 +162,13 @@ class PageLogSerializer(BaseSerializer): "workspace", "page", ] + + +class PageVersionSerializer(BaseSerializer): + class Meta: + model = PageVersion + fields = "__all__" + read_only_fields = [ + "workspace", + "page", + ] diff --git a/apiserver/plane/app/urls/page.py b/apiserver/plane/app/urls/page.py index a6d43600f..8ed0bd18d 100644 --- a/apiserver/plane/app/urls/page.py +++ b/apiserver/plane/app/urls/page.py @@ -7,6 +7,7 @@ from plane.app.views import ( PageLogEndpoint, SubPagesEndpoint, PagesDescriptionViewSet, + PageVersionEndpoint, ) @@ -90,4 +91,14 @@ urlpatterns = [ ), name="page-description", ), + path( + "workspaces//projects//pages//versions/", + PageVersionEndpoint.as_view(), + name="page-versions", + ), + path( + "workspaces//projects//pages//versions//", + PageVersionEndpoint.as_view(), + name="page-versions", + ), ] diff --git a/apiserver/plane/app/views/__init__.py b/apiserver/plane/app/views/__init__.py index 913385fa6..bb722de52 100644 --- a/apiserver/plane/app/views/__init__.py +++ b/apiserver/plane/app/views/__init__.py @@ -179,6 +179,7 @@ from .page.base import ( SubPagesEndpoint, PagesDescriptionViewSet, ) +from .page.version import PageVersionEndpoint from .search.base import GlobalSearchEndpoint from .search.issue import IssueSearchEndpoint diff --git a/apiserver/plane/app/views/page/base.py b/apiserver/plane/app/views/page/base.py index 2ce7ce11c..1b2944294 100644 --- a/apiserver/plane/app/views/page/base.py +++ b/apiserver/plane/app/views/page/base.py @@ -38,6 +38,7 @@ from plane.db.models import ( from ..base import BaseAPIView, BaseViewSet from plane.bgtasks.page_transaction_task import page_transaction +from plane.bgtasks.page_version_task import page_version def unarchive_archive_page_and_descendants(page_id, archived_at): @@ -481,16 +482,38 @@ class PagesDescriptionViewSet(BaseViewSet): status=472, ) + # Serialize the existing instance + existing_instance = json.dumps( + { + "description_html": page.description_html, + }, + cls=DjangoJSONEncoder, + ) + + # Get the base64 data from the request base64_data = request.data.get("description_binary") + # If base64 data is provided if base64_data: # Decode the base64 data to bytes new_binary_data = base64.b64decode(base64_data) - + # capture the page transaction + if request.data.get("description_html"): + page_transaction.delay( + new_value=request.data, + old_value=existing_instance, + page_id=pk, + ) # Store the updated binary data page.description_binary = new_binary_data page.description_html = request.data.get("description_html") page.save() + # Return a success response + page_version.delay( + page_id=page.id, + existing_instance=existing_instance, + user_id=request.user.id, + ) return Response({"message": "Updated successfully"}) else: return Response({"error": "No binary data provided"}) diff --git a/apiserver/plane/app/views/page/version.py b/apiserver/plane/app/views/page/version.py new file mode 100644 index 000000000..70f6bd978 --- /dev/null +++ b/apiserver/plane/app/views/page/version.py @@ -0,0 +1,37 @@ +# Third party imports +from rest_framework import status +from rest_framework.response import Response + +# Module imports +from plane.db.models import PageVersion +from ..base import BaseAPIView +from plane.app.permissions import ProjectEntityPermission +from plane.app.serializers import PageVersionSerializer + + +class PageVersionEndpoint(BaseAPIView): + + permission_classes = [ + ProjectEntityPermission, + ] + + def get(self, request, slug, project_id, page_id, pk=None): + # Check if pk is provided + if pk: + # Return a single page version + page_version = PageVersion.objects.get( + workspace__slug=slug, + page_id=page_id, + pk=pk, + ) + # Serialize the page version + serializer = PageVersionSerializer(page_version) + return Response(serializer.data, status=status.HTTP_200_OK) + # Return all page versions + page_versions = PageVersion.objects.filter( + workspace__slug=slug, + page_id=page_id, + ) + # Serialize the page versions + serializer = PageVersionSerializer(page_versions, many=True) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/apiserver/plane/app/views/project/base.py b/apiserver/plane/app/views/project/base.py index 55471e674..745805837 100644 --- a/apiserver/plane/app/views/project/base.py +++ b/apiserver/plane/app/views/project/base.py @@ -47,6 +47,7 @@ from plane.db.models import ( ProjectMember, State, Workspace, + IssueType, ) from plane.utils.cache import cache_response from plane.bgtasks.webhook_task import model_activity @@ -342,6 +343,13 @@ class ProjectViewSet(BaseViewSet): .first() ) + # Create the issue type + IssueType.objects.create( + name="Task", + description="A task that needs to be done", + project_id=project.id, + ) + model_activity.delay( model_name="project", model_id=str(project.id), diff --git a/apiserver/plane/app/views/search/issue.py b/apiserver/plane/app/views/search/issue.py index 50b468715..e4f3252f0 100644 --- a/apiserver/plane/app/views/search/issue.py +++ b/apiserver/plane/app/views/search/issue.py @@ -1,5 +1,4 @@ # Python imports -import re # Django imports from django.db.models import Q @@ -11,13 +10,7 @@ from rest_framework.response import Response # Module imports from .base import BaseAPIView from plane.db.models import ( - Workspace, - Project, Issue, - Cycle, - Module, - Page, - IssueView, ) from plane.utils.issue_search import search_issues diff --git a/apiserver/plane/bgtasks/dummy_data_task.py b/apiserver/plane/bgtasks/dummy_data_task.py index 74e210de6..d1915c3cf 100644 --- a/apiserver/plane/bgtasks/dummy_data_task.py +++ b/apiserver/plane/bgtasks/dummy_data_task.py @@ -21,6 +21,7 @@ from plane.db.models import ( Cycle, Module, Issue, + IssueType, IssueSequence, IssueAssignee, IssueLabel, @@ -336,6 +337,12 @@ def create_issues(workspace, project, user_id, issue_count): 65535 if largest_sort_order is None else largest_sort_order + 10000 ) + issue_type = IssueType.objects.create( + name="Task", + description="A task that needs to be completed.", + project=project, + ) + for _ in range(0, issue_count): start_date = [None, fake.date_this_year()][random.randint(0, 1)] end_date = ( @@ -364,6 +371,7 @@ def create_issues(workspace, project, user_id, issue_count): random.randint(0, 4) ], created_by_id=creators[random.randint(0, len(creators) - 1)], + type=issue_type, ) ) diff --git a/apiserver/plane/bgtasks/page_version_task.py b/apiserver/plane/bgtasks/page_version_task.py new file mode 100644 index 000000000..e29d28a85 --- /dev/null +++ b/apiserver/plane/bgtasks/page_version_task.py @@ -0,0 +1,44 @@ +# Python imports +import json + +# Third party imports +from celery import shared_task + +# Module imports +from plane.db.models import Page, PageVersion + + +@shared_task +def page_version( + page_id, + existing_instance, + user_id, +): + # Get the page + page = Page.objects.get(id=page_id) + + # Get the current instance + current_instance = ( + json.loads(existing_instance) if existing_instance is not None else {} + ) + + # Create a version if description_html is updated + if current_instance.get("description_html") != page.description_html: + # Create a new page version + PageVersion.objects.create( + page_id=page_id, + workspace_id=page.workspace_id, + description_html=page.description_html, + description_binary=page.description_binary, + ownned_by_id=user_id, + last_saved_at=page.updated_at, + ) + + # If page versions are greater than 20 delete the oldest one + if PageVersion.objects.filter(page_id=page_id).count() > 20: + # Delete the old page version + PageVersion.objects.filter(page_id=page_id).order_by( + "last_saved_at" + ).first().delete() + + return diff --git a/apiserver/plane/db/migrations/0070_issuetype_issue_type.py b/apiserver/plane/db/migrations/0070_issuetype_issue_type.py new file mode 100644 index 000000000..a19e5200e --- /dev/null +++ b/apiserver/plane/db/migrations/0070_issuetype_issue_type.py @@ -0,0 +1,307 @@ +# Generated by Django 4.2.11 on 2024-07-01 06:10 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +def create_issue_types(apps, schema_editor): + Project = apps.get_model("db", "Project") + Issue = apps.get_model("db", "Issue") + IssueType = apps.get_model("db", "IssueType") + # Create the issue types for all projects + IssueType.objects.bulk_create( + [ + IssueType( + name="Task", + description="A task that needs to be completed.", + project_id=project["id"], + workspace_id=project["workspace_id"], + ) + for project in Project.objects.values("id", "workspace_id") + ], + batch_size=1000, + ) + # Update the issue type for all existing issues + issue_types = { + str(issue_type["project_id"]): str(issue_type["id"]) + for issue_type in IssueType.objects.values("id", "project_id") + } + # Update the issue type for all existing issues + bulk_issues = [] + for issue in Issue.objects.all(): + issue.type_id = issue_types[str(issue.project_id)] + bulk_issues.append(issue) + + # Update the issue type for all existing issues + Issue.objects.bulk_update(bulk_issues, ["type_id"], batch_size=1000) + + +def create_page_versions(apps, schema_editor): + Page = apps.get_model("db", "Page") + PageVersion = apps.get_model("db", "PageVersion") + # Create the page versions for all pages + PageVersion.objects.bulk_create( + [ + PageVersion( + page_id=page["id"], + workspace_id=page["workspace_id"], + description_html=page["description_html"], + description_binary=page["description_binary"], + description_stripped=page["description_stripped"], + owned_by_id=page["owned_by_id"], + last_saved_at=page["updated_at"], + ) + for page in Page.objects.values( + "id", + "workspace_id", + "description_html", + "description_binary", + "description_stripped", + "owned_by_id", + "updated_at", + ) + ], + batch_size=1000, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("db", "0069_alter_account_provider_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="IssueType", + fields=[ + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ("name", models.CharField(max_length=255)), + ("description", models.TextField(blank=True)), + ("logo_props", models.JSONField(default=dict)), + ("sort_order", models.FloatField(default=65535)), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "project", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="project_%(class)s", + to="db.project", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="workspace_%(class)s", + to="db.workspace", + ), + ), + ( + "is_default", + models.BooleanField(default=True), + ), + ], + options={ + "verbose_name": "Issue Type", + "verbose_name_plural": "Issue Types", + "db_table": "issue_types", + "ordering": ("sort_order",), + }, + ), + migrations.AddField( + model_name="issue", + name="type", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="issue_type", + to="db.issuetype", + ), + ), + migrations.AddField( + model_name="apitoken", + name="is_service", + field=models.BooleanField(default=False), + ), + migrations.RunPython(create_issue_types), + migrations.CreateModel( + name="PageVersion", + fields=[ + ( + "created_at", + models.DateTimeField( + auto_now_add=True, verbose_name="Created At" + ), + ), + ( + "updated_at", + models.DateTimeField( + auto_now=True, verbose_name="Last Modified At" + ), + ), + ( + "id", + models.UUIDField( + db_index=True, + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + unique=True, + ), + ), + ( + "last_saved_at", + models.DateTimeField(default=django.utils.timezone.now), + ), + ("description_binary", models.BinaryField(null=True)), + ( + "description_html", + models.TextField(blank=True, default="

"), + ), + ( + "description_stripped", + models.TextField(blank=True, null=True), + ), + ( + "created_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_created_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Created By", + ), + ), + ( + "owned_by", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="page_versions", + to=settings.AUTH_USER_MODEL, + ), + ), + ( + "page", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="page_versions", + to="db.page", + ), + ), + ( + "updated_by", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="%(class)s_updated_by", + to=settings.AUTH_USER_MODEL, + verbose_name="Last Modified By", + ), + ), + ( + "workspace", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="page_versions", + to="db.workspace", + ), + ), + ], + options={ + "verbose_name": "Page Version", + "verbose_name_plural": "Page Versions", + "db_table": "page_versions", + "ordering": ("-created_at",), + }, + ), + migrations.AddField( + model_name="exporterhistory", + name="filters", + field=models.JSONField(blank=True, null=True), + ), + migrations.AddField( + model_name="exporterhistory", + name="name", + field=models.CharField( + blank=True, + max_length=255, + null=True, + verbose_name="Exporter Name", + ), + ), + migrations.AddField( + model_name="exporterhistory", + name="type", + field=models.CharField( + choices=[ + ("issue_exports", "Issue Exports"), + ("issue_work_logs", "Issue Work Logs"), + ], + default="issue_exports", + max_length=50, + ), + ), + migrations.AddField( + model_name="project", + name="is_time_tracking_enabled", + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name="project", + name="start_date", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name="project", + name="target_date", + field=models.DateTimeField(blank=True, null=True), + ), + migrations.RunPython(create_page_versions), + ] diff --git a/apiserver/plane/db/models/__init__.py b/apiserver/plane/db/models/__init__.py index 38586791d..89cc88bbd 100644 --- a/apiserver/plane/db/models/__init__.py +++ b/apiserver/plane/db/models/__init__.py @@ -50,7 +50,14 @@ from .notification import ( Notification, UserNotificationPreference, ) -from .page import Page, PageFavorite, PageLabel, PageLog, ProjectPage +from .page import ( + Page, + PageFavorite, + PageLabel, + PageLog, + ProjectPage, + PageVersion, +) from .project import ( Project, ProjectBaseModel, @@ -101,3 +108,5 @@ from .webhook import Webhook, WebhookLog from .dashboard import Dashboard, DashboardWidget, Widget from .favorite import UserFavorite + +from .issue_type import IssueType diff --git a/apiserver/plane/db/models/api.py b/apiserver/plane/db/models/api.py index 78da81814..bc24ee8a8 100644 --- a/apiserver/plane/db/models/api.py +++ b/apiserver/plane/db/models/api.py @@ -44,6 +44,7 @@ class APIToken(BaseModel): null=True, ) expired_at = models.DateTimeField(blank=True, null=True) + is_service = models.BooleanField(default=False) class Meta: verbose_name = "API Token" diff --git a/apiserver/plane/db/models/exporter.py b/apiserver/plane/db/models/exporter.py index 9790db68d..a6f5110e3 100644 --- a/apiserver/plane/db/models/exporter.py +++ b/apiserver/plane/db/models/exporter.py @@ -18,6 +18,17 @@ def generate_token(): class ExporterHistory(BaseModel): + name = models.CharField( + max_length=255, verbose_name="Exporter Name", null=True, blank=True + ) + type = models.CharField( + max_length=50, + default="issue_exports", + choices=( + ("issue_exports", "Issue Exports"), + ("issue_work_logs", "Issue Work Logs"), + ), + ) workspace = models.ForeignKey( "db.WorkSpace", on_delete=models.CASCADE, @@ -55,6 +66,7 @@ class ExporterHistory(BaseModel): on_delete=models.CASCADE, related_name="workspace_exporters", ) + filters = models.JSONField(blank=True, null=True) class Meta: verbose_name = "Exporter" diff --git a/apiserver/plane/db/models/issue.py b/apiserver/plane/db/models/issue.py index e82b974ed..a317b943e 100644 --- a/apiserver/plane/db/models/issue.py +++ b/apiserver/plane/db/models/issue.py @@ -164,6 +164,13 @@ class Issue(ProjectBaseModel): is_draft = models.BooleanField(default=False) external_source = models.CharField(max_length=255, null=True, blank=True) external_id = models.CharField(max_length=255, blank=True, null=True) + type = models.ForeignKey( + "db.IssueType", + on_delete=models.SET_NULL, + related_name="issue_type", + null=True, + blank=True, + ) objects = models.Manager() issue_objects = IssueManager() diff --git a/apiserver/plane/db/models/issue_type.py b/apiserver/plane/db/models/issue_type.py new file mode 100644 index 000000000..33affb660 --- /dev/null +++ b/apiserver/plane/db/models/issue_type.py @@ -0,0 +1,34 @@ +# Django imports +from django.db import models + +# Module imports +from .workspace import WorkspaceBaseModel + + +class IssueType(WorkspaceBaseModel): + name = models.CharField(max_length=255) + description = models.TextField(blank=True) + logo_props = models.JSONField(default=dict) + sort_order = models.FloatField(default=65535) + is_default = models.BooleanField(default=True) + + class Meta: + verbose_name = "Issue Type" + verbose_name_plural = "Issue Types" + db_table = "issue_types" + ordering = ("sort_order",) + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + # If we are adding a new issue type, we need to set the sort order + if self._state.adding: + # Get the largest sort order for the project + largest_sort_order = IssueType.objects.filter( + project=self.project + ).aggregate(largest=models.Max("sort_order"))["largest"] + # If there are issue types, set the sort order to the largest + 10000 + if largest_sort_order is not None: + self.sort_order = largest_sort_order + 10000 + super(IssueType, self).save(*args, **kwargs) diff --git a/apiserver/plane/db/models/page.py b/apiserver/plane/db/models/page.py index 721cf005e..71e9d3c25 100644 --- a/apiserver/plane/db/models/page.py +++ b/apiserver/plane/db/models/page.py @@ -1,6 +1,7 @@ import uuid from django.conf import settings +from django.utils import timezone # Django imports from django.db import models @@ -66,6 +67,15 @@ class Page(BaseModel): """Return owner email and page name""" return f"{self.owned_by.email} <{self.name}>" + def save(self, *args, **kwargs): + # Strip the html tags using html parser + self.description_stripped = ( + None + if (self.description_html == "" or self.description_html is None) + else strip_tags(self.description_html) + ) + super(Page, self).save(*args, **kwargs) + class PageLog(BaseModel): TYPE_CHOICES = ( @@ -249,3 +259,40 @@ class TeamPage(BaseModel): verbose_name_plural = "Team Pages" db_table = "team_pages" ordering = ("-created_at",) + + +class PageVersion(BaseModel): + workspace = models.ForeignKey( + "db.Workspace", + on_delete=models.CASCADE, + related_name="page_versions", + ) + page = models.ForeignKey( + "db.Page", + on_delete=models.CASCADE, + related_name="page_versions", + ) + last_saved_at = models.DateTimeField(default=timezone.now) + owned_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name="page_versions", + ) + description_binary = models.BinaryField(null=True) + description_html = models.TextField(blank=True, default="

") + description_stripped = models.TextField(blank=True, null=True) + + class Meta: + verbose_name = "Page Version" + verbose_name_plural = "Page Versions" + db_table = "page_versions" + ordering = ("-created_at",) + + def save(self, *args, **kwargs): + # Strip the html tags using html parser + self.description_stripped = ( + None + if (self.description_html == "" or self.description_html is None) + else strip_tags(self.description_html) + ) + super(PageVersion, self).save(*args, **kwargs) diff --git a/apiserver/plane/db/models/project.py b/apiserver/plane/db/models/project.py index ba8dbf580..a08ef2470 100644 --- a/apiserver/plane/db/models/project.py +++ b/apiserver/plane/db/models/project.py @@ -94,6 +94,7 @@ class Project(BaseModel): issue_views_view = models.BooleanField(default=True) page_view = models.BooleanField(default=True) inbox_view = models.BooleanField(default=False) + is_time_tracking_enabled = models.BooleanField(default=False) cover_image = models.URLField(blank=True, null=True, max_length=800) estimate = models.ForeignKey( "db.Estimate", @@ -115,6 +116,9 @@ class Project(BaseModel): related_name="default_state", ) archived_at = models.DateTimeField(null=True) + # Project start and target date + start_date = models.DateTimeField(null=True, blank=True) + target_date = models.DateTimeField(null=True, blank=True) def __str__(self): """Return name of the project"""