Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b9975dfa24 | |||
| 380ad340a6 |
@@ -1,3 +1,34 @@
|
||||
# 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=""
|
||||
|
||||
# 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 +41,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 +65,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 +76,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
@@ -4,7 +4,7 @@ module.exports = {
|
||||
extends: ["custom"],
|
||||
settings: {
|
||||
next: {
|
||||
rootDir: ["web/", "space/"],
|
||||
rootDir: ["apps/*"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
name: Build Pull Request Contents
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: ["opened", "synchronize"]
|
||||
|
||||
jobs:
|
||||
build-pull-request-contents:
|
||||
name: Build Pull Request Contents
|
||||
runs-on: ubuntu-20.04
|
||||
permissions:
|
||||
pull-requests: read
|
||||
|
||||
steps:
|
||||
- name: Checkout Repository to Actions
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Setup Node.js 18.x
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 18.x
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v38
|
||||
with:
|
||||
files_yaml: |
|
||||
apiserver:
|
||||
- apiserver/**
|
||||
web:
|
||||
- web/**
|
||||
deploy:
|
||||
- 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: |
|
||||
mv ./.npmrc ./web
|
||||
cd web
|
||||
yarn
|
||||
yarn build
|
||||
|
||||
- name: Build Plane's Deploy App
|
||||
if: steps.changed-files.outputs.deploy_any_changed == 'true'
|
||||
run: |
|
||||
cd space
|
||||
yarn
|
||||
yarn build
|
||||
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
name: Update Docker Images for Plane on Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [released, prereleased]
|
||||
|
||||
jobs:
|
||||
build_push_backend:
|
||||
name: Build and Push Api Server Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
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
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-frontend
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||
id: metaBackend
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-backend
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||
id: metaDeploy
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-deploy
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub) from Github Release
|
||||
id: metaProxy
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ${{ secrets.DOCKERHUB_USERNAME }}/plane-proxy
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
|
||||
- name: Build and Push Frontend to Docker Container Registry
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./web/Dockerfile.web
|
||||
platforms: linux/amd64
|
||||
tags: ${{ steps.metaFrontend.outputs.tags }}
|
||||
push: true
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Backend to Docker Hub
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: ./apiserver
|
||||
file: ./apiserver/Dockerfile.api
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.metaBackend.outputs.tags }}
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Plane-Deploy to Docker Hub
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./space/Dockerfile.space
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.metaDeploy.outputs.tags }}
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Build and Push Plane-Proxy to Docker Hub
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: ./nginx
|
||||
file: ./nginx/Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.metaProxy.outputs.tags }}
|
||||
env:
|
||||
DOCKER_BUILDKIT: 1
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
DOCKET_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
@@ -0,0 +1,77 @@
|
||||
name: Build and Push Backend Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'develop'
|
||||
- 'master'
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build_push_backend:
|
||||
name: Build and Push Api Server Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
with:
|
||||
platforms: linux/arm64,linux/amd64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
registry: "registry.hub.docker.com"
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub)
|
||||
id: ghmeta
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: makeplane/plane-backend
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Github)
|
||||
id: dkrmeta
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}-backend
|
||||
|
||||
- name: Build and Push to GitHub Container Registry
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: ./apiserver
|
||||
file: ./apiserver/Dockerfile.api
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha
|
||||
tags: ${{ steps.ghmeta.outputs.tags }}
|
||||
labels: ${{ steps.ghmeta.outputs.labels }}
|
||||
|
||||
- name: Build and Push to Docker Hub
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: ./apiserver
|
||||
file: ./apiserver/Dockerfile.api
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha
|
||||
tags: ${{ steps.dkrmeta.outputs.tags }}
|
||||
labels: ${{ steps.dkrmeta.outputs.labels }}
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
name: Build and Push Frontend Docker Image
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'develop'
|
||||
- 'master'
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
jobs:
|
||||
build_push_frontend:
|
||||
name: Build Frontend Docker Image
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v3.3.0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2.1.0
|
||||
with:
|
||||
platforms: linux/arm64,linux/amd64
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2.5.0
|
||||
|
||||
- name: Login to Github Container Registry
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
registry: "ghcr.io"
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v2.1.0
|
||||
with:
|
||||
registry: "registry.hub.docker.com"
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Docker Hub)
|
||||
id: ghmeta
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: makeplane/plane-frontend
|
||||
|
||||
- name: Extract metadata (tags, labels) for Docker (Github)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4.3.0
|
||||
with:
|
||||
images: ghcr.io/${{ github.repository }}-frontend
|
||||
|
||||
- name: Build and Push to GitHub Container Registry
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./apps/app/Dockerfile.web
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha
|
||||
tags: ${{ steps.ghmeta.outputs.tags }}
|
||||
labels: ${{ steps.ghmeta.outputs.labels }}
|
||||
|
||||
- name: Build and Push to Docker Container Registry
|
||||
uses: docker/build-push-action@v4.0.0
|
||||
with:
|
||||
context: .
|
||||
file: ./apps/app/Dockerfile.web
|
||||
platforms: linux/arm64,linux/amd64
|
||||
push: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha
|
||||
tags: ${{ steps.dkrmeta.outputs.tags }}
|
||||
labels: ${{ steps.dkrmeta.outputs.labels }}
|
||||
|
||||
+1
-3
@@ -70,6 +70,4 @@ package-lock.json
|
||||
# lock files
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
pnpm-workspace.yaml
|
||||
|
||||
.npmrc
|
||||
pnpm-workspace.yaml
|
||||
+12
-12
@@ -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.
|
||||
+3
-10
@@ -5,11 +5,9 @@ WORKDIR /app
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER
|
||||
|
||||
RUN yarn global add turbo
|
||||
RUN apk add tree
|
||||
COPY . .
|
||||
|
||||
RUN turbo prune --scope=app --scope=plane-deploy --docker
|
||||
CMD tree -I node_modules/
|
||||
RUN turbo prune --scope=app --docker
|
||||
|
||||
# Add lockfile and package.json's of isolated subworkspace
|
||||
FROM node:18-alpine AS installer
|
||||
@@ -23,14 +21,14 @@ COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/yarn.lock ./yarn.lock
|
||||
RUN yarn install
|
||||
|
||||
# # Build the project
|
||||
# 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
|
||||
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
|
||||
@@ -98,16 +96,11 @@ RUN adduser --system --uid 1001 captain
|
||||
|
||||
COPY --from=installer /app/apps/app/next.config.js .
|
||||
COPY --from=installer /app/apps/app/package.json .
|
||||
COPY --from=installer /app/apps/space/next.config.js .
|
||||
COPY --from=installer /app/apps/space/package.json .
|
||||
|
||||
COPY --from=installer --chown=captain:plane /app/apps/app/.next/standalone ./
|
||||
|
||||
COPY --from=installer --chown=captain:plane /app/apps/app/.next/static ./apps/app/.next/static
|
||||
|
||||
COPY --from=installer --chown=captain:plane /app/apps/space/.next/standalone ./
|
||||
COPY --from=installer --chown=captain:plane /app/apps/space/.next ./apps/space/.next
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED 1
|
||||
|
||||
# RUN rm /etc/nginx/conf.d/default.conf
|
||||
|
||||
@@ -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,41 +56,30 @@ 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
|
||||
|
||||
@@ -149,6 +140,7 @@ docker compose up -d
|
||||
</p>
|
||||
</p>
|
||||
|
||||
|
||||
## 📚Documentation
|
||||
|
||||
For full documentation, visit [docs.plane.so](https://docs.plane.so/)
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
# 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"
|
||||
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
@@ -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
|
||||
@@ -18,9 +18,6 @@ from .project import (
|
||||
ProjectFavoriteSerializer,
|
||||
ProjectLiteSerializer,
|
||||
ProjectMemberLiteSerializer,
|
||||
ProjectDeployBoardSerializer,
|
||||
ProjectMemberAdminSerializer,
|
||||
ProjectPublicMemberSerializer
|
||||
)
|
||||
from .state import StateSerializer, StateLiteSerializer
|
||||
from .view import IssueViewSerializer, IssueViewFavoriteSerializer
|
||||
@@ -31,6 +28,8 @@ from .issue import (
|
||||
IssueActivitySerializer,
|
||||
IssueCommentSerializer,
|
||||
IssuePropertySerializer,
|
||||
BlockerIssueSerializer,
|
||||
BlockedIssueSerializer,
|
||||
IssueAssigneeSerializer,
|
||||
LabelSerializer,
|
||||
IssueSerializer,
|
||||
@@ -42,10 +41,6 @@ from .issue import (
|
||||
IssueSubscriberSerializer,
|
||||
IssueReactionSerializer,
|
||||
CommentReactionSerializer,
|
||||
IssueVoteSerializer,
|
||||
IssueRelationSerializer,
|
||||
RelatedIssueSerializer,
|
||||
IssuePublicSerializer,
|
||||
)
|
||||
|
||||
from .module import (
|
||||
@@ -83,5 +78,3 @@ from .inbox import InboxSerializer, InboxIssueSerializer, IssueStateInboxSeriali
|
||||
from .analytic import AnalyticViewSerializer
|
||||
|
||||
from .notification import NotificationSerializer
|
||||
|
||||
from .exporter import ExporterHistorySerializer
|
||||
|
||||
@@ -14,11 +14,6 @@ from plane.db.models import Cycle, CycleIssue, CycleFavorite
|
||||
|
||||
class CycleWriteSerializer(BaseSerializer):
|
||||
|
||||
def validate(self, data):
|
||||
if data.get("start_date", None) is not None and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None):
|
||||
raise serializers.ValidationError("Start date cannot exceed end date")
|
||||
return data
|
||||
|
||||
class Meta:
|
||||
model = Cycle
|
||||
fields = "__all__"
|
||||
@@ -40,18 +35,12 @@ class CycleSerializer(BaseSerializer):
|
||||
started_estimates = serializers.IntegerField(read_only=True)
|
||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||
project_detail = ProjectLiteSerializer(read_only=True, source="project")
|
||||
|
||||
def validate(self, data):
|
||||
if data.get("start_date", None) is not None and data.get("end_date", None) is not None and data.get("start_date", None) > data.get("end_date", None):
|
||||
raise serializers.ValidationError("Start date cannot exceed end date")
|
||||
return data
|
||||
|
||||
def get_assignees(self, obj):
|
||||
members = [
|
||||
{
|
||||
"avatar": assignee.avatar,
|
||||
"first_name": assignee.first_name,
|
||||
"display_name": assignee.display_name,
|
||||
"id": assignee.id,
|
||||
}
|
||||
for issue_cycle in obj.issue_cycle.all()
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# Module imports
|
||||
from .base import BaseSerializer
|
||||
from plane.db.models import ExporterHistory
|
||||
from .user import UserLiteSerializer
|
||||
|
||||
|
||||
class ExporterHistorySerializer(BaseSerializer):
|
||||
initiated_by_detail = UserLiteSerializer(source="initiated_by", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ExporterHistory
|
||||
fields = [
|
||||
"id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"project",
|
||||
"provider",
|
||||
"status",
|
||||
"url",
|
||||
"initiated_by",
|
||||
"initiated_by_detail",
|
||||
"token",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
]
|
||||
read_only_fields = fields
|
||||
@@ -17,10 +17,12 @@ from plane.db.models import (
|
||||
IssueActivity,
|
||||
IssueComment,
|
||||
IssueProperty,
|
||||
IssueBlocker,
|
||||
IssueAssignee,
|
||||
IssueSubscriber,
|
||||
IssueLabel,
|
||||
Label,
|
||||
IssueBlocker,
|
||||
CycleIssue,
|
||||
Cycle,
|
||||
Module,
|
||||
@@ -29,8 +31,6 @@ from plane.db.models import (
|
||||
IssueAttachment,
|
||||
IssueReaction,
|
||||
CommentReaction,
|
||||
IssueVote,
|
||||
IssueRelation,
|
||||
)
|
||||
|
||||
|
||||
@@ -49,7 +49,6 @@ class IssueFlatSerializer(BaseSerializer):
|
||||
"target_date",
|
||||
"sequence_id",
|
||||
"sort_order",
|
||||
"is_draft",
|
||||
]
|
||||
|
||||
|
||||
@@ -81,12 +80,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__"
|
||||
@@ -99,18 +111,11 @@ class IssueCreateSerializer(BaseSerializer):
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
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)
|
||||
):
|
||||
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 +127,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 +186,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 +216,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 +267,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 +365,32 @@ class IssueLabelSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
|
||||
class IssueRelationSerializer(BaseSerializer):
|
||||
related_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 = [
|
||||
"related_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 +504,6 @@ class IssueAttachmentSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class IssueReactionSerializer(BaseSerializer):
|
||||
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
class Meta:
|
||||
model = IssueReaction
|
||||
fields = "__all__"
|
||||
@@ -452,6 +515,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,15 +548,6 @@ class CommentReactionSerializer(BaseSerializer):
|
||||
read_only_fields = ["workspace", "project", "comment", "actor"]
|
||||
|
||||
|
||||
class IssueVoteSerializer(BaseSerializer):
|
||||
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
|
||||
class Meta:
|
||||
model = IssueVote
|
||||
fields = ["issue", "vote", "workspace", "project", "actor", "actor_detail"]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class IssueCommentSerializer(BaseSerializer):
|
||||
actor_detail = UserLiteSerializer(read_only=True, source="actor")
|
||||
@@ -488,7 +555,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
|
||||
@@ -541,14 +608,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 +643,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 +661,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
|
||||
|
||||
@@ -40,11 +40,6 @@ class ModuleWriteSerializer(BaseSerializer):
|
||||
"updated_at",
|
||||
]
|
||||
|
||||
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):
|
||||
raise serializers.ValidationError("Start date cannot exceed target date")
|
||||
return data
|
||||
|
||||
def create(self, validated_data):
|
||||
members = validated_data.pop("members_list", None)
|
||||
|
||||
|
||||
@@ -14,8 +14,6 @@ from plane.db.models import (
|
||||
ProjectMemberInvite,
|
||||
ProjectIdentifier,
|
||||
ProjectFavorite,
|
||||
ProjectDeployBoard,
|
||||
ProjectPublicMember,
|
||||
)
|
||||
|
||||
|
||||
@@ -82,15 +80,7 @@ class ProjectSerializer(BaseSerializer):
|
||||
class ProjectLiteSerializer(BaseSerializer):
|
||||
class Meta:
|
||||
model = Project
|
||||
fields = [
|
||||
"id",
|
||||
"identifier",
|
||||
"name",
|
||||
"cover_image",
|
||||
"icon_prop",
|
||||
"emoji",
|
||||
"description",
|
||||
]
|
||||
fields = ["id", "identifier", "name"]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
@@ -104,8 +94,6 @@ class ProjectDetailSerializer(BaseSerializer):
|
||||
total_modules = serializers.IntegerField(read_only=True)
|
||||
is_member = serializers.BooleanField(read_only=True)
|
||||
sort_order = serializers.FloatField(read_only=True)
|
||||
member_role = serializers.IntegerField(read_only=True)
|
||||
is_deployed = serializers.BooleanField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Project
|
||||
@@ -113,7 +101,7 @@ class ProjectDetailSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class ProjectMemberSerializer(BaseSerializer):
|
||||
workspace = WorkspaceLiteSerializer(read_only=True)
|
||||
workspace = WorkSpaceSerializer(read_only=True)
|
||||
project = ProjectLiteSerializer(read_only=True)
|
||||
member = UserLiteSerializer(read_only=True)
|
||||
|
||||
@@ -127,6 +115,7 @@ class ProjectMemberAdminSerializer(BaseSerializer):
|
||||
project = ProjectLiteSerializer(read_only=True)
|
||||
member = UserAdminLiteSerializer(read_only=True)
|
||||
|
||||
|
||||
class Meta:
|
||||
model = ProjectMember
|
||||
fields = "__all__"
|
||||
@@ -159,6 +148,8 @@ class ProjectFavoriteSerializer(BaseSerializer):
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
class ProjectMemberLiteSerializer(BaseSerializer):
|
||||
member = UserLiteSerializer(read_only=True)
|
||||
is_subscribed = serializers.BooleanField(read_only=True)
|
||||
@@ -167,28 +158,3 @@ class ProjectMemberLiteSerializer(BaseSerializer):
|
||||
model = ProjectMember
|
||||
fields = ["member", "id", "is_subscribed"]
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class ProjectDeployBoardSerializer(BaseSerializer):
|
||||
project_details = ProjectLiteSerializer(read_only=True, source="project")
|
||||
workspace_detail = WorkspaceLiteSerializer(read_only=True, source="workspace")
|
||||
|
||||
class Meta:
|
||||
model = ProjectDeployBoard
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project", "anchor",
|
||||
]
|
||||
|
||||
|
||||
class ProjectPublicMemberSerializer(BaseSerializer):
|
||||
|
||||
class Meta:
|
||||
model = ProjectPublicMember
|
||||
fields = "__all__"
|
||||
read_only_fields = [
|
||||
"workspace",
|
||||
"project",
|
||||
"member",
|
||||
]
|
||||
|
||||
+2
-218
@@ -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,
|
||||
@@ -88,11 +86,9 @@ from plane.api.views import (
|
||||
IssueAttachmentEndpoint,
|
||||
IssueArchiveViewSet,
|
||||
IssueSubscriberViewSet,
|
||||
IssueCommentPublicViewSet,
|
||||
IssueReactionViewSet,
|
||||
IssueRelationViewSet,
|
||||
CommentReactionViewSet,
|
||||
IssueDraftViewSet,
|
||||
ExportIssuesEndpoint,
|
||||
## End Issues
|
||||
# States
|
||||
StateViewSet,
|
||||
@@ -168,23 +164,7 @@ from plane.api.views import (
|
||||
# Notification
|
||||
NotificationViewSet,
|
||||
UnreadNotificationEndpoint,
|
||||
MarkAllReadNotificationViewSet,
|
||||
## End Notification
|
||||
# Public Boards
|
||||
ProjectDeployBoardViewSet,
|
||||
ProjectIssuesPublicEndpoint,
|
||||
ProjectDeployBoardPublicSettingsEndpoint,
|
||||
IssueReactionPublicViewSet,
|
||||
CommentReactionPublicViewSet,
|
||||
InboxIssuePublicViewSet,
|
||||
IssueVotePublicViewSet,
|
||||
WorkspaceProjectDeployBoardEndpoint,
|
||||
IssueRetrievePublicEndpoint,
|
||||
## End Public Boards
|
||||
## Exporter
|
||||
ExportIssuesEndpoint,
|
||||
## End Exporter
|
||||
|
||||
)
|
||||
|
||||
|
||||
@@ -241,7 +221,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/",
|
||||
@@ -445,11 +425,6 @@ urlpatterns = [
|
||||
WorkspaceLabelsEndpoint.as_view(),
|
||||
name="workspace-labels",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/members/leave/",
|
||||
LeaveWorkspaceEndpoint.as_view(),
|
||||
name="workspace-labels",
|
||||
),
|
||||
## End Workspaces ##
|
||||
# Projects
|
||||
path(
|
||||
@@ -563,11 +538,6 @@ urlpatterns = [
|
||||
),
|
||||
name="project",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/members/leave/",
|
||||
LeaveProjectEndpoint.as_view(),
|
||||
name="project",
|
||||
),
|
||||
# End Projects
|
||||
# States
|
||||
path(
|
||||
@@ -1012,49 +982,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/",
|
||||
@@ -1553,148 +1480,5 @@ 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(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/",
|
||||
ProjectDeployBoardViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="project-deploy-board",
|
||||
),
|
||||
path(
|
||||
"workspaces/<str:slug>/projects/<uuid:project_id>/project-deploy-boards/<uuid:pk>/",
|
||||
ProjectDeployBoardViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="project-deploy-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/settings/",
|
||||
ProjectDeployBoardPublicSettingsEndpoint.as_view(),
|
||||
name="project-deploy-board-settings",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/",
|
||||
ProjectIssuesPublicEndpoint.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(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="issue-comments-project-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/comments/<uuid:pk>/",
|
||||
IssueCommentPublicViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="issue-comments-project-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/",
|
||||
IssueReactionPublicViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="issue-reactions-project-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/reactions/<str:reaction_code>/",
|
||||
IssueReactionPublicViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="issue-reactions-project-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/",
|
||||
CommentReactionPublicViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="comment-reactions-project-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/comments/<uuid:comment_id>/reactions/<str:reaction_code>/",
|
||||
CommentReactionPublicViewSet.as_view(
|
||||
{
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="comment-reactions-project-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/",
|
||||
InboxIssuePublicViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
}
|
||||
),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/inboxes/<uuid:inbox_id>/inbox-issues/<uuid:pk>/",
|
||||
InboxIssuePublicViewSet.as_view(
|
||||
{
|
||||
"get": "retrieve",
|
||||
"patch": "partial_update",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="inbox-issue",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/<uuid:project_id>/issues/<uuid:issue_id>/votes/",
|
||||
IssueVotePublicViewSet.as_view(
|
||||
{
|
||||
"get": "list",
|
||||
"post": "create",
|
||||
"delete": "destroy",
|
||||
}
|
||||
),
|
||||
name="issue-vote-project-board",
|
||||
),
|
||||
path(
|
||||
"public/workspaces/<str:slug>/project-boards/",
|
||||
WorkspaceProjectDeployBoardEndpoint.as_view(),
|
||||
name="workspace-project-boards",
|
||||
),
|
||||
## End Public Boards
|
||||
]
|
||||
|
||||
@@ -12,11 +12,7 @@ from .project import (
|
||||
ProjectUserViewsEndpoint,
|
||||
ProjectMemberUserEndpoint,
|
||||
ProjectFavoritesViewSet,
|
||||
ProjectDeployBoardViewSet,
|
||||
ProjectDeployBoardPublicSettingsEndpoint,
|
||||
ProjectMemberEndpoint,
|
||||
WorkspaceProjectDeployBoardEndpoint,
|
||||
LeaveProjectEndpoint,
|
||||
)
|
||||
from .user import (
|
||||
UserEndpoint,
|
||||
@@ -53,7 +49,6 @@ from .workspace import (
|
||||
WorkspaceUserProfileIssuesEndpoint,
|
||||
WorkspaceLabelsEndpoint,
|
||||
WorkspaceMembersEndpoint,
|
||||
LeaveWorkspaceEndpoint,
|
||||
)
|
||||
from .state import StateViewSet
|
||||
from .view import IssueViewViewSet, ViewIssuesEndpoint, IssueViewFavoriteViewSet
|
||||
@@ -80,16 +75,9 @@ from .issue import (
|
||||
IssueAttachmentEndpoint,
|
||||
IssueArchiveViewSet,
|
||||
IssueSubscriberViewSet,
|
||||
IssueCommentPublicViewSet,
|
||||
CommentReactionViewSet,
|
||||
IssueReactionViewSet,
|
||||
IssueReactionPublicViewSet,
|
||||
CommentReactionPublicViewSet,
|
||||
IssueVotePublicViewSet,
|
||||
IssueRelationViewSet,
|
||||
IssueRetrievePublicEndpoint,
|
||||
ProjectIssuesPublicEndpoint,
|
||||
IssueDraftViewSet,
|
||||
ExportIssuesEndpoint
|
||||
)
|
||||
|
||||
from .auth_extended import (
|
||||
@@ -157,7 +145,7 @@ from .estimate import (
|
||||
|
||||
from .release import ReleaseNotesEndpoint
|
||||
|
||||
from .inbox import InboxViewSet, InboxIssueViewSet, InboxIssuePublicViewSet
|
||||
from .inbox import InboxViewSet, InboxIssueViewSet
|
||||
|
||||
from .analytic import (
|
||||
AnalyticsEndpoint,
|
||||
@@ -167,6 +155,4 @@ from .analytic import (
|
||||
DefaultAnalyticsEndpoint,
|
||||
)
|
||||
|
||||
from .notification import NotificationViewSet, UnreadNotificationEndpoint, MarkAllReadNotificationViewSet
|
||||
|
||||
from .exporter import ExportIssuesEndpoint
|
||||
from .notification import NotificationViewSet, UnreadNotificationEndpoint
|
||||
@@ -243,21 +243,21 @@ class DefaultAnalyticsEndpoint(BaseAPIView):
|
||||
)
|
||||
most_issue_created_user = (
|
||||
queryset.exclude(created_by=None)
|
||||
.values("created_by__first_name", "created_by__last_name", "created_by__avatar", "created_by__display_name", "created_by__id")
|
||||
.values("created_by__first_name", "created_by__last_name", "created_by__avatar", "created_by__display_name")
|
||||
.annotate(count=Count("id"))
|
||||
.order_by("-count")
|
||||
)[:5]
|
||||
|
||||
most_issue_closed_user = (
|
||||
queryset.filter(completed_at__isnull=False, assignees__isnull=False)
|
||||
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name", "assignees__id")
|
||||
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name")
|
||||
.annotate(count=Count("id"))
|
||||
.order_by("-count")
|
||||
)[:5]
|
||||
|
||||
pending_issue_user = (
|
||||
queryset.filter(completed_at__isnull=True)
|
||||
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name", "assignees__id")
|
||||
.values("assignees__first_name", "assignees__last_name", "assignees__avatar", "assignees__display_name")
|
||||
.annotate(count=Count("id"))
|
||||
.order_by("-count")
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -165,9 +165,6 @@ class CycleViewSet(BaseViewSet):
|
||||
try:
|
||||
queryset = self.get_queryset()
|
||||
cycle_view = request.GET.get("cycle_view", "all")
|
||||
order_by = request.GET.get("order_by", "sort_order")
|
||||
|
||||
queryset = queryset.order_by(order_by)
|
||||
|
||||
# All Cycles
|
||||
if cycle_view == "all":
|
||||
@@ -191,10 +188,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(
|
||||
@@ -208,7 +206,7 @@ class CycleViewSet(BaseViewSet):
|
||||
filter=Q(completed_at__isnull=True),
|
||||
)
|
||||
)
|
||||
.order_by("display_name")
|
||||
.order_by("first_name", "last_name")
|
||||
)
|
||||
|
||||
label_distribution = (
|
||||
@@ -333,21 +331,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():
|
||||
@@ -380,10 +370,7 @@ class CycleViewSet(BaseViewSet):
|
||||
.annotate(last_name=F("assignees__last_name"))
|
||||
.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")
|
||||
.annotate(total_issues=Count("assignee_id"))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
@@ -517,7 +504,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)
|
||||
@@ -556,15 +542,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,
|
||||
)
|
||||
|
||||
@@ -726,6 +706,7 @@ class CycleDateCheckEndpoint(BaseAPIView):
|
||||
|
||||
|
||||
class CycleFavoriteViewSet(BaseViewSet):
|
||||
|
||||
serializer_class = CycleFavoriteSerializer
|
||||
model = CycleFavorite
|
||||
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from . import BaseAPIView
|
||||
from plane.api.permissions import WorkSpaceAdminPermission
|
||||
from plane.bgtasks.export_task import issue_export_task
|
||||
from plane.db.models import Project, ExporterHistory, Workspace
|
||||
|
||||
from plane.api.serializers import ExporterHistorySerializer
|
||||
|
||||
|
||||
class ExportIssuesEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
WorkSpaceAdminPermission,
|
||||
]
|
||||
model = ExporterHistory
|
||||
serializer_class = ExporterHistorySerializer
|
||||
|
||||
def post(self, request, slug):
|
||||
try:
|
||||
# Get the workspace
|
||||
workspace = Workspace.objects.get(slug=slug)
|
||||
|
||||
provider = request.data.get("provider", False)
|
||||
multiple = request.data.get("multiple", False)
|
||||
project_ids = request.data.get("project", [])
|
||||
|
||||
if provider in ["csv", "xlsx", "json"]:
|
||||
if not project_ids:
|
||||
project_ids = Project.objects.filter(
|
||||
workspace__slug=slug
|
||||
).values_list("id", flat=True)
|
||||
project_ids = [str(project_id) for project_id in project_ids]
|
||||
|
||||
exporter = ExporterHistory.objects.create(
|
||||
workspace=workspace,
|
||||
project=project_ids,
|
||||
initiated_by=request.user,
|
||||
provider=provider,
|
||||
)
|
||||
|
||||
issue_export_task.delay(
|
||||
provider=exporter.provider,
|
||||
workspace_id=workspace.id,
|
||||
project_ids=project_ids,
|
||||
token_id=exporter.token,
|
||||
multiple=multiple,
|
||||
slug=slug,
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"message": f"Once the export is ready you will be able to download it"
|
||||
},
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
{"error": f"Provider '{provider}' not found."},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
except Workspace.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Workspace 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,
|
||||
)
|
||||
|
||||
def get(self, request, slug):
|
||||
try:
|
||||
exporter_history = ExporterHistory.objects.filter(
|
||||
workspace__slug=slug
|
||||
).select_related("workspace","initiated_by")
|
||||
|
||||
if request.GET.get("per_page", False) and request.GET.get("cursor", False):
|
||||
return self.paginate(
|
||||
request=request,
|
||||
queryset=exporter_history,
|
||||
on_results=lambda exporter_history: ExporterHistorySerializer(
|
||||
exporter_history, many=True
|
||||
).data,
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
{"error": "per_page and cursor are required"},
|
||||
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,
|
||||
)
|
||||
@@ -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(
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ from sentry_sdk import capture_exception
|
||||
from .base import BaseViewSet
|
||||
from plane.api.permissions import ProjectBasePermission, ProjectLitePermission
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
Inbox,
|
||||
InboxIssue,
|
||||
Issue,
|
||||
@@ -22,7 +23,6 @@ from plane.db.models import (
|
||||
IssueLink,
|
||||
IssueAttachment,
|
||||
ProjectMember,
|
||||
ProjectDeployBoard,
|
||||
)
|
||||
from plane.api.serializers import (
|
||||
IssueSerializer,
|
||||
@@ -377,269 +377,4 @@ class InboxIssueViewSet(BaseViewSet):
|
||||
return Response(
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class InboxIssuePublicViewSet(BaseViewSet):
|
||||
serializer_class = InboxIssueSerializer
|
||||
model = InboxIssue
|
||||
|
||||
filterset_fields = [
|
||||
"status",
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=self.kwargs.get("slug"), project_id=self.kwargs.get("project_id"))
|
||||
if project_deploy_board is not None:
|
||||
return self.filter_queryset(
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(
|
||||
Q(snoozed_till__gte=timezone.now()) | Q(snoozed_till__isnull=True),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
inbox_id=self.kwargs.get("inbox_id"),
|
||||
)
|
||||
.select_related("issue", "workspace", "project")
|
||||
)
|
||||
else:
|
||||
return InboxIssue.objects.none()
|
||||
|
||||
def list(self, request, slug, project_id, inbox_id):
|
||||
try:
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
||||
if project_deploy_board.inbox is None:
|
||||
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
filters = issue_filters(request.query_params, "GET")
|
||||
issues = (
|
||||
Issue.objects.filter(
|
||||
issue_inbox__inbox_id=inbox_id,
|
||||
workspace__slug=slug,
|
||||
project_id=project_id,
|
||||
)
|
||||
.filter(**filters)
|
||||
.annotate(bridge_id=F("issue_inbox__id"))
|
||||
.select_related("workspace", "project", "state", "parent")
|
||||
.prefetch_related("assignees", "labels")
|
||||
.order_by("issue_inbox__snoozed_till", "issue_inbox__status")
|
||||
.annotate(
|
||||
sub_issues_count=Issue.issue_objects.filter(parent=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.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")
|
||||
)
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_inbox",
|
||||
queryset=InboxIssue.objects.only(
|
||||
"status", "duplicate_to", "snoozed_till", "source"
|
||||
),
|
||||
)
|
||||
)
|
||||
)
|
||||
issues_data = IssueStateInboxSerializer(issues, many=True).data
|
||||
return Response(
|
||||
issues_data,
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
return Response({"error": "Project Deploy Board does not exist"}, 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,
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id, inbox_id):
|
||||
try:
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
||||
if project_deploy_board.inbox is None:
|
||||
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
if not request.data.get("issue", {}).get("name", False):
|
||||
return Response(
|
||||
{"error": "Name is required"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Check for valid priority
|
||||
if not request.data.get("issue", {}).get("priority", None) in [
|
||||
"low",
|
||||
"medium",
|
||||
"high",
|
||||
"urgent",
|
||||
None,
|
||||
]:
|
||||
return Response(
|
||||
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# Create or get state
|
||||
state, _ = State.objects.get_or_create(
|
||||
name="Triage",
|
||||
group="backlog",
|
||||
description="Default state for managing all Inbox Issues",
|
||||
project_id=project_id,
|
||||
color="#ff7700",
|
||||
)
|
||||
|
||||
# create an issue
|
||||
issue = Issue.objects.create(
|
||||
name=request.data.get("issue", {}).get("name"),
|
||||
description=request.data.get("issue", {}).get("description", {}),
|
||||
description_html=request.data.get("issue", {}).get(
|
||||
"description_html", "<p></p>"
|
||||
),
|
||||
priority=request.data.get("issue", {}).get("priority", "low"),
|
||||
project_id=project_id,
|
||||
state=state,
|
||||
)
|
||||
|
||||
# Create an Issue Activity
|
||||
issue_activity.delay(
|
||||
type="issue.activity.created",
|
||||
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=None,
|
||||
)
|
||||
# create an inbox issue
|
||||
InboxIssue.objects.create(
|
||||
inbox_id=inbox_id,
|
||||
project_id=project_id,
|
||||
issue=issue,
|
||||
source=request.data.get("source", "in-app"),
|
||||
)
|
||||
|
||||
serializer = IssueStateInboxSerializer(issue)
|
||||
return Response(serializer.data, 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,
|
||||
)
|
||||
|
||||
def partial_update(self, request, slug, project_id, inbox_id, pk):
|
||||
try:
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
||||
if project_deploy_board.inbox is None:
|
||||
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
inbox_issue = InboxIssue.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||
)
|
||||
# Get the project member
|
||||
if str(inbox_issue.created_by_id) != str(request.user.id):
|
||||
return Response({"error": "You cannot edit inbox issues"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Get issue data
|
||||
issue_data = request.data.pop("issue", False)
|
||||
|
||||
|
||||
issue = Issue.objects.get(
|
||||
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
# viewers and guests since only viewers and guests
|
||||
issue_data = {
|
||||
"name": issue_data.get("name", issue.name),
|
||||
"description_html": issue_data.get("description_html", issue.description_html),
|
||||
"description": issue_data.get("description", issue.description)
|
||||
}
|
||||
|
||||
issue_serializer = IssueCreateSerializer(
|
||||
issue, data=issue_data, partial=True
|
||||
)
|
||||
|
||||
if issue_serializer.is_valid():
|
||||
current_instance = issue
|
||||
# Log all the updates
|
||||
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
|
||||
if issue is not None:
|
||||
issue_activity.delay(
|
||||
type="issue.activity.updated",
|
||||
requested_data=requested_data,
|
||||
actor_id=str(request.user.id),
|
||||
issue_id=str(issue.id),
|
||||
project_id=str(project_id),
|
||||
current_instance=json.dumps(
|
||||
IssueSerializer(current_instance).data,
|
||||
cls=DjangoJSONEncoder,
|
||||
),
|
||||
)
|
||||
issue_serializer.save()
|
||||
return Response(issue_serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(issue_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except InboxIssue.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Inbox Issue does not exist"},
|
||||
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,
|
||||
)
|
||||
|
||||
def retrieve(self, request, slug, project_id, inbox_id, pk):
|
||||
try:
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
||||
if project_deploy_board.inbox is None:
|
||||
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
inbox_issue = InboxIssue.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||
)
|
||||
issue = Issue.objects.get(
|
||||
pk=inbox_issue.issue_id, workspace__slug=slug, project_id=project_id
|
||||
)
|
||||
serializer = IssueStateInboxSerializer(issue)
|
||||
return Response(serializer.data, 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,
|
||||
)
|
||||
|
||||
def destroy(self, request, slug, project_id, inbox_id, pk):
|
||||
try:
|
||||
project_deploy_board = ProjectDeployBoard.objects.get(workspace__slug=slug, project_id=project_id)
|
||||
if project_deploy_board.inbox is None:
|
||||
return Response({"error": "Inbox is not enabled for this Project Board"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
inbox_issue = InboxIssue.objects.get(
|
||||
pk=pk, workspace__slug=slug, project_id=project_id, inbox_id=inbox_id
|
||||
)
|
||||
|
||||
if str(inbox_issue.created_by_id) != str(request.user.id):
|
||||
return Response({"error": "You cannot delete inbox issue"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
inbox_issue.delete()
|
||||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
except InboxIssue.DoesNotExist:
|
||||
return Response({"error": "Inbox Issue 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,
|
||||
)
|
||||
|
||||
)
|
||||
@@ -20,17 +20,6 @@ class SlackProjectSyncViewSet(BaseViewSet):
|
||||
serializer_class = SlackProjectSyncSerializer
|
||||
model = SlackProjectSync
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
.filter(project__project_projectmember__member=self.request.user)
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id, workspace_integration_id):
|
||||
try:
|
||||
serializer = SlackProjectSyncSerializer(data=request.data)
|
||||
@@ -56,10 +45,7 @@ class SlackProjectSyncViewSet(BaseViewSet):
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
except IntegrityError:
|
||||
return Response(
|
||||
{"error": "Slack is already enabled for the project"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
return Response({"error": "Slack is already enabled for the project"}, status=status.HTTP_400_BAD_REQUEST)
|
||||
except WorkspaceIntegration.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Workspace Integration does not exist"},
|
||||
|
||||
+27
-1133
File diff suppressed because it is too large
Load Diff
@@ -53,8 +53,6 @@ class ModuleViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
def get_queryset(self):
|
||||
order_by = self.request.GET.get("order_by", "sort_order")
|
||||
|
||||
subquery = ModuleFavorite.objects.filter(
|
||||
user=self.request.user,
|
||||
module_id=OuterRef("pk"),
|
||||
@@ -108,7 +106,7 @@ class ModuleViewSet(BaseViewSet):
|
||||
filter=Q(issue_module__issue__state__group="backlog"),
|
||||
)
|
||||
)
|
||||
.order_by(order_by, "name")
|
||||
.order_by("-is_favorite", "name")
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
@@ -175,9 +173,8 @@ class ModuleViewSet(BaseViewSet):
|
||||
.annotate(first_name=F("assignees__first_name"))
|
||||
.annotate(last_name=F("assignees__last_name"))
|
||||
.annotate(assignee_id=F("assignees__id"))
|
||||
.annotate(display_name=F("assignees__display_name"))
|
||||
.annotate(avatar=F("assignees__avatar"))
|
||||
.values("first_name", "last_name", "assignee_id", "avatar", "display_name")
|
||||
.values("first_name", "last_name", "assignee_id", "avatar")
|
||||
.annotate(total_issues=Count("assignee_id"))
|
||||
.annotate(
|
||||
completed_issues=Count(
|
||||
@@ -308,7 +305,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)
|
||||
@@ -347,15 +343,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,
|
||||
)
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -5,15 +5,7 @@ from datetime import datetime
|
||||
# Django imports
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import (
|
||||
Q,
|
||||
Exists,
|
||||
OuterRef,
|
||||
Func,
|
||||
F,
|
||||
Func,
|
||||
Subquery,
|
||||
)
|
||||
from django.db.models import Q, Exists, OuterRef, Func, F, Min, Subquery
|
||||
from django.core.validators import validate_email
|
||||
from django.conf import settings
|
||||
|
||||
@@ -21,7 +13,6 @@ from django.conf import settings
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework import serializers
|
||||
from rest_framework.permissions import AllowAny
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
@@ -32,17 +23,9 @@ from plane.api.serializers import (
|
||||
ProjectDetailSerializer,
|
||||
ProjectMemberInviteSerializer,
|
||||
ProjectFavoriteSerializer,
|
||||
IssueLiteSerializer,
|
||||
ProjectDeployBoardSerializer,
|
||||
ProjectMemberAdminSerializer,
|
||||
)
|
||||
|
||||
from plane.api.permissions import (
|
||||
ProjectBasePermission,
|
||||
ProjectEntityPermission,
|
||||
ProjectMemberPermission,
|
||||
ProjectLitePermission,
|
||||
)
|
||||
from plane.api.permissions import ProjectBasePermission, ProjectEntityPermission
|
||||
|
||||
from plane.db.models import (
|
||||
Project,
|
||||
@@ -65,7 +48,6 @@ from plane.db.models import (
|
||||
IssueAssignee,
|
||||
ModuleMember,
|
||||
Inbox,
|
||||
ProjectDeployBoard,
|
||||
)
|
||||
|
||||
from plane.bgtasks.project_invitation_task import project_invitation
|
||||
@@ -110,9 +92,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
)
|
||||
)
|
||||
.annotate(
|
||||
total_members=ProjectMember.objects.filter(
|
||||
project_id=OuterRef("id"), member__is_bot=False
|
||||
)
|
||||
total_members=ProjectMember.objects.filter(project_id=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
@@ -129,20 +109,6 @@ class ProjectViewSet(BaseViewSet):
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
.annotate(
|
||||
member_role=ProjectMember.objects.filter(
|
||||
project_id=OuterRef("pk"),
|
||||
member_id=self.request.user.id,
|
||||
).values("role")
|
||||
)
|
||||
.annotate(
|
||||
is_deployed=Exists(
|
||||
ProjectDeployBoard.objects.filter(
|
||||
project_id=OuterRef("pk"),
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
)
|
||||
)
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
|
||||
@@ -214,9 +180,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
project_id=serializer.data["id"], member=request.user, role=20
|
||||
)
|
||||
|
||||
if serializer.data["project_lead"] is not None and str(
|
||||
serializer.data["project_lead"]
|
||||
) != str(request.user.id):
|
||||
if serializer.data["project_lead"] is not None:
|
||||
ProjectMember.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
member_id=serializer.data["project_lead"],
|
||||
@@ -275,10 +239,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
data = serializer.data
|
||||
# Additional fields of the member
|
||||
data["sort_order"] = project_member.sort_order
|
||||
data["member_role"] = project_member.role
|
||||
data["is_member"] = True
|
||||
return Response(data, status=status.HTTP_201_CREATED)
|
||||
return Response(
|
||||
serializer.errors,
|
||||
@@ -386,9 +347,7 @@ class InviteProjectEndpoint(BaseAPIView):
|
||||
validate_email(email)
|
||||
# Check if user is already a member of workspace
|
||||
if ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
member__email=email,
|
||||
member__is_bot=False,
|
||||
project_id=project_id, member__email=email
|
||||
).exists():
|
||||
return Response(
|
||||
{"error": "User is already member of workspace"},
|
||||
@@ -482,7 +441,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(
|
||||
@@ -492,7 +451,7 @@ class UserProjectInvitationsViewset(BaseViewSet):
|
||||
|
||||
|
||||
class ProjectMemberViewSet(BaseViewSet):
|
||||
serializer_class = ProjectMemberAdminSerializer
|
||||
serializer_class = ProjectMemberSerializer
|
||||
model = ProjectMember
|
||||
permission_classes = [
|
||||
ProjectBasePermission,
|
||||
@@ -617,7 +576,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 +883,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"},
|
||||
@@ -1026,63 +986,6 @@ class ProjectFavoritesViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
|
||||
class ProjectDeployBoardViewSet(BaseViewSet):
|
||||
permission_classes = [
|
||||
ProjectMemberPermission,
|
||||
]
|
||||
serializer_class = ProjectDeployBoardSerializer
|
||||
model = ProjectDeployBoard
|
||||
|
||||
def get_queryset(self):
|
||||
return (
|
||||
super()
|
||||
.get_queryset()
|
||||
.filter(
|
||||
workspace__slug=self.kwargs.get("slug"),
|
||||
project_id=self.kwargs.get("project_id"),
|
||||
)
|
||||
.select_related("project")
|
||||
)
|
||||
|
||||
def create(self, request, slug, project_id):
|
||||
try:
|
||||
comments = request.data.get("comments", False)
|
||||
reactions = request.data.get("reactions", False)
|
||||
inbox = request.data.get("inbox", None)
|
||||
votes = request.data.get("votes", False)
|
||||
views = request.data.get(
|
||||
"views",
|
||||
{
|
||||
"list": True,
|
||||
"kanban": True,
|
||||
"calendar": True,
|
||||
"gantt": True,
|
||||
"spreadsheet": True,
|
||||
},
|
||||
)
|
||||
|
||||
project_deploy_board, _ = ProjectDeployBoard.objects.get_or_create(
|
||||
anchor=f"{slug}/{project_id}",
|
||||
project_id=project_id,
|
||||
)
|
||||
project_deploy_board.comments = comments
|
||||
project_deploy_board.reactions = reactions
|
||||
project_deploy_board.inbox = inbox
|
||||
project_deploy_board.votes = votes
|
||||
project_deploy_board.views = views
|
||||
|
||||
project_deploy_board.save()
|
||||
|
||||
serializer = ProjectDeployBoardSerializer(project_deploy_board)
|
||||
return Response(serializer.data, 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 ProjectMemberEndpoint(BaseAPIView):
|
||||
permission_classes = [
|
||||
ProjectEntityPermission,
|
||||
@@ -1091,9 +994,7 @@ class ProjectMemberEndpoint(BaseAPIView):
|
||||
def get(self, request, slug, project_id):
|
||||
try:
|
||||
project_members = ProjectMember.objects.filter(
|
||||
project_id=project_id,
|
||||
workspace__slug=slug,
|
||||
member__is_bot=False,
|
||||
project_id=project_id, workspace__slug=slug
|
||||
).select_related("project", "member")
|
||||
serializer = ProjectMemberSerializer(project_members, many=True)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
@@ -1103,109 +1004,3 @@ class ProjectMemberEndpoint(BaseAPIView):
|
||||
{"error": "Something went wrong please try again later"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
class ProjectDeployBoardPublicSettingsEndpoint(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
|
||||
)
|
||||
serializer = ProjectDeployBoardSerializer(project_deploy_board)
|
||||
return Response(serializer.data, status=status.HTTP_200_OK)
|
||||
except ProjectDeployBoard.DoesNotExist:
|
||||
return Response(
|
||||
{"error": "Project Deploy 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 = (
|
||||
Project.objects.filter(workspace__slug=slug)
|
||||
.annotate(
|
||||
is_public=Exists(
|
||||
ProjectDeployBoard.objects.filter(
|
||||
workspace__slug=slug, project_id=OuterRef("pk")
|
||||
)
|
||||
)
|
||||
)
|
||||
.filter(is_public=True)
|
||||
).values(
|
||||
"id",
|
||||
"identifier",
|
||||
"name",
|
||||
"description",
|
||||
"emoji",
|
||||
"icon_prop",
|
||||
"cover_image",
|
||||
)
|
||||
|
||||
return Response(projects, 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 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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -19,7 +19,6 @@ from plane.db.models import (
|
||||
IssueView,
|
||||
Issue,
|
||||
IssueViewFavorite,
|
||||
IssueReaction,
|
||||
)
|
||||
from plane.utils.issue_filters import issue_filters
|
||||
|
||||
@@ -78,12 +77,6 @@ class ViewIssuesEndpoint(BaseAPIView):
|
||||
.select_related("parent")
|
||||
.prefetch_related("assignees")
|
||||
.prefetch_related("labels")
|
||||
.prefetch_related(
|
||||
Prefetch(
|
||||
"issue_reactions",
|
||||
queryset=IssueReaction.objects.select_related("actor"),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
serializer = IssueLiteSerializer(issues, many=True)
|
||||
|
||||
@@ -47,7 +47,7 @@ from plane.api.serializers import (
|
||||
WorkspaceThemeSerializer,
|
||||
IssueActivitySerializer,
|
||||
IssueLiteSerializer,
|
||||
WorkspaceMemberAdminSerializer,
|
||||
WorkspaceMemberAdminSerializer
|
||||
)
|
||||
from plane.api.views.base import BaseAPIView
|
||||
from . import BaseViewSet
|
||||
@@ -107,16 +107,14 @@ class WorkSpaceViewSet(BaseViewSet):
|
||||
|
||||
def get_queryset(self):
|
||||
member_count = (
|
||||
WorkspaceMember.objects.filter(
|
||||
workspace=OuterRef("id"), member__is_bot=False
|
||||
)
|
||||
WorkspaceMember.objects.filter(workspace=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
|
||||
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")
|
||||
@@ -194,16 +192,14 @@ class UserWorkSpacesEndpoint(BaseAPIView):
|
||||
def get(self, request):
|
||||
try:
|
||||
member_count = (
|
||||
WorkspaceMember.objects.filter(
|
||||
workspace=OuterRef("id"), member__is_bot=False
|
||||
)
|
||||
WorkspaceMember.objects.filter(workspace=OuterRef("id"))
|
||||
.order_by()
|
||||
.annotate(count=Func(F("id"), function="Count"))
|
||||
.values("count")
|
||||
)
|
||||
|
||||
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 +528,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(
|
||||
@@ -629,9 +625,7 @@ class WorkSpaceMemberViewSet(BaseViewSet):
|
||||
if (
|
||||
workspace_member.role == 20
|
||||
and WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
role=20,
|
||||
member__is_bot=False,
|
||||
workspace__slug=slug, role=20
|
||||
).count()
|
||||
== 1
|
||||
):
|
||||
@@ -846,7 +840,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"},
|
||||
@@ -994,11 +988,11 @@ class UserWorkspaceDashboardEndpoint(BaseAPIView):
|
||||
|
||||
upcoming_issues = Issue.issue_objects.filter(
|
||||
~Q(state__group__in=["completed", "cancelled"]),
|
||||
start_date__gte=timezone.now(),
|
||||
target_date__gte=timezone.now(),
|
||||
workspace__slug=slug,
|
||||
assignees__in=[request.user],
|
||||
completed_at__isnull=True,
|
||||
).values("id", "name", "workspace__slug", "project_id", "start_date")
|
||||
).values("id", "name", "workspace__slug", "project_id", "target_date")
|
||||
|
||||
return Response(
|
||||
{
|
||||
@@ -1072,10 +1066,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,
|
||||
@@ -1083,7 +1077,6 @@ class WorkspaceUserProfileStatsEndpoint(BaseAPIView):
|
||||
.filter(**filters)
|
||||
.values("priority")
|
||||
.annotate(priority_count=Count("priority"))
|
||||
.filter(priority_count__gte=1)
|
||||
.annotate(
|
||||
priority_order=Case(
|
||||
*[
|
||||
@@ -1100,6 +1093,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 +1191,6 @@ class WorkspaceUserActivityEndpoint(BaseAPIView):
|
||||
projects = request.query_params.getlist("project", [])
|
||||
|
||||
queryset = IssueActivity.objects.filter(
|
||||
~Q(field__in=["comment", "vote", "reaction", "draft"]),
|
||||
workspace__slug=slug,
|
||||
project__project_projectmember__member=request.user,
|
||||
actor=user_id,
|
||||
@@ -1462,8 +1455,7 @@ class WorkspaceMembersEndpoint(BaseAPIView):
|
||||
def get(self, request, slug):
|
||||
try:
|
||||
workspace_members = WorkspaceMember.objects.filter(
|
||||
workspace__slug=slug,
|
||||
member__is_bot=False,
|
||||
workspace__slug=slug
|
||||
).select_related("workspace", "member")
|
||||
serialzier = WorkSpaceMemberSerializer(workspace_members, many=True)
|
||||
return Response(serialzier.data, status=status.HTTP_200_OK)
|
||||
@@ -1473,44 +1465,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,
|
||||
)
|
||||
|
||||
@@ -51,12 +51,12 @@ def analytic_export_task(email, data, slug):
|
||||
segmented = segment
|
||||
|
||||
assignee_details = {}
|
||||
if x_axis in ["assignees__id"] or segment in ["assignees__id"]:
|
||||
if x_axis in ["assignees__display_name"] or segment in ["assignees__display_name"]:
|
||||
assignee_details = (
|
||||
Issue.issue_objects.filter(workspace__slug=slug, **filters, assignees__avatar__isnull=False)
|
||||
.order_by("assignees__id")
|
||||
.distinct("assignees__id")
|
||||
.values("assignees__avatar", "assignees__display_name", "assignees__first_name", "assignees__last_name", "assignees__id")
|
||||
.values("assignees__avatar", "assignees__display_name", "assignees__first_name", "assignees__last_name")
|
||||
)
|
||||
|
||||
if segment:
|
||||
@@ -93,19 +93,19 @@ def analytic_export_task(email, data, slug):
|
||||
else:
|
||||
generated_row.append("0")
|
||||
# x-axis replacement for names
|
||||
if x_axis in ["assignees__id"]:
|
||||
assignee = [user for user in assignee_details if str(user.get("assignees__id")) == str(item)]
|
||||
if x_axis in ["assignees__display_name"]:
|
||||
assignee = [user for user in assignee_details if str(user.get("assignees__display_name")) == str(item)]
|
||||
if len(assignee):
|
||||
generated_row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
|
||||
rows.append(tuple(generated_row))
|
||||
|
||||
# If segment is ["assignees__display_name"] then replace segment_zero rows with first and last names
|
||||
if segmented in ["assignees__id"]:
|
||||
if segmented in ["assignees__display_name"]:
|
||||
for index, segm in enumerate(row_zero[2:]):
|
||||
# find the name of the user
|
||||
assignee = [user for user in assignee_details if str(user.get("assignees__id")) == str(segm)]
|
||||
assignee = [user for user in assignee_details if str(user.get("assignees__display_name")) == str(segm)]
|
||||
if len(assignee):
|
||||
row_zero[index + 2] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
|
||||
row_zero[index] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
|
||||
|
||||
rows = [tuple(row_zero)] + rows
|
||||
csv_buffer = io.StringIO()
|
||||
@@ -141,8 +141,8 @@ def analytic_export_task(email, data, slug):
|
||||
else distribution.get(item)[0].get("estimate "),
|
||||
]
|
||||
# x-axis replacement to names
|
||||
if x_axis in ["assignees__id"]:
|
||||
assignee = [user for user in assignee_details if str(user.get("assignees__id")) == str(item)]
|
||||
if x_axis in ["assignees__display_name"]:
|
||||
assignee = [user for user in assignee_details if str(user.get("assignees__display_name")) == str(item)]
|
||||
if len(assignee):
|
||||
row[0] = str(assignee[0].get("assignees__first_name")) + " " + str(assignee[0].get("assignees__last_name"))
|
||||
|
||||
|
||||
@@ -1,375 +0,0 @@
|
||||
# Python imports
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import boto3
|
||||
import zipfile
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
from sentry_sdk import capture_exception
|
||||
from botocore.client import Config
|
||||
from openpyxl import Workbook
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Issue, ExporterHistory
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
||||
def create_csv_file(data):
|
||||
csv_buffer = io.StringIO()
|
||||
csv_writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
|
||||
|
||||
for row in data:
|
||||
csv_writer.writerow(row)
|
||||
|
||||
csv_buffer.seek(0)
|
||||
return csv_buffer.getvalue()
|
||||
|
||||
|
||||
def create_json_file(data):
|
||||
return json.dumps(data)
|
||||
|
||||
|
||||
def create_xlsx_file(data):
|
||||
workbook = Workbook()
|
||||
sheet = workbook.active
|
||||
|
||||
for row in data:
|
||||
sheet.append(row)
|
||||
|
||||
xlsx_buffer = io.BytesIO()
|
||||
workbook.save(xlsx_buffer)
|
||||
xlsx_buffer.seek(0)
|
||||
return xlsx_buffer.getvalue()
|
||||
|
||||
|
||||
def create_zip_file(files):
|
||||
zip_buffer = io.BytesIO()
|
||||
with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zipf:
|
||||
for filename, file_content in files:
|
||||
zipf.writestr(filename, file_content)
|
||||
|
||||
zip_buffer.seek(0)
|
||||
return zip_buffer
|
||||
|
||||
|
||||
def upload_to_s3(zip_file, workspace_id, token_id, slug):
|
||||
file_name = f"{workspace_id}/export-{slug}-{token_id[:6]}-{timezone.now()}.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,
|
||||
)
|
||||
|
||||
exporter_instance = ExporterHistory.objects.get(token=token_id)
|
||||
|
||||
if presigned_url:
|
||||
exporter_instance.url = presigned_url
|
||||
exporter_instance.status = "completed"
|
||||
exporter_instance.key = file_name
|
||||
else:
|
||||
exporter_instance.status = "failed"
|
||||
|
||||
exporter_instance.save(update_fields=["status", "url", "key"])
|
||||
|
||||
|
||||
def generate_table_row(issue):
|
||||
return [
|
||||
f"""{issue["project__identifier"]}-{issue["sequence_id"]}""",
|
||||
issue["project__name"],
|
||||
issue["name"],
|
||||
issue["description_stripped"],
|
||||
issue["state__name"],
|
||||
issue["priority"],
|
||||
f"{issue['created_by__first_name']} {issue['created_by__last_name']}"
|
||||
if issue["created_by__first_name"] and issue["created_by__last_name"]
|
||||
else "",
|
||||
f"{issue['assignees__first_name']} {issue['assignees__last_name']}"
|
||||
if issue["assignees__first_name"] and issue["assignees__last_name"]
|
||||
else "",
|
||||
issue["labels__name"],
|
||||
issue["issue_cycle__cycle__name"],
|
||||
dateConverter(issue["issue_cycle__cycle__start_date"]),
|
||||
dateConverter(issue["issue_cycle__cycle__end_date"]),
|
||||
issue["issue_module__module__name"],
|
||||
dateConverter(issue["issue_module__module__start_date"]),
|
||||
dateConverter(issue["issue_module__module__target_date"]),
|
||||
dateTimeConverter(issue["created_at"]),
|
||||
dateTimeConverter(issue["updated_at"]),
|
||||
dateTimeConverter(issue["completed_at"]),
|
||||
dateTimeConverter(issue["archived_at"]),
|
||||
]
|
||||
|
||||
|
||||
def generate_json_row(issue):
|
||||
return {
|
||||
"ID": f"""{issue["project__identifier"]}-{issue["sequence_id"]}""",
|
||||
"Project": issue["project__name"],
|
||||
"Name": issue["name"],
|
||||
"Description": issue["description_stripped"],
|
||||
"State": issue["state__name"],
|
||||
"Priority": issue["priority"],
|
||||
"Created By": f"{issue['created_by__first_name']} {issue['created_by__last_name']}"
|
||||
if issue["created_by__first_name"] and issue["created_by__last_name"]
|
||||
else "",
|
||||
"Assignee": f"{issue['assignees__first_name']} {issue['assignees__last_name']}"
|
||||
if issue["assignees__first_name"] and issue["assignees__last_name"]
|
||||
else "",
|
||||
"Labels": issue["labels__name"],
|
||||
"Cycle Name": issue["issue_cycle__cycle__name"],
|
||||
"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"]),
|
||||
"Module Target Date": dateConverter(issue["issue_module__module__target_date"]),
|
||||
"Created At": dateTimeConverter(issue["created_at"]),
|
||||
"Updated At": dateTimeConverter(issue["updated_at"]),
|
||||
"Completed At": dateTimeConverter(issue["completed_at"]),
|
||||
"Archived At": dateTimeConverter(issue["archived_at"]),
|
||||
}
|
||||
|
||||
|
||||
def update_json_row(rows, row):
|
||||
matched_index = next(
|
||||
(
|
||||
index
|
||||
for index, existing_row in enumerate(rows)
|
||||
if existing_row["ID"] == row["ID"]
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if matched_index is not None:
|
||||
existing_assignees, existing_labels = (
|
||||
rows[matched_index]["Assignee"],
|
||||
rows[matched_index]["Labels"],
|
||||
)
|
||||
assignee, label = row["Assignee"], row["Labels"]
|
||||
|
||||
if assignee is not None and assignee not in existing_assignees:
|
||||
rows[matched_index]["Assignee"] += f", {assignee}"
|
||||
if label is not None and label not in existing_labels:
|
||||
rows[matched_index]["Labels"] += f", {label}"
|
||||
else:
|
||||
rows.append(row)
|
||||
|
||||
|
||||
def update_table_row(rows, row):
|
||||
matched_index = next(
|
||||
(index for index, existing_row in enumerate(rows) if existing_row[0] == row[0]),
|
||||
None,
|
||||
)
|
||||
|
||||
if matched_index is not None:
|
||||
existing_assignees, existing_labels = rows[matched_index][7:9]
|
||||
assignee, label = row[7:9]
|
||||
|
||||
if assignee is not None and assignee not in existing_assignees:
|
||||
rows[matched_index][7] += f", {assignee}"
|
||||
if label is not None and label not in existing_labels:
|
||||
rows[matched_index][8] += f", {label}"
|
||||
else:
|
||||
rows.append(row)
|
||||
|
||||
|
||||
def generate_csv(header, project_id, issues, files):
|
||||
"""
|
||||
Generate CSV export for all the passed issues.
|
||||
"""
|
||||
rows = [
|
||||
header,
|
||||
]
|
||||
for issue in issues:
|
||||
row = generate_table_row(issue)
|
||||
update_table_row(rows, row)
|
||||
csv_file = create_csv_file(rows)
|
||||
files.append((f"{project_id}.csv", csv_file))
|
||||
|
||||
|
||||
def generate_json(header, project_id, issues, files):
|
||||
rows = []
|
||||
for issue in issues:
|
||||
row = generate_json_row(issue)
|
||||
update_json_row(rows, row)
|
||||
json_file = create_json_file(rows)
|
||||
files.append((f"{project_id}.json", json_file))
|
||||
|
||||
|
||||
def generate_xlsx(header, project_id, issues, files):
|
||||
rows = [header]
|
||||
for issue in issues:
|
||||
row = generate_table_row(issue)
|
||||
update_table_row(rows, row)
|
||||
xlsx_file = create_xlsx_file(rows)
|
||||
files.append((f"{project_id}.xlsx", xlsx_file))
|
||||
|
||||
|
||||
@shared_task
|
||||
def issue_export_task(provider, workspace_id, project_ids, token_id, multiple, slug):
|
||||
try:
|
||||
exporter_instance = ExporterHistory.objects.get(token=token_id)
|
||||
exporter_instance.status = "processing"
|
||||
exporter_instance.save(update_fields=["status"])
|
||||
|
||||
workspace_issues = (
|
||||
(
|
||||
Issue.objects.filter(
|
||||
workspace__id=workspace_id,
|
||||
project_id__in=project_ids,
|
||||
project__project_projectmember__member=exporter_instance.initiated_by_id,
|
||||
)
|
||||
.select_related("project", "workspace", "state", "parent", "created_by")
|
||||
.prefetch_related(
|
||||
"assignees", "labels", "issue_cycle__cycle", "issue_module__module"
|
||||
)
|
||||
.values(
|
||||
"id",
|
||||
"project__identifier",
|
||||
"project__name",
|
||||
"project__id",
|
||||
"sequence_id",
|
||||
"name",
|
||||
"description_stripped",
|
||||
"priority",
|
||||
"state__name",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"completed_at",
|
||||
"archived_at",
|
||||
"issue_cycle__cycle__name",
|
||||
"issue_cycle__cycle__start_date",
|
||||
"issue_cycle__cycle__end_date",
|
||||
"issue_module__module__name",
|
||||
"issue_module__module__start_date",
|
||||
"issue_module__module__target_date",
|
||||
"created_by__first_name",
|
||||
"created_by__last_name",
|
||||
"assignees__first_name",
|
||||
"assignees__last_name",
|
||||
"labels__name",
|
||||
)
|
||||
)
|
||||
.order_by("project__identifier", "sequence_id")
|
||||
.distinct()
|
||||
)
|
||||
# CSV header
|
||||
header = [
|
||||
"ID",
|
||||
"Project",
|
||||
"Name",
|
||||
"Description",
|
||||
"State",
|
||||
"Priority",
|
||||
"Created By",
|
||||
"Assignee",
|
||||
"Labels",
|
||||
"Cycle Name",
|
||||
"Cycle Start Date",
|
||||
"Cycle End Date",
|
||||
"Module Name",
|
||||
"Module Start Date",
|
||||
"Module Target Date",
|
||||
"Created At",
|
||||
"Updated At",
|
||||
"Completed At",
|
||||
"Archived At",
|
||||
]
|
||||
|
||||
EXPORTER_MAPPER = {
|
||||
"csv": generate_csv,
|
||||
"json": generate_json,
|
||||
"xlsx": generate_xlsx,
|
||||
}
|
||||
|
||||
files = []
|
||||
if multiple:
|
||||
for project_id in project_ids:
|
||||
issues = workspace_issues.filter(project__id=project_id)
|
||||
exporter = EXPORTER_MAPPER.get(provider)
|
||||
if exporter is not None:
|
||||
exporter(
|
||||
header,
|
||||
project_id,
|
||||
issues,
|
||||
files,
|
||||
)
|
||||
|
||||
else:
|
||||
exporter = EXPORTER_MAPPER.get(provider)
|
||||
if exporter is not None:
|
||||
exporter(
|
||||
header,
|
||||
workspace_id,
|
||||
workspace_issues,
|
||||
files,
|
||||
)
|
||||
|
||||
zip_buffer = create_zip_file(files)
|
||||
upload_to_s3(zip_buffer, workspace_id, token_id, slug)
|
||||
|
||||
except Exception as e:
|
||||
exporter_instance = ExporterHistory.objects.get(token=token_id)
|
||||
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)
|
||||
capture_exception(e)
|
||||
return
|
||||
@@ -1,49 +0,0 @@
|
||||
# Python imports
|
||||
import boto3
|
||||
from datetime import timedelta
|
||||
|
||||
# Django imports
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
from botocore.client import Config
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import ExporterHistory
|
||||
|
||||
|
||||
@shared_task
|
||||
def delete_old_s3_link():
|
||||
# Get a list of keys and IDs to process
|
||||
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"),
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
ExporterHistory.objects.filter(id=exporter_id).update(url=None)
|
||||
@@ -24,9 +24,6 @@ from plane.db.models import (
|
||||
IssueSubscriber,
|
||||
Notification,
|
||||
IssueAssignee,
|
||||
IssueReaction,
|
||||
CommentReaction,
|
||||
IssueComment,
|
||||
)
|
||||
from plane.api.serializers import IssueActivitySerializer
|
||||
|
||||
@@ -187,24 +184,19 @@ def track_description(
|
||||
if current_instance.get("description_html") != requested_data.get(
|
||||
"description_html"
|
||||
):
|
||||
last_activity = IssueActivity.objects.filter(issue_id=issue_id).order_by("-created_at").first()
|
||||
if(last_activity is not None and last_activity.field == "description" and actor.id == last_activity.actor_id):
|
||||
last_activity.created_at = timezone.now()
|
||||
last_activity.save(update_fields=["created_at"])
|
||||
else:
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("description_html"),
|
||||
new_value=requested_data.get("description_html"),
|
||||
field="description",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the description to {requested_data.get('description_html')}",
|
||||
)
|
||||
)
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
actor=actor,
|
||||
verb="updated",
|
||||
old_value=current_instance.get("description_html"),
|
||||
new_value=requested_data.get("description_html"),
|
||||
field="description",
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"updated the description to {requested_data.get('description_html')}",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
# Track changes in issue target date
|
||||
@@ -393,19 +385,143 @@ def track_assignees(
|
||||
)
|
||||
|
||||
|
||||
# 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
|
||||
):
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
issue_id=issue_id,
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"created the issue",
|
||||
verb="created",
|
||||
actor=actor,
|
||||
)
|
||||
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(
|
||||
@@ -508,16 +624,23 @@ def update_issue_activity(
|
||||
"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,
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
for key in requested_data:
|
||||
func = ISSUE_ACTIVITY_MAPPER.get(key, None)
|
||||
if func is not None:
|
||||
@@ -894,264 +1017,6 @@ def delete_attachment_activity(
|
||||
)
|
||||
)
|
||||
|
||||
def create_issue_reaction_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_issue_reaction_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_comment_reaction_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_comment_reaction_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_issue_vote_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_issue_vote_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_issue_relation_activity(
|
||||
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 = (
|
||||
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"):
|
||||
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("issue"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def delete_issue_relation_activity(
|
||||
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 = (
|
||||
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:
|
||||
issue = Issue.objects.get(pk=current_instance.get("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 the {current_instance.get("relation_type")} relation',
|
||||
old_identifier=current_instance.get("issue"),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def create_draft_issue_activity(
|
||||
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"drafted the issue",
|
||||
field="draft",
|
||||
verb="created",
|
||||
actor=actor,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def update_draft_issue_activity(
|
||||
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 = (
|
||||
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,
|
||||
)
|
||||
)
|
||||
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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
def delete_draft_issue_activity(
|
||||
requested_data, current_instance, issue_id, project, actor, issue_activities
|
||||
):
|
||||
issue_activities.append(
|
||||
IssueActivity(
|
||||
project=project,
|
||||
workspace=project.workspace,
|
||||
comment=f"deleted the draft issue",
|
||||
field="draft",
|
||||
verb="deleted",
|
||||
actor=actor,
|
||||
)
|
||||
)
|
||||
|
||||
# Receive message from room group
|
||||
@shared_task
|
||||
@@ -1175,15 +1040,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_draft.activity.created",
|
||||
"issue_draft.activity.updated",
|
||||
"issue_draft.activity.deleted",
|
||||
]:
|
||||
issue = Issue.objects.filter(pk=issue_id).first()
|
||||
|
||||
@@ -1219,17 +1075,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)
|
||||
@@ -1269,15 +1114,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_draft.activity.created",
|
||||
"issue_draft.activity.updated",
|
||||
"issue_draft.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,22 +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,
|
||||
)
|
||||
for issue in updated_issues
|
||||
]
|
||||
for issue in issues_to_update
|
||||
]
|
||||
return
|
||||
except Exception as e:
|
||||
if settings.DEBUG:
|
||||
@@ -100,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,
|
||||
@@ -137,20 +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,
|
||||
)
|
||||
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:
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
# Python imports
|
||||
import csv
|
||||
import io
|
||||
|
||||
# Django imports
|
||||
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
|
||||
from django.utils import timezone
|
||||
|
||||
# Third party imports
|
||||
from celery import shared_task
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
# Module imports
|
||||
from plane.db.models import Issue
|
||||
|
||||
@shared_task
|
||||
def issue_export_task(email, data, slug, exporter_name):
|
||||
try:
|
||||
|
||||
project_ids = data.get("project_id", [])
|
||||
issues_filter = {"workspace__slug": slug}
|
||||
|
||||
if project_ids:
|
||||
issues_filter["project_id__in"] = project_ids
|
||||
|
||||
issues = (
|
||||
Issue.objects.filter(**issues_filter)
|
||||
.select_related("project", "workspace", "state", "parent", "created_by")
|
||||
.prefetch_related(
|
||||
"assignees", "labels", "issue_cycle__cycle", "issue_module__module"
|
||||
)
|
||||
.values_list(
|
||||
"project__identifier",
|
||||
"sequence_id",
|
||||
"name",
|
||||
"description_stripped",
|
||||
"priority",
|
||||
"start_date",
|
||||
"target_date",
|
||||
"state__name",
|
||||
"project__name",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"completed_at",
|
||||
"archived_at",
|
||||
"issue_cycle__cycle__name",
|
||||
"issue_cycle__cycle__start_date",
|
||||
"issue_cycle__cycle__end_date",
|
||||
"issue_module__module__name",
|
||||
"issue_module__module__start_date",
|
||||
"issue_module__module__target_date",
|
||||
"created_by__first_name",
|
||||
"created_by__last_name",
|
||||
"assignees__first_name",
|
||||
"assignees__last_name",
|
||||
"labels__name",
|
||||
)
|
||||
)
|
||||
|
||||
# CSV header
|
||||
header = [
|
||||
"Issue ID",
|
||||
"Project",
|
||||
"Name",
|
||||
"Description",
|
||||
"State",
|
||||
"Priority",
|
||||
"Created By",
|
||||
"Assignee",
|
||||
"Labels",
|
||||
"Cycle Name",
|
||||
"Cycle Start Date",
|
||||
"Cycle End Date",
|
||||
"Module Name",
|
||||
"Module Start Date",
|
||||
"Module Target Date",
|
||||
"Created At"
|
||||
"Updated At"
|
||||
"Completed At"
|
||||
"Archived At"
|
||||
]
|
||||
|
||||
# Prepare the CSV data
|
||||
rows = [header]
|
||||
|
||||
# Write data for each issue
|
||||
for issue in issues:
|
||||
(
|
||||
project_identifier,
|
||||
sequence_id,
|
||||
name,
|
||||
description,
|
||||
priority,
|
||||
start_date,
|
||||
target_date,
|
||||
state_name,
|
||||
project_name,
|
||||
created_at,
|
||||
updated_at,
|
||||
completed_at,
|
||||
archived_at,
|
||||
cycle_name,
|
||||
cycle_start_date,
|
||||
cycle_end_date,
|
||||
module_name,
|
||||
module_start_date,
|
||||
module_target_date,
|
||||
created_by_first_name,
|
||||
created_by_last_name,
|
||||
assignees_first_names,
|
||||
assignees_last_names,
|
||||
labels_names,
|
||||
) = issue
|
||||
|
||||
created_by_fullname = (
|
||||
f"{created_by_first_name} {created_by_last_name}"
|
||||
if created_by_first_name and created_by_last_name
|
||||
else ""
|
||||
)
|
||||
|
||||
assignees_names = ""
|
||||
if assignees_first_names and assignees_last_names:
|
||||
assignees_names = ", ".join(
|
||||
[
|
||||
f"{assignees_first_name} {assignees_last_name}"
|
||||
for assignees_first_name, assignees_last_name in zip(
|
||||
assignees_first_names, assignees_last_names
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
labels_names = ", ".join(labels_names) if labels_names else ""
|
||||
|
||||
row = [
|
||||
f"{project_identifier}-{sequence_id}",
|
||||
project_name,
|
||||
name,
|
||||
description,
|
||||
state_name,
|
||||
priority,
|
||||
created_by_fullname,
|
||||
assignees_names,
|
||||
labels_names,
|
||||
cycle_name,
|
||||
cycle_start_date,
|
||||
cycle_end_date,
|
||||
module_name,
|
||||
module_start_date,
|
||||
module_target_date,
|
||||
start_date,
|
||||
target_date,
|
||||
created_at,
|
||||
updated_at,
|
||||
completed_at,
|
||||
archived_at,
|
||||
]
|
||||
rows.append(row)
|
||||
|
||||
# Create CSV file in-memory
|
||||
csv_buffer = io.StringIO()
|
||||
writer = csv.writer(csv_buffer, delimiter=",", quoting=csv.QUOTE_ALL)
|
||||
|
||||
# Write CSV data to the buffer
|
||||
for row in rows:
|
||||
writer.writerow(row)
|
||||
|
||||
subject = "Your Issue Export is ready"
|
||||
|
||||
context = {
|
||||
"username": exporter_name,
|
||||
}
|
||||
|
||||
html_content = render_to_string("emails/exports/issues.html", context)
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
csv_buffer.seek(0)
|
||||
msg = EmailMultiAlternatives(
|
||||
subject, text_content, settings.EMAIL_FROM, [email]
|
||||
)
|
||||
msg.attach(f"{slug}-issues-{timezone.now().date()}.csv", csv_buffer.read(), "text/csv")
|
||||
msg.send(fail_silently=False)
|
||||
|
||||
except Exception as e:
|
||||
# Print logs if in DEBUG mode
|
||||
if settings.DEBUG:
|
||||
print(e)
|
||||
capture_exception(e)
|
||||
return
|
||||
@@ -20,10 +20,6 @@ app.conf.beat_schedule = {
|
||||
"task": "plane.bgtasks.issue_automation_task.archive_and_close_old_issues",
|
||||
"schedule": crontab(hour=0, minute=0),
|
||||
},
|
||||
"check-every-day-to-delete_exporter_history": {
|
||||
"task": "plane.bgtasks.exporter_expired_task.delete_old_s3_link",
|
||||
"schedule": crontab(hour=0, minute=0),
|
||||
},
|
||||
}
|
||||
|
||||
# Load task modules from all registered Django app configs.
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
# Generated by Django 4.2.3 on 2023-08-14 07:12
|
||||
|
||||
from django.conf import settings
|
||||
import django.contrib.postgres.fields
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import plane.db.models.exporter
|
||||
import plane.db.models.project
|
||||
import uuid
|
||||
import random
|
||||
import string
|
||||
|
||||
def generate_display_name(apps, schema_editor):
|
||||
UserModel = apps.get_model("db", "User")
|
||||
updated_users = []
|
||||
for obj in UserModel.objects.all():
|
||||
obj.display_name = (
|
||||
obj.email.split("@")[0]
|
||||
if len(obj.email.split("@"))
|
||||
else "".join(random.choice(string.ascii_letters) for _ in range(6))
|
||||
)
|
||||
updated_users.append(obj)
|
||||
UserModel.objects.bulk_update(updated_users, ["display_name"], batch_size=100)
|
||||
|
||||
|
||||
def rectify_field_issue_activity(apps, schema_editor):
|
||||
Model = apps.get_model("db", "IssueActivity")
|
||||
updated_activity = []
|
||||
for obj in Model.objects.filter(field="assignee"):
|
||||
obj.field = "assignees"
|
||||
updated_activity.append(obj)
|
||||
|
||||
Model.objects.bulk_update(updated_activity, ["field"], batch_size=100)
|
||||
|
||||
|
||||
def update_assignee_issue_activity(apps, schema_editor):
|
||||
Model = apps.get_model("db", "IssueActivity")
|
||||
updated_activity = []
|
||||
|
||||
# Get all the users
|
||||
User = apps.get_model("db", "User")
|
||||
users = User.objects.values("id", "email", "display_name")
|
||||
|
||||
for obj in Model.objects.filter(field="assignees"):
|
||||
if bool(obj.new_value) and not bool(obj.old_value):
|
||||
# Get user from list
|
||||
assigned_user = [
|
||||
user for user in users if user.get("email") == obj.new_value
|
||||
]
|
||||
if assigned_user:
|
||||
obj.new_value = assigned_user[0].get("display_name")
|
||||
obj.new_identifier = assigned_user[0].get("id")
|
||||
# Update the comment
|
||||
words = obj.comment.split()
|
||||
words[-1] = assigned_user[0].get("display_name")
|
||||
obj.comment = " ".join(words)
|
||||
|
||||
if bool(obj.old_value) and not bool(obj.new_value):
|
||||
# Get user from list
|
||||
assigned_user = [
|
||||
user for user in users if user.get("email") == obj.old_value
|
||||
]
|
||||
if assigned_user:
|
||||
obj.old_value = assigned_user[0].get("display_name")
|
||||
obj.old_identifier = assigned_user[0].get("id")
|
||||
# Update the comment
|
||||
words = obj.comment.split()
|
||||
words[-1] = assigned_user[0].get("display_name")
|
||||
obj.comment = " ".join(words)
|
||||
|
||||
updated_activity.append(obj)
|
||||
|
||||
Model.objects.bulk_update(
|
||||
updated_activity,
|
||||
["old_value", "new_value", "old_identifier", "new_identifier", "comment"],
|
||||
batch_size=200,
|
||||
)
|
||||
|
||||
|
||||
def update_name_activity(apps, schema_editor):
|
||||
Model = apps.get_model("db", "IssueActivity")
|
||||
update_activity = []
|
||||
for obj in Model.objects.filter(field="name"):
|
||||
obj.comment = obj.comment.replace("start date", "name")
|
||||
update_activity.append(obj)
|
||||
|
||||
Model.objects.bulk_update(update_activity, ["comment"], batch_size=1000)
|
||||
|
||||
|
||||
def random_cycle_order(apps, schema_editor):
|
||||
CycleModel = apps.get_model("db", "Cycle")
|
||||
updated_cycles = []
|
||||
for obj in CycleModel.objects.all():
|
||||
obj.sort_order = random.randint(1, 65536)
|
||||
updated_cycles.append(obj)
|
||||
CycleModel.objects.bulk_update(updated_cycles, ["sort_order"], batch_size=100)
|
||||
|
||||
|
||||
def random_module_order(apps, schema_editor):
|
||||
ModuleModel = apps.get_model("db", "Module")
|
||||
updated_modules = []
|
||||
for obj in ModuleModel.objects.all():
|
||||
obj.sort_order = random.randint(1, 65536)
|
||||
updated_modules.append(obj)
|
||||
ModuleModel.objects.bulk_update(updated_modules, ["sort_order"], batch_size=100)
|
||||
|
||||
|
||||
def update_user_issue_properties(apps, schema_editor):
|
||||
IssuePropertyModel = apps.get_model("db", "IssueProperty")
|
||||
updated_issue_properties = []
|
||||
for obj in IssuePropertyModel.objects.all():
|
||||
obj.properties["start_date"] = True
|
||||
updated_issue_properties.append(obj)
|
||||
IssuePropertyModel.objects.bulk_update(
|
||||
updated_issue_properties, ["properties"], batch_size=100
|
||||
)
|
||||
|
||||
|
||||
def workspace_member_properties(apps, schema_editor):
|
||||
WorkspaceMemberModel = apps.get_model("db", "WorkspaceMember")
|
||||
updated_workspace_members = []
|
||||
for obj in WorkspaceMemberModel.objects.all():
|
||||
obj.view_props["properties"]["start_date"] = True
|
||||
obj.default_props["properties"]["start_date"] = True
|
||||
updated_workspace_members.append(obj)
|
||||
|
||||
WorkspaceMemberModel.objects.bulk_update(
|
||||
updated_workspace_members, ["view_props", "default_props"], batch_size=100
|
||||
)
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0040_projectmember_preferences_user_cover_image_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cycle',
|
||||
name='sort_order',
|
||||
field=models.FloatField(default=65535),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issuecomment',
|
||||
name='access',
|
||||
field=models.CharField(choices=[('INTERNAL', 'INTERNAL'), ('EXTERNAL', 'EXTERNAL')], default='INTERNAL', max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='module',
|
||||
name='sort_order',
|
||||
field=models.FloatField(default=65535),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='display_name',
|
||||
field=models.CharField(default='', max_length=255),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ExporterHistory',
|
||||
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)),
|
||||
('project', django.contrib.postgres.fields.ArrayField(base_field=models.UUIDField(default=uuid.uuid4), blank=True, null=True, size=None)),
|
||||
('provider', models.CharField(choices=[('json', 'json'), ('csv', 'csv'), ('xlsx', 'xlsx')], max_length=50)),
|
||||
('status', models.CharField(choices=[('queued', 'Queued'), ('processing', 'Processing'), ('completed', 'Completed'), ('failed', 'Failed')], default='queued', max_length=50)),
|
||||
('reason', models.TextField(blank=True)),
|
||||
('key', models.TextField(blank=True)),
|
||||
('url', models.URLField(blank=True, max_length=800, null=True)),
|
||||
('token', models.CharField(default=plane.db.models.exporter.generate_token, max_length=255, unique=True)),
|
||||
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
|
||||
('initiated_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_exporters', to=settings.AUTH_USER_MODEL)),
|
||||
('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_exporters', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Exporter',
|
||||
'verbose_name_plural': 'Exporters',
|
||||
'db_table': 'exporters',
|
||||
'ordering': ('-created_at',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProjectDeployBoard',
|
||||
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)),
|
||||
('anchor', models.CharField(db_index=True, default=plane.db.models.project.get_anchor, max_length=255, unique=True)),
|
||||
('comments', models.BooleanField(default=False)),
|
||||
('reactions', models.BooleanField(default=False)),
|
||||
('votes', models.BooleanField(default=False)),
|
||||
('views', models.JSONField(default=plane.db.models.project.get_default_views)),
|
||||
('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')),
|
||||
('inbox', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='bord_inbox', to='db.inbox')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Project Deploy Board',
|
||||
'verbose_name_plural': 'Project Deploy Boards',
|
||||
'db_table': 'project_deploy_boards',
|
||||
'ordering': ('-created_at',),
|
||||
'unique_together': {('project', 'anchor')},
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='IssueVote',
|
||||
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)),
|
||||
('vote', models.IntegerField(choices=[(-1, 'DOWNVOTE'), (1, 'UPVOTE')])),
|
||||
('actor', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='votes', to=settings.AUTH_USER_MODEL)),
|
||||
('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='votes', to='db.issue')),
|
||||
('project', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='project_%(class)s', to='db.project')),
|
||||
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
|
||||
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_%(class)s', to='db.workspace')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Issue Vote',
|
||||
'verbose_name_plural': 'Issue Votes',
|
||||
'db_table': 'issue_votes',
|
||||
'ordering': ('-created_at',),
|
||||
'unique_together': {('issue', 'actor')},
|
||||
},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='modulelink',
|
||||
name='title',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
migrations.RunPython(generate_display_name),
|
||||
migrations.RunPython(rectify_field_issue_activity),
|
||||
migrations.RunPython(update_assignee_issue_activity),
|
||||
migrations.RunPython(update_name_activity),
|
||||
migrations.RunPython(random_cycle_order),
|
||||
migrations.RunPython(random_module_order),
|
||||
migrations.RunPython(update_user_issue_properties),
|
||||
migrations.RunPython(workspace_member_properties),
|
||||
]
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
# Generated by Django 4.2.3 on 2023-08-04 09:12
|
||||
import string
|
||||
import random
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
def generate_display_name(apps, schema_editor):
|
||||
UserModel = apps.get_model("db", "User")
|
||||
updated_users = []
|
||||
for obj in UserModel.objects.all():
|
||||
obj.display_name = (
|
||||
obj.email.split("@")[0]
|
||||
if len(obj.email.split("@"))
|
||||
else "".join(random.choice(string.ascii_letters) for _ in range(6))
|
||||
)
|
||||
updated_users.append(obj)
|
||||
UserModel.objects.bulk_update(updated_users, ["display_name"], batch_size=100)
|
||||
|
||||
|
||||
def rectify_field_issue_activity(apps, schema_editor):
|
||||
Model = apps.get_model("db", "IssueActivity")
|
||||
updated_activity = []
|
||||
for obj in Model.objects.filter(field="assignee"):
|
||||
obj.field = "assignees"
|
||||
updated_activity.append(obj)
|
||||
|
||||
Model.objects.bulk_update(updated_activity, ["field"], batch_size=100)
|
||||
|
||||
|
||||
def update_assignee_issue_activity(apps, schema_editor):
|
||||
Model = apps.get_model("db", "IssueActivity")
|
||||
updated_activity = []
|
||||
|
||||
# Get all the users
|
||||
User = apps.get_model("db", "User")
|
||||
users = User.objects.values("id", "email", "display_name")
|
||||
|
||||
for obj in Model.objects.filter(field="assignees"):
|
||||
if bool(obj.new_value) and not bool(obj.old_value):
|
||||
# Get user from list
|
||||
assigned_user = [
|
||||
user for user in users if user.get("email") == obj.new_value
|
||||
]
|
||||
if assigned_user:
|
||||
obj.new_value = assigned_user[0].get("display_name")
|
||||
obj.new_identifier = assigned_user[0].get("id")
|
||||
# Update the comment
|
||||
words = obj.comment.split()
|
||||
words[-1] = assigned_user[0].get("display_name")
|
||||
obj.comment = " ".join(words)
|
||||
|
||||
if bool(obj.old_value) and not bool(obj.new_value):
|
||||
# Get user from list
|
||||
assigned_user = [
|
||||
user for user in users if user.get("email") == obj.old_value
|
||||
]
|
||||
if assigned_user:
|
||||
obj.old_value = assigned_user[0].get("display_name")
|
||||
obj.old_identifier = assigned_user[0].get("id")
|
||||
# Update the comment
|
||||
words = obj.comment.split()
|
||||
words[-1] = assigned_user[0].get("display_name")
|
||||
obj.comment = " ".join(words)
|
||||
|
||||
updated_activity.append(obj)
|
||||
|
||||
Model.objects.bulk_update(
|
||||
updated_activity,
|
||||
["old_value", "new_value", "old_identifier", "new_identifier", "comment"],
|
||||
batch_size=200,
|
||||
)
|
||||
|
||||
|
||||
def update_name_activity(apps, schema_editor):
|
||||
Model = apps.get_model("db", "IssueActivity")
|
||||
update_activity = []
|
||||
for obj in Model.objects.filter(field="name"):
|
||||
obj.comment = obj.comment.replace("start date", "name")
|
||||
update_activity.append(obj)
|
||||
|
||||
Model.objects.bulk_update(update_activity, ["comment"], batch_size=1000)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("db", "0040_projectmember_preferences_user_cover_image_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="display_name",
|
||||
field=models.CharField(default="", max_length=255),
|
||||
),
|
||||
migrations.RunPython(generate_display_name),
|
||||
migrations.RunPython(rectify_field_issue_activity),
|
||||
migrations.RunPython(update_assignee_issue_activity),
|
||||
migrations.RunPython(update_name_activity),
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
@@ -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),
|
||||
]
|
||||
@@ -18,8 +18,6 @@ from .project import (
|
||||
ProjectMemberInvite,
|
||||
ProjectIdentifier,
|
||||
ProjectFavorite,
|
||||
ProjectDeployBoard,
|
||||
ProjectPublicMember,
|
||||
)
|
||||
|
||||
from .issue import (
|
||||
@@ -32,14 +30,12 @@ from .issue import (
|
||||
IssueAssignee,
|
||||
Label,
|
||||
IssueBlocker,
|
||||
IssueRelation,
|
||||
IssueLink,
|
||||
IssueSequence,
|
||||
IssueAttachment,
|
||||
IssueSubscriber,
|
||||
IssueReaction,
|
||||
CommentReaction,
|
||||
IssueVote,
|
||||
)
|
||||
|
||||
from .asset import FileAsset
|
||||
@@ -76,6 +72,4 @@ from .inbox import Inbox, InboxIssue
|
||||
|
||||
from .analytic import AnalyticView
|
||||
|
||||
from .notification import Notification
|
||||
|
||||
from .exporter import ExporterHistory
|
||||
from .notification import Notification
|
||||
@@ -17,7 +17,6 @@ class Cycle(ProjectBaseModel):
|
||||
related_name="owned_by_cycle",
|
||||
)
|
||||
view_props = models.JSONField(default=dict)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Cycle"
|
||||
@@ -25,17 +24,6 @@ class Cycle(ProjectBaseModel):
|
||||
db_table = "cycles"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self._state.adding:
|
||||
smallest_sort_order = Cycle.objects.filter(
|
||||
project=self.project
|
||||
).aggregate(smallest=models.Min("sort_order"))["smallest"]
|
||||
|
||||
if smallest_sort_order is not None:
|
||||
self.sort_order = smallest_sort_order - 10000
|
||||
|
||||
super(Cycle, self).save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the cycle"""
|
||||
return f"{self.name} <{self.project.name}>"
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import uuid
|
||||
|
||||
# Python imports
|
||||
from uuid import uuid4
|
||||
|
||||
# Django imports
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
|
||||
# Module imports
|
||||
from . import BaseModel
|
||||
|
||||
def generate_token():
|
||||
return uuid4().hex
|
||||
|
||||
class ExporterHistory(BaseModel):
|
||||
workspace = models.ForeignKey(
|
||||
"db.WorkSpace", on_delete=models.CASCADE, related_name="workspace_exporters"
|
||||
)
|
||||
project = ArrayField(models.UUIDField(default=uuid.uuid4), blank=True, null=True)
|
||||
provider = models.CharField(
|
||||
max_length=50,
|
||||
choices=(
|
||||
("json", "json"),
|
||||
("csv", "csv"),
|
||||
("xlsx", "xlsx"),
|
||||
),
|
||||
)
|
||||
status = models.CharField(
|
||||
max_length=50,
|
||||
choices=(
|
||||
("queued", "Queued"),
|
||||
("processing", "Processing"),
|
||||
("completed", "Completed"),
|
||||
("failed", "Failed"),
|
||||
),
|
||||
default="queued",
|
||||
)
|
||||
reason = models.TextField(blank=True)
|
||||
key = models.TextField(blank=True)
|
||||
url = models.URLField(max_length=800, blank=True, null=True)
|
||||
token = models.CharField(max_length=255, default=generate_token, unique=True)
|
||||
initiated_by = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="workspace_exporters"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Exporter"
|
||||
verbose_name_plural = "Exporters"
|
||||
db_table = "exporters"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the service"""
|
||||
return f"{self.provider} <{self.workspace.name}>"
|
||||
@@ -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()
|
||||
@@ -110,7 +108,11 @@ class Issue(ProjectBaseModel):
|
||||
~models.Q(name="Triage"), project=self.project
|
||||
).first()
|
||||
self.state = random_state
|
||||
if random_state.group == "started":
|
||||
self.start_date = timezone.now().date()
|
||||
else:
|
||||
if default_state.group == "started":
|
||||
self.start_date = timezone.now().date()
|
||||
self.state = default_state
|
||||
except ImportError:
|
||||
pass
|
||||
@@ -125,6 +127,8 @@ class Issue(ProjectBaseModel):
|
||||
PageBlock.objects.filter(issue_id=self.id).filter().update(
|
||||
completed_at=timezone.now()
|
||||
)
|
||||
elif self.state.group == "started":
|
||||
self.start_date = timezone.now().date()
|
||||
else:
|
||||
PageBlock.objects.filter(issue_id=self.id).filter().update(
|
||||
completed_at=None
|
||||
@@ -149,6 +153,9 @@ class Issue(ProjectBaseModel):
|
||||
if largest_sort_order is not None:
|
||||
self.sort_order = largest_sort_order + 10000
|
||||
|
||||
# If adding it to started state
|
||||
if self.state.group == "started":
|
||||
self.start_date = timezone.now().date()
|
||||
# Strip the html tags using html parser
|
||||
self.description_stripped = (
|
||||
None
|
||||
@@ -180,37 +187,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"
|
||||
@@ -326,7 +302,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,
|
||||
@@ -334,14 +310,6 @@ class IssueComment(ProjectBaseModel):
|
||||
related_name="comments",
|
||||
null=True,
|
||||
)
|
||||
access = models.CharField(
|
||||
choices=(
|
||||
("INTERNAL", "INTERNAL"),
|
||||
("EXTERNAL", "EXTERNAL"),
|
||||
),
|
||||
default="INTERNAL",
|
||||
max_length=100,
|
||||
)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.comment_stripped = (
|
||||
@@ -457,14 +425,13 @@ class IssueSubscriber(ProjectBaseModel):
|
||||
|
||||
|
||||
class IssueReaction(ProjectBaseModel):
|
||||
|
||||
actor = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="issue_reactions",
|
||||
)
|
||||
issue = models.ForeignKey(
|
||||
Issue, on_delete=models.CASCADE, related_name="issue_reactions"
|
||||
)
|
||||
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="issue_reactions")
|
||||
reaction = models.CharField(max_length=20)
|
||||
|
||||
class Meta:
|
||||
@@ -479,14 +446,13 @@ class IssueReaction(ProjectBaseModel):
|
||||
|
||||
|
||||
class CommentReaction(ProjectBaseModel):
|
||||
|
||||
actor = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="comment_reactions",
|
||||
)
|
||||
comment = models.ForeignKey(
|
||||
IssueComment, on_delete=models.CASCADE, related_name="comment_reactions"
|
||||
)
|
||||
comment = models.ForeignKey(IssueComment, on_delete=models.CASCADE, related_name="comment_reactions")
|
||||
reaction = models.CharField(max_length=20)
|
||||
|
||||
class Meta:
|
||||
@@ -500,29 +466,6 @@ class CommentReaction(ProjectBaseModel):
|
||||
return f"{self.issue.name} {self.actor.email}"
|
||||
|
||||
|
||||
class IssueVote(ProjectBaseModel):
|
||||
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name="votes")
|
||||
actor = models.ForeignKey(
|
||||
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="votes"
|
||||
)
|
||||
vote = models.IntegerField(
|
||||
choices=(
|
||||
(-1, "DOWNVOTE"),
|
||||
(1, "UPVOTE"),
|
||||
),
|
||||
default=1,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["issue", "actor",]
|
||||
verbose_name = "Issue Vote"
|
||||
verbose_name_plural = "Issue Votes"
|
||||
db_table = "issue_votes"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.issue.name} {self.actor.email}"
|
||||
|
||||
|
||||
# TODO: Find a better method to save the model
|
||||
@receiver(post_save, sender=Issue)
|
||||
|
||||
@@ -40,7 +40,6 @@ class Module(ProjectBaseModel):
|
||||
through_fields=("module", "member"),
|
||||
)
|
||||
view_props = models.JSONField(default=dict)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["name", "project"]
|
||||
@@ -49,17 +48,6 @@ class Module(ProjectBaseModel):
|
||||
db_table = "modules"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self._state.adding:
|
||||
smallest_sort_order = Module.objects.filter(
|
||||
project=self.project
|
||||
).aggregate(smallest=models.Min("sort_order"))["smallest"]
|
||||
|
||||
if smallest_sort_order is not None:
|
||||
self.sort_order = smallest_sort_order - 10000
|
||||
|
||||
super(Module, self).save(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} {self.start_date} {self.target_date}"
|
||||
|
||||
@@ -98,7 +86,7 @@ class ModuleIssue(ProjectBaseModel):
|
||||
|
||||
|
||||
class ModuleLink(ProjectBaseModel):
|
||||
title = models.CharField(max_length=255, blank=True, null=True)
|
||||
title = models.CharField(max_length=255, null=True)
|
||||
url = models.URLField()
|
||||
module = models.ForeignKey(
|
||||
Module, on_delete=models.CASCADE, related_name="link_module"
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
# Python imports
|
||||
from uuid import uuid4
|
||||
|
||||
# Django imports
|
||||
from django.db import models
|
||||
from django.conf import settings
|
||||
@@ -25,31 +22,21 @@ 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,
|
||||
}
|
||||
|
||||
|
||||
def get_default_preferences():
|
||||
return {"pages": {"block_display": True}}
|
||||
return {
|
||||
"pages": {
|
||||
"block_display": True
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Project(BaseModel):
|
||||
@@ -170,6 +157,7 @@ class ProjectMember(ProjectBaseModel):
|
||||
preferences = models.JSONField(default=get_default_preferences)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self._state.adding:
|
||||
smallest_sort_order = ProjectMember.objects.filter(
|
||||
@@ -229,56 +217,3 @@ class ProjectFavorite(ProjectBaseModel):
|
||||
def __str__(self):
|
||||
"""Return user of the project"""
|
||||
return f"{self.user.email} <{self.project.name}>"
|
||||
|
||||
|
||||
def get_anchor():
|
||||
return uuid4().hex
|
||||
|
||||
|
||||
def get_default_views():
|
||||
return {
|
||||
"list": True,
|
||||
"kanban": True,
|
||||
"calendar": True,
|
||||
"gantt": True,
|
||||
"spreadsheet": True,
|
||||
}
|
||||
|
||||
|
||||
class ProjectDeployBoard(ProjectBaseModel):
|
||||
anchor = models.CharField(
|
||||
max_length=255, default=get_anchor, unique=True, db_index=True
|
||||
)
|
||||
comments = models.BooleanField(default=False)
|
||||
reactions = models.BooleanField(default=False)
|
||||
inbox = models.ForeignKey(
|
||||
"db.Inbox", related_name="bord_inbox", on_delete=models.SET_NULL, null=True
|
||||
)
|
||||
votes = models.BooleanField(default=False)
|
||||
views = models.JSONField(default=get_default_views)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["project", "anchor"]
|
||||
verbose_name = "Project Deploy Board"
|
||||
verbose_name_plural = "Project Deploy Boards"
|
||||
db_table = "project_deploy_boards"
|
||||
ordering = ("-created_at",)
|
||||
|
||||
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",)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -16,41 +16,25 @@ 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,
|
||||
}
|
||||
},
|
||||
"showEmptyGroups": True,
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -214,4 +214,4 @@ SIMPLE_JWT = {
|
||||
CELERY_TIMEZONE = TIME_ZONE
|
||||
CELERY_TASK_SERIALIZER = 'json'
|
||||
CELERY_ACCEPT_CONTENT = ['application/json']
|
||||
CELERY_IMPORTS = ("plane.bgtasks.issue_automation_task","plane.bgtasks.exporter_expired_task")
|
||||
CELERY_IMPORTS = ("plane.bgtasks.issue_automation_task",)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -109,11 +124,10 @@ def filter_created_at(params, filter, method):
|
||||
else:
|
||||
if params.get("created_at", None) and len(params.get("created_at")):
|
||||
for query in params.get("created_at"):
|
||||
created_at_query = query.split(";")
|
||||
if len(created_at_query) == 2 and "after" in created_at_query:
|
||||
filter["created_at__date__gte"] = created_at_query[0]
|
||||
if query.get("timeline", "after") == "after":
|
||||
filter["created_at__date__gte"] = query.get("datetime")
|
||||
else:
|
||||
filter["created_at__date__lte"] = created_at_query[0]
|
||||
filter["created_at__date__lte"] = query.get("datetime")
|
||||
return filter
|
||||
|
||||
|
||||
@@ -130,11 +144,10 @@ def filter_updated_at(params, filter, method):
|
||||
else:
|
||||
if params.get("updated_at", None) and len(params.get("updated_at")):
|
||||
for query in params.get("updated_at"):
|
||||
updated_at_query = query.split(";")
|
||||
if len(updated_at_query) == 2 and "after" in updated_at_query:
|
||||
filter["updated_at__date__gte"] = updated_at_query[0]
|
||||
if query.get("timeline", "after") == "after":
|
||||
filter["updated_at__date__gte"] = query.get("datetime")
|
||||
else:
|
||||
filter["updated_at__date__lte"] = updated_at_query[0]
|
||||
filter["updated_at__date__lte"] = query.get("datetime")
|
||||
return filter
|
||||
|
||||
|
||||
@@ -151,11 +164,10 @@ def filter_start_date(params, filter, method):
|
||||
else:
|
||||
if params.get("start_date", None) and len(params.get("start_date")):
|
||||
for query in params.get("start_date"):
|
||||
start_date_query = query.split(";")
|
||||
if len(start_date_query) == 2 and "after" in start_date_query:
|
||||
filter["start_date__gte"] = start_date_query[0]
|
||||
if query.get("timeline", "after") == "after":
|
||||
filter["start_date__gte"] = query.get("datetime")
|
||||
else:
|
||||
filter["start_date__lte"] = start_date_query[0]
|
||||
filter["start_date__lte"] = query.get("datetime")
|
||||
return filter
|
||||
|
||||
|
||||
@@ -172,11 +184,10 @@ def filter_target_date(params, filter, method):
|
||||
else:
|
||||
if params.get("target_date", None) and len(params.get("target_date")):
|
||||
for query in params.get("target_date"):
|
||||
target_date_query = query.split(";")
|
||||
if len(target_date_query) == 2 and "after" in target_date_query:
|
||||
filter["target_date__gt"] = target_date_query[0]
|
||||
if query.get("timeline", "after") == "after":
|
||||
filter["target_date__gt"] = query.get("datetime")
|
||||
else:
|
||||
filter["target_date__lt"] = target_date_query[0]
|
||||
filter["target_date__lt"] = query.get("datetime")
|
||||
|
||||
return filter
|
||||
|
||||
@@ -194,11 +205,10 @@ def filter_completed_at(params, filter, method):
|
||||
else:
|
||||
if params.get("completed_at", None) and len(params.get("completed_at")):
|
||||
for query in params.get("completed_at"):
|
||||
completed_at_query = query.split(";")
|
||||
if len(completed_at_query) == 2 and "after" in completed_at_query:
|
||||
filter["completed_at__date__gte"] = completed_at_query[0]
|
||||
if query.get("timeline", "after") == "after":
|
||||
filter["completed_at__date__gte"] = query.get("datetime")
|
||||
else:
|
||||
filter["completed_at__lte"] = completed_at_query[0]
|
||||
filter["completed_at__lte"] = query.get("datetime")
|
||||
return filter
|
||||
|
||||
|
||||
@@ -214,6 +224,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(",")
|
||||
@@ -281,16 +292,9 @@ def filter_subscribed_issues(params, filter, method):
|
||||
return filter
|
||||
|
||||
|
||||
def filter_start_target_date_issues(params, filter, method):
|
||||
start_target_date = params.get("start_target_date", "false")
|
||||
if start_target_date == "true":
|
||||
filter["target_date__isnull"] = False
|
||||
filter["start_date__isnull"] = False
|
||||
return filter
|
||||
|
||||
|
||||
def issue_filters(query_params, method):
|
||||
filter = dict()
|
||||
print(query_params)
|
||||
|
||||
ISSUE_FILTER = {
|
||||
"state": filter_state,
|
||||
@@ -313,8 +317,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,
|
||||
"start_target_date": filter_start_target_date_issues,
|
||||
"subscriber": filter_subscribed_issues,
|
||||
}
|
||||
|
||||
for key, value in ISSUE_FILTER.items():
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
# 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
|
||||
scout-apm==2.26.1
|
||||
openpyxl==3.1.2
|
||||
psycopg-binary==3.1.9
|
||||
psycopg-c==3.1.9
|
||||
scout-apm==2.26.1
|
||||
@@ -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 +1 @@
|
||||
python-3.11.5
|
||||
python-3.11.4
|
||||
@@ -1,4 +1,7 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["custom"],
|
||||
rules: {
|
||||
"@next/next/no-img-element": "off",
|
||||
},
|
||||
};
|
||||
@@ -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
|
||||
|
||||
RUN /usr/local/bin/replace-env-vars.sh http://NEXT_PUBLIC_WEBAPP_URL_PLACEHOLDER ${NEXT_PUBLIC_API_BASE_URL}
|
||||
|
||||
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
|
||||
+4
-4
@@ -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"
|
||||
+3
-3
@@ -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>
|
||||
+4
-22
@@ -1,8 +1,3 @@
|
||||
// ui
|
||||
import { ProfileEmptyState } from "components/ui";
|
||||
// image
|
||||
import emptyUsers from "public/empty-state/empty_users.svg";
|
||||
|
||||
type Props = {
|
||||
users: {
|
||||
avatar: string | null;
|
||||
@@ -10,29 +5,18 @@ type Props = {
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
count: number;
|
||||
id: string;
|
||||
}[];
|
||||
title: string;
|
||||
emptyStateMessage: string;
|
||||
workspaceSlug: string;
|
||||
};
|
||||
|
||||
export const AnalyticsLeaderboard: React.FC<Props> = ({
|
||||
users,
|
||||
title,
|
||||
emptyStateMessage,
|
||||
workspaceSlug,
|
||||
}) => (
|
||||
export const AnalyticsLeaderboard: React.FC<Props> = ({ users, title }) => (
|
||||
<div className="p-3 border border-custom-border-200 rounded-[10px]">
|
||||
<h6 className="text-base font-medium">{title}</h6>
|
||||
{users.length > 0 ? (
|
||||
<div className="mt-3 space-y-3">
|
||||
{users.map((user) => (
|
||||
<a
|
||||
<div
|
||||
key={user.display_name ?? "None"}
|
||||
href={`/${workspaceSlug}/profile/${user.id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-start justify-between gap-4 text-xs"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -54,13 +38,11 @@ export const AnalyticsLeaderboard: React.FC<Props> = ({
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex-shrink-0">{user.count}</span>
|
||||
</a>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="px-7 py-4">
|
||||
<ProfileEmptyState title="No Data yet" description={emptyStateMessage} image={emptyUsers} />
|
||||
</div>
|
||||
<div className="text-custom-text-200 text-center text-sm py-8">No matching data found.</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
-6
@@ -60,11 +60,8 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
|
||||
lastName: user?.created_by__last_name,
|
||||
display_name: user?.created_by__display_name,
|
||||
count: user?.count,
|
||||
id: user?.created_by__id,
|
||||
}))}
|
||||
title="Most issues created"
|
||||
emptyStateMessage="Co-workers and the number issues created by them appears here."
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
/>
|
||||
<AnalyticsLeaderboard
|
||||
users={defaultAnalytics.most_issue_closed_user?.map((user) => ({
|
||||
@@ -73,11 +70,8 @@ export const ScopeAndDemand: React.FC<Props> = ({ fullScreen = true }) => {
|
||||
lastName: user?.assignees__last_name,
|
||||
display_name: user?.assignees__display_name,
|
||||
count: user?.count,
|
||||
id: user?.assignees__id,
|
||||
}))}
|
||||
title="Most issues closed"
|
||||
emptyStateMessage="Co-workers and the number issues closed by them appears here."
|
||||
workspaceSlug={workspaceSlug?.toString() ?? ""}
|
||||
/>
|
||||
<div className={fullScreen ? "md:col-span-2" : ""}>
|
||||
<AnalyticsYearWiseIssues defaultAnalytics={defaultAnalytics} />
|
||||
+3
-9
@@ -1,7 +1,5 @@
|
||||
// ui
|
||||
import { BarGraph, ProfileEmptyState } from "components/ui";
|
||||
// image
|
||||
import emptyBarGraph from "public/empty-state/empty_bar_graph.svg";
|
||||
import { BarGraph } from "components/ui";
|
||||
// types
|
||||
import { IDefaultAnalyticsResponse } from "types";
|
||||
|
||||
@@ -72,12 +70,8 @@ export const AnalyticsScope: React.FC<Props> = ({ defaultAnalytics }) => (
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<div className="px-7 py-4">
|
||||
<ProfileEmptyState
|
||||
title="No Data yet"
|
||||
description="Analysis of pending issues by co-workers appears here."
|
||||
image={emptyBarGraph}
|
||||
/>
|
||||
<div className="text-custom-text-200 text-center text-sm py-8">
|
||||
No matching data found.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
+2
-10
@@ -1,7 +1,5 @@
|
||||
// ui
|
||||
import { LineGraph, ProfileEmptyState } from "components/ui";
|
||||
// image
|
||||
import emptyGraph from "public/empty-state/empty_graph.svg";
|
||||
import { LineGraph } from "components/ui";
|
||||
// types
|
||||
import { IDefaultAnalyticsResponse } from "types";
|
||||
// constants
|
||||
@@ -50,13 +48,7 @@ export const AnalyticsYearWiseIssues: React.FC<Props> = ({ defaultAnalytics }) =
|
||||
enableArea
|
||||
/>
|
||||
) : (
|
||||
<div className="px-7 py-4">
|
||||
<ProfileEmptyState
|
||||
title="No Data yet"
|
||||
description="Close issues to view analysis of the same in the form of a graph."
|
||||
image={emptyGraph}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-custom-text-200 text-center text-sm py-8">No matching data found.</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user