Compare commits

..

3 Commits

Author SHA1 Message Date
NarayanBavisetti 0ed54f47ce fix: issue vote migration file 2023-08-29 13:30:35 +05:30
NarayanBavisetti 590c33cf41 fix: added default as 1 in vote 2023-08-29 13:23:12 +05:30
NarayanBavisetti 84451f250a fix: issue votes 2023-08-29 13:11:38 +05:30
1407 changed files with 17917 additions and 48570 deletions
+52
View File
@@ -1,3 +1,36 @@
# Frontend
# Extra image domains that need to be added for Next Image
NEXT_PUBLIC_EXTRA_IMAGE_DOMAINS=
# Google Client ID for Google OAuth
NEXT_PUBLIC_GOOGLE_CLIENTID=""
# Github ID for Github OAuth
NEXT_PUBLIC_GITHUB_ID=""
# Github App Name for GitHub Integration
NEXT_PUBLIC_GITHUB_APP_NAME=""
# Sentry DSN for error monitoring
NEXT_PUBLIC_SENTRY_DSN=""
# Enable/Disable OAUTH - default 0 for selfhosted instance
NEXT_PUBLIC_ENABLE_OAUTH=0
# Enable/Disable sentry
NEXT_PUBLIC_ENABLE_SENTRY=0
# Enable/Disable session recording
NEXT_PUBLIC_ENABLE_SESSION_RECORDER=0
# Enable/Disable event tracking
NEXT_PUBLIC_TRACK_EVENTS=0
# Slack for Slack Integration
NEXT_PUBLIC_SLACK_CLIENT_ID=""
# For Telemetry, set it to "app.plane.so"
NEXT_PUBLIC_PLAUSIBLE_DOMAIN=""
# public boards deploy url
NEXT_PUBLIC_DEPLOY_URL=""
# Backend
# Debug value for api server use it as 0 for production use
DEBUG=0
# Error logs
SENTRY_DSN=""
# Database Settings
PGUSER="plane"
PGPASSWORD="plane"
@@ -10,6 +43,15 @@ REDIS_HOST="plane-redis"
REDIS_PORT="6379"
REDIS_URL="redis://${REDIS_HOST}:6379/"
# Email Settings
EMAIL_HOST=""
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
EMAIL_PORT=587
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
EMAIL_USE_TLS="1"
EMAIL_USE_SSL="0"
# AWS Settings
AWS_REGION=""
AWS_ACCESS_KEY_ID="access-key"
@@ -25,6 +67,9 @@ OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
OPENAI_API_KEY="sk-" # add your openai key here
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
# Github
GITHUB_CLIENT_SECRET="" # For fetching release notes
# Settings related to Docker
DOCKERIZED=1
# set to 1 If using the pre-configured minio setup
@@ -33,3 +78,10 @@ USE_MINIO=1
# Nginx Configuration
NGINX_PORT=80
# Default Creds
DEFAULT_EMAIL="captain@plane.so"
DEFAULT_PASSWORD="password123"
# SignUps
ENABLE_SIGNUP="1"
# Auto generated and Required that will be generated from setup.sh
+1 -1
View File
@@ -4,7 +4,7 @@ module.exports = {
extends: ["custom"],
settings: {
next: {
rootDir: ["web/", "space/"],
rootDir: ["apps/*"],
},
},
};
@@ -29,21 +29,26 @@ jobs:
apiserver:
- apiserver/**
web:
- web/**
- apps/app/**
deploy:
- space/**
- apps/space/**
- name: Setup .npmrc for repository
run: |
echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc
- name: Build Plane's Main App
if: steps.changed-files.outputs.web_any_changed == 'true'
run: |
cd web
mv ./.npmrc ./apps/app
cd apps/app
yarn
yarn build
- name: Build Plane's Deploy App
if: steps.changed-files.outputs.deploy_any_changed == 'true'
run: |
cd space
cd apps/space
yarn
yarn build
+7 -3
View File
@@ -2,7 +2,7 @@ name: Update Docker Images for Plane on Release
on:
release:
types: [released, prereleased]
types: [released]
jobs:
build_push_backend:
@@ -22,6 +22,10 @@ jobs:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup .npmrc for repository
run: |
echo -e "@tiptap-pro:registry=https://registry.tiptap.dev/\n//registry.tiptap.dev/:_authToken=${{ secrets.TIPTAP_TOKEN }}" > .npmrc
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
id: metaFrontend
uses: docker/metadata-action@v4.3.0
@@ -58,7 +62,7 @@ jobs:
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./web/Dockerfile.web
file: ./apps/app/Dockerfile.web
platforms: linux/amd64
tags: ${{ steps.metaFrontend.outputs.tags }}
push: true
@@ -84,7 +88,7 @@ jobs:
uses: docker/build-push-action@v4.0.0
with:
context: .
file: ./space/Dockerfile.space
file: ./apps/space/Dockerfile.space
platforms: linux/amd64
push: true
tags: ${{ steps.metaDeploy.outputs.tags }}
+12 -12
View File
@@ -17,23 +17,23 @@ diverse, inclusive, and healthy community.
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
* Demonstrating empathy and kindness toward other people
* Being respectful of differing opinions, viewpoints, and experiences
* Giving and gracefully accepting constructive feedback
* Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
* Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
* The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
* Trolling, insulting or derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
@@ -106,7 +106,7 @@ Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
@@ -125,4 +125,4 @@ enforcement ladder](https://github.com/mozilla/diversity).
For answers to common questions about this code of conduct, see the FAQ at
https://www.contributor-covenant.org/faq. Translations are available at
https://www.contributor-covenant.org/translations.
https://www.contributor-covenant.org/translations.
+24 -11
View File
@@ -35,10 +35,12 @@
Meet [Plane](https://plane.so). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind 🧘‍♀️.
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve on our upcoming releases.
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account. Plane Cloud offers a hosted solution for Plane. If you prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/self-hosting).
## ⚡️ Quick start with Docker Compose
### Docker Compose Setup
@@ -54,30 +56,40 @@ chmod +x setup.sh
- Run setup.sh
```bash
./setup.sh http://localhost
./setup.sh http://localhost
```
> If running in a cloud env replace localhost with public facing IP address of the VM
- Setup Tiptap Pro
Visit [Tiptap Pro](https://collab.tiptap.dev/pro-extensions) and signup (it is free).
Create a **`.npmrc`** file, copy the following and replace your registry token generated from Tiptap Pro.
```
@tiptap-pro:registry=https://registry.tiptap.dev/
//registry.tiptap.dev/:_authToken=YOUR_REGISTRY_TOKEN
```
- Run Docker compose up
```bash
docker compose up -d
```
<strong>You can use the default email and password for your first login `captain@plane.so` and `password123`.</strong>
<strong>You can use the default email and password for your first login `captain@plane.so` and `password123`.</strong>
## 🚀 Features
- **Issue Planning and Tracking**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to issues for better organization and tracking.
- **Issue Attachments**: Collaborate effectively by attaching files to issues, making it easy for your team to find and share important project-related documents.
- **Layouts**: Customize your project view with your preferred layout - choose from List, Kanban, or Calendar to visualize your project in a way that makes sense to you.
- **Cycles**: Plan sprints with Cycles to keep your team on track and productive. Gain insights into your project's progress with burn-down charts and other useful features.
- **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to easily track and plan your project's progress.
- **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks.
- **Pages**: Plane pages function as an AI-powered notepad, allowing you to easily document issues, cycle plans, and module details, and then synchronize them with your issues.
- **Command K**: Enjoy a better user experience with the new Command + K menu. Easily manage and navigate through your projects from one convenient location.
- **GitHub Sync**: Streamline your planning process by syncing your GitHub issues with Plane. Keep all your issues in one place for better tracking and collaboration.
* **Issue Planning and Tracking**: Quickly create issues and add details using a powerful rich text editor that supports file uploads. Add sub-properties and references to issues for better organization and tracking.
* **Issue Attachments**: Collaborate effectively by attaching files to issues, making it easy for your team to find and share important project-related documents.
* **Layouts**: Customize your project view with your preferred layout - choose from List, Kanban, or Calendar to visualize your project in a way that makes sense to you.
* **Cycles**: Plan sprints with Cycles to keep your team on track and productive. Gain insights into your project's progress with burn-down charts and other useful features.
* **Modules**: Break down your large projects into smaller, more manageable modules. Assign modules between teams to easily track and plan your project's progress.
* **Views**: Create custom filters to display only the issues that matter to you. Save and share your filters in just a few clicks.
* **Pages**: Plane pages function as an AI-powered notepad, allowing you to easily document issues, cycle plans, and module details, and then synchronize them with your issues.
* **Command K**: Enjoy a better user experience with the new Command + K menu. Easily manage and navigate through your projects from one convenient location.
* **GitHub Sync**: Streamline your planning process by syncing your GitHub issues with Plane. Keep all your issues in one place for better tracking and collaboration.
## 📸 Screenshots
@@ -138,6 +150,7 @@ docker compose up -d
</p>
</p>
## 📚Documentation
For full documentation, visit [docs.plane.so](https://docs.plane.so/)
-61
View File
@@ -1,61 +0,0 @@
# Backend
# Debug value for api server use it as 0 for production use
DEBUG=0
DJANGO_SETTINGS_MODULE="plane.settings.selfhosted"
# Error logs
SENTRY_DSN=""
# Database Settings
PGUSER="plane"
PGPASSWORD="plane"
PGHOST="plane-db"
PGDATABASE="plane"
DATABASE_URL=postgresql://${PGUSER}:${PGPASSWORD}@${PGHOST}/${PGDATABASE}
# Redis Settings
REDIS_HOST="plane-redis"
REDIS_PORT="6379"
REDIS_URL="redis://${REDIS_HOST}:6379/"
# Email Settings
EMAIL_HOST=""
EMAIL_HOST_USER=""
EMAIL_HOST_PASSWORD=""
EMAIL_PORT=587
EMAIL_FROM="Team Plane <team@mailer.plane.so>"
EMAIL_USE_TLS="1"
EMAIL_USE_SSL="0"
# AWS Settings
AWS_REGION=""
AWS_ACCESS_KEY_ID="access-key"
AWS_SECRET_ACCESS_KEY="secret-key"
AWS_S3_ENDPOINT_URL="http://plane-minio:9000"
# Changing this requires change in the nginx.conf for uploads if using minio setup
AWS_S3_BUCKET_NAME="uploads"
# Maximum file upload limit
FILE_SIZE_LIMIT=5242880
# GPT settings
OPENAI_API_BASE="https://api.openai.com/v1" # change if using a custom endpoint
OPENAI_API_KEY="sk-" # add your openai key here
GPT_ENGINE="gpt-3.5-turbo" # use "gpt-4" if you have access
# Github
GITHUB_CLIENT_SECRET="" # For fetching release notes
# Settings related to Docker
DOCKERIZED=1
# set to 1 If using the pre-configured minio setup
USE_MINIO=1
# Nginx Configuration
NGINX_PORT=80
# Default Creds
DEFAULT_EMAIL="captain@plane.so"
DEFAULT_PASSWORD="password123"
# SignUps
ENABLE_SIGNUP="1"
+1 -1
View File
@@ -1,3 +1,3 @@
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
web: gunicorn -w 4 -k uvicorn.workers.UvicornWorker plane.asgi:application --bind 0.0.0.0:$PORT --config gunicorn.config.py --max-requests 10000 --max-requests-jitter 1000 --access-logfile -
worker: celery -A plane worker -l info
beat: celery -A plane beat -l INFO
+3 -5
View File
@@ -20,10 +20,9 @@ from .project import (
ProjectMemberLiteSerializer,
ProjectDeployBoardSerializer,
ProjectMemberAdminSerializer,
ProjectPublicMemberSerializer
)
from .state import StateSerializer, StateLiteSerializer
from .view import GlobalViewSerializer, IssueViewSerializer, IssueViewFavoriteSerializer
from .view import IssueViewSerializer, IssueViewFavoriteSerializer
from .cycle import CycleSerializer, CycleIssueSerializer, CycleFavoriteSerializer, CycleWriteSerializer
from .asset import FileAssetSerializer
from .issue import (
@@ -31,6 +30,8 @@ from .issue import (
IssueActivitySerializer,
IssueCommentSerializer,
IssuePropertySerializer,
BlockerIssueSerializer,
BlockedIssueSerializer,
IssueAssigneeSerializer,
LabelSerializer,
IssueSerializer,
@@ -43,9 +44,6 @@ from .issue import (
IssueReactionSerializer,
CommentReactionSerializer,
IssueVoteSerializer,
IssueRelationSerializer,
RelatedIssueSerializer,
IssuePublicSerializer,
)
from .module import (
+126 -69
View File
@@ -17,10 +17,12 @@ from plane.db.models import (
IssueActivity,
IssueComment,
IssueProperty,
IssueBlocker,
IssueAssignee,
IssueSubscriber,
IssueLabel,
Label,
IssueBlocker,
CycleIssue,
Cycle,
Module,
@@ -30,7 +32,6 @@ from plane.db.models import (
IssueReaction,
CommentReaction,
IssueVote,
IssueRelation,
)
@@ -49,7 +50,6 @@ class IssueFlatSerializer(BaseSerializer):
"target_date",
"sequence_id",
"sort_order",
"is_draft",
]
@@ -81,12 +81,25 @@ class IssueCreateSerializer(BaseSerializer):
required=False,
)
# List of issues that are blocking this issue
blockers_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()),
write_only=True,
required=False,
)
labels_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Label.objects.all()),
write_only=True,
required=False,
)
# List of issues that are blocked by this issue
blocks_list = serializers.ListField(
child=serializers.PrimaryKeyRelatedField(queryset=Issue.objects.all()),
write_only=True,
required=False,
)
class Meta:
model = Issue
fields = "__all__"
@@ -100,17 +113,15 @@ class IssueCreateSerializer(BaseSerializer):
]
def validate(self, data):
if (
data.get("start_date", None) is not None
and data.get("target_date", None) is not None
and data.get("start_date", None) > data.get("target_date", None)
):
if data.get("start_date", None) is not None and data.get("target_date", None) is not None and data.get("start_date", None) > data.get("target_date", None):
raise serializers.ValidationError("Start date cannot exceed target date")
return data
def create(self, validated_data):
blockers = validated_data.pop("blockers_list", None)
assignees = validated_data.pop("assignees_list", None)
labels = validated_data.pop("labels_list", None)
blocks = validated_data.pop("blocks_list", None)
project_id = self.context["project_id"]
workspace_id = self.context["workspace_id"]
@@ -122,6 +133,22 @@ class IssueCreateSerializer(BaseSerializer):
created_by_id = issue.created_by_id
updated_by_id = issue.updated_by_id
if blockers is not None and len(blockers):
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
block=issue,
blocked_by=blocker,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for blocker in blockers
],
batch_size=10,
)
if assignees is not None and len(assignees):
IssueAssignee.objects.bulk_create(
[
@@ -165,11 +192,29 @@ class IssueCreateSerializer(BaseSerializer):
batch_size=10,
)
if blocks is not None and len(blocks):
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
block=block,
blocked_by=issue,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for block in blocks
],
batch_size=10,
)
return issue
def update(self, instance, validated_data):
blockers = validated_data.pop("blockers_list", None)
assignees = validated_data.pop("assignees_list", None)
labels = validated_data.pop("labels_list", None)
blocks = validated_data.pop("blocks_list", None)
# Related models
project_id = instance.project_id
@@ -177,6 +222,23 @@ class IssueCreateSerializer(BaseSerializer):
created_by_id = instance.created_by_id
updated_by_id = instance.updated_by_id
if blockers is not None:
IssueBlocker.objects.filter(block=instance).delete()
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
block=instance,
blocked_by=blocker,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for blocker in blockers
],
batch_size=10,
)
if assignees is not None:
IssueAssignee.objects.filter(issue=instance).delete()
IssueAssignee.objects.bulk_create(
@@ -211,6 +273,23 @@ class IssueCreateSerializer(BaseSerializer):
batch_size=10,
)
if blocks is not None:
IssueBlocker.objects.filter(blocked_by=instance).delete()
IssueBlocker.objects.bulk_create(
[
IssueBlocker(
block=block,
blocked_by=instance,
project_id=project_id,
workspace_id=workspace_id,
created_by_id=created_by_id,
updated_by_id=updated_by_id,
)
for block in blocks
],
batch_size=10,
)
# Time updation occues even when other related models are updated
instance.updated_at = timezone.now()
return super().update(instance, validated_data)
@@ -292,39 +371,32 @@ class IssueLabelSerializer(BaseSerializer):
]
class IssueRelationSerializer(BaseSerializer):
issue_detail = IssueProjectLiteSerializer(read_only=True, source="related_issue")
class BlockedIssueSerializer(BaseSerializer):
blocked_issue_detail = IssueProjectLiteSerializer(source="block", read_only=True)
class Meta:
model = IssueRelation
model = IssueBlocker
fields = [
"issue_detail",
"relation_type",
"related_issue",
"issue",
"id"
]
read_only_fields = [
"workspace",
"project",
"blocked_issue_detail",
"blocked_by",
"block",
]
read_only_fields = fields
class RelatedIssueSerializer(BaseSerializer):
issue_detail = IssueProjectLiteSerializer(read_only=True, source="issue")
class BlockerIssueSerializer(BaseSerializer):
blocker_issue_detail = IssueProjectLiteSerializer(
source="blocked_by", read_only=True
)
class Meta:
model = IssueRelation
model = IssueBlocker
fields = [
"issue_detail",
"relation_type",
"related_issue",
"issue",
"id"
]
read_only_fields = [
"workspace",
"project",
"blocker_issue_detail",
"blocked_by",
"block",
]
read_only_fields = fields
class IssueAssigneeSerializer(BaseSerializer):
@@ -438,9 +510,6 @@ class IssueAttachmentSerializer(BaseSerializer):
class IssueReactionSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
class Meta:
model = IssueReaction
fields = "__all__"
@@ -452,6 +521,19 @@ class IssueReactionSerializer(BaseSerializer):
]
class IssueReactionLiteSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
class Meta:
model = IssueReaction
fields = [
"id",
"reaction",
"issue",
"actor_detail",
]
class CommentReactionLiteSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
@@ -472,13 +554,12 @@ class CommentReactionSerializer(BaseSerializer):
read_only_fields = ["workspace", "project", "comment", "actor"]
class IssueVoteSerializer(BaseSerializer):
actor_detail = UserLiteSerializer(read_only=True, source="actor")
class IssueVoteSerializer(BaseSerializer):
class Meta:
model = IssueVote
fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"]
fields = ["issue", "vote", "workspace_id", "project_id", "actor"]
read_only_fields = fields
@@ -488,7 +569,7 @@ class IssueCommentSerializer(BaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project")
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
comment_reactions = CommentReactionLiteSerializer(read_only=True, many=True)
is_member = serializers.BooleanField(read_only=True)
class Meta:
model = IssueComment
@@ -501,6 +582,7 @@ class IssueCommentSerializer(BaseSerializer):
"updated_by",
"created_at",
"updated_at",
"access",
]
@@ -541,14 +623,16 @@ class IssueSerializer(BaseSerializer):
parent_detail = IssueStateFlatSerializer(read_only=True, source="parent")
label_details = LabelSerializer(read_only=True, source="labels", many=True)
assignee_details = UserLiteSerializer(read_only=True, source="assignees", many=True)
related_issues = IssueRelationSerializer(read_only=True, source="issue_relation", many=True)
issue_relations = RelatedIssueSerializer(read_only=True, source="issue_related", many=True)
# List of issues blocked by this issue
blocked_issues = BlockedIssueSerializer(read_only=True, many=True)
# List of issues that block this issue
blocker_issues = BlockerIssueSerializer(read_only=True, many=True)
issue_cycle = IssueCycleDetailSerializer(read_only=True)
issue_module = IssueModuleDetailSerializer(read_only=True)
issue_link = IssueLinkSerializer(read_only=True, many=True)
issue_attachment = IssueAttachmentSerializer(read_only=True, many=True)
sub_issues_count = serializers.IntegerField(read_only=True)
issue_reactions = IssueReactionSerializer(read_only=True, many=True)
issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True)
class Meta:
model = Issue
@@ -574,7 +658,7 @@ class IssueLiteSerializer(BaseSerializer):
module_id = serializers.UUIDField(read_only=True)
attachment_count = serializers.IntegerField(read_only=True)
link_count = serializers.IntegerField(read_only=True)
issue_reactions = IssueReactionSerializer(read_only=True, many=True)
issue_reactions = IssueReactionLiteSerializer(read_only=True, many=True)
class Meta:
model = Issue
@@ -592,33 +676,6 @@ class IssueLiteSerializer(BaseSerializer):
]
class IssuePublicSerializer(BaseSerializer):
project_detail = ProjectLiteSerializer(read_only=True, source="project")
state_detail = StateLiteSerializer(read_only=True, source="state")
reactions = IssueReactionSerializer(read_only=True, many=True, source="issue_reactions")
votes = IssueVoteSerializer(read_only=True, many=True)
class Meta:
model = Issue
fields = [
"id",
"name",
"description_html",
"sequence_id",
"state",
"state_detail",
"project",
"project_detail",
"workspace",
"priority",
"target_date",
"reactions",
"votes",
]
read_only_fields = fields
class IssueSubscriberSerializer(BaseSerializer):
class Meta:
model = IssueSubscriber
+1 -14
View File
@@ -15,7 +15,6 @@ from plane.db.models import (
ProjectIdentifier,
ProjectFavorite,
ProjectDeployBoard,
ProjectPublicMember,
)
@@ -178,17 +177,5 @@ class ProjectDeployBoardSerializer(BaseSerializer):
fields = "__all__"
read_only_fields = [
"workspace",
"project", "anchor",
]
class ProjectPublicMemberSerializer(BaseSerializer):
class Meta:
model = ProjectPublicMember
fields = "__all__"
read_only_fields = [
"workspace",
"project",
"member",
"project" "anchor",
]
+1 -30
View File
@@ -5,39 +5,10 @@ from rest_framework import serializers
from .base import BaseSerializer
from .workspace import WorkspaceLiteSerializer
from .project import ProjectLiteSerializer
from plane.db.models import GlobalView, IssueView, IssueViewFavorite
from plane.db.models import IssueView, IssueViewFavorite
from plane.utils.issue_filters import issue_filters
class GlobalViewSerializer(BaseSerializer):
workspace_detail = WorkspaceLiteSerializer(source="workspace", read_only=True)
class Meta:
model = GlobalView
fields = "__all__"
read_only_fields = [
"workspace",
"query",
]
def create(self, validated_data):
query_params = validated_data.get("query_data", {})
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
return GlobalView.objects.create(**validated_data)
def update(self, instance, validated_data):
query_params = validated_data.get("query_data", {})
if bool(query_params):
validated_data["query"] = issue_filters(query_params, "POST")
else:
validated_data["query"] = dict()
validated_data["query"] = issue_filters(query_params, "PATCH")
return super().update(instance, validated_data)
class IssueViewSerializer(BaseSerializer):
is_favorite = serializers.BooleanField(read_only=True)
project_detail = ProjectLiteSerializer(source="project", read_only=True)
+9 -113
View File
@@ -51,7 +51,6 @@ from plane.api.views import (
WorkspaceUserProfileEndpoint,
WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint,
LeaveWorkspaceEndpoint,
## End Workspaces
# File Assets
FileAssetEndpoint,
@@ -69,7 +68,6 @@ from plane.api.views import (
UserProjectInvitationsViewset,
ProjectIdentifierEndpoint,
ProjectFavoritesViewSet,
LeaveProjectEndpoint,
## End Projects
# Issues
IssueViewSet,
@@ -90,9 +88,7 @@ from plane.api.views import (
IssueSubscriberViewSet,
IssueCommentPublicViewSet,
IssueReactionViewSet,
IssueRelationViewSet,
CommentReactionViewSet,
IssueDraftViewSet,
## End Issues
# States
StateViewSet,
@@ -102,8 +98,6 @@ from plane.api.views import (
BulkEstimatePointEndpoint,
## End Estimates
# Views
GlobalViewViewSet,
GlobalViewIssuesViewSet,
IssueViewViewSet,
ViewIssuesEndpoint,
IssueViewFavoriteViewSet,
@@ -170,22 +164,21 @@ from plane.api.views import (
# Notification
NotificationViewSet,
UnreadNotificationEndpoint,
MarkAllReadNotificationViewSet,
## End Notification
# Public Boards
ProjectDeployBoardViewSet,
ProjectIssuesPublicEndpoint,
ProjectDeployBoardIssuesPublicEndpoint,
ProjectDeployBoardPublicSettingsEndpoint,
IssueReactionPublicViewSet,
CommentReactionPublicViewSet,
InboxIssuePublicViewSet,
IssueVotePublicViewSet,
WorkspaceProjectDeployBoardEndpoint,
IssueRetrievePublicEndpoint,
## End Public Boards
## Exporter
ExportIssuesEndpoint,
## End Exporter
)
@@ -242,11 +235,7 @@ urlpatterns = [
UpdateUserTourCompletedEndpoint.as_view(),
name="user-tour",
),
path(
"users/workspaces/<str:slug>/activities/",
UserActivityEndpoint.as_view(),
name="user-activities",
),
path("users/activities/", UserActivityEndpoint.as_view(), name="user-activities"),
# user workspaces
path(
"users/me/workspaces/",
@@ -450,11 +439,6 @@ urlpatterns = [
WorkspaceLabelsEndpoint.as_view(),
name="workspace-labels",
),
path(
"workspaces/<str:slug>/members/leave/",
LeaveWorkspaceEndpoint.as_view(),
name="workspace-labels",
),
## End Workspaces ##
# Projects
path(
@@ -568,11 +552,6 @@ urlpatterns = [
),
name="project",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
LeaveProjectEndpoint.as_view(),
name="project",
),
# End Projects
# States
path(
@@ -654,37 +633,6 @@ urlpatterns = [
ViewIssuesEndpoint.as_view(),
name="project-view-issues",
),
path(
"workspaces/<str:slug>/views/",
GlobalViewViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="global-view",
),
path(
"workspaces/<str:slug>/views/<uuid:pk>/",
GlobalViewViewSet.as_view(
{
"get": "retrieve",
"put": "update",
"patch": "partial_update",
"delete": "destroy",
}
),
name="global-view",
),
path(
"workspaces/<str:slug>/issues/",
GlobalViewIssuesViewSet.as_view(
{
"get": "list",
}
),
name="global-view-issues",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/user-favorite-views/",
IssueViewFavoriteViewSet.as_view(
@@ -803,6 +751,11 @@ urlpatterns = [
),
name="project-issue",
),
path(
"workspaces/<str:slug>/issues/",
WorkSpaceIssuesEndpoint.as_view(),
name="workspace-issue",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-labels/",
LabelViewSet.as_view(
@@ -1043,49 +996,6 @@ urlpatterns = [
name="project-issue-archive",
),
## End Issue Archives
## Issue Relation
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/",
IssueRelationViewSet.as_view(
{
"post": "create",
}
),
name="issue-relation",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issues/<uuid:issue_id>/issue-relation/<uuid:pk>/",
IssueRelationViewSet.as_view(
{
"delete": "destroy",
}
),
name="issue-relation",
),
## End Issue Relation
## Issue Drafts
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/",
IssueDraftViewSet.as_view(
{
"get": "list",
"post": "create",
}
),
name="project-issue-draft",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/issue-drafts/<uuid:pk>/",
IssueDraftViewSet.as_view(
{
"get": "retrieve",
"patch": "partial_update",
"delete": "destroy",
}
),
name="project-issue-draft",
),
## End Issue Drafts
## File Assets
path(
"workspaces/<str:slug>/file-assets/",
@@ -1584,15 +1494,6 @@ urlpatterns = [
UnreadNotificationEndpoint.as_view(),
name="unread-notifications",
),
path(
"workspaces/<str:slug>/users/notifications/mark-all-read/",
MarkAllReadNotificationViewSet.as_view(
{
"post": "create",
}
),
name="mark-all-read-notifications",
),
## End Notification
# Public Boards
path(
@@ -1623,14 +1524,9 @@ urlpatterns = [
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/",
ProjectIssuesPublicEndpoint.as_view(),
ProjectDeployBoardIssuesPublicEndpoint.as_view(),
name="project-deploy-board",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/",
IssueRetrievePublicEndpoint.as_view(),
name="workspace-project-boards",
),
path(
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/",
IssueCommentPublicViewSet.as_view(
+6 -9
View File
@@ -12,11 +12,11 @@ from .project import (
ProjectUserViewsEndpoint,
ProjectMemberUserEndpoint,
ProjectFavoritesViewSet,
ProjectDeployBoardIssuesPublicEndpoint,
ProjectDeployBoardViewSet,
ProjectDeployBoardPublicSettingsEndpoint,
ProjectMemberEndpoint,
WorkspaceProjectDeployBoardEndpoint,
LeaveProjectEndpoint,
)
from .user import (
UserEndpoint,
@@ -53,10 +53,9 @@ from .workspace import (
WorkspaceUserProfileIssuesEndpoint,
WorkspaceLabelsEndpoint,
WorkspaceMembersEndpoint,
LeaveWorkspaceEndpoint,
)
from .state import StateViewSet
from .view import GlobalViewViewSet, GlobalViewIssuesViewSet, IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
from .cycle import (
CycleViewSet,
CycleIssueViewSet,
@@ -86,10 +85,6 @@ from .issue import (
IssueReactionPublicViewSet,
CommentReactionPublicViewSet,
IssueVotePublicViewSet,
IssueRelationViewSet,
IssueRetrievePublicEndpoint,
ProjectIssuesPublicEndpoint,
IssueDraftViewSet,
)
from .auth_extended import (
@@ -167,6 +162,8 @@ from .analytic import (
DefaultAnalyticsEndpoint,
)
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
from .notification import NotificationViewSet, UnreadNotificationEndpoint
from .exporter import ExportIssuesEndpoint
from .exporter import (
ExportIssuesEndpoint,
)
+8 -24
View File
@@ -18,21 +18,10 @@ class FileAssetEndpoint(BaseAPIView):
"""
def get(self, request, workspace_id, asset_key):
try:
asset_key = str(workspace_id) + "/" + asset_key
files = FileAsset.objects.filter(asset=asset_key)
if files.exists():
serializer = FileAssetSerializer(files, context={"request": request}, many=True)
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
else:
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
asset_key = str(workspace_id) + "/" + asset_key
files = FileAsset.objects.filter(asset=asset_key)
serializer = FileAssetSerializer(files, context={"request": request}, many=True)
return Response(serializer.data)
def post(self, request, slug):
try:
@@ -79,16 +68,11 @@ class UserAssetsEndpoint(BaseAPIView):
def get(self, request, asset_key):
try:
files = FileAsset.objects.filter(asset=asset_key, created_by=request.user)
if files.exists():
serializer = FileAssetSerializer(files, context={"request": request})
return Response({"data": serializer.data, "status": True}, status=status.HTTP_200_OK)
else:
return Response({"error": "Asset key does not exist", "status": False}, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
serializer = FileAssetSerializer(files, context={"request": request})
return Response(serializer.data)
except FileAsset.DoesNotExist:
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
{"error": "File Asset does not exist"}, status=status.HTTP_404_NOT_FOUND
)
def post(self, request):
+5 -22
View File
@@ -1,41 +1,24 @@
# Python imports
import zoneinfo
# Django imports
from django.urls import resolve
from django.conf import settings
from django.utils import timezone
# Third part imports
# Third part imports
from rest_framework import status
from rest_framework.viewsets import ModelViewSet
from rest_framework.exceptions import APIException
from rest_framework.views import APIView
from rest_framework.filters import SearchFilter
from rest_framework.permissions import IsAuthenticated
from rest_framework.exceptions import NotFound
from sentry_sdk import capture_exception
from django_filters.rest_framework import DjangoFilterBackend
# Module imports
from plane.db.models import Workspace, Project
from plane.utils.paginator import BasePaginator
class TimezoneMixin:
"""
This enables timezone conversion according
to the user set timezone
"""
def initial(self, request, *args, **kwargs):
super().initial(request, *args, **kwargs)
if request.user.is_authenticated:
timezone.activate(zoneinfo.ZoneInfo(request.user.user_timezone))
else:
timezone.deactivate()
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
class BaseViewSet(ModelViewSet, BasePaginator):
model = None
@@ -84,7 +67,7 @@ class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
return self.kwargs.get("pk", None)
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
class BaseAPIView(APIView, BasePaginator):
permission_classes = [
IsAuthenticated,
+13 -31
View File
@@ -80,7 +80,6 @@ class CycleViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
return super().perform_destroy(instance)
@@ -192,10 +191,11 @@ class CycleViewSet(BaseViewSet):
workspace__slug=slug,
project_id=project_id,
)
.annotate(display_name=F("assignees__display_name"))
.annotate(first_name=F("assignees__first_name"))
.annotate(last_name=F("assignees__last_name"))
.annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar"))
.values("display_name", "assignee_id", "avatar")
.values("first_name", "last_name", "assignee_id", "avatar")
.annotate(total_issues=Count("assignee_id"))
.annotate(
completed_issues=Count(
@@ -209,7 +209,7 @@ class CycleViewSet(BaseViewSet):
filter=Q(completed_at__isnull=True),
)
)
.order_by("display_name")
.order_by("first_name", "last_name")
)
label_distribution = (
@@ -334,21 +334,13 @@ class CycleViewSet(BaseViewSet):
workspace__slug=slug, project_id=project_id, pk=pk
)
request_data = request.data
if cycle.end_date is not None and cycle.end_date < timezone.now().date():
if "sort_order" in request_data:
# Can only change sort order
request_data = {
"sort_order": request_data.get("sort_order", cycle.sort_order)
}
else:
return Response(
{
"error": "The Cycle has already been completed so it cannot be edited"
},
status=status.HTTP_400_BAD_REQUEST,
)
return Response(
{
"error": "The Cycle has already been completed so it cannot be edited"
},
status=status.HTTP_400_BAD_REQUEST,
)
serializer = CycleWriteSerializer(cycle, data=request.data, partial=True)
if serializer.is_valid():
@@ -382,9 +374,7 @@ class CycleViewSet(BaseViewSet):
.annotate(assignee_id=F("assignees__id"))
.annotate(avatar=F("assignees__avatar"))
.annotate(display_name=F("assignees__display_name"))
.values(
"first_name", "last_name", "assignee_id", "avatar", "display_name"
)
.values("first_name", "last_name", "assignee_id", "avatar", "display_name")
.annotate(total_issues=Count("assignee_id"))
.annotate(
completed_issues=Count(
@@ -488,7 +478,6 @@ class CycleIssueViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
return super().perform_destroy(instance)
@@ -519,7 +508,6 @@ class CycleIssueViewSet(BaseViewSet):
try:
order_by = request.GET.get("order_by", "created_at")
group_by = request.GET.get("group_by", False)
sub_group_by = request.GET.get("sub_group_by", False)
filters = issue_filters(request.query_params, "GET")
issues = (
Issue.issue_objects.filter(issue_cycle__cycle_id=cycle_id)
@@ -558,15 +546,9 @@ class CycleIssueViewSet(BaseViewSet):
issues_data = IssueStateSerializer(issues, many=True).data
if sub_group_by and sub_group_by == group_by:
return Response(
{"error": "Group by and sub group by cannot be same"},
status=status.HTTP_400_BAD_REQUEST,
)
if group_by:
return Response(
group_results(issues_data, group_by, sub_group_by),
group_results(issues_data, group_by),
status=status.HTTP_200_OK,
)
@@ -664,7 +646,6 @@ class CycleIssueViewSet(BaseViewSet):
),
}
),
epoch = int(timezone.now().timestamp())
)
# Return all Cycle Issues
@@ -729,6 +710,7 @@ class CycleDateCheckEndpoint(BaseAPIView):
class CycleFavoriteViewSet(BaseViewSet):
serializer_class = CycleFavoriteSerializer
model = CycleFavorite
+3 -3
View File
@@ -41,9 +41,9 @@ class GPTIntegrationEndpoint(BaseAPIView):
final_text = task + "\n" + prompt
openai.api_key = settings.OPENAI_API_KEY
response = openai.ChatCompletion.create(
response = openai.Completion.create(
model=settings.GPT_ENGINE,
messages=[{"role": "user", "content": final_text}],
prompt=final_text,
temperature=0.7,
max_tokens=1024,
)
@@ -51,7 +51,7 @@ class GPTIntegrationEndpoint(BaseAPIView):
workspace = Workspace.objects.get(slug=slug)
project = Project.objects.get(pk=project_id)
text = response.choices[0].message.content.strip()
text = response.choices[0].text.strip()
text_html = text.replace("\n", "<br/>")
return Response(
{
-4
View File
@@ -213,7 +213,6 @@ class InboxIssueViewSet(BaseViewSet):
issue_id=str(issue.id),
project_id=str(project_id),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
# create an inbox issue
InboxIssue.objects.create(
@@ -278,7 +277,6 @@ class InboxIssueViewSet(BaseViewSet):
IssueSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
epoch = int(timezone.now().timestamp())
)
issue_serializer.save()
else:
@@ -520,7 +518,6 @@ class InboxIssuePublicViewSet(BaseViewSet):
issue_id=str(issue.id),
project_id=str(project_id),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
# create an inbox issue
InboxIssue.objects.create(
@@ -585,7 +582,6 @@ class InboxIssuePublicViewSet(BaseViewSet):
IssueSerializer(current_instance).data,
cls=DjangoJSONEncoder,
),
epoch = int(timezone.now().timestamp())
)
issue_serializer.save()
return Response(issue_serializer.data, status=status.HTTP_200_OK)
File diff suppressed because it is too large Load Diff
+1 -12
View File
@@ -2,7 +2,6 @@
import json
# Django Imports
from django.utils import timezone
from django.db import IntegrityError
from django.db.models import Prefetch, F, OuterRef, Func, Exists, Count, Q
from django.core import serializers
@@ -130,7 +129,6 @@ class ModuleViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
return super().perform_destroy(instance)
@@ -279,7 +277,6 @@ class ModuleIssueViewSet(BaseViewSet):
issue_id=str(self.kwargs.get("pk", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch = int(timezone.now().timestamp())
)
return super().perform_destroy(instance)
@@ -311,7 +308,6 @@ class ModuleIssueViewSet(BaseViewSet):
try:
order_by = request.GET.get("order_by", "created_at")
group_by = request.GET.get("group_by", False)
sub_group_by = request.GET.get("sub_group_by", False)
filters = issue_filters(request.query_params, "GET")
issues = (
Issue.issue_objects.filter(issue_module__module_id=module_id)
@@ -350,15 +346,9 @@ class ModuleIssueViewSet(BaseViewSet):
issues_data = IssueStateSerializer(issues, many=True).data
if sub_group_by and sub_group_by == group_by:
return Response(
{"error": "Group by and sub group by cannot be same"},
status=status.HTTP_400_BAD_REQUEST,
)
if group_by:
return Response(
group_results(issues_data, group_by, sub_group_by),
group_results(issues_data, group_by),
status=status.HTTP_200_OK,
)
@@ -447,7 +437,6 @@ class ModuleIssueViewSet(BaseViewSet):
),
}
),
epoch = int(timezone.now().timestamp())
)
return Response(
+3 -90
View File
@@ -10,13 +10,7 @@ from plane.utils.paginator import BasePaginator
# Module imports
from .base import BaseViewSet, BaseAPIView
from plane.db.models import (
Notification,
IssueAssignee,
IssueSubscriber,
Issue,
WorkspaceMember,
)
from plane.db.models import Notification, IssueAssignee, IssueSubscriber, Issue, WorkspaceMember
from plane.api.serializers import NotificationSerializer
@@ -89,17 +83,13 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
# Created issues
if type == "created":
if WorkspaceMember.objects.filter(
workspace__slug=slug, member=request.user, role__lt=15
).exists():
if WorkspaceMember.objects.filter(workspace__slug=slug, member=request.user, role__lt=15).exists():
notifications = Notification.objects.none()
else:
issue_ids = Issue.objects.filter(
workspace__slug=slug, created_by=request.user
).values_list("pk", flat=True)
notifications = notifications.filter(
entity_identifier__in=issue_ids
)
notifications = notifications.filter(entity_identifier__in=issue_ids)
# Pagination
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
@@ -284,80 +274,3 @@ class UnreadNotificationEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class MarkAllReadNotificationViewSet(BaseViewSet):
def create(self, request, slug):
try:
snoozed = request.data.get("snoozed", False)
archived = request.data.get("archived", False)
type = request.data.get("type", "all")
notifications = (
Notification.objects.filter(
workspace__slug=slug,
receiver_id=request.user.id,
read_at__isnull=True,
)
.select_related("workspace", "project", "triggered_by", "receiver")
.order_by("snoozed_till", "-created_at")
)
# Filter for snoozed notifications
if snoozed:
notifications = notifications.filter(
Q(snoozed_till__lt=timezone.now()) | Q(snoozed_till__isnull=False)
)
else:
notifications = notifications.filter(
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
)
# Filter for archived or unarchive
if archived:
notifications = notifications.filter(archived_at__isnull=False)
else:
notifications = notifications.filter(archived_at__isnull=True)
# Subscribed issues
if type == "watching":
issue_ids = IssueSubscriber.objects.filter(
workspace__slug=slug, subscriber_id=request.user.id
).values_list("issue_id", flat=True)
notifications = notifications.filter(entity_identifier__in=issue_ids)
# Assigned Issues
if type == "assigned":
issue_ids = IssueAssignee.objects.filter(
workspace__slug=slug, assignee_id=request.user.id
).values_list("issue_id", flat=True)
notifications = notifications.filter(entity_identifier__in=issue_ids)
# Created issues
if type == "created":
if WorkspaceMember.objects.filter(
workspace__slug=slug, member=request.user, role__lt=15
).exists():
notifications = Notification.objects.none()
else:
issue_ids = Issue.objects.filter(
workspace__slug=slug, created_by=request.user
).values_list("pk", flat=True)
notifications = notifications.filter(
entity_identifier__in=issue_ids
)
updated_notifications = []
for notification in notifications:
notification.read_at = timezone.now()
updated_notifications.append(notification)
Notification.objects.bulk_update(
updated_notifications, ["read_at"], batch_size=100
)
return Response({"message": "Successful"}, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+165 -50
View File
@@ -11,8 +11,14 @@ from django.db.models import (
OuterRef,
Func,
F,
Max,
CharField,
Func,
Subquery,
Prefetch,
When,
Case,
Value,
)
from django.core.validators import validate_email
from django.conf import settings
@@ -41,7 +47,6 @@ from plane.api.permissions import (
ProjectBasePermission,
ProjectEntityPermission,
ProjectMemberPermission,
ProjectLitePermission,
)
from plane.db.models import (
@@ -66,9 +71,16 @@ from plane.db.models import (
ModuleMember,
Inbox,
ProjectDeployBoard,
Issue,
IssueReaction,
IssueLink,
IssueAttachment,
Label,
)
from plane.bgtasks.project_invitation_task import project_invitation
from plane.utils.grouper import group_results
from plane.utils.issue_filters import issue_filters
class ProjectViewSet(BaseViewSet):
@@ -482,7 +494,7 @@ class UserProjectInvitationsViewset(BaseViewSet):
# Delete joined project invites
project_invitations.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
@@ -617,7 +629,7 @@ class ProjectMemberViewSet(BaseViewSet):
return Response(status=status.HTTP_204_NO_CONTENT)
except ProjectMember.DoesNotExist:
return Response(
{"error": "Project Member does not exist"}, status=status.HTTP_400_BAD_REQUEST
{"error": "Project Member does not exist"}, status=status.HTTP_400
)
except Exception as e:
capture_exception(e)
@@ -924,7 +936,8 @@ class ProjectUserViewsEndpoint(BaseAPIView):
project_member.save()
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(status=status.HTTP_200_OK)
except Project.DoesNotExist:
return Response(
{"error": "The requested resource does not exists"},
@@ -1130,11 +1143,158 @@ class ProjectDeployBoardPublicSettingsEndpoint(BaseAPIView):
)
class WorkspaceProjectDeployBoardEndpoint(BaseAPIView):
class ProjectDeployBoardIssuesPublicEndpoint(BaseAPIView):
permission_classes = [
AllowAny,
]
def get(self, request, slug, project_id):
try:
project_deploy_board = ProjectDeployBoard.objects.get(
workspace__slug=slug, project_id=project_id
)
filters = issue_filters(request.query_params, "GET")
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", None]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = (
Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.filter(project_id=project_id)
.filter(workspace__slug=slug)
.select_related("project", "workspace", "state", "parent")
.prefetch_related("assignees", "labels")
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("actor"),
)
)
.filter(**filters)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
# Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority":
priority_order = (
priority_order
if order_by_param == "priority"
else priority_order[::-1]
)
issue_queryset = issue_queryset.annotate(
priority_order=Case(
*[
When(priority=p, then=Value(i))
for i, p in enumerate(priority_order)
],
output_field=CharField(),
)
).order_by("priority_order")
# State Ordering
elif order_by_param in [
"state__name",
"state__group",
"-state__name",
"-state__group",
]:
state_order = (
state_order
if order_by_param in ["state__name", "state__group"]
else state_order[::-1]
)
issue_queryset = issue_queryset.annotate(
state_order=Case(
*[
When(state__group=state_group, then=Value(i))
for i, state_group in enumerate(state_order)
],
default=Value(len(state_order)),
output_field=CharField(),
)
).order_by("state_order")
# assignee and label ordering
elif order_by_param in [
"labels__name",
"-labels__name",
"assignees__first_name",
"-assignees__first_name",
]:
issue_queryset = issue_queryset.annotate(
max_values=Max(
order_by_param[1::]
if order_by_param.startswith("-")
else order_by_param
)
).order_by(
"-max_values" if order_by_param.startswith("-") else "max_values"
)
else:
issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueLiteSerializer(issue_queryset, many=True).data
states = State.objects.filter(
workspace__slug=slug, project_id=project_id
).values("name", "group", "color", "id")
labels = Label.objects.filter(
workspace__slug=slug, project_id=project_id
).values("id", "name", "color", "parent")
## Grouping the results
group_by = request.GET.get("group_by", False)
if group_by:
issues = group_results(issues, group_by)
return Response(
{
"issues": issues,
"states": states,
"labels": labels,
},
status=status.HTTP_200_OK,
)
except ProjectDeployBoard.DoesNotExist:
return Response(
{"error": "Board does not exists"}, status=status.HTTP_404_NOT_FOUND
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class WorkspaceProjectDeployBoardEndpoint(BaseAPIView):
permission_classes = [AllowAny,]
def get(self, request, slug):
try:
projects = (
@@ -1164,48 +1324,3 @@ class WorkspaceProjectDeployBoardEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class LeaveProjectEndpoint(BaseAPIView):
permission_classes = [
ProjectLitePermission,
]
def delete(self, request, slug, project_id):
try:
project_member = ProjectMember.objects.get(
workspace__slug=slug,
member=request.user,
project_id=project_id,
)
# Only Admin case
if (
project_member.role == 20
and ProjectMember.objects.filter(
workspace__slug=slug,
role=20,
project_id=project_id,
).count()
== 1
):
return Response(
{
"error": "You cannot leave the project since you are the only admin of the project you should delete the project"
},
status=status.HTTP_400_BAD_REQUEST,
)
# Delete the member from workspace
project_member.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except ProjectMember.DoesNotExist:
return Response(
{"error": "Workspace member does not exists"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+4 -4
View File
@@ -220,7 +220,7 @@ class IssueSearchEndpoint(BaseAPIView):
query = request.query_params.get("search", False)
workspace_search = request.query_params.get("workspace_search", "false")
parent = request.query_params.get("parent", "false")
issue_relation = request.query_params.get("issue_relation", "false")
blocker_blocked_by = request.query_params.get("blocker_blocked_by", "false")
cycle = request.query_params.get("cycle", "false")
module = request.query_params.get("module", "false")
sub_issue = request.query_params.get("sub_issue", "false")
@@ -247,12 +247,12 @@ class IssueSearchEndpoint(BaseAPIView):
"parent_id", flat=True
)
)
if issue_relation == "true" and issue_id:
if blocker_blocked_by == "true" and issue_id:
issue = Issue.issue_objects.get(pk=issue_id)
issues = issues.filter(
~Q(pk=issue_id),
~Q(issue_related__issue=issue),
~Q(issue_relation__related_issue=issue),
~Q(blocked_issues__block=issue),
~Q(blocker_issues__blocked_by=issue),
)
if sub_issue == "true" and issue_id:
issue = Issue.issue_objects.get(pk=issue_id)
+4 -4
View File
@@ -137,11 +137,11 @@ class UpdateUserTourCompletedEndpoint(BaseAPIView):
class UserActivityEndpoint(BaseAPIView, BasePaginator):
def get(self, request, slug):
def get(self, request):
try:
queryset = IssueActivity.objects.filter(
actor=request.user, workspace__slug=slug
).select_related("actor", "workspace", "issue", "project")
queryset = IssueActivity.objects.filter(actor=request.user).select_related(
"actor", "workspace", "issue", "project"
)
return self.paginate(
request=request,
+1 -189
View File
@@ -1,18 +1,4 @@
# Django imports
from django.db.models import (
Prefetch,
OuterRef,
Func,
F,
Case,
Value,
CharField,
When,
Exists,
Max,
)
from django.utils.decorators import method_decorator
from django.views.decorators.gzip import gzip_page
from django.db import IntegrityError
from django.db.models import Prefetch, OuterRef, Exists
@@ -24,192 +10,18 @@ from sentry_sdk import capture_exception
# Module imports
from . import BaseViewSet, BaseAPIView
from plane.api.serializers import (
GlobalViewSerializer,
IssueViewSerializer,
IssueLiteSerializer,
IssueViewFavoriteSerializer,
)
from plane.api.permissions import WorkspaceEntityPermission, ProjectEntityPermission
from plane.api.permissions import ProjectEntityPermission
from plane.db.models import (
Workspace,
GlobalView,
IssueView,
Issue,
IssueViewFavorite,
IssueReaction,
IssueLink,
IssueAttachment,
)
from plane.utils.issue_filters import issue_filters
from plane.utils.grouper import group_results
class GlobalViewViewSet(BaseViewSet):
serializer_class = GlobalViewSerializer
model = GlobalView
permission_classes = [
WorkspaceEntityPermission,
]
def perform_create(self, serializer):
workspace = Workspace.objects.get(slug=self.kwargs.get("slug"))
serializer.save(workspace_id=workspace.id)
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("workspace")
.order_by("-created_at")
.distinct()
)
class GlobalViewIssuesViewSet(BaseViewSet):
permission_classes = [
WorkspaceEntityPermission,
]
def get_queryset(self):
return (
Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.filter(workspace__slug=self.kwargs.get("slug"))
.select_related("project")
.select_related("workspace")
.select_related("state")
.select_related("parent")
.prefetch_related("assignees")
.prefetch_related("labels")
.prefetch_related(
Prefetch(
"issue_reactions",
queryset=IssueReaction.objects.select_related("actor"),
)
)
)
@method_decorator(gzip_page)
def list(self, request, slug):
try:
filters = issue_filters(request.query_params, "GET")
# Custom ordering for priority and state
priority_order = ["urgent", "high", "medium", "low", "none"]
state_order = ["backlog", "unstarted", "started", "completed", "cancelled"]
order_by_param = request.GET.get("order_by", "-created_at")
issue_queryset = (
self.get_queryset()
.filter(**filters)
.filter(project__project_projectmember__member=self.request.user)
.annotate(cycle_id=F("issue_cycle__cycle_id"))
.annotate(module_id=F("issue_module__module_id"))
.annotate(
link_count=IssueLink.objects.filter(issue=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
.annotate(
attachment_count=IssueAttachment.objects.filter(
issue=OuterRef("id")
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
)
)
# Priority Ordering
if order_by_param == "priority" or order_by_param == "-priority":
priority_order = (
priority_order
if order_by_param == "priority"
else priority_order[::-1]
)
issue_queryset = issue_queryset.annotate(
priority_order=Case(
*[
When(priority=p, then=Value(i))
for i, p in enumerate(priority_order)
],
output_field=CharField(),
)
).order_by("priority_order")
# State Ordering
elif order_by_param in [
"state__name",
"state__group",
"-state__name",
"-state__group",
]:
state_order = (
state_order
if order_by_param in ["state__name", "state__group"]
else state_order[::-1]
)
issue_queryset = issue_queryset.annotate(
state_order=Case(
*[
When(state__group=state_group, then=Value(i))
for i, state_group in enumerate(state_order)
],
default=Value(len(state_order)),
output_field=CharField(),
)
).order_by("state_order")
# assignee and label ordering
elif order_by_param in [
"labels__name",
"-labels__name",
"assignees__first_name",
"-assignees__first_name",
]:
issue_queryset = issue_queryset.annotate(
max_values=Max(
order_by_param[1::]
if order_by_param.startswith("-")
else order_by_param
)
).order_by(
"-max_values" if order_by_param.startswith("-") else "max_values"
)
else:
issue_queryset = issue_queryset.order_by(order_by_param)
issues = IssueLiteSerializer(issue_queryset, many=True).data
## Grouping the results
group_by = request.GET.get("group_by", False)
sub_group_by = request.GET.get("sub_group_by", False)
if sub_group_by and sub_group_by == group_by:
return Response(
{"error": "Group by and sub group by cannot be same"},
status=status.HTTP_400_BAD_REQUEST,
)
if group_by:
return Response(
group_results(issues, group_by, sub_group_by), status=status.HTTP_200_OK
)
return Response(issues, status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class IssueViewViewSet(BaseViewSet):
+7 -48
View File
@@ -116,7 +116,7 @@ class WorkSpaceViewSet(BaseViewSet):
)
issue_count = (
Issue.issue_objects.filter(workspace=OuterRef("id"))
Issue.objects.filter(workspace=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -203,7 +203,7 @@ class UserWorkSpacesEndpoint(BaseAPIView):
)
issue_count = (
Issue.issue_objects.filter(workspace=OuterRef("id"))
Issue.objects.filter(workspace=OuterRef("id"))
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
@@ -532,7 +532,7 @@ class UserWorkspaceInvitationsEndpoint(BaseViewSet):
# Delete joined workspace invites
workspace_invitations.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(status=status.HTTP_200_OK)
except Exception as e:
capture_exception(e)
return Response(
@@ -846,7 +846,7 @@ class WorkspaceMemberUserViewsEndpoint(BaseAPIView):
workspace_member.view_props = request.data.get("view_props", {})
workspace_member.save()
return Response(status=status.HTTP_204_NO_CONTENT)
return Response(status=status.HTTP_200_OK)
except WorkspaceMember.DoesNotExist:
return Response(
{"error": "User not a member of workspace"},
@@ -1072,10 +1072,10 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
.order_by("state_group")
)
priority_order = ["urgent", "high", "medium", "low", "none"]
priority_order = ["urgent", "high", "medium", "low", None]
priority_distribution = (
Issue.issue_objects.filter(
Issue.objects.filter(
workspace__slug=slug,
assignees__in=[user_id],
project__project_projectmember__member=request.user,
@@ -1100,6 +1100,7 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
created_issues = (
Issue.issue_objects.filter(
workspace__slug=slug,
assignees__in=[user_id],
project__project_projectmember__member=request.user,
created_by_id=user_id,
)
@@ -1197,7 +1198,6 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
projects = request.query_params.getlist("project", [])
queryset = IssueActivity.objects.filter(
~Q(field__in=["comment", "vote", "reaction"]),
workspace__slug=slug,
project__project_projectmember__member=request.user,
actor=user_id,
@@ -1473,44 +1473,3 @@ class WorkspaceMembersEndpoint(BaseAPIView):
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
class LeaveWorkspaceEndpoint(BaseAPIView):
permission_classes = [
WorkspaceEntityPermission,
]
def delete(self, request, slug):
try:
workspace_member = WorkspaceMember.objects.get(
workspace__slug=slug, member=request.user
)
# Only Admin case
if (
workspace_member.role == 20
and WorkspaceMember.objects.filter(
workspace__slug=slug, role=20
).count()
== 1
):
return Response(
{
"error": "You cannot leave the workspace since you are the only admin of the workspace you should delete the workspace"
},
status=status.HTTP_400_BAD_REQUEST,
)
# Delete the member from workspace
workspace_member.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except WorkspaceMember.DoesNotExist:
return Response(
{"error": "Workspace member does not exists"},
status=status.HTTP_400_BAD_REQUEST,
)
except Exception as e:
capture_exception(e)
return Response(
{"error": "Something went wrong please try again later"},
status=status.HTTP_400_BAD_REQUEST,
)
+26 -55
View File
@@ -4,7 +4,6 @@ import io
import json
import boto3
import zipfile
from urllib.parse import urlparse, urlunparse
# Django imports
from django.conf import settings
@@ -24,11 +23,9 @@ def dateTimeConverter(time):
if time:
return time.strftime("%a, %d %b %Y %I:%M:%S %Z%z")
def dateConverter(time):
if time:
return time.strftime("%a, %d %b %Y")
return time.strftime("%a, %d %b %Y")
def create_csv_file(data):
csv_buffer = io.StringIO()
@@ -69,53 +66,28 @@ def create_zip_file(files):
def upload_to_s3(zip_file, workspace_id, token_id, slug):
s3 = boto3.client(
"s3",
region_name=settings.AWS_REGION,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"),
)
file_name = f"{workspace_id}/export-{slug}-{token_id[:6]}-{timezone.now()}.zip"
s3.upload_fileobj(
zip_file,
settings.AWS_S3_BUCKET_NAME,
file_name,
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
)
expires_in = 7 * 24 * 60 * 60
if settings.DOCKERIZED and settings.USE_MINIO:
s3 = boto3.client(
"s3",
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"),
)
s3.upload_fileobj(
zip_file,
settings.AWS_STORAGE_BUCKET_NAME,
file_name,
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
)
presigned_url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": settings.AWS_STORAGE_BUCKET_NAME, "Key": file_name},
ExpiresIn=expires_in,
)
# Create the new url with updated domain and protocol
presigned_url = presigned_url.replace(
"http://plane-minio:9000/uploads/",
f"{settings.AWS_S3_URL_PROTOCOL}//{settings.AWS_S3_CUSTOM_DOMAIN}/",
)
else:
s3 = boto3.client(
"s3",
region_name=settings.AWS_REGION,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"),
)
s3.upload_fileobj(
zip_file,
settings.AWS_S3_BUCKET_NAME,
file_name,
ExtraArgs={"ACL": "public-read", "ContentType": "application/zip"},
)
presigned_url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": settings.AWS_S3_BUCKET_NAME, "Key": file_name},
ExpiresIn=expires_in,
)
presigned_url = s3.generate_presigned_url(
"get_object",
Params={"Bucket": settings.AWS_S3_BUCKET_NAME, "Key": file_name},
ExpiresIn=expires_in,
)
exporter_instance = ExporterHistory.objects.get(token=token_id)
@@ -126,7 +98,7 @@ def upload_to_s3(zip_file, workspace_id, token_id, slug):
else:
exporter_instance.status = "failed"
exporter_instance.save(update_fields=["status", "url", "key"])
exporter_instance.save(update_fields=["status", "url","key"])
def generate_table_row(issue):
@@ -173,7 +145,7 @@ def generate_json_row(issue):
else "",
"Labels": issue["labels__name"],
"Cycle Name": issue["issue_cycle__cycle__name"],
"Cycle Start Date": dateConverter(issue["issue_cycle__cycle__start_date"]),
"Cycle Start Date": dateConverter(issue["issue_cycle__cycle__start_date"]),
"Cycle End Date": dateConverter(issue["issue_cycle__cycle__end_date"]),
"Module Name": issue["issue_module__module__name"],
"Module Start Date": dateConverter(issue["issue_module__module__start_date"]),
@@ -270,9 +242,7 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s
workspace_issues = (
(
Issue.objects.filter(
workspace__id=workspace_id,
project_id__in=project_ids,
project__project_projectmember__member=exporter_instance.initiated_by_id,
workspace__id=workspace_id, project_id__in=project_ids
)
.select_related("project", "workspace", "state", "parent", "created_by")
.prefetch_related(
@@ -305,7 +275,7 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s
"labels__name",
)
)
.order_by("project__identifier", "sequence_id")
.order_by("project__identifier","sequence_id")
.distinct()
)
# CSV header
@@ -368,6 +338,7 @@ def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, s
exporter_instance.status = "failed"
exporter_instance.reason = str(e)
exporter_instance.save(update_fields=["status", "reason"])
# Print logs if in DEBUG mode
if settings.DEBUG:
print(e)
@@ -21,29 +21,18 @@ def delete_old_s3_link():
expired_exporter_history = ExporterHistory.objects.filter(
Q(url__isnull=False) & Q(created_at__lte=timezone.now() - timedelta(days=8))
).values_list("key", "id")
if settings.DOCKERIZED and settings.USE_MINIO:
s3 = boto3.client(
"s3",
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"),
)
else:
s3 = boto3.client(
"s3",
region_name="ap-south-1",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"),
)
s3 = boto3.client(
"s3",
region_name="ap-south-1",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
config=Config(signature_version="s3v4"),
)
for file_name, exporter_id in expired_exporter_history:
# Delete object from S3
if file_name:
if settings.DOCKERIZED and settings.USE_MINIO:
s3.delete_object(Bucket=settings.AWS_STORAGE_BUCKET_NAME, Key=file_name)
else:
s3.delete_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key=file_name)
s3.delete_object(Bucket=settings.AWS_S3_BUCKET_NAME, Key=file_name)
ExporterHistory.objects.filter(id=exporter_id).update(url=None)
+155 -412
View File
@@ -24,9 +24,6 @@ from plane.db.models import (
IssueSubscriber,
Notification,
IssueAssignee,
IssueReaction,
CommentReaction,
IssueComment,
)
from plane.api.serializers import IssueActivitySerializer
@@ -39,7 +36,6 @@ def track_name(
project,
actor,
issue_activities,
epoch
):
if current_instance.get("name") != requested_data.get("name"):
issue_activities.append(
@@ -53,7 +49,6 @@ def track_name(
project=project,
workspace=project.workspace,
comment=f"updated the name to {requested_data.get('name')}",
epoch=epoch,
)
)
@@ -66,7 +61,6 @@ def track_parent(
project,
actor,
issue_activities,
epoch
):
if current_instance.get("parent") != requested_data.get("parent"):
if requested_data.get("parent") == None:
@@ -84,7 +78,6 @@ def track_parent(
comment=f"updated the parent issue to None",
old_identifier=old_parent.id,
new_identifier=None,
epoch=epoch,
)
)
else:
@@ -105,7 +98,6 @@ def track_parent(
comment=f"updated the parent issue to {new_parent.name}",
old_identifier=old_parent.id if old_parent is not None else None,
new_identifier=new_parent.id,
epoch=epoch,
)
)
@@ -118,7 +110,6 @@ def track_priority(
project,
actor,
issue_activities,
epoch
):
if current_instance.get("priority") != requested_data.get("priority"):
if requested_data.get("priority") == None:
@@ -133,7 +124,6 @@ def track_priority(
project=project,
workspace=project.workspace,
comment=f"updated the priority to None",
epoch=epoch,
)
)
else:
@@ -148,7 +138,6 @@ def track_priority(
project=project,
workspace=project.workspace,
comment=f"updated the priority to {requested_data.get('priority')}",
epoch=epoch,
)
)
@@ -161,7 +150,6 @@ def track_state(
project,
actor,
issue_activities,
epoch
):
if current_instance.get("state") != requested_data.get("state"):
new_state = State.objects.get(pk=requested_data.get("state", None))
@@ -180,7 +168,6 @@ def track_state(
comment=f"updated the state to {new_state.name}",
old_identifier=old_state.id,
new_identifier=new_state.id,
epoch=epoch,
)
)
@@ -193,7 +180,6 @@ def track_description(
project,
actor,
issue_activities,
epoch
):
if current_instance.get("description_html") != requested_data.get(
"description_html"
@@ -214,7 +200,6 @@ def track_description(
project=project,
workspace=project.workspace,
comment=f"updated the description to {requested_data.get('description_html')}",
epoch=epoch,
)
)
@@ -227,7 +212,6 @@ def track_target_date(
project,
actor,
issue_activities,
epoch
):
if current_instance.get("target_date") != requested_data.get("target_date"):
if requested_data.get("target_date") == None:
@@ -242,7 +226,6 @@ def track_target_date(
project=project,
workspace=project.workspace,
comment=f"updated the target date to None",
epoch=epoch,
)
)
else:
@@ -257,7 +240,6 @@ def track_target_date(
project=project,
workspace=project.workspace,
comment=f"updated the target date to {requested_data.get('target_date')}",
epoch=epoch,
)
)
@@ -270,7 +252,6 @@ def track_start_date(
project,
actor,
issue_activities,
epoch
):
if current_instance.get("start_date") != requested_data.get("start_date"):
if requested_data.get("start_date") == None:
@@ -285,7 +266,6 @@ def track_start_date(
project=project,
workspace=project.workspace,
comment=f"updated the start date to None",
epoch=epoch,
)
)
else:
@@ -300,7 +280,6 @@ def track_start_date(
project=project,
workspace=project.workspace,
comment=f"updated the start date to {requested_data.get('start_date')}",
epoch=epoch,
)
)
@@ -313,7 +292,6 @@ def track_labels(
project,
actor,
issue_activities,
epoch
):
# Label Addition
if len(requested_data.get("labels_list")) > len(current_instance.get("labels")):
@@ -333,7 +311,6 @@ def track_labels(
comment=f"added label {label.name}",
new_identifier=label.id,
old_identifier=None,
epoch=epoch,
)
)
@@ -355,7 +332,6 @@ def track_labels(
comment=f"removed label {label.name}",
old_identifier=label.id,
new_identifier=None,
epoch=epoch,
)
)
@@ -368,7 +344,6 @@ def track_assignees(
project,
actor,
issue_activities,
epoch
):
# Assignee Addition
if len(requested_data.get("assignees_list")) > len(
@@ -389,7 +364,6 @@ def track_assignees(
workspace=project.workspace,
comment=f"added assignee {assignee.display_name}",
new_identifier=assignee.id,
epoch=epoch,
)
)
@@ -412,29 +386,151 @@ def track_assignees(
workspace=project.workspace,
comment=f"removed assignee {assignee.display_name}",
old_identifier=assignee.id,
epoch=epoch,
)
)
# Track changes in blocking issues
def track_blocks(
requested_data,
current_instance,
issue_id,
project,
actor,
issue_activities,
):
if len(requested_data.get("blocks_list")) > len(
current_instance.get("blocked_issues")
):
for block in requested_data.get("blocks_list"):
if (
len(
[
blocked
for blocked in current_instance.get("blocked_issues")
if blocked.get("block") == block
]
)
== 0
):
issue = Issue.objects.get(pk=block)
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="updated",
old_value="",
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
field="blocks",
project=project,
workspace=project.workspace,
comment=f"added blocking issue {project.identifier}-{issue.sequence_id}",
new_identifier=issue.id,
)
)
# Blocked Issue Removal
if len(requested_data.get("blocks_list")) < len(
current_instance.get("blocked_issues")
):
for blocked in current_instance.get("blocked_issues"):
if blocked.get("block") not in requested_data.get("blocks_list"):
issue = Issue.objects.get(pk=blocked.get("block"))
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="updated",
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
new_value="",
field="blocks",
project=project,
workspace=project.workspace,
comment=f"removed blocking issue {project.identifier}-{issue.sequence_id}",
old_identifier=issue.id,
)
)
# Track changes in blocked_by issues
def track_blockings(
requested_data,
current_instance,
issue_id,
project,
actor,
issue_activities,
):
if len(requested_data.get("blockers_list")) > len(
current_instance.get("blocker_issues")
):
for block in requested_data.get("blockers_list"):
if (
len(
[
blocked
for blocked in current_instance.get("blocker_issues")
if blocked.get("blocked_by") == block
]
)
== 0
):
issue = Issue.objects.get(pk=block)
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="updated",
old_value="",
new_value=f"{issue.project.identifier}-{issue.sequence_id}",
field="blocking",
project=project,
workspace=project.workspace,
comment=f"added blocked by issue {project.identifier}-{issue.sequence_id}",
new_identifier=issue.id,
)
)
# Blocked Issue Removal
if len(requested_data.get("blockers_list")) < len(
current_instance.get("blocker_issues")
):
for blocked in current_instance.get("blocker_issues"):
if blocked.get("blocked_by") not in requested_data.get("blockers_list"):
issue = Issue.objects.get(pk=blocked.get("blocked_by"))
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="updated",
old_value=f"{issue.project.identifier}-{issue.sequence_id}",
new_value="",
field="blocking",
project=project,
workspace=project.workspace,
comment=f"removed blocked by issue {project.identifier}-{issue.sequence_id}",
old_identifier=issue.id,
)
)
def create_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"created the issue",
verb="created",
actor=actor,
epoch=epoch,
)
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"created the issue",
verb="created",
actor=actor,
)
)
def track_estimate_points(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
if current_instance.get("estimate_point") != requested_data.get("estimate_point"):
if requested_data.get("estimate_point") == None:
@@ -449,7 +545,6 @@ def track_estimate_points(
project=project,
workspace=project.workspace,
comment=f"updated the estimate point to None",
epoch=epoch,
)
)
else:
@@ -464,13 +559,12 @@ def track_estimate_points(
project=project,
workspace=project.workspace,
comment=f"updated the estimate point to {requested_data.get('estimate_point')}",
epoch=epoch,
)
)
def track_archive_at(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
if requested_data.get("archived_at") is None:
issue_activities.append(
@@ -484,7 +578,6 @@ def track_archive_at(
field="archived_at",
old_value="archive",
new_value="restore",
epoch=epoch,
)
)
else:
@@ -499,13 +592,12 @@ def track_archive_at(
field="archived_at",
old_value=None,
new_value="archive",
epoch=epoch,
)
)
def track_closed_to(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
if requested_data.get("closed_to") is not None:
updated_state = State.objects.get(
@@ -525,24 +617,25 @@ def track_closed_to(
comment=f"Plane updated the state to {updated_state.name}",
old_identifier=None,
new_identifier=updated_state.id,
epoch=epoch,
)
)
def update_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
ISSUE_ACTIVITY_MAPPER = {
"name": track_name,
"parent": track_parent,
"priority": track_priority,
"state": track_state,
"description_html": track_description,
"description": track_description,
"target_date": track_target_date,
"start_date": track_start_date,
"labels_list": track_labels,
"assignees_list": track_assignees,
"blocks_list": track_blocks,
"blockers_list": track_blockings,
"estimate_point": track_estimate_points,
"archived_at": track_archive_at,
"closed_to": track_closed_to,
@@ -563,12 +656,11 @@ def update_issue_activity(
project,
actor,
issue_activities,
epoch
)
def delete_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
@@ -578,13 +670,12 @@ def delete_issue_activity(
verb="deleted",
actor=actor,
field="issue",
epoch=epoch,
)
)
def create_comment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -603,13 +694,12 @@ def create_comment_activity(
new_value=requested_data.get("comment_html", ""),
new_identifier=requested_data.get("id", None),
issue_comment_id=requested_data.get("id", None),
epoch=epoch,
)
)
def update_comment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -631,13 +721,12 @@ def update_comment_activity(
new_value=requested_data.get("comment_html", ""),
new_identifier=current_instance.get("id", None),
issue_comment_id=current_instance.get("id", None),
epoch=epoch,
)
)
def delete_comment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
@@ -648,13 +737,12 @@ def delete_comment_activity(
verb="deleted",
actor=actor,
field="comment",
epoch=epoch,
)
)
def create_cycle_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -686,7 +774,6 @@ def create_cycle_issue_activity(
comment=f"updated cycle from {old_cycle.name} to {new_cycle.name}",
old_identifier=old_cycle.id,
new_identifier=new_cycle.id,
epoch=epoch,
)
)
@@ -707,13 +794,12 @@ def create_cycle_issue_activity(
workspace=project.workspace,
comment=f"added cycle {cycle.name}",
new_identifier=cycle.id,
epoch=epoch,
)
)
def delete_cycle_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -737,13 +823,12 @@ def delete_cycle_issue_activity(
workspace=project.workspace,
comment=f"removed this issue from {cycle.name if cycle is not None else None}",
old_identifier=cycle.id if cycle is not None else None,
epoch=epoch,
)
)
def create_module_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -775,7 +860,6 @@ def create_module_issue_activity(
comment=f"updated module from {old_module.name} to {new_module.name}",
old_identifier=old_module.id,
new_identifier=new_module.id,
epoch=epoch,
)
)
@@ -795,13 +879,12 @@ def create_module_issue_activity(
workspace=project.workspace,
comment=f"added module {module.name}",
new_identifier=module.id,
epoch=epoch,
)
)
def delete_module_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -825,13 +908,12 @@ def delete_module_issue_activity(
workspace=project.workspace,
comment=f"removed this issue from {module.name if module is not None else None}",
old_identifier=module.id if module is not None else None,
epoch=epoch,
)
)
def create_link_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -849,13 +931,12 @@ def create_link_activity(
field="link",
new_value=requested_data.get("url", ""),
new_identifier=requested_data.get("id", None),
epoch=epoch,
)
)
def update_link_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -876,13 +957,12 @@ def update_link_activity(
old_identifier=current_instance.get("id"),
new_value=requested_data.get("url", ""),
new_identifier=current_instance.get("id", None),
epoch=epoch,
)
)
def delete_link_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
current_instance = (
@@ -899,14 +979,13 @@ def delete_link_activity(
actor=actor,
field="link",
old_value=current_instance.get("url", ""),
new_value="",
epoch=epoch,
new_value=""
)
)
def create_attachment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
@@ -924,13 +1003,12 @@ def create_attachment_activity(
field="attachment",
new_value=current_instance.get("asset", ""),
new_identifier=current_instance.get("id", None),
epoch=epoch,
)
)
def delete_attachment_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
requested_data, current_instance, issue_id, project, actor, issue_activities
):
issue_activities.append(
IssueActivity(
@@ -941,319 +1019,9 @@ def delete_attachment_activity(
verb="deleted",
actor=actor,
field="attachment",
epoch=epoch,
)
)
def create_issue_reaction_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
):
requested_data = json.loads(requested_data) if requested_data is not None else None
if requested_data and requested_data.get("reaction") is not None:
issue_reaction = IssueReaction.objects.filter(reaction=requested_data.get("reaction"), project=project, actor=actor).values_list('id', flat=True).first()
if issue_reaction is not None:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="created",
old_value=None,
new_value=requested_data.get("reaction"),
field="reaction",
project=project,
workspace=project.workspace,
comment="added the reaction",
old_identifier=None,
new_identifier=issue_reaction,
epoch=epoch,
)
)
def delete_issue_reaction_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
):
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
if current_instance and current_instance.get("reaction") is not None:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="deleted",
old_value=current_instance.get("reaction"),
new_value=None,
field="reaction",
project=project,
workspace=project.workspace,
comment="removed the reaction",
old_identifier=current_instance.get("identifier"),
new_identifier=None,
epoch=epoch,
)
)
def create_comment_reaction_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
):
requested_data = json.loads(requested_data) if requested_data is not None else None
if requested_data and requested_data.get("reaction") is not None:
comment_reaction_id, comment_id = CommentReaction.objects.filter(reaction=requested_data.get("reaction"), project=project, actor=actor).values_list('id', 'comment__id').first()
comment = IssueComment.objects.get(pk=comment_id,project=project)
if comment is not None and comment_reaction_id is not None and comment_id is not None:
issue_activities.append(
IssueActivity(
issue_id=comment.issue_id,
actor=actor,
verb="created",
old_value=None,
new_value=requested_data.get("reaction"),
field="reaction",
project=project,
workspace=project.workspace,
comment="added the reaction",
old_identifier=None,
new_identifier=comment_reaction_id,
epoch=epoch,
)
)
def delete_comment_reaction_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
):
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
if current_instance and current_instance.get("reaction") is not None:
issue_id = IssueComment.objects.filter(pk=current_instance.get("comment_id"), project=project).values_list('issue_id', flat=True).first()
if issue_id is not None:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="deleted",
old_value=current_instance.get("reaction"),
new_value=None,
field="reaction",
project=project,
workspace=project.workspace,
comment="removed the reaction",
old_identifier=current_instance.get("identifier"),
new_identifier=None,
epoch=epoch,
)
)
def create_issue_vote_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
):
requested_data = json.loads(requested_data) if requested_data is not None else None
if requested_data and requested_data.get("vote") is not None:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="created",
old_value=None,
new_value=requested_data.get("vote"),
field="vote",
project=project,
workspace=project.workspace,
comment="added the vote",
old_identifier=None,
new_identifier=None,
epoch=epoch,
)
)
def delete_issue_vote_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
):
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
if current_instance and current_instance.get("vote") is not None:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
actor=actor,
verb="deleted",
old_value=current_instance.get("vote"),
new_value=None,
field="vote",
project=project,
workspace=project.workspace,
comment="removed the vote",
old_identifier=current_instance.get("identifier"),
new_identifier=None,
epoch=epoch,
)
)
def create_issue_relation_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
if current_instance is None and requested_data.get("related_list") is not None:
for issue_relation in requested_data.get("related_list"):
if issue_relation.get("relation_type") == "blocked_by":
relation_type = "blocking"
else:
relation_type = issue_relation.get("relation_type")
issue = Issue.objects.get(pk=issue_relation.get("issue"))
issue_activities.append(
IssueActivity(
issue_id=issue_relation.get("related_issue"),
actor=actor,
verb="created",
old_value="",
new_value=f"{project.identifier}-{issue.sequence_id}",
field=relation_type,
project=project,
workspace=project.workspace,
comment=f'added {relation_type} relation',
old_identifier=issue_relation.get("issue"),
)
)
issue = Issue.objects.get(pk=issue_relation.get("related_issue"))
issue_activities.append(
IssueActivity(
issue_id=issue_relation.get("issue"),
actor=actor,
verb="created",
old_value="",
new_value=f"{project.identifier}-{issue.sequence_id}",
field=f'{issue_relation.get("relation_type")}',
project=project,
workspace=project.workspace,
comment=f'added {issue_relation.get("relation_type")} relation',
old_identifier=issue_relation.get("related_issue"),
epoch=epoch,
)
)
def delete_issue_relation_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
if current_instance is not None and requested_data.get("related_list") is None:
if current_instance.get("relation_type") == "blocked_by":
relation_type = "blocking"
else:
relation_type = current_instance.get("relation_type")
issue = Issue.objects.get(pk=current_instance.get("issue"))
issue_activities.append(
IssueActivity(
issue_id=current_instance.get("related_issue"),
actor=actor,
verb="deleted",
old_value=f"{project.identifier}-{issue.sequence_id}",
new_value="",
field=relation_type,
project=project,
workspace=project.workspace,
comment=f'deleted {relation_type} relation',
old_identifier=current_instance.get("issue"),
epoch=epoch,
)
)
issue = Issue.objects.get(pk=current_instance.get("related_issue"))
issue_activities.append(
IssueActivity(
issue_id=current_instance.get("issue"),
actor=actor,
verb="deleted",
old_value=f"{project.identifier}-{issue.sequence_id}",
new_value="",
field=f'{current_instance.get("relation_type")}',
project=project,
workspace=project.workspace,
comment=f'deleted {current_instance.get("relation_type")} relation',
old_identifier=current_instance.get("related_issue"),
epoch=epoch,
)
)
def create_draft_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
):
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"drafted the issue",
field="draft",
verb="created",
actor=actor,
epoch=epoch,
)
)
def update_draft_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
):
requested_data = json.loads(requested_data) if requested_data is not None else None
current_instance = (
json.loads(current_instance) if current_instance is not None else None
)
if requested_data.get("is_draft") is not None and requested_data.get("is_draft") == False:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"created the issue",
verb="updated",
actor=actor,
epoch=epoch,
)
)
else:
issue_activities.append(
IssueActivity(
issue_id=issue_id,
project=project,
workspace=project.workspace,
comment=f"updated the draft issue",
field="draft",
verb="updated",
actor=actor,
epoch=epoch,
)
)
def delete_draft_issue_activity(
requested_data, current_instance, issue_id, project, actor, issue_activities, epoch
):
issue_activities.append(
IssueActivity(
project=project,
workspace=project.workspace,
comment=f"deleted the draft issue",
field="draft",
verb="deleted",
actor=actor,
epoch=epoch,
)
)
# Receive message from room group
@shared_task
@@ -1264,7 +1032,6 @@ def issue_activity(
issue_id,
actor_id,
project_id,
epoch,
subscriber=True,
):
try:
@@ -1278,12 +1045,6 @@ def issue_activity(
"cycle.activity.deleted",
"module.activity.created",
"module.activity.deleted",
"issue_reaction.activity.created",
"issue_reaction.activity.deleted",
"comment_reaction.activity.created",
"comment_reaction.activity.deleted",
"issue_vote.activity.created",
"issue_vote.activity.deleted",
]:
issue = Issue.objects.filter(pk=issue_id).first()
@@ -1319,17 +1080,6 @@ def issue_activity(
"link.activity.deleted": delete_link_activity,
"attachment.activity.created": create_attachment_activity,
"attachment.activity.deleted": delete_attachment_activity,
"issue_relation.activity.created": create_issue_relation_activity,
"issue_relation.activity.deleted": delete_issue_relation_activity,
"issue_reaction.activity.created": create_issue_reaction_activity,
"issue_reaction.activity.deleted": delete_issue_reaction_activity,
"comment_reaction.activity.created": create_comment_reaction_activity,
"comment_reaction.activity.deleted": delete_comment_reaction_activity,
"issue_vote.activity.created": create_issue_vote_activity,
"issue_vote.activity.deleted": delete_issue_vote_activity,
"issue_draft.activity.created": create_draft_issue_activity,
"issue_draft.activity.updated": update_draft_issue_activity,
"issue_draft.activity.deleted": delete_draft_issue_activity,
}
func = ACTIVITY_MAPPER.get(type)
@@ -1341,7 +1091,6 @@ def issue_activity(
project,
actor,
issue_activities,
epoch,
)
# Save all the values to database
@@ -1370,12 +1119,6 @@ def issue_activity(
"cycle.activity.deleted",
"module.activity.created",
"module.activity.deleted",
"issue_reaction.activity.created",
"issue_reaction.activity.deleted",
"comment_reaction.activity.created",
"comment_reaction.activity.deleted",
"issue_vote.activity.created",
"issue_vote.activity.deleted",
]:
# Create Notifications
bulk_notifications = []
@@ -32,7 +32,7 @@ def archive_old_issues():
archive_in = project.archive_in
# Get all the issues whose updated_at in less that the archive_in month
issues = Issue.issue_objects.filter(
issues = Issue.objects.filter(
Q(
project=project_id,
archived_at__isnull=True,
@@ -64,23 +64,21 @@ def archive_old_issues():
issues_to_update.append(issue)
# Bulk Update the issues and log the activity
if issues_to_update:
updated_issues = Issue.objects.bulk_update(
issues_to_update, ["archived_at"], batch_size=100
Issue.objects.bulk_update(
issues_to_update, ["archived_at"], batch_size=100
)
[
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"archived_at": str(issue.archived_at)}),
actor_id=str(project.created_by_id),
issue_id=issue.id,
project_id=project_id,
current_instance=None,
subscriber=False,
)
[
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"archived_at": str(issue.archived_at)}),
actor_id=str(project.created_by_id),
issue_id=issue.id,
project_id=project_id,
current_instance=None,
subscriber=False,
epoch = int(timezone.now().timestamp())
)
for issue in updated_issues
]
for issue in issues_to_update
]
return
except Exception as e:
if settings.DEBUG:
@@ -101,7 +99,7 @@ def close_old_issues():
close_in = project.close_in
# Get all the issues whose updated_at in less that the close_in month
issues = Issue.issue_objects.filter(
issues = Issue.objects.filter(
Q(
project=project_id,
archived_at__isnull=True,
@@ -138,21 +136,19 @@ def close_old_issues():
issues_to_update.append(issue)
# Bulk Update the issues and log the activity
if issues_to_update:
updated_issues = Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
[
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"closed_to": str(issue.state_id)}),
actor_id=str(project.created_by_id),
issue_id=issue.id,
project_id=project_id,
current_instance=None,
subscriber=False,
epoch = int(timezone.now().timestamp())
)
for issue in updated_issues
]
Issue.objects.bulk_update(issues_to_update, ["state"], batch_size=100)
[
issue_activity.delay(
type="issue.activity.updated",
requested_data=json.dumps({"closed_to": str(issue.state_id)}),
actor_id=str(project.created_by_id),
issue_id=issue.id,
project_id=project_id,
current_instance=None,
subscriber=False,
)
for issue in issues_to_update
]
return
except Exception as e:
if settings.DEBUG:
File diff suppressed because one or more lines are too long
@@ -0,0 +1,26 @@
# Generated by Django 4.2.3 on 2023-08-29 07:58
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0041_cycle_sort_order_issuecomment_access_and_more'),
]
operations = [
migrations.AlterUniqueTogether(
name='issuevote',
unique_together=set(),
),
migrations.AlterField(
model_name='issuevote',
name='vote',
field=models.IntegerField(choices=[(-1, 'DOWNVOTE'), (1, 'UPVOTE')], default=1),
),
migrations.AlterUniqueTogether(
name='issuevote',
unique_together={('issue', 'actor', 'vote')},
),
]
@@ -1,84 +0,0 @@
# Generated by Django 4.2.3 on 2023-09-12 07:29
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
from plane.db.models import IssueRelation
from sentry_sdk import capture_exception
import uuid
def create_issue_relation(apps, schema_editor):
try:
IssueBlockerModel = apps.get_model("db", "IssueBlocker")
updated_issue_relation = []
for blocked_issue in IssueBlockerModel.objects.all():
updated_issue_relation.append(
IssueRelation(
issue_id=blocked_issue.block_id,
related_issue_id=blocked_issue.blocked_by_id,
relation_type="blocked_by",
project_id=blocked_issue.project_id,
workspace_id=blocked_issue.workspace_id,
created_by_id=blocked_issue.created_by_id,
updated_by_id=blocked_issue.updated_by_id,
)
)
IssueRelation.objects.bulk_create(updated_issue_relation, batch_size=100)
except Exception as e:
print(e)
capture_exception(e)
def update_issue_priority_choice(apps, schema_editor):
IssueModel = apps.get_model("db", "Issue")
updated_issues = []
for obj in IssueModel.objects.all():
if obj.priority is None:
obj.priority = "none"
updated_issues.append(obj)
IssueModel.objects.bulk_update(updated_issues, ["priority"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('db', '0042_alter_analyticview_created_by_and_more'),
]
operations = [
migrations.CreateModel(
name='IssueRelation',
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)),
('relation_type', models.CharField(choices=[('duplicate', 'Duplicate'), ('relates_to', 'Relates To'), ('blocked_by', 'Blocked By')], default='blocked_by', max_length=20, verbose_name='Issue Relation Type')),
('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')),
('issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_relation', to='db.issue')),
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
('related_issue', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='issue_related', to='db.issue')),
('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')),
],
options={
'verbose_name': 'Issue Relation',
'verbose_name_plural': 'Issue Relations',
'db_table': 'issue_relations',
'ordering': ('-created_at',),
'unique_together': {('issue', 'related_issue')},
},
),
migrations.AddField(
model_name='issue',
name='is_draft',
field=models.BooleanField(default=False),
),
migrations.AlterField(
model_name='issue',
name='priority',
field=models.CharField(choices=[('urgent', 'Urgent'), ('high', 'High'), ('medium', 'Medium'), ('low', 'Low'), ('none', 'None')], default='none', max_length=30, verbose_name='Issue Priority'),
),
migrations.RunPython(create_issue_relation),
migrations.RunPython(update_issue_priority_choice),
]
@@ -1,138 +0,0 @@
# Generated by Django 4.2.3 on 2023-09-13 07:09
from django.db import migrations
def workspace_member_props(old_props):
new_props = {
"filters": {
"priority": old_props.get("filters", {}).get("priority", None),
"state": old_props.get("filters", {}).get("state", None),
"state_group": old_props.get("filters", {}).get("state_group", None),
"assignees": old_props.get("filters", {}).get("assignees", None),
"created_by": old_props.get("filters", {}).get("created_by", None),
"labels": old_props.get("filters", {}).get("labels", None),
"start_date": old_props.get("filters", {}).get("start_date", None),
"target_date": old_props.get("filters", {}).get("target_date", None),
"subscriber": old_props.get("filters", {}).get("subscriber", None),
},
"display_filters": {
"group_by": old_props.get("groupByProperty", None),
"order_by": old_props.get("orderBy", "-created_at"),
"type": old_props.get("filters", {}).get("type", None),
"sub_issue": old_props.get("showSubIssues", True),
"show_empty_groups": old_props.get("showEmptyGroups", True),
"layout": old_props.get("issueView", "list"),
"calendar_date_range": old_props.get("calendarDateRange", ""),
},
"display_properties": {
"assignee": old_props.get("properties", {}).get("assignee",None),
"attachment_count": old_props.get("properties", {}).get("attachment_count", None),
"created_on": old_props.get("properties", {}).get("created_on", None),
"due_date": old_props.get("properties", {}).get("due_date", None),
"estimate": old_props.get("properties", {}).get("estimate", None),
"key": old_props.get("properties", {}).get("key", None),
"labels": old_props.get("properties", {}).get("labels", None),
"link": old_props.get("properties", {}).get("link", None),
"priority": old_props.get("properties", {}).get("priority", None),
"start_date": old_props.get("properties", {}).get("start_date", None),
"state": old_props.get("properties", {}).get("state", None),
"sub_issue_count": old_props.get("properties", {}).get("sub_issue_count", None),
"updated_on": old_props.get("properties", {}).get("updated_on", None),
},
}
return new_props
def project_member_props(old_props):
new_props = {
"filters": {
"priority": old_props.get("filters", {}).get("priority", None),
"state": old_props.get("filters", {}).get("state", None),
"state_group": old_props.get("filters", {}).get("state_group", None),
"assignees": old_props.get("filters", {}).get("assignees", None),
"created_by": old_props.get("filters", {}).get("created_by", None),
"labels": old_props.get("filters", {}).get("labels", None),
"start_date": old_props.get("filters", {}).get("start_date", None),
"target_date": old_props.get("filters", {}).get("target_date", None),
"subscriber": old_props.get("filters", {}).get("subscriber", None),
},
"display_filters": {
"group_by": old_props.get("groupByProperty", None),
"order_by": old_props.get("orderBy", "-created_at"),
"type": old_props.get("filters", {}).get("type", None),
"sub_issue": old_props.get("showSubIssues", True),
"show_empty_groups": old_props.get("showEmptyGroups", True),
"layout": old_props.get("issueView", "list"),
"calendar_date_range": old_props.get("calendarDateRange", ""),
},
}
return new_props
def cycle_module_props(old_props):
new_props = {
"filters": {
"priority": old_props.get("filters", {}).get("priority", None),
"state": old_props.get("filters", {}).get("state", None),
"state_group": old_props.get("filters", {}).get("state_group", None),
"assignees": old_props.get("filters", {}).get("assignees", None),
"created_by": old_props.get("filters", {}).get("created_by", None),
"labels": old_props.get("filters", {}).get("labels", None),
"start_date": old_props.get("filters", {}).get("start_date", None),
"target_date": old_props.get("filters", {}).get("target_date", None),
"subscriber": old_props.get("filters", {}).get("subscriber", None),
},
}
return new_props
def update_workspace_member_view_props(apps, schema_editor):
WorkspaceMemberModel = apps.get_model("db", "WorkspaceMember")
updated_workspace_member = []
for obj in WorkspaceMemberModel.objects.all():
obj.view_props = workspace_member_props(obj.view_props)
obj.default_props = workspace_member_props(obj.default_props)
updated_workspace_member.append(obj)
WorkspaceMemberModel.objects.bulk_update(updated_workspace_member, ["view_props", "default_props"], batch_size=100)
def update_project_member_view_props(apps, schema_editor):
ProjectMemberModel = apps.get_model("db", "ProjectMember")
updated_project_member = []
for obj in ProjectMemberModel.objects.all():
obj.view_props = project_member_props(obj.view_props)
obj.default_props = project_member_props(obj.default_props)
updated_project_member.append(obj)
ProjectMemberModel.objects.bulk_update(updated_project_member, ["view_props", "default_props"], batch_size=100)
def update_cycle_props(apps, schema_editor):
CycleModel = apps.get_model("db", "Cycle")
updated_cycle = []
for obj in CycleModel.objects.all():
if "filter" in obj.view_props:
obj.view_props = cycle_module_props(obj.view_props)
updated_cycle.append(obj)
CycleModel.objects.bulk_update(updated_cycle, ["view_props"], batch_size=100)
def update_module_props(apps, schema_editor):
ModuleModel = apps.get_model("db", "Module")
updated_module = []
for obj in ModuleModel.objects.all():
if "filter" in obj.view_props:
obj.view_props = cycle_module_props(obj.view_props)
updated_module.append(obj)
ModuleModel.objects.bulk_update(updated_module, ["view_props"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('db', '0043_alter_analyticview_created_by_and_more'),
]
operations = [
migrations.RunPython(update_workspace_member_view_props),
migrations.RunPython(update_project_member_view_props),
migrations.RunPython(update_cycle_props),
migrations.RunPython(update_module_props),
]
@@ -1,24 +0,0 @@
# Generated by Django 4.2.3 on 2023-09-15 06:55
from django.db import migrations
def update_issue_activity(apps, schema_editor):
IssueActivityModel = apps.get_model("db", "IssueActivity")
updated_issue_activity = []
for obj in IssueActivityModel.objects.all():
if obj.field == "blocks":
obj.field = "blocked_by"
updated_issue_activity.append(obj)
IssueActivityModel.objects.bulk_update(updated_issue_activity, ["field"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('db', '0044_auto_20230913_0709'),
]
operations = [
migrations.RunPython(update_issue_activity),
]
@@ -1,53 +0,0 @@
# Generated by Django 4.2.3 on 2023-09-19 14:21
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
def update_epoch(apps, schema_editor):
IssueActivity = apps.get_model('db', 'IssueActivity')
updated_issue_activity = []
for obj in IssueActivity.objects.all():
obj.epoch = int(obj.created_at.timestamp())
updated_issue_activity.append(obj)
IssueActivity.objects.bulk_update(updated_issue_activity, ["epoch"], batch_size=100)
class Migration(migrations.Migration):
dependencies = [
('db', '0045_auto_20230915_0655'),
]
operations = [
migrations.CreateModel(
name='GlobalView',
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, verbose_name='View Name')),
('description', models.TextField(blank=True, verbose_name='View Description')),
('query', models.JSONField(verbose_name='View Query')),
('access', models.PositiveSmallIntegerField(choices=[(0, 'Private'), (1, 'Public')], default=1)),
('query_data', models.JSONField(default=dict)),
('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')),
('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='global_views', to='db.workspace')),
],
options={
'verbose_name': 'Global View',
'verbose_name_plural': 'Global Views',
'db_table': 'global_views',
'ordering': ('-created_at',),
},
),
migrations.AddField(
model_name='issueactivity',
name='epoch',
field=models.FloatField(null=True),
),
migrations.RunPython(update_epoch),
]
+1 -3
View File
@@ -19,7 +19,6 @@ from .project import (
ProjectIdentifier,
ProjectFavorite,
ProjectDeployBoard,
ProjectPublicMember,
)
from .issue import (
@@ -32,7 +31,6 @@ from .issue import (
IssueAssignee,
Label,
IssueBlocker,
IssueRelation,
IssueLink,
IssueSequence,
IssueAttachment,
@@ -50,7 +48,7 @@ from .state import State
from .cycle import Cycle, CycleIssue, CycleFavorite
from .view import GlobalView, IssueView, IssueViewFavorite
from .view import IssueView, IssueViewFavorite
from .module import Module, ModuleMember, ModuleIssue, ModuleLink, ModuleFavorite
+4 -38
View File
@@ -29,7 +29,6 @@ class IssueManager(models.Manager):
| models.Q(issue_inbox__isnull=True)
)
.exclude(archived_at__isnull=False)
.exclude(is_draft=True)
)
@@ -39,7 +38,6 @@ class Issue(ProjectBaseModel):
("high", "High"),
("medium", "Medium"),
("low", "Low"),
("none", "None")
)
parent = models.ForeignKey(
"self",
@@ -66,7 +64,8 @@ class Issue(ProjectBaseModel):
max_length=30,
choices=PRIORITY_CHOICES,
verbose_name="Issue Priority",
default="none",
null=True,
blank=True,
)
start_date = models.DateField(null=True, blank=True)
target_date = models.DateField(null=True, blank=True)
@@ -84,7 +83,6 @@ class Issue(ProjectBaseModel):
sort_order = models.FloatField(default=65535)
completed_at = models.DateTimeField(null=True)
archived_at = models.DateField(null=True)
is_draft = models.BooleanField(default=False)
objects = models.Manager()
issue_objects = IssueManager()
@@ -180,37 +178,6 @@ class IssueBlocker(ProjectBaseModel):
return f"{self.block.name} {self.blocked_by.name}"
class IssueRelation(ProjectBaseModel):
RELATION_CHOICES = (
("duplicate", "Duplicate"),
("relates_to", "Relates To"),
("blocked_by", "Blocked By"),
)
issue = models.ForeignKey(
Issue, related_name="issue_relation", on_delete=models.CASCADE
)
related_issue = models.ForeignKey(
Issue, related_name="issue_related", on_delete=models.CASCADE
)
relation_type = models.CharField(
max_length=20,
choices=RELATION_CHOICES,
verbose_name="Issue Relation Type",
default="blocked_by",
)
class Meta:
unique_together = ["issue", "related_issue"]
verbose_name = "Issue Relation"
verbose_name_plural = "Issue Relations"
db_table = "issue_relations"
ordering = ("-created_at",)
def __str__(self):
return f"{self.issue.name} {self.related_issue.name}"
class IssueAssignee(ProjectBaseModel):
issue = models.ForeignKey(
Issue, on_delete=models.CASCADE, related_name="issue_assignee"
@@ -309,7 +276,6 @@ class IssueActivity(ProjectBaseModel):
)
old_identifier = models.UUIDField(null=True)
new_identifier = models.UUIDField(null=True)
epoch = models.FloatField(null=True)
class Meta:
verbose_name = "Issue Activity"
@@ -327,7 +293,7 @@ class IssueComment(ProjectBaseModel):
comment_json = models.JSONField(blank=True, default=dict)
comment_html = models.TextField(blank=True, default="<p></p>")
attachments = ArrayField(models.URLField(), size=10, blank=True, default=list)
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="issue_comments")
issue = models.ForeignKey(Issue, on_delete=models.CASCADE)
# System can also create comment
actor = models.ForeignKey(
settings.AUTH_USER_MODEL,
@@ -515,7 +481,7 @@ class IssueVote(ProjectBaseModel):
)
class Meta:
unique_together = ["issue", "actor",]
unique_together = ["issue", "actor", "vote"]
verbose_name = "Issue Vote"
verbose_name_plural = "Issue Votes"
db_table = "issue_votes"
+7 -35
View File
@@ -25,26 +25,13 @@ ROLE_CHOICES = (
def get_default_props():
return {
"filters": {
"priority": None,
"state": None,
"state_group": None,
"assignees": None,
"created_by": None,
"labels": None,
"start_date": None,
"target_date": None,
"subscriber": None,
},
"display_filters": {
"group_by": None,
"order_by": '-created_at',
"type": None,
"sub_issue": True,
"show_empty_groups": True,
"layout": "list",
"calendar_date_range": "",
},
"filters": {"type": None},
"orderBy": "-created_at",
"collapsed": True,
"issueView": "list",
"filterIssue": None,
"groupByProperty": None,
"showEmptyGroups": True,
}
@@ -267,18 +254,3 @@ class ProjectDeployBoard(ProjectBaseModel):
def __str__(self):
"""Return project and anchor"""
return f"{self.anchor} <{self.project.name}>"
class ProjectPublicMember(ProjectBaseModel):
member = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="public_project_members",
)
class Meta:
unique_together = ["project", "member"]
verbose_name = "Project Public Member"
verbose_name_plural = "Project Public Members"
db_table = "project_public_members"
ordering = ("-created_at",)
+4 -3
View File
@@ -2,7 +2,6 @@
import uuid
import string
import random
import pytz
# Django imports
from django.db import models
@@ -10,6 +9,9 @@ from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import AbstractBaseUser, UserManager, PermissionsMixin
from django.utils import timezone
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags
from django.conf import settings
# Third party imports
@@ -64,8 +66,7 @@ class User(AbstractBaseUser, PermissionsMixin):
billing_address = models.JSONField(null=True)
has_billing_address = models.BooleanField(default=False)
USER_TIMEZONE_CHOICES = tuple(zip(pytz.all_timezones, pytz.all_timezones))
user_timezone = models.CharField(max_length=255, default="UTC", choices=USER_TIMEZONE_CHOICES)
user_timezone = models.CharField(max_length=255, default="Asia/Kolkata")
last_active = models.DateTimeField(default=timezone.now, null=True)
last_login_time = models.DateTimeField(null=True)
+1 -24
View File
@@ -3,30 +3,7 @@ from django.db import models
from django.conf import settings
# Module import
from . import ProjectBaseModel, BaseModel
class GlobalView(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="global_views"
)
name = models.CharField(max_length=255, verbose_name="View Name")
description = models.TextField(verbose_name="View Description", blank=True)
query = models.JSONField(verbose_name="View Query")
access = models.PositiveSmallIntegerField(
default=1, choices=((0, "Private"), (1, "Public"))
)
query_data = models.JSONField(default=dict)
class Meta:
verbose_name = "Global View"
verbose_name_plural = "Global Views"
db_table = "global_views"
ordering = ("-created_at",)
def __str__(self):
"""Return name of the View"""
return f"{self.name} <{self.workspace.name}>"
from . import ProjectBaseModel
class IssueView(ProjectBaseModel):
+12 -27
View File
@@ -16,41 +16,26 @@ ROLE_CHOICES = (
def get_default_props():
return {
"filters": {
"priority": None,
"state": None,
"state_group": None,
"assignees": None,
"created_by": None,
"labels": None,
"start_date": None,
"target_date": None,
"subscriber": None,
},
"display_filters": {
"group_by": None,
"order_by": '-created_at',
"type": None,
"sub_issue": True,
"show_empty_groups": True,
"layout": "list",
"calendar_date_range": "",
},
"display_properties": {
"filters": {"type": None},
"groupByProperty": None,
"issueView": "list",
"orderBy": "-created_at",
"properties": {
"assignee": True,
"attachment_count": True,
"created_on": True,
"due_date": True,
"estimate": True,
"key": True,
"labels": True,
"link": True,
"priority": True,
"start_date": True,
"state": True,
"sub_issue_count": True,
"attachment_count": True,
"link": True,
"estimate": True,
"created_on": True,
"updated_on": True,
}
"start_date": True,
},
"showEmptyGroups": True,
}
+2 -2
View File
@@ -49,7 +49,7 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"crum.CurrentRequestUserMiddleware",
"django.middleware.gzip.GZipMiddleware",
]
]
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
@@ -161,7 +161,7 @@ MEDIA_URL = "/media/"
LANGUAGE_CODE = "en-us"
TIME_ZONE = "UTC"
TIME_ZONE = "Asia/Kolkata"
USE_I18N = True
+115 -76
View File
@@ -1,8 +1,10 @@
"""Production settings and globals."""
from urllib.parse import urlparse
import ssl
import certifi
import dj_database_url
from urllib.parse import urlparse
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration
@@ -89,89 +91,112 @@ if bool(os.environ.get("SENTRY_DSN", False)):
profiles_sample_rate=1.0,
)
# The AWS region to connect to.
AWS_REGION = os.environ.get("AWS_REGION", "")
if DOCKERIZED and USE_MINIO:
INSTALLED_APPS += ("storages",)
STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}
# The AWS access key to use.
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
# The AWS secret access key to use.
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key")
# The name of the bucket to store files in.
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads")
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = os.environ.get(
"AWS_S3_ENDPOINT_URL", "http://plane-minio:9000"
)
# Default permissions
AWS_DEFAULT_ACL = "public-read"
AWS_QUERYSTRING_AUTH = False
AWS_S3_FILE_OVERWRITE = False
# The AWS access key to use.
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "")
# Custom Domain settings
parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost"))
AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}"
AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:"
else:
# The AWS region to connect to.
AWS_REGION = os.environ.get("AWS_REGION", "")
# The AWS secret access key to use.
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "")
# The AWS access key to use.
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "")
# The optional AWS session token to use.
# AWS_SESSION_TOKEN = ""
# The AWS secret access key to use.
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "")
# The name of the bucket to store files in.
AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
# The optional AWS session token to use.
# AWS_SESSION_TOKEN = ""
# How to construct S3 URLs ("auto", "path", "virtual").
AWS_S3_ADDRESSING_STYLE = "auto"
# The name of the bucket to store files in.
AWS_S3_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME")
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
# How to construct S3 URLs ("auto", "path", "virtual").
AWS_S3_ADDRESSING_STYLE = "auto"
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
AWS_S3_KEY_PREFIX = ""
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = os.environ.get("AWS_S3_ENDPOINT_URL", "")
# Whether to enable authentication for stored files. If True, then generated URLs will include an authentication
# token valid for `AWS_S3_MAX_AGE_SECONDS`. If False, then generated URLs will not include an authentication token,
# and their permissions will be set to "public-read".
AWS_S3_BUCKET_AUTH = False
# A prefix to be applied to every stored file. This will be joined to every filename using the "/" separator.
AWS_S3_KEY_PREFIX = ""
# How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH`
# is True. It also affects the "Cache-Control" header of the files.
# Important: Changing this setting will not affect existing files.
AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours.
# Whether to enable authentication for stored files. If True, then generated URLs will include an authentication
# token valid for `AWS_S3_MAX_AGE_SECONDS`. If False, then generated URLs will not include an authentication token,
# and their permissions will be set to "public-read".
AWS_S3_BUCKET_AUTH = False
# A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting
# cannot be used with `AWS_S3_BUCKET_AUTH`.
AWS_S3_PUBLIC_URL = ""
# How long generated URLs are valid for. This affects the expiry of authentication tokens if `AWS_S3_BUCKET_AUTH`
# is True. It also affects the "Cache-Control" header of the files.
# Important: Changing this setting will not affect existing files.
AWS_S3_MAX_AGE_SECONDS = 60 * 60 # 1 hours.
# If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you
# understand the consequences before enabling.
# Important: Changing this setting will not affect existing files.
AWS_S3_REDUCED_REDUNDANCY = False
# A URL prefix to be used for generated URLs. This is useful if your bucket is served through a CDN. This setting
# cannot be used with `AWS_S3_BUCKET_AUTH`.
AWS_S3_PUBLIC_URL = ""
# The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a
# single `name` argument.
# Important: Changing this setting will not affect existing files.
AWS_S3_CONTENT_DISPOSITION = ""
# If True, then files will be stored with reduced redundancy. Check the S3 documentation and make sure you
# understand the consequences before enabling.
# Important: Changing this setting will not affect existing files.
AWS_S3_REDUCED_REDUNDANCY = False
# The Content-Language header used when the file is downloaded. This can be a string, or a function taking a
# single `name` argument.
# Important: Changing this setting will not affect existing files.
AWS_S3_CONTENT_LANGUAGE = ""
# The Content-Disposition header used when the file is downloaded. This can be a string, or a function taking a
# single `name` argument.
# Important: Changing this setting will not affect existing files.
AWS_S3_CONTENT_DISPOSITION = ""
# A mapping of custom metadata for each file. Each value can be a string, or a function taking a
# single `name` argument.
# Important: Changing this setting will not affect existing files.
AWS_S3_METADATA = {}
# The Content-Language header used when the file is downloaded. This can be a string, or a function taking a
# single `name` argument.
# Important: Changing this setting will not affect existing files.
AWS_S3_CONTENT_LANGUAGE = ""
# If True, then files will be stored using AES256 server-side encryption.
# If this is a string value (e.g., "aws:kms"), that encryption type will be used.
# Otherwise, server-side encryption is not be enabled.
# Important: Changing this setting will not affect existing files.
AWS_S3_ENCRYPT_KEY = False
# A mapping of custom metadata for each file. Each value can be a string, or a function taking a
# single `name` argument.
# Important: Changing this setting will not affect existing files.
AWS_S3_METADATA = {}
# The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present.
# This is only relevant if AWS S3 KMS server-side encryption is enabled (above).
# AWS_S3_KMS_ENCRYPTION_KEY_ID = ""
# If True, then files will be stored using AES256 server-side encryption.
# If this is a string value (e.g., "aws:kms"), that encryption type will be used.
# Otherwise, server-side encryption is not be enabled.
# Important: Changing this setting will not affect existing files.
AWS_S3_ENCRYPT_KEY = False
# If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their
# compressed size is smaller than their uncompressed size.
# Important: Changing this setting will not affect existing files.
AWS_S3_GZIP = True
# The AWS S3 KMS encryption key ID (the `SSEKMSKeyId` parameter) is set from this string if present.
# This is only relevant if AWS S3 KMS server-side encryption is enabled (above).
# AWS_S3_KMS_ENCRYPTION_KEY_ID = ""
# The signature version to use for S3 requests.
AWS_S3_SIGNATURE_VERSION = None
# If True, then text files will be stored using gzip content encoding. Files will only be gzipped if their
# compressed size is smaller than their uncompressed size.
# Important: Changing this setting will not affect existing files.
AWS_S3_GZIP = True
# If True, then files with the same name will overwrite each other. By default it's set to False to have
# extra characters appended.
AWS_S3_FILE_OVERWRITE = False
# The signature version to use for S3 requests.
AWS_S3_SIGNATURE_VERSION = None
STORAGES["default"] = {
"BACKEND": "django_s3_storage.storage.S3Storage",
}
# If True, then files with the same name will overwrite each other. By default it's set to False to have
# extra characters appended.
AWS_S3_FILE_OVERWRITE = False
STORAGES["default"] = {
"BACKEND": "django_s3_storage.storage.S3Storage",
}
# AWS Settings End
@@ -193,16 +218,27 @@ CSRF_COOKIE_SECURE = True
REDIS_URL = os.environ.get("REDIS_URL")
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": REDIS_URL,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False},
},
if DOCKERIZED:
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": REDIS_URL,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
}
}
else:
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": REDIS_URL,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"CONNECTION_POOL_KWARGS": {"ssl_cert_reqs": False},
},
}
}
}
WEB_URL = os.environ.get("WEB_URL", "https://app.plane.so")
@@ -225,16 +261,19 @@ broker_url = (
f"{redis_url}?ssl_cert_reqs={ssl.CERT_NONE.name}&ssl_ca_certs={certifi.where()}"
)
CELERY_RESULT_BACKEND = broker_url
CELERY_BROKER_URL = broker_url
if DOCKERIZED:
CELERY_BROKER_URL = REDIS_URL
CELERY_RESULT_BACKEND = REDIS_URL
else:
CELERY_RESULT_BACKEND = broker_url
CELERY_BROKER_URL = broker_url
GITHUB_ACCESS_TOKEN = os.environ.get("GITHUB_ACCESS_TOKEN", False)
# Enable or Disable signups
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
# Scout Settings
SCOUT_MONITOR = os.environ.get("SCOUT_MONITOR", False)
SCOUT_KEY = os.environ.get("SCOUT_KEY", "")
SCOUT_NAME = "Plane"
-128
View File
@@ -1,128 +0,0 @@
"""Self hosted settings and globals."""
from urllib.parse import urlparse
import dj_database_url
from urllib.parse import urlparse
from .common import * # noqa
# Database
DEBUG = int(os.environ.get("DEBUG", 0)) == 1
# Docker configurations
DOCKERIZED = 1
USE_MINIO = 1
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"NAME": "plane",
"USER": os.environ.get("PGUSER", ""),
"PASSWORD": os.environ.get("PGPASSWORD", ""),
"HOST": os.environ.get("PGHOST", ""),
}
}
# Parse database configuration from $DATABASE_URL
DATABASES["default"] = dj_database_url.config()
SITE_ID = 1
# File size limit
FILE_SIZE_LIMIT = int(os.environ.get("FILE_SIZE_LIMIT", 5242880))
CORS_ALLOW_METHODS = [
"DELETE",
"GET",
"OPTIONS",
"PATCH",
"POST",
"PUT",
]
CORS_ALLOW_HEADERS = [
"accept",
"accept-encoding",
"authorization",
"content-type",
"dnt",
"origin",
"user-agent",
"x-csrftoken",
"x-requested-with",
]
CORS_ALLOW_CREDENTIALS = True
CORS_ALLOW_ALL_ORIGINS = True
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
INSTALLED_APPS += ("storages",)
STORAGES["default"] = {"BACKEND": "storages.backends.s3boto3.S3Boto3Storage"}
# The AWS access key to use.
AWS_ACCESS_KEY_ID = os.environ.get("AWS_ACCESS_KEY_ID", "access-key")
# The AWS secret access key to use.
AWS_SECRET_ACCESS_KEY = os.environ.get("AWS_SECRET_ACCESS_KEY", "secret-key")
# The name of the bucket to store files in.
AWS_STORAGE_BUCKET_NAME = os.environ.get("AWS_S3_BUCKET_NAME", "uploads")
# The full URL to the S3 endpoint. Leave blank to use the default region URL.
AWS_S3_ENDPOINT_URL = os.environ.get(
"AWS_S3_ENDPOINT_URL", "http://plane-minio:9000"
)
# Default permissions
AWS_DEFAULT_ACL = "public-read"
AWS_QUERYSTRING_AUTH = False
AWS_S3_FILE_OVERWRITE = False
# Custom Domain settings
parsed_url = urlparse(os.environ.get("WEB_URL", "http://localhost"))
AWS_S3_CUSTOM_DOMAIN = f"{parsed_url.netloc}/{AWS_STORAGE_BUCKET_NAME}"
AWS_S3_URL_PROTOCOL = f"{parsed_url.scheme}:"
# Honor the 'X-Forwarded-Proto' header for request.is_secure()
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
# Allow all host headers
ALLOWED_HOSTS = [
"*",
]
# Security settings
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
# Redis URL
REDIS_URL = os.environ.get("REDIS_URL")
# Caches
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": REDIS_URL,
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
}
}
# URL used for email redirects
WEB_URL = os.environ.get("WEB_URL", "http://localhost")
# Celery settings
CELERY_BROKER_URL = REDIS_URL
CELERY_RESULT_BACKEND = REDIS_URL
# Enable or Disable signups
ENABLE_SIGNUP = os.environ.get("ENABLE_SIGNUP", "1") == "1"
# Analytics
ANALYTICS_BASE_API = False
# OPEN AI Settings
OPENAI_API_BASE = os.environ.get("OPENAI_API_BASE", "https://api.openai.com/v1")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", False)
GPT_ENGINE = os.environ.get("GPT_ENGINE", "gpt-3.5-turbo")
+2 -2
View File
@@ -96,7 +96,7 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
chart_data = {str(date): 0 for date in date_range}
completed_issues_distribution = (
Issue.issue_objects.filter(
Issue.objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_cycle__cycle_id=cycle_id,
@@ -118,7 +118,7 @@ def burndown_plot(queryset, slug, project_id, cycle_id=None, module_id=None):
chart_data = {str(date): 0 for date in date_range}
completed_issues_distribution = (
Issue.issue_objects.filter(
Issue.objects.filter(
workspace__slug=slug,
project_id=project_id,
issue_module__module_id=module_id,
+29 -131
View File
@@ -15,7 +15,7 @@ def resolve_keys(group_keys, value):
return value
def group_results(results_data, group_by, sub_group_by=False):
def group_results(results_data, group_by):
"""group results data into certain group_by
Args:
@@ -25,140 +25,38 @@ def group_results(results_data, group_by, sub_group_by=False):
Returns:
obj: grouped results
"""
if sub_group_by:
main_responsive_dict = dict()
response_dict = dict()
if sub_group_by == "priority":
main_responsive_dict = {
"urgent": {},
"high": {},
"medium": {},
"low": {},
"none": {},
}
if group_by == "priority":
response_dict = {
"urgent": [],
"high": [],
"medium": [],
"low": [],
"None": [],
}
for value in results_data:
main_group_attribute = resolve_keys(sub_group_by, value)
group_attribute = resolve_keys(group_by, value)
if isinstance(main_group_attribute, list) and not isinstance(group_attribute, list):
if len(main_group_attribute):
for attrib in main_group_attribute:
if str(attrib) not in main_responsive_dict:
main_responsive_dict[str(attrib)] = {}
if str(group_attribute) in main_responsive_dict[str(attrib)]:
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
else:
main_responsive_dict[str(attrib)][str(group_attribute)] = []
main_responsive_dict[str(attrib)][str(group_attribute)].append(value)
else:
if str(None) not in main_responsive_dict:
main_responsive_dict[str(None)] = {}
if str(group_attribute) in main_responsive_dict[str(None)]:
main_responsive_dict[str(None)][str(group_attribute)].append(value)
for value in results_data:
group_attribute = resolve_keys(group_by, value)
if isinstance(group_attribute, list):
if len(group_attribute):
for attrib in group_attribute:
if str(attrib) in response_dict:
response_dict[str(attrib)].append(value)
else:
main_responsive_dict[str(None)][str(group_attribute)] = []
main_responsive_dict[str(None)][str(group_attribute)].append(value)
elif isinstance(group_attribute, list) and not isinstance(main_group_attribute, list):
if str(main_group_attribute) not in main_responsive_dict:
main_responsive_dict[str(main_group_attribute)] = {}
if len(group_attribute):
for attrib in group_attribute:
if str(attrib) in main_responsive_dict[str(main_group_attribute)]:
main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value)
else:
main_responsive_dict[str(main_group_attribute)][str(attrib)] = []
main_responsive_dict[str(main_group_attribute)][str(attrib)].append(value)
else:
if str(None) in main_responsive_dict[str(main_group_attribute)]:
main_responsive_dict[str(main_group_attribute)][str(None)].append(value)
else:
main_responsive_dict[str(main_group_attribute)][str(None)] = []
main_responsive_dict[str(main_group_attribute)][str(None)].append(value)
elif isinstance(group_attribute, list) and isinstance(main_group_attribute, list):
if len(main_group_attribute):
for main_attrib in main_group_attribute:
if str(main_attrib) not in main_responsive_dict:
main_responsive_dict[str(main_attrib)] = {}
if len(group_attribute):
for attrib in group_attribute:
if str(attrib) in main_responsive_dict[str(main_attrib)]:
main_responsive_dict[str(main_attrib)][str(attrib)].append(value)
else:
main_responsive_dict[str(main_attrib)][str(attrib)] = []
main_responsive_dict[str(main_attrib)][str(attrib)].append(value)
else:
if str(None) in main_responsive_dict[str(main_attrib)]:
main_responsive_dict[str(main_attrib)][str(None)].append(value)
else:
main_responsive_dict[str(main_attrib)][str(None)] = []
main_responsive_dict[str(main_attrib)][str(None)].append(value)
else:
if str(None) not in main_responsive_dict:
main_responsive_dict[str(None)] = {}
if len(group_attribute):
for attrib in group_attribute:
if str(attrib) in main_responsive_dict[str(None)]:
main_responsive_dict[str(None)][str(attrib)].append(value)
else:
main_responsive_dict[str(None)][str(attrib)] = []
main_responsive_dict[str(None)][str(attrib)].append(value)
else:
if str(None) in main_responsive_dict[str(None)]:
main_responsive_dict[str(None)][str(None)].append(value)
else:
main_responsive_dict[str(None)][str(None)] = []
main_responsive_dict[str(None)][str(None)].append(value)
response_dict[str(attrib)] = []
response_dict[str(attrib)].append(value)
else:
main_group_attribute = resolve_keys(sub_group_by, value)
group_attribute = resolve_keys(group_by, value)
if str(main_group_attribute) not in main_responsive_dict:
main_responsive_dict[str(main_group_attribute)] = {}
if str(group_attribute) in main_responsive_dict[str(main_group_attribute)]:
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
if str(None) in response_dict:
response_dict[str(None)].append(value)
else:
main_responsive_dict[str(main_group_attribute)][str(group_attribute)] = []
main_responsive_dict[str(main_group_attribute)][str(group_attribute)].append(value)
return main_responsive_dict
else:
response_dict = dict()
if group_by == "priority":
response_dict = {
"urgent": [],
"high": [],
"medium": [],
"low": [],
"none": [],
}
for value in results_data:
group_attribute = resolve_keys(group_by, value)
if isinstance(group_attribute, list):
if len(group_attribute):
for attrib in group_attribute:
if str(attrib) in response_dict:
response_dict[str(attrib)].append(value)
else:
response_dict[str(attrib)] = []
response_dict[str(attrib)].append(value)
else:
if str(None) in response_dict:
response_dict[str(None)].append(value)
else:
response_dict[str(None)] = []
response_dict[str(None)].append(value)
response_dict[str(None)] = []
response_dict[str(None)].append(value)
else:
if str(group_attribute) in response_dict:
response_dict[str(group_attribute)].append(value)
else:
if str(group_attribute) in response_dict:
response_dict[str(group_attribute)].append(value)
else:
response_dict[str(group_attribute)] = []
response_dict[str(group_attribute)].append(value)
response_dict[str(group_attribute)] = []
response_dict[str(group_attribute)].append(value)
return response_dict
return response_dict
+20 -4
View File
@@ -1,7 +1,6 @@
from django.utils.timezone import make_aware
from django.utils.dateparse import parse_datetime
def filter_state(params, filter, method):
if method == "GET":
states = params.get("state").split(",")
@@ -24,6 +23,7 @@ def filter_state_group(params, filter, method):
return filter
def filter_estimate_point(params, filter, method):
if method == "GET":
estimate_points = params.get("estimate_point").split(",")
@@ -39,10 +39,25 @@ def filter_priority(params, filter, method):
if method == "GET":
priorities = params.get("priority").split(",")
if len(priorities) and "" not in priorities:
filter["priority__in"] = priorities
if len(priorities) == 1 and "null" in priorities:
filter["priority__isnull"] = True
elif len(priorities) > 1 and "null" in priorities:
filter["priority__isnull"] = True
filter["priority__in"] = [p for p in priorities if p != "null"]
else:
filter["priority__in"] = [p for p in priorities if p != "null"]
else:
if params.get("priority", None) and len(params.get("priority")):
filter["priority__in"] = params.get("priority")
priorities = params.get("priority")
if len(priorities) == 1 and "null" in priorities:
filter["priority__isnull"] = True
elif len(priorities) > 1 and "null" in priorities:
filter["priority__isnull"] = True
filter["priority__in"] = [p for p in priorities if p != "null"]
else:
filter["priority__in"] = [p for p in priorities if p != "null"]
return filter
@@ -214,6 +229,7 @@ def filter_issue_state_type(params, filter, method):
return filter
def filter_project(params, filter, method):
if method == "GET":
projects = params.get("project").split(",")
@@ -313,7 +329,7 @@ def issue_filters(query_params, method):
"module": filter_module,
"inbox_status": filter_inbox_status,
"sub_issue": filter_sub_issue_toggle,
"subscriber": filter_subscribed_issues,
"subscriber": filter_subscribed_issues,
"start_target_date": filter_start_target_date_issues,
}
+13 -13
View File
@@ -1,36 +1,36 @@
# base requirements
Django==4.2.5
Django==4.2.3
django-braces==1.15.0
django-taggit==4.0.0
psycopg==3.1.10
psycopg==3.1.9
django-oauth-toolkit==2.3.0
mistune==3.0.1
djangorestframework==3.14.0
redis==4.6.0
django-nested-admin==4.0.2
django-cors-headers==4.2.0
django-cors-headers==4.1.0
whitenoise==6.5.0
django-allauth==0.55.2
django-allauth==0.54.0
faker==18.11.2
django-filter==23.2
jsonmodels==2.6.0
djangorestframework-simplejwt==5.3.0
sentry-sdk==1.30.0
djangorestframework-simplejwt==5.2.2
sentry-sdk==1.27.0
django-s3-storage==0.14.0
django-crum==0.7.9
django-guardian==2.4.0
dj_rest_auth==2.2.5
google-auth==2.22.0
google-api-python-client==2.97.0
google-auth==2.21.0
google-api-python-client==2.92.0
django-redis==5.3.0
uvicorn==0.23.2
uvicorn==0.22.0
channels==4.0.0
openai==0.28.0
openai==0.27.8
slack-sdk==3.21.3
celery==5.3.4
celery==5.3.1
django_celery_beat==2.5.0
psycopg-binary==3.1.10
psycopg-c==3.1.10
psycopg-binary==3.1.9
psycopg-c==3.1.9
scout-apm==2.26.1
openpyxl==3.1.2
+5 -5
View File
@@ -1,11 +1,11 @@
-r base.txt
dj-database-url==2.1.0
gunicorn==21.2.0
dj-database-url==2.0.0
gunicorn==20.1.0
whitenoise==6.5.0
django-storages==1.14
boto3==1.28.40
django-anymail==10.1
django-storages==1.13.2
boto3==1.27.0
django-anymail==10.0
django-debug-toolbar==4.1.0
gevent==23.7.0
psycogreen==1.0.2
@@ -1,4 +1,7 @@
module.exports = {
root: true,
extends: ["custom"],
rules: {
"@next/next/no-img-element": "off",
},
};
+1 -1
View File
@@ -1,5 +1,5 @@
{
"printWidth": 120,
"printWidth": 100,
"tabWidth": 2,
"trailingComma": "es5"
}
+70
View File
@@ -0,0 +1,70 @@
FROM node:18-alpine AS builder
RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
RUN yarn global add turbo
COPY . .
RUN turbo prune --scope=app --docker
# Add lockfile and package.json's of isolated subworkspace
FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat
WORKDIR /app
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
# First install the dependencies (as they change less often)
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/yarn.lock ./yarn.lock
RUN yarn install --network-timeout 500000
# Build the project
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
COPY replace-env-vars.sh /usr/local/bin/
USER root
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN yarn turbo run build --filter=app
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_DEPLOY_URL=${NEXT_PUBLIC_DEPLOY_URL}
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL} app
FROM node:18-alpine AS runner
WORKDIR /app
# Don't run production as root
RUN addgroup --system --gid 1001 plane
RUN adduser --system --uid 1001 captain
USER captain
COPY --from=installer /app/apps/app/next.config.js .
COPY --from=installer /app/apps/app/package.json .
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./
COPY --from=installer --chown=captain:plane /app/apps/app/.next ./apps/app/.next
ARG NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL \
BUILT_NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
USER root
COPY replace-env-vars.sh /usr/local/bin/
COPY start.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/replace-env-vars.sh
RUN chmod +x /usr/local/bin/start.sh
USER captain
ENV NEXT_TELEMETRY_DISABLED 1
EXPOSE 3000
@@ -9,6 +9,7 @@ import { findStringWithMostCharacters } from "helpers/array.helper";
import { generateBarColor } from "helpers/analytics.helper";
// types
import { IAnalyticsParams, IAnalyticsResponse } from "types";
// constants
type Props = {
analytics: IAnalyticsResponse;
@@ -7,14 +7,19 @@ import analyticsService from "services/analytics.service";
import projectService from "services/project.service";
import cyclesService from "services/cycles.service";
import modulesService from "services/modules.service";
import trackEventServices from "services/track_event.service";
import trackEventServices from "services/track-event.service";
// hooks
import useProjects from "hooks/use-projects";
import useToast from "hooks/use-toast";
// ui
import { PrimaryButton, SecondaryButton } from "components/ui";
// icons
import { ArrowDownTrayIcon, ArrowPathIcon, CalendarDaysIcon, UserGroupIcon } from "@heroicons/react/24/outline";
import {
ArrowDownTrayIcon,
ArrowPathIcon,
CalendarDaysIcon,
UserGroupIcon,
} from "@heroicons/react/24/outline";
import { ContrastIcon, LayerDiagonalIcon } from "components/icons";
// helpers
import { renderShortDate } from "helpers/date-time.helper";
@@ -41,7 +46,13 @@ type Props = {
user: ICurrentUserResponse | undefined;
};
export const AnalyticsSidebar: React.FC<Props> = ({ analytics, params, fullScreen, isProjectLevel = false, user }) => {
export const AnalyticsSidebar: React.FC<Props> = ({
analytics,
params,
fullScreen,
isProjectLevel = false,
user,
}) => {
const router = useRouter();
const { workspaceSlug, projectId, cycleId, moduleId } = router.query;
@@ -50,7 +61,9 @@ export const AnalyticsSidebar: React.FC<Props> = ({ analytics, params, fullScree
const { setToastAlert } = useToast();
const { data: projectDetails } = useSWR(
workspaceSlug && projectId && !(cycleId || moduleId) ? PROJECT_DETAILS(projectId.toString()) : null,
workspaceSlug && projectId && !(cycleId || moduleId)
? PROJECT_DETAILS(projectId.toString())
: null,
workspaceSlug && projectId && !(cycleId || moduleId)
? () => projectService.getProject(workspaceSlug.toString(), projectId.toString())
: null
@@ -59,14 +72,24 @@ export const AnalyticsSidebar: React.FC<Props> = ({ analytics, params, fullScree
const { data: cycleDetails } = useSWR(
workspaceSlug && projectId && cycleId ? CYCLE_DETAILS(cycleId.toString()) : null,
workspaceSlug && projectId && cycleId
? () => cyclesService.getCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
? () =>
cyclesService.getCycleDetails(
workspaceSlug.toString(),
projectId.toString(),
cycleId.toString()
)
: null
);
const { data: moduleDetails } = useSWR(
workspaceSlug && projectId && moduleId ? MODULE_DETAILS(moduleId.toString()) : null,
workspaceSlug && projectId && moduleId
? () => modulesService.getModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString())
? () =>
modulesService.getModuleDetails(
workspaceSlug.toString(),
projectId.toString(),
moduleId.toString()
)
: null
);
@@ -155,7 +178,8 @@ export const AnalyticsSidebar: React.FC<Props> = ({ analytics, params, fullScree
);
};
const selectedProjects = params.project && params.project.length > 0 ? params.project : projects?.map((p) => p.id);
const selectedProjects =
params.project && params.project.length > 0 ? params.project : projects?.map((p) => p.id);
return (
<div
@@ -212,7 +236,9 @@ export const AnalyticsSidebar: React.FC<Props> = ({ analytics, params, fullScree
)}
<h5 className="flex items-center gap-1">
<p className="break-words">{truncateText(project.name, 20)}</p>
<span className="text-custom-text-200 text-xs ml-1">({project.identifier})</span>
<span className="text-custom-text-200 text-xs ml-1">
({project.identifier})
</span>
</h5>
</div>
<div className="mt-4 space-y-3 pl-2 w-full">
@@ -318,7 +344,10 @@ export const AnalyticsSidebar: React.FC<Props> = ({ analytics, params, fullScree
<div className="space-y-4 mt-4">
<div className="flex items-center gap-2 text-xs">
<h6 className="text-custom-text-200">Network</h6>
<span>{NETWORK_CHOICES.find((n) => n.key === projectDetails?.network)?.label ?? ""}</span>
<span>
{NETWORK_CHOICES.find((n) => n.key === projectDetails?.network)?.label ??
""}
</span>
</div>
</div>
</div>
@@ -1,13 +1,13 @@
// nivo
import { BarDatum } from "@nivo/bar";
// icons
import { PriorityIcon } from "components/icons";
import { getPriorityIcon } from "components/icons";
// helpers
import { addSpaceIfCamelCase } from "helpers/string.helper";
// helpers
import { generateBarColor, renderMonthAndYear } from "helpers/analytics.helper";
// types
import { IAnalyticsParams, IAnalyticsResponse, TIssuePriorities } from "types";
import { IAnalyticsParams, IAnalyticsResponse } from "types";
// constants
import { ANALYTICS_X_AXIS_VALUES, ANALYTICS_Y_AXIS_VALUES, DATE_KEYS } from "constants/analytics";
@@ -53,7 +53,7 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
>
<div className="flex items-center gap-2">
{params.segment === "priority" ? (
<PriorityIcon priority={key as TIssuePriorities} />
getPriorityIcon(key)
) : (
<span
className="h-3 w-3 flex-shrink-0 rounded"
@@ -91,7 +91,7 @@ export const AnalyticsTable: React.FC<Props> = ({ analytics, barGraphData, param
}`}
>
{params.x_axis === "priority" ? (
<PriorityIcon priority={item.name as TIssuePriorities} />
getPriorityIcon(`${item.name}`)
) : (
<span
className="h-3 w-3 rounded"
@@ -13,11 +13,15 @@ import analyticsService from "services/analytics.service";
import projectService from "services/project.service";
import cyclesService from "services/cycles.service";
import modulesService from "services/modules.service";
import trackEventServices from "services/track_event.service";
import trackEventServices from "services/track-event.service";
// components
import { CustomAnalytics, ScopeAndDemand } from "components/analytics";
// icons
import { ArrowsPointingInIcon, ArrowsPointingOutIcon, XMarkIcon } from "@heroicons/react/24/outline";
import {
ArrowsPointingInIcon,
ArrowsPointingOutIcon,
XMarkIcon,
} from "@heroicons/react/24/outline";
// types
import { IAnalyticsParams, IWorkspace } from "types";
// fetch-keys
@@ -63,7 +67,9 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
);
const { data: projectDetails } = useSWR(
workspaceSlug && projectId && !(cycleId || moduleId) ? PROJECT_DETAILS(projectId.toString()) : null,
workspaceSlug && projectId && !(cycleId || moduleId)
? PROJECT_DETAILS(projectId.toString())
: null,
workspaceSlug && projectId && !(cycleId || moduleId)
? () => projectService.getProject(workspaceSlug.toString(), projectId.toString())
: null
@@ -72,14 +78,24 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
const { data: cycleDetails } = useSWR(
workspaceSlug && projectId && cycleId ? CYCLE_DETAILS(cycleId.toString()) : null,
workspaceSlug && projectId && cycleId
? () => cyclesService.getCycleDetails(workspaceSlug.toString(), projectId.toString(), cycleId.toString())
? () =>
cyclesService.getCycleDetails(
workspaceSlug.toString(),
projectId.toString(),
cycleId.toString()
)
: null
);
const { data: moduleDetails } = useSWR(
workspaceSlug && projectId && moduleId ? MODULE_DETAILS(moduleId.toString()) : null,
workspaceSlug && projectId && moduleId
? () => modulesService.getModuleDetails(workspaceSlug.toString(), projectId.toString(), moduleId.toString())
? () =>
modulesService.getModuleDetails(
workspaceSlug.toString(),
projectId.toString(),
moduleId.toString()
)
: null
);
@@ -118,7 +134,8 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
eventPayload.moduleName = moduleDetails.name;
}
const eventType = tab === "Scope and Demand" ? "SCOPE_AND_DEMAND_ANALYTICS" : "CUSTOM_ANALYTICS";
const eventType =
tab === "Scope and Demand" ? "SCOPE_AND_DEMAND_ANALYTICS" : "CUSTOM_ANALYTICS";
trackEventServices.trackAnalyticsEvent(
eventPayload,
@@ -133,9 +150,9 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
return (
<div
className={`absolute top-0 z-30 h-full bg-custom-background-90 ${fullScreen ? "p-2 w-full" : "w-1/2"} ${
isOpen ? "right-0" : "-right-full"
} duration-300 transition-all`}
className={`absolute top-0 z-30 h-full bg-custom-background-90 ${
fullScreen ? "p-2 w-full" : "w-1/2"
} ${isOpen ? "right-0" : "-right-full"} duration-300 transition-all`}
>
<div
className={`flex h-full flex-col overflow-hidden border-custom-border-200 bg-custom-background-100 text-left ${
@@ -144,7 +161,8 @@ export const AnalyticsProjectModal: React.FC<Props> = ({ isOpen, onClose }) => {
>
<div className="flex items-center justify-between gap-4 bg-custom-background-100 px-5 py-4 text-sm">
<h3 className="break-words">
Analytics for {cycleId ? cycleDetails?.name : moduleId ? moduleDetails?.name : projectDetails?.name}
Analytics for{" "}
{cycleId ? cycleDetails?.name : moduleId ? moduleDetails?.name : projectDetails?.name}
</h3>
<div className="flex items-center gap-2">
<button
@@ -1,7 +1,7 @@
// icons
import { PlayIcon } from "@heroicons/react/24/outline";
// types
import { IDefaultAnalyticsResponse, TStateGroups } from "types";
import { IDefaultAnalyticsResponse } from "types";
// constants
import { STATE_GROUP_COLORS } from "constants/state";
@@ -27,7 +27,7 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
<span
className="h-2 w-2 rounded-full"
style={{
backgroundColor: STATE_GROUP_COLORS[group.state_group as TStateGroups],
backgroundColor: STATE_GROUP_COLORS[group.state_group],
}}
/>
<h6 className="capitalize">{group.state_group}</h6>
@@ -42,7 +42,7 @@ export const AnalyticsDemand: React.FC<Props> = ({ defaultAnalytics }) => (
className="absolute top-0 left-0 h-1 rounded duration-300"
style={{
width: `${percentage}%`,
backgroundColor: STATE_GROUP_COLORS[group.state_group as TStateGroups],
backgroundColor: STATE_GROUP_COLORS[group.state_group],
}}
/>
</div>
@@ -20,15 +20,18 @@ export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) =
{
id: "issues_closed",
color: "rgb(var(--color-primary-100))",
data: Object.entries(MONTHS_LIST).map(([index, month]) => ({
x: month.shortTitle,
data: MONTHS_LIST.map((month) => ({
x: month.label.substring(0, 3),
y:
defaultAnalytics.issue_completed_month_wise.find((data) => data.month === parseInt(index, 10))?.count ||
0,
defaultAnalytics.issue_completed_month_wise.find(
(data) => data.month === month.value
)?.count || 0,
})),
},
]}
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map((data) => data.count)}
customYAxisTickValues={defaultAnalytics.issue_completed_month_wise.map(
(data) => data.count
)}
height="300px"
colors={(datum) => datum.color}
curve="monotoneX"
@@ -0,0 +1,90 @@
import React, { useState } from "react";
// component
import { CustomSelect, ToggleSwitch } from "components/ui";
import { SelectMonthModal } from "components/automation";
// icons
import { ChevronDownIcon } from "@heroicons/react/24/outline";
// constants
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
// types
import { IProject } from "types";
type Props = {
projectDetails: IProject | undefined;
handleChange: (formData: Partial<IProject>) => Promise<void>;
};
export const AutoArchiveAutomation: React.FC<Props> = ({ projectDetails, handleChange }) => {
const [monthModal, setmonthModal] = useState(false);
const initialValues: Partial<IProject> = { archive_in: 1 };
return (
<>
<SelectMonthModal
type="auto-archive"
initialValues={initialValues}
isOpen={monthModal}
handleClose={() => setmonthModal(false)}
handleChange={handleChange}
/>
<div className="flex flex-col gap-7 px-6 py-5 rounded-[10px] border border-custom-border-300 bg-custom-background-90">
<div className="flex items-center justify-between gap-x-8 gap-y-2">
<div className="flex flex-col gap-2.5">
<h4 className="text-lg font-semibold">Auto-archive closed issues</h4>
<p className="text-sm text-custom-text-200">
Plane will automatically archive issues that have been completed or cancelled for the
configured time period.
</p>
</div>
<ToggleSwitch
value={projectDetails?.archive_in !== 0}
onChange={() =>
projectDetails?.archive_in === 0
? handleChange({ archive_in: 1 })
: handleChange({ archive_in: 0 })
}
size="sm"
/>
</div>
{projectDetails?.archive_in !== 0 && (
<div className="flex items-center justify-between gap-2 w-full">
<div className="w-1/2 text-base font-medium">
Auto-archive issues that are closed for
</div>
<div className="w-1/2">
<CustomSelect
value={projectDetails?.archive_in}
label={`${projectDetails?.archive_in} ${
projectDetails?.archive_in === 1 ? "Month" : "Months"
}`}
onChange={(val: number) => {
handleChange({ archive_in: val });
}}
input
verticalPosition="top"
width="w-full"
>
<>
{PROJECT_AUTOMATION_MONTHS.map((month) => (
<CustomSelect.Option key={month.label} value={month.value}>
{month.label}
</CustomSelect.Option>
))}
<button
type="button"
className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
onClick={() => setmonthModal(true)}
>
Customize Time Range
</button>
</>
</CustomSelect>
</div>
</div>
)}
</div>
</>
);
};
@@ -0,0 +1,176 @@
import React, { useState } from "react";
import useSWR from "swr";
import { useRouter } from "next/router";
// component
import { CustomSearchSelect, CustomSelect, ToggleSwitch } from "components/ui";
import { SelectMonthModal } from "components/automation";
// icons
import { ChevronDownIcon, Squares2X2Icon } from "@heroicons/react/24/outline";
import { getStateGroupIcon } from "components/icons";
// services
import stateService from "services/state.service";
// constants
import { PROJECT_AUTOMATION_MONTHS } from "constants/project";
import { STATES_LIST } from "constants/fetch-keys";
// types
import { IProject } from "types";
// helper
import { getStatesList } from "helpers/state.helper";
type Props = {
projectDetails: IProject | undefined;
handleChange: (formData: Partial<IProject>) => Promise<void>;
};
export const AutoCloseAutomation: React.FC<Props> = ({ projectDetails, handleChange }) => {
const [monthModal, setmonthModal] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId } = router.query;
const { data: stateGroups } = useSWR(
workspaceSlug && projectId ? STATES_LIST(projectId as string) : null,
workspaceSlug && projectId
? () => stateService.getStates(workspaceSlug as string, projectId as string)
: null
);
const states = getStatesList(stateGroups);
const options = states
?.filter((state) => state.group === "cancelled")
.map((state) => ({
value: state.id,
query: state.name,
content: (
<div className="flex items-center gap-2">
{getStateGroupIcon(state.group, "16", "16", state.color)}
{state.name}
</div>
),
}));
const multipleOptions = (options ?? []).length > 1;
const defaultState = stateGroups && stateGroups.cancelled ? stateGroups.cancelled[0].id : null;
const selectedOption = states?.find(
(s) => s.id === projectDetails?.default_state ?? defaultState
);
const currentDefaultState = states?.find((s) => s.id === defaultState);
const initialValues: Partial<IProject> = {
close_in: 1,
default_state: defaultState,
};
return (
<>
<SelectMonthModal
type="auto-close"
initialValues={initialValues}
isOpen={monthModal}
handleClose={() => setmonthModal(false)}
handleChange={handleChange}
/>
<div className="flex flex-col gap-7 px-6 py-5 rounded-[10px] border border-custom-border-300 bg-custom-background-90">
<div className="flex items-center justify-between gap-x-8 gap-y-2 ">
<div className="flex flex-col gap-2.5">
<h4 className="text-lg font-semibold">Auto-close inactive issues</h4>
<p className="text-sm text-custom-text-200">
Plane will automatically close the issues that have not been updated for the
configured time period.
</p>
</div>
<ToggleSwitch
value={projectDetails?.close_in !== 0}
onChange={() =>
projectDetails?.close_in === 0
? handleChange({ close_in: 1, default_state: defaultState })
: handleChange({ close_in: 0, default_state: null })
}
size="sm"
/>
</div>
{projectDetails?.close_in !== 0 && (
<div className="flex flex-col gap-4 w-full">
<div className="flex items-center justify-between gap-2 w-full">
<div className="w-1/2 text-base font-medium">
Auto-close issues that are inactive for
</div>
<div className="w-1/2">
<CustomSelect
value={projectDetails?.close_in}
label={`${projectDetails?.close_in} ${
projectDetails?.close_in === 1 ? "Month" : "Months"
}`}
onChange={(val: number) => {
handleChange({ close_in: val });
}}
input
width="w-full"
>
<>
{PROJECT_AUTOMATION_MONTHS.map((month) => (
<CustomSelect.Option key={month.label} value={month.value}>
{month.label}
</CustomSelect.Option>
))}
<button
type="button"
className="flex w-full select-none items-center rounded px-1 py-1.5 text-custom-text-200 hover:bg-custom-background-80"
onClick={() => setmonthModal(true)}
>
Customize Time Range
</button>
</>
</CustomSelect>
</div>
</div>
<div className="flex items-center justify-between gap-2 w-full">
<div className="w-1/2 text-base font-medium">Auto-close Status</div>
<div className="w-1/2 ">
<CustomSearchSelect
value={
projectDetails?.default_state ? projectDetails?.default_state : defaultState
}
label={
<div className="flex items-center gap-2">
{selectedOption ? (
getStateGroupIcon(selectedOption.group, "16", "16", selectedOption.color)
) : currentDefaultState ? (
getStateGroupIcon(
currentDefaultState.group,
"16",
"16",
currentDefaultState.color
)
) : (
<Squares2X2Icon className="h-3.5 w-3.5 text-custom-text-200" />
)}
{selectedOption?.name
? selectedOption.name
: currentDefaultState?.name ?? (
<span className="text-custom-text-200">State</span>
)}
</div>
}
onChange={(val: string) => {
handleChange({ default_state: val });
}}
options={options}
disabled={!multipleOptions}
width="w-full"
input
/>
</div>
</div>
</div>
)}
</div>
</>
);
};
@@ -104,7 +104,7 @@ export const SelectMonthModal: React.FC<Props> = ({
as="h3"
className="text-lg font-medium leading-6 text-custom-text-100"
>
Customise Time Range
Customize Time Range
</Dialog.Title>
<div className="mt-8 flex items-center gap-2">
<div className="flex w-full flex-col gap-1 justify-center">
@@ -10,7 +10,7 @@ import { Command } from "cmdk";
import { Dialog, Transition } from "@headlessui/react";
// services
import workspaceService from "services/workspace.service";
import issuesService from "services/issue.service";
import issuesService from "services/issues.service";
import inboxService from "services/inbox.service";
// hooks
import useProjectDetails from "hooks/use-project-details";
@@ -79,13 +79,16 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
const { data: issueDetails } = useSWR(
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
workspaceSlug && projectId && issueId
? () => issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
? () =>
issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
: null
);
const { data: inboxList } = useSWR(
workspaceSlug && projectId ? INBOX_LIST(projectId as string) : null,
workspaceSlug && projectId ? () => inboxService.getInboxes(workspaceSlug as string, projectId as string) : null
workspaceSlug && projectId
? () => inboxService.getInboxes(workspaceSlug as string, projectId as string)
: null
);
const updateIssue = useCallback(
@@ -269,7 +272,8 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
>
{issueDetails && (
<div className="overflow-hidden truncate rounded-md bg-custom-background-80 p-2 text-xs font-medium text-custom-text-200">
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id} {issueDetails.name}
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}{" "}
{issueDetails.name}
</div>
)}
{projectId && (
@@ -320,9 +324,12 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
</h5>
)}
{!isLoading && resultsCount === 0 && searchTerm !== "" && debouncedSearchTerm !== "" && (
<div className="my-4 text-center text-custom-text-200">No results found.</div>
)}
{!isLoading &&
resultsCount === 0 &&
searchTerm !== "" &&
debouncedSearchTerm !== "" && (
<div className="my-4 text-center text-custom-text-200">No results found.</div>
)}
{(isLoading || isSearching) && (
<Command.Loading>
@@ -355,7 +362,9 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
>
<div className="flex items-center gap-2 overflow-hidden text-custom-text-200">
<Icon iconName={currentSection.icon} />
<p className="block flex-1 truncate">{currentSection.itemName(item)}</p>
<p className="block flex-1 truncate">
{currentSection.itemName(item)}
</p>
</div>
</Command.Item>
))}
@@ -568,7 +577,9 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
redirect(`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`);
redirect(
`/${workspaceSlug}/projects/${projectId}/inbox/${inboxList?.[0]?.id}`
);
}}
className="focus:outline-none"
>
@@ -654,14 +665,17 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
className="focus:outline-none"
>
<div className="flex items-center gap-2 text-custom-text-200">
<DiscordIcon className="h-4 w-4" color="rgb(var(--color-text-200))" />
<DiscordIcon className="h-4 w-4" color="#6b7280" />
Join our Discord
</div>
</Command.Item>
<Command.Item
onSelect={() => {
setIsPaletteOpen(false);
window.open("https://github.com/makeplane/plane/issues/new/choose", "_blank");
window.open(
"https://github.com/makeplane/plane/issues/new/choose",
"_blank"
);
}}
className="focus:outline-none"
>
@@ -745,15 +759,29 @@ export const CommandK: React.FC<Props> = ({ deleteIssue, isPaletteOpen, setIsPal
</>
)}
{page === "change-issue-state" && issueDetails && (
<ChangeIssueState issue={issueDetails} setIsPaletteOpen={setIsPaletteOpen} user={user} />
<ChangeIssueState
issue={issueDetails}
setIsPaletteOpen={setIsPaletteOpen}
user={user}
/>
)}
{page === "change-issue-priority" && issueDetails && (
<ChangeIssuePriority issue={issueDetails} setIsPaletteOpen={setIsPaletteOpen} user={user} />
<ChangeIssuePriority
issue={issueDetails}
setIsPaletteOpen={setIsPaletteOpen}
user={user}
/>
)}
{page === "change-issue-assignee" && issueDetails && (
<ChangeIssueAssignee issue={issueDetails} setIsPaletteOpen={setIsPaletteOpen} user={user} />
<ChangeIssueAssignee
issue={issueDetails}
setIsPaletteOpen={setIsPaletteOpen}
user={user}
/>
)}
{page === "change-interface-theme" && (
<ChangeInterfaceTheme setIsPaletteOpen={setIsPaletteOpen} />
)}
{page === "change-interface-theme" && <ChangeInterfaceTheme setIsPaletteOpen={setIsPaletteOpen} />}
</Command.List>
</Command>
</Dialog.Panel>
@@ -1,7 +1,7 @@
import React, { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/router";
import useSWR from "swr";
import { observer } from "mobx-react-lite";
// hooks
import useToast from "hooks/use-toast";
import useUser from "hooks/use-user";
@@ -17,11 +17,14 @@ import { CreateUpdatePageModal } from "components/pages";
// helpers
import { copyTextToClipboard } from "helpers/string.helper";
// services
import issuesService from "services/issue.service";
import issuesService from "services/issues.service";
import inboxService from "services/inbox.service";
// fetch keys
import { ISSUE_DETAILS } from "constants/fetch-keys";
import { INBOX_LIST, ISSUE_DETAILS } from "constants/fetch-keys";
// mobx store
import { useMobxStore } from "lib/mobx/store-provider";
import { observable } from "mobx";
import { observer } from "mobx-react-lite";
export const CommandPalette: React.FC = observer(() => {
const store: any = useMobxStore();
@@ -38,7 +41,7 @@ export const CommandPalette: React.FC = observer(() => {
const [isCreateUpdatePageModalOpen, setIsCreateUpdatePageModalOpen] = useState(false);
const router = useRouter();
const { workspaceSlug, projectId, issueId, inboxId, cycleId, moduleId } = router.query;
const { workspaceSlug, projectId, issueId, inboxId } = router.query;
const { user } = useUser();
@@ -47,7 +50,8 @@ export const CommandPalette: React.FC = observer(() => {
const { data: issueDetails } = useSWR(
workspaceSlug && projectId && issueId ? ISSUE_DETAILS(issueId as string) : null,
workspaceSlug && projectId && issueId
? () => issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
? () =>
issuesService.retrieve(workspaceSlug as string, projectId as string, issueId as string)
: null
);
@@ -72,7 +76,7 @@ export const CommandPalette: React.FC = observer(() => {
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
const { key, ctrlKey, metaKey, altKey } = e;
const { key, ctrlKey, metaKey, altKey, shiftKey } = e;
if (!key) return;
const keyPressed = key.toLowerCase();
@@ -85,37 +89,37 @@ export const CommandPalette: React.FC = observer(() => {
)
return;
if (cmdClicked) {
if (keyPressed === "k") {
e.preventDefault();
setIsPaletteOpen(true);
} else if (keyPressed === "c" && altKey) {
e.preventDefault();
copyIssueUrlToClipboard();
} else if (keyPressed === "b") {
e.preventDefault();
store.theme.setSidebarCollapsed(!store?.theme?.sidebarCollapsed);
if (cmdClicked) {
if (keyPressed === "k") {
e.preventDefault();
setIsPaletteOpen(true);
} else if (keyPressed === "c" && altKey) {
e.preventDefault();
copyIssueUrlToClipboard();
} else if (keyPressed === "b") {
e.preventDefault();
store.theme.setSidebarCollapsed(!store?.theme?.sidebarCollapsed);
}
} else {
if (keyPressed === "c") {
setIsIssueModalOpen(true);
} else if (keyPressed === "p") {
setIsProjectModalOpen(true);
} else if (keyPressed === "v") {
setIsCreateViewModalOpen(true);
} else if (keyPressed === "d") {
setIsCreateUpdatePageModalOpen(true);
} else if (keyPressed === "h") {
setIsShortcutsModalOpen(true);
} else if (keyPressed === "q") {
setIsCreateCycleModalOpen(true);
} else if (keyPressed === "m") {
setIsCreateModuleModalOpen(true);
} else if (keyPressed === "backspace" || keyPressed === "delete") {
e.preventDefault();
setIsBulkDeleteIssuesModalOpen(true);
}
}
} else {
if (keyPressed === "c") {
setIsIssueModalOpen(true);
} else if (keyPressed === "p") {
setIsProjectModalOpen(true);
} else if (keyPressed === "v") {
setIsCreateViewModalOpen(true);
} else if (keyPressed === "d") {
setIsCreateUpdatePageModalOpen(true);
} else if (keyPressed === "h") {
setIsShortcutsModalOpen(true);
} else if (keyPressed === "q") {
setIsCreateCycleModalOpen(true);
} else if (keyPressed === "m") {
setIsCreateModuleModalOpen(true);
} else if (keyPressed === "backspace" || keyPressed === "delete") {
e.preventDefault();
setIsBulkDeleteIssuesModalOpen(true);
}
}
},
[copyIssueUrlToClipboard]
);
@@ -137,7 +141,11 @@ export const CommandPalette: React.FC = observer(() => {
<>
<ShortcutsModal isOpen={isShortcutsModalOpen} setIsOpen={setIsShortcutsModalOpen} />
{workspaceSlug && (
<CreateProjectModal isOpen={isProjectModalOpen} setIsOpen={setIsProjectModalOpen} user={user} />
<CreateProjectModal
isOpen={isProjectModalOpen}
setIsOpen={setIsProjectModalOpen}
user={user}
/>
)}
{projectId && (
<>
@@ -175,16 +183,17 @@ export const CommandPalette: React.FC = observer(() => {
isOpen={isIssueModalOpen}
handleClose={() => setIsIssueModalOpen(false)}
fieldsToShow={inboxId ? ["name", "description", "priority"] : ["all"]}
prePopulateData={
cycleId ? { cycle: cycleId.toString() } : moduleId ? { module: moduleId.toString() } : undefined
}
/>
<BulkDeleteIssuesModal
isOpen={isBulkDeleteIssuesModalOpen}
setIsOpen={setIsBulkDeleteIssuesModalOpen}
user={user}
/>
<CommandK deleteIssue={deleteIssue} isPaletteOpen={isPaletteOpen} setIsPaletteOpen={setIsPaletteOpen} />
<CommandK
deleteIssue={deleteIssue}
isPaletteOpen={isPaletteOpen}
setIsPaletteOpen={setIsPaletteOpen}
/>
</>
);
});
})
@@ -7,7 +7,7 @@ import { mutate } from "swr";
// cmdk
import { Command } from "cmdk";
// services
import issuesService from "services/issue.service";
import issuesService from "services/issues.service";
// hooks
import useProjectMembers from "hooks/use-project-members";
// constants
@@ -1,20 +1,18 @@
import React, { Dispatch, SetStateAction, useCallback } from "react";
import { useRouter } from "next/router";
import React, { Dispatch, SetStateAction, useCallback } from "react";
import { mutate } from "swr";
// cmdk
import { Command } from "cmdk";
// services
import issuesService from "services/issue.service";
import issuesService from "services/issues.service";
// types
import { ICurrentUserResponse, IIssue, TIssuePriorities } from "types";
import { ICurrentUserResponse, IIssue } from "types";
// constants
import { ISSUE_DETAILS, PROJECT_ISSUES_ACTIVITY } from "constants/fetch-keys";
import { PRIORITIES } from "constants/project";
// icons
import { CheckIcon, PriorityIcon } from "components/icons";
import { CheckIcon, getPriorityIcon } from "components/icons";
type Props = {
setIsPaletteOpen: Dispatch<SetStateAction<boolean>>;
@@ -56,7 +54,7 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue,
[workspaceSlug, issueId, projectId, user]
);
const handleIssueState = (priority: TIssuePriorities) => {
const handleIssueState = (priority: string | null) => {
submitChanges({ priority });
setIsPaletteOpen(false);
};
@@ -64,9 +62,13 @@ export const ChangeIssuePriority: React.FC<Props> = ({ setIsPaletteOpen, issue,
return (
<>
{PRIORITIES.map((priority) => (
<Command.Item key={priority} onSelect={() => handleIssueState(priority)} className="focus:outline-none">
<Command.Item
key={priority}
onSelect={() => handleIssueState(priority)}
className="focus:outline-none"
>
<div className="flex items-center space-x-3">
<PriorityIcon priority={priority} />
{getPriorityIcon(priority)}
<span className="capitalize">{priority ?? "None"}</span>
</div>
<div>{priority === issue.priority && <CheckIcon className="h-3 w-3" />}</div>

Some files were not shown because too many files have changed in this diff Show More